mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
57 Commits
feature/52
...
hotfix/dea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
353864e2f0 | ||
|
|
1b6b726036 | ||
|
|
4c56f394c5 | ||
|
|
a086111ab5 | ||
|
|
15a4718e58 | ||
|
|
40592b4477 | ||
|
|
d430f544f0 | ||
|
|
62e586cfda | ||
|
|
304f8a64e5 | ||
|
|
c672ae4012 | ||
|
|
fd693a4beb | ||
|
|
2c70339f23 | ||
|
|
59f0cc7d43 | ||
|
|
0ca58fe1bf | ||
|
|
8cf80a60a0 | ||
|
|
cffa7721bc | ||
|
|
066ab5d5be | ||
|
|
3bbf79a3c3 | ||
|
|
357485e32f | ||
|
|
39984342a6 | ||
|
|
c52f18e979 | ||
|
|
e58ec93087 | ||
|
|
4e6204817d | ||
|
|
c41355bcdf | ||
|
|
fa8e601660 | ||
|
|
708ec01704 | ||
|
|
332699ca74 | ||
|
|
3b0a63a53a | ||
|
|
327fdc745d | ||
|
|
297ec9100d | ||
|
|
298ab1acbe | ||
|
|
fe77a0ea8b | ||
|
|
48f588f53b | ||
|
|
7f4af304ac | ||
|
|
643b2b0e60 | ||
|
|
cd1ff5f277 | ||
|
|
46c70cae3e | ||
|
|
2cb1f9ec99 | ||
|
|
d2dcf638e3 | ||
|
|
a4241cbd7a | ||
|
|
dd3705f8bc | ||
|
|
514715589b | ||
|
|
0740273dbc | ||
|
|
bbb9c5d39c | ||
|
|
f0bd957a07 | ||
|
|
e4f289c67d | ||
|
|
2af16d92ea | ||
|
|
99e8e7cfe0 | ||
|
|
ac728f2dd9 | ||
|
|
2e012a124a | ||
|
|
d22e320294 | ||
|
|
a0f24aac17 | ||
|
|
7ae484fc83 | ||
|
|
0dcb31973f | ||
|
|
c2f393d249 | ||
|
|
2dbf7dda37 | ||
|
|
0addf392b6 |
@@ -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,18 +1,18 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Logger, LogLevel } from '@core/logger';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { debounceTime, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { RootState } from './root.state';
|
||||
import packageInfo from 'packageJson';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { Subject } from 'rxjs';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { injectStorage, UserStorageProvider } from '@isa/core/storage';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Logger, LogLevel } from "@core/logger";
|
||||
import { Store } from "@ngrx/store";
|
||||
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
|
||||
import { RootState } from "./root.state";
|
||||
import packageInfo from "packageJson";
|
||||
import { environment } from "../../environments/environment";
|
||||
import { Subject } from "rxjs";
|
||||
import { AuthService } from "@core/auth";
|
||||
import { injectStorage, UserStorageProvider } from "@isa/core/storage";
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class RootStateService {
|
||||
static LOCAL_STORAGE_KEY = 'ISA_APP_INITIALSTATE';
|
||||
static LOCAL_STORAGE_KEY = "ISA_APP_INITIALSTATE";
|
||||
|
||||
#storage = injectStorage(UserStorageProvider);
|
||||
|
||||
@@ -29,14 +29,17 @@ export class RootStateService {
|
||||
);
|
||||
}
|
||||
|
||||
window['clearUserState'] = () => {
|
||||
window["clearUserState"] = () => {
|
||||
this.clear();
|
||||
};
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.load();
|
||||
this._store.dispatch({ type: 'HYDRATE', payload: RootStateService.LoadFromLocalStorage() });
|
||||
this._store.dispatch({
|
||||
type: "HYDRATE",
|
||||
payload: RootStateService.LoadFromLocalStorage(),
|
||||
});
|
||||
this.initSave();
|
||||
}
|
||||
|
||||
@@ -50,14 +53,10 @@ export class RootStateService {
|
||||
const data = {
|
||||
...state,
|
||||
version: packageInfo.version,
|
||||
sub: this._authService.getClaimByKey('sub'),
|
||||
sub: this._authService.getClaimByKey("sub"),
|
||||
};
|
||||
RootStateService.SaveToLocalStorageRaw(JSON.stringify(data));
|
||||
return this.#storage.set('state', {
|
||||
...state,
|
||||
version: packageInfo.version,
|
||||
sub: this._authService.getClaimByKey('sub'),
|
||||
});
|
||||
return this.#storage.set("state", data);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
@@ -68,7 +67,7 @@ export class RootStateService {
|
||||
*/
|
||||
async load(): Promise<boolean> {
|
||||
try {
|
||||
const res = await this.#storage.get('state');
|
||||
const res = await this.#storage.get("state");
|
||||
|
||||
const storageContent = RootStateService.LoadFromLocalStorageRaw();
|
||||
|
||||
@@ -88,7 +87,7 @@ export class RootStateService {
|
||||
async clear() {
|
||||
try {
|
||||
this._cancelSave.next();
|
||||
await this.#storage.clear('state');
|
||||
await this.#storage.clear("state");
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
RootStateService.RemoveFromLocalStorage();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
@@ -112,7 +111,7 @@ export class RootStateService {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (error) {
|
||||
console.error('Error parsing local storage:', error);
|
||||
console.error("Error parsing local storage:", error);
|
||||
this.RemoveFromLocalStorage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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({
|
||||
|
||||
@@ -16,20 +16,20 @@ import {
|
||||
forwardRef,
|
||||
Optional,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { UiAutocompleteComponent } from '@ui/autocomplete';
|
||||
import { UiFormControlDirective } from '@ui/form-control';
|
||||
import { containsElement } from '@utils/common';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ScanAdapterService } from '@adapter/scan';
|
||||
import { injectCancelSearch } from '@shared/services/cancel-subject';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
} from "@angular/core";
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||
import { UiAutocompleteComponent } from "@ui/autocomplete";
|
||||
import { UiFormControlDirective } from "@ui/form-control";
|
||||
import { containsElement } from "@utils/common";
|
||||
import { Subscription } from "rxjs";
|
||||
import { ScanAdapterService } from "@adapter/scan";
|
||||
import { injectCancelSearch } from "@shared/services/cancel-subject";
|
||||
import { EnvironmentService } from "@core/environment";
|
||||
|
||||
@Component({
|
||||
selector: 'shared-searchbox',
|
||||
templateUrl: 'searchbox.component.html',
|
||||
styleUrls: ['searchbox.component.scss'],
|
||||
selector: "shared-searchbox",
|
||||
templateUrl: "searchbox.component.html",
|
||||
styleUrls: ["searchbox.component.scss"],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
@@ -49,9 +49,9 @@ export class SearchboxComponent
|
||||
cancelSearch = injectCancelSearch({ optional: true });
|
||||
|
||||
disabled: boolean;
|
||||
type = 'text';
|
||||
type = "text";
|
||||
|
||||
@ViewChild('input', { read: ElementRef, static: true })
|
||||
@ViewChild("input", { read: ElementRef, static: true })
|
||||
input: ElementRef;
|
||||
|
||||
@ContentChild(UiAutocompleteComponent)
|
||||
@@ -61,9 +61,9 @@ export class SearchboxComponent
|
||||
focusAfterViewInit = true;
|
||||
|
||||
@Input()
|
||||
placeholder = '';
|
||||
placeholder = "";
|
||||
|
||||
private _query = '';
|
||||
private _query = "";
|
||||
|
||||
@Input()
|
||||
get query() {
|
||||
@@ -94,7 +94,7 @@ export class SearchboxComponent
|
||||
scanner = false;
|
||||
|
||||
@Input()
|
||||
hint = '';
|
||||
hint = "";
|
||||
|
||||
@Input()
|
||||
autocompleteValueSelector: (item: any) => string = (item: any) => item;
|
||||
@@ -104,11 +104,11 @@ export class SearchboxComponent
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.setQuery('');
|
||||
this.setQuery("");
|
||||
this.cancelSearch();
|
||||
}
|
||||
|
||||
@HostBinding('class.autocomplete-opend')
|
||||
@HostBinding("class.autocomplete-opend")
|
||||
get autocompleteOpen() {
|
||||
return this.autocomplete?.opend;
|
||||
}
|
||||
@@ -213,13 +213,13 @@ export class SearchboxComponent
|
||||
}
|
||||
|
||||
clearHint() {
|
||||
this.hint = '';
|
||||
this.hint = "";
|
||||
this.focused.emit(true);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
onKeyup(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === "Enter") {
|
||||
if (this.autocomplete?.opend && this.autocomplete?.activeItem) {
|
||||
this.setQuery(this.autocomplete?.activeItem?.item);
|
||||
this.autocomplete?.close();
|
||||
@@ -227,7 +227,7 @@ export class SearchboxComponent
|
||||
this.search.emit(this.query);
|
||||
|
||||
event.preventDefault();
|
||||
} else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
||||
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
this.handleArrowUpDownEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -242,7 +242,7 @@ export class SearchboxComponent
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:click', ['$event'])
|
||||
@HostListener("window:click", ["$event"])
|
||||
focusLost(event: MouseEvent) {
|
||||
if (
|
||||
this.autocomplete?.opend &&
|
||||
@@ -256,9 +256,11 @@ export class SearchboxComponent
|
||||
this.search.emit(this.query);
|
||||
}
|
||||
|
||||
@HostListener('focusout', ['$event'])
|
||||
@HostListener("focusout", ["$event"])
|
||||
onBlur() {
|
||||
this.onTouched();
|
||||
if (typeof this.onTouched === "function") {
|
||||
this.onTouched();
|
||||
}
|
||||
this.focused.emit(false);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -323,7 +294,8 @@
|
||||
'remission',
|
||||
]"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
routerLinkActive="active"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/\d*\/remission\/(mandatory|department)"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"> </span>
|
||||
<span class="side-menu-group-item-label">Remission</span>
|
||||
@@ -338,7 +310,8 @@
|
||||
'return-receipt',
|
||||
]"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
routerLinkActive="active"
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/\d*\/remission\/return-receipt"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"> </span>
|
||||
<span class="side-menu-group-item-label">Warenbegleitscheine</span>
|
||||
@@ -346,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'),
|
||||
{
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
@import "../../../libs/ui/search-bar/src/search-bar.scss";
|
||||
@import "../../../libs/ui/skeleton-loader/src/skeleton-loader.scss";
|
||||
@import "../../../libs/ui/tooltip/src/tooltip.scss";
|
||||
@import "../../../libs/ui/label/src/label.scss";
|
||||
|
||||
.input-control {
|
||||
@apply rounded border border-solid border-[#AEB7C1] px-4 py-[1.125rem] outline-none;
|
||||
|
||||
@@ -16,20 +16,20 @@ import {
|
||||
forwardRef,
|
||||
Optional,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { UiAutocompleteComponent } from '@ui/autocomplete';
|
||||
import { UiFormControlDirective } from '@ui/form-control';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ScanAdapterService } from '@adapter/scan';
|
||||
import { injectCancelSearch } from '@shared/services/cancel-subject';
|
||||
import { containsElement } from '@utils/common';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
} from "@angular/core";
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||
import { UiAutocompleteComponent } from "@ui/autocomplete";
|
||||
import { UiFormControlDirective } from "@ui/form-control";
|
||||
import { Subscription } from "rxjs";
|
||||
import { ScanAdapterService } from "@adapter/scan";
|
||||
import { injectCancelSearch } from "@shared/services/cancel-subject";
|
||||
import { containsElement } from "@utils/common";
|
||||
import { EnvironmentService } from "@core/environment";
|
||||
|
||||
@Component({
|
||||
selector: 'ui-searchbox',
|
||||
templateUrl: 'searchbox.component.html',
|
||||
styleUrls: ['searchbox.component.scss'],
|
||||
selector: "ui-searchbox",
|
||||
templateUrl: "searchbox.component.html",
|
||||
styleUrls: ["searchbox.component.scss"],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
@@ -49,9 +49,9 @@ export class UiSearchboxNextComponent
|
||||
private readonly _cancelSearch = injectCancelSearch({ optional: true });
|
||||
|
||||
disabled: boolean;
|
||||
type = 'text';
|
||||
type = "text";
|
||||
|
||||
@ViewChild('input', { read: ElementRef, static: true })
|
||||
@ViewChild("input", { read: ElementRef, static: true })
|
||||
input: ElementRef;
|
||||
|
||||
@ContentChild(UiAutocompleteComponent)
|
||||
@@ -61,9 +61,9 @@ export class UiSearchboxNextComponent
|
||||
focusAfterViewInit: boolean = true;
|
||||
|
||||
@Input()
|
||||
placeholder: string = '';
|
||||
placeholder: string = "";
|
||||
|
||||
private _query = '';
|
||||
private _query = "";
|
||||
|
||||
@Input()
|
||||
get query() {
|
||||
@@ -94,7 +94,7 @@ export class UiSearchboxNextComponent
|
||||
scanner = false;
|
||||
|
||||
@Input()
|
||||
hint: string = '';
|
||||
hint: string = "";
|
||||
|
||||
@Output()
|
||||
hintCleared = new EventEmitter<void>();
|
||||
@@ -107,11 +107,11 @@ export class UiSearchboxNextComponent
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.setQuery('');
|
||||
this.setQuery("");
|
||||
this._cancelSearch();
|
||||
}
|
||||
|
||||
@HostBinding('class.autocomplete-opend')
|
||||
@HostBinding("class.autocomplete-opend")
|
||||
get autocompleteOpen() {
|
||||
return this.autocomplete?.opend;
|
||||
}
|
||||
@@ -212,14 +212,14 @@ export class UiSearchboxNextComponent
|
||||
}
|
||||
|
||||
clearHint() {
|
||||
this.hint = '';
|
||||
this.hint = "";
|
||||
this.focused.emit(true);
|
||||
this.hintCleared.emit();
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
onKeyup(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === "Enter") {
|
||||
if (this.autocomplete?.opend && this.autocomplete?.activeItem) {
|
||||
this.setQuery(this.autocomplete?.activeItem?.item);
|
||||
this.autocomplete?.close();
|
||||
@@ -227,7 +227,7 @@ export class UiSearchboxNextComponent
|
||||
this.search.emit(this.query);
|
||||
|
||||
event.preventDefault();
|
||||
} else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
||||
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
this.handleArrowUpDownEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -235,12 +235,14 @@ export class UiSearchboxNextComponent
|
||||
handleArrowUpDownEvent(event: KeyboardEvent) {
|
||||
this.autocomplete?.handleKeyboardEvent(event);
|
||||
if (this.autocomplete?.activeItem) {
|
||||
const query = this.autocompleteValueSelector(this.autocomplete.activeItem.item);
|
||||
const query = this.autocompleteValueSelector(
|
||||
this.autocomplete.activeItem.item,
|
||||
);
|
||||
this.setQuery(query, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:click', ['$event'])
|
||||
@HostListener("window:click", ["$event"])
|
||||
focusLost(event: MouseEvent) {
|
||||
if (
|
||||
this.autocomplete?.opend &&
|
||||
@@ -254,9 +256,11 @@ export class UiSearchboxNextComponent
|
||||
this.search.emit(this.query);
|
||||
}
|
||||
|
||||
@HostListener('focusout', ['$event'])
|
||||
@HostListener("focusout", ["$event"])
|
||||
onBlur() {
|
||||
this.onTouched();
|
||||
if (typeof this.onTouched === "function") {
|
||||
this.onTouched();
|
||||
}
|
||||
this.focused.emit(false);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
}
|
||||
|
||||
.ui-tooltip-panel {
|
||||
@apply pointer-events-auto;
|
||||
|
||||
.triangle {
|
||||
width: 30px;
|
||||
polygon {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { provideRouter } from '@angular/router';
|
||||
type ProductInfoInputs = {
|
||||
item: ProductInfoItem;
|
||||
orientation: ProductInfoOrientation;
|
||||
innerGridClass: string;
|
||||
};
|
||||
|
||||
const meta: Meta<ProductInfoInputs> = {
|
||||
@@ -50,8 +51,10 @@ const meta: Meta<ProductInfoInputs> = {
|
||||
value: 19.99,
|
||||
},
|
||||
},
|
||||
tag: 'Prio 2',
|
||||
},
|
||||
orientation: 'horizontal',
|
||||
innerGridClass: 'grid-cols-[minmax(20rem,1fr),auto]',
|
||||
},
|
||||
argTypes: {
|
||||
item: {
|
||||
@@ -68,6 +71,16 @@ const meta: Meta<ProductInfoInputs> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
innerGridClass: {
|
||||
control: 'text',
|
||||
description:
|
||||
'Custom CSS classes for the inner grid layout. (Applies on vertical layout only)',
|
||||
table: {
|
||||
defaultValue: {
|
||||
summary: 'grid-cols-[minmax(20rem,1fr),auto]',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
@@ -95,6 +108,7 @@ export const Default: Story = {
|
||||
value: 29.99,
|
||||
},
|
||||
},
|
||||
tag: 'Prio 2',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
39
apps/isa-app/stories/ui/label/ui-label.stories.ts
Normal file
39
apps/isa-app/stories/ui/label/ui-label.stories.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular';
|
||||
import { Labeltype, LabelPriority, LabelComponent } from '@isa/ui/label';
|
||||
|
||||
type UiLabelInputs = {
|
||||
type: Labeltype;
|
||||
priority: LabelPriority;
|
||||
};
|
||||
|
||||
const meta: Meta<UiLabelInputs> = {
|
||||
component: LabelComponent,
|
||||
title: 'ui/label/Label',
|
||||
argTypes: {
|
||||
type: {
|
||||
control: { type: 'select' },
|
||||
options: Object.values(Labeltype),
|
||||
description: 'Determines the label type',
|
||||
},
|
||||
priority: {
|
||||
control: { type: 'select' },
|
||||
options: Object.values(LabelPriority),
|
||||
description: 'Determines the label priority',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
type: 'tag',
|
||||
priority: 'high',
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<ui-label ${argsToTemplate(args)}>Prio 1</ui-label>`,
|
||||
}),
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<LabelComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,42 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { StorageProvider } from './storage-provider';
|
||||
import { UserStateService } from '@generated/swagger/isa-api';
|
||||
import { firstValueFrom, map, shareReplay } from 'rxjs';
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { StorageProvider } from "./storage-provider";
|
||||
import { UserStateService } from "@generated/swagger/isa-api";
|
||||
import { catchError, firstValueFrom, map, of } from "rxjs";
|
||||
import { isEmpty } from "lodash";
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class UserStorageProvider implements StorageProvider {
|
||||
#userStateService = inject(UserStateService);
|
||||
|
||||
private state$ = this.#userStateService.UserStateGetUserState().pipe(
|
||||
map((res) => {
|
||||
if (res.result?.content) {
|
||||
if (res?.result?.content) {
|
||||
return JSON.parse(res.result.content);
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
shareReplay(1),
|
||||
catchError((err) => {
|
||||
console.warn(
|
||||
"No UserStateGetUserState found, returning empty object:",
|
||||
err,
|
||||
);
|
||||
return of({}); // Return empty state fallback
|
||||
}),
|
||||
// shareReplay(1), #5249, #5270 Würde beim Fehlerfall den fehlerhaften Zustand behalten
|
||||
// Aktuell wird nun jedes mal 2 mal der UserState aufgerufen (GET + POST)
|
||||
// Damit bei der set Funktion immer der aktuelle Zustand verwendet wird
|
||||
);
|
||||
|
||||
async set(key: string, value: unknown): Promise<void> {
|
||||
async set(key: string, value: Record<string, unknown>): Promise<void> {
|
||||
const current = await firstValueFrom(this.state$);
|
||||
firstValueFrom(
|
||||
const content =
|
||||
current && !isEmpty(current)
|
||||
? { ...current, [key]: value }
|
||||
: { [key]: value };
|
||||
|
||||
await firstValueFrom(
|
||||
this.#userStateService.UserStateSetUserState({
|
||||
content: JSON.stringify({ ...current, [key]: value }),
|
||||
content: JSON.stringify(content),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -32,7 +47,6 @@ export class UserStorageProvider implements StorageProvider {
|
||||
}
|
||||
|
||||
async clear(key: string): Promise<void> {
|
||||
|
||||
const current = await firstValueFrom(this.state$);
|
||||
delete current[key];
|
||||
firstValueFrom(this.#userStateService.UserStateResetUserState());
|
||||
|
||||
@@ -51,14 +51,24 @@ export const isTolinoEligibleForReturn = (
|
||||
};
|
||||
}
|
||||
|
||||
// #5286 Anpassung des Tolino-Rückgabeflows (+ siehe Kommentare)
|
||||
const displayDamaged =
|
||||
answers[ReturnProcessQuestionKey.DisplayDamaged] === YesNoAnswer.Yes;
|
||||
const receivedDamaged = itemDamaged === ReturnReasonAnswer.ReceivedDamaged;
|
||||
const receiptOlderThan6Months = date
|
||||
? differenceInCalendarMonths(new Date(), parseISO(date)) >= 6
|
||||
: undefined;
|
||||
const receiptOlderThan24Months = date
|
||||
? differenceInCalendarMonths(new Date(), parseISO(date)) >= 24
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
itemDamaged === ReturnReasonAnswer.ReceivedDamaged &&
|
||||
receiptOlderThan6Months
|
||||
) {
|
||||
const isEligible =
|
||||
receiptOlderThan6Months &&
|
||||
!receiptOlderThan24Months &&
|
||||
receivedDamaged &&
|
||||
!displayDamaged;
|
||||
|
||||
if (!isEligible) {
|
||||
return {
|
||||
state: EligibleForReturnState.NotEligible,
|
||||
reason: 'Keine Retoure möglich',
|
||||
|
||||
@@ -4,16 +4,14 @@ import { firstValueFrom } from 'rxjs';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { ReturnTaskListStore } from '@isa/oms/data-access';
|
||||
import { ReturnReviewComponent } from '../return-review.component';
|
||||
import { ConfirmationDialogComponent, injectDialog } from '@isa/ui/dialog';
|
||||
import { injectConfirmationDialog } from '@isa/ui/dialog';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UncompletedTasksGuard
|
||||
implements CanDeactivate<ReturnReviewComponent>
|
||||
{
|
||||
#returnTaskListStore = inject(ReturnTaskListStore);
|
||||
#confirmationDialog = injectDialog(ConfirmationDialogComponent, {
|
||||
title: 'Aufgaben erledigen',
|
||||
});
|
||||
#confirmationDialog = injectConfirmationDialog();
|
||||
|
||||
processId = injectTabId();
|
||||
|
||||
@@ -45,6 +43,7 @@ export class UncompletedTasksGuard
|
||||
|
||||
async openDialog(): Promise<boolean> {
|
||||
const confirmDialogRef = this.#confirmationDialog({
|
||||
title: 'Aufgaben erledigen',
|
||||
data: {
|
||||
message:
|
||||
'Bitte schließen Sie die Aufgaben ab bevor Sie das die Rückgabe verlassen',
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
name="isaActionEdit"
|
||||
data-what="button"
|
||||
data-which="edit-return-item"
|
||||
(click)="navigateBack()"
|
||||
[disabled]="returnItemsAndPrintReciptPending()"
|
||||
(click)="location.back()"
|
||||
></ui-icon-button>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { createRoutingFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { ReturnSummaryItemComponent } from './return-summary-item.component';
|
||||
import { MockComponents, MockProvider } from 'ng-mocks';
|
||||
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
|
||||
import { createRoutingFactory, Spectator } from "@ngneat/spectator/jest";
|
||||
import { ReturnSummaryItemComponent } from "./return-summary-item.component";
|
||||
import { MockComponents, MockProvider } from "ng-mocks";
|
||||
import { ReturnProductInfoComponent } from "@isa/oms/shared/product-info";
|
||||
import {
|
||||
Product,
|
||||
ReturnProcess,
|
||||
ReturnProcessQuestionKey,
|
||||
ReturnProcessService,
|
||||
} from '@isa/oms/data-access';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { Router } from '@angular/router';
|
||||
} from "@isa/oms/data-access";
|
||||
import { NgIcon } from "@ng-icons/core";
|
||||
import { IconButtonComponent } from "@isa/ui/buttons";
|
||||
import { Location } from "@angular/common";
|
||||
|
||||
/**
|
||||
* Creates a mock ReturnProcess with default values that can be overridden
|
||||
@@ -21,20 +21,20 @@ function createMockReturnProcess(
|
||||
return {
|
||||
id: 1,
|
||||
processId: 1,
|
||||
productCategory: 'Electronics',
|
||||
productCategory: "Electronics",
|
||||
answers: {},
|
||||
receiptId: 123,
|
||||
receiptItem: {
|
||||
id: 321,
|
||||
product: {
|
||||
name: 'Test Product',
|
||||
name: "Test Product",
|
||||
},
|
||||
},
|
||||
...partial,
|
||||
} as ReturnProcess;
|
||||
}
|
||||
|
||||
describe('ReturnSummaryItemComponent', () => {
|
||||
describe("ReturnSummaryItemComponent", () => {
|
||||
let spectator: Spectator<ReturnSummaryItemComponent>;
|
||||
let returnProcessService: jest.Mocked<ReturnProcessService>;
|
||||
|
||||
@@ -48,7 +48,10 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
providers: [
|
||||
MockProvider(ReturnProcessService, {
|
||||
getReturnInfo: jest.fn(),
|
||||
eligibleForReturn: jest.fn().mockReturnValue({ state: 'eligible' }),
|
||||
eligibleForReturn: jest.fn().mockReturnValue({ state: "eligible" }),
|
||||
}),
|
||||
MockProvider(Location, {
|
||||
back: jest.fn(),
|
||||
}),
|
||||
],
|
||||
shallow: true,
|
||||
@@ -64,38 +67,38 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
spectator.detectChanges();
|
||||
});
|
||||
|
||||
describe('Component Creation', () => {
|
||||
it('should create the component', () => {
|
||||
describe("Component Creation", () => {
|
||||
it("should create the component", () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Return Information Display', () => {
|
||||
describe("Return Information Display", () => {
|
||||
const mockReturnInfo = {
|
||||
itemCondition: 'itemCondition',
|
||||
returnDetails: { [ReturnProcessQuestionKey.CaseDamaged]: 'no' },
|
||||
returnReason: 'returnReason',
|
||||
itemCondition: "itemCondition",
|
||||
returnDetails: { [ReturnProcessQuestionKey.CaseDamaged]: "no" },
|
||||
returnReason: "returnReason",
|
||||
otherProduct: {
|
||||
ean: 'ean',
|
||||
ean: "ean",
|
||||
} as Product,
|
||||
comment: 'comment',
|
||||
comment: "comment",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(returnProcessService, 'getReturnInfo')
|
||||
.spyOn(returnProcessService, "getReturnInfo")
|
||||
.mockReturnValue(mockReturnInfo);
|
||||
spectator.setInput('returnProcess', createMockReturnProcess({ id: 2 }));
|
||||
spectator.setInput("returnProcess", createMockReturnProcess({ id: 2 }));
|
||||
spectator.detectChanges();
|
||||
});
|
||||
it('should provide correct return information array', () => {
|
||||
it("should provide correct return information array", () => {
|
||||
// Arrange
|
||||
const expectedInfos = [
|
||||
'itemCondition',
|
||||
'returnReason',
|
||||
'Gehäuse beschädigt: no',
|
||||
'Geliefert wurde: ean',
|
||||
'comment',
|
||||
"itemCondition",
|
||||
"returnReason",
|
||||
"Gehäuse beschädigt: no",
|
||||
"Geliefert wurde: ean",
|
||||
"comment",
|
||||
];
|
||||
|
||||
// Act
|
||||
@@ -105,14 +108,14 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
expect(actualInfos).toEqual(expectedInfos);
|
||||
expect(actualInfos.length).toBe(5);
|
||||
});
|
||||
it('should render return info items with correct content', () => {
|
||||
it("should render return info items with correct content", () => {
|
||||
// Arrange
|
||||
const expectedInfos = [
|
||||
'itemCondition',
|
||||
'returnReason',
|
||||
'Gehäuse beschädigt: no',
|
||||
'Geliefert wurde: ean',
|
||||
'comment',
|
||||
"itemCondition",
|
||||
"returnReason",
|
||||
"Gehäuse beschädigt: no",
|
||||
"Geliefert wurde: ean",
|
||||
"comment",
|
||||
];
|
||||
|
||||
// Act
|
||||
@@ -125,14 +128,14 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
expect(listItems.length).toBe(expectedInfos.length);
|
||||
listItems.forEach((item, index) => {
|
||||
expect(item).toHaveText(expectedInfos[index]);
|
||||
expect(item).toHaveAttribute('data-info-index', index.toString());
|
||||
expect(item).toHaveAttribute("data-info-index", index.toString());
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined return info gracefully', () => {
|
||||
it("should handle undefined return info gracefully", () => {
|
||||
// Arrange
|
||||
returnProcessService.getReturnInfo.mockReturnValue(undefined);
|
||||
spectator.setInput('returnProcess', createMockReturnProcess({ id: 3 }));
|
||||
spectator.setInput("returnProcess", createMockReturnProcess({ id: 3 }));
|
||||
spectator.detectChanges();
|
||||
|
||||
// Act
|
||||
@@ -146,26 +149,26 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
expect(listItems.length).toBe(0);
|
||||
});
|
||||
|
||||
describe('returnDetails mapping', () => {
|
||||
it('should map multiple returnDetails keys to correct info strings', () => {
|
||||
describe("returnDetails mapping", () => {
|
||||
it("should map multiple returnDetails keys to correct info strings", () => {
|
||||
const expected = [
|
||||
'itemCondition',
|
||||
'returnReason',
|
||||
'Gehäuse beschädigt: Ja',
|
||||
'Display beschädigt: Nein',
|
||||
'Geliefert wurde: ean',
|
||||
'comment',
|
||||
"itemCondition",
|
||||
"returnReason",
|
||||
"Gehäuse beschädigt: Ja",
|
||||
"Display beschädigt: Nein",
|
||||
"Geliefert wurde: ean",
|
||||
"comment",
|
||||
];
|
||||
// Arrange
|
||||
const details = {
|
||||
[ReturnProcessQuestionKey.CaseDamaged]: 'Ja',
|
||||
[ReturnProcessQuestionKey.DisplayDamaged]: 'Nein',
|
||||
[ReturnProcessQuestionKey.CaseDamaged]: "Ja",
|
||||
[ReturnProcessQuestionKey.DisplayDamaged]: "Nein",
|
||||
};
|
||||
returnProcessService.getReturnInfo.mockReturnValue({
|
||||
...mockReturnInfo,
|
||||
returnDetails: details,
|
||||
});
|
||||
spectator.setInput('returnProcess', createMockReturnProcess({ id: 4 }));
|
||||
spectator.setInput("returnProcess", createMockReturnProcess({ id: 4 }));
|
||||
spectator.detectChanges();
|
||||
|
||||
// Act
|
||||
@@ -173,31 +176,31 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
expect(infos).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should not include returnDetails if empty', () => {
|
||||
it("should not include returnDetails if empty", () => {
|
||||
// Arrange
|
||||
returnProcessService.getReturnInfo.mockReturnValue({
|
||||
...mockReturnInfo,
|
||||
returnDetails: {},
|
||||
});
|
||||
spectator.setInput('returnProcess', createMockReturnProcess({ id: 5 }));
|
||||
spectator.setInput("returnProcess", createMockReturnProcess({ id: 5 }));
|
||||
spectator.detectChanges();
|
||||
|
||||
// Act
|
||||
const infos = spectator.component.returnInfos();
|
||||
|
||||
// Assert
|
||||
expect(infos.some((info) => info.includes('Gehäuse beschädigt'))).toBe(
|
||||
expect(infos.some((info) => info.includes("Gehäuse beschädigt"))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(infos.some((info) => info.includes('Zubehör fehlt'))).toBe(
|
||||
expect(infos.some((info) => info.includes("Zubehör fehlt"))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should render edit button with correct attributes', () => {
|
||||
describe("Navigation", () => {
|
||||
it("should render edit button with correct attributes", () => {
|
||||
// Assert
|
||||
const editButton = spectator.query(
|
||||
'[data-what="button"][data-which="edit-return-item"]',
|
||||
@@ -205,7 +208,7 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
expect(editButton).toExist();
|
||||
});
|
||||
|
||||
it('should navigate back when edit button is clicked', () => {
|
||||
it("should navigate back when edit button is clicked", () => {
|
||||
// Arrange
|
||||
const editButton = spectator.query(
|
||||
'[data-what="button"][data-which="edit-return-item"]',
|
||||
@@ -217,25 +220,20 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
}
|
||||
|
||||
// Assert
|
||||
expect(spectator.inject(Router).navigate).toHaveBeenCalledWith(
|
||||
['..'],
|
||||
expect.objectContaining({
|
||||
relativeTo: expect.anything(),
|
||||
}),
|
||||
);
|
||||
expect(spectator.inject(Location).back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the product info component', () => {
|
||||
it("should render the product info component", () => {
|
||||
const productInfo = spectator.query(ReturnProductInfoComponent);
|
||||
expect(productInfo).toExist();
|
||||
});
|
||||
|
||||
it('should compute eligibility state as eligible', () => {
|
||||
it("should compute eligibility state as eligible", () => {
|
||||
(returnProcessService.eligibleForReturn as jest.Mock).mockReturnValue({
|
||||
state: 'eligible',
|
||||
state: "eligible",
|
||||
});
|
||||
spectator.detectChanges();
|
||||
expect(spectator.component.eligibleForReturn()?.state).toBe('eligible');
|
||||
expect(spectator.component.eligibleForReturn()?.state).toBe("eligible");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,13 +4,13 @@ import {
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
} from "@angular/core";
|
||||
import { Location } from "@angular/common";
|
||||
import {
|
||||
isaActionChevronRight,
|
||||
isaActionClose,
|
||||
isaActionEdit,
|
||||
} from '@isa/icons';
|
||||
} from "@isa/icons";
|
||||
import {
|
||||
EligibleForReturn,
|
||||
EligibleForReturnState,
|
||||
@@ -18,10 +18,10 @@ import {
|
||||
ReturnProcessService,
|
||||
ProductCategory,
|
||||
returnDetailsMapping,
|
||||
} from '@isa/oms/data-access';
|
||||
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
} from "@isa/oms/data-access";
|
||||
import { ReturnProductInfoComponent } from "@isa/oms/shared/product-info";
|
||||
import { IconButtonComponent } from "@isa/ui/buttons";
|
||||
import { NgIcon, provideIcons } from "@ng-icons/core";
|
||||
|
||||
/**
|
||||
* Displays a single item in the return process summary, showing product details
|
||||
@@ -47,30 +47,34 @@ import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'oms-feature-return-summary-item',
|
||||
templateUrl: './return-summary-item.component.html',
|
||||
styleUrls: ['./return-summary-item.component.scss'],
|
||||
selector: "oms-feature-return-summary-item",
|
||||
templateUrl: "./return-summary-item.component.html",
|
||||
styleUrls: ["./return-summary-item.component.scss"],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ReturnProductInfoComponent, NgIcon, IconButtonComponent],
|
||||
providers: [
|
||||
provideIcons({ isaActionChevronRight, isaActionEdit, isaActionClose }),
|
||||
],
|
||||
host: {
|
||||
'data-what': 'list-item',
|
||||
'data-which': 'return-process-item',
|
||||
'[attr.data-receipt-id]': 'returnProcess()?.receiptId',
|
||||
'[attr.data-return-item-id]': 'returnProcess()?.returnItem?.id',
|
||||
"data-what": "list-item",
|
||||
"data-which": "return-process-item",
|
||||
"[attr.data-receipt-id]": "returnProcess()?.receiptId",
|
||||
"[attr.data-return-item-id]": "returnProcess()?.returnItem?.id",
|
||||
},
|
||||
})
|
||||
export class ReturnSummaryItemComponent {
|
||||
EligibleForReturnState = EligibleForReturnState;
|
||||
#returnProcessService = inject(ReturnProcessService);
|
||||
#router = inject(Router);
|
||||
#activatedRoute = inject(ActivatedRoute);
|
||||
|
||||
/** Angular Location service for navigation */
|
||||
location = inject(Location);
|
||||
|
||||
/** The return process object containing all information about the return */
|
||||
returnProcess = input.required<ReturnProcess>();
|
||||
|
||||
/** The status of the return items and print receipt operation */
|
||||
returnItemsAndPrintReciptPending = input<boolean>(false);
|
||||
|
||||
/**
|
||||
* Computes whether the current return process is eligible for return.
|
||||
*
|
||||
@@ -149,8 +153,4 @@ export class ReturnSummaryItemComponent {
|
||||
// remove duplicates
|
||||
return Array.from(new Set(result));
|
||||
});
|
||||
|
||||
navigateBack() {
|
||||
this.#router.navigate(['..'], { relativeTo: this.#activatedRoute });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
color="tertiary"
|
||||
size="small"
|
||||
class="px-[0.875rem] py-1 min-w-0 bg-white gap-1 absolute top-0 left-0"
|
||||
[disabled]="returnItemsAndPrintReciptStatusPending()"
|
||||
(click)="location.back()"
|
||||
>
|
||||
<ng-icon name="isaActionChevronLeft" size="1.5rem" class="-ml-2"></ng-icon>
|
||||
@@ -28,19 +29,22 @@
|
||||
data-which="return-process-item"
|
||||
[attr.data-item-id]="item.id"
|
||||
[attr.data-item-category]="item.productCategory"
|
||||
[returnItemsAndPrintReciptPending]="
|
||||
returnItemsAndPrintReciptStatusPending()
|
||||
"
|
||||
></oms-feature-return-summary-item>
|
||||
}
|
||||
</div>
|
||||
<div class="mt-6 text-center">
|
||||
@if (returnItemsAndPrintReciptStatus() !== 'success') {
|
||||
@if (returnItemsAndPrintReciptStatus() !== "success") {
|
||||
<button
|
||||
type="button"
|
||||
size="large"
|
||||
uiButton
|
||||
color="brand"
|
||||
(click)="returnItemsAndPrintRecipt()"
|
||||
[pending]="returnItemsAndPrintReciptStatus() === 'pending'"
|
||||
[disabled]="returnItemsAndPrintReciptStatus() === 'pending'"
|
||||
[pending]="returnItemsAndPrintReciptStatusPending()"
|
||||
[disabled]="returnItemsAndPrintReciptStatusPending()"
|
||||
data-what="button"
|
||||
data-which="return-and-print"
|
||||
>
|
||||
|
||||
@@ -78,9 +78,17 @@ export class ReturnSummaryComponent {
|
||||
>(undefined);
|
||||
|
||||
/**
|
||||
* Handles the return and print process for multiple items.
|
||||
* Computed signal to determine if the return items and print receipt operation is pending.
|
||||
*
|
||||
* This method:
|
||||
* This signal checks the current status of the returnItemsAndPrintReciptStatus signal
|
||||
* and returns true if the status is 'pending', otherwise false.
|
||||
*
|
||||
* @returns {boolean} True if the operation is pending, false otherwise
|
||||
*/
|
||||
returnItemsAndPrintReciptStatusPending = computed(() => {
|
||||
return this.returnItemsAndPrintReciptStatus() === 'pending';
|
||||
});
|
||||
/**
|
||||
* 1. Checks if a return process is already in progress
|
||||
* 2. Sets status to pending while processing
|
||||
* 3. Calls the ReturnProcessService to complete the return
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,17 @@ import {
|
||||
import { RemissionListType } from '@isa/remission/data-access';
|
||||
|
||||
describe('calculateStockToRemit', () => {
|
||||
it('should return predefinedReturnQuantity when provided', () => {
|
||||
it('should return predefinedReturnQuantity when provided (even if 0) - #5280 Fix', () => {
|
||||
const result = calculateStockToRemit({
|
||||
availableStock: 10,
|
||||
predefinedReturnQuantity: 0,
|
||||
remainingQuantityInStock: 2,
|
||||
});
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should return predefinedReturnQuantity when provided with positive value', () => {
|
||||
const result = calculateStockToRemit({
|
||||
availableStock: 10,
|
||||
predefinedReturnQuantity: 5,
|
||||
@@ -15,7 +25,7 @@ describe('calculateStockToRemit', () => {
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
|
||||
it('should calculate availableStock minus remainingQuantityInStock when no predefinedReturnQuantity', () => {
|
||||
it('should calculate availableStock minus remainingQuantityInStock when no predefinedReturnQuantity - #5269 Fix', () => {
|
||||
const result = calculateStockToRemit({
|
||||
availableStock: 10,
|
||||
remainingQuantityInStock: 3,
|
||||
@@ -23,6 +33,34 @@ describe('calculateStockToRemit', () => {
|
||||
|
||||
expect(result).toBe(7);
|
||||
});
|
||||
|
||||
it('should return 0 when approximation calculation would be negative - #5269 Fix', () => {
|
||||
const result = calculateStockToRemit({
|
||||
availableStock: 5,
|
||||
remainingQuantityInStock: 8,
|
||||
});
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle undefined remainingQuantityInStock when no predefinedReturnQuantity - #5269 Fix', () => {
|
||||
const result = calculateStockToRemit({
|
||||
availableStock: 10,
|
||||
remainingQuantityInStock: undefined,
|
||||
});
|
||||
|
||||
expect(result).toBe(10);
|
||||
});
|
||||
|
||||
it('should handle null remainingQuantityInStock when no predefinedReturnQuantity - #5269 Fix', () => {
|
||||
const result = calculateStockToRemit({
|
||||
availableStock: 10,
|
||||
// @ts-ignore - Testing runtime behavior with null
|
||||
remainingQuantityInStock: null,
|
||||
});
|
||||
|
||||
expect(result).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStockToRemit', () => {
|
||||
@@ -41,6 +79,35 @@ describe('getStockToRemit', () => {
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
|
||||
it('should handle Pflicht remission list type with zero predefined return quantity - #5280 Fix', () => {
|
||||
const remissionItem = {
|
||||
remainingQuantityInStock: 2,
|
||||
predefinedReturnQuantity: 0,
|
||||
} as any;
|
||||
|
||||
const result = getStockToRemit({
|
||||
remissionItem,
|
||||
remissionListType: RemissionListType.Pflicht,
|
||||
availableStock: 10,
|
||||
});
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle Pflicht remission list type without predefined return quantity - #5269 Fix', () => {
|
||||
const remissionItem = {
|
||||
remainingQuantityInStock: 3,
|
||||
} as any;
|
||||
|
||||
const result = getStockToRemit({
|
||||
remissionItem,
|
||||
remissionListType: RemissionListType.Pflicht,
|
||||
availableStock: 10,
|
||||
});
|
||||
|
||||
expect(result).toBe(7);
|
||||
});
|
||||
|
||||
it('should handle Abteilung remission list type with return suggestion', () => {
|
||||
const remissionItem = {
|
||||
remainingQuantityInStock: 1,
|
||||
@@ -59,4 +126,54 @@ describe('getStockToRemit', () => {
|
||||
|
||||
expect(result).toBe(8);
|
||||
});
|
||||
|
||||
it('should handle Abteilung remission list type with zero return suggestion - #5280 Fix', () => {
|
||||
const remissionItem = {
|
||||
remainingQuantityInStock: 1,
|
||||
returnItem: {
|
||||
data: {
|
||||
predefinedReturnQuantity: 0,
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = getStockToRemit({
|
||||
remissionItem,
|
||||
remissionListType: RemissionListType.Abteilung,
|
||||
availableStock: 10,
|
||||
});
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle Abteilung remission list type without return suggestion - #5269 Fix', () => {
|
||||
const remissionItem = {
|
||||
remainingQuantityInStock: 2,
|
||||
returnItem: {
|
||||
data: {},
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = getStockToRemit({
|
||||
remissionItem,
|
||||
remissionListType: RemissionListType.Abteilung,
|
||||
availableStock: 10,
|
||||
});
|
||||
|
||||
expect(result).toBe(8);
|
||||
});
|
||||
|
||||
it('should handle Abteilung remission list type with missing returnItem - #5269 Fix', () => {
|
||||
const remissionItem = {
|
||||
remainingQuantityInStock: 1,
|
||||
} as any;
|
||||
|
||||
const result = getStockToRemit({
|
||||
remissionItem,
|
||||
remissionListType: RemissionListType.Abteilung,
|
||||
availableStock: 10,
|
||||
});
|
||||
|
||||
expect(result).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,11 +24,11 @@ export const getStockToRemit = ({
|
||||
availableStock: number;
|
||||
}): number => {
|
||||
const remainingQuantityInStock = remissionItem?.remainingQuantityInStock;
|
||||
let predefinedReturnQuantity: number | undefined = 0;
|
||||
let predefinedReturnQuantity: number | undefined = undefined;
|
||||
|
||||
if (remissionListType === RemissionListType.Pflicht) {
|
||||
predefinedReturnQuantity =
|
||||
(remissionItem as ReturnItem)?.predefinedReturnQuantity ?? 0;
|
||||
predefinedReturnQuantity = (remissionItem as ReturnItem)
|
||||
?.predefinedReturnQuantity;
|
||||
}
|
||||
|
||||
if (remissionListType === RemissionListType.Abteilung) {
|
||||
@@ -62,10 +62,12 @@ export const calculateStockToRemit = ({
|
||||
predefinedReturnQuantity?: number;
|
||||
remainingQuantityInStock?: number;
|
||||
}): number => {
|
||||
// #5269 Fix - Mache Näherungskalkulation, wenn kein predefinedReturnQuantity Wert vom Backend kommt
|
||||
if (predefinedReturnQuantity === undefined) {
|
||||
const stockToRemit = availableStock - (remainingQuantityInStock ?? 0);
|
||||
return stockToRemit < 0 ? 0 : stockToRemit;
|
||||
}
|
||||
|
||||
// #5280 Fix - Ansonsten nehme immer den kalkulierten Wert vom Backend her auch wenn dieser 0 ist
|
||||
return predefinedReturnQuantity;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Item } from '@isa/catalogue/data-access';
|
||||
|
||||
/**
|
||||
* Helper function to extract the assortment string from an Item object.
|
||||
* The assortment is constructed by concatenating the value and the last character of the key
|
||||
* for each feature in the item's features array.
|
||||
* @param {Item} item - The item object from which to extract the assortment
|
||||
* @returns {string} The constructed assortment string
|
||||
*/
|
||||
export const getAssortmentFromItem = (item: Item): string => {
|
||||
if (!item.features || item.features.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return item.features.reduce((acc, feature) => {
|
||||
const value = feature.value ?? '';
|
||||
const key = feature.key ?? '';
|
||||
const lastChar = key.slice(-1); // gibt '' zurück, wenn key leer ist
|
||||
return acc + `${value}|${lastChar}`;
|
||||
}, '');
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Return } from '../models';
|
||||
|
||||
/**
|
||||
* Extracts all package numbers from all receipts in a return.
|
||||
* Only includes package numbers from receipts that have loaded data and where the package data exists.
|
||||
*
|
||||
* @param returnData - The return object containing receipts
|
||||
* @returns Comma-separated string of all package numbers from all receipts, or empty string if no packages found
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const packageNumbers = getPackageNumbersFromReturn(returnData);
|
||||
* console.log(`Package numbers: ${packageNumbers}`); // "PKG-001, PKG-002, PKG-003"
|
||||
* ```
|
||||
*/
|
||||
export const getPackageNumbersFromReturn = (returnData: Return): string => {
|
||||
if (!returnData?.receipts || returnData.receipts.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const allPackageNumbers = returnData.receipts.reduce<string[]>(
|
||||
(packageNumbers, receipt) => {
|
||||
const receiptPackages = receipt.data?.packages || [];
|
||||
|
||||
// Extract package numbers from loaded packages, filtering out packages without data or packageNumber
|
||||
const receiptPackageNumbers = receiptPackages
|
||||
.filter((pkg) => pkg.data?.packageNumber)
|
||||
.map((pkg) => pkg.data!.packageNumber!);
|
||||
|
||||
packageNumbers.push(...receiptPackageNumbers);
|
||||
return packageNumbers;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return allPackageNumbers.join(', ');
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Return } from '../models';
|
||||
|
||||
/**
|
||||
* Helper function to calculate the total item quantity from all receipts in a return.
|
||||
* If no receipts are present, returns 0.
|
||||
* @param {Return} returnData - The return object containing receipts
|
||||
* @return {number} Total item quantity from all receipts
|
||||
*/
|
||||
export const getReceiptItemQuantityFromReturn = (
|
||||
returnData: Return,
|
||||
): number => {
|
||||
if (!returnData?.receipts || returnData.receipts.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return returnData.receipts.reduce((totalItems, receipt) => {
|
||||
const items = receipt.data?.items;
|
||||
return totalItems + (items ? items.length : 0);
|
||||
}, 0);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Return } from '../models';
|
||||
import { ReceiptItem } from '../models';
|
||||
|
||||
/**
|
||||
* Extracts all receipt item data from all receipts in a return.
|
||||
* Only includes items from receipts that have loaded data and where the item data exists.
|
||||
*
|
||||
* @param returnData - The return object containing receipts
|
||||
* @returns Array of all receipt item data from all receipts, or empty array if no items found
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const items = getReceiptItemsFromReturn(returnData);
|
||||
* console.log(`Found ${items.length} receipt items across all receipts`);
|
||||
* ```
|
||||
*/
|
||||
export const getReceiptItemsFromReturn = (
|
||||
returnData: Return,
|
||||
): ReceiptItem[] => {
|
||||
if (!returnData?.receipts || returnData.receipts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return returnData.receipts.reduce<ReceiptItem[]>((items, receipt) => {
|
||||
const receiptItems = receipt.data?.items || [];
|
||||
|
||||
// Extract only the actual ReceiptItem data, filtering out items without data
|
||||
const itemData = receiptItems
|
||||
.filter((item) => item.data !== undefined)
|
||||
.map((item) => item.data!);
|
||||
|
||||
items.push(...itemData);
|
||||
return items;
|
||||
}, []);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Return } from '../models';
|
||||
|
||||
/**
|
||||
* Helper function to extract and format receipt numbers from a return object.
|
||||
* Returns "Keine Belege vorhanden" if no receipts, otherwise returns formatted receipt numbers.
|
||||
*
|
||||
* @param {Return} returnData - The return object containing receipts
|
||||
* @returns {string} The formatted receipt numbers or message
|
||||
*/
|
||||
export const getReceiptNumberFromReturn = (returnData: Return): string => {
|
||||
if (!returnData?.receipts || returnData.receipts.length === 0) {
|
||||
return 'Keine Belege vorhanden';
|
||||
}
|
||||
|
||||
const receiptNumbers = returnData.receipts
|
||||
.map((receipt) => receipt.data?.receiptNumber)
|
||||
.filter((receiptNumber) => receiptNumber && receiptNumber.length >= 12)
|
||||
.map((receiptNumber) => receiptNumber!.substring(6, 12));
|
||||
|
||||
return receiptNumbers.length > 0 ? receiptNumbers.join(', ') : '';
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { getReceiptStatusFromReturn } from './get-receipt-status-from-return.helper';
|
||||
import { ReceiptCompleteStatus, Return } from '../models';
|
||||
|
||||
describe('getReceiptStatusFromReturn', () => {
|
||||
it('should return Offen when no receipts exist', () => {
|
||||
// Arrange
|
||||
const returnData: Return = {
|
||||
receipts: [] as any,
|
||||
} as Return;
|
||||
|
||||
// Act
|
||||
const result = getReceiptStatusFromReturn(returnData);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(ReceiptCompleteStatus.Offen);
|
||||
});
|
||||
|
||||
it('should return Offen when receipts array is undefined', () => {
|
||||
// Arrange
|
||||
const returnData: Return = {} as Return;
|
||||
|
||||
// Act
|
||||
const result = getReceiptStatusFromReturn(returnData);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(ReceiptCompleteStatus.Offen);
|
||||
});
|
||||
|
||||
it('should return Abgeschlossen when at least one receipt is completed', () => {
|
||||
// Arrange
|
||||
const returnData: Return = {
|
||||
receipts: [
|
||||
{ data: { completed: 'Offen' } },
|
||||
{ data: { completed: 'Abgeschlossen' } },
|
||||
],
|
||||
} as Return;
|
||||
|
||||
// Act
|
||||
const result = getReceiptStatusFromReturn(returnData);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(ReceiptCompleteStatus.Abgeschlossen);
|
||||
});
|
||||
|
||||
it('should return Abgeschlossen when all receipts are incomplete', () => {
|
||||
// Arrange
|
||||
const returnData: Return = {
|
||||
receipts: [
|
||||
{ data: { completed: 'Abgeschlossen' } },
|
||||
{ data: { completed: 'Abgeschlossen' } },
|
||||
],
|
||||
} as Return;
|
||||
|
||||
// Act
|
||||
const result = getReceiptStatusFromReturn(returnData);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(ReceiptCompleteStatus.Abgeschlossen);
|
||||
});
|
||||
|
||||
it('should return Offen when receipt data is undefined', () => {
|
||||
// Arrange
|
||||
const returnData: Return = {
|
||||
receipts: [{ data: undefined }, {}],
|
||||
} as Return;
|
||||
|
||||
// Act
|
||||
const result = getReceiptStatusFromReturn(returnData);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(ReceiptCompleteStatus.Offen);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
ReceiptCompleteStatus,
|
||||
ReceiptCompleteStatusValue,
|
||||
Return,
|
||||
} from '../models';
|
||||
|
||||
/**
|
||||
* Helper function to determine the receipt status from a return object.
|
||||
* Returns 'Offen' if no receipts or all are incomplete, otherwise returns 'Abgeschlossen'.
|
||||
*
|
||||
* @param {Return} returnData - The return object containing receipts
|
||||
* @returns {ReceiptCompleteStatusValue} The completion status of the return
|
||||
*/
|
||||
export const getReceiptStatusFromReturn = (
|
||||
returnData: Return,
|
||||
): ReceiptCompleteStatusValue => {
|
||||
if (!returnData?.receipts || returnData.receipts.length === 0) {
|
||||
return ReceiptCompleteStatus.Offen;
|
||||
}
|
||||
|
||||
const hasCompletedReceipt = returnData.receipts.some(
|
||||
(receipt) => receipt.data?.completed,
|
||||
);
|
||||
|
||||
return hasCompletedReceipt
|
||||
? ReceiptCompleteStatus.Abgeschlossen
|
||||
: ReceiptCompleteStatus.Offen;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Item } from '@isa/catalogue/data-access';
|
||||
import { Price } from '../models';
|
||||
|
||||
/**
|
||||
* Helper function to extract the retail price from an Item object.
|
||||
* The function first checks for store-specific availabilities and falls back to the catalog availability if none are found.
|
||||
* @param {Item} item - The item object from which to extract the retail price
|
||||
* @returns {Price | undefined} The retail price if available, otherwise undefined
|
||||
*/
|
||||
export const getRetailPriceFromItem = (item: Item): Price | undefined => {
|
||||
let availability = item?.storeAvailabilities?.find((f) => !!f);
|
||||
|
||||
if (!availability) {
|
||||
availability = item?.catalogAvailability;
|
||||
}
|
||||
|
||||
if (!availability.price) {
|
||||
return {
|
||||
value: { value: 0, currency: 'EUR' },
|
||||
};
|
||||
}
|
||||
|
||||
return availability.price as Price;
|
||||
};
|
||||
@@ -2,3 +2,12 @@ export * from './calc-available-stock.helper';
|
||||
export * from './calc-stock-to-remit.helper';
|
||||
export * from './calc-target-stock.helper';
|
||||
export * from './calc-capacity.helper';
|
||||
export * from './get-receipt-status-from-return.helper';
|
||||
export * from './get-receipt-item-quantity-from-return.helper';
|
||||
export * from './get-receipt-number-from-return.helper';
|
||||
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;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Interface representing the data required to create a remission.
|
||||
*/
|
||||
export interface CreateRemission {
|
||||
/**
|
||||
* The unique identifier of the return group.
|
||||
*/
|
||||
returnId: number;
|
||||
|
||||
/**
|
||||
* The unique identifier of the receipt.
|
||||
*/
|
||||
receiptId: number;
|
||||
|
||||
/**
|
||||
* Map of property names to error messages for validation failures
|
||||
* Keys represent property names, values contain validation error messages
|
||||
*/
|
||||
invalidProperties?: Record<string, string>;
|
||||
}
|
||||
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
|
||||
@@ -15,4 +15,10 @@ export * from './supplier';
|
||||
export * from './receipt-return-tuple';
|
||||
export * from './receipt-return-suggestion-tuple';
|
||||
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,8 @@
|
||||
export const ReceiptCompleteStatus = {
|
||||
Offen: 'Offen',
|
||||
Abgeschlossen: 'Abgeschlossen',
|
||||
} as const;
|
||||
|
||||
export type ReceiptCompleteStatusKey = keyof typeof ReceiptCompleteStatus;
|
||||
export type ReceiptCompleteStatusValue =
|
||||
(typeof ReceiptCompleteStatus)[ReceiptCompleteStatusKey];
|
||||
@@ -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];
|
||||
@@ -1,7 +1,6 @@
|
||||
export const RemissionListType = {
|
||||
Pflicht: 'Pflichtremission',
|
||||
Abteilung: 'Abteilungsremission',
|
||||
Koerperlos: 'Körperlose Remi',
|
||||
} as const;
|
||||
|
||||
export type RemissionListTypeKey = keyof typeof RemissionListType;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -6,6 +6,8 @@ export const AddReturnSuggestionItemSchema = z.object({
|
||||
returnSuggestionId: z.number(),
|
||||
quantity: z.number().optional(),
|
||||
inStock: z.number(),
|
||||
impedimentComment: z.string().optional(),
|
||||
remainingQuantity: z.number().optional(),
|
||||
});
|
||||
|
||||
export type AddReturnSuggestionItem = z.infer<
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Zod schema for validating remission return receipt fetch parameters.
|
||||
* Ensures both receiptId and returnId are valid numbers.
|
||||
*
|
||||
* @constant
|
||||
* @type {z.ZodObject}
|
||||
*
|
||||
* @example
|
||||
* const params = FetchRemissionReturnReceiptSchema.parse({
|
||||
* receiptId: '123',
|
||||
* returnId: '456'
|
||||
* });
|
||||
* // Result: { receiptId: 123, returnId: 456 }
|
||||
*/
|
||||
export const FetchRemissionReturnReceiptSchema = z.object({
|
||||
/**
|
||||
* The receipt identifier - coerced to number for flexibility.
|
||||
*/
|
||||
receiptId: z.coerce.number(),
|
||||
|
||||
/**
|
||||
* The return identifier - coerced to number for flexibility.
|
||||
*/
|
||||
returnId: z.coerce.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Type representing the parsed output of FetchRemissionReturnReceiptSchema.
|
||||
* Contains validated and coerced receiptId and returnId as numbers.
|
||||
*
|
||||
* @typedef {Object} FetchRemissionReturnReceipt
|
||||
* @property {number} receiptId - The validated receipt identifier
|
||||
* @property {number} returnId - The validated return identifier
|
||||
*/
|
||||
export type FetchRemissionReturnReceipt = z.infer<
|
||||
typeof FetchRemissionReturnReceiptSchema
|
||||
>;
|
||||
|
||||
/**
|
||||
* Type representing the input parameters for FetchRemissionReturnReceiptSchema.
|
||||
* Accepts string or number values that can be coerced to numbers.
|
||||
*
|
||||
* @typedef {Object} FetchRemissionReturnParams
|
||||
* @property {string | number} receiptId - The receipt identifier (can be string or number)
|
||||
* @property {string | number} returnId - The return identifier (can be string or number)
|
||||
*/
|
||||
export type FetchRemissionReturnParams = z.input<
|
||||
typeof FetchRemissionReturnReceiptSchema
|
||||
>;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const FetchReturnSchema = z.object({
|
||||
returnId: z.coerce.number(),
|
||||
eagerLoading: z.coerce.number().optional(),
|
||||
});
|
||||
|
||||
export type FetchReturn = z.infer<typeof FetchReturnSchema>;
|
||||
|
||||
export type FetchReturnParams = z.input<typeof FetchReturnSchema>;
|
||||
@@ -4,8 +4,9 @@ export * from './assign-package.schema';
|
||||
export * from './create-receipt.schema';
|
||||
export * from './create-return.schema';
|
||||
export * from './fetch-query-settings.schema';
|
||||
export * from './fetch-remission-return-receipt.schema';
|
||||
export * from './fetch-remission-return-receipts.schema';
|
||||
export * from './fetch-stock-in-stock.schema';
|
||||
export * from './query-token.schema';
|
||||
export * from './fetch-required-capacity.schema';
|
||||
export * from './fetch-return.schema';
|
||||
export * from './update-item-impediment.schema';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const UpdateItemImpedimentSchema = z.object({
|
||||
itemId: z.number(),
|
||||
comment: z.string(),
|
||||
});
|
||||
|
||||
export type UpdateItemImpediment = z.infer<typeof UpdateItemImpedimentSchema>;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,10 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { ReturnService } from '@generated/swagger/inventory-api';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import { subDays } from 'date-fns';
|
||||
import {
|
||||
ResponseArgs,
|
||||
ResponseArgsError,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { RemissionStockService } from './remission-stock.service';
|
||||
import { Return } from '../models/return';
|
||||
@@ -14,17 +17,21 @@ import {
|
||||
CreateReceipt,
|
||||
CreateReturn,
|
||||
CreateReturnSchema,
|
||||
FetchRemissionReturnParams,
|
||||
FetchRemissionReturnReceiptSchema,
|
||||
FetchRemissionReturnReceiptsParams,
|
||||
FetchRemissionReturnReceiptsSchema,
|
||||
FetchReturnParams,
|
||||
FetchReturnSchema,
|
||||
UpdateItemImpediment,
|
||||
UpdateItemImpedimentSchema,
|
||||
} from '../schemas';
|
||||
import {
|
||||
CreateRemission,
|
||||
Receipt,
|
||||
ReceiptReturnSuggestionTuple,
|
||||
ReceiptReturnTuple,
|
||||
RemissionListType,
|
||||
RemissionItemType,
|
||||
ReturnItem,
|
||||
ReturnSuggestion,
|
||||
} from '../models';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { RemissionSupplierService } from './remission-supplier.service';
|
||||
@@ -56,18 +63,13 @@ export class RemissionReturnReceiptService {
|
||||
#logger = logger(() => ({ service: 'RemissionReturnReceiptService' }));
|
||||
|
||||
/**
|
||||
* Fetches all completed remission return receipts for the assigned stock.
|
||||
* Returns receipts marked as completed within the last 7 days.
|
||||
* Fetches remission return receipts based on the provided parameters.
|
||||
* Validates parameters using FetchRemissionReturnReceiptsSchema before making the request.
|
||||
*
|
||||
* @async
|
||||
* @param {FetchRemissionReturnReceiptsParams} params - The parameters for fetching the receipts
|
||||
* @param {AbortSignal} [abortSignal] - Optional signal to abort the request
|
||||
* @returns {Promise<Return[]>} Array of completed return objects with receipts
|
||||
* @returns {Promise<Return[]>} An array of remission return receipts
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
*
|
||||
* @example
|
||||
* const controller = new AbortController();
|
||||
* const completedReturns = await service
|
||||
* .fetchCompletedRemissionReturnReceipts(controller.signal);
|
||||
*/
|
||||
async fetchRemissionReturnReceipts(
|
||||
params: FetchRemissionReturnReceiptsParams,
|
||||
@@ -78,22 +80,19 @@ export class RemissionReturnReceiptService {
|
||||
const { start, returncompleted } =
|
||||
FetchRemissionReturnReceiptsSchema.parse(params);
|
||||
|
||||
// Default to 7 days ago if no start date is provided
|
||||
const startDate = start ?? subDays(new Date(), 7);
|
||||
|
||||
const assignedStock =
|
||||
await this.#remissionStockService.fetchAssignedStock(abortSignal);
|
||||
|
||||
this.#logger.info('Fetching completed returns from API', () => ({
|
||||
stockId: assignedStock.id,
|
||||
startDate: startDate.toISOString(),
|
||||
startDate: start?.toISOString(),
|
||||
}));
|
||||
|
||||
let req$ = this.#returnService.ReturnQueryReturns({
|
||||
stockId: assignedStock.id,
|
||||
queryToken: {
|
||||
filter: { returncompleted: returncompleted ? 'true' : 'false' },
|
||||
start: startDate.toISOString(),
|
||||
start: start?.toISOString(),
|
||||
eagerLoading: 3,
|
||||
},
|
||||
});
|
||||
@@ -121,43 +120,97 @@ export class RemissionReturnReceiptService {
|
||||
return returns;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Fetches a specific remission return receipt by receipt and return IDs.
|
||||
// * Validates parameters using FetchRemissionReturnReceiptSchema before making the request.
|
||||
// *
|
||||
// * @async
|
||||
// * @param {FetchRemissionReturnParams} params - The receipt and return identifiers
|
||||
// * @param {FetchRemissionReturnParams} params.receiptId - ID of the receipt to fetch
|
||||
// * @param {FetchRemissionReturnParams} params.returnId - ID of the return containing the receipt
|
||||
// * @param {AbortSignal} [abortSignal] - Optional signal to abort the request
|
||||
// * @returns {Promise<Receipt | undefined>} The receipt object if found, undefined otherwise
|
||||
// * @throws {ResponseArgsError} When the API request fails
|
||||
// * @throws {z.ZodError} When parameter validation fails
|
||||
// *
|
||||
// * @example
|
||||
// * const receipt = await service.fetchRemissionReturnReceipt({
|
||||
// * receiptId: '123',
|
||||
// * returnId: '456'
|
||||
// * });
|
||||
// */
|
||||
// async fetchRemissionReturnReceipt(
|
||||
// params: FetchRemissionReturnParams,
|
||||
// abortSignal?: AbortSignal,
|
||||
// ): Promise<Receipt | undefined> {
|
||||
// this.#logger.debug('Fetching remission return receipt', () => ({ params }));
|
||||
|
||||
// const { receiptId, returnId } =
|
||||
// FetchRemissionReturnReceiptSchema.parse(params);
|
||||
|
||||
// this.#logger.info('Fetching return receipt from API', () => ({
|
||||
// receiptId,
|
||||
// returnId,
|
||||
// }));
|
||||
|
||||
// let req$ = this.#returnService.ReturnGetReturnReceipt({
|
||||
// receiptId,
|
||||
// returnId,
|
||||
// eagerLoading: 2,
|
||||
// });
|
||||
|
||||
// if (abortSignal) {
|
||||
// this.#logger.debug('Request configured with abort signal');
|
||||
// req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
// }
|
||||
|
||||
// const res = await firstValueFrom(req$);
|
||||
|
||||
// if (res?.error) {
|
||||
// this.#logger.error(
|
||||
// 'Failed to fetch return receipt',
|
||||
// new Error(res.message || 'Unknown error'),
|
||||
// );
|
||||
// throw new ResponseArgsError(res);
|
||||
// }
|
||||
|
||||
// const receipt = res?.result as Receipt | undefined;
|
||||
// this.#logger.debug('Successfully fetched return receipt', () => ({
|
||||
// found: !!receipt,
|
||||
// }));
|
||||
|
||||
// return receipt;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Fetches a specific remission return receipt by receipt and return IDs.
|
||||
* Validates parameters using FetchRemissionReturnReceiptSchema before making the request.
|
||||
* Fetches a remission return by its ID.
|
||||
* Validates parameters using FetchReturnSchema before making the request.
|
||||
*
|
||||
* @async
|
||||
* @param {FetchRemissionReturnParams} params - The receipt and return identifiers
|
||||
* @param {FetchRemissionReturnParams} params.receiptId - ID of the receipt to fetch
|
||||
* @param {FetchRemissionReturnParams} params.returnId - ID of the return containing the receipt
|
||||
* @param {FetchReturnParams} params - The parameters for fetching the return
|
||||
* @param {AbortSignal} [abortSignal] - Optional signal to abort the request
|
||||
* @returns {Promise<Receipt | undefined>} The receipt object if found, undefined otherwise
|
||||
* @returns {Promise<Return | undefined>} The return object if found, undefined otherwise
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
* @throws {z.ZodError} When parameter validation fails
|
||||
*
|
||||
* @example
|
||||
* const receipt = await service.fetchRemissionReturnReceipt({
|
||||
* receiptId: '123',
|
||||
* returnId: '456'
|
||||
* });
|
||||
* const returnData = await service.fetchReturn({ returnId: 123 });
|
||||
*/
|
||||
async fetchRemissionReturnReceipt(
|
||||
params: FetchRemissionReturnParams,
|
||||
async fetchReturn(
|
||||
params: FetchReturnParams,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Receipt | undefined> {
|
||||
this.#logger.debug('Fetching remission return receipt', () => ({ params }));
|
||||
): Promise<Return | undefined> {
|
||||
this.#logger.debug('Fetching remission return', () => ({ params }));
|
||||
|
||||
const { receiptId, returnId } =
|
||||
FetchRemissionReturnReceiptSchema.parse(params);
|
||||
const { returnId, eagerLoading = 2 } = FetchReturnSchema.parse(params);
|
||||
|
||||
this.#logger.info('Fetching return receipt from API', () => ({
|
||||
receiptId,
|
||||
this.#logger.info('Fetching return from API', () => ({
|
||||
returnId,
|
||||
}));
|
||||
|
||||
let req$ = this.#returnService.ReturnGetReturnReceipt({
|
||||
receiptId,
|
||||
let req$ = this.#returnService.ReturnGetReturn({
|
||||
returnId,
|
||||
eagerLoading: 2,
|
||||
eagerLoading,
|
||||
});
|
||||
|
||||
if (abortSignal) {
|
||||
@@ -169,38 +222,40 @@ export class RemissionReturnReceiptService {
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to fetch return receipt',
|
||||
'Failed to fetch return',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const receipt = res?.result as Receipt | undefined;
|
||||
this.#logger.debug('Successfully fetched return receipt', () => ({
|
||||
found: !!receipt,
|
||||
const returnData = res?.result as Return | undefined;
|
||||
this.#logger.debug('Successfully fetched return', () => ({
|
||||
found: !!returnData,
|
||||
}));
|
||||
|
||||
return receipt;
|
||||
return returnData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new remission return with an optional receipt number.
|
||||
* Uses CreateReturnSchema to validate parameters before making the request.
|
||||
* Creates a new remission return with the specified parameters.
|
||||
* Validates parameters using CreateReturnSchema before making the request.
|
||||
*
|
||||
* @async
|
||||
* @param {CreateReturn} params - The parameters for creating the return
|
||||
* @param {AbortSignal} [abortSignal] - Optional signal to abort the request
|
||||
* @returns {Promise<Return | undefined>} The created return object if successful, undefined otherwise
|
||||
* @returns {Promise<ResponseArgs<Return> | undefined>} The created return object if successful, undefined otherwise
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
* @throws {z.ZodError} When parameter validation fails
|
||||
*
|
||||
* @example
|
||||
* const newReturn = await service.createReturn({ returnGroup: 'group1' });
|
||||
* const returnResponse = await service.createReturn({
|
||||
* returnGroup: 'group1',
|
||||
* });
|
||||
*/
|
||||
async createReturn(
|
||||
params: CreateReturn,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Return | undefined> {
|
||||
): Promise<ResponseArgs<Return> | undefined> {
|
||||
this.#logger.debug('Create remission return', () => ({ params }));
|
||||
|
||||
const suppliers =
|
||||
@@ -246,27 +301,27 @@ export class RemissionReturnReceiptService {
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const createdReturn = res?.result as Return | undefined;
|
||||
const returnResponse = res as ResponseArgs<Return> | undefined;
|
||||
this.#logger.debug('Successfully created return', () => ({
|
||||
found: !!createdReturn,
|
||||
found: !!returnResponse,
|
||||
}));
|
||||
|
||||
return createdReturn;
|
||||
return returnResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new remission return receipt with the specified parameters.
|
||||
* Validates parameters using CreateReceiptSchema before making the request.
|
||||
* Validates parameters using CreateReceipt before making the request.
|
||||
*
|
||||
* @async
|
||||
* @param {CreateReceipt} params - The parameters for creating the receipt
|
||||
* @param {AbortSignal} [abortSignal] - Optional signal to abort the request
|
||||
* @returns {Promise<Receipt | undefined>} The created receipt object if successful, undefined otherwise
|
||||
* @returns {Promise<ResponseArgs<Receipt> | undefined>} The created receipt object if successful, undefined otherwise
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
* @throws {z.ZodError} When parameter validation fails
|
||||
*
|
||||
* @example
|
||||
* const receipt = await service.createReceipt({
|
||||
* const receiptResponse = await service.createReceipt({
|
||||
* returnId: 123,
|
||||
* receiptNumber: 'ABC-123',
|
||||
* });
|
||||
@@ -274,7 +329,7 @@ export class RemissionReturnReceiptService {
|
||||
async createReceipt(
|
||||
params: CreateReceipt,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Receipt | undefined> {
|
||||
): Promise<ResponseArgs<Receipt> | undefined> {
|
||||
this.#logger.debug('Create remission return receipt', () => ({ params }));
|
||||
|
||||
const stock =
|
||||
@@ -319,22 +374,22 @@ export class RemissionReturnReceiptService {
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const receipt = res?.result as Receipt | undefined;
|
||||
const receiptResponse = res as ResponseArgs<Receipt> | undefined;
|
||||
this.#logger.debug('Successfully created return receipt', () => ({
|
||||
found: !!receipt,
|
||||
found: !!receiptResponse,
|
||||
}));
|
||||
|
||||
return receipt;
|
||||
return receiptResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a package number to an existing return receipt.
|
||||
* Validates parameters using AssignPackageSchema before making the request.
|
||||
* Assigns a package to the specified return receipt.
|
||||
* Validates parameters using AssignPackage before making the request.
|
||||
*
|
||||
* @async
|
||||
* @param {AssignPackage} params - The parameters for assigning the package number
|
||||
* @param {AssignPackage} params - The parameters for assigning the package
|
||||
* @param {AbortSignal} [abortSignal] - Optional signal to abort the request
|
||||
* @returns {Promise<Receipt | undefined>} The updated receipt object if successful, undefined otherwise
|
||||
* @returns {Promise<ResponseArgs<Receipt> | undefined>} The updated receipt object if successful, undefined otherwise
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
* @throws {z.ZodError} When parameter validation fails
|
||||
*
|
||||
@@ -348,7 +403,7 @@ export class RemissionReturnReceiptService {
|
||||
async assignPackage(
|
||||
params: AssignPackage,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Receipt | undefined> {
|
||||
): Promise<ResponseArgs<Receipt> | undefined> {
|
||||
this.#logger.debug('Assign package to return receipt', () => ({ params }));
|
||||
|
||||
const { returnId, receiptId, packageNumber } = params;
|
||||
@@ -382,12 +437,14 @@ export class RemissionReturnReceiptService {
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const receipt = res?.result as Receipt | undefined;
|
||||
this.#logger.debug('Successfully assigned package', () => ({
|
||||
found: !!receipt,
|
||||
}));
|
||||
const receiptWithAssignedPackageResponse = res as
|
||||
| ResponseArgs<Receipt>
|
||||
| undefined;
|
||||
|
||||
return receipt;
|
||||
this.#logger.debug('Successfully assigned package', () => ({
|
||||
found: !!receiptWithAssignedPackageResponse,
|
||||
}));
|
||||
return receiptWithAssignedPackageResponse;
|
||||
}
|
||||
|
||||
async removeReturnItemFromReturnReceipt(params: {
|
||||
@@ -408,6 +465,56 @@ export class RemissionReturnReceiptService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a return receipt and the associated return.
|
||||
* Validates parameters before making the request.
|
||||
*
|
||||
* @async
|
||||
* @param {Object} params - The parameters for the cancellation
|
||||
* @param {number} params.returnId - ID of the return to cancel
|
||||
* @param {number} params.receiptId - ID of the receipt to cancel
|
||||
* @return {Promise<void>} Resolves when the cancellation is successful
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
*/
|
||||
async cancelReturnReceipt(params: {
|
||||
returnId: number;
|
||||
receiptId: number;
|
||||
}): Promise<void> {
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnCancelReturnReceipt(params),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to cancel return receipt',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes a single return receipt and the associated return.
|
||||
* Validates parameters before making the request.
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<Return>} The completed return object
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
*/
|
||||
async cancelReturn(params: { returnId: number }): Promise<void> {
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnCancelReturn(params),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to cancel return',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteReturnItem(params: { itemId: number }) {
|
||||
this.#logger.debug('Deleting return item', () => ({ params }));
|
||||
const res = await firstValueFrom(
|
||||
@@ -425,6 +532,54 @@ export class RemissionReturnReceiptService {
|
||||
return res?.result as ReturnItem;
|
||||
}
|
||||
|
||||
async updateReturnItemImpediment(params: UpdateItemImpediment) {
|
||||
this.#logger.debug('Update return item impediment', () => ({ params }));
|
||||
|
||||
const { itemId, comment } = UpdateItemImpedimentSchema.parse(params);
|
||||
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnReturnItemImpediment({
|
||||
itemId,
|
||||
data: {
|
||||
comment,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to update return item impediment',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
return res?.result as ReturnItem;
|
||||
}
|
||||
|
||||
async updateReturnSuggestionImpediment(params: UpdateItemImpediment) {
|
||||
this.#logger.debug('Update return suggestion impediment', () => ({
|
||||
params,
|
||||
}));
|
||||
const { itemId, comment } = UpdateItemImpedimentSchema.parse(params);
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnReturnSuggestionImpediment({
|
||||
itemId,
|
||||
data: {
|
||||
comment,
|
||||
},
|
||||
}),
|
||||
);
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to update return suggestion impediment',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
return res?.result as ReturnSuggestion;
|
||||
}
|
||||
|
||||
async completeReturnReceipt({
|
||||
returnId,
|
||||
receiptId,
|
||||
@@ -476,6 +631,30 @@ export class RemissionReturnReceiptService {
|
||||
return res?.result as Return;
|
||||
}
|
||||
|
||||
async completeReturnGroup(params: { returnGroup: string }) {
|
||||
this.#logger.debug('Completing return group', () => ({
|
||||
returnId: params.returnGroup,
|
||||
}));
|
||||
|
||||
const res = await firstValueFrom(
|
||||
this.#returnService.ReturnFinalizeReturnGroup(params),
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
this.#logger.error(
|
||||
'Failed to complete return group',
|
||||
new Error(res.message || 'Unknown error'),
|
||||
);
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
this.#logger.info('Successfully completed return group', () => ({
|
||||
returnId: params.returnGroup,
|
||||
}));
|
||||
|
||||
return res?.result as Return[];
|
||||
}
|
||||
|
||||
async completeReturnReceiptAndReturn(params: {
|
||||
returnId: number;
|
||||
receiptId: number;
|
||||
@@ -590,6 +769,8 @@ export class RemissionReturnReceiptService {
|
||||
* returnSuggestionId: 789,
|
||||
* quantity: 10,
|
||||
* inStock: 5,
|
||||
* impedimentComment: 'Restmenge',
|
||||
* remainingQuantity: 5
|
||||
* });
|
||||
*/
|
||||
async addReturnSuggestionItem(
|
||||
@@ -598,8 +779,15 @@ export class RemissionReturnReceiptService {
|
||||
): Promise<ReceiptReturnSuggestionTuple | undefined> {
|
||||
this.#logger.debug('Adding return suggestion item', () => ({ params }));
|
||||
|
||||
const { returnId, receiptId, returnSuggestionId, quantity, inStock } =
|
||||
AddReturnSuggestionItemSchema.parse(params);
|
||||
const {
|
||||
returnId,
|
||||
receiptId,
|
||||
returnSuggestionId,
|
||||
quantity,
|
||||
inStock,
|
||||
impedimentComment,
|
||||
remainingQuantity,
|
||||
} = AddReturnSuggestionItemSchema.parse(params);
|
||||
|
||||
this.#logger.info('Add return suggestion item from API', () => ({
|
||||
returnId,
|
||||
@@ -607,6 +795,8 @@ export class RemissionReturnReceiptService {
|
||||
returnSuggestionId,
|
||||
quantity,
|
||||
inStock,
|
||||
impedimentComment,
|
||||
remainingQuantity,
|
||||
}));
|
||||
|
||||
let req$ = this.#returnService.ReturnAddReturnSuggestion({
|
||||
@@ -616,6 +806,8 @@ export class RemissionReturnReceiptService {
|
||||
returnSuggestionId,
|
||||
quantity,
|
||||
inStock,
|
||||
impedimentComment,
|
||||
remainingQuantity,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -645,89 +837,110 @@ export class RemissionReturnReceiptService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new remission process by creating a return and receipt.
|
||||
* Validates parameters using FetchRemissionReturnReceiptSchema before making the request.
|
||||
* Warenbegleitschein eröffnen
|
||||
* Creates a remission by generating a return and receipt.
|
||||
* Validates parameters using CreateRemissionSchema before making the request.
|
||||
*
|
||||
* @async
|
||||
* @param {Object} params - The parameters for starting the remission
|
||||
* @param {string | undefined} params.returnGroup - Optional group identifier for the return
|
||||
* @param {string | undefined} params.receiptNumber - Optional receipt number
|
||||
* @param {string} params.packageNumber - The package number to assign
|
||||
* @returns {Promise<FetchRemissionReturnParams | undefined>} The created return and receipt identifiers if successful, undefined otherwise
|
||||
* @param {CreateRemission} params - The parameters for creating the remission
|
||||
* @returns {Promise<CreateRemission | undefined>} The created remission object if successful, undefined otherwise
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
* @throws {z.ZodError} When parameter validation fails
|
||||
*
|
||||
* @example
|
||||
* const remission = await service.startRemission({
|
||||
* returnGroup: 'group1',
|
||||
* receiptNumber: 'ABC-123',
|
||||
* packageNumber: 'PKG-789',
|
||||
* const remission = await service.createRemission({
|
||||
* returnId: 123,
|
||||
* receiptId: 456,
|
||||
* });
|
||||
*/
|
||||
async startRemission({
|
||||
async createRemission({
|
||||
returnGroup,
|
||||
receiptNumber,
|
||||
packageNumber,
|
||||
}: {
|
||||
returnGroup: string | undefined;
|
||||
receiptNumber: string | undefined;
|
||||
packageNumber: string;
|
||||
}): Promise<FetchRemissionReturnParams | undefined> {
|
||||
this.#logger.debug('Starting remission', () => ({
|
||||
}): Promise<CreateRemission | undefined> {
|
||||
this.#logger.debug('Create remission', () => ({
|
||||
returnGroup,
|
||||
receiptNumber,
|
||||
packageNumber,
|
||||
}));
|
||||
|
||||
// Warenbegleitschein eröffnen
|
||||
const createdReturn: Return | undefined = await this.createReturn({
|
||||
returnGroup,
|
||||
});
|
||||
const createdReturn: ResponseArgs<Return> | undefined =
|
||||
await this.createReturn({
|
||||
returnGroup,
|
||||
});
|
||||
|
||||
if (!createdReturn) {
|
||||
if (!createdReturn || !createdReturn.result) {
|
||||
this.#logger.error('Failed to create return for remission');
|
||||
return;
|
||||
}
|
||||
|
||||
// Warenbegleitschein eröffnen
|
||||
const createdReceipt: Receipt | undefined = await this.createReceipt({
|
||||
returnId: createdReturn.id,
|
||||
receiptNumber,
|
||||
});
|
||||
const createdReceipt: ResponseArgs<Receipt> | undefined =
|
||||
await this.createReceipt({
|
||||
returnId: createdReturn.result.id,
|
||||
receiptNumber,
|
||||
});
|
||||
|
||||
if (!createdReceipt) {
|
||||
if (!createdReceipt || !createdReceipt.result) {
|
||||
this.#logger.error('Failed to create return receipt');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wannennummer zuweisen
|
||||
await this.assignPackage({
|
||||
returnId: createdReturn.id,
|
||||
receiptId: createdReceipt.id,
|
||||
packageNumber,
|
||||
});
|
||||
const invalidProperties = {
|
||||
...createdReturn.invalidProperties,
|
||||
...createdReceipt.invalidProperties,
|
||||
};
|
||||
|
||||
this.#logger.info('Successfully started remission', () => ({
|
||||
returnId: createdReturn.id,
|
||||
receiptId: createdReceipt.id,
|
||||
this.#logger.info('Successfully created remission', () => ({
|
||||
returnId: createdReturn.result.id,
|
||||
receiptId: createdReceipt.result.id,
|
||||
}));
|
||||
|
||||
return {
|
||||
returnId: createdReturn.id,
|
||||
receiptId: createdReceipt.id,
|
||||
returnId: createdReturn.result.id,
|
||||
receiptId: createdReceipt.result.id,
|
||||
invalidProperties,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -738,21 +951,32 @@ 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,
|
||||
returnSuggestionId: itemId,
|
||||
quantity: addItem.quantity,
|
||||
inStock: addItem.inStock,
|
||||
impedimentComment: (addItem as AddReturnSuggestionItem)
|
||||
.impedimentComment,
|
||||
remainingQuantity: (addItem as AddReturnSuggestionItem)
|
||||
.remainingQuantity,
|
||||
});
|
||||
}
|
||||
|
||||
// ReturnItem
|
||||
if (type === RemissionListType.Pflicht) {
|
||||
if (type === RemissionItemType.ReturnItem) {
|
||||
return await this.addReturnItem({
|
||||
returnId: addItem.returnId,
|
||||
receiptId: addItem.receiptId,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { Item } from '@isa/catalogue/data-access';
|
||||
import { RemissionStockService } from './remission-stock.service';
|
||||
import { getAssortmentFromItem, getRetailPriceFromItem } from '../helpers';
|
||||
|
||||
/**
|
||||
* Service responsible for remission search operations.
|
||||
@@ -323,7 +324,6 @@ export class RemissionSearchService {
|
||||
*
|
||||
* @todo After fetching, StockInStock should be called in the old DomainRemissionService
|
||||
*/
|
||||
// TODO: Im alten DomainRemissionService wird danach StockInStock abgerufen
|
||||
async fetchDepartmentList(
|
||||
params: RemissionQueryTokenInput,
|
||||
abortSignal?: AbortSignal,
|
||||
@@ -388,9 +388,9 @@ export class RemissionSearchService {
|
||||
let req = this.#remiService.RemiCanAddReturnItem({
|
||||
data: items.map((i) => ({
|
||||
product: i.item.product,
|
||||
assortment: 'Basissortiment|B',
|
||||
assortment: getAssortmentFromItem(i.item),
|
||||
predefinedReturnQuantity: i.quantity,
|
||||
retailPrice: i.item.catalogAvailability.price,
|
||||
retailPrice: getRetailPriceFromItem(i.item),
|
||||
source: 'manually-added',
|
||||
returnReason: i.reason,
|
||||
stock: { id: stock.id },
|
||||
@@ -424,10 +424,13 @@ export class RemissionSearchService {
|
||||
|
||||
const req$ = this.#remiService.RemiCreateReturnItem({
|
||||
data: items.map((i) => ({
|
||||
product: i.item.product,
|
||||
assortment: 'Basissortiment|B',
|
||||
product: {
|
||||
...i.item.product,
|
||||
catalogProductNumber: String(i.item.id),
|
||||
},
|
||||
assortment: getAssortmentFromItem(i.item),
|
||||
predefinedReturnQuantity: i.quantity,
|
||||
retailPrice: i.item.catalogAvailability.price,
|
||||
retailPrice: getRetailPriceFromItem(i.item),
|
||||
source: 'manually-added',
|
||||
returnReason: i.reason,
|
||||
stock: { id: stock.id },
|
||||
@@ -444,38 +447,4 @@ export class RemissionSearchService {
|
||||
|
||||
return res.successful?.map((r) => r.value) as ReturnItem[];
|
||||
}
|
||||
|
||||
async addToDepartmentList(
|
||||
items: { item: Item; quantity: number; reason: string }[],
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<ReturnSuggestion[]> {
|
||||
const stock = await this.#remiStockService.fetchAssignedStock(abortSignal);
|
||||
|
||||
if (!stock) {
|
||||
this.#logger.error('No assigned stock found for remission items');
|
||||
throw new Error('No assigned stock found');
|
||||
}
|
||||
|
||||
const req$ = this.#remiService.RemiCreateReturnSuggestions({
|
||||
data: items.map((i) => ({
|
||||
product: i.item.product,
|
||||
assortment: 'Basissortiment|B',
|
||||
predefinedReturnQuantity: i.quantity,
|
||||
retailPrice: i.item.catalogAvailability.price,
|
||||
source: 'manually-added',
|
||||
returnReason: i.reason,
|
||||
stock: { id: stock.id },
|
||||
})),
|
||||
});
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to add to department list', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.successful?.map((r) => r.value) as ReturnSuggestion[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ describe('RemissionStore', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
const mockRemissionReturnReceiptService = {
|
||||
fetchRemissionReturnReceipt: jest.fn(),
|
||||
fetchReturn: jest.fn(),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
||||
@@ -72,52 +72,29 @@ export const RemissionStore = signalStore(
|
||||
remissionReturnReceiptService = inject(RemissionReturnReceiptService),
|
||||
) => ({
|
||||
/**
|
||||
* Private resource for fetching the current remission receipt.
|
||||
*
|
||||
* This resource automatically tracks changes to returnId and receiptId from the store
|
||||
* and refetches the receipt data when either value changes. The resource returns
|
||||
* undefined when either ID is not set, preventing unnecessary HTTP requests.
|
||||
*
|
||||
* The resource uses the injected RemissionReturnReceiptService to fetch receipt data
|
||||
* and supports request cancellation via AbortSignal for proper cleanup.
|
||||
*
|
||||
* @private
|
||||
* @returns A resource instance that manages the receipt data fetching lifecycle
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Access the resource through computed signals
|
||||
* const receipt = computed(() => store._receiptResource.value());
|
||||
* const status = computed(() => store._receiptResource.status());
|
||||
* const error = computed(() => store._receiptResource.error());
|
||||
*
|
||||
* // Manually reload the resource
|
||||
* store._receiptResource.reload();
|
||||
* ```
|
||||
*
|
||||
* @see {@link https://angular.dev/guide/signals/resource} Angular Resource API documentation
|
||||
* Resource for fetching the receipt data based on the current receiptId.
|
||||
* This resource is automatically reloaded when the receiptId changes.
|
||||
* @returnId is undefined, the resource will not fetch any data.
|
||||
* @returnId is set, it fetches the receipt data from the service.
|
||||
*/
|
||||
_receiptResource: resource({
|
||||
_fetchReturnResource: resource({
|
||||
params: () => ({
|
||||
returnId: store.returnId(),
|
||||
receiptId: store.receiptId(),
|
||||
}),
|
||||
loader: async ({ params, abortSignal }) => {
|
||||
const { receiptId, returnId } = params;
|
||||
const { returnId } = params;
|
||||
|
||||
if (!receiptId || !returnId) {
|
||||
if (!returnId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const receipt =
|
||||
await remissionReturnReceiptService.fetchRemissionReturnReceipt(
|
||||
{
|
||||
returnId,
|
||||
receiptId,
|
||||
},
|
||||
abortSignal,
|
||||
);
|
||||
return receipt;
|
||||
const returnData = await remissionReturnReceiptService.fetchReturn(
|
||||
{
|
||||
returnId,
|
||||
},
|
||||
abortSignal,
|
||||
);
|
||||
return returnData;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
@@ -126,7 +103,7 @@ export const RemissionStore = signalStore(
|
||||
remissionStarted: computed(
|
||||
() => store.returnId() !== undefined && store.receiptId() !== undefined,
|
||||
),
|
||||
receipt: computed(() => store._receiptResource.value()),
|
||||
returnData: computed(() => store._fetchReturnResource.value()),
|
||||
})),
|
||||
withMethods((store) => ({
|
||||
/**
|
||||
@@ -158,15 +135,44 @@ export const RemissionStore = signalStore(
|
||||
returnId,
|
||||
receiptId,
|
||||
});
|
||||
store._receiptResource.reload();
|
||||
store._fetchReturnResource.reload();
|
||||
store.storeState();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reloads the receipt resource.
|
||||
* This method should be called when the receipt data needs to be refreshed.
|
||||
* Reloads the return resource to fetch the latest data.
|
||||
* This is useful when the return data might have changed and needs to be refreshed.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* remissionStore.reloadReturn();
|
||||
* ```
|
||||
*/
|
||||
reloadReceipt() {
|
||||
store._receiptResource.reload();
|
||||
reloadReturn() {
|
||||
store._fetchReturnResource.reload();
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the current remission matches the provided returnId and receiptId.
|
||||
* This is useful for determining if the current remission is active in the context of a component.
|
||||
*
|
||||
* @param returnId - The return ID to check against the current remission
|
||||
* @param receiptId - The receipt ID to check against the current remission
|
||||
* @returns {boolean} True if the current remission matches the provided IDs, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const isCurrent = remissionStore.isCurrentRemission(123, 456);
|
||||
* ```
|
||||
*/
|
||||
isCurrentRemission({
|
||||
returnId,
|
||||
receiptId,
|
||||
}: {
|
||||
returnId: number | undefined;
|
||||
receiptId: number | undefined;
|
||||
}): boolean {
|
||||
return store.returnId() === returnId && store.receiptId() === receiptId;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -273,15 +279,15 @@ export const RemissionStore = signalStore(
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets the remission store to its initial state.
|
||||
* Clears all selected items, quantities, and resets return/receipt IDs.
|
||||
* Clears the remission store state, resetting all values to their initial state.
|
||||
* This is useful for starting a new remission process or clearing the current state.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* remissionStore.resetRemission();
|
||||
* remissionStore.clearState();
|
||||
* ```
|
||||
*/
|
||||
finishRemission() {
|
||||
clearState() {
|
||||
patchState(store, initialState);
|
||||
store.storeState();
|
||||
},
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<filter-input-menu-button
|
||||
[filterInput]="filterDepartmentInput()"
|
||||
[label]="selectedDepartments()"
|
||||
[commitOnClose]="true"
|
||||
[label]="selectedDepartment()"
|
||||
[canApply]="true"
|
||||
(closed)="rollbackFilterInput()"
|
||||
>
|
||||
</filter-input-menu-button>
|
||||
|
||||
@if (displayCapacityValues()) {
|
||||
@if (selectedDepartment()) {
|
||||
<ui-toolbar class="ui-toolbar-rounded">
|
||||
<span class="isa-text-body-2-regular"
|
||||
><span class="isa-text-body-2-bold"
|
||||
<span class="flex gap-1 isa-text-body-2-regular"
|
||||
><span *uiSkeletonLoader="capacityFetching()" class="isa-text-body-2-bold"
|
||||
>{{ leistung() }}/{{ maxLeistung() }}</span
|
||||
>
|
||||
Leistung</span
|
||||
>
|
||||
<span class="isa-text-body-2-regular"
|
||||
><span class="isa-text-body-2-bold"
|
||||
<span class="flex gap-1 isa-text-body-2-regular"
|
||||
><span *uiSkeletonLoader="capacityFetching()" class="isa-text-body-2-bold"
|
||||
>{{ stapel() }}/{{ maxStapel() }}</span
|
||||
>
|
||||
Stapel</span
|
||||
@@ -23,7 +24,6 @@
|
||||
class="w-6 h-6 flex items-center justify-center text-isa-accent-blue"
|
||||
uiTooltip
|
||||
[title]="'Stapel/Leistungsplätze'"
|
||||
[content]="''"
|
||||
[triggerOn]="['click', 'hover']"
|
||||
>
|
||||
<ng-icon size="1.5rem" name="isaOtherInfo"></ng-icon>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ToolbarComponent } from '@isa/ui/toolbar';
|
||||
import { TooltipDirective } from '@isa/ui/tooltip';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { createRemissionCapacityResource } from '../resources';
|
||||
import { SkeletonLoaderDirective } from '@isa/ui/skeleton-loader';
|
||||
|
||||
@Component({
|
||||
selector: 'remi-feature-remission-list-department-elements',
|
||||
@@ -30,6 +31,7 @@ import { createRemissionCapacityResource } from '../resources';
|
||||
ToolbarComponent,
|
||||
TooltipDirective,
|
||||
NgIconComponent,
|
||||
SkeletonLoaderDirective,
|
||||
],
|
||||
})
|
||||
export class RemissionListDepartmentElementsComponent {
|
||||
@@ -50,16 +52,19 @@ 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;
|
||||
return 'Abteilung auswählen';
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -69,18 +74,23 @@ export class RemissionListDepartmentElementsComponent {
|
||||
*/
|
||||
capacityResource = createRemissionCapacityResource(() => {
|
||||
return {
|
||||
departments: this.selectedDepartments()
|
||||
?.split(',')
|
||||
.map((d) => d.trim()),
|
||||
departments: [this.selectedDepartment()],
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed signal to get the current value of the capacity resource.
|
||||
* @returns {Array} The current capacity values or an empty array if not available.
|
||||
*/
|
||||
capacityResourceValue = computed(() => this.capacityResource.value());
|
||||
|
||||
displayCapacityValues = computed(() => {
|
||||
const value = this.capacityResourceValue();
|
||||
return !!value && value?.length > 0;
|
||||
});
|
||||
/**
|
||||
* Computed signal to check if the capacity resource is currently fetching data.
|
||||
* @returns {boolean} True if the resource is loading, false otherwise.
|
||||
*/
|
||||
capacityFetching = computed(
|
||||
() => this.capacityResource.status() === 'loading',
|
||||
);
|
||||
|
||||
leistungValues = computed(() => {
|
||||
const value = this.capacityResourceValue();
|
||||
@@ -135,4 +145,9 @@ export class RemissionListDepartmentElementsComponent {
|
||||
})
|
||||
: 0;
|
||||
});
|
||||
|
||||
rollbackFilterInput() {
|
||||
const inputKey = this.filterDepartmentInput()?.key;
|
||||
this.#filterService.rollbackInput([inputKey!]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
@let emptyState = displayEmptyState();
|
||||
@if (emptyState) {
|
||||
<ui-empty-state
|
||||
class="w-full justify-self-center"
|
||||
[appearance]="emptyState.appearance"
|
||||
[title]="emptyState.title"
|
||||
[description]="emptyState.description"
|
||||
>
|
||||
</ui-empty-state>
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
input,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { FilterService } from '@isa/shared/filter';
|
||||
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';
|
||||
|
||||
type EmptyState =
|
||||
| {
|
||||
title: string;
|
||||
description: string;
|
||||
appearance: EmptyStateAppearance;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
@Component({
|
||||
selector: 'remi-feature-remission-list-empty-state',
|
||||
templateUrl: './remission-list-empty-state.component.html',
|
||||
styleUrl: './remission-list-empty-state.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [EmptyStateComponent],
|
||||
})
|
||||
export class RemissionListEmptyStateComponent {
|
||||
/**
|
||||
* FilterService instance for managing filter state and queries.
|
||||
* @private
|
||||
*/
|
||||
#filterService = inject(FilterService);
|
||||
|
||||
listFetching = input<boolean>();
|
||||
isDepartment = input<boolean>();
|
||||
isReloadSearch = input<boolean>();
|
||||
hasValidSearchTerm = input<boolean>();
|
||||
hits = input<number>();
|
||||
|
||||
/**
|
||||
* Computed signal that determines the appropriate empty state to display
|
||||
* based on the current state of the remission list, search term, and filters.
|
||||
* @returns An EmptyState object with title, description, and appearance, or undefined if no empty state should be shown.
|
||||
* The priority for empty states is as follows:
|
||||
* 1. Department list with no department selected.
|
||||
* 2. All done state when the list is fully processed and no items remain.
|
||||
* 3. No results state when there are no items matching the current search and filters.
|
||||
* If none of these conditions are met, returns undefined.
|
||||
* @see EmptyStateAppearance for possible appearance values.
|
||||
* @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() && !this.hasValidSearchTerm()) {
|
||||
// Prio 1: Abteilungsremission - Es ist noch keine Abteilung ausgewählt
|
||||
if (
|
||||
this.isDepartment() &&
|
||||
!this.#filterService.query()?.filter['abteilungen']
|
||||
) {
|
||||
return {
|
||||
title: 'Abteilung auswählen',
|
||||
description:
|
||||
'Wählen Sie zuerst eine Abteilung, anschließend werden die entsprechenden Positionen angezeigt.',
|
||||
appearance: EmptyStateAppearance.SelectAction,
|
||||
};
|
||||
}
|
||||
|
||||
// Prio 2: Liste abgearbeitet und keine Artikel mehr vorhanden
|
||||
if (
|
||||
this.hits() === 0 &&
|
||||
this.isReloadSearch()
|
||||
) {
|
||||
return {
|
||||
title: 'Alles erledigt',
|
||||
description: 'Hier gibt es gerade nichts zu tun',
|
||||
appearance: EmptyStateAppearance.AllDone,
|
||||
};
|
||||
}
|
||||
|
||||
// Prio 3: Keine Ergebnisse bei leerem Suchbegriff (nur Filter gesetzt)
|
||||
if (this.hits() === 0) {
|
||||
return {
|
||||
title: 'Keine Suchergebnisse',
|
||||
description:
|
||||
'Bitte prüfen Sie die Schreibweise oder ändern Sie die Filtereinstellungen.',
|
||||
appearance: EmptyStateAppearance.NoResults,
|
||||
};
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
uiTextButton
|
||||
color="strong"
|
||||
(click)="deleteItemFromList()"
|
||||
[disabled]="deleteRemissionListItemInProgress()"
|
||||
[pending]="deleteRemissionListItemInProgress()"
|
||||
[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]="deleteRemissionListItemInProgress()"
|
||||
[disabled]="removeOrUpdateItem().inProgress"
|
||||
data-what="button"
|
||||
data-which="change-remission-quantity"
|
||||
>
|
||||
|
||||
@@ -5,26 +5,29 @@ import {
|
||||
inject,
|
||||
input,
|
||||
model,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { FormsModule, Validators } from '@angular/forms';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import {
|
||||
RemissionItem,
|
||||
RemissionItemSource,
|
||||
RemissionListType,
|
||||
RemissionReturnReceiptService,
|
||||
RemissionStore,
|
||||
UpdateItem,
|
||||
} from '@isa/remission/data-access';
|
||||
import { TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { injectFeedbackDialog, injectNumberInputDialog } from '@isa/ui/dialog';
|
||||
import { CheckboxComponent } from '@isa/ui/input-controls';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { injectRemissionListType } from '../injects/inject-remission-list-type';
|
||||
|
||||
@Component({
|
||||
selector: 'remi-feature-remission-list-item-actions',
|
||||
templateUrl: './remission-list-item-actions.component.html',
|
||||
styleUrl: './remission-list-item-actions.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [FormsModule, TextButtonComponent, CheckboxComponent],
|
||||
imports: [FormsModule, TextButtonComponent],
|
||||
})
|
||||
export class RemissionListItemActionsComponent {
|
||||
/**
|
||||
@@ -53,6 +56,12 @@ export class RemissionListItemActionsComponent {
|
||||
*/
|
||||
#store = inject(RemissionStore);
|
||||
|
||||
/**
|
||||
* Signal indicating whether remission has started.
|
||||
* Used to determine if the item can be selected or not.
|
||||
*/
|
||||
remissionListType = injectRemissionListType();
|
||||
|
||||
/**
|
||||
* Service for handling remission return receipts.
|
||||
* @private
|
||||
@@ -66,18 +75,19 @@ export class RemissionListItemActionsComponent {
|
||||
item = input.required<RemissionItem>();
|
||||
|
||||
/**
|
||||
* Signal indicating whether the item has stock to remit.
|
||||
* This is used to conditionally display the select component.
|
||||
* The stock to remit for the current item.
|
||||
* This is used to determine if the remission quantity can be changed.
|
||||
* @default 0
|
||||
*/
|
||||
hasStockToRemit = input.required<boolean>();
|
||||
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.
|
||||
*/
|
||||
deleteRemissionListItemInProgress = model<boolean>();
|
||||
removeOrUpdateItem = model<UpdateItem>({
|
||||
inProgress: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Signal indicating whether remission has started.
|
||||
@@ -85,12 +95,18 @@ export class RemissionListItemActionsComponent {
|
||||
*/
|
||||
remissionStarted = computed(() => this.#store.remissionStarted());
|
||||
|
||||
/**
|
||||
* Input signal indicating whether the selected quantity differs from the stock to remit.
|
||||
* This is used to determine if the remission quantity can be changed.
|
||||
*/
|
||||
selectedQuantityDiffersFromStockToRemit = input<boolean>(true);
|
||||
|
||||
/**
|
||||
* Computes whether to display the button for changing remission quantity.
|
||||
* Only displays if remission has started and there is stock to remit.
|
||||
*/
|
||||
displayChangeQuantityButton = computed(
|
||||
() => this.remissionStarted() && this.hasStockToRemit(),
|
||||
() => this.remissionStarted() && this.stockToRemit() > 0,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -101,19 +117,33 @@ 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 for a new quantity and updates the store if valid.
|
||||
* Displays feedback dialog upon successful update.
|
||||
*
|
||||
* @returns A promise that resolves when the dialog is closed.
|
||||
* Prompts the user to enter a new quantity and updates the store with the new value
|
||||
* if valid.
|
||||
* 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,
|
||||
data: {
|
||||
message: 'Wie viele Exemplare können remittiert werden?',
|
||||
subMessage: this.selectedQuantityDiffersFromStockToRemit()
|
||||
? 'Originale Remi-Menge:'
|
||||
: undefined,
|
||||
subMessageValue: this.selectedQuantityDiffersFromStockToRemit()
|
||||
? `${this.stockToRemit()}x`
|
||||
: undefined,
|
||||
inputLabel: 'Remi-Menge',
|
||||
closeText: 'Produkt nicht gefunden',
|
||||
inputValidation: [
|
||||
{
|
||||
errorKey: 'required',
|
||||
@@ -130,36 +160,77 @@ export class RemissionListItemActionsComponent {
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
this.highlight.set(false);
|
||||
|
||||
// Dialog Close
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemId = this.item()?.id;
|
||||
const quantity = result?.inputValue;
|
||||
|
||||
if (itemId && quantity !== undefined && quantity > 0) {
|
||||
// Speichern CTA
|
||||
this.#store.updateRemissionQuantity(itemId, this.item(), quantity);
|
||||
this.#feedbackDialog({
|
||||
data: { message: 'Remi-Menge wurde geändert' },
|
||||
});
|
||||
} else if (itemId) {
|
||||
// Produkt nicht gefunden CTA
|
||||
try {
|
||||
this.removeOrUpdateItem.set({ inProgress: true });
|
||||
|
||||
let itemToUpdate: RemissionItem | undefined;
|
||||
if (this.remissionListType() === RemissionListType.Pflicht) {
|
||||
itemToUpdate =
|
||||
await this.#remissionReturnReceiptService.updateReturnItemImpediment(
|
||||
{
|
||||
itemId,
|
||||
comment: 'Produkt nicht gefunden',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (this.remissionListType() === RemissionListType.Abteilung) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the current item from the remission list.
|
||||
* Only proceeds if the item has an ID and deletion is not already in progress.
|
||||
* Sets the deleteRemissionListItemInProgress signal to true during deletion.
|
||||
* Logs an error if the deletion fails.
|
||||
* Only proceeds if the item has an ID and no other deletion is in progress.
|
||||
* Calls the service to delete the item and handles any errors.
|
||||
*/
|
||||
async deleteItemFromList() {
|
||||
const itemId = this.item()?.id;
|
||||
if (!itemId || this.deleteRemissionListItemInProgress()) {
|
||||
if (!itemId || this.removeOrUpdateItem().inProgress) {
|
||||
return;
|
||||
}
|
||||
this.deleteRemissionListItemInProgress.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.deleteRemissionListItemInProgress.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { RemissionItem, RemissionStore } from '@isa/remission/data-access';
|
||||
import { TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { CheckboxComponent } from '@isa/ui/input-controls';
|
||||
|
||||
@Component({
|
||||
@@ -15,7 +14,7 @@ import { CheckboxComponent } from '@isa/ui/input-controls';
|
||||
templateUrl: './remission-list-item-select.component.html',
|
||||
styleUrl: './remission-list-item-select.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [FormsModule, TextButtonComponent, CheckboxComponent],
|
||||
imports: [FormsModule, CheckboxComponent],
|
||||
})
|
||||
export class RemissionListItemSelectComponent {
|
||||
/**
|
||||
|
||||
@@ -28,10 +28,20 @@
|
||||
[availableStock]="availableStock()"
|
||||
[stockToRemit]="selectedStockToRemit() ?? stockToRemit()"
|
||||
[targetStock]="targetStock()"
|
||||
[stockFetching]="stockFetching()"
|
||||
[zob]="stock()?.minStockCategoryManagement ?? 0"
|
||||
></remi-product-stock-info>
|
||||
</ui-item-row-data>
|
||||
|
||||
@if (displayImpediment()) {
|
||||
<ui-item-row-data
|
||||
class="w-fit"
|
||||
[class.row-start-second]="desktopBreakpoint()"
|
||||
>
|
||||
<ui-label [type]="Labeltype.Notice">{{ impediment() }}</ui-label>
|
||||
</ui-item-row-data>
|
||||
}
|
||||
|
||||
<ui-item-row-data class="justify-end desktop:justify-between col-end-last">
|
||||
@if (desktopBreakpoint()) {
|
||||
<remi-feature-remission-list-item-select
|
||||
@@ -44,10 +54,11 @@
|
||||
|
||||
<remi-feature-remission-list-item-actions
|
||||
[item]="i"
|
||||
[hasStockToRemit]="hasStockToRemit()"
|
||||
(deleteRemissionListItemInProgressChange)="
|
||||
deleteRemissionListItemInProgress.set($event)
|
||||
[stockToRemit]="stockToRemit()"
|
||||
[selectedQuantityDiffersFromStockToRemit]="
|
||||
selectedQuantityDiffersFromStockToRemit()
|
||||
"
|
||||
(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 {
|
||||
@@ -10,6 +16,10 @@
|
||||
@apply isa-desktop:col-span-2 desktop-large:col-span-1;
|
||||
}
|
||||
|
||||
.row-start-second {
|
||||
grid-row-start: 2;
|
||||
}
|
||||
|
||||
.col-end-last {
|
||||
grid-column-end: -1;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
import { RemissionListItemSelectComponent } from './remission-list-item-select.component';
|
||||
import { RemissionListItemActionsComponent } from './remission-list-item-actions.component';
|
||||
import { LabelComponent } from '@isa/ui/label';
|
||||
import { signal } from '@angular/core';
|
||||
|
||||
// --- Setup dynamic mocking for injectRemissionListType ---
|
||||
@@ -25,6 +26,15 @@ jest.mock('../injects/inject-remission-list-type', () => ({
|
||||
injectRemissionListType: () => () => remissionListTypeValue,
|
||||
}));
|
||||
|
||||
// Mock the breakpoint function
|
||||
jest.mock('@isa/ui/layout', () => ({
|
||||
breakpoint: jest.fn(() => jest.fn(() => true)), // Default to desktop
|
||||
Breakpoint: {
|
||||
DekstopL: 'DekstopL',
|
||||
DekstopXL: 'DekstopXL',
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the calculation functions to have predictable behavior
|
||||
jest.mock('@isa/remission/data-access', () => ({
|
||||
...jest.requireActual('@isa/remission/data-access'),
|
||||
@@ -36,6 +46,7 @@ jest.mock('@isa/remission/data-access', () => ({
|
||||
// Mock the RemissionStore
|
||||
const mockRemissionStore = {
|
||||
selectedQuantity: signal({}),
|
||||
removeItem: jest.fn(),
|
||||
};
|
||||
|
||||
describe('RemissionListItemComponent', () => {
|
||||
@@ -85,6 +96,7 @@ describe('RemissionListItemComponent', () => {
|
||||
MockComponent(ProductShelfMetaInfoComponent),
|
||||
MockComponent(RemissionListItemSelectComponent),
|
||||
MockComponent(RemissionListItemActionsComponent),
|
||||
MockComponent(LabelComponent),
|
||||
],
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
@@ -101,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 {
|
||||
@@ -150,23 +163,64 @@ describe('RemissionListItemComponent', () => {
|
||||
expect(component.productGroupValue()).toBe(testValue);
|
||||
});
|
||||
|
||||
it('should have deleteRemissionListItemInProgress model with undefined default', () => {
|
||||
it('should have stockFetching input with false default', () => {
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
expect(component.deleteRemissionListItemInProgress()).toBeUndefined();
|
||||
expect(component.stockFetching()).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept deleteRemissionListItemInProgress model value', () => {
|
||||
it('should accept stockFetching input value', () => {
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.componentRef.setInput('deleteRemissionListItemInProgress', true);
|
||||
fixture.componentRef.setInput('stockFetching', true);
|
||||
fixture.detectChanges();
|
||||
expect(component.deleteRemissionListItemInProgress()).toBe(true);
|
||||
expect(component.stockFetching()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have removeOrUpdateItem output', () => {
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
expect(component.removeOrUpdateItem).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('computed properties', () => {
|
||||
describe('desktopBreakpoint', () => {
|
||||
it('should be defined and accessible', () => {
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.desktopBreakpoint).toBeDefined();
|
||||
expect(typeof component.desktopBreakpoint()).toBe('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remissionListType', () => {
|
||||
it('should return injected remission list type', () => {
|
||||
setRemissionListType(RemissionListType.Abteilung);
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.remissionListType()).toBe(RemissionListType.Abteilung);
|
||||
});
|
||||
|
||||
it('should update when remission list type changes', () => {
|
||||
setRemissionListType(RemissionListType.Pflicht);
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.remissionListType()).toBe(RemissionListType.Pflicht);
|
||||
|
||||
setRemissionListType(RemissionListType.Abteilung);
|
||||
expect(component.remissionListType()).toBe(RemissionListType.Abteilung);
|
||||
});
|
||||
});
|
||||
|
||||
describe('availableStock', () => {
|
||||
it('should calculate available stock correctly', () => {
|
||||
const {
|
||||
@@ -211,11 +265,19 @@ describe('RemissionListItemComponent', () => {
|
||||
});
|
||||
|
||||
describe('targetStock', () => {
|
||||
it('should calculate target stock correctly', () => {
|
||||
const { calculateTargetStock } = require('@isa/remission/data-access');
|
||||
it('should calculate target stock with remainingQuantityInStock when selected quantity matches stock to remit', () => {
|
||||
const {
|
||||
calculateTargetStock,
|
||||
getStockToRemit,
|
||||
} = require('@isa/remission/data-access');
|
||||
calculateTargetStock.mockReturnValue(75);
|
||||
getStockToRemit.mockReturnValue(25);
|
||||
mockRemissionStore.selectedQuantity.set({ 1: 25 }); // Same as stockToRemit
|
||||
|
||||
const mockItem = createMockReturnItem({ remainingQuantityInStock: 15 });
|
||||
const mockItem = createMockReturnItem({
|
||||
id: 1,
|
||||
remainingQuantityInStock: 15,
|
||||
});
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
@@ -223,7 +285,56 @@ describe('RemissionListItemComponent', () => {
|
||||
expect(component.targetStock()).toBe(75);
|
||||
expect(calculateTargetStock).toHaveBeenCalledWith({
|
||||
availableStock: 100, // default mock value
|
||||
stockToRemit: 0, // default mock value
|
||||
stockToRemit: 25,
|
||||
remainingQuantityInStock: 15,
|
||||
});
|
||||
});
|
||||
|
||||
it('should calculate target stock without remainingQuantityInStock when selected quantity differs from stock to remit', () => {
|
||||
const {
|
||||
calculateTargetStock,
|
||||
getStockToRemit,
|
||||
} = require('@isa/remission/data-access');
|
||||
calculateTargetStock.mockReturnValue(80);
|
||||
getStockToRemit.mockReturnValue(25);
|
||||
mockRemissionStore.selectedQuantity.set({ 1: 20 }); // Different from stockToRemit
|
||||
|
||||
const mockItem = createMockReturnItem({
|
||||
id: 1,
|
||||
remainingQuantityInStock: 15,
|
||||
});
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.targetStock()).toBe(80);
|
||||
expect(calculateTargetStock).toHaveBeenCalledWith({
|
||||
availableStock: 100, // default mock value
|
||||
stockToRemit: 20, // selected quantity, not calculated stockToRemit
|
||||
});
|
||||
});
|
||||
|
||||
it('should calculate target stock with remainingQuantityInStock when no selected quantity exists', () => {
|
||||
const {
|
||||
calculateTargetStock,
|
||||
getStockToRemit,
|
||||
} = require('@isa/remission/data-access');
|
||||
calculateTargetStock.mockReturnValue(75);
|
||||
getStockToRemit.mockReturnValue(25);
|
||||
mockRemissionStore.selectedQuantity.set({}); // No selected quantity
|
||||
|
||||
const mockItem = createMockReturnItem({
|
||||
id: 1,
|
||||
remainingQuantityInStock: 15,
|
||||
});
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.targetStock()).toBe(75);
|
||||
expect(calculateTargetStock).toHaveBeenCalledWith({
|
||||
availableStock: 100, // default mock value
|
||||
stockToRemit: 25, // calculated stockToRemit
|
||||
remainingQuantityInStock: 15,
|
||||
});
|
||||
});
|
||||
@@ -275,10 +386,55 @@ describe('RemissionListItemComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasStockToRemit', () => {
|
||||
it('should return true when stockToRemit > 0', () => {
|
||||
describe('selectedQuantityDiffersFromStockToRemit', () => {
|
||||
it('should return true when selected quantity differs from stock to remit', () => {
|
||||
const { getStockToRemit } = require('@isa/remission/data-access');
|
||||
getStockToRemit.mockReturnValue(10);
|
||||
mockRemissionStore.selectedQuantity.set({ 1: 15 });
|
||||
|
||||
const mockItem = createMockReturnItem({ id: 1 });
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.selectedQuantityDiffersFromStockToRemit()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when selected quantity equals stock to remit', () => {
|
||||
const { getStockToRemit } = require('@isa/remission/data-access');
|
||||
getStockToRemit.mockReturnValue(15);
|
||||
mockRemissionStore.selectedQuantity.set({ 1: 15 });
|
||||
|
||||
const mockItem = createMockReturnItem({ id: 1 });
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.selectedQuantityDiffersFromStockToRemit()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no selected quantity exists', () => {
|
||||
const { getStockToRemit } = require('@isa/remission/data-access');
|
||||
getStockToRemit.mockReturnValue(10);
|
||||
mockRemissionStore.selectedQuantity.set({});
|
||||
|
||||
const mockItem = createMockReturnItem({ id: 1 });
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.selectedQuantityDiffersFromStockToRemit()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasStockToRemit', () => {
|
||||
it('should return true when both availableStock > 0 and stockToRemit > 0', () => {
|
||||
const {
|
||||
getStockToRemit,
|
||||
calculateAvailableStock,
|
||||
} = require('@isa/remission/data-access');
|
||||
getStockToRemit.mockReturnValue(5);
|
||||
calculateAvailableStock.mockReturnValue(10);
|
||||
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
@@ -287,9 +443,13 @@ describe('RemissionListItemComponent', () => {
|
||||
expect(component.hasStockToRemit()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when stockToRemit is 0', () => {
|
||||
const { getStockToRemit } = require('@isa/remission/data-access');
|
||||
it('should return false when stockToRemit is 0 even if availableStock > 0', () => {
|
||||
const {
|
||||
getStockToRemit,
|
||||
calculateAvailableStock,
|
||||
} = require('@isa/remission/data-access');
|
||||
getStockToRemit.mockReturnValue(0);
|
||||
calculateAvailableStock.mockReturnValue(10);
|
||||
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
@@ -298,9 +458,73 @@ describe('RemissionListItemComponent', () => {
|
||||
expect(component.hasStockToRemit()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when stockToRemit is negative', () => {
|
||||
const { getStockToRemit } = require('@isa/remission/data-access');
|
||||
it('should return false when stockToRemit is negative even if availableStock > 0', () => {
|
||||
const {
|
||||
getStockToRemit,
|
||||
calculateAvailableStock,
|
||||
} = require('@isa/remission/data-access');
|
||||
getStockToRemit.mockReturnValue(-1);
|
||||
calculateAvailableStock.mockReturnValue(10);
|
||||
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.hasStockToRemit()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when availableStock is 0 even if stockToRemit > 0', () => {
|
||||
const {
|
||||
getStockToRemit,
|
||||
calculateAvailableStock,
|
||||
} = require('@isa/remission/data-access');
|
||||
getStockToRemit.mockReturnValue(5);
|
||||
calculateAvailableStock.mockReturnValue(0);
|
||||
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.hasStockToRemit()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when availableStock is negative even if stockToRemit > 0', () => {
|
||||
const {
|
||||
getStockToRemit,
|
||||
calculateAvailableStock,
|
||||
} = require('@isa/remission/data-access');
|
||||
getStockToRemit.mockReturnValue(5);
|
||||
calculateAvailableStock.mockReturnValue(-1);
|
||||
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.hasStockToRemit()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when both availableStock and stockToRemit are 0', () => {
|
||||
const {
|
||||
getStockToRemit,
|
||||
calculateAvailableStock,
|
||||
} = require('@isa/remission/data-access');
|
||||
getStockToRemit.mockReturnValue(0);
|
||||
calculateAvailableStock.mockReturnValue(0);
|
||||
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.hasStockToRemit()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when both availableStock and stockToRemit are negative', () => {
|
||||
const {
|
||||
getStockToRemit,
|
||||
calculateAvailableStock,
|
||||
} = require('@isa/remission/data-access');
|
||||
getStockToRemit.mockReturnValue(-1);
|
||||
calculateAvailableStock.mockReturnValue(-2);
|
||||
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
@@ -319,6 +543,135 @@ describe('RemissionListItemComponent', () => {
|
||||
const orientation = component.remiProductInfoOrientation();
|
||||
expect(['horizontal', 'vertical']).toContain(orientation);
|
||||
});
|
||||
|
||||
it('should depend on desktop breakpoint', () => {
|
||||
fixture.componentRef.setInput('item', createMockReturnItem());
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
// The function should compute based on the breakpoint
|
||||
const orientation = component.remiProductInfoOrientation();
|
||||
expect(typeof orientation).toBe('string');
|
||||
expect(['horizontal', 'vertical']).toContain(orientation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('displayImpediment', () => {
|
||||
it('should return truthy when item has impediment', () => {
|
||||
const mockItem = createMockReturnItem({
|
||||
impediment: {
|
||||
comment: 'Test impediment',
|
||||
attempts: 2,
|
||||
},
|
||||
});
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.displayImpediment()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return truthy when item is descendant of enabled impediment', () => {
|
||||
const mockItem = createMockReturnItem({
|
||||
descendantOf: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.displayImpediment()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return falsy when item has no impediment and is not descendant of enabled impediment', () => {
|
||||
const mockItem = createMockReturnItem({
|
||||
impediment: undefined,
|
||||
descendantOf: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.displayImpediment()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return falsy when item has no impediment and no descendantOf property', () => {
|
||||
const mockItem = createMockReturnItem({
|
||||
impediment: undefined,
|
||||
});
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.displayImpediment()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('impediment', () => {
|
||||
it('should return impediment comment when available', () => {
|
||||
const mockItem = createMockReturnItem({
|
||||
impediment: {
|
||||
comment: 'Custom impediment message',
|
||||
attempts: 3,
|
||||
},
|
||||
});
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.impediment()).toBe('Custom impediment message (3)');
|
||||
});
|
||||
|
||||
it('should return default "Restmenge" when no comment provided', () => {
|
||||
const mockItem = createMockReturnItem({
|
||||
impediment: {
|
||||
attempts: 2,
|
||||
},
|
||||
});
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.impediment()).toBe('Restmenge (2)');
|
||||
});
|
||||
|
||||
it('should return only comment when no attempts provided', () => {
|
||||
const mockItem = createMockReturnItem({
|
||||
impediment: {
|
||||
comment: 'Custom message',
|
||||
},
|
||||
});
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.impediment()).toBe('Custom message');
|
||||
});
|
||||
|
||||
it('should return default "Restmenge" when impediment is empty object', () => {
|
||||
const mockItem = createMockReturnItem({
|
||||
impediment: {},
|
||||
});
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.impediment()).toBe('Restmenge');
|
||||
});
|
||||
|
||||
it('should return "Restmenge" when impediment is undefined', () => {
|
||||
const mockItem = createMockReturnItem({
|
||||
impediment: undefined,
|
||||
});
|
||||
fixture.componentRef.setInput('item', mockItem);
|
||||
fixture.componentRef.setInput('stock', createMockStockInfo());
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.impediment()).toBe('Restmenge');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -361,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,18 +16,19 @@ import {
|
||||
ReturnItem,
|
||||
ReturnSuggestion,
|
||||
StockInfo,
|
||||
UpdateItem,
|
||||
} from '@isa/remission/data-access';
|
||||
import {
|
||||
ProductInfoComponent,
|
||||
ProductShelfMetaInfoComponent,
|
||||
ProductStockInfoComponent,
|
||||
} from '@isa/remission/shared/product';
|
||||
import { TextButtonComponent } from '@isa/ui/buttons';
|
||||
import { ClientRowImports, ItemRowDataImports } from '@isa/ui/item-rows';
|
||||
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||
import { injectRemissionListType } from '../injects/inject-remission-list-type';
|
||||
import { RemissionListItemSelectComponent } from './remission-list-item-select.component';
|
||||
import { RemissionListItemActionsComponent } from './remission-list-item-actions.component';
|
||||
import { LabelComponent, Labeltype } from '@isa/ui/label';
|
||||
|
||||
/**
|
||||
* Component representing a single item in the remission list.
|
||||
@@ -52,14 +54,20 @@ import { RemissionListItemActionsComponent } from './remission-list-item-actions
|
||||
ProductInfoComponent,
|
||||
ProductStockInfoComponent,
|
||||
ProductShelfMetaInfoComponent,
|
||||
TextButtonComponent,
|
||||
ClientRowImports,
|
||||
ItemRowDataImports,
|
||||
RemissionListItemSelectComponent,
|
||||
RemissionListItemActionsComponent,
|
||||
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.
|
||||
*/
|
||||
Labeltype = Labeltype;
|
||||
|
||||
/**
|
||||
* Store for managing selected remission quantities.
|
||||
* @private
|
||||
@@ -89,12 +97,18 @@ export class RemissionListItemComponent {
|
||||
stock = input.required<StockInfo>();
|
||||
|
||||
/**
|
||||
* ModelSignal indicating whether remission items are currently being processed.
|
||||
* Used to prevent multiple submissions or actions.
|
||||
* InputSignal indicating whether the stock information is currently being fetched.
|
||||
* Used to show loading states in the UI.
|
||||
* @default false
|
||||
*
|
||||
*/
|
||||
deleteRemissionListItemInProgress = model<boolean>();
|
||||
stockFetching = input<boolean>(false);
|
||||
|
||||
/**
|
||||
* Output event emitter for when the item is deleted or updated.
|
||||
* Emits an object containing the in-progress state and the item itself.
|
||||
*/
|
||||
removeOrUpdateItem = output<UpdateItem>();
|
||||
|
||||
/**
|
||||
* Optional product group value for display or filtering.
|
||||
@@ -118,9 +132,12 @@ export class RemissionListItemComponent {
|
||||
|
||||
/**
|
||||
* Computes whether the item has stock to remit.
|
||||
* Returns true if stockToRemit is greater than 0.
|
||||
* Returns true if stockToRemit and availableStock are greater than 0.
|
||||
* #5269 Added availableStock check
|
||||
*/
|
||||
hasStockToRemit = computed(() => this.stockToRemit() > 0);
|
||||
hasStockToRemit = computed(
|
||||
() => this.availableStock() > 0 && this.stockToRemit() > 0,
|
||||
);
|
||||
|
||||
/**
|
||||
* Computes the available stock for the item using stock and removedFromStock.
|
||||
@@ -141,6 +158,16 @@ export class RemissionListItemComponent {
|
||||
() => this.#store.selectedQuantity()?.[this.item().id!],
|
||||
);
|
||||
|
||||
/**
|
||||
* Computes whether the selected quantity equals the stock to remit.
|
||||
* This is used to determine if the remission quantity can be changed.
|
||||
*/
|
||||
selectedQuantityDiffersFromStockToRemit = computed(
|
||||
() =>
|
||||
this.selectedStockToRemit() !== undefined &&
|
||||
this.selectedStockToRemit() !== this.stockToRemit(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Computes the stock to remit based on the remission item and available stock.
|
||||
* Uses the getStockToRemit helper function.
|
||||
@@ -155,13 +182,54 @@ export class RemissionListItemComponent {
|
||||
|
||||
/**
|
||||
* Computes the target stock after remission.
|
||||
* @returns The calculated target stock.
|
||||
* Uses the calculateTargetStock helper function.
|
||||
* Takes into account the selected quantity and remaining quantity in stock.
|
||||
*/
|
||||
targetStock = computed(() =>
|
||||
calculateTargetStock({
|
||||
targetStock = computed(() => {
|
||||
if (this.selectedQuantityDiffersFromStockToRemit()) {
|
||||
return calculateTargetStock({
|
||||
availableStock: this.availableStock(),
|
||||
stockToRemit: this.selectedStockToRemit(),
|
||||
});
|
||||
}
|
||||
|
||||
return calculateTargetStock({
|
||||
availableStock: this.availableStock(),
|
||||
stockToRemit: this.stockToRemit(),
|
||||
remainingQuantityInStock: this.remainingQuantityInStock(),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Computes whether to display the impediment for the item.
|
||||
* Displays if the item is a descendant of an enabled impediment or if it has its own impediment.
|
||||
*/
|
||||
displayImpediment = computed(
|
||||
() =>
|
||||
(this.item() as ReturnItem)?.descendantOf?.enabled ||
|
||||
this.item()?.impediment,
|
||||
);
|
||||
|
||||
/**
|
||||
* Computes the impediment comment and attempts for display.
|
||||
* If no impediment comment is provided, defaults to 'Restmenge'.
|
||||
* Appends the number of attempts if available.
|
||||
*/
|
||||
impediment = computed(() => {
|
||||
const comment = this.item()?.impediment?.comment ?? 'Restmenge';
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
@for (kv of remissionListTypes; track kv.key) {
|
||||
<ui-dropdown-option
|
||||
[attr.data-what]="`remission-list-option-${kv.value}`"
|
||||
[disabled]="kv.value === RemissionListCategory.Koerperlos"
|
||||
[value]="kv.value"
|
||||
>{{ kv.value }}</ui-dropdown-option
|
||||
>
|
||||
|
||||
@@ -35,11 +35,7 @@ export class RemissionListSelectComponent {
|
||||
selectedRemissionListType = injectRemissionListType();
|
||||
|
||||
async changeRemissionType(remissionTypeValue: RemissionListType | undefined) {
|
||||
if (
|
||||
!remissionTypeValue ||
|
||||
remissionTypeValue === RemissionListType.Koerperlos
|
||||
)
|
||||
return;
|
||||
if (!remissionTypeValue) return;
|
||||
|
||||
await this.router.navigate(
|
||||
[remissionListTypeRouteMapping[remissionTypeValue]],
|
||||
@@ -57,7 +53,7 @@ export class RemissionListSelectComponent {
|
||||
}
|
||||
|
||||
if (type === RemissionListType.Abteilung) {
|
||||
return 'Abteilungen';
|
||||
return 'Abteilung';
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<remi-remission-processed-hint></remi-remission-processed-hint>
|
||||
<!-- TODO: #5136 - Code innerhalb remi-remission-processed-hint anpassen sobald Ticket #5215 umgesetzt ist -->
|
||||
<!-- <remi-remission-processed-hint></remi-remission-processed-hint> -->
|
||||
|
||||
@if (!remissionStarted()) {
|
||||
<remi-feature-remission-start-card></remi-feature-remission-start-card>
|
||||
@@ -22,17 +23,16 @@
|
||||
{{ 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
|
||||
#listElement
|
||||
[item]="item"
|
||||
[stock]="getStockForItem(item)"
|
||||
[stockFetching]="inStockFetching()"
|
||||
[productGroupValue]="getProductGroupValueForItem(item)"
|
||||
(deleteRemissionListItemInProgressChange)="
|
||||
onDeleteRemissionListItem($event)
|
||||
"
|
||||
(removeOrUpdateItem)="onRemoveOrUpdateItem($event)"
|
||||
></remi-feature-remission-list-item>
|
||||
} @placeholder {
|
||||
<div class="h-[7.75rem] w-full flex items-center justify-center">
|
||||
@@ -45,11 +45,23 @@
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<remi-feature-remission-list-empty-state
|
||||
[listFetching]="listFetching()"
|
||||
[isDepartment]="isDepartment()"
|
||||
[isReloadSearch]="searchTrigger() === 'reload'"
|
||||
[hasValidSearchTerm]="hasValidSearchTerm()"
|
||||
[hits]="hits()"
|
||||
></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"
|
||||
@@ -63,7 +75,7 @@
|
||||
size="large"
|
||||
color="brand"
|
||||
[pending]="remitItemsInProgress()"
|
||||
[disabled]="!hasSelectedItems() || deleteRemissionListItemInProgress()"
|
||||
[disabled]="!hasSelectedItems() || removeItemInProgress()"
|
||||
>
|
||||
</ui-stateful-button>
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user