diff --git a/src/app/(main)/testing/page.tsx b/src/app/(main)/testing/page.tsx index 1062628..14f4855 100644 --- a/src/app/(main)/testing/page.tsx +++ b/src/app/(main)/testing/page.tsx @@ -20,6 +20,7 @@ import { } from "@mui/material"; import { FileDownload } from "@mui/icons-material"; import dayjs from "dayjs"; +import { formatHongKongDateTime } from "@/utils/formatHongKongDateTime"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; import LotLabelPrintModal from "@/components/InventorySearch/LotLabelPrintModal"; @@ -502,7 +503,7 @@ export default function TestingPage() { display="block" sx={{ mt: 0.5 }} > - {laserLastReceive.sentAt ?? ""} {laserLastReceive.source ?? ""} + {formatHongKongDateTime(laserLastReceive.sentAt)} {laserLastReceive.source ?? ""} ) : null} diff --git a/src/app/api/laserPrint/actions.ts b/src/app/api/laserPrint/actions.ts index 952892c..f5e6c72 100644 --- a/src/app/api/laserPrint/actions.ts +++ b/src/app/api/laserPrint/actions.ts @@ -38,6 +38,20 @@ export interface LaserBag2Settings { lastReceiveSuccess?: LaserLastReceiveSuccess | null; } +/** Live TCP queries GetMarkData / GetMarkStatus / GetMarkedCount (same host/port as laser send). */ +export interface LaserBag2MarkInfo { + host: string; + port: number; + markData?: string | null; + /** 0 idle, 1 marking, 2 other — when the device returns a parseable digit */ + markStatus?: number | null; + markStatusLabel?: string | null; + markedCount?: number | null; + rawMarkStatus?: string | null; + rawMarkedCount?: string | null; + error?: string | null; +} + export interface LaserBag2SendRequest { itemId: number | null; stockInLineId: number | null; @@ -109,6 +123,20 @@ export async function fetchLaserBag2Settings(): Promise { return res.json() as Promise; } +export async function fetchLaserBag2MarkInfo(): Promise { + const base = (NEXT_PUBLIC_API_URL ?? "").replace(/\/$/, ""); + if (!base) { + throw new Error("NEXT_PUBLIC_API_URL is not set."); + } + const url = `${base}/plastic/laser-bag2-mark-info`; + const res = await clientAuthFetch(url, { method: "GET" }); + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new Error(`讀取打標狀態失敗(${res.status})${body ? `:${body.slice(0, 200)}` : ""}`); + } + return res.json() as Promise; +} + export async function sendLaserBag2Job(body: LaserBag2SendRequest): Promise { const url = `${NEXT_PUBLIC_API_URL}/plastic/print-laser-bag2`; const res = await clientAuthFetch(url, { diff --git a/src/components/LaserPrint/LaserPrintSearch.tsx b/src/components/LaserPrint/LaserPrintSearch.tsx index 5511cba..029aad0 100644 --- a/src/components/LaserPrint/LaserPrintSearch.tsx +++ b/src/components/LaserPrint/LaserPrintSearch.tsx @@ -21,14 +21,17 @@ import ChevronRight from "@mui/icons-material/ChevronRight"; import Settings from "@mui/icons-material/Settings"; import { checkPrinterStatus, + fetchLaserBag2MarkInfo, fetchLaserJobOrders, fetchLaserBag2Settings, + type LaserBag2MarkInfo, type LaserLastReceiveSuccess, JobOrderListItem, patchSetting, sendLaserBag2Job, } from "@/app/api/laserPrint/actions"; import dayjs from "dayjs"; +import { formatHongKongDateTime } from "@/utils/formatHongKongDateTime"; const BG_TOP = "#E8F4FC"; const BG_LIST = "#D4E8F7"; @@ -45,6 +48,8 @@ const PRINTER_RETRY_MS = 30 * 1000; const LASER_SEND_COUNT = 3; const BETWEEN_SEND_MS = 3000; const SUCCESS_SIGNAL_MS = 3500; +/** Poll laser TCP GetMarkData / GetMarkStatus / GetMarkedCount */ +const MARK_INFO_MS = 5000; function formatQty(val: number | null | undefined): string { if (val == null) return "—"; @@ -63,6 +68,11 @@ function getBatch(jo: JobOrderListItem): string { return (jo.lotNo || "—").trim() || "—"; } +function fmtMarkText(s: string | null | undefined): string { + if (s == null || s === "") return "—"; + return s; +} + function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -88,6 +98,8 @@ const LaserPrintSearch: React.FC = () => { const [settingsLoaded, setSettingsLoaded] = useState(false); const [printerConnected, setPrinterConnected] = useState(false); const [printerMessage, setPrinterMessage] = useState("檸檬機(激光機)未連接"); + const [markInfo, setMarkInfo] = useState(null); + const [markInfoError, setMarkInfoError] = useState(null); const loadSystemSettings = useCallback(async () => { try { @@ -173,6 +185,23 @@ const LaserPrintSearch: React.FC = () => { return () => clearInterval(id); }, [printerConnected, checkLaser, settingsLoaded]); + const loadMarkInfo = useCallback(async () => { + try { + const m = await fetchLaserBag2MarkInfo(); + setMarkInfo(m); + setMarkInfoError(null); + } catch (e) { + setMarkInfoError(e instanceof Error ? e.message : "無法讀取打標狀態"); + } + }, []); + + useEffect(() => { + if (!settingsLoaded) return; + void loadMarkInfo(); + const id = setInterval(() => void loadMarkInfo(), MARK_INFO_MS); + return () => clearInterval(id); + }, [settingsLoaded, loadMarkInfo]); + const goPrevDay = () => { setPlanDate((d) => dayjs(d).subtract(1, "day").format("YYYY-MM-DD")); }; @@ -229,6 +258,7 @@ const LaserPrintSearch: React.FC = () => { : ""; setSuccessSignal(`已送出 ${LASER_SEND_COUNT} 次至檸檬機(激光機)${ackHint}`); await loadSystemSettings(); + void loadMarkInfo(); } catch (e) { setErrorSnackbar({ open: true, @@ -249,6 +279,7 @@ const LaserPrintSearch: React.FC = () => { void checkLaser(); await loadSystemSettings(); void loadJobOrders(false); + void loadMarkInfo(); } catch (e) { setErrorSnackbar({ open: true, @@ -284,12 +315,65 @@ const LaserPrintSearch: React.FC = () => { JSON:{lastReceiveJson ?? "—"} - 時間:{lastLaserReceive.sentAt ?? "—"} 來源:{lastLaserReceive.source ?? "—"} + 時間:{formatHongKongDateTime(lastLaserReceive.sentAt)} 來源:{lastLaserReceive.source ?? "—"} {lastLaserReceive.printerAck ? ` 回覆:${lastLaserReceive.printerAck}` : ""} )} + {settingsLoaded && ( + + + 目前激光機打標(TCP:GetMarkData/GetMarkStatus/GetMarkedCount) + {markInfo ? ` ${markInfo.host}:${markInfo.port}` : ""} + + {markInfoError && ( + + {markInfoError} + + )} + {!markInfo && !markInfoError && ( + + + + 讀取中… + + + )} + {markInfo && ( + + + GetMarkStatus(0 待機/1 打標中/2 其他): + {markInfo.markStatusLabel ?? "—"} + {markInfo.markStatus != null ? `(${markInfo.markStatus})` : ""} + {markInfo.rawMarkStatus ? ` 原始:${markInfo.rawMarkStatus}` : ""} + + + GetMarkData(目前標記字串): + {fmtMarkText(markInfo.markData)} + + + GetMarkedCount: + {markInfo.markedCount != null + ? markInfo.markedCount.toLocaleString() + : fmtMarkText(markInfo.rawMarkedCount)} + {markInfo.markedCount != null && + markInfo.rawMarkedCount != null && + markInfo.rawMarkedCount !== "" && + String(markInfo.markedCount) !== markInfo.rawMarkedCount.trim() + ? ` 原始:${markInfo.rawMarkedCount}` + : ""} + + {markInfo.error ? ( + + 部分查詢:{markInfo.error} + + ) : null} + + )} + + )} + diff --git a/src/utils/formatHongKongDateTime.ts b/src/utils/formatHongKongDateTime.ts new file mode 100644 index 0000000..f7d7337 --- /dev/null +++ b/src/utils/formatHongKongDateTime.ts @@ -0,0 +1,18 @@ +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; +import timezone from "dayjs/plugin/timezone"; + +dayjs.extend(utc); +dayjs.extend(timezone); + +const HK = "Asia/Hong_Kong"; + +/** + * Formats an ISO-8601 instant (e.g. Java `Instant.toString()`) for Hong Kong (GMT+8). + */ +export function formatHongKongDateTime(value: string | null | undefined): string { + if (value == null || value === "") return "—"; + const ms = Date.parse(value); + if (Number.isNaN(ms)) return value; + return dayjs.utc(ms).tz(HK).format("YYYY-MM-DD HH:mm:ss [HKT]"); +}