import type { ItemLotTraceStockTakeEvent, ItemLotTraceStockTakeRecordDetail } from "@/app/api/itemTracing"; export const formatStockTakeRoundLabel = (event: ItemLotTraceStockTakeEvent): string => { const name = event.stockTakeRoundName?.trim(); if (name) return name; if (event.stockTakeRoundId != null) return `#${event.stockTakeRoundId}`; return ""; }; export const stockTakeEventSubtitle = (event: ItemLotTraceStockTakeEvent): string => { const parts = [ formatStockTakeRoundLabel(event), event.stockTakeSection?.trim(), event.warehouseCode?.trim(), event.lotNo?.trim() || undefined, ].filter(Boolean); return parts.length > 0 ? parts.join(" · ") : event.stockTakeCode || "—"; }; /** Book (system) qty at count time — prefer record snapshot over line initialQty. */ export const resolveStockTakeBookQty = ( detail: ItemLotTraceStockTakeRecordDetail | null | undefined, fallbackBeforeQty?: number | null, ): number | null => { if (detail?.bookQty != null && !Number.isNaN(Number(detail.bookQty))) { return Number(detail.bookQty); } if (fallbackBeforeQty != null && !Number.isNaN(Number(fallbackBeforeQty))) { return Number(fallbackBeforeQty); } return null; }; /** * Accepted physical qty used for variance / posting decision. * Prefer lastSelect (1=first, 2=second, 3=approver), then fallback chain, then line finalQty. */ export const resolveStockTakeAcceptedQty = ( detail: ItemLotTraceStockTakeRecordDetail | null | undefined, fallbackAfterQty?: number | null, ): number | null => { if (detail) { const pick = (n: number | null | undefined) => n != null && !Number.isNaN(Number(n)) ? Number(n) : null; switch (detail.lastSelect) { case 3: { const q = pick(detail.approverQty); if (q != null) return q; break; } case 2: { const q = pick(detail.pickerSecondQty); if (q != null) return q; break; } case 1: { const q = pick(detail.pickerFirstQty); if (q != null) return q; break; } default: break; } const fromApprover = pick(detail.approverQty); if (fromApprover != null) return fromApprover; const fromSecond = pick(detail.pickerSecondQty); if (fromSecond != null) return fromSecond; const fromFirst = pick(detail.pickerFirstQty); if (fromFirst != null) return fromFirst; } if (fallbackAfterQty != null && !Number.isNaN(Number(fallbackAfterQty))) { return Number(fallbackAfterQty); } return null; }; export interface StockTakeLifecycleStage { key: string; label: string; qty?: number | null; badQty?: number | null; handler?: string | null; timestamp?: string | null; isActive: boolean; } /** Build ordered lifecycle stages from stocktake record detail. */ export const buildStockTakeLifecycleStages = ( detail: ItemLotTraceStockTakeRecordDetail | null | undefined, ): StockTakeLifecycleStage[] => { if (!detail) return []; const stages: StockTakeLifecycleStage[] = []; // Stage 1: Round created — show book (帳面) qty for context stages.push({ key: "round-created", label: "stockTakeStageCreated", qty: detail.bookQty, handler: null, timestamp: detail.stockTakeStartTime ?? null, isActive: true, }); // Stage 2: Picker first count const hasFirstCount = detail.pickerFirstQty != null || detail.pickerFirstBadQty != null || Boolean(detail.stockTakerName?.trim()); stages.push({ key: "first-count", label: "stockTakeStageFirstCount", qty: detail.pickerFirstQty, badQty: detail.pickerFirstBadQty, handler: detail.stockTakerName, timestamp: detail.stockTakeStartTime ?? null, isActive: hasFirstCount, }); // Stage 3: Picker second count (re-count) — only if present const hasSecondCount = detail.pickerSecondQty != null || detail.pickerSecondBadQty != null; if (hasSecondCount) { stages.push({ key: "second-count", label: "stockTakeStageSecondCount", qty: detail.pickerSecondQty, badQty: detail.pickerSecondBadQty, handler: detail.stockTakerName, timestamp: detail.stockTakeEndTime ?? null, isActive: true, }); } // Stage 4: Approver manual input — only when lastSelect === 3 (admin entered own count) const hasApproverCount = detail.approverQty != null || detail.approverBadQty != null; if (detail.lastSelect === 3 && hasApproverCount) { stages.push({ key: "approver-count", label: "stockTakeStageApproverCount", qty: detail.approverQty, badQty: detail.approverBadQty, handler: detail.approverName, timestamp: detail.approverTime ?? null, isActive: true, }); } // Stage 5: Accepted — show accepted physical qty; variance via badQty for secondary line const isAccepted = detail.recordStatus?.toUpperCase() === "ACCEPTED" || detail.recordStatus?.toUpperCase() === "COMPLETED" || detail.recordStatus?.toUpperCase() === "COMPLETE"; const acceptedQty = resolveStockTakeAcceptedQty(detail, null); stages.push({ key: "accepted", label: isAccepted ? "stockTakeStageAccepted" : "stockTakeStagePending", qty: acceptedQty, badQty: detail.varianceQty, handler: detail.approverName, timestamp: detail.approverTime ?? detail.stockTakeEndTime ?? null, isActive: isAccepted || Boolean(detail.approverName?.trim()), }); return stages; };