mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
17 Commits
feature/52
...
hotfix/542
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
819a7bbe53 | ||
|
|
353864e2f0 | ||
|
|
1b6b726036 | ||
|
|
4c56f394c5 | ||
|
|
a086111ab5 | ||
|
|
15a4718e58 | ||
|
|
40592b4477 | ||
|
|
d430f544f0 | ||
|
|
62e586cfda | ||
|
|
304f8a64e5 | ||
|
|
c672ae4012 | ||
|
|
fd693a4beb | ||
|
|
2c70339f23 | ||
|
|
59f0cc7d43 | ||
|
|
0ca58fe1bf | ||
|
|
8cf80a60a0 | ||
|
|
2cb1f9ec99 |
@@ -153,12 +153,12 @@ const routes: Routes = [
|
||||
import('@page/goods-in').then((m) => m.GoodsInModule),
|
||||
canActivate: [CanActivateGoodsInGuard],
|
||||
},
|
||||
{
|
||||
path: 'remission',
|
||||
loadChildren: () =>
|
||||
import('@page/remission').then((m) => m.PageRemissionModule),
|
||||
canActivate: [CanActivateRemissionGuard],
|
||||
},
|
||||
// {
|
||||
// path: 'remission',
|
||||
// loadChildren: () =>
|
||||
// import('@page/remission').then((m) => m.PageRemissionModule),
|
||||
// canActivate: [CanActivateRemissionGuard],
|
||||
// },
|
||||
{
|
||||
path: 'package-inspection',
|
||||
loadChildren: () =>
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
<div class="notification-list scroll-bar">
|
||||
@for (notification of notifications; track notification) {
|
||||
<modal-notifications-list-item [item]="notification" (itemSelected)="itemSelected($event)"></modal-notifications-list-item>
|
||||
<modal-notifications-list-item
|
||||
[item]="notification"
|
||||
(itemSelected)="itemSelected($event)"
|
||||
></modal-notifications-list-item>
|
||||
<hr />
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a class="cta-primary" [routerLink]="['/filiale/remission/create']" (click)="navigated.emit()">Zur Remission</a>
|
||||
<a
|
||||
class="cta-primary"
|
||||
[routerLink]="remissionPath()"
|
||||
(click)="navigated.emit()"
|
||||
>Zur Remission</a
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, inject } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
inject,
|
||||
linkedSignal,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { PickupShelfInNavigationService } from '@shared/services/navigation';
|
||||
import { UiFilter } from '@ui/filter';
|
||||
import { MessageBoardItemDTO } from '@hub/notifications';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
|
||||
@Component({
|
||||
selector: 'modal-notifications-remission-group',
|
||||
@@ -11,7 +20,10 @@ import { MessageBoardItemDTO } from '@hub/notifications';
|
||||
standalone: false,
|
||||
})
|
||||
export class ModalNotificationsRemissionGroupComponent {
|
||||
private _pickupShelfInNavigationService = inject(PickupShelfInNavigationService);
|
||||
tabService = inject(TabService);
|
||||
private _pickupShelfInNavigationService = inject(
|
||||
PickupShelfInNavigationService,
|
||||
);
|
||||
|
||||
@Input()
|
||||
notifications: MessageBoardItemDTO[];
|
||||
@@ -19,11 +31,19 @@ export class ModalNotificationsRemissionGroupComponent {
|
||||
@Output()
|
||||
navigated = new EventEmitter<void>();
|
||||
|
||||
remissionPath = linkedSignal(() => [
|
||||
'/',
|
||||
this.tabService.activatedTab()?.id || this.tabService.nextId(),
|
||||
'remission',
|
||||
]);
|
||||
|
||||
constructor(private _router: Router) {}
|
||||
|
||||
itemSelected(item: MessageBoardItemDTO) {
|
||||
const defaultNav = this._pickupShelfInNavigationService.listRoute();
|
||||
const queryParams = UiFilter.getQueryParamsFromQueryTokenDTO(item.queryToken);
|
||||
const queryParams = UiFilter.getQueryParamsFromQueryTokenDTO(
|
||||
item.queryToken,
|
||||
);
|
||||
this._router.navigate(defaultNav.path, {
|
||||
queryParams: {
|
||||
...defaultNav.queryParams,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { REIHE_PREFIX_PATTERN } from './reihe.constants';
|
||||
|
||||
@Pipe({
|
||||
name: 'lineType',
|
||||
@@ -7,8 +8,8 @@ import { Pipe, PipeTransform } from '@angular/core';
|
||||
})
|
||||
export class LineTypePipe implements PipeTransform {
|
||||
transform(value: string, ...args: any[]): 'text' | 'reihe' {
|
||||
const REIHE_REGEX = /^Reihe:\s*"(.+)\"$/g;
|
||||
const reihe = REIHE_REGEX.exec(value)?.[1];
|
||||
const REIHE_REGEX = new RegExp(`^${REIHE_PREFIX_PATTERN}:\\s*"(.+)"$`, 'g');
|
||||
const reihe = REIHE_REGEX.exec(value)?.[2];
|
||||
return reihe ? 'reihe' : 'text';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
OnDestroy,
|
||||
Pipe,
|
||||
PipeTransform,
|
||||
} from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { ProductCatalogNavigationService } from '@shared/services/navigation';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Subscription, combineLatest, BehaviorSubject } from 'rxjs';
|
||||
import { distinctUntilChanged } from 'rxjs/operators';
|
||||
import { REIHE_PREFIX_PATTERN } from './reihe.constants';
|
||||
|
||||
@Pipe({
|
||||
name: 'reiheRoute',
|
||||
@@ -22,10 +28,13 @@ export class ReiheRoutePipe implements PipeTransform, OnDestroy {
|
||||
private application: ApplicationService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
) {
|
||||
this.subscription = combineLatest([this.application.activatedProcessId$, this.value$])
|
||||
this.subscription = combineLatest([
|
||||
this.application.activatedProcessId$,
|
||||
this.value$,
|
||||
])
|
||||
.pipe(distinctUntilChanged(isEqual))
|
||||
.subscribe(([processId, value]) => {
|
||||
const REIHE_REGEX = /[";]|Reihe:/g; // Entferne jedes Semikolon, Anführungszeichen und den String Reihe:
|
||||
const REIHE_REGEX = new RegExp(`[";]|${REIHE_PREFIX_PATTERN}:`, 'g'); // Entferne jedes Semikolon, Anführungszeichen und den String Reihe:/Reihe/Set:/Set/Reihe:
|
||||
const reihe = value?.replace(REIHE_REGEX, '')?.trim();
|
||||
|
||||
if (!reihe) {
|
||||
@@ -33,9 +42,15 @@ export class ReiheRoutePipe implements PipeTransform, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const main_qs = reihe.split('/')[0];
|
||||
// Entferne Zahlen am Ende, die mit Leerzeichen, Komma, Slash oder Semikolon getrennt sind
|
||||
// Beispiele: "Harry Potter 1" -> "Harry Potter", "Harry Potter,1" -> "Harry Potter", "Harry Potter/2" -> "Harry Potter"
|
||||
const main_qs = reihe
|
||||
.split('/')[0]
|
||||
.replace(/[\s,;]+\d+$/g, '')
|
||||
.trim();
|
||||
|
||||
const path = this.navigation.getArticleSearchResultsPath(processId).path;
|
||||
const path =
|
||||
this.navigation.getArticleSearchResultsPath(processId).path;
|
||||
|
||||
this.result = {
|
||||
path,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Shared regex pattern for matching Reihe line prefixes.
|
||||
* Matches: "Reihe:", "Reihe/Set:", or "Set/Reihe:"
|
||||
*/
|
||||
export const REIHE_PREFIX_PATTERN = '(Reihe|Reihe\\/Set|Set\\/Reihe)';
|
||||
@@ -1,4 +1,4 @@
|
||||
<ng-container *ifRole="'Store'">
|
||||
<!-- <ng-container *ifRole="'Store'">
|
||||
@if (customerType !== 'b2b') {
|
||||
<shared-checkbox
|
||||
[ngModel]="p4mUser"
|
||||
@@ -8,15 +8,17 @@
|
||||
Kundenkarte
|
||||
</shared-checkbox>
|
||||
}
|
||||
</ng-container>
|
||||
</ng-container> -->
|
||||
@for (option of filteredOptions$ | async; track option) {
|
||||
@if (option?.enabled !== false) {
|
||||
<shared-checkbox
|
||||
[ngModel]="option.value === customerType"
|
||||
(ngModelChange)="setValue({ customerType: $event ? option.value : undefined })"
|
||||
(ngModelChange)="
|
||||
setValue({ customerType: $event ? option.value : undefined })
|
||||
"
|
||||
[disabled]="isOptionDisabled(option)"
|
||||
[name]="option.value"
|
||||
>
|
||||
>
|
||||
{{ option.label }}
|
||||
</shared-checkbox>
|
||||
}
|
||||
|
||||
@@ -21,7 +21,13 @@ import { OptionDTO } from '@generated/swagger/checkout-api';
|
||||
import { UiCheckboxComponent } from '@ui/checkbox';
|
||||
import { first, isBoolean, isString } from 'lodash';
|
||||
import { combineLatest, Observable, Subject } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
export interface CustomerTypeSelectorState {
|
||||
processId: number;
|
||||
@@ -58,18 +64,18 @@ export class CustomerTypeSelectorComponent
|
||||
|
||||
@Input()
|
||||
get value() {
|
||||
if (this.p4mUser) {
|
||||
return `${this.customerType}-p4m`;
|
||||
}
|
||||
// if (this.p4mUser) {
|
||||
// return `${this.customerType}-p4m`;
|
||||
// }
|
||||
return this.customerType;
|
||||
}
|
||||
set value(value: string) {
|
||||
if (value.includes('-p4m')) {
|
||||
this.p4mUser = true;
|
||||
this.customerType = value.replace('-p4m', '');
|
||||
} else {
|
||||
this.customerType = value;
|
||||
}
|
||||
// if (value.includes('-p4m')) {
|
||||
// this.p4mUser = true;
|
||||
// this.customerType = value.replace('-p4m', '');
|
||||
// } else {
|
||||
this.customerType = value;
|
||||
// }
|
||||
}
|
||||
|
||||
@Output()
|
||||
@@ -111,30 +117,35 @@ export class CustomerTypeSelectorComponent
|
||||
get filteredOptions$() {
|
||||
const options$ = this.select((s) => s.options).pipe(distinctUntilChanged());
|
||||
const p4mUser$ = this.select((s) => s.p4mUser).pipe(distinctUntilChanged());
|
||||
const customerType$ = this.select((s) => s.customerType).pipe(distinctUntilChanged());
|
||||
const customerType$ = this.select((s) => s.customerType).pipe(
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
return combineLatest([options$, p4mUser$, customerType$]).pipe(
|
||||
filter(([options]) => options?.length > 0),
|
||||
map(([options, p4mUser, customerType]) => {
|
||||
const initial = { p4mUser: this.p4mUser, customerType: this.customerType };
|
||||
const initial = {
|
||||
p4mUser: this.p4mUser,
|
||||
customerType: this.customerType,
|
||||
};
|
||||
let result: OptionDTO[] = options;
|
||||
if (p4mUser) {
|
||||
result = result.filter((o) => o.value === 'store' || (o.value === 'webshop' && o.enabled !== false));
|
||||
// if (p4mUser) {
|
||||
// result = result.filter((o) => o.value === 'store' || (o.value === 'webshop' && o.enabled !== false));
|
||||
|
||||
result = result.map((o) => {
|
||||
if (o.value === 'store') {
|
||||
return { ...o, enabled: false };
|
||||
}
|
||||
return o;
|
||||
});
|
||||
}
|
||||
// result = result.map((o) => {
|
||||
// if (o.value === 'store') {
|
||||
// return { ...o, enabled: false };
|
||||
// }
|
||||
// return o;
|
||||
// });
|
||||
// }
|
||||
|
||||
if (customerType === 'b2b' && this.p4mUser) {
|
||||
this.p4mUser = false;
|
||||
}
|
||||
|
||||
if (initial.p4mUser !== this.p4mUser || initial.customerType !== this.customerType) {
|
||||
this.setValue({ customerType: this.customerType, p4mUser: this.p4mUser });
|
||||
}
|
||||
// if (initial.p4mUser !== this.p4mUser || initial.customerType !== this.customerType) {
|
||||
// this.setValue({ customerType: this.customerType, p4mUser: this.p4mUser });
|
||||
// }
|
||||
|
||||
return result;
|
||||
}),
|
||||
@@ -224,42 +235,53 @@ export class CustomerTypeSelectorComponent
|
||||
if (typeof value === 'string') {
|
||||
this.value = value;
|
||||
} else {
|
||||
if (isBoolean(value.p4mUser)) {
|
||||
this.p4mUser = value.p4mUser;
|
||||
}
|
||||
// if (isBoolean(value.p4mUser)) {
|
||||
// this.p4mUser = value.p4mUser;
|
||||
// }
|
||||
if (isString(value.customerType)) {
|
||||
this.customerType = value.customerType;
|
||||
} else if (this.p4mUser) {
|
||||
// Implementierung wie im PBI #3467 beschrieben
|
||||
// wenn customerType nicht gesetzt wird und p4mUser true ist,
|
||||
// dann customerType auf store setzen.
|
||||
// wenn dies nicht möglich ist da der Warenkob keinen store Kunden zulässt,
|
||||
// dann customerType auf webshop setzen.
|
||||
// wenn dies nicht möglich ist da der Warenkob keinen webshop Kunden zulässt,
|
||||
// dann customerType auf den ersten verfügbaren setzen und p4mUser auf false setzen.
|
||||
if (this.enabledOptions.some((o) => o.value === 'store')) {
|
||||
this.customerType = 'store';
|
||||
} else if (this.enabledOptions.some((o) => o.value === 'webshop')) {
|
||||
this.customerType = 'webshop';
|
||||
} else {
|
||||
this.p4mUser = false;
|
||||
const includesGuest = this.enabledOptions.some((o) => o.value === 'guest');
|
||||
this.customerType = includesGuest ? 'guest' : first(this.enabledOptions)?.value;
|
||||
}
|
||||
// } else if (this.p4mUser) {
|
||||
// // Implementierung wie im PBI #3467 beschrieben
|
||||
// // wenn customerType nicht gesetzt wird und p4mUser true ist,
|
||||
// // dann customerType auf store setzen.
|
||||
// // wenn dies nicht möglich ist da der Warenkob keinen store Kunden zulässt,
|
||||
// // dann customerType auf webshop setzen.
|
||||
// // wenn dies nicht möglich ist da der Warenkob keinen webshop Kunden zulässt,
|
||||
// // dann customerType auf den ersten verfügbaren setzen und p4mUser auf false setzen.
|
||||
// if (this.enabledOptions.some((o) => o.value === 'store')) {
|
||||
// this.customerType = 'store';
|
||||
// } else if (this.enabledOptions.some((o) => o.value === 'webshop')) {
|
||||
// this.customerType = 'webshop';
|
||||
// } else {
|
||||
// this.p4mUser = false;
|
||||
// const includesGuest = this.enabledOptions.some(
|
||||
// (o) => o.value === 'guest',
|
||||
// );
|
||||
// this.customerType = includesGuest
|
||||
// ? 'guest'
|
||||
// : first(this.enabledOptions)?.value;
|
||||
// }
|
||||
} else {
|
||||
// wenn customerType nicht gesetzt wird und p4mUser false ist,
|
||||
// dann customerType auf den ersten verfügbaren setzen der nicht mit dem aktuellen customerType übereinstimmt.
|
||||
this.customerType =
|
||||
first(this.enabledOptions.filter((o) => o.value === this.customerType))?.value ?? this.customerType;
|
||||
first(
|
||||
this.enabledOptions.filter((o) => o.value === this.customerType),
|
||||
)?.value ?? this.customerType;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.customerType !== initial.customerType || this.p4mUser !== initial.p4mUser) {
|
||||
if (
|
||||
this.customerType !== initial.customerType ||
|
||||
this.p4mUser !== initial.p4mUser
|
||||
) {
|
||||
this.onChange(this.value);
|
||||
this.onTouched();
|
||||
this.valueChanges.emit(this.value);
|
||||
}
|
||||
|
||||
this.checkboxes?.find((c) => c.name === this.customerType)?.writeValue(true);
|
||||
this.checkboxes
|
||||
?.find((c) => c.name === this.customerType)
|
||||
?.writeValue(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@ export * from './interests';
|
||||
export * from './name';
|
||||
export * from './newsletter';
|
||||
export * from './organisation';
|
||||
export * from './p4m-number';
|
||||
// export * from './p4m-number';
|
||||
export * from './phone-numbers';
|
||||
export * from './form-block';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// start:ng42.barrel
|
||||
export * from './p4m-number-form-block.component';
|
||||
export * from './p4m-number-form-block.module';
|
||||
// end:ng42.barrel
|
||||
// // start:ng42.barrel
|
||||
// export * from './p4m-number-form-block.component';
|
||||
// export * from './p4m-number-form-block.module';
|
||||
// // end:ng42.barrel
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<shared-form-control label="Kundenkartencode" class="flex-grow">
|
||||
<!-- <shared-form-control label="Kundenkartencode" class="flex-grow">
|
||||
<input
|
||||
placeholder="Kundenkartencode"
|
||||
class="input-control"
|
||||
@@ -13,4 +13,4 @@
|
||||
<button type="button" (click)="scan()">
|
||||
<shared-icon icon="barcode-scan" [size]="32"></shared-icon>
|
||||
</button>
|
||||
}
|
||||
} -->
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { UntypedFormControl, Validators } from '@angular/forms';
|
||||
import { FormBlockControl } from '../form-block';
|
||||
import { ScanAdapterService } from '@adapter/scan';
|
||||
// import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
// import { UntypedFormControl, Validators } from '@angular/forms';
|
||||
// import { FormBlockControl } from '../form-block';
|
||||
// import { ScanAdapterService } from '@adapter/scan';
|
||||
|
||||
@Component({
|
||||
selector: 'app-p4m-number-form-block',
|
||||
templateUrl: 'p4m-number-form-block.component.html',
|
||||
styleUrls: ['p4m-number-form-block.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class P4mNumberFormBlockComponent extends FormBlockControl<string> {
|
||||
get tabIndexEnd() {
|
||||
return this.tabIndexStart;
|
||||
}
|
||||
// @Component({
|
||||
// selector: 'app-p4m-number-form-block',
|
||||
// templateUrl: 'p4m-number-form-block.component.html',
|
||||
// styleUrls: ['p4m-number-form-block.component.scss'],
|
||||
// changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
// standalone: false,
|
||||
// })
|
||||
// export class P4mNumberFormBlockComponent extends FormBlockControl<string> {
|
||||
// get tabIndexEnd() {
|
||||
// return this.tabIndexStart;
|
||||
// }
|
||||
|
||||
constructor(
|
||||
private scanAdapter: ScanAdapterService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
// constructor(
|
||||
// private scanAdapter: ScanAdapterService,
|
||||
// private changeDetectorRef: ChangeDetectorRef,
|
||||
// ) {
|
||||
// super();
|
||||
// }
|
||||
|
||||
updateValidators(): void {
|
||||
this.control.setValidators([...this.getValidatorFn()]);
|
||||
this.control.setAsyncValidators(this.getAsyncValidatorFn());
|
||||
this.control.updateValueAndValidity();
|
||||
}
|
||||
// updateValidators(): void {
|
||||
// this.control.setValidators([...this.getValidatorFn()]);
|
||||
// this.control.setAsyncValidators(this.getAsyncValidatorFn());
|
||||
// this.control.updateValueAndValidity();
|
||||
// }
|
||||
|
||||
initializeControl(data?: string): void {
|
||||
this.control = new UntypedFormControl(data ?? '', [Validators.required], this.getAsyncValidatorFn());
|
||||
}
|
||||
// initializeControl(data?: string): void {
|
||||
// this.control = new UntypedFormControl(data ?? '', [Validators.required], this.getAsyncValidatorFn());
|
||||
// }
|
||||
|
||||
_patchValue(update: { previous: string; current: string }): void {
|
||||
this.control.patchValue(update.current);
|
||||
}
|
||||
// _patchValue(update: { previous: string; current: string }): void {
|
||||
// this.control.patchValue(update.current);
|
||||
// }
|
||||
|
||||
scan() {
|
||||
this.scanAdapter.scan().subscribe((result) => {
|
||||
this.control.patchValue(result);
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
// scan() {
|
||||
// this.scanAdapter.scan().subscribe((result) => {
|
||||
// this.control.patchValue(result);
|
||||
// this.changeDetectorRef.markForCheck();
|
||||
// });
|
||||
// }
|
||||
|
||||
canScan() {
|
||||
return this.scanAdapter.isReady();
|
||||
}
|
||||
}
|
||||
// canScan() {
|
||||
// return this.scanAdapter.isReady();
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
// import { NgModule } from '@angular/core';
|
||||
// import { CommonModule } from '@angular/common';
|
||||
|
||||
import { P4mNumberFormBlockComponent } from './p4m-number-form-block.component';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { FormControlComponent } from '@shared/components/form-control';
|
||||
// import { P4mNumberFormBlockComponent } from './p4m-number-form-block.component';
|
||||
// import { ReactiveFormsModule } from '@angular/forms';
|
||||
// import { IconComponent } from '@shared/components/icon';
|
||||
// import { FormControlComponent } from '@shared/components/form-control';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, ReactiveFormsModule, FormControlComponent, IconComponent],
|
||||
exports: [P4mNumberFormBlockComponent],
|
||||
declarations: [P4mNumberFormBlockComponent],
|
||||
})
|
||||
export class P4mNumberFormBlockModule {}
|
||||
// @NgModule({
|
||||
// imports: [CommonModule, ReactiveFormsModule, FormControlComponent, IconComponent],
|
||||
// exports: [P4mNumberFormBlockComponent],
|
||||
// declarations: [P4mNumberFormBlockComponent],
|
||||
// })
|
||||
// export class P4mNumberFormBlockModule {}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { ChangeDetectorRef, Directive, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Directive,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
AbstractControl,
|
||||
AsyncValidatorFn,
|
||||
@@ -11,7 +18,12 @@ import {
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { CrmCustomerService } from '@domain/crm';
|
||||
import { AddressDTO, CustomerDTO, PayerDTO, ShippingAddressDTO } from '@generated/swagger/crm-api';
|
||||
import {
|
||||
AddressDTO,
|
||||
CustomerDTO,
|
||||
PayerDTO,
|
||||
ShippingAddressDTO,
|
||||
} from '@generated/swagger/crm-api';
|
||||
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
import { UiValidators } from '@ui/validators';
|
||||
import { isNull } from 'lodash';
|
||||
@@ -42,7 +54,10 @@ import {
|
||||
mapCustomerInfoDtoToCustomerCreateFormData,
|
||||
} from './customer-create-form-data';
|
||||
import { AddressSelectionModalService } from '../modals';
|
||||
import { CustomerCreateNavigation, CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import {
|
||||
CustomerCreateNavigation,
|
||||
CustomerSearchNavigation,
|
||||
} from '@shared/services/navigation';
|
||||
|
||||
@Directive()
|
||||
export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
||||
@@ -104,7 +119,12 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
this.processId$
|
||||
.pipe(startWith(undefined), bufferCount(2, 1), takeUntil(this.onDestroy$), delay(100))
|
||||
.pipe(
|
||||
startWith(undefined),
|
||||
bufferCount(2, 1),
|
||||
takeUntil(this.onDestroy$),
|
||||
delay(100),
|
||||
)
|
||||
.subscribe(async ([previous, current]) => {
|
||||
if (previous === undefined) {
|
||||
await this._initFormData();
|
||||
@@ -155,7 +175,10 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async addOrUpdateBreadcrumb(processId: number, formData: CustomerCreateFormData) {
|
||||
async addOrUpdateBreadcrumb(
|
||||
processId: number,
|
||||
formData: CustomerCreateFormData,
|
||||
) {
|
||||
await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
|
||||
key: processId,
|
||||
name: 'Kundendaten erfassen',
|
||||
@@ -195,7 +218,10 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
||||
console.log('customerTypeChanged', customerType);
|
||||
}
|
||||
|
||||
addFormBlock(key: keyof CustomerCreateFormData, block: FormBlock<any, AbstractControl>) {
|
||||
addFormBlock(
|
||||
key: keyof CustomerCreateFormData,
|
||||
block: FormBlock<any, AbstractControl>,
|
||||
) {
|
||||
this.form.addControl(key, block.control);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
@@ -232,7 +258,10 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
||||
return true;
|
||||
}
|
||||
// Check Year + Month
|
||||
else if (inputDate.getFullYear() === minBirthDate.getFullYear() && inputDate.getMonth() < minBirthDate.getMonth()) {
|
||||
else if (
|
||||
inputDate.getFullYear() === minBirthDate.getFullYear() &&
|
||||
inputDate.getMonth() < minBirthDate.getMonth()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// Check Year + Month + Day
|
||||
@@ -279,70 +308,80 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
||||
);
|
||||
};
|
||||
|
||||
checkLoyalityCardValidator: AsyncValidatorFn = (control) => {
|
||||
return of(control.value).pipe(
|
||||
delay(500),
|
||||
mergeMap((value) => {
|
||||
const customerId = this.formData?._meta?.customerDto?.id ?? this.formData?._meta?.customerInfoDto?.id;
|
||||
return this.customerService.checkLoyaltyCard({ loyaltyCardNumber: value, customerId }).pipe(
|
||||
map((response) => {
|
||||
if (response.error) {
|
||||
throw response.message;
|
||||
}
|
||||
// checkLoyalityCardValidator: AsyncValidatorFn = (control) => {
|
||||
// return of(control.value).pipe(
|
||||
// delay(500),
|
||||
// mergeMap((value) => {
|
||||
// const customerId = this.formData?._meta?.customerDto?.id ?? this.formData?._meta?.customerInfoDto?.id;
|
||||
// return this.customerService.checkLoyaltyCard({ loyaltyCardNumber: value, customerId }).pipe(
|
||||
// map((response) => {
|
||||
// if (response.error) {
|
||||
// throw response.message;
|
||||
// }
|
||||
|
||||
/**
|
||||
* #4485 Kubi // Verhalten mit angelegte aber nicht verknüpfte Kundenkartencode in Kundensuche und Kundendaten erfassen ist nicht gleich
|
||||
* Fall1: Kundenkarte hat Daten in point4more:
|
||||
* Sobald Kundenkartencode in Feld "Kundenkartencode" reingegeben wird- werden die Daten von point4more in Formular "Kundendaten Erfassen" eingefügt und ersetzen (im Ganzen, nicht inkremental) die Daten in Felder, falls welche schon reingetippt werden.
|
||||
* Fall2: Kundenkarte hat keine Daten in point4more:
|
||||
* Sobald Kundenkartencode in Feld "Kundenkartencode" reingegeben wird- bleiben die Daten in Formular "Kundendaten Erfassen" in Felder, falls welche schon reingetippt werden.
|
||||
*/
|
||||
if (response.result && response.result.customer) {
|
||||
const customer = response.result.customer;
|
||||
const data = mapCustomerInfoDtoToCustomerCreateFormData(customer);
|
||||
// /**
|
||||
// * #4485 Kubi // Verhalten mit angelegte aber nicht verknüpfte Kundenkartencode in Kundensuche und Kundendaten erfassen ist nicht gleich
|
||||
// * Fall1: Kundenkarte hat Daten in point4more:
|
||||
// * Sobald Kundenkartencode in Feld "Kundenkartencode" reingegeben wird- werden die Daten von point4more in Formular "Kundendaten Erfassen" eingefügt und ersetzen (im Ganzen, nicht inkremental) die Daten in Felder, falls welche schon reingetippt werden.
|
||||
// * Fall2: Kundenkarte hat keine Daten in point4more:
|
||||
// * Sobald Kundenkartencode in Feld "Kundenkartencode" reingegeben wird- bleiben die Daten in Formular "Kundendaten Erfassen" in Felder, falls welche schon reingetippt werden.
|
||||
// */
|
||||
// if (response.result && response.result.customer) {
|
||||
// const customer = response.result.customer;
|
||||
// const data = mapCustomerInfoDtoToCustomerCreateFormData(customer);
|
||||
|
||||
if (data.name.firstName && data.name.lastName) {
|
||||
// Fall1
|
||||
this._formData.next(data);
|
||||
} else {
|
||||
// Fall2 Hier müssen die Metadaten gesetzt werden um eine verknüfung zur kundenkarte zu ermöglichen.
|
||||
const current = this.formData;
|
||||
current._meta = data._meta;
|
||||
current.p4m = data.p4m;
|
||||
}
|
||||
}
|
||||
// if (data.name.firstName && data.name.lastName) {
|
||||
// // Fall1
|
||||
// this._formData.next(data);
|
||||
// } else {
|
||||
// // Fall2 Hier müssen die Metadaten gesetzt werden um eine verknüfung zur kundenkarte zu ermöglichen.
|
||||
// const current = this.formData;
|
||||
// current._meta = data._meta;
|
||||
// current.p4m = data.p4m;
|
||||
// }
|
||||
// }
|
||||
|
||||
return null;
|
||||
}),
|
||||
catchError((error) => {
|
||||
if (error instanceof HttpErrorResponse) {
|
||||
if (error?.error?.invalidProperties?.loyaltyCardNumber) {
|
||||
return of({ invalid: error.error.invalidProperties.loyaltyCardNumber });
|
||||
} else {
|
||||
return of({ invalid: 'Kundenkartencode ist ungültig' });
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}),
|
||||
tap(() => {
|
||||
control.markAsTouched();
|
||||
this.cdr.markForCheck();
|
||||
}),
|
||||
);
|
||||
};
|
||||
// return null;
|
||||
// }),
|
||||
// catchError((error) => {
|
||||
// if (error instanceof HttpErrorResponse) {
|
||||
// if (error?.error?.invalidProperties?.loyaltyCardNumber) {
|
||||
// return of({ invalid: error.error.invalidProperties.loyaltyCardNumber });
|
||||
// } else {
|
||||
// return of({ invalid: 'Kundenkartencode ist ungültig' });
|
||||
// }
|
||||
// }
|
||||
// }),
|
||||
// );
|
||||
// }),
|
||||
// tap(() => {
|
||||
// control.markAsTouched();
|
||||
// this.cdr.markForCheck();
|
||||
// }),
|
||||
// );
|
||||
// };
|
||||
|
||||
async navigateToCustomerDetails(customer: CustomerDTO) {
|
||||
const processId = await this.processId$.pipe(first()).toPromise();
|
||||
const route = this.customerSearchNavigation.detailsRoute({ processId, customerId: customer.id, customer });
|
||||
const route = this.customerSearchNavigation.detailsRoute({
|
||||
processId,
|
||||
customerId: customer.id,
|
||||
customer,
|
||||
});
|
||||
|
||||
return this.router.navigate(route.path, { queryParams: route.urlTree.queryParams });
|
||||
return this.router.navigate(route.path, {
|
||||
queryParams: route.urlTree.queryParams,
|
||||
});
|
||||
}
|
||||
|
||||
async validateAddressData(address: AddressDTO): Promise<AddressDTO> {
|
||||
const addressValidationResult = await this.addressVlidationModal.validateAddress(address);
|
||||
const addressValidationResult =
|
||||
await this.addressVlidationModal.validateAddress(address);
|
||||
|
||||
if (addressValidationResult !== undefined && (addressValidationResult as any) !== 'continue') {
|
||||
if (
|
||||
addressValidationResult !== undefined &&
|
||||
(addressValidationResult as any) !== 'continue'
|
||||
) {
|
||||
address = addressValidationResult;
|
||||
}
|
||||
|
||||
@@ -389,7 +428,9 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
||||
} catch (error) {
|
||||
this.form.enable();
|
||||
setTimeout(() => {
|
||||
this.addressFormBlock.setAddressValidationError(error.error.invalidProperties);
|
||||
this.addressFormBlock.setAddressValidationError(
|
||||
error.error.invalidProperties,
|
||||
);
|
||||
}, 10);
|
||||
|
||||
return;
|
||||
@@ -397,7 +438,10 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.birthDate && isNull(UiValidators.date(new UntypedFormControl(data.birthDate)))) {
|
||||
if (
|
||||
data.birthDate &&
|
||||
isNull(UiValidators.date(new UntypedFormControl(data.birthDate)))
|
||||
) {
|
||||
customer.dateOfBirth = data.birthDate;
|
||||
}
|
||||
|
||||
@@ -406,11 +450,15 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
||||
|
||||
if (this.validateShippingAddress) {
|
||||
try {
|
||||
billingAddress.address = await this.validateAddressData(billingAddress.address);
|
||||
billingAddress.address = await this.validateAddressData(
|
||||
billingAddress.address,
|
||||
);
|
||||
} catch (error) {
|
||||
this.form.enable();
|
||||
setTimeout(() => {
|
||||
this.addressFormBlock.setAddressValidationError(error.error.invalidProperties);
|
||||
this.addressFormBlock.setAddressValidationError(
|
||||
error.error.invalidProperties,
|
||||
);
|
||||
}, 10);
|
||||
|
||||
return;
|
||||
@@ -426,15 +474,21 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
if (data.deviatingDeliveryAddress?.deviatingAddress) {
|
||||
const shippingAddress = this.mapToShippingAddress(data.deviatingDeliveryAddress);
|
||||
const shippingAddress = this.mapToShippingAddress(
|
||||
data.deviatingDeliveryAddress,
|
||||
);
|
||||
|
||||
if (this.validateShippingAddress) {
|
||||
try {
|
||||
shippingAddress.address = await this.validateAddressData(shippingAddress.address);
|
||||
shippingAddress.address = await this.validateAddressData(
|
||||
shippingAddress.address,
|
||||
);
|
||||
} catch (error) {
|
||||
this.form.enable();
|
||||
setTimeout(() => {
|
||||
this.deviatingDeliveryAddressFormBlock.setAddressValidationError(error.error.invalidProperties);
|
||||
this.deviatingDeliveryAddressFormBlock.setAddressValidationError(
|
||||
error.error.invalidProperties,
|
||||
);
|
||||
}, 10);
|
||||
|
||||
return;
|
||||
@@ -474,7 +528,13 @@ export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
|
||||
};
|
||||
}
|
||||
|
||||
mapToBillingAddress({ name, address, email, organisation, phoneNumbers }: DeviatingAddressFormBlockData): PayerDTO {
|
||||
mapToBillingAddress({
|
||||
name,
|
||||
address,
|
||||
email,
|
||||
organisation,
|
||||
phoneNumbers,
|
||||
}: DeviatingAddressFormBlockData): PayerDTO {
|
||||
return {
|
||||
gender: name?.gender,
|
||||
title: name?.title,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CreateB2BCustomerModule } from './create-b2b-customer/create-b2b-customer.module';
|
||||
import { CreateGuestCustomerModule } from './create-guest-customer';
|
||||
import { CreateP4MCustomerModule } from './create-p4m-customer';
|
||||
// import { CreateP4MCustomerModule } from './create-p4m-customer';
|
||||
import { CreateStoreCustomerModule } from './create-store-customer/create-store-customer.module';
|
||||
import { CreateWebshopCustomerModule } from './create-webshop-customer/create-webshop-customer.module';
|
||||
import { UpdateP4MWebshopCustomerModule } from './update-p4m-webshop-customer';
|
||||
// import { UpdateP4MWebshopCustomerModule } from './update-p4m-webshop-customer';
|
||||
import { CreateCustomerComponent } from './create-customer.component';
|
||||
|
||||
@NgModule({
|
||||
@@ -13,8 +13,8 @@ import { CreateCustomerComponent } from './create-customer.component';
|
||||
CreateGuestCustomerModule,
|
||||
CreateStoreCustomerModule,
|
||||
CreateWebshopCustomerModule,
|
||||
CreateP4MCustomerModule,
|
||||
UpdateP4MWebshopCustomerModule,
|
||||
// CreateP4MCustomerModule,
|
||||
// UpdateP4MWebshopCustomerModule,
|
||||
CreateCustomerComponent,
|
||||
],
|
||||
exports: [
|
||||
@@ -22,8 +22,8 @@ import { CreateCustomerComponent } from './create-customer.component';
|
||||
CreateGuestCustomerModule,
|
||||
CreateStoreCustomerModule,
|
||||
CreateWebshopCustomerModule,
|
||||
CreateP4MCustomerModule,
|
||||
UpdateP4MWebshopCustomerModule,
|
||||
// CreateP4MCustomerModule,
|
||||
// UpdateP4MWebshopCustomerModule,
|
||||
CreateCustomerComponent,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@if (formData$ | async; as data) {
|
||||
<!-- @if (formData$ | async; as data) {
|
||||
<form (keydown.enter)="$event.preventDefault()">
|
||||
<h1 class="title flex flex-row items-center justify-center">
|
||||
Kundendaten erfassen
|
||||
<!-- <span
|
||||
class="rounded-full ml-4 h-8 w-8 text-xl text-center border-2 border-solid border-brand text-brand">i</span> -->
|
||||
<span
|
||||
class="rounded-full ml-4 h-8 w-8 text-xl text-center border-2 border-solid border-brand text-brand">i</span>
|
||||
</h1>
|
||||
<p class="description">
|
||||
Um Sie als Kunde beim nächsten
|
||||
@@ -135,4 +135,4 @@
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
} -->
|
||||
|
||||
@@ -1,292 +1,292 @@
|
||||
import { Component, ChangeDetectionStrategy, ViewChild, OnInit } from '@angular/core';
|
||||
import { AsyncValidatorFn, ValidatorFn, Validators } from '@angular/forms';
|
||||
import { Result } from '@domain/defs';
|
||||
import { CustomerDTO, CustomerInfoDTO, KeyValueDTOOfStringAndString } from '@generated/swagger/crm-api';
|
||||
import { UiErrorModalComponent, UiModalResult } from '@ui/modal';
|
||||
import { NEVER, Observable, of } from 'rxjs';
|
||||
import { catchError, distinctUntilChanged, first, map, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
|
||||
import {
|
||||
AddressFormBlockComponent,
|
||||
AddressFormBlockData,
|
||||
DeviatingAddressFormBlockComponent,
|
||||
} from '../../components/form-blocks';
|
||||
import { NameFormBlockData } from '../../components/form-blocks/name/name-form-block-data';
|
||||
import { WebshopCustomnerAlreadyExistsModalComponent, WebshopCustomnerAlreadyExistsModalData } from '../../modals';
|
||||
import { validateEmail } from '../../validators/email-validator';
|
||||
import { AbstractCreateCustomer } from '../abstract-create-customer';
|
||||
import { encodeFormData, mapCustomerDtoToCustomerCreateFormData } from '../customer-create-form-data';
|
||||
import { zipCodeValidator } from '../../validators/zip-code-validator';
|
||||
// import { Component, ChangeDetectionStrategy, ViewChild, OnInit } from '@angular/core';
|
||||
// import { AsyncValidatorFn, ValidatorFn, Validators } from '@angular/forms';
|
||||
// import { Result } from '@domain/defs';
|
||||
// import { CustomerDTO, CustomerInfoDTO, KeyValueDTOOfStringAndString } from '@generated/swagger/crm-api';
|
||||
// import { UiErrorModalComponent, UiModalResult } from '@ui/modal';
|
||||
// import { NEVER, Observable, of } from 'rxjs';
|
||||
// import { catchError, distinctUntilChanged, first, map, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
|
||||
// import {
|
||||
// AddressFormBlockComponent,
|
||||
// AddressFormBlockData,
|
||||
// DeviatingAddressFormBlockComponent,
|
||||
// } from '../../components/form-blocks';
|
||||
// import { NameFormBlockData } from '../../components/form-blocks/name/name-form-block-data';
|
||||
// import { WebshopCustomnerAlreadyExistsModalComponent, WebshopCustomnerAlreadyExistsModalData } from '../../modals';
|
||||
// import { validateEmail } from '../../validators/email-validator';
|
||||
// import { AbstractCreateCustomer } from '../abstract-create-customer';
|
||||
// import { encodeFormData, mapCustomerDtoToCustomerCreateFormData } from '../customer-create-form-data';
|
||||
// import { zipCodeValidator } from '../../validators/zip-code-validator';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-p4m-customer',
|
||||
templateUrl: 'create-p4m-customer.component.html',
|
||||
styleUrls: ['../create-customer.scss', 'create-p4m-customer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class CreateP4MCustomerComponent extends AbstractCreateCustomer implements OnInit {
|
||||
validateAddress = true;
|
||||
// @Component({
|
||||
// selector: 'app-create-p4m-customer',
|
||||
// templateUrl: 'create-p4m-customer.component.html',
|
||||
// styleUrls: ['../create-customer.scss', 'create-p4m-customer.component.scss'],
|
||||
// changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
// standalone: false,
|
||||
// })
|
||||
// export class CreateP4MCustomerComponent extends AbstractCreateCustomer implements OnInit {
|
||||
// validateAddress = true;
|
||||
|
||||
validateShippingAddress = true;
|
||||
// validateShippingAddress = true;
|
||||
|
||||
get _customerType() {
|
||||
return this.activatedRoute.snapshot.data.customerType;
|
||||
}
|
||||
// get _customerType() {
|
||||
// return this.activatedRoute.snapshot.data.customerType;
|
||||
// }
|
||||
|
||||
get customerType() {
|
||||
return `${this._customerType}-p4m`;
|
||||
}
|
||||
// get customerType() {
|
||||
// return `${this._customerType}-p4m`;
|
||||
// }
|
||||
|
||||
nameRequiredMarks: (keyof NameFormBlockData)[] = ['gender', 'firstName', 'lastName'];
|
||||
// nameRequiredMarks: (keyof NameFormBlockData)[] = ['gender', 'firstName', 'lastName'];
|
||||
|
||||
nameValidationFns: Record<keyof NameFormBlockData, ValidatorFn[]> = {
|
||||
firstName: [Validators.required],
|
||||
lastName: [Validators.required],
|
||||
gender: [Validators.required],
|
||||
title: [],
|
||||
};
|
||||
// nameValidationFns: Record<keyof NameFormBlockData, ValidatorFn[]> = {
|
||||
// firstName: [Validators.required],
|
||||
// lastName: [Validators.required],
|
||||
// gender: [Validators.required],
|
||||
// title: [],
|
||||
// };
|
||||
|
||||
emailRequiredMark: boolean;
|
||||
// emailRequiredMark: boolean;
|
||||
|
||||
emailValidatorFn: ValidatorFn[];
|
||||
// emailValidatorFn: ValidatorFn[];
|
||||
|
||||
asyncEmailVlaidtorFn: AsyncValidatorFn[];
|
||||
// asyncEmailVlaidtorFn: AsyncValidatorFn[];
|
||||
|
||||
asyncLoyaltyCardValidatorFn: AsyncValidatorFn[];
|
||||
// asyncLoyaltyCardValidatorFn: AsyncValidatorFn[];
|
||||
|
||||
shippingAddressRequiredMarks: (keyof AddressFormBlockData)[] = [
|
||||
'street',
|
||||
'streetNumber',
|
||||
'zipCode',
|
||||
'city',
|
||||
'country',
|
||||
];
|
||||
// shippingAddressRequiredMarks: (keyof AddressFormBlockData)[] = [
|
||||
// 'street',
|
||||
// 'streetNumber',
|
||||
// 'zipCode',
|
||||
// 'city',
|
||||
// 'country',
|
||||
// ];
|
||||
|
||||
shippingAddressValidators: Record<string, ValidatorFn[]> = {
|
||||
street: [Validators.required],
|
||||
streetNumber: [Validators.required],
|
||||
zipCode: [Validators.required, zipCodeValidator()],
|
||||
city: [Validators.required],
|
||||
country: [Validators.required],
|
||||
};
|
||||
// shippingAddressValidators: Record<string, ValidatorFn[]> = {
|
||||
// street: [Validators.required],
|
||||
// streetNumber: [Validators.required],
|
||||
// zipCode: [Validators.required, zipCodeValidator()],
|
||||
// city: [Validators.required],
|
||||
// country: [Validators.required],
|
||||
// };
|
||||
|
||||
addressRequiredMarks: (keyof AddressFormBlockData)[];
|
||||
// addressRequiredMarks: (keyof AddressFormBlockData)[];
|
||||
|
||||
addressValidatorFns: Record<string, ValidatorFn[]>;
|
||||
// addressValidatorFns: Record<string, ValidatorFn[]>;
|
||||
|
||||
@ViewChild(AddressFormBlockComponent, { static: false })
|
||||
addressFormBlock: AddressFormBlockComponent;
|
||||
// @ViewChild(AddressFormBlockComponent, { static: false })
|
||||
// addressFormBlock: AddressFormBlockComponent;
|
||||
|
||||
@ViewChild(DeviatingAddressFormBlockComponent, { static: false })
|
||||
deviatingDeliveryAddressFormBlock: DeviatingAddressFormBlockComponent;
|
||||
// @ViewChild(DeviatingAddressFormBlockComponent, { static: false })
|
||||
// deviatingDeliveryAddressFormBlock: DeviatingAddressFormBlockComponent;
|
||||
|
||||
agbValidatorFns = [Validators.requiredTrue];
|
||||
// agbValidatorFns = [Validators.requiredTrue];
|
||||
|
||||
birthDateValidatorFns = [];
|
||||
// birthDateValidatorFns = [];
|
||||
|
||||
existingCustomer$: Observable<CustomerInfoDTO | CustomerDTO | null>;
|
||||
// existingCustomer$: Observable<CustomerInfoDTO | CustomerDTO | null>;
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
this.initMarksAndValidators();
|
||||
this.existingCustomer$ = this.customerExists$.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((exists) => {
|
||||
if (exists) {
|
||||
return this.fetchCustomerInfo();
|
||||
}
|
||||
return of(null);
|
||||
}),
|
||||
);
|
||||
// ngOnInit(): void {
|
||||
// super.ngOnInit();
|
||||
// this.initMarksAndValidators();
|
||||
// this.existingCustomer$ = this.customerExists$.pipe(
|
||||
// distinctUntilChanged(),
|
||||
// switchMap((exists) => {
|
||||
// if (exists) {
|
||||
// return this.fetchCustomerInfo();
|
||||
// }
|
||||
// return of(null);
|
||||
// }),
|
||||
// );
|
||||
|
||||
this.existingCustomer$
|
||||
.pipe(
|
||||
takeUntil(this.onDestroy$),
|
||||
switchMap((info) => {
|
||||
if (info) {
|
||||
return this.customerService.getCustomer(info.id, 2).pipe(
|
||||
map((res) => res.result),
|
||||
catchError((err) => NEVER),
|
||||
);
|
||||
}
|
||||
return NEVER;
|
||||
}),
|
||||
withLatestFrom(this.processId$),
|
||||
)
|
||||
.subscribe(([customer, processId]) => {
|
||||
if (customer) {
|
||||
this.modal
|
||||
.open({
|
||||
content: WebshopCustomnerAlreadyExistsModalComponent,
|
||||
data: {
|
||||
customer,
|
||||
processId,
|
||||
} as WebshopCustomnerAlreadyExistsModalData,
|
||||
title: 'Es existiert bereits ein Onlinekonto mit dieser E-Mail-Adresse',
|
||||
})
|
||||
.afterClosed$.subscribe(async (result: UiModalResult<boolean>) => {
|
||||
if (result.data) {
|
||||
this.navigateToUpdatePage(customer);
|
||||
} else {
|
||||
this.formData.email = '';
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// this.existingCustomer$
|
||||
// .pipe(
|
||||
// takeUntil(this.onDestroy$),
|
||||
// switchMap((info) => {
|
||||
// if (info) {
|
||||
// return this.customerService.getCustomer(info.id, 2).pipe(
|
||||
// map((res) => res.result),
|
||||
// catchError((err) => NEVER),
|
||||
// );
|
||||
// }
|
||||
// return NEVER;
|
||||
// }),
|
||||
// withLatestFrom(this.processId$),
|
||||
// )
|
||||
// .subscribe(([customer, processId]) => {
|
||||
// if (customer) {
|
||||
// this.modal
|
||||
// .open({
|
||||
// content: WebshopCustomnerAlreadyExistsModalComponent,
|
||||
// data: {
|
||||
// customer,
|
||||
// processId,
|
||||
// } as WebshopCustomnerAlreadyExistsModalData,
|
||||
// title: 'Es existiert bereits ein Onlinekonto mit dieser E-Mail-Adresse',
|
||||
// })
|
||||
// .afterClosed$.subscribe(async (result: UiModalResult<boolean>) => {
|
||||
// if (result.data) {
|
||||
// this.navigateToUpdatePage(customer);
|
||||
// } else {
|
||||
// this.formData.email = '';
|
||||
// this.cdr.markForCheck();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
async navigateToUpdatePage(customer: CustomerDTO) {
|
||||
const processId = await this.processId$.pipe(first()).toPromise();
|
||||
this.router.navigate(['/kunde', processId, 'customer', 'create', 'webshop-p4m', 'update'], {
|
||||
queryParams: {
|
||||
formData: encodeFormData({
|
||||
...mapCustomerDtoToCustomerCreateFormData(customer),
|
||||
p4m: this.formData.p4m,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
// async navigateToUpdatePage(customer: CustomerDTO) {
|
||||
// const processId = await this.processId$.pipe(first()).toPromise();
|
||||
// this.router.navigate(['/kunde', processId, 'customer', 'create', 'webshop-p4m', 'update'], {
|
||||
// queryParams: {
|
||||
// formData: encodeFormData({
|
||||
// ...mapCustomerDtoToCustomerCreateFormData(customer),
|
||||
// p4m: this.formData.p4m,
|
||||
// }),
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
initMarksAndValidators() {
|
||||
this.asyncLoyaltyCardValidatorFn = [this.checkLoyalityCardValidator];
|
||||
this.birthDateValidatorFns = [Validators.required, this.minBirthDateValidator()];
|
||||
if (this._customerType === 'webshop') {
|
||||
this.emailRequiredMark = true;
|
||||
this.emailValidatorFn = [Validators.required, Validators.email, validateEmail];
|
||||
this.asyncEmailVlaidtorFn = [this.emailExistsValidator];
|
||||
this.addressRequiredMarks = this.shippingAddressRequiredMarks;
|
||||
this.addressValidatorFns = this.shippingAddressValidators;
|
||||
} else {
|
||||
this.emailRequiredMark = false;
|
||||
this.emailValidatorFn = [Validators.email, validateEmail];
|
||||
}
|
||||
}
|
||||
// initMarksAndValidators() {
|
||||
// this.asyncLoyaltyCardValidatorFn = [this.checkLoyalityCardValidator];
|
||||
// this.birthDateValidatorFns = [Validators.required, this.minBirthDateValidator()];
|
||||
// if (this._customerType === 'webshop') {
|
||||
// this.emailRequiredMark = true;
|
||||
// this.emailValidatorFn = [Validators.required, Validators.email, validateEmail];
|
||||
// this.asyncEmailVlaidtorFn = [this.emailExistsValidator];
|
||||
// this.addressRequiredMarks = this.shippingAddressRequiredMarks;
|
||||
// this.addressValidatorFns = this.shippingAddressValidators;
|
||||
// } else {
|
||||
// this.emailRequiredMark = false;
|
||||
// this.emailValidatorFn = [Validators.email, validateEmail];
|
||||
// }
|
||||
// }
|
||||
|
||||
fetchCustomerInfo(): Observable<CustomerDTO | null> {
|
||||
const email = this.formData.email;
|
||||
return this.customerService.getOnlineCustomerByEmail(email).pipe(
|
||||
map((result) => {
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.modal.open({
|
||||
content: UiErrorModalComponent,
|
||||
data: err,
|
||||
});
|
||||
return [null];
|
||||
}),
|
||||
);
|
||||
}
|
||||
// fetchCustomerInfo(): Observable<CustomerDTO | null> {
|
||||
// const email = this.formData.email;
|
||||
// return this.customerService.getOnlineCustomerByEmail(email).pipe(
|
||||
// map((result) => {
|
||||
// if (result) {
|
||||
// return result;
|
||||
// }
|
||||
// return null;
|
||||
// }),
|
||||
// catchError((err) => {
|
||||
// this.modal.open({
|
||||
// content: UiErrorModalComponent,
|
||||
// data: err,
|
||||
// });
|
||||
// return [null];
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
|
||||
getInterests(): KeyValueDTOOfStringAndString[] {
|
||||
const interests: KeyValueDTOOfStringAndString[] = [];
|
||||
// getInterests(): KeyValueDTOOfStringAndString[] {
|
||||
// const interests: KeyValueDTOOfStringAndString[] = [];
|
||||
|
||||
for (const key in this.formData.interests) {
|
||||
if (this.formData.interests[key]) {
|
||||
interests.push({ key, group: 'KUBI_INTERESSEN' });
|
||||
}
|
||||
}
|
||||
// for (const key in this.formData.interests) {
|
||||
// if (this.formData.interests[key]) {
|
||||
// interests.push({ key, group: 'KUBI_INTERESSEN' });
|
||||
// }
|
||||
// }
|
||||
|
||||
return interests;
|
||||
}
|
||||
// return interests;
|
||||
// }
|
||||
|
||||
getNewsletter(): KeyValueDTOOfStringAndString | undefined {
|
||||
if (this.formData.newsletter) {
|
||||
return { key: 'kubi_newsletter', group: 'KUBI_NEWSLETTER' };
|
||||
}
|
||||
}
|
||||
// getNewsletter(): KeyValueDTOOfStringAndString | undefined {
|
||||
// if (this.formData.newsletter) {
|
||||
// return { key: 'kubi_newsletter', group: 'KUBI_NEWSLETTER' };
|
||||
// }
|
||||
// }
|
||||
|
||||
static MapCustomerInfoDtoToCustomerDto(customerInfoDto: CustomerInfoDTO): CustomerDTO {
|
||||
return {
|
||||
address: customerInfoDto.address,
|
||||
agentComment: customerInfoDto.agentComment,
|
||||
bonusCard: customerInfoDto.bonusCard,
|
||||
campaignCode: customerInfoDto.campaignCode,
|
||||
communicationDetails: customerInfoDto.communicationDetails,
|
||||
createdInBranch: customerInfoDto.createdInBranch,
|
||||
customerGroup: customerInfoDto.customerGroup,
|
||||
customerNumber: customerInfoDto.customerNumber,
|
||||
customerStatus: customerInfoDto.customerStatus,
|
||||
customerType: customerInfoDto.customerType,
|
||||
dateOfBirth: customerInfoDto.dateOfBirth,
|
||||
features: customerInfoDto.features,
|
||||
firstName: customerInfoDto.firstName,
|
||||
lastName: customerInfoDto.lastName,
|
||||
gender: customerInfoDto.gender,
|
||||
hasOnlineAccount: customerInfoDto.hasOnlineAccount,
|
||||
isGuestAccount: customerInfoDto.isGuestAccount,
|
||||
label: customerInfoDto.label,
|
||||
notificationChannels: customerInfoDto.notificationChannels,
|
||||
organisation: customerInfoDto.organisation,
|
||||
title: customerInfoDto.title,
|
||||
id: customerInfoDto.id,
|
||||
pId: customerInfoDto.pId,
|
||||
};
|
||||
}
|
||||
// static MapCustomerInfoDtoToCustomerDto(customerInfoDto: CustomerInfoDTO): CustomerDTO {
|
||||
// return {
|
||||
// address: customerInfoDto.address,
|
||||
// agentComment: customerInfoDto.agentComment,
|
||||
// bonusCard: customerInfoDto.bonusCard,
|
||||
// campaignCode: customerInfoDto.campaignCode,
|
||||
// communicationDetails: customerInfoDto.communicationDetails,
|
||||
// createdInBranch: customerInfoDto.createdInBranch,
|
||||
// customerGroup: customerInfoDto.customerGroup,
|
||||
// customerNumber: customerInfoDto.customerNumber,
|
||||
// customerStatus: customerInfoDto.customerStatus,
|
||||
// customerType: customerInfoDto.customerType,
|
||||
// dateOfBirth: customerInfoDto.dateOfBirth,
|
||||
// features: customerInfoDto.features,
|
||||
// firstName: customerInfoDto.firstName,
|
||||
// lastName: customerInfoDto.lastName,
|
||||
// gender: customerInfoDto.gender,
|
||||
// hasOnlineAccount: customerInfoDto.hasOnlineAccount,
|
||||
// isGuestAccount: customerInfoDto.isGuestAccount,
|
||||
// label: customerInfoDto.label,
|
||||
// notificationChannels: customerInfoDto.notificationChannels,
|
||||
// organisation: customerInfoDto.organisation,
|
||||
// title: customerInfoDto.title,
|
||||
// id: customerInfoDto.id,
|
||||
// pId: customerInfoDto.pId,
|
||||
// };
|
||||
// }
|
||||
|
||||
async saveCustomer(customer: CustomerDTO): Promise<CustomerDTO> {
|
||||
const isWebshop = this._customerType === 'webshop';
|
||||
let res: Result<CustomerDTO>;
|
||||
// async saveCustomer(customer: CustomerDTO): Promise<CustomerDTO> {
|
||||
// const isWebshop = this._customerType === 'webshop';
|
||||
// let res: Result<CustomerDTO>;
|
||||
|
||||
const { customerDto, customerInfoDto } = this.formData?._meta ?? {};
|
||||
// const { customerDto, customerInfoDto } = this.formData?._meta ?? {};
|
||||
|
||||
if (customerDto) {
|
||||
customer = { ...customerDto, ...customer };
|
||||
} else if (customerInfoDto) {
|
||||
customer = { ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto), ...customer };
|
||||
}
|
||||
// if (customerDto) {
|
||||
// customer = { ...customerDto, ...customer };
|
||||
// } else if (customerInfoDto) {
|
||||
// customer = { ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto), ...customer };
|
||||
// }
|
||||
|
||||
const p4mFeature = customer.features?.find((attr) => attr.key === 'p4mUser');
|
||||
if (p4mFeature) {
|
||||
p4mFeature.value = this.formData.p4m;
|
||||
} else {
|
||||
customer.features.push({
|
||||
key: 'p4mUser',
|
||||
value: this.formData.p4m,
|
||||
});
|
||||
}
|
||||
// const p4mFeature = customer.features?.find((attr) => attr.key === 'p4mUser');
|
||||
// if (p4mFeature) {
|
||||
// p4mFeature.value = this.formData.p4m;
|
||||
// } else {
|
||||
// customer.features.push({
|
||||
// key: 'p4mUser',
|
||||
// value: this.formData.p4m,
|
||||
// });
|
||||
// }
|
||||
|
||||
const interests = this.getInterests();
|
||||
// const interests = this.getInterests();
|
||||
|
||||
if (interests.length > 0) {
|
||||
customer.features?.push(...interests);
|
||||
// TODO: Klärung wie Interessen zukünftig gespeichert werden
|
||||
// await this._loyaltyCardService
|
||||
// .LoyaltyCardSaveInteressen({
|
||||
// customerId: res.result.id,
|
||||
// interessen: this.getInterests(),
|
||||
// })
|
||||
// .toPromise();
|
||||
}
|
||||
// if (interests.length > 0) {
|
||||
// customer.features?.push(...interests);
|
||||
// // TODO: Klärung wie Interessen zukünftig gespeichert werden
|
||||
// // await this._loyaltyCardService
|
||||
// // .LoyaltyCardSaveInteressen({
|
||||
// // customerId: res.result.id,
|
||||
// // interessen: this.getInterests(),
|
||||
// // })
|
||||
// // .toPromise();
|
||||
// }
|
||||
|
||||
const newsletter = this.getNewsletter();
|
||||
// const newsletter = this.getNewsletter();
|
||||
|
||||
if (newsletter) {
|
||||
customer.features.push(newsletter);
|
||||
} else {
|
||||
customer.features = customer.features.filter(
|
||||
(feature) => feature.key !== 'kubi_newsletter' && feature.group !== 'KUBI_NEWSLETTER',
|
||||
);
|
||||
}
|
||||
// if (newsletter) {
|
||||
// customer.features.push(newsletter);
|
||||
// } else {
|
||||
// customer.features = customer.features.filter(
|
||||
// (feature) => feature.key !== 'kubi_newsletter' && feature.group !== 'KUBI_NEWSLETTER',
|
||||
// );
|
||||
// }
|
||||
|
||||
if (isWebshop) {
|
||||
if (customer.id > 0) {
|
||||
if (this.formData?._meta?.hasLocalityCard) {
|
||||
res = await this.customerService.updateStoreP4MToWebshopP4M(customer);
|
||||
} else {
|
||||
res = await this.customerService.updateToP4MOnlineCustomer(customer);
|
||||
}
|
||||
} else {
|
||||
res = await this.customerService.createOnlineCustomer(customer).toPromise();
|
||||
}
|
||||
} else {
|
||||
res = await this.customerService.createStoreCustomer(customer).toPromise();
|
||||
}
|
||||
// if (isWebshop) {
|
||||
// if (customer.id > 0) {
|
||||
// if (this.formData?._meta?.hasLocalityCard) {
|
||||
// res = await this.customerService.updateStoreP4MToWebshopP4M(customer);
|
||||
// } else {
|
||||
// res = await this.customerService.updateToP4MOnlineCustomer(customer);
|
||||
// }
|
||||
// } else {
|
||||
// res = await this.customerService.createOnlineCustomer(customer).toPromise();
|
||||
// }
|
||||
// } else {
|
||||
// res = await this.customerService.createStoreCustomer(customer).toPromise();
|
||||
// }
|
||||
|
||||
return res.result;
|
||||
}
|
||||
}
|
||||
// return res.result;
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
// import { NgModule } from '@angular/core';
|
||||
// import { CommonModule } from '@angular/common';
|
||||
|
||||
import { CreateP4MCustomerComponent } from './create-p4m-customer.component';
|
||||
import {
|
||||
AddressFormBlockModule,
|
||||
BirthDateFormBlockModule,
|
||||
InterestsFormBlockModule,
|
||||
NameFormBlockModule,
|
||||
OrganisationFormBlockModule,
|
||||
P4mNumberFormBlockModule,
|
||||
NewsletterFormBlockModule,
|
||||
DeviatingAddressFormBlockComponentModule,
|
||||
AcceptAGBFormBlockModule,
|
||||
EmailFormBlockModule,
|
||||
PhoneNumbersFormBlockModule,
|
||||
} from '../../components/form-blocks';
|
||||
import { CustomerTypeSelectorModule } from '../../components/customer-type-selector';
|
||||
import { UiSpinnerModule } from '@ui/spinner';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { RouterModule } from '@angular/router';
|
||||
// import { CreateP4MCustomerComponent } from './create-p4m-customer.component';
|
||||
// import {
|
||||
// AddressFormBlockModule,
|
||||
// BirthDateFormBlockModule,
|
||||
// InterestsFormBlockModule,
|
||||
// NameFormBlockModule,
|
||||
// OrganisationFormBlockModule,
|
||||
// P4mNumberFormBlockModule,
|
||||
// NewsletterFormBlockModule,
|
||||
// DeviatingAddressFormBlockComponentModule,
|
||||
// AcceptAGBFormBlockModule,
|
||||
// EmailFormBlockModule,
|
||||
// PhoneNumbersFormBlockModule,
|
||||
// } from '../../components/form-blocks';
|
||||
// import { CustomerTypeSelectorModule } from '../../components/customer-type-selector';
|
||||
// import { UiSpinnerModule } from '@ui/spinner';
|
||||
// import { UiIconModule } from '@ui/icon';
|
||||
// import { RouterModule } from '@angular/router';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
CustomerTypeSelectorModule,
|
||||
AddressFormBlockModule,
|
||||
BirthDateFormBlockModule,
|
||||
InterestsFormBlockModule,
|
||||
NameFormBlockModule,
|
||||
OrganisationFormBlockModule,
|
||||
P4mNumberFormBlockModule,
|
||||
NewsletterFormBlockModule,
|
||||
DeviatingAddressFormBlockComponentModule,
|
||||
AcceptAGBFormBlockModule,
|
||||
EmailFormBlockModule,
|
||||
PhoneNumbersFormBlockModule,
|
||||
UiSpinnerModule,
|
||||
UiIconModule,
|
||||
RouterModule,
|
||||
],
|
||||
exports: [CreateP4MCustomerComponent],
|
||||
declarations: [CreateP4MCustomerComponent],
|
||||
})
|
||||
export class CreateP4MCustomerModule {}
|
||||
// @NgModule({
|
||||
// imports: [
|
||||
// CommonModule,
|
||||
// CustomerTypeSelectorModule,
|
||||
// AddressFormBlockModule,
|
||||
// BirthDateFormBlockModule,
|
||||
// InterestsFormBlockModule,
|
||||
// NameFormBlockModule,
|
||||
// OrganisationFormBlockModule,
|
||||
// P4mNumberFormBlockModule,
|
||||
// NewsletterFormBlockModule,
|
||||
// DeviatingAddressFormBlockComponentModule,
|
||||
// AcceptAGBFormBlockModule,
|
||||
// EmailFormBlockModule,
|
||||
// PhoneNumbersFormBlockModule,
|
||||
// UiSpinnerModule,
|
||||
// UiIconModule,
|
||||
// RouterModule,
|
||||
// ],
|
||||
// exports: [CreateP4MCustomerComponent],
|
||||
// declarations: [CreateP4MCustomerComponent],
|
||||
// })
|
||||
// export class CreateP4MCustomerModule {}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './create-p4m-customer.component';
|
||||
export * from './create-p4m-customer.module';
|
||||
// export * from './create-p4m-customer.component';
|
||||
// export * from './create-p4m-customer.module';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, ChangeDetectionStrategy, ViewChild } from '@angular/core';
|
||||
import { ValidatorFn, Validators } from '@angular/forms';
|
||||
import { CustomerDTO } from '@generated/swagger/crm-api';
|
||||
import { CustomerDTO, CustomerInfoDTO } from '@generated/swagger/crm-api';
|
||||
import { map } from 'rxjs/operators';
|
||||
import {
|
||||
AddressFormBlockComponent,
|
||||
@@ -10,13 +10,16 @@ import {
|
||||
import { NameFormBlockData } from '../../components/form-blocks/name/name-form-block-data';
|
||||
import { validateEmail } from '../../validators/email-validator';
|
||||
import { AbstractCreateCustomer } from '../abstract-create-customer';
|
||||
import { CreateP4MCustomerComponent } from '../create-p4m-customer';
|
||||
// import { CreateP4MCustomerComponent } from '../create-p4m-customer';
|
||||
import { zipCodeValidator } from '../../validators/zip-code-validator';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-webshop-customer',
|
||||
templateUrl: 'create-webshop-customer.component.html',
|
||||
styleUrls: ['../create-customer.scss', 'create-webshop-customer.component.scss'],
|
||||
styleUrls: [
|
||||
'../create-customer.scss',
|
||||
'create-webshop-customer.component.scss',
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
@@ -26,7 +29,11 @@ export class CreateWebshopCustomerComponent extends AbstractCreateCustomer {
|
||||
validateAddress = true;
|
||||
validateShippingAddress = true;
|
||||
|
||||
nameRequiredMarks: (keyof NameFormBlockData)[] = ['gender', 'firstName', 'lastName'];
|
||||
nameRequiredMarks: (keyof NameFormBlockData)[] = [
|
||||
'gender',
|
||||
'firstName',
|
||||
'lastName',
|
||||
];
|
||||
|
||||
nameValidationFns: Record<keyof NameFormBlockData, ValidatorFn[]> = {
|
||||
firstName: [Validators.required],
|
||||
@@ -35,7 +42,13 @@ export class CreateWebshopCustomerComponent extends AbstractCreateCustomer {
|
||||
title: [],
|
||||
};
|
||||
|
||||
addressRequiredMarks: (keyof AddressFormBlockData)[] = ['street', 'streetNumber', 'zipCode', 'city', 'country'];
|
||||
addressRequiredMarks: (keyof AddressFormBlockData)[] = [
|
||||
'street',
|
||||
'streetNumber',
|
||||
'zipCode',
|
||||
'city',
|
||||
'country',
|
||||
];
|
||||
|
||||
addressValidators: Record<string, ValidatorFn[]> = {
|
||||
street: [Validators.required],
|
||||
@@ -68,7 +81,11 @@ export class CreateWebshopCustomerComponent extends AbstractCreateCustomer {
|
||||
if (customerDto) {
|
||||
customer = { ...customerDto, ...customer };
|
||||
} else if (customerInfoDto) {
|
||||
customer = { ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto), ...customer };
|
||||
customer = {
|
||||
// ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto),
|
||||
...this.mapCustomerInfoDtoToCustomerDto(customerInfoDto),
|
||||
...customer,
|
||||
};
|
||||
}
|
||||
|
||||
const res = await this.customerService.updateToOnlineCustomer(customer);
|
||||
@@ -80,4 +97,34 @@ export class CreateWebshopCustomerComponent extends AbstractCreateCustomer {
|
||||
.toPromise();
|
||||
}
|
||||
}
|
||||
|
||||
mapCustomerInfoDtoToCustomerDto(
|
||||
customerInfoDto: CustomerInfoDTO,
|
||||
): CustomerDTO {
|
||||
return {
|
||||
address: customerInfoDto.address,
|
||||
agentComment: customerInfoDto.agentComment,
|
||||
bonusCard: customerInfoDto.bonusCard,
|
||||
campaignCode: customerInfoDto.campaignCode,
|
||||
communicationDetails: customerInfoDto.communicationDetails,
|
||||
createdInBranch: customerInfoDto.createdInBranch,
|
||||
customerGroup: customerInfoDto.customerGroup,
|
||||
customerNumber: customerInfoDto.customerNumber,
|
||||
customerStatus: customerInfoDto.customerStatus,
|
||||
customerType: customerInfoDto.customerType,
|
||||
dateOfBirth: customerInfoDto.dateOfBirth,
|
||||
features: customerInfoDto.features,
|
||||
firstName: customerInfoDto.firstName,
|
||||
lastName: customerInfoDto.lastName,
|
||||
gender: customerInfoDto.gender,
|
||||
hasOnlineAccount: customerInfoDto.hasOnlineAccount,
|
||||
isGuestAccount: customerInfoDto.isGuestAccount,
|
||||
label: customerInfoDto.label,
|
||||
notificationChannels: customerInfoDto.notificationChannels,
|
||||
organisation: customerInfoDto.organisation,
|
||||
title: customerInfoDto.title,
|
||||
id: customerInfoDto.id,
|
||||
pId: customerInfoDto.pId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CustomerDTO, Gender } from '@generated/swagger/crm-api';
|
||||
|
||||
export interface CreateCustomerQueryParams {
|
||||
p4mNumber?: string;
|
||||
// p4mNumber?: string;
|
||||
customerId?: number;
|
||||
gender?: Gender;
|
||||
title?: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from './create-b2b-customer';
|
||||
export * from './create-guest-customer';
|
||||
export * from './create-p4m-customer';
|
||||
// export * from './create-p4m-customer';
|
||||
export * from './create-store-customer';
|
||||
export * from './create-webshop-customer';
|
||||
export * from './defs';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@if (formData$ | async; as data) {
|
||||
<!-- @if (formData$ | async; as data) {
|
||||
<form (keydown.enter)="$event.preventDefault()">
|
||||
<h1 class="title flex flex-row items-center justify-center">Kundenkartendaten erfasen</h1>
|
||||
<p class="description">Bitte erfassen Sie die Kundenkarte</p>
|
||||
@@ -106,4 +106,4 @@
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
} -->
|
||||
|
||||
@@ -1,156 +1,156 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
|
||||
import { AsyncValidatorFn, ValidatorFn, Validators } from '@angular/forms';
|
||||
import { Result } from '@domain/defs';
|
||||
import { CustomerDTO, KeyValueDTOOfStringAndString, PayerDTO } from '@generated/swagger/crm-api';
|
||||
import { AddressFormBlockData } from '../../components/form-blocks';
|
||||
import { NameFormBlockData } from '../../components/form-blocks/name/name-form-block-data';
|
||||
import { AbstractCreateCustomer } from '../abstract-create-customer';
|
||||
import { CreateP4MCustomerComponent } from '../create-p4m-customer';
|
||||
// import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
|
||||
// import { AsyncValidatorFn, ValidatorFn, Validators } from '@angular/forms';
|
||||
// import { Result } from '@domain/defs';
|
||||
// import { CustomerDTO, KeyValueDTOOfStringAndString, PayerDTO } from '@generated/swagger/crm-api';
|
||||
// import { AddressFormBlockData } from '../../components/form-blocks';
|
||||
// import { NameFormBlockData } from '../../components/form-blocks/name/name-form-block-data';
|
||||
// import { AbstractCreateCustomer } from '../abstract-create-customer';
|
||||
// import { CreateP4MCustomerComponent } from '../create-p4m-customer';
|
||||
|
||||
@Component({
|
||||
selector: 'page-update-p4m-webshop-customer',
|
||||
templateUrl: 'update-p4m-webshop-customer.component.html',
|
||||
styleUrls: ['../create-customer.scss', 'update-p4m-webshop-customer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class UpdateP4MWebshopCustomerComponent extends AbstractCreateCustomer implements OnInit {
|
||||
customerType = 'webshop-p4m/update';
|
||||
// @Component({
|
||||
// selector: 'page-update-p4m-webshop-customer',
|
||||
// templateUrl: 'update-p4m-webshop-customer.component.html',
|
||||
// styleUrls: ['../create-customer.scss', 'update-p4m-webshop-customer.component.scss'],
|
||||
// changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
// standalone: false,
|
||||
// })
|
||||
// export class UpdateP4MWebshopCustomerComponent extends AbstractCreateCustomer implements OnInit {
|
||||
// customerType = 'webshop-p4m/update';
|
||||
|
||||
validateAddress = true;
|
||||
// validateAddress = true;
|
||||
|
||||
validateShippingAddress = true;
|
||||
// validateShippingAddress = true;
|
||||
|
||||
agbValidatorFns = [Validators.requiredTrue];
|
||||
// agbValidatorFns = [Validators.requiredTrue];
|
||||
|
||||
birthDateValidatorFns = [Validators.required, this.minBirthDateValidator()];
|
||||
// birthDateValidatorFns = [Validators.required, this.minBirthDateValidator()];
|
||||
|
||||
nameRequiredMarks: (keyof NameFormBlockData)[] = ['gender', 'firstName', 'lastName'];
|
||||
// nameRequiredMarks: (keyof NameFormBlockData)[] = ['gender', 'firstName', 'lastName'];
|
||||
|
||||
nameValidationFns: Record<keyof NameFormBlockData, ValidatorFn[]> = {
|
||||
firstName: [Validators.required],
|
||||
lastName: [Validators.required],
|
||||
gender: [Validators.required],
|
||||
title: [],
|
||||
};
|
||||
// nameValidationFns: Record<keyof NameFormBlockData, ValidatorFn[]> = {
|
||||
// firstName: [Validators.required],
|
||||
// lastName: [Validators.required],
|
||||
// gender: [Validators.required],
|
||||
// title: [],
|
||||
// };
|
||||
|
||||
addressRequiredMarks: (keyof AddressFormBlockData)[] = ['street', 'streetNumber', 'zipCode', 'city', 'country'];
|
||||
// addressRequiredMarks: (keyof AddressFormBlockData)[] = ['street', 'streetNumber', 'zipCode', 'city', 'country'];
|
||||
|
||||
addressValidatorFns: Record<string, ValidatorFn[]> = {
|
||||
street: [Validators.required],
|
||||
streetNumber: [Validators.required],
|
||||
zipCode: [Validators.required],
|
||||
city: [Validators.required],
|
||||
country: [Validators.required],
|
||||
};
|
||||
// addressValidatorFns: Record<string, ValidatorFn[]> = {
|
||||
// street: [Validators.required],
|
||||
// streetNumber: [Validators.required],
|
||||
// zipCode: [Validators.required],
|
||||
// city: [Validators.required],
|
||||
// country: [Validators.required],
|
||||
// };
|
||||
|
||||
asyncLoyaltyCardValidatorFn: AsyncValidatorFn[] = [this.checkLoyalityCardValidator];
|
||||
// asyncLoyaltyCardValidatorFn: AsyncValidatorFn[] = [this.checkLoyalityCardValidator];
|
||||
|
||||
get billingAddress(): PayerDTO | undefined {
|
||||
const payers = this.formData?._meta?.customerDto?.payers;
|
||||
// get billingAddress(): PayerDTO | undefined {
|
||||
// const payers = this.formData?._meta?.customerDto?.payers;
|
||||
|
||||
if (!payers || payers.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
// if (!payers || payers.length === 0) {
|
||||
// return undefined;
|
||||
// }
|
||||
|
||||
// the default payer is the payer with the latest isDefault(Date) value
|
||||
const defaultPayer = payers.reduce((prev, curr) =>
|
||||
new Date(prev.isDefault) > new Date(curr.isDefault) ? prev : curr,
|
||||
);
|
||||
// // the default payer is the payer with the latest isDefault(Date) value
|
||||
// const defaultPayer = payers.reduce((prev, curr) =>
|
||||
// new Date(prev.isDefault) > new Date(curr.isDefault) ? prev : curr,
|
||||
// );
|
||||
|
||||
return defaultPayer.payer.data;
|
||||
}
|
||||
// return defaultPayer.payer.data;
|
||||
// }
|
||||
|
||||
get shippingAddress() {
|
||||
const shippingAddresses = this.formData?._meta?.customerDto?.shippingAddresses;
|
||||
// get shippingAddress() {
|
||||
// const shippingAddresses = this.formData?._meta?.customerDto?.shippingAddresses;
|
||||
|
||||
if (!shippingAddresses || shippingAddresses.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
// if (!shippingAddresses || shippingAddresses.length === 0) {
|
||||
// return undefined;
|
||||
// }
|
||||
|
||||
// the default shipping address is the shipping address with the latest isDefault(Date) value
|
||||
const defaultShippingAddress = shippingAddresses.reduce((prev, curr) =>
|
||||
new Date(prev.data.isDefault) > new Date(curr.data.isDefault) ? prev : curr,
|
||||
);
|
||||
// // the default shipping address is the shipping address with the latest isDefault(Date) value
|
||||
// const defaultShippingAddress = shippingAddresses.reduce((prev, curr) =>
|
||||
// new Date(prev.data.isDefault) > new Date(curr.data.isDefault) ? prev : curr,
|
||||
// );
|
||||
|
||||
return defaultShippingAddress.data;
|
||||
}
|
||||
// return defaultShippingAddress.data;
|
||||
// }
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
}
|
||||
// ngOnInit() {
|
||||
// super.ngOnInit();
|
||||
// }
|
||||
|
||||
getInterests(): KeyValueDTOOfStringAndString[] {
|
||||
const interests: KeyValueDTOOfStringAndString[] = [];
|
||||
// getInterests(): KeyValueDTOOfStringAndString[] {
|
||||
// const interests: KeyValueDTOOfStringAndString[] = [];
|
||||
|
||||
for (const key in this.formData.interests) {
|
||||
if (this.formData.interests[key]) {
|
||||
interests.push({ key, group: 'KUBI_INTERESSEN' });
|
||||
}
|
||||
}
|
||||
// for (const key in this.formData.interests) {
|
||||
// if (this.formData.interests[key]) {
|
||||
// interests.push({ key, group: 'KUBI_INTERESSEN' });
|
||||
// }
|
||||
// }
|
||||
|
||||
return interests;
|
||||
}
|
||||
// return interests;
|
||||
// }
|
||||
|
||||
getNewsletter(): KeyValueDTOOfStringAndString | undefined {
|
||||
if (this.formData.newsletter) {
|
||||
return { key: 'kubi_newsletter', group: 'KUBI_NEWSLETTER' };
|
||||
}
|
||||
}
|
||||
// getNewsletter(): KeyValueDTOOfStringAndString | undefined {
|
||||
// if (this.formData.newsletter) {
|
||||
// return { key: 'kubi_newsletter', group: 'KUBI_NEWSLETTER' };
|
||||
// }
|
||||
// }
|
||||
|
||||
async saveCustomer(customer: CustomerDTO): Promise<CustomerDTO> {
|
||||
let res: Result<CustomerDTO>;
|
||||
// async saveCustomer(customer: CustomerDTO): Promise<CustomerDTO> {
|
||||
// let res: Result<CustomerDTO>;
|
||||
|
||||
const { customerDto, customerInfoDto } = this.formData?._meta ?? {};
|
||||
// const { customerDto, customerInfoDto } = this.formData?._meta ?? {};
|
||||
|
||||
if (customerDto) {
|
||||
customer = { ...customerDto, shippingAddresses: [], payers: [], ...customer };
|
||||
// if (customerDto) {
|
||||
// customer = { ...customerDto, shippingAddresses: [], payers: [], ...customer };
|
||||
|
||||
if (customerDto.shippingAddresses?.length) {
|
||||
customer.shippingAddresses.unshift(...customerDto.shippingAddresses);
|
||||
}
|
||||
if (customerDto.payers?.length) {
|
||||
customer.payers.unshift(...customerDto.payers);
|
||||
}
|
||||
} else if (customerInfoDto) {
|
||||
customer = { ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto), ...customer };
|
||||
}
|
||||
// if (customerDto.shippingAddresses?.length) {
|
||||
// customer.shippingAddresses.unshift(...customerDto.shippingAddresses);
|
||||
// }
|
||||
// if (customerDto.payers?.length) {
|
||||
// customer.payers.unshift(...customerDto.payers);
|
||||
// }
|
||||
// } else if (customerInfoDto) {
|
||||
// customer = { ...CreateP4MCustomerComponent.MapCustomerInfoDtoToCustomerDto(customerInfoDto), ...customer };
|
||||
// }
|
||||
|
||||
const p4mFeature = customer.features?.find((attr) => attr.key === 'p4mUser');
|
||||
if (p4mFeature) {
|
||||
p4mFeature.value = this.formData.p4m;
|
||||
} else {
|
||||
customer.features.push({
|
||||
key: 'p4mUser',
|
||||
value: this.formData.p4m,
|
||||
});
|
||||
}
|
||||
// const p4mFeature = customer.features?.find((attr) => attr.key === 'p4mUser');
|
||||
// if (p4mFeature) {
|
||||
// p4mFeature.value = this.formData.p4m;
|
||||
// } else {
|
||||
// customer.features.push({
|
||||
// key: 'p4mUser',
|
||||
// value: this.formData.p4m,
|
||||
// });
|
||||
// }
|
||||
|
||||
const interests = this.getInterests();
|
||||
// const interests = this.getInterests();
|
||||
|
||||
if (interests.length > 0) {
|
||||
customer.features?.push(...interests);
|
||||
// TODO: Klärung wie Interessen zukünftig gespeichert werden
|
||||
// await this._loyaltyCardService
|
||||
// .LoyaltyCardSaveInteressen({
|
||||
// customerId: res.result.id,
|
||||
// interessen: this.getInterests(),
|
||||
// })
|
||||
// .toPromise();
|
||||
}
|
||||
// if (interests.length > 0) {
|
||||
// customer.features?.push(...interests);
|
||||
// // TODO: Klärung wie Interessen zukünftig gespeichert werden
|
||||
// // await this._loyaltyCardService
|
||||
// // .LoyaltyCardSaveInteressen({
|
||||
// // customerId: res.result.id,
|
||||
// // interessen: this.getInterests(),
|
||||
// // })
|
||||
// // .toPromise();
|
||||
// }
|
||||
|
||||
const newsletter = this.getNewsletter();
|
||||
// const newsletter = this.getNewsletter();
|
||||
|
||||
if (newsletter) {
|
||||
customer.features.push(newsletter);
|
||||
} else {
|
||||
customer.features = customer.features.filter(
|
||||
(feature) => feature.key !== 'kubi_newsletter' && feature.group !== 'KUBI_NEWSLETTER',
|
||||
);
|
||||
}
|
||||
// if (newsletter) {
|
||||
// customer.features.push(newsletter);
|
||||
// } else {
|
||||
// customer.features = customer.features.filter(
|
||||
// (feature) => feature.key !== 'kubi_newsletter' && feature.group !== 'KUBI_NEWSLETTER',
|
||||
// );
|
||||
// }
|
||||
|
||||
res = await this.customerService.updateToP4MOnlineCustomer(customer);
|
||||
// res = await this.customerService.updateToP4MOnlineCustomer(customer);
|
||||
|
||||
return res.result;
|
||||
}
|
||||
}
|
||||
// return res.result;
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
// import { NgModule } from '@angular/core';
|
||||
// import { CommonModule } from '@angular/common';
|
||||
|
||||
import { UpdateP4MWebshopCustomerComponent } from './update-p4m-webshop-customer.component';
|
||||
// import { UpdateP4MWebshopCustomerComponent } from './update-p4m-webshop-customer.component';
|
||||
|
||||
import {
|
||||
AddressFormBlockModule,
|
||||
BirthDateFormBlockModule,
|
||||
InterestsFormBlockModule,
|
||||
NameFormBlockModule,
|
||||
OrganisationFormBlockModule,
|
||||
P4mNumberFormBlockModule,
|
||||
NewsletterFormBlockModule,
|
||||
DeviatingAddressFormBlockComponentModule,
|
||||
AcceptAGBFormBlockModule,
|
||||
EmailFormBlockModule,
|
||||
PhoneNumbersFormBlockModule,
|
||||
} from '../../components/form-blocks';
|
||||
import { UiFormControlModule } from '@ui/form-control';
|
||||
import { UiInputModule } from '@ui/input';
|
||||
import { CustomerPipesModule } from '@shared/pipes/customer';
|
||||
import { CustomerTypeSelectorModule } from '../../components/customer-type-selector';
|
||||
import { UiSpinnerModule } from '@ui/spinner';
|
||||
// import {
|
||||
// AddressFormBlockModule,
|
||||
// BirthDateFormBlockModule,
|
||||
// InterestsFormBlockModule,
|
||||
// NameFormBlockModule,
|
||||
// OrganisationFormBlockModule,
|
||||
// P4mNumberFormBlockModule,
|
||||
// NewsletterFormBlockModule,
|
||||
// DeviatingAddressFormBlockComponentModule,
|
||||
// AcceptAGBFormBlockModule,
|
||||
// EmailFormBlockModule,
|
||||
// PhoneNumbersFormBlockModule,
|
||||
// } from '../../components/form-blocks';
|
||||
// import { UiFormControlModule } from '@ui/form-control';
|
||||
// import { UiInputModule } from '@ui/input';
|
||||
// import { CustomerPipesModule } from '@shared/pipes/customer';
|
||||
// import { CustomerTypeSelectorModule } from '../../components/customer-type-selector';
|
||||
// import { UiSpinnerModule } from '@ui/spinner';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
CustomerTypeSelectorModule,
|
||||
AddressFormBlockModule,
|
||||
BirthDateFormBlockModule,
|
||||
InterestsFormBlockModule,
|
||||
NameFormBlockModule,
|
||||
OrganisationFormBlockModule,
|
||||
P4mNumberFormBlockModule,
|
||||
NewsletterFormBlockModule,
|
||||
DeviatingAddressFormBlockComponentModule,
|
||||
AcceptAGBFormBlockModule,
|
||||
EmailFormBlockModule,
|
||||
PhoneNumbersFormBlockModule,
|
||||
UiFormControlModule,
|
||||
UiInputModule,
|
||||
CustomerPipesModule,
|
||||
UiSpinnerModule,
|
||||
],
|
||||
exports: [UpdateP4MWebshopCustomerComponent],
|
||||
declarations: [UpdateP4MWebshopCustomerComponent],
|
||||
})
|
||||
export class UpdateP4MWebshopCustomerModule {}
|
||||
// @NgModule({
|
||||
// imports: [
|
||||
// CommonModule,
|
||||
// CustomerTypeSelectorModule,
|
||||
// AddressFormBlockModule,
|
||||
// BirthDateFormBlockModule,
|
||||
// InterestsFormBlockModule,
|
||||
// NameFormBlockModule,
|
||||
// OrganisationFormBlockModule,
|
||||
// P4mNumberFormBlockModule,
|
||||
// NewsletterFormBlockModule,
|
||||
// DeviatingAddressFormBlockComponentModule,
|
||||
// AcceptAGBFormBlockModule,
|
||||
// EmailFormBlockModule,
|
||||
// PhoneNumbersFormBlockModule,
|
||||
// UiFormControlModule,
|
||||
// UiInputModule,
|
||||
// CustomerPipesModule,
|
||||
// UiSpinnerModule,
|
||||
// ],
|
||||
// exports: [UpdateP4MWebshopCustomerComponent],
|
||||
// declarations: [UpdateP4MWebshopCustomerComponent],
|
||||
// })
|
||||
// export class UpdateP4MWebshopCustomerModule {}
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, inject, effect, untracked } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
inject,
|
||||
effect,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { BehaviorSubject, Subject, Subscription, fromEvent } from 'rxjs';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Subject,
|
||||
Subscription,
|
||||
firstValueFrom,
|
||||
fromEvent,
|
||||
} from 'rxjs';
|
||||
import { CustomerSearchStore } from './store/customer-search.store';
|
||||
import { provideComponentStore } from '@ngrx/component-store';
|
||||
import { Breadcrumb, BreadcrumbService } from '@core/breadcrumb';
|
||||
import { delay, filter, first, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { CustomerCreateNavigation, CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import {
|
||||
CustomerCreateNavigation,
|
||||
CustomerSearchNavigation,
|
||||
} from '@shared/services/navigation';
|
||||
import { CustomerSearchMainAutocompleteProvider } from './providers/customer-search-main-autocomplete.provider';
|
||||
import { FilterAutocompleteProvider } from '@shared/components/filter';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { provideCancelSearchSubject } from '@shared/services/cancel-subject';
|
||||
import { injectFeedbackErrorDialog } from '@isa/ui/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'page-customer-search',
|
||||
@@ -28,6 +46,7 @@ import { provideCancelSearchSubject } from '@shared/services/cancel-subject';
|
||||
standalone: false,
|
||||
})
|
||||
export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
#errorFeedbackDialog = injectFeedbackErrorDialog();
|
||||
private _store = inject(CustomerSearchStore);
|
||||
private _activatedRoute = inject(ActivatedRoute);
|
||||
private _router = inject(Router);
|
||||
@@ -37,7 +56,11 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
|
||||
private searchStore = inject(CustomerSearchStore);
|
||||
|
||||
keyEscPressed = toSignal(fromEvent(document, 'keydown').pipe(filter((e: KeyboardEvent) => e.key === 'Escape')));
|
||||
keyEscPressed = toSignal(
|
||||
fromEvent(document, 'keydown').pipe(
|
||||
filter((e: KeyboardEvent) => e.key === 'Escape'),
|
||||
),
|
||||
);
|
||||
|
||||
get breadcrumb() {
|
||||
let breadcrumb: string;
|
||||
@@ -53,7 +76,9 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
|
||||
private _breadcrumbs$ = this._store.processId$.pipe(
|
||||
filter((id) => !!id),
|
||||
switchMap((id) => this._breadcrumbService.getBreadcrumbsByKeyAndTag$(id, 'customer')),
|
||||
switchMap((id) =>
|
||||
this._breadcrumbService.getBreadcrumbsByKeyAndTag$(id, 'customer'),
|
||||
),
|
||||
);
|
||||
|
||||
side$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
@@ -97,53 +122,77 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
this.checkDetailsBreadcrumb();
|
||||
});
|
||||
|
||||
this._eventsSubscription = this._router.events.pipe(takeUntil(this._onDestroy$)).subscribe((event) => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
this.checkAndUpdateProcessId();
|
||||
this.checkAndUpdateSide();
|
||||
this.checkAndUpdateCustomerId();
|
||||
this.checkBreadcrumbs();
|
||||
}
|
||||
});
|
||||
|
||||
this._store.customerListResponse$
|
||||
this._eventsSubscription = this._router.events
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe(async ([response, filter, processId, restored, skipNavigation]) => {
|
||||
if (this._store.processId === processId) {
|
||||
if (skipNavigation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.hits === 1) {
|
||||
// Navigate to details page
|
||||
const customer = response.result[0];
|
||||
|
||||
if (customer.id < 0) {
|
||||
// navigate to create customer
|
||||
const route = this._createNavigation.upgradeCustomerRoute({ processId, customerInfo: customer });
|
||||
await this._router.navigate(route.path, { queryParams: route.queryParams });
|
||||
return;
|
||||
} else {
|
||||
const route = this._navigation.detailsRoute({ processId, customerId: customer.id });
|
||||
await this._router.navigate(route.path, { queryParams: filter.getQueryParams() });
|
||||
}
|
||||
} else if (response.hits > 1) {
|
||||
const route = this._navigation.listRoute({ processId, filter });
|
||||
|
||||
if (
|
||||
(['details'].includes(this.breadcrumb) &&
|
||||
response?.result?.some((c) => c.id === this._store.customerId)) ||
|
||||
restored
|
||||
) {
|
||||
await this._router.navigate([], { queryParams: route.queryParams });
|
||||
} else {
|
||||
await this._router.navigate(route.path, { queryParams: route.queryParams });
|
||||
}
|
||||
}
|
||||
|
||||
.subscribe((event) => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
this.checkAndUpdateProcessId();
|
||||
this.checkAndUpdateSide();
|
||||
this.checkAndUpdateCustomerId();
|
||||
this.checkBreadcrumbs();
|
||||
}
|
||||
});
|
||||
|
||||
this._store.customerListResponse$
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe(
|
||||
async ([response, filter, processId, restored, skipNavigation]) => {
|
||||
if (this._store.processId === processId) {
|
||||
if (skipNavigation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.hits === 1) {
|
||||
// Navigate to details page
|
||||
const customer = response.result[0];
|
||||
|
||||
if (customer.id < 0) {
|
||||
// #5375 - Zusätzlich soll bei Kunden bei denen ein Upgrade möglich ist ein Dialog angezeigt werden, dass Kundenneuanlage mit Kundenkarte nicht möglich ist
|
||||
await firstValueFrom(
|
||||
this.#errorFeedbackDialog({
|
||||
data: {
|
||||
errorMessage:
|
||||
'Kundenneuanlage mit Kundenkarte nicht möglich',
|
||||
},
|
||||
}).closed,
|
||||
);
|
||||
// navigate to create customer
|
||||
// const route = this._createNavigation.upgradeCustomerRoute({ processId, customerInfo: customer });
|
||||
// await this._router.navigate(route.path, { queryParams: route.queryParams });
|
||||
return;
|
||||
} else {
|
||||
const route = this._navigation.detailsRoute({
|
||||
processId,
|
||||
customerId: customer.id,
|
||||
});
|
||||
await this._router.navigate(route.path, {
|
||||
queryParams: filter.getQueryParams(),
|
||||
});
|
||||
}
|
||||
} else if (response.hits > 1) {
|
||||
const route = this._navigation.listRoute({ processId, filter });
|
||||
|
||||
if (
|
||||
(['details'].includes(this.breadcrumb) &&
|
||||
response?.result?.some(
|
||||
(c) => c.id === this._store.customerId,
|
||||
)) ||
|
||||
restored
|
||||
) {
|
||||
await this._router.navigate([], {
|
||||
queryParams: route.queryParams,
|
||||
});
|
||||
} else {
|
||||
await this._router.navigate(route.path, {
|
||||
queryParams: route.queryParams,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.checkBreadcrumbs();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -169,7 +218,11 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
this._store.setProcessId(processId);
|
||||
this._store.reset(this._activatedRoute.snapshot.queryParams);
|
||||
if (!['main', 'filter'].some((s) => s === this.breadcrumb)) {
|
||||
const skipNavigation = ['orders', 'order-details', 'order-details-history'].includes(this.breadcrumb);
|
||||
const skipNavigation = [
|
||||
'orders',
|
||||
'order-details',
|
||||
'order-details-history',
|
||||
].includes(this.breadcrumb);
|
||||
this._store.search({ skipNavigation });
|
||||
}
|
||||
}
|
||||
@@ -229,7 +282,9 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
const mainBreadcrumb = await this.getMainBreadcrumb();
|
||||
|
||||
if (!mainBreadcrumb) {
|
||||
const navigation = this._navigation.defaultRoute({ processId: this._store.processId });
|
||||
const navigation = this._navigation.defaultRoute({
|
||||
processId: this._store.processId,
|
||||
});
|
||||
const breadcrumb: Breadcrumb = {
|
||||
key: this._store.processId,
|
||||
tags: ['customer', 'search', 'main'],
|
||||
@@ -242,14 +297,19 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
this._breadcrumbService.addBreadcrumb(breadcrumb);
|
||||
} else {
|
||||
this._breadcrumbService.patchBreadcrumb(mainBreadcrumb.id, {
|
||||
params: { ...this.snapshot.queryParams, ...(mainBreadcrumb.params ?? {}) },
|
||||
params: {
|
||||
...this.snapshot.queryParams,
|
||||
...(mainBreadcrumb.params ?? {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getCreateCustomerBreadcrumb(): Promise<Breadcrumb | undefined> {
|
||||
const breadcrumbs = await this.getBreadcrumbs();
|
||||
return breadcrumbs.find((b) => b.tags.includes('create') && b.tags.includes('customer'));
|
||||
return breadcrumbs.find(
|
||||
(b) => b.tags.includes('create') && b.tags.includes('customer'),
|
||||
);
|
||||
}
|
||||
|
||||
async checkCreateCustomerBreadcrumb() {
|
||||
@@ -262,7 +322,9 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
|
||||
async getSearchBreadcrumb(): Promise<Breadcrumb | undefined> {
|
||||
const breadcrumbs = await this.getBreadcrumbs();
|
||||
return breadcrumbs.find((b) => b.tags.includes('list') && b.tags.includes('search'));
|
||||
return breadcrumbs.find(
|
||||
(b) => b.tags.includes('list') && b.tags.includes('search'),
|
||||
);
|
||||
}
|
||||
|
||||
async checkSearchBreadcrumb() {
|
||||
@@ -288,7 +350,9 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
const name = this._store.queryParams?.main_qs || 'Suche';
|
||||
|
||||
if (!searchBreadcrumb) {
|
||||
const navigation = this._navigation.listRoute({ processId: this._store.processId });
|
||||
const navigation = this._navigation.listRoute({
|
||||
processId: this._store.processId,
|
||||
});
|
||||
const breadcrumb: Breadcrumb = {
|
||||
key: this._store.processId,
|
||||
tags: ['customer', 'search', 'list'],
|
||||
@@ -300,7 +364,10 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
|
||||
this._breadcrumbService.addBreadcrumb(breadcrumb);
|
||||
} else {
|
||||
this._breadcrumbService.patchBreadcrumb(searchBreadcrumb.id, { params: this.snapshot.queryParams, name });
|
||||
this._breadcrumbService.patchBreadcrumb(searchBreadcrumb.id, {
|
||||
params: this.snapshot.queryParams,
|
||||
name,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (searchBreadcrumb) {
|
||||
@@ -311,7 +378,9 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
|
||||
async getDetailsBreadcrumb(): Promise<Breadcrumb | undefined> {
|
||||
const breadcrumbs = await this.getBreadcrumbs();
|
||||
return breadcrumbs.find((b) => b.tags.includes('details') && b.tags.includes('search'));
|
||||
return breadcrumbs.find(
|
||||
(b) => b.tags.includes('details') && b.tags.includes('search'),
|
||||
);
|
||||
}
|
||||
|
||||
async checkDetailsBreadcrumb() {
|
||||
@@ -333,7 +402,8 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
].includes(this.breadcrumb)
|
||||
) {
|
||||
const customer = this._store.customer;
|
||||
const fullName = `${customer?.firstName ?? ''} ${customer?.lastName ?? ''}`.trim();
|
||||
const fullName =
|
||||
`${customer?.firstName ?? ''} ${customer?.lastName ?? ''}`.trim();
|
||||
|
||||
if (!detailsBreadcrumb) {
|
||||
const navigation = this._navigation.detailsRoute({
|
||||
@@ -515,7 +585,10 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
async checkOrderDetailsBreadcrumb() {
|
||||
const orderDetailsBreadcrumb = await this.getOrderDetailsBreadcrumb();
|
||||
|
||||
if (this.breadcrumb === 'order-details' || this.breadcrumb === 'order-details-history') {
|
||||
if (
|
||||
this.breadcrumb === 'order-details' ||
|
||||
this.breadcrumb === 'order-details-history'
|
||||
) {
|
||||
if (!orderDetailsBreadcrumb) {
|
||||
const navigation = this._navigation.orderDetialsRoute({
|
||||
processId: this._store.processId,
|
||||
@@ -546,7 +619,8 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async checkOrderDetailsHistoryBreadcrumb() {
|
||||
const orderDetailsHistoryBreadcrumb = await this.getOrderDetailsHistoryBreadcrumb();
|
||||
const orderDetailsHistoryBreadcrumb =
|
||||
await this.getOrderDetailsHistoryBreadcrumb();
|
||||
|
||||
if (this.breadcrumb === 'order-details-history') {
|
||||
if (!orderDetailsHistoryBreadcrumb) {
|
||||
@@ -569,7 +643,9 @@ export class CustomerSearchComponent implements OnInit, OnDestroy {
|
||||
this._breadcrumbService.addBreadcrumb(breadcrumb);
|
||||
}
|
||||
} else if (orderDetailsHistoryBreadcrumb) {
|
||||
this._breadcrumbService.removeBreadcrumb(orderDetailsHistoryBreadcrumb.id);
|
||||
this._breadcrumbService.removeBreadcrumb(
|
||||
orderDetailsHistoryBreadcrumb.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Params, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
Params,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { CustomerCreateFormData, decodeFormData } from '../create-customer';
|
||||
import { CustomerCreateNavigation } from '@shared/services/navigation';
|
||||
@@ -9,7 +14,10 @@ export class CustomerCreateGuard {
|
||||
private checkoutService = inject(DomainCheckoutService);
|
||||
private customerCreateNavigation = inject(CustomerCreateNavigation);
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
|
||||
async canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
): Promise<boolean> {
|
||||
// exit with true if canActivateChild will be called
|
||||
if (route.firstChild) {
|
||||
return true;
|
||||
@@ -19,10 +27,15 @@ export class CustomerCreateGuard {
|
||||
|
||||
const processId = this.getProcessId(route);
|
||||
const formData = this.getFormData(route);
|
||||
const canActivateCustomerType = await this.setableCustomerTypes(processId, formData);
|
||||
const canActivateCustomerType = await this.setableCustomerTypes(
|
||||
processId,
|
||||
formData,
|
||||
);
|
||||
|
||||
if (canActivateCustomerType[customerType] !== true) {
|
||||
customerType = Object.keys(canActivateCustomerType).find((key) => canActivateCustomerType[key]);
|
||||
customerType = Object.keys(canActivateCustomerType).find(
|
||||
(key) => canActivateCustomerType[key],
|
||||
);
|
||||
}
|
||||
|
||||
await this.navigate(processId, customerType, route.queryParams);
|
||||
@@ -30,9 +43,14 @@ export class CustomerCreateGuard {
|
||||
return true;
|
||||
}
|
||||
|
||||
async canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
|
||||
async canActivateChild(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
): Promise<boolean> {
|
||||
const processId = this.getProcessId(route);
|
||||
const customerType = route.routeConfig.path?.replace('create/', '')?.replace('/update', '');
|
||||
const customerType = route.routeConfig.path
|
||||
?.replace('create/', '')
|
||||
?.replace('/update', '');
|
||||
|
||||
if (customerType === 'create-customer-main') {
|
||||
return true;
|
||||
@@ -40,29 +58,39 @@ export class CustomerCreateGuard {
|
||||
|
||||
const formData = this.getFormData(route);
|
||||
|
||||
const canActivateCustomerType = await this.setableCustomerTypes(processId, formData);
|
||||
const canActivateCustomerType = await this.setableCustomerTypes(
|
||||
processId,
|
||||
formData,
|
||||
);
|
||||
|
||||
if (canActivateCustomerType[customerType]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const activatableCustomerType = Object.keys(canActivateCustomerType)?.find((key) => canActivateCustomerType[key]);
|
||||
const activatableCustomerType = Object.keys(canActivateCustomerType)?.find(
|
||||
(key) => canActivateCustomerType[key],
|
||||
);
|
||||
|
||||
await this.navigate(processId, activatableCustomerType, route.queryParams);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async setableCustomerTypes(processId: number, formData: CustomerCreateFormData): Promise<Record<string, boolean>> {
|
||||
const res = await this.checkoutService.getSetableCustomerTypes(processId).toPromise();
|
||||
async setableCustomerTypes(
|
||||
processId: number,
|
||||
formData: CustomerCreateFormData,
|
||||
): Promise<Record<string, boolean>> {
|
||||
const res = await this.checkoutService
|
||||
.getSetableCustomerTypes(processId)
|
||||
.toPromise();
|
||||
|
||||
if (res.store) {
|
||||
res['store-p4m'] = true;
|
||||
}
|
||||
// if (res.store) {
|
||||
// res['store-p4m'] = true;
|
||||
// }
|
||||
|
||||
if (res.webshop) {
|
||||
res['webshop-p4m'] = true;
|
||||
}
|
||||
// if (res.webshop) {
|
||||
// res['webshop-p4m'] = true;
|
||||
// }
|
||||
|
||||
if (formData?._meta) {
|
||||
const customerType = formData._meta.customerType;
|
||||
@@ -107,7 +135,11 @@ export class CustomerCreateGuard {
|
||||
return {};
|
||||
}
|
||||
|
||||
navigate(processId: number, customerType: string, queryParams: Params): Promise<boolean> {
|
||||
navigate(
|
||||
processId: number,
|
||||
customerType: string,
|
||||
queryParams: Params,
|
||||
): Promise<boolean> {
|
||||
const path = this.customerCreateNavigation.createCustomerRoute({
|
||||
customerType,
|
||||
processId,
|
||||
|
||||
@@ -31,7 +31,9 @@ export class CantAddCustomerToCartModalComponent {
|
||||
get option() {
|
||||
return (
|
||||
this.ref.data.upgradeableTo?.options.values.find((upgradeOption) =>
|
||||
this.ref.data.required.options.values.some((requiredOption) => upgradeOption.key === requiredOption.key),
|
||||
this.ref.data.required.options.values.some(
|
||||
(requiredOption) => upgradeOption.key === requiredOption.key,
|
||||
),
|
||||
) || { value: this.queryParams }
|
||||
);
|
||||
}
|
||||
@@ -39,7 +41,9 @@ export class CantAddCustomerToCartModalComponent {
|
||||
get queryParams() {
|
||||
let option = this.ref.data.required?.options.values.find((f) => f.selected);
|
||||
if (!option) {
|
||||
option = this.ref.data.required?.options.values.find((f) => (isBoolean(f.enabled) ? f.enabled : true));
|
||||
option = this.ref.data.required?.options.values.find((f) =>
|
||||
isBoolean(f.enabled) ? f.enabled : true,
|
||||
);
|
||||
}
|
||||
return option ? { customertype: option.value } : {};
|
||||
}
|
||||
@@ -57,27 +61,29 @@ export class CantAddCustomerToCartModalComponent {
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
if (customer) {
|
||||
queryParams['formData'] = encodeFormData(mapCustomerDtoToCustomerCreateFormData(customer));
|
||||
queryParams['formData'] = encodeFormData(
|
||||
mapCustomerDtoToCustomerCreateFormData(customer),
|
||||
);
|
||||
}
|
||||
|
||||
if (option === 'webshop' && attributes.some((a) => a.key === 'p4mUser')) {
|
||||
const nav = this.customerCreateNavigation.createCustomerRoute({
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
customerType: 'webshop-p4m',
|
||||
});
|
||||
this.router.navigate(nav.path, {
|
||||
queryParams: { ...nav.queryParams, ...queryParams },
|
||||
});
|
||||
} else {
|
||||
const nav = this.customerCreateNavigation.createCustomerRoute({
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
customerType: option as any,
|
||||
});
|
||||
// if (option === 'webshop' && attributes.some((a) => a.key === 'p4mUser')) {
|
||||
// const nav = this.customerCreateNavigation.createCustomerRoute({
|
||||
// processId: this.applicationService.activatedProcessId,
|
||||
// customerType: 'webshop-p4m',
|
||||
// });
|
||||
// this.router.navigate(nav.path, {
|
||||
// queryParams: { ...nav.queryParams, ...queryParams },
|
||||
// });
|
||||
// } else {
|
||||
const nav = this.customerCreateNavigation.createCustomerRoute({
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
customerType: option as any,
|
||||
});
|
||||
|
||||
this.router.navigate(nav.path, {
|
||||
queryParams: { ...nav.queryParams, ...queryParams },
|
||||
});
|
||||
}
|
||||
this.router.navigate(nav.path, {
|
||||
queryParams: { ...nav.queryParams, ...queryParams },
|
||||
});
|
||||
// }
|
||||
|
||||
this.ref.close();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<div class="font-bold text-center border-t border-b border-solid border-disabled-customer -mx-4 py-4">
|
||||
<div
|
||||
class="font-bold text-center border-t border-b border-solid border-disabled-customer -mx-4 py-4"
|
||||
>
|
||||
{{ customer?.communicationDetails?.email }}
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-1 text-sm font-bold border-b border-solid border-disabled-customer -mx-4 py-4 px-14">
|
||||
<div
|
||||
class="grid grid-flow-row gap-1 text-sm font-bold border-b border-solid border-disabled-customer -mx-4 py-4 px-14"
|
||||
>
|
||||
@if (customer?.organisation?.name) {
|
||||
<span>{{ customer?.organisation?.name }}</span>
|
||||
}
|
||||
@@ -16,23 +20,26 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-flow-col gap-4 justify-around mt-12">
|
||||
<button class="border-2 border-solid border-brand rounded-full font-bold text-brand px-6 py-3 text-lg" (click)="close(false)">
|
||||
<button
|
||||
class="border-2 border-solid border-brand rounded-full font-bold text-brand px-6 py-3 text-lg"
|
||||
(click)="close(false)"
|
||||
>
|
||||
neues Onlinekonto anlegen
|
||||
</button>
|
||||
@if (!isWebshopWithP4M) {
|
||||
<button
|
||||
class="border-2 border-solid border-brand rounded-full font-bold text-white px-6 py-3 text-lg bg-brand"
|
||||
(click)="close(true)"
|
||||
>
|
||||
>
|
||||
Daten übernehmen
|
||||
</button>
|
||||
}
|
||||
@if (isWebshopWithP4M) {
|
||||
<!-- @if (isWebshopWithP4M) {
|
||||
<button
|
||||
class="border-2 border-solid border-brand rounded-full font-bold text-white px-6 py-3 text-lg bg-brand"
|
||||
(click)="selectCustomer()"
|
||||
>
|
||||
Datensatz auswählen
|
||||
</button>
|
||||
}
|
||||
} -->
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,11 @@ import { CustomerCreateGuard } from './guards/customer-create.guard';
|
||||
import {
|
||||
CreateB2BCustomerComponent,
|
||||
CreateGuestCustomerComponent,
|
||||
CreateP4MCustomerComponent,
|
||||
// CreateP4MCustomerComponent,
|
||||
CreateStoreCustomerComponent,
|
||||
CreateWebshopCustomerComponent,
|
||||
} from './create-customer';
|
||||
import { UpdateP4MWebshopCustomerComponent } from './create-customer/update-p4m-webshop-customer';
|
||||
// import { UpdateP4MWebshopCustomerComponent } from './create-customer/update-p4m-webshop-customer';
|
||||
import { CreateCustomerComponent } from './create-customer/create-customer.component';
|
||||
import { CustomerDataEditB2BComponent } from './customer-search/edit-main-view/customer-data-edit-b2b.component';
|
||||
import { CustomerDataEditB2CComponent } from './customer-search/edit-main-view/customer-data-edit-b2c.component';
|
||||
@@ -40,8 +40,16 @@ export const routes: Routes = [
|
||||
path: '',
|
||||
component: CustomerSearchComponent,
|
||||
children: [
|
||||
{ path: 'search', component: CustomerMainViewComponent, data: { side: 'main', breadcrumb: 'main' } },
|
||||
{ path: 'search/list', component: CustomerResultsMainViewComponent, data: { breadcrumb: 'search' } },
|
||||
{
|
||||
path: 'search',
|
||||
component: CustomerMainViewComponent,
|
||||
data: { side: 'main', breadcrumb: 'main' },
|
||||
},
|
||||
{
|
||||
path: 'search/list',
|
||||
component: CustomerResultsMainViewComponent,
|
||||
data: { breadcrumb: 'search' },
|
||||
},
|
||||
{
|
||||
path: 'search/filter',
|
||||
component: CustomerFilterMainViewComponent,
|
||||
@@ -80,7 +88,10 @@ export const routes: Routes = [
|
||||
{
|
||||
path: 'search/:customerId/orders/:orderId/:orderItemId/history',
|
||||
component: CustomerOrderDetailsHistoryMainViewComponent,
|
||||
data: { side: 'order-details', breadcrumb: 'order-details-history' },
|
||||
data: {
|
||||
side: 'order-details',
|
||||
breadcrumb: 'order-details-history',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/edit/b2b',
|
||||
@@ -140,13 +151,13 @@ export const routes: Routes = [
|
||||
{ path: 'create/webshop', component: CreateWebshopCustomerComponent },
|
||||
{ path: 'create/b2b', component: CreateB2BCustomerComponent },
|
||||
{ path: 'create/guest', component: CreateGuestCustomerComponent },
|
||||
{ path: 'create/webshop-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'webshop' } },
|
||||
{ path: 'create/store-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'store' } },
|
||||
{
|
||||
path: 'create/webshop-p4m/update',
|
||||
component: UpdateP4MWebshopCustomerComponent,
|
||||
data: { customerType: 'webshop' },
|
||||
},
|
||||
// { path: 'create/webshop-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'webshop' } },
|
||||
// { path: 'create/store-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'store' } },
|
||||
// {
|
||||
// path: 'create/webshop-p4m/update',
|
||||
// component: UpdateP4MWebshopCustomerComponent,
|
||||
// data: { customerType: 'webshop' },
|
||||
// },
|
||||
{
|
||||
path: 'create-customer-main',
|
||||
outlet: 'side',
|
||||
|
||||
@@ -16,13 +16,34 @@
|
||||
[deltaEnd]="150"
|
||||
[itemLength]="itemLength$ | async"
|
||||
[containerHeight]="24.5"
|
||||
>
|
||||
@for (bueryNumberGroup of items$ | async | groupBy: byBuyerNumberFn; track bueryNumberGroup) {
|
||||
>
|
||||
@for (
|
||||
bueryNumberGroup of items$ | async | groupBy: byBuyerNumberFn;
|
||||
track bueryNumberGroup
|
||||
) {
|
||||
<shared-goods-in-out-order-group>
|
||||
@for (orderNumberGroup of bueryNumberGroup.items | groupBy: byOrderNumberFn; track orderNumberGroup; let lastOrderNumber = $last) {
|
||||
@for (processingStatusGroup of orderNumberGroup.items | groupBy: byProcessingStatusFn; track processingStatusGroup; let lastProcessingStatus = $last) {
|
||||
@for (compartmentCodeGroup of processingStatusGroup.items | groupBy: byCompartmentCodeFn; track compartmentCodeGroup; let lastCompartmentCode = $last) {
|
||||
@for (item of compartmentCodeGroup.items; track item; let firstItem = $first) {
|
||||
@for (
|
||||
orderNumberGroup of bueryNumberGroup.items | groupBy: byOrderNumberFn;
|
||||
track orderNumberGroup;
|
||||
let lastOrderNumber = $last
|
||||
) {
|
||||
@for (
|
||||
processingStatusGroup of orderNumberGroup.items
|
||||
| groupBy: byProcessingStatusFn;
|
||||
track processingStatusGroup;
|
||||
let lastProcessingStatus = $last
|
||||
) {
|
||||
@for (
|
||||
compartmentCodeGroup of processingStatusGroup.items
|
||||
| groupBy: byCompartmentCodeFn;
|
||||
track compartmentCodeGroup;
|
||||
let lastCompartmentCode = $last
|
||||
) {
|
||||
@for (
|
||||
item of compartmentCodeGroup.items;
|
||||
track item;
|
||||
let firstItem = $first
|
||||
) {
|
||||
<shared-goods-in-out-order-group-item
|
||||
[item]="item"
|
||||
[showCompartmentCode]="firstItem"
|
||||
@@ -49,7 +70,6 @@
|
||||
<div class="empty-message">Es sind im Moment keine Artikel vorhanden</div>
|
||||
}
|
||||
|
||||
|
||||
<div class="actions">
|
||||
@if (actions$ | async; as actions) {
|
||||
@for (action of actions; track action) {
|
||||
@@ -57,19 +77,27 @@
|
||||
[disabled]="(changeActionLoader$ | async) || (loading$ | async)"
|
||||
class="cta-action cta-action-primary"
|
||||
(click)="handleAction(action)"
|
||||
>
|
||||
<ui-spinner
|
||||
[show]="(changeActionLoader$ | async) || (loading$ | async)"
|
||||
>{{ action.label }}</ui-spinner
|
||||
>
|
||||
<ui-spinner [show]="(changeActionLoader$ | async) || (loading$ | async)">{{ action.label }}</ui-spinner>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
@if (listEmpty$ | async) {
|
||||
<a class="cta-action cta-action-secondary" [routerLink]="['/filiale', 'goods', 'in']">
|
||||
<a
|
||||
class="cta-action cta-action-secondary"
|
||||
[routerLink]="['/filiale', 'goods', 'in']"
|
||||
>
|
||||
Zur Bestellpostensuche
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (listEmpty$ | async) {
|
||||
<a class="cta-action cta-action-primary" [routerLink]="['/filiale', 'remission']">Zur Remission</a>
|
||||
<a class="cta-action cta-action-primary" [routerLink]="remissionPath()"
|
||||
>Zur Remission</a
|
||||
>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
inject,
|
||||
linkedSignal,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { KeyValueDTOOfStringAndString, OrderItemListItemDTO } from '@generated/swagger/oms-api';
|
||||
import {
|
||||
KeyValueDTOOfStringAndString,
|
||||
OrderItemListItemDTO,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
import { UiScrollContainerComponent } from '@ui/scroll-container';
|
||||
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
|
||||
@@ -11,6 +22,7 @@ import { Config } from '@core/config';
|
||||
import { ToasterService } from '@shared/shell';
|
||||
import { PickupShelfInNavigationService } from '@shared/services/navigation';
|
||||
import { CacheService } from '@core/cache';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
|
||||
@Component({
|
||||
selector: 'page-goods-in-remission-preview',
|
||||
@@ -21,8 +33,12 @@ import { CacheService } from '@core/cache';
|
||||
standalone: false,
|
||||
})
|
||||
export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
|
||||
private _pickupShelfInNavigationService = inject(PickupShelfInNavigationService);
|
||||
@ViewChild(UiScrollContainerComponent) scrollContainer: UiScrollContainerComponent;
|
||||
tabService = inject(TabService);
|
||||
private _pickupShelfInNavigationService = inject(
|
||||
PickupShelfInNavigationService,
|
||||
);
|
||||
@ViewChild(UiScrollContainerComponent)
|
||||
scrollContainer: UiScrollContainerComponent;
|
||||
|
||||
items$ = this._store.results$;
|
||||
|
||||
@@ -50,10 +66,18 @@ export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
|
||||
byProcessingStatusFn = (item: OrderItemListItemDTO) => item.processingStatus;
|
||||
|
||||
byCompartmentCodeFn = (item: OrderItemListItemDTO) =>
|
||||
item.compartmentInfo ? `${item.compartmentCode}_${item.compartmentInfo}` : item.compartmentCode;
|
||||
item.compartmentInfo
|
||||
? `${item.compartmentCode}_${item.compartmentInfo}`
|
||||
: item.compartmentCode;
|
||||
|
||||
private readonly SCROLL_POSITION_TOKEN = 'REMISSION_PREVIEW_SCROLL_POSITION';
|
||||
|
||||
remissionPath = linkedSignal(() => [
|
||||
'/',
|
||||
this.tabService.activatedTab()?.id || this.tabService.nextId(),
|
||||
'remission',
|
||||
]);
|
||||
|
||||
constructor(
|
||||
private _breadcrumb: BreadcrumbService,
|
||||
private _store: GoodsInRemissionPreviewStore,
|
||||
@@ -78,12 +102,18 @@ export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private _removeScrollPositionFromCache(): void {
|
||||
this._cache.delete({ processId: this._config.get('process.ids.goodsIn'), token: this.SCROLL_POSITION_TOKEN });
|
||||
this._cache.delete({
|
||||
processId: this._config.get('process.ids.goodsIn'),
|
||||
token: this.SCROLL_POSITION_TOKEN,
|
||||
});
|
||||
}
|
||||
|
||||
private _addScrollPositionToCache(): void {
|
||||
this._cache.set<number>(
|
||||
{ processId: this._config.get('process.ids.goodsIn'), token: this.SCROLL_POSITION_TOKEN },
|
||||
{
|
||||
processId: this._config.get('process.ids.goodsIn'),
|
||||
token: this.SCROLL_POSITION_TOKEN,
|
||||
},
|
||||
this.scrollContainer?.scrollPos,
|
||||
);
|
||||
}
|
||||
@@ -108,7 +138,10 @@ export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
|
||||
|
||||
async updateBreadcrumb() {
|
||||
const crumbs = await this._breadcrumb
|
||||
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), ['goods-in', 'preview'])
|
||||
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
|
||||
'goods-in',
|
||||
'preview',
|
||||
])
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
for (const crumb of crumbs) {
|
||||
@@ -120,12 +153,15 @@ export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
|
||||
|
||||
async removeBreadcrumbs() {
|
||||
let breadcrumbsToDelete = await this._breadcrumb
|
||||
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), ['goods-in'])
|
||||
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
|
||||
'goods-in',
|
||||
])
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
breadcrumbsToDelete = breadcrumbsToDelete.filter(
|
||||
(crumb) => !crumb.tags.includes('preview') && !crumb.tags.includes('main'),
|
||||
(crumb) =>
|
||||
!crumb.tags.includes('preview') && !crumb.tags.includes('main'),
|
||||
);
|
||||
|
||||
breadcrumbsToDelete.forEach((crumb) => {
|
||||
@@ -133,11 +169,17 @@ export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
const detailsCrumbs = await this._breadcrumb
|
||||
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), ['goods-in', 'details'])
|
||||
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
|
||||
'goods-in',
|
||||
'details',
|
||||
])
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const editCrumbs = await this._breadcrumb
|
||||
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), ['goods-in', 'edit'])
|
||||
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
|
||||
'goods-in',
|
||||
'edit',
|
||||
])
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
@@ -152,32 +194,44 @@ export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
|
||||
|
||||
initInitialSearch() {
|
||||
if (this._store.hits === 0) {
|
||||
this._store.searchResult$.pipe(takeUntil(this._onDestroy$)).subscribe(async (result) => {
|
||||
await this.createBreadcrumb();
|
||||
this._store.searchResult$
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe(async (result) => {
|
||||
await this.createBreadcrumb();
|
||||
|
||||
this.scrollContainer?.scrollTo((await this._getScrollPositionFromCache()) ?? 0);
|
||||
this._removeScrollPositionFromCache();
|
||||
});
|
||||
this.scrollContainer?.scrollTo(
|
||||
(await this._getScrollPositionFromCache()) ?? 0,
|
||||
);
|
||||
this._removeScrollPositionFromCache();
|
||||
});
|
||||
}
|
||||
|
||||
this._store.search();
|
||||
}
|
||||
|
||||
async navigateToRemission() {
|
||||
await this._router.navigate(['/filiale/remission']);
|
||||
await this._router.navigate(this.remissionPath());
|
||||
}
|
||||
|
||||
navigateToDetails(orderItem: OrderItemListItemDTO) {
|
||||
const nav = this._pickupShelfInNavigationService.detailRoute({ item: orderItem, side: false });
|
||||
const nav = this._pickupShelfInNavigationService.detailRoute({
|
||||
item: orderItem,
|
||||
side: false,
|
||||
});
|
||||
|
||||
this._router.navigate(nav.path, { queryParams: { ...nav.queryParams, view: 'remission' } });
|
||||
this._router.navigate(nav.path, {
|
||||
queryParams: { ...nav.queryParams, view: 'remission' },
|
||||
});
|
||||
}
|
||||
|
||||
async handleAction(action: KeyValueDTOOfStringAndString) {
|
||||
this.changeActionLoader$.next(true);
|
||||
|
||||
try {
|
||||
const response = await this._store.createRemissionFromPreview().pipe(first()).toPromise();
|
||||
const response = await this._store
|
||||
.createRemissionFromPreview()
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
if (!response?.dialog) {
|
||||
this._toast.open({
|
||||
|
||||
@@ -3,7 +3,10 @@ import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { CustomerInfoDTO } from '@generated/swagger/crm-api';
|
||||
import { NavigationRoute } from './defs/navigation-route';
|
||||
import { encodeFormData, mapCustomerInfoDtoToCustomerCreateFormData } from 'apps/isa-app/src/page/customer';
|
||||
import {
|
||||
encodeFormData,
|
||||
mapCustomerInfoDtoToCustomerCreateFormData,
|
||||
} from 'apps/isa-app/src/page/customer';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CustomerCreateNavigation {
|
||||
@@ -33,7 +36,9 @@ export class CustomerCreateNavigation {
|
||||
|
||||
navigateToDefault(params: { processId: NumberInput }): Promise<boolean> {
|
||||
const route = this.defaultRoute(params);
|
||||
return this._router.navigate(route.path, { queryParams: route.queryParams });
|
||||
return this._router.navigate(route.path, {
|
||||
queryParams: route.queryParams,
|
||||
});
|
||||
}
|
||||
|
||||
createCustomerRoute(params: {
|
||||
@@ -54,7 +59,9 @@ export class CustomerCreateNavigation {
|
||||
];
|
||||
|
||||
let formData = params?.customerInfo
|
||||
? encodeFormData(mapCustomerInfoDtoToCustomerCreateFormData(params.customerInfo))
|
||||
? encodeFormData(
|
||||
mapCustomerInfoDtoToCustomerCreateFormData(params.customerInfo),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const urlTree = this._router.createUrlTree(path, {
|
||||
@@ -79,7 +86,9 @@ export class CustomerCreateNavigation {
|
||||
processId: NumberInput;
|
||||
customerInfo: CustomerInfoDTO;
|
||||
}): NavigationRoute {
|
||||
const formData = encodeFormData(mapCustomerInfoDtoToCustomerCreateFormData(customerInfo));
|
||||
const formData = encodeFormData(
|
||||
mapCustomerInfoDtoToCustomerCreateFormData(customerInfo),
|
||||
);
|
||||
const path = [
|
||||
'/kunde',
|
||||
coerceNumberProperty(processId),
|
||||
@@ -88,14 +97,16 @@ export class CustomerCreateNavigation {
|
||||
outlets: {
|
||||
primary: [
|
||||
'create',
|
||||
customerInfo?.features?.find((feature) => feature.key === 'webshop') ? 'webshop-p4m' : 'store-p4m',
|
||||
// customerInfo?.features?.find((feature) => feature.key === 'webshop') ? 'webshop-p4m' : 'store-p4m',
|
||||
],
|
||||
side: 'create-customer-main',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const urlTree = this._router.createUrlTree(path, { queryParams: { formData } });
|
||||
const urlTree = this._router.createUrlTree(path, {
|
||||
queryParams: { formData },
|
||||
});
|
||||
|
||||
return {
|
||||
path,
|
||||
|
||||
@@ -254,35 +254,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (remissionNavigation$ | async; as remissionNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu()"
|
||||
[routerLink]="remissionNavigation.path"
|
||||
[queryParams]="remissionNavigation.queryParams"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="assignment-return"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Remission</span>
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (packageInspectionNavigation$ | async; as packageInspectionNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); fetchAndOpenPackages()"
|
||||
[routerLink]="packageInspectionNavigation.path"
|
||||
[queryParams]="packageInspectionNavigation.queryParams"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="clipboard-check-outline"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Wareneingang</span>
|
||||
</a>
|
||||
}
|
||||
<div class="side-menu-group-sub-item-wrapper">
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
@@ -348,5 +319,20 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (packageInspectionNavigation$ | async; as packageInspectionNavigation) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); fetchAndOpenPackages()"
|
||||
[routerLink]="packageInspectionNavigation.path"
|
||||
[queryParams]="packageInspectionNavigation.queryParams"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shared-icon icon="clipboard-check-outline"></shared-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Wareneingang</span>
|
||||
</a>
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -191,14 +191,6 @@ export class ShellSideMenuComponent {
|
||||
// this._pickUpShelfInNavigation.listRoute()
|
||||
// );
|
||||
|
||||
remissionNavigation$ = this.getLastNavigationByProcessId(
|
||||
this.#config.get('process.ids.remission'),
|
||||
{
|
||||
path: ['/filiale', 'remission'],
|
||||
queryParams: {},
|
||||
},
|
||||
);
|
||||
|
||||
packageInspectionNavigation$ = this.getLastNavigationByProcessId(
|
||||
this.#config.get('process.ids.packageInspection'),
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ variables:
|
||||
value: '4'
|
||||
# Minor Version einstellen
|
||||
- name: 'Minor'
|
||||
value: '0'
|
||||
value: '1'
|
||||
- name: 'Patch'
|
||||
value: "$[counter(format('{0}.{1}', variables['Major'], variables['Minor']),0)]"
|
||||
- name: 'BuildUniqueID'
|
||||
|
||||
@@ -44,5 +44,8 @@ export class DataAccessError<TCode extends string, TData = void> extends Error {
|
||||
public readonly data: TData,
|
||||
) {
|
||||
super(message);
|
||||
// Set the prototype explicitly to maintain the correct prototype chain
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './lib/models';
|
||||
export * from './lib/stores';
|
||||
export * from './lib/schemas';
|
||||
export * from './lib/helpers';
|
||||
export * from './lib/guards';
|
||||
|
||||
2
libs/remission/data-access/src/lib/guards/index.ts
Normal file
2
libs/remission/data-access/src/lib/guards/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { isReturnItem } from './is-return-item';
|
||||
export { isReturnSuggestion } from './is-return-suggestion';
|
||||
42
libs/remission/data-access/src/lib/guards/is-return-item.ts
Normal file
42
libs/remission/data-access/src/lib/guards/is-return-item.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ReturnItem } from '../models/return-item';
|
||||
|
||||
/**
|
||||
* Type guard to check if an object is a valid ReturnItem
|
||||
* @param value - The value to check
|
||||
* @returns True if the value is a ReturnItem, false otherwise
|
||||
*/
|
||||
export const isReturnItem = (value: unknown): value is ReturnItem => {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const item = value as Partial<ReturnItem>;
|
||||
|
||||
// Check required properties from ReturnItem
|
||||
return (
|
||||
// Check product exists and has required properties
|
||||
typeof item.product === 'object' &&
|
||||
item.product !== null &&
|
||||
(typeof item.product.name === 'string' ||
|
||||
typeof item.product.ean === 'string') &&
|
||||
// Check retailPrice exists and has required nested structure
|
||||
typeof item.retailPrice === 'object' &&
|
||||
item.retailPrice !== null &&
|
||||
typeof item.retailPrice.value === 'object' &&
|
||||
item.retailPrice.value !== null &&
|
||||
typeof item.retailPrice.value.value === 'number' &&
|
||||
typeof item.retailPrice.value.currency === 'string' &&
|
||||
// Check source exists and is a valid string
|
||||
typeof item.source === 'string' &&
|
||||
item.source.length > 0 &&
|
||||
// Check inherited ReturnItemDTO properties (id is a number)
|
||||
typeof item.id === 'number' &&
|
||||
// ReturnItem-specific: Must NOT have ReturnSuggestion-specific fields
|
||||
!('accepted' in item) &&
|
||||
!('rejected' in item) &&
|
||||
!('sort' in item) &&
|
||||
// ReturnItem can have predefinedReturnQuantity, quantityReturned, or neither
|
||||
// If it has none of the ReturnSuggestion-specific fields, it's a ReturnItem
|
||||
true
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ReturnSuggestion } from '../models/return-suggestion';
|
||||
|
||||
/**
|
||||
* Type guard to check if an object is a valid ReturnSuggestion
|
||||
* @param value - The value to check
|
||||
* @returns True if the value is a ReturnSuggestion, false otherwise
|
||||
*/
|
||||
export const isReturnSuggestion = (
|
||||
value: unknown,
|
||||
): value is ReturnSuggestion => {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const suggestion = value as Partial<ReturnSuggestion>;
|
||||
|
||||
// Check required properties from ReturnSuggestion
|
||||
return (
|
||||
// Check product exists and has required properties
|
||||
typeof suggestion.product === 'object' &&
|
||||
suggestion.product !== null &&
|
||||
(typeof suggestion.product.name === 'string' ||
|
||||
typeof suggestion.product.ean === 'string') &&
|
||||
// Check retailPrice exists and has required nested structure
|
||||
typeof suggestion.retailPrice === 'object' &&
|
||||
suggestion.retailPrice !== null &&
|
||||
typeof suggestion.retailPrice.value === 'object' &&
|
||||
suggestion.retailPrice.value !== null &&
|
||||
typeof suggestion.retailPrice.value.value === 'number' &&
|
||||
typeof suggestion.retailPrice.value.currency === 'string' &&
|
||||
// Check source exists and is a valid string
|
||||
typeof suggestion.source === 'string' &&
|
||||
suggestion.source.length > 0 &&
|
||||
// Check inherited ReturnSuggestionDTO properties (id is a number)
|
||||
typeof suggestion.id === 'number' &&
|
||||
// ReturnSuggestion-specific: Must have at least one distinguishing property
|
||||
('accepted' in suggestion ||
|
||||
'rejected' in suggestion ||
|
||||
'sort' in suggestion) &&
|
||||
// Additionally, must NOT have ReturnItem-specific fields
|
||||
!('predefinedReturnQuantity' in suggestion) &&
|
||||
!('quantityReturned' in suggestion)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
import { getItemType } from './get-item-type.helper';
|
||||
import { RemissionItemType } from '../models';
|
||||
import { RemissionItem } from '../stores';
|
||||
|
||||
describe('getItemType', () => {
|
||||
describe('Happy Path - ReturnItem', () => {
|
||||
it('should return ReturnItem when item has predefinedReturnQuantity', () => {
|
||||
// Arrange
|
||||
const item = {
|
||||
id: 123,
|
||||
product: {
|
||||
name: 'Test Product',
|
||||
ean: '1234567890123',
|
||||
},
|
||||
retailPrice: {
|
||||
value: {
|
||||
value: 10.99,
|
||||
currency: 'EUR',
|
||||
},
|
||||
},
|
||||
source: 'manually-added',
|
||||
predefinedReturnQuantity: 1,
|
||||
quantity: 1,
|
||||
} as RemissionItem;
|
||||
|
||||
// Act
|
||||
const result = getItemType(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(RemissionItemType.ReturnItem);
|
||||
});
|
||||
|
||||
it('should return ReturnItem when item has no suggestion-specific fields', () => {
|
||||
// Arrange
|
||||
const item = {
|
||||
id: 456,
|
||||
product: {
|
||||
name: 'Another Product',
|
||||
ean: '9876543210987',
|
||||
},
|
||||
retailPrice: {
|
||||
value: {
|
||||
value: 15.5,
|
||||
currency: 'EUR',
|
||||
},
|
||||
},
|
||||
source: 'DisposalListModule',
|
||||
returnReason: 'Herstellerfehler',
|
||||
} as RemissionItem;
|
||||
|
||||
// Act
|
||||
const result = getItemType(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(RemissionItemType.ReturnItem);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Happy Path - ReturnSuggestion', () => {
|
||||
it('should return ReturnSuggestion when item has sort field', () => {
|
||||
// Arrange
|
||||
const item = {
|
||||
id: 789,
|
||||
product: {
|
||||
name: 'Suggestion Product',
|
||||
ean: '5555555555555',
|
||||
contributors: 'Test Author',
|
||||
format: 'TB',
|
||||
formatDetail: 'Taschenbuch',
|
||||
},
|
||||
retailPrice: {
|
||||
value: {
|
||||
value: 20.0,
|
||||
currency: 'EUR',
|
||||
},
|
||||
},
|
||||
source: 'manually-added',
|
||||
quantity: 3,
|
||||
sort: 1,
|
||||
} as RemissionItem;
|
||||
|
||||
// Act
|
||||
const result = getItemType(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(RemissionItemType.ReturnSuggestion);
|
||||
});
|
||||
|
||||
it('should return ReturnSuggestion when item has accepted field', () => {
|
||||
// Arrange
|
||||
const item = {
|
||||
id: 101,
|
||||
product: {
|
||||
name: 'Accepted Product',
|
||||
ean: '1111111111111',
|
||||
contributors: 'Test Author',
|
||||
format: 'TB',
|
||||
formatDetail: 'Taschenbuch',
|
||||
},
|
||||
retailPrice: {
|
||||
value: {
|
||||
value: 8.99,
|
||||
currency: 'EUR',
|
||||
},
|
||||
},
|
||||
source: 'DisposalListModule',
|
||||
quantity: 2,
|
||||
accepted: '2025-10-20T10:00:00Z',
|
||||
} as RemissionItem;
|
||||
|
||||
// Act
|
||||
const result = getItemType(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(RemissionItemType.ReturnSuggestion);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fallback with RemissionListType', () => {
|
||||
it('should return ReturnItem when neither guard matches and remissionListType is Pflichtremission', () => {
|
||||
// Arrange - Item that doesn't match either guard (missing required fields)
|
||||
const ambiguousItem = {
|
||||
id: 202,
|
||||
product: {
|
||||
name: 'Ambiguous Product',
|
||||
ean: '1234567890',
|
||||
contributors: 'Test',
|
||||
format: 'TB',
|
||||
formatDetail: 'Taschenbuch',
|
||||
},
|
||||
retailPrice: {
|
||||
value: {
|
||||
value: 12.5,
|
||||
currency: 'EUR',
|
||||
},
|
||||
},
|
||||
// Missing 'source' field - will fail both guards
|
||||
quantity: 1,
|
||||
} as unknown as RemissionItem;
|
||||
|
||||
// Act
|
||||
const result = getItemType(ambiguousItem, 'Pflichtremission');
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(RemissionItemType.ReturnItem);
|
||||
});
|
||||
|
||||
it('should return ReturnSuggestion when neither guard matches and remissionListType is Abteilungsremission', () => {
|
||||
// Arrange - Item that doesn't match either guard (missing required fields)
|
||||
const ambiguousItem = {
|
||||
id: 303,
|
||||
product: {
|
||||
name: 'Another Ambiguous Product',
|
||||
ean: '9876543210',
|
||||
contributors: 'Test',
|
||||
format: 'TB',
|
||||
formatDetail: 'Taschenbuch',
|
||||
},
|
||||
retailPrice: {
|
||||
value: {
|
||||
value: 7.5,
|
||||
currency: 'EUR',
|
||||
},
|
||||
},
|
||||
// Missing 'source' field - will fail both guards
|
||||
quantity: 1,
|
||||
} as unknown as RemissionItem;
|
||||
|
||||
// Act
|
||||
const result = getItemType(ambiguousItem, 'Abteilungsremission');
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(RemissionItemType.ReturnSuggestion);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unknown Type', () => {
|
||||
it('should return Unknown when no guards match and no remissionListType provided', () => {
|
||||
// Arrange
|
||||
const invalidItem = {
|
||||
id: 404,
|
||||
} as RemissionItem;
|
||||
|
||||
// Act
|
||||
const result = getItemType(invalidItem);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(RemissionItemType.Unknown);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { RemissionItemType, RemissionListType } from '../models';
|
||||
import { RemissionItem } from '../stores';
|
||||
import { isReturnItem, isReturnSuggestion } from '../guards';
|
||||
|
||||
/**
|
||||
* Determines the concrete type of a RemissionItem
|
||||
* @param item - The item to check
|
||||
* @param remissionListType - Optional fallback to determine type when guards are ambiguous
|
||||
* @returns The type as a RemissionItemType enum value
|
||||
*/
|
||||
export const getItemType = (
|
||||
item: RemissionItem,
|
||||
remissionListType?: RemissionListType,
|
||||
): RemissionItemType => {
|
||||
const isReturnItemType = isReturnItem(item);
|
||||
const isReturnSuggestionItemType = isReturnSuggestion(item);
|
||||
|
||||
// If exactly one guard matches, use it directly (without remissionListType)
|
||||
if (isReturnItemType && !isReturnSuggestionItemType) {
|
||||
return RemissionItemType.ReturnItem;
|
||||
}
|
||||
|
||||
if (isReturnSuggestionItemType && !isReturnItemType) {
|
||||
return RemissionItemType.ReturnSuggestion;
|
||||
}
|
||||
|
||||
// If both are true or both are false, use remissionListType as fallback
|
||||
if (remissionListType) {
|
||||
// Pflichtremission typically contains ReturnItems
|
||||
// Abteilungsremission typically contains ReturnSuggestions
|
||||
if (remissionListType === 'Pflichtremission') {
|
||||
return RemissionItemType.ReturnItem;
|
||||
}
|
||||
|
||||
if (remissionListType === 'Abteilungsremission') {
|
||||
return RemissionItemType.ReturnSuggestion;
|
||||
}
|
||||
}
|
||||
|
||||
// If no remissionListType provided or unrecognized, return Unknown
|
||||
return RemissionItemType.Unknown;
|
||||
};
|
||||
@@ -9,3 +9,5 @@ export * from './get-receipt-items-from-return.helper';
|
||||
export * from './get-package-numbers-from-return.helper';
|
||||
export * from './get-retail-price-from-item.helper';
|
||||
export * from './get-assortment-from-item.helper';
|
||||
export * from './order-by-list-items.helper';
|
||||
export * from './get-item-type.helper';
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { RemissionItem } from '../stores';
|
||||
|
||||
/**
|
||||
* Sorts the remission items in the response based on specific criteria:
|
||||
* - Items with impediments are moved to the end of the list.
|
||||
* - Within impediments, items are sorted by attempt count (ascending).
|
||||
* - Manually added items are prioritized to appear first.
|
||||
* - (Commented out) Items can be sorted by creation date in descending order.
|
||||
* @param {RemissionItem[]} items - The response object containing remission items to be sorted
|
||||
* @returns {void} The function modifies the response object in place
|
||||
*/
|
||||
export const orderByListItems = (items: RemissionItem[]): void => {
|
||||
items.sort((a, b) => {
|
||||
const aHasImpediment = !!a.impediment;
|
||||
const bHasImpediment = !!b.impediment;
|
||||
const aIsManuallyAdded = a.source === 'manually-added';
|
||||
const bIsManuallyAdded = b.source === 'manually-added';
|
||||
|
||||
// First priority: move all items with impediment to the end of the list
|
||||
if (!aHasImpediment && bHasImpediment) {
|
||||
return -1;
|
||||
}
|
||||
if (aHasImpediment && !bHasImpediment) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If both have impediments, sort by attempts (ascending)
|
||||
if (aHasImpediment && bHasImpediment) {
|
||||
const aAttempts = a.impediment?.attempts ?? 0;
|
||||
const bAttempts = b.impediment?.attempts ?? 0;
|
||||
return aAttempts - bAttempts;
|
||||
}
|
||||
|
||||
// Second priority: manually-added items come first
|
||||
if (aIsManuallyAdded && !bIsManuallyAdded) {
|
||||
return -1;
|
||||
}
|
||||
if (!aIsManuallyAdded && bIsManuallyAdded) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
3
libs/remission/data-access/src/lib/models/impediment.ts
Normal file
3
libs/remission/data-access/src/lib/models/impediment.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { ImpedimentDTO } from '@generated/swagger/inventory-api';
|
||||
|
||||
export type Impediment = ImpedimentDTO
|
||||
@@ -18,3 +18,7 @@ export * from './value-tuple-sting-and-integer';
|
||||
export * from './create-remission';
|
||||
export * from './remission-item-source';
|
||||
export * from './receipt-complete-status';
|
||||
export * from './remission-response-args-error-message';
|
||||
export * from './impediment';
|
||||
export * from './update-item';
|
||||
export * from './remission-item-type';
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export const RemissionItemType = {
|
||||
ReturnItem: 'ReturnItem',
|
||||
ReturnSuggestion: 'ReturnSuggestion',
|
||||
Unknown: 'Unknown',
|
||||
} as const;
|
||||
|
||||
export type RemissionItemType = keyof typeof RemissionItemType;
|
||||
export type RemissionItemTypeValue =
|
||||
(typeof RemissionItemType)[RemissionItemType];
|
||||
@@ -0,0 +1,11 @@
|
||||
// #5331 - Messages kommen bis auf AlreadyRemoved aus dem Backend
|
||||
export const RemissionResponseArgsErrorMessage = {
|
||||
AlreadyCompleted: 'Remission wurde bereits abgeschlossen',
|
||||
AlreadyRemitted: 'Artikel wurde bereits remittiert',
|
||||
AlreadyRemoved: 'Artikel konnte nicht entfernt werden',
|
||||
} as const;
|
||||
|
||||
export type RemissionResponseArgsErrorMessageKey =
|
||||
keyof typeof RemissionResponseArgsErrorMessage;
|
||||
export type RemissionResponseArgsErrorMessageValue =
|
||||
(typeof RemissionResponseArgsErrorMessage)[RemissionResponseArgsErrorMessageKey];
|
||||
7
libs/remission/data-access/src/lib/models/update-item.ts
Normal file
7
libs/remission/data-access/src/lib/models/update-item.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Impediment } from './impediment';
|
||||
|
||||
export interface UpdateItem {
|
||||
inProgress: boolean;
|
||||
itemId?: number;
|
||||
impediment?: Impediment;
|
||||
}
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
Stock,
|
||||
Receipt,
|
||||
ReturnItem,
|
||||
RemissionListType,
|
||||
ReceiptReturnTuple,
|
||||
ReceiptReturnSuggestionTuple,
|
||||
ReturnSuggestion,
|
||||
CreateRemission,
|
||||
RemissionItemType,
|
||||
} from '../models';
|
||||
import { subDays } from 'date-fns';
|
||||
import { of, throwError } from 'rxjs';
|
||||
@@ -1559,12 +1559,12 @@ describe('RemissionReturnReceiptService', () => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should call addReturnSuggestionItem for Abteilung type', async () => {
|
||||
it('should call addReturnSuggestionItem for ReturnSuggestion type', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Abteilung,
|
||||
type: RemissionItemType.ReturnSuggestion,
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -1578,16 +1578,18 @@ describe('RemissionReturnReceiptService', () => {
|
||||
returnSuggestionId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
impedimentComment: undefined,
|
||||
remainingQuantity: undefined,
|
||||
});
|
||||
expect(service.addReturnItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call addReturnItem for Pflicht type', async () => {
|
||||
it('should call addReturnItem for ReturnItem type', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Pflicht,
|
||||
type: RemissionItemType.ReturnItem,
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -1605,12 +1607,12 @@ describe('RemissionReturnReceiptService', () => {
|
||||
expect(service.addReturnSuggestionItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return undefined for unknown type', async () => {
|
||||
it('should return undefined for Unknown type', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: 'Unknown' as RemissionListType,
|
||||
type: RemissionItemType.Unknown,
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -1630,7 +1632,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Abteilung,
|
||||
type: RemissionItemType.ReturnSuggestion,
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
@@ -1641,6 +1643,8 @@ describe('RemissionReturnReceiptService', () => {
|
||||
returnSuggestionId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
impedimentComment: undefined,
|
||||
remainingQuantity: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1652,7 +1656,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Pflicht,
|
||||
type: RemissionItemType.ReturnItem,
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
@@ -1675,7 +1679,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Abteilung,
|
||||
type: RemissionItemType.ReturnSuggestion,
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -1689,6 +1693,8 @@ describe('RemissionReturnReceiptService', () => {
|
||||
returnSuggestionId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
impedimentComment: undefined,
|
||||
remainingQuantity: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1699,7 +1705,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Pflicht,
|
||||
type: RemissionItemType.ReturnItem,
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
Receipt,
|
||||
ReceiptReturnSuggestionTuple,
|
||||
ReceiptReturnTuple,
|
||||
RemissionListType,
|
||||
RemissionItemType,
|
||||
ReturnItem,
|
||||
ReturnSuggestion,
|
||||
} from '../models';
|
||||
@@ -905,14 +905,42 @@ export class RemissionReturnReceiptService {
|
||||
|
||||
/**
|
||||
* Remits an item to the return receipt based on its type.
|
||||
* Determines whether to add a return item or return suggestion item based on the remission list type.
|
||||
* Determines whether to add a return item or return suggestion item based on the remission item type.
|
||||
*
|
||||
* @async
|
||||
* @param {Object} params - The parameters for remitting the item
|
||||
* @param {number} params.itemId - The ID of the item to remit
|
||||
* @param {AddReturnItem | AddReturnSuggestionItem} params.addItem - The item data to add
|
||||
* @param {RemissionListType} params.type - The type of remission list (Abteilung or Pflicht)
|
||||
* @param {Omit<AddReturnItem, 'returnItemId'> | Omit<AddReturnSuggestionItem, 'returnSuggestionId'>} params.addItem - The item data to add (without the item ID field)
|
||||
* @param {RemissionItemType} params.type - The type of remission item (ReturnItem, ReturnSuggestion, or Unknown)
|
||||
* @returns {Promise<ReceiptReturnSuggestionTuple | ReceiptReturnTuple | undefined>} The updated receipt and return tuple if successful, undefined otherwise
|
||||
*
|
||||
* @example
|
||||
* // Remit a ReturnItem
|
||||
* const result = await service.remitItem({
|
||||
* itemId: 123,
|
||||
* addItem: {
|
||||
* returnId: 1,
|
||||
* receiptId: 2,
|
||||
* quantity: 10,
|
||||
* inStock: 5,
|
||||
* },
|
||||
* type: RemissionItemType.ReturnItem,
|
||||
* });
|
||||
*
|
||||
* @example
|
||||
* // Remit a ReturnSuggestion
|
||||
* const result = await service.remitItem({
|
||||
* itemId: 456,
|
||||
* addItem: {
|
||||
* returnId: 1,
|
||||
* receiptId: 2,
|
||||
* quantity: 10,
|
||||
* inStock: 5,
|
||||
* impedimentComment: 'Restmenge',
|
||||
* remainingQuantity: 5,
|
||||
* },
|
||||
* type: RemissionItemType.ReturnSuggestion,
|
||||
* });
|
||||
*/
|
||||
async remitItem({
|
||||
itemId,
|
||||
@@ -923,10 +951,17 @@ export class RemissionReturnReceiptService {
|
||||
addItem:
|
||||
| Omit<AddReturnItem, 'returnItemId'>
|
||||
| Omit<AddReturnSuggestionItem, 'returnSuggestionId'>;
|
||||
type: RemissionListType;
|
||||
type: RemissionItemType;
|
||||
}): Promise<ReceiptReturnSuggestionTuple | ReceiptReturnTuple | undefined> {
|
||||
if (type === RemissionItemType.Unknown) {
|
||||
this.#logger.error(
|
||||
'Invalid remission item type: None. Cannot remit item.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ReturnSuggestion
|
||||
if (type === RemissionListType.Abteilung) {
|
||||
if (type === RemissionItemType.ReturnSuggestion) {
|
||||
return await this.addReturnSuggestionItem({
|
||||
returnId: addItem.returnId,
|
||||
receiptId: addItem.receiptId,
|
||||
@@ -941,7 +976,7 @@ export class RemissionReturnReceiptService {
|
||||
}
|
||||
|
||||
// ReturnItem
|
||||
if (type === RemissionListType.Pflicht) {
|
||||
if (type === RemissionItemType.ReturnItem) {
|
||||
return await this.addReturnItem({
|
||||
returnId: addItem.returnId,
|
||||
receiptId: addItem.receiptId,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<filter-input-menu-button
|
||||
[filterInput]="filterDepartmentInput()"
|
||||
[label]="selectedDepartments()"
|
||||
[commitOnClose]="true"
|
||||
[label]="selectedDepartment()"
|
||||
[canApply]="true"
|
||||
(closed)="rollbackFilterInput()"
|
||||
>
|
||||
</filter-input-menu-button>
|
||||
|
||||
@if (selectedDepartments()) {
|
||||
@if (selectedDepartment()) {
|
||||
<ui-toolbar class="ui-toolbar-rounded">
|
||||
<span class="flex gap-1 isa-text-body-2-regular"
|
||||
><span *uiSkeletonLoader="capacityFetching()" class="isa-text-body-2-bold"
|
||||
|
||||
@@ -52,14 +52,17 @@ export class RemissionListDepartmentElementsComponent {
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal for the selected departments from the filter input.
|
||||
* If the input type is Checkbox and has selected values, it returns a comma-separated string.
|
||||
* Otherwise, it returns undefined.
|
||||
* Computed signal to get the selected department from the filter input.
|
||||
* Returns the committed value if department is selected, otherwise a default label.
|
||||
* @returns {string} The selected departments or a default label.
|
||||
*/
|
||||
selectedDepartments = computed(() => {
|
||||
selectedDepartment = computed(() => {
|
||||
const input = this.filterDepartmentInput();
|
||||
if (input?.type === InputType.Checkbox && input?.selected?.length > 0) {
|
||||
return input?.selected?.filter((selected) => !!selected).join(', ');
|
||||
if (input && input.type === InputType.Checkbox) {
|
||||
const committedValue = this.#filterService.queryParams()[input.key];
|
||||
if (input.selected.length > 0 && committedValue) {
|
||||
return committedValue;
|
||||
}
|
||||
}
|
||||
return 'Abteilung auswählen';
|
||||
});
|
||||
@@ -71,9 +74,7 @@ export class RemissionListDepartmentElementsComponent {
|
||||
*/
|
||||
capacityResource = createRemissionCapacityResource(() => {
|
||||
return {
|
||||
departments: this.selectedDepartments()
|
||||
?.split(',')
|
||||
.map((d) => d.trim()),
|
||||
departments: [this.selectedDepartment()],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -144,4 +145,9 @@ export class RemissionListDepartmentElementsComponent {
|
||||
})
|
||||
: 0;
|
||||
});
|
||||
|
||||
rollbackFilterInput() {
|
||||
const inputKey = this.filterDepartmentInput()?.key;
|
||||
this.#filterService.rollbackInput([inputKey!]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export class RemissionListEmptyStateComponent {
|
||||
* @remarks This logic ensures that the most relevant empty state is shown to the user based on their current context.
|
||||
*/
|
||||
displayEmptyState = computed<EmptyState>(() => {
|
||||
if (!this.listFetching()) {
|
||||
if (!this.listFetching() && !this.hasValidSearchTerm()) {
|
||||
// Prio 1: Abteilungsremission - Es ist noch keine Abteilung ausgewählt
|
||||
if (
|
||||
this.isDepartment() &&
|
||||
@@ -65,7 +65,6 @@ export class RemissionListEmptyStateComponent {
|
||||
|
||||
// Prio 2: Liste abgearbeitet und keine Artikel mehr vorhanden
|
||||
if (
|
||||
!this.hasValidSearchTerm() &&
|
||||
this.hits() === 0 &&
|
||||
this.isReloadSearch()
|
||||
) {
|
||||
@@ -77,7 +76,7 @@ export class RemissionListEmptyStateComponent {
|
||||
}
|
||||
|
||||
// Prio 3: Keine Ergebnisse bei leerem Suchbegriff (nur Filter gesetzt)
|
||||
if (!this.hasValidSearchTerm() && this.hits() === 0) {
|
||||
if (this.hits() === 0) {
|
||||
return {
|
||||
title: 'Keine Suchergebnisse',
|
||||
description:
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
uiTextButton
|
||||
color="strong"
|
||||
(click)="deleteItemFromList()"
|
||||
[disabled]="inProgress()"
|
||||
[pending]="inProgress()"
|
||||
[disabled]="removeOrUpdateItem().inProgress"
|
||||
[pending]="removeOrUpdateItem().inProgress"
|
||||
data-what="button"
|
||||
data-which="remove-remission-item"
|
||||
>
|
||||
@@ -17,11 +17,12 @@
|
||||
@if (displayChangeQuantityButton()) {
|
||||
<button
|
||||
class="self-end"
|
||||
[class.highlight]="highlight()"
|
||||
type="button"
|
||||
uiTextButton
|
||||
color="strong"
|
||||
(click)="openRemissionQuantityDialog()"
|
||||
[disabled]="inProgress()"
|
||||
[disabled]="removeOrUpdateItem().inProgress"
|
||||
data-what="button"
|
||||
data-which="change-remission-quantity"
|
||||
>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
inject,
|
||||
input,
|
||||
model,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { FormsModule, Validators } from '@angular/forms';
|
||||
import { logger } from '@isa/core/logging';
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
RemissionListType,
|
||||
RemissionReturnReceiptService,
|
||||
RemissionStore,
|
||||
UpdateItem,
|
||||
} from '@isa/remission/data-access';
|
||||
import { TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { injectFeedbackDialog, injectNumberInputDialog } from '@isa/ui/dialog';
|
||||
@@ -80,11 +82,12 @@ export class RemissionListItemActionsComponent {
|
||||
stockToRemit = input.required<number>();
|
||||
|
||||
/**
|
||||
* ModelSignal indicating whether remission items are currently being processed.
|
||||
* Used to prevent multiple submissions or actions.
|
||||
* @default false
|
||||
* Model to track if a delete operation is in progress.
|
||||
* And the item being deleted or updated.
|
||||
*/
|
||||
inProgress = model<boolean>();
|
||||
removeOrUpdateItem = model<UpdateItem>({
|
||||
inProgress: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Signal indicating whether remission has started.
|
||||
@@ -114,6 +117,12 @@ export class RemissionListItemActionsComponent {
|
||||
() => this.item()?.source === RemissionItemSource.ManuallyAdded,
|
||||
);
|
||||
|
||||
/**
|
||||
* Signal to highlight the change remission quantity button when dialog is open.
|
||||
* Used to improve accessibility and focus management.
|
||||
*/
|
||||
highlight = signal(false);
|
||||
|
||||
/**
|
||||
* Opens a dialog to change the remission quantity for the current item.
|
||||
* Prompts the user to enter a new quantity and updates the store with the new value
|
||||
@@ -121,6 +130,7 @@ export class RemissionListItemActionsComponent {
|
||||
* If the item is not found, it updates the impediment with a comment.
|
||||
*/
|
||||
async openRemissionQuantityDialog(): Promise<void> {
|
||||
this.highlight.set(true);
|
||||
const dialogRef = this.#dialog({
|
||||
title: 'Remi-Menge ändern',
|
||||
displayClose: true,
|
||||
@@ -150,6 +160,7 @@ export class RemissionListItemActionsComponent {
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
this.highlight.set(false);
|
||||
|
||||
// Dialog Close
|
||||
if (!result) {
|
||||
@@ -168,28 +179,37 @@ export class RemissionListItemActionsComponent {
|
||||
} else if (itemId) {
|
||||
// Produkt nicht gefunden CTA
|
||||
try {
|
||||
this.inProgress.set(true);
|
||||
this.removeOrUpdateItem.set({ inProgress: true });
|
||||
|
||||
let itemToUpdate: RemissionItem | undefined;
|
||||
if (this.remissionListType() === RemissionListType.Pflicht) {
|
||||
await this.#remissionReturnReceiptService.updateReturnItemImpediment({
|
||||
itemId,
|
||||
comment: 'Produkt nicht gefunden',
|
||||
});
|
||||
itemToUpdate =
|
||||
await this.#remissionReturnReceiptService.updateReturnItemImpediment(
|
||||
{
|
||||
itemId,
|
||||
comment: 'Produkt nicht gefunden',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (this.remissionListType() === RemissionListType.Abteilung) {
|
||||
await this.#remissionReturnReceiptService.updateReturnSuggestionImpediment(
|
||||
{
|
||||
itemId,
|
||||
comment: 'Produkt nicht gefunden',
|
||||
},
|
||||
);
|
||||
itemToUpdate =
|
||||
await this.#remissionReturnReceiptService.updateReturnSuggestionImpediment(
|
||||
{
|
||||
itemId,
|
||||
comment: 'Produkt nicht gefunden',
|
||||
},
|
||||
);
|
||||
}
|
||||
this.removeOrUpdateItem.set({
|
||||
inProgress: false,
|
||||
itemId,
|
||||
impediment: itemToUpdate?.impediment,
|
||||
});
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to update impediment', error);
|
||||
this.removeOrUpdateItem.set({ inProgress: false });
|
||||
}
|
||||
|
||||
this.inProgress.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,17 +220,17 @@ export class RemissionListItemActionsComponent {
|
||||
*/
|
||||
async deleteItemFromList() {
|
||||
const itemId = this.item()?.id;
|
||||
if (!itemId || this.inProgress()) {
|
||||
if (!itemId || this.removeOrUpdateItem().inProgress) {
|
||||
return;
|
||||
}
|
||||
this.inProgress.set(true);
|
||||
this.removeOrUpdateItem.set({ inProgress: true });
|
||||
|
||||
try {
|
||||
await this.#remissionReturnReceiptService.deleteReturnItem({ itemId });
|
||||
this.removeOrUpdateItem.set({ inProgress: false, itemId });
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to delete return item', error);
|
||||
this.removeOrUpdateItem.set({ inProgress: false });
|
||||
}
|
||||
|
||||
this.inProgress.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
[selectedQuantityDiffersFromStockToRemit]="
|
||||
selectedQuantityDiffersFromStockToRemit()
|
||||
"
|
||||
(inProgressChange)="inProgress.set($event)"
|
||||
(removeOrUpdateItemChange)="removeOrUpdateItem.emit($event)"
|
||||
></remi-feature-remission-list-item-actions>
|
||||
</ui-item-row-data>
|
||||
</ui-client-row>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
:host {
|
||||
@apply w-full;
|
||||
@apply w-full border border-solid border-transparent rounded-2xl;
|
||||
|
||||
&:has(
|
||||
[data-what="button"][data-which="change-remission-quantity"].highlight
|
||||
) {
|
||||
@apply border border-solid border-isa-accent-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-client-row {
|
||||
|
||||
@@ -46,6 +46,7 @@ jest.mock('@isa/remission/data-access', () => ({
|
||||
// Mock the RemissionStore
|
||||
const mockRemissionStore = {
|
||||
selectedQuantity: signal({}),
|
||||
removeItem: jest.fn(),
|
||||
};
|
||||
|
||||
describe('RemissionListItemComponent', () => {
|
||||
@@ -112,6 +113,7 @@ describe('RemissionListItemComponent', () => {
|
||||
// Reset mocks before each test
|
||||
jest.clearAllMocks();
|
||||
mockRemissionStore.selectedQuantity.set({});
|
||||
mockRemissionStore.removeItem.mockClear();
|
||||
|
||||
// Reset the mocked functions to return default values
|
||||
const {
|
||||
@@ -176,19 +178,11 @@ describe('RemissionListItemComponent', () => {
|
||||
expect(component.stockFetching()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have inProgress model with undefined default', () => {
|
||||
it('should have removeOrUpdateItem output', () => {
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
expect(component.inProgress()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should accept inProgress model value', () => {
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.componentRef.setInput('inProgress', true);
|
||||
fixture.detectChanges();
|
||||
expect(component.inProgress()).toBe(true);
|
||||
expect(component.removeOrUpdateItem).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -720,4 +714,37 @@ describe('RemissionListItemComponent', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngOnDestroy', () => {
|
||||
it('should remove item from store when component is destroyed', () => {
|
||||
// Arrange
|
||||
const mockItem = createMockReturnItem({ id: 123 });
|
||||
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
// Act
|
||||
component.ngOnDestroy();
|
||||
|
||||
// Assert
|
||||
expect(mockRemissionStore.removeItem).toHaveBeenCalledWith(123);
|
||||
expect(mockRemissionStore.removeItem).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not call removeItem when item has no id', () => {
|
||||
// Arrange
|
||||
const mockItem = createMockReturnItem({ id: undefined });
|
||||
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
// Act
|
||||
component.ngOnDestroy();
|
||||
|
||||
// Assert
|
||||
expect(mockRemissionStore.removeItem).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
model,
|
||||
OnDestroy,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
ReturnItem,
|
||||
ReturnSuggestion,
|
||||
StockInfo,
|
||||
UpdateItem,
|
||||
} from '@isa/remission/data-access';
|
||||
import {
|
||||
ProductInfoComponent,
|
||||
@@ -59,7 +61,7 @@ import { LabelComponent, Labeltype } from '@isa/ui/label';
|
||||
LabelComponent,
|
||||
],
|
||||
})
|
||||
export class RemissionListItemComponent {
|
||||
export class RemissionListItemComponent implements OnDestroy {
|
||||
/**
|
||||
* Type of label to display for the item.
|
||||
* Defaults to 'tag', can be changed to 'notice' or other types as needed.
|
||||
@@ -103,11 +105,10 @@ export class RemissionListItemComponent {
|
||||
stockFetching = input<boolean>(false);
|
||||
|
||||
/**
|
||||
* ModelSignal indicating whether remission items are currently being processed.
|
||||
* Used to prevent multiple submissions or actions.
|
||||
* @default false
|
||||
* Output event emitter for when the item is deleted or updated.
|
||||
* Emits an object containing the in-progress state and the item itself.
|
||||
*/
|
||||
inProgress = model<boolean>();
|
||||
removeOrUpdateItem = output<UpdateItem>();
|
||||
|
||||
/**
|
||||
* Optional product group value for display or filtering.
|
||||
@@ -219,4 +220,16 @@ export class RemissionListItemComponent {
|
||||
const attempts = this.item()?.impediment?.attempts;
|
||||
return `${comment}${attempts ? ` (${attempts})` : ''}`;
|
||||
});
|
||||
|
||||
/**
|
||||
* Cleans up the selected item from the store when the component is destroyed.
|
||||
* Removes the item using its ID.
|
||||
* @returns void
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
const itemId = this.item()?.id;
|
||||
if (itemId) {
|
||||
this.#store.removeItem(itemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
{{ hits() }} Einträge
|
||||
</span>
|
||||
|
||||
<div class="flex flex-col gap-4 w-full items-center justify-center mb-24">
|
||||
<div class="flex flex-col gap-4 w-full items-center justify-center mb-36">
|
||||
@for (item of items(); track item.id) {
|
||||
@defer (on viewport) {
|
||||
<remi-feature-remission-list-item
|
||||
@@ -32,7 +32,7 @@
|
||||
[stock]="getStockForItem(item)"
|
||||
[stockFetching]="inStockFetching()"
|
||||
[productGroupValue]="getProductGroupValueForItem(item)"
|
||||
(inProgressChange)="onListItemActionInProgress($event)"
|
||||
(removeOrUpdateItem)="onRemoveOrUpdateItem($event)"
|
||||
></remi-feature-remission-list-item>
|
||||
} @placeholder {
|
||||
<div class="h-[7.75rem] w-full flex items-center justify-center">
|
||||
@@ -54,9 +54,14 @@
|
||||
></remi-feature-remission-list-empty-state>
|
||||
</div>
|
||||
|
||||
<utils-scroll-top-button
|
||||
class="flex flex-col self-end fixed bottom-6 mr-6"
|
||||
[class.scroll-top-button-spacing-bottom]="remissionStarted()"
|
||||
></utils-scroll-top-button>
|
||||
|
||||
@if (remissionStarted()) {
|
||||
<ui-stateful-button
|
||||
class="fixed right-6 bottom-6"
|
||||
class="flex flex-col self-end fixed bottom-6 mr-6"
|
||||
(clicked)="remitItems()"
|
||||
(action)="remitItems()"
|
||||
[(state)]="remitItemsState"
|
||||
@@ -70,7 +75,7 @@
|
||||
size="large"
|
||||
color="brand"
|
||||
[pending]="remitItemsInProgress()"
|
||||
[disabled]="!hasSelectedItems() || listItemActionInProgress()"
|
||||
[disabled]="!hasSelectedItems() || removeItemInProgress()"
|
||||
>
|
||||
</ui-stateful-button>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.scroll-top-button-spacing-bottom {
|
||||
@apply bottom-[5.5rem];
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
effect,
|
||||
untracked,
|
||||
signal,
|
||||
linkedSignal,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import {
|
||||
@@ -16,7 +17,10 @@ import {
|
||||
FilterService,
|
||||
SearchTrigger,
|
||||
} from '@isa/shared/filter';
|
||||
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
||||
import {
|
||||
injectRestoreScrollPosition,
|
||||
ScrollTopButtonComponent,
|
||||
} from '@isa/utils/scroll-position';
|
||||
import { RemissionStartCardComponent } from './remission-start-card/remission-start-card.component';
|
||||
import { RemissionListSelectComponent } from './remission-list-select/remission-list-select.component';
|
||||
import {
|
||||
@@ -40,16 +44,21 @@ import {
|
||||
calculateAvailableStock,
|
||||
RemissionReturnReceiptService,
|
||||
getStockToRemit,
|
||||
RemissionListType,
|
||||
RemissionResponseArgsErrorMessage,
|
||||
UpdateItem,
|
||||
orderByListItems,
|
||||
getItemType,
|
||||
} from '@isa/remission/data-access';
|
||||
import { injectDialog } from '@isa/ui/dialog';
|
||||
import { injectDialog, injectFeedbackErrorDialog } from '@isa/ui/dialog';
|
||||
import { SearchItemToRemitDialogComponent } from '@isa/remission/shared/search-item-to-remit-dialog';
|
||||
import { RemissionListType } from '@isa/remission/data-access';
|
||||
import { RemissionReturnCardComponent } from './remission-return-card/remission-return-card.component';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { RemissionProcessedHintComponent } from './remission-processed-hint/remission-processed-hint.component';
|
||||
import { RemissionListDepartmentElementsComponent } from './remission-list-department-elements/remission-list-department-elements.component';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { RemissionListEmptyStateComponent } from './remission-list-empty-state/remission-list-empty-state.component';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
function querySettingsFactory() {
|
||||
return inject(ActivatedRoute).snapshot.data['querySettings'];
|
||||
@@ -92,6 +101,7 @@ function querySettingsFactory() {
|
||||
RemissionListDepartmentElementsComponent,
|
||||
RemissionProcessedHintComponent,
|
||||
RemissionListEmptyStateComponent,
|
||||
ScrollTopButtonComponent,
|
||||
],
|
||||
host: {
|
||||
'[class]':
|
||||
@@ -118,6 +128,7 @@ export class RemissionListComponent {
|
||||
activatedTabId = injectTabId();
|
||||
|
||||
searchItemToRemitDialog = injectDialog(SearchItemToRemitDialogComponent);
|
||||
errorDialog = injectFeedbackErrorDialog();
|
||||
|
||||
/**
|
||||
* FilterService instance for managing filter state and queries.
|
||||
@@ -167,7 +178,7 @@ export class RemissionListComponent {
|
||||
* Signal indicating whether a remission list item deletion is in progress.
|
||||
* Used to disable actions while deletion is happening.
|
||||
*/
|
||||
listItemActionInProgress = signal(false);
|
||||
removeItemInProgress = signal(false);
|
||||
|
||||
/**
|
||||
* Computed signal for the current search term from the filter service.
|
||||
@@ -256,7 +267,7 @@ export class RemissionListComponent {
|
||||
* Computed signal for the remission items to display.
|
||||
* @returns Array of ReturnItem or ReturnSuggestion.
|
||||
*/
|
||||
items = computed(() => {
|
||||
items = linkedSignal(() => {
|
||||
const value = this.listResponseValue();
|
||||
return value?.result ? value.result : [];
|
||||
});
|
||||
@@ -361,15 +372,29 @@ export class RemissionListComponent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the deletion of a remission list item.
|
||||
* Updates the in-progress state and reloads the list and receipt upon completion.
|
||||
*
|
||||
* @param inProgress - Whether the deletion is currently in progress
|
||||
* Handles the removal or update of an item from the remission list.
|
||||
* Updates the local items signal and the remission store accordingly.
|
||||
* Items with impediments are automatically moved to the end of the list and sorted by attempt count.
|
||||
* @param param0 - Object containing inProgress state, itemId, and optional impediment.
|
||||
*/
|
||||
onListItemActionInProgress(inProgress: boolean) {
|
||||
this.listItemActionInProgress.set(inProgress);
|
||||
if (!inProgress) {
|
||||
this.reloadListAndReturnData();
|
||||
onRemoveOrUpdateItem({ inProgress, itemId, impediment }: UpdateItem) {
|
||||
this.removeItemInProgress.set(inProgress);
|
||||
if (!inProgress && itemId) {
|
||||
if (!impediment || (impediment.attempts && impediment.attempts >= 4)) {
|
||||
this.items.set(this.items().filter((item) => item.id !== itemId)); // Filter Item if no impediment or attempts >= 4 (#5361)
|
||||
} else {
|
||||
// Update Item
|
||||
this.items.update((items) => {
|
||||
const updatedItems = items.map((item) =>
|
||||
item.id === itemId ? { ...item, impediment } : item,
|
||||
);
|
||||
orderByListItems(updatedItems);
|
||||
return updatedItems;
|
||||
});
|
||||
}
|
||||
|
||||
// Always Unselect Item
|
||||
this.#store.removeItem(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,36 +416,52 @@ export class RemissionListComponent {
|
||||
});
|
||||
|
||||
/**
|
||||
* Effect that handles the case when there are no items in the remission list after a search.
|
||||
* If the search was triggered by the user, it opens a dialog to search for items to remit.
|
||||
* If remission has already started, it adds the found items to the remission store and remits them.
|
||||
* If not, it navigates to the default remission list.
|
||||
* Effect that handles scenarios where a search yields no results.
|
||||
* If the search was user-initiated and returned no hits, it opens a dialog
|
||||
* to allow the user to add a new item to remit.
|
||||
* If only one hit is found and a remission is started, it selects that item automatically.
|
||||
* This effect runs whenever the remission or stock resource status changes,
|
||||
* or when the search term changes.
|
||||
* It ensures that the user is prompted appropriately based on their actions and the current state of the remission process.
|
||||
* It also checks if the remission is started or if the list type is 'Abteilung' to determine navigation behavior.
|
||||
* @see {@link
|
||||
* https://angular.dev/guide/effects} for more information on Angular effects.
|
||||
* @remarks This effect uses `untracked` to avoid unnecessary re-evaluations
|
||||
* when accessing certain signals.
|
||||
*/
|
||||
emptySearchResultEffect = effect(() => {
|
||||
const status = this.remissionResource.status();
|
||||
const stockStatus = this.inStockResource.status();
|
||||
const searchTerm: string | undefined = this.searchTerm();
|
||||
|
||||
if (status !== 'resolved') {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasItems = !!this.remissionResource.value()?.result?.length;
|
||||
|
||||
if (hasItems || !searchTerm || !this.hasValidSearchTerm()) {
|
||||
if (status !== 'resolved' || stockStatus !== 'resolved') {
|
||||
return;
|
||||
}
|
||||
|
||||
untracked(() => {
|
||||
if (!this.searchTriggeredByUser()) {
|
||||
const hits = this.hits();
|
||||
|
||||
// #5338 - Select item automatically if only one hit after search
|
||||
if (
|
||||
!!hits ||
|
||||
!searchTerm ||
|
||||
!this.hasValidSearchTerm() ||
|
||||
!this.searchTriggeredByUser()
|
||||
) {
|
||||
if (hits === 1 && this.remissionStarted()) {
|
||||
this.#store.clearSelectedItems();
|
||||
this.preselectRemissionItem(this.items()[0]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.searchItemToRemitDialog({
|
||||
data: {
|
||||
searchTerm,
|
||||
isDepartment: this.isDepartment(),
|
||||
},
|
||||
}).closed.subscribe(async (result) => {
|
||||
this.#store.clearSelectedItems();
|
||||
if (result) {
|
||||
if (this.remissionStarted()) {
|
||||
for (const item of result) {
|
||||
@@ -432,9 +473,8 @@ export class RemissionListComponent {
|
||||
} else if (this.isDepartment()) {
|
||||
return await this.navigateToDefaultRemissionList();
|
||||
}
|
||||
|
||||
this.reloadListAndReturnData();
|
||||
}
|
||||
this.reloadListAndReturnData();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -489,21 +529,14 @@ export class RemissionListComponent {
|
||||
? undefined
|
||||
: inStock - quantity,
|
||||
},
|
||||
type: remissionListType,
|
||||
type: getItemType(item, remissionListType),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.remitItemsState.set('success');
|
||||
this.reloadListAndReturnData();
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to remit items', error);
|
||||
this.remitItemsError.set(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Artikel konnten nicht remittiert werden',
|
||||
);
|
||||
this.remitItemsState.set('error');
|
||||
await this.handleRemitItemsError(error);
|
||||
}
|
||||
|
||||
this.#store.clearSelectedItems();
|
||||
@@ -520,6 +553,62 @@ export class RemissionListComponent {
|
||||
this.#store.reloadReturn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-Selects a remission item if it has available stock and can be remitted.
|
||||
* Updates the remission store with the selected item.
|
||||
* @param item - The ReturnItem or ReturnSuggestion to select.
|
||||
* @returns void
|
||||
*/
|
||||
preselectRemissionItem(item: RemissionItem) {
|
||||
if (!!item && item.id) {
|
||||
const inStock = this.getAvailableStockForItem(item);
|
||||
const stockToRemit = getStockToRemit({
|
||||
remissionItem: item,
|
||||
remissionListType: this.selectedRemissionListType(),
|
||||
availableStock: inStock,
|
||||
});
|
||||
|
||||
if (inStock > 0 && stockToRemit > 0) {
|
||||
this.#store.selectRemissionItem(item.id, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors that occur during the remission of items.
|
||||
* Logs the error, displays an error dialog, and reloads the list and return data.
|
||||
* If the error indicates that the remission is already completed, it clears the remission state.
|
||||
* Sets the stateful button to 'error' to indicate the failure.
|
||||
* @param error - The error object caught during the remission process.
|
||||
* @returns A promise that resolves when the error handling is complete.
|
||||
*/
|
||||
async handleRemitItemsError(error: any) {
|
||||
this.#logger.error('Failed to remit items', error);
|
||||
|
||||
const errorMessage =
|
||||
error?.error?.message ??
|
||||
error?.message ??
|
||||
'Artikel konnten nicht remittiert werden';
|
||||
|
||||
this.remitItemsError.set(errorMessage);
|
||||
|
||||
await firstValueFrom(
|
||||
this.errorDialog({
|
||||
data: {
|
||||
errorMessage,
|
||||
},
|
||||
}).closed,
|
||||
);
|
||||
|
||||
if (errorMessage === RemissionResponseArgsErrorMessage.AlreadyCompleted) {
|
||||
this.#store.clearState();
|
||||
}
|
||||
|
||||
this.reloadListAndReturnData();
|
||||
|
||||
this.remitItemsState.set('error'); // Stateful-Button auf Error setzen
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the default remission list based on the current activated tab ID.
|
||||
* This method is used to redirect the user to the remission list after completing or starting a remission.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { inject, resource } from '@angular/core';
|
||||
import { ListResponseArgs, ResponseArgsError } from '@isa/common/data-access';
|
||||
import {
|
||||
orderByListItems,
|
||||
QueryTokenInput,
|
||||
RemissionItem,
|
||||
RemissionListType,
|
||||
@@ -9,7 +10,6 @@ import {
|
||||
RemissionSupplierService,
|
||||
} from '@isa/remission/data-access';
|
||||
import { SearchTrigger } from '@isa/shared/filter';
|
||||
import { parseISO, compareDesc } from 'date-fns';
|
||||
import { isEan } from '@isa/utils/ean-validation';
|
||||
|
||||
/**
|
||||
@@ -77,11 +77,10 @@ export const createRemissionListResource = (
|
||||
const isReload = params.searchTrigger === 'reload';
|
||||
|
||||
// #5273
|
||||
// #5387 Hotfix Navigation | Reload has priority over Exact Search
|
||||
if (isReload) {
|
||||
queryToken.input = {};
|
||||
}
|
||||
|
||||
if (exactSearch) {
|
||||
} else if (exactSearch) {
|
||||
queryToken.filter = {};
|
||||
queryToken.orderBy = [];
|
||||
}
|
||||
@@ -144,7 +143,7 @@ export const createRemissionListResource = (
|
||||
const hasOrderBy = !!queryToken?.orderBy && queryToken.orderBy.length > 0;
|
||||
|
||||
if (!hasOrderBy && res && res.result && Array.isArray(res.result)) {
|
||||
sortResponseResult(res);
|
||||
orderByListItems(res.result);
|
||||
}
|
||||
|
||||
return res;
|
||||
@@ -152,55 +151,6 @@ export const createRemissionListResource = (
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts the remission items in the response based on specific criteria:
|
||||
* - Items with impediments are moved to the end of the list.
|
||||
* - Manually added items are prioritized to appear first.
|
||||
* - (Commented out) Items can be sorted by creation date in descending order.
|
||||
* @param {ListResponseArgs<RemissionItem>} resopnse - The response object containing remission items to be sorted
|
||||
* @returns {void} The function modifies the response object in place
|
||||
*/
|
||||
const sortResponseResult = (
|
||||
resopnse: ListResponseArgs<RemissionItem>,
|
||||
): void => {
|
||||
resopnse.result.sort((a, b) => {
|
||||
const aHasImpediment = !!a.impediment;
|
||||
const bHasImpediment = !!b.impediment;
|
||||
const aIsManuallyAdded = a.source === 'manually-added';
|
||||
const bIsManuallyAdded = b.source === 'manually-added';
|
||||
|
||||
// First priority: move all items with impediment to the end of the list
|
||||
if (!aHasImpediment && bHasImpediment) {
|
||||
return -1;
|
||||
}
|
||||
if (aHasImpediment && !bHasImpediment) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Second priority: manually-added items come first
|
||||
if (aIsManuallyAdded && !bIsManuallyAdded) {
|
||||
return -1;
|
||||
}
|
||||
if (!aIsManuallyAdded && bIsManuallyAdded) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// #5295 Fix - Sortierung über Created (Pflichtremission) wird wie auch die Sortierung über die SORT Nummer (Abteilungsremission) bereits über das Backend erledigt
|
||||
// Third priority: sort by created date (latest first)
|
||||
// if (a.created && b.created) {
|
||||
// const dateA = parseISO(a.created);
|
||||
// const dateB = parseISO(b.created);
|
||||
// return compareDesc(dateA, dateB); // Descending order (latest first)
|
||||
// }
|
||||
|
||||
// // Handle cases where created date might be missing
|
||||
// if (a.created && !b.created) return -1;
|
||||
// if (!a.created && b.created) return 1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
|
||||
// #5128 #5234 Bei Exact Search soll er über Alle Listen nur mit dem Input ohne aktive Filter / orderBy suchen
|
||||
/**
|
||||
* Checks if the query token is an exact search based on the search trigger.
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ReceiptItem,
|
||||
RemissionResponseArgsErrorMessage,
|
||||
RemissionReturnReceiptService,
|
||||
} from '@isa/remission/data-access';
|
||||
import { ProductFormatComponent } from '@isa/shared/product-foramt';
|
||||
@@ -20,6 +21,8 @@ import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaActionClose } from '@isa/icons';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { injectFeedbackErrorDialog } from '@isa/ui/dialog';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Component for displaying a single receipt item within the remission return receipt details.
|
||||
@@ -55,6 +58,8 @@ export class RemissionReturnReceiptDetailsItemComponent {
|
||||
}));
|
||||
#returnReceiptService = inject(RemissionReturnReceiptService);
|
||||
|
||||
errorDialog = injectFeedbackErrorDialog();
|
||||
|
||||
/**
|
||||
* Required input for the receipt item to display.
|
||||
* Contains product information and quantity details.
|
||||
@@ -85,7 +90,7 @@ export class RemissionReturnReceiptDetailsItemComponent {
|
||||
|
||||
removing = signal(false);
|
||||
|
||||
removed = output<ReceiptItem>();
|
||||
reloadReturn = output<void>();
|
||||
|
||||
async remove() {
|
||||
if (this.removing()) {
|
||||
@@ -98,10 +103,25 @@ export class RemissionReturnReceiptDetailsItemComponent {
|
||||
returnId: this.returnId(),
|
||||
receiptItemId: this.item().id,
|
||||
});
|
||||
this.removed.emit(this.item());
|
||||
} catch (error) {
|
||||
this.#logger.error('Failed to remove item', error);
|
||||
await this.handleRemoveItemError(error);
|
||||
}
|
||||
this.reloadReturn.emit();
|
||||
this.removing.set(false);
|
||||
}
|
||||
|
||||
async handleRemoveItemError(error: any) {
|
||||
this.#logger.error('Failed to remove item', error);
|
||||
|
||||
const errorMessage =
|
||||
error?.error?.message ?? RemissionResponseArgsErrorMessage.AlreadyRemoved;
|
||||
|
||||
await firstValueFrom(
|
||||
this.errorDialog({
|
||||
data: {
|
||||
errorMessage,
|
||||
},
|
||||
}).closed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
[removeable]="canRemoveItems()"
|
||||
[receiptId]="receiptId()"
|
||||
[returnId]="returnId()"
|
||||
(removed)="returnResource.reload()"
|
||||
(reloadReturn)="returnResource.reload()"
|
||||
></remi-remission-return-receipt-details-item>
|
||||
@if (!last) {
|
||||
<hr class="border-isa-neutral-300" />
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
DropdownButtonComponent,
|
||||
DropdownOptionComponent,
|
||||
} from '@isa/ui/input-controls';
|
||||
import { QuantityAndReason } from './select-remi-quantity-and-reason.component';
|
||||
import { QuantityAndReason } from './select-remi-quantity-and-reason-dialog.component';
|
||||
import { ReturnValue } from '@isa/common/data-access';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaActionChevronDown, isaActionChevronUp } from '@isa/icons';
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
@if (item()) {
|
||||
<remi-select-remi-quantity-and-reason></remi-select-remi-quantity-and-reason>
|
||||
} @else {
|
||||
<button
|
||||
class="absolute top-4 right-[1.33rem]"
|
||||
type="button"
|
||||
uiTextButton
|
||||
size="small"
|
||||
color="subtle"
|
||||
(click)="close(undefined)"
|
||||
tabindex="-1"
|
||||
data-what="button"
|
||||
data-which="close-dialog"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
<remi-search-item-to-remit-list></remi-search-item-to-remit-list>
|
||||
}
|
||||
<button
|
||||
class="absolute top-4 right-[1.33rem]"
|
||||
type="button"
|
||||
uiTextButton
|
||||
size="small"
|
||||
color="subtle"
|
||||
(click)="close(undefined)"
|
||||
tabindex="-1"
|
||||
data-what="button"
|
||||
data-which="close-dialog"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
<remi-search-item-to-remit-list></remi-search-item-to-remit-list>
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
effect,
|
||||
isSignal,
|
||||
linkedSignal,
|
||||
signal,
|
||||
Signal,
|
||||
} from '@angular/core';
|
||||
import { DialogContentDirective, NumberInputValidation } from '@isa/ui/dialog';
|
||||
import { Item } from '@isa/catalogue/data-access';
|
||||
import { TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaActionSearch } from '@isa/icons';
|
||||
import { SearchItemToRemitListComponent } from './search-item-to-remit-list.component';
|
||||
import { SelectRemiQuantityAndReasonComponent } from './select-remi-quantity-and-reason.component';
|
||||
import { Validators } from '@angular/forms';
|
||||
import { ReturnSuggestion, ReturnItem } from '@isa/remission/data-access';
|
||||
import { ReturnItem } from '@isa/remission/data-access';
|
||||
|
||||
export type SearchItemToRemitDialogData = {
|
||||
searchTerm: string | Signal<string>;
|
||||
isDepartment: boolean;
|
||||
};
|
||||
|
||||
export type SearchItemToRemitDialogResult =
|
||||
SearchItemToRemitDialogData extends { isDepartment: infer D }
|
||||
? D extends true
|
||||
? ReturnSuggestion
|
||||
: ReturnItem
|
||||
: never;
|
||||
// #5273, #4768 Fix - Nur ReturnItems sind zugelassen und dürfen zur Pflichtremission hinzugefügt werden
|
||||
export type SearchItemToRemitDialogResult = ReturnItem;
|
||||
|
||||
@Component({
|
||||
selector: 'remi-search-item-to-remit-dialog',
|
||||
@@ -35,11 +25,7 @@ export type SearchItemToRemitDialogResult =
|
||||
styleUrls: ['./search-item-to-remit-dialog.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
TextButtonComponent,
|
||||
SearchItemToRemitListComponent,
|
||||
SelectRemiQuantityAndReasonComponent,
|
||||
],
|
||||
imports: [TextButtonComponent, SearchItemToRemitListComponent],
|
||||
providers: [provideIcons({ isaActionSearch })],
|
||||
})
|
||||
export class SearchItemToRemitDialogComponent extends DialogContentDirective<
|
||||
@@ -51,35 +37,4 @@ export class SearchItemToRemitDialogComponent extends DialogContentDirective<
|
||||
? this.data.searchTerm()
|
||||
: this.data.searchTerm,
|
||||
);
|
||||
|
||||
item = signal<Item | undefined>(undefined);
|
||||
|
||||
itemEffect = effect(() => {
|
||||
const item = this.item();
|
||||
this.dialogRef.updateSize(item ? '36rem' : 'auto');
|
||||
|
||||
if (item) {
|
||||
this.dialog.title.set(`Dieser Artikel steht nicht auf der Remi Liste`);
|
||||
} else {
|
||||
this.dialog.title.set(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
quantityValidators: NumberInputValidation[] = [
|
||||
{
|
||||
errorKey: 'required',
|
||||
inputValidator: Validators.required,
|
||||
errorText: 'Bitte geben Sie eine Menge an.',
|
||||
},
|
||||
{
|
||||
errorKey: 'min',
|
||||
inputValidator: Validators.min(1),
|
||||
errorText: 'Die Menge muss mindestens 1 sein.',
|
||||
},
|
||||
{
|
||||
errorKey: 'max',
|
||||
inputValidator: Validators.max(1000),
|
||||
errorText: 'Die Menge darf höchstens 1000 sein.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
name="isaActionSearch"
|
||||
color="brand"
|
||||
(click)="triggerSearch()"
|
||||
[pending]="searchResource.isLoading()"
|
||||
[pending]="searchResource.isLoading() || inStockResource.isLoading()"
|
||||
data-what="button"
|
||||
data-which="search-submit"
|
||||
></ui-icon-button>
|
||||
@@ -34,24 +34,23 @@
|
||||
<ng-icon size="1.5rem" name="isaOtherInfo"></ng-icon>
|
||||
</button>
|
||||
</p>
|
||||
<div class="overflow-y-auto">
|
||||
<div #list class="overflow-y-auto overflow-x-hidden">
|
||||
@if (searchResource.value()?.result; as items) {
|
||||
@for (item of items; track item.id) {
|
||||
@for (item of availableSearchResults(); track item.id) {
|
||||
@defer {
|
||||
@let inStock = getAvailableStockForItem(item);
|
||||
@if (inStock > 0) {
|
||||
<remi-search-item-to-remit
|
||||
[item]="item"
|
||||
[inStock]="inStock"
|
||||
data-what="list-item"
|
||||
data-which="search-result"
|
||||
[attr.data-item-id]="item.id"
|
||||
></remi-search-item-to-remit>
|
||||
}
|
||||
<remi-search-item-to-remit
|
||||
[item]="item"
|
||||
[inStock]="getAvailableStockForItem(item)"
|
||||
data-what="list-item"
|
||||
data-which="search-result"
|
||||
[attr.data-item-id]="item.id"
|
||||
></remi-search-item-to-remit>
|
||||
}
|
||||
}
|
||||
}
|
||||
@if (!hasItems() && !searchResource.isLoading()) {
|
||||
@if (
|
||||
!hasItems() && !searchResource.isLoading() && !inStockResource.isLoading()
|
||||
) {
|
||||
<ui-empty-state
|
||||
class="w-full justify-self-center"
|
||||
title="Keine Suchergebnisse"
|
||||
@@ -59,4 +58,8 @@
|
||||
>
|
||||
</ui-empty-state>
|
||||
}
|
||||
<utils-scroll-top-button
|
||||
class="flex flex-col self-end absolute bottom-6 right-6"
|
||||
[target]="list"
|
||||
></utils-scroll-top-button>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,8 @@ import { TooltipDirective } from '@isa/ui/tooltip';
|
||||
import { createInStockResource } from './instock.resource';
|
||||
import { calculateAvailableStock } from '@isa/remission/data-access';
|
||||
import { EmptyStateComponent } from '@isa/ui/empty-state';
|
||||
import { ScrollTopButtonComponent } from '@isa/utils/scroll-position';
|
||||
|
||||
@Component({
|
||||
selector: 'remi-search-item-to-remit-list',
|
||||
templateUrl: './search-item-to-remit-list.component.html',
|
||||
@@ -48,6 +50,7 @@ import { EmptyStateComponent } from '@isa/ui/empty-state';
|
||||
TooltipDirective,
|
||||
NgIcon,
|
||||
EmptyStateComponent,
|
||||
ScrollTopButtonComponent,
|
||||
],
|
||||
providers: [provideIcons({ isaActionSearch, isaOtherInfo })],
|
||||
})
|
||||
@@ -57,6 +60,14 @@ export class SearchItemToRemitListComponent implements OnInit {
|
||||
|
||||
searchParams = signal<SearchByTermInput | undefined>(undefined);
|
||||
|
||||
availableSearchResults = computed(() => {
|
||||
return (
|
||||
this.searchResource.value()?.result?.filter((item) => {
|
||||
return this.getAvailableStockForItem(item) > 0;
|
||||
}) ?? []
|
||||
);
|
||||
});
|
||||
|
||||
inStockResource = createInStockResource(() => {
|
||||
return {
|
||||
itemIds:
|
||||
@@ -69,7 +80,7 @@ export class SearchItemToRemitListComponent implements OnInit {
|
||||
inStockResponseValue = computed(() => this.inStockResource.value());
|
||||
|
||||
hasItems = computed(() => {
|
||||
return (this.searchResource.value()?.result?.length ?? 0) > 0;
|
||||
return (this.availableSearchResults()?.length ?? 0) > 0;
|
||||
});
|
||||
|
||||
stockInfoMap = computed(() => {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
type="button"
|
||||
uiTextButton
|
||||
color="strong"
|
||||
(click)="host.item.set(item())"
|
||||
(click)="openQuantityAndReasonDialog()"
|
||||
>
|
||||
Remimenge auswählen
|
||||
</button>
|
||||
|
||||
@@ -10,6 +10,9 @@ import { ProductInfoComponent } from '@isa/remission/shared/product';
|
||||
import { TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||
import { SearchItemToRemitDialogComponent } from './search-item-to-remit-dialog.component';
|
||||
import { injectDialog } from '@isa/ui/dialog';
|
||||
import { SelectRemiQuantityAndReasonDialogComponent } from './select-remi-quantity-and-reason-dialog.component';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'remi-search-item-to-remit',
|
||||
@@ -20,6 +23,9 @@ import { SearchItemToRemitDialogComponent } from './search-item-to-remit-dialog.
|
||||
})
|
||||
export class SearchItemToRemitComponent {
|
||||
host = inject(SearchItemToRemitDialogComponent);
|
||||
quantityAndReasonDialog = injectDialog(
|
||||
SelectRemiQuantityAndReasonDialogComponent,
|
||||
);
|
||||
|
||||
item = input.required<Item>();
|
||||
inStock = input.required<number>();
|
||||
@@ -29,4 +35,22 @@ export class SearchItemToRemitComponent {
|
||||
productInfoOrientation = computed(() => {
|
||||
return this.desktopBreakpoint() ? 'vertical' : 'horizontal';
|
||||
});
|
||||
|
||||
async openQuantityAndReasonDialog() {
|
||||
if (this.item()) {
|
||||
const dialogRef = this.quantityAndReasonDialog({
|
||||
title: 'Dieser Artikel steht nicht auf der Remi Liste',
|
||||
data: {
|
||||
item: this.item(),
|
||||
inStock: this.inStock(),
|
||||
},
|
||||
width: '36rem',
|
||||
});
|
||||
const dialogResult = await firstValueFrom(dialogRef.closed);
|
||||
|
||||
if (dialogResult) {
|
||||
this.host.close(dialogResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +1,94 @@
|
||||
<p class="text-isa-neutral-600 isa-text-body-1-regular">
|
||||
Wie viele Exemplare können remittiert werden?
|
||||
</p>
|
||||
<div class="flex flex-col gap-4">
|
||||
@for (
|
||||
quantityAndReason of quantitiesAndResons();
|
||||
track $index;
|
||||
let i = $index
|
||||
) {
|
||||
<div class="flex items-center gap-1">
|
||||
<remi-quantity-and-reason-item
|
||||
[position]="$index + 1"
|
||||
[quantityAndReason]="quantityAndReason"
|
||||
(quantityAndReasonChange)="setQuantityAndReason($index, $event)"
|
||||
class="flex-1"
|
||||
data-what="component"
|
||||
data-which="quantity-reason-item"
|
||||
[attr.data-position]="$index + 1"
|
||||
></remi-quantity-and-reason-item>
|
||||
@if (i > 0) {
|
||||
<ui-icon-button
|
||||
type="button"
|
||||
(click)="removeQuantityReasonItem($index)"
|
||||
data-what="button"
|
||||
data-which="remove-quantity"
|
||||
[attr.data-position]="$index + 1"
|
||||
name="isaActionClose"
|
||||
color="neutral"
|
||||
></ui-icon-button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 -ml-5"
|
||||
uiTextButton
|
||||
color="strong"
|
||||
(click)="addQuantityReasonItem()"
|
||||
data-what="button"
|
||||
data-which="add-quantity"
|
||||
>
|
||||
<ng-icon name="isaActionPlus" size="1.5rem"></ng-icon>
|
||||
<div>Menge hinzufügen</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-isa-accent-red isa-text-body-1-regular">
|
||||
<span>
|
||||
@if (canReturnErrors(); as errors) {
|
||||
@for (error of errors; track $index) {
|
||||
{{ error }}
|
||||
}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
color="secondary"
|
||||
size="large"
|
||||
uiButton
|
||||
(click)="host.item.set(undefined)"
|
||||
data-what="button"
|
||||
data-which="back"
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
color="primary"
|
||||
size="large"
|
||||
uiButton
|
||||
[pending]="canAddToRemiListResource.isLoading()"
|
||||
[disabled]="canAddToRemiListResource.isLoading() || canReturn() === false"
|
||||
(click)="addToRemiList()"
|
||||
data-what="button"
|
||||
data-which="save-remission"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
<remi-product-info
|
||||
[item]="{
|
||||
product: data.item.product,
|
||||
retailPrice: data.item.catalogAvailability.price,
|
||||
}"
|
||||
></remi-product-info>
|
||||
<div class="text-isa-neutral-900 flex flex-row items-center justify-end gap-8">
|
||||
<span class="isa-text-body-2-regular">Aktueller Bestand</span>
|
||||
<span class="isa-text-body-2-bold">{{ data.inStock }}x</span>
|
||||
</div>
|
||||
<p class="text-isa-neutral-600 isa-text-body-1-regular">
|
||||
Wie viele Exemplare können remittiert werden?
|
||||
</p>
|
||||
<div class="flex flex-col gap-4">
|
||||
@for (
|
||||
quantityAndReason of quantitiesAndResons();
|
||||
track $index;
|
||||
let i = $index
|
||||
) {
|
||||
<div class="flex items-center gap-1">
|
||||
<remi-quantity-and-reason-item
|
||||
[position]="$index + 1"
|
||||
[quantityAndReason]="quantityAndReason"
|
||||
(quantityAndReasonChange)="setQuantityAndReason($index, $event)"
|
||||
class="flex-1"
|
||||
data-what="component"
|
||||
data-which="quantity-reason-item"
|
||||
[attr.data-position]="$index + 1"
|
||||
></remi-quantity-and-reason-item>
|
||||
@if (i > 0) {
|
||||
<ui-icon-button
|
||||
type="button"
|
||||
(click)="removeQuantityReasonItem($index)"
|
||||
data-what="button"
|
||||
data-which="remove-quantity"
|
||||
[attr.data-position]="$index + 1"
|
||||
name="isaActionClose"
|
||||
color="neutral"
|
||||
></ui-icon-button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 -ml-5"
|
||||
uiTextButton
|
||||
color="strong"
|
||||
(click)="addQuantityReasonItem()"
|
||||
data-what="button"
|
||||
data-which="add-quantity"
|
||||
>
|
||||
<ng-icon name="isaActionPlus" size="1.5rem"></ng-icon>
|
||||
<div>Menge hinzufügen</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-isa-accent-red isa-text-body-1-regular">
|
||||
<span>
|
||||
@if (canReturnErrors(); as errors) {
|
||||
@for (error of errors; track $index) {
|
||||
{{ error }}
|
||||
}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
color="secondary"
|
||||
size="large"
|
||||
uiButton
|
||||
(click)="close(undefined)"
|
||||
data-what="button"
|
||||
data-which="back"
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
color="primary"
|
||||
size="large"
|
||||
uiButton
|
||||
[pending]="canAddToRemiListResource.isLoading()"
|
||||
[disabled]="canAddToRemiListResource.isLoading() || canReturn() === false"
|
||||
(click)="addToRemiList()"
|
||||
data-what="button"
|
||||
data-which="save-remission"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply grid grid-flow-row gap-6 h-full;
|
||||
}
|
||||
@@ -1,184 +1,196 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
model,
|
||||
resource,
|
||||
} from '@angular/core';
|
||||
import { SearchItemToRemitDialogComponent } from './search-item-to-remit-dialog.component';
|
||||
import { QuantityAndReasonItemComponent } from './quantity-and-reason-item.component';
|
||||
import {
|
||||
ButtonComponent,
|
||||
TextButtonComponent,
|
||||
IconButtonComponent,
|
||||
} from '@isa/ui/buttons';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaActionPlus, isaActionClose } from '@isa/icons';
|
||||
import {
|
||||
RemissionSearchService,
|
||||
RemissionStore,
|
||||
ReturnItem,
|
||||
ReturnSuggestion,
|
||||
} from '@isa/remission/data-access';
|
||||
import { injectFeedbackDialog } from '@isa/ui/dialog';
|
||||
import { BatchResponseArgs } from '@isa/common/data-access';
|
||||
|
||||
export interface QuantityAndReason {
|
||||
quantity: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'remi-select-remi-quantity-and-reason',
|
||||
templateUrl: './select-remi-quantity-and-reason.component.html',
|
||||
styleUrls: ['./select-remi-quantity-and-reason.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
QuantityAndReasonItemComponent,
|
||||
TextButtonComponent,
|
||||
NgIcon,
|
||||
ButtonComponent,
|
||||
IconButtonComponent,
|
||||
],
|
||||
providers: [provideIcons({ isaActionPlus, isaActionClose })],
|
||||
})
|
||||
export class SelectRemiQuantityAndReasonComponent {
|
||||
#remiService = inject(RemissionSearchService);
|
||||
#remiStore = inject(RemissionStore);
|
||||
#feedbackDialog = injectFeedbackDialog();
|
||||
host = inject(SearchItemToRemitDialogComponent);
|
||||
|
||||
initialItem: QuantityAndReason = { quantity: 0, reason: '' };
|
||||
|
||||
quantitiesAndResons = model<QuantityAndReason[]>([this.initialItem]);
|
||||
|
||||
addQuantityReasonItem(): void {
|
||||
this.quantitiesAndResons.update((items) => [...items, this.initialItem]);
|
||||
}
|
||||
|
||||
removeQuantityReasonItem(position: number): void {
|
||||
const currentItems = this.quantitiesAndResons();
|
||||
if (currentItems.length > 1) {
|
||||
this.quantitiesAndResons.update((items) =>
|
||||
items.filter((_, index) => index !== position),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setQuantityAndReason(position: number, qar: QuantityAndReason): void {
|
||||
this.quantitiesAndResons.update((items) => {
|
||||
const newItems = [...items];
|
||||
newItems[position] = qar;
|
||||
return newItems;
|
||||
});
|
||||
}
|
||||
|
||||
params = computed(() => {
|
||||
const items = this.quantitiesAndResons();
|
||||
const item = this.host.item();
|
||||
|
||||
if (!item) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return items.map((qar) => ({
|
||||
item,
|
||||
quantity: qar.quantity,
|
||||
reason: qar.reason,
|
||||
}));
|
||||
});
|
||||
|
||||
canAddToRemiListResource = resource({
|
||||
params: this.params,
|
||||
loader: async ({ params, abortSignal }) => {
|
||||
if (
|
||||
!this.host.item() ||
|
||||
params.some((p) => !p.reason) ||
|
||||
params.some((p) => !p.quantity)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const maxQuantityErrors = params.filter((p) => !(p.quantity <= 999));
|
||||
if (maxQuantityErrors.length > 0) {
|
||||
const errRes: BatchResponseArgs<ReturnItem> = {
|
||||
completed: false,
|
||||
error: true,
|
||||
total: maxQuantityErrors.length,
|
||||
invalidProperties: {
|
||||
quantity: 'Die Menge darf maximal 999 sein.',
|
||||
},
|
||||
};
|
||||
return errRes;
|
||||
}
|
||||
|
||||
return this.#remiService.canAddItemToRemiList(params, abortSignal);
|
||||
},
|
||||
});
|
||||
|
||||
canReturn = computed(() => {
|
||||
const results = this.canAddToRemiListResource.value();
|
||||
|
||||
if (!results) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (results.failed && results.failed.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
results.successful &&
|
||||
results.successful.length === this.quantitiesAndResons().length
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
canReturnErrors = computed(() => {
|
||||
const results = this.canAddToRemiListResource.value();
|
||||
if (results?.invalidProperties) {
|
||||
return Object.values(results.invalidProperties);
|
||||
}
|
||||
|
||||
if (!results?.failed) {
|
||||
return [];
|
||||
}
|
||||
return results.failed.map((item) =>
|
||||
item.invalidProperties
|
||||
? Object.values(item.invalidProperties).join(', ')
|
||||
: [],
|
||||
) as string[];
|
||||
});
|
||||
|
||||
async addToRemiList() {
|
||||
const canAddValue = this.canAddToRemiListResource.value();
|
||||
|
||||
if (!canAddValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (canAddValue.failed?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// #5273, #4768 Fix - Items dürfen nur zur Pflichtremission hinzugefügt werden
|
||||
const result: Array<ReturnItem> = await this.#remiService.addToList(
|
||||
this.params(),
|
||||
);
|
||||
|
||||
this.#feedbackDialog({
|
||||
data: {
|
||||
message: this.#remiStore.remissionStarted()
|
||||
? 'Wurde zum Warenbegleitschein hinzugefügt'
|
||||
: 'Wurde zur Remi Liste hinzugefügt',
|
||||
},
|
||||
});
|
||||
|
||||
this.host.close(result);
|
||||
}
|
||||
}
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
model,
|
||||
resource,
|
||||
} from '@angular/core';
|
||||
import { QuantityAndReasonItemComponent } from './quantity-and-reason-item.component';
|
||||
import {
|
||||
ButtonComponent,
|
||||
TextButtonComponent,
|
||||
IconButtonComponent,
|
||||
} from '@isa/ui/buttons';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaActionPlus, isaActionClose } from '@isa/icons';
|
||||
import {
|
||||
RemissionSearchService,
|
||||
RemissionStore,
|
||||
ReturnItem,
|
||||
} from '@isa/remission/data-access';
|
||||
import { DialogContentDirective, injectFeedbackDialog } from '@isa/ui/dialog';
|
||||
import { BatchResponseArgs } from '@isa/common/data-access';
|
||||
import { Item } from '@isa/catalogue/data-access';
|
||||
import { ProductInfoComponent } from '@isa/remission/shared/product';
|
||||
|
||||
export type SelectRemiQuantityAndReasonDialogData = {
|
||||
item: Item;
|
||||
inStock: number;
|
||||
};
|
||||
|
||||
export type SelectRemiQuantityAndReasonDialogResult =
|
||||
| undefined
|
||||
| Array<ReturnItem>;
|
||||
|
||||
export interface QuantityAndReason {
|
||||
quantity: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'remi-select-remi-quantity-and-reason-dialog',
|
||||
templateUrl: './select-remi-quantity-and-reason-dialog.component.html',
|
||||
styleUrls: ['./select-remi-quantity-and-reason-dialog.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
QuantityAndReasonItemComponent,
|
||||
TextButtonComponent,
|
||||
NgIcon,
|
||||
ButtonComponent,
|
||||
IconButtonComponent,
|
||||
ProductInfoComponent,
|
||||
],
|
||||
providers: [provideIcons({ isaActionPlus, isaActionClose })],
|
||||
})
|
||||
export class SelectRemiQuantityAndReasonDialogComponent extends DialogContentDirective<
|
||||
SelectRemiQuantityAndReasonDialogData,
|
||||
SelectRemiQuantityAndReasonDialogResult
|
||||
> {
|
||||
#remiService = inject(RemissionSearchService);
|
||||
#remiStore = inject(RemissionStore);
|
||||
#feedbackDialog = injectFeedbackDialog();
|
||||
|
||||
initialItem: QuantityAndReason = { quantity: 0, reason: '' };
|
||||
|
||||
quantitiesAndResons = model<QuantityAndReason[]>([this.initialItem]);
|
||||
|
||||
addQuantityReasonItem(): void {
|
||||
this.quantitiesAndResons.update((items) => [...items, this.initialItem]);
|
||||
}
|
||||
|
||||
removeQuantityReasonItem(position: number): void {
|
||||
const currentItems = this.quantitiesAndResons();
|
||||
if (currentItems.length > 1) {
|
||||
this.quantitiesAndResons.update((items) =>
|
||||
items.filter((_, index) => index !== position),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setQuantityAndReason(position: number, qar: QuantityAndReason): void {
|
||||
this.quantitiesAndResons.update((items) => {
|
||||
const newItems = [...items];
|
||||
newItems[position] = qar;
|
||||
return newItems;
|
||||
});
|
||||
}
|
||||
|
||||
params = computed(() => {
|
||||
const items = this.quantitiesAndResons();
|
||||
const item = this.data.item;
|
||||
|
||||
if (!item) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return items.map((qar) => ({
|
||||
item,
|
||||
quantity: qar.quantity,
|
||||
reason: qar.reason,
|
||||
}));
|
||||
});
|
||||
|
||||
canAddToRemiListResource = resource({
|
||||
params: this.params,
|
||||
loader: async ({ params, abortSignal }) => {
|
||||
if (
|
||||
!this.data.item ||
|
||||
params.some((p) => !p.reason) ||
|
||||
params.some((p) => !p.quantity)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const maxQuantityErrors = params.filter((p) => !(p.quantity <= 999));
|
||||
if (maxQuantityErrors.length > 0) {
|
||||
const errRes: BatchResponseArgs<ReturnItem> = {
|
||||
completed: false,
|
||||
error: true,
|
||||
total: maxQuantityErrors.length,
|
||||
invalidProperties: {
|
||||
quantity: 'Die Menge darf maximal 999 sein.',
|
||||
},
|
||||
};
|
||||
return errRes;
|
||||
}
|
||||
|
||||
return this.#remiService.canAddItemToRemiList(params, abortSignal);
|
||||
},
|
||||
});
|
||||
|
||||
canReturn = computed(() => {
|
||||
const results = this.canAddToRemiListResource.value();
|
||||
|
||||
if (!results) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (results.failed && results.failed.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
results.successful &&
|
||||
results.successful.length === this.quantitiesAndResons().length
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
canReturnErrors = computed(() => {
|
||||
const results = this.canAddToRemiListResource.value();
|
||||
if (results?.invalidProperties) {
|
||||
return Object.values(results.invalidProperties);
|
||||
}
|
||||
|
||||
if (!results?.failed) {
|
||||
return [];
|
||||
}
|
||||
return results.failed.map((item) =>
|
||||
item.invalidProperties
|
||||
? Object.values(item.invalidProperties).join(', ')
|
||||
: [],
|
||||
) as string[];
|
||||
});
|
||||
|
||||
async addToRemiList() {
|
||||
const canAddValue = this.canAddToRemiListResource.value();
|
||||
|
||||
if (!canAddValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (canAddValue.failed?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// #5273, #4768 Fix - Items dürfen nur zur Pflichtremission hinzugefügt werden
|
||||
const result: Array<ReturnItem> = await this.#remiService.addToList(
|
||||
this.params(),
|
||||
);
|
||||
|
||||
this.#feedbackDialog({
|
||||
data: {
|
||||
message: this.#remiStore.remissionStarted()
|
||||
? 'Wurde zum Warenbegleitschein hinzugefügt'
|
||||
: 'Wurde zur Remi Liste hinzugefügt',
|
||||
},
|
||||
});
|
||||
|
||||
this.close(result);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
@apply grid grid-flow-row gap-6;
|
||||
}
|
||||
@@ -42,5 +42,6 @@
|
||||
[filterInput]="input"
|
||||
(applied)="applied.emit()"
|
||||
(reseted)="reseted.emit()"
|
||||
[canApply]="canApply()"
|
||||
></filter-input-menu>
|
||||
</ng-template>
|
||||
|
||||
@@ -68,6 +68,13 @@ export class FilterInputMenuButtonComponent {
|
||||
*/
|
||||
reseted = output<void>();
|
||||
|
||||
/**
|
||||
* Indicates whether the filter can be applied.
|
||||
* Defaults to false.
|
||||
* @default false
|
||||
*/
|
||||
canApply = input<boolean>(false);
|
||||
|
||||
/**
|
||||
* Emits an event when the input menu is applied.
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
></filter-input-renderer>
|
||||
<filter-actions
|
||||
[inputKey]="filterInput().key"
|
||||
[canApply]="false"
|
||||
[canApply]="canApply()"
|
||||
(applied)="applied.emit()"
|
||||
(reseted)="reseted.emit()"
|
||||
></filter-actions>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
input,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
import { FilterInput } from '../../core';
|
||||
import { FilterActionsComponent } from '../../actions';
|
||||
import { InputRendererComponent } from '../../inputs/input-renderer';
|
||||
@@ -30,4 +35,11 @@ export class FilterInputMenuComponent {
|
||||
* Emits an event when the filter input is applied.
|
||||
*/
|
||||
applied = output<void>();
|
||||
|
||||
/**
|
||||
* Indicates whether the filter can be applied.
|
||||
* Defaults to false.
|
||||
* @default false
|
||||
*/
|
||||
canApply = input<boolean>(false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="w-full flex flex-col gap-4 items-center justify-center">
|
||||
<span
|
||||
class="bg-isa-accent-red rounded-[6.25rem] flex flex-row items-center justify-center p-3"
|
||||
>
|
||||
<ng-icon
|
||||
class="text-isa-white"
|
||||
size="1.5rem"
|
||||
name="isaActionClose"
|
||||
></ng-icon>
|
||||
</span>
|
||||
<p
|
||||
class="isa-text-body-1-bold text-isa-neutral-900"
|
||||
data-what="error-message"
|
||||
>
|
||||
{{ data.errorMessage }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,56 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import {
|
||||
FeedbackErrorDialogComponent,
|
||||
FeedbackErrorDialogData,
|
||||
} from './feedback-error-dialog.component';
|
||||
import { DialogRef, DIALOG_DATA } from '@angular/cdk/dialog';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import { DialogComponent } from '../dialog.component';
|
||||
|
||||
// Test suite for FeedbackErrorDialogComponent
|
||||
describe('FeedbackErrorDialogComponent', () => {
|
||||
let spectator: Spectator<FeedbackErrorDialogComponent>;
|
||||
const mockData: FeedbackErrorDialogData = {
|
||||
errorMessage: 'Something went wrong',
|
||||
};
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: FeedbackErrorDialogComponent,
|
||||
imports: [NgIcon],
|
||||
providers: [
|
||||
{
|
||||
provide: DialogRef,
|
||||
useValue: { close: jest.fn() },
|
||||
},
|
||||
{
|
||||
provide: DIALOG_DATA,
|
||||
useValue: mockData,
|
||||
},
|
||||
{
|
||||
provide: DialogComponent,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display the error message passed in data', () => {
|
||||
const messageElement = spectator.query('[data-what="error-message"]');
|
||||
expect(messageElement).toHaveText('Something went wrong');
|
||||
});
|
||||
|
||||
it('should render the close icon', () => {
|
||||
// The icon should be present with isaActionClose
|
||||
const iconElement = spectator.query('ng-icon');
|
||||
expect(iconElement).toBeTruthy();
|
||||
expect(iconElement).toHaveAttribute('name', 'isaActionClose');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { DialogContentDirective } from '../dialog-content.directive';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaActionClose } from '@isa/icons';
|
||||
|
||||
/**
|
||||
* Input data for the error message dialog
|
||||
*/
|
||||
export interface FeedbackErrorDialogData {
|
||||
/** The Error message text to display in the dialog */
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple feedback dialog component that displays an error message and an error icon.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ui-feedback-error-dialog',
|
||||
templateUrl: './feedback-error-dialog.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgIcon],
|
||||
providers: [provideIcons({ isaActionClose })],
|
||||
host: {
|
||||
'[class]': '["ui-feedback-error-dialog"]',
|
||||
},
|
||||
})
|
||||
export class FeedbackErrorDialogComponent extends DialogContentDirective<
|
||||
FeedbackErrorDialogData,
|
||||
void
|
||||
> {}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
injectTextInputDialog,
|
||||
injectNumberInputDialog,
|
||||
injectConfirmationDialog,
|
||||
injectFeedbackErrorDialog,
|
||||
} from './injects';
|
||||
import { MessageDialogComponent } from './message-dialog/message-dialog.component';
|
||||
import { DialogComponent } from './dialog.component';
|
||||
@@ -17,6 +18,7 @@ import { TextInputDialogComponent } from './text-input-dialog/text-input-dialog.
|
||||
import { FeedbackDialogComponent } from './feedback-dialog/feedback-dialog.component';
|
||||
import { NumberInputDialogComponent } from './number-input-dialog/number-input-dialog.component';
|
||||
import { ConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component';
|
||||
import { FeedbackErrorDialogComponent } from './feedback-error-dialog/feedback-error-dialog.component';
|
||||
|
||||
// Test component extending DialogContentDirective for testing
|
||||
@Component({ template: '' })
|
||||
@@ -290,4 +292,23 @@ describe('Dialog Injects', () => {
|
||||
expect(injector.get(DIALOG_CONTENT)).toBe(ConfirmationDialogComponent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectFeedbackErrorDialog', () => {
|
||||
it('should create a dialog injector for FeedbackErrorDialogComponent', () => {
|
||||
// Act
|
||||
const openFeedbackErrorDialog = TestBed.runInInjectionContext(() =>
|
||||
injectFeedbackErrorDialog(),
|
||||
);
|
||||
openFeedbackErrorDialog({
|
||||
data: {
|
||||
errorMessage: 'Test error message',
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
const callOptions = mockDialogOpen.mock.calls[0][1];
|
||||
const injector = callOptions.injector;
|
||||
expect(injector.get(DIALOG_CONTENT)).toBe(FeedbackErrorDialogComponent);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,10 @@ import {
|
||||
ConfirmationDialogComponent,
|
||||
ConfirmationDialogData,
|
||||
} from './confirmation-dialog/confirmation-dialog.component';
|
||||
import {
|
||||
FeedbackErrorDialogComponent,
|
||||
FeedbackErrorDialogData,
|
||||
} from './feedback-error-dialog/feedback-error-dialog.component';
|
||||
|
||||
export interface InjectDialogOptions {
|
||||
/** Optional title override for the dialog */
|
||||
@@ -173,3 +177,17 @@ export const injectFeedbackDialog = (
|
||||
classList: ['gap-0'],
|
||||
...options,
|
||||
});
|
||||
|
||||
/**
|
||||
* Convenience function that returns a pre-configured FeedbackErrorDialog injector
|
||||
* @returns A function to open a feedback error dialog
|
||||
*/
|
||||
export const injectFeedbackErrorDialog = (
|
||||
options?: OpenDialogOptions<FeedbackErrorDialogData>,
|
||||
) =>
|
||||
injectDialog(FeedbackErrorDialogComponent, {
|
||||
disableClose: false,
|
||||
minWidth: '20rem',
|
||||
classList: ['gap-0'],
|
||||
...options,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './lib/inject-restore-scroll-position';
|
||||
export * from './lib/provide-scroll-position-restoration';
|
||||
export * from './lib/store-scroll-position';
|
||||
export * from './lib/scroll-top-button.component';
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture } from '@angular/core/testing';
|
||||
import { ScrollTopButtonComponent } from './scroll-top-button.component';
|
||||
|
||||
describe('ScrollTopButtonComponent (happy path)', () => {
|
||||
let fixture: ComponentFixture<ScrollTopButtonComponent>;
|
||||
let component: ScrollTopButtonComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ScrollTopButtonComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ScrollTopButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
// Polyfill / Reset matchMedia für jedes Test-Setup
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query: string) => ({
|
||||
matches: false, // Default: keine reduzierte Animation
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated, aber Angular / libs könnten darauf zugreifen
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call scrollTo with smooth when prefers-reduced-motion is false', () => {
|
||||
// Arrange
|
||||
const targetEl = document.createElement('div');
|
||||
(targetEl as any).scrollTo = jest.fn();
|
||||
fixture.componentRef.setInput('target', targetEl);
|
||||
// matchMedia default (set in beforeEach) returns matches: false
|
||||
|
||||
// Act
|
||||
component.scrollTop();
|
||||
|
||||
// Assert
|
||||
expect((targetEl as any).scrollTo).toHaveBeenCalledWith({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
});
|
||||
|
||||
it('should call scrollTo with auto when prefers-reduced-motion is true', () => {
|
||||
// Arrange
|
||||
(window.matchMedia as jest.Mock).mockImplementationOnce(
|
||||
(query: string) => ({
|
||||
matches: true, // reduzierte Bewegungen bevorzugt
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
const targetEl = document.createElement('div');
|
||||
(targetEl as any).scrollTo = jest.fn();
|
||||
fixture.componentRef.setInput('target', targetEl);
|
||||
|
||||
// Act
|
||||
component.scrollTop();
|
||||
|
||||
// Assert
|
||||
expect((targetEl as any).scrollTo).toHaveBeenCalledWith({
|
||||
top: 0,
|
||||
behavior: 'auto',
|
||||
});
|
||||
});
|
||||
|
||||
it('should render button when target element scrolled down', () => {
|
||||
// Arrange
|
||||
jest.useFakeTimers();
|
||||
const targetEl = document.createElement('div');
|
||||
(targetEl as any).scrollTo = jest.fn();
|
||||
targetEl.scrollTop = 150; // > 0 so truthy
|
||||
fixture.componentRef.setInput('target', targetEl);
|
||||
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
targetEl.dispatchEvent(new Event('scroll'));
|
||||
jest.advanceTimersByTime(20); // allow debounceTime(10) to elapse
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const button = fixture.nativeElement.querySelector(
|
||||
'[data-what="scroll-top-button"]',
|
||||
);
|
||||
expect(button).not.toBeNull();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not render button when target element at top (scrollTop = 0)', () => {
|
||||
// Arrange
|
||||
jest.useFakeTimers();
|
||||
const targetEl = document.createElement('div');
|
||||
(targetEl as any).scrollTo = jest.fn();
|
||||
targetEl.scrollTop = 0; // top position
|
||||
fixture.componentRef.setInput('target', targetEl);
|
||||
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
targetEl.dispatchEvent(new Event('scroll'));
|
||||
jest.advanceTimersByTime(20);
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const button = fixture.nativeElement.querySelector(
|
||||
'[data-what="scroll-top-button"]',
|
||||
);
|
||||
expect(button).toBeNull();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaSortByUpMedium } from '@isa/icons';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { debounceTime, fromEvent, switchMap } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'utils-scroll-top-button',
|
||||
imports: [IconButtonComponent],
|
||||
providers: [provideIcons({ isaSortByUpMedium })],
|
||||
template: `
|
||||
@if (display()) {
|
||||
<button
|
||||
uiIconButton
|
||||
aria-label="Scroll to top"
|
||||
type="button"
|
||||
color="tertiary"
|
||||
size="large"
|
||||
data-what="scroll-top-button"
|
||||
name="isaSortByUpMedium"
|
||||
(click)="scrollTop()"
|
||||
></button>
|
||||
}
|
||||
`,
|
||||
host: {
|
||||
'[class]': '["utils-scroll-top-button"]',
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ScrollTopButtonComponent {
|
||||
/** The scroll target, either `window` or a specific element. */
|
||||
target = input<Window | HTMLElement>(window);
|
||||
|
||||
/** Whether the target is an `HTMLElement`. */
|
||||
isTargetElement = computed(() => this.target() instanceof HTMLElement);
|
||||
|
||||
/** The scroll event signal. */
|
||||
scrollEvent = toSignal(
|
||||
toObservable(this.target).pipe(
|
||||
switchMap((target) => fromEvent(target, 'scroll').pipe(debounceTime(16))),
|
||||
),
|
||||
);
|
||||
|
||||
/** Whether to display the button. */
|
||||
display = computed(() => {
|
||||
this.scrollEvent();
|
||||
const target = this.target();
|
||||
|
||||
if (target instanceof HTMLElement) {
|
||||
return target.scrollTop;
|
||||
}
|
||||
|
||||
return target.scrollY;
|
||||
});
|
||||
|
||||
/** Scrolls to the top of the page. */
|
||||
scrollTop() {
|
||||
const prefersReducedMotion = window.matchMedia(
|
||||
'(prefers-reduced-motion: reduce)',
|
||||
).matches; // Anforderung im Ticket
|
||||
|
||||
this.target().scrollTo({
|
||||
top: 0,
|
||||
behavior: prefersReducedMotion ? 'auto' : 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
57
package-lock.json
generated
57
package-lock.json
generated
@@ -1095,6 +1095,18 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@angular/build/node_modules/@types/node": {
|
||||
"version": "24.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz",
|
||||
"integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/build/node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -1324,7 +1336,6 @@
|
||||
"version": "20.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.1.2.tgz",
|
||||
"integrity": "sha512-NMSDavN+CJYvSze6wq7DpbrUA/EqiAD7GQoeJtuOknzUpPlWQmFOoHzTMKW+S34XlNEw+YQT0trv3DKcrE+T/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "7.28.0",
|
||||
@@ -11920,6 +11931,17 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/retry": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
|
||||
@@ -14657,7 +14679,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
@@ -15132,7 +15153,6 @@
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
|
||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
@@ -16350,7 +16370,7 @@
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
@@ -19062,7 +19082,7 @@
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
@@ -27562,6 +27582,17 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
||||
"integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -27601,7 +27632,6 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
@@ -27656,7 +27686,6 @@
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/regenerate": {
|
||||
@@ -28638,7 +28667,7 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
@@ -29179,7 +29208,6 @@
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -31717,7 +31745,7 @@
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -31777,6 +31805,15 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
|
||||
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
|
||||
|
||||
Reference in New Issue
Block a user