FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 

167 строки
5.3 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. * Accepted physical qty used for variance / posting decision.
  32. * Prefer lastSelect (1=first, 2=second, 3=approver), then fallback chain, then line finalQty.
  33. */
  34. export const resolveStockTakeAcceptedQty = (
  35. detail: ItemLotTraceStockTakeRecordDetail | null | undefined,
  36. fallbackAfterQty?: number | null,
  37. ): number | null => {
  38. if (detail) {
  39. const pick = (n: number | null | undefined) =>
  40. n != null && !Number.isNaN(Number(n)) ? Number(n) : null;
  41. switch (detail.lastSelect) {
  42. case 3: {
  43. const q = pick(detail.approverQty);
  44. if (q != null) return q;
  45. break;
  46. }
  47. case 2: {
  48. const q = pick(detail.pickerSecondQty);
  49. if (q != null) return q;
  50. break;
  51. }
  52. case 1: {
  53. const q = pick(detail.pickerFirstQty);
  54. if (q != null) return q;
  55. break;
  56. }
  57. default:
  58. break;
  59. }
  60. const fromApprover = pick(detail.approverQty);
  61. if (fromApprover != null) return fromApprover;
  62. const fromSecond = pick(detail.pickerSecondQty);
  63. if (fromSecond != null) return fromSecond;
  64. const fromFirst = pick(detail.pickerFirstQty);
  65. if (fromFirst != null) return fromFirst;
  66. }
  67. if (fallbackAfterQty != null && !Number.isNaN(Number(fallbackAfterQty))) {
  68. return Number(fallbackAfterQty);
  69. }
  70. return null;
  71. };
  72. export interface StockTakeLifecycleStage {
  73. key: string;
  74. label: string;
  75. qty?: number | null;
  76. badQty?: number | null;
  77. handler?: string | null;
  78. timestamp?: string | null;
  79. isActive: boolean;
  80. }
  81. /** Build ordered lifecycle stages from stocktake record detail. */
  82. export const buildStockTakeLifecycleStages = (
  83. detail: ItemLotTraceStockTakeRecordDetail | null | undefined,
  84. ): StockTakeLifecycleStage[] => {
  85. if (!detail) return [];
  86. const stages: StockTakeLifecycleStage[] = [];
  87. // Stage 1: Round created — show book (帳面) qty for context
  88. stages.push({
  89. key: "round-created",
  90. label: "stockTakeStageCreated",
  91. qty: detail.bookQty,
  92. handler: null,
  93. timestamp: detail.stockTakeStartTime ?? null,
  94. isActive: true,
  95. });
  96. // Stage 2: Picker first count
  97. const hasFirstCount =
  98. detail.pickerFirstQty != null ||
  99. detail.pickerFirstBadQty != null ||
  100. Boolean(detail.stockTakerName?.trim());
  101. stages.push({
  102. key: "first-count",
  103. label: "stockTakeStageFirstCount",
  104. qty: detail.pickerFirstQty,
  105. badQty: detail.pickerFirstBadQty,
  106. handler: detail.stockTakerName,
  107. timestamp: detail.stockTakeStartTime ?? null,
  108. isActive: hasFirstCount,
  109. });
  110. // Stage 3: Picker second count (re-count) — only if present
  111. const hasSecondCount =
  112. detail.pickerSecondQty != null || detail.pickerSecondBadQty != null;
  113. if (hasSecondCount) {
  114. stages.push({
  115. key: "second-count",
  116. label: "stockTakeStageSecondCount",
  117. qty: detail.pickerSecondQty,
  118. badQty: detail.pickerSecondBadQty,
  119. handler: detail.stockTakerName,
  120. timestamp: detail.stockTakeEndTime ?? null,
  121. isActive: true,
  122. });
  123. }
  124. // Stage 4: Approver manual input — only when lastSelect === 3 (admin entered own count)
  125. const hasApproverCount =
  126. detail.approverQty != null || detail.approverBadQty != null;
  127. if (detail.lastSelect === 3 && hasApproverCount) {
  128. stages.push({
  129. key: "approver-count",
  130. label: "stockTakeStageApproverCount",
  131. qty: detail.approverQty,
  132. badQty: detail.approverBadQty,
  133. handler: detail.approverName,
  134. timestamp: detail.approverTime ?? null,
  135. isActive: true,
  136. });
  137. }
  138. // Stage 5: Accepted — show accepted physical qty; variance via badQty for secondary line
  139. const isAccepted =
  140. detail.recordStatus?.toUpperCase() === "ACCEPTED" ||
  141. detail.recordStatus?.toUpperCase() === "COMPLETED" ||
  142. detail.recordStatus?.toUpperCase() === "COMPLETE";
  143. const acceptedQty = resolveStockTakeAcceptedQty(detail, null);
  144. stages.push({
  145. key: "accepted",
  146. label: isAccepted ? "stockTakeStageAccepted" : "stockTakeStagePending",
  147. qty: acceptedQty,
  148. badQty: detail.varianceQty,
  149. handler: detail.approverName,
  150. timestamp: detail.approverTime ?? detail.stockTakeEndTime ?? null,
  151. isActive: isAccepted || Boolean(detail.approverName?.trim()),
  152. });
  153. return stages;
  154. };