Merged PR 2057: feat(checkout): add branch selection to reward catalog

feat(checkout): add branch selection to reward catalog

- Add new select-branch-dropdown library with BranchDropdownComponent
  and SelectedBranchDropdownComponent for branch selection
- Extend DropdownButtonComponent with filter and option subcomponents
- Integrate branch selection into reward catalog page
- Add BranchesResource for fetching available branches
- Update CheckoutMetadataService with branch selection persistence
- Add comprehensive tests for dropdown components

Related work items: #5464
This commit is contained in:
Lorenz Hilpert
2025-11-27 10:38:52 +00:00
committed by Nino Righi
parent 4589146e31
commit 7950994d66
44 changed files with 3210 additions and 1580 deletions

View File

@@ -4,6 +4,11 @@ import { RemissionStockService } from '../services';
import { FetchStockInStock } from '../schemas';
import { StockInfo } from '../models';
export type StockInfoResourceParams = { itemId: number } & (
| { stockId?: number }
| { branchId: number }
);
/**
* Smart batching resource for stock information.
* Collects item params from multiple components, waits for a batching window,
@@ -19,11 +24,12 @@ import { StockInfo } from '../models';
*/
@Injectable({ providedIn: 'root' })
export class StockInfoResource extends BatchingResource<
{ itemId: number; stockId?: number },
StockInfoResourceParams,
FetchStockInStock,
StockInfo
> {
#stockService = inject(RemissionStockService);
#currentBatchBranchId?: number;
constructor() {
super(250); // batchWindowMs
@@ -43,27 +49,51 @@ export class StockInfoResource extends BatchingResource<
* Build API request params from list of item params.
*/
protected buildParams(
paramsList: { itemId: number; stockId?: number }[],
paramsList: { itemId: number; stockId?: number; branchId?: number }[],
): FetchStockInStock {
const first = paramsList[0];
// Track branchId for result matching (StockInfo doesn't contain branchId)
this.#currentBatchBranchId = first?.branchId;
if (first?.branchId !== undefined) {
return {
itemIds: paramsList.map((p) => p.itemId),
branchId: first.branchId,
};
}
return {
itemIds: paramsList.map((p) => p.itemId),
stockId: paramsList[0]?.stockId,
stockId: first?.stockId,
};
}
/**
* Extract params from result for cache matching.
* Uses tracked branchId since StockInfo doesn't contain it.
*/
protected getKeyFromResult(
stock: StockInfo,
): { itemId: number; stockId?: number } | undefined {
return stock.itemId !== undefined ? { itemId: stock.itemId } : undefined;
): { itemId: number; stockId?: number; branchId?: number } | undefined {
if (stock.itemId === undefined) {
return undefined;
}
return {
itemId: stock.itemId,
branchId: this.#currentBatchBranchId,
};
}
/**
* Generate cache key from params.
*/
protected getCacheKey(params: { itemId: number; stockId?: number }): string {
protected getCacheKey(params: {
itemId: number;
stockId?: number;
branchId?: number;
}): string {
if (params.branchId !== undefined) {
return `branch-${params.branchId}-${params.itemId}`;
}
return `${params.stockId ?? 'default'}-${params.itemId}`;
}
@@ -76,7 +106,8 @@ export class StockInfoResource extends BatchingResource<
): { itemId: number; stockId?: number }[] {
return params.itemIds.map((itemId) => ({
itemId,
stockId: params.stockId,
stockId: 'stockId' in params ? params.stockId : undefined,
branchId: 'branchId' in params ? params.branchId : undefined,
}));
}

View File

@@ -1,8 +1,22 @@
import { z } from 'zod';
export const FetchStockInStockSchema = z.object({
export const FetchStockInStockWithStockIdSchema = z.object({
stockId: z.number().describe('Stock identifier').optional(),
itemIds: z.array(z.number()).describe('Item ids'),
});
export const FetchStockInStockWithBranchIdSchema = z.object({
branchId: z.number().describe('Branch identifier'),
});
export const FetchStockInStockSchema = z
.object({
itemIds: z.array(z.number()).describe('Item ids'),
})
.and(
z.union([
FetchStockInStockWithBranchIdSchema,
FetchStockInStockWithStockIdSchema,
]),
);
export type FetchStockInStock = z.infer<typeof FetchStockInStockSchema>;

View File

@@ -157,8 +157,17 @@ export class RemissionStockService {
let assignedStockId: number;
if (parsed.stockId) {
if ('stockId' in parsed && parsed.stockId) {
assignedStockId = parsed.stockId;
} else if ('branchId' in parsed) {
const stock = await this.fetchStock(parsed.branchId, abortSignal);
if (!stock) {
this.#logger.warn('No stock found for branch', () => ({
branchId: parsed.branchId,
}));
return [];
}
assignedStockId = stock.id;
} else {
assignedStockId = await this.fetchAssignedStock(abortSignal).then(
(s) => s.id,
@@ -166,7 +175,7 @@ export class RemissionStockService {
}
this.#logger.info('Fetching stock info from API', () => ({
stockId: parsed.stockId,
stockId: assignedStockId,
itemCount: parsed.itemIds.length,
}));