FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

177 lines
5.7 KiB

  1. import type { ItemLotTraceStockTakeEvent, ItemLotTraceStockTakeRecordDetail } from "@/app/api/itemTracing";
  2. export const formatStockTakeRoundLabel = (event: ItemLotTraceStockTakeEvent): string => {
  3. const name = event.stockTakeRoundName?.trim();
  4. if (name) return name;
  5. if (event.stockTakeRoundId != null) return `#${event.stockTakeRoundId}`;
  6. return "";
  7. };
  8. export const stockTakeEventSubtitle = (event: ItemLotTraceStockTakeEvent): string => {
  9. const parts = [
  10. formatStockTakeRoundLabel(event),
  11. event.stockTakeSection?.trim(),
  12. event.warehouseCode?.trim(),
  13. event.lotNo?.trim() || undefined,
  14. ].filter(Boolean);
  15. return parts.length > 0 ? parts.join(" · ") : event.stockTakeCode || "—";
  16. };
  17. /** Book (system) qty at count time — prefer record snapshot over line initialQty. */
  18. export const resolveStockTakeBookQty = (
  19. detail: ItemLotTraceStockTakeRecordDetail | null | undefined,
  20. fallbackBeforeQty?: number | null,
  21. ): number | null => {
  22. if (detail?.bookQty != null && !Number.isNaN(Number(detail.bookQty))) {
  23. return Number(detail.bookQty);
  24. }
  25. if (fallbackBeforeQty != null && !Number.isNaN(Number(fallbackBeforeQty))) {
  26. return Number(fallbackBeforeQty);
  27. }
  28. return null;
  29. };
  30. /**
  31. * FP-MTMS Version Checklist | Functions Ref. No. 23 | v1.0.0 | 2026-07-20
  32. * Accepted physical qty used for variance / posting decision.
  33. * Prefer lastSelect (1=first, 2=second, 3=approver), then fallback chain, then line finalQty.
  34. * Incomplete rounds (no count yet) must not surface as 0 via COALESCE(finalQty, 0).
  35. */
  36. export const resolveStockTakeAcceptedQty = (
  37. detail: ItemLotTraceStockTakeRecordDetail | null | undefined,
  38. fallbackAfterQty?: number | null,
  39. ): number | null => {
  40. const pick = (n: number | null | undefined) =>
  41. n != null && !Number.isNaN(Number(n)) ? Number(n) : null;
  42. if (detail) {
  43. switch (detail.lastSelect) {
  44. case 3: {
  45. const q = pick(detail.approverQty);
  46. if (q != null) return q;
  47. break;
  48. }
  49. case 2: {
  50. const q = pick(detail.pickerSecondQty);
  51. if (q != null) return q;
  52. break;
  53. }
  54. case 1: {
  55. const q = pick(detail.pickerFirstQty);
  56. if (q != null) return q;
  57. break;
  58. }
  59. default:
  60. break;
  61. }
  62. const fromApprover = pick(detail.approverQty);
  63. if (fromApprover != null) return fromApprover;
  64. const fromSecond = pick(detail.pickerSecondQty);
  65. if (fromSecond != null) return fromSecond;
  66. const fromFirst = pick(detail.pickerFirstQty);
  67. if (fromFirst != null) return fromFirst;
  68. const status = (detail.recordStatus ?? "").trim().toUpperCase();
  69. const accepted =
  70. status === "ACCEPTED" || status === "COMPLETED" || status === "COMPLETE";
  71. if (!accepted) {
  72. return null;
  73. }
  74. }
  75. if (fallbackAfterQty != null && !Number.isNaN(Number(fallbackAfterQty))) {
  76. return Number(fallbackAfterQty);
  77. }
  78. return null;
  79. };
  80. export interface StockTakeLifecycleStage {
  81. key: string;
  82. label: string;
  83. qty?: number | null;
  84. badQty?: number | null;
  85. handler?: string | null;
  86. timestamp?: string | null;
  87. isActive: boolean;
  88. }
  89. /** Build ordered lifecycle stages from stocktake record detail. */
  90. export const buildStockTakeLifecycleStages = (
  91. detail: ItemLotTraceStockTakeRecordDetail | null | undefined,
  92. ): StockTakeLifecycleStage[] => {
  93. if (!detail) return [];
  94. const stages: StockTakeLifecycleStage[] = [];
  95. // Stage 1: Round created — show book (帳面) qty for context
  96. stages.push({
  97. key: "round-created",
  98. label: "stockTakeStageCreated",
  99. qty: detail.bookQty,
  100. handler: null,
  101. timestamp: detail.stockTakeStartTime ?? null,
  102. isActive: true,
  103. });
  104. // Stage 2: Picker first count
  105. const hasFirstCount =
  106. detail.pickerFirstQty != null ||
  107. detail.pickerFirstBadQty != null ||
  108. Boolean(detail.stockTakerName?.trim());
  109. stages.push({
  110. key: "first-count",
  111. label: "stockTakeStageFirstCount",
  112. qty: detail.pickerFirstQty,
  113. badQty: detail.pickerFirstBadQty,
  114. handler: detail.stockTakerName,
  115. timestamp: detail.stockTakeStartTime ?? null,
  116. isActive: hasFirstCount,
  117. });
  118. // Stage 3: Picker second count (re-count) — only if present
  119. const hasSecondCount =
  120. detail.pickerSecondQty != null || detail.pickerSecondBadQty != null;
  121. if (hasSecondCount) {
  122. stages.push({
  123. key: "second-count",
  124. label: "stockTakeStageSecondCount",
  125. qty: detail.pickerSecondQty,
  126. badQty: detail.pickerSecondBadQty,
  127. handler: detail.stockTakerName,
  128. timestamp: detail.stockTakeEndTime ?? null,
  129. isActive: true,
  130. });
  131. }
  132. // Stage 4: Approver manual input — only when lastSelect === 3 (admin entered own count)
  133. const hasApproverCount =
  134. detail.approverQty != null || detail.approverBadQty != null;
  135. if (detail.lastSelect === 3 && hasApproverCount) {
  136. stages.push({
  137. key: "approver-count",
  138. label: "stockTakeStageApproverCount",
  139. qty: detail.approverQty,
  140. badQty: detail.approverBadQty,
  141. handler: detail.approverName,
  142. timestamp: detail.approverTime ?? null,
  143. isActive: true,
  144. });
  145. }
  146. // Stage 5: Accepted — show accepted physical qty; variance via badQty for secondary line
  147. const isAccepted =
  148. detail.recordStatus?.toUpperCase() === "ACCEPTED" ||
  149. detail.recordStatus?.toUpperCase() === "COMPLETED" ||
  150. detail.recordStatus?.toUpperCase() === "COMPLETE";
  151. const acceptedQty = resolveStockTakeAcceptedQty(detail, null);
  152. stages.push({
  153. key: "accepted",
  154. label: isAccepted ? "stockTakeStageAccepted" : "stockTakeStagePending",
  155. qty: acceptedQty,
  156. badQty: detail.varianceQty,
  157. handler: detail.approverName,
  158. timestamp: detail.approverTime ?? detail.stockTakeEndTime ?? null,
  159. isActive: isAccepted || Boolean(detail.approverName?.trim()),
  160. });
  161. return stages;
  162. };