mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
25 Commits
feature/54
...
0a1f25a1ee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a1f25a1ee | ||
|
|
609a7ed6dd | ||
|
|
5bebd3de4d | ||
|
|
b7e69dacf7 | ||
|
|
a8cca9143e | ||
|
|
16b9761573 | ||
|
|
7a86fcf507 | ||
|
|
1cc13eebe1 | ||
|
|
44426109bd | ||
|
|
bb9e9ff90e | ||
|
|
e5c7c18c40 | ||
|
|
e3c60f14f7 | ||
|
|
5fe85282e7 | ||
|
|
9a8eac3f9a | ||
|
|
93752efb9d | ||
|
|
0c546802fa | ||
|
|
3ed3d0b466 | ||
|
|
daf79d55a5 | ||
|
|
062a8044f2 | ||
|
|
86b0493591 | ||
|
|
85f1184648 | ||
|
|
803a53253c | ||
|
|
abcb8e2cb4 | ||
|
|
598a77b288 | ||
|
|
e5dd1e312d |
@@ -3,6 +3,9 @@ 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() {
|
||||
|
||||
@@ -5,6 +5,9 @@ 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],
|
||||
@@ -14,7 +17,9 @@ export class ScanditScanAdapterModule {
|
||||
static forRoot() {
|
||||
return {
|
||||
ngModule: ScanditScanAdapterModule,
|
||||
providers: [{ provide: SCAN_ADAPTER, useClass: ScanditScanAdapter, multi: true }],
|
||||
providers: [
|
||||
{ provide: SCAN_ADAPTER, useClass: ScanditScanAdapter, multi: true },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Config } from '@core/config';
|
||||
import { ComponentPortal } from '@angular/cdk/portal';
|
||||
import { ScanditOverlayComponent } from './scandit-overlay.component';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { injectNetworkStatus$ } from 'apps/isa-app/src/app/services/network-status.service';
|
||||
import { injectNetworkStatus$ } from '@isa/core/connectivity';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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 {}
|
||||
@@ -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 {}
|
||||
@@ -1,4 +1,4 @@
|
||||
@if ($offlineBannerVisible()) {
|
||||
<!-- @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>
|
||||
@@ -25,4 +25,4 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet></router-outlet> -->
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
@apply block;
|
||||
}
|
||||
@@ -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();
|
||||
// });
|
||||
// });
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
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 { injectOnline$ } from './services/network-status.service';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
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);
|
||||
|
||||
@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();
|
||||
|
||||
$online = toSignal(injectOnline$());
|
||||
// $offlineBannerVisible = signal(false);
|
||||
|
||||
$offlineBannerVisible = signal(false);
|
||||
// $onlineBannerVisible = signal(false);
|
||||
|
||||
$onlineBannerVisible = signal(false);
|
||||
// private onlineBannerDismissTimeout: any;
|
||||
|
||||
private onlineBannerDismissTimeout: any;
|
||||
// onlineEffects = effect(() => {
|
||||
// const status = this.$networkStatus();
|
||||
// const online = status === 'online';
|
||||
// const offlineBannerVisible = this.$offlineBannerVisible();
|
||||
|
||||
onlineEffects = effect(() => {
|
||||
const online = this.$online();
|
||||
const offlineBannerVisible = this.$offlineBannerVisible();
|
||||
// untracked(() => {
|
||||
// this.$offlineBannerVisible.set(!online);
|
||||
|
||||
untracked(() => {
|
||||
this.$offlineBannerVisible.set(!online);
|
||||
// if (!online) {
|
||||
// this.$onlineBannerVisible.set(false);
|
||||
// clearTimeout(this.onlineBannerDismissTimeout);
|
||||
// }
|
||||
|
||||
if (!online) {
|
||||
this.$onlineBannerVisible.set(false);
|
||||
clearTimeout(this.onlineBannerDismissTimeout);
|
||||
}
|
||||
// if (offlineBannerVisible && online) {
|
||||
// this.$onlineBannerVisible.set(true);
|
||||
// this.onlineBannerDismissTimeout = setTimeout(() => this.$onlineBannerVisible.set(false), 5000);
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
if (offlineBannerVisible && online) {
|
||||
this.$onlineBannerVisible.set(true);
|
||||
this.onlineBannerDismissTimeout = setTimeout(() => this.$onlineBannerVisible.set(false), 5000);
|
||||
}
|
||||
});
|
||||
});
|
||||
// private _checkForUpdates: number = this._config.get('checkForUpdates');
|
||||
|
||||
private _checkForUpdates: number = this._config.get('checkForUpdates');
|
||||
// get checkForUpdates(): number {
|
||||
// return this._checkForUpdates ?? 60 * 60 * 1000; // default 1 hour
|
||||
// }
|
||||
|
||||
get checkForUpdates(): number {
|
||||
return this._checkForUpdates ?? 60 * 60 * 1000; // default 1 hour
|
||||
}
|
||||
// // For Unit Testing
|
||||
// set checkForUpdates(time: number) {
|
||||
// this._checkForUpdates = time;
|
||||
// }
|
||||
|
||||
// For Unit Testing
|
||||
set checkForUpdates(time: number) {
|
||||
this._checkForUpdates = time;
|
||||
}
|
||||
// subscriptions = new Subscription();
|
||||
|
||||
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;
|
||||
// }
|
||||
|
||||
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));
|
||||
|
||||
ngOnInit() {
|
||||
this.setTitle();
|
||||
this.logVersion();
|
||||
asapScheduler.schedule(() => this.determinePlatform(), 250);
|
||||
this._appService.getSection$().subscribe(this.sectionChangeHandler.bind(this));
|
||||
// this.setupSilentRefresh();
|
||||
// }
|
||||
|
||||
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();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// 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'));
|
||||
// }
|
||||
|
||||
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;',
|
||||
// );
|
||||
// }
|
||||
|
||||
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');
|
||||
// }
|
||||
// }
|
||||
|
||||
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');
|
||||
// }
|
||||
// }
|
||||
|
||||
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;
|
||||
// }
|
||||
|
||||
updateClient() {
|
||||
if (!this._swUpdate.isEnabled) {
|
||||
return;
|
||||
}
|
||||
// this.initialCheckForUpdate();
|
||||
// this.checkForUpdate();
|
||||
// }
|
||||
|
||||
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();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
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();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
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);
|
||||
|
||||
@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');
|
||||
}
|
||||
}
|
||||
}
|
||||
// return strategy.login('Sie sind nicht mehr angemeldet');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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 {
|
||||
@@ -67,10 +66,9 @@ import {
|
||||
matWifi,
|
||||
matWifiOff,
|
||||
} from '@ng-icons/material-icons/baseline';
|
||||
import { NetworkStatusService } from './services/network-status.service';
|
||||
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');
|
||||
@@ -106,7 +147,8 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
let online = false;
|
||||
const networkStatus = injector.get(NetworkStatusService);
|
||||
while (!online) {
|
||||
online = await firstValueFrom(networkStatus.online$);
|
||||
const status = await firstValueFrom(networkStatus.status$);
|
||||
online = status === 'online';
|
||||
|
||||
if (!online) {
|
||||
logger.warn('Waiting for network connection');
|
||||
@@ -161,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') });
|
||||
@@ -171,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),
|
||||
@@ -182,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, () => ({
|
||||
@@ -223,7 +263,7 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
};
|
||||
}
|
||||
|
||||
export function _notificationsHubOptionsFactory(
|
||||
function notificationsHubOptionsFactory(
|
||||
config: Config,
|
||||
auth: AuthService,
|
||||
): SignalRHubOptions {
|
||||
@@ -257,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 {}
|
||||
};
|
||||
0
apps/isa-app/src/app/app.css
Normal file
0
apps/isa-app/src/app/app.css
Normal file
3
apps/isa-app/src/app/app.html
Normal file
3
apps/isa-app/src/app/app.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<shell-layout>
|
||||
<router-outlet />
|
||||
</shell-layout>
|
||||
@@ -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 {}
|
||||
11
apps/isa-app/src/app/app.ts
Normal file
11
apps/isa-app/src/app/app.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { ShellLayoutComponent } from '@isa/shell/layout';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.html',
|
||||
styleUrls: ['./app.css'],
|
||||
imports: [RouterOutlet, ShellLayoutComponent],
|
||||
})
|
||||
export class App {}
|
||||
@@ -5,7 +5,7 @@ import { ScanAdapterService } from '@adapter/scan';
|
||||
import { AuthService as IsaAuthService } from '@generated/swagger/isa-api';
|
||||
import { UiConfirmModalComponent, UiErrorModalComponent, UiModalResult, UiModalService } from '@ui/modal';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { injectNetworkStatus$ } from '../services/network-status.service';
|
||||
import { injectNetworkStatus$ } from '@isa/core/connectivity';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { from, NEVER, Observable, throwError } from 'rxjs';
|
||||
import { catchError, filter, mergeMap, takeUntil } from 'rxjs/operators';
|
||||
import { AuthService, LoginStrategy } from '@core/auth';
|
||||
import { injectOnline$ } from '../services/network-status.service';
|
||||
import { injectNetworkStatus$ } from '@isa/core/connectivity';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
@Injectable()
|
||||
@@ -17,7 +17,7 @@ export class HttpErrorInterceptor implements HttpInterceptor {
|
||||
#logger = logger(() => ({
|
||||
'http-interceptor': 'HttpErrorInterceptor',
|
||||
}));
|
||||
#offline$ = injectOnline$().pipe(filter((online) => !online));
|
||||
#offline$ = injectNetworkStatus$().pipe(filter((status) => status === 'offline'));
|
||||
#injector = inject(Injector);
|
||||
#auth = inject(AuthService);
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<shell-root>
|
||||
<router-outlet></router-outlet>
|
||||
</shell-root>
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './network-status.service';
|
||||
@@ -1,25 +0,0 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NetworkStatusService {
|
||||
online$ = new Observable<boolean>((subscriber) => {
|
||||
const handler = () => subscriber.next(navigator.onLine);
|
||||
|
||||
window.addEventListener('online', handler);
|
||||
window.addEventListener('offline', handler);
|
||||
|
||||
handler();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handler);
|
||||
window.removeEventListener('offline', handler);
|
||||
};
|
||||
});
|
||||
|
||||
status$ = this.online$.pipe(map((online) => (online ? 'online' : 'offline')));
|
||||
}
|
||||
|
||||
export const injectNetworkStatus$ = () => inject(NetworkStatusService).status$;
|
||||
|
||||
export const injectOnline$ = () => inject(NetworkStatusService).online$;
|
||||
@@ -1,2 +1 @@
|
||||
export * from './token-login.component';
|
||||
export * from './token-login.module';
|
||||
|
||||
@@ -7,7 +7,6 @@ import { AuthService } from '@core/auth';
|
||||
templateUrl: 'token-login.component.html',
|
||||
styleUrls: ['token-login.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class TokenLoginComponent implements OnInit {
|
||||
constructor(
|
||||
@@ -17,7 +16,10 @@ export class TokenLoginComponent implements OnInit {
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (this._route.snapshot.params.token && !this._authService.isAuthenticated()) {
|
||||
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()) {
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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 {}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
@@ -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>;
|
||||
} = {};
|
||||
|
||||
if (changes.name) {
|
||||
tabChanges.name = changes.name;
|
||||
}
|
||||
|
||||
patchProcessData(processId: number, data: Record<string, any>) {
|
||||
this.store.dispatch(patchProcessData({ processId, data }));
|
||||
// Store other ApplicationProcess properties in metadata
|
||||
const metadataKeys = [
|
||||
'section',
|
||||
'type',
|
||||
'closeable',
|
||||
'confirmClosing',
|
||||
'created',
|
||||
'activated',
|
||||
'data',
|
||||
];
|
||||
metadataKeys.forEach((key) => {
|
||||
if (tabChanges.metadata === undefined) {
|
||||
tabChanges.metadata = {};
|
||||
}
|
||||
|
||||
getSelectedBranch$(processId?: number): Observable<BranchDTO> {
|
||||
if (!processId) {
|
||||
return this.activatedProcessId$.pipe(
|
||||
switchMap((processId) =>
|
||||
this.getProcessById$(processId).pipe(
|
||||
map((process) => process?.data?.selectedBranch),
|
||||
),
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
return this.getProcessById$(processId).pipe(
|
||||
map((process) => process?.data?.selectedBranch),
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 './store/application.actions';
|
||||
|
||||
@@ -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> }>(),
|
||||
export const removeProcess = createAction(
|
||||
`${prefix} Remove Process`,
|
||||
props<{ processId: number }>(),
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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]);
|
||||
// });
|
||||
// });
|
||||
@@ -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),
|
||||
);
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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
|
||||
@@ -3,7 +3,7 @@ import { inject, Injectable } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { UiConfirmModalComponent, UiModalResult, UiModalService } from '@ui/modal';
|
||||
import { injectNetworkStatus$ } from '../../app/services';
|
||||
import { injectNetworkStatus$ } from '@isa/core/connectivity';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthService as IsaAuthService } from '@generated/swagger/isa-api';
|
||||
import { firstValueFrom, lastValueFrom } from 'rxjs';
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
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';
|
||||
|
||||
@NgModule()
|
||||
export class CoreBreadcrumbModule {
|
||||
static forRoot(): ModuleWithProviders<CoreBreadcrumbModule> {
|
||||
return {
|
||||
ngModule: CoreBreadcrumbForRootModule,
|
||||
};
|
||||
export function provideCoreBreadcrumb(): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([
|
||||
provideState({ name: featureName, reducer: breadcrumbReducer }),
|
||||
provideEffects(BreadcrumbEffects),
|
||||
BreadcrumbService,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [StoreModule.forFeature(featureName, breadcrumbReducer), EffectsModule.forFeature([BreadcrumbEffects])],
|
||||
providers: [BreadcrumbService],
|
||||
})
|
||||
export class CoreBreadcrumbForRootModule {}
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,15 @@ import {
|
||||
AvailabilityType,
|
||||
} from '@generated/swagger/availability-api';
|
||||
import { AvailabilityDTO as CatAvailabilityDTO } from '@generated/swagger/cat-search-api';
|
||||
import { map, shareReplay, switchMap, withLatestFrom, mergeMap, timeout, first } from 'rxjs/operators';
|
||||
import {
|
||||
map,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
withLatestFrom,
|
||||
mergeMap,
|
||||
timeout,
|
||||
first,
|
||||
} from 'rxjs/operators';
|
||||
import { isArray, memorize } from '@utils/common';
|
||||
import { LogisticianDTO, LogisticianService } from '@generated/swagger/oms-api';
|
||||
import {
|
||||
@@ -30,7 +38,7 @@ import { AvailabilityByBranchDTO, ItemData, Ssc } from './defs';
|
||||
import { Availability } from './defs/availability';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainAvailabilityService {
|
||||
// Ticket #3378 Keep Result List Items and Details Page SSC in sync
|
||||
sscs$ = new BehaviorSubject<Array<Ssc>>([]);
|
||||
@@ -45,8 +53,12 @@ export class DomainAvailabilityService {
|
||||
) {}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
memorizedAvailabilityShippingAvailability(request: Array<AvailabilityRequestDTO>) {
|
||||
return this._availabilityService.AvailabilityShippingAvailability(request).pipe(shareReplay(1));
|
||||
memorizedAvailabilityShippingAvailability(
|
||||
request: Array<AvailabilityRequestDTO>,
|
||||
) {
|
||||
return this._availabilityService
|
||||
.AvailabilityShippingAvailability(request)
|
||||
.pipe(shareReplay(1));
|
||||
}
|
||||
|
||||
@memorize()
|
||||
@@ -60,7 +72,9 @@ export class DomainAvailabilityService {
|
||||
@memorize()
|
||||
getTakeAwaySupplier(): Observable<SupplierDTO> {
|
||||
return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe(
|
||||
map(({ result }) => result?.find((supplier) => supplier?.supplierNumber === 'F')),
|
||||
map(({ result }) =>
|
||||
result?.find((supplier) => supplier?.supplierNumber === 'F'),
|
||||
),
|
||||
shareReplay(1),
|
||||
);
|
||||
}
|
||||
@@ -117,7 +131,9 @@ export class DomainAvailabilityService {
|
||||
@memorize({})
|
||||
getLogisticians(): Observable<LogisticianDTO> {
|
||||
return this._logisticanService.LogisticianGetLogisticians({}).pipe(
|
||||
map((response) => response.result?.find((l) => l.logisticianNumber === '2470')),
|
||||
map((response) =>
|
||||
response.result?.find((l) => l.logisticianNumber === '2470'),
|
||||
),
|
||||
shareReplay(1),
|
||||
);
|
||||
}
|
||||
@@ -133,22 +149,27 @@ export class DomainAvailabilityService {
|
||||
price: PriceDTO;
|
||||
quantity: number;
|
||||
}): Observable<AvailabilityByBranchDTO[]> {
|
||||
return this._stockService.StockStockRequest({ stockRequest: { branchIds, itemId } }).pipe(
|
||||
return this._stockService
|
||||
.StockStockRequest({ stockRequest: { branchIds, itemId } })
|
||||
.pipe(
|
||||
map((response) => response.result),
|
||||
withLatestFrom(this.getTakeAwaySupplier()),
|
||||
map(([result, supplier]) => {
|
||||
const availabilities: AvailabilityByBranchDTO[] = result.map((stockInfo) => {
|
||||
const availabilities: AvailabilityByBranchDTO[] = result.map(
|
||||
(stockInfo) => {
|
||||
return {
|
||||
availableQuantity: stockInfo.availableQuantity,
|
||||
availabilityType: quantity <= stockInfo.inStock ? 1024 : 1, // 1024 (=Available)
|
||||
inStock: stockInfo.inStock,
|
||||
supplierSSC: quantity <= stockInfo.inStock ? '999' : '',
|
||||
supplierSSCText: quantity <= stockInfo.inStock ? 'Filialentnahme' : '',
|
||||
supplierSSCText:
|
||||
quantity <= stockInfo.inStock ? 'Filialentnahme' : '',
|
||||
price,
|
||||
supplier: { id: supplier?.id },
|
||||
branchId: stockInfo.branchId,
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
return availabilities;
|
||||
}),
|
||||
shareReplay(1),
|
||||
@@ -165,11 +186,16 @@ export class DomainAvailabilityService {
|
||||
quantity: number;
|
||||
branch?: BranchDTO;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
const request = branch ? this.getStockByBranch(branch.id) : this.getDefaultStock();
|
||||
const request = branch
|
||||
? this.getStockByBranch(branch.id)
|
||||
: this.getDefaultStock();
|
||||
return request.pipe(
|
||||
switchMap((s) =>
|
||||
combineLatest([
|
||||
this._stockService.StockInStock({ articleIds: [item.itemId], stockId: s.id }),
|
||||
this._stockService.StockInStock({
|
||||
articleIds: [item.itemId],
|
||||
stockId: s.id,
|
||||
}),
|
||||
this.getTakeAwaySupplier(),
|
||||
this.getDefaultBranch(),
|
||||
]),
|
||||
@@ -201,11 +227,19 @@ export class DomainAvailabilityService {
|
||||
quantity: number;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
return combineLatest([
|
||||
this._stockService.StockStockRequest({ stockRequest: { branchIds: [branch.id], itemId } }),
|
||||
this._stockService.StockStockRequest({
|
||||
stockRequest: { branchIds: [branch.id], itemId },
|
||||
}),
|
||||
this.getTakeAwaySupplier(),
|
||||
]).pipe(
|
||||
map(([response, supplier]) => {
|
||||
return this._mapToTakeAwayAvailability({ response, supplier, branchId: branch.id, quantity, price });
|
||||
return this._mapToTakeAwayAvailability({
|
||||
response,
|
||||
supplier,
|
||||
branchId: branch.id,
|
||||
quantity,
|
||||
price,
|
||||
});
|
||||
}),
|
||||
shareReplay(1),
|
||||
);
|
||||
@@ -222,9 +256,13 @@ export class DomainAvailabilityService {
|
||||
quantity: number;
|
||||
branchId?: number;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
const request = branchId ? this.getStockByBranch(branchId) : this.getDefaultStock();
|
||||
const request = branchId
|
||||
? this.getStockByBranch(branchId)
|
||||
: this.getDefaultStock();
|
||||
return request.pipe(
|
||||
switchMap((s) => this._stockService.StockInStockByEAN({ eans, stockId: s.id })),
|
||||
switchMap((s) =>
|
||||
this._stockService.StockInStockByEAN({ eans, stockId: s.id }),
|
||||
),
|
||||
withLatestFrom(this.getTakeAwaySupplier(), this.getDefaultBranch()),
|
||||
map(([response, supplier, defaultBranch]) => {
|
||||
return this._mapToTakeAwayAvailability({
|
||||
@@ -239,10 +277,19 @@ export class DomainAvailabilityService {
|
||||
);
|
||||
}
|
||||
|
||||
getTakeAwayAvailabilitiesByEans({ eans }: { eans: string[] }): Observable<StockInfoDTO[]> {
|
||||
getTakeAwayAvailabilitiesByEans({
|
||||
eans,
|
||||
}: {
|
||||
eans: string[];
|
||||
}): Observable<StockInfoDTO[]> {
|
||||
const eansFiltered = Array.from(new Set(eans));
|
||||
return this.getDefaultStock().pipe(
|
||||
switchMap((s) => this._stockService.StockInStockByEAN({ eans: eansFiltered, stockId: s.id })),
|
||||
switchMap((s) =>
|
||||
this._stockService.StockInStockByEAN({
|
||||
eans: eansFiltered,
|
||||
stockId: s.id,
|
||||
}),
|
||||
),
|
||||
withLatestFrom(this.getTakeAwaySupplier(), this.getDefaultBranch()),
|
||||
map((response) => response[0].result),
|
||||
shareReplay(1),
|
||||
@@ -276,7 +323,13 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
|
||||
getDeliveryAvailability({
|
||||
item,
|
||||
quantity,
|
||||
}: {
|
||||
item: ItemData;
|
||||
quantity: number;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
return this.memorizedAvailabilityShippingAvailability([
|
||||
{
|
||||
ean: item?.ean,
|
||||
@@ -292,7 +345,13 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDigDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
|
||||
getDigDeliveryAvailability({
|
||||
item,
|
||||
quantity,
|
||||
}: {
|
||||
item: ItemData;
|
||||
quantity: number;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
return this.memorizedAvailabilityShippingAvailability([
|
||||
{
|
||||
qty: quantity,
|
||||
@@ -312,7 +371,10 @@ export class DomainAvailabilityService {
|
||||
sscText: preferred?.sscText,
|
||||
supplier: { id: preferred?.supplierId },
|
||||
isPrebooked: preferred?.isPrebooked,
|
||||
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
|
||||
estimatedShippingDate:
|
||||
preferred?.requestStatusCode === '32'
|
||||
? preferred?.altAt
|
||||
: preferred?.at,
|
||||
estimatedDelivery: preferred?.estimatedDelivery,
|
||||
price: preferred?.price,
|
||||
logistician: { id: preferred?.logisticianId },
|
||||
@@ -343,7 +405,11 @@ export class DomainAvailabilityService {
|
||||
return currentBranch$.pipe(
|
||||
timeout(5000),
|
||||
mergeMap((defaultBranch) =>
|
||||
this.getPickUpAvailability({ item, quantity, branch: branch ?? defaultBranch }).pipe(
|
||||
this.getPickUpAvailability({
|
||||
item,
|
||||
quantity,
|
||||
branch: branch ?? defaultBranch,
|
||||
}).pipe(
|
||||
mergeMap((availability) =>
|
||||
logistician$.pipe(
|
||||
map((logistician) => ({
|
||||
@@ -359,7 +425,11 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDownloadAvailability({ item }: { item: ItemData }): Observable<AvailabilityDTO> {
|
||||
getDownloadAvailability({
|
||||
item,
|
||||
}: {
|
||||
item: ItemData;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
return this.memorizedAvailabilityShippingAvailability([
|
||||
{
|
||||
ean: item?.ean,
|
||||
@@ -378,7 +448,10 @@ export class DomainAvailabilityService {
|
||||
sscText: preferred?.sscText,
|
||||
supplier: { id: preferred?.supplierId },
|
||||
isPrebooked: preferred?.isPrebooked,
|
||||
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
|
||||
estimatedShippingDate:
|
||||
preferred?.requestStatusCode === '32'
|
||||
? preferred?.altAt
|
||||
: preferred?.at,
|
||||
price: preferred?.price,
|
||||
supplierProductNumber: preferred?.supplierProductNumber,
|
||||
logistician: { id: preferred?.logisticianId },
|
||||
@@ -392,12 +465,18 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getTakeAwayAvailabilities(items: { id: number; price: PriceDTO }[], branchId: number) {
|
||||
getTakeAwayAvailabilities(
|
||||
items: { id: number; price: PriceDTO }[],
|
||||
branchId: number,
|
||||
) {
|
||||
return this._stockService.StockGetStocksByBranch({ branchId }).pipe(
|
||||
map((req) => req.result?.find((_) => true)?.id),
|
||||
switchMap((stockId) =>
|
||||
stockId
|
||||
? this._stockService.StockInStock({ articleIds: items.map((i) => i.id), stockId })
|
||||
? this._stockService.StockInStock({
|
||||
articleIds: items.map((i) => i.id),
|
||||
stockId,
|
||||
})
|
||||
: of({ result: [] } as ResponseArgsOfIEnumerableOfStockInfoDTO),
|
||||
),
|
||||
timeout(20000),
|
||||
@@ -417,10 +496,19 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getPickUpAvailabilities(payload: AvailabilityRequestDTO[], preferred?: boolean) {
|
||||
return this._availabilityService.AvailabilityStoreAvailability(payload).pipe(
|
||||
getPickUpAvailabilities(
|
||||
payload: AvailabilityRequestDTO[],
|
||||
preferred?: boolean,
|
||||
) {
|
||||
return this._availabilityService
|
||||
.AvailabilityStoreAvailability(payload)
|
||||
.pipe(
|
||||
timeout(20000),
|
||||
map((response) => (preferred ? this._mapToPickUpAvailability(response.result) : response.result)),
|
||||
map((response) =>
|
||||
preferred
|
||||
? this._mapToPickUpAvailability(response.result)
|
||||
: response.result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -448,7 +536,10 @@ export class DomainAvailabilityService {
|
||||
timeout(20000),
|
||||
switchMap((availability) =>
|
||||
logistician$.pipe(
|
||||
map((logistician) => ({ availability: [...availability], logistician: { id: logistician.id } })),
|
||||
map((logistician) => ({
|
||||
availability: [...availability],
|
||||
logistician: { id: logistician.id },
|
||||
})),
|
||||
),
|
||||
),
|
||||
shareReplay(1),
|
||||
@@ -465,7 +556,10 @@ export class DomainAvailabilityService {
|
||||
return availability?.price || catalogAvailability?.price;
|
||||
case 'delivery':
|
||||
case 'dig-delivery':
|
||||
if (catalogAvailability?.price?.value?.value < availability?.price?.value?.value) {
|
||||
if (
|
||||
catalogAvailability?.price?.value?.value <
|
||||
availability?.price?.value?.value
|
||||
) {
|
||||
return catalogAvailability?.price;
|
||||
}
|
||||
return availability?.price || catalogAvailability?.price;
|
||||
@@ -477,7 +571,9 @@ export class DomainAvailabilityService {
|
||||
if (availability?.supplier?.id === 16 && availability?.inStock == 0) {
|
||||
return false;
|
||||
}
|
||||
return [2, 32, 256, 1024, 2048, 4096].some((code) => availability?.availabilityType === code);
|
||||
return [2, 32, 256, 1024, 2048, 4096].some(
|
||||
(code) => availability?.availabilityType === code,
|
||||
);
|
||||
}
|
||||
|
||||
private _mapToTakeAwayAvailability({
|
||||
@@ -524,7 +620,10 @@ export class DomainAvailabilityService {
|
||||
|
||||
const availability = {
|
||||
itemId: stockInfo.itemId,
|
||||
availabilityType: quantity <= inStock ? (1024 as AvailabilityType) : (1 as AvailabilityType), // 1024 (=Available)
|
||||
availabilityType:
|
||||
quantity <= inStock
|
||||
? (1024 as AvailabilityType)
|
||||
: (1 as AvailabilityType), // 1024 (=Available)
|
||||
inStock: inStock,
|
||||
supplierSSC: quantity <= inStock ? '999' : '',
|
||||
supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '',
|
||||
@@ -539,7 +638,10 @@ export class DomainAvailabilityService {
|
||||
): Availability<AvailabilityDTO, SwaggerAvailabilityDTO>[] {
|
||||
if (isArray(availabilities)) {
|
||||
const preferred = availabilities.filter((f) => f.preferred === 1);
|
||||
const totalAvailable = availabilities.reduce((sum, av) => sum + (av?.qty || 0), 0);
|
||||
const totalAvailable = availabilities.reduce(
|
||||
(sum, av) => sum + (av?.qty || 0),
|
||||
0,
|
||||
);
|
||||
|
||||
return preferred.map((p) => {
|
||||
return [
|
||||
@@ -550,7 +652,8 @@ export class DomainAvailabilityService {
|
||||
sscText: p?.sscText,
|
||||
supplier: { id: p?.supplierId },
|
||||
isPrebooked: p?.isPrebooked,
|
||||
estimatedShippingDate: p?.requestStatusCode === '32' ? p?.altAt : p?.at,
|
||||
estimatedShippingDate:
|
||||
p?.requestStatusCode === '32' ? p?.altAt : p?.at,
|
||||
price: p?.price,
|
||||
inStock: totalAvailable,
|
||||
supplierProductNumber: p?.supplierProductNumber,
|
||||
@@ -565,7 +668,9 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
}
|
||||
|
||||
private _mapToShippingAvailability(availabilities: SwaggerAvailabilityDTO[]): AvailabilityDTO[] {
|
||||
private _mapToShippingAvailability(
|
||||
availabilities: SwaggerAvailabilityDTO[],
|
||||
): AvailabilityDTO[] {
|
||||
const preferred = availabilities.filter((f) => f.preferred === 1);
|
||||
return preferred.map((p) => {
|
||||
return {
|
||||
@@ -585,7 +690,10 @@ export class DomainAvailabilityService {
|
||||
});
|
||||
}
|
||||
|
||||
getInStockByEan(params: { eans: string[]; branchId?: number }): Observable<Record<string, StockInfoDTO>> {
|
||||
getInStockByEan(params: {
|
||||
eans: string[];
|
||||
branchId?: number;
|
||||
}): Observable<Record<string, StockInfoDTO>> {
|
||||
let branchId$ = of(params.branchId);
|
||||
|
||||
if (!params.branchId) {
|
||||
@@ -597,31 +705,41 @@ export class DomainAvailabilityService {
|
||||
|
||||
const stock$ = branchId$.pipe(
|
||||
mergeMap((branchId) =>
|
||||
this._stockService.StockGetStocksByBranch({ branchId }).pipe(map((response) => response.result?.[0])),
|
||||
this._stockService
|
||||
.StockGetStocksByBranch({ branchId })
|
||||
.pipe(map((response) => response.result?.[0])),
|
||||
),
|
||||
);
|
||||
|
||||
return stock$.pipe(
|
||||
mergeMap((stock) =>
|
||||
this._stockService.StockInStockByEAN({ eans: params.eans, stockId: stock.id }).pipe(
|
||||
this._stockService
|
||||
.StockInStockByEAN({ eans: params.eans, stockId: stock.id })
|
||||
.pipe(
|
||||
map((response) => {
|
||||
const result = response.result ?? [];
|
||||
|
||||
for (const stockInfo of result) {
|
||||
stockInfo.ean = stockInfo.ean;
|
||||
}
|
||||
|
||||
return result.reduce<Record<string, StockInfoDTO>>((acc, stockInfo) => {
|
||||
return result.reduce<Record<string, StockInfoDTO>>(
|
||||
(acc, stockInfo) => {
|
||||
acc[stockInfo.ean] = stockInfo;
|
||||
return acc;
|
||||
}, {});
|
||||
},
|
||||
{},
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getInStock({ itemIds, branchId }: { itemIds: number[]; branchId: number }): Observable<StockInfoDTO[]> {
|
||||
getInStock({
|
||||
itemIds,
|
||||
branchId,
|
||||
}: {
|
||||
itemIds: number[];
|
||||
branchId: number;
|
||||
}): Observable<StockInfoDTO[]> {
|
||||
return this.getStockByBranch(branchId).pipe(
|
||||
mergeMap((stock) =>
|
||||
this._stockService
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './availability.module';
|
||||
export * from './availability.service';
|
||||
export * from './defs';
|
||||
export * from './in-stock.service';
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { memorize } from '@utils/common';
|
||||
import { map, share, shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainCatalogService {
|
||||
constructor(
|
||||
private searchService: SearchService,
|
||||
@@ -34,7 +34,9 @@ export class DomainCatalogService {
|
||||
}
|
||||
|
||||
getSearchHistory({ take }: { take: number }) {
|
||||
return this.searchService.SearchHistory(take ?? 5).pipe(map((res) => res.result));
|
||||
return this.searchService
|
||||
.SearchHistory(take ?? 5)
|
||||
.pipe(map((res) => res.result));
|
||||
}
|
||||
|
||||
@memorize({ ttl: 120000 })
|
||||
@@ -84,7 +86,11 @@ export class DomainCatalogService {
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getPromotionPoints({ items }: { items: { id: number; quantity: number; price?: number }[] }) {
|
||||
getPromotionPoints({
|
||||
items,
|
||||
}: {
|
||||
items: { id: number; quantity: number; price?: number }[];
|
||||
}) {
|
||||
return this.promotionService.PromotionLesepunkte(items).pipe(shareReplay());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './catalog.module';
|
||||
export * from './catalog.service';
|
||||
export * from './thumbnail-url.pipe';
|
||||
export * from './thumbnail.service';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,15 +3,23 @@ import { memorize } from '@utils/common';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
import { DomainCatalogService } from './catalog.service';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainCatalogThumbnailService {
|
||||
constructor(private domainCatalogService: DomainCatalogService) {}
|
||||
|
||||
@memorize()
|
||||
getThumnaulUrl({ ean, height, width }: { width?: number; height?: number; ean?: string }) {
|
||||
getThumnaulUrl({
|
||||
ean,
|
||||
height,
|
||||
width,
|
||||
}: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
ean?: string;
|
||||
}) {
|
||||
return this.domainCatalogService.getSettings().pipe(
|
||||
map((settings) => {
|
||||
let thumbnailUrl = settings.imageUrl.replace(/{ean}/, ean);
|
||||
const thumbnailUrl = settings.imageUrl.replace(/{ean}/, ean);
|
||||
return thumbnailUrl;
|
||||
}),
|
||||
shareReplay(),
|
||||
|
||||
@@ -1,29 +1,15 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
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';
|
||||
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],
|
||||
};
|
||||
export function provideDomainCheckout(): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([
|
||||
provideState({ name: storeFeatureName, reducer: domainCheckoutReducer }),
|
||||
provideEffects(DomainCheckoutEffects),
|
||||
DomainCheckoutService,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
StoreModule.forFeature(storeFeatureName, domainCheckoutReducer),
|
||||
EffectsModule.forFeature([DomainCheckoutEffects]),
|
||||
],
|
||||
})
|
||||
export class RootDomainCheckoutModule {}
|
||||
|
||||
@@ -1071,7 +1071,7 @@ export class DomainCheckoutService {
|
||||
});
|
||||
} else if (orderType === 'B2B-Versand') {
|
||||
const branch = await this.applicationService
|
||||
.getSelectedBranch$(processId)
|
||||
.getSelectedBranch$()
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
availability$ =
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { InfoService } from '@generated/swagger/isa-api';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainDashboardService {
|
||||
constructor(private readonly _infoService: InfoService) {}
|
||||
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './dashboard.service';
|
||||
export * from './defs';
|
||||
export * from './domain-isa.module';
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AbholfachService, AutocompleteTokenDTO, QueryTokenDTO } from '@generated/swagger/oms-api';
|
||||
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()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainGoodsService {
|
||||
constructor(
|
||||
private abholfachService: AbholfachService,
|
||||
@@ -19,11 +23,15 @@ export class DomainGoodsService {
|
||||
}
|
||||
|
||||
wareneingangComplete(autocompleteToken: AutocompleteTokenDTO) {
|
||||
return this.abholfachService.AbholfachWareneingangAutocomplete(autocompleteToken);
|
||||
return this.abholfachService.AbholfachWareneingangAutocomplete(
|
||||
autocompleteToken,
|
||||
);
|
||||
}
|
||||
|
||||
warenausgabeComplete(autocompleteToken: AutocompleteTokenDTO) {
|
||||
return this.abholfachService.AbholfachWarenausgabeAutocomplete(autocompleteToken);
|
||||
return this.abholfachService.AbholfachWarenausgabeAutocomplete(
|
||||
autocompleteToken,
|
||||
);
|
||||
}
|
||||
|
||||
getWareneingangItemByOrderNumber(orderNumber: string) {
|
||||
@@ -81,12 +89,16 @@ export class DomainGoodsService {
|
||||
|
||||
@memorize()
|
||||
goodsInQuerySettings() {
|
||||
return this.abholfachService.AbholfachWareneingangQuerySettings().pipe(shareReplay());
|
||||
return this.abholfachService
|
||||
.AbholfachWareneingangQuerySettings()
|
||||
.pipe(shareReplay());
|
||||
}
|
||||
|
||||
@memorize()
|
||||
goodsOutQuerySettings() {
|
||||
return this.abholfachService.AbholfachWarenausgabeQuerySettings().pipe(shareReplay());
|
||||
return this.abholfachService
|
||||
.AbholfachWarenausgabeQuerySettings()
|
||||
.pipe(shareReplay());
|
||||
}
|
||||
|
||||
goodsInList(queryToken: QueryTokenDTO) {
|
||||
@@ -95,7 +107,9 @@ export class DomainGoodsService {
|
||||
|
||||
@memorize()
|
||||
goodsInListQuerySettings() {
|
||||
return this.abholfachService.AbholfachWareneingangslisteQuerySettings().pipe(shareReplay());
|
||||
return this.abholfachService
|
||||
.AbholfachWareneingangslisteQuerySettings()
|
||||
.pipe(shareReplay());
|
||||
}
|
||||
|
||||
goodsInCleanupList() {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import { memorize } from '@utils/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainOmsService {
|
||||
constructor(
|
||||
private orderService: OrderService,
|
||||
@@ -33,9 +33,16 @@ export class DomainOmsService {
|
||||
private _orderCheckoutService: OrderCheckoutService,
|
||||
) {}
|
||||
|
||||
getOrderItemsByCustomerNumber(customerNumber: string, skip: number): Observable<OrderListItemDTO[]> {
|
||||
getOrderItemsByCustomerNumber(
|
||||
customerNumber: string,
|
||||
skip: number,
|
||||
): Observable<OrderListItemDTO[]> {
|
||||
return this.orderService
|
||||
.OrderGetOrdersByBuyerNumber({ buyerNumber: customerNumber, take: 20, skip })
|
||||
.OrderGetOrdersByBuyerNumber({
|
||||
buyerNumber: customerNumber,
|
||||
take: 20,
|
||||
skip,
|
||||
})
|
||||
.pipe(map((orders) => orders.result));
|
||||
}
|
||||
|
||||
@@ -55,7 +62,9 @@ export class DomainOmsService {
|
||||
|
||||
getReceipts(
|
||||
orderItemSubsetIds: number[],
|
||||
): Observable<ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO[]> {
|
||||
): Observable<
|
||||
ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO[]
|
||||
> {
|
||||
return this.receiptService
|
||||
.ReceiptGetReceiptsByOrderItemSubset({
|
||||
payload: {
|
||||
@@ -68,25 +77,43 @@ export class DomainOmsService {
|
||||
}
|
||||
|
||||
getReorderReasons() {
|
||||
return this._orderCheckoutService.OrderCheckoutGetReorderReasons().pipe(map((response) => response.result));
|
||||
return this._orderCheckoutService
|
||||
.OrderCheckoutGetReorderReasons()
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getVATs() {
|
||||
return this.vatService.VATGetVATs({}).pipe(map((response) => response.result));
|
||||
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(
|
||||
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));
|
||||
patchOrderItem(payload: {
|
||||
orderItemId: number;
|
||||
orderId: number;
|
||||
orderItem: Partial<OrderItemDTO>;
|
||||
}) {
|
||||
return this.orderService
|
||||
.OrderPatchOrderItem(payload)
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
patchOrderItemSubset(payload: {
|
||||
@@ -95,7 +122,9 @@ export class DomainOmsService {
|
||||
orderId: number;
|
||||
orderItemSubset: Partial<OrderItemSubsetDTO>;
|
||||
}) {
|
||||
return this.orderService.OrderPatchOrderItemSubset(payload).pipe(map((response) => response.result));
|
||||
return this.orderService
|
||||
.OrderPatchOrderItemSubset(payload)
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
patchComment({
|
||||
@@ -150,13 +179,20 @@ export class DomainOmsService {
|
||||
orderItemSubsetId,
|
||||
orderItemSubset: {
|
||||
estimatedShippingDate:
|
||||
estimatedShippingDate instanceof Date ? estimatedShippingDate.toJSON() : estimatedShippingDate,
|
||||
estimatedShippingDate instanceof Date
|
||||
? estimatedShippingDate.toJSON()
|
||||
: estimatedShippingDate,
|
||||
},
|
||||
})
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
setPickUpDeadline(orderId: number, orderItemId: number, orderItemSubsetId: number, pickUpDeadline: string) {
|
||||
setPickUpDeadline(
|
||||
orderId: number,
|
||||
orderItemId: number,
|
||||
orderItemSubsetId: number,
|
||||
pickUpDeadline: string,
|
||||
) {
|
||||
return this.orderService
|
||||
.OrderPatchOrderItemSubset({
|
||||
orderId,
|
||||
@@ -178,7 +214,9 @@ export class DomainOmsService {
|
||||
}
|
||||
|
||||
changeStockStatusCode(payload: ChangeStockStatusCodeValues[]) {
|
||||
return this.orderService.OrderChangeStockStatusCode(payload).pipe(map((response) => response.result));
|
||||
return this.orderService
|
||||
.OrderChangeStockStatusCode(payload)
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
orderAtSupplier({
|
||||
@@ -197,7 +235,13 @@ export class DomainOmsService {
|
||||
});
|
||||
}
|
||||
|
||||
getNotifications(orderId: number): Observable<{ selected: NotificationChannel; email: string; mobile: string }> {
|
||||
getNotifications(
|
||||
orderId: number,
|
||||
): Observable<{
|
||||
selected: NotificationChannel;
|
||||
email: string;
|
||||
mobile: string;
|
||||
}> {
|
||||
return this.getOrder(orderId).pipe(
|
||||
map((order) => ({
|
||||
selected: order.notificationChannels,
|
||||
@@ -208,10 +252,15 @@ export class DomainOmsService {
|
||||
}
|
||||
|
||||
getOrderSource(orderId: number): Observable<string> {
|
||||
return this.getOrder(orderId).pipe(map((order) => order?.features?.orderSource));
|
||||
return this.getOrder(orderId).pipe(
|
||||
map((order) => order?.features?.orderSource),
|
||||
);
|
||||
}
|
||||
|
||||
updateNotifications(orderId: number, changes: { selected: NotificationChannel; email: string; mobile: string }) {
|
||||
updateNotifications(
|
||||
orderId: number,
|
||||
changes: { selected: NotificationChannel; email: string; mobile: string },
|
||||
) {
|
||||
const communicationDetails = {
|
||||
email: changes.email,
|
||||
mobile: changes.mobile,
|
||||
@@ -224,7 +273,11 @@ export class DomainOmsService {
|
||||
delete communicationDetails.mobile;
|
||||
}
|
||||
|
||||
return this.updateOrder({ orderId, notificationChannels: changes.selected, communicationDetails });
|
||||
return this.updateOrder({
|
||||
orderId,
|
||||
notificationChannels: changes.selected,
|
||||
communicationDetails,
|
||||
});
|
||||
}
|
||||
|
||||
updateOrder({
|
||||
@@ -270,7 +323,13 @@ export class DomainOmsService {
|
||||
.pipe(map((res) => res.result));
|
||||
}
|
||||
|
||||
generateNotifications({ orderId, taskTypes }: { orderId: number; taskTypes: string[] }) {
|
||||
generateNotifications({
|
||||
orderId,
|
||||
taskTypes,
|
||||
}: {
|
||||
orderId: number;
|
||||
taskTypes: string[];
|
||||
}) {
|
||||
return this.orderService.OrderRegenerateOrderItemStatusTasks({
|
||||
orderId,
|
||||
taskTypes,
|
||||
@@ -302,10 +361,16 @@ export class DomainOmsService {
|
||||
.pipe(
|
||||
map((res) =>
|
||||
res.result
|
||||
.sort((a, b) => new Date(b.completed).getTime() - new Date(a.completed).getTime())
|
||||
.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));
|
||||
(data[result.name] = data[result.name] || []).push(
|
||||
new Date(result.completed),
|
||||
);
|
||||
return data;
|
||||
},
|
||||
{} as Record<string, Date[]>,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ReceiptOrderItemSubsetReferenceValues, ReceiptService } from '@generated/swagger/oms-api';
|
||||
import {
|
||||
ReceiptOrderItemSubsetReferenceValues,
|
||||
ReceiptService,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { memorize } from '@utils/common';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainReceiptService {
|
||||
constructor(private receiptService: ReceiptService) {}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './defs';
|
||||
export * from './mappings';
|
||||
export * from './remission.module';
|
||||
export * from './remission.service';
|
||||
|
||||
@@ -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 {}
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import { Logger } from '@core/logger';
|
||||
import { RemissionPlacementType } from '@domain/remission';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainRemissionService {
|
||||
constructor(
|
||||
private readonly _logger: Logger,
|
||||
@@ -214,7 +214,7 @@ export class DomainRemissionService {
|
||||
|
||||
getStockInformation(
|
||||
items: RemissionListItem[],
|
||||
recalculate: boolean = false,
|
||||
recalculate = false,
|
||||
) {
|
||||
return this.getCurrentStock().pipe(
|
||||
switchMap((stock) =>
|
||||
@@ -407,10 +407,10 @@ export class DomainRemissionService {
|
||||
|
||||
async deleteReturn(returnId: number) {
|
||||
const returnDto = await this.getReturn(returnId).toPromise();
|
||||
for (const receipt of returnDto?.receipts) {
|
||||
await this.deleteReceipt(returnDto.id, receipt.id);
|
||||
for (const receipt of returnDto?.receipts ?? []) {
|
||||
await this.deleteReceipt(returnDto!.id, receipt.id);
|
||||
}
|
||||
await this.deleteRemission(returnDto.id);
|
||||
await this.deleteRemission(returnDto!.id);
|
||||
}
|
||||
|
||||
addReturnItem({
|
||||
|
||||
@@ -1,28 +1,39 @@
|
||||
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";
|
||||
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");
|
||||
moment.locale('de');
|
||||
|
||||
import { AppModule } from "./app/app.module";
|
||||
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 configRes = await fetch('/config/config.json');
|
||||
|
||||
const config = await configRes.json();
|
||||
|
||||
platformBrowserDynamic([
|
||||
await bootstrapApplication(App, {
|
||||
...appConfig,
|
||||
providers: [
|
||||
{ provide: CONFIG_DATA, useValue: config },
|
||||
]).bootstrapModule(AppModule);
|
||||
...appConfig.providers,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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 }))),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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]) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]) => {
|
||||
|
||||
@@ -14,8 +14,8 @@ import { UiTooltipModule } from '@ui/tooltip';
|
||||
imports: [
|
||||
CommonModule,
|
||||
PageCatalogRoutingModule,
|
||||
ArticleSearchModule,
|
||||
ArticleDetailsModule,
|
||||
ArticleSearchModule,
|
||||
BreadcrumbModule,
|
||||
BranchSelectorComponent,
|
||||
SharedSplitscreenComponent,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -183,7 +183,7 @@ export class CustomerOrderSearchResultsComponent
|
||||
debounceTime(150),
|
||||
switchMap(([processId, params]) =>
|
||||
this._application
|
||||
.getSelectedBranch$(processId)
|
||||
.getSelectedBranch$()
|
||||
.pipe(map((selectedBranch) => ({ processId, params, selectedBranch }))),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,10 @@ import { Icon, IconAlias, IconConfig } from './interfaces';
|
||||
import { IconLoader } from './loader';
|
||||
import { Observable, Subject, isObservable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
/**
|
||||
* @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>();
|
||||
|
||||
@@ -12,10 +12,17 @@ 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">
|
||||
<svg
|
||||
[style.width.rem]="size / 16"
|
||||
[style.height.rem]="size / 16"
|
||||
[attr.viewBox]="viewBox"
|
||||
>
|
||||
<path fill="currentColor" [attr.d]="data" />
|
||||
</svg>
|
||||
`,
|
||||
@@ -31,7 +38,7 @@ export class IconComponent implements OnInit, OnDestroy, OnChanges {
|
||||
viewBox: string;
|
||||
|
||||
@Input()
|
||||
size: number = 24;
|
||||
size = 24;
|
||||
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
@@ -41,7 +48,9 @@ export class IconComponent implements OnInit, OnDestroy, OnChanges {
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._iconRegistry.updated.pipe(takeUntil(this._onDestroy$)).subscribe(() => {
|
||||
this._iconRegistry.updated
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe(() => {
|
||||
this.updateIcon();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ 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) {
|
||||
@@ -17,6 +20,9 @@ export function provideIcon(loaderProvider?: Provider) {
|
||||
return providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use UiIconModule from '@isa/ui/icon' instead.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [IconComponent],
|
||||
exports: [IconComponent],
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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',
|
||||
|
||||
@@ -2,7 +2,10 @@ import { Injectable } from '@angular/core';
|
||||
import { SvgIcon } from './defs';
|
||||
import { IconAlias } from './defs/icon-alias';
|
||||
|
||||
@Injectable()
|
||||
/**
|
||||
* @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>();
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, Optional, Inject, HostBinding } from '@angular/core';
|
||||
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',
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
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">
|
||||
<svg
|
||||
[style.width.rem]="size / 16"
|
||||
[style.height.rem]="size / 16"
|
||||
[attr.viewBox]="viewBox"
|
||||
>
|
||||
<path fill="currentColor" [attr.d]="data" />
|
||||
</svg>
|
||||
`,
|
||||
@@ -20,7 +34,7 @@ export class UISvgIconComponent implements OnChanges {
|
||||
viewBox: string;
|
||||
|
||||
@Input()
|
||||
size: number = 24;
|
||||
size = 24;
|
||||
|
||||
constructor(
|
||||
private readonly _iconRegistry: IconRegistry,
|
||||
|
||||
@@ -7,6 +7,9 @@ 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();
|
||||
|
||||
@@ -27,6 +30,9 @@ export function _rootIconRegistryFactory(config: UiIconConfig): IconRegistry {
|
||||
return registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use UiIconModule from '@isa/ui/icon' instead.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
declarations: [UiIconComponent, UiIconBadgeComponent, UISvgIconComponent],
|
||||
|
||||
682
claude-code-guide.md
Normal file
682
claude-code-guide.md
Normal file
@@ -0,0 +1,682 @@
|
||||
# The Complete Claude Code Guide
|
||||
## From Configuration to Mastery
|
||||
|
||||
*A comprehensive reference for instructions, agents, commands, skills, hooks, and best practices*
|
||||
|
||||
*Compiled from Anthropic Engineering Blog, Official Documentation, and Community Best Practices — November 2025*
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#introduction)
|
||||
2. [Part 1: Effective Instructions Through CLAUDE.md](#part-1-effective-instructions-through-claudemd)
|
||||
3. [Part 2: Commands](#part-2-commands)
|
||||
4. [Part 3: The Agentic Architecture](#part-3-the-agentic-architecture)
|
||||
5. [Part 4: Agent Skills](#part-4-agent-skills)
|
||||
6. [Part 5: Hooks](#part-5-hooks)
|
||||
7. [Part 6: Best Practices](#part-6-best-practices)
|
||||
8. [Part 7: Context Engineering](#part-7-context-engineering)
|
||||
9. [Part 8: Advanced Features](#part-8-advanced-features)
|
||||
10. [Conclusion](#conclusion)
|
||||
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Claude Code represents a paradigm shift in AI-assisted development—an agentic command-line tool that provides near-raw model access without forcing specific workflows. Unlike traditional code assistants that offer suggestions, Claude Code follows an autonomous feedback loop: **gather context, take action, verify work, and repeat**.
|
||||
|
||||
This guide synthesizes official Anthropic documentation, engineering blog posts, and community best practices into a comprehensive reference for maximizing productivity with Claude Code. Three key configuration layers determine Claude's behavior:
|
||||
|
||||
- **CLAUDE.md files** for project context
|
||||
- **Hooks** for deterministic control
|
||||
- **Skills** for modular capabilities
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Effective Instructions Through CLAUDE.md
|
||||
|
||||
CLAUDE.md serves as Claude's persistent memory—a special file automatically loaded into context at session start. This file fundamentally shapes how Claude understands and works with your codebase.
|
||||
|
||||
### The Configuration Hierarchy
|
||||
|
||||
Claude loads CLAUDE.md files in a specific precedence order, allowing layered configuration from organization-wide policies down to personal preferences:
|
||||
|
||||
| Location | Scope | Version Control |
|
||||
|----------|-------|-----------------|
|
||||
| `/Library/Application Support/ClaudeCode/CLAUDE.md` (macOS) | Enterprise-wide | IT-managed |
|
||||
| `~/.claude/CLAUDE.md` | All projects | Personal |
|
||||
| `./CLAUDE.md` or `./.claude/CLAUDE.md` | Project-wide | Committed to git |
|
||||
| `./CLAUDE.local.md` | Project-specific personal | Git-ignored |
|
||||
| Child directories | On-demand loading | Per-directory |
|
||||
|
||||
Claude recursively loads CLAUDE.md files from the current working directory up to the root, then pulls in child directory files on-demand when accessing those locations.
|
||||
|
||||
### Structure and Syntax
|
||||
|
||||
A well-crafted CLAUDE.md contains concise, actionable information organized into clear sections:
|
||||
|
||||
```markdown
|
||||
# Bash commands
|
||||
- npm run build: Build the project
|
||||
- npm run typecheck: Run the typechecker
|
||||
- npm test -- --watch: Run tests in watch mode
|
||||
|
||||
# Code style
|
||||
- Use ES modules (import/export), not CommonJS (require)
|
||||
- Destructure imports when possible: import { foo } from 'bar'
|
||||
- Prefer named exports over default exports
|
||||
|
||||
# Workflow
|
||||
- Always typecheck after making code changes
|
||||
- Prefer running single tests over the full suite for performance
|
||||
- Commit logical units of work with descriptive messages
|
||||
|
||||
# Architecture
|
||||
- Frontend: Next.js with TypeScript in /app
|
||||
- Backend: Node.js with Express in /api
|
||||
- Database: PostgreSQL with Prisma ORM
|
||||
```
|
||||
|
||||
### Import Syntax
|
||||
|
||||
The `@path/to/file` syntax extends CLAUDE.md's capabilities by referencing external files. Maximum recursion depth is **5 hops**.
|
||||
|
||||
Example: *"For complex usage or if you encounter FooBarError, see @docs/troubleshooting.md for advanced steps."*
|
||||
|
||||
### Critical Anti-Patterns to Avoid
|
||||
|
||||
- **Don't @-file documentation directly:** Embedding entire files bloats context unnecessarily. Instead, provide paths with context: "For complex usage or if you encounter FooBarError, see path/to/docs.md"
|
||||
|
||||
- **Don't just say "never" without alternatives:** Negative-only constraints trap the agent. Always pair prohibitions with preferred approaches: "Never use the --foo flag; prefer --bar instead"
|
||||
|
||||
- **Keep it concise:** Large teams at Anthropic cap their CLAUDE.md files at approximately **13KB**. If CLI commands require paragraphs to explain, write a simpler bash wrapper instead—keeping CLAUDE.md concise forces better tooling design.
|
||||
|
||||
- **Use CLAUDE.md as a forcing function:** Keeping it short forces better tooling design.
|
||||
|
||||
### Quick Memory Feature
|
||||
|
||||
- Press **`#`** during any session to add memories that Claude automatically incorporates into the appropriate CLAUDE.md file
|
||||
- Use **`/memory`** to view and edit all loaded memories
|
||||
- Use **`/init`** to bootstrap a new CLAUDE.md by having Claude analyze your codebase
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Commands
|
||||
|
||||
Claude Code provides extensive command functionality through built-in slash commands and user-definable custom commands.
|
||||
|
||||
### Essential Built-in Commands
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/clear` | Reset context window—**use frequently between tasks** |
|
||||
| `/compact` | Compress context while preserving critical information |
|
||||
| `/context` | Visualize current token usage in the 200k window |
|
||||
| `/init` | Generate CLAUDE.md from codebase analysis |
|
||||
| `/memory` | View and edit CLAUDE.md files |
|
||||
| `/permissions` | Manage tool allowlists interactively |
|
||||
| `/hooks` | Configure automation hooks via menu interface |
|
||||
| `/model` | Switch between Claude models (Opus, Sonnet, Haiku) |
|
||||
| `/rewind` | Roll back conversation and code state |
|
||||
| `/add-dir` | Add directories to current session |
|
||||
| `/terminal-setup` | Configure terminal for optimal Claude Code usage |
|
||||
| `/ide` | Connect to IDE for linter integration |
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| **Escape** | Stop Claude mid-execution |
|
||||
| **Escape twice** | Jump back to previous messages or fork conversation |
|
||||
| **Shift+Tab** | Toggle auto-accept mode (⏵⏵ indicator) |
|
||||
| **Shift+Tab twice** | Activate Plan Mode |
|
||||
| **Ctrl+V** | Paste images |
|
||||
| **Up arrow** | Navigate chat history |
|
||||
| **#** | Add quick memory to CLAUDE.md |
|
||||
| **@** | Tag files with tab-completion |
|
||||
|
||||
### Creating Custom Slash Commands
|
||||
|
||||
Store prompt templates as Markdown files in `.claude/commands/` (project-scoped) or `~/.claude/commands/` (user-scoped). Commands support frontmatter metadata:
|
||||
|
||||
```markdown
|
||||
---
|
||||
allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*)
|
||||
argument-hint: [message]
|
||||
description: Create a git commit with the specified message
|
||||
model: claude-3-5-haiku-20241022
|
||||
---
|
||||
Create a git commit with message: $ARGUMENTS
|
||||
|
||||
Follow these steps:
|
||||
1. Run `git status` to check staged changes
|
||||
2. Review what will be committed
|
||||
3. Create the commit with the provided message
|
||||
4. Confirm success
|
||||
```
|
||||
|
||||
**Dynamic variables in custom commands:**
|
||||
|
||||
- `$ARGUMENTS`: All arguments passed to the command
|
||||
- `$1`, `$2`, `$3`: Positional arguments
|
||||
- `@filename`: Include file contents
|
||||
- `!command`: Execute bash command before processing
|
||||
|
||||
Use subdirectories for namespaced organization: `.claude/commands/testing/unit.md` becomes `/project:testing:unit`.
|
||||
|
||||
> 💡 **Best Practice:** Keep slash commands as simple shortcuts, not complex workflows. If you have a long list of complex custom commands, you've created an anti-pattern.
|
||||
|
||||
---
|
||||
|
||||
## Part 3: The Agentic Architecture
|
||||
|
||||
Claude Code functions as a fully autonomous agent with access to powerful tools. Understanding this architecture helps you leverage its capabilities effectively.
|
||||
|
||||
### Core Design Principle
|
||||
|
||||
The key design principle behind Claude Code is that Claude needs the same tools that programmers use every day. By giving Claude access to the user's computer via the terminal, it can read files, write and edit files, run tests, debug, and iterate until tasks succeed.
|
||||
|
||||
### The Agent Feedback Loop
|
||||
|
||||
1. **Gather Context:** Navigate filesystem, read files, use tools to understand the task
|
||||
2. **Take Action:** Execute bash commands, edit files, run scripts
|
||||
3. **Verify Work:** Run tests, check linting, validate output
|
||||
4. **Repeat:** Continue until task is complete
|
||||
|
||||
### Permission Modes
|
||||
|
||||
| Mode | Behavior | Activation |
|
||||
|------|----------|------------|
|
||||
| Normal | Asks permission for risky actions | Default |
|
||||
| Auto-Accept | Executes without confirmation | Shift+Tab toggle |
|
||||
| Plan Mode | Research only, no modifications | `--permission-mode plan` or Shift+Tab×2 |
|
||||
| Dangerous | Skips all permissions | `--dangerously-skip-permissions` (containers only) |
|
||||
|
||||
### Extended Thinking Keywords
|
||||
|
||||
Claude Code maps trigger words to increasing thinking budgets. Use these progressively for more complex analysis:
|
||||
|
||||
**`"think"` < `"think hard"` < `"think harder"` < `"ultrathink"`**
|
||||
|
||||
Each level allocates progressively more computational budget:
|
||||
- `"think"` triggers ~4,000 tokens
|
||||
- Medium phrases ~10,000 tokens
|
||||
- `"ultrathink"` provides maximum analysis depth at ~32,000 tokens
|
||||
|
||||
### Multi-Agent Patterns
|
||||
|
||||
Claude Code excels at multi-agent workflows. Anthropic's internal research shows that **multi-agent Claude Opus 4 with Sonnet 4 subagents outperformed single-agent Opus 4 by 90.2%** on research evaluations.
|
||||
|
||||
#### The Orchestrator-Worker Pattern
|
||||
|
||||
1. A lead agent analyzes the query and develops strategy
|
||||
2. Subagents spawn in parallel with isolated context windows
|
||||
3. Each subagent acts as an "intelligent filter," returning condensed findings
|
||||
4. The lead agent synthesizes results into coherent output
|
||||
|
||||
#### Practical Multi-Claude Workflows
|
||||
|
||||
- **Writer + Reviewer:** One Claude writes code, another reviews—use `/clear` between or run in separate terminals
|
||||
- **Git worktrees:** `git worktree add ../project-feature-a feature-a` enables parallel sessions on different branches
|
||||
- **Task() feature:** Use Claude's built-in Task() to spawn clones of the general agent with isolated context
|
||||
- **Multiple checkouts:** Create 3-4 git checkouts in separate folders, run different Claude instances with different tasks
|
||||
|
||||
### Headless Mode for Automation
|
||||
|
||||
Claude Code supports non-interactive execution for CI/CD and scripting:
|
||||
|
||||
```bash
|
||||
# Basic execution
|
||||
claude -p "your prompt here"
|
||||
|
||||
# With JSON output for parsing
|
||||
claude -p "analyze code" --output-format json
|
||||
|
||||
# Streaming JSON for real-time processing
|
||||
claude -p "analyze code" --output-format stream-json
|
||||
|
||||
# Continue previous conversation
|
||||
claude --continue -p "follow up question"
|
||||
|
||||
# Resume specific session
|
||||
claude --resume <session-id> -p "continue task"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Agent Skills
|
||||
|
||||
Skills extend Claude's functionality through organized folders containing instructions, scripts, and resources. Unlike slash commands (user-invoked), **skills are model-invoked**—Claude autonomously decides when to use them based on task context.
|
||||
|
||||
### What is a Skill?
|
||||
|
||||
A skill is a directory containing a SKILL.md file with organized folders of instructions, scripts, and resources that give agents additional capabilities. Building a skill for an agent is like putting together an onboarding guide for a new hire.
|
||||
|
||||
### Skill Structure
|
||||
|
||||
Skills live in `~/.claude/skills/` (personal) or `.claude/skills/` (project). Each skill requires a SKILL.md file with YAML frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: generating-commit-messages
|
||||
description: Generates clear commit messages from git diffs. Use when writing commit messages or reviewing staged changes.
|
||||
---
|
||||
|
||||
# Generating Commit Messages
|
||||
|
||||
## Instructions
|
||||
1. Run `git diff --staged` to see changes
|
||||
2. Analyze the nature and scope of modifications
|
||||
3. Suggest a commit message with:
|
||||
- Summary under 50 characters (imperative mood)
|
||||
- Detailed description if needed
|
||||
- List of affected components
|
||||
|
||||
## Best Practices
|
||||
- Use present tense ("Add feature" not "Added feature")
|
||||
- Explain what and why, not how
|
||||
- Reference issue numbers when applicable
|
||||
```
|
||||
|
||||
The **description field is critical**—it's the primary signal Claude uses to decide when to invoke a skill.
|
||||
|
||||
### Progressive Disclosure
|
||||
|
||||
Progressive disclosure is the core design principle that makes Agent Skills flexible and scalable. Like a well-organized manual:
|
||||
|
||||
1. **At startup:** Only skill names and descriptions load into context
|
||||
2. **When triggered:** Full SKILL.md content loads when the skill is relevant
|
||||
3. **On-demand:** Additional referenced files load during execution
|
||||
|
||||
This means you can have many skills available without bloating every session's context window.
|
||||
|
||||
### Skills vs. Slash Commands
|
||||
|
||||
| Aspect | Slash Commands | Agent Skills |
|
||||
|--------|----------------|--------------|
|
||||
| Invocation | User-invoked (`/command`) | Model-invoked (automatic) |
|
||||
| Complexity | Simple prompts, single file | Multiple files + scripts |
|
||||
| Use case | Quick shortcuts | Comprehensive workflows |
|
||||
| Discovery | Listed in `/help` | Based on task context |
|
||||
|
||||
### Best Practices for Building Skills
|
||||
|
||||
- **Start with evaluation:** Identify specific gaps in your agents' capabilities by running them on representative tasks and observing where they struggle
|
||||
- **Structure for scale:** When the SKILL.md file becomes unwieldy, split its content into separate files and reference them
|
||||
- **Think from Claude's perspective:** Pay special attention to the name and description—Claude uses these when deciding whether to trigger the skill
|
||||
- **Iterate with Claude:** Ask Claude to capture its successful approaches into reusable context within a skill
|
||||
|
||||
---
|
||||
|
||||
## Part 5: Hooks
|
||||
|
||||
Hooks provide deterministic control over Claude Code's behavior through shell commands that execute at specific lifecycle points. While CLAUDE.md offers "should-do" suggestions, **hooks enforce "must-do" rules**.
|
||||
|
||||
### Hook Events
|
||||
|
||||
| Event | Trigger | Can Block? |
|
||||
|-------|---------|------------|
|
||||
| `SessionStart` | Session begins | No |
|
||||
| `SessionEnd` | Session ends | No |
|
||||
| `UserPromptSubmit` | User submits prompt | Yes |
|
||||
| `PreToolUse` | Before tool execution | Yes |
|
||||
| `PostToolUse` | After tool completion | No |
|
||||
| `Stop` | Claude completes response | No |
|
||||
| `Notification` | Claude needs user input | No |
|
||||
| `PreCompact` | Before context compaction | No |
|
||||
| `PermissionRequest` | When permission dialog shown | No |
|
||||
| `SubagentStop` | When subagent tasks complete | No |
|
||||
|
||||
### Configuration Structure
|
||||
|
||||
Configure hooks in settings files (`~/.claude/settings.json`, `.claude/settings.json`, or `.claude/settings.local.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "if echo \"$CLAUDE_FILE_PATHS\" | grep -q '\\.py$'; then black \"$CLAUDE_FILE_PATHS\"; fi"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Matcher Syntax
|
||||
|
||||
- **Exact match:** `"Write"` matches only Write tool
|
||||
- **Regex:** `"Edit|Write"` matches either tool
|
||||
- **Wildcard:** `"*"` or `""` matches everything
|
||||
- **File patterns:** `"Write(*.py)"` matches Python file writes
|
||||
|
||||
### Exit Codes
|
||||
|
||||
- **Exit 0:** Success (stdout shown in transcript mode)
|
||||
- **Exit 2:** Blocking error (stderr fed back to Claude for correction)
|
||||
- **Other codes:** Non-blocking error (stderr shown to user)
|
||||
|
||||
### Practical Hook Examples
|
||||
|
||||
**Auto-format TypeScript after edits:**
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [{
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "jq -r '.tool_input.file_path' | { read file_path; if echo \"$file_path\" | grep -q '\\.ts$'; then npx prettier --write \"$file_path\"; fi; }"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Desktop notification when Claude finishes (macOS):**
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"Stop": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "osascript -e 'display notification \"Claude has finished!\" with title \"✅ Claude Done\" sound name \"Glass\"'"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Block dangerous commands:**
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [{
|
||||
"matcher": "Bash",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "if [[ \"$CLAUDE_TOOL_INPUT\" == *\"rm -rf\"* ]]; then echo 'Blocked!' && exit 2; fi"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hook Strategies
|
||||
|
||||
- **Block-at-Submit Hooks:** Primary strategy—check state at commit time, forcing Claude into a "test-and-fix" loop until the build is green
|
||||
- **Hint Hooks:** Non-blocking hooks that provide "fire-and-forget" feedback for suboptimal behavior
|
||||
|
||||
> 💡 **Best Practice:** Do NOT use "block-at-write" hooks. Blocking an agent mid-plan confuses it. Let the agent finish its work, then check the final result at commit time.
|
||||
|
||||
### Security Best Practices for Hooks
|
||||
|
||||
- Always quote shell variables: `"$VAR"` not `$VAR`
|
||||
- Validate and sanitize all inputs
|
||||
- Block path traversal by checking for `..`
|
||||
- Use absolute paths with `$CLAUDE_PROJECT_DIR`
|
||||
- Default timeout: 60 seconds per command
|
||||
|
||||
---
|
||||
|
||||
## Part 6: Best Practices
|
||||
|
||||
### The Explore-Plan-Code-Commit Workflow
|
||||
|
||||
This four-phase workflow consistently produces better results than immediate coding:
|
||||
|
||||
1. **Explore:** Ask Claude to read relevant files, images, or URLs—explicitly say "don't write code yet"
|
||||
2. **Plan:** Request a plan using thinking keywords ("think hard about the approach")
|
||||
3. **Document:** Have Claude create a plan document or GitHub issue as a checkpoint
|
||||
4. **Execute:** Implement the solution, then commit and create a PR
|
||||
|
||||
### Test-Driven Development with Claude
|
||||
|
||||
TDD aligns perfectly with Claude Code's verification-oriented nature:
|
||||
|
||||
1. Ask Claude to write tests based on expected input/output pairs
|
||||
2. Have Claude run tests and confirm they fail (no implementation yet)
|
||||
3. Commit the tests
|
||||
4. Ask Claude to write code that passes tests without modifying them
|
||||
5. Use subagents to verify implementation isn't overfitting
|
||||
6. Commit the working code
|
||||
|
||||
### Prompting Strategies
|
||||
|
||||
Specificity dramatically improves success rates:
|
||||
|
||||
| Ineffective | Effective |
|
||||
|-------------|-----------|
|
||||
| "add tests for foo.py" | "Write a new test case for foo.py, covering the edge case where the user is logged out. Avoid mocks." |
|
||||
| "why is this API weird?" | "Look through ExecutionFactory's git history and summarize how its API evolved" |
|
||||
| "add a calendar widget" | "Look at HotDogWidget.php for our widget pattern. Follow it to implement a calendar widget with month selection and pagination." |
|
||||
|
||||
#### Key Prompting Principles
|
||||
|
||||
- Give all context—Claude can't read your mind
|
||||
- Mention edge cases explicitly
|
||||
- Reference similar patterns in the codebase
|
||||
- Provide concrete examples instead of abstract descriptions
|
||||
- Break large tasks into smaller, verifiable chunks
|
||||
- Encourage Claude to ask clarifying questions during planning
|
||||
|
||||
### Context Management
|
||||
|
||||
The 200k token context window fills quickly. Monitor with `/context` and manage proactively:
|
||||
|
||||
- **Use `/clear` aggressively** between unrelated tasks
|
||||
- **Avoid `/compact`** when possible—auto-compaction is opaque and error-prone
|
||||
- **Document and clear** for complex tasks: dump progress to a `.md` file, `/clear`, then resume by reading the file
|
||||
- Fresh monorepo sessions start at ~20k tokens baseline
|
||||
|
||||
### Common Pitfalls to Avoid
|
||||
|
||||
1. **Not using /clear enough:** Context pollution causes unpredictable behavior
|
||||
2. **Treating Claude like autocomplete:** Real power comes from planning first
|
||||
3. **Vague prompts:** Specificity dramatically improves success rate
|
||||
4. **Massive one-shot tasks:** Break into smaller, verifiable chunks
|
||||
5. **Not giving visual context:** Screenshots improve UI work significantly
|
||||
6. **Ignoring the escape key:** Course-correct actively rather than letting Claude go down rabbit holes
|
||||
7. **Not staging git changes:** Stage early and often as checkpoints
|
||||
8. **Complex custom slash commands:** Keep them as simple shortcuts, not replacements for good CLAUDE.md
|
||||
|
||||
### Configuration for Claude 4.x Models
|
||||
|
||||
Claude 4.x models follow instructions more precisely but require explicit requests for "above and beyond" behavior. Add these prompts to CLAUDE.md:
|
||||
|
||||
**For proactive action:**
|
||||
|
||||
```markdown
|
||||
By default, implement changes rather than only suggesting them. If intent is unclear, infer the most useful likely action and proceed, using tools to discover missing details instead of guessing.
|
||||
```
|
||||
|
||||
**To prevent over-engineering (especially for Opus 4.5):**
|
||||
|
||||
```markdown
|
||||
Avoid over-engineering. Only make changes directly requested or clearly necessary. Don't add features, refactor code, or make "improvements" beyond what was asked. A bug fix doesn't need surrounding code cleaned up.
|
||||
```
|
||||
|
||||
**To minimize hallucinations:**
|
||||
|
||||
```markdown
|
||||
Never speculate about code you have not opened. If the user references a specific file, you MUST read the file before answering. Investigate and read relevant files BEFORE answering questions about the codebase.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 7: Context Engineering
|
||||
|
||||
Context engineering is the natural progression of prompt engineering. While prompt engineering focuses on writing effective prompts, context engineering manages the entire context state including system instructions, tools, external data, and message history.
|
||||
|
||||
### Why Context Engineering Matters
|
||||
|
||||
LLMs, like humans, lose focus at a certain point. This phenomenon is called **context rot**: as the number of tokens increases, the model's ability to accurately recall information decreases. Context must be treated as a finite resource with diminishing marginal returns.
|
||||
|
||||
### The Guiding Principle
|
||||
|
||||
> **Find the smallest possible set of high-signal tokens that maximize the likelihood of your desired outcome.**
|
||||
|
||||
### System Prompts: The Goldilocks Zone
|
||||
|
||||
System prompts should be extremely clear and use simple, direct language at the right "altitude":
|
||||
|
||||
- **Too prescriptive:** Hardcoding complex if-else logic creates brittle agents
|
||||
- **Too vague:** High-level guidance fails to give concrete signals
|
||||
- **Just right:** Specific enough to guide behavior, flexible enough to provide strong heuristics
|
||||
|
||||
### Just-In-Time Context Retrieval
|
||||
|
||||
Rather than pre-processing all relevant data up front, agents can maintain lightweight identifiers (file paths, queries, links) and use these references to dynamically load data at runtime.
|
||||
|
||||
This approach mirrors human cognition: we don't memorize entire corpuses, but use external organization systems like file systems, inboxes, and bookmarks to retrieve relevant information on demand.
|
||||
|
||||
Claude Code uses this hybrid model: CLAUDE.md files are naively dropped into context up front, while primitives like `glob` and `grep` allow it to navigate its environment and retrieve files just-in-time.
|
||||
|
||||
### Techniques for Long-Horizon Tasks
|
||||
|
||||
#### Compaction
|
||||
|
||||
Take a conversation nearing the context window limit, summarize its contents, and reinitiate with the summary. The art of compaction lies in selecting what to keep vs. discard:
|
||||
|
||||
- Start by maximizing recall to capture all relevant information
|
||||
- Then iterate to improve precision by eliminating superfluous content
|
||||
- One safe form: clearing tool calls and results deep in message history
|
||||
|
||||
#### Structured Note-Taking (Agentic Memory)
|
||||
|
||||
Regularly write notes persisted outside the context window that get pulled back in when needed:
|
||||
|
||||
- Like Claude Code creating a to-do list
|
||||
- Or your custom agent maintaining a `NOTES.md` file
|
||||
- Enables tracking progress across complex tasks
|
||||
|
||||
#### Sub-Agent Architectures
|
||||
|
||||
Specialized sub-agents handle focused tasks with clean context windows:
|
||||
|
||||
- Each subagent might use tens of thousands of tokens exploring
|
||||
- But returns only a condensed summary (1,000-2,000 tokens)
|
||||
- The lead agent focuses on synthesizing results
|
||||
- Clear separation of concerns keeps the main context clean
|
||||
|
||||
---
|
||||
|
||||
## Part 8: Advanced Features
|
||||
|
||||
### Code Execution with MCP
|
||||
|
||||
Anthropic's code execution with MCP pattern restructures how agents interact with tools. Instead of loading all tool definitions upfront, agents write code to interact with MCP servers, achieving up to **98.7% reduction in token consumption** (from 150,000 to 2,000 tokens).
|
||||
|
||||
#### How It Works
|
||||
|
||||
Present MCP servers as code APIs in a filesystem structure:
|
||||
|
||||
```
|
||||
servers
|
||||
├── google-drive
|
||||
│ ├── getDocument.ts
|
||||
│ └── index.ts
|
||||
├── salesforce
|
||||
│ ├── updateRecord.ts
|
||||
│ └── index.ts
|
||||
└── ... (other servers)
|
||||
```
|
||||
|
||||
The agent discovers tools by exploring the filesystem, loading only the definitions it needs for the current task.
|
||||
|
||||
#### Benefits
|
||||
|
||||
- **Progressive disclosure:** Load tools on-demand rather than all up-front
|
||||
- **Context-efficient data handling:** Filter and transform data in the execution environment before returning to the model
|
||||
- **Privacy-preserving operations:** Intermediate results stay in the execution environment by default
|
||||
- **State persistence and skills:** Save working code as reusable functions in a `./skills/` directory
|
||||
|
||||
### Claude Code GitHub Action
|
||||
|
||||
The GitHub Action runs Claude Code in a GHA container. You control the entire container and environment, giving you more access to data and stronger sandboxing and audit controls than any other product provides.
|
||||
|
||||
**Use cases:**
|
||||
|
||||
- Build custom "PR-from-anywhere" tooling triggered from Slack, Jira, or CloudWatch alerts
|
||||
- Review GHA logs for common mistakes to create a data-driven improvement flywheel
|
||||
- Supports all advanced features including Hooks and MCP
|
||||
|
||||
**Example meta-improvement loop:**
|
||||
|
||||
```bash
|
||||
$ query-claude-gha-logs --since 5d | claude -p "see what the other claudes were getting stuck on and fix it, then put up a PR"
|
||||
```
|
||||
|
||||
### Sandboxing Features
|
||||
|
||||
Claude Code includes native sandboxing with filesystem and network isolation:
|
||||
|
||||
- **Filesystem isolation:** Claude can only access or modify specific directories, preventing prompt-injected Claude from modifying sensitive system files
|
||||
- **Network isolation:** Claude can only connect to approved servers, preventing exfiltration of sensitive information
|
||||
- **Activation:** `claude --sandbox`
|
||||
|
||||
Both isolation types are needed—without network isolation, a compromised agent could exfiltrate files; without filesystem isolation, it could escape the sandbox and gain network access.
|
||||
|
||||
### Claude Agent SDK
|
||||
|
||||
The SDK that powers Claude Code can power many other types of agents too. Use it for:
|
||||
|
||||
- **Massive parallel scripting:** Write bash scripts that call `claude -p "..."` in parallel for large-scale refactors
|
||||
- **Building internal chat tools:** Wrap complex processes in a simple chat interface for non-technical users
|
||||
- **Rapid agent prototyping:** Build and test agentic prototypes before committing to full deployment scaffolding
|
||||
|
||||
### Settings.json Configuration
|
||||
|
||||
Key configurations for advanced users:
|
||||
|
||||
- **HTTPS_PROXY/HTTP_PROXY:** For debugging and fine-grained network sandboxing
|
||||
- **MCP_TOOL_TIMEOUT/BASH_MAX_TIMEOUT_MS:** Increase for long, complex commands
|
||||
- **ANTHROPIC_API_KEY:** Use enterprise API keys for usage-based pricing
|
||||
- **permissions:** Self-audit the list of commands allowed to auto-run
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
Claude Code's power emerges from the thoughtful combination of its configuration systems:
|
||||
|
||||
- **CLAUDE.md** establishes context and preferences, shaping how Claude understands your project
|
||||
- **Hooks** enforce deterministic rules, ensuring consistent behavior regardless of how Claude interprets instructions
|
||||
- **Skills** provide modular capabilities that Claude invokes autonomously when appropriate
|
||||
- **Commands** offer user-triggered shortcuts for common workflows
|
||||
|
||||
The key insight from Anthropic's engineering team: **start simple, add complexity only when needed**. A well-crafted CLAUDE.md file often eliminates the need for elaborate hooks or custom skills. Use the explore-plan-code-commit workflow to prevent Claude from jumping straight to implementation. Clear context frequently. Be specific in your prompts.
|
||||
|
||||
Multi-agent workflows represent the frontier of Claude Code productivity, with orchestrator-worker patterns achieving **90% better results** than single-agent approaches on complex tasks. As you gain experience, experiment with git worktrees for parallel sessions, headless mode for automation, and the built-in Task() feature for spawning focused subagents.
|
||||
|
||||
The tool rewards investment in configuration. The secret isn't in the prompts—it's in the process. Plan first, think appropriately hard, collaborate actively, and teach Claude about your specific context through CLAUDE.md files.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- [Anthropic Engineering: Claude Code Best Practices](https://www.anthropic.com/engineering/claude-code-best-practices)
|
||||
- [Anthropic Engineering: Building Agents with the Claude Agent SDK](https://www.anthropic.com/engineering/building-agents-with-the-claude-agent-sdk)
|
||||
- [Anthropic Engineering: Equipping Agents with Agent Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)
|
||||
- [Anthropic Engineering: Effective Context Engineering for AI Agents](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents)
|
||||
- [Anthropic Engineering: Writing Effective Tools for Agents](https://www.anthropic.com/engineering/writing-tools-for-agents)
|
||||
- [Anthropic Engineering: Code Execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp)
|
||||
- [Anthropic Engineering: How We Built Our Multi-Agent Research System](https://www.anthropic.com/engineering/multi-agent-research-system)
|
||||
- [Claude Code Documentation](https://docs.anthropic.com/en/docs/claude-code/)
|
||||
- [Claude 4.x Prompting Best Practices](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-4-best-practices)
|
||||
- [How I Use Every Claude Code Feature - Shrivu Shankar](https://blog.sshh.io/p/how-i-use-every-claude-code-feature)
|
||||
- [Mastering the Vibe: Claude Code Best Practices - Dinanjana Gunaratne](https://dinanjana.medium.com/mastering-the-vibe-claude-code-best-practices-that-actually-work-823371daf64c)
|
||||
|
||||
---
|
||||
|
||||
*— End of Guide —*
|
||||
@@ -1,11 +1,11 @@
|
||||
# Library Reference Guide
|
||||
|
||||
> **Last Updated:** 2025-11-28
|
||||
> **Last Updated:** 2025-12-10
|
||||
> **Angular Version:** 20.3.6
|
||||
> **Nx Version:** 21.3.2
|
||||
> **Total Libraries:** 74
|
||||
> **Total Libraries:** 81
|
||||
|
||||
All 74 libraries in the monorepo have comprehensive README.md documentation located at `libs/[domain]/[layer]/[feature]/README.md`.
|
||||
All 81 libraries in the monorepo have comprehensive README.md documentation located at `libs/[domain]/[layer]/[feature]/README.md`.
|
||||
|
||||
**IMPORTANT: Always use the `docs-researcher` subagent** to retrieve and analyze library documentation. This keeps the main context clean and prevents pollution.
|
||||
|
||||
@@ -66,7 +66,7 @@ A comprehensive loyalty rewards catalog feature for Angular applications support
|
||||
|
||||
---
|
||||
|
||||
## Common Libraries (3 libraries)
|
||||
## Common Libraries (4 libraries)
|
||||
|
||||
### `@isa/common/data-access`
|
||||
A foundational data access library providing core utilities, error handling, RxJS operators, response models, and advanced batching infrastructure for Angular applications.
|
||||
@@ -83,6 +83,11 @@ A comprehensive print management library for Angular applications providing prin
|
||||
|
||||
**Location:** `libs/common/print/`
|
||||
|
||||
### `@isa/common/title-management`
|
||||
Reusable title management patterns for Angular applications with reactive updates and tab integration.
|
||||
|
||||
**Location:** `libs/common/title-management/`
|
||||
|
||||
---
|
||||
|
||||
## Core Libraries (6 libraries)
|
||||
@@ -97,16 +102,16 @@ A lightweight, type-safe configuration management system for Angular application
|
||||
|
||||
**Location:** `libs/core/config/`
|
||||
|
||||
### `@isa/core/connectivity`
|
||||
**Type:** Util Library
|
||||
|
||||
**Location:** `libs/core/connectivity/`
|
||||
|
||||
### `@isa/core/logging`
|
||||
A structured, high-performance logging library for Angular applications with hierarchical context support and flexible sink architecture.
|
||||
|
||||
**Location:** `libs/core/logging/`
|
||||
|
||||
### `@isa/core/navigation`
|
||||
A reusable Angular library providing **context preservation** for multi-step navigation flows with automatic tab-scoped storage.
|
||||
|
||||
**Location:** `libs/core/navigation/`
|
||||
|
||||
### `@isa/core/storage`
|
||||
A powerful, type-safe storage library for Angular applications built on top of NgRx Signals. This library provides seamless integration between NgRx Signal Stores and various storage backends including localStorage, sessionStorage, IndexedDB, and server-side user state.
|
||||
|
||||
@@ -425,6 +430,40 @@ A lightweight Zod utility library for safe parsing with automatic fallback to or
|
||||
|
||||
---
|
||||
|
||||
## Shell Domain (6 libraries)
|
||||
|
||||
### `@isa/shell/common`
|
||||
**Type:** Util Library
|
||||
|
||||
**Location:** `libs/shell/common/`
|
||||
|
||||
### `@isa/shell/header`
|
||||
**Type:** Feature Library
|
||||
|
||||
**Location:** `libs/shell/header/`
|
||||
|
||||
### `@isa/shell/layout`
|
||||
**Type:** Feature Library
|
||||
|
||||
**Location:** `libs/shell/layout/`
|
||||
|
||||
### `@isa/shell/navigation`
|
||||
Collapsible navigation menu components for the application shell sidebar.
|
||||
|
||||
**Location:** `libs/shell/navigation/`
|
||||
|
||||
### `@isa/shell/notifications`
|
||||
**Type:** Feature Library
|
||||
|
||||
**Location:** `libs/shell/notifications/`
|
||||
|
||||
### `@isa/shell/tabs`
|
||||
UI components for displaying and managing browser-like tabs in the application shell.
|
||||
|
||||
**Location:** `libs/shell/tabs/`
|
||||
|
||||
---
|
||||
|
||||
## How to Use This Guide
|
||||
|
||||
1. **Quick Lookup**: Use this guide to find the purpose of any library in the monorepo
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Provides role-based authorization utilities for the ISA Frontend application.
|
||||
*/
|
||||
|
||||
export { AuthService } from './lib/auth.service';
|
||||
export { RoleService } from './lib/role.service';
|
||||
export { IfRoleDirective } from './lib/if-role.directive';
|
||||
export { TokenProvider, TOKEN_PROVIDER, parseJwt } from './lib/token-provider';
|
||||
|
||||
14
libs/core/auth/src/lib/auth.service.ts
Normal file
14
libs/core/auth/src/lib/auth.service.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthService {
|
||||
#logger = logger({ service: 'AuthService' });
|
||||
#oAuthService = inject(OAuthService);
|
||||
|
||||
logout(): void {
|
||||
this.#logger.info('User logging out');
|
||||
this.#oAuthService.logOut();
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ import { Role } from './role';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RoleService {
|
||||
private readonly _log = logger({ service: 'RoleService' });
|
||||
private readonly _tokenProvider = inject(TOKEN_PROVIDER);
|
||||
#logger = logger({ service: 'RoleService' });
|
||||
#tokenProvider = inject(TOKEN_PROVIDER);
|
||||
|
||||
/**
|
||||
* Check if the authenticated user has specific role(s)
|
||||
@@ -45,10 +45,10 @@ export class RoleService {
|
||||
const roles = coerceArray(role);
|
||||
|
||||
try {
|
||||
const userRoles = this._tokenProvider.getClaimByKey('role');
|
||||
const userRoles = this.#tokenProvider.getClaimByKey('role');
|
||||
|
||||
if (!userRoles) {
|
||||
this._log.debug('No roles found in token claims');
|
||||
this.#logger.debug('No roles found in token claims');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -57,14 +57,14 @@ export class RoleService {
|
||||
|
||||
const hasAllRoles = roles.every((r) => userRolesArray.includes(r));
|
||||
|
||||
this._log.debug(`Role check: ${roles.join(', ')} => ${hasAllRoles}`, () => ({
|
||||
this.#logger.debug(`Role check: ${roles.join(', ')} => ${hasAllRoles}`, () => ({
|
||||
requiredRoles: roles,
|
||||
userRoles: userRolesArray,
|
||||
}));
|
||||
|
||||
return hasAllRoles;
|
||||
} catch (error) {
|
||||
this._log.error('Error checking roles', error as Error, () => ({ requiredRoles: roles }));
|
||||
this.#logger.error('Error checking roles', error as Error, () => ({ requiredRoles: roles }));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
95
libs/core/connectivity/README.md
Normal file
95
libs/core/connectivity/README.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# @isa/core/connectivity
|
||||
|
||||
> **Type:** Util Library
|
||||
> **Domain:** Core
|
||||
> **Path:** `libs/core/connectivity`
|
||||
|
||||
## Overview
|
||||
|
||||
Network connectivity monitoring service that tracks browser online/offline status using the Navigator API.
|
||||
|
||||
## Features
|
||||
|
||||
- Real-time network status detection
|
||||
- Observable and Signal-based APIs
|
||||
- Immediate emission on subscription
|
||||
- Shared replay for multiple subscribers
|
||||
|
||||
## Installation
|
||||
|
||||
```typescript
|
||||
import {
|
||||
NetworkStatusService,
|
||||
NetworkStatus,
|
||||
injectNetworkStatus$,
|
||||
injectNetworkStatus
|
||||
} from '@isa/core/connectivity';
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### As Observable
|
||||
|
||||
```typescript
|
||||
@Component({...})
|
||||
export class MyComponent {
|
||||
networkService = inject(NetworkStatusService);
|
||||
|
||||
constructor() {
|
||||
this.networkService.status$.subscribe(status => {
|
||||
console.log('Network status:', status); // 'online' | 'offline'
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Injection Functions
|
||||
|
||||
```typescript
|
||||
@Component({...})
|
||||
export class MyComponent {
|
||||
// As Observable
|
||||
networkStatus$ = injectNetworkStatus$();
|
||||
|
||||
// As Signal
|
||||
networkStatus = injectNetworkStatus();
|
||||
|
||||
isOffline = computed(() => this.networkStatus() === 'offline');
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### NetworkStatusService
|
||||
|
||||
| Member | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `status$` | `Observable<NetworkStatus>` | Emits network status changes |
|
||||
|
||||
### Types
|
||||
|
||||
```typescript
|
||||
type NetworkStatus = 'online' | 'offline';
|
||||
```
|
||||
|
||||
### Injection Functions
|
||||
|
||||
| Function | Returns | Description |
|
||||
|----------|---------|-------------|
|
||||
| `injectNetworkStatus$()` | `Observable<NetworkStatus>` | Observable of network status |
|
||||
| `injectNetworkStatus()` | `Signal<NetworkStatus \| undefined>` | Signal of network status |
|
||||
|
||||
## Dependencies
|
||||
|
||||
**External:**
|
||||
- `rxjs` - Observable streams
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npx nx test core-connectivity
|
||||
```
|
||||
|
||||
## Related Libraries
|
||||
|
||||
- [`@isa/shell/layout`](../../shell/layout) - Uses for network status banner
|
||||
34
libs/core/connectivity/eslint.config.cjs
Normal file
34
libs/core/connectivity/eslint.config.cjs
Normal file
@@ -0,0 +1,34 @@
|
||||
const nx = require('@nx/eslint-plugin');
|
||||
const baseConfig = require('../../../eslint.config.js');
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'core',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'core',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
20
libs/core/connectivity/project.json
Normal file
20
libs/core/connectivity/project.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "core-connectivity",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/core/connectivity/src",
|
||||
"prefix": "core",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:core", "type:core"],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"options": {
|
||||
"reportsDirectory": "../../../coverage/libs/core/connectivity"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
6
libs/core/connectivity/src/index.ts
Normal file
6
libs/core/connectivity/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
NetworkStatusService,
|
||||
NetworkStatus,
|
||||
injectNetworkStatus$,
|
||||
injectNetworkStatus,
|
||||
} from './lib/network-status.service';
|
||||
137
libs/core/connectivity/src/lib/network-status.service.spec.ts
Normal file
137
libs/core/connectivity/src/lib/network-status.service.spec.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { firstValueFrom, take, toArray } from 'rxjs';
|
||||
import {
|
||||
NetworkStatusService,
|
||||
injectNetworkStatus$,
|
||||
injectNetworkStatus,
|
||||
} from './network-status.service';
|
||||
|
||||
describe('NetworkStatusService', () => {
|
||||
let service: NetworkStatusService;
|
||||
let originalNavigatorOnLine: boolean;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(NetworkStatusService);
|
||||
originalNavigatorOnLine = navigator.onLine;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(navigator, 'onLine', {
|
||||
value: originalNavigatorOnLine,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
const setNavigatorOnLine = (value: boolean) => {
|
||||
Object.defineProperty(navigator, 'onLine', {
|
||||
value,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
};
|
||||
|
||||
describe('status$', () => {
|
||||
it('should emit initial status based on navigator.onLine', async () => {
|
||||
setNavigatorOnLine(true);
|
||||
const newService = TestBed.inject(NetworkStatusService);
|
||||
|
||||
const status = await firstValueFrom(newService.status$);
|
||||
|
||||
expect(status).toBe('online');
|
||||
});
|
||||
|
||||
it('should emit "offline" when navigator.onLine is false', async () => {
|
||||
setNavigatorOnLine(false);
|
||||
|
||||
const status = await firstValueFrom(service.status$);
|
||||
|
||||
expect(status).toBe('offline');
|
||||
});
|
||||
|
||||
it('should emit "online" when online event is dispatched', async () => {
|
||||
setNavigatorOnLine(false);
|
||||
|
||||
const statusPromise = firstValueFrom(service.status$.pipe(take(2), toArray()));
|
||||
|
||||
setNavigatorOnLine(true);
|
||||
window.dispatchEvent(new Event('online'));
|
||||
|
||||
const statuses = await statusPromise;
|
||||
|
||||
expect(statuses).toContain('online');
|
||||
});
|
||||
|
||||
it('should emit "offline" when offline event is dispatched', async () => {
|
||||
setNavigatorOnLine(true);
|
||||
|
||||
const statusPromise = firstValueFrom(service.status$.pipe(take(2), toArray()));
|
||||
|
||||
setNavigatorOnLine(false);
|
||||
window.dispatchEvent(new Event('offline'));
|
||||
|
||||
const statuses = await statusPromise;
|
||||
|
||||
expect(statuses).toContain('offline');
|
||||
});
|
||||
|
||||
it('should share the same observable instance (shareReplay)', async () => {
|
||||
const subscription1Values: string[] = [];
|
||||
const subscription2Values: string[] = [];
|
||||
|
||||
const sub1 = service.status$.subscribe((v) => subscription1Values.push(v));
|
||||
const sub2 = service.status$.subscribe((v) => subscription2Values.push(v));
|
||||
|
||||
// Both subscribers should receive the same initial value
|
||||
expect(subscription1Values.length).toBeGreaterThan(0);
|
||||
expect(subscription2Values.length).toBeGreaterThan(0);
|
||||
expect(subscription1Values[0]).toBe(subscription2Values[0]);
|
||||
|
||||
sub1.unsubscribe();
|
||||
sub2.unsubscribe();
|
||||
});
|
||||
|
||||
it('should replay last value to new subscribers', async () => {
|
||||
// First subscriber gets the value
|
||||
const firstValue = await firstValueFrom(service.status$);
|
||||
|
||||
// Second subscriber should get the same replayed value
|
||||
const secondValue = await firstValueFrom(service.status$);
|
||||
|
||||
expect(firstValue).toBe(secondValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectNetworkStatus$', () => {
|
||||
it('should return status$ observable from service', () => {
|
||||
TestBed.runInInjectionContext(() => {
|
||||
const status$ = injectNetworkStatus$();
|
||||
|
||||
expect(status$).toBe(service.status$);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectNetworkStatus', () => {
|
||||
it('should return a signal with current network status', () => {
|
||||
setNavigatorOnLine(true);
|
||||
|
||||
TestBed.runInInjectionContext(() => {
|
||||
const statusSignal = injectNetworkStatus();
|
||||
|
||||
expect(statusSignal()).toBe('online');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a signal that reflects offline status', () => {
|
||||
setNavigatorOnLine(false);
|
||||
|
||||
TestBed.runInInjectionContext(() => {
|
||||
const statusSignal = injectNetworkStatus();
|
||||
|
||||
expect(statusSignal()).toBe('offline');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
63
libs/core/connectivity/src/lib/network-status.service.ts
Normal file
63
libs/core/connectivity/src/lib/network-status.service.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import {
|
||||
map,
|
||||
Observable,
|
||||
fromEvent,
|
||||
merge,
|
||||
startWith,
|
||||
shareReplay,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
|
||||
/** Network connectivity status values. */
|
||||
export type NetworkStatus = 'online' | 'offline';
|
||||
|
||||
/**
|
||||
* Service for monitoring browser network connectivity status.
|
||||
*
|
||||
* Listens to browser online/offline events and provides a reactive
|
||||
* observable that emits the current network status. The observable
|
||||
* is shared and replays the latest value to new subscribers.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* readonly networkService = inject(NetworkStatusService);
|
||||
*
|
||||
* // Subscribe to status changes
|
||||
* this.networkService.status$.subscribe(status => {
|
||||
* console.log('Network status:', status);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NetworkStatusService {
|
||||
#logger = logger({ service: 'NetworkStatusService' });
|
||||
|
||||
/**
|
||||
* Observable that emits current network status ('online' | 'offline').
|
||||
* Emits immediately on subscription with current state.
|
||||
*/
|
||||
readonly status$: Observable<NetworkStatus> = merge(
|
||||
fromEvent(window, 'online'),
|
||||
fromEvent(window, 'offline'),
|
||||
).pipe(
|
||||
startWith(null), // emit immediately
|
||||
map((): NetworkStatus => (navigator.onLine ? 'online' : 'offline')),
|
||||
tap((status) => this.#logger.debug('Network status changed', () => ({ status }))),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection function to get the network status observable.
|
||||
* @returns Observable of network status
|
||||
*/
|
||||
export const injectNetworkStatus$ = () => inject(NetworkStatusService).status$;
|
||||
|
||||
/**
|
||||
* Injection function to get network status as a signal.
|
||||
* @returns Signal of network status (undefined initially until first emission)
|
||||
*/
|
||||
export const injectNetworkStatus = () => toSignal(injectNetworkStatus$());
|
||||
13
libs/core/connectivity/src/test-setup.ts
Normal file
13
libs/core/connectivity/src/test-setup.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import '@angular/compiler';
|
||||
import '@analogjs/vitest-angular/setup-zone';
|
||||
|
||||
import {
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting,
|
||||
} from '@angular/platform-browser/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting(),
|
||||
);
|
||||
30
libs/core/connectivity/tsconfig.json
Normal file
30
libs/core/connectivity/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
libs/core/connectivity/tsconfig.lib.json
Normal file
27
libs/core/connectivity/tsconfig.lib.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
29
libs/core/connectivity/tsconfig.spec.json
Normal file
29
libs/core/connectivity/tsconfig.spec.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"files": ["src/test-setup.ts"]
|
||||
}
|
||||
29
libs/core/connectivity/vite.config.mts
Normal file
29
libs/core/connectivity/vite.config.mts
Normal file
@@ -0,0 +1,29 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import angular from '@analogjs/vite-plugin-angular';
|
||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default
|
||||
// @ts-expect-error - Vitest reporter tuple types have complex inference issues
|
||||
defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../../node_modules/.vite/libs/core/connectivity',
|
||||
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
reporters: [
|
||||
'default',
|
||||
['junit', { outputFile: '../../../testresults/junit-core-connectivity.xml' }],
|
||||
],
|
||||
coverage: {
|
||||
reportsDirectory: '../../../coverage/libs/core/connectivity',
|
||||
provider: 'v8' as const,
|
||||
reporter: ['text', 'cobertura'],
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -1,6 +1,18 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { computed, inject, Signal } from '@angular/core';
|
||||
import { Params, QueryParamsHandling } from '@angular/router';
|
||||
import { Config } from '@isa/core/config';
|
||||
import { z } from 'zod';
|
||||
import { TabService } from './tab';
|
||||
|
||||
const ReservedProcessIdsSchema = z.object({
|
||||
goodsOut: z.number(),
|
||||
goodsIn: z.number(),
|
||||
taskCalendar: z.number(),
|
||||
packageInspection: z.number(),
|
||||
assortment: z.number(),
|
||||
pickupShelf: z.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Injects the current activated tab as a signal.
|
||||
* @returns A signal that emits the current activated tab or null if no tab is activated.
|
||||
@@ -16,3 +28,226 @@ export function injectTab() {
|
||||
export function injectTabId() {
|
||||
return inject(TabService).activatedTabId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a tab ID that is safe to use for navigation.
|
||||
* If the current tab ID is a reserved process ID, generates a new ID using Date.now().
|
||||
*/
|
||||
function getNavigableTabId(
|
||||
activeTabId: number | null | undefined,
|
||||
reservedIds: Set<number>,
|
||||
): number {
|
||||
if (
|
||||
activeTabId === undefined ||
|
||||
activeTabId === null ||
|
||||
reservedIds.has(activeTabId)
|
||||
) {
|
||||
return Date.now();
|
||||
}
|
||||
return activeTabId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the reserved process IDs from config.
|
||||
*/
|
||||
function injectReservedProcessIds(): Set<number> {
|
||||
const config = inject(Config);
|
||||
const processIds = config.get('process.ids', ReservedProcessIdsSchema);
|
||||
return new Set(Object.values(processIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for configuring tab route navigation.
|
||||
*/
|
||||
export type TabRouteOptions = {
|
||||
/** Query parameters to append to the route URL. */
|
||||
queryParams?: Params;
|
||||
/** Strategy for handling existing query parameters when navigating. */
|
||||
queryParamsHandling?: QueryParamsHandling;
|
||||
};
|
||||
|
||||
/**
|
||||
* Auxiliary route outlets configuration.
|
||||
* @example
|
||||
* { outlets: { primary: ['filter'], side: ['search'] } }
|
||||
* { outlets: { primary: ['details', itemId], side: null } }
|
||||
*/
|
||||
export type AuxiliaryOutlets = {
|
||||
outlets: Record<string, (string | number)[] | string | null>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A path segment can be a string, number, or an auxiliary outlets object.
|
||||
*/
|
||||
export type PathSegment = string | number | AuxiliaryOutlets;
|
||||
|
||||
/**
|
||||
* A route configuration for tab-based navigation.
|
||||
* Contains the route path segments and optional query parameters.
|
||||
*/
|
||||
export type TabRoute = {
|
||||
/** Route path segments including the tab ID prefix. */
|
||||
route: PathSegment[];
|
||||
/** Query parameters to append to the route URL. */
|
||||
queryParams?: Params;
|
||||
/** Strategy for handling existing query parameters when navigating. */
|
||||
queryParamsHandling?: QueryParamsHandling;
|
||||
};
|
||||
|
||||
/**
|
||||
* A route configuration with an associated label for display purposes.
|
||||
* Useful for navigation menus and breadcrumbs.
|
||||
*/
|
||||
export type LabeledTabRoute = TabRoute & { label: string };
|
||||
|
||||
/**
|
||||
* Creates a computed signal that returns a route object with the current tab ID
|
||||
* or a new tab ID (using Date.now()) if no tab is active.
|
||||
*
|
||||
* @param pathSegments - The path segments to append after the tab ID (supports auxiliary outlets)
|
||||
* @param options - Optional query params and handling
|
||||
* @returns A computed signal with the route object
|
||||
*
|
||||
* @example
|
||||
* // Basic usage
|
||||
* const route = injectTabRoute(['product']);
|
||||
*
|
||||
* // With query params
|
||||
* const route = injectTabRoute(['product'], { queryParams: { view: 'grid' } });
|
||||
*
|
||||
* // With auxiliary outlets
|
||||
* const route = injectTabRoute(['product', { outlets: { primary: ['filter'], side: ['search'] } }]);
|
||||
*/
|
||||
export function injectTabRoute(
|
||||
pathSegments: PathSegment[],
|
||||
options?: TabRouteOptions,
|
||||
): Signal<TabRoute> {
|
||||
const activeTabId = injectTabId();
|
||||
const reservedIds = injectReservedProcessIds();
|
||||
|
||||
return computed(() => {
|
||||
const tabId = getNavigableTabId(activeTabId(), reservedIds);
|
||||
return {
|
||||
route: ['/', tabId, ...pathSegments],
|
||||
...(options?.queryParams && { queryParams: options.queryParams }),
|
||||
...(options?.queryParamsHandling && {
|
||||
queryParamsHandling: options.queryParamsHandling,
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a computed signal that returns a labeled route object with the current tab ID
|
||||
* or a new tab ID (using Date.now()) if no tab is active.
|
||||
*
|
||||
* @param label - The label for the route
|
||||
* @param pathSegments - The path segments to append after the tab ID (supports auxiliary outlets)
|
||||
* @param options - Optional query params and handling
|
||||
* @returns A computed signal with the labeled route object
|
||||
*
|
||||
* @example
|
||||
* // Basic usage
|
||||
* const route = injectLabeledTabRoute('Products', ['product']);
|
||||
*
|
||||
* // With query params
|
||||
* const route = injectLabeledTabRoute('Products', ['product'], { queryParams: { view: 'grid' } });
|
||||
*
|
||||
* // With auxiliary outlets
|
||||
* const route = injectLabeledTabRoute('Products', ['product', { outlets: { primary: ['filter'], side: ['search'] } }]);
|
||||
*/
|
||||
export function injectLabeledTabRoute(
|
||||
label: string,
|
||||
pathSegments: PathSegment[],
|
||||
options?: TabRouteOptions,
|
||||
): Signal<LabeledTabRoute> {
|
||||
const activeTabId = injectTabId();
|
||||
const reservedIds = injectReservedProcessIds();
|
||||
|
||||
return computed(() => {
|
||||
const tabId = getNavigableTabId(activeTabId(), reservedIds);
|
||||
return {
|
||||
label,
|
||||
route: ['/', tabId, ...pathSegments],
|
||||
...(options?.queryParams && { queryParams: options.queryParams }),
|
||||
...(options?.queryParamsHandling && {
|
||||
queryParamsHandling: options.queryParamsHandling,
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a computed signal that returns a legacy route object with the current tab ID
|
||||
* or a new tab ID (using Date.now()) for routes under /kunde/:tabId/...
|
||||
*
|
||||
* @param pathSegments - The path segments to append after /kunde/:tabId (supports auxiliary outlets)
|
||||
* @param options - Optional query params and handling
|
||||
* @returns A computed signal with the route object
|
||||
*
|
||||
* @deprecated Use `injectTabRoute` instead. Legacy routes under /kunde/:tabId will be migrated to /:tabId.
|
||||
*
|
||||
* @example
|
||||
* // Creates route like /kunde/123456/product
|
||||
* const route = injectLegacyTabRoute(['product']);
|
||||
*
|
||||
* // With auxiliary outlets: /kunde/123456/product/(primary:filter//side:search)
|
||||
* const route = injectLegacyTabRoute(['product', { outlets: { primary: ['filter'], side: ['search'] } }]);
|
||||
*/
|
||||
export function injectLegacyTabRoute(
|
||||
pathSegments: PathSegment[],
|
||||
options?: TabRouteOptions,
|
||||
): Signal<TabRoute> {
|
||||
const activeTabId = injectTabId();
|
||||
const reservedIds = injectReservedProcessIds();
|
||||
|
||||
return computed(() => {
|
||||
const tabId = getNavigableTabId(activeTabId(), reservedIds);
|
||||
return {
|
||||
route: ['/kunde', tabId, ...pathSegments],
|
||||
...(options?.queryParams && { queryParams: options.queryParams }),
|
||||
...(options?.queryParamsHandling && {
|
||||
queryParamsHandling: options.queryParamsHandling,
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a computed signal that returns a labeled legacy route object with the current tab ID
|
||||
* or a new tab ID (using Date.now()) for routes under /kunde/:tabId/...
|
||||
*
|
||||
* @param label - The label for the route
|
||||
* @param pathSegments - The path segments to append after /kunde/:tabId (supports auxiliary outlets)
|
||||
* @param options - Optional query params and handling
|
||||
* @returns A computed signal with the labeled route object
|
||||
*
|
||||
* @deprecated Use `injectLabeledTabRoute` instead. Legacy routes under /kunde/:tabId will be migrated to /:tabId.
|
||||
*
|
||||
* @example
|
||||
* // Creates route like /kunde/123456/product
|
||||
* const route = injectLabeledLegacyTabRoute('Products', ['product']);
|
||||
*
|
||||
* // With auxiliary outlets: /kunde/123456/product/(primary:filter//side:search)
|
||||
* const route = injectLabeledLegacyTabRoute('Products', ['product', { outlets: { primary: ['filter'], side: ['search'] } }]);
|
||||
*/
|
||||
export function injectLabeledLegacyTabRoute(
|
||||
label: string,
|
||||
pathSegments: PathSegment[],
|
||||
options?: TabRouteOptions,
|
||||
): Signal<LabeledTabRoute> {
|
||||
const activeTabId = injectTabId();
|
||||
const reservedIds = injectReservedProcessIds();
|
||||
|
||||
return computed(() => {
|
||||
const tabId = getNavigableTabId(activeTabId(), reservedIds);
|
||||
return {
|
||||
label,
|
||||
route: ['/kunde', tabId, ...pathSegments],
|
||||
...(options?.queryParams && { queryParams: options.queryParams }),
|
||||
...(options?.queryParamsHandling && {
|
||||
queryParamsHandling: options.queryParamsHandling,
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@ngrx/signals';
|
||||
import {
|
||||
addEntity,
|
||||
removeAllEntities,
|
||||
removeEntity,
|
||||
updateEntity,
|
||||
withEntities,
|
||||
@@ -56,6 +57,15 @@ export const TabService = signalStore(
|
||||
}
|
||||
return store.entities().find((e) => e.id === activeTabId) ?? null;
|
||||
}),
|
||||
/**
|
||||
* Returns tabs sorted by activation time (most recently activated first).
|
||||
* Tabs without activatedAt are placed at the end.
|
||||
*/
|
||||
tabsByActivationOrder: computed<Tab[]>(() => {
|
||||
return [...store.entities()].sort(
|
||||
(a, b) => (b.activatedAt ?? 0) - (a.activatedAt ?? 0),
|
||||
);
|
||||
}),
|
||||
})),
|
||||
withMethods((store) => ({
|
||||
addTab(add: z.input<typeof AddTabSchema>) {
|
||||
@@ -139,13 +149,43 @@ export const TabService = signalStore(
|
||||
metadataKeys: Object.keys(metadata),
|
||||
}));
|
||||
},
|
||||
removeTab(id: number) {
|
||||
/**
|
||||
* Removes a tab by ID.
|
||||
* @returns The previously active tab if the removed tab was active, null otherwise.
|
||||
*/
|
||||
removeTab(id: number): Tab | null {
|
||||
const wasActive = store.activatedTabId() === id;
|
||||
|
||||
// Find the next tab to activate before removing
|
||||
let previousTab: Tab | null = null;
|
||||
if (wasActive) {
|
||||
previousTab =
|
||||
store
|
||||
.tabsByActivationOrder()
|
||||
.find((tab) => tab.id !== id && tab.activatedAt !== undefined) ??
|
||||
null;
|
||||
}
|
||||
|
||||
patchState(store, removeEntity(id));
|
||||
if (wasActive) {
|
||||
patchState(store, { activatedTabId: null });
|
||||
}
|
||||
store._logger.info('Tab removed', () => ({ tabId: id, wasActive }));
|
||||
|
||||
store._logger.info('Tab removed', () => ({
|
||||
tabId: id,
|
||||
wasActive,
|
||||
previousTabId: previousTab?.id ?? null,
|
||||
}));
|
||||
|
||||
return previousTab;
|
||||
},
|
||||
/**
|
||||
* Removes all tabs.
|
||||
*/
|
||||
removeAllTabs(): void {
|
||||
const tabCount = store.entities().length;
|
||||
patchState(store, removeAllEntities(), { activatedTabId: null });
|
||||
store._logger.info('All tabs removed', () => ({ count: tabCount }));
|
||||
},
|
||||
navigateToLocation(
|
||||
id: number,
|
||||
|
||||
File diff suppressed because one or more lines are too long
234
libs/shell/common/README.md
Normal file
234
libs/shell/common/README.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# shell-common
|
||||
|
||||
> **Type:** Util Library
|
||||
> **Domain:** Shell
|
||||
> **Path:** `libs/shell/common`
|
||||
|
||||
## Overview
|
||||
|
||||
Shared services and types for shell-domain components. Provides state management for navigation, font size, and notifications.
|
||||
|
||||
## Services
|
||||
|
||||
### NavigationService
|
||||
|
||||
Controls the navigation drawer open/closed state.
|
||||
|
||||
```typescript
|
||||
import { NavigationService } from '@isa/shell/common';
|
||||
|
||||
@Component({...})
|
||||
export class MyComponent {
|
||||
navigationService = inject(NavigationService);
|
||||
|
||||
// Read state (readonly signal)
|
||||
isOpen = this.navigationService.get;
|
||||
|
||||
// Toggle navigation
|
||||
toggleNav() {
|
||||
this.navigationService.toggle();
|
||||
}
|
||||
|
||||
// Set specific state
|
||||
closeNav() {
|
||||
this.navigationService.set(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**API:**
|
||||
- `get` - Readonly signal of navigation state (`boolean`)
|
||||
- `toggle()` - Toggles navigation open/closed
|
||||
- `set(state: boolean)` - Sets navigation state
|
||||
|
||||
### FontSizeService
|
||||
|
||||
Manages application-wide font size for accessibility.
|
||||
|
||||
```typescript
|
||||
import { FontSizeService, FontSize } from '@isa/shell/common';
|
||||
|
||||
@Component({...})
|
||||
export class MyComponent {
|
||||
fontSizeService = inject(FontSizeService);
|
||||
|
||||
// Read current size
|
||||
currentSize = this.fontSizeService.get;
|
||||
|
||||
// Get size in pixels
|
||||
currentPx = this.fontSizeService.getPx;
|
||||
|
||||
// Change font size
|
||||
setLarge() {
|
||||
this.fontSizeService.set('large');
|
||||
}
|
||||
|
||||
// Convert rem to px
|
||||
getPixels(rem: number) {
|
||||
return this.fontSizeService.remToPx(rem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**API:**
|
||||
- `get` - Readonly signal of current font size
|
||||
- `getPx` - Computed signal of font size in pixels
|
||||
- `set(size: FontSize)` - Sets font size
|
||||
- `remToPx(rem: number)` - Converts rem to pixels
|
||||
- `fontSizeEffect` - Effect that syncs font size to document
|
||||
|
||||
**Types:**
|
||||
```typescript
|
||||
type FontSize = 'small' | 'medium' | 'large';
|
||||
// Maps to: 14px | 16px | 18px
|
||||
```
|
||||
|
||||
### TabsCollapsedService
|
||||
|
||||
Controls the collapsed/expanded state of the shell tabs bar.
|
||||
|
||||
```typescript
|
||||
import { TabsCollapsedService } from '@isa/shell/common';
|
||||
|
||||
@Component({...})
|
||||
export class MyComponent {
|
||||
tabsCollapsed = inject(TabsCollapsedService);
|
||||
|
||||
// Read state (readonly signal)
|
||||
isCollapsed = this.tabsCollapsed.get;
|
||||
|
||||
// Toggle collapsed state
|
||||
toggleTabs() {
|
||||
this.tabsCollapsed.toggle();
|
||||
}
|
||||
|
||||
// Set specific state
|
||||
expandTabs() {
|
||||
this.tabsCollapsed.set(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**API:**
|
||||
- `get` - Readonly signal of collapsed state (`boolean`)
|
||||
- `toggle()` - Toggles collapsed/expanded state
|
||||
- `set(state: boolean)` - Sets collapsed state (no-op if already equal)
|
||||
|
||||
### NotificationsService
|
||||
|
||||
Manages application notifications with read status tracking.
|
||||
|
||||
```typescript
|
||||
import { NotificationsService, Notification } from '@isa/shell/common';
|
||||
|
||||
@Component({...})
|
||||
export class MyComponent {
|
||||
notificationsService = inject(NotificationsService);
|
||||
|
||||
// Read all notifications
|
||||
notifications = this.notificationsService.get;
|
||||
|
||||
// Get unread count
|
||||
unreadCount = this.notificationsService.unreadCount;
|
||||
|
||||
// Add notification
|
||||
notify() {
|
||||
this.notificationsService.add({
|
||||
id: 'unique-id',
|
||||
group: 'Orders',
|
||||
title: 'New Order',
|
||||
message: 'Order #123 received',
|
||||
timestamp: Date.now(),
|
||||
action: {
|
||||
type: 'navigate',
|
||||
label: 'View',
|
||||
target: 'internal',
|
||||
route: '/orders/123'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Mark as read
|
||||
markRead(id: string) {
|
||||
this.notificationsService.markAsRead(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**API:**
|
||||
- `get` - Readonly signal of all notifications
|
||||
- `unreadCount` - Computed signal of unread count
|
||||
- `add(notification: Notification)` - Adds notification
|
||||
- `remove(id: NotificationId)` - Removes notification
|
||||
- `clear()` - Removes all notifications
|
||||
- `markAsRead(id: NotificationId)` - Marks single notification as read
|
||||
- `markAllAsRead()` - Marks all notifications as read
|
||||
|
||||
## Types
|
||||
|
||||
### Notification
|
||||
|
||||
```typescript
|
||||
type Notification = {
|
||||
id: string | number;
|
||||
group: string;
|
||||
title: string;
|
||||
message: string;
|
||||
action: NotificationAction;
|
||||
markedAsRead?: number; // timestamp
|
||||
timestamp: number;
|
||||
};
|
||||
```
|
||||
|
||||
### NotificationAction
|
||||
|
||||
```typescript
|
||||
type NotificationAction =
|
||||
| NotificationActionNavigate
|
||||
| NotificationActionCallback;
|
||||
|
||||
type NotificationActionNavigate = {
|
||||
type: 'navigate';
|
||||
label: string;
|
||||
target: 'internal' | 'external';
|
||||
route: string;
|
||||
};
|
||||
|
||||
type NotificationActionCallback = {
|
||||
type: 'callback';
|
||||
label: string;
|
||||
callback: () => void;
|
||||
};
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```typescript
|
||||
import {
|
||||
NavigationService,
|
||||
FontSizeService,
|
||||
FontSize,
|
||||
TabsCollapsedService,
|
||||
NotificationsService,
|
||||
Notification
|
||||
} from '@isa/shell/common';
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Internal:**
|
||||
- `@isa/core/logging` - Logger factory
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npx nx test shell-common
|
||||
```
|
||||
|
||||
## Related Libraries
|
||||
|
||||
- [`@isa/shell/header`](../header) - Uses NavigationService, FontSizeService, NotificationsService
|
||||
- [`@isa/shell/layout`](../layout) - Uses NavigationService, FontSizeService, TabsCollabsedService
|
||||
- [`@isa/shell/tabs`](../tabs) - Uses TabsCollabsedService
|
||||
- [`@isa/shell/notifications`](../notifications) - Uses NotificationsService
|
||||
- [`@isa/shell/navigation`](../navigation) - Uses NavigationService
|
||||
34
libs/shell/common/eslint.config.cjs
Normal file
34
libs/shell/common/eslint.config.cjs
Normal file
@@ -0,0 +1,34 @@
|
||||
const nx = require('@nx/eslint-plugin');
|
||||
const baseConfig = require('../../../eslint.config.js');
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'shell',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'shell',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user