mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
87 Commits
fix/3810-s
...
ipad-bugfi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e31c5f5a19 | ||
|
|
22837bbf8d | ||
|
|
6324486dca | ||
|
|
653100f539 | ||
|
|
d671ba583d | ||
|
|
fba324c6cb | ||
|
|
ccbec5bcff | ||
|
|
da1978bf21 | ||
|
|
1672b89775 | ||
|
|
d9a9db6ec8 | ||
|
|
89b8d07bb4 | ||
|
|
c65c8edd2d | ||
|
|
5f7ce96919 | ||
|
|
ca10c01398 | ||
|
|
24f6ba117d | ||
|
|
7508144e27 | ||
|
|
9f0fec8046 | ||
|
|
67d70fac8e | ||
|
|
1775f6fd89 | ||
|
|
438c367101 | ||
|
|
cb9d8ffa91 | ||
|
|
2ecb0c5cf6 | ||
|
|
8a55d52b2b | ||
|
|
762a5a2072 | ||
|
|
405e1ed023 | ||
|
|
41177436d4 | ||
|
|
d1fca976a2 | ||
|
|
59cf407c26 | ||
|
|
e95828a514 | ||
|
|
8b915c7c83 | ||
|
|
ebc6a01b7a | ||
|
|
8cad3c4c14 | ||
|
|
a5537c21a1 | ||
|
|
edf96434b7 | ||
|
|
257df95c72 | ||
|
|
a268df503a | ||
|
|
a57ccbe4c2 | ||
|
|
7f37771dc7 | ||
|
|
3a753dde83 | ||
|
|
8e91b1363b | ||
|
|
e77929ab89 | ||
|
|
02031e97e3 | ||
|
|
ede1c505d4 | ||
|
|
5cd5b685d2 | ||
|
|
d856f1d1cc | ||
|
|
10bb912bda | ||
|
|
3b2135a570 | ||
|
|
9c5d209887 | ||
|
|
763a770bcf | ||
|
|
f8f3456ba3 | ||
|
|
4da7f02cf7 | ||
|
|
4772e24c78 | ||
|
|
c3a9b82abb | ||
|
|
4716940708 | ||
|
|
0eb09e2dbb | ||
|
|
111a33b12f | ||
|
|
bcff2272ab | ||
|
|
f30ae91854 | ||
|
|
7eaad843a9 | ||
|
|
cb367d32c3 | ||
|
|
b4cf88bd54 | ||
|
|
b8097fcd3a | ||
|
|
6db2238096 | ||
|
|
cd426d5534 | ||
|
|
c214d47aad | ||
|
|
c09c44ec5f | ||
|
|
f9f6d0d836 | ||
|
|
74a6c75c21 | ||
|
|
526ebc77bc | ||
|
|
addac44c0f | ||
|
|
303d575fde | ||
|
|
bf8438b229 | ||
|
|
ea8bbafbfa | ||
|
|
14815e79d5 | ||
|
|
4a3de35224 | ||
|
|
974f549c31 | ||
|
|
76596939c5 | ||
|
|
138974bca7 | ||
|
|
02aee02694 | ||
|
|
e2ada75611 | ||
|
|
78b757c55b | ||
|
|
dfd273e7bf | ||
|
|
1a72c23412 | ||
|
|
55a92ad029 | ||
|
|
aea6a0d131 | ||
|
|
e76e031675 | ||
|
|
dc42107668 |
34
angular.json
34
angular.json
@@ -1600,6 +1600,40 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@domain/product-list": {
|
||||
"projectType": "library",
|
||||
"root": "apps/domain/product-list",
|
||||
"sourceRoot": "apps/domain/product-list/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "apps/domain/product-list/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "apps/domain/product-list/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "apps/domain/product-list/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "apps/domain/product-list/tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export class NativeScanAdapter implements ScanAdapter {
|
||||
|
||||
init(): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(this.nativeContainerService.isUiWebview().isNative);
|
||||
resolve(this.nativeContainerService.isNative);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Store } from '@ngrx/store';
|
||||
import { BranchDTO } from '@swagger/checkout';
|
||||
import { isBoolean, isNumber } from '@utils/common';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { first, map, switchMap } from 'rxjs/operators';
|
||||
import { ApplicationProcess } from './defs';
|
||||
import {
|
||||
removeProcess,
|
||||
@@ -70,7 +70,13 @@ export class ApplicationService {
|
||||
this.store.dispatch(patchProcessData({ processId, data }));
|
||||
}
|
||||
|
||||
getSelectedBranch$(processId: number): Observable<BranchDTO> {
|
||||
getSelectedBranch$(processId?: number): Observable<BranchDTO> {
|
||||
if (!processId) {
|
||||
return this.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranch)))
|
||||
);
|
||||
}
|
||||
|
||||
return this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranch));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { AuthService } from './auth.service';
|
||||
import { OAuthModule } from 'angular-oauth2-oidc';
|
||||
@NgModule({})
|
||||
import { IfRoleDirective } from './if-role.directive';
|
||||
@NgModule({
|
||||
declarations: [IfRoleDirective],
|
||||
exports: [IfRoleDirective],
|
||||
})
|
||||
export class AuthModule {
|
||||
static forRoot(): ModuleWithProviders<AuthModule> {
|
||||
return {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { coerceArray, coerceStringArray } from '@angular/cdk/coercion';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Config } from '@core/config';
|
||||
import { isNullOrUndefined } from '@utils/common';
|
||||
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
|
||||
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { asapScheduler, BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -14,20 +15,29 @@ export class AuthService {
|
||||
return this._initialized.asObservable();
|
||||
}
|
||||
|
||||
constructor(private _config: Config, private readonly _oAuthService: OAuthService) {}
|
||||
private _authConfig: AuthConfig;
|
||||
|
||||
constructor(private _config: Config, private readonly _oAuthService: OAuthService) {
|
||||
this._oAuthService.events?.subscribe((event) => {
|
||||
if (event.type === 'token_received') {
|
||||
console.log('SSO Token Expiration:', new Date(this._oAuthService.getAccessTokenExpiration()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this._initialized.getValue()) {
|
||||
throw new Error('AuthService is already initialized');
|
||||
}
|
||||
|
||||
const authConfig: AuthConfig = this._config.get('@core/auth');
|
||||
this._authConfig = this._config.get('@core/auth');
|
||||
|
||||
authConfig.redirectUri = window.location.origin;
|
||||
authConfig.silentRefreshRedirectUri = window.location.origin + '/silent-refresh.html';
|
||||
authConfig.useSilentRefresh = true;
|
||||
this._authConfig.redirectUri = window.location.origin;
|
||||
|
||||
this._oAuthService.configure(authConfig);
|
||||
this._authConfig.silentRefreshRedirectUri = window.location.origin + '/silent-refresh.html';
|
||||
this._authConfig.useSilentRefresh = true;
|
||||
|
||||
this._oAuthService.configure(this._authConfig);
|
||||
this._oAuthService.tokenValidationHandler = new JwksValidationHandler();
|
||||
|
||||
this._oAuthService.setupAutomaticSilentRefresh();
|
||||
@@ -84,5 +94,32 @@ export class AuthService {
|
||||
|
||||
async logout() {
|
||||
await this._oAuthService.revokeTokenAndLogout();
|
||||
// asapScheduler.schedule(() => {
|
||||
// window.location.reload();
|
||||
// }, 250);
|
||||
}
|
||||
|
||||
hasRole(role: string | string[]) {
|
||||
const roles = coerceArray(role);
|
||||
|
||||
const userRoles = this.getClaimByKey('role');
|
||||
|
||||
if (isNullOrUndefined(userRoles)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return roles.every((r) => userRoles.includes(r));
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
try {
|
||||
if (this._authConfig.responseType.includes('code') && this._authConfig.scope.includes('offline_access')) {
|
||||
await this._oAuthService.refreshToken();
|
||||
} else {
|
||||
await this._oAuthService.silentRefresh();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
65
apps/core/auth/src/lib/if-role.directive.spec.ts
Normal file
65
apps/core/auth/src/lib/if-role.directive.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
// import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator';
|
||||
// import { IfRoleDirective } from './if-role.directive';
|
||||
// import { AuthService } from './auth.service';
|
||||
// import { TemplateRef, ViewContainerRef } from '@angular/core';
|
||||
|
||||
// describe('IfRoleDirective', () => {
|
||||
// let spectator: SpectatorDirective<IfRoleDirective>;
|
||||
// const createDirective = createDirectiveFactory({
|
||||
// directive: IfRoleDirective,
|
||||
// mocks: [AuthService],
|
||||
// });
|
||||
|
||||
// it('should create an instance', () => {
|
||||
// spectator = createDirective(`<div *ifRole="'admin'"></div>`);
|
||||
// expect(spectator.directive).toBeTruthy();
|
||||
// });
|
||||
|
||||
// it('should render template when user has the role', () => {
|
||||
// spectator = createDirective(`<div *ifRole="'admin'"></div>`);
|
||||
// const authService = spectator.inject(AuthService);
|
||||
// const viewContainerRef = spectator.inject(ViewContainerRef);
|
||||
// const templateRef = spectator.inject(TemplateRef);
|
||||
// authService.hasRole.and.returnValue(true);
|
||||
// spectator.directive.ngOnChanges();
|
||||
// expect(viewContainerRef.createEmbeddedView).toHaveBeenCalledWith(templateRef, spectator.directive.getContext());
|
||||
// });
|
||||
|
||||
// it('should render else template when user does not have the role', () => {
|
||||
// authService.hasRole.and.returnValue(false);
|
||||
// const elseTemplateRef = {} as TemplateRef<any>;
|
||||
// spectator = createDirective(`<ng-template #elseTemplateRef></ng-template><div *ifRole="'admin'; else elseTemplateRef"></div>`, {
|
||||
// hostProps: {
|
||||
// elseTemplateRef,
|
||||
// },
|
||||
// });
|
||||
// spectator.directive.ngOnChanges();
|
||||
// expect(viewContainerRef.createEmbeddedView).toHaveBeenCalledWith(elseTemplateRef, spectator.directive.getContext());
|
||||
// });
|
||||
|
||||
// it('should render else template when user does not have the role using ifNotRole input', () => {
|
||||
// authService.hasRole.and.returnValue(false);
|
||||
// const elseTemplateRef = {} as TemplateRef<any>;
|
||||
// spectator = createDirective(`<ng-template #elseTemplateRef></ng-template><div *ifNotRole="'admin'; else elseTemplateRef"></div>`, {
|
||||
// hostProps: {
|
||||
// elseTemplateRef,
|
||||
// },
|
||||
// });
|
||||
// spectator.directive.ngOnChanges();
|
||||
// expect(viewContainerRef.createEmbeddedView).toHaveBeenCalledWith(elseTemplateRef, spectator.directive.getContext());
|
||||
// });
|
||||
|
||||
// it('should clear view when user does not have the role and elseTemplateRef is not defined', () => {
|
||||
// authService.hasRole.and.returnValue(false);
|
||||
// spectator = createDirective(`<div *ifRole="'admin'"></div>`);
|
||||
// spectator.directive.ngOnChanges();
|
||||
// expect(viewContainerRef.clear).toHaveBeenCalled();
|
||||
// });
|
||||
|
||||
// it('should set $implicit to ifRole or ifNotRole input', () => {
|
||||
// spectator = createDirective(`<div *ifRole="'admin'"></div>`);
|
||||
// expect(spectator.directive.getContext().$implicit).toEqual('admin');
|
||||
// spectator.setInput('ifNotRole', 'user');
|
||||
// expect(spectator.directive.getContext().$implicit).toEqual('user');
|
||||
// });
|
||||
// });
|
||||
59
apps/core/auth/src/lib/if-role.directive.ts
Normal file
59
apps/core/auth/src/lib/if-role.directive.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Directive, Input, OnChanges, TemplateRef, ViewContainerRef } from '@angular/core';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[ifRole],[ifRoleElse],[ifNotRole],[ifNotRoleElse]',
|
||||
})
|
||||
export class IfRoleDirective implements OnChanges {
|
||||
@Input()
|
||||
ifRole: string | string[];
|
||||
|
||||
@Input()
|
||||
ifRoleElse: TemplateRef<any>;
|
||||
|
||||
@Input()
|
||||
ifNotRole: string | string[];
|
||||
|
||||
@Input()
|
||||
ifNotRoleElse: TemplateRef<any>;
|
||||
|
||||
get renderTemplateRef() {
|
||||
if (this.ifRole) {
|
||||
return this._authService.hasRole(this.ifRole);
|
||||
}
|
||||
if (this.ifNotRole) {
|
||||
return !this._authService.hasRole(this.ifNotRole);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
get elseTemplateRef() {
|
||||
return this.ifRoleElse || this.ifNotRoleElse;
|
||||
}
|
||||
|
||||
constructor(private _templateRef: TemplateRef<any>, private _viewContainer: ViewContainerRef, private _authService: AuthService) {}
|
||||
|
||||
ngOnChanges() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.renderTemplateRef) {
|
||||
this._viewContainer.createEmbeddedView(this._templateRef, this.getContext());
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.elseTemplateRef) {
|
||||
this._viewContainer.createEmbeddedView(this.elseTemplateRef, this.getContext());
|
||||
return;
|
||||
}
|
||||
|
||||
this._viewContainer.clear();
|
||||
}
|
||||
|
||||
getContext(): { $implicit: string | string[] } {
|
||||
return {
|
||||
$implicit: this.ifRole || this.ifNotRole,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,26 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { NativeContainerService } from 'native-container';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EnvironmentService {
|
||||
constructor(private _platform: Platform) {}
|
||||
constructor(private _platform: Platform, private _nativeContainer: NativeContainerService) {}
|
||||
|
||||
isDesktop(): boolean {
|
||||
return !this.isTablet();
|
||||
}
|
||||
|
||||
isTablet(): boolean {
|
||||
return this._platform.ANDROID || this._platform.IOS;
|
||||
return this.isNative() || this.isSafari();
|
||||
}
|
||||
|
||||
isNative(): boolean {
|
||||
return this._nativeContainer.isNative;
|
||||
}
|
||||
|
||||
isSafari(): boolean {
|
||||
return this._platform.SAFARI;
|
||||
return (this._platform.ANDROID || this._platform.IOS) && this._platform.SAFARI;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getStockByBranch(branch: BranchDTO): Observable<StockDTO> {
|
||||
return this._stockService.StockGetStocksByBranch({ branchId: branch.id }).pipe(
|
||||
getStockByBranch(branchId: number): Observable<StockDTO> {
|
||||
return this._stockService.StockGetStocksByBranch({ branchId }).pipe(
|
||||
map((response) => response.result),
|
||||
map((result) => result?.find((_) => true)),
|
||||
shareReplay(1)
|
||||
@@ -149,7 +149,7 @@ export class DomainAvailabilityService {
|
||||
quantity: number;
|
||||
branch?: BranchDTO;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
const request = !!branch ? this.getStockByBranch(branch) : this.getDefaultStock();
|
||||
const request = !!branch ? this.getStockByBranch(branch.id) : this.getDefaultStock();
|
||||
return request.pipe(
|
||||
switchMap((s) =>
|
||||
combineLatest([
|
||||
@@ -160,7 +160,7 @@ export class DomainAvailabilityService {
|
||||
),
|
||||
map(([response, supplier, defaultBranch]) => {
|
||||
const price = item?.price;
|
||||
return this._mapToTakeAwayAvailability({ response, supplier, branch: branch ?? defaultBranch, quantity, price });
|
||||
return this._mapToTakeAwayAvailability({ response, supplier, branchId: branch.id ?? defaultBranch.id, quantity, price });
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
@@ -183,7 +183,7 @@ export class DomainAvailabilityService {
|
||||
this.getTakeAwaySupplier(),
|
||||
]).pipe(
|
||||
map(([response, supplier]) => {
|
||||
return this._mapToTakeAwayAvailability({ response, supplier, branch, quantity, price });
|
||||
return this._mapToTakeAwayAvailability({ response, supplier, branchId: branch.id, quantity, price });
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
@@ -193,16 +193,19 @@ export class DomainAvailabilityService {
|
||||
eans,
|
||||
price,
|
||||
quantity,
|
||||
branchId,
|
||||
}: {
|
||||
eans: string[];
|
||||
price: PriceDTO;
|
||||
quantity: number;
|
||||
branchId?: number;
|
||||
}): Observable<AvailabilityDTO> {
|
||||
return this.getDefaultStock().pipe(
|
||||
const request = !!branchId ? this.getStockByBranch(branchId) : this.getDefaultStock();
|
||||
return request.pipe(
|
||||
switchMap((s) => this._stockService.StockInStockByEAN({ eans, stockId: s.id })),
|
||||
withLatestFrom(this.getTakeAwaySupplier(), this.getDefaultBranch()),
|
||||
map(([response, supplier, branch]) => {
|
||||
return this._mapToTakeAwayAvailability({ response, supplier, branch, quantity, price });
|
||||
map(([response, supplier, defaultBranch]) => {
|
||||
return this._mapToTakeAwayAvailability({ response, supplier, branchId: branchId ?? defaultBranch.id, quantity, price });
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
@@ -476,17 +479,17 @@ export class DomainAvailabilityService {
|
||||
private _mapToTakeAwayAvailability({
|
||||
response,
|
||||
supplier,
|
||||
branch,
|
||||
branchId,
|
||||
quantity,
|
||||
price,
|
||||
}: {
|
||||
response: ResponseArgsOfIEnumerableOfStockInfoDTO;
|
||||
supplier: SupplierDTO;
|
||||
branch: BranchDTO;
|
||||
branchId: number;
|
||||
quantity: number;
|
||||
price: PriceDTO;
|
||||
}): AvailabilityDTO {
|
||||
const stockInfo = response.result?.find((si) => si.branchId === branch.id);
|
||||
const stockInfo = response.result?.find((si) => si.branchId === branchId);
|
||||
const inStock = stockInfo?.inStock ?? 0;
|
||||
const availability: AvailabilityDTO = {
|
||||
availabilityType: quantity <= inStock ? 1024 : 1, // 1024 (=Available)
|
||||
@@ -609,7 +612,7 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
|
||||
getInStock({ itemIds, branchId }: { itemIds: number[]; branchId: number }): Observable<StockInfoDTO[]> {
|
||||
return this.getStockByBranch({ id: branchId }).pipe(
|
||||
return this.getStockByBranch(branchId).pipe(
|
||||
mergeMap((stock) =>
|
||||
this._stockService.StockInStock({ articleIds: itemIds, stockId: stock.id }).pipe(map((response) => response.result))
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { StockInfoDTO } from '@swagger/remi';
|
||||
import { groupBy } from 'lodash';
|
||||
import { BehaviorSubject, combineLatest, Observable, OperatorFunction, Subject, timer } from 'rxjs';
|
||||
import { buffer, bufferTime, bufferWhen, debounceTime } from 'rxjs/operators';
|
||||
import { groupBy, isEqual, uniqWith } from 'lodash';
|
||||
import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
|
||||
import { buffer, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
import { DomainAvailabilityService } from './availability.service';
|
||||
|
||||
export type ItemBranch = { itemId: number; branchId: number };
|
||||
@@ -25,8 +25,9 @@ export class DomainInStockService {
|
||||
}
|
||||
|
||||
private _handleStockDataToFetch = (itemBranchData: ItemBranch[]) => {
|
||||
if (itemBranchData?.length > 0) {
|
||||
this._fetchStockData(itemBranchData);
|
||||
const unique = uniqWith(itemBranchData, isEqual);
|
||||
if (unique?.length > 0) {
|
||||
this._fetchStockData(unique);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -44,16 +45,17 @@ export class DomainInStockService {
|
||||
const key = this.getKey({ itemId, branchId });
|
||||
this._addToInStockQueue({ itemId, branchId });
|
||||
|
||||
const sub = combineLatest([this._inStockMap, this._inStockFetchingMap]).subscribe(([inStockMap, inStockFetchingMap]) => {
|
||||
const inStock: InStock = {
|
||||
itemId,
|
||||
branchId,
|
||||
inStock: inStockMap[key],
|
||||
fetching: inStockFetchingMap[key] ?? false,
|
||||
};
|
||||
|
||||
obs.next(inStock);
|
||||
});
|
||||
const sub = combineLatest([this._inStockMap, this._inStockFetchingMap])
|
||||
.pipe(distinctUntilChanged(isEqual))
|
||||
.subscribe(([inStockMap, inStockFetchingMap]) => {
|
||||
const inStock: InStock = {
|
||||
itemId,
|
||||
branchId,
|
||||
inStock: inStockMap[key],
|
||||
fetching: inStockFetchingMap[key] ?? false,
|
||||
};
|
||||
obs.next(inStock);
|
||||
});
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
@@ -71,10 +73,10 @@ export class DomainInStockService {
|
||||
this._inStockFetchingMap.next({ ...current, [key]: value });
|
||||
}
|
||||
|
||||
private _setInStock({ itemId, branchId }: ItemBranch, value: number) {
|
||||
private _setInStock({ itemId, branchId }: ItemBranch, inStock: number) {
|
||||
const key = this.getKey({ itemId, branchId });
|
||||
const current = this._inStockMap.getValue();
|
||||
this._inStockMap.next({ ...current, [key]: value });
|
||||
this._inStockMap.next({ ...current, [key]: inStock });
|
||||
}
|
||||
|
||||
private _fetchStockData(itemBranchData: ItemBranch[]) {
|
||||
@@ -92,9 +94,11 @@ export class DomainInStockService {
|
||||
itemIds.forEach((itemId) => {
|
||||
const stockInfo = stockInfos.find((stockInfo) => stockInfo.itemId === itemId && stockInfo.branchId === branchId);
|
||||
let inStock = 0;
|
||||
|
||||
if (stockInfo?.inStock) {
|
||||
inStock = stockInfo.inStock;
|
||||
}
|
||||
|
||||
this._setInStockFetching({ itemId, branchId }, false);
|
||||
this._setInStock({ itemId, branchId }, inStock);
|
||||
});
|
||||
|
||||
@@ -918,7 +918,7 @@ export class DomainCheckoutService {
|
||||
//#region Common
|
||||
|
||||
private updateProcessCount(processId: number, count: number) {
|
||||
this.applicationService.patchProcess(processId, { data: { count } });
|
||||
this.applicationService.patchProcessData(processId, { count });
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -22,7 +22,7 @@ export class PrintCompartmentLabelActionHandler extends ActionHandler<OrderItems
|
||||
content: PrintModalComponent,
|
||||
config: { showScrollbarY: false },
|
||||
data: {
|
||||
printImmediately: !this.nativeContainerService.isUiWebview().isNative,
|
||||
printImmediately: !this.nativeContainerService.isNative,
|
||||
printerType: 'Label',
|
||||
print: (printer) =>
|
||||
this.domainPrinterService
|
||||
|
||||
@@ -23,7 +23,7 @@ export class PrintShippingNoteActionHandler extends ActionHandler<OrderItemsCont
|
||||
content: PrintModalComponent,
|
||||
config: { showScrollbarY: false },
|
||||
data: {
|
||||
printImmediately: !this.nativeContainerService.isUiWebview().isNative,
|
||||
printImmediately: !this.nativeContainerService.isNative,
|
||||
printerType: 'Label',
|
||||
print: async (printer) => {
|
||||
try {
|
||||
|
||||
29
apps/domain/oms/src/lib/customer-order.service.ts
Normal file
29
apps/domain/oms/src/lib/customer-order.service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AutocompleteTokenDTO, OrderService, QueryTokenDTO } from '@swagger/oms';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DomainCustomerOrderService {
|
||||
constructor(private _orderService: OrderService) {}
|
||||
|
||||
complete(payload: AutocompleteTokenDTO) {
|
||||
return this._orderService.OrderKundenbestellungenAutocomplete(payload);
|
||||
}
|
||||
|
||||
search(payload: QueryTokenDTO) {
|
||||
return this._orderService.OrderKundenbestellungen({ ...payload });
|
||||
// branch_id'
|
||||
}
|
||||
|
||||
getOrderItemsByOrderNumber(orderNumber: string) {
|
||||
return this._orderService.OrderKundenbestellungen({
|
||||
filter: { all_branches: 'true', archive: 'true' },
|
||||
input: {
|
||||
qs: orderNumber,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
settings() {
|
||||
return this._orderService.OrderKundenbestellungenSettings();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
AutocompleteTokenDTO,
|
||||
BranchService,
|
||||
ChangeStockStatusCodeValues,
|
||||
HistoryDTO,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
OrderItemSubsetDTO,
|
||||
OrderListItemDTO,
|
||||
OrderService,
|
||||
QueryTokenDTO,
|
||||
ReceiptService,
|
||||
ResponseArgsOfIEnumerableOfHistoryDTO,
|
||||
StatusValues,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ReceiptOrderItemSubsetReferenceValues, ReceiptService } from '@swagger/oms';
|
||||
import { memorize } from '@utils/common';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class DomainReceiptService {
|
||||
@@ -9,9 +11,12 @@ export class DomainReceiptService {
|
||||
return this.receiptService.ReceiptCreateShippingNote2(params);
|
||||
}
|
||||
|
||||
@memorize({ ttl: 1000 })
|
||||
getReceipts(payload: ReceiptOrderItemSubsetReferenceValues) {
|
||||
return this.receiptService.ReceiptGetReceiptsByOrderItemSubset({
|
||||
payload: payload,
|
||||
});
|
||||
return this.receiptService
|
||||
.ReceiptGetReceiptsByOrderItemSubset({
|
||||
payload: payload,
|
||||
})
|
||||
.pipe(shareReplay(1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from './lib/receipt.service';
|
||||
export * from './lib/oms.service';
|
||||
export * from './lib/oms.module';
|
||||
export * from './lib/action-handlers';
|
||||
export * from './lib/customer-order.service';
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
CheckoutPrintService,
|
||||
ItemDTO,
|
||||
OMSPrintService,
|
||||
PriceQRCodeDTO,
|
||||
PrintRequestOfIEnumerableOfItemDTO,
|
||||
PrintRequestOfIEnumerableOfLong,
|
||||
PrintRequestOfIEnumerableOfPriceQRCodeDTO,
|
||||
@@ -13,6 +12,12 @@ import {
|
||||
ResponseArgs,
|
||||
LoyaltyCardPrintService,
|
||||
} from '@swagger/print';
|
||||
import {
|
||||
DocumentPayloadOfIEnumerableOfProductListItemDTO,
|
||||
ProductListItemDTO,
|
||||
ProductListService,
|
||||
ResponseArgsOfString,
|
||||
} from '@swagger/wws';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, filter, map, switchMap, timeout } from 'rxjs/operators';
|
||||
import { Printer } from './defs/printer.model';
|
||||
@@ -27,7 +32,8 @@ export class DomainPrinterService {
|
||||
private catalogPrintService: CatalogPrintService,
|
||||
private checkoutPrintService: CheckoutPrintService,
|
||||
private eisPublicDocumentService: EISPublicDocumentService,
|
||||
private _loyaltyCardPrintService: LoyaltyCardPrintService
|
||||
private _loyaltyCardPrintService: LoyaltyCardPrintService,
|
||||
private _productListService: ProductListService
|
||||
) {}
|
||||
|
||||
getAvailablePrinters(): Observable<Printer[] | { error: string }> {
|
||||
@@ -202,6 +208,29 @@ export class DomainPrinterService {
|
||||
});
|
||||
}
|
||||
|
||||
printProductListItemsResponse(payload: DocumentPayloadOfIEnumerableOfProductListItemDTO): Observable<ResponseArgsOfString> {
|
||||
return this._productListService.ProductListProductListItemPdfAsBase64(payload);
|
||||
}
|
||||
|
||||
printProductListItems({
|
||||
data,
|
||||
printer,
|
||||
title,
|
||||
}: {
|
||||
data: ProductListItemDTO[];
|
||||
printer: string;
|
||||
title?: string;
|
||||
}): Observable<ResponseArgs> {
|
||||
return this.printProductListItemsResponse({ data, title }).pipe(
|
||||
switchMap((res) => {
|
||||
if (!res.error) {
|
||||
return this.printPdf({ printer, data: res.result });
|
||||
}
|
||||
return of(res);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
printDisplayInfoDTOArticles({
|
||||
articles,
|
||||
printer,
|
||||
|
||||
25
apps/domain/product-list/README.md
Normal file
25
apps/domain/product-list/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# ProductList
|
||||
|
||||
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.0.0.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name --project product-list` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project product-list`.
|
||||
|
||||
> Note: Don't forget to add `--project product-list` or else it will be added to the default project in your `angular.json` file.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build product-list` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||
|
||||
## Publishing
|
||||
|
||||
After building your library with `ng build product-list`, go to the dist folder `cd dist/product-list` and run `npm publish`.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test product-list` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
||||
7
apps/domain/product-list/ng-package.json
Normal file
7
apps/domain/product-list/ng-package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../../dist/domain/product-list",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
11
apps/domain/product-list/package.json
Normal file
11
apps/domain/product-list/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@domain/product-list",
|
||||
"version": "0.0.1",
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^15.0.0",
|
||||
"@angular/core": "^15.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
}
|
||||
4
apps/domain/product-list/src/lib/product-list.module.ts
Normal file
4
apps/domain/product-list/src/lib/product-list.module.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
@NgModule({})
|
||||
export class ProductListModule {}
|
||||
52
apps/domain/product-list/src/lib/product-list.service.ts
Normal file
52
apps/domain/product-list/src/lib/product-list.service.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
BatchResponseArgsOfProductListItemDTOAndString,
|
||||
ListResponseArgsOfProductListItemDTO,
|
||||
ProductListItemDTO,
|
||||
ProductListService,
|
||||
QuerySettingsDTO,
|
||||
QueryTokenDTO,
|
||||
ResponseArgsOfProductListItemDTO,
|
||||
ResponseArgsOfQuerySettingsDTO,
|
||||
} from '@swagger/wws';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DomainProductListService {
|
||||
constructor(private _productList: ProductListService) {}
|
||||
|
||||
getQuerySettingsResponse(): Observable<ResponseArgsOfQuerySettingsDTO> {
|
||||
return this._productList.ProductListQueryProductListItemsSettings();
|
||||
}
|
||||
|
||||
getQuerySettings(): Observable<QuerySettingsDTO> {
|
||||
return this.getQuerySettingsResponse().pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
queryProductListResponse(queryToken: QueryTokenDTO): Observable<ListResponseArgsOfProductListItemDTO> {
|
||||
return this._productList.ProductListQueryProductListItem(queryToken);
|
||||
}
|
||||
|
||||
queryProductList(queryToken: QueryTokenDTO): Observable<ProductListItemDTO[]> {
|
||||
return this.queryProductListResponse(queryToken).pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
completeProductListItemResponse(productListItemUId: string): Observable<ResponseArgsOfProductListItemDTO> {
|
||||
return this._productList.ProductListProductListItemCompleted(productListItemUId);
|
||||
}
|
||||
|
||||
completeProductListItem(productListItemUId: string): Observable<ProductListItemDTO> {
|
||||
return this.completeProductListItemResponse(productListItemUId).pipe(map((response) => response.result));
|
||||
}
|
||||
|
||||
completeProductListItemsResponse(uids: string[]): Observable<BatchResponseArgsOfProductListItemDTOAndString> {
|
||||
return this._productList.ProductListProductListItemsCompleted(uids);
|
||||
}
|
||||
|
||||
completeProductListItems(uids: string[]): Observable<BatchResponseArgsOfProductListItemDTOAndString> {
|
||||
return this.completeProductListItemsResponse(uids);
|
||||
}
|
||||
}
|
||||
6
apps/domain/product-list/src/public-api.ts
Normal file
6
apps/domain/product-list/src/public-api.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
* Public API Surface of product-list
|
||||
*/
|
||||
|
||||
export * from './lib/product-list.service';
|
||||
export * from './lib/product-list.module';
|
||||
14
apps/domain/product-list/tsconfig.lib.json
Normal file
14
apps/domain/product-list/tsconfig.lib.json
Normal file
@@ -0,0 +1,14 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../out-tsc/lib",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
10
apps/domain/product-list/tsconfig.lib.prod.json
Normal file
10
apps/domain/product-list/tsconfig.lib.prod.json
Normal file
@@ -0,0 +1,10 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.lib.json",
|
||||
"compilerOptions": {
|
||||
"declarationMap": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"compilationMode": "partial"
|
||||
}
|
||||
}
|
||||
14
apps/domain/product-list/tsconfig.spec.json
Normal file
14
apps/domain/product-list/tsconfig.spec.json
Normal file
@@ -0,0 +1,14 @@
|
||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Inject, Injectable, InjectionToken } from '@angular/core';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { SignalrHub, SignalRHubOptions } from '@core/signalr';
|
||||
import { BehaviorSubject, merge, of } from 'rxjs';
|
||||
import { filter, map, shareReplay, tap, withLatestFrom } from 'rxjs/operators';
|
||||
@@ -9,7 +10,16 @@ export const NOTIFICATIONS_HUB_OPTIONS = new InjectionToken<SignalRHubOptions>('
|
||||
@Injectable()
|
||||
export class NotificationsHub extends SignalrHub {
|
||||
updateNotification$ = new BehaviorSubject<MessageBoardItemDTO>(undefined);
|
||||
constructor(@Inject(NOTIFICATIONS_HUB_OPTIONS) options: SignalRHubOptions) {
|
||||
|
||||
get branchNo() {
|
||||
return String(this._auth.getClaimByKey('branch_no') || this._auth.getClaimByKey('sub'));
|
||||
}
|
||||
|
||||
get sessionStoragesessionStorageKey() {
|
||||
return `NOTIFICATIONS_BOARD_${this.branchNo}`;
|
||||
}
|
||||
|
||||
constructor(@Inject(NOTIFICATIONS_HUB_OPTIONS) options: SignalRHubOptions, private _auth: AuthService) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
@@ -31,12 +41,12 @@ export class NotificationsHub extends SignalrHub {
|
||||
|
||||
private _storeNotifactions(data: EnvelopeDTO<MessageBoardItemDTO[]>) {
|
||||
if (data) {
|
||||
localStorage.setItem('NOTIFICATIONS_BOARD', JSON.stringify(data));
|
||||
sessionStorage.setItem(this.sessionStoragesessionStorageKey, JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
private _getNotifications(): EnvelopeDTO<MessageBoardItemDTO[]> {
|
||||
const stringData = localStorage.getItem('NOTIFICATIONS_BOARD');
|
||||
const stringData = sessionStorage.getItem(this.sessionStoragesessionStorageKey);
|
||||
if (stringData) {
|
||||
return JSON.parse(stringData);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isDevMode, NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { DebugComponent } from './debug/debug.component';
|
||||
import {
|
||||
CanActivateCartGuard,
|
||||
CanActivateCartWithProcessIdGuard,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
CanActivateTaskCalendarGuard,
|
||||
IsAuthenticatedGuard,
|
||||
} from './guards';
|
||||
import { CanActivateAssortmentGuard } from './guards/can-activate-assortment.guard';
|
||||
import { CanActivatePackageInspectionGuard } from './guards/can-activate-package-inspection.guard';
|
||||
import { PreviewComponent } from './preview';
|
||||
import { BranchSectionResolver, CustomerSectionResolver, ProcessIdResolver } from './resolvers';
|
||||
@@ -55,6 +57,17 @@ const routes: Routes = [
|
||||
canActivate: [CanActivateProductWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
},
|
||||
{
|
||||
path: 'order',
|
||||
loadChildren: () => import('@page/customer-order').then((m) => m.CustomerOrderModule),
|
||||
canActivate: [CanActivateGoodsOutGuard],
|
||||
},
|
||||
{
|
||||
path: ':processId/order',
|
||||
loadChildren: () => import('@page/customer-order').then((m) => m.CustomerOrderModule),
|
||||
canActivate: [CanActivateGoodsOutWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
},
|
||||
{
|
||||
path: 'customer',
|
||||
loadChildren: () => import('@page/customer').then((m) => m.PageCustomerModule),
|
||||
@@ -115,6 +128,11 @@ const routes: Routes = [
|
||||
loadChildren: () => import('@page/package-inspection').then((m) => m.PackageInspectionModule),
|
||||
canActivate: [CanActivatePackageInspectionGuard],
|
||||
},
|
||||
{
|
||||
path: 'assortment',
|
||||
loadChildren: () => import('@page/assortment').then((m) => m.AssortmentModule),
|
||||
canActivate: [CanActivateAssortmentGuard],
|
||||
},
|
||||
{ path: '**', redirectTo: 'task-calendar', pathMatch: 'full' },
|
||||
],
|
||||
resolve: { section: BranchSectionResolver },
|
||||
|
||||
@@ -9,6 +9,8 @@ import { CommonModule } from '@angular/common';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { NotificationsHub } from '@hub/notifications';
|
||||
import { UserStateService } from '@swagger/isa';
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { AuthService } from '@core/auth';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let spectator: Spectator<AppComponent>;
|
||||
@@ -21,7 +23,7 @@ describe('AppComponent', () => {
|
||||
component: AppComponent,
|
||||
imports: [CommonModule, RouterTestingModule],
|
||||
providers: [],
|
||||
mocks: [Config, SwUpdate, UserStateService],
|
||||
mocks: [Config, SwUpdate, UserStateService, UiModalService, AuthService],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Component, Inject, OnInit, Renderer2 } from '@angular/core';
|
||||
import { Component, HostListener, Inject, OnInit, Renderer2 } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { SwUpdate, UpdateAvailableEvent } from '@angular/service-worker';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { Config } from '@core/config';
|
||||
import { NotificationsHub } from '@hub/notifications';
|
||||
import packageInfo from 'package';
|
||||
import { interval, Observable, Subscription } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { Router } from '@angular/router';
|
||||
import { asapScheduler, interval, Observable, Subscription } from 'rxjs';
|
||||
import { UserStateService } from '@swagger/isa';
|
||||
import { IsaLogProvider } from './providers';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -41,19 +42,34 @@ export class AppComponent implements OnInit {
|
||||
private readonly _renderer: Renderer2,
|
||||
private readonly _swUpdate: SwUpdate,
|
||||
private readonly _notifications: NotificationsHub,
|
||||
private readonly _platform: Platform,
|
||||
private router: Router,
|
||||
private infoService: UserStateService
|
||||
private infoService: UserStateService,
|
||||
private readonly _environment: EnvironmentService,
|
||||
private readonly _authService: AuthService,
|
||||
private readonly _modal: UiModalService
|
||||
) {
|
||||
this.updateClient();
|
||||
IsaLogProvider.InfoService = infoService;
|
||||
IsaLogProvider.InfoService = this.infoService;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.setTitle();
|
||||
this.logVersion();
|
||||
this.determinePlatform();
|
||||
asapScheduler.schedule(() => this.determinePlatform(), 250);
|
||||
this._appService.getSection$().subscribe(this.sectionChangeHandler.bind(this));
|
||||
|
||||
this.setupSilentRefresh();
|
||||
}
|
||||
|
||||
// Setup interval for silent refresh
|
||||
setupSilentRefresh() {
|
||||
const silentRefreshInterval = this._config.get('silentRefresh.interval');
|
||||
if (silentRefreshInterval > 0) {
|
||||
interval(silentRefreshInterval).subscribe(() => {
|
||||
if (this._authService.isAuthenticated()) {
|
||||
this._authService.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setTitle() {
|
||||
@@ -65,13 +81,15 @@ export class AppComponent implements OnInit {
|
||||
}
|
||||
|
||||
determinePlatform() {
|
||||
if (this._platform.IOS && !this._platform.SAFARI) {
|
||||
this._renderer.addClass(this._document.body, 'tablet');
|
||||
if (this._environment.isNative()) {
|
||||
this._renderer.addClass(this._document.body, 'tablet-native');
|
||||
} else if (this._platform.IOS && this._platform.SAFARI) {
|
||||
this._renderer.addClass(this._document.body, 'tablet');
|
||||
} else if (this._environment.isTablet()) {
|
||||
this._renderer.addClass(this._document.body, 'tablet-browser');
|
||||
} else if (this._platform.isBrowser) {
|
||||
}
|
||||
if (this._environment.isTablet()) {
|
||||
this._renderer.addClass(this._document.body, 'tablet');
|
||||
}
|
||||
if (this._environment.isDesktop()) {
|
||||
this._renderer.addClass(this._document.body, 'desktop');
|
||||
}
|
||||
}
|
||||
@@ -86,49 +104,6 @@ export class AppComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Implementation before Angular Version 13.x.x
|
||||
|
||||
// async updateClient() {
|
||||
// if (!this._swUpdate.isEnabled) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// await this.initialCheckForUpdate();
|
||||
// this.checkForUpdateInterval();
|
||||
// }
|
||||
|
||||
// checkForUpdateInterval() {
|
||||
// this.updateAvailableObs = this._swUpdate.available.pipe(
|
||||
// tap((availableEvent) => {
|
||||
// if (availableEvent?.current?.hash !== availableEvent?.available?.hash) {
|
||||
// this._notifications.updateNotification();
|
||||
// this.subscriptions.unsubscribe();
|
||||
// }
|
||||
// })
|
||||
// );
|
||||
// this.subscriptions.add(
|
||||
// interval(this._checkForUpdates).subscribe(async () => {
|
||||
// await this._swUpdate.checkForUpdate();
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
|
||||
// async initialCheckForUpdate() {
|
||||
// this.updateAvailableObs = this._swUpdate.available.pipe(
|
||||
// tap((availableEvent) => {
|
||||
// if (availableEvent?.current?.hash !== availableEvent?.available?.hash) {
|
||||
// location.reload();
|
||||
// }
|
||||
// })
|
||||
// );
|
||||
// this.subscriptions.add(this.updateAvailableObs.subscribe());
|
||||
// await this._swUpdate.checkForUpdate();
|
||||
// }
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Implementation for Angular Version 13.x.x
|
||||
|
||||
updateClient() {
|
||||
if (!this._swUpdate.isEnabled) {
|
||||
return;
|
||||
@@ -155,4 +130,22 @@ export class AppComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('window:visibilitychange', ['$event'])
|
||||
onVisibilityChange(event: Event) {
|
||||
// refresh token when app is in background
|
||||
if (this._document.hidden && this._authService.isAuthenticated()) {
|
||||
this._authService.refresh();
|
||||
} else if (!this._authService.isAuthenticated()) {
|
||||
return this._modal
|
||||
.open({
|
||||
content: UiMessageModalComponent,
|
||||
title: 'Sie sind nicht mehr angemeldet',
|
||||
data: { message: 'Sie werden neu angemeldet' },
|
||||
})
|
||||
.afterClosed$.subscribe(() => {
|
||||
this._authService.login();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,18 @@ import { RootStateService } from './store/root-state.service';
|
||||
import * as Commands from './commands';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { PreviewComponent } from './preview';
|
||||
import { NativeContainerService } from 'native-container';
|
||||
|
||||
registerLocaleData(localeDe, localeDeExtra);
|
||||
registerLocaleData(localeDe, 'de', localeDeExtra);
|
||||
|
||||
export function _appInitializerFactory(config: Config, auth: AuthService, injector: Injector, scanAdapter: ScanAdapterService) {
|
||||
export function _appInitializerFactory(
|
||||
config: Config,
|
||||
auth: AuthService,
|
||||
injector: Injector,
|
||||
scanAdapter: ScanAdapterService,
|
||||
nativeContainer: NativeContainerService
|
||||
) {
|
||||
return async () => {
|
||||
const statusElement = document.querySelector('#init-status');
|
||||
statusElement.innerHTML = 'Konfigurationen werden geladen...';
|
||||
@@ -52,6 +59,10 @@ export function _appInitializerFactory(config: Config, auth: AuthService, inject
|
||||
await state.init();
|
||||
}
|
||||
|
||||
statusElement.innerHTML = 'Native Container wird initialisiert...';
|
||||
await nativeContainer.init();
|
||||
|
||||
statusElement.innerHTML = 'Scanner wird initialisiert...';
|
||||
await scanAdapter.init();
|
||||
};
|
||||
}
|
||||
@@ -104,7 +115,7 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: _appInitializerFactory,
|
||||
multi: true,
|
||||
deps: [Config, AuthService, Injector, ScanAdapterService],
|
||||
deps: [Config, AuthService, Injector, ScanAdapterService, NativeContainerService],
|
||||
},
|
||||
{
|
||||
provide: NOTIFICATIONS_HUB_OPTIONS,
|
||||
|
||||
11
apps/isa-app/src/app/debug/debug.component.html
Normal file
11
apps/isa-app/src/app/debug/debug.component.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="odd:bg-slate-200 grid grid-flow-col justify-start" *ngFor="let log of logs$ | async">
|
||||
<div class="p-2 w-100 grow-0">
|
||||
{{ log.timestamp | date }}
|
||||
</div>
|
||||
<div class="p-2 w-50 grow-0">
|
||||
{{ log.type }}
|
||||
</div>
|
||||
<div class="p-2 grow">
|
||||
{{ log.args | json }}
|
||||
</div>
|
||||
</div>
|
||||
6
apps/isa-app/src/app/debug/debug.component.scss
Normal file
6
apps/isa-app/src/app/debug/debug.component.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
:host {
|
||||
@apply block;
|
||||
}
|
||||
:host {
|
||||
@apply grid grid-flow-row bg-white overflow-scroll;
|
||||
}
|
||||
17
apps/isa-app/src/app/debug/debug.component.ts
Normal file
17
apps/isa-app/src/app/debug/debug.component.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { DebugService } from './debug.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-debug',
|
||||
templateUrl: 'debug.component.html',
|
||||
styleUrls: ['debug.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
})
|
||||
export class DebugComponent {
|
||||
logs$ = this.debugService.logs$;
|
||||
|
||||
constructor(private debugService: DebugService) {}
|
||||
}
|
||||
25
apps/isa-app/src/app/debug/debug.service.ts
Normal file
25
apps/isa-app/src/app/debug/debug.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, fromEvent } from 'rxjs';
|
||||
|
||||
export interface ConsoleLog {
|
||||
timestamp?: Date;
|
||||
type: 'log' | 'warn' | 'error';
|
||||
args: any[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DebugService {
|
||||
private _consoleSubject = new BehaviorSubject<ConsoleLog[]>([]);
|
||||
|
||||
logs$ = this._consoleSubject.asObservable();
|
||||
|
||||
constructor() {
|
||||
fromEvent(window, 'message').subscribe((event: MessageEvent) => {
|
||||
this.add({ type: 'log', args: [event.data] });
|
||||
});
|
||||
}
|
||||
|
||||
add(log: ConsoleLog) {
|
||||
this._consoleSubject.next([...this._consoleSubject.value, { ...log, timestamp: new Date() }]);
|
||||
}
|
||||
}
|
||||
24
apps/isa-app/src/app/guards/can-activate-assortment.guard.ts
Normal file
24
apps/isa-app/src/app/guards/can-activate-assortment.guard.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { Config } from '@core/config';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanActivateAssortmentGuard implements CanActivate {
|
||||
constructor(private readonly _applicationService: ApplicationService, private readonly _config: Config) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const process = await this._applicationService.getProcessById$(this._config.get('process.ids.assortment')).pipe(first()).toPromise();
|
||||
if (!process) {
|
||||
await this._applicationService.createProcess({
|
||||
id: this._config.get('process.ids.assortment'),
|
||||
type: 'assortment',
|
||||
section: 'branch',
|
||||
name: 'Sortiment',
|
||||
});
|
||||
}
|
||||
this._applicationService.activateProcess(this._config.get('process.ids.assortment'));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export class IsAuthenticatedGuard implements CanActivate {
|
||||
|
||||
const result = await this._scanService
|
||||
.scan({
|
||||
exclude: ['Dev', 'Native'],
|
||||
exclude: ['Dev'],
|
||||
})
|
||||
?.toPromise();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpInterceptor, HttpEvent, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { NEVER, Observable, throwError } from 'rxjs';
|
||||
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
|
||||
import { catchError, mergeMap, tap } from 'rxjs/operators';
|
||||
import { AuthService } from '@core/auth';
|
||||
@@ -25,6 +25,7 @@ export class HttpErrorInterceptor implements HttpInterceptor {
|
||||
})
|
||||
.afterClosed$.pipe(mergeMap(() => throwError(error)));
|
||||
} else if (error.status === 401) {
|
||||
console.log('401', error);
|
||||
return this._modal
|
||||
.open({
|
||||
content: UiMessageModalComponent,
|
||||
@@ -35,7 +36,7 @@ export class HttpErrorInterceptor implements HttpInterceptor {
|
||||
tap(() => {
|
||||
this._auth.login();
|
||||
}),
|
||||
mergeMap(() => throwError(error))
|
||||
mergeMap(() => NEVER)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,10 @@
|
||||
<shared-branch-selector [value]="selectedBranch$ | async" (valueChange)="setNewBranch($event)"></shared-branch-selector>
|
||||
<h1>Platform: {{ platform | json }}</h1>
|
||||
<br />
|
||||
<h1>{{ appVersion }}</h1>
|
||||
<br />
|
||||
<h1>{{ userAgent }}</h1>
|
||||
<br />
|
||||
<h1>Navigator: {{ navigator | json }}</h1>
|
||||
<br />
|
||||
<br />
|
||||
<h1>Device: {{ device }}</h1>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Platform, PlatformModule } from '@angular/cdk/platform';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { BranchSelectorComponent } from '@shared/components/branch-selector';
|
||||
@@ -8,13 +9,48 @@ import { BehaviorSubject } from 'rxjs';
|
||||
selector: 'app-preview',
|
||||
templateUrl: 'preview.component.html',
|
||||
styleUrls: ['preview.component.css'],
|
||||
imports: [CommonModule, BranchSelectorComponent],
|
||||
imports: [CommonModule, BranchSelectorComponent, PlatformModule],
|
||||
standalone: true,
|
||||
})
|
||||
export class PreviewComponent implements OnInit {
|
||||
selectedBranch$ = new BehaviorSubject<BranchDTO>({});
|
||||
|
||||
constructor() {}
|
||||
get appVersion() {
|
||||
return 'App Version: ' + (window.navigator as any).appVersion;
|
||||
}
|
||||
|
||||
get userAgent() {
|
||||
return 'User Agent: ' + (window.navigator as any).userAgent;
|
||||
}
|
||||
|
||||
get navigator() {
|
||||
const nav = {};
|
||||
for (let i in window.navigator) nav[i] = navigator[i];
|
||||
return nav;
|
||||
}
|
||||
|
||||
get platform() {
|
||||
return this._platform;
|
||||
}
|
||||
|
||||
get device() {
|
||||
const isIpadNative = this._platform.IOS && !this._platform.SAFARI;
|
||||
const isIpadMini6Native = window?.navigator?.userAgent?.includes('Macintosh') && !this._platform.SAFARI;
|
||||
const isNative = isIpadNative || isIpadMini6Native;
|
||||
const isPWA = this._platform.IOS && this._platform.SAFARI;
|
||||
const isDesktop = !isNative && !isPWA;
|
||||
if (isNative) {
|
||||
if (isIpadMini6Native) {
|
||||
return 'IPAD mini 6 Native App';
|
||||
} else if (isIpadNative) {
|
||||
return 'IPAD mini 2 Native App or IPAD mini 5 Native App';
|
||||
}
|
||||
} else if (isPWA) {
|
||||
return 'IPAD Safari PWA';
|
||||
} else if (isDesktop) return 'Desktop or Macintosh';
|
||||
}
|
||||
|
||||
constructor(private readonly _platform: Platform) {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="shell-footer-wrapper">
|
||||
<shell-footer *ngIf="section$ | async; let section">
|
||||
<ng-container *ngIf="section === 'customer'">
|
||||
<a (click)="resetSelectedBranch()" [routerLink]="[customerBasePath$ | async, 'product']" routerLinkActive="active">
|
||||
<a [routerLink]="[customerBasePath$ | async, 'product']" routerLinkActive="active">
|
||||
<ui-icon icon="catalog" size="30px"></ui-icon>
|
||||
Artikelsuche
|
||||
</a>
|
||||
@@ -47,12 +47,20 @@
|
||||
<ui-icon icon="customer" size="24px"></ui-icon>
|
||||
Kundensuche
|
||||
</a>
|
||||
<a [routerLink]="[customerBasePath$ | async, 'goods', 'out']" routerLinkActive="active">
|
||||
<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
|
||||
@@ -72,3 +80,9 @@
|
||||
</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,5 +1,5 @@
|
||||
:host {
|
||||
@apply block;
|
||||
@apply block relative min-h-screen;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { AuthModule, AuthService } from '@core/auth';
|
||||
import { Config } from '@core/config';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
@@ -16,7 +16,7 @@ import { DashboardComponent } from '@page/dashboard';
|
||||
import { ShellFooterComponent } from '@shell/footer';
|
||||
import { ShellHeaderComponent } from '@shell/header';
|
||||
import { ShellProcessComponent, ShellProcessTabComponent } from '@shell/process';
|
||||
import { UiIconComponent } from '@ui/icon';
|
||||
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';
|
||||
@@ -46,19 +46,28 @@ describe('ShellComponent', () => {
|
||||
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),
|
||||
MockComponent(UiIconComponent),
|
||||
],
|
||||
mocks: [BreadcrumbService, DomainAvailabilityService, AuthService, DomainDashboardService, Config, WrongDestinationModalService],
|
||||
mocks: [
|
||||
BreadcrumbService,
|
||||
DomainAvailabilityService,
|
||||
AuthService,
|
||||
DomainDashboardService,
|
||||
Config,
|
||||
WrongDestinationModalService,
|
||||
IconRegistry,
|
||||
],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -174,11 +183,13 @@ describe('ShellComponent', () => {
|
||||
expect(spectator.query('shell-footer')).not.toBeVisible();
|
||||
});
|
||||
|
||||
it('should display the menu items for section customer', () => {
|
||||
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');
|
||||
@@ -193,12 +204,16 @@ describe('ShellComponent', () => {
|
||||
spectator.detectComponentChanges();
|
||||
|
||||
const anchors = spectator.queryAll('shell-footer a');
|
||||
expect(anchors[0]).toHaveText('Tätigkeitskalender');
|
||||
expect(anchors[0]).toHaveAttribute('href', '/filiale/task-calendar');
|
||||
expect(anchors[1]).toHaveText('Abholfach');
|
||||
expect(anchors[1]).toHaveAttribute('href', '/filiale/goods/in');
|
||||
expect(anchors[2]).toHaveText('Remission');
|
||||
expect(anchors[2]).toHaveAttribute('href', '/filiale/remission');
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ import { WrongDestinationModalService } from 'apps/page/package-inspection/src/l
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ShellComponent {
|
||||
isDevelopment = Boolean(this._config.get('debug'));
|
||||
|
||||
debugOpen = false;
|
||||
|
||||
@ViewChildren('processTabs')
|
||||
readonly processTabs: QueryList<ShellProcessTabComponent>;
|
||||
|
||||
@@ -164,13 +168,6 @@ export class ShellComponent {
|
||||
});
|
||||
}
|
||||
|
||||
async resetSelectedBranch() {
|
||||
const processId = await this.activatedProcessId$.pipe(take(1)).toPromise();
|
||||
if (!!processId) {
|
||||
this._appService.patchProcessData(processId, { selectedBranch: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
trackByIdFn: TrackByFunction<ApplicationProcess> = (_, process) => process.id;
|
||||
|
||||
fetchAndOpenPackages = () => this._wrongDestinationModalService.fetchAndOpen();
|
||||
|
||||
@@ -9,9 +9,21 @@ 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],
|
||||
imports: [
|
||||
RouterModule,
|
||||
CommonModule,
|
||||
ShellHeaderModule,
|
||||
ShellProcessModule,
|
||||
ShellFooterModule,
|
||||
UiIconModule,
|
||||
OverlayModule,
|
||||
AuthModule,
|
||||
DebugComponent,
|
||||
],
|
||||
exports: [ShellComponent],
|
||||
declarations: [ShellComponent],
|
||||
providers: [],
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"title": "ISA - Feature",
|
||||
"silentRefresh": {
|
||||
"interval": 300000
|
||||
},
|
||||
"@cdn/product-image": {
|
||||
"url": "https://produktbilder.paragon-data.net"
|
||||
},
|
||||
@@ -8,7 +11,7 @@
|
||||
"clientId": "hug-isa",
|
||||
"responseType": "id_token token",
|
||||
"oidc": true,
|
||||
"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"
|
||||
"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": {
|
||||
"logLevel": "debug"
|
||||
@@ -60,7 +63,8 @@
|
||||
"goodsIn": 2000,
|
||||
"taskCalendar": 3000,
|
||||
"remission": 4000,
|
||||
"packageInspection": 5000
|
||||
"packageInspection": 5000,
|
||||
"assortment": 6000
|
||||
}
|
||||
},
|
||||
"checkForUpdates": 3600000,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"title": "ISA - Integration",
|
||||
"silentRefresh": {
|
||||
"interval": 300000
|
||||
},
|
||||
"@cdn/product-image": {
|
||||
"url": "https://produktbilder.paragon-data.net"
|
||||
},
|
||||
@@ -8,7 +11,7 @@
|
||||
"clientId": "hug-isa",
|
||||
"responseType": "id_token token",
|
||||
"oidc": true,
|
||||
"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"
|
||||
"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": {
|
||||
"logLevel": "debug"
|
||||
@@ -60,7 +63,8 @@
|
||||
"goodsIn": 2000,
|
||||
"taskCalendar": 3000,
|
||||
"remission": 4000,
|
||||
"packageInspection": 5000
|
||||
"packageInspection": 5000,
|
||||
"assortment": 6000
|
||||
}
|
||||
},
|
||||
"checkForUpdates": 3600000,
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
{
|
||||
"title": "ISA - Local",
|
||||
"silentRefresh": {
|
||||
"interval": 300000
|
||||
},
|
||||
"debug": true,
|
||||
"@cdn/product-image": {
|
||||
"url": "https://produktbilder.paragon-data.net"
|
||||
},
|
||||
"@core/auth": {
|
||||
"issuer": "https://sso-test.paragon-data.de",
|
||||
"clientId": "hug-isa",
|
||||
"responseType": "id_token token",
|
||||
"oidc": true,
|
||||
"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"
|
||||
"clientId": "isa-client",
|
||||
"responseType": "code",
|
||||
"scope": "openid profile cmf_user isa-isa-webapi isa-checkout-webapi isa-cat-webapi isa-ava-webapi isa-crm-webapi isa-review-webapi isa-kpi-webapi isa-oms-webapi isa-nbo-webapi isa-print-webapi eis-service isa-inv-webapi isa-wws-webapi",
|
||||
"showDebugInformation": true
|
||||
},
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
@@ -60,7 +64,8 @@
|
||||
"goodsIn": 2000,
|
||||
"taskCalendar": 3000,
|
||||
"remission": 4000,
|
||||
"packageInspection": 5000
|
||||
"packageInspection": 5000,
|
||||
"assortment": 6000
|
||||
}
|
||||
},
|
||||
"checkForUpdates": 3600000,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"title": "ISA - Production",
|
||||
"silentRefresh": {
|
||||
"interval": 300000
|
||||
},
|
||||
"@cdn/product-image": {
|
||||
"url": "https://produktbilder.paragon-data.net"
|
||||
},
|
||||
@@ -8,7 +11,7 @@
|
||||
"clientId": "hug-isa",
|
||||
"responseType": "id_token token",
|
||||
"oidc": true,
|
||||
"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"
|
||||
"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": {
|
||||
"logLevel": "debug"
|
||||
@@ -60,7 +63,8 @@
|
||||
"goodsIn": 2000,
|
||||
"taskCalendar": 3000,
|
||||
"remission": 4000,
|
||||
"packageInspection": 5000
|
||||
"packageInspection": 5000,
|
||||
"assortment": 6000
|
||||
}
|
||||
},
|
||||
"checkForUpdates": 3600000,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"title": "ISA - Staging",
|
||||
"silentRefresh": {
|
||||
"interval": 300000
|
||||
},
|
||||
"@cdn/product-image": {
|
||||
"url": "https://produktbilder.paragon-data.net"
|
||||
},
|
||||
@@ -8,7 +11,7 @@
|
||||
"clientId": "hug-isa",
|
||||
"responseType": "id_token token",
|
||||
"oidc": true,
|
||||
"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"
|
||||
"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": {
|
||||
"logLevel": "debug"
|
||||
@@ -41,7 +44,7 @@
|
||||
"rootUrl": "https://isa-staging.paragon-systems.de/inv/v1"
|
||||
},
|
||||
"@swagger/wws": {
|
||||
"rootUrl": "https://isa-staging.paragon-data.net/wws/v1"
|
||||
"rootUrl": "https://isa-staging.paragon-systems.de/wws/v1"
|
||||
},
|
||||
"hubs": {
|
||||
"notifications": {
|
||||
@@ -60,7 +63,8 @@
|
||||
"goodsIn": 2000,
|
||||
"taskCalendar": 3000,
|
||||
"remission": 4000,
|
||||
"packageInspection": 5000
|
||||
"packageInspection": 5000,
|
||||
"assortment": 6000
|
||||
}
|
||||
},
|
||||
"checkForUpdates": 3600000,
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
{
|
||||
"title": "ISA - Test",
|
||||
"silentRefresh": {
|
||||
"interval": 300000
|
||||
},
|
||||
"debug": true,
|
||||
"@cdn/product-image": {
|
||||
"url": "https://produktbilder.paragon-data.net"
|
||||
},
|
||||
"@core/auth": {
|
||||
"issuer": "https://sso-test.paragon-data.de",
|
||||
"clientId": "hug-isa",
|
||||
"responseType": "id_token token",
|
||||
"oidc": true,
|
||||
"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"
|
||||
"clientId": "isa-client",
|
||||
"responseType": "code",
|
||||
"scope": "openid profile cmf_user isa-isa-webapi isa-checkout-webapi isa-cat-webapi isa-ava-webapi isa-crm-webapi isa-review-webapi isa-kpi-webapi isa-oms-webapi isa-nbo-webapi isa-print-webapi eis-service isa-inv-webapi isa-wws-webapi",
|
||||
"showDebugInformation": true
|
||||
},
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
@@ -60,7 +64,8 @@
|
||||
"goodsIn": 2000,
|
||||
"taskCalendar": 3000,
|
||||
"remission": 4000,
|
||||
"packageInspection": 5000
|
||||
"packageInspection": 5000,
|
||||
"assortment": 6000
|
||||
}
|
||||
},
|
||||
"checkForUpdates": 3600000,
|
||||
|
||||
@@ -5,12 +5,36 @@ import * as moment from 'moment';
|
||||
moment.locale('de');
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { DebugService } from './app/debug/debug.service';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic()
|
||||
const debugService = new DebugService();
|
||||
|
||||
const consoleLog = console.log;
|
||||
|
||||
console.log = (...args) => {
|
||||
debugService.add({ type: 'log', args });
|
||||
consoleLog(...args);
|
||||
};
|
||||
|
||||
const consoleWarn = console.warn;
|
||||
|
||||
console.warn = (...args) => {
|
||||
debugService.add({ type: 'warn', args });
|
||||
consoleWarn(...args);
|
||||
};
|
||||
|
||||
const consoleError = console.error;
|
||||
|
||||
console.error = (...args) => {
|
||||
debugService.add({ type: 'error', args });
|
||||
consoleError(...args);
|
||||
};
|
||||
|
||||
platformBrowserDynamic([{ provide: DebugService, useValue: debugService }])
|
||||
.bootstrapModule(AppModule)
|
||||
.catch((err) => console.error(err));
|
||||
|
||||
@@ -16,7 +16,14 @@
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/ // Run `npm install --save web-animations-js`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
parent.postMessage(location.hash, location.origin);
|
||||
var checks = [/[\?|&|#]code=/, /[\?|&|#]error=/, /[\?|&|#]token=/, /[\?|&|#]id_token=/];
|
||||
|
||||
function isResponse(str) {
|
||||
if (!str) return false;
|
||||
for (var i = 0; i < checks.length; i++) {
|
||||
if (str.match(checks[i])) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var message = isResponse(location.hash) ? location.hash : '#' + location.search;
|
||||
|
||||
if (window.parent && window.parent !== window) {
|
||||
// if loaded as an iframe during silent refresh
|
||||
window.parent.postMessage(message, location.origin);
|
||||
} else if (window.opener && window.opener !== window) {
|
||||
// if loaded as a popup during initial login
|
||||
window.opener.postMessage(message, location.origin);
|
||||
} else {
|
||||
// last resort for a popup which has been through redirects and can't use window.opener
|
||||
localStorage.setItem('auth_hash', message);
|
||||
localStorage.removeItem('auth_hash');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -32,7 +32,7 @@ body {
|
||||
}
|
||||
|
||||
.desktop .scroll-bar::-webkit-scrollbar-track {
|
||||
@apply my-4;
|
||||
// @apply my-4;
|
||||
-webkit-box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
background-color: white;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { combineLatest } from 'rxjs';
|
||||
import { filter, map, shareReplay, switchMap, tap } from 'rxjs/operators';
|
||||
import { geoDistance } from '@utils/common';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ApplicationService } from '@core/application';
|
||||
|
||||
@Component({
|
||||
selector: 'modal-availabilities',
|
||||
@@ -20,7 +21,11 @@ export class ModalAvailabilitiesComponent {
|
||||
stockFetching$ = new BehaviorSubject(true);
|
||||
item = this.modalRef.data.item;
|
||||
itemId = this.modalRef.data.itemId || this.modalRef.data.item.id;
|
||||
userbranch$ = this.domainAvailabilityService.getDefaultBranch();
|
||||
userbranch$ = combineLatest([
|
||||
this.applicationService.getSelectedBranch$(this.applicationService.activatedProcessId),
|
||||
this.domainAvailabilityService.getDefaultBranch(),
|
||||
]).pipe(map(([selectedBranch, defaultBranch]) => selectedBranch || defaultBranch));
|
||||
|
||||
branches$ = this.domainAvailabilityService.getBranches();
|
||||
|
||||
filteredBranches$ = combineLatest([this.branches$, this.userbranch$, this.search$]).pipe(
|
||||
@@ -66,7 +71,8 @@ export class ModalAvailabilitiesComponent {
|
||||
|
||||
constructor(
|
||||
private modalRef: UiModalRef<BranchDTO, { item: ItemDTO; itemId: number }>,
|
||||
private domainAvailabilityService: DomainAvailabilityService
|
||||
private domainAvailabilityService: DomainAvailabilityService,
|
||||
private applicationService: ApplicationService
|
||||
) {}
|
||||
|
||||
filter(query: string) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { DomainOmsService } from '@domain/oms';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
@@ -68,17 +69,15 @@ export class ReorderModalComponent extends ComponentStore<GoodsInListReorderModa
|
||||
)
|
||||
);
|
||||
|
||||
readonly currentBranch$ = this.domainAvailabilityService.getDefaultBranch();
|
||||
|
||||
readonly storeAvailabilities$ = combineLatest([this.orderItem$, this.currentBranch$]).pipe(
|
||||
switchMap(([item, branch]) =>
|
||||
readonly storeAvailabilities$ = this.orderItem$.pipe(
|
||||
switchMap((item) =>
|
||||
this.domainAvailabilityService
|
||||
.getPickUpAvailabilities([
|
||||
{
|
||||
qty: item.quantity,
|
||||
ean: item.product.ean,
|
||||
itemId: item.product?.catalogProductNumber,
|
||||
shopId: branch.id,
|
||||
shopId: item?.targetBranchId,
|
||||
price: item.retailPrice,
|
||||
},
|
||||
])
|
||||
@@ -99,6 +98,7 @@ export class ReorderModalComponent extends ComponentStore<GoodsInListReorderModa
|
||||
eans: [item.product.ean],
|
||||
quantity: item.quantity,
|
||||
price: item.retailPrice,
|
||||
branchId: item.targetBranchId,
|
||||
})
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
@@ -141,7 +141,8 @@ export class ReorderModalComponent extends ComponentStore<GoodsInListReorderModa
|
||||
constructor(
|
||||
public modalRef: UiModalRef<ReorderResult, { item: OrderItemListItemDTO; showReasons: boolean }>,
|
||||
private domainAvailabilityService: DomainAvailabilityService,
|
||||
private _omsService: DomainOmsService
|
||||
private _omsService: DomainOmsService,
|
||||
private _applicationService: ApplicationService
|
||||
) {
|
||||
super({
|
||||
orderItem: modalRef.data?.item,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, fromEvent, Subject } from 'rxjs';
|
||||
import { Observable, fromEvent, Subject, ReplaySubject } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { WindowRef } from './window-ref.service';
|
||||
import { ScanRequestType } from './scan-request.type';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@@ -11,11 +11,15 @@ export class NativeContainerService {
|
||||
private wm: Observable<any>;
|
||||
public windowMessages = new Subject<any>();
|
||||
|
||||
private webViewDetected = false;
|
||||
private webViewEventRecieved = false;
|
||||
private browserDetected = false;
|
||||
|
||||
constructor(private windowRef: WindowRef, private _environmentService: EnvironmentService) {
|
||||
private _init$ = new ReplaySubject<boolean>(1);
|
||||
|
||||
get isNative() {
|
||||
return this.webViewEventRecieved;
|
||||
}
|
||||
|
||||
constructor(private windowRef: WindowRef, private _platform: Platform) {
|
||||
this.defineWindowCallback();
|
||||
|
||||
this.wm = fromEvent(this.windowRef.nativeWindow, 'message').pipe(
|
||||
@@ -27,11 +31,16 @@ export class NativeContainerService {
|
||||
this.wm.subscribe((data) => {
|
||||
if (data.status === 'INIT') {
|
||||
this.webViewEventRecieved = true;
|
||||
this._init$.next(true);
|
||||
}
|
||||
this.windowMessages.next(data);
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
return this._init$.asObservable();
|
||||
}
|
||||
|
||||
public openScanner(scanRequestType: ScanRequestType) {
|
||||
const scanRequest = {
|
||||
[scanRequestType]: true,
|
||||
@@ -46,7 +55,6 @@ export class NativeContainerService {
|
||||
this.windowRef.nativeWindow.postMessage({ status: 'IN_PROGRESS', data: 'Scan Started' }, '*');
|
||||
|
||||
try {
|
||||
// if (this.isUiWebview() && this.isUiWebview().isNative) {
|
||||
(this.windowRef.nativeWindow as any).webkit.messageHandlers.scanRequest.postMessage(message);
|
||||
} catch (error) {
|
||||
this.windowRef.nativeWindow.postMessage({ status: 'ERROR', data: 'Not a WebView' }, '*');
|
||||
@@ -54,30 +62,6 @@ export class NativeContainerService {
|
||||
}
|
||||
}
|
||||
|
||||
public isUiWebview() {
|
||||
// const navigator = this.windowRef.nativeWindow.navigator as Navigator;
|
||||
// alert(this.deviceDetector.browser);
|
||||
// const standalone = (navigator as any).standalone,
|
||||
// userAgent = navigator.userAgent.toLowerCase(),
|
||||
// safari = /safari/.test(userAgent),
|
||||
// ios = /iphone|ipod|ipad/.test(userAgent),
|
||||
// chrome = /chrome/.test(userAgent) && /Google Inc/.test(navigator.vendor),
|
||||
// crios = /crios/.test(userAgent);
|
||||
|
||||
// this.webViewDetected = ios && !standalone && !safari;
|
||||
// this.browserDetected = !standalone && (safari || chrome) && !crios;
|
||||
|
||||
return {
|
||||
isSafari: this._environmentService.isSafari(),
|
||||
isNative: this.webViewEventRecieved,
|
||||
};
|
||||
}
|
||||
|
||||
public isIpadMini6() {
|
||||
const width = window.innerWidth > 0 ? window.innerWidth : screen.width;
|
||||
return width === 744;
|
||||
}
|
||||
|
||||
private defineWindowCallback() {
|
||||
if (this.windowRef.nativeWindow['scanResults'] === undefined) {
|
||||
this.windowRef.nativeWindow['scanResults'] = (result) => window.postMessage(result, '*');
|
||||
@@ -91,6 +75,7 @@ export class NativeContainerService {
|
||||
try {
|
||||
(this.windowRef.nativeWindow as any).webkit.messageHandlers.scanRequest.postMessage('PING');
|
||||
} catch (error) {
|
||||
this._init$.next(false);
|
||||
this.windowRef.nativeWindow.postMessage({ status: 'ERROR', data: 'Not a WebView' }, '*');
|
||||
this.windowRef.nativeWindow.postMessage('PING', '*');
|
||||
}
|
||||
|
||||
7
apps/page/assortment/ng-package.json
Normal file
7
apps/page/assortment/ng-package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../../dist/page/yellow-pages",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
3
apps/page/assortment/src/lib/assortment.component.css
Normal file
3
apps/page/assortment/src/lib/assortment.component.css
Normal file
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply block;
|
||||
}
|
||||
3
apps/page/assortment/src/lib/assortment.component.html
Normal file
3
apps/page/assortment/src/lib/assortment.component.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<shared-breadcrumb class="my-4" [key]="breadcrumbKey"></shared-breadcrumb>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
67
apps/page/assortment/src/lib/assortment.component.spec.ts
Normal file
67
apps/page/assortment/src/lib/assortment.component.spec.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { Config } from '@core/config';
|
||||
import { Spectator, createComponentFactory } from '@ngneat/spectator';
|
||||
import { BreadcrumbComponent } from '@shared/components/breadcrumb';
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
import { AssortmentComponent } from './assortment.component';
|
||||
|
||||
describe('AssortmentComponent', () => {
|
||||
let spectator: Spectator<AssortmentComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: AssortmentComponent,
|
||||
imports: [RouterTestingModule],
|
||||
mocks: [Config, BreadcrumbService],
|
||||
declarations: [MockComponent(BreadcrumbComponent)],
|
||||
});
|
||||
|
||||
let configMock: jasmine.SpyObj<Config>;
|
||||
let breadcrumbServiceMock: jasmine.SpyObj<BreadcrumbService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
spectator = createComponent();
|
||||
await spectator.fixture.whenStable();
|
||||
|
||||
configMock = spectator.inject(Config);
|
||||
breadcrumbServiceMock = spectator.inject(BreadcrumbService);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('get breadcrumbKey(): string', () => {
|
||||
it('should call Config.get("process.ids.assortment") and return the result', () => {
|
||||
const expected = 'expected';
|
||||
configMock.get.and.returnValue(expected);
|
||||
|
||||
const actual = spectator.component.breadcrumbKey;
|
||||
|
||||
expect(configMock.get).toHaveBeenCalledWith('process.ids.assortment');
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngOnInit()', () => {
|
||||
it('should call createBreadcrumbIfNotExists()', () => {
|
||||
const spy = spyOn(spectator.component, 'createBreadcrumbIfNotExists');
|
||||
|
||||
spectator.component.ngOnInit();
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('should render the ShellBreadcrumbComponent and pass the breadcrumbKey as @Input() key', () => {
|
||||
const expected = 'expected';
|
||||
configMock.get.and.returnValue(expected);
|
||||
|
||||
spectator.detectComponentChanges();
|
||||
|
||||
const shellBreadcrumb = spectator.query(BreadcrumbComponent);
|
||||
expect(shellBreadcrumb).toBeTruthy();
|
||||
expect(shellBreadcrumb.key).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
33
apps/page/assortment/src/lib/assortment.component.ts
Normal file
33
apps/page/assortment/src/lib/assortment.component.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Breadcrumb, BreadcrumbService } from '@core/breadcrumb';
|
||||
import { Config } from '@core/config';
|
||||
|
||||
@Component({
|
||||
selector: 'page-assortment',
|
||||
templateUrl: 'assortment.component.html',
|
||||
styleUrls: ['assortment.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AssortmentComponent implements OnInit {
|
||||
get breadcrumbKey(): string {
|
||||
return this._config.get('process.ids.assortment');
|
||||
}
|
||||
|
||||
constructor(private _config: Config, private _breadcrumb: BreadcrumbService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.createBreadcrumbIfNotExists();
|
||||
}
|
||||
|
||||
async createBreadcrumbIfNotExists(): Promise<void> {
|
||||
const crumb: Breadcrumb = {
|
||||
key: this.breadcrumbKey,
|
||||
name: 'Sortiment',
|
||||
path: '/filiale/assortment',
|
||||
section: 'branch',
|
||||
tags: ['main'],
|
||||
};
|
||||
|
||||
await this._breadcrumb.addBreadcrumbIfNotExists(crumb);
|
||||
}
|
||||
}
|
||||
16
apps/page/assortment/src/lib/assortment.module.ts
Normal file
16
apps/page/assortment/src/lib/assortment.module.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { BreadcrumbModule } from '@shared/components/breadcrumb';
|
||||
import { routes } from './routes';
|
||||
|
||||
import { AssortmentComponent } from './assortment.component';
|
||||
import { PriceUpdateModule } from './price-update/price-update.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, RouterModule.forChild(routes), BreadcrumbModule, PriceUpdateModule],
|
||||
exports: [AssortmentComponent],
|
||||
declarations: [AssortmentComponent],
|
||||
providers: [],
|
||||
})
|
||||
export class AssortmentModule {}
|
||||
7
apps/page/assortment/src/lib/price-update/index.ts
Normal file
7
apps/page/assortment/src/lib/price-update/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// start:ng42.barrel
|
||||
export * from './price-update.component.state';
|
||||
export * from './price-update.component.store';
|
||||
export * from './price-update.component';
|
||||
export * from './price-update.module';
|
||||
export * from './price-update-list';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,6 @@
|
||||
// start:ng42.barrel
|
||||
export * from './price-update-item-loader.component';
|
||||
export * from './price-update-item.component';
|
||||
export * from './price-update-list.component';
|
||||
export * from './price-update-list.module';
|
||||
// end:ng42.barrel
|
||||
@@ -0,0 +1,9 @@
|
||||
:host {
|
||||
@apply flex flex-row items-center bg-white rounded px-4;
|
||||
height: 53px;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
@apply block bg-gray-300 h-6;
|
||||
animation: load 1s ease-in-out infinite;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="skeleton w-44"></div>
|
||||
<div class="skeleton w-28 ml-8"></div>
|
||||
<div class="skeleton w-28 ml-8"></div>
|
||||
<div class="grow"></div>
|
||||
<div class="skeleton w-32"></div>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
import { PriceUpdateItemLoaderComponent } from './price-update-item-loader.component';
|
||||
|
||||
describe('PriceUpdateItemLoaderComponent', () => {
|
||||
let spectator: Spectator<PriceUpdateItemLoaderComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: PriceUpdateItemLoaderComponent,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'page-price-update-item-loader',
|
||||
templateUrl: 'price-update-item-loader.component.html',
|
||||
styleUrls: ['price-update-item-loader.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PriceUpdateItemLoaderComponent implements OnInit {
|
||||
constructor() {}
|
||||
|
||||
ngOnInit() {}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
:host {
|
||||
@apply flex flex-col w-full;
|
||||
height: 267px;
|
||||
}
|
||||
|
||||
.page-price-update-item__item-card {
|
||||
@apply grid grid-flow-col;
|
||||
grid-template-columns: 63px auto minmax(230px, auto);
|
||||
box-shadow: 0px 0px 10px rgba(220, 226, 233, 0.5);
|
||||
}
|
||||
|
||||
.page-price-update-item__item-details {
|
||||
@apply grid grid-flow-row;
|
||||
grid-template-rows: 27px 70px 27px 27px auto;
|
||||
}
|
||||
|
||||
.page-price-update-item__item-image {
|
||||
box-shadow: 0px 6px 18px rgba(0, 0, 0, 0.197935);
|
||||
}
|
||||
|
||||
.page-price-update-item__item-addition {
|
||||
@apply grid grid-flow-row justify-items-end;
|
||||
grid-template-rows: 27px 27px 41px 52px auto;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
gültig ab <span class="font-bold ml-2">{{ item?.task?.dueDate | date }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="page-price-update-item__item-card p-5 h-[212px] bg-white">
|
||||
<div class="page-price-update-item__item-thumbnail text-center mr-4 w-[47px] h-[73px]">
|
||||
<img
|
||||
class="page-price-update-item__item-image w-[47px] h-[73px]"
|
||||
loading="lazy"
|
||||
*ngIf="item?.product?.ean | productImage; let productImage"
|
||||
[src]="productImage"
|
||||
[alt]="item?.product?.name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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 }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="page-price-update-item__item-title font-bold text-2xl"
|
||||
[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-xs]="item?.product?.name?.length >= 100"
|
||||
>
|
||||
{{ item?.product?.name }}
|
||||
</div>
|
||||
|
||||
<div class="page-price-update-item__item-format">
|
||||
<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 }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-price-update-item__item-misc">
|
||||
{{ environment.isTablet() ? (item?.product?.manufacturer | substr: 18) : item?.product?.manufacturer }} | {{ item?.product?.ean }}
|
||||
<br />
|
||||
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
|
||||
{{ publicationDate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-price-update-item__item-addition">
|
||||
<div class="page-price-update-item__item-product-group-details">{{ item?.product?.productGroupDetails }}</div>
|
||||
<div class="page-price-update-item__item-compartment">
|
||||
{{ item?.compartmentInfo?.label }}
|
||||
</div>
|
||||
<div class="page-price-update-item__item-price font-bold">
|
||||
{{ item?.product?.price?.value?.value | currency: 'EUR':'code' }}
|
||||
</div>
|
||||
|
||||
<div class="page-price-update-item__item-select-bullet">
|
||||
<input *ngIf="isSelectable" [ngModel]="selected$ | async" (ngModelChange)="setSelected()" class="isa-select-bullet" type="checkbox" />
|
||||
</div>
|
||||
|
||||
<div class="page-price-update-item__item-stock flex flex-row font-bold">
|
||||
<ui-icon class="mt-px-2 mr-1" 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
|
||||
>
|
||||
x
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
import { DateAdapter } from '@ui/common';
|
||||
import { PriceUpdateComponentStore } from '../price-update.component.store';
|
||||
import { PriceUpdateItemComponent } from './price-update-item.component';
|
||||
|
||||
xdescribe('PriceUpdateItemComponent', () => {
|
||||
let spectator: Spectator<PriceUpdateItemComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: PriceUpdateItemComponent,
|
||||
mocks: [DateAdapter, DomainAvailabilityService, ApplicationService, PriceUpdateComponentStore],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
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 { PriceUpdateComponentStore } from '../price-update.component.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-price-update-item',
|
||||
templateUrl: 'price-update-item.component.html',
|
||||
styleUrls: ['price-update-item.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [DatePipe],
|
||||
})
|
||||
export class PriceUpdateItemComponent {
|
||||
@Input()
|
||||
item: ProductListItemDTO;
|
||||
|
||||
get publicationDate() {
|
||||
if (!!this.item?.product?.publicationDate) {
|
||||
const date = this._dateAdapter.parseDate(this.item.product.publicationDate);
|
||||
|
||||
if (this._dateAdapter.getDate(date) === 1 && this._dateAdapter.getMonth(date) === 0) {
|
||||
return this._datePipe.transform(date, 'y');
|
||||
}
|
||||
|
||||
return this._datePipe.transform(date, 'dd. MMMM y');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
get isSelectable() {
|
||||
return this._store.isSelectable(this.item);
|
||||
}
|
||||
|
||||
selected$ = this._store.selectedItemUids$.pipe(map((selectedItemUids) => selectedItemUids.includes(this.item?.uId)));
|
||||
|
||||
defaultBranch$ = this._availability.getDefaultBranch();
|
||||
|
||||
inStock$ = this.defaultBranch$.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;
|
||||
// })
|
||||
// )
|
||||
),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
constructor(
|
||||
private _dateAdapter: DateAdapter,
|
||||
private _datePipe: DatePipe,
|
||||
private _availability: DomainAvailabilityService,
|
||||
public applicationService: ApplicationService,
|
||||
public environment: EnvironmentService,
|
||||
private _stockService: DomainInStockService,
|
||||
private _store: PriceUpdateComponentStore
|
||||
) {}
|
||||
|
||||
setSelected() {
|
||||
const isSelected = this._store.selectedItemUids.includes(this.item?.uId);
|
||||
this._store.setSelected({ selected: !isSelected, itemUid: this.item?.uId });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
:host {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
cdk-virtual-scroll-viewport {
|
||||
height: calc(100vh - 500px);
|
||||
}
|
||||
|
||||
.page-price-update-list__print-cta:disabled {
|
||||
@apply text-inactive-branch;
|
||||
}
|
||||
|
||||
.page-price-update-list__action-wrapper {
|
||||
@apply grid grid-flow-col gap-4 justify-center my-6 fixed bottom-24 inset-x-0;
|
||||
}
|
||||
|
||||
.page-price-update-list__order-by {
|
||||
box-shadow: 0px 0px 10px rgba(220, 226, 233, 0.5);
|
||||
}
|
||||
|
||||
::ng-deep page-price-update-list ui-order-by-filter .order-by-filter-button-wrapper .order-by-filter-button {
|
||||
@apply text-black;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<div class="page-price-update-list__header bg-background-liste flex flex-col items-end py-4">
|
||||
<button
|
||||
[disabled]="getSelectableItems().length === 0"
|
||||
(click)="print()"
|
||||
type="button"
|
||||
class="page-price-update-list__print-cta text-lg font-bold text-[#F70400] pr-5 mb-3"
|
||||
>
|
||||
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">
|
||||
<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 }})
|
||||
</button>
|
||||
<button class="page-price-update-list__cta-select-all" type="button" (click)="selectAll()" *ngIf="selectedItems?.length === 0">
|
||||
Alle auswählen ({{ getSelectableItems().length }})
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="page-price-update-list__items-count inline-flex flex-row items-center pr-5 text-sm">
|
||||
{{ items?.length ??
|
||||
0 }}
|
||||
Titel
|
||||
</div>
|
||||
</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">
|
||||
<ui-order-by-filter [orderBy]="orderBy$ | async" (selectedOrderByChange)="search()"> </ui-order-by-filter>
|
||||
</div>
|
||||
|
||||
<cdk-virtual-scroll-viewport #scrollContainer [itemSize]="267" minBufferPx="1200" maxBufferPx="1200" class="scroll-bar">
|
||||
<page-price-update-item
|
||||
*cdkVirtualFor="let item of items; let first; trackBy: trackByFn"
|
||||
[item]="item"
|
||||
[class.mt-px-10]="!first"
|
||||
></page-price-update-item>
|
||||
|
||||
<page-price-update-item-loader *ngIf="fetching"> </page-price-update-item-loader>
|
||||
|
||||
<div class="h-28"></div>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
|
||||
<div class="page-price-update-list__action-wrapper">
|
||||
<button
|
||||
*ngIf="!fetching"
|
||||
[disabled]="(selectedItemUids$ | async).length === 0 || (loading$ | async)"
|
||||
class="page-price-update-list__complete-items isa-button isa-cta-button isa-button-primary px-11"
|
||||
type="button"
|
||||
(click)="onComplete()"
|
||||
>
|
||||
<ui-spinner [show]="loading$ | async">Erledigt</ui-spinner>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
import { PriceUpdateComponentStore } from '../price-update.component.store';
|
||||
import { PriceUpdateItemComponent } from './price-update-item.component';
|
||||
import { PriceUpdateListComponent } from './price-update-list.component';
|
||||
|
||||
xdescribe('PriceUpdateListComponent', () => {
|
||||
let spectator: Spectator<PriceUpdateListComponent>;
|
||||
let priceUpdateStoreMock: jasmine.SpyObj<PriceUpdateComponentStore>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: PriceUpdateListComponent,
|
||||
declarations: [MockComponent(PriceUpdateItemComponent)],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
priceUpdateStoreMock = spectator.inject(PriceUpdateComponentStore, true);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the length of the items in .page-price-update-list__items-count', () => {
|
||||
spectator.setInput('items', [{ uId: '1' }, { uId: '2' }]);
|
||||
|
||||
spectator.detectChanges();
|
||||
|
||||
expect(spectator.query('.page-price-update-list__items-count')).toHaveText('2 Titel');
|
||||
});
|
||||
|
||||
it('should render the PriceUpdateItemComponent for each item', () => {
|
||||
spectator.setInput('items', [{ uId: '1' }, { uId: '2' }]);
|
||||
|
||||
spectator.detectChanges();
|
||||
|
||||
const listItem = spectator.queryAll(PriceUpdateItemComponent);
|
||||
expect(listItem).toHaveLength(2);
|
||||
expect(listItem[0].item).toEqual({ uId: '1' });
|
||||
expect(listItem[1].item).toEqual({ uId: '2' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
|
||||
import { ChangeDetectionStrategy, Component, Input, ViewChild } from '@angular/core';
|
||||
import { ProductListItemDTO } from '@swagger/wws';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PriceUpdateComponentStore } from '../price-update.component.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-price-update-list',
|
||||
templateUrl: 'price-update-list.component.html',
|
||||
styleUrls: ['price-update-list.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PriceUpdateListComponent {
|
||||
@ViewChild('scrollContainer', { static: true })
|
||||
scrollContainer: CdkVirtualScrollViewport;
|
||||
|
||||
@Input()
|
||||
items: ProductListItemDTO[] = [];
|
||||
|
||||
@Input()
|
||||
fetching: boolean = false;
|
||||
|
||||
selectedItemUids$ = this._store.selectedItemUids$;
|
||||
|
||||
loading$ = this._store.loading$;
|
||||
|
||||
orderBy$ = this._store.filter$.pipe(map((filter) => filter?.orderBy));
|
||||
|
||||
trackByFn = (index: number, item: ProductListItemDTO) => item.uId;
|
||||
|
||||
constructor(private _store: PriceUpdateComponentStore) {}
|
||||
|
||||
getSelectableItems() {
|
||||
return this._store.items.filter((item) => this._store.isSelectable(item)) ?? [];
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
const selectedItemUids = this.getSelectableItems().map((item) => item.uId);
|
||||
this._store.patchState({ selectedItemUids });
|
||||
}
|
||||
|
||||
unselectAll() {
|
||||
this._store.patchState({ selectedItemUids: [] });
|
||||
}
|
||||
|
||||
print() {
|
||||
this._store.print();
|
||||
}
|
||||
|
||||
onComplete() {
|
||||
this._store.complete();
|
||||
}
|
||||
|
||||
search() {
|
||||
this._store.fetchItems();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ProductImageModule } from '@cdn/product-image';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { UiOrderByFilterModule } from '@ui/filter';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { UiSpinnerModule } from '@ui/spinner';
|
||||
import { PriceUpdateItemLoaderComponent } from './price-update-item-loader.component';
|
||||
import { PriceUpdateItemComponent } from './price-update-item.component';
|
||||
import { PriceUpdateListComponent } from './price-update-list.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
CommonModule,
|
||||
UiCommonModule,
|
||||
UiIconModule,
|
||||
ProductImageModule,
|
||||
ScrollingModule,
|
||||
UiSpinnerModule,
|
||||
UiOrderByFilterModule,
|
||||
],
|
||||
exports: [PriceUpdateListComponent, PriceUpdateItemComponent, PriceUpdateItemLoaderComponent],
|
||||
declarations: [PriceUpdateListComponent, PriceUpdateItemComponent, PriceUpdateItemLoaderComponent],
|
||||
providers: [],
|
||||
})
|
||||
export class PriceUpdateListModule {}
|
||||
@@ -0,0 +1,7 @@
|
||||
:host {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
::ng-deep ui-filter-input-options .input-options {
|
||||
max-height: calc(100vh - 520px);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<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>
|
||||
<button
|
||||
(click)="filterOverlay.open()"
|
||||
class="absolute right-0 top-0 h-14 rounded px-5 text-lg bg-cadet-blue flex flex-row flex-nowrap items-center justify-center"
|
||||
type="button"
|
||||
>
|
||||
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
|
||||
Filter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<page-price-update-list *ngIf="showList$ | async; else noResults" [items]="store.items$ | async" [fetching]="store.fetching$ | async">
|
||||
</page-price-update-list>
|
||||
|
||||
<shell-filter-overlay #filterOverlay class="relative">
|
||||
<div class="relative">
|
||||
<button type="button" class="absolute top-4 right-4 text-cadet" (click)="closeFilterOverlay()">
|
||||
<ui-svg-icon [icon]="'close'" [size]="28"></ui-svg-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 class="text-3xl text-center font-bold mt-8">Filter</h3>
|
||||
|
||||
<ui-filter
|
||||
*ngIf="filterOverlay.isOpen"
|
||||
#filter
|
||||
class="mx-4"
|
||||
[filter]="store.pendingFilter$ | async"
|
||||
(search)="applyFilter()"
|
||||
[loading]="store.fetching$ | async"
|
||||
[hint]="hint$ | async"
|
||||
></ui-filter>
|
||||
|
||||
<div class="absolute bottom-8 left-0 right-0 grid grid-flow-col gap-4 justify-center">
|
||||
<button
|
||||
type="button"
|
||||
class="px-6 py-4 font-bold bg-white text-brand border-2 border-solid border-brand rounded-full"
|
||||
(click)="store.resetPendingFilter()"
|
||||
>
|
||||
Filter zurücksetzen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-6 py-4 font-bold bg-brand text-white border-2 border-solid border-brand rounded-full disabled:bg-cadet-blue disabled:cursor-progress disabled:border-cadet-blue"
|
||||
(click)="applyFilter()"
|
||||
[disabled]="store.fetching$ | async"
|
||||
>
|
||||
<ui-spinner [show]="store.fetching$ | async">
|
||||
Filter anwenden
|
||||
</ui-spinner>
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { Config } from '@core/config';
|
||||
import { DomainProductListService } from '@domain/product-list';
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
import { ProductListItemDTO } from '@swagger/wws';
|
||||
import { UISvgIconComponent } from '@ui/icon';
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
import { PriceUpdateListComponent } from './price-update-list';
|
||||
import { PriceUpdateComponent } from './price-update.component';
|
||||
import { PriceUpdateComponentStore } from './price-update.component.store';
|
||||
|
||||
xdescribe('PriceUpdateComponent', () => {
|
||||
let spectator: Spectator<PriceUpdateComponent>;
|
||||
let priceUpdateStoreMock: jasmine.SpyObj<PriceUpdateComponentStore>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: PriceUpdateComponent,
|
||||
mocks: [DomainProductListService, UiModalService, ActivatedRoute, Config, BreadcrumbService],
|
||||
declarations: [MockComponent(PriceUpdateListComponent), MockComponent(UISvgIconComponent)],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
priceUpdateStoreMock = spectator.inject(PriceUpdateComponentStore, true);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the PriceUpdateListComponent', () => {
|
||||
expect(spectator.query(PriceUpdateListComponent)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set the input package of the PriceUpdateListComponent', () => {
|
||||
const items: ProductListItemDTO[] = [{ uId: '1' }, { uId: '2' }];
|
||||
spectator.component.store.setItems(items);
|
||||
|
||||
spectator.detectChanges();
|
||||
|
||||
expect(spectator.query(PriceUpdateListComponent).items).toEqual(items);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ProductListItemDTO } from '@swagger/wws';
|
||||
import { UiFilter } from '@ui/filter';
|
||||
|
||||
export interface PriceUpdateComponentState {
|
||||
showFilter: boolean;
|
||||
fetching: boolean;
|
||||
filter: UiFilter | undefined;
|
||||
pendingFilter: UiFilter | undefined;
|
||||
defaultFilter: UiFilter | undefined;
|
||||
items: ProductListItemDTO[];
|
||||
selectedItemUids: string[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const INITIAL_PRICE_UPDATE_COMPONENT_STATE: PriceUpdateComponentState = {
|
||||
showFilter: false,
|
||||
fetching: false,
|
||||
filter: undefined,
|
||||
pendingFilter: undefined,
|
||||
defaultFilter: undefined,
|
||||
items: [],
|
||||
selectedItemUids: [],
|
||||
loading: false,
|
||||
};
|
||||
@@ -0,0 +1,201 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DomainPrinterService } from '@domain/printer';
|
||||
import { DomainProductListService } from '@domain/product-list';
|
||||
import { PrintModalComponent, PrintModalData } from '@modal/printer';
|
||||
import { ComponentStore, OnStoreInit, tapResponse } from '@ngrx/component-store';
|
||||
import {
|
||||
BatchResponseArgsOfProductListItemDTOAndString,
|
||||
ListResponseArgsOfProductListItemDTO,
|
||||
ProductListItemDTO,
|
||||
QuerySettingsDTO,
|
||||
} from '@swagger/wws';
|
||||
import { UiFilter } from '@ui/filter';
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { isNil } from 'lodash';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { INITIAL_PRICE_UPDATE_COMPONENT_STATE, PriceUpdateComponentState } from './price-update.component.state';
|
||||
|
||||
@Injectable()
|
||||
export class PriceUpdateComponentStore extends ComponentStore<PriceUpdateComponentState> implements OnStoreInit {
|
||||
showFilter$ = this.select((state) => state.showFilter);
|
||||
|
||||
fetching$ = this.select((state) => state.fetching);
|
||||
|
||||
loading$ = this.select((state) => state.loading);
|
||||
|
||||
filter$ = this.select((state) => state.filter);
|
||||
|
||||
get filter(): UiFilter {
|
||||
return this.get((f) => f.filter);
|
||||
}
|
||||
|
||||
pendingFilter$ = this.select((state) => state.pendingFilter);
|
||||
|
||||
defaultFilter$ = this.select((state) => state.defaultFilter);
|
||||
|
||||
items$ = this.select((state) => state.items);
|
||||
|
||||
get items() {
|
||||
return this.get((s) => s.items);
|
||||
}
|
||||
|
||||
selectedItemUids$ = this.select((s) => s.selectedItemUids);
|
||||
|
||||
get selectedItemUids() {
|
||||
return this.get((s) => s.selectedItemUids);
|
||||
}
|
||||
|
||||
private _onFetchingItemsResponse$ = new Subject<ListResponseArgsOfProductListItemDTO>();
|
||||
|
||||
onFetchingItemsResponse$ = this._onFetchingItemsResponse$.asObservable();
|
||||
|
||||
constructor(
|
||||
private _productListService: DomainProductListService,
|
||||
private _domainPrinterService: DomainPrinterService,
|
||||
private _uiModal: UiModalService
|
||||
) {
|
||||
super(INITIAL_PRICE_UPDATE_COMPONENT_STATE);
|
||||
}
|
||||
|
||||
ngrxOnStoreInit() {
|
||||
this.fetchSettings();
|
||||
}
|
||||
|
||||
setShowFilter = this.updater((state, showFilter: boolean) => ({ ...state, showFilter }));
|
||||
|
||||
setFetching = this.updater((state, fetching: boolean) => ({ ...state, fetching }));
|
||||
|
||||
setLoading = this.updater((state, loading: boolean) => ({ ...state, loading }));
|
||||
|
||||
setFilter = this.updater((state, filter: UiFilter) => ({ ...state, filter }));
|
||||
|
||||
setPendingFilter = this.updater((state, pendingFilter: UiFilter) => ({ ...state, pendingFilter }));
|
||||
|
||||
setDefaultFilter = this.updater((state, defaultFilter: UiFilter) => ({ ...state, defaultFilter }));
|
||||
|
||||
setItems = this.updater((state, items: ProductListItemDTO[]) => ({ ...state, items }));
|
||||
|
||||
setSelected({ selected, itemUid }: { selected: boolean; itemUid: string }) {
|
||||
if (selected) {
|
||||
this.patchState({
|
||||
selectedItemUids: [...this.selectedItemUids, itemUid],
|
||||
});
|
||||
} else if (!selected) {
|
||||
this.patchState({
|
||||
selectedItemUids: this.selectedItemUids.filter((id) => id !== itemUid),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fetchSettings = this.effect(($) =>
|
||||
$.pipe(switchMap((_) => this._productListService.getQuerySettings().pipe(tapResponse(this.onFetchSettingsResponse, this.onFetchError))))
|
||||
);
|
||||
|
||||
fetchItems = this.effect(($) =>
|
||||
$.pipe(
|
||||
tap(() => this.beforeFetchingItems()),
|
||||
debounceTime(250),
|
||||
withLatestFrom(this.filter$),
|
||||
switchMap(([_, filter]) =>
|
||||
this._productListService
|
||||
.queryProductListResponse(filter.getQueryToken())
|
||||
.pipe(tapResponse(this.onFetchingItemsResponse, this.onFetchError))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
complete = this.effect(($) =>
|
||||
$.pipe(
|
||||
tap(() => this.beforeComplete()),
|
||||
withLatestFrom(this.selectedItemUids$),
|
||||
switchMap(([_, selectedItemUids]) =>
|
||||
this._productListService.completeProductListItems(selectedItemUids).pipe(tapResponse(this.onCompleteResponse, this.onCompleteError))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
print() {
|
||||
const selectableItems = this.items.filter((item) => this.isSelectable(item));
|
||||
if (selectableItems?.length > 0) {
|
||||
this._uiModal.open({
|
||||
content: PrintModalComponent,
|
||||
data: {
|
||||
printerType: 'Office',
|
||||
print: (printer) =>
|
||||
this._domainPrinterService.printProductListItems({ data: selectableItems, printer, title: 'Preisänderungen' }).toPromise(),
|
||||
} as PrintModalData,
|
||||
config: {
|
||||
panelClass: [],
|
||||
showScrollbarY: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
beforeComplete() {
|
||||
this.setLoading(true);
|
||||
}
|
||||
|
||||
beforeFetchingItems() {
|
||||
this.setFetching(true);
|
||||
this.setItems([]);
|
||||
this.patchState({ selectedItemUids: [] });
|
||||
}
|
||||
|
||||
onCompleteResponse = (response: BatchResponseArgsOfProductListItemDTOAndString) => {
|
||||
let items = this.items;
|
||||
|
||||
if (response.error) {
|
||||
this._uiModal.error('Setzen der Produkte auf Erledigt enthält fehlerhafte Einträge.', new Error(response.message));
|
||||
} else if (response.successful) {
|
||||
items = items.filter((item) => !response.successful.some((s) => s.key === item.uId));
|
||||
} else {
|
||||
this._uiModal.error(
|
||||
'Setzen der Produkte auf Erledigt enthält keine erfolgreichen Einträge.',
|
||||
new Error('Keine erfolgreichen Einträge')
|
||||
);
|
||||
}
|
||||
|
||||
this.patchState({ selectedItemUids: [] });
|
||||
this.setItems(items);
|
||||
this.setLoading(false);
|
||||
};
|
||||
|
||||
onCompleteError = (error: any) => {
|
||||
console.error(error);
|
||||
this._uiModal.error('Setzen der Produkte auf Erledigt fehlgeschlagen.', error);
|
||||
this.setLoading(false);
|
||||
};
|
||||
|
||||
onFetchSettingsResponse = (querySettings: QuerySettingsDTO) => {
|
||||
const filter = UiFilter.create(querySettings);
|
||||
this.setDefaultFilter(UiFilter.create(filter));
|
||||
this.setPendingFilter(UiFilter.create(filter));
|
||||
this.setFilter(UiFilter.create(filter));
|
||||
};
|
||||
|
||||
onFetchingItemsResponse = (response: ListResponseArgsOfProductListItemDTO) => {
|
||||
this.setFetching(false);
|
||||
this.setItems(response.result);
|
||||
this._onFetchingItemsResponse$.next(response);
|
||||
};
|
||||
|
||||
onFetchError = (error: any) => {
|
||||
console.error(error);
|
||||
this._uiModal.error('Laden der Produktliste fehlgeschlagen.', error);
|
||||
this.setFetching(false);
|
||||
this.setItems([]);
|
||||
};
|
||||
|
||||
isSelectable(item: ProductListItemDTO) {
|
||||
// True wenn das Item das aktuelle oder ein älteres Datum beim gültig ab Wert hat
|
||||
return new Date(item?.task?.dueDate) <= new Date(Date.now()) && isNil(item.task.completed);
|
||||
}
|
||||
|
||||
restorePendingFilter = this.updater((state) => ({ ...state, pendingFilter: UiFilter.create(state.filter) }));
|
||||
|
||||
resetPendingFilter = this.updater((state) => ({ ...state, pendingFilter: UiFilter.create(state.defaultFilter) }));
|
||||
|
||||
applyPendingFilterToFilter = this.updater((state) => ({ ...state, filter: UiFilter.create(state.pendingFilter) }));
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
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 { UiFilter, UiFilterComponent } from '@ui/filter';
|
||||
import { PriceUpdateComponentStore } from './price-update.component.store';
|
||||
import { combineLatest, Subject, Subscription } from 'rxjs';
|
||||
import { filter, first, map } from 'rxjs/operators';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { Breadcrumb, BreadcrumbService } from '@core/breadcrumb';
|
||||
import { ListResponseArgsOfProductListItemDTO } from '@swagger/wws';
|
||||
|
||||
@Component({
|
||||
selector: 'page-price-update',
|
||||
templateUrl: 'price-update.component.html',
|
||||
styleUrls: ['price-update.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [provideComponentStore(PriceUpdateComponentStore)],
|
||||
})
|
||||
export class PriceUpdateComponent implements OnInit {
|
||||
private _subscription = new Subscription();
|
||||
|
||||
get breadcrumbKey(): string {
|
||||
return this._config.get('process.ids.assortment');
|
||||
}
|
||||
|
||||
hint$ = new Subject<string>();
|
||||
|
||||
@ViewChild(ShellFilterOverlayComponent)
|
||||
filterOverlay: ShellFilterOverlayComponent;
|
||||
|
||||
/**
|
||||
* Zeigt die liste an, wenn entweder keine items geladen werden oder wenn items geladen wurden
|
||||
* und mindestens ein item vorhanden ist.
|
||||
*/
|
||||
showList$ = combineLatest([this.store.fetching$, this.store.items$]).pipe(map(([fetching, items]) => fetching || items.length > 0));
|
||||
|
||||
constructor(
|
||||
public store: PriceUpdateComponentStore,
|
||||
private _activatedRoute: ActivatedRoute,
|
||||
private _config: Config,
|
||||
private _breadcrumb: BreadcrumbService,
|
||||
private _router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initFilterSubscription();
|
||||
|
||||
this.initFetchResponseSubscription();
|
||||
|
||||
this.removeBreadcrumbs();
|
||||
}
|
||||
|
||||
initFilterSubscription() {
|
||||
const initialFilter$ = this.store.filter$.pipe(
|
||||
filter((f) => f instanceof UiFilter),
|
||||
first()
|
||||
);
|
||||
const queryParams$ = this._activatedRoute.queryParams;
|
||||
|
||||
const filterSub = combineLatest([initialFilter$, queryParams$]).subscribe(([filter, queryParams]) => {
|
||||
const restored = this.restoreFilterFromQueryParams(filter, queryParams);
|
||||
this.fetchItems(restored);
|
||||
});
|
||||
|
||||
this._subscription.add(filterSub);
|
||||
}
|
||||
|
||||
initFetchResponseSubscription() {
|
||||
const onFetchItemsResponseSub = this.store.onFetchingItemsResponse$.subscribe(this.onFetchingItemsResponse);
|
||||
|
||||
this._subscription.add(onFetchItemsResponseSub);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._subscription.unsubscribe();
|
||||
}
|
||||
|
||||
restoreFilterFromQueryParams(filter: UiFilter, queryParams: Params): UiFilter {
|
||||
const nextFilter = UiFilter.create(filter);
|
||||
nextFilter.fromQueryParams(queryParams);
|
||||
this.store.setFilter(nextFilter);
|
||||
this.store.setPendingFilter(nextFilter);
|
||||
return nextFilter;
|
||||
}
|
||||
|
||||
applyFilter() {
|
||||
this.store.applyPendingFilterToFilter();
|
||||
this.fetchItems(this.store.filter);
|
||||
}
|
||||
|
||||
fetchItems(filter: UiFilter) {
|
||||
this.hint$.next('');
|
||||
this.store.fetchItems();
|
||||
this.patchLocation(filter);
|
||||
}
|
||||
|
||||
onFetchingItemsResponse = (response: ListResponseArgsOfProductListItemDTO) => {
|
||||
this.createBreadcrumbIfNotExists(this.store.filter);
|
||||
if (response.error) {
|
||||
console.error(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.result.length === 0 && this.filterOverlay.isOpen) {
|
||||
this.hint$.next('Keine Preisänderungen vorhanden');
|
||||
return;
|
||||
}
|
||||
|
||||
this.filterOverlay.close();
|
||||
};
|
||||
|
||||
async patchLocation(filter: UiFilter): Promise<void> {
|
||||
this._router.navigate([], {
|
||||
queryParams: filter.getQueryParams(),
|
||||
});
|
||||
}
|
||||
|
||||
async createBreadcrumbIfNotExists(filter: UiFilter): Promise<void> {
|
||||
const crumb: Breadcrumb = {
|
||||
key: this.breadcrumbKey,
|
||||
name: 'Preisänderung',
|
||||
path: '/filiale/assortment/price-update',
|
||||
section: 'branch',
|
||||
params: filter.getQueryParams(),
|
||||
tags: ['filter'],
|
||||
};
|
||||
|
||||
await this._breadcrumb.addOrUpdateBreadcrumbIfNotExists(crumb);
|
||||
}
|
||||
|
||||
async removeBreadcrumbs(): Promise<void> {
|
||||
const filterCrumbs = await this._breadcrumb.getBreadcrumbsByKeyAndTag$(this.breadcrumbKey, 'filter').pipe(first()).toPromise();
|
||||
const crumbs = [...filterCrumbs];
|
||||
for (let crumb of crumbs) {
|
||||
await this._breadcrumb.removeBreadcrumb(crumb.id);
|
||||
}
|
||||
}
|
||||
|
||||
closeFilterOverlay() {
|
||||
this.hint$.next('');
|
||||
this.store.restorePendingFilter();
|
||||
this.filterOverlay.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ShellFilterOverlayModule } from '@shell/filter-overlay';
|
||||
import { UiFilterNextModule } from '@ui/filter';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { UiSpinnerModule } from '@ui/spinner';
|
||||
import { PriceUpdateListModule } from './price-update-list';
|
||||
import { PriceUpdateComponent } from './price-update.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, PriceUpdateListModule, UiIconModule, UiFilterNextModule, ShellFilterOverlayModule, UiSpinnerModule],
|
||||
exports: [PriceUpdateComponent],
|
||||
declarations: [PriceUpdateComponent],
|
||||
providers: [],
|
||||
})
|
||||
export class PriceUpdateModule {}
|
||||
14
apps/page/assortment/src/lib/routes.ts
Normal file
14
apps/page/assortment/src/lib/routes.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { AssortmentComponent } from './assortment.component';
|
||||
import { PriceUpdateComponent } from './price-update/price-update.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AssortmentComponent,
|
||||
children: [
|
||||
{ path: 'price-update', component: PriceUpdateComponent },
|
||||
{ path: '**', redirectTo: 'price-update' },
|
||||
],
|
||||
},
|
||||
];
|
||||
6
apps/page/assortment/src/public-api.ts
Normal file
6
apps/page/assortment/src/public-api.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
* Public API Surface of checkout
|
||||
*/
|
||||
|
||||
export * from './lib/assortment.component';
|
||||
export * from './lib/assortment.module';
|
||||
@@ -68,7 +68,7 @@
|
||||
<div class="row stock">
|
||||
<div data-name="product-manufacturer">{{ item.product?.manufacturer }}</div>
|
||||
|
||||
<div class="right quantity">
|
||||
<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">
|
||||
@@ -77,6 +77,9 @@
|
||||
</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>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { ItemDTO } from '@swagger/cat';
|
||||
import { DateAdapter } from '@ui/common';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-details',
|
||||
@@ -90,6 +91,29 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
);
|
||||
|
||||
defaultBranch$ = this._availability.getDefaultBranch();
|
||||
|
||||
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.applicationService.getSelectedBranch$(processId))
|
||||
);
|
||||
|
||||
stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranchId$]).pipe(
|
||||
map(([defaultBranch, selectedBranch]) => {
|
||||
if (defaultBranch?.branchType === 4) {
|
||||
if (!selectedBranch) {
|
||||
return 'Wählen Sie eine Filiale aus, um den Bestand zu sehen.';
|
||||
}
|
||||
return 'Sie sehen den Bestand einer anderen Filiale.';
|
||||
} else {
|
||||
if (selectedBranch && defaultBranch.id !== selectedBranch?.id) {
|
||||
return 'Sie sehen den Bestand einer anderen Filiale.';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
constructor(
|
||||
public readonly applicationService: ApplicationService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
@@ -100,7 +124,8 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
private breadcrumb: BreadcrumbService,
|
||||
private _dateAdapter: DateAdapter,
|
||||
private _datePipe: DatePipe,
|
||||
public elementRef: ElementRef
|
||||
public elementRef: ElementRef,
|
||||
private _availability: DomainAvailabilityService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -265,6 +290,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -277,6 +303,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
const branch = selectedBranch || (await this.store.branch$.pipe(first()).toPromise());
|
||||
|
||||
this.uiModal.open({
|
||||
content: PurchasingOptionsModalComponent,
|
||||
data: {
|
||||
|
||||
@@ -51,6 +51,7 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
|
||||
this._activatedRoute.url
|
||||
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._activatedRoute.queryParams, this._processId$))
|
||||
.subscribe(([_, queryParams, processId]) => {
|
||||
this.resetFilter(queryParams);
|
||||
this.removeBreadcrumbs(processId);
|
||||
this.addOrUpdateBreadcrumbs(processId, queryParams);
|
||||
});
|
||||
@@ -81,6 +82,13 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
|
||||
this._onDestroy$.complete();
|
||||
}
|
||||
|
||||
resetFilter(queryParams: Record<string, string>) {
|
||||
const currentQueryParams = this._articleSearch.filter?.getQueryParams();
|
||||
if (!isEqual(currentQueryParams, queryParams)) {
|
||||
this._articleSearch.resetFilter();
|
||||
}
|
||||
}
|
||||
|
||||
async removeBreadcrumbs(processId: number) {
|
||||
this._breadcrumb.removeBreadcrumbsByKeyAndTags(processId, ['checkout']);
|
||||
}
|
||||
|
||||
@@ -116,6 +116,10 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
|
||||
this.patchState({ filter });
|
||||
}
|
||||
|
||||
async resetFilter() {
|
||||
await this.setDefaultFilter();
|
||||
}
|
||||
|
||||
setSelected({ selected, itemId }: { selected: boolean; itemId: number }) {
|
||||
const included = this.selectedItemIds.includes(itemId);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
:host {
|
||||
@apply flex flex-row rounded-card bg-white mb-2 p-4;
|
||||
height: 155px;
|
||||
height: 187px;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
|
||||
@@ -33,13 +33,30 @@
|
||||
<ui-select-bullet [ngModel]="selected" (ngModelChange)="setSelected($event)"></ui-select-bullet>
|
||||
</div>
|
||||
|
||||
<div class="item-stock">
|
||||
<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>
|
||||
x
|
||||
<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>
|
||||
</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="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">
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ItemDTO } from '@swagger/cat';
|
||||
import { DateAdapter } from '@ui/common';
|
||||
import { isEqual } from 'lodash';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { debounceTime, switchMap, tap } from 'rxjs/operators';
|
||||
import { debounceTime, switchMap, map, tap, shareReplay } from 'rxjs/operators';
|
||||
import { ArticleSearchService } from '../article-search.store';
|
||||
|
||||
export interface SearchResultItemComponentState {
|
||||
@@ -83,6 +83,31 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
switchMap((processId) => this.applicationService.getSelectedBranch$(processId))
|
||||
);
|
||||
|
||||
isOrderBranch$ = combineLatest([this.defaultBranch$, this.selectedBranchId$]).pipe(
|
||||
map(([defaultBranch, selectedBranch]) => {
|
||||
const branch = selectedBranch ?? defaultBranch;
|
||||
return branch.branchType !== 4;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranchId$]).pipe(
|
||||
map(([defaultBranch, selectedBranch]) => {
|
||||
if (defaultBranch?.branchType === 4) {
|
||||
if (!selectedBranch) {
|
||||
return 'Wählen Sie eine Filiale aus, um den Bestand zu sehen.';
|
||||
}
|
||||
return 'Sie sehen den Bestand einer anderen Filiale.';
|
||||
} else {
|
||||
if (selectedBranch && defaultBranch.id !== selectedBranch?.id) {
|
||||
return 'Sie sehen den Bestand einer anderen Filiale.';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
inStock$ = combineLatest([this.item$, this.selectedBranchId$, this.defaultBranch$]).pipe(
|
||||
debounceTime(100),
|
||||
switchMap(([item, branch, defaultBranch]) =>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DomainCatalogModule } from '@domain/catalog';
|
||||
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';
|
||||
@@ -28,6 +29,7 @@ import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe
|
||||
UiSpinnerModule,
|
||||
UiOrderByFilterModule,
|
||||
ScrollingModule,
|
||||
UiTooltipModule,
|
||||
],
|
||||
exports: [ArticleSearchResultsComponent, SearchResultItemComponent],
|
||||
declarations: [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<shared-breadcrumb class="my-4" [key]="activatedProcessId$ | async" [tags]="['catalog']">
|
||||
<shared-branch-selector [value]="selectedBranch$ | async" (valueChange)="patchProcessData($event)"> </shared-branch-selector>
|
||||
<shared-branch-selector [branchType]="1" [value]="selectedBranch$ | async" (valueChange)="patchProcessData($event)">
|
||||
</shared-branch-selector>
|
||||
</shared-breadcrumb>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -9,7 +9,7 @@ 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, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { first, map, shareReplay, switchMap, take, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { Subject, NEVER, combineLatest, BehaviorSubject } from 'rxjs';
|
||||
import { DomainCatalogService } from '@domain/catalog';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
@@ -443,11 +443,15 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
|
||||
|
||||
const customerFeatures = await this.customerFeatures$.pipe(first()).toPromise();
|
||||
|
||||
const branch = await this.domainCheckoutService
|
||||
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
|
||||
|
||||
@@ -5,6 +5,7 @@ import { map, mergeMap, switchMap, withLatestFrom } from 'rxjs/operators';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { ApplicationService } from '@core/application';
|
||||
|
||||
interface PurchasingOptionsListModalState {
|
||||
processId: number;
|
||||
@@ -101,7 +102,11 @@ export class PurchasingOptionsListModalStore extends ComponentStore<PurchasingOp
|
||||
})
|
||||
);
|
||||
|
||||
constructor(private _availabilityService: DomainAvailabilityService, private _checkoutService: DomainCheckoutService) {
|
||||
constructor(
|
||||
private _availabilityService: DomainAvailabilityService,
|
||||
private _checkoutService: DomainCheckoutService,
|
||||
private _application: ApplicationService
|
||||
) {
|
||||
super({
|
||||
processId: undefined,
|
||||
shoppingCartItems: [],
|
||||
@@ -398,6 +403,12 @@ export class PurchasingOptionsListModalStore extends ComponentStore<PurchasingOp
|
||||
)
|
||||
);
|
||||
|
||||
getCurrentBranch() {
|
||||
return combineLatest([this._application.getSelectedBranch$(), this._availabilityService.getDefaultBranch()]).pipe(
|
||||
map(([selectedBranch, defaultBranch]) => selectedBranch || defaultBranch)
|
||||
);
|
||||
}
|
||||
|
||||
loadBranches = this.effect(($) =>
|
||||
$.pipe(
|
||||
switchMap(() =>
|
||||
@@ -407,7 +418,7 @@ export class PurchasingOptionsListModalStore extends ComponentStore<PurchasingOp
|
||||
(branch) => branch.status === 1 && branch.branchType === 1 && branch.isOnline === true && branch.isShippingEnabled === true
|
||||
)
|
||||
),
|
||||
withLatestFrom(this._availabilityService.getDefaultBranch()),
|
||||
withLatestFrom(this.getCurrentBranch()),
|
||||
tapResponse(
|
||||
([branches, currentBranch]) => {
|
||||
this.patchState({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user