|
|
|
@@ -0,0 +1,167 @@ |
|
|
|
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, unknown>) => 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); |
|
|
|
} |
|
|
|
|
|
|
|
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], |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
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("不可用") || |
|
|
|
m.includes("未上架") |
|
|
|
) { |
|
|
|
return "status_unavailable"; |
|
|
|
} |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
export function buildUnpickableScanRowPatch( |
|
|
|
scannedLot: WorkbenchPickLotLike | null | undefined, |
|
|
|
availability: UnpickableScanAvailability, |
|
|
|
): Record<string, unknown> { |
|
|
|
const patch: Record<string, unknown> = { 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: "此批號狀態:可提貨" }; |
|
|
|
} |