mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
23 Commits
1cc13eebe1
...
fix/5237-H
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba09cb2508 | ||
|
|
d9b653073b | ||
|
|
de3edaa0f9 | ||
|
|
964a6026a0 | ||
|
|
83ad5f526e | ||
|
|
ccc5285602 | ||
|
|
7200eaefbf | ||
|
|
39e56a275e | ||
|
|
6c41214d69 | ||
|
|
6e55b7b0da | ||
|
|
5711a75188 | ||
|
|
3696fb5b2d | ||
|
|
7e7721b222 | ||
|
|
14be1365bd | ||
|
|
d5324675ef | ||
|
|
f10338a48b | ||
|
|
aa57d27924 | ||
|
|
6cb9aea7d1 | ||
|
|
fdfb54a3a0 | ||
|
|
5f94549539 | ||
|
|
aee63711e4 | ||
|
|
ee9f030a99 | ||
|
|
7884e1af32 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -80,3 +80,6 @@ CLAUDE.md
|
||||
*.pyc
|
||||
.vite
|
||||
reports/
|
||||
|
||||
# Local iPad dev setup (proxy)
|
||||
/local-dev/
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { DevScanAdapter } from './dev.scan-adapter';
|
||||
import { NativeScanAdapter } from './native.scan-adapter';
|
||||
import { SCAN_ADAPTER } from './tokens';
|
||||
|
||||
/**
|
||||
* @deprecated Use '@isa/shared/scanner' instead.
|
||||
*/
|
||||
@NgModule({})
|
||||
export class ScanAdapterModule {
|
||||
static forRoot() {
|
||||
return {
|
||||
ngModule: ScanAdapterModule,
|
||||
providers: [
|
||||
{ provide: SCAN_ADAPTER, useClass: NativeScanAdapter, multi: true },
|
||||
{ provide: SCAN_ADAPTER, useClass: DevScanAdapter, multi: true },
|
||||
],
|
||||
// Use for testing:
|
||||
// providers: [{ provide: SCAN_ADAPTER, useClass: dev ? DevScanAdapter : NativeScanAdapter, multi: true }],
|
||||
};
|
||||
}
|
||||
}
|
||||
import { NgModule } from '@angular/core';
|
||||
import { DevScanAdapter } from './dev.scan-adapter';
|
||||
import { NativeScanAdapter } from './native.scan-adapter';
|
||||
import { SCAN_ADAPTER } from './tokens';
|
||||
|
||||
@NgModule({})
|
||||
export class ScanAdapterModule {
|
||||
static forRoot() {
|
||||
return {
|
||||
ngModule: ScanAdapterModule,
|
||||
providers: [
|
||||
{ provide: SCAN_ADAPTER, useClass: NativeScanAdapter, multi: true },
|
||||
{ provide: SCAN_ADAPTER, useClass: DevScanAdapter, multi: true },
|
||||
],
|
||||
// Use for testing:
|
||||
// providers: [{ provide: SCAN_ADAPTER, useClass: dev ? DevScanAdapter : NativeScanAdapter, multi: true }],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ScanditOverlayComponent } from './scandit-overlay.component';
|
||||
import { ScanditScanAdapter } from './scandit.scan-adapter';
|
||||
import { SCAN_ADAPTER } from '../tokens';
|
||||
|
||||
/**
|
||||
* @deprecated Use @isa/shared/scanner instead.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
exports: [ScanditOverlayComponent],
|
||||
declarations: [ScanditOverlayComponent],
|
||||
})
|
||||
export class ScanditScanAdapterModule {
|
||||
static forRoot() {
|
||||
return {
|
||||
ngModule: ScanditScanAdapterModule,
|
||||
providers: [
|
||||
{ provide: SCAN_ADAPTER, useClass: ScanditScanAdapter, multi: true },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ScanditOverlayComponent } from './scandit-overlay.component';
|
||||
import { ScanditScanAdapter } from './scandit.scan-adapter';
|
||||
import { SCAN_ADAPTER } from '../tokens';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
exports: [ScanditOverlayComponent],
|
||||
declarations: [ScanditOverlayComponent],
|
||||
})
|
||||
export class ScanditScanAdapterModule {
|
||||
static forRoot() {
|
||||
return {
|
||||
ngModule: ScanditScanAdapterModule,
|
||||
providers: [{ provide: SCAN_ADAPTER, useClass: ScanditScanAdapter, multi: true }],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 '@isa/core/connectivity';
|
||||
import { injectNetworkStatus$ } from 'apps/isa-app/src/app/services/network-status.service';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Injectable()
|
||||
|
||||
19
apps/isa-app/src/app/app-domain.module.ts
Normal file
19
apps/isa-app/src/app/app-domain.module.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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,4 +1,5 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import {
|
||||
CanActivateCartGuard,
|
||||
CanActivateCartWithProcessIdGuard,
|
||||
@@ -10,12 +11,13 @@ import {
|
||||
CanActivateProductWithProcessIdGuard,
|
||||
IsAuthenticatedGuard,
|
||||
} from './guards';
|
||||
import { MainComponent } from './main.component';
|
||||
import {
|
||||
BranchSectionResolver,
|
||||
CustomerSectionResolver,
|
||||
ProcessIdResolver,
|
||||
} from './resolvers';
|
||||
import { TokenLoginComponent } from './token-login';
|
||||
import { TokenLoginComponent, TokenLoginModule } from './token-login';
|
||||
import {
|
||||
ActivateProcessIdGuard,
|
||||
ActivateProcessIdWithConfigKeyGuard,
|
||||
@@ -26,8 +28,9 @@ import {
|
||||
processResolverFn,
|
||||
hasTabIdGuard,
|
||||
} from '@isa/core/tabs';
|
||||
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
|
||||
|
||||
export const routes: Routes = [
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'kunde/dashboard', pathMatch: 'full' },
|
||||
{
|
||||
path: 'login',
|
||||
@@ -42,6 +45,7 @@ export const routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: 'kunde',
|
||||
component: MainComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
@@ -68,6 +72,8 @@ export 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: () =>
|
||||
@@ -116,6 +122,7 @@ export 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),
|
||||
},
|
||||
@@ -134,6 +141,7 @@ export const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'filiale',
|
||||
component: MainComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'task-calendar',
|
||||
@@ -146,6 +154,7 @@ export 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),
|
||||
},
|
||||
@@ -179,6 +188,7 @@ export const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: ':tabId',
|
||||
component: MainComponent,
|
||||
resolve: { process: processResolverFn, tab: tabResolverFn },
|
||||
canActivate: [IsAuthenticatedGuard, hasTabIdGuard],
|
||||
children: [
|
||||
@@ -208,6 +218,7 @@ export const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: 'return',
|
||||
loadChildren: () =>
|
||||
@@ -235,3 +246,16 @@ export const routes: Routes = [
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
bindToComponentInputs: true,
|
||||
enableTracing: false,
|
||||
}),
|
||||
TokenLoginModule,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
providers: [provideScrollPositionRestoration()],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
34
apps/isa-app/src/app/app-store.module.ts
Normal file
34
apps/isa-app/src/app/app-store.module.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
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 {}
|
||||
40
apps/isa-app/src/app/app-swagger.module.ts
Normal file
40
apps/isa-app/src/app/app-swagger.module.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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,28 +1,28 @@
|
||||
<!-- @if ($offlineBannerVisible()) {
|
||||
<div [@fadeInOut] class="bg-brand text-white text-center fixed inset-x-0 top-0 z-tooltip p-4">
|
||||
<h3 class="font-bold grid grid-flow-col items-center justify-center text-xl gap-4">
|
||||
<div>
|
||||
<ng-icon name="matWifiOff"></ng-icon>
|
||||
</div>
|
||||
|
||||
<div>Sie sind offline, keine Verbindung zum Netzwerk.</div>
|
||||
</h3>
|
||||
<p>Bereits geladene Ihnalte werden angezeigt, Interaktionen sind aktuell nicht möglich.</p>
|
||||
</div>
|
||||
}
|
||||
@if ($onlineBannerVisible()) {
|
||||
<div [@fadeInOut] class="bg-green-500 text-white text-center fixed inset-x-0 top-0 z-tooltip p-4">
|
||||
<h3 class="font-bold grid grid-flow-col items-center justify-center text-xl gap-4">
|
||||
<div>
|
||||
<ng-icon name="matWifi"></ng-icon>
|
||||
</div>
|
||||
|
||||
<div>Sie sind wieder online.</div>
|
||||
</h3>
|
||||
<button class="fixed top-2 right-4 text-3xl w-12 h-12" type="button" (click)="$onlineBannerVisible.set(false)">
|
||||
<ng-icon name="matClose"></ng-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<router-outlet></router-outlet> -->
|
||||
@if ($offlineBannerVisible()) {
|
||||
<div [@fadeInOut] class="bg-brand text-white text-center fixed inset-x-0 top-0 z-tooltip p-4">
|
||||
<h3 class="font-bold grid grid-flow-col items-center justify-center text-xl gap-4">
|
||||
<div>
|
||||
<ng-icon name="matWifiOff"></ng-icon>
|
||||
</div>
|
||||
|
||||
<div>Sie sind offline, keine Verbindung zum Netzwerk.</div>
|
||||
</h3>
|
||||
<p>Bereits geladene Ihnalte werden angezeigt, Interaktionen sind aktuell nicht möglich.</p>
|
||||
</div>
|
||||
}
|
||||
@if ($onlineBannerVisible()) {
|
||||
<div [@fadeInOut] class="bg-green-500 text-white text-center fixed inset-x-0 top-0 z-tooltip p-4">
|
||||
<h3 class="font-bold grid grid-flow-col items-center justify-center text-xl gap-4">
|
||||
<div>
|
||||
<ng-icon name="matWifi"></ng-icon>
|
||||
</div>
|
||||
|
||||
<div>Sie sind wieder online.</div>
|
||||
</h3>
|
||||
<button class="fixed top-2 right-4 text-3xl w-12 h-12" type="button" (click)="$onlineBannerVisible.set(false)">
|
||||
<ng-icon name="matClose"></ng-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
3
apps/isa-app/src/app/app.component.scss
Normal file
3
apps/isa-app/src/app/app.component.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply block;
|
||||
}
|
||||
137
apps/isa-app/src/app/app.component.spec.ts
Normal file
137
apps/isa-app/src/app/app.component.spec.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
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,205 +1,206 @@
|
||||
// import {
|
||||
// Component,
|
||||
// effect,
|
||||
// HostListener,
|
||||
// inject,
|
||||
// Inject,
|
||||
// Injector,
|
||||
// OnInit,
|
||||
// Renderer2,
|
||||
// signal,
|
||||
// untracked,
|
||||
// DOCUMENT
|
||||
// } from '@angular/core';
|
||||
// import { Title } from '@angular/platform-browser';
|
||||
// import { SwUpdate } from '@angular/service-worker';
|
||||
// import { ApplicationService } from '@core/application';
|
||||
// import { Config } from '@core/config';
|
||||
// import { NotificationsHub } from '@hub/notifications';
|
||||
// import packageInfo from 'packageJson';
|
||||
// import { asapScheduler, interval, Subscription } from 'rxjs';
|
||||
// import { UserStateService } from '@generated/swagger/isa-api';
|
||||
// import { IsaLogProvider } from './providers';
|
||||
// import { EnvironmentService } from '@core/environment';
|
||||
// import { AuthService, LoginStrategy } from '@core/auth';
|
||||
// import { UiMessageModalComponent, UiModalService } from '@ui/modal';
|
||||
// import { injectNetworkStatus } from '@isa/core/connectivity';
|
||||
// import { animate, style, transition, trigger } from '@angular/animations';
|
||||
|
||||
// @Component({
|
||||
// selector: 'app-root',
|
||||
// templateUrl: './app.component.html',
|
||||
// styleUrls: ['./app.component.scss'],
|
||||
// animations: [
|
||||
// trigger('fadeInOut', [
|
||||
// transition(':enter', [
|
||||
// // :enter wird ausgelöst, wenn das Element zum DOM hinzugefügt wird
|
||||
// style({ opacity: 0, transform: 'translateY(-100%)' }),
|
||||
// animate('300ms', style({ opacity: 1, transform: 'translateY(0)' })),
|
||||
// ]),
|
||||
// transition(':leave', [
|
||||
// // :leave wird ausgelöst, wenn das Element aus dem DOM entfernt wird
|
||||
// animate('300ms', style({ opacity: 0, transform: 'translateY(-100%)' })),
|
||||
// ]),
|
||||
// ]),
|
||||
// ],
|
||||
// standalone: false,
|
||||
// })
|
||||
// export class AppComponent implements OnInit {
|
||||
// readonly injector = inject(Injector);
|
||||
|
||||
// $networkStatus = injectNetworkStatus();
|
||||
|
||||
// $offlineBannerVisible = signal(false);
|
||||
|
||||
// $onlineBannerVisible = signal(false);
|
||||
|
||||
// private onlineBannerDismissTimeout: any;
|
||||
|
||||
// onlineEffects = effect(() => {
|
||||
// const status = this.$networkStatus();
|
||||
// const online = status === 'online';
|
||||
// const offlineBannerVisible = this.$offlineBannerVisible();
|
||||
|
||||
// untracked(() => {
|
||||
// this.$offlineBannerVisible.set(!online);
|
||||
|
||||
// if (!online) {
|
||||
// this.$onlineBannerVisible.set(false);
|
||||
// clearTimeout(this.onlineBannerDismissTimeout);
|
||||
// }
|
||||
|
||||
// if (offlineBannerVisible && online) {
|
||||
// this.$onlineBannerVisible.set(true);
|
||||
// this.onlineBannerDismissTimeout = setTimeout(() => this.$onlineBannerVisible.set(false), 5000);
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
// private _checkForUpdates: number = this._config.get('checkForUpdates');
|
||||
|
||||
// get checkForUpdates(): number {
|
||||
// return this._checkForUpdates ?? 60 * 60 * 1000; // default 1 hour
|
||||
// }
|
||||
|
||||
// // For Unit Testing
|
||||
// set checkForUpdates(time: number) {
|
||||
// this._checkForUpdates = time;
|
||||
// }
|
||||
|
||||
// subscriptions = new Subscription();
|
||||
|
||||
// constructor(
|
||||
// private readonly _config: Config,
|
||||
// private readonly _title: Title,
|
||||
// private readonly _appService: ApplicationService,
|
||||
// @Inject(DOCUMENT) private readonly _document: Document,
|
||||
// private readonly _renderer: Renderer2,
|
||||
// private readonly _swUpdate: SwUpdate,
|
||||
// private readonly _notifications: NotificationsHub,
|
||||
// private infoService: UserStateService,
|
||||
// private readonly _environment: EnvironmentService,
|
||||
// private readonly _authService: AuthService,
|
||||
// private readonly _modal: UiModalService,
|
||||
// ) {
|
||||
// this.updateClient();
|
||||
// IsaLogProvider.InfoService = this.infoService;
|
||||
// }
|
||||
|
||||
// ngOnInit() {
|
||||
// this.setTitle();
|
||||
// this.logVersion();
|
||||
// asapScheduler.schedule(() => this.determinePlatform(), 250);
|
||||
// this._appService.getSection$().subscribe(this.sectionChangeHandler.bind(this));
|
||||
|
||||
// this.setupSilentRefresh();
|
||||
// }
|
||||
|
||||
// // Setup interval for silent refresh
|
||||
// setupSilentRefresh() {
|
||||
// const silentRefreshInterval = this._config.get('silentRefresh.interval');
|
||||
// if (silentRefreshInterval > 0) {
|
||||
// interval(silentRefreshInterval).subscribe(() => {
|
||||
// if (this._authService.isAuthenticated()) {
|
||||
// this._authService.refresh();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// setTitle() {
|
||||
// this._title.setTitle(this._config.get('title'));
|
||||
// }
|
||||
|
||||
// logVersion() {
|
||||
// console.log(
|
||||
// `%c${this._config.get('title')}\r\nVersion: ${packageInfo.version}`,
|
||||
// 'font-weight: bold; font-size: 20px;',
|
||||
// );
|
||||
// }
|
||||
|
||||
// determinePlatform() {
|
||||
// if (this._environment.isNative()) {
|
||||
// this._renderer.addClass(this._document.body, 'tablet-native');
|
||||
// } else if (this._environment.isTablet()) {
|
||||
// this._renderer.addClass(this._document.body, 'tablet-browser');
|
||||
// }
|
||||
// if (this._environment.isTablet()) {
|
||||
// this._renderer.addClass(this._document.body, 'tablet');
|
||||
// }
|
||||
// if (this._environment.isDesktop()) {
|
||||
// this._renderer.addClass(this._document.body, 'desktop');
|
||||
// }
|
||||
// }
|
||||
|
||||
// sectionChangeHandler(section: string) {
|
||||
// if (section === 'customer') {
|
||||
// this._renderer.removeClass(this._document.body, 'branch');
|
||||
// this._renderer.addClass(this._document.body, 'customer');
|
||||
// } else if (section === 'branch') {
|
||||
// this._renderer.removeClass(this._document.body, 'customer');
|
||||
// this._renderer.addClass(this._document.body, 'branch');
|
||||
// }
|
||||
// }
|
||||
|
||||
// updateClient() {
|
||||
// if (!this._swUpdate.isEnabled) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this.initialCheckForUpdate();
|
||||
// this.checkForUpdate();
|
||||
// }
|
||||
|
||||
// checkForUpdate() {
|
||||
// interval(this._checkForUpdates).subscribe(() => {
|
||||
// this._swUpdate.checkForUpdate().then((value) => {
|
||||
// console.log('check for update', value);
|
||||
// if (value) {
|
||||
// this._notifications.updateNotification();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// initialCheckForUpdate() {
|
||||
// this._swUpdate.checkForUpdate().then((value) => {
|
||||
// console.log('initial check for update', value);
|
||||
// if (value) {
|
||||
// location.reload();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// @HostListener('window:visibilitychange', ['$event'])
|
||||
// onVisibilityChange(event: Event) {
|
||||
// // refresh token when app is in background
|
||||
// if (this._document.hidden && this._authService.isAuthenticated()) {
|
||||
// this._authService.refresh();
|
||||
// } else if (!this._authService.isAuthenticated()) {
|
||||
// const strategy = this.injector.get(LoginStrategy);
|
||||
|
||||
// return strategy.login('Sie sind nicht mehr angemeldet');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
import {
|
||||
Component,
|
||||
effect,
|
||||
HostListener,
|
||||
inject,
|
||||
Inject,
|
||||
Injector,
|
||||
OnInit,
|
||||
Renderer2,
|
||||
signal,
|
||||
untracked,
|
||||
DOCUMENT
|
||||
} from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { Config } from '@core/config';
|
||||
import { NotificationsHub } from '@hub/notifications';
|
||||
import packageInfo from 'packageJson';
|
||||
import { asapScheduler, interval, Subscription } from 'rxjs';
|
||||
import { UserStateService } from '@generated/swagger/isa-api';
|
||||
import { IsaLogProvider } from './providers';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { AuthService, LoginStrategy } from '@core/auth';
|
||||
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
|
||||
import { 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);
|
||||
|
||||
$online = toSignal(injectOnline$());
|
||||
|
||||
$offlineBannerVisible = signal(false);
|
||||
|
||||
$onlineBannerVisible = signal(false);
|
||||
|
||||
private onlineBannerDismissTimeout: any;
|
||||
|
||||
onlineEffects = effect(() => {
|
||||
const online = this.$online();
|
||||
const offlineBannerVisible = this.$offlineBannerVisible();
|
||||
|
||||
untracked(() => {
|
||||
this.$offlineBannerVisible.set(!online);
|
||||
|
||||
if (!online) {
|
||||
this.$onlineBannerVisible.set(false);
|
||||
clearTimeout(this.onlineBannerDismissTimeout);
|
||||
}
|
||||
|
||||
if (offlineBannerVisible && online) {
|
||||
this.$onlineBannerVisible.set(true);
|
||||
this.onlineBannerDismissTimeout = setTimeout(() => this.$onlineBannerVisible.set(false), 5000);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
private _checkForUpdates: number = this._config.get('checkForUpdates');
|
||||
|
||||
get checkForUpdates(): number {
|
||||
return this._checkForUpdates ?? 60 * 60 * 1000; // default 1 hour
|
||||
}
|
||||
|
||||
// For Unit Testing
|
||||
set checkForUpdates(time: number) {
|
||||
this._checkForUpdates = time;
|
||||
}
|
||||
|
||||
subscriptions = new Subscription();
|
||||
|
||||
constructor(
|
||||
private readonly _config: Config,
|
||||
private readonly _title: Title,
|
||||
private readonly _appService: ApplicationService,
|
||||
@Inject(DOCUMENT) private readonly _document: Document,
|
||||
private readonly _renderer: Renderer2,
|
||||
private readonly _swUpdate: SwUpdate,
|
||||
private readonly _notifications: NotificationsHub,
|
||||
private infoService: UserStateService,
|
||||
private readonly _environment: EnvironmentService,
|
||||
private readonly _authService: AuthService,
|
||||
private readonly _modal: UiModalService,
|
||||
) {
|
||||
this.updateClient();
|
||||
IsaLogProvider.InfoService = this.infoService;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.setTitle();
|
||||
this.logVersion();
|
||||
asapScheduler.schedule(() => this.determinePlatform(), 250);
|
||||
this._appService.getSection$().subscribe(this.sectionChangeHandler.bind(this));
|
||||
|
||||
this.setupSilentRefresh();
|
||||
}
|
||||
|
||||
// Setup interval for silent refresh
|
||||
setupSilentRefresh() {
|
||||
const silentRefreshInterval = this._config.get('silentRefresh.interval');
|
||||
if (silentRefreshInterval > 0) {
|
||||
interval(silentRefreshInterval).subscribe(() => {
|
||||
if (this._authService.isAuthenticated()) {
|
||||
this._authService.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setTitle() {
|
||||
this._title.setTitle(this._config.get('title'));
|
||||
}
|
||||
|
||||
logVersion() {
|
||||
console.log(
|
||||
`%c${this._config.get('title')}\r\nVersion: ${packageInfo.version}`,
|
||||
'font-weight: bold; font-size: 20px;',
|
||||
);
|
||||
}
|
||||
|
||||
determinePlatform() {
|
||||
if (this._environment.isNative()) {
|
||||
this._renderer.addClass(this._document.body, 'tablet-native');
|
||||
} else if (this._environment.isTablet()) {
|
||||
this._renderer.addClass(this._document.body, 'tablet-browser');
|
||||
}
|
||||
if (this._environment.isTablet()) {
|
||||
this._renderer.addClass(this._document.body, 'tablet');
|
||||
}
|
||||
if (this._environment.isDesktop()) {
|
||||
this._renderer.addClass(this._document.body, 'desktop');
|
||||
}
|
||||
}
|
||||
|
||||
sectionChangeHandler(section: string) {
|
||||
if (section === 'customer') {
|
||||
this._renderer.removeClass(this._document.body, 'branch');
|
||||
this._renderer.addClass(this._document.body, 'customer');
|
||||
} else if (section === 'branch') {
|
||||
this._renderer.removeClass(this._document.body, 'customer');
|
||||
this._renderer.addClass(this._document.body, 'branch');
|
||||
}
|
||||
}
|
||||
|
||||
updateClient() {
|
||||
if (!this._swUpdate.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.initialCheckForUpdate();
|
||||
this.checkForUpdate();
|
||||
}
|
||||
|
||||
checkForUpdate() {
|
||||
interval(this._checkForUpdates).subscribe(() => {
|
||||
this._swUpdate.checkForUpdate().then((value) => {
|
||||
console.log('check for update', value);
|
||||
if (value) {
|
||||
this._notifications.updateNotification();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initialCheckForUpdate() {
|
||||
this._swUpdate.checkForUpdate().then((value) => {
|
||||
console.log('initial check for update', value);
|
||||
if (value) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('window:visibilitychange', ['$event'])
|
||||
onVisibilityChange(event: Event) {
|
||||
// refresh token when app is in background
|
||||
if (this._document.hidden && this._authService.isAuthenticated()) {
|
||||
this._authService.refresh();
|
||||
} else if (!this._authService.isAuthenticated()) {
|
||||
const strategy = this.injector.get(LoginStrategy);
|
||||
|
||||
return strategy.login('Sie sind nicht mehr angemeldet');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<shell-layout>
|
||||
<router-outlet />
|
||||
</shell-layout>
|
||||
@@ -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 { 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 { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { PlatformModule } from '@angular/cdk/platform';
|
||||
|
||||
import { Config } from '@core/config';
|
||||
import { AuthModule, AuthService, LoginStrategy } from '@core/auth';
|
||||
import { CoreCommandModule } from '@core/command';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
|
||||
import { rootReducer } from './store/root.reducer';
|
||||
import { RootState } from './store/root.state';
|
||||
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 { 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 { provideCoreBreadcrumb } from '@core/breadcrumb';
|
||||
import { CoreBreadcrumbModule } 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,6 +59,7 @@ 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 {
|
||||
@@ -66,9 +67,10 @@ import {
|
||||
matWifi,
|
||||
matWifiOff,
|
||||
} from '@ng-icons/material-icons/baseline';
|
||||
import { NetworkStatusService } from '@isa/core/connectivity';
|
||||
import { NetworkStatusService } from './services/network-status.service';
|
||||
import { debounceTime, filter, firstValueFrom, switchMap } from 'rxjs';
|
||||
import { provideMatomo, withRouter, withRouteData } from 'ngx-matomo-client';
|
||||
import { provideMatomo } from 'ngx-matomo-client';
|
||||
import { withRouter, withRouteData } from 'ngx-matomo-client';
|
||||
import {
|
||||
provideLogging,
|
||||
withLogLevel,
|
||||
@@ -85,58 +87,15 @@ import {
|
||||
import { Store } from '@ngrx/store';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import z from 'zod';
|
||||
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
|
||||
import { TitleStrategy } from '@angular/router';
|
||||
import { TabNavigationService } from '@isa/core/tabs';
|
||||
|
||||
// Domain modules
|
||||
import { provideDomainCheckout } from '@domain/checkout';
|
||||
registerLocaleData(localeDe, localeDeExtra);
|
||||
registerLocaleData(localeDe, 'de', localeDeExtra);
|
||||
|
||||
// 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) {
|
||||
export 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');
|
||||
@@ -147,8 +106,7 @@ function appInitializerFactory(_config: Config, injector: Injector) {
|
||||
let online = false;
|
||||
const networkStatus = injector.get(NetworkStatusService);
|
||||
while (!online) {
|
||||
const status = await firstValueFrom(networkStatus.status$);
|
||||
online = status === 'online';
|
||||
online = await firstValueFrom(networkStatus.online$);
|
||||
|
||||
if (!online) {
|
||||
logger.warn('Waiting for network connection');
|
||||
@@ -203,6 +161,7 @@ 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') });
|
||||
@@ -212,7 +171,7 @@ 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),
|
||||
@@ -223,6 +182,7 @@ 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, () => ({
|
||||
@@ -263,7 +223,7 @@ function appInitializerFactory(_config: Config, injector: Injector) {
|
||||
};
|
||||
}
|
||||
|
||||
function notificationsHubOptionsFactory(
|
||||
export function _notificationsHubOptionsFactory(
|
||||
config: Config,
|
||||
auth: AuthService,
|
||||
): SignalRHubOptions {
|
||||
@@ -297,151 +257,80 @@ const USER_SUB_FACTORY = () => {
|
||||
return signal(validation.data);
|
||||
};
|
||||
|
||||
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,
|
||||
@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',
|
||||
}),
|
||||
|
||||
// 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
|
||||
ScanAdapterModule.forRoot(),
|
||||
ScanditScanAdapterModule.forRoot(),
|
||||
PlatformModule,
|
||||
IconModule.forRoot(),
|
||||
NgIconsModule.withIcons({ matWifiOff, matClose, matWifi }),
|
||||
],
|
||||
providers: [
|
||||
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,
|
||||
},
|
||||
|
||||
// Locale settings
|
||||
{
|
||||
provide: ApplicationService,
|
||||
useClass: ApplicationServiceAdapter,
|
||||
},
|
||||
{ provide: LOCALE_ID, useValue: 'de-DE' },
|
||||
{ provide: DEFAULT_CURRENCY_CODE, useValue: 'EUR' },
|
||||
|
||||
// Analytics
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideMatomo(
|
||||
{ trackerUrl: 'https://matomo.paragon-data.net', siteId: '1' },
|
||||
withRouter(),
|
||||
withRouteData(),
|
||||
),
|
||||
|
||||
// User storage
|
||||
provideLogging(withLogLevel(LogLevel.Debug), withSink(ConsoleLogSink)),
|
||||
{
|
||||
provide: DEFAULT_CURRENCY_CODE,
|
||||
useValue: 'EUR',
|
||||
},
|
||||
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 {}
|
||||
@@ -1,11 +0,0 @@
|
||||
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 '@isa/core/connectivity';
|
||||
import { injectNetworkStatus$ } from '../services/network-status.service';
|
||||
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 { injectNetworkStatus$ } from '@isa/core/connectivity';
|
||||
import { injectOnline$ } from '../services/network-status.service';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
@Injectable()
|
||||
@@ -17,7 +17,7 @@ export class HttpErrorInterceptor implements HttpInterceptor {
|
||||
#logger = logger(() => ({
|
||||
'http-interceptor': 'HttpErrorInterceptor',
|
||||
}));
|
||||
#offline$ = injectNetworkStatus$().pipe(filter((status) => status === 'offline'));
|
||||
#offline$ = injectOnline$().pipe(filter((online) => !online));
|
||||
#injector = inject(Injector);
|
||||
#auth = inject(AuthService);
|
||||
|
||||
|
||||
3
apps/isa-app/src/app/main.component.html
Normal file
3
apps/isa-app/src/app/main.component.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<shell-root>
|
||||
<router-outlet></router-outlet>
|
||||
</shell-root>
|
||||
11
apps/isa-app/src/app/main.component.ts
Normal file
11
apps/isa-app/src/app/main.component.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main',
|
||||
templateUrl: 'main.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class MainComponent {
|
||||
constructor() {}
|
||||
}
|
||||
1
apps/isa-app/src/app/services/index.ts
Normal file
1
apps/isa-app/src/app/services/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './network-status.service';
|
||||
25
apps/isa-app/src/app/services/network-status.service.ts
Normal file
25
apps/isa-app/src/app/services/network-status.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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 +1,2 @@
|
||||
export * from './token-login.component';
|
||||
export * from './token-login.module';
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AuthService } from '@core/auth';
|
||||
|
||||
@Component({
|
||||
selector: 'app-token-login',
|
||||
templateUrl: 'token-login.component.html',
|
||||
styleUrls: ['token-login.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TokenLoginComponent implements OnInit {
|
||||
constructor(
|
||||
private _route: ActivatedRoute,
|
||||
private _authService: AuthService,
|
||||
private _router: Router,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (
|
||||
this._route.snapshot.params.token &&
|
||||
!this._authService.isAuthenticated()
|
||||
) {
|
||||
this._authService.setKeyCardToken(this._route.snapshot.params.token);
|
||||
this._authService.login();
|
||||
} else if (!this._authService.isAuthenticated()) {
|
||||
this._authService.login();
|
||||
} else if (this._authService.isAuthenticated()) {
|
||||
this._router.navigate(['/']);
|
||||
}
|
||||
}
|
||||
}
|
||||
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AuthService } from '@core/auth';
|
||||
|
||||
@Component({
|
||||
selector: 'app-token-login',
|
||||
templateUrl: 'token-login.component.html',
|
||||
styleUrls: ['token-login.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class TokenLoginComponent implements OnInit {
|
||||
constructor(
|
||||
private _route: ActivatedRoute,
|
||||
private _authService: AuthService,
|
||||
private _router: Router,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (this._route.snapshot.params.token && !this._authService.isAuthenticated()) {
|
||||
this._authService.setKeyCardToken(this._route.snapshot.params.token);
|
||||
this._authService.login();
|
||||
} else if (!this._authService.isAuthenticated()) {
|
||||
this._authService.login();
|
||||
} else if (this._authService.isAuthenticated()) {
|
||||
this._router.navigate(['/']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
apps/isa-app/src/app/token-login/token-login.module.ts
Normal file
11
apps/isa-app/src/app/token-login/token-login.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
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 {}
|
||||
23
apps/isa-app/src/core/application/application.module.ts
Normal file
23
apps/isa-app/src/core/application/application.module.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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 {}
|
||||
337
apps/isa-app/src/core/application/application.service-adapter.ts
Normal file
337
apps/isa-app/src/core/application/application.service-adapter.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
233
apps/isa-app/src/core/application/application.service.spec.ts
Normal file
233
apps/isa-app/src/core/application/application.service.spec.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
// 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,68 +1,41 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, of, firstValueFrom } from 'rxjs';
|
||||
import { map, filter, withLatestFrom } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { BranchDTO } from '@generated/swagger/checkout-api';
|
||||
import { isBoolean, isNumber } from '@utils/common';
|
||||
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';
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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' })
|
||||
@Injectable()
|
||||
export class 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+$/;
|
||||
private activatedProcessIdSubject = new BehaviorSubject<number>(undefined);
|
||||
|
||||
get activatedProcessId() {
|
||||
return this.#tabService.activatedTabId();
|
||||
return this.activatedProcessIdSubject.value;
|
||||
}
|
||||
|
||||
get activatedProcessId$() {
|
||||
return this.#activatedProcessId$;
|
||||
return this.activatedProcessIdSubject.asObservable();
|
||||
}
|
||||
|
||||
getProcesses$(
|
||||
section?: 'customer' | 'branch',
|
||||
): Observable<ApplicationProcess[]> {
|
||||
return this.#processes$.pipe(
|
||||
constructor(private store: Store) {}
|
||||
|
||||
getProcesses$(section?: 'customer' | 'branch') {
|
||||
const processes$ = this.store.select(selectProcesses);
|
||||
return processes$.pipe(
|
||||
map((processes) =>
|
||||
processes.filter((process) =>
|
||||
section ? process.section === section : true,
|
||||
@@ -72,96 +45,69 @@ export class ApplicationService {
|
||||
}
|
||||
|
||||
getProcessById$(processId: number): Observable<ApplicationProcess> {
|
||||
return this.#processes$.pipe(
|
||||
return this.getProcesses$().pipe(
|
||||
map((processes) => processes.find((process) => process.id === processId)),
|
||||
);
|
||||
}
|
||||
|
||||
getSection$(): Observable<'customer' | 'branch'> {
|
||||
return this.#section.asObservable();
|
||||
getSection$() {
|
||||
return this.store.select(selectSection);
|
||||
}
|
||||
|
||||
getTitle$(): Observable<'Kundenbereich' | 'Filialbereich'> {
|
||||
getTitle$() {
|
||||
return this.getSection$().pipe(
|
||||
map((section) =>
|
||||
section === 'customer' ? 'Kundenbereich' : 'Filialbereich',
|
||||
),
|
||||
map((section) => {
|
||||
return section === 'customer' ? 'Kundenbereich' : 'Filialbereich';
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
getActivatedProcessId$(): Observable<number> {
|
||||
return this.activatedProcessId$;
|
||||
getActivatedProcessId$() {
|
||||
return this.store
|
||||
.select(selectActivatedProcess)
|
||||
.pipe(map((process) => process?.id));
|
||||
}
|
||||
|
||||
activateProcess(activatedProcessId: number): void {
|
||||
this.#tabService.activateTab(activatedProcessId);
|
||||
activateProcess(activatedProcessId: number) {
|
||||
this.store.dispatch(setActivatedProcess({ activatedProcessId }));
|
||||
this.activatedProcessIdSubject.next(activatedProcessId);
|
||||
}
|
||||
|
||||
removeProcess(processId: number): void {
|
||||
this.#tabService.removeTab(processId);
|
||||
this.#store.dispatch(removeProcess({ processId }));
|
||||
removeProcess(processId: number) {
|
||||
this.store.dispatch(removeProcess({ processId }));
|
||||
}
|
||||
|
||||
patchProcess(processId: number, changes: Partial<ApplicationProcess>): void {
|
||||
const tabChanges: {
|
||||
name?: string;
|
||||
tags?: string[];
|
||||
metadata?: Record<string, unknown>;
|
||||
} = {};
|
||||
patchProcess(processId: number, changes: Partial<ApplicationProcess>) {
|
||||
this.store.dispatch(patchProcess({ processId, changes }));
|
||||
}
|
||||
|
||||
if (changes.name) {
|
||||
tabChanges.name = changes.name;
|
||||
patchProcessData(processId: number, data: Record<string, any>) {
|
||||
this.store.dispatch(patchProcessData({ processId, data }));
|
||||
}
|
||||
|
||||
getSelectedBranch$(processId?: number): Observable<BranchDTO> {
|
||||
if (!processId) {
|
||||
return this.activatedProcessId$.pipe(
|
||||
switchMap((processId) =>
|
||||
this.getProcessById$(processId).pipe(
|
||||
map((process) => process?.data?.selectedBranch),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 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),
|
||||
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 firstValueFrom(this.getProcesses$('customer'));
|
||||
const processes = await this.getProcesses$('customer')
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
const processIds = processes
|
||||
.filter((x) => this.REGEX_PROCESS_NAME.test(x.name))
|
||||
@@ -178,18 +124,14 @@ export class ApplicationService {
|
||||
};
|
||||
|
||||
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];
|
||||
async createProcess(process: ApplicationProcess) {
|
||||
const existingProcess = await this.getProcessById$(process?.id)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
if (existingProcess?.id === process?.id) {
|
||||
throw new Error('Process Id existiert bereits');
|
||||
}
|
||||
@@ -206,28 +148,13 @@ export class ApplicationService {
|
||||
process.confirmClosing = true;
|
||||
}
|
||||
|
||||
process.created = this.createTimestamp();
|
||||
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,
|
||||
},
|
||||
});
|
||||
this.store.dispatch(addProcess({ process }));
|
||||
}
|
||||
|
||||
setSection(section: 'customer' | 'branch'): void {
|
||||
this.#section.next(section);
|
||||
setSection(section: 'customer' | 'branch') {
|
||||
this.store.dispatch(setSection({ section }));
|
||||
}
|
||||
|
||||
getLastActivatedProcessWithSectionAndType$(
|
||||
@@ -263,74 +190,7 @@ export class ApplicationService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
private _createTimestamp() {
|
||||
return Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './application.service';
|
||||
export * from './defs';
|
||||
export * from './store/application.actions';
|
||||
export * from './application.module';
|
||||
export * from './application.service';
|
||||
export * from './application.service-adapter';
|
||||
export * from './defs';
|
||||
export * from './store';
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
import { createAction, props } from '@ngrx/store';
|
||||
|
||||
const prefix = '[CORE-APPLICATION]';
|
||||
|
||||
export const removeProcess = createAction(
|
||||
`${prefix} Remove Process`,
|
||||
props<{ processId: number }>(),
|
||||
);
|
||||
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> }>(),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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]);
|
||||
// });
|
||||
// });
|
||||
@@ -0,0 +1,18 @@
|
||||
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),
|
||||
);
|
||||
13
apps/isa-app/src/core/application/store/application.state.ts
Normal file
13
apps/isa-app/src/core/application/store/application.state.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ApplicationProcess } from '../defs';
|
||||
|
||||
export interface ApplicationState {
|
||||
title: string;
|
||||
processes: ApplicationProcess[];
|
||||
section: 'customer' | 'branch';
|
||||
}
|
||||
|
||||
export const INITIAL_APPLICATION_STATE: ApplicationState = {
|
||||
title: '',
|
||||
processes: [],
|
||||
section: 'customer',
|
||||
};
|
||||
6
apps/isa-app/src/core/application/store/index.ts
Normal file
6
apps/isa-app/src/core/application/store/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// 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 '@isa/core/connectivity';
|
||||
import { injectNetworkStatus$ } from '../../app/services';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthService as IsaAuthService } from '@generated/swagger/isa-api';
|
||||
import { firstValueFrom, lastValueFrom } from 'rxjs';
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
||||
import { provideEffects } from '@ngrx/effects';
|
||||
import { provideState } from '@ngrx/store';
|
||||
import { BreadcrumbService } from './breadcrumb.service';
|
||||
import { BreadcrumbEffects } from './store/breadcrumb.effect';
|
||||
import { breadcrumbReducer } from './store/breadcrumb.reducer';
|
||||
import { featureName } from './store/breadcrumb.state';
|
||||
|
||||
export function provideCoreBreadcrumb(): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([
|
||||
provideState({ name: featureName, reducer: breadcrumbReducer }),
|
||||
provideEffects(BreadcrumbEffects),
|
||||
BreadcrumbService,
|
||||
]);
|
||||
}
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { BreadcrumbService } from './breadcrumb.service';
|
||||
import { BreadcrumbEffects } from './store/breadcrumb.effect';
|
||||
import { breadcrumbReducer } from './store/breadcrumb.reducer';
|
||||
import { featureName } from './store/breadcrumb.state';
|
||||
|
||||
@NgModule()
|
||||
export class CoreBreadcrumbModule {
|
||||
static forRoot(): ModuleWithProviders<CoreBreadcrumbModule> {
|
||||
return {
|
||||
ngModule: CoreBreadcrumbForRootModule,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [StoreModule.forFeature(featureName, breadcrumbReducer), EffectsModule.forFeature([BreadcrumbEffects])],
|
||||
providers: [BreadcrumbService],
|
||||
})
|
||||
export class CoreBreadcrumbForRootModule {}
|
||||
|
||||
12
apps/isa-app/src/domain/availability/availability.module.ts
Normal file
12
apps/isa-app/src/domain/availability/availability.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { DomainAvailabilityService } from './availability.service';
|
||||
|
||||
@NgModule()
|
||||
export class DomainAvailabilityModule {
|
||||
static forRoot(): ModuleWithProviders<DomainAvailabilityModule> {
|
||||
return {
|
||||
ngModule: DomainAvailabilityModule,
|
||||
providers: [DomainAvailabilityService],
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
export * from './availability.module';
|
||||
export * from './availability.service';
|
||||
export * from './defs';
|
||||
export * from './in-stock.service';
|
||||
|
||||
18
apps/isa-app/src/domain/catalog/catalog.module.ts
Normal file
18
apps/isa-app/src/domain/catalog/catalog.module.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
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],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,111 +1,105 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import {
|
||||
AutocompleteTokenDTO,
|
||||
PromotionService,
|
||||
QueryTokenDTO,
|
||||
SearchService,
|
||||
} from '@generated/swagger/cat-search-api';
|
||||
import { memorize } from '@utils/common';
|
||||
import { map, share, shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainCatalogService {
|
||||
constructor(
|
||||
private searchService: SearchService,
|
||||
private promotionService: PromotionService,
|
||||
private applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
@memorize()
|
||||
getFilters() {
|
||||
return this.searchService.SearchSearchFilter().pipe(
|
||||
map((res) => res.result),
|
||||
shareReplay(),
|
||||
);
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getOrderBy() {
|
||||
return this.searchService.SearchSearchSort().pipe(
|
||||
map((res) => res.result),
|
||||
shareReplay(),
|
||||
);
|
||||
}
|
||||
|
||||
getSearchHistory({ take }: { take: number }) {
|
||||
return this.searchService
|
||||
.SearchHistory(take ?? 5)
|
||||
.pipe(map((res) => res.result));
|
||||
}
|
||||
|
||||
@memorize({ ttl: 120000 })
|
||||
search({ queryToken }: { queryToken: QueryTokenDTO }) {
|
||||
return this.searchService
|
||||
.SearchSearch({
|
||||
...queryToken,
|
||||
stockId: null,
|
||||
})
|
||||
.pipe(share());
|
||||
}
|
||||
|
||||
@memorize({ ttl: 120000 })
|
||||
searchWithStockId({ queryToken }: { queryToken: QueryTokenDTO }) {
|
||||
return this.searchService
|
||||
.SearchSearch2({
|
||||
queryToken,
|
||||
stockId: queryToken?.stockId ?? null,
|
||||
})
|
||||
.pipe(share());
|
||||
}
|
||||
|
||||
getDetailsById({ id }: { id: number }) {
|
||||
return this.searchService.SearchDetail({
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
getDetailsByEan({ ean }: { ean: string }) {
|
||||
return this.searchService.SearchDetailByEAN(ean);
|
||||
}
|
||||
|
||||
searchByIds({ ids }: { ids: number[] }) {
|
||||
return this.searchService.SearchById(ids);
|
||||
}
|
||||
|
||||
searchByEans({ eans }: { eans: string[] }) {
|
||||
return this.searchService.SearchByEAN(eans);
|
||||
}
|
||||
|
||||
searchTop({ queryToken }: { queryToken: QueryTokenDTO }) {
|
||||
return this.searchService.SearchTop(queryToken);
|
||||
}
|
||||
|
||||
searchComplete({ queryToken }: { queryToken: AutocompleteTokenDTO }) {
|
||||
return this.searchService.SearchAutocomplete(queryToken);
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getPromotionPoints({
|
||||
items,
|
||||
}: {
|
||||
items: { id: number; quantity: number; price?: number }[];
|
||||
}) {
|
||||
return this.promotionService.PromotionLesepunkte(items).pipe(shareReplay());
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getSettings() {
|
||||
return this.searchService.SearchSettings().pipe(
|
||||
map((res) => res.result),
|
||||
shareReplay(),
|
||||
);
|
||||
}
|
||||
|
||||
getRecommendations({ digId }: { digId: number }) {
|
||||
return this.searchService.SearchGetRecommendations({
|
||||
digId: digId + '',
|
||||
sessionId: this.applicationService.activatedProcessId + '',
|
||||
});
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import {
|
||||
AutocompleteTokenDTO,
|
||||
PromotionService,
|
||||
QueryTokenDTO,
|
||||
SearchService,
|
||||
} from '@generated/swagger/cat-search-api';
|
||||
import { memorize } from '@utils/common';
|
||||
import { map, share, shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class DomainCatalogService {
|
||||
constructor(
|
||||
private searchService: SearchService,
|
||||
private promotionService: PromotionService,
|
||||
private applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
@memorize()
|
||||
getFilters() {
|
||||
return this.searchService.SearchSearchFilter().pipe(
|
||||
map((res) => res.result),
|
||||
shareReplay(),
|
||||
);
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getOrderBy() {
|
||||
return this.searchService.SearchSearchSort().pipe(
|
||||
map((res) => res.result),
|
||||
shareReplay(),
|
||||
);
|
||||
}
|
||||
|
||||
getSearchHistory({ take }: { take: number }) {
|
||||
return this.searchService.SearchHistory(take ?? 5).pipe(map((res) => res.result));
|
||||
}
|
||||
|
||||
@memorize({ ttl: 120000 })
|
||||
search({ queryToken }: { queryToken: QueryTokenDTO }) {
|
||||
return this.searchService
|
||||
.SearchSearch({
|
||||
...queryToken,
|
||||
stockId: null,
|
||||
})
|
||||
.pipe(share());
|
||||
}
|
||||
|
||||
@memorize({ ttl: 120000 })
|
||||
searchWithStockId({ queryToken }: { queryToken: QueryTokenDTO }) {
|
||||
return this.searchService
|
||||
.SearchSearch2({
|
||||
queryToken,
|
||||
stockId: queryToken?.stockId ?? null,
|
||||
})
|
||||
.pipe(share());
|
||||
}
|
||||
|
||||
getDetailsById({ id }: { id: number }) {
|
||||
return this.searchService.SearchDetail({
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
getDetailsByEan({ ean }: { ean: string }) {
|
||||
return this.searchService.SearchDetailByEAN(ean);
|
||||
}
|
||||
|
||||
searchByIds({ ids }: { ids: number[] }) {
|
||||
return this.searchService.SearchById(ids);
|
||||
}
|
||||
|
||||
searchByEans({ eans }: { eans: string[] }) {
|
||||
return this.searchService.SearchByEAN(eans);
|
||||
}
|
||||
|
||||
searchTop({ queryToken }: { queryToken: QueryTokenDTO }) {
|
||||
return this.searchService.SearchTop(queryToken);
|
||||
}
|
||||
|
||||
searchComplete({ queryToken }: { queryToken: AutocompleteTokenDTO }) {
|
||||
return this.searchService.SearchAutocomplete(queryToken);
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getPromotionPoints({ items }: { items: { id: number; quantity: number; price?: number }[] }) {
|
||||
return this.promotionService.PromotionLesepunkte(items).pipe(shareReplay());
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getSettings() {
|
||||
return this.searchService.SearchSettings().pipe(
|
||||
map((res) => res.result),
|
||||
shareReplay(),
|
||||
);
|
||||
}
|
||||
|
||||
getRecommendations({ digId }: { digId: number }) {
|
||||
return this.searchService.SearchGetRecommendations({
|
||||
digId: digId + '',
|
||||
sessionId: this.applicationService.activatedProcessId + '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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: true,
|
||||
standalone: false,
|
||||
})
|
||||
export class ThumbnailUrlPipe implements PipeTransform, OnDestroy {
|
||||
private input$ = new BehaviorSubject<{ width?: number; height?: number; ean?: string }>(undefined);
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { memorize } from '@utils/common';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
import { DomainCatalogService } from './catalog.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainCatalogThumbnailService {
|
||||
constructor(private domainCatalogService: DomainCatalogService) {}
|
||||
|
||||
@memorize()
|
||||
getThumnaulUrl({
|
||||
ean,
|
||||
height,
|
||||
width,
|
||||
}: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
ean?: string;
|
||||
}) {
|
||||
return this.domainCatalogService.getSettings().pipe(
|
||||
map((settings) => {
|
||||
const thumbnailUrl = settings.imageUrl.replace(/{ean}/, ean);
|
||||
return thumbnailUrl;
|
||||
}),
|
||||
shareReplay(),
|
||||
);
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import { memorize } from '@utils/common';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
import { DomainCatalogService } from './catalog.service';
|
||||
|
||||
@Injectable()
|
||||
export class DomainCatalogThumbnailService {
|
||||
constructor(private domainCatalogService: DomainCatalogService) {}
|
||||
|
||||
@memorize()
|
||||
getThumnaulUrl({ ean, height, width }: { width?: number; height?: number; ean?: string }) {
|
||||
return this.domainCatalogService.getSettings().pipe(
|
||||
map((settings) => {
|
||||
let thumbnailUrl = settings.imageUrl.replace(/{ean}/, ean);
|
||||
return thumbnailUrl;
|
||||
}),
|
||||
shareReplay(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
||||
import { provideEffects } from '@ngrx/effects';
|
||||
import { provideState } from '@ngrx/store';
|
||||
import { DomainCheckoutService } from './checkout.service';
|
||||
import { DomainCheckoutEffects } from './store/domain-checkout.effects';
|
||||
import { domainCheckoutReducer } from './store/domain-checkout.reducer';
|
||||
import { storeFeatureName } from './store/domain-checkout.state';
|
||||
|
||||
export function provideDomainCheckout(): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([
|
||||
provideState({ name: storeFeatureName, reducer: domainCheckoutReducer }),
|
||||
provideEffects(DomainCheckoutEffects),
|
||||
DomainCheckoutService,
|
||||
]);
|
||||
}
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { DomainCheckoutService } from './checkout.service';
|
||||
import { domainCheckoutReducer } from './store/domain-checkout.reducer';
|
||||
import { storeFeatureName } from './store/domain-checkout.state';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { DomainCheckoutEffects } from './store/domain-checkout.effects';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [StoreModule.forFeature(storeFeatureName, domainCheckoutReducer)],
|
||||
providers: [DomainCheckoutService],
|
||||
})
|
||||
export class DomainCheckoutModule {
|
||||
static forRoot(): ModuleWithProviders<DomainCheckoutModule> {
|
||||
return {
|
||||
ngModule: RootDomainCheckoutModule,
|
||||
providers: [DomainCheckoutService],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
StoreModule.forFeature(storeFeatureName, domainCheckoutReducer),
|
||||
EffectsModule.forFeature([DomainCheckoutEffects]),
|
||||
],
|
||||
})
|
||||
export class RootDomainCheckoutModule {}
|
||||
|
||||
@@ -1071,7 +1071,7 @@ export class DomainCheckoutService {
|
||||
});
|
||||
} else if (orderType === 'B2B-Versand') {
|
||||
const branch = await this.applicationService
|
||||
.getSelectedBranch$()
|
||||
.getSelectedBranch$(processId)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
availability$ =
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { InfoService } from '@generated/swagger/isa-api';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainDashboardService {
|
||||
constructor(private readonly _infoService: InfoService) {}
|
||||
|
||||
feed() {
|
||||
return this._infoService.InfoInfo({});
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import { InfoService } from '@generated/swagger/isa-api';
|
||||
|
||||
@Injectable()
|
||||
export class DomainDashboardService {
|
||||
constructor(private readonly _infoService: InfoService) {}
|
||||
|
||||
feed() {
|
||||
return this._infoService.InfoInfo({});
|
||||
}
|
||||
}
|
||||
|
||||
12
apps/isa-app/src/domain/isa/domain-isa.module.ts
Normal file
12
apps/isa-app/src/domain/isa/domain-isa.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
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,2 +1,3 @@
|
||||
export * from './dashboard.service';
|
||||
export * from './defs';
|
||||
export * from './domain-isa.module';
|
||||
|
||||
@@ -1,130 +1,116 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
AbholfachService,
|
||||
AutocompleteTokenDTO,
|
||||
QueryTokenDTO,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { DateAdapter } from '@ui/common';
|
||||
import { memorize } from '@utils/common';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainGoodsService {
|
||||
constructor(
|
||||
private abholfachService: AbholfachService,
|
||||
private dateAdapter: DateAdapter,
|
||||
) {}
|
||||
|
||||
searchWareneingang(queryToken: QueryTokenDTO) {
|
||||
return this.abholfachService.AbholfachWareneingang(queryToken);
|
||||
}
|
||||
|
||||
searchWarenausgabe(queryToken: QueryTokenDTO) {
|
||||
return this.abholfachService.AbholfachWarenausgabe(queryToken);
|
||||
}
|
||||
|
||||
wareneingangComplete(autocompleteToken: AutocompleteTokenDTO) {
|
||||
return this.abholfachService.AbholfachWareneingangAutocomplete(
|
||||
autocompleteToken,
|
||||
);
|
||||
}
|
||||
|
||||
warenausgabeComplete(autocompleteToken: AutocompleteTokenDTO) {
|
||||
return this.abholfachService.AbholfachWarenausgabeAutocomplete(
|
||||
autocompleteToken,
|
||||
);
|
||||
}
|
||||
|
||||
getWareneingangItemByOrderNumber(orderNumber: string) {
|
||||
return this.abholfachService.AbholfachWareneingang({
|
||||
filter: { all_branches: 'true', archive: 'true' },
|
||||
input: {
|
||||
qs: orderNumber,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getWarenausgabeItemByOrderNumber(orderNumber: string, archive: boolean) {
|
||||
return this.abholfachService.AbholfachWarenausgabe({
|
||||
filter: { all_branches: 'true', archive: `${archive}` },
|
||||
input: {
|
||||
qs: orderNumber,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getWarenausgabeItemByCompartment(compartmentCode: string, archive: boolean) {
|
||||
return this.abholfachService.AbholfachWarenausgabe({
|
||||
filter: { all_branches: 'true', archive: `${archive}` },
|
||||
input: {
|
||||
qs: compartmentCode,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getWareneingangItemByCustomerNumber(customerNumber: string) {
|
||||
// Suche anhand der Kundennummer mit Status Bestellt, nachbestellt, eingetroffen, weitergeleitet intern
|
||||
return this.abholfachService.AbholfachWareneingang({
|
||||
filter: { orderitemprocessingstatus: '16;128;8192;1048576' },
|
||||
input: {
|
||||
customer_no: customerNumber,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
list() {
|
||||
const base = this.dateAdapter.today();
|
||||
const startDate = this.dateAdapter.addCalendarDays(base, -5);
|
||||
const endDate = this.dateAdapter.addCalendarDays(base, 1);
|
||||
const queryToken: QueryTokenDTO = {
|
||||
filter: {
|
||||
orderitemprocessingstatus: '16;8192;1024;512;2048',
|
||||
estimatedshippingdate: `"${startDate.toJSON()}"-"${endDate.toJSON()}"`,
|
||||
},
|
||||
orderBy: [{ by: 'estimatedshippingdate' }],
|
||||
skip: 0,
|
||||
take: 20,
|
||||
};
|
||||
return this.searchWareneingang(queryToken);
|
||||
}
|
||||
|
||||
@memorize()
|
||||
goodsInQuerySettings() {
|
||||
return this.abholfachService
|
||||
.AbholfachWareneingangQuerySettings()
|
||||
.pipe(shareReplay());
|
||||
}
|
||||
|
||||
@memorize()
|
||||
goodsOutQuerySettings() {
|
||||
return this.abholfachService
|
||||
.AbholfachWarenausgabeQuerySettings()
|
||||
.pipe(shareReplay());
|
||||
}
|
||||
|
||||
goodsInList(queryToken: QueryTokenDTO) {
|
||||
return this.abholfachService.AbholfachWareneingangsliste(queryToken);
|
||||
}
|
||||
|
||||
@memorize()
|
||||
goodsInListQuerySettings() {
|
||||
return this.abholfachService
|
||||
.AbholfachWareneingangslisteQuerySettings()
|
||||
.pipe(shareReplay());
|
||||
}
|
||||
|
||||
goodsInCleanupList() {
|
||||
return this.abholfachService.AbholfachAbholfachbereinigungsliste();
|
||||
}
|
||||
|
||||
goodsInReservationList(queryToken: QueryTokenDTO) {
|
||||
return this.abholfachService.AbholfachReservierungen(queryToken);
|
||||
}
|
||||
|
||||
goodsInRemissionPreviewList() {
|
||||
return this.abholfachService.AbholfachAbholfachremissionsvorschau();
|
||||
}
|
||||
|
||||
createGoodsInRemissionFromPreviewList() {
|
||||
return this.abholfachService.AbholfachCreateAbholfachremission();
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AbholfachService, AutocompleteTokenDTO, QueryTokenDTO } from '@generated/swagger/oms-api';
|
||||
import { DateAdapter } from '@ui/common';
|
||||
import { memorize } from '@utils/common';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
@Injectable()
|
||||
export class DomainGoodsService {
|
||||
constructor(
|
||||
private abholfachService: AbholfachService,
|
||||
private dateAdapter: DateAdapter,
|
||||
) {}
|
||||
|
||||
searchWareneingang(queryToken: QueryTokenDTO) {
|
||||
return this.abholfachService.AbholfachWareneingang(queryToken);
|
||||
}
|
||||
|
||||
searchWarenausgabe(queryToken: QueryTokenDTO) {
|
||||
return this.abholfachService.AbholfachWarenausgabe(queryToken);
|
||||
}
|
||||
|
||||
wareneingangComplete(autocompleteToken: AutocompleteTokenDTO) {
|
||||
return this.abholfachService.AbholfachWareneingangAutocomplete(autocompleteToken);
|
||||
}
|
||||
|
||||
warenausgabeComplete(autocompleteToken: AutocompleteTokenDTO) {
|
||||
return this.abholfachService.AbholfachWarenausgabeAutocomplete(autocompleteToken);
|
||||
}
|
||||
|
||||
getWareneingangItemByOrderNumber(orderNumber: string) {
|
||||
return this.abholfachService.AbholfachWareneingang({
|
||||
filter: { all_branches: 'true', archive: 'true' },
|
||||
input: {
|
||||
qs: orderNumber,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getWarenausgabeItemByOrderNumber(orderNumber: string, archive: boolean) {
|
||||
return this.abholfachService.AbholfachWarenausgabe({
|
||||
filter: { all_branches: 'true', archive: `${archive}` },
|
||||
input: {
|
||||
qs: orderNumber,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getWarenausgabeItemByCompartment(compartmentCode: string, archive: boolean) {
|
||||
return this.abholfachService.AbholfachWarenausgabe({
|
||||
filter: { all_branches: 'true', archive: `${archive}` },
|
||||
input: {
|
||||
qs: compartmentCode,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getWareneingangItemByCustomerNumber(customerNumber: string) {
|
||||
// Suche anhand der Kundennummer mit Status Bestellt, nachbestellt, eingetroffen, weitergeleitet intern
|
||||
return this.abholfachService.AbholfachWareneingang({
|
||||
filter: { orderitemprocessingstatus: '16;128;8192;1048576' },
|
||||
input: {
|
||||
customer_no: customerNumber,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
list() {
|
||||
const base = this.dateAdapter.today();
|
||||
const startDate = this.dateAdapter.addCalendarDays(base, -5);
|
||||
const endDate = this.dateAdapter.addCalendarDays(base, 1);
|
||||
const queryToken: QueryTokenDTO = {
|
||||
filter: {
|
||||
orderitemprocessingstatus: '16;8192;1024;512;2048',
|
||||
estimatedshippingdate: `"${startDate.toJSON()}"-"${endDate.toJSON()}"`,
|
||||
},
|
||||
orderBy: [{ by: 'estimatedshippingdate' }],
|
||||
skip: 0,
|
||||
take: 20,
|
||||
};
|
||||
return this.searchWareneingang(queryToken);
|
||||
}
|
||||
|
||||
@memorize()
|
||||
goodsInQuerySettings() {
|
||||
return this.abholfachService.AbholfachWareneingangQuerySettings().pipe(shareReplay());
|
||||
}
|
||||
|
||||
@memorize()
|
||||
goodsOutQuerySettings() {
|
||||
return this.abholfachService.AbholfachWarenausgabeQuerySettings().pipe(shareReplay());
|
||||
}
|
||||
|
||||
goodsInList(queryToken: QueryTokenDTO) {
|
||||
return this.abholfachService.AbholfachWareneingangsliste(queryToken);
|
||||
}
|
||||
|
||||
@memorize()
|
||||
goodsInListQuerySettings() {
|
||||
return this.abholfachService.AbholfachWareneingangslisteQuerySettings().pipe(shareReplay());
|
||||
}
|
||||
|
||||
goodsInCleanupList() {
|
||||
return this.abholfachService.AbholfachAbholfachbereinigungsliste();
|
||||
}
|
||||
|
||||
goodsInReservationList(queryToken: QueryTokenDTO) {
|
||||
return this.abholfachService.AbholfachReservierungen(queryToken);
|
||||
}
|
||||
|
||||
goodsInRemissionPreviewList() {
|
||||
return this.abholfachService.AbholfachAbholfachremissionsvorschau();
|
||||
}
|
||||
|
||||
createGoodsInRemissionFromPreviewList() {
|
||||
return this.abholfachService.AbholfachCreateAbholfachremission();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,6 @@ 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';
|
||||
|
||||
14
apps/isa-app/src/domain/oms/oms.module.ts
Normal file
14
apps/isa-app/src/domain/oms/oms.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
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],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,381 +1,316 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
BranchService,
|
||||
BuyerDTO,
|
||||
ChangeStockStatusCodeValues,
|
||||
HistoryDTO,
|
||||
NotificationChannel,
|
||||
OrderCheckoutService,
|
||||
OrderDTO,
|
||||
OrderItemDTO,
|
||||
OrderItemSubsetDTO,
|
||||
OrderListItemDTO,
|
||||
OrderService,
|
||||
ReceiptService,
|
||||
StatusValues,
|
||||
StockStatusCodeService,
|
||||
ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO,
|
||||
ValueTupleOfOrderItemSubsetDTOAndOrderItemSubsetDTO,
|
||||
VATService,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { memorize } from '@utils/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainOmsService {
|
||||
constructor(
|
||||
private orderService: OrderService,
|
||||
private receiptService: ReceiptService,
|
||||
private branchService: BranchService,
|
||||
private vatService: VATService,
|
||||
private stockStatusCodeService: StockStatusCodeService,
|
||||
private _orderCheckoutService: OrderCheckoutService,
|
||||
) {}
|
||||
|
||||
getOrderItemsByCustomerNumber(
|
||||
customerNumber: string,
|
||||
skip: number,
|
||||
): Observable<OrderListItemDTO[]> {
|
||||
return this.orderService
|
||||
.OrderGetOrdersByBuyerNumber({
|
||||
buyerNumber: customerNumber,
|
||||
take: 20,
|
||||
skip,
|
||||
})
|
||||
.pipe(map((orders) => orders.result));
|
||||
}
|
||||
|
||||
getOrder(orderId: number): Observable<OrderDTO> {
|
||||
return this.orderService.OrderGetOrder(orderId).pipe(map((o) => o.result));
|
||||
}
|
||||
|
||||
getBranches() {
|
||||
return this.branchService.BranchGetBranches({});
|
||||
}
|
||||
|
||||
getHistory(orderItemSubsetId: number): Observable<HistoryDTO[]> {
|
||||
return this.orderService
|
||||
.OrderGetOrderItemStatusHistory({ orderItemSubsetId })
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
getReceipts(
|
||||
orderItemSubsetIds: number[],
|
||||
): Observable<
|
||||
ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO[]
|
||||
> {
|
||||
return this.receiptService
|
||||
.ReceiptGetReceiptsByOrderItemSubset({
|
||||
payload: {
|
||||
receiptType: 65 as unknown as any,
|
||||
ids: orderItemSubsetIds,
|
||||
eagerLoading: 1,
|
||||
},
|
||||
})
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
getReorderReasons() {
|
||||
return this._orderCheckoutService
|
||||
.OrderCheckoutGetReorderReasons()
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getVATs() {
|
||||
return this.vatService
|
||||
.VATGetVATs({})
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
// ttl 4 Stunden
|
||||
@memorize({ ttl: 14400000 })
|
||||
getStockStatusCodes({
|
||||
supplierId,
|
||||
eagerLoading = 0,
|
||||
}: {
|
||||
supplierId: number;
|
||||
eagerLoading?: number;
|
||||
}) {
|
||||
return this.stockStatusCodeService
|
||||
.StockStatusCodeGetStockStatusCodes({ supplierId, eagerLoading })
|
||||
.pipe(
|
||||
map((response) => response.result),
|
||||
shareReplay(),
|
||||
);
|
||||
}
|
||||
|
||||
patchOrderItem(payload: {
|
||||
orderItemId: number;
|
||||
orderId: number;
|
||||
orderItem: Partial<OrderItemDTO>;
|
||||
}) {
|
||||
return this.orderService
|
||||
.OrderPatchOrderItem(payload)
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
patchOrderItemSubset(payload: {
|
||||
orderItemSubsetId: number;
|
||||
orderItemId: number;
|
||||
orderId: number;
|
||||
orderItemSubset: Partial<OrderItemSubsetDTO>;
|
||||
}) {
|
||||
return this.orderService
|
||||
.OrderPatchOrderItemSubset(payload)
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
patchComment({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
specialComment,
|
||||
}: {
|
||||
orderId: number;
|
||||
orderItemId: number;
|
||||
orderItemSubsetId: number;
|
||||
specialComment: string;
|
||||
}) {
|
||||
return this.orderService
|
||||
.OrderPatchOrderItemSubset({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
orderItemSubset: {
|
||||
specialComment,
|
||||
},
|
||||
})
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
changeOrderStatus(
|
||||
orderId: number,
|
||||
orderItemId: number,
|
||||
orderItemSubsetId: number,
|
||||
data: StatusValues,
|
||||
): Observable<ValueTupleOfOrderItemSubsetDTOAndOrderItemSubsetDTO> {
|
||||
return this.orderService
|
||||
.OrderChangeStatus({
|
||||
data,
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
})
|
||||
.pipe(map((o) => o.result));
|
||||
}
|
||||
|
||||
setEstimatedShippingDate(
|
||||
orderId: number,
|
||||
orderItemId: number,
|
||||
orderItemSubsetId: number,
|
||||
estimatedShippingDate: Date | string,
|
||||
) {
|
||||
return this.orderService
|
||||
.OrderPatchOrderItemSubset({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
orderItemSubset: {
|
||||
estimatedShippingDate:
|
||||
estimatedShippingDate instanceof Date
|
||||
? estimatedShippingDate.toJSON()
|
||||
: estimatedShippingDate,
|
||||
},
|
||||
})
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
setPickUpDeadline(
|
||||
orderId: number,
|
||||
orderItemId: number,
|
||||
orderItemSubsetId: number,
|
||||
pickUpDeadline: string,
|
||||
) {
|
||||
return this.orderService
|
||||
.OrderPatchOrderItemSubset({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
orderItemSubset: {
|
||||
compartmentStop: pickUpDeadline,
|
||||
},
|
||||
})
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
setPreferredPickUpDate({ data }: { data: { [key: string]: string } }) {
|
||||
return this.orderService.OrderSetPreferredPickUpDate({ data });
|
||||
}
|
||||
|
||||
changeOrderItemStatus(data: OrderService.OrderChangeStatusParams) {
|
||||
return this.orderService.OrderChangeStatus(data);
|
||||
}
|
||||
|
||||
changeStockStatusCode(payload: ChangeStockStatusCodeValues[]) {
|
||||
return this.orderService
|
||||
.OrderChangeStockStatusCode(payload)
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
orderAtSupplier({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
}: {
|
||||
orderId: number;
|
||||
orderItemId: number;
|
||||
orderItemSubsetId: number;
|
||||
}) {
|
||||
return this._orderCheckoutService.OrderCheckoutOrderSubsetItemAtSupplier({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
});
|
||||
}
|
||||
|
||||
getNotifications(
|
||||
orderId: number,
|
||||
): Observable<{
|
||||
selected: NotificationChannel;
|
||||
email: string;
|
||||
mobile: string;
|
||||
}> {
|
||||
return this.getOrder(orderId).pipe(
|
||||
map((order) => ({
|
||||
selected: order.notificationChannels,
|
||||
email: order.buyer?.communicationDetails?.email,
|
||||
mobile: order.buyer?.communicationDetails?.mobile,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
getOrderSource(orderId: number): Observable<string> {
|
||||
return this.getOrder(orderId).pipe(
|
||||
map((order) => order?.features?.orderSource),
|
||||
);
|
||||
}
|
||||
|
||||
updateNotifications(
|
||||
orderId: number,
|
||||
changes: { selected: NotificationChannel; email: string; mobile: string },
|
||||
) {
|
||||
const communicationDetails = {
|
||||
email: changes.email,
|
||||
mobile: changes.mobile,
|
||||
};
|
||||
|
||||
if (!(changes.selected & 1)) {
|
||||
delete communicationDetails.email;
|
||||
}
|
||||
if (!(changes.selected & 2)) {
|
||||
delete communicationDetails.mobile;
|
||||
}
|
||||
|
||||
return this.updateOrder({
|
||||
orderId,
|
||||
notificationChannels: changes.selected,
|
||||
communicationDetails,
|
||||
});
|
||||
}
|
||||
|
||||
updateOrder({
|
||||
orderId,
|
||||
notificationChannels,
|
||||
communicationDetails,
|
||||
firstName,
|
||||
lastName,
|
||||
organisation,
|
||||
}: {
|
||||
orderId: number;
|
||||
notificationChannels?: NotificationChannel;
|
||||
communicationDetails?: { email?: string; mobile?: string };
|
||||
lastName?: string;
|
||||
firstName?: string;
|
||||
organisation?: string;
|
||||
}) {
|
||||
const buyer: BuyerDTO = {};
|
||||
|
||||
if (communicationDetails) {
|
||||
buyer.communicationDetails = { ...communicationDetails };
|
||||
}
|
||||
|
||||
if (!!lastName || !!firstName) {
|
||||
buyer.lastName = lastName;
|
||||
buyer.firstName = firstName;
|
||||
}
|
||||
|
||||
if (!!organisation && !!buyer.organisation) {
|
||||
buyer.organisation = {
|
||||
name: organisation,
|
||||
};
|
||||
}
|
||||
|
||||
return this.orderService
|
||||
.OrderPatchOrder({
|
||||
orderId: orderId,
|
||||
order: {
|
||||
notificationChannels,
|
||||
buyer,
|
||||
},
|
||||
})
|
||||
.pipe(map((res) => res.result));
|
||||
}
|
||||
|
||||
generateNotifications({
|
||||
orderId,
|
||||
taskTypes,
|
||||
}: {
|
||||
orderId: number;
|
||||
taskTypes: string[];
|
||||
}) {
|
||||
return this.orderService.OrderRegenerateOrderItemStatusTasks({
|
||||
orderId,
|
||||
taskTypes,
|
||||
});
|
||||
}
|
||||
|
||||
getCompletedTasks({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
take,
|
||||
skip,
|
||||
}: {
|
||||
orderId: number;
|
||||
orderItemId: number;
|
||||
orderItemSubsetId: number;
|
||||
take?: number;
|
||||
skip?: number;
|
||||
}): Observable<Record<string, Date[]>> {
|
||||
return this.orderService
|
||||
.OrderGetOrderItemSubsetTasks({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
completed: new Date(0).toISOString(),
|
||||
take,
|
||||
skip,
|
||||
})
|
||||
.pipe(
|
||||
map((res) =>
|
||||
res.result
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.completed).getTime() -
|
||||
new Date(a.completed).getTime(),
|
||||
)
|
||||
.reduce(
|
||||
(data, result) => {
|
||||
(data[result.name] = data[result.name] || []).push(
|
||||
new Date(result.completed),
|
||||
);
|
||||
return data;
|
||||
},
|
||||
{} as Record<string, Date[]>,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
BranchService,
|
||||
BuyerDTO,
|
||||
ChangeStockStatusCodeValues,
|
||||
HistoryDTO,
|
||||
NotificationChannel,
|
||||
OrderCheckoutService,
|
||||
OrderDTO,
|
||||
OrderItemDTO,
|
||||
OrderItemSubsetDTO,
|
||||
OrderListItemDTO,
|
||||
OrderService,
|
||||
ReceiptService,
|
||||
StatusValues,
|
||||
StockStatusCodeService,
|
||||
ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO,
|
||||
ValueTupleOfOrderItemSubsetDTOAndOrderItemSubsetDTO,
|
||||
VATService,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { memorize } from '@utils/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class DomainOmsService {
|
||||
constructor(
|
||||
private orderService: OrderService,
|
||||
private receiptService: ReceiptService,
|
||||
private branchService: BranchService,
|
||||
private vatService: VATService,
|
||||
private stockStatusCodeService: StockStatusCodeService,
|
||||
private _orderCheckoutService: OrderCheckoutService,
|
||||
) {}
|
||||
|
||||
getOrderItemsByCustomerNumber(customerNumber: string, skip: number): Observable<OrderListItemDTO[]> {
|
||||
return this.orderService
|
||||
.OrderGetOrdersByBuyerNumber({ buyerNumber: customerNumber, take: 20, skip })
|
||||
.pipe(map((orders) => orders.result));
|
||||
}
|
||||
|
||||
getOrder(orderId: number): Observable<OrderDTO> {
|
||||
return this.orderService.OrderGetOrder(orderId).pipe(map((o) => o.result));
|
||||
}
|
||||
|
||||
getBranches() {
|
||||
return this.branchService.BranchGetBranches({});
|
||||
}
|
||||
|
||||
getHistory(orderItemSubsetId: number): Observable<HistoryDTO[]> {
|
||||
return this.orderService
|
||||
.OrderGetOrderItemStatusHistory({ orderItemSubsetId })
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
getReceipts(
|
||||
orderItemSubsetIds: number[],
|
||||
): Observable<ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO[]> {
|
||||
return this.receiptService
|
||||
.ReceiptGetReceiptsByOrderItemSubset({
|
||||
payload: {
|
||||
receiptType: 65 as unknown as any,
|
||||
ids: orderItemSubsetIds,
|
||||
eagerLoading: 1,
|
||||
},
|
||||
})
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
getReorderReasons() {
|
||||
return this._orderCheckoutService.OrderCheckoutGetReorderReasons().pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getVATs() {
|
||||
return this.vatService.VATGetVATs({}).pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
// ttl 4 Stunden
|
||||
@memorize({ ttl: 14400000 })
|
||||
getStockStatusCodes({ supplierId, eagerLoading = 0 }: { supplierId: number; eagerLoading?: number }) {
|
||||
return this.stockStatusCodeService.StockStatusCodeGetStockStatusCodes({ supplierId, eagerLoading }).pipe(
|
||||
map((response) => response.result),
|
||||
shareReplay(),
|
||||
);
|
||||
}
|
||||
|
||||
patchOrderItem(payload: { orderItemId: number; orderId: number; orderItem: Partial<OrderItemDTO> }) {
|
||||
return this.orderService.OrderPatchOrderItem(payload).pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
patchOrderItemSubset(payload: {
|
||||
orderItemSubsetId: number;
|
||||
orderItemId: number;
|
||||
orderId: number;
|
||||
orderItemSubset: Partial<OrderItemSubsetDTO>;
|
||||
}) {
|
||||
return this.orderService.OrderPatchOrderItemSubset(payload).pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
patchComment({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
specialComment,
|
||||
}: {
|
||||
orderId: number;
|
||||
orderItemId: number;
|
||||
orderItemSubsetId: number;
|
||||
specialComment: string;
|
||||
}) {
|
||||
return this.orderService
|
||||
.OrderPatchOrderItemSubset({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
orderItemSubset: {
|
||||
specialComment,
|
||||
},
|
||||
})
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
changeOrderStatus(
|
||||
orderId: number,
|
||||
orderItemId: number,
|
||||
orderItemSubsetId: number,
|
||||
data: StatusValues,
|
||||
): Observable<ValueTupleOfOrderItemSubsetDTOAndOrderItemSubsetDTO> {
|
||||
return this.orderService
|
||||
.OrderChangeStatus({
|
||||
data,
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
})
|
||||
.pipe(map((o) => o.result));
|
||||
}
|
||||
|
||||
setEstimatedShippingDate(
|
||||
orderId: number,
|
||||
orderItemId: number,
|
||||
orderItemSubsetId: number,
|
||||
estimatedShippingDate: Date | string,
|
||||
) {
|
||||
return this.orderService
|
||||
.OrderPatchOrderItemSubset({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
orderItemSubset: {
|
||||
estimatedShippingDate:
|
||||
estimatedShippingDate instanceof Date ? estimatedShippingDate.toJSON() : estimatedShippingDate,
|
||||
},
|
||||
})
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
setPickUpDeadline(orderId: number, orderItemId: number, orderItemSubsetId: number, pickUpDeadline: string) {
|
||||
return this.orderService
|
||||
.OrderPatchOrderItemSubset({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
orderItemSubset: {
|
||||
compartmentStop: pickUpDeadline,
|
||||
},
|
||||
})
|
||||
.pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
setPreferredPickUpDate({ data }: { data: { [key: string]: string } }) {
|
||||
return this.orderService.OrderSetPreferredPickUpDate({ data });
|
||||
}
|
||||
|
||||
changeOrderItemStatus(data: OrderService.OrderChangeStatusParams) {
|
||||
return this.orderService.OrderChangeStatus(data);
|
||||
}
|
||||
|
||||
changeStockStatusCode(payload: ChangeStockStatusCodeValues[]) {
|
||||
return this.orderService.OrderChangeStockStatusCode(payload).pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
orderAtSupplier({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
}: {
|
||||
orderId: number;
|
||||
orderItemId: number;
|
||||
orderItemSubsetId: number;
|
||||
}) {
|
||||
return this._orderCheckoutService.OrderCheckoutOrderSubsetItemAtSupplier({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
});
|
||||
}
|
||||
|
||||
getNotifications(orderId: number): Observable<{ selected: NotificationChannel; email: string; mobile: string }> {
|
||||
return this.getOrder(orderId).pipe(
|
||||
map((order) => ({
|
||||
selected: order.notificationChannels,
|
||||
email: order.buyer?.communicationDetails?.email,
|
||||
mobile: order.buyer?.communicationDetails?.mobile,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
getOrderSource(orderId: number): Observable<string> {
|
||||
return this.getOrder(orderId).pipe(map((order) => order?.features?.orderSource));
|
||||
}
|
||||
|
||||
updateNotifications(orderId: number, changes: { selected: NotificationChannel; email: string; mobile: string }) {
|
||||
const communicationDetails = {
|
||||
email: changes.email,
|
||||
mobile: changes.mobile,
|
||||
};
|
||||
|
||||
if (!(changes.selected & 1)) {
|
||||
delete communicationDetails.email;
|
||||
}
|
||||
if (!(changes.selected & 2)) {
|
||||
delete communicationDetails.mobile;
|
||||
}
|
||||
|
||||
return this.updateOrder({ orderId, notificationChannels: changes.selected, communicationDetails });
|
||||
}
|
||||
|
||||
updateOrder({
|
||||
orderId,
|
||||
notificationChannels,
|
||||
communicationDetails,
|
||||
firstName,
|
||||
lastName,
|
||||
organisation,
|
||||
}: {
|
||||
orderId: number;
|
||||
notificationChannels?: NotificationChannel;
|
||||
communicationDetails?: { email?: string; mobile?: string };
|
||||
lastName?: string;
|
||||
firstName?: string;
|
||||
organisation?: string;
|
||||
}) {
|
||||
const buyer: BuyerDTO = {};
|
||||
|
||||
if (communicationDetails) {
|
||||
buyer.communicationDetails = { ...communicationDetails };
|
||||
}
|
||||
|
||||
if (!!lastName || !!firstName) {
|
||||
buyer.lastName = lastName;
|
||||
buyer.firstName = firstName;
|
||||
}
|
||||
|
||||
if (!!organisation && !!buyer.organisation) {
|
||||
buyer.organisation = {
|
||||
name: organisation,
|
||||
};
|
||||
}
|
||||
|
||||
return this.orderService
|
||||
.OrderPatchOrder({
|
||||
orderId: orderId,
|
||||
order: {
|
||||
notificationChannels,
|
||||
buyer,
|
||||
},
|
||||
})
|
||||
.pipe(map((res) => res.result));
|
||||
}
|
||||
|
||||
generateNotifications({ orderId, taskTypes }: { orderId: number; taskTypes: string[] }) {
|
||||
return this.orderService.OrderRegenerateOrderItemStatusTasks({
|
||||
orderId,
|
||||
taskTypes,
|
||||
});
|
||||
}
|
||||
|
||||
getCompletedTasks({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
take,
|
||||
skip,
|
||||
}: {
|
||||
orderId: number;
|
||||
orderItemId: number;
|
||||
orderItemSubsetId: number;
|
||||
take?: number;
|
||||
skip?: number;
|
||||
}): Observable<Record<string, Date[]>> {
|
||||
return this.orderService
|
||||
.OrderGetOrderItemSubsetTasks({
|
||||
orderId,
|
||||
orderItemId,
|
||||
orderItemSubsetId,
|
||||
completed: new Date(0).toISOString(),
|
||||
take,
|
||||
skip,
|
||||
})
|
||||
.pipe(
|
||||
map((res) =>
|
||||
res.result
|
||||
.sort((a, b) => new Date(b.completed).getTime() - new Date(a.completed).getTime())
|
||||
.reduce(
|
||||
(data, result) => {
|
||||
(data[result.name] = data[result.name] || []).push(new Date(result.completed));
|
||||
return data;
|
||||
},
|
||||
{} as Record<string, Date[]>,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
ReceiptOrderItemSubsetReferenceValues,
|
||||
ReceiptService,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { memorize } from '@utils/common';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainReceiptService {
|
||||
constructor(private receiptService: ReceiptService) {}
|
||||
|
||||
createShippingNotes(params: ReceiptService.ReceiptCreateShippingNote2Params) {
|
||||
return this.receiptService.ReceiptCreateShippingNote2(params);
|
||||
}
|
||||
|
||||
@memorize({ ttl: 1000 })
|
||||
getReceipts(payload: ReceiptOrderItemSubsetReferenceValues) {
|
||||
return this.receiptService
|
||||
.ReceiptGetReceiptsByOrderItemSubset({
|
||||
payload: payload,
|
||||
})
|
||||
.pipe(shareReplay(1));
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ReceiptOrderItemSubsetReferenceValues, ReceiptService } from '@generated/swagger/oms-api';
|
||||
import { memorize } from '@utils/common';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class DomainReceiptService {
|
||||
constructor(private receiptService: ReceiptService) {}
|
||||
|
||||
createShippingNotes(params: ReceiptService.ReceiptCreateShippingNote2Params) {
|
||||
return this.receiptService.ReceiptCreateShippingNote2(params);
|
||||
}
|
||||
|
||||
@memorize({ ttl: 1000 })
|
||||
getReceipts(payload: ReceiptOrderItemSubsetReferenceValues) {
|
||||
return this.receiptService
|
||||
.ReceiptGetReceiptsByOrderItemSubset({
|
||||
payload: payload,
|
||||
})
|
||||
.pipe(shareReplay(1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './defs';
|
||||
export * from './mappings';
|
||||
export * from './remission.module';
|
||||
export * from './remission.service';
|
||||
|
||||
17
apps/isa-app/src/domain/remission/remission.module.ts
Normal file
17
apps/isa-app/src/domain/remission/remission.module.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
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 {}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,43 +1,32 @@
|
||||
import { enableProdMode, isDevMode } from '@angular/core';
|
||||
import { CONFIG_DATA } from '@isa/core/config';
|
||||
import { setDefaultOptions } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||
import * as moment from 'moment';
|
||||
import 'moment/locale/de';
|
||||
|
||||
setDefaultOptions({ locale: de });
|
||||
moment.locale('de');
|
||||
|
||||
registerLocaleData(localeDe, localeDeExtra);
|
||||
registerLocaleData(localeDe, 'de', localeDeExtra);
|
||||
|
||||
import { App } from './app/app';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
|
||||
if (!isDevMode()) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const configRes = await fetch('/config/config.json');
|
||||
|
||||
const config = await configRes.json();
|
||||
|
||||
await bootstrapApplication(App, {
|
||||
...appConfig,
|
||||
providers: [
|
||||
{ provide: CONFIG_DATA, useValue: config },
|
||||
...appConfig.providers,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
bootstrap();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
import { enableProdMode, isDevMode } from "@angular/core";
|
||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||
import { CONFIG_DATA } from "@isa/core/config";
|
||||
import { setDefaultOptions } from "date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
import * as moment from "moment";
|
||||
import "moment/locale/de";
|
||||
|
||||
setDefaultOptions({ locale: de });
|
||||
moment.locale("de");
|
||||
|
||||
import { AppModule } from "./app/app.module";
|
||||
|
||||
if (!isDevMode()) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const configRes = await fetch("/config/config.json");
|
||||
|
||||
const config = await configRes.json();
|
||||
|
||||
platformBrowserDynamic([
|
||||
{ provide: CONFIG_DATA, useValue: config },
|
||||
]).bootstrapModule(AppModule);
|
||||
}
|
||||
|
||||
try {
|
||||
bootstrap();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
@@ -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.getSelectedBranch$(this.applicationService.activatedProcessId),
|
||||
this.domainAvailabilityService.getDefaultBranch(),
|
||||
]).pipe(map(([selectedBranch, defaultBranch]) => selectedBranch || defaultBranch));
|
||||
|
||||
|
||||
@@ -192,7 +192,11 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
);
|
||||
|
||||
selectedBranchId$ = this.applicationService.getSelectedBranch$();
|
||||
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
|
||||
switchMap((processId) =>
|
||||
this.applicationService.getSelectedBranch$(processId),
|
||||
),
|
||||
);
|
||||
|
||||
get isTablet$() {
|
||||
return this._environment.matchTablet$;
|
||||
@@ -324,7 +328,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
debounceTime(0),
|
||||
switchMap((params) =>
|
||||
this.applicationService
|
||||
.getSelectedBranch$()
|
||||
.getSelectedBranch$(Number(params.processId))
|
||||
.pipe(map((selectedBranch) => ({ params, selectedBranch }))),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -98,9 +98,11 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
this.subscriptions.add(
|
||||
this.application
|
||||
.getSelectedBranch$()
|
||||
.pipe(debounceTime(0))
|
||||
this.application.activatedProcessId$
|
||||
.pipe(
|
||||
debounceTime(0),
|
||||
switchMap((processId) => this.application.getSelectedBranch$(processId)),
|
||||
)
|
||||
.subscribe((selectedBranch) => {
|
||||
const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id;
|
||||
if (branchChanged) {
|
||||
@@ -141,7 +143,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
|
||||
const clean = { ...params };
|
||||
|
||||
for (const key in clean) {
|
||||
if (key === 'main_qs') {
|
||||
if (key === 'main_qs' || key?.includes('order_by')) {
|
||||
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 = false;
|
||||
@Input() selected: boolean = false;
|
||||
|
||||
@Input()
|
||||
get selectable() {
|
||||
@@ -91,7 +91,9 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
|
||||
defaultBranch$ = this._availability.getDefaultBranch();
|
||||
|
||||
selectedBranchId$ = this.applicationService.getSelectedBranch$();
|
||||
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.applicationService.getSelectedBranch$(processId)),
|
||||
);
|
||||
|
||||
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$().pipe(
|
||||
this.application.getSelectedBranch$(processId).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 { ThumbnailUrlPipe } from '@domain/catalog';
|
||||
import { DomainCatalogModule } 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,
|
||||
ThumbnailUrlPipe,
|
||||
DomainCatalogModule,
|
||||
UiCommonModule,
|
||||
UiIconModule,
|
||||
UiSelectBulletModule,
|
||||
|
||||
@@ -77,7 +77,9 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
ngOnInit() {
|
||||
this.activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId)));
|
||||
|
||||
this.selectedBranch$ = this.application.getSelectedBranch$();
|
||||
this.selectedBranch$ = this.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.application.getSelectedBranch$(Number(processId))),
|
||||
);
|
||||
|
||||
this.stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranch$]).pipe(
|
||||
map(([defaultBranch, selectedBranch]) => {
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BranchSelectorComponent } from '@shared/components/branch-selector';
|
||||
import { BreadcrumbModule } from '@shared/components/breadcrumb';
|
||||
import { ArticleDetailsModule } from './article-details/article-details.module';
|
||||
import { ArticleSearchModule } from './article-search/article-search.module';
|
||||
import { PageCatalogRoutingModule } from './page-catalog-routing.module';
|
||||
import { PageCatalogComponent } from './page-catalog.component';
|
||||
import { SharedSplitscreenComponent } from '@shared/components/splitscreen';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
PageCatalogRoutingModule,
|
||||
ArticleDetailsModule,
|
||||
ArticleSearchModule,
|
||||
BreadcrumbModule,
|
||||
BranchSelectorComponent,
|
||||
SharedSplitscreenComponent,
|
||||
UiCommonModule,
|
||||
UiTooltipModule,
|
||||
],
|
||||
exports: [],
|
||||
declarations: [PageCatalogComponent],
|
||||
})
|
||||
export class PageCatalogModule {}
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BranchSelectorComponent } from '@shared/components/branch-selector';
|
||||
import { BreadcrumbModule } from '@shared/components/breadcrumb';
|
||||
import { ArticleDetailsModule } from './article-details/article-details.module';
|
||||
import { ArticleSearchModule } from './article-search/article-search.module';
|
||||
import { PageCatalogRoutingModule } from './page-catalog-routing.module';
|
||||
import { PageCatalogComponent } from './page-catalog.component';
|
||||
import { SharedSplitscreenComponent } from '@shared/components/splitscreen';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
PageCatalogRoutingModule,
|
||||
ArticleSearchModule,
|
||||
ArticleDetailsModule,
|
||||
BreadcrumbModule,
|
||||
BranchSelectorComponent,
|
||||
SharedSplitscreenComponent,
|
||||
UiCommonModule,
|
||||
UiTooltipModule,
|
||||
],
|
||||
exports: [],
|
||||
declarations: [PageCatalogComponent],
|
||||
})
|
||||
export class PageCatalogModule {}
|
||||
|
||||
@@ -73,9 +73,11 @@ export class CustomerOrderSearchMainComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
this._subscriptions.add(
|
||||
this._application
|
||||
.getSelectedBranch$()
|
||||
.pipe(debounceTime(0))
|
||||
this._application.activatedProcessId$
|
||||
.pipe(
|
||||
debounceTime(0),
|
||||
switchMap((processId) => this._application.getSelectedBranch$(processId)),
|
||||
)
|
||||
.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$()
|
||||
.getSelectedBranch$(processId)
|
||||
.pipe(map((selectedBranch) => ({ processId, params, selectedBranch }))),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -49,7 +49,9 @@ export class CustomerOrderComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.selectedBranch$ = this.application.getSelectedBranch$();
|
||||
this.selectedBranch$ = this.application.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.application.getSelectedBranch$(Number(processId))),
|
||||
);
|
||||
|
||||
/* Ticket #4544 - Suchrequest abbrechen bei Prozesswechsel
|
||||
/ um zu verhindern, dass die Suche in einen anderen Kundenbestellungen Prozess übernommen wird
|
||||
|
||||
@@ -9,8 +9,7 @@ import { DecimalPipe } from '@angular/common';
|
||||
import { Component, Input, OnInit, inject } from '@angular/core';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { BonusCardInfoDTO } from '@generated/swagger/crm-api';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
import { injectTabId, TabService } from '@isa/core/tabs';
|
||||
import { Router } from '@angular/router';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
|
||||
@@ -47,7 +46,7 @@ import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
export class KundenkarteComponent implements OnInit {
|
||||
#tabId = injectTabId();
|
||||
#router = inject(Router);
|
||||
#navigationState = inject(NavigationStateService);
|
||||
#tabService = inject(TabService);
|
||||
#customerNavigationService = inject(CustomerSearchNavigation);
|
||||
|
||||
@Input() cardDetails: BonusCardInfoDTO;
|
||||
@@ -69,13 +68,12 @@ export class KundenkarteComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#navigationState.preserveContext(
|
||||
{
|
||||
this.#tabService.patchTabMetadata(tabId, {
|
||||
'select-customer': {
|
||||
returnUrl: `/${tabId}/reward`,
|
||||
autoTriggerContinueFn: true,
|
||||
},
|
||||
'select-customer',
|
||||
);
|
||||
});
|
||||
|
||||
await this.#router.navigate(
|
||||
this.#customerNavigationService.detailsRoute({
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnDestroy,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
@@ -9,8 +15,17 @@ import { map } from 'rxjs/operators';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class CustomerComponent {
|
||||
export class CustomerComponent implements OnDestroy {
|
||||
private _tabService = inject(TabService);
|
||||
processId$ = this._activatedRoute.data.pipe(map((data) => data.processId));
|
||||
|
||||
constructor(private _activatedRoute: ActivatedRoute) {}
|
||||
|
||||
ngOnDestroy() {
|
||||
const tab = this._tabService.activatedTab();
|
||||
// #5512 Always clear preserved select-customer context if navigating out of customer area
|
||||
this._tabService.patchTabMetadata(tab.id, {
|
||||
'select-customer': null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,11 @@ export class DetailsMainViewBillingAddressesComponent
|
||||
customer as unknown as Customer,
|
||||
),
|
||||
);
|
||||
// Clear the selected payer ID when using customer address
|
||||
this.crmTabMetadataService.setSelectedPayerId(
|
||||
this.tabId(),
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,6 +191,11 @@ export class DetailsMainViewDeliveryAddressesComponent
|
||||
customer as unknown as Customer,
|
||||
),
|
||||
);
|
||||
// Clear the selected shipping address ID when using customer address
|
||||
this.crmTabMetadataService.setSelectedShippingAddressId(
|
||||
this.tabId(),
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,9 +49,14 @@ import {
|
||||
NavigateAfterRewardSelection,
|
||||
RewardSelectionPopUpService,
|
||||
} from '@isa/checkout/shared/reward-selection-dialog';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { ShippingAddressDTO as CrmShippingAddressDTO } from '@generated/swagger/crm-api';
|
||||
|
||||
interface SelectCustomerContext {
|
||||
returnUrl?: string;
|
||||
autoTriggerContinueFn?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomerDetailsViewMainState {
|
||||
isBusy: boolean;
|
||||
shoppingCart: ShoppingCartDTO;
|
||||
@@ -80,7 +85,7 @@ export class CustomerDetailsViewMainComponent
|
||||
private _router = inject(Router);
|
||||
private _activatedRoute = inject(ActivatedRoute);
|
||||
private _genderSettings = inject(GenderSettingsService);
|
||||
private _navigationState = inject(NavigationStateService);
|
||||
private _tabService = inject(TabService);
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
customerService = inject(CrmCustomerService);
|
||||
@@ -97,18 +102,19 @@ export class CustomerDetailsViewMainComponent
|
||||
map(([fetchingCustomer, fetchingList]) => fetchingCustomer || fetchingList),
|
||||
);
|
||||
|
||||
async getReturnUrlFromContext(): Promise<string | null> {
|
||||
// Get from preserved context (survives intermediate navigations, auto-scoped to tab)
|
||||
const context = await this._navigationState.restoreContext<{
|
||||
returnUrl?: string;
|
||||
}>('select-customer');
|
||||
getReturnUrlFromContext(): string | null {
|
||||
// Get from preserved context (survives intermediate navigations, scoped to tab)
|
||||
const context = this._tabService.activatedTab()?.metadata?.[
|
||||
'select-customer'
|
||||
] as SelectCustomerContext | undefined;
|
||||
|
||||
return context?.returnUrl ?? null;
|
||||
}
|
||||
|
||||
async checkHasReturnUrl(): Promise<void> {
|
||||
const hasContext =
|
||||
await this._navigationState.hasPreservedContext('select-customer');
|
||||
checkHasReturnUrl(): void {
|
||||
const hasContext = !!this._tabService.activatedTab()?.metadata?.[
|
||||
'select-customer'
|
||||
];
|
||||
this.hasReturnUrl.set(hasContext);
|
||||
}
|
||||
|
||||
@@ -321,24 +327,23 @@ export class CustomerDetailsViewMainComponent
|
||||
|
||||
ngOnInit() {
|
||||
// Check if we have a return URL context
|
||||
this.checkHasReturnUrl().then(async () => {
|
||||
// Check if we should auto-trigger continue() (only from Kundenkarte)
|
||||
const context = await this._navigationState.restoreContext<{
|
||||
returnUrl?: string;
|
||||
autoTriggerContinueFn?: boolean;
|
||||
}>('select-customer');
|
||||
this.checkHasReturnUrl();
|
||||
|
||||
if (context?.autoTriggerContinueFn) {
|
||||
// Clear the autoTriggerContinueFn flag immediately (preserves returnUrl automatically)
|
||||
await this._navigationState.patchContext(
|
||||
{ autoTriggerContinueFn: undefined },
|
||||
'select-customer',
|
||||
);
|
||||
// Check if we should auto-trigger continue() (only from Kundenkarte)
|
||||
const tab = this._tabService.activatedTab();
|
||||
const context = tab?.metadata?.['select-customer'] as
|
||||
| SelectCustomerContext
|
||||
| undefined;
|
||||
|
||||
// Auto-trigger continue() ONLY when coming from Kundenkarte
|
||||
this.continue();
|
||||
}
|
||||
});
|
||||
if (context?.autoTriggerContinueFn && tab) {
|
||||
// Clear the autoTriggerContinueFn flag immediately (preserves returnUrl)
|
||||
this._tabService.patchTabMetadata(tab.id, {
|
||||
'select-customer': { ...context, autoTriggerContinueFn: undefined },
|
||||
});
|
||||
|
||||
// Auto-trigger continue() ONLY when coming from Kundenkarte
|
||||
this.continue();
|
||||
}
|
||||
|
||||
this.processId$
|
||||
.pipe(
|
||||
@@ -436,10 +441,18 @@ export class CustomerDetailsViewMainComponent
|
||||
|
||||
// #5262 Check for reward selection flow before navigation
|
||||
if (this.hasReturnUrl()) {
|
||||
// Restore from preserved context (auto-scoped to current tab) and clean up
|
||||
const context = await this._navigationState.restoreAndClearContext<{
|
||||
returnUrl?: string;
|
||||
}>('select-customer');
|
||||
// Restore from preserved context (scoped to current tab) and clean up
|
||||
const tab = this._tabService.activatedTab();
|
||||
const context = tab?.metadata?.['select-customer'] as
|
||||
| SelectCustomerContext
|
||||
| undefined;
|
||||
|
||||
// Clear the context
|
||||
if (tab) {
|
||||
this._tabService.patchTabMetadata(tab.id, {
|
||||
'select-customer': null,
|
||||
});
|
||||
}
|
||||
|
||||
if (context?.returnUrl) {
|
||||
await this._router.navigateByUrl(context.returnUrl);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { CustomerSearchStore } from '../store';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { CustomerMenuComponent } from '../../components/customer-menu';
|
||||
@@ -51,7 +51,7 @@ export class KundenkarteMainViewComponent implements OnDestroy {
|
||||
#cardTransactionsResource = inject(CustomerCardTransactionsResource);
|
||||
elementRef = inject(ElementRef);
|
||||
#router = inject(Router);
|
||||
#navigationState = inject(NavigationStateService);
|
||||
#tabService = inject(TabService);
|
||||
#customerNavigationService = inject(CustomerSearchNavigation);
|
||||
|
||||
/**
|
||||
@@ -120,13 +120,12 @@ export class KundenkarteMainViewComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
// Preserve context for auto-triggering continue() in details view
|
||||
this.#navigationState.preserveContext(
|
||||
{
|
||||
this.#tabService.patchTabMetadata(tabId, {
|
||||
'select-customer': {
|
||||
returnUrl: `/${tabId}/reward`,
|
||||
autoTriggerContinueFn: true,
|
||||
},
|
||||
'select-customer',
|
||||
);
|
||||
});
|
||||
|
||||
// Navigate to customer details - will auto-trigger continue()
|
||||
await this.#router.navigate(
|
||||
|
||||
@@ -1,113 +1,110 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Icon, IconAlias, IconConfig } from './interfaces';
|
||||
import { IconLoader } from './loader';
|
||||
import { Observable, Subject, isObservable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* @deprecated Use UiIconModule from '@isa/ui/icon' instead.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class IconRegistry {
|
||||
private _icons = new Map<string, Icon>();
|
||||
private _aliases = new Map<string, string>();
|
||||
private _fallback: string;
|
||||
private _viewBox: string;
|
||||
|
||||
updated = new Subject<void>();
|
||||
|
||||
private _initComplete = false;
|
||||
|
||||
constructor(private _iconLoader: IconLoader) {
|
||||
this._loadIcons();
|
||||
}
|
||||
|
||||
private async _loadIcons(): Promise<void> {
|
||||
const load = this._iconLoader.getIcons();
|
||||
|
||||
if (load instanceof Promise) {
|
||||
const config = await load;
|
||||
this._init(config);
|
||||
} else if (isObservable(load)) {
|
||||
load.subscribe((config) => {
|
||||
this._init(config);
|
||||
});
|
||||
} else {
|
||||
this._init(load);
|
||||
}
|
||||
}
|
||||
|
||||
private _init(config: IconConfig): void {
|
||||
this.register(...config.icons);
|
||||
this.alias(...config.aliases);
|
||||
this.setViewBox(config.viewBox);
|
||||
this.setFallback(config.fallback);
|
||||
|
||||
this._initComplete = true;
|
||||
|
||||
this.updated.next();
|
||||
}
|
||||
|
||||
register(...icons: Icon[]): IconRegistry {
|
||||
icons?.forEach((icon) => {
|
||||
this._icons.set(icon.name, icon);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setViewBox(viewBox: string): void {
|
||||
this._viewBox = viewBox;
|
||||
}
|
||||
|
||||
alias(...aliases: IconAlias[]): IconRegistry {
|
||||
aliases?.forEach((alias) => {
|
||||
this._aliases.set(alias.alias, alias.name);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setFallback(name: string): void {
|
||||
this._fallback = name;
|
||||
}
|
||||
|
||||
get(name: string): Icon | undefined {
|
||||
const alias = this._aliases.get(name);
|
||||
let iconName = name;
|
||||
if (alias) {
|
||||
iconName = alias;
|
||||
}
|
||||
|
||||
let icon = this._icons.get(iconName);
|
||||
|
||||
if (!icon && this._initComplete) {
|
||||
if (alias) {
|
||||
console.warn(`Not found: Icon with name ${name} (${iconName})`);
|
||||
} else {
|
||||
console.warn(`Unable to find icon: '${name}'`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!icon && this._fallback) {
|
||||
icon = this._icons.get(this._fallback);
|
||||
}
|
||||
|
||||
return { ...icon, viewBox: icon?.viewBox || this._viewBox };
|
||||
}
|
||||
|
||||
get$(name: string): Observable<Icon | undefined> {
|
||||
return new Observable<Icon | undefined>((subscriber) => {
|
||||
let icon = this.get(name);
|
||||
subscriber.next(icon);
|
||||
subscriber.complete();
|
||||
|
||||
const sub = this.updated.subscribe(() => {
|
||||
icon = this.get(name);
|
||||
subscriber.next(icon);
|
||||
subscriber.complete();
|
||||
});
|
||||
|
||||
return () => sub.unsubscribe();
|
||||
});
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Icon, IconAlias, IconConfig } from './interfaces';
|
||||
import { IconLoader } from './loader';
|
||||
import { Observable, Subject, isObservable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class IconRegistry {
|
||||
private _icons = new Map<string, Icon>();
|
||||
private _aliases = new Map<string, string>();
|
||||
private _fallback: string;
|
||||
private _viewBox: string;
|
||||
|
||||
updated = new Subject<void>();
|
||||
|
||||
private _initComplete = false;
|
||||
|
||||
constructor(private _iconLoader: IconLoader) {
|
||||
this._loadIcons();
|
||||
}
|
||||
|
||||
private async _loadIcons(): Promise<void> {
|
||||
const load = this._iconLoader.getIcons();
|
||||
|
||||
if (load instanceof Promise) {
|
||||
const config = await load;
|
||||
this._init(config);
|
||||
} else if (isObservable(load)) {
|
||||
load.subscribe((config) => {
|
||||
this._init(config);
|
||||
});
|
||||
} else {
|
||||
this._init(load);
|
||||
}
|
||||
}
|
||||
|
||||
private _init(config: IconConfig): void {
|
||||
this.register(...config.icons);
|
||||
this.alias(...config.aliases);
|
||||
this.setViewBox(config.viewBox);
|
||||
this.setFallback(config.fallback);
|
||||
|
||||
this._initComplete = true;
|
||||
|
||||
this.updated.next();
|
||||
}
|
||||
|
||||
register(...icons: Icon[]): IconRegistry {
|
||||
icons?.forEach((icon) => {
|
||||
this._icons.set(icon.name, icon);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setViewBox(viewBox: string): void {
|
||||
this._viewBox = viewBox;
|
||||
}
|
||||
|
||||
alias(...aliases: IconAlias[]): IconRegistry {
|
||||
aliases?.forEach((alias) => {
|
||||
this._aliases.set(alias.alias, alias.name);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setFallback(name: string): void {
|
||||
this._fallback = name;
|
||||
}
|
||||
|
||||
get(name: string): Icon | undefined {
|
||||
const alias = this._aliases.get(name);
|
||||
let iconName = name;
|
||||
if (alias) {
|
||||
iconName = alias;
|
||||
}
|
||||
|
||||
let icon = this._icons.get(iconName);
|
||||
|
||||
if (!icon && this._initComplete) {
|
||||
if (alias) {
|
||||
console.warn(`Not found: Icon with name ${name} (${iconName})`);
|
||||
} else {
|
||||
console.warn(`Unable to find icon: '${name}'`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!icon && this._fallback) {
|
||||
icon = this._icons.get(this._fallback);
|
||||
}
|
||||
|
||||
return { ...icon, viewBox: icon?.viewBox || this._viewBox };
|
||||
}
|
||||
|
||||
get$(name: string): Observable<Icon | undefined> {
|
||||
return new Observable<Icon | undefined>((subscriber) => {
|
||||
let icon = this.get(name);
|
||||
subscriber.next(icon);
|
||||
subscriber.complete();
|
||||
|
||||
const sub = this.updated.subscribe(() => {
|
||||
icon = this.get(name);
|
||||
subscriber.next(icon);
|
||||
subscriber.complete();
|
||||
});
|
||||
|
||||
return () => sub.unsubscribe();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +1,66 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { IconRegistry } from './icon-registry';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* @deprecated Use UiIconModule from '@isa/ui/icon' instead.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'shared-icon',
|
||||
template: `
|
||||
<svg
|
||||
[style.width.rem]="size / 16"
|
||||
[style.height.rem]="size / 16"
|
||||
[attr.viewBox]="viewBox"
|
||||
>
|
||||
<path fill="currentColor" [attr.d]="data" />
|
||||
</svg>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
})
|
||||
export class IconComponent implements OnInit, OnDestroy, OnChanges {
|
||||
@Input()
|
||||
icon: string;
|
||||
|
||||
data: string;
|
||||
|
||||
viewBox: string;
|
||||
|
||||
@Input()
|
||||
size = 24;
|
||||
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private readonly _iconRegistry: IconRegistry,
|
||||
private readonly _cdr: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._iconRegistry.updated
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe(() => {
|
||||
this.updateIcon();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._onDestroy$.next();
|
||||
this._onDestroy$.complete();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.icon) {
|
||||
this.updateIcon();
|
||||
}
|
||||
}
|
||||
|
||||
updateIcon(): void {
|
||||
const icon = this._iconRegistry.get(this.icon);
|
||||
this.data = icon?.data;
|
||||
this.viewBox = icon?.viewBox;
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { IconRegistry } from './icon-registry';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-icon',
|
||||
template: `
|
||||
<svg [style.width.rem]="size / 16" [style.height.rem]="size / 16" [attr.viewBox]="viewBox">
|
||||
<path fill="currentColor" [attr.d]="data" />
|
||||
</svg>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
})
|
||||
export class IconComponent implements OnInit, OnDestroy, OnChanges {
|
||||
@Input()
|
||||
icon: string;
|
||||
|
||||
data: string;
|
||||
|
||||
viewBox: string;
|
||||
|
||||
@Input()
|
||||
size: number = 24;
|
||||
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private readonly _iconRegistry: IconRegistry,
|
||||
private readonly _cdr: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._iconRegistry.updated.pipe(takeUntil(this._onDestroy$)).subscribe(() => {
|
||||
this.updateIcon();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._onDestroy$.next();
|
||||
this._onDestroy$.complete();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.icon) {
|
||||
this.updateIcon();
|
||||
}
|
||||
}
|
||||
|
||||
updateIcon(): void {
|
||||
const icon = this._iconRegistry.get(this.icon);
|
||||
this.data = icon?.data;
|
||||
this.viewBox = icon?.viewBox;
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,31 @@
|
||||
import { NgModule, Provider } from '@angular/core';
|
||||
import { IconComponent } from './icon.component';
|
||||
import { IconLoader, JsonIconLoader } from './loader';
|
||||
import { IconRegistry } from './icon-registry';
|
||||
|
||||
/**
|
||||
* @deprecated Use UiIconModule from '@isa/ui/icon' instead.
|
||||
*/
|
||||
export function provideIcon(loaderProvider?: Provider) {
|
||||
const providers: Provider[] = [IconRegistry];
|
||||
if (!loaderProvider) {
|
||||
providers.push({
|
||||
provide: IconLoader,
|
||||
useClass: JsonIconLoader,
|
||||
});
|
||||
} else {
|
||||
providers.push(loaderProvider);
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use UiIconModule from '@isa/ui/icon' instead.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [IconComponent],
|
||||
exports: [IconComponent],
|
||||
})
|
||||
export class IconModule {
|
||||
static forRoot(loaderProvider?: Provider) {
|
||||
return {
|
||||
ngModule: IconModule,
|
||||
providers: provideIcon(loaderProvider),
|
||||
};
|
||||
}
|
||||
}
|
||||
import { NgModule, Provider } from '@angular/core';
|
||||
import { IconComponent } from './icon.component';
|
||||
import { IconLoader, JsonIconLoader } from './loader';
|
||||
import { IconRegistry } from './icon-registry';
|
||||
|
||||
export function provideIcon(loaderProvider?: Provider) {
|
||||
const providers: Provider[] = [IconRegistry];
|
||||
if (!loaderProvider) {
|
||||
providers.push({
|
||||
provide: IconLoader,
|
||||
useClass: JsonIconLoader,
|
||||
});
|
||||
} else {
|
||||
providers.push(loaderProvider);
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [IconComponent],
|
||||
exports: [IconComponent],
|
||||
})
|
||||
export class IconModule {
|
||||
static forRoot(loaderProvider?: Provider) {
|
||||
return {
|
||||
ngModule: IconModule,
|
||||
providers: provideIcon(loaderProvider),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
|
||||
/**
|
||||
* @deprecated Use UiIconModule from '@isa/ui/icon' instead.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ui-icon-badge',
|
||||
templateUrl: 'icon-badge.component.html',
|
||||
styleUrls: ['icon-badge.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class UiIconBadgeComponent {
|
||||
@Input()
|
||||
icon: string;
|
||||
|
||||
@Input()
|
||||
alt: string;
|
||||
|
||||
@Input()
|
||||
area: 'customer' | 'branch' = 'customer';
|
||||
}
|
||||
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-icon-badge',
|
||||
templateUrl: 'icon-badge.component.html',
|
||||
styleUrls: ['icon-badge.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class UiIconBadgeComponent {
|
||||
@Input()
|
||||
icon: string;
|
||||
|
||||
@Input()
|
||||
alt: string;
|
||||
|
||||
@Input()
|
||||
area: 'customer' | 'branch' = 'customer';
|
||||
}
|
||||
|
||||
@@ -1,62 +1,59 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SvgIcon } from './defs';
|
||||
import { IconAlias } from './defs/icon-alias';
|
||||
|
||||
/**
|
||||
* @deprecated Use UiIconModule from '@isa/ui/icon' instead.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class IconRegistry {
|
||||
private _icons = new Map<string, SvgIcon>();
|
||||
private _aliases = new Map<string, string>();
|
||||
private _fallback: string;
|
||||
private _viewBox: string;
|
||||
|
||||
register(...icons: SvgIcon[]): IconRegistry {
|
||||
icons?.forEach((icon) => {
|
||||
this._icons.set(icon.name, icon);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setViewBox(viewBox: string): void {
|
||||
this._viewBox = viewBox;
|
||||
}
|
||||
|
||||
alias(...aliases: IconAlias[]): IconRegistry {
|
||||
aliases?.forEach((alias) => {
|
||||
this._aliases.set(alias.alias, alias.name);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setFallback(name: string): void {
|
||||
this._fallback = name;
|
||||
}
|
||||
|
||||
get(name: string): SvgIcon | undefined {
|
||||
const alias = this._aliases.get(name);
|
||||
let iconName = name;
|
||||
if (alias) {
|
||||
iconName = alias;
|
||||
}
|
||||
|
||||
let icon = this._icons.get(iconName);
|
||||
|
||||
if (!icon) {
|
||||
if (alias) {
|
||||
console.warn(`Not found: Icon with name ${name} (${iconName})`);
|
||||
} else {
|
||||
console.warn(`Unable to find icon: '${name}'`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!icon && this._fallback) {
|
||||
icon = this._icons.get(this._fallback);
|
||||
}
|
||||
|
||||
return { ...icon, viewBox: icon?.viewBox || this._viewBox };
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SvgIcon } from './defs';
|
||||
import { IconAlias } from './defs/icon-alias';
|
||||
|
||||
@Injectable()
|
||||
export class IconRegistry {
|
||||
private _icons = new Map<string, SvgIcon>();
|
||||
private _aliases = new Map<string, string>();
|
||||
private _fallback: string;
|
||||
private _viewBox: string;
|
||||
|
||||
register(...icons: SvgIcon[]): IconRegistry {
|
||||
icons?.forEach((icon) => {
|
||||
this._icons.set(icon.name, icon);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setViewBox(viewBox: string): void {
|
||||
this._viewBox = viewBox;
|
||||
}
|
||||
|
||||
alias(...aliases: IconAlias[]): IconRegistry {
|
||||
aliases?.forEach((alias) => {
|
||||
this._aliases.set(alias.alias, alias.name);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setFallback(name: string): void {
|
||||
this._fallback = name;
|
||||
}
|
||||
|
||||
get(name: string): SvgIcon | undefined {
|
||||
const alias = this._aliases.get(name);
|
||||
let iconName = name;
|
||||
if (alias) {
|
||||
iconName = alias;
|
||||
}
|
||||
|
||||
let icon = this._icons.get(iconName);
|
||||
|
||||
if (!icon) {
|
||||
if (alias) {
|
||||
console.warn(`Not found: Icon with name ${name} (${iconName})`);
|
||||
} else {
|
||||
console.warn(`Unable to find icon: '${name}'`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!icon && this._fallback) {
|
||||
icon = this._icons.get(this._fallback);
|
||||
}
|
||||
|
||||
return { ...icon, viewBox: icon?.viewBox || this._viewBox };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,29 @@
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
Optional,
|
||||
Inject,
|
||||
HostBinding,
|
||||
} from '@angular/core';
|
||||
import { UI_ICON_HREF, UI_ICON_VIEW_BOX } from './tokens';
|
||||
|
||||
/**
|
||||
* @deprecated Use UiIconModule from '@isa/ui/icon' instead.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ui-icon',
|
||||
templateUrl: 'icon.component.html',
|
||||
styleUrls: ['icon.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class UiIconComponent {
|
||||
@Input()
|
||||
@HostBinding('attr.icon')
|
||||
icon: string;
|
||||
|
||||
@Input()
|
||||
size = '1em';
|
||||
|
||||
@Input()
|
||||
rotate = '0deg';
|
||||
|
||||
constructor(
|
||||
@Optional() @Inject(UI_ICON_HREF) public iconHref: string,
|
||||
@Optional() @Inject(UI_ICON_VIEW_BOX) public viewBox: string,
|
||||
) {
|
||||
this.iconHref = this.iconHref || '/assets/icons.svg';
|
||||
this.viewBox = this.viewBox || '0 0 32 32';
|
||||
}
|
||||
}
|
||||
import { Component, ChangeDetectionStrategy, Input, Optional, Inject, HostBinding } from '@angular/core';
|
||||
import { UI_ICON_HREF, UI_ICON_VIEW_BOX } from './tokens';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-icon',
|
||||
templateUrl: 'icon.component.html',
|
||||
styleUrls: ['icon.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class UiIconComponent {
|
||||
@Input()
|
||||
@HostBinding('attr.icon')
|
||||
icon: string;
|
||||
|
||||
@Input()
|
||||
size = '1em';
|
||||
|
||||
@Input()
|
||||
rotate = '0deg';
|
||||
|
||||
constructor(
|
||||
@Optional() @Inject(UI_ICON_HREF) public iconHref: string,
|
||||
@Optional() @Inject(UI_ICON_VIEW_BOX) public viewBox: string,
|
||||
) {
|
||||
this.iconHref = this.iconHref || '/assets/icons.svg';
|
||||
this.viewBox = this.viewBox || '0 0 32 32';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,38 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { IconRegistry } from './icon-registry';
|
||||
|
||||
/**
|
||||
* @deprecated Use UiIconModule from '@isa/ui/icon' instead.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ui-svg-icon',
|
||||
template: `
|
||||
<svg
|
||||
[style.width.rem]="size / 16"
|
||||
[style.height.rem]="size / 16"
|
||||
[attr.viewBox]="viewBox"
|
||||
>
|
||||
<path fill="currentColor" [attr.d]="data" />
|
||||
</svg>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class UISvgIconComponent implements OnChanges {
|
||||
@Input()
|
||||
icon: string;
|
||||
|
||||
data: string;
|
||||
|
||||
viewBox: string;
|
||||
|
||||
@Input()
|
||||
size = 24;
|
||||
|
||||
constructor(
|
||||
private readonly _iconRegistry: IconRegistry,
|
||||
private readonly _cdr: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.icon) {
|
||||
const icon = this._iconRegistry.get(this.icon);
|
||||
this.data = icon?.data;
|
||||
this.viewBox = icon?.viewBox;
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
}
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { IconRegistry } from './icon-registry';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-svg-icon',
|
||||
template: `
|
||||
<svg [style.width.rem]="size / 16" [style.height.rem]="size / 16" [attr.viewBox]="viewBox">
|
||||
<path fill="currentColor" [attr.d]="data" />
|
||||
</svg>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class UISvgIconComponent implements OnChanges {
|
||||
@Input()
|
||||
icon: string;
|
||||
|
||||
data: string;
|
||||
|
||||
viewBox: string;
|
||||
|
||||
@Input()
|
||||
size: number = 24;
|
||||
|
||||
constructor(
|
||||
private readonly _iconRegistry: IconRegistry,
|
||||
private readonly _cdr: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.icon) {
|
||||
const icon = this._iconRegistry.get(this.icon);
|
||||
this.data = icon?.data;
|
||||
this.viewBox = icon?.viewBox;
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +1,57 @@
|
||||
import { ModuleWithProviders, NgModule, Provider } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { UiIconComponent } from './icon.component';
|
||||
import { UiIconBadgeComponent } from './icon-badge/icon-badge.component';
|
||||
import { UISvgIconComponent } from './svg-icon.component';
|
||||
import { IconRegistry } from './icon-registry';
|
||||
import { UI_ICON_CFG } from './tokens';
|
||||
import { UiIconConfig } from './icon-config';
|
||||
|
||||
/**
|
||||
* @deprecated Use UiIconModule from '@isa/ui/icon' instead.
|
||||
*/
|
||||
export function _rootIconRegistryFactory(config: UiIconConfig): IconRegistry {
|
||||
const registry = new IconRegistry();
|
||||
|
||||
if (config?.fallback) {
|
||||
registry.setFallback(config.fallback);
|
||||
}
|
||||
if (config?.aliases) {
|
||||
registry.alias(...config.aliases);
|
||||
}
|
||||
if (config?.icons) {
|
||||
registry.register(...config.icons);
|
||||
}
|
||||
|
||||
if (config?.viewBox) {
|
||||
registry.setViewBox(config.viewBox);
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use UiIconModule from '@isa/ui/icon' instead.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
declarations: [UiIconComponent, UiIconBadgeComponent, UISvgIconComponent],
|
||||
exports: [UiIconComponent, UiIconBadgeComponent, UISvgIconComponent],
|
||||
})
|
||||
export class UiIconModule {
|
||||
static forRoot(config?: UiIconConfig): ModuleWithProviders<UiIconModule> {
|
||||
const providers: Provider[] = [
|
||||
{
|
||||
provide: IconRegistry,
|
||||
useFactory: _rootIconRegistryFactory,
|
||||
deps: [UI_ICON_CFG],
|
||||
},
|
||||
];
|
||||
|
||||
if (config) {
|
||||
providers.push({
|
||||
provide: UI_ICON_CFG,
|
||||
useValue: config,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
ngModule: UiIconModule,
|
||||
providers,
|
||||
};
|
||||
}
|
||||
}
|
||||
import { ModuleWithProviders, NgModule, Provider } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { UiIconComponent } from './icon.component';
|
||||
import { UiIconBadgeComponent } from './icon-badge/icon-badge.component';
|
||||
import { UISvgIconComponent } from './svg-icon.component';
|
||||
import { IconRegistry } from './icon-registry';
|
||||
import { UI_ICON_CFG } from './tokens';
|
||||
import { UiIconConfig } from './icon-config';
|
||||
|
||||
export function _rootIconRegistryFactory(config: UiIconConfig): IconRegistry {
|
||||
const registry = new IconRegistry();
|
||||
|
||||
if (config?.fallback) {
|
||||
registry.setFallback(config.fallback);
|
||||
}
|
||||
if (config?.aliases) {
|
||||
registry.alias(...config.aliases);
|
||||
}
|
||||
if (config?.icons) {
|
||||
registry.register(...config.icons);
|
||||
}
|
||||
|
||||
if (config?.viewBox) {
|
||||
registry.setViewBox(config.viewBox);
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
declarations: [UiIconComponent, UiIconBadgeComponent, UISvgIconComponent],
|
||||
exports: [UiIconComponent, UiIconBadgeComponent, UISvgIconComponent],
|
||||
})
|
||||
export class UiIconModule {
|
||||
static forRoot(config?: UiIconConfig): ModuleWithProviders<UiIconModule> {
|
||||
const providers: Provider[] = [
|
||||
{
|
||||
provide: IconRegistry,
|
||||
useFactory: _rootIconRegistryFactory,
|
||||
deps: [UI_ICON_CFG],
|
||||
},
|
||||
];
|
||||
|
||||
if (config) {
|
||||
providers.push({
|
||||
provide: UI_ICON_CFG,
|
||||
useValue: config,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
ngModule: UiIconModule,
|
||||
providers,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,43 @@
|
||||
@if (modalRef.data.subtitle; as subtitle) {
|
||||
<h2 class="subtitle">{{ subtitle }}</h2>
|
||||
}
|
||||
@if (modalRef.data.content; as content) {
|
||||
<p class="content">
|
||||
{{ content }}
|
||||
</p>
|
||||
|
||||
<!-- QR Code Display Mode -->
|
||||
@if (shouldShowQrCode(); as showQr) {
|
||||
@if (parsedContent(); as parsed) {
|
||||
@if (parsed.textBefore) {
|
||||
<p class="content">{{ parsed.textBefore }}</p>
|
||||
}
|
||||
|
||||
<div class="qr-code-container">
|
||||
<qrcode
|
||||
[qrdata]="parsed.url!"
|
||||
[width]="200"
|
||||
[errorCorrectionLevel]="'M'"
|
||||
[margin]="2"
|
||||
></qrcode>
|
||||
</div>
|
||||
|
||||
@if (parsed.textAfter) {
|
||||
<p class="content">{{ parsed.textAfter }}</p>
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
<!-- Default Text Display Mode -->
|
||||
@if (modalRef.data.content; as content) {
|
||||
<p class="content">
|
||||
{{ content }}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
@if (modalRef.data.actions; as actions) {
|
||||
<div class="actions">
|
||||
@for (action of actions; track action) {
|
||||
<button [class.selected]="action.selected" (click)="handleCommand(action.command)">
|
||||
<button
|
||||
[class.selected]="action.selected"
|
||||
(click)="handleCommand(action.command)"
|
||||
>
|
||||
{{ action.label }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -15,11 +15,15 @@
|
||||
@apply text-lg text-center whitespace-pre-wrap mb-8 px-16;
|
||||
}
|
||||
|
||||
.qr-code-container {
|
||||
@apply flex flex-col items-center justify-center mb-8;
|
||||
}
|
||||
|
||||
.actions {
|
||||
@apply text-center mb-8;
|
||||
|
||||
button {
|
||||
@apply border-2 border-solid border-brand bg-white text-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap ml-4;
|
||||
@apply border-2 border-solid border-brand bg-white text-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap;
|
||||
|
||||
&.selected {
|
||||
@apply bg-brand text-white;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, computed, OnInit } from '@angular/core';
|
||||
import { CommandService } from '@core/command';
|
||||
import { UiModalRef } from '../defs/modal-ref';
|
||||
import { DialogModel } from './dialog.model';
|
||||
import { parseDialogContentForUrl } from './dialog.helper';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-dialog-modal',
|
||||
@@ -10,6 +11,26 @@ import { DialogModel } from './dialog.model';
|
||||
standalone: false,
|
||||
})
|
||||
export class UiDialogModalComponent implements OnInit {
|
||||
/**
|
||||
* Parsed content with URL extracted for QR code display.
|
||||
* Only relevant when showUrlAsQrCode is true.
|
||||
*/
|
||||
readonly parsedContent = computed(() => {
|
||||
const data = this.modalRef.data;
|
||||
if (!data.showUrlAsQrCode) {
|
||||
return null;
|
||||
}
|
||||
return parseDialogContentForUrl(data.content);
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether to show the QR code instead of the URL text.
|
||||
*/
|
||||
readonly shouldShowQrCode = computed(() => {
|
||||
const parsed = this.parsedContent();
|
||||
return parsed !== null && parsed.url !== null;
|
||||
});
|
||||
|
||||
constructor(
|
||||
public modalRef: UiModalRef<any, DialogModel<any>>,
|
||||
private _command: CommandService,
|
||||
|
||||
48
apps/isa-app/src/ui/modal/dialog/dialog.helper.ts
Normal file
48
apps/isa-app/src/ui/modal/dialog/dialog.helper.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ParsedDialogContent } from './dialog.model';
|
||||
|
||||
/**
|
||||
* Regular expression to match URLs in text.
|
||||
* Matches http:// and https:// URLs.
|
||||
*/
|
||||
const URL_REGEX = /https?:\/\/[^\s]+/i;
|
||||
|
||||
/**
|
||||
* Parses the dialog content and extracts the first URL.
|
||||
* Splits the content into text before the URL, the URL itself, and text after.
|
||||
*
|
||||
* @param content - The dialog content string to parse
|
||||
* @returns ParsedDialogContent with the split content
|
||||
*/
|
||||
export const parseDialogContentForUrl = (
|
||||
content: string | undefined,
|
||||
): ParsedDialogContent => {
|
||||
if (!content) {
|
||||
return { textBefore: '', url: null, textAfter: '' };
|
||||
}
|
||||
|
||||
const match = content.match(URL_REGEX);
|
||||
|
||||
if (!match || match.index === undefined) {
|
||||
return { textBefore: content, url: null, textAfter: '' };
|
||||
}
|
||||
|
||||
const url = match[0];
|
||||
const urlIndex = match.index;
|
||||
const textBefore = content.substring(0, urlIndex).trim();
|
||||
const textAfter = content.substring(urlIndex + url.length).trim();
|
||||
|
||||
return { textBefore, url, textAfter };
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the given content contains a URL.
|
||||
*
|
||||
* @param content - The content string to check
|
||||
* @returns true if a URL is found, false otherwise
|
||||
*/
|
||||
export const contentHasUrl = (content: string | undefined): boolean => {
|
||||
if (!content) {
|
||||
return false;
|
||||
}
|
||||
return URL_REGEX.test(content);
|
||||
};
|
||||
152
apps/isa-app/src/ui/modal/dialog/dialog.model.spec.ts
Normal file
152
apps/isa-app/src/ui/modal/dialog/dialog.model.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { contentHasUrl, parseDialogContentForUrl } from './dialog.helper';
|
||||
import { ParsedDialogContent } from './dialog.model';
|
||||
|
||||
describe('parseDialogContentForUrl', () => {
|
||||
it('should return empty result for undefined content', () => {
|
||||
const result = parseDialogContentForUrl(undefined);
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: '',
|
||||
url: null,
|
||||
textAfter: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty result for empty string', () => {
|
||||
const result = parseDialogContentForUrl('');
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: '',
|
||||
url: null,
|
||||
textAfter: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return content as textBefore when no URL is found', () => {
|
||||
const content = 'This is some text without a URL';
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: content,
|
||||
url: null,
|
||||
textAfter: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract https URL from content', () => {
|
||||
const content = 'Text before https://example.com text after';
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: 'Text before',
|
||||
url: 'https://example.com',
|
||||
textAfter: 'text after',
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract http URL from content', () => {
|
||||
const content = 'Text before http://example.com text after';
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: 'Text before',
|
||||
url: 'http://example.com',
|
||||
textAfter: 'text after',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle URL at the beginning of content', () => {
|
||||
const content = 'https://example.com/path text after';
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: '',
|
||||
url: 'https://example.com/path',
|
||||
textAfter: 'text after',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle URL at the end of content', () => {
|
||||
const content = 'Text before https://example.com/path';
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: 'Text before',
|
||||
url: 'https://example.com/path',
|
||||
textAfter: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle real-world content with newlines', () => {
|
||||
const content = `Punkte: 80500
|
||||
Um alle Vorteile der Kundenkarte nutzen zu können, ist eine Verknüpfung zu einem Online-Konto notwendig. Kund:innen können sich über den QR-Code selbstständig anmelden oder die Kundenkarte dem bestehendem Konto hinzufügen. Bereits gesammelte Punkte werden übernommen.
|
||||
https://h-k.me/QOHNTFVA`;
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result.url).toBe('https://h-k.me/QOHNTFVA');
|
||||
expect(result.textBefore).toContain('Punkte: 80500');
|
||||
expect(result.textBefore).toContain(
|
||||
'Bereits gesammelte Punkte werden übernommen.',
|
||||
);
|
||||
expect(result.textAfter).toBe('');
|
||||
});
|
||||
|
||||
it('should extract only the first URL when multiple URLs are present', () => {
|
||||
const content = 'First https://first.com then https://second.com';
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result).toEqual<ParsedDialogContent>({
|
||||
textBefore: 'First',
|
||||
url: 'https://first.com',
|
||||
textAfter: 'then https://second.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle URLs with paths and query parameters', () => {
|
||||
const content =
|
||||
'Visit https://example.com/path?query=value&foo=bar for more';
|
||||
|
||||
const result = parseDialogContentForUrl(content);
|
||||
|
||||
expect(result.url).toBe('https://example.com/path?query=value&foo=bar');
|
||||
expect(result.textBefore).toBe('Visit');
|
||||
expect(result.textAfter).toBe('for more');
|
||||
});
|
||||
});
|
||||
|
||||
describe('contentHasUrl', () => {
|
||||
it('should return false for undefined content', () => {
|
||||
expect(contentHasUrl(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty string', () => {
|
||||
expect(contentHasUrl('')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for content without URL', () => {
|
||||
expect(contentHasUrl('This is text without a URL')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for content with https URL', () => {
|
||||
expect(contentHasUrl('Check out https://example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for content with http URL', () => {
|
||||
expect(contentHasUrl('Check out http://example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for real-world content', () => {
|
||||
const content = `Punkte: 80500
|
||||
Um alle Vorteile der Kundenkarte nutzen zu können...
|
||||
https://h-k.me/QOHNTFVA`;
|
||||
|
||||
expect(contentHasUrl(content)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,7 @@
|
||||
import { DialogSettings, KeyValueDTOOfStringAndString } from '@generated/swagger/crm-api';
|
||||
import {
|
||||
DialogSettings,
|
||||
KeyValueDTOOfStringAndString,
|
||||
} from '@generated/swagger/crm-api';
|
||||
|
||||
export interface DialogModel<T = any> {
|
||||
actions?: Array<KeyValueDTOOfStringAndString>;
|
||||
@@ -14,4 +17,21 @@ export interface DialogModel<T = any> {
|
||||
* default: true
|
||||
*/
|
||||
handleCommand?: boolean;
|
||||
/**
|
||||
* If true, URLs in the content will be displayed as QR codes.
|
||||
* default: false
|
||||
*/
|
||||
showUrlAsQrCode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of parsing content for URLs
|
||||
*/
|
||||
export interface ParsedDialogContent {
|
||||
/** Text before the URL */
|
||||
textBefore: string;
|
||||
/** The extracted URL (if any) */
|
||||
url: string | null;
|
||||
/** Text after the URL */
|
||||
textAfter: string;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
import { DialogModel } from './dialog.model';
|
||||
import { ToasterService } from '@shared/shell';
|
||||
import { contentHasUrl } from './dialog.helper';
|
||||
|
||||
@Injectable()
|
||||
export class OpenDialogInterceptor implements HttpInterceptor {
|
||||
@@ -21,7 +22,10 @@ export class OpenDialogInterceptor implements HttpInterceptor {
|
||||
private _toast: ToasterService,
|
||||
) {}
|
||||
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
intercept(
|
||||
req: HttpRequest<any>,
|
||||
next: HttpHandler,
|
||||
): Observable<HttpEvent<any>> {
|
||||
return next.handle(req).pipe(
|
||||
tap((response) => {
|
||||
if (response instanceof HttpResponse) {
|
||||
@@ -59,9 +63,17 @@ export class OpenDialogInterceptor implements HttpInterceptor {
|
||||
}
|
||||
|
||||
openDialog(model: DialogModel<any>) {
|
||||
// Auto-detect URLs and enable QR code display if URL is found
|
||||
// Can be overridden by explicitly setting showUrlAsQrCode in the model
|
||||
const showUrlAsQrCode =
|
||||
model.showUrlAsQrCode ?? contentHasUrl(model.content);
|
||||
|
||||
this._modal.open({
|
||||
content: UiDialogModalComponent,
|
||||
data: model,
|
||||
data: {
|
||||
...model,
|
||||
showUrlAsQrCode,
|
||||
},
|
||||
title: model.title,
|
||||
config: {
|
||||
canClose: (model.settings & 1) === 1,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { OverlayModule } from '@angular/cdk/overlay';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { UiModalComponent } from './modal.component';
|
||||
import { UiModalService } from './modal.service';
|
||||
import { UiDebugModalComponent } from './debug-modal/debug-modal.component';
|
||||
import { UiMessageModalComponent } from './message-modal.component';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
@@ -10,9 +9,10 @@ import { UiDialogModalComponent } from './dialog/dialog-modal.component';
|
||||
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { OpenDialogInterceptor } from './dialog/open-dialog.interceptor';
|
||||
import { UiPromptModalComponent } from './prompt-modal';
|
||||
import { QRCodeComponent } from 'angularx-qrcode';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, OverlayModule, UiIconModule],
|
||||
imports: [CommonModule, OverlayModule, UiIconModule, QRCodeComponent],
|
||||
declarations: [
|
||||
UiModalComponent,
|
||||
UiDebugModalComponent,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { debounceTime, filter } from 'rxjs/operators';
|
||||
|
||||
@Directive({
|
||||
selector: '[uiScrollContainer]',
|
||||
@@ -27,30 +27,48 @@ export class UiScrollContainerDirective implements OnChanges, OnInit {
|
||||
@Input()
|
||||
deltaEnd = 0;
|
||||
|
||||
private scrollEvent$ = new Subject<Event>();
|
||||
private scrollEvent$ = new Subject<void>();
|
||||
|
||||
/**
|
||||
* Tracks the last scrollHeight when reachEnd was emitted.
|
||||
* This prevents duplicate emissions at the same scroll position after content loads.
|
||||
*/
|
||||
private lastEmittedScrollHeight = 0;
|
||||
|
||||
@Output()
|
||||
reachStart = this.scrollEvent$.pipe(
|
||||
filter((event) => {
|
||||
debounceTime(100),
|
||||
filter(() => {
|
||||
if (this.direction === 'vertical') {
|
||||
const top = this.nativeElement.scrollTop;
|
||||
return top <= this.deltaStart;
|
||||
} else {
|
||||
throw new Error('not implemented');
|
||||
return this.nativeElement.scrollTop <= this.deltaStart;
|
||||
}
|
||||
throw new Error('Horizontal scroll not implemented');
|
||||
}),
|
||||
);
|
||||
|
||||
@Output()
|
||||
reachEnd = this.scrollEvent$.pipe(
|
||||
filter((event) => {
|
||||
debounceTime(100),
|
||||
filter(() => {
|
||||
if (this.direction === 'vertical') {
|
||||
const top = this.nativeElement.scrollTop;
|
||||
const height = this.nativeElement.scrollHeight - this.nativeElement.clientHeight - this.deltaEnd;
|
||||
return top >= height;
|
||||
} else {
|
||||
throw new Error('not implemented');
|
||||
const { scrollTop, scrollHeight, clientHeight } = this.nativeElement;
|
||||
const threshold = scrollHeight - clientHeight - this.deltaEnd;
|
||||
const isAtEnd = scrollTop >= threshold;
|
||||
|
||||
if (!isAtEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only emit if scrollHeight changed (new content loaded)
|
||||
// This prevents re-emitting when user is still at end after a load
|
||||
if (scrollHeight !== this.lastEmittedScrollHeight) {
|
||||
this.lastEmittedScrollHeight = scrollHeight;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
throw new Error('Horizontal scroll not implemented');
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -71,17 +89,33 @@ export class UiScrollContainerDirective implements OnChanges, OnInit {
|
||||
ngOnChanges({ direction }: SimpleChanges): void {
|
||||
if (direction) {
|
||||
if (this.direction === 'horizontal') {
|
||||
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow-x', 'auto');
|
||||
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow-y', 'auto');
|
||||
this.renderer.setStyle(
|
||||
this.elementRef.nativeElement,
|
||||
'overflow-x',
|
||||
'auto',
|
||||
);
|
||||
this.renderer.setStyle(
|
||||
this.elementRef.nativeElement,
|
||||
'overflow-y',
|
||||
'auto',
|
||||
);
|
||||
} else {
|
||||
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow-y', 'auto');
|
||||
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow-x', 'hidden');
|
||||
this.renderer.setStyle(
|
||||
this.elementRef.nativeElement,
|
||||
'overflow-y',
|
||||
'auto',
|
||||
);
|
||||
this.renderer.setStyle(
|
||||
this.elementRef.nativeElement,
|
||||
'overflow-x',
|
||||
'hidden',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('scroll', ['$event'])
|
||||
onScroll(event: Event) {
|
||||
this.scrollEvent$.next(event);
|
||||
@HostListener('scroll')
|
||||
onScroll() {
|
||||
this.scrollEvent$.next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,20 +8,27 @@
|
||||
(reachEnd)="reachedEnd()"
|
||||
(reachStart)="reachedStart()"
|
||||
[deltaEnd]="deltaEnd"
|
||||
>
|
||||
@if (!loading) {
|
||||
>
|
||||
@if (!loading || itemLength > 0) {
|
||||
<ng-content></ng-content>
|
||||
} @else {
|
||||
@if (useLoadAnimation) {
|
||||
}
|
||||
|
||||
@if (loading && useLoadAnimation) {
|
||||
@if (itemLength === 0 || itemLength === undefined) {
|
||||
<!-- Initial load: show multiple skeletons -->
|
||||
<ui-skeleton-loader [template]="skeletonTemplate"></ui-skeleton-loader>
|
||||
@for (skeletons of createSkeletons(); track skeletons) {
|
||||
@for (skeleton of createSkeletons(); track skeleton) {
|
||||
<ui-skeleton-loader [template]="skeletonTemplate"></ui-skeleton-loader>
|
||||
}
|
||||
} @else {
|
||||
<ui-content-loader [loading]="loading"></ui-content-loader>
|
||||
<!-- Load more: show single skeleton at the end -->
|
||||
<ui-skeleton-loader [template]="skeletonTemplate"></ui-skeleton-loader>
|
||||
}
|
||||
}
|
||||
|
||||
@if (loading && !useLoadAnimation) {
|
||||
<ui-content-loader [loading]="loading"></ui-content-loader>
|
||||
}
|
||||
|
||||
@if (showSpacer && !loading) {
|
||||
<div class="spacer"></div>
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
|
||||
@@ -16,7 +18,7 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class UiScrollContainerComponent implements OnInit {
|
||||
export class UiScrollContainerComponent implements OnInit, OnChanges {
|
||||
@ViewChild('scrollContainer', { read: ElementRef, static: true })
|
||||
scrollContainer: ElementRef;
|
||||
|
||||
@@ -61,14 +63,49 @@ export class UiScrollContainerComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
createSkeletons() {
|
||||
if (this.itemLength && this.itemLength !== 0) {
|
||||
return Array.from(Array(this.itemLength - 1), (_, i) => i);
|
||||
} else {
|
||||
return [];
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// When new items are loaded, adjust scroll position so user can scroll down again
|
||||
if (changes['itemLength']) {
|
||||
const prevLength = changes['itemLength'].previousValue ?? 0;
|
||||
const newLength = changes['itemLength'].currentValue ?? 0;
|
||||
|
||||
// Only adjust if items were added (not on initial load or reset)
|
||||
if (newLength > prevLength && prevLength > 0) {
|
||||
this.adjustScrollPositionAfterLoad();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After new items are loaded, adjust scroll position so user is not at the very end.
|
||||
* This allows them to scroll down again to trigger the next load.
|
||||
*/
|
||||
private adjustScrollPositionAfterLoad(): void {
|
||||
const el = this.scrollContainer?.nativeElement;
|
||||
if (!el) return;
|
||||
|
||||
// Wait for DOM to update with new items
|
||||
setTimeout(() => {
|
||||
const maxScroll = el.scrollHeight - el.clientHeight;
|
||||
const currentScroll = el.scrollTop;
|
||||
|
||||
// Only adjust if we're at or very near the end
|
||||
if (currentScroll >= maxScroll - this.deltaEnd - 20) {
|
||||
// Move scroll position up by deltaEnd + buffer so user has room to scroll
|
||||
const offset = this.deltaEnd + 100;
|
||||
const targetScroll = Math.max(0, maxScroll - offset);
|
||||
el.scrollTop = targetScroll;
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
createSkeletons(): number[] {
|
||||
if (this.itemLength && this.itemLength !== 0) {
|
||||
return Array.from({ length: this.itemLength - 1 }, (_, i) => i);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
reachedEnd() {
|
||||
this.reachEnd.emit();
|
||||
}
|
||||
@@ -79,7 +116,8 @@ export class UiScrollContainerComponent implements OnInit {
|
||||
|
||||
get scrollPersantage() {
|
||||
const scrollHeight =
|
||||
this.scrollContainer?.nativeElement?.scrollHeight - this.scrollContainer?.nativeElement?.clientHeight;
|
||||
this.scrollContainer?.nativeElement?.scrollHeight -
|
||||
this.scrollContainer?.nativeElement?.clientHeight;
|
||||
if (scrollHeight === 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -95,7 +133,10 @@ export class UiScrollContainerComponent implements OnInit {
|
||||
|
||||
scrollTo(top: number) {
|
||||
setTimeout(() => {
|
||||
this.scrollContainer?.nativeElement?.scrollTo({ top, behavior: 'smooth' });
|
||||
this.scrollContainer?.nativeElement?.scrollTo({
|
||||
top,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,682 +0,0 @@
|
||||
# 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-12-03
|
||||
> **Last Updated:** 2025-11-28
|
||||
> **Angular Version:** 20.3.6
|
||||
> **Nx Version:** 21.3.2
|
||||
> **Total Libraries:** 81
|
||||
> **Total Libraries:** 74
|
||||
|
||||
All 81 libraries in the monorepo have comprehensive README.md documentation located at `libs/[domain]/[layer]/[feature]/README.md`.
|
||||
All 74 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 (4 libraries)
|
||||
## Common Libraries (3 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,14 +83,9 @@ 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 (7 libraries)
|
||||
## Core Libraries (6 libraries)
|
||||
|
||||
### `@isa/core/auth`
|
||||
Type-safe role-based authorization utilities with Angular signals integration for the ISA Frontend application.
|
||||
@@ -102,11 +97,6 @@ A lightweight, type-safe configuration management system for Angular application
|
||||
|
||||
**Location:** `libs/core/config/`
|
||||
|
||||
### `@isa/core/connectivity`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
**Location:** `libs/core/connectivity/`
|
||||
|
||||
### `@isa/core/logging`
|
||||
A structured, high-performance logging library for Angular applications with hierarchical context support and flexible sink architecture.
|
||||
|
||||
@@ -435,35 +425,6 @@ A lightweight Zod utility library for safe parsing with automatic fallback to or
|
||||
|
||||
---
|
||||
|
||||
## Shell Domain (5 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`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
**Location:** `libs/shell/navigation/`
|
||||
|
||||
### `@isa/shell/notifications`
|
||||
**Type:** Feature Library
|
||||
|
||||
**Location:** `libs/shell/notifications/`
|
||||
|
||||
---
|
||||
|
||||
## How to Use This Guide
|
||||
|
||||
1. **Quick Lookup**: Use this guide to find the purpose of any library in the monorepo
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* tslint:disable */
|
||||
import { EntityDTOBaseOfDisplayOrderItemDTOAndIOrderItem } from './entity-dtobase-of-display-order-item-dtoand-iorder-item';
|
||||
import { KeyValueDTOOfStringAndString } from './key-value-dtoof-string-and-string';
|
||||
import { LoyaltyDTO } from './loyalty-dto';
|
||||
import { DisplayOrderDTO } from './display-order-dto';
|
||||
import { PriceDTO } from './price-dto';
|
||||
@@ -9,6 +10,11 @@ import { QuantityUnitType } from './quantity-unit-type';
|
||||
import { DisplayOrderItemSubsetDTO } from './display-order-item-subset-dto';
|
||||
export interface DisplayOrderItemDTO extends EntityDTOBaseOfDisplayOrderItemDTOAndIOrderItem{
|
||||
|
||||
/**
|
||||
* Mögliche Aktionen
|
||||
*/
|
||||
actions?: Array<KeyValueDTOOfStringAndString>;
|
||||
|
||||
/**
|
||||
* Bemerkung des Auftraggebers
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user