Merged PR 2008: fix(reward-print, reward-popup, reward-destination): improve reward cart stab...

fix(reward-print, reward-popup, reward-destination): improve reward cart stability and UX

- fix: remove console.log statement from calculate-price-value helper
- fix: add loading/pending state to print button to prevent duplicate prints
- fix: debounce reward selection resource reloading to prevent race conditions
- fix: correct reward cart item destination-info alignment and flex behavior
- fix: support OrderType in OrderDestinationComponent alongside OrderTypeFeature
- fix: use unitPrice instead of total for price calculations in reward items
- refactor: update calculatePriceValue test descriptions for clarity
- fix: fallback to order.orderType when features don't contain orderType

The reward selection popup now properly waits for all resources to reload
before resolving, preventing timing issues with cart synchronization.
Print button shows pending state during print operations.
Destination info components now handle both legacy OrderType and new
OrderTypeFeature enums for better compatibility.

Ref: #5442, #5445
This commit is contained in:
Nino Righi
2025-11-06 16:32:10 +00:00
committed by Lorenz Hilpert
parent af7bad03f5
commit f04e36e710
11 changed files with 34 additions and 38 deletions

View File

@@ -55,8 +55,6 @@ import {
RewardSelectionPopUpService,
} from '@isa/checkout/shared/reward-selection-dialog';
import { injectConfirmationDialog, injectFeedbackDialog } from '@isa/ui/dialog';
@Component({
selector: 'page-article-details',
templateUrl: 'article-details.component.html',
@@ -210,7 +208,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
).path;
}
showMore: boolean = false;
showMore = false;
@ViewChild('detailsContainer', { read: ElementRef, static: false })
detailsContainer: ElementRef;
@@ -610,7 +608,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
async navigateToResultList() {
const processId = this.applicationService.activatedProcessId;
let crumbs = await this.breadcrumb
const crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, [
'catalog',
'details',

View File

@@ -3,10 +3,10 @@ import { calculatePriceValue } from './calculate-price-value.helper';
import { RewardSelectionItem } from '@isa/checkout/data-access';
describe('calculatePriceValue', () => {
it('should return item total price when available', () => {
it('should return item unit price when available', () => {
const item: RewardSelectionItem = {
item: {
total: { value: { value: 99.99 } },
unitPrice: { value: { value: 99.99 } },
},
cartQuantity: 1,
rewardCartQuantity: 0,
@@ -20,7 +20,7 @@ describe('calculatePriceValue', () => {
expect(result).toBe(99.99);
});
it('should return availability price when total price not available', () => {
it('should return availability price when unit price not available', () => {
const item: RewardSelectionItem = {
item: {
availability: { price: { value: { value: 79.99 } } },

View File

@@ -3,13 +3,14 @@ import { RewardSelectionItem } from '@isa/checkout/data-access';
export const calculatePriceValue = (
rewardSelectionItem: RewardSelectionItem,
): number => {
const itemTotalPrice = rewardSelectionItem.item?.total?.value?.value;
console.log(rewardSelectionItem);
const itemUnitPrice = rewardSelectionItem.item?.unitPrice?.value?.value;
const availabilityPrice =
rewardSelectionItem.item?.availability?.price?.value?.value;
const catalogPrice = rewardSelectionItem.catalogPrice?.value?.value;
if (itemTotalPrice != null && itemTotalPrice !== 0) {
return itemTotalPrice;
if (itemUnitPrice != null && itemUnitPrice !== 0) {
return itemUnitPrice;
}
if (availabilityPrice != null && availabilityPrice !== 0) {

View File

@@ -9,9 +9,7 @@ import {
ProductInfoComponent,
DisplayOrderDestinationInfoComponent,
} from '@isa/checkout/shared/product-info';
import {
DisplayOrderItemDTO,
} from '@generated/swagger/oms-api';
import { DisplayOrderItemDTO } from '@generated/swagger/oms-api';
import { Product } from '@isa/common/data-access';
import { type OrderItemGroup } from '@isa/checkout/data-access';
@@ -34,7 +32,7 @@ export class OrderConfirmationItemListItemComponent {
* This now receives an OrderItemGroup which contains the necessary order information
* (features, targetBranch, shippingAddress) required by the DisplayOrderDestinationInfoComponent.
*/
order = input.required<Pick<OrderItemGroup, 'features' | 'targetBranch' | 'shippingAddress'>>();
order = input.required<OrderItemGroup>();
productItem = computed<Product | undefined>(() => {
return this.item()?.product;

View File

@@ -29,7 +29,7 @@
@if (!isHorizontal()) {
<checkout-destination-info
[underline]="true"
class="cursor-pointer mt-4 max-w-[14.25rem] grow-0 shrink-0"
class="cursor-pointer mt-4 w-[14.25rem] grow items-end"
(click)="updatePurchaseOption()"
[shoppingCartItem]="itm"
></checkout-destination-info>

View File

@@ -1,3 +1,3 @@
:host {
@apply contents;
@apply flex;
}

View File

@@ -1,7 +1,7 @@
import { Component, computed, inject, input } from '@angular/core';
import { Component, computed, input } from '@angular/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { getOrderTypeFeature } from '@isa/checkout/data-access';
import { DisplayOrder, DisplayOrderItem } from '@isa/oms/data-access';
import { OrderItemGroup } from '@isa/checkout/data-access';
import { DisplayOrderItem } from '@isa/oms/data-access';
import {
OrderDestinationComponent,
ShippingAddress,
@@ -22,15 +22,13 @@ export class DisplayOrderDestinationInfoComponent {
});
// Accept the parent DisplayOrder (required for branch info)
order = input.required<DisplayOrder>();
order = input.required<OrderItemGroup>();
// Optionally accept DisplayOrderItem (for potential future item-specific logic)
item = input<DisplayOrderItem>();
orderType = computed(() => {
const order = this.order();
const features = order.features;
return getOrderTypeFeature(features);
return this.order().orderType;
});
mappedBranch = computed(() => {

View File

@@ -16,7 +16,7 @@ import {
import { PrimaryCustomerCardResource } from '@isa/crm/data-access';
import { firstValueFrom } from 'rxjs';
import { toObservable } from '@angular/core/rxjs-interop';
import { filter, first } from 'rxjs/operators';
import { debounceTime, filter } from 'rxjs/operators';
import {
PriceAndRedemptionPointsResource,
ItemWithOrderType,
@@ -177,15 +177,14 @@ export class RewardSelectionService {
}
async reloadResources(): Promise<void> {
// Start reloading all resources
// Note: PrimaryCustomerCard, Price and redemption points will be loaded automatically by the effect
// when selectionItemsWithOrderType changes after cart resources are reloaded
this.#shoppingCartResource.reload();
this.#rewardShoppingCartResource.reload();
// Wait until all resources are fully loaded (isLoading becomes false)
await firstValueFrom(
this.#isLoading$.pipe(filter((isLoading) => !isLoading)),
this.#isLoading$.pipe(
debounceTime(50),
filter((isLoading) => !isLoading),
),
);
}
}

View File

@@ -3,6 +3,8 @@
data-which="print"
class="self-start"
(click)="print()"
[pending]="printing()"
[disabled]="printing()"
uiInfoButton
>
<span uiInfoButtonLabel><ng-content></ng-content></span>

View File

@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { OrderDestinationComponent } from './order-destination.component';
import { OrderType } from '@isa/common/data-access';
import { OrderTypeFeature } from '@isa/common/data-access';
describe('OrderDestinationComponent', () => {
let component: OrderDestinationComponent;
@@ -23,28 +23,28 @@ describe('OrderDestinationComponent', () => {
});
it('should display delivery icon for delivery order type', () => {
fixture.componentRef.setInput('orderType', OrderType.Delivery);
fixture.componentRef.setInput('orderType', OrderTypeFeature.Delivery);
fixture.detectChanges();
expect(component.destinationIcon()).toBe('isaDeliveryVersand');
});
it('should display pickup icon for pickup order type', () => {
fixture.componentRef.setInput('orderType', OrderType.Pickup);
fixture.componentRef.setInput('orderType', OrderTypeFeature.Pickup);
fixture.detectChanges();
expect(component.destinationIcon()).toBe('isaDeliveryRuecklage2');
});
it('should display in-store icon for in-store order type', () => {
fixture.componentRef.setInput('orderType', OrderType.InStore);
fixture.componentRef.setInput('orderType', OrderTypeFeature.InStore);
fixture.detectChanges();
expect(component.destinationIcon()).toBe('isaDeliveryRuecklage1');
});
it('should display branch name when branch is provided', () => {
fixture.componentRef.setInput('orderType', OrderType.Pickup);
fixture.componentRef.setInput('orderType', OrderTypeFeature.Pickup);
fixture.componentRef.setInput('branch', { name: 'Test Branch' });
fixture.detectChanges();
@@ -52,7 +52,7 @@ describe('OrderDestinationComponent', () => {
});
it('should display shipping address name for delivery', () => {
fixture.componentRef.setInput('orderType', OrderType.Delivery);
fixture.componentRef.setInput('orderType', OrderTypeFeature.Delivery);
fixture.componentRef.setInput('shippingAddress', {
firstName: 'John',
lastName: 'Doe',
@@ -68,7 +68,7 @@ describe('OrderDestinationComponent', () => {
city: 'Vienna',
zipCode: '1010',
};
fixture.componentRef.setInput('orderType', OrderType.Delivery);
fixture.componentRef.setInput('orderType', OrderTypeFeature.Delivery);
fixture.componentRef.setInput('shippingAddress', {
firstName: 'John',
lastName: 'Doe',
@@ -85,7 +85,7 @@ describe('OrderDestinationComponent', () => {
city: 'Vienna',
zipCode: '1020',
};
fixture.componentRef.setInput('orderType', OrderType.Pickup);
fixture.componentRef.setInput('orderType', OrderTypeFeature.Pickup);
fixture.componentRef.setInput('branch', {
name: 'Test Branch',
address: testAddress,

View File

@@ -9,7 +9,7 @@ import {
} from '@isa/icons';
import { InlineAddressComponent } from '@isa/shared/address';
import { logger } from '@isa/core/logging';
import { OrderTypeFeature } from '@isa/common/data-access';
import { OrderType, OrderTypeFeature } from '@isa/common/data-access';
export type Address = {
apartment?: string;