mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
263 Commits
2.3
...
feature/39
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
967df6316b | ||
|
|
b440ddbe82 | ||
|
|
878bf44d0b | ||
|
|
d6e0d92132 | ||
|
|
da6489eb7a | ||
|
|
819827cc4c | ||
|
|
d09b5b1ce7 | ||
|
|
cc03ef4f9c | ||
|
|
b4dbd8889d | ||
|
|
483faad86a | ||
|
|
0dbc745ed0 | ||
|
|
5c6f416391 | ||
|
|
d97b6afac8 | ||
|
|
771816f3af | ||
|
|
0626538aea | ||
|
|
a1ad4e4a05 | ||
|
|
6df48eb555 | ||
|
|
27ab4526e2 | ||
|
|
9a24b34fbc | ||
|
|
d01e01534b | ||
|
|
5bca1f2a81 | ||
|
|
807b300885 | ||
|
|
b16ffa4352 | ||
|
|
da79d04ef4 | ||
|
|
cf619df576 | ||
|
|
8054c96315 | ||
|
|
2363f424f5 | ||
|
|
6ab1ea2e70 | ||
|
|
c9ce7d6762 | ||
|
|
6ee1b0a7f8 | ||
|
|
d881920312 | ||
|
|
516465db37 | ||
|
|
08e95cec55 | ||
|
|
1d865c47d7 | ||
|
|
6b0beba1d9 | ||
|
|
8aafee672e | ||
|
|
9d886cd33f | ||
|
|
95d1ea3530 | ||
|
|
d3014e7e9a | ||
|
|
da143c3412 | ||
|
|
b10a7a923e | ||
|
|
08601203df | ||
|
|
526ba9f2a0 | ||
|
|
f1fc1d17a5 | ||
|
|
6e07c86eed | ||
|
|
14199391e0 | ||
|
|
bb834f6274 | ||
|
|
8688935f25 | ||
|
|
1b7d257e97 | ||
|
|
fff4b222cb | ||
|
|
c63dee8509 | ||
|
|
607bc320fb | ||
|
|
72393ebca5 | ||
|
|
ecef17846b | ||
|
|
45265eedd4 | ||
|
|
04da34e677 | ||
|
|
1e504d9e0c | ||
|
|
75528d37d3 | ||
|
|
f4579ef8dc | ||
|
|
fc5496fda6 | ||
|
|
5cf6f4da38 | ||
|
|
60fde8b103 | ||
|
|
734fe33f40 | ||
|
|
64da928c36 | ||
|
|
dba0b1b3c7 | ||
|
|
073746a9bc | ||
|
|
45c14e3b79 | ||
|
|
7023fe747b | ||
|
|
141c7fe1d6 | ||
|
|
78e76818b5 | ||
|
|
f78a773fab | ||
|
|
60d007d9eb | ||
|
|
1ec253333e | ||
|
|
bd332b6bd9 | ||
|
|
bc9bdbebe3 | ||
|
|
0b471cc5bc | ||
|
|
c2eed61b83 | ||
|
|
2d403b4c56 | ||
|
|
b3e4ca90ee | ||
|
|
d4a3a4bc06 | ||
|
|
b674b5faf6 | ||
|
|
edb21308d4 | ||
|
|
26ad0153d8 | ||
|
|
e9b2acca5b | ||
|
|
4180ee61d6 | ||
|
|
a442a50708 | ||
|
|
59e650a1f1 | ||
|
|
69b6470cda | ||
|
|
08f00c6578 | ||
|
|
51c4066222 | ||
|
|
c9fbbd78a8 | ||
|
|
ce5388be61 | ||
|
|
02d60e9bd5 | ||
|
|
35b7f5700f | ||
|
|
efec7ecb26 | ||
|
|
ee81f795fe | ||
|
|
ad557ff919 | ||
|
|
bf5fae08b2 | ||
|
|
88321928bf | ||
|
|
a7d50a9439 | ||
|
|
4c6dcd15da | ||
|
|
84f9d14be0 | ||
|
|
e2f173f250 | ||
|
|
fdd9617604 | ||
|
|
bc3cedfe50 | ||
|
|
78f9093931 | ||
|
|
a889c768d1 | ||
|
|
e36319a73e | ||
|
|
199ae95bcd | ||
|
|
30875f0491 | ||
|
|
95d9d17aa7 | ||
|
|
4c641adeda | ||
|
|
4bd4158dab | ||
|
|
4965976f6c | ||
|
|
b43f512887 | ||
|
|
e6f2b46fce | ||
|
|
01c84b361a | ||
|
|
28ad07b372 | ||
|
|
6ade77d458 | ||
|
|
4187e13861 | ||
|
|
729451fa48 | ||
|
|
d77fe8c540 | ||
|
|
a257ddd8e0 | ||
|
|
90154bd497 | ||
|
|
f96224569f | ||
|
|
1c695104f9 | ||
|
|
ab9a35dd89 | ||
|
|
6daa96119d | ||
|
|
128a280dee | ||
|
|
475c885344 | ||
|
|
ee62649bf6 | ||
|
|
109999d66f | ||
|
|
9ec34b07c4 | ||
|
|
2fd2d701dd | ||
|
|
5574252b5b | ||
|
|
5e79d4dc52 | ||
|
|
bb91782079 | ||
|
|
2bbcb15740 | ||
|
|
216d302a86 | ||
|
|
e18b9a4200 | ||
|
|
310395d166 | ||
|
|
651f44914f | ||
|
|
4a97800a05 | ||
|
|
f2e124903c | ||
|
|
cb7334d63b | ||
|
|
f98aac5231 | ||
|
|
9c4e94ce8d | ||
|
|
65a19feffc | ||
|
|
62a1be7abe | ||
|
|
ad4481cfc7 | ||
|
|
e4c20b953d | ||
|
|
e8044fae1b | ||
|
|
c4818319aa | ||
|
|
8cb25d6ca1 | ||
|
|
5a14e0afbd | ||
|
|
0804eeeccb | ||
|
|
f015169011 | ||
|
|
201ea2ee9c | ||
|
|
961211e638 | ||
|
|
22a494e31e | ||
|
|
75e24771b3 | ||
|
|
97b30d5b14 | ||
|
|
ca5dbb9d6f | ||
|
|
e065c1a8da | ||
|
|
fc76f34d38 | ||
|
|
27e5afacde | ||
|
|
8a4fe7aedd | ||
|
|
ba01807add | ||
|
|
3b89777648 | ||
|
|
bb510788eb | ||
|
|
1b85c8ff50 | ||
|
|
4d5e81a638 | ||
|
|
7676ae8143 | ||
|
|
9a17f95026 | ||
|
|
e6b44d8365 | ||
|
|
874f8f4758 | ||
|
|
7b12857a35 | ||
|
|
d6d919ed52 | ||
|
|
600687f652 | ||
|
|
ed144f0a15 | ||
|
|
c0c2cc86d3 | ||
|
|
4b2bfefc9b | ||
|
|
11e79c4830 | ||
|
|
45989d7abd | ||
|
|
ae27da1127 | ||
|
|
ca21931d93 | ||
|
|
5c9f4c5b21 | ||
|
|
c134f645ef | ||
|
|
6f0933a350 | ||
|
|
c9a90211ee | ||
|
|
95d96dd295 | ||
|
|
86bf079f6f | ||
|
|
c202490555 | ||
|
|
da0100dd35 | ||
|
|
b634247463 | ||
|
|
84df6493f6 | ||
|
|
d3858c731c | ||
|
|
f247ac641c | ||
|
|
be1a9e8f7e | ||
|
|
d86f595b1f | ||
|
|
74bf2133c6 | ||
|
|
e4570946c4 | ||
|
|
a8213d79fd | ||
|
|
13ec323ac4 | ||
|
|
c544cebba9 | ||
|
|
74dffe8af2 | ||
|
|
9d3bb9dcf3 | ||
|
|
05e58aa060 | ||
|
|
f74d14d573 | ||
|
|
741e651a20 | ||
|
|
4e1bd89378 | ||
|
|
503f44891f | ||
|
|
5bf326f680 | ||
|
|
e7793b15e3 | ||
|
|
d2e16ca256 | ||
|
|
31164befc9 | ||
|
|
8f4dfa0674 | ||
|
|
eb7a01907a | ||
|
|
fb1fd1ec7c | ||
|
|
7528c7df63 | ||
|
|
e6ca19ab91 | ||
|
|
dc3e097dfd | ||
|
|
a5e8c06dda | ||
|
|
bf7fd13ef2 | ||
|
|
a424e015b4 | ||
|
|
c67fef64fe | ||
|
|
12055de1fc | ||
|
|
a2ad2f8c0b | ||
|
|
af7cebda66 | ||
|
|
2230cf2e7b | ||
|
|
aa048e8d22 | ||
|
|
4961fb9756 | ||
|
|
6801e3858a | ||
|
|
266358f0cc | ||
|
|
6717f0ee3d | ||
|
|
8880ed0df6 | ||
|
|
395fd544e5 | ||
|
|
2c10d6bf10 | ||
|
|
d2c307b08a | ||
|
|
4dc98f7980 | ||
|
|
c89ee18db3 | ||
|
|
c4aa7999a6 | ||
|
|
a8bfedcd5d | ||
|
|
628dbd5227 | ||
|
|
c05b290e49 | ||
|
|
696015b6a4 | ||
|
|
f2f70e1d83 | ||
|
|
b4d967f721 | ||
|
|
bf760677ef | ||
|
|
4d1dbaa2f3 | ||
|
|
595bb27d99 | ||
|
|
7b72532c9e | ||
|
|
6b756fe893 | ||
|
|
54c7f51766 | ||
|
|
94b787655e | ||
|
|
cf1c4d37b9 | ||
|
|
ada16bac6c | ||
|
|
eefb6062c7 | ||
|
|
470a451168 | ||
|
|
e3b018c5f7 | ||
|
|
bd695e21d4 | ||
|
|
aaf156cee3 | ||
|
|
e8020ffde6 |
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@@ -3,6 +3,5 @@
|
||||
"johnpapa.angular2",
|
||||
"esbenp.prettier-vscode",
|
||||
"angular.ng-template",
|
||||
"eg2.vscode-npm-script"
|
||||
]
|
||||
}
|
||||
197
angular.json
197
angular.json
@@ -375,37 +375,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@shell/breadcrumb": {
|
||||
"projectType": "library",
|
||||
"root": "apps/shell/breadcrumb",
|
||||
"sourceRoot": "apps/shell/breadcrumb/src",
|
||||
"prefix": "shell",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"tsConfig": "apps/shell/breadcrumb/tsconfig.lib.json",
|
||||
"project": "apps/shell/breadcrumb/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "apps/shell/breadcrumb/tsconfig.lib.prod.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "apps/shell/breadcrumb/tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@domain/defs": {
|
||||
"projectType": "library",
|
||||
"root": "apps/domain/defs",
|
||||
@@ -840,37 +809,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@shell/header": {
|
||||
"projectType": "library",
|
||||
"root": "apps/shell/header",
|
||||
"sourceRoot": "apps/shell/header/src",
|
||||
"prefix": "shell",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"tsConfig": "apps/shell/header/tsconfig.lib.json",
|
||||
"project": "apps/shell/header/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "apps/shell/header/tsconfig.lib.prod.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "apps/shell/header/tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@modal/reorder": {
|
||||
"projectType": "library",
|
||||
"root": "apps/modal/reorder",
|
||||
@@ -1058,74 +996,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@shell/footer": {
|
||||
"projectType": "library",
|
||||
"root": "apps/shell/footer",
|
||||
"sourceRoot": "apps/shell/footer/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "apps/shell/footer/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "apps/shell/footer/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "apps/shell/footer/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "apps/shell/footer/tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@shell/process": {
|
||||
"projectType": "library",
|
||||
"root": "apps/shell/process",
|
||||
"sourceRoot": "apps/shell/process/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "apps/shell/process/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "apps/shell/process/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "apps/shell/process/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "apps/shell/process/tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@domain/isa": {
|
||||
"projectType": "library",
|
||||
"root": "apps/domain/isa",
|
||||
@@ -1160,40 +1030,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@shell/filter-overlay": {
|
||||
"projectType": "library",
|
||||
"root": "apps/shell/filter-overlay",
|
||||
"sourceRoot": "apps/shell/filter-overlay/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "apps/shell/filter-overlay/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "apps/shell/filter-overlay/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "apps/shell/filter-overlay/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "apps/shell/filter-overlay/tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@store/search-component-store": {
|
||||
"projectType": "library",
|
||||
"root": "apps/store/search-component-store",
|
||||
@@ -1634,6 +1470,39 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"shell": {
|
||||
"projectType": "library",
|
||||
"root": "apps/shell",
|
||||
"sourceRoot": "apps/shell/src",
|
||||
"prefix": "shell",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "apps/shell/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "apps/shell/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "apps/shell/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "apps/shell/tsconfig.spec.json",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
|
||||
@@ -11,9 +11,10 @@ export class DevScanAdapter implements ScanAdapter {
|
||||
constructor(private _modal: UiModalService, private _environmentService: EnvironmentService) {}
|
||||
|
||||
async init(): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(isDevMode());
|
||||
});
|
||||
return Promise.resolve(false);
|
||||
// return new Promise((resolve, reject) => {
|
||||
// resolve(isDevMode());
|
||||
// });
|
||||
}
|
||||
|
||||
scan(): Observable<string> {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { NgModule } from '@angular/core';
|
||||
import { ProductImagePipe } from './product-image.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ProductImagePipe],
|
||||
imports: [],
|
||||
declarations: [],
|
||||
imports: [ProductImagePipe],
|
||||
exports: [ProductImagePipe],
|
||||
})
|
||||
export class ProductImageModule {}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { ProductImageService } from './product-image.service';
|
||||
|
||||
@Pipe({
|
||||
name: 'productImage',
|
||||
standalone: true,
|
||||
pure: true,
|
||||
})
|
||||
export class ProductImagePipe implements PipeTransform {
|
||||
constructor(private imageService: ProductImageService) {}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { BranchDTO } from '@swagger/checkout';
|
||||
import { isBoolean, isNumber } from '@utils/common';
|
||||
@@ -16,19 +15,18 @@ import {
|
||||
selectActivatedProcess,
|
||||
patchProcess,
|
||||
patchProcessData,
|
||||
selectTitle,
|
||||
setTitle,
|
||||
} from './store';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationService {
|
||||
/** @deprecated */
|
||||
private activatedProcessIdSubject = new BehaviorSubject<number>(undefined);
|
||||
|
||||
/** @deprecated */
|
||||
get activatedProcessId() {
|
||||
return this.activatedProcessIdSubject.value;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
get activatedProcessId$() {
|
||||
return this.activatedProcessIdSubject.asObservable();
|
||||
}
|
||||
@@ -48,6 +46,14 @@ export class ApplicationService {
|
||||
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));
|
||||
|
||||
@@ -3,6 +3,8 @@ import { ApplicationProcess } from '..';
|
||||
|
||||
const prefix = '[CORE-APPLICATION]';
|
||||
|
||||
export const setTitle = createAction(`${prefix} Set Title`, props<{ title: string }>());
|
||||
|
||||
export const setSection = createAction(`${prefix} Set Section`, props<{ section: 'customer' | 'branch' }>());
|
||||
|
||||
export const addProcess = createAction(`${prefix} Add Process`, props<{ process: ApplicationProcess }>());
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { Action, createReducer, on } from '@ngrx/store';
|
||||
import { setSection, addProcess, removeProcess, setActivatedProcess, patchProcess, patchProcessData } from './application.actions';
|
||||
import {
|
||||
setSection,
|
||||
addProcess,
|
||||
removeProcess,
|
||||
setActivatedProcess,
|
||||
patchProcess,
|
||||
patchProcessData,
|
||||
setTitle,
|
||||
} from './application.actions';
|
||||
import { ApplicationState, INITIAL_APPLICATION_STATE } from './application.state';
|
||||
|
||||
const _applicationReducer = createReducer(
|
||||
INITIAL_APPLICATION_STATE,
|
||||
on(setTitle, (state, { title }) => ({ ...state, title })),
|
||||
on(setSection, (state, { section }) => ({ ...state, section })),
|
||||
on(addProcess, (state, { process }) => ({ ...state, processes: [...state.processes, { data: {}, ...process }] })),
|
||||
on(removeProcess, (state, { processId }) => {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||
import { ApplicationState } from './application.state';
|
||||
export const selectApplicationState = createFeatureSelector<ApplicationState>('core-application');
|
||||
|
||||
export const selectTitle = createSelector(selectApplicationState, (s) => s.title);
|
||||
|
||||
export const selectSection = createSelector(selectApplicationState, (s) => s.section);
|
||||
|
||||
export const selectProcesses = createSelector(selectApplicationState, (s) => s.processes);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ApplicationProcess } from '../defs';
|
||||
|
||||
export interface ApplicationState {
|
||||
title: string;
|
||||
processes: ApplicationProcess[];
|
||||
section: 'customer' | 'branch';
|
||||
}
|
||||
|
||||
export const INITIAL_APPLICATION_STATE: ApplicationState = {
|
||||
title: '',
|
||||
processes: [],
|
||||
section: 'customer',
|
||||
};
|
||||
|
||||
@@ -135,9 +135,9 @@ export class BreadcrumbService {
|
||||
crumbs.forEach((crumb) => this.removeBreadcrumb(crumb.id));
|
||||
}
|
||||
|
||||
getLatestBreadcrumbForSection(section: 'customer' | 'branch') {
|
||||
getLatestBreadcrumbForSection(section: 'customer' | 'branch', predicate: (crumb: Breadcrumb) => boolean = (_) => true) {
|
||||
return this.store
|
||||
.select(selectors.selectBreadcrumbsBySection, { section })
|
||||
.pipe(map((crumbs) => crumbs.sort((a, b) => b.changed - a.changed).find((f) => true)));
|
||||
.pipe(map((crumbs) => crumbs.sort((a, b) => b.timestamp - a.timestamp).find((f) => predicate(f))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface Breadcrumb {
|
||||
/**
|
||||
* Url
|
||||
*/
|
||||
path: string;
|
||||
path: string | any[];
|
||||
|
||||
/**
|
||||
* Query Parameter
|
||||
|
||||
@@ -1,17 +1,77 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { NativeContainerService } from 'native-container';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
const MATCH_TABLET = '(max-width: 1023px)';
|
||||
|
||||
const MATCH_DESKTOP_SMALL = '(min-width: 1024px) and (max-width: 1439px)';
|
||||
|
||||
const MATCH_DESKTOP = '(min-width: 1280px)';
|
||||
|
||||
const MATCH_DESKTOP_LARGE = '(min-width: 1440px)';
|
||||
|
||||
const MATCH_DESKTOP_XLARGE = '(min-width: 1920px)';
|
||||
|
||||
const MATCH_DESKTOP_XXLARGE = '(min-width: 2736px)';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EnvironmentService {
|
||||
constructor(private _platform: Platform, private _nativeContainer: NativeContainerService) {}
|
||||
constructor(
|
||||
private _platform: Platform,
|
||||
private _nativeContainer: NativeContainerService,
|
||||
private _breakpointObserver: BreakpointObserver
|
||||
) {}
|
||||
|
||||
matchTablet(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_TABLET);
|
||||
}
|
||||
|
||||
matchTablet$ = this._breakpointObserver.observe(MATCH_TABLET).pipe(map((result) => result.matches));
|
||||
|
||||
matchDesktopSmall(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_DESKTOP_SMALL);
|
||||
}
|
||||
|
||||
matchDesktopSmall$ = this._breakpointObserver.observe(MATCH_DESKTOP_SMALL).pipe(map((result) => result.matches));
|
||||
|
||||
matchDesktop(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_DESKTOP);
|
||||
}
|
||||
|
||||
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP).pipe(map((result) => result.matches));
|
||||
|
||||
matchDesktopLarge(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_DESKTOP_LARGE);
|
||||
}
|
||||
|
||||
matchDesktopLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_LARGE).pipe(map((result) => result.matches));
|
||||
|
||||
matchDesktopXLarge(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_DESKTOP_XLARGE);
|
||||
}
|
||||
|
||||
matchDesktopXLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_XLARGE).pipe(map((result) => result.matches));
|
||||
|
||||
matchDesktopXXLarge(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_DESKTOP_XXLARGE);
|
||||
}
|
||||
|
||||
matchDesktopXXLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_XXLARGE).pipe(map((result) => result.matches));
|
||||
|
||||
/**
|
||||
* @deprecated Use `matchDesktopSmall` or 'matchDesktop' instead.
|
||||
*/
|
||||
isDesktop(): boolean {
|
||||
return !this.isTablet();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `matchTablet` instead.
|
||||
*/
|
||||
isTablet(): boolean {
|
||||
return this.isNative() || this.isSafari();
|
||||
}
|
||||
@@ -21,6 +81,6 @@ export class EnvironmentService {
|
||||
}
|
||||
|
||||
isSafari(): boolean {
|
||||
return (this._platform.ANDROID || this._platform.IOS) && this._platform.SAFARI;
|
||||
return this._platform.IOS && this._platform.SAFARI;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { AnimationTriggerMetadata, trigger, state, transition, style, animate } from '@angular/animations';
|
||||
|
||||
export const slideAnimationTime = 150;
|
||||
export const toastAnimations: {
|
||||
readonly slideToast: AnimationTriggerMetadata;
|
||||
} = {
|
||||
slideToast: trigger('slideAnimation', [
|
||||
state('default', style({ transform: 'translateY(0%)' })),
|
||||
transition('void => *', [style({ transform: 'translateY(-100%)' }), animate(`${slideAnimationTime}ms ease-in`)]),
|
||||
transition('default => closing', animate(`${slideAnimationTime}ms ease-in`, style({ transform: 'translateY(-100%)' }))),
|
||||
]),
|
||||
};
|
||||
|
||||
export type ToastAnimationState = 'default' | 'closing';
|
||||
@@ -1,4 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './toast';
|
||||
export * from './toast-ref';
|
||||
// end:ng42.barrel
|
||||
@@ -1,17 +0,0 @@
|
||||
import { OverlayRef } from '@angular/cdk/overlay';
|
||||
|
||||
export class ToastRef {
|
||||
constructor(private readonly _overlay: OverlayRef) {}
|
||||
|
||||
close() {
|
||||
this._overlay.dispose();
|
||||
}
|
||||
|
||||
isVisible() {
|
||||
return this._overlay && this._overlay.overlayElement;
|
||||
}
|
||||
|
||||
getPosition() {
|
||||
return this._overlay.overlayElement.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { TemplateRef } from '@angular/core';
|
||||
|
||||
export interface Toast {
|
||||
title?: string;
|
||||
text?: string;
|
||||
timer?: number;
|
||||
position?: 'top-left' | 'top' | 'top-right' | 'bottom-right' | 'bottom' | 'bottom-left';
|
||||
size?: 'width-full' | 'content';
|
||||
template?: TemplateRef<any>; // For rendering dynamic content
|
||||
templateContext?: {}; // For rendering dynamic content
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './toast.component';
|
||||
export * from './toast.module';
|
||||
export * from './toast.service';
|
||||
export * from './defs';
|
||||
export * from './animation';
|
||||
export * from './tokens';
|
||||
// end:ng42.barrel
|
||||
@@ -1,15 +0,0 @@
|
||||
<div class="toast-main" [style.width]="width" [@slideAnimation]="{ value: animationState }" (@slideAnimation.done)="onSlideFinished()">
|
||||
<button class="absolute top-2 right-2 p-6 border-none bg-transparent" (click)="close()">
|
||||
<ui-icon icon="close" size="20px"></ui-icon>
|
||||
</button>
|
||||
<div class="toast-content flex flex-col justify-center items-center">
|
||||
<h1 class="text-card-sub font-bold text-center py-3 whitespace-pre-wrap">{{ data.title }}</h1>
|
||||
<ng-container *ngIf="data.text; else templateRef">
|
||||
<p class="block text-base overflow-y-hidden pb-3 text-center overflow-x-hidden">{{ data.text }}</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #templateRef>
|
||||
<ng-container *ngTemplateOutlet="data.template; context: data.templateContext"> </ng-container>
|
||||
</ng-template>
|
||||
@@ -1,12 +0,0 @@
|
||||
.toast-main {
|
||||
@apply block relative mx-auto box-border text-white p-4;
|
||||
background-color: var(--toast-background);
|
||||
min-width: 18.75rem;
|
||||
max-width: calc(100vw - 2rem);
|
||||
min-height: 5rem;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
min-height: 3rem;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||
import { toastAnimations, ToastAnimationState, slideAnimationTime } from './animation';
|
||||
import { Toast, ToastRef } from './defs';
|
||||
import { TOAST_CONFIG_TOKEN } from './tokens';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-toast',
|
||||
templateUrl: 'toast.component.html',
|
||||
styleUrls: ['toast.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [toastAnimations.slideToast],
|
||||
})
|
||||
export class ToastComponent implements OnInit, OnDestroy {
|
||||
timeoutRef?: any;
|
||||
animationState: ToastAnimationState = 'default';
|
||||
width = '55.25rem';
|
||||
|
||||
constructor(
|
||||
@Inject(TOAST_CONFIG_TOKEN) public readonly data: Toast,
|
||||
private readonly _ref: ToastRef,
|
||||
private readonly _cdr: ChangeDetectorRef
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.data?.size) {
|
||||
this.width = this.data?.size === 'width-full' ? '100vw' : '55.25rem';
|
||||
}
|
||||
|
||||
this.timeoutRef = setTimeout(() => {
|
||||
this.close();
|
||||
this._cdr.markForCheck();
|
||||
}, slideAnimationTime + (this.data.timer ?? 5000));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
clearTimeout(this.timeoutRef);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.animationState = 'closing';
|
||||
}
|
||||
|
||||
onSlideFinished() {
|
||||
if (this.animationState === 'closing') {
|
||||
this._ref.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { ToastComponent } from './toast.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ToastComponent],
|
||||
imports: [CommonModule, OverlayModule, UiIconModule],
|
||||
exports: [ToastComponent],
|
||||
})
|
||||
export class ToastModule {}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { Overlay } from '@angular/cdk/overlay';
|
||||
import { ComponentPortal } from '@angular/cdk/portal';
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { Toast, ToastRef } from './defs';
|
||||
import { ToastComponent } from './toast.component';
|
||||
import { TOAST_CONFIG_TOKEN } from './tokens';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ToastService {
|
||||
private _lastToastRef: ToastRef;
|
||||
|
||||
get lastToastRef() {
|
||||
return this._lastToastRef;
|
||||
}
|
||||
|
||||
set lastToastRef(toastRef: ToastRef) {
|
||||
this._lastToastRef = toastRef;
|
||||
}
|
||||
|
||||
constructor(private readonly _overlay: Overlay, private readonly _injector: Injector) {}
|
||||
|
||||
create(data: Toast) {
|
||||
const positionStrategy = this.getPositionStrategy(data);
|
||||
const overlayRef = this._overlay.create({ positionStrategy });
|
||||
|
||||
this.lastToastRef = new ToastRef(overlayRef);
|
||||
const injector = this.getInjector(data, this.lastToastRef);
|
||||
|
||||
const toastPortal = new ComponentPortal(ToastComponent, null, injector);
|
||||
overlayRef.attach(toastPortal);
|
||||
|
||||
return this.lastToastRef;
|
||||
}
|
||||
|
||||
getInjector(data: Toast, ref: ToastRef) {
|
||||
return Injector.create({
|
||||
parent: this._injector,
|
||||
providers: [
|
||||
{ provide: TOAST_CONFIG_TOKEN, useValue: data },
|
||||
{ provide: ToastRef, useValue: ref },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
getPositionStrategy(data: Toast) {
|
||||
switch (data?.position) {
|
||||
case 'top':
|
||||
return this._overlay.position().global().top(this.getNextPosition()).centerHorizontally();
|
||||
case 'top-left':
|
||||
return this._overlay.position().global().top(this.getNextPosition()).left('1rem');
|
||||
case 'top-right':
|
||||
return this._overlay.position().global().top(this.getNextPosition()).right('1rem');
|
||||
case 'bottom':
|
||||
return this._overlay.position().global().bottom(this.getNextPosition(true)).centerHorizontally();
|
||||
case 'bottom-left':
|
||||
return this._overlay.position().global().bottom(this.getNextPosition(true)).left('1rem');
|
||||
case 'bottom-right':
|
||||
return this._overlay.position().global().bottom(this.getNextPosition(true)).right('1rem');
|
||||
default:
|
||||
return this._overlay.position().global().top(this.getNextPosition()).centerHorizontally();
|
||||
}
|
||||
}
|
||||
|
||||
getNextPosition(fromBottom?: boolean) {
|
||||
const lastToastIsVisible = this.lastToastRef && this.lastToastRef.isVisible();
|
||||
|
||||
let position = fromBottom ? 6 : 9;
|
||||
|
||||
if (lastToastIsVisible && fromBottom) {
|
||||
position = (window.innerHeight - this.lastToastRef.getPosition().bottom + this.lastToastRef.getPosition().height + 16) / 16;
|
||||
} else if (lastToastIsVisible) {
|
||||
position = (this.lastToastRef.getPosition().bottom + 16) / 16;
|
||||
}
|
||||
|
||||
return position + 'rem';
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { Toast } from './defs';
|
||||
|
||||
export const TOAST_CONFIG_TOKEN = new InjectionToken<Toast>('TOAST_DATA');
|
||||
@@ -1,5 +0,0 @@
|
||||
/*
|
||||
* Public API Surface of toast
|
||||
*/
|
||||
|
||||
export * from './lib';
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
StoreCheckoutSupplierService,
|
||||
SupplierDTO,
|
||||
} from '@swagger/checkout';
|
||||
import { combineLatest, Observable, of } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
|
||||
import {
|
||||
AvailabilityRequestDTO,
|
||||
AvailabilityService,
|
||||
@@ -21,11 +21,16 @@ import { isArray, memorize } from '@utils/common';
|
||||
import { LogisticianDTO, LogisticianService } from '@swagger/oms';
|
||||
import { ResponseArgsOfIEnumerableOfStockInfoDTO, StockDTO, StockInfoDTO, StockService } from '@swagger/remi';
|
||||
import { PriceDTO } from '@swagger/availability';
|
||||
import { AvailabilityByBranchDTO, ItemData } from './defs';
|
||||
import { AvailabilityByBranchDTO, ItemData, Ssc } from './defs';
|
||||
import { Availability } from './defs/availability';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class DomainAvailabilityService {
|
||||
// Ticket #3378 Keep Result List Items and Details Page SSC in sync
|
||||
sscs$ = new BehaviorSubject<Array<Ssc>>([]);
|
||||
sscsObs$ = this.sscs$.asObservable();
|
||||
|
||||
constructor(
|
||||
private _availabilityService: AvailabilityService,
|
||||
private _logisticanService: LogisticianService,
|
||||
@@ -155,7 +160,6 @@ export class DomainAvailabilityService {
|
||||
quantity: number;
|
||||
branch?: BranchDTO;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
console.log('getTakeAwayAvailability', item, quantity, branch);
|
||||
const request = !!branch ? this.getStockByBranch(branch.id) : this.getDefaultStock();
|
||||
return request.pipe(
|
||||
switchMap((s) =>
|
||||
@@ -480,6 +484,10 @@ export class DomainAvailabilityService {
|
||||
};
|
||||
}
|
||||
|
||||
private _priceIsEmpty(price: PriceDTO) {
|
||||
return isEmpty(price?.value) || isEmpty(price?.vat);
|
||||
}
|
||||
|
||||
private _mapToTakeAwayAvailability({
|
||||
response,
|
||||
supplier,
|
||||
@@ -500,7 +508,7 @@ export class DomainAvailabilityService {
|
||||
inStock: inStock,
|
||||
supplierSSC: quantity <= inStock ? '999' : '',
|
||||
supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '',
|
||||
price: price ?? stockInfo?.retailPrice,
|
||||
price: this._priceIsEmpty(price) ? stockInfo?.retailPrice : price,
|
||||
supplier: { id: supplier?.id },
|
||||
// TODO: Change after API Update
|
||||
// LH: 2021-03-09 preis Property hat nun ein Fallback auf retailPrice
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './availability-by-branch-dto';
|
||||
export * from './availability';
|
||||
export * from './item-data';
|
||||
export * from './ssc';
|
||||
|
||||
5
apps/domain/availability/src/lib/defs/ssc.ts
Normal file
5
apps/domain/availability/src/lib/defs/ssc.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Ssc {
|
||||
itemId?: number;
|
||||
ssc?: string;
|
||||
sscText?: string;
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
StoreCheckoutPayerService,
|
||||
StoreCheckoutBranchService,
|
||||
ItemsResult,
|
||||
ShoppingCartItemDTO,
|
||||
} from '@swagger/checkout';
|
||||
import {
|
||||
DisplayOrderDTO,
|
||||
@@ -36,20 +37,45 @@ import {
|
||||
ResponseArgsOfValueTupleOfIEnumerableOfDisplayOrderDTOAndIEnumerableOfKeyValueDTOOfStringAndString,
|
||||
} from '@swagger/oms';
|
||||
import { isNullOrUndefined, memorize } from '@utils/common';
|
||||
import { combineLatest, Observable, of, concat, isObservable, throwError } from 'rxjs';
|
||||
import { bufferCount, catchError, filter, first, map, mergeMap, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { combineLatest, Observable, of, concat, isObservable, throwError, interval, zip, EMPTY, Subscription } from 'rxjs';
|
||||
import {
|
||||
bufferCount,
|
||||
catchError,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
first,
|
||||
map,
|
||||
mergeMap,
|
||||
share,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import * as DomainCheckoutSelectors from './store/domain-checkout.selectors';
|
||||
import * as DomainCheckoutActions from './store/domain-checkout.actions';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { DomainAvailabilityService, ItemData } from '@domain/availability';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { CustomerDTO, EntityDTOContainerOfAttributeDTO } from '@swagger/crm';
|
||||
import { CustomerDTO } from '@swagger/crm';
|
||||
import { Config } from '@core/config';
|
||||
import parseDuration from 'parse-duration';
|
||||
import { CheckoutEntity } from './store/defs/checkout.entity';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class DomainCheckoutService {
|
||||
get olaExpiration() {
|
||||
const exp = this._config.get('@domain/checkout.olaExpiration') ?? '5m';
|
||||
return parseDuration(exp);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private store: Store<any>,
|
||||
private _config: Config,
|
||||
private applicationService: ApplicationService,
|
||||
private storeCheckoutService: StoreCheckoutService,
|
||||
private orderCheckoutService: OrderCheckoutService,
|
||||
@@ -119,14 +145,14 @@ export class DomainCheckoutService {
|
||||
})
|
||||
.pipe(
|
||||
map((response) => response.result),
|
||||
tap((shoppingCart) =>
|
||||
tap((shoppingCart) => {
|
||||
this.store.dispatch(
|
||||
DomainCheckoutActions.setShoppingCart({
|
||||
processId,
|
||||
shoppingCart,
|
||||
})
|
||||
)
|
||||
),
|
||||
);
|
||||
}),
|
||||
tap((shoppingCart) => this.updateProcessCount(processId, shoppingCart?.items?.length))
|
||||
)
|
||||
)
|
||||
@@ -249,11 +275,24 @@ export class DomainCheckoutService {
|
||||
shoppingCartItemId: number;
|
||||
availability: AvailabilityDTO;
|
||||
}) {
|
||||
return this._shoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability({
|
||||
shoppingCartId,
|
||||
shoppingCartItemId,
|
||||
availability,
|
||||
});
|
||||
return this._shoppingCartService
|
||||
.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability({
|
||||
shoppingCartId,
|
||||
shoppingCartItemId,
|
||||
availability,
|
||||
})
|
||||
.pipe(
|
||||
map((response) => response.result),
|
||||
tap((shoppingCart) => {
|
||||
this.store.dispatch(
|
||||
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId({
|
||||
shoppingCartId,
|
||||
availability,
|
||||
shoppingCartItemId,
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
updateItemInShoppingCart({
|
||||
@@ -265,7 +304,7 @@ export class DomainCheckoutService {
|
||||
shoppingCartItemId: number;
|
||||
update: UpdateShoppingCartItemDTO;
|
||||
}): Observable<ShoppingCartDTO> {
|
||||
return this.getShoppingCart({ processId }).pipe(
|
||||
return this.getShoppingCart({ processId, latest: true }).pipe(
|
||||
first(),
|
||||
mergeMap((shoppingCart) =>
|
||||
this._shoppingCartService
|
||||
@@ -276,8 +315,21 @@ export class DomainCheckoutService {
|
||||
})
|
||||
.pipe(
|
||||
map((response) => response.result),
|
||||
tap((shoppingCart) => this.store.dispatch(DomainCheckoutActions.setShoppingCart({ processId, shoppingCart }))),
|
||||
tap((shoppingCart) => this.updateProcessCount(processId, shoppingCart?.items?.length))
|
||||
tap((shoppingCart) => {
|
||||
this.store.dispatch(DomainCheckoutActions.setShoppingCart({ processId, shoppingCart }));
|
||||
|
||||
if (update.availability) {
|
||||
this.store.dispatch(
|
||||
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory({
|
||||
processId,
|
||||
availability: update.availability,
|
||||
shoppingCartItemId,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.updateProcessCount(processId, shoppingCart?.items?.length);
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -547,6 +599,172 @@ export class DomainCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
async refreshAvailability({
|
||||
processId,
|
||||
shoppingCartItemId,
|
||||
}: {
|
||||
processId: number;
|
||||
shoppingCartItemId: number;
|
||||
}): Promise<AvailabilityDTO> {
|
||||
const shoppingCart = await this.getShoppingCart({ processId }).pipe(first()).toPromise();
|
||||
const item = shoppingCart?.items.find((item) => item.id === shoppingCartItemId)?.data;
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemData: ItemData = {
|
||||
ean: item.product.ean,
|
||||
itemId: Number(item.product.catalogProductNumber),
|
||||
price: item.availability.price,
|
||||
};
|
||||
|
||||
let availability: AvailabilityDTO;
|
||||
|
||||
switch (item.features.orderType) {
|
||||
case 'Abholung':
|
||||
const abholung = await this.availabilityService
|
||||
.getPickUpAvailability({
|
||||
item: itemData,
|
||||
branch: item.destination?.data?.targetBranch?.data,
|
||||
quantity: item.quantity,
|
||||
})
|
||||
.toPromise();
|
||||
availability = abholung[0];
|
||||
break;
|
||||
case 'Rücklage':
|
||||
const ruecklage = await this.availabilityService
|
||||
.getTakeAwayAvailability({
|
||||
item: itemData,
|
||||
quantity: item.quantity,
|
||||
branch: item.destination?.data?.targetBranch?.data,
|
||||
})
|
||||
.toPromise();
|
||||
availability = ruecklage;
|
||||
break;
|
||||
case 'Download':
|
||||
const download = await this.availabilityService
|
||||
.getDownloadAvailability({
|
||||
item: itemData,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
availability = download;
|
||||
break;
|
||||
|
||||
case 'Versand':
|
||||
const versand = await this.availabilityService
|
||||
.getDeliveryAvailability({
|
||||
item: itemData,
|
||||
quantity: item.quantity,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
availability = versand;
|
||||
break;
|
||||
|
||||
case 'DIG-Versand':
|
||||
const digVersand = await this.availabilityService
|
||||
.getDigDeliveryAvailability({
|
||||
item: itemData,
|
||||
quantity: item.quantity,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
availability = digVersand;
|
||||
break;
|
||||
|
||||
case 'B2B-Versand':
|
||||
const b2bVersand = await this.availabilityService
|
||||
.getB2bDeliveryAvailability({
|
||||
item: itemData,
|
||||
quantity: item.quantity,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
availability = b2bVersand;
|
||||
break;
|
||||
}
|
||||
|
||||
await this.updateItemInShoppingCart({
|
||||
processId,
|
||||
update: { availability },
|
||||
shoppingCartItemId: item.id,
|
||||
}).toPromise();
|
||||
|
||||
return availability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the availability of all items is valid
|
||||
* @param param0 Process Id
|
||||
* @returns true if the availability of all items is valid
|
||||
*/
|
||||
validateOlaStatus({ processId, interval }: { processId: number; interval?: number }): Observable<boolean> {
|
||||
return new Observable((observer) => {
|
||||
const enity$ = this.store.select(DomainCheckoutSelectors.selectCheckoutEntityByProcessId, { processId });
|
||||
|
||||
const olaExpiration = this.olaExpiration;
|
||||
|
||||
let timeout: any;
|
||||
|
||||
let subscription: Subscription;
|
||||
|
||||
function check() {
|
||||
const now = Date.now();
|
||||
|
||||
subscription?.unsubscribe();
|
||||
subscription = enity$.pipe(take(1)).subscribe((entity) => {
|
||||
if (!entity || !entity.shoppingCart || !entity.shoppingCart.items) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp ?? {};
|
||||
const shoppingCart = entity.shoppingCart;
|
||||
|
||||
const timestamps = shoppingCart.items
|
||||
?.map((i) => i.data)
|
||||
?.filter((item) => !!item?.features?.orderType)
|
||||
?.map((item) => itemAvailabilityTimestamp[`${item.id}_${item.features.orderType}`]);
|
||||
|
||||
if (timestamps?.length > 0) {
|
||||
const oldestTimestamp = Math.min(...timestamps);
|
||||
observer.next(now - oldestTimestamp < olaExpiration);
|
||||
}
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
check.call(this);
|
||||
}, interval ?? olaExpiration / 10);
|
||||
});
|
||||
}
|
||||
|
||||
check.call(this);
|
||||
|
||||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
validateAvailabilities({ processId }: { processId: number }): Observable<boolean> {
|
||||
return this.getShoppingCart({ processId }).pipe(
|
||||
map((shoppingCart) => {
|
||||
const items = shoppingCart?.items?.map((item) => item.data) || [];
|
||||
|
||||
return items.every((i) => this.availabilityService.isAvailable({ availability: i.availability }));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
checkoutIsValid({ processId }: { processId: number }): Observable<boolean> {
|
||||
const olaStatus$ = this.validateOlaStatus({ processId, interval: 250 });
|
||||
|
||||
const availabilities$ = this.validateAvailabilities({ processId });
|
||||
|
||||
return combineLatest([olaStatus$, availabilities$]).pipe(map(([olaStatus, availabilities]) => olaStatus && availabilities));
|
||||
}
|
||||
|
||||
completeCheckout({ processId }: { processId: number }): Observable<DisplayOrderDTO[]> {
|
||||
const refreshShoppingCart$ = this.getShoppingCart({ processId, latest: true }).pipe(first());
|
||||
const refreshCheckout$ = this.getCheckout({ processId, refresh: true }).pipe(first());
|
||||
@@ -700,21 +918,23 @@ export class DomainCheckoutService {
|
||||
)
|
||||
);
|
||||
|
||||
return updateDestination$
|
||||
.pipe(tap(console.log.bind(window, 'updateDestination$')))
|
||||
return of(undefined)
|
||||
.pipe(
|
||||
mergeMap((_) => updateDestination$.pipe(tap(console.log.bind(window, 'updateDestination$')))),
|
||||
mergeMap((_) => refreshShoppingCart$.pipe(tap(console.log.bind(window, 'refreshShoppingCart$')))),
|
||||
mergeMap((_) => setSpecialComment$.pipe(tap(console.log.bind(window, 'setSpecialComment$')))),
|
||||
mergeMap((_) => refreshCheckout$.pipe(tap(console.log.bind(window, 'refreshCheckout$')))),
|
||||
mergeMap((_) => checkAvailabilities$.pipe(tap(console.log.bind(window, 'checkAvailabilities$')))),
|
||||
mergeMap((_) => updateAvailabilities$.pipe(tap(console.log.bind(window, 'updateAvailabilities$')))),
|
||||
mergeMap((_) => updateAvailabilities$.pipe(tap(console.log.bind(window, 'updateAvailabilities$'))))
|
||||
)
|
||||
.pipe(
|
||||
mergeMap((_) => setBuyer$.pipe(tap(console.log.bind(window, 'setBuyer$')))),
|
||||
mergeMap((_) => setNotificationChannels$.pipe(tap(console.log.bind(window, 'setNotificationChannels$')))),
|
||||
mergeMap((_) => setPayer$.pipe(tap(console.log.bind(window, 'setPayer$')))),
|
||||
mergeMap((_) => setPaymentType$.pipe(tap(console.log.bind(window, 'setPaymentType$')))),
|
||||
mergeMap((_) => setDestination$.pipe(tap(console.log.bind(window, 'setDestination$'))))
|
||||
)
|
||||
.pipe(mergeMap((_) => completeOrder$.pipe(tap(console.log.bind(window, 'completeOrder$')))));
|
||||
mergeMap((_) => setDestination$.pipe(tap(console.log.bind(window, 'setDestination$')))),
|
||||
mergeMap((_) => completeOrder$.pipe(tap(console.log.bind(window, 'completeOrder$'))))
|
||||
);
|
||||
}
|
||||
|
||||
completeKulturpassOrder({
|
||||
@@ -978,6 +1198,5 @@ export class DomainCheckoutService {
|
||||
private updateProcessCount(processId: number, count: number) {
|
||||
this.applicationService.patchProcessData(processId, { count });
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { BuyerDTO, CheckoutDTO, NotificationChannel, PayerDTO, ShippingAddressDTO, ShoppingCartDTO } from '@swagger/checkout';
|
||||
import {
|
||||
AvailabilityDTO,
|
||||
BuyerDTO,
|
||||
CheckoutDTO,
|
||||
NotificationChannel,
|
||||
PayerDTO,
|
||||
ShippingAddressDTO,
|
||||
ShoppingCartDTO,
|
||||
} from '@swagger/checkout';
|
||||
import { CustomerDTO } from '@swagger/crm';
|
||||
import { DisplayOrderDTO } from '@swagger/oms';
|
||||
|
||||
@@ -14,4 +22,5 @@ export interface CheckoutEntity {
|
||||
specialComment: string;
|
||||
notificationChannels: NotificationChannel;
|
||||
olaErrorIds: number[];
|
||||
itemAvailabilityTimestamp: Record<string, number>;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ShippingAddressDTO,
|
||||
BuyerDTO,
|
||||
PayerDTO,
|
||||
AvailabilityDTO,
|
||||
} from '@swagger/checkout';
|
||||
import { CustomerDTO } from '@swagger/crm';
|
||||
import { DisplayOrderDTO, DisplayOrderItemDTO } from '@swagger/oms';
|
||||
@@ -61,3 +62,13 @@ export const setSpecialComment = createAction(`${prefix} Set Agent Comment`, pro
|
||||
export const setOlaError = createAction(`${prefix} Set Ola Error`, props<{ processId: number; olaErrorIds: number[] }>());
|
||||
|
||||
export const setCustomer = createAction(`${prefix} Set Customer`, props<{ processId: number; customer: CustomerDTO }>());
|
||||
|
||||
export const addShoppingCartItemAvailabilityToHistory = createAction(
|
||||
`${prefix} Add Shopping Cart Item Availability To History`,
|
||||
props<{ processId: number; shoppingCartItemId: number; availability: AvailabilityDTO }>()
|
||||
);
|
||||
|
||||
export const addShoppingCartItemAvailabilityToHistoryByShoppingCartId = createAction(
|
||||
`${prefix} Add Shopping Cart Item Availability To History By Shopping Cart Id`,
|
||||
props<{ shoppingCartId: number; shoppingCartItemId: number; availability: AvailabilityDTO }>()
|
||||
);
|
||||
|
||||
@@ -10,7 +10,22 @@ const _domainCheckoutReducer = createReducer(
|
||||
initialCheckoutState,
|
||||
on(DomainCheckoutActions.setShoppingCart, (s, { processId, shoppingCart }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
|
||||
const addedShoppingCartItems =
|
||||
shoppingCart?.items?.filter((item) => !entity.shoppingCart?.items?.find((i) => i.id === item.id))?.map((item) => item.data) ?? [];
|
||||
|
||||
entity.shoppingCart = shoppingCart;
|
||||
|
||||
entity.itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp ? { ...entity.itemAvailabilityTimestamp } : {};
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
for (let shoppingCartItem of addedShoppingCartItems) {
|
||||
if (shoppingCartItem.features?.orderType) {
|
||||
entity.itemAvailabilityTimestamp[`${shoppingCartItem.id}_${shoppingCartItem.features.orderType}`] = now;
|
||||
}
|
||||
}
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setCheckout, (s, { processId, checkout }) => {
|
||||
@@ -100,7 +115,40 @@ const _domainCheckoutReducer = createReducer(
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.customer = customer;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
})
|
||||
}),
|
||||
on(DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory, (s, { processId, shoppingCartItemId, availability }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
|
||||
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp ? { ...entity?.itemAvailabilityTimestamp } : {};
|
||||
|
||||
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId)?.data;
|
||||
|
||||
if (!item?.features?.orderType) return s;
|
||||
|
||||
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] = Date.now();
|
||||
|
||||
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(
|
||||
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId,
|
||||
(s, { shoppingCartId, shoppingCartItemId, availability }) => {
|
||||
const entity = getCheckoutEntityByShoppingCartId({ shoppingCartId, entities: s.entities });
|
||||
|
||||
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp ? { ...entity?.itemAvailabilityTimestamp } : {};
|
||||
|
||||
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId)?.data;
|
||||
|
||||
if (!item?.features?.orderType) return s;
|
||||
|
||||
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] = Date.now();
|
||||
|
||||
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export function domainCheckoutReducer(state, action) {
|
||||
@@ -123,8 +171,20 @@ function getOrCreateCheckoutEntity({ entities, processId }: { entities: Dictiona
|
||||
notificationChannels: 0,
|
||||
olaErrorIds: [],
|
||||
customer: undefined,
|
||||
// availabilityHistory: [],
|
||||
itemAvailabilityTimestamp: {},
|
||||
};
|
||||
}
|
||||
|
||||
return { ...entity };
|
||||
}
|
||||
|
||||
function getCheckoutEntityByShoppingCartId({
|
||||
entities,
|
||||
shoppingCartId,
|
||||
}: {
|
||||
entities: Dictionary<CheckoutEntity>;
|
||||
shoppingCartId: number;
|
||||
}): CheckoutEntity {
|
||||
return Object.values(entities).find((entity) => entity.shoppingCart?.id === shoppingCartId);
|
||||
}
|
||||
|
||||
@@ -18,14 +18,15 @@ import {
|
||||
NotificationChannel,
|
||||
PayerDTO,
|
||||
PayerService,
|
||||
QueryTokenDTO,
|
||||
ResponseArgsOfIEnumerableOfBonusCardInfoDTO,
|
||||
ShippingAddressDTO,
|
||||
ShippingAddressService,
|
||||
} from '@swagger/crm';
|
||||
import { isArray } from '@utils/common';
|
||||
import { isArray, memorize } from '@utils/common';
|
||||
import { PagedResult, Result } from 'apps/domain/defs/src/public-api';
|
||||
import { Observable, of, ReplaySubject } from 'rxjs';
|
||||
import { catchError, map, mergeMap, retry } from 'rxjs/operators';
|
||||
import { catchError, map, mergeMap, retry, shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CrmCustomerService {
|
||||
@@ -38,6 +39,14 @@ export class CrmCustomerService {
|
||||
private loyaltyCardService: LoyaltyCardService
|
||||
) {}
|
||||
|
||||
@memorize()
|
||||
filterSettings() {
|
||||
return this.customerService.CustomerCustomerQuerySettings().pipe(
|
||||
map((res) => res.result),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
complete(queryString: string, filter?: { [key: string]: string }): Observable<Result<AutocompleteDTO[]>> {
|
||||
return this.customerService.CustomerCustomerAutocomplete({
|
||||
input: queryString,
|
||||
@@ -66,6 +75,15 @@ export class CrmCustomerService {
|
||||
});
|
||||
}
|
||||
|
||||
getCustomersWithQueryToken(queryToken: QueryTokenDTO) {
|
||||
if (queryToken.skip === undefined) queryToken.skip = 0;
|
||||
if (queryToken.take === undefined) queryToken.take = 20;
|
||||
if (queryToken.input === undefined) queryToken.input = { qs: '' };
|
||||
if (queryToken.filter === undefined) queryToken.filter = {};
|
||||
|
||||
return this.customerService.CustomerListCustomers(queryToken);
|
||||
}
|
||||
|
||||
getCustomersByCustomerCardNumber(queryString: string): Observable<PagedResult<CustomerInfoDTO>> {
|
||||
return this.customerService.CustomerGetCustomerByBonuscard(!!queryString ? queryString : undefined);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { UiModalService } from '@ui/modal';
|
||||
import { ReorderModalComponent, ReorderResult } from '@modal/reorder';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { AvailabilityDTO2, OrderItemListItemDTO } from '@swagger/oms';
|
||||
import { ToastService } from '@core/toast';
|
||||
import { ToasterService } from '@shared/shell';
|
||||
|
||||
@Injectable()
|
||||
export class ReOrderActionHandler extends ActionHandler<OrderItemsContext> {
|
||||
@@ -13,7 +13,7 @@ export class ReOrderActionHandler extends ActionHandler<OrderItemsContext> {
|
||||
private _command: CommandService,
|
||||
private _domainCheckoutService: DomainCheckoutService,
|
||||
private _uiModal: UiModalService,
|
||||
private _toastService: ToastService
|
||||
private _toastService: ToasterService
|
||||
) {
|
||||
super('REORDER');
|
||||
}
|
||||
@@ -71,8 +71,8 @@ export class ReOrderActionHandler extends ActionHandler<OrderItemsContext> {
|
||||
case 'Falscher Titel geliefert (richtiges Etikett)':
|
||||
break;
|
||||
default:
|
||||
this._toastService.create({
|
||||
title: 'Artikel wurde nachbestellt',
|
||||
this._toastService.open({
|
||||
message: 'Artikel wurde nachbestellt',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AutocompleteTokenDTO, OrderService, QueryTokenDTO } from '@swagger/oms';
|
||||
import { memorize } from '@utils/common';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainCustomerOrderService {
|
||||
@@ -14,16 +16,18 @@ export class DomainCustomerOrderService {
|
||||
// branch_id'
|
||||
}
|
||||
|
||||
getOrderItemsByOrderNumber(orderNumber: string) {
|
||||
getOrderItemsByOrderNumber(params: { compartmentCode?: string; orderId: number }) {
|
||||
return this._orderService.OrderKundenbestellungen({
|
||||
filter: { all_branches: 'true', archive: 'true' },
|
||||
input: {
|
||||
qs: orderNumber,
|
||||
},
|
||||
input: { order_id: String(params.orderId), compartment_code: params.compartmentCode },
|
||||
});
|
||||
}
|
||||
|
||||
@memorize()
|
||||
settings() {
|
||||
return this._orderService.OrderKundenbestellungenSettings();
|
||||
return this._orderService.OrderKundenbestellungenSettings().pipe(
|
||||
map((res) => res?.result),
|
||||
shareReplay()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// start:ng42.barrel
|
||||
export * from './hub-notification.module';
|
||||
export * from './notifications.hub';
|
||||
export * from './defs';
|
||||
// end:ng42.barrel
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { isDevMode, NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { DebugComponent } from './debug/debug.component';
|
||||
import {
|
||||
CanActivateCartGuard,
|
||||
CanActivateCartWithProcessIdGuard,
|
||||
CanActivateCustomerGuard,
|
||||
CanActivateCustomerOrdersGuard,
|
||||
CanActivateCustomerOrdersWithProcessIdGuard,
|
||||
CanActivateCustomerWithProcessIdGuard,
|
||||
CanActivateGoodsInGuard,
|
||||
CanActivateGoodsOutGuard,
|
||||
@@ -17,9 +18,9 @@ import {
|
||||
} 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 { ShellComponent, ShellModule } from './shell';
|
||||
import { TokenLoginComponent, TokenLoginModule } from './token-login';
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -40,7 +41,7 @@ const routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: 'kunde',
|
||||
component: ShellComponent,
|
||||
component: MainComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
@@ -60,22 +61,22 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'order',
|
||||
loadChildren: () => import('@page/customer-order').then((m) => m.CustomerOrderModule),
|
||||
canActivate: [CanActivateGoodsOutGuard],
|
||||
canActivate: [CanActivateCustomerOrdersGuard],
|
||||
},
|
||||
{
|
||||
path: ':processId/order',
|
||||
loadChildren: () => import('@page/customer-order').then((m) => m.CustomerOrderModule),
|
||||
canActivate: [CanActivateGoodsOutWithProcessIdGuard],
|
||||
canActivate: [CanActivateCustomerOrdersWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
},
|
||||
{
|
||||
path: 'customer',
|
||||
loadChildren: () => import('@page/customer').then((m) => m.PageCustomerModule),
|
||||
loadChildren: () => import('@page/customer-rd').then((m) => m.CustomerModule),
|
||||
canActivate: [CanActivateCustomerGuard],
|
||||
},
|
||||
{
|
||||
path: ':processId/customer',
|
||||
loadChildren: () => import('@page/customer').then((m) => m.PageCustomerModule),
|
||||
loadChildren: () => import('@page/customer-rd').then((m) => m.CustomerModule),
|
||||
canActivate: [CanActivateCustomerWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
},
|
||||
@@ -106,7 +107,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'filiale',
|
||||
component: ShellComponent,
|
||||
component: MainComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'task-calendar',
|
||||
@@ -152,7 +153,7 @@ if (isDevMode()) {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes), ShellModule, TokenLoginModule],
|
||||
imports: [RouterModule.forRoot(routes), TokenLoginModule],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
:host {
|
||||
@apply block box-border;
|
||||
}
|
||||
|
||||
button {
|
||||
@apply fixed bottom-4 right-2 bg-blue-500 text-white font-bold py-2 px-4 rounded z-tooltip;
|
||||
@apply block;
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ export class AppComponent implements OnInit {
|
||||
checkForUpdate() {
|
||||
interval(this._checkForUpdates).subscribe(() => {
|
||||
this._swUpdate.checkForUpdate().then((value) => {
|
||||
console.log('check for update', value);
|
||||
if (value) {
|
||||
this._notifications.updateNotification();
|
||||
}
|
||||
@@ -125,6 +126,7 @@ export class AppComponent implements OnInit {
|
||||
|
||||
initialCheckForUpdate() {
|
||||
this._swUpdate.checkForUpdate().then((value) => {
|
||||
console.log('initial check for update', value);
|
||||
if (value) {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
@@ -32,9 +32,11 @@ import { IsaErrorHandler } from './providers/isa.error-handler';
|
||||
import { ScanAdapterModule, ScanAdapterService, ScanditScanAdapterModule } from '@adapter/scan';
|
||||
import { RootStateService } from './store/root-state.service';
|
||||
import * as Commands from './commands';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { PreviewComponent } from './preview';
|
||||
import { NativeContainerService } from 'native-container';
|
||||
import { ShellModule } from '@shared/shell';
|
||||
import { MainComponent } from './main.component';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
|
||||
registerLocaleData(localeDe, localeDeExtra);
|
||||
registerLocaleData(localeDe, 'de', localeDeExtra);
|
||||
@@ -74,11 +76,12 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
declarations: [AppComponent, MainComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
HttpClientModule,
|
||||
ShellModule.forRoot(),
|
||||
AppRoutingModule,
|
||||
AppSwaggerModule,
|
||||
AppDomainModule,
|
||||
@@ -103,31 +106,7 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
|
||||
ScanAdapterModule.forRoot(),
|
||||
ScanditScanAdapterModule.forRoot(),
|
||||
PlatformModule,
|
||||
UiIconModule.forRoot({
|
||||
aliases: [
|
||||
{ alias: 'd-account', name: 'account' },
|
||||
{ alias: 'd-no-account', name: 'package-variant-closed' },
|
||||
{ name: 'isa-audio', alias: 'AU' },
|
||||
{ name: 'isa-audio', alias: 'CAS' },
|
||||
{ name: 'isa-audio', alias: 'DL' },
|
||||
{ name: 'isa-audio', alias: 'KAS' },
|
||||
{ name: 'isa-hard-cover', alias: 'BUCH' },
|
||||
{ name: 'isa-hard-cover', alias: 'GEB' },
|
||||
{ name: 'isa-hard-cover', alias: 'HC' },
|
||||
{ name: 'isa-hard-cover', alias: 'KT' },
|
||||
{ name: 'isa-ebook', alias: 'EB' },
|
||||
{ name: 'isa-non-book', alias: 'GLO' },
|
||||
{ name: 'isa-non-book', alias: 'HDL' },
|
||||
{ name: 'isa-non-book', alias: 'NB' },
|
||||
{ name: 'isa-non-book', alias: 'SPL' },
|
||||
{ name: 'isa-calendar', alias: 'KA' },
|
||||
{ name: 'isa-scroll', alias: 'MA' },
|
||||
{ name: 'isa-software', alias: 'SW' },
|
||||
{ name: 'isa-soft-cover', alias: 'TB' },
|
||||
{ name: 'isa-video', alias: 'VI' },
|
||||
{ name: 'isa-news-paper', alias: 'ZS' },
|
||||
],
|
||||
}),
|
||||
IconModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { CheckoutNavigationService } from '@shared/services';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanActivateCartGuard implements CanActivate {
|
||||
constructor(private readonly _applicationService: ApplicationService, private readonly _router: Router) {}
|
||||
constructor(private readonly _applicationService: ApplicationService, private _checkoutNavigationService: CheckoutNavigationService) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
|
||||
@@ -21,7 +22,7 @@ export class CanActivateCartGuard implements CanActivate {
|
||||
name: `Vorgang ${processes.length + 1}`,
|
||||
});
|
||||
}
|
||||
await this._router.navigate(['/kunde', lastActivatedProcessId, 'cart']);
|
||||
await this._checkoutNavigationService.navigateToCheckoutReview({ processId: lastActivatedProcessId });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanActivateCustomerOrdersWithProcessIdGuard implements CanActivate {
|
||||
constructor(private readonly _applicationService: ApplicationService, private readonly _breadcrumbService: BreadcrumbService) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const process = await this._applicationService
|
||||
.getProcessById$(+route.params.processId)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
if (!process) {
|
||||
await this._applicationService.createProcess({
|
||||
id: +route.params.processId,
|
||||
type: 'customer-order',
|
||||
section: 'customer',
|
||||
name: `Kundenbestellungen`,
|
||||
});
|
||||
}
|
||||
|
||||
await this.removeBreadcrumbWithSameProcessId(route);
|
||||
this._applicationService.activateProcess(+route.params.processId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fix #3292: Alle Breadcrumbs die nichts mit dem aktuellen Prozess zu tun haben, müssen removed werden
|
||||
async removeBreadcrumbWithSameProcessId(route: ActivatedRouteSnapshot) {
|
||||
const crumbs = await this._breadcrumbService
|
||||
.getBreadcrumbByKey$(+route.params.processId)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
// Entferne alle Crumbs die nichts mit den Kundenbestellungen zu tun haben
|
||||
if (crumbs.length > 1) {
|
||||
const crumbsToRemove = crumbs.filter((crumb) => crumb.tags.find((tag) => tag === 'customer-order') === undefined);
|
||||
for (const crumb of crumbsToRemove) {
|
||||
await this._breadcrumbService.removeBreadcrumb(crumb.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processNumber(processes: ApplicationProcess[]) {
|
||||
const processNumbers = processes?.map((process) => Number(process?.name?.replace(/\D/g, '')));
|
||||
return !!processNumbers && processNumbers?.length > 0 ? Math.max(...processNumbers) + 1 : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { CustomerOrdersNavigationService } from '@shared/services';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanActivateCustomerOrdersGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _checkoutService: DomainCheckoutService,
|
||||
private readonly _navigationService: CustomerOrdersNavigationService
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
|
||||
|
||||
let lastActivatedProcessId = (
|
||||
await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'cart').pipe(first()).toPromise()
|
||||
)?.id;
|
||||
|
||||
const lastActivatedCartCheckoutProcessId = (
|
||||
await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'cart-checkout').pipe(first()).toPromise()
|
||||
)?.id;
|
||||
|
||||
const activatedProcessId = await this._applicationService.getActivatedProcessId$().pipe(first()).toPromise();
|
||||
|
||||
// Darf nur reinkommen wenn der aktuell aktive Tab ein Bestellabschluss Tab ist
|
||||
if (!!lastActivatedCartCheckoutProcessId && lastActivatedCartCheckoutProcessId === activatedProcessId) {
|
||||
await this.fromCartCheckoutProcess(processes, route, lastActivatedCartCheckoutProcessId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!lastActivatedProcessId) {
|
||||
await this.fromGoodsOutProcess(processes, route);
|
||||
return false;
|
||||
} else {
|
||||
await this._navigationService.navigateToCustomerOrdersSearch({ processId: lastActivatedProcessId });
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bei offenen Kundenbestellungen und Klick auf Kundenbestellungen
|
||||
async fromGoodsOutProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot) {
|
||||
const newProcessId = Date.now();
|
||||
await this._applicationService.createProcess({
|
||||
id: newProcessId,
|
||||
type: 'cart',
|
||||
section: 'customer',
|
||||
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
|
||||
});
|
||||
|
||||
await this._navigationService.navigateToCustomerOrdersSearch({ processId: newProcessId });
|
||||
}
|
||||
|
||||
// Bei offener Bestellbestätigung und Klick auf Kundenbestellungen
|
||||
async fromCartCheckoutProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot, processId: number) {
|
||||
// Um alle Checkout Daten zu resetten die mit dem Prozess assoziiert sind
|
||||
this._checkoutService.removeProcess({ processId });
|
||||
|
||||
// Ändere type cart-checkout zu customer-order
|
||||
this._applicationService.patchProcess(processId, {
|
||||
id: processId,
|
||||
type: 'cart',
|
||||
section: 'customer',
|
||||
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
|
||||
data: {},
|
||||
});
|
||||
|
||||
// Navigation
|
||||
await this._navigationService.navigateToCustomerOrdersSearch({ processId });
|
||||
}
|
||||
|
||||
getUrlFromSnapshot(route: ActivatedRouteSnapshot, url: string[] = []): string[] {
|
||||
url.push(...route.url.map((segment) => segment.path));
|
||||
if (route.firstChild) {
|
||||
return this.getUrlFromSnapshot(route.firstChild, url);
|
||||
}
|
||||
return url.filter((segment) => !!segment);
|
||||
}
|
||||
|
||||
processNumber(processes: ApplicationProcess[]) {
|
||||
const processNumbers = processes?.map((process) => Number(process?.name?.replace(/\D/g, '')));
|
||||
return !!processNumbers && processNumbers.length > 0 ? this.findMissingNumber(processNumbers) : 1;
|
||||
}
|
||||
|
||||
findMissingNumber(processNumbers: number[]) {
|
||||
for (let missingNumber = 1; missingNumber < Math.max(...processNumbers); missingNumber++) {
|
||||
if (!processNumbers.find((number) => number === missingNumber)) {
|
||||
return missingNumber;
|
||||
}
|
||||
}
|
||||
return Math.max(...processNumbers) + 1;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -9,7 +10,7 @@ export class CanActivateProductGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _checkoutService: DomainCheckoutService,
|
||||
private readonly _router: Router
|
||||
private readonly _navigationService: ProductCatalogNavigationService
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
@@ -38,17 +39,17 @@ export class CanActivateProductGuard implements CanActivate {
|
||||
}
|
||||
|
||||
if (!lastActivatedProcessId) {
|
||||
await this.fromCartProcess(processes, route);
|
||||
await this.fromCartProcess(processes);
|
||||
return false;
|
||||
} else {
|
||||
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(lastActivatedProcessId)]));
|
||||
await this._navigationService.navigateToProductSearch({ processId: lastActivatedProcessId });
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bei offener Artikelsuche/Kundensuche und Klick auf Footer Artikelsuche
|
||||
async fromCartProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot) {
|
||||
async fromCartProcess(processes: ApplicationProcess[]) {
|
||||
const newProcessId = Date.now();
|
||||
await this._applicationService.createProcess({
|
||||
id: newProcessId,
|
||||
@@ -57,7 +58,7 @@ export class CanActivateProductGuard implements CanActivate {
|
||||
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
|
||||
});
|
||||
|
||||
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(newProcessId)]));
|
||||
await this._navigationService.navigateToProductSearch({ processId: newProcessId });
|
||||
}
|
||||
|
||||
// Bei offener Warenausgabe und Klick auf Footer Artikelsuche
|
||||
@@ -81,7 +82,7 @@ export class CanActivateProductGuard implements CanActivate {
|
||||
});
|
||||
|
||||
// Navigation
|
||||
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(processId)]));
|
||||
await this._navigationService.navigateToProductSearch({ processId });
|
||||
}
|
||||
|
||||
// Bei offener Bestellbestätigung und Klick auf Footer Artikelsuche
|
||||
@@ -99,7 +100,7 @@ export class CanActivateProductGuard implements CanActivate {
|
||||
});
|
||||
|
||||
// Navigation
|
||||
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(processId)]));
|
||||
await this._navigationService.navigateToProductSearch({ processId });
|
||||
}
|
||||
|
||||
getUrlFromSnapshot(route: ActivatedRouteSnapshot, url: string[] = []): string[] {
|
||||
|
||||
@@ -5,6 +5,8 @@ export * from './can-activate-customer.guard';
|
||||
export * from './can-activate-goods-in.guard';
|
||||
export * from './can-activate-goods-out-with-process-id.guard';
|
||||
export * from './can-activate-goods-out.guard';
|
||||
export * from './can-activate-customer-orders.guard';
|
||||
export * from './can-activate-customer-orders-with-process-id.guard';
|
||||
export * from './can-activate-product-with-process-id.guard';
|
||||
export * from './can-activate-product.guard';
|
||||
export * from './can-activate-remission.guard';
|
||||
|
||||
3
apps/isa-app/src/app/main.component.html
Normal file
3
apps/isa-app/src/app/main.component.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<shell-root>
|
||||
<router-outlet></router-outlet>
|
||||
</shell-root>
|
||||
10
apps/isa-app/src/app/main.component.ts
Normal file
10
apps/isa-app/src/app/main.component.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main',
|
||||
templateUrl: 'main.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MainComponent {
|
||||
constructor() {}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './shell.component';
|
||||
export * from './shell.module';
|
||||
// end:ng42.barrel
|
||||
@@ -1,88 +0,0 @@
|
||||
<div class="shell-header-wrapper">
|
||||
<shell-header [section]="section$ | async" (sectionChange)="setSection($event)">
|
||||
<a [routerLink]="['/kunde/dashboard']" routerLinkActive="active" class="dashboard-btn">
|
||||
<ui-icon icon="dashboard" size="26px"></ui-icon>
|
||||
</a>
|
||||
<button class="notifications-btn" [disabled]="(notificationCount$ | async) === 0" (click)="openNotifications()">
|
||||
<ui-icon icon="notification" size="26px"></ui-icon>
|
||||
<span class="notification-counter" *ngIf="notificationCount$ | async; let count">{{ count }}</span>
|
||||
</button>
|
||||
<button (click)="logout()" class="logout-btn">
|
||||
<span *ngIf="currentBranch$ | async; let currentBranch">{{ currentBranch.key | uppercase }}</span>
|
||||
<ui-icon icon="logout" size="26px"></ui-icon>
|
||||
</button>
|
||||
</shell-header>
|
||||
</div>
|
||||
<div class="shell-process-wrapper">
|
||||
<shell-process
|
||||
[label]="addProcessLabel$ | async"
|
||||
[canAddProcess]="canAddProcess$ | async"
|
||||
(addProcess)="addProcess(); processTabs?.last?.triggerAnimation()"
|
||||
>
|
||||
<shell-process-tab
|
||||
#processTabs
|
||||
(activateProcess)="activateProcess($event)"
|
||||
(closeProcess)="closeProcess($event)"
|
||||
(processAction)="processAction($event)"
|
||||
*ngFor="let process of processes$ | async; trackBy: trackByIdFn"
|
||||
[isActive]="(activatedProcessId$ | async) === process.id"
|
||||
[process]="process"
|
||||
></shell-process-tab>
|
||||
</shell-process>
|
||||
</div>
|
||||
<div class="main-wrapper">
|
||||
<main>
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="shell-footer-wrapper">
|
||||
<shell-footer *ngIf="section$ | async; let section">
|
||||
<ng-container *ngIf="section === 'customer'">
|
||||
<a [routerLink]="[customerBasePath$ | async, 'product']" routerLinkActive="active">
|
||||
<ui-icon icon="catalog" size="30px"></ui-icon>
|
||||
Artikelsuche
|
||||
</a>
|
||||
<a [routerLink]="[customerBasePath$ | async, 'customer']" routerLinkActive="active">
|
||||
<ui-icon icon="customer" size="24px"></ui-icon>
|
||||
Kundensuche
|
||||
</a>
|
||||
<a *ifRole="'Store'" [routerLink]="[customerBasePath$ | async, 'goods', 'out']" routerLinkActive="active">
|
||||
<ui-icon icon="box_out" size="24px"></ui-icon>
|
||||
Warenausgabe
|
||||
</a>
|
||||
<a *ifRole="'CallCenter'" [routerLink]="[customerBasePath$ | async, 'order']" routerLinkActive="active">
|
||||
<ui-svg-icon icon="package-variant-closed" [size]="28"></ui-svg-icon>
|
||||
Kundenbestellungen
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="section === 'branch'">
|
||||
<a [routerLink]="['/filiale/assortment']" routerLinkActive="active">
|
||||
<ui-svg-icon icon="shape-outline" [size]="24"></ui-svg-icon>
|
||||
Sortiment
|
||||
</a>
|
||||
<a [routerLink]="['/filiale/task-calendar']" routerLinkActive="active">
|
||||
<ui-icon icon="calendar_check" size="24px"></ui-icon>
|
||||
Tätigkeitskalender
|
||||
</a>
|
||||
<a [routerLink]="['/filiale/goods/in']" routerLinkActive="active">
|
||||
<ui-icon icon="box_return" size="24px"></ui-icon>
|
||||
Abholfach
|
||||
</a>
|
||||
<a [routerLink]="[remissionUrl$ | async]" [queryParams]="remissionQueryParams$ | async" routerLinkActive="active">
|
||||
<ui-icon icon="documents_refresh" size="24px"></ui-icon>
|
||||
Remission
|
||||
</a>
|
||||
<a [routerLink]="['/filiale/package-inspection']" routerLinkActive="active" (click)="fetchAndOpenPackages()">
|
||||
<ui-svg-icon icon="clipboard-check-outline" [size]="24"></ui-svg-icon>
|
||||
Wareneingang
|
||||
</a>
|
||||
</ng-container>
|
||||
</shell-footer>
|
||||
</div>
|
||||
|
||||
<button *ngIf="isDevelopment" class="block absolute bottom-0 right-0 z-tooltip p-4 opacity-5" (click)="debugOpen = !debugOpen">
|
||||
<ui-svg-icon icon="bug-outline"></ui-svg-icon>
|
||||
</button>
|
||||
|
||||
<app-debug *ngIf="debugOpen" class="absolute inset-x-0 top-0 max-h-[calc(100vh-80px)]"></app-debug>
|
||||
@@ -1,60 +0,0 @@
|
||||
:host {
|
||||
@apply block relative min-h-screen;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
@apply fixed right-0 left-0 overflow-auto;
|
||||
top: 8.375rem;
|
||||
bottom: 5rem;
|
||||
|
||||
main {
|
||||
@apply w-full max-w-content mx-auto px-4 self-stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.shell-header-wrapper {
|
||||
@apply fixed top-0 left-0 right-0 bg-white;
|
||||
|
||||
shell-header {
|
||||
@apply w-full max-w-content mx-auto;
|
||||
}
|
||||
|
||||
button.notifications-btn {
|
||||
@apply relative;
|
||||
|
||||
.notification-counter {
|
||||
@apply absolute flex items-center justify-center top-2 right-px-3 text-sm rounded-full w-6 h-6 font-semibold;
|
||||
background-color: var(--shell-notification-counter-background);
|
||||
color: var(--shell-notification-counter-text);
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shell-process-wrapper {
|
||||
@apply fixed left-0 right-0 bg-white;
|
||||
top: 5.125rem;
|
||||
|
||||
shell-process {
|
||||
@apply w-full max-w-content mx-auto;
|
||||
height: 52px;
|
||||
}
|
||||
}
|
||||
|
||||
shell-process {
|
||||
height: 52px;
|
||||
grid-area: process;
|
||||
}
|
||||
|
||||
.shell-footer-wrapper {
|
||||
@apply fixed bottom-0 left-0 right-0 bg-white z-fixed shadow-card;
|
||||
|
||||
shell-footer {
|
||||
@apply w-full max-w-content mx-auto;
|
||||
|
||||
.active {
|
||||
@apply font-bold;
|
||||
color: var(--shell-footer-link-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,445 +0,0 @@
|
||||
// unit test ShellComponent with Spectator
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { AuthModule, AuthService } from '@core/auth';
|
||||
import { Config } from '@core/config';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { DomainDashboardService } from '@domain/isa';
|
||||
import { NotificationsHub } from '@hub/notifications';
|
||||
import { ModalNotificationsComponent } from '@modal/notifications';
|
||||
import { Spectator, createComponentFactory, SpyObject, createSpyObject } from '@ngneat/spectator';
|
||||
import { DashboardComponent } from '@page/dashboard';
|
||||
import { ShellFooterComponent } from '@shell/footer';
|
||||
import { ShellHeaderComponent } from '@shell/header';
|
||||
import { ShellProcessComponent, ShellProcessTabComponent } from '@shell/process';
|
||||
import { IconRegistry, UiIconComponent, UiIconModule } from '@ui/icon';
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { EnvelopeDTO, MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
import { of } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { ShellComponent } from './shell.component';
|
||||
import { WrongDestinationModalService } from 'apps/page/package-inspection/src/lib/components/wrong-destination-modal/wrong-destination-modal.service';
|
||||
|
||||
// DummyComponent Class
|
||||
@Component({
|
||||
selector: 'dummy-component',
|
||||
template: '<div></div>',
|
||||
})
|
||||
class DummyComponent {
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
describe('ShellComponent', () => {
|
||||
let spectator: Spectator<ShellComponent>;
|
||||
let applicationServiceMock: SpyObject<ApplicationService>;
|
||||
let modalServiceMock: SpyObject<UiModalService>;
|
||||
let notificationsHubMock: SpyObject<NotificationsHub>;
|
||||
let router: Router;
|
||||
let breadcrumbServiceMock: SpyObject<BreadcrumbService>;
|
||||
let authServiceMock: SpyObject<AuthService>;
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: ShellComponent,
|
||||
imports: [
|
||||
UiIconModule,
|
||||
RouterTestingModule.withRoutes([
|
||||
{ path: 'kunde', component: DummyComponent },
|
||||
{ path: 'kunde/dashboard', component: DashboardComponent },
|
||||
]),
|
||||
AuthModule,
|
||||
],
|
||||
declarations: [
|
||||
MockComponent(ShellHeaderComponent),
|
||||
MockComponent(ShellFooterComponent),
|
||||
MockComponent(ShellProcessComponent),
|
||||
MockComponent(ShellProcessTabComponent),
|
||||
],
|
||||
mocks: [
|
||||
BreadcrumbService,
|
||||
DomainAvailabilityService,
|
||||
AuthService,
|
||||
DomainDashboardService,
|
||||
Config,
|
||||
WrongDestinationModalService,
|
||||
IconRegistry,
|
||||
],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
applicationServiceMock = createSpyObject(ApplicationService);
|
||||
applicationServiceMock.getSection$.and.returnValue(of('customer'));
|
||||
applicationServiceMock.getProcesses$.and.returnValue(of([]));
|
||||
applicationServiceMock.getProcessById$.and.returnValue(of({ id: 4000 }));
|
||||
applicationServiceMock.getActivatedProcessId$.and.returnValue(of(undefined));
|
||||
applicationServiceMock.getLastActivatedProcessWithSectionAndType$.and.returnValue(of({}));
|
||||
applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(of({}));
|
||||
|
||||
notificationsHubMock = createSpyObject(NotificationsHub);
|
||||
notificationsHubMock.notifications$ = of({});
|
||||
|
||||
modalServiceMock = createSpyObject(UiModalService);
|
||||
|
||||
authServiceMock = createSpyObject(AuthService);
|
||||
|
||||
spectator = createComponent({
|
||||
providers: [
|
||||
{ provide: ApplicationService, useValue: applicationServiceMock },
|
||||
{ provide: NotificationsHub, useValue: notificationsHubMock },
|
||||
{ provide: UiModalService, useValue: modalServiceMock },
|
||||
{ provide: AuthService, useValue: authServiceMock },
|
||||
],
|
||||
});
|
||||
|
||||
breadcrumbServiceMock = spectator.inject(BreadcrumbService);
|
||||
router = spectator.inject(Router);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('shell-header', () => {
|
||||
it('should call setSection() on sectionChange event with the section argument', () => {
|
||||
spyOn(spectator.component, 'setSection');
|
||||
spectator.triggerEventHandler('shell-header', 'sectionChange', 'branch');
|
||||
expect(spectator.component.setSection).toHaveBeenCalledWith('branch');
|
||||
});
|
||||
|
||||
it('should render the header buttons', () => {
|
||||
// Test verhält sich anders, wenn die größe des Browserfensters kleiner ist als 640px, da
|
||||
// die Buttons dann unsichtbar werden und ins Drei-Punkt Menü verschoben werden.
|
||||
if (document.body.clientWidth > 639) {
|
||||
expect(spectator.query('shell-header .notifications-btn')).toBeVisible();
|
||||
expect(spectator.query('shell-header .dashboard-btn')).toBeVisible();
|
||||
expect(spectator.query('shell-header .logout-btn')).toBeVisible();
|
||||
} else {
|
||||
expect(spectator.query('shell-header .notifications-btn')).not.toBeVisible();
|
||||
expect(spectator.query('shell-header .dashboard-btn')).not.toBeVisible();
|
||||
expect(spectator.query('shell-header .logout-btn')).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
it('should have a anchor tag which navigates to /kunde/dashboard', () => {
|
||||
const anchor = spectator.query('shell-header a');
|
||||
expect(anchor).toHaveAttribute('href', '/kunde/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('shell-process', () => {
|
||||
it('should call addProcess() on addProcess event', () => {
|
||||
spyOn(spectator.component, 'addProcess');
|
||||
spectator.triggerEventHandler('shell-process', 'addProcess', undefined);
|
||||
expect(spectator.component.addProcess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('shell-process-tab', () => {
|
||||
it('should render for each process', () => {
|
||||
const processes = [{}, {}, {}];
|
||||
applicationServiceMock.getSection$.and.returnValue(of('customer'));
|
||||
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.queryAll('shell-process-tab')).toHaveLength(processes.length);
|
||||
});
|
||||
|
||||
it('should call activateProcess() on activateProcess event', () => {
|
||||
const processes = [{ id: 1 }];
|
||||
|
||||
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
|
||||
spectator.detectComponentChanges();
|
||||
|
||||
spyOn(spectator.component, 'activateProcess');
|
||||
spectator.triggerEventHandler('shell-process-tab', 'activateProcess', processes[0].id);
|
||||
expect(spectator.component.activateProcess).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should call closeProcess() on closeProcess event', () => {
|
||||
const processes = [{ id: 1 }];
|
||||
|
||||
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
|
||||
spectator.detectComponentChanges();
|
||||
|
||||
spyOn(spectator.component, 'closeProcess');
|
||||
spectator.triggerEventHandler('shell-process-tab', 'closeProcess', processes[0].id);
|
||||
expect(spectator.component.closeProcess).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shell-footer', () => {
|
||||
it('should render when section is set', () => {
|
||||
applicationServiceMock.getSection$.and.returnValue(of('customer'));
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.query('shell-footer')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should not render when section is undefined', () => {
|
||||
applicationServiceMock.getSection$.and.returnValue(of(undefined));
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.query('shell-footer')).not.toBeVisible();
|
||||
});
|
||||
|
||||
xit('should display the menu items for section customer', () => {
|
||||
applicationServiceMock.getSection$.and.returnValue(of('customer'));
|
||||
spectator.component.customerBasePath$ = of('/kunde/1');
|
||||
spectator.detectComponentChanges();
|
||||
|
||||
authServiceMock.hasRole.and.returnValue(true);
|
||||
|
||||
const anchors = spectator.queryAll('shell-footer a');
|
||||
expect(anchors[0]).toHaveText('Artikelsuche');
|
||||
expect(anchors[0]).toHaveAttribute('href', '/kunde/1/product');
|
||||
expect(anchors[1]).toHaveText('Kundensuche');
|
||||
expect(anchors[1]).toHaveAttribute('href', '/kunde/1/customer');
|
||||
expect(anchors[2]).toHaveText('Warenausgabe');
|
||||
expect(anchors[2]).toHaveAttribute('href', '/kunde/1/goods/out');
|
||||
});
|
||||
|
||||
it('should display the menu items for section branch', () => {
|
||||
applicationServiceMock.getSection$.and.returnValue(of('branch'));
|
||||
spectator.detectComponentChanges();
|
||||
|
||||
const anchors = spectator.queryAll('shell-footer a');
|
||||
expect(anchors[0]).toHaveText('Sortiment');
|
||||
expect(anchors[0]).toHaveAttribute('href', '/filiale/assortment');
|
||||
expect(anchors[1]).toHaveText('Tätigkeitskalender');
|
||||
expect(anchors[1]).toHaveAttribute('href', '/filiale/task-calendar');
|
||||
expect(anchors[2]).toHaveText('Abholfach');
|
||||
expect(anchors[2]).toHaveAttribute('href', '/filiale/goods/in');
|
||||
expect(anchors[3]).toHaveText('Remission');
|
||||
expect(anchors[3]).toHaveAttribute('href', '/filiale/remission');
|
||||
// expect(anchors[4]).toHaveText('Wareneingang');
|
||||
// expect(anchors[4]).toHaveAttribute('href', '/filiale/package-inspection');
|
||||
});
|
||||
});
|
||||
|
||||
describe('activatedProcessId$', () => {
|
||||
it('should call _appService.getActivatedProcessId$() and return its value', async () => {
|
||||
applicationServiceMock.getActivatedProcessId$.and.returnValue(of(1));
|
||||
const processId = await spectator.component.activatedProcessId$.pipe(first()).toPromise();
|
||||
expect(processId).toBe(1);
|
||||
expect(applicationServiceMock.getActivatedProcessId$).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('section$', () => {
|
||||
it('should call _appService.getSection$() and return its value', async () => {
|
||||
applicationServiceMock.getSection$.and.returnValue(of('branch'));
|
||||
const section = await spectator.component.section$.pipe(first()).toPromise();
|
||||
expect(section).toBe('branch');
|
||||
expect(applicationServiceMock.getSection$).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('processes$', () => {
|
||||
it('should call _appService.processes$() and return its value', async () => {
|
||||
applicationServiceMock.getProcesses$.and.returnValue(of([{}, {}]));
|
||||
const processes = await spectator.component.processes$.pipe(first()).toPromise();
|
||||
expect(processes).toHaveLength(2);
|
||||
expect(applicationServiceMock.getProcesses$).toHaveBeenCalledWith('customer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remissionProcess$', () => {
|
||||
it('should call _appService.getProcessById$() with Remission Id and return its value', async () => {
|
||||
applicationServiceMock.getProcessById$.and.returnValue(of({ id: 4000 }));
|
||||
await spectator.component.remissionProcess$.pipe(first()).toPromise();
|
||||
expect(applicationServiceMock.getProcessById$).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('remissionUrl$', () => {
|
||||
it('should return the correct url if process.data.active is available', async () => {
|
||||
const process = {
|
||||
id: 4000,
|
||||
data: {
|
||||
active: 9999,
|
||||
},
|
||||
};
|
||||
applicationServiceMock.getProcessById$.and.returnValue(of(process));
|
||||
const url = await spectator.component.remissionUrl$.pipe(first()).toPromise();
|
||||
expect(url).toBe('/filiale/remission/9999/list');
|
||||
});
|
||||
|
||||
it('should return the correct url if process.data.active is not available', async () => {
|
||||
const process = {
|
||||
id: 4000,
|
||||
data: {},
|
||||
};
|
||||
applicationServiceMock.getProcessById$.and.returnValue(of(process));
|
||||
const url = await spectator.component.remissionUrl$.pipe(first()).toPromise();
|
||||
expect(url).toBe('/filiale/remission');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remissionQueryParams$', () => {
|
||||
it('should return the correct queryParams if process.data.active and process.data.queryParams are available', async () => {
|
||||
const process = {
|
||||
id: 4000,
|
||||
data: {
|
||||
active: 9999,
|
||||
queryParams: { filter: 'test' },
|
||||
},
|
||||
};
|
||||
applicationServiceMock.getProcessById$.and.returnValue(of(process));
|
||||
const queryParams = await spectator.component.remissionQueryParams$.pipe(first()).toPromise();
|
||||
expect(queryParams).toEqual(process.data.queryParams);
|
||||
});
|
||||
|
||||
it('should return the correct queryParams if process.data.active and process.data.queryParams are not available', async () => {
|
||||
const process = {
|
||||
id: 4000,
|
||||
data: {},
|
||||
};
|
||||
applicationServiceMock.getProcessById$.and.returnValue(of(process));
|
||||
const queryParams = await spectator.component.remissionQueryParams$.pipe(first()).toPromise();
|
||||
expect(queryParams).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSection()', () => {
|
||||
it('should call _appService.setSection() with the argument section', async () => {
|
||||
await spectator.component.setSection('customer');
|
||||
expect(applicationServiceMock.setSection).toHaveBeenCalledWith('customer');
|
||||
});
|
||||
|
||||
it('should call activateProcess if getLastActivatedProcessWithSection returns a value', async () => {
|
||||
applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(of({ id: 1 }));
|
||||
spyOn(spectator.component, 'activateProcess');
|
||||
await spectator.component.setSection('customer');
|
||||
expect(spectator.component.activateProcess).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout()', () => {
|
||||
it('should call _authService.logout()', () => {
|
||||
spectator.component.logout();
|
||||
expect(authServiceMock.logout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addProcess()', () => {
|
||||
it('should call navigate to /kunde/{timestamp}/product', () => {
|
||||
spyOn(router, 'navigate');
|
||||
spyOn(Date, 'now').and.returnValue(123);
|
||||
spectator.component.addProcess();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/kunde', 123, 'product']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeProcess()', () => {
|
||||
it('should call _appService.removeProcess() with the processId argument', () => {
|
||||
const processes = [{}, {}, {}];
|
||||
applicationServiceMock.getSection$.and.returnValue(of('customer'));
|
||||
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
|
||||
spectator.component.closeProcess(1);
|
||||
expect(applicationServiceMock.removeProcess).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should navigate to kunde/dashboard if no process is available', async () => {
|
||||
spyOn(router, 'navigate');
|
||||
applicationServiceMock.getSection$.and.returnValue(of('customer'));
|
||||
applicationServiceMock.getProcesses$.and.returnValue(of([]));
|
||||
spectator.detectComponentChanges();
|
||||
await spectator.component.closeProcess(1);
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/kunde', 'dashboard']);
|
||||
});
|
||||
|
||||
it('should not navigate to kunde/dashboard if processes are available', async () => {
|
||||
spyOn(router, 'navigate');
|
||||
const processes = [
|
||||
{ id: 1, name: 'test', section: 'customer' },
|
||||
{ id: 2, name: 'test', section: 'customer' },
|
||||
];
|
||||
|
||||
applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(of({}));
|
||||
applicationServiceMock.getSection$.and.returnValue(of('customer'));
|
||||
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
|
||||
await spectator.component.closeProcess(1);
|
||||
expect(router.navigate).not.toHaveBeenCalledWith(['/kunde', 'dashboard']);
|
||||
});
|
||||
|
||||
it('should activate the next process when it was not the last process', async () => {
|
||||
spyOn(spectator.component, 'activateProcess');
|
||||
|
||||
applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(
|
||||
of({
|
||||
id: 2,
|
||||
name: 'test',
|
||||
section: 'customer',
|
||||
activated: 2,
|
||||
})
|
||||
);
|
||||
|
||||
const processes = [
|
||||
{ id: 1, name: 'test', section: 'customer', activated: 1 },
|
||||
{ id: 2, name: 'test', section: 'customer', activated: 2 },
|
||||
];
|
||||
applicationServiceMock.getSection$.and.returnValue(of('customer'));
|
||||
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
|
||||
await spectator.component.closeProcess(1);
|
||||
|
||||
expect(spectator.component.activateProcess).toHaveBeenCalledWith(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('activateProcess()', () => {
|
||||
it('should get the last activated breadcrumb by key and if it is defined it navigates to its path with queryParams', async () => {
|
||||
const crumb = { path: '/kunde/product', params: { id: 1 } };
|
||||
spyOn(router, 'navigate');
|
||||
breadcrumbServiceMock.getLastActivatedBreadcrumbByKey$.and.returnValue(of(crumb));
|
||||
|
||||
await spectator.component.activateProcess(1);
|
||||
expect(router.navigate).toHaveBeenCalledWith([crumb.path], { queryParams: crumb.params });
|
||||
});
|
||||
|
||||
it('should navigate to /kunde if no breadcrumb for this process exists', async () => {
|
||||
breadcrumbServiceMock.getLastActivatedBreadcrumbByKey$.and.returnValue(of(undefined));
|
||||
spyOn(router, 'navigate');
|
||||
|
||||
await spectator.component.activateProcess(1);
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/kunde', 1, 'product']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processAction()', () => {
|
||||
it('should navigate to cart when process type is cart', () => {
|
||||
spyOn(router, 'navigate');
|
||||
|
||||
const process: ApplicationProcess = { id: 1, name: 'Vorgang', section: 'customer', type: 'cart' };
|
||||
spectator.component.processAction(process);
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/kunde', process.id, 'cart']);
|
||||
});
|
||||
|
||||
it('should not navigate to when process type is not cart', () => {
|
||||
spyOn(router, 'navigate');
|
||||
|
||||
const process: ApplicationProcess = { id: 1, name: 'Vorgang', section: 'customer', type: 'goods-out' };
|
||||
spectator.component.processAction(process);
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('openNotifications()', () => {
|
||||
it('should call modalService.open() with the ModalNotificationComponent', async () => {
|
||||
const notifications: EnvelopeDTO<MessageBoardItemDTO[]> = {
|
||||
data: [{}, {}, {}],
|
||||
};
|
||||
|
||||
spectator.component.notifications$ = of(notifications);
|
||||
|
||||
await spectator.component.openNotifications();
|
||||
expect(modalServiceMock.open).toHaveBeenCalledWith({
|
||||
content: ModalNotificationsComponent,
|
||||
data: notifications,
|
||||
config: {
|
||||
showScrollbarY: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,176 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy, ViewChildren, QueryList, TrackByFunction, NgZone } from '@angular/core';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { first, map, shareReplay, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { NotificationsHub } from '@hub/notifications';
|
||||
import { ModalNotificationsComponent } from '@modal/notifications';
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { Router } from '@angular/router';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { ShellProcessTabComponent } from '@shell/process';
|
||||
import { Config } from '@core/config';
|
||||
import { WrongDestinationModalService } from 'apps/page/package-inspection/src/lib/components/wrong-destination-modal/wrong-destination-modal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shell',
|
||||
templateUrl: 'shell.component.html',
|
||||
styleUrls: ['shell.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ShellComponent {
|
||||
isDevelopment = Boolean(this._config.get('debug'));
|
||||
|
||||
debugOpen = false;
|
||||
|
||||
@ViewChildren('processTabs')
|
||||
readonly processTabs: QueryList<ShellProcessTabComponent>;
|
||||
|
||||
notifications$ = this._notificationsHub.notifications$;
|
||||
|
||||
notificationCount$ = this.notifications$.pipe(
|
||||
map((notifications) => Object.values(notifications).reduce((acc, val) => acc + val?.length ?? 0, 0))
|
||||
);
|
||||
|
||||
get activatedProcessId$() {
|
||||
return this._appService.getActivatedProcessId$().pipe(
|
||||
tap((activatedProcessId) => {
|
||||
this.processTabs?.find((process) => process?.process?.id === activatedProcessId && !process?.isActive)?.slideIntoView();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
customerBasePath$ = this.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this._appService.getProcessById$(processId)),
|
||||
map((process) => {
|
||||
if (!!process && process.section === 'customer' && process.type !== 'cart-checkout') {
|
||||
// Übernehme aktiven Prozess
|
||||
return `/kunde/${process.id}`;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return '/kunde';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
get section$() {
|
||||
return this._appService.getSection$().pipe(shareReplay());
|
||||
}
|
||||
|
||||
get processes$() {
|
||||
return this.section$.pipe(switchMap((section) => this._appService.getProcesses$(section)));
|
||||
}
|
||||
|
||||
get remissionProcess$() {
|
||||
return this._appService.getProcessById$(this._config.get('process.ids.remission'));
|
||||
}
|
||||
|
||||
get remissionUrl$() {
|
||||
return this.remissionProcess$.pipe(
|
||||
map((process) => (process?.data?.active ? `/filiale/remission/${process.data.active}/list` : '/filiale/remission'))
|
||||
);
|
||||
}
|
||||
|
||||
get remissionQueryParams$() {
|
||||
return this.remissionProcess$.pipe(
|
||||
map((process) => (process?.data?.active && process?.data?.queryParams ? process.data.queryParams : {}))
|
||||
);
|
||||
}
|
||||
|
||||
get addProcessLabel$() {
|
||||
return combineLatest([this.section$, this.processes$]).pipe(
|
||||
map(([section, processes]) => (section === 'customer' && processes.length === 0 ? 'VORGANG STARTEN' : ''))
|
||||
);
|
||||
}
|
||||
|
||||
get canAddProcess$() {
|
||||
return this.section$.pipe(map((section) => section === 'customer'));
|
||||
}
|
||||
|
||||
get currentBranch$() {
|
||||
return this._availabilityService.getDefaultBranch();
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _appService: ApplicationService,
|
||||
private readonly _config: Config,
|
||||
private readonly _notificationsHub: NotificationsHub,
|
||||
private readonly _modal: UiModalService,
|
||||
private readonly _router: Router,
|
||||
private readonly _breadcrumbService: BreadcrumbService,
|
||||
private readonly _authService: AuthService,
|
||||
private readonly _availabilityService: DomainAvailabilityService,
|
||||
private readonly _zone: NgZone,
|
||||
private readonly _wrongDestinationModalService: WrongDestinationModalService
|
||||
) {}
|
||||
|
||||
async setSection(section: 'customer' | 'branch') {
|
||||
this._appService.setSection(section);
|
||||
|
||||
const lastProcessId = (await this._appService.getLastActivatedProcessWithSection$(section).pipe(first()).toPromise())?.id;
|
||||
if (lastProcessId) {
|
||||
this.activateProcess(lastProcessId);
|
||||
} else {
|
||||
this._router.navigate([section === 'customer' ? '/kunde' : '/filiale']);
|
||||
}
|
||||
}
|
||||
|
||||
// Process werden über Guards erstellt und aktiviert. An dieser Stelle wird nur navigiert
|
||||
async addProcess() {
|
||||
const processId = Date.now();
|
||||
await this._router.navigate(['/kunde', processId, 'product']);
|
||||
}
|
||||
|
||||
async activateProcess(activatedProcessId: number) {
|
||||
try {
|
||||
const latestCrumb = await this._breadcrumbService?.getLastActivatedBreadcrumbByKey$(activatedProcessId)?.pipe(take(1)).toPromise();
|
||||
await this._zone.run(async () => {
|
||||
if (latestCrumb) {
|
||||
await this._router.navigate([latestCrumb.path], { queryParams: latestCrumb.params });
|
||||
} else {
|
||||
await this._router.navigate(['/kunde', activatedProcessId, 'product']);
|
||||
}
|
||||
});
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
async closeProcess(processId: number) {
|
||||
this._appService.removeProcess(processId);
|
||||
|
||||
const processes = await this.processes$.pipe(first()).toPromise();
|
||||
if (processes.length === 0) {
|
||||
await this._router.navigate(['/kunde', 'dashboard']);
|
||||
return;
|
||||
}
|
||||
|
||||
const section = await this.section$.pipe(first()).toPromise();
|
||||
const lastActivatedProcess = await this._appService.getLastActivatedProcessWithSection$(section).pipe(first()).toPromise();
|
||||
this.activateProcess(lastActivatedProcess?.id);
|
||||
}
|
||||
|
||||
processAction(process: ApplicationProcess) {
|
||||
if (process?.type === 'cart') {
|
||||
this._router.navigate(['/kunde', process.id, 'cart']);
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this._authService.logout();
|
||||
}
|
||||
|
||||
async openNotifications() {
|
||||
const notifications = await this.notifications$.pipe(first()).toPromise();
|
||||
this._modal.open({
|
||||
content: ModalNotificationsComponent,
|
||||
data: notifications,
|
||||
config: {
|
||||
showScrollbarY: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
trackByIdFn: TrackByFunction<ApplicationProcess> = (_, process) => process.id;
|
||||
|
||||
fetchAndOpenPackages = () => this._wrongDestinationModalService.fetchAndOpen();
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
|
||||
import { ShellHeaderModule } from '@shell/header';
|
||||
import { ShellProcessModule } from '@shell/process';
|
||||
import { ShellFooterModule } from '@shell/footer';
|
||||
|
||||
import { ShellComponent } from './shell.component';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AuthModule } from '@core/auth';
|
||||
import { DebugComponent } from '../debug/debug.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule,
|
||||
CommonModule,
|
||||
ShellHeaderModule,
|
||||
ShellProcessModule,
|
||||
ShellFooterModule,
|
||||
UiIconModule,
|
||||
OverlayModule,
|
||||
AuthModule,
|
||||
DebugComponent,
|
||||
],
|
||||
exports: [ShellComponent],
|
||||
declarations: [ShellComponent],
|
||||
providers: [],
|
||||
})
|
||||
export class ShellModule {}
|
||||
@@ -256,6 +256,21 @@
|
||||
"name": "badge",
|
||||
"data": "M140-80q-24 0-42-18t-18-42v-480q0-24 18-42t42-18h250v-140q0-24 18-42t42.411-18h59.178Q534-880 552-862t18 42v140h250q24 0 42 18t18 42v480q0 24-18 42t-42 18H140Zm0-60h680v-480H570v30q0 28-18 44t-42.411 16h-59.178Q426-530 408-546t-18-44v-30H140v480Zm92-107h239v-14q0-18-9-32t-23-19q-32-11-50-14.5t-35-3.5q-19 0-40.5 4.5T265-312q-15 5-24 19t-9 32v14Zm336-67h170v-50H568v50Zm-214-50q22.5 0 38.25-15.75T408-418q0-22.5-15.75-38.25T354-472q-22.5 0-38.25 15.75T300-418q0 22.5 15.75 38.25T354-364Zm214-63h170v-50H568v50ZM450-590h60v-230h-60v230Zm30 210Z",
|
||||
"viewBox": "0 -960 960 960"
|
||||
},
|
||||
{
|
||||
"name": "text-increase",
|
||||
"data": "m40-200 220-560h80l220 560h-75l-57-150H172l-57 150H40Zm156-214h208L302-685h-4L196-414Zm534 94v-130H600v-60h130v-130h60v130h130v60H790v130h-60Z",
|
||||
"viewBox": "0 -960 960 960"
|
||||
},
|
||||
{
|
||||
"name": "text-decrease",
|
||||
"data": "m40-200 220-560h80l220 560h-75l-57-150H172l-57 150H40Zm156-214h208L302-685h-4L196-414Zm414-36v-60h310v60H610Z",
|
||||
"viewBox":"0 -960 960 960"
|
||||
},
|
||||
{
|
||||
"name": "calendar-today",
|
||||
"data": "M180-80q-24 0-42-18t-18-42v-620q0-24 18-42t42-18h65v-60h65v60h340v-60h65v60h65q24 0 42 18t18 42v620q0 24-18 42t-42 18H180Zm0-60h600v-430H180v430Zm0-490h600v-130H180v130Zm0 0v-130 130Z",
|
||||
"viewBox": "0 -960 960 960"
|
||||
}
|
||||
|
||||
],
|
||||
@@ -324,6 +339,14 @@
|
||||
"name": "isa-hard-cover",
|
||||
"alias": "GEH"
|
||||
},
|
||||
{
|
||||
"name": "isa-hard-cover",
|
||||
"alias": "PP"
|
||||
},
|
||||
{
|
||||
"name": "isa-hard-cover",
|
||||
"alias": "DR"
|
||||
},
|
||||
{
|
||||
"name": "isa-hard-cover",
|
||||
"alias": "HC"
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
font-style: normal;
|
||||
font-weight: 100 700;
|
||||
src: url(./materials-icons-outlined.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Material Symbols Rounded';
|
||||
font-style: normal;
|
||||
font-weight: 100 700;
|
||||
src: url(./materials-icons-rounded.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Material Symbols Sharp';
|
||||
font-style: normal;
|
||||
font-weight: 100 700;
|
||||
src: url(./materials-icons-sharp.woff2) format('woff2');
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -16,6 +16,9 @@
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "5m"
|
||||
},
|
||||
"@swagger/isa": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
|
||||
},
|
||||
@@ -70,5 +73,7 @@
|
||||
"checkForUpdates": 3600000,
|
||||
"licence": {
|
||||
"scandit": "AZ7zLw2eLmFWHbYP4RDq8VAEgAxmNGYcPU8YpOc3DryEXj4zMzYQFrQuUm0YewGQYEESXjpRwGX1NYmKY3pXHnAn2DeqIzh2an+FUu9socQlbQnJiHJHoWBAqcqWSua+P12tc95P3s9aaEEYvSjUy7Md88f7N+sk6zZbUmqbMXeXqmZwdkmRoUY/2w0CiiiA4gBFHgu4sMeNQ9dWyfxKTUPf5AnsxnuYpCt5KLxJWSYDv8HHj0mx8DCJTe1m2ony97Lge3JbJ5Dd+Zz6SCwqik7fv53Qole9s/3m66lYFWKAzWRKkHN1zts78CmPxPb+AAHVoqlBM3duvYmnCxxGOmlXabKUNuDR2ExaMu/nlo532jqqy25Cet/FP1UAs96ZGRgzEcHxGPp6kA53lJ15zd+cxz6G93E83AmYJkhddXBQElWEaGtQRfrEzRGmvcksR+V8MMYjGmhkVbQxGGqpnfP4IxbuEFcef6bxxTiulzo75gXoqZTt+7C1qpDcrMM3Yp0Z8RBw3JlV2tLk4FYFZpxY8QrXIcjvRYKExtQ9e5sSbST4Vx95YhEUd6iX0SBPDzcmgR4/Ef6gvJfoWgz68+rqhBGckphdHi2Mf/pYuAlh2jbwtrkErE2xWARBejR/UcU/A3F7k9RkFd5/QZC7qhsE6bZH7uhpkptIbi5XkXagwYy1oJD7yJs4VLOJteYWferRm8h1auxXew5tL8VLHciF+lLj6h8PTUDt2blLgUjHtualqlCwdSTzJyYwk4oswGGDk6E48X7LXpzuhtR8TYTOi2REN0uuTbO/slFBRw+CaYUnD0LjB9p2lb8ndcdV9adzBKmwPxiOtlOELQ=="
|
||||
}
|
||||
},
|
||||
"@shared/icon": "/assets/icons.json"
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"title": "ISA - Integration",
|
||||
"silentRefresh": {
|
||||
"interval": 300000
|
||||
"interval": 60000
|
||||
},
|
||||
"@cdn/product-image": {
|
||||
"url": "https://produktbilder.paragon-data.net"
|
||||
@@ -15,6 +15,9 @@
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "5m"
|
||||
},
|
||||
"@swagger/isa": {
|
||||
"rootUrl": "https://isa-integration.paragon-data.net/isa/v1"
|
||||
},
|
||||
@@ -66,8 +69,9 @@
|
||||
"assortment": 6000
|
||||
}
|
||||
},
|
||||
"checkForUpdates": 3600000,
|
||||
"checkForUpdates": 900000,
|
||||
"licence": {
|
||||
"scandit": "AZ7zLw2eLmFWHbYP4RDq8VAEgAxmNGYcPU8YpOc3DryEXj4zMzYQFrQuUm0YewGQYEESXjpRwGX1NYmKY3pXHnAn2DeqIzh2an+FUu9socQlbQnJiHJHoWBAqcqWSua+P12tc95P3s9aaEEYvSjUy7Md88f7N+sk6zZbUmqbMXeXqmZwdkmRoUY/2w0CiiiA4gBFHgu4sMeNQ9dWyfxKTUPf5AnsxnuYpCt5KLxJWSYDv8HHj0mx8DCJTe1m2ony97Lge3JbJ5Dd+Zz6SCwqik7fv53Qole9s/3m66lYFWKAzWRKkHN1zts78CmPxPb+AAHVoqlBM3duvYmnCxxGOmlXabKUNuDR2ExaMu/nlo532jqqy25Cet/FP1UAs96ZGRgzEcHxGPp6kA53lJ15zd+cxz6G93E83AmYJkhddXBQElWEaGtQRfrEzRGmvcksR+V8MMYjGmhkVbQxGGqpnfP4IxbuEFcef6bxxTiulzo75gXoqZTt+7C1qpDcrMM3Yp0Z8RBw3JlV2tLk4FYFZpxY8QrXIcjvRYKExtQ9e5sSbST4Vx95YhEUd6iX0SBPDzcmgR4/Ef6gvJfoWgz68+rqhBGckphdHi2Mf/pYuAlh2jbwtrkErE2xWARBejR/UcU/A3F7k9RkFd5/QZC7qhsE6bZH7uhpkptIbi5XkXagwYy1oJD7yJs4VLOJteYWferRm8h1auxXew5tL8VLHciF+lLj6h8PTUDt2blLgUjHtualqlCwdSTzJyYwk4oswGGDk6E48X7LXpzuhtR8TYTOi2REN0uuTbO/slFBRw+CaYUnD0LjB9p2lb8ndcdV9adzBKmwPxiOtlOELQ=="
|
||||
}
|
||||
},
|
||||
"@shared/icon": "/assets/icons.json"
|
||||
}
|
||||
@@ -47,6 +47,9 @@
|
||||
"@swagger/wws": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/wws/v1"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "30s"
|
||||
},
|
||||
"hubs": {
|
||||
"notifications": {
|
||||
"url": "https://isa-test.paragon-data.net/isa/v1/rt",
|
||||
@@ -71,5 +74,6 @@
|
||||
"checkForUpdates": 3600000,
|
||||
"licence": {
|
||||
"scandit": "AZ7zLw2eLmFWHbYP4RDq8VAEgAxmNGYcPU8YpOc3DryEXj4zMzYQFrQuUm0YewGQYEESXjpRwGX1NYmKY3pXHnAn2DeqIzh2an+FUu9socQlbQnJiHJHoWBAqcqWSua+P12tc95P3s9aaEEYvSjUy7Md88f7N+sk6zZbUmqbMXeXqmZwdkmRoUY/2w0CiiiA4gBFHgu4sMeNQ9dWyfxKTUPf5AnsxnuYpCt5KLxJWSYDv8HHj0mx8DCJTe1m2ony97Lge3JbJ5Dd+Zz6SCwqik7fv53Qole9s/3m66lYFWKAzWRKkHN1zts78CmPxPb+AAHVoqlBM3duvYmnCxxGOmlXabKUNuDR2ExaMu/nlo532jqqy25Cet/FP1UAs96ZGRgzEcHxGPp6kA53lJ15zd+cxz6G93E83AmYJkhddXBQElWEaGtQRfrEzRGmvcksR+V8MMYjGmhkVbQxGGqpnfP4IxbuEFcef6bxxTiulzo75gXoqZTt+7C1qpDcrMM3Yp0Z8RBw3JlV2tLk4FYFZpxY8QrXIcjvRYKExtQ9e5sSbST4Vx95YhEUd6iX0SBPDzcmgR4/Ef6gvJfoWgz68+rqhBGckphdHi2Mf/pYuAlh2jbwtrkErE2xWARBejR/UcU/A3F7k9RkFd5/QZC7qhsE6bZH7uhpkptIbi5XkXagwYy1oJD7yJs4VLOJteYWferRm8h1auxXew5tL8VLHciF+lLj6h8PTUDt2blLgUjHtualqlCwdSTzJyYwk4oswGGDk6E48X7LXpzuhtR8TYTOi2REN0uuTbO/slFBRw+CaYUnD0LjB9p2lb8ndcdV9adzBKmwPxiOtlOELQ=="
|
||||
}
|
||||
},
|
||||
"@shared/icon": "/assets/icons.json"
|
||||
}
|
||||
@@ -16,6 +16,9 @@
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "5m"
|
||||
},
|
||||
"@swagger/isa": {
|
||||
"rootUrl": "https://isa.paragon-systems.de/isa/v1"
|
||||
},
|
||||
@@ -70,5 +73,6 @@
|
||||
"checkForUpdates": 3600000,
|
||||
"licence": {
|
||||
"scandit": "AZZzfQ+eLFl3Dzf1QSBag1lDibIoOPh4W33erRIRe3SDUMkHDX8eczEjd2TnfRMWoE5lXOBGtESCWICN9EbrmI1S9Lu5APsvvEOD+K54ADwIVawx0HNZRAc8/+9Vf/izcEGOFQFGBQJyR6vzdzFv5HcjznhxI9E3LiF+uVQPtCqsVYzpkMWIrC5VCg2uwNrj9Bw6f8zYi/lZPrDMS5yVKVcajeK7sh9QAq17dR0opjIIuP5t5nDEJ7hnITwtTR5HaM6cX/KhKpTILOgKexvLYqrK6QJWpU85sDwqwn6T7av4V68qL3XrUo60dScop4QsvraQe1HkRsffl6DkAEoX0RNMS5qVWjGerW7lvA/DQd9hsAO3jWFDR9hVDyt2VvmzzFKnHYqTYxC5qG4bCEJ0RJjy6tEP5Q7vL5SxWygVadmjPv+TwDOCS7DxzxIjcO+BXQY7gW6qn0hx9fXzyvO3avrGWqyImMlgEApZq+36ANqtRcPD/stEe4i0N9dSPhYoHPcc/9/9jpts43FozlgfY4wY8Wt5ybB3X0caISMmB/klFIJKKN7num439z3+Xk7ENB/Xvb0XAtnOt/cuxQYsGQ7fb62GOO/7Va5fdE9ZfaIJsS5ToE6oIbV04pLUssJf9cUMsyPFVELYSJmyGPQQFRz0TTxxRvPapIWrfa2x5x3hYUpNTAdY3v0fN9l/1ZqNSBmIBLH/LoXaVJQ2DydGD1/QFZ2Z/S7zTYKg5/cSEpUgiYtbwutNZSjRH29ucSizC524k+Zst95T8G7LJaWCT8SQAcKXqCnjpiEGWzD++h0jXjn6BWjUnIHi0te+27vF/z6UQL00sWco5hUIqF66EiU="
|
||||
}
|
||||
},
|
||||
"@shared/icon": "/assets/icons.json"
|
||||
}
|
||||
@@ -16,6 +16,9 @@
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "5m"
|
||||
},
|
||||
"@swagger/isa": {
|
||||
"rootUrl": "https://isa-staging.paragon-systems.de/isa/v1"
|
||||
},
|
||||
@@ -70,5 +73,6 @@
|
||||
"checkForUpdates": 3600000,
|
||||
"licence": {
|
||||
"scandit": "AZZzfQ+eLFl3Dzf1QSBag1lDibIoOPh4W33erRIRe3SDUMkHDX8eczEjd2TnfRMWoE5lXOBGtESCWICN9EbrmI1S9Lu5APsvvEOD+K54ADwIVawx0HNZRAc8/+9Vf/izcEGOFQFGBQJyR6vzdzFv5HcjznhxI9E3LiF+uVQPtCqsVYzpkMWIrC5VCg2uwNrj9Bw6f8zYi/lZPrDMS5yVKVcajeK7sh9QAq17dR0opjIIuP5t5nDEJ7hnITwtTR5HaM6cX/KhKpTILOgKexvLYqrK6QJWpU85sDwqwn6T7av4V68qL3XrUo60dScop4QsvraQe1HkRsffl6DkAEoX0RNMS5qVWjGerW7lvA/DQd9hsAO3jWFDR9hVDyt2VvmzzFKnHYqTYxC5qG4bCEJ0RJjy6tEP5Q7vL5SxWygVadmjPv+TwDOCS7DxzxIjcO+BXQY7gW6qn0hx9fXzyvO3avrGWqyImMlgEApZq+36ANqtRcPD/stEe4i0N9dSPhYoHPcc/9/9jpts43FozlgfY4wY8Wt5ybB3X0caISMmB/klFIJKKN7num439z3+Xk7ENB/Xvb0XAtnOt/cuxQYsGQ7fb62GOO/7Va5fdE9ZfaIJsS5ToE6oIbV04pLUssJf9cUMsyPFVELYSJmyGPQQFRz0TTxxRvPapIWrfa2x5x3hYUpNTAdY3v0fN9l/1ZqNSBmIBLH/LoXaVJQ2DydGD1/QFZ2Z/S7zTYKg5/cSEpUgiYtbwutNZSjRH29ucSizC524k+Zst95T8G7LJaWCT8SQAcKXqCnjpiEGWzD++h0jXjn6BWjUnIHi0te+27vF/z6UQL00sWco5hUIqF66EiU="
|
||||
}
|
||||
},
|
||||
"@shared/icon": "/assets/icons.json"
|
||||
}
|
||||
@@ -17,6 +17,9 @@
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "5m"
|
||||
},
|
||||
"@swagger/isa": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
|
||||
},
|
||||
@@ -71,5 +74,6 @@
|
||||
"checkForUpdates": 3600000,
|
||||
"licence": {
|
||||
"scandit": "AZ7zLw2eLmFWHbYP4RDq8VAEgAxmNGYcPU8YpOc3DryEXj4zMzYQFrQuUm0YewGQYEESXjpRwGX1NYmKY3pXHnAn2DeqIzh2an+FUu9socQlbQnJiHJHoWBAqcqWSua+P12tc95P3s9aaEEYvSjUy7Md88f7N+sk6zZbUmqbMXeXqmZwdkmRoUY/2w0CiiiA4gBFHgu4sMeNQ9dWyfxKTUPf5AnsxnuYpCt5KLxJWSYDv8HHj0mx8DCJTe1m2ony97Lge3JbJ5Dd+Zz6SCwqik7fv53Qole9s/3m66lYFWKAzWRKkHN1zts78CmPxPb+AAHVoqlBM3duvYmnCxxGOmlXabKUNuDR2ExaMu/nlo532jqqy25Cet/FP1UAs96ZGRgzEcHxGPp6kA53lJ15zd+cxz6G93E83AmYJkhddXBQElWEaGtQRfrEzRGmvcksR+V8MMYjGmhkVbQxGGqpnfP4IxbuEFcef6bxxTiulzo75gXoqZTt+7C1qpDcrMM3Yp0Z8RBw3JlV2tLk4FYFZpxY8QrXIcjvRYKExtQ9e5sSbST4Vx95YhEUd6iX0SBPDzcmgR4/Ef6gvJfoWgz68+rqhBGckphdHi2Mf/pYuAlh2jbwtrkErE2xWARBejR/UcU/A3F7k9RkFd5/QZC7qhsE6bZH7uhpkptIbi5XkXagwYy1oJD7yJs4VLOJteYWferRm8h1auxXew5tL8VLHciF+lLj6h8PTUDt2blLgUjHtualqlCwdSTzJyYwk4oswGGDk6E48X7LXpzuhtR8TYTOi2REN0uuTbO/slFBRw+CaYUnD0LjB9p2lb8ndcdV9adzBKmwPxiOtlOELQ=="
|
||||
}
|
||||
},
|
||||
"@shared/icon": "/assets/icons.json"
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
debug: false,
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
debug: false,
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link href="/assets/fonts/fonts.css" rel="stylesheet" />
|
||||
<link href="/assets/icons/icons.css" rel="stylesheet" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
<meta name="theme-color" content="#1976d2" />
|
||||
</head>
|
||||
|
||||
@@ -14,26 +14,28 @@ if (environment.production) {
|
||||
|
||||
const debugService = new DebugService();
|
||||
|
||||
const consoleLog = console.log;
|
||||
if (environment.debug) {
|
||||
const consoleLog = console.log;
|
||||
|
||||
console.log = (...args) => {
|
||||
debugService.add({ type: 'log', args });
|
||||
consoleLog(...args);
|
||||
};
|
||||
console.log = (...args) => {
|
||||
debugService.add({ type: 'log', args });
|
||||
consoleLog(...args);
|
||||
};
|
||||
|
||||
const consoleWarn = console.warn;
|
||||
const consoleWarn = console.warn;
|
||||
|
||||
console.warn = (...args) => {
|
||||
debugService.add({ type: 'warn', args });
|
||||
consoleWarn(...args);
|
||||
};
|
||||
console.warn = (...args) => {
|
||||
debugService.add({ type: 'warn', args });
|
||||
consoleWarn(...args);
|
||||
};
|
||||
|
||||
const consoleError = console.error;
|
||||
const consoleError = console.error;
|
||||
|
||||
console.error = (...args) => {
|
||||
debugService.add({ type: 'error', args });
|
||||
consoleError(...args);
|
||||
};
|
||||
console.error = (...args) => {
|
||||
debugService.add({ type: 'error', args });
|
||||
consoleError(...args);
|
||||
};
|
||||
}
|
||||
|
||||
platformBrowserDynamic([{ provide: DebugService, useValue: debugService }])
|
||||
.bootstrapModule(AppModule)
|
||||
|
||||
@@ -11,15 +11,11 @@
|
||||
@import './scss/customer';
|
||||
@import './scss/branch';
|
||||
|
||||
* {
|
||||
@apply font-sans;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-background;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0; // remove scrollbar space
|
||||
height: 0;
|
||||
@@ -61,3 +57,18 @@ body {
|
||||
@apply block bg-gray-300 h-6;
|
||||
animation: load 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.input-control {
|
||||
@apply rounded border border-solid border-[#AEB7C1] px-4 py-[1.125rem] outline-none;
|
||||
}
|
||||
|
||||
// .input-control:focus,
|
||||
// .input-control:not(:placeholder-shown) {
|
||||
// @apply bg-white;
|
||||
// }
|
||||
|
||||
.input-control.ng-touched.ng-invalid {
|
||||
@apply border-brand;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@apply text-center text-regular my-6;
|
||||
@apply text-center text-p2 my-6;
|
||||
}
|
||||
|
||||
.bold {
|
||||
@@ -62,7 +62,7 @@ hr {
|
||||
@apply flex flex-row items-center;
|
||||
|
||||
.cta-reserve {
|
||||
@apply absolute bg-transparent text-brand text-base font-bold border-none px-1 -mr-1;
|
||||
@apply absolute bg-transparent text-brand text-p2 font-bold border-none px-1 -mr-1;
|
||||
right: 85px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import { createComponentFactory, Spectator, SpyObject } from '@ngneat/spectator';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { Router } from '@angular/router';
|
||||
import { UiFilter } from '@ui/filter';
|
||||
import { MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
|
||||
import { Component } from '@angular/core';
|
||||
import { ModalNotificationsListItemComponent } from '../notifications-list-item/notifications-list-item.component';
|
||||
import { ModalNotificationsRemissionGroupComponent } from './notifications-remission-group.component';
|
||||
|
||||
// DummyComponent Class
|
||||
@Component({
|
||||
selector: 'dummy-component',
|
||||
template: '<div></div>',
|
||||
})
|
||||
class DummyComponent {
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
describe('ModalNotificationsRemissionGroupComponent', () => {
|
||||
let spectator: Spectator<ModalNotificationsRemissionGroupComponent>;
|
||||
let uiFilterMock: SpyObject<UiFilter>;
|
||||
let router: Router;
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: ModalNotificationsRemissionGroupComponent,
|
||||
declarations: [ModalNotificationsListItemComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterTestingModule.withRoutes([
|
||||
{ path: 'filiale/goods/in/results', component: DummyComponent },
|
||||
{ path: 'filiale/remission/create', component: DummyComponent },
|
||||
]),
|
||||
UiIconModule,
|
||||
],
|
||||
providers: [],
|
||||
mocks: [UiFilter],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent({ props: { notifications: [] } });
|
||||
router = spectator.inject(Router);
|
||||
uiFilterMock = spectator.inject(UiFilter);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('notifications input', () => {
|
||||
it('should display the right notification-counter value based on the length of the input array', () => {
|
||||
spectator.setInput({ notifications: [{}, {}, {}] });
|
||||
expect(spectator.query('.notification-counter')).toHaveText('3');
|
||||
});
|
||||
|
||||
it('should not display notification-counter if input array has length 0', () => {
|
||||
spectator.setInput({ notifications: [] });
|
||||
expect(spectator.query('.notification-counter')).toHaveText('');
|
||||
});
|
||||
|
||||
it('should render modal-notifications-list-item based on the input array', () => {
|
||||
const notifications = [{}, {}];
|
||||
spectator.setInput({ notifications });
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.queryAll('modal-notifications-list-item')).toHaveLength(notifications.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('itemSelected()', () => {
|
||||
it('should navigate to results with queryParams from UiFilter.getQueryParamsFromQueryTokenDTO()', () => {
|
||||
const item: MessageBoardItemDTO = { queryToken: { input: { main_qs: 'test' } } };
|
||||
spyOn(UiFilter, 'getQueryParamsFromQueryTokenDTO').and.returnValue(item.queryToken.input);
|
||||
spyOn(router, 'navigate');
|
||||
spectator.component.itemSelected(item);
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/filiale/goods/in/results'], { queryParams: item.queryToken.input });
|
||||
});
|
||||
|
||||
it('should emit the navigated event after select item', () => {
|
||||
const item: MessageBoardItemDTO = { queryToken: { input: { main_qs: 'test' } } };
|
||||
spyOn(spectator.component.navigated, 'emit');
|
||||
spectator.component.itemSelected(item);
|
||||
expect(spectator.component.navigated.emit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions CTA', () => {
|
||||
it('should navigate to remission page after clicking the CTA', () => {
|
||||
const cta = spectator.query('.cta-primary');
|
||||
expect(cta).toHaveText('Zur Remission');
|
||||
expect(cta).toHaveAttribute('href', '/filiale/remission/create');
|
||||
});
|
||||
|
||||
it('should emit the navigated event after clicking the CTA', () => {
|
||||
const cta = spectator.query('.cta-primary') as HTMLAnchorElement;
|
||||
spyOn(spectator.component.navigated, 'emit');
|
||||
spectator.click(cta);
|
||||
expect(spectator.component.navigated.emit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
import { createComponentFactory, Spectator, SpyObject } from '@ngneat/spectator';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { Router } from '@angular/router';
|
||||
import { UiFilter } from '@ui/filter';
|
||||
import { MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
|
||||
import { Component } from '@angular/core';
|
||||
import { ModalNotificationsListItemComponent } from '../notifications-list-item/notifications-list-item.component';
|
||||
import { ModalNotificationsRemissionGroupComponent } from './notifications-remission-group.component';
|
||||
|
||||
// DummyComponent Class
|
||||
@Component({
|
||||
selector: 'dummy-component',
|
||||
template: '<div></div>',
|
||||
})
|
||||
class DummyComponent {
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
describe('ModalNotificationsRemissionGroupComponent', () => {
|
||||
let spectator: Spectator<ModalNotificationsRemissionGroupComponent>;
|
||||
let uiFilterMock: SpyObject<UiFilter>;
|
||||
let router: Router;
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: ModalNotificationsRemissionGroupComponent,
|
||||
declarations: [ModalNotificationsListItemComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterTestingModule.withRoutes([
|
||||
{ path: 'filiale/goods/in/results', component: DummyComponent },
|
||||
{ path: 'filiale/remission/create', component: DummyComponent },
|
||||
]),
|
||||
UiIconModule,
|
||||
],
|
||||
providers: [],
|
||||
mocks: [UiFilter],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent({ props: { notifications: [] } });
|
||||
router = spectator.inject(Router);
|
||||
uiFilterMock = spectator.inject(UiFilter);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('notifications input', () => {
|
||||
it('should display the right notification-counter value based on the length of the input array', () => {
|
||||
spectator.setInput({ notifications: [{}, {}, {}] });
|
||||
expect(spectator.query('.notification-counter')).toHaveText('3');
|
||||
});
|
||||
|
||||
it('should not display notification-counter if input array has length 0', () => {
|
||||
spectator.setInput({ notifications: [] });
|
||||
expect(spectator.query('.notification-counter')).toHaveText('');
|
||||
});
|
||||
|
||||
it('should render modal-notifications-list-item based on the input array', () => {
|
||||
const notifications = [{}, {}];
|
||||
spectator.setInput({ notifications });
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.queryAll('modal-notifications-list-item')).toHaveLength(notifications.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('itemSelected()', () => {
|
||||
it('should navigate to results with queryParams from UiFilter.getQueryParamsFromQueryTokenDTO()', () => {
|
||||
const item: MessageBoardItemDTO = { queryToken: { input: { main_qs: 'test' } } };
|
||||
spyOn(UiFilter, 'getQueryParamsFromQueryTokenDTO').and.returnValue(item.queryToken.input);
|
||||
spyOn(router, 'navigate');
|
||||
spectator.component.itemSelected(item);
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/filiale/goods/in/results'], { queryParams: item.queryToken.input });
|
||||
});
|
||||
|
||||
it('should emit the navigated event after select item', () => {
|
||||
const item: MessageBoardItemDTO = { queryToken: { input: { main_qs: 'test' } } };
|
||||
spyOn(spectator.component.navigated, 'emit');
|
||||
spectator.component.itemSelected(item);
|
||||
expect(spectator.component.navigated.emit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions CTA', () => {
|
||||
it('should navigate to remission page after clicking the CTA', () => {
|
||||
const cta = spectator.query('.cta-primary');
|
||||
expect(cta).toHaveText('Zur Remission');
|
||||
expect(cta).toHaveAttribute('href', '/filiale/remission/create');
|
||||
});
|
||||
|
||||
it('should emit the navigated event after clicking the CTA', () => {
|
||||
const cta = spectator.query('.cta-primary') as HTMLAnchorElement;
|
||||
spyOn(spectator.component.navigated, 'emit');
|
||||
spectator.click(cta);
|
||||
expect(spectator.component.navigated.emit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
import { createComponentFactory, Spectator, SpyObject } from '@ngneat/spectator';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ModalNotificationsReservationGroupComponent } from './notifications-reservation-group.component';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { Router } from '@angular/router';
|
||||
import { UiFilter } from '@ui/filter';
|
||||
import { MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
|
||||
import { Component } from '@angular/core';
|
||||
import { ModalNotificationsListItemComponent } from '../notifications-list-item/notifications-list-item.component';
|
||||
|
||||
// DummyComponent Class
|
||||
@Component({
|
||||
selector: 'dummy-component',
|
||||
template: '<div></div>',
|
||||
})
|
||||
class DummyComponent {
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
describe('ModalNotificationsReservationGroupComponent', () => {
|
||||
let spectator: Spectator<ModalNotificationsReservationGroupComponent>;
|
||||
let uiFilterMock: SpyObject<UiFilter>;
|
||||
let router: Router;
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: ModalNotificationsReservationGroupComponent,
|
||||
declarations: [ModalNotificationsListItemComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterTestingModule.withRoutes([
|
||||
{ path: 'filiale/goods/in/results', component: DummyComponent },
|
||||
{ path: 'filiale/goods/in/reservation', component: DummyComponent },
|
||||
]),
|
||||
UiIconModule,
|
||||
],
|
||||
providers: [],
|
||||
mocks: [UiFilter],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent({ props: { notifications: [] } });
|
||||
router = spectator.inject(Router);
|
||||
uiFilterMock = spectator.inject(UiFilter);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('notifications input', () => {
|
||||
it('should display the right notification-counter value based on the length of the input array', () => {
|
||||
spectator.setInput({ notifications: [{}, {}, {}] });
|
||||
expect(spectator.query('.notification-counter')).toHaveText('3');
|
||||
});
|
||||
|
||||
it('should not display notification-counter if input array has length 0', () => {
|
||||
spectator.setInput({ notifications: [] });
|
||||
expect(spectator.query('.notification-counter')).toHaveText('');
|
||||
});
|
||||
|
||||
it('should render modal-notifications-list-item based on the input array', () => {
|
||||
const notifications = [{}, {}];
|
||||
spectator.setInput({ notifications });
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.queryAll('modal-notifications-list-item')).toHaveLength(notifications.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('itemSelected()', () => {
|
||||
it('should navigate to results with queryParams from UiFilter.getQueryParamsFromQueryTokenDTO()', () => {
|
||||
const item: MessageBoardItemDTO = { queryToken: { input: { main_qs: 'test' } } };
|
||||
spyOn(UiFilter, 'getQueryParamsFromQueryTokenDTO').and.returnValue(item.queryToken.input);
|
||||
spyOn(router, 'navigate');
|
||||
spectator.component.itemSelected(item);
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/filiale/goods/in/results'], { queryParams: item.queryToken.input });
|
||||
});
|
||||
|
||||
it('should emit the navigated event after select item', () => {
|
||||
const item: MessageBoardItemDTO = { queryToken: { input: { main_qs: 'test' } } };
|
||||
spyOn(spectator.component.navigated, 'emit');
|
||||
spectator.component.itemSelected(item);
|
||||
expect(spectator.component.navigated.emit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions CTA', () => {
|
||||
it('should navigate to reservation page after clicking the CTA', () => {
|
||||
const cta = spectator.query('.cta-primary');
|
||||
expect(cta).toHaveText('Zu den Reservierungen');
|
||||
expect(cta).toHaveAttribute('href', '/filiale/goods/in/reservation');
|
||||
});
|
||||
|
||||
it('should emit the navigated event after clicking the CTA', () => {
|
||||
const cta = spectator.query('.cta-primary') as HTMLAnchorElement;
|
||||
spyOn(spectator.component.navigated, 'emit');
|
||||
spectator.click(cta);
|
||||
expect(spectator.component.navigated.emit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
import { createComponentFactory, Spectator, SpyObject } from '@ngneat/spectator';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { Router } from '@angular/router';
|
||||
import { UiFilter } from '@ui/filter';
|
||||
import { MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
|
||||
import { Component } from '@angular/core';
|
||||
import { ModalNotificationsListItemComponent } from '../notifications-list-item/notifications-list-item.component';
|
||||
import { ModalNotificationsTaskCalendarGroupComponent } from './notifications-task-calendar-group.component';
|
||||
|
||||
// DummyComponent Class
|
||||
@Component({
|
||||
selector: 'dummy-component',
|
||||
template: '<div></div>',
|
||||
})
|
||||
class DummyComponent {
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
describe('ModalNotificationsTaskCalendarGroupComponent', () => {
|
||||
let spectator: Spectator<ModalNotificationsTaskCalendarGroupComponent>;
|
||||
let uiFilterMock: SpyObject<UiFilter>;
|
||||
let router: Router;
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: ModalNotificationsTaskCalendarGroupComponent,
|
||||
declarations: [ModalNotificationsListItemComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterTestingModule.withRoutes([
|
||||
{ path: 'filiale/goods/in/results', component: DummyComponent },
|
||||
{ path: 'filiale/task-calendar/calendar', component: DummyComponent },
|
||||
]),
|
||||
UiIconModule,
|
||||
],
|
||||
providers: [],
|
||||
mocks: [UiFilter],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent({ props: { notifications: [] } });
|
||||
router = spectator.inject(Router);
|
||||
uiFilterMock = spectator.inject(UiFilter);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('notifications input', () => {
|
||||
it('should display the right notification-counter value based on the length of the input array', () => {
|
||||
spectator.setInput({ notifications: [{}, {}, {}] });
|
||||
expect(spectator.query('.notification-counter')).toHaveText('3');
|
||||
});
|
||||
|
||||
it('should not display notification-counter if input array has length 0', () => {
|
||||
spectator.setInput({ notifications: [] });
|
||||
expect(spectator.query('.notification-counter')).toHaveText('');
|
||||
});
|
||||
|
||||
it('should render modal-notifications-list-item based on the input array', () => {
|
||||
const notifications = [{}, {}];
|
||||
spectator.setInput({ notifications });
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.queryAll('modal-notifications-list-item')).toHaveLength(notifications.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('itemSelected()', () => {
|
||||
it('should navigate to results with queryParams from UiFilter.getQueryParamsFromQueryTokenDTO()', () => {
|
||||
const item: MessageBoardItemDTO = { queryToken: { input: { main_qs: 'test' } } };
|
||||
spyOn(UiFilter, 'getQueryParamsFromQueryTokenDTO').and.returnValue(item.queryToken.input);
|
||||
spyOn(router, 'navigate');
|
||||
spectator.component.itemSelected(item);
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/filiale/goods/in/results'], { queryParams: item.queryToken.input });
|
||||
});
|
||||
|
||||
it('should emit the navigated event after select item', () => {
|
||||
const item: MessageBoardItemDTO = { queryToken: { input: { main_qs: 'test' } } };
|
||||
spyOn(spectator.component.navigated, 'emit');
|
||||
spectator.component.itemSelected(item);
|
||||
expect(spectator.component.navigated.emit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions CTA', () => {
|
||||
it('should navigate to reservation page after clicking the CTA', () => {
|
||||
const cta = spectator.query('.cta-primary');
|
||||
expect(cta).toHaveText('Zum Tätigkeitskalender');
|
||||
expect(cta).toHaveAttribute('href', '/filiale/task-calendar/calendar');
|
||||
});
|
||||
|
||||
it('should emit the navigated event after clicking the CTA', () => {
|
||||
const cta = spectator.query('.cta-primary') as HTMLAnchorElement;
|
||||
spyOn(spectator.component.navigated, 'emit');
|
||||
spectator.click(cta);
|
||||
expect(spectator.component.navigated.emit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { ModalNotificationsUpdateGroupComponent } from './notifications-update-group.component';
|
||||
|
||||
describe('ModalNotificationsUpdateGroupComponent', () => {
|
||||
let spectator: Spectator<ModalNotificationsUpdateGroupComponent>;
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: ModalNotificationsUpdateGroupComponent,
|
||||
imports: [CommonModule, UiIconModule],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent({ props: { notifications: [] } });
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('notifications input', () => {
|
||||
it('should display the right notification-counter value based on the length of the input array', () => {
|
||||
spectator.setInput({ notifications: [{}, {}, {}] });
|
||||
expect(spectator.query('.notification-counter')).toHaveText('3');
|
||||
});
|
||||
|
||||
it('should not display notification-counter if input array has length 0', () => {
|
||||
spectator.setInput({ notifications: [] });
|
||||
expect(spectator.query('.notification-counter')).toHaveText('');
|
||||
});
|
||||
|
||||
it('should render notification-headline and notification-text based on the input array', () => {
|
||||
const notifications = [{}, {}];
|
||||
spectator.setInput({ notifications });
|
||||
spectator.detectComponentChanges();
|
||||
expect(spectator.queryAll('.notification-headline')).toHaveLength(notifications.length);
|
||||
expect(spectator.queryAll('.notification-text')).toHaveLength(notifications.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -64,11 +64,11 @@ modal-notifications {
|
||||
@apply flex flex-row justify-between items-start;
|
||||
|
||||
h1 {
|
||||
@apply text-regular font-bold mb-2;
|
||||
@apply text-p2 font-bold mb-2;
|
||||
}
|
||||
|
||||
.notification-edit-cta {
|
||||
@apply bg-transparent text-brand text-base font-bold border-none px-1 -mr-1;
|
||||
@apply bg-transparent text-brand text-p2 font-bold border-none px-1 -mr-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { createComponentFactory, mockProvider, Spectator, SpyObject } from '@ngneat/spectator';
|
||||
import { ModalNotificationsComponent } from './notifications.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { UiModalRef, UiModalService } from '@ui/modal';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
describe('ModalNotificationsComponent', () => {
|
||||
let spectator: Spectator<ModalNotificationsComponent>;
|
||||
let modalRefMock: SpyObject<UiModalRef>;
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: ModalNotificationsComponent,
|
||||
imports: [CommonModule],
|
||||
providers: [mockProvider(UiModalRef, { data: { product: {} } })],
|
||||
mocks: [UiModalService],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
modalRefMock = spectator.inject(UiModalRef);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('close()', () => {
|
||||
it('should call _modalRef.close()', () => {
|
||||
spectator.component.close();
|
||||
expect(modalRefMock.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('get activeCard', () => {
|
||||
it('should return the activeCard', () => {
|
||||
spectator.component.activeCard = 'testCard';
|
||||
expect(spectator.component.activeCard).toBe('testCard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get activeCard$', () => {
|
||||
it('should return activeCard$', () => {
|
||||
spectator.component.activeCard = 'testCard';
|
||||
|
||||
spectator.component.activeCard$
|
||||
.subscribe((c) => {
|
||||
expect(c).toBe('testCard');
|
||||
})
|
||||
.unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
describe('get groupedNotifications', () => {
|
||||
it('should return the groupedNotifications', () => {
|
||||
spectator.component.groupedNotifications = [{ group: 'test', items: [] }];
|
||||
expect(spectator.component.groupedNotifications).toEqual([{ group: 'test', items: [] }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get groupedNotifications$', () => {
|
||||
it('should return the groupedNotifications$', () => {
|
||||
spectator.component.groupedNotifications = [{ group: 'test', items: [] }];
|
||||
|
||||
spectator.component.groupedNotifications$
|
||||
.subscribe((gn) => {
|
||||
expect(gn).toEqual([{ group: 'test', items: [] }]);
|
||||
})
|
||||
.unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
describe('activeNotifications$', () => {
|
||||
it('should return the item with the activeCard within the group', () => {
|
||||
spectator.component.groupedNotifications = [{ group: 'testCard', items: [{ text: 'testmessage' }] }];
|
||||
spectator.component.activeCard = 'testCard';
|
||||
spectator.component.activeNotifications$
|
||||
.subscribe((an) => {
|
||||
expect(an).toEqual([{ text: 'testmessage' }]);
|
||||
})
|
||||
.unsubscribe();
|
||||
});
|
||||
|
||||
it('should not return anything if the item with the activeCard is not within the group', () => {
|
||||
spectator.component.groupedNotifications = [{ group: 'testCard', items: [{ text: 'testmessage' }] }];
|
||||
spectator.component.activeCard = 'testCardtest';
|
||||
spectator.component.activeNotifications$
|
||||
.subscribe((an) => {
|
||||
expect(an).toBeUndefined();
|
||||
})
|
||||
.unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngOnInit()', () => {
|
||||
it('should call patchState with activeCard', () => {
|
||||
const activeCard = 'test';
|
||||
spectator = createComponent({ props: { activeCard, groupedNotifications: [{ group: activeCard, items: [] }] } });
|
||||
spyOn(spectator.component, 'patchState');
|
||||
spectator.component.ngOnInit();
|
||||
expect(spectator.component.patchState).toHaveBeenCalledWith({
|
||||
activeCard: [{ group: activeCard, items: [] }].find((_) => true)?.group,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@apply text-center text-regular font-semibold text-brand;
|
||||
@apply text-center text-p2 font-semibold text-brand;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ ui-select {
|
||||
@apply mt-px-35 text-center;
|
||||
|
||||
.print-btn {
|
||||
@apply border-none outline-none bg-brand text-white font-bold text-cta-l px-px-25 py-px-15 rounded-full my-8;
|
||||
@apply border-none outline-none bg-brand text-white font-bold text-p1 px-px-25 py-px-15 rounded-full my-8;
|
||||
|
||||
&:disabled {
|
||||
@apply bg-inactive-branch;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-regular font-semibold text-center;
|
||||
@apply text-p2 font-semibold text-center;
|
||||
}
|
||||
|
||||
hr {
|
||||
@@ -23,11 +23,11 @@ hr {
|
||||
@apply flex flex-col ml-5;
|
||||
|
||||
.product-name {
|
||||
@apply text-regular font-bold;
|
||||
@apply text-p2 font-bold;
|
||||
}
|
||||
|
||||
.product-format {
|
||||
@apply mt-5 text-regular font-bold flex items-center;
|
||||
@apply mt-5 text-p2 font-bold flex items-center;
|
||||
|
||||
.format-icon {
|
||||
@apply mr-2;
|
||||
@@ -35,11 +35,11 @@ hr {
|
||||
}
|
||||
|
||||
.product-ean {
|
||||
@apply text-regular font-bold;
|
||||
@apply text-p2 font-bold;
|
||||
}
|
||||
|
||||
.quantity {
|
||||
@apply text-regular font-bold;
|
||||
@apply text-p2 font-bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +31,12 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
@apply ml-5 text-regular font-bold;
|
||||
@apply ml-5 text-p2 font-bold;
|
||||
}
|
||||
|
||||
.btn-expand,
|
||||
.btn-collapse {
|
||||
@apply border-none bg-transparent outline-none text-regular text-ucla-blue font-bold -mt-2;
|
||||
@apply border-none bg-transparent outline-none text-p2 text-ucla-blue font-bold -mt-2;
|
||||
|
||||
ui-icon {
|
||||
@apply inline mx-1 align-middle;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<div
|
||||
class="page-price-update-item__item-header flex flex-row w-full items-center justify-between bg-[rgba(0,128,121,0.15)] mb-px-2 px-5 h-[53px] rounded-t-card"
|
||||
class="page-price-update-item__item-header flex flex-row w-full items-center justify-between bg-[rgba(0,128,121,0.15)] mb-px-2 px-5 h-[53px] rounded-t"
|
||||
>
|
||||
<p class="page-price-update-item__item-instruction font-bold text-lg">{{ item?.task?.instruction }}</p>
|
||||
<p class="page-price-update-item__item-due-date text-base">
|
||||
<p class="page-price-update-item__item-due-date text-p2">
|
||||
gültig ab <span class="font-bold ml-2">{{ item?.task?.dueDate | date }}</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -20,15 +20,15 @@
|
||||
|
||||
<div class="page-price-update-item__item-details">
|
||||
<div class="page-price-update-item__item-contributors flex flex-row">
|
||||
{{ environment.isTablet() ? (item?.product?.contributors | substr: 42) : item?.product?.contributors }}
|
||||
{{ environment.isTablet() ? (item?.product?.contributors | substr: 38) : item?.product?.contributors }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="page-price-update-item__item-title font-bold text-2xl"
|
||||
class="page-price-update-item__item-title font-bold text-h3"
|
||||
[class.text-xl]="item?.product?.name?.length >= 35"
|
||||
[class.text-lg]="item?.product?.name?.length >= 40"
|
||||
[class.text-md]="item?.product?.name?.length >= 50"
|
||||
[class.text-sm]="item?.product?.name?.length >= 60"
|
||||
[class.text-p3]="item?.product?.name?.length >= 60"
|
||||
[class.text-xs]="item?.product?.name?.length >= 100"
|
||||
>
|
||||
{{ item?.product?.name }}
|
||||
@@ -43,7 +43,7 @@
|
||||
src="assets/images/Icon_{{ item?.product?.format }}.svg"
|
||||
[alt]="item?.product?.formatDetail"
|
||||
/>
|
||||
{{ item?.product?.formatDetail }}
|
||||
{{ environment.isTablet() ? (item?.product?.formatDetail | substr: 25) : item?.product?.formatDetail }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
|
||||
<div class="page-price-update-item__item-select-bullet">
|
||||
<input *ngIf="isSelectable" [ngModel]="selected$ | async" (ngModelChange)="setSelected()" class="isa-select-bullet" type="checkbox" />
|
||||
<input *ngIf="isSelectable" [ngModel]="selected" (ngModelChange)="setSelected()" class="isa-select-bullet" type="checkbox" />
|
||||
</div>
|
||||
|
||||
<div class="page-price-update-item__item-stock flex flex-row font-bold">
|
||||
|
||||
@@ -47,7 +47,7 @@ export class PriceUpdateItemComponent {
|
||||
return this._store.isSelectable(this.item);
|
||||
}
|
||||
|
||||
selected$ = this._store.selectedItemUids$.pipe(map((selectedItemUids) => selectedItemUids.includes(this.item?.uId)));
|
||||
@Input() selected: boolean = false;
|
||||
|
||||
defaultBranch$ = this._availability.getDefaultBranch();
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
Drucken
|
||||
</button>
|
||||
<div class="flex flex-row items-center justify-end">
|
||||
<div *ngIf="getSelectableItems().length > 0" class="text-[#0556B4] font-bold text-sm mr-5">
|
||||
<div *ngIf="getSelectableItems().length > 0" class="text-[#0556B4] font-bold text-p3 mr-5">
|
||||
<ng-container *ngIf="selectedItemUids$ | async; let selectedItems">
|
||||
<button class="page-price-update-list__cta-unselect-all" *ngIf="selectedItems?.length > 0" type="button" (click)="unselectAll()">
|
||||
Alle entfernen ({{ selectedItems?.length }})
|
||||
@@ -18,7 +18,7 @@
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="page-price-update-list__items-count inline-flex flex-row items-center pr-5 text-sm">
|
||||
<div class="page-price-update-list__items-count inline-flex flex-row items-center pr-5 text-p3">
|
||||
{{ items?.length ??
|
||||
0 }}
|
||||
Titel
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-price-update-list__order-by h-[53px] flex flex-row items-center justify-center bg-white rounded-t-card mb-px-2">
|
||||
<div class="page-price-update-list__order-by h-[53px] flex flex-row items-center justify-center bg-white rounded-t mb-px-2">
|
||||
<ui-order-by-filter [orderBy]="orderBy$ | async" (selectedOrderByChange)="search()"> </ui-order-by-filter>
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<page-price-update-item
|
||||
*cdkVirtualFor="let item of items; let first; trackBy: trackByFn"
|
||||
[item]="item"
|
||||
[selected]="isSelected(item)"
|
||||
[class.mt-px-10]="!first"
|
||||
></page-price-update-item>
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ export class PriceUpdateListComponent {
|
||||
return this._store.items.filter((item) => this._store.isSelectable(item)) ?? [];
|
||||
}
|
||||
|
||||
isSelected(item: ProductListItemDTO) {
|
||||
return this._store.selectedItemUids.includes(item.uId);
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
const selectedItemUids = this.getSelectableItems().map((item) => item.uId);
|
||||
this._store.patchState({ selectedItemUids });
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<div class="flex flex-row items-center h-14 bg-white relative rounded-t font-bold shadow-lg">
|
||||
<h3 class="text-center grow font-bold text-2xl">Preisänderung</h3>
|
||||
<h3 class="text-center grow font-bold text-h3">Preisänderung</h3>
|
||||
<button
|
||||
(click)="filterOverlay.open()"
|
||||
class="absolute right-0 top-0 h-14 rounded px-5 text-lg bg-cadet-blue flex flex-row flex-nowrap items-center justify-center"
|
||||
type="button"
|
||||
>
|
||||
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
|
||||
<shared-icon class="mr-2" icon="filter-variant"></shared-icon>
|
||||
Filter
|
||||
</button>
|
||||
</div>
|
||||
@@ -16,7 +16,7 @@
|
||||
<shell-filter-overlay #filterOverlay class="relative">
|
||||
<div class="relative">
|
||||
<button type="button" class="absolute top-4 right-4 text-cadet" (click)="closeFilterOverlay()">
|
||||
<ui-svg-icon [icon]="'close'" [size]="28"></ui-svg-icon>
|
||||
<shared-icon [icon]="'close'" [size]="28"></shared-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
</shell-filter-overlay>
|
||||
|
||||
<ng-template #noResults>
|
||||
<div class="bg-white text-2xl text-center pt-10 font-bold rounded-b h-[calc(100vh_-_370px)]">Keine Preisänderungen vorhanden.</div>
|
||||
<div class="bg-white text-h3 text-center pt-10 font-bold rounded-b h-[calc(100vh_-_370px)]">Keine Preisänderungen vorhanden.</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Config } from '@core/config';
|
||||
import { provideComponentStore } from '@ngrx/component-store';
|
||||
import { ShellFilterOverlayComponent } from '@shell/filter-overlay';
|
||||
import { SharedFilterOverlayComponent } from '@shared/components/filter-overlay';
|
||||
import { UiFilter, UiFilterComponent } from '@ui/filter';
|
||||
import { PriceUpdateComponentStore } from './price-update.component.store';
|
||||
import { combineLatest, Subject, Subscription } from 'rxjs';
|
||||
@@ -26,8 +26,8 @@ export class PriceUpdateComponent implements OnInit {
|
||||
|
||||
hint$ = new Subject<string>();
|
||||
|
||||
@ViewChild(ShellFilterOverlayComponent)
|
||||
filterOverlay: ShellFilterOverlayComponent;
|
||||
@ViewChild(SharedFilterOverlayComponent)
|
||||
filterOverlay: SharedFilterOverlayComponent;
|
||||
|
||||
/**
|
||||
* Zeigt die liste an, wenn entweder keine items geladen werden oder wenn items geladen wurden
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ShellFilterOverlayModule } from '@shell/filter-overlay';
|
||||
import { SharedFilterOverlayModule } from '@shared/components/filter-overlay';
|
||||
import { UiFilterNextModule } from '@ui/filter';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { UiSpinnerModule } from '@ui/spinner';
|
||||
import { PriceUpdateListModule } from './price-update-list';
|
||||
import { PriceUpdateComponent } from './price-update.component';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, PriceUpdateListModule, UiIconModule, UiFilterNextModule, ShellFilterOverlayModule, UiSpinnerModule],
|
||||
imports: [CommonModule, PriceUpdateListModule, UiFilterNextModule, SharedFilterOverlayModule, UiSpinnerModule, IconComponent],
|
||||
exports: [PriceUpdateComponent],
|
||||
declarations: [PriceUpdateComponent],
|
||||
providers: [],
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply text-[#0556B4];
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<a [routerLink]="route?.path" [queryParams]="route?.queryParams">
|
||||
<ng-content></ng-content>
|
||||
</a>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-details-text-link',
|
||||
templateUrl: 'article-details-text-link.component.html',
|
||||
styleUrls: ['article-details-text-link.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page-article-details-text-link' },
|
||||
standalone: true,
|
||||
imports: [RouterLink],
|
||||
})
|
||||
export class ArticleDetailsTextLinkComponent {
|
||||
@Input()
|
||||
route: { path: string[]; queryParams?: Record<string, string> };
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply block whitespace-pre-line;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<ng-container *ngFor="let line of lines">
|
||||
<ng-container [ngSwitch]="line | lineType">
|
||||
<page-article-details-text-link *ngSwitchCase="'reihe'" [route]="line | reiheRoute"> {{ line }} <br /> </page-article-details-text-link>
|
||||
<ng-container *ngSwitchDefault> {{ line }} <br /> </ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, Renderer2, ElementRef, OnChanges, SimpleChanges, HostBinding } from '@angular/core';
|
||||
import { TextDTO } from '@swagger/cat';
|
||||
import { ArticleDetailsTextLinkComponent } from './article-details-text-link.component';
|
||||
import { NgFor, NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common';
|
||||
import { LineTypePipe } from './line-type.pipe';
|
||||
import { ReiheRoutePipe } from './reihe-route.pipe';
|
||||
|
||||
const REIHE_REGEX = /^Reihe:\s*"(.+)\"$/m;
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-details-text',
|
||||
templateUrl: 'article-details-text.component.html',
|
||||
styleUrls: ['article-details-text.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page-article-details-text' },
|
||||
standalone: true,
|
||||
imports: [ArticleDetailsTextLinkComponent, NgFor, NgSwitch, NgSwitchCase, NgSwitchDefault, LineTypePipe, ReiheRoutePipe],
|
||||
})
|
||||
export class ArticleDetailsTextComponent {
|
||||
@Input()
|
||||
text: TextDTO;
|
||||
|
||||
get lines() {
|
||||
return this.text?.value?.split('\n');
|
||||
}
|
||||
|
||||
constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
|
||||
|
||||
// ngOnChanges(changes: SimpleChanges): void {
|
||||
// if (changes.text) {
|
||||
// this.renderText();
|
||||
// }
|
||||
// }
|
||||
|
||||
// renderText() {
|
||||
// console.log(this.getReihe());
|
||||
// }
|
||||
|
||||
// getReihe() {
|
||||
// REIHE_REGEX.exec(this.text?.value)?.forEach((match, groupIndex) => {
|
||||
// if (groupIndex === 0) return;
|
||||
// return match;
|
||||
// });
|
||||
// }
|
||||
|
||||
// @HostBinding('innerHTML')
|
||||
// get htmlContent() {
|
||||
// let content = this.text?.value;
|
||||
|
||||
// REIHE_REGEX.exec(content)?.forEach((match, groupIndex) => {
|
||||
// if (groupIndex === 0) return;
|
||||
|
||||
// const aElement: HTMLAnchorElement = this.renderer.createElement('a');
|
||||
// const text = this.renderer.createText(match);
|
||||
// this.renderer.appendChild(aElement, text);
|
||||
// this.renderer.setAttribute(aElement, 'href', `/search?query=${match}`);
|
||||
|
||||
// content = content.replace(match, aElement.outerHTML);
|
||||
// });
|
||||
|
||||
// return content;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'lineType',
|
||||
standalone: true,
|
||||
pure: true,
|
||||
})
|
||||
export class LineTypePipe implements PipeTransform {
|
||||
transform(value: string, ...args: any[]): 'text' | 'reihe' {
|
||||
const REIHE_REGEX = /^Reihe:\s*"(.+)\"$/g;
|
||||
const reihe = REIHE_REGEX.exec(value)?.[1];
|
||||
return reihe ? 'reihe' : 'text';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Subscription, combineLatest, BehaviorSubject } from 'rxjs';
|
||||
import { distinctUntilChanged } from 'rxjs/operators';
|
||||
|
||||
@Pipe({
|
||||
name: 'reiheRoute',
|
||||
standalone: true,
|
||||
pure: false,
|
||||
})
|
||||
export class ReiheRoutePipe implements PipeTransform, OnDestroy {
|
||||
private subscription: Subscription;
|
||||
|
||||
value$ = new BehaviorSubject<string>('');
|
||||
|
||||
result: { path: string[]; queryParams?: Record<string, string> };
|
||||
|
||||
constructor(
|
||||
private navigation: ProductCatalogNavigationService,
|
||||
private application: ApplicationService,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {
|
||||
this.subscription = combineLatest([this.application.activatedProcessId$, this.value$])
|
||||
.pipe(distinctUntilChanged(isEqual))
|
||||
.subscribe(([processId, value]) => {
|
||||
const REIHE_REGEX = /^Reihe:\s*"(.+)\"$/g;
|
||||
const reihe = REIHE_REGEX.exec(value)?.[1];
|
||||
|
||||
if (!reihe) {
|
||||
this.result = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const main_qs = reihe.split('/')[0];
|
||||
|
||||
const path = this.navigation.getArticleSearchResultsPath(processId);
|
||||
|
||||
this.result = {
|
||||
path,
|
||||
queryParams: {
|
||||
main_qs,
|
||||
main_serial: 'serial',
|
||||
},
|
||||
};
|
||||
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
this.value$?.unsubscribe();
|
||||
}
|
||||
|
||||
transform(value: string, ...args: any[]) {
|
||||
this.value$.next(value);
|
||||
|
||||
return this.result;
|
||||
|
||||
// const REIHE_REGEX = /^Reihe:\s*"(.+)\"$/g;
|
||||
// const reihe = REIHE_REGEX.exec(value)?.[1];
|
||||
|
||||
// this.navigation.getArticleSearchResultsPath(this.)
|
||||
|
||||
// return reihe ? `/search?query=${reihe}` : null;
|
||||
}
|
||||
}
|
||||
@@ -1,141 +1,236 @@
|
||||
<ng-container *ngIf="!showRecommendations">
|
||||
<div #detailsContainer class="product-card">
|
||||
<div #detailsContainer class="page-article-details__container px-5 relative">
|
||||
<ng-container *ngIf="store.item$ | async; let item">
|
||||
<div class="product-details">
|
||||
<div class="product-image">
|
||||
<button class="image-button" (click)="showImages()">
|
||||
<img (load)="loadImage()" [src]="item.imageId | productImage: 195:315:true" alt="product image" />
|
||||
<ui-icon *ngIf="imageLoaded$ | async" icon="search_add" size="22px"></ui-icon>
|
||||
</button>
|
||||
<div class="page-article-details__product-details mb-3">
|
||||
<div class="page-article-details__product-bookmark justify-self-end">
|
||||
<div *ngIf="showArchivBadge$ | async" class="archiv-badge">
|
||||
<button [uiOverlayTrigger]="archivTooltip" class="p-0 m-0 outline-none border-none bg-transparent relative -top-px-5">
|
||||
<img src="/assets/images/bookmark_benachrichtigung_archiv.svg" alt="Archiv Badge" />
|
||||
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #archivTooltip [closeable]="true">
|
||||
<ng-container *ngIf="isAvailable$ | async; else notAvailable">
|
||||
Archivtitel. Wird nicht mehr gedruckt. Artikel ist bestellbar, weil lieferbar.
|
||||
</ng-container>
|
||||
<ng-template #notAvailable>
|
||||
Archivtitel. Wird nicht mehr gedruckt. Nicht bestellbar.
|
||||
</ng-template>
|
||||
</ui-tooltip>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="showSubscriptionBadge$ | async">
|
||||
<button [uiOverlayTrigger]="subscribtionTooltip" class="p-0 m-0 outline-none border-none bg-transparent relative -top-px-5">
|
||||
<img src="/assets/images/bookmark_subscription.svg" alt="Fortsetzungsartikel Badge" />
|
||||
</button>
|
||||
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #subscribtionTooltip [closeable]="true"
|
||||
>Artikel ist ein Fortsetzungsartikel,<br />
|
||||
Artikel muss über eine Aboabteilung<br />
|
||||
bestellt werden.
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div *ngIf="showPromotionBadge$ | async" class="promotion-badge">
|
||||
<button [uiOverlayTrigger]="promotionTooltip" class="p-0 m-0 outline-none border-none bg-transparent relative -top-px-5">
|
||||
<ui-icon-badge icon="gift" alt="Prämienkatalog Badge"></ui-icon-badge>
|
||||
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #promotionTooltip [closeable]="true">
|
||||
Dieser Artikel befindet sich im Prämienkatalog.
|
||||
</ui-tooltip>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button (click)="showReviews()" class="recessions" *ngIf="item.reviews?.length > 0">
|
||||
<div class="page-article-details__product-image-recessions flex flex-col items-center">
|
||||
<div class="page-article-details__product-image">
|
||||
<button class="border-none outline-none bg-transparent relative" (click)="showImages()">
|
||||
<img
|
||||
class="max-h-[19.6875rem] max-w-[12.1875rem] rounded"
|
||||
(load)="loadImage()"
|
||||
[src]="item.imageId | productImage: 195:315:true"
|
||||
alt="product image"
|
||||
/>
|
||||
<ui-icon
|
||||
class="absolute text-[#A7B9CB] inline-block bottom-[14px] right-[18px]"
|
||||
*ngIf="imageLoaded$ | async"
|
||||
icon="search_add"
|
||||
size="25px"
|
||||
></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
(click)="showReviews()"
|
||||
class="page-article-details__product-recessions flex flex-col mt-2 items-center bg-transparent border-none outline-none"
|
||||
*ngIf="item.reviews?.length > 0"
|
||||
>
|
||||
<ui-stars [rating]="store.reviewRating$ | async"></ui-stars>
|
||||
|
||||
<div class="cta-recessions">{{ item.reviews.length }} Rezensionen</div>
|
||||
<div class="text-p2 text-[#0556B4] font-bold">{{ item.reviews.length }} Rezensionen</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="product-info">
|
||||
<div class="row" [class.bookmark-badge-gap]="isBadgeVisible$ | async">
|
||||
<div>
|
||||
<a
|
||||
*ngFor="let contributor of contributors$ | async; let last = last"
|
||||
class="autor"
|
||||
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'search', 'results']"
|
||||
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
|
||||
>
|
||||
{{ contributor }}{{ last ? '' : ';' }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="page-article-details__product-contributors">
|
||||
<a
|
||||
*ngFor="let contributor of contributors$ | async; let last = last"
|
||||
class="text-[#0556B4] font-semibold no-underline text-p2"
|
||||
[routerLink]="resultsPath"
|
||||
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
|
||||
>
|
||||
{{ contributor }}{{ last ? '' : ';' }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button class="cta-print right" (click)="print()">Drucken</button>
|
||||
</div>
|
||||
<div class="title">
|
||||
{{ item.product?.name }}
|
||||
<div class="page-article-details__product-print justify-self-end" [class.mt-4]="isBadgeVisible$ | async">
|
||||
<button class="bg-transparent text-brand font-bold text-lg outline-none border-none p-0" (click)="print()">Drucken</button>
|
||||
</div>
|
||||
|
||||
<div class="page-article-details__product-title text-h3 font-bold mb-6">
|
||||
{{ item.product?.name }}
|
||||
</div>
|
||||
|
||||
<div class="page-article-details__product-misc flex flex-col mb-4">
|
||||
<div
|
||||
class="page-article-details__product-format flex items-center font-bold text-p3"
|
||||
*ngIf="item?.product?.format && item?.product?.formatDetail"
|
||||
>
|
||||
<img
|
||||
*ngIf="item?.product?.format !== '--'"
|
||||
class="flex mr-2 h-[1.125rem]"
|
||||
[src]="'/assets/images/Icon_' + item.product?.format + '.svg'"
|
||||
[alt]="item.product?.formatDetail"
|
||||
/>
|
||||
{{ item.product?.formatDetail }}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
<div class="format" *ngIf="item?.product?.format && item?.product?.formatDetail">
|
||||
<img
|
||||
*ngIf="item?.product?.format !== '--'"
|
||||
class="format-icon"
|
||||
[src]="'/assets/images/Icon_' + item.product?.format + '.svg'"
|
||||
[alt]="item.product?.formatDetail"
|
||||
/>
|
||||
{{ item.product?.formatDetail }}
|
||||
</div>
|
||||
<div *ngIf="item?.product?.volume">Band/Reihe {{ item?.product?.volume }}</div>
|
||||
<div class="page-article-details__product-volume" *ngIf="item?.product?.volume">Band/Reihe {{ item?.product?.volume }}</div>
|
||||
|
||||
<div>{{ publicationDate$ | async }}</div>
|
||||
</div>
|
||||
<div class="page-article-details__product-publication">{{ publicationDate$ | async }}</div>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<div class="price" *ngIf="item.catalogAvailability?.price?.value?.value; else retailPrice">
|
||||
{{ item.catalogAvailability?.price?.value?.value | currency: item.catalogAvailability?.price?.value?.currency:'code' }}
|
||||
</div>
|
||||
<ng-template #retailPrice>
|
||||
<div class="price" *ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability">
|
||||
{{
|
||||
takeAwayAvailability?.retailPrice?.value?.value | currency: takeAwayAvailability?.retailPrice?.value?.currency:'code'
|
||||
}}
|
||||
</div>
|
||||
</ng-template>
|
||||
<div *ngIf="store.promotionPoints$ | async; let promotionPoints">{{ promotionPoints }} Lesepunkte</div>
|
||||
</div>
|
||||
<div class="page-article-details__product-price-info flex flex-col mb-4">
|
||||
<div class="page-article-details__product-price font-bold text-xl self-end" *ngIf="price$ | async; let price">
|
||||
{{ price?.value?.value | currency: price?.value?.currency:'code' }}
|
||||
</div>
|
||||
<div *ngIf="price$ | async; let price" class="page-article-details__product-price-bound self-end">
|
||||
{{ price?.vat?.vatType | vat: (priceMaintained$ | async) }}
|
||||
</div>
|
||||
<div class="page-article-details__product-points self-end" *ngIf="store.promotionPoints$ | async; let promotionPoints">
|
||||
{{ promotionPoints }} Lesepunkte
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row stock">
|
||||
<div data-name="product-manufacturer">{{ item.product?.manufacturer }}</div>
|
||||
<div class="page-article-details__product-origin-infos flex flex-col mb-4">
|
||||
<div class="page-article-details__product-manufacturer" data-name="product-manufacturer">{{ item.product?.manufacturer }}</div>
|
||||
|
||||
<div class="right quantity" [uiOverlayTrigger]="tooltip" [overlayTriggerDisabled]="!(stockTooltipText$ | async)">
|
||||
<div class="fetching small" *ngIf="store.fetchingTakeAwayAvailability$ | async"></div>
|
||||
<ng-container *ngIf="!(store.fetchingTakeAwayAvailability$ | async)">
|
||||
<ng-container *ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability">
|
||||
<ui-icon icon="home" size="22px"></ui-icon>
|
||||
{{ takeAwayAvailability.inStock || 0 }}x
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<div class="page-article-details__product-language" *ngIf="item?.product?.locale" data-name="product-language">
|
||||
{{ item?.product?.locale }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-article-details__product-stock flex justify-end items-center">
|
||||
<div class="h-5 w-16 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]" *ngIf="store.fetchingTakeAwayAvailability$ | async"></div>
|
||||
<button
|
||||
class="flex flex-row py-4 pl-4"
|
||||
type="button"
|
||||
[uiOverlayTrigger]="tooltip"
|
||||
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
|
||||
(click)="showTooltip()"
|
||||
*ngIf="!(store.fetchingTakeAwayAvailability$ | async)"
|
||||
>
|
||||
<ng-container *ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability">
|
||||
<ui-icon class="mr-2 mb-1" icon="home" size="15px"></ui-icon>
|
||||
<span class="font-bold text-p3">{{ takeAwayAvailability.inStock || 0 }}x</span>
|
||||
</ng-container>
|
||||
</button>
|
||||
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-12" [closeable]="true">
|
||||
{{ stockTooltipText$ | async }}
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="page-article-details__product-ean-specs flex flex-col">
|
||||
<div class="page-article-details__product-ean" data-name="product-ean">{{ item.product?.ean }}</div>
|
||||
|
||||
<div class="page-article-details__product-specs">
|
||||
<ng-container *ngIf="item?.specs?.length > 0">
|
||||
{{ (item?.specs)[0]?.value }}
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-article-details__product-availabilities flex flex-row items-center justify-end mt-4">
|
||||
<div
|
||||
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
|
||||
*ngIf="store.fetchingTakeAwayAvailability$ | async; else showAvailabilityTakeAwayIcon"
|
||||
></div>
|
||||
<ng-template #showAvailabilityTakeAwayIcon>
|
||||
<div
|
||||
*ngIf="store.isTakeAwayAvailabilityAvailable$ | async"
|
||||
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center"
|
||||
>
|
||||
<ui-icon class="mx-1" icon="shopping_bag" size="18px"> </ui-icon>
|
||||
</div>
|
||||
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
|
||||
{{ stockTooltipText$ | async }}
|
||||
</ng-template>
|
||||
|
||||
<div
|
||||
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
|
||||
*ngIf="store.fetchingPickUpAvailability$ | async; else showAvailabilityPickUpIcon"
|
||||
></div>
|
||||
<ng-template #showAvailabilityPickUpIcon>
|
||||
<div
|
||||
#uiOverlayTrigger="uiOverlayTrigger"
|
||||
[uiOverlayTrigger]="orderDeadlineTooltip"
|
||||
*ngIf="store.isPickUpAvailabilityAvailable$ | async"
|
||||
class="page-article-details__product-pick-up-availability w-[2.25rem] h-[2.25rem] cursor-pointer bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
|
||||
[class.tooltip-active]="uiOverlayTrigger.opened"
|
||||
>
|
||||
<shared-icon icon="isa-box-out" [size]="24"></shared-icon>
|
||||
</div>
|
||||
|
||||
<ui-tooltip [warning]="true" yPosition="above" xPosition="after" [yOffset]="-12" #orderDeadlineTooltip [closeable]="true">
|
||||
<b>{{ (store.pickUpAvailability$ | async)?.orderDeadline | orderDeadline }}</b>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div *ngIf="item?.product?.locale" data-name="product-language">{{ item?.product?.locale }}</div>
|
||||
</ng-template>
|
||||
|
||||
<div class="row">
|
||||
<div data-name="product-ean">{{ item.product?.ean }}</div>
|
||||
<div class="right">
|
||||
<div class="availability-icons">
|
||||
<div class="fetching xsmall" *ngIf="store.fetchingTakeAwayAvailability$ | async; else showAvailabilityTakeAwayIcon"></div>
|
||||
<ng-template #showAvailabilityTakeAwayIcon>
|
||||
<ui-icon *ngIf="store.isTakeAwayAvailabilityAvailable$ | async" icon="shopping_bag" size="18px"> </ui-icon>
|
||||
</ng-template>
|
||||
|
||||
<div class="fetching xsmall" *ngIf="store.fetchingPickUpAvailability$ | async; else showAvailabilityPickUpIcon"></div>
|
||||
<ng-template #showAvailabilityPickUpIcon>
|
||||
<ui-icon
|
||||
[uiOverlayTrigger]="orderDeadlineTooltip"
|
||||
*ngIf="store.isPickUpAvailabilityAvailable$ | async"
|
||||
icon="box_out"
|
||||
size="18px"
|
||||
></ui-icon>
|
||||
|
||||
<ui-tooltip
|
||||
[warning]="true"
|
||||
yPosition="above"
|
||||
xPosition="after"
|
||||
[yOffset]="-11"
|
||||
[xOffset]="8"
|
||||
#orderDeadlineTooltip
|
||||
[closeable]="true"
|
||||
>
|
||||
<b>{{ (store.pickUpAvailability$ | async)?.orderDeadline | orderDeadline }}</b>
|
||||
</ui-tooltip>
|
||||
</ng-template>
|
||||
|
||||
<div class="fetching xsmall" *ngIf="store.fetchingDeliveryAvailability$ | async; else showAvailabilityDeliveryIcon"></div>
|
||||
<ng-template #showAvailabilityDeliveryIcon>
|
||||
<ui-icon *ngIf="showDeliveryTruck$ | async" class="truck" icon="truck" size="30px"></ui-icon>
|
||||
</ng-template>
|
||||
|
||||
<div
|
||||
class="fetching xsmall"
|
||||
*ngIf="store.fetchingDeliveryB2BAvailability$ | async; else showAvailabilityDeliveryB2BIcon"
|
||||
></div>
|
||||
<ng-template #showAvailabilityDeliveryB2BIcon>
|
||||
<ui-icon *ngIf="showDeliveryB2BTruck$ | async" class="truck_b2b" icon="truck_b2b" size="40px"> </ui-icon>
|
||||
</ng-template>
|
||||
|
||||
<span *ngIf="store.isDownload$ | async" class="download-icon">
|
||||
<ui-icon icon="download" size="18px"></ui-icon>
|
||||
<span class="label">Download</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
|
||||
*ngIf="store.fetchingDeliveryAvailability$ | async; else showAvailabilityDeliveryIcon"
|
||||
></div>
|
||||
<ng-template #showAvailabilityDeliveryIcon>
|
||||
<div
|
||||
*ngIf="showDeliveryTruck$ | async"
|
||||
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
|
||||
>
|
||||
<ui-icon class="-mb-px-5 -mt-px-5 mx-1" icon="truck" size="30px"></ui-icon>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div
|
||||
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
|
||||
*ngIf="store.fetchingDeliveryB2BAvailability$ | async; else showAvailabilityDeliveryB2BIcon"
|
||||
></div>
|
||||
<ng-template #showAvailabilityDeliveryB2BIcon>
|
||||
<div
|
||||
*ngIf="showDeliveryB2BTruck$ | async"
|
||||
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
|
||||
>
|
||||
<ui-icon class="-mb-px-10 -mt-px-10 mx-1" icon="truck_b2b" size="30px"> </ui-icon>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<span *ngIf="store.isDownload$ | async" class="flex flex-row items-center">
|
||||
<div class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3">
|
||||
<ui-icon class="mx-1" icon="download" size="18px"></ui-icon>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="page-article-details__shelf-ssc">
|
||||
<div class="page-article-details__ssc flex justify-end my-2 font-bold text-lg">
|
||||
<div class="w-52 h-px-20 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]" *ngIf="fetchingAvailabilities$ | async"></div>
|
||||
<ng-container *ngIf="!(fetchingAvailabilities$ | async)">
|
||||
<div class="text-right" *ngIf="store.sscText$ | async; let sscText">
|
||||
{{ sscText }}
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="shelfinfo right" *ngIf="store.isDownload$ | async">
|
||||
<div class="page-article-details__shelfinfo text-right" *ngIf="store.isDownload$ | async">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label;
|
||||
@@ -162,24 +257,7 @@
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="specs">
|
||||
<ng-container *ngIf="item?.specs?.length > 0">
|
||||
{{ (item?.specs)[0]?.value }}
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="right ssc">
|
||||
<div class="fetching" *ngIf="fetchingAvailabilities$ | async"></div>
|
||||
<ng-container *ngIf="!(fetchingAvailabilities$ | async)">
|
||||
<div *ngIf="store.sscText$ | async; let sscText">
|
||||
{{ sscText }}
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shelfinfo right" *ngIf="!(store.isDownload$ | async)">
|
||||
<div class="page-article-details__shelfinfo text-right" *ngIf="!(store.isDownload$ | async)">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label;
|
||||
@@ -203,109 +281,105 @@
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bookmark">
|
||||
<div *ngIf="showArchivBadge$ | async" class="archiv-badge">
|
||||
<button [uiOverlayTrigger]="archivTooltip" class="bookmark-badge">
|
||||
<img src="/assets/images/bookmark_benachrichtigung_archiv.svg" alt="Archiv Badge" />
|
||||
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #archivTooltip [closeable]="true">
|
||||
<ng-container *ngIf="isAvailable$ | async; else notAvailable">
|
||||
Archivtitel. Wird nicht mehr gedruckt. Artikel ist bestellbar, weil lieferbar.
|
||||
</ng-container>
|
||||
<ng-template #notAvailable>
|
||||
Archivtitel. Wird nicht mehr gedruckt. Nicht bestellbar.
|
||||
</ng-template>
|
||||
</ui-tooltip>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="showSubscriptionBadge$ | async">
|
||||
<button [uiOverlayTrigger]="subscribtionTooltip" class="bookmark-badge">
|
||||
<img src="/assets/images/bookmark_subscription.svg" alt="Fortsetzungsartikel Badge" />
|
||||
</button>
|
||||
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #subscribtionTooltip [closeable]="true"
|
||||
>Artikel ist ein Fortsetzungsartikel,<br />
|
||||
Artikel muss über eine Aboabteilung<br />
|
||||
bestellt werden.
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div *ngIf="showPromotionBadge$ | async" class="promotion-badge">
|
||||
<button [uiOverlayTrigger]="promotionTooltip" class="bookmark-badge">
|
||||
<ui-icon-badge icon="gift" alt="Prämienkatalog Badge"></ui-icon-badge>
|
||||
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #promotionTooltip [closeable]="true">
|
||||
Dieser Artikel befindet sich im Prämienkatalog.
|
||||
</ui-tooltip>
|
||||
</button>
|
||||
<div class="page-article-details__product-formats-container mt-3" *ngIf="item.family?.length > 0">
|
||||
<hr class="bg-[#E6EFF9] border-t-2" />
|
||||
<div class="pt-3">
|
||||
<div class="page-article-details__product-formats">
|
||||
<span class="mr-2">Auch verfügbar als</span>
|
||||
|
||||
<ui-slider [scrollDistance]="250">
|
||||
<a
|
||||
class="mr-4 text-[#0556B4] font-bold no-underline px-2"
|
||||
*ngFor="let format of item.family"
|
||||
[routerLink]="getDetailsPath(format.product.ean)"
|
||||
[queryParamsHandling]="!(isTablet$ | async) ? 'preserve' : ''"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<img
|
||||
class="mr-2"
|
||||
*ngIf="!!format.product?.format"
|
||||
[src]="'/assets/images/OF_Icon_' + format.product?.format + '.svg'"
|
||||
alt="format icon"
|
||||
/>
|
||||
{{ format.product?.formatDetail }}
|
||||
<span class="ml-1">{{ format.catalogAvailability?.price?.value?.value | currency: '€' }}</span>
|
||||
</span>
|
||||
</a>
|
||||
</ui-slider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-actions">
|
||||
<button *ngIf="!(store.isDownload$ | async)" class="cta-availabilities" (click)="showAvailabilities()">
|
||||
Vorrätig in anderer Filiale?
|
||||
</button>
|
||||
<hr class="bg-[#E6EFF9] border-t-2 my-3" />
|
||||
|
||||
<div #description class="page-article-details__product-description flex flex-col flex-grow" *ngIf="item.texts?.length > 0">
|
||||
<page-article-details-text [text]="item.texts[0]"> </page-article-details-text>
|
||||
|
||||
<button
|
||||
class="cta-continue"
|
||||
(click)="showPurchasingModal()"
|
||||
[disabled]="
|
||||
!(isAvailable$ | async) || (fetchingAvailabilities$ | async) || (item?.features && (item?.features)[0]?.key === 'PFO')
|
||||
"
|
||||
class="font-bold flex flex-row text-[#0556B4] items-center mt-2"
|
||||
*ngIf="!showMore && item?.texts?.length > 1"
|
||||
(click)="showMore = !showMore"
|
||||
>
|
||||
In den Warenkorb
|
||||
Mehr <ui-icon class="ml-2" size="15px" icon="arrow"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<ng-container *ngIf="item.family?.length > 0">
|
||||
<div class="product-formats">
|
||||
<span class="label">Auch verfügbar als</span>
|
||||
|
||||
<ui-slider [scrollDistance]="250">
|
||||
<a
|
||||
class="product-family"
|
||||
*ngFor="let format of item.family"
|
||||
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'details', 'ean', format.product.ean]"
|
||||
>
|
||||
<span class="format-detail">
|
||||
<img
|
||||
*ngIf="!!format.product?.format"
|
||||
[src]="'/assets/images/OF_Icon_' + format.product?.format + '.svg'"
|
||||
alt="format icon"
|
||||
/>
|
||||
{{ format.product?.formatDetail }}
|
||||
<span class="price">{{ format.catalogAvailability?.price?.value?.value | currency: '€' }}</span>
|
||||
</span>
|
||||
</a>
|
||||
</ui-slider>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
</ng-container>
|
||||
|
||||
<div class="product-description" *ngIf="item.texts?.length > 0">
|
||||
<div class="info">
|
||||
{{ item.texts[0].value }}
|
||||
</div>
|
||||
|
||||
<div class="product-text">
|
||||
<div *ngIf="showMore" class="page-article-details__product-description-text flex flex-col whitespace-pre-line break-words">
|
||||
<span *ngFor="let text of item.texts | slice: 1">
|
||||
<h3 class="header">{{ text.label }}</h3>
|
||||
<h3 class="my-4 text-p2 font-bold">{{ text.label }}</h3>
|
||||
{{ text.value }}
|
||||
</span>
|
||||
<button class="scroll-top-cta" (click)="scrollTop()">
|
||||
<ui-icon class="arrow" icon="arrow" size="20px"></ui-icon>
|
||||
|
||||
<button class="font-bold flex flex-row text-[#0556B4] items-center mt-2" (click)="showMore = !showMore">
|
||||
<ui-icon class="transform ml-0 mr-2 rotate-180" size="15px" icon="arrow"></ui-icon> Weniger
|
||||
</button>
|
||||
|
||||
<button class="page-article-details__scroll-top-cta" (click)="scrollTop(description)">
|
||||
<ui-icon class="text-[#0556B4]" icon="arrow" size="20px"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="min-h-[6.25rem]"></div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!showRecommendations">
|
||||
<div
|
||||
*ngIf="store.item$ | async; let item"
|
||||
class="page-article-details__actions w-full sticky text-center left-0 -mb-4 bottom-10 z-fixed"
|
||||
>
|
||||
<button
|
||||
*ngIf="!(store.isDownload$ | async)"
|
||||
class="text-brand border-2 border-brand bg-white font-bold text-lg px-[1.375rem] py-4 rounded-full mr-px-30"
|
||||
(click)="showAvailabilities()"
|
||||
>
|
||||
Bestände in anderen Filialen
|
||||
</button>
|
||||
<button
|
||||
class="text-white bg-brand border-brand font-bold text-lg px-[1.375rem] py-4 rounded-full border-none no-underline"
|
||||
(click)="showPurchasingModal()"
|
||||
[disabled]="
|
||||
!(isAvailable$ | async) || (fetchingAvailabilities$ | async) || (item?.features && (item?.features)[0]?.key === 'PFO')
|
||||
"
|
||||
>
|
||||
In den Warenkorb
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="page-article-details__product-recommendations sticky bottom-0 -mx-5">
|
||||
<button
|
||||
*ngIf="store.item$ | async; let item"
|
||||
class="shadow-[#dce2e9_0px_-2px_18px_0px] sticky bottom-4 border-none outline-none left-0 right-0 flex items-center px-5 h-14 min-h-[3.5rem] bg-white w-full"
|
||||
(click)="showRecommendations = true"
|
||||
>
|
||||
<span class="uppercase text-[#0556B4] font-bold text-p3">Empfehlungen</span>
|
||||
<img class="absolute right-5 bottom-3 h-12" src="assets/images/recommendation_tag.png" alt="recommendation icon" />
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<button *ngIf="store.item$ | async; let item" class="product-recommendations" (click)="showRecommendations = true">
|
||||
<span class="label">Empfehlungen</span>
|
||||
<img src="assets/images/recommendation_tag.png" alt="recommendation icon" />
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<div class="recommendations-overlay" @slideYAnimation *ngIf="showRecommendations">
|
||||
<button class="product-button" (click)="showRecommendations = false">{{ (store.item$ | async)?.product?.name }}</button>
|
||||
<div class="page-article-details__recommendations-overlay absolute rounded-t" @slideYAnimation *ngIf="showRecommendations">
|
||||
<page-article-recommendations (close)="showRecommendations = false"></page-article-recommendations>
|
||||
</div>
|
||||
|
||||
@@ -1,269 +1,103 @@
|
||||
:host {
|
||||
@apply flex flex-col;
|
||||
@apply box-border block h-split-screen-tablet desktop-small:h-split-screen-desktop;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
@apply flex flex-col bg-white w-full rounded-card shadow-card;
|
||||
.page-article-details__container {
|
||||
@apply h-full w-full overflow-y-scroll overflow-hidden bg-white rounded shadow-card flex flex-col;
|
||||
}
|
||||
|
||||
.product-details {
|
||||
@apply flex flex-row p-5;
|
||||
.page-article-details__product-details {
|
||||
@apply grid gap-x-5;
|
||||
grid-template-columns: max-content auto;
|
||||
grid-template-rows: 2.1875rem repeat(11, minmax(auto, max-content));
|
||||
grid-template-areas:
|
||||
'. . . bookmark'
|
||||
'image contributors contributors contributors'
|
||||
'image title title print'
|
||||
'image title title .'
|
||||
'image misc price price'
|
||||
'image misc price price'
|
||||
'image origin origin stock'
|
||||
'image origin origin stock'
|
||||
'image specs availabilities availabilities'
|
||||
'image specs ssc ssc'
|
||||
'image . ssc ssc'
|
||||
'image . ssc ssc';
|
||||
}
|
||||
|
||||
.bookmark {
|
||||
@apply absolute flex;
|
||||
top: 52px;
|
||||
right: 25px;
|
||||
z-index: 100;
|
||||
}
|
||||
.page-article-details__product-bookmark {
|
||||
grid-area: bookmark;
|
||||
}
|
||||
|
||||
.bookmark-badge {
|
||||
@apply p-0 m-0 outline-none border-none bg-transparent relative;
|
||||
}
|
||||
.page-article-details__product-image-recessions {
|
||||
grid-area: image;
|
||||
}
|
||||
|
||||
.promotion-badge {
|
||||
margin-top: -1px;
|
||||
}
|
||||
.page-article-details__product-contributors {
|
||||
grid-area: contributors;
|
||||
}
|
||||
|
||||
.bookmark-badge-gap {
|
||||
@apply mt-px-35;
|
||||
}
|
||||
.page-article-details__product-print {
|
||||
grid-area: print;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
@apply flex flex-col items-center justify-start mr-5;
|
||||
.page-article-details__product-title {
|
||||
grid-area: title;
|
||||
}
|
||||
|
||||
.recessions {
|
||||
@apply flex flex-col items-center mt-4 bg-transparent border-none outline-none;
|
||||
.page-article-details__product-misc {
|
||||
grid-area: misc;
|
||||
}
|
||||
|
||||
.cta-recessions {
|
||||
@apply text-regular text-dark-cerulean font-bold mt-2;
|
||||
}
|
||||
}
|
||||
.page-article-details__product-price-info {
|
||||
grid-area: price;
|
||||
}
|
||||
|
||||
.image-button {
|
||||
@apply border-none outline-none bg-transparent relative;
|
||||
.page-article-details__product-origin-infos {
|
||||
grid-area: origin;
|
||||
}
|
||||
|
||||
ui-icon {
|
||||
@apply absolute text-dark-cerulean inline-block;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
}
|
||||
}
|
||||
.page-article-details__product-stock {
|
||||
grid-area: stock;
|
||||
}
|
||||
|
||||
img {
|
||||
@apply rounded-xl shadow-card;
|
||||
box-shadow: 0 0 18px 0 #b8b3b7;
|
||||
max-height: 315px;
|
||||
max-width: 195px;
|
||||
}
|
||||
}
|
||||
.page-article-details__product-ean-specs {
|
||||
grid-area: specs;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
@apply w-full;
|
||||
.page-article-details__product-availabilities {
|
||||
grid-area: availabilities;
|
||||
}
|
||||
|
||||
.title {
|
||||
@apply text-3xl font-bold mb-6;
|
||||
}
|
||||
.page-article-details__shelf-ssc {
|
||||
grid-area: ssc;
|
||||
}
|
||||
|
||||
.format,
|
||||
.ssc,
|
||||
.quantity {
|
||||
@apply font-bold text-lg;
|
||||
}
|
||||
.page-article-details__product-description-text {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.stock {
|
||||
min-height: 44px;
|
||||
}
|
||||
.page-article-details__product-formats {
|
||||
@apply grid whitespace-nowrap items-center max-w-full;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.quantity {
|
||||
@apply flex justify-end mt-4;
|
||||
.page-article-details__scroll-top-cta {
|
||||
@apply flex items-center justify-center self-end border-none outline-none bg-white relative rounded p-0 mt-8 mr-4;
|
||||
box-shadow: 0px 0px 20px 0px rgba(89, 100, 112, 0.5);
|
||||
transform: rotate(-90deg);
|
||||
border-radius: 100%;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
}
|
||||
|
||||
ui-icon {
|
||||
@apply mr-1 text-ucla-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.format {
|
||||
@apply flex items-center;
|
||||
|
||||
.format-icon {
|
||||
@apply flex mr-2;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.ssc {
|
||||
@apply flex justify-end my-2;
|
||||
}
|
||||
|
||||
.price {
|
||||
@apply font-bold text-xl;
|
||||
}
|
||||
|
||||
.shelfinfo {
|
||||
@apply text-ucla-blue;
|
||||
}
|
||||
|
||||
.fetching {
|
||||
@apply w-52 h-px-20;
|
||||
background-color: #e6eff9;
|
||||
animation: load 0.75s linear infinite;
|
||||
}
|
||||
|
||||
.xsmall {
|
||||
@apply w-6;
|
||||
}
|
||||
|
||||
.small {
|
||||
@apply w-16;
|
||||
}
|
||||
|
||||
.medium {
|
||||
@apply w-40;
|
||||
}
|
||||
|
||||
.availability-icons {
|
||||
@apply flex flex-row items-center justify-end text-dark-cerulean mt-4;
|
||||
|
||||
ui-icon {
|
||||
@apply mx-1;
|
||||
}
|
||||
|
||||
.truck {
|
||||
@apply -mb-px-5 -mt-px-5;
|
||||
}
|
||||
|
||||
.truck_b2b {
|
||||
@apply -mb-px-10 -mt-px-10;
|
||||
}
|
||||
|
||||
.label {
|
||||
@apply font-bold;
|
||||
}
|
||||
|
||||
.download-icon {
|
||||
@apply flex flex-row items-center;
|
||||
}
|
||||
}
|
||||
|
||||
.cta-print {
|
||||
@apply bg-transparent text-brand font-bold text-xl outline-none border-none p-0;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
@apply grid items-end;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
@apply text-right self-start;
|
||||
}
|
||||
|
||||
hr {
|
||||
@apply bg-glitter h-1;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
@apply flex flex-col flex-grow px-5 py-5;
|
||||
min-height: calc(100vh - 769px);
|
||||
|
||||
.info {
|
||||
@apply whitespace-pre-line;
|
||||
}
|
||||
|
||||
.product-text {
|
||||
@apply flex flex-col whitespace-pre-line mb-px-100 break-words;
|
||||
|
||||
h3 {
|
||||
@apply my-4;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply text-regular font-bold;
|
||||
}
|
||||
|
||||
.scroll-top-cta {
|
||||
@apply flex items-center justify-center self-end border-none outline-none bg-white relative rounded p-0 mt-8 mr-4;
|
||||
box-shadow: 0px 0px 20px 0px rgba(89, 100, 112, 0.5);
|
||||
transform: rotate(-90deg);
|
||||
border-radius: 100%;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
|
||||
.arrow {
|
||||
color: #1f466c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-actions {
|
||||
@apply text-right px-5 py-4;
|
||||
|
||||
.cta-availabilities {
|
||||
@apply text-brand border-none border-brand bg-white font-bold text-lg px-4 py-2 rounded-full;
|
||||
}
|
||||
.cta-continue {
|
||||
@apply text-white bg-brand font-bold text-lg px-4 py-2 rounded-full border-none ml-4 no-underline;
|
||||
|
||||
&:disabled {
|
||||
@apply bg-inactive-branch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-formats {
|
||||
@apply grid whitespace-nowrap items-center px-5 py-4;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: auto 1fr;
|
||||
max-width: 100%;
|
||||
|
||||
.label {
|
||||
@apply mr-2;
|
||||
}
|
||||
|
||||
.product-family {
|
||||
@apply mr-4 text-active-customer font-bold no-underline px-2;
|
||||
|
||||
.format-detail {
|
||||
@apply flex items-center;
|
||||
|
||||
img {
|
||||
@apply mr-2;
|
||||
}
|
||||
}
|
||||
|
||||
.price {
|
||||
@apply ml-1;
|
||||
}
|
||||
}
|
||||
.page-article-details__actions {
|
||||
button:disabled {
|
||||
@apply bg-inactive-branch cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.product-recommendations {
|
||||
@apply sticky bottom-0 border-none outline-none left-0 right-0 flex items-center px-5 h-16 bg-white w-full;
|
||||
box-shadow: #dce2e9 0px -2px 18px 0px;
|
||||
|
||||
.label {
|
||||
@apply uppercase text-active-customer font-bold text-small;
|
||||
}
|
||||
|
||||
img {
|
||||
@apply absolute right-5 bottom-5 h-12;
|
||||
}
|
||||
}
|
||||
|
||||
.recommendations-overlay {
|
||||
@apply absolute w-full top-0 rounded-t-card;
|
||||
top: 56px;
|
||||
|
||||
.product-button {
|
||||
@apply flex flex-row justify-center items-center w-full text-xl bg-white text-ucla-blue font-bold border-none outline-none rounded-t-card;
|
||||
box-shadow: 0 -2px 24px 0 #dce2e9;
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.autor {
|
||||
@apply text-active-customer font-bold no-underline;
|
||||
.tooltip-active {
|
||||
@apply bg-[#596470] text-white;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ElementRef } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { DomainPrinterService } from '@domain/printer';
|
||||
@@ -8,7 +8,7 @@ import { BranchDTO } from '@swagger/checkout';
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { ModalReviewsComponent } from '@modal/reviews';
|
||||
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
|
||||
import { debounceTime, filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
import { debounceTime, filter, first, map, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { ArticleDetailsStore } from './article-details.store';
|
||||
import { ModalImagesComponent } from 'apps/modal/images/src/public-api';
|
||||
import { ProductImageService } from 'apps/cdn/product-image/src/public-api';
|
||||
@@ -19,8 +19,10 @@ import { ItemDTO } from '@swagger/cat';
|
||||
import { DateAdapter } from '@ui/common';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-details',
|
||||
@@ -95,27 +97,78 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
);
|
||||
|
||||
defaultBranch$ = this._availability.getDefaultBranch();
|
||||
|
||||
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.applicationService.getSelectedBranch$(processId))
|
||||
);
|
||||
|
||||
stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranchId$]).pipe(
|
||||
get isTablet$() {
|
||||
return this._environment.matchTablet$;
|
||||
}
|
||||
|
||||
get resultsPath() {
|
||||
return this._navigationService.getArticleSearchResultsPath(this.applicationService.activatedProcessId);
|
||||
}
|
||||
|
||||
showMore: boolean = false;
|
||||
|
||||
@ViewChild('detailsContainer', { read: ElementRef, static: false })
|
||||
detailsContainer: ElementRef;
|
||||
|
||||
get detailsContainerNative(): HTMLElement {
|
||||
return this.detailsContainer?.nativeElement;
|
||||
}
|
||||
|
||||
stockTooltipText$ = combineLatest([this.store.defaultBranch$, this.selectedBranchId$]).pipe(
|
||||
map(([defaultBranch, selectedBranch]) => {
|
||||
if (defaultBranch?.branchType === 4) {
|
||||
if (!selectedBranch) {
|
||||
return 'Wählen Sie eine Filiale aus, um den Bestand zu sehen.';
|
||||
}
|
||||
if (defaultBranch?.branchType !== 4 && selectedBranch && defaultBranch.id !== selectedBranch?.id) {
|
||||
return 'Sie sehen den Bestand einer anderen Filiale.';
|
||||
} else {
|
||||
if (selectedBranch && defaultBranch.id !== selectedBranch?.id) {
|
||||
return 'Sie sehen den Bestand einer anderen Filiale.';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
shareReplay(1)
|
||||
})
|
||||
);
|
||||
|
||||
priceMaintained$ = combineLatest([
|
||||
this.store.item$,
|
||||
this.store.takeAwayAvailability$,
|
||||
this.store.deliveryAvailability$,
|
||||
this.store.deliveryDigAvailability$,
|
||||
this.store.deliveryB2BAvailability$,
|
||||
]).pipe(
|
||||
map(([item, takeAway, delivery, deliveryDig, deliveryB2B]) =>
|
||||
[item, takeAway, delivery, deliveryDig, deliveryB2B].some((i) => i?.priceMaintained)
|
||||
)
|
||||
);
|
||||
|
||||
price$ = combineLatest([
|
||||
this.store.item$,
|
||||
this.store.takeAwayAvailability$,
|
||||
this.store.deliveryAvailability$,
|
||||
this.store.deliveryDigAvailability$,
|
||||
this.store.deliveryB2BAvailability$,
|
||||
]).pipe(
|
||||
map(([item, takeAway, delivery, deliveryDig, deliveryB2B]) => {
|
||||
if (item?.catalogAvailability?.price?.value?.value) {
|
||||
return item?.catalogAvailability?.price;
|
||||
}
|
||||
|
||||
if (takeAway?.price?.value?.value) {
|
||||
return takeAway.price;
|
||||
}
|
||||
|
||||
if (delivery?.price?.value?.value) {
|
||||
return delivery.price;
|
||||
}
|
||||
|
||||
if (deliveryDig?.price?.value?.value) {
|
||||
return deliveryDig.price;
|
||||
}
|
||||
|
||||
if (deliveryB2B?.price?.value?.value) {
|
||||
return deliveryB2B.price;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
constructor(
|
||||
@@ -128,11 +181,13 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
private breadcrumb: BreadcrumbService,
|
||||
private _dateAdapter: DateAdapter,
|
||||
private _datePipe: DatePipe,
|
||||
public elementRef: ElementRef,
|
||||
private _purchaseOptionsModalService: PurchaseOptionsModalService,
|
||||
private _availability: DomainAvailabilityService,
|
||||
private _navigationService: ProductCatalogNavigationService,
|
||||
private _checkoutNavigationService: CheckoutNavigationService,
|
||||
private _environment: EnvironmentService,
|
||||
private _router: Router,
|
||||
private _domainCheckoutService: DomainCheckoutService
|
||||
private _domainCheckoutService: DomainCheckoutService,
|
||||
private _store: Store
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -158,25 +213,42 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
const id$ = this.activatedRoute.params.pipe(
|
||||
tap((_) => (this.showRecommendations = false)),
|
||||
map((params) => Number(params?.id) || undefined),
|
||||
filter((f) => !!f)
|
||||
);
|
||||
|
||||
const ean$ = this.activatedRoute.params.pipe(
|
||||
tap((_) => (this.showRecommendations = false)),
|
||||
map((params) => params?.ean || undefined),
|
||||
filter((f) => !!f)
|
||||
);
|
||||
|
||||
const more$ = this.activatedRoute.params.subscribe(() => (this.showMore = false));
|
||||
|
||||
this.subscriptions.add(processIdSubscription);
|
||||
this.subscriptions.add(more$);
|
||||
this.subscriptions.add(this.store.loadDefaultBranch());
|
||||
this.subscriptions.add(this.store.loadItemById(id$));
|
||||
this.subscriptions.add(this.store.loadItemByEan(ean$));
|
||||
this.subscriptions.add(this.store.item$.pipe(filter((item) => !!item)).subscribe((item) => this.updateBreadcrumb(item)));
|
||||
this.subscriptions.add(
|
||||
this.store.item$
|
||||
.pipe(
|
||||
withLatestFrom(this.isTablet$),
|
||||
filter(([item, isTablet]) => !!item)
|
||||
)
|
||||
.subscribe(([item, isTablet]) => (isTablet ? this.updateBreadcrumb(item) : this.updateBreadcrumbDesktop(item)))
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscriptions.unsubscribe();
|
||||
}
|
||||
|
||||
getDetailsPath(ean?: string) {
|
||||
return this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, ean });
|
||||
}
|
||||
|
||||
async updateBreadcrumb(item: ItemDTO) {
|
||||
const crumbs = await this.breadcrumb
|
||||
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog', 'details', `${item.id}`])
|
||||
@@ -190,13 +262,49 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
this.breadcrumb.addBreadcrumbIfNotExists({
|
||||
key: this.applicationService.activatedProcessId,
|
||||
name: item.product?.name,
|
||||
path: `/kunde/${this.applicationService.activatedProcessId}/product/details/${item.id}`,
|
||||
path: this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: item.id }),
|
||||
params: this.activatedRoute.snapshot.queryParams,
|
||||
tags: ['catalog', 'details', `${item.id}`],
|
||||
section: 'customer',
|
||||
});
|
||||
}
|
||||
|
||||
async showTooltip() {
|
||||
const text = await this.stockTooltipText$.pipe(first()).toPromise();
|
||||
if (!text) {
|
||||
// Show Tooltip attached to branch selector dropdown
|
||||
this._store.dispatch({ type: 'OPEN_TOOLTIP_NO_BRANCH_SELECTED' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateBreadcrumbDesktop(item: ItemDTO) {
|
||||
const crumbs = await this.breadcrumb
|
||||
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog', 'details'])
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
if (crumbs.length === 0) {
|
||||
this.breadcrumb.addBreadcrumbIfNotExists({
|
||||
key: this.applicationService.activatedProcessId,
|
||||
name: item.product?.name,
|
||||
path: this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: item.id }),
|
||||
params: this.activatedRoute.snapshot.queryParams,
|
||||
tags: ['catalog', 'details', `${item.id}`],
|
||||
section: 'customer',
|
||||
});
|
||||
} else {
|
||||
const crumb = crumbs.find((_) => true);
|
||||
this.breadcrumb.patchBreadcrumb(crumb.id, {
|
||||
key: this.applicationService.activatedProcessId,
|
||||
name: item.product?.name,
|
||||
path: this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: item.id }),
|
||||
params: this.activatedRoute.snapshot.queryParams,
|
||||
tags: ['catalog', 'details', `${item.id}`],
|
||||
section: 'customer',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async print() {
|
||||
const item = await this.store.item$.pipe(first()).toPromise();
|
||||
this.uiModal.open({
|
||||
@@ -287,9 +395,9 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
if (customer) {
|
||||
this.navigateToShoppingCart();
|
||||
await this.navigateToShoppingCart();
|
||||
} else {
|
||||
this.navigateToCustomerSearch();
|
||||
await this.navigateToCustomerSearch();
|
||||
}
|
||||
} else if (result?.data === 'continue-shopping') {
|
||||
this.navigateToResultList();
|
||||
@@ -297,8 +405,8 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
navigateToShoppingCart() {
|
||||
this._router.navigate([`/kunde/${this.applicationService.activatedProcessId}/cart/review`]);
|
||||
async navigateToShoppingCart() {
|
||||
await this._checkoutNavigationService.navigateToCheckoutReview({ processId: this.applicationService.activatedProcessId });
|
||||
}
|
||||
|
||||
async navigateToCustomerSearch() {
|
||||
@@ -320,24 +428,23 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async navigateToResultList() {
|
||||
const processId = this.applicationService.activatedProcessId;
|
||||
let crumbs = await this.breadcrumb
|
||||
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog'])
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
crumbs = crumbs.filter((crumb) => !crumb.tags?.includes('details'));
|
||||
|
||||
const crumb = crumbs[crumbs.length - 1];
|
||||
if (crumb) {
|
||||
this._router.navigate([crumb.path], { queryParams: crumb.params });
|
||||
if (!!crumb) {
|
||||
this._router.navigate(this._navigationService.getArticleSearchResultsPath(processId), { queryParams: crumb.params });
|
||||
} else {
|
||||
this._router.navigate([`/kunde/${this.applicationService.activatedProcessId}/product`]);
|
||||
this._navigationService.navigateToProductSearch({ processId });
|
||||
}
|
||||
}
|
||||
|
||||
scrollTop() {
|
||||
const element = this.elementRef.nativeElement.closest('.main-wrapper');
|
||||
element?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
scrollTop(div: HTMLDivElement) {
|
||||
this.detailsContainerNative?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
loadImage() {
|
||||
|
||||
@@ -11,6 +11,8 @@ import { PipesModule } from '../shared/pipes/pipes.module';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
import { ArticleDetailsTextComponent } from './article-details-text/article-details-text.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -22,8 +24,10 @@ import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
|
||||
UiSliderModule,
|
||||
UiCommonModule,
|
||||
UiTooltipModule,
|
||||
IconModule,
|
||||
PipesModule,
|
||||
OrderDeadlinePipeModule,
|
||||
ArticleDetailsTextComponent,
|
||||
],
|
||||
exports: [ArticleDetailsComponent, ArticleRecommendationsComponent],
|
||||
declarations: [ArticleDetailsComponent, ArticleRecommendationsComponent],
|
||||
|
||||
@@ -61,8 +61,12 @@ export class ArticleDetailsStore extends ComponentStore<ArticleDetailsState> {
|
||||
return this.get((s) => s.branch);
|
||||
}
|
||||
|
||||
get defaultBranch$() {
|
||||
return this.domainAvailabilityService.getDefaultBranch();
|
||||
}
|
||||
|
||||
readonly branch$ = this.select((s) => s.branch).pipe(
|
||||
withLatestFrom(this.domainAvailabilityService.getDefaultBranch()),
|
||||
withLatestFrom(this.defaultBranch$),
|
||||
map(([selectedBranch, defaultBranch]) => selectedBranch ?? defaultBranch)
|
||||
);
|
||||
|
||||
@@ -267,9 +271,8 @@ export class ArticleDetailsStore extends ComponentStore<ArticleDetailsState> {
|
||||
this.deliveryB2BAvailability$,
|
||||
this.downloadAvailability$,
|
||||
]).pipe(
|
||||
map(([item, isDownload, pickupAvailability, deliveryDigAvailability, deliveryB2BAvailability, downloadAvailability]) => {
|
||||
// const availability = isDownload ? downloadAvailability : pickupAvailability || deliveryDigAvailability || deliveryB2BAvailability;
|
||||
|
||||
withLatestFrom(this.domainAvailabilityService.sscs$),
|
||||
map(([[item, isDownload, pickupAvailability, deliveryDigAvailability, deliveryB2BAvailability, downloadAvailability], sscs]) => {
|
||||
let availability: AvailabilityDTO;
|
||||
|
||||
if (isDownload) {
|
||||
@@ -284,15 +287,30 @@ export class ArticleDetailsStore extends ComponentStore<ArticleDetailsState> {
|
||||
}
|
||||
}
|
||||
|
||||
let ssc = '';
|
||||
let sscText = 'Keine Lieferanten vorhanden';
|
||||
|
||||
if (item?.catalogAvailability?.supplier === 'S' && !isDownload) {
|
||||
return [item?.catalogAvailability?.ssc, item?.catalogAvailability?.sscText].filter((f) => !!f).join(' - ');
|
||||
ssc = item?.catalogAvailability?.ssc;
|
||||
sscText = item?.catalogAvailability?.sscText;
|
||||
|
||||
return [ssc, sscText].filter((f) => !!f).join(' - ');
|
||||
}
|
||||
|
||||
if (availability?.ssc || availability?.sscText) {
|
||||
return [availability?.ssc, availability?.sscText].filter((f) => !!f).join(' - ');
|
||||
ssc = availability?.ssc;
|
||||
sscText = availability?.sscText;
|
||||
|
||||
const sscExists = !!sscs?.find((ssc) => !!item?.id && ssc?.itemId === item.id);
|
||||
const sscEqualsCatalogSsc = ssc === item.catalogAvailability.ssc && sscText === item.catalogAvailability.sscText;
|
||||
|
||||
// To keep result list in sync with details page
|
||||
if (!sscExists && !sscEqualsCatalogSsc) {
|
||||
this.domainAvailabilityService.sscs$.next([...sscs, { itemId: item?.id, ssc, sscText }]);
|
||||
}
|
||||
}
|
||||
|
||||
return 'Keine Lieferanten vorhanden';
|
||||
return [ssc, sscText].filter((f) => !!f).join(' - ');
|
||||
})
|
||||
);
|
||||
|
||||
@@ -349,6 +367,24 @@ export class ArticleDetailsStore extends ComponentStore<ArticleDetailsState> {
|
||||
)
|
||||
);
|
||||
|
||||
readonly loadDefaultBranch = this.effect(($) =>
|
||||
$.pipe(
|
||||
switchMap(() => this.domainAvailabilityService.getDefaultBranch()),
|
||||
withLatestFrom(this.branch$),
|
||||
tapResponse<[BranchDTO, BranchDTO]>(
|
||||
([defaultBranch, selectedBranch]) => this.patchState({ branch: selectedBranch ?? defaultBranch }),
|
||||
async (err) => {
|
||||
this.patchState({ branch: undefined });
|
||||
const errorModalRef = this._modal.open({
|
||||
content: UiErrorModalComponent,
|
||||
title: 'Fehler beim Laden der Filiale',
|
||||
});
|
||||
await errorModalRef.afterClosed$.toPromise();
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
setProcessId = this.updater((s, processId: number) => {
|
||||
return {
|
||||
...s,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user