Merged PR 1985: 🐛 fix(reward-order-confirmation): group items by item-level delivery type

🐛 fix(reward-order-confirmation): group items by item-level delivery type

Fix incorrect delivery type headers by grouping order items based on
item.features.orderType instead of order.features.orderType, since items
within a single order can have different delivery types.

Changes:
- Add groupDisplayOrderItemsByDeliveryType helper to group items by delivery type
- Add groupDisplayOrderItemsByBranch helper to group items by branch
- Refactor OrderConfirmationItemListComponent to use item-level grouping
- Move list rendering and grouping logic from parent to child component
- Update template to use branchGroup.items instead of branchGroup.allItems

Fixes #5403

Related work items: #5403
This commit is contained in:
Lorenz Hilpert
2025-10-27 09:24:24 +00:00
committed by Nino Righi
parent 6e614683c5
commit 9239f8960d
7 changed files with 239 additions and 130 deletions

View File

@@ -0,0 +1,110 @@
import { DisplayOrderItem } from '@isa/oms/data-access';
import { OrderType } from '../models';
/**
* Represents a group of order items sharing the same branch (for pickup/in-store)
* or all items for delivery types that don't have branch-specific grouping.
*/
export type OrderItemBranchGroup = {
/** Branch ID (undefined for delivery types without branch grouping) */
branchId?: number;
/** Branch name (undefined for delivery types without branch grouping) */
branchName?: string;
/** Array of items in this branch group */
items: DisplayOrderItem[];
};
/**
* Order types that require grouping by branch/filiale.
* Other order types (Versand, DIG-Versand, etc.) are grouped together without branch subdivision.
*/
const ORDER_TYPES_WITH_BRANCH_GROUPING = [
OrderType.Pickup,
OrderType.InStore,
] as const;
/**
* Sorts branch groups by branch ID (ascending).
*/
const sortByBranchId = (
entries: [number, DisplayOrderItem[]][],
): [number, DisplayOrderItem[]][] => {
return [...entries].sort(([a], [b]) => a - b);
};
/**
* Groups display order items by their target branch.
* Only applies branch-level grouping for Abholung (Pickup) and Rücklage (InStore) order types.
* For other delivery types (Versand, DIG-Versand, etc.), returns a single group with all items.
*
* Uses item.order.targetBranch for grouping since items inherit their parent order's branch information.
*
* @param orderType - The order type to determine if branch grouping is needed
* @param items - Array of DisplayOrderItem objects to group
* @returns Array of OrderItemBranchGroup objects, each containing items for a specific branch
*
* @example
* ```typescript
* // For Abholung (pickup) items from different branches
* const pickupItems = [
* { id: 1, order: { targetBranch: { id: 1, name: 'München' } } },
* { id: 2, order: { targetBranch: { id: 2, name: 'Berlin' } } }
* ];
* const groups = groupDisplayOrderItemsByBranch('Abholung', pickupItems);
* // [
* // { branchId: 1, branchName: 'München', items: [item1] },
* // { branchId: 2, branchName: 'Berlin', items: [item2] }
* // ]
*
* // For Versand (delivery) items
* const deliveryItems = [
* { id: 1, ... },
* { id: 2, ... }
* ];
* const groups = groupDisplayOrderItemsByBranch('Versand', deliveryItems);
* // [
* // { branchId: undefined, branchName: undefined, items: [item1, item2] }
* // ]
* ```
*/
export function groupDisplayOrderItemsByBranch(
orderType: OrderType | string,
items: DisplayOrderItem[],
): OrderItemBranchGroup[] {
const needsBranchGrouping =
orderType === OrderType.Pickup || orderType === OrderType.InStore;
if (!needsBranchGrouping) {
// For delivery types without branch grouping, return single group with all items
return [
{
branchId: undefined,
branchName: undefined,
items,
},
];
}
// For Abholung/Rücklage, group by item.order.targetBranch.id
const branchGroups = items.reduce((map, item) => {
const branchId = item.order?.targetBranch?.id ?? 0;
if (!map.has(branchId)) {
map.set(branchId, []);
}
map.get(branchId)!.push(item);
return map;
}, new Map<number, DisplayOrderItem[]>());
// Convert Map to array of OrderItemBranchGroup, sorted by branch ID
return sortByBranchId(Array.from(branchGroups.entries())).map(
([branchId, branchItems]) => {
const branch = branchItems[0]?.order?.targetBranch;
return {
branchId: branchId || undefined,
branchName: branch?.name,
items: branchItems,
};
},
);
}

View File

@@ -0,0 +1,48 @@
import { DisplayOrder, DisplayOrderItem } from '@isa/oms/data-access';
import { getOrderTypeFeature } from './get-order-type-feature.helper';
import { OrderType } from '../models';
/**
* Groups display order items by their delivery type (item.features.orderType).
*
* Unlike groupDisplayOrdersByDeliveryType which groups entire orders,
* this groups individual items since items within one order can have different delivery types.
*
* @param orders - Array of DisplayOrder objects containing items to group
* @returns Map where keys are order types (Abholung, Rücklage, Versand, etc.)
* and values are arrays of items with that type
*
* @example
* ```typescript
* const orders = [
* {
* id: 1,
* features: { orderType: 'Abholung' },
* items: [
* { id: 1, features: { orderType: 'Abholung' }, ... },
* { id: 2, features: { orderType: 'Rücklage' }, ... }
* ]
* }
* ];
*
* const grouped = groupDisplayOrderItemsByDeliveryType(orders);
* // Map {
* // 'Abholung' => [item1],
* // 'Rücklage' => [item2]
* // }
* ```
*/
export function groupDisplayOrderItemsByDeliveryType(
orders: DisplayOrder[],
): Map<OrderType | string, DisplayOrderItem[]> {
const allItems = orders.flatMap((order) => order.items ?? []);
return allItems.reduce((map, item) => {
const orderType = getOrderTypeFeature(item.features) ?? 'Unbekannt';
if (!map.has(orderType)) {
map.set(orderType, []);
}
map.get(orderType)!.push(item);
return map;
}, new Map<OrderType | string, DisplayOrderItem[]>());
}

View File

@@ -12,6 +12,8 @@ export * from './group-by-branch.helper';
export * from './group-by-order-type.helper';
export * from './group-display-orders-by-branch.helper';
export * from './group-display-orders-by-delivery-type.helper';
export * from './group-display-order-items-by-branch.helper';
export * from './group-display-order-items-by-delivery-type.helper';
export * from './item-selection-changed.helper';
export * from './merge-reward-selection-items.helper';
export * from './should-show-grouping.helper';

View File

@@ -1,14 +1,39 @@
<div
class="flex p-2 items-center gap-[0.625rem] self-stretch rounded-2xl bg-isa-neutral-200 w-full isa-text-body-2-bold"
>
<ng-icon [name]="orderTypeIcon()" size="1.5rem"></ng-icon>
<div>
{{ orderType() }}
</div>
</div>
@for (group of groupedOrders(); track group.orderType) {
<!-- Delivery type section -->
<div
class="flex flex-col gap-2 self-stretch"
data-what="delivery-type-group"
[attr.data-which]="group.orderType"
>
@for (branchGroup of group.branchGroups; track branchGroup.branchId ?? 0) {
<!-- Branch header (only shown when multiple branches exist within same delivery type) -->
@if (branchGroup.branchName && group.branchGroups.length > 1) {
<div
class="flex p-2 items-center gap-[0.625rem] self-stretch rounded-2xl bg-isa-neutral-200 w-full isa-text-body-2-bold"
data-what="branch-header"
[attr.data-which]="branchGroup.branchName"
>
<ng-icon [name]="group.icon" size="1.5rem" />
<div>{{ group.orderType }} - {{ branchGroup.branchName }}</div>
</div>
} @else {
<!-- Order type header (for single branch or Versand types) -->
<div
class="flex p-2 items-center gap-[0.625rem] self-stretch rounded-2xl bg-isa-neutral-200 w-full isa-text-body-2-bold"
data-what="order-type-header"
[attr.data-which]="group.orderType"
>
<ng-icon [name]="group.icon" size="1.5rem" />
<div>
{{ group.orderType }}
</div>
</div>
}
@for (item of items(); track item.id) {
<checkout-order-confirmation-item-list-item
[item]="item"
></checkout-order-confirmation-item-list-item>
<!-- Items from all orders in this (orderType + branch) group -->
@for (item of branchGroup.items; track item.id) {
<checkout-order-confirmation-item-list-item [item]="item" />
}
}
</div>
}

View File

@@ -2,19 +2,24 @@ import {
ChangeDetectionStrategy,
Component,
computed,
input,
inject,
} from '@angular/core';
import { OrderConfirmationItemListItemComponent } from './order-confirmation-item-list-item/order-confirmation-item-list-item.component';
import { getOrderTypeFeature, OrderType } from '@isa/checkout/data-access';
import {
groupDisplayOrderItemsByDeliveryType,
groupDisplayOrderItemsByBranch,
getOrderTypeIcon,
} from '@isa/checkout/data-access';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
isaDeliveryVersand,
isaDeliveryRuecklage2,
isaDeliveryRuecklage1,
isaDeliveryB2BVersand1,
} from '@isa/icons';
import { DisplayOrder } from '@isa/oms/data-access';
import { OrderConfiramtionStore } from '../reward-order-confirmation.store';
@Component({
selector: 'checkout-order-confirmation-item-list',
@@ -27,33 +32,40 @@ import { DisplayOrder } from '@isa/oms/data-access';
isaDeliveryVersand,
isaDeliveryRuecklage2,
isaDeliveryRuecklage1,
isaDeliveryB2BVersand1,
}),
],
})
export class OrderConfirmationItemListComponent {
order = input.required<DisplayOrder>();
#store = inject(OrderConfiramtionStore);
orderType = computed(() => {
return getOrderTypeFeature(this.order().features);
orders = this.#store.orders;
/**
* Groups order items hierarchically by delivery type and branch.
* - Primary grouping: By delivery type from item.features.orderType (Abholung, Rücklage, Versand, etc.)
* - Secondary grouping: By branch (only for Abholung and Rücklage)
*
* Note: Groups by item-level orderType, not order-level, since items within
* a single order can have different delivery types.
*/
groupedOrders = computed(() => {
const orders = this.orders();
if (!orders || orders.length === 0) {
return [];
}
const byDeliveryType = groupDisplayOrderItemsByDeliveryType(orders);
const result = Array.from(byDeliveryType.entries()).map(
([orderType, items]) => ({
orderType,
icon: getOrderTypeIcon(orderType),
branchGroups: groupDisplayOrderItemsByBranch(orderType, items),
}),
);
console.log('Grouped orders:', result);
return result;
});
orderTypeIcon = computed(() => {
const orderType = this.orderType();
if (OrderType.Delivery === orderType) {
return 'isaDeliveryVersand';
}
if (OrderType.Pickup === orderType) {
return 'isaDeliveryRuecklage2';
}
if (OrderType.InStore === orderType) {
return 'isaDeliveryRuecklage1';
}
return 'isaDeliveryVersand';
});
items = computed(() => this.order().items ?? []);
}

View File

@@ -1,47 +1,8 @@
<div
class="bg-isa-white p-6 rounded-2xl flex flex-col gap-6 items-start self-stretch"
>
<checkout-order-confirmation-header></checkout-order-confirmation-header>
<checkout-order-confirmation-header />
<div class="w-full h-[0.0625rem] bg-[#DEE2E6]"></div>
<checkout-order-confirmation-addresses></checkout-order-confirmation-addresses>
@for (group of groupedOrders(); track group.orderType) {
<!-- Delivery type section -->
<div class="flex flex-col gap-2 self-stretch" data-what="delivery-type-group" [attr.data-which]="group.orderType">
@for (branchGroup of group.branchGroups; track branchGroup.branchId ?? 0) {
<!-- Branch header (only shown when multiple branches exist within same delivery type) -->
@if (branchGroup.branchName && group.branchGroups.length > 1) {
<div
class="flex p-2 items-center gap-[0.625rem] self-stretch rounded-2xl bg-isa-neutral-200 w-full isa-text-body-2-bold"
data-what="branch-header"
[attr.data-which]="branchGroup.branchName"
>
<ng-icon [name]="group.icon" size="1.5rem"></ng-icon>
<div>
{{ group.orderType }} - {{ branchGroup.branchName }}
</div>
</div>
} @else {
<!-- Order type header (for single branch or Versand types) -->
<div
class="flex p-2 items-center gap-[0.625rem] self-stretch rounded-2xl bg-isa-neutral-200 w-full isa-text-body-2-bold"
data-what="order-type-header"
[attr.data-which]="group.orderType"
>
<ng-icon [name]="group.icon" size="1.5rem"></ng-icon>
<div>
{{ group.orderType }}
</div>
</div>
}
<!-- Items from all orders in this (orderType + branch) group -->
@for (item of branchGroup.allItems; track item.id) {
<checkout-order-confirmation-item-list-item
[item]="item"
></checkout-order-confirmation-item-list-item>
}
}
</div>
}
<checkout-order-confirmation-addresses />
<checkout-order-confirmation-item-list />
</div>

View File

@@ -11,22 +11,9 @@ import { toSignal } from '@angular/core/rxjs-interop';
import { OrderConfirmationAddressesComponent } from './order-confirmation-addresses/order-confirmation-addresses.component';
import { OrderConfirmationHeaderComponent } from './order-confirmation-header/order-confirmation-header.component';
import { OrderConfirmationItemListComponent } from './order-confirmation-item-list/order-confirmation-item-list.component';
import { OrderConfirmationItemListItemComponent } from './order-confirmation-item-list/order-confirmation-item-list-item/order-confirmation-item-list-item.component';
import { ActivatedRoute } from '@angular/router';
import { TabService } from '@isa/core/tabs';
import { OrderConfiramtionStore } from './reward-order-confirmation.store';
import {
groupDisplayOrdersByDeliveryType,
groupDisplayOrdersByBranch,
getOrderTypeIcon,
} from '@isa/checkout/data-access';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
isaDeliveryVersand,
isaDeliveryRuecklage2,
isaDeliveryRuecklage1,
isaDeliveryB2BVersand1,
} from '@isa/icons';
@Component({
selector: 'checkout-reward-order-confirmation',
@@ -37,18 +24,8 @@ import {
OrderConfirmationHeaderComponent,
OrderConfirmationAddressesComponent,
OrderConfirmationItemListComponent,
OrderConfirmationItemListItemComponent,
NgIcon,
],
providers: [
OrderConfiramtionStore,
provideIcons({
isaDeliveryVersand,
isaDeliveryRuecklage2,
isaDeliveryRuecklage1,
isaDeliveryB2BVersand1,
}),
],
providers: [OrderConfiramtionStore],
})
export class RewardOrderConfirmationComponent {
#store = inject(OrderConfiramtionStore);
@@ -66,28 +43,6 @@ export class RewardOrderConfirmationComponent {
return param ? param.split('+').map((strId) => parseInt(strId, 10)) : [];
});
orders = this.#store.orders;
/**
* Groups orders hierarchically by delivery type and branch.
* - Primary grouping: By delivery type (Abholung, Rücklage, Versand, etc.)
* - Secondary grouping: By branch (only for Abholung and Rücklage)
*/
groupedOrders = computed(() => {
const orders = this.orders();
if (!orders || orders.length === 0) {
return [];
}
const byDeliveryType = groupDisplayOrdersByDeliveryType(orders);
return Array.from(byDeliveryType.entries()).map(([orderType, typeOrders]) => ({
orderType,
icon: getOrderTypeIcon(orderType),
branchGroups: groupDisplayOrdersByBranch(orderType, typeOrders),
}));
});
constructor() {
effect(() => {
const tabId = this.#tabId() || undefined;
@@ -97,9 +52,5 @@ export class RewardOrderConfirmationComponent {
this.#store.patch({ tabId, orderIds });
});
});
effect(() => {
console.log('Orders to display:', this.orders());
});
}
}