|
- 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);
- }
-
- /** 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<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: "此批號狀態:可提貨" };
- }
|