diff --git a/apps/isa-app/src/app/app.module.ts b/apps/isa-app/src/app/app.module.ts
index f1dcb5b21..df58b206e 100644
--- a/apps/isa-app/src/app/app.module.ts
+++ b/apps/isa-app/src/app/app.module.ts
@@ -112,7 +112,7 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
const auth = injector.get(AuthService);
try {
await auth.init();
- } catch (error) {
+ } catch {
statusElement.innerHTML = 'Authentifizierung wird durchgeführt...';
const strategy = injector.get(LoginStrategy);
await strategy.login();
@@ -137,7 +137,7 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
}
// Subscribe on Store changes and save to user storage
store.pipe(debounceTime(1000)).subscribe((state) => {
- userStorage.set('store', state);
+ userStorage.set('store', { ...state, version });
});
} catch (error) {
console.error('Error during app initialization', error);
diff --git a/apps/isa-app/src/app/guards/can-activate-customer.guard.ts b/apps/isa-app/src/app/guards/can-activate-customer.guard.ts
index df44356ae..61235bae9 100644
--- a/apps/isa-app/src/app/guards/can-activate-customer.guard.ts
+++ b/apps/isa-app/src/app/guards/can-activate-customer.guard.ts
@@ -1,205 +1,206 @@
-import { Injectable } from "@angular/core";
-import {
- ActivatedRouteSnapshot,
- Router,
- RouterStateSnapshot,
-} from "@angular/router";
-import { ApplicationProcess, ApplicationService } from "@core/application";
-import { DomainCheckoutService } from "@domain/checkout";
-import { logger } from "@isa/core/logging";
-import { CustomerSearchNavigation } from "@shared/services/navigation";
-import { first } from "rxjs/operators";
-
-@Injectable({ providedIn: "root" })
-export class CanActivateCustomerGuard {
- #logger = logger(() => ({
- context: "CanActivateCustomerGuard",
- tags: ["guard", "customer", "navigation"],
- }));
-
- constructor(
- private readonly _applicationService: ApplicationService,
- private readonly _checkoutService: DomainCheckoutService,
- private readonly _router: Router,
- private readonly _navigation: CustomerSearchNavigation,
- ) {}
-
- async canActivate(
- route: ActivatedRouteSnapshot,
- { url }: RouterStateSnapshot,
- ) {
- if (url.startsWith("/kunde/customer/search/")) {
- const processId = Date.now(); // Generate a new process ID
- // Extract parts before and after the pattern
- const parts = url.split("/kunde/customer/");
- if (parts.length === 2) {
- const prefix = parts[0] + "/kunde/";
- const suffix = "customer/" + parts[1];
-
- // Construct the new URL with process ID inserted
- const newUrl = `${prefix}${processId}/${suffix}`;
-
- this.#logger.info("Redirecting to URL with process ID", () => ({
- originalUrl: url,
- newUrl,
- processId,
- }));
-
- // Navigate to the new URL and prevent original navigation
- this._router.navigateByUrl(newUrl);
- return false;
- }
- }
-
- const processes = await this._applicationService
- .getProcesses$("customer")
- .pipe(first())
- .toPromise();
- const lastActivatedProcessId = (
- await this._applicationService
- .getLastActivatedProcessWithSectionAndType$("customer", "cart")
- .pipe(first())
- .toPromise()
- )?.id;
-
- const lastActivatedCartCheckoutProcessId = (
- await this._applicationService
- .getLastActivatedProcessWithSectionAndType$("customer", "cart-checkout")
- .pipe(first())
- .toPromise()
- )?.id;
-
- const lastActivatedGoodsOutProcessId = (
- await this._applicationService
- .getLastActivatedProcessWithSectionAndType$("customer", "goods-out")
- .pipe(first())
- .toPromise()
- )?.id;
-
- const activatedProcessId = await this._applicationService
- .getActivatedProcessId$()
- .pipe(first())
- .toPromise();
-
- // Darf nur reinkommen wenn der aktuell aktive Tab ein Bestellabschluss Tab ist
- if (
- !!lastActivatedCartCheckoutProcessId &&
- lastActivatedCartCheckoutProcessId === activatedProcessId
- ) {
- await this.fromCartCheckoutProcess(
- processes,
- lastActivatedCartCheckoutProcessId,
- );
- return false;
- } else if (
- !!lastActivatedGoodsOutProcessId &&
- lastActivatedGoodsOutProcessId === activatedProcessId
- ) {
- await this.fromGoodsOutProcess(processes, lastActivatedGoodsOutProcessId);
- return false;
- }
-
- if (!lastActivatedProcessId) {
- await this.fromCartProcess(processes);
- return false;
- } else {
- await this.navigateToDefaultRoute(lastActivatedProcessId);
- }
- return false;
- }
-
- async navigateToDefaultRoute(processId: number) {
- const route = this._navigation.defaultRoute({ processId });
-
- await this._router.navigate(route.path, { queryParams: route.queryParams });
- }
-
- // Bei offener Artikelsuche/Kundensuche und Klick auf Footer Kundensuche
- async fromCartProcess(processes: ApplicationProcess[]) {
- const newProcessId = Date.now();
- await this._applicationService.createProcess({
- id: newProcessId,
- type: "cart",
- section: "customer",
- name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === "cart"))}`,
- });
-
- await this.navigateToDefaultRoute(newProcessId);
- }
-
- // Bei offener Bestellbestätigung und Klick auf Footer Kundensuche
- async fromCartCheckoutProcess(
- processes: ApplicationProcess[],
- processId: number,
- ) {
- // Um alle Checkout Daten zu resetten die mit dem Prozess assoziiert sind
- this._checkoutService.removeProcess({ processId });
-
- // Ändere type cart-checkout zu cart
- this._applicationService.patchProcess(processId, {
- id: processId,
- type: "cart",
- section: "customer",
- name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === "cart"))}`,
- data: {},
- });
-
- // Navigation
- await this.navigateToDefaultRoute(processId);
- }
-
- // Bei offener Warenausgabe und Klick auf Footer Kundensuche
- async fromGoodsOutProcess(
- processes: ApplicationProcess[],
- processId: number,
- ) {
- const buyer = await this._checkoutService
- .getBuyer({ processId })
- .pipe(first())
- .toPromise();
- const customerFeatures = await this._checkoutService
- .getCustomerFeatures({ processId })
- .pipe(first())
- .toPromise();
- const name = buyer
- ? customerFeatures?.b2b
- ? buyer.organisation?.name
- ? buyer.organisation?.name
- : buyer.lastName
- : buyer.lastName
- : `Vorgang ${this.processNumber(processes.filter((process) => process.type === "cart"))}`;
-
- // Ändere type goods-out zu cart
- this._applicationService.patchProcess(processId, {
- id: processId,
- type: "cart",
- section: "customer",
- name,
- });
-
- // Navigation
- await this.navigateToDefaultRoute(processId);
- }
-
- processNumber(processes: ApplicationProcess[]) {
- const processNumbers = processes?.map((process) =>
- Number(process?.name?.replace(/\D/g, "")),
- );
- return !!processNumbers && processNumbers.length > 0
- ? this.findMissingNumber(processNumbers)
- : 1;
- }
-
- findMissingNumber(processNumbers: number[]) {
- for (
- let missingNumber = 1;
- missingNumber < Math.max(...processNumbers);
- missingNumber++
- ) {
- if (!processNumbers.find((number) => number === missingNumber)) {
- return missingNumber;
- }
- }
- return Math.max(...processNumbers) + 1;
- }
-}
+import { Injectable } from '@angular/core';
+import {
+ ActivatedRouteSnapshot,
+ Router,
+ RouterStateSnapshot,
+} from '@angular/router';
+import { ApplicationProcess, ApplicationService } from '@core/application';
+import { DomainCheckoutService } from '@domain/checkout';
+import { logger } from '@isa/core/logging';
+import { CustomerSearchNavigation } from '@shared/services/navigation';
+import { first } from 'rxjs/operators';
+
+@Injectable({ providedIn: 'root' })
+export class CanActivateCustomerGuard {
+ #logger = logger(() => ({
+ module: 'isa-app',
+ importMetaUrl: import.meta.url,
+ class: 'CanActivateCustomerGuard',
+ }));
+
+ constructor(
+ private readonly _applicationService: ApplicationService,
+ private readonly _checkoutService: DomainCheckoutService,
+ private readonly _router: Router,
+ private readonly _navigation: CustomerSearchNavigation,
+ ) {}
+
+ async canActivate(
+ route: ActivatedRouteSnapshot,
+ { url }: RouterStateSnapshot,
+ ) {
+ if (url.startsWith('/kunde/customer/search/')) {
+ const processId = Date.now(); // Generate a new process ID
+ // Extract parts before and after the pattern
+ const parts = url.split('/kunde/customer/');
+ if (parts.length === 2) {
+ const prefix = parts[0] + '/kunde/';
+ const suffix = 'customer/' + parts[1];
+
+ // Construct the new URL with process ID inserted
+ const newUrl = `${prefix}${processId}/${suffix}`;
+
+ this.#logger.info('Redirecting to URL with process ID', () => ({
+ originalUrl: url,
+ newUrl,
+ processId,
+ }));
+
+ // Navigate to the new URL and prevent original navigation
+ this._router.navigateByUrl(newUrl);
+ return false;
+ }
+ }
+
+ const processes = await this._applicationService
+ .getProcesses$('customer')
+ .pipe(first())
+ .toPromise();
+ const lastActivatedProcessId = (
+ await this._applicationService
+ .getLastActivatedProcessWithSectionAndType$('customer', 'cart')
+ .pipe(first())
+ .toPromise()
+ )?.id;
+
+ const lastActivatedCartCheckoutProcessId = (
+ await this._applicationService
+ .getLastActivatedProcessWithSectionAndType$('customer', 'cart-checkout')
+ .pipe(first())
+ .toPromise()
+ )?.id;
+
+ const lastActivatedGoodsOutProcessId = (
+ await this._applicationService
+ .getLastActivatedProcessWithSectionAndType$('customer', 'goods-out')
+ .pipe(first())
+ .toPromise()
+ )?.id;
+
+ const activatedProcessId = await this._applicationService
+ .getActivatedProcessId$()
+ .pipe(first())
+ .toPromise();
+
+ // Darf nur reinkommen wenn der aktuell aktive Tab ein Bestellabschluss Tab ist
+ if (
+ !!lastActivatedCartCheckoutProcessId &&
+ lastActivatedCartCheckoutProcessId === activatedProcessId
+ ) {
+ await this.fromCartCheckoutProcess(
+ processes,
+ lastActivatedCartCheckoutProcessId,
+ );
+ return false;
+ } else if (
+ !!lastActivatedGoodsOutProcessId &&
+ lastActivatedGoodsOutProcessId === activatedProcessId
+ ) {
+ await this.fromGoodsOutProcess(processes, lastActivatedGoodsOutProcessId);
+ return false;
+ }
+
+ if (!lastActivatedProcessId) {
+ await this.fromCartProcess(processes);
+ return false;
+ } else {
+ await this.navigateToDefaultRoute(lastActivatedProcessId);
+ }
+ return false;
+ }
+
+ async navigateToDefaultRoute(processId: number) {
+ const route = this._navigation.defaultRoute({ processId });
+
+ await this._router.navigate(route.path, { queryParams: route.queryParams });
+ }
+
+ // Bei offener Artikelsuche/Kundensuche und Klick auf Footer Kundensuche
+ async fromCartProcess(processes: ApplicationProcess[]) {
+ const newProcessId = Date.now();
+ await this._applicationService.createProcess({
+ id: newProcessId,
+ type: 'cart',
+ section: 'customer',
+ name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
+ });
+
+ await this.navigateToDefaultRoute(newProcessId);
+ }
+
+ // Bei offener Bestellbestätigung und Klick auf Footer Kundensuche
+ async fromCartCheckoutProcess(
+ processes: ApplicationProcess[],
+ processId: number,
+ ) {
+ // Um alle Checkout Daten zu resetten die mit dem Prozess assoziiert sind
+ this._checkoutService.removeProcess({ processId });
+
+ // Ändere type cart-checkout zu cart
+ this._applicationService.patchProcess(processId, {
+ id: processId,
+ type: 'cart',
+ section: 'customer',
+ name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
+ data: {},
+ });
+
+ // Navigation
+ await this.navigateToDefaultRoute(processId);
+ }
+
+ // Bei offener Warenausgabe und Klick auf Footer Kundensuche
+ async fromGoodsOutProcess(
+ processes: ApplicationProcess[],
+ processId: number,
+ ) {
+ const buyer = await this._checkoutService
+ .getBuyer({ processId })
+ .pipe(first())
+ .toPromise();
+ const customerFeatures = await this._checkoutService
+ .getCustomerFeatures({ processId })
+ .pipe(first())
+ .toPromise();
+ const name = buyer
+ ? customerFeatures?.b2b
+ ? buyer.organisation?.name
+ ? buyer.organisation?.name
+ : buyer.lastName
+ : buyer.lastName
+ : `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`;
+
+ // Ändere type goods-out zu cart
+ this._applicationService.patchProcess(processId, {
+ id: processId,
+ type: 'cart',
+ section: 'customer',
+ name,
+ });
+
+ // Navigation
+ await this.navigateToDefaultRoute(processId);
+ }
+
+ processNumber(processes: ApplicationProcess[]) {
+ const processNumbers = processes?.map((process) =>
+ Number(process?.name?.replace(/\D/g, '')),
+ );
+ return !!processNumbers && processNumbers.length > 0
+ ? this.findMissingNumber(processNumbers)
+ : 1;
+ }
+
+ findMissingNumber(processNumbers: number[]) {
+ for (
+ let missingNumber = 1;
+ missingNumber < Math.max(...processNumbers);
+ missingNumber++
+ ) {
+ if (!processNumbers.find((number) => number === missingNumber)) {
+ return missingNumber;
+ }
+ }
+ return Math.max(...processNumbers) + 1;
+ }
+}
diff --git a/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.html b/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.html
index ee19a4520..c0fb6593b 100644
--- a/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.html
+++ b/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.html
@@ -1,185 +1,268 @@
-
-
-
![]()
-
-
-
- {{ product?.contributors }}
-
-
- {{ product?.name }}
-
-
-
- {{ product?.formatDetail }}
-
-
- {{ product?.manufacturer }}
- @if (product?.manufacturer && product?.ean) {
- |
- }
- {{ product?.ean }}
-
-
- {{ product?.volume }}
- @if (product?.volume && product?.publicationDate) {
- |
- }
- {{ product?.publicationDate | date: 'dd. MMMM yyyy' }}
-
-
- @if ((availabilities$ | async)?.length) {
-
Verfügbar als
- }
- @for (availability of availabilities$ | async; track availability) {
-
-
- @switch (availability.purchaseOption) {
- @case ('delivery') {
-
- {{ availability.data.estimatedDelivery?.start | date: 'EE dd.MM.' }}
- -
- {{ availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.' }}
- }
- @case ('dig-delivery') {
-
- {{ availability.data.estimatedDelivery?.start | date: 'EE dd.MM.' }}
- -
- {{ availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.' }}
- }
- @case ('b2b-delivery') {
-
- {{ availability.data.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
- }
- @case ('pickup') {
-
- {{ availability.data.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
-
- {{ availability.data?.orderDeadline | orderDeadline }}
-
- }
- @case ('in-store') {
-
- {{ availability.data.inStock }}x
- @if (isEVT) {
- ab {{ isEVT | date: 'dd. MMMM yyyy' }}
- } @else {
- ab sofort
- }
- }
- @case ('download') {
-
- Download
- }
- }
-
-
- }
-
-
-
-
-
- @if (canEditVat$ | async) {
-
- @for (vat of vats$ | async; track vat) {
-
- }
-
- }
- @if (canEditPrice$ | async) {
-
-
- @if (priceFormControl?.invalid && priceFormControl?.dirty) {
-
- }
-
-
- EUR
- Preis ist ungültig
- Preis ist ungültig
- Preis ist ungültig
- Preis ist ungültig
-
- } @else {
- {{ priceValue$ | async | currency: 'EUR' : 'code' }}
- }
-
-
- Tragen Sie hier den
-
- Gutscheinbetrag ein.
-
-
-
-
-
- @if ((canAddResult$ | async)?.canAdd) {
-
- }
-
-
- @if (canAddResult$ | async; as canAddResult) {
- @if (!canAddResult.canAdd) {
-
- {{ canAddResult.message }}
-
- }
- }
-
- @if (showMaxAvailableQuantity$ | async) {
-
- {{ (availability$ | async)?.inStock }} Exemplare sofort lieferbar
-
- }
- @if (showNotAvailable$ | async) {
-
Derzeit nicht bestellbar
- }
-
-
-
+
+
+
![]()
+
+
+
+ {{ product?.contributors }}
+
+
+ {{ product?.name }}
+
+
+
+ {{ product?.formatDetail }}
+
+
+ {{ product?.manufacturer }}
+ @if (product?.manufacturer && product?.ean) {
+ |
+ }
+ {{ product?.ean }}
+
+
+ {{ product?.volume }}
+ @if (product?.volume && product?.publicationDate) {
+ |
+ }
+ {{ product?.publicationDate | date: 'dd. MMMM yyyy' }}
+
+
+ @if ((availabilities$ | async)?.length) {
+
Verfügbar als
+ }
+ @for (availability of availabilities$ | async; track availability) {
+
+
+ @switch (availability.purchaseOption) {
+ @case ('delivery') {
+
+ {{
+ availability.data.estimatedDelivery?.start | date: 'EE dd.MM.'
+ }}
+ -
+ {{
+ availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.'
+ }}
+ }
+ @case ('dig-delivery') {
+
+ {{
+ availability.data.estimatedDelivery?.start | date: 'EE dd.MM.'
+ }}
+ -
+ {{
+ availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.'
+ }}
+ }
+ @case ('b2b-delivery') {
+
+ {{
+ availability.data.estimatedShippingDate
+ | date: 'dd. MMMM yyyy'
+ }}
+ }
+ @case ('pickup') {
+
+ {{
+ availability.data.estimatedShippingDate
+ | date: 'dd. MMMM yyyy'
+ }}
+
+ {{ availability.data?.orderDeadline | orderDeadline }}
+
+ }
+ @case ('in-store') {
+
+ {{ availability.data.inStock }}x
+ @if (isEVT) {
+ ab {{ isEVT | date: 'dd. MMMM yyyy' }}
+ } @else {
+ ab sofort
+ }
+ }
+ @case ('download') {
+
+ Download
+ }
+ }
+
+
+ }
+
+
+
+
+
+ @if (showRedemptionPoints()) {
+ Einlösen für:
+ {{
+ redemptionPoints() * quantityFormControl.value
+ }}
+ Lesepunkte
+ } @else {
+ @if (canEditVat$ | async) {
+
+ @for (vat of vats$ | async; track vat) {
+
+ }
+
+ }
+ @if (canEditPrice$ | async) {
+
+
+ @if (priceFormControl?.invalid && priceFormControl?.dirty) {
+
+ }
+
+
+ EUR
+ Preis ist ungültig
+ Preis ist ungültig
+ Preis ist ungültig
+ Preis ist ungültig
+
+ } @else {
+ {{ priceValue$ | async | currency: 'EUR' : 'code' }}
+ }
+ }
+
+ Tragen Sie hier den
+
+ Gutscheinbetrag ein.
+
+
+
+
+
+ @if ((canAddResult$ | async)?.canAdd) {
+
+ }
+
+
+ @if (canAddResult$ | async; as canAddResult) {
+ @if (!canAddResult.canAdd) {
+
+ {{ canAddResult.message }}
+
+ }
+ }
+
+ @if (showMaxAvailableQuantity$ | async) {
+
+ {{ (availability$ | async)?.inStock }} Exemplare sofort lieferbar
+
+ }
+ @if (showNotAvailable$ | async) {
+
Derzeit nicht bestellbar
+ }
+
+
+
+@if (showLowStockMessage()) {
+
+
+
Geringer Bestand - Artikel holen vor Abschluss
+
+}
diff --git a/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.ts b/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.ts
index 68e76960e..3ee5d58ba 100644
--- a/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.ts
+++ b/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.ts
@@ -1,349 +1,467 @@
-import { CommonModule } from '@angular/common';
-import { Component, ChangeDetectionStrategy, Input, OnInit, OnDestroy, OnChanges, SimpleChanges } from '@angular/core';
-import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
-import { ProductImageModule } from '@cdn/product-image';
-import { InputControlModule } from '@shared/components/input-control';
-import { ElementLifecycleModule } from '@shared/directives/element-lifecycle';
-import { UiCommonModule, UiOverlayTriggerDirective } from '@ui/common';
-import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
-import { UiSpinnerModule } from '@ui/spinner';
-import { UiTooltipModule } from '@ui/tooltip';
-import { combineLatest, ReplaySubject, Subscription } from 'rxjs';
-import { IconComponent } from '@shared/components/icon';
-import { map, take, shareReplay, startWith, switchMap, withLatestFrom } from 'rxjs/operators';
-import { GIFT_CARD_MAX_PRICE, PRICE_PATTERN } from '../constants';
-import { Item, PurchaseOptionsStore, isItemDTO, isShoppingCartItemDTO } from '../store';
-import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
-import { UiSelectModule } from '@ui/select';
-import { KeyValueDTOOfStringAndString } from '@generated/swagger/cat-search-api';
-import { ScaleContentComponent } from '@shared/components/scale-content';
-import moment from 'moment';
-
-@Component({
- selector: 'shared-purchase-options-list-item',
- templateUrl: 'purchase-options-list-item.component.html',
- styleUrls: ['purchase-options-list-item.component.css'],
- changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [
- CommonModule,
- UiQuantityDropdownModule,
- UiSelectModule,
- ProductImageModule,
- IconComponent,
- UiSpinnerModule,
- ReactiveFormsModule,
- InputControlModule,
- FormsModule,
- ElementLifecycleModule,
- UiTooltipModule,
- UiCommonModule,
- ScaleContentComponent,
- OrderDeadlinePipeModule,
- ],
- host: { class: 'shared-purchase-options-list-item' },
-})
-export class PurchaseOptionsListItemComponent implements OnInit, OnDestroy, OnChanges {
- private _subscriptions = new Subscription();
-
- private _itemSubject = new ReplaySubject- (1);
-
- @Input() item: Item;
-
- get item$() {
- return this._itemSubject.asObservable();
- }
-
- get product() {
- return this.item.product;
- }
-
- quantityFormControl = new FormControl(null);
-
- private readonly _giftCardValidators = [
- Validators.required,
- Validators.min(1),
- Validators.max(GIFT_CARD_MAX_PRICE),
- Validators.pattern(PRICE_PATTERN),
- ];
-
- private readonly _defaultValidators = [
- Validators.required,
- Validators.min(0.01),
- Validators.max(999.99),
- Validators.pattern(PRICE_PATTERN),
- ];
-
- priceFormControl = new FormControl(null);
-
- manualVatFormControl = new FormControl('', [Validators.required]);
-
- selectedFormControl = new FormControl(false);
-
- availabilities$ = this.item$.pipe(switchMap((item) => this._store.getAvailabilitiesForItem$(item.id)));
-
- availability$ = combineLatest([this.item$, this._store.purchaseOption$]).pipe(
- switchMap(([item, purchaseOption]) => this._store.getAvailabilityWithPurchaseOption$(item.id, purchaseOption)),
- map((availability) => availability?.data),
- );
-
- price$ = this.item$.pipe(switchMap((item) => this._store.getPrice$(item.id)));
-
- priceValue$ = this.price$.pipe(map((price) => price?.value?.value));
-
- // Ticket #4074 analog zu Ticket #2244
- // take(2) um die Response des Katalogpreises und danach um die Response der OLAs abzuwarten
- // Logik gilt ausschließlich für Archivartikel
- setManualPrice$ = this.price$.pipe(
- take(2),
- map((price) => {
- // Logik nur beim Hinzufügen über Kaufoptionen, da über Ändern im Warenkorb die Info fehlt ob das jeweilige ShoppingCartItem ein Archivartikel ist oder nicht
- const features = this.item?.features as KeyValueDTOOfStringAndString[];
- if (!!features && Array.isArray(features)) {
- const isArchive = !!features?.find((feature) => feature?.enabled === true && feature?.key === 'ARC');
- return isArchive ? !price?.value?.value || price?.vat === undefined : false;
- }
- return false;
- }),
- );
-
- vats$ = this._store.vats$.pipe(shareReplay());
-
- priceVat$ = this.price$.pipe(map((price) => price?.vat?.vatType));
-
- canAddResult$ = this.item$.pipe(
- switchMap((item) => this._store.getCanAddResultForItemAndCurrentPurchaseOption$(item.id)),
- );
-
- canEditPrice$ = this.item$.pipe(
- switchMap((item) => combineLatest([this.canAddResult$, this._store.getCanEditPrice$(item.id)])),
- map(([canAddResult, canEditPrice]) => canAddResult?.canAdd && canEditPrice),
- );
-
- canEditVat$ = this.item$.pipe(
- switchMap((item) => combineLatest([this.canAddResult$, this._store.getCanEditVat$(item.id)])),
- map(([canAddResult, canEditVat]) => canAddResult?.canAdd && canEditVat),
- );
-
- isGiftCard$ = this.item$.pipe(switchMap((item) => this._store.getIsGiftCard$(item.id)));
-
- maxSelectableQuantity$ = combineLatest([this._store.purchaseOption$, this.availability$]).pipe(
- map(([purchaseOption, availability]) => {
- if (purchaseOption === 'in-store') {
- return availability?.inStock;
- }
-
- return 999;
- }),
- startWith(999),
- );
-
- showMaxAvailableQuantity$ = combineLatest([this._store.purchaseOption$, this.availability$, this.item$]).pipe(
- map(([purchaseOption, availability, item]) => {
- if (purchaseOption === 'pickup' && availability?.inStock < item.quantity) {
- return true;
- }
-
- return false;
- }),
- );
-
- fetchingAvailabilities$ = this.item$
- .pipe(switchMap((item) => this._store.getFetchingAvailabilitiesForItem$(item.id)))
- .pipe(map((fetchingAvailabilities) => fetchingAvailabilities.length > 0));
-
- showNotAvailable$ = combineLatest([this.availabilities$, this.fetchingAvailabilities$]).pipe(
- map(([availabilities, fetchingAvailabilities]) => {
- if (fetchingAvailabilities) {
- return false;
- }
-
- if (availabilities.length === 0) {
- return true;
- }
-
- return false;
- }),
- );
-
- // Ticket #4813 Artikeldetailseite // EVT-Datum bei der Kaufoption Rücklage anzeigen
- get isEVT() {
- // Einstieg über Kaufoptionen - Hier wird die Katalogavailability verwendet die am ItemDTO hängt
- if (isItemDTO(this.item, this._store.type)) {
- const firstDayOfSale = this.item?.catalogAvailability?.firstDayOfSale;
- return this.firstDayOfSaleBiggerThanToday(firstDayOfSale);
- }
-
- // Einstieg über Warenkorb - Da wir hier ein ShoppingCartItemDTO haben werden hier die Katalogdaten neu gefetched und eine Katalogavailability drangehängt
- if (isShoppingCartItemDTO(this.item, this._store.type)) {
- const catalogAvailabilities = this._store.availabilities?.filter(
- (availability) => availability?.purchaseOption === 'catalog',
- );
- // #4813 Fix: Hier muss als Kriterium auf EAN statt itemId verglichen werden, denn ein ShoppingCartItemDTO (this.item) hat eine andere ItemId wie das ItemDTO (availability.itemId)
- const firstDayOfSale = catalogAvailabilities?.find(
- (availability) => this.item?.product?.ean === availability?.ean,
- )?.data?.firstDayOfSale;
- return this.firstDayOfSaleBiggerThanToday(firstDayOfSale);
- }
-
- return undefined;
- }
-
- constructor(private _store: PurchaseOptionsStore) {}
-
- firstDayOfSaleBiggerThanToday(firstDayOfSale: string): Date {
- if (!!firstDayOfSale && moment(firstDayOfSale)?.isAfter(moment())) {
- return moment(firstDayOfSale).toDate();
- }
- return undefined;
- }
-
- onPriceInputInit(target: HTMLElement, overlayTrigger: UiOverlayTriggerDirective) {
- if (this._store.getIsGiftCard(this.item.id)) {
- overlayTrigger.open();
- }
-
- target?.focus();
- }
-
- // Wichtig für das korrekte Setzen des Preises an das Item für den Endpoint request
- parsePrice(value: string) {
- if (PRICE_PATTERN.test(value)) {
- return parseFloat(value.replace(',', '.'));
- }
- }
-
- stringifyPrice(value: number) {
- if (!value) return '';
-
- const price = value.toFixed(2).replace('.', ',');
- if (price.includes(',')) {
- const [integer, decimal] = price.split(',');
- return `${integer},${decimal.padEnd(2, '0')}`;
- }
-
- return price;
- }
-
- ngOnInit(): void {
- this.initPriceValidatorSubscription();
- this.initQuantitySubscription();
- this.initPriceSubscription();
- this.initVatSubscription();
- this.initSelectedSubscription();
- }
-
- ngOnChanges({ item }: SimpleChanges) {
- if (item) {
- this._itemSubject.next(this.item);
- }
- }
-
- ngOnDestroy(): void {
- this._itemSubject.complete();
- this._subscriptions.unsubscribe();
- }
-
- initPriceValidatorSubscription() {
- const sub = this.item$.pipe(switchMap((item) => this._store.getIsGiftCard$(item.id))).subscribe((isGiftCard) => {
- if (isGiftCard) {
- this.priceFormControl.setValidators(this._giftCardValidators);
- } else {
- this.priceFormControl.setValidators(this._defaultValidators);
- }
- });
-
- this._subscriptions.add(sub);
- }
-
- initQuantitySubscription() {
- const sub = this.item$.subscribe((item) => {
- if (this.quantityFormControl.value !== item.quantity) {
- this.quantityFormControl.setValue(item.quantity);
- }
- });
-
- const valueChangesSub = this.quantityFormControl.valueChanges.subscribe((quantity) => {
- if (this.item.quantity !== quantity) {
- this._store.setItemQuantity(this.item.id, quantity);
- }
- });
-
- this._subscriptions.add(sub);
- this._subscriptions.add(valueChangesSub);
- }
-
- initPriceSubscription() {
- const sub = combineLatest([this.canEditPrice$, this.price$]).subscribe(([canEditPrice, price]) => {
- if (!canEditPrice) {
- return;
- }
-
- const priceStr = this.stringifyPrice(price?.value?.value);
- if (priceStr === '') return;
-
- if (this.parsePrice(this.priceFormControl.value) !== price?.value?.value) {
- this.priceFormControl.setValue(priceStr);
- }
- });
-
- const valueChangesSub = combineLatest([this.canEditPrice$, this.priceFormControl.valueChanges]).subscribe(
- ([canEditPrice, value]) => {
- if (!canEditPrice) {
- return;
- }
-
- const price = this._store.getPrice(this.item.id);
- const parsedPrice = this.parsePrice(value);
-
- if (!parsedPrice) {
- this._store.setPrice(this.item.id, null);
- return;
- }
-
- if (price[this.item.id] !== parsedPrice) {
- this._store.setPrice(this.item.id, this.parsePrice(value));
- }
- },
- );
- this._subscriptions.add(sub);
- this._subscriptions.add(valueChangesSub);
- }
-
- initVatSubscription() {
- const valueChangesSub = this.manualVatFormControl.valueChanges
- .pipe(withLatestFrom(this.vats$))
- .subscribe(([formVatType, vats]) => {
- const price = this._store.getPrice(this.item.id);
-
- const vat = vats.find((vat) => vat?.vatType === Number(formVatType));
-
- if (!vat) {
- this._store.setVat(this.item.id, null);
- return;
- }
-
- if (price[this.item.id]?.vat?.vatType !== vat?.vatType) {
- this._store.setVat(this.item.id, vat);
- }
- });
- this._subscriptions.add(valueChangesSub);
- }
-
- initSelectedSubscription() {
- const sub = this.item$
- .pipe(switchMap((item) => this._store.selectedItemIds$.pipe(map((ids) => ids.includes(item.id)))))
- .subscribe((selected) => {
- if (this.selectedFormControl.value !== selected) {
- this.selectedFormControl.setValue(selected);
- }
- });
- const valueChangesSub = this.selectedFormControl.valueChanges.subscribe((selected) => {
- const current = this._store.selectedItemIds.includes(this.item.id);
- if (current !== selected) {
- this._store.setSelectedItem(this.item.id, selected);
- }
- });
- this._subscriptions.add(sub);
- this._subscriptions.add(valueChangesSub);
- }
-}
+import { CommonModule } from '@angular/common';
+import {
+ Component,
+ ChangeDetectionStrategy,
+ OnInit,
+ OnDestroy,
+ OnChanges,
+ SimpleChanges,
+ computed,
+ input,
+} from '@angular/core';
+import {
+ FormControl,
+ FormsModule,
+ ReactiveFormsModule,
+ Validators,
+} from '@angular/forms';
+import { ProductImageModule } from '@cdn/product-image';
+import { InputControlModule } from '@shared/components/input-control';
+import { ElementLifecycleModule } from '@shared/directives/element-lifecycle';
+import { UiCommonModule, UiOverlayTriggerDirective } from '@ui/common';
+import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
+import { UiSpinnerModule } from '@ui/spinner';
+import { UiTooltipModule } from '@ui/tooltip';
+import { combineLatest, ReplaySubject, Subscription } from 'rxjs';
+import { IconComponent } from '@shared/components/icon';
+import {
+ map,
+ take,
+ shareReplay,
+ startWith,
+ switchMap,
+ withLatestFrom,
+} from 'rxjs/operators';
+import { GIFT_CARD_MAX_PRICE, PRICE_PATTERN } from '../constants';
+import {
+ Item,
+ PurchaseOptionsStore,
+ isItemDTO,
+ isShoppingCartItemDTO,
+} from '../store';
+import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
+import { UiSelectModule } from '@ui/select';
+import { KeyValueDTOOfStringAndString } from '@generated/swagger/cat-search-api';
+import { ScaleContentComponent } from '@shared/components/scale-content';
+import moment from 'moment';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { NgIcon, provideIcons } from '@ng-icons/core';
+import { isaOtherInfo } from '@isa/icons';
+
+@Component({
+ selector: 'shared-purchase-options-list-item',
+ templateUrl: 'purchase-options-list-item.component.html',
+ styleUrls: ['purchase-options-list-item.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [
+ CommonModule,
+ UiQuantityDropdownModule,
+ UiSelectModule,
+ ProductImageModule,
+ IconComponent,
+ UiSpinnerModule,
+ ReactiveFormsModule,
+ InputControlModule,
+ FormsModule,
+ ElementLifecycleModule,
+ UiTooltipModule,
+ UiCommonModule,
+ ScaleContentComponent,
+ OrderDeadlinePipeModule,
+ NgIcon,
+ ],
+ host: { class: 'shared-purchase-options-list-item' },
+ providers: [provideIcons({ isaOtherInfo })],
+})
+export class PurchaseOptionsListItemComponent
+ implements OnInit, OnDestroy, OnChanges
+{
+ private _subscriptions = new Subscription();
+
+ private _itemSubject = new ReplaySubject
- (1);
+
+ item = input.required
- ();
+
+ get item$() {
+ return this._itemSubject.asObservable();
+ }
+
+ get product() {
+ return this.item().product;
+ }
+
+ redemptionPoints = computed(() => {
+ const item = this.item();
+ if (isShoppingCartItemDTO(item, this._store.type)) {
+ return item.loyalty?.value;
+ }
+
+ return item.redemptionPoints;
+ });
+
+ showRedemptionPoints = toSignal(this._store.useRedemptionPoints$);
+
+ quantityFormControl = new FormControl(null);
+
+ private readonly _giftCardValidators = [
+ Validators.required,
+ Validators.min(1),
+ Validators.max(GIFT_CARD_MAX_PRICE),
+ Validators.pattern(PRICE_PATTERN),
+ ];
+
+ private readonly _defaultValidators = [
+ Validators.required,
+ Validators.min(0.01),
+ Validators.max(999.99),
+ Validators.pattern(PRICE_PATTERN),
+ ];
+
+ priceFormControl = new FormControl(null);
+
+ manualVatFormControl = new FormControl('', [Validators.required]);
+
+ selectedFormControl = new FormControl(false);
+
+ availabilities$ = this.item$.pipe(
+ switchMap((item) => this._store.getAvailabilitiesForItem$(item.id)),
+ );
+
+ availability$ = combineLatest([this.item$, this._store.purchaseOption$]).pipe(
+ switchMap(([item, purchaseOption]) =>
+ this._store.getAvailabilityWithPurchaseOption$(item.id, purchaseOption),
+ ),
+ map((availability) => availability?.data),
+ );
+
+ availability = toSignal(this.availability$);
+
+ price$ = this.item$.pipe(switchMap((item) => this._store.getPrice$(item.id)));
+
+ priceValue$ = this.price$.pipe(map((price) => price?.value?.value));
+
+ // Ticket #4074 analog zu Ticket #2244
+ // take(2) um die Response des Katalogpreises und danach um die Response der OLAs abzuwarten
+ // Logik gilt ausschließlich für Archivartikel
+ setManualPrice$ = this.price$.pipe(
+ take(2),
+ map((price) => {
+ // Logik nur beim Hinzufügen über Kaufoptionen, da über Ändern im Warenkorb die Info fehlt ob das jeweilige ShoppingCartItem ein Archivartikel ist oder nicht
+ const features = this.item().features as KeyValueDTOOfStringAndString[];
+ if (!!features && Array.isArray(features)) {
+ const isArchive = !!features?.find(
+ (feature) => feature?.enabled === true && feature?.key === 'ARC',
+ );
+ return isArchive
+ ? !price?.value?.value || price?.vat === undefined
+ : false;
+ }
+ return false;
+ }),
+ );
+
+ vats$ = this._store.vats$.pipe(shareReplay());
+
+ priceVat$ = this.price$.pipe(map((price) => price?.vat?.vatType));
+
+ canAddResult$ = this.item$.pipe(
+ switchMap((item) =>
+ this._store.getCanAddResultForItemAndCurrentPurchaseOption$(item.id),
+ ),
+ );
+
+ canEditPrice$ = this.item$.pipe(
+ switchMap((item) =>
+ combineLatest([
+ this.canAddResult$,
+ this._store.getCanEditPrice$(item.id),
+ ]),
+ ),
+ map(([canAddResult, canEditPrice]) => canAddResult?.canAdd && canEditPrice),
+ );
+
+ canEditVat$ = this.item$.pipe(
+ switchMap((item) =>
+ combineLatest([this.canAddResult$, this._store.getCanEditVat$(item.id)]),
+ ),
+ map(([canAddResult, canEditVat]) => canAddResult?.canAdd && canEditVat),
+ );
+
+ isGiftCard$ = this.item$.pipe(
+ switchMap((item) => this._store.getIsGiftCard$(item.id)),
+ );
+
+ maxSelectableQuantity$ = combineLatest([
+ this._store.purchaseOption$,
+ this.availability$,
+ ]).pipe(
+ map(([purchaseOption, availability]) => {
+ if (purchaseOption === 'in-store') {
+ return availability?.inStock;
+ }
+
+ return 999;
+ }),
+ startWith(999),
+ );
+
+ showMaxAvailableQuantity$ = combineLatest([
+ this._store.purchaseOption$,
+ this.availability$,
+ this.item$,
+ ]).pipe(
+ map(([purchaseOption, availability, item]) => {
+ if (
+ purchaseOption === 'pickup' &&
+ availability?.inStock < item.quantity
+ ) {
+ return true;
+ }
+
+ return false;
+ }),
+ );
+
+ fetchingAvailabilities$ = this.item$
+ .pipe(
+ switchMap((item) =>
+ this._store.getFetchingAvailabilitiesForItem$(item.id),
+ ),
+ )
+ .pipe(map((fetchingAvailabilities) => fetchingAvailabilities.length > 0));
+
+ showNotAvailable$ = combineLatest([
+ this.availabilities$,
+ this.fetchingAvailabilities$,
+ ]).pipe(
+ map(([availabilities, fetchingAvailabilities]) => {
+ if (fetchingAvailabilities) {
+ return false;
+ }
+
+ if (availabilities.length === 0) {
+ return true;
+ }
+
+ return false;
+ }),
+ );
+
+ // Ticket #4813 Artikeldetailseite // EVT-Datum bei der Kaufoption Rücklage anzeigen
+ get isEVT() {
+ // Einstieg über Kaufoptionen - Hier wird die Katalogavailability verwendet die am ItemDTO hängt
+ if (isItemDTO(this.item, this._store.type)) {
+ const firstDayOfSale = this.item?.catalogAvailability?.firstDayOfSale;
+ return this.firstDayOfSaleBiggerThanToday(firstDayOfSale);
+ }
+
+ // Einstieg über Warenkorb - Da wir hier ein ShoppingCartItemDTO haben werden hier die Katalogdaten neu gefetched und eine Katalogavailability drangehängt
+ if (isShoppingCartItemDTO(this.item, this._store.type)) {
+ const catalogAvailabilities = this._store.availabilities?.filter(
+ (availability) => availability?.purchaseOption === 'catalog',
+ );
+ // #4813 Fix: Hier muss als Kriterium auf EAN statt itemId verglichen werden, denn ein ShoppingCartItemDTO (this.item) hat eine andere ItemId wie das ItemDTO (availability.itemId)
+ const firstDayOfSale = catalogAvailabilities?.find(
+ (availability) => this.item().product?.ean === availability?.ean,
+ )?.data?.firstDayOfSale;
+ return this.firstDayOfSaleBiggerThanToday(firstDayOfSale);
+ }
+
+ return undefined;
+ }
+
+ useRedemptionPoints = toSignal(this._store.useRedemptionPoints$);
+
+ purchaseOption = toSignal(this._store.purchaseOption$);
+
+ isReservePurchaseOption = computed(() => {
+ return this.purchaseOption() === 'in-store';
+ });
+
+ showLowStockMessage = computed(() => {
+ return (
+ this.useRedemptionPoints() &&
+ this.isReservePurchaseOption() &&
+ this.availability().inStock < 2
+ );
+ });
+
+ constructor(private _store: PurchaseOptionsStore) {}
+
+ firstDayOfSaleBiggerThanToday(firstDayOfSale: string): Date {
+ if (!!firstDayOfSale && moment(firstDayOfSale)?.isAfter(moment())) {
+ return moment(firstDayOfSale).toDate();
+ }
+ return undefined;
+ }
+
+ onPriceInputInit(
+ target: HTMLElement,
+ overlayTrigger: UiOverlayTriggerDirective,
+ ) {
+ if (this._store.getIsGiftCard(this.item().id)) {
+ overlayTrigger.open();
+ }
+
+ target?.focus();
+ }
+
+ // Wichtig für das korrekte Setzen des Preises an das Item für den Endpoint request
+ parsePrice(value: string) {
+ if (PRICE_PATTERN.test(value)) {
+ return parseFloat(value.replace(',', '.'));
+ }
+ }
+
+ stringifyPrice(value: number) {
+ if (!value) return '';
+
+ const price = value.toFixed(2).replace('.', ',');
+ if (price.includes(',')) {
+ const [integer, decimal] = price.split(',');
+ return `${integer},${decimal.padEnd(2, '0')}`;
+ }
+
+ return price;
+ }
+
+ ngOnInit(): void {
+ this.initPriceValidatorSubscription();
+ this.initQuantitySubscription();
+ this.initPriceSubscription();
+ this.initVatSubscription();
+ this.initSelectedSubscription();
+ }
+
+ ngOnChanges({ item }: SimpleChanges) {
+ if (item) {
+ this._itemSubject.next(this.item());
+ }
+ }
+
+ ngOnDestroy(): void {
+ this._itemSubject.complete();
+ this._subscriptions.unsubscribe();
+ }
+
+ initPriceValidatorSubscription() {
+ const sub = this.item$
+ .pipe(switchMap((item) => this._store.getIsGiftCard$(item.id)))
+ .subscribe((isGiftCard) => {
+ if (isGiftCard) {
+ this.priceFormControl.setValidators(this._giftCardValidators);
+ } else {
+ this.priceFormControl.setValidators(this._defaultValidators);
+ }
+ });
+
+ this._subscriptions.add(sub);
+ }
+
+ initQuantitySubscription() {
+ const sub = this.item$.subscribe((item) => {
+ if (this.quantityFormControl.value !== item.quantity) {
+ this.quantityFormControl.setValue(item.quantity);
+ }
+ });
+
+ const valueChangesSub = this.quantityFormControl.valueChanges.subscribe(
+ (quantity) => {
+ if (this.item().quantity !== quantity) {
+ this._store.setItemQuantity(this.item().id, quantity);
+ }
+ },
+ );
+
+ this._subscriptions.add(sub);
+ this._subscriptions.add(valueChangesSub);
+ }
+
+ initPriceSubscription() {
+ const sub = combineLatest([this.canEditPrice$, this.price$]).subscribe(
+ ([canEditPrice, price]) => {
+ if (!canEditPrice) {
+ return;
+ }
+
+ const priceStr = this.stringifyPrice(price?.value?.value);
+ if (priceStr === '') return;
+
+ if (
+ this.parsePrice(this.priceFormControl.value) !== price?.value?.value
+ ) {
+ this.priceFormControl.setValue(priceStr);
+ }
+ },
+ );
+
+ const valueChangesSub = combineLatest([
+ this.canEditPrice$,
+ this.priceFormControl.valueChanges,
+ ]).subscribe(([canEditPrice, value]) => {
+ if (!canEditPrice) {
+ return;
+ }
+
+ const price = this._store.getPrice(this.item().id);
+ const parsedPrice = this.parsePrice(value);
+
+ if (!parsedPrice) {
+ this._store.setPrice(this.item().id, null);
+ return;
+ }
+
+ if (price[this.item().id] !== parsedPrice) {
+ this._store.setPrice(this.item().id, this.parsePrice(value));
+ }
+ });
+ this._subscriptions.add(sub);
+ this._subscriptions.add(valueChangesSub);
+ }
+
+ initVatSubscription() {
+ const valueChangesSub = this.manualVatFormControl.valueChanges
+ .pipe(withLatestFrom(this.vats$))
+ .subscribe(([formVatType, vats]) => {
+ const price = this._store.getPrice(this.item().id);
+
+ const vat = vats.find((vat) => vat?.vatType === Number(formVatType));
+
+ if (!vat) {
+ this._store.setVat(this.item().id, null);
+ return;
+ }
+
+ if (price[this.item().id]?.vat?.vatType !== vat?.vatType) {
+ this._store.setVat(this.item().id, vat);
+ }
+ });
+ this._subscriptions.add(valueChangesSub);
+ }
+
+ initSelectedSubscription() {
+ const sub = this.item$
+ .pipe(
+ switchMap((item) =>
+ this._store.selectedItemIds$.pipe(
+ map((ids) => ids.includes(item.id)),
+ ),
+ ),
+ )
+ .subscribe((selected) => {
+ if (this.selectedFormControl.value !== selected) {
+ this.selectedFormControl.setValue(selected);
+ }
+ });
+ const valueChangesSub = this.selectedFormControl.valueChanges.subscribe(
+ (selected) => {
+ const current = this._store.selectedItemIds.includes(this.item().id);
+ if (current !== selected) {
+ this._store.setSelectedItem(this.item().id, selected);
+ }
+ },
+ );
+ this._subscriptions.add(sub);
+ this._subscriptions.add(valueChangesSub);
+ }
+}
diff --git a/apps/isa-app/src/modal/purchase-options/purchase-options-modal.data.ts b/apps/isa-app/src/modal/purchase-options/purchase-options-modal.data.ts
index 71003d551..e27b429e1 100644
--- a/apps/isa-app/src/modal/purchase-options/purchase-options-modal.data.ts
+++ b/apps/isa-app/src/modal/purchase-options/purchase-options-modal.data.ts
@@ -10,6 +10,7 @@ export interface PurchaseOptionsModalData {
tabId: number;
shoppingCartId: number;
type: ActionType;
+ useRedemptionPoints?: boolean;
items: Array;
pickupBranch?: BranchDTO;
inStoreBranch?: BranchDTO;
@@ -19,6 +20,7 @@ export interface PurchaseOptionsModalData {
export interface PurchaseOptionsModalContext {
shoppingCartId: number;
type: ActionType;
+ useRedemptionPoints: boolean;
items: Array;
selectedCustomer?: Customer;
selectedBranch?: BranchDTO;
diff --git a/apps/isa-app/src/modal/purchase-options/purchase-options-modal.service.ts b/apps/isa-app/src/modal/purchase-options/purchase-options-modal.service.ts
index 3df4c9fe2..c5a1030ae 100644
--- a/apps/isa-app/src/modal/purchase-options/purchase-options-modal.service.ts
+++ b/apps/isa-app/src/modal/purchase-options/purchase-options-modal.service.ts
@@ -21,6 +21,7 @@ export class PurchaseOptionsModalService {
data: PurchaseOptionsModalData,
): Promise> {
const context: PurchaseOptionsModalContext = {
+ useRedemptionPoints: !!data.useRedemptionPoints,
...data,
};
diff --git a/apps/isa-app/src/modal/purchase-options/store/purchase-options.selectors.ts b/apps/isa-app/src/modal/purchase-options/store/purchase-options.selectors.ts
index 6b911ac45..fe99dfe83 100644
--- a/apps/isa-app/src/modal/purchase-options/store/purchase-options.selectors.ts
+++ b/apps/isa-app/src/modal/purchase-options/store/purchase-options.selectors.ts
@@ -1,9 +1,7 @@
-import { PriceDTO, PriceValueDTO } from '@generated/swagger/checkout-api';
+import { PriceDTO } from '@generated/swagger/checkout-api';
import {
DEFAULT_PRICE_DTO,
- DEFAULT_PRICE_VALUE,
GIFT_CARD_MAX_PRICE,
- GIFT_CARD_TYPE,
PURCHASE_OPTIONS,
} from '../constants';
import { isArchive, isGiftCard, isItemDTO } from './purchase-options.helpers';
@@ -22,6 +20,10 @@ export function getType(state: PurchaseOptionsState): ActionType {
return state.type;
}
+export function getUseRedemptionPoints(state: PurchaseOptionsState): boolean {
+ return state.useRedemptionPoints;
+}
+
export function getShoppingCartId(state: PurchaseOptionsState): number {
return state.shoppingCartId;
}
@@ -337,7 +339,7 @@ export function getAvailabilityPriceForPurchaseOption(
return (state) => {
const item = getItems(state).find((item) => item.id === itemId);
const type = getType(state);
- let availabilities = getAvailabilitiesForItem(itemId)(state);
+ const availabilities = getAvailabilitiesForItem(itemId)(state);
let availability = availabilities.find(
(availability) => availability.purchaseOption === purchaseOption,
@@ -530,7 +532,7 @@ export function getAvailabilityWithPurchaseOption(
purchaseOption: PurchaseOption,
): (state: PurchaseOptionsState) => Availability {
return (state) => {
- let availabilities = getAvailabilitiesForItem(itemId, true)(state);
+ const availabilities = getAvailabilitiesForItem(itemId, true)(state);
let availability = availabilities.find(
(availability) => availability.purchaseOption === purchaseOption,
@@ -597,7 +599,7 @@ export function canContinue(state: PurchaseOptionsState): boolean {
const actionType = getType(state);
- for (let item of items) {
+ for (const item of items) {
if (isGiftCard(item, actionType)) {
const price = getPriceForPurchaseOption(item.id, purchaseOption)(state);
if (
@@ -647,7 +649,7 @@ export function canContinue(state: PurchaseOptionsState): boolean {
return false;
}
- let availability = getAvailabilityWithPurchaseOption(
+ const availability = getAvailabilityWithPurchaseOption(
item.id,
purchaseOption,
)(state);
diff --git a/apps/isa-app/src/modal/purchase-options/store/purchase-options.state.ts b/apps/isa-app/src/modal/purchase-options/store/purchase-options.state.ts
index c27673c69..50e7315e5 100644
--- a/apps/isa-app/src/modal/purchase-options/store/purchase-options.state.ts
+++ b/apps/isa-app/src/modal/purchase-options/store/purchase-options.state.ts
@@ -35,4 +35,6 @@ export interface PurchaseOptionsState {
customerFeatures: Record;
fetchingAvailabilities: Array;
+
+ useRedemptionPoints: boolean;
}
diff --git a/apps/isa-app/src/modal/purchase-options/store/purchase-options.store.ts b/apps/isa-app/src/modal/purchase-options/store/purchase-options.store.ts
index eaf1a6757..3fb7d70d4 100644
--- a/apps/isa-app/src/modal/purchase-options/store/purchase-options.store.ts
+++ b/apps/isa-app/src/modal/purchase-options/store/purchase-options.store.ts
@@ -1,5 +1,6 @@
import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
+import { logger } from '@isa/core/logging';
import { PurchaseOptionsModalContext } from '../purchase-options-modal.data';
import { PurchaseOptionsService } from './purchase-options.service';
import { PurchaseOptionsState } from './purchase-options.state';
@@ -39,7 +40,7 @@ import { uniqueId } from 'lodash';
import { VATDTO } from '@generated/swagger/oms-api';
import { DomainCatalogService } from '@domain/catalog';
import { ItemDTO } from '@generated/swagger/cat-search-api';
-import { OrderType } from '@isa/checkout/data-access';
+import { Loyalty, OrderType, Promotion } from '@isa/checkout/data-access';
@Injectable()
export class PurchaseOptionsStore extends ComponentStore {
@@ -49,6 +50,12 @@ export class PurchaseOptionsStore extends ComponentStore {
type$ = this.select(Selectors.getType);
+ get useRedemptionPoints() {
+ return this.get(Selectors.getUseRedemptionPoints);
+ }
+
+ useRedemptionPoints$ = this.select(Selectors.getUseRedemptionPoints);
+
get shoppingCartId() {
return this.get(Selectors.getShoppingCartId);
}
@@ -149,6 +156,12 @@ export class PurchaseOptionsStore extends ComponentStore {
return this._service.getVats$();
}
+ #logger = logger(() => ({
+ class: 'PurchaseOptionsStore',
+ module: 'purchase-options',
+ importMetaUrl: import.meta.url,
+ }));
+
constructor(
private _service: PurchaseOptionsService,
private _catalogService: DomainCatalogService,
@@ -167,6 +180,7 @@ export class PurchaseOptionsStore extends ComponentStore {
canAddResults: [],
customerFeatures: {},
fetchingAvailabilities: [],
+ useRedemptionPoints: false,
});
}
@@ -198,6 +212,7 @@ export class PurchaseOptionsStore extends ComponentStore {
type,
inStoreBranch,
pickupBranch,
+ useRedemptionPoints: showRedemptionPoints,
}: PurchaseOptionsModalContext) {
const defaultBranch = await this._service.fetchDefaultBranch().toPromise();
@@ -213,6 +228,7 @@ export class PurchaseOptionsStore extends ComponentStore {
this.patchState({
type: type,
shoppingCartId,
+ useRedemptionPoints: showRedemptionPoints,
items: items.map((item) => ({
...item,
quantity: item['quantity'] ?? 1,
@@ -233,7 +249,7 @@ export class PurchaseOptionsStore extends ComponentStore {
private async _loadAvailabilities() {
const items = this.items;
- const promises: Promise[] = [];
+ const promises: Promise[] = [];
this._loadCatalogueAvailability();
@@ -258,7 +274,7 @@ export class PurchaseOptionsStore extends ComponentStore {
private async loadItemAvailability(itemId: number) {
const item = this.items.find((item) => item.id === itemId);
- const promises: Promise[] = [];
+ const promises: Promise[] = [];
const purchaseOption = this.purchaseOption;
@@ -344,7 +360,11 @@ export class PurchaseOptionsStore extends ComponentStore {
this._checkAndSetAvailability(availability);
} catch (err) {
- console.error('_loadPickupAvailability', err);
+ this.#logger.error('Failed to load pickup availability', err, () => ({
+ itemId: itemData.sourceId,
+ quantity: itemData.quantity,
+ branchId: branch?.id,
+ }));
}
this.removeFetchingAvailability({
@@ -377,7 +397,11 @@ export class PurchaseOptionsStore extends ComponentStore {
this._checkAndSetAvailability(availability);
} catch (err) {
- console.error('_loadInStoreAvailability', err);
+ this.#logger.error('Failed to load in-store availability', err, () => ({
+ itemId: itemData.sourceId,
+ quantity: itemData.quantity,
+ branchId: branch?.id,
+ }));
}
this.removeFetchingAvailability({
@@ -406,7 +430,10 @@ export class PurchaseOptionsStore extends ComponentStore {
this._checkAndSetAvailability(availability);
} catch (error) {
- console.error('_loadDeliveryAvailability', error);
+ this.#logger.error('Failed to load delivery availability', error, () => ({
+ itemId: itemData.sourceId,
+ quantity: itemData.quantity,
+ }));
}
this.removeFetchingAvailability({
@@ -436,7 +463,14 @@ export class PurchaseOptionsStore extends ComponentStore {
this._checkAndSetAvailability(availability);
} catch (error) {
- console.error('_loadDigDeliveryAvailability', error);
+ this.#logger.error(
+ 'Failed to load digital delivery availability',
+ error,
+ () => ({
+ itemId: itemData.sourceId,
+ quantity: itemData.quantity,
+ }),
+ );
}
this.removeFetchingAvailability({
@@ -466,7 +500,14 @@ export class PurchaseOptionsStore extends ComponentStore {
this._checkAndSetAvailability(availability);
} catch (error) {
- console.error('_loadB2bDeliveryAvailability', error);
+ this.#logger.error(
+ 'Failed to load B2B delivery availability',
+ error,
+ () => ({
+ itemId: itemData.sourceId,
+ quantity: itemData.quantity,
+ }),
+ );
}
this.removeFetchingAvailability({
@@ -496,7 +537,9 @@ export class PurchaseOptionsStore extends ComponentStore {
this._checkAndSetAvailability(availability);
} catch (error) {
- console.error('_loadDownloadAvailability', error);
+ this.#logger.error('Failed to load download availability', error, () => ({
+ itemId: itemData.sourceId,
+ }));
}
this.removeFetchingAvailability({
@@ -524,7 +567,7 @@ export class PurchaseOptionsStore extends ComponentStore {
this.patchState({ availabilities });
} else {
- let availabilities = this.availabilities.filter(
+ const availabilities = this.availabilities.filter(
(a) =>
!(
a.itemId === availability.itemId &&
@@ -600,7 +643,7 @@ export class PurchaseOptionsStore extends ComponentStore {
}
// Get Abholung availability
- let pickupAvailability = this.availabilities.find(
+ const pickupAvailability = this.availabilities.find(
(a) => a.itemId === item.id && a.purchaseOption === 'pickup',
);
if (pickupAvailability) {
@@ -650,7 +693,11 @@ export class PurchaseOptionsStore extends ComponentStore {
});
});
} 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 {
item.id,
);
} catch (error) {
- console.error('removeItem', error);
+ this.#logger.error(
+ 'Failed to remove item from shopping cart',
+ error,
+ () => ({
+ shoppingCartId: this.shoppingCartId,
+ itemId: item.id,
+ }),
+ );
}
}
@@ -863,7 +917,7 @@ export class PurchaseOptionsStore extends ComponentStore {
return this.select(Selectors.getIsGiftCard(itemId));
}
- setPrice(itemId: number, value: number, manually: boolean = false) {
+ setPrice(itemId: number, value: number, manually = false) {
const prices = this.prices;
let price = prices[itemId];
@@ -959,14 +1013,30 @@ export class PurchaseOptionsStore extends ComponentStore {
purchaseOption: PurchaseOption,
): AddToShoppingCartDTO {
const item = this.items.find((i) => i.id === itemId);
+
+ if (!isItemDTO(item, this.type)) {
+ throw new Error('Invalid item');
+ }
+
const price = this.getPriceForPurchaseOption(itemId, this.purchaseOption);
const availability = this.getAvailabilityWithPurchaseOption(
itemId,
purchaseOption,
);
- if (!isItemDTO(item, this.type)) {
- throw new Error('Invalid item');
+ let promotion: Promotion | null = { value: item.promoPoints };
+ let loyalty: Loyalty | null = null;
+ const redemptionPoints: number | null = item.redemptionPoints || null;
+
+ // "Lesepunkte einlösen" logic
+ // If "Lesepunkte einlösen" is checked and item has redemption points, set price to 0 and remove promotion
+ if (this.useRedemptionPoints) {
+ // If loyalty is set, we need to remove promotion
+ promotion = null;
+ // Set loyalty points from item
+ loyalty = { value: redemptionPoints };
+ // Set price to 0
+ price.value.value = 0;
}
let destination: EntityDTOContainerOfDestinationDTO;
@@ -990,13 +1060,8 @@ export class PurchaseOptionsStore extends ComponentStore {
catalogProductNumber:
item?.product?.catalogProductNumber ?? String(item.id),
},
- promotion: { value: item.promoPoints },
- // retailPrice: {
- // value: price.value.value,
- // currency: price.value.currency,
- // vatType: price.vat.vatType,
- // },
- // shopItemId: item.id ?? +item.product.catalogProductNumber,
+ promotion,
+ loyalty,
};
}
@@ -1005,14 +1070,20 @@ export class PurchaseOptionsStore extends ComponentStore {
purchaseOption: PurchaseOption,
): UpdateShoppingCartItemDTO {
const item = this.items.find((i) => i.id === itemId);
+
+ if (!isShoppingCartItemDTO(item, this.type)) {
+ throw new Error('Invalid item');
+ }
const price = this.getPriceForPurchaseOption(itemId, this.purchaseOption);
const availability = this.getAvailabilityWithPurchaseOption(
itemId,
purchaseOption,
);
- if (!isShoppingCartItemDTO(item, this.type)) {
- throw new Error('Invalid item');
+ // If loyalty points is set we know it is a redemption item
+ // we need to make sure we don't update the price
+ if (this.useRedemptionPoints) {
+ price.value.value = 0;
}
let destination: EntityDTOContainerOfDestinationDTO;
@@ -1030,11 +1101,6 @@ export class PurchaseOptionsStore extends ComponentStore {
quantity: item.quantity,
availability: { ...availability.data, price },
destination,
- // retailPrice: {
- // value: price.value.value,
- // currency: price.value.currency,
- // vatType: price.vat.vatType,
- // },
};
}
@@ -1051,7 +1117,7 @@ export class PurchaseOptionsStore extends ComponentStore {
);
await this._service.addItemToShoppingCart(this.shoppingCartId, payloads);
} else if (type === 'update') {
- const payloads = this.selectedItemIds.map((itemId) =>
+ this.selectedItemIds.map((itemId) =>
this.getUpdateShoppingCartItemDTOForItem(itemId, purchaseOption),
);
diff --git a/apps/isa-app/src/page/checkout/checkout-review/shopping-cart-item/shopping-cart-item.component.html b/apps/isa-app/src/page/checkout/checkout-review/shopping-cart-item/shopping-cart-item.component.html
index dbfe28c09..6b9df3f36 100644
--- a/apps/isa-app/src/page/checkout/checkout-review/shopping-cart-item/shopping-cart-item.component.html
+++ b/apps/isa-app/src/page/checkout/checkout-review/shopping-cart-item/shopping-cart-item.component.html
@@ -1,129 +1,169 @@
-
-
-
-
-
= 40 && isTablet"
- [class.text-p1]="item?.product?.name?.length >= 50 || !isTablet"
- [class.text-p2]="item?.product?.name?.length >= 60 && isTablet"
- [class.text-p3]="item?.product?.name?.length >= 100"
- >
-
{{ item?.product?.name }}
-
-
-@if (item?.product?.format && item?.product?.formatDetail) {
-
-}
-
-
-
{{ item?.product?.manufacturer | substr: 25 }} | {{ item?.product?.ean }}
-
- {{ item?.product?.volume }}
- @if (item?.product?.volume && item?.product?.publicationDate) {
- |
- }
- {{ item?.product?.publicationDate | date }}
-
- @if (notAvailable$ | async) {
-
- Nicht verfügbar
-
- }
-
- @if (refreshingAvailabilit$ | async) {
-
- } @else {
- @if (orderType === 'Abholung') {
-
- Abholung ab {{ item?.availability?.estimatedShippingDate | date }}
-
- }
- @if (orderType === 'Versand' || orderType === 'B2B-Versand' || orderType === 'DIG-Versand') {
-
- @if (item?.availability?.estimatedDelivery) {
- Zustellung zwischen {{ (item?.availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }}
- und
- {{ (item?.availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
- } @else {
- Versand {{ item?.availability?.estimatedShippingDate | date }}
- }
-
- }
- }
-
-
- @if (olaError$ | async) {
-
Artikel nicht verfügbar
- }
-
-
-
-
{{ item?.availability?.price?.value?.value | currency: 'EUR' : 'code' }}
-
- @if (!(isDummy$ | async)) {
-
- } @else {
-
{{ item?.quantity }}x
- }
-
- @if (quantityError) {
-
- {{ quantityError }}
-
- }
-
-
-@if (orderType !== 'Download') {
-
- @if (!(hasOrderType$ | async)) {
-
- }
- @if (canEdit$ | async) {
-
- }
-
-}
+
+
+
+
+= 40 && isTablet"
+ [class.text-p1]="item?.product?.name?.length >= 50 || !isTablet"
+ [class.text-p2]="item?.product?.name?.length >= 60 && isTablet"
+ [class.text-p3]="item?.product?.name?.length >= 100"
+>
+
{{ item?.product?.name }}
+
+
+@if (item?.product?.format && item?.product?.formatDetail) {
+
+}
+
+
+
+ {{ item?.product?.manufacturer | substr: 25 }} | {{ item?.product?.ean }}
+
+
+ {{ item?.product?.volume }}
+ @if (item?.product?.volume && item?.product?.publicationDate) {
+ |
+ }
+ {{ item?.product?.publicationDate | date }}
+
+ @if (notAvailable$ | async) {
+
+ Nicht verfügbar
+
+ }
+
+ @if (refreshingAvailabilit$ | async) {
+
+ } @else {
+ @if (orderType === 'Abholung') {
+
+ Abholung ab {{ item?.availability?.estimatedShippingDate | date }}
+
+ }
+ @if (
+ orderType === 'Versand' ||
+ orderType === 'B2B-Versand' ||
+ orderType === 'DIG-Versand'
+ ) {
+
+ @if (item?.availability?.estimatedDelivery) {
+ Zustellung zwischen
+ {{
+ (
+ item?.availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.'
+ )?.replace('.', '')
+ }}
+ und
+ {{
+ (
+ item?.availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.'
+ )?.replace('.', '')
+ }}
+ } @else {
+ Versand {{ item?.availability?.estimatedShippingDate | date }}
+ }
+
+ }
+ }
+
+ @if (olaError$ | async) {
+
Artikel nicht verfügbar
+ }
+
+
+
+
+ {{ item?.availability?.price?.value?.value | currency: 'EUR' : 'code' }}
+
+
+ @if (!(isDummy$ | async)) {
+
+ } @else {
+
{{ item?.quantity }}x
+ }
+
+ @if (quantityError) {
+
+ {{ quantityError }}
+
+ }
+
+
+@if (orderType !== 'Download') {
+
+ @if (!(hasOrderType$ | async)) {
+
+ }
+ @if (canEdit$ | async) {
+
+ }
+
+}
diff --git a/apps/isa-app/src/page/checkout/checkout-review/shopping-cart-item/shopping-cart-item.component.ts b/apps/isa-app/src/page/checkout/checkout-review/shopping-cart-item/shopping-cart-item.component.ts
index 5887639aa..4a7f9024b 100644
--- a/apps/isa-app/src/page/checkout/checkout-review/shopping-cart-item/shopping-cart-item.component.ts
+++ b/apps/isa-app/src/page/checkout/checkout-review/shopping-cart-item/shopping-cart-item.component.ts
@@ -1,255 +1,298 @@
-import {
- ChangeDetectionStrategy,
- ChangeDetectorRef,
- Component,
- EventEmitter,
- Input,
- NgZone,
- OnInit,
- Output,
- inject,
-} from '@angular/core';
-import { ApplicationService } from '@core/application';
-import { EnvironmentService } from '@core/environment';
-import { DomainAvailabilityService } from '@domain/availability';
-import { DomainCheckoutService } from '@domain/checkout';
-import { ComponentStore } from '@ngrx/component-store';
-import { ProductCatalogNavigationService } from '@shared/services/navigation';
-import { ItemType, ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
-import { cloneDeep } from 'lodash';
-import moment from 'moment';
-import { combineLatest } from 'rxjs';
-import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
-
-export interface ShoppingCartItemComponentState {
- item: ShoppingCartItemDTO;
- orderType: string;
- loadingOnItemChangeById?: number;
- loadingOnQuantityChangeById?: number;
- refreshingAvailability: boolean;
- sscChanged: boolean;
- sscTextChanged: boolean;
- estimatedShippingDateChanged: boolean;
-}
-
-@Component({
- selector: 'page-shopping-cart-item',
- templateUrl: 'shopping-cart-item.component.html',
- styleUrls: ['shopping-cart-item.component.scss'],
- changeDetection: ChangeDetectionStrategy.OnPush,
- standalone: false,
-})
-export class ShoppingCartItemComponent extends ComponentStore implements OnInit {
- private _zone = inject(NgZone);
-
- @Output() changeItem = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO }>();
- @Output() changeDummyItem = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO }>();
- @Output() changeQuantity = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO; quantity: number }>();
-
- @Input()
- get item() {
- return this.get((s) => s.item);
- }
- set item(item: ShoppingCartItemDTO) {
- if (this.item !== item) {
- this.patchState({ item });
- }
- }
- readonly item$ = this.select((s) => s.item);
-
- readonly contributors$ = this.item$.pipe(
- map((item) => item?.product?.contributors?.split(';').map((val) => val.trim())),
- );
-
- @Input()
- get orderType() {
- return this.get((s) => s.orderType);
- }
- set orderType(orderType: string) {
- if (this.orderType !== orderType) {
- this.patchState({ orderType });
- }
- }
- readonly orderType$ = this.select((s) => s.orderType);
-
- @Input()
- get loadingOnItemChangeById() {
- return this.get((s) => s.loadingOnItemChangeById);
- }
- set loadingOnItemChangeById(loadingOnItemChangeById: number) {
- if (this.loadingOnItemChangeById !== loadingOnItemChangeById) {
- this.patchState({ loadingOnItemChangeById });
- }
- }
- readonly loadingOnItemChangeById$ = this.select((s) => s.loadingOnItemChangeById).pipe(shareReplay());
-
- @Input()
- get loadingOnQuantityChangeById() {
- return this.get((s) => s.loadingOnQuantityChangeById);
- }
- set loadingOnQuantityChangeById(loadingOnQuantityChangeById: number) {
- if (this.loadingOnQuantityChangeById !== loadingOnQuantityChangeById) {
- this.patchState({ loadingOnQuantityChangeById });
- }
- }
- readonly loadingOnQuantityChangeById$ = this.select((s) => s.loadingOnQuantityChangeById).pipe(shareReplay());
-
- @Input()
- quantityError: string;
-
- isDummy$ = this.item$.pipe(
- map((item) => item?.availability?.supplyChannel === 'MANUALLY'),
- shareReplay(),
- );
- hasOrderType$ = this.orderType$.pipe(
- map((orderType) => orderType !== undefined),
- shareReplay(),
- );
-
- canEdit$ = combineLatest([this.isDummy$, this.hasOrderType$, this.item$]).pipe(
- map(([isDummy, hasOrderType, item]) => {
- if (item.itemType === (66560 as ItemType)) {
- return false;
- }
- return isDummy || hasOrderType;
- }),
- );
-
- quantityRange$ = combineLatest([this.orderType$, this.item$]).pipe(
- map(([orderType, item]) => (orderType === 'Rücklage' ? item.availability?.inStock : 999)),
- );
-
- isDownloadAvailable$ = combineLatest([this.item$, this.orderType$]).pipe(
- filter(([_, orderType]) => orderType === 'Download'),
- switchMap(([item]) =>
- this.availabilityService.getDownloadAvailability({
- item: { ean: item.product.ean, price: item.availability.price, itemId: +item.product.catalogProductNumber },
- }),
- ),
- map((availability) => availability && this.availabilityService.isAvailable({ availability })),
- );
-
- olaError$ = this.checkoutService
- .getOlaErrors({ processId: this.application.activatedProcessId })
- .pipe(map((ids) => ids?.find((id) => id === this.item.id)));
-
- get productSearchResultsPath() {
- return this._productNavigationService.getArticleSearchResultsPath(this.application.activatedProcessId).path;
- }
-
- get productSearchDetailsPath() {
- return this._productNavigationService.getArticleDetailsPathByEan({
- processId: this.application.activatedProcessId,
- ean: this.item?.product?.ean,
- }).path;
- }
-
- get isTablet() {
- return this._environment.matchTablet();
- }
-
- refreshingAvailabilit$ = this.select((s) => s.refreshingAvailability);
-
- sscChanged$ = this.select((s) => s.sscChanged || s.sscTextChanged);
-
- estimatedShippingDateChanged$ = this.select((s) => s.estimatedShippingDateChanged);
-
- notAvailable$ = this.item$.pipe(
- map((item) => {
- const availability = item?.availability;
-
- if (availability.availabilityType === 0) {
- return false;
- }
-
- if (availability.inStock && item.quantity > availability.inStock) {
- return true;
- }
-
- return !this.availabilityService.isAvailable({ availability });
- }),
- );
-
- constructor(
- private availabilityService: DomainAvailabilityService,
- private checkoutService: DomainCheckoutService,
- public application: ApplicationService,
- private _productNavigationService: ProductCatalogNavigationService,
- private _environment: EnvironmentService,
- private _cdr: ChangeDetectorRef,
- ) {
- super({
- item: undefined,
- orderType: '',
- refreshingAvailability: false,
- sscChanged: false,
- sscTextChanged: false,
- estimatedShippingDateChanged: false,
- });
- }
-
- ngOnInit() {}
-
- async onChangeItem() {
- const isDummy = await this.isDummy$.pipe(first()).toPromise();
- isDummy
- ? this.changeDummyItem.emit({ shoppingCartItem: this.item })
- : this.changeItem.emit({ shoppingCartItem: this.item });
- }
-
- onChangeQuantity(quantity: number) {
- this.changeQuantity.emit({ shoppingCartItem: this.item, quantity });
- }
-
- async refreshAvailability() {
- const currentAvailability = cloneDeep(this.item.availability);
-
- try {
- this.patchRefreshingAvailability(true);
- this._cdr.markForCheck();
- const availability = await this.checkoutService.refreshAvailability({
- processId: this.application.activatedProcessId,
- shoppingCartItemId: this.item.id,
- });
-
- if (currentAvailability.ssc !== availability.ssc) {
- this.sscChanged();
- }
- if (currentAvailability.sscText !== availability.sscText) {
- this.ssctextChanged();
- }
- if (
- moment(currentAvailability.estimatedShippingDate)
- .startOf('day')
- .diff(moment(availability.estimatedShippingDate).startOf('day'))
- ) {
- this.estimatedShippingDateChanged();
- }
- } catch (error) {}
-
- this.patchRefreshingAvailability(false);
- this._cdr.markForCheck();
- }
-
- patchRefreshingAvailability(value: boolean) {
- this._zone.run(() => {
- this.patchState({ refreshingAvailability: value });
- this._cdr.markForCheck();
- });
- }
-
- ssctextChanged() {
- this.patchState({ sscTextChanged: true });
- this._cdr.markForCheck();
- }
-
- sscChanged() {
- this.patchState({ sscChanged: true });
- this._cdr.markForCheck();
- }
-
- estimatedShippingDateChanged() {
- this.patchState({ estimatedShippingDateChanged: true });
- this._cdr.markForCheck();
- }
-}
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ EventEmitter,
+ Input,
+ NgZone,
+ OnInit,
+ Output,
+ inject,
+} from '@angular/core';
+import { ApplicationService } from '@core/application';
+import { EnvironmentService } from '@core/environment';
+import { DomainAvailabilityService } from '@domain/availability';
+import { DomainCheckoutService } from '@domain/checkout';
+import { ComponentStore } from '@ngrx/component-store';
+import { ProductCatalogNavigationService } from '@shared/services/navigation';
+import { ItemType, ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
+import { cloneDeep } from 'lodash';
+import moment from 'moment';
+import { combineLatest } from 'rxjs';
+import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
+
+export interface ShoppingCartItemComponentState {
+ item: ShoppingCartItemDTO;
+ orderType: string;
+ loadingOnItemChangeById?: number;
+ loadingOnQuantityChangeById?: number;
+ refreshingAvailability: boolean;
+ sscChanged: boolean;
+ sscTextChanged: boolean;
+ estimatedShippingDateChanged: boolean;
+}
+
+@Component({
+ selector: 'page-shopping-cart-item',
+ templateUrl: 'shopping-cart-item.component.html',
+ styleUrls: ['shopping-cart-item.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: false,
+})
+export class ShoppingCartItemComponent
+ extends ComponentStore
+ implements OnInit
+{
+ private _zone = inject(NgZone);
+
+ @Output() changeItem = new EventEmitter<{
+ shoppingCartItem: ShoppingCartItemDTO;
+ }>();
+ @Output() changeDummyItem = new EventEmitter<{
+ shoppingCartItem: ShoppingCartItemDTO;
+ }>();
+ @Output() changeQuantity = new EventEmitter<{
+ shoppingCartItem: ShoppingCartItemDTO;
+ quantity: number;
+ }>();
+
+ @Input()
+ get item() {
+ return this.get((s) => s.item);
+ }
+ set item(item: ShoppingCartItemDTO) {
+ if (this.item !== item) {
+ this.patchState({ item });
+ }
+ }
+ readonly item$ = this.select((s) => s.item);
+
+ readonly contributors$ = this.item$.pipe(
+ map((item) =>
+ item?.product?.contributors?.split(';').map((val) => val.trim()),
+ ),
+ );
+
+ get showLoyaltyValue() {
+ return this.item?.loyalty?.value > 0;
+ }
+
+ @Input()
+ get orderType() {
+ return this.get((s) => s.orderType);
+ }
+ set orderType(orderType: string) {
+ if (this.orderType !== orderType) {
+ this.patchState({ orderType });
+ }
+ }
+ readonly orderType$ = this.select((s) => s.orderType);
+
+ @Input()
+ get loadingOnItemChangeById() {
+ return this.get((s) => s.loadingOnItemChangeById);
+ }
+ set loadingOnItemChangeById(loadingOnItemChangeById: number) {
+ if (this.loadingOnItemChangeById !== loadingOnItemChangeById) {
+ this.patchState({ loadingOnItemChangeById });
+ }
+ }
+ readonly loadingOnItemChangeById$ = this.select(
+ (s) => s.loadingOnItemChangeById,
+ ).pipe(shareReplay());
+
+ @Input()
+ get loadingOnQuantityChangeById() {
+ return this.get((s) => s.loadingOnQuantityChangeById);
+ }
+ set loadingOnQuantityChangeById(loadingOnQuantityChangeById: number) {
+ if (this.loadingOnQuantityChangeById !== loadingOnQuantityChangeById) {
+ this.patchState({ loadingOnQuantityChangeById });
+ }
+ }
+ readonly loadingOnQuantityChangeById$ = this.select(
+ (s) => s.loadingOnQuantityChangeById,
+ ).pipe(shareReplay());
+
+ @Input()
+ quantityError: string;
+
+ isDummy$ = this.item$.pipe(
+ map((item) => item?.availability?.supplyChannel === 'MANUALLY'),
+ shareReplay(),
+ );
+ hasOrderType$ = this.orderType$.pipe(
+ map((orderType) => orderType !== undefined),
+ shareReplay(),
+ );
+
+ canEdit$ = combineLatest([
+ this.isDummy$,
+ this.hasOrderType$,
+ this.item$,
+ ]).pipe(
+ map(([isDummy, hasOrderType, item]) => {
+ if (item.itemType === (66560 as ItemType)) {
+ return false;
+ }
+ return isDummy || hasOrderType;
+ }),
+ );
+
+ quantityRange$ = combineLatest([this.orderType$, this.item$]).pipe(
+ map(([orderType, item]) =>
+ orderType === 'Rücklage' ? item.availability?.inStock : 999,
+ ),
+ );
+
+ isDownloadAvailable$ = combineLatest([this.item$, this.orderType$]).pipe(
+ filter(([, orderType]) => orderType === 'Download'),
+ switchMap(([item]) =>
+ this.availabilityService.getDownloadAvailability({
+ item: {
+ ean: item.product.ean,
+ price: item.availability.price,
+ itemId: +item.product.catalogProductNumber,
+ },
+ }),
+ ),
+ map(
+ (availability) =>
+ availability && this.availabilityService.isAvailable({ availability }),
+ ),
+ );
+
+ olaError$ = this.checkoutService
+ .getOlaErrors({ processId: this.application.activatedProcessId })
+ .pipe(map((ids) => ids?.find((id) => id === this.item.id)));
+
+ get productSearchResultsPath() {
+ return this._productNavigationService.getArticleSearchResultsPath(
+ this.application.activatedProcessId,
+ ).path;
+ }
+
+ get productSearchDetailsPath() {
+ return this._productNavigationService.getArticleDetailsPathByEan({
+ processId: this.application.activatedProcessId,
+ ean: this.item?.product?.ean,
+ }).path;
+ }
+
+ get isTablet() {
+ return this._environment.matchTablet();
+ }
+
+ refreshingAvailabilit$ = this.select((s) => s.refreshingAvailability);
+
+ sscChanged$ = this.select((s) => s.sscChanged || s.sscTextChanged);
+
+ estimatedShippingDateChanged$ = this.select(
+ (s) => s.estimatedShippingDateChanged,
+ );
+
+ notAvailable$ = this.item$.pipe(
+ map((item) => {
+ const availability = item?.availability;
+
+ if (availability.availabilityType === 0) {
+ return false;
+ }
+
+ if (availability.inStock && item.quantity > availability.inStock) {
+ return true;
+ }
+
+ return !this.availabilityService.isAvailable({ availability });
+ }),
+ );
+
+ constructor(
+ private availabilityService: DomainAvailabilityService,
+ private checkoutService: DomainCheckoutService,
+ public application: ApplicationService,
+ private _productNavigationService: ProductCatalogNavigationService,
+ private _environment: EnvironmentService,
+ private _cdr: ChangeDetectorRef,
+ ) {
+ super({
+ item: undefined,
+ orderType: '',
+ refreshingAvailability: false,
+ sscChanged: false,
+ sscTextChanged: false,
+ estimatedShippingDateChanged: false,
+ });
+ }
+
+ ngOnInit() {
+ // Component initialization
+ }
+
+ async onChangeItem() {
+ const isDummy = await this.isDummy$.pipe(first()).toPromise();
+ if (isDummy) {
+ this.changeDummyItem.emit({ shoppingCartItem: this.item });
+ } else {
+ this.changeItem.emit({ shoppingCartItem: this.item });
+ }
+ }
+
+ onChangeQuantity(quantity: number) {
+ this.changeQuantity.emit({ shoppingCartItem: this.item, quantity });
+ }
+
+ async refreshAvailability() {
+ const currentAvailability = cloneDeep(this.item.availability);
+
+ try {
+ this.patchRefreshingAvailability(true);
+ this._cdr.markForCheck();
+ const availability = await this.checkoutService.refreshAvailability({
+ processId: this.application.activatedProcessId,
+ shoppingCartItemId: this.item.id,
+ });
+
+ if (currentAvailability.ssc !== availability.ssc) {
+ this.sscChanged();
+ }
+ if (currentAvailability.sscText !== availability.sscText) {
+ this.ssctextChanged();
+ }
+ if (
+ moment(currentAvailability.estimatedShippingDate)
+ .startOf('day')
+ .diff(moment(availability.estimatedShippingDate).startOf('day'))
+ ) {
+ this.estimatedShippingDateChanged();
+ }
+ } catch {
+ // Error handling for availability refresh
+ }
+
+ this.patchRefreshingAvailability(false);
+ this._cdr.markForCheck();
+ }
+
+ patchRefreshingAvailability(value: boolean) {
+ this._zone.run(() => {
+ this.patchState({ refreshingAvailability: value });
+ this._cdr.markForCheck();
+ });
+ }
+
+ ssctextChanged() {
+ this.patchState({ sscTextChanged: true });
+ this._cdr.markForCheck();
+ }
+
+ sscChanged() {
+ this.patchState({ sscChanged: true });
+ this._cdr.markForCheck();
+ }
+
+ estimatedShippingDateChanged() {
+ this.patchState({ estimatedShippingDateChanged: true });
+ this._cdr.markForCheck();
+ }
+}
diff --git a/apps/isa-app/src/shared/shell/process-bar/process-bar-item/process-bar-item.component.ts b/apps/isa-app/src/shared/shell/process-bar/process-bar-item/process-bar-item.component.ts
index 823f1f415..b38278d0a 100644
--- a/apps/isa-app/src/shared/shell/process-bar/process-bar-item/process-bar-item.component.ts
+++ b/apps/isa-app/src/shared/shell/process-bar/process-bar-item/process-bar-item.component.ts
@@ -25,8 +25,9 @@ import {
combineLatest,
isObservable,
} from 'rxjs';
-import { map, switchMap, tap } from 'rxjs/operators';
+import { map, tap } from 'rxjs/operators';
import { TabService } from '@isa/core/tabs';
+import { CheckoutMetadataService } from '@isa/checkout/data-access';
@Component({
selector: 'shell-process-bar-item',
@@ -39,9 +40,22 @@ export class ShellProcessBarItemComponent
implements OnInit, OnDestroy, OnChanges
{
#tabService = inject(TabService);
+ #checkoutMetadataService = inject(CheckoutMetadataService);
tab = computed(() => this.#tabService.entityMap()[this.process().id]);
+ tabEffect = effect(() => {
+ console.log('tabEffect', this.tab());
+ });
+
+ shoppingCartId = computed(() => {
+ return this.#checkoutMetadataService.getShoppingCartId(this.process().id);
+ });
+
+ shoppingCartIdEffect = effect(() => {
+ console.log('shoppingCartIdEffect', this.shoppingCartId());
+ });
+
private _process$ = new BehaviorSubject(undefined);
process$ = this._process$.asObservable();
@@ -91,7 +105,7 @@ export class ShellProcessBarItemComponent
latestBreadcrumb$: Observable = NEVER;
- routerLink$: Observable = NEVER;
+ routerLink$: Observable = NEVER;
queryParams$: Observable