FPSMS-frontend
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

182 linhas
6.1 KiB

  1. import dayjs from "dayjs";
  2. export type WorkbenchPickLotLike = {
  3. status?: string;
  4. stockOutLineStatus?: string;
  5. lotAvailability?: string;
  6. lotStatus?: string;
  7. expiryDate?: string;
  8. noLot?: boolean;
  9. lotNo?: string;
  10. availableQty?: number;
  11. };
  12. export type PickOrderT = (key: string, options?: Record<string, unknown>) => string;
  13. function solStatusOf(lot: WorkbenchPickLotLike | null | undefined): string {
  14. return String(lot?.stockOutLineStatus || lot?.status || "").toLowerCase();
  15. }
  16. /** lotAvailability === expired(後端標記) */
  17. export function isLotAvailabilityExpired(
  18. lot: WorkbenchPickLotLike | null | undefined,
  19. ): boolean {
  20. return String(lot?.lotAvailability || "").toLowerCase() === "expired";
  21. }
  22. /** inventory_lot_line.status = unavailable */
  23. export function isInventoryLotLineUnavailable(
  24. lot: WorkbenchPickLotLike | null | undefined,
  25. ): boolean {
  26. if (!lot) return false;
  27. const solSt = solStatusOf(lot);
  28. if (solSt === "completed" || solSt === "partially_completed" || solSt === "partially_complete") {
  29. return false;
  30. }
  31. if (String(lot.lotAvailability || "").toLowerCase() === "status_unavailable") return true;
  32. return String(lot.lotStatus || "").toLowerCase() === "unavailable";
  33. }
  34. /** 含 expiryDate 日期判斷 */
  35. export function isWorkbenchSourceLotExpired(
  36. lot: WorkbenchPickLotLike | null | undefined,
  37. ): boolean {
  38. if (!lot) return false;
  39. if (isLotAvailabilityExpired(lot)) return true;
  40. if (String(lot.lotAvailability || "").toLowerCase() === "expired") return true;
  41. if (lot.expiryDate) {
  42. const d = dayjs(lot.expiryDate).startOf("day");
  43. if (d.isValid() && d.isBefore(dayjs().startOf("day"))) return true;
  44. }
  45. return false;
  46. }
  47. /** 過期或不可用:單筆 Just Complete / 顯示數量與批量提交一致,固定 qty=0 */
  48. export function isWorkbenchZeroCompleteLot(
  49. lot: WorkbenchPickLotLike | null | undefined,
  50. ): boolean {
  51. return isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot);
  52. }
  53. /** Backend messages with dynamic ids — map prefix to i18n key (pickOrder namespace). */
  54. const WORKBENCH_REJECT_PREFIX_I18N: Array<[RegExp, string]> = [
  55. [/^No inventory lot lines for inventoryLotId=\d+/i, "No inventory lot lines for inventoryLotId"],
  56. [/^No inventory lot for stockInLineId=\d+/i, "No inventory lot for stockInLineId"],
  57. [/^This lot is not yet putaway\.?$/i, "This lot is not yet putaway"],
  58. ];
  59. export function translateWorkbenchRejectMessage(raw: string, t: PickOrderT): string {
  60. const msg = raw.trim();
  61. if (!msg) return msg;
  62. const expiredMatch = msg.match(/^Lot is expired \(expiry=([^)]+)\)\.?$/i);
  63. if (expiredMatch) {
  64. return t("Lot is expired (expiry={{expiry}})", {
  65. expiry: expiredMatch[1],
  66. });
  67. }
  68. for (const [pattern, i18nKey] of WORKBENCH_REJECT_PREFIX_I18N) {
  69. if (pattern.test(msg)) return t(i18nKey);
  70. }
  71. return t(msg);
  72. }
  73. export function isExpiredWorkbenchReminderMessage(msg: string): boolean {
  74. const trimmed = msg.trim();
  75. if (!trimmed) return false;
  76. if (/^lot is expired \(expiry=/i.test(trimmed)) return true;
  77. return /已過期/.test(trimmed) || /掃描批號已過期/.test(trimmed);
  78. }
  79. export type UnpickableScanAvailability = "expired" | "status_unavailable";
  80. export function inferUnpickableScanAvailability(
  81. failMsg: string | null | undefined,
  82. ): UnpickableScanAvailability | null {
  83. const m = String(failMsg ?? "").trim().toLowerCase();
  84. if (!m) return null;
  85. if (
  86. m.includes("expired") ||
  87. m.includes("过期") ||
  88. m.includes("已過期") ||
  89. /^lot is expired/.test(m)
  90. ) {
  91. return "expired";
  92. }
  93. if (
  94. m.includes("unavailable") ||
  95. m.includes("not available") ||
  96. m.includes("not yet putaway") ||
  97. m.includes("no inventory lot lines") ||
  98. m.includes("no inventory lot for stockinlineid") ||
  99. m.includes("不可用") ||
  100. m.includes("未上架") ||
  101. m.includes("尚未上架")
  102. ) {
  103. return "status_unavailable";
  104. }
  105. return null;
  106. }
  107. export function buildUnpickableScanRowPatch(
  108. scannedLot: WorkbenchPickLotLike | null | undefined,
  109. availability: UnpickableScanAvailability,
  110. ): Record<string, unknown> {
  111. const patch: Record<string, unknown> = { lotAvailability: availability };
  112. if (availability === "status_unavailable") {
  113. patch.lotStatus = "unavailable";
  114. }
  115. if (scannedLot && "lotNo" in scannedLot && scannedLot.lotNo) {
  116. patch.lotNo = scannedLot.lotNo;
  117. }
  118. if (scannedLot && "stockInLineId" in scannedLot && scannedLot.stockInLineId) {
  119. patch.stockInLineId = scannedLot.stockInLineId;
  120. }
  121. if (scannedLot?.expiryDate) patch.expiryDate = scannedLot.expiryDate;
  122. return patch;
  123. }
  124. export function getWorkbenchSourceLotStatusSummary(lot: WorkbenchPickLotLike | null | undefined): {
  125. severity: "success" | "warning" | "error";
  126. text: string;
  127. } {
  128. if (!lot) {
  129. return { severity: "warning", text: "無法判斷此批號狀態" };
  130. }
  131. if (isWorkbenchSourceLotExpired(lot)) {
  132. return { severity: "error", text: "此批號狀態:已過期" };
  133. }
  134. const solSt = solStatusOf(lot);
  135. if (solSt === "rejected") {
  136. return { severity: "warning", text: "此出庫行:已拒絕,請改掃其他批號" };
  137. }
  138. if (solSt === "completed" || solSt === "partially_completed" || solSt === "partially_complete") {
  139. return { severity: "warning", text: "此出庫行:已完成,無需再提貨" };
  140. }
  141. const isNoLotRow =
  142. lot.noLot === true || !lot.lotNo || String(lot.lotNo || "").trim() === "";
  143. if (isNoLotRow) {
  144. return {
  145. severity: "warning",
  146. text: "尚未綁定批號/無可用庫存列:請掃描週邊入庫或轉倉 QR",
  147. };
  148. }
  149. const av = String(lot.lotAvailability || "").toLowerCase();
  150. if (av === "insufficient_stock") {
  151. return { severity: "warning", text: "此批號狀態:已用畢(無剩餘庫存)" };
  152. }
  153. const avail = Number(lot.availableQty);
  154. if (lot.lotNo && Number.isFinite(avail) && avail <= 0) {
  155. return { severity: "warning", text: "此批號狀態:已用畢(可用量為 0)" };
  156. }
  157. if (isInventoryLotLineUnavailable(lot)) {
  158. return {
  159. severity: "warning",
  160. text: "此批號狀態:庫存不可用(未上架或行狀態不可用)",
  161. };
  162. }
  163. return { severity: "success", text: "此批號狀態:可提貨" };
  164. }