Compare commits

...

4 Commits

Author SHA1 Message Date
Nino
f0acfb6af1 feature(ui-dialog): Adjusted Feedback Error Dialog displaying invalidErrors if available
Ref: #5417
2025-12-19 17:44:21 +01:00
Nino
d9b653073b fix(isa-app-customer): Fixed selection of other addresses in customer area
Ref: #5522
2025-12-18 17:51:44 +01:00
Nino Righi
de3edaa0f9 Merged PR 2077: fix(checkout-data-access, checkout-reward-shopping-cart, checkout-reward-sele...
fix(checkout-data-access, checkout-reward-shopping-cart, checkout-reward-selection-dialog): Show Low Stock message inside Dialog, Adjusted Item Identifyer so that mergedItems inside reward-selection-dialog service works properly, Adjusted Error Message Logic and Quantity Select Logic based on purchasing Options for Abholung

Ref: #5523
2025-12-10 17:12:47 +00:00
Nino Righi
964a6026a0 Merged PR 2076: fix(common-data-access, crm-data-access): Improved Error handling, handling i...
fix(common-data-access, crm-data-access): Improved Error handling, handling invalidProperties errors corretly inside crm customer card area

Refs: #5528, #5529
2025-12-10 17:11:22 +00:00
18 changed files with 229 additions and 41 deletions

View File

@@ -166,6 +166,11 @@ export class DetailsMainViewBillingAddressesComponent
customer as unknown as Customer,
),
);
// Clear the selected payer ID when using customer address
this.crmTabMetadataService.setSelectedPayerId(
this.tabId(),
undefined,
);
}
});
}

View File

@@ -191,6 +191,11 @@ export class DetailsMainViewDeliveryAddressesComponent
customer as unknown as Customer,
),
);
// Clear the selected shipping address ID when using customer address
this.crmTabMetadataService.setSelectedShippingAddressId(
this.tabId(),
undefined,
);
}
});
}

View File

@@ -4,12 +4,13 @@ import {
} from '@isa/checkout/data-access';
/**
* Creates a unique key for an item based on EAN, destination, and orderItemType.
* Creates a unique key for an item based on EAN, targetBranchId, and orderItemType.
* Items are only considered identical if all three match.
*/
export const getItemKey = (item: ShoppingCartItem): string => {
const ean = item.product.ean ?? 'no-ean';
const destinationId = item.destination?.data?.id ?? 'no-destination';
const targetBranchId =
item.destination?.data?.targetBranch?.id ?? 'no-target-branch-id';
const orderType = getOrderTypeFeature(item.features) ?? 'no-orderType';
return `${ean}|${destinationId}|${orderType}`;
return `${ean}|${targetBranchId}|${orderType}`;
};

View File

@@ -66,7 +66,8 @@ export class RewardShoppingCartItemQuantityControlComponent {
if (
orderType === OrderTypeFeature.Delivery ||
orderType === OrderTypeFeature.DigitalShipping ||
orderType === OrderTypeFeature.B2BShipping
orderType === OrderTypeFeature.B2BShipping ||
orderType === OrderTypeFeature.Pickup
) {
return 999;
}

View File

@@ -37,11 +37,20 @@
</div>
</div>
@if (quantityControl.maxQuantity() < 2 && !isDownload()) {
<div
class="text-isa-accent-red isa-text-body-2-bold 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>
@if (!isDownload()) {
@if (showLowStockMessage()) {
<div
class="text-isa-accent-red isa-text-body-2-bold flex flex-row items-center gap-2"
>
<ng-icon name="isaOtherInfo" size="1.5rem"></ng-icon>
<div>{{ inStock() }} Exemplare sofort lieferbar</div>
</div>
} @else if (quantityControl.maxQuantity() < 2) {
<div
class="text-isa-accent-red isa-text-body-2-bold 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>
}
}

View File

@@ -72,6 +72,16 @@ export class RewardShoppingCartItemComponent {
hasOrderTypeFeature(this.item().features, ['Download']),
);
isAbholung = computed(() =>
hasOrderTypeFeature(this.item().features, ['Abholung']),
);
inStock = computed(() => this.item().availability?.inStock ?? 0);
showLowStockMessage = computed(() => {
return this.isAbholung() && this.inStock() < 2;
});
async updatePurchaseOption() {
const shoppingCartItemId = this.itemId();
const shoppingCartId = this.shoppingCartId();

View File

@@ -1,3 +1,3 @@
:host {
@apply text-isa-accent-red isa-text-body-2-bold flex flex-row gap-2 items-center;
@apply text-isa-accent-red isa-text-body-2-bold flex flex-col gap-2 items-start;
}

View File

@@ -1,8 +1,10 @@
@if (store.totalLoyaltyPointsNeeded() > store.customerRewardPoints()) {
<ng-icon
class="w-6 h-6 inline-flex items-center justify-center"
size="1.5rem"
name="isaOtherInfo"
></ng-icon>
<span>Lesepunkte reichen nicht für alle Artikel</span>
<div class="flex flex-row gap-2 items-center">
<ng-icon
class="w-6 h-6 inline-flex items-center justify-center"
size="1.5rem"
name="isaOtherInfo"
></ng-icon>
<span>Lesepunkte reichen nicht für alle Artikel</span>
</div>
}

View File

@@ -41,10 +41,7 @@ export class RewardSelectionInputsComponent {
hasCorrectOrderType = computed(() => {
const item = this.rewardSelectionItem().item;
return hasOrderTypeFeature(item.features, [
OrderTypeFeature.InStore,
OrderTypeFeature.Pickup,
]);
return hasOrderTypeFeature(item.features, [OrderTypeFeature.InStore]);
});
hasStock = computed(() => {

View File

@@ -27,3 +27,16 @@
<lib-reward-selection-inputs></lib-reward-selection-inputs>
</div>
@if (showLowStockMessage()) {
<div
class="flex flex-row gap-2 items-center text-isa-accent-red isa-text-body-2-bold"
>
<ng-icon
class="w-6 h-6 inline-flex items-center justify-center"
size="1.5rem"
name="isaOtherInfo"
></ng-icon>
<span>{{ inStock() }} Exemplare sofort lieferbar</span>
</div>
}

View File

@@ -1,8 +1,18 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
input,
} from '@angular/core';
import { ProductImageDirective } from '@isa/shared/product-image';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
import { RewardSelectionInputsComponent } from './reward-selection-inputs/reward-selection-inputs.component';
import { RewardSelectionItem } from '@isa/checkout/data-access';
import {
hasOrderTypeFeature,
RewardSelectionItem,
} from '@isa/checkout/data-access';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { isaOtherInfo } from '@isa/icons';
@Component({
selector: 'lib-reward-selection-item',
@@ -13,8 +23,24 @@ import { RewardSelectionItem } from '@isa/checkout/data-access';
ProductImageDirective,
ProductRouterLinkDirective,
RewardSelectionInputsComponent,
NgIcon,
],
providers: [provideIcons({ isaOtherInfo })],
})
export class RewardSelectionItemComponent {
rewardSelectionItem = input.required<RewardSelectionItem>();
inStock = computed(
() => this.rewardSelectionItem().item?.availability?.inStock ?? 0,
);
isAbholung = computed(() =>
hasOrderTypeFeature(this.rewardSelectionItem()?.item?.features, [
'Abholung',
]),
);
showLowStockMessage = computed(() => {
return this.isAbholung() && this.inStock() < 2;
});
}

View File

@@ -34,8 +34,15 @@ export const catchResponseArgsErrorPipe = <T>(): OperatorFunction<T, T> =>
return throwError(() => err);
}),
mergeMap((response) => {
if (isResponseArgs(response) && response.error === true) {
return throwError(() => new ResponseArgsError(response));
if (isResponseArgs(response)) {
// Treat as error if error flag is true OR if invalidProperties has entries
const hasInvalidProps =
response.invalidProperties &&
Object.keys(response.invalidProperties).length > 0;
if (response.error === true || hasInvalidProps) {
return throwError(() => new ResponseArgsError(response));
}
}
return [response];

View File

@@ -2,6 +2,7 @@ import { Injectable, inject, resource, signal, computed } from '@angular/core';
import { logger } from '@isa/core/logging';
import { CustomerBonRedemptionFacade } from '../facades/customer-bon-redemption.facade';
import { LoyaltyBonResponse } from '@generated/swagger/crm-api';
import { ResponseArgsError } from '@isa/common/data-access';
/**
* Resource for checking/validating Bon numbers.
@@ -47,8 +48,24 @@ export class CustomerBonCheckResource {
this.#logger.debug('Bon checked', () => ({
bonNr,
found: !!response?.result,
hasInvalidProperties:
!!response?.invalidProperties &&
Object.keys(response.invalidProperties).length > 0,
}));
// Check for invalidProperties even when error is false
// Backend may return { error: false, invalidProperties: {...} } for validation issues
if (
response?.invalidProperties &&
Object.keys(response.invalidProperties).length > 0
) {
this.#logger.warn('Bon check has invalid properties', () => ({
bonNr,
invalidProperties: response.invalidProperties,
}));
throw new ResponseArgsError(response);
}
return response?.result;
},
defaultValue: undefined,

View File

@@ -224,15 +224,10 @@ export class CrmSearchService {
req$ = req$.pipe(takeUntilAborted(abortSignal));
}
try {
const res = await firstValueFrom(req$);
this.#logger.debug('Successfully fetched current booking partner store');
const res = await firstValueFrom(req$);
this.#logger.debug('Successfully fetched current booking partner store');
return res?.result;
} catch (error) {
this.#logger.error('Error fetching current booking partner store', error);
return undefined;
}
return res?.result;
}
async addBooking(

View File

@@ -114,12 +114,7 @@ export class CrmFeatureCustomerBonRedemptionComponent {
}
// Handle API errors
else if (error) {
let errorMsg = 'Bon-Validierung fehlgeschlagen';
if (error instanceof ResponseArgsError) {
errorMsg = error.message || errorMsg;
} else if (error instanceof Error) {
errorMsg = error.message;
}
const errorMsg = this.#extractErrorMessage(error);
this.store.setError(errorMsg);
}
});
@@ -224,4 +219,23 @@ export class CrmFeatureCustomerBonRedemptionComponent {
this.store.reset();
this.#bonCheckResource.reset();
}
/**
* Extract error message from various error types.
* ResponseArgsError already formats invalidProperties into a readable message.
*/
#extractErrorMessage(error: unknown): string {
const defaultMsg = 'Bon-Validierung fehlgeschlagen';
const actualError = (error as { cause?: unknown })?.cause ?? error;
if (actualError instanceof ResponseArgsError) {
return actualError.message || defaultMsg;
}
if (actualError instanceof Error) {
return actualError.message || defaultMsg;
}
return defaultMsg;
}
}

View File

@@ -14,4 +14,17 @@
>
{{ errorMessage }}
</p>
@if (invalidProperties) {
<ul
class="w-full flex flex-col gap-2 text-isa-neutral-900"
data-what="invalid-properties"
>
@for (entry of invalidProperties | keyvalue; track entry.key) {
<li>
<strong>{{ entry.key }}:</strong> {{ entry.value }}
</li>
}
</ul>
}
</div>

View File

@@ -99,4 +99,56 @@ describe('FeedbackErrorDialogComponent', () => {
expect(iconElement).toHaveAttribute('size', '1.5rem');
});
});
describe('invalidProperties', () => {
it('should not display invalidProperties when not present', () => {
// Arrange
currentDialogData = { errorMessage: 'Test error' };
spectator = createComponent();
// Assert
expect(spectator.query('[data-what="invalid-properties"]')).toBeFalsy();
});
it('should display invalidProperties from error object', () => {
// Arrange
currentDialogData = {
error: {
invalidProperties: {
field1: 'Fehler bei Feld 1',
field2: 'Fehler bei Feld 2',
},
},
};
spectator = createComponent();
// Assert
const list = spectator.query('[data-what="invalid-properties"]');
expect(list).toBeTruthy();
expect(list).toHaveText('field1');
expect(list).toHaveText('Fehler bei Feld 1');
expect(list).toHaveText('field2');
expect(list).toHaveText('Fehler bei Feld 2');
});
it('should display invalidProperties from error.error (HttpErrorResponse)', () => {
// Arrange
currentDialogData = {
error: {
error: {
invalidProperties: {
email: 'Ungültige E-Mail',
},
},
},
};
spectator = createComponent();
// Assert
const list = spectator.query('[data-what="invalid-properties"]');
expect(list).toBeTruthy();
expect(list).toHaveText('email');
expect(list).toHaveText('Ungültige E-Mail');
});
});
});

View File

@@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KeyValuePipe } from '@angular/common';
import { DialogContentDirective } from '../dialog-content.directive';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { isaActionClose } from '@isa/icons';
@@ -26,7 +27,7 @@ export type FeedbackErrorDialogData =
selector: 'ui-feedback-error-dialog',
templateUrl: './feedback-error-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgIcon],
imports: [NgIcon, KeyValuePipe],
providers: [provideIcons({ isaActionClose })],
host: {
'[class]': '["ui-feedback-error-dialog"]',
@@ -54,4 +55,24 @@ export class FeedbackErrorDialogComponent extends DialogContentDirective<
}));
return 'Ein unbekannter Fehler ist aufgetreten';
}
/** Hole invalidProperties wenn vorhanden (direkt am error oder in error.error) */
get invalidProperties(): Record<string, string> | null {
if (!('error' in this.data)) return null;
const err = this.data.error as Record<string, unknown>;
// Check 1: Direkt auf error
if (err?.['invalidProperties']) {
return err['invalidProperties'] as Record<string, string>;
}
// Check 2: Auf error.error (HttpErrorResponse)
const inner = err?.['error'] as Record<string, unknown>;
if (inner?.['invalidProperties']) {
return inner['invalidProperties'] as Record<string, string>;
}
return null;
}
}