mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
12 Commits
7200eaefbf
...
feature/47
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58b0fde3f8 | ||
|
|
22b0c57fb0 | ||
|
|
e59ddaf6a6 | ||
|
|
a74b4ed791 | ||
|
|
3017f341af | ||
|
|
c75a6d87f9 | ||
|
|
b85b69f078 | ||
|
|
93c050f09a | ||
|
|
23b2a05d47 | ||
|
|
bd746f9915 | ||
|
|
91f70b0503 | ||
|
|
3237dff1bc |
@@ -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 {}
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
12
apps/isa-app/src/core/request-tracker/actions.ts
Normal file
12
apps/isa-app/src/core/request-tracker/actions.ts
Normal 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[] }>());
|
||||
1
apps/isa-app/src/core/request-tracker/constants.ts
Normal file
1
apps/isa-app/src/core/request-tracker/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const NGRX_FEATURE_NAME = '@core/request-tracker';
|
||||
9
apps/isa-app/src/core/request-tracker/index.ts
Normal file
9
apps/isa-app/src/core/request-tracker/index.ts
Normal 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';
|
||||
28
apps/isa-app/src/core/request-tracker/inject.ts
Normal file
28
apps/isa-app/src/core/request-tracker/inject.ts
Normal 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)),
|
||||
);
|
||||
}
|
||||
32
apps/isa-app/src/core/request-tracker/reducer.ts
Normal file
32
apps/isa-app/src/core/request-tracker/reducer.ts
Normal 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;
|
||||
}),
|
||||
);
|
||||
24
apps/isa-app/src/core/request-tracker/request-status.ts
Normal file
24
apps/isa-app/src/core/request-tracker/request-status.ts
Normal 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;
|
||||
@@ -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],
|
||||
};
|
||||
}
|
||||
}
|
||||
62
apps/isa-app/src/core/request-tracker/request-tracker.ts
Normal file
62
apps/isa-app/src/core/request-tracker/request-tracker.ts
Normal 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();
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
17
apps/isa-app/src/core/request-tracker/selectors.ts
Normal file
17
apps/isa-app/src/core/request-tracker/selectors.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
5
apps/isa-app/src/core/request-tracker/state.ts
Normal file
5
apps/isa-app/src/core/request-tracker/state.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { RequestStatus } from './request-status';
|
||||
|
||||
export type RequestTrackerState = Record<string, RequestStatus>;
|
||||
|
||||
export const initialState: RequestTrackerState = {};
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply grid grid-rows-[auto_1fr] gap-3 bg-white w-full h-full p-4 relative;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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(), {});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export const DESTROYED = new InjectionToken<Subject<void>>('RETOURE_FILTER_COMPONENT_DESTROYED');
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
7
apps/isa-app/src/page/retoure/index.ts
Normal file
7
apps/isa-app/src/page/retoure/index.ts
Normal 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';
|
||||
42
apps/isa-app/src/page/retoure/inject-apply-filter.ts
Normal file
42
apps/isa-app/src/page/retoure/inject-apply-filter.ts
Normal 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';
|
||||
}
|
||||
};
|
||||
}
|
||||
11
apps/isa-app/src/page/retoure/inject-process-id.ts
Normal file
11
apps/isa-app/src/page/retoure/inject-process-id.ts
Normal 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)),
|
||||
);
|
||||
}
|
||||
7
apps/isa-app/src/page/retoure/inject-query-params.ts
Normal file
7
apps/isa-app/src/page/retoure/inject-query-params.ts
Normal 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;
|
||||
}
|
||||
@@ -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 } };
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply block;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply block max-h-full overflow-hidden h-full;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
:host {
|
||||
@apply block bg-white rounded px-4 py-6 text-center;
|
||||
height: calc(100% - 1rem);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
167
apps/isa-app/src/page/retoure/retoure-breadcrumb.service.ts
Normal file
167
apps/isa-app/src/page/retoure/retoure-breadcrumb.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
apps/isa-app/src/page/retoure/retoure-page.module.ts
Normal file
18
apps/isa-app/src/page/retoure/retoure-page.module.ts
Normal 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 {}
|
||||
@@ -0,0 +1,5 @@
|
||||
:host {
|
||||
@apply grid grid-rows-[auto,1fr] h-screen;
|
||||
|
||||
max-height: calc(100vh - 8.3125rem);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<shared-breadcrumb [key]="processId$ | async" [tags]="['retoure']"></shared-breadcrumb>
|
||||
<div class="overflow-scroll" #filterOrigin>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
78
apps/isa-app/src/page/retoure/retoure-root-page.component.ts
Normal file
78
apps/isa-app/src/page/retoure/retoure-root-page.component.ts
Normal 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',
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
}
|
||||
29
apps/isa-app/src/page/retoure/router.config.ts
Normal file
29
apps/isa-app/src/page/retoure/router.config.ts
Normal 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 },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -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)';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './receipt-item-details.component';
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply grid grid-cols-[1fr,auto] bg-white p-4;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './receipt-details.component';
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply grid grid-flow-row gap-4 bg-white p-4 rounded mt-4;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply block bg-white p-4;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply grid grid-rows-[auto_1fr] gap-[0.125rem];
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply grid grid-rows-[auto_1fr] gap-[0.125rem];
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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)';
|
||||
}
|
||||
}
|
||||
1
apps/isa-app/src/page/retoure/store/constants.ts
Normal file
1
apps/isa-app/src/page/retoure/store/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const RETOURE_STORE_FEATURE_KEY = '[Retoure]';
|
||||
@@ -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 }));
|
||||
}
|
||||
41
apps/isa-app/src/page/retoure/store/retoure.actions.ts
Normal file
41
apps/isa-app/src/page/retoure/store/retoure.actions.ts
Normal 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 }>());
|
||||
76
apps/isa-app/src/page/retoure/store/retoure.effects.ts
Normal file
76
apps/isa-app/src/page/retoure/store/retoure.effects.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
12
apps/isa-app/src/page/retoure/store/retoure.entities.ts
Normal file
12
apps/isa-app/src/page/retoure/store/retoure.entities.ts
Normal 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;
|
||||
}
|
||||
24
apps/isa-app/src/page/retoure/store/retoure.reducer.ts
Normal file
24
apps/isa-app/src/page/retoure/store/retoure.reducer.ts
Normal 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),
|
||||
})),
|
||||
);
|
||||
@@ -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))));
|
||||
}
|
||||
42
apps/isa-app/src/page/retoure/store/retoure.selectors.ts
Normal file
42
apps/isa-app/src/page/retoure/store/retoure.selectors.ts
Normal 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];
|
||||
}
|
||||
});
|
||||
22
apps/isa-app/src/page/retoure/store/retoure.state.ts
Normal file
22
apps/isa-app/src/page/retoure/store/retoure.state.ts
Normal 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,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Directive, TemplateRef, inject } from '@angular/core';
|
||||
|
||||
@Directive({ selector: '[sharedDataTableInfoLabel]', standalone: true })
|
||||
export class DataTableInfoLabelDirective {
|
||||
templateRef = inject(TemplateRef);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Directive, TemplateRef, inject } from '@angular/core';
|
||||
|
||||
@Directive({ selector: '[sharedDataTableInfoValue]', standalone: true })
|
||||
export class DataTableInfoValueDirective {
|
||||
templateRef = inject(TemplateRef);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
shared-data-table {
|
||||
@apply table;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>>;
|
||||
}
|
||||
@@ -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 {}
|
||||
5
apps/isa-app/src/shared/components/data-table/index.ts
Normal file
5
apps/isa-app/src/shared/components/data-table/index.ts
Normal 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';
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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 {}
|
||||
1
apps/isa-app/src/shared/components/product/index.ts
Normal file
1
apps/isa-app/src/shared/components/product/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './product-thumbnail.component';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<img [src]="thumbnailUrl() | async" [alt]="name()" loading="lazy" class="product-thumbnail" />
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './quantity-selector.component';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
3
apps/isa-app/src/shared/pipes/enum/index.ts
Normal file
3
apps/isa-app/src/shared/pipes/enum/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './payment-status.pipe';
|
||||
export * from './payment-type.pipe';
|
||||
export * from './receipt-type.pipe';
|
||||
18
apps/isa-app/src/shared/pipes/enum/payment-status.pipe.ts
Normal file
18
apps/isa-app/src/shared/pipes/enum/payment-status.pipe.ts
Normal 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()];
|
||||
}
|
||||
}
|
||||
18
apps/isa-app/src/shared/pipes/enum/payment-type.pipe.ts
Normal file
18
apps/isa-app/src/shared/pipes/enum/payment-type.pipe.ts
Normal 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()];
|
||||
}
|
||||
}
|
||||
18
apps/isa-app/src/shared/pipes/enum/receipt-type.pipe.ts
Normal file
18
apps/isa-app/src/shared/pipes/enum/receipt-type.pipe.ts
Normal 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()];
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user