diff --git a/apps/isa-app/src/app/app-routing.module.ts b/apps/isa-app/src/app/app-routing.module.ts
index dc720f9ce..cac01b1ef 100644
--- a/apps/isa-app/src/app/app-routing.module.ts
+++ b/apps/isa-app/src/app/app-routing.module.ts
@@ -1,4 +1,4 @@
-import { isDevMode, NgModule } from '@angular/core';
+import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {
CanActivateCartGuard,
@@ -7,23 +7,17 @@ import {
CanActivateCustomerOrdersGuard,
CanActivateCustomerOrdersWithProcessIdGuard,
CanActivateCustomerWithProcessIdGuard,
- CanActivateGoodsInGuard,
CanActivateProductGuard,
CanActivateProductWithProcessIdGuard,
- CanActivateTaskCalendarGuard,
IsAuthenticatedGuard,
} from './guards';
-import { CanActivateAssortmentGuard } from './guards/can-activate-assortment.guard';
-import { CanActivatePackageInspectionGuard } from './guards/can-activate-package-inspection.guard';
import { MainComponent } from './main.component';
-import { PreviewComponent } from './preview';
import {
BranchSectionResolver,
CustomerSectionResolver,
ProcessIdResolver,
} from './resolvers';
import { TokenLoginComponent, TokenLoginModule } from './token-login';
-import { ProcessIdGuard } from './guards/process-id.guard';
import {
ActivateProcessIdGuard,
ActivateProcessIdWithConfigKeyGuard,
@@ -74,8 +68,12 @@ const routes: Routes = [
loadChildren: () =>
import('@page/catalog').then((m) => m.PageCatalogModule),
canActivate: [CanActivateProductWithProcessIdGuard],
- resolve: { processId: ProcessIdResolver },
+ resolve: {
+ 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: () =>
@@ -87,7 +85,9 @@ const routes: Routes = [
loadChildren: () =>
import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersWithProcessIdGuard],
- resolve: { processId: ProcessIdResolver },
+ resolve: {
+ processId: ProcessIdResolver,
+ },
},
{
path: 'customer',
@@ -100,7 +100,9 @@ const routes: Routes = [
loadChildren: () =>
import('@page/customer').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerWithProcessIdGuard],
- resolve: { processId: ProcessIdResolver },
+ resolve: {
+ processId: ProcessIdResolver,
+ },
},
{
path: 'cart',
@@ -113,10 +115,13 @@ const routes: Routes = [
loadChildren: () =>
import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartWithProcessIdGuard],
+ resolve: {
+ processId: ProcessIdResolver,
+ },
},
{
path: 'pickup-shelf',
- canActivate: [ProcessIdGuard],
+ canActivate: [ActivateProcessIdGuard],
// NOTE: This is a workaround for the canActivate guard not being called
loadChildren: () =>
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
@@ -126,6 +131,9 @@ const routes: Routes = [
canActivate: [ActivateProcessIdGuard],
loadChildren: () =>
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
+ resolve: {
+ processId: ProcessIdResolver,
+ },
},
{ path: '**', redirectTo: 'dashboard', pathMatch: 'full' },
],
@@ -141,7 +149,7 @@ const routes: Routes = [
import('@page/task-calendar').then(
(m) => m.PageTaskCalendarModule,
),
- canActivate: [CanActivateTaskCalendarGuard],
+ canActivate: [ActivateProcessIdWithConfigKeyGuard('taskCalendar')],
},
{
path: 'pickup-shelf',
@@ -154,27 +162,23 @@ const routes: Routes = [
path: 'goods/in',
loadChildren: () =>
import('@page/goods-in').then((m) => m.GoodsInModule),
- canActivate: [CanActivateGoodsInGuard],
+ canActivate: [ActivateProcessIdWithConfigKeyGuard('goodsIn')],
},
- // {
- // path: 'remission',
- // loadChildren: () =>
- // import('@page/remission').then((m) => m.PageRemissionModule),
- // canActivate: [CanActivateRemissionGuard],
- // },
{
path: 'package-inspection',
loadChildren: () =>
import('@page/package-inspection').then(
(m) => m.PackageInspectionModule,
),
- canActivate: [CanActivatePackageInspectionGuard],
+ canActivate: [
+ ActivateProcessIdWithConfigKeyGuard('packageInspection'),
+ ],
},
{
path: 'assortment',
loadChildren: () =>
import('@page/assortment').then((m) => m.AssortmentModule),
- canActivate: [CanActivateAssortmentGuard],
+ canActivate: [ActivateProcessIdWithConfigKeyGuard('assortment')],
},
{ path: '**', redirectTo: 'task-calendar', pathMatch: 'full' },
],
@@ -243,13 +247,6 @@ const routes: Routes = [
},
];
-if (isDevMode()) {
- routes.unshift({
- path: 'preview',
- component: PreviewComponent,
- });
-}
-
@NgModule({
imports: [
RouterModule.forRoot(routes, {
diff --git a/apps/isa-app/src/app/app.module.ts b/apps/isa-app/src/app/app.module.ts
index e0e96751e..c793753a7 100644
--- a/apps/isa-app/src/app/app.module.ts
+++ b/apps/isa-app/src/app/app.module.ts
@@ -1,4 +1,5 @@
import { version } from '../../../../package.json';
+import { IsaTitleStrategy } from '@isa/common/title-management';
import {
HTTP_INTERCEPTORS,
provideHttpClient,
@@ -56,7 +57,6 @@ import {
ScanditScanAdapterModule,
} from '@adapter/scan';
import * as Commands from './commands';
-import { PreviewComponent } from './preview';
import { NativeContainerService } from '@external/native-container';
import { ShellModule } from '@shared/shell';
import { MainComponent } from './main.component';
@@ -87,6 +87,7 @@ import {
import { Store } from '@ngrx/store';
import { OAuthService } from 'angular-oauth2-oidc';
import z from 'zod';
+import { TitleStrategy } from '@angular/router';
import { TabNavigationService } from '@isa/core/tabs';
registerLocaleData(localeDe, localeDeExtra);
@@ -270,7 +271,6 @@ const USER_SUB_FACTORY = () => {
CoreCommandModule.forRoot(Object.values(Commands)),
CoreLoggerModule.forRoot(),
AppStoreModule,
- PreviewComponent,
AuthModule.forRoot(),
CoreApplicationModule.forRoot(),
UiModalModule.forRoot(),
@@ -330,6 +330,7 @@ const USER_SUB_FACTORY = () => {
useValue: 'EUR',
},
provideUserSubFactory(USER_SUB_FACTORY),
+ { provide: TitleStrategy, useClass: IsaTitleStrategy },
],
})
export class AppModule {}
diff --git a/apps/isa-app/src/app/guards/activate-process-id.guard.ts b/apps/isa-app/src/app/guards/activate-process-id.guard.ts
index 9759d4dd1..83e67203e 100644
--- a/apps/isa-app/src/app/guards/activate-process-id.guard.ts
+++ b/apps/isa-app/src/app/guards/activate-process-id.guard.ts
@@ -1,47 +1,82 @@
-import { inject } from '@angular/core';
-import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router';
-import { ApplicationService } from '@core/application';
-import { Config } from '@core/config';
-import { take } from 'rxjs/operators';
-
-export const ActivateProcessIdGuard: CanActivateFn = async (
- route: ActivatedRouteSnapshot,
- state: RouterStateSnapshot,
-) => {
- const application = inject(ApplicationService);
-
- const processIdStr = route.params.processId;
-
- if (!processIdStr) {
- return false;
- }
-
- const processId = Number(processIdStr);
-
- // Check if Process already exists
- const process = await application.getProcessById$(processId).pipe(take(1)).toPromise();
-
- if (!process) {
- application.createCustomerProcess(processId);
- }
-
- application.activateProcess(processId);
-
- return true;
-};
-
-export const ActivateProcessIdWithConfigKeyGuard: (key: string) => CanActivateFn =
- (key) => async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
- const application = inject(ApplicationService);
- const config = inject(Config);
-
- const processId = config.get(`process.ids.${key}`);
-
- if (isNaN(processId)) {
- return false;
- }
-
- application.activateProcess(processId);
-
- return true;
- };
+import { inject } from '@angular/core';
+import {
+ ActivatedRouteSnapshot,
+ CanActivateFn,
+ RouterStateSnapshot,
+} from '@angular/router';
+import { ApplicationService } from '@core/application';
+import { Config } from '@core/config';
+import { take } from 'rxjs/operators';
+import z from 'zod';
+
+export const ActivateProcessIdGuard: CanActivateFn = async (
+ route: ActivatedRouteSnapshot,
+ state: RouterStateSnapshot,
+) => {
+ const application = inject(ApplicationService);
+
+ const processIdStr = route.params.processId;
+
+ if (!processIdStr) {
+ return false;
+ }
+
+ const processId = Number(processIdStr);
+
+ // Check if Process already exists
+ const process = await application
+ .getProcessById$(processId)
+ .pipe(take(1))
+ .toPromise();
+
+ if (!process) {
+ application.createProcess({
+ id: processId,
+ type: 'cart',
+ section: 'customer',
+ name: `Vorgang ${processId}`,
+ });
+ }
+
+ application.activateProcess(processId);
+
+ return true;
+};
+
+export const ActivateProcessIdWithConfigKeyGuard: (
+ key: string,
+) => CanActivateFn =
+ (key) =>
+ async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
+ const application = inject(ApplicationService);
+ const config = inject(Config);
+
+ const processId = config.get(`process.ids.${key}`, z.coerce.number());
+
+ if (typeof processId !== 'number' || isNaN(processId)) {
+ return false;
+ }
+
+ const processTitle = config.get(
+ `process.titles.${key}`,
+ z.string().default(key),
+ );
+
+ const process = await application
+ .getProcessById$(processId)
+ .pipe(take(1))
+ .toPromise();
+
+ if (!process) {
+ await application.createProcess({
+ id: processId,
+ type: key,
+ section: 'customer', // Not important anymore
+ name: processTitle,
+ });
+ }
+
+ application.activateProcess(processId);
+
+ return true;
+ };
diff --git a/apps/isa-app/src/app/guards/can-activate-goods-in.guard.ts b/apps/isa-app/src/app/guards/can-activate-goods-in.guard.ts
index 24533cc01..bad7888b3 100644
--- a/apps/isa-app/src/app/guards/can-activate-goods-in.guard.ts
+++ b/apps/isa-app/src/app/guards/can-activate-goods-in.guard.ts
@@ -1,34 +1,34 @@
-import { Injectable } from '@angular/core';
-import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
-import { ApplicationService } from '@core/application';
-import { Config } from '@core/config';
-import { first } from 'rxjs/operators';
-import { z } from 'zod';
-
-@Injectable({ providedIn: 'root' })
-export class CanActivateGoodsInGuard {
- constructor(
- private readonly _applicationService: ApplicationService,
- private readonly _config: Config,
- ) {}
-
- async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
- const pid = this._config.get('process.ids.goodsIn', z.number());
- const process = await this._applicationService
- .getProcessById$(pid)
- .pipe(first())
- .toPromise();
- if (!process) {
- await this._applicationService.createProcess({
- id: this._config.get('process.ids.goodsIn'),
- type: 'goods-in',
- section: 'branch',
- name: '',
- });
- }
- this._applicationService.activateProcess(
- this._config.get('process.ids.goodsIn'),
- );
- return true;
- }
-}
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
+import { ApplicationService } from '@core/application';
+import { Config } from '@core/config';
+import { first } from 'rxjs/operators';
+import { z } from 'zod';
+
+@Injectable({ providedIn: 'root' })
+export class CanActivateGoodsInGuard {
+ constructor(
+ private readonly _applicationService: ApplicationService,
+ private readonly _config: Config,
+ ) {}
+
+ async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+ const pid = this._config.get('process.ids.goodsIn', z.number());
+ const process = await this._applicationService
+ .getProcessById$(pid)
+ .pipe(first())
+ .toPromise();
+ if (!process) {
+ await this._applicationService.createProcess({
+ id: this._config.get('process.ids.goodsIn'),
+ type: 'goods-in',
+ section: 'branch',
+ name: 'Abholfach',
+ });
+ }
+ this._applicationService.activateProcess(
+ this._config.get('process.ids.goodsIn'),
+ );
+ return true;
+ }
+}
diff --git a/apps/isa-app/src/app/preview/index.ts b/apps/isa-app/src/app/preview/index.ts
deleted file mode 100644
index 5b19177c2..000000000
--- a/apps/isa-app/src/app/preview/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-// start:ng42.barrel
-export * from './preview.component';
-// end:ng42.barrel
diff --git a/apps/isa-app/src/app/preview/preview.component.css b/apps/isa-app/src/app/preview/preview.component.css
deleted file mode 100644
index 882123fb5..000000000
--- a/apps/isa-app/src/app/preview/preview.component.css
+++ /dev/null
@@ -1,3 +0,0 @@
-:host {
- @apply grid min-h-screen content-center justify-center;
-}
diff --git a/apps/isa-app/src/app/preview/preview.component.html b/apps/isa-app/src/app/preview/preview.component.html
deleted file mode 100644
index 0975604cc..000000000
--- a/apps/isa-app/src/app/preview/preview.component.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
Platform: {{ platform | json }}
-
-{{ appVersion }}
-
-{{ userAgent }}
-
-Navigator: {{ navigator | json }}
-
-
-Device: {{ device }}
diff --git a/apps/isa-app/src/app/preview/preview.component.ts b/apps/isa-app/src/app/preview/preview.component.ts
deleted file mode 100644
index 4933035c3..000000000
--- a/apps/isa-app/src/app/preview/preview.component.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { Platform, PlatformModule } from '@angular/cdk/platform';
-import { CommonModule } from '@angular/common';
-import { Component } from '@angular/core';
-import { BranchDTO } from '@generated/swagger/checkout-api';
-import { BehaviorSubject } from 'rxjs';
-
-@Component({
- selector: 'app-preview',
- templateUrl: 'preview.component.html',
- styleUrls: ['preview.component.css'],
- imports: [CommonModule, PlatformModule],
-})
-export class PreviewComponent {
- selectedBranch$ = new BehaviorSubject({});
-
- get appVersion() {
- return 'App Version: ' + (window.navigator as any).appVersion;
- }
-
- get userAgent() {
- return 'User Agent: ' + (window.navigator as any).userAgent;
- }
-
- get navigator() {
- const nav = {};
- for (const i in window.navigator) nav[i] = navigator[i];
- return nav;
- }
-
- get platform() {
- return this._platform;
- }
-
- get device() {
- const isIpadNative = this._platform.IOS && !this._platform.SAFARI;
- const isIpadMini6Native = window?.navigator?.userAgent?.includes('Macintosh') && !this._platform.SAFARI;
- const isNative = isIpadNative || isIpadMini6Native;
- const isPWA = this._platform.IOS && this._platform.SAFARI;
- const isDesktop = !isNative && !isPWA;
- if (isNative) {
- if (isIpadMini6Native) {
- return 'IPAD mini 6 Native App';
- } else if (isIpadNative) {
- return 'IPAD mini 2 Native App or IPAD mini 5 Native App';
- }
- } else if (isPWA) {
- return 'IPAD Safari PWA';
- } else if (isDesktop) return 'Desktop or Macintosh';
- }
-
- constructor(private readonly _platform: Platform) {}
-
- setNewBranch(branch: BranchDTO) {
- this.selectedBranch$.next(branch);
- }
-}
diff --git a/apps/isa-app/src/app/resolvers/index.ts b/apps/isa-app/src/app/resolvers/index.ts
index 6c6b2b637..e2bdb9a65 100644
--- a/apps/isa-app/src/app/resolvers/index.ts
+++ b/apps/isa-app/src/app/resolvers/index.ts
@@ -1,4 +1,2 @@
-// start:ng42.barrel
-export * from './process-id.resolver';
-export * from './section.resolver';
-// end:ng42.barrel
+export * from './process-id.resolver';
+export * from './section.resolver';
diff --git a/apps/isa-app/src/config/config.json b/apps/isa-app/src/config/config.json
index d3fe1a09f..1e709cba4 100644
--- a/apps/isa-app/src/config/config.json
+++ b/apps/isa-app/src/config/config.json
@@ -1,87 +1,86 @@
-{
- "title": "ISA - Local",
- "silentRefresh": {
- "interval": 300000
- },
- "debug": true,
- "dev-scanner": true,
- "@cdn/product-image": {
- "url": "https://produktbilder.paragon-data.net"
- },
- "@core/auth": {
- "issuer": "https://sso-test.paragon-data.de",
- "clientId": "isa-client",
- "responseType": "code",
- "scope": "openid profile cmf_user isa-isa-webapi isa-checkout-webapi isa-cat-webapi isa-ava-webapi isa-crm-webapi isa-review-webapi isa-kpi-webapi isa-oms-webapi isa-nbo-webapi isa-print-webapi eis-service isa-inv-webapi isa-wws-webapi",
- "showDebugInformation": true
- },
- "@core/logger": {
- "logLevel": "debug"
- },
- "@swagger/isa": {
- "rootUrl": "https://isa-test.paragon-data.net/isa/v1"
- },
- "@swagger/cat": {
- "rootUrl": "https://isa-test.paragon-data.net/catsearch/v6"
- },
- "@swagger/av": {
- "rootUrl": "https://isa-test.paragon-data.net/ava/v6"
- },
- "@swagger/checkout": {
- "rootUrl": "https://isa-test.paragon-data.net/checkout/v6"
- },
- "@swagger/crm": {
- "rootUrl": "https://isa-test.paragon-data.net/crm/v6"
- },
- "@swagger/oms": {
- "rootUrl": "https://isa-test.paragon-data.net/oms/v6"
- },
- "@swagger/print": {
- "rootUrl": "https://isa-test.paragon-data.net/print/v1"
- },
- "@swagger/eis": {
- "rootUrl": "https://filialinformationsystem-test.paragon-systems.de/eiswebapi/v1"
- },
- "@swagger/remi": {
- "rootUrl": "https://isa-test.paragon-data.net/inv/v6"
- },
- "@swagger/wws": {
- "rootUrl": "https://isa-test.paragon-data.net/wws/v1"
- },
- "@domain/checkout": {
- "olaExpiration": "5m"
- },
- "hubs": {
- "notifications": {
- "url": "https://isa-test.paragon-data.net/isa/v1/rt",
- "enableAutomaticReconnect": false,
- "httpOptions": {
- "transport": 1,
- "logMessageContent": true,
- "skipNegotiation": true
- }
- }
- },
- "process": {
- "ids": {
- "goodsOut": 1000,
- "goodsIn": 2000,
- "taskCalendar": 3000,
- "remission": 4000,
- "packageInspection": 5000,
- "assortment": 6000,
- "pickupShelf": 7000
- }
- },
- "checkForUpdates": 3600000,
- "licence": {
- "scandit": "Ae8F2Wx2RMq5Lvn7UUAlWzVFZTt2+ubMAF8XtDpmPlNkBeG/LWs1M7AbgDW0LQqYLnszClEENaEHS56/6Ts2vrJ1Ux03CXUjK3jUvZpF5OchXR1CpnmpepJ6WxPCd7LMVHUGG1BbwPLDTFjP3y8uT0caTSmmGrYQWAs4CZcEF+ZBabP0z7vfm+hCZF/ebj9qqCJZcW8nH/n19hohshllzYBjFXjh87P2lIh1s6yZS3OaQWWXo/o0AKdxx7T6CVyR0/G5zq6uYJWf6rs3euUBEhpzOZHbHZK86Lvy2AVBEyVkkcttlDW1J2fA4l1W1JV/Xibz8AQV6kG482EpGF42KEoK48paZgX3e1AQsqUtmqzw294dcP4zMVstnw5/WrwKKi/5E/nOOJT2txYP1ZufIjPrwNFsqTlv7xCQlHjMzFGYwT816yD5qLRLbwOtjrkUPXNZLZ06T4upvWwJDmm8XgdeoDqMjHdcO4lwji1bl9EiIYJ/2qnsk9yZ2FqSaHzn4cbiL0f5u2HFlNAP0GUujGRlthGhHi6o4dFU+WAxKsFMKVt+SfoQUazNKHFVQgiAklTIZxIc/HUVzRvOLMxf+wFDerraBtcqGJg+g/5mrWYqeDBGhCBHtKiYf6244IJ4afzNTiH1/30SJcRzXwbEa3A7q1fJTx9/nLTOfVPrJKBQs7f/OQs2dA7LDCel8mzXdbjvsNQaeU5+iCIAq6zbTNKy1xT8wwj+VZrQmtNJs+qeznD+u29nCM24h8xCmRpvNPo4/Mww/lrTNrrNwLBSn1pMIwsH7yS9hH0v0oNAM3A6bVtk1D9qEkbyw+xZa+MZGpMP0D0CdcsqHalPcm5r/Ik="
- },
- "gender": {
- "0": "Keine Anrede",
- "1": "Enby",
- "2": "Herr",
- "4": "Frau"
- },
- "@shared/icon": "/assets/icons.json"
-}
+{
+ "title": "ISA - Local",
+ "silentRefresh": {
+ "interval": 300000
+ },
+ "debug": true,
+ "dev-scanner": true,
+ "@cdn/product-image": {
+ "url": "https://produktbilder.paragon-data.net"
+ },
+ "@core/auth": {
+ "issuer": "https://sso-test.paragon-data.de",
+ "clientId": "isa-client",
+ "responseType": "code",
+ "scope": "openid profile cmf_user isa-isa-webapi isa-checkout-webapi isa-cat-webapi isa-ava-webapi isa-crm-webapi isa-review-webapi isa-kpi-webapi isa-oms-webapi isa-nbo-webapi isa-print-webapi eis-service isa-inv-webapi isa-wws-webapi",
+ "showDebugInformation": true
+ },
+ "@core/logger": {
+ "logLevel": "debug"
+ },
+ "@swagger/isa": {
+ "rootUrl": "https://isa-test.paragon-data.net/isa/v1"
+ },
+ "@swagger/cat": {
+ "rootUrl": "https://isa-test.paragon-data.net/catsearch/v6"
+ },
+ "@swagger/av": {
+ "rootUrl": "https://isa-test.paragon-data.net/ava/v6"
+ },
+ "@swagger/checkout": {
+ "rootUrl": "https://isa-test.paragon-data.net/checkout/v6"
+ },
+ "@swagger/crm": {
+ "rootUrl": "https://isa-test.paragon-data.net/crm/v6"
+ },
+ "@swagger/oms": {
+ "rootUrl": "https://isa-test.paragon-data.net/oms/v6"
+ },
+ "@swagger/print": {
+ "rootUrl": "https://isa-test.paragon-data.net/print/v1"
+ },
+ "@swagger/eis": {
+ "rootUrl": "https://filialinformationsystem-test.paragon-systems.de/eiswebapi/v1"
+ },
+ "@swagger/remi": {
+ "rootUrl": "https://isa-test.paragon-data.net/inv/v6"
+ },
+ "@swagger/wws": {
+ "rootUrl": "https://isa-test.paragon-data.net/wws/v1"
+ },
+ "@domain/checkout": {
+ "olaExpiration": "5m"
+ },
+ "hubs": {
+ "notifications": {
+ "url": "https://isa-test.paragon-data.net/isa/v1/rt",
+ "enableAutomaticReconnect": false,
+ "httpOptions": {
+ "transport": 1,
+ "logMessageContent": true,
+ "skipNegotiation": true
+ }
+ }
+ },
+ "process": {
+ "ids": {
+ "goodsOut": 1000,
+ "goodsIn": 2000,
+ "taskCalendar": 3000,
+ "packageInspection": 5000,
+ "assortment": 6000,
+ "pickupShelf": 2000
+ }
+ },
+ "checkForUpdates": 3600000,
+ "licence": {
+ "scandit": "Ae8F2Wx2RMq5Lvn7UUAlWzVFZTt2+ubMAF8XtDpmPlNkBeG/LWs1M7AbgDW0LQqYLnszClEENaEHS56/6Ts2vrJ1Ux03CXUjK3jUvZpF5OchXR1CpnmpepJ6WxPCd7LMVHUGG1BbwPLDTFjP3y8uT0caTSmmGrYQWAs4CZcEF+ZBabP0z7vfm+hCZF/ebj9qqCJZcW8nH/n19hohshllzYBjFXjh87P2lIh1s6yZS3OaQWWXo/o0AKdxx7T6CVyR0/G5zq6uYJWf6rs3euUBEhpzOZHbHZK86Lvy2AVBEyVkkcttlDW1J2fA4l1W1JV/Xibz8AQV6kG482EpGF42KEoK48paZgX3e1AQsqUtmqzw294dcP4zMVstnw5/WrwKKi/5E/nOOJT2txYP1ZufIjPrwNFsqTlv7xCQlHjMzFGYwT816yD5qLRLbwOtjrkUPXNZLZ06T4upvWwJDmm8XgdeoDqMjHdcO4lwji1bl9EiIYJ/2qnsk9yZ2FqSaHzn4cbiL0f5u2HFlNAP0GUujGRlthGhHi6o4dFU+WAxKsFMKVt+SfoQUazNKHFVQgiAklTIZxIc/HUVzRvOLMxf+wFDerraBtcqGJg+g/5mrWYqeDBGhCBHtKiYf6244IJ4afzNTiH1/30SJcRzXwbEa3A7q1fJTx9/nLTOfVPrJKBQs7f/OQs2dA7LDCel8mzXdbjvsNQaeU5+iCIAq6zbTNKy1xT8wwj+VZrQmtNJs+qeznD+u29nCM24h8xCmRpvNPo4/Mww/lrTNrrNwLBSn1pMIwsH7yS9hH0v0oNAM3A6bVtk1D9qEkbyw+xZa+MZGpMP0D0CdcsqHalPcm5r/Ik="
+ },
+ "gender": {
+ "0": "Keine Anrede",
+ "1": "Enby",
+ "2": "Herr",
+ "4": "Frau"
+ },
+ "@shared/icon": "/assets/icons.json"
+}
diff --git a/apps/isa-app/src/core/application/application.service.ts b/apps/isa-app/src/core/application/application.service.ts
index e36958608..07bc16b26 100644
--- a/apps/isa-app/src/core/application/application.service.ts
+++ b/apps/isa-app/src/core/application/application.service.ts
@@ -1,174 +1,196 @@
-import { Injectable } from '@angular/core';
-import { Store } from '@ngrx/store';
-import { BranchDTO } from '@generated/swagger/checkout-api';
-import { isBoolean, isNumber } from '@utils/common';
-import { BehaviorSubject, Observable } from 'rxjs';
-import { first, map, switchMap } from 'rxjs/operators';
-import { ApplicationProcess } from './defs';
-import {
- removeProcess,
- selectSection,
- selectProcesses,
- setSection,
- addProcess,
- setActivatedProcess,
- selectActivatedProcess,
- patchProcess,
- patchProcessData,
- selectTitle,
- setTitle,
-} from './store';
-
-@Injectable()
-export class ApplicationService {
- private activatedProcessIdSubject = new BehaviorSubject(undefined);
-
- get activatedProcessId() {
- return this.activatedProcessIdSubject.value;
- }
-
- get activatedProcessId$() {
- return this.activatedProcessIdSubject.asObservable();
- }
-
- 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))),
- );
- }
-
- getProcessById$(processId: number): Observable {
- return this.getProcesses$().pipe(map((processes) => processes.find((process) => process.id === processId)));
- }
-
- getSection$() {
- return this.store.select(selectSection);
- }
-
- getTitle$() {
- return this.getSection$().pipe(
- map((section) => {
- return section === 'customer' ? 'Kundenbereich' : 'Filialbereich';
- }),
- );
- }
-
- /** @deprecated */
- getActivatedProcessId$() {
- return this.store.select(selectActivatedProcess).pipe(map((process) => process?.id));
- }
-
- activateProcess(activatedProcessId: number) {
- this.store.dispatch(setActivatedProcess({ activatedProcessId }));
- this.activatedProcessIdSubject.next(activatedProcessId);
- }
-
- removeProcess(processId: number) {
- this.store.dispatch(removeProcess({ processId }));
- }
-
- patchProcess(processId: number, changes: Partial) {
- this.store.dispatch(patchProcess({ processId, changes }));
- }
-
- patchProcessData(processId: number, data: Record) {
- this.store.dispatch(patchProcessData({ processId, data }));
- }
-
- getSelectedBranch$(processId?: number): Observable {
- if (!processId) {
- return this.activatedProcessId$.pipe(
- switchMap((processId) => this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranch))),
- );
- }
-
- return this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranch));
- }
-
- readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
-
- async createCustomerProcess(processId?: number): Promise {
- const processes = await this.getProcesses$('customer').pipe(first()).toPromise();
-
- 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;
- }
-
- 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');
- }
-
- 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;
- this.store.dispatch(addProcess({ process }));
- }
-
- setSection(section: 'customer' | 'branch') {
- this.store.dispatch(setSection({ section }));
- }
-
- getLastActivatedProcessWithSectionAndType$(
- section: 'customer' | 'branch',
- type: string,
- ): Observable {
- return this.getProcesses$(section).pipe(
- map((processes) =>
- processes
- ?.filter((process) => process.type === type)
- ?.reduce((latest, current) => {
- if (!latest) {
- return current;
- }
- return latest?.activated > current?.activated ? latest : current;
- }, undefined),
- ),
- );
- }
-
- getLastActivatedProcessWithSection$(section: 'customer' | 'branch'): Observable {
- return this.getProcesses$(section).pipe(
- map((processes) =>
- processes?.reduce((latest, current) => {
- if (!latest) {
- return current;
- }
- return latest?.activated > current?.activated ? latest : current;
- }, undefined),
- ),
- );
- }
-
- private _createTimestamp() {
- return Date.now();
- }
-}
+import { Injectable } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { BranchDTO } from '@generated/swagger/checkout-api';
+import { isBoolean, isNumber } from '@utils/common';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { first, map, switchMap } from 'rxjs/operators';
+import { ApplicationProcess } from './defs';
+import {
+ removeProcess,
+ selectSection,
+ selectProcesses,
+ setSection,
+ addProcess,
+ setActivatedProcess,
+ selectActivatedProcess,
+ patchProcess,
+ patchProcessData,
+ selectTitle,
+ setTitle,
+} from './store';
+
+@Injectable()
+export class ApplicationService {
+ private activatedProcessIdSubject = new BehaviorSubject(undefined);
+
+ get activatedProcessId() {
+ return this.activatedProcessIdSubject.value;
+ }
+
+ get activatedProcessId$() {
+ return this.activatedProcessIdSubject.asObservable();
+ }
+
+ 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,
+ ),
+ ),
+ );
+ }
+
+ getProcessById$(processId: number): Observable {
+ return this.getProcesses$().pipe(
+ map((processes) => processes.find((process) => process.id === processId)),
+ );
+ }
+
+ getSection$() {
+ return this.store.select(selectSection);
+ }
+
+ getTitle$() {
+ return this.getSection$().pipe(
+ map((section) => {
+ return section === 'customer' ? 'Kundenbereich' : 'Filialbereich';
+ }),
+ );
+ }
+
+ /** @deprecated */
+ getActivatedProcessId$() {
+ return this.store
+ .select(selectActivatedProcess)
+ .pipe(map((process) => process?.id));
+ }
+
+ activateProcess(activatedProcessId: number) {
+ this.store.dispatch(setActivatedProcess({ activatedProcessId }));
+ this.activatedProcessIdSubject.next(activatedProcessId);
+ }
+
+ removeProcess(processId: number) {
+ this.store.dispatch(removeProcess({ processId }));
+ }
+
+ patchProcess(processId: number, changes: Partial) {
+ this.store.dispatch(patchProcess({ processId, changes }));
+ }
+
+ patchProcessData(processId: number, data: Record) {
+ this.store.dispatch(patchProcessData({ processId, data }));
+ }
+
+ getSelectedBranch$(processId?: number): Observable {
+ if (!processId) {
+ return this.activatedProcessId$.pipe(
+ switchMap((processId) =>
+ this.getProcessById$(processId).pipe(
+ map((process) => process?.data?.selectedBranch),
+ ),
+ ),
+ );
+ }
+
+ return this.getProcessById$(processId).pipe(
+ map((process) => process?.data?.selectedBranch),
+ );
+ }
+
+ readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
+
+ async createCustomerProcess(processId?: number): Promise {
+ const processes = await this.getProcesses$('customer')
+ .pipe(first())
+ .toPromise();
+
+ 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;
+ }
+
+ 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');
+ }
+
+ 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;
+ this.store.dispatch(addProcess({ process }));
+ }
+
+ setSection(section: 'customer' | 'branch') {
+ this.store.dispatch(setSection({ section }));
+ }
+
+ getLastActivatedProcessWithSectionAndType$(
+ section: 'customer' | 'branch',
+ type: string,
+ ): Observable {
+ return this.getProcesses$(section).pipe(
+ map((processes) =>
+ processes
+ ?.filter((process) => process.type === type)
+ ?.reduce((latest, current) => {
+ if (!latest) {
+ return current;
+ }
+ return latest?.activated > current?.activated ? latest : current;
+ }, undefined),
+ ),
+ );
+ }
+
+ getLastActivatedProcessWithSection$(
+ section: 'customer' | 'branch',
+ ): Observable {
+ return this.getProcesses$(section).pipe(
+ map((processes) =>
+ processes?.reduce((latest, current) => {
+ if (!latest) {
+ return current;
+ }
+ return latest?.activated > current?.activated ? latest : current;
+ }, undefined),
+ ),
+ );
+ }
+
+ private _createTimestamp() {
+ return Date.now();
+ }
+}
diff --git a/apps/isa-app/src/domain/checkout/store/domain-checkout.reducer.ts b/apps/isa-app/src/domain/checkout/store/domain-checkout.reducer.ts
index 7b0a027b1..4f9ff411a 100644
--- a/apps/isa-app/src/domain/checkout/store/domain-checkout.reducer.ts
+++ b/apps/isa-app/src/domain/checkout/store/domain-checkout.reducer.ts
@@ -35,7 +35,7 @@ const _domainCheckoutReducer = createReducer(
const now = Date.now();
- for (let shoppingCartItem of addedShoppingCartItems) {
+ for (const shoppingCartItem of addedShoppingCartItems) {
if (shoppingCartItem.features?.orderType) {
entity.itemAvailabilityTimestamp[
`${shoppingCartItem.id}_${shoppingCartItem.features.orderType}`
@@ -275,7 +275,7 @@ function getOrCreateCheckoutEntity({
entities: Dictionary;
processId: number;
}): CheckoutEntity {
- let entity = entities[processId];
+ const entity = entities[processId];
if (isNullOrUndefined(entity)) {
return {
diff --git a/apps/isa-app/src/page/assortment/routes.ts b/apps/isa-app/src/page/assortment/routes.ts
index b2660d1c1..c7fe29076 100644
--- a/apps/isa-app/src/page/assortment/routes.ts
+++ b/apps/isa-app/src/page/assortment/routes.ts
@@ -1,14 +1,19 @@
-import { Routes } from '@angular/router';
-import { AssortmentComponent } from './assortment.component';
-import { PriceUpdateComponent } from './price-update/price-update.component';
-
-export const routes: Routes = [
- {
- path: '',
- component: AssortmentComponent,
- children: [
- { path: 'price-update', component: PriceUpdateComponent },
- { path: '**', redirectTo: 'price-update' },
- ],
- },
-];
+import { Routes } from '@angular/router';
+import { AssortmentComponent } from './assortment.component';
+import { PriceUpdateComponent } from './price-update/price-update.component';
+
+export const routes: Routes = [
+ {
+ path: '',
+ component: AssortmentComponent,
+ title: 'Sortiment',
+ children: [
+ {
+ path: 'price-update',
+ component: PriceUpdateComponent,
+ title: 'Sortiment - Preisänderung',
+ },
+ { path: '**', redirectTo: 'price-update' },
+ ],
+ },
+];
diff --git a/apps/isa-app/src/page/catalog/page-catalog-routing.module.ts b/apps/isa-app/src/page/catalog/page-catalog-routing.module.ts
index 5d796fd6c..be9e8e03c 100644
--- a/apps/isa-app/src/page/catalog/page-catalog-routing.module.ts
+++ b/apps/isa-app/src/page/catalog/page-catalog-routing.module.ts
@@ -1,125 +1,135 @@
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { ArticleDetailsComponent } from './article-details/article-details.component';
-import { ArticleSearchComponent } from './article-search/article-search.component';
-import { ArticleSearchFilterComponent } from './article-search/search-filter/search-filter.component';
-import { ArticleSearchMainComponent } from './article-search/search-main/search-main.component';
-import { ArticleSearchResultsComponent } from './article-search/search-results/search-results.component';
-import { PageCatalogComponent } from './page-catalog.component';
-import { MatomoRouteData } from 'ngx-matomo-client';
-
-const routes: Routes = [
- {
- path: '',
- component: PageCatalogComponent,
- data: {
- matomo: {
- title: 'Artikelsuche',
- } as MatomoRouteData,
- },
- children: [
- {
- path: 'filter',
- component: ArticleSearchFilterComponent,
- data: {
- matomo: {
- title: 'Artikelsuche - Filter',
- } as MatomoRouteData,
- },
- },
- {
- path: 'filter/:id',
- component: ArticleSearchFilterComponent,
- data: {
- matomo: {
- title: 'Artikelsuche - Filter',
- } as MatomoRouteData,
- },
- },
- {
- path: 'search',
- component: ArticleSearchComponent,
- outlet: 'side',
- data: {
- matomo: {
- title: 'Artikelsuche',
- } as MatomoRouteData,
- },
- children: [
- {
- path: '',
- component: ArticleSearchMainComponent,
- },
- ],
- },
- {
- path: 'results',
- component: ArticleSearchResultsComponent,
- data: {
- matomo: {
- title: 'Artikelsuche - Trefferliste',
- } as MatomoRouteData,
- },
- },
- {
- path: 'results',
- component: ArticleSearchResultsComponent,
- outlet: 'side',
- data: {
- matomo: {
- title: 'Artikelsuche - Trefferliste',
- } as MatomoRouteData,
- },
- },
- {
- path: 'results/:id',
- component: ArticleSearchResultsComponent,
- outlet: 'side',
- data: {
- matomo: {
- title: 'Artikelsuche - Artikeldetails',
- } as MatomoRouteData,
- },
- },
- {
- path: 'results/:ean/ean',
- component: ArticleSearchResultsComponent,
- outlet: 'side',
- data: {
- matomo: {
- title: 'Artikelsuche - Artikeldetails (EAN)',
- } as MatomoRouteData,
- },
- },
- {
- path: 'details/:id',
- component: ArticleDetailsComponent,
- data: {
- matomo: {
- title: 'Artikelsuche - Artikeldetails (ID)',
- } as MatomoRouteData,
- },
- },
- {
- path: 'details/:ean/ean',
- component: ArticleDetailsComponent,
- data: {
- matomo: {
- title: 'Artikelsuche - Artikeldetails (EAN)',
- } as MatomoRouteData,
- },
- },
- {
- path: '',
- pathMatch: 'full',
- redirectTo: 'filter',
- },
- ],
- },
-];
-
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule],
-})
-export class PageCatalogRoutingModule {}
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { ArticleDetailsComponent } from './article-details/article-details.component';
+import { ArticleSearchComponent } from './article-search/article-search.component';
+import { ArticleSearchFilterComponent } from './article-search/search-filter/search-filter.component';
+import { ArticleSearchMainComponent } from './article-search/search-main/search-main.component';
+import { ArticleSearchResultsComponent } from './article-search/search-results/search-results.component';
+import { PageCatalogComponent } from './page-catalog.component';
+import { MatomoRouteData } from 'ngx-matomo-client';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: PageCatalogComponent,
+ title: 'Artikelsuche',
+ data: {
+ matomo: {
+ title: 'Artikelsuche',
+ } as MatomoRouteData,
+ },
+ children: [
+ {
+ path: 'filter',
+ component: ArticleSearchFilterComponent,
+ title: 'Artikelsuche - Filter',
+ data: {
+ matomo: {
+ title: 'Artikelsuche - Filter',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'filter/:id',
+ component: ArticleSearchFilterComponent,
+ title: 'Artikelsuche - Filter',
+ data: {
+ matomo: {
+ title: 'Artikelsuche - Filter',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'search',
+ component: ArticleSearchComponent,
+ outlet: 'side',
+ title: 'Artikelsuche',
+ data: {
+ matomo: {
+ title: 'Artikelsuche',
+ } as MatomoRouteData,
+ },
+ children: [
+ {
+ path: '',
+ component: ArticleSearchMainComponent,
+ },
+ ],
+ },
+ {
+ path: 'results',
+ component: ArticleSearchResultsComponent,
+ title: 'Artikelsuche - Trefferliste',
+ data: {
+ matomo: {
+ title: 'Artikelsuche - Trefferliste',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'results',
+ component: ArticleSearchResultsComponent,
+ outlet: 'side',
+ title: 'Artikelsuche - Trefferliste',
+ data: {
+ matomo: {
+ title: 'Artikelsuche - Trefferliste',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'results/:id',
+ component: ArticleSearchResultsComponent,
+ outlet: 'side',
+ title: 'Artikelsuche - Artikeldetails',
+ data: {
+ matomo: {
+ title: 'Artikelsuche - Artikeldetails',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'results/:ean/ean',
+ component: ArticleSearchResultsComponent,
+ outlet: 'side',
+ title: 'Artikelsuche - Artikeldetails (EAN)',
+ data: {
+ matomo: {
+ title: 'Artikelsuche - Artikeldetails (EAN)',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'details/:id',
+ component: ArticleDetailsComponent,
+ title: 'Artikelsuche - Artikeldetails (ID)',
+ data: {
+ matomo: {
+ title: 'Artikelsuche - Artikeldetails (ID)',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'details/:ean/ean',
+ component: ArticleDetailsComponent,
+ title: 'Artikelsuche - Artikeldetails (EAN)',
+ data: {
+ matomo: {
+ title: 'Artikelsuche - Artikeldetails (EAN)',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: '',
+ pathMatch: 'full',
+ redirectTo: 'filter',
+ },
+ ],
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+})
+export class PageCatalogRoutingModule {}
diff --git a/apps/isa-app/src/page/checkout/page-checkout-routing.module.ts b/apps/isa-app/src/page/checkout/page-checkout-routing.module.ts
index 041b761b9..2dc6b2e30 100644
--- a/apps/isa-app/src/page/checkout/page-checkout-routing.module.ts
+++ b/apps/isa-app/src/page/checkout/page-checkout-routing.module.ts
@@ -14,20 +14,24 @@ const routes: Routes = [
{
path: 'details',
component: CheckoutReviewDetailsComponent,
+ title: 'Bestelldetails',
outlet: 'side',
},
{
path: 'review',
component: CheckoutReviewComponent,
+ title: 'Bestellung überprüfen',
},
{
path: 'summary',
component: CheckoutSummaryComponent,
+ title: 'Bestellübersicht',
canDeactivate: [canDeactivateTabCleanup],
},
{
path: 'summary/:orderIds',
component: CheckoutSummaryComponent,
+ title: 'Bestellübersicht',
canDeactivate: [canDeactivateTabCleanup],
},
{ path: '', pathMatch: 'full', redirectTo: 'review' },
diff --git a/apps/isa-app/src/page/customer/routes.ts b/apps/isa-app/src/page/customer/routes.ts
index 68ff545e5..61f96e5ed 100644
--- a/apps/isa-app/src/page/customer/routes.ts
+++ b/apps/isa-app/src/page/customer/routes.ts
@@ -1,170 +1,187 @@
-import { Routes } from '@angular/router';
-import { CustomerComponent } from './customer-page.component';
-import { CustomerSearchComponent } from './customer-search/customer-search.component';
-import { CustomerResultsMainViewComponent } from './customer-search/results-main-view/results-main-view.component';
-import { CustomerDetailsViewMainComponent } from './customer-search/details-main-view/details-main-view.component';
-import { CustomerHistoryMainViewComponent } from './customer-search/history-main-view/history-main-view.component';
-import { CustomerFilterMainViewComponent } from './customer-search/filter-main-view/filter-main-view.component';
-import { CustomerCreateGuard } from './guards/customer-create.guard';
-import {
- CreateB2BCustomerComponent,
- CreateGuestCustomerComponent,
- // CreateP4MCustomerComponent,
- CreateStoreCustomerComponent,
- CreateWebshopCustomerComponent,
-} from './create-customer';
-// import { UpdateP4MWebshopCustomerComponent } from './create-customer/update-p4m-webshop-customer';
-import { CreateCustomerComponent } from './create-customer/create-customer.component';
-import { CustomerDataEditB2BComponent } from './customer-search/edit-main-view/customer-data-edit-b2b.component';
-import { CustomerDataEditB2CComponent } from './customer-search/edit-main-view/customer-data-edit-b2c.component';
-import { AddBillingAddressMainViewComponent } from './customer-search/add-billing-address-main-view/add-billing-address-main-view.component';
-import { AddShippingAddressMainViewComponent } from './customer-search/add-shipping-address-main-view/add-shipping-address-main-view.component';
-import { EditBillingAddressMainViewComponent } from './customer-search/edit-billing-address-main-view/edit-billing-address-main-view.component';
-import { EditShippingAddressMainViewComponent } from './customer-search/edit-shipping-address-main-view/edit-shipping-address-main-view.component';
-import { CustomerOrdersMainViewComponent } from './customer-search/orders-main-view/orders-main-view.component';
-import { OrderDetailsMainViewComponent } from './customer-search/order-details-main-view/order-details-main-view.component';
-import { KundenkarteMainViewComponent } from './customer-search/kundenkarte-main-view/kundenkarte-main-view.component';
-import { CustomerOrderDetailsHistoryMainViewComponent } from './customer-search/order-details-history-main-view/order-details-history-main-view.component';
-import { CustomerMainViewComponent } from './customer-search/main-view/main-view.component';
-import { MainSideViewComponent } from './customer-search/main-side-view/main-side-view.component';
-import { CustomerResultsSideViewComponent } from './customer-search/results-side-view/results-side-view.component';
-import { OrderDetailsSideViewComponent } from './customer-search/order-details-side-view/order-details-side-view.component';
-import { CustomerCreateSideViewComponent } from './create-customer/customer-create-side-view';
-
-export const routes: Routes = [
- {
- path: '',
- component: CustomerComponent,
- children: [
- {
- path: '',
- component: CustomerSearchComponent,
- children: [
- {
- path: 'search',
- component: CustomerMainViewComponent,
- data: { side: 'main', breadcrumb: 'main' },
- },
- {
- path: 'search/list',
- component: CustomerResultsMainViewComponent,
- data: { breadcrumb: 'search' },
- },
- {
- path: 'search/filter',
- component: CustomerFilterMainViewComponent,
- data: { side: 'results', breadcrumb: 'filter' },
- },
- {
- path: 'search/:customerId',
- component: CustomerDetailsViewMainComponent,
- data: { side: 'results', breadcrumb: 'details' },
- },
- {
- path: 'search/:customerId/history',
- component: CustomerHistoryMainViewComponent,
- data: { side: 'results', breadcrumb: 'history' },
- },
- {
- path: 'search/:customerId/kundenkarte',
- component: KundenkarteMainViewComponent,
- data: { side: 'results', breadcrumb: 'kundenkarte' },
- },
- {
- path: 'search/:customerId/orders',
- component: CustomerOrdersMainViewComponent,
- data: { side: 'results', breadcrumb: 'orders' },
- },
- {
- path: 'search/:customerId/orders/:orderId',
- component: OrderDetailsMainViewComponent,
- data: { side: 'order-details', breadcrumb: 'order-details' },
- },
- {
- path: 'search/:customerId/orders/:orderId/:orderItemId',
- component: OrderDetailsMainViewComponent,
- data: { side: 'order-details', breadcrumb: 'order-details' },
- },
- {
- path: 'search/:customerId/orders/:orderId/:orderItemId/history',
- component: CustomerOrderDetailsHistoryMainViewComponent,
- data: {
- side: 'order-details',
- breadcrumb: 'order-details-history',
- },
- },
- {
- path: 'search/:customerId/edit/b2b',
- component: CustomerDataEditB2BComponent,
- data: { side: 'results', breadcrumb: 'edit' },
- },
- {
- path: 'search/:customerId/edit',
- component: CustomerDataEditB2CComponent,
- data: { side: 'results', breadcrumb: 'edit' },
- },
- {
- path: 'search/:customerId/billingaddress/add',
- component: AddBillingAddressMainViewComponent,
- data: { side: 'results', breadcrumb: 'add-billing-address' },
- },
- {
- path: 'search/:customerId/billingaddress/:payerId/edit',
- component: EditBillingAddressMainViewComponent,
- data: { side: 'results', breadcrumb: 'edit-billing-address' },
- },
- {
- path: 'search/:customerId/shippingaddress/add',
- component: AddShippingAddressMainViewComponent,
- data: { side: 'results', breadcrumb: 'add-shipping-address' },
- },
- {
- path: 'search/:customerId/shippingaddress/:shippingAddressId/edit',
- component: EditShippingAddressMainViewComponent,
- data: { side: 'results', breadcrumb: 'edit-shipping-address' },
- },
- {
- path: 'search-customer-main',
- outlet: 'side',
- component: MainSideViewComponent,
- },
- {
- path: 'results',
- outlet: 'side',
- component: CustomerResultsSideViewComponent,
- },
- {
- path: 'order-details',
- outlet: 'side',
- component: OrderDetailsSideViewComponent,
- },
- ],
- },
- {
- path: '',
- component: CreateCustomerComponent,
- canActivate: [CustomerCreateGuard],
- canActivateChild: [CustomerCreateGuard],
- children: [
- { path: 'create', component: CreateStoreCustomerComponent },
- { path: 'create/store', component: CreateStoreCustomerComponent },
- { path: 'create/webshop', component: CreateWebshopCustomerComponent },
- { path: 'create/b2b', component: CreateB2BCustomerComponent },
- { path: 'create/guest', component: CreateGuestCustomerComponent },
- // { path: 'create/webshop-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'webshop' } },
- // { path: 'create/store-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'store' } },
- // {
- // path: 'create/webshop-p4m/update',
- // component: UpdateP4MWebshopCustomerComponent,
- // data: { customerType: 'webshop' },
- // },
- {
- path: 'create-customer-main',
- outlet: 'side',
- component: CustomerCreateSideViewComponent,
- },
- ],
- },
- ],
- },
-];
+import { Routes } from '@angular/router';
+import { CustomerComponent } from './customer-page.component';
+import { CustomerSearchComponent } from './customer-search/customer-search.component';
+import { CustomerResultsMainViewComponent } from './customer-search/results-main-view/results-main-view.component';
+import { CustomerDetailsViewMainComponent } from './customer-search/details-main-view/details-main-view.component';
+import { CustomerHistoryMainViewComponent } from './customer-search/history-main-view/history-main-view.component';
+import { CustomerFilterMainViewComponent } from './customer-search/filter-main-view/filter-main-view.component';
+import { CustomerCreateGuard } from './guards/customer-create.guard';
+import {
+ CreateB2BCustomerComponent,
+ CreateGuestCustomerComponent,
+ // CreateP4MCustomerComponent,
+ CreateStoreCustomerComponent,
+ CreateWebshopCustomerComponent,
+} from './create-customer';
+// import { UpdateP4MWebshopCustomerComponent } from './create-customer/update-p4m-webshop-customer';
+import { CreateCustomerComponent } from './create-customer/create-customer.component';
+import { CustomerDataEditB2BComponent } from './customer-search/edit-main-view/customer-data-edit-b2b.component';
+import { CustomerDataEditB2CComponent } from './customer-search/edit-main-view/customer-data-edit-b2c.component';
+import { AddBillingAddressMainViewComponent } from './customer-search/add-billing-address-main-view/add-billing-address-main-view.component';
+import { AddShippingAddressMainViewComponent } from './customer-search/add-shipping-address-main-view/add-shipping-address-main-view.component';
+import { EditBillingAddressMainViewComponent } from './customer-search/edit-billing-address-main-view/edit-billing-address-main-view.component';
+import { EditShippingAddressMainViewComponent } from './customer-search/edit-shipping-address-main-view/edit-shipping-address-main-view.component';
+import { CustomerOrdersMainViewComponent } from './customer-search/orders-main-view/orders-main-view.component';
+import { OrderDetailsMainViewComponent } from './customer-search/order-details-main-view/order-details-main-view.component';
+import { KundenkarteMainViewComponent } from './customer-search/kundenkarte-main-view/kundenkarte-main-view.component';
+import { CustomerOrderDetailsHistoryMainViewComponent } from './customer-search/order-details-history-main-view/order-details-history-main-view.component';
+import { CustomerMainViewComponent } from './customer-search/main-view/main-view.component';
+import { MainSideViewComponent } from './customer-search/main-side-view/main-side-view.component';
+import { CustomerResultsSideViewComponent } from './customer-search/results-side-view/results-side-view.component';
+import { OrderDetailsSideViewComponent } from './customer-search/order-details-side-view/order-details-side-view.component';
+import { CustomerCreateSideViewComponent } from './create-customer/customer-create-side-view';
+
+export const routes: Routes = [
+ {
+ path: '',
+ component: CustomerComponent,
+ children: [
+ {
+ path: '',
+ component: CustomerSearchComponent,
+ children: [
+ {
+ path: 'search',
+ component: CustomerMainViewComponent,
+ title: 'Kundensuche',
+ data: { side: 'main', breadcrumb: 'main' },
+ },
+ {
+ path: 'search/list',
+ component: CustomerResultsMainViewComponent,
+ title: 'Kundensuche - Trefferliste',
+ data: { breadcrumb: 'search' },
+ },
+ {
+ path: 'search/filter',
+ component: CustomerFilterMainViewComponent,
+ title: 'Kundensuche - Filter',
+ data: { side: 'results', breadcrumb: 'filter' },
+ },
+ {
+ path: 'search/:customerId',
+ component: CustomerDetailsViewMainComponent,
+ title: 'Kundendetails',
+ data: { side: 'results', breadcrumb: 'details' },
+ },
+ {
+ path: 'search/:customerId/history',
+ component: CustomerHistoryMainViewComponent,
+ title: 'Kundendetails - Verlauf',
+ data: { side: 'results', breadcrumb: 'history' },
+ },
+ {
+ path: 'search/:customerId/kundenkarte',
+ component: KundenkarteMainViewComponent,
+ title: 'Kundendetails - Kundenkarte',
+ data: { side: 'results', breadcrumb: 'kundenkarte' },
+ },
+ {
+ path: 'search/:customerId/orders',
+ component: CustomerOrdersMainViewComponent,
+ title: 'Kundendetails - Bestellungen',
+ data: { side: 'results', breadcrumb: 'orders' },
+ },
+ {
+ path: 'search/:customerId/orders/:orderId',
+ component: OrderDetailsMainViewComponent,
+ title: 'Kundendetails - Bestelldetails',
+ data: { side: 'order-details', breadcrumb: 'order-details' },
+ },
+ {
+ path: 'search/:customerId/orders/:orderId/:orderItemId',
+ component: OrderDetailsMainViewComponent,
+ title: 'Kundendetails - Bestelldetails',
+ data: { side: 'order-details', breadcrumb: 'order-details' },
+ },
+ {
+ path: 'search/:customerId/orders/:orderId/:orderItemId/history',
+ component: CustomerOrderDetailsHistoryMainViewComponent,
+ title: 'Kundendetails - Bestelldetails Verlauf',
+ data: {
+ side: 'order-details',
+ breadcrumb: 'order-details-history',
+ },
+ },
+ {
+ path: 'search/:customerId/edit/b2b',
+ component: CustomerDataEditB2BComponent,
+ title: 'Kundendetails - Bearbeiten (B2B)',
+ data: { side: 'results', breadcrumb: 'edit' },
+ },
+ {
+ path: 'search/:customerId/edit',
+ component: CustomerDataEditB2CComponent,
+ title: 'Kundendetails - Bearbeiten',
+ data: { side: 'results', breadcrumb: 'edit' },
+ },
+ {
+ path: 'search/:customerId/billingaddress/add',
+ component: AddBillingAddressMainViewComponent,
+ title: 'Kundendetails - Neue Rechnungsadresse',
+ data: { side: 'results', breadcrumb: 'add-billing-address' },
+ },
+ {
+ path: 'search/:customerId/billingaddress/:payerId/edit',
+ component: EditBillingAddressMainViewComponent,
+ title: 'Kundendetails - Rechnungsadresse bearbeiten',
+ data: { side: 'results', breadcrumb: 'edit-billing-address' },
+ },
+ {
+ path: 'search/:customerId/shippingaddress/add',
+ component: AddShippingAddressMainViewComponent,
+ title: 'Kundendetails - Neue Lieferadresse',
+ data: { side: 'results', breadcrumb: 'add-shipping-address' },
+ },
+ {
+ path: 'search/:customerId/shippingaddress/:shippingAddressId/edit',
+ component: EditShippingAddressMainViewComponent,
+ title: 'Kundendetails - Lieferadresse bearbeiten',
+ data: { side: 'results', breadcrumb: 'edit-shipping-address' },
+ },
+ {
+ path: 'search-customer-main',
+ outlet: 'side',
+ component: MainSideViewComponent,
+ },
+ {
+ path: 'results',
+ outlet: 'side',
+ component: CustomerResultsSideViewComponent,
+ },
+ {
+ path: 'order-details',
+ outlet: 'side',
+ component: OrderDetailsSideViewComponent,
+ },
+ ],
+ },
+ {
+ path: '',
+ component: CreateCustomerComponent,
+ canActivate: [CustomerCreateGuard],
+ canActivateChild: [CustomerCreateGuard],
+ title: 'Kundendaten erfassen',
+ children: [
+ { path: 'create', component: CreateStoreCustomerComponent },
+ { path: 'create/store', component: CreateStoreCustomerComponent },
+ { path: 'create/webshop', component: CreateWebshopCustomerComponent },
+ { path: 'create/b2b', component: CreateB2BCustomerComponent },
+ { path: 'create/guest', component: CreateGuestCustomerComponent },
+ // { path: 'create/webshop-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'webshop' } },
+ // { path: 'create/store-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'store' } },
+ // {
+ // path: 'create/webshop-p4m/update',
+ // component: UpdateP4MWebshopCustomerComponent,
+ // data: { customerType: 'webshop' },
+ // },
+ {
+ path: 'create-customer-main',
+ outlet: 'side',
+ component: CustomerCreateSideViewComponent,
+ },
+ ],
+ },
+ ],
+ },
+];
diff --git a/apps/isa-app/src/page/dashboard/dashboard-routing-module.ts b/apps/isa-app/src/page/dashboard/dashboard-routing-module.ts
index e38f3e1b6..7206032a1 100644
--- a/apps/isa-app/src/page/dashboard/dashboard-routing-module.ts
+++ b/apps/isa-app/src/page/dashboard/dashboard-routing-module.ts
@@ -1,16 +1,17 @@
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { DashboardComponent } from './dashboard.component';
-
-const routes: Routes = [
- {
- path: '',
- component: DashboardComponent,
- },
-];
-
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule],
-})
-export class DashboardRoutingModule {}
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { DashboardComponent } from './dashboard.component';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: DashboardComponent,
+ title: 'Dashboard',
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+})
+export class DashboardRoutingModule {}
diff --git a/apps/isa-app/src/page/goods-in/good-in-routing.module.ts b/apps/isa-app/src/page/goods-in/good-in-routing.module.ts
index d0835b2e4..626b4ea8d 100644
--- a/apps/isa-app/src/page/goods-in/good-in-routing.module.ts
+++ b/apps/isa-app/src/page/goods-in/good-in-routing.module.ts
@@ -1,35 +1,51 @@
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { GoodsInCleanupListComponent } from './goods-in-cleanup-list/goods-in-cleanup-list.component';
-import { GoodsInCleanupListModule } from './goods-in-cleanup-list/goods-in-cleanup-list.module';
-import { GoodsInListComponent } from './goods-in-list/goods-in-list.component';
-import { GoodsInListModule } from './goods-in-list/goods-in-list.module';
-import { GoodsInRemissionPreviewComponent } from './goods-in-remission-preview/goods-in-remission-preview.component';
-import { GoodsInRemissionPreviewModule } from './goods-in-remission-preview/goods-in-remission-preview.module';
-import { GoodsInReservationComponent } from './goods-in-reservation/goods-in-reservation.component';
-import { GoodsInReservationModule } from './goods-in-reservation/goods-in-reservation.module';
-import { GoodsInComponent } from './goods-in.component';
-
-const routes: Routes = [
- {
- path: '',
- component: GoodsInComponent,
- children: [
- { path: 'list', component: GoodsInListComponent },
- { path: 'reservation', component: GoodsInReservationComponent },
- { path: 'cleanup', component: GoodsInCleanupListComponent },
- { path: 'preview', component: GoodsInRemissionPreviewComponent },
- ],
- },
-];
-@NgModule({
- imports: [
- RouterModule.forChild(routes),
- GoodsInListModule,
- GoodsInCleanupListModule,
- GoodsInReservationModule,
- GoodsInRemissionPreviewModule,
- ],
- exports: [RouterModule],
-})
-export class GoodsInRoutingModule {}
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { GoodsInCleanupListComponent } from './goods-in-cleanup-list/goods-in-cleanup-list.component';
+import { GoodsInCleanupListModule } from './goods-in-cleanup-list/goods-in-cleanup-list.module';
+import { GoodsInListComponent } from './goods-in-list/goods-in-list.component';
+import { GoodsInListModule } from './goods-in-list/goods-in-list.module';
+import { GoodsInRemissionPreviewComponent } from './goods-in-remission-preview/goods-in-remission-preview.component';
+import { GoodsInRemissionPreviewModule } from './goods-in-remission-preview/goods-in-remission-preview.module';
+import { GoodsInReservationComponent } from './goods-in-reservation/goods-in-reservation.component';
+import { GoodsInReservationModule } from './goods-in-reservation/goods-in-reservation.module';
+import { GoodsInComponent } from './goods-in.component';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: GoodsInComponent,
+ children: [
+ {
+ path: 'list',
+ component: GoodsInListComponent,
+ title: 'Abholfach - Fehlende',
+ },
+ {
+ path: 'reservation',
+ component: GoodsInReservationComponent,
+ title: 'Abholfach - Reservierung',
+ },
+ {
+ path: 'cleanup',
+ component: GoodsInCleanupListComponent,
+ title: 'Abholfach - Ausräumen',
+ },
+ {
+ path: 'preview',
+ component: GoodsInRemissionPreviewComponent,
+ title: 'Abholfach - Vorschau',
+ },
+ ],
+ },
+];
+@NgModule({
+ imports: [
+ RouterModule.forChild(routes),
+ GoodsInListModule,
+ GoodsInCleanupListModule,
+ GoodsInReservationModule,
+ GoodsInRemissionPreviewModule,
+ ],
+ exports: [RouterModule],
+})
+export class GoodsInRoutingModule {}
diff --git a/apps/isa-app/src/page/package-inspection/routes.ts b/apps/isa-app/src/page/package-inspection/routes.ts
index efcfee2fe..79fa1ece7 100644
--- a/apps/isa-app/src/page/package-inspection/routes.ts
+++ b/apps/isa-app/src/page/package-inspection/routes.ts
@@ -1,22 +1,24 @@
-import { Routes } from '@angular/router';
-import { PackageDetailsComponent } from './package-details';
-import { PackageInspectionComponent } from './package-inspection.component';
-import { PackageResultComponent } from './package-result';
-
-export const packageInspectionRoutes: Routes = [
- {
- path: '',
- component: PackageInspectionComponent,
- children: [
- {
- path: 'packages',
- component: PackageResultComponent,
- },
- {
- path: 'packages/:id',
- component: PackageDetailsComponent,
- },
- { path: '**', redirectTo: 'packages' },
- ],
- },
-];
+import { Routes } from '@angular/router';
+import { PackageDetailsComponent } from './package-details';
+import { PackageInspectionComponent } from './package-inspection.component';
+import { PackageResultComponent } from './package-result';
+
+export const packageInspectionRoutes: Routes = [
+ {
+ path: '',
+ component: PackageInspectionComponent,
+ title: 'Packstück-Prüfung',
+ children: [
+ {
+ path: 'packages',
+ component: PackageResultComponent,
+ },
+ {
+ path: 'packages/:id',
+ component: PackageDetailsComponent,
+ title: 'Packstück-Prüfung - Details',
+ },
+ { path: '**', redirectTo: 'packages' },
+ ],
+ },
+];
diff --git a/apps/isa-app/src/page/pickup-shelf/pickup-shelf-in/routes.ts b/apps/isa-app/src/page/pickup-shelf/pickup-shelf-in/routes.ts
index 579adbb19..1c6ac9c7e 100644
--- a/apps/isa-app/src/page/pickup-shelf/pickup-shelf-in/routes.ts
+++ b/apps/isa-app/src/page/pickup-shelf/pickup-shelf-in/routes.ts
@@ -1,146 +1,168 @@
-import { Routes } from '@angular/router';
-import { PickupShelfInComponent } from './pickup-shelf-in.component';
-import { PickupShelfFilterComponent } from '../shared/pickup-shelf-filter/pickup-shelf-filter.component';
-import { PickupShelfInDetailsComponent } from './pickup-shelf-in-details/pickup-shelf-in-details.component';
-import { viewResolver } from '../resolvers/view.resolver';
-import { PickUpShelfHistoryComponent } from '../shared/pickup-shelf-history/pickup-shelf-history.component';
-import { PickUpShelfInMainSideViewComponent } from './pickup-shelf-in-main-side-view/pickup-shelf-in-main-side-view.component';
-import { PickUpShelfInMainComponent } from './pickup-shelf-in-main/pickup-shelf-in-main.component';
-import { PickUpShelfInListComponent } from './pickup-shelf-in-list/pickup-shelf-in-list.component';
-import { PickupShelfInEditComponent } from './pickup-shelf-in-edit/pickup-shelf-in-edit.component';
-import { MatomoRouteData } from 'ngx-matomo-client';
-
-export const routes: Routes = [
- {
- path: '',
- component: PickupShelfInComponent,
- resolve: {
- view: viewResolver,
- },
- runGuardsAndResolvers: 'always',
- children: [
- {
- path: 'main',
- component: PickUpShelfInMainComponent,
- data: {
- view: 'main',
- matomo: {
- title: 'Abholfach',
- } as MatomoRouteData,
- },
- },
- {
- path: 'list',
- component: PickUpShelfInListComponent,
- data: {
- view: 'list',
- matomo: {
- title: 'Abholfach - Trefferliste',
- } as MatomoRouteData,
- },
- },
- {
- path: 'list/filter',
- component: PickupShelfFilterComponent,
- data: {
- view: 'filter',
- matomo: {
- title: 'Abholfach - Filter',
- } as MatomoRouteData,
- },
- },
- {
- path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId/edit',
- component: PickupShelfInEditComponent,
- data: {
- view: 'edit',
- matomo: {
- title: 'Abholfach - Bearbeiten',
- } as MatomoRouteData,
- },
- },
- {
- path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId/edit',
- component: PickupShelfInEditComponent,
- data: {
- view: 'edit',
- matomo: {
- title: 'Abholfach - Bearbeiten',
- } as MatomoRouteData,
- },
- },
- {
- path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId/edit',
- component: PickupShelfInEditComponent,
- data: {
- view: 'edit',
- matomo: {
- title: 'Abholfach - Bearbeiten',
- } as MatomoRouteData,
- },
- },
- {
- path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId/history',
- component: PickUpShelfHistoryComponent,
- data: {
- view: 'history',
- matomo: {
- title: 'Abholfach - Historie',
- } as MatomoRouteData,
- },
- },
- {
- path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId/history',
- component: PickUpShelfHistoryComponent,
- data: {
- view: 'history',
- matomo: {
- title: 'Abholfach - Historie',
- } as MatomoRouteData,
- },
- },
- {
- path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId/history',
- component: PickUpShelfHistoryComponent,
- data: {
- view: 'history',
- matomo: {
- title: 'Abholfach - Historie',
- } as MatomoRouteData,
- },
- },
- {
- path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId',
- component: PickupShelfInDetailsComponent,
- data: {
- view: 'details',
- matomo: {
- title: 'Abholfach - Details',
- } as MatomoRouteData,
- },
- },
- {
- path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId',
- component: PickupShelfInDetailsComponent,
- data: {
- view: 'details',
- matomo: {
- title: 'Abholfach - Details',
- } as MatomoRouteData,
- },
- },
- {
- path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId',
- component: PickupShelfInDetailsComponent,
- data: {
- view: 'details',
- matomo: {
- title: 'Abholfach - Details',
- } as MatomoRouteData,
- },
- },
- { path: 'search', component: PickUpShelfInMainSideViewComponent, outlet: 'side' },
- { path: 'list', component: PickUpShelfInListComponent, data: { view: 'list' }, outlet: 'side' },
- ],
- },
-];
+import { Routes } from '@angular/router';
+import { PickupShelfInComponent } from './pickup-shelf-in.component';
+import { PickupShelfFilterComponent } from '../shared/pickup-shelf-filter/pickup-shelf-filter.component';
+import { PickupShelfInDetailsComponent } from './pickup-shelf-in-details/pickup-shelf-in-details.component';
+import { viewResolver } from '../resolvers/view.resolver';
+import { PickUpShelfHistoryComponent } from '../shared/pickup-shelf-history/pickup-shelf-history.component';
+import { PickUpShelfInMainSideViewComponent } from './pickup-shelf-in-main-side-view/pickup-shelf-in-main-side-view.component';
+import { PickUpShelfInMainComponent } from './pickup-shelf-in-main/pickup-shelf-in-main.component';
+import { PickUpShelfInListComponent } from './pickup-shelf-in-list/pickup-shelf-in-list.component';
+import { PickupShelfInEditComponent } from './pickup-shelf-in-edit/pickup-shelf-in-edit.component';
+import { MatomoRouteData } from 'ngx-matomo-client';
+
+export const routes: Routes = [
+ {
+ path: '',
+ component: PickupShelfInComponent,
+ resolve: {
+ view: viewResolver,
+ },
+ runGuardsAndResolvers: 'always',
+ title: 'Abholfach - Einbuchen',
+ children: [
+ {
+ path: 'main',
+ component: PickUpShelfInMainComponent,
+ data: {
+ view: 'main',
+ matomo: {
+ title: 'Abholfach',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'list',
+ component: PickUpShelfInListComponent,
+ title: 'Abholfach - Trefferliste',
+ data: {
+ view: 'list',
+ matomo: {
+ title: 'Abholfach - Trefferliste',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'list/filter',
+ component: PickupShelfFilterComponent,
+ title: 'Abholfach - Filter',
+ data: {
+ view: 'filter',
+ matomo: {
+ title: 'Abholfach - Filter',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId/edit',
+ component: PickupShelfInEditComponent,
+ title: 'Abholfach - Bearbeiten',
+ data: {
+ view: 'edit',
+ matomo: {
+ title: 'Abholfach - Bearbeiten',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId/edit',
+ component: PickupShelfInEditComponent,
+ title: 'Abholfach - Bearbeiten',
+ data: {
+ view: 'edit',
+ matomo: {
+ title: 'Abholfach - Bearbeiten',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId/edit',
+ component: PickupShelfInEditComponent,
+ title: 'Abholfach - Bearbeiten',
+ data: {
+ view: 'edit',
+ matomo: {
+ title: 'Abholfach - Bearbeiten',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId/history',
+ component: PickUpShelfHistoryComponent,
+ title: 'Abholfach - Historie',
+ data: {
+ view: 'history',
+ matomo: {
+ title: 'Abholfach - Historie',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId/history',
+ component: PickUpShelfHistoryComponent,
+ title: 'Abholfach - Historie',
+ data: {
+ view: 'history',
+ matomo: {
+ title: 'Abholfach - Historie',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId/history',
+ component: PickUpShelfHistoryComponent,
+ title: 'Abholfach - Historie',
+ data: {
+ view: 'history',
+ matomo: {
+ title: 'Abholfach - Historie',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId',
+ component: PickupShelfInDetailsComponent,
+ title: 'Abholfach - Details',
+ data: {
+ view: 'details',
+ matomo: {
+ title: 'Abholfach - Details',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId',
+ component: PickupShelfInDetailsComponent,
+ title: 'Abholfach - Details',
+ data: {
+ view: 'details',
+ matomo: {
+ title: 'Abholfach - Details',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId',
+ component: PickupShelfInDetailsComponent,
+ title: 'Abholfach - Details',
+ data: {
+ view: 'details',
+ matomo: {
+ title: 'Abholfach - Details',
+ } as MatomoRouteData,
+ },
+ },
+ {
+ path: 'search',
+ component: PickUpShelfInMainSideViewComponent,
+
+ outlet: 'side',
+ },
+ {
+ path: 'list',
+ component: PickUpShelfInListComponent,
+ data: { view: 'list' },
+ outlet: 'side',
+ },
+ ],
+ },
+];
diff --git a/apps/isa-app/src/page/pickup-shelf/pickup-shelf-out/routes.ts b/apps/isa-app/src/page/pickup-shelf/pickup-shelf-out/routes.ts
index 1efa8c5f3..1fced0f6b 100644
--- a/apps/isa-app/src/page/pickup-shelf/pickup-shelf-out/routes.ts
+++ b/apps/isa-app/src/page/pickup-shelf/pickup-shelf-out/routes.ts
@@ -1,75 +1,107 @@
-import { Routes } from '@angular/router';
-import { PickupShelfOutComponent } from './pickup-shelf-out.component';
-import { PickupShelfOutMainComponent } from './pickup-shelf-out-main/pickup-shelf-out-main.component';
-
-import { PickupShelfOutListComponent } from './pickup-shelf-out-list/pickup-shelf-out-list.component';
-import { PickupShelfFilterComponent } from '../shared/pickup-shelf-filter/pickup-shelf-filter.component';
-import { PickupShelfOutDetailsComponent } from './pickup-shelf-out-details/pickup-shelf-out-details.component';
-import { PickupShelfOutMainSideViewComponent } from './pickup-shelf-out-main-side-view/pickup-shelf-out-main-side-view.component';
-import { viewResolver } from '../resolvers/view.resolver';
-import { PickUpShelfHistoryComponent } from '../shared/pickup-shelf-history/pickup-shelf-history.component';
-import { PickupShelfOutEditComponent } from './pickup-shelf-out-edit/pickup-shelf-out-edit.component';
-
-export const routes: Routes = [
- {
- path: '',
- component: PickupShelfOutComponent,
- resolve: {
- view: viewResolver,
- },
- runGuardsAndResolvers: 'always',
- children: [
- { path: 'main', component: PickupShelfOutMainComponent, data: { view: 'main' } },
- { path: 'list', component: PickupShelfOutListComponent, data: { view: 'list' } },
- { path: 'list/filter', component: PickupShelfFilterComponent, data: { view: 'filter' } },
- {
- path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/edit',
- component: PickupShelfOutEditComponent,
- data: { view: 'edit' },
- },
- {
- path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus/edit',
- component: PickupShelfOutEditComponent,
- data: { view: 'edit' },
- },
-
- {
- path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus/edit',
- component: PickupShelfOutEditComponent,
- data: { view: 'edit' },
- },
- {
- path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/history',
- component: PickUpShelfHistoryComponent,
- data: { view: 'history' },
- },
- {
- path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus/history',
- component: PickUpShelfHistoryComponent,
- data: { view: 'history' },
- },
- {
- path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus/history',
- component: PickUpShelfHistoryComponent,
- data: { view: 'history' },
- },
- {
- path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus',
- component: PickupShelfOutDetailsComponent,
- data: { view: 'details' },
- },
- {
- path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus',
- component: PickupShelfOutDetailsComponent,
- data: { view: 'details' },
- },
- {
- path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus',
- component: PickupShelfOutDetailsComponent,
- data: { view: 'details' },
- },
- { path: 'search', component: PickupShelfOutMainSideViewComponent, outlet: 'side' },
- { path: 'list', component: PickupShelfOutListComponent, data: { view: 'list' }, outlet: 'side' },
- ],
- },
-];
+import { Routes } from '@angular/router';
+import { PickupShelfOutComponent } from './pickup-shelf-out.component';
+import { PickupShelfOutMainComponent } from './pickup-shelf-out-main/pickup-shelf-out-main.component';
+
+import { PickupShelfOutListComponent } from './pickup-shelf-out-list/pickup-shelf-out-list.component';
+import { PickupShelfFilterComponent } from '../shared/pickup-shelf-filter/pickup-shelf-filter.component';
+import { PickupShelfOutDetailsComponent } from './pickup-shelf-out-details/pickup-shelf-out-details.component';
+import { PickupShelfOutMainSideViewComponent } from './pickup-shelf-out-main-side-view/pickup-shelf-out-main-side-view.component';
+import { viewResolver } from '../resolvers/view.resolver';
+import { PickUpShelfHistoryComponent } from '../shared/pickup-shelf-history/pickup-shelf-history.component';
+import { PickupShelfOutEditComponent } from './pickup-shelf-out-edit/pickup-shelf-out-edit.component';
+
+export const routes: Routes = [
+ {
+ path: '',
+ component: PickupShelfOutComponent,
+ resolve: {
+ view: viewResolver,
+ },
+ runGuardsAndResolvers: 'always',
+ title: 'Warenausgabe',
+ children: [
+ {
+ path: 'main',
+ component: PickupShelfOutMainComponent,
+ data: { view: 'main' },
+ },
+ {
+ path: 'list',
+ component: PickupShelfOutListComponent,
+ data: { view: 'list' },
+ },
+ {
+ path: 'list/filter',
+ component: PickupShelfFilterComponent,
+ data: { view: 'filter' },
+ },
+ {
+ path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/edit',
+ component: PickupShelfOutEditComponent,
+ title: 'Warenausgabe - Details',
+ data: { view: 'edit' },
+ },
+ {
+ path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus/edit',
+ component: PickupShelfOutEditComponent,
+ title: 'Warenausgabe - Details',
+ data: { view: 'edit' },
+ },
+
+ {
+ path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus/edit',
+ component: PickupShelfOutEditComponent,
+ title: 'Warenausgabe - Details',
+ data: { view: 'edit' },
+ },
+ {
+ path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/history',
+ component: PickUpShelfHistoryComponent,
+ title: 'Warenausgabe - Verlauf',
+ data: { view: 'history' },
+ },
+ {
+ path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus/history',
+ component: PickUpShelfHistoryComponent,
+ title: 'Warenausgabe - Verlauf',
+ data: { view: 'history' },
+ },
+ {
+ path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus/history',
+ component: PickUpShelfHistoryComponent,
+ title: 'Warenausgabe - Verlauf',
+ data: { view: 'history' },
+ },
+ {
+ path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus',
+ component: PickupShelfOutDetailsComponent,
+ title: 'Warenausgabe - Details',
+ data: { view: 'details' },
+ },
+ {
+ path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus',
+ component: PickupShelfOutDetailsComponent,
+ title: 'Warenausgabe - Details',
+ data: { view: 'details' },
+ },
+ {
+ path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus',
+ component: PickupShelfOutDetailsComponent,
+ title: 'Warenausgabe - Details',
+ data: { view: 'details' },
+ },
+ {
+ path: 'search',
+ component: PickupShelfOutMainSideViewComponent,
+ outlet: 'side',
+ data: { view: 'search' },
+ },
+ {
+ path: 'list',
+ component: PickupShelfOutListComponent,
+ data: { view: 'list' },
+ outlet: 'side',
+ },
+ ],
+ },
+];
diff --git a/apps/isa-app/src/page/task-calendar/page-task-calendar-routing.module.ts b/apps/isa-app/src/page/task-calendar/page-task-calendar-routing.module.ts
index b4287cc21..3d0f4e9d6 100644
--- a/apps/isa-app/src/page/task-calendar/page-task-calendar-routing.module.ts
+++ b/apps/isa-app/src/page/task-calendar/page-task-calendar-routing.module.ts
@@ -1,27 +1,36 @@
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { PageTaskCalendarComponent } from './page-task-calendar.component';
-import { CalendarComponent } from './pages/calendar/calendar.component';
-import { CalendarModule } from './pages/calendar/calendar.module';
-import { TaskSearchComponent } from './pages/task-search';
-import { TasksComponent } from './pages/tasks/tasks.component';
-import { TasksModule } from './pages/tasks/tasks.module';
-
-const routes: Routes = [
- {
- path: '',
- component: PageTaskCalendarComponent,
- children: [
- { path: 'calendar', component: CalendarComponent },
- { path: 'tasks', component: TasksComponent },
- { path: 'search', component: TaskSearchComponent },
- { path: '', pathMatch: 'full', redirectTo: 'tasks' },
- ],
- },
-];
-
-@NgModule({
- imports: [RouterModule.forChild(routes), CalendarModule, TasksModule],
- exports: [RouterModule],
-})
-export class PageTaskCalendarRoutingModule {}
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { PageTaskCalendarComponent } from './page-task-calendar.component';
+import { CalendarComponent } from './pages/calendar/calendar.component';
+import { CalendarModule } from './pages/calendar/calendar.module';
+import { TaskSearchComponent } from './pages/task-search';
+import { TasksComponent } from './pages/tasks/tasks.component';
+import { TasksModule } from './pages/tasks/tasks.module';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: PageTaskCalendarComponent,
+ title: 'Tätigkeitskalender',
+ children: [
+ { path: 'calendar', component: CalendarComponent },
+ {
+ path: 'tasks',
+ title: 'Tätigkeitskalender - Aufgaben',
+ component: TasksComponent,
+ },
+ {
+ path: 'search',
+ title: 'Tätigkeitskalender - Suche',
+ component: TaskSearchComponent,
+ },
+ { path: '', pathMatch: 'full', redirectTo: 'tasks' },
+ ],
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes), CalendarModule, TasksModule],
+ exports: [RouterModule],
+})
+export class PageTaskCalendarRoutingModule {}
diff --git a/libs/checkout/feature/reward-catalog/src/lib/routes.ts b/libs/checkout/feature/reward-catalog/src/lib/routes.ts
index 10e45b694..491b898b1 100644
--- a/libs/checkout/feature/reward-catalog/src/lib/routes.ts
+++ b/libs/checkout/feature/reward-catalog/src/lib/routes.ts
@@ -6,6 +6,7 @@ export const routes: Routes = [
{
path: '',
component: RewardCatalogComponent,
+ title: 'Prämienshop',
resolve: { querySettings: querySettingsResolverFn },
data: {
scrollPositionRestoration: true,
diff --git a/libs/checkout/feature/reward-order-confirmation/src/lib/routes.ts b/libs/checkout/feature/reward-order-confirmation/src/lib/routes.ts
index 0cae641f8..5a226fb3d 100644
--- a/libs/checkout/feature/reward-order-confirmation/src/lib/routes.ts
+++ b/libs/checkout/feature/reward-order-confirmation/src/lib/routes.ts
@@ -6,6 +6,7 @@ import { canDeactivateTabCleanup } from '@isa/core/tabs';
export const routes: Routes = [
{
path: ':displayOrderIds',
+ title: 'Prämienshop - Bestellbestätigung',
providers: [
CoreCommandModule.forChild(OMS_ACTION_HANDLERS).providers ?? [],
],
diff --git a/libs/checkout/feature/reward-shopping-cart/src/lib/routes.ts b/libs/checkout/feature/reward-shopping-cart/src/lib/routes.ts
index 086c0fe59..369746e64 100644
--- a/libs/checkout/feature/reward-shopping-cart/src/lib/routes.ts
+++ b/libs/checkout/feature/reward-shopping-cart/src/lib/routes.ts
@@ -4,6 +4,7 @@ import { RewardShoppingCartComponent } from './reward-shopping-cart.component';
export const routes: Routes = [
{
path: '',
+ title: 'Prämienshop - Warenkorb',
component: RewardShoppingCartComponent,
},
];
diff --git a/libs/common/title-management/README.md b/libs/common/title-management/README.md
new file mode 100644
index 000000000..f336382fa
--- /dev/null
+++ b/libs/common/title-management/README.md
@@ -0,0 +1,600 @@
+# @isa/common/title-management
+
+> Reusable title management patterns for Angular applications with reactive updates and tab integration.
+
+## Overview
+
+This library provides two complementary approaches for managing page titles in the ISA application:
+
+1. **`IsaTitleStrategy`** - A custom TitleStrategy for route-based static titles
+2. **`usePageTitle()`** - A reactive helper function for component-based dynamic titles
+
+Both approaches automatically:
+- Add the ISA prefix from config to all titles
+- Update the TabService for multi-tab navigation
+- Set the browser document title
+
+## When to Use What
+
+| Scenario | Recommended Approach |
+|----------|---------------------|
+| Static page title (never changes) | Route configuration with `IsaTitleStrategy` |
+| Dynamic title based on user input (search, filters) | `usePageTitle()` in component |
+| Title depends on loaded data (item name, ID) | `usePageTitle()` in component |
+| Wizard/multi-step flows with changing steps | `usePageTitle()` in component |
+| Combination of static base + dynamic suffix | Both (route + `usePageTitle()`) |
+
+## Installation
+
+This library is already installed and configured in your workspace. Import from:
+
+```typescript
+import { IsaTitleStrategy, usePageTitle } from '@isa/common/title-management';
+```
+
+## Setup
+
+### 1. Configure IsaTitleStrategy in AppModule
+
+To enable automatic title management for all routes, add the `IsaTitleStrategy` to your app providers:
+
+```typescript
+// apps/isa-app/src/app/app.module.ts
+import { TitleStrategy } from '@angular/router';
+import { IsaTitleStrategy } from '@isa/common/title-management';
+
+@NgModule({
+ providers: [
+ { provide: TitleStrategy, useClass: IsaTitleStrategy }
+ ]
+})
+export class AppModule {}
+```
+
+**Note:** This replaces Angular's default `TitleStrategy` with our custom implementation that adds the ISA prefix and updates tabs.
+
+## Usage
+
+### Static Titles (Route Configuration)
+
+For pages with fixed titles, simply add a `title` property to your route:
+
+```typescript
+// In your routing module
+const routes: Routes = [
+ {
+ path: 'dashboard',
+ component: DashboardComponent,
+ title: 'Dashboard' // Will become "ISA - Dashboard"
+ },
+ {
+ path: 'artikelsuche',
+ component: ArticleSearchComponent,
+ title: 'Artikelsuche' // Will become "ISA - Artikelsuche"
+ },
+ {
+ path: 'returns',
+ component: ReturnsComponent,
+ title: 'Rückgaben' // Will become "ISA - Rückgaben"
+ }
+];
+```
+
+The `IsaTitleStrategy` will automatically:
+- Add the configured prefix (default: "ISA")
+- Update the active tab name
+- Set the document title
+
+### Dynamic Titles (Component with Signals)
+
+For pages where the title depends on component state, use the `usePageTitle()` helper:
+
+#### Example 1: Search Page with Query Term
+
+```typescript
+import { Component, signal, computed } from '@angular/core';
+import { usePageTitle } from '@isa/common/title-management';
+
+@Component({
+ selector: 'app-article-search',
+ standalone: true,
+ template: `
+
+ {{ pageTitle().title }}
+ `
+})
+export class ArticleSearchComponent {
+ searchTerm = signal('');
+
+ // Computed signal that updates when searchTerm changes
+ pageTitle = computed(() => {
+ const term = this.searchTerm();
+ return {
+ title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche'
+ };
+ });
+
+ constructor() {
+ // Title updates automatically when searchTerm changes!
+ usePageTitle(this.pageTitle);
+ }
+}
+```
+
+**Result:**
+- Initial load: `ISA - Artikelsuche`
+- After searching "Laptop": `ISA - Artikelsuche - "Laptop"`
+- Tab name also updates automatically
+
+#### Example 2: Detail Page with Item Name
+
+```typescript
+import { Component, signal, computed, inject, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { usePageTitle } from '@isa/common/title-management';
+
+@Component({
+ selector: 'app-product-details',
+ standalone: true,
+ template: `{{ productName() || 'Loading...' }}
`
+})
+export class ProductDetailsComponent implements OnInit {
+ private route = inject(ActivatedRoute);
+
+ productName = signal(null);
+
+ pageTitle = computed(() => {
+ const name = this.productName();
+ return {
+ title: name ? `Produkt - ${name}` : 'Produkt Details'
+ };
+ });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+
+ ngOnInit() {
+ // Load product data...
+ this.productName.set('Samsung Galaxy S24');
+ }
+}
+```
+
+#### Example 3: Combining Route Title with Dynamic Content
+
+```typescript
+import { Component, signal, computed, inject } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { usePageTitle } from '@isa/common/title-management';
+
+@Component({
+ selector: 'app-order-wizard',
+ standalone: true,
+ template: `Step {{ currentStep() }} of 3
`
+})
+export class OrderWizardComponent {
+ private route = inject(ActivatedRoute);
+
+ currentStep = signal(1);
+
+ pageTitle = computed(() => {
+ // Get base title from route config
+ const baseTitle = this.route.snapshot.title || 'Bestellung';
+ const step = this.currentStep();
+ return {
+ title: `${baseTitle} - Schritt ${step}/3`
+ };
+ });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+}
+```
+
+### Nested Components (Parent/Child Routes)
+
+When using `usePageTitle()` in nested component hierarchies (parent/child routes), the **deepest component automatically wins**. When the child component is destroyed (e.g., navigating away), the parent's title is automatically restored.
+
+**This happens automatically** - no configuration or depth tracking needed!
+
+#### Example: Dashboard → Settings Flow
+
+```typescript
+// Parent route: /dashboard
+@Component({
+ selector: 'app-dashboard',
+ standalone: true,
+ template: ``
+})
+export class DashboardComponent {
+ pageTitle = signal({ title: 'Dashboard' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ // Sets: "ISA - Dashboard"
+ }
+}
+
+// Child route: /dashboard/settings
+@Component({
+ selector: 'app-settings',
+ standalone: true,
+ template: `Settings
`
+})
+export class SettingsComponent {
+ pageTitle = signal({ title: 'Settings' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ // Sets: "ISA - Settings" (child wins!)
+ }
+}
+
+// Navigation flow:
+// 1. Navigate to /dashboard → Title: "ISA - Dashboard"
+// 2. Navigate to /dashboard/settings → Title: "ISA - Settings" (child takes over)
+// 3. Navigate back to /dashboard → Title: "ISA - Dashboard" (parent restored automatically)
+```
+
+#### How It Works
+
+The library uses an internal registry that tracks component creation order:
+- **Last-registered (deepest) component controls the title**
+- **Parent components' title updates are ignored** while child is active
+- **Automatic cleanup** via Angular's `DestroyRef` - when child is destroyed, parent becomes active again
+
+#### Real-World Scenario
+
+```typescript
+// Main page with search
+@Component({...})
+export class ArticleSearchComponent {
+ searchTerm = signal('');
+
+ pageTitle = computed(() => {
+ const term = this.searchTerm();
+ return {
+ title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche'
+ };
+ });
+
+ constructor() {
+ usePageTitle(this.pageTitle); // "ISA - Artikelsuche"
+ }
+}
+
+// Detail view (child route)
+@Component({...})
+export class ArticleDetailsComponent {
+ articleName = signal('Samsung Galaxy S24');
+
+ pageTitle = computed(() => ({
+ title: `Artikel - ${this.articleName()}`
+ }));
+
+ constructor() {
+ usePageTitle(this.pageTitle); // "ISA - Artikel - Samsung Galaxy S24" (wins!)
+ }
+}
+
+// When user closes detail view → "ISA - Artikelsuche" is restored automatically
+```
+
+#### Important Notes
+
+✅ **Works with any nesting depth** - Grandparent → Parent → Child → Grandchild
+
+✅ **No manual depth tracking** - Registration order determines precedence
+
+✅ **Automatic restoration** - Parent title restored when child is destroyed
+
+⚠️ **Parent signal updates are ignored** while child is active (by design!)
+
+### Tab Subtitles
+
+You can include a subtitle in the signal to display additional context in the tab:
+
+```typescript
+constructor() {
+ this.pageTitle = signal({
+ title: 'Dashboard',
+ subtitle: 'Active Orders'
+ });
+ usePageTitle(this.pageTitle);
+}
+```
+
+**Use Cases for Subtitles:**
+- **Status indicators**: `"Pending"`, `"Active"`, `"Completed"`
+- **Context information**: `"3 items"`, `"Last updated: 2min ago"`
+- **Category labels**: `"Customer"`, `"Order"`, `"Product"`
+- **Step indicators**: `"Step 2 of 5"`, `"Review"`
+
+#### Example: Order Processing with Status
+
+```typescript
+@Component({
+ selector: 'app-order-details',
+ standalone: true,
+ template: `
+ Order {{ orderId() }}
+ Status: {{ orderStatus() }}
+ `
+})
+export class OrderDetailsComponent {
+ orderId = signal('12345');
+ orderStatus = signal<'pending' | 'processing' | 'complete'>('pending');
+
+ // Status labels for subtitle
+ statusLabels = {
+ pending: 'Awaiting Payment',
+ processing: 'In Progress',
+ complete: 'Completed'
+ };
+
+ pageTitle = computed(() => ({
+ title: `Order ${this.orderId()}`,
+ subtitle: this.statusLabels[this.orderStatus()]
+ }));
+
+ constructor() {
+ // Title and subtitle both update dynamically
+ usePageTitle(this.pageTitle);
+ }
+}
+```
+
+#### Example: Search Results with Count
+
+```typescript
+@Component({
+ selector: 'app-article-search',
+ standalone: true,
+ template: `...`
+})
+export class ArticleSearchComponent {
+ searchTerm = signal('');
+ resultCount = signal(0);
+
+ pageTitle = computed(() => {
+ const term = this.searchTerm();
+ const count = this.resultCount();
+ return {
+ title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche',
+ subtitle: `${count} Ergebnis${count === 1 ? '' : 'se'}`
+ };
+ });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+}
+```
+
+### Optional Title and Subtitle
+
+Both `title` and `subtitle` are optional. When a property is `undefined`, it will not be updated:
+
+#### Skip Title Update (Subtitle Only)
+
+```typescript
+// Only update the subtitle, keep existing document title
+pageTitle = signal({ subtitle: '3 items' });
+usePageTitle(this.pageTitle);
+```
+
+#### Skip Subtitle Update (Title Only)
+
+```typescript
+// Only update the title, no subtitle
+pageTitle = signal({ title: 'Dashboard' });
+usePageTitle(this.pageTitle);
+```
+
+#### Skip All Updates
+
+```typescript
+// Empty object - skips all updates
+pageTitle = signal({});
+usePageTitle(this.pageTitle);
+```
+
+#### Conditional Updates
+
+```typescript
+// Skip title update when data is not loaded
+pageTitle = computed(() => {
+ const name = this.productName();
+ return {
+ title: name ? `Product - ${name}` : undefined,
+ subtitle: 'Loading...'
+ };
+});
+usePageTitle(this.pageTitle);
+```
+
+## Migration Guide
+
+### From Resolver-Based Titles
+
+If you're currently using a resolver for titles (e.g., `resolveTitle`), here's how to migrate:
+
+**Before (with resolver):**
+```typescript
+// In resolver file
+export const resolveTitle: (keyOrTitle: string) => ResolveFn =
+ (keyOrTitle) => (route, state) => {
+ const config = inject(Config);
+ const title = inject(Title);
+ const tabService = inject(TabService);
+
+ const titleFromConfig = config.get(`process.titles.${keyOrTitle}`, z.string().default(keyOrTitle));
+ // ... manual title setting logic
+ return titleFromConfig;
+ };
+
+// In routing module
+{
+ path: 'dashboard',
+ component: DashboardComponent,
+ resolve: { title: resolveTitle('Dashboard') }
+}
+```
+
+**After (with IsaTitleStrategy):**
+```typescript
+// No resolver needed - just use route config
+{
+ path: 'dashboard',
+ component: DashboardComponent,
+ title: 'Dashboard' // Much simpler!
+}
+```
+
+**For dynamic titles, use usePageTitle() instead:**
+```typescript
+// In component
+pageTitle = computed(() => ({ title: this.dynamicTitle() }));
+
+constructor() {
+ usePageTitle(this.pageTitle);
+}
+```
+
+## API Reference
+
+### `IsaTitleStrategy`
+
+Custom TitleStrategy implementation that extends Angular's TitleStrategy.
+
+**Methods:**
+- `updateTitle(snapshot: RouterStateSnapshot): void` - Called automatically by Angular Router
+
+**Dependencies:**
+- `@isa/core/config` - For title prefix configuration
+- `@isa/core/tabs` - For tab name updates
+- `@angular/platform-browser` - For document title updates
+
+**Configuration:**
+```typescript
+// In app providers
+{ provide: TitleStrategy, useClass: IsaTitleStrategy }
+```
+
+### `usePageTitle(titleSubtitleSignal)`
+
+Reactive helper function for managing dynamic component titles and subtitles.
+
+**Parameters:**
+- `titleSubtitleSignal: Signal` - A signal containing optional title and subtitle
+
+**Returns:** `void`
+
+**Dependencies:**
+- `@isa/core/config` - For title prefix configuration
+- `@isa/core/tabs` - For tab name updates
+- `@angular/platform-browser` - For document title updates
+
+**Example:**
+```typescript
+const pageTitle = computed(() => ({
+ title: `Search - ${query()}`,
+ subtitle: `${count()} results`
+}));
+usePageTitle(pageTitle);
+```
+
+### `PageTitleInput`
+
+Input interface for `usePageTitle()`.
+
+**Properties:**
+- `title?: string` - Optional page title (without ISA prefix). When undefined, document title is not updated.
+- `subtitle?: string` - Optional subtitle to display in the tab. When undefined, tab subtitle is not updated.
+
+## Best Practices
+
+### ✅ Do
+
+- Use route-based titles for static pages
+- Use `usePageTitle()` for dynamic content-dependent titles
+- Keep title signals computed from other signals for reactivity
+- Use descriptive, user-friendly titles
+- Combine route titles with component-level refinements for clarity
+- Use subtitles for status indicators, context info, or step numbers
+- Return `undefined` for title/subtitle when you want to skip updates
+
+### ❌ Don't
+
+- Add the "ISA" prefix manually (it's added automatically)
+- Call `Title.setTitle()` directly (use these utilities instead)
+- Create multiple effects updating the same title (use one computed signal)
+- Put long, complex logic in title computations (keep them simple)
+
+## Examples from ISA Codebase
+
+### Artikelsuche (Search with Term)
+```typescript
+@Component({...})
+export class ArticleSearchComponent {
+ searchTerm = signal('');
+
+ pageTitle = computed(() => {
+ const term = this.searchTerm();
+ return {
+ title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche'
+ };
+ });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+}
+```
+
+### Rückgabe Details (Return with ID)
+```typescript
+@Component({...})
+export class ReturnDetailsComponent {
+ returnId = signal(null);
+
+ pageTitle = computed(() => {
+ const id = this.returnId();
+ return {
+ title: id ? `Rückgabe - ${id}` : 'Rückgabe Details'
+ };
+ });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+}
+```
+
+## Testing
+
+Run tests for this library:
+
+```bash
+npx nx test common-title-management --skip-nx-cache
+```
+
+## Architecture Notes
+
+This library is placed in the **common** domain (not core) because:
+- It's a reusable utility pattern that features opt into
+- Components can function without it (unlike core infrastructure)
+- Provides patterns for solving a recurring problem (page titles)
+- Similar to other common libraries like decorators and data-access utilities
+
+## Related Libraries
+
+- `@isa/core/config` - Configuration management
+- `@isa/core/tabs` - Multi-tab navigation
+- `@isa/core/navigation` - Navigation context preservation
+
+## Support
+
+For issues or questions, refer to the main ISA documentation or contact the development team.
diff --git a/libs/common/title-management/eslint.config.cjs b/libs/common/title-management/eslint.config.cjs
new file mode 100644
index 000000000..3484a6e9e
--- /dev/null
+++ b/libs/common/title-management/eslint.config.cjs
@@ -0,0 +1,34 @@
+const nx = require('@nx/eslint-plugin');
+const baseConfig = require('../../../eslint.config.js');
+
+module.exports = [
+ ...baseConfig,
+ ...nx.configs['flat/angular'],
+ ...nx.configs['flat/angular-template'],
+ {
+ files: ['**/*.ts'],
+ rules: {
+ '@angular-eslint/directive-selector': [
+ 'error',
+ {
+ type: 'attribute',
+ prefix: 'common',
+ style: 'camelCase',
+ },
+ ],
+ '@angular-eslint/component-selector': [
+ 'error',
+ {
+ type: 'element',
+ prefix: 'common',
+ style: 'kebab-case',
+ },
+ ],
+ },
+ },
+ {
+ files: ['**/*.html'],
+ // Override or add rules here
+ rules: {},
+ },
+];
diff --git a/libs/common/title-management/project.json b/libs/common/title-management/project.json
new file mode 100644
index 000000000..bb80018f5
--- /dev/null
+++ b/libs/common/title-management/project.json
@@ -0,0 +1,20 @@
+{
+ "name": "common-title-management",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/common/title-management/src",
+ "prefix": "common",
+ "projectType": "library",
+ "tags": [],
+ "targets": {
+ "test": {
+ "executor": "@nx/vite:test",
+ "outputs": ["{options.reportsDirectory}"],
+ "options": {
+ "reportsDirectory": "../../../coverage/libs/common/title-management"
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint"
+ }
+ }
+}
diff --git a/libs/common/title-management/src/index.ts b/libs/common/title-management/src/index.ts
new file mode 100644
index 000000000..eb17b138b
--- /dev/null
+++ b/libs/common/title-management/src/index.ts
@@ -0,0 +1,3 @@
+export * from './lib/isa-title.strategy';
+export * from './lib/use-page-title.function';
+export * from './lib/title-management.types';
diff --git a/libs/common/title-management/src/lib/isa-title.strategy.spec.ts b/libs/common/title-management/src/lib/isa-title.strategy.spec.ts
new file mode 100644
index 000000000..d10044aa2
--- /dev/null
+++ b/libs/common/title-management/src/lib/isa-title.strategy.spec.ts
@@ -0,0 +1,157 @@
+import { TestBed } from '@angular/core/testing';
+import { Title } from '@angular/platform-browser';
+import { RouterStateSnapshot } from '@angular/router';
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import { signal } from '@angular/core';
+import { IsaTitleStrategy } from './isa-title.strategy';
+import { TabService } from '@isa/core/tabs';
+import { TITLE_PREFIX } from './title-prefix';
+
+describe('IsaTitleStrategy', () => {
+ let strategy: IsaTitleStrategy;
+ let titleServiceMock: { setTitle: ReturnType; getTitle: ReturnType };
+ let tabServiceMock: {
+ activatedTabId: ReturnType>;
+ patchTab: ReturnType;
+ };
+
+ beforeEach(() => {
+ // Arrange - Create mocks
+ titleServiceMock = {
+ setTitle: vi.fn(),
+ getTitle: vi.fn().mockReturnValue(''),
+ };
+
+ tabServiceMock = {
+ activatedTabId: signal(123),
+ patchTab: vi.fn(),
+ };
+
+ TestBed.configureTestingModule({
+ providers: [
+ IsaTitleStrategy,
+ { provide: Title, useValue: titleServiceMock },
+ { provide: TITLE_PREFIX, useValue: 'ISA' },
+ { provide: TabService, useValue: tabServiceMock },
+ ],
+ });
+
+ strategy = TestBed.inject(IsaTitleStrategy);
+ });
+
+ it('should be created', () => {
+ expect(strategy).toBeTruthy();
+ });
+
+ describe('updateTitle', () => {
+ it('should set document title with ISA prefix', () => {
+ // Arrange
+ const mockSnapshot = {
+ url: '/dashboard',
+ } as RouterStateSnapshot;
+
+ // Mock buildTitle to return a specific title
+ vi.spyOn(strategy, 'buildTitle').mockReturnValue('Dashboard');
+
+ // Act
+ strategy.updateTitle(mockSnapshot);
+
+ // Assert
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
+ });
+
+ it('should update tab name via TabService', () => {
+ // Arrange
+ const mockSnapshot = {
+ url: '/search',
+ } as RouterStateSnapshot;
+
+ vi.spyOn(strategy, 'buildTitle').mockReturnValue('Artikelsuche');
+
+ // Act
+ strategy.updateTitle(mockSnapshot);
+
+ // Assert
+ expect(tabServiceMock.patchTab).toHaveBeenCalledWith(123, {
+ name: 'Artikelsuche',
+ });
+ });
+
+ it('should use custom prefix from TITLE_PREFIX', () => {
+ // Arrange - Reset TestBed and configure with custom TITLE_PREFIX
+ TestBed.resetTestingModule();
+ TestBed.configureTestingModule({
+ providers: [
+ IsaTitleStrategy,
+ { provide: Title, useValue: titleServiceMock },
+ { provide: TITLE_PREFIX, useValue: 'MyApp' }, // Custom prefix
+ { provide: TabService, useValue: tabServiceMock },
+ ],
+ });
+
+ const customStrategy = TestBed.inject(IsaTitleStrategy);
+
+ const mockSnapshot = {
+ url: '/settings',
+ } as RouterStateSnapshot;
+
+ vi.spyOn(customStrategy, 'buildTitle').mockReturnValue('Settings');
+
+ // Act
+ customStrategy.updateTitle(mockSnapshot);
+
+ // Assert
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith('MyApp - Settings');
+ });
+
+ it('should not update tab when activatedTabId is null', () => {
+ // Arrange
+ tabServiceMock.activatedTabId.set(null);
+
+ const mockSnapshot = {
+ url: '/dashboard',
+ } as RouterStateSnapshot;
+
+ vi.spyOn(strategy, 'buildTitle').mockReturnValue('Dashboard');
+
+ // Act
+ strategy.updateTitle(mockSnapshot);
+
+ // Assert
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
+ expect(tabServiceMock.patchTab).not.toHaveBeenCalled();
+ });
+
+ it('should not update anything when buildTitle returns empty string', () => {
+ // Arrange
+ const mockSnapshot = {
+ url: '/no-title',
+ } as RouterStateSnapshot;
+
+ vi.spyOn(strategy, 'buildTitle').mockReturnValue('');
+
+ // Act
+ strategy.updateTitle(mockSnapshot);
+
+ // Assert
+ expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
+ expect(tabServiceMock.patchTab).not.toHaveBeenCalled();
+ });
+
+ it('should not update anything when buildTitle returns undefined', () => {
+ // Arrange
+ const mockSnapshot = {
+ url: '/no-title',
+ } as RouterStateSnapshot;
+
+ vi.spyOn(strategy, 'buildTitle').mockReturnValue(undefined as any);
+
+ // Act
+ strategy.updateTitle(mockSnapshot);
+
+ // Assert
+ expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
+ expect(tabServiceMock.patchTab).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/libs/common/title-management/src/lib/isa-title.strategy.ts b/libs/common/title-management/src/lib/isa-title.strategy.ts
new file mode 100644
index 000000000..bbb7568ce
--- /dev/null
+++ b/libs/common/title-management/src/lib/isa-title.strategy.ts
@@ -0,0 +1,89 @@
+import { Injectable, inject, Injector } from '@angular/core';
+import { Title } from '@angular/platform-browser';
+import { RouterStateSnapshot, TitleStrategy } from '@angular/router';
+import { TabService } from '@isa/core/tabs';
+import { TITLE_PREFIX } from './title-prefix';
+
+/**
+ * Custom TitleStrategy for the ISA application that:
+ * 1. Automatically adds the ISA prefix from config to all page titles
+ * 2. Updates the TabService with the page title for tab management
+ * 3. Sets the browser document title
+ *
+ * @example
+ * ```typescript
+ * // In app.module.ts or app.config.ts
+ * import { TitleStrategy } from '@angular/router';
+ * import { IsaTitleStrategy } from '@isa/common/title-management';
+ *
+ * @NgModule({
+ * providers: [
+ * { provide: TitleStrategy, useClass: IsaTitleStrategy }
+ * ]
+ * })
+ * export class AppModule {}
+ * ```
+ *
+ * @example
+ * ```typescript
+ * // Usage in routes
+ * const routes: Routes = [
+ * {
+ * path: 'dashboard',
+ * component: DashboardComponent,
+ * title: 'Dashboard' // Will become "ISA - Dashboard"
+ * },
+ * {
+ * path: 'search',
+ * component: SearchComponent,
+ * title: 'Artikelsuche' // Will become "ISA - Artikelsuche"
+ * }
+ * ];
+ * ```
+ */
+@Injectable({ providedIn: 'root' })
+export class IsaTitleStrategy extends TitleStrategy {
+ readonly #title = inject(Title);
+ readonly #injector = inject(Injector);
+ readonly #titlePrefix = inject(TITLE_PREFIX);
+
+ // Lazy injection to avoid circular dependency:
+ // TitleStrategy → TabService → UserStorageProvider → AuthService → Router → TitleStrategy
+ #getTabService() {
+ return this.#injector.get(TabService);
+ }
+
+ /**
+ * Updates the page title when navigation occurs.
+ * This method is called automatically by Angular's Router.
+ *
+ * @param snapshot - The router state snapshot containing route data
+ */
+ override updateTitle(snapshot: RouterStateSnapshot): void {
+ const pageTitle = this.buildTitle(snapshot);
+
+ if (pageTitle) {
+ this.#updateTitleAndTab(pageTitle);
+ }
+ }
+
+ /**
+ * Updates both the document title and the tab name with the given page title.
+ *
+ * @param pageTitle - The page title to set (without prefix)
+ * @private
+ */
+ #updateTitleAndTab(pageTitle: string): void {
+ const fullTitle = `${this.#titlePrefix} - ${pageTitle}`;
+
+ this.#title.setTitle(fullTitle);
+
+ const tabService = this.#getTabService();
+ const activeTabId = tabService.activatedTabId();
+ if (activeTabId !== null) {
+ tabService.patchTab(activeTabId, {
+ name: pageTitle,
+ });
+ }
+ }
+}
diff --git a/libs/common/title-management/src/lib/title-management.types.ts b/libs/common/title-management/src/lib/title-management.types.ts
new file mode 100644
index 000000000..43e2a78e8
--- /dev/null
+++ b/libs/common/title-management/src/lib/title-management.types.ts
@@ -0,0 +1,29 @@
+/**
+ * Input type for the `usePageTitle` helper function.
+ * Contains optional title and subtitle properties that can be set independently.
+ */
+export interface PageTitleInput {
+ /**
+ * Optional page title (without ISA prefix).
+ * When undefined, the document title will not be updated.
+ * @example
+ * ```typescript
+ * { title: 'Dashboard' }
+ * { title: searchTerm() ? `Search - "${searchTerm()}"` : undefined }
+ * ```
+ */
+ title?: string;
+
+ /**
+ * Optional subtitle to display in the tab.
+ * When undefined, the tab subtitle will not be updated.
+ * Useful for status indicators, context information, or step numbers.
+ * @example
+ * ```typescript
+ * { subtitle: 'Active Orders' }
+ * { subtitle: 'Step 2 of 5' }
+ * { title: 'Order Details', subtitle: 'Pending' }
+ * ```
+ */
+ subtitle?: string;
+}
diff --git a/libs/common/title-management/src/lib/title-prefix.ts b/libs/common/title-management/src/lib/title-prefix.ts
new file mode 100644
index 000000000..715b26c40
--- /dev/null
+++ b/libs/common/title-management/src/lib/title-prefix.ts
@@ -0,0 +1,14 @@
+import { inject, InjectionToken } from '@angular/core';
+import { Config } from '@core/config';
+import { z } from 'zod';
+
+export const TITLE_PREFIX = new InjectionToken(
+ 'isa.common.title-management.title-prefix',
+ {
+ providedIn: 'root',
+ factory: () => {
+ const config = inject(Config);
+ return config.get('title', z.string().default('ISA'));
+ },
+ },
+);
diff --git a/libs/common/title-management/src/lib/title-registry.service.spec.ts b/libs/common/title-management/src/lib/title-registry.service.spec.ts
new file mode 100644
index 000000000..4821ee41f
--- /dev/null
+++ b/libs/common/title-management/src/lib/title-registry.service.spec.ts
@@ -0,0 +1,255 @@
+import { TestBed } from '@angular/core/testing';
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import { TitleRegistryService } from './title-registry.service';
+
+describe('TitleRegistryService', () => {
+ let service: TitleRegistryService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [TitleRegistryService],
+ });
+
+ service = TestBed.inject(TitleRegistryService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ describe('register', () => {
+ it('should register and immediately execute the updater', () => {
+ // Arrange
+ const updater = vi.fn();
+
+ // Act
+ service.register(updater);
+
+ // Assert
+ expect(updater).toHaveBeenCalledOnce();
+ });
+
+ it('should return a unique symbol for each registration', () => {
+ // Arrange
+ const updater1 = vi.fn();
+ const updater2 = vi.fn();
+
+ // Act
+ const id1 = service.register(updater1);
+ const id2 = service.register(updater2);
+
+ // Assert
+ expect(id1).not.toBe(id2);
+ expect(typeof id1).toBe('symbol');
+ expect(typeof id2).toBe('symbol');
+ });
+
+ it('should make the last registered updater the active one', () => {
+ // Arrange
+ const updater1 = vi.fn();
+ const updater2 = vi.fn();
+ const updater3 = vi.fn();
+
+ const id1 = service.register(updater1);
+ const id2 = service.register(updater2);
+ const id3 = service.register(updater3);
+
+ vi.clearAllMocks();
+
+ // Act - Only the last registered should execute
+ service.updateIfActive(id1, updater1);
+ service.updateIfActive(id2, updater2);
+ service.updateIfActive(id3, updater3);
+
+ // Assert
+ expect(updater1).not.toHaveBeenCalled();
+ expect(updater2).not.toHaveBeenCalled();
+ expect(updater3).toHaveBeenCalledOnce();
+ });
+ });
+
+ describe('unregister', () => {
+ it('should remove the registration', () => {
+ // Arrange
+ const updater = vi.fn();
+ const id = service.register(updater);
+ vi.clearAllMocks();
+
+ // Act
+ service.unregister(id);
+ service.updateIfActive(id, updater);
+
+ // Assert - Updater should not be called after unregistration
+ expect(updater).not.toHaveBeenCalled();
+ });
+
+ it('should restore the previous registration when active one is unregistered', () => {
+ // Arrange
+ const updater1 = vi.fn();
+ const updater2 = vi.fn();
+
+ const id1 = service.register(updater1);
+ const id2 = service.register(updater2);
+
+ vi.clearAllMocks();
+
+ // Act - Unregister the active one (id2)
+ service.unregister(id2);
+
+ // Assert - updater1 should have been called to restore title
+ expect(updater1).toHaveBeenCalledOnce();
+ expect(updater2).not.toHaveBeenCalled();
+ });
+
+ it('should make the previous registration active after unregistering current active', () => {
+ // Arrange
+ const updater1 = vi.fn();
+ const updater2 = vi.fn();
+
+ const id1 = service.register(updater1);
+ const id2 = service.register(updater2);
+
+ service.unregister(id2);
+ vi.clearAllMocks();
+
+ // Act - id1 should now be active
+ service.updateIfActive(id1, updater1);
+ service.updateIfActive(id2, updater2);
+
+ // Assert
+ expect(updater1).toHaveBeenCalledOnce();
+ expect(updater2).not.toHaveBeenCalled();
+ });
+
+ it('should handle unregistering a non-active registration', () => {
+ // Arrange
+ const updater1 = vi.fn();
+ const updater2 = vi.fn();
+ const updater3 = vi.fn();
+
+ const id1 = service.register(updater1);
+ const id2 = service.register(updater2);
+ const id3 = service.register(updater3);
+
+ vi.clearAllMocks();
+
+ // Act - Unregister the middle one (not active)
+ service.unregister(id2);
+
+ // Assert - id3 should still be active, updater2 should not be called
+ service.updateIfActive(id3, updater3);
+ expect(updater2).not.toHaveBeenCalled();
+ expect(updater3).toHaveBeenCalledOnce();
+ });
+
+ it('should handle unregistering when no registrations remain', () => {
+ // Arrange
+ const updater = vi.fn();
+ const id = service.register(updater);
+ vi.clearAllMocks();
+
+ // Act
+ service.unregister(id);
+
+ // Assert - No errors should occur
+ service.updateIfActive(id, updater);
+ expect(updater).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('updateIfActive', () => {
+ it('should execute updater only if it is the active registration', () => {
+ // Arrange
+ const updater1 = vi.fn();
+ const updater2 = vi.fn();
+
+ const id1 = service.register(updater1);
+ const id2 = service.register(updater2); // This becomes active
+
+ vi.clearAllMocks();
+
+ // Act
+ service.updateIfActive(id1, updater1);
+ service.updateIfActive(id2, updater2);
+
+ // Assert
+ expect(updater1).not.toHaveBeenCalled(); // Not active
+ expect(updater2).toHaveBeenCalledOnce(); // Active
+ });
+
+ it('should not execute updater for unregistered id', () => {
+ // Arrange
+ const updater = vi.fn();
+ const id = service.register(updater);
+
+ service.unregister(id);
+ vi.clearAllMocks();
+
+ // Act
+ service.updateIfActive(id, updater);
+
+ // Assert
+ expect(updater).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('nested component scenario', () => {
+ it('should handle parent → child → back to parent flow', () => {
+ // Arrange - Simulate parent component
+ const parentUpdater = vi.fn(() => 'Parent Title');
+ const parentId = service.register(parentUpdater);
+
+ vi.clearAllMocks();
+
+ // Act 1 - Child component registers
+ const childUpdater = vi.fn(() => 'Child Title');
+ const childId = service.register(childUpdater);
+
+ // Assert 1 - Child is active
+ expect(childUpdater).toHaveBeenCalledOnce();
+
+ vi.clearAllMocks();
+
+ // Act 2 - Child component is destroyed
+ service.unregister(childId);
+
+ // Assert 2 - Parent title is restored
+ expect(parentUpdater).toHaveBeenCalledOnce();
+
+ vi.clearAllMocks();
+
+ // Act 3 - Parent updates should work
+ service.updateIfActive(parentId, parentUpdater);
+
+ // Assert 3 - Parent is active again
+ expect(parentUpdater).toHaveBeenCalledOnce();
+ });
+
+ it('should handle three-level nesting (grandparent → parent → child)', () => {
+ // Arrange
+ const grandparentUpdater = vi.fn();
+ const parentUpdater = vi.fn();
+ const childUpdater = vi.fn();
+
+ const grandparentId = service.register(grandparentUpdater);
+ const parentId = service.register(parentUpdater);
+ const childId = service.register(childUpdater);
+
+ vi.clearAllMocks();
+
+ // Act 1 - Verify child is active
+ service.updateIfActive(childId, childUpdater);
+ expect(childUpdater).toHaveBeenCalledOnce();
+
+ // Act 2 - Remove child, parent should become active
+ service.unregister(childId);
+ expect(parentUpdater).toHaveBeenCalledOnce();
+
+ vi.clearAllMocks();
+
+ // Act 3 - Remove parent, grandparent should become active
+ service.unregister(parentId);
+ expect(grandparentUpdater).toHaveBeenCalledOnce();
+ });
+ });
+});
diff --git a/libs/common/title-management/src/lib/title-registry.service.ts b/libs/common/title-management/src/lib/title-registry.service.ts
new file mode 100644
index 000000000..5731ddadf
--- /dev/null
+++ b/libs/common/title-management/src/lib/title-registry.service.ts
@@ -0,0 +1,79 @@
+import { Injectable } from '@angular/core';
+
+/**
+ * Internal service that tracks which component is currently managing the page title.
+ * This ensures that in nested component hierarchies (parent/child routes),
+ * the deepest (most recently registered) component's title takes precedence.
+ *
+ * When a child component is destroyed, the parent component's title is automatically restored.
+ *
+ * @internal
+ */
+@Injectable({ providedIn: 'root' })
+export class TitleRegistryService {
+ #registrations = new Map void>();
+ #activeRegistration: symbol | null = null;
+
+ /**
+ * Register a new title updater. The most recently registered updater
+ * becomes the active one and will control the title.
+ *
+ * This implements a stack-like behavior where the last component to register
+ * (deepest in the component hierarchy) takes precedence.
+ *
+ * @param updater - Function that updates the title
+ * @returns A symbol that uniquely identifies this registration
+ */
+ register(updater: () => void): symbol {
+ const id = Symbol('title-registration');
+ this.#registrations.set(id, updater);
+ this.#activeRegistration = id;
+
+ // Execute the updater immediately since it's now active
+ updater();
+
+ return id;
+ }
+
+ /**
+ * Unregister a title updater. If this was the active updater,
+ * the previous one (if any) becomes active and its title is restored.
+ *
+ * This ensures that when a child component is destroyed, the parent
+ * component's title is automatically restored.
+ *
+ * @param id - The symbol identifying the registration to remove
+ */
+ unregister(id: symbol): void {
+ this.#registrations.delete(id);
+
+ // If we just unregistered the active one, activate the most recent remaining one
+ if (this.#activeRegistration === id) {
+ // Get the last registration (most recent)
+ const entries = Array.from(this.#registrations.entries());
+ if (entries.length > 0) {
+ const [lastId, lastUpdater] = entries[entries.length - 1];
+ this.#activeRegistration = lastId;
+ // Restore the previous component's title
+ lastUpdater();
+ } else {
+ this.#activeRegistration = null;
+ }
+ }
+ }
+
+ /**
+ * Execute the updater only if it's the currently active registration.
+ *
+ * This prevents inactive (parent) components from overwriting the title
+ * set by active (child) components.
+ *
+ * @param id - The symbol identifying the registration
+ * @param updater - Function to execute if this registration is active
+ */
+ updateIfActive(id: symbol, updater: () => void): void {
+ if (this.#activeRegistration === id) {
+ updater();
+ }
+ }
+}
diff --git a/libs/common/title-management/src/lib/use-page-title.function.spec.ts b/libs/common/title-management/src/lib/use-page-title.function.spec.ts
new file mode 100644
index 000000000..2f7b4d63d
--- /dev/null
+++ b/libs/common/title-management/src/lib/use-page-title.function.spec.ts
@@ -0,0 +1,746 @@
+import { TestBed } from '@angular/core/testing';
+import { Component, signal, computed } from '@angular/core';
+import { Title } from '@angular/platform-browser';
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import { usePageTitle } from './use-page-title.function';
+import { TabService } from '@isa/core/tabs';
+import { TITLE_PREFIX } from './title-prefix';
+
+describe('usePageTitle', () => {
+ let titleServiceMock: { setTitle: ReturnType; getTitle: ReturnType };
+ let tabServiceMock: {
+ activatedTabId: ReturnType>;
+ patchTab: ReturnType;
+ };
+
+ beforeEach(() => {
+ // Arrange - Create mocks
+ titleServiceMock = {
+ setTitle: vi.fn(),
+ getTitle: vi.fn().mockReturnValue(''),
+ };
+
+ tabServiceMock = {
+ activatedTabId: signal(456),
+ patchTab: vi.fn(),
+ };
+
+ TestBed.configureTestingModule({
+ providers: [
+ { provide: Title, useValue: titleServiceMock },
+ { provide: TITLE_PREFIX, useValue: 'ISA' },
+ { provide: TabService, useValue: tabServiceMock },
+ ],
+ });
+ });
+
+ it('should set initial title on component creation', () => {
+ // Arrange
+ @Component({
+ standalone: true,
+ template: 'Test
',
+ })
+ class TestComponent {
+ pageTitle = signal({ title: 'Dashboard' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act
+ TestBed.createComponent(TestComponent);
+ TestBed.flushEffects();
+
+ // Assert
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
+ expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
+ name: 'Dashboard',
+ });
+ });
+
+ it('should update title when signal changes', () => {
+ // Arrange
+ @Component({
+ standalone: true,
+ template: 'Test
',
+ })
+ class TestComponent {
+ searchTerm = signal('');
+ pageTitle = computed(() => {
+ const term = this.searchTerm();
+ return {
+ title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche',
+ };
+ });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ const fixture = TestBed.createComponent(TestComponent);
+ const component = fixture.componentInstance;
+ TestBed.flushEffects();
+
+ // Clear previous calls
+ vi.clearAllMocks();
+
+ // Act
+ component.searchTerm.set('Laptop');
+ TestBed.flushEffects();
+
+ // Assert
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith(
+ 'ISA - Artikelsuche - "Laptop"'
+ );
+ expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
+ name: 'Artikelsuche - "Laptop"',
+ });
+ });
+
+ it('should not update tab when activatedTabId is null', () => {
+ // Arrange
+ tabServiceMock.activatedTabId.set(null);
+
+ @Component({
+ standalone: true,
+ template: 'Test
',
+ })
+ class TestComponent {
+ pageTitle = signal({ title: 'Profile' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act
+ TestBed.createComponent(TestComponent);
+ TestBed.flushEffects();
+
+ // Assert
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Profile');
+ expect(tabServiceMock.patchTab).not.toHaveBeenCalled();
+ });
+
+ it('should use custom prefix from TITLE_PREFIX', () => {
+ // Arrange
+ TestBed.overrideProvider(TITLE_PREFIX, { useValue: 'MyApp' });
+
+ @Component({
+ standalone: true,
+ template: 'Test
',
+ })
+ class TestComponent {
+ pageTitle = signal({ title: 'About' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act
+ TestBed.createComponent(TestComponent);
+ TestBed.flushEffects();
+
+ // Assert
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith('MyApp - About');
+ });
+
+ it('should handle multiple signal updates correctly', () => {
+ // Arrange
+ @Component({
+ standalone: true,
+ template: 'Test
',
+ })
+ class TestComponent {
+ counter = signal(0);
+ pageTitle = computed(() => ({ title: `Page ${this.counter()}` }));
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ const fixture = TestBed.createComponent(TestComponent);
+ const component = fixture.componentInstance;
+ TestBed.flushEffects();
+
+ // Clear initial call
+ vi.clearAllMocks();
+
+ // Act
+ component.counter.set(1);
+ TestBed.flushEffects();
+ component.counter.set(2);
+ TestBed.flushEffects();
+ component.counter.set(3);
+ TestBed.flushEffects();
+
+ // Assert
+ expect(titleServiceMock.setTitle).toHaveBeenCalledTimes(3);
+ expect(titleServiceMock.setTitle).toHaveBeenNthCalledWith(1, 'ISA - Page 1');
+ expect(titleServiceMock.setTitle).toHaveBeenNthCalledWith(2, 'ISA - Page 2');
+ expect(titleServiceMock.setTitle).toHaveBeenNthCalledWith(3, 'ISA - Page 3');
+ });
+
+ describe('nested components', () => {
+ it('should prioritize child component title over parent', () => {
+ // Arrange - Parent component
+ @Component({
+ standalone: true,
+ template: 'Parent
',
+ })
+ class ParentComponent {
+ pageTitle = signal({ title: 'Dashboard' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Child component
+ @Component({
+ standalone: true,
+ template: 'Child
',
+ })
+ class ChildComponent {
+ pageTitle = signal({ title: 'Settings' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act - Create parent first
+ TestBed.createComponent(ParentComponent);
+ TestBed.flushEffects();
+
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
+
+ vi.clearAllMocks();
+
+ // Act - Create child (should win)
+ TestBed.createComponent(ChildComponent);
+ TestBed.flushEffects();
+
+ // Assert - Child title should be set
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Settings');
+ });
+
+ it('should restore parent title when child component is destroyed', () => {
+ // Arrange - Parent component
+ @Component({
+ standalone: true,
+ template: 'Parent
',
+ })
+ class ParentComponent {
+ pageTitle = signal({ title: 'Dashboard' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Child component
+ @Component({
+ standalone: true,
+ template: 'Child
',
+ })
+ class ChildComponent {
+ pageTitle = signal({ title: 'Settings' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act - Create both components
+ TestBed.createComponent(ParentComponent);
+ TestBed.flushEffects();
+
+ const childFixture = TestBed.createComponent(ChildComponent);
+ TestBed.flushEffects();
+
+ vi.clearAllMocks();
+
+ // Act - Destroy child
+ childFixture.destroy();
+
+ // Assert - Parent title should be restored
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
+ });
+
+ it('should handle parent title updates when child is active', () => {
+ // Arrange - Parent component with mutable signal
+ @Component({
+ standalone: true,
+ template: 'Parent
',
+ })
+ class ParentComponent {
+ pageTitle = signal({ title: 'Dashboard' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Child component
+ @Component({
+ standalone: true,
+ template: 'Child
',
+ })
+ class ChildComponent {
+ pageTitle = signal({ title: 'Settings' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act - Create both
+ const parentFixture = TestBed.createComponent(ParentComponent);
+ const parent = parentFixture.componentInstance;
+ TestBed.flushEffects();
+
+ TestBed.createComponent(ChildComponent);
+ TestBed.flushEffects();
+
+ vi.clearAllMocks();
+
+ // Act - Parent tries to update (should be ignored while child is active)
+ parent.pageTitle.set({ title: 'Dashboard Updated' });
+ TestBed.flushEffects();
+
+ // Assert - Title should still be child's (parent update ignored)
+ expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
+ });
+
+ it('should allow parent title updates after child is destroyed', () => {
+ // Arrange - Parent component with mutable signal
+ @Component({
+ standalone: true,
+ template: 'Parent
',
+ })
+ class ParentComponent {
+ pageTitle = signal({ title: 'Dashboard' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Child component
+ @Component({
+ standalone: true,
+ template: 'Child
',
+ })
+ class ChildComponent {
+ pageTitle = signal({ title: 'Settings' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act - Create both
+ const parentFixture = TestBed.createComponent(ParentComponent);
+ const parent = parentFixture.componentInstance;
+ TestBed.flushEffects();
+
+ const childFixture = TestBed.createComponent(ChildComponent);
+ TestBed.flushEffects();
+
+ // Destroy child
+ childFixture.destroy();
+
+ vi.clearAllMocks();
+
+ // Act - Parent updates now (should work)
+ parent.pageTitle.set({ title: 'Dashboard Updated' });
+ TestBed.flushEffects();
+
+ // Assert - Parent update should be reflected
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard Updated');
+ });
+
+ it('should handle three-level nesting (grandparent → parent → child)', () => {
+ // Arrange - Three levels of components
+ @Component({
+ standalone: true,
+ template: 'Grandparent
',
+ })
+ class GrandparentComponent {
+ pageTitle = signal({ title: 'Main' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ @Component({
+ standalone: true,
+ template: 'Parent
',
+ })
+ class ParentComponent {
+ pageTitle = signal({ title: 'Dashboard' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ @Component({
+ standalone: true,
+ template: 'Child
',
+ })
+ class ChildComponent {
+ pageTitle = signal({ title: 'Settings' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act - Create all three
+ TestBed.createComponent(GrandparentComponent);
+ TestBed.flushEffects();
+
+ TestBed.createComponent(ParentComponent);
+ TestBed.flushEffects();
+
+ const childFixture = TestBed.createComponent(ChildComponent);
+ TestBed.flushEffects();
+
+ // Verify child is active
+ expect(titleServiceMock.setTitle).toHaveBeenLastCalledWith('ISA - Settings');
+
+ vi.clearAllMocks();
+
+ // Act - Destroy child
+ childFixture.destroy();
+
+ // Assert - Parent title should be restored
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
+ });
+ });
+
+ describe('subtitle', () => {
+ it('should set tab subtitle when provided', () => {
+ // Arrange
+ @Component({
+ standalone: true,
+ template: 'Test
',
+ })
+ class TestComponent {
+ pageTitle = signal({ title: 'Dashboard', subtitle: 'Overview' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act
+ TestBed.createComponent(TestComponent);
+ TestBed.flushEffects();
+
+ // Assert
+ expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
+ name: 'Dashboard',
+ subtitle: 'Overview',
+ });
+ });
+
+ it('should not include subtitle in patchTab when not provided', () => {
+ // Arrange
+ @Component({
+ standalone: true,
+ template: 'Test
',
+ })
+ class TestComponent {
+ pageTitle = signal({ title: 'Dashboard' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act
+ TestBed.createComponent(TestComponent);
+ TestBed.flushEffects();
+
+ // Assert
+ const callArgs = tabServiceMock.patchTab.mock.calls[0];
+ expect(callArgs[0]).toBe(456);
+ expect(callArgs[1]).toEqual({ name: 'Dashboard' });
+ expect(callArgs[1]).not.toHaveProperty('subtitle');
+ });
+
+ it('should maintain subtitle across title signal changes', () => {
+ // Arrange
+ @Component({
+ standalone: true,
+ template: 'Test
',
+ })
+ class TestComponent {
+ searchTerm = signal('');
+ pageTitle = computed(() => {
+ const term = this.searchTerm();
+ return {
+ title: term ? `Search - "${term}"` : 'Search',
+ subtitle: 'Active',
+ };
+ });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ const fixture = TestBed.createComponent(TestComponent);
+ const component = fixture.componentInstance;
+ TestBed.flushEffects();
+
+ vi.clearAllMocks();
+
+ // Act
+ component.searchTerm.set('Laptop');
+ TestBed.flushEffects();
+
+ // Assert
+ expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
+ name: 'Search - "Laptop"',
+ subtitle: 'Active',
+ });
+ });
+
+ it('should handle nested components each with different subtitles', () => {
+ // Arrange - Parent component
+ @Component({
+ standalone: true,
+ template: 'Parent
',
+ })
+ class ParentComponent {
+ pageTitle = signal({ title: 'Dashboard', subtitle: 'Main' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Child component
+ @Component({
+ standalone: true,
+ template: 'Child
',
+ })
+ class ChildComponent {
+ pageTitle = signal({ title: 'Settings', subtitle: 'Preferences' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act - Create parent first
+ TestBed.createComponent(ParentComponent);
+ TestBed.flushEffects();
+
+ expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
+ name: 'Dashboard',
+ subtitle: 'Main',
+ });
+
+ vi.clearAllMocks();
+
+ // Act - Create child (should win)
+ TestBed.createComponent(ChildComponent);
+ TestBed.flushEffects();
+
+ // Assert - Child subtitle should be set
+ expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
+ name: 'Settings',
+ subtitle: 'Preferences',
+ });
+ });
+ });
+
+ describe('optional title/subtitle', () => {
+ it('should skip document title update when title is undefined', () => {
+ // Arrange
+ @Component({
+ standalone: true,
+ template: 'Test
',
+ })
+ class TestComponent {
+ pageTitle = signal({ subtitle: 'Loading' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act
+ TestBed.createComponent(TestComponent);
+ TestBed.flushEffects();
+
+ // Assert
+ expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
+ expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
+ subtitle: 'Loading',
+ });
+ });
+
+ it('should skip tab subtitle update when subtitle is undefined', () => {
+ // Arrange
+ @Component({
+ standalone: true,
+ template: 'Test
',
+ })
+ class TestComponent {
+ pageTitle = signal({ title: 'Dashboard' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act
+ TestBed.createComponent(TestComponent);
+ TestBed.flushEffects();
+
+ // Assert
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
+ const callArgs = tabServiceMock.patchTab.mock.calls[0];
+ expect(callArgs[1]).toEqual({ name: 'Dashboard' });
+ expect(callArgs[1]).not.toHaveProperty('subtitle');
+ });
+
+ it('should handle empty object (skip all updates)', () => {
+ // Arrange
+ @Component({
+ standalone: true,
+ template: 'Test
',
+ })
+ class TestComponent {
+ pageTitle = signal({});
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act
+ TestBed.createComponent(TestComponent);
+ TestBed.flushEffects();
+
+ // Assert
+ expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
+ expect(tabServiceMock.patchTab).not.toHaveBeenCalled();
+ });
+
+ it('should handle dynamic changes from defined to undefined', () => {
+ // Arrange
+ @Component({
+ standalone: true,
+ template: 'Test
',
+ })
+ class TestComponent {
+ showTitle = signal(true);
+ pageTitle = computed(() => ({
+ title: this.showTitle() ? 'Dashboard' : undefined,
+ subtitle: 'Active',
+ }));
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ const fixture = TestBed.createComponent(TestComponent);
+ const component = fixture.componentInstance;
+ TestBed.flushEffects();
+
+ // Verify initial state
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Dashboard');
+
+ vi.clearAllMocks();
+
+ // Act - Hide title
+ component.showTitle.set(false);
+ TestBed.flushEffects();
+
+ // Assert - Title should not be updated, but subtitle should
+ expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
+ expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
+ subtitle: 'Active',
+ });
+ });
+
+ it('should handle both title and subtitle', () => {
+ // Arrange
+ @Component({
+ standalone: true,
+ template: 'Test
',
+ })
+ class TestComponent {
+ pageTitle = signal({ title: 'Orders', subtitle: '3 items' });
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ // Act
+ TestBed.createComponent(TestComponent);
+ TestBed.flushEffects();
+
+ // Assert
+ expect(titleServiceMock.setTitle).toHaveBeenCalledWith('ISA - Orders');
+ expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
+ name: 'Orders',
+ subtitle: '3 items',
+ });
+ });
+
+ it('should handle subtitle only (no title)', () => {
+ // Arrange
+ @Component({
+ standalone: true,
+ template: 'Test
',
+ })
+ class TestComponent {
+ count = signal(5);
+ pageTitle = computed(() => ({
+ subtitle: `${this.count()} items`,
+ }));
+
+ constructor() {
+ usePageTitle(this.pageTitle);
+ }
+ }
+
+ const fixture = TestBed.createComponent(TestComponent);
+ const component = fixture.componentInstance;
+ TestBed.flushEffects();
+
+ // Assert initial
+ expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
+ expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
+ subtitle: '5 items',
+ });
+
+ vi.clearAllMocks();
+
+ // Act - Update count
+ component.count.set(10);
+ TestBed.flushEffects();
+
+ // Assert - Only subtitle updates
+ expect(titleServiceMock.setTitle).not.toHaveBeenCalled();
+ expect(tabServiceMock.patchTab).toHaveBeenCalledWith(456, {
+ subtitle: '10 items',
+ });
+ });
+ });
+});
diff --git a/libs/common/title-management/src/lib/use-page-title.function.ts b/libs/common/title-management/src/lib/use-page-title.function.ts
new file mode 100644
index 000000000..06d71b2c2
--- /dev/null
+++ b/libs/common/title-management/src/lib/use-page-title.function.ts
@@ -0,0 +1,203 @@
+import { inject, effect, Signal, DestroyRef } from '@angular/core';
+import { Title } from '@angular/platform-browser';
+import { TabService } from '@isa/core/tabs';
+import { PageTitleInput } from './title-management.types';
+import { TitleRegistryService } from './title-registry.service';
+import { TITLE_PREFIX } from './title-prefix';
+
+/**
+ * Reactive helper function for managing dynamic page titles and subtitles in components.
+ * Uses Angular signals and effects to automatically update the document title
+ * and tab name/subtitle whenever the provided signal changes.
+ *
+ * This is ideal for pages where the title depends on component state, such as:
+ * - Search pages with query terms
+ * - Detail pages with item names
+ * - Wizard flows with step names
+ * - Filter pages with applied filters
+ * - Status indicators with changing subtitles
+ *
+ * @param titleSubtitleSignal - A signal containing optional title and subtitle
+ *
+ * @example
+ * ```typescript
+ * // Basic usage with static title
+ * import { Component, signal } from '@angular/core';
+ * import { usePageTitle } from '@isa/common/title-management';
+ *
+ * @Component({
+ * selector: 'app-dashboard',
+ * template: `Dashboard
`
+ * })
+ * export class DashboardComponent {
+ * pageTitle = signal({ title: 'Dashboard' });
+ *
+ * constructor() {
+ * usePageTitle(this.pageTitle);
+ * }
+ * }
+ * ```
+ *
+ * @example
+ * ```typescript
+ * // Dynamic title with search term
+ * import { Component, signal, computed } from '@angular/core';
+ * import { usePageTitle } from '@isa/common/title-management';
+ *
+ * @Component({
+ * selector: 'app-article-search',
+ * template: ``
+ * })
+ * export class ArticleSearchComponent {
+ * searchTerm = signal('');
+ *
+ * pageTitle = computed(() => {
+ * const term = this.searchTerm();
+ * return {
+ * title: term ? `Artikelsuche - "${term}"` : 'Artikelsuche'
+ * };
+ * });
+ *
+ * constructor() {
+ * usePageTitle(this.pageTitle);
+ * // Title updates automatically when searchTerm changes!
+ * }
+ * }
+ * ```
+ *
+ * @example
+ * ```typescript
+ * // Title with subtitle
+ * import { Component, signal, computed } from '@angular/core';
+ * import { usePageTitle } from '@isa/common/title-management';
+ *
+ * @Component({
+ * selector: 'app-order-details',
+ * template: `Order Details
`
+ * })
+ * export class OrderDetailsComponent {
+ * orderId = signal('12345');
+ * orderStatus = signal<'pending' | 'processing' | 'complete'>('pending');
+ *
+ * statusLabels = {
+ * pending: 'Awaiting Payment',
+ * processing: 'In Progress',
+ * complete: 'Completed'
+ * };
+ *
+ * pageTitle = computed(() => ({
+ * title: `Order ${this.orderId()}`,
+ * subtitle: this.statusLabels[this.orderStatus()]
+ * }));
+ *
+ * constructor() {
+ * usePageTitle(this.pageTitle);
+ * }
+ * }
+ * ```
+ *
+ * @example
+ * ```typescript
+ * // Skip title when undefined (e.g., data not loaded yet)
+ * import { Component, signal, computed } from '@angular/core';
+ * import { usePageTitle } from '@isa/common/title-management';
+ *
+ * @Component({
+ * selector: 'app-product-details',
+ * template: `{{ productName() || 'Loading...' }}
`
+ * })
+ * export class ProductDetailsComponent {
+ * productName = signal(null);
+ *
+ * pageTitle = computed(() => {
+ * const name = this.productName();
+ * return {
+ * title: name ? `Product - ${name}` : undefined
+ * };
+ * });
+ *
+ * constructor() {
+ * usePageTitle(this.pageTitle);
+ * // Title only updates when productName is not null
+ * }
+ * }
+ * ```
+ *
+ * @example
+ * ```typescript
+ * // Nested components - child component's title automatically takes precedence
+ * // Parent component (route: /dashboard)
+ * export class DashboardComponent {
+ * pageTitle = signal({ title: 'Dashboard' });
+ * constructor() {
+ * usePageTitle(this.pageTitle); // "ISA - Dashboard"
+ * }
+ * }
+ *
+ * // Child component (route: /dashboard/settings)
+ * export class SettingsComponent {
+ * pageTitle = signal({ title: 'Settings' });
+ * constructor() {
+ * usePageTitle(this.pageTitle); // "ISA - Settings" (wins!)
+ * }
+ * }
+ * // When SettingsComponent is destroyed → "ISA - Dashboard" (automatically restored)
+ * ```
+ */
+export function usePageTitle(
+ titleSubtitleSignal: Signal
+): void {
+ const title = inject(Title);
+ const titlePrefix = inject(TITLE_PREFIX);
+ const tabService = inject(TabService);
+ const registry = inject(TitleRegistryService);
+ const destroyRef = inject(DestroyRef);
+
+ // Create the updater function that will be called by the registry
+ const updateTitle = () => {
+ const { title: pageTitle, subtitle } = titleSubtitleSignal();
+
+ // Update document title if title is defined
+ if (pageTitle !== undefined) {
+ const fullTitle = `${titlePrefix} - ${pageTitle}`;
+ title.setTitle(fullTitle);
+ }
+
+ // Update tab if activeTabId exists
+ const activeTabId = tabService.activatedTabId();
+ if (activeTabId !== null) {
+ // Build patch object conditionally based on what's defined
+ const patch: { name?: string; subtitle?: string } = {};
+
+ if (pageTitle !== undefined) {
+ patch.name = pageTitle;
+ }
+
+ if (subtitle !== undefined) {
+ patch.subtitle = subtitle;
+ }
+
+ // Only patch if we have something to update
+ if (Object.keys(patch).length > 0) {
+ tabService.patchTab(activeTabId, patch);
+ }
+ }
+ };
+
+ // Register with the registry (this will execute updateTitle immediately)
+ const registrationId = registry.register(updateTitle);
+
+ // Automatically unregister when component is destroyed
+ destroyRef.onDestroy(() => {
+ registry.unregister(registrationId);
+ });
+
+ // React to signal changes, but only update if this is the active registration
+ effect(() => {
+ // Access the signal to track it as a dependency
+ titleSubtitleSignal();
+
+ // Only update if this component is the active one
+ registry.updateIfActive(registrationId, updateTitle);
+ });
+}
diff --git a/libs/common/title-management/src/test-setup.ts b/libs/common/title-management/src/test-setup.ts
new file mode 100644
index 000000000..cebf5ae72
--- /dev/null
+++ b/libs/common/title-management/src/test-setup.ts
@@ -0,0 +1,13 @@
+import '@angular/compiler';
+import '@analogjs/vitest-angular/setup-zone';
+
+import {
+ BrowserTestingModule,
+ platformBrowserTesting,
+} from '@angular/platform-browser/testing';
+import { getTestBed } from '@angular/core/testing';
+
+getTestBed().initTestEnvironment(
+ BrowserTestingModule,
+ platformBrowserTesting(),
+);
diff --git a/libs/common/title-management/tsconfig.json b/libs/common/title-management/tsconfig.json
new file mode 100644
index 000000000..3268ed4dc
--- /dev/null
+++ b/libs/common/title-management/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "importHelpers": true,
+ "moduleResolution": "bundler",
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve"
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "typeCheckHostBindings": true,
+ "strictTemplates": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ]
+}
diff --git a/libs/common/title-management/tsconfig.lib.json b/libs/common/title-management/tsconfig.lib.json
new file mode 100644
index 000000000..312ee86bb
--- /dev/null
+++ b/libs/common/title-management/tsconfig.lib.json
@@ -0,0 +1,27 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "src/**/*.spec.ts",
+ "src/test-setup.ts",
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "vite.config.ts",
+ "vite.config.mts",
+ "vitest.config.ts",
+ "vitest.config.mts",
+ "src/**/*.test.tsx",
+ "src/**/*.spec.tsx",
+ "src/**/*.test.js",
+ "src/**/*.spec.js",
+ "src/**/*.test.jsx",
+ "src/**/*.spec.jsx"
+ ],
+ "include": ["src/**/*.ts"]
+}
diff --git a/libs/common/title-management/tsconfig.spec.json b/libs/common/title-management/tsconfig.spec.json
new file mode 100644
index 000000000..5785a8a5f
--- /dev/null
+++ b/libs/common/title-management/tsconfig.spec.json
@@ -0,0 +1,29 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [
+ "vitest/globals",
+ "vitest/importMeta",
+ "vite/client",
+ "node",
+ "vitest"
+ ]
+ },
+ "include": [
+ "vite.config.ts",
+ "vite.config.mts",
+ "vitest.config.ts",
+ "vitest.config.mts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.test.tsx",
+ "src/**/*.spec.tsx",
+ "src/**/*.test.js",
+ "src/**/*.spec.js",
+ "src/**/*.test.jsx",
+ "src/**/*.spec.jsx",
+ "src/**/*.d.ts"
+ ],
+ "files": ["src/test-setup.ts"]
+}
diff --git a/libs/common/title-management/vite.config.mts b/libs/common/title-management/vite.config.mts
new file mode 100644
index 000000000..0a6c852da
--- /dev/null
+++ b/libs/common/title-management/vite.config.mts
@@ -0,0 +1,33 @@
+///
+import { defineConfig } from 'vite';
+import angular from '@analogjs/vite-plugin-angular';
+import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
+import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
+
+export default
+// @ts-expect-error - Vitest reporter tuple types have complex inference issues, but config works correctly at runtime
+defineConfig(() => ({
+ root: __dirname,
+ cacheDir: '../../../node_modules/.vite/libs/common/title-management',
+ plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
+ // Uncomment this if you are using workers.
+ // worker: {
+ // plugins: [ nxViteTsPaths() ],
+ // },
+ test: {
+ watch: false,
+ globals: true,
+ environment: 'jsdom',
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+ setupFiles: ['src/test-setup.ts'],
+ reporters: [
+ 'default',
+ ['junit', { outputFile: '../../../testresults/junit-common-title-management.xml' }],
+ ],
+ coverage: {
+ reportsDirectory: '../../../coverage/libs/common/title-management',
+ provider: 'v8' as const,
+ reporter: ['text', 'cobertura'],
+ },
+ },
+}));
diff --git a/libs/core/storage/src/lib/storage-providers/user.storage-provider.ts b/libs/core/storage/src/lib/storage-providers/user.storage-provider.ts
index 6b9cf58ec..b27c46dde 100644
--- a/libs/core/storage/src/lib/storage-providers/user.storage-provider.ts
+++ b/libs/core/storage/src/lib/storage-providers/user.storage-provider.ts
@@ -1,4 +1,4 @@
-import { inject, Injectable } from '@angular/core';
+import { inject, Injectable, Injector } from '@angular/core';
import { StorageProvider } from './storage-provider';
import {
ResponseArgsOfUserState,
@@ -32,9 +32,13 @@ const DEFAULT_USER_STATE_RESPONSE: ResponseArgsOfUserState = {
export class UserStorageProvider implements StorageProvider {
#logger = logger(() => ({ service: 'UserStorageProvider' }));
#userStateService = inject(UserStateService);
- #authService = inject(AuthService);
+ #injector = inject(Injector);
- #loadUserState = this.#authService.authenticated$.pipe(
+ #getAuthService() {
+ return this.#injector.get(AuthService);
+ }
+
+ #loadUserState = this.#getAuthService().authenticated$.pipe(
filter((authenticated) => authenticated),
switchMap(() =>
this.#userStateService.UserStateGetUserState().pipe(
diff --git a/libs/core/tabs/README.md b/libs/core/tabs/README.md
index 7d37a8bb0..3cdcb0afd 100644
--- a/libs/core/tabs/README.md
+++ b/libs/core/tabs/README.md
@@ -142,6 +142,7 @@ Each tab is stored as an NgRx entity with the following structure:
interface Tab {
id: number; // Unique identifier
name: string; // Display name
+ subtitle: string; // Subtitle (defaults to empty string)
createdAt: number; // Creation timestamp (ms)
activatedAt?: number; // Last activation timestamp
metadata: Record; // Flexible metadata storage
@@ -295,6 +296,7 @@ Creates and adds a new tab to the store.
**Parameters:**
- `params: AddTabInput` - Tab creation parameters (validated with Zod)
- `name?: string` - Display name (default: 'Neuer Vorgang')
+ - `subtitle?: string` - Subtitle (default: '')
- `tags?: string[]` - Initial tags (default: [])
- `metadata?: Record` - Initial metadata (default: {})
- `id?: number` - Optional ID (auto-generated if omitted)
@@ -306,6 +308,7 @@ Creates and adds a new tab to the store.
```typescript
const tab = this.#tabService.addTab({
name: 'Customer Order #1234',
+ subtitle: 'Pending approval',
tags: ['order', 'customer'],
metadata: { orderId: 1234, status: 'pending' }
});
@@ -337,6 +340,7 @@ Partially updates a tab's properties.
- `id: number` - Tab ID to update
- `changes: PatchTabInput` - Partial tab updates
- `name?: string` - Updated display name
+ - `subtitle?: string` - Updated subtitle
- `tags?: string[]` - Updated tags array
- `metadata?: Record` - Metadata to merge
- `location?: TabLocationHistory` - Updated location history
@@ -345,6 +349,7 @@ Partially updates a tab's properties.
```typescript
this.#tabService.patchTab(42, {
name: 'Updated Name',
+ subtitle: 'New subtitle',
tags: ['new-tag'],
metadata: { additionalField: 'value' }
});
diff --git a/libs/core/tabs/src/lib/schemas.ts b/libs/core/tabs/src/lib/schemas.ts
index 5c2a1b74a..be9cc2c6b 100644
--- a/libs/core/tabs/src/lib/schemas.ts
+++ b/libs/core/tabs/src/lib/schemas.ts
@@ -100,6 +100,8 @@ export const TabSchema = z.object({
id: z.number(),
/** Display name for the tab (minimum 1 character) */
name: z.string().default('Neuer Vorgang'),
+ /** Subtitle for the tab */
+ subtitle: z.string().default(''),
/** Creation timestamp (milliseconds since epoch) */
createdAt: z.number(),
/** Last activation timestamp (optional) */
@@ -123,6 +125,8 @@ export interface Tab {
id: number;
/** Display name for the tab */
name: string;
+ /** Subtitle for the tab */
+ subtitle: string;
/** Creation timestamp */
createdAt: number;
/** Last activation timestamp */
@@ -146,6 +150,8 @@ export interface TabCreate {
id?: number;
/** Display name for the tab */
name: string;
+ /** Subtitle for the tab */
+ subtitle?: string;
/** Creation timestamp */
createdAt: number;
/** Last activation timestamp */
@@ -170,6 +176,8 @@ export const PersistedTabSchema = z
id: z.number(),
/** Tab display name */
name: z.string().default('Neuer Vorgang'),
+ /** Tab subtitle */
+ subtitle: z.string(),
/** Creation timestamp */
createdAt: z.number(),
/** Last activation timestamp */
@@ -195,6 +203,8 @@ export type TabInput = z.input;
export const AddTabSchema = z.object({
/** Display name for the new tab */
name: z.string().default('Neuer Vorgang'),
+ /** Subtitle for the new tab */
+ subtitle: z.string().default(''),
/** Initial tags for the tab */
tags: TabTagsSchema,
/** Initial metadata for the tab */
@@ -221,6 +231,8 @@ export const TabUpdateSchema = z
.object({
/** Updated display name */
name: z.string().min(1).optional(),
+ /** Updated subtitle */
+ subtitle: z.string().optional(),
/** Updated activation timestamp */
activatedAt: z.number().optional(),
/** Updated metadata object */
diff --git a/libs/core/tabs/src/lib/tab.ts b/libs/core/tabs/src/lib/tab.ts
index 8f8ac9171..8d5921399 100644
--- a/libs/core/tabs/src/lib/tab.ts
+++ b/libs/core/tabs/src/lib/tab.ts
@@ -63,6 +63,7 @@ export const TabService = signalStore(
const tab: Tab = {
id: parsed.id ?? store._generateId(),
name: parsed.name,
+ subtitle: parsed.subtitle,
createdAt: Date.now(),
activatedAt: parsed.activatedAt,
tags: parsed.tags,
diff --git a/libs/oms/feature/return-search/src/lib/routes.ts b/libs/oms/feature/return-search/src/lib/routes.ts
index e3851e8c2..6d589bede 100644
--- a/libs/oms/feature/return-search/src/lib/routes.ts
+++ b/libs/oms/feature/return-search/src/lib/routes.ts
@@ -1,31 +1,34 @@
-import { Routes } from '@angular/router';
-import { ReturnSearchMainComponent } from './return-search-main/return-search-main.component';
-import { ReturnSearchComponent } from './return-search.component';
-import { querySettingsResolverFn } from './resolvers/query-settings.resolver-fn';
-import { ReturnSearchResultComponent } from './return-search-result/return-search-result.component';
-
-export const routes: Routes = [
- {
- path: '',
- component: ReturnSearchComponent,
- resolve: { querySettings: querySettingsResolverFn },
- children: [
- { path: '', component: ReturnSearchMainComponent },
- {
- path: 'receipts',
- component: ReturnSearchResultComponent,
- data: { scrollPositionRestoration: true },
- },
- ],
- },
- {
- path: 'receipt',
- loadChildren: () =>
- import('@isa/oms/feature/return-details').then((feat) => feat.routes),
- },
- {
- path: 'process',
- loadChildren: () =>
- import('@isa/oms/feature/return-process').then((feat) => feat.routes),
- },
-];
+import { Routes } from '@angular/router';
+import { ReturnSearchMainComponent } from './return-search-main/return-search-main.component';
+import { ReturnSearchComponent } from './return-search.component';
+import { querySettingsResolverFn } from './resolvers/query-settings.resolver-fn';
+import { ReturnSearchResultComponent } from './return-search-result/return-search-result.component';
+
+export const routes: Routes = [
+ {
+ path: '',
+ component: ReturnSearchComponent,
+ resolve: { querySettings: querySettingsResolverFn },
+ title: 'Retoure',
+ children: [
+ { path: '', component: ReturnSearchMainComponent },
+ {
+ path: 'receipts',
+ component: ReturnSearchResultComponent,
+ data: { scrollPositionRestoration: true },
+ },
+ ],
+ },
+ {
+ path: 'receipt',
+ title: 'Retoure - Beleg',
+ loadChildren: () =>
+ import('@isa/oms/feature/return-details').then((feat) => feat.routes),
+ },
+ {
+ path: 'process',
+ title: 'Retoure - Prozess',
+ loadChildren: () =>
+ import('@isa/oms/feature/return-process').then((feat) => feat.routes),
+ },
+];
diff --git a/libs/remission/feature/remission-list/src/lib/routes.ts b/libs/remission/feature/remission-list/src/lib/routes.ts
index e6d01393e..bee6df9cb 100644
--- a/libs/remission/feature/remission-list/src/lib/routes.ts
+++ b/libs/remission/feature/remission-list/src/lib/routes.ts
@@ -1,31 +1,33 @@
-import { Routes } from '@angular/router';
-import { RemissionListComponent } from './remission-list.component';
-import { querySettingsResolverFn } from './resolvers/query-settings.resolver-fn';
-import { querySettingsDepartmentResolverFn } from './resolvers/query-settings-department.resolver-fn';
-import { RemissionListType } from '@isa/remission/data-access';
-
-export const routes: Routes = [
- {
- path: 'mandatory',
- component: RemissionListComponent,
- resolve: { querySettings: querySettingsResolverFn },
- data: {
- scrollPositionRestoration: true,
- remiType: RemissionListType.Pflicht,
- },
- },
- {
- path: 'department',
- component: RemissionListComponent,
- resolve: { querySettings: querySettingsDepartmentResolverFn },
- data: {
- scrollPositionRestoration: true,
- remiType: RemissionListType.Abteilung,
- },
- },
- {
- path: '',
- redirectTo: 'mandatory',
- pathMatch: 'full',
- },
-];
+import { Routes } from '@angular/router';
+import { RemissionListComponent } from './remission-list.component';
+import { querySettingsResolverFn } from './resolvers/query-settings.resolver-fn';
+import { querySettingsDepartmentResolverFn } from './resolvers/query-settings-department.resolver-fn';
+import { RemissionListType } from '@isa/remission/data-access';
+
+export const routes: Routes = [
+ {
+ path: 'mandatory',
+ component: RemissionListComponent,
+ resolve: { querySettings: querySettingsResolverFn },
+ title: 'Remission - Pflicht',
+ data: {
+ scrollPositionRestoration: true,
+ remiType: RemissionListType.Pflicht,
+ },
+ },
+ {
+ path: 'department',
+ component: RemissionListComponent,
+ resolve: { querySettings: querySettingsDepartmentResolverFn },
+ title: 'Remission - Abteilung',
+ data: {
+ scrollPositionRestoration: true,
+ remiType: RemissionListType.Abteilung,
+ },
+ },
+ {
+ path: '',
+ redirectTo: 'mandatory',
+ pathMatch: 'full',
+ },
+];
diff --git a/libs/remission/feature/remission-return-receipt-list/src/lib/routes.ts b/libs/remission/feature/remission-return-receipt-list/src/lib/routes.ts
index 186832729..747c4e679 100644
--- a/libs/remission/feature/remission-return-receipt-list/src/lib/routes.ts
+++ b/libs/remission/feature/remission-return-receipt-list/src/lib/routes.ts
@@ -4,12 +4,14 @@ export const routes: Routes = [
{
path: '',
component: RemissionReturnReceiptListComponent,
+ title: 'Remission',
data: {
scrollPositionRestoration: true,
},
},
{
path: ':returnId/:receiptId',
+ title: 'Remission - Beleg',
loadComponent: () =>
import('@isa/remission/feature/remission-return-receipt-details').then(
(m) => m.RemissionReturnReceiptDetailsComponent,
diff --git a/package-lock.json b/package-lock.json
index e59fa4e67..5d5b04f15 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13674,9 +13674,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
- "version": "19.2.6",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
- "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"dev": true,
"license": "MIT",
"peer": true,
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 9985c12e0..0f5b3d3db 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -65,6 +65,9 @@
"@isa/common/data-access": ["libs/common/data-access/src/index.ts"],
"@isa/common/decorators": ["libs/common/decorators/src/index.ts"],
"@isa/common/print": ["libs/common/print/src/index.ts"],
+ "@isa/common/title-management": [
+ "libs/common/title-management/src/index.ts"
+ ],
"@isa/core/auth": ["libs/core/auth/src/index.ts"],
"@isa/core/config": ["libs/core/config/src/index.ts"],
"@isa/core/logging": ["libs/core/logging/src/index.ts"],