Merged PR 1984: fix(reward-confirmation): improve action card visibility and status messages

fix(reward-confirmation): improve action card visibility and status messages

Refactor confirmation action card to only display for items with 'Rücklage' feature.
Replace boolean completion check with state-based system using ProcessingStatusState
enum (Cancelled, NotFound, Collected). Add specific completion messages for each
state to provide clearer user feedback.

Changes:
- Add displayActionCard computed signal to check for 'Rücklage' feature
- Replace getProcessingStatusCompleted with getProcessingStatusState helper
- Add ProcessingStatusState enum with three states (Cancelled, NotFound, Collected)
- Update completion messages in template to use @switch based on processingStatus
- Wrap entire action card in @if block checking displayActionCard
- Add proper test coverage for new helper function
- Update component spec to provide required dependencies

Ref: #5391, #5404, #5406
This commit is contained in:
Nino Righi
2025-10-24 16:35:20 +00:00
committed by Lorenz Hilpert
parent 27541ab94a
commit 6e614683c5
10 changed files with 259 additions and 184 deletions

View File

@@ -1,64 +1,78 @@
<div @if (displayActionCard()) {
class="w-[24.5rem] h-full p-4 flex flex-col gap-4 rounded-lg bg-isa-secondary-100" <div
[class.confirmation-list-item-done]="item().status !== 1" class="w-[24.5rem] h-full p-4 flex flex-col gap-4 rounded-lg bg-isa-secondary-100"
> [class.confirmation-list-item-done]="item().status !== 1"
@if (!isComplete()) { >
<div @if (!isComplete()) {
data-what="confirmation-message" <div
data-which="confirmation-comment" data-what="confirmation-message"
class="isa-text-body-2-bold" data-which="confirmation-comment"
> class="isa-text-body-2-bold"
Bitte buchen Sie die Prämie aus dem Abholfach aus oder wählen Sie eine
andere Aktion.
</div>
<div class="flex flex-row justify-between items-center">
<ui-dropdown
class="h-8 border-none pl-0 hover:bg-transparent"
[value]="selectedAction()"
(valueChange)="setDropdownAction($event)"
> >
<ui-dropdown-option [value]="LoyaltyCollectType.Collect" Bitte buchen Sie die Prämie aus dem Abholfach aus oder wählen Sie eine
>Prämie ausbuchen</ui-dropdown-option andere Aktion.
> </div>
<ui-dropdown-option [value]="LoyaltyCollectType.OutOfStock"
>Nicht gefunden</ui-dropdown-option
>
<ui-dropdown-option [value]="LoyaltyCollectType.Cancel"
>Stornieren</ui-dropdown-option
>
</ui-dropdown>
<button <div class="flex flex-row justify-between items-center">
class="flex items-center gap-2 self-end" <ui-dropdown
type="button" class="h-8 border-none pl-0 hover:bg-transparent"
uiButton [value]="selectedAction()"
color="primary" (valueChange)="setDropdownAction($event)"
size="small" >
(click)="onCollect()" <ui-dropdown-option [value]="LoyaltyCollectType.Collect"
[pending]="isLoading()" >Prämie ausbuchen</ui-dropdown-option
[disabled]="isLoading()" >
data-what="button" <ui-dropdown-option [value]="LoyaltyCollectType.OutOfStock"
data-which="complete" >Nicht gefunden</ui-dropdown-option
>
<ui-dropdown-option [value]="LoyaltyCollectType.Cancel"
>Stornieren</ui-dropdown-option
>
</ui-dropdown>
<button
class="flex items-center gap-2 self-end"
type="button"
uiButton
color="primary"
size="small"
(click)="onCollect()"
[pending]="isLoading()"
[disabled]="isLoading()"
data-what="button"
data-which="complete"
>
<ng-icon name="isaActionCheck" uiButtonIcon></ng-icon>
Abschließen
</button>
</div>
} @else {
<div
data-what="done-message"
data-which="done-comment"
class="isa-text-body-2-bold"
> >
<ng-icon name="isaActionCheck" uiButtonIcon></ng-icon> @switch (processingStatus()) {
Abschließen @case (ProcessingStatusState.Cancelled) {
</button> Artikel wurde storniert und die Lesepunkte wieder gutgeschrieben.
</div> }
} @else { @case (ProcessingStatusState.NotFound) {
<div Die Prämienbestellung wurde storniert und die Lesepunkte wieder
data-what="done-message" gutgeschrieben. Bitte korrigieren Sie bei Bedarf den Filialbestand.
data-which="done-comment" }
class="isa-text-body-2-bold" @case (ProcessingStatusState.Collected) {
> Der Artikel wurde aus dem Bestand ausgebucht und kann dem Kunden
Artikel wurde Storniert und Lesepunkte gut geschrieben. mitgegeben werden.
</div> }
}
</div>
<span <span
class="flex items-center gap-2 self-end text-isa-accent-green isa-text-body-2-bold" class="flex items-center gap-2 self-end text-isa-accent-green isa-text-body-2-bold"
> >
<ng-icon name="isaActionCheck"></ng-icon> <ng-icon name="isaActionCheck"></ng-icon>
Abgeschlossen Abgeschlossen
</span> </span>
} }
</div> </div>
}

View File

@@ -13,7 +13,8 @@ import {
OrderRewardCollectFacade, OrderRewardCollectFacade,
LoyaltyCollectType, LoyaltyCollectType,
OrderItemSubsetResource, OrderItemSubsetResource,
getProcessingStatusCompleted, getProcessingStatusState,
ProcessingStatusState,
} from '@isa/oms/data-access'; } from '@isa/oms/data-access';
import { ButtonComponent } from '@isa/ui/buttons'; import { ButtonComponent } from '@isa/ui/buttons';
import { NgIcon } from '@ng-icons/core'; import { NgIcon } from '@ng-icons/core';
@@ -23,6 +24,7 @@ import {
DropdownButtonComponent, DropdownButtonComponent,
DropdownOptionComponent, DropdownOptionComponent,
} from '@isa/ui/input-controls'; } from '@isa/ui/input-controls';
import { hasOrderTypeFeature } from '@isa/checkout/data-access';
@Component({ @Component({
selector: 'checkout-confirmation-list-item-action-card', selector: 'checkout-confirmation-list-item-action-card',
@@ -39,6 +41,7 @@ import {
}) })
export class ConfirmationListItemActionCardComponent { export class ConfirmationListItemActionCardComponent {
LoyaltyCollectType = LoyaltyCollectType; LoyaltyCollectType = LoyaltyCollectType;
ProcessingStatusState = ProcessingStatusState;
#orderRewardCollectFacade = inject(OrderRewardCollectFacade); #orderRewardCollectFacade = inject(OrderRewardCollectFacade);
#store = inject(OrderConfiramtionStore); #store = inject(OrderConfiramtionStore);
#orderItemSubsetResource = inject(OrderItemSubsetResource); #orderItemSubsetResource = inject(OrderItemSubsetResource);
@@ -61,13 +64,22 @@ export class ConfirmationListItemActionCardComponent {
orderItemSubsets = this.#orderItemSubsetResource.orderItemSubsets; orderItemSubsets = this.#orderItemSubsetResource.orderItemSubsets;
selectedAction = signal<LoyaltyCollectType>(LoyaltyCollectType.Collect); selectedAction = signal<LoyaltyCollectType>(LoyaltyCollectType.Collect);
isComplete = computed(() => {
processingStatus = computed(() => {
const subsets = this.orderItemSubsets(); const subsets = this.orderItemSubsets();
const statuses = subsets?.map((subset) => subset.processingStatus); const statuses = subsets?.map((subset) => subset.processingStatus);
return getProcessingStatusCompleted(statuses); return getProcessingStatusState(statuses);
}); });
isLoading = signal(false); isLoading = signal(false);
isComplete = computed(() => {
return this.processingStatus() !== undefined;
});
displayActionCard = computed(() =>
hasOrderTypeFeature(this.item().features, ['Rücklage']),
);
constructor() { constructor() {
effect(() => { effect(() => {
const item = this.item(); const item = this.item();

View File

@@ -77,10 +77,12 @@ export class OrderConfirmationItemListItemComponent {
)?.data; )?.data;
// Fallback: use DisplayOrderItem features if not found in cart // Fallback: use DisplayOrderItem features if not found in cart
return foundItem ?? { return (
features: item.features, foundItem ?? {
availability: undefined, features: item.features,
destination: undefined, availability: undefined,
}; destination: undefined,
}
);
}); });
} }

View File

@@ -1,97 +0,0 @@
import { OrderItemProcessingStatusValue } from '../../schemas';
import { getProcessingStatusCompleted } from './get-processing-status-completed.helper';
describe('getProcessingStatusCompleted', () => {
it('should return true when all statuses are different from Bestellt (16)', () => {
// Arrange
const statuses = [
OrderItemProcessingStatusValue.Versendet, // 64
OrderItemProcessingStatusValue.Eingetroffen, // 128
OrderItemProcessingStatusValue.Abgeholt, // 256
];
// Act
const result = getProcessingStatusCompleted(statuses);
// Assert
expect(result).toBe(true);
});
it('should return true when statuses include various completed states', () => {
// Arrange
const statuses = [
OrderItemProcessingStatusValue.Zugestellt, // 4194304
OrderItemProcessingStatusValue.Abgeholt, // 256
OrderItemProcessingStatusValue.Versendet, // 64
];
// Act
const result = getProcessingStatusCompleted(statuses);
// Assert
expect(result).toBe(true);
});
it('should return false when at least one status is Bestellt (16)', () => {
// Arrange
const statuses = [
OrderItemProcessingStatusValue.Versendet, // 64
OrderItemProcessingStatusValue.Bestellt, // 16
OrderItemProcessingStatusValue.Abgeholt, // 256
];
// Act
const result = getProcessingStatusCompleted(statuses);
// Assert
expect(result).toBe(false);
});
it('should return false when all statuses are Bestellt (16)', () => {
// Arrange
const statuses = [
OrderItemProcessingStatusValue.Bestellt, // 16
OrderItemProcessingStatusValue.Bestellt, // 16
OrderItemProcessingStatusValue.Bestellt, // 16
];
// Act
const result = getProcessingStatusCompleted(statuses);
// Assert
expect(result).toBe(false);
});
it('should return false when array is empty', () => {
// Arrange
const statuses: number[] = [];
// Act
const result = getProcessingStatusCompleted(statuses);
// Assert
expect(result).toBe(false);
});
it('should return false when statuses is undefined', () => {
// Arrange
const statuses = undefined;
// Act
const result = getProcessingStatusCompleted(statuses);
// Assert
expect(result).toBe(false);
});
it('should return true with single status different from Bestellt', () => {
// Arrange
const statuses = [OrderItemProcessingStatusValue.Abgeholt]; // 256
// Act
const result = getProcessingStatusCompleted(statuses);
// Assert
expect(result).toBe(true);
});
});

View File

@@ -1,18 +0,0 @@
import { OrderItemProcessingStatusValue } from '../../schemas';
/**
* Checks if all processing statuses are completed (not in "Bestellt" state).
* Returns true if all statuses are different from "Bestellt" (16).
* Returns false if any status is still "Bestellt" (16) or if the array is empty/undefined.
*/
export const getProcessingStatusCompleted = (
statuses: number[] | undefined,
): boolean => {
if (!statuses || statuses.length === 0) {
return false;
}
return statuses.every(
(status) => status !== OrderItemProcessingStatusValue.Bestellt,
);
};

View File

@@ -0,0 +1,92 @@
import { OrderItemProcessingStatusValue } from '../../schemas';
import { ProcessingStatusState } from '../../models';
import { getProcessingStatusState } from './get-processing-status-state.helper';
describe('getProcessingStatusState', () => {
describe('Cancelled status', () => {
it('should return Cancelled when all items are cancelled', () => {
// Arrange
const statuses = [
OrderItemProcessingStatusValue.StorniertKunde, // 512
OrderItemProcessingStatusValue.Storniert, // 1024
OrderItemProcessingStatusValue.StorniertLieferant, // 2048
];
// Act
const result = getProcessingStatusState(statuses);
// Assert
expect(result).toBe(ProcessingStatusState.Cancelled);
});
});
describe('NotFound status', () => {
it('should return NotFound when all items are NichtLieferbar', () => {
// Arrange
const statuses = [
OrderItemProcessingStatusValue.NichtLieferbar, // 4096
OrderItemProcessingStatusValue.NichtLieferbar, // 4096
];
// Act
const result = getProcessingStatusState(statuses);
// Assert
expect(result).toBe(ProcessingStatusState.NotFound);
});
});
describe('Collected status', () => {
it('should return Collected when all items are Abgeholt', () => {
// Arrange
const statuses = [
OrderItemProcessingStatusValue.Abgeholt, // 256
OrderItemProcessingStatusValue.Abgeholt, // 256
];
// Act
const result = getProcessingStatusState(statuses);
// Assert
expect(result).toBe(ProcessingStatusState.Collected);
});
});
describe('Undefined cases', () => {
it('should return undefined when array is empty', () => {
// Arrange
const statuses: number[] = [];
// Act
const result = getProcessingStatusState(statuses);
// Assert
expect(result).toBeUndefined();
});
it('should return undefined when statuses is undefined', () => {
// Arrange
const statuses = undefined;
// Act
const result = getProcessingStatusState(statuses);
// Assert
expect(result).toBeUndefined();
});
it('should return undefined when items have mixed statuses', () => {
// Arrange
const statuses = [
OrderItemProcessingStatusValue.StorniertKunde, // 512
OrderItemProcessingStatusValue.Abgeholt, // 256
];
// Act
const result = getProcessingStatusState(statuses);
// Assert
expect(result).toBeUndefined();
});
});
});

View File

@@ -0,0 +1,55 @@
import { OrderItemProcessingStatusValue } from '../../schemas';
import { ProcessingStatusState } from '../../models';
/**
* Determines the completion status of order items based on their processing statuses.
*
* @param statuses - Array of processing status values to evaluate
* @returns The processing status state:
* - `ProcessingStatusState.Cancelled` if all items are cancelled
* - `ProcessingStatusState.NotFound` if all items are marked as not available
* - `ProcessingStatusState.Collected` if all items are collected
* - `undefined` if statuses don't match any completion state
*
* @example
* ```ts
* const statuses = [512, 1024]; // StorniertKunde, Storniert
* getProcessingStatusState(statuses); // ProcessingStatusState.Cancelled
* ```
*/
export const getProcessingStatusState = (
statuses: number[] | undefined,
): ProcessingStatusState | undefined => {
if (!statuses || statuses.length === 0) {
return undefined;
}
// Check if all statuses are cancelled
const allCancelled = statuses.every(
(status) =>
status === OrderItemProcessingStatusValue.StorniertKunde ||
status === OrderItemProcessingStatusValue.Storniert ||
status === OrderItemProcessingStatusValue.StorniertLieferant,
);
if (allCancelled) {
return ProcessingStatusState.Cancelled;
}
// Check if all statuses are not available
const allNotFound = statuses.every(
(status) => status === OrderItemProcessingStatusValue.NichtLieferbar,
);
if (allNotFound) {
return ProcessingStatusState.NotFound;
}
// Check if all statuses are collected
const allCollected = statuses.every(
(status) => status === OrderItemProcessingStatusValue.Abgeholt,
);
if (allCollected) {
return ProcessingStatusState.Collected;
}
return undefined;
};

View File

@@ -1 +1 @@
export * from './get-processing-status-completed.helper'; export * from './get-processing-status-state.helper';

View File

@@ -5,6 +5,7 @@ export * from './eligible-for-return';
export * from './gender'; export * from './gender';
export * from './logistician'; export * from './logistician';
export * from './order'; export * from './order';
export * from './processing-status-state';
export * from './quantity'; export * from './quantity';
export * from './receipt-item-list-item'; export * from './receipt-item-list-item';
export * from './receipt-item-task-list-item'; export * from './receipt-item-task-list-item';

View File

@@ -0,0 +1,14 @@
/**
* Processing status state types for order items
*/
export const ProcessingStatusState = {
/** Item was cancelled by customer, merchant, or supplier */
Cancelled: 'cancelled',
/** Item was not found / not available */
NotFound: 'not-found',
/** Item was successfully collected */
Collected: 'collected',
} as const;
export type ProcessingStatusState =
(typeof ProcessingStatusState)[keyof typeof ProcessingStatusState];