"use client"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { Alert, Box, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, Paper, Snackbar, Stack, TextField, Typography, } from "@mui/material"; import ChevronLeft from "@mui/icons-material/ChevronLeft"; import ChevronRight from "@mui/icons-material/ChevronRight"; import Settings from "@mui/icons-material/Settings"; import { checkPrinterStatus, fetchLaserJobOrders, fetchLaserBag2Settings, type LaserLastReceiveSuccess, JobOrderListItem, patchSetting, expiryDateForLaserSend, sendLaserBag2Job, } from "@/app/api/laserPrint/actions"; import dayjs from "dayjs"; import { formatHongKongDateTime } from "@/utils/formatHongKongDateTime"; const BG_TOP = "#E8F4FC"; const BG_LIST = "#D4E8F7"; const BG_ROW = "#C5E1F5"; const BG_ROW_SELECTED = "#6BB5FF"; const BG_STATUS_ERROR = "#FFCCCB"; const BG_STATUS_OK = "#90EE90"; const FG_STATUS_ERROR = "#B22222"; const FG_STATUS_OK = "#006400"; const REFRESH_MS = 60 * 1000; const PRINTER_CHECK_MS = 60 * 1000; const PRINTER_RETRY_MS = 30 * 1000; const LASER_SEND_COUNT = 3; const BETWEEN_SEND_MS = 3000; const SUCCESS_SIGNAL_MS = 3500; function formatQty(val: number | null | undefined): string { if (val == null) return "—"; try { const n = Number(val); if (Number.isInteger(n)) return n.toLocaleString(); return n .toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 }) .replace(/\.?0+$/, ""); } catch { return String(val); } } function getBatch(jo: JobOrderListItem): string { return (jo.lotNo || "—").trim() || "—"; } function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } const LaserPrintSearch: React.FC = () => { const [planDate, setPlanDate] = useState(() => dayjs().format("YYYY-MM-DD")); const [jobOrders, setJobOrders] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [connected, setConnected] = useState(false); const [selectedId, setSelectedId] = useState(null); const [sendingJobId, setSendingJobId] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); const [errorSnackbar, setErrorSnackbar] = useState<{ open: boolean; message: string }>({ open: false, message: "", }); const [successSignal, setSuccessSignal] = useState(null); const [laserHost, setLaserHost] = useState(""); const [laserPort, setLaserPort] = useState("45678"); const [laserItemCodes, setLaserItemCodes] = useState(""); const [lastLaserReceive, setLastLaserReceive] = useState(null); const [settingsLoaded, setSettingsLoaded] = useState(false); const [printerConnected, setPrinterConnected] = useState(false); const [printerMessage, setPrinterMessage] = useState("激光機未連接"); const sendInFlightRef = useRef(false); const loadSystemSettings = useCallback(async () => { try { const s = await fetchLaserBag2Settings(); const host = (s.host ?? "").trim(); setLaserHost(host === "192.168.18.77" || host === "192.168.17.10" ? "" : host); setLaserPort(String(s.port)); const itemCodes = (s.itemCodes ?? "") .split(",") .map((code) => code.trim()) .filter((code) => code && code !== "PP1175" && code !== "PP2211") .join(","); setLaserItemCodes(itemCodes); setLastLaserReceive(s.lastReceiveSuccess ?? null); setSettingsLoaded(true); } catch (e) { setLastLaserReceive(null); setErrorSnackbar({ open: true, message: e instanceof Error ? e.message : "無法載入系統設定", }); setSettingsLoaded(true); } }, []); useEffect(() => { void loadSystemSettings(); }, [loadSystemSettings]); useEffect(() => { if (!successSignal) return; const t = setTimeout(() => setSuccessSignal(null), SUCCESS_SIGNAL_MS); return () => clearTimeout(t); }, [successSignal]); const loadJobOrders = useCallback( async (fromUserChange = false) => { setLoading(true); setError(null); try { const data = await fetchLaserJobOrders(planDate); setJobOrders(data); setConnected(true); if (fromUserChange) setSelectedId(null); } catch (e) { setError(e instanceof Error ? e.message : "連接不到服務器"); setConnected(false); setJobOrders([]); } finally { setLoading(false); } }, [planDate], ); useEffect(() => { void loadJobOrders(true); }, [planDate]); useEffect(() => { if (!connected) return; const id = setInterval(() => void loadJobOrders(false), REFRESH_MS); return () => clearInterval(id); }, [connected, loadJobOrders]); /** Server uses DB LASER_PRINT.host/port (omit body) so UI matches /plastic/print-laser-bag2. */ const checkLaser = useCallback(async () => { try { const result = await checkPrinterStatus({ printerType: "laser" }); setPrinterConnected(result.connected); setPrinterMessage(result.message); } catch (e) { setPrinterConnected(false); setPrinterMessage(e instanceof Error ? e.message : "激光機狀態檢查失敗"); } }, []); useEffect(() => { if (!settingsLoaded) return; void checkLaser(); }, [settingsLoaded, checkLaser]); useEffect(() => { if (!settingsLoaded) return; const intervalMs = printerConnected ? PRINTER_CHECK_MS : PRINTER_RETRY_MS; const id = setInterval(() => { void checkLaser(); }, intervalMs); return () => clearInterval(id); }, [printerConnected, checkLaser, settingsLoaded]); const goPrevDay = () => { setPlanDate((d) => dayjs(d).subtract(1, "day").format("YYYY-MM-DD")); }; const goNextDay = () => { setPlanDate((d) => dayjs(d).add(1, "day").format("YYYY-MM-DD")); }; const sendOne = (jo: JobOrderListItem) => sendLaserBag2Job({ itemId: jo.itemId, stockInLineId: jo.stockInLineId, itemCode: jo.itemCode, itemName: jo.itemName, jobOrderId: jo.id, jobOrderNo: jo.code, lotNo: jo.lotNo, expiryDate: expiryDateForLaserSend(jo.expiryDate), source: "MANUAL", }); const handleRowClick = async (jo: JobOrderListItem) => { if (sendInFlightRef.current || sendingJobId !== null) return; if (!laserHost.trim()) { setErrorSnackbar({ open: true, message: "請在系統設定中填寫激光機 IP。" }); return; } sendInFlightRef.current = true; setSelectedId(jo.id); setSendingJobId(jo.id); try { let lastAck: string | undefined; let anyReceiveAck = false; let sentOk = 0; let laterFail: string | null = null; for (let i = 0; i < LASER_SEND_COUNT; i++) { const r = await sendOne(jo); if (!r.success) { const failMsg = r.message?.trim() || `第 ${i + 1} 次送出失敗`; if (sentOk === 0) { setErrorSnackbar({ open: true, message: failMsg }); return; } laterFail = failMsg; break; } sentOk += 1; if (r.printerAck) lastAck = r.printerAck; if (r.receiveAcknowledged) anyReceiveAck = true; if (i < LASER_SEND_COUNT - 1) { await delay(BETWEEN_SEND_MS); } } const ackHint = anyReceiveAck && lastAck ? `(印表機已回覆:${lastAck})` : lastAck ? `(最後回覆:${lastAck})` : ""; setSuccessSignal( laterFail ? `已送出 ${sentOk} 次至激光機${ackHint}(後續重送失敗:${laterFail})` : `已送出 ${sentOk} 次至激光機${ackHint}`, ); await loadSystemSettings(); } catch (e) { setErrorSnackbar({ open: true, message: e instanceof Error ? e.message : "送出失敗", }); } finally { setSendingJobId(null); sendInFlightRef.current = false; } }; const saveSettings = async () => { try { await patchSetting("LASER_PRINT.host", laserHost.trim()); await patchSetting("LASER_PRINT.port", laserPort.trim() || "45678"); await patchSetting("LASER_PRINT.itemCodes", laserItemCodes.trim()); setSuccessSignal("設定已儲存"); setSettingsOpen(false); void checkLaser(); await loadSystemSettings(); void loadJobOrders(false); } catch (e) { setErrorSnackbar({ open: true, message: e instanceof Error ? e.message : "儲存失敗", }); } }; return ( {successSignal && ( setSuccessSignal(null)}> {successSignal} )} {settingsLoaded && lastLaserReceive && ( 上次印表機已確認工單:{lastLaserReceive.jobOrderNo ?? "—"}  {formatHongKongDateTime(lastLaserReceive.sentAt)} {lastLaserReceive.source ? ` (${lastLaserReceive.source === "AUTO" ? "自動送出" : "手動點選"})` : ""} 此時間只會在激光機回覆 receive 時更新。之後送出失敗不會改這裡。 )} setPlanDate(e.target.value)} size="small" sx={{ width: 160 }} InputLabelProps={{ shrink: true }} disabled={sendingJobId !== null} /> 激光機: {loading ? ( ) : error ? ( {error} ) : jobOrders.length === 0 ? ( 當日無工單 ) : ( {jobOrders.map((jo) => { const batch = getBatch(jo); const qtyStr = formatQty(jo.reqQty); const isSelected = selectedId === jo.id; const isSending = sendingJobId === jo.id; return ( void handleRowClick(jo)} > {batch} {qtyStr !== "—" && ( 數量:{qtyStr} )} {jo.code || "—"} {jo.itemCode || "—"} {jo.itemName || "—"} {isSending && } ); })} )} setSettingsOpen(false)} maxWidth="sm" fullWidth> 激光機(系統設定) 儲存後寫入資料庫,後端送出走此 IP/埠。未填 IP 則不會送出。 setLaserHost(e.target.value)} fullWidth /> setLaserPort(e.target.value)} fullWidth /> setLaserItemCodes(e.target.value)} fullWidth placeholder="例如 DEMO-FG-01" helperText="可輸入多個品號,例如 DEMO-FG-01,DEMO-FG-02。留空則列表顯示當日全部包裝工單。" /> setErrorSnackbar((s) => ({ ...s, open: false }))} message={errorSnackbar.message} anchorOrigin={{ vertical: "bottom", horizontal: "center" }} /> ); }; export default LaserPrintSearch;