Merged PR 1513: Kaufoptionen

Related work items: #3365, #3366, #3385, #3386, #3391
This commit is contained in:
Lorenz Hilpert
2023-03-20 17:11:53 +00:00
committed by Nino Righi
parent 80bfc59356
commit f4c1c3dd7f
125 changed files with 5128 additions and 5909 deletions

View File

@@ -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);
}

View File

@@ -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[];
})
);
})

View File

@@ -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' },
],
}),
],

View File

@@ -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],
});
}

View File

@@ -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);
}

View File

@@ -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',
});
}

View File

@@ -1,4 +1,3 @@
// start:ng42.barrel
export * from './page-checkout.module';
export * from './page-checkout-modals.module';
// end:ng42.barrel

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './delivery-option-list.component';
// end:ng42.barrel

View File

@@ -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

View File

@@ -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;
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './pick-up-option-list.component';
// end:ng42.barrel

View File

@@ -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>

View File

@@ -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 }));
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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 }] });
}
}
}

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -1,7 +0,0 @@
import { ShoppingCartItemDTO } from '@swagger/checkout';
export interface PurchasingOptionsListModalData {
processId: number;
shoppingCartItems?: ShoppingCartItemDTO[];
customerFeatures: { [key: string]: string };
}

View File

@@ -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 {}

View File

@@ -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: [] });
}
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './take-away-option-list.component';
// end:ng42.barrel

View File

@@ -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>

View File

@@ -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 }));
}
}

View File

@@ -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>

View File

@@ -1,9 +0,0 @@
.option-icon {
margin-top: -12px;
width: 70px;
}
h4 {
@apply font-bold;
margin-top: -2px;
}

View File

@@ -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');
}
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './b2b-delivery-option.component';
// end:ng42.barrel

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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');
}
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './delivery-option.component';
// end:ng42.barrel

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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');
}
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './dig-delivery-option.component';
// end:ng42.barrel

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './pick-up-option.component';
// end:ng42.barrel

View File

@@ -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>

View File

@@ -1,3 +0,0 @@
h4 {
@apply font-bold;
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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 {}

View File

@@ -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) => {}
)
)
)
)
);
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './take-away-option.component';
// end:ng42.barrel

View File

@@ -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>

View File

@@ -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');
}
}

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -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);

View File

@@ -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

View File

@@ -0,0 +1,6 @@
{
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -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() {}
}

View File

@@ -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() {}
}

View File

@@ -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];
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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 {}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,6 @@
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({ selector: '[sharedInputControlOutlet]' })
export class OutletDirective {
constructor(public viewContainerRef: ViewContainerRef) {}
}

View File

@@ -0,0 +1,6 @@
import { Directive } from '@angular/core';
@Directive({ selector: 'sharedinput-control-prefix', host: { class: 'shared-input-control-prefix' } })
export class PrefixDirective {
constructor() {}
}

View File

@@ -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() {}
}

View File

@@ -0,0 +1 @@
export type SharedInputControlStatus = 'valid' | 'invalid' | 'disabled' | 'pending' | 'dirty' | 'pristine' | 'touched' | 'untouched';

View File

@@ -0,0 +1,3 @@
export * from './lib/input-control.component';
export * from './lib/input.directive';
export * from './lib/input-control.module';

View File

@@ -0,0 +1,6 @@
{
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -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 };

View 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';
}

View File

@@ -0,0 +1 @@
export * from './purchase-options-list-header.component';

View File

@@ -0,0 +1,3 @@
:host {
@apply mt-4 mb-2 flex flex-row justify-end;
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -0,0 +1 @@
export * from './purchase-options-list-item.component';

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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>;
}

View File

@@ -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,
});
}
}

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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');
}
}

View File

@@ -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>

View File

@@ -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');
}
}

View File

@@ -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>

View File

@@ -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