mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1960: feat: implement reward points system in purchase options
feat: implement reward points system in purchase options - Add version tracking to application store for data migration support - Integrate redemption points display in purchase options list items - Update purchase options modal to handle reward point calculations - Enhance shopping cart item component with reward point functionality - Add reward point schemas and validation to checkout data access - Update user storage provider with versioning support - Improve logger configuration in customer guard - Update package dependencies for reward functionality - Fix ESLint errors for code quality compliance Refs: #5352 Related work items: #5263, #5352, #5355
This commit is contained in:
committed by
Nino Righi
parent
2387c60228
commit
c745f82f3a
@@ -112,7 +112,7 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
const auth = injector.get(AuthService);
|
||||
try {
|
||||
await auth.init();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
statusElement.innerHTML = 'Authentifizierung wird durchgeführt...';
|
||||
const strategy = injector.get(LoginStrategy);
|
||||
await strategy.login();
|
||||
@@ -137,7 +137,7 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
}
|
||||
// Subscribe on Store changes and save to user storage
|
||||
store.pipe(debounceTime(1000)).subscribe((state) => {
|
||||
userStorage.set('store', state);
|
||||
userStorage.set('store', { ...state, version });
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error during app initialization', error);
|
||||
|
||||
@@ -1,205 +1,206 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
} from "@angular/router";
|
||||
import { ApplicationProcess, ApplicationService } from "@core/application";
|
||||
import { DomainCheckoutService } from "@domain/checkout";
|
||||
import { logger } from "@isa/core/logging";
|
||||
import { CustomerSearchNavigation } from "@shared/services/navigation";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class CanActivateCustomerGuard {
|
||||
#logger = logger(() => ({
|
||||
context: "CanActivateCustomerGuard",
|
||||
tags: ["guard", "customer", "navigation"],
|
||||
}));
|
||||
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _checkoutService: DomainCheckoutService,
|
||||
private readonly _router: Router,
|
||||
private readonly _navigation: CustomerSearchNavigation,
|
||||
) {}
|
||||
|
||||
async canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
{ url }: RouterStateSnapshot,
|
||||
) {
|
||||
if (url.startsWith("/kunde/customer/search/")) {
|
||||
const processId = Date.now(); // Generate a new process ID
|
||||
// Extract parts before and after the pattern
|
||||
const parts = url.split("/kunde/customer/");
|
||||
if (parts.length === 2) {
|
||||
const prefix = parts[0] + "/kunde/";
|
||||
const suffix = "customer/" + parts[1];
|
||||
|
||||
// Construct the new URL with process ID inserted
|
||||
const newUrl = `${prefix}${processId}/${suffix}`;
|
||||
|
||||
this.#logger.info("Redirecting to URL with process ID", () => ({
|
||||
originalUrl: url,
|
||||
newUrl,
|
||||
processId,
|
||||
}));
|
||||
|
||||
// Navigate to the new URL and prevent original navigation
|
||||
this._router.navigateByUrl(newUrl);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const processes = await this._applicationService
|
||||
.getProcesses$("customer")
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const lastActivatedProcessId = (
|
||||
await this._applicationService
|
||||
.getLastActivatedProcessWithSectionAndType$("customer", "cart")
|
||||
.pipe(first())
|
||||
.toPromise()
|
||||
)?.id;
|
||||
|
||||
const lastActivatedCartCheckoutProcessId = (
|
||||
await this._applicationService
|
||||
.getLastActivatedProcessWithSectionAndType$("customer", "cart-checkout")
|
||||
.pipe(first())
|
||||
.toPromise()
|
||||
)?.id;
|
||||
|
||||
const lastActivatedGoodsOutProcessId = (
|
||||
await this._applicationService
|
||||
.getLastActivatedProcessWithSectionAndType$("customer", "goods-out")
|
||||
.pipe(first())
|
||||
.toPromise()
|
||||
)?.id;
|
||||
|
||||
const activatedProcessId = await this._applicationService
|
||||
.getActivatedProcessId$()
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
// Darf nur reinkommen wenn der aktuell aktive Tab ein Bestellabschluss Tab ist
|
||||
if (
|
||||
!!lastActivatedCartCheckoutProcessId &&
|
||||
lastActivatedCartCheckoutProcessId === activatedProcessId
|
||||
) {
|
||||
await this.fromCartCheckoutProcess(
|
||||
processes,
|
||||
lastActivatedCartCheckoutProcessId,
|
||||
);
|
||||
return false;
|
||||
} else if (
|
||||
!!lastActivatedGoodsOutProcessId &&
|
||||
lastActivatedGoodsOutProcessId === activatedProcessId
|
||||
) {
|
||||
await this.fromGoodsOutProcess(processes, lastActivatedGoodsOutProcessId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!lastActivatedProcessId) {
|
||||
await this.fromCartProcess(processes);
|
||||
return false;
|
||||
} else {
|
||||
await this.navigateToDefaultRoute(lastActivatedProcessId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async navigateToDefaultRoute(processId: number) {
|
||||
const route = this._navigation.defaultRoute({ processId });
|
||||
|
||||
await this._router.navigate(route.path, { queryParams: route.queryParams });
|
||||
}
|
||||
|
||||
// Bei offener Artikelsuche/Kundensuche und Klick auf Footer Kundensuche
|
||||
async fromCartProcess(processes: ApplicationProcess[]) {
|
||||
const newProcessId = Date.now();
|
||||
await this._applicationService.createProcess({
|
||||
id: newProcessId,
|
||||
type: "cart",
|
||||
section: "customer",
|
||||
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === "cart"))}`,
|
||||
});
|
||||
|
||||
await this.navigateToDefaultRoute(newProcessId);
|
||||
}
|
||||
|
||||
// Bei offener Bestellbestätigung und Klick auf Footer Kundensuche
|
||||
async fromCartCheckoutProcess(
|
||||
processes: ApplicationProcess[],
|
||||
processId: number,
|
||||
) {
|
||||
// Um alle Checkout Daten zu resetten die mit dem Prozess assoziiert sind
|
||||
this._checkoutService.removeProcess({ processId });
|
||||
|
||||
// Ändere type cart-checkout zu cart
|
||||
this._applicationService.patchProcess(processId, {
|
||||
id: processId,
|
||||
type: "cart",
|
||||
section: "customer",
|
||||
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === "cart"))}`,
|
||||
data: {},
|
||||
});
|
||||
|
||||
// Navigation
|
||||
await this.navigateToDefaultRoute(processId);
|
||||
}
|
||||
|
||||
// Bei offener Warenausgabe und Klick auf Footer Kundensuche
|
||||
async fromGoodsOutProcess(
|
||||
processes: ApplicationProcess[],
|
||||
processId: number,
|
||||
) {
|
||||
const buyer = await this._checkoutService
|
||||
.getBuyer({ processId })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const customerFeatures = await this._checkoutService
|
||||
.getCustomerFeatures({ processId })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const name = buyer
|
||||
? customerFeatures?.b2b
|
||||
? buyer.organisation?.name
|
||||
? buyer.organisation?.name
|
||||
: buyer.lastName
|
||||
: buyer.lastName
|
||||
: `Vorgang ${this.processNumber(processes.filter((process) => process.type === "cart"))}`;
|
||||
|
||||
// Ändere type goods-out zu cart
|
||||
this._applicationService.patchProcess(processId, {
|
||||
id: processId,
|
||||
type: "cart",
|
||||
section: "customer",
|
||||
name,
|
||||
});
|
||||
|
||||
// Navigation
|
||||
await this.navigateToDefaultRoute(processId);
|
||||
}
|
||||
|
||||
processNumber(processes: ApplicationProcess[]) {
|
||||
const processNumbers = processes?.map((process) =>
|
||||
Number(process?.name?.replace(/\D/g, "")),
|
||||
);
|
||||
return !!processNumbers && processNumbers.length > 0
|
||||
? this.findMissingNumber(processNumbers)
|
||||
: 1;
|
||||
}
|
||||
|
||||
findMissingNumber(processNumbers: number[]) {
|
||||
for (
|
||||
let missingNumber = 1;
|
||||
missingNumber < Math.max(...processNumbers);
|
||||
missingNumber++
|
||||
) {
|
||||
if (!processNumbers.find((number) => number === missingNumber)) {
|
||||
return missingNumber;
|
||||
}
|
||||
}
|
||||
return Math.max(...processNumbers) + 1;
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanActivateCustomerGuard {
|
||||
#logger = logger(() => ({
|
||||
module: 'isa-app',
|
||||
importMetaUrl: import.meta.url,
|
||||
class: 'CanActivateCustomerGuard',
|
||||
}));
|
||||
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _checkoutService: DomainCheckoutService,
|
||||
private readonly _router: Router,
|
||||
private readonly _navigation: CustomerSearchNavigation,
|
||||
) {}
|
||||
|
||||
async canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
{ url }: RouterStateSnapshot,
|
||||
) {
|
||||
if (url.startsWith('/kunde/customer/search/')) {
|
||||
const processId = Date.now(); // Generate a new process ID
|
||||
// Extract parts before and after the pattern
|
||||
const parts = url.split('/kunde/customer/');
|
||||
if (parts.length === 2) {
|
||||
const prefix = parts[0] + '/kunde/';
|
||||
const suffix = 'customer/' + parts[1];
|
||||
|
||||
// Construct the new URL with process ID inserted
|
||||
const newUrl = `${prefix}${processId}/${suffix}`;
|
||||
|
||||
this.#logger.info('Redirecting to URL with process ID', () => ({
|
||||
originalUrl: url,
|
||||
newUrl,
|
||||
processId,
|
||||
}));
|
||||
|
||||
// Navigate to the new URL and prevent original navigation
|
||||
this._router.navigateByUrl(newUrl);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const processes = await this._applicationService
|
||||
.getProcesses$('customer')
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const lastActivatedProcessId = (
|
||||
await this._applicationService
|
||||
.getLastActivatedProcessWithSectionAndType$('customer', 'cart')
|
||||
.pipe(first())
|
||||
.toPromise()
|
||||
)?.id;
|
||||
|
||||
const lastActivatedCartCheckoutProcessId = (
|
||||
await this._applicationService
|
||||
.getLastActivatedProcessWithSectionAndType$('customer', 'cart-checkout')
|
||||
.pipe(first())
|
||||
.toPromise()
|
||||
)?.id;
|
||||
|
||||
const lastActivatedGoodsOutProcessId = (
|
||||
await this._applicationService
|
||||
.getLastActivatedProcessWithSectionAndType$('customer', 'goods-out')
|
||||
.pipe(first())
|
||||
.toPromise()
|
||||
)?.id;
|
||||
|
||||
const activatedProcessId = await this._applicationService
|
||||
.getActivatedProcessId$()
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
// Darf nur reinkommen wenn der aktuell aktive Tab ein Bestellabschluss Tab ist
|
||||
if (
|
||||
!!lastActivatedCartCheckoutProcessId &&
|
||||
lastActivatedCartCheckoutProcessId === activatedProcessId
|
||||
) {
|
||||
await this.fromCartCheckoutProcess(
|
||||
processes,
|
||||
lastActivatedCartCheckoutProcessId,
|
||||
);
|
||||
return false;
|
||||
} else if (
|
||||
!!lastActivatedGoodsOutProcessId &&
|
||||
lastActivatedGoodsOutProcessId === activatedProcessId
|
||||
) {
|
||||
await this.fromGoodsOutProcess(processes, lastActivatedGoodsOutProcessId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!lastActivatedProcessId) {
|
||||
await this.fromCartProcess(processes);
|
||||
return false;
|
||||
} else {
|
||||
await this.navigateToDefaultRoute(lastActivatedProcessId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async navigateToDefaultRoute(processId: number) {
|
||||
const route = this._navigation.defaultRoute({ processId });
|
||||
|
||||
await this._router.navigate(route.path, { queryParams: route.queryParams });
|
||||
}
|
||||
|
||||
// Bei offener Artikelsuche/Kundensuche und Klick auf Footer Kundensuche
|
||||
async fromCartProcess(processes: ApplicationProcess[]) {
|
||||
const newProcessId = Date.now();
|
||||
await this._applicationService.createProcess({
|
||||
id: newProcessId,
|
||||
type: 'cart',
|
||||
section: 'customer',
|
||||
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
|
||||
});
|
||||
|
||||
await this.navigateToDefaultRoute(newProcessId);
|
||||
}
|
||||
|
||||
// Bei offener Bestellbestätigung und Klick auf Footer Kundensuche
|
||||
async fromCartCheckoutProcess(
|
||||
processes: ApplicationProcess[],
|
||||
processId: number,
|
||||
) {
|
||||
// Um alle Checkout Daten zu resetten die mit dem Prozess assoziiert sind
|
||||
this._checkoutService.removeProcess({ processId });
|
||||
|
||||
// Ändere type cart-checkout zu cart
|
||||
this._applicationService.patchProcess(processId, {
|
||||
id: processId,
|
||||
type: 'cart',
|
||||
section: 'customer',
|
||||
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
|
||||
data: {},
|
||||
});
|
||||
|
||||
// Navigation
|
||||
await this.navigateToDefaultRoute(processId);
|
||||
}
|
||||
|
||||
// Bei offener Warenausgabe und Klick auf Footer Kundensuche
|
||||
async fromGoodsOutProcess(
|
||||
processes: ApplicationProcess[],
|
||||
processId: number,
|
||||
) {
|
||||
const buyer = await this._checkoutService
|
||||
.getBuyer({ processId })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const customerFeatures = await this._checkoutService
|
||||
.getCustomerFeatures({ processId })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const name = buyer
|
||||
? customerFeatures?.b2b
|
||||
? buyer.organisation?.name
|
||||
? buyer.organisation?.name
|
||||
: buyer.lastName
|
||||
: buyer.lastName
|
||||
: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`;
|
||||
|
||||
// Ändere type goods-out zu cart
|
||||
this._applicationService.patchProcess(processId, {
|
||||
id: processId,
|
||||
type: 'cart',
|
||||
section: 'customer',
|
||||
name,
|
||||
});
|
||||
|
||||
// Navigation
|
||||
await this.navigateToDefaultRoute(processId);
|
||||
}
|
||||
|
||||
processNumber(processes: ApplicationProcess[]) {
|
||||
const processNumbers = processes?.map((process) =>
|
||||
Number(process?.name?.replace(/\D/g, '')),
|
||||
);
|
||||
return !!processNumbers && processNumbers.length > 0
|
||||
? this.findMissingNumber(processNumbers)
|
||||
: 1;
|
||||
}
|
||||
|
||||
findMissingNumber(processNumbers: number[]) {
|
||||
for (
|
||||
let missingNumber = 1;
|
||||
missingNumber < Math.max(...processNumbers);
|
||||
missingNumber++
|
||||
) {
|
||||
if (!processNumbers.find((number) => number === missingNumber)) {
|
||||
return missingNumber;
|
||||
}
|
||||
}
|
||||
return Math.max(...processNumbers) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,185 +1,268 @@
|
||||
<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" sharedScaleContent>
|
||||
{{ product?.name }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__format flex flex-row items-center">
|
||||
<shared-icon [icon]="product?.format"></shared-icon>
|
||||
<span class="ml-2 font-bold">{{ product?.formatDetail }}</span>
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__manufacturer-and-ean">
|
||||
{{ product?.manufacturer }}
|
||||
@if (product?.manufacturer && product?.ean) {
|
||||
<span>|</span>
|
||||
}
|
||||
{{ product?.ean }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__volume-and-publication-date">
|
||||
{{ product?.volume }}
|
||||
@if (product?.volume && product?.publicationDate) {
|
||||
<span>|</span>
|
||||
}
|
||||
{{ product?.publicationDate | date: 'dd. MMMM yyyy' }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__availabilities mt-3 grid grid-flow-row gap-2 justify-start">
|
||||
@if ((availabilities$ | async)?.length) {
|
||||
<div class="whitespace-nowrap self-center">Verfügbar als</div>
|
||||
}
|
||||
@for (availability of availabilities$ | async; track availability) {
|
||||
<div class="grid grid-flow-col gap-4 justify-start">
|
||||
<div
|
||||
class="shared-purchase-options-list-item__availability grid grid-flow-col gap-2 items-center"
|
||||
[attr.data-option]="availability.purchaseOption"
|
||||
>
|
||||
@switch (availability.purchaseOption) {
|
||||
@case ('delivery') {
|
||||
<shared-icon icon="isa-truck" [size]="22"></shared-icon>
|
||||
{{ availability.data.estimatedDelivery?.start | date: 'EE dd.MM.' }}
|
||||
-
|
||||
{{ availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.' }}
|
||||
}
|
||||
@case ('dig-delivery') {
|
||||
<shared-icon icon="isa-truck" [size]="22"></shared-icon>
|
||||
{{ availability.data.estimatedDelivery?.start | date: 'EE dd.MM.' }}
|
||||
-
|
||||
{{ availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.' }}
|
||||
}
|
||||
@case ('b2b-delivery') {
|
||||
<shared-icon icon="isa-b2b-truck" [size]="24"></shared-icon>
|
||||
{{ availability.data.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
|
||||
}
|
||||
@case ('pickup') {
|
||||
<shared-icon
|
||||
class="cursor-pointer"
|
||||
#uiOverlayTrigger="uiOverlayTrigger"
|
||||
[uiOverlayTrigger]="orderDeadlineTooltip"
|
||||
[class.tooltip-active]="uiOverlayTrigger.opened"
|
||||
icon="isa-box-out"
|
||||
[size]="18"
|
||||
></shared-icon>
|
||||
{{ availability.data.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
|
||||
<ui-tooltip
|
||||
#orderDeadlineTooltip
|
||||
yPosition="above"
|
||||
xPosition="after"
|
||||
[yOffset]="-12"
|
||||
[xOffset]="4"
|
||||
[warning]="true"
|
||||
[closeable]="true"
|
||||
>
|
||||
<b>{{ availability.data?.orderDeadline | orderDeadline }}</b>
|
||||
</ui-tooltip>
|
||||
}
|
||||
@case ('in-store') {
|
||||
<shared-icon icon="isa-shopping-bag" [size]="18"></shared-icon>
|
||||
{{ availability.data.inStock }}x
|
||||
@if (isEVT) {
|
||||
ab {{ isEVT | date: 'dd. MMMM yyyy' }}
|
||||
} @else {
|
||||
ab sofort
|
||||
}
|
||||
}
|
||||
@case ('download') {
|
||||
<shared-icon icon="isa-download" [size]="22"></shared-icon>
|
||||
Download
|
||||
}
|
||||
}
|
||||
</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 flex flex-row items-center">
|
||||
<div class="relative flex flex-row justify-end items-start">
|
||||
@if (canEditVat$ | async) {
|
||||
<ui-select
|
||||
class="w-[6.5rem] min-h-[3.4375rem] p-4 rounded-card border border-solid border-[#AEB7C1] mr-4"
|
||||
tabindex="-1"
|
||||
[formControl]="manualVatFormControl"
|
||||
[defaultLabel]="'MwSt'"
|
||||
>
|
||||
@for (vat of vats$ | async; track vat) {
|
||||
<ui-select-option [label]="vat.name + '%'" [value]="vat.vatType"></ui-select-option>
|
||||
}
|
||||
</ui-select>
|
||||
}
|
||||
@if (canEditPrice$ | async) {
|
||||
<shared-input-control
|
||||
[class.ml-6]="priceFormControl?.invalid && priceFormControl?.dirty"
|
||||
>
|
||||
<shared-input-control-indicator>
|
||||
@if (priceFormControl?.invalid && priceFormControl?.dirty) {
|
||||
<shared-icon icon="mat-info"></shared-icon>
|
||||
}
|
||||
</shared-input-control-indicator>
|
||||
<input
|
||||
[uiOverlayTrigger]="giftCardTooltip"
|
||||
triggerOn="none"
|
||||
#quantityInput
|
||||
#priceOverlayTrigger="uiOverlayTrigger"
|
||||
sharedInputControlInput
|
||||
type="string"
|
||||
class="w-24"
|
||||
[formControl]="priceFormControl"
|
||||
placeholder="00,00"
|
||||
(sharedOnInit)="onPriceInputInit(quantityInput, priceOverlayTrigger)"
|
||||
sharedNumberValue
|
||||
/>
|
||||
<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-error error="min">Preis ist ungültig</shared-input-control-error>
|
||||
<shared-input-control-error error="max">Preis ist ungültig</shared-input-control-error>
|
||||
</shared-input-control>
|
||||
} @else {
|
||||
{{ priceValue$ | async | currency: 'EUR' : 'code' }}
|
||||
}
|
||||
|
||||
<ui-tooltip [warning]="true" xPosition="after" yPosition="below" [xOffset]="-55" [yOffset]="18" [closeable]="true" #giftCardTooltip>
|
||||
Tragen Sie hier den
|
||||
<br />
|
||||
Gutscheinbetrag ein.
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<ui-quantity-dropdown class="mt-2" [formControl]="quantityFormControl" [range]="maxSelectableQuantity$ | async"></ui-quantity-dropdown>
|
||||
<div class="pt-7">
|
||||
@if ((canAddResult$ | async)?.canAdd) {
|
||||
<input
|
||||
class="fancy-checkbox"
|
||||
[class.checked]="selectedFormControl?.value"
|
||||
[formControl]="selectedFormControl"
|
||||
type="checkbox"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (canAddResult$ | async; as canAddResult) {
|
||||
@if (!canAddResult.canAdd) {
|
||||
<span class="inline-block font-bold text-[#BE8100] mt-[14px] max-w-[19rem]">
|
||||
{{ canAddResult.message }}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
@if (showMaxAvailableQuantity$ | async) {
|
||||
<span class="font-bold text-[#BE8100] mt-[14px]">
|
||||
{{ (availability$ | async)?.inStock }} Exemplare sofort lieferbar
|
||||
</span>
|
||||
}
|
||||
@if (showNotAvailable$ | async) {
|
||||
<span class="font-bold text-[#BE8100] mt-[14px]">Derzeit nicht bestellbar</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="w-16"></div>
|
||||
<div class="grow shared-purchase-options-list-item__availabilities"></div>
|
||||
</div>
|
||||
<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"
|
||||
sharedScaleContent
|
||||
>
|
||||
{{ product?.name }}
|
||||
</div>
|
||||
<div
|
||||
class="shared-purchase-options-list-item__format flex flex-row items-center"
|
||||
>
|
||||
<shared-icon [icon]="product?.format"></shared-icon>
|
||||
<span class="ml-2 font-bold">{{ product?.formatDetail }}</span>
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__manufacturer-and-ean">
|
||||
{{ product?.manufacturer }}
|
||||
@if (product?.manufacturer && product?.ean) {
|
||||
<span>|</span>
|
||||
}
|
||||
{{ product?.ean }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__volume-and-publication-date">
|
||||
{{ product?.volume }}
|
||||
@if (product?.volume && product?.publicationDate) {
|
||||
<span>|</span>
|
||||
}
|
||||
{{ product?.publicationDate | date: 'dd. MMMM yyyy' }}
|
||||
</div>
|
||||
<div
|
||||
class="shared-purchase-options-list-item__availabilities mt-3 grid grid-flow-row gap-2 justify-start"
|
||||
>
|
||||
@if ((availabilities$ | async)?.length) {
|
||||
<div class="whitespace-nowrap self-center">Verfügbar als</div>
|
||||
}
|
||||
@for (availability of availabilities$ | async; track availability) {
|
||||
<div class="grid grid-flow-col gap-4 justify-start">
|
||||
<div
|
||||
class="shared-purchase-options-list-item__availability grid grid-flow-col gap-2 items-center"
|
||||
[attr.data-option]="availability.purchaseOption"
|
||||
>
|
||||
@switch (availability.purchaseOption) {
|
||||
@case ('delivery') {
|
||||
<shared-icon icon="isa-truck" [size]="22"></shared-icon>
|
||||
{{
|
||||
availability.data.estimatedDelivery?.start | date: 'EE dd.MM.'
|
||||
}}
|
||||
-
|
||||
{{
|
||||
availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.'
|
||||
}}
|
||||
}
|
||||
@case ('dig-delivery') {
|
||||
<shared-icon icon="isa-truck" [size]="22"></shared-icon>
|
||||
{{
|
||||
availability.data.estimatedDelivery?.start | date: 'EE dd.MM.'
|
||||
}}
|
||||
-
|
||||
{{
|
||||
availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.'
|
||||
}}
|
||||
}
|
||||
@case ('b2b-delivery') {
|
||||
<shared-icon icon="isa-b2b-truck" [size]="24"></shared-icon>
|
||||
{{
|
||||
availability.data.estimatedShippingDate
|
||||
| date: 'dd. MMMM yyyy'
|
||||
}}
|
||||
}
|
||||
@case ('pickup') {
|
||||
<shared-icon
|
||||
class="cursor-pointer"
|
||||
#uiOverlayTrigger="uiOverlayTrigger"
|
||||
[uiOverlayTrigger]="orderDeadlineTooltip"
|
||||
[class.tooltip-active]="uiOverlayTrigger.opened"
|
||||
icon="isa-box-out"
|
||||
[size]="18"
|
||||
></shared-icon>
|
||||
{{
|
||||
availability.data.estimatedShippingDate
|
||||
| date: 'dd. MMMM yyyy'
|
||||
}}
|
||||
<ui-tooltip
|
||||
#orderDeadlineTooltip
|
||||
yPosition="above"
|
||||
xPosition="after"
|
||||
[yOffset]="-12"
|
||||
[xOffset]="4"
|
||||
[warning]="true"
|
||||
[closeable]="true"
|
||||
>
|
||||
<b>{{ availability.data?.orderDeadline | orderDeadline }}</b>
|
||||
</ui-tooltip>
|
||||
}
|
||||
@case ('in-store') {
|
||||
<shared-icon icon="isa-shopping-bag" [size]="18"></shared-icon>
|
||||
{{ availability.data.inStock }}x
|
||||
@if (isEVT) {
|
||||
ab {{ isEVT | date: 'dd. MMMM yyyy' }}
|
||||
} @else {
|
||||
ab sofort
|
||||
}
|
||||
}
|
||||
@case ('download') {
|
||||
<shared-icon icon="isa-download" [size]="22"></shared-icon>
|
||||
Download
|
||||
}
|
||||
}
|
||||
</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 flex flex-row items-center"
|
||||
>
|
||||
<div class="relative flex flex-row justify-end items-start">
|
||||
@if (showRedemptionPoints()) {
|
||||
<span class="isa-text-body-2-regular text-isa-neutral-600"
|
||||
>Einlösen für:</span
|
||||
>
|
||||
<span class="ml-2 isa-text-body-2-bold text-isa-secondary-900"
|
||||
>{{
|
||||
redemptionPoints() * quantityFormControl.value
|
||||
}}
|
||||
Lesepunkte</span
|
||||
>
|
||||
} @else {
|
||||
@if (canEditVat$ | async) {
|
||||
<ui-select
|
||||
class="w-[6.5rem] min-h-[3.4375rem] p-4 rounded-card border border-solid border-[#AEB7C1] mr-4"
|
||||
tabindex="-1"
|
||||
[formControl]="manualVatFormControl"
|
||||
[defaultLabel]="'MwSt'"
|
||||
>
|
||||
@for (vat of vats$ | async; track vat) {
|
||||
<ui-select-option
|
||||
[label]="vat.name + '%'"
|
||||
[value]="vat.vatType"
|
||||
></ui-select-option>
|
||||
}
|
||||
</ui-select>
|
||||
}
|
||||
@if (canEditPrice$ | async) {
|
||||
<shared-input-control
|
||||
[class.ml-6]="
|
||||
priceFormControl?.invalid && priceFormControl?.dirty
|
||||
"
|
||||
>
|
||||
<shared-input-control-indicator>
|
||||
@if (priceFormControl?.invalid && priceFormControl?.dirty) {
|
||||
<shared-icon icon="mat-info"></shared-icon>
|
||||
}
|
||||
</shared-input-control-indicator>
|
||||
<input
|
||||
[uiOverlayTrigger]="giftCardTooltip"
|
||||
triggerOn="none"
|
||||
#quantityInput
|
||||
#priceOverlayTrigger="uiOverlayTrigger"
|
||||
sharedInputControlInput
|
||||
type="string"
|
||||
class="w-24"
|
||||
[formControl]="priceFormControl"
|
||||
placeholder="00,00"
|
||||
(sharedOnInit)="
|
||||
onPriceInputInit(quantityInput, priceOverlayTrigger)
|
||||
"
|
||||
sharedNumberValue
|
||||
/>
|
||||
<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-error error="min"
|
||||
>Preis ist ungültig</shared-input-control-error
|
||||
>
|
||||
<shared-input-control-error error="max"
|
||||
>Preis ist ungültig</shared-input-control-error
|
||||
>
|
||||
</shared-input-control>
|
||||
} @else {
|
||||
{{ priceValue$ | async | currency: 'EUR' : 'code' }}
|
||||
}
|
||||
}
|
||||
<ui-tooltip
|
||||
[warning]="true"
|
||||
xPosition="after"
|
||||
yPosition="below"
|
||||
[xOffset]="-55"
|
||||
[yOffset]="18"
|
||||
[closeable]="true"
|
||||
#giftCardTooltip
|
||||
>
|
||||
Tragen Sie hier den
|
||||
<br />
|
||||
Gutscheinbetrag ein.
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<ui-quantity-dropdown
|
||||
class="mt-2"
|
||||
[formControl]="quantityFormControl"
|
||||
[range]="maxSelectableQuantity$ | async"
|
||||
data-what="purchase-option-quantity"
|
||||
[attr.data-which]="product?.ean"
|
||||
></ui-quantity-dropdown>
|
||||
<div class="pt-7">
|
||||
@if ((canAddResult$ | async)?.canAdd) {
|
||||
<input
|
||||
class="fancy-checkbox"
|
||||
[class.checked]="selectedFormControl?.value"
|
||||
[formControl]="selectedFormControl"
|
||||
type="checkbox"
|
||||
data-what="purchase-option-selector"
|
||||
[attr.data-which]="product?.ean"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (canAddResult$ | async; as canAddResult) {
|
||||
@if (!canAddResult.canAdd) {
|
||||
<span
|
||||
class="inline-block font-bold text-[#BE8100] mt-[14px] max-w-[19rem]"
|
||||
>
|
||||
{{ canAddResult.message }}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
@if (showMaxAvailableQuantity$ | async) {
|
||||
<span class="font-bold text-[#BE8100] mt-[14px]">
|
||||
{{ (availability$ | async)?.inStock }} Exemplare sofort lieferbar
|
||||
</span>
|
||||
}
|
||||
@if (showNotAvailable$ | async) {
|
||||
<span class="font-bold text-[#BE8100] mt-[14px]"
|
||||
>Derzeit nicht bestellbar</span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="w-16"></div>
|
||||
<div class="grow shared-purchase-options-list-item__availabilities"></div>
|
||||
</div>
|
||||
@if (showLowStockMessage()) {
|
||||
<div
|
||||
class="text-isa-accent-red isa-text-body-2-bold mt-6 flex flex-row items-center gap-2"
|
||||
>
|
||||
<ng-icon name="isaOtherInfo" size="1.5rem"></ng-icon>
|
||||
<div>Geringer Bestand - Artikel holen vor Abschluss</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,349 +1,467 @@
|
||||
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 { ElementLifecycleModule } from '@shared/directives/element-lifecycle';
|
||||
import { UiCommonModule, UiOverlayTriggerDirective } from '@ui/common';
|
||||
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
|
||||
import { UiSpinnerModule } from '@ui/spinner';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
import { combineLatest, ReplaySubject, Subscription } from 'rxjs';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { map, take, shareReplay, startWith, switchMap, withLatestFrom } from 'rxjs/operators';
|
||||
import { GIFT_CARD_MAX_PRICE, PRICE_PATTERN } from '../constants';
|
||||
import { Item, PurchaseOptionsStore, isItemDTO, isShoppingCartItemDTO } from '../store';
|
||||
import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
|
||||
import { UiSelectModule } from '@ui/select';
|
||||
import { KeyValueDTOOfStringAndString } from '@generated/swagger/cat-search-api';
|
||||
import { ScaleContentComponent } from '@shared/components/scale-content';
|
||||
import moment from 'moment';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-purchase-options-list-item',
|
||||
templateUrl: 'purchase-options-list-item.component.html',
|
||||
styleUrls: ['purchase-options-list-item.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
UiQuantityDropdownModule,
|
||||
UiSelectModule,
|
||||
ProductImageModule,
|
||||
IconComponent,
|
||||
UiSpinnerModule,
|
||||
ReactiveFormsModule,
|
||||
InputControlModule,
|
||||
FormsModule,
|
||||
ElementLifecycleModule,
|
||||
UiTooltipModule,
|
||||
UiCommonModule,
|
||||
ScaleContentComponent,
|
||||
OrderDeadlinePipeModule,
|
||||
],
|
||||
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);
|
||||
|
||||
private readonly _giftCardValidators = [
|
||||
Validators.required,
|
||||
Validators.min(1),
|
||||
Validators.max(GIFT_CARD_MAX_PRICE),
|
||||
Validators.pattern(PRICE_PATTERN),
|
||||
];
|
||||
|
||||
private readonly _defaultValidators = [
|
||||
Validators.required,
|
||||
Validators.min(0.01),
|
||||
Validators.max(999.99),
|
||||
Validators.pattern(PRICE_PATTERN),
|
||||
];
|
||||
|
||||
priceFormControl = new FormControl<string>(null);
|
||||
|
||||
manualVatFormControl = new FormControl<string>('', [Validators.required]);
|
||||
|
||||
selectedFormControl = new FormControl<boolean>(false);
|
||||
|
||||
availabilities$ = this.item$.pipe(switchMap((item) => this._store.getAvailabilitiesForItem$(item.id)));
|
||||
|
||||
availability$ = combineLatest([this.item$, this._store.purchaseOption$]).pipe(
|
||||
switchMap(([item, purchaseOption]) => this._store.getAvailabilityWithPurchaseOption$(item.id, purchaseOption)),
|
||||
map((availability) => availability?.data),
|
||||
);
|
||||
|
||||
price$ = this.item$.pipe(switchMap((item) => this._store.getPrice$(item.id)));
|
||||
|
||||
priceValue$ = this.price$.pipe(map((price) => price?.value?.value));
|
||||
|
||||
// Ticket #4074 analog zu Ticket #2244
|
||||
// take(2) um die Response des Katalogpreises und danach um die Response der OLAs abzuwarten
|
||||
// Logik gilt ausschließlich für Archivartikel
|
||||
setManualPrice$ = this.price$.pipe(
|
||||
take(2),
|
||||
map((price) => {
|
||||
// Logik nur beim Hinzufügen über Kaufoptionen, da über Ändern im Warenkorb die Info fehlt ob das jeweilige ShoppingCartItem ein Archivartikel ist oder nicht
|
||||
const features = this.item?.features as KeyValueDTOOfStringAndString[];
|
||||
if (!!features && Array.isArray(features)) {
|
||||
const isArchive = !!features?.find((feature) => feature?.enabled === true && feature?.key === 'ARC');
|
||||
return isArchive ? !price?.value?.value || price?.vat === undefined : false;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
|
||||
vats$ = this._store.vats$.pipe(shareReplay());
|
||||
|
||||
priceVat$ = this.price$.pipe(map((price) => price?.vat?.vatType));
|
||||
|
||||
canAddResult$ = this.item$.pipe(
|
||||
switchMap((item) => this._store.getCanAddResultForItemAndCurrentPurchaseOption$(item.id)),
|
||||
);
|
||||
|
||||
canEditPrice$ = this.item$.pipe(
|
||||
switchMap((item) => combineLatest([this.canAddResult$, this._store.getCanEditPrice$(item.id)])),
|
||||
map(([canAddResult, canEditPrice]) => canAddResult?.canAdd && canEditPrice),
|
||||
);
|
||||
|
||||
canEditVat$ = this.item$.pipe(
|
||||
switchMap((item) => combineLatest([this.canAddResult$, this._store.getCanEditVat$(item.id)])),
|
||||
map(([canAddResult, canEditVat]) => canAddResult?.canAdd && canEditVat),
|
||||
);
|
||||
|
||||
isGiftCard$ = this.item$.pipe(switchMap((item) => this._store.getIsGiftCard$(item.id)));
|
||||
|
||||
maxSelectableQuantity$ = combineLatest([this._store.purchaseOption$, this.availability$]).pipe(
|
||||
map(([purchaseOption, availability]) => {
|
||||
if (purchaseOption === 'in-store') {
|
||||
return availability?.inStock;
|
||||
}
|
||||
|
||||
return 999;
|
||||
}),
|
||||
startWith(999),
|
||||
);
|
||||
|
||||
showMaxAvailableQuantity$ = combineLatest([this._store.purchaseOption$, this.availability$, this.item$]).pipe(
|
||||
map(([purchaseOption, availability, item]) => {
|
||||
if (purchaseOption === 'pickup' && availability?.inStock < item.quantity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
|
||||
fetchingAvailabilities$ = this.item$
|
||||
.pipe(switchMap((item) => this._store.getFetchingAvailabilitiesForItem$(item.id)))
|
||||
.pipe(map((fetchingAvailabilities) => fetchingAvailabilities.length > 0));
|
||||
|
||||
showNotAvailable$ = combineLatest([this.availabilities$, this.fetchingAvailabilities$]).pipe(
|
||||
map(([availabilities, fetchingAvailabilities]) => {
|
||||
if (fetchingAvailabilities) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (availabilities.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
|
||||
// Ticket #4813 Artikeldetailseite // EVT-Datum bei der Kaufoption Rücklage anzeigen
|
||||
get isEVT() {
|
||||
// Einstieg über Kaufoptionen - Hier wird die Katalogavailability verwendet die am ItemDTO hängt
|
||||
if (isItemDTO(this.item, this._store.type)) {
|
||||
const firstDayOfSale = this.item?.catalogAvailability?.firstDayOfSale;
|
||||
return this.firstDayOfSaleBiggerThanToday(firstDayOfSale);
|
||||
}
|
||||
|
||||
// Einstieg über Warenkorb - Da wir hier ein ShoppingCartItemDTO haben werden hier die Katalogdaten neu gefetched und eine Katalogavailability drangehängt
|
||||
if (isShoppingCartItemDTO(this.item, this._store.type)) {
|
||||
const catalogAvailabilities = this._store.availabilities?.filter(
|
||||
(availability) => availability?.purchaseOption === 'catalog',
|
||||
);
|
||||
// #4813 Fix: Hier muss als Kriterium auf EAN statt itemId verglichen werden, denn ein ShoppingCartItemDTO (this.item) hat eine andere ItemId wie das ItemDTO (availability.itemId)
|
||||
const firstDayOfSale = catalogAvailabilities?.find(
|
||||
(availability) => this.item?.product?.ean === availability?.ean,
|
||||
)?.data?.firstDayOfSale;
|
||||
return this.firstDayOfSaleBiggerThanToday(firstDayOfSale);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
constructor(private _store: PurchaseOptionsStore) {}
|
||||
|
||||
firstDayOfSaleBiggerThanToday(firstDayOfSale: string): Date {
|
||||
if (!!firstDayOfSale && moment(firstDayOfSale)?.isAfter(moment())) {
|
||||
return moment(firstDayOfSale).toDate();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
onPriceInputInit(target: HTMLElement, overlayTrigger: UiOverlayTriggerDirective) {
|
||||
if (this._store.getIsGiftCard(this.item.id)) {
|
||||
overlayTrigger.open();
|
||||
}
|
||||
|
||||
target?.focus();
|
||||
}
|
||||
|
||||
// Wichtig für das korrekte Setzen des Preises an das Item für den Endpoint request
|
||||
parsePrice(value: string) {
|
||||
if (PRICE_PATTERN.test(value)) {
|
||||
return parseFloat(value.replace(',', '.'));
|
||||
}
|
||||
}
|
||||
|
||||
stringifyPrice(value: number) {
|
||||
if (!value) return '';
|
||||
|
||||
const price = value.toFixed(2).replace('.', ',');
|
||||
if (price.includes(',')) {
|
||||
const [integer, decimal] = price.split(',');
|
||||
return `${integer},${decimal.padEnd(2, '0')}`;
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initPriceValidatorSubscription();
|
||||
this.initQuantitySubscription();
|
||||
this.initPriceSubscription();
|
||||
this.initVatSubscription();
|
||||
this.initSelectedSubscription();
|
||||
}
|
||||
|
||||
ngOnChanges({ item }: SimpleChanges) {
|
||||
if (item) {
|
||||
this._itemSubject.next(this.item);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._itemSubject.complete();
|
||||
this._subscriptions.unsubscribe();
|
||||
}
|
||||
|
||||
initPriceValidatorSubscription() {
|
||||
const sub = this.item$.pipe(switchMap((item) => this._store.getIsGiftCard$(item.id))).subscribe((isGiftCard) => {
|
||||
if (isGiftCard) {
|
||||
this.priceFormControl.setValidators(this._giftCardValidators);
|
||||
} else {
|
||||
this.priceFormControl.setValidators(this._defaultValidators);
|
||||
}
|
||||
});
|
||||
|
||||
this._subscriptions.add(sub);
|
||||
}
|
||||
|
||||
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 = combineLatest([this.canEditPrice$, this.price$]).subscribe(([canEditPrice, price]) => {
|
||||
if (!canEditPrice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const priceStr = this.stringifyPrice(price?.value?.value);
|
||||
if (priceStr === '') return;
|
||||
|
||||
if (this.parsePrice(this.priceFormControl.value) !== price?.value?.value) {
|
||||
this.priceFormControl.setValue(priceStr);
|
||||
}
|
||||
});
|
||||
|
||||
const valueChangesSub = combineLatest([this.canEditPrice$, this.priceFormControl.valueChanges]).subscribe(
|
||||
([canEditPrice, value]) => {
|
||||
if (!canEditPrice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const price = this._store.getPrice(this.item.id);
|
||||
const parsedPrice = this.parsePrice(value);
|
||||
|
||||
if (!parsedPrice) {
|
||||
this._store.setPrice(this.item.id, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (price[this.item.id] !== parsedPrice) {
|
||||
this._store.setPrice(this.item.id, this.parsePrice(value));
|
||||
}
|
||||
},
|
||||
);
|
||||
this._subscriptions.add(sub);
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
|
||||
initVatSubscription() {
|
||||
const valueChangesSub = this.manualVatFormControl.valueChanges
|
||||
.pipe(withLatestFrom(this.vats$))
|
||||
.subscribe(([formVatType, vats]) => {
|
||||
const price = this._store.getPrice(this.item.id);
|
||||
|
||||
const vat = vats.find((vat) => vat?.vatType === Number(formVatType));
|
||||
|
||||
if (!vat) {
|
||||
this._store.setVat(this.item.id, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (price[this.item.id]?.vat?.vatType !== vat?.vatType) {
|
||||
this._store.setVat(this.item.id, vat);
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
computed,
|
||||
input,
|
||||
} 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 { ElementLifecycleModule } from '@shared/directives/element-lifecycle';
|
||||
import { UiCommonModule, UiOverlayTriggerDirective } from '@ui/common';
|
||||
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
|
||||
import { UiSpinnerModule } from '@ui/spinner';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
import { combineLatest, ReplaySubject, Subscription } from 'rxjs';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import {
|
||||
map,
|
||||
take,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
import { GIFT_CARD_MAX_PRICE, PRICE_PATTERN } from '../constants';
|
||||
import {
|
||||
Item,
|
||||
PurchaseOptionsStore,
|
||||
isItemDTO,
|
||||
isShoppingCartItemDTO,
|
||||
} from '../store';
|
||||
import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
|
||||
import { UiSelectModule } from '@ui/select';
|
||||
import { KeyValueDTOOfStringAndString } from '@generated/swagger/cat-search-api';
|
||||
import { ScaleContentComponent } from '@shared/components/scale-content';
|
||||
import moment from 'moment';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaOtherInfo } from '@isa/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-purchase-options-list-item',
|
||||
templateUrl: 'purchase-options-list-item.component.html',
|
||||
styleUrls: ['purchase-options-list-item.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
UiQuantityDropdownModule,
|
||||
UiSelectModule,
|
||||
ProductImageModule,
|
||||
IconComponent,
|
||||
UiSpinnerModule,
|
||||
ReactiveFormsModule,
|
||||
InputControlModule,
|
||||
FormsModule,
|
||||
ElementLifecycleModule,
|
||||
UiTooltipModule,
|
||||
UiCommonModule,
|
||||
ScaleContentComponent,
|
||||
OrderDeadlinePipeModule,
|
||||
NgIcon,
|
||||
],
|
||||
host: { class: 'shared-purchase-options-list-item' },
|
||||
providers: [provideIcons({ isaOtherInfo })],
|
||||
})
|
||||
export class PurchaseOptionsListItemComponent
|
||||
implements OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
private _subscriptions = new Subscription();
|
||||
|
||||
private _itemSubject = new ReplaySubject<Item>(1);
|
||||
|
||||
item = input.required<Item>();
|
||||
|
||||
get item$() {
|
||||
return this._itemSubject.asObservable();
|
||||
}
|
||||
|
||||
get product() {
|
||||
return this.item().product;
|
||||
}
|
||||
|
||||
redemptionPoints = computed(() => {
|
||||
const item = this.item();
|
||||
if (isShoppingCartItemDTO(item, this._store.type)) {
|
||||
return item.loyalty?.value;
|
||||
}
|
||||
|
||||
return item.redemptionPoints;
|
||||
});
|
||||
|
||||
showRedemptionPoints = toSignal(this._store.useRedemptionPoints$);
|
||||
|
||||
quantityFormControl = new FormControl<number>(null);
|
||||
|
||||
private readonly _giftCardValidators = [
|
||||
Validators.required,
|
||||
Validators.min(1),
|
||||
Validators.max(GIFT_CARD_MAX_PRICE),
|
||||
Validators.pattern(PRICE_PATTERN),
|
||||
];
|
||||
|
||||
private readonly _defaultValidators = [
|
||||
Validators.required,
|
||||
Validators.min(0.01),
|
||||
Validators.max(999.99),
|
||||
Validators.pattern(PRICE_PATTERN),
|
||||
];
|
||||
|
||||
priceFormControl = new FormControl<string>(null);
|
||||
|
||||
manualVatFormControl = new FormControl<string>('', [Validators.required]);
|
||||
|
||||
selectedFormControl = new FormControl<boolean>(false);
|
||||
|
||||
availabilities$ = this.item$.pipe(
|
||||
switchMap((item) => this._store.getAvailabilitiesForItem$(item.id)),
|
||||
);
|
||||
|
||||
availability$ = combineLatest([this.item$, this._store.purchaseOption$]).pipe(
|
||||
switchMap(([item, purchaseOption]) =>
|
||||
this._store.getAvailabilityWithPurchaseOption$(item.id, purchaseOption),
|
||||
),
|
||||
map((availability) => availability?.data),
|
||||
);
|
||||
|
||||
availability = toSignal(this.availability$);
|
||||
|
||||
price$ = this.item$.pipe(switchMap((item) => this._store.getPrice$(item.id)));
|
||||
|
||||
priceValue$ = this.price$.pipe(map((price) => price?.value?.value));
|
||||
|
||||
// Ticket #4074 analog zu Ticket #2244
|
||||
// take(2) um die Response des Katalogpreises und danach um die Response der OLAs abzuwarten
|
||||
// Logik gilt ausschließlich für Archivartikel
|
||||
setManualPrice$ = this.price$.pipe(
|
||||
take(2),
|
||||
map((price) => {
|
||||
// Logik nur beim Hinzufügen über Kaufoptionen, da über Ändern im Warenkorb die Info fehlt ob das jeweilige ShoppingCartItem ein Archivartikel ist oder nicht
|
||||
const features = this.item().features as KeyValueDTOOfStringAndString[];
|
||||
if (!!features && Array.isArray(features)) {
|
||||
const isArchive = !!features?.find(
|
||||
(feature) => feature?.enabled === true && feature?.key === 'ARC',
|
||||
);
|
||||
return isArchive
|
||||
? !price?.value?.value || price?.vat === undefined
|
||||
: false;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
|
||||
vats$ = this._store.vats$.pipe(shareReplay());
|
||||
|
||||
priceVat$ = this.price$.pipe(map((price) => price?.vat?.vatType));
|
||||
|
||||
canAddResult$ = this.item$.pipe(
|
||||
switchMap((item) =>
|
||||
this._store.getCanAddResultForItemAndCurrentPurchaseOption$(item.id),
|
||||
),
|
||||
);
|
||||
|
||||
canEditPrice$ = this.item$.pipe(
|
||||
switchMap((item) =>
|
||||
combineLatest([
|
||||
this.canAddResult$,
|
||||
this._store.getCanEditPrice$(item.id),
|
||||
]),
|
||||
),
|
||||
map(([canAddResult, canEditPrice]) => canAddResult?.canAdd && canEditPrice),
|
||||
);
|
||||
|
||||
canEditVat$ = this.item$.pipe(
|
||||
switchMap((item) =>
|
||||
combineLatest([this.canAddResult$, this._store.getCanEditVat$(item.id)]),
|
||||
),
|
||||
map(([canAddResult, canEditVat]) => canAddResult?.canAdd && canEditVat),
|
||||
);
|
||||
|
||||
isGiftCard$ = this.item$.pipe(
|
||||
switchMap((item) => this._store.getIsGiftCard$(item.id)),
|
||||
);
|
||||
|
||||
maxSelectableQuantity$ = combineLatest([
|
||||
this._store.purchaseOption$,
|
||||
this.availability$,
|
||||
]).pipe(
|
||||
map(([purchaseOption, availability]) => {
|
||||
if (purchaseOption === 'in-store') {
|
||||
return availability?.inStock;
|
||||
}
|
||||
|
||||
return 999;
|
||||
}),
|
||||
startWith(999),
|
||||
);
|
||||
|
||||
showMaxAvailableQuantity$ = combineLatest([
|
||||
this._store.purchaseOption$,
|
||||
this.availability$,
|
||||
this.item$,
|
||||
]).pipe(
|
||||
map(([purchaseOption, availability, item]) => {
|
||||
if (
|
||||
purchaseOption === 'pickup' &&
|
||||
availability?.inStock < item.quantity
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
|
||||
fetchingAvailabilities$ = this.item$
|
||||
.pipe(
|
||||
switchMap((item) =>
|
||||
this._store.getFetchingAvailabilitiesForItem$(item.id),
|
||||
),
|
||||
)
|
||||
.pipe(map((fetchingAvailabilities) => fetchingAvailabilities.length > 0));
|
||||
|
||||
showNotAvailable$ = combineLatest([
|
||||
this.availabilities$,
|
||||
this.fetchingAvailabilities$,
|
||||
]).pipe(
|
||||
map(([availabilities, fetchingAvailabilities]) => {
|
||||
if (fetchingAvailabilities) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (availabilities.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
|
||||
// Ticket #4813 Artikeldetailseite // EVT-Datum bei der Kaufoption Rücklage anzeigen
|
||||
get isEVT() {
|
||||
// Einstieg über Kaufoptionen - Hier wird die Katalogavailability verwendet die am ItemDTO hängt
|
||||
if (isItemDTO(this.item, this._store.type)) {
|
||||
const firstDayOfSale = this.item?.catalogAvailability?.firstDayOfSale;
|
||||
return this.firstDayOfSaleBiggerThanToday(firstDayOfSale);
|
||||
}
|
||||
|
||||
// Einstieg über Warenkorb - Da wir hier ein ShoppingCartItemDTO haben werden hier die Katalogdaten neu gefetched und eine Katalogavailability drangehängt
|
||||
if (isShoppingCartItemDTO(this.item, this._store.type)) {
|
||||
const catalogAvailabilities = this._store.availabilities?.filter(
|
||||
(availability) => availability?.purchaseOption === 'catalog',
|
||||
);
|
||||
// #4813 Fix: Hier muss als Kriterium auf EAN statt itemId verglichen werden, denn ein ShoppingCartItemDTO (this.item) hat eine andere ItemId wie das ItemDTO (availability.itemId)
|
||||
const firstDayOfSale = catalogAvailabilities?.find(
|
||||
(availability) => this.item().product?.ean === availability?.ean,
|
||||
)?.data?.firstDayOfSale;
|
||||
return this.firstDayOfSaleBiggerThanToday(firstDayOfSale);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
useRedemptionPoints = toSignal(this._store.useRedemptionPoints$);
|
||||
|
||||
purchaseOption = toSignal(this._store.purchaseOption$);
|
||||
|
||||
isReservePurchaseOption = computed(() => {
|
||||
return this.purchaseOption() === 'in-store';
|
||||
});
|
||||
|
||||
showLowStockMessage = computed(() => {
|
||||
return (
|
||||
this.useRedemptionPoints() &&
|
||||
this.isReservePurchaseOption() &&
|
||||
this.availability().inStock < 2
|
||||
);
|
||||
});
|
||||
|
||||
constructor(private _store: PurchaseOptionsStore) {}
|
||||
|
||||
firstDayOfSaleBiggerThanToday(firstDayOfSale: string): Date {
|
||||
if (!!firstDayOfSale && moment(firstDayOfSale)?.isAfter(moment())) {
|
||||
return moment(firstDayOfSale).toDate();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
onPriceInputInit(
|
||||
target: HTMLElement,
|
||||
overlayTrigger: UiOverlayTriggerDirective,
|
||||
) {
|
||||
if (this._store.getIsGiftCard(this.item().id)) {
|
||||
overlayTrigger.open();
|
||||
}
|
||||
|
||||
target?.focus();
|
||||
}
|
||||
|
||||
// Wichtig für das korrekte Setzen des Preises an das Item für den Endpoint request
|
||||
parsePrice(value: string) {
|
||||
if (PRICE_PATTERN.test(value)) {
|
||||
return parseFloat(value.replace(',', '.'));
|
||||
}
|
||||
}
|
||||
|
||||
stringifyPrice(value: number) {
|
||||
if (!value) return '';
|
||||
|
||||
const price = value.toFixed(2).replace('.', ',');
|
||||
if (price.includes(',')) {
|
||||
const [integer, decimal] = price.split(',');
|
||||
return `${integer},${decimal.padEnd(2, '0')}`;
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initPriceValidatorSubscription();
|
||||
this.initQuantitySubscription();
|
||||
this.initPriceSubscription();
|
||||
this.initVatSubscription();
|
||||
this.initSelectedSubscription();
|
||||
}
|
||||
|
||||
ngOnChanges({ item }: SimpleChanges) {
|
||||
if (item) {
|
||||
this._itemSubject.next(this.item());
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._itemSubject.complete();
|
||||
this._subscriptions.unsubscribe();
|
||||
}
|
||||
|
||||
initPriceValidatorSubscription() {
|
||||
const sub = this.item$
|
||||
.pipe(switchMap((item) => this._store.getIsGiftCard$(item.id)))
|
||||
.subscribe((isGiftCard) => {
|
||||
if (isGiftCard) {
|
||||
this.priceFormControl.setValidators(this._giftCardValidators);
|
||||
} else {
|
||||
this.priceFormControl.setValidators(this._defaultValidators);
|
||||
}
|
||||
});
|
||||
|
||||
this._subscriptions.add(sub);
|
||||
}
|
||||
|
||||
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 = combineLatest([this.canEditPrice$, this.price$]).subscribe(
|
||||
([canEditPrice, price]) => {
|
||||
if (!canEditPrice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const priceStr = this.stringifyPrice(price?.value?.value);
|
||||
if (priceStr === '') return;
|
||||
|
||||
if (
|
||||
this.parsePrice(this.priceFormControl.value) !== price?.value?.value
|
||||
) {
|
||||
this.priceFormControl.setValue(priceStr);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const valueChangesSub = combineLatest([
|
||||
this.canEditPrice$,
|
||||
this.priceFormControl.valueChanges,
|
||||
]).subscribe(([canEditPrice, value]) => {
|
||||
if (!canEditPrice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const price = this._store.getPrice(this.item().id);
|
||||
const parsedPrice = this.parsePrice(value);
|
||||
|
||||
if (!parsedPrice) {
|
||||
this._store.setPrice(this.item().id, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (price[this.item().id] !== parsedPrice) {
|
||||
this._store.setPrice(this.item().id, this.parsePrice(value));
|
||||
}
|
||||
});
|
||||
this._subscriptions.add(sub);
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
|
||||
initVatSubscription() {
|
||||
const valueChangesSub = this.manualVatFormControl.valueChanges
|
||||
.pipe(withLatestFrom(this.vats$))
|
||||
.subscribe(([formVatType, vats]) => {
|
||||
const price = this._store.getPrice(this.item().id);
|
||||
|
||||
const vat = vats.find((vat) => vat?.vatType === Number(formVatType));
|
||||
|
||||
if (!vat) {
|
||||
this._store.setVat(this.item().id, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (price[this.item().id]?.vat?.vatType !== vat?.vatType) {
|
||||
this._store.setVat(this.item().id, vat);
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface PurchaseOptionsModalData {
|
||||
tabId: number;
|
||||
shoppingCartId: number;
|
||||
type: ActionType;
|
||||
useRedemptionPoints?: boolean;
|
||||
items: Array<ItemDTO | ShoppingCartItemDTO>;
|
||||
pickupBranch?: BranchDTO;
|
||||
inStoreBranch?: BranchDTO;
|
||||
@@ -19,6 +20,7 @@ export interface PurchaseOptionsModalData {
|
||||
export interface PurchaseOptionsModalContext {
|
||||
shoppingCartId: number;
|
||||
type: ActionType;
|
||||
useRedemptionPoints: boolean;
|
||||
items: Array<ItemDTO | ShoppingCartItemDTO>;
|
||||
selectedCustomer?: Customer;
|
||||
selectedBranch?: BranchDTO;
|
||||
|
||||
@@ -21,6 +21,7 @@ export class PurchaseOptionsModalService {
|
||||
data: PurchaseOptionsModalData,
|
||||
): Promise<UiModalRef<string, PurchaseOptionsModalData>> {
|
||||
const context: PurchaseOptionsModalContext = {
|
||||
useRedemptionPoints: !!data.useRedemptionPoints,
|
||||
...data,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { PriceDTO, PriceValueDTO } from '@generated/swagger/checkout-api';
|
||||
import { PriceDTO } from '@generated/swagger/checkout-api';
|
||||
import {
|
||||
DEFAULT_PRICE_DTO,
|
||||
DEFAULT_PRICE_VALUE,
|
||||
GIFT_CARD_MAX_PRICE,
|
||||
GIFT_CARD_TYPE,
|
||||
PURCHASE_OPTIONS,
|
||||
} from '../constants';
|
||||
import { isArchive, isGiftCard, isItemDTO } from './purchase-options.helpers';
|
||||
@@ -22,6 +20,10 @@ export function getType(state: PurchaseOptionsState): ActionType {
|
||||
return state.type;
|
||||
}
|
||||
|
||||
export function getUseRedemptionPoints(state: PurchaseOptionsState): boolean {
|
||||
return state.useRedemptionPoints;
|
||||
}
|
||||
|
||||
export function getShoppingCartId(state: PurchaseOptionsState): number {
|
||||
return state.shoppingCartId;
|
||||
}
|
||||
@@ -337,7 +339,7 @@ export function getAvailabilityPriceForPurchaseOption(
|
||||
return (state) => {
|
||||
const item = getItems(state).find((item) => item.id === itemId);
|
||||
const type = getType(state);
|
||||
let availabilities = getAvailabilitiesForItem(itemId)(state);
|
||||
const availabilities = getAvailabilitiesForItem(itemId)(state);
|
||||
|
||||
let availability = availabilities.find(
|
||||
(availability) => availability.purchaseOption === purchaseOption,
|
||||
@@ -530,7 +532,7 @@ export function getAvailabilityWithPurchaseOption(
|
||||
purchaseOption: PurchaseOption,
|
||||
): (state: PurchaseOptionsState) => Availability {
|
||||
return (state) => {
|
||||
let availabilities = getAvailabilitiesForItem(itemId, true)(state);
|
||||
const availabilities = getAvailabilitiesForItem(itemId, true)(state);
|
||||
|
||||
let availability = availabilities.find(
|
||||
(availability) => availability.purchaseOption === purchaseOption,
|
||||
@@ -597,7 +599,7 @@ export function canContinue(state: PurchaseOptionsState): boolean {
|
||||
|
||||
const actionType = getType(state);
|
||||
|
||||
for (let item of items) {
|
||||
for (const item of items) {
|
||||
if (isGiftCard(item, actionType)) {
|
||||
const price = getPriceForPurchaseOption(item.id, purchaseOption)(state);
|
||||
if (
|
||||
@@ -647,7 +649,7 @@ export function canContinue(state: PurchaseOptionsState): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
let availability = getAvailabilityWithPurchaseOption(
|
||||
const availability = getAvailabilityWithPurchaseOption(
|
||||
item.id,
|
||||
purchaseOption,
|
||||
)(state);
|
||||
|
||||
@@ -35,4 +35,6 @@ export interface PurchaseOptionsState {
|
||||
customerFeatures: Record<string, string>;
|
||||
|
||||
fetchingAvailabilities: Array<FetchingAvailability>;
|
||||
|
||||
useRedemptionPoints: boolean;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { PurchaseOptionsModalContext } from '../purchase-options-modal.data';
|
||||
import { PurchaseOptionsService } from './purchase-options.service';
|
||||
import { PurchaseOptionsState } from './purchase-options.state';
|
||||
@@ -39,7 +40,7 @@ import { uniqueId } from 'lodash';
|
||||
import { VATDTO } from '@generated/swagger/oms-api';
|
||||
import { DomainCatalogService } from '@domain/catalog';
|
||||
import { ItemDTO } from '@generated/swagger/cat-search-api';
|
||||
import { OrderType } from '@isa/checkout/data-access';
|
||||
import { Loyalty, OrderType, Promotion } from '@isa/checkout/data-access';
|
||||
|
||||
@Injectable()
|
||||
export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
@@ -49,6 +50,12 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
|
||||
type$ = this.select(Selectors.getType);
|
||||
|
||||
get useRedemptionPoints() {
|
||||
return this.get(Selectors.getUseRedemptionPoints);
|
||||
}
|
||||
|
||||
useRedemptionPoints$ = this.select(Selectors.getUseRedemptionPoints);
|
||||
|
||||
get shoppingCartId() {
|
||||
return this.get(Selectors.getShoppingCartId);
|
||||
}
|
||||
@@ -149,6 +156,12 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
return this._service.getVats$();
|
||||
}
|
||||
|
||||
#logger = logger(() => ({
|
||||
class: 'PurchaseOptionsStore',
|
||||
module: 'purchase-options',
|
||||
importMetaUrl: import.meta.url,
|
||||
}));
|
||||
|
||||
constructor(
|
||||
private _service: PurchaseOptionsService,
|
||||
private _catalogService: DomainCatalogService,
|
||||
@@ -167,6 +180,7 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
canAddResults: [],
|
||||
customerFeatures: {},
|
||||
fetchingAvailabilities: [],
|
||||
useRedemptionPoints: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -198,6 +212,7 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
type,
|
||||
inStoreBranch,
|
||||
pickupBranch,
|
||||
useRedemptionPoints: showRedemptionPoints,
|
||||
}: PurchaseOptionsModalContext) {
|
||||
const defaultBranch = await this._service.fetchDefaultBranch().toPromise();
|
||||
|
||||
@@ -213,6 +228,7 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
this.patchState({
|
||||
type: type,
|
||||
shoppingCartId,
|
||||
useRedemptionPoints: showRedemptionPoints,
|
||||
items: items.map((item) => ({
|
||||
...item,
|
||||
quantity: item['quantity'] ?? 1,
|
||||
@@ -233,7 +249,7 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
private async _loadAvailabilities() {
|
||||
const items = this.items;
|
||||
|
||||
const promises: Promise<any>[] = [];
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
this._loadCatalogueAvailability();
|
||||
|
||||
@@ -258,7 +274,7 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
private async loadItemAvailability(itemId: number) {
|
||||
const item = this.items.find((item) => item.id === itemId);
|
||||
|
||||
const promises: Promise<any>[] = [];
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
const purchaseOption = this.purchaseOption;
|
||||
|
||||
@@ -344,7 +360,11 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
|
||||
this._checkAndSetAvailability(availability);
|
||||
} catch (err) {
|
||||
console.error('_loadPickupAvailability', err);
|
||||
this.#logger.error('Failed to load pickup availability', err, () => ({
|
||||
itemId: itemData.sourceId,
|
||||
quantity: itemData.quantity,
|
||||
branchId: branch?.id,
|
||||
}));
|
||||
}
|
||||
|
||||
this.removeFetchingAvailability({
|
||||
@@ -377,7 +397,11 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
|
||||
this._checkAndSetAvailability(availability);
|
||||
} catch (err) {
|
||||
console.error('_loadInStoreAvailability', err);
|
||||
this.#logger.error('Failed to load in-store availability', err, () => ({
|
||||
itemId: itemData.sourceId,
|
||||
quantity: itemData.quantity,
|
||||
branchId: branch?.id,
|
||||
}));
|
||||
}
|
||||
|
||||
this.removeFetchingAvailability({
|
||||
@@ -406,7 +430,10 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
|
||||
this._checkAndSetAvailability(availability);
|
||||
} catch (error) {
|
||||
console.error('_loadDeliveryAvailability', error);
|
||||
this.#logger.error('Failed to load delivery availability', error, () => ({
|
||||
itemId: itemData.sourceId,
|
||||
quantity: itemData.quantity,
|
||||
}));
|
||||
}
|
||||
|
||||
this.removeFetchingAvailability({
|
||||
@@ -436,7 +463,14 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
|
||||
this._checkAndSetAvailability(availability);
|
||||
} catch (error) {
|
||||
console.error('_loadDigDeliveryAvailability', error);
|
||||
this.#logger.error(
|
||||
'Failed to load digital delivery availability',
|
||||
error,
|
||||
() => ({
|
||||
itemId: itemData.sourceId,
|
||||
quantity: itemData.quantity,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
this.removeFetchingAvailability({
|
||||
@@ -466,7 +500,14 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
|
||||
this._checkAndSetAvailability(availability);
|
||||
} catch (error) {
|
||||
console.error('_loadB2bDeliveryAvailability', error);
|
||||
this.#logger.error(
|
||||
'Failed to load B2B delivery availability',
|
||||
error,
|
||||
() => ({
|
||||
itemId: itemData.sourceId,
|
||||
quantity: itemData.quantity,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
this.removeFetchingAvailability({
|
||||
@@ -496,7 +537,9 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
|
||||
this._checkAndSetAvailability(availability);
|
||||
} catch (error) {
|
||||
console.error('_loadDownloadAvailability', error);
|
||||
this.#logger.error('Failed to load download availability', error, () => ({
|
||||
itemId: itemData.sourceId,
|
||||
}));
|
||||
}
|
||||
|
||||
this.removeFetchingAvailability({
|
||||
@@ -524,7 +567,7 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
|
||||
this.patchState({ availabilities });
|
||||
} else {
|
||||
let availabilities = this.availabilities.filter(
|
||||
const availabilities = this.availabilities.filter(
|
||||
(a) =>
|
||||
!(
|
||||
a.itemId === availability.itemId &&
|
||||
@@ -600,7 +643,7 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
}
|
||||
|
||||
// Get Abholung availability
|
||||
let pickupAvailability = this.availabilities.find(
|
||||
const pickupAvailability = this.availabilities.find(
|
||||
(a) => a.itemId === item.id && a.purchaseOption === 'pickup',
|
||||
);
|
||||
if (pickupAvailability) {
|
||||
@@ -650,7 +693,11 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('_loadCanAdd', error);
|
||||
this.#logger.error('Failed to load canAdd results', error, () => ({
|
||||
orderType: key,
|
||||
shoppingCartId: this.shoppingCartId,
|
||||
itemCount: itemPayloads.length,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -795,7 +842,14 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
item.id,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('removeItem', error);
|
||||
this.#logger.error(
|
||||
'Failed to remove item from shopping cart',
|
||||
error,
|
||||
() => ({
|
||||
shoppingCartId: this.shoppingCartId,
|
||||
itemId: item.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,7 +917,7 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
return this.select(Selectors.getIsGiftCard(itemId));
|
||||
}
|
||||
|
||||
setPrice(itemId: number, value: number, manually: boolean = false) {
|
||||
setPrice(itemId: number, value: number, manually = false) {
|
||||
const prices = this.prices;
|
||||
let price = prices[itemId];
|
||||
|
||||
@@ -959,14 +1013,30 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
purchaseOption: PurchaseOption,
|
||||
): AddToShoppingCartDTO {
|
||||
const item = this.items.find((i) => i.id === itemId);
|
||||
|
||||
if (!isItemDTO(item, this.type)) {
|
||||
throw new Error('Invalid item');
|
||||
}
|
||||
|
||||
const price = this.getPriceForPurchaseOption(itemId, this.purchaseOption);
|
||||
const availability = this.getAvailabilityWithPurchaseOption(
|
||||
itemId,
|
||||
purchaseOption,
|
||||
);
|
||||
|
||||
if (!isItemDTO(item, this.type)) {
|
||||
throw new Error('Invalid item');
|
||||
let promotion: Promotion | null = { value: item.promoPoints };
|
||||
let loyalty: Loyalty | null = null;
|
||||
const redemptionPoints: number | null = item.redemptionPoints || null;
|
||||
|
||||
// "Lesepunkte einlösen" logic
|
||||
// If "Lesepunkte einlösen" is checked and item has redemption points, set price to 0 and remove promotion
|
||||
if (this.useRedemptionPoints) {
|
||||
// If loyalty is set, we need to remove promotion
|
||||
promotion = null;
|
||||
// Set loyalty points from item
|
||||
loyalty = { value: redemptionPoints };
|
||||
// Set price to 0
|
||||
price.value.value = 0;
|
||||
}
|
||||
|
||||
let destination: EntityDTOContainerOfDestinationDTO;
|
||||
@@ -990,13 +1060,8 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
catalogProductNumber:
|
||||
item?.product?.catalogProductNumber ?? String(item.id),
|
||||
},
|
||||
promotion: { value: item.promoPoints },
|
||||
// retailPrice: {
|
||||
// value: price.value.value,
|
||||
// currency: price.value.currency,
|
||||
// vatType: price.vat.vatType,
|
||||
// },
|
||||
// shopItemId: item.id ?? +item.product.catalogProductNumber,
|
||||
promotion,
|
||||
loyalty,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1005,14 +1070,20 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
purchaseOption: PurchaseOption,
|
||||
): UpdateShoppingCartItemDTO {
|
||||
const item = this.items.find((i) => i.id === itemId);
|
||||
|
||||
if (!isShoppingCartItemDTO(item, this.type)) {
|
||||
throw new Error('Invalid item');
|
||||
}
|
||||
const price = this.getPriceForPurchaseOption(itemId, this.purchaseOption);
|
||||
const availability = this.getAvailabilityWithPurchaseOption(
|
||||
itemId,
|
||||
purchaseOption,
|
||||
);
|
||||
|
||||
if (!isShoppingCartItemDTO(item, this.type)) {
|
||||
throw new Error('Invalid item');
|
||||
// If loyalty points is set we know it is a redemption item
|
||||
// we need to make sure we don't update the price
|
||||
if (this.useRedemptionPoints) {
|
||||
price.value.value = 0;
|
||||
}
|
||||
|
||||
let destination: EntityDTOContainerOfDestinationDTO;
|
||||
@@ -1030,11 +1101,6 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
quantity: item.quantity,
|
||||
availability: { ...availability.data, price },
|
||||
destination,
|
||||
// retailPrice: {
|
||||
// value: price.value.value,
|
||||
// currency: price.value.currency,
|
||||
// vatType: price.vat.vatType,
|
||||
// },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1051,7 +1117,7 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
);
|
||||
await this._service.addItemToShoppingCart(this.shoppingCartId, payloads);
|
||||
} else if (type === 'update') {
|
||||
const payloads = this.selectedItemIds.map((itemId) =>
|
||||
this.selectedItemIds.map((itemId) =>
|
||||
this.getUpdateShoppingCartItemDTOForItem(itemId, purchaseOption),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,129 +1,169 @@
|
||||
<div class="item-thumbnail">
|
||||
<a [routerLink]="productSearchDetailsPath" [queryParams]="{ main_qs: item?.product?.ean }">
|
||||
@if (item?.product?.ean | productImage; as thumbnailUrl) {
|
||||
<img loading="lazy" [src]="thumbnailUrl" [alt]="item?.product?.name" />
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="item-contributors">
|
||||
@for (contributor of contributors$ | async; track contributor; let last = $last) {
|
||||
<a
|
||||
[routerLink]="productSearchResultsPath"
|
||||
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
|
||||
(click)="$event?.stopPropagation()"
|
||||
>
|
||||
{{ contributor }}{{ last ? '' : ';' }}
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="item-title font-bold text-h2 mb-4"
|
||||
[class.text-h3]="item?.product?.name?.length >= 40 && isTablet"
|
||||
[class.text-p1]="item?.product?.name?.length >= 50 || !isTablet"
|
||||
[class.text-p2]="item?.product?.name?.length >= 60 && isTablet"
|
||||
[class.text-p3]="item?.product?.name?.length >= 100"
|
||||
>
|
||||
<a [routerLink]="productSearchDetailsPath" [queryParams]="{ main_qs: item?.product?.ean }">{{ item?.product?.name }}</a>
|
||||
</div>
|
||||
|
||||
@if (item?.product?.format && item?.product?.formatDetail) {
|
||||
<div class="item-format">
|
||||
@if (item?.product?.format !== '--') {
|
||||
<img
|
||||
src="assets/images/Icon_{{ item?.product?.format }}.svg"
|
||||
[alt]="item?.product?.formatDetail"
|
||||
/>
|
||||
}
|
||||
{{ item?.product?.formatDetail }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="item-info text-p2">
|
||||
<div class="mb-1">{{ item?.product?.manufacturer | substr: 25 }} | {{ item?.product?.ean }}</div>
|
||||
<div class="mb-1">
|
||||
{{ item?.product?.volume }}
|
||||
@if (item?.product?.volume && item?.product?.publicationDate) {
|
||||
<span>|</span>
|
||||
}
|
||||
{{ item?.product?.publicationDate | date }}
|
||||
</div>
|
||||
@if (notAvailable$ | async) {
|
||||
<div>
|
||||
<span class="text-brand item-date">Nicht verfügbar</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (refreshingAvailabilit$ | async) {
|
||||
<shared-skeleton-loader class="w-40"></shared-skeleton-loader>
|
||||
} @else {
|
||||
@if (orderType === 'Abholung') {
|
||||
<div class="item-date" [class.availability-changed]="estimatedShippingDateChanged$ | async">
|
||||
Abholung ab {{ item?.availability?.estimatedShippingDate | date }}
|
||||
</div>
|
||||
}
|
||||
@if (orderType === 'Versand' || orderType === 'B2B-Versand' || orderType === 'DIG-Versand') {
|
||||
<div
|
||||
class="item-date"
|
||||
[class.availability-changed]="estimatedShippingDateChanged$ | async"
|
||||
>
|
||||
@if (item?.availability?.estimatedDelivery) {
|
||||
Zustellung zwischen {{ (item?.availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }}
|
||||
und
|
||||
{{ (item?.availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
|
||||
} @else {
|
||||
Versand {{ item?.availability?.estimatedShippingDate | date }}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@if (olaError$ | async) {
|
||||
<div class="item-availability-message">Artikel nicht verfügbar</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="item-price-stock flex flex-col">
|
||||
<div class="text-p2 font-bold">{{ item?.availability?.price?.value?.value | currency: 'EUR' : 'code' }}</div>
|
||||
<div class="text-p2 font-normal">
|
||||
@if (!(isDummy$ | async)) {
|
||||
<ui-quantity-dropdown
|
||||
[ngModel]="item?.quantity"
|
||||
(ngModelChange)="onChangeQuantity($event)"
|
||||
[showSpinner]="(loadingOnQuantityChangeById$ | async) === item?.id"
|
||||
[disabled]="(loadingOnItemChangeById$ | async) === item?.id"
|
||||
[range]="quantityRange$ | async"
|
||||
></ui-quantity-dropdown>
|
||||
} @else {
|
||||
<div class="mt-2">{{ item?.quantity }}x</div>
|
||||
}
|
||||
</div>
|
||||
@if (quantityError) {
|
||||
<div class="quantity-error">
|
||||
{{ quantityError }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (orderType !== 'Download') {
|
||||
<div class="actions">
|
||||
@if (!(hasOrderType$ | async)) {
|
||||
<button
|
||||
[disabled]="(loadingOnQuantityChangeById$ | async) === item?.id || (loadingOnItemChangeById$ | async) === item?.id"
|
||||
(click)="onChangeItem()"
|
||||
>
|
||||
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id">Lieferweg auswählen</ui-spinner>
|
||||
</button>
|
||||
}
|
||||
@if (canEdit$ | async) {
|
||||
<button
|
||||
[disabled]="(loadingOnQuantityChangeById$ | async) === item?.id || (loadingOnItemChangeById$ | async) === item?.id"
|
||||
(click)="onChangeItem()"
|
||||
>
|
||||
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id">Lieferweg ändern</ui-spinner>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="item-thumbnail">
|
||||
<a
|
||||
[routerLink]="productSearchDetailsPath"
|
||||
[queryParams]="{ main_qs: item?.product?.ean }"
|
||||
>
|
||||
@if (item?.product?.ean | productImage; as thumbnailUrl) {
|
||||
<img loading="lazy" [src]="thumbnailUrl" [alt]="item?.product?.name" />
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="item-contributors">
|
||||
@for (
|
||||
contributor of contributors$ | async;
|
||||
track contributor;
|
||||
let last = $last
|
||||
) {
|
||||
<a
|
||||
[routerLink]="productSearchResultsPath"
|
||||
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
|
||||
(click)="$event?.stopPropagation()"
|
||||
>
|
||||
{{ contributor }}{{ last ? '' : ';' }}
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="item-title font-bold text-h2 mb-4"
|
||||
[class.text-h3]="item?.product?.name?.length >= 40 && isTablet"
|
||||
[class.text-p1]="item?.product?.name?.length >= 50 || !isTablet"
|
||||
[class.text-p2]="item?.product?.name?.length >= 60 && isTablet"
|
||||
[class.text-p3]="item?.product?.name?.length >= 100"
|
||||
>
|
||||
<a
|
||||
[routerLink]="productSearchDetailsPath"
|
||||
[queryParams]="{ main_qs: item?.product?.ean }"
|
||||
>{{ item?.product?.name }}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
@if (item?.product?.format && item?.product?.formatDetail) {
|
||||
<div class="item-format">
|
||||
@if (item?.product?.format !== '--') {
|
||||
<img
|
||||
src="assets/images/Icon_{{ item?.product?.format }}.svg"
|
||||
[alt]="item?.product?.formatDetail"
|
||||
/>
|
||||
}
|
||||
{{ item?.product?.formatDetail }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="item-info text-p2">
|
||||
<div class="mb-1">
|
||||
{{ item?.product?.manufacturer | substr: 25 }} | {{ item?.product?.ean }}
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
{{ item?.product?.volume }}
|
||||
@if (item?.product?.volume && item?.product?.publicationDate) {
|
||||
<span>|</span>
|
||||
}
|
||||
{{ item?.product?.publicationDate | date }}
|
||||
</div>
|
||||
@if (notAvailable$ | async) {
|
||||
<div>
|
||||
<span class="text-brand item-date">Nicht verfügbar</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (refreshingAvailabilit$ | async) {
|
||||
<shared-skeleton-loader class="w-40"></shared-skeleton-loader>
|
||||
} @else {
|
||||
@if (orderType === 'Abholung') {
|
||||
<div
|
||||
class="item-date"
|
||||
[class.availability-changed]="estimatedShippingDateChanged$ | async"
|
||||
>
|
||||
Abholung ab {{ item?.availability?.estimatedShippingDate | date }}
|
||||
</div>
|
||||
}
|
||||
@if (
|
||||
orderType === 'Versand' ||
|
||||
orderType === 'B2B-Versand' ||
|
||||
orderType === 'DIG-Versand'
|
||||
) {
|
||||
<div
|
||||
class="item-date"
|
||||
[class.availability-changed]="estimatedShippingDateChanged$ | async"
|
||||
>
|
||||
@if (item?.availability?.estimatedDelivery) {
|
||||
Zustellung zwischen
|
||||
{{
|
||||
(
|
||||
item?.availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.'
|
||||
)?.replace('.', '')
|
||||
}}
|
||||
und
|
||||
{{
|
||||
(
|
||||
item?.availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.'
|
||||
)?.replace('.', '')
|
||||
}}
|
||||
} @else {
|
||||
Versand {{ item?.availability?.estimatedShippingDate | date }}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (olaError$ | async) {
|
||||
<div class="item-availability-message">Artikel nicht verfügbar</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="item-price-stock flex flex-col">
|
||||
<div class="text-p2 font-bold">
|
||||
{{ item?.availability?.price?.value?.value | currency: 'EUR' : 'code' }}
|
||||
</div>
|
||||
<div class="text-p2 font-normal">
|
||||
@if (!(isDummy$ | async)) {
|
||||
<ui-quantity-dropdown
|
||||
[ngModel]="item?.quantity"
|
||||
(ngModelChange)="onChangeQuantity($event)"
|
||||
[showSpinner]="(loadingOnQuantityChangeById$ | async) === item?.id"
|
||||
[disabled]="(loadingOnItemChangeById$ | async) === item?.id"
|
||||
[range]="quantityRange$ | async"
|
||||
></ui-quantity-dropdown>
|
||||
} @else {
|
||||
<div class="mt-2">{{ item?.quantity }}x</div>
|
||||
}
|
||||
</div>
|
||||
@if (quantityError) {
|
||||
<div class="quantity-error">
|
||||
{{ quantityError }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (orderType !== 'Download') {
|
||||
<div class="actions">
|
||||
@if (!(hasOrderType$ | async)) {
|
||||
<button
|
||||
[disabled]="
|
||||
(loadingOnQuantityChangeById$ | async) === item?.id ||
|
||||
(loadingOnItemChangeById$ | async) === item?.id
|
||||
"
|
||||
(click)="onChangeItem()"
|
||||
>
|
||||
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id"
|
||||
>Lieferweg auswählen</ui-spinner
|
||||
>
|
||||
</button>
|
||||
}
|
||||
@if (canEdit$ | async) {
|
||||
<button
|
||||
[disabled]="
|
||||
(loadingOnQuantityChangeById$ | async) === item?.id ||
|
||||
(loadingOnItemChangeById$ | async) === item?.id
|
||||
"
|
||||
(click)="onChangeItem()"
|
||||
>
|
||||
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id"
|
||||
>Lieferweg ändern</ui-spinner
|
||||
>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,255 +1,298 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
NgZone,
|
||||
OnInit,
|
||||
Output,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { ProductCatalogNavigationService } from '@shared/services/navigation';
|
||||
import { ItemType, ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
|
||||
export interface ShoppingCartItemComponentState {
|
||||
item: ShoppingCartItemDTO;
|
||||
orderType: string;
|
||||
loadingOnItemChangeById?: number;
|
||||
loadingOnQuantityChangeById?: number;
|
||||
refreshingAvailability: boolean;
|
||||
sscChanged: boolean;
|
||||
sscTextChanged: boolean;
|
||||
estimatedShippingDateChanged: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'page-shopping-cart-item',
|
||||
templateUrl: 'shopping-cart-item.component.html',
|
||||
styleUrls: ['shopping-cart-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class ShoppingCartItemComponent extends ComponentStore<ShoppingCartItemComponentState> implements OnInit {
|
||||
private _zone = inject(NgZone);
|
||||
|
||||
@Output() changeItem = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO }>();
|
||||
@Output() changeDummyItem = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO }>();
|
||||
@Output() changeQuantity = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO; quantity: number }>();
|
||||
|
||||
@Input()
|
||||
get item() {
|
||||
return this.get((s) => s.item);
|
||||
}
|
||||
set item(item: ShoppingCartItemDTO) {
|
||||
if (this.item !== item) {
|
||||
this.patchState({ item });
|
||||
}
|
||||
}
|
||||
readonly item$ = this.select((s) => s.item);
|
||||
|
||||
readonly contributors$ = this.item$.pipe(
|
||||
map((item) => item?.product?.contributors?.split(';').map((val) => val.trim())),
|
||||
);
|
||||
|
||||
@Input()
|
||||
get orderType() {
|
||||
return this.get((s) => s.orderType);
|
||||
}
|
||||
set orderType(orderType: string) {
|
||||
if (this.orderType !== orderType) {
|
||||
this.patchState({ orderType });
|
||||
}
|
||||
}
|
||||
readonly orderType$ = this.select((s) => s.orderType);
|
||||
|
||||
@Input()
|
||||
get loadingOnItemChangeById() {
|
||||
return this.get((s) => s.loadingOnItemChangeById);
|
||||
}
|
||||
set loadingOnItemChangeById(loadingOnItemChangeById: number) {
|
||||
if (this.loadingOnItemChangeById !== loadingOnItemChangeById) {
|
||||
this.patchState({ loadingOnItemChangeById });
|
||||
}
|
||||
}
|
||||
readonly loadingOnItemChangeById$ = this.select((s) => s.loadingOnItemChangeById).pipe(shareReplay());
|
||||
|
||||
@Input()
|
||||
get loadingOnQuantityChangeById() {
|
||||
return this.get((s) => s.loadingOnQuantityChangeById);
|
||||
}
|
||||
set loadingOnQuantityChangeById(loadingOnQuantityChangeById: number) {
|
||||
if (this.loadingOnQuantityChangeById !== loadingOnQuantityChangeById) {
|
||||
this.patchState({ loadingOnQuantityChangeById });
|
||||
}
|
||||
}
|
||||
readonly loadingOnQuantityChangeById$ = this.select((s) => s.loadingOnQuantityChangeById).pipe(shareReplay());
|
||||
|
||||
@Input()
|
||||
quantityError: string;
|
||||
|
||||
isDummy$ = this.item$.pipe(
|
||||
map((item) => item?.availability?.supplyChannel === 'MANUALLY'),
|
||||
shareReplay(),
|
||||
);
|
||||
hasOrderType$ = this.orderType$.pipe(
|
||||
map((orderType) => orderType !== undefined),
|
||||
shareReplay(),
|
||||
);
|
||||
|
||||
canEdit$ = combineLatest([this.isDummy$, this.hasOrderType$, this.item$]).pipe(
|
||||
map(([isDummy, hasOrderType, item]) => {
|
||||
if (item.itemType === (66560 as ItemType)) {
|
||||
return false;
|
||||
}
|
||||
return isDummy || hasOrderType;
|
||||
}),
|
||||
);
|
||||
|
||||
quantityRange$ = combineLatest([this.orderType$, this.item$]).pipe(
|
||||
map(([orderType, item]) => (orderType === 'Rücklage' ? item.availability?.inStock : 999)),
|
||||
);
|
||||
|
||||
isDownloadAvailable$ = combineLatest([this.item$, this.orderType$]).pipe(
|
||||
filter(([_, orderType]) => orderType === 'Download'),
|
||||
switchMap(([item]) =>
|
||||
this.availabilityService.getDownloadAvailability({
|
||||
item: { ean: item.product.ean, price: item.availability.price, itemId: +item.product.catalogProductNumber },
|
||||
}),
|
||||
),
|
||||
map((availability) => availability && this.availabilityService.isAvailable({ availability })),
|
||||
);
|
||||
|
||||
olaError$ = this.checkoutService
|
||||
.getOlaErrors({ processId: this.application.activatedProcessId })
|
||||
.pipe(map((ids) => ids?.find((id) => id === this.item.id)));
|
||||
|
||||
get productSearchResultsPath() {
|
||||
return this._productNavigationService.getArticleSearchResultsPath(this.application.activatedProcessId).path;
|
||||
}
|
||||
|
||||
get productSearchDetailsPath() {
|
||||
return this._productNavigationService.getArticleDetailsPathByEan({
|
||||
processId: this.application.activatedProcessId,
|
||||
ean: this.item?.product?.ean,
|
||||
}).path;
|
||||
}
|
||||
|
||||
get isTablet() {
|
||||
return this._environment.matchTablet();
|
||||
}
|
||||
|
||||
refreshingAvailabilit$ = this.select((s) => s.refreshingAvailability);
|
||||
|
||||
sscChanged$ = this.select((s) => s.sscChanged || s.sscTextChanged);
|
||||
|
||||
estimatedShippingDateChanged$ = this.select((s) => s.estimatedShippingDateChanged);
|
||||
|
||||
notAvailable$ = this.item$.pipe(
|
||||
map((item) => {
|
||||
const availability = item?.availability;
|
||||
|
||||
if (availability.availabilityType === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (availability.inStock && item.quantity > availability.inStock) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !this.availabilityService.isAvailable({ availability });
|
||||
}),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private availabilityService: DomainAvailabilityService,
|
||||
private checkoutService: DomainCheckoutService,
|
||||
public application: ApplicationService,
|
||||
private _productNavigationService: ProductCatalogNavigationService,
|
||||
private _environment: EnvironmentService,
|
||||
private _cdr: ChangeDetectorRef,
|
||||
) {
|
||||
super({
|
||||
item: undefined,
|
||||
orderType: '',
|
||||
refreshingAvailability: false,
|
||||
sscChanged: false,
|
||||
sscTextChanged: false,
|
||||
estimatedShippingDateChanged: false,
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
async onChangeItem() {
|
||||
const isDummy = await this.isDummy$.pipe(first()).toPromise();
|
||||
isDummy
|
||||
? this.changeDummyItem.emit({ shoppingCartItem: this.item })
|
||||
: this.changeItem.emit({ shoppingCartItem: this.item });
|
||||
}
|
||||
|
||||
onChangeQuantity(quantity: number) {
|
||||
this.changeQuantity.emit({ shoppingCartItem: this.item, quantity });
|
||||
}
|
||||
|
||||
async refreshAvailability() {
|
||||
const currentAvailability = cloneDeep(this.item.availability);
|
||||
|
||||
try {
|
||||
this.patchRefreshingAvailability(true);
|
||||
this._cdr.markForCheck();
|
||||
const availability = await this.checkoutService.refreshAvailability({
|
||||
processId: this.application.activatedProcessId,
|
||||
shoppingCartItemId: this.item.id,
|
||||
});
|
||||
|
||||
if (currentAvailability.ssc !== availability.ssc) {
|
||||
this.sscChanged();
|
||||
}
|
||||
if (currentAvailability.sscText !== availability.sscText) {
|
||||
this.ssctextChanged();
|
||||
}
|
||||
if (
|
||||
moment(currentAvailability.estimatedShippingDate)
|
||||
.startOf('day')
|
||||
.diff(moment(availability.estimatedShippingDate).startOf('day'))
|
||||
) {
|
||||
this.estimatedShippingDateChanged();
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
this.patchRefreshingAvailability(false);
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
patchRefreshingAvailability(value: boolean) {
|
||||
this._zone.run(() => {
|
||||
this.patchState({ refreshingAvailability: value });
|
||||
this._cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ssctextChanged() {
|
||||
this.patchState({ sscTextChanged: true });
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
sscChanged() {
|
||||
this.patchState({ sscChanged: true });
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
estimatedShippingDateChanged() {
|
||||
this.patchState({ estimatedShippingDateChanged: true });
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
NgZone,
|
||||
OnInit,
|
||||
Output,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { ProductCatalogNavigationService } from '@shared/services/navigation';
|
||||
import { ItemType, ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
|
||||
export interface ShoppingCartItemComponentState {
|
||||
item: ShoppingCartItemDTO;
|
||||
orderType: string;
|
||||
loadingOnItemChangeById?: number;
|
||||
loadingOnQuantityChangeById?: number;
|
||||
refreshingAvailability: boolean;
|
||||
sscChanged: boolean;
|
||||
sscTextChanged: boolean;
|
||||
estimatedShippingDateChanged: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'page-shopping-cart-item',
|
||||
templateUrl: 'shopping-cart-item.component.html',
|
||||
styleUrls: ['shopping-cart-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class ShoppingCartItemComponent
|
||||
extends ComponentStore<ShoppingCartItemComponentState>
|
||||
implements OnInit
|
||||
{
|
||||
private _zone = inject(NgZone);
|
||||
|
||||
@Output() changeItem = new EventEmitter<{
|
||||
shoppingCartItem: ShoppingCartItemDTO;
|
||||
}>();
|
||||
@Output() changeDummyItem = new EventEmitter<{
|
||||
shoppingCartItem: ShoppingCartItemDTO;
|
||||
}>();
|
||||
@Output() changeQuantity = new EventEmitter<{
|
||||
shoppingCartItem: ShoppingCartItemDTO;
|
||||
quantity: number;
|
||||
}>();
|
||||
|
||||
@Input()
|
||||
get item() {
|
||||
return this.get((s) => s.item);
|
||||
}
|
||||
set item(item: ShoppingCartItemDTO) {
|
||||
if (this.item !== item) {
|
||||
this.patchState({ item });
|
||||
}
|
||||
}
|
||||
readonly item$ = this.select((s) => s.item);
|
||||
|
||||
readonly contributors$ = this.item$.pipe(
|
||||
map((item) =>
|
||||
item?.product?.contributors?.split(';').map((val) => val.trim()),
|
||||
),
|
||||
);
|
||||
|
||||
get showLoyaltyValue() {
|
||||
return this.item?.loyalty?.value > 0;
|
||||
}
|
||||
|
||||
@Input()
|
||||
get orderType() {
|
||||
return this.get((s) => s.orderType);
|
||||
}
|
||||
set orderType(orderType: string) {
|
||||
if (this.orderType !== orderType) {
|
||||
this.patchState({ orderType });
|
||||
}
|
||||
}
|
||||
readonly orderType$ = this.select((s) => s.orderType);
|
||||
|
||||
@Input()
|
||||
get loadingOnItemChangeById() {
|
||||
return this.get((s) => s.loadingOnItemChangeById);
|
||||
}
|
||||
set loadingOnItemChangeById(loadingOnItemChangeById: number) {
|
||||
if (this.loadingOnItemChangeById !== loadingOnItemChangeById) {
|
||||
this.patchState({ loadingOnItemChangeById });
|
||||
}
|
||||
}
|
||||
readonly loadingOnItemChangeById$ = this.select(
|
||||
(s) => s.loadingOnItemChangeById,
|
||||
).pipe(shareReplay());
|
||||
|
||||
@Input()
|
||||
get loadingOnQuantityChangeById() {
|
||||
return this.get((s) => s.loadingOnQuantityChangeById);
|
||||
}
|
||||
set loadingOnQuantityChangeById(loadingOnQuantityChangeById: number) {
|
||||
if (this.loadingOnQuantityChangeById !== loadingOnQuantityChangeById) {
|
||||
this.patchState({ loadingOnQuantityChangeById });
|
||||
}
|
||||
}
|
||||
readonly loadingOnQuantityChangeById$ = this.select(
|
||||
(s) => s.loadingOnQuantityChangeById,
|
||||
).pipe(shareReplay());
|
||||
|
||||
@Input()
|
||||
quantityError: string;
|
||||
|
||||
isDummy$ = this.item$.pipe(
|
||||
map((item) => item?.availability?.supplyChannel === 'MANUALLY'),
|
||||
shareReplay(),
|
||||
);
|
||||
hasOrderType$ = this.orderType$.pipe(
|
||||
map((orderType) => orderType !== undefined),
|
||||
shareReplay(),
|
||||
);
|
||||
|
||||
canEdit$ = combineLatest([
|
||||
this.isDummy$,
|
||||
this.hasOrderType$,
|
||||
this.item$,
|
||||
]).pipe(
|
||||
map(([isDummy, hasOrderType, item]) => {
|
||||
if (item.itemType === (66560 as ItemType)) {
|
||||
return false;
|
||||
}
|
||||
return isDummy || hasOrderType;
|
||||
}),
|
||||
);
|
||||
|
||||
quantityRange$ = combineLatest([this.orderType$, this.item$]).pipe(
|
||||
map(([orderType, item]) =>
|
||||
orderType === 'Rücklage' ? item.availability?.inStock : 999,
|
||||
),
|
||||
);
|
||||
|
||||
isDownloadAvailable$ = combineLatest([this.item$, this.orderType$]).pipe(
|
||||
filter(([, orderType]) => orderType === 'Download'),
|
||||
switchMap(([item]) =>
|
||||
this.availabilityService.getDownloadAvailability({
|
||||
item: {
|
||||
ean: item.product.ean,
|
||||
price: item.availability.price,
|
||||
itemId: +item.product.catalogProductNumber,
|
||||
},
|
||||
}),
|
||||
),
|
||||
map(
|
||||
(availability) =>
|
||||
availability && this.availabilityService.isAvailable({ availability }),
|
||||
),
|
||||
);
|
||||
|
||||
olaError$ = this.checkoutService
|
||||
.getOlaErrors({ processId: this.application.activatedProcessId })
|
||||
.pipe(map((ids) => ids?.find((id) => id === this.item.id)));
|
||||
|
||||
get productSearchResultsPath() {
|
||||
return this._productNavigationService.getArticleSearchResultsPath(
|
||||
this.application.activatedProcessId,
|
||||
).path;
|
||||
}
|
||||
|
||||
get productSearchDetailsPath() {
|
||||
return this._productNavigationService.getArticleDetailsPathByEan({
|
||||
processId: this.application.activatedProcessId,
|
||||
ean: this.item?.product?.ean,
|
||||
}).path;
|
||||
}
|
||||
|
||||
get isTablet() {
|
||||
return this._environment.matchTablet();
|
||||
}
|
||||
|
||||
refreshingAvailabilit$ = this.select((s) => s.refreshingAvailability);
|
||||
|
||||
sscChanged$ = this.select((s) => s.sscChanged || s.sscTextChanged);
|
||||
|
||||
estimatedShippingDateChanged$ = this.select(
|
||||
(s) => s.estimatedShippingDateChanged,
|
||||
);
|
||||
|
||||
notAvailable$ = this.item$.pipe(
|
||||
map((item) => {
|
||||
const availability = item?.availability;
|
||||
|
||||
if (availability.availabilityType === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (availability.inStock && item.quantity > availability.inStock) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !this.availabilityService.isAvailable({ availability });
|
||||
}),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private availabilityService: DomainAvailabilityService,
|
||||
private checkoutService: DomainCheckoutService,
|
||||
public application: ApplicationService,
|
||||
private _productNavigationService: ProductCatalogNavigationService,
|
||||
private _environment: EnvironmentService,
|
||||
private _cdr: ChangeDetectorRef,
|
||||
) {
|
||||
super({
|
||||
item: undefined,
|
||||
orderType: '',
|
||||
refreshingAvailability: false,
|
||||
sscChanged: false,
|
||||
sscTextChanged: false,
|
||||
estimatedShippingDateChanged: false,
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// Component initialization
|
||||
}
|
||||
|
||||
async onChangeItem() {
|
||||
const isDummy = await this.isDummy$.pipe(first()).toPromise();
|
||||
if (isDummy) {
|
||||
this.changeDummyItem.emit({ shoppingCartItem: this.item });
|
||||
} else {
|
||||
this.changeItem.emit({ shoppingCartItem: this.item });
|
||||
}
|
||||
}
|
||||
|
||||
onChangeQuantity(quantity: number) {
|
||||
this.changeQuantity.emit({ shoppingCartItem: this.item, quantity });
|
||||
}
|
||||
|
||||
async refreshAvailability() {
|
||||
const currentAvailability = cloneDeep(this.item.availability);
|
||||
|
||||
try {
|
||||
this.patchRefreshingAvailability(true);
|
||||
this._cdr.markForCheck();
|
||||
const availability = await this.checkoutService.refreshAvailability({
|
||||
processId: this.application.activatedProcessId,
|
||||
shoppingCartItemId: this.item.id,
|
||||
});
|
||||
|
||||
if (currentAvailability.ssc !== availability.ssc) {
|
||||
this.sscChanged();
|
||||
}
|
||||
if (currentAvailability.sscText !== availability.sscText) {
|
||||
this.ssctextChanged();
|
||||
}
|
||||
if (
|
||||
moment(currentAvailability.estimatedShippingDate)
|
||||
.startOf('day')
|
||||
.diff(moment(availability.estimatedShippingDate).startOf('day'))
|
||||
) {
|
||||
this.estimatedShippingDateChanged();
|
||||
}
|
||||
} catch {
|
||||
// Error handling for availability refresh
|
||||
}
|
||||
|
||||
this.patchRefreshingAvailability(false);
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
patchRefreshingAvailability(value: boolean) {
|
||||
this._zone.run(() => {
|
||||
this.patchState({ refreshingAvailability: value });
|
||||
this._cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ssctextChanged() {
|
||||
this.patchState({ sscTextChanged: true });
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
sscChanged() {
|
||||
this.patchState({ sscChanged: true });
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
estimatedShippingDateChanged() {
|
||||
this.patchState({ estimatedShippingDateChanged: true });
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ import {
|
||||
combineLatest,
|
||||
isObservable,
|
||||
} from 'rxjs';
|
||||
import { map, switchMap, tap } from 'rxjs/operators';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { CheckoutMetadataService } from '@isa/checkout/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-process-bar-item',
|
||||
@@ -39,9 +40,22 @@ export class ShellProcessBarItemComponent
|
||||
implements OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
#tabService = inject(TabService);
|
||||
#checkoutMetadataService = inject(CheckoutMetadataService);
|
||||
|
||||
tab = computed(() => this.#tabService.entityMap()[this.process().id]);
|
||||
|
||||
tabEffect = effect(() => {
|
||||
console.log('tabEffect', this.tab());
|
||||
});
|
||||
|
||||
shoppingCartId = computed(() => {
|
||||
return this.#checkoutMetadataService.getShoppingCartId(this.process().id);
|
||||
});
|
||||
|
||||
shoppingCartIdEffect = effect(() => {
|
||||
console.log('shoppingCartIdEffect', this.shoppingCartId());
|
||||
});
|
||||
|
||||
private _process$ = new BehaviorSubject<ApplicationProcess>(undefined);
|
||||
|
||||
process$ = this._process$.asObservable();
|
||||
@@ -91,7 +105,7 @@ export class ShellProcessBarItemComponent
|
||||
|
||||
latestBreadcrumb$: Observable<Breadcrumb> = NEVER;
|
||||
|
||||
routerLink$: Observable<string[] | any[]> = NEVER;
|
||||
routerLink$: Observable<string[] | unknown[]> = NEVER;
|
||||
|
||||
queryParams$: Observable<object> = NEVER;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
EntityContainerSchema,
|
||||
AvailabilityDTOSchema,
|
||||
CampaignDTOSchema,
|
||||
LoyaltyDTOSchema,
|
||||
@@ -9,14 +8,14 @@ import {
|
||||
PriceSchema,
|
||||
EntityDTOContainerOfDestinationDTOSchema,
|
||||
ItemTypeSchema,
|
||||
PriceValueSchema,
|
||||
} from './base-schemas';
|
||||
|
||||
const AddToShoppingCartDTOSchema = z.object({
|
||||
const AddToShoppingCartDefaultSchema = z.object({
|
||||
availability: AvailabilityDTOSchema,
|
||||
campaign: CampaignDTOSchema,
|
||||
destination: EntityDTOContainerOfDestinationDTOSchema,
|
||||
itemType: ItemTypeSchema,
|
||||
loyalty: LoyaltyDTOSchema,
|
||||
product: ProductDTOSchema,
|
||||
promotion: PromotionDTOSchema,
|
||||
quantity: z.number().int().positive(),
|
||||
@@ -24,9 +23,34 @@ const AddToShoppingCartDTOSchema = z.object({
|
||||
shopItemId: z.number().int().positive().optional(),
|
||||
});
|
||||
|
||||
// When loyalty points are used the price value must be 0 and promotion is not allowed
|
||||
// and availability must not contain a price
|
||||
// and loyalty must be present
|
||||
const AddToShoppingCartWithRedemptionPointsSchema =
|
||||
AddToShoppingCartDefaultSchema.omit({
|
||||
availability: true,
|
||||
promotion: true,
|
||||
}).extend({
|
||||
availability: AvailabilityDTOSchema.unwrap()
|
||||
.omit({ price: true })
|
||||
.extend({
|
||||
price: PriceSchema.unwrap()
|
||||
.omit({ value: true })
|
||||
.extend({
|
||||
value: PriceValueSchema.unwrap().extend({ value: z.literal(0) }),
|
||||
}),
|
||||
}),
|
||||
loyalty: LoyaltyDTOSchema,
|
||||
});
|
||||
|
||||
const AddToShoppingCartSchema = z.union([
|
||||
AddToShoppingCartDefaultSchema,
|
||||
AddToShoppingCartWithRedemptionPointsSchema,
|
||||
]);
|
||||
|
||||
export const AddItemToShoppingCartParamsSchema = z.object({
|
||||
shoppingCartId: z.number().int().positive(),
|
||||
items: z.array(AddToShoppingCartDTOSchema).min(1),
|
||||
items: z.array(AddToShoppingCartSchema).min(1),
|
||||
});
|
||||
|
||||
export type AddItemToShoppingCartParams = z.infer<
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
EntityContainerSchema,
|
||||
AvailabilityDTOSchema,
|
||||
CampaignDTOSchema,
|
||||
LoyaltyDTOSchema,
|
||||
@@ -10,7 +9,7 @@ import {
|
||||
} from './base-schemas';
|
||||
import { UpdateShoppingCartItem } from '../models';
|
||||
|
||||
const UpdateShoppingCartItemParamsValueSchema = z.object({
|
||||
const UpdateShoppingCartItemParamsValueDefaultSchema = z.object({
|
||||
availability: AvailabilityDTOSchema,
|
||||
buyerComment: z.string().optional(),
|
||||
campaign: CampaignDTOSchema,
|
||||
@@ -22,6 +21,34 @@ const UpdateShoppingCartItemParamsValueSchema = z.object({
|
||||
specialComment: z.string().optional(),
|
||||
});
|
||||
|
||||
// When loyalty points are used the price value must be 0 and promotion is not allowed
|
||||
// and availability must not contain a price
|
||||
// and loyalty must be present
|
||||
const UpdateShoppingCartItemParamsValueWithRedemptionPointsSchema =
|
||||
UpdateShoppingCartItemParamsValueDefaultSchema.omit({
|
||||
availability: true,
|
||||
promotion: true,
|
||||
}).extend({
|
||||
availability: AvailabilityDTOSchema.unwrap()
|
||||
.omit({ price: true })
|
||||
.extend({
|
||||
price: PriceSchema.unwrap()
|
||||
.omit({ value: true })
|
||||
.extend({
|
||||
value: z.object({
|
||||
value: z.literal(0),
|
||||
currency: z.string(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
loyalty: LoyaltyDTOSchema,
|
||||
});
|
||||
|
||||
const UpdateShoppingCartItemParamsValueSchema = z.union([
|
||||
UpdateShoppingCartItemParamsValueDefaultSchema,
|
||||
UpdateShoppingCartItemParamsValueWithRedemptionPointsSchema,
|
||||
]);
|
||||
|
||||
export const UpdateShoppingCartItemParamsSchema = z.object({
|
||||
shoppingCartId: z.number().int().positive(),
|
||||
shoppingCartItemId: z.number().int().positive(),
|
||||
|
||||
@@ -28,7 +28,7 @@ export class CheckoutMetadataService {
|
||||
|
||||
setShoppingCartId(tabId: number, shoppingCartId: number | undefined) {
|
||||
this.#tabService.patchTabMetadata(tabId, {
|
||||
CHECKOUT_SHOPPING_CART_ID_METADATA_KEY: shoppingCartId,
|
||||
[CHECKOUT_SHOPPING_CART_ID_METADATA_KEY]: shoppingCartId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -80,8 +80,9 @@ export class UserStorageProvider implements StorageProvider {
|
||||
|
||||
@ValidateParam(0, z.string().min(1))
|
||||
get(key: string): unknown {
|
||||
console.log('Getting user state key:', key);
|
||||
return structuredClone(this.#state[key]);
|
||||
const data = structuredClone(this.#state[key]);
|
||||
console.log('Got user state data:', key, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
@ValidateParam(0, z.string().min(1))
|
||||
|
||||
70417
package-lock.json
generated
70417
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
268
package.json
268
package.json
@@ -1,138 +1,130 @@
|
||||
{
|
||||
"name": "hima",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "nx serve isa-app --ssl",
|
||||
"pretest": "npx trash-cli testresults",
|
||||
"test": "npx nx run-many --tuiAutoExit true -t test --exclude isa-app",
|
||||
"ci": "npx nx run-many -t test --exclude isa-app -c ci --tuiAutoExit true",
|
||||
"build": "nx build isa-app --configuration=development",
|
||||
"build-prod": "nx build isa-app --configuration=production",
|
||||
"lint": "nx lint",
|
||||
"e2e": "nx e2e",
|
||||
"generate:swagger": "nx run-many -t generate -p tag:generated,swagger",
|
||||
"fix:files:swagger": "node ./tools/fix-files.js generated/swagger",
|
||||
"prettier": "prettier --write .",
|
||||
"pretty-quick": "pretty-quick --staged",
|
||||
"prepare": "husky",
|
||||
"storybook": "npx nx run isa-app:storybook"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular-architects/ngrx-toolkit": "^20.4.0",
|
||||
"@angular/animations": "20.1.2",
|
||||
"@angular/cdk": "20.1.2",
|
||||
"@angular/common": "20.1.2",
|
||||
"@angular/compiler": "20.1.2",
|
||||
"@angular/core": "20.1.2",
|
||||
"@angular/forms": "20.1.2",
|
||||
"@angular/localize": "20.1.2",
|
||||
"@angular/platform-browser": "20.1.2",
|
||||
"@angular/platform-browser-dynamic": "20.1.2",
|
||||
"@angular/router": "20.1.2",
|
||||
"@angular/service-worker": "20.1.2",
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"@ng-icons/core": "32.0.0",
|
||||
"@ng-icons/material-icons": "32.0.0",
|
||||
"@ngrx/component-store": "^20.0.0",
|
||||
"@ngrx/effects": "^20.0.0",
|
||||
"@ngrx/entity": "^20.0.0",
|
||||
"@ngrx/operators": "^20.0.0",
|
||||
"@ngrx/signals": "^20.0.0",
|
||||
"@ngrx/store": "^20.0.0",
|
||||
"@ngrx/store-devtools": "^20.0.0",
|
||||
"angular-oauth2-oidc": "20.0.0",
|
||||
"angular-oauth2-oidc-jwks": "20.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"ng2-pdf-viewer": "^10.4.0",
|
||||
"ngx-matomo-client": "^8.0.0",
|
||||
"parse-duration": "^2.1.3",
|
||||
"rxjs": "~7.8.2",
|
||||
"scandit-web-datacapture-barcode": "^6.28.1",
|
||||
"scandit-web-datacapture-core": "^6.28.1",
|
||||
"tslib": "^2.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
"zod": "^3.24.2",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@analogjs/vite-plugin-angular": "1.19.1",
|
||||
"@analogjs/vitest-angular": "1.19.1",
|
||||
"@angular-devkit/build-angular": "20.1.1",
|
||||
"@angular-devkit/core": "20.1.1",
|
||||
"@angular-devkit/schematics": "20.1.1",
|
||||
"@angular/build": "20.1.1",
|
||||
"@angular/cli": "~20.1.0",
|
||||
"@angular/compiler-cli": "20.1.2",
|
||||
"@angular/language-service": "20.1.2",
|
||||
"@angular/pwa": "20.1.1",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@ngneat/spectator": "19.6.2",
|
||||
"@nx/angular": "21.3.2",
|
||||
"@nx/eslint": "21.3.2",
|
||||
"@nx/eslint-plugin": "21.3.2",
|
||||
"@nx/jest": "21.3.2",
|
||||
"@nx/js": "21.3.2",
|
||||
"@nx/storybook": "21.3.2",
|
||||
"@nx/vite": "21.3.2",
|
||||
"@nx/web": "21.3.2",
|
||||
"@nx/workspace": "21.3.2",
|
||||
"@schematics/angular": "20.1.1",
|
||||
"@storybook/addon-docs": "^9.0.11",
|
||||
"@storybook/angular": "^9.0.5",
|
||||
"@swc-node/register": "1.10.10",
|
||||
"@swc/core": "1.12.1",
|
||||
"@swc/helpers": "0.5.17",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/node": "18.16.9",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/utils": "^8.33.1",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/ui": "^3.1.1",
|
||||
"angular-eslint": "20.1.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-environment-node": "^29.7.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"jest-preset-angular": "14.6.0",
|
||||
"jiti": "2.4.2",
|
||||
"jsdom": "~22.1.0",
|
||||
"jsonc-eslint-parser": "^2.1.0",
|
||||
"ng-mocks": "14.13.5",
|
||||
"ng-packagr": "20.1.0",
|
||||
"ng-swagger-gen": "^2.3.1",
|
||||
"nx": "21.3.2",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-url": "~10.1.3",
|
||||
"prettier": "^3.5.2",
|
||||
"pretty-quick": "~4.0.0",
|
||||
"storybook": "^9.0.5",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "^8.33.1",
|
||||
"vite": "6.3.5",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/linux-x64": "^0.25.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.00.0",
|
||||
"npm": ">=10.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"jest-environment-jsdom": {
|
||||
"jsdom": "26.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "hima",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "nx serve isa-app --ssl",
|
||||
"pretest": "npx trash-cli testresults",
|
||||
"test": "npx nx run-many --tuiAutoExit true -t test --exclude isa-app",
|
||||
"ci": "npx nx run-many -t test --exclude isa-app -c ci --tuiAutoExit true",
|
||||
"build": "nx build isa-app --configuration=development",
|
||||
"build-prod": "nx build isa-app --configuration=production",
|
||||
"lint": "nx lint",
|
||||
"e2e": "nx e2e",
|
||||
"generate:swagger": "nx run-many -t generate -p tag:generated,swagger",
|
||||
"fix:files:swagger": "node ./tools/fix-files.js generated/swagger",
|
||||
"prettier": "prettier --write .",
|
||||
"pretty-quick": "pretty-quick --staged",
|
||||
"prepare": "husky",
|
||||
"storybook": "npx nx run isa-app:storybook"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular-architects/ngrx-toolkit": "^20.4.0",
|
||||
"@angular/animations": "20.1.2",
|
||||
"@angular/cdk": "20.1.2",
|
||||
"@angular/common": "20.1.2",
|
||||
"@angular/compiler": "20.1.2",
|
||||
"@angular/core": "20.1.2",
|
||||
"@angular/forms": "20.1.2",
|
||||
"@angular/localize": "20.1.2",
|
||||
"@angular/platform-browser": "20.1.2",
|
||||
"@angular/platform-browser-dynamic": "20.1.2",
|
||||
"@angular/router": "20.1.2",
|
||||
"@angular/service-worker": "20.1.2",
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"@ng-icons/core": "32.0.0",
|
||||
"@ng-icons/material-icons": "32.0.0",
|
||||
"@ngrx/component-store": "^20.0.0",
|
||||
"@ngrx/effects": "^20.0.0",
|
||||
"@ngrx/entity": "^20.0.0",
|
||||
"@ngrx/operators": "^20.0.0",
|
||||
"@ngrx/signals": "^20.0.0",
|
||||
"@ngrx/store": "^20.0.0",
|
||||
"@ngrx/store-devtools": "^20.0.0",
|
||||
"angular-oauth2-oidc": "20.0.0",
|
||||
"angular-oauth2-oidc-jwks": "20.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"ng2-pdf-viewer": "^10.4.0",
|
||||
"ngx-matomo-client": "^8.0.0",
|
||||
"parse-duration": "^2.1.3",
|
||||
"rxjs": "~7.8.2",
|
||||
"scandit-web-datacapture-barcode": "^6.28.1",
|
||||
"scandit-web-datacapture-core": "^6.28.1",
|
||||
"tslib": "^2.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
"zod": "^3.24.2",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@analogjs/vite-plugin-angular": "1.19.1",
|
||||
"@analogjs/vitest-angular": "1.19.1",
|
||||
"@angular-devkit/build-angular": "20.1.1",
|
||||
"@angular-devkit/core": "20.1.1",
|
||||
"@angular-devkit/schematics": "20.1.1",
|
||||
"@angular/build": "20.1.1",
|
||||
"@angular/cli": "~20.1.0",
|
||||
"@angular/compiler-cli": "20.1.2",
|
||||
"@angular/language-service": "20.1.2",
|
||||
"@angular/pwa": "20.1.1",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@ngneat/spectator": "19.6.2",
|
||||
"@nx/angular": "21.3.2",
|
||||
"@nx/eslint": "21.3.2",
|
||||
"@nx/eslint-plugin": "21.3.2",
|
||||
"@nx/jest": "21.3.2",
|
||||
"@nx/js": "21.3.2",
|
||||
"@nx/storybook": "21.3.2",
|
||||
"@nx/vite": "21.3.2",
|
||||
"@nx/web": "21.3.2",
|
||||
"@nx/workspace": "21.3.2",
|
||||
"@schematics/angular": "20.1.1",
|
||||
"@storybook/addon-docs": "^9.0.11",
|
||||
"@storybook/angular": "^9.0.5",
|
||||
"@swc-node/register": "1.10.10",
|
||||
"@swc/core": "1.12.1",
|
||||
"@swc/helpers": "0.5.17",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/node": "18.16.9",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/utils": "^8.33.1",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/ui": "^3.1.1",
|
||||
"angular-eslint": "20.1.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-environment-node": "^29.7.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"jest-preset-angular": "14.6.0",
|
||||
"jiti": "2.4.2",
|
||||
"jsdom": "~22.1.0",
|
||||
"jsonc-eslint-parser": "^2.1.0",
|
||||
"ng-mocks": "14.13.5",
|
||||
"ng-packagr": "20.1.0",
|
||||
"ng-swagger-gen": "^2.3.1",
|
||||
"nx": "21.3.2",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-url": "~10.1.3",
|
||||
"prettier": "^3.5.2",
|
||||
"pretty-quick": "~4.0.0",
|
||||
"storybook": "^9.0.5",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "^8.33.1",
|
||||
"vite": "6.3.5",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.00.0",
|
||||
"npm": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user