Compare commits

..

4 Commits

Author SHA1 Message Date
Nino Righi
d303b1444b Zoom Update and Article Result List Styling Update 2023-04-03 17:54:08 +02:00
Nino Righi
150e7965ee Zwischencommit 2023-03-29 12:57:05 +02:00
Nino Righi
3fcf3d9396 Responsive Design Splitscreen Article Search Results Update 2023-03-28 15:08:31 +02:00
Nino Righi
52278b8baf Initial Responsive Design Implementation based on Article Search 2023-03-24 17:19:59 +01:00
1883 changed files with 30927 additions and 60825 deletions

View File

@@ -1 +0,0 @@
npm run pretty-quick

View File

@@ -3,5 +3,6 @@
"johnpapa.angular2",
"esbenp.prettier-vscode",
"angular.ng-template",
"eg2.vscode-npm-script"
]
}

4
TASKS.md Normal file
View File

@@ -0,0 +1,4 @@
- Neue Icon Module (z.B. mit SVG sprites)
- Breadcrumb Navigation (Neu)
- Remissions Produkt Liste (Refactoring / Neu)
- Angular Version (Upgrade)

View File

@@ -375,6 +375,37 @@
}
}
},
"@shell/breadcrumb": {
"projectType": "library",
"root": "apps/shell/breadcrumb",
"sourceRoot": "apps/shell/breadcrumb/src",
"prefix": "shell",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/shell/breadcrumb/tsconfig.lib.json",
"project": "apps/shell/breadcrumb/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/breadcrumb/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/breadcrumb/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@domain/defs": {
"projectType": "library",
"root": "apps/domain/defs",
@@ -809,6 +840,37 @@
}
}
},
"@shell/header": {
"projectType": "library",
"root": "apps/shell/header",
"sourceRoot": "apps/shell/header/src",
"prefix": "shell",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/shell/header/tsconfig.lib.json",
"project": "apps/shell/header/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/header/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/header/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@modal/reorder": {
"projectType": "library",
"root": "apps/modal/reorder",
@@ -895,7 +957,8 @@
"pdfjs-dist/build/pdf",
"pdfjs-dist/web/pdf_viewer",
"pdfjs-dist/es5/build/pdf",
"pdfjs-dist/es5/web/pdf_viewer"
"pdfjs-dist/es5/web/pdf_viewer",
"scandit-sdk"
],
"outputPath": "dist/isa-app",
"index": "apps/isa-app/src/index.html",
@@ -911,7 +974,7 @@
"apps/isa-app/src/manifest.webmanifest",
{
"glob": "**/*",
"input": "node_modules/scandit-web-datacapture-barcode/build/engine",
"input": "node_modules/scandit-sdk/build",
"output": "scandit"
}
],
@@ -958,10 +1021,10 @@
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "isa-app:build:production"
"browserTarget": "isa-app:build:production"
},
"development": {
"buildTarget": "isa-app:build:development"
"browserTarget": "isa-app:build:development"
}
},
"defaultConfiguration": "development"
@@ -969,7 +1032,7 @@
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "isa-app:build"
"browserTarget": "isa-app:build"
}
},
"test": {
@@ -995,6 +1058,74 @@
}
}
},
"@shell/footer": {
"projectType": "library",
"root": "apps/shell/footer",
"sourceRoot": "apps/shell/footer/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/shell/footer/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/footer/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/shell/footer/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/footer/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@shell/process": {
"projectType": "library",
"root": "apps/shell/process",
"sourceRoot": "apps/shell/process/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/shell/process/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/process/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/shell/process/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/process/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@domain/isa": {
"projectType": "library",
"root": "apps/domain/isa",
@@ -1029,6 +1160,40 @@
}
}
},
"@shell/filter-overlay": {
"projectType": "library",
"root": "apps/shell/filter-overlay",
"sourceRoot": "apps/shell/filter-overlay/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/shell/filter-overlay/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/filter-overlay/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/shell/filter-overlay/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/filter-overlay/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@store/search-component-store": {
"projectType": "library",
"root": "apps/store/search-component-store",
@@ -1470,8 +1635,5 @@
}
}
}
},
"cli": {
"analytics": false
}
}

View File

@@ -11,10 +11,13 @@ export class DevScanAdapter implements ScanAdapter {
constructor(private _modal: UiModalService, private _environmentService: EnvironmentService) {}
async init(): Promise<boolean> {
return Promise.resolve(false);
// return new Promise((resolve, reject) => {
// resolve(isDevMode());
// });
if (this._environmentService.isTablet()) {
return new Promise((resolve, reject) => {
resolve(isDevMode());
});
}
return false;
}
scan(): Observable<string> {

View File

@@ -14,6 +14,7 @@ export class ScanAdapterService {
async init(): Promise<void> {
for (const adapter of this.scanAdapters) {
const isReady = await adapter.init();
console.log('ScanAdapterService.init', adapter.name, isReady);
this._readyAdapters[adapter.name] = isReady;
}
}
@@ -23,30 +24,42 @@ export class ScanAdapterService {
}
getAdapter(name: string): ScanAdapter | undefined {
return this._readyAdapters[name] && this.scanAdapters.find((adapter) => adapter.name === name);
return this.scanAdapters.find((adapter) => adapter.name === name);
}
// return true if at least one adapter is ready
isReady(): boolean {
return Object.values(this._readyAdapters).some((ready) => ready);
}
scan(): Observable<string> {
const adapterOrder = ['Native', 'Scandit', 'Dev'];
scan(ops: { use?: string; include?: string[]; exclude?: string[] } = { exclude: ['Dev'] }): Observable<string> {
let adapter: ScanAdapter;
for (const name of adapterOrder) {
adapter = this.getAdapter(name);
if (adapter) {
break;
}
if (ops.use == undefined) {
// get the first adapter that is ready to use
adapter = this.scanAdapters
.filter((adapter) => {
if (ops.include?.length) {
return ops.include.includes(adapter.name);
} else if (ops.exclude?.length) {
return !ops.exclude.includes(adapter.name);
} else {
return true;
}
})
.find((adapter) => this._readyAdapters[adapter.name]);
} else {
adapter = this.getAdapter(ops.use);
}
if (!adapter) {
return throwError('No adapter found');
}
if (this._readyAdapters[adapter.name] == false) {
return throwError('Adapter is not ready');
}
return adapter.scan();
}
}

View File

@@ -3,15 +3,13 @@
}
.scanner-container {
/* width: 100vw;
height: 100vh;
max-width: 100vh;
max-height: 100vh; */
width: 100vw;
max-width: 95vw;
max-height: calc(95vh - 120px);
}
.close-scanner {
@apply absolute bottom-12 left-[50%] -translate-x-[50%] block px-6 py-4 bg-white text-brand border-2 border-solid border-brand rounded-full text-lg font-bold mx-auto mt-4;
@apply whitespace-nowrap;
@apply block px-6 py-4 bg-white text-brand border-2 border-solid border-brand rounded-full text-lg font-bold mx-auto mt-4;
}
@screen desktop {

View File

@@ -1,2 +1,4 @@
<div class="scanner-container" #scanContainer></div>
<button class="close-scanner" type="button" (click)="close()">Scan abbrechen</button>
<button class="close-scanner" type="button" (click)="close()">
Scan abbrechen
</button>

View File

@@ -1,7 +1,6 @@
import { Component, ChangeDetectionStrategy, ElementRef, ViewChild, NgZone, AfterViewInit, OnDestroy, OnInit } from '@angular/core';
import { UiModalService } from '@ui/modal';
import { BarcodeCapture, BarcodeCaptureSettings, Symbology } from 'scandit-web-datacapture-barcode';
import { Camera, DataCaptureContext, DataCaptureView, FrameSourceState } from 'scandit-web-datacapture-core';
import { Component, ChangeDetectionStrategy, ElementRef, ViewChild, NgZone, AfterViewInit, OnDestroy } from '@angular/core';
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
import { Barcode, BarcodePicker, ScanResult, ScanSettings } from 'scandit-sdk';
@Component({
selector: 'app-scandit-overlay',
@@ -9,11 +8,8 @@ import { Camera, DataCaptureContext, DataCaptureView, FrameSourceState } from 's
styleUrls: ['scandit-overlay.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ScanditOverlayComponent implements OnInit, AfterViewInit, OnDestroy {
private dataCaptureContext: DataCaptureContext;
private dataCaptureView: DataCaptureView;
private barcodeCapture: BarcodeCapture;
private camera: Camera;
export class ScanditOverlayComponent implements AfterViewInit, OnDestroy {
private _barcodePicker: BarcodePicker;
private _onScan?: (code: string) => void;
@@ -21,61 +17,54 @@ export class ScanditOverlayComponent implements OnInit, AfterViewInit, OnDestroy
@ViewChild('scanContainer', { read: ElementRef, static: true }) scanContainer: ElementRef;
constructor(
private _zone: NgZone,
private _modal: UiModalService,
) {}
constructor(private _zone: NgZone, private _modal: UiModalService) {}
ngOnInit(): void {
this.dataCaptureView = new DataCaptureView();
this.dataCaptureView.connectToElement(this.scanContainer.nativeElement);
this.dataCaptureView.showProgressBar();
ngAfterViewInit(): void {
this.createBarcodePicker()
.then(() => {
this._barcodePicker.on('scan', (scanResult) => {
this._zone.run(() => this.handleScanrResult(scanResult));
});
})
.catch((err: Error) => {
this._modal
.open({
content: UiMessageModalComponent,
title: 'Zugriff auf Kamera verweigert',
data: { message: 'Falls Sie den Zugriff erlauben möchten, können Sie das über die Webseiteinstellung Ihres Browsers.' },
})
.afterClosed$.subscribe(() => {
this._onClose?.();
});
});
}
async ngAfterViewInit() {
this.dataCaptureContext = await DataCaptureContext.create();
this.dataCaptureView.setContext(this.dataCaptureContext);
this.barcodeCapture = await BarcodeCapture.forContext(this.dataCaptureContext, this.getScanSettings());
this.barcodeCapture.addListener({
didScan: (_, session, __) => {
this._zone.run(() => {
const result = session.newlyRecognizedBarcode;
const code = result?.data ?? '';
this._onScan?.(code);
});
},
async createBarcodePicker() {
this._barcodePicker = await BarcodePicker.create(this.scanContainer.nativeElement, {
playSoundOnScan: true,
vibrateOnScan: true,
});
this.camera = Camera.default;
this.dataCaptureContext.setFrameSource(this.camera);
await this.camera.switchToDesiredState(FrameSourceState.On);
this.dataCaptureView.hideProgressBar();
this._barcodePicker.applyScanSettings(this.getScanSettings());
}
getScanSettings(): BarcodeCaptureSettings {
const settings = new BarcodeCaptureSettings();
getScanSettings(): ScanSettings {
return new ScanSettings({
blurryRecognition: false,
settings.enableSymbologies([
Symbology.EAN8,
Symbology.EAN13UPCA,
Symbology.UPCE,
Symbology.Code128,
Symbology.Code39,
Symbology.Code93,
Symbology.InterleavedTwoOfFive,
Symbology.QR,
]);
return settings;
enabledSymbologies: [
Barcode.Symbology.EAN8,
Barcode.Symbology.EAN13,
Barcode.Symbology.UPCA,
Barcode.Symbology.UPCE,
Barcode.Symbology.CODE128,
Barcode.Symbology.CODE39,
Barcode.Symbology.CODE93,
Barcode.Symbology.INTERLEAVED_2_OF_5,
Barcode.Symbology.QR,
],
codeDuplicateFilter: 1000,
});
}
onScan(fn: (code: string) => void) {
@@ -86,14 +75,26 @@ export class ScanditOverlayComponent implements OnInit, AfterViewInit, OnDestroy
this._onClose = fn;
}
handleScanrResult(scanRestul: ScanResult) {
let result: string | undefined;
if (scanRestul.barcodes.length) {
result = scanRestul.barcodes[0].data;
} else if (scanRestul.texts.length) {
result = scanRestul.texts[0].value;
}
if (result) {
this._onScan?.(result);
}
}
close() {
this._onClose?.();
}
ngOnDestroy(): void {
this._zone.runOutsideAngular(() => {
this.barcodeCapture?.setEnabled(false);
this.camera?.switchToDesiredState(FrameSourceState.Off);
this._barcodePicker?.destroy(true);
});
}
}

View File

@@ -3,35 +3,23 @@ import { Observable, Subscriber } from 'rxjs';
import { ScanAdapter } from '../scan-adapter';
import { Overlay } from '@angular/cdk/overlay';
import { configure } from 'scandit-web-datacapture-core';
import { barcodeCaptureLoader } from 'scandit-web-datacapture-barcode';
import { configure } from 'scandit-sdk';
// import { ScanditModalComponent } from './scandit-modal';
import { Config } from '@core/config';
import { ComponentPortal } from '@angular/cdk/portal';
import { ScanditOverlayComponent } from './scandit-overlay.component';
import { EnvironmentService } from '@core/environment';
import { injectNetworkStatus$ } from 'apps/isa-app/src/app/services/network-status.service';
import { toSignal } from '@angular/core/rxjs-interop';
@Injectable()
export class ScanditScanAdapter implements ScanAdapter {
readonly name = 'Scandit';
private $networkStatus = toSignal(injectNetworkStatus$());
constructor(
private readonly _config: Config,
private _overlay: Overlay,
private _environmentService: EnvironmentService,
) {}
constructor(private readonly _config: Config, private _overlay: Overlay, private _environmentService: EnvironmentService) {}
async init(): Promise<boolean> {
if (this._environmentService.isTablet()) {
await configure({
licenseKey: this._config.get('licence.scandit'),
libraryLocation: new URL('scandit', document.baseURI).toString(),
moduleLoaders: [barcodeCaptureLoader()],
await configure(this._config.get('licence.scandit'), {
engineLocation: '/scandit/',
});
return true;
@@ -42,11 +30,6 @@ export class ScanditScanAdapter implements ScanAdapter {
scan(): Observable<string> {
return new Observable((observer) => {
if (this.$networkStatus() === 'offline') {
observer.error(new Error('No network connection'));
return;
}
const overlay = this.createOverlay();
const portal = this.createPortal();
@@ -66,7 +49,7 @@ export class ScanditScanAdapter implements ScanAdapter {
sub.add(
overlay.backdropClick().subscribe(() => {
complete();
}),
})
);
ref.instance.onScan((code) => {

View File

@@ -1,31 +0,0 @@
import { Directive, HostListener, Input } from '@angular/core';
import { ProductCatalogNavigationService } from '@shared/services';
@Directive({
selector: '[productImageNavigation]',
standalone: true,
})
export class NavigateOnClickDirective {
@Input('productImageNavigation') ean: string;
constructor(private readonly _productCatalogNavigation: ProductCatalogNavigationService) {}
@HostListener('click', ['$event'])
async onClick(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
if (this.ean) {
await this._navigateToProductSearchDetails();
}
}
private async _navigateToProductSearchDetails() {
await this._productCatalogNavigation
.getArticleDetailsPathByEan({
processId: Date.now(),
ean: this.ean,
extras: { queryParams: { main_qs: this.ean } },
})
.navigate();
}
}

View File

@@ -2,8 +2,8 @@ import { NgModule } from '@angular/core';
import { ProductImagePipe } from './product-image.pipe';
@NgModule({
declarations: [],
imports: [ProductImagePipe],
declarations: [ProductImagePipe],
imports: [],
exports: [ProductImagePipe],
})
export class ProductImageModule {}

View File

@@ -3,8 +3,6 @@ import { ProductImageService } from './product-image.service';
@Pipe({
name: 'productImage',
standalone: true,
pure: true,
})
export class ProductImagePipe implements PipeTransform {
constructor(private imageService: ProductImageService) {}

View File

@@ -5,5 +5,4 @@
export * from './lib/product-image.service';
export * from './lib/product-image.module';
export * from './lib/product-image.pipe';
export * from './lib/product-image-navigation.directive';
export * from './lib/tokens';

View File

@@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { Store } from '@ngrx/store';
import { BranchDTO } from '@swagger/checkout';
import { isBoolean, isNumber } from '@utils/common';
@@ -15,18 +16,19 @@ import {
selectActivatedProcess,
patchProcess,
patchProcessData,
selectTitle,
setTitle,
} from './store';
@Injectable()
export class ApplicationService {
/** @deprecated */
private activatedProcessIdSubject = new BehaviorSubject<number>(undefined);
/** @deprecated */
get activatedProcessId() {
return this.activatedProcessIdSubject.value;
}
/** @deprecated */
get activatedProcessId$() {
return this.activatedProcessIdSubject.asObservable();
}
@@ -46,14 +48,6 @@ export class ApplicationService {
return this.store.select(selectSection);
}
getTitle$() {
return this.getSection$().pipe(
map((section) => {
return section === 'customer' ? 'Kundenbereich' : 'Filialbereich';
})
);
}
/** @deprecated */
getActivatedProcessId$() {
return this.store.select(selectActivatedProcess).pipe(map((process) => process?.id));
@@ -86,28 +80,6 @@ export class ApplicationService {
return this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranch));
}
readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
async createCustomerProcess(processId?: number): Promise<ApplicationProcess> {
const processes = await this.getProcesses$('customer').pipe(first()).toPromise();
const processIds = processes.filter((x) => this.REGEX_PROCESS_NAME.test(x.name)).map((x) => +x.name.split(' ')[1]);
const maxId = processIds.length > 0 ? Math.max(...processIds) : 0;
const process: ApplicationProcess = {
id: processId ?? Date.now(),
type: 'cart',
name: `Vorgang ${maxId + 1}`,
section: 'customer',
closeable: true,
};
await this.createProcess(process);
return process;
}
async createProcess(process: ApplicationProcess) {
const existingProcess = await this.getProcessById$(process?.id).pipe(first()).toPromise();
if (existingProcess?.id === process?.id) {

View File

@@ -3,8 +3,6 @@ import { ApplicationProcess } from '..';
const prefix = '[CORE-APPLICATION]';
export const setTitle = createAction(`${prefix} Set Title`, props<{ title: string }>());
export const setSection = createAction(`${prefix} Set Section`, props<{ section: 'customer' | 'branch' }>());
export const addProcess = createAction(`${prefix} Add Process`, props<{ process: ApplicationProcess }>());

View File

@@ -1,18 +1,9 @@
import { Action, createReducer, on } from '@ngrx/store';
import {
setSection,
addProcess,
removeProcess,
setActivatedProcess,
patchProcess,
patchProcessData,
setTitle,
} from './application.actions';
import { setSection, addProcess, removeProcess, setActivatedProcess, patchProcess, patchProcessData } from './application.actions';
import { ApplicationState, INITIAL_APPLICATION_STATE } from './application.state';
const _applicationReducer = createReducer(
INITIAL_APPLICATION_STATE,
on(setTitle, (state, { title }) => ({ ...state, title })),
on(setSection, (state, { section }) => ({ ...state, section })),
on(addProcess, (state, { process }) => ({ ...state, processes: [...state.processes, { data: {}, ...process }] })),
on(removeProcess, (state, { processId }) => {

View File

@@ -2,8 +2,6 @@ import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ApplicationState } from './application.state';
export const selectApplicationState = createFeatureSelector<ApplicationState>('core-application');
export const selectTitle = createSelector(selectApplicationState, (s) => s.title);
export const selectSection = createSelector(selectApplicationState, (s) => s.section);
export const selectProcesses = createSelector(selectApplicationState, (s) => s.processes);

View File

@@ -1,13 +1,11 @@
import { ApplicationProcess } from '../defs';
export interface ApplicationState {
title: string;
processes: ApplicationProcess[];
section: 'customer' | 'branch';
}
export const INITIAL_APPLICATION_STATE: ApplicationState = {
title: '',
processes: [],
section: 'customer',
};

View File

@@ -17,10 +17,7 @@ export class AuthService {
private _authConfig: AuthConfig;
constructor(
private _config: Config,
private readonly _oAuthService: OAuthService,
) {
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()));
@@ -48,8 +45,6 @@ export class AuthService {
await this._oAuthService.loadDiscoveryDocumentAndTryLogin();
} catch (error) {
this.login();
throw error;
}
this._initialized.next(true);

View File

@@ -135,9 +135,9 @@ export class BreadcrumbService {
crumbs.forEach((crumb) => this.removeBreadcrumb(crumb.id));
}
getLatestBreadcrumbForSection(section: 'customer' | 'branch', predicate: (crumb: Breadcrumb) => boolean = (_) => true) {
getLatestBreadcrumbForSection(section: 'customer' | 'branch') {
return this.store
.select(selectors.selectBreadcrumbsBySection, { section })
.pipe(map((crumbs) => crumbs.sort((a, b) => b.timestamp - a.timestamp).find((f) => predicate(f))));
.pipe(map((crumbs) => crumbs.sort((a, b) => b.changed - a.changed).find((f) => true)));
}
}

View File

@@ -22,7 +22,7 @@ export interface Breadcrumb {
/**
* Url
*/
path: string | any[];
path: string;
/**
* Query Parameter

View File

@@ -1,24 +1,14 @@
import { Injectable } from '@angular/core';
import { CacheOptions } from './cache-options';
import { Cached } from './cached';
import { interval } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class CacheService {
constructor() {
this._registerCleanupTask();
}
constructor() {}
_registerCleanupTask() {
this.cleanup();
interval(1000 * 60).subscribe(() => {
this.cleanup();
});
}
set<T>(token: Object, data: T, options?: CacheOptions) {
set(token: Object, data: any, options?: CacheOptions) {
const persist = options?.persist;
const ttl = options?.ttl;
const cached: Cached = {
@@ -27,8 +17,6 @@ export class CacheService {
if (ttl) {
cached.until = Date.now() + ttl;
} else {
cached.until = Date.now() + 1000 * 60 * 60 * 12;
}
if (persist) {
@@ -41,15 +29,13 @@ export class CacheService {
return cached;
}
get<T = any>(token: Object, from?: 'session' | 'persist'): T {
get<T = any>(token: Object, from: 'session' | 'persist' = 'session'): T {
let cached: Cached;
if (from === 'session') {
cached = this.deserialize(sessionStorage.getItem(this.getKey(token)));
} else if (from === 'persist') {
cached = this.deserialize(localStorage.getItem(this.getKey(token)));
} else {
cached = this.deserialize(sessionStorage.getItem(this.getKey(token))) || this.deserialize(localStorage.getItem(this.getKey(token)));
}
if (!cached) {
@@ -73,8 +59,7 @@ export class CacheService {
}
private getKey(token: Object) {
const key = `CacheService_` + this.hash(JSON.stringify(token));
return key;
return this.hash(JSON.stringify(token));
}
private hash(data: string): string {
@@ -92,25 +77,4 @@ export class CacheService {
private deserialize(data: string): Cached {
return JSON.parse(data);
}
cleanup() {
// get all keys created by this service by looking for the service name and remove the entries
// that ttl is expired
let localStorageKeys = Object.keys(localStorage).filter((key) => key.startsWith('CacheService_'));
let seesionStorageKeys = Object.keys(sessionStorage).filter((key) => key.startsWith('CacheService_'));
localStorageKeys.forEach((key) => {
const cached = this.deserialize(localStorage.getItem(key));
if (cached.until < Date.now()) {
localStorage.removeItem(key);
}
});
seesionStorageKeys.forEach((key) => {
const cached = this.deserialize(sessionStorage.getItem(key));
if (cached.until < Date.now()) {
sessionStorage.removeItem(key);
}
});
}
}

View File

@@ -1,6 +1,4 @@
import { CommandService } from './command.service';
export abstract class ActionHandler<T = any> {
constructor(readonly action: string) {}
abstract handler(data: T, service?: CommandService): Promise<T>;
abstract handler(data: T): Promise<T>;
}

View File

@@ -1,12 +1,8 @@
import { ModuleWithProviders, NgModule, Provider, Type } from '@angular/core';
import { ModuleWithProviders, NgModule, Type } from '@angular/core';
import { ActionHandler } from './action-handler.interface';
import { CommandService } from './command.service';
import { FEATURE_ACTION_HANDLERS, ROOT_ACTION_HANDLERS } from './tokens';
export function provideActionHandlers(actionHandlers: Type<ActionHandler>[]): Provider[] {
return [CommandService, actionHandlers.map((handler) => ({ provide: FEATURE_ACTION_HANDLERS, useClass: handler, multi: true }))];
}
@NgModule({})
export class CoreCommandModule {
static forRoot(actionHandlers: Type<ActionHandler>[]): ModuleWithProviders<CoreCommandModule> {

View File

@@ -16,7 +16,7 @@ export class CommandService {
throw new Error('Action Handler does not exist');
}
data = await handler.handler(data, this);
data = await handler.handler(data);
}
return data;
}

View File

@@ -1,77 +1,17 @@
import { Injectable } from '@angular/core';
import { Platform } from '@angular/cdk/platform';
import { NativeContainerService } from 'native-container';
import { BreakpointObserver } from '@angular/cdk/layout';
import { map } from 'rxjs/operators';
const MATCH_TABLET = '(max-width: 1023px)';
const MATCH_DESKTOP_SMALL = '(min-width: 1024px) and (max-width: 1279px)';
const MATCH_DESKTOP = '(min-width: 1280px)';
const MATCH_DESKTOP_LARGE = '(min-width: 1440px)';
const MATCH_DESKTOP_XLARGE = '(min-width: 1920px)';
const MATCH_DESKTOP_XXLARGE = '(min-width: 2736px)';
@Injectable({
providedIn: 'root',
})
export class EnvironmentService {
constructor(
private _platform: Platform,
private _nativeContainer: NativeContainerService,
private _breakpointObserver: BreakpointObserver
) {}
constructor(private _platform: Platform, private _nativeContainer: NativeContainerService) {}
matchTablet(): boolean {
return this._breakpointObserver.isMatched(MATCH_TABLET);
}
matchTablet$ = this._breakpointObserver.observe(MATCH_TABLET).pipe(map((result) => result.matches));
matchDesktopSmall(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_SMALL);
}
matchDesktopSmall$ = this._breakpointObserver.observe(MATCH_DESKTOP_SMALL).pipe(map((result) => result.matches));
matchDesktop(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP);
}
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP).pipe(map((result) => result.matches));
matchDesktopLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_LARGE);
}
matchDesktopLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_LARGE).pipe(map((result) => result.matches));
matchDesktopXLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_XLARGE);
}
matchDesktopXLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_XLARGE).pipe(map((result) => result.matches));
matchDesktopXXLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_XXLARGE);
}
matchDesktopXXLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_XXLARGE).pipe(map((result) => result.matches));
/**
* @deprecated Use `matchDesktopSmall` or 'matchDesktop' instead.
*/
isDesktop(): boolean {
return !this.isTablet();
}
/**
* @deprecated Use `matchTablet` instead.
*/
isTablet(): boolean {
return this.isNative() || this.isSafari();
}
@@ -81,6 +21,6 @@ export class EnvironmentService {
}
isSafari(): boolean {
return this._platform.IOS && this._platform.SAFARI;
return (this._platform.ANDROID || this._platform.IOS) && this._platform.SAFARI;
}
}

View File

@@ -1,5 +1,6 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/core/toast",
"lib": {
"entryFile": "src/public-api.ts"
}

View File

@@ -1,3 +1,3 @@
// start:ng42.barrel
export * from './gender';
export * from './toast-animation';
// end:ng42.barrel

View File

@@ -0,0 +1,14 @@
import { AnimationTriggerMetadata, trigger, state, transition, style, animate } from '@angular/animations';
export const slideAnimationTime = 150;
export const toastAnimations: {
readonly slideToast: AnimationTriggerMetadata;
} = {
slideToast: trigger('slideAnimation', [
state('default', style({ transform: 'translateY(0%)' })),
transition('void => *', [style({ transform: 'translateY(-100%)' }), animate(`${slideAnimationTime}ms ease-in`)]),
transition('default => closing', animate(`${slideAnimationTime}ms ease-in`, style({ transform: 'translateY(-100%)' }))),
]),
};
export type ToastAnimationState = 'default' | 'closing';

View File

@@ -0,0 +1,4 @@
// start:ng42.barrel
export * from './toast';
export * from './toast-ref';
// end:ng42.barrel

View File

@@ -0,0 +1,17 @@
import { OverlayRef } from '@angular/cdk/overlay';
export class ToastRef {
constructor(private readonly _overlay: OverlayRef) {}
close() {
this._overlay.dispose();
}
isVisible() {
return this._overlay && this._overlay.overlayElement;
}
getPosition() {
return this._overlay.overlayElement.getBoundingClientRect();
}
}

View File

@@ -0,0 +1,11 @@
import { TemplateRef } from '@angular/core';
export interface Toast {
title?: string;
text?: string;
timer?: number;
position?: 'top-left' | 'top' | 'top-right' | 'bottom-right' | 'bottom' | 'bottom-left';
size?: 'width-full' | 'content';
template?: TemplateRef<any>; // For rendering dynamic content
templateContext?: {}; // For rendering dynamic content
}

View File

@@ -0,0 +1,8 @@
// start:ng42.barrel
export * from './toast.component';
export * from './toast.module';
export * from './toast.service';
export * from './defs';
export * from './animation';
export * from './tokens';
// end:ng42.barrel

View File

@@ -0,0 +1,15 @@
<div class="toast-main" [style.width]="width" [@slideAnimation]="{ value: animationState }" (@slideAnimation.done)="onSlideFinished()">
<button class="absolute top-2 right-2 p-6 border-none bg-transparent" (click)="close()">
<ui-icon icon="close" size="20px"></ui-icon>
</button>
<div class="toast-content flex flex-col justify-center items-center">
<h1 class="text-card-sub font-bold text-center py-3 whitespace-pre-wrap">{{ data.title }}</h1>
<ng-container *ngIf="data.text; else templateRef">
<p class="block text-base overflow-y-hidden pb-3 text-center overflow-x-hidden">{{ data.text }}</p>
</ng-container>
</div>
</div>
<ng-template #templateRef>
<ng-container *ngTemplateOutlet="data.template; context: data.templateContext"> </ng-container>
</ng-template>

View File

@@ -0,0 +1,12 @@
.toast-main {
@apply block relative mx-auto box-border text-white p-4;
background-color: var(--toast-background);
min-width: 18.75rem;
max-width: calc(100vw - 2rem);
min-height: 5rem;
border-radius: 25px;
}
.toast-content {
min-height: 3rem;
}

View File

@@ -0,0 +1,48 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { toastAnimations, ToastAnimationState, slideAnimationTime } from './animation';
import { Toast, ToastRef } from './defs';
import { TOAST_CONFIG_TOKEN } from './tokens';
@Component({
selector: 'lib-toast',
templateUrl: 'toast.component.html',
styleUrls: ['toast.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [toastAnimations.slideToast],
})
export class ToastComponent implements OnInit, OnDestroy {
timeoutRef?: any;
animationState: ToastAnimationState = 'default';
width = '55.25rem';
constructor(
@Inject(TOAST_CONFIG_TOKEN) public readonly data: Toast,
private readonly _ref: ToastRef,
private readonly _cdr: ChangeDetectorRef
) {}
ngOnInit(): void {
if (this.data?.size) {
this.width = this.data?.size === 'width-full' ? '100vw' : '55.25rem';
}
this.timeoutRef = setTimeout(() => {
this.close();
this._cdr.markForCheck();
}, slideAnimationTime + (this.data.timer ?? 5000));
}
ngOnDestroy() {
clearTimeout(this.timeoutRef);
}
close() {
this.animationState = 'closing';
}
onSlideFinished() {
if (this.animationState === 'closing') {
this._ref.close();
}
}
}

View File

@@ -0,0 +1,12 @@
import { OverlayModule } from '@angular/cdk/overlay';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { UiIconModule } from '@ui/icon';
import { ToastComponent } from './toast.component';
@NgModule({
declarations: [ToastComponent],
imports: [CommonModule, OverlayModule, UiIconModule],
exports: [ToastComponent],
})
export class ToastModule {}

View File

@@ -1,13 +1,13 @@
import { TestBed } from '@angular/core/testing';
import { NavigationService } from './navigation.service';
import { ToastService } from './toast.service';
xdescribe('NavigationService', () => {
let service: NavigationService;
describe('ToastService', () => {
let service: ToastService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(NavigationService);
service = TestBed.inject(ToastService);
});
it('should be created', () => {

View File

@@ -0,0 +1,79 @@
import { Overlay } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { Injectable, Injector } from '@angular/core';
import { Toast, ToastRef } from './defs';
import { ToastComponent } from './toast.component';
import { TOAST_CONFIG_TOKEN } from './tokens';
@Injectable({
providedIn: 'root',
})
export class ToastService {
private _lastToastRef: ToastRef;
get lastToastRef() {
return this._lastToastRef;
}
set lastToastRef(toastRef: ToastRef) {
this._lastToastRef = toastRef;
}
constructor(private readonly _overlay: Overlay, private readonly _injector: Injector) {}
create(data: Toast) {
const positionStrategy = this.getPositionStrategy(data);
const overlayRef = this._overlay.create({ positionStrategy });
this.lastToastRef = new ToastRef(overlayRef);
const injector = this.getInjector(data, this.lastToastRef);
const toastPortal = new ComponentPortal(ToastComponent, null, injector);
overlayRef.attach(toastPortal);
return this.lastToastRef;
}
getInjector(data: Toast, ref: ToastRef) {
return Injector.create({
parent: this._injector,
providers: [
{ provide: TOAST_CONFIG_TOKEN, useValue: data },
{ provide: ToastRef, useValue: ref },
],
});
}
getPositionStrategy(data: Toast) {
switch (data?.position) {
case 'top':
return this._overlay.position().global().top(this.getNextPosition()).centerHorizontally();
case 'top-left':
return this._overlay.position().global().top(this.getNextPosition()).left('1rem');
case 'top-right':
return this._overlay.position().global().top(this.getNextPosition()).right('1rem');
case 'bottom':
return this._overlay.position().global().bottom(this.getNextPosition(true)).centerHorizontally();
case 'bottom-left':
return this._overlay.position().global().bottom(this.getNextPosition(true)).left('1rem');
case 'bottom-right':
return this._overlay.position().global().bottom(this.getNextPosition(true)).right('1rem');
default:
return this._overlay.position().global().top(this.getNextPosition()).centerHorizontally();
}
}
getNextPosition(fromBottom?: boolean) {
const lastToastIsVisible = this.lastToastRef && this.lastToastRef.isVisible();
let position = fromBottom ? 6 : 9;
if (lastToastIsVisible && fromBottom) {
position = (window.innerHeight - this.lastToastRef.getPosition().bottom + this.lastToastRef.getPosition().height + 16) / 16;
} else if (lastToastIsVisible) {
position = (this.lastToastRef.getPosition().bottom + 16) / 16;
}
return position + 'rem';
}
}

View File

@@ -0,0 +1,4 @@
import { InjectionToken } from '@angular/core';
import { Toast } from './defs';
export const TOAST_CONFIG_TOKEN = new InjectionToken<Toast>('TOAST_DATA');

View File

@@ -0,0 +1,5 @@
/*
* Public API Surface of toast
*/
export * from './lib';

View File

@@ -8,7 +8,7 @@ import {
StoreCheckoutSupplierService,
SupplierDTO,
} from '@swagger/checkout';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import { combineLatest, Observable, of } from 'rxjs';
import {
AvailabilityRequestDTO,
AvailabilityService,
@@ -21,22 +21,17 @@ import { isArray, memorize } from '@utils/common';
import { LogisticianDTO, LogisticianService } from '@swagger/oms';
import { ResponseArgsOfIEnumerableOfStockInfoDTO, StockDTO, StockInfoDTO, StockService } from '@swagger/remi';
import { PriceDTO } from '@swagger/availability';
import { AvailabilityByBranchDTO, ItemData, Ssc } from './defs';
import { AvailabilityByBranchDTO, ItemData } from './defs';
import { Availability } from './defs/availability';
import { isEmpty } from 'lodash';
@Injectable()
export class DomainAvailabilityService {
// Ticket #3378 Keep Result List Items and Details Page SSC in sync
sscs$ = new BehaviorSubject<Array<Ssc>>([]);
sscsObs$ = this.sscs$.asObservable();
constructor(
private _availabilityService: AvailabilityService,
private _logisticanService: LogisticianService,
private _stockService: StockService,
private _supplierService: StoreCheckoutSupplierService,
private _branchService: StoreCheckoutBranchService,
private _branchService: StoreCheckoutBranchService
) {}
@memorize({ ttl: 10000 })
@@ -48,7 +43,7 @@ export class DomainAvailabilityService {
getSuppliers(): Observable<SupplierDTO[]> {
return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe(
map((response) => response.result),
shareReplay(1),
shareReplay(1)
);
}
@@ -56,7 +51,7 @@ export class DomainAvailabilityService {
getTakeAwaySupplier(): Observable<SupplierDTO> {
return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe(
map(({ result }) => result?.find((supplier) => supplier?.supplierNumber === 'F')),
shareReplay(1),
shareReplay(1)
);
}
@@ -64,7 +59,7 @@ export class DomainAvailabilityService {
getBranches(): Observable<BranchDTO[]> {
return this._branchService.StoreCheckoutBranchGetBranches({}).pipe(
map((response) => response.result),
shareReplay(1),
shareReplay(1)
);
}
@@ -73,7 +68,7 @@ export class DomainAvailabilityService {
return this._stockService.StockGetStocksByBranch({ branchId }).pipe(
map((response) => response.result),
map((result) => result?.find((_) => true)),
shareReplay(1),
shareReplay(1)
);
}
@@ -81,7 +76,7 @@ export class DomainAvailabilityService {
getDefaultStock(): Observable<StockDTO> {
return this._stockService.StockCurrentStock().pipe(
map((response) => response.result),
shareReplay(1),
shareReplay(1)
);
}
@@ -105,7 +100,7 @@ export class DomainAvailabilityService {
status: response.result.status,
version: response.result.version,
})),
shareReplay(1),
shareReplay(1)
);
}
@@ -113,7 +108,7 @@ export class DomainAvailabilityService {
getLogisticians(): Observable<LogisticianDTO> {
return this._logisticanService.LogisticianGetLogisticians({}).pipe(
map((response) => response.result?.find((l) => l.logisticianNumber === '2470')),
shareReplay(1),
shareReplay(1)
);
}
@@ -146,11 +141,10 @@ export class DomainAvailabilityService {
});
return availabilities;
}),
shareReplay(1),
shareReplay(1)
);
}
@memorize({ ttl: 10000 })
getTakeAwayAvailability({
item,
quantity,
@@ -167,13 +161,13 @@ export class DomainAvailabilityService {
this._stockService.StockInStock({ articleIds: [item.itemId], stockId: s.id }),
this.getTakeAwaySupplier(),
this.getDefaultBranch(),
]),
])
),
map(([response, supplier, defaultBranch]) => {
const price = item?.price;
return this._mapToTakeAwayAvailability({ response, supplier, branchId: branch?.id ?? defaultBranch?.id, quantity, price });
return this._mapToTakeAwayAvailability({ response, supplier, branchId: branch.id ?? defaultBranch.id, quantity, price });
}),
shareReplay(1),
shareReplay(1)
);
}
@@ -196,7 +190,7 @@ export class DomainAvailabilityService {
map(([response, supplier]) => {
return this._mapToTakeAwayAvailability({ response, supplier, branchId: branch.id, quantity, price });
}),
shareReplay(1),
shareReplay(1)
);
}
@@ -218,7 +212,7 @@ export class DomainAvailabilityService {
map(([response, supplier, defaultBranch]) => {
return this._mapToTakeAwayAvailability({ response, supplier, branchId: branchId ?? defaultBranch.id, quantity, price });
}),
shareReplay(1),
shareReplay(1)
);
}
@@ -228,7 +222,7 @@ export class DomainAvailabilityService {
switchMap((s) => this._stockService.StockInStockByEAN({ eans: eansFiltered, stockId: s.id })),
withLatestFrom(this.getTakeAwaySupplier(), this.getDefaultBranch()),
map((response) => response[0].result),
shareReplay(1),
shareReplay(1)
);
}
@@ -254,7 +248,7 @@ export class DomainAvailabilityService {
])
.pipe(
map((r) => this._mapToPickUpAvailability(r.result)?.find((_) => true)),
shareReplay(1),
shareReplay(1)
);
}
@@ -270,7 +264,7 @@ export class DomainAvailabilityService {
]).pipe(
timeout(5000),
map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)),
shareReplay(1),
shareReplay(1)
);
}
@@ -289,7 +283,7 @@ export class DomainAvailabilityService {
const availabilities = r.result;
const preferred = availabilities?.find((f) => f.preferred === 1);
return {
const availability: AvailabilityDTO = {
availabilityType: preferred?.status,
ssc: preferred?.ssc,
sscText: preferred?.sscText,
@@ -302,10 +296,10 @@ export class DomainAvailabilityService {
supplierProductNumber: preferred?.supplierProductNumber,
supplierInfo: preferred?.requestStatusCode,
lastRequest: preferred?.requested,
priceMaintained: preferred?.priceMaintained,
};
return availability;
}),
shareReplay(1),
shareReplay(1)
);
}
@@ -329,12 +323,12 @@ export class DomainAvailabilityService {
this.getPickUpAvailability({ item, quantity, branch: branch ?? defaultBranch }).pipe(
mergeMap((availability) =>
logistician$.pipe(
map((logistician) => ({ ...(availability?.length > 0 ? availability[0] : []), logistician: { id: logistician.id } })),
),
map((logistician) => ({ ...(availability?.length > 0 ? availability[0] : []), logistician: { id: logistician.id } }))
)
),
shareReplay(1),
),
),
shareReplay(1)
)
)
);
}
@@ -352,7 +346,7 @@ export class DomainAvailabilityService {
const availabilities = r.result;
const preferred = availabilities?.find((f) => f.preferred === 1);
return {
const availability: AvailabilityDTO = {
availabilityType: preferred?.status,
ssc: preferred?.ssc,
sscText: preferred?.sscText,
@@ -364,10 +358,10 @@ export class DomainAvailabilityService {
logistician: { id: preferred?.logisticianId },
supplierInfo: preferred?.requestStatusCode,
lastRequest: preferred?.requested,
priceMaintained: preferred?.priceMaintained,
};
return availability;
}),
shareReplay(1),
shareReplay(1)
);
}
@@ -378,7 +372,7 @@ export class DomainAvailabilityService {
switchMap((stockId) =>
stockId
? this._stockService.StockInStock({ articleIds: items.map((i) => i.id), stockId })
: of({ result: [] } as ResponseArgsOfIEnumerableOfStockInfoDTO),
: of({ result: [] } as ResponseArgsOfIEnumerableOfStockInfoDTO)
),
timeout(20000),
withLatestFrom(this.getTakeAwaySupplier()),
@@ -389,10 +383,10 @@ export class DomainAvailabilityService {
supplier,
quantity: 1,
price: items?.find((i) => i.id === stockInfo.itemId)?.price,
}),
})
);
}),
shareReplay(1),
shareReplay(1)
);
}
@@ -400,7 +394,7 @@ export class DomainAvailabilityService {
getPickUpAvailabilities(payload: AvailabilityRequestDTO[], preferred?: boolean) {
return this._availabilityService.AvailabilityStoreAvailability(payload).pipe(
timeout(20000),
map((response) => (preferred ? this._mapToPickUpAvailability(response.result) : response.result)),
map((response) => (preferred ? this._mapToPickUpAvailability(response.result) : response.result))
);
}
@@ -408,7 +402,7 @@ export class DomainAvailabilityService {
getDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
return this.memorizedAvailabilityShippingAvailability(payload).pipe(
timeout(20000),
map((response) => this._mapToShippingAvailability(response.result)),
map((response) => this._mapToShippingAvailability(response.result))
);
}
@@ -416,7 +410,7 @@ export class DomainAvailabilityService {
getDigDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
return this.memorizedAvailabilityShippingAvailability(payload).pipe(
timeout(20000),
map((response) => this._mapToShippingAvailability(response.result)),
map((response) => this._mapToShippingAvailability(response.result))
);
}
@@ -427,16 +421,16 @@ export class DomainAvailabilityService {
return this.getPickUpAvailabilities(payload, true).pipe(
timeout(20000),
switchMap((availability) =>
logistician$.pipe(map((logistician) => ({ availability: [...availability], logistician: { id: logistician.id } }))),
logistician$.pipe(map((logistician) => ({ availability: [...availability], logistician: { id: logistician.id } })))
),
shareReplay(1),
shareReplay(1)
);
}
getPriceForAvailability(
purchasingOption: string,
catalogAvailability: CatAvailabilityDTO | AvailabilityDTO,
availability: AvailabilityDTO,
availability: AvailabilityDTO
): PriceDTO {
switch (purchasingOption) {
case 'take-away':
@@ -458,6 +452,32 @@ export class DomainAvailabilityService {
return [2, 32, 256, 1024, 2048, 4096].some((code) => availability?.availabilityType === code);
}
mapToOlaAvailability({
availability,
item,
quantity,
}: {
availability: AvailabilityDTO;
item: ItemDTO;
quantity: number;
}): OLAAvailabilityDTO {
return {
status: availability?.availabilityType,
at: availability?.estimatedShippingDate,
ean: item?.product?.ean,
itemId: item?.id,
format: item?.product?.format,
isPrebooked: availability?.isPrebooked,
logisticianId: availability?.logistician?.id,
price: availability?.price,
qty: quantity,
ssc: availability?.ssc,
sscText: availability?.sscText,
supplierId: availability?.supplier?.id,
supplierProductNumber: availability?.supplierProductNumber,
};
}
private _mapToTakeAwayAvailability({
response,
supplier,
@@ -478,7 +498,7 @@ export class DomainAvailabilityService {
inStock: inStock,
supplierSSC: quantity <= inStock ? '999' : '',
supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '',
price: stockInfo?.retailPrice ?? price, // #4553 Es soll nun immer der retailPrice aus der InStock Abfrage verwendet werden, egal ob "price" empty ist oder nicht
price: price ?? stockInfo?.retailPrice,
supplier: { id: supplier?.id },
// TODO: Change after API Update
// LH: 2021-03-09 preis Property hat nun ein Fallback auf retailPrice
@@ -520,7 +540,6 @@ export class DomainAvailabilityService {
return preferred.map((p) => {
return [
{
orderDeadline: p?.orderDeadline,
availabilityType: p?.status,
ssc: p?.ssc,
sscText: p?.sscText,
@@ -533,7 +552,6 @@ export class DomainAvailabilityService {
supplierInfo: p?.requestStatusCode,
lastRequest: p?.requested,
itemId: p.itemId,
priceMaintained: p.priceMaintained,
},
p,
];
@@ -541,8 +559,9 @@ export class DomainAvailabilityService {
}
}
private _mapToShippingAvailability(availabilities: SwaggerAvailabilityDTO[]): AvailabilityDTO[] {
private _mapToShippingAvailability(availabilities: SwaggerAvailabilityDTO[]) {
const preferred = availabilities.filter((f) => f.preferred === 1);
return preferred.map((p) => {
return {
availabilityType: p?.status,
@@ -556,7 +575,6 @@ export class DomainAvailabilityService {
supplierInfo: p?.requestStatusCode,
lastRequest: p?.requested,
itemId: p.itemId,
priceMaintained: p.priceMaintained,
};
});
}
@@ -567,12 +585,12 @@ export class DomainAvailabilityService {
if (!params.branchId) {
branchId$ = this.getDefaultBranch().pipe(
first(),
map((b) => b.id),
map((b) => b.id)
);
}
const stock$ = branchId$.pipe(
mergeMap((branchId) => this._stockService.StockGetStocksByBranch({ branchId }).pipe(map((response) => response.result?.[0]))),
mergeMap((branchId) => this._stockService.StockGetStocksByBranch({ branchId }).pipe(map((response) => response.result?.[0])))
);
return stock$.pipe(
@@ -589,17 +607,17 @@ export class DomainAvailabilityService {
acc[stockInfo.ean] = stockInfo;
return acc;
}, {});
}),
),
),
})
)
)
);
}
getInStock({ itemIds, branchId }: { itemIds: number[]; branchId: number }): Observable<StockInfoDTO[]> {
return this.getStockByBranch(branchId).pipe(
mergeMap((stock) =>
this._stockService.StockInStock({ articleIds: itemIds, stockId: stock.id }).pipe(map((response) => response.result)),
),
this._stockService.StockInStock({ articleIds: itemIds, stockId: stock.id }).pipe(map((response) => response.result))
)
);
}
}

View File

@@ -1,4 +1,3 @@
export * from './availability-by-branch-dto';
export * from './availability';
export * from './item-data';
export * from './ssc';

View File

@@ -1,5 +0,0 @@
export interface Ssc {
itemId?: number;
ssc?: string;
sscText?: string;
}

View File

@@ -45,8 +45,6 @@ export class DomainInStockService {
const key = this.getKey({ itemId, branchId });
this._addToInStockQueue({ itemId, branchId });
let _previousValue: InStock;
const sub = combineLatest([this._inStockMap, this._inStockFetchingMap])
.pipe(distinctUntilChanged(isEqual))
.subscribe(([inStockMap, inStockFetchingMap]) => {
@@ -56,12 +54,7 @@ export class DomainInStockService {
inStock: inStockMap[key],
fetching: inStockFetchingMap[key] ?? false,
};
if (!isEqual(inStock, _previousValue)) {
obs.next(inStock);
}
_previousValue = inStock;
obs.next(inStock);
});
return () => {
sub.unsubscribe();

View File

@@ -11,12 +11,9 @@ export class ThumbnailUrlPipe implements PipeTransform, OnDestroy {
private input$ = new BehaviorSubject<{ width?: number; height?: number; ean?: string }>(undefined);
private result: string;
private onDestroy$ = new Subject<void>();
private onDestroy$ = new Subject();
constructor(
private domainCatalogThumbnailService: DomainCatalogThumbnailService,
private cdr: ChangeDetectorRef,
) {}
constructor(private domainCatalogThumbnailService: DomainCatalogThumbnailService, private cdr: ChangeDetectorRef) {}
ngOnDestroy(): void {
this.onDestroy$.next();
@@ -30,7 +27,7 @@ export class ThumbnailUrlPipe implements PipeTransform, OnDestroy {
this.input$
.pipe(
takeUntil(this.onDestroy$),
switchMap((input) => this.domainCatalogThumbnailService.getThumnaulUrl(input)),
switchMap((input) => this.domainCatalogThumbnailService.getThumnaulUrl(input))
)
.subscribe((result) => {
this.result = result;

View File

@@ -27,53 +27,23 @@ import {
StoreCheckoutPayerService,
StoreCheckoutBranchService,
ItemsResult,
KulturPassService,
ProductDTO,
KulturPassResult,
} from '@swagger/checkout';
import {
DisplayOrderDTO,
DisplayOrderItemDTO,
OrderCheckoutService,
ReorderValues,
ResponseArgsOfValueTupleOfIEnumerableOfDisplayOrderDTOAndIEnumerableOfKeyValueDTOOfStringAndString,
} from '@swagger/oms';
import { DisplayOrderDTO, DisplayOrderItemDTO, OrderCheckoutService, ReorderValues } from '@swagger/oms';
import { isNullOrUndefined, memorize } from '@utils/common';
import { combineLatest, Observable, of, concat, isObservable, throwError, interval as rxjsInterval } from 'rxjs';
import {
bufferCount,
catchError,
distinctUntilChanged,
filter,
first,
map,
mergeMap,
shareReplay,
switchMap,
tap,
withLatestFrom,
startWith,
} from 'rxjs/operators';
import { combineLatest, Observable, of, concat, isObservable, throwError } from 'rxjs';
import { bufferCount, catchError, filter, first, map, mergeMap, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import * as DomainCheckoutSelectors from './store/domain-checkout.selectors';
import * as DomainCheckoutActions from './store/domain-checkout.actions';
import { DomainAvailabilityService, ItemData } from '@domain/availability';
import { DomainAvailabilityService } from '@domain/availability';
import { HttpErrorResponse } from '@angular/common/http';
import { ApplicationService } from '@core/application';
import { CustomerDTO } from '@swagger/crm';
import { Config } from '@core/config';
import parseDuration from 'parse-duration';
import { CustomerDTO, EntityDTOContainerOfAttributeDTO } from '@swagger/crm';
@Injectable()
export class DomainCheckoutService {
get olaExpiration() {
const exp = this._config.get('@domain/checkout.olaExpiration') ?? '5m';
return parseDuration(exp);
}
constructor(
private store: Store<any>,
private _config: Config,
private applicationService: ApplicationService,
private storeCheckoutService: StoreCheckoutService,
private orderCheckoutService: OrderCheckoutService,
@@ -82,8 +52,7 @@ export class DomainCheckoutService {
private _paymentService: StoreCheckoutPaymentService,
private _buyerService: StoreCheckoutBuyerService,
private _payerService: StoreCheckoutPayerService,
private _branchService: StoreCheckoutBranchService,
private _kulturpassService: KulturPassService
private _branchService: StoreCheckoutBranchService
) {}
//#region shoppingcart
@@ -144,14 +113,14 @@ export class DomainCheckoutService {
})
.pipe(
map((response) => response.result),
tap((shoppingCart) => {
tap((shoppingCart) =>
this.store.dispatch(
DomainCheckoutActions.setShoppingCart({
processId,
shoppingCart,
})
);
}),
)
),
tap((shoppingCart) => this.updateProcessCount(processId, shoppingCart?.items?.length))
)
)
@@ -230,10 +199,6 @@ export class DomainCheckoutService {
);
}
canAddItemsKulturpass(payload: ProductDTO[]): Observable<KulturPassResult[]> {
return this._kulturpassService.KulturPassCanAddForKulturPass({ payload }).pipe(map((response) => response?.result));
}
canAddItems({
processId,
payload,
@@ -278,24 +243,11 @@ export class DomainCheckoutService {
shoppingCartItemId: number;
availability: AvailabilityDTO;
}) {
return this._shoppingCartService
.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability({
shoppingCartId,
shoppingCartItemId,
availability,
})
.pipe(
map((response) => response.result),
tap((shoppingCart) => {
this.store.dispatch(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId({
shoppingCartId,
availability,
shoppingCartItemId,
})
);
})
);
return this._shoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability({
shoppingCartId,
shoppingCartItemId,
availability,
});
}
updateItemInShoppingCart({
@@ -307,7 +259,7 @@ export class DomainCheckoutService {
shoppingCartItemId: number;
update: UpdateShoppingCartItemDTO;
}): Observable<ShoppingCartDTO> {
return this.getShoppingCart({ processId, latest: true }).pipe(
return this.getShoppingCart({ processId }).pipe(
first(),
mergeMap((shoppingCart) =>
this._shoppingCartService
@@ -318,21 +270,8 @@ export class DomainCheckoutService {
})
.pipe(
map((response) => response.result),
tap((shoppingCart) => {
this.store.dispatch(DomainCheckoutActions.setShoppingCart({ processId, shoppingCart }));
if (update.availability) {
this.store.dispatch(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory({
processId,
availability: update.availability,
shoppingCartItemId,
})
);
}
this.updateProcessCount(processId, shoppingCart?.items?.length);
})
tap((shoppingCart) => this.store.dispatch(DomainCheckoutActions.setShoppingCart({ processId, shoppingCart }))),
tap((shoppingCart) => this.updateProcessCount(processId, shoppingCart?.items?.length))
)
)
);
@@ -433,9 +372,8 @@ export class DomainCheckoutService {
_setBuyer({ processId, buyer }: { processId: number; buyer: BuyerDTO }): Observable<CheckoutDTO> {
return this.getCheckout({ processId }).pipe(
first(),
mergeMap((checkout) => {
console.log('checkout', checkout, processId);
return this._buyerService
mergeMap((checkout) =>
this._buyerService
.StoreCheckoutBuyerSetBuyerPOST({
checkoutId: checkout?.id,
buyerDTO: buyer,
@@ -443,8 +381,8 @@ export class DomainCheckoutService {
.pipe(
map((response) => response.result),
tap((checkout) => this.store.dispatch(DomainCheckoutActions.setCheckout({ processId, checkout })))
);
})
)
)
);
}
@@ -602,175 +540,6 @@ export class DomainCheckoutService {
);
}
async refreshAvailability({
processId,
shoppingCartItemId,
}: {
processId: number;
shoppingCartItemId: number;
}): Promise<AvailabilityDTO> {
const shoppingCart = await this.getShoppingCart({ processId }).pipe(first()).toPromise();
const item = shoppingCart?.items.find((item) => item.id === shoppingCartItemId)?.data;
if (!item) {
return;
}
const itemData: ItemData = {
ean: item.product.ean,
itemId: Number(item.product.catalogProductNumber),
price: item.availability.price,
};
let availability: AvailabilityDTO;
switch (item.features.orderType) {
case 'Abholung':
const abholung = await this.availabilityService
.getPickUpAvailability({
item: itemData,
branch: item.destination?.data?.targetBranch?.data,
quantity: item.quantity,
})
.toPromise();
availability = abholung[0];
break;
case 'Rücklage':
const ruecklage = await this.availabilityService
.getTakeAwayAvailability({
item: itemData,
quantity: item.quantity,
branch: item.destination?.data?.targetBranch?.data,
})
.toPromise();
availability = ruecklage;
break;
case 'Download':
const download = await this.availabilityService
.getDownloadAvailability({
item: itemData,
})
.toPromise();
availability = download;
break;
case 'Versand':
const versand = await this.availabilityService
.getDeliveryAvailability({
item: itemData,
quantity: item.quantity,
})
.toPromise();
availability = versand;
break;
case 'DIG-Versand':
const digVersand = await this.availabilityService
.getDigDeliveryAvailability({
item: itemData,
quantity: item.quantity,
})
.toPromise();
availability = digVersand;
break;
case 'B2B-Versand':
const b2bVersand = await this.availabilityService
.getB2bDeliveryAvailability({
item: itemData,
quantity: item.quantity,
})
.toPromise();
availability = b2bVersand;
break;
}
await this.updateItemInShoppingCart({
processId,
update: { availability },
shoppingCartItemId: item.id,
}).toPromise();
return availability;
}
/**
* Check if the availability of all items is valid
* @param param0 Process Id
* @returns true if the availability of all items is valid
*/
validateOlaStatus({ processId, interval }: { processId: number; interval?: number }): Observable<boolean> {
return rxjsInterval(interval ?? this.olaExpiration / 10).pipe(
startWith(0),
switchMap(() =>
this.store.select(DomainCheckoutSelectors.selectCheckoutEntityByProcessId, { processId }).pipe(
map((entity) => {
const now = Date.now();
if (!entity || !entity.shoppingCart || !entity.shoppingCart.items) {
return;
}
const itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp ?? {};
const shoppingCart = entity.shoppingCart;
const timestamps = shoppingCart.items
?.map((i) => i.data)
?.filter((item) => !!item?.features?.orderType)
?.map((item) => {
const orderType = item.features.orderType;
let timestamp = itemAvailabilityTimestamp[`${item.id}_${orderType}`];
if (timestamp) {
return timestamp;
}
if (orderType.endsWith('Versand')) {
timestamp =
itemAvailabilityTimestamp[`${item.id}_Versand`] ??
itemAvailabilityTimestamp[`${item.id}_DIG-Versand`] ??
itemAvailabilityTimestamp[`${item.id}_B2B-Versand`];
}
return timestamp;
})
?.filter((timestamp) => !!timestamp);
if (timestamps?.length > 0) {
const oldestTimestamp = Math.min(...timestamps);
const expirationTimestamp = oldestTimestamp + this.olaExpiration;
return expirationTimestamp > now;
}
return false;
})
)
),
distinctUntilChanged()
);
}
validateAvailabilities({ processId }: { processId: number }): Observable<boolean> {
return this.getShoppingCart({ processId }).pipe(
map((shoppingCart) => {
const items = shoppingCart?.items?.map((item) => item.data) || [];
return items.every((i) => this.availabilityService.isAvailable({ availability: i.availability }));
})
);
}
checkoutIsValid({ processId }: { processId: number }): Observable<boolean> {
const olaStatus$ = this.validateOlaStatus({ processId, interval: 250 });
const availabilities$ = this.validateAvailabilities({ processId });
return combineLatest([olaStatus$, availabilities$]).pipe(map(([olaStatus, availabilities]) => olaStatus && availabilities));
}
completeCheckout({ processId }: { processId: number }): Observable<DisplayOrderDTO[]> {
const refreshShoppingCart$ = this.getShoppingCart({ processId, latest: true }).pipe(first());
const refreshCheckout$ = this.getCheckout({ processId, refresh: true }).pipe(first());
@@ -924,64 +693,21 @@ export class DomainCheckoutService {
)
);
return of(undefined)
return updateDestination$
.pipe(tap(console.log.bind(window, 'updateDestination$')))
.pipe(
mergeMap((_) => updateDestination$.pipe(tap(console.log.bind(window, 'updateDestination$')))),
mergeMap((_) => refreshShoppingCart$.pipe(tap(console.log.bind(window, 'refreshShoppingCart$')))),
mergeMap((_) => setSpecialComment$.pipe(tap(console.log.bind(window, 'setSpecialComment$')))),
mergeMap((_) => refreshCheckout$.pipe(tap(console.log.bind(window, 'refreshCheckout$')))),
mergeMap((_) => checkAvailabilities$.pipe(tap(console.log.bind(window, 'checkAvailabilities$')))),
mergeMap((_) => updateAvailabilities$.pipe(tap(console.log.bind(window, 'updateAvailabilities$'))))
)
.pipe(
mergeMap((_) => updateAvailabilities$.pipe(tap(console.log.bind(window, 'updateAvailabilities$')))),
mergeMap((_) => setBuyer$.pipe(tap(console.log.bind(window, 'setBuyer$')))),
mergeMap((_) => setNotificationChannels$.pipe(tap(console.log.bind(window, 'setNotificationChannels$')))),
mergeMap((_) => setPayer$.pipe(tap(console.log.bind(window, 'setPayer$')))),
mergeMap((_) => setPaymentType$.pipe(tap(console.log.bind(window, 'setPaymentType$')))),
mergeMap((_) => setDestination$.pipe(tap(console.log.bind(window, 'setDestination$')))),
mergeMap((_) => completeOrder$.pipe(tap(console.log.bind(window, 'completeOrder$'))))
);
}
completeKulturpassOrder({
processId,
orderItemSubsetId,
}: {
processId: number;
orderItemSubsetId: number;
}): Observable<ResponseArgsOfValueTupleOfIEnumerableOfDisplayOrderDTOAndIEnumerableOfKeyValueDTOOfStringAndString> {
const refreshShoppingCart$ = this.getShoppingCart({ processId, latest: true }).pipe(first());
const refreshCheckout$ = this.getCheckout({ processId, refresh: true }).pipe(first());
const setBuyer$ = this.getBuyer({ processId }).pipe(
first(),
mergeMap((buyer) => this._setBuyer({ processId, buyer }))
);
const setPayer$ = this.getPayer({ processId }).pipe(
first(),
mergeMap((payer) => this._setPayer({ processId, payer }))
);
const checkAvailabilities$ = this.checkAvailabilities({ processId });
const updateAvailabilities$ = this.updateAvailabilities({ processId });
return refreshShoppingCart$.pipe(
mergeMap((_) => refreshCheckout$),
mergeMap((_) => checkAvailabilities$),
mergeMap((_) => updateAvailabilities$),
mergeMap((_) => setBuyer$),
mergeMap((_) => setPayer$),
mergeMap((checkout) =>
this.orderCheckoutService.OrderCheckoutCreateKulturPassOrder({
payload: {
checkoutId: checkout.id,
orderItemSubsetId: String(orderItemSubsetId),
},
})
mergeMap((_) => setDestination$.pipe(tap(console.log.bind(window, 'setDestination$'))))
)
);
.pipe(mergeMap((_) => completeOrder$.pipe(tap(console.log.bind(window, 'completeOrder$')))));
}
updateDestination({
@@ -1021,11 +747,6 @@ export class DomainCheckoutService {
//#region Common
// Fix für Ticket #4619 Versand Artikel im Warenkob -> keine Änderung bei Kundendaten erfassen
// Auskommentiert, da dieser Aufruf oftmals mit gleichen Parametern aufgerufen wird (ohne ausgewählten Kunden nur ein leeres Objekt bei customerFeatures)
// memorize macht keinen deepCompare von Objekten und denkt hier, dass immer der gleiche Return Wert zurückkommt, allerdings ist das hier oft nicht der Fall
// und der Decorator memorized dann fälschlicherweise
// @memorize()
canSetCustomer({
processId,
customerFeatures,
@@ -1033,26 +754,24 @@ export class DomainCheckoutService {
processId: number;
customerFeatures?: { [key: string]: string };
}): Observable<{ ok?: boolean; filter?: { [key: string]: string }; message?: string; create?: InputDTO }> {
return this.getShoppingCart({ processId })
.pipe(
first(),
mergeMap((shoppingCart) =>
this._shoppingCartService
.StoreCheckoutShoppingCartCanAddBuyer({
shoppingCartId: shoppingCart.id,
payload: { customerFeatures },
})
.pipe(
map((response) => ({
ok: response.result.ok,
filter: response.result.queryToken?.filter || {},
message: response.message,
create: response.result.create,
}))
)
)
return this.getShoppingCart({ processId }).pipe(
first(),
mergeMap((shoppingCart) =>
this._shoppingCartService
.StoreCheckoutShoppingCartCanAddBuyer({
shoppingCartId: shoppingCart.id,
payload: { customerFeatures },
})
.pipe(
map((response) => ({
ok: response.result.ok,
filter: response.result.queryToken?.filter || {},
message: response.message,
create: response.result.create,
}))
)
)
.pipe(shareReplay(1));
);
}
setNotificationChannels({ processId, notificationChannels }: { processId: number; notificationChannels: NotificationChannel }): void {
@@ -1211,5 +930,6 @@ export class DomainCheckoutService {
private updateProcessCount(processId: number, count: number) {
this.applicationService.patchProcessData(processId, { count });
}
//#endregion
}

View File

@@ -1,12 +1,4 @@
import {
AvailabilityDTO,
BuyerDTO,
CheckoutDTO,
NotificationChannel,
PayerDTO,
ShippingAddressDTO,
ShoppingCartDTO,
} from '@swagger/checkout';
import { BuyerDTO, CheckoutDTO, NotificationChannel, PayerDTO, ShippingAddressDTO, ShoppingCartDTO } from '@swagger/checkout';
import { CustomerDTO } from '@swagger/crm';
import { DisplayOrderDTO } from '@swagger/oms';
@@ -22,5 +14,4 @@ export interface CheckoutEntity {
specialComment: string;
notificationChannels: NotificationChannel;
olaErrorIds: number[];
itemAvailabilityTimestamp: Record<string, number | undefined>;
}

View File

@@ -7,7 +7,6 @@ import {
ShippingAddressDTO,
BuyerDTO,
PayerDTO,
AvailabilityDTO,
} from '@swagger/checkout';
import { CustomerDTO } from '@swagger/crm';
import { DisplayOrderDTO, DisplayOrderItemDTO } from '@swagger/oms';
@@ -62,13 +61,3 @@ export const setSpecialComment = createAction(`${prefix} Set Agent Comment`, pro
export const setOlaError = createAction(`${prefix} Set Ola Error`, props<{ processId: number; olaErrorIds: number[] }>());
export const setCustomer = createAction(`${prefix} Set Customer`, props<{ processId: number; customer: CustomerDTO }>());
export const addShoppingCartItemAvailabilityToHistory = createAction(
`${prefix} Add Shopping Cart Item Availability To History`,
props<{ processId: number; shoppingCartItemId: number; availability: AvailabilityDTO }>()
);
export const addShoppingCartItemAvailabilityToHistoryByShoppingCartId = createAction(
`${prefix} Add Shopping Cart Item Availability To History By Shopping Cart Id`,
props<{ shoppingCartId: number; shoppingCartItemId: number; availability: AvailabilityDTO }>()
);

View File

@@ -10,22 +10,7 @@ const _domainCheckoutReducer = createReducer(
initialCheckoutState,
on(DomainCheckoutActions.setShoppingCart, (s, { processId, shoppingCart }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
const addedShoppingCartItems =
shoppingCart?.items?.filter((item) => !entity.shoppingCart?.items?.find((i) => i.id === item.id))?.map((item) => item.data) ?? [];
entity.shoppingCart = shoppingCart;
entity.itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp ? { ...entity.itemAvailabilityTimestamp } : {};
const now = Date.now();
for (let shoppingCartItem of addedShoppingCartItems) {
if (shoppingCartItem.features?.orderType) {
entity.itemAvailabilityTimestamp[`${shoppingCartItem.id}_${shoppingCartItem.features.orderType}`] = now;
}
}
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setCheckout, (s, { processId, checkout }) => {
@@ -115,40 +100,7 @@ const _domainCheckoutReducer = createReducer(
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.customer = customer;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory, (s, { processId, shoppingCartItemId, availability }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp ? { ...entity?.itemAvailabilityTimestamp } : {};
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId)?.data;
if (!item?.features?.orderType) return s;
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] = Date.now();
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId,
(s, { shoppingCartId, shoppingCartItemId, availability }) => {
const entity = getCheckoutEntityByShoppingCartId({ shoppingCartId, entities: s.entities });
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp ? { ...entity?.itemAvailabilityTimestamp } : {};
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId)?.data;
if (!item?.features?.orderType) return s;
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] = Date.now();
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
return storeCheckoutAdapter.setOne(entity, s);
}
)
})
);
export function domainCheckoutReducer(state, action) {
@@ -171,20 +123,8 @@ function getOrCreateCheckoutEntity({ entities, processId }: { entities: Dictiona
notificationChannels: 0,
olaErrorIds: [],
customer: undefined,
// availabilityHistory: [],
itemAvailabilityTimestamp: {},
};
}
return { ...entity };
}
function getCheckoutEntityByShoppingCartId({
entities,
shoppingCartId,
}: {
entities: Dictionary<CheckoutEntity>;
shoppingCartId: number;
}): CheckoutEntity {
return Object.values(entities).find((entity) => entity.shoppingCart?.id === shoppingCartId);
}

View File

@@ -18,15 +18,14 @@ import {
NotificationChannel,
PayerDTO,
PayerService,
QueryTokenDTO,
ResponseArgsOfIEnumerableOfBonusCardInfoDTO,
ShippingAddressDTO,
ShippingAddressService,
} from '@swagger/crm';
import { isArray, memorize } from '@utils/common';
import { isArray } from '@utils/common';
import { PagedResult, Result } from 'apps/domain/defs/src/public-api';
import { Observable, of, ReplaySubject } from 'rxjs';
import { catchError, map, mergeMap, retry, shareReplay } from 'rxjs/operators';
import { catchError, map, mergeMap, retry } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CrmCustomerService {
@@ -39,14 +38,6 @@ export class CrmCustomerService {
private loyaltyCardService: LoyaltyCardService
) {}
@memorize()
filterSettings() {
return this.customerService.CustomerCustomerQuerySettings().pipe(
map((res) => res.result),
shareReplay(1)
);
}
complete(queryString: string, filter?: { [key: string]: string }): Observable<Result<AutocompleteDTO[]>> {
return this.customerService.CustomerCustomerAutocomplete({
input: queryString,
@@ -75,15 +66,6 @@ export class CrmCustomerService {
});
}
getCustomersWithQueryToken(queryToken: QueryTokenDTO) {
if (queryToken.skip === undefined) queryToken.skip = 0;
if (queryToken.take === undefined) queryToken.take = 20;
if (queryToken.input === undefined) queryToken.input = { qs: '' };
if (queryToken.filter === undefined) queryToken.filter = {};
return this.customerService.CustomerListCustomers(queryToken);
}
getCustomersByCustomerCardNumber(queryString: string): Observable<PagedResult<CustomerInfoDTO>> {
return this.customerService.CustomerGetCustomerByBonuscard(!!queryString ? queryString : undefined);
}

View File

@@ -1,88 +0,0 @@
import { ActionHandler } from '@core/command';
import {
AcceptedActionHandler,
ArrivedActionHandler,
AssembledActionHandler,
AvailableForDownloadActionHandler,
BackToStockActionHandler,
CanceledByBuyerActionHandler,
CanceledByRetailerActionHandler,
CanceledBySupplierActionHandler,
CreateShippingNoteActionHandler,
DeliveredActionHandler,
DetermineSupplierActionHandler,
DispatchedActionHandler,
DownloadedActionHandler,
FetchedActionHandler,
InProcessActionHandler,
NotAvailableActionHandler,
NotFetchedActionHandler,
OrderAtSupplierActionHandler,
OrderingActionHandler,
OverdueActionHandler,
PackedActionHandler,
ParkedActionHandler,
PlacedActionHandler,
PreparationForShippingActionHandler,
PrintCompartmentLabelActionHandler,
PrintShippingNoteActionHandler,
ReOrderActionHandler,
RedirectedInternaqllyActionHandler,
RequestedActionHandler,
ReserverdActionHandler,
ReturnedByBuyerActionHandler,
ShippingNoteActionHandler,
SupplierTemporarilyOutOfStockActionHandler,
ReOrderedActionHandler,
CollectOnDeliveryNoteActionHandler,
PrintPriceDiffQrCodeLabelActionHandler,
CollectWithSmallAmountinvoiceActionHandler,
PrintSmallamountinvoiceActionHandler,
ShopWithKulturpassActionHandler,
ChangeOrderItemStatusBaseActionHandler,
CreateReturnItemActionHandler,
} from './action-handlers';
import { Type } from '@angular/core';
export const ActionHandlerServices: Type<ActionHandler>[] = [
AcceptedActionHandler,
ArrivedActionHandler,
AssembledActionHandler,
AvailableForDownloadActionHandler,
BackToStockActionHandler,
CanceledByBuyerActionHandler,
CanceledByRetailerActionHandler,
CanceledBySupplierActionHandler,
CreateShippingNoteActionHandler,
DeliveredActionHandler,
DetermineSupplierActionHandler,
DispatchedActionHandler,
DownloadedActionHandler,
FetchedActionHandler,
InProcessActionHandler,
NotAvailableActionHandler,
NotFetchedActionHandler,
OrderAtSupplierActionHandler,
OrderingActionHandler,
OverdueActionHandler,
PackedActionHandler,
ParkedActionHandler,
PlacedActionHandler,
PreparationForShippingActionHandler,
PrintCompartmentLabelActionHandler,
PrintShippingNoteActionHandler,
ReOrderActionHandler,
RedirectedInternaqllyActionHandler,
RequestedActionHandler,
ReserverdActionHandler,
ReturnedByBuyerActionHandler,
ShippingNoteActionHandler,
SupplierTemporarilyOutOfStockActionHandler,
ReOrderedActionHandler,
CollectOnDeliveryNoteActionHandler,
PrintPriceDiffQrCodeLabelActionHandler,
CollectWithSmallAmountinvoiceActionHandler,
PrintSmallamountinvoiceActionHandler,
ShopWithKulturpassActionHandler,
CreateReturnItemActionHandler,
];

View File

@@ -21,7 +21,6 @@ export class CollectOnDeliveryNoteActionHandler extends ActionHandler<OrderItems
const response = await this.orderService
.OrderCollectOnDeliveryNote({
data,
eagerLoading: 1,
})
.toPromise();
@@ -30,7 +29,7 @@ export class CollectOnDeliveryNoteActionHandler extends ActionHandler<OrderItems
return {
...context,
receipts: response.result.map((r) => r.data),
receipts: response.result,
};
}
}

View File

@@ -1,35 +0,0 @@
import { Injectable } from '@angular/core';
import { ActionHandler } from '@core/command';
import { OrderService } from '@swagger/oms';
import { OrderItemsContext } from './order-items.context';
@Injectable()
export class CollectWithSmallAmountinvoiceActionHandler extends ActionHandler<OrderItemsContext> {
constructor(private orderService: OrderService) {
super('COLLECT_WITH_SMALLAMOUNTINVOICE');
}
async handler(context: OrderItemsContext): Promise<OrderItemsContext> {
const data: Record<number, number> = {};
context.items.forEach((orderItemSubsetId) => {
data[orderItemSubsetId.orderItemSubsetId] =
context.itemQuantity?.get(orderItemSubsetId.orderItemSubsetId) ?? orderItemSubsetId.quantity;
});
const response = await this.orderService
.OrderCollectWithSmallAmountInvoice({
data,
eagerLoading: 1,
})
.toPromise();
// Für korrekte Navigation nach Aufruf, da ProcessingStatus Serverseitig auf abgeholt gesetzt wird
context.items?.forEach((i) => (i.processingStatus = 256));
return {
...context,
receipts: response.result.map((r) => r.data),
};
}
}

View File

@@ -1,3 +1,4 @@
// start:ng42.barrel
export * from './accepted.action-handler';
export * from './arrived.action-handler';
export * from './assembled.action-handler';
@@ -6,10 +7,6 @@ export * from './back-to-stock.action-handler';
export * from './canceled-by-buyer.action-handler';
export * from './canceled-by-retailer.action-handler';
export * from './canceled-by-supplier.action-handler';
export * from './change-order-item-status-base.action-handler';
export * from './collect-on-deliverynote.action-handler';
export * from './collect-with-smallamountinvoice.action-handler';
export * from './create-returnitem.action-handler';
export * from './create-shipping-note.action-handler';
export * from './delivered.action-handler';
export * from './determine-supplier.action-handler';
@@ -28,15 +25,16 @@ export * from './parked.action-handler';
export * from './placed.action-handler';
export * from './preperation-for-shipping.action-handler';
export * from './print-compartment-label.action-handler';
export * from './print-pricediffqrcodelabel.action-handler';
export * from './print-shipping-note.action-handler';
export * from './print-smallamountinvoice.action-handler';
export * from './re-order.action-handler';
export * from './re-ordered.action-handler';
export * from './re-order.action-handler';
export * from './redirected-internally.action-handler';
export * from './requested.action-handler';
export * from './reserved.action-handler';
export * from './returned-by-buyer.action-handler';
export * from './shipping-note.action-handler';
export * from './shop-with-kulturpass.action-handler';
export * from './supplier-temporarily-out-of-stock.action-handler copy';
export * from './collect-on-deliverynote.action-handler';
export * from './create-returnitem.action-handler';
export * from './print-pricediffqrcodelabel.action-handler';
// end:ng42.barrel

View File

@@ -1,4 +1,4 @@
import { OrderItemListItemDTO, ReceiptDTO, OrderDTO } from '@swagger/oms';
import { OrderItemListItemDTO, ReceiptDTO } from '@swagger/oms';
export interface OrderItemsContext {
items: OrderItemListItemDTO[];
@@ -12,6 +12,4 @@ export interface OrderItemsContext {
receipts?: ReceiptDTO[];
shippingDelayComment?: string;
order?: OrderDTO;
}

View File

@@ -1,60 +1,37 @@
import { Injectable } from '@angular/core';
import { ActionHandler } from '@core/command';
import { DomainPrinterService, Printer } from '@domain/printer';
import { DomainPrinterService } from '@domain/printer';
import { PrintModalComponent, PrintModalData } from '@modal/printer';
import { UiModalService } from '@ui/modal';
import { NativeContainerService } from 'native-container';
import { OrderItemsContext } from './order-items.context';
import { EnvironmentService } from '@core/environment';
@Injectable()
export class PrintCompartmentLabelActionHandler extends ActionHandler<OrderItemsContext> {
constructor(
private uiModal: UiModalService,
private domainPrinterService: DomainPrinterService,
private nativeContainerService: NativeContainerService,
private _environmentSerivce: EnvironmentService
private nativeContainerService: NativeContainerService
) {
super('PRINT_COMPARTMENTLABEL');
}
printCompartmentLabelHelper(printer: string, orderItemSubsetIds: number[]) {
return this.domainPrinterService
.printCompartmentLabel({
printer,
orderItemSubsetIds,
})
.toPromise();
}
async handler(data: OrderItemsContext): Promise<OrderItemsContext> {
const printerList = await this.domainPrinterService.getAvailableLabelPrinters().toPromise();
let printer: Printer;
await this.uiModal
.open({
content: PrintModalComponent,
config: { showScrollbarY: false },
data: {
printImmediately: !this.nativeContainerService.isNative,
printerType: 'Label',
print: (printer) =>
this.domainPrinterService
.printCompartmentLabel({ printer, orderItemSubsetIds: data.items.map((item) => item.orderItemSubsetId) })
.toPromise(),
} as PrintModalData,
})
.afterClosed$.toPromise();
if (Array.isArray(printerList)) {
printer = printerList.find((printer) => printer.selected === true);
}
if (!printer || this._environmentSerivce.matchTablet()) {
await this.uiModal
.open({
content: PrintModalComponent,
config: { showScrollbarY: false },
data: {
printImmediately: !this._environmentSerivce.matchTablet(),
printerType: 'Label',
print: (printer) =>
this.printCompartmentLabelHelper(
printer,
data.items.map((item) => item.orderItemSubsetId)
),
} as PrintModalData,
})
.afterClosed$.toPromise();
} else {
await this.printCompartmentLabelHelper(
printer.key,
data.items.map((item) => item.orderItemSubsetId)
);
}
return data;
}
}

View File

@@ -6,60 +6,44 @@ import { UiModalService } from '@ui/modal';
import { PrintModalComponent, PrintModalData } from '@modal/printer';
import { groupBy } from '@ui/common';
import { NativeContainerService } from 'native-container';
import { ReceiptDTO } from '@swagger/oms';
import { EnvironmentService } from '@core/environment';
@Injectable()
export class PrintShippingNoteActionHandler extends ActionHandler<OrderItemsContext> {
constructor(
private uiModal: UiModalService,
private domainPrinterService: DomainPrinterService,
private nativeContainerService: NativeContainerService,
private _environmentSerivce: EnvironmentService
private nativeContainerService: NativeContainerService
) {
super('PRINT_SHIPPINGNOTE');
}
async printShippingNoteHelper(printer: string, receipts: ReceiptDTO[]) {
try {
for (const group of groupBy(receipts, (receipt) => receipt?.buyer?.buyerNumber)) {
await this.domainPrinterService.printShippingNote({ printer, receipts: group?.items?.map((r) => r?.id) }).toPromise();
}
return {
error: false,
};
} catch (error) {
console.error(error);
return {
error: true,
message: error?.message || error,
};
}
}
async handler(data: OrderItemsContext): Promise<OrderItemsContext> {
const printerList = await this.domainPrinterService.getAvailableLabelPrinters().toPromise();
const receipts = data?.receipts?.filter((r) => r?.receiptType & 1);
let printer: Printer;
if (Array.isArray(printerList)) {
printer = printerList.find((printer) => printer.selected === true);
}
if (!printer || this._environmentSerivce.matchTablet()) {
await this.uiModal
.open({
content: PrintModalComponent,
config: { showScrollbarY: false },
data: {
printImmediately: !this.nativeContainerService.isNative,
printerType: 'Label',
print: async (printer) => await this.printShippingNoteHelper(printer, receipts),
} as PrintModalData,
})
.afterClosed$.toPromise();
} else {
await this.printShippingNoteHelper(printer.key, receipts);
}
await this.uiModal
.open({
content: PrintModalComponent,
config: { showScrollbarY: false },
data: {
printImmediately: !this.nativeContainerService.isNative,
printerType: 'Label',
print: async (printer) => {
try {
for (const group of groupBy(data?.receipts, (receipt) => receipt?.buyer?.buyerNumber)) {
await this.domainPrinterService.printShippingNote({ printer, receipts: group?.items?.map((r) => r?.id) }).toPromise();
}
return {
error: false,
};
} catch (error) {
console.error(error);
return {
error: true,
message: error?.message || error,
};
}
},
} as PrintModalData,
})
.afterClosed$.toPromise();
return data;
}

View File

@@ -1,56 +0,0 @@
import { Injectable } from '@angular/core';
import { ActionHandler } from '@core/command';
import { OrderItemsContext } from './order-items.context';
import { OMSPrintService } from '@swagger/print';
import { UiModalService } from '@ui/modal';
import { PrintModalComponent, PrintModalData } from '@modal/printer';
import { NativeContainerService } from 'native-container';
import { groupBy } from '@ui/common';
@Injectable()
export class PrintSmallamountinvoiceActionHandler extends ActionHandler<OrderItemsContext> {
constructor(
private uiModal: UiModalService,
private omsPrintService: OMSPrintService,
private nativeContainerService: NativeContainerService
) {
super('PRINT_SMALLAMOUNTINVOICE');
}
async handler(data: OrderItemsContext): Promise<OrderItemsContext> {
await this.uiModal
.open({
content: PrintModalComponent,
config: { showScrollbarY: false },
data: {
printImmediately: !this.nativeContainerService.isNative,
printerType: 'Label',
print: async (printer) => {
try {
const receipts = data?.receipts?.filter((r) => r?.receiptType & 128);
for (const group of groupBy(receipts, (receipt) => receipt?.buyer?.buyerNumber)) {
await this.omsPrintService
.OMSPrintKleinbetragsrechnung({
data: group?.items?.map((r) => r?.id),
printer,
})
.toPromise();
}
return {
error: false,
};
} catch (error) {
console.error(error);
return {
error: true,
message: error?.message || error,
};
}
},
} as PrintModalData,
})
.afterClosed$.toPromise();
return data;
}
}

View File

@@ -5,7 +5,7 @@ import { UiModalService } from '@ui/modal';
import { ReorderModalComponent, ReorderResult } from '@modal/reorder';
import { DomainCheckoutService } from '@domain/checkout';
import { AvailabilityDTO2, OrderItemListItemDTO } from '@swagger/oms';
import { ToasterService } from '@shared/shell';
import { ToastService } from '@core/toast';
@Injectable()
export class ReOrderActionHandler extends ActionHandler<OrderItemsContext> {
@@ -13,7 +13,7 @@ export class ReOrderActionHandler extends ActionHandler<OrderItemsContext> {
private _command: CommandService,
private _domainCheckoutService: DomainCheckoutService,
private _uiModal: UiModalService,
private _toastService: ToasterService
private _toastService: ToastService
) {
super('REORDER');
}
@@ -71,8 +71,8 @@ export class ReOrderActionHandler extends ActionHandler<OrderItemsContext> {
case 'Falscher Titel geliefert (richtiges Etikett)':
break;
default:
this._toastService.open({
message: 'Artikel wurde nachbestellt',
this._toastService.create({
title: 'Artikel wurde nachbestellt',
});
}
}

View File

@@ -1,82 +0,0 @@
import { Injectable } from '@angular/core';
import { OrderItemsContext } from './order-items.context';
import { ActionHandler, CommandService } from '@core/command';
import { KulturpassOrderModalService } from '@shared/modals/kulturpass-order-modal';
import { DisplayOrderItemSubsetDTO, OrderItemListItemDTO, ReceiptDTO } from '@swagger/oms';
import { DomainReceiptService } from '../receipt.service';
import { DomainGoodsService } from '../goods.service';
import { map } from 'rxjs/operators';
@Injectable()
export class ShopWithKulturpassActionHandler extends ActionHandler<OrderItemsContext> {
constructor(
private _modal: KulturpassOrderModalService,
private _receiptService: DomainReceiptService,
private _goodsService: DomainGoodsService
) {
super('SHOP_WITH_KULTURPASS');
}
async handler(data: OrderItemsContext, service: CommandService): Promise<OrderItemsContext> {
const items: OrderItemListItemDTO[] = [];
const receipts: ReceiptDTO[] = [];
let command: string;
for (const item of data.items) {
const result = await this._modal.open({ orderItemListItem: item, order: data.order }).afterClosed$.toPromise();
if (result.data == null) {
return data;
}
const displayOrder = result.data[0];
command = result.data[1];
if (displayOrder) {
const subsetItems = displayOrder.items.reduce((acc, item) => [...acc, ...item.subsetItems], [] as DisplayOrderItemSubsetDTO[]);
const orderItems = await this.getItems(displayOrder.orderNumber);
items.push(...orderItems);
const subsetItemIds = subsetItems.map((item) => item.id);
const r = await this.getReceipts(subsetItemIds);
receipts.push(...r);
}
}
if (!command) {
return {
...data,
items,
receipts,
};
} else {
return service.handleCommand(command, {
...data,
items,
receipts,
});
}
}
getReceipts(ids: number[]) {
return this._receiptService
.getReceipts({
receiptType: 128,
eagerLoading: 1,
ids,
})
.pipe(map((res) => res.result.map((data) => data.item3.data).filter((data) => !!data)))
.toPromise();
}
getItems(orderNumber: string) {
return this._goodsService
.getWarenausgabeItemByOrderNumber(orderNumber, false)
.pipe(map((res) => res.result))
.toPromise();
}
}

View File

@@ -1,7 +1,5 @@
import { Injectable } from '@angular/core';
import { AutocompleteTokenDTO, OrderService, QueryTokenDTO } from '@swagger/oms';
import { memorize } from '@utils/common';
import { map, shareReplay } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class DomainCustomerOrderService {
@@ -16,18 +14,16 @@ export class DomainCustomerOrderService {
// branch_id'
}
getOrderItemsByOrderNumber(params: { compartmentCode?: string; orderId: number }) {
getOrderItemsByOrderNumber(orderNumber: string) {
return this._orderService.OrderKundenbestellungen({
filter: { all_branches: 'true', archive: 'true' },
input: { order_id: String(params.orderId), compartment_code: params.compartmentCode },
input: {
qs: orderNumber,
},
});
}
@memorize()
settings() {
return this._orderService.OrderKundenbestellungenSettings().pipe(
map((res) => res?.result),
shareReplay()
);
return this._orderService.OrderKundenbestellungenSettings();
}
}

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import {
AutocompleteTokenDTO,
BranchService,
BuyerDTO,
ChangeStockStatusCodeValues,
HistoryDTO,
NotificationChannel,
@@ -11,7 +11,9 @@ import {
OrderItemSubsetDTO,
OrderListItemDTO,
OrderService,
QueryTokenDTO,
ReceiptService,
ResponseArgsOfIEnumerableOfHistoryDTO,
StatusValues,
StockStatusCodeService,
ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO,
@@ -20,7 +22,7 @@ import {
} from '@swagger/oms';
import { memorize } from '@utils/common';
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { map, mergeMap, shareReplay } from 'rxjs/operators';
@Injectable()
export class DomainOmsService {
@@ -30,7 +32,7 @@ export class DomainOmsService {
private branchService: BranchService,
private vatService: VATService,
private stockStatusCodeService: StockStatusCodeService,
private _orderCheckoutService: OrderCheckoutService,
private _orderCheckoutService: OrderCheckoutService
) {}
getOrderItemsByCustomerNumber(customerNumber: string, skip: number): Observable<OrderListItemDTO[]> {
@@ -55,7 +57,7 @@ export class DomainOmsService {
return this.receiptService
.ReceiptGetReceiptsByOrderItemSubset({
payload: {
receiptType: 65 as unknown as any,
receiptType: (65 as unknown) as any,
ids: orderItemSubsetIds,
eagerLoading: 1,
},
@@ -77,7 +79,7 @@ export class DomainOmsService {
getStockStatusCodes({ supplierId, eagerLoading = 0 }: { supplierId: number; eagerLoading?: number }) {
return this.stockStatusCodeService.StockStatusCodeGetStockStatusCodes({ supplierId, eagerLoading }).pipe(
map((response) => response.result),
shareReplay(),
shareReplay()
);
}
@@ -121,7 +123,7 @@ export class DomainOmsService {
orderId: number,
orderItemId: number,
orderItemSubsetId: number,
data: StatusValues,
data: StatusValues
): Observable<ValueTupleOfOrderItemSubsetDTOAndOrderItemSubsetDTO> {
return this.orderService
.OrderChangeStatus({
@@ -185,14 +187,10 @@ export class DomainOmsService {
selected: order.notificationChannels,
email: order.buyer?.communicationDetails?.email,
mobile: order.buyer?.communicationDetails?.mobile,
})),
}))
);
}
getOrderSource(orderId: number): Observable<string> {
return this.getOrder(orderId).pipe(map((order) => order?.features?.orderSource));
}
updateNotifications(orderId: number, changes: { selected: NotificationChannel; email: string; mobile: string }) {
const communicationDetails = {
email: changes.email,
@@ -206,47 +204,12 @@ export class DomainOmsService {
delete communicationDetails.mobile;
}
return this.updateOrder({ orderId, notificationChannels: changes.selected, communicationDetails });
}
updateOrder({
orderId,
notificationChannels,
communicationDetails,
firstName,
lastName,
organisation,
}: {
orderId: number;
notificationChannels?: NotificationChannel;
communicationDetails?: { email?: string; mobile?: string };
lastName?: string;
firstName?: string;
organisation?: string;
}) {
const buyer: BuyerDTO = {};
if (!!communicationDetails) {
buyer.communicationDetails = { ...communicationDetails };
}
if (!!lastName || !!firstName) {
buyer.lastName = lastName;
buyer.firstName = firstName;
}
if (!!organisation) {
buyer.organisation = {
name: organisation,
};
}
return this.orderService
.OrderPatchOrder({
orderId: orderId,
order: {
notificationChannels,
buyer,
notificationChannels: changes.selected,
buyer: { communicationDetails },
},
})
.pipe(map((res) => res.result));
@@ -278,14 +241,11 @@ export class DomainOmsService {
map((res) =>
res.result
.sort((a, b) => new Date(b.completed).getTime() - new Date(a.completed).getTime())
.reduce(
(data, result) => {
(data[result.name] = data[result.name] || []).push(new Date(result.completed));
return data;
},
{} as Record<string, Date[]>,
),
),
.reduce((data, result) => {
(data[result.name] = data[result.name] || []).push(new Date(result.completed));
return data;
}, {} as Record<string, Date[]>)
)
);
}
}

View File

@@ -2,7 +2,6 @@
* Public API Surface of oms
*/
export * from './lib/action-handler-services';
export * from './lib/goods.service';
export * from './lib/receipt.service';
export * from './lib/oms.service';

View File

@@ -1,11 +0,0 @@
import { PackageArrivalStatusDTO } from '@swagger/wws';
export abstract class PackageInspectionEvent {
constructor(public readonly type: string) {}
}
export class PackageStatusChangedEvent extends PackageInspectionEvent {
constructor(public readonly packageId: string, public readonly status: PackageArrivalStatusDTO) {
super('PackageStatusChangedEvent');
}
}

View File

@@ -1,8 +1,10 @@
import { Injectable } from '@angular/core';
import {
ListResponseArgsOfPackageDTO,
ListResponseArgsOfPackageDTO2,
PackageArrivalStatusDTO,
PackageDetailResponseDTO,
PackageDTO,
PackageDTO2,
QuerySettingsDTO,
QueryTokenDTO,
@@ -11,18 +13,13 @@ import {
ResponseArgsOfQuerySettingsDTO,
WareneingangService,
} from '@swagger/wws';
import { Observable, Subject } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { PackageInspectionEvent, PackageStatusChangedEvent } from './events';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class DomainPackageInspectionService {
private _events = new Subject<PackageInspectionEvent>();
events = this._events.asObservable();
constructor(private _wareneingang: WareneingangService) {}
getQuerySettingsResponse(): Observable<ResponseArgsOfQuerySettingsDTO> {
@@ -50,26 +47,23 @@ export class DomainPackageInspectionService {
}
changePackageStatusResponse(pkg: PackageDTO2, modifier: string): Observable<ResponseArgsOfPackageArrivalStatusDTO> {
return this._wareneingang
.WareneingangChangePackageStatus({
packageId: pkg.id,
payload: {
id: pkg.id,
annotation: pkg.annotation,
area: pkg.area,
arrivalChecked: pkg.arrivalChecked,
arrivalStatus: pkg.arrivalStatus,
deliveryNoteNumber: pkg.deliveryNoteNumber,
deliveryTarget: pkg.deliveryTarget,
estimatedDeliveryDate: pkg.estimatedDeliveryDate,
packageNumber: pkg.packageNumber,
supplier: pkg.supplier,
trackingNumber: pkg.trackingNumber,
scanId: pkg.scanId,
},
modifier,
})
.pipe(tap((res) => this._events.next(new PackageStatusChangedEvent(pkg.id, res.result))));
return this._wareneingang.WareneingangChangePackageStatus({
packageId: pkg.id,
payload: {
id: pkg.id,
annotation: pkg.annotation,
area: pkg.area,
arrivalChecked: pkg.arrivalChecked,
arrivalStatus: pkg.arrivalStatus,
deliveryNoteNumber: pkg.deliveryNoteNumber,
deliveryTarget: pkg.deliveryTarget,
estimatedDeliveryDate: pkg.estimatedDeliveryDate,
packageNumber: pkg.packageNumber,
supplier: pkg.supplier,
trackingNumber: pkg.trackingNumber,
},
modifier,
});
}
changePackageStatus(pkg: PackageDTO2, modifier: string): Observable<PackageArrivalStatusDTO> {

View File

@@ -1,6 +1,6 @@
/*
* Public API Surface of package-inspection
*/
export * from './lib/events';
export * from './lib/package-inspection.service';
export * from './lib/package-inspection.module';

View File

@@ -1,60 +0,0 @@
import { Injectable, inject } from '@angular/core';
import { AbholfachService, AutocompleteTokenDTO, ListResponseArgsOfDBHOrderItemListItemDTO, QueryTokenDTO } from '@swagger/oms';
import { PickupShelfIOService } from './pickup-shelf-io.service';
import { Observable, throwError } from 'rxjs';
import { Filter } from '@shared/components/filter';
@Injectable({ providedIn: 'root' })
export class PickupShelfInService extends PickupShelfIOService {
private _abholfachService = inject(AbholfachService);
name() {
return 'PickupShelfInService';
}
getQuerySettings() {
return this._abholfachService.AbholfachWareneingangQuerySettings();
}
search(queryToken: QueryTokenDTO) {
return this._abholfachService.AbholfachWareneingang(queryToken);
}
complete(autocompleteToken: AutocompleteTokenDTO) {
return this._abholfachService.AbholfachWareneingangAutocomplete(autocompleteToken);
}
getOrderItemsByOrderNumberOrCompartmentCode(args: {
orderNumber?: string;
compartmentCode?: string;
filter?: Filter;
}): Observable<ListResponseArgsOfDBHOrderItemListItemDTO> {
if (!args.orderNumber && !args.compartmentCode) {
return throwError(
'PickupShelfInService.getOrderItemsByOrderNumberOrCompartmentCode(): Either orderNumber or compartmentCode must be provided.'
);
}
const { orderdate } = args.filter?.getQueryToken()?.filter ?? {};
return this._abholfachService.AbholfachWareneingang({
input: {
qs: args.compartmentCode ?? args.orderNumber,
},
filter: {
archive: String(true),
all_branches: String(true),
orderdate,
},
});
}
getOrderItemsByCustomerNumber(args: { customerNumber: string }): Observable<ListResponseArgsOfDBHOrderItemListItemDTO> {
return this._abholfachService.AbholfachWareneingang({
filter: { orderitemprocessingstatus: '16;128;8192;1048576' },
input: {
customer_name: args.customerNumber,
},
});
}
}

View File

@@ -1,29 +0,0 @@
import { Injectable } from '@angular/core';
import { Filter } from '@shared/components/filter';
import {
AutocompleteTokenDTO,
ListResponseArgsOfDBHOrderItemListItemDTO,
QueryTokenDTO,
ResponseArgsOfIEnumerableOfAutocompleteDTO,
ResponseArgsOfQuerySettingsDTO,
} from '@swagger/oms';
import { Observable } from 'rxjs';
@Injectable()
export abstract class PickupShelfIOService {
abstract name(): string;
abstract getQuerySettings(): Observable<ResponseArgsOfQuerySettingsDTO>;
abstract search(queryToken: QueryTokenDTO): Observable<ListResponseArgsOfDBHOrderItemListItemDTO>;
abstract complete(autocompleteToken: AutocompleteTokenDTO): Observable<ResponseArgsOfIEnumerableOfAutocompleteDTO>;
abstract getOrderItemsByOrderNumberOrCompartmentCode(args: {
orderNumber?: string;
compartmentCode?: string;
filter?: Filter;
}): Observable<ListResponseArgsOfDBHOrderItemListItemDTO>;
abstract getOrderItemsByCustomerNumber(args: { customerNumber: string }): Observable<ListResponseArgsOfDBHOrderItemListItemDTO>;
}

View File

@@ -1,56 +0,0 @@
import { Injectable, inject } from '@angular/core';
import { AbholfachService, AutocompleteTokenDTO, ListResponseArgsOfDBHOrderItemListItemDTO, QueryTokenDTO } from '@swagger/oms';
import { PickupShelfIOService } from './pickup-shelf-io.service';
import { Observable, throwError } from 'rxjs';
import { Filter } from '@shared/components/filter';
@Injectable({ providedIn: 'root' })
export class PickupShelfOutService extends PickupShelfIOService {
private _abholfachService = inject(AbholfachService);
name() {
return 'PickupShelfOutService';
}
getQuerySettings() {
return this._abholfachService.AbholfachWarenausgabeQuerySettings();
}
search(queryToken: QueryTokenDTO) {
return this._abholfachService.AbholfachWarenausgabe(queryToken);
}
complete(autocompleteToken: AutocompleteTokenDTO) {
return this._abholfachService.AbholfachWarenausgabeAutocomplete(autocompleteToken);
}
getOrderItemsByOrderNumberOrCompartmentCode(args: {
orderNumber?: string;
compartmentCode?: string;
filter?: Filter;
}): Observable<ListResponseArgsOfDBHOrderItemListItemDTO> {
if (!args.orderNumber && !args.compartmentCode) {
return throwError(
'PickupShelfOutService.getOrderItemsByOrderNumberOrCompartmentCode(): Either orderNumber or compartmentCode must be provided.'
);
}
const { orderdate, supplier_id } = args.filter?.getQueryToken()?.filter ?? {};
return this._abholfachService.AbholfachWarenausgabe({
input: {
qs: args.compartmentCode ?? args.orderNumber,
},
filter: {
archive: String(true),
all_branches: String(true),
orderdate,
supplier_id,
},
});
}
getOrderItemsByCustomerNumber(args: { customerNumber: string }): Observable<ListResponseArgsOfDBHOrderItemListItemDTO> {
throw new Error('Method not implemented.');
}
}

View File

@@ -1,27 +0,0 @@
import { Injectable, inject } from '@angular/core';
import { DBHOrderItemListItemDTO, OrderItemDTO, OrderItemSubsetDTO, OrderService } from '@swagger/oms';
@Injectable({ providedIn: 'root' })
export class PickupShelfService {
private _orderService = inject(OrderService);
getOrderByOrderId(orderId: number) {
return this._orderService.OrderGetOrder(orderId);
}
patchOrderItemSubset(item: DBHOrderItemListItemDTO, changes: Partial<OrderItemSubsetDTO>) {
return this._orderService.OrderPatchOrderItemSubset({
orderId: item.orderId,
orderItemId: item.orderItemId,
orderItemSubsetId: item.orderItemSubsetId,
orderItemSubset: changes,
});
}
getOrderItemSubsetTasks(item: DBHOrderItemListItemDTO) {
return this._orderService.OrderGetOrderItemSubsetTasks({
orderId: item.orderId,
orderItemId: item.orderItemId,
orderItemSubsetId: item.orderItemSubsetId,
});
}
}

View File

@@ -1,4 +0,0 @@
export * from './lib/pickup-shelf-in.service';
export * from './lib/pickup-shelf-io.service';
export * from './lib/pickup-shelf-out.service';
export * from './lib/pickup-shelf.service';

View File

@@ -447,7 +447,7 @@ export class DomainRemissionService {
* Create a new receipt for the given return/remission
* @param returnId Return ID
* @param receiptNumber Receipt number
* @returns ReceiptDTO
* @returns ReturnDTO - ShippingDocument
*/
async createReceipt(returnDTO: ReturnDTO, receiptNumber?: string): Promise<ReceiptDTO> {
const stock = await this._getStock();
@@ -471,41 +471,14 @@ export class DomainRemissionService {
return receipt;
}
/**
* Create a new Package and assign it to a receipt
* @param returnId Return ID
* @param receiptId Receipt ID
* @param packageNumber Packagenumber
* @returns ReceiptDTO
*/
async createReceiptAndAssignPackage({
returnId,
receiptId,
packageNumber,
}: {
returnId: number;
receiptId: number;
packageNumber: string;
}): Promise<ReceiptDTO> {
const response = await this._returnService
.ReturnCreateAndAssignPackage({
returnId,
receiptId,
data: {
packageNumber,
},
})
.toPromise();
const receipt: ReceiptDTO = response.result;
return receipt;
}
async completeReceipt(returnId: number, receiptId: number): Promise<ReceiptDTO> {
async completeReceipt(returnId: number, receiptId: number, packageCode: string): Promise<ReceiptDTO> {
const res = await this._returnService
.ReturnFinalizeReceipt({
returnId,
receiptId,
data: {},
data: {
packageCode,
},
})
.toPromise();

View File

@@ -1,5 +1,4 @@
// start:ng42.barrel
export * from './hub-notification.module';
export * from './notifications.hub';
export * from './defs';
// end:ng42.barrel

View File

@@ -4,118 +4,61 @@ import { SignalrHub, SignalRHubOptions } from '@core/signalr';
import { BehaviorSubject, merge, of } from 'rxjs';
import { filter, map, shareReplay, tap, withLatestFrom } from 'rxjs/operators';
import { EnvelopeDTO, MessageBoardItemDTO } from './defs';
import { cloneDeep } from 'lodash';
export const NOTIFICATIONS_HUB_OPTIONS = new InjectionToken<SignalRHubOptions>('hub.notifications.options');
@Injectable()
export class NotificationsHub extends SignalrHub {
updateNotification$ = new BehaviorSubject<MessageBoardItemDTO>(undefined);
get branchNo() {
return String(this._auth.getClaimByKey('branch_no') || this._auth.getClaimByKey('sub'));
}
// get sessionStoragesessionStorageKey() {
// return `NOTIFICATIONS_BOARD_${this.branchNo}`;
// }
get sessionStoragesessionStorageKey() {
return `NOTIFICATIONS_BOARD_AREA_${this.branchNo}`;
return `NOTIFICATIONS_BOARD_${this.branchNo}`;
}
messageBoardItems$ = new BehaviorSubject<Record<string, MessageBoardItemDTO[]>>({});
constructor(@Inject(NOTIFICATIONS_HUB_OPTIONS) options: SignalRHubOptions, private _auth: AuthService) {
super(options);
this.messageBoardItems$.next(this._getNotifications());
this.messageBoardItems$.subscribe((data) => {
this._storeNotifactions(data);
});
this.listen<EnvelopeDTO<MessageBoardItemDTO[]>>('messageBoard').subscribe((envelope) => {
if (envelope.action === 'refresh') {
this.refreshMessageBoardItems(envelope.target.area, envelope.data);
}
});
}
refreshMessageBoardItems(targetArea: string, messages: MessageBoardItemDTO[]) {
const current = cloneDeep(this.messageBoardItems$.value);
current[targetArea] = messages ?? [];
this.messageBoardItems$.next(current);
}
notifications$ = this.messageBoardItems$.asObservable().pipe(
map((data) => {
const messages = { ...data };
const keys = Object.keys(data);
for (let key of keys) {
if (data[key].length === 0 || data[key] === undefined) {
delete messages[key];
}
notifications$ = merge(
of(this._getNotifications()).pipe(filter((f) => !!f)),
this.listen<EnvelopeDTO<MessageBoardItemDTO[]>>('messageBoard')
).pipe(
withLatestFrom(this.updateNotification$),
map(([d, update]) => {
const data = d;
if (update && !!data && !data?.data?.find((message) => message?.category === 'ISA-Update')) {
data.data.push(update);
}
return messages;
})
return data;
}),
tap((data) => this._storeNotifactions(data)),
shareReplay(1)
);
// notifications$ = merge(
// of(this._getNotifications()).pipe(filter((f) => !!f)),
// this.listen<EnvelopeDTO<MessageBoardItemDTO[]>>('messageBoard')
// ).pipe(
// withLatestFrom(this.updateNotification$),
// map(([d, update]) => {
// console.log('notifications$', d, update);
// const data = d;
// if (update && !!data && !data?.data?.find((message) => message?.category === 'ISA-Update')) {
// data.data.push(update);
// }
// return data;
// }),
// tap((data) => this._storeNotifactions(data)),
// shareReplay(1)
// );
// private _storeNotifactions(data: EnvelopeDTO<MessageBoardItemDTO[]>) {
// if (data) {
// sessionStorage.setItem(this.sessionStoragesessionStorageKey, JSON.stringify(data));
// }
// }
// private _getNotifications(): EnvelopeDTO<MessageBoardItemDTO[]> {
// const stringData = sessionStorage.getItem(this.sessionStoragesessionStorageKey);
// if (stringData) {
// return JSON.parse(stringData);
// }
// return undefined;
// }
private _getNotifications(): Record<string, MessageBoardItemDTO[]> {
const stringData = sessionStorage.getItem(this.sessionStoragesessionStorageKey);
if (stringData) {
return JSON.parse(stringData);
}
return {};
}
private _storeNotifactions(data: Record<string, MessageBoardItemDTO[]>) {
private _storeNotifactions(data: EnvelopeDTO<MessageBoardItemDTO[]>) {
if (data) {
delete data['messageBoard/isa-update'];
sessionStorage.setItem(this.sessionStoragesessionStorageKey, JSON.stringify(data));
}
}
private _getNotifications(): EnvelopeDTO<MessageBoardItemDTO[]> {
const stringData = sessionStorage.getItem(this.sessionStoragesessionStorageKey);
if (stringData) {
return JSON.parse(stringData);
}
return undefined;
}
updateNotification() {
this.refreshMessageBoardItems('messageBoard/isa-update', [
{
category: 'ISA-Update',
type: 'update',
headline: 'Update Benachrichtigung',
text: 'Es steht eine aktuellere Version der ISA bereit. Bitte aktualisieren Sie die Anwendung.',
},
]);
this.updateNotification$.next({
category: 'ISA-Update',
type: 'update',
headline: 'Update Benachrichtigung',
text: 'Es steht eine aktuellere Version der ISA bereit. Bitte aktualisieren Sie die Anwendung.',
});
}
}

View File

@@ -1,13 +1,14 @@
import { isDevMode, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DebugComponent } from './debug/debug.component';
import {
CanActivateCartGuard,
CanActivateCartWithProcessIdGuard,
CanActivateCustomerGuard,
CanActivateCustomerOrdersGuard,
CanActivateCustomerOrdersWithProcessIdGuard,
CanActivateCustomerWithProcessIdGuard,
CanActivateGoodsInGuard,
CanActivateGoodsOutGuard,
CanActivateGoodsOutWithProcessIdGuard,
CanActivateProductGuard,
CanActivateProductWithProcessIdGuard,
CanActivateRemissionGuard,
@@ -16,12 +17,10 @@ import {
} from './guards';
import { CanActivateAssortmentGuard } from './guards/can-activate-assortment.guard';
import { CanActivatePackageInspectionGuard } from './guards/can-activate-package-inspection.guard';
import { MainComponent } from './main.component';
import { PreviewComponent } from './preview';
import { BranchSectionResolver, CustomerSectionResolver, ProcessIdResolver } from './resolvers';
import { ShellComponent, ShellModule } from './shell';
import { TokenLoginComponent, TokenLoginModule } from './token-login';
import { ProcessIdGuard } from './guards/process-id.guard';
import { ActivateProcessIdGuard, ActivateProcessIdWithConfigKeyGuard } from './guards/activate-process-id.guard';
const routes: Routes = [
{
@@ -36,111 +35,111 @@ const routes: Routes = [
canActivate: [IsAuthenticatedGuard],
children: [
{
path: 'kunde',
component: MainComponent,
path: '',
canActivate: [],
children: [
{
path: 'dashboard',
loadChildren: () => import('@page/dashboard').then((m) => m.DashboardModule),
path: 'kunde',
component: ShellComponent,
children: [
{
path: 'dashboard',
loadChildren: () => import('@page/dashboard').then((m) => m.DashboardModule),
},
{
path: 'product',
loadChildren: () => import('@page/catalog').then((m) => m.PageCatalogModule),
canActivate: [CanActivateProductGuard],
},
{
path: ':processId/product',
loadChildren: () => import('@page/catalog').then((m) => m.PageCatalogModule),
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),
canActivate: [CanActivateCustomerGuard],
},
{
path: ':processId/customer',
loadChildren: () => import('@page/customer').then((m) => m.PageCustomerModule),
canActivate: [CanActivateCustomerWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'cart',
loadChildren: () => import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartGuard],
},
{
path: ':processId/cart',
loadChildren: () => import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartWithProcessIdGuard],
},
{
path: 'goods/out',
loadChildren: () => import('@page/goods-out').then((m) => m.GoodsOutModule),
canActivate: [CanActivateGoodsOutGuard],
},
{
path: ':processId/goods/out',
loadChildren: () => import('@page/goods-out').then((m) => m.GoodsOutModule),
canActivate: [CanActivateGoodsOutWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{ path: '**', redirectTo: 'dashboard', pathMatch: 'full' },
],
resolve: { section: CustomerSectionResolver },
},
{
path: 'product',
loadChildren: () => import('@page/catalog').then((m) => m.PageCatalogModule),
canActivate: [CanActivateProductGuard],
path: 'filiale',
component: ShellComponent,
children: [
{
path: 'task-calendar',
loadChildren: () => import('@page/task-calendar').then((m) => m.PageTaskCalendarModule),
canActivate: [CanActivateTaskCalendarGuard],
},
{
path: 'goods/in',
loadChildren: () => import('@page/goods-in').then((m) => m.GoodsInModule),
canActivate: [CanActivateGoodsInGuard],
},
{
path: 'remission',
loadChildren: () => import('@page/remission').then((m) => m.PageRemissionModule),
canActivate: [CanActivateRemissionGuard],
},
{
path: 'package-inspection',
loadChildren: () => import('@page/package-inspection').then((m) => m.PackageInspectionModule),
canActivate: [CanActivatePackageInspectionGuard],
},
{
path: 'assortment',
loadChildren: () => import('@page/assortment').then((m) => m.AssortmentModule),
canActivate: [CanActivateAssortmentGuard],
},
{ path: '**', redirectTo: 'task-calendar', pathMatch: 'full' },
],
resolve: { section: BranchSectionResolver },
},
{
path: ':processId/product',
loadChildren: () => import('@page/catalog').then((m) => m.PageCatalogModule),
canActivate: [CanActivateProductWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'order',
loadChildren: () => import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersGuard],
},
{
path: ':processId/order',
loadChildren: () => import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'customer',
loadChildren: () => import('@page/customer').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerGuard],
},
{
path: ':processId/customer',
loadChildren: () => import('@page/customer').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'cart',
loadChildren: () => import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartGuard],
},
{
path: ':processId/cart',
loadChildren: () => import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartWithProcessIdGuard],
},
{
path: 'pickup-shelf',
canActivate: [ProcessIdGuard],
// NOTE: This is a workaround for the canActivate guard not being called
loadChildren: () => import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
},
{
path: ':processId/pickup-shelf',
canActivate: [ActivateProcessIdGuard],
loadChildren: () => import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
},
{ path: '**', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: '**', redirectTo: 'kunde', pathMatch: 'full' },
],
resolve: { section: CustomerSectionResolver },
},
{
path: 'filiale',
component: MainComponent,
children: [
{
path: 'task-calendar',
loadChildren: () => import('@page/task-calendar').then((m) => m.PageTaskCalendarModule),
canActivate: [CanActivateTaskCalendarGuard],
},
{
path: 'pickup-shelf',
canActivate: [ActivateProcessIdWithConfigKeyGuard('pickupShelf')],
// NOTE: This is a workaround for the canActivate guard not being called
loadChildren: () => import('@page/pickup-shelf').then((m) => m.PickupShelfInModule),
},
{
path: 'goods/in',
loadChildren: () => import('@page/goods-in').then((m) => m.GoodsInModule),
canActivate: [CanActivateGoodsInGuard],
},
{
path: 'remission',
loadChildren: () => import('@page/remission').then((m) => m.PageRemissionModule),
canActivate: [CanActivateRemissionGuard],
},
{
path: 'package-inspection',
loadChildren: () => import('@page/package-inspection').then((m) => m.PackageInspectionModule),
canActivate: [CanActivatePackageInspectionGuard],
},
{
path: 'assortment',
loadChildren: () => import('@page/assortment').then((m) => m.AssortmentModule),
canActivate: [CanActivateAssortmentGuard],
},
{ path: '**', redirectTo: 'task-calendar', pathMatch: 'full' },
],
resolve: { section: BranchSectionResolver },
},
{ path: '**', redirectTo: 'kunde', pathMatch: 'full' },
],
},
];
@@ -153,7 +152,7 @@ if (isDevMode()) {
}
@NgModule({
imports: [RouterModule.forRoot(routes), TokenLoginModule],
imports: [RouterModule.forRoot(routes), ShellModule, TokenLoginModule],
exports: [RouterModule],
})
export class AppRoutingModule {}

View File

@@ -28,7 +28,7 @@ export const metaReducers: MetaReducer<RootState>[] = !environment.production ?
imports: [
StoreModule.forRoot(rootReducer, { metaReducers }),
EffectsModule.forRoot([]),
StoreDevtoolsModule.instrument({ name: 'ISA Ngrx Application Store', connectInZone: true }),
StoreDevtoolsModule.instrument({ name: 'ISA Ngrx Application Store' }),
],
})
export class AppStoreModule {}

View File

@@ -1,28 +1 @@
@if ($offlineBannerVisible()) {
<div [@fadeInOut] class="bg-brand text-white text-center fixed inset-x-0 top-0 z-tooltip p-4">
<h3 class="font-bold grid grid-flow-col items-center justify-center text-xl gap-4">
<div>
<ng-icon name="matWifiOff"></ng-icon>
</div>
<div>Sie sind offline, keine Verbindung zum Netzwerk.</div>
</h3>
<p>Bereits geladene Ihnalte werden angezeigt, Interaktionen sind aktuell nicht möglich.</p>
</div>
}
@if ($onlineBannerVisible()) {
<div [@fadeInOut] class="bg-green-500 text-white text-center fixed inset-x-0 top-0 z-tooltip p-4">
<h3 class="font-bold grid grid-flow-col items-center justify-center text-xl gap-4">
<div>
<ng-icon name="matWifi"></ng-icon>
</div>
<div>Sie sind wieder online.</div>
</h3>
<button class="fixed top-2 right-4 text-3xl w-12 h-12" type="button" (click)="$onlineBannerVisible.set(false)">
<ng-icon name="matClose"></ng-icon>
</button>
</div>
}
<router-outlet></router-outlet>

View File

@@ -1,3 +1,7 @@
:host {
@apply block;
@apply block box-border;
}
button {
@apply fixed bottom-4 right-2 bg-blue-500 text-white font-bold py-2 px-4 rounded z-tooltip;
}

View File

@@ -1,71 +1,30 @@
import { DOCUMENT } from '@angular/common';
import { Component, effect, HostListener, Inject, OnInit, Renderer2, signal, untracked } from '@angular/core';
import { Component, HostListener, Inject, OnInit, Renderer2 } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { SwUpdate } from '@angular/service-worker';
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 { asapScheduler, interval, Subscription } from 'rxjs';
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 { injectOnline$ } from './services/network-status.service';
import { toSignal } from '@angular/core/rxjs-interop';
import { animate, style, transition, trigger } from '@angular/animations';
import { tap } from 'rxjs/operators';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
animations: [
trigger('fadeInOut', [
transition(':enter', [
// :enter wird ausgelöst, wenn das Element zum DOM hinzugefügt wird
style({ opacity: 0, transform: 'translateY(-100%)' }),
animate('300ms', style({ opacity: 1, transform: 'translateY(0)' })),
]),
transition(':leave', [
// :leave wird ausgelöst, wenn das Element aus dem DOM entfernt wird
animate('300ms', style({ opacity: 0, transform: 'translateY(-100%)' })),
]),
]),
],
})
export class AppComponent implements OnInit {
$online = toSignal(injectOnline$());
$offlineBannerVisible = signal(false);
$onlineBannerVisible = signal(false);
private onlineBannerDismissTimeout: any;
onlineEffects = effect(() => {
const online = this.$online();
const offlineBannerVisible = this.$offlineBannerVisible();
untracked(() => {
this.$offlineBannerVisible.set(!online);
if (!online) {
this.$onlineBannerVisible.set(false);
clearTimeout(this.onlineBannerDismissTimeout);
}
if (offlineBannerVisible && online) {
this.$onlineBannerVisible.set(true);
this.onlineBannerDismissTimeout = setTimeout(() => this.$onlineBannerVisible.set(false), 5000);
}
});
});
private _checkForUpdates: number = this._config.get('checkForUpdates');
updateAvailableObs: Observable<UpdateAvailableEvent>;
get checkForUpdates(): number {
return this._checkForUpdates ?? 60 * 60 * 1000; // default 1 hour
return this._checkForUpdates;
}
// For Unit Testing
@@ -86,7 +45,7 @@ export class AppComponent implements OnInit {
private infoService: UserStateService,
private readonly _environment: EnvironmentService,
private readonly _authService: AuthService,
private readonly _modal: UiModalService,
private readonly _modal: UiModalService
) {
this.updateClient();
IsaLogProvider.InfoService = this.infoService;
@@ -157,7 +116,6 @@ export class AppComponent implements OnInit {
checkForUpdate() {
interval(this._checkForUpdates).subscribe(() => {
this._swUpdate.checkForUpdate().then((value) => {
console.log('check for update', value);
if (value) {
this._notifications.updateNotification();
}
@@ -167,7 +125,6 @@ export class AppComponent implements OnInit {
initialCheckForUpdate() {
this._swUpdate.checkForUpdate().then((value) => {
console.log('initial check for update', value);
if (value) {
location.reload();
}

View File

@@ -32,15 +32,9 @@ import { IsaErrorHandler } from './providers/isa.error-handler';
import { ScanAdapterModule, ScanAdapterService, ScanditScanAdapterModule } from '@adapter/scan';
import { RootStateService } from './store/root-state.service';
import * as Commands from './commands';
import { UiIconModule } from '@ui/icon';
import { PreviewComponent } from './preview';
import { NativeContainerService } from 'native-container';
import { ShellModule } from '@shared/shell';
import { MainComponent } from './main.component';
import { IconModule } from '@shared/components/icon';
import { NgIconsModule } from '@ng-icons/core';
import { matClose, matWifi, matWifiOff } from '@ng-icons/material-icons/baseline';
import { NetworkStatusService } from './services/network-status.service';
import { firstValueFrom } from 'rxjs';
registerLocaleData(localeDe, localeDeExtra);
registerLocaleData(localeDe, 'de', localeDeExtra);
@@ -50,68 +44,26 @@ export function _appInitializerFactory(
auth: AuthService,
injector: Injector,
scanAdapter: ScanAdapterService,
nativeContainer: NativeContainerService,
networkStatus: NetworkStatusService,
nativeContainer: NativeContainerService
) {
return async () => {
const statusElement = document.querySelector('#init-status');
const laoderElement = document.querySelector('#init-loader');
statusElement.innerHTML = 'Konfigurationen werden geladen...';
await config.init();
statusElement.innerHTML = 'Authentifizierung wird geprüft...';
await auth.init();
try {
let online = false;
while (!online) {
online = await firstValueFrom(networkStatus.online$);
if (!online) {
statusElement.innerHTML =
'<b>Warte auf Netzwerkverbindung (WLAN)</b><br><br>Bitte prüfen Sie die Netzwerkverbindung (WLAN).<br>Sobald eine Netzwerkverbindung besteht, wird die App automatisch neu geladen.';
await new Promise((resolve) => setTimeout(resolve, 250));
}
}
statusElement.innerHTML = 'Konfigurationen werden geladen...';
await config.init();
statusElement.innerHTML = 'Authentifizierung wird geprüft...';
await auth.init();
if (auth.isAuthenticated()) {
statusElement.innerHTML = 'App wird initialisiert...';
const state = injector.get(RootStateService);
await state.init();
}
statusElement.innerHTML = 'Native Container wird initialisiert...';
await nativeContainer.init();
statusElement.innerHTML = 'Scanner wird initialisiert...';
await scanAdapter.init();
} catch (error) {
laoderElement.remove();
statusElement.classList.add('text-xl');
statusElement.innerHTML = '<b>Fehler bei der Initialisierung</b><br><br>Bitte prüfen Sie die Netzwerkverbindung (WLAN).<br><br>';
const reload = document.createElement('button');
reload.classList.add('bg-brand', 'text-white', 'p-2', 'rounded', 'cursor-pointer');
reload.innerHTML = 'App neu laden';
reload.onclick = () => window.location.reload();
statusElement.appendChild(reload);
const preLabel = document.createElement('div');
preLabel.classList.add('mt-12');
preLabel.innerHTML = 'Fehlermeldung:';
statusElement.appendChild(preLabel);
const pre = document.createElement('pre');
pre.classList.add('mt-4', 'text-wrap');
pre.innerHTML = error.message;
statusElement.appendChild(pre);
console.error('Error during app initialization', error);
throw error;
if (auth.isAuthenticated()) {
statusElement.innerHTML = 'App wird initialisiert...';
const state = injector.get(RootStateService);
await state.init();
}
statusElement.innerHTML = 'Native Container wird initialisiert...';
await nativeContainer.init();
statusElement.innerHTML = 'Scanner wird initialisiert...';
await scanAdapter.init();
};
}
@@ -122,12 +74,11 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
}
@NgModule({
declarations: [AppComponent, MainComponent],
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
ShellModule.forRoot(),
AppRoutingModule,
AppSwaggerModule,
AppDomainModule,
@@ -152,15 +103,38 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
ScanAdapterModule.forRoot(),
ScanditScanAdapterModule.forRoot(),
PlatformModule,
IconModule.forRoot(),
NgIconsModule.withIcons({ matWifiOff, matClose, matWifi }),
UiIconModule.forRoot({
aliases: [
{ alias: 'd-account', name: 'account' },
{ alias: 'd-no-account', name: 'package-variant-closed' },
{ name: 'isa-audio', alias: 'AU' },
{ name: 'isa-audio', alias: 'CAS' },
{ name: 'isa-audio', alias: 'DL' },
{ name: 'isa-audio', alias: 'KAS' },
{ name: 'isa-hard-cover', alias: 'BUCH' },
{ name: 'isa-hard-cover', alias: 'GEB' },
{ name: 'isa-hard-cover', alias: 'HC' },
{ name: 'isa-hard-cover', alias: 'KT' },
{ name: 'isa-ebook', alias: 'EB' },
{ name: 'isa-non-book', alias: 'GLO' },
{ name: 'isa-non-book', alias: 'HDL' },
{ name: 'isa-non-book', alias: 'NB' },
{ name: 'isa-non-book', alias: 'SPL' },
{ name: 'isa-calendar', alias: 'KA' },
{ name: 'isa-scroll', alias: 'MA' },
{ name: 'isa-software', alias: 'SW' },
{ name: 'isa-soft-cover', alias: 'TB' },
{ name: 'isa-video', alias: 'VI' },
{ name: 'isa-news-paper', alias: 'ZS' },
],
}),
],
providers: [
{
provide: APP_INITIALIZER,
useFactory: _appInitializerFactory,
multi: true,
deps: [Config, AuthService, Injector, ScanAdapterService, NativeContainerService, NetworkStatusService],
deps: [Config, AuthService, Injector, ScanAdapterService, NativeContainerService],
},
{
provide: NOTIFICATIONS_HUB_OPTIONS,

View File

@@ -1,46 +0,0 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router';
import { ApplicationService } from '@core/application';
import { Config } from '@core/config';
import { take } from 'rxjs/operators';
export const ActivateProcessIdGuard: CanActivateFn = async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const application = inject(ApplicationService);
const processIdStr = route.params.processId;
if (!processIdStr) {
return false;
}
const processId = Number(processIdStr);
// Check if Process already exists
const process = await application.getProcessById$(processId).pipe(take(1)).toPromise();
if (!process) {
application.createCustomerProcess(processId);
}
application.activateProcess(processId);
return true;
};
export const ActivateProcessIdWithConfigKeyGuard: (key: string) => CanActivateFn = (key) => async (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) => {
const application = inject(ApplicationService);
const config = inject(Config);
const processId = config.get(`process.ids.${key}`);
if (isNaN(processId)) {
return false;
}
application.activateProcess(processId);
return true;
};

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
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 {
export class CanActivateAssortmentGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService, private readonly _config: Config) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

View File

@@ -1,10 +1,10 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateCartWithProcessIdGuard {
export class CanActivateCartWithProcessIdGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

View File

@@ -1,12 +1,11 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationService } from '@core/application';
import { CheckoutNavigationService } from '@shared/services';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateCartGuard {
constructor(private readonly _applicationService: ApplicationService, private _checkoutNavigationService: CheckoutNavigationService) {}
export class CanActivateCartGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService, private readonly _router: Router) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
@@ -22,7 +21,7 @@ export class CanActivateCartGuard {
name: `Vorgang ${processes.length + 1}`,
});
}
await this._checkoutNavigationService.getCheckoutReviewPath(lastActivatedProcessId).path;
await this._router.navigate(['/kunde', lastActivatedProcessId, 'cart']);
return false;
}
}

View File

@@ -1,64 +0,0 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateCustomerOrdersWithProcessIdGuard {
constructor(private readonly _applicationService: ApplicationService, private readonly _breadcrumbService: BreadcrumbService) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const process = await this._applicationService
.getProcessById$(+route.params.processId)
.pipe(first())
.toPromise();
if (!process) {
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
await this._applicationService.createProcess({
id: +route.params.processId,
type: 'cart',
section: 'customer',
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
});
}
await this.removeBreadcrumbWithSameProcessId(route);
this._applicationService.activateProcess(+route.params.processId);
return true;
}
// Fix #3292: Alle Breadcrumbs die nichts mit dem aktuellen Prozess zu tun haben, müssen removed werden
async removeBreadcrumbWithSameProcessId(route: ActivatedRouteSnapshot) {
const crumbs = await this._breadcrumbService
.getBreadcrumbByKey$(+route.params.processId)
.pipe(first())
.toPromise();
// Entferne alle Crumbs die nichts mit den Kundenbestellungen zu tun haben
if (crumbs.length > 1) {
const crumbsToRemove = crumbs.filter((crumb) => crumb.tags.find((tag) => tag === 'customer-order') === undefined);
for (const crumb of crumbsToRemove) {
await this._breadcrumbService.removeBreadcrumb(crumb.id);
}
}
}
processNumber(processes: ApplicationProcess[]) {
const processNumbers = processes?.map((process) => Number(process?.name?.replace(/\D/g, '')));
return !!processNumbers && processNumbers.length > 0 ? this.findMissingNumber(processNumbers) : 1;
}
findMissingNumber(processNumbers: number[]) {
// Ticket #3272 Bei Klick auf "+" bzw. neuen Prozess hinzufügen soll der neue Tab immer die höchste Nummer haben (wie aktuell im Produktiv)
// ----------------------------------------------------------------------------------------------------------------------------------------
// for (let missingNumber = 1; missingNumber < Math.max(...processNumbers); missingNumber++) {
// if (!processNumbers.find((number) => number === missingNumber)) {
// return missingNumber;
// }
// }
return Math.max(...processNumbers) + 1;
}
}

View File

@@ -1,97 +0,0 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { CustomerOrdersNavigationService } from '@shared/services';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateCustomerOrdersGuard {
constructor(
private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService,
private readonly _navigationService: CustomerOrdersNavigationService
) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
let lastActivatedProcessId = (
await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'cart').pipe(first()).toPromise()
)?.id;
const lastActivatedCartCheckoutProcessId = (
await this._applicationService.getLastActivatedProcessWithSectionAndType$('customer', 'cart-checkout').pipe(first()).toPromise()
)?.id;
const activatedProcessId = await this._applicationService.getActivatedProcessId$().pipe(first()).toPromise();
// Darf nur reinkommen wenn der aktuell aktive Tab ein Bestellabschluss Tab ist
if (!!lastActivatedCartCheckoutProcessId && lastActivatedCartCheckoutProcessId === activatedProcessId) {
await this.fromCartCheckoutProcess(processes, route, lastActivatedCartCheckoutProcessId);
return false;
}
if (!lastActivatedProcessId) {
await this.fromGoodsOutProcess(processes, route);
return false;
} else {
await this._navigationService.getCustomerOrdersBasePath(lastActivatedProcessId).navigate();
}
return false;
}
// Bei offenen Kundenbestellungen und Klick auf Kundenbestellungen
async fromGoodsOutProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot) {
const newProcessId = Date.now();
await this._applicationService.createProcess({
id: newProcessId,
type: 'cart',
section: 'customer',
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
});
await this._navigationService.getCustomerOrdersBasePath(newProcessId).navigate();
}
// Bei offener Bestellbestätigung und Klick auf Kundenbestellungen
async fromCartCheckoutProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot, processId: number) {
// Um alle Checkout Daten zu resetten die mit dem Prozess assoziiert sind
this._checkoutService.removeProcess({ processId });
// Ändere type cart-checkout zu customer-order
this._applicationService.patchProcess(processId, {
id: processId,
type: 'cart',
section: 'customer',
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
data: {},
});
// Navigation
await this._navigationService.getCustomerOrdersBasePath(processId).navigate();
}
getUrlFromSnapshot(route: ActivatedRouteSnapshot, url: string[] = []): string[] {
url.push(...route.url.map((segment) => segment.path));
if (route.firstChild) {
return this.getUrlFromSnapshot(route.firstChild, url);
}
return url.filter((segment) => !!segment);
}
processNumber(processes: ApplicationProcess[]) {
const processNumbers = processes?.map((process) => Number(process?.name?.replace(/\D/g, '')));
return !!processNumbers && processNumbers.length > 0 ? this.findMissingNumber(processNumbers) : 1;
}
findMissingNumber(processNumbers: number[]) {
for (let missingNumber = 1; missingNumber < Math.max(...processNumbers); missingNumber++) {
if (!processNumbers.find((number) => number === missingNumber)) {
return missingNumber;
}
}
return Math.max(...processNumbers) + 1;
}
}

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateCustomerWithProcessIdGuard {
export class CanActivateCustomerWithProcessIdGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService, private readonly _breadcrumbService: BreadcrumbService) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

View File

@@ -1,17 +1,15 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { CustomerSearchNavigation } from '@shared/services';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateCustomerGuard {
export class CanActivateCustomerGuard implements CanActivate {
constructor(
private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService,
private readonly _router: Router,
private readonly _navigation: CustomerSearchNavigation
private readonly _router: Router
) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
@@ -43,17 +41,11 @@ export class CanActivateCustomerGuard {
await this.fromCartProcess(processes);
return false;
} else {
await this.navigateToDefaultRoute(lastActivatedProcessId);
await this._router.navigate(['/kunde', String(lastActivatedProcessId), 'customer']);
}
return false;
}
async navigateToDefaultRoute(processId: number) {
const route = this._navigation.defaultRoute({ processId });
await this._router.navigate(route.path, { queryParams: route.queryParams });
}
// Bei offener Artikelsuche/Kundensuche und Klick auf Footer Kundensuche
async fromCartProcess(processes: ApplicationProcess[]) {
const newProcessId = Date.now();
@@ -64,7 +56,7 @@ export class CanActivateCustomerGuard {
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
});
await this.navigateToDefaultRoute(newProcessId);
await this._router.navigate(['/kunde', String(newProcessId), 'customer']);
}
// Bei offener Bestellbestätigung und Klick auf Footer Kundensuche
@@ -82,7 +74,7 @@ export class CanActivateCustomerGuard {
});
// Navigation
await this.navigateToDefaultRoute(processId);
await this._router.navigate(['/kunde', String(processId), 'customer']);
}
// Bei offener Warenausgabe und Klick auf Footer Kundensuche
@@ -106,7 +98,7 @@ export class CanActivateCustomerGuard {
});
// Navigation
await this.navigateToDefaultRoute(processId);
await this._router.navigate(['/kunde', String(processId), 'customer']);
}
processNumber(processes: ApplicationProcess[]) {

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
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 CanActivateGoodsInGuard {
export class CanActivateGoodsInGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService, private readonly _config: Config) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
@@ -15,7 +15,7 @@ export class CanActivateGoodsInGuard {
id: this._config.get('process.ids.goodsIn'),
type: 'goods-in',
section: 'branch',
name: '',
name: 'Abholfach',
});
}
this._applicationService.activateProcess(this._config.get('process.ids.goodsIn'));

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateGoodsOutWithProcessIdGuard {
export class CanActivateGoodsOutWithProcessIdGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService, private readonly _breadcrumbService: BreadcrumbService) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateGoodsOutGuard {
export class CanActivateGoodsOutGuard implements CanActivate {
constructor(
private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService,

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
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 CanActivatePackageInspectionGuard {
export class CanActivatePackageInspectionGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService, private readonly _config: Config) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateProductWithProcessIdGuard {
export class CanActivateProductWithProcessIdGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService, private readonly _breadcrumbService: BreadcrumbService) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

View File

@@ -1,16 +1,21 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { DomainCheckoutService } from '@domain/checkout';
import { ProductCatalogNavigationService } from '@shared/services';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateProductGuard {
export class CanActivateProductGuard implements CanActivate {
get isTablet() {
return this._environment.isTablet();
}
constructor(
private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService,
private readonly _navigationService: ProductCatalogNavigationService
private readonly _environment: EnvironmentService,
private readonly _router: Router
) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
@@ -39,17 +44,17 @@ export class CanActivateProductGuard {
}
if (!lastActivatedProcessId) {
await this.fromCartProcess(processes);
await this.fromCartProcess(processes, route);
return false;
} else {
await this._navigationService.getArticleSearchBasePath(lastActivatedProcessId).navigate();
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(lastActivatedProcessId)]));
}
return false;
}
// Bei offener Artikelsuche/Kundensuche und Klick auf Footer Artikelsuche
async fromCartProcess(processes: ApplicationProcess[]) {
async fromCartProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot) {
const newProcessId = Date.now();
await this._applicationService.createProcess({
id: newProcessId,
@@ -58,7 +63,17 @@ export class CanActivateProductGuard {
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
});
await this._navigationService.getArticleSearchBasePath(newProcessId).navigate();
if (this.isTablet) {
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(newProcessId)]));
} else {
// TODO: Sollte auch von getUrlFromSnapshot kommen
await this._router.navigate([
'/kunde',
String(newProcessId),
'product',
{ outlets: { main: null, left: 'search', right: 'filter' } },
]);
}
}
// Bei offener Warenausgabe und Klick auf Footer Artikelsuche
@@ -82,7 +97,7 @@ export class CanActivateProductGuard {
});
// Navigation
await this._navigationService.getArticleSearchBasePath(processId).navigate();
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(processId)]));
}
// Bei offener Bestellbestätigung und Klick auf Footer Artikelsuche
@@ -100,7 +115,7 @@ export class CanActivateProductGuard {
});
// Navigation
await this._navigationService.getArticleSearchBasePath(processId).navigate();
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(processId)]));
}
getUrlFromSnapshot(route: ActivatedRouteSnapshot, url: string[] = []): string[] {

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationService } from '@core/application';
import { Config } from '@core/config';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateRemissionGuard {
export class CanActivateRemissionGuard implements CanActivate {
constructor(
private readonly _applicationService: ApplicationService,
private readonly _config: Config,

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
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 CanActivateTaskCalendarGuard {
export class CanActivateTaskCalendarGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService, private readonly _config: Config) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

View File

@@ -5,8 +5,6 @@ export * from './can-activate-customer.guard';
export * from './can-activate-goods-in.guard';
export * from './can-activate-goods-out-with-process-id.guard';
export * from './can-activate-goods-out.guard';
export * from './can-activate-customer-orders.guard';
export * from './can-activate-customer-orders-with-process-id.guard';
export * from './can-activate-product-with-process-id.guard';
export * from './can-activate-product.guard';
export * from './can-activate-remission.guard';

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '@core/auth';
import { ScanAdapterService } from '@adapter/scan';
import { AuthService as IsaAuthService } from '@swagger/isa';
@@ -7,14 +7,13 @@ import { UiConfirmModalComponent, UiErrorModalComponent, UiModalService } from '
import { EnvironmentService } from '@core/environment';
@Injectable({ providedIn: 'root' })
export class IsAuthenticatedGuard {
export class IsAuthenticatedGuard implements CanActivate {
constructor(
private _router: Router,
private _authService: AuthService,
private _scanService: ScanAdapterService,
private _isaAuthService: IsaAuthService,
private _modal: UiModalService,
private _environmentService: EnvironmentService,
private _environmentService: EnvironmentService
) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
@@ -53,7 +52,11 @@ export class IsAuthenticatedGuard {
return undefined;
}
const result = await this._scanService.scan()?.toPromise();
const result = await this._scanService
.scan({
exclude: ['Dev'],
})
?.toPromise();
if (typeof result === 'string') {
try {

Some files were not shown because too many files have changed in this diff Show More