mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
143 Commits
ipad-tests
...
fix/4082-S
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aa9ffec5d | ||
|
|
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 | ||
|
|
4dc98f7980 | ||
|
|
c89ee18db3 | ||
|
|
c4aa7999a6 | ||
|
|
696015b6a4 | ||
|
|
f2f70e1d83 | ||
|
|
4d1dbaa2f3 | ||
|
|
595bb27d99 | ||
|
|
7b72532c9e | ||
|
|
6b756fe893 | ||
|
|
54c7f51766 | ||
|
|
94b787655e | ||
|
|
259d0f1648 | ||
|
|
872d3ff383 | ||
|
|
cf1c4d37b9 | ||
|
|
ada16bac6c | ||
|
|
d68055f8a9 | ||
|
|
8d98dcf7e7 | ||
|
|
c828a69f66 | ||
|
|
eefb6062c7 | ||
|
|
470a451168 | ||
|
|
e3b018c5f7 | ||
|
|
bd695e21d4 | ||
|
|
6311ebe467 | ||
|
|
f8a5ceed97 | ||
|
|
0420bda5da | ||
|
|
6dc532f40e | ||
|
|
0560d01f30 | ||
|
|
bc67ec3287 | ||
|
|
bed35a2377 | ||
|
|
aaf156cee3 | ||
|
|
e8020ffde6 | ||
|
|
8d6bd80902 | ||
|
|
8aa870dddd | ||
|
|
26a3c76d5f | ||
|
|
18f738f2c3 | ||
|
|
bba50ccbcc | ||
|
|
3eea3b913d | ||
|
|
2dbeec831e | ||
|
|
88a06628e3 | ||
|
|
37ceb30ffb | ||
|
|
ebd0515e96 | ||
|
|
ece5d0fa0d | ||
|
|
6be214c6cd | ||
|
|
c470453ea4 | ||
|
|
bbe9326954 | ||
|
|
27961bb4e5 | ||
|
|
4952c090ef | ||
|
|
74e4016625 | ||
|
|
c389008811 | ||
|
|
e6dcf22012 | ||
|
|
d067f925b9 | ||
|
|
8b5609f765 | ||
|
|
dd04a1f2af | ||
|
|
9caabb6cc0 | ||
|
|
33b28d5f41 | ||
|
|
475f9b5e34 | ||
|
|
60f1348ea5 | ||
|
|
533b6e1fcf | ||
|
|
8961730b74 | ||
|
|
5d580714c8 | ||
|
|
daf1ead75b | ||
|
|
aef2654a39 | ||
|
|
8243cd3528 | ||
|
|
447456d7a6 | ||
|
|
241a34d7a8 | ||
|
|
4e67b2e8b9 | ||
|
|
8bc2ea8373 | ||
|
|
00a6a113c8 | ||
|
|
dc04619128 | ||
|
|
e066da3762 | ||
|
|
ad96278956 | ||
|
|
83406277ad | ||
|
|
9e89348381 | ||
|
|
0fb7419598 | ||
|
|
1790298cb4 | ||
|
|
ffe8e39c85 | ||
|
|
67e0f4bd46 | ||
|
|
e7b3a58da3 | ||
|
|
598f9f3777 | ||
|
|
0a7dca2e12 | ||
|
|
4b342778df | ||
|
|
10e8fd904a | ||
|
|
1de342fd3b | ||
|
|
f4c1c3dd7f | ||
|
|
80bfc59356 | ||
|
|
3796f3ed5f | ||
|
|
f2e03d22d8 | ||
|
|
ef967b66e8 | ||
|
|
58ea70cc6c | ||
|
|
e4823950df | ||
|
|
8f9923ba5d | ||
|
|
72bbd2c36e | ||
|
|
9bdb902a56 | ||
|
|
bbc2e55ae3 | ||
|
|
6995bdb527 | ||
|
|
772aed597b | ||
|
|
9e18825c27 | ||
|
|
0882bc2ca7 | ||
|
|
d9a2601f75 | ||
|
|
37a04cadf8 | ||
|
|
b01ce5b3b6 | ||
|
|
c7e6f00ddb | ||
|
|
85831ffe5d |
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"
|
||||
]
|
||||
}
|
||||
200
angular.json
200
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,42 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
@@ -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.changed - a.changed).find((f) => predicate(f))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface Breadcrumb {
|
||||
/**
|
||||
* Url
|
||||
*/
|
||||
path: string;
|
||||
path: string | any[];
|
||||
|
||||
/**
|
||||
* Query Parameter
|
||||
|
||||
@@ -1,17 +1,52 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { NativeContainerService } from 'native-container';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
|
||||
const MATCH_TABLET = '(max-width: 1024px)';
|
||||
|
||||
const MATCH_DESKTOP_SMALL = '(min-width: 1025px) and (max-width: 1439px)';
|
||||
|
||||
const MATCH_DESKTOP = '(min-width: 1440px)';
|
||||
|
||||
@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);
|
||||
|
||||
matchDesktopSmall(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_DESKTOP_SMALL);
|
||||
}
|
||||
|
||||
matchDesktopSmall$ = this._breakpointObserver.observe(MATCH_DESKTOP_SMALL);
|
||||
|
||||
matchDesktop(): boolean {
|
||||
return this._breakpointObserver.isMatched(MATCH_DESKTOP);
|
||||
}
|
||||
|
||||
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP);
|
||||
|
||||
/**
|
||||
* @deprecated Use `matchDesktopSmall` or 'matchDesktop' instead.
|
||||
*/
|
||||
isDesktop(): boolean {
|
||||
return !this.isTablet();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `matchTablet` instead.
|
||||
*/
|
||||
isTablet(): boolean {
|
||||
return this.isNative() || this.isSafari();
|
||||
}
|
||||
@@ -21,6 +56,6 @@ export class EnvironmentService {
|
||||
}
|
||||
|
||||
isSafari(): boolean {
|
||||
return (this._platform.ANDROID || this._platform.IOS) && this._platform.SAFARI;
|
||||
return this._platform.IOS && this._platform.SAFARI;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<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>
|
||||
<h1 class="text-xl 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>
|
||||
<p class="block text-p2 overflow-y-hidden pb-3 text-center overflow-x-hidden">{{ data.text }}</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,11 @@ export class DomainAvailabilityService {
|
||||
private _branchService: StoreCheckoutBranchService
|
||||
) {}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
memorizedAvailabilityShippingAvailability(request: Array<AvailabilityRequestDTO>) {
|
||||
return this._availabilityService.AvailabilityShippingAvailability(request).pipe(shareReplay(1));
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getSuppliers(): Observable<SupplierDTO[]> {
|
||||
return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe(
|
||||
@@ -160,7 +165,7 @@ export class DomainAvailabilityService {
|
||||
),
|
||||
map(([response, supplier, defaultBranch]) => {
|
||||
const price = item?.price;
|
||||
return this._mapToTakeAwayAvailability({ response, supplier, branchId: branch.id ?? defaultBranch.id, quantity, price });
|
||||
return this._mapToTakeAwayAvailability({ response, supplier, branchId: branch?.id ?? defaultBranch?.id, quantity, price });
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
@@ -249,57 +254,53 @@ export class DomainAvailabilityService {
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService
|
||||
.AvailabilityShippingAvailability([
|
||||
{
|
||||
ean: item?.ean,
|
||||
itemId: item?.itemId ? String(item?.itemId) : null,
|
||||
price: item?.price,
|
||||
qty: quantity,
|
||||
},
|
||||
])
|
||||
.pipe(
|
||||
timeout(5000),
|
||||
map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)),
|
||||
shareReplay(1)
|
||||
);
|
||||
return this.memorizedAvailabilityShippingAvailability([
|
||||
{
|
||||
ean: item?.ean,
|
||||
itemId: item?.itemId ? String(item?.itemId) : null,
|
||||
price: item?.price,
|
||||
qty: quantity,
|
||||
},
|
||||
]).pipe(
|
||||
timeout(5000),
|
||||
map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDigDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService
|
||||
.AvailabilityShippingAvailability([
|
||||
{
|
||||
qty: quantity,
|
||||
ean: item?.ean,
|
||||
itemId: item?.itemId ? String(item?.itemId) : null,
|
||||
price: item?.price,
|
||||
},
|
||||
])
|
||||
.pipe(
|
||||
timeout(5000),
|
||||
map((r) => {
|
||||
const availabilities = r.result;
|
||||
const preferred = availabilities?.find((f) => f.preferred === 1);
|
||||
return this.memorizedAvailabilityShippingAvailability([
|
||||
{
|
||||
qty: quantity,
|
||||
ean: item?.ean,
|
||||
itemId: item?.itemId ? String(item?.itemId) : null,
|
||||
price: item?.price,
|
||||
},
|
||||
]).pipe(
|
||||
timeout(5000),
|
||||
map((r) => {
|
||||
const availabilities = r.result;
|
||||
const preferred = availabilities?.find((f) => f.preferred === 1);
|
||||
|
||||
const availability: AvailabilityDTO = {
|
||||
availabilityType: preferred?.status,
|
||||
ssc: preferred?.ssc,
|
||||
sscText: preferred?.sscText,
|
||||
supplier: { id: preferred?.supplierId },
|
||||
isPrebooked: preferred?.isPrebooked,
|
||||
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
|
||||
estimatedDelivery: preferred?.estimatedDelivery,
|
||||
price: preferred?.price,
|
||||
logistician: { id: preferred?.logisticianId },
|
||||
supplierProductNumber: preferred?.supplierProductNumber,
|
||||
supplierInfo: preferred?.requestStatusCode,
|
||||
lastRequest: preferred?.requested,
|
||||
};
|
||||
return availability;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
const availability: AvailabilityDTO = {
|
||||
availabilityType: preferred?.status,
|
||||
ssc: preferred?.ssc,
|
||||
sscText: preferred?.sscText,
|
||||
supplier: { id: preferred?.supplierId },
|
||||
isPrebooked: preferred?.isPrebooked,
|
||||
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
|
||||
estimatedDelivery: preferred?.estimatedDelivery,
|
||||
price: preferred?.price,
|
||||
logistician: { id: preferred?.logisticianId },
|
||||
supplierProductNumber: preferred?.supplierProductNumber,
|
||||
supplierInfo: preferred?.requestStatusCode,
|
||||
lastRequest: preferred?.requested,
|
||||
};
|
||||
return availability;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
@@ -333,37 +334,35 @@ export class DomainAvailabilityService {
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDownloadAvailability({ item }: { item: ItemData }): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService
|
||||
.AvailabilityShippingAvailability([
|
||||
{
|
||||
ean: item?.ean,
|
||||
itemId: item?.itemId ? String(item?.itemId) : null,
|
||||
price: item?.price,
|
||||
qty: 1,
|
||||
},
|
||||
])
|
||||
.pipe(
|
||||
map((r) => {
|
||||
const availabilities = r.result;
|
||||
const preferred = availabilities?.find((f) => f.preferred === 1);
|
||||
return this.memorizedAvailabilityShippingAvailability([
|
||||
{
|
||||
ean: item?.ean,
|
||||
itemId: item?.itemId ? String(item?.itemId) : null,
|
||||
price: item?.price,
|
||||
qty: 1,
|
||||
},
|
||||
]).pipe(
|
||||
map((r) => {
|
||||
const availabilities = r.result;
|
||||
const preferred = availabilities?.find((f) => f.preferred === 1);
|
||||
|
||||
const availability: AvailabilityDTO = {
|
||||
availabilityType: preferred?.status,
|
||||
ssc: preferred?.ssc,
|
||||
sscText: preferred?.sscText,
|
||||
supplier: { id: preferred?.supplierId },
|
||||
isPrebooked: preferred?.isPrebooked,
|
||||
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
|
||||
price: preferred?.price,
|
||||
supplierProductNumber: preferred?.supplierProductNumber,
|
||||
logistician: { id: preferred?.logisticianId },
|
||||
supplierInfo: preferred?.requestStatusCode,
|
||||
lastRequest: preferred?.requested,
|
||||
};
|
||||
return availability;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
const availability: AvailabilityDTO = {
|
||||
availabilityType: preferred?.status,
|
||||
ssc: preferred?.ssc,
|
||||
sscText: preferred?.sscText,
|
||||
supplier: { id: preferred?.supplierId },
|
||||
isPrebooked: preferred?.isPrebooked,
|
||||
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
|
||||
price: preferred?.price,
|
||||
supplierProductNumber: preferred?.supplierProductNumber,
|
||||
logistician: { id: preferred?.logisticianId },
|
||||
supplierInfo: preferred?.requestStatusCode,
|
||||
lastRequest: preferred?.requested,
|
||||
};
|
||||
return availability;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
@@ -401,7 +400,7 @@ export class DomainAvailabilityService {
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
|
||||
return this._availabilityService.AvailabilityShippingAvailability(payload).pipe(
|
||||
return this.memorizedAvailabilityShippingAvailability(payload).pipe(
|
||||
timeout(20000),
|
||||
map((response) => this._mapToShippingAvailability(response.result))
|
||||
);
|
||||
@@ -409,7 +408,7 @@ export class DomainAvailabilityService {
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDigDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
|
||||
return this._availabilityService.AvailabilityShippingAvailability(payload).pipe(
|
||||
return this.memorizedAvailabilityShippingAvailability(payload).pipe(
|
||||
timeout(20000),
|
||||
map((response) => this._mapToShippingAvailability(response.result))
|
||||
);
|
||||
@@ -447,6 +446,9 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
|
||||
isAvailable({ availability }: { availability: AvailabilityDTO }) {
|
||||
if (availability?.supplier?.id === 16 && availability?.inStock == 0) {
|
||||
return false;
|
||||
}
|
||||
return [2, 32, 256, 1024, 2048, 4096].some((code) => availability?.availabilityType === code);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ export class DomainInStockService {
|
||||
const key = this.getKey({ itemId, branchId });
|
||||
this._addToInStockQueue({ itemId, branchId });
|
||||
|
||||
let _previousValue: InStock;
|
||||
|
||||
const sub = combineLatest([this._inStockMap, this._inStockFetchingMap])
|
||||
.pipe(distinctUntilChanged(isEqual))
|
||||
.subscribe(([inStockMap, inStockFetchingMap]) => {
|
||||
@@ -54,7 +56,12 @@ export class DomainInStockService {
|
||||
inStock: inStockMap[key],
|
||||
fetching: inStockFetchingMap[key] ?? false,
|
||||
};
|
||||
obs.next(inStock);
|
||||
|
||||
if (!isEqual(inStock, _previousValue)) {
|
||||
obs.next(inStock);
|
||||
}
|
||||
|
||||
_previousValue = inStock;
|
||||
});
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
StoreCheckoutBuyerService,
|
||||
StoreCheckoutPayerService,
|
||||
StoreCheckoutBranchService,
|
||||
ItemsResult,
|
||||
} from '@swagger/checkout';
|
||||
import { DisplayOrderDTO, DisplayOrderItemDTO, OrderCheckoutService, ReorderValues } from '@swagger/oms';
|
||||
import { isNullOrUndefined, memorize } from '@utils/common';
|
||||
@@ -198,7 +199,15 @@ export class DomainCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
canAddItems({ processId, payload, orderType }: { processId: number; payload: ItemPayload[]; orderType: string }) {
|
||||
canAddItems({
|
||||
processId,
|
||||
payload,
|
||||
orderType,
|
||||
}: {
|
||||
processId: number;
|
||||
payload: ItemPayload[];
|
||||
orderType: string;
|
||||
}): Observable<ItemsResult[]> {
|
||||
return this.getShoppingCart({ processId }).pipe(
|
||||
first(),
|
||||
withLatestFrom(this.store.select(DomainCheckoutSelectors.selectCustomerFeaturesByProcessId, { processId })),
|
||||
@@ -217,7 +226,8 @@ export class DomainCheckoutService {
|
||||
})
|
||||
.pipe(
|
||||
map((response) => {
|
||||
return response.result;
|
||||
// TODO: remove this when the API is fixed
|
||||
return (response.result as unknown) as ItemsResult[];
|
||||
})
|
||||
);
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
AutocompleteTokenDTO,
|
||||
BranchService,
|
||||
ChangeStockStatusCodeValues,
|
||||
HistoryDTO,
|
||||
@@ -11,9 +10,7 @@ import {
|
||||
OrderItemSubsetDTO,
|
||||
OrderListItemDTO,
|
||||
OrderService,
|
||||
QueryTokenDTO,
|
||||
ReceiptService,
|
||||
ResponseArgsOfIEnumerableOfHistoryDTO,
|
||||
StatusValues,
|
||||
StockStatusCodeService,
|
||||
ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO,
|
||||
@@ -22,7 +19,7 @@ import {
|
||||
} from '@swagger/oms';
|
||||
import { memorize } from '@utils/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, mergeMap, shareReplay } from 'rxjs/operators';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class DomainOmsService {
|
||||
@@ -191,6 +188,10 @@ export class DomainOmsService {
|
||||
);
|
||||
}
|
||||
|
||||
getOrderSource(orderId: number): Observable<string> {
|
||||
return this.getOrder(orderId).pipe(map((order) => order?.features?.orderSource));
|
||||
}
|
||||
|
||||
updateNotifications(orderId: number, changes: { selected: NotificationChannel; email: string; mobile: string }) {
|
||||
const communicationDetails = {
|
||||
email: changes.email,
|
||||
|
||||
@@ -447,7 +447,7 @@ export class DomainRemissionService {
|
||||
* Create a new receipt for the given return/remission
|
||||
* @param returnId Return ID
|
||||
* @param receiptNumber Receipt number
|
||||
* @returns ReturnDTO - ShippingDocument
|
||||
* @returns ReceiptDTO
|
||||
*/
|
||||
async createReceipt(returnDTO: ReturnDTO, receiptNumber?: string): Promise<ReceiptDTO> {
|
||||
const stock = await this._getStock();
|
||||
@@ -471,14 +471,41 @@ export class DomainRemissionService {
|
||||
return receipt;
|
||||
}
|
||||
|
||||
async completeReceipt(returnId: number, receiptId: number, packageCode: string): Promise<ReceiptDTO> {
|
||||
/**
|
||||
* Create a new Package and assign it to a receipt
|
||||
* @param returnId Return ID
|
||||
* @param receiptId Receipt ID
|
||||
* @param packageNumber Packagenumber
|
||||
* @returns ReceiptDTO
|
||||
*/
|
||||
async createReceiptAndAssignPackage({
|
||||
returnId,
|
||||
receiptId,
|
||||
packageNumber,
|
||||
}: {
|
||||
returnId: number;
|
||||
receiptId: number;
|
||||
packageNumber: string;
|
||||
}): Promise<ReceiptDTO> {
|
||||
const response = await this._returnService
|
||||
.ReturnCreateAndAssignPackage({
|
||||
returnId,
|
||||
receiptId,
|
||||
data: {
|
||||
packageNumber,
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
const receipt: ReceiptDTO = response.result;
|
||||
return receipt;
|
||||
}
|
||||
|
||||
async completeReceipt(returnId: number, receiptId: number): Promise<ReceiptDTO> {
|
||||
const res = await this._returnService
|
||||
.ReturnFinalizeReceipt({
|
||||
returnId,
|
||||
receiptId,
|
||||
data: {
|
||||
packageCode,
|
||||
},
|
||||
data: {},
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { isDevMode, NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { DebugComponent } from './debug/debug.component';
|
||||
import {
|
||||
CanActivateCartGuard,
|
||||
CanActivateCartWithProcessIdGuard,
|
||||
@@ -17,9 +16,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 +39,7 @@ const routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: 'kunde',
|
||||
component: ShellComponent,
|
||||
component: MainComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
@@ -106,7 +105,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'filiale',
|
||||
component: ShellComponent,
|
||||
component: MainComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'task-calendar',
|
||||
@@ -152,7 +151,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;
|
||||
}
|
||||
|
||||
@@ -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 { UiIconModule, UI_ICON_CFG } from '@ui/icon';
|
||||
import { PreviewComponent } from './preview';
|
||||
import { NativeContainerService } from 'native-container';
|
||||
import { ShellModule } from '@shared/shell';
|
||||
import { MainComponent } from './main.component';
|
||||
|
||||
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,12 +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' },
|
||||
],
|
||||
}),
|
||||
UiIconModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -137,6 +135,11 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
|
||||
useClass: IsaErrorHandler,
|
||||
},
|
||||
{ provide: LOCALE_ID, useValue: 'de-DE' },
|
||||
{
|
||||
provide: UI_ICON_CFG,
|
||||
useFactory: (config: Config) => config.get('@ui/icon'),
|
||||
deps: [Config],
|
||||
},
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, 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,6 +10,7 @@ export class CanActivateProductGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _checkoutService: DomainCheckoutService,
|
||||
private readonly _navigationService: ProductCatalogNavigationService,
|
||||
private readonly _router: Router
|
||||
) {}
|
||||
|
||||
@@ -38,7 +40,7 @@ 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)]));
|
||||
@@ -48,7 +50,7 @@ export class CanActivateProductGuard implements CanActivate {
|
||||
}
|
||||
|
||||
// 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 +59,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
|
||||
|
||||
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,174 +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((message) => message?.data?.length));
|
||||
|
||||
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 {}
|
||||
@@ -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.
File diff suppressed because one or more lines are too long
@@ -8,9 +8,8 @@
|
||||
},
|
||||
"@core/auth": {
|
||||
"issuer": "https://sso-test.paragon-data.de",
|
||||
"clientId": "hug-isa",
|
||||
"responseType": "id_token token",
|
||||
"oidc": true,
|
||||
"clientId": "isa-client",
|
||||
"responseType": "code",
|
||||
"scope": "openid profile cmf_user isa-isa-webapi isa-checkout-webapi isa-cat-webapi isa-ava-webapi isa-crm-webapi isa-review-webapi isa-kpi-webapi isa-oms-webapi isa-nbo-webapi isa-print-webapi eis-service isa-inv-webapi isa-wws-webapi"
|
||||
},
|
||||
"@core/logger": {
|
||||
@@ -41,7 +40,7 @@
|
||||
"rootUrl": "https://filialinformationsystem-integration.paragon-systems.de/eiswebapi/v1"
|
||||
},
|
||||
"@swagger/remi": {
|
||||
"rootUrl": "https://isa-integration.paragon-data.net/inv/v1"
|
||||
"rootUrl": "https://isa-integration.paragon-data.net/inv/v6"
|
||||
},
|
||||
"@swagger/wws": {
|
||||
"rootUrl": "https://isa-integration.paragon-data.net/wws/v1"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -41,10 +41,10 @@
|
||||
"rootUrl": "https://filialinformationsystem.paragon-systems.de/eiswebapi/v1"
|
||||
},
|
||||
"@swagger/remi": {
|
||||
"rootUrl": "https://isa.paragon-systems.de/inv/v1"
|
||||
"rootUrl": "https://isa.paragon-systems.de/inv/v6"
|
||||
},
|
||||
"@swagger/wws": {
|
||||
"rootUrl": "https://isa.paragon-data.net/wws/v1"
|
||||
"rootUrl": "https://isa.paragon-systems.de/wws/v1"
|
||||
},
|
||||
"hubs": {
|
||||
"notifications": {
|
||||
@@ -69,6 +69,6 @@
|
||||
},
|
||||
"checkForUpdates": 3600000,
|
||||
"licence": {
|
||||
"scandit": ""
|
||||
"scandit": "AfHi/mY+RbwJD5nC7SuWn3I14pFUOfSbQ2QG//4aV3zWQjwix30kHqsqraA8ZiipDBql8YlwIyV6VPBMUiAX4s9YHDxHHsWwq2BUB3ImzDEcU1jmMH/5yakGUYpCQ68D0iZ8SG9sS0QBb3iFdCHc1r9DFr1cMTxM7zOvb/AUoIVmieHZXnx9ioUgCvczsLiuX3hwvTW3lhbvJ4uUyqTWK4sWFVwoY4AIWSFrPwwrkV2DksMKT5fMJT3GWgPypvTIGwWvpRfLWwKlc1Z3ckyb84khsnaWD2wr+hdgu/K8YIMmgGszm5KIZ/G05YfDNZtQ4jby+5RZvQwWR8rxM35rJgf73OkMSpuL9jw3T0TTAlvpkGRLzVVuCw9VjlBLqfPNEZ6VsEwFuAla9IYUvFHCsjypg2J6UpxHXrTYmbsSu5Jm8frVfS5znPPTO9D/4rF6ZVv2PxY9PgUgJUvwMa/VMc/nse3RRRf8RGT4rUItfJDFO8pujD76vVEWq/KixQRoMdLgDLyxhsFVftkxqhZhyEfFZzsEy49LSojJ28vpHpBWLeCQBmnZ7JZ4C5yOQiqSQV/assBq2zJN2q+vCDp8qy5j1rED1SX5Ec7JpgpgnU4chLIf5Zn7bP/hNGT3pEYBuXeDXXN8ke1pcc3fc3m0FysDG0o56XVCUqImZ8Ezi8eujZciKDrWbtljhKTj7cnfuJx0sVHF6Bh5i4YfgA/Z+NL+MtH2EVIF67e6hEz6PWYTcoh3ybBaJfxb2FNvGJutNKg04GwMhYq6K2IddBt0fDiBt0SGM0oSBlUP3DKCUmXcf2a6ASbrcqv6Wz1jHt0pY4U8bEpg7qSbW3VDyvdPgyQ="
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@
|
||||
"rootUrl": "https://filialinformationsystem-staging.paragon-systems.de/eiswebapi/v1"
|
||||
},
|
||||
"@swagger/remi": {
|
||||
"rootUrl": "https://isa-staging.paragon-systems.de/inv/v1"
|
||||
"rootUrl": "https://isa-staging.paragon-systems.de/inv/v6"
|
||||
},
|
||||
"@swagger/wws": {
|
||||
"rootUrl": "https://isa-staging.paragon-systems.de/wws/v1"
|
||||
@@ -69,6 +69,6 @@
|
||||
},
|
||||
"checkForUpdates": 3600000,
|
||||
"licence": {
|
||||
"scandit": ""
|
||||
"scandit": "AfHi/mY+RbwJD5nC7SuWn3I14pFUOfSbQ2QG//4aV3zWQjwix30kHqsqraA8ZiipDBql8YlwIyV6VPBMUiAX4s9YHDxHHsWwq2BUB3ImzDEcU1jmMH/5yakGUYpCQ68D0iZ8SG9sS0QBb3iFdCHc1r9DFr1cMTxM7zOvb/AUoIVmieHZXnx9ioUgCvczsLiuX3hwvTW3lhbvJ4uUyqTWK4sWFVwoY4AIWSFrPwwrkV2DksMKT5fMJT3GWgPypvTIGwWvpRfLWwKlc1Z3ckyb84khsnaWD2wr+hdgu/K8YIMmgGszm5KIZ/G05YfDNZtQ4jby+5RZvQwWR8rxM35rJgf73OkMSpuL9jw3T0TTAlvpkGRLzVVuCw9VjlBLqfPNEZ6VsEwFuAla9IYUvFHCsjypg2J6UpxHXrTYmbsSu5Jm8frVfS5znPPTO9D/4rF6ZVv2PxY9PgUgJUvwMa/VMc/nse3RRRf8RGT4rUItfJDFO8pujD76vVEWq/KixQRoMdLgDLyxhsFVftkxqhZhyEfFZzsEy49LSojJ28vpHpBWLeCQBmnZ7JZ4C5yOQiqSQV/assBq2zJN2q+vCDp8qy5j1rED1SX5Ec7JpgpgnU4chLIf5Zn7bP/hNGT3pEYBuXeDXXN8ke1pcc3fc3m0FysDG0o56XVCUqImZ8Ezi8eujZciKDrWbtljhKTj7cnfuJx0sVHF6Bh5i4YfgA/Z+NL+MtH2EVIF67e6hEz6PWYTcoh3ybBaJfxb2FNvGJutNKg04GwMhYq6K2IddBt0fDiBt0SGM0oSBlUP3DKCUmXcf2a6ASbrcqv6Wz1jHt0pY4U8bEpg7qSbW3VDyvdPgyQ="
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
@@ -11,15 +11,21 @@
|
||||
@import './scss/customer';
|
||||
@import './scss/branch';
|
||||
|
||||
* {
|
||||
@apply font-sans;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1680px) {
|
||||
:root {
|
||||
font-size: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0; // remove scrollbar space
|
||||
height: 0;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ modal-notifications {
|
||||
}
|
||||
|
||||
.notification-card {
|
||||
@apply text-center text-xl text-inactive-branch block bg-white rounded-t-card font-bold no-underline py-4 border-none outline-none shadow-card -ml-4;
|
||||
@apply text-center text-xl text-inactive-branch block bg-white rounded-t font-bold no-underline py-4 border-none outline-none shadow-card -ml-4;
|
||||
width: calc(100% + 2rem);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ modal-notifications {
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply font-bold text-2xl ml-4;
|
||||
@apply font-bold text-h3 ml-4;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,11 +66,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ import { EnvironmentService } from '@core/environment';
|
||||
import { DomainAvailabilityService, DomainInStockService } from '@domain/availability';
|
||||
import { ProductListItemDTO } from '@swagger/wws';
|
||||
import { DateAdapter } from '@ui/common';
|
||||
import { debounceTime, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
import { debounceTime, filter, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
import { PriceUpdateComponentStore } from '../price-update.component.store';
|
||||
import { ReplaySubject, combineLatest } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'page-price-update-item',
|
||||
@@ -16,8 +17,18 @@ import { PriceUpdateComponentStore } from '../price-update.component.store';
|
||||
providers: [DatePipe],
|
||||
})
|
||||
export class PriceUpdateItemComponent {
|
||||
private _item$ = new ReplaySubject<ProductListItemDTO>(1);
|
||||
|
||||
private _item: ProductListItemDTO;
|
||||
|
||||
@Input()
|
||||
item: ProductListItemDTO;
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
set item(value) {
|
||||
this._item = value;
|
||||
this._item$.next(value);
|
||||
}
|
||||
|
||||
get publicationDate() {
|
||||
if (!!this.item?.product?.publicationDate) {
|
||||
@@ -40,21 +51,14 @@ export class PriceUpdateItemComponent {
|
||||
|
||||
defaultBranch$ = this._availability.getDefaultBranch();
|
||||
|
||||
inStock$ = this.defaultBranch$.pipe(
|
||||
inStock$ = combineLatest([this.defaultBranch$, this._item$]).pipe(
|
||||
debounceTime(100),
|
||||
switchMap(
|
||||
(defaultBranch) =>
|
||||
this._stockService.getInStock$({
|
||||
itemId: Number(this.item?.product?.catalogProductNumber),
|
||||
branchId: defaultBranch?.id,
|
||||
})
|
||||
// TODO: Bugfixing INSTOCK
|
||||
// .pipe(
|
||||
// map((instock) => {
|
||||
// this.item.product.ean === '9783551775559' ? console.log({ item: this.item, instock }) : '';
|
||||
// return instock;
|
||||
// })
|
||||
// )
|
||||
filter(([defaultBranch, item]) => !!defaultBranch && !!item),
|
||||
switchMap(([defaultBranch, item]) =>
|
||||
this._stockService.getInStock$({
|
||||
itemId: Number(item?.product?.catalogProductNumber),
|
||||
branchId: defaultBranch?.id,
|
||||
})
|
||||
),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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"
|
||||
@@ -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,6 +1,6 @@
|
||||
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';
|
||||
@@ -8,7 +8,7 @@ import { PriceUpdateListModule } from './price-update-list';
|
||||
import { PriceUpdateComponent } from './price-update.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, PriceUpdateListModule, UiIconModule, UiFilterNextModule, ShellFilterOverlayModule, UiSpinnerModule],
|
||||
imports: [CommonModule, PriceUpdateListModule, UiIconModule, UiFilterNextModule, SharedFilterOverlayModule, UiSpinnerModule],
|
||||
exports: [PriceUpdateComponent],
|
||||
declarations: [PriceUpdateComponent],
|
||||
providers: [],
|
||||
|
||||
@@ -1,124 +1,240 @@
|
||||
<ng-container *ngIf="!showRecommendations">
|
||||
<div #detailsContainer class="product-card">
|
||||
<div 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 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>
|
||||
|
||||
<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="page-article-details__product-volume" *ngIf="item?.product?.volume">Band/Reihe {{ item?.product?.volume }}</div>
|
||||
|
||||
<div class="page-article-details__product-publication">{{ publicationDate$ | async }}</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="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="page-article-details__product-price font-bold text-xl self-end"
|
||||
*ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability"
|
||||
>
|
||||
{{ takeAwayAvailability?.retailPrice?.value?.value | currency: takeAwayAvailability?.retailPrice?.value?.currency:'code' }}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<button class="cta-print right" (click)="print()">Drucken</button>
|
||||
</div>
|
||||
<div class="title">
|
||||
{{ item.product?.name }}
|
||||
<div class="page-article-details__product-points self-end" *ngIf="store.promotionPoints$ | async; let promotionPoints">
|
||||
{{ promotionPoints }} Lesepunkte
|
||||
</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 }}
|
||||
<!-- TODO: Ticket PREISGEBUNDEN -->
|
||||
<div class="page-article-details__product-price-bound self-end"></div>
|
||||
</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="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>
|
||||
<div
|
||||
class="flex flex-row py-4 pl-4"
|
||||
[uiOverlayTrigger]="tooltip"
|
||||
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
|
||||
*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>
|
||||
</div>
|
||||
</div>
|
||||
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
|
||||
{{ stockTooltipText$ | async }}
|
||||
</ui-tooltip>
|
||||
|
||||
<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>
|
||||
</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
|
||||
*ngIf="store.isPickUpAvailabilityAvailable$ | 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="mx-1" icon="box_out" size="18px"></ui-icon>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<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>
|
||||
<span class="font-bold">Download</span>
|
||||
</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 *ngIf="store.sscText$ | async; let sscText">
|
||||
{{ sscText }}
|
||||
</div>
|
||||
<div *ngIf="item?.product?.volume">Band/Reihe {{ item?.product?.volume }}</div>
|
||||
|
||||
<div>{{ 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>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="row stock">
|
||||
<div 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>
|
||||
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
|
||||
{{ stockTooltipText$ | async }}
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div *ngIf="item?.product?.locale" data-name="product-language">{{ item?.product?.locale }}</div>
|
||||
|
||||
<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 *ngIf="store.isPickUpAvailabilityAvailable$ | async" icon="box_out" size="18px"></ui-icon>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="shelfinfo right" *ngIf="store.isDownload$ | async">
|
||||
<div class="page-article-details__shelfinfo" *ngIf="store.isDownload$ | async">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label;
|
||||
@@ -145,24 +261,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;
|
||||
@@ -186,109 +285,118 @@
|
||||
</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>
|
||||
<button
|
||||
class="cta-continue"
|
||||
(click)="showPurchasingModal()"
|
||||
[disabled]="
|
||||
!(isAvailable$ | async) || (fetchingAvailabilities$ | async) || (item?.features && (item?.features)[0]?.key === 'PFO')
|
||||
"
|
||||
>
|
||||
In den Warenkorb
|
||||
</button>
|
||||
</div>
|
||||
<hr class="bg-[#E6EFF9] border-t-2 my-3" />
|
||||
|
||||
<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">
|
||||
<div
|
||||
#description
|
||||
class="page-article-details__product-description flex flex-col flex-grow overflow-hidden overflow-y-scroll"
|
||||
*ngIf="item.texts?.length > 0"
|
||||
>
|
||||
<div class="whitespace-pre-line">
|
||||
{{ item.texts[0].value }}
|
||||
</div>
|
||||
|
||||
<div class="product-text">
|
||||
<button
|
||||
class="font-bold flex flex-row text-[#0556B4] items-center mt-2"
|
||||
*ngIf="!showMore && item?.texts?.length > 1"
|
||||
(click)="showMore = !showMore"
|
||||
>
|
||||
Mehr <ui-icon class="ml-2" size="15px" icon="arrow"></ui-icon>
|
||||
</button>
|
||||
|
||||
<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 absolute text-center left-0 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 -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 top-16 rounded-t" @slideYAnimation *ngIf="showRecommendations">
|
||||
<button
|
||||
class="h-[3.75rem] shadow-[0_-2px_24px_0_#dce2e9] flex flex-row justify-center items-center w-full text-xl bg-white text-ucla-blue font-bold border-none outline-none rounded-t"
|
||||
(click)="showRecommendations = false"
|
||||
>
|
||||
{{ (store.item$ | async)?.product?.name }}
|
||||
</button>
|
||||
<page-article-recommendations (close)="showRecommendations = false"></page-article-recommendations>
|
||||
</div>
|
||||
|
||||
@@ -1,269 +1,99 @@
|
||||
:host {
|
||||
@apply flex flex-col;
|
||||
@apply box-border block h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
|
||||
}
|
||||
|
||||
.product-card {
|
||||
@apply flex flex-col bg-white w-full rounded-card shadow-card;
|
||||
|
||||
.product-details {
|
||||
@apply flex flex-row p-5;
|
||||
|
||||
.bookmark {
|
||||
@apply absolute flex;
|
||||
top: 52px;
|
||||
right: 25px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.bookmark-badge {
|
||||
@apply p-0 m-0 outline-none border-none bg-transparent relative;
|
||||
}
|
||||
|
||||
.promotion-badge {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.bookmark-badge-gap {
|
||||
@apply mt-px-35;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
@apply flex flex-col items-center justify-start mr-5;
|
||||
|
||||
.recessions {
|
||||
@apply flex flex-col items-center mt-4 bg-transparent border-none outline-none;
|
||||
|
||||
.cta-recessions {
|
||||
@apply text-regular text-dark-cerulean font-bold mt-2;
|
||||
}
|
||||
}
|
||||
|
||||
.image-button {
|
||||
@apply border-none outline-none bg-transparent relative;
|
||||
|
||||
ui-icon {
|
||||
@apply absolute text-dark-cerulean inline-block;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
@apply rounded-xl shadow-card;
|
||||
box-shadow: 0 0 18px 0 #b8b3b7;
|
||||
max-height: 315px;
|
||||
max-width: 195px;
|
||||
}
|
||||
}
|
||||
|
||||
.product-info {
|
||||
@apply w-full;
|
||||
|
||||
.title {
|
||||
@apply text-3xl font-bold mb-6;
|
||||
}
|
||||
|
||||
.format,
|
||||
.ssc,
|
||||
.quantity {
|
||||
@apply font-bold text-lg;
|
||||
}
|
||||
|
||||
.stock {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.quantity {
|
||||
@apply flex justify-end mt-4;
|
||||
|
||||
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__container {
|
||||
@apply h-full w-full bg-white rounded shadow-card flex flex-col;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.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 misc price'
|
||||
'image misc misc price'
|
||||
'image origin origin stock'
|
||||
'image origin origin stock'
|
||||
'image specs availabilities availabilities'
|
||||
'image specs ssc ssc'
|
||||
'image . ssc ssc'
|
||||
'image . ssc ssc';
|
||||
}
|
||||
|
||||
.recommendations-overlay {
|
||||
@apply absolute w-full top-0 rounded-t-card;
|
||||
top: 56px;
|
||||
.page-article-details__product-bookmark {
|
||||
grid-area: bookmark;
|
||||
}
|
||||
|
||||
.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;
|
||||
.page-article-details__product-image-recessions {
|
||||
grid-area: image;
|
||||
}
|
||||
|
||||
.page-article-details__product-contributors {
|
||||
grid-area: contributors;
|
||||
}
|
||||
|
||||
.page-article-details__product-print {
|
||||
grid-area: print;
|
||||
}
|
||||
|
||||
.page-article-details__product-title {
|
||||
grid-area: title;
|
||||
}
|
||||
|
||||
.page-article-details__product-misc {
|
||||
grid-area: misc;
|
||||
}
|
||||
|
||||
.page-article-details__product-price-info {
|
||||
grid-area: price;
|
||||
}
|
||||
|
||||
.page-article-details__product-origin-infos {
|
||||
grid-area: origin;
|
||||
}
|
||||
|
||||
.page-article-details__product-stock {
|
||||
grid-area: stock;
|
||||
}
|
||||
|
||||
.page-article-details__product-ean-specs {
|
||||
grid-area: specs;
|
||||
}
|
||||
|
||||
.page-article-details__product-availabilities {
|
||||
grid-area: availabilities;
|
||||
}
|
||||
|
||||
.page-article-details__shelf-ssc {
|
||||
grid-area: ssc;
|
||||
}
|
||||
|
||||
.page-article-details__product-description-text {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.page-article-details__product-formats {
|
||||
@apply grid whitespace-nowrap items-center max-w-full;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.page-article-details__actions {
|
||||
&:disabled {
|
||||
@apply bg-inactive-branch;
|
||||
}
|
||||
}
|
||||
|
||||
.autor {
|
||||
@apply text-active-customer font-bold no-underline;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ElementRef } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { DomainPrinterService } from '@domain/printer';
|
||||
import { ItemDTO as PrinterItemDTO } from '@swagger/print';
|
||||
import { PrintModalComponent, PrintModalData } from '@modal/printer';
|
||||
import { AvailabilityDTO, BranchDTO } from '@swagger/checkout';
|
||||
import { BranchDTO } from '@swagger/checkout';
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { ModalReviewsComponent } from '@modal/reviews';
|
||||
import { PurchasingOptionsModalComponent, PurchasingOptionsModalData } from 'apps/page/checkout/src/lib/modals/purchasing-options-modal';
|
||||
import { PurchasingOptions } from 'apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.store';
|
||||
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
|
||||
import { debounceTime, filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
import { debounceTime, filter, first, map, shareReplay, switchMap, takeUntil, 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';
|
||||
@@ -20,7 +18,11 @@ import { BreadcrumbService } from '@core/breadcrumb';
|
||||
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 { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-details',
|
||||
@@ -66,6 +68,10 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
this.store.isDeliveryB2BAvailabilityAvailable$,
|
||||
]).pipe(map(([digDelivery, b2bDelivery]) => b2bDelivery && !digDelivery));
|
||||
|
||||
customerFeatures$ = this.applicationService.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this._domainCheckoutService.getCustomerFeatures({ processId }))
|
||||
);
|
||||
|
||||
showSubscriptionBadge$ = this.store.item$.pipe(map((item) => item?.features?.find((i) => i.key === 'PFO')));
|
||||
|
||||
showPromotionBadge$ = this.store.item$.pipe(map((item) => item?.features?.find((i) => i.key === 'Promotion')));
|
||||
@@ -114,6 +120,19 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
get isTablet$() {
|
||||
return this._environment.matchTablet$.pipe(
|
||||
map((state) => state?.matches),
|
||||
shareReplay()
|
||||
);
|
||||
}
|
||||
|
||||
get resultsPath() {
|
||||
return this._navigationService.getArticleSearchResultsPath(this.applicationService.activatedProcessId);
|
||||
}
|
||||
|
||||
showMore: boolean = false;
|
||||
|
||||
constructor(
|
||||
public readonly applicationService: ApplicationService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
@@ -125,7 +144,12 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
private _dateAdapter: DateAdapter,
|
||||
private _datePipe: DatePipe,
|
||||
public elementRef: ElementRef,
|
||||
private _availability: DomainAvailabilityService
|
||||
private _purchaseOptionsModalService: PurchaseOptionsModalService,
|
||||
private _availability: DomainAvailabilityService,
|
||||
private _navigationService: ProductCatalogNavigationService,
|
||||
private _environment: EnvironmentService,
|
||||
private _router: Router,
|
||||
private _domainCheckoutService: DomainCheckoutService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -160,16 +184,30 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
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.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}`])
|
||||
@@ -183,13 +221,41 @@ 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 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({
|
||||
@@ -262,64 +328,73 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async showPurchasingModal(selectedBranch?: BranchDTO) {
|
||||
let availableOptions: PurchasingOptions[] = [];
|
||||
const availabilities: { [key: string]: AvailabilityDTO } = {};
|
||||
const item = await this.store.item$.pipe(first()).toPromise();
|
||||
|
||||
const takeNow = await this.store.isTakeAwayAvailabilityAvailable$.pipe(first()).toPromise();
|
||||
if (takeNow) {
|
||||
availableOptions.push('take-away');
|
||||
availabilities['take-away'] = await this.store.takeAwayAvailability$.pipe(first()).toPromise();
|
||||
}
|
||||
|
||||
const download = await this.store.isDownloadAvailabilityAvailable$.pipe(first()).toPromise();
|
||||
if (download) {
|
||||
availableOptions.push('download');
|
||||
availabilities['download'] = await this.store.downloadAvailability$.pipe(first()).toPromise();
|
||||
}
|
||||
|
||||
const pickup = await this.store.isPickUpAvailabilityAvailable$.pipe(first()).toPromise();
|
||||
if (pickup) {
|
||||
availableOptions.push('pick-up');
|
||||
availabilities['pick-up'] = await this.store.pickUpAvailability$.pipe(first()).toPromise();
|
||||
}
|
||||
|
||||
const digDelivery = await this.store.isDeliveryDigAvailabilityAvailable$.pipe(first()).toPromise();
|
||||
if (digDelivery) {
|
||||
availableOptions.push('dig-delivery');
|
||||
availabilities['dig-delivery'] = await this.store.deliveryDigAvailability$.pipe(first()).toPromise();
|
||||
}
|
||||
|
||||
const b2b = await this.store.isDeliveryB2BAvailabilityAvailable$.pipe(first()).toPromise();
|
||||
|
||||
if (b2b) {
|
||||
availableOptions.push('b2b-delivery');
|
||||
availabilities['b2b-delivery'] = await this.store.deliveryB2BAvailability$.pipe(first()).toPromise();
|
||||
}
|
||||
|
||||
if (availableOptions.includes('dig-delivery') && availableOptions.includes('b2b-delivery')) {
|
||||
availableOptions.push('delivery');
|
||||
availabilities['delivery'] = await this.store.deliveryAvailability$.pipe(first()).toPromise();
|
||||
availableOptions = availableOptions.filter((option) => !(option === 'dig-delivery' || option === 'b2b-delivery'));
|
||||
}
|
||||
|
||||
const branch = selectedBranch || (await this.store.branch$.pipe(first()).toPromise());
|
||||
|
||||
this.uiModal.open({
|
||||
content: PurchasingOptionsModalComponent,
|
||||
data: {
|
||||
availableOptions,
|
||||
option: selectedBranch ? 'take-away' : undefined,
|
||||
item: await this.store.item$.pipe(first()).toPromise(),
|
||||
branchId: branch?.id,
|
||||
this._purchaseOptionsModalService
|
||||
.open({
|
||||
type: 'add',
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
availabilities,
|
||||
} as PurchasingOptionsModalData,
|
||||
});
|
||||
items: [item],
|
||||
})
|
||||
.afterClosed$.subscribe(async (result) => {
|
||||
if (result?.data === 'continue') {
|
||||
const customer = await this._domainCheckoutService
|
||||
.getBuyer({ processId: this.applicationService.activatedProcessId })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
if (customer) {
|
||||
this.navigateToShoppingCart();
|
||||
} else {
|
||||
this.navigateToCustomerSearch();
|
||||
}
|
||||
console.log('continue');
|
||||
} else if (result?.data === 'continue-shopping') {
|
||||
this.navigateToResultList();
|
||||
console.log('continue-shopping');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scrollTop() {
|
||||
const element = this.elementRef.nativeElement.closest('.main-wrapper');
|
||||
element?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
navigateToShoppingCart() {
|
||||
this._router.navigate([`/kunde/${this.applicationService.activatedProcessId}/cart/review`]);
|
||||
}
|
||||
|
||||
async navigateToCustomerSearch() {
|
||||
try {
|
||||
const response = await this.customerFeatures$
|
||||
.pipe(
|
||||
first(),
|
||||
switchMap((customerFeatures) => {
|
||||
return this._domainCheckoutService.canSetCustomer({ processId: this.applicationService.activatedProcessId, customerFeatures });
|
||||
})
|
||||
)
|
||||
.toPromise();
|
||||
this._router.navigate(['/kunde', this.applicationService.activatedProcessId, 'customer', 'search'], {
|
||||
queryParams: { filter_customertype: response.filter.customertype },
|
||||
});
|
||||
} catch (error) {
|
||||
this._router.navigate(['/kunde', this.applicationService.activatedProcessId, 'customer', 'search']);
|
||||
}
|
||||
}
|
||||
|
||||
async navigateToResultList() {
|
||||
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 });
|
||||
} else {
|
||||
this._router.navigate([`/kunde/${this.applicationService.activatedProcessId}/product`]);
|
||||
}
|
||||
}
|
||||
|
||||
scrollTop(div: HTMLDivElement) {
|
||||
div?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
loadImage() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
:host {
|
||||
@apply flex flex-col bg-white;
|
||||
@apply flex flex-col bg-white h-[calc(100vh-16.5rem-3.75rem)] desktop-small:h-[calc(100vh-15.1rem-3.75rem)];
|
||||
box-shadow: 0px -2px 24px 0px #dce2e9;
|
||||
height: calc(100vh - 342px);
|
||||
}
|
||||
|
||||
h1 {
|
||||
@@ -18,7 +17,7 @@ p {
|
||||
@apply mt-10;
|
||||
|
||||
.label {
|
||||
@apply flex flex-row items-center uppercase text-dark-cerulean font-bold text-sm ml-6;
|
||||
@apply flex flex-row items-center uppercase text-dark-cerulean font-bold text-p3 ml-6;
|
||||
|
||||
ui-icon {
|
||||
@apply mr-2;
|
||||
@@ -26,7 +25,7 @@ p {
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
@apply flex justify-center text-cta-l mt-12;
|
||||
@apply flex justify-center text-p1 mt-12;
|
||||
}
|
||||
|
||||
.article {
|
||||
@@ -40,7 +39,7 @@ p {
|
||||
}
|
||||
|
||||
.format {
|
||||
@apply text-regular mt-2;
|
||||
@apply text-p2 mt-2;
|
||||
}
|
||||
|
||||
.price {
|
||||
|
||||
@@ -1,13 +1 @@
|
||||
<button class="filter" [class.active]="hasFilter$ | async" (click)="filterActive$.next(true); shellFilterOverlay.open()">
|
||||
<ui-icon size="20px" icon="filter_alit"></ui-icon>
|
||||
<span class="label">Filter</span>
|
||||
</button>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<shell-filter-overlay #shellFilterOverlay>
|
||||
<page-article-search-filter
|
||||
*ngIf="filterActive$ | async"
|
||||
(close)="filterActive$.next(false); shellFilterOverlay.close()"
|
||||
></page-article-search-filter>
|
||||
</shell-filter-overlay>
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
:host {
|
||||
@apply flex flex-col w-full box-content relative;
|
||||
}
|
||||
|
||||
.filter {
|
||||
@apply font-sans flex self-end items-center mb-4 font-bold bg-wild-blue-yonder border-0 text-regular py-px-8 px-px-15 rounded-filter justify-center z-sticky;
|
||||
width: 106px;
|
||||
min-width: 106px;
|
||||
|
||||
.label {
|
||||
@apply ml-px-5;
|
||||
}
|
||||
|
||||
&.active {
|
||||
@apply bg-active-customer text-white ml-px-5;
|
||||
}
|
||||
@apply flex flex-col w-full h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)] box-content relative;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { UiFilterAutocompleteProvider } from '@ui/filter';
|
||||
import { isEqual } from 'lodash';
|
||||
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
||||
import { filter, first, map, takeUntil, withLatestFrom } from 'rxjs/operators';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { map, takeUntil, withLatestFrom } from 'rxjs/operators';
|
||||
import { ArticleSearchService } from './article-search.store';
|
||||
import { FocusSearchboxEvent } from './focus-searchbox.event';
|
||||
import { ArticleSearchMainAutocompleteProvider } from './providers';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { FilterAutocompleteProvider } from 'apps/shared/components/filter/src/lib';
|
||||
import { isEqual } from 'lodash';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-search',
|
||||
@@ -15,9 +17,8 @@ import { ArticleSearchMainAutocompleteProvider } from './providers';
|
||||
styleUrls: ['article-search.component.scss'],
|
||||
providers: [
|
||||
FocusSearchboxEvent,
|
||||
ArticleSearchService,
|
||||
{
|
||||
provide: UiFilterAutocompleteProvider,
|
||||
provide: FilterAutocompleteProvider,
|
||||
useClass: ArticleSearchMainAutocompleteProvider,
|
||||
multi: true,
|
||||
},
|
||||
@@ -28,22 +29,16 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
|
||||
private _onDestroy$ = new Subject();
|
||||
private _processId$: Observable<number>;
|
||||
|
||||
initialFilter$ = this._articleSearch.filter$.pipe(
|
||||
filter((filter) => !!filter),
|
||||
first()
|
||||
);
|
||||
get isTablet() {
|
||||
return this._environmentService.matchTablet();
|
||||
}
|
||||
|
||||
hasFilter$ = this._articleSearch.filter$.pipe(
|
||||
withLatestFrom(this.initialFilter$),
|
||||
map(([filter, initialFilter]) => !isEqual(filter?.getQueryParams(), initialFilter?.getQueryParams()))
|
||||
);
|
||||
|
||||
filterActive$ = new BehaviorSubject<boolean>(false);
|
||||
constructor(
|
||||
private _breadcrumb: BreadcrumbService,
|
||||
private _router: Router,
|
||||
private _articleSearch: ArticleSearchService,
|
||||
private _activatedRoute: ActivatedRoute
|
||||
private _activatedRoute: ActivatedRoute,
|
||||
private _navigationService: ProductCatalogNavigationService,
|
||||
private _environmentService: EnvironmentService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -60,12 +55,17 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
|
||||
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._processId$))
|
||||
.subscribe(([state, processId]) => {
|
||||
if (state.searchState === '') {
|
||||
const params = state.filter.getQueryParams();
|
||||
if (state.hits === 1) {
|
||||
const item = state.items.find((f) => f);
|
||||
this._router.navigate(['/kunde', processId, 'product', 'details', item.id]);
|
||||
this._navigationService.navigateToDetails({
|
||||
processId,
|
||||
itemId: item.id,
|
||||
queryParams: this.isTablet ? undefined : params,
|
||||
});
|
||||
} else {
|
||||
const params = state.filter.getQueryParams();
|
||||
this._router.navigate(['/kunde', processId, 'product', 'search', 'results'], {
|
||||
this._navigationService.navigateToResults({
|
||||
processId,
|
||||
queryParams: params,
|
||||
});
|
||||
}
|
||||
@@ -73,6 +73,20 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
cleanupQueryParams(params: Record<string, string> = {}) {
|
||||
const clean = { ...params };
|
||||
|
||||
for (const key in clean) {
|
||||
if (Object.prototype.hasOwnProperty.call(clean, key)) {
|
||||
if (clean[key] == undefined) {
|
||||
delete clean[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
||||
initProcessId() {
|
||||
this._processId$ = this._activatedRoute.parent.data.pipe(map((data) => Number(data.processId)));
|
||||
}
|
||||
@@ -96,7 +110,7 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
|
||||
await this._breadcrumb.addBreadcrumbIfNotExists({
|
||||
key: processId,
|
||||
name: 'Artikelsuche',
|
||||
path: `/kunde/${processId}/product`,
|
||||
path: this._navigationService.getArticleSearchBasePath(processId),
|
||||
params: queryParams,
|
||||
tags: ['catalog', 'main'],
|
||||
section: 'customer',
|
||||
|
||||
@@ -6,12 +6,13 @@ import { ArticleSearchComponent } from './article-search.component';
|
||||
import { SearchResultsModule } from './search-results/search-results.module';
|
||||
import { SearchMainModule } from './search-main/search-main.module';
|
||||
import { SearchFilterModule } from './search-filter/search-filter.module';
|
||||
import { ShellFilterOverlayModule } from '@shell/filter-overlay';
|
||||
import { ArticleSearchService } from './article-search.store';
|
||||
import { SharedFilterOverlayModule } from '@shared/components/filter-overlay';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, RouterModule, UiIconModule, SearchResultsModule, SearchMainModule, SearchFilterModule, ShellFilterOverlayModule],
|
||||
imports: [CommonModule, RouterModule, UiIconModule, SearchResultsModule, SearchMainModule, SearchFilterModule, SharedFilterOverlayModule],
|
||||
exports: [ArticleSearchComponent],
|
||||
declarations: [ArticleSearchComponent],
|
||||
providers: [],
|
||||
providers: [ArticleSearchService],
|
||||
})
|
||||
export class ArticleSearchModule {}
|
||||
|
||||
@@ -3,24 +3,31 @@ import { DomainCatalogService } from '@domain/catalog';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { debounceTime, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||
|
||||
import { UiFilter } from '@ui/filter';
|
||||
import { ComponentStore, tapResponse } from '@ngrx/component-store';
|
||||
import { ItemDTO, QueryTokenDTO } from '@swagger/cat';
|
||||
import { ItemDTO, QueryTokenDTO, UISettingsDTO } from '@swagger/cat';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { BranchDTO } from '@swagger/checkout';
|
||||
import { Filter } from 'apps/shared/components/filter/src/lib';
|
||||
|
||||
export interface ArticleSearchState {
|
||||
processId: number;
|
||||
filter: UiFilter;
|
||||
filter: Filter;
|
||||
searchState: '' | 'fetching' | 'empty' | 'error';
|
||||
items: ItemDTO[];
|
||||
hits: number;
|
||||
selectedBranch: BranchDTO;
|
||||
selectedItemIds: number[];
|
||||
defaultSettings?: UISettingsDTO;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
|
||||
get defaultSettings() {
|
||||
return this.get((s) => s.defaultSettings);
|
||||
}
|
||||
|
||||
readonly defaultSettings$ = this.select((s) => s.defaultSettings);
|
||||
|
||||
get processId() {
|
||||
return this.get((s) => s.processId);
|
||||
}
|
||||
@@ -100,19 +107,19 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
|
||||
}
|
||||
|
||||
async setDefaultFilter(defaultQueryParams?: Record<string, string>) {
|
||||
const filter = await this.catalog
|
||||
.getSettings()
|
||||
.pipe(map((settings) => UiFilter.create(settings)))
|
||||
.toPromise();
|
||||
const defaultSettings = await this.catalog.getSettings().toPromise();
|
||||
|
||||
const filter = Filter.create(defaultSettings);
|
||||
|
||||
if (!!defaultQueryParams) {
|
||||
filter?.fromQueryParams(defaultQueryParams);
|
||||
}
|
||||
|
||||
this.setFilter(filter);
|
||||
this.patchState({ defaultSettings });
|
||||
}
|
||||
|
||||
setFilter(filter: UiFilter) {
|
||||
setFilter(filter: Filter) {
|
||||
this.patchState({ filter });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DomainCatalogService } from '@domain/catalog';
|
||||
import { UiFilterAutocomplete, UiFilterAutocompleteProvider, UiInput } from '@ui/filter';
|
||||
import { FilterAutocomplete, FilterAutocompleteProvider, FilterInput } from 'apps/shared/components/filter/src/lib';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class ArticleSearchMainAutocompleteProvider extends UiFilterAutocompleteProvider {
|
||||
export class ArticleSearchMainAutocompleteProvider extends FilterAutocompleteProvider {
|
||||
for = 'catalog';
|
||||
|
||||
constructor(private domainCatalogSearch: DomainCatalogService) {
|
||||
super();
|
||||
}
|
||||
|
||||
complete(input: UiInput): Observable<UiFilterAutocomplete[]> {
|
||||
complete(input: FilterInput): Observable<FilterAutocomplete[]> {
|
||||
const token = input?.parent?.parent?.getQueryToken();
|
||||
const filter = token?.filter;
|
||||
const type = Object.keys(token?.input).join(';');
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
<ng-container *ngIf="filter$ | async; let filter">
|
||||
<div class="catalog-search-filter-content">
|
||||
<button class="btn-close" type="button" (click)="close.emit()">
|
||||
<ui-icon icon="close" size="20px"></ui-icon>
|
||||
</button>
|
||||
<div class="w-full flex flex-row justify-end items-center">
|
||||
<button (click)="clearFilter(filter)" class="text-[#0556B4] p-4">Alle Filter entfernen</button>
|
||||
<a
|
||||
*ngIf="showFilterClose$ | async"
|
||||
class="text-black p-4 outline-none border-none bg-transparent"
|
||||
[routerLink]="closeFilterRoute"
|
||||
queryParamsHandling="preserve"
|
||||
>
|
||||
<ui-icon icon="close" size="15px"></ui-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="catalog-search-filter-content-main">
|
||||
<h1 class="text-3xl font-bold text-center py-4">Filter</h1>
|
||||
<ui-filter
|
||||
<div class="catalog-search-filter-content-main -mt-14 desktop-small:-mt-8 desktop:-mt-12">
|
||||
<h1 class="text-h3 text-[1.625rem] font-bold text-center pt-6 pb-10">Filter</h1>
|
||||
<shared-filter
|
||||
[filter]="filter"
|
||||
[loading]="fetching$ | async"
|
||||
(search)="applyFilter(filter)"
|
||||
[hint]="searchboxHint$ | async"
|
||||
resizeInputOptionsToElement="page-article-search-filter .cta-wrapper"
|
||||
[scanner]="true"
|
||||
></ui-filter>
|
||||
></shared-filter>
|
||||
</div>
|
||||
|
||||
<div class="cta-wrapper">
|
||||
<button class="cta-reset-filter" (click)="resetFilter(filter)" [disabled]="fetching$ | async">
|
||||
Filter zurücksetzen
|
||||
</button>
|
||||
|
||||
<button class="cta-apply-filter" (click)="applyFilter(filter)" [disabled]="fetching$ | async">
|
||||
<ui-spinner [show]="fetching$ | async">
|
||||
Filter anwenden
|
||||
</ui-spinner>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cta-wrapper">
|
||||
<button class="cta-reset-filter" (click)="resetFilter(filter)" [disabled]="fetching$ | async">
|
||||
Filter zurücksetzen
|
||||
</button>
|
||||
|
||||
<button class="cta-apply-filter" (click)="applyFilter(filter)" [disabled]="fetching$ | async">
|
||||
<ui-spinner [show]="fetching$ | async">
|
||||
Filter anwenden
|
||||
</ui-spinner>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
:host {
|
||||
@apply block bg-glitter;
|
||||
@apply block bg-white h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
|
||||
}
|
||||
|
||||
.catalog-search-filter-content {
|
||||
@apply relative mx-auto p-4;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
@apply absolute text-cool-grey top-3 p-4 right-4 outline-none border-none bg-transparent;
|
||||
}
|
||||
|
||||
.catalog-search-filter-content-main {
|
||||
h1.title {
|
||||
@apply text-center;
|
||||
@@ -17,14 +13,12 @@
|
||||
}
|
||||
|
||||
.cta-wrapper {
|
||||
@apply fixed bottom-8 whitespace-nowrap;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
@apply text-center whitespace-nowrap absolute bottom-8 left-0 w-full;
|
||||
}
|
||||
|
||||
.cta-reset-filter,
|
||||
.cta-apply-filter {
|
||||
@apply text-lg font-bold px-6 py-3 rounded-full border-solid border-2 border-brand outline-none mx-2;
|
||||
@apply text-lg font-bold px-6 py-[0.85rem] rounded-full border-solid border-2 border-brand outline-none mx-2;
|
||||
|
||||
&:disabled {
|
||||
@apply bg-inactive-branch cursor-not-allowed border-none text-white;
|
||||
@@ -38,3 +32,7 @@
|
||||
.cta-apply-filter {
|
||||
@apply text-white bg-brand;
|
||||
}
|
||||
|
||||
::ng-deep page-article-search-filter shared-filter shared-filter-input-group-main {
|
||||
@apply desktop:hidden px-16;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { UiFilter, UiFilterComponent } from '@ui/filter';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { map, takeUntil, withLatestFrom } from 'rxjs/operators';
|
||||
import { ArticleSearchService } from '../article-search.store';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { Filter, FilterComponent } from 'apps/shared/components/filter/src/lib';
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-search-filter',
|
||||
@@ -10,51 +14,102 @@ import { ArticleSearchService } from '../article-search.store';
|
||||
styleUrls: ['search-filter.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ArticleSearchFilterComponent implements OnInit {
|
||||
export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
|
||||
@Output()
|
||||
close = new EventEmitter();
|
||||
|
||||
_processId$ = this._activatedRoute.parent.data.pipe(map((data) => Number(data.processId)));
|
||||
|
||||
fetching$: Observable<boolean>;
|
||||
|
||||
filter$: Observable<UiFilter>;
|
||||
filter$: Observable<Filter>;
|
||||
|
||||
searchboxHint$ = this.articleSearch.searchboxHint$;
|
||||
|
||||
@ViewChild(UiFilterComponent, { static: false })
|
||||
uiFilterComponent: UiFilterComponent;
|
||||
@ViewChild(FilterComponent, { static: false })
|
||||
uiFilterComponent: FilterComponent;
|
||||
|
||||
constructor(private articleSearch: ArticleSearchService) {}
|
||||
get isDesktop() {
|
||||
return this._environment.matchDesktop();
|
||||
}
|
||||
|
||||
get showFilterClose$() {
|
||||
return this._environment.matchDesktop$.pipe(map((state) => !(state?.matches && this.leftOutlet === 'search')));
|
||||
}
|
||||
|
||||
get leftOutlet() {
|
||||
return this._navigationService.getOutletLocations(this._activatedRoute)?.left;
|
||||
}
|
||||
|
||||
get mainOutlet() {
|
||||
return this._navigationService.getOutletLocations(this._activatedRoute)?.main;
|
||||
}
|
||||
|
||||
get closeFilterRoute() {
|
||||
const processId = Number(this._activatedRoute?.parent?.snapshot?.data?.processId);
|
||||
const itemId = this._navigationService.getOutletParams(this._activatedRoute)?.right?.id;
|
||||
if (!itemId) {
|
||||
if (this.leftOutlet === 'search') {
|
||||
return this._navigationService.getArticleSearchBasePath(processId);
|
||||
} else if (this.mainOutlet === 'results' || this.leftOutlet === 'results') {
|
||||
return this._navigationService.getArticleSearchResultsPath(processId);
|
||||
}
|
||||
} else {
|
||||
return this._navigationService.getArticleDetailsPath({ processId, itemId });
|
||||
}
|
||||
}
|
||||
|
||||
private _onDestroy$ = new Subject();
|
||||
|
||||
constructor(
|
||||
private articleSearch: ArticleSearchService,
|
||||
private _environment: EnvironmentService,
|
||||
private _activatedRoute: ActivatedRoute,
|
||||
public application: ApplicationService,
|
||||
private _navigationService: ProductCatalogNavigationService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.fetching$ = this.articleSearch.fetching$;
|
||||
this.filter$ = this.articleSearch.filter$.pipe(
|
||||
map((filter) => UiFilter.create(filter))
|
||||
// tap((filter) =>
|
||||
// filter.fromQueryParams({
|
||||
// main_qs: 'harry potter',
|
||||
// filter_format: 'eb;!hc',
|
||||
// filter_dbhwgr: '110;121',
|
||||
// main_author: 'author',
|
||||
// filter_region: '9780*|9781*;97884*',
|
||||
// 'filter_reading-age': '1-10',
|
||||
// main_stock: '1-',
|
||||
// })
|
||||
// )
|
||||
);
|
||||
this.filter$ = this.articleSearch.filter$.pipe(map((filter) => Filter.create(filter)));
|
||||
}
|
||||
|
||||
applyFilter(value: UiFilter) {
|
||||
ngOnDestroy(): void {
|
||||
this._onDestroy$.next();
|
||||
this._onDestroy$.complete();
|
||||
}
|
||||
|
||||
applyFilter(value: Filter) {
|
||||
this.uiFilterComponent?.cancelAutocomplete();
|
||||
this.articleSearch.setFilter(value);
|
||||
this.articleSearch.search({ clear: true });
|
||||
this.articleSearch.searchCompleted.pipe(take(1)).subscribe((s) => {
|
||||
if (s.searchState === '') {
|
||||
this.close.emit();
|
||||
}
|
||||
});
|
||||
this.articleSearch.searchCompleted
|
||||
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._processId$))
|
||||
.subscribe(([state, processId]) => {
|
||||
if (state.searchState === '') {
|
||||
// Check if desktop is necessary, otherwise it would trigger navigation twice (Inside Article-Search.component and here)
|
||||
if (state.hits === 1 && !this.isDesktop) {
|
||||
const item = state.items.find((f) => f);
|
||||
this._navigationService.navigateToDetails({
|
||||
processId,
|
||||
itemId: item.id,
|
||||
});
|
||||
} else if (!this.isDesktop) {
|
||||
const params = state.filter.getQueryParams();
|
||||
this._navigationService.navigateToResults({
|
||||
processId,
|
||||
queryParams: params,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resetFilter(value: UiFilter) {
|
||||
clearFilter(value: Filter) {
|
||||
value.unselectAll();
|
||||
}
|
||||
|
||||
resetFilter(value: Filter) {
|
||||
const queryParams = { main_qs: value?.getQueryParams()?.main_qs || '' };
|
||||
this.articleSearch.setDefaultFilter(queryParams);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { UiFilterNextModule } from '@ui/filter';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { UiSpinnerModule } from '@ui/spinner';
|
||||
|
||||
import { ArticleSearchFilterComponent } from './search-filter.component';
|
||||
import { FilterNextModule } from 'apps/shared/components/filter/src/lib';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiFilterNextModule, UiIconModule, UiSpinnerModule],
|
||||
imports: [CommonModule, RouterModule, FilterNextModule, UiIconModule, UiSpinnerModule],
|
||||
exports: [ArticleSearchFilterComponent],
|
||||
declarations: [ArticleSearchFilterComponent],
|
||||
providers: [],
|
||||
|
||||
@@ -1,26 +1,50 @@
|
||||
<div class="card-search-article">
|
||||
<h1 class="title">Artikelsuche</h1>
|
||||
<p class="info">
|
||||
<div class="bg-white rounded py-10 px-4 text-center shadow-[0_-2px_24px_0_#dce2e9] h-full">
|
||||
<h1 class="text-h3 text-[1.625rem] font-bold mb-[0.375rem]">Artikelsuche</h1>
|
||||
<p class="text-lg mb-10">
|
||||
Welchen Artikel suchen Sie?
|
||||
</p>
|
||||
<ng-container *ngIf="filter$ | async; let filter">
|
||||
<ui-filter-filter-group-main [inputGroup]="filter?.filter | group: 'main'"></ui-filter-filter-group-main>
|
||||
<ui-filter-input-group-main
|
||||
[hint]="searchboxHint$ | async"
|
||||
[loading]="fetching$ | async"
|
||||
[inputGroup]="filter?.input | group: 'main'"
|
||||
(search)="search(filter)"
|
||||
[showDescription]="false"
|
||||
[scanner]="true"
|
||||
></ui-filter-input-group-main>
|
||||
<shared-filter-filter-group-main
|
||||
class="mb-8 w-full"
|
||||
*ngIf="!(isDesktop$ | async)"
|
||||
[inputGroup]="filter?.filter | group: 'main'"
|
||||
></shared-filter-filter-group-main>
|
||||
<div class="flex flex-row px-12 justify-center desktop:px-0">
|
||||
<shared-filter-input-group-main
|
||||
class="block w-full mr-3 desktop:mx-auto"
|
||||
[hint]="searchboxHint$ | async"
|
||||
[loading]="fetching$ | async"
|
||||
[inputGroup]="filter?.input | group: 'main'"
|
||||
(search)="search(filter)"
|
||||
[showDescription]="false"
|
||||
[scanner]="true"
|
||||
></shared-filter-input-group-main>
|
||||
<a
|
||||
*ngIf="!(isDesktop$ | async)"
|
||||
class="page-search-main__filter w-[6.75rem] h-14 rounded font-bold px-5 mb-4 text-lg bg-[#AEB7C1] flex flex-row flex-nowrap items-center justify-center"
|
||||
[class.active]="hasFilter$ | async"
|
||||
[routerLink]="openFilterRoute"
|
||||
queryParamsHandling="preserve"
|
||||
>
|
||||
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
|
||||
Filter
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="recent-searches-wrapper">
|
||||
<h3 class="recent-searches-header">Deine letzten Suchanfragen</h3>
|
||||
<ul>
|
||||
<li class="recent-searches-items" *ngFor="let recentQuery of history$ | async">
|
||||
<button (click)="setQueryHistory(filter, recentQuery.friendlyName)">
|
||||
<ui-icon icon="search" size="15px"></ui-icon>
|
||||
<p>{{ recentQuery.friendlyName }}</p>
|
||||
<div class="flex flex-col items-start ml-12 desktop:ml-8 py-6 bg-white">
|
||||
<h3 class="text-p3 font-bold mb-3">Deine letzten Suchanfragen</h3>
|
||||
<ul class="flex flex-col justify-start overflow-hidden items-start m-0 p-0 bg-white w-full">
|
||||
<li class="list-none pb-3" *ngFor="let recentQuery of history$ | async">
|
||||
<button
|
||||
class="flex flex-row items-center outline-none border-none bg-white text-black text-p2 m-0 p-0"
|
||||
(click)="setQueryHistory(filter, recentQuery.friendlyName)"
|
||||
>
|
||||
<ui-icon
|
||||
class="flex w-8 h-8 justify-center items-center mr-3 rounded-full text-black bg-[#edeff0]"
|
||||
icon="search"
|
||||
size="0.875rem"
|
||||
></ui-icon>
|
||||
<p class="m-0 p-0 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-[25rem]">{{ recentQuery.friendlyName }}</p>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,71 +1,13 @@
|
||||
:host {
|
||||
@apply flex flex-col box-border;
|
||||
@apply flex flex-col box-border h-full;
|
||||
}
|
||||
|
||||
.title {
|
||||
@apply text-page-heading font-bold;
|
||||
}
|
||||
|
||||
.info {
|
||||
@apply text-2xl mt-1 mb-px-30;
|
||||
}
|
||||
|
||||
.filter-chips {
|
||||
@apply flex flex-row justify-center;
|
||||
}
|
||||
|
||||
.card-search-article {
|
||||
@apply bg-white rounded p-4 text-center;
|
||||
|
||||
box-shadow: 0 -2px 24px 0 #dce2e9;
|
||||
}
|
||||
|
||||
.card-search-article {
|
||||
min-height: calc(100vh - 380px);
|
||||
}
|
||||
|
||||
ui-filter-filter-group-main {
|
||||
@apply mb-8 w-full;
|
||||
.page-search-main__filter {
|
||||
&.active {
|
||||
@apply bg-[#596470] text-white ml-px-5;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep page-article-search-main ui-filter-filter-group-main .ui-filter-chip:not(.selected) {
|
||||
@apply bg-glitter;
|
||||
}
|
||||
|
||||
ui-filter-input-group-main {
|
||||
@apply block mx-auto;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.recent-searches-wrapper {
|
||||
@apply flex flex-col mx-auto items-start py-6 bg-white;
|
||||
width: 50%;
|
||||
z-index: 0;
|
||||
|
||||
.recent-searches-header {
|
||||
@apply text-sm font-bold mb-4;
|
||||
}
|
||||
|
||||
ul {
|
||||
@apply flex flex-col justify-start overflow-hidden items-start m-0 p-0 bg-white w-full;
|
||||
z-index: 0;
|
||||
|
||||
.recent-searches-items {
|
||||
@apply list-none pb-px-15;
|
||||
|
||||
button {
|
||||
@apply flex flex-row items-center outline-none border-none bg-white text-black text-base m-0 p-0;
|
||||
|
||||
ui-icon {
|
||||
@apply flex w-px-35 h-px-35 justify-center items-center mr-3 rounded-full text-black;
|
||||
background-color: #e6eff9;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply m-0 p-0 whitespace-nowrap overflow-hidden overflow-ellipsis;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { DomainCatalogService } from '@domain/catalog';
|
||||
import { UiFilter, UiFilterInputGroupMainComponent } from '@ui/filter';
|
||||
import { combineLatest, NEVER, Subscription } from 'rxjs';
|
||||
import { catchError, debounceTime, first, switchMap } from 'rxjs/operators';
|
||||
import { catchError, debounceTime, first, switchMap, map, shareReplay } from 'rxjs/operators';
|
||||
import { ArticleSearchService } from '../article-search.store';
|
||||
import { isEqual } from 'lodash';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/filter/src/lib';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-search-main',
|
||||
@@ -26,15 +28,47 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
|
||||
|
||||
subscriptions = new Subscription();
|
||||
|
||||
@ViewChild(UiFilterInputGroupMainComponent, { static: false })
|
||||
uiInputGroupMain: UiFilterInputGroupMainComponent;
|
||||
hasFilter$ = combineLatest([this.searchService.filter$, this.searchService.defaultSettings$]).pipe(
|
||||
map(([filter, defaultFilter]) => {
|
||||
const filterQueryParams = filter?.getQueryParams();
|
||||
return !isEqual(this.resetQueryParamsQueryAndOrderBy(filterQueryParams), Filter.create(defaultFilter).getQueryParams());
|
||||
})
|
||||
);
|
||||
|
||||
@ViewChild(FilterInputGroupMainComponent, { static: false })
|
||||
sharedFilterInputGroupMain: FilterInputGroupMainComponent;
|
||||
|
||||
get isDesktop$() {
|
||||
return this._environment.matchDesktop$.pipe(
|
||||
map((state) => state?.matches),
|
||||
shareReplay()
|
||||
);
|
||||
}
|
||||
|
||||
get leftOutlet() {
|
||||
return this._navigationService.getOutletLocations(this.route.parent)?.left;
|
||||
}
|
||||
|
||||
get openFilterRoute() {
|
||||
if (this.leftOutlet === 'search') {
|
||||
return this._navigationService.getArticleSearchBaseAndFilterPath(this.application.activatedProcessId);
|
||||
} else {
|
||||
const itemId = this._navigationService?.getOutletParams(this.route)?.right?.id;
|
||||
return this._navigationService.getArticleSearchResultsAndFilterPath({
|
||||
processId: this.application.activatedProcessId,
|
||||
itemId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private searchService: ArticleSearchService,
|
||||
private catalog: DomainCatalogService,
|
||||
private route: ActivatedRoute,
|
||||
private application: ApplicationService,
|
||||
private breadcrumb: BreadcrumbService
|
||||
private breadcrumb: BreadcrumbService,
|
||||
private _environment: EnvironmentService,
|
||||
private _navigationService: ProductCatalogNavigationService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -44,7 +78,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
|
||||
.subscribe(async ([processId, queryParams]) => {
|
||||
const processChanged = processId !== this.searchService.processId;
|
||||
|
||||
if (!(this.searchService.filter instanceof UiFilter)) {
|
||||
if (!(this.searchService.filter instanceof Filter)) {
|
||||
await this.searchService.setDefaultFilter();
|
||||
}
|
||||
|
||||
@@ -57,6 +91,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
|
||||
const cleanQueryParams = this.cleanupQueryParams(queryParams);
|
||||
|
||||
if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) {
|
||||
// Reset Filter on Product Search Shell Navigation click
|
||||
await this.searchService.setDefaultFilter(queryParams);
|
||||
}
|
||||
|
||||
@@ -85,16 +120,30 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
|
||||
this.updateBreadcrumb(this.searchService.processId, this.searchService.filter.getQueryParams());
|
||||
}
|
||||
|
||||
search(filter: UiFilter) {
|
||||
this.uiInputGroupMain.cancelAutocomplete();
|
||||
search(filter: Filter) {
|
||||
this.sharedFilterInputGroupMain.cancelAutocomplete();
|
||||
this.searchService.setFilter(filter);
|
||||
this.searchService.search({ clear: true });
|
||||
}
|
||||
|
||||
setQueryHistory(filter: UiFilter, query: string) {
|
||||
setQueryHistory(filter: Filter, query: string) {
|
||||
filter.fromQueryParams({ main_qs: query });
|
||||
}
|
||||
|
||||
resetQueryParamsQueryAndOrderBy(params: Record<string, string> = {}) {
|
||||
const clean = { ...params };
|
||||
|
||||
for (const key in clean) {
|
||||
if (key === 'main_qs' || key?.includes('order_by')) {
|
||||
clean[key] = undefined;
|
||||
} else if (key?.includes('order_by')) {
|
||||
delete clean[key];
|
||||
}
|
||||
}
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
||||
cleanupQueryParams(params: Record<string, string> = {}) {
|
||||
const clean = { ...params };
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { UiFilterNextModule } from '@ui/filter';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { ArticleSearchMainComponent } from './search-main.component';
|
||||
import { FilterNextModule } from 'apps/shared/components/filter/src/lib';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiIconModule, UiFilterNextModule],
|
||||
imports: [CommonModule, RouterModule, UiIconModule, FilterNextModule],
|
||||
exports: [ArticleSearchMainComponent],
|
||||
declarations: [ArticleSearchMainComponent],
|
||||
providers: [],
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
<div class="thumbnail animation"></div>
|
||||
<div class="col">
|
||||
<div class="author animation"></div>
|
||||
<div class="row">
|
||||
<div class="title animation"></div>
|
||||
<div class="price animation"></div>
|
||||
<ng-container *ngIf="!(mainOutletActive$ | async); else mainOutlet">
|
||||
<div class="bg-ucla-blue rounded w-[4.375rem] h-[5.625rem] animate-[load_1s_linear_infinite]"></div>
|
||||
<div class="flex flex-col flex-grow">
|
||||
<div class="h-4 bg-ucla-blue ml-4 mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
|
||||
<div class="flex flex-row justify-between flex-grow">
|
||||
<div class="h-6 bg-ucla-blue ml-4 w-[12.5rem] animate-[load_1s_linear_infinite]"></div>
|
||||
<div class="h-6 bg-ucla-blue ml-4 w-[4.6875rem] animate-[load_1s_linear_infinite]"></div>
|
||||
</div>
|
||||
<div class="flex-grow"></div>
|
||||
<div class="flex flex-row justify-between flex-grow">
|
||||
<div class="h-4 bg-ucla-blue ml-4 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
|
||||
<div class="h-4 bg-ucla-blue ml-4 w-[3.125rem] animate-[load_1s_linear_infinite]"></div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between flex-grow">
|
||||
<div class="h-4 bg-ucla-blue ml-4 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
|
||||
<div class="h-4 bg-ucla-blue ml-4 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space"></div>
|
||||
<div class="row">
|
||||
<div class="category animation"></div>
|
||||
<div class="stock animation"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #mainOutlet>
|
||||
<div class="bg-ucla-blue rounded w-[3rem] h-[4.125rem] animate-[load_1s_linear_infinite]"></div>
|
||||
<div class="flex flex-col ml-4 w-[36.6%]">
|
||||
<div class="h-4 bg-ucla-blue mb-2 w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
|
||||
<div class="h-6 bg-ucla-blue mb-2 w-[13.5rem] animate-[load_1s_linear_infinite]"></div>
|
||||
<div class="h-6 bg-ucla-blue w-[13.5rem] animate-[load_1s_linear_infinite]"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="manufacturer animation"></div>
|
||||
<div class="ava animation"></div>
|
||||
<div class="flex flex-col w-[32.5%]">
|
||||
<div class="h-4 bg-ucla-blue mb-2 w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
|
||||
<div class="h-4 bg-ucla-blue mb-2 w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
|
||||
<div class="h-4 bg-ucla-blue w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-[20%]">
|
||||
<div class="h-4 bg-ucla-blue mb-2 w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
|
||||
<div class="h-4 bg-ucla-blue mb-2 w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
|
||||
<div class="h-4 bg-ucla-blue w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,61 +1,3 @@
|
||||
:host {
|
||||
@apply flex flex-row rounded-card bg-white mb-2 p-4;
|
||||
height: 187px;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 70px;
|
||||
height: 90px;
|
||||
@apply bg-ucla-blue rounded-card;
|
||||
}
|
||||
|
||||
.space {
|
||||
@apply flex-grow;
|
||||
}
|
||||
|
||||
.col {
|
||||
@apply flex flex-col flex-grow;
|
||||
}
|
||||
|
||||
.row {
|
||||
@apply flex flex-row justify-between flex-grow;
|
||||
}
|
||||
|
||||
.author {
|
||||
width: 150px;
|
||||
@apply h-4 bg-ucla-blue ml-4 mb-2;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 300px;
|
||||
@apply h-6 bg-ucla-blue ml-4;
|
||||
}
|
||||
|
||||
.price {
|
||||
width: 100px;
|
||||
@apply h-6 bg-ucla-blue ml-4;
|
||||
}
|
||||
|
||||
.category {
|
||||
width: 200px;
|
||||
@apply h-4 bg-ucla-blue ml-4;
|
||||
}
|
||||
|
||||
.manufacturer {
|
||||
width: 200px;
|
||||
@apply h-4 bg-ucla-blue ml-4;
|
||||
}
|
||||
|
||||
.stock {
|
||||
width: 75px;
|
||||
@apply h-4 bg-ucla-blue ml-4;
|
||||
}
|
||||
|
||||
.ava {
|
||||
width: 150px;
|
||||
@apply h-4 bg-ucla-blue ml-4;
|
||||
}
|
||||
|
||||
.animation {
|
||||
animation: load 1s linear infinite;
|
||||
@apply flex flex-row rounded bg-white mb-2 p-4 w-full h-[212px] desktop-small:h-[181px];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, HostBinding } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'page-search-result-item-loading',
|
||||
@@ -7,5 +10,13 @@ import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SearchResultItemLoadingComponent {
|
||||
constructor() {}
|
||||
get mainOutletActive$() {
|
||||
return this._navigationService?.mainOutletActive$(this._activatedRoute).pipe(shareReplay());
|
||||
}
|
||||
|
||||
constructor(private _navigationService: ProductCatalogNavigationService, private _activatedRoute: ActivatedRoute) {}
|
||||
|
||||
@HostBinding('style') get class() {
|
||||
return this._navigationService.mainOutletActive(this._activatedRoute) ? { height: '6.125rem' } : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +1,126 @@
|
||||
<a class="product-list-result-content" [routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'details', item?.id]">
|
||||
<div class="item-thumbnail">
|
||||
<img loading="lazy" *ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl" [src]="thumbnailUrl" [alt]="item?.product?.name" />
|
||||
</div>
|
||||
|
||||
<div class="item-contributors">
|
||||
<a
|
||||
*ngFor="let contributor of contributors; let last = last"
|
||||
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'search', 'results']"
|
||||
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
|
||||
(click)="$event?.stopPropagation()"
|
||||
>
|
||||
{{ contributor }}{{ last ? '' : ';' }}
|
||||
</a>
|
||||
<a
|
||||
class="page-search-result-item__item-card hover p-5 desktop-small:px-4 desktop-small:py-[0.625rem] h-[13.25rem] desktop-small:h-[11.3125rem] bg-white border border-solid border-transparent rounded"
|
||||
[class.page-search-result-item__item-card-main]="mainOutletActive$ | async"
|
||||
[routerLink]="detailsPath"
|
||||
[routerLinkActive]="!isTablet && !(mainOutletActive$ | async) ? 'active' : ''"
|
||||
[queryParamsHandling]="!isTablet ? 'preserve' : ''"
|
||||
>
|
||||
<div class="page-search-result-item__item-thumbnail text-center mr-4 w-[50px] h-[79px]">
|
||||
<img
|
||||
class="page-search-result-item__item-image w-[50px] h-[79px]"
|
||||
loading="lazy"
|
||||
*ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl"
|
||||
[src]="thumbnailUrl"
|
||||
[alt]="item?.product?.name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="item-title"
|
||||
[class.xl]="item?.product?.name?.length >= 35"
|
||||
[class.lg]="item?.product?.name?.length >= 40"
|
||||
[class.md]="item?.product?.name?.length >= 50"
|
||||
[class.sm]="item?.product?.name?.length >= 60"
|
||||
[class.xs]="item?.product?.name?.length >= 100"
|
||||
class="page-search-result-item__item-grid-container"
|
||||
[class.page-search-result-item__item-grid-container-main]="mainOutletActive$ | async"
|
||||
>
|
||||
{{ item?.product?.name }}
|
||||
</div>
|
||||
<div
|
||||
class="page-search-result-item__item-contributors desktop-small:text-p3 font-bold text-[#0556B4] text-ellipsis overflow-hidden max-w-[24rem] whitespace-nowrap"
|
||||
>
|
||||
<a
|
||||
*ngFor="let contributor of contributors; let last = last"
|
||||
[routerLink]="resultsPath"
|
||||
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
|
||||
(click)="$event?.stopPropagation()"
|
||||
>
|
||||
{{ contributor }}{{ last ? '' : ';' }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="item-price">
|
||||
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }}
|
||||
</div>
|
||||
<div
|
||||
class="page-search-result-item__item-title font-bold text-h3"
|
||||
[class.text-xl]="item?.product?.name?.length >= 35 && isTablet"
|
||||
[class.text-lg]="item?.product?.name?.length >= 40 && isTablet"
|
||||
[class.text-md]="item?.product?.name?.length >= 50 && isTablet"
|
||||
[class.text-p3]="item?.product?.name?.length >= 60 || !isTablet"
|
||||
[class.text-xs]="item?.product?.name?.length >= 100"
|
||||
>
|
||||
{{ item?.product?.name }}
|
||||
</div>
|
||||
|
||||
<div *ngIf="selectable" class="item-data-selector">
|
||||
<ui-select-bullet [ngModel]="selected" (ngModelChange)="setSelected($event)"></ui-select-bullet>
|
||||
</div>
|
||||
|
||||
<div class="item-stock z-dropdown" [uiOverlayTrigger]="tooltip" [overlayTriggerDisabled]="!(stockTooltipText$ | async)">
|
||||
<ng-container *ngIf="isOrderBranch$ | async">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<ui-icon icon="home" size="1em"></ui-icon>
|
||||
<span
|
||||
*ngIf="inStock$ | async; let stock"
|
||||
[class.skeleton]="stock.inStock === undefined"
|
||||
class="min-w-[1rem] text-right inline-block"
|
||||
>{{ stock?.inStock }}</span
|
||||
>
|
||||
<span>x</span>
|
||||
<div class="page-search-result-item__item-format desktop-small:text-p3">
|
||||
<div *ngIf="item?.product?.format && item?.product?.formatDetail" class="font-bold flex flex-row">
|
||||
<img
|
||||
class="mr-3"
|
||||
*ngIf="item?.product?.format !== '--'"
|
||||
loading="lazy"
|
||||
src="assets/images/Icon_{{ item?.product?.format }}.svg"
|
||||
[alt]="item?.product?.formatDetail"
|
||||
/>
|
||||
{{ item?.product?.formatDetail | substr: 30 }}
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!(isOrderBranch$ | async)">
|
||||
<div class="flex flex-row items-center justify-between z-dropdown">
|
||||
<ui-icon class="block" icon="home" size="1em"></ui-icon>
|
||||
<span class="min-w-[1rem] text-center inline-block">-</span>
|
||||
<span>x</span>
|
||||
</div>
|
||||
|
||||
<div class="page-search-result-item__item-manufacturer desktop-small:text-p3">
|
||||
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }}
|
||||
</div>
|
||||
|
||||
<div class="page-search-result-item__item-misc desktop-small:text-p3">
|
||||
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
|
||||
{{ publicationDate }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="page-search-result-item__item-price desktop-small:text-p3 font-bold justify-self-end"
|
||||
[class.page-search-result-item__item-price-main]="mainOutletActive$ | async"
|
||||
>
|
||||
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }}
|
||||
</div>
|
||||
|
||||
<div class="page-search-result-item__item-select-bullet justify-self-end">
|
||||
<input
|
||||
*ngIf="selectable"
|
||||
(click)="$event.stopPropagation()"
|
||||
[ngModel]="selected$ | async"
|
||||
(ngModelChange)="setSelected()"
|
||||
class="isa-select-bullet"
|
||||
type="checkbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="page-search-result-item__item-stock desktop-small:text-p3 font-bold z-dropdown justify-self-start"
|
||||
[class.justify-self-end]="!(mainOutletActive$ | async)"
|
||||
[uiOverlayTrigger]="tooltip"
|
||||
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
|
||||
>
|
||||
<ng-container *ngIf="isOrderBranch$ | async">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<ui-icon icon="home" size="1em"></ui-icon>
|
||||
<span
|
||||
*ngIf="inStock$ | async; let stock"
|
||||
[class.skeleton]="stock.inStock === undefined"
|
||||
class="min-w-[1rem] text-right inline-block"
|
||||
>{{ stock?.inStock }}</span
|
||||
>
|
||||
<span>x</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!(isOrderBranch$ | async)">
|
||||
<div class="flex flex-row items-center justify-between z-dropdown">
|
||||
<ui-icon class="block" icon="home" size="1em"></ui-icon>
|
||||
<span class="min-w-[1rem] text-center inline-block">-</span>
|
||||
<span>x</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
|
||||
{{ stockTooltipText$ | async }}
|
||||
</ui-tooltip>
|
||||
|
||||
<div
|
||||
class="page-search-result-item__item-ssc desktop-small:text-p3 w-full text-right overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
[class.page-search-result-item__item-ssc-main]="mainOutletActive$ | async"
|
||||
>
|
||||
<div class="hidden" [class.page-search-result-item__item-ssc-tooltip]="mainOutletActive$ | async">
|
||||
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }}
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
|
||||
{{ stockTooltipText$ | async }}
|
||||
</ui-tooltip>
|
||||
<!-- <div class="item-stock"><ui-icon icon="home" size="1em"></ui-icon> {{ item?.stockInfos | stockInfos }} x</div> -->
|
||||
|
||||
<div class="item-ssc" [class.xs]="item?.catalogAvailability?.sscText?.length >= 60">
|
||||
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }}
|
||||
</div>
|
||||
|
||||
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail">
|
||||
<img
|
||||
*ngIf="item?.product?.format !== '--'"
|
||||
loading="lazy"
|
||||
src="assets/images/Icon_{{ item?.product?.format }}.svg"
|
||||
[alt]="item?.product?.formatDetail"
|
||||
/>
|
||||
{{ item?.product?.formatDetail }}
|
||||
</div>
|
||||
|
||||
<div class="item-misc">
|
||||
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }} <br />
|
||||
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
|
||||
{{ publicationDate }}
|
||||
<strong>{{ item?.catalogAvailability?.ssc }}</strong> - {{ item?.catalogAvailability?.sscText }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -1,113 +1,104 @@
|
||||
.product-list-result-content {
|
||||
@apply text-black no-underline grid;
|
||||
grid-template-columns: 102px 50% auto;
|
||||
grid-template-rows: auto;
|
||||
:host {
|
||||
@apply flex flex-col w-full h-[13.25rem] desktop-small:h-[11.3125rem];
|
||||
}
|
||||
|
||||
.page-search-result-item__item-card {
|
||||
@apply grid grid-flow-col;
|
||||
grid-template-columns: 3.9375rem auto;
|
||||
box-shadow: 0px 0px 10px rgba(220, 226, 233, 0.5);
|
||||
}
|
||||
|
||||
.page-search-result-item__item-grid-container {
|
||||
@apply grid grid-flow-row gap-y-[0.375rem] gap-x-6;
|
||||
grid-template-areas:
|
||||
'item-thumbnail item-contributors item-contributors'
|
||||
'item-thumbnail item-title item-price'
|
||||
'item-thumbnail item-title item-data-selector'
|
||||
'item-thumbnail item-format item-stock'
|
||||
'item-thumbnail item-misc item-ssc';
|
||||
'contributors contributors contributors'
|
||||
'title title price'
|
||||
'title title price'
|
||||
'title title select'
|
||||
'format format select'
|
||||
'manufacturer manufacturer stock'
|
||||
'misc ssc ssc';
|
||||
}
|
||||
|
||||
.item-thumbnail {
|
||||
grid-area: item-thumbnail;
|
||||
width: 70px;
|
||||
@apply mr-8;
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 150px;
|
||||
@apply rounded-card shadow-cta;
|
||||
.page-search-result-item__item-grid-container-main {
|
||||
@apply gap-x-4;
|
||||
grid-template-rows: 1.3125rem 1.3125rem auto;
|
||||
grid-template-columns: 37.5% 32.5% 20% auto;
|
||||
grid-template-areas:
|
||||
'contributors format price .'
|
||||
'title manufacturer stock select'
|
||||
'title misc ssc .';
|
||||
}
|
||||
|
||||
.page-search-result-item__item-contributors {
|
||||
grid-area: contributors;
|
||||
}
|
||||
|
||||
.page-search-result-item__item-price {
|
||||
grid-area: price;
|
||||
}
|
||||
|
||||
.page-search-result-item__item-price-main {
|
||||
@apply justify-self-start;
|
||||
}
|
||||
|
||||
.page-search-result-item__item-title {
|
||||
grid-area: title;
|
||||
}
|
||||
|
||||
.page-search-result-item__item-format {
|
||||
grid-area: format;
|
||||
}
|
||||
|
||||
.page-search-result-item__item-manufacturer {
|
||||
grid-area: manufacturer;
|
||||
}
|
||||
|
||||
.page-search-result-item__item-misc {
|
||||
grid-area: misc;
|
||||
}
|
||||
|
||||
.page-search-result-item__item-select-bullet {
|
||||
grid-area: select;
|
||||
}
|
||||
|
||||
.page-search-result-item__item-stock {
|
||||
grid-area: stock;
|
||||
}
|
||||
|
||||
.page-search-result-item__item-ssc {
|
||||
grid-area: ssc;
|
||||
}
|
||||
|
||||
.page-search-result-item__item-ssc:hover {
|
||||
.page-search-result-item__item-ssc-tooltip {
|
||||
@apply absolute whitespace-normal w-[25%] block rounded bg-white z-tooltip p-2 -mt-[2rem] -ml-[4rem] text-p2 shadow;
|
||||
}
|
||||
}
|
||||
|
||||
.item-contributors {
|
||||
grid-area: item-contributors;
|
||||
height: 22px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 600px;
|
||||
white-space: nowrap;
|
||||
.page-search-result-item__item-ssc-main {
|
||||
@apply text-left overflow-hidden text-ellipsis whitespace-nowrap;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-active-customer font-bold no-underline;
|
||||
}
|
||||
}
|
||||
|
||||
.item-title {
|
||||
grid-area: item-title;
|
||||
@apply font-bold text-2xl;
|
||||
height: 64px;
|
||||
max-height: 64px;
|
||||
}
|
||||
|
||||
.item-title.xl {
|
||||
@apply font-bold text-xl;
|
||||
}
|
||||
|
||||
.item-title.lg {
|
||||
@apply font-bold text-lg;
|
||||
}
|
||||
|
||||
.item-title.md {
|
||||
@apply font-bold text-base;
|
||||
}
|
||||
|
||||
.item-title.sm {
|
||||
@apply font-bold text-sm;
|
||||
}
|
||||
|
||||
.item-title.xs {
|
||||
@apply font-bold text-xs;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
grid-area: item-price;
|
||||
@apply font-bold text-xl text-right;
|
||||
}
|
||||
|
||||
.item-format {
|
||||
grid-area: item-format;
|
||||
@apply flex flex-row items-center font-bold text-lg whitespace-nowrap;
|
||||
|
||||
img {
|
||||
@apply mr-2;
|
||||
}
|
||||
}
|
||||
|
||||
.item-stock {
|
||||
grid-area: item-stock;
|
||||
@apply flex flex-row justify-end items-baseline font-bold text-lg;
|
||||
|
||||
ui-icon {
|
||||
@apply text-active-customer mr-2;
|
||||
}
|
||||
}
|
||||
|
||||
.item-misc {
|
||||
grid-area: item-misc;
|
||||
}
|
||||
|
||||
.item-ssc {
|
||||
grid-area: item-ssc;
|
||||
@apply font-bold text-right;
|
||||
}
|
||||
|
||||
.item-ssc.xs {
|
||||
@apply font-bold text-xs;
|
||||
}
|
||||
|
||||
.item-data-selector {
|
||||
@apply w-full flex justify-end;
|
||||
grid-area: item-data-selector;
|
||||
}
|
||||
|
||||
ui-select-bullet {
|
||||
@apply p-4 -m-4 z-dropdown;
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.item-contributors {
|
||||
max-width: 780px;
|
||||
.page-search-result-item__item-image {
|
||||
box-shadow: 0px 6px 18px rgba(0, 0, 0, 0.197935);
|
||||
}
|
||||
|
||||
.active,
|
||||
.hover:hover {
|
||||
@apply bg-[#D8DFE5] border border-solid border-[#0556B4];
|
||||
|
||||
.page-search-result-item__item-select-bullet {
|
||||
.isa-select-bullet::before {
|
||||
@apply bg-[#fff];
|
||||
}
|
||||
|
||||
.isa-select-bullet:checked:before {
|
||||
@apply bg-[#596470];
|
||||
}
|
||||
|
||||
.isa-select-bullet:hover::before {
|
||||
@apply bg-[#778490];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output, HostListener, HostBinding } from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { DomainAvailabilityService, DomainInStockService } from '@domain/availability';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { ItemDTO } from '@swagger/cat';
|
||||
import { DateAdapter } from '@ui/common';
|
||||
import { isEqual } from 'lodash';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { debounceTime, switchMap, map, tap, shareReplay } from 'rxjs/operators';
|
||||
import { debounceTime, switchMap, map, shareReplay, filter, first } from 'rxjs/operators';
|
||||
import { ArticleSearchService } from '../article-search.store';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
export interface SearchResultItemComponentState {
|
||||
item?: ItemDTO;
|
||||
@@ -36,16 +39,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
|
||||
readonly item$ = this.select((s) => s.item);
|
||||
|
||||
@Input()
|
||||
get selected() {
|
||||
return this.get((s) => s.selected);
|
||||
}
|
||||
set selected(selected: boolean) {
|
||||
if (this.selected !== selected) {
|
||||
this.patchState({ selected });
|
||||
}
|
||||
}
|
||||
readonly selected$ = this.select((s) => s.selected);
|
||||
selected$ = this._articleSearchService.selectedItemIds$.pipe(map((selectedItemIds) => selectedItemIds.includes(this.item?.id)));
|
||||
|
||||
@Input()
|
||||
get selectable() {
|
||||
@@ -58,7 +52,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
}
|
||||
|
||||
@Output()
|
||||
selectedChange = new EventEmitter<boolean>();
|
||||
selectedChange = new EventEmitter<ItemDTO>();
|
||||
|
||||
get contributors() {
|
||||
return this.item?.product?.contributors?.split(';').map((val) => val.trim());
|
||||
@@ -77,6 +71,22 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
return '';
|
||||
}
|
||||
|
||||
get isTablet() {
|
||||
return this._environment.matchTablet();
|
||||
}
|
||||
|
||||
get detailsPath() {
|
||||
return this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: this.item?.id });
|
||||
}
|
||||
|
||||
get resultsPath() {
|
||||
return this._navigationService.getArticleSearchResultsPath(this.applicationService.activatedProcessId);
|
||||
}
|
||||
|
||||
get mainOutletActive$() {
|
||||
return this._navigationService?.mainOutletActive$(this._activatedRoute).pipe(shareReplay());
|
||||
}
|
||||
|
||||
defaultBranch$ = this._availability.getDefaultBranch();
|
||||
|
||||
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
|
||||
@@ -110,9 +120,11 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
|
||||
inStock$ = combineLatest([this.item$, this.selectedBranchId$, this.defaultBranch$]).pipe(
|
||||
debounceTime(100),
|
||||
filter(([item, branch, defaultBranch]) => !!item && !!defaultBranch),
|
||||
switchMap(([item, branch, defaultBranch]) =>
|
||||
this._stockService.getInStock$({ itemId: item.id, branchId: branch?.id ?? defaultBranch?.id })
|
||||
)
|
||||
),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
constructor(
|
||||
@@ -121,7 +133,10 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
private _articleSearchService: ArticleSearchService,
|
||||
public applicationService: ApplicationService,
|
||||
private _stockService: DomainInStockService,
|
||||
private _availability: DomainAvailabilityService
|
||||
private _availability: DomainAvailabilityService,
|
||||
private _environment: EnvironmentService,
|
||||
private _navigationService: ProductCatalogNavigationService,
|
||||
private _activatedRoute: ActivatedRoute
|
||||
) {
|
||||
super({
|
||||
selected: false,
|
||||
@@ -129,7 +144,16 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
});
|
||||
}
|
||||
|
||||
setSelected(selected: boolean) {
|
||||
this._articleSearchService.setSelected({ selected, itemId: this.item?.id });
|
||||
setSelected() {
|
||||
const isSelected = this._articleSearchService.selectedItemIds.includes(this.item?.id);
|
||||
this._articleSearchService.setSelected({ selected: !isSelected, itemId: this.item?.id });
|
||||
|
||||
if (!this.isTablet) {
|
||||
this.selectedChange.emit(this.item);
|
||||
}
|
||||
}
|
||||
|
||||
@HostBinding('style') get class() {
|
||||
return this._navigationService.mainOutletActive(this._activatedRoute) ? { height: '6.125rem' } : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,77 @@
|
||||
<div class="filter-wrapper">
|
||||
<div class="hits" *ngIf="hits$ | async; let hits">{{ hits }} Titel</div>
|
||||
<ui-order-by-filter [orderBy]="(filter$ | async)?.orderBy" (selectedOrderByChange)="search(); updateBreadcrumbs()"> </ui-order-by-filter>
|
||||
<div
|
||||
class="page-search-results__header bg-background-liste flex items-end justify-between"
|
||||
[class.pb-4]="!(mainOutletActive$ | async)"
|
||||
[class.flex-col]="!(mainOutletActive$ | async)"
|
||||
>
|
||||
<div class="flex flex-row w-full desktop-small:w-min" [class.desktop:w-full]="!(mainOutletActive$ | async)">
|
||||
<shared-filter-input-group-main
|
||||
*ngIf="filter$ | async; let filter"
|
||||
class="block mr-3 w-full desktop-small:w-[23.5rem]"
|
||||
[class.desktop:w-full]="!(mainOutletActive$ | async)"
|
||||
[hint]="searchboxHint$ | async"
|
||||
[loading]="fetching$ | async"
|
||||
[inputGroup]="filter?.input | group: 'main'"
|
||||
(search)="search(filter)"
|
||||
[showDescription]="false"
|
||||
[scanner]="true"
|
||||
></shared-filter-input-group-main>
|
||||
|
||||
<a
|
||||
class="page-search-results__filter w-[6.75rem] h-14 rounded font-bold px-5 mb-4 text-lg bg-[#AEB7C1] flex flex-row flex-nowrap items-center justify-center"
|
||||
[class.active]="hasFilter$ | async"
|
||||
[routerLink]="filterRoute"
|
||||
queryParamsHandling="preserve"
|
||||
>
|
||||
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
|
||||
Filter
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="hits$ | async; let hits"
|
||||
class="page-search-results__items-count inline-flex flex-row items-center pr-5 text-p3"
|
||||
[class.mb-4]="mainOutletActive$ | async"
|
||||
>
|
||||
{{ hits ??
|
||||
0 }}
|
||||
Titel
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-search-results__order-by" [class.page-search-results__order-by-main]="mainOutletActive$ | async">
|
||||
<shared-order-by-filter
|
||||
[groupBy]="(mainOutletActive$ | async) ? [2, 2, 2] : []"
|
||||
[orderBy]="(filter$ | async)?.orderBy"
|
||||
(selectedOrderByChange)="search(); updateBreadcrumbs()"
|
||||
>
|
||||
</shared-order-by-filter>
|
||||
</div>
|
||||
|
||||
<cdk-virtual-scroll-viewport
|
||||
#scrollContainer
|
||||
class="product-list scroll-bar scroll-bar-margin"
|
||||
[itemSize]="187"
|
||||
minBufferPx="1200"
|
||||
maxBufferPx="1200"
|
||||
class="product-list"
|
||||
[itemSize]="(mainOutletActive$ | async) ? 98 : 187"
|
||||
minBufferPx="2800"
|
||||
maxBufferPx="2800"
|
||||
(scrolledIndexChange)="scrolledIndexChange($event)"
|
||||
>
|
||||
<div class="product-list-result" *cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId">
|
||||
<search-result-item
|
||||
[selected]="item | searchResultSelected: searchService.selectedItemIds"
|
||||
[selectable]="isSelectable(item)"
|
||||
[item]="item"
|
||||
></search-result-item>
|
||||
</div>
|
||||
<search-result-item
|
||||
class="page-search-results__result-item"
|
||||
[class.page-search-results__result-item-main]="mainOutletActive$ | async"
|
||||
*cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId"
|
||||
(selectedChange)="addToCart($event)"
|
||||
[selectable]="isSelectable(item)"
|
||||
[item]="item"
|
||||
></search-result-item>
|
||||
<page-search-result-item-loading *ngIf="fetching$ | async"></page-search-result-item-loading>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
|
||||
<div class="actions">
|
||||
<div *ngIf="isTablet" class="actions z-fixed">
|
||||
<button
|
||||
[disabled]="loading$ | async"
|
||||
*ngIf="(selectedItemIds$ | async)?.length > 0"
|
||||
class="cta-cart cta-action-primary"
|
||||
(click)="addSelectedItemsToCart()"
|
||||
(click)="addToCart()"
|
||||
>
|
||||
<ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner>
|
||||
</button>
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
:host {
|
||||
@apply box-border grid;
|
||||
max-height: calc(100vh - 364px);
|
||||
height: 100vh;
|
||||
grid-template-rows: auto 1fr;
|
||||
@apply box-border grid h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
|
||||
grid-template-rows: auto auto 1fr;
|
||||
}
|
||||
|
||||
.product-list {
|
||||
@apply m-0 p-0 mt-2;
|
||||
@apply m-0 p-0 mt-px-2;
|
||||
}
|
||||
|
||||
.product-list-result {
|
||||
@apply list-none bg-white rounded-card p-4 mb-2;
|
||||
height: 187px;
|
||||
max-height: 187px;
|
||||
.page-search-results__result-item {
|
||||
@apply mb-px-10;
|
||||
}
|
||||
|
||||
.filter-wrapper {
|
||||
@apply block relative;
|
||||
.page-search-results__result-item-main {
|
||||
@apply mb-[5px];
|
||||
}
|
||||
|
||||
.hits {
|
||||
@apply text-inactive-branch font-semibold absolute top-2 right-0;
|
||||
}
|
||||
.page-search-results__order-by {
|
||||
@apply bg-white rounded px-6 desktop-small:px-8;
|
||||
}
|
||||
|
||||
ui-order-by-filter {
|
||||
@apply mx-auto;
|
||||
.page-search-results__order-by-main {
|
||||
@apply pl-[4.9375rem] px-4;
|
||||
}
|
||||
|
||||
.page-search-results__filter {
|
||||
&.active {
|
||||
@apply bg-[#596470] text-white ml-px-5;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
@apply fixed bottom-28 inline-grid grid-flow-col gap-7;
|
||||
@apply fixed bottom-16 inline-grid grid-flow-col gap-7;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
@@ -44,3 +46,16 @@
|
||||
@apply bg-brand text-white;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep page-search-results .page-search-results__order-by-main shared-order-by-filter {
|
||||
@apply grid grid-flow-col justify-items-start gap-x-4 justify-start;
|
||||
grid-template-columns: 37.5% 32.5% 20% auto;
|
||||
|
||||
.group {
|
||||
@apply desktop-small:justify-start;
|
||||
}
|
||||
|
||||
.order-by-filter-button {
|
||||
@apply ml-0 mr-7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,20 @@ import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, ViewC
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { ItemDTO } from '@swagger/cat';
|
||||
import { AddToShoppingCartDTO } from '@swagger/checkout';
|
||||
import { UiFilter } from '@ui/filter';
|
||||
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
import { CacheService } from 'apps/core/cache/src/public-api';
|
||||
import { isEqual } from 'lodash';
|
||||
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
|
||||
import { debounceTime, first, map, switchMap } from 'rxjs/operators';
|
||||
import { debounceTime, first, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators';
|
||||
import { ArticleSearchService } from '../article-search.store';
|
||||
import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
|
||||
import { SearchResultItemComponent } from './search-result-item.component';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/filter/src/lib';
|
||||
|
||||
@Component({
|
||||
selector: 'page-search-results',
|
||||
@@ -27,6 +29,9 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('scrollContainer', { static: true })
|
||||
scrollContainer: CdkVirtualScrollViewport;
|
||||
|
||||
@ViewChild(FilterInputGroupMainComponent, { static: false })
|
||||
sharedFilterInputGroupMain: FilterInputGroupMainComponent;
|
||||
|
||||
results$ = this.searchService.items$;
|
||||
fetching$ = this.searchService.fetching$;
|
||||
hits$ = this.searchService.hits$;
|
||||
@@ -35,6 +40,8 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
|
||||
selectedItemIds$ = this.searchService.selectedItemIds$;
|
||||
|
||||
searchboxHint$ = this.searchService.searchboxHint$;
|
||||
|
||||
selectedItems$ = combineLatest([this.results$, this.selectedItemIds$]).pipe(
|
||||
map(([items, selectedItemIds]) => {
|
||||
return items?.filter((item) => selectedItemIds?.find((selectedItemId) => item.id === selectedItemId));
|
||||
@@ -47,14 +54,39 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
|
||||
trackByItemId: TrackByFunction<ItemDTO> = (index, item) => item.id;
|
||||
|
||||
get isTablet() {
|
||||
return this._environment.matchTablet();
|
||||
}
|
||||
|
||||
hasFilter$ = combineLatest([this.searchService.filter$, this.searchService.defaultSettings$]).pipe(
|
||||
map(([filter, defaultFilter]) => {
|
||||
const filterQueryParams = filter?.getQueryParams();
|
||||
return !isEqual(this.resetQueryParamsQueryAndOrderBy(filterQueryParams), Filter.create(defaultFilter).getQueryParams());
|
||||
})
|
||||
);
|
||||
|
||||
get filterRoute() {
|
||||
const itemId = this._navigationService?.getOutletParams(this.route)?.right?.id;
|
||||
return this._navigationService.getArticleSearchResultsAndFilterPath({
|
||||
processId: this.application.activatedProcessId,
|
||||
itemId,
|
||||
});
|
||||
}
|
||||
|
||||
get mainOutletActive$() {
|
||||
return this._navigationService?.mainOutletActive$(this.route).pipe(shareReplay());
|
||||
}
|
||||
|
||||
constructor(
|
||||
public searchService: ArticleSearchService,
|
||||
private route: ActivatedRoute,
|
||||
private application: ApplicationService,
|
||||
public application: ApplicationService,
|
||||
private breadcrumb: BreadcrumbService,
|
||||
private cache: CacheService,
|
||||
private _uiModal: UiModalService,
|
||||
private _checkoutService: DomainCheckoutService
|
||||
private _checkoutService: DomainCheckoutService,
|
||||
private _environment: EnvironmentService,
|
||||
private _navigationService: ProductCatalogNavigationService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -72,7 +104,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id;
|
||||
|
||||
if (processChanged) {
|
||||
if (!!this.searchService.processId && this.searchService.filter instanceof UiFilter) {
|
||||
if (!!this.searchService.processId && this.searchService.filter instanceof Filter) {
|
||||
this.cacheCurrentData(
|
||||
this.searchService.processId,
|
||||
this.searchService.filter.getQueryParams(),
|
||||
@@ -87,7 +119,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
this.searchService.setBranch(selectedBranch);
|
||||
}
|
||||
|
||||
if (!(this.searchService.filter instanceof UiFilter)) {
|
||||
if (!(this.searchService.filter instanceof Filter)) {
|
||||
await this.searchService.setDefaultFilter();
|
||||
}
|
||||
|
||||
@@ -114,9 +146,32 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
|
||||
await this.updateBreadcrumbs(processId, queryParams);
|
||||
await this.createBreadcrumb(processId, queryParams);
|
||||
await this.removeDetailsBreadcrumb(processId);
|
||||
if (this.isTablet || this.route?.outlet === 'main') {
|
||||
await this.removeDetailsBreadcrumb(processId);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.subscriptions.add(
|
||||
this.searchService.searchCompleted.pipe(withLatestFrom(this.application.activatedProcessId$)).subscribe(([state, processId]) => {
|
||||
if (state.searchState === '') {
|
||||
const params = state.filter.getQueryParams();
|
||||
if ((state.hits === 1 && this.isTablet) || (!this.isTablet && !this._navigationService.mainOutletActive(this.route))) {
|
||||
const item = state.items.find((f) => f);
|
||||
this._navigationService.navigateToDetails({
|
||||
processId,
|
||||
itemId: item.id,
|
||||
queryParams: this.isTablet ? undefined : params,
|
||||
});
|
||||
} else if (this.isTablet || this._navigationService.mainOutletActive(this.route)) {
|
||||
this._navigationService.navigateToResults({
|
||||
processId,
|
||||
queryParams: params,
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -127,7 +182,26 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
this.unselectAll();
|
||||
}
|
||||
|
||||
search() {
|
||||
resetQueryParamsQueryAndOrderBy(params: Record<string, string> = {}) {
|
||||
const clean = { ...params };
|
||||
|
||||
for (const key in clean) {
|
||||
if (key === 'main_qs') {
|
||||
clean[key] = undefined;
|
||||
} else if (key?.includes('order_by')) {
|
||||
delete clean[key];
|
||||
}
|
||||
}
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
||||
search(filter?: Filter) {
|
||||
if (!!filter) {
|
||||
this.sharedFilterInputGroupMain.cancelAutocomplete();
|
||||
this.searchService.setFilter(filter);
|
||||
}
|
||||
|
||||
this.searchService.search({ clear: true });
|
||||
}
|
||||
|
||||
@@ -187,7 +261,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
await this.breadcrumb.addBreadcrumbIfNotExists({
|
||||
key: processId,
|
||||
name,
|
||||
path: `/kunde/${this.application.activatedProcessId}/product/search/results`,
|
||||
path: this._navigationService.getArticleSearchResultsPath(this.application.activatedProcessId),
|
||||
params: queryParams,
|
||||
section: 'customer',
|
||||
tags: ['catalog', 'filter', 'results'],
|
||||
@@ -241,6 +315,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
// Zeige Select Radio Button nicht an wenn Item Archivartikel oder Fortsetzungsartikel ist
|
||||
const isArchiv = item?.catalogAvailability?.status === 1;
|
||||
const isFortsetzung = item?.features?.find((i) => i?.key === 'PFO');
|
||||
|
||||
return !(isArchiv || isFortsetzung);
|
||||
}
|
||||
|
||||
@@ -249,29 +324,44 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
this.searchService.patchState({ selectedItemIds: [] });
|
||||
}
|
||||
|
||||
async addSelectedItemsToCart() {
|
||||
async addToCart(item?: ItemDTO) {
|
||||
this.loading$.next(true);
|
||||
const selectedItems = await this.selectedItems$.pipe(first()).toPromise();
|
||||
|
||||
if (!!item) {
|
||||
await this.addItemsToCart(item);
|
||||
} else {
|
||||
await this.addItemsToCart();
|
||||
}
|
||||
|
||||
this.loading$.next(false);
|
||||
}
|
||||
|
||||
private _createShoppingCartItem(item: ItemDTO): AddToShoppingCartDTO {
|
||||
return {
|
||||
quantity: 1,
|
||||
availability: {
|
||||
availabilityType: item?.catalogAvailability?.status,
|
||||
price: item?.catalogAvailability?.price,
|
||||
supplierProductNumber: item?.ids?.dig ? String(item?.ids?.dig) : item?.product?.supplierProductNumber,
|
||||
},
|
||||
product: {
|
||||
catalogProductNumber: String(item?.id),
|
||||
...item?.product,
|
||||
},
|
||||
itemType: item?.type,
|
||||
promotion: { points: item?.promoPoints },
|
||||
};
|
||||
}
|
||||
|
||||
async addItemsToCart(item?: ItemDTO) {
|
||||
const selectedItems = !item ? await this.selectedItems$.pipe(first()).toPromise() : [item];
|
||||
const items: AddToShoppingCartDTO[] = [];
|
||||
|
||||
const canAddItemsPayload = [];
|
||||
|
||||
for (const item of selectedItems) {
|
||||
const shoppingCartItem = this._createShoppingCartItem(item);
|
||||
const isDownload = item?.product?.format === 'EB' || item?.product?.format === 'DL';
|
||||
const shoppingCartItem: AddToShoppingCartDTO = {
|
||||
quantity: 1,
|
||||
availability: {
|
||||
availabilityType: item?.catalogAvailability?.status,
|
||||
price: item?.catalogAvailability?.price,
|
||||
supplierProductNumber: item?.ids?.dig ? String(item.ids?.dig) : item?.product?.supplierProductNumber,
|
||||
},
|
||||
product: {
|
||||
catalogProductNumber: String(item?.id),
|
||||
...item?.product,
|
||||
},
|
||||
itemType: item.type,
|
||||
promotion: { points: item?.promoPoints },
|
||||
};
|
||||
|
||||
if (isDownload) {
|
||||
shoppingCartItem.destination = { data: { target: 16 } };
|
||||
canAddItemsPayload.push({
|
||||
|
||||
@@ -8,7 +8,6 @@ import { UiCommonModule } from '@ui/common';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { UiSelectBulletModule } from '@ui/select-bullet';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
import { UiOrderByFilterModule } from 'apps/ui/filter/src/lib/next/order-by-filter/order-by-filter.module';
|
||||
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
|
||||
import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
|
||||
import { StockInfosPipe } from './order-by-filter/stick-infos.pipe';
|
||||
@@ -16,6 +15,9 @@ import { SearchResultItemLoadingComponent } from './search-result-item-loading.c
|
||||
import { SearchResultItemComponent } from './search-result-item.component';
|
||||
import { ArticleSearchResultsComponent } from './search-results.component';
|
||||
import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe';
|
||||
import { FilterAutocompleteProvider, FilterNextModule, OrderByFilterModule } from 'apps/shared/components/filter/src/lib';
|
||||
import { FocusSearchboxEvent } from '../focus-searchbox.event';
|
||||
import { ArticleSearchMainAutocompleteProvider } from '../providers';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -27,9 +29,10 @@ import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe
|
||||
UiIconModule,
|
||||
UiSelectBulletModule,
|
||||
UiSpinnerModule,
|
||||
UiOrderByFilterModule,
|
||||
OrderByFilterModule,
|
||||
ScrollingModule,
|
||||
UiTooltipModule,
|
||||
FilterNextModule,
|
||||
],
|
||||
exports: [ArticleSearchResultsComponent, SearchResultItemComponent],
|
||||
declarations: [
|
||||
@@ -40,6 +43,13 @@ import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe
|
||||
SearchResultSelectedPipe,
|
||||
AddedToCartModalComponent,
|
||||
],
|
||||
providers: [],
|
||||
providers: [
|
||||
FocusSearchboxEvent,
|
||||
{
|
||||
provide: FilterAutocompleteProvider,
|
||||
useClass: ArticleSearchMainAutocompleteProvider,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class SearchResultsModule {}
|
||||
|
||||
@@ -2,10 +2,65 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { ArticleDetailsComponent } from './article-details/article-details.component';
|
||||
import { ArticleSearchComponent } from './article-search/article-search.component';
|
||||
import { ArticleSearchFilterComponent } from './article-search/search-filter/search-filter.component';
|
||||
import { ArticleSearchMainComponent } from './article-search/search-main/search-main.component';
|
||||
import { ArticleSearchResultsComponent } from './article-search/search-results/search-results.component';
|
||||
import { PageCatalogComponent } from './page-catalog.component';
|
||||
|
||||
const auxiliaryRoutes = [
|
||||
{
|
||||
path: 'search',
|
||||
component: ArticleSearchComponent,
|
||||
outlet: 'left',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: ArticleSearchMainComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'filter',
|
||||
component: ArticleSearchFilterComponent,
|
||||
outlet: 'right',
|
||||
},
|
||||
{
|
||||
path: 'filter/:id',
|
||||
component: ArticleSearchFilterComponent,
|
||||
outlet: 'right',
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'left',
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'main',
|
||||
},
|
||||
{
|
||||
path: 'results/:id',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'left',
|
||||
},
|
||||
{
|
||||
path: 'results/ean/:ean',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'left',
|
||||
},
|
||||
{
|
||||
path: 'details/ean/:ean',
|
||||
component: ArticleDetailsComponent,
|
||||
outlet: 'right',
|
||||
},
|
||||
{
|
||||
path: 'details/:id',
|
||||
component: ArticleDetailsComponent,
|
||||
outlet: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
@@ -19,12 +74,16 @@ const routes: Routes = [
|
||||
path: '',
|
||||
component: ArticleSearchMainComponent,
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
component: ArticleSearchResultsComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
component: ArticleSearchResultsComponent,
|
||||
},
|
||||
{
|
||||
path: 'filter',
|
||||
component: ArticleSearchFilterComponent,
|
||||
},
|
||||
{
|
||||
path: 'details/ean/:ean',
|
||||
component: ArticleDetailsComponent,
|
||||
@@ -33,6 +92,7 @@ const routes: Routes = [
|
||||
path: 'details/:id',
|
||||
component: ArticleDetailsComponent,
|
||||
},
|
||||
...auxiliaryRoutes,
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
<shared-breadcrumb class="my-4" [key]="activatedProcessId$ | async" [tags]="['catalog']">
|
||||
<shared-branch-selector [branchType]="1" [value]="selectedBranch$ | async" (valueChange)="patchProcessData($event)">
|
||||
<shared-branch-selector
|
||||
[filterCurrentBranch]="!!auth.hasRole('Store')"
|
||||
[orderBy]="auth.hasRole('Store') ? 'distance' : 'name'"
|
||||
[branchType]="1"
|
||||
[value]="selectedBranch$ | async"
|
||||
(valueChange)="patchProcessData($event)"
|
||||
>
|
||||
</shared-branch-selector>
|
||||
</shared-breadcrumb>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<ng-container *ngIf="routerEvents$ | async">
|
||||
<ng-container *ngIf="!(isDesktop$ | async); else desktop">
|
||||
<router-outlet></router-outlet>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #desktop>
|
||||
<ng-container *ngIf="showMainOutlet$ | async">
|
||||
<router-outlet name="main"></router-outlet>
|
||||
</ng-container>
|
||||
|
||||
<div class="grid grid-cols-[minmax(31rem,.5fr)_1fr] gap-6">
|
||||
<div *ngIf="showLeftOutlet$ | async" class="block">
|
||||
<router-outlet name="left"></router-outlet>
|
||||
</div>
|
||||
|
||||
<div *ngIf="showRightOutlet$ | async">
|
||||
<router-outlet name="right"></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
:host {
|
||||
@apply block relative;
|
||||
@apply block relative h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
|
||||
}
|
||||
|
||||
shell-breadcrumb {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { BranchSelectorComponent } from '@shared/components/branch-selector';
|
||||
import { BreadcrumbComponent } from '@shared/components/breadcrumb';
|
||||
import { BranchDTO } from '@swagger/checkout';
|
||||
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
import { BehaviorSubject, from, fromEvent, Observable, Subject } from 'rxjs';
|
||||
import { first, map, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { fromEvent, Observable, Subject } from 'rxjs';
|
||||
import { first, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'page-catalog',
|
||||
@@ -27,11 +29,38 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
_onDestroy$ = new Subject<boolean>();
|
||||
|
||||
get isTablet$() {
|
||||
return this._environmentService.matchTablet$.pipe(
|
||||
map((state) => state.matches),
|
||||
shareReplay()
|
||||
);
|
||||
}
|
||||
|
||||
get isDesktop$() {
|
||||
return this._environmentService.matchDesktop$.pipe(
|
||||
map((state) => {
|
||||
return state.matches;
|
||||
}),
|
||||
shareReplay()
|
||||
);
|
||||
}
|
||||
|
||||
routerEvents$ = this._router.events.pipe(shareReplay());
|
||||
|
||||
showMainOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'main')));
|
||||
|
||||
showLeftOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'left')));
|
||||
|
||||
showRightOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'right')));
|
||||
|
||||
constructor(
|
||||
public application: ApplicationService,
|
||||
private _uiModal: UiModalService,
|
||||
public auth: AuthService,
|
||||
private _environmentService: EnvironmentService,
|
||||
private _renderer: Renderer2
|
||||
private _renderer: Renderer2,
|
||||
private _activatedRoute: ActivatedRoute,
|
||||
private _router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -41,19 +70,19 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (this._environmentService.isTablet()) {
|
||||
fromEvent(this.branchSelectorRef.nativeElement, 'focusin')
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe((_) => {
|
||||
fromEvent(this.branchSelectorRef.nativeElement, 'focusin')
|
||||
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.isTablet$))
|
||||
.subscribe(([_, isTablet]) => {
|
||||
if (isTablet) {
|
||||
this._renderer.setStyle(this.branchSelectorRef?.nativeElement, 'width', this.branchSelectorWidth);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
fromEvent(this.branchSelectorRef.nativeElement, 'focusout')
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe((_) => {
|
||||
this._renderer.removeStyle(this.branchSelectorRef?.nativeElement, 'width');
|
||||
});
|
||||
}
|
||||
fromEvent(this.branchSelectorRef.nativeElement, 'focusout')
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe((_) => {
|
||||
this._renderer.removeStyle(this.branchSelectorRef?.nativeElement, 'width');
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
||||
@@ -2,22 +2,13 @@ import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BranchSelectorComponent } from '@shared/components/branch-selector';
|
||||
import { BreadcrumbModule } from '@shared/components/breadcrumb';
|
||||
import { ShellBreadcrumbModule } from '@shell/breadcrumb';
|
||||
import { ArticleDetailsModule } from './article-details/article-details.module';
|
||||
import { ArticleSearchModule } from './article-search/article-search.module';
|
||||
import { PageCatalogRoutingModule } from './page-catalog-routing.module';
|
||||
import { PageCatalogComponent } from './page-catalog.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
PageCatalogRoutingModule,
|
||||
ShellBreadcrumbModule,
|
||||
ArticleSearchModule,
|
||||
ArticleDetailsModule,
|
||||
BreadcrumbModule,
|
||||
BranchSelectorComponent,
|
||||
],
|
||||
imports: [CommonModule, PageCatalogRoutingModule, ArticleSearchModule, ArticleDetailsModule, BreadcrumbModule, BranchSelectorComponent],
|
||||
exports: [],
|
||||
declarations: [PageCatalogComponent],
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@apply block bg-white shadow-card rounded-card pt-6;
|
||||
@apply block bg-white shadow-card rounded pt-6;
|
||||
|
||||
.header {
|
||||
@apply text-right;
|
||||
@@ -12,11 +12,11 @@
|
||||
@apply text-center;
|
||||
|
||||
.title {
|
||||
@apply text-2xl text-page-heading font-bold;
|
||||
@apply text-h3 text-h2 font-bold;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
@apply text-2xl mt-1 mb-0;
|
||||
@apply text-h3 mt-1 mb-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ button {
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply bg-white rounded-card shadow-card;
|
||||
@apply bg-white rounded shadow-card;
|
||||
}
|
||||
|
||||
.card-empty {
|
||||
@@ -26,7 +26,7 @@ button {
|
||||
width: fit-content;
|
||||
|
||||
h1 {
|
||||
@apply text-2xl font-bold my-1;
|
||||
@apply text-h3 font-bold my-1;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ button {
|
||||
}
|
||||
|
||||
.sub-header {
|
||||
@apply text-center text-2xl font-normal my-0 mb-8;
|
||||
@apply text-center text-h3 font-normal my-0 mb-8;
|
||||
}
|
||||
|
||||
hr {
|
||||
@@ -134,7 +134,7 @@ h1 {
|
||||
}
|
||||
|
||||
.branch-name {
|
||||
@apply text-regular overflow-hidden overflow-ellipsis ml-4;
|
||||
@apply text-p2 overflow-hidden overflow-ellipsis ml-4;
|
||||
}
|
||||
|
||||
.book-icon {
|
||||
@@ -156,7 +156,7 @@ h1 {
|
||||
}
|
||||
|
||||
.shipping-cost-info {
|
||||
@apply text-sm mr-4 self-end;
|
||||
@apply text-p3 mr-4 self-end;
|
||||
}
|
||||
|
||||
.total-value {
|
||||
@@ -164,5 +164,5 @@ h1 {
|
||||
}
|
||||
|
||||
.total-item-reading-points {
|
||||
@apply text-base font-bold text-ucla-blue;
|
||||
@apply text-p2 font-bold text-ucla-blue;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { AvailabilityDTO, DestinationDTO, NotificationChannel, ShoppingCartItemDTO, ShoppingCartDTO } from '@swagger/checkout';
|
||||
import { UiErrorModalComponent, UiMessageModalComponent, UiModalService } from '@ui/modal';
|
||||
import { PrintModalData, PrintModalComponent } from '@modal/printer';
|
||||
import { PurchasingOptionsModalComponent, PurchasingOptionsModalData } from '../modals/purchasing-options-modal';
|
||||
import { PurchasingOptions } from '../modals/purchasing-options-modal/purchasing-options-modal.store';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { first, map, shareReplay, switchMap, take, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { Subject, NEVER, combineLatest, BehaviorSubject } from 'rxjs';
|
||||
@@ -15,13 +13,11 @@ import { DomainCatalogService } from '@domain/catalog';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { DomainPrinterService } from '@domain/printer';
|
||||
import { CheckoutDummyComponent } from '../checkout-dummy/checkout-dummy.component';
|
||||
import { ResponseArgsOfItemDTO } from '@swagger/cat';
|
||||
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
|
||||
import { emailNotificationValidator, mobileNotificationValidator } from '@shared/components/notification-channel-control';
|
||||
import { PurchasingOptionsListModalComponent } from '../modals/purchasing-options-list-modal';
|
||||
import { PurchasingOptionsListModalData } from '../modals/purchasing-options-list-modal/purchasing-options-list-modal.data';
|
||||
import { ComponentStore, tapResponse } from '@ngrx/component-store';
|
||||
import { CheckoutDummyData } from '../checkout-dummy/checkout-dummy-data';
|
||||
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
|
||||
|
||||
export interface CheckoutReviewComponentState {
|
||||
shoppingCart: ShoppingCartDTO;
|
||||
@@ -242,7 +238,8 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
|
||||
private domainCatalogService: DomainCatalogService,
|
||||
private breadcrumb: BreadcrumbService,
|
||||
private domainPrinterService: DomainPrinterService,
|
||||
private _fb: UntypedFormBuilder
|
||||
private _fb: UntypedFormBuilder,
|
||||
private _purchaseOptionsModalService: PurchaseOptionsModalService
|
||||
) {
|
||||
super({
|
||||
shoppingCart: undefined,
|
||||
@@ -274,7 +271,7 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
|
||||
shoppingCart,
|
||||
shoppingCartItems,
|
||||
});
|
||||
this.checkQuantityErrors(shoppingCartItems);
|
||||
// this.checkQuantityErrors(shoppingCartItems);
|
||||
},
|
||||
(err) => {},
|
||||
() => {}
|
||||
@@ -285,15 +282,15 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
|
||||
)
|
||||
);
|
||||
|
||||
checkQuantityErrors(shoppingCartItems: ShoppingCartItemDTO[]) {
|
||||
shoppingCartItems.forEach((item) => {
|
||||
if (item.features?.orderType === 'Rücklage') {
|
||||
this.setQuantityError(item, item.availability, item.quantity > item.availability?.inStock);
|
||||
} else {
|
||||
this.setQuantityError(item, item.availability, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
// checkQuantityErrors(shoppingCartItems: ShoppingCartItemDTO[]) {
|
||||
// shoppingCartItems.forEach((item) => {
|
||||
// if (item.features?.orderType === 'Abholung') {
|
||||
// this.setQuantityError(item, item.availability, item.quantity > item.availability?.inStock);
|
||||
// } else {
|
||||
// this.setQuantityError(item, item.availability, false);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
async updateBreadcrumb() {
|
||||
await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
|
||||
@@ -434,171 +431,10 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
|
||||
}
|
||||
|
||||
async changeItem({ shoppingCartItem }: { shoppingCartItem: ShoppingCartItemDTO }) {
|
||||
this.loadingOnItemChangeById$.next(shoppingCartItem.id);
|
||||
|
||||
const quantity = shoppingCartItem.quantity;
|
||||
|
||||
const branchNo = this.auth.getClaimByKey('branch_no');
|
||||
const branchId = shoppingCartItem?.destination?.data?.targetBranch?.id;
|
||||
|
||||
const customerFeatures = await this.customerFeatures$.pipe(first()).toPromise();
|
||||
|
||||
let branch = await this.domainCheckoutService
|
||||
.getBranches()
|
||||
.pipe(map((branches) => branches.find((branch) => (branchId ? branch.id === branchId : branch.branchNumber === branchNo))))
|
||||
.toPromise();
|
||||
|
||||
if (!branch) {
|
||||
branch = await this.applicationService.getSelectedBranch$().pipe(take(1)).toPromise();
|
||||
}
|
||||
|
||||
let catalogItem: ResponseArgsOfItemDTO;
|
||||
if (Number.isInteger(shoppingCartItem?.product?.catalogProductNumber)) {
|
||||
catalogItem = await this.domainCatalogService
|
||||
.getDetailsById({ id: Number(shoppingCartItem.product.catalogProductNumber) })
|
||||
.toPromise();
|
||||
} else if (shoppingCartItem?.product?.ean) {
|
||||
catalogItem = await this.domainCatalogService.getDetailsByEan({ ean: shoppingCartItem.product.ean }).toPromise();
|
||||
}
|
||||
|
||||
let takeAwayAvailability: AvailabilityDTO;
|
||||
if (!!catalogItem?.result?.product) {
|
||||
takeAwayAvailability = await this.availabilityService
|
||||
.getTakeAwayAvailability({
|
||||
item: {
|
||||
itemId: catalogItem.result.id,
|
||||
ean: catalogItem.result.product.ean,
|
||||
price: catalogItem.result.catalogAvailability?.price,
|
||||
},
|
||||
quantity,
|
||||
})
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
const pickupAvailability = await this.availabilityService
|
||||
.getPickUpAvailability({
|
||||
item: {
|
||||
itemId: Number(shoppingCartItem.product.catalogProductNumber),
|
||||
ean: shoppingCartItem.product.ean,
|
||||
price: shoppingCartItem.availability.price,
|
||||
},
|
||||
branch,
|
||||
quantity,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
const digAvailability = await this.availabilityService
|
||||
.getDigDeliveryAvailability({
|
||||
item: {
|
||||
itemId: Number(shoppingCartItem.product.catalogProductNumber),
|
||||
ean: shoppingCartItem.product.ean,
|
||||
price: shoppingCartItem.availability.price,
|
||||
},
|
||||
quantity,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
const b2bAvailability = await this.availabilityService
|
||||
.getB2bDeliveryAvailability({
|
||||
item: {
|
||||
itemId: Number(shoppingCartItem.product.catalogProductNumber),
|
||||
ean: shoppingCartItem.product.ean,
|
||||
price: shoppingCartItem.availability.price,
|
||||
},
|
||||
quantity,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
const downloadAvailability = await this.availabilityService
|
||||
.getDownloadAvailability({
|
||||
item: {
|
||||
itemId: Number(shoppingCartItem.product.catalogProductNumber),
|
||||
ean: shoppingCartItem.product.ean,
|
||||
price: shoppingCartItem.availability.price,
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
let availableOptions: PurchasingOptions[] = [];
|
||||
const availabilities: { [key: string]: AvailabilityDTO } = {};
|
||||
|
||||
if (takeAwayAvailability && this.availabilityService.isAvailable({ availability: takeAwayAvailability })) {
|
||||
availableOptions.push('take-away');
|
||||
availabilities['take-away'] = takeAwayAvailability;
|
||||
}
|
||||
|
||||
if (downloadAvailability && this.availabilityService.isAvailable({ availability: downloadAvailability })) {
|
||||
availableOptions.push('download');
|
||||
availabilities['download'] = downloadAvailability;
|
||||
}
|
||||
|
||||
if (pickupAvailability && this.availabilityService.isAvailable({ availability: pickupAvailability[0] })) {
|
||||
if (pickupAvailability[1].availableFor) {
|
||||
if ((pickupAvailability[1].availableFor & 2) === 2) {
|
||||
availableOptions.push('pick-up');
|
||||
availabilities['pick-up'] = pickupAvailability[0];
|
||||
}
|
||||
} else {
|
||||
availableOptions.push('pick-up');
|
||||
availabilities['pick-up'] = pickupAvailability[0];
|
||||
}
|
||||
|
||||
if (!customerFeatures?.webshop && this.availabilityService.isAvailable({ availability: b2bAvailability })) {
|
||||
availableOptions.push('b2b-delivery');
|
||||
availabilities['b2b-delivery'] = b2bAvailability;
|
||||
}
|
||||
}
|
||||
|
||||
if (digAvailability && this.availabilityService.isAvailable({ availability: digAvailability }) && !customerFeatures?.b2b) {
|
||||
availableOptions.push('dig-delivery');
|
||||
availabilities['dig-delivery'] = digAvailability;
|
||||
}
|
||||
|
||||
if (availableOptions.includes('dig-delivery') && availableOptions.includes('b2b-delivery')) {
|
||||
let shippingAvailability = await this.availabilityService
|
||||
.getDeliveryAvailability({
|
||||
item: {
|
||||
itemId: Number(shoppingCartItem.product.catalogProductNumber),
|
||||
ean: shoppingCartItem.product.ean,
|
||||
price: shoppingCartItem.availability.price,
|
||||
},
|
||||
quantity,
|
||||
})
|
||||
.toPromise();
|
||||
if (shippingAvailability && this.availabilityService.isAvailable({ availability: shippingAvailability })) {
|
||||
availableOptions.push('delivery');
|
||||
availabilities['delivery'] = shippingAvailability;
|
||||
availableOptions = availableOptions.filter((option) => !(option === 'dig-delivery' || option === 'b2b-delivery'));
|
||||
}
|
||||
}
|
||||
|
||||
this.loadingOnItemChangeById$.next(undefined);
|
||||
this.cdr.markForCheck();
|
||||
|
||||
const itemId = Number(shoppingCartItem.product.catalogProductNumber);
|
||||
const modal = this.uiModal.open({
|
||||
content: PurchasingOptionsModalComponent,
|
||||
data: {
|
||||
availableOptions,
|
||||
item: {
|
||||
id: itemId,
|
||||
itemId: itemId,
|
||||
product: shoppingCartItem.product,
|
||||
price: shoppingCartItem.availability.price,
|
||||
catalogAvailability: {
|
||||
status: shoppingCartItem.availability.availabilityType,
|
||||
price: shoppingCartItem.availability.price,
|
||||
},
|
||||
},
|
||||
shoppingCartItem,
|
||||
branchId: branch?.id,
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
availabilities,
|
||||
} as PurchasingOptionsModalData,
|
||||
});
|
||||
|
||||
modal.afterClosed$.pipe(takeUntil(this._orderCompleted)).subscribe(() => {
|
||||
this.setQuantityError(shoppingCartItem, undefined, false);
|
||||
this._purchaseOptionsModalService.open({
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
items: [shoppingCartItem],
|
||||
type: 'update',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -649,7 +485,7 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
this.setQuantityError(shoppingCartItem, availability, availability?.inStock < quantity);
|
||||
// this.setQuantityError(shoppingCartItem, availability, availability?.inStock < quantity);
|
||||
break;
|
||||
case 'Abholung':
|
||||
availability = await this.availabilityService
|
||||
@@ -727,7 +563,7 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
this.setQuantityError(shoppingCartItem, availability, false);
|
||||
// this.setQuantityError(shoppingCartItem, availability, false);
|
||||
} else if (availability) {
|
||||
// Wenn das Ergebnis der Availability Abfrage keinen Preis zurückliefert (z.B. HFI Geschenkkarte), wird der Preis aus der
|
||||
// Availability vor der Abfrage verwendet
|
||||
@@ -758,16 +594,16 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
|
||||
this.loadingOnQuantityChangeById$.next(undefined);
|
||||
}
|
||||
|
||||
setQuantityError(item: ShoppingCartItemDTO, availability: AvailabilityDTO, error: boolean) {
|
||||
const quantityErrors: { [key: string]: string } = this.quantityError$.value;
|
||||
if (error) {
|
||||
quantityErrors[item.product.catalogProductNumber] = `${availability.inStock} Exemplar(e) sofort lieferbar`;
|
||||
this.quantityError$.next({ ...quantityErrors });
|
||||
} else {
|
||||
delete quantityErrors[item.product.catalogProductNumber];
|
||||
this.quantityError$.next({ ...quantityErrors });
|
||||
}
|
||||
}
|
||||
// setQuantityError(item: ShoppingCartItemDTO, availability: AvailabilityDTO, error: boolean) {
|
||||
// const quantityErrors: { [key: string]: string } = this.quantityError$.value;
|
||||
// if (error) {
|
||||
// quantityErrors[item.product.catalogProductNumber] = `${availability.inStock} Exemplar(e) sofort lieferbar`;
|
||||
// this.quantityError$.next({ ...quantityErrors });
|
||||
// } else {
|
||||
// delete quantityErrors[item.product.catalogProductNumber];
|
||||
// this.quantityError$.next({ ...quantityErrors });
|
||||
// }
|
||||
// }
|
||||
|
||||
// Bei unbekannten Kunden und DIG Bestellung findet ein Vergleich der Preise statt
|
||||
compareDeliveryAndCatalogPrice(availability: AvailabilityDTO, orderType: string, shoppingCartItemPrice: number) {
|
||||
@@ -816,16 +652,10 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
|
||||
}
|
||||
|
||||
async showPurchasingListModal(shoppingCartItems: ShoppingCartItemDTO[]) {
|
||||
const customerFeatures = await this.customerFeatures$.pipe(first()).toPromise();
|
||||
this.uiModal.open({
|
||||
content: PurchasingOptionsListModalComponent,
|
||||
title: 'Wie möchten Sie die Artikel erhalten?',
|
||||
config: { showScrollbarY: false },
|
||||
data: {
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
shoppingCartItems: shoppingCartItems,
|
||||
customerFeatures,
|
||||
} as PurchasingOptionsListModalData,
|
||||
this._purchaseOptionsModalService.open({
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
items: shoppingCartItems,
|
||||
type: 'update',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ button {
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 150px;
|
||||
@apply rounded-card shadow-cta;
|
||||
@apply rounded shadow-cta;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,11 +59,11 @@ button {
|
||||
}
|
||||
|
||||
.item-title.md {
|
||||
@apply font-bold text-base;
|
||||
@apply font-bold text-p2;
|
||||
}
|
||||
|
||||
.item-title.sm {
|
||||
@apply font-bold text-sm;
|
||||
@apply font-bold text-p3;
|
||||
}
|
||||
|
||||
.item-title.xs {
|
||||
@@ -88,7 +88,7 @@ button {
|
||||
}
|
||||
|
||||
.quantity-error {
|
||||
@apply text-dark-goldenrod font-bold text-sm whitespace-nowrap;
|
||||
@apply text-dark-goldenrod font-bold text-p3 whitespace-nowrap;
|
||||
}
|
||||
|
||||
ui-quantity-dropdown {
|
||||
@@ -113,7 +113,7 @@ button {
|
||||
}
|
||||
|
||||
.item-availability-message {
|
||||
@apply text-dark-goldenrod font-bold text-sm whitespace-nowrap;
|
||||
@apply text-dark-goldenrod font-bold text-p3 whitespace-nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ label {
|
||||
}
|
||||
|
||||
textarea {
|
||||
@apply flex-grow text-base border-none outline-none p-0 resize-none;
|
||||
@apply flex-grow text-p2 border-none outline-none p-0 resize-none;
|
||||
}
|
||||
|
||||
button {
|
||||
@apply text-brand font-bold text-cta-l outline-none border-none bg-transparent ml-1;
|
||||
@apply text-brand font-bold text-p1 outline-none border-none bg-transparent ml-1;
|
||||
|
||||
ui-icon {
|
||||
@apply text-ucla-blue;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply bg-white rounded-card shadow-card overflow-scroll;
|
||||
@apply bg-white rounded shadow-card overflow-scroll;
|
||||
height: calc(100vh - 410px);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
}
|
||||
|
||||
.order-customer {
|
||||
@apply font-bold text-2xl;
|
||||
@apply font-bold text-h3;
|
||||
}
|
||||
|
||||
.grow {
|
||||
@@ -178,15 +178,15 @@
|
||||
}
|
||||
|
||||
.promotion-points {
|
||||
@apply text-ucla-blue text-regular font-bold flex-grow;
|
||||
@apply text-ucla-blue text-p2 font-bold flex-grow;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
@apply font-bold text-cta-l;
|
||||
@apply font-bold text-p1;
|
||||
}
|
||||
|
||||
.price-info {
|
||||
@apply text-right text-sm;
|
||||
@apply text-right text-p3;
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// start:ng42.barrel
|
||||
export * from './page-checkout.module';
|
||||
export * from './page-checkout-modals.module';
|
||||
// end:ng42.barrel
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<div class="option-icon">
|
||||
<ui-icon size="50px" icon="truck"></ui-icon>
|
||||
</div>
|
||||
<button
|
||||
class="option-chip"
|
||||
[disabled]="optionChipDisabled$ | async"
|
||||
(click)="optionChange('delivery')"
|
||||
[class.selected]="(selectedOption$ | async) === 'delivery'"
|
||||
>
|
||||
Versand
|
||||
</button>
|
||||
<p>Möchten Sie die Artikel<br />geliefert bekommen?</p>
|
||||
<p>Versandkostenfrei</p>
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-delivery-option-list',
|
||||
templateUrl: 'delivery-option-list.component.html',
|
||||
styleUrls: ['../list-options.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DeliveryOptionListComponent {
|
||||
selectedOption$ = this._store.selectedFilterOption$;
|
||||
optionChipDisabled$ = this._store.fetchingAvailabilities$;
|
||||
|
||||
constructor(private _store: PurchasingOptionsListModalStore) {}
|
||||
|
||||
optionChange(option: string) {
|
||||
if (this._store.selectedFilterOption === option) {
|
||||
this._store.selectedFilterOption = undefined;
|
||||
} else {
|
||||
this._store.selectedFilterOption = option;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './delivery-option-list.component';
|
||||
// end:ng42.barrel
|
||||
@@ -1,7 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './delivery-option';
|
||||
export * from './pick-up-option';
|
||||
export * from './take-away-option';
|
||||
export * from './purchasing-options-list-modal.component';
|
||||
export * from './purchasing-options-list-modal.module';
|
||||
// end:ng42.barrel
|
||||
@@ -1,44 +0,0 @@
|
||||
:host {
|
||||
@apply block w-72;
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
@apply text-ucla-blue mx-auto;
|
||||
width: 40px;
|
||||
|
||||
.truck-b2b {
|
||||
margin-top: -21px;
|
||||
margin-bottom: -12px;
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.option-chip {
|
||||
@apply rounded-full text-base px-4 py-3 bg-glitter text-inactive-customer border-none font-bold;
|
||||
|
||||
&.selected {
|
||||
@apply bg-active-customer text-white;
|
||||
}
|
||||
}
|
||||
|
||||
.option-description {
|
||||
@apply my-2;
|
||||
}
|
||||
|
||||
.option-select {
|
||||
@apply mt-4 mb-4 border-2 border-solid border-brand text-brand text-cta-l font-bold bg-white rounded-full py-3 px-6;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply my-4;
|
||||
}
|
||||
|
||||
::ng-deep page-purchasing-options-list-modal ui-branch-dropdown .wrapper {
|
||||
@apply mx-auto;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
::ng-deep page-pick-up-option-list .option-chip:disabled,
|
||||
::ng-deep page-take-away-option-list .option-chip:disabled {
|
||||
@apply bg-disabled-branch border-disabled-branch text-white;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './pick-up-option-list.component';
|
||||
// end:ng42.barrel
|
||||
@@ -1,18 +0,0 @@
|
||||
<div class="option-icon">
|
||||
<ui-icon size="50px" icon="box_out"></ui-icon>
|
||||
</div>
|
||||
<button
|
||||
class="option-chip"
|
||||
[disabled]="optionChipDisabled$ | async"
|
||||
(click)="optionChange('pick-up')"
|
||||
[class.selected]="(selectedOption$ | async) === 'pick-up'"
|
||||
>
|
||||
Abholung
|
||||
</button>
|
||||
<p>Möchten Sie die Artikel<br />in einer unserer Filialen<br />abholen?</p>
|
||||
|
||||
<ui-branch-dropdown
|
||||
[branches]="branches$ | async"
|
||||
[selected]="selectedBranch$ | async"
|
||||
(selectBranch)="selectBranch($event)"
|
||||
></ui-branch-dropdown>
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { BranchDTO } from '@swagger/checkout';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-pick-up-option-list',
|
||||
templateUrl: 'pick-up-option-list.component.html',
|
||||
styleUrls: ['../list-options.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PickUpOptionListComponent {
|
||||
branches$ = this._store.branches$;
|
||||
selectedBranch$ = this._store.selectedPickUpBranch$.pipe(
|
||||
map((branch) => {
|
||||
// Determins if branch is targetBranch
|
||||
if (branch?.branchType === 1) {
|
||||
return branch.name;
|
||||
}
|
||||
})
|
||||
);
|
||||
selectedOption$ = this._store.selectedFilterOption$;
|
||||
optionChipDisabled$ = combineLatest([this._store.fetchingAvailabilities$, this.selectedBranch$]).pipe(
|
||||
map(([fetching, selectedBranch]) => {
|
||||
return fetching || !selectedBranch;
|
||||
})
|
||||
);
|
||||
|
||||
constructor(private _store: PurchasingOptionsListModalStore) {}
|
||||
|
||||
optionChange(option: string) {
|
||||
if (this._store.selectedFilterOption === option) {
|
||||
this._store.selectedFilterOption = undefined;
|
||||
} else {
|
||||
this._store.selectedFilterOption = option;
|
||||
}
|
||||
}
|
||||
|
||||
async selectBranch(branch: BranchDTO) {
|
||||
this._store.lastSelectedFilterOption$.next(undefined);
|
||||
|
||||
this._store.selectedPickUpBranch = branch;
|
||||
|
||||
const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise();
|
||||
shoppingCartItems.forEach((item) => this._store.loadPickUpAvailability({ item }));
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
<div class="item-thumbnail">
|
||||
<img loading="lazy" *ngIf="item?.product?.ean | productImage; let thumbnailUrl" [src]="thumbnailUrl" [alt]="item?.product?.name" />
|
||||
</div>
|
||||
|
||||
<div class="item-contributors">
|
||||
{{ item.product.contributors }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="item-title"
|
||||
[class.xl]="item?.product?.name?.length >= 35"
|
||||
[class.lg]="item?.product?.name?.length >= 40"
|
||||
[class.md]="item?.product?.name?.length >= 50"
|
||||
[class.sm]="item?.product?.name?.length >= 60"
|
||||
[class.xs]="item?.product?.name?.length >= 100"
|
||||
>
|
||||
{{ item?.product?.name }}
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="canAdd$ | async; let canAdd">
|
||||
<div class="item-can-add" *ngIf="canAdd !== true">
|
||||
{{ canAdd }}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail">
|
||||
<img
|
||||
*ngIf="item?.product?.format !== '--'"
|
||||
src="assets/images/Icon_{{ item?.product?.format }}.svg"
|
||||
[alt]="item?.product?.formatDetail"
|
||||
/>
|
||||
{{ item?.product?.formatDetail }}
|
||||
</div>
|
||||
|
||||
<div class="item-info">
|
||||
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }} <br />
|
||||
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
|
||||
{{ item?.product?.publicationDate | date: 'dd. MMMM yyyy' }}
|
||||
</div>
|
||||
|
||||
<div class="item-price-stock">
|
||||
<div class="price">
|
||||
<ng-container *ngIf="showTooltip$ | async">
|
||||
<button [uiOverlayTrigger]="tooltipContent" #tooltip="uiOverlayTrigger" class="info-tooltip-button" type="button">
|
||||
i
|
||||
</button>
|
||||
<ui-tooltip #tooltipContent yPosition="above" xPosition="after" [yOffset]="-16">
|
||||
Günstigerer Preis aus Hugendubel Katalog wird übernommen
|
||||
</ui-tooltip>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ui-quantity-dropdown
|
||||
[disabled]="fetchingAvailabilities$ | async"
|
||||
[ngModel]="item.quantity"
|
||||
(ngModelChange)="changeQuantity($event)"
|
||||
[range]="quantityRange$ | async"
|
||||
>
|
||||
</ui-quantity-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item-select">
|
||||
<ui-select-bullet
|
||||
*ngIf="selectVisible$ | async"
|
||||
[disabled]="selectDisabled$ | async"
|
||||
[ngModel]="isSelected$ | async"
|
||||
(ngModelChange)="selected($event)"
|
||||
></ui-select-bullet>
|
||||
</div>
|
||||
|
||||
<div class="item-availability">
|
||||
<div class="fetching" *ngIf="fetchingAvailabilities$ | async; else availabilities"></div>
|
||||
<ng-template #availabilities>
|
||||
<ng-container *ngIf="notAvailable$ | async; else available">
|
||||
<span class="hint">Derzeit nicht bestellbar</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #available>
|
||||
<span>Verfügbar als</span>
|
||||
<div *ngIf="takeAwayAvailabilities$ | async; let takeAwayAvailabilites">
|
||||
<ui-icon icon="shopping_bag" size="18px"></ui-icon>
|
||||
<span class="instock">{{ takeAwayAvailabilites?.inStock }}x</span> ab sofort
|
||||
</div>
|
||||
|
||||
<div *ngIf="!!(pickUpAvailabilities$ | async)">
|
||||
<ui-icon icon="box_out" size="18px"></ui-icon>
|
||||
{{ (pickUpAvailabilities$ | async)?.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
|
||||
</div>
|
||||
|
||||
<div *ngIf="!!(deliveryDigAvailabilities$ | async); else b2b">
|
||||
<ui-icon class="truck" icon="truck" size="30px"></ui-icon>
|
||||
<ng-container *ngIf="deliveryDigAvailabilities$ | async; let deliveryDigAvailabilities">
|
||||
<ng-container *ngIf="deliveryDigAvailabilities?.estimatedDelivery; else estimatedShippingDate">
|
||||
{{ (deliveryDigAvailabilities?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} -
|
||||
{{ (deliveryDigAvailabilities?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
|
||||
</ng-container>
|
||||
<ng-template #estimatedShippingDate>
|
||||
{{ deliveryDigAvailabilities.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #b2b>
|
||||
<div *ngIf="!!(deliveryB2bAvailabilities$ | async)">
|
||||
<ui-icon class="truck-b2b" icon="truck_b2b" size="40px"></ui-icon>
|
||||
{{ (deliveryB2bAvailabilities$ | async)?.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user