diff --git a/src/app/(main)/testing/page.tsx b/src/app/(main)/testing/page.tsx index d2ce781..ac10ecb 100644 --- a/src/app/(main)/testing/page.tsx +++ b/src/app/(main)/testing/page.tsx @@ -30,8 +30,10 @@ import { type JobOrderListItem, } from "@/app/api/bagPrint/actions"; import { + fetchLaserBag2Settings, runLaserBag2AutoSend, type LaserBag2AutoSendReport, + type LaserLastReceiveSuccess, } from "@/app/api/laserPrint/actions"; import * as XLSX from "xlsx"; @@ -79,6 +81,7 @@ export default function TestingPage() { const [laserAutoLoading, setLaserAutoLoading] = useState(false); const [laserAutoReport, setLaserAutoReport] = useState(null); const [laserAutoError, setLaserAutoError] = useState(null); + const [laserLastReceive, setLaserLastReceive] = useState(null); const onpackPayload = useMemo(() => buildOnPackJobOrdersPayload(onpackJobOrders), [onpackJobOrders]); @@ -105,6 +108,22 @@ export default function TestingPage() { }; }, [tabValue, onpackPlanDate]); + useEffect(() => { + if (tabValue !== 2) return; + let cancelled = false; + (async () => { + try { + const s = await fetchLaserBag2Settings(); + if (!cancelled) setLaserLastReceive(s.lastReceiveSuccess ?? null); + } catch { + if (!cancelled) setLaserLastReceive(null); + } + })(); + return () => { + cancelled = true; + }; + }, [tabValue]); + const handleDownloadGrnPreviewXlsx = async () => { try { const response = await clientAuthFetch( @@ -179,6 +198,12 @@ export default function TestingPage() { limitPerRun: Number.isFinite(lim) ? lim : 0, }); setLaserAutoReport(report); + try { + const s = await fetchLaserBag2Settings(); + setLaserLastReceive(s.lastReceiveSuccess ?? null); + } catch { + /* ignore */ + } } catch (e) { setLaserAutoError(e instanceof Error ? e.message : String(e)); } finally { @@ -351,6 +376,28 @@ export default function TestingPage() {
+ {laserLastReceive ? ( + + + 上次印表機已確認(receive)的工單(資料庫) + + + 工單號:{laserLastReceive.jobOrderNo ?? "—"} Lot:{laserLastReceive.lotNo ?? "—"} + + + JSON:{" "} + {laserLastReceive.itemId != null && laserLastReceive.stockInLineId != null + ? JSON.stringify({ + itemId: laserLastReceive.itemId, + stockInLineId: laserLastReceive.stockInLineId, + }) + : "—"} + + + {laserLastReceive.sentAt ?? ""} {laserLastReceive.source ?? ""} + + + ) : null} 依資料庫 LASER_PRINT.hostLASER_PRINT.portLASER_PRINT.itemCodes 查當日包裝工單並送 TCP(每筆工單預設 3 次、間隔 3 秒,與前端點列相同)。 排程預設關閉;啟用請設 laser.bag2.auto-send.enabled=true(後端 application.yml)。 diff --git a/src/app/api/laserPrint/actions.ts b/src/app/api/laserPrint/actions.ts index eb76fd4..952892c 100644 --- a/src/app/api/laserPrint/actions.ts +++ b/src/app/api/laserPrint/actions.ts @@ -18,11 +18,24 @@ export interface JobOrderListItem { laserPrintedQty?: number; } +export interface LaserLastReceiveSuccess { + jobOrderId?: number | null; + jobOrderNo?: string | null; + lotNo?: string | null; + itemId?: number | null; + stockInLineId?: number | null; + printerAck?: string | null; + sentAt?: string | null; + source?: string | null; +} + export interface LaserBag2Settings { host: string; port: number; /** Comma-separated item codes; empty string = show all packaging job orders */ itemCodes: string; + /** Last job where the laser returned a receive ack (from DB settings). */ + lastReceiveSuccess?: LaserLastReceiveSuccess | null; } export interface LaserBag2SendRequest { @@ -32,12 +45,20 @@ export interface LaserBag2SendRequest { itemName: string | null; printerIp?: string; printerPort?: number; + jobOrderId?: number | null; + jobOrderNo?: string | null; + lotNo?: string | null; + source?: string | null; } export interface LaserBag2SendResponse { success: boolean; message: string; payloadSent?: string | null; + /** Raw TCP reply from the laser plugin (often `receive;;`). */ + printerAck?: string | null; + /** True when the peer reply contained `receive` and not `invalid`. */ + receiveAcknowledged?: boolean; } /** @@ -133,6 +154,8 @@ export interface LaserBag2JobSendResult { itemCode: string | null; success: boolean; message: string; + printerAck?: string | null; + receiveAcknowledged?: boolean; } export interface LaserBag2AutoSendReport { diff --git a/src/components/LaserPrint/LaserPrintSearch.tsx b/src/components/LaserPrint/LaserPrintSearch.tsx index 873f57b..5511cba 100644 --- a/src/components/LaserPrint/LaserPrintSearch.tsx +++ b/src/components/LaserPrint/LaserPrintSearch.tsx @@ -23,6 +23,7 @@ import { checkPrinterStatus, fetchLaserJobOrders, fetchLaserBag2Settings, + type LaserLastReceiveSuccess, JobOrderListItem, patchSetting, sendLaserBag2Job, @@ -83,6 +84,7 @@ const LaserPrintSearch: React.FC = () => { const [laserHost, setLaserHost] = useState("192.168.18.77"); const [laserPort, setLaserPort] = useState("45678"); const [laserItemCodes, setLaserItemCodes] = useState("PP1175"); + const [lastLaserReceive, setLastLaserReceive] = useState(null); const [settingsLoaded, setSettingsLoaded] = useState(false); const [printerConnected, setPrinterConnected] = useState(false); const [printerMessage, setPrinterMessage] = useState("檸檬機(激光機)未連接"); @@ -93,8 +95,10 @@ const LaserPrintSearch: React.FC = () => { setLaserHost(s.host); setLaserPort(String(s.port)); setLaserItemCodes(s.itemCodes ?? "PP1175"); + setLastLaserReceive(s.lastReceiveSuccess ?? null); setSettingsLoaded(true); } catch (e) { + setLastLaserReceive(null); setErrorSnackbar({ open: true, message: e instanceof Error ? e.message : "無法載入系統設定", @@ -183,6 +187,10 @@ const LaserPrintSearch: React.FC = () => { stockInLineId: jo.stockInLineId, itemCode: jo.itemCode, itemName: jo.itemName, + jobOrderId: jo.id, + jobOrderNo: jo.code, + lotNo: jo.lotNo, + source: "MANUAL", }); const handleRowClick = async (jo: JobOrderListItem) => { @@ -196,6 +204,8 @@ const LaserPrintSearch: React.FC = () => { setSelectedId(jo.id); setSendingJobId(jo.id); try { + let lastAck: string | undefined; + let anyReceiveAck = false; for (let i = 0; i < LASER_SEND_COUNT; i++) { const r = await sendOne(jo); if (!r.success) { @@ -205,11 +215,20 @@ const LaserPrintSearch: React.FC = () => { }); return; } + if (r.printerAck) lastAck = r.printerAck; + if (r.receiveAcknowledged) anyReceiveAck = true; if (i < LASER_SEND_COUNT - 1) { await delay(BETWEEN_SEND_MS); } } - setSuccessSignal(`已送出 ${LASER_SEND_COUNT} 次至檸檬機(激光機)`); + const ackHint = + anyReceiveAck && lastAck + ? `(印表機已回覆:${lastAck})` + : lastAck + ? `(最後回覆:${lastAck})` + : ""; + setSuccessSignal(`已送出 ${LASER_SEND_COUNT} 次至檸檬機(激光機)${ackHint}`); + await loadSystemSettings(); } catch (e) { setErrorSnackbar({ open: true, @@ -238,6 +257,14 @@ const LaserPrintSearch: React.FC = () => { } }; + const lastReceiveJson = + lastLaserReceive?.itemId != null && lastLaserReceive?.stockInLineId != null + ? JSON.stringify({ + itemId: lastLaserReceive.itemId, + stockInLineId: lastLaserReceive.stockInLineId, + }) + : null; + return ( {successSignal && ( @@ -245,6 +272,23 @@ const LaserPrintSearch: React.FC = () => { {successSignal} )} + {settingsLoaded && lastLaserReceive && ( + + + 上次印表機已確認(receive)的工單 + + + 工單號:{lastLaserReceive.jobOrderNo ?? "—"} 批號/Lot:{lastLaserReceive.lotNo ?? "—"} + + + JSON:{lastReceiveJson ?? "—"} + + + 時間:{lastLaserReceive.sentAt ?? "—"} 來源:{lastLaserReceive.source ?? "—"} + {lastLaserReceive.printerAck ? ` 回覆:${lastLaserReceive.printerAck}` : ""} + + + )}