#4185 OLA im Warenkorb

This commit is contained in:
Lorenz Hilpert
2023-08-16 14:54:14 +02:00
parent da79d04ef4
commit 807b300885
18 changed files with 454 additions and 40 deletions

View File

@@ -27,6 +27,7 @@ import {
StoreCheckoutPayerService,
StoreCheckoutBranchService,
ItemsResult,
ShoppingCartItemDTO,
} from '@swagger/checkout';
import {
DisplayOrderDTO,
@@ -36,20 +37,40 @@ import {
ResponseArgsOfValueTupleOfIEnumerableOfDisplayOrderDTOAndIEnumerableOfKeyValueDTOOfStringAndString,
} from '@swagger/oms';
import { isNullOrUndefined, memorize } from '@utils/common';
import { combineLatest, Observable, of, concat, isObservable, throwError } from 'rxjs';
import { bufferCount, catchError, filter, first, map, mergeMap, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { combineLatest, Observable, of, concat, isObservable, throwError, interval } from 'rxjs';
import {
bufferCount,
catchError,
distinctUntilChanged,
filter,
first,
map,
mergeMap,
shareReplay,
switchMap,
tap,
withLatestFrom,
} from 'rxjs/operators';
import * as DomainCheckoutSelectors from './store/domain-checkout.selectors';
import * as DomainCheckoutActions from './store/domain-checkout.actions';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainAvailabilityService, ItemData } from '@domain/availability';
import { HttpErrorResponse } from '@angular/common/http';
import { ApplicationService } from '@core/application';
import { CustomerDTO, EntityDTOContainerOfAttributeDTO } from '@swagger/crm';
import { CustomerDTO } from '@swagger/crm';
import { Config } from '@core/config';
import parseDuration from 'parse-duration';
@Injectable()
export class DomainCheckoutService {
get olaExpiration() {
const exp = this._config.get('@domain/checkout.olaExpiration') ?? '5m';
return parseDuration(exp);
}
constructor(
private store: Store<any>,
private _config: Config,
private applicationService: ApplicationService,
private storeCheckoutService: StoreCheckoutService,
private orderCheckoutService: OrderCheckoutService,
@@ -119,14 +140,14 @@ export class DomainCheckoutService {
})
.pipe(
map((response) => response.result),
tap((shoppingCart) =>
tap((shoppingCart) => {
this.store.dispatch(
DomainCheckoutActions.setShoppingCart({
processId,
shoppingCart,
})
)
),
);
}),
tap((shoppingCart) => this.updateProcessCount(processId, shoppingCart?.items?.length))
)
)
@@ -249,11 +270,24 @@ export class DomainCheckoutService {
shoppingCartItemId: number;
availability: AvailabilityDTO;
}) {
return this._shoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability({
shoppingCartId,
shoppingCartItemId,
availability,
});
return this._shoppingCartService
.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability({
shoppingCartId,
shoppingCartItemId,
availability,
})
.pipe(
map((response) => response.result),
tap((shoppingCart) => {
this.store.dispatch(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId({
shoppingCartId,
availability,
shoppingCartItemId,
})
);
})
);
}
updateItemInShoppingCart({
@@ -265,7 +299,7 @@ export class DomainCheckoutService {
shoppingCartItemId: number;
update: UpdateShoppingCartItemDTO;
}): Observable<ShoppingCartDTO> {
return this.getShoppingCart({ processId }).pipe(
return this.getShoppingCart({ processId, latest: true }).pipe(
first(),
mergeMap((shoppingCart) =>
this._shoppingCartService
@@ -276,8 +310,21 @@ export class DomainCheckoutService {
})
.pipe(
map((response) => response.result),
tap((shoppingCart) => this.store.dispatch(DomainCheckoutActions.setShoppingCart({ processId, shoppingCart }))),
tap((shoppingCart) => this.updateProcessCount(processId, shoppingCart?.items?.length))
tap((shoppingCart) => {
this.store.dispatch(DomainCheckoutActions.setShoppingCart({ processId, shoppingCart }));
if (update.availability) {
this.store.dispatch(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory({
processId,
availability: update.availability,
shoppingCartItemId,
})
);
}
this.updateProcessCount(processId, shoppingCart?.items?.length);
})
)
)
);
@@ -547,6 +594,148 @@ export class DomainCheckoutService {
);
}
async refreshAvailability({
processId,
shoppingCartItemId,
}: {
processId: number;
shoppingCartItemId: number;
}): Promise<AvailabilityDTO> {
const shoppingCart = await this.getShoppingCart({ processId }).pipe(first()).toPromise();
const item = shoppingCart?.items.find((item) => item.id === shoppingCartItemId)?.data;
if (!item) {
return;
}
const itemData: ItemData = {
ean: item.product.ean,
itemId: Number(item.product.catalogProductNumber),
price: item.availability.price,
};
let availability: AvailabilityDTO;
switch (item.features.orderType) {
case 'Abholung':
const abholung = await this.availabilityService
.getPickUpAvailability({
item: itemData,
branch: item.destination?.data?.targetBranch?.data,
quantity: item.quantity,
})
.toPromise();
availability = abholung[0];
break;
case 'Rücklage':
const ruecklage = await this.availabilityService
.getTakeAwayAvailability({
item: itemData,
quantity: item.quantity,
branch: item.destination?.data?.targetBranch?.data,
})
.toPromise();
availability = ruecklage;
break;
case 'Download':
const download = await this.availabilityService
.getDownloadAvailability({
item: itemData,
})
.toPromise();
availability = download;
break;
case 'Versand':
const versand = await this.availabilityService
.getDeliveryAvailability({
item: itemData,
quantity: item.quantity,
})
.toPromise();
availability = versand;
break;
case 'DIG-Versand':
const digVersand = await this.availabilityService
.getDigDeliveryAvailability({
item: itemData,
quantity: item.quantity,
})
.toPromise();
availability = digVersand;
break;
case 'B2B-Versand':
const b2bVersand = await this.availabilityService
.getB2bDeliveryAvailability({
item: itemData,
quantity: item.quantity,
})
.toPromise();
availability = b2bVersand;
break;
}
await this.updateItemInShoppingCart({
processId,
update: { availability },
shoppingCartItemId: item.id,
}).toPromise();
return availability;
}
validateOlaStatus({ processId }: { processId: number }): Observable<boolean> {
const enity$ = this.store.select(DomainCheckoutSelectors.selectCheckoutEntityByProcessId, { processId });
return combineLatest([enity$, interval(250)]).pipe(
mergeMap(([entity]) => {
const availabilityHistory = entity.availabilityHistory;
const shoppingCart = entity.shoppingCart;
const now = Date.now();
const exp = this.olaExpiration;
for (const item of shoppingCart.items.map((i) => i.data)) {
const latestAvailability = availabilityHistory
.filter((h) => h.shoppingCartItemId === item.id && h.type === item.features.orderType)
.reduce((prev, curr) => {
return prev.timestamp > curr.timestamp ? prev : curr;
});
if (latestAvailability.timestamp + exp < now) {
return of(false);
}
}
return of(true);
}),
distinctUntilChanged()
);
}
validateAvailabilities({ processId }: { processId: number }): Observable<boolean> {
return this.getShoppingCart({ processId }).pipe(
map((shoppingCart) => {
const items = shoppingCart?.items?.map((item) => item.data) || [];
return items.every((i) => this.availabilityService.isAvailable({ availability: i.availability }));
})
);
}
checkoutIsValid({ processId }: { processId: number }): Observable<boolean> {
const olaStatus$ = this.validateOlaStatus({ processId });
const availabilities$ = this.validateAvailabilities({ processId });
return combineLatest([olaStatus$, availabilities$]).pipe(map(([olaStatus, availabilities]) => olaStatus && availabilities));
}
completeCheckout({ processId }: { processId: number }): Observable<DisplayOrderDTO[]> {
const refreshShoppingCart$ = this.getShoppingCart({ processId, latest: true }).pipe(first());
const refreshCheckout$ = this.getCheckout({ processId, refresh: true }).pipe(first());
@@ -700,21 +889,23 @@ export class DomainCheckoutService {
)
);
return updateDestination$
.pipe(tap(console.log.bind(window, 'updateDestination$')))
return this.validateOlaStatus({ processId })
.pipe(
mergeMap((_) => updateDestination$.pipe(tap(console.log.bind(window, 'updateDestination$')))),
mergeMap((_) => refreshShoppingCart$.pipe(tap(console.log.bind(window, 'refreshShoppingCart$')))),
mergeMap((_) => setSpecialComment$.pipe(tap(console.log.bind(window, 'setSpecialComment$')))),
mergeMap((_) => refreshCheckout$.pipe(tap(console.log.bind(window, 'refreshCheckout$')))),
mergeMap((_) => checkAvailabilities$.pipe(tap(console.log.bind(window, 'checkAvailabilities$')))),
mergeMap((_) => updateAvailabilities$.pipe(tap(console.log.bind(window, 'updateAvailabilities$')))),
mergeMap((_) => updateAvailabilities$.pipe(tap(console.log.bind(window, 'updateAvailabilities$'))))
)
.pipe(
mergeMap((_) => setBuyer$.pipe(tap(console.log.bind(window, 'setBuyer$')))),
mergeMap((_) => setNotificationChannels$.pipe(tap(console.log.bind(window, 'setNotificationChannels$')))),
mergeMap((_) => setPayer$.pipe(tap(console.log.bind(window, 'setPayer$')))),
mergeMap((_) => setPaymentType$.pipe(tap(console.log.bind(window, 'setPaymentType$')))),
mergeMap((_) => setDestination$.pipe(tap(console.log.bind(window, 'setDestination$'))))
)
.pipe(mergeMap((_) => completeOrder$.pipe(tap(console.log.bind(window, 'completeOrder$')))));
mergeMap((_) => setDestination$.pipe(tap(console.log.bind(window, 'setDestination$')))),
mergeMap((_) => completeOrder$.pipe(tap(console.log.bind(window, 'completeOrder$'))))
);
}
completeKulturpassOrder({

View File

@@ -1,4 +1,12 @@
import { BuyerDTO, CheckoutDTO, NotificationChannel, PayerDTO, ShippingAddressDTO, ShoppingCartDTO } from '@swagger/checkout';
import {
AvailabilityDTO,
BuyerDTO,
CheckoutDTO,
NotificationChannel,
PayerDTO,
ShippingAddressDTO,
ShoppingCartDTO,
} from '@swagger/checkout';
import { CustomerDTO } from '@swagger/crm';
import { DisplayOrderDTO } from '@swagger/oms';
@@ -14,4 +22,5 @@ export interface CheckoutEntity {
specialComment: string;
notificationChannels: NotificationChannel;
olaErrorIds: number[];
availabilityHistory: Array<{ shoppingCartItemId: number; availability: AvailabilityDTO; timestamp: number; type: string }>;
}

View File

@@ -7,6 +7,7 @@ import {
ShippingAddressDTO,
BuyerDTO,
PayerDTO,
AvailabilityDTO,
} from '@swagger/checkout';
import { CustomerDTO } from '@swagger/crm';
import { DisplayOrderDTO, DisplayOrderItemDTO } from '@swagger/oms';
@@ -61,3 +62,13 @@ export const setSpecialComment = createAction(`${prefix} Set Agent Comment`, pro
export const setOlaError = createAction(`${prefix} Set Ola Error`, props<{ processId: number; olaErrorIds: number[] }>());
export const setCustomer = createAction(`${prefix} Set Customer`, props<{ processId: number; customer: CustomerDTO }>());
export const addShoppingCartItemAvailabilityToHistory = createAction(
`${prefix} Add Shopping Cart Item Availability To History`,
props<{ processId: number; shoppingCartItemId: number; availability: AvailabilityDTO }>()
);
export const addShoppingCartItemAvailabilityToHistoryByShoppingCartId = createAction(
`${prefix} Add Shopping Cart Item Availability To History By Shopping Cart Id`,
props<{ shoppingCartId: number; shoppingCartItemId: number; availability: AvailabilityDTO }>()
);

View File

@@ -10,7 +10,26 @@ const _domainCheckoutReducer = createReducer(
initialCheckoutState,
on(DomainCheckoutActions.setShoppingCart, (s, { processId, shoppingCart }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
const addedShoppingCartItems =
shoppingCart?.items?.filter((item) => !entity.shoppingCart?.items?.find((i) => i.id === item.id))?.map((item) => item.data) ?? [];
entity.shoppingCart = shoppingCart;
for (let shoppingCartItem of addedShoppingCartItems) {
if (!shoppingCartItem.features?.orderType) continue;
entity.availabilityHistory = [
...(entity?.availabilityHistory ?? []),
{
availability: shoppingCartItem.availability,
shoppingCartItemId: shoppingCartItem.id,
timestamp: Date.now(),
type: shoppingCartItem.features.orderType,
},
];
}
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setCheckout, (s, { processId, checkout }) => {
@@ -100,7 +119,36 @@ const _domainCheckoutReducer = createReducer(
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.customer = customer;
return storeCheckoutAdapter.setOne(entity, s);
})
}),
on(DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory, (s, { processId, shoppingCartItemId, availability }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
const availabilityHistory = entity?.availabilityHistory ? [...entity?.availabilityHistory] : [];
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId).data;
availabilityHistory.push({ availability, shoppingCartItemId, timestamp: Date.now(), type: item.features.orderType });
entity.availabilityHistory = availabilityHistory;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId,
(s, { shoppingCartId, shoppingCartItemId, availability }) => {
const entity = getCheckoutEntityByShoppingCartId({ shoppingCartId, entities: s.entities });
const availabilityHistory = entity?.availabilityHistory ? [...entity?.availabilityHistory] : [];
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId).data;
availabilityHistory.push({ availability, shoppingCartItemId, timestamp: Date.now(), type: item.features.orderType });
entity.availabilityHistory = availabilityHistory;
return storeCheckoutAdapter.setOne(entity, s);
}
)
);
export function domainCheckoutReducer(state, action) {
@@ -123,8 +171,19 @@ function getOrCreateCheckoutEntity({ entities, processId }: { entities: Dictiona
notificationChannels: 0,
olaErrorIds: [],
customer: undefined,
availabilityHistory: [],
};
}
return { ...entity };
}
function getCheckoutEntityByShoppingCartId({
entities,
shoppingCartId,
}: {
entities: Dictionary<CheckoutEntity>;
shoppingCartId: number;
}): CheckoutEntity {
return Object.values(entities).find((entity) => entity.shoppingCart?.id === shoppingCartId);
}

View File

@@ -16,6 +16,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
},

View File

@@ -15,6 +15,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa-integration.paragon-data.net/isa/v1"
},

View File

@@ -47,6 +47,9 @@
"@swagger/wws": {
"rootUrl": "https://isa-test.paragon-data.net/wws/v1"
},
"@domain/checkout": {
"olaExpiration": "1m"
},
"hubs": {
"notifications": {
"url": "https://isa-test.paragon-data.net/isa/v1/rt",

View File

@@ -16,6 +16,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa.paragon-systems.de/isa/v1"
},

View File

@@ -16,6 +16,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa-staging.paragon-systems.de/isa/v1"
},

View File

@@ -17,6 +17,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
},

View File

@@ -126,7 +126,9 @@
[disabled]="
showOrderButtonSpinner ||
((primaryCtaLabel$ | async) === 'Bestellen' && !(checkNotificationChannelControl$ | async)) ||
notificationsControl?.invalid
notificationsControl?.invalid ||
(checkingOla$ | async) ||
(checkoutIsInValid$ | async)
"
>
<ui-spinner [show]="showOrderButtonSpinner">

View File

@@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { DomainAvailabilityService } from '@domain/availability';
@@ -6,8 +6,8 @@ import { DomainCheckoutService } from '@domain/checkout';
import { AvailabilityDTO, DestinationDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
import { PrintModalData, PrintModalComponent } from '@modal/printer';
import { first, map, shareReplay, switchMap, takeUntil } from 'rxjs/operators';
import { Subject, NEVER, combineLatest, BehaviorSubject } from 'rxjs';
import { catchError, delay, first, map, shareReplay, switchMap, takeUntil } from 'rxjs/operators';
import { Subject, NEVER, combineLatest, BehaviorSubject, interval, of, merge } from 'rxjs';
import { DomainCatalogService } from '@domain/catalog';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainPrinterService } from '@domain/printer';
@@ -17,6 +17,8 @@ import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-mod
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
import { EnvironmentService } from '@core/environment';
import { CheckoutReviewStore } from './checkout-review.store';
import { ToasterService } from '@shared/shell';
import { ShoppingCartItemComponent } from './shopping-cart-item/shopping-cart-item.component';
@Component({
selector: 'page-checkout-review',
@@ -25,6 +27,8 @@ import { CheckoutReviewStore } from './checkout-review.store';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckoutReviewComponent implements OnInit, OnDestroy {
checkingOla$ = new BehaviorSubject<boolean>(false);
payer$ = this._store.payer$;
buyer$ = this._store.buyer$;
@@ -149,6 +153,11 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
loadingOnQuantityChangeById$ = new Subject<number>();
showOrderButtonSpinner: boolean;
checkoutIsInValid$ = this.applicationService.activatedProcessId$.pipe(
switchMap((processId) => this.domainCheckoutService.checkoutIsValid({ processId })),
map((valid) => !valid)
);
get productSearchBasePath() {
return this._productNavigationService.getArticleSearchBasePath(this.applicationService.activatedProcessId);
}
@@ -159,6 +168,9 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
private _onDestroy$ = new Subject<void>();
@ViewChildren(ShoppingCartItemComponent)
private _shoppingCartItems: QueryList<ShoppingCartItemComponent>;
constructor(
private domainCheckoutService: DomainCheckoutService,
public applicationService: ApplicationService,
@@ -173,7 +185,8 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
private _productNavigationService: ProductCatalogNavigationService,
private _navigationService: CheckoutNavigationService,
private _environmentService: EnvironmentService,
private _store: CheckoutReviewStore
private _store: CheckoutReviewStore,
private _toaster: ToasterService
) {}
async ngOnInit() {
@@ -183,6 +196,12 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
await this.removeBreadcrumbs();
await this.updateBreadcrumb();
this.registerOlaCechk();
window['Checkout'] = {
refreshAvailabilities: this.refreshAvailabilities.bind(this),
};
}
ngOnDestroy(): void {
@@ -191,6 +210,29 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
this._onDestroy$.complete();
}
registerOlaCechk() {
this.applicationService.activatedProcessId$
.pipe(
takeUntil(this._onDestroy$),
switchMap((processId) =>
this.domainCheckoutService.validateOlaStatus({
processId,
})
)
)
.subscribe((result) => {
this.refreshAvailabilities();
});
}
async refreshAvailabilities() {
this.checkingOla$.next(true);
for (let itemComp of this._shoppingCartItems.toArray()) {
await itemComp.refreshAvailability();
}
this.checkingOla$.next(false);
}
async updateBreadcrumb() {
await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId,
@@ -476,6 +518,8 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
title: 'Hinweis',
data: { message: message.trim() },
});
} else if (error) {
this.uiModal.error('Fehler beim abschließen der Bestellung', error);
}
if (error.status === 409) {

View File

@@ -20,6 +20,7 @@ import { CheckoutReviewDetailsComponent } from './details/checkout-review-detail
import { CheckoutReviewStore } from './checkout-review.store';
import { IconModule } from '@shared/components/icon';
import { TextFieldModule } from '@angular/cdk/text-field';
import { LoaderComponent } from '@shared/components/loader';
@NgModule({
imports: [
@@ -39,6 +40,7 @@ import { TextFieldModule } from '@angular/cdk/text-field';
UiCheckboxModule,
SharedNotificationChannelControlModule,
TextFieldModule,
LoaderComponent,
],
exports: [CheckoutReviewComponent, CheckoutReviewDetailsComponent],
declarations: [CheckoutReviewComponent, SpecialCommentComponent, ShoppingCartItemComponent, CheckoutReviewDetailsComponent],

View File

@@ -40,16 +40,26 @@
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
{{ item?.product?.publicationDate | date }}
</div>
<div class="item-date" *ngIf="orderType === 'Abholung'">Abholung ab {{ item?.availability?.estimatedShippingDate | date }}</div>
<div class="item-date" *ngIf="orderType === 'Versand' || orderType === 'B2B-Versand' || orderType === 'DIG-Versand'">
<ng-container *ngIf="item?.availability?.estimatedDelivery; else estimatedShippingDate">
Zustellung zwischen {{ (item?.availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }}
und
{{ (item?.availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
</ng-container>
<ng-template #estimatedShippingDate> Versand {{ item?.availability?.estimatedShippingDate | date }} </ng-template>
<div *ngIf="notAvailable$ | async">
<span class="text-brand item-date">Nicht verfügbar</span>
</div>
<ui-spinner class="ava-loader" [show]="refreshingAvailabilit$ | async">
<div class="item-date" [class.ssc-changed]="sscChanged$ | async" *ngIf="orderType === 'Abholung'">
Abholung ab {{ item?.availability?.estimatedShippingDate | date }}
</div>
<div
class="item-date"
[class.ssc-changed]="sscChanged$ | async"
*ngIf="orderType === 'Versand' || orderType === 'B2B-Versand' || orderType === 'DIG-Versand'"
>
<ng-container *ngIf="item?.availability?.estimatedDelivery; else estimatedShippingDate">
Zustellung zwischen {{ (item?.availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }}
und
{{ (item?.availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
</ng-container>
<ng-template #estimatedShippingDate> Versand {{ item?.availability?.estimatedShippingDate | date }} </ng-template>
</div>
</ui-spinner>
<div class="item-availability-message" *ngIf="olaError$ | async">
Artikel nicht verfügbar

View File

@@ -101,8 +101,16 @@ button {
}
}
.ssc-changed {
@apply text-dark-goldenrod;
}
::ng-deep page-shopping-cart-item ui-quantity-dropdown {
.current-quantity {
font-weight: normal !important;
}
}
::ng-deep page-shopping-cart-item .ava-loader ui-icon {
left: 0 !important;
}

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { DomainAvailabilityService } from '@domain/availability';
@@ -6,6 +6,7 @@ import { DomainCheckoutService } from '@domain/checkout';
import { ComponentStore } from '@ngrx/component-store';
import { ProductCatalogNavigationService } from '@shared/services';
import { ItemType, ShoppingCartItemDTO } from '@swagger/checkout';
import { cloneDeep } from 'lodash';
import { combineLatest } from 'rxjs';
import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
@@ -14,6 +15,9 @@ export interface ShoppingCartItemComponentState {
orderType: string;
loadingOnItemChangeById?: number;
loadingOnQuantityChangeById?: number;
refreshingAvailability: boolean;
sscChanged: boolean;
sscTextChanged: boolean;
}
@Component({
@@ -127,14 +131,35 @@ export class ShoppingCartItemComponent extends ComponentStore<ShoppingCartItemCo
return this._environment.matchTablet();
}
refreshingAvailabilit$ = this.select((s) => s.refreshingAvailability);
sscChanged$ = this.select((s) => s.sscChanged || s.sscTextChanged);
notAvailable$ = this.item$.pipe(
map((item) => {
const availability = item?.availability;
if (availability.availabilityType === 0) {
return false;
}
if (availability.inStock && item.quantity > availability.inStock) {
return true;
}
return !this.availabilityService.isAvailable({ availability });
})
);
constructor(
private availabilityService: DomainAvailabilityService,
private checkoutService: DomainCheckoutService,
public application: ApplicationService,
private _productNavigationService: ProductCatalogNavigationService,
private _environment: EnvironmentService
private _environment: EnvironmentService,
private _cdr: ChangeDetectorRef
) {
super({ item: undefined, orderType: '' });
super({ item: undefined, orderType: '', refreshingAvailability: false, sscChanged: false, sscTextChanged: false });
}
ngOnInit() {}
@@ -147,4 +172,27 @@ export class ShoppingCartItemComponent extends ComponentStore<ShoppingCartItemCo
onChangeQuantity(quantity: number) {
this.changeQuantity.emit({ shoppingCartItem: this.item, quantity });
}
async refreshAvailability() {
const currentAvailability = cloneDeep(this.item.availability);
try {
this.patchState({ refreshingAvailability: true });
this._cdr.markForCheck();
const availability = await this.checkoutService.refreshAvailability({
processId: this.application.activatedProcessId,
shoppingCartItemId: this.item.id,
});
if (currentAvailability.ssc !== availability.ssc) {
this.patchState({ sscChanged: true });
}
if (currentAvailability.sscText !== availability.sscText) {
this.patchState({ sscTextChanged: true });
}
} catch (error) {}
this.patchState({ refreshingAvailability: false });
this._cdr.markForCheck();
}
}

11
package-lock.json generated
View File

@@ -34,6 +34,7 @@
"lodash": "^4.17.21",
"moment": "^2.29.4",
"ng2-pdf-viewer": "^9.1.3",
"parse-duration": "^1.1.0",
"rxjs": "^6.6.7",
"scandit-sdk": "^5.13.2",
"socket.io": "^4.5.4",
@@ -11303,6 +11304,11 @@
"node": ">=6"
}
},
"node_modules/parse-duration": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.1.0.tgz",
"integrity": "sha512-z6t9dvSJYaPoQq7quMzdEagSFtpGu+utzHqqxmpVWNNZRIXnvqyCvn9XsTdh7c/w0Bqmdz3RB3YnRaKtpRtEXQ=="
},
"node_modules/parse-entities": {
"version": "2.0.0",
"dev": true,
@@ -23615,6 +23621,11 @@
"callsites": "^3.0.0"
}
},
"parse-duration": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.1.0.tgz",
"integrity": "sha512-z6t9dvSJYaPoQq7quMzdEagSFtpGu+utzHqqxmpVWNNZRIXnvqyCvn9XsTdh7c/w0Bqmdz3RB3YnRaKtpRtEXQ=="
},
"parse-entities": {
"version": "2.0.0",
"dev": true,

View File

@@ -88,6 +88,7 @@
"lodash": "^4.17.21",
"moment": "^2.29.4",
"ng2-pdf-viewer": "^9.1.3",
"parse-duration": "^1.1.0",
"rxjs": "^6.6.7",
"scandit-sdk": "^5.13.2",
"socket.io": "^4.5.4",