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

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

View File

@@ -1,19 +1,22 @@
import { NgModule } from '@angular/core';
import { 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 }],
};
}
}

View File

@@ -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 },
],
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,206 +1,205 @@
import {
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');
// }
// }
// }

View File

@@ -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<RootState>,
): ActionReducer<RootState> {
return function (state, action) {
if (action.type === 'HYDRATE') {
return reducer(action['payload'], action);
}
return reducer(state, action);
};
}
const metaReducers: MetaReducer<RootState>[] = [storeHydrateMetaReducer];
// --- Swagger Configuration ---
const swaggerConfigSchema = z.object({ rootUrl: z.string() });
function createSwaggerConfigFactory(name: string) {
return function () {
return inject(Config).get(`@swagger/${name}`, swaggerConfigSchema);
};
}
const serviceWorkerBypassInterceptor: HttpInterceptorFn = (req, next) => {
return next(req.clone({ setHeaders: { 'ngsw-bypass': 'true' } }));
};
// --- App Initializer ---
function appInitializerFactory(_config: Config, injector: Injector) {
return async () => {
// 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 {}
};

View File

View File

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

View File

@@ -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 {}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(['/']);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,41 +1,68 @@
import { Injectable } from '@angular/core';
import { 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<number>(undefined);
#store = inject(Store);
#tabService = inject(TabService);
#activatedProcessId$ = toObservable(this.#tabService.activatedTabId);
#tabs$ = toObservable(this.#tabService.entities);
#processes$ = this.#tabs$.pipe(
map((tabs) => tabs.map((tab) => this.mapTabToProcess(tab))),
);
#section = new BehaviorSubject<'customer' | 'branch'>('customer');
readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
get activatedProcessId() {
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<ApplicationProcess[]> {
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<ApplicationProcess> {
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<number> {
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<ApplicationProcess>) {
this.store.dispatch(patchProcess({ processId, changes }));
}
patchProcess(processId: number, changes: Partial<ApplicationProcess>): void {
const tabChanges: {
name?: string;
tags?: string[];
metadata?: Record<string, unknown>;
} = {};
patchProcessData(processId: number, data: Record<string, any>) {
this.store.dispatch(patchProcessData({ processId, data }));
}
getSelectedBranch$(processId?: number): Observable<BranchDTO> {
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<string, unknown>): void {
const currentProcess = this.#tabService.entityMap()[processId];
const currentData: TabMetadata =
(currentProcess?.metadata?.['process_data'] as TabMetadata) ?? {};
this.#tabService.patchTab(processId, {
metadata: { [`process_data`]: { ...currentData, ...data } },
});
}
getSelectedBranch$(): Observable<BranchDTO> {
return this.#processes$.pipe(
withLatestFrom(this.#activatedProcessId$),
map(([processes, activatedProcessId]) =>
processes.find((process) => process.id === activatedProcessId),
),
filter((process): process is ApplicationProcess => !!process),
map((process) => process.data?.selectedBranch as BranchDTO),
);
}
readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
async createCustomerProcess(processId?: number): Promise<ApplicationProcess> {
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<void> {
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<number>(tab.metadata, 'process_created') ??
tab.createdAt,
activated:
this.getMetadataValue<number>(tab.metadata, 'process_activated') ??
tab.activatedAt ??
0,
section:
this.getMetadataValue<'customer' | 'branch'>(
tab.metadata,
'process_section',
) ?? 'customer',
type: this.getMetadataValue<string>(tab.metadata, 'process_type'),
closeable:
this.getMetadataValue<boolean>(tab.metadata, 'process_closeable') ??
true,
confirmClosing:
this.getMetadataValue<boolean>(
tab.metadata,
'process_confirmClosing',
) ?? true,
data: this.extractDataFromMetadata(tab.metadata),
};
}
/**
* Extracts ApplicationProcess data properties from tab metadata.
* Data properties are stored with a 'data_' prefix in tab metadata.
*
* @param metadata - The tab metadata object
* @returns The extracted data object or undefined if no data properties exist
*/
private extractDataFromMetadata(
metadata: TabMetadata,
): Record<string, unknown> | undefined {
// Return the complete data object stored under 'process_data'
const processData = metadata?.['process_data'];
if (
processData &&
typeof processData === 'object' &&
processData !== null
) {
return processData as Record<string, unknown>;
}
return undefined;
}
private getMetadataValue<T>(
metadata: TabMetadata,
key: string,
): T | undefined {
return metadata?.[key] as T | undefined;
}
private createTimestamp(): number {
return Date.now();
}
}

View File

@@ -1,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';

View File

@@ -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<ApplicationProcess> }>(),
);
export const patchProcessData = createAction(
`${prefix} Patch Process Data`,
props<{ processId: number; data: Record<string, any> }>(),
);
import { createAction, props } from '@ngrx/store';
const prefix = '[CORE-APPLICATION]';
export const removeProcess = createAction(
`${prefix} Remove Process`,
props<{ processId: number }>(),
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,15 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { 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<CoreBreadcrumbModule> {
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,
]);
}

View File

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

View File

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,105 +1,111 @@
import { Injectable } from '@angular/core';
import { 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 + '',
});
}
}

View File

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

View File

@@ -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);

View File

@@ -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(),
);
}
}

View File

@@ -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<DomainCheckoutModule> {
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,
]);
}

View File

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

View File

@@ -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({});
}
}

View File

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

View File

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

View File

@@ -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();
}
}

View File

@@ -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';

View File

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

View File

@@ -1,316 +1,381 @@
import { Injectable } from '@angular/core';
import {
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<OrderListItemDTO[]> {
return this.orderService
.OrderGetOrdersByBuyerNumber({ buyerNumber: customerNumber, take: 20, skip })
.pipe(map((orders) => orders.result));
}
getOrder(orderId: number): Observable<OrderDTO> {
return this.orderService.OrderGetOrder(orderId).pipe(map((o) => o.result));
}
getBranches() {
return this.branchService.BranchGetBranches({});
}
getHistory(orderItemSubsetId: number): Observable<HistoryDTO[]> {
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<OrderItemDTO> }) {
return this.orderService.OrderPatchOrderItem(payload).pipe(map((response) => response.result));
}
patchOrderItemSubset(payload: {
orderItemSubsetId: number;
orderItemId: number;
orderId: number;
orderItemSubset: Partial<OrderItemSubsetDTO>;
}) {
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<ValueTupleOfOrderItemSubsetDTOAndOrderItemSubsetDTO> {
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<string> {
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<Record<string, Date[]>> {
return this.orderService
.OrderGetOrderItemSubsetTasks({
orderId,
orderItemId,
orderItemSubsetId,
completed: new Date(0).toISOString(),
take,
skip,
})
.pipe(
map((res) =>
res.result
.sort((a, b) => new Date(b.completed).getTime() - new Date(a.completed).getTime())
.reduce(
(data, result) => {
(data[result.name] = data[result.name] || []).push(new Date(result.completed));
return data;
},
{} as Record<string, Date[]>,
),
),
);
}
}
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<OrderListItemDTO[]> {
return this.orderService
.OrderGetOrdersByBuyerNumber({
buyerNumber: customerNumber,
take: 20,
skip,
})
.pipe(map((orders) => orders.result));
}
getOrder(orderId: number): Observable<OrderDTO> {
return this.orderService.OrderGetOrder(orderId).pipe(map((o) => o.result));
}
getBranches() {
return this.branchService.BranchGetBranches({});
}
getHistory(orderItemSubsetId: number): Observable<HistoryDTO[]> {
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<OrderItemDTO>;
}) {
return this.orderService
.OrderPatchOrderItem(payload)
.pipe(map((response) => response.result));
}
patchOrderItemSubset(payload: {
orderItemSubsetId: number;
orderItemId: number;
orderId: number;
orderItemSubset: Partial<OrderItemSubsetDTO>;
}) {
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<ValueTupleOfOrderItemSubsetDTOAndOrderItemSubsetDTO> {
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<string> {
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<Record<string, Date[]>> {
return this.orderService
.OrderGetOrderItemSubsetTasks({
orderId,
orderItemId,
orderItemSubsetId,
completed: new Date(0).toISOString(),
take,
skip,
})
.pipe(
map((res) =>
res.result
.sort(
(a, b) =>
new Date(b.completed).getTime() -
new Date(a.completed).getTime(),
)
.reduce(
(data, result) => {
(data[result.name] = data[result.name] || []).push(
new Date(result.completed),
);
return data;
},
{} as Record<string, Date[]>,
),
),
);
}
}

View File

@@ -1,22 +1,25 @@
import { Injectable } from '@angular/core';
import { 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));
}
}

View File

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

View File

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

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,43 @@
import { enableProdMode, isDevMode } from "@angular/core";
import { 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);
}

View File

@@ -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));

View File

@@ -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 }))),
),
)

View File

@@ -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];

View File

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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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]) => {

View File

@@ -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 {}

View File

@@ -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) {

View File

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

View File

@@ -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

View File

@@ -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<string, Icon>();
private _aliases = new Map<string, string>();
private _fallback: string;
private _viewBox: string;
updated = new Subject<void>();
private _initComplete = false;
constructor(private _iconLoader: IconLoader) {
this._loadIcons();
}
private async _loadIcons(): Promise<void> {
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<Icon | undefined> {
return new Observable<Icon | undefined>((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<string, Icon>();
private _aliases = new Map<string, string>();
private _fallback: string;
private _viewBox: string;
updated = new Subject<void>();
private _initComplete = false;
constructor(private _iconLoader: IconLoader) {
this._loadIcons();
}
private async _loadIcons(): Promise<void> {
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<Icon | undefined> {
return new Observable<Icon | undefined>((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();
});
}
}

View File

@@ -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: `
<svg [style.width.rem]="size / 16" [style.height.rem]="size / 16" [attr.viewBox]="viewBox">
<path fill="currentColor" [attr.d]="data" />
</svg>
`,
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<void>();
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: `
<svg
[style.width.rem]="size / 16"
[style.height.rem]="size / 16"
[attr.viewBox]="viewBox"
>
<path fill="currentColor" [attr.d]="data" />
</svg>
`,
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<void>();
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();
}
}

View File

@@ -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),
};
}
}

View File

@@ -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';
}

View File

@@ -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<string, SvgIcon>();
private _aliases = new Map<string, string>();
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<string, SvgIcon>();
private _aliases = new Map<string, string>();
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 };
}
}

View File

@@ -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';
}
}

View File

@@ -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: `
<svg [style.width.rem]="size / 16" [style.height.rem]="size / 16" [attr.viewBox]="viewBox">
<path fill="currentColor" [attr.d]="data" />
</svg>
`,
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: `
<svg
[style.width.rem]="size / 16"
[style.height.rem]="size / 16"
[attr.viewBox]="viewBox"
>
<path fill="currentColor" [attr.d]="data" />
</svg>
`,
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();
}
}
}

View File

@@ -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<UiIconModule> {
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<UiIconModule> {
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,
};
}
}