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
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
data-what="confirmation-message"
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)"
@if (displayActionCard()) {
<div
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
data-what="confirmation-message"
data-which="confirmation-comment"
class="isa-text-body-2-bold"
>
<ui-dropdown-option [value]="LoyaltyCollectType.Collect"
>Prämie ausbuchen</ui-dropdown-option
>
<ui-dropdown-option [value]="LoyaltyCollectType.OutOfStock"
>Nicht gefunden</ui-dropdown-option
>
<ui-dropdown-option [value]="LoyaltyCollectType.Cancel"
>Stornieren</ui-dropdown-option
>
</ui-dropdown>
Bitte buchen Sie die Prämie aus dem Abholfach aus oder wählen Sie eine
andere Aktion.
</div>
<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"
<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"
>Prämie ausbuchen</ui-dropdown-option
>
<ui-dropdown-option [value]="LoyaltyCollectType.OutOfStock"
>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>
Abschließen
</button>
</div>
} @else {
<div
data-what="done-message"
data-which="done-comment"
class="isa-text-body-2-bold"
>
Artikel wurde Storniert und Lesepunkte gut geschrieben.
</div>
@switch (processingStatus()) {
@case (ProcessingStatusState.Cancelled) {
Artikel wurde storniert und die Lesepunkte wieder gutgeschrieben.
}
@case (ProcessingStatusState.NotFound) {
Die Prämienbestellung wurde storniert und die Lesepunkte wieder
gutgeschrieben. Bitte korrigieren Sie bei Bedarf den Filialbestand.
}
@case (ProcessingStatusState.Collected) {
Der Artikel wurde aus dem Bestand ausgebucht und kann dem Kunden
mitgegeben werden.
}
}
</div>
<span
class="flex items-center gap-2 self-end text-isa-accent-green isa-text-body-2-bold"
>
<ng-icon name="isaActionCheck"></ng-icon>
Abgeschlossen
</span>
}
</div>
<span
class="flex items-center gap-2 self-end text-isa-accent-green isa-text-body-2-bold"
>
<ng-icon name="isaActionCheck"></ng-icon>
Abgeschlossen
</span>
}
</div>
}

View File

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

View File

@@ -77,10 +77,12 @@ export class OrderConfirmationItemListItemComponent {
)?.data;
// Fallback: use DisplayOrderItem features if not found in cart
return foundItem ?? {
features: item.features,
availability: undefined,
destination: undefined,
};
return (
foundItem ?? {
features: item.features,
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 './logistician';
export * from './order';
export * from './processing-status-state';
export * from './quantity';
export * from './receipt-item-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];