Merged PR 2020: feat(confirmation-list-item-action-card): improve action card visibility logi...

feat(confirmation-list-item-action-card): improve action card visibility logic and add ordered state

Enhance the action card display logic to show only when both feature flag
and command/completion conditions are met. Add support for "Ordered" state
to distinguish pending items from completed ones.

Changes:
- Add hasLoyaltyCollectCommand helper to check for LOYALTY_COLLECT_COMMAND
- Update displayActionCard logic to require both Rücklage feature AND
  (loyalty collect command OR completion state)
- Add ProcessingStatusState.Ordered to distinguish ordered vs completed items
- Update isComplete to exclude ordered items from completion state
- Move role-based visibility check to outer container level
- Remove unused CSS class for completed state
- Add comprehensive unit tests for new helpers

The action card now correctly appears only for items that need user action,
hiding for CallCenter role and for items without the required commands.

Ref: #5459
This commit is contained in:
Nino Righi
2025-11-11 12:16:17 +00:00
committed by Lorenz Hilpert
parent 4a7b74a6c5
commit 6df02d9e86
10 changed files with 208 additions and 9 deletions

View File

@@ -0,0 +1,122 @@
import { describe, it, expect } from 'vitest';
import { hasLoyaltyCollectCommand } from './has-loyalty-collect-command.helper';
import { DisplayOrderItemSubset } from '@isa/oms/data-access';
describe('hasLoyaltyCollectCommand', () => {
describe('when items is undefined', () => {
it('should return false', () => {
// Act
const result = hasLoyaltyCollectCommand(undefined);
// Assert
expect(result).toBe(false);
});
});
describe('when items is empty array', () => {
it('should return false', () => {
// Arrange
const items: DisplayOrderItemSubset[] = [];
// Act
const result = hasLoyaltyCollectCommand(items);
// Assert
expect(result).toBe(false);
});
});
describe('when items have no actions', () => {
it('should return false', () => {
// Arrange
const items: DisplayOrderItemSubset[] = [
{
id: 1,
quantity: 1,
} as DisplayOrderItemSubset,
];
// Act
const result = hasLoyaltyCollectCommand(items);
// Assert
expect(result).toBe(false);
});
});
describe('when items have actions but no LOYALTY_COLLECT_COMMAND', () => {
it('should return false', () => {
// Arrange
const items: any[] = [
{
id: 1,
quantity: 1,
actions: [
{ command: 'SOME_OTHER_COMMAND', label: 'Other Action' },
{ command: 'ANOTHER_COMMAND', label: 'Another Action' },
],
},
];
// Act
const result = hasLoyaltyCollectCommand(items);
// Assert
expect(result).toBe(false);
});
});
describe('when items have LOYALTY_COLLECT_COMMAND action', () => {
it('should return true', () => {
// Arrange
const items: any[] = [
{
id: 1,
quantity: 1,
actions: [
{
command: 'LOYALTY_COLLECT_COMMAND',
label: 'Abschließen',
selected: true,
value: 'Abschließen',
},
],
},
];
// Act
const result = hasLoyaltyCollectCommand(items);
// Assert
expect(result).toBe(true);
});
});
describe('when items have multiple actions including LOYALTY_COLLECT_COMMAND', () => {
it('should return true', () => {
// Arrange
const items: any[] = [
{
id: 1,
quantity: 1,
actions: [
{ command: 'SOME_OTHER_COMMAND', label: 'Other Action' },
{
command: 'LOYALTY_COLLECT_COMMAND',
label: 'Abschließen',
selected: true,
value: 'Abschließen',
},
{ command: 'ANOTHER_COMMAND', label: 'Another Action' },
],
},
];
// Act
const result = hasLoyaltyCollectCommand(items);
// Assert
expect(result).toBe(true);
});
});
});

View File

@@ -0,0 +1,17 @@
import { DisplayOrderItemSubset } from '@isa/oms/data-access';
/**
* Checks if any of the subset items has a LOYALTY_COLLECT_COMMAND action
* @param items - Array of DisplayOrderItemSubset to check
* @returns true if at least one item has a LOYALTY_COLLECT_COMMAND action
*/
export const hasLoyaltyCollectCommand = (
items?: DisplayOrderItemSubset[],
): boolean => {
const firstItem = items?.find((_) => true);
return (
firstItem?.actions?.some((action) =>
action?.command?.includes('LOYALTY_COLLECT_COMMAND'),
) ?? false
);
};

View File

@@ -1,5 +1,6 @@
export * from './get-order-type-feature.helper';
export * from './has-order-type-feature.helper';
export * from './has-loyalty-collect-command.helper';
export * from './checkout-analysis.helpers';
export * from './checkout-business-logic.helpers';
export * from './checkout-data.helpers';

View File

@@ -1,9 +1,9 @@
@if (displayActionCard()) {
<div
class="w-72 desktop-large:w-[24.5rem] justify-between h-full p-4 flex flex-col gap-4 rounded-lg bg-isa-secondary-100"
[class.confirmation-list-item-done]="item().status !== 1"
data-which="action-card"
data-what="action-card"
*ifNotRole="Role.CallCenter"
>
@if (!isComplete()) {
<div
@@ -16,7 +16,6 @@
</div>
<div
*ifNotRole="Role.CallCenter"
class="flex flex-col gap-[0.62rem] desktop-large:gap-0 desktop-large:flex-row desktop-large:justify-between desktop-large:items-center"
>
<ui-dropdown

View File

@@ -31,6 +31,7 @@ import {
import {
hasOrderTypeFeature,
buildItemQuantityMap,
hasLoyaltyCollectCommand,
} from '@isa/checkout/data-access';
import { IfRoleDirective, Role } from '@isa/core/auth';
@@ -92,11 +93,25 @@ export class ConfirmationListItemActionCardComponent {
});
isComplete = computed(() => {
return this.processingStatus() !== undefined;
return (
this.processingStatus() !== undefined &&
this.processingStatus() !== ProcessingStatusState.Ordered
);
});
displayActionCard = computed(() =>
hasOrderTypeFeature(this.item().features, ['Rücklage']),
/**
* #5459 - Determines whether the action card should be displayed for this order item.
*
* The action card is shown when ALL of the following conditions are met:
* - The item MUST have the 'Rücklage' order type feature
* - AND one of the following:
* - The item has a loyalty collect command available (for collecting rewards)
* - OR the item processing is complete (for displaying the completed state)
*/
displayActionCard = computed(
() =>
hasOrderTypeFeature(this.item().features, ['Rücklage']) &&
(hasLoyaltyCollectCommand(this.item().subsetItems) || this.isComplete()),
);
constructor() {

View File

@@ -3,6 +3,22 @@ import { ProcessingStatusState } from '../../models';
import { getProcessingStatusState } from './get-processing-status-state.helper';
describe('getProcessingStatusState', () => {
describe('Ordered status', () => {
it('should return Ordered when all items are Bestellt', () => {
// Arrange
const statuses = [
OrderItemProcessingStatusValue.Bestellt,
OrderItemProcessingStatusValue.Bestellt,
];
// Act
const result = getProcessingStatusState(statuses);
// Assert
expect(result).toBe(ProcessingStatusState.Ordered);
});
});
describe('Cancelled status', () => {
it('should return Cancelled when all items are cancelled', () => {
// Arrange
@@ -117,5 +133,19 @@ describe('getProcessingStatusState', () => {
// Assert
expect(result).toBeUndefined();
});
it('should return undefined when items have mix of ordered and cancelled', () => {
// Arrange
const statuses = [
OrderItemProcessingStatusValue.Bestellt,
OrderItemProcessingStatusValue.Storniert, // 1024
];
// Act
const result = getProcessingStatusState(statuses);
// Assert
expect(result).toBeUndefined();
});
});
});

View File

@@ -24,6 +24,14 @@ export const getProcessingStatusState = (
return undefined;
}
// Check if all statuses are ordered
const allOrdered = statuses.every(
(status) => status === OrderItemProcessingStatusValue.Bestellt,
);
if (allOrdered) {
return ProcessingStatusState.Ordered;
}
// Check if all statuses are cancelled
const allCancelled = statuses.every(
(status) =>

View File

@@ -2,6 +2,8 @@
* Processing status state types for order items
*/
export const ProcessingStatusState = {
/** Item is still ordered and pending processing */
Ordered: 'ordered',
/** Item was cancelled by customer, merchant, or supplier */
Cancelled: 'cancelled',
/** Item was not found / not available */

View File

@@ -1,10 +1,18 @@
import { EntitySchema, DateRangeSchema } from '@isa/common/data-access';
import {
EntitySchema,
DateRangeSchema,
KeyValueOfStringAndStringSchema,
} from '@isa/common/data-access';
import { z } from 'zod';
import { OrderItemProcessingStatusValueSchema } from './order-item-processing-status-value.schema';
// Forward declaration for circular reference
export const DisplayOrderItemSubsetSchema = z
.object({
actions: z
.array(KeyValueOfStringAndStringSchema)
.describe('Possible actions')
.optional(),
compartmentCode: z.string().describe('Compartment code').optional(),
compartmentInfo: z.string().describe('Compartment information').optional(),
compartmentStart: z.string().describe('Compartment start').optional(),