Merged PR 1568: #3996 Customer Order Details and History

#3996 Customer Order Details and History
This commit is contained in:
Nino Righi
2023-06-28 16:03:23 +00:00
committed by Lorenz Hilpert
parent ed144f0a15
commit 874f8f4758
41 changed files with 2555 additions and 200 deletions

View File

@@ -0,0 +1,349 @@
<ng-container *ngIf="orderItem$ | async; let orderItem">
<div class="grid grid-flow-row gap-px-2">
<div class="bg-[#F5F7FA] flex flex-row justify-between items-center p-4 rounded-t">
<div class="grid grid-flow-col gap-[0.4375rem] items-center" *ngIf="features$ | async; let features; else: featureLoading">
<shared-icon *ngIf="features?.length > 0" [size]="24" icon="person"></shared-icon>
<div class="grid grid-flow-col gap-2 items-center font-bold text-p2" *ngFor="let feature of features">
{{ feature?.description }}
</div>
</div>
<button
[disabled]="editButtonDisabled$ | async"
class="page-customer-order-details-header__edit-cta bg-transparent text-brand font-bold border-none text-p1"
*ngIf="editClick.observers.length"
(click)="editClick.emit(orderItem)"
>
Bearbeiten
</button>
</div>
<div class="page-customer-order-details-header__details bg-white px-4 pt-4 pb-5">
<h2
class="page-customer-order-details-header__details-header items-center"
[class.mb-8]="!orderItem?.features?.paid && !isKulturpass"
>
<div class="text-h2">
{{ orderItem?.organisation }}
<ng-container *ngIf="!!orderItem?.organisation && (!!orderItem?.firstName || !!orderItem?.lastName)"> - </ng-container>
{{ orderItem?.lastName }}
{{ orderItem?.firstName }}
</div>
<div class="page-customer-order-details-header__header-compartment text-h3">
{{ orderItem?.compartmentCode }}{{ orderItem?.compartmentInfo && '_' + orderItem?.compartmentInfo }}
</div>
</h2>
<div class="page-customer-order-details-header__paid-marker mt-[0.375rem]" *ngIf="orderItem?.features?.paid && !isKulturpass">
<div class="font-bold w-fit desktop-small:text-p2 px-3 py-[0.125rem] rounded text-white bg-[#26830C]">
{{ orderItem?.features?.paid }}
</div>
</div>
<div class="page-customer-order-details-header__paid-marker mt-[0.375rem] text-[#26830C]" *ngIf="isKulturpass">
<svg class="fill-current mr-2" xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22">
<path
d="M880-740v520q0 24-18 42t-42 18H140q-24 0-42-18t-18-42v-520q0-24 18-42t42-18h680q24 0 42 18t18 42ZM140-631h680v-109H140v109Zm0 129v282h680v-282H140Zm0 282v-520 520Z"
/>
</svg>
<strong> Bezahlt über KulturPass </strong>
</div>
<div class="page-customer-order-details-header__details-wrapper -mt-3">
<div class="flex flex-row page-customer-order-details-header__buyer-number" data-detail-id="Kundennummer">
<div class="w-[9rem]">Kundennummer</div>
<div class="flex flex-row font-bold">{{ orderItem?.buyerNumber }}</div>
</div>
<div class="flex flex-row page-customer-order-details-header__order-number" data-detail-id="VorgangId">
<div class="w-[9rem]">Vorgang-ID</div>
<div class="flex flex-row font-bold">{{ orderItem?.orderNumber }}</div>
</div>
<div class="flex flex-row page-customer-order-details-header__order-date" data-detail-id="Bestelldatum">
<div class="w-[9rem]">Bestelldatum</div>
<div class="flex flex-row font-bold">{{ orderItem?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
</div>
<div class="flex flex-row page-customer-order-details-header__processing-status justify-between" data-detail-id="Status">
<div class="w-[9rem]">Status</div>
<div *ngIf="!(changeStatusLoader$ | async)" class="flex flex-row font-bold -mr-[0.125rem]">
<shared-icon
class="mr-2 text-black flex items-center justify-center"
[size]="16"
*ngIf="orderItem.processingStatus | processingStatus: 'icon'; let icon"
[icon]="icon"
></shared-icon>
<span *ngIf="!(canEditStatus$ | async)">
{{ orderItem?.processingStatus | processingStatus }}
</span>
<ng-container *ngIf="canEditStatus$ | async">
<button
class="cta-status-dropdown"
[uiOverlayTrigger]="statusDropdown"
[disabled]="changeStatusDisabled$ | async"
#dropdown="uiOverlayTrigger"
>
<div class="mr-[0.375rem]">
{{ orderItem?.processingStatus | processingStatus }}
</div>
<shared-icon
[size]="24"
[class.rotate-0]="!dropdown.opened"
[class.-rotate-180]="dropdown.opened"
icon="arrow-drop-down"
></shared-icon>
</button>
<ui-dropdown #statusDropdown yPosition="below" xPosition="after" [xOffset]="8">
<button uiDropdownItem *ngFor="let action of statusActions$ | async" (click)="handleActionClick(action)">
{{ action.label }}
</button>
</ui-dropdown>
</ng-container>
</div>
<ui-spinner *ngIf="changeStatusLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"></ui-spinner>
</div>
<div class="flex flex-row page-customer-order-details-header__order-source" data-detail-id="Bestellkanal">
<div class="w-[9rem]">Bestellkanal</div>
<div class="flex flex-row font-bold">{{ order?.features?.orderSource }}</div>
</div>
<div
class="flex flex-row page-customer-order-details-header__change-date justify-between"
[ngSwitch]="orderItem.processingStatus"
data-detail-id="Geaendert"
>
<!-- orderType 1 === Abholung / Rücklage; orderType 2 === Versand / B2B-Versand; orderType 4 === Download (ist manchmal auch orderType 2) -->
<ng-container *ngIf="orderItem.orderType === 1; else changeDate">
<ng-container *ngSwitchCase="16">
<ng-container *ngTemplateOutlet="vslLieferdatum"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="8192">
<ng-container *ngTemplateOutlet="vslLieferdatum"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="128">
<ng-container *ngTemplateOutlet="abholfrist"></ng-container>
</ng-container>
</ng-container>
<ng-template #changeDate>
<div class="w-[9rem]">Geändert</div>
<div class="flex flex-row font-bold">{{ orderItem?.processingStatusDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
</ng-template>
</div>
<div
class="flex flex-row page-customer-order-details-header__pick-up justify-between"
data-detail-id="Wunschdatum"
*ngIf="orderItem.orderType === 1 && (orderItem.processingStatus === 16 || orderItem.processingStatus === 8192)"
>
<ng-container *ngTemplateOutlet="preferredPickUpDate"></ng-container>
</div>
<div class="flex flex-col page-customer-order-details-header__dig-and-notification">
<div
*ngIf="orderItem.orderType === 1"
class="flex flex-row page-customer-order-details-header__notification"
data-detail-id="Benachrichtigung"
>
<div class="w-[9rem]">Benachrichtigung</div>
<div class="flex flex-row font-bold">{{ (notificationsChannel | notificationsChannel) || '-' }}</div>
</div>
<div
*ngIf="!!digOrderNumber && orderItem.orderType !== 1"
class="flex flex-row page-customer-order-details-header__dig-number"
data-detail-id="Dig-Bestellnummer"
>
<div class="w-[9rem]">Dig-Bestell Nr.</div>
<div class="flex flex-row font-bold">{{ digOrderNumber }}</div>
</div>
</div>
</div>
</div>
<div class="page-customer-order-details-header__select" *ngIf="showMultiselect$ | async">
<button class="cta-select-all" (click)="selectAll()">Alle auswählen</button>
{{ selectedOrderItemCount$ | async }} von {{ orderItemCount$ | async }} Titeln
</div>
<div class="flex flex-row items-center relative bg-[#F5F7FA] p-4 rounded-t">
<div *ngIf="showFeature" class="flex flex-row items-center mr-3">
<ng-container [ngSwitch]="order.features.orderType">
<ng-container *ngSwitchCase="'Versand'">
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
<shared-icon [size]="24" icon="isa-truck"></shared-icon>
</div>
<p class="font-bold text-p1">Versand</p>
</ng-container>
<ng-container *ngSwitchCase="'DIG-Versand'">
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
<shared-icon [size]="24" icon="isa-truck"></shared-icon>
</div>
<p class="font-bold text-p1">Versand</p>
</ng-container>
<ng-container *ngSwitchCase="'B2B-Versand'">
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
<shared-icon [size]="24" icon="isa-b2b-truck"></shared-icon>
</div>
<p class="font-bold text-p1">B2B-Versand</p>
</ng-container>
<ng-container *ngSwitchCase="'Abholung'">
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
<shared-icon [size]="24" icon="isa-box-out"></shared-icon>
</div>
<p class="font-bold text-p1 mr-3">Abholung</p>
{{ orderItem.targetBranch }}
</ng-container>
<ng-container *ngSwitchCase="'Rücklage'">
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
<shared-icon [size]="24" icon="isa-shopping-bag"></shared-icon>
</div>
<p class="font-bold text-p1">Rücklage</p>
</ng-container>
<ng-container *ngSwitchCase="'Download'">
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
<shared-icon [size]="24" icon="isa-download"></shared-icon>
</div>
<p class="font-bold text-p1">Download</p>
</ng-container>
</ng-container>
</div>
<div class="page-customer-order-details-header__additional-addresses" *ngIf="showAddresses">
<button (click)="openAddresses = !openAddresses" class="text-[#0556B4]">
Lieferadresse / Rechnungsadresse {{ openAddresses ? 'ausblenden' : 'anzeigen' }}
</button>
<div class="page-customer-order-details-header__addresses-popover" *ngIf="openAddresses">
<button (click)="openAddresses = !openAddresses" class="close">
<shared-icon icon="close" [size]="24"></shared-icon>
</button>
<div class="page-customer-order-details-header__addresses-popover-data">
<div *ngIf="order.shipping" class="page-customer-order-details-header__addresses-popover-delivery">
<p>Lieferadresse</p>
<div class="page-customer-order-details-header__addresses-popover-delivery-data">
<ng-container *ngIf="order.shipping?.data?.organisation">
<p>{{ order.shipping?.data?.organisation?.name }}</p>
<p>{{ order.shipping?.data?.organisation?.department }}</p>
</ng-container>
<p>{{ order.shipping?.data?.firstName }} {{ order.shipping?.data?.lastName }}</p>
<p>{{ order.shipping?.data?.address?.street }} {{ order.shipping?.data?.address?.streetNumber }}</p>
<p>{{ order.shipping?.data?.address?.zipCode }} {{ order.shipping?.data?.address?.city }}</p>
<p>{{ order.shipping?.data?.address?.info }}</p>
</div>
</div>
<div *ngIf="order.billing" class="page-customer-order-details-header__addresses-popover-billing">
<p>Rechnungsadresse</p>
<div class="page-customer-order-details-header__addresses-popover-billing-data">
<ng-container *ngIf="order.billing?.data?.organisation">
<p>{{ order.billing?.data?.organisation?.name }}</p>
<p>{{ order.billing?.data?.organisation?.department }}</p>
</ng-container>
<p>{{ order.billing?.data?.firstName }} {{ order.billing?.data?.lastName }}</p>
<p>{{ order.billing?.data?.address?.street }} {{ order.billing?.data?.address?.streetNumber }}</p>
<p>{{ order.billing?.data?.address?.zipCode }} {{ order.billing?.data?.address?.city }}</p>
<p>{{ order.billing?.data?.address?.info }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<ng-template #featureLoading>
<div class="fetch-wrapper">
<div class="fetching"></div>
<div class="fetching"></div>
<div class="fetching"></div>
</div>
</ng-template>
<ng-template #abholfrist>
<div class="w-[9rem]">Abholfrist</div>
<div *ngIf="!(changeDateLoader$ | async)" class="flex flex-row font-bold">
<button
[uiOverlayTrigger]="deadlineDatepicker"
#deadlineDatepickerTrigger="uiOverlayTrigger"
[disabled]="!isKulturpass && (!!orderItem?.features?.paid || (changeDateDisabled$ | async))"
class="cta-pickup-deadline"
>
<strong class="border-r border-[#AEB7C1] pr-4">
{{ orderItem?.pickUpDeadline | date: 'dd.MM.yy' }}
</strong>
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
</button>
<ui-datepicker
#deadlineDatepicker
yPosition="below"
xPosition="after"
[xOffset]="8"
[min]="minDateDatepicker"
[disabledDaysOfWeek]="[0]"
[selected]="orderItem?.pickUpDeadline"
(save)="updatePickupDeadline($event)"
>
</ui-datepicker>
</div>
<ui-spinner *ngIf="changeDateLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"></ui-spinner>
</ng-template>
<ng-template #preferredPickUpDate>
<div class="w-[9rem]">Zurücklegen bis</div>
<div *ngIf="!(changePreferredDateLoader$ | async)" class="flex flex-row font-bold">
<button
[uiOverlayTrigger]="preferredPickUpDatePicker"
#preferredPickUpDatePickerTrigger="uiOverlayTrigger"
[disabled]="(!isKulturpass && !!orderItem?.features?.paid) || (changeDateDisabled$ | async)"
class="cta-pickup-preferred"
>
<strong *ngIf="preferredPickUpDate$ | async; let pickUpDate; else: selectTemplate">
{{ pickUpDate | date: 'dd.MM.yy' }}
</strong>
<ng-template #selectTemplate>
<strong class="border-r border-[#AEB7C1] pr-4">Auswählen</strong>
</ng-template>
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
</button>
<ui-datepicker
#preferredPickUpDatePicker
yPosition="below"
xPosition="after"
[xOffset]="8"
[min]="minDateDatepicker"
[disabledDaysOfWeek]="[0]"
[selected]="(preferredPickUpDate$ | async) || today"
(save)="updatePreferredPickUpDate($event)"
>
</ui-datepicker>
</div>
<ui-spinner *ngIf="changePreferredDateLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"> </ui-spinner>
</ng-template>
<ng-template #vslLieferdatum>
<div class="w-[9rem]">vsl. Lieferdatum</div>
<div *ngIf="!(changeDateLoader$ | async)" class="flex flex-row font-bold">
<button
class="cta-datepicker"
[disabled]="changeDateDisabled$ | async"
[uiOverlayTrigger]="uiDatepicker"
#datepicker="uiOverlayTrigger"
>
<span class="border-r border-[#AEB7C1] pr-4">
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
</span>
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
</button>
<ui-datepicker
#uiDatepicker
yPosition="below"
xPosition="after"
[xOffset]="8"
[min]="minDateDatepicker"
[disabledDaysOfWeek]="[0]"
[selected]="orderItem?.estimatedShippingDate"
(save)="updateEstimatedShippingDate($event)"
>
</ui-datepicker>
</div>
<ui-spinner *ngIf="changeDateLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"></ui-spinner>
</ng-template>
</ng-container>

View File

@@ -0,0 +1,140 @@
:host {
@apply block mb-[0.125rem];
}
.page-customer-order-details-header__edit-cta {
&:disabled {
@apply text-inactive-customer;
}
}
.page-customer-order-details-header__paid-marker {
@apply font-bold flex items-center justify-end;
shared-icon {
@apply mr-2 text-white bg-green-600 w-px-25 h-px-25 flex items-center justify-center rounded-full;
}
}
.page-customer-order-details-header__details {
@apply grid grid-flow-row pt-4;
}
.page-customer-order-details-header__details-header {
@apply flex flex-row justify-between font-bold;
}
.page-customer-order-details-header__details-wrapper {
@apply grid grid-flow-row gap-x-6 gap-y-[0.375rem];
grid-template-columns: 50% auto;
grid-template-areas:
'buyernumber .'
'ordernumber .'
'orderdate processingstatus'
'ordersource changedate'
'dignotification pickup';
.detail {
shared-icon {
@apply flex items-center;
}
.loader {
width: 130px;
}
}
}
.page-customer-order-details-header__buyer-number {
grid-area: buyernumber;
}
.page-customer-order-details-header__order-number {
grid-area: ordernumber;
}
.page-customer-order-details-header__order-date {
grid-area: orderdate;
}
.page-customer-order-details-header__processing-status {
grid-area: processingstatus;
}
.page-customer-order-details-header__order-source {
grid-area: ordersource;
}
.page-customer-order-details-header__change-date {
grid-area: changedate;
}
.page-customer-order-details-header__pick-up {
grid-area: pickup;
}
.page-customer-order-details-header__dig-and-notification {
grid-area: dignotification;
}
.cta-status-dropdown,
.cta-datepicker,
.cta-pickup-deadline,
.cta-pickup-preferred {
@apply flex flex-row border-none outline-none text-p2 font-bold bg-transparent items-center px-0 mx-0;
&:disabled {
@apply text-disabled-customer cursor-not-allowed;
shared-icon {
@apply text-disabled-customer;
}
}
}
.page-customer-order-details-header__select {
@apply flex flex-col items-end;
}
.page-customer-order-details-header__additional-addresses {
.page-customer-order-details-header__addresses-popover {
@apply absolute inset-x-0 top-16 bottom-0 z-popover;
.close {
@apply bg-white absolute right-0 p-6;
}
.page-customer-order-details-header__addresses-popover-data {
@apply flex flex-col bg-white p-6 z-popover min-h-[200px];
box-shadow: 0px 6px 24px rgba(206, 212, 219, 0.8);
.page-customer-order-details-header__addresses-popover-delivery {
@apply grid mb-6;
grid-template-columns: 153px auto;
.page-customer-order-details-header__addresses-popover-delivery-data {
p {
@apply font-bold;
}
}
}
.page-customer-order-details-header__addresses-popover-billing {
@apply grid;
grid-template-columns: 153px auto;
.page-customer-order-details-header__addresses-popover-billing-data {
p {
@apply font-bold;
}
}
}
}
}
}
.cta-select-all {
@apply text-brand bg-transparent text-p2 font-bold outline-none border-none px-4 py-4 -mr-4;
}
.fetch-wrapper {
@apply grid grid-flow-col gap-4;
}
.fetching {
@apply w-24 h-px-20 bg-customer;
animation: load 0.75s linear infinite;
}

View File

@@ -0,0 +1,217 @@
import {
Component,
ChangeDetectionStrategy,
ChangeDetectorRef,
Output,
EventEmitter,
Input,
OnChanges,
SimpleChanges,
} from '@angular/core';
import { CrmCustomerService } from '@domain/crm';
import { DomainOmsService } from '@domain/oms';
import { NotificationChannel } from '@swagger/checkout';
import { KeyValueDTOOfStringAndString, OrderDTO, OrderItemListItemDTO } from '@swagger/oms';
import { DateAdapter } from '@ui/common';
import { cloneDeep } from 'lodash';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
import { CustomerOrderDetailsStore } from '../customer-order-details.store';
@Component({
selector: 'page-customer-order-details-header',
templateUrl: 'customer-order-details-header.component.html',
styleUrls: ['customer-order-details-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerOrderDetailsHeaderComponent implements OnChanges {
@Output()
editClick = new EventEmitter<OrderItemListItemDTO>();
@Output()
handleAction = new EventEmitter<KeyValueDTOOfStringAndString>();
@Input()
order: OrderDTO;
get isKulturpass() {
return this.order?.features?.orderSource === 'KulturPass';
}
minDateDatepicker = this.dateAdapter.addCalendarDays(this.dateAdapter.today(), -1);
today = this.dateAdapter.today();
selectedOrderItemCount$ = this._store.selectedeOrderItemSubsetIds$.pipe(map((ids) => ids?.length ?? 0));
orderItemCount$ = this._store.items$.pipe(map((items) => items?.length ?? 0));
orderItem$ = this._store.items$.pipe(map((orderItems) => orderItems?.find((_) => true)));
preferredPickUpDate$ = new BehaviorSubject<Date>(undefined);
notificationsChannel: NotificationChannel = 0;
changeDateLoader$ = new BehaviorSubject<boolean>(false);
changePreferredDateLoader$ = new BehaviorSubject<boolean>(false);
changeStatusLoader$ = new BehaviorSubject<boolean>(false);
changeStatusDisabled$ = this._store.changeActionDisabled$;
changeDateDisabled$ = this.changeStatusDisabled$;
features$ = this.orderItem$.pipe(
filter((orderItem) => !!orderItem),
switchMap((orderItem) =>
this.customerService.getCustomers(orderItem.buyerNumber).pipe(
map((res) => res.result.find((c) => c.customerNumber === orderItem.buyerNumber)),
map((customer) => customer?.features || []),
map((features) => features.filter((f) => f.enabled && !!f.description))
)
),
shareReplay()
);
statusActions$ = this.orderItem$.pipe(map((orderItem) => orderItem?.actions?.filter((action) => action.enabled === false)));
showMultiselect$ = combineLatest([this._store.items$, this._store.fetchPartial$, this._store.itemsSelectable$]).pipe(
map(([orderItems, fetchPartial, multiSelect]) => multiSelect && fetchPartial && orderItems?.length > 1)
);
crudaUpdate$ = this.orderItem$.pipe(map((orederItem) => !!(orederItem?.cruda & 4)));
editButtonDisabled$ = combineLatest([this.changeStatusLoader$, this.crudaUpdate$]).pipe(
map(([changeStatusLoader, crudaUpdate]) => changeStatusLoader || !crudaUpdate)
);
canEditStatus$ = combineLatest([this.statusActions$, this.crudaUpdate$]).pipe(
map(([statusActions, crudaUpdate]) => statusActions?.length > 0 && crudaUpdate)
);
openAddresses: boolean = false;
get digOrderNumber(): string {
return this.order?.linkedRecords?.find((_) => true)?.number;
}
get showAddresses(): boolean {
return (this.order?.orderType === 2 || this.order?.orderType === 4) && (!!this.order?.shipping || !!this.order?.billing);
}
get showFeature(): boolean {
return !!this.order?.features && !!this.order?.features?.orderType;
}
constructor(
private _store: CustomerOrderDetailsStore,
private customerService: CrmCustomerService,
private dateAdapter: DateAdapter,
private omsService: DomainOmsService,
private cdr: ChangeDetectorRef
) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes.order) {
this.findLatestPreferredPickUpDate();
this.computeNotificationChannel();
}
}
computeNotificationChannel() {
const order = this.order;
this.notificationsChannel = order?.notificationChannels ?? 0;
this.cdr.markForCheck();
}
async updatePickupDeadline(deadline: Date) {
this.changeDateLoader$.next(true);
this.changeStatusDisabled$.next(true);
const orderItems = cloneDeep(this._store.items);
for (const item of orderItems) {
if (this.dateAdapter.compareDate(deadline, new Date(item.pickUpDeadline)) !== 0) {
try {
const res = await this.omsService
.setPickUpDeadline(item.orderId, item.orderItemId, item.orderItemSubsetId, deadline?.toISOString())
.pipe(first())
.toPromise();
item.pickUpDeadline = deadline.toISOString();
} catch (error) {
console.error(error);
}
}
}
this.changeDateLoader$.next(false);
this.changeStatusDisabled$.next(false);
this._store.updateOrderItems(orderItems);
}
async updateEstimatedShippingDate(estimatedShippingDate: Date) {
this.changeDateLoader$.next(true);
this.changeStatusDisabled$.next(true);
const orderItems = cloneDeep(this._store.items);
for (const item of orderItems) {
if (this.dateAdapter.compareDate(estimatedShippingDate, new Date(item.pickUpDeadline)) !== 0) {
try {
const res = await this.omsService
.setEstimatedShippingDate(item.orderId, item.orderItemId, item.orderItemSubsetId, estimatedShippingDate?.toISOString())
.pipe(first())
.toPromise();
item.estimatedShippingDate = res.estimatedShippingDate;
} catch (error) {
console.error(error);
}
}
}
this.changeDateLoader$.next(false);
this.changeStatusDisabled$.next(false);
this._store.updateOrderItems(orderItems);
}
async handleActionClick(action: KeyValueDTOOfStringAndString) {
this.changeStatusLoader$.next(true);
this.handleAction.emit(action);
setTimeout(() => this.changeStatusLoader$.next(false), 1000);
this.cdr.markForCheck();
}
selectAll() {
this._store.selectAllOrderItems();
}
async updatePreferredPickUpDate(date: Date) {
this.changePreferredDateLoader$.next(true);
this.changeStatusDisabled$.next(true);
const orderItems = cloneDeep(this._store.items);
const data: Record<string, string> = {};
orderItems.forEach((item) => {
data[`${item.orderItemSubsetId}`] = date?.toISOString();
});
try {
await this.omsService.setPreferredPickUpDate({ data }).toPromise();
this.order.items.forEach((item) => {
item.data.subsetItems.forEach((subsetItem) => (subsetItem.data.preferredPickUpDate = date.toISOString()));
});
this.findLatestPreferredPickUpDate();
} catch (error) {
console.error(error);
}
this.changePreferredDateLoader$.next(false);
this.changeStatusDisabled$.next(false);
}
findLatestPreferredPickUpDate() {
let latestDate;
const subsetItems = this.order?.items
?.reduce((acc, item) => {
return [...acc, ...item.data.subsetItems];
}, [])
?.filter((a) => !!a.data.preferredPickUpDate);
if (subsetItems?.length > 0) {
latestDate = new Date(
subsetItems?.reduce((a, b) => {
return new Date(a.data.preferredPickUpDate) > new Date(b.data.preferredPickUpDate) ? a : b;
})?.data?.preferredPickUpDate
);
}
this.preferredPickUpDate$.next(latestDate);
}
}

View File

@@ -0,0 +1,3 @@
// start:ng42.barrel
export * from './customer-order-details-header.component';
// end:ng42.barrel

View File

@@ -0,0 +1,226 @@
<ng-container *ngIf="orderItem$ | async; let orderItem">
<div #features class="page-customer-order-details-item__features">
<ng-container *ngIf="orderItem?.features?.prebooked">
<img [uiOverlayTrigger]="prebookedTooltip" src="/assets/images/tag_icon_preorder.svg" [alt]="orderItem?.features?.prebooked" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #prebookedTooltip [closeable]="true">
Artikel wird für Sie vorgemerkt.
</ui-tooltip>
</ng-container>
<ng-container *ngIf="notificationsSent$ | async; let notificationsSent">
<ng-container *ngIf="notificationsSent?.NOTIFICATION_EMAIL">
<img [uiOverlayTrigger]="emailTooltip" src="/assets/images/email_bookmark.svg" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #emailTooltip [closeable]="true">
Per E-Mail benachrichtigt <br />
<ng-container *ngFor="let notification of notificationsSent?.NOTIFICATION_EMAIL">
{{ notification | date: 'dd.MM.yyyy | HH:mm' }} Uhr<br />
</ng-container>
</ui-tooltip>
</ng-container>
<ng-container *ngIf="notificationsSent?.NOTIFICATION_SMS">
<img [uiOverlayTrigger]="smsTooltip" src="/assets/images/sms_bookmark.svg" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #smsTooltip [closeable]="true">
Per SMS benachrichtigt <br />
<ng-container *ngFor="let notification of notificationsSent?.NOTIFICATION_SMS">
{{ notification | date: 'dd.MM.yyyy | HH:mm' }} Uhr<br />
</ng-container>
</ui-tooltip>
</ng-container>
</ng-container>
</div>
<div class="page-customer-order-details-item__item-container">
<div class="page-customer-order-details-item__thumbnail">
<img [src]="orderItem.product?.ean | productImage" [alt]="orderItem.product?.name" />
</div>
<div class="page-customer-order-details-item__details">
<div class="flex flex-row justify-between items-start mb-[1.3125rem]">
<h3
[uiElementDistance]="features"
#elementDistance="uiElementDistance"
[style.max-width.px]="elementDistance.distanceChange | async"
class="flex flex-col"
>
<div class="font-normal mb-[0.375rem]">{{ orderItem.product?.contributors }}</div>
<div>{{ orderItem.product?.name }}</div>
</h3>
<div class="history-wrapper">
<button class="cta-history text-p1" (click)="historyClick.emit(orderItem)">
Historie
</button>
</div>
</div>
<div class="detail">
<div class="label">Menge</div>
<div class="value">
<ng-container *ngIf="!(canChangeQuantity$ | async)"> {{ orderItem?.quantity }}x </ng-container>
<ui-quantity-dropdown
*ngIf="canChangeQuantity$ | async"
[showTrash]="false"
[range]="orderItem?.quantity"
[(ngModel)]="quantity"
[showSpinner]="false"
>
</ui-quantity-dropdown>
<span class="overall-quantity">(von {{ orderItem?.overallQuantity }})</span>
</div>
</div>
<div class="detail" *ngIf="!!orderItem.product?.formatDetail">
<div class="label">Format</div>
<div class="value">
<img
*ngIf="orderItem?.product?.format && orderItem?.product?.format !== 'UNKNOWN'"
class="format-icon"
[src]="'/assets/images/Icon_' + orderItem.product?.format + '.svg'"
alt="format icon"
/>
<span>{{ orderItem.product?.formatDetail }}</span>
</div>
</div>
<div class="detail" *ngIf="!!orderItem.product?.ean">
<div class="label">ISBN/EAN</div>
<div class="value">{{ orderItem.product?.ean }}</div>
</div>
<div class="detail" *ngIf="!!orderItem.price">
<div class="label">Preis</div>
<div class="value">{{ orderItem.price | currency: 'EUR' }}</div>
</div>
<div class="detail" *ngIf="!!orderItem.retailPrice?.vat?.inPercent">
<div class="label">MwSt</div>
<div class="value">{{ orderItem.retailPrice?.vat?.inPercent }}%</div>
</div>
<hr class="border-[#EDEFF0] border-t-2 my-4" />
<div class="detail" *ngIf="orderItem.supplier">
<div class="label">Lieferant</div>
<div class="value">{{ orderItem.supplier }}</div>
</div>
<div class="detail" *ngIf="!!orderItem.ssc || !!orderItem.sscText">
<div class="label">Meldenummer</div>
<div class="value">{{ orderItem.ssc }} - {{ orderItem.sscText }}</div>
</div>
<div class="detail" *ngIf="!!orderItem.targetBranch">
<div class="label">Zielfiliale</div>
<div class="value">{{ orderItem.targetBranch }}</div>
</div>
<div class="detail">
<div class="label">
<ng-container
*ngIf="
orderItemFeature(orderItem) === 'Versand' ||
orderItemFeature(orderItem) === 'B2B-Versand' ||
orderItemFeature(orderItem) === 'DIG-Versand'
"
>{{ orderItem?.estimatedDelivery ? 'Lieferung zwischen' : 'Lieferung ab' }}</ng-container
>
<ng-container *ngIf="orderItemFeature(orderItem) === 'Abholung' || orderItemFeature(orderItem) === 'Rücklage'">
Abholung ab
</ng-container>
</div>
<ng-container *ngIf="!!orderItem?.estimatedDelivery || !!orderItem?.estimatedShippingDate">
<div class="value bg-[#D8DFE5] rounded w-max px-2">
<ng-container *ngIf="!!orderItem?.estimatedDelivery; else estimatedShippingDate">
{{ orderItem?.estimatedDelivery?.start | date: 'dd.MM.yy' }} und
{{ orderItem?.estimatedDelivery?.stop | date: 'dd.MM.yy' }}
</ng-container>
</div>
</ng-container>
<ng-template #estimatedShippingDate>
<ng-container *ngIf="!!orderItem?.estimatedShippingDate">
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
</ng-container>
</ng-template>
</div>
<hr class="border-[#EDEFF0] border-t-2 my-4" />
<div class="detail" *ngIf="!!orderItem?.compartmentCode">
<div class="label">Abholfachnr.</div>
<div class="value">{{ orderItem?.compartmentCode }}</div>
</div>
<div class="detail">
<div class="label">Vormerker</div>
<div class="value">{{ orderItem.isPrebooked ? 'Ja' : 'Nein' }}</div>
</div>
<hr class="border-[#EDEFF0] border-t-2 my-4" />
<div class="detail" *ngIf="!!orderItem.paymentProcessing">
<div class="label">Zahlungsweg</div>
<div class="value">{{ orderItem.paymentProcessing || '-' }}</div>
</div>
<div class="detail" *ngIf="!!orderItem.paymentType">
<div class="label">Zahlungsart</div>
<div class="value">{{ orderItem.paymentType | paymentType }}</div>
</div>
<h4 class="receipt-header" *ngIf="receiptCount$ | async; let count">
{{ count > 1 ? 'Belege' : 'Beleg' }}
</h4>
<ng-container *ngFor="let receipt of receipts$ | async">
<div class="detail" *ngIf="!!receipt?.receiptNumber">
<div class="label">Belegnummer</div>
<div class="value">{{ receipt?.receiptNumber }}</div>
</div>
<div class="detail" *ngIf="!!receipt?.printedDate">
<div class="label">Erstellt am</div>
<div class="value">{{ receipt?.printedDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
</div>
<div class="detail" *ngIf="!!receipt?.receiptText">
<div class="label">Rechnungstext</div>
<div class="value">{{ receipt?.receiptText || '-' }}</div>
</div>
<div class="detail" *ngIf="!!receipt?.receiptType">
<div class="label">Belegart</div>
<div class="value">
{{ receipt?.receiptType === 1 ? 'Lieferschein' : receipt?.receiptType === 64 ? 'Zahlungsbeleg' : '-' }}
</div>
</div>
</ng-container>
<div class="page-customer-order-details-item__comment flex flex-col items-start mt-[1.625rem]">
<div class="label mb-[0.375rem]">Anmerkung</div>
<div class="flex flex-row w-full">
<textarea
matInput
cdkTextareaAutosize
#autosize="cdkTextareaAutosize"
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="5"
#specialCommentInput
(keydown.delete)="triggerResize()"
(keydown.backspace)="triggerResize()"
type="text"
name="comment"
placeholder="Eine Anmerkung hinzufügen"
[formControl]="specialCommentControl"
[class.inactive]="!specialCommentControl.dirty"
></textarea>
<div class="comment-actions">
<button
type="reset"
class="clear"
*ngIf="!!specialCommentControl.value?.length"
(click)="specialCommentControl.setValue(''); saveSpecialComment(); triggerResize()"
>
<shared-icon icon="close" [size]="24"></shared-icon>
</button>
<button
class="cta-save"
type="submit"
*ngIf="specialCommentControl?.enabled && specialCommentControl.dirty"
(click)="saveSpecialComment()"
>
Speichern
</button>
</div>
</div>
</div>
<ui-select-bullet *ngIf="selectable$ | async" [ngModel]="selected$ | async" (ngModelChange)="setSelected($event)"> </ui-select-bullet>
</div>
</div>
</ng-container>

View File

@@ -0,0 +1,123 @@
:host {
@apply grid grid-flow-row gap-0.5 relative;
}
button {
@apply px-1;
}
.page-customer-order-details-item__features {
@apply absolute grid grid-flow-col gap-2 -top-1 right-24;
img {
@apply w-12 h-12;
z-index: 1;
}
}
.page-customer-order-details-item__item-container {
@apply grid gap-8 bg-white p-4;
grid-template-columns: auto 1fr;
}
.page-customer-order-details-item__thumbnail {
img {
@apply rounded shadow-cta w-[3.625rem] h-[5.9375rem];
}
}
.page-customer-order-details-item__details {
@apply relative;
h3 {
@apply mt-0 font-bold;
}
.detail {
@apply grid gap-x-7 my-1;
grid-template-columns: auto 1fr auto;
.label {
@apply w-[8.125rem];
}
.value {
@apply flex flex-row items-center font-bold;
.format-icon {
@apply mr-2;
}
}
.overall-quantity {
@apply ml-2;
}
}
}
.page-customer-order-details-item__comment {
textarea {
@apply w-full flex-grow rounded bg-[#EDEFF0] border-[#AEB7C1] border border-solid outline-none text-p2 p-4;
resize: none;
height: auto !important;
}
textarea.inactive {
@apply text-warning font-bold;
@apply w-full flex-grow rounded bg-[#EDEFF0] border-[#AEB7C1] border border-solid outline-none text-p2 p-4 text-warning font-bold;
// ipad color fix
-webkit-text-fill-color: rgb(190, 129, 0);
opacity: 1;
}
textarea::placeholder,
textarea.inactive::placeholder {
@apply text-[#89949E] font-normal;
-webkit-text-fill-color: #89949e;
}
input {
@apply flex-grow bg-transparent border-none outline-none text-p2 mx-4;
}
input.inactive {
@apply text-warning font-bold;
@apply flex-grow bg-transparent border-none outline-none text-p2 mx-4 text-warning font-bold;
// ipad color fix
-webkit-text-fill-color: rgb(190, 129, 0);
opacity: 1;
}
button {
@apply bg-transparent text-brand font-bold text-p2 outline-none border-none;
}
button.clear {
@apply text-black;
}
.comment-actions {
@apply flex justify-center items-center;
}
}
.history-wrapper {
@apply text-right;
}
.cta-history {
@apply font-bold text-brand outline-none border-none bg-transparent -mr-1;
}
.cta-edit,
.cta-save {
@apply -mr-1;
}
ui-select-bullet {
@apply absolute top-20 right-0 p-5;
}
.receipt-header {
@apply mb-1;
}

View File

@@ -0,0 +1,237 @@
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import {
Component,
ChangeDetectionStrategy,
Input,
Output,
EventEmitter,
ChangeDetectorRef,
OnDestroy,
OnInit,
ViewChild,
} from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import { DomainOmsService, DomainReceiptService } from '@domain/oms';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { OrderDTO, OrderItemListItemDTO, ReceiptDTO, ReceiptType } from '@swagger/oms';
import { isEqual } from 'lodash';
import { combineLatest, NEVER, Subject, Observable } from 'rxjs';
import { catchError, filter, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
import { CustomerOrderDetailsStore } from '../customer-order-details.store';
export interface CustomerOrderDetailsItemComponentState {
orderItem?: OrderItemListItemDTO;
order?: OrderDTO;
quantity?: number;
receipts?: ReceiptDTO[];
selected?: boolean;
more: boolean;
}
@Component({
selector: 'page-customer-order-details-item',
templateUrl: 'customer-order-details-item.component.html',
styleUrls: ['customer-order-details-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerOrderDetailsItemComponent extends ComponentStore<CustomerOrderDetailsItemComponentState> implements OnInit, OnDestroy {
@ViewChild('autosize') autosize: CdkTextareaAutosize;
@Output()
historyClick = new EventEmitter<OrderItemListItemDTO>();
@Output()
specialCommentChanged = new EventEmitter<void>();
@Input()
get orderItem() {
return this.get((s) => s.orderItem);
}
set orderItem(orderItem: OrderItemListItemDTO) {
if (!isEqual(this.orderItem, orderItem)) {
// Remove Prev OrderItem from selected list
this._store.selectOrderItem(this.orderItem, false);
this.patchState({ orderItem, quantity: orderItem?.quantity, receipts: [], more: false });
this.specialCommentControl.reset(orderItem?.specialComment);
// Add New OrderItem to selected list if selected was set to true by its input
if (this.get((s) => s.selected)) {
this._store.selectOrderItem(this.orderItem, true);
}
}
}
@Input()
get order() {
return this.get((s) => s.order);
}
set order(order: OrderDTO) {
this.patchState({ order });
}
readonly orderItem$ = this.select((s) => s.orderItem);
notificationsSent$ = this.orderItem$.pipe(
filter((oi) => !!oi),
switchMap((oi) =>
this._omsService
.getCompletedTasks({ orderId: oi.orderId, orderItemId: oi.orderItemId, orderItemSubsetId: oi.orderItemSubsetId, take: 4, skip: 0 })
.pipe(catchError(() => NEVER))
)
);
canChangeQuantity$ = combineLatest([this.orderItem$, this._store.fetchPartial$]).pipe(
map(([item, partialPickup]) => ([16, 8192].includes(item?.processingStatus) || partialPickup) && item.quantity > 1)
);
get quantity() {
return this.get((s) => s.quantity);
}
set quantity(quantity: number) {
if (this.quantity !== quantity) {
this.patchState({ quantity });
}
}
readonly quantity$ = this.select((s) => s.quantity);
@Input()
get selected() {
return this._store.selectedeOrderItemSubsetIds.includes(this.orderItem?.orderItemSubsetId);
}
set selected(selected: boolean) {
if (this.selected !== selected) {
this.patchState({ selected });
this._store.selectOrderItem(this.orderItem, selected);
}
}
readonly selected$ = combineLatest([this.orderItem$, this._store.selectedeOrderItemSubsetIds$]).pipe(
map(([orderItem, selectedItems]) => selectedItems.includes(orderItem?.orderItemSubsetId))
);
@Output()
selectedChange = new EventEmitter<boolean>();
get selectable() {
return this._store.itemsSelectable && this._store.items.length > 1 && this._store.fetchPartial;
}
readonly selectable$ = combineLatest([this._store.items$, this._store.itemsSelectable$, this._store.fetchPartial$]).pipe(
map(([orderItems, selectable, fetchPartial]) => orderItems.length > 1 && selectable && fetchPartial)
);
get receipts() {
return this.get((s) => s.receipts);
}
set receipts(receipts: ReceiptDTO[]) {
if (!isEqual(this.receipts, receipts)) {
this.patchState({ receipts });
this._store.updateReceipts(receipts);
}
}
readonly receipts$ = this.select((s) => s.receipts);
readonly receiptCount$ = this.select((s) => s.receipts?.length);
specialCommentControl = new UntypedFormControl();
more$ = this.select((s) => s.more);
private _onDestroy$ = new Subject();
constructor(
private _store: CustomerOrderDetailsStore,
private _domainReceiptService: DomainReceiptService,
private _omsService: DomainOmsService,
private _cdr: ChangeDetectorRef
) {
super({
more: false,
});
}
ngOnInit() {}
ngOnDestroy() {
// Remove Prev OrderItem from selected list
this._store.selectOrderItem(this.orderItem, false);
this._onDestroy$.next();
this._onDestroy$.complete();
}
loadReceipts = this.effect((done$: Observable<(receipts: ReceiptDTO[]) => void | undefined>) =>
done$.pipe(
withLatestFrom(this.orderItem$),
filter(([_, orderItem]) => !!orderItem),
switchMap(([done, orderItem]) =>
this._domainReceiptService
.getReceipts({
receiptType: 65 as ReceiptType,
ids: [orderItem.orderItemSubsetId],
eagerLoading: 1,
})
.pipe(
tapResponse(
(res) => {
const receipts = res.result.map((r) => r.item3?.data).filter((f) => !!f);
this.receipts = receipts;
if (typeof done === 'function') {
done?.(receipts);
}
},
(err) => {
if (typeof done === 'function') {
done?.([]);
}
},
() => {}
)
)
)
)
);
async saveSpecialComment() {
const { orderId, orderItemId, orderItemSubsetId } = this.orderItem;
try {
this.specialCommentControl.reset(this.specialCommentControl.value);
const res = await this._omsService
.patchComment({ orderId, orderItemId, orderItemSubsetId, specialComment: this.specialCommentControl.value ?? '' })
.pipe(first())
.toPromise();
this.orderItem = { ...this.orderItem, specialComment: this.specialCommentControl.value ?? '' };
this._store.updateOrderItems([this.orderItem]);
this.specialCommentChanged.emit();
} catch (error) {
console.error(error);
}
}
setMore(more: boolean) {
this.patchState({ more });
if (more && this.receipts.length === 0) {
this.loadReceipts(undefined);
}
}
setSelected(selected: boolean) {
this.selected = selected;
this.selectedChange.emit(selected);
this._cdr.markForCheck();
}
orderItemFeature(orderItemListItem: OrderItemListItemDTO) {
const orderItems = this.order?.items;
return orderItems?.find((orderItem) => orderItem.data.id === orderItemListItem.orderItemId)?.data?.features?.orderType;
}
triggerResize() {
this.autosize.reset();
}
}

View File

@@ -0,0 +1,3 @@
// start:ng42.barrel
export * from './customer-order-details-item.component';
// end:ng42.barrel

View File

@@ -0,0 +1,28 @@
<div class="page-customer-order-details-tags__wrapper">
<button
class="page-customer-order-details-tags__tag"
type="button"
[class.selected]="tag === (selected$ | async) && !inputFocus.focused"
*ngFor="let tag of defaultTags"
(click)="setCompartmentInfo(tag)"
>
{{ tag }}
</button>
<button
(click)="inputFocus.focus()"
type="button"
class="page-customer-order-details-tags__tag"
[class.selected]="(inputValue$ | async) === (selected$ | async) && (inputValue$ | async)"
>
<input
#inputFocus="uiFocus"
uiFocus
type="text"
[ngModel]="inputValue$ | async"
(ngModelChange)="inputValueSubject.next($event); setCompartmentInfo(inputValue)"
placeholder="..."
[size]="controlSize$ | async"
maxlength="15"
/>
</button>
</div>

View File

@@ -0,0 +1,40 @@
:host {
@apply block bg-white p-4;
}
.page-customer-order-details-tags__wrapper {
@apply grid grid-flow-col justify-center gap-2 w-auto mx-auto;
}
.page-customer-order-details-tags__tag {
@apply w-auto text-p2 border border-solid bg-white border-inactive-customer py-px-10 px-5 font-bold text-inactive-customer rounded-full;
&:focus:not(.selected) {
@apply bg-white border-inactive-customer text-inactive-customer;
}
&.selected,
&:focus-within {
@apply bg-inactive-customer text-white;
}
input {
@apply border-none outline-none text-p2 font-bold w-auto text-center text-inactive-customer bg-transparent p-0 m-0;
}
input::placeholder {
@apply text-inactive-customer;
}
input:focus {
@apply text-white;
}
input:focus::placeholder {
@apply text-white;
}
&.selected input:not(:focus) {
@apply text-white;
}
}

View File

@@ -0,0 +1,78 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { BehaviorSubject, Subscription } from 'rxjs';
import { first, map } from 'rxjs/operators';
@Component({
selector: 'page-customer-order-details-tags',
templateUrl: 'customer-order-details-tags.component.html',
styleUrls: ['customer-order-details-tags.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CustomerOrderDetailsTagsComponent), multi: true }],
})
export class CustomerOrderDetailsTagsComponent implements OnInit, OnDestroy, ControlValueAccessor {
selected$ = new BehaviorSubject<string>('');
readonly defaultTags = ['Maxi', 'Mini', 'Kleinkram', 'Nonbook', 'Kalender'];
inputValueSubject = new BehaviorSubject<string>('');
inputValue$ = this.inputValueSubject.asObservable();
disabled = false;
private onChange = (value: string) => {};
private onTouched = () => {};
get inputValue() {
return this.inputValueSubject.value;
}
controlSize$ = this.inputValue$.pipe(map((value) => (value ? Math.min(value?.length, 15) : 3)));
private subscription = new Subscription();
constructor(private _cdr: ChangeDetectorRef) {}
writeValue(obj: any): void {
this.selected$.next(obj);
this._cdr.markForCheck();
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
ngOnInit() {
this.subscription.add(
this.selected$.pipe(map((selected) => (this.defaultTags.includes(selected) ? '' : selected))).subscribe(this.inputValueSubject)
);
}
ngOnDestroy() {
this.inputValueSubject.unsubscribe();
this.subscription.unsubscribe();
}
async setCompartmentInfo(compartmentInfo: string) {
const currentCompartmentInfo = await this.selected$.pipe(first()).toPromise();
if (currentCompartmentInfo === compartmentInfo) {
this.onChange(undefined);
this.selected$.next(undefined);
} else {
this.onChange(compartmentInfo);
this.selected$.next(compartmentInfo);
}
this._cdr.markForCheck();
}
}

View File

@@ -0,0 +1,3 @@
// start:ng42.barrel
export * from './customer-order-details-tags.component';
// end:ng42.barrel

View File

@@ -1,11 +1,41 @@
<shared-goods-in-out-order-details (actionHandled)="actionHandled($event)" [itemsSelectable]="hasActions$ | async" [fetchPartial]="true">
<shared-goods-in-out-order-details-header [order]="order$ | async" (editClick)="navigateToEditPage($event)">
</shared-goods-in-out-order-details-header>
<shared-goods-in-out-order-details-item
<div class="mb-4">
<page-customer-order-details-header
(editClick)="navigateToEditPage($event)"
(handleAction)="handleAction($event)"
[order]="order$ | async"
></page-customer-order-details-header>
<page-customer-order-details-item
*ngFor="let item of items$ | async"
[orderItem]="item"
[order]="order$ | async"
[selected]="true"
></shared-goods-in-out-order-details-item>
<shared-goods-in-out-order-details-tags></shared-goods-in-out-order-details-tags>
</shared-goods-in-out-order-details>
(historyClick)="navigateToHistoryPage($event)"
(specialCommentChanged)="updateCustomerOrderResults()"
></page-customer-order-details-item>
<page-customer-order-details-tags *ngIf="showTagsComponent$ | async"></page-customer-order-details-tags>
</div>
<div class="page-customer-order-details__action-wrapper">
<button
[disabled]="addToPreviousCompartmentActionDisabled$ | async"
*ngIf="addToPreviousCompartmentAction$ | async; let action"
class="cta-action shadow-action"
[class.cta-action-primary]="action.selected"
[class.cta-action-secondary]="!action.selected"
(click)="handleAction(action, { compartmentCode: latestCompartmentCode, compartmentInfo: latestCompartmentInfo })"
>
<ui-spinner [show]="(changeActionLoader$ | async) === action.command"
>{{ latestCompartmentCode$ | async | addToPreviousCompartmentCodeLabelPipe }} zubuchen</ui-spinner
>
</button>
<button
[disabled]="actionsDisabled$ | async"
class="cta-action shadow-action"
[class.cta-action-primary]="action.selected"
[class.cta-action-secondary]="!action.selected"
*ngFor="let action of mainActions$ | async"
(click)="handleAction(action)"
>
<ui-spinner [show]="(changeActionLoader$ | async) === action.command">{{ action.label }}</ui-spinner>
</button>
</div>

View File

@@ -1,4 +1,29 @@
:host {
@apply box-border block overflow-y-scroll h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
// max-height: calc(100vh - 305px);
@apply box-border grid overflow-y-scroll h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
}
.page-customer-order-details__action-wrapper {
@apply sticky bottom-4 mt-4 text-center;
.cta-action {
@apply border-2 border-solid border-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap m-2;
}
.fetching {
@apply bg-inactive-branch;
}
.cta-action-primary {
@apply bg-brand text-white;
}
.cta-action-secondary {
@apply bg-white text-brand;
}
button {
&:disabled {
@apply bg-inactive-customer border-inactive-customer text-white cursor-not-allowed;
}
}
}

View File

@@ -1,59 +1,55 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, ContentChildren, OnDestroy, OnInit, QueryList } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { DomainCustomerOrderService, DomainGoodsService, DomainOmsService, OrderItemsContext } from '@domain/oms';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { OrderItemsContext } from '@domain/oms';
import { provideComponentStore } from '@ngrx/component-store';
import { CustomerOrdersNavigationService } from '@shared/services';
import { ListResponseArgsOfOrderItemListItemDTO, OrderItemListItemDTO, OrderItemProcessingStatusValue } from '@swagger/oms';
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
import { combineLatest, Observable, Subject } from 'rxjs';
import { debounceTime, filter, first, map, shareReplay, switchMap, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
export interface CustomerOrderDetailsComponentState {
fetching: boolean;
orderNumber?: string;
buyerNumber?: string;
processingStatus?: OrderItemProcessingStatusValue;
compartmentCode?: string;
items?: OrderItemListItemDTO[];
orderId?: number;
archive?: boolean;
}
import { KeyValueDTOOfStringAndString, OrderItemListItemDTO, OrderItemProcessingStatusValue, ReceiptDTO } from '@swagger/oms';
import { UiErrorModalComponent, UiMessageModalComponent, UiModalService } from '@ui/modal';
import { BehaviorSubject, Subscription, combineLatest } from 'rxjs';
import { first, map } from 'rxjs/operators';
import { CustomerOrderDetailsStore } from './customer-order-details.store';
import { CustomerOrderDetailsItemComponent } from './customer-order-details-item';
import { unionBy } from 'lodash';
import { CommandService } from '@core/command';
@Component({
selector: 'page-customer-order-details',
templateUrl: 'customer-order-details.component.html',
styleUrls: ['customer-order-details.component.scss'],
providers: [provideComponentStore(CustomerOrderDetailsStore)],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerOrderDetailsComponent extends ComponentStore<CustomerOrderDetailsComponentState> implements OnInit, OnDestroy {
orderNumber$ = this.select((s) => decodeURIComponent(s.orderNumber ?? '') || undefined);
export class CustomerOrderDetailsComponent implements OnInit, OnDestroy {
@ContentChildren(CustomerOrderDetailsItemComponent)
customerOrderDetailsItemComponents: QueryList<CustomerOrderDetailsItemComponent>;
buyerNumber$ = this.select((s) => s.buyerNumber);
buyerNumber$ = this._store.buyerNumber$;
compartmentCode$ = this.select((s) => decodeURIComponent(s.compartmentCode ?? '') || undefined);
compartmentCode$ = this._store.compartmentCode$;
processingStatus$ = this.select((s) => s.processingStatus);
latestCompartmentCode$ = this._store.latestCompartmentCode$;
archive$ = this.select((s) => s.archive);
get archive() {
return this.get((s) => s.archive);
}
latestCompartmentCode = this._store.latestCompartmentCode;
items$ = this.select((s) => s.items ?? []);
latestCompartmentInfo = this._store.latestCompartmentInfo;
orderId$ = this.select((s) => s.orderId);
compartmentInfo$ = this._store.compartmentInfo$;
get items() {
return this.get((s) => s.items);
}
compartmentInfo = this._store.compartmentInfo;
order$ = this.orderId$.pipe(
filter((orderId) => !!orderId),
switchMap((orderId) => this._omsService.getOrder(orderId)),
shareReplay()
);
addToPreviousCompartmentAction$ = this._store.addToPreviousCompartmentAction$;
mainActions$ = this._store.mainActions$;
processingStatus$ = this._store.processingStatus$;
items$ = this._store.items$;
order$ = this._store.order$;
fetching$ = this._store.fetching$;
get processId() {
return +this._activatedRoute.snapshot.parent.data.processId;
@@ -61,52 +57,103 @@ export class CustomerOrderDetailsComponent extends ComponentStore<CustomerOrderD
processId$ = this._activatedRoute.parent.data.pipe(map((params) => +params.processId));
hasActions$ = this.items$.pipe(map((items) => items?.some((item) => !!item.actions && item.actions.length > 0)));
private subscriptions = new Subscription();
private _onDestroy$ = new Subject();
get isTablet() {
return this._environment.matchTablet();
get isDesktop() {
return this._environment.matchDesktop();
}
changeActionLoader$ = new BehaviorSubject<string>(undefined);
changeActionDisabled$ = this._store.changeActionDisabled$;
showTagsComponent$ = combineLatest([this.items$, this.fetching$]).pipe(
map(([items, fetching]) => {
const first = items?.find((_) => true);
const hasArrivedAction = first?.actions?.some((a) => a.command?.includes('ARRIVED'));
return hasArrivedAction && !fetching;
})
);
actionsDisabled$ = combineLatest([
this.changeActionDisabled$,
this._store.select((s) => (s.fetchPartial ? s.selectedeOrderItemSubsetIds.length === 0 : false)),
this.items$,
]).pipe(map(([disabled, partial, orderItems]) => disabled || (partial && orderItems.length > 1)));
addToPreviousCompartmentActionDisabled$ = combineLatest([this.compartmentInfo$, this.changeActionDisabled$, this.fetching$]).pipe(
map(([compartmentInfo, changeActionDisabled, fetching]) => (!!compartmentInfo || changeActionDisabled) && fetching)
);
constructor(
private _activatedRoute: ActivatedRoute,
private _domainGoodsInService: DomainCustomerOrderService,
private _store: CustomerOrderDetailsStore,
private _navigationService: CustomerOrdersNavigationService,
private _omsService: DomainOmsService,
private _breadcrumb: BreadcrumbService,
private _router: Router,
private _uiModal: UiModalService,
private _environment: EnvironmentService
) {
super({
fetching: false,
});
}
private _environment: EnvironmentService,
private _commandService: CommandService
) {}
ngOnInit() {
this._activatedRoute.queryParams.pipe(takeUntil(this._onDestroy$)).subscribe((params) => {
const buyerNumber: string = decodeURIComponent(params.buyerNumber ?? '');
const archive = params?.archive || false;
this.patchState({ buyerNumber, archive });
});
this.subscriptions.add(
this._activatedRoute.queryParams.subscribe((params) => {
const buyerNumber: string = decodeURIComponent(params.buyerNumber ?? '');
this._store.patchState({ buyerNumber });
})
);
this._activatedRoute.params.pipe(takeUntil(this._onDestroy$)).subscribe(async (params) => {
const orderNumber: string = params?.orderNumber;
const compartmentCode = params?.compartmentCode;
const processingStatus = +params?.processingStatus as OrderItemProcessingStatusValue;
this.subscriptions.add(
this._activatedRoute.params.subscribe(async (params) => {
const orderNumber: string = params?.orderNumber;
const compartmentCode = params?.compartmentCode;
const processingStatus = +params?.processingStatus as OrderItemProcessingStatusValue;
this.patchState({ orderNumber, compartmentCode, processingStatus });
await this.removeDetailsCrumbs();
this.loadItems();
});
if (
this._store.orderNumber !== orderNumber ||
this._store.compartmentCode !== compartmentCode ||
this._store.processingStatus !== processingStatus
) {
this._store.patchState({
orderNumber: !!orderNumber ? decodeURIComponent(orderNumber) : undefined,
compartmentCode: !!compartmentCode ? decodeURIComponent(compartmentCode) : undefined,
processingStatus,
});
this._store.loadItems();
}
await this.removeDetailsCrumbs();
})
);
this.subscriptions.add(
this._store.searchCompleted.subscribe((state) => {
const items = state?.items;
this.openModalIfItemsHaveDifferentCustomers(items);
this.itemsSelectable(items);
const firstItem = items?.find((_) => true);
this.updateBreadcrumb(firstItem);
const orderId = firstItem?.orderId;
if (!!orderId && this._store?.order?.id !== orderId) {
this._store.patchState({ orderId });
this._store.loadOrder();
}
})
);
this.removeBreadcrumbs();
}
ngOnDestroy() {
this._onDestroy$.next();
this._onDestroy$.complete();
this.subscriptions.unsubscribe();
}
itemsSelectable(items: OrderItemListItemDTO[]) {
this._store.patchState({
itemsSelectable: items?.some((item) => !!item?.actions && item?.actions?.length > 0),
});
}
async updateBreadcrumb(item: OrderItemListItemDTO) {
@@ -116,8 +163,8 @@ export class CustomerOrderDetailsComponent extends ComponentStore<CustomerOrderD
name: item?.compartmentCode || item?.orderNumber,
path: this.getDetailsPath(item),
params: {
buyerNumber: item.buyerNumber,
...this._activatedRoute.snapshot.queryParams,
buyerNumber: item.buyerNumber,
},
section: 'customer',
tags: ['customer-order', 'details', item?.compartmentCode || item?.orderNumber],
@@ -131,9 +178,17 @@ export class CustomerOrderDetailsComponent extends ComponentStore<CustomerOrderD
.pipe(first())
.toPromise();
const historyCrumbs = await this._breadcrumb
.getBreadcrumbsByKeyAndTags$(this.processId, ['customer-order', 'history'])
.pipe(first())
.toPromise();
editCrumbs.forEach((crumb) => {
this._breadcrumb.removeBreadcrumb(crumb.id, true);
});
historyCrumbs.forEach((crumb) => {
this._breadcrumb.removeBreadcrumb(crumb.id, true);
});
}
async removeDetailsCrumbs() {
@@ -147,42 +202,8 @@ export class CustomerOrderDetailsComponent extends ComponentStore<CustomerOrderD
});
}
// tslint:disable-next-line: member-ordering
loadItems = this.effect(($) =>
$.pipe(
tap(() => this.patchState({ fetching: false })),
debounceTime(500),
withLatestFrom(this.orderNumber$, this.compartmentCode$, this.archive$),
switchMap(([_, orderNumber, compartmentCode, archive]) => {
let request$: Observable<ListResponseArgsOfOrderItemListItemDTO>;
request$ = this._domainGoodsInService.getOrderItemsByOrderNumber(orderNumber ?? compartmentCode);
return combineLatest([request$, this.processingStatus$, this.buyerNumber$]).pipe(
tapResponse(
([res, processingStatus, buyerNumber]) => {
const items = res.result.filter((item) => {
return item.processingStatus === processingStatus && (!!buyerNumber ? item.buyerNumber === buyerNumber : true);
});
this.openModalIfItemsHaveDifferentCustomers(items);
this.patchState({
items,
orderId: items[0].orderId,
});
this.updateBreadcrumb(items[0]);
},
(err) => {},
() => {
this.patchState({ fetching: false });
}
)
);
})
)
);
openModalIfItemsHaveDifferentCustomers(items: OrderItemListItemDTO[]) {
const buyerNumbers = new Set(items.map((item) => item.buyerNumber));
const buyerNumbers = new Set(items?.map((item) => item?.buyerNumber));
if (buyerNumbers.size > 1) {
this._uiModal.open({
content: UiMessageModalComponent,
@@ -196,21 +217,18 @@ export class CustomerOrderDetailsComponent extends ComponentStore<CustomerOrderD
navigateToEditPage(orderItem: OrderItemListItemDTO) {
this._router.navigate(this.getEditPath(orderItem), {
queryParams: { buyerNumber: orderItem.buyerNumber, archive: this.archive },
queryParams: { ...this._activatedRoute.snapshot.queryParams, buyerNumber: orderItem?.buyerNumber },
});
}
async actionHandled(handler: { orderItemsContext: OrderItemsContext; command: string; navigation: 'details' | 'main' | 'reservation' }) {
if (handler.navigation === 'main') {
await this._navigationService.navigateToCustomerOrdersSearch({ processId: this.processId });
} else {
const item: OrderItemListItemDTO = handler.orderItemsContext.items.find((_) => true);
await this._router.navigate(this.getDetailsPath(item), {
queryParams: { buyerNumber: item.buyerNumber, archive: this.archive },
});
await this.removeDetailsCrumbs();
this.loadItems();
}
navigateToHistoryPage(orderItem: OrderItemListItemDTO) {
this._router.navigate(this.getHistoryPath(orderItem), {
queryParams: {
...this._activatedRoute.snapshot.queryParams,
buyerNumber: orderItem?.buyerNumber,
orderItemSubsetId: orderItem?.orderItemSubsetId,
},
});
}
getDetailsPath(item: OrderItemListItemDTO) {
@@ -230,4 +248,133 @@ export class CustomerOrderDetailsComponent extends ComponentStore<CustomerOrderD
orderNumber: item?.orderNumber ? encodeURIComponent(item.orderNumber) : undefined,
});
}
getHistoryPath(item: OrderItemListItemDTO) {
return this._navigationService.getCustomerOrdersHistoryPath({
processId: this.processId,
processingStatus: item?.processingStatus,
compartmentCode: item?.compartmentCode ? encodeURIComponent(item.compartmentCode) : undefined,
orderNumber: item?.orderNumber ? encodeURIComponent(item.orderNumber) : undefined,
});
}
getItemQuantityMap(): Map<number, number> {
return new Map(
this.customerOrderDetailsItemComponents.toArray().map((component) => [component.orderItem.orderItemSubsetId, component.quantity])
);
}
getitemsToUpdate(): OrderItemListItemDTO[] {
if (this._store.fetchPartial && this._store.itemsSelectable && this._store.items.length > 1) {
return this._store.items.filter((orderItem) => this._store.selectedeOrderItemSubsetIds.includes(orderItem.orderItemSubsetId));
}
return this._store.items;
}
async handleAction(
action: KeyValueDTOOfStringAndString,
{ compartmentCode, compartmentInfo }: { compartmentCode?: string; compartmentInfo?: string } = {}
) {
if (action.command.includes('FETCHED_PARTIAL')) {
this._store.patchState({ fetchPartial: true });
return;
}
let receipts: ReceiptDTO[] = [];
if (action.command.includes('PRINT_SHIPPINGNOTE')) {
const receiptsPromise = this.customerOrderDetailsItemComponents.toArray().map(
(itemComponent) =>
new Promise<ReceiptDTO[]>((resolve) => {
itemComponent.loadReceipts((r) => resolve(r));
})
);
receipts = await Promise.all(receiptsPromise).then((r) => r.reduce((acc, val) => acc.concat(val), []));
receipts = unionBy(receipts, 'id');
}
// #2737 Bei Zubuchen kein Abholfachzettel ausdrucken
let command = action.command;
if (compartmentCode) {
command = action.command.replace('|PRINT_COMPARTMENTLABEL', '');
}
this.changeActionLoader$.next(action.command);
this.changeActionDisabled$.next(true);
let commandData: OrderItemsContext = {
items: this.getitemsToUpdate(),
compartmentInfo: compartmentInfo || this.compartmentInfo,
compartmentCode:
action.command.includes('PRINT_PRICEDIFFQRCODELABEL') && !compartmentCode
? this._store.items?.find((_) => true)?.compartmentCode
: compartmentCode,
itemQuantity: this.getItemQuantityMap(),
receipts,
};
try {
commandData = await this._commandService.handleCommand(command, commandData);
let navigateTo: 'details' | 'main' | 'reservation' = 'details';
if (action.command.includes('ARRIVED')) {
navigateTo = await this.arrivedActionNavigation();
}
if (action.command.includes('PRINT_PRICEDIFFQRCODELABEL')) {
navigateTo = 'main';
}
await this.actionHandled({ orderItemsContext: commandData, command: action.command, navigation: navigateTo });
this._store.updateOrderItems(commandData.items);
} catch (error) {
this._uiModal.open({
content: UiErrorModalComponent,
data: error,
});
console.error(error);
}
if (action.command.includes('FETCHED_PARTIAL')) {
this._store.patchState({ fetchPartial: false });
}
this._store.selectedeOrderItemSubsetIds = [];
setTimeout(() => {
this.changeActionDisabled$.next(false);
this.changeActionLoader$.next(undefined);
}, 1000);
}
async actionHandled(handler: { orderItemsContext: OrderItemsContext; command: string; navigation: 'details' | 'main' | 'reservation' }) {
if (handler.navigation === 'main') {
await this._navigationService.navigateToCustomerOrdersSearch({ processId: this.processId });
} else {
const item: OrderItemListItemDTO = handler.orderItemsContext.items.find((_) => true);
await this._router.navigate(this.getDetailsPath(item), {
queryParams: { ...this._activatedRoute.snapshot.queryParams, buyerNumber: item.buyerNumber },
});
await this.updateCustomerOrderResults();
await this.removeDetailsCrumbs();
this._store.loadItems();
}
}
async updateCustomerOrderResults() {
if (this.isDesktop) {
await this._router.navigate([], {
queryParams: { ...this._activatedRoute.snapshot.queryParams, updateResults: true },
});
}
}
async arrivedActionNavigation(): Promise<'main'> {
const detailsCrumbs = await this._breadcrumb
.getBreadcrumbsByKeyAndTags$('customer-order', ['customer-order', 'details'])
.pipe(first())
.toPromise();
detailsCrumbs.forEach((crumb) => {
this._breadcrumb.removeBreadcrumb(crumb.id, true);
});
return 'main';
}
}

View File

@@ -1,13 +1,51 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SharedGoodsInOutOrderDetailsModule } from '@shared/components/goods-in-out';
import { CustomerOrderDetailsComponent } from './customer-order-details.component';
import { CustomerOrderDetailsItemComponent } from './customer-order-details-item';
import { CustomerOrderDetailsHeaderComponent } from './customer-order-details-header';
import { CustomerOrderDetailsTagsComponent } from './customer-order-details-tags';
import { UiCommonModule } from '@ui/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { UiSpinnerModule } from '@ui/spinner';
import { IconModule } from '@shared/components/icon';
import { UiTooltipModule } from '@ui/tooltip';
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
import { TextFieldModule } from '@angular/cdk/text-field';
import { CustomerOrderPipesModule } from '../pipes';
import { ProductImageModule } from '@cdn/product-image';
import { CustomerOrderDetailsStore } from './customer-order-details.store';
import { UiDatepickerModule } from '@ui/datepicker';
import { UiDropdownModule } from '@ui/dropdown';
@NgModule({
imports: [CommonModule, SharedGoodsInOutOrderDetailsModule],
exports: [CustomerOrderDetailsComponent],
declarations: [CustomerOrderDetailsComponent],
providers: [],
imports: [
CommonModule,
UiCommonModule,
FormsModule,
ReactiveFormsModule,
UiSpinnerModule,
IconModule,
UiTooltipModule,
UiDatepickerModule,
UiQuantityDropdownModule,
UiDropdownModule,
TextFieldModule,
CustomerOrderPipesModule,
ProductImageModule,
],
exports: [
CustomerOrderDetailsComponent,
CustomerOrderDetailsItemComponent,
CustomerOrderDetailsHeaderComponent,
CustomerOrderDetailsTagsComponent,
],
declarations: [
CustomerOrderDetailsComponent,
CustomerOrderDetailsItemComponent,
CustomerOrderDetailsHeaderComponent,
CustomerOrderDetailsTagsComponent,
],
providers: [CustomerOrderDetailsStore],
})
export class CustomerOrderDetailsModule {}

View File

@@ -0,0 +1,308 @@
import { Injectable, OnDestroy } from '@angular/core';
import { DomainCustomerOrderService, DomainOmsService, OrderItemsContext } from '@domain/oms';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import {
ListResponseArgsOfOrderItemListItemDTO,
OrderDTO,
OrderItemListItemDTO,
OrderItemProcessingStatusValue,
ReceiptDTO,
} from '@swagger/oms';
import { isEqual } from 'lodash';
import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { debounceTime, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
export interface CustomerOrderDetailsComponentState {
order?: OrderDTO;
orderNumber?: string;
orderId?: number;
buyerNumber?: string;
items?: OrderItemListItemDTO[];
fetching?: boolean;
processingStatus?: OrderItemProcessingStatusValue;
receipts: ReceiptDTO[];
compartmentCode?: string;
compartmentInfo?: string;
latestCompartmentCode?: string;
latestCompartmentInfo?: string;
fetchPartial?: boolean;
itemsSelectable?: boolean;
selectedeOrderItemSubsetIds: number[];
}
@Injectable()
export class CustomerOrderDetailsStore extends ComponentStore<CustomerOrderDetailsComponentState> implements OnDestroy {
get items() {
return this.get((s) => s.items);
}
set items(items: OrderItemListItemDTO[]) {
if (!isEqual(this.items, items)) {
this.patchState({ items });
}
}
items$ = this.select((s) => s.items);
get orderNumber() {
return this.get((s) => s.orderNumber);
}
set orderNumber(orderNumber: string) {
this.patchState({ orderNumber });
}
orderNumber$ = this.select((s) => s.orderNumber);
get orderId() {
return this.get((s) => s.orderId);
}
set orderId(orderId: number) {
this.patchState({ orderId });
}
orderId$ = this.select((s) => s.orderId);
get order() {
return this.get((s) => s.order);
}
set order(order: OrderDTO) {
this.patchState({ order });
}
order$ = this.select((s) => s.order);
get processingStatus() {
return this.get((s) => s.processingStatus);
}
set processingStatus(processingStatus: OrderItemProcessingStatusValue) {
this.patchState({ processingStatus });
}
processingStatus$ = this.select((s) => s.processingStatus);
get itemsSelectable() {
return this.get((s) => s.itemsSelectable);
}
set itemsSelectable(itemsSelectable: boolean) {
this.patchState({ itemsSelectable });
}
itemsSelectable$ = this.select((s) => s.itemsSelectable);
get fetching() {
return this.get((s) => s.fetching);
}
set fetching(fetching: boolean) {
this.patchState({ fetching });
}
fetching$ = this.select((s) => s.fetching);
get compartmentCode() {
return this.get((s) => s.compartmentCode);
}
set compartmentCode(compartmentCode: string) {
this.patchState({ compartmentCode });
}
compartmentCode$ = this.select((s) => s.compartmentCode);
get buyerNumber() {
return this.get((s) => s.buyerNumber);
}
set buyerNumber(buyerNumber: string) {
this.patchState({ buyerNumber });
}
buyerNumber$ = this.select((s) => s.buyerNumber);
get selectedeOrderItemSubsetIds() {
return this.get((s) => s.selectedeOrderItemSubsetIds);
}
set selectedeOrderItemSubsetIds(selectedeOrderItemSubsetIds: number[]) {
if (!isEqual(this.selectedeOrderItemSubsetIds, selectedeOrderItemSubsetIds)) {
this.patchState({ selectedeOrderItemSubsetIds });
}
}
selectedeOrderItemSubsetIds$ = this.select((s) => s.selectedeOrderItemSubsetIds);
get compartmentInfo() {
return this.get((s) => s.compartmentInfo);
}
set compartmentInfo(compartmentInfo: string) {
if (this.compartmentInfo !== compartmentInfo) {
this.patchState({ compartmentInfo });
}
}
compartmentInfo$ = this.select((s) => s.compartmentInfo);
get fetchPartial() {
return this.get((s) => s.fetchPartial);
}
fetchPartial$ = this.select((s) => s.fetchPartial);
mainActions$ = combineLatest([this.items$, this.fetchPartial$]).pipe(
map(([items, fetchPartial]) =>
items
?.find((_) => true)
?.actions?.filter((action) => typeof action?.enabled !== 'boolean')
?.filter((action) => (fetchPartial ? !action.command.includes('FETCHED_PARTIAL') : true))
?.sort((a, b) => (a.selected === b.selected ? 0 : a.selected ? -1 : 1))
)
);
showTagsComponent$ = this.items$.pipe(
map((orderItems) => {
const first = orderItems?.find((_) => true);
return first?.actions?.some((a) => a.command?.includes('ARRIVED')) || false;
})
);
get latestCompartmentCode() {
return this.get((s) => s.latestCompartmentCode);
}
set latestCompartmentCode(latestCompartmentCode: string) {
if (this.latestCompartmentCode !== latestCompartmentCode) {
this.patchState({ latestCompartmentCode });
}
}
readonly latestCompartmentCode$ = combineLatest([
this.select((s) => s.latestCompartmentCode),
this.select((s) => s.latestCompartmentInfo),
]).pipe(
map(([code, info]) => {
if (!!info) {
return `${code}_${info}`;
}
return code;
})
);
get latestCompartmentInfo() {
return this.get((s) => s.latestCompartmentInfo);
}
set latestCompartmentInfo(latestCompartmentInfo: string) {
if (this.latestCompartmentInfo !== latestCompartmentInfo) {
this.patchState({ latestCompartmentInfo });
}
}
readonly latestCompartmentInfo$ = this.select((s) => s.latestCompartmentInfo);
addToPreviousCompartmentAction$ = combineLatest([this.items$, this.latestCompartmentCode$]).pipe(
map(([orderItems, latestCompartmentCode]) => {
const orderItem = orderItems?.find((_) => true);
if ([16, 8192].includes(orderItem?.processingStatus) && latestCompartmentCode) {
// Zubuchen von Bezahlte und unbezahlte Bestellungen nicht möglich
// Zubuchen bei Pay&Collect nur innerhalb der gleichen Bestellung möglich
return orderItem.actions.find((a) => a.key === '128');
}
return undefined;
})
);
searchCompleted = new Subject<CustomerOrderDetailsComponentState>();
actionHandled = new Subject<{
orderItemsContext: OrderItemsContext;
command: string;
navigation: 'details' | 'main' | 'reservation';
}>();
changeActionDisabled$ = new BehaviorSubject<boolean>(false);
constructor(private _domainCustomerOrderService: DomainCustomerOrderService, private _domainOmsService: DomainOmsService) {
super({
items: [],
selectedeOrderItemSubsetIds: [],
receipts: [],
fetchPartial: true,
});
}
loadItems = this.effect(($) =>
$.pipe(
tap(() => this.patchState({ fetching: true })),
debounceTime(500),
withLatestFrom(this.orderNumber$, this.compartmentCode$),
switchMap(([_, orderNumber, compartmentCode]) => {
return this._domainCustomerOrderService.getOrderItemsByOrderNumber(orderNumber ?? compartmentCode).pipe(
withLatestFrom(this.processingStatus$, this.buyerNumber$),
tapResponse(
([res, processingStatus, buyerNumber]) => {
const items = res?.result?.filter((item) => {
return item.processingStatus === processingStatus && (!!buyerNumber ? item.buyerNumber === buyerNumber : true);
});
this.patchState({
items,
fetching: false,
orderId: items?.find((_) => true)?.orderId,
});
this.searchCompleted.next(this.get());
},
(err) => {
console.error(err);
this.patchState({ fetching: false, items: [] });
this.searchCompleted.next(this.get());
}
)
);
})
)
);
loadOrder = this.effect(($) =>
$.pipe(
withLatestFrom(this.orderId$),
switchMap(([_, orderId]) => {
return this._domainOmsService.getOrder(orderId).pipe(
tapResponse(
(response) => {
this.patchState({ order: response });
},
(err) => {
this.patchState({ order: undefined });
}
)
);
})
)
);
selectAllOrderItems() {
this.patchState({ selectedeOrderItemSubsetIds: this.items?.map((item) => item.orderItemSubsetId) });
}
selectOrderItem(item: OrderItemListItemDTO, selected: boolean) {
const included = this.selectedeOrderItemSubsetIds.includes(item?.orderItemSubsetId);
if (!included && selected) {
this.patchState({ selectedeOrderItemSubsetIds: [...this.selectedeOrderItemSubsetIds, item.orderItemSubsetId] });
} else if (included && !selected) {
this.patchState({ selectedeOrderItemSubsetIds: this.selectedeOrderItemSubsetIds.filter((id) => id !== item?.orderItemSubsetId) });
}
}
updateOrderItems(orderItems: OrderItemListItemDTO[]) {
this.patchState({
items: this.items.map((item) => {
const newItem = orderItems.find((i) => i.orderItemSubsetId === item.orderItemSubsetId);
return newItem ? newItem : item;
}),
});
}
updateReceipts(receipts: ReceiptDTO[]) {
this.patchState({
receipts,
});
}
}

View File

@@ -1,2 +1,5 @@
export * from './customer-order-details.component';
export * from './customer-order-details.module';
export * from './customer-order-details-header';
export * from './customer-order-details-item';
export * from './customer-order-details-tags';

View File

@@ -1,4 +1,3 @@
div {
@apply overflow-y-scroll;
height: calc(100vh - 305px);
@apply box-border grid overflow-y-scroll h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
}

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { DomainCustomerOrderService, DomainGoodsService, DomainOmsService } from '@domain/oms';
@@ -38,7 +38,7 @@ export class CustomerOrderEditComponent implements OnInit {
items$ = combineLatest([this.orderNumber$, this.compartmentCode$]).pipe(
switchMap(([orderNumber, compartmentCode]) => {
return this._domainGoodsInService.getOrderItemsByOrderNumber(compartmentCode ?? orderNumber);
return this._domainCustomerOrderService.getOrderItemsByOrderNumber(compartmentCode ?? orderNumber);
}),
withLatestFrom(this.processingStatus$, this.buyerNumber$),
map(([response, processingStatus, buyerNumber]) => {
@@ -57,9 +57,8 @@ export class CustomerOrderEditComponent implements OnInit {
constructor(
private _activatedRoute: ActivatedRoute,
private _breadcrumb: BreadcrumbService,
private _domainGoodsInService: DomainCustomerOrderService,
private _domainCustomerOrderService: DomainCustomerOrderService,
private _navigation: CustomerOrdersNavigationService,
private _router: Router,
private _uiModal: UiModalService,
private _environment: EnvironmentService
) {}
@@ -72,19 +71,17 @@ export class CustomerOrderEditComponent implements OnInit {
const orderNumber = this._activatedRoute.snapshot.params.orderNumber;
const compartmentCode = this._activatedRoute.snapshot.params.compartmentCode;
const processingStatus = this._activatedRoute.snapshot.params.processingStatus;
const buyerNumber = this._activatedRoute.snapshot.queryParams.buyerNumber;
const archive = this._activatedRoute.snapshot.queryParams.archive;
await this._breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: this.processId,
name: 'Bearbeiten',
path: this._navigation.getCustomerOrdersEditPath({
processId: this.processId,
processingStatus,
compartmentCode: compartmentCode ? encodeURIComponent(compartmentCode) : undefined,
orderNumber: orderNumber ? encodeURIComponent(orderNumber) : undefined,
compartmentCode: compartmentCode ?? undefined,
orderNumber: orderNumber ?? undefined,
}),
section: 'customer',
params: { buyerNumber, archive },
params: this._activatedRoute.snapshot.queryParams,
tags: ['customer-order', 'edit', compartmentCode || orderNumber],
});
}
@@ -93,18 +90,13 @@ export class CustomerOrderEditComponent implements OnInit {
const orderNumber = this._activatedRoute.snapshot.params.orderNumber;
const compartmentCode = this._activatedRoute.snapshot.params.compartmentCode;
const processingStatus = options?.processingStatus ? options.processingStatus : this._activatedRoute.snapshot.params.processingStatus;
const buyerNumber = this._activatedRoute.snapshot.queryParams.buyerNumber;
const archive = this._activatedRoute.snapshot.queryParams.archive;
await this._navigation.navigateToDetails({
processId: this.processId,
processingStatus: processingStatus,
compartmentCode: compartmentCode ? encodeURIComponent(compartmentCode) : undefined,
orderNumber: orderNumber ? encodeURIComponent(orderNumber) : undefined,
queryParams: {
buyerNumber,
archive,
},
compartmentCode: compartmentCode ?? undefined,
orderNumber: orderNumber ?? undefined,
queryParams: this._activatedRoute.snapshot.queryParams,
});
}

View File

@@ -0,0 +1,19 @@
<div class="bg-[#f5f7fa]">
<div class="bg-white px-7 flex flex-col py-5">
<button class="self-end" type="button" (click)="navigateToDetailsPage({})">
<shared-icon icon="close" [size]="26"></shared-icon>
</button>
<div class="flex flex-row">
<div class="flex flex-row flex-grow">
<span class="mr-3">Kundenname</span>
<strong>{{ customerName$ | async }}</strong>
</div>
<div class="flex flex-row flex-grow">
<span class="mr-3">Kundennummer</span>
<strong>{{ customerNumber$ | async }}</strong>
</div>
</div>
</div>
<shared-history-list [history]="history$ | async"> </shared-history-list>
</div>

View File

@@ -0,0 +1,3 @@
:host {
@apply box-border grid overflow-y-scroll h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
}

View File

@@ -0,0 +1,97 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainCustomerOrderService, DomainOmsService } from '@domain/oms';
import { CustomerOrdersNavigationService } from '@shared/services';
import { HistoryDTO } from '@swagger/oms';
import { Observable, combineLatest } from 'rxjs';
import { map, switchMap, shareReplay, take } from 'rxjs/operators';
@Component({
selector: 'page-customer-order-history',
templateUrl: 'customer-order-history.component.html',
styleUrls: ['customer-order-history.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerOrderHistoryComponent implements OnInit {
get processId() {
return +this._activatedRoute.snapshot.parent.data.processId;
}
processId$ = this._activatedRoute.parent.data.pipe(map((params) => +params.processId));
orderNumber$: Observable<string> = this._activatedRoute.params.pipe(
map((params) => decodeURIComponent(params.orderNumber ?? '') || undefined)
);
compartmentCode$: Observable<string> = this._activatedRoute.params.pipe(
map((params) => decodeURIComponent(params?.compartmentCode ?? '') || undefined)
);
get orderItemSubsetId() {
return +this._activatedRoute.snapshot.queryParams.orderItemSubsetId;
}
historyItem$ = combineLatest([this.orderNumber$, this.compartmentCode$]).pipe(
switchMap(([orderNumber, compartmentCode]) =>
this._domainCustomerOrderService.getOrderItemsByOrderNumber(compartmentCode ?? orderNumber)
),
map((response) => response?.result?.find((listItem) => listItem?.orderItemSubsetId === this.orderItemSubsetId)),
shareReplay()
);
customerName$ = this.historyItem$.pipe(
map((historyItem) =>
[historyItem?.organisation ?? historyItem.organisation, historyItem.lastName, historyItem.firstName].filter((i) => !!i).join(', ')
)
);
customerNumber$ = this.historyItem$.pipe(map((historyItem) => historyItem.buyerNumber));
history$ = this.historyItem$.pipe(switchMap((historyItem) => this._omsService.getHistory(historyItem.orderItemSubsetId).pipe(take(1))));
constructor(
private _activatedRoute: ActivatedRoute,
private _breadcrumb: BreadcrumbService,
private _navigation: CustomerOrdersNavigationService,
private _domainCustomerOrderService: DomainCustomerOrderService,
private _omsService: DomainOmsService
) {}
ngOnInit() {
this.updateBreadcrumb();
}
async updateBreadcrumb() {
const orderNumber = this._activatedRoute.snapshot.params.orderNumber;
const compartmentCode = this._activatedRoute.snapshot.params.compartmentCode;
const processingStatus = this._activatedRoute.snapshot.params.processingStatus;
await this._breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: this.processId,
name: 'Historie',
path: this._navigation.getCustomerOrdersHistoryPath({
processId: this.processId,
processingStatus,
compartmentCode: compartmentCode ?? undefined,
orderNumber: orderNumber ?? undefined,
}),
section: 'customer',
params: this._activatedRoute.snapshot.queryParams,
tags: ['customer-order', 'history', compartmentCode || orderNumber],
});
}
async navigateToDetailsPage({ options }: { options?: { processingStatus?: number } }) {
const orderNumber = this._activatedRoute.snapshot.params.orderNumber;
const compartmentCode = this._activatedRoute.snapshot.params.compartmentCode;
const processingStatus = options?.processingStatus ? options.processingStatus : this._activatedRoute.snapshot.params.processingStatus;
await this._navigation.navigateToDetails({
processId: this.processId,
processingStatus: processingStatus,
compartmentCode: compartmentCode ?? undefined,
orderNumber: orderNumber ?? undefined,
queryParams: this._activatedRoute.snapshot.queryParams,
});
}
}

View File

@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { CustomerOrderHistoryComponent } from './customer-order-history.component';
import { CommonModule } from '@angular/common';
import { SharedHistoryListModule } from '@shared/components/history';
import { IconModule } from '@shared/components/icon';
@NgModule({
imports: [CommonModule, SharedHistoryListModule, IconModule],
exports: [CustomerOrderHistoryComponent],
declarations: [CustomerOrderHistoryComponent],
providers: [],
})
export class CustomerOrderHistoryModule {}

View File

@@ -0,0 +1,4 @@
// start:ng42.barrel
export * from './customer-order-history.component';
export * from './customer-order-history.module';
// end:ng42.barrel

View File

@@ -6,6 +6,7 @@ import { CustomerOrderSearchComponent, CustomerOrderSearchFilterComponent, Custo
import { CustomerOrderSearchMainComponent, CustomerOrderSearchMainModule } from './customer-order-search/search-main';
import { CustomerOrderSearchResultsComponent, CustomerOrderSearchResultsModule } from './customer-order-search/search-results';
import { CustomerOrderComponent } from './customer-order.component';
import { CustomerOrderHistoryComponent } from './customer-order-history';
const auxiliaryRoutes = [
{
@@ -25,12 +26,12 @@ const auxiliaryRoutes = [
outlet: 'right',
},
{
path: 'filter/:compartmentCode/:processingStatus',
path: 'filter/compartment/:compartmentCode/:processingStatus',
component: CustomerOrderSearchFilterComponent,
outlet: 'right',
},
{
path: 'filter/:orderNumber/:processingStatus',
path: 'filter/order/:orderNumber/:processingStatus',
component: CustomerOrderSearchFilterComponent,
outlet: 'right',
},
@@ -74,6 +75,16 @@ const auxiliaryRoutes = [
component: CustomerOrderEditComponent,
outlet: 'right',
},
{
path: 'details/compartment/:compartmentCode/:processingStatus/history',
component: CustomerOrderHistoryComponent,
outlet: 'right',
},
{
path: 'details/order/:orderNumber/:processingStatus/history',
component: CustomerOrderHistoryComponent,
outlet: 'right',
},
];
const routes: Routes = [
@@ -107,6 +118,14 @@ const routes: Routes = [
path: 'details/order/:orderNumber/:processingStatus/edit',
component: CustomerOrderEditComponent,
},
{
path: 'details/compartment/:compartmentCode/:processingStatus/history',
component: CustomerOrderHistoryComponent,
},
{
path: 'details/order/:orderNumber/:processingStatus/history',
component: CustomerOrderHistoryComponent,
},
...auxiliaryRoutes,
],
},

View File

@@ -59,8 +59,8 @@ export class CustomerOrderSearchFilterComponent implements OnInit, OnDestroy {
return this._navigationService.getCustomerOrdersDetailsPath({
processId: this.processId,
processingStatus,
compartmentCode: compartmentCode ? encodeURIComponent(compartmentCode) : undefined,
orderNumber: orderNumber ? encodeURIComponent(orderNumber) : undefined,
compartmentCode: compartmentCode ?? undefined,
orderNumber: orderNumber ?? undefined,
});
}
}

View File

@@ -88,24 +88,29 @@
class="page-customer-order-item__item-buyer-number desktop-small:text-h3 justify-self-end font-bold"
[class.page-customer-order-item__item-buyer-number-main]="!mainOutletActive"
>
{{ item?.buyerNumber }}
<ng-container *ngIf="item?.compartmentCode; else buyerNumber"
>{{ item?.compartmentCode }}{{ item?.compartmentInfo && '_' + item?.compartmentInfo }}</ng-container
>
<ng-template #buyerNumber>{{ item?.buyerNumber }}</ng-template>
</div>
<div
class="page-customer-order-item__item-processing-status flex flex-row font-bold desktop-small:text-p2 justify-self-end self-center"
class="page-customer-order-item__item-processing-paid-status flex flex-col font-bold desktop-small:text-p2 justify-self-end self-center"
>
<shared-icon
class="flex items-center justify-center mr-1"
[size]="16"
*ngIf="item.processingStatus | processingStatus: 'icon'; let icon"
[icon]="icon"
></shared-icon>
{{ item.processingStatus | processingStatus }}
</div>
<div class="page-customer-order-item__item-processing-status flex flex-row mb-[0.375rem]">
<shared-icon
class="flex items-center justify-center mr-1"
[size]="16"
*ngIf="item.processingStatus | processingStatus: 'icon'; let icon"
[icon]="icon"
></shared-icon>
{{ item.processingStatus | processingStatus }}
</div>
<div class="page-customer-order-item__item-paid flex flex-row justify-self-end self-center">
<div class="font-bold w-fit desktop-small:text-p2 px-3 py-[0.125rem] rounded text-white bg-[#26830C]" *ngIf="item.features?.paid">
{{ item.features?.paid }}
<div class="page-customer-order-item__item-paid flex flex-row self-end">
<div class="font-bold w-fit desktop-small:text-p2 px-3 py-[0.125rem] rounded text-white bg-[#26830C]" *ngIf="item.features?.paid">
{{ item.features?.paid }}
</div>
</div>
</div>

View File

@@ -13,8 +13,8 @@
grid-template-columns: 3.125rem auto;
grid-template-rows: 2rem 2rem 10px auto;
grid-template-areas:
'buyernumber buyernumber processingstatus processingstatus'
'buyernumber buyernumber paid paid'
'buyernumber buyernumber status status'
'buyernumber buyernumber status status'
'separator separator separator separator'
'thumbnail title title title'
'thumbnail title title title'
@@ -27,8 +27,8 @@
grid-template-columns: 3.125rem 30% 17.5% 27.5% auto;
grid-template-areas:
'thumbnail title format date buyernumber'
'thumbnail title ean quantity processingstatus'
'thumbnail comment price target paid';
'thumbnail title ean quantity status'
'thumbnail comment price target status';
}
.page-customer-order-item__item-thumbnail {
@@ -79,12 +79,8 @@
@apply justify-self-start self-center;
}
.page-customer-order-item__item-processing-status {
grid-area: processingstatus;
}
.page-customer-order-item__item-paid {
grid-area: paid;
.page-customer-order-item__item-processing-paid-status {
grid-area: status;
}
.page-customer-order-item__item-select-bullet {

View File

@@ -78,7 +78,6 @@ export class CustomerOrderItemComponent extends ComponentStore<CustomerOrderItem
const orderNumber = this.item?.orderNumber;
const processingStatus = this.item?.processingStatus;
const compartmentCode = this.item?.compartmentCode;
return this._navigationService.getCustomerOrdersDetailsPath({
processId: this._applicationService.activatedProcessId,
processingStatus,

View File

@@ -129,8 +129,8 @@ export class CustomerOrderSearchResultsComponent extends ComponentStore<Customer
return this._navigationService.getCustomerOrdersResultsAndFilterPath({
processId: this._application.activatedProcessId,
processingStatus,
compartmentCode: compartmentCode ? encodeURIComponent(compartmentCode) : undefined,
orderNumber: orderNumber ? encodeURIComponent(orderNumber) : undefined,
compartmentCode: compartmentCode ?? undefined,
orderNumber: orderNumber ?? undefined,
});
}
@@ -173,6 +173,19 @@ export class CustomerOrderSearchResultsComponent extends ComponentStore<Customer
}
ngOnInit() {
// Aktualisiere die Result Liste über actionHandled oder specialCommentChanged in den CustomerOrderDetails
// updateResults wird über actionHandled oder specialCommentChanged in die queryParams überführt
this._searchResultSubscription.add(
this._activatedRoute.queryParams.subscribe((queryParams) => {
const updateResults = queryParams.updateResults;
if (!!updateResults) {
this.search();
const clean = { ...queryParams };
delete clean['updateResults'];
}
})
);
this._searchResultSubscription.add(
this.processId$
.pipe(
@@ -258,10 +271,19 @@ export class CustomerOrderSearchResultsComponent extends ComponentStore<Customer
const editCrumbs = await this._breadcrumb.getBreadcrumbsByKeyAndTags$(processId, ['customer-order', 'edit']).pipe(first()).toPromise();
const historyCrumbs = await this._breadcrumb
.getBreadcrumbsByKeyAndTags$(processId, ['customer-order', 'history'])
.pipe(first())
.toPromise();
editCrumbs.forEach((crumb) => {
this._breadcrumb.removeBreadcrumb(crumb.id, true);
});
historyCrumbs.forEach((crumb) => {
this._breadcrumb.removeBreadcrumb(crumb.id, true);
});
detailsCrumbs.forEach((crumb) => {
this._breadcrumb.removeBreadcrumb(crumb.id, true);
});

View File

@@ -13,8 +13,8 @@ import { CustomerOrderItemComponent } from './customer-order-item.component';
import { FilterModule } from '@shared/components/filter';
import { IconModule } from '@shared/components/icon';
import { ProductImageModule } from '@cdn/product-image';
import { ProcessingStatusPipe } from './customer-order-processing-status.pipe';
import { FormsModule } from '@angular/forms';
import { CustomerOrderPipesModule } from '../../pipes';
@NgModule({
imports: [
@@ -28,6 +28,7 @@ import { FormsModule } from '@angular/forms';
IconModule,
UiSpinnerModule,
UiScrollContainerModule,
CustomerOrderPipesModule,
],
exports: [CustomerOrderSearchResultsComponent, CustomerOrderItemComponent],
declarations: [
@@ -35,7 +36,6 @@ import { FormsModule } from '@angular/forms';
CustomerOrderItemSelectablePipe,
CustomerOrderItemSelectedPipe,
CustomerOrderItemComponent,
ProcessingStatusPipe,
],
})
export class CustomerOrderSearchResultsModule {}

View File

@@ -1,5 +1,4 @@
export * from './customer-order-item-selectable.pipe';
export * from './customer-order-processing-status.pipe';
export * from './customer-order-item-selectede.pipe';
export * from './customer-order-search-results.component';
export * from './customer-order-search-results.module';

View File

@@ -0,0 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'addToPreviousCompartmentCodeLabelPipe',
})
export class AddToPreviousCompartmentCodeLabelPipe implements PipeTransform {
transform(compartmentCode: string): string {
const compartmentCodeSplit = compartmentCode?.split('_');
compartmentCodeSplit?.shift();
return compartmentCodeSplit?.join('_');
}
}

View File

@@ -1,9 +1,9 @@
import { SpectatorPipe, createPipeFactory } from '@ngneat/spectator';
import { ProcessingStatusPipe } from './customer-order-processing-status.pipe';
import { CustomerOrderProcessingStatusPipe } from './customer-order-processing-status.pipe';
describe('ProcessingStatusPipe', () => {
let spectator: SpectatorPipe<ProcessingStatusPipe>;
const createPipe = createPipeFactory(ProcessingStatusPipe);
describe('CustomerOrderProcessingStatusPipe', () => {
let spectator: SpectatorPipe<CustomerOrderProcessingStatusPipe>;
const createPipe = createPipeFactory(CustomerOrderProcessingStatusPipe);
it('should return icon name (close) if type is icon', () => {
spectator = createPipe(`{{ 1024 | processingStatus:'icon' }}`);

View File

@@ -37,7 +37,7 @@ export const ProcessingStatusNameMap = new Map<number, { value: string; disabled
name: 'processingStatus',
})
@Injectable()
export class ProcessingStatusPipe implements PipeTransform {
export class CustomerOrderProcessingStatusPipe implements PipeTransform {
icon = {
16: 'done',
128: 'done',

View File

@@ -0,0 +1,7 @@
// start:ng42.barrel
export * from './customer-order-processing-status.pipe';
export * from './notifications-channel.pipe';
export * from './payment-type.pipe';
export * from './pipes.module';
export * from './add-to-prevoius-compartment-code-label.pipe';
// end:ng42.barrel

View File

@@ -0,0 +1,27 @@
import { Pipe, PipeTransform } from '@angular/core';
import { NotificationChannel } from '@swagger/oms';
@Pipe({
name: 'notificationsChannel',
})
export class CustomerOrderNotificationsChannelPipe implements PipeTransform {
static channels = new Map<NotificationChannel, string>([
[1, 'E-Mail'],
[2, 'SMS'],
[4, 'Telefon'],
[8, 'Fax'],
[16, 'Brief'],
]);
transform(value: NotificationChannel = 0): string {
const result: string[] = [];
for (const [channel, name] of CustomerOrderNotificationsChannelPipe.channels) {
if (value & channel) {
result.push(name);
}
}
return result.join(' | ');
}
}

View File

@@ -0,0 +1,29 @@
import { Pipe, PipeTransform } from '@angular/core';
const paymentTypeNames = {
0: '',
1: 'bei Abholung',
2: 'Kostenfrei',
4: 'Bar',
8: 'Bankeinzug',
16: 'Einzugsermächtigung',
32: 'EC-Karte',
64: 'Kreditkarte',
128: 'Gegen Rechnung',
256: 'Vorauskasse',
512: 'Gutschein',
1024: 'Sammelrechnung',
2048: 'PayPal',
4069: 'InstantTransfer',
8192: 'PayOnDelivery',
16384: 'BonusCard',
};
@Pipe({
name: 'paymentType',
})
export class CustomerOrderPaymentTypePipe implements PipeTransform {
transform(value: any, ...args: any[]): any {
return paymentTypeNames[value] || '-';
}
}

View File

@@ -0,0 +1,24 @@
import { NgModule } from '@angular/core';
import { CustomerOrderProcessingStatusPipe } from './customer-order-processing-status.pipe';
import { CustomerOrderPaymentTypePipe } from './payment-type.pipe';
import { CustomerOrderNotificationsChannelPipe } from './notifications-channel.pipe';
import { AddToPreviousCompartmentCodeLabelPipe } from './add-to-prevoius-compartment-code-label.pipe';
@NgModule({
imports: [],
exports: [
CustomerOrderProcessingStatusPipe,
CustomerOrderPaymentTypePipe,
CustomerOrderNotificationsChannelPipe,
AddToPreviousCompartmentCodeLabelPipe,
],
declarations: [
CustomerOrderProcessingStatusPipe,
CustomerOrderPaymentTypePipe,
CustomerOrderNotificationsChannelPipe,
AddToPreviousCompartmentCodeLabelPipe,
],
providers: [],
})
export class CustomerOrderPipesModule {}

View File

@@ -37,23 +37,49 @@ export class CustomerOrdersNavigationService extends NavigationService {
compartmentCode?: string;
orderNumber?: string;
}): any[] {
return [
'/kunde',
processId,
'order',
{
outlets: {
primary: 'filter',
main: null,
left: 'results',
right: !!compartmentCode
? ['filter', compartmentCode, processingStatus]
: !!orderNumber
? ['filter', orderNumber, processingStatus]
: 'filter',
if (!!compartmentCode) {
return [
'/kunde',
processId,
'order',
{
outlets: {
primary: 'filter',
main: null,
left: 'results',
right: ['filter', 'compartment', compartmentCode, processingStatus],
},
},
},
];
];
} else if (!!orderNumber) {
return [
'/kunde',
processId,
'order',
{
outlets: {
primary: 'filter',
main: null,
left: 'results',
right: ['filter', 'order', orderNumber, processingStatus],
},
},
];
} else {
return [
'/kunde',
processId,
'order',
{
outlets: {
primary: 'filter',
main: null,
left: 'results',
right: 'filter',
},
},
];
}
}
getCustomerOrdersDetailsPath({
@@ -140,6 +166,48 @@ export class CustomerOrdersNavigationService extends NavigationService {
}
}
getCustomerOrdersHistoryPath({
processId,
processingStatus,
compartmentCode,
orderNumber,
}: {
processId: number;
processingStatus: number;
compartmentCode?: string;
orderNumber?: string;
}): any[] {
if (!!compartmentCode) {
return [
'/kunde',
processId,
'order',
{
outlets: {
primary: ['details', 'compartment', compartmentCode, processingStatus, 'history'],
main: null,
left: ['results', compartmentCode, processingStatus],
right: ['details', 'compartment', compartmentCode, processingStatus, 'history'],
},
},
];
} else {
return [
'/kunde',
processId,
'order',
{
outlets: {
primary: ['details', 'order', orderNumber, processingStatus, 'history'],
main: null,
left: ['results', orderNumber, processingStatus],
right: ['details', 'order', orderNumber, processingStatus, 'history'],
},
},
];
}
}
async navigateToCustomerOrdersSearch({
processId,
queryParams,
@@ -215,4 +283,26 @@ export class CustomerOrdersNavigationService extends NavigationService {
queryParamsHandling,
});
}
async navigateToHistory({
processId,
processingStatus,
compartmentCode,
orderNumber,
queryParams,
queryParamsHandling,
}: {
processId: number;
processingStatus: number;
compartmentCode?: string;
orderNumber?: string;
queryParams?: Record<string, string>;
queryParamsHandling?: 'merge' | 'preserve' | '';
}) {
await this._navigateTo({
routerLink: this.getCustomerOrdersHistoryPath({ processId, processingStatus, compartmentCode, orderNumber }),
queryParams,
queryParamsHandling,
});
}
}