From 803a53253cf9804d9eeb7f7ef3ea252ca9d4598b Mon Sep 17 00:00:00 2001 From: Lorenz Hilpert Date: Wed, 3 Dec 2025 14:16:47 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(isa-app):=20migrate=20to=20sta?= =?UTF-8?q?ndalone=20component=20architecture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/isa-app/src/adapter/scan/scan.module.ts | 41 +- .../scandit/scandit-scan-adapter.module.ts | 45 +- apps/isa-app/src/app/app-domain.module.ts | 19 - apps/isa-app/src/app/app-store.module.ts | 34 - apps/isa-app/src/app/app-swagger.module.ts | 40 - apps/isa-app/src/app/app.component.html | 56 +- apps/isa-app/src/app/app.component.scss | 3 - apps/isa-app/src/app/app.component.spec.ts | 137 -- apps/isa-app/src/app/app.component.ts | 411 +++-- .../src/app/{app.module.ts => app.config.ts} | 258 ++- apps/isa-app/src/app/app.css | 0 apps/isa-app/src/app/app.html | 1 + .../{app-routing.module.ts => app.routes.ts} | 30 +- apps/isa-app/src/app/app.ts | 10 + apps/isa-app/src/app/main.component.html | 3 - apps/isa-app/src/app/main.component.ts | 11 - apps/isa-app/src/app/token-login/index.ts | 1 - .../app/token-login/token-login.component.ts | 60 +- .../src/app/token-login/token-login.module.ts | 11 - .../core/application/application.module.ts | 23 - .../application.service-adapter.ts | 337 ---- .../application/application.service.spec.ts | 233 --- .../core/application/application.service.ts | 292 +++- apps/isa-app/src/core/application/index.ts | 8 +- .../application/store/application.actions.ts | 35 +- .../store/application.reducer.spec.ts | 200 --- .../application/store/application.reducer.ts | 56 - .../store/application.selectors.spec.ts | 35 - .../store/application.selectors.ts | 18 - .../application/store/application.state.ts | 13 - .../src/core/application/store/index.ts | 6 - .../core/breadcrumb/core-breadcrumb.module.ts | 37 +- .../availability/availability.module.ts | 12 - .../availability/availability.service.ts | 1384 +++++++++-------- apps/isa-app/src/domain/availability/index.ts | 1 - .../src/domain/catalog/catalog.module.ts | 18 - .../src/domain/catalog/catalog.service.ts | 216 +-- apps/isa-app/src/domain/catalog/index.ts | 1 - .../src/domain/catalog/thumbnail-url.pipe.ts | 2 +- .../src/domain/catalog/thumbnail.service.ts | 48 +- .../src/domain/checkout/checkout.module.ts | 44 +- .../src/domain/checkout/checkout.service.ts | 2 +- .../src/domain/isa/dashboard.service.ts | 22 +- .../src/domain/isa/domain-isa.module.ts | 12 - apps/isa-app/src/domain/isa/index.ts | 1 - apps/isa-app/src/domain/oms/goods.service.ts | 246 +-- apps/isa-app/src/domain/oms/index.ts | 1 - apps/isa-app/src/domain/oms/oms.module.ts | 14 - apps/isa-app/src/domain/oms/oms.service.ts | 697 +++++---- .../isa-app/src/domain/oms/receipt.service.ts | 47 +- apps/isa-app/src/domain/remission/index.ts | 1 - .../src/domain/remission/remission.module.ts | 17 - .../src/domain/remission/remission.service.ts | 1236 +++++++-------- apps/isa-app/src/main.ts | 75 +- .../availabilities.component.ts | 2 +- .../article-details.component.ts | 8 +- .../search-main/search-main.component.ts | 10 +- .../search-result-item.component.ts | 6 +- .../search-results.component.ts | 2 +- .../search-results/search-results.module.ts | 4 +- .../page/catalog/page-catalog.component.ts | 4 +- .../src/page/catalog/page-catalog.module.ts | 56 +- .../customer-order-search-main.component.ts | 8 +- ...customer-order-search-results.component.ts | 2 +- .../customer-order.component.ts | 4 +- .../shared/components/icon/icon-registry.ts | 223 +-- .../shared/components/icon/icon.component.ts | 141 +- .../src/shared/components/icon/icon.module.ts | 68 +- .../icon/icon-badge/icon-badge.component.ts | 41 +- apps/isa-app/src/ui/icon/icon-registry.ts | 121 +- apps/isa-app/src/ui/icon/icon.component.ts | 68 +- .../isa-app/src/ui/icon/svg-icon.component.ts | 90 +- apps/isa-app/src/ui/icon/ui-icon.module.ts | 120 +- 73 files changed, 3375 insertions(+), 4164 deletions(-) delete mode 100644 apps/isa-app/src/app/app-domain.module.ts delete mode 100644 apps/isa-app/src/app/app-store.module.ts delete mode 100644 apps/isa-app/src/app/app-swagger.module.ts delete mode 100644 apps/isa-app/src/app/app.component.scss delete mode 100644 apps/isa-app/src/app/app.component.spec.ts rename apps/isa-app/src/app/{app.module.ts => app.config.ts} (59%) create mode 100644 apps/isa-app/src/app/app.css create mode 100644 apps/isa-app/src/app/app.html rename apps/isa-app/src/app/{app-routing.module.ts => app.routes.ts} (84%) create mode 100644 apps/isa-app/src/app/app.ts delete mode 100644 apps/isa-app/src/app/main.component.html delete mode 100644 apps/isa-app/src/app/main.component.ts delete mode 100644 apps/isa-app/src/app/token-login/token-login.module.ts delete mode 100644 apps/isa-app/src/core/application/application.module.ts delete mode 100644 apps/isa-app/src/core/application/application.service-adapter.ts delete mode 100644 apps/isa-app/src/core/application/application.service.spec.ts delete mode 100644 apps/isa-app/src/core/application/store/application.reducer.spec.ts delete mode 100644 apps/isa-app/src/core/application/store/application.reducer.ts delete mode 100644 apps/isa-app/src/core/application/store/application.selectors.spec.ts delete mode 100644 apps/isa-app/src/core/application/store/application.selectors.ts delete mode 100644 apps/isa-app/src/core/application/store/application.state.ts delete mode 100644 apps/isa-app/src/core/application/store/index.ts delete mode 100644 apps/isa-app/src/domain/availability/availability.module.ts delete mode 100644 apps/isa-app/src/domain/catalog/catalog.module.ts delete mode 100644 apps/isa-app/src/domain/isa/domain-isa.module.ts delete mode 100644 apps/isa-app/src/domain/oms/oms.module.ts delete mode 100644 apps/isa-app/src/domain/remission/remission.module.ts diff --git a/apps/isa-app/src/adapter/scan/scan.module.ts b/apps/isa-app/src/adapter/scan/scan.module.ts index 02bd42cdd..c223aa241 100644 --- a/apps/isa-app/src/adapter/scan/scan.module.ts +++ b/apps/isa-app/src/adapter/scan/scan.module.ts @@ -1,19 +1,22 @@ -import { NgModule } from '@angular/core'; -import { DevScanAdapter } from './dev.scan-adapter'; -import { NativeScanAdapter } from './native.scan-adapter'; -import { SCAN_ADAPTER } from './tokens'; - -@NgModule({}) -export class ScanAdapterModule { - static forRoot() { - return { - ngModule: ScanAdapterModule, - providers: [ - { provide: SCAN_ADAPTER, useClass: NativeScanAdapter, multi: true }, - { provide: SCAN_ADAPTER, useClass: DevScanAdapter, multi: true }, - ], - // Use for testing: - // providers: [{ provide: SCAN_ADAPTER, useClass: dev ? DevScanAdapter : NativeScanAdapter, multi: true }], - }; - } -} +import { NgModule } from '@angular/core'; +import { DevScanAdapter } from './dev.scan-adapter'; +import { NativeScanAdapter } from './native.scan-adapter'; +import { SCAN_ADAPTER } from './tokens'; + +/** + * @deprecated Use '@isa/shared/scanner' instead. + */ +@NgModule({}) +export class ScanAdapterModule { + static forRoot() { + return { + ngModule: ScanAdapterModule, + providers: [ + { provide: SCAN_ADAPTER, useClass: NativeScanAdapter, multi: true }, + { provide: SCAN_ADAPTER, useClass: DevScanAdapter, multi: true }, + ], + // Use for testing: + // providers: [{ provide: SCAN_ADAPTER, useClass: dev ? DevScanAdapter : NativeScanAdapter, multi: true }], + }; + } +} diff --git a/apps/isa-app/src/adapter/scan/scandit/scandit-scan-adapter.module.ts b/apps/isa-app/src/adapter/scan/scandit/scandit-scan-adapter.module.ts index 7c1f659c7..7921472b5 100644 --- a/apps/isa-app/src/adapter/scan/scandit/scandit-scan-adapter.module.ts +++ b/apps/isa-app/src/adapter/scan/scandit/scandit-scan-adapter.module.ts @@ -1,20 +1,25 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { ScanditOverlayComponent } from './scandit-overlay.component'; -import { ScanditScanAdapter } from './scandit.scan-adapter'; -import { SCAN_ADAPTER } from '../tokens'; - -@NgModule({ - imports: [CommonModule], - exports: [ScanditOverlayComponent], - declarations: [ScanditOverlayComponent], -}) -export class ScanditScanAdapterModule { - static forRoot() { - return { - ngModule: ScanditScanAdapterModule, - providers: [{ provide: SCAN_ADAPTER, useClass: ScanditScanAdapter, multi: true }], - }; - } -} +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { ScanditOverlayComponent } from './scandit-overlay.component'; +import { ScanditScanAdapter } from './scandit.scan-adapter'; +import { SCAN_ADAPTER } from '../tokens'; + +/** + * @deprecated Use @isa/shared/scanner instead. + */ +@NgModule({ + imports: [CommonModule], + exports: [ScanditOverlayComponent], + declarations: [ScanditOverlayComponent], +}) +export class ScanditScanAdapterModule { + static forRoot() { + return { + ngModule: ScanditScanAdapterModule, + providers: [ + { provide: SCAN_ADAPTER, useClass: ScanditScanAdapter, multi: true }, + ], + }; + } +} diff --git a/apps/isa-app/src/app/app-domain.module.ts b/apps/isa-app/src/app/app-domain.module.ts deleted file mode 100644 index a6243517e..000000000 --- a/apps/isa-app/src/app/app-domain.module.ts +++ /dev/null @@ -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 {} diff --git a/apps/isa-app/src/app/app-store.module.ts b/apps/isa-app/src/app/app-store.module.ts deleted file mode 100644 index b82da3e29..000000000 --- a/apps/isa-app/src/app/app-store.module.ts +++ /dev/null @@ -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, -): ActionReducer { - return function (state, action) { - if (action.type === 'HYDRATE') { - return reducer(action['payload'], action); - } - return reducer(state, action); - }; -} - -export const metaReducers: MetaReducer[] = !environment.production - ? [storeInLocalStorage] - : [storeInLocalStorage]; - -@NgModule({ - imports: [ - StoreModule.forRoot(rootReducer, { metaReducers }), - EffectsModule.forRoot([]), - StoreDevtoolsModule.instrument({ - name: 'ISA Ngrx Application Store', - connectInZone: true, - }), - ], -}) -export class AppStoreModule {} diff --git a/apps/isa-app/src/app/app-swagger.module.ts b/apps/isa-app/src/app/app-swagger.module.ts deleted file mode 100644 index 28d097aa1..000000000 --- a/apps/isa-app/src/app/app-swagger.module.ts +++ /dev/null @@ -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 {} diff --git a/apps/isa-app/src/app/app.component.html b/apps/isa-app/src/app/app.component.html index 0aef4c9f5..617d1e651 100644 --- a/apps/isa-app/src/app/app.component.html +++ b/apps/isa-app/src/app/app.component.html @@ -1,28 +1,28 @@ -@if ($offlineBannerVisible()) { -
-

-
- -
- -
Sie sind offline, keine Verbindung zum Netzwerk.
-

-

Bereits geladene Ihnalte werden angezeigt, Interaktionen sind aktuell nicht möglich.

-
-} -@if ($onlineBannerVisible()) { -
-

-
- -
- -
Sie sind wieder online.
-

- -
-} - - + diff --git a/apps/isa-app/src/app/app.component.scss b/apps/isa-app/src/app/app.component.scss deleted file mode 100644 index 249c71ad1..000000000 --- a/apps/isa-app/src/app/app.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -:host { - @apply block; -} diff --git a/apps/isa-app/src/app/app.component.spec.ts b/apps/isa-app/src/app/app.component.spec.ts deleted file mode 100644 index ce54fbc2d..000000000 --- a/apps/isa-app/src/app/app.component.spec.ts +++ /dev/null @@ -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; - let config: SpyObject; - let renderer: SpyObject; - let applicationServiceMock: SpyObject; - let notificationsHubMock: SpyObject; - let swUpdateMock: SpyObject; - 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(); - // }); - // }); -}); diff --git a/apps/isa-app/src/app/app.component.ts b/apps/isa-app/src/app/app.component.ts index cf4ab1001..4fdcb8789 100644 --- a/apps/isa-app/src/app/app.component.ts +++ b/apps/isa-app/src/app/app.component.ts @@ -1,206 +1,205 @@ - -import { - Component, - effect, - HostListener, - inject, - Inject, - Injector, - OnInit, - Renderer2, - signal, - untracked, - DOCUMENT -} from '@angular/core'; -import { Title } from '@angular/platform-browser'; -import { SwUpdate } from '@angular/service-worker'; -import { ApplicationService } from '@core/application'; -import { Config } from '@core/config'; -import { NotificationsHub } from '@hub/notifications'; -import packageInfo from 'packageJson'; -import { asapScheduler, interval, Subscription } from 'rxjs'; -import { UserStateService } from '@generated/swagger/isa-api'; -import { IsaLogProvider } from './providers'; -import { EnvironmentService } from '@core/environment'; -import { AuthService, LoginStrategy } from '@core/auth'; -import { UiMessageModalComponent, UiModalService } from '@ui/modal'; -import { injectNetworkStatus } from '@isa/core/connectivity'; -import { animate, style, transition, trigger } from '@angular/animations'; - -@Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'], - animations: [ - trigger('fadeInOut', [ - transition(':enter', [ - // :enter wird ausgelöst, wenn das Element zum DOM hinzugefügt wird - style({ opacity: 0, transform: 'translateY(-100%)' }), - animate('300ms', style({ opacity: 1, transform: 'translateY(0)' })), - ]), - transition(':leave', [ - // :leave wird ausgelöst, wenn das Element aus dem DOM entfernt wird - animate('300ms', style({ opacity: 0, transform: 'translateY(-100%)' })), - ]), - ]), - ], - standalone: false, -}) -export class AppComponent implements OnInit { - readonly injector = inject(Injector); - - $networkStatus = injectNetworkStatus(); - - $offlineBannerVisible = signal(false); - - $onlineBannerVisible = signal(false); - - private onlineBannerDismissTimeout: any; - - onlineEffects = effect(() => { - const status = this.$networkStatus(); - const online = status === 'online'; - const offlineBannerVisible = this.$offlineBannerVisible(); - - untracked(() => { - this.$offlineBannerVisible.set(!online); - - if (!online) { - this.$onlineBannerVisible.set(false); - clearTimeout(this.onlineBannerDismissTimeout); - } - - if (offlineBannerVisible && online) { - this.$onlineBannerVisible.set(true); - this.onlineBannerDismissTimeout = setTimeout(() => this.$onlineBannerVisible.set(false), 5000); - } - }); - }); - - private _checkForUpdates: number = this._config.get('checkForUpdates'); - - get checkForUpdates(): number { - return this._checkForUpdates ?? 60 * 60 * 1000; // default 1 hour - } - - // For Unit Testing - set checkForUpdates(time: number) { - this._checkForUpdates = time; - } - - subscriptions = new Subscription(); - - constructor( - private readonly _config: Config, - private readonly _title: Title, - private readonly _appService: ApplicationService, - @Inject(DOCUMENT) private readonly _document: Document, - private readonly _renderer: Renderer2, - private readonly _swUpdate: SwUpdate, - private readonly _notifications: NotificationsHub, - private infoService: UserStateService, - private readonly _environment: EnvironmentService, - private readonly _authService: AuthService, - private readonly _modal: UiModalService, - ) { - this.updateClient(); - IsaLogProvider.InfoService = this.infoService; - } - - ngOnInit() { - this.setTitle(); - this.logVersion(); - asapScheduler.schedule(() => this.determinePlatform(), 250); - this._appService.getSection$().subscribe(this.sectionChangeHandler.bind(this)); - - this.setupSilentRefresh(); - } - - // Setup interval for silent refresh - setupSilentRefresh() { - const silentRefreshInterval = this._config.get('silentRefresh.interval'); - if (silentRefreshInterval > 0) { - interval(silentRefreshInterval).subscribe(() => { - if (this._authService.isAuthenticated()) { - this._authService.refresh(); - } - }); - } - } - - setTitle() { - this._title.setTitle(this._config.get('title')); - } - - logVersion() { - console.log( - `%c${this._config.get('title')}\r\nVersion: ${packageInfo.version}`, - 'font-weight: bold; font-size: 20px;', - ); - } - - determinePlatform() { - if (this._environment.isNative()) { - this._renderer.addClass(this._document.body, 'tablet-native'); - } else if (this._environment.isTablet()) { - this._renderer.addClass(this._document.body, 'tablet-browser'); - } - if (this._environment.isTablet()) { - this._renderer.addClass(this._document.body, 'tablet'); - } - if (this._environment.isDesktop()) { - this._renderer.addClass(this._document.body, 'desktop'); - } - } - - sectionChangeHandler(section: string) { - if (section === 'customer') { - this._renderer.removeClass(this._document.body, 'branch'); - this._renderer.addClass(this._document.body, 'customer'); - } else if (section === 'branch') { - this._renderer.removeClass(this._document.body, 'customer'); - this._renderer.addClass(this._document.body, 'branch'); - } - } - - updateClient() { - if (!this._swUpdate.isEnabled) { - return; - } - - this.initialCheckForUpdate(); - this.checkForUpdate(); - } - - checkForUpdate() { - interval(this._checkForUpdates).subscribe(() => { - this._swUpdate.checkForUpdate().then((value) => { - console.log('check for update', value); - if (value) { - this._notifications.updateNotification(); - } - }); - }); - } - - initialCheckForUpdate() { - this._swUpdate.checkForUpdate().then((value) => { - console.log('initial check for update', value); - if (value) { - location.reload(); - } - }); - } - - @HostListener('window:visibilitychange', ['$event']) - onVisibilityChange(event: Event) { - // refresh token when app is in background - if (this._document.hidden && this._authService.isAuthenticated()) { - this._authService.refresh(); - } else if (!this._authService.isAuthenticated()) { - const strategy = this.injector.get(LoginStrategy); - - return strategy.login('Sie sind nicht mehr angemeldet'); - } - } -} +// import { +// Component, +// effect, +// HostListener, +// inject, +// Inject, +// Injector, +// OnInit, +// Renderer2, +// signal, +// untracked, +// DOCUMENT +// } from '@angular/core'; +// import { Title } from '@angular/platform-browser'; +// import { SwUpdate } from '@angular/service-worker'; +// import { ApplicationService } from '@core/application'; +// import { Config } from '@core/config'; +// import { NotificationsHub } from '@hub/notifications'; +// import packageInfo from 'packageJson'; +// import { asapScheduler, interval, Subscription } from 'rxjs'; +// import { UserStateService } from '@generated/swagger/isa-api'; +// import { IsaLogProvider } from './providers'; +// import { EnvironmentService } from '@core/environment'; +// import { AuthService, LoginStrategy } from '@core/auth'; +// import { UiMessageModalComponent, UiModalService } from '@ui/modal'; +// import { injectNetworkStatus } from '@isa/core/connectivity'; +// import { animate, style, transition, trigger } from '@angular/animations'; + +// @Component({ +// selector: 'app-root', +// templateUrl: './app.component.html', +// styleUrls: ['./app.component.scss'], +// animations: [ +// trigger('fadeInOut', [ +// transition(':enter', [ +// // :enter wird ausgelöst, wenn das Element zum DOM hinzugefügt wird +// style({ opacity: 0, transform: 'translateY(-100%)' }), +// animate('300ms', style({ opacity: 1, transform: 'translateY(0)' })), +// ]), +// transition(':leave', [ +// // :leave wird ausgelöst, wenn das Element aus dem DOM entfernt wird +// animate('300ms', style({ opacity: 0, transform: 'translateY(-100%)' })), +// ]), +// ]), +// ], +// standalone: false, +// }) +// export class AppComponent implements OnInit { +// readonly injector = inject(Injector); + +// $networkStatus = injectNetworkStatus(); + +// $offlineBannerVisible = signal(false); + +// $onlineBannerVisible = signal(false); + +// private onlineBannerDismissTimeout: any; + +// onlineEffects = effect(() => { +// const status = this.$networkStatus(); +// const online = status === 'online'; +// const offlineBannerVisible = this.$offlineBannerVisible(); + +// untracked(() => { +// this.$offlineBannerVisible.set(!online); + +// if (!online) { +// this.$onlineBannerVisible.set(false); +// clearTimeout(this.onlineBannerDismissTimeout); +// } + +// if (offlineBannerVisible && online) { +// this.$onlineBannerVisible.set(true); +// this.onlineBannerDismissTimeout = setTimeout(() => this.$onlineBannerVisible.set(false), 5000); +// } +// }); +// }); + +// private _checkForUpdates: number = this._config.get('checkForUpdates'); + +// get checkForUpdates(): number { +// return this._checkForUpdates ?? 60 * 60 * 1000; // default 1 hour +// } + +// // For Unit Testing +// set checkForUpdates(time: number) { +// this._checkForUpdates = time; +// } + +// subscriptions = new Subscription(); + +// constructor( +// private readonly _config: Config, +// private readonly _title: Title, +// private readonly _appService: ApplicationService, +// @Inject(DOCUMENT) private readonly _document: Document, +// private readonly _renderer: Renderer2, +// private readonly _swUpdate: SwUpdate, +// private readonly _notifications: NotificationsHub, +// private infoService: UserStateService, +// private readonly _environment: EnvironmentService, +// private readonly _authService: AuthService, +// private readonly _modal: UiModalService, +// ) { +// this.updateClient(); +// IsaLogProvider.InfoService = this.infoService; +// } + +// ngOnInit() { +// this.setTitle(); +// this.logVersion(); +// asapScheduler.schedule(() => this.determinePlatform(), 250); +// this._appService.getSection$().subscribe(this.sectionChangeHandler.bind(this)); + +// this.setupSilentRefresh(); +// } + +// // Setup interval for silent refresh +// setupSilentRefresh() { +// const silentRefreshInterval = this._config.get('silentRefresh.interval'); +// if (silentRefreshInterval > 0) { +// interval(silentRefreshInterval).subscribe(() => { +// if (this._authService.isAuthenticated()) { +// this._authService.refresh(); +// } +// }); +// } +// } + +// setTitle() { +// this._title.setTitle(this._config.get('title')); +// } + +// logVersion() { +// console.log( +// `%c${this._config.get('title')}\r\nVersion: ${packageInfo.version}`, +// 'font-weight: bold; font-size: 20px;', +// ); +// } + +// determinePlatform() { +// if (this._environment.isNative()) { +// this._renderer.addClass(this._document.body, 'tablet-native'); +// } else if (this._environment.isTablet()) { +// this._renderer.addClass(this._document.body, 'tablet-browser'); +// } +// if (this._environment.isTablet()) { +// this._renderer.addClass(this._document.body, 'tablet'); +// } +// if (this._environment.isDesktop()) { +// this._renderer.addClass(this._document.body, 'desktop'); +// } +// } + +// sectionChangeHandler(section: string) { +// if (section === 'customer') { +// this._renderer.removeClass(this._document.body, 'branch'); +// this._renderer.addClass(this._document.body, 'customer'); +// } else if (section === 'branch') { +// this._renderer.removeClass(this._document.body, 'customer'); +// this._renderer.addClass(this._document.body, 'branch'); +// } +// } + +// updateClient() { +// if (!this._swUpdate.isEnabled) { +// return; +// } + +// this.initialCheckForUpdate(); +// this.checkForUpdate(); +// } + +// checkForUpdate() { +// interval(this._checkForUpdates).subscribe(() => { +// this._swUpdate.checkForUpdate().then((value) => { +// console.log('check for update', value); +// if (value) { +// this._notifications.updateNotification(); +// } +// }); +// }); +// } + +// initialCheckForUpdate() { +// this._swUpdate.checkForUpdate().then((value) => { +// console.log('initial check for update', value); +// if (value) { +// location.reload(); +// } +// }); +// } + +// @HostListener('window:visibilitychange', ['$event']) +// onVisibilityChange(event: Event) { +// // refresh token when app is in background +// if (this._document.hidden && this._authService.isAuthenticated()) { +// this._authService.refresh(); +// } else if (!this._authService.isAuthenticated()) { +// const strategy = this.injector.get(LoginStrategy); + +// return strategy.login('Sie sind nicht mehr angemeldet'); +// } +// } +// } diff --git a/apps/isa-app/src/app/app.module.ts b/apps/isa-app/src/app/app.config.ts similarity index 59% rename from apps/isa-app/src/app/app.module.ts rename to apps/isa-app/src/app/app.config.ts index 1b725dfc7..b2e0a402c 100644 --- a/apps/isa-app/src/app/app.module.ts +++ b/apps/isa-app/src/app/app.config.ts @@ -2,51 +2,51 @@ import { version } from '../../../../package.json'; import { IsaTitleStrategy } from '@isa/common/title-management'; import { HTTP_INTERCEPTORS, + HttpInterceptorFn, provideHttpClient, + withInterceptors, withInterceptorsFromDi, } from '@angular/common/http'; import { + ApplicationConfig, DEFAULT_CURRENCY_CODE, ErrorHandler, + importProvidersFrom, Injector, LOCALE_ID, - NgModule, inject, provideAppInitializer, + provideZoneChangeDetection, signal, + isDevMode, } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { PlatformModule } from '@angular/cdk/platform'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { + 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 { AuthModule, AuthService, LoginStrategy } from '@core/auth'; import { CoreCommandModule } from '@core/command'; -import { AppRoutingModule } from './app-routing.module'; -import { AppComponent } from './app.component'; -import { - ApplicationService, - ApplicationServiceAdapter, - CoreApplicationModule, -} from '@core/application'; -import { AppStoreModule } from './app-store.module'; +import { routes } from './app.routes'; + +import { rootReducer } from './store/root.reducer'; +import { RootState } from './store/root.state'; import { ServiceWorkerModule } from '@angular/service-worker'; import { environment } from '../environments/environment'; -import { AppSwaggerModule } from './app-swagger.module'; -import { AppDomainModule } from './app-domain.module'; import { UiModalModule } from '@ui/modal'; import { NotificationsHubModule, NOTIFICATIONS_HUB_OPTIONS, } from '@hub/notifications'; import { SignalRHubOptions } from '@core/signalr'; -import { CoreBreadcrumbModule } from '@core/breadcrumb'; +import { provideCoreBreadcrumb } from '@core/breadcrumb'; 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 { CoreLoggerModule, LOG_PROVIDER } from '@core/logger'; import { IsaLogProvider } from './providers'; @@ -59,7 +59,6 @@ import { import * as Commands from './commands'; import { NativeContainerService } from '@external/native-container'; import { ShellModule } from '@shared/shell'; -import { MainComponent } from './main.component'; import { IconModule } from '@shared/components/icon'; import { NgIconsModule } from '@ng-icons/core'; import { @@ -69,8 +68,7 @@ import { } from '@ng-icons/material-icons/baseline'; import { NetworkStatusService } from '@isa/core/connectivity'; import { debounceTime, filter, firstValueFrom, switchMap } from 'rxjs'; -import { provideMatomo } from 'ngx-matomo-client'; -import { withRouter, withRouteData } from 'ngx-matomo-client'; +import { provideMatomo, withRouter, withRouteData } from 'ngx-matomo-client'; import { provideLogging, withLogLevel, @@ -87,15 +85,58 @@ import { import { Store } from '@ngrx/store'; import { OAuthService } from 'angular-oauth2-oidc'; import z from 'zod'; -import { TitleStrategy } from '@angular/router'; +import { provideScrollPositionRestoration } from '@isa/utils/scroll-position'; import { TabNavigationService } from '@isa/core/tabs'; -registerLocaleData(localeDe, localeDeExtra); -registerLocaleData(localeDe, 'de', localeDeExtra); +// Domain modules +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, +): ActionReducer { + return function (state, action) { + if (action.type === 'HYDRATE') { + return reducer(action['payload'], action); + } + return reducer(state, action); + }; +} + +const metaReducers: MetaReducer[] = [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 () => { - // Get logging service for initialization logging const logger = loggerFactory(() => ({ service: 'AppInitializer' })); const statusElement = document.querySelector('#init-status'); const laoderElement = document.querySelector('#init-loader'); @@ -162,7 +203,6 @@ export function _appInitializerFactory(config: Config, injector: Injector) { await userStorage.init(); const store = injector.get(Store); - // Hydrate Ngrx Store const state = userStorage.get('store'); if (state && state['version'] === version) { 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', })); } - // Subscribe on Store changes and save to user storage + auth.initialized$ .pipe( filter((initialized) => initialized), @@ -183,7 +223,6 @@ export function _appInitializerFactory(config: Config, injector: Injector) { }); logger.info('Application initialization completed'); - // Inject tab navigation service to initialize it injector.get(TabNavigationService).init(); } catch (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, auth: AuthService, ): SignalRHubOptions { @@ -258,80 +297,151 @@ const USER_SUB_FACTORY = () => { return signal(validation.data); }; -@NgModule({ - 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 }), - ], +export const appConfig: ApplicationConfig = { 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(() => { - const initializerFn = _appInitializerFactory( + const initializerFn = appInitializerFactory( inject(Config), inject(Injector), ); return initializerFn(); }), + + // Notifications hub { provide: NOTIFICATIONS_HUB_OPTIONS, - useFactory: _notificationsHubOptionsFactory, + useFactory: notificationsHubOptionsFactory, deps: [Config, AuthService], }, + + // HTTP interceptors { provide: HTTP_INTERCEPTORS, useClass: HttpErrorInterceptor, multi: true, }, + + // Logging { provide: LOG_PROVIDER, useClass: IsaLogProvider, multi: true, }, + provideLogging( + withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Info), + withSink(ConsoleLogSink), + ), + + // Error handling { provide: ErrorHandler, useClass: IsaErrorHandler, }, - { - provide: ApplicationService, - useClass: ApplicationServiceAdapter, - }, + + // Locale settings { provide: LOCALE_ID, useValue: 'de-DE' }, - provideHttpClient(withInterceptorsFromDi()), + { provide: DEFAULT_CURRENCY_CODE, useValue: 'EUR' }, + + // Analytics provideMatomo( { trackerUrl: 'https://matomo.paragon-data.net', siteId: '1' }, withRouter(), withRouteData(), ), - provideLogging(withLogLevel(LogLevel.Debug), withSink(ConsoleLogSink)), - { - provide: DEFAULT_CURRENCY_CODE, - useValue: 'EUR', - }, + + // User storage provideUserSubFactory(USER_SUB_FACTORY), + + // Title strategy { 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 {} +}; diff --git a/apps/isa-app/src/app/app.css b/apps/isa-app/src/app/app.css new file mode 100644 index 000000000..e69de29bb diff --git a/apps/isa-app/src/app/app.html b/apps/isa-app/src/app/app.html new file mode 100644 index 000000000..67e7bd4cd --- /dev/null +++ b/apps/isa-app/src/app/app.html @@ -0,0 +1 @@ + diff --git a/apps/isa-app/src/app/app-routing.module.ts b/apps/isa-app/src/app/app.routes.ts similarity index 84% rename from apps/isa-app/src/app/app-routing.module.ts rename to apps/isa-app/src/app/app.routes.ts index cac01b1ef..8edf7c84a 100644 --- a/apps/isa-app/src/app/app-routing.module.ts +++ b/apps/isa-app/src/app/app.routes.ts @@ -1,5 +1,4 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { Routes } from '@angular/router'; import { CanActivateCartGuard, CanActivateCartWithProcessIdGuard, @@ -11,13 +10,12 @@ import { CanActivateProductWithProcessIdGuard, IsAuthenticatedGuard, } from './guards'; -import { MainComponent } from './main.component'; import { BranchSectionResolver, CustomerSectionResolver, ProcessIdResolver, } from './resolvers'; -import { TokenLoginComponent, TokenLoginModule } from './token-login'; +import { TokenLoginComponent } from './token-login'; import { ActivateProcessIdGuard, ActivateProcessIdWithConfigKeyGuard, @@ -28,9 +26,8 @@ import { processResolverFn, hasTabIdGuard, } 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: 'login', @@ -45,7 +42,6 @@ const routes: Routes = [ children: [ { path: 'kunde', - component: MainComponent, children: [ { path: 'dashboard', @@ -72,8 +68,6 @@ const routes: Routes = [ 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', loadChildren: () => @@ -122,7 +116,6 @@ const routes: Routes = [ { path: 'pickup-shelf', canActivate: [ActivateProcessIdGuard], - // NOTE: This is a workaround for the canActivate guard not being called loadChildren: () => import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule), }, @@ -141,7 +134,6 @@ const routes: Routes = [ }, { path: 'filiale', - component: MainComponent, children: [ { path: 'task-calendar', @@ -154,7 +146,6 @@ const routes: Routes = [ { path: 'pickup-shelf', canActivate: [ActivateProcessIdWithConfigKeyGuard('pickupShelf')], - // NOTE: This is a workaround for the canActivate guard not being called loadChildren: () => import('@page/pickup-shelf').then((m) => m.PickupShelfInModule), }, @@ -188,7 +179,6 @@ const routes: Routes = [ }, { path: ':tabId', - component: MainComponent, resolve: { process: processResolverFn, tab: tabResolverFn }, canActivate: [IsAuthenticatedGuard, hasTabIdGuard], children: [ @@ -218,7 +208,6 @@ const routes: Routes = [ }, ], }, - { path: 'return', 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 {} diff --git a/apps/isa-app/src/app/app.ts b/apps/isa-app/src/app/app.ts new file mode 100644 index 000000000..983d8ce97 --- /dev/null +++ b/apps/isa-app/src/app/app.ts @@ -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 {} diff --git a/apps/isa-app/src/app/main.component.html b/apps/isa-app/src/app/main.component.html deleted file mode 100644 index 2e22b20c1..000000000 --- a/apps/isa-app/src/app/main.component.html +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/isa-app/src/app/main.component.ts b/apps/isa-app/src/app/main.component.ts deleted file mode 100644 index a5e5d9e0c..000000000 --- a/apps/isa-app/src/app/main.component.ts +++ /dev/null @@ -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() {} -} diff --git a/apps/isa-app/src/app/token-login/index.ts b/apps/isa-app/src/app/token-login/index.ts index a77688b56..0505b7b2b 100644 --- a/apps/isa-app/src/app/token-login/index.ts +++ b/apps/isa-app/src/app/token-login/index.ts @@ -1,2 +1 @@ export * from './token-login.component'; -export * from './token-login.module'; diff --git a/apps/isa-app/src/app/token-login/token-login.component.ts b/apps/isa-app/src/app/token-login/token-login.component.ts index 7fa1b81bb..d50873a2f 100644 --- a/apps/isa-app/src/app/token-login/token-login.component.ts +++ b/apps/isa-app/src/app/token-login/token-login.component.ts @@ -1,29 +1,31 @@ -import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { AuthService } from '@core/auth'; - -@Component({ - selector: 'app-token-login', - templateUrl: 'token-login.component.html', - styleUrls: ['token-login.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false, -}) -export class TokenLoginComponent implements OnInit { - constructor( - private _route: ActivatedRoute, - private _authService: AuthService, - private _router: Router, - ) {} - - ngOnInit() { - if (this._route.snapshot.params.token && !this._authService.isAuthenticated()) { - this._authService.setKeyCardToken(this._route.snapshot.params.token); - this._authService.login(); - } else if (!this._authService.isAuthenticated()) { - this._authService.login(); - } else if (this._authService.isAuthenticated()) { - this._router.navigate(['/']); - } - } -} +import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AuthService } from '@core/auth'; + +@Component({ + selector: 'app-token-login', + templateUrl: 'token-login.component.html', + styleUrls: ['token-login.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TokenLoginComponent implements OnInit { + constructor( + private _route: ActivatedRoute, + private _authService: AuthService, + private _router: Router, + ) {} + + ngOnInit() { + if ( + this._route.snapshot.params.token && + !this._authService.isAuthenticated() + ) { + this._authService.setKeyCardToken(this._route.snapshot.params.token); + this._authService.login(); + } else if (!this._authService.isAuthenticated()) { + this._authService.login(); + } else if (this._authService.isAuthenticated()) { + this._router.navigate(['/']); + } + } +} diff --git a/apps/isa-app/src/app/token-login/token-login.module.ts b/apps/isa-app/src/app/token-login/token-login.module.ts deleted file mode 100644 index 3225d0cc7..000000000 --- a/apps/isa-app/src/app/token-login/token-login.module.ts +++ /dev/null @@ -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 {} diff --git a/apps/isa-app/src/core/application/application.module.ts b/apps/isa-app/src/core/application/application.module.ts deleted file mode 100644 index 844f7a451..000000000 --- a/apps/isa-app/src/core/application/application.module.ts +++ /dev/null @@ -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 { - return { - ngModule: RootCoreApplicationModule, - }; - } -} - -@NgModule({ - imports: [StoreModule.forFeature('core-application', applicationReducer)], - providers: [ApplicationService], -}) -export class RootCoreApplicationModule {} diff --git a/apps/isa-app/src/core/application/application.service-adapter.ts b/apps/isa-app/src/core/application/application.service-adapter.ts deleted file mode 100644 index 7966583d8..000000000 --- a/apps/isa-app/src/core/application/application.service-adapter.ts +++ /dev/null @@ -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 { - return this.#processes$.pipe( - map((processes) => - processes.filter((process) => - section ? process.section === section : true, - ), - ), - ); - } - - getProcessById$(processId: number): Observable { - 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 { - 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): void { - const tabChanges: { - name?: string; - tags?: string[]; - metadata?: Record; - } = {}; - - 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): 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 { - 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 { - 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 { - 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 { - 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 { - 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(tab.metadata, 'process_created') ?? - tab.createdAt, - activated: - this.getMetadataValue(tab.metadata, 'process_activated') ?? - tab.activatedAt ?? - 0, - section: - this.getMetadataValue<'customer' | 'branch'>( - tab.metadata, - 'process_section', - ) ?? 'customer', - type: this.getMetadataValue(tab.metadata, 'process_type'), - closeable: - this.getMetadataValue(tab.metadata, 'process_closeable') ?? - true, - confirmClosing: - this.getMetadataValue( - 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 | 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; - } - - return undefined; - } - - private getMetadataValue( - metadata: TabMetadata, - key: string, - ): T | undefined { - return metadata?.[key] as T | undefined; - } - - private createTimestamp(): number { - return Date.now(); - } -} diff --git a/apps/isa-app/src/core/application/application.service.spec.ts b/apps/isa-app/src/core/application/application.service.spec.ts deleted file mode 100644 index 0514f293d..000000000 --- a/apps/isa-app/src/core/application/application.service.spec.ts +++ /dev/null @@ -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; -// let store: SpyObject; -// 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()); -// }); -// }); -// }); diff --git a/apps/isa-app/src/core/application/application.service.ts b/apps/isa-app/src/core/application/application.service.ts index 07bc16b26..0bd6ff07d 100644 --- a/apps/isa-app/src/core/application/application.service.ts +++ b/apps/isa-app/src/core/application/application.service.ts @@ -1,41 +1,68 @@ -import { Injectable } from '@angular/core'; -import { Store } from '@ngrx/store'; +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 { BehaviorSubject, Observable } from 'rxjs'; -import { first, map, switchMap } from 'rxjs/operators'; -import { ApplicationProcess } from './defs'; -import { - removeProcess, - selectSection, - selectProcesses, - setSection, - addProcess, - setActivatedProcess, - selectActivatedProcess, - patchProcess, - patchProcessData, - selectTitle, - setTitle, -} from './store'; +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'; -@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 { - private activatedProcessIdSubject = new BehaviorSubject(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() { - return this.activatedProcessIdSubject.value; + return this.#tabService.activatedTabId(); } get activatedProcessId$() { - return this.activatedProcessIdSubject.asObservable(); + return this.#activatedProcessId$; } - constructor(private store: Store) {} - - getProcesses$(section?: 'customer' | 'branch') { - const processes$ = this.store.select(selectProcesses); - return processes$.pipe( + getProcesses$( + section?: 'customer' | 'branch', + ): Observable { + return this.#processes$.pipe( map((processes) => processes.filter((process) => section ? process.section === section : true, @@ -45,69 +72,96 @@ export class ApplicationService { } getProcessById$(processId: number): Observable { - return this.getProcesses$().pipe( + return this.#processes$.pipe( map((processes) => processes.find((process) => process.id === processId)), ); } - getSection$() { - return this.store.select(selectSection); + getSection$(): Observable<'customer' | 'branch'> { + return this.#section.asObservable(); } - getTitle$() { + getTitle$(): Observable<'Kundenbereich' | 'Filialbereich'> { return this.getSection$().pipe( - map((section) => { - return section === 'customer' ? 'Kundenbereich' : 'Filialbereich'; - }), + map((section) => + section === 'customer' ? 'Kundenbereich' : 'Filialbereich', + ), ); } /** @deprecated */ - getActivatedProcessId$() { - return this.store - .select(selectActivatedProcess) - .pipe(map((process) => process?.id)); + getActivatedProcessId$(): Observable { + return this.activatedProcessId$; } - activateProcess(activatedProcessId: number) { - this.store.dispatch(setActivatedProcess({ activatedProcessId })); - this.activatedProcessIdSubject.next(activatedProcessId); + activateProcess(activatedProcessId: number): void { + this.#tabService.activateTab(activatedProcessId); } - removeProcess(processId: number) { - this.store.dispatch(removeProcess({ processId })); + removeProcess(processId: number): void { + this.#tabService.removeTab(processId); + this.#store.dispatch(removeProcess({ processId })); } - patchProcess(processId: number, changes: Partial) { - this.store.dispatch(patchProcess({ processId, changes })); - } + patchProcess(processId: number, changes: Partial): void { + const tabChanges: { + name?: string; + tags?: string[]; + metadata?: Record; + } = {}; - patchProcessData(processId: number, data: Record) { - this.store.dispatch(patchProcessData({ processId, data })); - } - - getSelectedBranch$(processId?: number): Observable { - if (!processId) { - return this.activatedProcessId$.pipe( - switchMap((processId) => - this.getProcessById$(processId).pipe( - map((process) => process?.data?.selectedBranch), - ), - ), - ); + if (changes.name) { + tabChanges.name = changes.name; } - return this.getProcessById$(processId).pipe( - map((process) => process?.data?.selectedBranch), + // 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): 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 { + 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 { - const processes = await this.getProcesses$('customer') - .pipe(first()) - .toPromise(); + const processes = await firstValueFrom(this.getProcesses$('customer')); const processIds = processes .filter((x) => this.REGEX_PROCESS_NAME.test(x.name)) @@ -124,14 +178,18 @@ export class ApplicationService { }; await this.createProcess(process); - return process; } - async createProcess(process: ApplicationProcess) { - const existingProcess = await this.getProcessById$(process?.id) - .pipe(first()) - .toPromise(); + /** + * 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 { + const existingProcess = this.#tabService.entityMap()[process.id]; if (existingProcess?.id === process?.id) { throw new Error('Process Id existiert bereits'); } @@ -148,13 +206,28 @@ export class ApplicationService { process.confirmClosing = true; } - process.created = this._createTimestamp(); + process.created = this.createTimestamp(); 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') { - this.store.dispatch(setSection({ section })); + setSection(section: 'customer' | 'branch'): void { + this.#section.next(section); } 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(tab.metadata, 'process_created') ?? + tab.createdAt, + activated: + this.getMetadataValue(tab.metadata, 'process_activated') ?? + tab.activatedAt ?? + 0, + section: + this.getMetadataValue<'customer' | 'branch'>( + tab.metadata, + 'process_section', + ) ?? 'customer', + type: this.getMetadataValue(tab.metadata, 'process_type'), + closeable: + this.getMetadataValue(tab.metadata, 'process_closeable') ?? + true, + confirmClosing: + this.getMetadataValue( + 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 | 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; + } + + return undefined; + } + + private getMetadataValue( + metadata: TabMetadata, + key: string, + ): T | undefined { + return metadata?.[key] as T | undefined; + } + + private createTimestamp(): number { return Date.now(); } } diff --git a/apps/isa-app/src/core/application/index.ts b/apps/isa-app/src/core/application/index.ts index 6d8aaad14..d8685033b 100644 --- a/apps/isa-app/src/core/application/index.ts +++ b/apps/isa-app/src/core/application/index.ts @@ -1,5 +1,3 @@ -export * from './application.module'; -export * from './application.service'; -export * from './application.service-adapter'; -export * from './defs'; -export * from './store'; +export * from './application.service'; +export * from './defs'; +export * from './store/application.actions'; diff --git a/apps/isa-app/src/core/application/store/application.actions.ts b/apps/isa-app/src/core/application/store/application.actions.ts index 6cf479556..5c61a028a 100644 --- a/apps/isa-app/src/core/application/store/application.actions.ts +++ b/apps/isa-app/src/core/application/store/application.actions.ts @@ -1,27 +1,8 @@ -import { createAction, props } from '@ngrx/store'; -import { ApplicationProcess } from '..'; - -const prefix = '[CORE-APPLICATION]'; - -export const setTitle = createAction(`${prefix} Set Title`, props<{ title: string }>()); - -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 }>(), -); - -export const patchProcessData = createAction( - `${prefix} Patch Process Data`, - props<{ processId: number; data: Record }>(), -); +import { createAction, props } from '@ngrx/store'; + +const prefix = '[CORE-APPLICATION]'; + +export const removeProcess = createAction( + `${prefix} Remove Process`, + props<{ processId: number }>(), +); diff --git a/apps/isa-app/src/core/application/store/application.reducer.spec.ts b/apps/isa-app/src/core/application/store/application.reducer.spec.ts deleted file mode 100644 index ece8e09dd..000000000 --- a/apps/isa-app/src/core/application/store/application.reducer.spec.ts +++ /dev/null @@ -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(); - }); - }); -}); diff --git a/apps/isa-app/src/core/application/store/application.reducer.ts b/apps/isa-app/src/core/application/store/application.reducer.ts deleted file mode 100644 index 25cf3cbd4..000000000 --- a/apps/isa-app/src/core/application/store/application.reducer.ts +++ /dev/null @@ -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); -} diff --git a/apps/isa-app/src/core/application/store/application.selectors.spec.ts b/apps/isa-app/src/core/application/store/application.selectors.spec.ts deleted file mode 100644 index 626ffcf2d..000000000 --- a/apps/isa-app/src/core/application/store/application.selectors.spec.ts +++ /dev/null @@ -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]); -// }); -// }); diff --git a/apps/isa-app/src/core/application/store/application.selectors.ts b/apps/isa-app/src/core/application/store/application.selectors.ts deleted file mode 100644 index e1b8eef40..000000000 --- a/apps/isa-app/src/core/application/store/application.selectors.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createFeatureSelector, createSelector } from '@ngrx/store'; -import { ApplicationState } from './application.state'; -export const selectApplicationState = createFeatureSelector('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), -); diff --git a/apps/isa-app/src/core/application/store/application.state.ts b/apps/isa-app/src/core/application/store/application.state.ts deleted file mode 100644 index 58fd76f89..000000000 --- a/apps/isa-app/src/core/application/store/application.state.ts +++ /dev/null @@ -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', -}; diff --git a/apps/isa-app/src/core/application/store/index.ts b/apps/isa-app/src/core/application/store/index.ts deleted file mode 100644 index 026b1fa29..000000000 --- a/apps/isa-app/src/core/application/store/index.ts +++ /dev/null @@ -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 diff --git a/apps/isa-app/src/core/breadcrumb/core-breadcrumb.module.ts b/apps/isa-app/src/core/breadcrumb/core-breadcrumb.module.ts index 967dc4ec2..a06e816ea 100644 --- a/apps/isa-app/src/core/breadcrumb/core-breadcrumb.module.ts +++ b/apps/isa-app/src/core/breadcrumb/core-breadcrumb.module.ts @@ -1,22 +1,15 @@ -import { ModuleWithProviders, NgModule } from '@angular/core'; -import { EffectsModule } from '@ngrx/effects'; -import { StoreModule } from '@ngrx/store'; -import { BreadcrumbService } from './breadcrumb.service'; -import { BreadcrumbEffects } from './store/breadcrumb.effect'; -import { breadcrumbReducer } from './store/breadcrumb.reducer'; -import { featureName } from './store/breadcrumb.state'; - -@NgModule() -export class CoreBreadcrumbModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: CoreBreadcrumbForRootModule, - }; - } -} - -@NgModule({ - imports: [StoreModule.forFeature(featureName, breadcrumbReducer), EffectsModule.forFeature([BreadcrumbEffects])], - providers: [BreadcrumbService], -}) -export class CoreBreadcrumbForRootModule {} +import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; +import { provideEffects } from '@ngrx/effects'; +import { provideState } from '@ngrx/store'; +import { BreadcrumbService } from './breadcrumb.service'; +import { BreadcrumbEffects } from './store/breadcrumb.effect'; +import { breadcrumbReducer } from './store/breadcrumb.reducer'; +import { featureName } from './store/breadcrumb.state'; + +export function provideCoreBreadcrumb(): EnvironmentProviders { + return makeEnvironmentProviders([ + provideState({ name: featureName, reducer: breadcrumbReducer }), + provideEffects(BreadcrumbEffects), + BreadcrumbService, + ]); +} diff --git a/apps/isa-app/src/domain/availability/availability.module.ts b/apps/isa-app/src/domain/availability/availability.module.ts deleted file mode 100644 index b6af259aa..000000000 --- a/apps/isa-app/src/domain/availability/availability.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ModuleWithProviders, NgModule } from '@angular/core'; -import { DomainAvailabilityService } from './availability.service'; - -@NgModule() -export class DomainAvailabilityModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: DomainAvailabilityModule, - providers: [DomainAvailabilityService], - }; - } -} diff --git a/apps/isa-app/src/domain/availability/availability.service.ts b/apps/isa-app/src/domain/availability/availability.service.ts index 77eb3007f..aef783166 100644 --- a/apps/isa-app/src/domain/availability/availability.service.ts +++ b/apps/isa-app/src/domain/availability/availability.service.ts @@ -1,633 +1,751 @@ -import { Injectable } from '@angular/core'; -import { ItemDTO } from '@generated/swagger/cat-search-api'; -import { - AvailabilityDTO, - BranchDTO, - OLAAvailabilityDTO, - StoreCheckoutBranchService, - StoreCheckoutSupplierService, - SupplierDTO, -} from '@generated/swagger/checkout-api'; -import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs'; -import { - AvailabilityRequestDTO, - AvailabilityService, - AvailabilityDTO as SwaggerAvailabilityDTO, - AvailabilityType, -} from '@generated/swagger/availability-api'; -import { AvailabilityDTO as CatAvailabilityDTO } from '@generated/swagger/cat-search-api'; -import { map, shareReplay, switchMap, withLatestFrom, mergeMap, timeout, first } from 'rxjs/operators'; -import { isArray, memorize } from '@utils/common'; -import { LogisticianDTO, LogisticianService } from '@generated/swagger/oms-api'; -import { - ResponseArgsOfIEnumerableOfStockInfoDTO, - StockDTO, - StockInfoDTO, - StockService, -} from '@generated/swagger/inventory-api'; -import { PriceDTO } from '@generated/swagger/availability-api'; -import { AvailabilityByBranchDTO, ItemData, Ssc } from './defs'; -import { Availability } from './defs/availability'; -import { isEmpty } from 'lodash'; - -@Injectable() -export class DomainAvailabilityService { - // Ticket #3378 Keep Result List Items and Details Page SSC in sync - sscs$ = new BehaviorSubject>([]); - sscsObs$ = this.sscs$.asObservable(); - - constructor( - private _availabilityService: AvailabilityService, - private _logisticanService: LogisticianService, - private _stockService: StockService, - private _supplierService: StoreCheckoutSupplierService, - private _branchService: StoreCheckoutBranchService, - ) {} - - @memorize({ ttl: 10000 }) - memorizedAvailabilityShippingAvailability(request: Array) { - return this._availabilityService.AvailabilityShippingAvailability(request).pipe(shareReplay(1)); - } - - @memorize() - getSuppliers(): Observable { - return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe( - map((response) => response.result), - shareReplay(1), - ); - } - - @memorize() - getTakeAwaySupplier(): Observable { - return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe( - map(({ result }) => result?.find((supplier) => supplier?.supplierNumber === 'F')), - shareReplay(1), - ); - } - - @memorize() - getBranches(): Observable { - return this._branchService.StoreCheckoutBranchGetBranches({}).pipe( - map((response) => response.result), - shareReplay(1), - ); - } - - @memorize() - getStockByBranch(branchId: number): Observable { - return this._stockService.StockGetStocksByBranch({ branchId }).pipe( - map((response) => response.result), - map((result) => result?.find((_) => true)), - shareReplay(1), - ); - } - - @memorize() - getDefaultStock(): Observable { - return this._stockService.StockCurrentStock().pipe( - map((response) => response.result), - shareReplay(1), - ); - } - - @memorize() - getDefaultBranch(): Observable { - return this._stockService.StockCurrentBranch().pipe( - map((response) => ({ - id: response.result.id, - name: response.result.name, - address: response.result.address, - branchType: response.result.branchType, - branchNumber: response.result.branchNumber, - changed: response.result.changed, - created: response.result.created, - isDefault: response.result.isDefault, - isOnline: response.result.isOnline, - key: response.result.key, - label: response.result.label, - pId: response.result.pId, - shortName: response.result.shortName, - status: response.result.status, - version: response.result.version, - })), - shareReplay(1), - ); - } - - @memorize({}) - getLogisticians(): Observable { - return this._logisticanService.LogisticianGetLogisticians({}).pipe( - map((response) => response.result?.find((l) => l.logisticianNumber === '2470')), - shareReplay(1), - ); - } - - getTakeAwayAvailabilityByBranches({ - branchIds, - itemId, - price, - quantity, - }: { - branchIds: number[]; - itemId: number; - price: PriceDTO; - quantity: number; - }): Observable { - return this._stockService.StockStockRequest({ stockRequest: { branchIds, itemId } }).pipe( - map((response) => response.result), - withLatestFrom(this.getTakeAwaySupplier()), - map(([result, supplier]) => { - const availabilities: AvailabilityByBranchDTO[] = result.map((stockInfo) => { - return { - availableQuantity: stockInfo.availableQuantity, - availabilityType: quantity <= stockInfo.inStock ? 1024 : 1, // 1024 (=Available) - inStock: stockInfo.inStock, - supplierSSC: quantity <= stockInfo.inStock ? '999' : '', - supplierSSCText: quantity <= stockInfo.inStock ? 'Filialentnahme' : '', - price, - supplier: { id: supplier?.id }, - branchId: stockInfo.branchId, - }; - }); - return availabilities; - }), - shareReplay(1), - ); - } - - @memorize({ ttl: 10000 }) - getTakeAwayAvailability({ - item, - quantity, - branch, - }: { - item: ItemData; - quantity: number; - branch?: BranchDTO; - }): Observable { - const request = branch ? this.getStockByBranch(branch.id) : this.getDefaultStock(); - return request.pipe( - switchMap((s) => - combineLatest([ - this._stockService.StockInStock({ articleIds: [item.itemId], stockId: s.id }), - this.getTakeAwaySupplier(), - this.getDefaultBranch(), - ]), - ), - map(([response, supplier, defaultBranch]) => { - const price = item?.price; - return this._mapToTakeAwayAvailability({ - response, - supplier, - branchId: branch?.id ?? defaultBranch?.id, - quantity, - price, - }); - }), - shareReplay(1), - ); - } - - @memorize({ ttl: 10000 }) - getTakeAwayAvailabilityByBranch({ - branch, - itemId, - price, - quantity, - }: { - branch: BranchDTO; - itemId: number; - price: PriceDTO; - quantity: number; - }): Observable { - return combineLatest([ - this._stockService.StockStockRequest({ stockRequest: { branchIds: [branch.id], itemId } }), - this.getTakeAwaySupplier(), - ]).pipe( - map(([response, supplier]) => { - return this._mapToTakeAwayAvailability({ response, supplier, branchId: branch.id, quantity, price }); - }), - shareReplay(1), - ); - } - - getTakeAwayAvailabilityByEan({ - eans, - price, - quantity, - branchId, - }: { - eans: string[]; - price: PriceDTO; - quantity: number; - branchId?: number; - }): Observable { - const request = branchId ? this.getStockByBranch(branchId) : this.getDefaultStock(); - return request.pipe( - switchMap((s) => this._stockService.StockInStockByEAN({ eans, stockId: s.id })), - withLatestFrom(this.getTakeAwaySupplier(), this.getDefaultBranch()), - map(([response, supplier, defaultBranch]) => { - return this._mapToTakeAwayAvailability({ - response, - supplier, - branchId: branchId ?? defaultBranch.id, - quantity, - price, - }); - }), - shareReplay(1), - ); - } - - getTakeAwayAvailabilitiesByEans({ eans }: { eans: string[] }): Observable { - const eansFiltered = Array.from(new Set(eans)); - return this.getDefaultStock().pipe( - switchMap((s) => this._stockService.StockInStockByEAN({ eans: eansFiltered, stockId: s.id })), - withLatestFrom(this.getTakeAwaySupplier(), this.getDefaultBranch()), - map((response) => response[0].result), - shareReplay(1), - ); - } - - @memorize({ ttl: 10000 }) - getPickUpAvailability({ - item, - branch, - quantity, - }: { - item: ItemData; - quantity: number; - branch: BranchDTO; - }): Observable> { - return this._availabilityService - .AvailabilityStoreAvailability([ - { - qty: quantity, - ean: item?.ean, - itemId: item?.itemId ? String(item?.itemId) : null, - shopId: branch?.id, - price: item?.price, - }, - ]) - .pipe( - map((r) => this._mapToPickUpAvailability(r.result)?.find((_) => true)), - shareReplay(1), - ); - } - - @memorize({ ttl: 10000 }) - getDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable { - return this.memorizedAvailabilityShippingAvailability([ - { - ean: item?.ean, - itemId: item?.itemId ? String(item?.itemId) : null, - price: item?.price, - qty: quantity, - }, - ]).pipe( - timeout(5000), - map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)), - shareReplay(1), - ); - } - - @memorize({ ttl: 10000 }) - getDigDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable { - return this.memorizedAvailabilityShippingAvailability([ - { - qty: quantity, - ean: item?.ean, - itemId: item?.itemId ? String(item?.itemId) : null, - price: item?.price, - }, - ]).pipe( - timeout(5000), - map((r) => { - const availabilities = r.result; - const preferred = availabilities?.find((f) => f.preferred === 1); - - return { - availabilityType: preferred?.status, - ssc: preferred?.ssc, - sscText: preferred?.sscText, - supplier: { id: preferred?.supplierId }, - isPrebooked: preferred?.isPrebooked, - estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at, - estimatedDelivery: preferred?.estimatedDelivery, - price: preferred?.price, - logistician: { id: preferred?.logisticianId }, - supplierProductNumber: preferred?.supplierProductNumber, - supplierInfo: preferred?.requestStatusCode, - lastRequest: preferred?.requested, - priceMaintained: preferred?.priceMaintained, - }; - }), - shareReplay(1), - ); - } - - @memorize({ ttl: 10000 }) - getB2bDeliveryAvailability({ - item, - quantity, - branch, - }: { - item: ItemData; - quantity: number; - branch?: BranchDTO; - }): Observable { - const logistician$ = this.getLogisticians(); - - const currentBranch$ = this.getDefaultBranch(); - - return currentBranch$.pipe( - timeout(5000), - mergeMap((defaultBranch) => - this.getPickUpAvailability({ item, quantity, branch: branch ?? defaultBranch }).pipe( - mergeMap((availability) => - logistician$.pipe( - map((logistician) => ({ - ...(availability?.length > 0 ? availability[0] : []), - logistician: { id: logistician.id }, - })), - ), - ), - shareReplay(1), - ), - ), - ); - } - - @memorize({ ttl: 10000 }) - getDownloadAvailability({ item }: { item: ItemData }): Observable { - return this.memorizedAvailabilityShippingAvailability([ - { - ean: item?.ean, - itemId: item?.itemId ? String(item?.itemId) : null, - price: item?.price, - qty: 1, - }, - ]).pipe( - map((r) => { - const availabilities = r.result; - const preferred = availabilities?.find((f) => f.preferred === 1); - - return { - availabilityType: preferred?.status, - ssc: preferred?.ssc, - sscText: preferred?.sscText, - supplier: { id: preferred?.supplierId }, - isPrebooked: preferred?.isPrebooked, - estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at, - price: preferred?.price, - supplierProductNumber: preferred?.supplierProductNumber, - logistician: { id: preferred?.logisticianId }, - supplierInfo: preferred?.requestStatusCode, - lastRequest: preferred?.requested, - priceMaintained: preferred?.priceMaintained, - }; - }), - shareReplay(1), - ); - } - - @memorize({ ttl: 10000 }) - getTakeAwayAvailabilities(items: { id: number; price: PriceDTO }[], branchId: number) { - return this._stockService.StockGetStocksByBranch({ branchId }).pipe( - map((req) => req.result?.find((_) => true)?.id), - switchMap((stockId) => - stockId - ? this._stockService.StockInStock({ articleIds: items.map((i) => i.id), stockId }) - : of({ result: [] } as ResponseArgsOfIEnumerableOfStockInfoDTO), - ), - timeout(20000), - withLatestFrom(this.getTakeAwaySupplier()), - map(([response, supplier]) => { - return response.result?.map((stockInfo) => - this._mapToTakeAwayAvailabilities({ - stockInfo, - supplier, - quantity: 1, - price: items?.find((i) => i.id === stockInfo.itemId)?.price, - }), - ); - }), - shareReplay(1), - ); - } - - @memorize({ ttl: 10000 }) - getPickUpAvailabilities(payload: AvailabilityRequestDTO[], preferred?: boolean) { - return this._availabilityService.AvailabilityStoreAvailability(payload).pipe( - timeout(20000), - map((response) => (preferred ? this._mapToPickUpAvailability(response.result) : response.result)), - ); - } - - @memorize({ ttl: 10000 }) - getDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) { - return this.memorizedAvailabilityShippingAvailability(payload).pipe( - timeout(20000), - map((response) => this._mapToShippingAvailability(response.result)), - ); - } - - @memorize({ ttl: 10000 }) - getDigDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) { - return this.memorizedAvailabilityShippingAvailability(payload).pipe( - timeout(20000), - map((response) => this._mapToShippingAvailability(response.result)), - ); - } - - @memorize({ ttl: 10000 }) - getB2bDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) { - const logistician$ = this.getLogisticians(); - - return this.getPickUpAvailabilities(payload, true).pipe( - timeout(20000), - switchMap((availability) => - logistician$.pipe( - map((logistician) => ({ availability: [...availability], logistician: { id: logistician.id } })), - ), - ), - shareReplay(1), - ); - } - - getPriceForAvailability( - purchasingOption: string, - catalogAvailability: CatAvailabilityDTO | AvailabilityDTO, - availability: AvailabilityDTO, - ): PriceDTO { - switch (purchasingOption) { - case 'take-away': - return availability?.price || catalogAvailability?.price; - case 'delivery': - case 'dig-delivery': - if (catalogAvailability?.price?.value?.value < availability?.price?.value?.value) { - return catalogAvailability?.price; - } - return availability?.price || catalogAvailability?.price; - } - return availability?.price; - } - - isAvailable({ availability }: { availability: AvailabilityDTO }) { - if (availability?.supplier?.id === 16 && availability?.inStock == 0) { - return false; - } - return [2, 32, 256, 1024, 2048, 4096].some((code) => availability?.availabilityType === code); - } - - private _mapToTakeAwayAvailability({ - response, - supplier, - branchId, - quantity, - price, - }: { - response: ResponseArgsOfIEnumerableOfStockInfoDTO; - supplier: SupplierDTO; - branchId: number; - quantity: number; - price: PriceDTO; - }): AvailabilityDTO { - const stockInfo = response.result?.find((si) => si.branchId === branchId); - const inStock = stockInfo?.inStock ?? 0; - const availability: AvailabilityDTO = { - availabilityType: quantity <= inStock ? 1024 : 1, // 1024 (=Available) - inStock: inStock, - supplierSSC: quantity <= inStock ? '999' : '', - supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '', - price: stockInfo?.retailPrice ?? price, // #4553 Es soll nun immer der retailPrice aus der InStock Abfrage verwendet werden, egal ob "price" empty ist oder nicht - supplier: { id: supplier?.id }, - // TODO: Change after API Update - // LH: 2021-03-09 preis Property hat nun ein Fallback auf retailPrice - // retailPrice: (stockInfo as any)?.retailPrice, - }; - return availability; - } - - private _mapToTakeAwayAvailabilities({ - stockInfo, - quantity, - price, - supplier, - }: { - stockInfo: StockInfoDTO; - quantity: number; - price: PriceDTO; - supplier: SupplierDTO; - }) { - const inStock = stockInfo?.inStock ?? 0; - - const availability = { - itemId: stockInfo.itemId, - availabilityType: quantity <= inStock ? (1024 as AvailabilityType) : (1 as AvailabilityType), // 1024 (=Available) - inStock: inStock, - supplierSSC: quantity <= inStock ? '999' : '', - supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '', - price, - supplier: { id: supplier?.id }, - }; - return availability; - } - - private _mapToPickUpAvailability( - availabilities: SwaggerAvailabilityDTO[], - ): Availability[] { - if (isArray(availabilities)) { - const preferred = availabilities.filter((f) => f.preferred === 1); - const totalAvailable = availabilities.reduce((sum, av) => sum + (av?.qty || 0), 0); - - return preferred.map((p) => { - return [ - { - orderDeadline: p?.orderDeadline, - availabilityType: p?.status, - ssc: p?.ssc, - sscText: p?.sscText, - supplier: { id: p?.supplierId }, - isPrebooked: p?.isPrebooked, - estimatedShippingDate: p?.requestStatusCode === '32' ? p?.altAt : p?.at, - price: p?.price, - inStock: totalAvailable, - supplierProductNumber: p?.supplierProductNumber, - supplierInfo: p?.requestStatusCode, - lastRequest: p?.requested, - itemId: p.itemId, - priceMaintained: p.priceMaintained, - }, - p, - ]; - }); - } - } - - private _mapToShippingAvailability(availabilities: SwaggerAvailabilityDTO[]): AvailabilityDTO[] { - const preferred = availabilities.filter((f) => f.preferred === 1); - return preferred.map((p) => { - return { - availabilityType: p?.status, - ssc: p?.ssc, - sscText: p?.sscText, - isPrebooked: p?.isPrebooked, - estimatedShippingDate: p?.requestStatusCode === '32' ? p?.altAt : p?.at, - estimatedDelivery: p?.estimatedDelivery, - price: p?.price, - supplierProductNumber: p?.supplierProductNumber, - supplierInfo: p?.requestStatusCode, - lastRequest: p?.requested, - itemId: p.itemId, - priceMaintained: p.priceMaintained, - }; - }); - } - - getInStockByEan(params: { eans: string[]; branchId?: number }): Observable> { - let branchId$ = of(params.branchId); - - if (!params.branchId) { - branchId$ = this.getDefaultBranch().pipe( - first(), - map((b) => b.id), - ); - } - - const stock$ = branchId$.pipe( - mergeMap((branchId) => - this._stockService.StockGetStocksByBranch({ branchId }).pipe(map((response) => response.result?.[0])), - ), - ); - - return stock$.pipe( - mergeMap((stock) => - this._stockService.StockInStockByEAN({ eans: params.eans, stockId: stock.id }).pipe( - map((response) => { - const result = response.result ?? []; - - for (const stockInfo of result) { - stockInfo.ean = stockInfo.ean; - } - - return result.reduce>((acc, stockInfo) => { - acc[stockInfo.ean] = stockInfo; - return acc; - }, {}); - }), - ), - ), - ); - } - - getInStock({ itemIds, branchId }: { itemIds: number[]; branchId: number }): Observable { - return this.getStockByBranch(branchId).pipe( - mergeMap((stock) => - this._stockService - .StockInStock({ articleIds: itemIds, stockId: stock.id }) - .pipe(map((response) => response.result)), - ), - ); - } -} +import { Injectable } from '@angular/core'; +import { ItemDTO } from '@generated/swagger/cat-search-api'; +import { + AvailabilityDTO, + BranchDTO, + OLAAvailabilityDTO, + StoreCheckoutBranchService, + StoreCheckoutSupplierService, + SupplierDTO, +} from '@generated/swagger/checkout-api'; +import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs'; +import { + AvailabilityRequestDTO, + AvailabilityService, + AvailabilityDTO as SwaggerAvailabilityDTO, + AvailabilityType, +} from '@generated/swagger/availability-api'; +import { AvailabilityDTO as CatAvailabilityDTO } from '@generated/swagger/cat-search-api'; +import { + map, + shareReplay, + switchMap, + withLatestFrom, + mergeMap, + timeout, + first, +} from 'rxjs/operators'; +import { isArray, memorize } from '@utils/common'; +import { LogisticianDTO, LogisticianService } from '@generated/swagger/oms-api'; +import { + ResponseArgsOfIEnumerableOfStockInfoDTO, + StockDTO, + StockInfoDTO, + StockService, +} from '@generated/swagger/inventory-api'; +import { PriceDTO } from '@generated/swagger/availability-api'; +import { AvailabilityByBranchDTO, ItemData, Ssc } from './defs'; +import { Availability } from './defs/availability'; +import { isEmpty } from 'lodash'; + +@Injectable({ providedIn: 'root' }) +export class DomainAvailabilityService { + // Ticket #3378 Keep Result List Items and Details Page SSC in sync + sscs$ = new BehaviorSubject>([]); + sscsObs$ = this.sscs$.asObservable(); + + constructor( + private _availabilityService: AvailabilityService, + private _logisticanService: LogisticianService, + private _stockService: StockService, + private _supplierService: StoreCheckoutSupplierService, + private _branchService: StoreCheckoutBranchService, + ) {} + + @memorize({ ttl: 10000 }) + memorizedAvailabilityShippingAvailability( + request: Array, + ) { + return this._availabilityService + .AvailabilityShippingAvailability(request) + .pipe(shareReplay(1)); + } + + @memorize() + getSuppliers(): Observable { + return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe( + map((response) => response.result), + shareReplay(1), + ); + } + + @memorize() + getTakeAwaySupplier(): Observable { + return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe( + map(({ result }) => + result?.find((supplier) => supplier?.supplierNumber === 'F'), + ), + shareReplay(1), + ); + } + + @memorize() + getBranches(): Observable { + return this._branchService.StoreCheckoutBranchGetBranches({}).pipe( + map((response) => response.result), + shareReplay(1), + ); + } + + @memorize() + getStockByBranch(branchId: number): Observable { + return this._stockService.StockGetStocksByBranch({ branchId }).pipe( + map((response) => response.result), + map((result) => result?.find((_) => true)), + shareReplay(1), + ); + } + + @memorize() + getDefaultStock(): Observable { + return this._stockService.StockCurrentStock().pipe( + map((response) => response.result), + shareReplay(1), + ); + } + + @memorize() + getDefaultBranch(): Observable { + return this._stockService.StockCurrentBranch().pipe( + map((response) => ({ + id: response.result.id, + name: response.result.name, + address: response.result.address, + branchType: response.result.branchType, + branchNumber: response.result.branchNumber, + changed: response.result.changed, + created: response.result.created, + isDefault: response.result.isDefault, + isOnline: response.result.isOnline, + key: response.result.key, + label: response.result.label, + pId: response.result.pId, + shortName: response.result.shortName, + status: response.result.status, + version: response.result.version, + })), + shareReplay(1), + ); + } + + @memorize({}) + getLogisticians(): Observable { + return this._logisticanService.LogisticianGetLogisticians({}).pipe( + map((response) => + response.result?.find((l) => l.logisticianNumber === '2470'), + ), + shareReplay(1), + ); + } + + getTakeAwayAvailabilityByBranches({ + branchIds, + itemId, + price, + quantity, + }: { + branchIds: number[]; + itemId: number; + price: PriceDTO; + quantity: number; + }): Observable { + return this._stockService + .StockStockRequest({ stockRequest: { branchIds, itemId } }) + .pipe( + map((response) => response.result), + withLatestFrom(this.getTakeAwaySupplier()), + map(([result, supplier]) => { + const availabilities: AvailabilityByBranchDTO[] = result.map( + (stockInfo) => { + return { + availableQuantity: stockInfo.availableQuantity, + availabilityType: quantity <= stockInfo.inStock ? 1024 : 1, // 1024 (=Available) + inStock: stockInfo.inStock, + supplierSSC: quantity <= stockInfo.inStock ? '999' : '', + supplierSSCText: + quantity <= stockInfo.inStock ? 'Filialentnahme' : '', + price, + supplier: { id: supplier?.id }, + branchId: stockInfo.branchId, + }; + }, + ); + return availabilities; + }), + shareReplay(1), + ); + } + + @memorize({ ttl: 10000 }) + getTakeAwayAvailability({ + item, + quantity, + branch, + }: { + item: ItemData; + quantity: number; + branch?: BranchDTO; + }): Observable { + const request = branch + ? this.getStockByBranch(branch.id) + : this.getDefaultStock(); + return request.pipe( + switchMap((s) => + combineLatest([ + this._stockService.StockInStock({ + articleIds: [item.itemId], + stockId: s.id, + }), + this.getTakeAwaySupplier(), + this.getDefaultBranch(), + ]), + ), + map(([response, supplier, defaultBranch]) => { + const price = item?.price; + return this._mapToTakeAwayAvailability({ + response, + supplier, + branchId: branch?.id ?? defaultBranch?.id, + quantity, + price, + }); + }), + shareReplay(1), + ); + } + + @memorize({ ttl: 10000 }) + getTakeAwayAvailabilityByBranch({ + branch, + itemId, + price, + quantity, + }: { + branch: BranchDTO; + itemId: number; + price: PriceDTO; + quantity: number; + }): Observable { + return combineLatest([ + this._stockService.StockStockRequest({ + stockRequest: { branchIds: [branch.id], itemId }, + }), + this.getTakeAwaySupplier(), + ]).pipe( + map(([response, supplier]) => { + return this._mapToTakeAwayAvailability({ + response, + supplier, + branchId: branch.id, + quantity, + price, + }); + }), + shareReplay(1), + ); + } + + getTakeAwayAvailabilityByEan({ + eans, + price, + quantity, + branchId, + }: { + eans: string[]; + price: PriceDTO; + quantity: number; + branchId?: number; + }): Observable { + const request = branchId + ? this.getStockByBranch(branchId) + : this.getDefaultStock(); + return request.pipe( + switchMap((s) => + this._stockService.StockInStockByEAN({ eans, stockId: s.id }), + ), + withLatestFrom(this.getTakeAwaySupplier(), this.getDefaultBranch()), + map(([response, supplier, defaultBranch]) => { + return this._mapToTakeAwayAvailability({ + response, + supplier, + branchId: branchId ?? defaultBranch.id, + quantity, + price, + }); + }), + shareReplay(1), + ); + } + + getTakeAwayAvailabilitiesByEans({ + eans, + }: { + eans: string[]; + }): Observable { + const eansFiltered = Array.from(new Set(eans)); + return this.getDefaultStock().pipe( + switchMap((s) => + this._stockService.StockInStockByEAN({ + eans: eansFiltered, + stockId: s.id, + }), + ), + withLatestFrom(this.getTakeAwaySupplier(), this.getDefaultBranch()), + map((response) => response[0].result), + shareReplay(1), + ); + } + + @memorize({ ttl: 10000 }) + getPickUpAvailability({ + item, + branch, + quantity, + }: { + item: ItemData; + quantity: number; + branch: BranchDTO; + }): Observable> { + return this._availabilityService + .AvailabilityStoreAvailability([ + { + qty: quantity, + ean: item?.ean, + itemId: item?.itemId ? String(item?.itemId) : null, + shopId: branch?.id, + price: item?.price, + }, + ]) + .pipe( + map((r) => this._mapToPickUpAvailability(r.result)?.find((_) => true)), + shareReplay(1), + ); + } + + @memorize({ ttl: 10000 }) + getDeliveryAvailability({ + item, + quantity, + }: { + item: ItemData; + quantity: number; + }): Observable { + return this.memorizedAvailabilityShippingAvailability([ + { + ean: item?.ean, + itemId: item?.itemId ? String(item?.itemId) : null, + price: item?.price, + qty: quantity, + }, + ]).pipe( + timeout(5000), + map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)), + shareReplay(1), + ); + } + + @memorize({ ttl: 10000 }) + getDigDeliveryAvailability({ + item, + quantity, + }: { + item: ItemData; + quantity: number; + }): Observable { + return this.memorizedAvailabilityShippingAvailability([ + { + qty: quantity, + ean: item?.ean, + itemId: item?.itemId ? String(item?.itemId) : null, + price: item?.price, + }, + ]).pipe( + timeout(5000), + map((r) => { + const availabilities = r.result; + const preferred = availabilities?.find((f) => f.preferred === 1); + + return { + availabilityType: preferred?.status, + ssc: preferred?.ssc, + sscText: preferred?.sscText, + supplier: { id: preferred?.supplierId }, + isPrebooked: preferred?.isPrebooked, + estimatedShippingDate: + preferred?.requestStatusCode === '32' + ? preferred?.altAt + : preferred?.at, + estimatedDelivery: preferred?.estimatedDelivery, + price: preferred?.price, + logistician: { id: preferred?.logisticianId }, + supplierProductNumber: preferred?.supplierProductNumber, + supplierInfo: preferred?.requestStatusCode, + lastRequest: preferred?.requested, + priceMaintained: preferred?.priceMaintained, + }; + }), + shareReplay(1), + ); + } + + @memorize({ ttl: 10000 }) + getB2bDeliveryAvailability({ + item, + quantity, + branch, + }: { + item: ItemData; + quantity: number; + branch?: BranchDTO; + }): Observable { + const logistician$ = this.getLogisticians(); + + const currentBranch$ = this.getDefaultBranch(); + + return currentBranch$.pipe( + timeout(5000), + mergeMap((defaultBranch) => + this.getPickUpAvailability({ + item, + quantity, + branch: branch ?? defaultBranch, + }).pipe( + mergeMap((availability) => + logistician$.pipe( + map((logistician) => ({ + ...(availability?.length > 0 ? availability[0] : []), + logistician: { id: logistician.id }, + })), + ), + ), + shareReplay(1), + ), + ), + ); + } + + @memorize({ ttl: 10000 }) + getDownloadAvailability({ + item, + }: { + item: ItemData; + }): Observable { + return this.memorizedAvailabilityShippingAvailability([ + { + ean: item?.ean, + itemId: item?.itemId ? String(item?.itemId) : null, + price: item?.price, + qty: 1, + }, + ]).pipe( + map((r) => { + const availabilities = r.result; + const preferred = availabilities?.find((f) => f.preferred === 1); + + return { + availabilityType: preferred?.status, + ssc: preferred?.ssc, + sscText: preferred?.sscText, + supplier: { id: preferred?.supplierId }, + isPrebooked: preferred?.isPrebooked, + estimatedShippingDate: + preferred?.requestStatusCode === '32' + ? preferred?.altAt + : preferred?.at, + price: preferred?.price, + supplierProductNumber: preferred?.supplierProductNumber, + logistician: { id: preferred?.logisticianId }, + supplierInfo: preferred?.requestStatusCode, + lastRequest: preferred?.requested, + priceMaintained: preferred?.priceMaintained, + }; + }), + shareReplay(1), + ); + } + + @memorize({ ttl: 10000 }) + getTakeAwayAvailabilities( + items: { id: number; price: PriceDTO }[], + branchId: number, + ) { + return this._stockService.StockGetStocksByBranch({ branchId }).pipe( + map((req) => req.result?.find((_) => true)?.id), + switchMap((stockId) => + stockId + ? this._stockService.StockInStock({ + articleIds: items.map((i) => i.id), + stockId, + }) + : of({ result: [] } as ResponseArgsOfIEnumerableOfStockInfoDTO), + ), + timeout(20000), + withLatestFrom(this.getTakeAwaySupplier()), + map(([response, supplier]) => { + return response.result?.map((stockInfo) => + this._mapToTakeAwayAvailabilities({ + stockInfo, + supplier, + quantity: 1, + price: items?.find((i) => i.id === stockInfo.itemId)?.price, + }), + ); + }), + shareReplay(1), + ); + } + + @memorize({ ttl: 10000 }) + getPickUpAvailabilities( + payload: AvailabilityRequestDTO[], + preferred?: boolean, + ) { + return this._availabilityService + .AvailabilityStoreAvailability(payload) + .pipe( + timeout(20000), + map((response) => + preferred + ? this._mapToPickUpAvailability(response.result) + : response.result, + ), + ); + } + + @memorize({ ttl: 10000 }) + getDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) { + return this.memorizedAvailabilityShippingAvailability(payload).pipe( + timeout(20000), + map((response) => this._mapToShippingAvailability(response.result)), + ); + } + + @memorize({ ttl: 10000 }) + getDigDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) { + return this.memorizedAvailabilityShippingAvailability(payload).pipe( + timeout(20000), + map((response) => this._mapToShippingAvailability(response.result)), + ); + } + + @memorize({ ttl: 10000 }) + getB2bDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) { + const logistician$ = this.getLogisticians(); + + return this.getPickUpAvailabilities(payload, true).pipe( + timeout(20000), + switchMap((availability) => + logistician$.pipe( + map((logistician) => ({ + availability: [...availability], + logistician: { id: logistician.id }, + })), + ), + ), + shareReplay(1), + ); + } + + getPriceForAvailability( + purchasingOption: string, + catalogAvailability: CatAvailabilityDTO | AvailabilityDTO, + availability: AvailabilityDTO, + ): PriceDTO { + switch (purchasingOption) { + case 'take-away': + return availability?.price || catalogAvailability?.price; + case 'delivery': + case 'dig-delivery': + if ( + catalogAvailability?.price?.value?.value < + availability?.price?.value?.value + ) { + return catalogAvailability?.price; + } + return availability?.price || catalogAvailability?.price; + } + return availability?.price; + } + + isAvailable({ availability }: { availability: AvailabilityDTO }) { + if (availability?.supplier?.id === 16 && availability?.inStock == 0) { + return false; + } + return [2, 32, 256, 1024, 2048, 4096].some( + (code) => availability?.availabilityType === code, + ); + } + + private _mapToTakeAwayAvailability({ + response, + supplier, + branchId, + quantity, + price, + }: { + response: ResponseArgsOfIEnumerableOfStockInfoDTO; + supplier: SupplierDTO; + branchId: number; + quantity: number; + price: PriceDTO; + }): AvailabilityDTO { + const stockInfo = response.result?.find((si) => si.branchId === branchId); + const inStock = stockInfo?.inStock ?? 0; + const availability: AvailabilityDTO = { + availabilityType: quantity <= inStock ? 1024 : 1, // 1024 (=Available) + inStock: inStock, + supplierSSC: quantity <= inStock ? '999' : '', + supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '', + price: stockInfo?.retailPrice ?? price, // #4553 Es soll nun immer der retailPrice aus der InStock Abfrage verwendet werden, egal ob "price" empty ist oder nicht + supplier: { id: supplier?.id }, + // TODO: Change after API Update + // LH: 2021-03-09 preis Property hat nun ein Fallback auf retailPrice + // retailPrice: (stockInfo as any)?.retailPrice, + }; + return availability; + } + + private _mapToTakeAwayAvailabilities({ + stockInfo, + quantity, + price, + supplier, + }: { + stockInfo: StockInfoDTO; + quantity: number; + price: PriceDTO; + supplier: SupplierDTO; + }) { + const inStock = stockInfo?.inStock ?? 0; + + const availability = { + itemId: stockInfo.itemId, + availabilityType: + quantity <= inStock + ? (1024 as AvailabilityType) + : (1 as AvailabilityType), // 1024 (=Available) + inStock: inStock, + supplierSSC: quantity <= inStock ? '999' : '', + supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '', + price, + supplier: { id: supplier?.id }, + }; + return availability; + } + + private _mapToPickUpAvailability( + availabilities: SwaggerAvailabilityDTO[], + ): Availability[] { + if (isArray(availabilities)) { + const preferred = availabilities.filter((f) => f.preferred === 1); + const totalAvailable = availabilities.reduce( + (sum, av) => sum + (av?.qty || 0), + 0, + ); + + return preferred.map((p) => { + return [ + { + orderDeadline: p?.orderDeadline, + availabilityType: p?.status, + ssc: p?.ssc, + sscText: p?.sscText, + supplier: { id: p?.supplierId }, + isPrebooked: p?.isPrebooked, + estimatedShippingDate: + p?.requestStatusCode === '32' ? p?.altAt : p?.at, + price: p?.price, + inStock: totalAvailable, + supplierProductNumber: p?.supplierProductNumber, + supplierInfo: p?.requestStatusCode, + lastRequest: p?.requested, + itemId: p.itemId, + priceMaintained: p.priceMaintained, + }, + p, + ]; + }); + } + } + + private _mapToShippingAvailability( + availabilities: SwaggerAvailabilityDTO[], + ): AvailabilityDTO[] { + const preferred = availabilities.filter((f) => f.preferred === 1); + return preferred.map((p) => { + return { + availabilityType: p?.status, + ssc: p?.ssc, + sscText: p?.sscText, + isPrebooked: p?.isPrebooked, + estimatedShippingDate: p?.requestStatusCode === '32' ? p?.altAt : p?.at, + estimatedDelivery: p?.estimatedDelivery, + price: p?.price, + supplierProductNumber: p?.supplierProductNumber, + supplierInfo: p?.requestStatusCode, + lastRequest: p?.requested, + itemId: p.itemId, + priceMaintained: p.priceMaintained, + }; + }); + } + + getInStockByEan(params: { + eans: string[]; + branchId?: number; + }): Observable> { + let branchId$ = of(params.branchId); + + if (!params.branchId) { + branchId$ = this.getDefaultBranch().pipe( + first(), + map((b) => b.id), + ); + } + + const stock$ = branchId$.pipe( + mergeMap((branchId) => + this._stockService + .StockGetStocksByBranch({ branchId }) + .pipe(map((response) => response.result?.[0])), + ), + ); + + return stock$.pipe( + mergeMap((stock) => + this._stockService + .StockInStockByEAN({ eans: params.eans, stockId: stock.id }) + .pipe( + map((response) => { + const result = response.result ?? []; + + + return result.reduce>( + (acc, stockInfo) => { + acc[stockInfo.ean] = stockInfo; + return acc; + }, + {}, + ); + }), + ), + ), + ); + } + + getInStock({ + itemIds, + branchId, + }: { + itemIds: number[]; + branchId: number; + }): Observable { + return this.getStockByBranch(branchId).pipe( + mergeMap((stock) => + this._stockService + .StockInStock({ articleIds: itemIds, stockId: stock.id }) + .pipe(map((response) => response.result)), + ), + ); + } +} diff --git a/apps/isa-app/src/domain/availability/index.ts b/apps/isa-app/src/domain/availability/index.ts index 0a0e08df0..cedec6ffd 100644 --- a/apps/isa-app/src/domain/availability/index.ts +++ b/apps/isa-app/src/domain/availability/index.ts @@ -1,4 +1,3 @@ -export * from './availability.module'; export * from './availability.service'; export * from './defs'; export * from './in-stock.service'; diff --git a/apps/isa-app/src/domain/catalog/catalog.module.ts b/apps/isa-app/src/domain/catalog/catalog.module.ts deleted file mode 100644 index 104b8823e..000000000 --- a/apps/isa-app/src/domain/catalog/catalog.module.ts +++ /dev/null @@ -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 { - return { - ngModule: DomainCatalogModule, - providers: [DomainCatalogService, DomainCatalogThumbnailService], - }; - } -} diff --git a/apps/isa-app/src/domain/catalog/catalog.service.ts b/apps/isa-app/src/domain/catalog/catalog.service.ts index e43c05977..e33305e11 100644 --- a/apps/isa-app/src/domain/catalog/catalog.service.ts +++ b/apps/isa-app/src/domain/catalog/catalog.service.ts @@ -1,105 +1,111 @@ -import { Injectable } from '@angular/core'; -import { ApplicationService } from '@core/application'; -import { - AutocompleteTokenDTO, - PromotionService, - QueryTokenDTO, - SearchService, -} from '@generated/swagger/cat-search-api'; -import { memorize } from '@utils/common'; -import { map, share, shareReplay } from 'rxjs/operators'; - -@Injectable() -export class DomainCatalogService { - constructor( - private searchService: SearchService, - private promotionService: PromotionService, - private applicationService: ApplicationService, - ) {} - - @memorize() - getFilters() { - return this.searchService.SearchSearchFilter().pipe( - map((res) => res.result), - shareReplay(), - ); - } - - @memorize() - getOrderBy() { - return this.searchService.SearchSearchSort().pipe( - map((res) => res.result), - shareReplay(), - ); - } - - getSearchHistory({ take }: { take: number }) { - return this.searchService.SearchHistory(take ?? 5).pipe(map((res) => res.result)); - } - - @memorize({ ttl: 120000 }) - search({ queryToken }: { queryToken: QueryTokenDTO }) { - return this.searchService - .SearchSearch({ - ...queryToken, - stockId: null, - }) - .pipe(share()); - } - - @memorize({ ttl: 120000 }) - searchWithStockId({ queryToken }: { queryToken: QueryTokenDTO }) { - return this.searchService - .SearchSearch2({ - queryToken, - stockId: queryToken?.stockId ?? null, - }) - .pipe(share()); - } - - getDetailsById({ id }: { id: number }) { - return this.searchService.SearchDetail({ - id, - }); - } - - getDetailsByEan({ ean }: { ean: string }) { - return this.searchService.SearchDetailByEAN(ean); - } - - searchByIds({ ids }: { ids: number[] }) { - return this.searchService.SearchById(ids); - } - - searchByEans({ eans }: { eans: string[] }) { - return this.searchService.SearchByEAN(eans); - } - - searchTop({ queryToken }: { queryToken: QueryTokenDTO }) { - return this.searchService.SearchTop(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() - getSettings() { - return this.searchService.SearchSettings().pipe( - map((res) => res.result), - shareReplay(), - ); - } - - getRecommendations({ digId }: { digId: number }) { - return this.searchService.SearchGetRecommendations({ - digId: digId + '', - sessionId: this.applicationService.activatedProcessId + '', - }); - } -} +import { Injectable } from '@angular/core'; +import { ApplicationService } from '@core/application'; +import { + AutocompleteTokenDTO, + PromotionService, + QueryTokenDTO, + SearchService, +} from '@generated/swagger/cat-search-api'; +import { memorize } from '@utils/common'; +import { map, share, shareReplay } from 'rxjs/operators'; + +@Injectable({ providedIn: 'root' }) +export class DomainCatalogService { + constructor( + private searchService: SearchService, + private promotionService: PromotionService, + private applicationService: ApplicationService, + ) {} + + @memorize() + getFilters() { + return this.searchService.SearchSearchFilter().pipe( + map((res) => res.result), + shareReplay(), + ); + } + + @memorize() + getOrderBy() { + return this.searchService.SearchSearchSort().pipe( + map((res) => res.result), + shareReplay(), + ); + } + + getSearchHistory({ take }: { take: number }) { + return this.searchService + .SearchHistory(take ?? 5) + .pipe(map((res) => res.result)); + } + + @memorize({ ttl: 120000 }) + search({ queryToken }: { queryToken: QueryTokenDTO }) { + return this.searchService + .SearchSearch({ + ...queryToken, + stockId: null, + }) + .pipe(share()); + } + + @memorize({ ttl: 120000 }) + searchWithStockId({ queryToken }: { queryToken: QueryTokenDTO }) { + return this.searchService + .SearchSearch2({ + queryToken, + stockId: queryToken?.stockId ?? null, + }) + .pipe(share()); + } + + getDetailsById({ id }: { id: number }) { + return this.searchService.SearchDetail({ + id, + }); + } + + getDetailsByEan({ ean }: { ean: string }) { + return this.searchService.SearchDetailByEAN(ean); + } + + searchByIds({ ids }: { ids: number[] }) { + return this.searchService.SearchById(ids); + } + + searchByEans({ eans }: { eans: string[] }) { + return this.searchService.SearchByEAN(eans); + } + + searchTop({ queryToken }: { queryToken: QueryTokenDTO }) { + return this.searchService.SearchTop(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() + getSettings() { + return this.searchService.SearchSettings().pipe( + map((res) => res.result), + shareReplay(), + ); + } + + getRecommendations({ digId }: { digId: number }) { + return this.searchService.SearchGetRecommendations({ + digId: digId + '', + sessionId: this.applicationService.activatedProcessId + '', + }); + } +} diff --git a/apps/isa-app/src/domain/catalog/index.ts b/apps/isa-app/src/domain/catalog/index.ts index 8a28a1827..55b7b6cad 100644 --- a/apps/isa-app/src/domain/catalog/index.ts +++ b/apps/isa-app/src/domain/catalog/index.ts @@ -1,4 +1,3 @@ -export * from './catalog.module'; export * from './catalog.service'; export * from './thumbnail-url.pipe'; export * from './thumbnail.service'; diff --git a/apps/isa-app/src/domain/catalog/thumbnail-url.pipe.ts b/apps/isa-app/src/domain/catalog/thumbnail-url.pipe.ts index e299bfea8..6932657b6 100644 --- a/apps/isa-app/src/domain/catalog/thumbnail-url.pipe.ts +++ b/apps/isa-app/src/domain/catalog/thumbnail-url.pipe.ts @@ -6,7 +6,7 @@ import { DomainCatalogThumbnailService } from './thumbnail.service'; @Pipe({ name: 'thumbnailUrl', pure: false, - standalone: false, + standalone: true, }) export class ThumbnailUrlPipe implements PipeTransform, OnDestroy { private input$ = new BehaviorSubject<{ width?: number; height?: number; ean?: string }>(undefined); diff --git a/apps/isa-app/src/domain/catalog/thumbnail.service.ts b/apps/isa-app/src/domain/catalog/thumbnail.service.ts index bcee8c402..594b72007 100644 --- a/apps/isa-app/src/domain/catalog/thumbnail.service.ts +++ b/apps/isa-app/src/domain/catalog/thumbnail.service.ts @@ -1,20 +1,28 @@ -import { Injectable } from '@angular/core'; -import { memorize } from '@utils/common'; -import { map, shareReplay } from 'rxjs/operators'; -import { DomainCatalogService } from './catalog.service'; - -@Injectable() -export class DomainCatalogThumbnailService { - constructor(private domainCatalogService: DomainCatalogService) {} - - @memorize() - getThumnaulUrl({ ean, height, width }: { width?: number; height?: number; ean?: string }) { - return this.domainCatalogService.getSettings().pipe( - map((settings) => { - let thumbnailUrl = settings.imageUrl.replace(/{ean}/, ean); - return thumbnailUrl; - }), - shareReplay(), - ); - } -} +import { Injectable } from '@angular/core'; +import { memorize } from '@utils/common'; +import { map, shareReplay } from 'rxjs/operators'; +import { DomainCatalogService } from './catalog.service'; + +@Injectable({ providedIn: 'root' }) +export class DomainCatalogThumbnailService { + constructor(private domainCatalogService: DomainCatalogService) {} + + @memorize() + getThumnaulUrl({ + ean, + height, + width, + }: { + width?: number; + height?: number; + ean?: string; + }) { + return this.domainCatalogService.getSettings().pipe( + map((settings) => { + const thumbnailUrl = settings.imageUrl.replace(/{ean}/, ean); + return thumbnailUrl; + }), + shareReplay(), + ); + } +} diff --git a/apps/isa-app/src/domain/checkout/checkout.module.ts b/apps/isa-app/src/domain/checkout/checkout.module.ts index a071a8cea..9e2bdbc31 100644 --- a/apps/isa-app/src/domain/checkout/checkout.module.ts +++ b/apps/isa-app/src/domain/checkout/checkout.module.ts @@ -1,29 +1,15 @@ -import { ModuleWithProviders, NgModule } from '@angular/core'; -import { StoreModule } from '@ngrx/store'; -import { DomainCheckoutService } from './checkout.service'; -import { domainCheckoutReducer } from './store/domain-checkout.reducer'; -import { storeFeatureName } from './store/domain-checkout.state'; -import { EffectsModule } from '@ngrx/effects'; -import { DomainCheckoutEffects } from './store/domain-checkout.effects'; - -@NgModule({ - declarations: [], - imports: [StoreModule.forFeature(storeFeatureName, domainCheckoutReducer)], - providers: [DomainCheckoutService], -}) -export class DomainCheckoutModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: RootDomainCheckoutModule, - providers: [DomainCheckoutService], - }; - } -} - -@NgModule({ - imports: [ - StoreModule.forFeature(storeFeatureName, domainCheckoutReducer), - EffectsModule.forFeature([DomainCheckoutEffects]), - ], -}) -export class RootDomainCheckoutModule {} +import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; +import { provideEffects } from '@ngrx/effects'; +import { provideState } from '@ngrx/store'; +import { DomainCheckoutService } from './checkout.service'; +import { DomainCheckoutEffects } from './store/domain-checkout.effects'; +import { domainCheckoutReducer } from './store/domain-checkout.reducer'; +import { storeFeatureName } from './store/domain-checkout.state'; + +export function provideDomainCheckout(): EnvironmentProviders { + return makeEnvironmentProviders([ + provideState({ name: storeFeatureName, reducer: domainCheckoutReducer }), + provideEffects(DomainCheckoutEffects), + DomainCheckoutService, + ]); +} diff --git a/apps/isa-app/src/domain/checkout/checkout.service.ts b/apps/isa-app/src/domain/checkout/checkout.service.ts index 52e7f53b3..0453ebdcd 100644 --- a/apps/isa-app/src/domain/checkout/checkout.service.ts +++ b/apps/isa-app/src/domain/checkout/checkout.service.ts @@ -1071,7 +1071,7 @@ export class DomainCheckoutService { }); } else if (orderType === 'B2B-Versand') { const branch = await this.applicationService - .getSelectedBranch$(processId) + .getSelectedBranch$() .pipe(first()) .toPromise(); availability$ = diff --git a/apps/isa-app/src/domain/isa/dashboard.service.ts b/apps/isa-app/src/domain/isa/dashboard.service.ts index ee8c886e6..ff07413ee 100644 --- a/apps/isa-app/src/domain/isa/dashboard.service.ts +++ b/apps/isa-app/src/domain/isa/dashboard.service.ts @@ -1,11 +1,11 @@ -import { Injectable } from '@angular/core'; -import { InfoService } from '@generated/swagger/isa-api'; - -@Injectable() -export class DomainDashboardService { - constructor(private readonly _infoService: InfoService) {} - - feed() { - return this._infoService.InfoInfo({}); - } -} +import { Injectable } from '@angular/core'; +import { InfoService } from '@generated/swagger/isa-api'; + +@Injectable({ providedIn: 'root' }) +export class DomainDashboardService { + constructor(private readonly _infoService: InfoService) {} + + feed() { + return this._infoService.InfoInfo({}); + } +} diff --git a/apps/isa-app/src/domain/isa/domain-isa.module.ts b/apps/isa-app/src/domain/isa/domain-isa.module.ts deleted file mode 100644 index 981f5f961..000000000 --- a/apps/isa-app/src/domain/isa/domain-isa.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ModuleWithProviders, NgModule } from '@angular/core'; -import { DomainDashboardService } from './dashboard.service'; - -@NgModule({}) -export class DomainIsaModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: DomainIsaModule, - providers: [DomainDashboardService], - }; - } -} diff --git a/apps/isa-app/src/domain/isa/index.ts b/apps/isa-app/src/domain/isa/index.ts index 771948171..75fc44fbb 100644 --- a/apps/isa-app/src/domain/isa/index.ts +++ b/apps/isa-app/src/domain/isa/index.ts @@ -1,3 +1,2 @@ export * from './dashboard.service'; export * from './defs'; -export * from './domain-isa.module'; diff --git a/apps/isa-app/src/domain/oms/goods.service.ts b/apps/isa-app/src/domain/oms/goods.service.ts index 46496ed6d..0253b1364 100644 --- a/apps/isa-app/src/domain/oms/goods.service.ts +++ b/apps/isa-app/src/domain/oms/goods.service.ts @@ -1,116 +1,130 @@ -import { Injectable } from '@angular/core'; -import { AbholfachService, AutocompleteTokenDTO, QueryTokenDTO } from '@generated/swagger/oms-api'; -import { DateAdapter } from '@ui/common'; -import { memorize } from '@utils/common'; -import { shareReplay } from 'rxjs/operators'; -@Injectable() -export class DomainGoodsService { - constructor( - private abholfachService: AbholfachService, - private dateAdapter: DateAdapter, - ) {} - - searchWareneingang(queryToken: QueryTokenDTO) { - return this.abholfachService.AbholfachWareneingang(queryToken); - } - - searchWarenausgabe(queryToken: QueryTokenDTO) { - return this.abholfachService.AbholfachWarenausgabe(queryToken); - } - - wareneingangComplete(autocompleteToken: AutocompleteTokenDTO) { - return this.abholfachService.AbholfachWareneingangAutocomplete(autocompleteToken); - } - - warenausgabeComplete(autocompleteToken: AutocompleteTokenDTO) { - return this.abholfachService.AbholfachWarenausgabeAutocomplete(autocompleteToken); - } - - getWareneingangItemByOrderNumber(orderNumber: string) { - return this.abholfachService.AbholfachWareneingang({ - filter: { all_branches: 'true', archive: 'true' }, - input: { - qs: orderNumber, - }, - }); - } - - getWarenausgabeItemByOrderNumber(orderNumber: string, archive: boolean) { - return this.abholfachService.AbholfachWarenausgabe({ - filter: { all_branches: 'true', archive: `${archive}` }, - input: { - qs: orderNumber, - }, - }); - } - - getWarenausgabeItemByCompartment(compartmentCode: string, archive: boolean) { - return this.abholfachService.AbholfachWarenausgabe({ - filter: { all_branches: 'true', archive: `${archive}` }, - input: { - qs: compartmentCode, - }, - }); - } - - getWareneingangItemByCustomerNumber(customerNumber: string) { - // Suche anhand der Kundennummer mit Status Bestellt, nachbestellt, eingetroffen, weitergeleitet intern - return this.abholfachService.AbholfachWareneingang({ - filter: { orderitemprocessingstatus: '16;128;8192;1048576' }, - input: { - customer_no: customerNumber, - }, - }); - } - - list() { - const base = this.dateAdapter.today(); - const startDate = this.dateAdapter.addCalendarDays(base, -5); - const endDate = this.dateAdapter.addCalendarDays(base, 1); - const queryToken: QueryTokenDTO = { - filter: { - orderitemprocessingstatus: '16;8192;1024;512;2048', - estimatedshippingdate: `"${startDate.toJSON()}"-"${endDate.toJSON()}"`, - }, - orderBy: [{ by: 'estimatedshippingdate' }], - skip: 0, - take: 20, - }; - return this.searchWareneingang(queryToken); - } - - @memorize() - goodsInQuerySettings() { - return this.abholfachService.AbholfachWareneingangQuerySettings().pipe(shareReplay()); - } - - @memorize() - goodsOutQuerySettings() { - return this.abholfachService.AbholfachWarenausgabeQuerySettings().pipe(shareReplay()); - } - - goodsInList(queryToken: QueryTokenDTO) { - return this.abholfachService.AbholfachWareneingangsliste(queryToken); - } - - @memorize() - goodsInListQuerySettings() { - return this.abholfachService.AbholfachWareneingangslisteQuerySettings().pipe(shareReplay()); - } - - goodsInCleanupList() { - return this.abholfachService.AbholfachAbholfachbereinigungsliste(); - } - - goodsInReservationList(queryToken: QueryTokenDTO) { - return this.abholfachService.AbholfachReservierungen(queryToken); - } - - goodsInRemissionPreviewList() { - return this.abholfachService.AbholfachAbholfachremissionsvorschau(); - } - - createGoodsInRemissionFromPreviewList() { - return this.abholfachService.AbholfachCreateAbholfachremission(); - } -} +import { Injectable } from '@angular/core'; +import { + AbholfachService, + AutocompleteTokenDTO, + QueryTokenDTO, +} from '@generated/swagger/oms-api'; +import { DateAdapter } from '@ui/common'; +import { memorize } from '@utils/common'; +import { shareReplay } from 'rxjs/operators'; +@Injectable({ providedIn: 'root' }) +export class DomainGoodsService { + constructor( + private abholfachService: AbholfachService, + private dateAdapter: DateAdapter, + ) {} + + searchWareneingang(queryToken: QueryTokenDTO) { + return this.abholfachService.AbholfachWareneingang(queryToken); + } + + searchWarenausgabe(queryToken: QueryTokenDTO) { + return this.abholfachService.AbholfachWarenausgabe(queryToken); + } + + wareneingangComplete(autocompleteToken: AutocompleteTokenDTO) { + return this.abholfachService.AbholfachWareneingangAutocomplete( + autocompleteToken, + ); + } + + warenausgabeComplete(autocompleteToken: AutocompleteTokenDTO) { + return this.abholfachService.AbholfachWarenausgabeAutocomplete( + autocompleteToken, + ); + } + + getWareneingangItemByOrderNumber(orderNumber: string) { + return this.abholfachService.AbholfachWareneingang({ + filter: { all_branches: 'true', archive: 'true' }, + input: { + qs: orderNumber, + }, + }); + } + + getWarenausgabeItemByOrderNumber(orderNumber: string, archive: boolean) { + return this.abholfachService.AbholfachWarenausgabe({ + filter: { all_branches: 'true', archive: `${archive}` }, + input: { + qs: orderNumber, + }, + }); + } + + getWarenausgabeItemByCompartment(compartmentCode: string, archive: boolean) { + return this.abholfachService.AbholfachWarenausgabe({ + filter: { all_branches: 'true', archive: `${archive}` }, + input: { + qs: compartmentCode, + }, + }); + } + + getWareneingangItemByCustomerNumber(customerNumber: string) { + // Suche anhand der Kundennummer mit Status Bestellt, nachbestellt, eingetroffen, weitergeleitet intern + return this.abholfachService.AbholfachWareneingang({ + filter: { orderitemprocessingstatus: '16;128;8192;1048576' }, + input: { + customer_no: customerNumber, + }, + }); + } + + list() { + const base = this.dateAdapter.today(); + const startDate = this.dateAdapter.addCalendarDays(base, -5); + const endDate = this.dateAdapter.addCalendarDays(base, 1); + const queryToken: QueryTokenDTO = { + filter: { + orderitemprocessingstatus: '16;8192;1024;512;2048', + estimatedshippingdate: `"${startDate.toJSON()}"-"${endDate.toJSON()}"`, + }, + orderBy: [{ by: 'estimatedshippingdate' }], + skip: 0, + take: 20, + }; + return this.searchWareneingang(queryToken); + } + + @memorize() + goodsInQuerySettings() { + return this.abholfachService + .AbholfachWareneingangQuerySettings() + .pipe(shareReplay()); + } + + @memorize() + goodsOutQuerySettings() { + return this.abholfachService + .AbholfachWarenausgabeQuerySettings() + .pipe(shareReplay()); + } + + goodsInList(queryToken: QueryTokenDTO) { + return this.abholfachService.AbholfachWareneingangsliste(queryToken); + } + + @memorize() + goodsInListQuerySettings() { + return this.abholfachService + .AbholfachWareneingangslisteQuerySettings() + .pipe(shareReplay()); + } + + goodsInCleanupList() { + return this.abholfachService.AbholfachAbholfachbereinigungsliste(); + } + + goodsInReservationList(queryToken: QueryTokenDTO) { + return this.abholfachService.AbholfachReservierungen(queryToken); + } + + goodsInRemissionPreviewList() { + return this.abholfachService.AbholfachAbholfachremissionsvorschau(); + } + + createGoodsInRemissionFromPreviewList() { + return this.abholfachService.AbholfachCreateAbholfachremission(); + } +} diff --git a/apps/isa-app/src/domain/oms/index.ts b/apps/isa-app/src/domain/oms/index.ts index bad02eec9..2232f38ba 100644 --- a/apps/isa-app/src/domain/oms/index.ts +++ b/apps/isa-app/src/domain/oms/index.ts @@ -2,6 +2,5 @@ export * from './action-handler-services'; export * from './action-handlers'; export * from './customer-order.service'; export * from './goods.service'; -export * from './oms.module'; export * from './oms.service'; export * from './receipt.service'; diff --git a/apps/isa-app/src/domain/oms/oms.module.ts b/apps/isa-app/src/domain/oms/oms.module.ts deleted file mode 100644 index fc5cb7150..000000000 --- a/apps/isa-app/src/domain/oms/oms.module.ts +++ /dev/null @@ -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 { - return { - ngModule: DomainOmsModule, - providers: [DomainOmsService, DomainGoodsService, DomainReceiptService], - }; - } -} diff --git a/apps/isa-app/src/domain/oms/oms.service.ts b/apps/isa-app/src/domain/oms/oms.service.ts index b91e0750c..edce2cf99 100644 --- a/apps/isa-app/src/domain/oms/oms.service.ts +++ b/apps/isa-app/src/domain/oms/oms.service.ts @@ -1,316 +1,381 @@ -import { Injectable } from '@angular/core'; -import { - BranchService, - BuyerDTO, - ChangeStockStatusCodeValues, - HistoryDTO, - NotificationChannel, - OrderCheckoutService, - OrderDTO, - OrderItemDTO, - OrderItemSubsetDTO, - OrderListItemDTO, - OrderService, - ReceiptService, - StatusValues, - StockStatusCodeService, - ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO, - ValueTupleOfOrderItemSubsetDTOAndOrderItemSubsetDTO, - VATService, -} from '@generated/swagger/oms-api'; -import { memorize } from '@utils/common'; -import { Observable } from 'rxjs'; -import { map, shareReplay } from 'rxjs/operators'; - -@Injectable() -export class DomainOmsService { - constructor( - private orderService: OrderService, - private receiptService: ReceiptService, - private branchService: BranchService, - private vatService: VATService, - private stockStatusCodeService: StockStatusCodeService, - private _orderCheckoutService: OrderCheckoutService, - ) {} - - getOrderItemsByCustomerNumber(customerNumber: string, skip: number): Observable { - return this.orderService - .OrderGetOrdersByBuyerNumber({ buyerNumber: customerNumber, take: 20, skip }) - .pipe(map((orders) => orders.result)); - } - - getOrder(orderId: number): Observable { - return this.orderService.OrderGetOrder(orderId).pipe(map((o) => o.result)); - } - - getBranches() { - return this.branchService.BranchGetBranches({}); - } - - getHistory(orderItemSubsetId: number): Observable { - return this.orderService - .OrderGetOrderItemStatusHistory({ orderItemSubsetId }) - .pipe(map((response) => response.result)); - } - - getReceipts( - orderItemSubsetIds: number[], - ): Observable { - return this.receiptService - .ReceiptGetReceiptsByOrderItemSubset({ - payload: { - receiptType: 65 as unknown as any, - ids: orderItemSubsetIds, - eagerLoading: 1, - }, - }) - .pipe(map((response) => response.result)); - } - - getReorderReasons() { - return this._orderCheckoutService.OrderCheckoutGetReorderReasons().pipe(map((response) => response.result)); - } - - @memorize() - getVATs() { - return this.vatService.VATGetVATs({}).pipe(map((response) => response.result)); - } - - // ttl 4 Stunden - @memorize({ ttl: 14400000 }) - getStockStatusCodes({ supplierId, eagerLoading = 0 }: { supplierId: number; eagerLoading?: number }) { - return this.stockStatusCodeService.StockStatusCodeGetStockStatusCodes({ supplierId, eagerLoading }).pipe( - map((response) => response.result), - shareReplay(), - ); - } - - patchOrderItem(payload: { orderItemId: number; orderId: number; orderItem: Partial }) { - return this.orderService.OrderPatchOrderItem(payload).pipe(map((response) => response.result)); - } - - patchOrderItemSubset(payload: { - orderItemSubsetId: number; - orderItemId: number; - orderId: number; - orderItemSubset: Partial; - }) { - return this.orderService.OrderPatchOrderItemSubset(payload).pipe(map((response) => response.result)); - } - - patchComment({ - orderId, - orderItemId, - orderItemSubsetId, - specialComment, - }: { - orderId: number; - orderItemId: number; - orderItemSubsetId: number; - specialComment: string; - }) { - return this.orderService - .OrderPatchOrderItemSubset({ - orderId, - orderItemId, - orderItemSubsetId, - orderItemSubset: { - specialComment, - }, - }) - .pipe(map((response) => response.result)); - } - - changeOrderStatus( - orderId: number, - orderItemId: number, - orderItemSubsetId: number, - data: StatusValues, - ): Observable { - return this.orderService - .OrderChangeStatus({ - data, - orderId, - orderItemId, - orderItemSubsetId, - }) - .pipe(map((o) => o.result)); - } - - setEstimatedShippingDate( - orderId: number, - orderItemId: number, - orderItemSubsetId: number, - estimatedShippingDate: Date | string, - ) { - return this.orderService - .OrderPatchOrderItemSubset({ - orderId, - orderItemId, - orderItemSubsetId, - orderItemSubset: { - estimatedShippingDate: - estimatedShippingDate instanceof Date ? estimatedShippingDate.toJSON() : estimatedShippingDate, - }, - }) - .pipe(map((response) => response.result)); - } - - setPickUpDeadline(orderId: number, orderItemId: number, orderItemSubsetId: number, pickUpDeadline: string) { - return this.orderService - .OrderPatchOrderItemSubset({ - orderId, - orderItemId, - orderItemSubsetId, - orderItemSubset: { - compartmentStop: pickUpDeadline, - }, - }) - .pipe(map((response) => response.result)); - } - - setPreferredPickUpDate({ data }: { data: { [key: string]: string } }) { - return this.orderService.OrderSetPreferredPickUpDate({ data }); - } - - changeOrderItemStatus(data: OrderService.OrderChangeStatusParams) { - return this.orderService.OrderChangeStatus(data); - } - - changeStockStatusCode(payload: ChangeStockStatusCodeValues[]) { - return this.orderService.OrderChangeStockStatusCode(payload).pipe(map((response) => response.result)); - } - - orderAtSupplier({ - orderId, - orderItemId, - orderItemSubsetId, - }: { - orderId: number; - orderItemId: number; - orderItemSubsetId: number; - }) { - return this._orderCheckoutService.OrderCheckoutOrderSubsetItemAtSupplier({ - orderId, - orderItemId, - orderItemSubsetId, - }); - } - - getNotifications(orderId: number): Observable<{ selected: NotificationChannel; email: string; mobile: string }> { - return this.getOrder(orderId).pipe( - map((order) => ({ - selected: order.notificationChannels, - email: order.buyer?.communicationDetails?.email, - mobile: order.buyer?.communicationDetails?.mobile, - })), - ); - } - - getOrderSource(orderId: number): Observable { - return this.getOrder(orderId).pipe(map((order) => order?.features?.orderSource)); - } - - updateNotifications(orderId: number, changes: { selected: NotificationChannel; email: string; mobile: string }) { - const communicationDetails = { - email: changes.email, - mobile: changes.mobile, - }; - - if (!(changes.selected & 1)) { - delete communicationDetails.email; - } - if (!(changes.selected & 2)) { - delete communicationDetails.mobile; - } - - return this.updateOrder({ orderId, notificationChannels: changes.selected, communicationDetails }); - } - - updateOrder({ - orderId, - notificationChannels, - communicationDetails, - firstName, - lastName, - organisation, - }: { - orderId: number; - notificationChannels?: NotificationChannel; - communicationDetails?: { email?: string; mobile?: string }; - lastName?: string; - firstName?: string; - organisation?: string; - }) { - const buyer: BuyerDTO = {}; - - if (communicationDetails) { - buyer.communicationDetails = { ...communicationDetails }; - } - - if (!!lastName || !!firstName) { - buyer.lastName = lastName; - buyer.firstName = firstName; - } - - if (!!organisation && !!buyer.organisation) { - buyer.organisation = { - 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> { - 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, - ), - ), - ); - } -} +import { Injectable } from '@angular/core'; +import { + BranchService, + BuyerDTO, + ChangeStockStatusCodeValues, + HistoryDTO, + NotificationChannel, + OrderCheckoutService, + OrderDTO, + OrderItemDTO, + OrderItemSubsetDTO, + OrderListItemDTO, + OrderService, + ReceiptService, + StatusValues, + StockStatusCodeService, + ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO, + ValueTupleOfOrderItemSubsetDTOAndOrderItemSubsetDTO, + VATService, +} from '@generated/swagger/oms-api'; +import { memorize } from '@utils/common'; +import { Observable } from 'rxjs'; +import { map, shareReplay } from 'rxjs/operators'; + +@Injectable({ providedIn: 'root' }) +export class DomainOmsService { + constructor( + private orderService: OrderService, + private receiptService: ReceiptService, + private branchService: BranchService, + private vatService: VATService, + private stockStatusCodeService: StockStatusCodeService, + private _orderCheckoutService: OrderCheckoutService, + ) {} + + getOrderItemsByCustomerNumber( + customerNumber: string, + skip: number, + ): Observable { + return this.orderService + .OrderGetOrdersByBuyerNumber({ + buyerNumber: customerNumber, + take: 20, + skip, + }) + .pipe(map((orders) => orders.result)); + } + + getOrder(orderId: number): Observable { + return this.orderService.OrderGetOrder(orderId).pipe(map((o) => o.result)); + } + + getBranches() { + return this.branchService.BranchGetBranches({}); + } + + getHistory(orderItemSubsetId: number): Observable { + return this.orderService + .OrderGetOrderItemStatusHistory({ orderItemSubsetId }) + .pipe(map((response) => response.result)); + } + + getReceipts( + orderItemSubsetIds: number[], + ): Observable< + ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO[] + > { + return this.receiptService + .ReceiptGetReceiptsByOrderItemSubset({ + payload: { + receiptType: 65 as unknown as any, + ids: orderItemSubsetIds, + eagerLoading: 1, + }, + }) + .pipe(map((response) => response.result)); + } + + getReorderReasons() { + return this._orderCheckoutService + .OrderCheckoutGetReorderReasons() + .pipe(map((response) => response.result)); + } + + @memorize() + getVATs() { + return this.vatService + .VATGetVATs({}) + .pipe(map((response) => response.result)); + } + + // ttl 4 Stunden + @memorize({ ttl: 14400000 }) + getStockStatusCodes({ + supplierId, + eagerLoading = 0, + }: { + supplierId: number; + eagerLoading?: number; + }) { + return this.stockStatusCodeService + .StockStatusCodeGetStockStatusCodes({ supplierId, eagerLoading }) + .pipe( + map((response) => response.result), + shareReplay(), + ); + } + + patchOrderItem(payload: { + orderItemId: number; + orderId: number; + orderItem: Partial; + }) { + return this.orderService + .OrderPatchOrderItem(payload) + .pipe(map((response) => response.result)); + } + + patchOrderItemSubset(payload: { + orderItemSubsetId: number; + orderItemId: number; + orderId: number; + orderItemSubset: Partial; + }) { + return this.orderService + .OrderPatchOrderItemSubset(payload) + .pipe(map((response) => response.result)); + } + + patchComment({ + orderId, + orderItemId, + orderItemSubsetId, + specialComment, + }: { + orderId: number; + orderItemId: number; + orderItemSubsetId: number; + specialComment: string; + }) { + return this.orderService + .OrderPatchOrderItemSubset({ + orderId, + orderItemId, + orderItemSubsetId, + orderItemSubset: { + specialComment, + }, + }) + .pipe(map((response) => response.result)); + } + + changeOrderStatus( + orderId: number, + orderItemId: number, + orderItemSubsetId: number, + data: StatusValues, + ): Observable { + return this.orderService + .OrderChangeStatus({ + data, + orderId, + orderItemId, + orderItemSubsetId, + }) + .pipe(map((o) => o.result)); + } + + setEstimatedShippingDate( + orderId: number, + orderItemId: number, + orderItemSubsetId: number, + estimatedShippingDate: Date | string, + ) { + return this.orderService + .OrderPatchOrderItemSubset({ + orderId, + orderItemId, + orderItemSubsetId, + orderItemSubset: { + estimatedShippingDate: + estimatedShippingDate instanceof Date + ? estimatedShippingDate.toJSON() + : estimatedShippingDate, + }, + }) + .pipe(map((response) => response.result)); + } + + setPickUpDeadline( + orderId: number, + orderItemId: number, + orderItemSubsetId: number, + pickUpDeadline: string, + ) { + return this.orderService + .OrderPatchOrderItemSubset({ + orderId, + orderItemId, + orderItemSubsetId, + orderItemSubset: { + compartmentStop: pickUpDeadline, + }, + }) + .pipe(map((response) => response.result)); + } + + setPreferredPickUpDate({ data }: { data: { [key: string]: string } }) { + return this.orderService.OrderSetPreferredPickUpDate({ data }); + } + + changeOrderItemStatus(data: OrderService.OrderChangeStatusParams) { + return this.orderService.OrderChangeStatus(data); + } + + changeStockStatusCode(payload: ChangeStockStatusCodeValues[]) { + return this.orderService + .OrderChangeStockStatusCode(payload) + .pipe(map((response) => response.result)); + } + + orderAtSupplier({ + orderId, + orderItemId, + orderItemSubsetId, + }: { + orderId: number; + orderItemId: number; + orderItemSubsetId: number; + }) { + return this._orderCheckoutService.OrderCheckoutOrderSubsetItemAtSupplier({ + orderId, + orderItemId, + orderItemSubsetId, + }); + } + + getNotifications( + orderId: number, + ): Observable<{ + selected: NotificationChannel; + email: string; + mobile: string; + }> { + return this.getOrder(orderId).pipe( + map((order) => ({ + selected: order.notificationChannels, + email: order.buyer?.communicationDetails?.email, + mobile: order.buyer?.communicationDetails?.mobile, + })), + ); + } + + getOrderSource(orderId: number): Observable { + return this.getOrder(orderId).pipe( + map((order) => order?.features?.orderSource), + ); + } + + updateNotifications( + orderId: number, + changes: { selected: NotificationChannel; email: string; mobile: string }, + ) { + const communicationDetails = { + email: changes.email, + mobile: changes.mobile, + }; + + if (!(changes.selected & 1)) { + delete communicationDetails.email; + } + if (!(changes.selected & 2)) { + delete communicationDetails.mobile; + } + + return this.updateOrder({ + orderId, + notificationChannels: changes.selected, + communicationDetails, + }); + } + + updateOrder({ + orderId, + notificationChannels, + communicationDetails, + firstName, + lastName, + organisation, + }: { + orderId: number; + notificationChannels?: NotificationChannel; + communicationDetails?: { email?: string; mobile?: string }; + lastName?: string; + firstName?: string; + organisation?: string; + }) { + const buyer: BuyerDTO = {}; + + if (communicationDetails) { + buyer.communicationDetails = { ...communicationDetails }; + } + + if (!!lastName || !!firstName) { + buyer.lastName = lastName; + buyer.firstName = firstName; + } + + if (!!organisation && !!buyer.organisation) { + buyer.organisation = { + 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> { + 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, + ), + ), + ); + } +} diff --git a/apps/isa-app/src/domain/oms/receipt.service.ts b/apps/isa-app/src/domain/oms/receipt.service.ts index 28295f268..1ec43fa22 100644 --- a/apps/isa-app/src/domain/oms/receipt.service.ts +++ b/apps/isa-app/src/domain/oms/receipt.service.ts @@ -1,22 +1,25 @@ -import { Injectable } from '@angular/core'; -import { ReceiptOrderItemSubsetReferenceValues, ReceiptService } from '@generated/swagger/oms-api'; -import { memorize } from '@utils/common'; -import { shareReplay } from 'rxjs/operators'; - -@Injectable() -export class DomainReceiptService { - constructor(private receiptService: ReceiptService) {} - - createShippingNotes(params: ReceiptService.ReceiptCreateShippingNote2Params) { - return this.receiptService.ReceiptCreateShippingNote2(params); - } - - @memorize({ ttl: 1000 }) - getReceipts(payload: ReceiptOrderItemSubsetReferenceValues) { - return this.receiptService - .ReceiptGetReceiptsByOrderItemSubset({ - payload: payload, - }) - .pipe(shareReplay(1)); - } -} +import { Injectable } from '@angular/core'; +import { + ReceiptOrderItemSubsetReferenceValues, + ReceiptService, +} from '@generated/swagger/oms-api'; +import { memorize } from '@utils/common'; +import { shareReplay } from 'rxjs/operators'; + +@Injectable({ providedIn: 'root' }) +export class DomainReceiptService { + constructor(private receiptService: ReceiptService) {} + + createShippingNotes(params: ReceiptService.ReceiptCreateShippingNote2Params) { + return this.receiptService.ReceiptCreateShippingNote2(params); + } + + @memorize({ ttl: 1000 }) + getReceipts(payload: ReceiptOrderItemSubsetReferenceValues) { + return this.receiptService + .ReceiptGetReceiptsByOrderItemSubset({ + payload: payload, + }) + .pipe(shareReplay(1)); + } +} diff --git a/apps/isa-app/src/domain/remission/index.ts b/apps/isa-app/src/domain/remission/index.ts index 03ddd7733..88a10f1f0 100644 --- a/apps/isa-app/src/domain/remission/index.ts +++ b/apps/isa-app/src/domain/remission/index.ts @@ -1,4 +1,3 @@ export * from './defs'; export * from './mappings'; -export * from './remission.module'; export * from './remission.service'; diff --git a/apps/isa-app/src/domain/remission/remission.module.ts b/apps/isa-app/src/domain/remission/remission.module.ts deleted file mode 100644 index 338cb7eb9..000000000 --- a/apps/isa-app/src/domain/remission/remission.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ModuleWithProviders, NgModule } from '@angular/core'; -import { DomainRemissionService } from './remission.service'; - -@NgModule({}) -export class DomainRemissionModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: RootDomainRemissionModule, - }; - } -} - -@NgModule({ - imports: [], - providers: [DomainRemissionService], -}) -export class RootDomainRemissionModule {} diff --git a/apps/isa-app/src/domain/remission/remission.service.ts b/apps/isa-app/src/domain/remission/remission.service.ts index 5a70d9195..b45709199 100644 --- a/apps/isa-app/src/domain/remission/remission.service.ts +++ b/apps/isa-app/src/domain/remission/remission.service.ts @@ -1,618 +1,618 @@ -import { Injectable } from '@angular/core'; -import { - ItemDTO, - ListResponseArgsOfItemDTO, - SearchService, -} from '@generated/swagger/cat-search-api'; -import { - RemiService, - StockService, - SupplierService, - ReturnService, - RemiQueryTokenDTO, - QueryTokenDTO, - ReturnItemDTO, - StockDTO, - ReceiptDTO, - ReturnDTO, - ReturnQueryTokenDTO, - BatchResponseArgsOfReturnItemDTOAndReturnItemDTO, -} from '@generated/swagger/inventory-api'; -import { memorize } from '@utils/common'; -import { Observable, of, throwError } from 'rxjs'; -import { catchError, map, shareReplay, switchMap } from 'rxjs/operators'; -import { RemissionListItem } from './defs'; -import { - fromItemDto, - mapFromReturnItemDTO, - mapFromReturnSuggestionDTO, -} from './mappings'; -import { Logger } from '@core/logger'; -import { RemissionPlacementType } from '@domain/remission'; - -@Injectable() -export class DomainRemissionService { - constructor( - private readonly _logger: Logger, - private readonly _remiService: RemiService, - private readonly _stockService: StockService, - private readonly _supplierService: SupplierService, - private readonly _returnService: ReturnService, - private readonly _search: SearchService, - ) {} - - @memorize() - getCurrentStock() { - return this._stockService.StockCurrentStock().pipe( - map((res) => res.result), - catchError((err: Error) => { - this._logger.error('Fehler beim Laden des aktuellen Lagers', err); - return throwError(err); - }), - shareReplay(), - ); - } - - @memorize() - getSuppliers() { - return this.getCurrentStock().pipe( - switchMap((stock) => - this._supplierService.SupplierGetSuppliers({ - stockId: stock.id, - }), - ), - map((res) => res.result), - catchError((err: Error) => { - this._logger.error('Fehler beim Laden der Lieferanten', err); - return throwError(err); - }), - shareReplay(), - ); - } - - getSources() { - return of(['Pflichtremission', 'Abteilungsremission']); - } - - getQuerySettings(arg: { source: string; supplierId: number }) { - return this.getCurrentStock().pipe( - switchMap((stock) => - this._remiService - .RemiRemissionQuerySettings({ - remiType: arg.source, - supplierId: arg.supplierId, - stockId: stock.id, - }) - .pipe(map((res) => res.result)), - ), - catchError((err: Error) => { - this._logger.error('Fehler beim Laden des Filters', err); - return throwError(err); - }), - ); - } - - @memorize() - getProductGroups() { - return this.getCurrentStock().pipe( - switchMap((stock) => - this._remiService.RemiProductgroups({ stockId: stock.id }).pipe( - map((res) => res.result), - catchError((err: Error) => { - this._logger.error('Fehler beim Laden der Produktgruppen', err); - return throwError(err); - }), - ), - ), - shareReplay(), - ); - } - - @memorize() - getReturnReasons() { - return this.getCurrentStock().pipe( - switchMap((stock) => - this._returnService - .ReturnGetReturnReasons({ - stockId: stock.id, - }) - .pipe( - map((res) => res.result), - catchError((err: Error) => { - this._logger.error('Fehler beim Laden der Remigünde', err); - return throwError(err); - }), - ), - ), - shareReplay(), - ); - } - - getItems(arg: { - source: string; - supplierId: number; - queryToken: QueryTokenDTO; - }): Observable<{ hits: number; result: RemissionListItem[] }> { - let result$: Observable<{ hits: number; result: RemissionListItem[] }>; - - if (arg.source === 'Pflichtremission') { - result$ = this.getCurrentStock().pipe( - switchMap((stock) => - this.getItemsForPflichtremission({ - queryToken: { - stockId: stock.id, - supplierId: arg.supplierId, - ...arg.queryToken, - }, - }), - ), - ); - } else if (arg.source === 'Abteilungsremission') { - if (!arg.queryToken.filter.abteilungen) { - result$ = of({ hits: 0, result: [] }); - } else { - result$ = this.getCurrentStock().pipe( - switchMap((stock) => - this.getItemsForAbteilungsremission({ - queryToken: { - stockId: stock.id, - supplierId: arg.supplierId, - ...arg.queryToken, - }, - }), - ), - ); - } - } else { - this._logger.error('Unbekannte Quelle', arg.source); - return throwError(new Error(`Unknown source: ${arg.source}`)); - } - - return result$.pipe( - switchMap((res) => - res?.hits - ? this.getStockInformation(res.result).pipe( - map((result) => ({ - hits: res.hits, - result, - })), - ) - : of(res), - ), - ); - } - - getItemsForPflichtremission(arg: { - queryToken: RemiQueryTokenDTO; - }): Observable<{ hits: number; result: RemissionListItem[] }> { - return this._remiService - .RemiPflichtremissionsartikel({ - queryToken: arg.queryToken, - }) - .pipe( - map((res) => ({ - hits: res.hits, - result: res.result.map(mapFromReturnItemDTO), - })), - ); - } - - getItemsForAbteilungsremission(arg: { - queryToken: RemiQueryTokenDTO; - }): Observable<{ hits: number; result: RemissionListItem[] }> { - return this._remiService - .RemiUeberlauf({ - queryToken: arg.queryToken, - }) - .pipe( - map((res) => ({ - hits: res.hits, - result: res.result.map(mapFromReturnSuggestionDTO), - })), - ); - } - - getStockInformation( - items: RemissionListItem[], - recalculate: boolean = false, - ) { - return this.getCurrentStock().pipe( - switchMap((stock) => - this._stockService - .StockInStock({ - stockId: stock.id, - articleIds: items - .filter((item) => !!item.dto.product.catalogProductNumber) - .map((item) => +item.dto.product.catalogProductNumber), - }) - .pipe( - map((res) => { - const o = items.map((item) => { - const stockInfo = res?.result?.find( - (stockInfo) => - stockInfo.itemId === +item.dto.product.catalogProductNumber, - ); - - if (!stockInfo) { - const defaultStockData = { - inStock: 0, - remainingQuantity: 0, - remissionQuantity: item.remissionQuantity || 0, - }; - - return { ...item, ...defaultStockData }; - } - - const availableStock = - stockInfo.inStock - stockInfo.removedFromStock; - const inStock = availableStock < 0 ? 0 : availableStock; - - let { remainingQuantity, remissionQuantity } = item; - - if (!remissionQuantity || recalculate) { - remissionQuantity = inStock - (remainingQuantity || 0); - if (remissionQuantity < 0) { - remissionQuantity = 0; - } - } - if (!remainingQuantity || recalculate) { - remainingQuantity = inStock - (remissionQuantity || 0); - if (remainingQuantity < 0) { - remainingQuantity = 0; - } - } - - return { - ...item, - remainingQuantity, - remissionQuantity, - inStock, - }; - }); - - return o; - }), - ), - ), - ); - } - - getRequiredCapacities(params: { - departments?: string[]; - supplierId: number; - }) { - return this.getCurrentStock().pipe( - switchMap((stock) => - this._remiService - .RemiGetRequiredCapacities({ - stockId: stock?.id, - payload: { - departments: params?.departments || [], - supplierId: params?.supplierId, - }, - }) - .pipe(map((res) => res.result)), - ), - ); - } - - searchItemToRemit(ean: string): Observable { - return this.getCurrentStock().pipe( - switchMap((stock) => - this._search - .SearchSearch({ - stockId: stock.id, - input: { qs: ean }, - doNotTrack: true, - }) - .pipe( - catchError((err) => of({ hits: 0, result: [] })), - map((res) => [res, stock] as [ListResponseArgsOfItemDTO, StockDTO]), - ), - ), - map(([res, stock]) => { - if (res.hits === 0) { - return undefined; - } - - const item = res.result[0] as ItemDTO; - - return fromItemDto(item, stock); - }), - ); - } - - canAddReturnItem( - item: ReturnItemDTO, - ): Observable { - return this._remiService.RemiCanAddReturnItem({ - data: [item], - }); - } - - async createReturn( - supplierId: number, - returnGroup?: string, - ): Promise { - const response = await this._returnService - .ReturnCreateReturn({ - data: { - supplier: { id: supplierId }, - returnGroup: returnGroup ?? String(Date.now()), - }, - }) - .toPromise(); - - return response.result; - } - - completeReturn(returnId: number) { - return this._returnService.ReturnFinalizeReturn({ returnId }).toPromise(); - } - - async completeRemission(returnId: number): Promise { - const returnDto = await this.getReturn(returnId).toPromise(); - const response = await this._returnService - .ReturnFinalizeReturnGroup({ - returnGroup: returnDto.returnGroup, - }) - .toPromise(); - - return response.result; - } - - async deleteRemission(returnId: number): Promise { - await this._returnService - .ReturnCancelReturn({ - returnId, - }) - .toPromise(); - } - - getReturns(params: { - start?: Date; - returncompleted: boolean; - }): Observable { - const queryToken: ReturnQueryTokenDTO = { - start: params.start?.toISOString(), - filter: { - returncompleted: params.returncompleted ? 'true' : 'false', - }, - eagerLoading: 3, - input: {}, - }; - - Object.keys(queryToken).forEach((key) => { - if (!queryToken[key]) { - delete queryToken[key]; - } - }); - - return this.getCurrentStock().pipe( - switchMap((stock) => - this._returnService.ReturnQueryReturns({ - stockId: stock.id, - queryToken, - }), - ), - map((res) => res.result), - ); - } - - getReturn(returnId: number): Observable { - return this._returnService - .ReturnGetReturn({ returnId, eagerLoading: 3 }) - .pipe(map((res) => res.result)); - } - - async deleteReturn(returnId: number) { - const returnDto = await this.getReturn(returnId).toPromise(); - for (const receipt of returnDto?.receipts) { - await this.deleteReceipt(returnDto.id, receipt.id); - } - await this.deleteRemission(returnDto.id); - } - - addReturnItem({ - returnId, - receiptId, - returnItemId, - quantity, - placementType, - inStock, - }: { - returnId: number; - receiptId: number; - returnItemId: number; - quantity?: number; - placementType?: RemissionPlacementType; - inStock: number; - }) { - return this._returnService - .ReturnAddReturnItem({ - returnId, - receiptId, - data: { returnItemId, quantity, placementType, inStock }, - }) - .pipe(map((r) => r.result)); - } - - addReturnSuggestion({ - returnId, - receiptId, - returnSuggestionId, - quantity, - placementType, - inStock, - impedimentComment, - remainingQuantity, - }: { - returnId: number; - receiptId: number; - returnSuggestionId: number; - quantity?: number; - placementType?: RemissionPlacementType; - inStock: number; - impedimentComment: string; - remainingQuantity: number; - }) { - return this._returnService - .ReturnAddReturnSuggestion({ - returnId, - receiptId, - data: { - returnSuggestionId, - quantity, - placementType, - inStock, - impedimentComment, - remainingQuantity, - }, - }) - .pipe(map((r) => r.result)); - } - - removeReturnItemFromList({ itemId }: { itemId: number }) { - return this._returnService.ReturnDeleteReturnItem({ itemId }); - } - - removeReturnItemFromReceipt({ - returnId, - receiptId, - receiptItemId, - }: { - returnId: number; - receiptId: number; - receiptItemId: number; - }) { - return this._returnService.ReturnRemoveReturnItem({ - returnId, - receiptItemId, - receiptId, - }); - } - - returnImpediment(itemId: number) { - return this._returnService - .ReturnReturnItemImpediment({ - itemId, - data: { comment: 'Produkt nicht gefunden' }, - }) - .pipe(map((r) => r.result)); - } - - returnSuggestion(itemId: number) { - return this._returnService - .ReturnReturnSuggestionImpediment({ - itemId, - data: { comment: 'Produkt nicht gefunden' }, - }) - .pipe(map((r) => r.result)); - } - - /** - * Create a new receipt for the given return/remission - * @param returnId Return ID - * @param receiptNumber Receipt number - * @returns ReceiptDTO - */ - async createReceipt( - returnDTO: ReturnDTO, - receiptNumber?: string, - ): Promise { - const stock = await this._getStock(); - - const response = await this._returnService - .ReturnCreateReceipt({ - returnId: returnDTO.id, - data: { - receiptNumber: receiptNumber ?? null, - stock: { - id: stock.id, - }, - supplier: { id: returnDTO.supplier.id }, - receiptType: 1, // ShippingNote = 1 - }, - }) - .toPromise(); - - const receipt: ReceiptDTO = response.result; - - return receipt; - } - - /** - * Create a new Package and assign it to a receipt - * @param returnId Return ID - * @param receiptId Receipt ID - * @param packageNumber Packagenumber - * @returns ReceiptDTO - */ - async createReceiptAndAssignPackage({ - returnId, - receiptId, - packageNumber, - }: { - returnId: number; - receiptId: number; - packageNumber: string; - }): Promise { - const response = await this._returnService - .ReturnCreateAndAssignPackage({ - returnId, - receiptId, - data: { - packageNumber, - }, - }) - .toPromise(); - const receipt: ReceiptDTO = response.result; - return receipt; - } - - async completeReceipt( - returnId: number, - receiptId: number, - ): Promise { - const res = await this._returnService - .ReturnFinalizeReceipt({ - returnId, - receiptId, - data: {}, - }) - .toPromise(); - - const result = res.result; - - return result; - } - - async deleteReceipt(returnId: number, receiptId: number): Promise { - await this._returnService - .ReturnCancelReturnReceipt({ - returnId, - receiptId, - }) - .toPromise(); - } - - addProductToRemit(item: ReturnItemDTO, reason: string, quantity: number) { - return this._remiService.RemiCreateReturnItem({ - data: [ - { - assortment: item.assortment, - product: item.product, - returnReason: reason, - predefinedReturnQuantity: quantity, - stock: item.stock, - retailPrice: item.retailPrice, - source: 'manually-added', - }, - ], - }); - } - - private _getStock() { - return this.getCurrentStock().toPromise(); - } -} +import { Injectable } from '@angular/core'; +import { + ItemDTO, + ListResponseArgsOfItemDTO, + SearchService, +} from '@generated/swagger/cat-search-api'; +import { + RemiService, + StockService, + SupplierService, + ReturnService, + RemiQueryTokenDTO, + QueryTokenDTO, + ReturnItemDTO, + StockDTO, + ReceiptDTO, + ReturnDTO, + ReturnQueryTokenDTO, + BatchResponseArgsOfReturnItemDTOAndReturnItemDTO, +} from '@generated/swagger/inventory-api'; +import { memorize } from '@utils/common'; +import { Observable, of, throwError } from 'rxjs'; +import { catchError, map, shareReplay, switchMap } from 'rxjs/operators'; +import { RemissionListItem } from './defs'; +import { + fromItemDto, + mapFromReturnItemDTO, + mapFromReturnSuggestionDTO, +} from './mappings'; +import { Logger } from '@core/logger'; +import { RemissionPlacementType } from '@domain/remission'; + +@Injectable({ providedIn: 'root' }) +export class DomainRemissionService { + constructor( + private readonly _logger: Logger, + private readonly _remiService: RemiService, + private readonly _stockService: StockService, + private readonly _supplierService: SupplierService, + private readonly _returnService: ReturnService, + private readonly _search: SearchService, + ) {} + + @memorize() + getCurrentStock() { + return this._stockService.StockCurrentStock().pipe( + map((res) => res.result), + catchError((err: Error) => { + this._logger.error('Fehler beim Laden des aktuellen Lagers', err); + return throwError(err); + }), + shareReplay(), + ); + } + + @memorize() + getSuppliers() { + return this.getCurrentStock().pipe( + switchMap((stock) => + this._supplierService.SupplierGetSuppliers({ + stockId: stock.id, + }), + ), + map((res) => res.result), + catchError((err: Error) => { + this._logger.error('Fehler beim Laden der Lieferanten', err); + return throwError(err); + }), + shareReplay(), + ); + } + + getSources() { + return of(['Pflichtremission', 'Abteilungsremission']); + } + + getQuerySettings(arg: { source: string; supplierId: number }) { + return this.getCurrentStock().pipe( + switchMap((stock) => + this._remiService + .RemiRemissionQuerySettings({ + remiType: arg.source, + supplierId: arg.supplierId, + stockId: stock.id, + }) + .pipe(map((res) => res.result)), + ), + catchError((err: Error) => { + this._logger.error('Fehler beim Laden des Filters', err); + return throwError(err); + }), + ); + } + + @memorize() + getProductGroups() { + return this.getCurrentStock().pipe( + switchMap((stock) => + this._remiService.RemiProductgroups({ stockId: stock.id }).pipe( + map((res) => res.result), + catchError((err: Error) => { + this._logger.error('Fehler beim Laden der Produktgruppen', err); + return throwError(err); + }), + ), + ), + shareReplay(), + ); + } + + @memorize() + getReturnReasons() { + return this.getCurrentStock().pipe( + switchMap((stock) => + this._returnService + .ReturnGetReturnReasons({ + stockId: stock.id, + }) + .pipe( + map((res) => res.result), + catchError((err: Error) => { + this._logger.error('Fehler beim Laden der Remigünde', err); + return throwError(err); + }), + ), + ), + shareReplay(), + ); + } + + getItems(arg: { + source: string; + supplierId: number; + queryToken: QueryTokenDTO; + }): Observable<{ hits: number; result: RemissionListItem[] }> { + let result$: Observable<{ hits: number; result: RemissionListItem[] }>; + + if (arg.source === 'Pflichtremission') { + result$ = this.getCurrentStock().pipe( + switchMap((stock) => + this.getItemsForPflichtremission({ + queryToken: { + stockId: stock.id, + supplierId: arg.supplierId, + ...arg.queryToken, + }, + }), + ), + ); + } else if (arg.source === 'Abteilungsremission') { + if (!arg.queryToken.filter.abteilungen) { + result$ = of({ hits: 0, result: [] }); + } else { + result$ = this.getCurrentStock().pipe( + switchMap((stock) => + this.getItemsForAbteilungsremission({ + queryToken: { + stockId: stock.id, + supplierId: arg.supplierId, + ...arg.queryToken, + }, + }), + ), + ); + } + } else { + this._logger.error('Unbekannte Quelle', arg.source); + return throwError(new Error(`Unknown source: ${arg.source}`)); + } + + return result$.pipe( + switchMap((res) => + res?.hits + ? this.getStockInformation(res.result).pipe( + map((result) => ({ + hits: res.hits, + result, + })), + ) + : of(res), + ), + ); + } + + getItemsForPflichtremission(arg: { + queryToken: RemiQueryTokenDTO; + }): Observable<{ hits: number; result: RemissionListItem[] }> { + return this._remiService + .RemiPflichtremissionsartikel({ + queryToken: arg.queryToken, + }) + .pipe( + map((res) => ({ + hits: res.hits, + result: res.result.map(mapFromReturnItemDTO), + })), + ); + } + + getItemsForAbteilungsremission(arg: { + queryToken: RemiQueryTokenDTO; + }): Observable<{ hits: number; result: RemissionListItem[] }> { + return this._remiService + .RemiUeberlauf({ + queryToken: arg.queryToken, + }) + .pipe( + map((res) => ({ + hits: res.hits, + result: res.result.map(mapFromReturnSuggestionDTO), + })), + ); + } + + getStockInformation( + items: RemissionListItem[], + recalculate = false, + ) { + return this.getCurrentStock().pipe( + switchMap((stock) => + this._stockService + .StockInStock({ + stockId: stock.id, + articleIds: items + .filter((item) => !!item.dto.product.catalogProductNumber) + .map((item) => +item.dto.product.catalogProductNumber), + }) + .pipe( + map((res) => { + const o = items.map((item) => { + const stockInfo = res?.result?.find( + (stockInfo) => + stockInfo.itemId === +item.dto.product.catalogProductNumber, + ); + + if (!stockInfo) { + const defaultStockData = { + inStock: 0, + remainingQuantity: 0, + remissionQuantity: item.remissionQuantity || 0, + }; + + return { ...item, ...defaultStockData }; + } + + const availableStock = + stockInfo.inStock - stockInfo.removedFromStock; + const inStock = availableStock < 0 ? 0 : availableStock; + + let { remainingQuantity, remissionQuantity } = item; + + if (!remissionQuantity || recalculate) { + remissionQuantity = inStock - (remainingQuantity || 0); + if (remissionQuantity < 0) { + remissionQuantity = 0; + } + } + if (!remainingQuantity || recalculate) { + remainingQuantity = inStock - (remissionQuantity || 0); + if (remainingQuantity < 0) { + remainingQuantity = 0; + } + } + + return { + ...item, + remainingQuantity, + remissionQuantity, + inStock, + }; + }); + + return o; + }), + ), + ), + ); + } + + getRequiredCapacities(params: { + departments?: string[]; + supplierId: number; + }) { + return this.getCurrentStock().pipe( + switchMap((stock) => + this._remiService + .RemiGetRequiredCapacities({ + stockId: stock?.id, + payload: { + departments: params?.departments || [], + supplierId: params?.supplierId, + }, + }) + .pipe(map((res) => res.result)), + ), + ); + } + + searchItemToRemit(ean: string): Observable { + return this.getCurrentStock().pipe( + switchMap((stock) => + this._search + .SearchSearch({ + stockId: stock.id, + input: { qs: ean }, + doNotTrack: true, + }) + .pipe( + catchError((err) => of({ hits: 0, result: [] })), + map((res) => [res, stock] as [ListResponseArgsOfItemDTO, StockDTO]), + ), + ), + map(([res, stock]) => { + if (res.hits === 0) { + return undefined; + } + + const item = res.result[0] as ItemDTO; + + return fromItemDto(item, stock); + }), + ); + } + + canAddReturnItem( + item: ReturnItemDTO, + ): Observable { + return this._remiService.RemiCanAddReturnItem({ + data: [item], + }); + } + + async createReturn( + supplierId: number, + returnGroup?: string, + ): Promise { + const response = await this._returnService + .ReturnCreateReturn({ + data: { + supplier: { id: supplierId }, + returnGroup: returnGroup ?? String(Date.now()), + }, + }) + .toPromise(); + + return response.result; + } + + completeReturn(returnId: number) { + return this._returnService.ReturnFinalizeReturn({ returnId }).toPromise(); + } + + async completeRemission(returnId: number): Promise { + const returnDto = await this.getReturn(returnId).toPromise(); + const response = await this._returnService + .ReturnFinalizeReturnGroup({ + returnGroup: returnDto.returnGroup, + }) + .toPromise(); + + return response.result; + } + + async deleteRemission(returnId: number): Promise { + await this._returnService + .ReturnCancelReturn({ + returnId, + }) + .toPromise(); + } + + getReturns(params: { + start?: Date; + returncompleted: boolean; + }): Observable { + const queryToken: ReturnQueryTokenDTO = { + start: params.start?.toISOString(), + filter: { + returncompleted: params.returncompleted ? 'true' : 'false', + }, + eagerLoading: 3, + input: {}, + }; + + Object.keys(queryToken).forEach((key) => { + if (!queryToken[key]) { + delete queryToken[key]; + } + }); + + return this.getCurrentStock().pipe( + switchMap((stock) => + this._returnService.ReturnQueryReturns({ + stockId: stock.id, + queryToken, + }), + ), + map((res) => res.result), + ); + } + + getReturn(returnId: number): Observable { + return this._returnService + .ReturnGetReturn({ returnId, eagerLoading: 3 }) + .pipe(map((res) => res.result)); + } + + async deleteReturn(returnId: number) { + const returnDto = await this.getReturn(returnId).toPromise(); + for (const receipt of returnDto?.receipts ?? []) { + await this.deleteReceipt(returnDto!.id, receipt.id); + } + await this.deleteRemission(returnDto!.id); + } + + addReturnItem({ + returnId, + receiptId, + returnItemId, + quantity, + placementType, + inStock, + }: { + returnId: number; + receiptId: number; + returnItemId: number; + quantity?: number; + placementType?: RemissionPlacementType; + inStock: number; + }) { + return this._returnService + .ReturnAddReturnItem({ + returnId, + receiptId, + data: { returnItemId, quantity, placementType, inStock }, + }) + .pipe(map((r) => r.result)); + } + + addReturnSuggestion({ + returnId, + receiptId, + returnSuggestionId, + quantity, + placementType, + inStock, + impedimentComment, + remainingQuantity, + }: { + returnId: number; + receiptId: number; + returnSuggestionId: number; + quantity?: number; + placementType?: RemissionPlacementType; + inStock: number; + impedimentComment: string; + remainingQuantity: number; + }) { + return this._returnService + .ReturnAddReturnSuggestion({ + returnId, + receiptId, + data: { + returnSuggestionId, + quantity, + placementType, + inStock, + impedimentComment, + remainingQuantity, + }, + }) + .pipe(map((r) => r.result)); + } + + removeReturnItemFromList({ itemId }: { itemId: number }) { + return this._returnService.ReturnDeleteReturnItem({ itemId }); + } + + removeReturnItemFromReceipt({ + returnId, + receiptId, + receiptItemId, + }: { + returnId: number; + receiptId: number; + receiptItemId: number; + }) { + return this._returnService.ReturnRemoveReturnItem({ + returnId, + receiptItemId, + receiptId, + }); + } + + returnImpediment(itemId: number) { + return this._returnService + .ReturnReturnItemImpediment({ + itemId, + data: { comment: 'Produkt nicht gefunden' }, + }) + .pipe(map((r) => r.result)); + } + + returnSuggestion(itemId: number) { + return this._returnService + .ReturnReturnSuggestionImpediment({ + itemId, + data: { comment: 'Produkt nicht gefunden' }, + }) + .pipe(map((r) => r.result)); + } + + /** + * Create a new receipt for the given return/remission + * @param returnId Return ID + * @param receiptNumber Receipt number + * @returns ReceiptDTO + */ + async createReceipt( + returnDTO: ReturnDTO, + receiptNumber?: string, + ): Promise { + const stock = await this._getStock(); + + const response = await this._returnService + .ReturnCreateReceipt({ + returnId: returnDTO.id, + data: { + receiptNumber: receiptNumber ?? null, + stock: { + id: stock.id, + }, + supplier: { id: returnDTO.supplier.id }, + receiptType: 1, // ShippingNote = 1 + }, + }) + .toPromise(); + + const receipt: ReceiptDTO = response.result; + + return receipt; + } + + /** + * Create a new Package and assign it to a receipt + * @param returnId Return ID + * @param receiptId Receipt ID + * @param packageNumber Packagenumber + * @returns ReceiptDTO + */ + async createReceiptAndAssignPackage({ + returnId, + receiptId, + packageNumber, + }: { + returnId: number; + receiptId: number; + packageNumber: string; + }): Promise { + const response = await this._returnService + .ReturnCreateAndAssignPackage({ + returnId, + receiptId, + data: { + packageNumber, + }, + }) + .toPromise(); + const receipt: ReceiptDTO = response.result; + return receipt; + } + + async completeReceipt( + returnId: number, + receiptId: number, + ): Promise { + const res = await this._returnService + .ReturnFinalizeReceipt({ + returnId, + receiptId, + data: {}, + }) + .toPromise(); + + const result = res.result; + + return result; + } + + async deleteReceipt(returnId: number, receiptId: number): Promise { + await this._returnService + .ReturnCancelReturnReceipt({ + returnId, + receiptId, + }) + .toPromise(); + } + + addProductToRemit(item: ReturnItemDTO, reason: string, quantity: number) { + return this._remiService.RemiCreateReturnItem({ + data: [ + { + assortment: item.assortment, + product: item.product, + returnReason: reason, + predefinedReturnQuantity: quantity, + stock: item.stock, + retailPrice: item.retailPrice, + source: 'manually-added', + }, + ], + }); + } + + private _getStock() { + return this.getCurrentStock().toPromise(); + } +} diff --git a/apps/isa-app/src/main.ts b/apps/isa-app/src/main.ts index ff648a905..25aa0326e 100644 --- a/apps/isa-app/src/main.ts +++ b/apps/isa-app/src/main.ts @@ -1,32 +1,43 @@ -import { enableProdMode, isDevMode } from "@angular/core"; -import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; -import { CONFIG_DATA } from "@isa/core/config"; -import { setDefaultOptions } from "date-fns"; -import { de } from "date-fns/locale"; -import * as moment from "moment"; -import "moment/locale/de"; - -setDefaultOptions({ locale: de }); -moment.locale("de"); - -import { AppModule } from "./app/app.module"; - -if (!isDevMode()) { - enableProdMode(); -} - -async function bootstrap() { - const configRes = await fetch("/config/config.json"); - - const config = await configRes.json(); - - platformBrowserDynamic([ - { provide: CONFIG_DATA, useValue: config }, - ]).bootstrapModule(AppModule); -} - -try { - bootstrap(); -} catch (error) { - console.error(error); -} +import { enableProdMode, isDevMode } from '@angular/core'; +import { CONFIG_DATA } from '@isa/core/config'; +import { setDefaultOptions } from 'date-fns'; +import { de } from 'date-fns/locale'; +import localeDe from '@angular/common/locales/de'; +import localeDeExtra from '@angular/common/locales/extra/de'; +import * as moment from 'moment'; +import 'moment/locale/de'; + +setDefaultOptions({ locale: de }); +moment.locale('de'); + +registerLocaleData(localeDe, localeDeExtra); +registerLocaleData(localeDe, 'de', localeDeExtra); + +import { App } from './app/app'; +import { appConfig } from './app/app.config'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { registerLocaleData } from '@angular/common'; + +if (!isDevMode()) { + enableProdMode(); +} + +async function bootstrap() { + const configRes = await fetch('/config/config.json'); + + const config = await configRes.json(); + + await bootstrapApplication(App, { + ...appConfig, + providers: [ + { provide: CONFIG_DATA, useValue: config }, + ...appConfig.providers, + ], + }); +} + +try { + bootstrap(); +} catch (error) { + console.error(error); +} diff --git a/apps/isa-app/src/modal/availabilities/availabilities.component.ts b/apps/isa-app/src/modal/availabilities/availabilities.component.ts index dddd7f656..844e2ba26 100644 --- a/apps/isa-app/src/modal/availabilities/availabilities.component.ts +++ b/apps/isa-app/src/modal/availabilities/availabilities.component.ts @@ -26,7 +26,7 @@ export class ModalAvailabilitiesComponent { item = this.modalRef.data.item; itemId = this.modalRef.data.itemId || this.modalRef.data.item.id; userbranch$ = combineLatest([ - this.applicationService.getSelectedBranch$(this.applicationService.activatedProcessId), + this.applicationService.getSelectedBranch$(), this.domainAvailabilityService.getDefaultBranch(), ]).pipe(map(([selectedBranch, defaultBranch]) => selectedBranch || defaultBranch)); diff --git a/apps/isa-app/src/page/catalog/article-details/article-details.component.ts b/apps/isa-app/src/page/catalog/article-details/article-details.component.ts index 509ade6fb..97475e4b6 100644 --- a/apps/isa-app/src/page/catalog/article-details/article-details.component.ts +++ b/apps/isa-app/src/page/catalog/article-details/article-details.component.ts @@ -192,11 +192,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy { }), ); - selectedBranchId$ = this.applicationService.activatedProcessId$.pipe( - switchMap((processId) => - this.applicationService.getSelectedBranch$(processId), - ), - ); + selectedBranchId$ = this.applicationService.getSelectedBranch$(); get isTablet$() { return this._environment.matchTablet$; @@ -328,7 +324,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy { debounceTime(0), switchMap((params) => this.applicationService - .getSelectedBranch$(Number(params.processId)) + .getSelectedBranch$() .pipe(map((selectedBranch) => ({ params, selectedBranch }))), ), ) diff --git a/apps/isa-app/src/page/catalog/article-search/search-main/search-main.component.ts b/apps/isa-app/src/page/catalog/article-search/search-main/search-main.component.ts index 0f9633558..5b0220db1 100644 --- a/apps/isa-app/src/page/catalog/article-search/search-main/search-main.component.ts +++ b/apps/isa-app/src/page/catalog/article-search/search-main/search-main.component.ts @@ -98,11 +98,9 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy { ); this.subscriptions.add( - this.application.activatedProcessId$ - .pipe( - debounceTime(0), - switchMap((processId) => this.application.getSelectedBranch$(processId)), - ) + this.application + .getSelectedBranch$() + .pipe(debounceTime(0)) .subscribe((selectedBranch) => { const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id; if (branchChanged) { @@ -143,7 +141,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy { const clean = { ...params }; for (const key in clean) { - if (key === 'main_qs' || key?.includes('order_by')) { + if (key === 'main_qs') { clean[key] = undefined; } else if (key?.includes('order_by')) { delete clean[key]; diff --git a/apps/isa-app/src/page/catalog/article-search/search-results/search-result-item.component.ts b/apps/isa-app/src/page/catalog/article-search/search-results/search-result-item.component.ts index b7fcb1130..184d981dd 100644 --- a/apps/isa-app/src/page/catalog/article-search/search-results/search-result-item.component.ts +++ b/apps/isa-app/src/page/catalog/article-search/search-results/search-result-item.component.ts @@ -40,7 +40,7 @@ export class SearchResultItemComponent extends ComponentStore s.item); - @Input() selected: boolean = false; + @Input() selected = false; @Input() get selectable() { @@ -91,9 +91,7 @@ export class SearchResultItemComponent extends ComponentStore this.applicationService.getSelectedBranch$(processId)), - ); + selectedBranchId$ = this.applicationService.getSelectedBranch$(); isOrderBranch$ = combineLatest([this.defaultBranch$, this.selectedBranchId$]).pipe( map(([defaultBranch, selectedBranch]) => { diff --git a/apps/isa-app/src/page/catalog/article-search/search-results/search-results.component.ts b/apps/isa-app/src/page/catalog/article-search/search-results/search-results.component.ts index 3554dcf3b..d8a5485a8 100644 --- a/apps/isa-app/src/page/catalog/article-search/search-results/search-results.component.ts +++ b/apps/isa-app/src/page/catalog/article-search/search-results/search-results.component.ts @@ -157,7 +157,7 @@ export class ArticleSearchResultsComponent .pipe( debounceTime(0), switchMap(([processId, queryParams]) => - this.application.getSelectedBranch$(processId).pipe( + this.application.getSelectedBranch$().pipe( map((selectedBranch) => ({ processId, queryParams, diff --git a/apps/isa-app/src/page/catalog/article-search/search-results/search-results.module.ts b/apps/isa-app/src/page/catalog/article-search/search-results/search-results.module.ts index c5671fde1..b56c3bdc3 100644 --- a/apps/isa-app/src/page/catalog/article-search/search-results/search-results.module.ts +++ b/apps/isa-app/src/page/catalog/article-search/search-results/search-results.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; -import { DomainCatalogModule } from '@domain/catalog'; +import { ThumbnailUrlPipe } from '@domain/catalog'; import { UiCommonModule } from '@ui/common'; import { UiIconModule } from '@ui/icon'; import { UiSelectBulletModule } from '@ui/select-bullet'; @@ -26,7 +26,7 @@ import { MatomoModule } from 'ngx-matomo-client'; CommonModule, FormsModule, RouterModule, - DomainCatalogModule, + ThumbnailUrlPipe, UiCommonModule, UiIconModule, UiSelectBulletModule, diff --git a/apps/isa-app/src/page/catalog/page-catalog.component.ts b/apps/isa-app/src/page/catalog/page-catalog.component.ts index 6d57f6d97..7382c4e36 100644 --- a/apps/isa-app/src/page/catalog/page-catalog.component.ts +++ b/apps/isa-app/src/page/catalog/page-catalog.component.ts @@ -77,9 +77,7 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy { ngOnInit() { this.activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId))); - this.selectedBranch$ = this.activatedProcessId$.pipe( - switchMap((processId) => this.application.getSelectedBranch$(Number(processId))), - ); + this.selectedBranch$ = this.application.getSelectedBranch$(); this.stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranch$]).pipe( map(([defaultBranch, selectedBranch]) => { diff --git a/apps/isa-app/src/page/catalog/page-catalog.module.ts b/apps/isa-app/src/page/catalog/page-catalog.module.ts index 7458ee733..3ad5bb8b8 100644 --- a/apps/isa-app/src/page/catalog/page-catalog.module.ts +++ b/apps/isa-app/src/page/catalog/page-catalog.module.ts @@ -1,28 +1,28 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { BranchSelectorComponent } from '@shared/components/branch-selector'; -import { BreadcrumbModule } from '@shared/components/breadcrumb'; -import { ArticleDetailsModule } from './article-details/article-details.module'; -import { ArticleSearchModule } from './article-search/article-search.module'; -import { PageCatalogRoutingModule } from './page-catalog-routing.module'; -import { PageCatalogComponent } from './page-catalog.component'; -import { SharedSplitscreenComponent } from '@shared/components/splitscreen'; -import { UiCommonModule } from '@ui/common'; -import { UiTooltipModule } from '@ui/tooltip'; - -@NgModule({ - imports: [ - CommonModule, - PageCatalogRoutingModule, - ArticleSearchModule, - ArticleDetailsModule, - BreadcrumbModule, - BranchSelectorComponent, - SharedSplitscreenComponent, - UiCommonModule, - UiTooltipModule, - ], - exports: [], - declarations: [PageCatalogComponent], -}) -export class PageCatalogModule {} +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { BranchSelectorComponent } from '@shared/components/branch-selector'; +import { BreadcrumbModule } from '@shared/components/breadcrumb'; +import { ArticleDetailsModule } from './article-details/article-details.module'; +import { ArticleSearchModule } from './article-search/article-search.module'; +import { PageCatalogRoutingModule } from './page-catalog-routing.module'; +import { PageCatalogComponent } from './page-catalog.component'; +import { SharedSplitscreenComponent } from '@shared/components/splitscreen'; +import { UiCommonModule } from '@ui/common'; +import { UiTooltipModule } from '@ui/tooltip'; + +@NgModule({ + imports: [ + CommonModule, + PageCatalogRoutingModule, + ArticleDetailsModule, + ArticleSearchModule, + BreadcrumbModule, + BranchSelectorComponent, + SharedSplitscreenComponent, + UiCommonModule, + UiTooltipModule, + ], + exports: [], + declarations: [PageCatalogComponent], +}) +export class PageCatalogModule {} diff --git a/apps/isa-app/src/page/customer-order/customer-order-search/search-main/customer-order-search-main.component.ts b/apps/isa-app/src/page/customer-order/customer-order-search/search-main/customer-order-search-main.component.ts index 05d74c648..a7c0bd550 100644 --- a/apps/isa-app/src/page/customer-order/customer-order-search/search-main/customer-order-search-main.component.ts +++ b/apps/isa-app/src/page/customer-order/customer-order-search/search-main/customer-order-search-main.component.ts @@ -73,11 +73,9 @@ export class CustomerOrderSearchMainComponent implements OnInit, OnDestroy { ); this._subscriptions.add( - this._application.activatedProcessId$ - .pipe( - debounceTime(0), - switchMap((processId) => this._application.getSelectedBranch$(processId)), - ) + this._application + .getSelectedBranch$() + .pipe(debounceTime(0)) .subscribe((selectedBranch) => { const branchChanged = selectedBranch?.id !== this._customerOrderSearchStore?.selectedBranch?.id; if (branchChanged) { diff --git a/apps/isa-app/src/page/customer-order/customer-order-search/search-results/customer-order-search-results.component.ts b/apps/isa-app/src/page/customer-order/customer-order-search/search-results/customer-order-search-results.component.ts index e654a50df..c7b0bcb3f 100644 --- a/apps/isa-app/src/page/customer-order/customer-order-search/search-results/customer-order-search-results.component.ts +++ b/apps/isa-app/src/page/customer-order/customer-order-search/search-results/customer-order-search-results.component.ts @@ -183,7 +183,7 @@ export class CustomerOrderSearchResultsComponent debounceTime(150), switchMap(([processId, params]) => this._application - .getSelectedBranch$(processId) + .getSelectedBranch$() .pipe(map((selectedBranch) => ({ processId, params, selectedBranch }))), ), ) diff --git a/apps/isa-app/src/page/customer-order/customer-order.component.ts b/apps/isa-app/src/page/customer-order/customer-order.component.ts index 9e0091bbd..bef9a500e 100644 --- a/apps/isa-app/src/page/customer-order/customer-order.component.ts +++ b/apps/isa-app/src/page/customer-order/customer-order.component.ts @@ -49,9 +49,7 @@ export class CustomerOrderComponent implements OnInit, AfterViewInit, OnDestroy ) {} ngOnInit(): void { - this.selectedBranch$ = this.application.activatedProcessId$.pipe( - switchMap((processId) => this.application.getSelectedBranch$(Number(processId))), - ); + this.selectedBranch$ = this.application.getSelectedBranch$(); /* Ticket #4544 - Suchrequest abbrechen bei Prozesswechsel / um zu verhindern, dass die Suche in einen anderen Kundenbestellungen Prozess übernommen wird diff --git a/apps/isa-app/src/shared/components/icon/icon-registry.ts b/apps/isa-app/src/shared/components/icon/icon-registry.ts index 71abe4552..bfcdc0f6d 100644 --- a/apps/isa-app/src/shared/components/icon/icon-registry.ts +++ b/apps/isa-app/src/shared/components/icon/icon-registry.ts @@ -1,110 +1,113 @@ -import { Injectable } from '@angular/core'; -import { Icon, IconAlias, IconConfig } from './interfaces'; -import { IconLoader } from './loader'; -import { Observable, Subject, isObservable } from 'rxjs'; - -@Injectable() -export class IconRegistry { - private _icons = new Map(); - private _aliases = new Map(); - private _fallback: string; - private _viewBox: string; - - updated = new Subject(); - - private _initComplete = false; - - constructor(private _iconLoader: IconLoader) { - this._loadIcons(); - } - - private async _loadIcons(): Promise { - const load = this._iconLoader.getIcons(); - - if (load instanceof Promise) { - const config = await load; - this._init(config); - } else if (isObservable(load)) { - load.subscribe((config) => { - this._init(config); - }); - } else { - this._init(load); - } - } - - private _init(config: IconConfig): void { - this.register(...config.icons); - this.alias(...config.aliases); - this.setViewBox(config.viewBox); - this.setFallback(config.fallback); - - this._initComplete = true; - - this.updated.next(); - } - - register(...icons: Icon[]): IconRegistry { - icons?.forEach((icon) => { - this._icons.set(icon.name, icon); - }); - - return this; - } - - setViewBox(viewBox: string): void { - this._viewBox = viewBox; - } - - alias(...aliases: IconAlias[]): IconRegistry { - aliases?.forEach((alias) => { - this._aliases.set(alias.alias, alias.name); - }); - - return this; - } - - setFallback(name: string): void { - this._fallback = name; - } - - get(name: string): Icon | undefined { - const alias = this._aliases.get(name); - let iconName = name; - if (alias) { - iconName = alias; - } - - let icon = this._icons.get(iconName); - - if (!icon && this._initComplete) { - 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); - } - - return { ...icon, viewBox: icon?.viewBox || this._viewBox }; - } - - get$(name: string): Observable { - return new Observable((subscriber) => { - let icon = this.get(name); - subscriber.next(icon); - subscriber.complete(); - - const sub = this.updated.subscribe(() => { - icon = this.get(name); - subscriber.next(icon); - subscriber.complete(); - }); - - return () => sub.unsubscribe(); - }); - } -} +import { Injectable } from '@angular/core'; +import { Icon, IconAlias, IconConfig } from './interfaces'; +import { IconLoader } from './loader'; +import { Observable, Subject, isObservable } from 'rxjs'; + +/** + * @deprecated Use UiIconModule from '@isa/ui/icon' instead. + */ +@Injectable({ providedIn: 'root' }) +export class IconRegistry { + private _icons = new Map(); + private _aliases = new Map(); + private _fallback: string; + private _viewBox: string; + + updated = new Subject(); + + private _initComplete = false; + + constructor(private _iconLoader: IconLoader) { + this._loadIcons(); + } + + private async _loadIcons(): Promise { + const load = this._iconLoader.getIcons(); + + if (load instanceof Promise) { + const config = await load; + this._init(config); + } else if (isObservable(load)) { + load.subscribe((config) => { + this._init(config); + }); + } else { + this._init(load); + } + } + + private _init(config: IconConfig): void { + this.register(...config.icons); + this.alias(...config.aliases); + this.setViewBox(config.viewBox); + this.setFallback(config.fallback); + + this._initComplete = true; + + this.updated.next(); + } + + register(...icons: Icon[]): IconRegistry { + icons?.forEach((icon) => { + this._icons.set(icon.name, icon); + }); + + return this; + } + + setViewBox(viewBox: string): void { + this._viewBox = viewBox; + } + + alias(...aliases: IconAlias[]): IconRegistry { + aliases?.forEach((alias) => { + this._aliases.set(alias.alias, alias.name); + }); + + return this; + } + + setFallback(name: string): void { + this._fallback = name; + } + + get(name: string): Icon | undefined { + const alias = this._aliases.get(name); + let iconName = name; + if (alias) { + iconName = alias; + } + + let icon = this._icons.get(iconName); + + if (!icon && this._initComplete) { + 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); + } + + return { ...icon, viewBox: icon?.viewBox || this._viewBox }; + } + + get$(name: string): Observable { + return new Observable((subscriber) => { + let icon = this.get(name); + subscriber.next(icon); + subscriber.complete(); + + const sub = this.updated.subscribe(() => { + icon = this.get(name); + subscriber.next(icon); + subscriber.complete(); + }); + + return () => sub.unsubscribe(); + }); + } +} diff --git a/apps/isa-app/src/shared/components/icon/icon.component.ts b/apps/isa-app/src/shared/components/icon/icon.component.ts index f1926e5d5..d243c07b5 100644 --- a/apps/isa-app/src/shared/components/icon/icon.component.ts +++ b/apps/isa-app/src/shared/components/icon/icon.component.ts @@ -1,66 +1,75 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - Input, - OnChanges, - OnDestroy, - OnInit, - SimpleChanges, -} from '@angular/core'; -import { IconRegistry } from './icon-registry'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; - -@Component({ - selector: 'shared-icon', - template: ` - - - - `, - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, -}) -export class IconComponent implements OnInit, OnDestroy, OnChanges { - @Input() - icon: string; - - data: string; - - viewBox: string; - - @Input() - size: number = 24; - - private _onDestroy$ = new Subject(); - - constructor( - private readonly _iconRegistry: IconRegistry, - private readonly _cdr: ChangeDetectorRef, - ) {} - - ngOnInit(): void { - this._iconRegistry.updated.pipe(takeUntil(this._onDestroy$)).subscribe(() => { - this.updateIcon(); - }); - } - - ngOnDestroy(): void { - this._onDestroy$.next(); - this._onDestroy$.complete(); - } - - ngOnChanges(changes: SimpleChanges): void { - 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(); - } -} +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, +} from '@angular/core'; +import { IconRegistry } from './icon-registry'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +/** + * @deprecated Use UiIconModule from '@isa/ui/icon' instead. + */ +@Component({ + selector: 'shared-icon', + template: ` + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, +}) +export class IconComponent implements OnInit, OnDestroy, OnChanges { + @Input() + icon: string; + + data: string; + + viewBox: string; + + @Input() + size = 24; + + private _onDestroy$ = new Subject(); + + constructor( + private readonly _iconRegistry: IconRegistry, + private readonly _cdr: ChangeDetectorRef, + ) {} + + ngOnInit(): void { + this._iconRegistry.updated + .pipe(takeUntil(this._onDestroy$)) + .subscribe(() => { + this.updateIcon(); + }); + } + + ngOnDestroy(): void { + this._onDestroy$.next(); + this._onDestroy$.complete(); + } + + ngOnChanges(changes: SimpleChanges): void { + 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(); + } +} diff --git a/apps/isa-app/src/shared/components/icon/icon.module.ts b/apps/isa-app/src/shared/components/icon/icon.module.ts index aaa249c4c..0b3e41bc7 100644 --- a/apps/isa-app/src/shared/components/icon/icon.module.ts +++ b/apps/isa-app/src/shared/components/icon/icon.module.ts @@ -1,31 +1,37 @@ -import { NgModule, Provider } from '@angular/core'; -import { IconComponent } from './icon.component'; -import { IconLoader, JsonIconLoader } from './loader'; -import { IconRegistry } from './icon-registry'; - -export function provideIcon(loaderProvider?: Provider) { - const providers: Provider[] = [IconRegistry]; - if (!loaderProvider) { - providers.push({ - provide: IconLoader, - useClass: JsonIconLoader, - }); - } else { - providers.push(loaderProvider); - } - - return providers; -} - -@NgModule({ - imports: [IconComponent], - exports: [IconComponent], -}) -export class IconModule { - static forRoot(loaderProvider?: Provider) { - return { - ngModule: IconModule, - providers: provideIcon(loaderProvider), - }; - } -} +import { NgModule, Provider } from '@angular/core'; +import { IconComponent } from './icon.component'; +import { IconLoader, JsonIconLoader } from './loader'; +import { IconRegistry } from './icon-registry'; + +/** + * @deprecated Use UiIconModule from '@isa/ui/icon' instead. + */ +export function provideIcon(loaderProvider?: Provider) { + const providers: Provider[] = [IconRegistry]; + if (!loaderProvider) { + providers.push({ + provide: IconLoader, + useClass: JsonIconLoader, + }); + } else { + providers.push(loaderProvider); + } + + return providers; +} + +/** + * @deprecated Use UiIconModule from '@isa/ui/icon' instead. + */ +@NgModule({ + imports: [IconComponent], + exports: [IconComponent], +}) +export class IconModule { + static forRoot(loaderProvider?: Provider) { + return { + ngModule: IconModule, + providers: provideIcon(loaderProvider), + }; + } +} diff --git a/apps/isa-app/src/ui/icon/icon-badge/icon-badge.component.ts b/apps/isa-app/src/ui/icon/icon-badge/icon-badge.component.ts index e29a89240..febb91e93 100644 --- a/apps/isa-app/src/ui/icon/icon-badge/icon-badge.component.ts +++ b/apps/isa-app/src/ui/icon/icon-badge/icon-badge.component.ts @@ -1,19 +1,22 @@ -import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; - -@Component({ - selector: 'ui-icon-badge', - templateUrl: 'icon-badge.component.html', - styleUrls: ['icon-badge.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false, -}) -export class UiIconBadgeComponent { - @Input() - icon: string; - - @Input() - alt: string; - - @Input() - area: 'customer' | 'branch' = 'customer'; -} +import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; + +/** + * @deprecated Use UiIconModule from '@isa/ui/icon' instead. + */ +@Component({ + selector: 'ui-icon-badge', + templateUrl: 'icon-badge.component.html', + styleUrls: ['icon-badge.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false, +}) +export class UiIconBadgeComponent { + @Input() + icon: string; + + @Input() + alt: string; + + @Input() + area: 'customer' | 'branch' = 'customer'; +} diff --git a/apps/isa-app/src/ui/icon/icon-registry.ts b/apps/isa-app/src/ui/icon/icon-registry.ts index f9786c22e..bd7c0567a 100644 --- a/apps/isa-app/src/ui/icon/icon-registry.ts +++ b/apps/isa-app/src/ui/icon/icon-registry.ts @@ -1,59 +1,62 @@ -import { Injectable } from '@angular/core'; -import { SvgIcon } from './defs'; -import { IconAlias } from './defs/icon-alias'; - -@Injectable() -export class IconRegistry { - private _icons = new Map(); - private _aliases = new Map(); - private _fallback: string; - private _viewBox: string; - - register(...icons: SvgIcon[]): IconRegistry { - icons?.forEach((icon) => { - this._icons.set(icon.name, icon); - }); - - return this; - } - - setViewBox(viewBox: string): void { - this._viewBox = viewBox; - } - - alias(...aliases: IconAlias[]): IconRegistry { - aliases?.forEach((alias) => { - this._aliases.set(alias.alias, alias.name); - }); - - return this; - } - - setFallback(name: string): void { - this._fallback = name; - } - - get(name: string): SvgIcon | undefined { - const alias = this._aliases.get(name); - let iconName = name; - if (alias) { - iconName = alias; - } - - let icon = this._icons.get(iconName); - - if (!icon) { - 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); - } - - return { ...icon, viewBox: icon?.viewBox || this._viewBox }; - } -} +import { Injectable } from '@angular/core'; +import { SvgIcon } from './defs'; +import { IconAlias } from './defs/icon-alias'; + +/** + * @deprecated Use UiIconModule from '@isa/ui/icon' instead. + */ +@Injectable({ providedIn: 'root' }) +export class IconRegistry { + private _icons = new Map(); + private _aliases = new Map(); + private _fallback: string; + private _viewBox: string; + + register(...icons: SvgIcon[]): IconRegistry { + icons?.forEach((icon) => { + this._icons.set(icon.name, icon); + }); + + return this; + } + + setViewBox(viewBox: string): void { + this._viewBox = viewBox; + } + + alias(...aliases: IconAlias[]): IconRegistry { + aliases?.forEach((alias) => { + this._aliases.set(alias.alias, alias.name); + }); + + return this; + } + + setFallback(name: string): void { + this._fallback = name; + } + + get(name: string): SvgIcon | undefined { + const alias = this._aliases.get(name); + let iconName = name; + if (alias) { + iconName = alias; + } + + let icon = this._icons.get(iconName); + + if (!icon) { + 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); + } + + return { ...icon, viewBox: icon?.viewBox || this._viewBox }; + } +} diff --git a/apps/isa-app/src/ui/icon/icon.component.ts b/apps/isa-app/src/ui/icon/icon.component.ts index a1998147d..ce0d313f0 100644 --- a/apps/isa-app/src/ui/icon/icon.component.ts +++ b/apps/isa-app/src/ui/icon/icon.component.ts @@ -1,29 +1,39 @@ -import { Component, ChangeDetectionStrategy, Input, Optional, Inject, HostBinding } from '@angular/core'; -import { UI_ICON_HREF, UI_ICON_VIEW_BOX } from './tokens'; - -@Component({ - selector: 'ui-icon', - templateUrl: 'icon.component.html', - styleUrls: ['icon.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false, -}) -export class UiIconComponent { - @Input() - @HostBinding('attr.icon') - icon: string; - - @Input() - 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'; - } -} +import { + Component, + ChangeDetectionStrategy, + Input, + Optional, + Inject, + HostBinding, +} from '@angular/core'; +import { UI_ICON_HREF, UI_ICON_VIEW_BOX } from './tokens'; + +/** + * @deprecated Use UiIconModule from '@isa/ui/icon' instead. + */ +@Component({ + selector: 'ui-icon', + templateUrl: 'icon.component.html', + styleUrls: ['icon.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false, +}) +export class UiIconComponent { + @Input() + @HostBinding('attr.icon') + icon: string; + + @Input() + 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'; + } +} diff --git a/apps/isa-app/src/ui/icon/svg-icon.component.ts b/apps/isa-app/src/ui/icon/svg-icon.component.ts index fc5e113ba..ced5f4a39 100644 --- a/apps/isa-app/src/ui/icon/svg-icon.component.ts +++ b/apps/isa-app/src/ui/icon/svg-icon.component.ts @@ -1,38 +1,52 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; -import { IconRegistry } from './icon-registry'; - -@Component({ - selector: 'ui-svg-icon', - template: ` - - - - `, - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false, -}) -export class UISvgIconComponent implements OnChanges { - @Input() - icon: string; - - data: string; - - viewBox: string; - - @Input() - size: number = 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(); - } - } -} +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnChanges, + SimpleChanges, +} from '@angular/core'; +import { IconRegistry } from './icon-registry'; + +/** + * @deprecated Use UiIconModule from '@isa/ui/icon' instead. + */ +@Component({ + selector: 'ui-svg-icon', + template: ` + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false, +}) +export class UISvgIconComponent implements OnChanges { + @Input() + icon: string; + + data: string; + + viewBox: string; + + @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(); + } + } +} diff --git a/apps/isa-app/src/ui/icon/ui-icon.module.ts b/apps/isa-app/src/ui/icon/ui-icon.module.ts index 40f20c81b..94f738130 100644 --- a/apps/isa-app/src/ui/icon/ui-icon.module.ts +++ b/apps/isa-app/src/ui/icon/ui-icon.module.ts @@ -1,57 +1,63 @@ -import { ModuleWithProviders, NgModule, Provider } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { UiIconComponent } from './icon.component'; -import { UiIconBadgeComponent } from './icon-badge/icon-badge.component'; -import { UISvgIconComponent } from './svg-icon.component'; -import { IconRegistry } from './icon-registry'; -import { UI_ICON_CFG } from './tokens'; -import { UiIconConfig } from './icon-config'; - -export function _rootIconRegistryFactory(config: UiIconConfig): IconRegistry { - const registry = new IconRegistry(); - - if (config?.fallback) { - registry.setFallback(config.fallback); - } - if (config?.aliases) { - registry.alias(...config.aliases); - } - if (config?.icons) { - registry.register(...config.icons); - } - - if (config?.viewBox) { - registry.setViewBox(config.viewBox); - } - - return registry; -} - -@NgModule({ - imports: [CommonModule], - declarations: [UiIconComponent, UiIconBadgeComponent, UISvgIconComponent], - exports: [UiIconComponent, UiIconBadgeComponent, UISvgIconComponent], -}) -export class UiIconModule { - static forRoot(config?: UiIconConfig): ModuleWithProviders { - const providers: Provider[] = [ - { - provide: IconRegistry, - useFactory: _rootIconRegistryFactory, - deps: [UI_ICON_CFG], - }, - ]; - - if (config) { - providers.push({ - provide: UI_ICON_CFG, - useValue: config, - }); - } - - return { - ngModule: UiIconModule, - providers, - }; - } -} +import { ModuleWithProviders, NgModule, Provider } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { UiIconComponent } from './icon.component'; +import { UiIconBadgeComponent } from './icon-badge/icon-badge.component'; +import { UISvgIconComponent } from './svg-icon.component'; +import { IconRegistry } from './icon-registry'; +import { UI_ICON_CFG } from './tokens'; +import { UiIconConfig } from './icon-config'; + +/** + * @deprecated Use UiIconModule from '@isa/ui/icon' instead. + */ +export function _rootIconRegistryFactory(config: UiIconConfig): IconRegistry { + const registry = new IconRegistry(); + + if (config?.fallback) { + registry.setFallback(config.fallback); + } + if (config?.aliases) { + registry.alias(...config.aliases); + } + if (config?.icons) { + registry.register(...config.icons); + } + + if (config?.viewBox) { + registry.setViewBox(config.viewBox); + } + + return registry; +} + +/** + * @deprecated Use UiIconModule from '@isa/ui/icon' instead. + */ +@NgModule({ + imports: [CommonModule], + declarations: [UiIconComponent, UiIconBadgeComponent, UISvgIconComponent], + exports: [UiIconComponent, UiIconBadgeComponent, UISvgIconComponent], +}) +export class UiIconModule { + static forRoot(config?: UiIconConfig): ModuleWithProviders { + const providers: Provider[] = [ + { + provide: IconRegistry, + useFactory: _rootIconRegistryFactory, + deps: [UI_ICON_CFG], + }, + ]; + + if (config) { + providers.push({ + provide: UI_ICON_CFG, + useValue: config, + }); + } + + return { + ngModule: UiIconModule, + providers, + }; + } +}