feat(isa-app): migrate to standalone component architecture

- Replace NgModule bootstrap with bootstrapApplication and ApplicationConfig
- Convert app.module.ts to app.config.ts with provider functions
- Convert routing module to standalone routes array
- Remove domain NgModules (services now providedIn: 'root')
- Remove NgRx application store in favor of signals
- Update icon components and registries for modern patterns
- Update page components for standalone compatibility
This commit is contained in:
Lorenz Hilpert
2025-12-03 14:16:47 +01:00
parent abcb8e2cb4
commit 803a53253c
73 changed files with 3375 additions and 4164 deletions

View File

@@ -1,19 +1,22 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { DevScanAdapter } from './dev.scan-adapter'; import { DevScanAdapter } from './dev.scan-adapter';
import { NativeScanAdapter } from './native.scan-adapter'; import { NativeScanAdapter } from './native.scan-adapter';
import { SCAN_ADAPTER } from './tokens'; import { SCAN_ADAPTER } from './tokens';
@NgModule({}) /**
export class ScanAdapterModule { * @deprecated Use '@isa/shared/scanner' instead.
static forRoot() { */
return { @NgModule({})
ngModule: ScanAdapterModule, export class ScanAdapterModule {
providers: [ static forRoot() {
{ provide: SCAN_ADAPTER, useClass: NativeScanAdapter, multi: true }, return {
{ provide: SCAN_ADAPTER, useClass: DevScanAdapter, multi: true }, ngModule: ScanAdapterModule,
], providers: [
// Use for testing: { provide: SCAN_ADAPTER, useClass: NativeScanAdapter, multi: true },
// providers: [{ provide: SCAN_ADAPTER, useClass: dev ? DevScanAdapter : NativeScanAdapter, multi: true }], { provide: SCAN_ADAPTER, useClass: DevScanAdapter, multi: true },
}; ],
} // Use for testing:
} // providers: [{ provide: SCAN_ADAPTER, useClass: dev ? DevScanAdapter : NativeScanAdapter, multi: true }],
};
}
}

View File

@@ -1,20 +1,25 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ScanditOverlayComponent } from './scandit-overlay.component'; import { ScanditOverlayComponent } from './scandit-overlay.component';
import { ScanditScanAdapter } from './scandit.scan-adapter'; import { ScanditScanAdapter } from './scandit.scan-adapter';
import { SCAN_ADAPTER } from '../tokens'; import { SCAN_ADAPTER } from '../tokens';
@NgModule({ /**
imports: [CommonModule], * @deprecated Use @isa/shared/scanner instead.
exports: [ScanditOverlayComponent], */
declarations: [ScanditOverlayComponent], @NgModule({
}) imports: [CommonModule],
export class ScanditScanAdapterModule { exports: [ScanditOverlayComponent],
static forRoot() { declarations: [ScanditOverlayComponent],
return { })
ngModule: ScanditScanAdapterModule, export class ScanditScanAdapterModule {
providers: [{ provide: SCAN_ADAPTER, useClass: ScanditScanAdapter, multi: true }], static forRoot() {
}; return {
} ngModule: ScanditScanAdapterModule,
} providers: [
{ provide: SCAN_ADAPTER, useClass: ScanditScanAdapter, multi: true },
],
};
}
}

View File

@@ -1,19 +0,0 @@
import { NgModule } from '@angular/core';
import { DomainAvailabilityModule } from '@domain/availability';
import { DomainCatalogModule } from '@domain/catalog';
import { DomainIsaModule } from '@domain/isa';
import { DomainCheckoutModule } from '@domain/checkout';
import { DomainOmsModule } from '@domain/oms';
import { DomainRemissionModule } from '@domain/remission';
@NgModule({
imports: [
DomainIsaModule.forRoot(),
DomainCatalogModule.forRoot(),
DomainAvailabilityModule.forRoot(),
DomainCheckoutModule.forRoot(),
DomainOmsModule.forRoot(),
DomainRemissionModule.forRoot(),
],
})
export class AppDomainModule {}

View File

@@ -1,34 +0,0 @@
import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { ActionReducer, MetaReducer, StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';
import { rootReducer } from './store/root.reducer';
import { RootState } from './store/root.state';
export function storeInLocalStorage(
reducer: ActionReducer<any>,
): ActionReducer<any> {
return function (state, action) {
if (action.type === 'HYDRATE') {
return reducer(action['payload'], action);
}
return reducer(state, action);
};
}
export const metaReducers: MetaReducer<RootState>[] = !environment.production
? [storeInLocalStorage]
: [storeInLocalStorage];
@NgModule({
imports: [
StoreModule.forRoot(rootReducer, { metaReducers }),
EffectsModule.forRoot([]),
StoreDevtoolsModule.instrument({
name: 'ISA Ngrx Application Store',
connectInZone: true,
}),
],
})
export class AppStoreModule {}

View File

@@ -1,40 +0,0 @@
import { HttpInterceptorFn, provideHttpClient, withInterceptors } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { Config } from '@core/config';
import { AvConfiguration } from '@generated/swagger/availability-api';
import { CatConfiguration } from '@generated/swagger/cat-search-api';
import { CheckoutConfiguration } from '@generated/swagger/checkout-api';
import { CrmConfiguration } from '@generated/swagger/crm-api';
import { EisConfiguration } from '@generated/swagger/eis-api';
import { IsaConfiguration } from '@generated/swagger/isa-api';
import { OmsConfiguration } from '@generated/swagger/oms-api';
import { PrintConfiguration } from '@generated/swagger/print-api';
import { RemiConfiguration } from '@generated/swagger/inventory-api';
import { WwsConfiguration } from '@generated/swagger/wws-api';
export function createConfigurationFactory(name: string) {
return function (config: Config): { rootUrl: string } {
return config.get(`@swagger/${name}`);
};
}
const serviceWorkerInterceptor: HttpInterceptorFn = (req, next) => {
return next(req.clone({ setHeaders: { 'ngsw-bypass': 'true' } }));
};
@NgModule({
providers: [
provideHttpClient(withInterceptors([serviceWorkerInterceptor])),
{ provide: AvConfiguration, useFactory: createConfigurationFactory('av'), deps: [Config] },
{ provide: CatConfiguration, useFactory: createConfigurationFactory('cat'), deps: [Config] },
{ provide: CheckoutConfiguration, useFactory: createConfigurationFactory('checkout'), deps: [Config] },
{ provide: CrmConfiguration, useFactory: createConfigurationFactory('crm'), deps: [Config] },
{ provide: EisConfiguration, useFactory: createConfigurationFactory('eis'), deps: [Config] },
{ provide: IsaConfiguration, useFactory: createConfigurationFactory('isa'), deps: [Config] },
{ provide: OmsConfiguration, useFactory: createConfigurationFactory('oms'), deps: [Config] },
{ provide: PrintConfiguration, useFactory: createConfigurationFactory('print'), deps: [Config] },
{ provide: RemiConfiguration, useFactory: createConfigurationFactory('remi'), deps: [Config] },
{ provide: WwsConfiguration, useFactory: createConfigurationFactory('wws'), deps: [Config] },
],
})
export class AppSwaggerModule {}

View File

@@ -1,28 +1,28 @@
@if ($offlineBannerVisible()) { <!-- @if ($offlineBannerVisible()) {
<div [@fadeInOut] class="bg-brand text-white text-center fixed inset-x-0 top-0 z-tooltip p-4"> <div [@fadeInOut] class="bg-brand text-white text-center fixed inset-x-0 top-0 z-tooltip p-4">
<h3 class="font-bold grid grid-flow-col items-center justify-center text-xl gap-4"> <h3 class="font-bold grid grid-flow-col items-center justify-center text-xl gap-4">
<div> <div>
<ng-icon name="matWifiOff"></ng-icon> <ng-icon name="matWifiOff"></ng-icon>
</div> </div>
<div>Sie sind offline, keine Verbindung zum Netzwerk.</div> <div>Sie sind offline, keine Verbindung zum Netzwerk.</div>
</h3> </h3>
<p>Bereits geladene Ihnalte werden angezeigt, Interaktionen sind aktuell nicht möglich.</p> <p>Bereits geladene Ihnalte werden angezeigt, Interaktionen sind aktuell nicht möglich.</p>
</div> </div>
} }
@if ($onlineBannerVisible()) { @if ($onlineBannerVisible()) {
<div [@fadeInOut] class="bg-green-500 text-white text-center fixed inset-x-0 top-0 z-tooltip p-4"> <div [@fadeInOut] class="bg-green-500 text-white text-center fixed inset-x-0 top-0 z-tooltip p-4">
<h3 class="font-bold grid grid-flow-col items-center justify-center text-xl gap-4"> <h3 class="font-bold grid grid-flow-col items-center justify-center text-xl gap-4">
<div> <div>
<ng-icon name="matWifi"></ng-icon> <ng-icon name="matWifi"></ng-icon>
</div> </div>
<div>Sie sind wieder online.</div> <div>Sie sind wieder online.</div>
</h3> </h3>
<button class="fixed top-2 right-4 text-3xl w-12 h-12" type="button" (click)="$onlineBannerVisible.set(false)"> <button class="fixed top-2 right-4 text-3xl w-12 h-12" type="button" (click)="$onlineBannerVisible.set(false)">
<ng-icon name="matClose"></ng-icon> <ng-icon name="matClose"></ng-icon>
</button> </button>
</div> </div>
} }
<router-outlet></router-outlet> <router-outlet></router-outlet> -->

View File

@@ -1,3 +0,0 @@
:host {
@apply block;
}

View File

@@ -1,137 +0,0 @@
import { Spectator, createComponentFactory, SpyObject, createSpyObject } from '@ngneat/spectator';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import { Config } from '@core/config';
import { ApplicationService } from '@core/application';
import { of } from 'rxjs';
import { Renderer2 } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SwUpdate } from '@angular/service-worker';
import { NotificationsHub } from '@hub/notifications';
import { UserStateService } from '@generated/swagger/isa-api';
import { UiModalService } from '@ui/modal';
import { AuthService } from '@core/auth';
describe('AppComponent', () => {
let spectator: Spectator<AppComponent>;
let config: SpyObject<Config>;
let renderer: SpyObject<Renderer2>;
let applicationServiceMock: SpyObject<ApplicationService>;
let notificationsHubMock: SpyObject<NotificationsHub>;
let swUpdateMock: SpyObject<SwUpdate>;
const createComponent = createComponentFactory({
component: AppComponent,
imports: [CommonModule, RouterTestingModule],
providers: [],
mocks: [Config, SwUpdate, UserStateService, UiModalService, AuthService],
});
beforeEach(() => {
applicationServiceMock = createSpyObject(ApplicationService);
applicationServiceMock.getSection$.and.returnValue(of('customer'));
applicationServiceMock.getActivatedProcessId$.and.returnValue(of(undefined));
renderer = jasmine.createSpyObj('Renderer2', ['addClass', 'removeClass']);
notificationsHubMock = createSpyObject(NotificationsHub);
notificationsHubMock.notifications$ = of({});
swUpdateMock = createSpyObject(SwUpdate);
spectator = createComponent({
providers: [
{ provide: ApplicationService, useValue: applicationServiceMock },
{
provide: Renderer2,
useValue: renderer,
},
{ provide: NotificationsHub, useValue: notificationsHubMock },
{ provide: SwUpdate, useValue: swUpdateMock },
],
});
config = spectator.inject(Config);
});
it('should create the app', () => {
expect(spectator.component).toBeTruthy();
});
it('should have a router outlet', () => {
expect(spectator.query('router-outlet')).toExist();
});
describe('ngOnInit', () => {
it('should call setTitle', () => {
const spy = spyOn(spectator.component, 'setTitle');
spectator.component.ngOnInit();
expect(spy).toHaveBeenCalled();
});
it('should call logVersion', () => {
const spy = spyOn(spectator.component, 'logVersion');
spectator.component.ngOnInit();
expect(spy).toHaveBeenCalled();
});
});
describe('setTitle', () => {
it('should call Title.setTitle()', () => {
const spyTitleSetTitle = spyOn(spectator.component['_title'], 'setTitle');
config.get.and.returnValue('test');
spectator.component.setTitle();
expect(spyTitleSetTitle).toHaveBeenCalledWith('test');
});
});
describe('logVersion', () => {
it('should call console.log()', () => {
const spyConsoleLog = spyOn(console, 'log');
config.get.and.returnValue('test');
spectator.component.logVersion();
expect(spyConsoleLog).toHaveBeenCalled();
});
});
// --------------------------------------------------------
// Unit Tests Implementation for Angular Version 13.x.x
// describe('updateClient()', () => {
// it('should call checkForUpdate() if SwUpdate.isEnabled is True', () => {
// spyOn(spectator.component, 'checkForUpdate');
// spyOn(spectator.component, 'initialCheckForUpdate');
// (swUpdateMock as any).isEnabled = true;
// spectator.component.updateClient();
// expect(spectator.component.initialCheckForUpdate).toHaveBeenCalled();
// expect(spectator.component.checkForUpdate).toHaveBeenCalled();
// });
// it('should not call checkForUpdate() if SwUpdate.isEnabled is False', () => {
// spyOn(spectator.component, 'checkForUpdate');
// spyOn(spectator.component, 'initialCheckForUpdate');
// (swUpdateMock as any).isEnabled = false;
// spectator.component.updateClient();
// expect(spectator.component.initialCheckForUpdate).not.toHaveBeenCalled();
// expect(spectator.component.checkForUpdate).not.toHaveBeenCalled();
// });
// });
// describe('checkForUpdate', () => {
// it('should call swUpdate.checkForUpdate() and notifications.updateNotification() every second', fakeAsync(() => {
// swUpdateMock.checkForUpdate.and.returnValue(Promise.resolve());
// spectator.component.checkForUpdates = 1000;
// spectator.component.checkForUpdate();
// spectator.detectChanges();
// tick(1100);
// expect(notificationsHubMock.updateNotification).toHaveBeenCalled();
// discardPeriodicTasks();
// }));
// });
// describe('initialCheckForUpdate', () => {
// it('should call swUpdate.checkForUpdate()', () => {
// swUpdateMock.checkForUpdate.and.returnValue(new Promise(undefined));
// spectator.component.initialCheckForUpdate();
// expect(swUpdateMock.checkForUpdate).toHaveBeenCalled();
// });
// });
});

View File

@@ -1,206 +1,205 @@
// import {
import { // Component,
Component, // effect,
effect, // HostListener,
HostListener, // inject,
inject, // Inject,
Inject, // Injector,
Injector, // OnInit,
OnInit, // Renderer2,
Renderer2, // signal,
signal, // untracked,
untracked, // DOCUMENT
DOCUMENT // } from '@angular/core';
} from '@angular/core'; // import { Title } from '@angular/platform-browser';
import { Title } from '@angular/platform-browser'; // import { SwUpdate } from '@angular/service-worker';
import { SwUpdate } from '@angular/service-worker'; // import { ApplicationService } from '@core/application';
import { ApplicationService } from '@core/application'; // import { Config } from '@core/config';
import { Config } from '@core/config'; // import { NotificationsHub } from '@hub/notifications';
import { NotificationsHub } from '@hub/notifications'; // import packageInfo from 'packageJson';
import packageInfo from 'packageJson'; // import { asapScheduler, interval, Subscription } from 'rxjs';
import { asapScheduler, interval, Subscription } from 'rxjs'; // import { UserStateService } from '@generated/swagger/isa-api';
import { UserStateService } from '@generated/swagger/isa-api'; // import { IsaLogProvider } from './providers';
import { IsaLogProvider } from './providers'; // import { EnvironmentService } from '@core/environment';
import { EnvironmentService } from '@core/environment'; // import { AuthService, LoginStrategy } from '@core/auth';
import { AuthService, LoginStrategy } from '@core/auth'; // import { UiMessageModalComponent, UiModalService } from '@ui/modal';
import { UiMessageModalComponent, UiModalService } from '@ui/modal'; // import { injectNetworkStatus } from '@isa/core/connectivity';
import { injectNetworkStatus } from '@isa/core/connectivity'; // import { animate, style, transition, trigger } from '@angular/animations';
import { animate, style, transition, trigger } from '@angular/animations';
// @Component({
@Component({ // selector: 'app-root',
selector: 'app-root', // templateUrl: './app.component.html',
templateUrl: './app.component.html', // styleUrls: ['./app.component.scss'],
styleUrls: ['./app.component.scss'], // animations: [
animations: [ // trigger('fadeInOut', [
trigger('fadeInOut', [ // transition(':enter', [
transition(':enter', [ // // :enter wird ausgelöst, wenn das Element zum DOM hinzugefügt wird
// :enter wird ausgelöst, wenn das Element zum DOM hinzugefügt wird // style({ opacity: 0, transform: 'translateY(-100%)' }),
style({ opacity: 0, transform: 'translateY(-100%)' }), // animate('300ms', style({ opacity: 1, transform: 'translateY(0)' })),
animate('300ms', style({ opacity: 1, transform: 'translateY(0)' })), // ]),
]), // transition(':leave', [
transition(':leave', [ // // :leave wird ausgelöst, wenn das Element aus dem DOM entfernt wird
// :leave wird ausgelöst, wenn das Element aus dem DOM entfernt wird // animate('300ms', style({ opacity: 0, transform: 'translateY(-100%)' })),
animate('300ms', style({ opacity: 0, transform: 'translateY(-100%)' })), // ]),
]), // ]),
]), // ],
], // standalone: false,
standalone: false, // })
}) // export class AppComponent implements OnInit {
export class AppComponent implements OnInit { // readonly injector = inject(Injector);
readonly injector = inject(Injector);
// $networkStatus = injectNetworkStatus();
$networkStatus = injectNetworkStatus();
// $offlineBannerVisible = signal(false);
$offlineBannerVisible = signal(false);
// $onlineBannerVisible = signal(false);
$onlineBannerVisible = signal(false);
// private onlineBannerDismissTimeout: any;
private onlineBannerDismissTimeout: any;
// onlineEffects = effect(() => {
onlineEffects = effect(() => { // const status = this.$networkStatus();
const status = this.$networkStatus(); // const online = status === 'online';
const online = status === 'online'; // const offlineBannerVisible = this.$offlineBannerVisible();
const offlineBannerVisible = this.$offlineBannerVisible();
// untracked(() => {
untracked(() => { // this.$offlineBannerVisible.set(!online);
this.$offlineBannerVisible.set(!online);
// if (!online) {
if (!online) { // this.$onlineBannerVisible.set(false);
this.$onlineBannerVisible.set(false); // clearTimeout(this.onlineBannerDismissTimeout);
clearTimeout(this.onlineBannerDismissTimeout); // }
}
// if (offlineBannerVisible && online) {
if (offlineBannerVisible && online) { // this.$onlineBannerVisible.set(true);
this.$onlineBannerVisible.set(true); // this.onlineBannerDismissTimeout = setTimeout(() => this.$onlineBannerVisible.set(false), 5000);
this.onlineBannerDismissTimeout = setTimeout(() => this.$onlineBannerVisible.set(false), 5000); // }
} // });
}); // });
});
// private _checkForUpdates: number = this._config.get('checkForUpdates');
private _checkForUpdates: number = this._config.get('checkForUpdates');
// get checkForUpdates(): number {
get checkForUpdates(): number { // return this._checkForUpdates ?? 60 * 60 * 1000; // default 1 hour
return this._checkForUpdates ?? 60 * 60 * 1000; // default 1 hour // }
}
// // For Unit Testing
// For Unit Testing // set checkForUpdates(time: number) {
set checkForUpdates(time: number) { // this._checkForUpdates = time;
this._checkForUpdates = time; // }
}
// subscriptions = new Subscription();
subscriptions = new Subscription();
// constructor(
constructor( // private readonly _config: Config,
private readonly _config: Config, // private readonly _title: Title,
private readonly _title: Title, // private readonly _appService: ApplicationService,
private readonly _appService: ApplicationService, // @Inject(DOCUMENT) private readonly _document: Document,
@Inject(DOCUMENT) private readonly _document: Document, // private readonly _renderer: Renderer2,
private readonly _renderer: Renderer2, // private readonly _swUpdate: SwUpdate,
private readonly _swUpdate: SwUpdate, // private readonly _notifications: NotificationsHub,
private readonly _notifications: NotificationsHub, // private infoService: UserStateService,
private infoService: UserStateService, // private readonly _environment: EnvironmentService,
private readonly _environment: EnvironmentService, // private readonly _authService: AuthService,
private readonly _authService: AuthService, // private readonly _modal: UiModalService,
private readonly _modal: UiModalService, // ) {
) { // this.updateClient();
this.updateClient(); // IsaLogProvider.InfoService = this.infoService;
IsaLogProvider.InfoService = this.infoService; // }
}
// ngOnInit() {
ngOnInit() { // this.setTitle();
this.setTitle(); // this.logVersion();
this.logVersion(); // asapScheduler.schedule(() => this.determinePlatform(), 250);
asapScheduler.schedule(() => this.determinePlatform(), 250); // this._appService.getSection$().subscribe(this.sectionChangeHandler.bind(this));
this._appService.getSection$().subscribe(this.sectionChangeHandler.bind(this));
// this.setupSilentRefresh();
this.setupSilentRefresh(); // }
}
// // Setup interval for silent refresh
// Setup interval for silent refresh // setupSilentRefresh() {
setupSilentRefresh() { // const silentRefreshInterval = this._config.get('silentRefresh.interval');
const silentRefreshInterval = this._config.get('silentRefresh.interval'); // if (silentRefreshInterval > 0) {
if (silentRefreshInterval > 0) { // interval(silentRefreshInterval).subscribe(() => {
interval(silentRefreshInterval).subscribe(() => { // if (this._authService.isAuthenticated()) {
if (this._authService.isAuthenticated()) { // this._authService.refresh();
this._authService.refresh(); // }
} // });
}); // }
} // }
}
// setTitle() {
setTitle() { // this._title.setTitle(this._config.get('title'));
this._title.setTitle(this._config.get('title')); // }
}
// logVersion() {
logVersion() { // console.log(
console.log( // `%c${this._config.get('title')}\r\nVersion: ${packageInfo.version}`,
`%c${this._config.get('title')}\r\nVersion: ${packageInfo.version}`, // 'font-weight: bold; font-size: 20px;',
'font-weight: bold; font-size: 20px;', // );
); // }
}
// determinePlatform() {
determinePlatform() { // if (this._environment.isNative()) {
if (this._environment.isNative()) { // this._renderer.addClass(this._document.body, 'tablet-native');
this._renderer.addClass(this._document.body, 'tablet-native'); // } else if (this._environment.isTablet()) {
} else if (this._environment.isTablet()) { // this._renderer.addClass(this._document.body, 'tablet-browser');
this._renderer.addClass(this._document.body, 'tablet-browser'); // }
} // if (this._environment.isTablet()) {
if (this._environment.isTablet()) { // this._renderer.addClass(this._document.body, 'tablet');
this._renderer.addClass(this._document.body, 'tablet'); // }
} // if (this._environment.isDesktop()) {
if (this._environment.isDesktop()) { // this._renderer.addClass(this._document.body, 'desktop');
this._renderer.addClass(this._document.body, 'desktop'); // }
} // }
}
// sectionChangeHandler(section: string) {
sectionChangeHandler(section: string) { // if (section === 'customer') {
if (section === 'customer') { // this._renderer.removeClass(this._document.body, 'branch');
this._renderer.removeClass(this._document.body, 'branch'); // this._renderer.addClass(this._document.body, 'customer');
this._renderer.addClass(this._document.body, 'customer'); // } else if (section === 'branch') {
} else if (section === 'branch') { // this._renderer.removeClass(this._document.body, 'customer');
this._renderer.removeClass(this._document.body, 'customer'); // this._renderer.addClass(this._document.body, 'branch');
this._renderer.addClass(this._document.body, 'branch'); // }
} // }
}
// updateClient() {
updateClient() { // if (!this._swUpdate.isEnabled) {
if (!this._swUpdate.isEnabled) { // return;
return; // }
}
// this.initialCheckForUpdate();
this.initialCheckForUpdate(); // this.checkForUpdate();
this.checkForUpdate(); // }
}
// checkForUpdate() {
checkForUpdate() { // interval(this._checkForUpdates).subscribe(() => {
interval(this._checkForUpdates).subscribe(() => { // this._swUpdate.checkForUpdate().then((value) => {
this._swUpdate.checkForUpdate().then((value) => { // console.log('check for update', value);
console.log('check for update', value); // if (value) {
if (value) { // this._notifications.updateNotification();
this._notifications.updateNotification(); // }
} // });
}); // });
}); // }
}
// initialCheckForUpdate() {
initialCheckForUpdate() { // this._swUpdate.checkForUpdate().then((value) => {
this._swUpdate.checkForUpdate().then((value) => { // console.log('initial check for update', value);
console.log('initial check for update', value); // if (value) {
if (value) { // location.reload();
location.reload(); // }
} // });
}); // }
}
// @HostListener('window:visibilitychange', ['$event'])
@HostListener('window:visibilitychange', ['$event']) // onVisibilityChange(event: Event) {
onVisibilityChange(event: Event) { // // refresh token when app is in background
// refresh token when app is in background // if (this._document.hidden && this._authService.isAuthenticated()) {
if (this._document.hidden && this._authService.isAuthenticated()) { // this._authService.refresh();
this._authService.refresh(); // } else if (!this._authService.isAuthenticated()) {
} else if (!this._authService.isAuthenticated()) { // const strategy = this.injector.get(LoginStrategy);
const strategy = this.injector.get(LoginStrategy);
// return strategy.login('Sie sind nicht mehr angemeldet');
return strategy.login('Sie sind nicht mehr angemeldet'); // }
} // }
} // }
}

View File

@@ -2,51 +2,51 @@ import { version } from '../../../../package.json';
import { IsaTitleStrategy } from '@isa/common/title-management'; import { IsaTitleStrategy } from '@isa/common/title-management';
import { import {
HTTP_INTERCEPTORS, HTTP_INTERCEPTORS,
HttpInterceptorFn,
provideHttpClient, provideHttpClient,
withInterceptors,
withInterceptorsFromDi, withInterceptorsFromDi,
} from '@angular/common/http'; } from '@angular/common/http';
import { import {
ApplicationConfig,
DEFAULT_CURRENCY_CODE, DEFAULT_CURRENCY_CODE,
ErrorHandler, ErrorHandler,
importProvidersFrom,
Injector, Injector,
LOCALE_ID, LOCALE_ID,
NgModule,
inject, inject,
provideAppInitializer, provideAppInitializer,
provideZoneChangeDetection,
signal, signal,
isDevMode,
} from '@angular/core'; } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import {
import { PlatformModule } from '@angular/cdk/platform'; provideRouter,
TitleStrategy,
withComponentInputBinding,
} from '@angular/router';
import { ActionReducer, MetaReducer, provideStore } from '@ngrx/store';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { Config } from '@core/config'; import { Config } from '@core/config';
import { AuthModule, AuthService, LoginStrategy } from '@core/auth'; import { AuthModule, AuthService, LoginStrategy } from '@core/auth';
import { CoreCommandModule } from '@core/command'; import { CoreCommandModule } from '@core/command';
import { AppRoutingModule } from './app-routing.module'; import { routes } from './app.routes';
import { AppComponent } from './app.component';
import { import { rootReducer } from './store/root.reducer';
ApplicationService, import { RootState } from './store/root.state';
ApplicationServiceAdapter,
CoreApplicationModule,
} from '@core/application';
import { AppStoreModule } from './app-store.module';
import { ServiceWorkerModule } from '@angular/service-worker'; import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { AppSwaggerModule } from './app-swagger.module';
import { AppDomainModule } from './app-domain.module';
import { UiModalModule } from '@ui/modal'; import { UiModalModule } from '@ui/modal';
import { import {
NotificationsHubModule, NotificationsHubModule,
NOTIFICATIONS_HUB_OPTIONS, NOTIFICATIONS_HUB_OPTIONS,
} from '@hub/notifications'; } from '@hub/notifications';
import { SignalRHubOptions } from '@core/signalr'; import { SignalRHubOptions } from '@core/signalr';
import { CoreBreadcrumbModule } from '@core/breadcrumb'; import { provideCoreBreadcrumb } from '@core/breadcrumb';
import { UiCommonModule } from '@ui/common'; import { UiCommonModule } from '@ui/common';
import { registerLocaleData } from '@angular/common';
import localeDe from '@angular/common/locales/de';
import localeDeExtra from '@angular/common/locales/extra/de';
import { HttpErrorInterceptor } from './interceptors'; import { HttpErrorInterceptor } from './interceptors';
import { CoreLoggerModule, LOG_PROVIDER } from '@core/logger'; import { CoreLoggerModule, LOG_PROVIDER } from '@core/logger';
import { IsaLogProvider } from './providers'; import { IsaLogProvider } from './providers';
@@ -59,7 +59,6 @@ import {
import * as Commands from './commands'; import * as Commands from './commands';
import { NativeContainerService } from '@external/native-container'; import { NativeContainerService } from '@external/native-container';
import { ShellModule } from '@shared/shell'; import { ShellModule } from '@shared/shell';
import { MainComponent } from './main.component';
import { IconModule } from '@shared/components/icon'; import { IconModule } from '@shared/components/icon';
import { NgIconsModule } from '@ng-icons/core'; import { NgIconsModule } from '@ng-icons/core';
import { import {
@@ -69,8 +68,7 @@ import {
} from '@ng-icons/material-icons/baseline'; } from '@ng-icons/material-icons/baseline';
import { NetworkStatusService } from '@isa/core/connectivity'; import { NetworkStatusService } from '@isa/core/connectivity';
import { debounceTime, filter, firstValueFrom, switchMap } from 'rxjs'; import { debounceTime, filter, firstValueFrom, switchMap } from 'rxjs';
import { provideMatomo } from 'ngx-matomo-client'; import { provideMatomo, withRouter, withRouteData } from 'ngx-matomo-client';
import { withRouter, withRouteData } from 'ngx-matomo-client';
import { import {
provideLogging, provideLogging,
withLogLevel, withLogLevel,
@@ -87,15 +85,58 @@ import {
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { OAuthService } from 'angular-oauth2-oidc'; import { OAuthService } from 'angular-oauth2-oidc';
import z from 'zod'; import z from 'zod';
import { TitleStrategy } from '@angular/router'; import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
import { TabNavigationService } from '@isa/core/tabs'; import { TabNavigationService } from '@isa/core/tabs';
registerLocaleData(localeDe, localeDeExtra); // Domain modules
registerLocaleData(localeDe, 'de', localeDeExtra); import { provideDomainCheckout } from '@domain/checkout';
export function _appInitializerFactory(config: Config, injector: Injector) { // Swagger API configurations
import { AvConfiguration } from '@generated/swagger/availability-api';
import { CatConfiguration } from '@generated/swagger/cat-search-api';
import { CheckoutConfiguration } from '@generated/swagger/checkout-api';
import { CrmConfiguration } from '@generated/swagger/crm-api';
import { EisConfiguration } from '@generated/swagger/eis-api';
import { IsaConfiguration } from '@generated/swagger/isa-api';
import { OmsConfiguration } from '@generated/swagger/oms-api';
import { PrintConfiguration } from '@generated/swagger/print-api';
import { RemiConfiguration } from '@generated/swagger/inventory-api';
import { WwsConfiguration } from '@generated/swagger/wws-api';
import { UiIconModule } from '@ui/icon';
// --- Store Configuration ---
function storeHydrateMetaReducer(
reducer: ActionReducer<RootState>,
): ActionReducer<RootState> {
return function (state, action) {
if (action.type === 'HYDRATE') {
return reducer(action['payload'], action);
}
return reducer(state, action);
};
}
const metaReducers: MetaReducer<RootState>[] = [storeHydrateMetaReducer];
// --- Swagger Configuration ---
const swaggerConfigSchema = z.object({ rootUrl: z.string() });
function createSwaggerConfigFactory(name: string) {
return function () {
return inject(Config).get(`@swagger/${name}`, swaggerConfigSchema);
};
}
const serviceWorkerBypassInterceptor: HttpInterceptorFn = (req, next) => {
return next(req.clone({ setHeaders: { 'ngsw-bypass': 'true' } }));
};
// --- App Initializer ---
function appInitializerFactory(_config: Config, injector: Injector) {
return async () => { return async () => {
// Get logging service for initialization logging
const logger = loggerFactory(() => ({ service: 'AppInitializer' })); const logger = loggerFactory(() => ({ service: 'AppInitializer' }));
const statusElement = document.querySelector('#init-status'); const statusElement = document.querySelector('#init-status');
const laoderElement = document.querySelector('#init-loader'); const laoderElement = document.querySelector('#init-loader');
@@ -162,7 +203,6 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
await userStorage.init(); await userStorage.init();
const store = injector.get(Store); const store = injector.get(Store);
// Hydrate Ngrx Store
const state = userStorage.get('store'); const state = userStorage.get('store');
if (state && state['version'] === version) { if (state && state['version'] === version) {
store.dispatch({ type: 'HYDRATE', payload: userStorage.get('store') }); store.dispatch({ type: 'HYDRATE', payload: userStorage.get('store') });
@@ -172,7 +212,7 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
reason: state ? 'version mismatch' : 'no stored state', reason: state ? 'version mismatch' : 'no stored state',
})); }));
} }
// Subscribe on Store changes and save to user storage
auth.initialized$ auth.initialized$
.pipe( .pipe(
filter((initialized) => initialized), filter((initialized) => initialized),
@@ -183,7 +223,6 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
}); });
logger.info('Application initialization completed'); logger.info('Application initialization completed');
// Inject tab navigation service to initialize it
injector.get(TabNavigationService).init(); injector.get(TabNavigationService).init();
} catch (error) { } catch (error) {
logger.error('Application initialization failed', error as Error, () => ({ logger.error('Application initialization failed', error as Error, () => ({
@@ -224,7 +263,7 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
}; };
} }
export function _notificationsHubOptionsFactory( function notificationsHubOptionsFactory(
config: Config, config: Config,
auth: AuthService, auth: AuthService,
): SignalRHubOptions { ): SignalRHubOptions {
@@ -258,80 +297,151 @@ const USER_SUB_FACTORY = () => {
return signal(validation.data); return signal(validation.data);
}; };
@NgModule({ export const appConfig: ApplicationConfig = {
declarations: [AppComponent, MainComponent],
bootstrap: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
ShellModule.forRoot(),
AppRoutingModule,
AppSwaggerModule,
AppDomainModule,
CoreBreadcrumbModule.forRoot(),
CoreCommandModule.forRoot(Object.values(Commands)),
CoreLoggerModule.forRoot(),
AppStoreModule,
AuthModule.forRoot(),
CoreApplicationModule.forRoot(),
UiModalModule.forRoot(),
UiCommonModule.forRoot(),
NotificationsHubModule.forRoot(),
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.production,
registrationStrategy: 'registerWhenStable:30000',
}),
ScanAdapterModule.forRoot(),
ScanditScanAdapterModule.forRoot(),
PlatformModule,
IconModule.forRoot(),
NgIconsModule.withIcons({ matWifiOff, matClose, matWifi }),
],
providers: [ providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideAnimationsAsync('animations'),
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(
withInterceptorsFromDi(),
withInterceptors([serviceWorkerBypassInterceptor]),
),
provideScrollPositionRestoration(),
// NgRx Store
provideStore(rootReducer, { metaReducers }),
provideCoreBreadcrumb(),
provideDomainCheckout(),
provideStoreDevtools({
name: 'ISA Ngrx Application Store',
connectInZone: true,
}),
// Swagger API configurations
{
provide: AvConfiguration,
useFactory: createSwaggerConfigFactory('av'),
},
{
provide: CatConfiguration,
useFactory: createSwaggerConfigFactory('cat'),
},
{
provide: CheckoutConfiguration,
useFactory: createSwaggerConfigFactory('checkout'),
},
{
provide: CrmConfiguration,
useFactory: createSwaggerConfigFactory('crm'),
},
{
provide: EisConfiguration,
useFactory: createSwaggerConfigFactory('eis'),
},
{
provide: IsaConfiguration,
useFactory: createSwaggerConfigFactory('isa'),
},
{
provide: OmsConfiguration,
useFactory: createSwaggerConfigFactory('oms'),
},
{
provide: PrintConfiguration,
useFactory: createSwaggerConfigFactory('print'),
},
{
provide: RemiConfiguration,
useFactory: createSwaggerConfigFactory('remi'),
},
{
provide: WwsConfiguration,
useFactory: createSwaggerConfigFactory('wws'),
},
// App initializer
provideAppInitializer(() => { provideAppInitializer(() => {
const initializerFn = _appInitializerFactory( const initializerFn = appInitializerFactory(
inject(Config), inject(Config),
inject(Injector), inject(Injector),
); );
return initializerFn(); return initializerFn();
}), }),
// Notifications hub
{ {
provide: NOTIFICATIONS_HUB_OPTIONS, provide: NOTIFICATIONS_HUB_OPTIONS,
useFactory: _notificationsHubOptionsFactory, useFactory: notificationsHubOptionsFactory,
deps: [Config, AuthService], deps: [Config, AuthService],
}, },
// HTTP interceptors
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
useClass: HttpErrorInterceptor, useClass: HttpErrorInterceptor,
multi: true, multi: true,
}, },
// Logging
{ {
provide: LOG_PROVIDER, provide: LOG_PROVIDER,
useClass: IsaLogProvider, useClass: IsaLogProvider,
multi: true, multi: true,
}, },
provideLogging(
withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Info),
withSink(ConsoleLogSink),
),
// Error handling
{ {
provide: ErrorHandler, provide: ErrorHandler,
useClass: IsaErrorHandler, useClass: IsaErrorHandler,
}, },
{
provide: ApplicationService, // Locale settings
useClass: ApplicationServiceAdapter,
},
{ provide: LOCALE_ID, useValue: 'de-DE' }, { provide: LOCALE_ID, useValue: 'de-DE' },
provideHttpClient(withInterceptorsFromDi()), { provide: DEFAULT_CURRENCY_CODE, useValue: 'EUR' },
// Analytics
provideMatomo( provideMatomo(
{ trackerUrl: 'https://matomo.paragon-data.net', siteId: '1' }, { trackerUrl: 'https://matomo.paragon-data.net', siteId: '1' },
withRouter(), withRouter(),
withRouteData(), withRouteData(),
), ),
provideLogging(withLogLevel(LogLevel.Debug), withSink(ConsoleLogSink)),
{ // User storage
provide: DEFAULT_CURRENCY_CODE,
useValue: 'EUR',
},
provideUserSubFactory(USER_SUB_FACTORY), provideUserSubFactory(USER_SUB_FACTORY),
// Title strategy
{ provide: TitleStrategy, useClass: IsaTitleStrategy }, { provide: TitleStrategy, useClass: IsaTitleStrategy },
// Import providers from NgModules
importProvidersFrom(
// Core modules
CoreCommandModule.forRoot(Object.values(Commands)),
CoreLoggerModule.forRoot(),
AuthModule.forRoot(),
// UI modules
UiModalModule.forRoot(),
UiCommonModule.forRoot(),
// Hub modules
NotificationsHubModule.forRoot(),
// Service Worker
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.production,
registrationStrategy: 'registerWhenStable:30000',
}),
// Scan adapter
ScanAdapterModule.forRoot(),
ScanditScanAdapterModule.forRoot(),
UiIconModule.forRoot(),
IconModule.forRoot(),
),
], ],
}) };
export class AppModule {}

View File

View File

@@ -0,0 +1 @@
<router-outlet />

View File

@@ -1,5 +1,4 @@
import { NgModule } from '@angular/core'; import { Routes } from '@angular/router';
import { RouterModule, Routes } from '@angular/router';
import { import {
CanActivateCartGuard, CanActivateCartGuard,
CanActivateCartWithProcessIdGuard, CanActivateCartWithProcessIdGuard,
@@ -11,13 +10,12 @@ import {
CanActivateProductWithProcessIdGuard, CanActivateProductWithProcessIdGuard,
IsAuthenticatedGuard, IsAuthenticatedGuard,
} from './guards'; } from './guards';
import { MainComponent } from './main.component';
import { import {
BranchSectionResolver, BranchSectionResolver,
CustomerSectionResolver, CustomerSectionResolver,
ProcessIdResolver, ProcessIdResolver,
} from './resolvers'; } from './resolvers';
import { TokenLoginComponent, TokenLoginModule } from './token-login'; import { TokenLoginComponent } from './token-login';
import { import {
ActivateProcessIdGuard, ActivateProcessIdGuard,
ActivateProcessIdWithConfigKeyGuard, ActivateProcessIdWithConfigKeyGuard,
@@ -28,9 +26,8 @@ import {
processResolverFn, processResolverFn,
hasTabIdGuard, hasTabIdGuard,
} from '@isa/core/tabs'; } from '@isa/core/tabs';
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
const routes: Routes = [ export const routes: Routes = [
{ path: '', redirectTo: 'kunde/dashboard', pathMatch: 'full' }, { path: '', redirectTo: 'kunde/dashboard', pathMatch: 'full' },
{ {
path: 'login', path: 'login',
@@ -45,7 +42,6 @@ const routes: Routes = [
children: [ children: [
{ {
path: 'kunde', path: 'kunde',
component: MainComponent,
children: [ children: [
{ {
path: 'dashboard', path: 'dashboard',
@@ -72,8 +68,6 @@ const routes: Routes = [
processId: ProcessIdResolver, processId: ProcessIdResolver,
}, },
}, },
// TODO: Check if order and :processId/order is still being used
// If not, remove these routes and the related guards and resolvers
{ {
path: 'order', path: 'order',
loadChildren: () => loadChildren: () =>
@@ -122,7 +116,6 @@ const routes: Routes = [
{ {
path: 'pickup-shelf', path: 'pickup-shelf',
canActivate: [ActivateProcessIdGuard], canActivate: [ActivateProcessIdGuard],
// NOTE: This is a workaround for the canActivate guard not being called
loadChildren: () => loadChildren: () =>
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule), import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
}, },
@@ -141,7 +134,6 @@ const routes: Routes = [
}, },
{ {
path: 'filiale', path: 'filiale',
component: MainComponent,
children: [ children: [
{ {
path: 'task-calendar', path: 'task-calendar',
@@ -154,7 +146,6 @@ const routes: Routes = [
{ {
path: 'pickup-shelf', path: 'pickup-shelf',
canActivate: [ActivateProcessIdWithConfigKeyGuard('pickupShelf')], canActivate: [ActivateProcessIdWithConfigKeyGuard('pickupShelf')],
// NOTE: This is a workaround for the canActivate guard not being called
loadChildren: () => loadChildren: () =>
import('@page/pickup-shelf').then((m) => m.PickupShelfInModule), import('@page/pickup-shelf').then((m) => m.PickupShelfInModule),
}, },
@@ -188,7 +179,6 @@ const routes: Routes = [
}, },
{ {
path: ':tabId', path: ':tabId',
component: MainComponent,
resolve: { process: processResolverFn, tab: tabResolverFn }, resolve: { process: processResolverFn, tab: tabResolverFn },
canActivate: [IsAuthenticatedGuard, hasTabIdGuard], canActivate: [IsAuthenticatedGuard, hasTabIdGuard],
children: [ children: [
@@ -218,7 +208,6 @@ const routes: Routes = [
}, },
], ],
}, },
{ {
path: 'return', path: 'return',
loadChildren: () => loadChildren: () =>
@@ -246,16 +235,3 @@ const routes: Routes = [
], ],
}, },
]; ];
@NgModule({
imports: [
RouterModule.forRoot(routes, {
bindToComponentInputs: true,
enableTracing: false,
}),
TokenLoginModule,
],
exports: [RouterModule],
providers: [provideScrollPositionRestoration()],
})
export class AppRoutingModule {}

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrls: ['./app.css'],
imports: [RouterOutlet],
})
export class App {}

View File

@@ -1,3 +0,0 @@
<shell-root>
<router-outlet></router-outlet>
</shell-root>

View File

@@ -1,11 +0,0 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-main',
templateUrl: 'main.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class MainComponent {
constructor() {}
}

View File

@@ -1,2 +1 @@
export * from './token-login.component'; export * from './token-login.component';
export * from './token-login.module';

View File

@@ -1,29 +1,31 @@
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { AuthService } from '@core/auth'; import { AuthService } from '@core/auth';
@Component({ @Component({
selector: 'app-token-login', selector: 'app-token-login',
templateUrl: 'token-login.component.html', templateUrl: 'token-login.component.html',
styleUrls: ['token-login.component.scss'], styleUrls: ['token-login.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false, })
}) export class TokenLoginComponent implements OnInit {
export class TokenLoginComponent implements OnInit { constructor(
constructor( private _route: ActivatedRoute,
private _route: ActivatedRoute, private _authService: AuthService,
private _authService: AuthService, private _router: Router,
private _router: Router, ) {}
) {}
ngOnInit() {
ngOnInit() { if (
if (this._route.snapshot.params.token && !this._authService.isAuthenticated()) { this._route.snapshot.params.token &&
this._authService.setKeyCardToken(this._route.snapshot.params.token); !this._authService.isAuthenticated()
this._authService.login(); ) {
} else if (!this._authService.isAuthenticated()) { this._authService.setKeyCardToken(this._route.snapshot.params.token);
this._authService.login(); this._authService.login();
} else if (this._authService.isAuthenticated()) { } else if (!this._authService.isAuthenticated()) {
this._router.navigate(['/']); this._authService.login();
} } else if (this._authService.isAuthenticated()) {
} this._router.navigate(['/']);
} }
}
}

View File

@@ -1,11 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TokenLoginComponent } from './token-login.component';
@NgModule({
imports: [CommonModule],
exports: [TokenLoginComponent],
declarations: [TokenLoginComponent],
})
export class TokenLoginModule {}

View File

@@ -1,23 +0,0 @@
import { NgModule, ModuleWithProviders } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { applicationReducer } from './store';
import { ApplicationService } from './application.service';
@NgModule({
declarations: [],
imports: [],
exports: [],
})
export class CoreApplicationModule {
static forRoot(): ModuleWithProviders<CoreApplicationModule> {
return {
ngModule: RootCoreApplicationModule,
};
}
}
@NgModule({
imports: [StoreModule.forFeature('core-application', applicationReducer)],
providers: [ApplicationService],
})
export class RootCoreApplicationModule {}

View File

@@ -1,337 +0,0 @@
import { inject, Injectable } from '@angular/core';
import { BehaviorSubject, Observable, of, firstValueFrom } from 'rxjs';
import { map, filter, withLatestFrom } from 'rxjs/operators';
import { BranchDTO } from '@generated/swagger/checkout-api';
import { isBoolean, isNumber } from '@utils/common';
import { ApplicationService } from './application.service';
import { TabService } from '@isa/core/tabs';
import { ApplicationProcess } from './defs/application-process';
import { Tab, TabMetadata } from '@isa/core/tabs';
import { toObservable } from '@angular/core/rxjs-interop';
import { Store } from '@ngrx/store';
import { removeProcess } from './store/application.actions';
/**
* Adapter service that bridges the old ApplicationService interface with the new TabService.
*
* This adapter allows existing code that depends on ApplicationService to work with the new
* TabService without requiring immediate code changes. It maps ApplicationProcess concepts
* to Tab entities, storing process-specific data in tab metadata.
*
* Key mappings:
* - ApplicationProcess.id <-> Tab.id
* - ApplicationProcess.name <-> Tab.name
* - ApplicationProcess metadata (section, type, etc.) <-> Tab.metadata with 'process_' prefix
* - ApplicationProcess.data <-> Tab.metadata with 'data_' prefix
*
* @example
* ```typescript
* // Inject the adapter instead of the original service
* constructor(private applicationService: ApplicationServiceAdapter) {}
*
* // Use the same API as before
* const process = await this.applicationService.createCustomerProcess();
* this.applicationService.activateProcess(process.id);
* ```
*/
@Injectable({ providedIn: 'root' })
export class ApplicationServiceAdapter extends ApplicationService {
#store = inject(Store);
#tabService = inject(TabService);
#activatedProcessId$ = toObservable(this.#tabService.activatedTabId);
#tabs$ = toObservable(this.#tabService.entities);
#processes$ = this.#tabs$.pipe(
map((tabs) => tabs.map((tab) => this.mapTabToProcess(tab))),
);
#section = new BehaviorSubject<'customer' | 'branch'>('customer');
readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
get activatedProcessId() {
return this.#tabService.activatedTabId();
}
get activatedProcessId$() {
return this.#activatedProcessId$;
}
getProcesses$(
section?: 'customer' | 'branch',
): Observable<ApplicationProcess[]> {
return this.#processes$.pipe(
map((processes) =>
processes.filter((process) =>
section ? process.section === section : true,
),
),
);
}
getProcessById$(processId: number): Observable<ApplicationProcess> {
return this.#processes$.pipe(
map((processes) => processes.find((process) => process.id === processId)),
);
}
getSection$(): Observable<'customer' | 'branch'> {
return this.#section.asObservable();
}
getTitle$(): Observable<'Kundenbereich' | 'Filialbereich'> {
return this.getSection$().pipe(
map((section) =>
section === 'customer' ? 'Kundenbereich' : 'Filialbereich',
),
);
}
/** @deprecated */
getActivatedProcessId$(): Observable<number> {
return this.activatedProcessId$;
}
activateProcess(activatedProcessId: number): void {
this.#tabService.activateTab(activatedProcessId);
}
removeProcess(processId: number): void {
this.#tabService.removeTab(processId);
this.#store.dispatch(removeProcess({ processId }));
}
patchProcess(processId: number, changes: Partial<ApplicationProcess>): void {
const tabChanges: {
name?: string;
tags?: string[];
metadata?: Record<string, unknown>;
} = {};
if (changes.name) {
tabChanges.name = changes.name;
}
// Store other ApplicationProcess properties in metadata
const metadataKeys = [
'section',
'type',
'closeable',
'confirmClosing',
'created',
'activated',
'data',
];
metadataKeys.forEach((key) => {
if (tabChanges.metadata === undefined) {
tabChanges.metadata = {};
}
if (changes[key as keyof ApplicationProcess] !== undefined) {
tabChanges.metadata[`process_${key}`] =
changes[key as keyof ApplicationProcess];
}
});
// Apply the changes to the tab
this.#tabService.patchTab(processId, tabChanges);
}
patchProcessData(processId: number, data: Record<string, unknown>): void {
const currentProcess = this.#tabService.entityMap()[processId];
const currentData: TabMetadata =
(currentProcess?.metadata?.['process_data'] as TabMetadata) ?? {};
this.#tabService.patchTab(processId, {
metadata: { [`process_data`]: { ...currentData, ...data } },
});
}
getSelectedBranch$(): Observable<BranchDTO> {
return this.#processes$.pipe(
withLatestFrom(this.#activatedProcessId$),
map(([processes, activatedProcessId]) =>
processes.find((process) => process.id === activatedProcessId),
),
filter((process): process is ApplicationProcess => !!process),
map((process) => process.data?.selectedBranch as BranchDTO),
);
}
async createCustomerProcess(processId?: number): Promise<ApplicationProcess> {
const processes = await firstValueFrom(this.getProcesses$('customer'));
const processIds = processes
.filter((x) => this.REGEX_PROCESS_NAME.test(x.name))
.map((x) => +x.name.split(' ')[1]);
const maxId = processIds.length > 0 ? Math.max(...processIds) : 0;
const process: ApplicationProcess = {
id: processId ?? Date.now(),
type: 'cart',
name: `Vorgang ${maxId + 1}`,
section: 'customer',
closeable: true,
};
await this.createProcess(process);
return process;
}
/**
* Creates a new ApplicationProcess by first creating a Tab and then storing
* process-specific properties in the tab's metadata.
*
* @param process - The ApplicationProcess to create
* @throws {Error} If process ID already exists or is invalid
*/
async createProcess(process: ApplicationProcess): Promise<void> {
const existingProcess = this.#tabService.entityMap()[process.id];
if (existingProcess?.id === process?.id) {
throw new Error('Process Id existiert bereits');
}
if (!isNumber(process.id)) {
throw new Error('Process Id nicht gesetzt');
}
if (!isBoolean(process.closeable)) {
process.closeable = true;
}
if (!isBoolean(process.confirmClosing)) {
process.confirmClosing = true;
}
process.created = this.createTimestamp();
process.activated = 0;
// Create tab with process data and preserve the process ID
this.#tabService.addTab({
id: process.id,
name: process.name,
tags: [process.section, process.type].filter(Boolean),
metadata: {
process_section: process.section,
process_type: process.type,
process_closeable: process.closeable,
process_confirmClosing: process.confirmClosing,
process_created: process.created,
process_activated: process.activated,
process_data: process.data,
},
});
}
setSection(section: 'customer' | 'branch'): void {
this.#section.next(section);
}
getLastActivatedProcessWithSectionAndType$(
section: 'customer' | 'branch',
type: string,
): Observable<ApplicationProcess> {
return this.getProcesses$(section).pipe(
map((processes) =>
processes
?.filter((process) => process.type === type)
?.reduce((latest, current) => {
if (!latest) {
return current;
}
return latest?.activated > current?.activated ? latest : current;
}, undefined),
),
);
}
getLastActivatedProcessWithSection$(
section: 'customer' | 'branch',
): Observable<ApplicationProcess> {
return this.getProcesses$(section).pipe(
map((processes) =>
processes?.reduce((latest, current) => {
if (!latest) {
return current;
}
return latest?.activated > current?.activated ? latest : current;
}, undefined),
),
);
}
/**
* Maps Tab entities to ApplicationProcess objects by extracting process-specific
* metadata and combining it with tab properties.
*
* @param tab - The tab entity to convert
* @returns The corresponding ApplicationProcess object
*/
private mapTabToProcess(tab: Tab): ApplicationProcess {
return {
id: tab.id,
name: tab.name,
created:
this.getMetadataValue<number>(tab.metadata, 'process_created') ??
tab.createdAt,
activated:
this.getMetadataValue<number>(tab.metadata, 'process_activated') ??
tab.activatedAt ??
0,
section:
this.getMetadataValue<'customer' | 'branch'>(
tab.metadata,
'process_section',
) ?? 'customer',
type: this.getMetadataValue<string>(tab.metadata, 'process_type'),
closeable:
this.getMetadataValue<boolean>(tab.metadata, 'process_closeable') ??
true,
confirmClosing:
this.getMetadataValue<boolean>(
tab.metadata,
'process_confirmClosing',
) ?? true,
data: this.extractDataFromMetadata(tab.metadata),
};
}
/**
* Extracts ApplicationProcess data properties from tab metadata.
* Data properties are stored with a 'data_' prefix in tab metadata.
*
* @param metadata - The tab metadata object
* @returns The extracted data object or undefined if no data properties exist
*/
private extractDataFromMetadata(
metadata: TabMetadata,
): Record<string, unknown> | undefined {
// Return the complete data object stored under 'process_data'
const processData = metadata?.['process_data'];
if (
processData &&
typeof processData === 'object' &&
processData !== null
) {
return processData as Record<string, unknown>;
}
return undefined;
}
private getMetadataValue<T>(
metadata: TabMetadata,
key: string,
): T | undefined {
return metadata?.[key] as T | undefined;
}
private createTimestamp(): number {
return Date.now();
}
}

View File

@@ -1,233 +0,0 @@
// import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator';
// import { Store } from '@ngrx/store';
// import { Observable, of } from 'rxjs';
// import { first } from 'rxjs/operators';
// import { ApplicationProcess } from './defs';
// import { ApplicationService } from './application.service';
// import * as actions from './store/application.actions';
// describe('ApplicationService', () => {
// let spectator: SpectatorService<ApplicationService>;
// let store: SpyObject<Store>;
// const createService = createServiceFactory({
// service: ApplicationService,
// mocks: [Store],
// });
// beforeEach(() => {
// spectator = createService({});
// store = spectator.inject(Store);
// });
// it('should be created', () => {
// expect(spectator.service).toBeTruthy();
// });
// describe('activatedProcessId$', () => {
// it('should return an observable', () => {
// expect(spectator.service.activatedProcessId$).toBeInstanceOf(Observable);
// });
// });
// describe('activatedProcessId', () => {
// it('should return the process id as a number', () => {
// spyOnProperty(spectator.service['activatedProcessIdSubject'] as any, 'value').and.returnValue(2);
// expect(spectator.service.activatedProcessId).toBe(2);
// });
// });
// describe('getProcesses$()', () => {
// it('should call select on store and return all selected processes', async () => {
// const processes: ApplicationProcess[] = [
// { id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
// { id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
// ];
// store.select.and.returnValue(of(processes));
// const result = await spectator.service.getProcesses$().pipe(first()).toPromise();
// expect(result).toEqual(processes);
// expect(store.select).toHaveBeenCalled();
// });
// it('should call select on store and return all section customer processes', async () => {
// const processes: ApplicationProcess[] = [
// { id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
// { id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
// ];
// store.select.and.returnValue(of(processes));
// const result = await spectator.service.getProcesses$('customer').pipe(first()).toPromise();
// expect(result).toEqual([processes[0]]);
// expect(store.select).toHaveBeenCalled();
// });
// it('should call select on store and return all section branch processes', async () => {
// const processes: ApplicationProcess[] = [
// { id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
// { id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
// ];
// store.select.and.returnValue(of(processes));
// const result = await spectator.service.getProcesses$('branch').pipe(first()).toPromise();
// expect(result).toEqual([processes[1]]);
// expect(store.select).toHaveBeenCalled();
// });
// });
// describe('getProcessById$()', () => {
// it('should return the process by id', async () => {
// const processes: ApplicationProcess[] = [
// { id: 1, name: 'Vorgang 1', section: 'customer' },
// { id: 2, name: 'Vorgang 2', section: 'customer' },
// ];
// spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));
// const process = await spectator.service.getProcessById$(1).toPromise();
// expect(process.id).toBe(1);
// });
// });
// describe('getSection$()', () => {
// it('should return the selected section branch', async () => {
// const section = 'branch';
// store.select.and.returnValue(of(section));
// const result = await spectator.service.getSection$().pipe(first()).toPromise();
// expect(result).toEqual(section);
// expect(store.select).toHaveBeenCalled();
// });
// });
// describe('getActivatedProcessId$', () => {
// it('should return the current selected activated process id', async () => {
// const activatedProcessId = 2;
// store.select.and.returnValue(of({ id: activatedProcessId }));
// const result = await spectator.service.getActivatedProcessId$().pipe(first()).toPromise();
// expect(result).toEqual(activatedProcessId);
// expect(store.select).toHaveBeenCalled();
// });
// });
// describe('activateProcess()', () => {
// it('should dispatch action setActivatedProcess with argument activatedProcessId and action type', () => {
// const activatedProcessId = 2;
// spectator.service.activateProcess(activatedProcessId);
// expect(store.dispatch).toHaveBeenCalledWith({ activatedProcessId, type: actions.setActivatedProcess.type });
// });
// });
// describe('removeProcess()', () => {
// it('should dispatch action removeProcess with argument processId and action type', () => {
// const processId = 2;
// spectator.service.removeProcess(processId);
// expect(store.dispatch).toHaveBeenCalledWith({ processId, type: actions.removeProcess.type });
// });
// });
// describe('createProcess()', () => {
// it('should dispatch action addProcess with process', async () => {
// const process: ApplicationProcess = {
// id: 1,
// name: 'Vorgang 1',
// section: 'customer',
// type: 'cart',
// };
// const timestamp = 100;
// spyOn(spectator.service as any, '_createTimestamp').and.returnValue(timestamp);
// spyOn(spectator.service, 'getProcessById$').and.returnValue(of(undefined));
// await spectator.service.createProcess(process);
// expect(store.dispatch).toHaveBeenCalledWith({
// type: actions.addProcess.type,
// process: {
// ...process,
// activated: 0,
// created: timestamp,
// },
// });
// });
// it('should throw an error if the process id is already existing', async () => {
// const process: ApplicationProcess = {
// id: 1,
// name: 'Vorgang 1',
// section: 'customer',
// type: 'cart',
// };
// spyOn(spectator.service, 'getProcessById$').and.returnValue(of(process));
// await expectAsync(spectator.service.createProcess(process)).toBeRejectedWithError('Process Id existiert bereits');
// });
// it('should throw an error if the process id is not a number', async () => {
// const process: ApplicationProcess = {
// id: undefined,
// name: 'Vorgang 1',
// section: 'customer',
// type: 'cart',
// };
// spyOn(spectator.service, 'getProcessById$').and.returnValue(of({ id: 5, name: 'Vorgang 2', section: 'customer' }));
// await expectAsync(spectator.service.createProcess(process)).toBeRejectedWithError('Process Id nicht gesetzt');
// });
// });
// describe('patchProcess', () => {
// it('should dispatch action patchProcess with changes', async () => {
// const process: ApplicationProcess = {
// id: 1,
// name: 'Vorgang 1',
// section: 'customer',
// type: 'cart',
// };
// await spectator.service.patchProcess(process.id, process);
// expect(store.dispatch).toHaveBeenCalledWith({
// type: actions.patchProcess.type,
// processId: process.id,
// changes: {
// ...process,
// },
// });
// });
// });
// describe('setSection()', () => {
// it('should dispatch action setSection with argument section and action type', () => {
// const section = 'customer';
// spectator.service.setSection(section);
// expect(store.dispatch).toHaveBeenCalledWith({ section, type: actions.setSection.type });
// });
// });
// describe('getLastActivatedProcessWithSectionAndType()', () => {
// it('should return the last activated process by section and type', async () => {
// const processes: ApplicationProcess[] = [
// { id: 1, name: 'Vorgang 1', section: 'customer', type: 'cart', activated: 100 },
// { id: 2, name: 'Vorgang 2', section: 'customer', type: 'cart', activated: 200 },
// { id: 3, name: 'Vorgang 3', section: 'customer', type: 'goodsOut', activated: 300 },
// ];
// spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));
// expect(await spectator.service.getLastActivatedProcessWithSectionAndType$('customer', 'cart').pipe(first()).toPromise()).toBe(
// processes[1]
// );
// });
// });
// describe('getLastActivatedProcessWithSection()', () => {
// it('should return the last activated process by section', async () => {
// const processes: ApplicationProcess[] = [
// { id: 1, name: 'Vorgang 1', section: 'customer', activated: 100 },
// { id: 2, name: 'Vorgang 2', section: 'customer', activated: 200 },
// { id: 3, name: 'Vorgang 3', section: 'customer', activated: 300 },
// ];
// spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));
// expect(await spectator.service.getLastActivatedProcessWithSection$('customer').pipe(first()).toPromise()).toBe(processes[2]);
// });
// });
// describe('_createTimestamp', () => {
// it('should return the current timestamp in ms', () => {
// expect(spectator.service['_createTimestamp']()).toBeCloseTo(Date.now());
// });
// });
// });

View File

@@ -1,41 +1,68 @@
import { Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { Store } from '@ngrx/store'; import { BehaviorSubject, Observable, of, firstValueFrom } from 'rxjs';
import { map, filter, withLatestFrom } from 'rxjs/operators';
import { BranchDTO } from '@generated/swagger/checkout-api'; import { BranchDTO } from '@generated/swagger/checkout-api';
import { isBoolean, isNumber } from '@utils/common'; import { isBoolean, isNumber } from '@utils/common';
import { BehaviorSubject, Observable } from 'rxjs'; import { TabService } from '@isa/core/tabs';
import { first, map, switchMap } from 'rxjs/operators'; import { ApplicationProcess } from './defs/application-process';
import { ApplicationProcess } from './defs'; import { Tab, TabMetadata } from '@isa/core/tabs';
import { import { toObservable } from '@angular/core/rxjs-interop';
removeProcess, import { Store } from '@ngrx/store';
selectSection, import { removeProcess } from './store/application.actions';
selectProcesses,
setSection,
addProcess,
setActivatedProcess,
selectActivatedProcess,
patchProcess,
patchProcessData,
selectTitle,
setTitle,
} from './store';
@Injectable() /**
* Adapter service that bridges the old ApplicationService interface with the new TabService.
*
* This adapter allows existing code that depends on ApplicationService to work with the new
* TabService without requiring immediate code changes. It maps ApplicationProcess concepts
* to Tab entities, storing process-specific data in tab metadata.
*
* Key mappings:
* - ApplicationProcess.id <-> Tab.id
* - ApplicationProcess.name <-> Tab.name
* - ApplicationProcess metadata (section, type, etc.) <-> Tab.metadata with 'process_' prefix
* - ApplicationProcess.data <-> Tab.metadata with 'data_' prefix
*
* @example
* ```typescript
* // Inject the adapter instead of the original service
* constructor(private applicationService: ApplicationServiceAdapter) {}
*
* // Use the same API as before
* const process = await this.applicationService.createCustomerProcess();
* this.applicationService.activateProcess(process.id);
* ```
*/
@Injectable({ providedIn: 'root' })
export class ApplicationService { export class ApplicationService {
private activatedProcessIdSubject = new BehaviorSubject<number>(undefined); #store = inject(Store);
#tabService = inject(TabService);
#activatedProcessId$ = toObservable(this.#tabService.activatedTabId);
#tabs$ = toObservable(this.#tabService.entities);
#processes$ = this.#tabs$.pipe(
map((tabs) => tabs.map((tab) => this.mapTabToProcess(tab))),
);
#section = new BehaviorSubject<'customer' | 'branch'>('customer');
readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
get activatedProcessId() { get activatedProcessId() {
return this.activatedProcessIdSubject.value; return this.#tabService.activatedTabId();
} }
get activatedProcessId$() { get activatedProcessId$() {
return this.activatedProcessIdSubject.asObservable(); return this.#activatedProcessId$;
} }
constructor(private store: Store) {} getProcesses$(
section?: 'customer' | 'branch',
getProcesses$(section?: 'customer' | 'branch') { ): Observable<ApplicationProcess[]> {
const processes$ = this.store.select(selectProcesses); return this.#processes$.pipe(
return processes$.pipe(
map((processes) => map((processes) =>
processes.filter((process) => processes.filter((process) =>
section ? process.section === section : true, section ? process.section === section : true,
@@ -45,69 +72,96 @@ export class ApplicationService {
} }
getProcessById$(processId: number): Observable<ApplicationProcess> { getProcessById$(processId: number): Observable<ApplicationProcess> {
return this.getProcesses$().pipe( return this.#processes$.pipe(
map((processes) => processes.find((process) => process.id === processId)), map((processes) => processes.find((process) => process.id === processId)),
); );
} }
getSection$() { getSection$(): Observable<'customer' | 'branch'> {
return this.store.select(selectSection); return this.#section.asObservable();
} }
getTitle$() { getTitle$(): Observable<'Kundenbereich' | 'Filialbereich'> {
return this.getSection$().pipe( return this.getSection$().pipe(
map((section) => { map((section) =>
return section === 'customer' ? 'Kundenbereich' : 'Filialbereich'; section === 'customer' ? 'Kundenbereich' : 'Filialbereich',
}), ),
); );
} }
/** @deprecated */ /** @deprecated */
getActivatedProcessId$() { getActivatedProcessId$(): Observable<number> {
return this.store return this.activatedProcessId$;
.select(selectActivatedProcess)
.pipe(map((process) => process?.id));
} }
activateProcess(activatedProcessId: number) { activateProcess(activatedProcessId: number): void {
this.store.dispatch(setActivatedProcess({ activatedProcessId })); this.#tabService.activateTab(activatedProcessId);
this.activatedProcessIdSubject.next(activatedProcessId);
} }
removeProcess(processId: number) { removeProcess(processId: number): void {
this.store.dispatch(removeProcess({ processId })); this.#tabService.removeTab(processId);
this.#store.dispatch(removeProcess({ processId }));
} }
patchProcess(processId: number, changes: Partial<ApplicationProcess>) { patchProcess(processId: number, changes: Partial<ApplicationProcess>): void {
this.store.dispatch(patchProcess({ processId, changes })); const tabChanges: {
} name?: string;
tags?: string[];
metadata?: Record<string, unknown>;
} = {};
patchProcessData(processId: number, data: Record<string, any>) { if (changes.name) {
this.store.dispatch(patchProcessData({ processId, data })); tabChanges.name = changes.name;
}
getSelectedBranch$(processId?: number): Observable<BranchDTO> {
if (!processId) {
return this.activatedProcessId$.pipe(
switchMap((processId) =>
this.getProcessById$(processId).pipe(
map((process) => process?.data?.selectedBranch),
),
),
);
} }
return this.getProcessById$(processId).pipe( // Store other ApplicationProcess properties in metadata
map((process) => process?.data?.selectedBranch), const metadataKeys = [
'section',
'type',
'closeable',
'confirmClosing',
'created',
'activated',
'data',
];
metadataKeys.forEach((key) => {
if (tabChanges.metadata === undefined) {
tabChanges.metadata = {};
}
if (changes[key as keyof ApplicationProcess] !== undefined) {
tabChanges.metadata[`process_${key}`] =
changes[key as keyof ApplicationProcess];
}
});
// Apply the changes to the tab
this.#tabService.patchTab(processId, tabChanges);
}
patchProcessData(processId: number, data: Record<string, unknown>): void {
const currentProcess = this.#tabService.entityMap()[processId];
const currentData: TabMetadata =
(currentProcess?.metadata?.['process_data'] as TabMetadata) ?? {};
this.#tabService.patchTab(processId, {
metadata: { [`process_data`]: { ...currentData, ...data } },
});
}
getSelectedBranch$(): Observable<BranchDTO> {
return this.#processes$.pipe(
withLatestFrom(this.#activatedProcessId$),
map(([processes, activatedProcessId]) =>
processes.find((process) => process.id === activatedProcessId),
),
filter((process): process is ApplicationProcess => !!process),
map((process) => process.data?.selectedBranch as BranchDTO),
); );
} }
readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
async createCustomerProcess(processId?: number): Promise<ApplicationProcess> { async createCustomerProcess(processId?: number): Promise<ApplicationProcess> {
const processes = await this.getProcesses$('customer') const processes = await firstValueFrom(this.getProcesses$('customer'));
.pipe(first())
.toPromise();
const processIds = processes const processIds = processes
.filter((x) => this.REGEX_PROCESS_NAME.test(x.name)) .filter((x) => this.REGEX_PROCESS_NAME.test(x.name))
@@ -124,14 +178,18 @@ export class ApplicationService {
}; };
await this.createProcess(process); await this.createProcess(process);
return process; return process;
} }
async createProcess(process: ApplicationProcess) { /**
const existingProcess = await this.getProcessById$(process?.id) * Creates a new ApplicationProcess by first creating a Tab and then storing
.pipe(first()) * process-specific properties in the tab's metadata.
.toPromise(); *
* @param process - The ApplicationProcess to create
* @throws {Error} If process ID already exists or is invalid
*/
async createProcess(process: ApplicationProcess): Promise<void> {
const existingProcess = this.#tabService.entityMap()[process.id];
if (existingProcess?.id === process?.id) { if (existingProcess?.id === process?.id) {
throw new Error('Process Id existiert bereits'); throw new Error('Process Id existiert bereits');
} }
@@ -148,13 +206,28 @@ export class ApplicationService {
process.confirmClosing = true; process.confirmClosing = true;
} }
process.created = this._createTimestamp(); process.created = this.createTimestamp();
process.activated = 0; process.activated = 0;
this.store.dispatch(addProcess({ process }));
// Create tab with process data and preserve the process ID
this.#tabService.addTab({
id: process.id,
name: process.name,
tags: [process.section, process.type].filter(Boolean),
metadata: {
process_section: process.section,
process_type: process.type,
process_closeable: process.closeable,
process_confirmClosing: process.confirmClosing,
process_created: process.created,
process_activated: process.activated,
process_data: process.data,
},
});
} }
setSection(section: 'customer' | 'branch') { setSection(section: 'customer' | 'branch'): void {
this.store.dispatch(setSection({ section })); this.#section.next(section);
} }
getLastActivatedProcessWithSectionAndType$( getLastActivatedProcessWithSectionAndType$(
@@ -190,7 +263,74 @@ export class ApplicationService {
); );
} }
private _createTimestamp() { /**
* Maps Tab entities to ApplicationProcess objects by extracting process-specific
* metadata and combining it with tab properties.
*
* @param tab - The tab entity to convert
* @returns The corresponding ApplicationProcess object
*/
private mapTabToProcess(tab: Tab): ApplicationProcess {
return {
id: tab.id,
name: tab.name,
created:
this.getMetadataValue<number>(tab.metadata, 'process_created') ??
tab.createdAt,
activated:
this.getMetadataValue<number>(tab.metadata, 'process_activated') ??
tab.activatedAt ??
0,
section:
this.getMetadataValue<'customer' | 'branch'>(
tab.metadata,
'process_section',
) ?? 'customer',
type: this.getMetadataValue<string>(tab.metadata, 'process_type'),
closeable:
this.getMetadataValue<boolean>(tab.metadata, 'process_closeable') ??
true,
confirmClosing:
this.getMetadataValue<boolean>(
tab.metadata,
'process_confirmClosing',
) ?? true,
data: this.extractDataFromMetadata(tab.metadata),
};
}
/**
* Extracts ApplicationProcess data properties from tab metadata.
* Data properties are stored with a 'data_' prefix in tab metadata.
*
* @param metadata - The tab metadata object
* @returns The extracted data object or undefined if no data properties exist
*/
private extractDataFromMetadata(
metadata: TabMetadata,
): Record<string, unknown> | undefined {
// Return the complete data object stored under 'process_data'
const processData = metadata?.['process_data'];
if (
processData &&
typeof processData === 'object' &&
processData !== null
) {
return processData as Record<string, unknown>;
}
return undefined;
}
private getMetadataValue<T>(
metadata: TabMetadata,
key: string,
): T | undefined {
return metadata?.[key] as T | undefined;
}
private createTimestamp(): number {
return Date.now(); return Date.now();
} }
} }

View File

@@ -1,5 +1,3 @@
export * from './application.module'; export * from './application.service';
export * from './application.service'; export * from './defs';
export * from './application.service-adapter'; export * from './store/application.actions';
export * from './defs';
export * from './store';

View File

@@ -1,27 +1,8 @@
import { createAction, props } from '@ngrx/store'; import { createAction, props } from '@ngrx/store';
import { ApplicationProcess } from '..';
const prefix = '[CORE-APPLICATION]';
const prefix = '[CORE-APPLICATION]';
export const removeProcess = createAction(
export const setTitle = createAction(`${prefix} Set Title`, props<{ title: string }>()); `${prefix} Remove Process`,
props<{ processId: number }>(),
export const setSection = createAction(`${prefix} Set Section`, props<{ section: 'customer' | 'branch' }>()); );
export const addProcess = createAction(`${prefix} Add Process`, props<{ process: ApplicationProcess }>());
export const removeProcess = createAction(`${prefix} Remove Process`, props<{ processId: number }>());
export const setActivatedProcess = createAction(
`${prefix} Set Activated Process`,
props<{ activatedProcessId: number }>(),
);
export const patchProcess = createAction(
`${prefix} Patch Process`,
props<{ processId: number; changes: Partial<ApplicationProcess> }>(),
);
export const patchProcessData = createAction(
`${prefix} Patch Process Data`,
props<{ processId: number; data: Record<string, any> }>(),
);

View File

@@ -1,200 +0,0 @@
import { INITIAL_APPLICATION_STATE } from './application.state';
import * as actions from './application.actions';
import { applicationReducer } from './application.reducer';
import { ApplicationProcess } from '../defs';
import { ApplicationState } from './application.state';
describe('applicationReducer', () => {
describe('setSection()', () => {
it('should return modified state with section customer', () => {
const initialState = INITIAL_APPLICATION_STATE;
const action = actions.setSection({ section: 'customer' });
const state = applicationReducer(initialState, action);
expect(state).toEqual({
...initialState,
section: 'customer',
});
});
it('should return modified state with section branch', () => {
const initialState = INITIAL_APPLICATION_STATE;
const action = actions.setSection({ section: 'branch' });
const state = applicationReducer(initialState, action);
expect(state).toEqual({
...initialState,
section: 'branch',
});
});
});
describe('addProcess()', () => {
it('should return modified state with new process if no processes are registered in the state', () => {
const initialState = INITIAL_APPLICATION_STATE;
const process: ApplicationProcess = {
id: 1,
name: 'Vorgang',
section: 'customer',
type: 'cart',
data: {},
};
const action = actions.addProcess({ process });
const state = applicationReducer(initialState, action);
expect(state.processes[0]).toEqual(process);
});
});
describe('patchProcess()', () => {
it('should return modified state with updated process when id is found', () => {
const initialState = INITIAL_APPLICATION_STATE;
const process: ApplicationProcess = {
id: 1,
name: 'Vorgang',
section: 'customer',
type: 'cart',
};
const action = actions.patchProcess({ processId: process.id, changes: { ...process, name: 'Test' } });
const state = applicationReducer(
{
...initialState,
processes: [process],
},
action,
);
expect(state.processes[0].name).toEqual('Test');
});
it('should return unmodified state when id is not existing', () => {
const initialState = INITIAL_APPLICATION_STATE;
const process: ApplicationProcess = {
id: 1,
name: 'Vorgang',
section: 'customer',
type: 'cart',
};
const action = actions.patchProcess({ processId: process.id, changes: { ...process, id: 2 } });
const state = applicationReducer(
{
...initialState,
processes: [process],
},
action,
);
expect(state.processes).toEqual([process]);
});
});
describe('removeProcess()', () => {
it('should return initial state if no processes are registered in the state', () => {
const initialState = INITIAL_APPLICATION_STATE;
const action = actions.removeProcess({ processId: 2 });
const state = applicationReducer(initialState, action);
expect(state).toEqual(initialState);
});
it('should return the unmodified state if processId not found', () => {
const initialState = INITIAL_APPLICATION_STATE;
const modifiedState: ApplicationState = {
...initialState,
section: 'customer',
processes: [
{
id: 1,
name: 'Vorgang',
section: 'customer',
type: 'cart',
},
{
id: 4,
name: 'Vorgang',
section: 'customer',
type: 'goods-out',
},
] as ApplicationProcess[],
};
const action = actions.removeProcess({ processId: 2 });
const state = applicationReducer(modifiedState, action);
expect(state).toEqual(modifiedState);
});
it('should return modified state, after process gets removed', () => {
const initialState = INITIAL_APPLICATION_STATE;
const modifiedState: ApplicationState = {
...initialState,
section: 'customer',
processes: [
{
id: 1,
name: 'Vorgang',
section: 'customer',
type: 'cart',
},
{
id: 2,
name: 'Vorgang',
section: 'customer',
type: 'goods-out',
},
] as ApplicationProcess[],
};
const action = actions.removeProcess({ processId: 2 });
const state = applicationReducer(modifiedState, action);
expect(state.processes).toEqual([
{
id: 1,
name: 'Vorgang',
section: 'customer',
type: 'cart',
},
]);
});
});
describe('setActivatedProcess()', () => {
it('should return modified state with process.activated value', () => {
const process: ApplicationProcess = {
id: 3,
name: 'Vorgang 3',
section: 'customer',
};
const initialState: ApplicationState = {
...INITIAL_APPLICATION_STATE,
processes: [process],
};
const action = actions.setActivatedProcess({ activatedProcessId: 3 });
const state = applicationReducer(initialState, action);
expect(state.processes[0].activated).toBeDefined();
});
it('should return modified state without process.activated value when activatedProcessId doesnt exist', () => {
const process: ApplicationProcess = {
id: 1,
name: 'Vorgang 3',
section: 'customer',
};
const initialState: ApplicationState = {
...INITIAL_APPLICATION_STATE,
processes: [process],
};
const action = actions.setActivatedProcess({ activatedProcessId: 3 });
const state = applicationReducer(initialState, action);
expect(state.processes[0].activated).toBeUndefined();
});
});
});

View File

@@ -1,56 +0,0 @@
import { Action, createReducer, on } from '@ngrx/store';
import {
setSection,
addProcess,
removeProcess,
setActivatedProcess,
patchProcess,
patchProcessData,
setTitle,
} from './application.actions';
import { ApplicationState, INITIAL_APPLICATION_STATE } from './application.state';
const _applicationReducer = createReducer(
INITIAL_APPLICATION_STATE,
on(setTitle, (state, { title }) => ({ ...state, title })),
on(setSection, (state, { section }) => ({ ...state, section })),
on(addProcess, (state, { process }) => ({ ...state, processes: [...state.processes, { data: {}, ...process }] })),
on(removeProcess, (state, { processId }) => {
const processes = state?.processes?.filter((process) => process.id !== processId) || [];
return { ...state, processes };
}),
on(setActivatedProcess, (state, { activatedProcessId }) => {
const processes = state.processes.map((process) => {
if (process.id === activatedProcessId) {
return { ...process, activated: Date.now() };
}
return process;
});
return { ...state, processes };
}),
on(patchProcess, (state, { processId, changes }) => {
const processes = state.processes.map((process) => {
if (process.id === processId) {
return { ...process, ...changes, id: processId };
}
return process;
});
return { ...state, processes };
}),
on(patchProcessData, (state, { processId, data }) => {
const processes = state.processes.map((process) => {
if (process.id === processId) {
return { ...process, data: { ...(process.data || {}), ...data } };
}
return process;
});
return { ...state, processes };
}),
);
export function applicationReducer(state: ApplicationState, action: Action) {
return _applicationReducer(state, action);
}

View File

@@ -1,35 +0,0 @@
// import { ApplicationState } from './application.state';
// import { ApplicationProcess } from '../defs';
// import * as selectors from './application.selectors';
// describe('applicationSelectors', () => {
// it('should select the processes', () => {
// const processes: ApplicationProcess[] = [{ id: 1, name: 'Vorgang 1', section: 'customer' }];
// const state: ApplicationState = {
// processes,
// section: 'customer',
// };
// expect(selectors.selectProcesses.projector(state)).toEqual(processes);
// });
// it('should select the section', () => {
// const state: ApplicationState = {
// processes: [],
// section: 'customer',
// };
// expect(selectors.selectSection.projector(state)).toEqual('customer');
// });
// it('should select the activatedProcess', () => {
// const processes: ApplicationProcess[] = [
// { id: 1, name: 'Vorgang 1', section: 'customer', activated: 100 },
// { id: 2, name: 'Vorgang 2', section: 'customer', activated: 300 },
// { id: 3, name: 'Vorgang 3', section: 'customer', activated: 200 },
// ];
// const state: ApplicationState = {
// processes,
// section: 'customer',
// };
// expect(selectors.selectActivatedProcess.projector(state)).toEqual(processes[1]);
// });
// });

View File

@@ -1,18 +0,0 @@
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ApplicationState } from './application.state';
export const selectApplicationState = createFeatureSelector<ApplicationState>('core-application');
export const selectTitle = createSelector(selectApplicationState, (s) => s.title);
export const selectSection = createSelector(selectApplicationState, (s) => s.section);
export const selectProcesses = createSelector(selectApplicationState, (s) => s.processes);
export const selectActivatedProcess = createSelector(selectApplicationState, (s) =>
s?.processes?.reduce((process, current) => {
if (!process) {
return current;
}
return process.activated > current.activated ? process : current;
}, undefined),
);

View File

@@ -1,13 +0,0 @@
import { ApplicationProcess } from '../defs';
export interface ApplicationState {
title: string;
processes: ApplicationProcess[];
section: 'customer' | 'branch';
}
export const INITIAL_APPLICATION_STATE: ApplicationState = {
title: '',
processes: [],
section: 'customer',
};

View File

@@ -1,6 +0,0 @@
// start:ng42.barrel
export * from './application.actions';
export * from './application.reducer';
export * from './application.selectors';
export * from './application.state';
// end:ng42.barrel

View File

@@ -1,22 +1,15 @@
import { ModuleWithProviders, NgModule } from '@angular/core'; import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
import { EffectsModule } from '@ngrx/effects'; import { provideEffects } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store'; import { provideState } from '@ngrx/store';
import { BreadcrumbService } from './breadcrumb.service'; import { BreadcrumbService } from './breadcrumb.service';
import { BreadcrumbEffects } from './store/breadcrumb.effect'; import { BreadcrumbEffects } from './store/breadcrumb.effect';
import { breadcrumbReducer } from './store/breadcrumb.reducer'; import { breadcrumbReducer } from './store/breadcrumb.reducer';
import { featureName } from './store/breadcrumb.state'; import { featureName } from './store/breadcrumb.state';
@NgModule() export function provideCoreBreadcrumb(): EnvironmentProviders {
export class CoreBreadcrumbModule { return makeEnvironmentProviders([
static forRoot(): ModuleWithProviders<CoreBreadcrumbModule> { provideState({ name: featureName, reducer: breadcrumbReducer }),
return { provideEffects(BreadcrumbEffects),
ngModule: CoreBreadcrumbForRootModule, BreadcrumbService,
}; ]);
} }
}
@NgModule({
imports: [StoreModule.forFeature(featureName, breadcrumbReducer), EffectsModule.forFeature([BreadcrumbEffects])],
providers: [BreadcrumbService],
})
export class CoreBreadcrumbForRootModule {}

View File

@@ -1,12 +0,0 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { DomainAvailabilityService } from './availability.service';
@NgModule()
export class DomainAvailabilityModule {
static forRoot(): ModuleWithProviders<DomainAvailabilityModule> {
return {
ngModule: DomainAvailabilityModule,
providers: [DomainAvailabilityService],
};
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
export * from './availability.module';
export * from './availability.service'; export * from './availability.service';
export * from './defs'; export * from './defs';
export * from './in-stock.service'; export * from './in-stock.service';

View File

@@ -1,18 +0,0 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { DomainCatalogService } from './catalog.service';
import { ThumbnailUrlPipe } from './thumbnail-url.pipe';
import { DomainCatalogThumbnailService } from './thumbnail.service';
@NgModule({
declarations: [ThumbnailUrlPipe],
imports: [],
exports: [ThumbnailUrlPipe],
})
export class DomainCatalogModule {
static forRoot(): ModuleWithProviders<DomainCatalogModule> {
return {
ngModule: DomainCatalogModule,
providers: [DomainCatalogService, DomainCatalogThumbnailService],
};
}
}

View File

@@ -1,105 +1,111 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ApplicationService } from '@core/application'; import { ApplicationService } from '@core/application';
import { import {
AutocompleteTokenDTO, AutocompleteTokenDTO,
PromotionService, PromotionService,
QueryTokenDTO, QueryTokenDTO,
SearchService, SearchService,
} from '@generated/swagger/cat-search-api'; } from '@generated/swagger/cat-search-api';
import { memorize } from '@utils/common'; import { memorize } from '@utils/common';
import { map, share, shareReplay } from 'rxjs/operators'; import { map, share, shareReplay } from 'rxjs/operators';
@Injectable() @Injectable({ providedIn: 'root' })
export class DomainCatalogService { export class DomainCatalogService {
constructor( constructor(
private searchService: SearchService, private searchService: SearchService,
private promotionService: PromotionService, private promotionService: PromotionService,
private applicationService: ApplicationService, private applicationService: ApplicationService,
) {} ) {}
@memorize() @memorize()
getFilters() { getFilters() {
return this.searchService.SearchSearchFilter().pipe( return this.searchService.SearchSearchFilter().pipe(
map((res) => res.result), map((res) => res.result),
shareReplay(), shareReplay(),
); );
} }
@memorize() @memorize()
getOrderBy() { getOrderBy() {
return this.searchService.SearchSearchSort().pipe( return this.searchService.SearchSearchSort().pipe(
map((res) => res.result), map((res) => res.result),
shareReplay(), shareReplay(),
); );
} }
getSearchHistory({ take }: { take: number }) { getSearchHistory({ take }: { take: number }) {
return this.searchService.SearchHistory(take ?? 5).pipe(map((res) => res.result)); return this.searchService
} .SearchHistory(take ?? 5)
.pipe(map((res) => res.result));
@memorize({ ttl: 120000 }) }
search({ queryToken }: { queryToken: QueryTokenDTO }) {
return this.searchService @memorize({ ttl: 120000 })
.SearchSearch({ search({ queryToken }: { queryToken: QueryTokenDTO }) {
...queryToken, return this.searchService
stockId: null, .SearchSearch({
}) ...queryToken,
.pipe(share()); stockId: null,
} })
.pipe(share());
@memorize({ ttl: 120000 }) }
searchWithStockId({ queryToken }: { queryToken: QueryTokenDTO }) {
return this.searchService @memorize({ ttl: 120000 })
.SearchSearch2({ searchWithStockId({ queryToken }: { queryToken: QueryTokenDTO }) {
queryToken, return this.searchService
stockId: queryToken?.stockId ?? null, .SearchSearch2({
}) queryToken,
.pipe(share()); stockId: queryToken?.stockId ?? null,
} })
.pipe(share());
getDetailsById({ id }: { id: number }) { }
return this.searchService.SearchDetail({
id, getDetailsById({ id }: { id: number }) {
}); return this.searchService.SearchDetail({
} id,
});
getDetailsByEan({ ean }: { ean: string }) { }
return this.searchService.SearchDetailByEAN(ean);
} getDetailsByEan({ ean }: { ean: string }) {
return this.searchService.SearchDetailByEAN(ean);
searchByIds({ ids }: { ids: number[] }) { }
return this.searchService.SearchById(ids);
} searchByIds({ ids }: { ids: number[] }) {
return this.searchService.SearchById(ids);
searchByEans({ eans }: { eans: string[] }) { }
return this.searchService.SearchByEAN(eans);
} searchByEans({ eans }: { eans: string[] }) {
return this.searchService.SearchByEAN(eans);
searchTop({ queryToken }: { queryToken: QueryTokenDTO }) { }
return this.searchService.SearchTop(queryToken);
} searchTop({ queryToken }: { queryToken: QueryTokenDTO }) {
return this.searchService.SearchTop(queryToken);
searchComplete({ queryToken }: { queryToken: AutocompleteTokenDTO }) { }
return this.searchService.SearchAutocomplete(queryToken);
} searchComplete({ queryToken }: { queryToken: AutocompleteTokenDTO }) {
return this.searchService.SearchAutocomplete(queryToken);
@memorize() }
getPromotionPoints({ items }: { items: { id: number; quantity: number; price?: number }[] }) {
return this.promotionService.PromotionLesepunkte(items).pipe(shareReplay()); @memorize()
} getPromotionPoints({
items,
@memorize() }: {
getSettings() { items: { id: number; quantity: number; price?: number }[];
return this.searchService.SearchSettings().pipe( }) {
map((res) => res.result), return this.promotionService.PromotionLesepunkte(items).pipe(shareReplay());
shareReplay(), }
);
} @memorize()
getSettings() {
getRecommendations({ digId }: { digId: number }) { return this.searchService.SearchSettings().pipe(
return this.searchService.SearchGetRecommendations({ map((res) => res.result),
digId: digId + '', shareReplay(),
sessionId: this.applicationService.activatedProcessId + '', );
}); }
}
} getRecommendations({ digId }: { digId: number }) {
return this.searchService.SearchGetRecommendations({
digId: digId + '',
sessionId: this.applicationService.activatedProcessId + '',
});
}
}

View File

@@ -1,4 +1,3 @@
export * from './catalog.module';
export * from './catalog.service'; export * from './catalog.service';
export * from './thumbnail-url.pipe'; export * from './thumbnail-url.pipe';
export * from './thumbnail.service'; export * from './thumbnail.service';

View File

@@ -6,7 +6,7 @@ import { DomainCatalogThumbnailService } from './thumbnail.service';
@Pipe({ @Pipe({
name: 'thumbnailUrl', name: 'thumbnailUrl',
pure: false, pure: false,
standalone: false, standalone: true,
}) })
export class ThumbnailUrlPipe implements PipeTransform, OnDestroy { export class ThumbnailUrlPipe implements PipeTransform, OnDestroy {
private input$ = new BehaviorSubject<{ width?: number; height?: number; ean?: string }>(undefined); private input$ = new BehaviorSubject<{ width?: number; height?: number; ean?: string }>(undefined);

View File

@@ -1,20 +1,28 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { memorize } from '@utils/common'; import { memorize } from '@utils/common';
import { map, shareReplay } from 'rxjs/operators'; import { map, shareReplay } from 'rxjs/operators';
import { DomainCatalogService } from './catalog.service'; import { DomainCatalogService } from './catalog.service';
@Injectable() @Injectable({ providedIn: 'root' })
export class DomainCatalogThumbnailService { export class DomainCatalogThumbnailService {
constructor(private domainCatalogService: DomainCatalogService) {} constructor(private domainCatalogService: DomainCatalogService) {}
@memorize() @memorize()
getThumnaulUrl({ ean, height, width }: { width?: number; height?: number; ean?: string }) { getThumnaulUrl({
return this.domainCatalogService.getSettings().pipe( ean,
map((settings) => { height,
let thumbnailUrl = settings.imageUrl.replace(/{ean}/, ean); width,
return thumbnailUrl; }: {
}), width?: number;
shareReplay(), height?: number;
); ean?: string;
} }) {
} return this.domainCatalogService.getSettings().pipe(
map((settings) => {
const thumbnailUrl = settings.imageUrl.replace(/{ean}/, ean);
return thumbnailUrl;
}),
shareReplay(),
);
}
}

View File

@@ -1,29 +1,15 @@
import { ModuleWithProviders, NgModule } from '@angular/core'; import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
import { StoreModule } from '@ngrx/store'; import { provideEffects } from '@ngrx/effects';
import { DomainCheckoutService } from './checkout.service'; import { provideState } from '@ngrx/store';
import { domainCheckoutReducer } from './store/domain-checkout.reducer'; import { DomainCheckoutService } from './checkout.service';
import { storeFeatureName } from './store/domain-checkout.state'; import { DomainCheckoutEffects } from './store/domain-checkout.effects';
import { EffectsModule } from '@ngrx/effects'; import { domainCheckoutReducer } from './store/domain-checkout.reducer';
import { DomainCheckoutEffects } from './store/domain-checkout.effects'; import { storeFeatureName } from './store/domain-checkout.state';
@NgModule({ export function provideDomainCheckout(): EnvironmentProviders {
declarations: [], return makeEnvironmentProviders([
imports: [StoreModule.forFeature(storeFeatureName, domainCheckoutReducer)], provideState({ name: storeFeatureName, reducer: domainCheckoutReducer }),
providers: [DomainCheckoutService], provideEffects(DomainCheckoutEffects),
}) DomainCheckoutService,
export class DomainCheckoutModule { ]);
static forRoot(): ModuleWithProviders<DomainCheckoutModule> { }
return {
ngModule: RootDomainCheckoutModule,
providers: [DomainCheckoutService],
};
}
}
@NgModule({
imports: [
StoreModule.forFeature(storeFeatureName, domainCheckoutReducer),
EffectsModule.forFeature([DomainCheckoutEffects]),
],
})
export class RootDomainCheckoutModule {}

View File

@@ -1071,7 +1071,7 @@ export class DomainCheckoutService {
}); });
} else if (orderType === 'B2B-Versand') { } else if (orderType === 'B2B-Versand') {
const branch = await this.applicationService const branch = await this.applicationService
.getSelectedBranch$(processId) .getSelectedBranch$()
.pipe(first()) .pipe(first())
.toPromise(); .toPromise();
availability$ = availability$ =

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { InfoService } from '@generated/swagger/isa-api'; import { InfoService } from '@generated/swagger/isa-api';
@Injectable() @Injectable({ providedIn: 'root' })
export class DomainDashboardService { export class DomainDashboardService {
constructor(private readonly _infoService: InfoService) {} constructor(private readonly _infoService: InfoService) {}
feed() { feed() {
return this._infoService.InfoInfo({}); return this._infoService.InfoInfo({});
} }
} }

View File

@@ -1,12 +0,0 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { DomainDashboardService } from './dashboard.service';
@NgModule({})
export class DomainIsaModule {
static forRoot(): ModuleWithProviders<DomainIsaModule> {
return {
ngModule: DomainIsaModule,
providers: [DomainDashboardService],
};
}
}

View File

@@ -1,3 +1,2 @@
export * from './dashboard.service'; export * from './dashboard.service';
export * from './defs'; export * from './defs';
export * from './domain-isa.module';

View File

@@ -1,116 +1,130 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { AbholfachService, AutocompleteTokenDTO, QueryTokenDTO } from '@generated/swagger/oms-api'; import {
import { DateAdapter } from '@ui/common'; AbholfachService,
import { memorize } from '@utils/common'; AutocompleteTokenDTO,
import { shareReplay } from 'rxjs/operators'; QueryTokenDTO,
@Injectable() } from '@generated/swagger/oms-api';
export class DomainGoodsService { import { DateAdapter } from '@ui/common';
constructor( import { memorize } from '@utils/common';
private abholfachService: AbholfachService, import { shareReplay } from 'rxjs/operators';
private dateAdapter: DateAdapter, @Injectable({ providedIn: 'root' })
) {} export class DomainGoodsService {
constructor(
searchWareneingang(queryToken: QueryTokenDTO) { private abholfachService: AbholfachService,
return this.abholfachService.AbholfachWareneingang(queryToken); private dateAdapter: DateAdapter,
} ) {}
searchWarenausgabe(queryToken: QueryTokenDTO) { searchWareneingang(queryToken: QueryTokenDTO) {
return this.abholfachService.AbholfachWarenausgabe(queryToken); return this.abholfachService.AbholfachWareneingang(queryToken);
} }
wareneingangComplete(autocompleteToken: AutocompleteTokenDTO) { searchWarenausgabe(queryToken: QueryTokenDTO) {
return this.abholfachService.AbholfachWareneingangAutocomplete(autocompleteToken); return this.abholfachService.AbholfachWarenausgabe(queryToken);
} }
warenausgabeComplete(autocompleteToken: AutocompleteTokenDTO) { wareneingangComplete(autocompleteToken: AutocompleteTokenDTO) {
return this.abholfachService.AbholfachWarenausgabeAutocomplete(autocompleteToken); return this.abholfachService.AbholfachWareneingangAutocomplete(
} autocompleteToken,
);
getWareneingangItemByOrderNumber(orderNumber: string) { }
return this.abholfachService.AbholfachWareneingang({
filter: { all_branches: 'true', archive: 'true' }, warenausgabeComplete(autocompleteToken: AutocompleteTokenDTO) {
input: { return this.abholfachService.AbholfachWarenausgabeAutocomplete(
qs: orderNumber, autocompleteToken,
}, );
}); }
}
getWareneingangItemByOrderNumber(orderNumber: string) {
getWarenausgabeItemByOrderNumber(orderNumber: string, archive: boolean) { return this.abholfachService.AbholfachWareneingang({
return this.abholfachService.AbholfachWarenausgabe({ filter: { all_branches: 'true', archive: 'true' },
filter: { all_branches: 'true', archive: `${archive}` }, input: {
input: { qs: orderNumber,
qs: orderNumber, },
}, });
}); }
}
getWarenausgabeItemByOrderNumber(orderNumber: string, archive: boolean) {
getWarenausgabeItemByCompartment(compartmentCode: string, archive: boolean) { return this.abholfachService.AbholfachWarenausgabe({
return this.abholfachService.AbholfachWarenausgabe({ filter: { all_branches: 'true', archive: `${archive}` },
filter: { all_branches: 'true', archive: `${archive}` }, input: {
input: { qs: orderNumber,
qs: compartmentCode, },
}, });
}); }
}
getWarenausgabeItemByCompartment(compartmentCode: string, archive: boolean) {
getWareneingangItemByCustomerNumber(customerNumber: string) { return this.abholfachService.AbholfachWarenausgabe({
// Suche anhand der Kundennummer mit Status Bestellt, nachbestellt, eingetroffen, weitergeleitet intern filter: { all_branches: 'true', archive: `${archive}` },
return this.abholfachService.AbholfachWareneingang({ input: {
filter: { orderitemprocessingstatus: '16;128;8192;1048576' }, qs: compartmentCode,
input: { },
customer_no: customerNumber, });
}, }
});
} getWareneingangItemByCustomerNumber(customerNumber: string) {
// Suche anhand der Kundennummer mit Status Bestellt, nachbestellt, eingetroffen, weitergeleitet intern
list() { return this.abholfachService.AbholfachWareneingang({
const base = this.dateAdapter.today(); filter: { orderitemprocessingstatus: '16;128;8192;1048576' },
const startDate = this.dateAdapter.addCalendarDays(base, -5); input: {
const endDate = this.dateAdapter.addCalendarDays(base, 1); customer_no: customerNumber,
const queryToken: QueryTokenDTO = { },
filter: { });
orderitemprocessingstatus: '16;8192;1024;512;2048', }
estimatedshippingdate: `"${startDate.toJSON()}"-"${endDate.toJSON()}"`,
}, list() {
orderBy: [{ by: 'estimatedshippingdate' }], const base = this.dateAdapter.today();
skip: 0, const startDate = this.dateAdapter.addCalendarDays(base, -5);
take: 20, const endDate = this.dateAdapter.addCalendarDays(base, 1);
}; const queryToken: QueryTokenDTO = {
return this.searchWareneingang(queryToken); filter: {
} orderitemprocessingstatus: '16;8192;1024;512;2048',
estimatedshippingdate: `"${startDate.toJSON()}"-"${endDate.toJSON()}"`,
@memorize() },
goodsInQuerySettings() { orderBy: [{ by: 'estimatedshippingdate' }],
return this.abholfachService.AbholfachWareneingangQuerySettings().pipe(shareReplay()); skip: 0,
} take: 20,
};
@memorize() return this.searchWareneingang(queryToken);
goodsOutQuerySettings() { }
return this.abholfachService.AbholfachWarenausgabeQuerySettings().pipe(shareReplay());
} @memorize()
goodsInQuerySettings() {
goodsInList(queryToken: QueryTokenDTO) { return this.abholfachService
return this.abholfachService.AbholfachWareneingangsliste(queryToken); .AbholfachWareneingangQuerySettings()
} .pipe(shareReplay());
}
@memorize()
goodsInListQuerySettings() { @memorize()
return this.abholfachService.AbholfachWareneingangslisteQuerySettings().pipe(shareReplay()); goodsOutQuerySettings() {
} return this.abholfachService
.AbholfachWarenausgabeQuerySettings()
goodsInCleanupList() { .pipe(shareReplay());
return this.abholfachService.AbholfachAbholfachbereinigungsliste(); }
}
goodsInList(queryToken: QueryTokenDTO) {
goodsInReservationList(queryToken: QueryTokenDTO) { return this.abholfachService.AbholfachWareneingangsliste(queryToken);
return this.abholfachService.AbholfachReservierungen(queryToken); }
}
@memorize()
goodsInRemissionPreviewList() { goodsInListQuerySettings() {
return this.abholfachService.AbholfachAbholfachremissionsvorschau(); return this.abholfachService
} .AbholfachWareneingangslisteQuerySettings()
.pipe(shareReplay());
createGoodsInRemissionFromPreviewList() { }
return this.abholfachService.AbholfachCreateAbholfachremission();
} goodsInCleanupList() {
} return this.abholfachService.AbholfachAbholfachbereinigungsliste();
}
goodsInReservationList(queryToken: QueryTokenDTO) {
return this.abholfachService.AbholfachReservierungen(queryToken);
}
goodsInRemissionPreviewList() {
return this.abholfachService.AbholfachAbholfachremissionsvorschau();
}
createGoodsInRemissionFromPreviewList() {
return this.abholfachService.AbholfachCreateAbholfachremission();
}
}

View File

@@ -2,6 +2,5 @@ export * from './action-handler-services';
export * from './action-handlers'; export * from './action-handlers';
export * from './customer-order.service'; export * from './customer-order.service';
export * from './goods.service'; export * from './goods.service';
export * from './oms.module';
export * from './oms.service'; export * from './oms.service';
export * from './receipt.service'; export * from './receipt.service';

View File

@@ -1,14 +0,0 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { DomainGoodsService } from './goods.service';
import { DomainOmsService } from './oms.service';
import { DomainReceiptService } from './receipt.service';
@NgModule()
export class DomainOmsModule {
static forRoot(): ModuleWithProviders<DomainOmsModule> {
return {
ngModule: DomainOmsModule,
providers: [DomainOmsService, DomainGoodsService, DomainReceiptService],
};
}
}

View File

@@ -1,316 +1,381 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import {
BranchService, BranchService,
BuyerDTO, BuyerDTO,
ChangeStockStatusCodeValues, ChangeStockStatusCodeValues,
HistoryDTO, HistoryDTO,
NotificationChannel, NotificationChannel,
OrderCheckoutService, OrderCheckoutService,
OrderDTO, OrderDTO,
OrderItemDTO, OrderItemDTO,
OrderItemSubsetDTO, OrderItemSubsetDTO,
OrderListItemDTO, OrderListItemDTO,
OrderService, OrderService,
ReceiptService, ReceiptService,
StatusValues, StatusValues,
StockStatusCodeService, StockStatusCodeService,
ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO, ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO,
ValueTupleOfOrderItemSubsetDTOAndOrderItemSubsetDTO, ValueTupleOfOrderItemSubsetDTOAndOrderItemSubsetDTO,
VATService, VATService,
} from '@generated/swagger/oms-api'; } from '@generated/swagger/oms-api';
import { memorize } from '@utils/common'; import { memorize } from '@utils/common';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators'; import { map, shareReplay } from 'rxjs/operators';
@Injectable() @Injectable({ providedIn: 'root' })
export class DomainOmsService { export class DomainOmsService {
constructor( constructor(
private orderService: OrderService, private orderService: OrderService,
private receiptService: ReceiptService, private receiptService: ReceiptService,
private branchService: BranchService, private branchService: BranchService,
private vatService: VATService, private vatService: VATService,
private stockStatusCodeService: StockStatusCodeService, private stockStatusCodeService: StockStatusCodeService,
private _orderCheckoutService: OrderCheckoutService, private _orderCheckoutService: OrderCheckoutService,
) {} ) {}
getOrderItemsByCustomerNumber(customerNumber: string, skip: number): Observable<OrderListItemDTO[]> { getOrderItemsByCustomerNumber(
return this.orderService customerNumber: string,
.OrderGetOrdersByBuyerNumber({ buyerNumber: customerNumber, take: 20, skip }) skip: number,
.pipe(map((orders) => orders.result)); ): Observable<OrderListItemDTO[]> {
} return this.orderService
.OrderGetOrdersByBuyerNumber({
getOrder(orderId: number): Observable<OrderDTO> { buyerNumber: customerNumber,
return this.orderService.OrderGetOrder(orderId).pipe(map((o) => o.result)); take: 20,
} skip,
})
getBranches() { .pipe(map((orders) => orders.result));
return this.branchService.BranchGetBranches({}); }
}
getOrder(orderId: number): Observable<OrderDTO> {
getHistory(orderItemSubsetId: number): Observable<HistoryDTO[]> { return this.orderService.OrderGetOrder(orderId).pipe(map((o) => o.result));
return this.orderService }
.OrderGetOrderItemStatusHistory({ orderItemSubsetId })
.pipe(map((response) => response.result)); getBranches() {
} return this.branchService.BranchGetBranches({});
}
getReceipts(
orderItemSubsetIds: number[], getHistory(orderItemSubsetId: number): Observable<HistoryDTO[]> {
): Observable<ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO[]> { return this.orderService
return this.receiptService .OrderGetOrderItemStatusHistory({ orderItemSubsetId })
.ReceiptGetReceiptsByOrderItemSubset({ .pipe(map((response) => response.result));
payload: { }
receiptType: 65 as unknown as any,
ids: orderItemSubsetIds, getReceipts(
eagerLoading: 1, orderItemSubsetIds: number[],
}, ): Observable<
}) ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO[]
.pipe(map((response) => response.result)); > {
} return this.receiptService
.ReceiptGetReceiptsByOrderItemSubset({
getReorderReasons() { payload: {
return this._orderCheckoutService.OrderCheckoutGetReorderReasons().pipe(map((response) => response.result)); receiptType: 65 as unknown as any,
} ids: orderItemSubsetIds,
eagerLoading: 1,
@memorize() },
getVATs() { })
return this.vatService.VATGetVATs({}).pipe(map((response) => response.result)); .pipe(map((response) => response.result));
} }
// ttl 4 Stunden getReorderReasons() {
@memorize({ ttl: 14400000 }) return this._orderCheckoutService
getStockStatusCodes({ supplierId, eagerLoading = 0 }: { supplierId: number; eagerLoading?: number }) { .OrderCheckoutGetReorderReasons()
return this.stockStatusCodeService.StockStatusCodeGetStockStatusCodes({ supplierId, eagerLoading }).pipe( .pipe(map((response) => response.result));
map((response) => response.result), }
shareReplay(),
); @memorize()
} getVATs() {
return this.vatService
patchOrderItem(payload: { orderItemId: number; orderId: number; orderItem: Partial<OrderItemDTO> }) { .VATGetVATs({})
return this.orderService.OrderPatchOrderItem(payload).pipe(map((response) => response.result)); .pipe(map((response) => response.result));
} }
patchOrderItemSubset(payload: { // ttl 4 Stunden
orderItemSubsetId: number; @memorize({ ttl: 14400000 })
orderItemId: number; getStockStatusCodes({
orderId: number; supplierId,
orderItemSubset: Partial<OrderItemSubsetDTO>; eagerLoading = 0,
}) { }: {
return this.orderService.OrderPatchOrderItemSubset(payload).pipe(map((response) => response.result)); supplierId: number;
} eagerLoading?: number;
}) {
patchComment({ return this.stockStatusCodeService
orderId, .StockStatusCodeGetStockStatusCodes({ supplierId, eagerLoading })
orderItemId, .pipe(
orderItemSubsetId, map((response) => response.result),
specialComment, shareReplay(),
}: { );
orderId: number; }
orderItemId: number;
orderItemSubsetId: number; patchOrderItem(payload: {
specialComment: string; orderItemId: number;
}) { orderId: number;
return this.orderService orderItem: Partial<OrderItemDTO>;
.OrderPatchOrderItemSubset({ }) {
orderId, return this.orderService
orderItemId, .OrderPatchOrderItem(payload)
orderItemSubsetId, .pipe(map((response) => response.result));
orderItemSubset: { }
specialComment,
}, patchOrderItemSubset(payload: {
}) orderItemSubsetId: number;
.pipe(map((response) => response.result)); orderItemId: number;
} orderId: number;
orderItemSubset: Partial<OrderItemSubsetDTO>;
changeOrderStatus( }) {
orderId: number, return this.orderService
orderItemId: number, .OrderPatchOrderItemSubset(payload)
orderItemSubsetId: number, .pipe(map((response) => response.result));
data: StatusValues, }
): Observable<ValueTupleOfOrderItemSubsetDTOAndOrderItemSubsetDTO> {
return this.orderService patchComment({
.OrderChangeStatus({ orderId,
data, orderItemId,
orderId, orderItemSubsetId,
orderItemId, specialComment,
orderItemSubsetId, }: {
}) orderId: number;
.pipe(map((o) => o.result)); orderItemId: number;
} orderItemSubsetId: number;
specialComment: string;
setEstimatedShippingDate( }) {
orderId: number, return this.orderService
orderItemId: number, .OrderPatchOrderItemSubset({
orderItemSubsetId: number, orderId,
estimatedShippingDate: Date | string, orderItemId,
) { orderItemSubsetId,
return this.orderService orderItemSubset: {
.OrderPatchOrderItemSubset({ specialComment,
orderId, },
orderItemId, })
orderItemSubsetId, .pipe(map((response) => response.result));
orderItemSubset: { }
estimatedShippingDate:
estimatedShippingDate instanceof Date ? estimatedShippingDate.toJSON() : estimatedShippingDate, changeOrderStatus(
}, orderId: number,
}) orderItemId: number,
.pipe(map((response) => response.result)); orderItemSubsetId: number,
} data: StatusValues,
): Observable<ValueTupleOfOrderItemSubsetDTOAndOrderItemSubsetDTO> {
setPickUpDeadline(orderId: number, orderItemId: number, orderItemSubsetId: number, pickUpDeadline: string) { return this.orderService
return this.orderService .OrderChangeStatus({
.OrderPatchOrderItemSubset({ data,
orderId, orderId,
orderItemId, orderItemId,
orderItemSubsetId, orderItemSubsetId,
orderItemSubset: { })
compartmentStop: pickUpDeadline, .pipe(map((o) => o.result));
}, }
})
.pipe(map((response) => response.result)); setEstimatedShippingDate(
} orderId: number,
orderItemId: number,
setPreferredPickUpDate({ data }: { data: { [key: string]: string } }) { orderItemSubsetId: number,
return this.orderService.OrderSetPreferredPickUpDate({ data }); estimatedShippingDate: Date | string,
} ) {
return this.orderService
changeOrderItemStatus(data: OrderService.OrderChangeStatusParams) { .OrderPatchOrderItemSubset({
return this.orderService.OrderChangeStatus(data); orderId,
} orderItemId,
orderItemSubsetId,
changeStockStatusCode(payload: ChangeStockStatusCodeValues[]) { orderItemSubset: {
return this.orderService.OrderChangeStockStatusCode(payload).pipe(map((response) => response.result)); estimatedShippingDate:
} estimatedShippingDate instanceof Date
? estimatedShippingDate.toJSON()
orderAtSupplier({ : estimatedShippingDate,
orderId, },
orderItemId, })
orderItemSubsetId, .pipe(map((response) => response.result));
}: { }
orderId: number;
orderItemId: number; setPickUpDeadline(
orderItemSubsetId: number; orderId: number,
}) { orderItemId: number,
return this._orderCheckoutService.OrderCheckoutOrderSubsetItemAtSupplier({ orderItemSubsetId: number,
orderId, pickUpDeadline: string,
orderItemId, ) {
orderItemSubsetId, return this.orderService
}); .OrderPatchOrderItemSubset({
} orderId,
orderItemId,
getNotifications(orderId: number): Observable<{ selected: NotificationChannel; email: string; mobile: string }> { orderItemSubsetId,
return this.getOrder(orderId).pipe( orderItemSubset: {
map((order) => ({ compartmentStop: pickUpDeadline,
selected: order.notificationChannels, },
email: order.buyer?.communicationDetails?.email, })
mobile: order.buyer?.communicationDetails?.mobile, .pipe(map((response) => response.result));
})), }
);
} setPreferredPickUpDate({ data }: { data: { [key: string]: string } }) {
return this.orderService.OrderSetPreferredPickUpDate({ data });
getOrderSource(orderId: number): Observable<string> { }
return this.getOrder(orderId).pipe(map((order) => order?.features?.orderSource));
} changeOrderItemStatus(data: OrderService.OrderChangeStatusParams) {
return this.orderService.OrderChangeStatus(data);
updateNotifications(orderId: number, changes: { selected: NotificationChannel; email: string; mobile: string }) { }
const communicationDetails = {
email: changes.email, changeStockStatusCode(payload: ChangeStockStatusCodeValues[]) {
mobile: changes.mobile, return this.orderService
}; .OrderChangeStockStatusCode(payload)
.pipe(map((response) => response.result));
if (!(changes.selected & 1)) { }
delete communicationDetails.email;
} orderAtSupplier({
if (!(changes.selected & 2)) { orderId,
delete communicationDetails.mobile; orderItemId,
} orderItemSubsetId,
}: {
return this.updateOrder({ orderId, notificationChannels: changes.selected, communicationDetails }); orderId: number;
} orderItemId: number;
orderItemSubsetId: number;
updateOrder({ }) {
orderId, return this._orderCheckoutService.OrderCheckoutOrderSubsetItemAtSupplier({
notificationChannels, orderId,
communicationDetails, orderItemId,
firstName, orderItemSubsetId,
lastName, });
organisation, }
}: {
orderId: number; getNotifications(
notificationChannels?: NotificationChannel; orderId: number,
communicationDetails?: { email?: string; mobile?: string }; ): Observable<{
lastName?: string; selected: NotificationChannel;
firstName?: string; email: string;
organisation?: string; mobile: string;
}) { }> {
const buyer: BuyerDTO = {}; return this.getOrder(orderId).pipe(
map((order) => ({
if (communicationDetails) { selected: order.notificationChannels,
buyer.communicationDetails = { ...communicationDetails }; email: order.buyer?.communicationDetails?.email,
} mobile: order.buyer?.communicationDetails?.mobile,
})),
if (!!lastName || !!firstName) { );
buyer.lastName = lastName; }
buyer.firstName = firstName;
} getOrderSource(orderId: number): Observable<string> {
return this.getOrder(orderId).pipe(
if (!!organisation && !!buyer.organisation) { map((order) => order?.features?.orderSource),
buyer.organisation = { );
name: organisation, }
};
} updateNotifications(
orderId: number,
return this.orderService changes: { selected: NotificationChannel; email: string; mobile: string },
.OrderPatchOrder({ ) {
orderId: orderId, const communicationDetails = {
order: { email: changes.email,
notificationChannels, mobile: changes.mobile,
buyer, };
},
}) if (!(changes.selected & 1)) {
.pipe(map((res) => res.result)); delete communicationDetails.email;
} }
if (!(changes.selected & 2)) {
generateNotifications({ orderId, taskTypes }: { orderId: number; taskTypes: string[] }) { delete communicationDetails.mobile;
return this.orderService.OrderRegenerateOrderItemStatusTasks({ }
orderId,
taskTypes, return this.updateOrder({
}); orderId,
} notificationChannels: changes.selected,
communicationDetails,
getCompletedTasks({ });
orderId, }
orderItemId,
orderItemSubsetId, updateOrder({
take, orderId,
skip, notificationChannels,
}: { communicationDetails,
orderId: number; firstName,
orderItemId: number; lastName,
orderItemSubsetId: number; organisation,
take?: number; }: {
skip?: number; orderId: number;
}): Observable<Record<string, Date[]>> { notificationChannels?: NotificationChannel;
return this.orderService communicationDetails?: { email?: string; mobile?: string };
.OrderGetOrderItemSubsetTasks({ lastName?: string;
orderId, firstName?: string;
orderItemId, organisation?: string;
orderItemSubsetId, }) {
completed: new Date(0).toISOString(), const buyer: BuyerDTO = {};
take,
skip, if (communicationDetails) {
}) buyer.communicationDetails = { ...communicationDetails };
.pipe( }
map((res) =>
res.result if (!!lastName || !!firstName) {
.sort((a, b) => new Date(b.completed).getTime() - new Date(a.completed).getTime()) buyer.lastName = lastName;
.reduce( buyer.firstName = firstName;
(data, result) => { }
(data[result.name] = data[result.name] || []).push(new Date(result.completed));
return data; if (!!organisation && !!buyer.organisation) {
}, buyer.organisation = {
{} as Record<string, Date[]>, name: organisation,
), };
), }
);
} return this.orderService
} .OrderPatchOrder({
orderId: orderId,
order: {
notificationChannels,
buyer,
},
})
.pipe(map((res) => res.result));
}
generateNotifications({
orderId,
taskTypes,
}: {
orderId: number;
taskTypes: string[];
}) {
return this.orderService.OrderRegenerateOrderItemStatusTasks({
orderId,
taskTypes,
});
}
getCompletedTasks({
orderId,
orderItemId,
orderItemSubsetId,
take,
skip,
}: {
orderId: number;
orderItemId: number;
orderItemSubsetId: number;
take?: number;
skip?: number;
}): Observable<Record<string, Date[]>> {
return this.orderService
.OrderGetOrderItemSubsetTasks({
orderId,
orderItemId,
orderItemSubsetId,
completed: new Date(0).toISOString(),
take,
skip,
})
.pipe(
map((res) =>
res.result
.sort(
(a, b) =>
new Date(b.completed).getTime() -
new Date(a.completed).getTime(),
)
.reduce(
(data, result) => {
(data[result.name] = data[result.name] || []).push(
new Date(result.completed),
);
return data;
},
{} as Record<string, Date[]>,
),
),
);
}
}

View File

@@ -1,22 +1,25 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ReceiptOrderItemSubsetReferenceValues, ReceiptService } from '@generated/swagger/oms-api'; import {
import { memorize } from '@utils/common'; ReceiptOrderItemSubsetReferenceValues,
import { shareReplay } from 'rxjs/operators'; ReceiptService,
} from '@generated/swagger/oms-api';
@Injectable() import { memorize } from '@utils/common';
export class DomainReceiptService { import { shareReplay } from 'rxjs/operators';
constructor(private receiptService: ReceiptService) {}
@Injectable({ providedIn: 'root' })
createShippingNotes(params: ReceiptService.ReceiptCreateShippingNote2Params) { export class DomainReceiptService {
return this.receiptService.ReceiptCreateShippingNote2(params); constructor(private receiptService: ReceiptService) {}
}
createShippingNotes(params: ReceiptService.ReceiptCreateShippingNote2Params) {
@memorize({ ttl: 1000 }) return this.receiptService.ReceiptCreateShippingNote2(params);
getReceipts(payload: ReceiptOrderItemSubsetReferenceValues) { }
return this.receiptService
.ReceiptGetReceiptsByOrderItemSubset({ @memorize({ ttl: 1000 })
payload: payload, getReceipts(payload: ReceiptOrderItemSubsetReferenceValues) {
}) return this.receiptService
.pipe(shareReplay(1)); .ReceiptGetReceiptsByOrderItemSubset({
} payload: payload,
} })
.pipe(shareReplay(1));
}
}

View File

@@ -1,4 +1,3 @@
export * from './defs'; export * from './defs';
export * from './mappings'; export * from './mappings';
export * from './remission.module';
export * from './remission.service'; export * from './remission.service';

View File

@@ -1,17 +0,0 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { DomainRemissionService } from './remission.service';
@NgModule({})
export class DomainRemissionModule {
static forRoot(): ModuleWithProviders<DomainRemissionModule> {
return {
ngModule: RootDomainRemissionModule,
};
}
}
@NgModule({
imports: [],
providers: [DomainRemissionService],
})
export class RootDomainRemissionModule {}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,43 @@
import { enableProdMode, isDevMode } from "@angular/core"; import { enableProdMode, isDevMode } from '@angular/core';
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; import { CONFIG_DATA } from '@isa/core/config';
import { CONFIG_DATA } from "@isa/core/config"; import { setDefaultOptions } from 'date-fns';
import { setDefaultOptions } from "date-fns"; import { de } from 'date-fns/locale';
import { de } from "date-fns/locale"; import localeDe from '@angular/common/locales/de';
import * as moment from "moment"; import localeDeExtra from '@angular/common/locales/extra/de';
import "moment/locale/de"; import * as moment from 'moment';
import 'moment/locale/de';
setDefaultOptions({ locale: de });
moment.locale("de"); setDefaultOptions({ locale: de });
moment.locale('de');
import { AppModule } from "./app/app.module";
registerLocaleData(localeDe, localeDeExtra);
if (!isDevMode()) { registerLocaleData(localeDe, 'de', localeDeExtra);
enableProdMode();
} import { App } from './app/app';
import { appConfig } from './app/app.config';
async function bootstrap() { import { bootstrapApplication } from '@angular/platform-browser';
const configRes = await fetch("/config/config.json"); import { registerLocaleData } from '@angular/common';
const config = await configRes.json(); if (!isDevMode()) {
enableProdMode();
platformBrowserDynamic([ }
{ provide: CONFIG_DATA, useValue: config },
]).bootstrapModule(AppModule); async function bootstrap() {
} const configRes = await fetch('/config/config.json');
try { const config = await configRes.json();
bootstrap();
} catch (error) { await bootstrapApplication(App, {
console.error(error); ...appConfig,
} providers: [
{ provide: CONFIG_DATA, useValue: config },
...appConfig.providers,
],
});
}
try {
bootstrap();
} catch (error) {
console.error(error);
}

View File

@@ -26,7 +26,7 @@ export class ModalAvailabilitiesComponent {
item = this.modalRef.data.item; item = this.modalRef.data.item;
itemId = this.modalRef.data.itemId || this.modalRef.data.item.id; itemId = this.modalRef.data.itemId || this.modalRef.data.item.id;
userbranch$ = combineLatest([ userbranch$ = combineLatest([
this.applicationService.getSelectedBranch$(this.applicationService.activatedProcessId), this.applicationService.getSelectedBranch$(),
this.domainAvailabilityService.getDefaultBranch(), this.domainAvailabilityService.getDefaultBranch(),
]).pipe(map(([selectedBranch, defaultBranch]) => selectedBranch || defaultBranch)); ]).pipe(map(([selectedBranch, defaultBranch]) => selectedBranch || defaultBranch));

View File

@@ -192,11 +192,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
}), }),
); );
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe( selectedBranchId$ = this.applicationService.getSelectedBranch$();
switchMap((processId) =>
this.applicationService.getSelectedBranch$(processId),
),
);
get isTablet$() { get isTablet$() {
return this._environment.matchTablet$; return this._environment.matchTablet$;
@@ -328,7 +324,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
debounceTime(0), debounceTime(0),
switchMap((params) => switchMap((params) =>
this.applicationService this.applicationService
.getSelectedBranch$(Number(params.processId)) .getSelectedBranch$()
.pipe(map((selectedBranch) => ({ params, selectedBranch }))), .pipe(map((selectedBranch) => ({ params, selectedBranch }))),
), ),
) )

View File

@@ -98,11 +98,9 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
); );
this.subscriptions.add( this.subscriptions.add(
this.application.activatedProcessId$ this.application
.pipe( .getSelectedBranch$()
debounceTime(0), .pipe(debounceTime(0))
switchMap((processId) => this.application.getSelectedBranch$(processId)),
)
.subscribe((selectedBranch) => { .subscribe((selectedBranch) => {
const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id; const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id;
if (branchChanged) { if (branchChanged) {
@@ -143,7 +141,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
const clean = { ...params }; const clean = { ...params };
for (const key in clean) { for (const key in clean) {
if (key === 'main_qs' || key?.includes('order_by')) { if (key === 'main_qs') {
clean[key] = undefined; clean[key] = undefined;
} else if (key?.includes('order_by')) { } else if (key?.includes('order_by')) {
delete clean[key]; delete clean[key];

View File

@@ -40,7 +40,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
readonly item$ = this.select((s) => s.item); readonly item$ = this.select((s) => s.item);
@Input() selected: boolean = false; @Input() selected = false;
@Input() @Input()
get selectable() { get selectable() {
@@ -91,9 +91,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
defaultBranch$ = this._availability.getDefaultBranch(); defaultBranch$ = this._availability.getDefaultBranch();
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe( selectedBranchId$ = this.applicationService.getSelectedBranch$();
switchMap((processId) => this.applicationService.getSelectedBranch$(processId)),
);
isOrderBranch$ = combineLatest([this.defaultBranch$, this.selectedBranchId$]).pipe( isOrderBranch$ = combineLatest([this.defaultBranch$, this.selectedBranchId$]).pipe(
map(([defaultBranch, selectedBranch]) => { map(([defaultBranch, selectedBranch]) => {

View File

@@ -157,7 +157,7 @@ export class ArticleSearchResultsComponent
.pipe( .pipe(
debounceTime(0), debounceTime(0),
switchMap(([processId, queryParams]) => switchMap(([processId, queryParams]) =>
this.application.getSelectedBranch$(processId).pipe( this.application.getSelectedBranch$().pipe(
map((selectedBranch) => ({ map((selectedBranch) => ({
processId, processId,
queryParams, queryParams,

View File

@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { DomainCatalogModule } from '@domain/catalog'; import { ThumbnailUrlPipe } from '@domain/catalog';
import { UiCommonModule } from '@ui/common'; import { UiCommonModule } from '@ui/common';
import { UiIconModule } from '@ui/icon'; import { UiIconModule } from '@ui/icon';
import { UiSelectBulletModule } from '@ui/select-bullet'; import { UiSelectBulletModule } from '@ui/select-bullet';
@@ -26,7 +26,7 @@ import { MatomoModule } from 'ngx-matomo-client';
CommonModule, CommonModule,
FormsModule, FormsModule,
RouterModule, RouterModule,
DomainCatalogModule, ThumbnailUrlPipe,
UiCommonModule, UiCommonModule,
UiIconModule, UiIconModule,
UiSelectBulletModule, UiSelectBulletModule,

View File

@@ -77,9 +77,7 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
ngOnInit() { ngOnInit() {
this.activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId))); this.activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId)));
this.selectedBranch$ = this.activatedProcessId$.pipe( this.selectedBranch$ = this.application.getSelectedBranch$();
switchMap((processId) => this.application.getSelectedBranch$(Number(processId))),
);
this.stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranch$]).pipe( this.stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranch$]).pipe(
map(([defaultBranch, selectedBranch]) => { map(([defaultBranch, selectedBranch]) => {

View File

@@ -1,28 +1,28 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { BranchSelectorComponent } from '@shared/components/branch-selector'; import { BranchSelectorComponent } from '@shared/components/branch-selector';
import { BreadcrumbModule } from '@shared/components/breadcrumb'; import { BreadcrumbModule } from '@shared/components/breadcrumb';
import { ArticleDetailsModule } from './article-details/article-details.module'; import { ArticleDetailsModule } from './article-details/article-details.module';
import { ArticleSearchModule } from './article-search/article-search.module'; import { ArticleSearchModule } from './article-search/article-search.module';
import { PageCatalogRoutingModule } from './page-catalog-routing.module'; import { PageCatalogRoutingModule } from './page-catalog-routing.module';
import { PageCatalogComponent } from './page-catalog.component'; import { PageCatalogComponent } from './page-catalog.component';
import { SharedSplitscreenComponent } from '@shared/components/splitscreen'; import { SharedSplitscreenComponent } from '@shared/components/splitscreen';
import { UiCommonModule } from '@ui/common'; import { UiCommonModule } from '@ui/common';
import { UiTooltipModule } from '@ui/tooltip'; import { UiTooltipModule } from '@ui/tooltip';
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
PageCatalogRoutingModule, PageCatalogRoutingModule,
ArticleSearchModule, ArticleDetailsModule,
ArticleDetailsModule, ArticleSearchModule,
BreadcrumbModule, BreadcrumbModule,
BranchSelectorComponent, BranchSelectorComponent,
SharedSplitscreenComponent, SharedSplitscreenComponent,
UiCommonModule, UiCommonModule,
UiTooltipModule, UiTooltipModule,
], ],
exports: [], exports: [],
declarations: [PageCatalogComponent], declarations: [PageCatalogComponent],
}) })
export class PageCatalogModule {} export class PageCatalogModule {}

View File

@@ -73,11 +73,9 @@ export class CustomerOrderSearchMainComponent implements OnInit, OnDestroy {
); );
this._subscriptions.add( this._subscriptions.add(
this._application.activatedProcessId$ this._application
.pipe( .getSelectedBranch$()
debounceTime(0), .pipe(debounceTime(0))
switchMap((processId) => this._application.getSelectedBranch$(processId)),
)
.subscribe((selectedBranch) => { .subscribe((selectedBranch) => {
const branchChanged = selectedBranch?.id !== this._customerOrderSearchStore?.selectedBranch?.id; const branchChanged = selectedBranch?.id !== this._customerOrderSearchStore?.selectedBranch?.id;
if (branchChanged) { if (branchChanged) {

View File

@@ -183,7 +183,7 @@ export class CustomerOrderSearchResultsComponent
debounceTime(150), debounceTime(150),
switchMap(([processId, params]) => switchMap(([processId, params]) =>
this._application this._application
.getSelectedBranch$(processId) .getSelectedBranch$()
.pipe(map((selectedBranch) => ({ processId, params, selectedBranch }))), .pipe(map((selectedBranch) => ({ processId, params, selectedBranch }))),
), ),
) )

View File

@@ -49,9 +49,7 @@ export class CustomerOrderComponent implements OnInit, AfterViewInit, OnDestroy
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.selectedBranch$ = this.application.activatedProcessId$.pipe( this.selectedBranch$ = this.application.getSelectedBranch$();
switchMap((processId) => this.application.getSelectedBranch$(Number(processId))),
);
/* Ticket #4544 - Suchrequest abbrechen bei Prozesswechsel /* Ticket #4544 - Suchrequest abbrechen bei Prozesswechsel
/ um zu verhindern, dass die Suche in einen anderen Kundenbestellungen Prozess übernommen wird / um zu verhindern, dass die Suche in einen anderen Kundenbestellungen Prozess übernommen wird

View File

@@ -1,110 +1,113 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Icon, IconAlias, IconConfig } from './interfaces'; import { Icon, IconAlias, IconConfig } from './interfaces';
import { IconLoader } from './loader'; import { IconLoader } from './loader';
import { Observable, Subject, isObservable } from 'rxjs'; import { Observable, Subject, isObservable } from 'rxjs';
@Injectable() /**
export class IconRegistry { * @deprecated Use UiIconModule from '@isa/ui/icon' instead.
private _icons = new Map<string, Icon>(); */
private _aliases = new Map<string, string>(); @Injectable({ providedIn: 'root' })
private _fallback: string; export class IconRegistry {
private _viewBox: string; private _icons = new Map<string, Icon>();
private _aliases = new Map<string, string>();
updated = new Subject<void>(); private _fallback: string;
private _viewBox: string;
private _initComplete = false;
updated = new Subject<void>();
constructor(private _iconLoader: IconLoader) {
this._loadIcons(); private _initComplete = false;
}
constructor(private _iconLoader: IconLoader) {
private async _loadIcons(): Promise<void> { this._loadIcons();
const load = this._iconLoader.getIcons(); }
if (load instanceof Promise) { private async _loadIcons(): Promise<void> {
const config = await load; const load = this._iconLoader.getIcons();
this._init(config);
} else if (isObservable(load)) { if (load instanceof Promise) {
load.subscribe((config) => { const config = await load;
this._init(config); this._init(config);
}); } else if (isObservable(load)) {
} else { load.subscribe((config) => {
this._init(load); this._init(config);
} });
} } else {
this._init(load);
private _init(config: IconConfig): void { }
this.register(...config.icons); }
this.alias(...config.aliases);
this.setViewBox(config.viewBox); private _init(config: IconConfig): void {
this.setFallback(config.fallback); this.register(...config.icons);
this.alias(...config.aliases);
this._initComplete = true; this.setViewBox(config.viewBox);
this.setFallback(config.fallback);
this.updated.next();
} this._initComplete = true;
register(...icons: Icon[]): IconRegistry { this.updated.next();
icons?.forEach((icon) => { }
this._icons.set(icon.name, icon);
}); register(...icons: Icon[]): IconRegistry {
icons?.forEach((icon) => {
return this; this._icons.set(icon.name, icon);
} });
setViewBox(viewBox: string): void { return this;
this._viewBox = viewBox; }
}
setViewBox(viewBox: string): void {
alias(...aliases: IconAlias[]): IconRegistry { this._viewBox = viewBox;
aliases?.forEach((alias) => { }
this._aliases.set(alias.alias, alias.name);
}); alias(...aliases: IconAlias[]): IconRegistry {
aliases?.forEach((alias) => {
return this; this._aliases.set(alias.alias, alias.name);
} });
setFallback(name: string): void { return this;
this._fallback = name; }
}
setFallback(name: string): void {
get(name: string): Icon | undefined { this._fallback = name;
const alias = this._aliases.get(name); }
let iconName = name;
if (alias) { get(name: string): Icon | undefined {
iconName = alias; const alias = this._aliases.get(name);
} let iconName = name;
if (alias) {
let icon = this._icons.get(iconName); iconName = alias;
}
if (!icon && this._initComplete) {
if (alias) { let icon = this._icons.get(iconName);
console.warn(`Not found: Icon with name ${name} (${iconName})`);
} else { if (!icon && this._initComplete) {
console.warn(`Unable to find icon: '${name}'`); if (alias) {
} console.warn(`Not found: Icon with name ${name} (${iconName})`);
} } else {
console.warn(`Unable to find icon: '${name}'`);
if (!icon && this._fallback) { }
icon = this._icons.get(this._fallback); }
}
if (!icon && this._fallback) {
return { ...icon, viewBox: icon?.viewBox || this._viewBox }; icon = this._icons.get(this._fallback);
} }
get$(name: string): Observable<Icon | undefined> { return { ...icon, viewBox: icon?.viewBox || this._viewBox };
return new Observable<Icon | undefined>((subscriber) => { }
let icon = this.get(name);
subscriber.next(icon); get$(name: string): Observable<Icon | undefined> {
subscriber.complete(); return new Observable<Icon | undefined>((subscriber) => {
let icon = this.get(name);
const sub = this.updated.subscribe(() => { subscriber.next(icon);
icon = this.get(name); subscriber.complete();
subscriber.next(icon);
subscriber.complete(); const sub = this.updated.subscribe(() => {
}); icon = this.get(name);
subscriber.next(icon);
return () => sub.unsubscribe(); subscriber.complete();
}); });
}
} return () => sub.unsubscribe();
});
}
}

View File

@@ -1,66 +1,75 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
SimpleChanges, SimpleChanges,
} from '@angular/core'; } from '@angular/core';
import { IconRegistry } from './icon-registry'; import { IconRegistry } from './icon-registry';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@Component({ /**
selector: 'shared-icon', * @deprecated Use UiIconModule from '@isa/ui/icon' instead.
template: ` */
<svg [style.width.rem]="size / 16" [style.height.rem]="size / 16" [attr.viewBox]="viewBox"> @Component({
<path fill="currentColor" [attr.d]="data" /> selector: 'shared-icon',
</svg> template: `
`, <svg
changeDetection: ChangeDetectionStrategy.OnPush, [style.width.rem]="size / 16"
standalone: true, [style.height.rem]="size / 16"
}) [attr.viewBox]="viewBox"
export class IconComponent implements OnInit, OnDestroy, OnChanges { >
@Input() <path fill="currentColor" [attr.d]="data" />
icon: string; </svg>
`,
data: string; changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
viewBox: string; })
export class IconComponent implements OnInit, OnDestroy, OnChanges {
@Input() @Input()
size: number = 24; icon: string;
private _onDestroy$ = new Subject<void>(); data: string;
constructor( viewBox: string;
private readonly _iconRegistry: IconRegistry,
private readonly _cdr: ChangeDetectorRef, @Input()
) {} size = 24;
ngOnInit(): void { private _onDestroy$ = new Subject<void>();
this._iconRegistry.updated.pipe(takeUntil(this._onDestroy$)).subscribe(() => {
this.updateIcon(); constructor(
}); private readonly _iconRegistry: IconRegistry,
} private readonly _cdr: ChangeDetectorRef,
) {}
ngOnDestroy(): void {
this._onDestroy$.next(); ngOnInit(): void {
this._onDestroy$.complete(); this._iconRegistry.updated
} .pipe(takeUntil(this._onDestroy$))
.subscribe(() => {
ngOnChanges(changes: SimpleChanges): void { this.updateIcon();
if (changes.icon) { });
this.updateIcon(); }
}
} ngOnDestroy(): void {
this._onDestroy$.next();
updateIcon(): void { this._onDestroy$.complete();
const icon = this._iconRegistry.get(this.icon); }
this.data = icon?.data;
this.viewBox = icon?.viewBox; ngOnChanges(changes: SimpleChanges): void {
this._cdr.markForCheck(); if (changes.icon) {
} this.updateIcon();
} }
}
updateIcon(): void {
const icon = this._iconRegistry.get(this.icon);
this.data = icon?.data;
this.viewBox = icon?.viewBox;
this._cdr.markForCheck();
}
}

View File

@@ -1,31 +1,37 @@
import { NgModule, Provider } from '@angular/core'; import { NgModule, Provider } from '@angular/core';
import { IconComponent } from './icon.component'; import { IconComponent } from './icon.component';
import { IconLoader, JsonIconLoader } from './loader'; import { IconLoader, JsonIconLoader } from './loader';
import { IconRegistry } from './icon-registry'; import { IconRegistry } from './icon-registry';
export function provideIcon(loaderProvider?: Provider) { /**
const providers: Provider[] = [IconRegistry]; * @deprecated Use UiIconModule from '@isa/ui/icon' instead.
if (!loaderProvider) { */
providers.push({ export function provideIcon(loaderProvider?: Provider) {
provide: IconLoader, const providers: Provider[] = [IconRegistry];
useClass: JsonIconLoader, if (!loaderProvider) {
}); providers.push({
} else { provide: IconLoader,
providers.push(loaderProvider); useClass: JsonIconLoader,
} });
} else {
return providers; providers.push(loaderProvider);
} }
@NgModule({ return providers;
imports: [IconComponent], }
exports: [IconComponent],
}) /**
export class IconModule { * @deprecated Use UiIconModule from '@isa/ui/icon' instead.
static forRoot(loaderProvider?: Provider) { */
return { @NgModule({
ngModule: IconModule, imports: [IconComponent],
providers: provideIcon(loaderProvider), exports: [IconComponent],
}; })
} export class IconModule {
} static forRoot(loaderProvider?: Provider) {
return {
ngModule: IconModule,
providers: provideIcon(loaderProvider),
};
}
}

View File

@@ -1,19 +1,22 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({ /**
selector: 'ui-icon-badge', * @deprecated Use UiIconModule from '@isa/ui/icon' instead.
templateUrl: 'icon-badge.component.html', */
styleUrls: ['icon-badge.component.scss'], @Component({
changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ui-icon-badge',
standalone: false, templateUrl: 'icon-badge.component.html',
}) styleUrls: ['icon-badge.component.scss'],
export class UiIconBadgeComponent { changeDetection: ChangeDetectionStrategy.OnPush,
@Input() standalone: false,
icon: string; })
export class UiIconBadgeComponent {
@Input() @Input()
alt: string; icon: string;
@Input() @Input()
area: 'customer' | 'branch' = 'customer'; alt: string;
}
@Input()
area: 'customer' | 'branch' = 'customer';
}

View File

@@ -1,59 +1,62 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { SvgIcon } from './defs'; import { SvgIcon } from './defs';
import { IconAlias } from './defs/icon-alias'; import { IconAlias } from './defs/icon-alias';
@Injectable() /**
export class IconRegistry { * @deprecated Use UiIconModule from '@isa/ui/icon' instead.
private _icons = new Map<string, SvgIcon>(); */
private _aliases = new Map<string, string>(); @Injectable({ providedIn: 'root' })
private _fallback: string; export class IconRegistry {
private _viewBox: string; private _icons = new Map<string, SvgIcon>();
private _aliases = new Map<string, string>();
register(...icons: SvgIcon[]): IconRegistry { private _fallback: string;
icons?.forEach((icon) => { private _viewBox: string;
this._icons.set(icon.name, icon);
}); register(...icons: SvgIcon[]): IconRegistry {
icons?.forEach((icon) => {
return this; this._icons.set(icon.name, icon);
} });
setViewBox(viewBox: string): void { return this;
this._viewBox = viewBox; }
}
setViewBox(viewBox: string): void {
alias(...aliases: IconAlias[]): IconRegistry { this._viewBox = viewBox;
aliases?.forEach((alias) => { }
this._aliases.set(alias.alias, alias.name);
}); alias(...aliases: IconAlias[]): IconRegistry {
aliases?.forEach((alias) => {
return this; this._aliases.set(alias.alias, alias.name);
} });
setFallback(name: string): void { return this;
this._fallback = name; }
}
setFallback(name: string): void {
get(name: string): SvgIcon | undefined { this._fallback = name;
const alias = this._aliases.get(name); }
let iconName = name;
if (alias) { get(name: string): SvgIcon | undefined {
iconName = alias; const alias = this._aliases.get(name);
} let iconName = name;
if (alias) {
let icon = this._icons.get(iconName); iconName = alias;
}
if (!icon) {
if (alias) { let icon = this._icons.get(iconName);
console.warn(`Not found: Icon with name ${name} (${iconName})`);
} else { if (!icon) {
console.warn(`Unable to find icon: '${name}'`); if (alias) {
} console.warn(`Not found: Icon with name ${name} (${iconName})`);
} } else {
console.warn(`Unable to find icon: '${name}'`);
if (!icon && this._fallback) { }
icon = this._icons.get(this._fallback); }
}
if (!icon && this._fallback) {
return { ...icon, viewBox: icon?.viewBox || this._viewBox }; icon = this._icons.get(this._fallback);
} }
}
return { ...icon, viewBox: icon?.viewBox || this._viewBox };
}
}

View File

@@ -1,29 +1,39 @@
import { Component, ChangeDetectionStrategy, Input, Optional, Inject, HostBinding } from '@angular/core'; import {
import { UI_ICON_HREF, UI_ICON_VIEW_BOX } from './tokens'; Component,
ChangeDetectionStrategy,
@Component({ Input,
selector: 'ui-icon', Optional,
templateUrl: 'icon.component.html', Inject,
styleUrls: ['icon.component.scss'], HostBinding,
changeDetection: ChangeDetectionStrategy.OnPush, } from '@angular/core';
standalone: false, import { UI_ICON_HREF, UI_ICON_VIEW_BOX } from './tokens';
})
export class UiIconComponent { /**
@Input() * @deprecated Use UiIconModule from '@isa/ui/icon' instead.
@HostBinding('attr.icon') */
icon: string; @Component({
selector: 'ui-icon',
@Input() templateUrl: 'icon.component.html',
size = '1em'; styleUrls: ['icon.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
@Input() standalone: false,
rotate = '0deg'; })
export class UiIconComponent {
constructor( @Input()
@Optional() @Inject(UI_ICON_HREF) public iconHref: string, @HostBinding('attr.icon')
@Optional() @Inject(UI_ICON_VIEW_BOX) public viewBox: string, icon: string;
) {
this.iconHref = this.iconHref || '/assets/icons.svg'; @Input()
this.viewBox = this.viewBox || '0 0 32 32'; size = '1em';
}
} @Input()
rotate = '0deg';
constructor(
@Optional() @Inject(UI_ICON_HREF) public iconHref: string,
@Optional() @Inject(UI_ICON_VIEW_BOX) public viewBox: string,
) {
this.iconHref = this.iconHref || '/assets/icons.svg';
this.viewBox = this.viewBox || '0 0 32 32';
}
}

View File

@@ -1,38 +1,52 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import {
import { IconRegistry } from './icon-registry'; ChangeDetectionStrategy,
ChangeDetectorRef,
@Component({ Component,
selector: 'ui-svg-icon', Input,
template: ` OnChanges,
<svg [style.width.rem]="size / 16" [style.height.rem]="size / 16" [attr.viewBox]="viewBox"> SimpleChanges,
<path fill="currentColor" [attr.d]="data" /> } from '@angular/core';
</svg> import { IconRegistry } from './icon-registry';
`,
changeDetection: ChangeDetectionStrategy.OnPush, /**
standalone: false, * @deprecated Use UiIconModule from '@isa/ui/icon' instead.
}) */
export class UISvgIconComponent implements OnChanges { @Component({
@Input() selector: 'ui-svg-icon',
icon: string; template: `
<svg
data: string; [style.width.rem]="size / 16"
[style.height.rem]="size / 16"
viewBox: string; [attr.viewBox]="viewBox"
>
@Input() <path fill="currentColor" [attr.d]="data" />
size: number = 24; </svg>
`,
constructor( changeDetection: ChangeDetectionStrategy.OnPush,
private readonly _iconRegistry: IconRegistry, standalone: false,
private readonly _cdr: ChangeDetectorRef, })
) {} export class UISvgIconComponent implements OnChanges {
@Input()
ngOnChanges(changes: SimpleChanges): void { icon: string;
if (changes.icon) {
const icon = this._iconRegistry.get(this.icon); data: string;
this.data = icon?.data;
this.viewBox = icon?.viewBox; viewBox: string;
this._cdr.markForCheck();
} @Input()
} size = 24;
}
constructor(
private readonly _iconRegistry: IconRegistry,
private readonly _cdr: ChangeDetectorRef,
) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes.icon) {
const icon = this._iconRegistry.get(this.icon);
this.data = icon?.data;
this.viewBox = icon?.viewBox;
this._cdr.markForCheck();
}
}
}

View File

@@ -1,57 +1,63 @@
import { ModuleWithProviders, NgModule, Provider } from '@angular/core'; import { ModuleWithProviders, NgModule, Provider } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { UiIconComponent } from './icon.component'; import { UiIconComponent } from './icon.component';
import { UiIconBadgeComponent } from './icon-badge/icon-badge.component'; import { UiIconBadgeComponent } from './icon-badge/icon-badge.component';
import { UISvgIconComponent } from './svg-icon.component'; import { UISvgIconComponent } from './svg-icon.component';
import { IconRegistry } from './icon-registry'; import { IconRegistry } from './icon-registry';
import { UI_ICON_CFG } from './tokens'; import { UI_ICON_CFG } from './tokens';
import { UiIconConfig } from './icon-config'; import { UiIconConfig } from './icon-config';
export function _rootIconRegistryFactory(config: UiIconConfig): IconRegistry { /**
const registry = new IconRegistry(); * @deprecated Use UiIconModule from '@isa/ui/icon' instead.
*/
if (config?.fallback) { export function _rootIconRegistryFactory(config: UiIconConfig): IconRegistry {
registry.setFallback(config.fallback); const registry = new IconRegistry();
}
if (config?.aliases) { if (config?.fallback) {
registry.alias(...config.aliases); registry.setFallback(config.fallback);
} }
if (config?.icons) { if (config?.aliases) {
registry.register(...config.icons); registry.alias(...config.aliases);
} }
if (config?.icons) {
if (config?.viewBox) { registry.register(...config.icons);
registry.setViewBox(config.viewBox); }
}
if (config?.viewBox) {
return registry; registry.setViewBox(config.viewBox);
} }
@NgModule({ return registry;
imports: [CommonModule], }
declarations: [UiIconComponent, UiIconBadgeComponent, UISvgIconComponent],
exports: [UiIconComponent, UiIconBadgeComponent, UISvgIconComponent], /**
}) * @deprecated Use UiIconModule from '@isa/ui/icon' instead.
export class UiIconModule { */
static forRoot(config?: UiIconConfig): ModuleWithProviders<UiIconModule> { @NgModule({
const providers: Provider[] = [ imports: [CommonModule],
{ declarations: [UiIconComponent, UiIconBadgeComponent, UISvgIconComponent],
provide: IconRegistry, exports: [UiIconComponent, UiIconBadgeComponent, UISvgIconComponent],
useFactory: _rootIconRegistryFactory, })
deps: [UI_ICON_CFG], export class UiIconModule {
}, static forRoot(config?: UiIconConfig): ModuleWithProviders<UiIconModule> {
]; const providers: Provider[] = [
{
if (config) { provide: IconRegistry,
providers.push({ useFactory: _rootIconRegistryFactory,
provide: UI_ICON_CFG, deps: [UI_ICON_CFG],
useValue: config, },
}); ];
}
if (config) {
return { providers.push({
ngModule: UiIconModule, provide: UI_ICON_CFG,
providers, useValue: config,
}; });
} }
}
return {
ngModule: UiIconModule,
providers,
};
}
}