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