mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1513: Kaufoptionen
Related work items: #3365, #3366, #3385, #3386, #3391
This commit is contained in:
committed by
Nino Righi
parent
80bfc59356
commit
f4c1c3dd7f
@@ -34,6 +34,11 @@ export class DomainAvailabilityService {
|
||||
private _branchService: StoreCheckoutBranchService
|
||||
) {}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
memorizedAvailabilityShippingAvailability(request: Array<AvailabilityRequestDTO>) {
|
||||
return this._availabilityService.AvailabilityShippingAvailability(request).pipe(shareReplay(1));
|
||||
}
|
||||
|
||||
@memorize()
|
||||
getSuppliers(): Observable<SupplierDTO[]> {
|
||||
return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe(
|
||||
@@ -249,57 +254,53 @@ export class DomainAvailabilityService {
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService
|
||||
.AvailabilityShippingAvailability([
|
||||
{
|
||||
ean: item?.ean,
|
||||
itemId: item?.itemId ? String(item?.itemId) : null,
|
||||
price: item?.price,
|
||||
qty: quantity,
|
||||
},
|
||||
])
|
||||
.pipe(
|
||||
timeout(5000),
|
||||
map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)),
|
||||
shareReplay(1)
|
||||
);
|
||||
return this.memorizedAvailabilityShippingAvailability([
|
||||
{
|
||||
ean: item?.ean,
|
||||
itemId: item?.itemId ? String(item?.itemId) : null,
|
||||
price: item?.price,
|
||||
qty: quantity,
|
||||
},
|
||||
]).pipe(
|
||||
timeout(5000),
|
||||
map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDigDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService
|
||||
.AvailabilityShippingAvailability([
|
||||
{
|
||||
qty: quantity,
|
||||
ean: item?.ean,
|
||||
itemId: item?.itemId ? String(item?.itemId) : null,
|
||||
price: item?.price,
|
||||
},
|
||||
])
|
||||
.pipe(
|
||||
timeout(5000),
|
||||
map((r) => {
|
||||
const availabilities = r.result;
|
||||
const preferred = availabilities?.find((f) => f.preferred === 1);
|
||||
return this.memorizedAvailabilityShippingAvailability([
|
||||
{
|
||||
qty: quantity,
|
||||
ean: item?.ean,
|
||||
itemId: item?.itemId ? String(item?.itemId) : null,
|
||||
price: item?.price,
|
||||
},
|
||||
]).pipe(
|
||||
timeout(5000),
|
||||
map((r) => {
|
||||
const availabilities = r.result;
|
||||
const preferred = availabilities?.find((f) => f.preferred === 1);
|
||||
|
||||
const availability: AvailabilityDTO = {
|
||||
availabilityType: preferred?.status,
|
||||
ssc: preferred?.ssc,
|
||||
sscText: preferred?.sscText,
|
||||
supplier: { id: preferred?.supplierId },
|
||||
isPrebooked: preferred?.isPrebooked,
|
||||
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
|
||||
estimatedDelivery: preferred?.estimatedDelivery,
|
||||
price: preferred?.price,
|
||||
logistician: { id: preferred?.logisticianId },
|
||||
supplierProductNumber: preferred?.supplierProductNumber,
|
||||
supplierInfo: preferred?.requestStatusCode,
|
||||
lastRequest: preferred?.requested,
|
||||
};
|
||||
return availability;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
const availability: AvailabilityDTO = {
|
||||
availabilityType: preferred?.status,
|
||||
ssc: preferred?.ssc,
|
||||
sscText: preferred?.sscText,
|
||||
supplier: { id: preferred?.supplierId },
|
||||
isPrebooked: preferred?.isPrebooked,
|
||||
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
|
||||
estimatedDelivery: preferred?.estimatedDelivery,
|
||||
price: preferred?.price,
|
||||
logistician: { id: preferred?.logisticianId },
|
||||
supplierProductNumber: preferred?.supplierProductNumber,
|
||||
supplierInfo: preferred?.requestStatusCode,
|
||||
lastRequest: preferred?.requested,
|
||||
};
|
||||
return availability;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
@@ -333,37 +334,35 @@ export class DomainAvailabilityService {
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDownloadAvailability({ item }: { item: ItemData }): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService
|
||||
.AvailabilityShippingAvailability([
|
||||
{
|
||||
ean: item?.ean,
|
||||
itemId: item?.itemId ? String(item?.itemId) : null,
|
||||
price: item?.price,
|
||||
qty: 1,
|
||||
},
|
||||
])
|
||||
.pipe(
|
||||
map((r) => {
|
||||
const availabilities = r.result;
|
||||
const preferred = availabilities?.find((f) => f.preferred === 1);
|
||||
return this.memorizedAvailabilityShippingAvailability([
|
||||
{
|
||||
ean: item?.ean,
|
||||
itemId: item?.itemId ? String(item?.itemId) : null,
|
||||
price: item?.price,
|
||||
qty: 1,
|
||||
},
|
||||
]).pipe(
|
||||
map((r) => {
|
||||
const availabilities = r.result;
|
||||
const preferred = availabilities?.find((f) => f.preferred === 1);
|
||||
|
||||
const availability: AvailabilityDTO = {
|
||||
availabilityType: preferred?.status,
|
||||
ssc: preferred?.ssc,
|
||||
sscText: preferred?.sscText,
|
||||
supplier: { id: preferred?.supplierId },
|
||||
isPrebooked: preferred?.isPrebooked,
|
||||
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
|
||||
price: preferred?.price,
|
||||
supplierProductNumber: preferred?.supplierProductNumber,
|
||||
logistician: { id: preferred?.logisticianId },
|
||||
supplierInfo: preferred?.requestStatusCode,
|
||||
lastRequest: preferred?.requested,
|
||||
};
|
||||
return availability;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
const availability: AvailabilityDTO = {
|
||||
availabilityType: preferred?.status,
|
||||
ssc: preferred?.ssc,
|
||||
sscText: preferred?.sscText,
|
||||
supplier: { id: preferred?.supplierId },
|
||||
isPrebooked: preferred?.isPrebooked,
|
||||
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
|
||||
price: preferred?.price,
|
||||
supplierProductNumber: preferred?.supplierProductNumber,
|
||||
logistician: { id: preferred?.logisticianId },
|
||||
supplierInfo: preferred?.requestStatusCode,
|
||||
lastRequest: preferred?.requested,
|
||||
};
|
||||
return availability;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
@@ -401,7 +400,7 @@ export class DomainAvailabilityService {
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
|
||||
return this._availabilityService.AvailabilityShippingAvailability(payload).pipe(
|
||||
return this.memorizedAvailabilityShippingAvailability(payload).pipe(
|
||||
timeout(20000),
|
||||
map((response) => this._mapToShippingAvailability(response.result))
|
||||
);
|
||||
@@ -409,7 +408,7 @@ export class DomainAvailabilityService {
|
||||
|
||||
@memorize({ ttl: 10000 })
|
||||
getDigDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
|
||||
return this._availabilityService.AvailabilityShippingAvailability(payload).pipe(
|
||||
return this.memorizedAvailabilityShippingAvailability(payload).pipe(
|
||||
timeout(20000),
|
||||
map((response) => this._mapToShippingAvailability(response.result))
|
||||
);
|
||||
@@ -447,6 +446,9 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
|
||||
isAvailable({ availability }: { availability: AvailabilityDTO }) {
|
||||
if (availability?.supplier?.id === 16 && availability?.inStock == 0) {
|
||||
return false;
|
||||
}
|
||||
return [2, 32, 256, 1024, 2048, 4096].some((code) => availability?.availabilityType === code);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
StoreCheckoutBuyerService,
|
||||
StoreCheckoutPayerService,
|
||||
StoreCheckoutBranchService,
|
||||
ItemsResult,
|
||||
} from '@swagger/checkout';
|
||||
import { DisplayOrderDTO, DisplayOrderItemDTO, OrderCheckoutService, ReorderValues } from '@swagger/oms';
|
||||
import { isNullOrUndefined, memorize } from '@utils/common';
|
||||
@@ -198,7 +199,15 @@ export class DomainCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
canAddItems({ processId, payload, orderType }: { processId: number; payload: ItemPayload[]; orderType: string }) {
|
||||
canAddItems({
|
||||
processId,
|
||||
payload,
|
||||
orderType,
|
||||
}: {
|
||||
processId: number;
|
||||
payload: ItemPayload[];
|
||||
orderType: string;
|
||||
}): Observable<ItemsResult[]> {
|
||||
return this.getShoppingCart({ processId }).pipe(
|
||||
first(),
|
||||
withLatestFrom(this.store.select(DomainCheckoutSelectors.selectCustomerFeaturesByProcessId, { processId })),
|
||||
@@ -217,7 +226,8 @@ export class DomainCheckoutService {
|
||||
})
|
||||
.pipe(
|
||||
map((response) => {
|
||||
return response.result;
|
||||
// TODO: remove this when the API is fixed
|
||||
return (response.result as unknown) as ItemsResult[];
|
||||
})
|
||||
);
|
||||
})
|
||||
|
||||
@@ -107,6 +107,25 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
|
||||
aliases: [
|
||||
{ alias: 'd-account', name: 'account' },
|
||||
{ alias: 'd-no-account', name: 'package-variant-closed' },
|
||||
{ name: 'isa-audio', alias: 'AU' },
|
||||
{ name: 'isa-audio', alias: 'CAS' },
|
||||
{ name: 'isa-audio', alias: 'DL' },
|
||||
{ name: 'isa-audio', alias: 'KAS' },
|
||||
{ name: 'isa-hard-cover', alias: 'BUCH' },
|
||||
{ name: 'isa-hard-cover', alias: 'GEB' },
|
||||
{ name: 'isa-hard-cover', alias: 'HC' },
|
||||
{ name: 'isa-hard-cover', alias: 'KT' },
|
||||
{ name: 'isa-ebook', alias: 'EB' },
|
||||
{ name: 'isa-non-book', alias: 'GLO' },
|
||||
{ name: 'isa-non-book', alias: 'HDL' },
|
||||
{ name: 'isa-non-book', alias: 'NB' },
|
||||
{ name: 'isa-non-book', alias: 'SPL' },
|
||||
{ name: 'isa-calendar', alias: 'KA' },
|
||||
{ name: 'isa-scroll', alias: 'MA' },
|
||||
{ name: 'isa-software', alias: 'SW' },
|
||||
{ name: 'isa-soft-cover', alias: 'TB' },
|
||||
{ name: 'isa-video', alias: 'VI' },
|
||||
{ name: 'isa-news-paper', alias: 'ZS' },
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -4,11 +4,9 @@ import { ApplicationService } from '@core/application';
|
||||
import { DomainPrinterService } from '@domain/printer';
|
||||
import { ItemDTO as PrinterItemDTO } from '@swagger/print';
|
||||
import { PrintModalComponent, PrintModalData } from '@modal/printer';
|
||||
import { AvailabilityDTO, BranchDTO } from '@swagger/checkout';
|
||||
import { BranchDTO } from '@swagger/checkout';
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { ModalReviewsComponent } from '@modal/reviews';
|
||||
import { PurchasingOptionsModalComponent, PurchasingOptionsModalData } from 'apps/page/checkout/src/lib/modals/purchasing-options-modal';
|
||||
import { PurchasingOptions } from 'apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.store';
|
||||
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
|
||||
import { debounceTime, filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
import { ArticleDetailsStore } from './article-details.store';
|
||||
@@ -20,6 +18,7 @@ import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { ItemDTO } from '@swagger/cat';
|
||||
import { DateAdapter } from '@ui/common';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
|
||||
@Component({
|
||||
@@ -125,6 +124,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
private _dateAdapter: DateAdapter,
|
||||
private _datePipe: DatePipe,
|
||||
public elementRef: ElementRef,
|
||||
private _purchaseOptionsModalService: PurchaseOptionsModalService,
|
||||
private _availability: DomainAvailabilityService
|
||||
) {}
|
||||
|
||||
@@ -262,58 +262,12 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async showPurchasingModal(selectedBranch?: BranchDTO) {
|
||||
let availableOptions: PurchasingOptions[] = [];
|
||||
const availabilities: { [key: string]: AvailabilityDTO } = {};
|
||||
const item = await this.store.item$.pipe(first()).toPromise();
|
||||
|
||||
const takeNow = await this.store.isTakeAwayAvailabilityAvailable$.pipe(first()).toPromise();
|
||||
if (takeNow) {
|
||||
availableOptions.push('take-away');
|
||||
availabilities['take-away'] = await this.store.takeAwayAvailability$.pipe(first()).toPromise();
|
||||
}
|
||||
|
||||
const download = await this.store.isDownloadAvailabilityAvailable$.pipe(first()).toPromise();
|
||||
if (download) {
|
||||
availableOptions.push('download');
|
||||
availabilities['download'] = await this.store.downloadAvailability$.pipe(first()).toPromise();
|
||||
}
|
||||
|
||||
const pickup = await this.store.isPickUpAvailabilityAvailable$.pipe(first()).toPromise();
|
||||
if (pickup) {
|
||||
availableOptions.push('pick-up');
|
||||
availabilities['pick-up'] = await this.store.pickUpAvailability$.pipe(first()).toPromise();
|
||||
}
|
||||
|
||||
const digDelivery = await this.store.isDeliveryDigAvailabilityAvailable$.pipe(first()).toPromise();
|
||||
if (digDelivery) {
|
||||
availableOptions.push('dig-delivery');
|
||||
availabilities['dig-delivery'] = await this.store.deliveryDigAvailability$.pipe(first()).toPromise();
|
||||
}
|
||||
|
||||
const b2b = await this.store.isDeliveryB2BAvailabilityAvailable$.pipe(first()).toPromise();
|
||||
|
||||
if (b2b) {
|
||||
availableOptions.push('b2b-delivery');
|
||||
availabilities['b2b-delivery'] = await this.store.deliveryB2BAvailability$.pipe(first()).toPromise();
|
||||
}
|
||||
|
||||
if (availableOptions.includes('dig-delivery') && availableOptions.includes('b2b-delivery')) {
|
||||
availableOptions.push('delivery');
|
||||
availabilities['delivery'] = await this.store.deliveryAvailability$.pipe(first()).toPromise();
|
||||
availableOptions = availableOptions.filter((option) => !(option === 'dig-delivery' || option === 'b2b-delivery'));
|
||||
}
|
||||
|
||||
const branch = selectedBranch || (await this.store.branch$.pipe(first()).toPromise());
|
||||
|
||||
this.uiModal.open({
|
||||
content: PurchasingOptionsModalComponent,
|
||||
data: {
|
||||
availableOptions,
|
||||
option: selectedBranch ? 'take-away' : undefined,
|
||||
item: await this.store.item$.pipe(first()).toPromise(),
|
||||
branchId: branch?.id,
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
availabilities,
|
||||
} as PurchasingOptionsModalData,
|
||||
this._purchaseOptionsModalService.open({
|
||||
type: 'add',
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
items: [item],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -241,6 +241,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
|
||||
// Zeige Select Radio Button nicht an wenn Item Archivartikel oder Fortsetzungsartikel ist
|
||||
const isArchiv = item?.catalogAvailability?.status === 1;
|
||||
const isFortsetzung = item?.features?.find((i) => i?.key === 'PFO');
|
||||
|
||||
return !(isArchiv || isFortsetzung);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { AvailabilityDTO, DestinationDTO, NotificationChannel, ShoppingCartItemDTO, ShoppingCartDTO } from '@swagger/checkout';
|
||||
import { UiErrorModalComponent, UiMessageModalComponent, UiModalService } from '@ui/modal';
|
||||
import { PrintModalData, PrintModalComponent } from '@modal/printer';
|
||||
import { PurchasingOptionsModalComponent, PurchasingOptionsModalData } from '../modals/purchasing-options-modal';
|
||||
import { PurchasingOptions } from '../modals/purchasing-options-modal/purchasing-options-modal.store';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { first, map, shareReplay, switchMap, take, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { Subject, NEVER, combineLatest, BehaviorSubject } from 'rxjs';
|
||||
@@ -15,13 +13,11 @@ import { DomainCatalogService } from '@domain/catalog';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { DomainPrinterService } from '@domain/printer';
|
||||
import { CheckoutDummyComponent } from '../checkout-dummy/checkout-dummy.component';
|
||||
import { ResponseArgsOfItemDTO } from '@swagger/cat';
|
||||
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
|
||||
import { emailNotificationValidator, mobileNotificationValidator } from '@shared/components/notification-channel-control';
|
||||
import { PurchasingOptionsListModalComponent } from '../modals/purchasing-options-list-modal';
|
||||
import { PurchasingOptionsListModalData } from '../modals/purchasing-options-list-modal/purchasing-options-list-modal.data';
|
||||
import { ComponentStore, tapResponse } from '@ngrx/component-store';
|
||||
import { CheckoutDummyData } from '../checkout-dummy/checkout-dummy-data';
|
||||
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
|
||||
|
||||
export interface CheckoutReviewComponentState {
|
||||
shoppingCart: ShoppingCartDTO;
|
||||
@@ -242,7 +238,8 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
|
||||
private domainCatalogService: DomainCatalogService,
|
||||
private breadcrumb: BreadcrumbService,
|
||||
private domainPrinterService: DomainPrinterService,
|
||||
private _fb: UntypedFormBuilder
|
||||
private _fb: UntypedFormBuilder,
|
||||
private _purchaseOptionsModalService: PurchaseOptionsModalService
|
||||
) {
|
||||
super({
|
||||
shoppingCart: undefined,
|
||||
@@ -434,171 +431,10 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
|
||||
}
|
||||
|
||||
async changeItem({ shoppingCartItem }: { shoppingCartItem: ShoppingCartItemDTO }) {
|
||||
this.loadingOnItemChangeById$.next(shoppingCartItem.id);
|
||||
|
||||
const quantity = shoppingCartItem.quantity;
|
||||
|
||||
const branchNo = this.auth.getClaimByKey('branch_no');
|
||||
const branchId = shoppingCartItem?.destination?.data?.targetBranch?.id;
|
||||
|
||||
const customerFeatures = await this.customerFeatures$.pipe(first()).toPromise();
|
||||
|
||||
let branch = await this.domainCheckoutService
|
||||
.getBranches()
|
||||
.pipe(map((branches) => branches.find((branch) => (branchId ? branch.id === branchId : branch.branchNumber === branchNo))))
|
||||
.toPromise();
|
||||
|
||||
if (!branch) {
|
||||
branch = await this.applicationService.getSelectedBranch$().pipe(take(1)).toPromise();
|
||||
}
|
||||
|
||||
let catalogItem: ResponseArgsOfItemDTO;
|
||||
if (Number.isInteger(shoppingCartItem?.product?.catalogProductNumber)) {
|
||||
catalogItem = await this.domainCatalogService
|
||||
.getDetailsById({ id: Number(shoppingCartItem.product.catalogProductNumber) })
|
||||
.toPromise();
|
||||
} else if (shoppingCartItem?.product?.ean) {
|
||||
catalogItem = await this.domainCatalogService.getDetailsByEan({ ean: shoppingCartItem.product.ean }).toPromise();
|
||||
}
|
||||
|
||||
let takeAwayAvailability: AvailabilityDTO;
|
||||
if (!!catalogItem?.result?.product) {
|
||||
takeAwayAvailability = await this.availabilityService
|
||||
.getTakeAwayAvailability({
|
||||
item: {
|
||||
itemId: catalogItem.result.id,
|
||||
ean: catalogItem.result.product.ean,
|
||||
price: catalogItem.result.catalogAvailability?.price,
|
||||
},
|
||||
quantity,
|
||||
})
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
const pickupAvailability = await this.availabilityService
|
||||
.getPickUpAvailability({
|
||||
item: {
|
||||
itemId: Number(shoppingCartItem.product.catalogProductNumber),
|
||||
ean: shoppingCartItem.product.ean,
|
||||
price: shoppingCartItem.availability.price,
|
||||
},
|
||||
branch,
|
||||
quantity,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
const digAvailability = await this.availabilityService
|
||||
.getDigDeliveryAvailability({
|
||||
item: {
|
||||
itemId: Number(shoppingCartItem.product.catalogProductNumber),
|
||||
ean: shoppingCartItem.product.ean,
|
||||
price: shoppingCartItem.availability.price,
|
||||
},
|
||||
quantity,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
const b2bAvailability = await this.availabilityService
|
||||
.getB2bDeliveryAvailability({
|
||||
item: {
|
||||
itemId: Number(shoppingCartItem.product.catalogProductNumber),
|
||||
ean: shoppingCartItem.product.ean,
|
||||
price: shoppingCartItem.availability.price,
|
||||
},
|
||||
quantity,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
const downloadAvailability = await this.availabilityService
|
||||
.getDownloadAvailability({
|
||||
item: {
|
||||
itemId: Number(shoppingCartItem.product.catalogProductNumber),
|
||||
ean: shoppingCartItem.product.ean,
|
||||
price: shoppingCartItem.availability.price,
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
let availableOptions: PurchasingOptions[] = [];
|
||||
const availabilities: { [key: string]: AvailabilityDTO } = {};
|
||||
|
||||
if (takeAwayAvailability && this.availabilityService.isAvailable({ availability: takeAwayAvailability })) {
|
||||
availableOptions.push('take-away');
|
||||
availabilities['take-away'] = takeAwayAvailability;
|
||||
}
|
||||
|
||||
if (downloadAvailability && this.availabilityService.isAvailable({ availability: downloadAvailability })) {
|
||||
availableOptions.push('download');
|
||||
availabilities['download'] = downloadAvailability;
|
||||
}
|
||||
|
||||
if (pickupAvailability && this.availabilityService.isAvailable({ availability: pickupAvailability[0] })) {
|
||||
if (pickupAvailability[1].availableFor) {
|
||||
if ((pickupAvailability[1].availableFor & 2) === 2) {
|
||||
availableOptions.push('pick-up');
|
||||
availabilities['pick-up'] = pickupAvailability[0];
|
||||
}
|
||||
} else {
|
||||
availableOptions.push('pick-up');
|
||||
availabilities['pick-up'] = pickupAvailability[0];
|
||||
}
|
||||
|
||||
if (!customerFeatures?.webshop && this.availabilityService.isAvailable({ availability: b2bAvailability })) {
|
||||
availableOptions.push('b2b-delivery');
|
||||
availabilities['b2b-delivery'] = b2bAvailability;
|
||||
}
|
||||
}
|
||||
|
||||
if (digAvailability && this.availabilityService.isAvailable({ availability: digAvailability }) && !customerFeatures?.b2b) {
|
||||
availableOptions.push('dig-delivery');
|
||||
availabilities['dig-delivery'] = digAvailability;
|
||||
}
|
||||
|
||||
if (availableOptions.includes('dig-delivery') && availableOptions.includes('b2b-delivery')) {
|
||||
let shippingAvailability = await this.availabilityService
|
||||
.getDeliveryAvailability({
|
||||
item: {
|
||||
itemId: Number(shoppingCartItem.product.catalogProductNumber),
|
||||
ean: shoppingCartItem.product.ean,
|
||||
price: shoppingCartItem.availability.price,
|
||||
},
|
||||
quantity,
|
||||
})
|
||||
.toPromise();
|
||||
if (shippingAvailability && this.availabilityService.isAvailable({ availability: shippingAvailability })) {
|
||||
availableOptions.push('delivery');
|
||||
availabilities['delivery'] = shippingAvailability;
|
||||
availableOptions = availableOptions.filter((option) => !(option === 'dig-delivery' || option === 'b2b-delivery'));
|
||||
}
|
||||
}
|
||||
|
||||
this.loadingOnItemChangeById$.next(undefined);
|
||||
this.cdr.markForCheck();
|
||||
|
||||
const itemId = Number(shoppingCartItem.product.catalogProductNumber);
|
||||
const modal = this.uiModal.open({
|
||||
content: PurchasingOptionsModalComponent,
|
||||
data: {
|
||||
availableOptions,
|
||||
item: {
|
||||
id: itemId,
|
||||
itemId: itemId,
|
||||
product: shoppingCartItem.product,
|
||||
price: shoppingCartItem.availability.price,
|
||||
catalogAvailability: {
|
||||
status: shoppingCartItem.availability.availabilityType,
|
||||
price: shoppingCartItem.availability.price,
|
||||
},
|
||||
},
|
||||
shoppingCartItem,
|
||||
branchId: branch?.id,
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
availabilities,
|
||||
} as PurchasingOptionsModalData,
|
||||
});
|
||||
|
||||
modal.afterClosed$.pipe(takeUntil(this._orderCompleted)).subscribe(() => {
|
||||
this.setQuantityError(shoppingCartItem, undefined, false);
|
||||
this._purchaseOptionsModalService.open({
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
items: [shoppingCartItem],
|
||||
type: 'update',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -816,16 +652,10 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
|
||||
}
|
||||
|
||||
async showPurchasingListModal(shoppingCartItems: ShoppingCartItemDTO[]) {
|
||||
const customerFeatures = await this.customerFeatures$.pipe(first()).toPromise();
|
||||
this.uiModal.open({
|
||||
content: PurchasingOptionsListModalComponent,
|
||||
title: 'Wie möchten Sie die Artikel erhalten?',
|
||||
config: { showScrollbarY: false },
|
||||
data: {
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
shoppingCartItems: shoppingCartItems,
|
||||
customerFeatures,
|
||||
} as PurchasingOptionsListModalData,
|
||||
this._purchaseOptionsModalService.open({
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
items: shoppingCartItems,
|
||||
type: 'update',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// start:ng42.barrel
|
||||
export * from './page-checkout.module';
|
||||
export * from './page-checkout-modals.module';
|
||||
// end:ng42.barrel
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<div class="option-icon">
|
||||
<ui-icon size="50px" icon="truck"></ui-icon>
|
||||
</div>
|
||||
<button
|
||||
class="option-chip"
|
||||
[disabled]="optionChipDisabled$ | async"
|
||||
(click)="optionChange('delivery')"
|
||||
[class.selected]="(selectedOption$ | async) === 'delivery'"
|
||||
>
|
||||
Versand
|
||||
</button>
|
||||
<p>Möchten Sie die Artikel<br />geliefert bekommen?</p>
|
||||
<p>Versandkostenfrei</p>
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-delivery-option-list',
|
||||
templateUrl: 'delivery-option-list.component.html',
|
||||
styleUrls: ['../list-options.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DeliveryOptionListComponent {
|
||||
selectedOption$ = this._store.selectedFilterOption$;
|
||||
optionChipDisabled$ = this._store.fetchingAvailabilities$;
|
||||
|
||||
constructor(private _store: PurchasingOptionsListModalStore) {}
|
||||
|
||||
optionChange(option: string) {
|
||||
if (this._store.selectedFilterOption === option) {
|
||||
this._store.selectedFilterOption = undefined;
|
||||
} else {
|
||||
this._store.selectedFilterOption = option;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './delivery-option-list.component';
|
||||
// end:ng42.barrel
|
||||
@@ -1,7 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './delivery-option';
|
||||
export * from './pick-up-option';
|
||||
export * from './take-away-option';
|
||||
export * from './purchasing-options-list-modal.component';
|
||||
export * from './purchasing-options-list-modal.module';
|
||||
// end:ng42.barrel
|
||||
@@ -1,44 +0,0 @@
|
||||
:host {
|
||||
@apply block w-72;
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
@apply text-ucla-blue mx-auto;
|
||||
width: 40px;
|
||||
|
||||
.truck-b2b {
|
||||
margin-top: -21px;
|
||||
margin-bottom: -12px;
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.option-chip {
|
||||
@apply rounded-full text-base px-4 py-3 bg-glitter text-inactive-customer border-none font-bold;
|
||||
|
||||
&.selected {
|
||||
@apply bg-active-customer text-white;
|
||||
}
|
||||
}
|
||||
|
||||
.option-description {
|
||||
@apply my-2;
|
||||
}
|
||||
|
||||
.option-select {
|
||||
@apply mt-4 mb-4 border-2 border-solid border-brand text-brand text-cta-l font-bold bg-white rounded-full py-3 px-6;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply my-4;
|
||||
}
|
||||
|
||||
::ng-deep page-purchasing-options-list-modal ui-branch-dropdown .wrapper {
|
||||
@apply mx-auto;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
::ng-deep page-pick-up-option-list .option-chip:disabled,
|
||||
::ng-deep page-take-away-option-list .option-chip:disabled {
|
||||
@apply bg-disabled-branch border-disabled-branch text-white;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './pick-up-option-list.component';
|
||||
// end:ng42.barrel
|
||||
@@ -1,18 +0,0 @@
|
||||
<div class="option-icon">
|
||||
<ui-icon size="50px" icon="box_out"></ui-icon>
|
||||
</div>
|
||||
<button
|
||||
class="option-chip"
|
||||
[disabled]="optionChipDisabled$ | async"
|
||||
(click)="optionChange('pick-up')"
|
||||
[class.selected]="(selectedOption$ | async) === 'pick-up'"
|
||||
>
|
||||
Abholung
|
||||
</button>
|
||||
<p>Möchten Sie die Artikel<br />in einer unserer Filialen<br />abholen?</p>
|
||||
|
||||
<ui-branch-dropdown
|
||||
[branches]="branches$ | async"
|
||||
[selected]="selectedBranch$ | async"
|
||||
(selectBranch)="selectBranch($event)"
|
||||
></ui-branch-dropdown>
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { BranchDTO } from '@swagger/checkout';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-pick-up-option-list',
|
||||
templateUrl: 'pick-up-option-list.component.html',
|
||||
styleUrls: ['../list-options.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PickUpOptionListComponent {
|
||||
branches$ = this._store.branches$;
|
||||
selectedBranch$ = this._store.selectedPickUpBranch$.pipe(
|
||||
map((branch) => {
|
||||
// Determins if branch is targetBranch
|
||||
if (branch?.branchType === 1) {
|
||||
return branch.name;
|
||||
}
|
||||
})
|
||||
);
|
||||
selectedOption$ = this._store.selectedFilterOption$;
|
||||
optionChipDisabled$ = combineLatest([this._store.fetchingAvailabilities$, this.selectedBranch$]).pipe(
|
||||
map(([fetching, selectedBranch]) => {
|
||||
return fetching || !selectedBranch;
|
||||
})
|
||||
);
|
||||
|
||||
constructor(private _store: PurchasingOptionsListModalStore) {}
|
||||
|
||||
optionChange(option: string) {
|
||||
if (this._store.selectedFilterOption === option) {
|
||||
this._store.selectedFilterOption = undefined;
|
||||
} else {
|
||||
this._store.selectedFilterOption = option;
|
||||
}
|
||||
}
|
||||
|
||||
async selectBranch(branch: BranchDTO) {
|
||||
this._store.lastSelectedFilterOption$.next(undefined);
|
||||
|
||||
this._store.selectedPickUpBranch = branch;
|
||||
|
||||
const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise();
|
||||
shoppingCartItems.forEach((item) => this._store.loadPickUpAvailability({ item }));
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
<div class="item-thumbnail">
|
||||
<img loading="lazy" *ngIf="item?.product?.ean | productImage; let thumbnailUrl" [src]="thumbnailUrl" [alt]="item?.product?.name" />
|
||||
</div>
|
||||
|
||||
<div class="item-contributors">
|
||||
{{ item.product.contributors }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="item-title"
|
||||
[class.xl]="item?.product?.name?.length >= 35"
|
||||
[class.lg]="item?.product?.name?.length >= 40"
|
||||
[class.md]="item?.product?.name?.length >= 50"
|
||||
[class.sm]="item?.product?.name?.length >= 60"
|
||||
[class.xs]="item?.product?.name?.length >= 100"
|
||||
>
|
||||
{{ item?.product?.name }}
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="canAdd$ | async; let canAdd">
|
||||
<div class="item-can-add" *ngIf="canAdd !== true">
|
||||
{{ canAdd }}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail">
|
||||
<img
|
||||
*ngIf="item?.product?.format !== '--'"
|
||||
src="assets/images/Icon_{{ item?.product?.format }}.svg"
|
||||
[alt]="item?.product?.formatDetail"
|
||||
/>
|
||||
{{ item?.product?.formatDetail }}
|
||||
</div>
|
||||
|
||||
<div class="item-info">
|
||||
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }} <br />
|
||||
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
|
||||
{{ item?.product?.publicationDate | date: 'dd. MMMM yyyy' }}
|
||||
</div>
|
||||
|
||||
<div class="item-price-stock">
|
||||
<div class="price">
|
||||
<ng-container *ngIf="showTooltip$ | async">
|
||||
<button [uiOverlayTrigger]="tooltipContent" #tooltip="uiOverlayTrigger" class="info-tooltip-button" type="button">
|
||||
i
|
||||
</button>
|
||||
<ui-tooltip #tooltipContent yPosition="above" xPosition="after" [yOffset]="-16">
|
||||
Günstigerer Preis aus Hugendubel Katalog wird übernommen
|
||||
</ui-tooltip>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ui-quantity-dropdown
|
||||
[disabled]="fetchingAvailabilities$ | async"
|
||||
[ngModel]="item.quantity"
|
||||
(ngModelChange)="changeQuantity($event)"
|
||||
[range]="quantityRange$ | async"
|
||||
>
|
||||
</ui-quantity-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item-select">
|
||||
<ui-select-bullet
|
||||
*ngIf="selectVisible$ | async"
|
||||
[disabled]="selectDisabled$ | async"
|
||||
[ngModel]="isSelected$ | async"
|
||||
(ngModelChange)="selected($event)"
|
||||
></ui-select-bullet>
|
||||
</div>
|
||||
|
||||
<div class="item-availability">
|
||||
<div class="fetching" *ngIf="fetchingAvailabilities$ | async; else availabilities"></div>
|
||||
<ng-template #availabilities>
|
||||
<ng-container *ngIf="notAvailable$ | async; else available">
|
||||
<span class="hint">Derzeit nicht bestellbar</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #available>
|
||||
<span>Verfügbar als</span>
|
||||
<div *ngIf="takeAwayAvailabilities$ | async; let takeAwayAvailabilites">
|
||||
<ui-icon icon="shopping_bag" size="18px"></ui-icon>
|
||||
<span class="instock">{{ takeAwayAvailabilites?.inStock }}x</span> ab sofort
|
||||
</div>
|
||||
|
||||
<div *ngIf="!!(pickUpAvailabilities$ | async)">
|
||||
<ui-icon icon="box_out" size="18px"></ui-icon>
|
||||
{{ (pickUpAvailabilities$ | async)?.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
|
||||
</div>
|
||||
|
||||
<div *ngIf="!!(deliveryDigAvailabilities$ | async); else b2b">
|
||||
<ui-icon class="truck" icon="truck" size="30px"></ui-icon>
|
||||
<ng-container *ngIf="deliveryDigAvailabilities$ | async; let deliveryDigAvailabilities">
|
||||
<ng-container *ngIf="deliveryDigAvailabilities?.estimatedDelivery; else estimatedShippingDate">
|
||||
{{ (deliveryDigAvailabilities?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} -
|
||||
{{ (deliveryDigAvailabilities?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
|
||||
</ng-container>
|
||||
<ng-template #estimatedShippingDate>
|
||||
{{ deliveryDigAvailabilities.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #b2b>
|
||||
<div *ngIf="!!(deliveryB2bAvailabilities$ | async)">
|
||||
<ui-icon class="truck-b2b" icon="truck_b2b" size="40px"></ui-icon>
|
||||
{{ (deliveryB2bAvailabilities$ | async)?.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -1,180 +0,0 @@
|
||||
:host {
|
||||
@apply text-black no-underline grid py-4;
|
||||
grid-template-columns: 102px 60% auto;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas:
|
||||
'item-thumbnail item-contributors item-contributors'
|
||||
'item-thumbnail item-title item-price-stock'
|
||||
'item-thumbnail item-can-add item-price-stock'
|
||||
'item-thumbnail item-format item-price-stock'
|
||||
'item-thumbnail item-info item-select'
|
||||
'item-thumbnail item-date item-select'
|
||||
'item-thumbnail item-ssc item-select'
|
||||
'item-thumbnail item-availability item-select';
|
||||
}
|
||||
|
||||
.item-thumbnail {
|
||||
grid-area: item-thumbnail;
|
||||
width: 70px;
|
||||
@apply mr-8;
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 150px;
|
||||
@apply rounded-card shadow-cta;
|
||||
}
|
||||
}
|
||||
|
||||
.item-contributors {
|
||||
@apply font-bold no-underline;
|
||||
grid-area: item-contributors;
|
||||
height: 22px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 600px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
grid-area: item-title;
|
||||
@apply font-bold text-lg mb-4;
|
||||
max-height: 64px;
|
||||
}
|
||||
|
||||
.item-title.xl {
|
||||
@apply font-bold text-xl;
|
||||
}
|
||||
|
||||
.item-title.lg {
|
||||
@apply font-bold text-lg;
|
||||
}
|
||||
|
||||
.item-title.md {
|
||||
@apply font-bold text-base;
|
||||
}
|
||||
|
||||
.item-title.sm {
|
||||
@apply font-bold text-sm;
|
||||
}
|
||||
|
||||
.item-title.xs {
|
||||
@apply font-bold text-xs;
|
||||
}
|
||||
|
||||
.item-format {
|
||||
grid-area: item-format;
|
||||
@apply flex flex-row items-center font-bold text-lg whitespace-nowrap;
|
||||
|
||||
img {
|
||||
@apply mr-2;
|
||||
}
|
||||
}
|
||||
|
||||
.item-price-stock {
|
||||
grid-area: item-price-stock;
|
||||
@apply font-bold text-xl text-right;
|
||||
|
||||
.price {
|
||||
@apply flex flex-row justify-end items-center;
|
||||
}
|
||||
|
||||
.info-tooltip-button {
|
||||
@apply border-font-customer border-solid border-2 bg-white rounded-full text-base font-bold mr-3;
|
||||
border-style: outset;
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.quantity-btn {
|
||||
@apply flex flex-row items-center p-0 w-full text-right outline-none border-none bg-transparent text-lg;
|
||||
}
|
||||
|
||||
.quantity-btn-icon {
|
||||
@apply inline-flex ml-2;
|
||||
transition: transform 200ms ease-in-out;
|
||||
}
|
||||
|
||||
ui-quantity-dropdown {
|
||||
@apply flex justify-end mt-2;
|
||||
|
||||
&.disabled {
|
||||
@apply cursor-not-allowed bg-inactive-branch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-stock {
|
||||
grid-area: item-stock;
|
||||
@apply flex flex-row justify-end items-baseline font-bold text-lg;
|
||||
|
||||
ui-icon {
|
||||
@apply text-active-customer mr-2;
|
||||
}
|
||||
}
|
||||
|
||||
.item-info {
|
||||
grid-area: item-info;
|
||||
}
|
||||
|
||||
.item-availability {
|
||||
@apply flex flex-row items-center mt-4 whitespace-nowrap text-sm;
|
||||
grid-area: item-availability;
|
||||
|
||||
.fetching {
|
||||
@apply w-52 h-px-20;
|
||||
background-color: #e6eff9;
|
||||
animation: load 0.75s linear infinite;
|
||||
}
|
||||
|
||||
span {
|
||||
@apply mr-4;
|
||||
}
|
||||
|
||||
.instock {
|
||||
@apply mr-2 font-bold;
|
||||
}
|
||||
|
||||
ui-icon {
|
||||
@apply text-dark-cerulean mx-2;
|
||||
}
|
||||
|
||||
div {
|
||||
@apply mr-4 flex items-center;
|
||||
}
|
||||
|
||||
.truck {
|
||||
@apply -mb-px-5 -mt-px-5;
|
||||
}
|
||||
|
||||
.truck-b2b {
|
||||
@apply -mb-px-10 -mt-px-10;
|
||||
}
|
||||
}
|
||||
|
||||
.item-can-add {
|
||||
@apply text-xl text-dark-goldenrod font-semibold;
|
||||
grid-area: item-can-add;
|
||||
}
|
||||
|
||||
.item-select {
|
||||
@apply flex items-center justify-end;
|
||||
grid-area: item-select;
|
||||
|
||||
ui-select-bullet {
|
||||
@apply cursor-pointer p-4 -m-4 z-dropdown;
|
||||
|
||||
&.disabled {
|
||||
@apply cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
@apply text-xl text-dark-goldenrod font-semibold;
|
||||
}
|
||||
|
||||
@screen desktop {
|
||||
.item-availability {
|
||||
@apply text-base;
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { AvailabilityDTO, ShoppingCartItemDTO } from '@swagger/checkout';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { filter, map, shareReplay, withLatestFrom } from 'rxjs/operators';
|
||||
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-purchasing-options-list-item',
|
||||
templateUrl: 'purchasing-options-list-item.component.html',
|
||||
styleUrls: ['purchasing-options-list-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PurchasingOptionsListItemComponent {
|
||||
@Input()
|
||||
item: ShoppingCartItemDTO;
|
||||
|
||||
isSelected$ = this._store.selectedShoppingCartItems$.pipe(
|
||||
map((selectedShoppingCartItems) => !!selectedShoppingCartItems?.find((item) => item.id === this.item.id))
|
||||
);
|
||||
|
||||
fetchingAvailabilities$ = combineLatest([
|
||||
this._store.takeAwayAvailabilities$,
|
||||
this._store.pickUpAvailabilities$,
|
||||
this._store.deliveryAvailabilities$,
|
||||
this._store.deliveryDigAvailabilities$,
|
||||
this._store.deliveryB2bAvailabilities$,
|
||||
]).pipe(
|
||||
map(
|
||||
([takeAway, pickUp, delivery, digDelivery, b2bDelivery]) =>
|
||||
!takeAway ||
|
||||
takeAway[this.item.product.catalogProductNumber] === true ||
|
||||
!pickUp ||
|
||||
pickUp[this.item.product.catalogProductNumber] === true ||
|
||||
!delivery ||
|
||||
delivery[this.item.product.catalogProductNumber] === true ||
|
||||
!digDelivery ||
|
||||
digDelivery[this.item.product.catalogProductNumber] === true ||
|
||||
!b2bDelivery ||
|
||||
b2bDelivery[this.item.product.catalogProductNumber] === true
|
||||
)
|
||||
);
|
||||
|
||||
takeAwayAvailabilities$ = this._store.takeAwayAvailabilities$.pipe(
|
||||
map((takeAwayAvailabilities) => {
|
||||
if (takeAwayAvailabilities) {
|
||||
const availability = takeAwayAvailabilities[this.item.product?.catalogProductNumber];
|
||||
|
||||
if (typeof availability === 'boolean') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return availability;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}),
|
||||
shareReplay()
|
||||
);
|
||||
|
||||
pickUpAvailabilities$: Observable<AvailabilityDTO> = this._store.pickUpAvailabilities$.pipe(
|
||||
map((pickUpAvailabilities) => {
|
||||
if (pickUpAvailabilities) {
|
||||
const availability = pickUpAvailabilities[this.item.product?.catalogProductNumber];
|
||||
|
||||
if (typeof availability === 'boolean') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return availability;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}),
|
||||
shareReplay()
|
||||
);
|
||||
|
||||
deliveryAvailabilities$ = this._store.deliveryAvailabilities$.pipe(
|
||||
map((shippingAvailabilities) => (!!shippingAvailabilities ? shippingAvailabilities[this.item.product?.catalogProductNumber] : [])),
|
||||
shareReplay()
|
||||
);
|
||||
|
||||
deliveryDigAvailabilities$: Observable<AvailabilityDTO> = this._store.deliveryDigAvailabilities$.pipe(
|
||||
map((shippingAvailabilities) => {
|
||||
if (shippingAvailabilities) {
|
||||
const availability = shippingAvailabilities[this.item.product?.catalogProductNumber];
|
||||
|
||||
if (typeof availability === 'boolean') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return availability;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}),
|
||||
shareReplay()
|
||||
);
|
||||
|
||||
deliveryB2bAvailabilities$ = this._store.deliveryB2bAvailabilities$.pipe(
|
||||
map((shippingAvailabilities) => {
|
||||
if (shippingAvailabilities) {
|
||||
const availability = shippingAvailabilities[this.item.product?.catalogProductNumber];
|
||||
|
||||
if (typeof availability === 'boolean') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return availability;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}),
|
||||
shareReplay()
|
||||
);
|
||||
|
||||
notAvailable$ = combineLatest([
|
||||
this.fetchingAvailabilities$,
|
||||
this.takeAwayAvailabilities$,
|
||||
this.pickUpAvailabilities$,
|
||||
this.deliveryAvailabilities$,
|
||||
this.deliveryDigAvailabilities$,
|
||||
this.deliveryB2bAvailabilities$,
|
||||
]).pipe(
|
||||
map(
|
||||
([fetching, takeAway, store, delivery, deliveryDig, deliveryB2b]) =>
|
||||
!fetching && !takeAway && !store && !delivery && !deliveryDig && !deliveryB2b
|
||||
)
|
||||
);
|
||||
|
||||
showTooltip$ = this._store.selectedFilterOption$.pipe(
|
||||
withLatestFrom(this.deliveryAvailabilities$, this.deliveryDigAvailabilities$),
|
||||
map(([option, delivery, deliveryDig]) => {
|
||||
if (option === 'delivery') {
|
||||
const deliveryAvailability = (deliveryDig as AvailabilityDTO) || (delivery as AvailabilityDTO);
|
||||
|
||||
const shippingPrice = deliveryAvailability?.price?.value?.value;
|
||||
const catalogPrice = this.item?.availability?.price?.value?.value;
|
||||
return catalogPrice < shippingPrice;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
);
|
||||
|
||||
price$ = combineLatest([this.fetchingAvailabilities$, this._store.selectedFilterOption$]).pipe(
|
||||
filter(([fetching]) => !fetching),
|
||||
withLatestFrom(
|
||||
this.takeAwayAvailabilities$,
|
||||
this.pickUpAvailabilities$,
|
||||
this.deliveryAvailabilities$,
|
||||
this.deliveryDigAvailabilities$,
|
||||
this.deliveryB2bAvailabilities$
|
||||
),
|
||||
map(([[_, option], takeAway, pickUp, delivery, deliveryDig, deliveryB2b]) => {
|
||||
let availability;
|
||||
|
||||
switch (option) {
|
||||
case 'take-away':
|
||||
availability = takeAway;
|
||||
break;
|
||||
case 'pick-up':
|
||||
availability = pickUp;
|
||||
break;
|
||||
case 'delivery':
|
||||
if (deliveryDig || delivery) {
|
||||
availability = deliveryDig || delivery;
|
||||
} else {
|
||||
availability = deliveryB2b;
|
||||
option = 'b2b-delivery';
|
||||
availability.p;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return this.item.availability?.price ?? this.item.unitPrice;
|
||||
}
|
||||
|
||||
return this._availabilityService.getPriceForAvailability(option, this.item.availability, availability) ?? this.item.unitPrice;
|
||||
})
|
||||
);
|
||||
|
||||
selectDisabled$ = this._store.selectedFilterOption$.pipe(map((selectedFilterOption) => !selectedFilterOption));
|
||||
|
||||
selectVisible$ = combineLatest([this._store.canAdd$, this._store.selectedShoppingCartItems$]).pipe(
|
||||
withLatestFrom(
|
||||
this._store.selectedFilterOption$,
|
||||
this._store.deliveryAvailabilities$,
|
||||
this._store.deliveryDigAvailabilities$,
|
||||
this._store.deliveryB2bAvailabilities$,
|
||||
this._store.fetchingAvailabilities$
|
||||
),
|
||||
map(([[canAdd, items], option, delivery, deliveryDig, deliveryB2b, fetching]) => {
|
||||
if (!option || fetching) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Select immer sichtbar bei ausgewählten Items
|
||||
if (items?.find((item) => item.product?.catalogProductNumber === this.item.product?.catalogProductNumber)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Select nur anzeigen, wenn ein anderes ausgewähltes Item die gleiche Verfügbarkeit hat (B2B Versand z.B.)
|
||||
if (items?.length > 0 && option === 'delivery' && canAdd[this.item.product.catalogProductNumber]?.status < 2) {
|
||||
if (items.every((item) => delivery[item.product?.catalogProductNumber]) && delivery[this.item.product?.catalogProductNumber]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
items.every((item) => deliveryDig[item.product?.catalogProductNumber]) &&
|
||||
deliveryDig[this.item.product?.catalogProductNumber]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
items.every((item) => deliveryB2b[item.product?.catalogProductNumber]) &&
|
||||
deliveryB2b[this.item.product?.catalogProductNumber]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return canAdd && canAdd[this.item.product.catalogProductNumber]?.status < 2;
|
||||
})
|
||||
);
|
||||
|
||||
canAdd$ = this._store.canAdd$.pipe(
|
||||
filter((canAdd) => !!this.item && !!canAdd),
|
||||
map((canAdd) => !!canAdd[this.item.product.catalogProductNumber]?.message)
|
||||
);
|
||||
|
||||
quantityRange$ = combineLatest([this._store.selectedFilterOption$, this.takeAwayAvailabilities$]).pipe(
|
||||
map(([option, availability]) => (option === 'take-away' ? (availability as AvailabilityDTO)?.inStock : 999))
|
||||
);
|
||||
|
||||
constructor(private _store: PurchasingOptionsListModalStore, private _availabilityService: DomainAvailabilityService) {}
|
||||
|
||||
selected(value: boolean) {
|
||||
this._store.selectShoppingCartItem([this.item], value);
|
||||
}
|
||||
|
||||
changeQuantity(quantity: number) {
|
||||
if (quantity === 0) {
|
||||
this._store.removeShoppingCartItem(this.item);
|
||||
} else {
|
||||
this._store.updateItemQuantity({ itemId: this.item.id, quantity });
|
||||
this._store.loadAvailabilities({ items: [{ ...this.item, quantity }] });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<div class="options">
|
||||
<page-take-away-option-list></page-take-away-option-list>
|
||||
<page-pick-up-option-list></page-pick-up-option-list>
|
||||
<page-delivery-option-list></page-delivery-option-list>
|
||||
</div>
|
||||
|
||||
<div class="items" *ngIf="shoppingCartItems$ | async; let shoppingCartItems">
|
||||
<div class="item-actions">
|
||||
<ng-container>
|
||||
<button
|
||||
*ngIf="!(allShoppingCartItemsSelected$ | async); else unselectAll"
|
||||
class="cta-select-all"
|
||||
[disabled]="selectAllCtaDisabled$ | async"
|
||||
(click)="selectAll(shoppingCartItems, true)"
|
||||
>
|
||||
Alle auswählen
|
||||
</button>
|
||||
|
||||
<ng-template #unselectAll>
|
||||
<button class="cta-select-all" [disabled]="selectAllCtaDisabled$ | async" (click)="selectAll(shoppingCartItems, false)">
|
||||
Alle abwählen
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<br />
|
||||
{{ (selectedShoppingCartItems$ | async)?.length || 0 }} von {{ shoppingCartItems?.length || 0 }} Artikeln
|
||||
</div>
|
||||
|
||||
<div class="item-list scroll-bar" *ngIf="shoppingCartItems?.length > 0; else emptyMessage">
|
||||
<hr />
|
||||
<ng-container *ngFor="let item of shoppingCartItems">
|
||||
<page-purchasing-options-list-item [item]="item"></page-purchasing-options-list-item>
|
||||
<hr />
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #emptyMessage>
|
||||
<div class="empty-message">Keine Artikel für die ausgewählte Kaufoption verfügbar</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="cta-apply" [disabled]="applyCtaDisabled$ | async" (click)="apply()">
|
||||
<ui-spinner [show]="addItemsLoader$ | async">
|
||||
Übernehmen
|
||||
</ui-spinner>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,49 +0,0 @@
|
||||
:host {
|
||||
@apply block box-border;
|
||||
}
|
||||
|
||||
.options {
|
||||
@apply flex flex-row box-border text-center justify-center mt-4;
|
||||
}
|
||||
|
||||
.items {
|
||||
min-height: 440px;
|
||||
|
||||
.item-actions {
|
||||
@apply text-right;
|
||||
|
||||
.cta-select-all {
|
||||
@apply text-brand bg-transparent text-base font-bold outline-none border-none px-4 py-4 -mr-4;
|
||||
|
||||
&:disabled {
|
||||
@apply text-inactive-branch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-list {
|
||||
@apply overflow-y-scroll overflow-x-hidden -ml-4;
|
||||
max-height: calc(100vh - 580px);
|
||||
width: calc(100% + 2rem);
|
||||
|
||||
page-purchasing-options-list-item {
|
||||
@apply px-4;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
@apply text-inactive-branch my-8 text-center font-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
@apply flex justify-center mt-8;
|
||||
|
||||
.cta-apply {
|
||||
@apply text-white border-2 border-solid border-brand bg-brand font-bold text-lg px-4 py-2 rounded-full;
|
||||
|
||||
&:disabled {
|
||||
@apply bg-inactive-branch border-inactive-branch;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { ShoppingCartItemDTO, UpdateShoppingCartItemDTO } from '@swagger/checkout';
|
||||
import { UiErrorModalComponent, UiModalRef, UiModalService } from '@ui/modal';
|
||||
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
|
||||
import { debounceTime, filter, first, map, shareReplay, takeUntil, withLatestFrom } from 'rxjs/operators';
|
||||
import { PurchasingOptionsListModalData } from './purchasing-options-list-modal.data';
|
||||
import { PurchasingOptionsListModalStore } from './purchasing-options-list-modal.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-purchasing-options-list-modal',
|
||||
templateUrl: 'purchasing-options-list-modal.component.html',
|
||||
styleUrls: ['purchasing-options-list-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [PurchasingOptionsListModalStore],
|
||||
})
|
||||
export class PurchasingOptionsListModalComponent implements OnInit {
|
||||
private _onDestroy$ = new Subject();
|
||||
|
||||
addItemsLoader$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
shoppingCartItems$ = combineLatest([
|
||||
this._store.fetchingAvailabilities$,
|
||||
this._store.selectedFilterOption$,
|
||||
this._store.shoppingCartItems$,
|
||||
]).pipe(
|
||||
withLatestFrom(
|
||||
this._store.takeAwayAvailabilities$,
|
||||
this._store.pickUpAvailabilities$,
|
||||
this._store.deliveryAvailabilities$,
|
||||
this._store.deliveryDigAvailabilities$,
|
||||
this._store.deliveryB2bAvailabilities$
|
||||
),
|
||||
map(
|
||||
([
|
||||
[_, selectedFilterOption, shoppingCartItems],
|
||||
takeAwayAvailability,
|
||||
pickUpAvailability,
|
||||
deliveryAvailability,
|
||||
deliveryDigAvailability,
|
||||
deliveryB2bAvailability,
|
||||
]) => {
|
||||
if (!!takeAwayAvailability && !!pickUpAvailability && !!deliveryAvailability) {
|
||||
switch (selectedFilterOption) {
|
||||
case 'take-away':
|
||||
return shoppingCartItems.filter((item) => !!takeAwayAvailability[item.product?.catalogProductNumber]);
|
||||
case 'pick-up':
|
||||
return shoppingCartItems.filter((item) => !!pickUpAvailability[item.product?.catalogProductNumber]);
|
||||
case 'delivery':
|
||||
return shoppingCartItems.filter(
|
||||
(item) =>
|
||||
!!deliveryAvailability[item.product?.catalogProductNumber] ||
|
||||
!!deliveryDigAvailability[item.product?.catalogProductNumber] ||
|
||||
!!deliveryB2bAvailability[item.product?.catalogProductNumber]
|
||||
);
|
||||
}
|
||||
}
|
||||
return shoppingCartItems;
|
||||
}
|
||||
),
|
||||
map((shoppingCartItems) => shoppingCartItems?.sort((a, b) => a.product?.name.localeCompare(b.product?.name))),
|
||||
shareReplay()
|
||||
);
|
||||
|
||||
selectedShoppingCartItems$ = this._store.selectedShoppingCartItems$;
|
||||
|
||||
allShoppingCartItemsSelected$ = combineLatest([this.shoppingCartItems$, this.selectedShoppingCartItems$]).pipe(
|
||||
map(
|
||||
([shoppingCartItems, selectedShoppingCartItems]) =>
|
||||
shoppingCartItems.every((item) => selectedShoppingCartItems.find((i) => item.id === i.id)) && shoppingCartItems?.length > 0
|
||||
)
|
||||
);
|
||||
|
||||
canAddItems$ = this._store.canAdd$.pipe(
|
||||
map((canAdd) => {
|
||||
for (const key in canAdd) {
|
||||
if (Object.prototype.hasOwnProperty.call(canAdd, key)) {
|
||||
if (!!canAdd[key]?.message) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
shareReplay()
|
||||
);
|
||||
|
||||
selectAllCtaDisabled$ = combineLatest([this._store.selectedFilterOption$, this.canAddItems$]).pipe(
|
||||
withLatestFrom(this.shoppingCartItems$),
|
||||
map(([[selectedFilterOption, canAddItems], items]) => !selectedFilterOption || items?.length === 0 || !canAddItems)
|
||||
);
|
||||
|
||||
applyCtaDisabled$ = combineLatest([this.addItemsLoader$, this._store.selectedFilterOption$, this._store.selectedShoppingCartItems$]).pipe(
|
||||
withLatestFrom(this.shoppingCartItems$),
|
||||
map(
|
||||
([[addItemsLoader, selectedFilterOption, selectedShoppingCartItems], shoppingCartItems]) =>
|
||||
addItemsLoader || !selectedFilterOption || shoppingCartItems?.length === 0 || selectedShoppingCartItems?.length === 0
|
||||
)
|
||||
);
|
||||
|
||||
constructor(
|
||||
private _modalRef: UiModalRef<any, PurchasingOptionsListModalData>,
|
||||
private _modal: UiModalService,
|
||||
private _store: PurchasingOptionsListModalStore,
|
||||
private _availability: DomainAvailabilityService,
|
||||
private _checkout: DomainCheckoutService
|
||||
) {
|
||||
this._store.shoppingCartItems = _modalRef.data.shoppingCartItems;
|
||||
this._store.customerFeatures = _modalRef.data.customerFeatures;
|
||||
this._store.processId = _modalRef.data.processId;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._store.loadBranches();
|
||||
|
||||
// Beim Wechsel der ausgewählten Filteroption oder der Branches die Auswahl leeren
|
||||
combineLatest([this._store.selectedFilterOption$, this._store.selectedTakeAwayBranch$, this._store.selectedPickUpBranch$])
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe(() => this._store.clearSelectedShoppingCartItems());
|
||||
|
||||
this._store.selectedFilterOption$
|
||||
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.shoppingCartItems$))
|
||||
.subscribe(([option, items]) => this.checkCanAdd(option, items));
|
||||
|
||||
this._store.fetchingAvailabilities$
|
||||
.pipe(
|
||||
takeUntil(this._onDestroy$),
|
||||
debounceTime(250),
|
||||
filter((fetching) => !fetching),
|
||||
withLatestFrom(this.shoppingCartItems$, this._store.selectedFilterOption$)
|
||||
)
|
||||
.subscribe(([_, items, option]) => this.checkCanAdd(option, items));
|
||||
|
||||
this.canAddItems$
|
||||
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.shoppingCartItems$, this._store.selectedFilterOption$))
|
||||
.subscribe(([showSelectAll, items, option]) => {
|
||||
if (items?.length > 0 && this._store.lastSelectedFilterOption$.value !== option) {
|
||||
this.selectAll(items, showSelectAll && !!option);
|
||||
}
|
||||
|
||||
// Nach dem Übernehmen von Items wird eine neue CanAdd Abfrage ausgeführt, in diesem Fall soll aber nicht alles ausgewählt werden
|
||||
this._store.lastSelectedFilterOption$.next(option);
|
||||
});
|
||||
}
|
||||
|
||||
checkCanAdd(selectedFilterOption: string, items: ShoppingCartItemDTO[]) {
|
||||
if (!!selectedFilterOption && items?.length > 0) {
|
||||
this._store.checkCanAddItems(items);
|
||||
} else {
|
||||
this._store.patchState({ canAdd: {} });
|
||||
}
|
||||
}
|
||||
|
||||
async selectAll(items: ShoppingCartItemDTO[], value: boolean) {
|
||||
this._store.selectShoppingCartItem(items, value);
|
||||
}
|
||||
|
||||
async apply() {
|
||||
this.addItemsLoader$.next(true);
|
||||
|
||||
try {
|
||||
const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise();
|
||||
const items = await this._store.selectedShoppingCartItems$.pipe(first()).toPromise();
|
||||
const takeAwayAvailabilities = await this._store.takeAwayAvailabilities$.pipe(first()).toPromise();
|
||||
const pickupAvailabilities = await this._store.pickUpAvailabilities$.pipe(first()).toPromise();
|
||||
const deliveryAvailabilities = await this._store.deliveryAvailabilities$.pipe(first()).toPromise();
|
||||
const deliveryB2bAvailabilities = await this._store.deliveryB2bAvailabilities$.pipe(first()).toPromise();
|
||||
const deliveryDigAvailabilities = await this._store.deliveryDigAvailabilities$.pipe(first()).toPromise();
|
||||
const selectedTakeAwayBranch = await this._store.selectedTakeAwayBranch$.pipe(first()).toPromise();
|
||||
const selectedPickUpBranch = await this._store.selectedPickUpBranch$.pipe(first()).toPromise();
|
||||
let option = this._store.selectedFilterOption;
|
||||
|
||||
for (const item of items) {
|
||||
let availability;
|
||||
switch (this._store.selectedFilterOption) {
|
||||
case 'take-away':
|
||||
availability = takeAwayAvailabilities[item.product.catalogProductNumber];
|
||||
break;
|
||||
case 'pick-up':
|
||||
availability = pickupAvailabilities[item.product.catalogProductNumber];
|
||||
break;
|
||||
case 'delivery':
|
||||
if (
|
||||
deliveryDigAvailabilities[item.product.catalogProductNumber] &&
|
||||
deliveryB2bAvailabilities[item.product.catalogProductNumber] &&
|
||||
deliveryAvailabilities[item.product.catalogProductNumber]
|
||||
) {
|
||||
availability = deliveryAvailabilities[item.product.catalogProductNumber];
|
||||
} else if (deliveryDigAvailabilities[item.product.catalogProductNumber]) {
|
||||
availability = deliveryDigAvailabilities[item.product.catalogProductNumber];
|
||||
} else if (deliveryB2bAvailabilities[item.product.catalogProductNumber]) {
|
||||
availability = deliveryB2bAvailabilities[item.product.catalogProductNumber];
|
||||
option = 'b2b-delivery';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const price = this._availability.getPriceForAvailability(option, item.availability, availability);
|
||||
|
||||
// Negative Preise und nicht vorhandene Availability ignorieren
|
||||
if (price?.value?.value < 0 || !availability) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const updateItem: UpdateShoppingCartItemDTO = {
|
||||
quantity: item.quantity,
|
||||
availability: {
|
||||
...availability,
|
||||
price: price ? price : item.unitPrice,
|
||||
},
|
||||
promotion: item?.promotion?.points ? { points: item.promotion.points } : undefined,
|
||||
};
|
||||
|
||||
switch (this._store.selectedFilterOption) {
|
||||
case 'take-away':
|
||||
updateItem.destination = {
|
||||
data: { target: 1, targetBranch: { id: selectedTakeAwayBranch.id } },
|
||||
};
|
||||
break;
|
||||
case 'pick-up':
|
||||
updateItem.destination = {
|
||||
data: { target: 1, targetBranch: { id: selectedPickUpBranch.id } },
|
||||
};
|
||||
break;
|
||||
case 'delivery':
|
||||
case 'dig-delivery':
|
||||
case 'b2b-delivery':
|
||||
updateItem.destination = {
|
||||
data: { target: 2, logistician: availability?.logistician },
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
await this._checkout
|
||||
.updateItemInShoppingCart({
|
||||
processId: this._modalRef.data.processId,
|
||||
shoppingCartItemId: item.id,
|
||||
update: {
|
||||
...updateItem,
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
const remainingItems = shoppingCartItems.filter((i) => !items.find((j) => i.id === j.id));
|
||||
this._store.shoppingCartItems = [...remainingItems];
|
||||
|
||||
this._store.clearSelectedShoppingCartItems();
|
||||
|
||||
if (remainingItems?.length === 0) {
|
||||
this._modalRef.close();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this._modal.open({ content: UiErrorModalComponent, data: error, title: 'Fehler beim Hinzufügen zum Warenkorb' });
|
||||
} finally {
|
||||
this.addItemsLoader$.next(false);
|
||||
}
|
||||
|
||||
const shoppingCartItems = await this.shoppingCartItems$.pipe(first()).toPromise();
|
||||
if (shoppingCartItems?.length > 0) {
|
||||
this._store.checkCanAddItems(shoppingCartItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { ShoppingCartItemDTO } from '@swagger/checkout';
|
||||
|
||||
export interface PurchasingOptionsListModalData {
|
||||
processId: number;
|
||||
shoppingCartItems?: ShoppingCartItemDTO[];
|
||||
customerFeatures: { [key: string]: string };
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { PurchasingOptionsListModalComponent } from './purchasing-options-list-modal.component';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { ProductImageModule } from '@cdn/product-image';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { UiSelectBulletModule } from '@ui/select-bullet';
|
||||
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
|
||||
import { PickUpOptionListComponent } from './pick-up-option/pick-up-option-list.component';
|
||||
import { TakeAwayOptionListComponent } from './take-away-option/take-away-option-list.component';
|
||||
import { DeliveryOptionListComponent } from './delivery-option/delivery-option-list.component';
|
||||
import { PurchasingOptionsListItemComponent } from './purchasing-options-list-item/purchasing-options-list-item.component';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { UiBranchDropdownModule } from '@ui/branch-dropdown';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
UiCommonModule,
|
||||
UiIconModule,
|
||||
UiSelectBulletModule,
|
||||
UiQuantityDropdownModule,
|
||||
ProductImageModule,
|
||||
UiBranchDropdownModule,
|
||||
UiTooltipModule,
|
||||
UiSpinnerModule,
|
||||
],
|
||||
exports: [PurchasingOptionsListModalComponent],
|
||||
declarations: [
|
||||
PurchasingOptionsListModalComponent,
|
||||
PurchasingOptionsListItemComponent,
|
||||
PickUpOptionListComponent,
|
||||
TakeAwayOptionListComponent,
|
||||
DeliveryOptionListComponent,
|
||||
],
|
||||
})
|
||||
export class PurchasingOptionsListModalModule {}
|
||||
@@ -1,598 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ComponentStore, tapResponse } from '@ngrx/component-store';
|
||||
import { AvailabilityDTO, BranchDTO, ShoppingCartItemDTO } from '@swagger/checkout';
|
||||
import { map, mergeMap, switchMap, withLatestFrom } from 'rxjs/operators';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { ApplicationService } from '@core/application';
|
||||
|
||||
interface PurchasingOptionsListModalState {
|
||||
processId: number;
|
||||
shoppingCartItems: ShoppingCartItemDTO[];
|
||||
selectedFilterOption: string;
|
||||
takeAwayAvailabilities: { [key: string]: AvailabilityDTO | true };
|
||||
pickUpAvailabilities: { [key: string]: AvailabilityDTO | true };
|
||||
deliveryAvailabilities: { [key: string]: AvailabilityDTO | true };
|
||||
deliveryB2bAvailabilities: { [key: string]: AvailabilityDTO | true };
|
||||
deliveryDigAvailabilities: { [key: string]: AvailabilityDTO | true };
|
||||
customerFeatures: { [key: string]: string };
|
||||
canAdd: { [key: string]: { message: string; status: number } };
|
||||
selectedShoppingCartItems: ShoppingCartItemDTO[];
|
||||
branches: BranchDTO[];
|
||||
currentBranch: BranchDTO;
|
||||
selectedTakeAwayBranch: BranchDTO;
|
||||
selectedPickUpBranch: BranchDTO;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PurchasingOptionsListModalStore extends ComponentStore<PurchasingOptionsListModalState> {
|
||||
lastSelectedFilterOption$ = new BehaviorSubject<string>(undefined);
|
||||
|
||||
branches$ = this.select((s) => s.branches);
|
||||
currentBranch$ = this.select((s) => s.currentBranch);
|
||||
takeAwayAvailabilities$ = this.select((s) => s.takeAwayAvailabilities);
|
||||
pickUpAvailabilities$ = this.select((s) => s.pickUpAvailabilities);
|
||||
deliveryAvailabilities$ = this.select((s) => s.deliveryAvailabilities);
|
||||
deliveryB2bAvailabilities$ = this.select((s) => s.deliveryB2bAvailabilities);
|
||||
canAdd$ = this.select((s) => s.canAdd);
|
||||
deliveryDigAvailabilities$ = this.select((s) => s.deliveryDigAvailabilities);
|
||||
|
||||
shoppingCartItems$ = this.select((s) => s.shoppingCartItems);
|
||||
|
||||
set shoppingCartItems(shoppingCartItems: ShoppingCartItemDTO[]) {
|
||||
shoppingCartItems = shoppingCartItems.sort((a, b) => a.product?.name.localeCompare(b.product.name));
|
||||
this.patchState({ shoppingCartItems });
|
||||
}
|
||||
|
||||
processId$ = this.select((s) => s.processId);
|
||||
|
||||
set processId(processId: number) {
|
||||
this.patchState({ processId });
|
||||
}
|
||||
|
||||
customerFeatures$ = this.select((s) => s.customerFeatures);
|
||||
|
||||
set customerFeatures(customerFeatures: { [key: string]: string }) {
|
||||
this.patchState({ customerFeatures });
|
||||
}
|
||||
|
||||
selectedFilterOption$ = this.select((s) => s.selectedFilterOption);
|
||||
|
||||
set selectedFilterOption(selectedFilterOption: string) {
|
||||
this.patchState({ selectedFilterOption });
|
||||
}
|
||||
|
||||
get selectedFilterOption() {
|
||||
return this.get((s) => s.selectedFilterOption);
|
||||
}
|
||||
|
||||
selectedShoppingCartItems$ = this.select((s) => s.selectedShoppingCartItems);
|
||||
|
||||
get selectedShoppingCartItems() {
|
||||
return this.get((s) => s.selectedShoppingCartItems);
|
||||
}
|
||||
|
||||
selectedTakeAwayBranch$ = this.select((s) => s.selectedTakeAwayBranch);
|
||||
|
||||
set selectedTakeAwayBranch(selectedTakeAwayBranch: BranchDTO) {
|
||||
this.patchState({ selectedTakeAwayBranch });
|
||||
}
|
||||
|
||||
selectedPickUpBranch$ = this.select((s) => s.selectedPickUpBranch);
|
||||
|
||||
set selectedPickUpBranch(selectedPickUpBranch: BranchDTO) {
|
||||
this.patchState({ selectedPickUpBranch });
|
||||
}
|
||||
|
||||
fetchingAvailabilities$ = combineLatest([this.takeAwayAvailabilities$, this.pickUpAvailabilities$, this.deliveryAvailabilities$]).pipe(
|
||||
map(([takeAway, pickUp, delivery]) => {
|
||||
const fetchingCheck = (obj) => {
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
const element = obj[key];
|
||||
if (typeof element === 'boolean') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return !takeAway || fetchingCheck(takeAway) || !pickUp || fetchingCheck(pickUp) || !delivery || fetchingCheck(delivery);
|
||||
})
|
||||
);
|
||||
|
||||
constructor(
|
||||
private _availabilityService: DomainAvailabilityService,
|
||||
private _checkoutService: DomainCheckoutService,
|
||||
private _application: ApplicationService
|
||||
) {
|
||||
super({
|
||||
processId: undefined,
|
||||
shoppingCartItems: [],
|
||||
selectedFilterOption: undefined,
|
||||
pickUpAvailabilities: undefined,
|
||||
deliveryAvailabilities: undefined,
|
||||
takeAwayAvailabilities: undefined,
|
||||
deliveryB2bAvailabilities: undefined,
|
||||
deliveryDigAvailabilities: undefined,
|
||||
selectedShoppingCartItems: [],
|
||||
branches: [],
|
||||
currentBranch: undefined,
|
||||
selectedTakeAwayBranch: undefined,
|
||||
selectedPickUpBranch: undefined,
|
||||
customerFeatures: undefined,
|
||||
canAdd: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
loadAvailabilities(options: { items?: ShoppingCartItemDTO[] }) {
|
||||
const shoppingCartItems = options.items ?? this.get((s) => s.shoppingCartItems);
|
||||
|
||||
for (const item of shoppingCartItems) {
|
||||
this.loadTakeAwayAvailability({ item });
|
||||
this.loadPickUpAvailability({ item });
|
||||
this.loadDeliveryAvailability({ item });
|
||||
this.loadDeliveryB2bAvailability({ item });
|
||||
this.loadDeliveryDigAvailability({ item });
|
||||
}
|
||||
}
|
||||
|
||||
readonly setAvailabilityFetching = this.updater((state, { name, id, fetching }: { name: string; id: string; fetching?: boolean }) => {
|
||||
const availability = { ...state[name] };
|
||||
|
||||
if (fetching) {
|
||||
availability[id] = fetching;
|
||||
} else {
|
||||
delete availability[id];
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
[name]: {
|
||||
...availability,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
readonly setAvailability = this.updater((state, { name, availability }: { name: string; availability: any }) => {
|
||||
const av = { ...state[name] };
|
||||
|
||||
if (this._availabilityService.isAvailable({ availability })) {
|
||||
av[availability.itemId] = availability;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
[name]: av,
|
||||
};
|
||||
});
|
||||
|
||||
loadPickUpAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
|
||||
options$.pipe(
|
||||
withLatestFrom(this.selectedPickUpBranch$),
|
||||
mergeMap(([options, branch]) => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'pickUpAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
fetching: true,
|
||||
});
|
||||
|
||||
return this._availabilityService
|
||||
.getPickUpAvailability({
|
||||
item: {
|
||||
itemId: +options.item.product.catalogProductNumber,
|
||||
ean: options.item.product.ean,
|
||||
price: options.item.availability.price,
|
||||
},
|
||||
branch,
|
||||
quantity: options.item.quantity,
|
||||
})
|
||||
.pipe(
|
||||
map((av) => {
|
||||
if (av?.length > 0) {
|
||||
if (av[1].availableFor) {
|
||||
if ((av[1].availableFor & 2) === 2) {
|
||||
return av[0];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
return av[0];
|
||||
}
|
||||
}
|
||||
}),
|
||||
tapResponse(
|
||||
(availability) => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'pickUpAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
});
|
||||
this.setAvailability({
|
||||
name: 'pickUpAvailabilities',
|
||||
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
|
||||
});
|
||||
},
|
||||
() => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'pickUpAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
fetching: false,
|
||||
});
|
||||
this.setAvailability({ name: 'pickUpAvailabilities', availability: {} });
|
||||
}
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
loadDeliveryAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
|
||||
options$.pipe(
|
||||
mergeMap((options) => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'deliveryAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
fetching: true,
|
||||
});
|
||||
|
||||
return this._availabilityService
|
||||
.getDeliveryAvailability({
|
||||
item: {
|
||||
itemId: +options.item.product.catalogProductNumber,
|
||||
ean: options.item.product.ean,
|
||||
price: options.item.availability.price,
|
||||
},
|
||||
quantity: options.item.quantity,
|
||||
})
|
||||
.pipe(
|
||||
tapResponse(
|
||||
(availability) => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'deliveryAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
});
|
||||
this.setAvailability({
|
||||
name: 'deliveryAvailabilities',
|
||||
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
|
||||
});
|
||||
},
|
||||
() => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'deliveryAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
fetching: false,
|
||||
});
|
||||
this.setAvailability({ name: 'deliveryAvailabilities', availability: {} });
|
||||
}
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
loadDeliveryB2bAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
|
||||
options$.pipe(
|
||||
mergeMap((options) => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'deliveryB2bAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
fetching: true,
|
||||
});
|
||||
|
||||
return this._availabilityService
|
||||
.getB2bDeliveryAvailability({
|
||||
item: {
|
||||
itemId: +options.item.product.catalogProductNumber,
|
||||
ean: options.item.product.ean,
|
||||
price: options.item.availability.price,
|
||||
},
|
||||
quantity: options.item.quantity,
|
||||
})
|
||||
.pipe(
|
||||
tapResponse(
|
||||
(availability) => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'deliveryB2bAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
});
|
||||
|
||||
this.setAvailability({
|
||||
name: 'deliveryB2bAvailabilities',
|
||||
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
|
||||
});
|
||||
},
|
||||
() => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'deliveryB2bAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
fetching: false,
|
||||
});
|
||||
this.setAvailability({ name: 'deliveryB2bAvailabilities', availability: {} });
|
||||
}
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
loadDeliveryDigAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
|
||||
options$.pipe(
|
||||
mergeMap((options) => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'deliveryDigAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
fetching: true,
|
||||
});
|
||||
|
||||
return this._availabilityService
|
||||
.getDigDeliveryAvailability({
|
||||
item: {
|
||||
itemId: +options.item.product.catalogProductNumber,
|
||||
ean: options.item.product.ean,
|
||||
price: options.item.availability.price,
|
||||
},
|
||||
quantity: options.item.quantity,
|
||||
})
|
||||
.pipe(
|
||||
tapResponse(
|
||||
(availability) => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'deliveryDigAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
});
|
||||
this.setAvailability({
|
||||
name: 'deliveryDigAvailabilities',
|
||||
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
|
||||
});
|
||||
},
|
||||
() => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'deliveryDigAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
fetching: false,
|
||||
});
|
||||
this.setAvailability({ name: 'deliveryDigAvailabilities', availability: {} });
|
||||
}
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
loadTakeAwayAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
|
||||
options$.pipe(
|
||||
withLatestFrom(this.selectedTakeAwayBranch$),
|
||||
mergeMap(([options, branch]) => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'takeAwayAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
fetching: true,
|
||||
});
|
||||
|
||||
return this._availabilityService
|
||||
.getTakeAwayAvailabilityByBranch({
|
||||
itemId: +options.item.product.catalogProductNumber,
|
||||
price: options.item.availability.price,
|
||||
quantity: options.item.quantity,
|
||||
branch,
|
||||
})
|
||||
.pipe(
|
||||
tapResponse(
|
||||
(availability) => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'takeAwayAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
});
|
||||
this.setAvailability({
|
||||
name: 'takeAwayAvailabilities',
|
||||
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
|
||||
});
|
||||
},
|
||||
() => {
|
||||
this.setAvailabilityFetching({
|
||||
name: 'takeAwayAvailabilities',
|
||||
id: options.item.product.catalogProductNumber,
|
||||
fetching: false,
|
||||
});
|
||||
this.setAvailability({ name: 'takeAwayAvailabilities', availability: {} });
|
||||
}
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
getCurrentBranch() {
|
||||
return combineLatest([this._application.getSelectedBranch$(), this._availabilityService.getDefaultBranch()]).pipe(
|
||||
map(([selectedBranch, defaultBranch]) => selectedBranch || defaultBranch)
|
||||
);
|
||||
}
|
||||
|
||||
loadBranches = this.effect(($) =>
|
||||
$.pipe(
|
||||
switchMap(() =>
|
||||
this._availabilityService.getBranches().pipe(
|
||||
map((branches) =>
|
||||
branches.filter(
|
||||
(branch) => branch.status === 1 && branch.branchType === 1 && branch.isOnline === true && branch.isShippingEnabled === true
|
||||
)
|
||||
),
|
||||
withLatestFrom(this.getCurrentBranch()),
|
||||
tapResponse(
|
||||
([branches, currentBranch]) => {
|
||||
this.patchState({
|
||||
branches,
|
||||
selectedTakeAwayBranch: currentBranch,
|
||||
selectedPickUpBranch: currentBranch,
|
||||
currentBranch,
|
||||
});
|
||||
this.loadAvailabilities({});
|
||||
},
|
||||
() =>
|
||||
this.patchState({
|
||||
branches: [],
|
||||
selectedTakeAwayBranch: undefined,
|
||||
selectedPickUpBranch: undefined,
|
||||
currentBranch: undefined,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
checkCanAddItems = this.effect((items$: Observable<ShoppingCartItemDTO[]>) =>
|
||||
items$.pipe(
|
||||
withLatestFrom(
|
||||
this.processId$,
|
||||
this.selectedFilterOption$,
|
||||
this.takeAwayAvailabilities$,
|
||||
this.pickUpAvailabilities$,
|
||||
this.deliveryAvailabilities$,
|
||||
this.deliveryB2bAvailabilities$,
|
||||
this.deliveryDigAvailabilities$
|
||||
),
|
||||
mergeMap(([items, processId, selectedOption, takeAway, pickUp, delivery, deliveryB2b, deliveryDig]) => {
|
||||
let orderType: string;
|
||||
const payload = items.map((item) => {
|
||||
switch (selectedOption) {
|
||||
case 'take-away':
|
||||
orderType = 'Rücklage';
|
||||
return {
|
||||
availabilities: [this.getOlaAvailability(takeAway[item.product.catalogProductNumber], item)],
|
||||
id: item.product.catalogProductNumber,
|
||||
};
|
||||
case 'pick-up':
|
||||
orderType = 'Abholung';
|
||||
return {
|
||||
availabilities: [this.getOlaAvailability(pickUp[item.product.catalogProductNumber], item)],
|
||||
id: item.product.catalogProductNumber,
|
||||
};
|
||||
case 'delivery':
|
||||
orderType = 'Versand';
|
||||
if (
|
||||
deliveryDig[item.product.catalogProductNumber] &&
|
||||
deliveryB2b[item.product.catalogProductNumber] &&
|
||||
delivery[item.product.catalogProductNumber]
|
||||
) {
|
||||
return {
|
||||
availabilities: [this.getOlaAvailability(delivery[item.product.catalogProductNumber], item)],
|
||||
id: item.product.catalogProductNumber,
|
||||
};
|
||||
} else if (deliveryDig[item.product.catalogProductNumber]) {
|
||||
return {
|
||||
availabilities: [this.getOlaAvailability(deliveryDig[item.product.catalogProductNumber], item)],
|
||||
id: item.product.catalogProductNumber,
|
||||
};
|
||||
} else if (deliveryB2b[item.product.catalogProductNumber]) {
|
||||
return {
|
||||
availabilities: [this.getOlaAvailability(deliveryB2b[item.product.catalogProductNumber], item)],
|
||||
id: item.product.catalogProductNumber,
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return this._checkoutService.canAddItems({ processId, payload, orderType }).pipe(
|
||||
tapResponse(
|
||||
(result: any) => {
|
||||
const canAdd = {};
|
||||
|
||||
result?.forEach((r) => {
|
||||
canAdd[r.id] = { message: r.message, status: r.status };
|
||||
});
|
||||
|
||||
this.patchState({ canAdd });
|
||||
},
|
||||
(error: Error) => {
|
||||
const canAdd = {};
|
||||
|
||||
items?.forEach((i) => {
|
||||
canAdd[i.product?.catalogProductNumber] = { message: error?.message };
|
||||
});
|
||||
|
||||
this.patchState({ canAdd });
|
||||
}
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
getOlaAvailability(availability: AvailabilityDTO, item: ShoppingCartItemDTO) {
|
||||
return {
|
||||
qty: item.quantity,
|
||||
ean: item.product.ean,
|
||||
itemId: item.product.catalogProductNumber,
|
||||
format: item.product.format,
|
||||
at: availability?.estimatedShippingDate,
|
||||
isPrebooked: availability?.isPrebooked,
|
||||
status: availability?.availabilityType,
|
||||
logisticianId: availability?.logistician?.id,
|
||||
price: availability?.price,
|
||||
ssc: availability?.ssc,
|
||||
sscText: availability?.sscText,
|
||||
supplierId: availability?.supplier?.id,
|
||||
};
|
||||
}
|
||||
|
||||
readonly updateItemQuantity = this.updater((state, value: { itemId: number; quantity: number }) => {
|
||||
const itemToUpdate = state.shoppingCartItems.find((item) => item.id === value.itemId);
|
||||
const otherItems = state.shoppingCartItems.filter((item) => item.id !== value.itemId);
|
||||
const updatedItem = { ...itemToUpdate, quantity: value.quantity };
|
||||
const shoppingCartItems = [...otherItems, updatedItem].sort((a, b) => a.product?.name.localeCompare(b.product.name));
|
||||
|
||||
// Ausgewählte Items auch aktualisieren
|
||||
let selectedShoppingCartItems = state.selectedShoppingCartItems;
|
||||
if (state.selectedShoppingCartItems.find((item) => item.id === value.itemId)) {
|
||||
const selectedItems = state.selectedShoppingCartItems.filter((item) => item.id !== value.itemId);
|
||||
selectedShoppingCartItems = [...selectedItems, updatedItem].sort((a, b) => a.product?.name.localeCompare(b.product.name));
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
shoppingCartItems,
|
||||
selectedShoppingCartItems,
|
||||
};
|
||||
});
|
||||
|
||||
async removeShoppingCartItem(item: ShoppingCartItemDTO) {
|
||||
const items = this.get((s) => s.shoppingCartItems);
|
||||
const processId = this.get((s) => s.processId);
|
||||
|
||||
await this._checkoutService
|
||||
.updateItemInShoppingCart({
|
||||
processId,
|
||||
shoppingCartItemId: item.id,
|
||||
update: {
|
||||
quantity: 0,
|
||||
availability: null,
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
this.selectShoppingCartItem([item], false);
|
||||
const shoppingCartItems = items.filter((i) => i.id !== item.id);
|
||||
this.patchState({ shoppingCartItems });
|
||||
}
|
||||
|
||||
selectShoppingCartItem(shoppingCartItems: ShoppingCartItemDTO[], selected: boolean) {
|
||||
if (selected) {
|
||||
this.patchState({
|
||||
selectedShoppingCartItems: [
|
||||
...this.selectedShoppingCartItems.filter((item) => !shoppingCartItems.find((i) => item.id === i.id)),
|
||||
...shoppingCartItems,
|
||||
],
|
||||
});
|
||||
} else {
|
||||
this.patchState({
|
||||
selectedShoppingCartItems: this.selectedShoppingCartItems.filter((item) => !shoppingCartItems.find((i) => item.id === i.id)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clearSelectedShoppingCartItems() {
|
||||
this.patchState({ selectedShoppingCartItems: [] });
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './take-away-option-list.component';
|
||||
// end:ng42.barrel
|
||||
@@ -1,18 +0,0 @@
|
||||
<div class="option-icon">
|
||||
<ui-icon size="50px" icon="shopping_bag"></ui-icon>
|
||||
</div>
|
||||
<button
|
||||
class="option-chip"
|
||||
[disabled]="optionChipDisabled$ | async"
|
||||
(click)="optionChange('take-away')"
|
||||
[class.selected]="(selectedOption$ | async) === 'take-away'"
|
||||
>
|
||||
Rücklage
|
||||
</button>
|
||||
<p>Möchten Sie die Artikel<br />zurücklegen lassen oder<br />sofort mitnehmen?</p>
|
||||
|
||||
<ui-branch-dropdown
|
||||
[branches]="branches$ | async"
|
||||
[selected]="selectedBranch$ | async"
|
||||
(selectBranch)="selectBranch($event)"
|
||||
></ui-branch-dropdown>
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { BranchDTO } from '@swagger/checkout';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-take-away-option-list',
|
||||
templateUrl: 'take-away-option-list.component.html',
|
||||
styleUrls: ['../list-options.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TakeAwayOptionListComponent {
|
||||
branches$ = this._store.branches$;
|
||||
selectedBranch$ = this._store.selectedTakeAwayBranch$.pipe(
|
||||
map((branch) => {
|
||||
// Determins if branch is targetBranch
|
||||
if (branch?.branchType === 1) {
|
||||
return branch.name;
|
||||
}
|
||||
})
|
||||
);
|
||||
selectedOption$ = this._store.selectedFilterOption$;
|
||||
optionChipDisabled$ = combineLatest([this._store.fetchingAvailabilities$, this.selectedBranch$]).pipe(
|
||||
map(([fetching, selectedBranch]) => {
|
||||
return fetching || !selectedBranch;
|
||||
})
|
||||
);
|
||||
|
||||
constructor(private _store: PurchasingOptionsListModalStore) {}
|
||||
|
||||
optionChange(option: string) {
|
||||
if (this._store.selectedFilterOption === option) {
|
||||
this._store.selectedFilterOption = undefined;
|
||||
} else {
|
||||
this._store.selectedFilterOption = option;
|
||||
}
|
||||
}
|
||||
|
||||
async selectBranch(branch: BranchDTO) {
|
||||
this._store.lastSelectedFilterOption$.next(undefined);
|
||||
|
||||
this._store.selectedTakeAwayBranch = branch;
|
||||
|
||||
const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise();
|
||||
shoppingCartItems.forEach((item) => this._store.loadTakeAwayAvailability({ item }));
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<ng-container *ngIf="item$ | async; let item">
|
||||
<ng-container *ngIf="availability$ | async; let availability">
|
||||
<div class="option-icon">
|
||||
<ui-icon size="80px" icon="truck_b2b"></ui-icon>
|
||||
</div>
|
||||
|
||||
<h4>B2B Versand</h4>
|
||||
<p>
|
||||
Als B2B Kunde können wir Ihnen den Artikel auch liefern.
|
||||
</p>
|
||||
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
|
||||
<div class="grow"></div>
|
||||
<span class="delivery">Versandkostenfrei</span>
|
||||
<span class="date"
|
||||
>Versanddatum <strong>{{ availability?.estimatedShippingDate | date: 'shortDate' }}</strong></span
|
||||
>
|
||||
<div>
|
||||
<button [disabled]="availability.price?.value?.value < 0" type="button" class="select-option" (click)="select()">
|
||||
Auswählen
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -1,9 +0,0 @@
|
||||
.option-icon {
|
||||
margin-top: -12px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply font-bold;
|
||||
margin-top: -2px;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-b2b-delivery-option',
|
||||
templateUrl: 'b2b-delivery-option.component.html',
|
||||
styleUrls: ['../option.scss', 'b2b-delivery-option.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class B2BDeliveryOptionComponent {
|
||||
readonly item$ = this._purchasingOptionsModalStore.selectItem;
|
||||
|
||||
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['b2b-delivery']));
|
||||
|
||||
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
|
||||
map(([availability, item]) => this._availabilityService.getPriceForAvailability('b2b-delivery', item.catalogAvailability, availability))
|
||||
);
|
||||
|
||||
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
|
||||
|
||||
select() {
|
||||
this._purchasingOptionsModalStore.setOption('b2b-delivery');
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './b2b-delivery-option.component';
|
||||
// end:ng42.barrel
|
||||
@@ -1,41 +0,0 @@
|
||||
<ng-container *ngIf="item$ | async; let item">
|
||||
<ng-container *ngIf="availability$ | async; let availability">
|
||||
<div class="option-icon">
|
||||
<ui-icon size="50px" icon="truck"></ui-icon>
|
||||
</div>
|
||||
<h4>Versand</h4>
|
||||
<p>
|
||||
Möchten Sie den Artikel geliefert bekommen?
|
||||
</p>
|
||||
<div class="price-wrapper">
|
||||
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
|
||||
<ng-container *ngIf="showTooltip$ | async">
|
||||
<button [uiOverlayTrigger]="tooltipContent" #tooltip="uiOverlayTrigger" class="info-tooltip-button" type="button">
|
||||
i
|
||||
</button>
|
||||
<ui-tooltip #tooltipContent yPosition="above" xPosition="after" [yOffset]="-16">
|
||||
Günstigerer Preis aus Hugendubel Katalog wird übernommen
|
||||
</ui-tooltip>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<span class="delivery">Versandkostenfrei</span>
|
||||
<span *ngIf="availability?.estimatedDelivery; else estimatedShippingDateTmpl" class="date">
|
||||
Zustellung zwischen <br />
|
||||
<strong
|
||||
>{{ (availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
|
||||
{{ (availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}</strong
|
||||
>
|
||||
</span>
|
||||
<ng-template #estimatedShippingDateTmpl>
|
||||
<span class="date">
|
||||
Versanddatum <strong>{{ availability?.estimatedShippingDate | date }}</strong>
|
||||
</span>
|
||||
</ng-template>
|
||||
<div>
|
||||
<button [disabled]="availability.price?.value?.value < 0" type="button" class="select-option" (click)="select()">
|
||||
Auswählen
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -1,15 +0,0 @@
|
||||
.price-wrapper {
|
||||
@apply mt-2;
|
||||
}
|
||||
|
||||
.info-tooltip-button {
|
||||
@apply border-font-customer border-solid border-2 bg-white rounded-full text-base font-bold;
|
||||
border-style: outset;
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply font-bold;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-delivery-option',
|
||||
templateUrl: 'delivery-option.component.html',
|
||||
styleUrls: ['../option.scss', 'delivery-option.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DeliveryOptionComponent {
|
||||
readonly item$ = this._purchasingOptionsModalStore.selectItem;
|
||||
|
||||
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['delivery']));
|
||||
|
||||
readonly showTooltip$ = combineLatest([this.availability$, this.item$]).pipe(
|
||||
map(([availability, item]) => {
|
||||
const shippingPrice = availability?.price?.value?.value;
|
||||
const catalogPrice = item?.catalogAvailability?.price?.value?.value;
|
||||
return catalogPrice < shippingPrice;
|
||||
})
|
||||
);
|
||||
|
||||
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
|
||||
map(([availability, item]) => this._availabilityService.getPriceForAvailability('delivery', item.catalogAvailability, availability))
|
||||
);
|
||||
|
||||
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
|
||||
|
||||
select() {
|
||||
this._purchasingOptionsModalStore.setOption('delivery');
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './delivery-option.component';
|
||||
// end:ng42.barrel
|
||||
@@ -1,40 +0,0 @@
|
||||
<ng-container *ngIf="item$ | async; let item">
|
||||
<ng-container *ngIf="availability$ | async; let availability">
|
||||
<div class="option-icon">
|
||||
<ui-icon size="50px" icon="truck"></ui-icon>
|
||||
</div>
|
||||
<h4>DIG Versand</h4>
|
||||
<p>Möchten Sie den Artikel geliefert bekommen?</p>
|
||||
<div class="price-wrapper">
|
||||
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
|
||||
<ng-container *ngIf="showTooltip$ | async">
|
||||
<button [uiOverlayTrigger]="tooltipContent" #tooltip="uiOverlayTrigger" class="info-tooltip-button" type="button">
|
||||
i
|
||||
</button>
|
||||
<ui-tooltip #tooltipContent yPosition="above" xPosition="after" [yOffset]="-16">
|
||||
Günstigerer Preis aus Hugendubel Katalog wird übernommen
|
||||
</ui-tooltip>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<span class="delivery">Versandkostenfrei</span>
|
||||
<span *ngIf="availability?.estimatedDelivery; else estimatedShippingDateTmpl" class="date">
|
||||
Zustellung zwischen <br />
|
||||
<strong
|
||||
>{{ (availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
|
||||
{{ (availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}</strong
|
||||
>
|
||||
</span>
|
||||
<ng-template #estimatedShippingDateTmpl>
|
||||
<span class="date">
|
||||
Versanddatum <strong>{{ availability?.estimatedShippingDate | date }}</strong>
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
<div>
|
||||
<button [disabled]="availability.price?.value?.value < 0" type="button" class="select-option" (click)="select()">
|
||||
Auswählen
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -1,15 +0,0 @@
|
||||
.price-wrapper {
|
||||
@apply mt-2;
|
||||
}
|
||||
|
||||
.info-tooltip-button {
|
||||
@apply border-font-customer bg-white rounded-full text-base font-bold;
|
||||
border-style: outset;
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply font-bold;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-dig-delivery-option',
|
||||
templateUrl: 'dig-delivery-option.component.html',
|
||||
styleUrls: ['../option.scss', 'dig-delivery-option.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DigDeliveryOptionComponent {
|
||||
readonly item$ = this._purchasingOptionsModalStore.selectItem;
|
||||
|
||||
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['dig-delivery']));
|
||||
|
||||
readonly showTooltip$ = combineLatest([this.availability$, this.item$]).pipe(
|
||||
map(([availability, item]) => {
|
||||
const shippingPrice = availability?.price?.value?.value;
|
||||
const catalogPrice = item?.catalogAvailability?.price?.value?.value;
|
||||
return catalogPrice < shippingPrice;
|
||||
})
|
||||
);
|
||||
|
||||
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
|
||||
map(([availability, item]) => this._availabilityService.getPriceForAvailability('dig-delivery', item.catalogAvailability, availability))
|
||||
);
|
||||
|
||||
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
|
||||
|
||||
select() {
|
||||
this._purchasingOptionsModalStore.setOption('dig-delivery');
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './dig-delivery-option.component';
|
||||
// end:ng42.barrel
|
||||
@@ -1,6 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './options';
|
||||
export * from './purchasing-options-modal.component';
|
||||
export * from './purchasing-options-modal.data';
|
||||
export * from './purchasing-options-modal.module';
|
||||
// end:ng42.barrel
|
||||
@@ -1,41 +0,0 @@
|
||||
:host {
|
||||
@apply flex flex-col box-border text-center;
|
||||
width: 202px;
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
@apply text-ucla-blue mx-auto;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-2xl mt-4 mb-0;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply my-2;
|
||||
}
|
||||
|
||||
.price {
|
||||
@apply font-bold my-2;
|
||||
}
|
||||
|
||||
.delivery {
|
||||
@apply text-regular mb-px-5;
|
||||
}
|
||||
|
||||
.date {
|
||||
@apply text-cta-l whitespace-nowrap;
|
||||
}
|
||||
|
||||
.grow {
|
||||
@apply flex-grow;
|
||||
}
|
||||
|
||||
.select-option {
|
||||
@apply mt-4 mb-4 border-2 border-solid border-brand text-brand text-cta-l font-bold bg-white rounded-full py-3 px-6;
|
||||
}
|
||||
|
||||
.select-option:disabled {
|
||||
@apply bg-disabled-branch border-disabled-branch text-white;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './b2b-delivery-option';
|
||||
export * from './delivery-option';
|
||||
export * from './dig-delivery-option';
|
||||
export * from './pick-up-option';
|
||||
export * from './take-away-option';
|
||||
// end:ng42.barrel
|
||||
@@ -1,3 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './pick-up-option.component';
|
||||
// end:ng42.barrel
|
||||
@@ -1,32 +0,0 @@
|
||||
<ng-container *ngIf="item$ | async; let item">
|
||||
<ng-container *ngIf="availability$ | async; let availability">
|
||||
<div class="option-icon">
|
||||
<ui-icon size="50px" icon="box_out"></ui-icon>
|
||||
</div>
|
||||
<h4>Abholung</h4>
|
||||
<p>
|
||||
Möchten Sie den Artikel in einer unserer Filialen abholen?
|
||||
</p>
|
||||
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
|
||||
<ui-branch-dropdown
|
||||
class="min-h-[38px]"
|
||||
[branches]="branches$ | async"
|
||||
[selected]="selected$ | async"
|
||||
(selectBranch)="selectBranch($event)"
|
||||
></ui-branch-dropdown>
|
||||
<span class="date"
|
||||
>Abholung ab <strong>{{ (availability$ | async)?.estimatedShippingDate | date: 'shortDate' }}</strong></span
|
||||
>
|
||||
<div class="grow"></div>
|
||||
<div>
|
||||
<button
|
||||
[disabled]="availability.price?.value?.value < 0 || !(selected$ | async)"
|
||||
type="button"
|
||||
class="select-option"
|
||||
(click)="select()"
|
||||
>
|
||||
Auswählen
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -1,3 +0,0 @@
|
||||
h4 {
|
||||
@apply font-bold;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { BranchDTO } from '@swagger/checkout';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-pick-up-option',
|
||||
templateUrl: 'pick-up-option.component.html',
|
||||
styleUrls: ['../option.scss', 'pick-up-option.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PickUpOptionComponent {
|
||||
branches$: Observable<BranchDTO[]> = this._purchasingOptionsModalStore.selectAvailableBranches;
|
||||
selected$: Observable<string> = this._purchasingOptionsModalStore.selectBranch.pipe(
|
||||
map((branch) => {
|
||||
// Determins if branch is targetBranch
|
||||
if (branch?.branchType === 1) {
|
||||
return branch.name;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
readonly item$ = this._purchasingOptionsModalStore.selectItem;
|
||||
|
||||
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['pick-up']));
|
||||
|
||||
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
|
||||
map(([availability, item]) => this._availabilityService.getPriceForAvailability('pick-up', item.catalogAvailability, availability))
|
||||
);
|
||||
|
||||
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
|
||||
|
||||
select() {
|
||||
this._purchasingOptionsModalStore.setOption('pick-up');
|
||||
}
|
||||
|
||||
selectBranch(branch: BranchDTO) {
|
||||
this._purchasingOptionsModalStore.setBranch(branch);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<form *ngIf="control" [formGroup]="control">
|
||||
<ui-form-control label="MwSt" variant="default" *ngIf="!hideVat">
|
||||
<ui-select formControlName="vat">
|
||||
<ui-select-option *ngFor="let vat of vats$ | async" [label]="vat.name + '%'" [value]="vat.vatType"> </ui-select-option>
|
||||
</ui-select>
|
||||
</ui-form-control>
|
||||
|
||||
<ui-form-control class="price" label="Preis" variant="default">
|
||||
<input uiInput formControlName="price" [max]="maxValue" maxLength="6" />
|
||||
</ui-form-control>
|
||||
</form>
|
||||
@@ -1,11 +0,0 @@
|
||||
form {
|
||||
@apply grid grid-flow-col items-center justify-end gap-4 mb-2;
|
||||
}
|
||||
|
||||
ui-form-control {
|
||||
@apply w-32;
|
||||
}
|
||||
|
||||
::ng-deep page-purchasing-options-modal-price-input ui-form-control .input-wrapper input {
|
||||
@apply w-20;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { DomainOmsService } from '@domain/oms';
|
||||
import { VATType } from '@swagger/checkout';
|
||||
import { VATDTO } from '@swagger/oms';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'page-purchasing-options-modal-price-input',
|
||||
templateUrl: 'purchasing-options-modal-price-input.component.html',
|
||||
styleUrls: ['purchasing-options-modal-price-input.component.scss'],
|
||||
providers: [UntypedFormBuilder],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PurchasingOptionsModalPriceInputComponent implements OnInit {
|
||||
control: UntypedFormGroup;
|
||||
|
||||
vats$: Observable<VATDTO[]> = this._omsService.getVATs().pipe(shareReplay());
|
||||
|
||||
@Output()
|
||||
priceChanged = new EventEmitter<number>();
|
||||
|
||||
@Output()
|
||||
vatChanged = new EventEmitter<VATType>();
|
||||
|
||||
private _subscriptions = new Subscription();
|
||||
|
||||
@Input()
|
||||
hideVat = false;
|
||||
|
||||
@Input()
|
||||
maxValue = 99999;
|
||||
|
||||
constructor(private _omsService: DomainOmsService, private _fb: UntypedFormBuilder) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.initForm();
|
||||
}
|
||||
|
||||
initForm() {
|
||||
const fb = this._fb;
|
||||
this.control = fb.group({
|
||||
price: fb.control(undefined, [Validators.required, Validators.pattern(/^\d+([\,]\d{1,2})?$/), Validators.max(this.maxValue)]),
|
||||
vat: fb.control(0, [Validators.required]),
|
||||
});
|
||||
|
||||
this._subscriptions.add(
|
||||
this.control.get('price').valueChanges.subscribe((price) => this.priceChanged.emit(Number(String(price).replace(',', '.'))))
|
||||
);
|
||||
this._subscriptions.add(this.control.get('vat').valueChanges.subscribe(this.vatChanged));
|
||||
|
||||
this.control.markAllAsTouched();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { UiFormControlModule } from '@ui/form-control';
|
||||
import { UiInputModule } from '@ui/input';
|
||||
import { UiSelectModule } from '@ui/select';
|
||||
|
||||
import { PurchasingOptionsModalPriceInputComponent } from './purchasing-options-modal-price-input.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiFormControlModule, UiInputModule, UiSelectModule, FormsModule, ReactiveFormsModule],
|
||||
exports: [PurchasingOptionsModalPriceInputComponent],
|
||||
declarations: [PurchasingOptionsModalPriceInputComponent],
|
||||
providers: [],
|
||||
})
|
||||
export class PurchasingOptionsModalPriceInputModule {}
|
||||
@@ -1,140 +0,0 @@
|
||||
<ng-container *ngIf="(hasOption$ | async) === false">
|
||||
<h3 class="modal-title">Wie möchten Sie den Artikel erhalten?</h3>
|
||||
|
||||
<div class="options-wrapper">
|
||||
<ng-container *ngFor="let option of availableOptions$ | async" [ngSwitch]="option">
|
||||
<page-take-away-option *ngSwitchCase="'take-away'"></page-take-away-option>
|
||||
<page-pick-up-option *ngSwitchCase="'pick-up'"></page-pick-up-option>
|
||||
<page-delivery-option *ngSwitchCase="'delivery'"></page-delivery-option>
|
||||
<page-dig-delivery-option *ngSwitchCase="'dig-delivery'"></page-dig-delivery-option>
|
||||
<page-b2b-delivery-option *ngSwitchCase="'b2b-delivery'"></page-b2b-delivery-option>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="(availableOptions$ | async).length === 0">
|
||||
<p class="hint">Derzeit nicht bestellbar</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="hasOption$ | async">
|
||||
<h3 class="modal-title">Artikel dem Warenkorb hinzufügen</h3>
|
||||
|
||||
<div class="option-product-summary" *ngIf="item$ | async; let item">
|
||||
<div class="header-row">
|
||||
<h5 class="option-name" *ngIf="option$ | async; let option">
|
||||
<ng-container *ngIf="option | purchaseOptionIcon; let icon">
|
||||
<ui-icon [size]="icon === 'truck_b2b' ? '40px' : '23px'" [icon]="icon"></ui-icon>
|
||||
</ng-container>
|
||||
|
||||
{{ option | purchaseOptionName }}
|
||||
</h5>
|
||||
<span *ngIf="(option$ | async) !== 'download'">
|
||||
in der Filiale:
|
||||
<span class="option-branch">{{ (branch$ | async)?.name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="product-row">
|
||||
<img class="thumbnail" [src]="(item?.imageId !== undefined ? item?.imageId : item?.product?.ean) | productImage: 80:100:true" />
|
||||
<div class="details">
|
||||
<h6 class="title">{{ item?.product?.contributors }} - {{ item?.product?.name }}</h6>
|
||||
<strong class="can-add-error" *ngIf="canAddError$ | async; let canAddError">{{ canAddError }}</strong>
|
||||
<div class="grow"></div>
|
||||
<div class="format" *ngIf="item?.product?.format && item?.product?.formatDetail">
|
||||
<img
|
||||
*ngIf="item?.product?.format !== '--'"
|
||||
src="assets/images/Icon_{{ item?.product?.format }}.svg"
|
||||
[alt]="item?.product?.formatDetail"
|
||||
/>
|
||||
{{ item?.product?.formatDetail }}
|
||||
</div>
|
||||
|
||||
<div class="price">
|
||||
{{ price$ | async | currency: item?.catalogAvailability?.price?.value?.currency || 'EUR':'code' }}
|
||||
</div>
|
||||
<div class="date" *ngIf="option$ | async; let option">
|
||||
<ng-container *ngIf="option === 'pick-up'">
|
||||
Abholung ab {{ (getAvailability(option) | async)?.estimatedShippingDate | date: 'shortDate' }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showDeliveryInfo$ | async">
|
||||
<ng-container *ngIf="getAvailability(option) | async; let availability">
|
||||
<ng-container *ngIf="availability?.estimatedDelivery; else estimatedShippingDate">
|
||||
Zustellung zwischen {{ (availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
|
||||
{{ (availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
|
||||
</ng-container>
|
||||
<ng-template #estimatedShippingDate> Versanddatum {{ availability?.estimatedShippingDate | date: 'shortDate' }} </ng-template>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quantity">
|
||||
<div class="row">
|
||||
<ui-quantity-dropdown
|
||||
#quantityControl
|
||||
[showSpinner]="purchasingOptionsModalStore.selectFetchingAvailability | async"
|
||||
[ngModel]="quantity$ | async"
|
||||
[range]="quantityRange$ | async"
|
||||
(ngModelChange)="changeQuantity($event)"
|
||||
>
|
||||
</ui-quantity-dropdown>
|
||||
<button *ngIf="!quantityControl?.customInput" (click)="backToSetOptions()" class="cta-modify">Ändern</button>
|
||||
</div>
|
||||
<div class="quantity-error" *ngIf="quantityError$ | async; let message">{{ message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-price" *ngIf="showCustomPrice$ | async">
|
||||
<page-purchasing-options-modal-price-input
|
||||
[hideVat]="isGiftCard(item.type)"
|
||||
[maxValue]="isGiftCard(item.type) ? 200 : 99999"
|
||||
(priceChanged)="changeCustomPrice($event)"
|
||||
(vatChanged)="changeCustomVat($event)"
|
||||
>
|
||||
</page-purchasing-options-modal-price-input>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="summary-row" *ngIf="quantity$ | async; let quantity">
|
||||
<div class="reading-points">
|
||||
{{ quantity }} Artikel
|
||||
<ng-container *ngIf="promoPoints$ | async; let promoPoints"> | {{ promoPoints }} Lesepunkte </ng-container>
|
||||
</div>
|
||||
<div class="subtotal">
|
||||
Zwischensumme
|
||||
{{ (price$ | async) * quantity | currency: item?.catalogAvailability?.price?.value?.currency || 'EUR':'code' }}
|
||||
<div class="shipping-cost" *ngIf="showDeliveryInfo$ | async">
|
||||
ohne Versandkosten
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions" *ngIf="option$ | async; let option">
|
||||
<button
|
||||
*ngIf="canContinueShopping$ | async"
|
||||
class="cta-continue-shopping"
|
||||
[disabled]="(fetching$ | async) || (canContinueShopping$ | async) === false || (customPriceInvalid$ | async) === true"
|
||||
(click)="continue('continue-shopping')"
|
||||
>
|
||||
Weiter einkaufen
|
||||
</button>
|
||||
<button *ngIf="canUpgrade$ | async" class="cta-upgrade-customer" (click)="continue('add-customer-data')">
|
||||
Kundendaten erfassen
|
||||
</button>
|
||||
<button
|
||||
*ngIf="showTakeAwayButton$ | async"
|
||||
[disabled]="(fetching$ | async) || (canAdd$ | async) === false || (customPriceInvalid$ | async) === true"
|
||||
class="cta-continue"
|
||||
(click)="continue()"
|
||||
>
|
||||
Reservieren
|
||||
</button>
|
||||
<button
|
||||
*ngIf="showDefaultContinueButton$ | async"
|
||||
class="cta-continue"
|
||||
(click)="continue()"
|
||||
[disabled]="(fetching$ | async) || (canAdd$ | async) === false || (customPriceInvalid$ | async) === true"
|
||||
>
|
||||
Fortfahren
|
||||
</button>
|
||||
<button *ngIf="showContinueWithoutAdding$ | async" class="cta-continue" (click)="continue()">
|
||||
Ohne Artikel Fortfahren
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -1,124 +0,0 @@
|
||||
:host {
|
||||
@apply block box-border;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@apply text-center mt-2 text-xl font-bold;
|
||||
}
|
||||
|
||||
.cta-modify {
|
||||
@apply self-end bg-transparent text-brand font-bold text-lg outline-none border-none ml-4;
|
||||
}
|
||||
|
||||
.options-wrapper {
|
||||
@apply flex flex-row justify-evenly items-stretch mt-2;
|
||||
}
|
||||
|
||||
.option-product-summary {
|
||||
@apply flex flex-col box-border;
|
||||
|
||||
hr {
|
||||
@apply my-4;
|
||||
}
|
||||
}
|
||||
|
||||
.option-name {
|
||||
@apply flex flex-row items-center font-bold text-card-sub mb-2 mt-1;
|
||||
|
||||
ui-icon {
|
||||
@apply mr-2 text-ucla-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.option-branch {
|
||||
@apply font-bold;
|
||||
}
|
||||
|
||||
.product-row {
|
||||
@apply flex flex-row items-center;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
@apply flex flex-row justify-between font-bold;
|
||||
}
|
||||
|
||||
.reading-points {
|
||||
@apply text-ucla-blue;
|
||||
}
|
||||
|
||||
.subtotal {
|
||||
@apply text-lg;
|
||||
}
|
||||
|
||||
.shipping-cost {
|
||||
@apply text-sm text-right font-normal;
|
||||
}
|
||||
|
||||
img.thumbnail {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.grow {
|
||||
@apply flex-grow;
|
||||
}
|
||||
|
||||
.details {
|
||||
@apply ml-4 flex flex-col font-bold self-stretch flex-grow;
|
||||
|
||||
.title {
|
||||
@apply text-base m-0;
|
||||
}
|
||||
}
|
||||
|
||||
.format {
|
||||
@apply flex flex-row items-center whitespace-nowrap;
|
||||
|
||||
img {
|
||||
@apply mr-2;
|
||||
}
|
||||
}
|
||||
|
||||
.quantity {
|
||||
@apply self-end flex flex-col justify-end;
|
||||
|
||||
.row {
|
||||
@apply flex flex-row justify-end;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
@apply flex flex-row justify-end items-center mt-8;
|
||||
}
|
||||
|
||||
.cta-continue-shopping {
|
||||
@apply text-brand border-2 border-solid border-brand bg-white font-bold text-lg px-4 py-2 rounded-full;
|
||||
|
||||
::ng-deep.spin {
|
||||
@apply text-brand;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply text-inactive-branch border-inactive-branch cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.cta-continue,
|
||||
.cta-upgrade-customer {
|
||||
@apply text-white bg-brand font-bold text-lg px-4 py-2 rounded-full border-none ml-4 no-underline;
|
||||
|
||||
&:disabled {
|
||||
@apply bg-inactive-branch cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.can-add-error {
|
||||
@apply text-xl text-dark-goldenrod mt-2;
|
||||
}
|
||||
|
||||
.quantity-error {
|
||||
@apply text-dark-goldenrod font-bold text-sm mt-2;
|
||||
}
|
||||
|
||||
.hint {
|
||||
@apply text-dark-goldenrod font-bold text-xl;
|
||||
}
|
||||
@@ -1,396 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { AddToShoppingCartDTO, AvailabilityDTO, ItemType, VATType } from '@swagger/checkout';
|
||||
import { UiModalRef } from '@ui/modal';
|
||||
import { shareReplay, debounceTime, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { PurchasingOptionsModalData } from './purchasing-options-modal.data';
|
||||
import { PurchasingOptionsModalStore } from './purchasing-options-modal.store';
|
||||
import { DomainCatalogService } from '@domain/catalog';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import {
|
||||
encodeFormData,
|
||||
mapCustomerDtoToCustomerCreateFormData,
|
||||
} from 'apps/page/customer/src/lib/create-customer/customer-create-form-data';
|
||||
import { isNumber } from '@utils/common';
|
||||
|
||||
@Component({
|
||||
selector: 'page-purchasing-options-modal',
|
||||
templateUrl: 'purchasing-options-modal.component.html',
|
||||
styleUrls: ['purchasing-options-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [PurchasingOptionsModalStore],
|
||||
})
|
||||
export class PurchasingOptionsModalComponent {
|
||||
readonly item$ = this.purchasingOptionsModalStore.selectItem;
|
||||
|
||||
readonly availableOptions$ = this.purchasingOptionsModalStore.selectAvailableOptions.pipe(shareReplay());
|
||||
|
||||
readonly option$ = this.purchasingOptionsModalStore.selectOption;
|
||||
|
||||
readonly hasOption$ = this.purchasingOptionsModalStore.selectHasOption;
|
||||
|
||||
readonly quantity$ = this.purchasingOptionsModalStore.selectQuantity;
|
||||
|
||||
readonly canAdd$ = this.purchasingOptionsModalStore.selectCanAdd;
|
||||
|
||||
readonly canAddError$ = this.purchasingOptionsModalStore.selectCanAddError;
|
||||
|
||||
readonly canUpgrade$ = this.purchasingOptionsModalStore.selectCanUpgrade;
|
||||
|
||||
readonly availability$ = this.purchasingOptionsModalStore.selectAvailability;
|
||||
|
||||
readonly branch$ = this.purchasingOptionsModalStore.selectBranch;
|
||||
|
||||
readonly canContinueShopping$ = this.purchasingOptionsModalStore.selectCanContinueShopping;
|
||||
|
||||
readonly canContinueShoppingIsLoading$ = this.purchasingOptionsModalStore.selectCanContinueShoppingIsLoading;
|
||||
|
||||
readonly quantityError$ = this.purchasingOptionsModalStore.selectQuantityError;
|
||||
|
||||
readonly showCustomPrice$ = this.purchasingOptionsModalStore.selectAvailabilities.pipe(
|
||||
withLatestFrom(this.option$),
|
||||
map(([availabilities, option]) => !availabilities[option]?.price?.value?.value)
|
||||
);
|
||||
|
||||
readonly customPriceInvalid$ = combineLatest([
|
||||
this.item$,
|
||||
this.showCustomPrice$,
|
||||
this.purchasingOptionsModalStore.selectCustomPrice,
|
||||
this.purchasingOptionsModalStore.selectCustomVat,
|
||||
]).pipe(
|
||||
map(([item, showCustomPrice, customPrice, customVat]) => {
|
||||
if (!showCustomPrice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((item.type as any) === 66560) {
|
||||
return !isNumber(customPrice) || customPrice < 1 || customPrice > 200;
|
||||
}
|
||||
|
||||
return !customPrice || !customVat;
|
||||
})
|
||||
);
|
||||
|
||||
readonly showTakeAwayButton$ = combineLatest([
|
||||
this.option$,
|
||||
this.purchasingOptionsModalStore.selectFetchingAvailability,
|
||||
this.purchasingOptionsModalStore.selectCheckingCanAdd,
|
||||
this.canAdd$,
|
||||
this.canUpgrade$,
|
||||
]).pipe(
|
||||
map(([option, fetchingAvailability, checkingCanAdd, canAdd, canUpgrade]) => {
|
||||
if (option !== 'take-away') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fetchingAvailability && !checkingCanAdd && !canAdd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !canUpgrade;
|
||||
})
|
||||
);
|
||||
|
||||
readonly showDefaultContinueButton$ = combineLatest([
|
||||
this.option$,
|
||||
this.purchasingOptionsModalStore.selectFetchingAvailability,
|
||||
this.purchasingOptionsModalStore.selectCheckingCanAdd,
|
||||
this.canAdd$,
|
||||
this.canUpgrade$,
|
||||
]).pipe(
|
||||
map(([option, fetchingAvailability, checkingCanAdd, canAdd, canUpgrade]) => {
|
||||
if (option === 'take-away') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fetchingAvailability && !checkingCanAdd && !canAdd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !canUpgrade;
|
||||
})
|
||||
);
|
||||
|
||||
readonly showContinueWithoutAdding$ = combineLatest([this.showTakeAwayButton$, this.showDefaultContinueButton$, this.canUpgrade$]).pipe(
|
||||
map(([showTakeAway, showDefault, canUpgrade]) => !canUpgrade && !(showTakeAway || showDefault))
|
||||
);
|
||||
|
||||
readonly showDeliveryInfo$ = this.option$.pipe(map((option) => ['delivery', 'b2b-delivery', 'dig-delivery'].indexOf(option) > -1));
|
||||
|
||||
readonly fetching$ = combineLatest([
|
||||
this.purchasingOptionsModalStore.selectFetchingAvailability,
|
||||
this.purchasingOptionsModalStore.selectCheckingCanAdd,
|
||||
]).pipe(map(([fetching, checking]) => fetching || checking));
|
||||
|
||||
customerFeatures$ = this.application.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.checkoutService.getCustomerFeatures({ processId }))
|
||||
);
|
||||
|
||||
readonly customer$ = this.application.activatedProcessId$.pipe(switchMap((processId) => this.checkoutService.getCustomer({ processId })));
|
||||
|
||||
price$ = combineLatest([
|
||||
this.purchasingOptionsModalStore.selectAvailabilities,
|
||||
this.option$,
|
||||
this.purchasingOptionsModalStore.selectCustomPrice,
|
||||
]).pipe(
|
||||
map(([availabilities, option, customPrice]) => {
|
||||
if (option && !!availabilities[option]) {
|
||||
if (availabilities[option]?.price?.value?.value) {
|
||||
return availabilities[option]?.price?.value?.value;
|
||||
}
|
||||
return availabilities[option]?.price?.value?.value ?? customPrice;
|
||||
} else {
|
||||
const key = Object.keys(availabilities).find((key) => !!availabilities[key]?.price?.value?.value);
|
||||
return availabilities[key]?.price?.value?.value ?? customPrice;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
vat$ = combineLatest([
|
||||
this.purchasingOptionsModalStore.selectAvailabilities,
|
||||
this.option$,
|
||||
this.purchasingOptionsModalStore.selectCustomVat,
|
||||
]).pipe(
|
||||
map(([availabilities, option, customVat]) => {
|
||||
if (option && !!availabilities[option]) {
|
||||
if (availabilities[option]?.price?.vat?.vatType) {
|
||||
return availabilities[option]?.price?.vat?.vatType;
|
||||
}
|
||||
return availabilities[option]?.price?.vat?.vatType ?? customVat;
|
||||
} else {
|
||||
const key = Object.keys(availabilities).find((key) => !!availabilities[key]?.price?.vat?.vatType);
|
||||
return availabilities[key]?.price?.vat?.vatType ?? customVat;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
readonly promoPoints$ = combineLatest([this.item$, this.quantity$, this.price$]).pipe(
|
||||
debounceTime(250),
|
||||
switchMap(([item, quantity, price]) =>
|
||||
this.domainCatalogService
|
||||
.getPromotionPoints({
|
||||
items: [
|
||||
{
|
||||
id: item.id,
|
||||
quantity: quantity,
|
||||
price: price,
|
||||
},
|
||||
],
|
||||
})
|
||||
.pipe(map((res) => res.result[item.id]))
|
||||
)
|
||||
);
|
||||
|
||||
quantityRange$ = combineLatest([this.option$, this.availability$]).pipe(
|
||||
map(([option, availability]) => (option === 'take-away' && availability?.inStock ? availability.inStock : 999))
|
||||
);
|
||||
|
||||
activeSpinner: string;
|
||||
|
||||
constructor(
|
||||
public modalRef: UiModalRef<any, PurchasingOptionsModalData>,
|
||||
public purchasingOptionsModalStore: PurchasingOptionsModalStore,
|
||||
private application: ApplicationService,
|
||||
private router: Router,
|
||||
private checkoutService: DomainCheckoutService,
|
||||
private domainCatalogService: DomainCatalogService,
|
||||
private breadcrumb: BreadcrumbService
|
||||
) {
|
||||
this.purchasingOptionsModalStore.setShoppingCartItem(this.modalRef.data.shoppingCartItem);
|
||||
this.purchasingOptionsModalStore.setItem(this.modalRef.data.item);
|
||||
this.purchasingOptionsModalStore.setProcessId(this.modalRef.data.processId || this.application.activatedProcessId);
|
||||
this.purchasingOptionsModalStore.setAvailabilities(this.modalRef.data.availabilities || {});
|
||||
this.purchasingOptionsModalStore.setQuantity(this.modalRef?.data?.shoppingCartItem?.quantity || 1);
|
||||
this.purchasingOptionsModalStore.setOption(this.modalRef.data.option);
|
||||
this.purchasingOptionsModalStore.setAvailableOptions(this.modalRef.data.availableOptions);
|
||||
|
||||
if (
|
||||
this.modalRef.data.availableOptions?.some((option) => option === 'pick-up' || option === 'take-away') ||
|
||||
['take-away', 'pick-up'].includes(this.modalRef.data.option)
|
||||
) {
|
||||
this.purchasingOptionsModalStore.loadBranches(this.modalRef?.data?.branchId);
|
||||
}
|
||||
}
|
||||
|
||||
changeCustomVat(vat: VATType) {
|
||||
this.purchasingOptionsModalStore.setCustomVat(vat);
|
||||
}
|
||||
|
||||
changeCustomPrice(price: number) {
|
||||
this.purchasingOptionsModalStore.setCustomPrice(price);
|
||||
}
|
||||
|
||||
backToSetOptions() {
|
||||
this.purchasingOptionsModalStore.setOption(undefined);
|
||||
}
|
||||
|
||||
getAvailability(option: string): Observable<AvailabilityDTO> {
|
||||
return this.purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava[option]));
|
||||
}
|
||||
|
||||
async changeQuantity(quantity: number = 1) {
|
||||
this.purchasingOptionsModalStore.setQuantity(quantity);
|
||||
|
||||
if (quantity === 0) {
|
||||
this.modalRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
async continue(navigate: 'continue' | 'continue-shopping' | 'add-customer-data' = 'continue') {
|
||||
this.activeSpinner = navigate ? 'continue-shopping' : 'continue';
|
||||
try {
|
||||
const processId = await this.purchasingOptionsModalStore.selectProcessId.pipe(first()).toPromise();
|
||||
const buyer = await this.checkoutService.getBuyer({ processId }).pipe(first()).toPromise();
|
||||
const item = await this.item$.pipe(first()).toPromise();
|
||||
const quantity = await this.quantity$.pipe(first()).toPromise();
|
||||
const availability = await this.availability$.pipe(first()).toPromise();
|
||||
const option = await this.option$.pipe(first()).toPromise();
|
||||
const branch = await this.branch$.pipe(first()).toPromise();
|
||||
const shoppingCartItem = await this.purchasingOptionsModalStore.selectShoppingCartItem.pipe(first()).toPromise();
|
||||
const canAdd = await this.canAdd$.pipe(first()).toPromise();
|
||||
const customPrice = await this.purchasingOptionsModalStore.selectCustomPrice.pipe(first()).toPromise();
|
||||
const customVat = (await this.purchasingOptionsModalStore.selectCustomVat.pipe(first()).toPromise()) ?? 0;
|
||||
const customer = await this.checkoutService.getCustomer({ processId }).pipe(first()).toPromise();
|
||||
|
||||
if (canAdd || navigate === 'add-customer-data') {
|
||||
const newItem: AddToShoppingCartDTO = {
|
||||
quantity,
|
||||
availability,
|
||||
product: {
|
||||
catalogProductNumber: '',
|
||||
...item.product,
|
||||
},
|
||||
promotion: { points: item.promoPoints },
|
||||
itemType: item.type,
|
||||
};
|
||||
|
||||
newItem.product.catalogProductNumber = String(item.id);
|
||||
|
||||
if (!!customPrice && !!customVat) {
|
||||
newItem.availability.price = {
|
||||
value: {
|
||||
value: customPrice,
|
||||
currency: 'EUR',
|
||||
},
|
||||
vat: {
|
||||
vatType: customVat,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const price = await this.price$.pipe(first()).toPromise();
|
||||
const vat = await this.vat$.pipe(first()).toPromise();
|
||||
newItem.availability.price = {
|
||||
value: {
|
||||
value: price,
|
||||
currency: 'EUR',
|
||||
},
|
||||
vat: {
|
||||
vatType: vat,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
switch (option) {
|
||||
case 'take-away':
|
||||
case 'pick-up':
|
||||
newItem.destination = {
|
||||
data: { target: 1, targetBranch: { id: branch.id } },
|
||||
};
|
||||
break;
|
||||
case 'delivery':
|
||||
case 'dig-delivery':
|
||||
case 'b2b-delivery':
|
||||
newItem.destination = {
|
||||
data: { target: 2, logistician: availability.logistician },
|
||||
};
|
||||
break;
|
||||
case 'download':
|
||||
newItem.destination = {
|
||||
data: { target: 16, logistician: availability.logistician },
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
if (shoppingCartItem) {
|
||||
await this.checkoutService
|
||||
.updateItemInShoppingCart({
|
||||
processId,
|
||||
shoppingCartItemId: shoppingCartItem?.id,
|
||||
update: {
|
||||
availability: newItem.availability,
|
||||
quantity: newItem.quantity,
|
||||
destination: newItem.destination,
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
} else {
|
||||
await this.checkoutService
|
||||
.addItemToShoppingCart({
|
||||
processId,
|
||||
items: [newItem],
|
||||
})
|
||||
.toPromise();
|
||||
}
|
||||
}
|
||||
|
||||
this.modalRef.close();
|
||||
|
||||
if (shoppingCartItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigate === 'continue-shopping') {
|
||||
const crumbs = await this.breadcrumb
|
||||
.getBreadcrumbsByKeyAndTags$(processId, ['catalog', 'filter', 'results'])
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
if (!!crumbs && crumbs.length > 0) {
|
||||
const queryParams = crumbs[0].params;
|
||||
this.router.navigate(['/kunde', this.application.activatedProcessId, 'product', 'search', 'results'], { queryParams });
|
||||
} else {
|
||||
// Route back to search if no result page was loaded (f.e. When searching Article and landing directly on details page)
|
||||
this.router.navigate(['/kunde', this.application.activatedProcessId, 'product', 'search']);
|
||||
}
|
||||
} else if (navigate === 'continue') {
|
||||
// Set filter for navigation to customer search if customer is not set
|
||||
let filter: { [key: string]: string };
|
||||
if (!buyer) {
|
||||
filter = await this.customerFeatures$
|
||||
.pipe(
|
||||
first(),
|
||||
switchMap((customerFeatures) => {
|
||||
return this.checkoutService.canSetCustomer({ processId, customerFeatures });
|
||||
}),
|
||||
map((res) => res.filter)
|
||||
)
|
||||
.toPromise();
|
||||
this.router.navigate(['/kunde', this.application.activatedProcessId, 'customer', 'search'], {
|
||||
queryParams: { filter_customertype: filter.customertype },
|
||||
});
|
||||
} else {
|
||||
this.router.navigate(['/kunde', this.application.activatedProcessId, 'cart', 'review']);
|
||||
}
|
||||
} else if (navigate === 'add-customer-data') {
|
||||
if (customer?.attributes.some((attr) => attr.data.key === 'p4mUser')) {
|
||||
this.router.navigate(['/kunde', this.application.activatedProcessId, 'customer', 'create', 'webshop-p4m'], {
|
||||
queryParams: { formData: encodeFormData(mapCustomerDtoToCustomerCreateFormData(customer)) },
|
||||
});
|
||||
} else {
|
||||
this.router.navigate(['/kunde', this.application.activatedProcessId, 'customer', 'create', 'webshop'], {
|
||||
queryParams: { formData: encodeFormData(mapCustomerDtoToCustomerCreateFormData(customer)) },
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('PurchasingOptionsModalComponent.continue', error);
|
||||
}
|
||||
this.activeSpinner = undefined;
|
||||
}
|
||||
|
||||
isGiftCard(itemType: ItemType): boolean {
|
||||
return (itemType as any) === 66560;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { ItemDTO } from '@swagger/cat';
|
||||
import { AvailabilityDTO, ShoppingCartItemDTO } from '@swagger/checkout';
|
||||
import { PurchasingOptions } from './purchasing-options-modal.store';
|
||||
|
||||
export interface PurchasingOptionsModalData {
|
||||
item: ItemDTO;
|
||||
availableOptions: PurchasingOptions[];
|
||||
processId?: number;
|
||||
option?: PurchasingOptions;
|
||||
shoppingCartItem?: ShoppingCartItemDTO;
|
||||
availabilities?: { [key: string]: AvailabilityDTO };
|
||||
branchId?: number;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
|
||||
import {
|
||||
B2BDeliveryOptionComponent,
|
||||
TakeAwayOptionComponent,
|
||||
PickUpOptionComponent,
|
||||
DeliveryOptionComponent,
|
||||
DigDeliveryOptionComponent,
|
||||
} from './options';
|
||||
|
||||
import { PurchasingOptionsModalComponent } from './purchasing-options-modal.component';
|
||||
import { PageCheckoutPipeModule } from '../../pipes/page-checkout-pipe.module';
|
||||
import { ProductImageModule } from 'apps/cdn/product-image/src/public-api';
|
||||
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
|
||||
import { KeyNavigationModule } from '../../shared/key-navigation/key-navigation.module';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
|
||||
import { PurchasingOptionsModalPriceInputModule } from './price-input/purchasing-options-modal-price-input.module';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { UiBranchDropdownModule } from '@ui/branch-dropdown';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
UiCommonModule,
|
||||
FormsModule,
|
||||
UiIconModule,
|
||||
OverlayModule,
|
||||
PageCheckoutPipeModule,
|
||||
ProductImageModule,
|
||||
UiQuantityDropdownModule,
|
||||
UiSpinnerModule,
|
||||
KeyNavigationModule,
|
||||
RouterModule,
|
||||
PurchasingOptionsModalPriceInputModule,
|
||||
UiTooltipModule,
|
||||
UiBranchDropdownModule,
|
||||
],
|
||||
exports: [PurchasingOptionsModalComponent],
|
||||
declarations: [
|
||||
PurchasingOptionsModalComponent,
|
||||
B2BDeliveryOptionComponent,
|
||||
TakeAwayOptionComponent,
|
||||
PickUpOptionComponent,
|
||||
DeliveryOptionComponent,
|
||||
DigDeliveryOptionComponent,
|
||||
],
|
||||
})
|
||||
export class PurchasingOptionsModalModule {}
|
||||
@@ -1,498 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { CrmCustomerService } from '@domain/crm';
|
||||
import { ComponentStore, tapResponse } from '@ngrx/component-store';
|
||||
import { ItemDTO } from '@swagger/cat';
|
||||
import { AvailabilityDTO, BranchDTO, OLAAvailabilityDTO, ShoppingCartItemDTO, VATType } from '@swagger/checkout';
|
||||
import { isBoolean, isNullOrUndefined, isString } from '@utils/common';
|
||||
import { NEVER, Observable } from 'rxjs';
|
||||
import { delay, filter, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||
|
||||
export type PurchasingOptions = 'take-away' | 'pick-up' | 'delivery' | 'dig-delivery' | 'b2b-delivery' | 'download';
|
||||
|
||||
interface PurchasingOptionsModalState {
|
||||
item?: ItemDTO;
|
||||
shoppingCartItem?: ShoppingCartItemDTO;
|
||||
option?: PurchasingOptions;
|
||||
defaultBranch?: BranchDTO;
|
||||
branch?: BranchDTO;
|
||||
processId?: number;
|
||||
fetchingAvailability: boolean;
|
||||
availableOptions: PurchasingOptions[];
|
||||
availableBranches: BranchDTO[];
|
||||
quantity: number;
|
||||
maxQuantityError: boolean;
|
||||
checkingCanAdd: boolean;
|
||||
canAdd: boolean;
|
||||
canAddError?: string;
|
||||
canUpgrade: boolean;
|
||||
availabilities: { [key: string]: AvailabilityDTO };
|
||||
customPrice?: number;
|
||||
customVat?: VATType;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PurchasingOptionsModalStore extends ComponentStore<PurchasingOptionsModalState> {
|
||||
readonly selectItem = this.select((s) => s.item);
|
||||
|
||||
readonly selectShoppingCartItem = this.select((s) => s.shoppingCartItem);
|
||||
|
||||
readonly selectOption = this.select((s) => s.option);
|
||||
|
||||
readonly selectOrderType = this.select(this.selectOption, (option) => {
|
||||
switch (option) {
|
||||
case 'take-away':
|
||||
return 'Rücklage';
|
||||
case 'pick-up':
|
||||
return 'Abholung';
|
||||
case 'delivery':
|
||||
return 'Versand';
|
||||
case 'b2b-delivery':
|
||||
return 'B2B-Versand';
|
||||
case 'dig-delivery':
|
||||
return 'DIG-Versand';
|
||||
case 'download':
|
||||
return 'Download';
|
||||
}
|
||||
});
|
||||
|
||||
readonly selectHasOption = this.select((s) => !!s.option);
|
||||
|
||||
readonly selectBranch = this.select((s) => {
|
||||
return s.branch || s.defaultBranch;
|
||||
});
|
||||
|
||||
readonly selectQuantity = this.select((s) => s.quantity);
|
||||
|
||||
readonly selectCustomPrice = this.select((s) => s.customPrice);
|
||||
|
||||
readonly selectCustomVat = this.select((s) => s.customVat);
|
||||
|
||||
readonly selectAvailabilities = this.select((s) => s.availabilities);
|
||||
|
||||
readonly selectAvailability = this.select((s) => s.availabilities[s.option]);
|
||||
|
||||
readonly selectAvailabilityIsValid = this.select(this.selectAvailability, (availability) =>
|
||||
this.availabilityService.isAvailable({ availability })
|
||||
);
|
||||
|
||||
readonly selectAvailableOptions = this.select((s) => s.availableOptions);
|
||||
|
||||
readonly selectAvailableBranches = this.select((s) => s.availableBranches);
|
||||
|
||||
readonly selectProcessId = this.select((s) => s.processId);
|
||||
|
||||
readonly selectCheckingCanAdd = this.select((s) => s.checkingCanAdd);
|
||||
|
||||
readonly selectCanAdd = this.select(
|
||||
this.selectAvailability,
|
||||
this.select((s) => s.canAdd),
|
||||
(availability, canAdd) => canAdd && this.availabilityService.isAvailable({ availability })
|
||||
);
|
||||
|
||||
readonly selectCanAddError = this.select((s) => s.canAddError);
|
||||
|
||||
readonly selectFetchingAvailability = this.select((s) => s.fetchingAvailability);
|
||||
|
||||
readonly selectMaxQuantityError = this.select((s) => s.maxQuantityError);
|
||||
|
||||
readonly selectQuantityError = this.select(
|
||||
this.selectQuantity,
|
||||
this.selectOption,
|
||||
this.selectAvailability,
|
||||
this.selectFetchingAvailability,
|
||||
this.selectMaxQuantityError,
|
||||
(quantity, option, availability, fetching, maxQuantityError) => {
|
||||
if (!fetching) {
|
||||
if (maxQuantityError) {
|
||||
return `Achtung, Maximal 999 Exemplare bestellbar.`;
|
||||
}
|
||||
|
||||
if (availability?.inStock < quantity) {
|
||||
if (option === 'pick-up') {
|
||||
return `${availability?.inStock} Exemplare sofort lieferbar.`;
|
||||
}
|
||||
if (option === 'take-away') {
|
||||
return `${availability?.inStock} Exemplare sofort lieferbar.`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.availabilityService.isAvailable({ availability })) {
|
||||
return availability?.sscText;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
);
|
||||
|
||||
readonly selectOlaAvailability = this.select(
|
||||
(s): OLAAvailabilityDTO =>
|
||||
this.availabilityService.mapToOlaAvailability({
|
||||
availability: s.availabilities[s.option],
|
||||
item: s.item,
|
||||
quantity: s.quantity,
|
||||
})
|
||||
);
|
||||
|
||||
readonly selectCanUpgrade = this.select((s) => s.canUpgrade);
|
||||
|
||||
readonly selectCanContinueShopping = this.select(
|
||||
this.selectFetchingAvailability,
|
||||
this.selectCanAdd,
|
||||
this.selectOption,
|
||||
this.selectQuantityError,
|
||||
this.selectQuantity,
|
||||
this.selectCanUpgrade,
|
||||
(fetching, canAdd, option, quantityError, quantity, canUpgrade) => {
|
||||
let hasError = !!quantityError;
|
||||
|
||||
if (option === 'pick-up' && quantity <= 999) {
|
||||
hasError = false;
|
||||
}
|
||||
return !fetching && (canAdd || canUpgrade);
|
||||
}
|
||||
).pipe(delay(1));
|
||||
|
||||
readonly selectCanContinueShoppingIsLoading = this.select(this.selectFetchingAvailability, this.selectCanAdd, (fetching, canAdd) => {
|
||||
return fetching;
|
||||
});
|
||||
|
||||
constructor(
|
||||
private checkoutService: DomainCheckoutService,
|
||||
private availabilityService: DomainAvailabilityService,
|
||||
private customerService: CrmCustomerService,
|
||||
private applicationService: ApplicationService
|
||||
) {
|
||||
super({
|
||||
availableBranches: [],
|
||||
availableOptions: [],
|
||||
quantity: 1,
|
||||
canAdd: false,
|
||||
canUpgrade: false,
|
||||
availabilities: {},
|
||||
checkingCanAdd: false,
|
||||
fetchingAvailability: false,
|
||||
maxQuantityError: false,
|
||||
});
|
||||
|
||||
this.loadDefaultBranch();
|
||||
}
|
||||
|
||||
readonly setItem = this.updater((state, item: ItemDTO) => {
|
||||
this.loadAvailability();
|
||||
return {
|
||||
...state,
|
||||
item,
|
||||
availability: undefined,
|
||||
canAdd: false,
|
||||
canAddError: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
readonly setShoppingCartItem = this.updater((state, shoppingCartItem: ShoppingCartItemDTO) => {
|
||||
this.loadAvailability();
|
||||
return {
|
||||
...state,
|
||||
shoppingCartItem,
|
||||
};
|
||||
});
|
||||
|
||||
readonly setOption = this.updater((state, option: PurchasingOptions) => {
|
||||
this.loadAvailability();
|
||||
return {
|
||||
...state,
|
||||
option,
|
||||
availability: undefined,
|
||||
canAdd: false,
|
||||
canAddError: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
readonly setBranch = this.updater((state, branch: BranchDTO) => {
|
||||
this.loadAvailability();
|
||||
return {
|
||||
...state,
|
||||
branch,
|
||||
availability: undefined,
|
||||
canAdd: false,
|
||||
canAddError: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
readonly setBranchId = this.updater((state, branchId: number) => {
|
||||
const branch = state.availableBranches.find((branch) => branch.id === branchId);
|
||||
|
||||
this.loadAvailability();
|
||||
return {
|
||||
...state,
|
||||
branchId,
|
||||
branch,
|
||||
};
|
||||
});
|
||||
|
||||
readonly setAvailability = this.updater(
|
||||
(state, { availability, option, item }: { availability: AvailabilityDTO; option: PurchasingOptions; item: ItemDTO }) => {
|
||||
this.checkCanAdd();
|
||||
let updatedAvailability = availability;
|
||||
if ((option && option === 'delivery') || option === 'dig-delivery') {
|
||||
const catalogPrice = item?.catalogAvailability?.price?.value?.value;
|
||||
const availabilityPrice = availability?.price?.value?.value;
|
||||
const updatedPrice = catalogPrice <= availabilityPrice ? catalogPrice : availabilityPrice;
|
||||
updatedAvailability = {
|
||||
...availability,
|
||||
price: {
|
||||
...availability.price,
|
||||
value: {
|
||||
...availability.price.value,
|
||||
value: updatedPrice,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
availabilities: {
|
||||
...state.availabilities,
|
||||
[option]: updatedAvailability,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
readonly setQuantity = this.updater((state, quantity: number = 1) => {
|
||||
let qty = quantity;
|
||||
if (quantity > 999) {
|
||||
qty = 999;
|
||||
this.patchState({ maxQuantityError: true });
|
||||
} else {
|
||||
this.patchState({ maxQuantityError: false });
|
||||
}
|
||||
|
||||
this.loadAvailability();
|
||||
return {
|
||||
...state,
|
||||
quantity: qty,
|
||||
canAdd: false,
|
||||
canAddError: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
readonly setCustomPrice = this.updater((state, customPrice: number) => {
|
||||
return {
|
||||
...state,
|
||||
customPrice,
|
||||
};
|
||||
});
|
||||
|
||||
readonly setCustomVat = this.updater((state, customVat: VATType) => {
|
||||
return {
|
||||
...state,
|
||||
customVat,
|
||||
};
|
||||
});
|
||||
|
||||
readonly setAvailableOptions = this.updater((state, availableOptions: PurchasingOptions[]) => {
|
||||
let option = state.option;
|
||||
if (availableOptions?.length === 1 && availableOptions[0] === 'download') {
|
||||
option = availableOptions[0];
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
availableOptions,
|
||||
option,
|
||||
};
|
||||
});
|
||||
|
||||
readonly setAvailableBranches = this.updater((state, availableBranches: BranchDTO[]) => {
|
||||
const branch = state.branch || state.defaultBranch;
|
||||
return {
|
||||
...state,
|
||||
availableBranches,
|
||||
branch,
|
||||
};
|
||||
});
|
||||
|
||||
readonly setProcessId = this.updater((state, processId: number) => ({
|
||||
...state,
|
||||
processId,
|
||||
}));
|
||||
|
||||
readonly setCanAdd = this.updater((state, canAddItem: true | string) => {
|
||||
let canAdd = isBoolean(canAddItem) ? Boolean(canAddItem) : false;
|
||||
if (!canAdd) {
|
||||
this.checkCanUpgrade();
|
||||
}
|
||||
return { ...state, canAdd, canAddError: isString(canAddItem) ? String(canAddItem) : undefined };
|
||||
});
|
||||
|
||||
readonly setAvailabilities = this.updater((state, availabilities: { [key: string]: AvailabilityDTO }) => {
|
||||
this.checkCanAdd();
|
||||
return {
|
||||
...state,
|
||||
availabilities: {
|
||||
...state.availabilities,
|
||||
...availabilities,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
loadBranches = this.effect((branchId$: Observable<number>) =>
|
||||
branchId$.pipe(
|
||||
switchMap((branchId) =>
|
||||
this.checkoutService.getBranches().pipe(
|
||||
tapResponse(
|
||||
(branches: BranchDTO[]) => {
|
||||
this.setAvailableBranches(branches);
|
||||
this.setBranchId(branchId);
|
||||
},
|
||||
() => this.setAvailableBranches([])
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
loadAvailability = this.effect(($) =>
|
||||
$.pipe(
|
||||
delay(10),
|
||||
withLatestFrom(this.selectItem, this.selectQuantity, this.selectOption, this.selectBranch),
|
||||
switchMap(([_, item, quantity, option, branch]) => {
|
||||
let availability$: Observable<AvailabilityDTO> = NEVER;
|
||||
|
||||
if (!isNullOrUndefined(item) && quantity > 0 && isString(option)) {
|
||||
this.patchState({ fetchingAvailability: true });
|
||||
switch (option) {
|
||||
case 'take-away':
|
||||
availability$ = this.availabilityService.getTakeAwayAvailabilityByBranch({
|
||||
itemId: item.id,
|
||||
price: item.catalogAvailability.price,
|
||||
quantity,
|
||||
branch,
|
||||
});
|
||||
break;
|
||||
case 'pick-up':
|
||||
if (!isNullOrUndefined(branch)) {
|
||||
availability$ = this.availabilityService
|
||||
.getPickUpAvailability({
|
||||
item: { itemId: item.id, ean: item.product.ean, price: item.catalogAvailability.price },
|
||||
quantity,
|
||||
branch,
|
||||
})
|
||||
.pipe(
|
||||
map((av) => {
|
||||
if (av?.length > 0) {
|
||||
if (av[1].availableFor) {
|
||||
if ((av[1].availableFor & 2) === 2) {
|
||||
return av[0];
|
||||
} else {
|
||||
undefined;
|
||||
}
|
||||
} else {
|
||||
return av[0];
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'delivery':
|
||||
availability$ = this.availabilityService.getDeliveryAvailability({
|
||||
item: { itemId: item.id, ean: item.product.ean, price: item.catalogAvailability.price },
|
||||
quantity,
|
||||
});
|
||||
break;
|
||||
case 'dig-delivery':
|
||||
availability$ = this.availabilityService.getDigDeliveryAvailability({
|
||||
item: { itemId: item.id, ean: item.product.ean, price: item.catalogAvailability.price },
|
||||
quantity,
|
||||
});
|
||||
break;
|
||||
case 'b2b-delivery':
|
||||
if (!isNullOrUndefined(branch)) {
|
||||
availability$ = this.availabilityService.getB2bDeliveryAvailability({
|
||||
item: { itemId: item.id, ean: item.product.ean, price: item.catalogAvailability.price },
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'download':
|
||||
availability$ = this.availabilityService.getDownloadAvailability({
|
||||
item: { itemId: item.id, ean: item.product.ean, price: item.catalogAvailability.price },
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
return availability$.pipe(
|
||||
tapResponse(
|
||||
(availability) => {
|
||||
this.setAvailability({ option, availability, item });
|
||||
this.patchState({ fetchingAvailability: false });
|
||||
},
|
||||
() => {
|
||||
this.setAvailability(null);
|
||||
this.patchState({ fetchingAvailability: false });
|
||||
}
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
checkCanAdd = this.effect(($) =>
|
||||
$.pipe(
|
||||
delay(10),
|
||||
withLatestFrom(this.selectOlaAvailability, this.selectProcessId, this.selectOrderType),
|
||||
switchMap(([_, availability, processId, orderType]) => {
|
||||
this.patchState({ checkingCanAdd: true });
|
||||
return this.checkoutService.canAddItems({ processId, payload: [{ availabilities: [availability] }], orderType }).pipe(
|
||||
tapResponse(
|
||||
(response: any) => {
|
||||
this.setCanAdd(response?.find((_) => true)?.status === 0 ? true : response?.find((_) => true)?.message);
|
||||
},
|
||||
(error: Error) => this.setCanAdd(error?.message)
|
||||
),
|
||||
tap((_) => this.patchState({ checkingCanAdd: false }))
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
checkCanUpgrade = this.effect(($) =>
|
||||
$.pipe(
|
||||
withLatestFrom(this.applicationService.activatedProcessId$),
|
||||
switchMap(([_, processId]) => this.checkoutService.getBuyer({ processId })),
|
||||
map((buyer) => buyer?.source),
|
||||
filter((customerId) => !isNaN(customerId)),
|
||||
switchMap((customerId) =>
|
||||
this.customerService.canUpgrade(customerId).pipe(
|
||||
tapResponse(
|
||||
(response) => {
|
||||
let canUpgrade = response.options?.values?.some((u) => u.value === 'webshop');
|
||||
this.patchState({ canUpgrade });
|
||||
},
|
||||
(error) => {
|
||||
this.patchState({ canUpgrade: false });
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
readonly loadDefaultBranch = this.effect(($) =>
|
||||
$.pipe(
|
||||
switchMap((_) =>
|
||||
this.availabilityService.getDefaultBranch().pipe(
|
||||
tapResponse(
|
||||
(defaultBranch) => this.patchState({ defaultBranch }),
|
||||
(err) => {}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './take-away-option.component';
|
||||
// end:ng42.barrel
|
||||
@@ -1,18 +0,0 @@
|
||||
<ng-container *ngIf="item$ | async; let item">
|
||||
<ng-container *ngIf="availability$ | async; let availability">
|
||||
<div class="option-icon">
|
||||
<ui-icon size="50px" icon="shopping_bag"></ui-icon>
|
||||
</div>
|
||||
<h4>Rücklage / Filialentnahme</h4>
|
||||
<p>
|
||||
Möchten Sie den Artikel zurücklegen lassen oder sofort mitnehmen?
|
||||
</p>
|
||||
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
|
||||
<div class="grow"></div>
|
||||
<div>
|
||||
<button [disabled]="availability.price?.value?.value < 0" type="button" class="select-option" (click)="select()">
|
||||
Auswählen
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -1,3 +0,0 @@
|
||||
h4 {
|
||||
@apply font-bold;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-take-away-option',
|
||||
templateUrl: 'take-away-option.component.html',
|
||||
styleUrls: ['../option.scss', 'take-away-option.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TakeAwayOptionComponent {
|
||||
readonly item$ = this._purchasingOptionsModalStore.selectItem;
|
||||
|
||||
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['take-away']));
|
||||
|
||||
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
|
||||
map(([availability, item]) => this._availabilityService.getPriceForAvailability('take-away', item.catalogAvailability, availability))
|
||||
);
|
||||
|
||||
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
|
||||
|
||||
select() {
|
||||
this._purchasingOptionsModalStore.setOption('take-away');
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { PurchasingOptionsListModalModule } from './modals/purchasing-options-list-modal';
|
||||
|
||||
import { PurchasingOptionsModalModule } from './modals/purchasing-options-modal';
|
||||
|
||||
@NgModule({
|
||||
imports: [PurchasingOptionsModalModule, PurchasingOptionsListModalModule],
|
||||
exports: [PurchasingOptionsModalModule, PurchasingOptionsListModalModule],
|
||||
})
|
||||
export class PageCheckoutModalsModule {}
|
||||
@@ -4,14 +4,12 @@ import { ShellBreadcrumbModule } from '@shell/breadcrumb';
|
||||
import { CheckoutDummyModule } from './checkout-dummy/checkout-dummy.module';
|
||||
import { CheckoutReviewModule } from './checkout-review/checkout-review.module';
|
||||
import { CheckoutSummaryModule } from './checkout-summary/checkout-summary.module';
|
||||
import { PageCheckoutModalsModule } from './page-checkout-modals.module';
|
||||
import { PageCheckoutRoutingModule } from './page-checkout-routing.module';
|
||||
import { PageCheckoutComponent } from './page-checkout.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
PageCheckoutModalsModule,
|
||||
CheckoutSummaryModule,
|
||||
PageCheckoutRoutingModule,
|
||||
CheckoutReviewModule,
|
||||
@@ -19,6 +17,6 @@ import { PageCheckoutComponent } from './page-checkout.component';
|
||||
ShellBreadcrumbModule,
|
||||
],
|
||||
declarations: [PageCheckoutComponent],
|
||||
exports: [PageCheckoutModalsModule],
|
||||
exports: [],
|
||||
})
|
||||
export class PageCheckoutModule {}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
::ng-deep shared-branch-selector ui-autocomplete .ui-autocomplete-output-wrapper {
|
||||
@apply overflow-hidden overflow-y-auto max-w-content rounded-b-md;
|
||||
max-height: 500px;
|
||||
max-height: 350px;
|
||||
width: 100%;
|
||||
left: unset;
|
||||
box-shadow: 0px 14px 14px rgba(206, 212, 219, 0.2);
|
||||
|
||||
@@ -20,7 +20,7 @@ import { UiAutocompleteComponent, UiAutocompleteModule } from '@ui/autocomplete'
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { isNaN } from 'lodash';
|
||||
import { combineLatest, Subject } from 'rxjs';
|
||||
import { asapScheduler, combineLatest, Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { BranchSelectorStore } from './branch-selector.store';
|
||||
|
||||
@@ -79,6 +79,13 @@ export class BranchSelectorComponent implements OnInit, OnDestroy, AfterViewInit
|
||||
onChange = (value: BranchDTO) => {};
|
||||
onTouched = () => {};
|
||||
|
||||
@ViewChild('branchInput')
|
||||
branchInput: ElementRef<HTMLInputElement>;
|
||||
|
||||
get isOpen() {
|
||||
return this.autocompleteComponent?.open ?? false;
|
||||
}
|
||||
|
||||
constructor(public store: BranchSelectorStore, private _elementRef: ElementRef) {}
|
||||
|
||||
writeValue(obj: any): void {
|
||||
@@ -188,6 +195,13 @@ export class BranchSelectorComponent implements OnInit, OnDestroy, AfterViewInit
|
||||
this.complete.next('');
|
||||
}
|
||||
|
||||
focus() {
|
||||
asapScheduler.schedule(() => {
|
||||
this.branchInput?.nativeElement?.focus();
|
||||
this.openComplete();
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('focusout', ['$event'])
|
||||
closeAutocomplete(event?: FocusEvent) {
|
||||
// Soll bei Klick auf den Branch-Selector und auf die Scrollbar das Autocomplete nicht schließen
|
||||
|
||||
6
apps/shared/components/input-control/ng-package.json
Normal file
6
apps/shared/components/input-control/ng-package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, TemplateRef, ViewChild } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-input-control-error',
|
||||
template: `<ng-template #template>
|
||||
<div class="shared-input-control-error">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</ng-template>`,
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
})
|
||||
export class ErrorComponent {
|
||||
@Input() error: string;
|
||||
|
||||
@ViewChild('template', { static: true }) tempalteRef: TemplateRef<any>;
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, TemplateRef, ViewChild } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-input-control-indicator',
|
||||
template: `<ng-template #template>
|
||||
<div class="shared-input-control-indicator">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</ng-template>`,
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
})
|
||||
export class IndicatorComponent {
|
||||
@Input()
|
||||
invalid: boolean | undefined;
|
||||
|
||||
@Input()
|
||||
valid: boolean | undefined;
|
||||
|
||||
@Input()
|
||||
dirty: boolean | undefined;
|
||||
|
||||
@Input()
|
||||
disabled: boolean | undefined;
|
||||
|
||||
@Input()
|
||||
enabled: boolean | undefined;
|
||||
|
||||
@Input()
|
||||
pristine: boolean | undefined;
|
||||
|
||||
@Input()
|
||||
pending: boolean | undefined;
|
||||
|
||||
@ViewChild('template', { static: true }) tempalteRef: TemplateRef<any>;
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
.shared-input-control {
|
||||
@apply relative leading-[21px] text-base font-bold;
|
||||
}
|
||||
|
||||
.shared-input-control:has(input.ng-invalid.ng-dirty) {
|
||||
@apply text-[#F70400];
|
||||
}
|
||||
|
||||
.shared-input-control-wrapper {
|
||||
@apply flex flex-row items-center grow border border-solid border-[#AEB7C1] rounded-[5px] p-4;
|
||||
}
|
||||
|
||||
.shared-input-control-wrapper:has(input.ng-invalid.ng-dirty) {
|
||||
@apply border-[#F70400] text-[#F70400];
|
||||
}
|
||||
|
||||
.shared-input-control-input {
|
||||
@apply outline-none grow truncate;
|
||||
}
|
||||
|
||||
.shared-input-control-input.ng-invalid.ng-dirty::placeholder {
|
||||
@apply text-[#F70400];
|
||||
}
|
||||
|
||||
.shared-input-control-indicator {
|
||||
@apply absolute -left-2 top-4 -translate-x-full;
|
||||
}
|
||||
|
||||
.shared-input-control-prefix,
|
||||
.shared-input-control-suffix {
|
||||
@apply inline-block grow-0;
|
||||
}
|
||||
|
||||
.shared-input-control-prefix {
|
||||
@apply -ml-2 mr-2;
|
||||
}
|
||||
|
||||
.shared-input-control-suffix {
|
||||
@apply -mr-2 ml-2;
|
||||
}
|
||||
|
||||
.shared-input-control-error {
|
||||
@apply text-left mt-[2px];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<ng-container sharedInputControlOutlet #indicatorOutlet> </ng-container>
|
||||
<div class="shared-input-control-wrapper">
|
||||
<ng-content select="shared-input-control-prefix"></ng-content>
|
||||
|
||||
<ng-content select="[sharedInputControlInput]"></ng-content>
|
||||
|
||||
<ng-content select="shared-input-control-suffix"></ng-content>
|
||||
</div>
|
||||
|
||||
<ng-container sharedInputControlOutlet #errorOutlet> </ng-container>
|
||||
@@ -0,0 +1,119 @@
|
||||
import { OnDestroy, TemplateRef } from '@angular/core';
|
||||
import { QueryList } from '@angular/core';
|
||||
import { ContentChildren } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, ViewEncapsulation, AfterContentInit, ContentChild, ViewChild } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ErrorComponent } from './error.component';
|
||||
import { IndicatorComponent } from './indicator.component';
|
||||
import { InputDirective } from './input.directive';
|
||||
import { OutletDirective } from './outlet.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-input-control',
|
||||
templateUrl: 'input-control.component.html',
|
||||
styleUrls: ['input-control.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'shared-input-control' },
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class InputControlComponent implements AfterContentInit, OnDestroy {
|
||||
@ContentChild(InputDirective, { static: false, read: InputDirective })
|
||||
inputDirective: InputDirective;
|
||||
|
||||
@ViewChild('errorOutlet', { read: OutletDirective, static: true })
|
||||
errorOutlet: OutletDirective;
|
||||
|
||||
@ViewChild('indicatorOutlet', { read: OutletDirective, static: true })
|
||||
indicatorOutlet: OutletDirective;
|
||||
|
||||
@ContentChildren(ErrorComponent, { read: ErrorComponent })
|
||||
errorTemplates: QueryList<ErrorComponent>;
|
||||
|
||||
@ContentChildren(IndicatorComponent, { read: IndicatorComponent })
|
||||
indicatorTemplates: QueryList<IndicatorComponent>;
|
||||
|
||||
currentError: ErrorComponent;
|
||||
|
||||
currentIndicator: IndicatorComponent;
|
||||
|
||||
private _subscriptions = new Subscription();
|
||||
|
||||
constructor() {}
|
||||
|
||||
renderError(): void {
|
||||
const errors = this.inputDirective?.control.errors;
|
||||
|
||||
if (!errors || this.inputDirective?.control.pristine) {
|
||||
this.errorOutlet.viewContainerRef.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const errorTemplate = this.errorTemplates.find((x) => x.error in errors);
|
||||
|
||||
const tempalteChanged = errorTemplate !== this.currentError;
|
||||
|
||||
if (tempalteChanged) {
|
||||
this.errorOutlet.viewContainerRef.clear();
|
||||
|
||||
if (errorTemplate) {
|
||||
this.errorOutlet.viewContainerRef.createEmbeddedView(errorTemplate.tempalteRef);
|
||||
}
|
||||
}
|
||||
|
||||
this.currentError = errorTemplate;
|
||||
}
|
||||
|
||||
renderIndicator(): void {
|
||||
const { invalid, valid, dirty, disabled, enabled, pristine, pending } = this.inputDirective?.control;
|
||||
|
||||
const indicatorTemplate = this.indicatorTemplates.find((i) => {
|
||||
// find the first indicator that matches the current state of the control
|
||||
// id state is undefined then it will not be checked
|
||||
return (
|
||||
(i.invalid === invalid || i.invalid === undefined) &&
|
||||
(i.valid === valid || i.valid === undefined) &&
|
||||
(i.dirty === dirty || i.dirty === undefined) &&
|
||||
(i.disabled === disabled || i.disabled === undefined) &&
|
||||
(i.enabled === enabled || i.enabled === undefined) &&
|
||||
(i.pristine === pristine || i.pristine === undefined) &&
|
||||
(i.pending === pending || i.pending === undefined)
|
||||
);
|
||||
});
|
||||
|
||||
const tempalteChanged = indicatorTemplate !== this.currentIndicator;
|
||||
|
||||
if (tempalteChanged) {
|
||||
this.indicatorOutlet.viewContainerRef.clear();
|
||||
|
||||
if (indicatorTemplate) {
|
||||
this.indicatorOutlet.viewContainerRef.createEmbeddedView(indicatorTemplate.tempalteRef);
|
||||
}
|
||||
}
|
||||
|
||||
this.currentIndicator = indicatorTemplate;
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
if (!this.inputDirective) {
|
||||
console.error(new Error(`No input[sharedInput] found in \`<shared-input-control>\` component`));
|
||||
}
|
||||
|
||||
const statusChangesSub = this.inputDirective.control.statusChanges.subscribe(() => {
|
||||
this.renderError();
|
||||
this.renderIndicator();
|
||||
});
|
||||
const tempalteChangesSub = this.errorTemplates.changes.subscribe(() => {
|
||||
this.renderError();
|
||||
this.renderIndicator();
|
||||
});
|
||||
this.renderError();
|
||||
this.renderIndicator();
|
||||
|
||||
this._subscriptions.add(statusChangesSub);
|
||||
this._subscriptions.add(tempalteChangesSub);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._subscriptions.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { InputControlComponent } from './input-control.component';
|
||||
import { InputDirective } from './input.directive';
|
||||
import { PrefixDirective } from './prefix.directive';
|
||||
import { SuffixDirective } from './suffix.directive';
|
||||
import { ErrorComponent } from './error.component';
|
||||
import { OutletDirective } from './outlet.directive';
|
||||
import { IndicatorComponent } from './indicator.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
exports: [InputControlComponent, InputDirective, PrefixDirective, SuffixDirective, ErrorComponent, IndicatorComponent],
|
||||
declarations: [
|
||||
InputControlComponent,
|
||||
InputDirective,
|
||||
PrefixDirective,
|
||||
SuffixDirective,
|
||||
ErrorComponent,
|
||||
OutletDirective,
|
||||
IndicatorComponent,
|
||||
],
|
||||
})
|
||||
export class InputControlModule {}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { OnInit } from '@angular/core';
|
||||
import { OnDestroy } from '@angular/core';
|
||||
import { Optional, Directive, Self } from '@angular/core';
|
||||
import { AbstractFormGroupDirective, NgControl, ValidationErrors } from '@angular/forms';
|
||||
import { SharedInputControlStatus } from './types';
|
||||
|
||||
@Directive({
|
||||
selector: 'input[sharedInputControlInput]:not([type=radio]):not([type=checkbox])',
|
||||
host: { class: 'shared-input-control-input' },
|
||||
})
|
||||
export class InputDirective implements OnInit, OnDestroy {
|
||||
get control(): NgControl | AbstractFormGroupDirective | null {
|
||||
return this._ngControl;
|
||||
}
|
||||
|
||||
get errors(): ValidationErrors | null {
|
||||
return this.control?.errors;
|
||||
}
|
||||
|
||||
get status(): SharedInputControlStatus {
|
||||
return this.control?.status as SharedInputControlStatus;
|
||||
}
|
||||
|
||||
constructor(@Self() @Optional() private _ngControl: NgControl) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
|
||||
private _getControlStatus({ status, dirty, touched }: { status: string; dirty: boolean; touched: boolean }): SharedInputControlStatus[] {
|
||||
const result: SharedInputControlStatus[] = [];
|
||||
|
||||
if (status === 'VALID') {
|
||||
result.push('valid');
|
||||
} else if (status === 'INVALID') {
|
||||
result.push('invalid');
|
||||
} else if (status === 'DISABLED') {
|
||||
result.push('disabled');
|
||||
} else if (status === 'PENDING') {
|
||||
result.push('pending');
|
||||
}
|
||||
|
||||
if (dirty) {
|
||||
result.push('dirty');
|
||||
} else {
|
||||
result.push('pristine');
|
||||
}
|
||||
|
||||
if (touched) {
|
||||
result.push('touched');
|
||||
} else {
|
||||
result.push('untouched');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Directive, ViewContainerRef } from '@angular/core';
|
||||
|
||||
@Directive({ selector: '[sharedInputControlOutlet]' })
|
||||
export class OutletDirective {
|
||||
constructor(public viewContainerRef: ViewContainerRef) {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Directive } from '@angular/core';
|
||||
|
||||
@Directive({ selector: 'sharedinput-control-prefix', host: { class: 'shared-input-control-prefix' } })
|
||||
export class PrefixDirective {
|
||||
constructor() {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Directive } from '@angular/core';
|
||||
|
||||
@Directive({ selector: 'shared-input-control-suffix', host: { class: 'shared-input-control-suffix' } })
|
||||
export class SuffixDirective {
|
||||
constructor() {}
|
||||
}
|
||||
1
apps/shared/components/input-control/src/lib/types.ts
Normal file
1
apps/shared/components/input-control/src/lib/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type SharedInputControlStatus = 'valid' | 'invalid' | 'disabled' | 'pending' | 'dirty' | 'pristine' | 'touched' | 'untouched';
|
||||
3
apps/shared/components/input-control/src/public-api.ts
Normal file
3
apps/shared/components/input-control/src/public-api.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './lib/input-control.component';
|
||||
export * from './lib/input.directive';
|
||||
export * from './lib/input-control.module';
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ItemType, PriceDTO, PriceValueDTO, VATValueDTO } from '@swagger/checkout';
|
||||
import { OrderType, PurchaseOption } from './store';
|
||||
|
||||
export const PURCHASE_OPTIONS: PurchaseOption[] = ['in-store', 'pickup', 'delivery', 'dig-delivery', 'b2b-delivery', 'download'];
|
||||
|
||||
export const DELIVERY_PURCHASE_OPTIONS: PurchaseOption[] = ['delivery', 'dig-delivery', 'b2b-delivery'];
|
||||
|
||||
export const PURCHASE_OPTION_TO_ORDER_TYPE: { [purchaseOption: string]: OrderType } = {
|
||||
'in-store': 'Rücklage',
|
||||
pickup: 'Abholung',
|
||||
delivery: 'Versand',
|
||||
'dig-delivery': 'Versand',
|
||||
'b2b-delivery': 'Versand',
|
||||
};
|
||||
|
||||
export const GIFT_CARD_TYPE = 66560 as ItemType;
|
||||
|
||||
export const DEFAULT_PRICE_DTO: PriceDTO = { value: { value: undefined }, vat: { vatType: 0 } };
|
||||
|
||||
export const DEFAULT_PRICE_VALUE: PriceValueDTO = { value: 0, currency: 'EUR' };
|
||||
|
||||
export const DEFAULT_VAT_VALUE: VATValueDTO = { value: 0 };
|
||||
19
apps/shared/modals/purchase-options-modal/src/lib/helpers.ts
Normal file
19
apps/shared/modals/purchase-options-modal/src/lib/helpers.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ItemDTO } from '@swagger/cat';
|
||||
import { ShoppingCartItemDTO } from '@swagger/checkout';
|
||||
import { ActionType } from './types';
|
||||
|
||||
export function isItemDTO(item: any, type: ActionType): item is ItemDTO {
|
||||
return type === 'add';
|
||||
}
|
||||
|
||||
export function isItemDTOArray(items: any, type: ActionType): items is ItemDTO[] {
|
||||
return type === 'add';
|
||||
}
|
||||
|
||||
export function isShoppingCartItemDTO(item: any, type: ActionType): item is ShoppingCartItemDTO {
|
||||
return type === 'update';
|
||||
}
|
||||
|
||||
export function isShoppingCartItemDTOArray(items: any, type: ActionType): items is ShoppingCartItemDTO[] {
|
||||
return type === 'update';
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './purchase-options-list-header.component';
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply mt-4 mb-2 flex flex-row justify-end;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="flex flex-col text-right" [class.hidden]="hideHeader$ | async">
|
||||
<button type="button" class="font-bold text-[#0556B4]" *ngIf="selectButton$ | async" (click)="selectAll()">Alle auswählen</button>
|
||||
<button type="button" class="font-bold text-[#0556B4]" *ngIf="unselectButton$ | async" (click)="unselectAll()">Alle abwählen</button>
|
||||
<span class="mt-2">{{ selectedItemsCount$ | async }} von {{ itemsCount$ | async }} Artikel</span>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PurchaseOptionsStore } from '../store';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-purchase-options-list-header',
|
||||
templateUrl: 'purchase-options-list-header.component.html',
|
||||
styleUrls: ['purchase-options-list-header.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
})
|
||||
export class PurchaseOptionsListHeaderComponent {
|
||||
itemsCount$ = this.store.itemsForList$.pipe(map((items) => items.length));
|
||||
|
||||
selectedItemsCount$ = this.store.selectedItemIds$.pipe(map((ids) => ids.length));
|
||||
|
||||
unselectButton$ = combineLatest([this.itemsCount$, this.selectedItemsCount$]).pipe(
|
||||
map(([itemsCount, selectedItemsCount]) => itemsCount === selectedItemsCount)
|
||||
);
|
||||
|
||||
selectButton$ = this.unselectButton$.pipe(map((unselectButton) => !unselectButton));
|
||||
|
||||
hideHeader$ = this.store.items$.pipe(map((items) => items?.length === 1));
|
||||
|
||||
constructor(public store: PurchaseOptionsStore) {}
|
||||
|
||||
selectAll() {
|
||||
// this.store.setSelectedForSelectableItems(true);
|
||||
}
|
||||
|
||||
unselectAll() {
|
||||
// this._store.setSelectedForSelectableItems(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './purchase-options-list-item.component';
|
||||
@@ -0,0 +1,49 @@
|
||||
:host {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
.fancy-checkbox {
|
||||
@apply relative appearance-none w-8 h-8;
|
||||
}
|
||||
|
||||
.fancy-checkbox::before {
|
||||
@apply absolute;
|
||||
@apply block;
|
||||
@apply rounded-full;
|
||||
@apply bg-[#AEB7C1];
|
||||
@apply cursor-pointer;
|
||||
@apply transition-all;
|
||||
@apply duration-200;
|
||||
@apply ease-in-out;
|
||||
content: '';
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.fancy-checkbox:hover::before {
|
||||
@apply bg-[#778490];
|
||||
}
|
||||
|
||||
.fancy-checkbox:checked::before {
|
||||
@apply bg-[#596470];
|
||||
}
|
||||
|
||||
.fancy-checkbox::after {
|
||||
content: '';
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' viewBox='0 0 24 24' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' xml:space='preserve' xmlns:serif='http://www.serif.com/'%3E%3Cpath fill='white' d='M21.432,3.715C21.432,3.715 21.432,3.715 21.432,3.715C21.115,3.76 20.823,3.911 20.604,4.143C16.03,8.727 12.603,12.511 8.244,16.932C8.244,16.932 3.297,12.754 3.297,12.754C2.909,12.424 2.374,12.327 1.895,12.499C1.895,12.499 1.895,12.499 1.895,12.499C1.415,12.672 1.065,13.088 0.975,13.589C0.885,14.091 1.072,14.602 1.463,14.929L7.415,19.966C7.981,20.441 8.818,20.403 9.338,19.877C14.251,14.954 17.761,11.007 22.616,6.141C23.057,5.714 23.172,5.051 22.903,4.499C22.903,4.499 22.903,4.499 22.903,4.499C22.633,3.948 22.04,3.631 21.432,3.715Z'/%3E%3C/svg%3E%0A");
|
||||
@apply absolute;
|
||||
top: 0.4rem;
|
||||
left: 0.4rem;
|
||||
bottom: 0.4rem;
|
||||
right: 0.4rem;
|
||||
@apply cursor-pointer opacity-0;
|
||||
@apply transition-all;
|
||||
@apply duration-200;
|
||||
@apply ease-in-out;
|
||||
}
|
||||
|
||||
.fancy-checkbox:checked::after {
|
||||
@apply opacity-100;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<div class="flex flex-row">
|
||||
<div class="shared-purchase-options-list-item__thumbnail w-16 max-h-28">
|
||||
<img class="rounded shadow-card max-w-full max-h-full" [src]="product?.ean | productImage" [alt]="product?.name" />
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__product grow ml-4">
|
||||
<div class="shared-purchase-options-list-item__contributors font-bold">
|
||||
{{ product?.contributors }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__name font-bold h-12">
|
||||
{{ product?.name }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__format flex flex-row items-center">
|
||||
<ui-svg-icon [icon]="product?.format"></ui-svg-icon>
|
||||
<span class="ml-2 font-bold">{{ product?.formatDetail }}</span>
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__manufacturer-and-ean">
|
||||
{{ product?.manufacturer }}
|
||||
<span *ngIf="product?.manufacturer && product?.ean">|</span>
|
||||
{{ product?.ean }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__volume-and-publication-date">
|
||||
{{ product?.volume }}
|
||||
<span *ngIf="product?.volume && product?.publicationDate">|</span>
|
||||
{{ product?.publicationDate | date: 'dd. MMMM yyyy' }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__availabilities mt-6 grid grid-flow-col gap-4 justify-start">
|
||||
<div>Verfügbar als</div>
|
||||
<div *ngFor="let availability of availabilities$ | async" class="grid grid-flow-col gap-4 justify-start">
|
||||
<div
|
||||
[ngSwitch]="availability.purchaseOption"
|
||||
class="shared-purchase-options-list-item__availability grid grid-flow-col gap-2 items-center"
|
||||
[attr.data-option]="availability.purchaseOption"
|
||||
>
|
||||
<ng-container *ngSwitchCase="'delivery'">
|
||||
<ui-svg-icon icon="isa-truck" [size]="22"></ui-svg-icon>
|
||||
{{ availability.data.estimatedDelivery?.start | date: 'EE dd.MM.' }}
|
||||
-
|
||||
{{ availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.' }}
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'dig-delivery'">
|
||||
<ui-svg-icon icon="isa-truck" [size]="22"></ui-svg-icon>
|
||||
{{ availability.data.estimatedDelivery?.start | date: 'EE dd.MM.' }}
|
||||
-
|
||||
{{ availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.' }}
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'b2b-delivery'">
|
||||
<ui-svg-icon icon="isa-b2b-truck" [size]="24"></ui-svg-icon>
|
||||
{{ availability.data.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'pickup'">
|
||||
<ui-svg-icon icon="isa-box-out" [size]="18"></ui-svg-icon>
|
||||
{{ availability.data.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'in-store'">
|
||||
<ui-svg-icon icon="isa-shopping-bag" [size]="18"></ui-svg-icon>
|
||||
{{ availability.data.inStock }}x ab sofort
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'download'">
|
||||
<ui-svg-icon icon="isa-download" [size]="22"></ui-svg-icon>
|
||||
Download
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__price text-right ml-4 flex flex-col items-end">
|
||||
<div class="shared-purchase-options-list-item__price-value font-bold text-xl" *ngIf="!(canEditPrice$ | async)">
|
||||
{{ priceValue$ | async | currency: 'EUR':'code' }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__price-value font-bold text-xl" *ngIf="canEditPrice$ | async">
|
||||
<div class="relative flex flex-col">
|
||||
<shared-input-control>
|
||||
<shared-input-control-indicator>
|
||||
<ui-svg-icon *ngIf="priceFormControl?.invalid && priceFormControl?.dirty" icon="mat-info"></ui-svg-icon>
|
||||
</shared-input-control-indicator>
|
||||
<input sharedInputControlInput type="text" class="w-24" [formControl]="priceFormControl" placeholder="00,00" />
|
||||
<shared-input-control-suffix>EUR</shared-input-control-suffix>
|
||||
<shared-input-control-error error="required">Preis ist ungültig</shared-input-control-error>
|
||||
<shared-input-control-error error="pattern">Preis ist ungültig</shared-input-control-error>
|
||||
</shared-input-control>
|
||||
</div>
|
||||
</div>
|
||||
<ui-quantity-dropdown class="mt-2" [formControl]="quantityFormControl" [range]="maxSelectableQuantity$ | async"> </ui-quantity-dropdown>
|
||||
<input
|
||||
*ngIf="(canAddResult$ | async)?.canAdd"
|
||||
[class.hidden]="hideCheckbox$ | async"
|
||||
class="fancy-checkbox mt-7"
|
||||
[formControl]="selectedFormControl"
|
||||
type="checkbox"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="w-16"></div>
|
||||
<div class="grow shared-purchase-options-list-item__availabilities"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,173 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, ChangeDetectionStrategy, Input, OnInit, OnDestroy, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ProductImageModule } from '@cdn/product-image';
|
||||
import { InputControlModule } from '@shared/components/input-control';
|
||||
import { AvailabilityDTO } from '@swagger/checkout';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
|
||||
import { UiSpinnerModule } from '@ui/spinner';
|
||||
import { combineLatest, Observable, ReplaySubject, Subscription } from 'rxjs';
|
||||
import { map, startWith, switchMap } from 'rxjs/operators';
|
||||
import { Item, mapToItemData, PurchaseOptionsService, PurchaseOptionsStore } from '../store';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-purchase-options-list-item',
|
||||
templateUrl: 'purchase-options-list-item.component.html',
|
||||
styleUrls: ['purchase-options-list-item.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
UiQuantityDropdownModule,
|
||||
ProductImageModule,
|
||||
UiIconModule,
|
||||
UiSpinnerModule,
|
||||
ReactiveFormsModule,
|
||||
InputControlModule,
|
||||
FormsModule,
|
||||
],
|
||||
host: { class: 'shared-purchase-options-list-item' },
|
||||
})
|
||||
export class PurchaseOptionsListItemComponent implements OnInit, OnDestroy, OnChanges {
|
||||
private _subscriptions = new Subscription();
|
||||
|
||||
private _itemSubject = new ReplaySubject<Item>(1);
|
||||
|
||||
@Input() item: Item;
|
||||
|
||||
get item$() {
|
||||
return this._itemSubject.asObservable();
|
||||
}
|
||||
|
||||
get product() {
|
||||
return this.item.product;
|
||||
}
|
||||
|
||||
quantityFormControl = new FormControl<number>(null);
|
||||
|
||||
priceFormControl = new FormControl<number>(null, [Validators.required, Validators.pattern(/^\d+(,\d{1,2})?$/)]);
|
||||
|
||||
selectedFormControl = new FormControl<boolean>(false);
|
||||
|
||||
maxSelectableQuantity$: Observable<number>;
|
||||
|
||||
availabilities$ = this.item$.pipe(switchMap((item) => this._store.getAvailabilitiesForItem$(item.id)));
|
||||
|
||||
availability$: Observable<AvailabilityDTO>;
|
||||
|
||||
price$ = this.item$.pipe(switchMap((item) => this._store.getPrice$(item.id)));
|
||||
|
||||
priceValue$ = this.price$.pipe(map((price) => price?.value?.value));
|
||||
|
||||
priceVat$ = this.price$.pipe(map((price) => price?.vat?.value));
|
||||
|
||||
canEditPrice$ = this.item$.pipe(switchMap((item) => this._store.getCanEditPrice$(item.id)));
|
||||
|
||||
hideCheckbox$ = combineLatest([this._store.items$, this._store.purchaseOption$]).pipe(
|
||||
map(([items, purchaseOption]) => purchaseOption == undefined || items?.length === 1)
|
||||
);
|
||||
|
||||
canAddResult$ = this.item$.pipe(switchMap((item) => this._store.getCanAddResultForItemAndCurrentPurchaseOption$(item.id)));
|
||||
|
||||
constructor(private _store: PurchaseOptionsStore, private _service: PurchaseOptionsService) {}
|
||||
|
||||
initAvailability$() {
|
||||
combineLatest([this.item$, this._store.purchaseOption$]);
|
||||
|
||||
this.availability$ = combineLatest([this.item$, this._store.purchaseOption$, this._store.inStoreBranch$]).pipe(
|
||||
switchMap(([item, purchaseOption, inStoreBranch]) => {
|
||||
const itemData = mapToItemData(item, this._store.type);
|
||||
|
||||
if (purchaseOption === 'in-store' && inStoreBranch) {
|
||||
return this._service.fetchInStoreAvailability(itemData, item.quantity, inStoreBranch);
|
||||
}
|
||||
|
||||
return [];
|
||||
})
|
||||
);
|
||||
|
||||
this.maxSelectableQuantity$ = combineLatest([this._store.purchaseOption$, this.availability$]).pipe(
|
||||
map(([purchaseOption, availability]) => {
|
||||
if (purchaseOption === 'in-store') {
|
||||
return availability?.inStock;
|
||||
}
|
||||
|
||||
return 999;
|
||||
}),
|
||||
startWith(999)
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initQuantitySubscription();
|
||||
this.initPriceSubscription();
|
||||
this.initSelectedSubscription();
|
||||
this.initAvailability$();
|
||||
}
|
||||
|
||||
ngOnChanges({ item }: SimpleChanges) {
|
||||
if (item) {
|
||||
this._itemSubject.next(this.item);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._itemSubject.complete();
|
||||
this._subscriptions.unsubscribe();
|
||||
}
|
||||
|
||||
initQuantitySubscription() {
|
||||
const sub = this.item$.subscribe((item) => {
|
||||
if (this.quantityFormControl.value !== item.quantity) {
|
||||
this.quantityFormControl.setValue(item.quantity);
|
||||
}
|
||||
});
|
||||
|
||||
const valueChangesSub = this.quantityFormControl.valueChanges.subscribe((quantity) => {
|
||||
if (this.item.quantity !== quantity) {
|
||||
this._store.setItemQuantity(this.item.id, quantity);
|
||||
}
|
||||
});
|
||||
|
||||
this._subscriptions.add(sub);
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
|
||||
initPriceSubscription() {
|
||||
const sub = this.price$.subscribe((price) => {
|
||||
if (this.priceFormControl.value !== price?.value?.value) {
|
||||
this.priceFormControl.setValue(price?.value?.value);
|
||||
}
|
||||
});
|
||||
|
||||
const valueChangesSub = this.priceFormControl.valueChanges.subscribe((value) => {
|
||||
if (this.priceFormControl.valid) {
|
||||
const price = this._store.getPrice(this.item.id);
|
||||
if (price[this.item.id] !== value) {
|
||||
this._store.setPrice(this.item.id, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
this._subscriptions.add(sub);
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
|
||||
initSelectedSubscription() {
|
||||
const sub = this.item$
|
||||
.pipe(switchMap((item) => this._store.selectedItemIds$.pipe(map((ids) => ids.includes(item.id)))))
|
||||
.subscribe((selected) => {
|
||||
if (this.selectedFormControl.value !== selected) {
|
||||
this.selectedFormControl.setValue(selected);
|
||||
}
|
||||
});
|
||||
const valueChangesSub = this.selectedFormControl.valueChanges.subscribe((selected) => {
|
||||
const current = this._store.selectedItemIds.includes(this.item.id);
|
||||
if (current !== selected) {
|
||||
this._store.setSelectedItem(this.item.id, selected);
|
||||
}
|
||||
});
|
||||
this._subscriptions.add(sub);
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
:host {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto auto 1fr auto;
|
||||
max-height: 85vh;
|
||||
@apply pt-6;
|
||||
}
|
||||
|
||||
.shared-purchase-options-modal__items {
|
||||
overflow: scroll;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<h3 class="text-center font-bold text-2xl">Lieferung auswählen</h3>
|
||||
<p class="text-center font-2xl mt-4">
|
||||
Wie möchten Sie die Artikel erhalten?
|
||||
</p>
|
||||
<div class="rounded p-4 shadow-card mt-4 grid grid-flow-col gap-4 justify-center items-center relative">
|
||||
<!-- <ng-container *ngFor="let option of purchasingOptions$ | async">
|
||||
<ng-container [ngSwitch]="option">
|
||||
<app-delivery-purchase-options-tile *ngSwitchCase="'delivery'"> </app-delivery-purchase-options-tile>
|
||||
<app-in-store-purchase-options-tile *ngSwitchCase="'in-store'"> </app-in-store-purchase-options-tile>
|
||||
<app-pickup-purchase-options-tile *ngSwitchCase="'pickup'"> </app-pickup-purchase-options-tile>
|
||||
<app-download-purchase-options-tile *ngSwitchCase="'download'"> </app-download-purchase-options-tile>
|
||||
</ng-container>
|
||||
</ng-container> -->
|
||||
|
||||
<ng-container *ngIf="!(isDownloadOnly$ | async)">
|
||||
<app-in-store-purchase-options-tile> </app-in-store-purchase-options-tile>
|
||||
<app-pickup-purchase-options-tile> </app-pickup-purchase-options-tile>
|
||||
<app-delivery-purchase-options-tile> </app-delivery-purchase-options-tile>
|
||||
</ng-container>
|
||||
|
||||
<app-download-purchase-options-tile *ngIf="hasDownload$ | async"> </app-download-purchase-options-tile>
|
||||
</div>
|
||||
<shared-purchase-options-list-header></shared-purchase-options-list-header>
|
||||
<div class="shared-purchase-options-modal__items -mx-4">
|
||||
<shared-purchase-options-list-item
|
||||
class="border-t border-gray-200 p-4 border-solid"
|
||||
*ngFor="let item of items$ | async; trackBy: itemTrackBy"
|
||||
[item]="item"
|
||||
></shared-purchase-options-list-item>
|
||||
</div>
|
||||
<div class="text-center -mx-4 border-t border-gray-200 p-4 border-solid">
|
||||
<ng-container *ngIf="type === 'add'">
|
||||
<button type="button" class="isa-cta-button" [disabled]="!(canContinue$ | async) || saving" (click)="save()">Weiter einkaufen</button>
|
||||
<button type="button" class="ml-4 isa-cta-button isa-button-primary" [disabled]="!(canContinue$ | async) || saving" (click)="save()">
|
||||
Fortfahren
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="type === 'update'">
|
||||
<button type="button" class="ml-4 isa-cta-button isa-button-primary" [disabled]="!(canContinue$ | async) || saving" (click)="save()">
|
||||
Fortfahren
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, TrackByFunction } from '@angular/core';
|
||||
import { UiModalRef } from '@ui/modal';
|
||||
import { PurchaseOptionsModalData } from './purchase-options-modal.data';
|
||||
|
||||
import { PurchaseOptionsListHeaderComponent } from './purchase-options-list-header';
|
||||
import { PurchaseOptionsListItemComponent } from './purchase-options-list-item';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { of, Subject } from 'rxjs';
|
||||
import {
|
||||
DeliveryPurchaseOptionTileComponent,
|
||||
DownloadPurchaseOptionTileComponent,
|
||||
InStorePurchaseOptionTileComponent,
|
||||
PickupPurchaseOptionTileComponent,
|
||||
} from './purchase-options-tile';
|
||||
import { AddToShoppingCartDTO } from '@swagger/checkout';
|
||||
import { Item, PurchaseOptionsStore } from './store';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-purchase-options-modal',
|
||||
templateUrl: 'purchase-options-modal.component.html',
|
||||
styleUrls: ['purchase-options-modal.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
providers: [PurchaseOptionsStore],
|
||||
imports: [
|
||||
CommonModule,
|
||||
PurchaseOptionsListHeaderComponent,
|
||||
PurchaseOptionsListItemComponent,
|
||||
DeliveryPurchaseOptionTileComponent,
|
||||
InStorePurchaseOptionTileComponent,
|
||||
PickupPurchaseOptionTileComponent,
|
||||
DownloadPurchaseOptionTileComponent,
|
||||
],
|
||||
})
|
||||
export class PurchaseOptionsModalComponent implements OnInit, OnDestroy {
|
||||
get type() {
|
||||
return this._uiModalRef.data.type;
|
||||
}
|
||||
|
||||
items$ = this.store.items$;
|
||||
|
||||
purchasingOptions$ = this.store.getPurchaseOptionsInAvailabilities$;
|
||||
|
||||
isDownloadOnly$ = this.purchasingOptions$.pipe(
|
||||
map((purchasingOptions) => purchasingOptions.length === 1 && purchasingOptions[0] === 'download')
|
||||
);
|
||||
|
||||
hasDownload$ = this.purchasingOptions$.pipe(map((purchasingOptions) => purchasingOptions.includes('download')));
|
||||
|
||||
canContinue$ = this.store.canContinue$.pipe(shareReplay(1));
|
||||
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
saving = false;
|
||||
|
||||
constructor(private _uiModalRef: UiModalRef<void, PurchaseOptionsModalData>, public store: PurchaseOptionsStore) {
|
||||
this.store.initialize(this._uiModalRef.data);
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._onDestroy$.next();
|
||||
this._onDestroy$.complete();
|
||||
}
|
||||
|
||||
itemTrackBy: TrackByFunction<Item> = (_, item) => item.id;
|
||||
|
||||
async save() {
|
||||
if (this.saving) {
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
await this.store.save();
|
||||
|
||||
if (this.store.items.length === 0) {
|
||||
this._uiModalRef.close();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ItemDTO } from '@swagger/cat';
|
||||
import { ShoppingCartItemDTO, BranchDTO } from '@swagger/checkout';
|
||||
import { ActionType } from './store';
|
||||
|
||||
export interface PurchaseOptionsModalData {
|
||||
processId: number;
|
||||
type: ActionType;
|
||||
items: Array<ItemDTO | ShoppingCartItemDTO>;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UiModalRef, UiModalService } from '@ui/modal';
|
||||
import { PurchaseOptionsModalComponent } from './purchase-options-modal.component';
|
||||
import { PurchaseOptionsModalData } from './purchase-options-modal.data';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PurchaseOptionsModalService {
|
||||
constructor(private _uiModal: UiModalService) {}
|
||||
|
||||
open(data: PurchaseOptionsModalData): UiModalRef<void, PurchaseOptionsModalData> {
|
||||
return this._uiModal.open<void, PurchaseOptionsModalData>({
|
||||
content: PurchaseOptionsModalComponent,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ChangeDetectorRef, Directive, HostBinding, HostListener } from '@angular/core';
|
||||
import { asapScheduler } from 'rxjs';
|
||||
import { PurchaseOption, PurchaseOptionsStore } from '../store';
|
||||
|
||||
@Directive({})
|
||||
export abstract class BasePurchaseOptionDirective {
|
||||
protected abstract store: PurchaseOptionsStore;
|
||||
protected abstract cdr: ChangeDetectorRef;
|
||||
|
||||
@HostBinding('class.selected')
|
||||
get selected() {
|
||||
return this.store.purchaseOption === this.purchaseOption;
|
||||
}
|
||||
|
||||
constructor(protected purchaseOption: PurchaseOption) {}
|
||||
|
||||
@HostListener('click')
|
||||
setPurchaseOptions() {
|
||||
this.store.setPurchaseOption(this.purchaseOption);
|
||||
this.store.resetSelectedItems();
|
||||
asapScheduler.schedule(() => {
|
||||
const items = this.store.getItemsThatHaveAnAvailabilityAndCanAddForPurchaseOption(this.purchaseOption);
|
||||
items.forEach((item) => this.store.setSelectedItem(item.id, true));
|
||||
});
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="purchase-options-tile__heading">
|
||||
<div class="icon-wrapper">
|
||||
<ui-svg-icon icon="isa-truck"></ui-svg-icon>
|
||||
</div>
|
||||
<span class="purchase-option-name">Versand</span>
|
||||
</div>
|
||||
<div class="purchase-options-tile__body">
|
||||
Artikel geliefert bekommen?
|
||||
</div>
|
||||
<div class="purchase-options-tile__actions">
|
||||
<span>Versandkostenfrei</span>
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { PurchaseOptionsStore } from '../store';
|
||||
import { BasePurchaseOptionDirective } from './base-purchase-option.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-delivery-purchase-options-tile',
|
||||
templateUrl: 'delivery-purchase-options-tile.component.html',
|
||||
styleUrls: ['purchase-options-tile.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, UiIconModule],
|
||||
})
|
||||
export class DeliveryPurchaseOptionTileComponent extends BasePurchaseOptionDirective {
|
||||
constructor(protected store: PurchaseOptionsStore, protected cdr: ChangeDetectorRef) {
|
||||
super('delivery');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="purchase-options-tile__heading">
|
||||
<div class="icon-wrapper">
|
||||
<ui-svg-icon icon="isa-download"></ui-svg-icon>
|
||||
</div>
|
||||
<span class="purchase-option-name">Download</span>
|
||||
</div>
|
||||
<div class="purchase-options-tile__body">
|
||||
Für den Kauf benötigen Sie ein Onlinekonto
|
||||
</div>
|
||||
<div class="purchase-options-tile__actions">
|
||||
<span>Sofort verfügbar</span>
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { PurchaseOptionsStore } from '../store';
|
||||
import { BasePurchaseOptionDirective } from './base-purchase-option.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-download-purchase-options-tile',
|
||||
templateUrl: 'download-purchase-options-tile.component.html',
|
||||
styleUrls: ['purchase-options-tile.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, UiIconModule],
|
||||
})
|
||||
export class DownloadPurchaseOptionTileComponent extends BasePurchaseOptionDirective {
|
||||
constructor(protected store: PurchaseOptionsStore, protected cdr: ChangeDetectorRef) {
|
||||
super('download');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<div class="purchase-options-tile__heading">
|
||||
<div class="icon-wrapper">
|
||||
<ui-svg-icon icon="isa-shopping-bag"></ui-svg-icon>
|
||||
</div>
|
||||
<span class="purchase-option-name">Rücklage</span>
|
||||
</div>
|
||||
<div class="purchase-options-tile__body">
|
||||
Artikel zurücklegen lassen oder sofort mitnehmen?
|
||||
</div>
|
||||
<div class="purchase-options-tile__actions group">
|
||||
<button type="button" class="w-[176px] flex flex-row items-center justify-between" (click)="sharedBranchSelector.focus()">
|
||||
<span class="grow text-left truncate">{{ inStoreBranch$ | async | branchName }}</span>
|
||||
<span class="transition-all group-focus-within:rotate-180">
|
||||
<ui-svg-icon icon="menu-down"></ui-svg-icon>
|
||||
</span>
|
||||
</button>
|
||||
<shared-branch-selector
|
||||
#sharedBranchSelector
|
||||
class="absolute inset-x-0 invisible group-focus-within:visible"
|
||||
[ngModel]="undefined"
|
||||
(ngModelChange)="setInStoreBranch($event)"
|
||||
[branchType]="1"
|
||||
></shared-branch-selector>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BranchSelectorComponent } from '@shared/components/branch-selector';
|
||||
import { BranchNamePipe } from '@shared/pipes/branch';
|
||||
import { BranchDTO } from '@swagger/checkout';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { PurchaseOptionsStore } from '../store';
|
||||
import { BasePurchaseOptionDirective } from './base-purchase-option.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-in-store-purchase-options-tile',
|
||||
templateUrl: 'in-store-purchase-options-tile.component.html',
|
||||
styleUrls: ['purchase-options-tile.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, UiIconModule, BranchSelectorComponent, FormsModule, BranchNamePipe],
|
||||
})
|
||||
export class InStorePurchaseOptionTileComponent extends BasePurchaseOptionDirective {
|
||||
inStoreBranch$ = this.store.inStoreBranch$;
|
||||
|
||||
constructor(protected store: PurchaseOptionsStore, protected cdr: ChangeDetectorRef) {
|
||||
super('in-store');
|
||||
}
|
||||
|
||||
setInStoreBranch(branch?: BranchDTO) {
|
||||
this.store.setInStoreBranch(branch);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user