import dayjs from "dayjs"; export type WorkbenchPickLotLike = { status?: string; stockOutLineStatus?: string; lotAvailability?: string; lotStatus?: string; expiryDate?: string; noLot?: boolean; lotNo?: string; availableQty?: number; }; export type PickOrderT = (key: string, options?: Record) => string; function solStatusOf(lot: WorkbenchPickLotLike | null | undefined): string { return String(lot?.stockOutLineStatus || lot?.status || "").toLowerCase(); } /** lotAvailability === expired(後端標記) */ export function isLotAvailabilityExpired( lot: WorkbenchPickLotLike | null | undefined, ): boolean { return String(lot?.lotAvailability || "").toLowerCase() === "expired"; } /** inventory_lot_line.status = unavailable */ export function isInventoryLotLineUnavailable( lot: WorkbenchPickLotLike | null | undefined, ): boolean { if (!lot) return false; const solSt = solStatusOf(lot); if (solSt === "completed" || solSt === "partially_completed" || solSt === "partially_complete") { return false; } if (String(lot.lotAvailability || "").toLowerCase() === "status_unavailable") return true; return String(lot.lotStatus || "").toLowerCase() === "unavailable"; } /** 含 expiryDate 日期判斷 */ export function isWorkbenchSourceLotExpired( lot: WorkbenchPickLotLike | null | undefined, ): boolean { if (!lot) return false; if (isLotAvailabilityExpired(lot)) return true; if (String(lot.lotAvailability || "").toLowerCase() === "expired") return true; if (lot.expiryDate) { const d = dayjs(lot.expiryDate).startOf("day"); if (d.isValid() && d.isBefore(dayjs().startOf("day"))) return true; } return false; } /** 過期或不可用:單筆 Just Complete / 顯示數量與批量提交一致,固定 qty=0 */ export function isWorkbenchZeroCompleteLot( lot: WorkbenchPickLotLike | null | undefined, ): boolean { return isLotAvailabilityExpired(lot) || isInventoryLotLineUnavailable(lot); } /** Backend messages with dynamic ids — map prefix to i18n key (pickOrder namespace). */ const WORKBENCH_REJECT_PREFIX_I18N: Array<[RegExp, string]> = [ [/^No inventory lot lines for inventoryLotId=\d+/i, "No inventory lot lines for inventoryLotId"], [/^No inventory lot for stockInLineId=\d+/i, "No inventory lot for stockInLineId"], [/^This lot is not yet putaway\.?$/i, "This lot is not yet putaway"], ]; export function translateWorkbenchRejectMessage(raw: string, t: PickOrderT): string { const msg = raw.trim(); if (!msg) return msg; const expiredMatch = msg.match(/^Lot is expired \(expiry=([^)]+)\)\.?$/i); if (expiredMatch) { return t("Lot is expired (expiry={{expiry}})", { expiry: expiredMatch[1], }); } for (const [pattern, i18nKey] of WORKBENCH_REJECT_PREFIX_I18N) { if (pattern.test(msg)) return t(i18nKey); } return t(msg); } export function isExpiredWorkbenchReminderMessage(msg: string): boolean { const trimmed = msg.trim(); if (!trimmed) return false; if (/^lot is expired \(expiry=/i.test(trimmed)) return true; return /已過期/.test(trimmed) || /掃描批號已過期/.test(trimmed); } export type UnpickableScanAvailability = "expired" | "status_unavailable"; export function inferUnpickableScanAvailability( failMsg: string | null | undefined, ): UnpickableScanAvailability | null { const m = String(failMsg ?? "").trim().toLowerCase(); if (!m) return null; if ( m.includes("expired") || m.includes("过期") || m.includes("已過期") || /^lot is expired/.test(m) ) { return "expired"; } if ( m.includes("unavailable") || m.includes("not available") || m.includes("not yet putaway") || m.includes("no inventory lot lines") || m.includes("no inventory lot for stockinlineid") || m.includes("不可用") || m.includes("未上架") || m.includes("尚未上架") ) { return "status_unavailable"; } return null; } export function buildUnpickableScanRowPatch( scannedLot: WorkbenchPickLotLike | null | undefined, availability: UnpickableScanAvailability, ): Record { const patch: Record = { lotAvailability: availability }; if (availability === "status_unavailable") { patch.lotStatus = "unavailable"; } if (scannedLot && "lotNo" in scannedLot && scannedLot.lotNo) { patch.lotNo = scannedLot.lotNo; } if (scannedLot && "stockInLineId" in scannedLot && scannedLot.stockInLineId) { patch.stockInLineId = scannedLot.stockInLineId; } if (scannedLot?.expiryDate) patch.expiryDate = scannedLot.expiryDate; return patch; } export function getWorkbenchSourceLotStatusSummary(lot: WorkbenchPickLotLike | null | undefined): { severity: "success" | "warning" | "error"; text: string; } { if (!lot) { return { severity: "warning", text: "無法判斷此批號狀態" }; } if (isWorkbenchSourceLotExpired(lot)) { return { severity: "error", text: "此批號狀態:已過期" }; } const solSt = solStatusOf(lot); if (solSt === "rejected") { return { severity: "warning", text: "此出庫行:已拒絕,請改掃其他批號" }; } if (solSt === "completed" || solSt === "partially_completed" || solSt === "partially_complete") { return { severity: "warning", text: "此出庫行:已完成,無需再提貨" }; } const isNoLotRow = lot.noLot === true || !lot.lotNo || String(lot.lotNo || "").trim() === ""; if (isNoLotRow) { return { severity: "warning", text: "尚未綁定批號/無可用庫存列:請掃描週邊入庫或轉倉 QR", }; } const av = String(lot.lotAvailability || "").toLowerCase(); if (av === "insufficient_stock") { return { severity: "warning", text: "此批號狀態:已用畢(無剩餘庫存)" }; } const avail = Number(lot.availableQty); if (lot.lotNo && Number.isFinite(avail) && avail <= 0) { return { severity: "warning", text: "此批號狀態:已用畢(可用量為 0)" }; } if (isInventoryLotLineUnavailable(lot)) { return { severity: "warning", text: "此批號狀態:庫存不可用(未上架或行狀態不可用)", }; } return { severity: "success", text: "此批號狀態:可提貨" }; }