Compare commits

...

12 Commits

Author SHA1 Message Date
Lorenz Hilpert
58b0fde3f8 Merge branch 'develop' into feature/4736-retoure 2024-10-18 14:43:29 +02:00
Lorenz Hilpert
22b0c57fb0 Styling Produktinfo in Retoure Details angepasst 2024-05-17 20:34:50 +02:00
Lorenz Hilpert
e59ddaf6a6 Anpassung Label und Felder in Retoure Details 2024-05-15 17:09:24 +02:00
Lorenz Hilpert
a74b4ed791 Details Page um mehrere Felder erweitert
Select Logik erweitert
Templates für Date felder angelegt
2024-05-14 19:05:29 +02:00
Lorenz Hilpert
3017f341af Mengenauswahl Input Styling angepasst 2024-05-14 11:54:48 +02:00
Lorenz Hilpert
c75a6d87f9 Improved Quantity Selector 2024-05-14 11:46:33 +02:00
Lorenz Hilpert
b85b69f078 Artikelmenge wurde nicht korrekt angezeigt 2024-05-13 18:11:35 +02:00
Lorenz Hilpert
93c050f09a Aktuallisierung des Breadcrumbs wenn sie der Suchparameter ändert 2024-05-13 17:28:29 +02:00
Lorenz Hilpert
23b2a05d47 Loader für die Details-Page
 Loader für die Items in der List-Page
 Breadcrumb in der List und Details Page eingebunden
 Nachladen von Items wenn Ende des ScrollContainers erreicht wurde
 Mengenauswahl eingebunden
2024-05-13 17:05:20 +02:00
Lorenz Hilpert
bd746f9915 Retoure Styling und data-table komponente 2024-05-10 18:56:52 +02:00
Lorenz Hilpert
91f70b0503 Removed race to catch fetch result 2024-05-03 15:39:45 +02:00
Lorenz Hilpert
3237dff1bc Suche und Anzeige von Retouren 2024-05-03 14:42:10 +02:00
915 changed files with 6893 additions and 4618 deletions

View File

@@ -97,6 +97,10 @@ const routes: Routes = [
canActivate: [ActivateProcessIdGuard],
loadChildren: () => import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
},
{
path: 'retoure',
loadChildren: () => import('@page/retoure').then((m) => m.RetourePageModule),
},
{ path: '**', redirectTo: 'dashboard', pathMatch: 'full' },
],
resolve: { section: CustomerSectionResolver },
@@ -153,7 +157,12 @@ if (isDevMode()) {
}
@NgModule({
imports: [RouterModule.forRoot(routes), TokenLoginModule],
imports: [
RouterModule.forRoot(routes, {
paramsInheritanceStrategy: 'always',
}),
TokenLoginModule,
],
exports: [RouterModule],
})
export class AppRoutingModule {}

View File

@@ -285,8 +285,27 @@
"name": "gift",
"data": "M2 21V10H0V4H5.2C5.11667 3.85 5.0625 3.69167 5.0375 3.525C5.0125 3.35833 5 3.18333 5 3C5 2.16667 5.29167 1.45833 5.875 0.875C6.45833 0.291667 7.16667 0 8 0C8.38333 0 8.74167 0.0708333 9.075 0.2125C9.40833 0.354167 9.71667 0.55 10 0.8C10.2833 0.533333 10.5917 0.333333 10.925 0.2C11.2583 0.0666667 11.6167 0 12 0C12.8333 0 13.5417 0.291667 14.125 0.875C14.7083 1.45833 15 2.16667 15 3C15 3.18333 14.9833 3.35417 14.95 3.5125C14.9167 3.67083 14.8667 3.83333 14.8 4H20V10H18V21H2ZM12 2C11.7167 2 11.4792 2.09583 11.2875 2.2875C11.0958 2.47917 11 2.71667 11 3C11 3.28333 11.0958 3.52083 11.2875 3.7125C11.4792 3.90417 11.7167 4 12 4C12.2833 4 12.5208 3.90417 12.7125 3.7125C12.9042 3.52083 13 3.28333 13 3C13 2.71667 12.9042 2.47917 12.7125 2.2875C12.5208 2.09583 12.2833 2 12 2ZM7 3C7 3.28333 7.09583 3.52083 7.2875 3.7125C7.47917 3.90417 7.71667 4 8 4C8.28333 4 8.52083 3.90417 8.7125 3.7125C8.90417 3.52083 9 3.28333 9 3C9 2.71667 8.90417 2.47917 8.7125 2.2875C8.52083 2.09583 8.28333 2 8 2C7.71667 2 7.47917 2.09583 7.2875 2.2875C7.09583 2.47917 7 2.71667 7 3ZM2 6V8H9V6H2ZM9 19V10H4V19H9ZM11 19H16V10H11V19ZM18 8V6H11V8H18Z",
"viewBox": "0 0 20 21"
},
{
"name": "undo",
"data": "M280-200v-80h284q63 0 109.5-40T720-420q0-60-46.5-100T564-560H312l104 104-56 56-200-200 200-200 56 56-104 104h252q97 0 166.5 63T800-420q0 94-69.5 157T564-200H280Z",
"viewBox": "0 -960 960 960"
},
{
"name": "delete",
"data": "M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z",
"viewBox": "0 -960 960 960"
},
{
"name": "save",
"data": "M840-680v480q0 33-23.5 56.5T760-120H200q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h480l160 160Zm-80 34L646-760H200v560h560v-446ZM480-240q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35ZM240-560h360v-160H240v160Zm-40-86v446-560 114Z",
"viewBox": "0 -960 960 960"
},
{
"name": "check",
"data": "M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z",
"viewBox": "0 -960 960 960"
}
],
"aliases": [
{

View File

@@ -81,6 +81,53 @@
"2": "Herr",
"4": "Frau"
},
"paymentStatus": {
"0": "-",
"1": "ProformaInvoicePrinted",
"2": "InvoicePrinted",
"4": "Bezahlt",
"8": "OnApproval",
"16": "Kostenlos",
"32": "Mahnung",
"64": "ProformaInvoicePaid",
"128": "Canceled",
"256": "Mahnung",
"512": "PartiallyPaid",
"1024": "Outstanding"
},
"paymentType": {
"0": "-",
"1": "WhenCollecting",
"2": "Kostenlos",
"4": "Bar",
"8": "DirectDebit",
"16": "DebitAdviceMandate",
"32": "DebitCard",
"64": "Kreditkarte",
"128": "Rechnung",
"256": "Vorkasse",
"512": "Voucher",
"1024": "CollectiveInvoice",
"2048": "PayPal",
"4096": "InstantTransfer",
"8192": "BonusCard",
"16384": "AmazonPay"
},
"receiptType": {
"0": "-",
"1": "Lieferschein",
"2": "Gutschrift",
"4": "Sammelgutschrift",
"8": "CollectiveCreditNote",
"16": "Sammellieferschein für Kundenkartenkäufe",
"32": "Sammelgutschrift für Kundenkartenkäufe",
"64": "Zahlungsbeleg",
"128": "Rechnung",
"256": "Sammelrechnung",
"512": "Proformarechnung / Vorauskasse",
"1024": "Bon/Quittung",
"2048": "Rücknahmebeleg"
},
"@shared/icon": "/assets/icons.json"
}

View File

@@ -79,6 +79,53 @@
"1": "Enby",
"2": "Herr",
"4": "Frau"
},
},
"paymentStatus": {
"0": "-",
"1": "ProformaInvoicePrinted",
"2": "InvoicePrinted",
"4": "Bezahlt",
"8": "OnApproval",
"16": "Kostenlos",
"32": "Mahnung",
"64": "ProformaInvoicePaid",
"128": "Canceled",
"256": "Mahnung",
"512": "PartiallyPaid",
"1024": "Outstanding"
},
"paymentType": {
"0": "-",
"1": "WhenCollecting",
"2": "Kostenlos",
"4": "Bar",
"8": "DirectDebit",
"16": "DebitAdviceMandate",
"32": "DebitCard",
"64": "Kreditkarte",
"128": "Rechnung",
"256": "Vorkasse",
"512": "Voucher",
"1024": "CollectiveInvoice",
"2048": "PayPal",
"4096": "InstantTransfer",
"8192": "BonusCard",
"16384": "AmazonPay"
},
"receiptType": {
"0": "-",
"1": "Lieferschein",
"2": "Gutschrift",
"4": "Sammelgutschrift",
"8": "CollectiveCreditNote",
"16": "Sammellieferschein für Kundenkartenkäufe",
"32": "Sammelgutschrift für Kundenkartenkäufe",
"64": "Zahlungsbeleg",
"128": "Rechnung",
"256": "Sammelrechnung",
"512": "Proformarechnung / Vorauskasse",
"1024": "Bon/Quittung",
"2048": "Rücknahmebeleg"
},
"@shared/icon": "/assets/icons.json"
}

View File

@@ -82,5 +82,52 @@
"2": "Herr",
"4": "Frau"
},
"paymentStatus": {
"0": "-",
"1": "ProformaInvoicePrinted",
"2": "InvoicePrinted",
"4": "Bezahlt",
"8": "OnApproval",
"16": "Kostenlos",
"32": "Mahnung",
"64": "ProformaInvoicePaid",
"128": "Canceled",
"256": "Mahnung",
"512": "PartiallyPaid",
"1024": "Outstanding"
},
"paymentType": {
"0": "-",
"1": "WhenCollecting",
"2": "Kostenlos",
"4": "Bar",
"8": "DirectDebit",
"16": "DebitAdviceMandate",
"32": "DebitCard",
"64": "Kreditkarte",
"128": "Rechnung",
"256": "Vorkasse",
"512": "Voucher",
"1024": "CollectiveInvoice",
"2048": "PayPal",
"4096": "InstantTransfer",
"8192": "BonusCard",
"16384": "AmazonPay"
},
"receiptType": {
"0": "-",
"1": "Lieferschein",
"2": "Gutschrift",
"4": "Sammelgutschrift",
"8": "CollectiveCreditNote",
"16": "Sammellieferschein für Kundenkartenkäufe",
"32": "Sammelgutschrift für Kundenkartenkäufe",
"64": "Zahlungsbeleg",
"128": "Rechnung",
"256": "Sammelrechnung",
"512": "Proformarechnung / Vorauskasse",
"1024": "Bon/Quittung",
"2048": "Rücknahmebeleg"
},
"@shared/icon": "/assets/icons.json"
}

View File

@@ -81,5 +81,52 @@
"2": "Herr",
"4": "Frau"
},
"paymentStatus": {
"0": "-",
"1": "ProformaInvoicePrinted",
"2": "InvoicePrinted",
"4": "Bezahlt",
"8": "OnApproval",
"16": "Kostenlos",
"32": "Mahnung",
"64": "ProformaInvoicePaid",
"128": "Canceled",
"256": "Mahnung",
"512": "PartiallyPaid",
"1024": "Outstanding"
},
"paymentType": {
"0": "-",
"1": "WhenCollecting",
"2": "Kostenlos",
"4": "Bar",
"8": "DirectDebit",
"16": "DebitAdviceMandate",
"32": "DebitCard",
"64": "Kreditkarte",
"128": "Rechnung",
"256": "Vorkasse",
"512": "Voucher",
"1024": "CollectiveInvoice",
"2048": "PayPal",
"4096": "InstantTransfer",
"8192": "BonusCard",
"16384": "AmazonPay"
},
"receiptType": {
"0": "-",
"1": "Lieferschein",
"2": "Gutschrift",
"4": "Sammelgutschrift",
"8": "CollectiveCreditNote",
"16": "Sammellieferschein für Kundenkartenkäufe",
"32": "Sammelgutschrift für Kundenkartenkäufe",
"64": "Zahlungsbeleg",
"128": "Rechnung",
"256": "Sammelrechnung",
"512": "Proformarechnung / Vorauskasse",
"1024": "Bon/Quittung",
"2048": "Rücknahmebeleg"
},
"@shared/icon": "/assets/icons.json"
}

View File

@@ -80,6 +80,53 @@
"1": "Enby",
"2": "Herr",
"4": "Frau"
},
},
"paymentStatus": {
"0": "-",
"1": "ProformaInvoicePrinted",
"2": "InvoicePrinted",
"4": "Bezahlt",
"8": "OnApproval",
"16": "Kostenlos",
"32": "Mahnung",
"64": "ProformaInvoicePaid",
"128": "Canceled",
"256": "Mahnung",
"512": "PartiallyPaid",
"1024": "Outstanding"
},
"paymentType": {
"0": "-",
"1": "WhenCollecting",
"2": "Kostenlos",
"4": "Bar",
"8": "DirectDebit",
"16": "DebitAdviceMandate",
"32": "DebitCard",
"64": "Kreditkarte",
"128": "Rechnung",
"256": "Vorkasse",
"512": "Voucher",
"1024": "CollectiveInvoice",
"2048": "PayPal",
"4096": "InstantTransfer",
"8192": "BonusCard",
"16384": "AmazonPay"
},
"receiptType": {
"0": "-",
"1": "Lieferschein",
"2": "Gutschrift",
"4": "Sammelgutschrift",
"8": "CollectiveCreditNote",
"16": "Sammellieferschein für Kundenkartenkäufe",
"32": "Sammelgutschrift für Kundenkartenkäufe",
"64": "Zahlungsbeleg",
"128": "Rechnung",
"256": "Sammelrechnung",
"512": "Proformarechnung / Vorauskasse",
"1024": "Bon/Quittung",
"2048": "Rücknahmebeleg"
},
"@shared/icon": "/assets/icons.json"
}

View File

@@ -81,6 +81,53 @@
"1": "Enby",
"2": "Herr",
"4": "Frau"
},
},
"paymentStatus": {
"0": "-",
"1": "ProformaInvoicePrinted",
"2": "InvoicePrinted",
"4": "Bezahlt",
"8": "OnApproval",
"16": "Kostenlos",
"32": "Mahnung",
"64": "ProformaInvoicePaid",
"128": "Canceled",
"256": "Mahnung",
"512": "PartiallyPaid",
"1024": "Outstanding"
},
"paymentType": {
"0": "-",
"1": "WhenCollecting",
"2": "Kostenlos",
"4": "Bar",
"8": "DirectDebit",
"16": "DebitAdviceMandate",
"32": "DebitCard",
"64": "Kreditkarte",
"128": "Rechnung",
"256": "Vorkasse",
"512": "Voucher",
"1024": "CollectiveInvoice",
"2048": "PayPal",
"4096": "InstantTransfer",
"8192": "BonusCard",
"16384": "AmazonPay"
},
"receiptType": {
"0": "-",
"1": "Lieferschein",
"2": "Gutschrift",
"4": "Sammelgutschrift",
"8": "CollectiveCreditNote",
"16": "Sammellieferschein für Kundenkartenkäufe",
"32": "Sammelgutschrift für Kundenkartenkäufe",
"64": "Zahlungsbeleg",
"128": "Rechnung",
"256": "Sammelrechnung",
"512": "Proformarechnung / Vorauskasse",
"1024": "Bon/Quittung",
"2048": "Rücknahmebeleg"
},
"@shared/icon": "/assets/icons.json"
}

View File

@@ -0,0 +1,12 @@
import { createAction, props } from '@ngrx/store';
import { NGRX_FEATURE_NAME } from './constants';
export const requestStarted = createAction(`${NGRX_FEATURE_NAME} Request Started`, props<{ id: string }>());
export const requestSucceeded = createAction(`${NGRX_FEATURE_NAME} Request Succeeded`, props<{ id: string; data: any }>());
export const requestFailed = createAction(`${NGRX_FEATURE_NAME} Request Failed`, props<{ id: string; error: any }>());
export const requestCanceled = createAction(`${NGRX_FEATURE_NAME} Request Canceled`, props<{ id: string }>());
export const removeRequests = createAction(`${NGRX_FEATURE_NAME} Remove Requests`, props<{ ids: string[] }>());

View File

@@ -0,0 +1 @@
export const NGRX_FEATURE_NAME = '@core/request-tracker';

View File

@@ -0,0 +1,9 @@
export * from './actions';
export * from './constants';
export * from './inject';
export * from './reducer';
export * from './request-status';
export * from './request-tracker.module';
export * from './request-tracker';
export * from './selectors';
export * from './state';

View File

@@ -0,0 +1,28 @@
import { Signal, inject, isSignal } from '@angular/core';
import { RequestTracker } from './request-tracker';
import { Observable, isObservable, of } from 'rxjs';
import { distinctUntilChanged, switchMap } from 'rxjs/operators';
import { toObservable } from '@angular/core/rxjs-interop';
export function injectRequestTracker() {
return inject(RequestTracker);
}
export function injectRequestStatus(ids: Array<string> | Signal<Array<string>> | Observable<Array<string>>) {
const requestTracker = injectRequestTracker();
let ids$: Observable<Array<string>>;
if (isObservable(ids)) {
ids$ = ids;
} else if (isSignal(ids)) {
ids$ = toObservable(ids);
} else {
ids$ = of(ids);
}
return ids$.pipe(
distinctUntilChanged(),
switchMap((ids) => requestTracker.status(ids)),
);
}

View File

@@ -0,0 +1,32 @@
import { createReducer, on } from '@ngrx/store';
import { removeRequests, requestCanceled, requestFailed, requestStarted, requestSucceeded } from './actions';
import { RequestTrackerState, initialState } from './state';
export const requestStatusTrackerReducer = createReducer<RequestTrackerState>(
initialState,
on(requestStarted, (state, { id }) => {
const next = structuredClone(state);
next[id] = { id, status: 'pending', timestamp: Date.now() };
return next;
}),
on(requestSucceeded, (state, { id, data }) => {
const next = structuredClone(state);
next[id] = { id, status: 'success', data, timestamp: Date.now() };
return next;
}),
on(requestFailed, (state, { id, error }) => {
const next = structuredClone(state);
next[id] = { id, status: 'error', error, timestamp: Date.now() };
return next;
}),
on(requestCanceled, (state, { id }) => {
const next = structuredClone(state);
next[id] = { id, status: 'canceled', timestamp: Date.now() };
return next;
}),
on(removeRequests, (state, { ids }) => {
const next = structuredClone(state);
ids.forEach((id) => delete next[id]);
return next;
}),
);

View File

@@ -0,0 +1,24 @@
export interface RequestStatusBase {
id: string;
timestamp: number;
}
export interface RequestStatusPending extends RequestStatusBase {
status: 'pending';
}
export interface RequestStatusSuccess<T> extends RequestStatusBase {
status: 'success';
data: T;
}
export interface RequestStatusError extends RequestStatusBase {
status: 'error';
error: any;
}
export interface RequestStatusCanceled extends RequestStatusBase {
status: 'canceled';
}
export type RequestStatus<T = any> = RequestStatusPending | RequestStatusSuccess<T> | RequestStatusError | RequestStatusCanceled;

View File

@@ -0,0 +1,15 @@
import { NgModule, ModuleWithProviders } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { NGRX_FEATURE_NAME } from './constants';
import { requestStatusTrackerReducer } from './reducer';
import { RequestTracker } from './request-tracker';
@NgModule({})
export class RequestTrackerModule {
static forRoot(): ModuleWithProviders<RequestTrackerModule> {
return {
ngModule: RequestTrackerModule,
providers: [StoreModule.forFeature(NGRX_FEATURE_NAME, requestStatusTrackerReducer).providers, RequestTracker],
};
}
}

View File

@@ -0,0 +1,62 @@
import { Injectable, inject } from '@angular/core';
import { MonoTypeOperatorFunction, Observable } from 'rxjs';
import { removeRequests, requestCanceled, requestFailed, requestStarted, requestSucceeded } from './actions';
import { Store } from '@ngrx/store';
import { selectRequestStatus, selectRequestsOlderThan } from './selectors';
import { duration } from 'moment';
import { RequestStatus } from './request-status';
function createId(ids: Array<string | number>) {
return ids.join('-');
}
@Injectable()
export class RequestTracker {
private store = inject(Store);
constructor() {
this.store.select(selectRequestsOlderThan(duration(1, 'minute'))).subscribe((ids) => {
if (ids.length > 0) this.store.dispatch(removeRequests({ ids }));
});
}
status = (ids: Array<string | number>) => this.store.select(selectRequestStatus(createId(ids)));
track =
<T>(ids: Array<string | number>): MonoTypeOperatorFunction<T> =>
(source: Observable<T>) => {
const id = createId(ids);
return new Observable<T>((observer) => {
this.store.dispatch(requestStarted({ id }));
let status: RequestStatus['status'] = 'pending';
const subscription = source.subscribe({
next: (res) => {
if (status === 'pending') {
status = 'success';
observer.next(res);
this.store.dispatch(requestSucceeded({ id, data: res }));
}
observer.complete();
},
error: (err) => {
if (status === 'pending') {
status = 'error';
observer.error(err);
this.store.dispatch(requestFailed({ id, error: err }));
}
},
});
return () => {
if (status === 'pending') {
this.store.dispatch(requestCanceled({ id }));
}
subscription.unsubscribe();
};
});
};
}

View File

@@ -0,0 +1,17 @@
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { RequestTrackerState } from './state';
import { NGRX_FEATURE_NAME } from './constants';
import { Duration } from 'moment';
export const selectTrackerStateFeature = createFeatureSelector<RequestTrackerState>(NGRX_FEATURE_NAME);
export const selectRequestStatus = (id: string) => createSelector(selectTrackerStateFeature, (s) => s[id]);
export const selectRequestsOlderThan = (duration: Duration) =>
createSelector(selectTrackerStateFeature, (s) => {
const now = Date.now();
return Object.keys(s).filter((key) => {
const timestamp = s[key].timestamp;
return timestamp < now - duration.asMilliseconds();
});
});

View File

@@ -0,0 +1,5 @@
import { RequestStatus } from './request-status';
export type RequestTrackerState = Record<string, RequestStatus>;
export const initialState: RequestTrackerState = {};

View File

@@ -8,7 +8,7 @@ export class DomainCatalogThumbnailService {
constructor(private domainCatalogService: DomainCatalogService) {}
@memorize()
getThumnaulUrl({ ean, height, width }: { width?: number; height?: number; ean?: string }) {
getThumnaulUrl({ ean }: { ean?: string }) {
return this.domainCatalogService.getSettings().pipe(
map((settings) => {
let thumbnailUrl = settings.imageUrl.replace(/{ean}/, ean);

View File

@@ -0,0 +1,3 @@
:host {
@apply grid grid-rows-[auto_1fr] gap-3 bg-white w-full h-full p-4 relative;
}

View File

@@ -0,0 +1,36 @@
<div class="grid grid-flow-row">
<div class="text-right -mr-2 -mt-2">
<button class="w-12 h-12 inline-grid justify-center items-center" type="button" (click)="overlayRef.detach()">
<shared-icon icon="close"></shared-icon>
</button>
</div>
<h1 class="text-2xl font-bold text-center">Filter</h1>
</div>
<div class="relative">
<shared-filter
*ngIf="filter(); let filter"
[filter]="filter"
[loading]="fetching()"
[hint]="hint()"
(search)="applyFilter(filter)"
[scanner]="true"
></shared-filter>
<div class="grid grid-flow-col gap-6 items-center justify-center absolute left-0 right-0 bottom-6">
<button
type="button"
(click)="resetFilter()"
class="bg-white border-2 border-solid border-brand text-brand text-xl font-bold rounded-full px-6 py-3 disabled:border-components-button-disabled-content disabled:text-components-button-disabled-content"
>
<shared-loader [loading]="fetching()"> Filter zurücksetzen </shared-loader>
</button>
<button
type="button"
(click)="applyFilter(filter())"
class="bg-brand border-2 border-solid border-brand text-white text-xl font-bold rounded-full px-6 py-3 disabled:bg-components-button-disabled-content disabled:border-components-button-disabled-content"
>
<shared-loader [loading]="fetching()"> Filter anwenden </shared-loader>
</button>
</div>
</div>

View File

@@ -0,0 +1,71 @@
import { Component, ChangeDetectionStrategy, signal, inject, DestroyRef, OnInit } from '@angular/core';
import { Filter, FilterModule } from '@shared/components/filter';
import { injectSearchFilter } from '../../store/retoure.selectors.inject';
import { injectProcessId } from '../../inject-process-id';
import { AsyncPipe, NgIf } from '@angular/common';
import { LoaderComponent } from '@shared/components/loader';
import { OverlayRef } from '@angular/cdk/overlay';
import { IconComponent } from '@shared/components/icon';
import { DESTROYED } from './injection-token';
import { injectSetSearchQueryParams } from '../../store/retoure.actions.inject';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { injectApplyFilter } from '../../inject-apply-filter';
@Component({
selector: 'page-filter-container',
templateUrl: 'filter-container.component.html',
styleUrls: ['filter-container.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [FilterModule, AsyncPipe, NgIf, LoaderComponent, IconComponent],
})
export class FilterContainerComponent implements OnInit {
activatedRoute = inject(ActivatedRoute);
destroyRef = inject(DestroyRef);
destroyed = inject(DESTROYED);
processId$ = injectProcessId();
processId = toSignal(injectProcessId());
filter$ = injectSearchFilter(this.processId$);
filter = toSignal(this.filter$);
fetching = signal(false);
hint = signal<string>(null);
overlayRef = inject(OverlayRef);
setQueryParams = injectSetSearchQueryParams();
applyFilterFn = injectApplyFilter();
async applyFilter(filter: Filter) {
this.fetching.set(true);
this.hint.set(null);
const message = await this.applyFilterFn(filter);
if (message) {
this.hint.set(message);
}
this.fetching.set(false);
}
ngOnInit(): void {
this.destroyRef.onDestroy(() => {
this.destroyed.next();
this.destroyed.complete();
});
}
resetFilter() {
this.setQueryParams(this.processId(), {});
}
}

View File

@@ -0,0 +1,53 @@
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { Injector, inject } from '@angular/core';
import { FilterContainerComponent } from './filter-container.component';
import { ComponentPortal } from '@angular/cdk/portal';
import { Router } from '@angular/router';
import { Subject } from 'rxjs';
import { DESTROYED } from './injection-token';
import { takeUntil } from 'rxjs/operators';
export function injectOpenFilter() {
const overlay = inject(Overlay);
const injector = inject(Injector);
const router = inject(Router);
return (origin: Element) => {
const overlayRef = overlay.create({
width: origin.clientWidth,
height: origin.clientHeight,
maxHeight: origin.clientHeight,
maxWidth: origin.clientWidth,
positionStrategy: overlay
.position()
.flexibleConnectedTo(origin)
.withPositions([
{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'top',
},
]),
});
const destroyed = new Subject<void>();
const filterPortal = new ComponentPortal(
FilterContainerComponent,
undefined,
Injector.create({
providers: [
{ provide: OverlayRef, useValue: overlayRef },
{ provide: DESTROYED, useValue: destroyed },
],
parent: injector,
}),
);
overlayRef.attach(filterPortal);
router.events.pipe(takeUntil(destroyed)).subscribe((event) => {
overlayRef.detach();
});
};
}

View File

@@ -0,0 +1,4 @@
import { InjectionToken } from '@angular/core';
import { Subject } from 'rxjs';
export const DESTROYED = new InjectionToken<Subject<void>>('RETOURE_FILTER_COMPONENT_DESTROYED');

View File

@@ -0,0 +1,59 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationService } from '@core/application';
import { first } from 'rxjs/operators';
import { RetoureBreadcrumbService } from '../retoure-breadcrumb.service';
export const canActivateRetoure: CanActivateFn = async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const app = inject(ApplicationService);
const router = inject(Router);
const retoureBreadcrumbService = inject(RetoureBreadcrumbService);
let processId = +route.params.processId;
if (processId) {
const process = await app.getProcessById$(processId).pipe(first()).toPromise();
if (process) {
await app.activateProcess(processId);
return true;
}
processId = undefined;
}
const processes = await app.getProcesses$('customer').pipe(first()).toPromise();
const lastActivatedProcess = processes.reduce((acc, process) => {
if (acc === undefined) {
return process;
}
if (acc.activated < process.activated) {
return process;
}
}, undefined);
if (lastActivatedProcess) {
processId = lastActivatedProcess.id;
}
if (!lastActivatedProcess) {
processId = Date.now();
await app.createProcess({
id: processId,
name: `Vorgang ${processes.length + 1}`,
section: 'customer',
type: 'cart',
});
}
const latestBreadcrumb = await retoureBreadcrumbService.latestBreadcrumbForProcess(processId);
if (latestBreadcrumb?.path) {
await router.navigate([latestBreadcrumb.path]);
return true;
}
await router.navigate(['/kunde', 'retoure', processId]);
return true;
};

View File

@@ -0,0 +1,14 @@
import { CanDeactivateFn } from '@angular/router';
import { ListPageComponent } from '../pages/list-page/list-page.component';
export const canDeactivateRetoureList: CanDeactivateFn<ListPageComponent> = (component: ListPageComponent) => {
const scrollPosition = component.scrollContainer.scrollPosition;
const { main_qs, filter_receipt_type, filter_receipt_date } = component.activatedRoute.snapshot.queryParams;
const processId = component.processId();
localStorage.setItem(
`retoure-list-scroll-${processId}-${[processId, main_qs, filter_receipt_type, filter_receipt_date].join('&')}`,
JSON.stringify(scrollPosition),
);
return true;
};

View File

@@ -0,0 +1,7 @@
import { CanDeactivateFn } from '@angular/router';
import { RetoureRootPageComponent } from '../retoure-root-page.component';
export const canDeactivateRetoure: CanDeactivateFn<RetoureRootPageComponent> = async (component: RetoureRootPageComponent) => {
// TODO: Update Breadcrumb before deactivating
return true;
};

View File

@@ -0,0 +1,12 @@
import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular/router';
export const retoureListScrollPositionResolver: ResolveFn<number> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const processId = route.params.processId;
const { main_qs, filter_receipt_type, filter_receipt_date } = route.queryParams;
const scrollPosition = localStorage.getItem(
`retoure-list-scroll-${processId}-${[processId, main_qs, filter_receipt_type, filter_receipt_date].join('&')}`,
);
localStorage.removeItem(`retoure-list-scroll-${processId}-${[processId, main_qs, filter_receipt_type, filter_receipt_date].join('&')}`);
return scrollPosition ? JSON.parse(scrollPosition) : 0;
};

View File

@@ -0,0 +1,7 @@
export * from './inject-apply-filter';
export * from './inject-process-id';
export * from './inject-query-params';
export * from './retoure-breadcrumb.service';
export * from './retoure-page.module';
export * from './retoure-root-page.component';
export * from './router.config';

View File

@@ -0,0 +1,42 @@
import { ActivatedRoute, Router } from '@angular/router';
import { injectFetchRetoure, injectFetchRetoureResult, injectSetSearchQueryParams } from './store/retoure.actions.inject';
import { DestroyRef, inject } from '@angular/core';
import { Filter } from '@shared/components/filter';
import { injectProcessId } from './inject-process-id';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
export function injectApplyFilter() {
const router = inject(Router);
const activatedRoute = inject(ActivatedRoute);
const setQueryParams = injectSetSearchQueryParams();
const fetchRetoure = injectFetchRetoure();
const processId = toSignal(injectProcessId());
const fetchRetoureResult = injectFetchRetoureResult();
const destroyRef = inject(DestroyRef);
return async (filter: Filter): Promise<string | null> => {
const queryParams = filter.getQueryParams();
setQueryParams(processId(), queryParams);
fetchRetoure(processId());
try {
const result = await fetchRetoureResult(processId()).pipe(takeUntilDestroyed(destroyRef)).toPromise();
if (result.type === '[Retoure] Fetch Retoure Success') {
if (result.items.length === 1) {
await router.navigate(['/kunde', 'retoure', processId(), 'list', result.items[0].id], { queryParams: queryParams });
return null;
} else if (result.items.length > 1) {
await router.navigate(['/kunde', 'retoure', processId(), 'list'], { relativeTo: activatedRoute, queryParams: queryParams });
return null;
} else {
return 'Keine Ergebnisse gefunden';
}
} else {
return 'Error fetching retoure';
}
} catch (error) {
return 'Error fetching retoure';
}
};
}

View File

@@ -0,0 +1,11 @@
import { inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { filter, map } from 'rxjs/operators';
export function injectProcessId() {
const activatedRoute = inject(ActivatedRoute);
return activatedRoute.params.pipe(
map((params) => +params.processId),
filter((processId) => !isNaN(processId)),
);
}

View File

@@ -0,0 +1,7 @@
import { inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
export function injectQueryParams() {
const activatedRoute = inject(ActivatedRoute);
return activatedRoute.queryParams;
}

View File

@@ -0,0 +1,49 @@
import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { ReceiptItemDTO } from '@swagger/oms';
export interface DetailsPageComponentState {
items: ReceiptItemDTO[];
selectedItems: Array<ReceiptItemDTO['id']>;
selectedQuantity: Record<ReceiptItemDTO['id'], ReceiptItemDTO['quantity']['quantity']>;
}
@Injectable()
export class DetailsPageComponentStore extends ComponentStore<DetailsPageComponentState> {
items$ = this.select((state) => state.items);
selectedItems$ = this.select((state) => state.selectedItems);
constructor() {
super({
items: [],
selectedItems: [],
selectedQuantity: {},
});
}
selectedQuantity$ = (id: ReceiptItemDTO['id']) =>
this.select((state) => state.selectedQuantity[id] ?? state.items.find((item) => item.id === id)?.quantity ?? 0);
selectable$ = (id: ReceiptItemDTO['id']) => this.select((state) => state.selectedItems.includes(id));
selected$ = (id: ReceiptItemDTO['id']) => this.select((state) => state.selectedItems.includes(id));
quantity$ = (id: ReceiptItemDTO['id']) => this.select((state) => state.selectedQuantity[id]);
setItems = this.updater((state, items: ReceiptItemDTO[]) => {
return { ...state, items, selectedItems: [], selectedQuantity: {} };
});
selectItem = this.updater((state, id: ReceiptItemDTO['id']) => {
return { ...state, selectedItems: [...state.selectedItems, id] };
});
deselectItem = this.updater((state, id: ReceiptItemDTO['id']) => {
return { ...state, selectedItems: state.selectedItems.filter((selectedId) => selectedId !== id) };
});
setQuantity = this.updater((state, { id, quantity }: { id: ReceiptItemDTO['id']; quantity: ReceiptItemDTO['quantity']['quantity'] }) => {
return { ...state, selectedQuantity: { ...state.selectedQuantity, [id]: quantity } };
});
}

View File

@@ -0,0 +1,3 @@
:host {
@apply block;
}

View File

@@ -0,0 +1,21 @@
<div *ngIf="receipt$ | async; let receipt; else: loadingTpl" class="grid grid-flow-row gap-[0.125rem]">
<page-receipt-details class="rounded-t" [receipt]="receipt"> </page-receipt-details>
<div *ngIf="items()?.length > 1" class="bg-white px-4 py-2 text-right">
<button type="button" class="text-components-menu-hover-border" (click)="selectAll()">Alle auswählen</button>
</div>
<div class="grid grid-flow-row gap-[0.125rem]">
<page-receipt-item-details
class="last:rounded-b overflow-hidden"
*ngFor="let item of items$ | async; trackBy: trackById"
[item]="item"
[quantity]="quantity$(item.id) | async"
(quantityChange)="quantityChange(item.id, $event)"
[selected]="selected$(item.id) | async"
(selectedChange)="selectedChange(item.id, $event)"
[hideSelectBullet]="items()?.length === 1"
></page-receipt-item-details>
</div>
</div>
<ng-template #loadingTpl>
<page-receipt-details-loader></page-receipt-details-loader>
</ng-template>

View File

@@ -0,0 +1,114 @@
import {
Component,
ChangeDetectionStrategy,
inject,
computed,
TrackByFunction,
effect,
untracked,
OnInit,
DestroyRef,
} from '@angular/core';
import { injectFetchReceipt } from '../../store/retoure.actions.inject';
import { ActivatedRoute } from '@angular/router';
import { map, tap } from 'rxjs/operators';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { injectSelectReceipt } from '../../store/retoure.selectors.inject';
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { ReceiptDetailsComponent } from '../../shared/receipt-details';
import { ReceiptDetailsItemComponent } from '../../shared/receipt-details-item';
import { provideComponentStore } from '@ngrx/component-store';
import { DetailsPageComponentStore } from './details-page.component-store';
import { injectProcessId } from '../../inject-process-id';
import { combineLatest } from 'rxjs';
import { RetoureBreadcrumbService } from '../../retoure-breadcrumb.service';
import { ReceiptDetailsLoaderComponent } from '../../shared/receipt-details/receipt-details-loader.component';
@Component({
selector: 'page-details-page',
templateUrl: 'details-page.component.html',
styleUrls: ['details-page.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ReceiptDetailsLoaderComponent, NgIf, NgFor, AsyncPipe, ReceiptDetailsComponent, ReceiptDetailsItemComponent],
providers: [provideComponentStore(DetailsPageComponentStore)],
})
export class DetailsPageComponent implements OnInit {
destroyRef = inject(DestroyRef);
snapshot = inject(ActivatedRoute).snapshot;
retoureBreadcrumbService = inject(RetoureBreadcrumbService);
store = inject(DetailsPageComponentStore);
processId$ = injectProcessId();
processId = toSignal(this.processId$);
activatedRoute = inject(ActivatedRoute);
receiptId$ = this.activatedRoute.params.pipe(map((params) => +params.receiptId));
receiptId = toSignal(this.receiptId$);
fetchReceipt = injectFetchReceipt();
receipt$ = injectSelectReceipt(this.receiptId$);
items$ = this.receipt$.pipe(map((receipt) => receipt?.items?.map((i) => i.data) || []));
items = toSignal(this.items$);
item$ = (id: number) => this.items$.pipe(map((items) => items.find((i) => i.id === id)));
processIdAndReceiptId$ = combineLatest([this.processId$, this.receiptId$]).pipe(
map(([processId, receiptId]) => ({ processId, receiptId })),
);
receipt = toSignal(this.receipt$);
fetching = computed(() => !this.receipt());
trackById: TrackByFunction<{ id: number }> = (_, item) => item?.id;
quantity$ = (id: number) => this.store.quantity$(id);
selected$ = (id: number) => this.store.selected$(id);
ngOnInit() {
this.retoureBreadcrumbService.createOrUpdateDetailBreadcrumb(this.snapshot);
this.items$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((items) => {
if (items?.length === 1) {
this.store.selectItem(items[0].id);
}
});
}
selectedChange(id: number, selected: boolean) {
if (selected) {
this.store.selectItem(id);
} else {
this.store.deselectItem(id);
}
}
selectAll() {
this.items().forEach((item) => this.store.selectItem(item.id));
}
quantityChange(id: number, quantity: number) {
this.store.setQuantity({ id, quantity });
}
onReceipt = effect(() => {
const receipt = this.receipt();
if (!receipt) {
untracked(() => {
this.fetchReceipt(this.receiptId());
});
}
});
}

View File

@@ -0,0 +1,3 @@
:host {
@apply block max-h-full overflow-hidden h-full;
}

View File

@@ -0,0 +1,36 @@
<div class="grid grid-rows-[auto_1fr] max-h-full">
<div class="bg-[#F5F7FA] rounded-t">
<div class="grid grid-cols-[1fr_auto] gap-2">
<shared-filter-input-group-main
[inputGroup]="inputMain()"
(search)="applyFilter(filter())"
[scanner]="true"
[loading]="fetchingRetoure()"
[hint]="resultHint()"
></shared-filter-input-group-main>
<button
class="bg-[#AEB7C1] h-14 px-4 rounded font-bold text-xl grid grid-flow-col items-center gap-2"
[class.active]="hasFilter()"
(click)="openFilter(elementRef.nativeElement)"
>
<shared-icon icon="filter-variant"></shared-icon>
<span> Filter </span>
</button>
</div>
<div class="flex flex-row justify-end">
<div class="p-4">{{ hits$ | async }} Treffer</div>
</div>
</div>
<div class="overflow-auto scroll-bar max-h-[calc(100vh_-_20.7rem)]" sharedScrollContainer delta="250">
<a
*ngFor="let item of items$ | async; trackBy: trackItemById"
[routerLink]="[item?.id]"
[queryParams]="{ receiptNumber: item.receiptNumber }"
[queryParamsHandling]="'merge'"
>
<page-retoure-list-item class="mb-2" [item]="item" [attr.data-receipt-id]="item?.id"></page-retoure-list-item>
</a>
<page-retoure-list-item-loader *ngIf="fetchingRetoure()"></page-retoure-list-item-loader>
</div>
</div>

View File

@@ -0,0 +1,131 @@
import {
Component,
ChangeDetectionStrategy,
computed,
signal,
inject,
TrackByFunction,
ElementRef,
ViewChild,
effect,
OnInit,
DestroyRef,
} from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { Filter, FilterInputGroupMainModule } from '@shared/components/filter';
import { injectSearchFilter, injectSearchHits, injectSearchResult } from '../../store/retoure.selectors.inject';
import { injectProcessId } from '../../inject-process-id';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { RetoureListItemComponent } from '../../shared/retoure-list-item/retoure-list-item.component';
import { map } from 'rxjs/operators';
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { injectOpenFilter } from '../../containers/filter-container/inject-open-filter';
import { IconComponent } from '@shared/components/icon';
import { ScrollContainerDirective } from '@shared/directives/scroll-container';
import { injectApplyFilter } from '../../inject-apply-filter';
import { RetoureBreadcrumbService } from '../../retoure-breadcrumb.service';
import { injectFetchRetoure, injectFetchRetoureResult } from '../../store/retoure.actions.inject';
import { RetoureListItemLoaderComponent } from '../../shared/retoure-list-item/retoure-list-item-loader.component';
@Component({
selector: 'page-list-page',
templateUrl: 'list-page.component.html',
styleUrls: ['list-page.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
FilterInputGroupMainModule,
RetoureListItemComponent,
AsyncPipe,
IconComponent,
RouterLink,
NgFor,
ScrollContainerDirective,
RetoureListItemLoaderComponent,
NgIf,
],
})
export class ListPageComponent implements OnInit {
destroyRef = inject(DestroyRef);
retoureBreadcrumbService = inject(RetoureBreadcrumbService);
@ViewChild(ScrollContainerDirective, { static: true }) scrollContainer: ScrollContainerDirective;
activatedRoute = inject(ActivatedRoute);
scrollPosition = toSignal(this.activatedRoute.data.pipe(map((data) => data.scrollPosition)));
processId$ = injectProcessId();
processId = toSignal(injectProcessId());
filter = toSignal(injectSearchFilter(this.processId$));
inputMain = computed(() => this.filter().input?.find((input) => input.group === 'main'));
fetchingRetoure = signal(false);
resultHint = signal<string>(null);
items$ = injectSearchResult(this.processId$);
items = toSignal(this.items$);
hits$ = injectSearchHits(this.processId$);
hits = toSignal(this.hits$);
elementRef = inject(ElementRef);
openFilter = injectOpenFilter();
hasFilter = computed(() => Object.values(this.filter().getQueryParams()).some((value) => value));
fetchRetoureFn = injectFetchRetoure();
fetchRetoureResult = injectFetchRetoureResult();
onScroppPosition = effect(() => {
const position = this.scrollPosition();
setTimeout(() => this.scrollContainer.scrollTo(position));
});
applyFilterFn = injectApplyFilter();
ngOnInit(): void {
this.retoureBreadcrumbService.createOrUpdateListBreadcrumb(this.activatedRoute.snapshot);
this.retoureBreadcrumbService.removeDetailBreadcrumb(this.activatedRoute.snapshot);
this.scrollContainer.scrolledToBottom.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
if (this.fetchingRetoure()) return;
if (this.items().length >= this.hits()) return;
this.fetchingRetoure.set(true);
this.fetchRetoureFn(this.processId());
this.fetchRetoureResult(this.processId())
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((result) => {
this.fetchingRetoure.set(false);
});
});
}
async applyFilter(filter: Filter) {
this.fetchingRetoure.set(true);
this.resultHint.set(null);
const message = await this.applyFilterFn(filter);
this.retoureBreadcrumbService.createOrUpdateListBreadcrumb(this.activatedRoute.snapshot);
if (message) {
this.resultHint.set(message);
}
this.fetchingRetoure.set(false);
}
trackItemById: TrackByFunction<{ id: number }> = (index, item) => item.id;
}

View File

@@ -0,0 +1,4 @@
:host {
@apply block bg-white rounded px-4 py-6 text-center;
height: calc(100% - 1rem);
}

View File

@@ -0,0 +1,24 @@
<div class="grid grid-flow-row gap-4">
<h1 class="text-[1.625rem] font-bold">Rückgabe</h1>
<p class="text-lg">
Scannen Sie den Beleg des Kunden oder suchen Sie nach Kundenname, <br />
E-Mail-Adresse, Kundenkartennummer oder Rechnungsnummer.
</p>
<div class="grid grid-cols-[1fr_auto] gap-2">
<shared-filter-input-group-main
[inputGroup]="inputMain()"
(search)="applyFilter(filter())"
[scanner]="true"
[loading]="fetchingRetoure()"
[hint]="resultHint()"
></shared-filter-input-group-main>
<button
class="bg-[#AEB7C1] h-14 px-4 rounded font-bold text-xl grid grid-flow-col items-center gap-2"
[class.active]="hasFilter()"
(click)="openFilter(elementRef.nativeElement)"
>
<shared-icon icon="filter-variant"></shared-icon>
<span> Filter </span>
</button>
</div>
</div>

View File

@@ -0,0 +1,66 @@
import { Component, ChangeDetectionStrategy, computed, inject, signal, ElementRef, OnInit } from '@angular/core';
import { Filter, FilterInputGroupMainModule } from '@shared/components/filter';
import { IconComponent } from '@shared/components/icon';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { injectProcessId } from '../../inject-process-id';
import { AsyncPipe } from '@angular/common';
import { injectSearchFilter } from '../../store/retoure.selectors.inject';
import { toSignal } from '@angular/core/rxjs-interop';
import { injectFetchRetoure, injectFetchRetoureResult, injectSetSearchQueryParams } from '../../store/retoure.actions.inject';
import { injectOpenFilter } from '../../containers/filter-container/inject-open-filter';
import { injectApplyFilter } from '../../inject-apply-filter';
import { RetoureBreadcrumbService } from '../../retoure-breadcrumb.service';
@Component({
selector: 'main-page',
templateUrl: 'main-page.component.html',
styleUrls: ['main-page.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [FilterInputGroupMainModule, IconComponent, RouterLink, AsyncPipe],
})
export class MainPageComponent implements OnInit {
snapshot = inject(ActivatedRoute).snapshot;
retoureBreadcrumbService = inject(RetoureBreadcrumbService);
router = inject(Router);
processId$ = injectProcessId();
processId = toSignal(this.processId$);
filter = toSignal(injectSearchFilter(this.processId$));
inputMain = computed(() => this.filter().input?.find((input) => input.group === 'main'));
fetchingRetoure = signal(false);
resultHint = signal<string>(null);
hasFilter = computed(() => Object.values(this.filter().getQueryParams()).some((value) => value));
elementRef = inject(ElementRef);
openFilter = injectOpenFilter();
applyFilterFn = injectApplyFilter();
ngOnInit(): void {
this.retoureBreadcrumbService.createOrUpdateMainBreadcrumb(this.snapshot);
this.retoureBreadcrumbService.removeListBreadcrumb(this.snapshot);
}
async applyFilter(filter: Filter) {
this.fetchingRetoure.set(true);
this.resultHint.set(null);
const message = await this.applyFilterFn(filter);
if (message) {
this.resultHint.set(message);
}
this.fetchingRetoure.set(false);
}
}

View File

@@ -0,0 +1,167 @@
import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { isEqual } from 'lodash';
import { first, take } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class RetoureBreadcrumbService {
breadcrumbService = inject(BreadcrumbService);
processId(snapshot: ActivatedRouteSnapshot) {
return +snapshot.params.processId;
}
queryParams(snapshot: ActivatedRouteSnapshot) {
return snapshot.queryParams;
}
async latestBreadcrumbForProcess(processId: number) {
const crumbs = await this.breadcrumbService.getBreadcrumbsByKeyAndTag$(processId, 'retoure').pipe(take(1)).toPromise();
return crumbs.reduce((acc, crumb) => {
if (acc === undefined) {
return crumb;
}
if (acc.changed < crumb.changed) {
return crumb;
}
}, undefined);
}
async createOrUpdateMainBreadcrumb(snapshot: ActivatedRouteSnapshot) {
const processId = this.processId(snapshot);
const queryParams = this.queryParams(snapshot);
if (!processId) {
return;
}
const crumbs = await this.breadcrumbService
.getBreadcrumbsByKeyAndTags$(processId, ['retoure', 'root', String(processId)])
.pipe(first())
.toPromise();
if (crumbs.length === 0) {
await this.breadcrumbService.addBreadcrumb({
key: processId,
tags: ['retoure', 'root', String(processId)],
name: 'Rückgabe',
path: '/kunde/retoure/' + processId,
params: queryParams,
section: 'customer',
});
} else {
if (!isEqual(crumbs[0].params, queryParams)) {
await this.breadcrumbService.patchBreadcrumb(crumbs[0].id, {
params: queryParams,
});
}
}
}
async createOrUpdateListBreadcrumb(snapshot: ActivatedRouteSnapshot) {
const processId = this.processId(snapshot);
const queryParams = this.queryParams(snapshot);
if (!processId) {
return;
}
await this.createOrUpdateMainBreadcrumb(snapshot);
const crumbs = await this.breadcrumbService
.getBreadcrumbsByKeyAndTags$(processId, ['retoure', 'list', String(processId)])
.pipe(first())
.toPromise();
if (crumbs.length === 0) {
await this.breadcrumbService.addBreadcrumb({
key: processId,
tags: ['retoure', 'list', String(processId)],
name: queryParams['main_qs'] ?? 'Alle',
path: '/kunde/retoure/' + processId + '/list',
params: queryParams,
section: 'customer',
});
} else {
if (!isEqual(crumbs[0].params, queryParams) || crumbs[0].name !== queryParams['main_qs']) {
await this.breadcrumbService.patchBreadcrumb(crumbs[0].id, {
name: queryParams['main_qs'] ?? 'Alle',
params: queryParams,
});
}
}
}
async removeListBreadcrumb(snapshot: ActivatedRouteSnapshot) {
const processId = this.processId(snapshot);
if (!processId) {
return;
}
await this.removeDetailBreadcrumb(snapshot);
const crumbs = await this.breadcrumbService
.getBreadcrumbsByKeyAndTags$(processId, ['retoure', 'list', String(processId)])
.pipe(first())
.toPromise();
if (crumbs.length > 0) {
await this.breadcrumbService.removeBreadcrumb(crumbs[0].id);
}
}
async createOrUpdateDetailBreadcrumb(snapshot: ActivatedRouteSnapshot) {
const processId = this.processId(snapshot);
const queryParams = this.queryParams(snapshot);
if (!processId) {
return;
}
await this.createOrUpdateListBreadcrumb(snapshot);
const crumbs = await this.breadcrumbService
.getBreadcrumbsByKeyAndTags$(processId, ['retoure', 'details', String(processId)])
.pipe(first())
.toPromise();
if (crumbs.length === 0) {
await this.breadcrumbService.addBreadcrumb({
key: processId,
tags: ['retoure', 'details', String(processId)],
name: queryParams?.receiptNumber || 'Details',
path: '/kunde/retoure/' + processId + '/list',
params: queryParams,
section: 'customer',
});
} else {
if (!isEqual(crumbs[0].params, queryParams) || crumbs[0].name !== queryParams?.receiptNumber) {
await this.breadcrumbService.patchBreadcrumb(crumbs[0].id, {
name: queryParams?.receiptNumber || 'Details',
params: queryParams,
});
}
}
}
async removeDetailBreadcrumb(snapshot: ActivatedRouteSnapshot) {
const processId = this.processId(snapshot);
if (!processId) {
return;
}
const crumbs = await this.breadcrumbService
.getBreadcrumbsByKeyAndTags$(processId, ['retoure', 'details', String(processId)])
.pipe(first())
.toPromise();
if (crumbs.length > 0) {
await this.breadcrumbService.removeBreadcrumb(crumbs[0].id);
}
}
}

View File

@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { retoureRouterConfig } from './router.config';
import { StoreModule } from '@ngrx/store';
import { RETOURE_STORE_FEATURE_KEY } from './store/constants';
import { retoureReducer } from './store/retoure.reducer';
import { EffectsModule } from '@ngrx/effects';
import { RetoureEffects } from './store/retoure.effects';
@NgModule({
imports: [
StoreModule.forFeature(RETOURE_STORE_FEATURE_KEY, retoureReducer),
EffectsModule.forFeature([RetoureEffects]),
RouterModule.forChild(retoureRouterConfig),
],
})
export class RetourePageModule {}

View File

@@ -0,0 +1,5 @@
:host {
@apply grid grid-rows-[auto,1fr] h-screen;
max-height: calc(100vh - 8.3125rem);
}

View File

@@ -0,0 +1,4 @@
<shared-breadcrumb [key]="processId$ | async" [tags]="['retoure']"></shared-breadcrumb>
<div class="overflow-scroll" #filterOrigin>
<router-outlet></router-outlet>
</div>

View File

@@ -0,0 +1,78 @@
import { AsyncPipe } from '@angular/common';
import { Component, ChangeDetectionStrategy, inject, OnInit, DestroyRef, ViewChild, ElementRef } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { BreadcrumbModule } from '@shared/components/breadcrumb';
import { injectProcessId } from './inject-process-id';
import { injectQueryParams } from './inject-query-params';
@Component({
selector: 'page-retoure-root-page',
templateUrl: 'retoure-root-page.component.html',
styleUrls: ['retoure-root-page.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [RouterOutlet, BreadcrumbModule, AsyncPipe],
})
export class RetoureRootPageComponent implements OnInit {
destroyRef = inject(DestroyRef);
processId$ = injectProcessId();
queryParams$ = injectQueryParams();
@ViewChild('filterOrigin')
filterOrigin: ElementRef;
ngOnInit(): void {
// combineLatest([this.processId$, this.queryParams$])
// .pipe(takeUntilDestroyed(this.destroyRef))
// .subscribe(([processId, queryParams]) => {
// this.createOrUpdateMainBreadcrumb(processId, queryParams);
// // this.setQueryParams(processId, queryParams);
// });
}
// async createOrUpdateMainBreadcrumb(processId: number, queryParams: Record<string, string>) {
// if (!processId) {
// return;
// }
// const crumbs = await this.breadcrumbService.getBreadcrumbsByKeyAndTags$(processId, ['retoure', 'root']).pipe(first()).toPromise();
// if (crumbs.length === 0) {
// await this.breadcrumbService.addBreadcrumb({
// key: processId,
// tags: ['retoure', 'root'],
// name: 'Rückgabe',
// path: '/kunde/retoure/' + processId,
// params: queryParams,
// section: 'customer',
// });
// }
// if (crumbs.length > 0 && !isEqual(crumbs[0].params, queryParams)) {
// await this.breadcrumbService.patchBreadcrumb(crumbs[0].id, {
// params: queryParams,
// });
// }
// }
// async createUpdateOrDeleteListBreadcrumb(processId: number, queryParams: Record<string, string>) {
// if (!processId) {
// return;
// }
// const crumbs = await this.breadcrumbService.getBreadcrumbsByKeyAndTags$(processId, ['retoure', 'list']).pipe(first()).toPromise();
// if (crumbs.length === 0) {
// await this.breadcrumbService.addBreadcrumb({
// key: processId,
// tags: ['retoure', 'list'],
// name: 'Rückgabe',
// path: '/kunde/retoure/' + processId + '/list',
// params: queryParams,
// section: 'customer',
// });
// }
// }
}

View File

@@ -0,0 +1,29 @@
import { Routes } from '@angular/router';
import { RetoureRootPageComponent } from './retoure-root-page.component';
import { canActivateRetoure } from './guards/can-activate-retoure.guard';
import { canDeactivateRetoure } from './guards/can-deactivate-retoure.guard';
import { MainPageComponent } from './pages/main-page/main-page.component';
import { ListPageComponent } from './pages/list-page/list-page.component';
import { DetailsPageComponent } from './pages/details-page/details-page.component';
import { canDeactivateRetoureList } from './guards/can-deactivate-retoure-list.guard';
import { retoureListScrollPositionResolver } from './guards/retoure-list-scroll-position.resolver';
export const retoureRouterConfig: Routes = [
{ path: '', component: RetoureRootPageComponent, canActivate: [canActivateRetoure] },
{
path: ':processId',
component: RetoureRootPageComponent,
canActivate: [canActivateRetoure],
canDeactivate: [canDeactivateRetoure],
children: [
{ path: 'list/:receiptId', component: DetailsPageComponent },
{
path: 'list',
component: ListPageComponent,
canDeactivate: [canDeactivateRetoureList],
resolve: { scrollPosition: retoureListScrollPositionResolver },
},
{ path: '', component: MainPageComponent },
],
},
];

View File

@@ -0,0 +1,41 @@
import { Pipe, PipeTransform } from '@angular/core';
import { AddresseeDTO, PayerDTO2 } from '@swagger/oms';
export interface Addressee extends AddresseeDTO {
__typename__: 'Addressee';
}
export interface Payer extends PayerDTO2 {
__typename__: 'Payer';
}
@Pipe({
name: 'displayName',
standalone: true,
})
export class DisplayNamePipe implements PipeTransform {
transform(addresseeOrPayer: Addressee | Payer, ...args: any[]): any {
let nameStr = '';
if (addresseeOrPayer.__typename__ === 'Addressee' && addresseeOrPayer?.person?.lastName) {
nameStr += addresseeOrPayer.person.lastName;
nameStr += ' ';
} else if (addresseeOrPayer.__typename__ === 'Payer' && addresseeOrPayer?.lastName) {
nameStr += addresseeOrPayer.lastName;
nameStr += ' ';
}
if (addresseeOrPayer.__typename__ === 'Addressee' && addresseeOrPayer?.person?.firstName) {
nameStr += addresseeOrPayer.person.firstName;
} else if (addresseeOrPayer.__typename__ === 'Payer' && addresseeOrPayer?.firstName) {
nameStr += addresseeOrPayer.firstName;
}
if (addresseeOrPayer?.organisation?.name) {
nameStr += nameStr.length > 0 ? ' - ' : '';
nameStr += addresseeOrPayer.organisation.name;
}
return nameStr?.length > 0 ? nameStr : '(Keine Daten)';
}
}

View File

@@ -0,0 +1 @@
export * from './receipt-item-details.component';

View File

@@ -0,0 +1,3 @@
:host {
@apply grid grid-cols-[1fr,auto] bg-white p-4;
}

View File

@@ -0,0 +1,44 @@
<div class="grid grid-cols-[5rem,1fr] gap-4" *ngIf="item(); let item">
<div>
<shared-product-thumbnail [product]="item.product"></shared-product-thumbnail>
</div>
<div class="grid grid-cols-[1fr,auto]">
<div class="grid grid-flow-row gap-1">
<div class="text-sm">{{ item.product?.contributors }}</div>
<div class="text-lg font-semibold mb-2">{{ item.product?.name }}</div>
<div class="grid grid-flow-col justify-start items-center gap-4">
<img [src]="'/assets/images/Icon_' + item.product?.format + '.svg'" [alt]="item.product?.formatDetail" />
<span>{{ item.product?.formatDetail }}</span>
</div>
<div *ngIf="item?.product?.serial">Band/Reihe {{ item.product.serial }}</div>
<div *ngIf="item?.product?.edition">Edition {{ item.product.edition }}</div>
<div *ngIf="item?.product?.publicationDate">
{{ item?.product?.publicationDate | date: 'dd. MMMM yyyy' }}
</div>
<div class="h-4"></div>
<div *ngIf="item?.product?.manufacturer">
{{ item?.product?.manufacturer }}
</div>
<div *ngIf="item?.product?.locale">
{{ item.product.locale }}
</div>
<div class="h-4"></div>
<div *ngIf="item?.product?.ean">
{{ item.product.ean }}
</div>
</div>
<div>
<div class="text-2xl font-bold">{{ item?.price?.value?.value | currency: item?.price?.value?.currency : 'code' }}</div>
<div *ngIf="item?.price?.vat?.inPercent">inkl. {{ item?.price?.vat?.inPercent }}% MwSt.</div>
</div>
</div>
</div>
<div class="grid items-center">
<ui-select-bullet
*ngIf="!hideSelectBullet"
class="p-6"
[ngModel]="coerceSelected"
(ngModelChange)="selectedChange.emit($event)"
></ui-select-bullet>
</div>

View File

@@ -0,0 +1,69 @@
import { BooleanInput, NumberInput, coerceBooleanProperty } from '@angular/cdk/coercion';
import { CurrencyPipe, DatePipe, NgIf } from '@angular/common';
import {
Component,
ChangeDetectionStrategy,
Input,
Output,
EventEmitter,
signal,
computed,
inject,
ChangeDetectorRef,
AfterViewInit,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ProductThumbnailComponent } from '@shared/components/product';
import { QuantitySelectorComponent } from '@shared/components/quantity-selector';
import { ReceiptItemDTO } from '@swagger/oms';
import { UiSelectBulletModule } from '@ui/select-bullet';
@Component({
selector: 'page-receipt-item-details',
templateUrl: 'receipt-item-details.component.html',
styleUrls: ['receipt-item-details.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CurrencyPipe, DatePipe, UiSelectBulletModule, NgIf, QuantitySelectorComponent, FormsModule, ProductThumbnailComponent],
})
export class ReceiptDetailsItemComponent implements AfterViewInit {
cdr = inject(ChangeDetectorRef);
item = signal<ReceiptItemDTO>(null);
@Input({ alias: 'item', required: true })
set itemInput(value: ReceiptItemDTO) {
this.item.set(value);
}
originalQuantity = computed<number>(() => {
const item = this.item();
return item?.quantity?.quantity;
});
@Input()
selectable: BooleanInput = false;
@Input()
selected: BooleanInput = false;
get coerceSelected(): boolean {
return coerceBooleanProperty(this.selected);
}
@Output()
selectedChange = new EventEmitter<boolean>();
@Input()
quantity: NumberInput = 0;
@Output()
quantityChange = new EventEmitter<number>();
@Input({ transform: coerceBooleanProperty })
hideSelectBullet: BooleanInput = false;
ngAfterViewInit(): void {
setTimeout(() => this.cdr.detectChanges());
}
}

View File

@@ -0,0 +1 @@
export * from './receipt-details.component';

View File

@@ -0,0 +1,3 @@
:host {
@apply grid grid-flow-row gap-4 bg-white p-4 rounded mt-4;
}

View File

@@ -0,0 +1,27 @@
<div class="flex flex-row justify-between">
<div>
<shared-skeleton-loader class="h-7 w-96"></shared-skeleton-loader>
</div>
<div>
<shared-skeleton-loader class="h-6 w-36"></shared-skeleton-loader>
</div>
</div>
<div class="grid grid-cols-[9rem,1fr] gap-2">
<div><shared-skeleton-loader class="h-4 w-28"></shared-skeleton-loader></div>
<div>
<shared-skeleton-loader class="h-4 w-52"></shared-skeleton-loader> <br />
<shared-skeleton-loader class="h-4 w-44"></shared-skeleton-loader>
</div>
<div><shared-skeleton-loader class="h-4 w-24"></shared-skeleton-loader></div>
<div><shared-skeleton-loader class="h-4 w-56"></shared-skeleton-loader></div>
<div><shared-skeleton-loader class="h-4 w-28"></shared-skeleton-loader></div>
<div><shared-skeleton-loader class="h-4 w-40"></shared-skeleton-loader></div>
<div><shared-skeleton-loader class="h-4 w-24"></shared-skeleton-loader></div>
<div><shared-skeleton-loader class="h-4 w-44"></shared-skeleton-loader></div>
<div><shared-skeleton-loader class="h-4 w-24"></shared-skeleton-loader></div>
<div><shared-skeleton-loader class="h-4 w-52"></shared-skeleton-loader></div>
<div><shared-skeleton-loader class="h-4 w-28"></shared-skeleton-loader></div>
<div><shared-skeleton-loader class="h-4 w-48"></shared-skeleton-loader></div>
<div><shared-skeleton-loader class="h-4 w-24"></shared-skeleton-loader></div>
<div><shared-skeleton-loader class="h-4 w-36"></shared-skeleton-loader></div>
</div>

View File

@@ -0,0 +1,14 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { SkeletonLoaderComponent } from '@shared/components/loader';
@Component({
selector: 'page-receipt-details-loader',
templateUrl: 'receipt-details-loader.component.html',
styleUrls: ['receipt-details-loader.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [SkeletonLoaderComponent],
})
export class ReceiptDetailsLoaderComponent {
constructor() {}
}

View File

@@ -0,0 +1,3 @@
:host {
@apply block bg-white p-4;
}

View File

@@ -0,0 +1,71 @@
<div class="grid grid-flow-row gap-4">
<div class="grid grid-cols-[1fr,auto] justify-between items-center">
<div>
<h1 class="text-2xl font-bold">{{ billing | displayName }}</h1>
</div>
</div>
<div class="grid grid-cols-[1fr,auto] justify-between items-start">
<div class="grid grid-cols-[1fr,auto]">
<shared-data-table>
<shared-data-table-info label="Anschrift" *ngIf="receipt.billing?.address">
<div *sharedDataTableInfoValue>
{{ receipt.billing?.address?.street }} {{ receipt.billing?.address?.streetNumber }} <br />
{{ receipt.billing?.address?.zipCode }} {{ receipt.billing?.address?.city }}
</div>
</shared-data-table-info>
<shared-data-table-info label="Lieferanschrift" *ngIf="receipt.shipping">
<div *sharedDataTableInfoValue>
{{ receipt.shipping?.lastName }} {{ receipt.shipping?.firstName }}
<br *ngIf="receipt.shipping?.lastName || receipt.shipping?.firstName" />
{{ receipt.shipping?.organisation?.name }} <br *ngIf="receipt.shipping?.organisation?.name" />
{{ receipt.shipping?.address?.street }} {{ receipt.shipping?.address?.streetNumber }} <br />
{{ receipt.shipping?.address?.zipCode }} {{ receipt.shipping?.address?.city }}
</div>
</shared-data-table-info>
<shared-data-table-info label="Beleg-Nr." [value]="receipt?.receiptNumber"></shared-data-table-info>
<shared-data-table-info
label="Belegdatum"
[value]="(receipt?.printedDate | date: 'dd.MM.yyyy | HH:mm') + ' Uhr'"
></shared-data-table-info>
<shared-data-table-info label="Belegart" [value]="receipt?.receiptType | receiptType"></shared-data-table-info>
<shared-data-table-info
*ngIf="receipt?.order?.data?.orderNumber"
label="Vorgang-ID"
[value]="receipt?.order?.data?.orderNumber"
></shared-data-table-info>
<shared-data-table-info
*ngIf="receipt?.order?.data?.orderDate"
label="Bestelldatum"
[value]="(receipt?.order?.data?.orderDate | date: 'dd.MM.yyyy | HH:mm') + ' Uhr'"
></shared-data-table-info>
<shared-data-table-info
*ngIf="receipt?.payment?.data?.paymentNumber"
label="Zahlungs-Ref."
[value]="receipt?.payment?.data?.paymentNumber"
></shared-data-table-info>
<shared-data-table-info
*ngIf="receipt?.payment?.data?.paymentComment"
label="Zahlungs-Info"
[value]="receipt?.payment?.data?.paymentComment"
></shared-data-table-info>
<shared-data-table-info
*ngIf="receipt?.agentComment"
label="Miterarbeiter-Info"
[value]="receipt?.agentComment"
></shared-data-table-info>
<shared-data-table-info label="Menge" [value]="receipt?.items?.length + ' Artikel'"></shared-data-table-info>
</shared-data-table>
<div></div>
</div>
<div class="text-right">
<div class="grid grid-flow-col justify-end items-center gap-3 text-accent-green" *ngIf="receipt?.paymentInfo?.paymentStatus">
<shared-icon icon="credit-card"></shared-icon>
<span class="font-bold text-lg">{{ receipt?.paymentInfo?.paymentStatus | paymentStatus }}</span>
</div>
<div *ngIf="receipt?.paymentInfo?.paymentType">
<span>Zahlungsart</span> <br />
<span class="font-bold">{{ receipt?.paymentInfo?.paymentType | paymentType }}</span>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,32 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { ReceiptDTO } from '@swagger/oms';
import { DisplayNamePipe, Payer } from '../pipes/display-name.pipe';
import { IconComponent } from '@shared/components/icon';
import { DataTableModule } from '@shared/components/data-table';
import { DatePipe, NgIf } from '@angular/common';
import { PaymentStatusPipe, ReceiptTypePipe } from '@shared/pipes/enum';
import { PaymentTypePipe } from '@shared/pipes/customer';
@Component({
selector: 'page-receipt-details',
templateUrl: 'receipt-details.component.html',
styleUrls: ['receipt-details.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [DisplayNamePipe, IconComponent, DataTableModule, DatePipe, ReceiptTypePipe, PaymentStatusPipe, PaymentTypePipe, NgIf],
})
export class ReceiptDetailsComponent {
@Input({ required: true })
receipt: ReceiptDTO;
get billing(): Payer {
if (!this.receipt?.billing) {
return null;
}
return {
__typename__: 'Payer',
...this.receipt.billing,
};
}
}

View File

@@ -0,0 +1,3 @@
:host {
@apply grid grid-rows-[auto_1fr] gap-[0.125rem];
}

View File

@@ -0,0 +1,25 @@
<div class="grid grid-flow-col px-4 py-2 bg-white rounded-t">
<div class="text-2xl font-bold">
<shared-skeleton-loader class="h-6 w-80"></shared-skeleton-loader>
</div>
</div>
<div class="grid grid-cols-[1fr_auto] px-4 py-2 bg-white rounded-b">
<div class="grid grid-flow-row gap-2">
<div class="grid grid-cols-[9rem_1fr]">
<div><shared-skeleton-loader class="h-4 w-24"></shared-skeleton-loader></div>
<div class="font-bold"><shared-skeleton-loader class="h-4 w-32"></shared-skeleton-loader></div>
</div>
<div class="grid grid-cols-[9rem_1fr]">
<div><shared-skeleton-loader class="h-4 w-28"></shared-skeleton-loader></div>
<div class="font-bold"><shared-skeleton-loader class="h-4 w-28"></shared-skeleton-loader></div>
</div>
<div class="grid grid-cols-[9rem_1fr]">
<div><shared-skeleton-loader class="h-4 w-24"></shared-skeleton-loader></div>
<div class="font-bold"><shared-skeleton-loader class="h-4 w-24"></shared-skeleton-loader></div>
</div>
<div class="grid grid-cols-[9rem_1fr] items-center">
<div><shared-skeleton-loader class="h-4 w-20"></shared-skeleton-loader></div>
<div class="font-bold"><shared-skeleton-loader class="h-4"></shared-skeleton-loader></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,14 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { SkeletonLoaderComponent } from '@shared/components/loader';
@Component({
selector: 'page-retoure-list-item-loader',
templateUrl: 'retoure-list-item-loader.component.html',
styleUrls: ['retoure-list-item-loader.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [SkeletonLoaderComponent],
})
export class RetoureListItemLoaderComponent {
constructor() {}
}

View File

@@ -0,0 +1,3 @@
:host {
@apply grid grid-rows-[auto_1fr] gap-[0.125rem];
}

View File

@@ -0,0 +1,25 @@
<div class="grid grid-flow-col px-4 py-2 bg-white rounded-t">
<div class="text-2xl font-bold">
{{ name }}
</div>
</div>
<div class="grid grid-cols-[1fr_auto] px-4 py-2 bg-white rounded-b">
<div class="grid grid-flow-row gap-2">
<div class="grid grid-cols-[9rem_1fr]">
<div>Beleg-Nr.</div>
<div class="font-bold">{{ item?.receiptNumber }}</div>
</div>
<div class="grid grid-cols-[9rem_1fr]">
<div>Beleg-Datum</div>
<div class="font-bold">{{ item?.printedDate | date }}</div>
</div>
<div class="grid grid-cols-[9rem_1fr]">
<div>Beleg-Art</div>
<div class="font-bold">{{ item?.receiptType | receiptType }}</div>
</div>
<div class="grid grid-cols-[9rem_1fr] items-center">
<div>Artikel</div>
<div class="font-bold">{{ item?.items ?? 0 }}</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,41 @@
import { DatePipe, NgIf } from '@angular/common';
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { ReceiptTypePipe } from '@shared/pipes/enum';
import { ReceiptListItemDTO } from '@swagger/oms';
@Component({
selector: 'page-retoure-list-item',
templateUrl: 'retoure-list-item.component.html',
styleUrls: ['retoure-list-item.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgIf, DatePipe, ReceiptTypePipe],
})
export class RetoureListItemComponent {
@Input({ required: true })
item: ReceiptListItemDTO;
get name() {
if (!this.item) {
return '(Keine Daten)';
}
let nameStr = '';
if (this.item?.billing?.person?.lastName) {
nameStr += this.item.billing.person.lastName;
nameStr += ' ';
}
if (this.item?.billing?.person?.firstName) {
nameStr += this.item.billing.person.firstName;
}
if (this.item?.billing?.organisation?.name) {
nameStr += nameStr.length > 0 ? ' - ' : '';
nameStr += this.item.billing.organisation.name;
}
return nameStr?.length > 0 ? nameStr : '(Keine Daten)';
}
}

View File

@@ -0,0 +1 @@
export const RETOURE_STORE_FEATURE_KEY = '[Retoure]';

View File

@@ -0,0 +1,31 @@
import { inject } from '@angular/core';
import * as Actions from './retoure.actions';
import { Store } from '@ngrx/store';
import { Actions as NgrxActions, ofType } from '@ngrx/effects';
import { filter, take } from 'rxjs/operators';
export function injectSetSearchQueryParams() {
const store = inject(Store);
return (processId: number, queryParams: Record<string, string>) =>
store.dispatch(Actions.setSearchQueryParams({ processId, queryParams }));
}
export function injectFetchRetoure() {
const store = inject(Store);
return (processId: number) => store.dispatch(Actions.fetchRetoure({ processId }));
}
export function injectFetchRetoureResult() {
const actions = inject(NgrxActions);
return (processId: number) =>
actions.pipe(
ofType(Actions.fetchRetoureSuccess, Actions.fetchRetoureError),
filter((action) => action.processId === processId),
take(1),
);
}
export function injectFetchReceipt() {
const store = inject(Store);
return (id: number) => store.dispatch(Actions.fetchReceipt({ id }));
}

View File

@@ -0,0 +1,41 @@
import { createAction, props } from '@ngrx/store';
import { RETOURE_STORE_FEATURE_KEY } from './constants';
import { QuerySettingsDTO, ReceiptDTO, ReceiptListItemDTO } from '@swagger/oms';
export const fetchRetoureSettings = createAction(`${RETOURE_STORE_FEATURE_KEY} Fetch Retoure Settings`);
export const fetchRetoureSettingsSuccess = createAction(
`${RETOURE_STORE_FEATURE_KEY} Fetch Retoure Settings Success`,
props<{ settings: QuerySettingsDTO }>(),
);
export const fetchRetoureSettingsError = createAction(
`${RETOURE_STORE_FEATURE_KEY} Fetch Retoure Settings Error`,
props<{ error: Error }>(),
);
export const setSearchQueryParams = createAction(
`${RETOURE_STORE_FEATURE_KEY} Set Search Query Params`,
props<{ processId: number; queryParams: Record<string, string> }>(),
);
export const fetchRetoure = createAction(`${RETOURE_STORE_FEATURE_KEY} Fetch Retoure`, props<{ processId: number }>());
export const fetchRetoureSuccess = createAction(
`${RETOURE_STORE_FEATURE_KEY} Fetch Retoure Success`,
props<{ processId: number; items: ReceiptListItemDTO[]; hits: number }>(),
);
export const fetchRetoureError = createAction(
`${RETOURE_STORE_FEATURE_KEY} Fetch Retoure Error`,
props<{ processId: number; error: Error }>(),
);
export const fetchReceipt = createAction(`${RETOURE_STORE_FEATURE_KEY} Fetch Receipt`, props<{ id: number }>());
export const fetchReceiptSuccess = createAction(
`${RETOURE_STORE_FEATURE_KEY} Fetch Receipt Success`,
props<{ id: number; item: ReceiptDTO }>(),
);
export const fetchReceiptError = createAction(`${RETOURE_STORE_FEATURE_KEY} Fetch Receipt Error`, props<{ id: number; error: Error }>());

View File

@@ -0,0 +1,76 @@
import { Injectable, inject } from '@angular/core';
import { Actions, OnInitEffects, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, mergeMap, switchMap, withLatestFrom } from 'rxjs/operators';
import { ReceiptService } from '@swagger/oms';
import { Action, Store } from '@ngrx/store';
import {
fetchReceipt,
fetchReceiptError,
fetchReceiptSuccess,
fetchRetoure,
fetchRetoureError,
fetchRetoureSettings,
fetchRetoureSettingsError,
fetchRetoureSettingsSuccess,
fetchRetoureSuccess,
} from './retoure.actions';
import { selectSearchFilter, selectSearchResult } from './retoure.selectors';
import { of } from 'rxjs';
@Injectable()
export class RetoureEffects implements OnInitEffects {
private store = inject(Store);
private actions$ = inject(Actions);
private receiptService = inject(ReceiptService);
fetchRetoureSettings$ = createEffect(() =>
this.actions$.pipe(
ofType(fetchRetoureSettings),
switchMap(() =>
this.receiptService.ReceiptQueryReceiptSettings().pipe(
map((res) => fetchRetoureSettingsSuccess({ settings: res.result })),
catchError((error) => [fetchRetoureSettingsError({ error })]),
),
),
),
);
fetchRetoure$ = createEffect(() =>
this.actions$.pipe(
ofType(fetchRetoure),
mergeMap((action) =>
of(action).pipe(
withLatestFrom(this.store.select(selectSearchFilter(action.processId)), this.store.select(selectSearchResult(action.processId))),
),
),
switchMap(([action, filter, items]) =>
this.receiptService
.ReceiptQueryReceipt({
queryToken: { ...filter.getQueryToken(), take: 20, skip: items.length },
})
.pipe(
map((res) => fetchRetoureSuccess({ processId: action.processId, items: res.result, hits: res.hits })),
catchError((error) => [fetchRetoureError({ processId: action.processId, error })]),
),
),
),
);
fetchReceipt$ = createEffect(() =>
this.actions$.pipe(
ofType(fetchReceipt),
switchMap((action) =>
this.receiptService.ReceiptGetReceipt({ receiptId: action.id }).pipe(
map((res) => fetchReceiptSuccess({ id: action.id, item: res.result })),
catchError((error) => [fetchReceiptError({ id: action.id, error })]),
),
),
),
);
ngrxOnInitEffects(): Action {
return fetchRetoureSettings();
}
}

View File

@@ -0,0 +1,12 @@
import { ReceiptListItemDTO } from '@swagger/oms';
export interface RetoureSearch {
// Ist die gleiche wie die Prozess ID
id: number;
queryParams: Record<string, string>;
result: ReceiptListItemDTO[];
hits: number;
}

View File

@@ -0,0 +1,24 @@
import { createReducer, on } from '@ngrx/store';
import { INITIAL_RETOURE_STATE, receiptEntityAdapter, searchEntityAdapter } from './retoure.state';
import * as Actions from './retoure.actions';
export const retoureReducer = createReducer(
INITIAL_RETOURE_STATE,
on(Actions.fetchRetoureSettingsSuccess, (state, { settings }) => ({ ...state, settings })),
on(Actions.setSearchQueryParams, (state, { processId, queryParams }) => ({
...state,
search: searchEntityAdapter.setOne({ id: processId, hits: 0, result: [], queryParams }, state.search),
})),
on(Actions.fetchRetoureSuccess, (state, { processId, items, hits }) => {
const result = state.search.entities[processId]?.result ?? [];
return {
...state,
search: searchEntityAdapter.updateOne({ id: processId, changes: { hits, result: [...result, ...items] } }, state.search),
};
}),
on(Actions.fetchReceiptSuccess, (state, { id, item }) => ({
...state,
receipt: receiptEntityAdapter.upsertOne({ id, ...item }, state.receipt),
})),
);

View File

@@ -0,0 +1,27 @@
import { inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import * as Selectors from './retoure.selectors';
import { switchMap } from 'rxjs/operators';
import { Filter } from '@shared/components/filter';
import { ReceiptDTO, ReceiptListItemDTO } from '@swagger/oms';
export function injectSearchFilter(processId$: Observable<number>): Observable<Filter> {
const store = inject(Store);
return processId$.pipe(switchMap((processId) => store.select(Selectors.selectSearchFilter(processId))));
}
export function injectSearchResult(processId$: Observable<number>): Observable<Array<ReceiptListItemDTO>> {
const store = inject(Store);
return processId$.pipe(switchMap((processId) => store.select(Selectors.selectSearchResult(processId))));
}
export function injectSearchHits(processId$: Observable<number>): Observable<number> {
const store = inject(Store);
return processId$.pipe(switchMap((processId) => store.select(Selectors.selectSearchHits(processId))));
}
export function injectSelectReceipt(receiptId$: Observable<number>): Observable<ReceiptDTO & { __source__?: 'ReceiptListItemDTO' }> {
const store = inject(Store);
return receiptId$.pipe(switchMap((id) => store.select(Selectors.selectReceipt(id))));
}

View File

@@ -0,0 +1,42 @@
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { RetoureState, searchEntityAdapter } from './retoure.state';
import { RETOURE_STORE_FEATURE_KEY } from './constants';
import { Filter } from '@shared/components/filter';
import { isEmpty } from 'lodash';
import { ReceiptDTO } from '@swagger/oms';
export const selectRetoureState = createFeatureSelector<RetoureState>(RETOURE_STORE_FEATURE_KEY);
export const selectSearchState = createSelector(selectRetoureState, (s) => s.search);
const searchSelectors = searchEntityAdapter.getSelectors(selectSearchState);
export const selectRetoureSettings = createSelector(selectRetoureState, (s) => s.settings);
export const selectSearchQueryParams = (processId: number) =>
createSelector(searchSelectors.selectEntities, (entities) => entities[processId]?.queryParams ?? {});
export const selectSearchFilter = (processId: number) =>
createSelector(selectRetoureSettings, selectSearchQueryParams(processId), (settings, queryParams) => {
const filter = Filter.create(settings);
if (queryParams && !isEmpty(queryParams)) {
filter.fromQueryParams(queryParams);
}
return filter;
});
export const selectSearch = (processId: number) => createSelector(searchSelectors.selectEntities, (entities) => entities[processId]);
export const selectSearchResult = (processId: number) => createSelector(selectSearch(processId), (entity) => entity?.result ?? []);
export const selectSearchHits = (processId: number) =>
createSelector(searchSelectors.selectEntities, (entities) => entities[processId]?.hits ?? 0);
export const selectReceiptEntityState = createSelector(selectRetoureState, (s) => s.receipt);
export const selectReceipt = (id: number) =>
createSelector(selectReceiptEntityState, (s) => {
if (s.entities[id]) {
return s.entities[id];
}
});

View File

@@ -0,0 +1,22 @@
import { EntityState, createEntityAdapter } from '@ngrx/entity';
import { RetoureSearch } from './retoure.entities';
import { QuerySettingsDTO, ReceiptDTO } from '@swagger/oms';
export interface RetoureState {
settings?: QuerySettingsDTO;
search: EntityState<RetoureSearch>;
receipt: EntityState<ReceiptDTO>;
}
export const searchEntityAdapter = createEntityAdapter<RetoureSearch>();
export const receiptEntityAdapter = createEntityAdapter<ReceiptDTO>();
export const INITIAL_SEARCH_STATE: EntityState<RetoureSearch> = searchEntityAdapter.getInitialState();
export const INITIAL_RECEIPT_STATE: EntityState<ReceiptDTO> = receiptEntityAdapter.getInitialState();
export const INITIAL_RETOURE_STATE: RetoureState = {
search: INITIAL_SEARCH_STATE,
receipt: INITIAL_RECEIPT_STATE,
};

View File

@@ -0,0 +1,6 @@
import { Directive, TemplateRef, inject } from '@angular/core';
@Directive({ selector: '[sharedDataTableInfoLabel]', standalone: true })
export class DataTableInfoLabelDirective {
templateRef = inject(TemplateRef);
}

View File

@@ -0,0 +1,6 @@
import { Directive, TemplateRef, inject } from '@angular/core';
@Directive({ selector: '[sharedDataTableInfoValue]', standalone: true })
export class DataTableInfoValueDirective {
templateRef = inject(TemplateRef);
}

View File

@@ -0,0 +1,14 @@
import { ContentChild, Directive, Input } from '@angular/core';
import { DataTableInfoLabelDirective } from './data-table-info-label.directive';
import { DataTableInfoValueDirective } from './data-table-info-value.directive';
@Directive({ selector: 'shared-data-table-info', standalone: true })
export class DataTableInfoDirective<TData> {
@Input() label: string;
@Input() value: string;
@ContentChild(DataTableInfoLabelDirective) labelTemplate: DataTableInfoLabelDirective;
@ContentChild(DataTableInfoValueDirective) valueTemplate: DataTableInfoValueDirective;
}

View File

@@ -0,0 +1,3 @@
shared-data-table {
@apply table;
}

View File

@@ -0,0 +1,21 @@
<ng-container *ngFor="let info of infos">
<div class="table-row">
<div class="table-cell w-[9rem] py-1" data-which="label-cell">
<ng-container *ngIf="info.labelTemplate; else labelTmpl">
<ng-container [ngTemplateOutlet]="info.labelTemplate.templateRef"></ng-container>
</ng-container>
<ng-template #labelTmpl>
{{ info.label }}
</ng-template>
</div>
<div class="table-cell font-bold py-1" data-which="value-cell">
<ng-container *ngIf="info.valueTemplate; else valueTmpl">
<ng-container [ngTemplateOutlet]="info.valueTemplate.templateRef"></ng-container>
</ng-container>
<ng-template #valueTmpl>
{{ info.value }}
</ng-template>
</div>
</div>
</ng-container>

View File

@@ -0,0 +1,16 @@
import { Component, ChangeDetectionStrategy, ViewEncapsulation, ContentChildren, QueryList, Input } from '@angular/core';
import { DataTableInfoDirective } from './data-table-info.directive';
import { NgFor, NgIf, NgTemplateOutlet } from '@angular/common';
@Component({
selector: 'shared-data-table',
templateUrl: 'data-table.component.html',
styleUrls: ['data-table.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgFor, NgIf, NgTemplateOutlet],
encapsulation: ViewEncapsulation.None,
})
export class DataTableComponent {
@ContentChildren(DataTableInfoDirective) infos: QueryList<DataTableInfoDirective<any>>;
}

View File

@@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { DataTableComponent } from './data-table.component';
import { DataTableInfoDirective } from './data-table-info.directive';
import { DataTableInfoLabelDirective } from './data-table-info-label.directive';
import { DataTableInfoValueDirective } from './data-table-info-value.directive';
@NgModule({
imports: [DataTableComponent, DataTableInfoDirective, DataTableInfoLabelDirective, DataTableInfoValueDirective],
exports: [DataTableComponent, DataTableInfoDirective, DataTableInfoLabelDirective, DataTableInfoValueDirective],
})
export class DataTableModule {}

View File

@@ -0,0 +1,5 @@
export * from './data-table-info-label.directive';
export * from './data-table-info-value.directive';
export * from './data-table-info.directive';
export * from './data-table.component';
export * from './data-table.module';

View File

@@ -1,11 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'shared-product-list-item',
templateUrl: 'product-list-item.component.html',
})
export class ProductListItemComponent implements OnInit {
constructor() {}
ngOnInit() {}
}

View File

@@ -1,11 +0,0 @@
import { NgModule } from '@angular/core';
import { ProductListItemComponent } from './product-list-item.component';
@NgModule({
imports: [],
exports: [],
declarations: [ProductListItemComponent],
providers: [],
})
export class ProductListItemModule {}

View File

@@ -0,0 +1 @@
export * from './product-thumbnail.component';

View File

@@ -0,0 +1,7 @@
:host {
@apply inline-block rounded shadow-card overflow-hidden border border-gray-300 border-solid;
}
img {
@apply object-cover w-full;
}

View File

@@ -0,0 +1 @@
<img [src]="thumbnailUrl() | async" [alt]="name()" loading="lazy" class="product-thumbnail" />

View File

@@ -0,0 +1,42 @@
import { AsyncPipe } from '@angular/common';
import { Component, ChangeDetectionStrategy, inject, Input, signal, computed, ElementRef } from '@angular/core';
import { DomainCatalogThumbnailService } from '@domain/catalog';
export interface ProductThumbnailProductInput {
ean?: string;
name?: string;
}
@Component({
selector: 'shared-product-thumbnail',
templateUrl: 'product-thumbnail.component.html',
styleUrls: ['product-thumbnail.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [AsyncPipe],
})
export class ProductThumbnailComponent {
thumbnailService = inject(DomainCatalogThumbnailService);
private _product = signal<ProductThumbnailProductInput>(undefined);
elementRef: ElementRef<Element> = inject(ElementRef);
widthPx = signal<number>(undefined);
heightPx = signal<number>(undefined);
ean = computed(() => this._product()?.ean);
name = computed(() => this._product()?.name ?? this._product()?.ean);
thumbnailUrl = computed(() => this.thumbnailService.getThumnaulUrl({ ean: this.ean() }));
@Input({ required: true })
get product(): { ean?: string; name?: string } | undefined {
return this._product();
}
set product(value: { ean?: string; name?: string } | undefined) {
this._product.set(value);
}
}

View File

@@ -0,0 +1 @@
export * from './quantity-selector.component';

View File

@@ -0,0 +1,48 @@
:host {
@apply inline-block;
}
[role='menuitem'],
.delete-btn {
@apply px-4 h-12 min-w-[5.25rem];
@apply rounded border border-solid border-white;
}
[role='menuitem']:hover {
@apply border-components-menu-hover-border bg-components-menu-hover;
}
.control-wrapper:focus-within {
@apply outline-none border-components-menu-hover-border;
}
.control-wrapper:has(.ng-invalid.ng-dirty) {
@apply border-brand;
}
input[type='number'] {
@apply focus:outline-none;
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input.ng-dirty.ng-invalid {
@apply text-brand;
}
.control-wrapper button:disabled {
@apply text-inactive-branch;
}
.control-wrapper:has(.ng-invalid.ng-dirty) button {
@apply text-brand;
}

View File

@@ -0,0 +1,29 @@
<button type="button" class="grid grid-flow-col gap-1" [cdkMenuTriggerFor]="menuTmpl" [cdkMenuPosition]="[menuPosition]">
<div>
{{ _value() }}
</div>
<div class="rotate-90 text-components-menu-hover-border">
<shared-icon icon="chevron-right"></shared-icon>
</div>
</button>
<ng-template #menuTmpl>
<div class="bg-white rounded shadow-card py-2 grid grid-flow-row" cdkMenu>
<button cdkMenuItem *ngFor="let val of selectableValues()" (click)="setValue(val)">{{ val }}</button>
<div class="control-wrapper border-2 border-solid border-white rounded relative">
<input
cdkMenuGroup
*ngIf="maxValue > 10"
class="h-12 w-full text-center"
type="number"
placeholder="10+"
(keydown)="$event.stopPropagation()"
[formControl]="quantityControl"
(keyup.enter)="quantityControl.valid && setValue(quantityControl.value)"
/>
<button type="button" class="absolute top-0 bottom-0 right-4 text-components-menu-hover-border" [disabled]="quantityControl.invalid">
<shared-icon icon="save" (click)="setValue(quantityControl.value)"></shared-icon>
</button>
</div>
</div>
</ng-template>

View File

@@ -0,0 +1,145 @@
import { BooleanInput, NumberInput, coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';
import {
Component,
ChangeDetectionStrategy,
signal,
Input,
Output,
EventEmitter,
computed,
ViewChild,
OnInit,
inject,
DestroyRef,
} from '@angular/core';
import { ControlValueAccessor, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { IconComponent } from '@shared/components/icon';
import { CdkMenuModule, CdkMenuTrigger } from '@angular/cdk/menu';
import { ConnectedPosition } from '@angular/cdk/overlay';
import { NgFor, NgIf } from '@angular/common';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'shared-quantity-selector',
templateUrl: 'quantity-selector.component.html',
styleUrls: ['quantity-selector.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [IconComponent, CdkMenuModule, NgFor, NgIf, ReactiveFormsModule],
})
export class QuantitySelectorComponent implements ControlValueAccessor, OnInit {
private destroyRef = inject(DestroyRef);
@ViewChild(CdkMenuTrigger, { static: true })
menuTrigger: CdkMenuTrigger;
private onChange = (_: number) => {};
private onTouched = () => {};
quantityControl = new FormControl<number>(null);
menuPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top',
};
_value = signal<number>(0);
@Input()
set value(value: NumberInput) {
this.writeValue(value);
}
get value(): number {
return this._value();
}
_minValue = signal<number>(1);
@Input()
set minValue(value: NumberInput) {
this._minValue.set(coerceNumberProperty(value));
this.updateQuantityControlValidators();
}
get minValue(): number {
return this._minValue();
}
_maxValue = signal<number>(Infinity);
@Input()
set maxValue(value: NumberInput) {
this._maxValue.set(coerceNumberProperty(value));
this.updateQuantityControlValidators();
}
get maxValue(): number {
return this._maxValue();
}
selectableValues = computed(() => {
const maxValue = Math.min(this.maxValue, this.maxValue === 10 ? 10 : 9);
const minValue = Math.max(this.minValue, 1);
return Array.from({ length: maxValue - minValue + 1 }, (_, i) => i + minValue);
});
@Output()
valueChange = new EventEmitter<number>();
_disabled = signal<boolean>(false);
ngOnInit(): void {
this.menuTrigger.closed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.quantityControl.reset();
});
}
updateQuantityControlValidators() {
this.quantityControl.setValidators([
Validators.required,
Validators.min(this.minValue),
Validators.max(this.maxValue),
(c) => (isNaN(+c.value) ? { invalid: true } : null),
]);
}
@Input()
set disabled(value: BooleanInput) {
this.setDisabledState(coerceBooleanProperty(value));
}
get disabled(): boolean {
return this._disabled();
}
writeValue(obj: any): void {
const value = coerceNumberProperty(obj);
if (isNaN(value)) {
this.value = 0;
console.warn('Invalid value provided for quantity selector', obj);
}
this._value.set(value);
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this._disabled.set(isDisabled);
}
setValue(value: number) {
this._value.set(value);
this.onChange(value);
this.valueChange.emit(value);
this.onTouched();
this.menuTrigger.close();
}
}

View File

@@ -0,0 +1,3 @@
export * from './payment-status.pipe';
export * from './payment-type.pipe';
export * from './receipt-type.pipe';

View File

@@ -0,0 +1,18 @@
import { Pipe, PipeTransform, inject } from '@angular/core';
import { Config } from '@core/config';
import { PaymentStatus } from '@swagger/oms';
@Pipe({
name: 'paymentStatus',
standalone: true,
pure: true,
})
export class PaymentStatusPipe implements PipeTransform {
config = inject(Config);
paymentStatus: Record<string, string> = this.config.get('paymentStatus');
transform(value: PaymentStatus): string {
return this.paymentStatus[(value ?? 0).toString()];
}
}

View File

@@ -0,0 +1,18 @@
import { Pipe, PipeTransform, inject } from '@angular/core';
import { Config } from '@core/config';
import { PaymentType } from '@swagger/oms';
@Pipe({
name: 'paymentType',
standalone: true,
pure: true,
})
export class PaymentTypePipe implements PipeTransform {
config = inject(Config);
paymentType: Record<string, string> = this.config.get('paymentType');
transform(value: PaymentType): string {
return this.paymentType[(value ?? 0).toString()];
}
}

View File

@@ -0,0 +1,18 @@
import { Pipe, PipeTransform, inject } from '@angular/core';
import { Config } from '@core/config';
import { ReceiptType } from '@swagger/oms';
@Pipe({
name: 'receiptType',
standalone: true,
pure: true,
})
export class ReceiptTypePipe implements PipeTransform {
config = inject(Config);
receiptType: Record<string, string> = this.config.get('receiptType');
transform(value: ReceiptType): string {
return this.receiptType[(value ?? 0)?.toString()];
}
}

View File

@@ -97,6 +97,17 @@
</span>
<span class="side-menu-group-item-label"> Bestellungen </span>
</a>
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="['/kunde', 'retoure']"
(isActiveChange)="focusSearchBox()"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="undo"></shared-icon>
</span>
<span class="side-menu-group-item-label"> Rückgabe </span>
</a>
</nav>
</div>

View File

@@ -1,2 +1,2 @@
/* tslint:disable */
export type InputType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 3072 | 4096 | 8192 | 12288;
export type InputType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 3072 | 4096 | 8192 | 12288;

View File

@@ -1,6 +1,6 @@
/* tslint:disable */
import { ResponseArgsOfIEnumerableOfBranchDTO } from './response-args-of-ienumerable-of-branch-dto';
export interface ListResponseArgsOfBranchDTO extends ResponseArgsOfIEnumerableOfBranchDTO{
export interface ListResponseArgsOfBranchDTO extends ResponseArgsOfIEnumerableOfBranchDTO {
completed?: boolean;
hits?: number;
skip?: number;

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