| @@ -30,8 +30,10 @@ import { | |||||
| type JobOrderListItem, | type JobOrderListItem, | ||||
| } from "@/app/api/bagPrint/actions"; | } from "@/app/api/bagPrint/actions"; | ||||
| import { | import { | ||||
| fetchLaserBag2Settings, | |||||
| runLaserBag2AutoSend, | runLaserBag2AutoSend, | ||||
| type LaserBag2AutoSendReport, | type LaserBag2AutoSendReport, | ||||
| type LaserLastReceiveSuccess, | |||||
| } from "@/app/api/laserPrint/actions"; | } from "@/app/api/laserPrint/actions"; | ||||
| import * as XLSX from "xlsx"; | import * as XLSX from "xlsx"; | ||||
| @@ -79,6 +81,7 @@ export default function TestingPage() { | |||||
| const [laserAutoLoading, setLaserAutoLoading] = useState(false); | const [laserAutoLoading, setLaserAutoLoading] = useState(false); | ||||
| const [laserAutoReport, setLaserAutoReport] = useState<LaserBag2AutoSendReport | null>(null); | const [laserAutoReport, setLaserAutoReport] = useState<LaserBag2AutoSendReport | null>(null); | ||||
| const [laserAutoError, setLaserAutoError] = useState<string | null>(null); | const [laserAutoError, setLaserAutoError] = useState<string | null>(null); | ||||
| const [laserLastReceive, setLaserLastReceive] = useState<LaserLastReceiveSuccess | null>(null); | |||||
| const onpackPayload = useMemo(() => buildOnPackJobOrdersPayload(onpackJobOrders), [onpackJobOrders]); | const onpackPayload = useMemo(() => buildOnPackJobOrdersPayload(onpackJobOrders), [onpackJobOrders]); | ||||
| @@ -105,6 +108,22 @@ export default function TestingPage() { | |||||
| }; | }; | ||||
| }, [tabValue, onpackPlanDate]); | }, [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 () => { | const handleDownloadGrnPreviewXlsx = async () => { | ||||
| try { | try { | ||||
| const response = await clientAuthFetch( | const response = await clientAuthFetch( | ||||
| @@ -179,6 +198,12 @@ export default function TestingPage() { | |||||
| limitPerRun: Number.isFinite(lim) ? lim : 0, | limitPerRun: Number.isFinite(lim) ? lim : 0, | ||||
| }); | }); | ||||
| setLaserAutoReport(report); | setLaserAutoReport(report); | ||||
| try { | |||||
| const s = await fetchLaserBag2Settings(); | |||||
| setLaserLastReceive(s.lastReceiveSuccess ?? null); | |||||
| } catch { | |||||
| /* ignore */ | |||||
| } | |||||
| } catch (e) { | } catch (e) { | ||||
| setLaserAutoError(e instanceof Error ? e.message : String(e)); | setLaserAutoError(e instanceof Error ? e.message : String(e)); | ||||
| } finally { | } finally { | ||||
| @@ -351,6 +376,28 @@ export default function TestingPage() { | |||||
| <TabPanel value={tabValue} index={2}> | <TabPanel value={tabValue} index={2}> | ||||
| <Section title="3. Laser Bag2 自動送(與 /laserPrint 相同邏輯)"> | <Section title="3. Laser Bag2 自動送(與 /laserPrint 相同邏輯)"> | ||||
| {laserLastReceive ? ( | |||||
| <Alert severity="info" sx={{ mb: 2 }}> | |||||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||||
| 上次印表機已確認(receive)的工單(資料庫) | |||||
| </Typography> | |||||
| <Typography variant="body2" sx={{ mt: 0.5 }}> | |||||
| 工單號:{laserLastReceive.jobOrderNo ?? "—"} Lot:{laserLastReceive.lotNo ?? "—"} | |||||
| </Typography> | |||||
| <Typography variant="body2" sx={{ mt: 0.5, fontFamily: "monospace" }}> | |||||
| JSON:{" "} | |||||
| {laserLastReceive.itemId != null && laserLastReceive.stockInLineId != null | |||||
| ? JSON.stringify({ | |||||
| itemId: laserLastReceive.itemId, | |||||
| stockInLineId: laserLastReceive.stockInLineId, | |||||
| }) | |||||
| : "—"} | |||||
| </Typography> | |||||
| <Typography variant="caption" color="textSecondary" display="block" sx={{ mt: 0.5 }}> | |||||
| {laserLastReceive.sentAt ?? ""} {laserLastReceive.source ?? ""} | |||||
| </Typography> | |||||
| </Alert> | |||||
| ) : null} | |||||
| <Alert severity="warning" sx={{ mb: 2 }}> | <Alert severity="warning" sx={{ mb: 2 }}> | ||||
| 依資料庫 <strong>LASER_PRINT.host</strong>、<strong>LASER_PRINT.port</strong>、<strong>LASER_PRINT.itemCodes</strong> 查當日包裝工單並送 TCP(每筆工單預設 3 次、間隔 3 秒,與前端點列相同)。 | 依資料庫 <strong>LASER_PRINT.host</strong>、<strong>LASER_PRINT.port</strong>、<strong>LASER_PRINT.itemCodes</strong> 查當日包裝工單並送 TCP(每筆工單預設 3 次、間隔 3 秒,與前端點列相同)。 | ||||
| 排程預設關閉;啟用請設 <code>laser.bag2.auto-send.enabled=true</code>(後端 application.yml)。 | 排程預設關閉;啟用請設 <code>laser.bag2.auto-send.enabled=true</code>(後端 application.yml)。 | ||||
| @@ -18,11 +18,24 @@ export interface JobOrderListItem { | |||||
| laserPrintedQty?: number; | 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 { | export interface LaserBag2Settings { | ||||
| host: string; | host: string; | ||||
| port: number; | port: number; | ||||
| /** Comma-separated item codes; empty string = show all packaging job orders */ | /** Comma-separated item codes; empty string = show all packaging job orders */ | ||||
| itemCodes: string; | itemCodes: string; | ||||
| /** Last job where the laser returned a receive ack (from DB settings). */ | |||||
| lastReceiveSuccess?: LaserLastReceiveSuccess | null; | |||||
| } | } | ||||
| export interface LaserBag2SendRequest { | export interface LaserBag2SendRequest { | ||||
| @@ -32,12 +45,20 @@ export interface LaserBag2SendRequest { | |||||
| itemName: string | null; | itemName: string | null; | ||||
| printerIp?: string; | printerIp?: string; | ||||
| printerPort?: number; | printerPort?: number; | ||||
| jobOrderId?: number | null; | |||||
| jobOrderNo?: string | null; | |||||
| lotNo?: string | null; | |||||
| source?: string | null; | |||||
| } | } | ||||
| export interface LaserBag2SendResponse { | export interface LaserBag2SendResponse { | ||||
| success: boolean; | success: boolean; | ||||
| message: string; | message: string; | ||||
| payloadSent?: string | null; | 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; | itemCode: string | null; | ||||
| success: boolean; | success: boolean; | ||||
| message: string; | message: string; | ||||
| printerAck?: string | null; | |||||
| receiveAcknowledged?: boolean; | |||||
| } | } | ||||
| export interface LaserBag2AutoSendReport { | export interface LaserBag2AutoSendReport { | ||||
| @@ -23,6 +23,7 @@ import { | |||||
| checkPrinterStatus, | checkPrinterStatus, | ||||
| fetchLaserJobOrders, | fetchLaserJobOrders, | ||||
| fetchLaserBag2Settings, | fetchLaserBag2Settings, | ||||
| type LaserLastReceiveSuccess, | |||||
| JobOrderListItem, | JobOrderListItem, | ||||
| patchSetting, | patchSetting, | ||||
| sendLaserBag2Job, | sendLaserBag2Job, | ||||
| @@ -83,6 +84,7 @@ const LaserPrintSearch: React.FC = () => { | |||||
| const [laserHost, setLaserHost] = useState("192.168.18.77"); | const [laserHost, setLaserHost] = useState("192.168.18.77"); | ||||
| const [laserPort, setLaserPort] = useState("45678"); | const [laserPort, setLaserPort] = useState("45678"); | ||||
| const [laserItemCodes, setLaserItemCodes] = useState("PP1175"); | const [laserItemCodes, setLaserItemCodes] = useState("PP1175"); | ||||
| const [lastLaserReceive, setLastLaserReceive] = useState<LaserLastReceiveSuccess | null>(null); | |||||
| const [settingsLoaded, setSettingsLoaded] = useState(false); | const [settingsLoaded, setSettingsLoaded] = useState(false); | ||||
| const [printerConnected, setPrinterConnected] = useState(false); | const [printerConnected, setPrinterConnected] = useState(false); | ||||
| const [printerMessage, setPrinterMessage] = useState("檸檬機(激光機)未連接"); | const [printerMessage, setPrinterMessage] = useState("檸檬機(激光機)未連接"); | ||||
| @@ -93,8 +95,10 @@ const LaserPrintSearch: React.FC = () => { | |||||
| setLaserHost(s.host); | setLaserHost(s.host); | ||||
| setLaserPort(String(s.port)); | setLaserPort(String(s.port)); | ||||
| setLaserItemCodes(s.itemCodes ?? "PP1175"); | setLaserItemCodes(s.itemCodes ?? "PP1175"); | ||||
| setLastLaserReceive(s.lastReceiveSuccess ?? null); | |||||
| setSettingsLoaded(true); | setSettingsLoaded(true); | ||||
| } catch (e) { | } catch (e) { | ||||
| setLastLaserReceive(null); | |||||
| setErrorSnackbar({ | setErrorSnackbar({ | ||||
| open: true, | open: true, | ||||
| message: e instanceof Error ? e.message : "無法載入系統設定", | message: e instanceof Error ? e.message : "無法載入系統設定", | ||||
| @@ -183,6 +187,10 @@ const LaserPrintSearch: React.FC = () => { | |||||
| stockInLineId: jo.stockInLineId, | stockInLineId: jo.stockInLineId, | ||||
| itemCode: jo.itemCode, | itemCode: jo.itemCode, | ||||
| itemName: jo.itemName, | itemName: jo.itemName, | ||||
| jobOrderId: jo.id, | |||||
| jobOrderNo: jo.code, | |||||
| lotNo: jo.lotNo, | |||||
| source: "MANUAL", | |||||
| }); | }); | ||||
| const handleRowClick = async (jo: JobOrderListItem) => { | const handleRowClick = async (jo: JobOrderListItem) => { | ||||
| @@ -196,6 +204,8 @@ const LaserPrintSearch: React.FC = () => { | |||||
| setSelectedId(jo.id); | setSelectedId(jo.id); | ||||
| setSendingJobId(jo.id); | setSendingJobId(jo.id); | ||||
| try { | try { | ||||
| let lastAck: string | undefined; | |||||
| let anyReceiveAck = false; | |||||
| for (let i = 0; i < LASER_SEND_COUNT; i++) { | for (let i = 0; i < LASER_SEND_COUNT; i++) { | ||||
| const r = await sendOne(jo); | const r = await sendOne(jo); | ||||
| if (!r.success) { | if (!r.success) { | ||||
| @@ -205,11 +215,20 @@ const LaserPrintSearch: React.FC = () => { | |||||
| }); | }); | ||||
| return; | return; | ||||
| } | } | ||||
| if (r.printerAck) lastAck = r.printerAck; | |||||
| if (r.receiveAcknowledged) anyReceiveAck = true; | |||||
| if (i < LASER_SEND_COUNT - 1) { | if (i < LASER_SEND_COUNT - 1) { | ||||
| await delay(BETWEEN_SEND_MS); | 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) { | } catch (e) { | ||||
| setErrorSnackbar({ | setErrorSnackbar({ | ||||
| open: true, | 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 ( | return ( | ||||
| <Box sx={{ minHeight: "70vh", display: "flex", flexDirection: "column" }}> | <Box sx={{ minHeight: "70vh", display: "flex", flexDirection: "column" }}> | ||||
| {successSignal && ( | {successSignal && ( | ||||
| @@ -245,6 +272,23 @@ const LaserPrintSearch: React.FC = () => { | |||||
| {successSignal} | {successSignal} | ||||
| </Alert> | </Alert> | ||||
| )} | )} | ||||
| {settingsLoaded && lastLaserReceive && ( | |||||
| <Alert severity="info" sx={{ mb: 2 }}> | |||||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | |||||
| 上次印表機已確認(receive)的工單 | |||||
| </Typography> | |||||
| <Typography variant="body2" component="div" sx={{ mt: 0.5 }}> | |||||
| 工單號:{lastLaserReceive.jobOrderNo ?? "—"} 批號/Lot:{lastLaserReceive.lotNo ?? "—"} | |||||
| </Typography> | |||||
| <Typography variant="body2" component="div" sx={{ mt: 0.5, fontFamily: "monospace" }}> | |||||
| JSON:{lastReceiveJson ?? "—"} | |||||
| </Typography> | |||||
| <Typography variant="caption" color="text.secondary" display="block" sx={{ mt: 0.5 }}> | |||||
| 時間:{lastLaserReceive.sentAt ?? "—"} 來源:{lastLaserReceive.source ?? "—"} | |||||
| {lastLaserReceive.printerAck ? ` 回覆:${lastLaserReceive.printerAck}` : ""} | |||||
| </Typography> | |||||
| </Alert> | |||||
| )} | |||||
| <Paper sx={{ p: 2, mb: 2, backgroundColor: BG_TOP }}> | <Paper sx={{ p: 2, mb: 2, backgroundColor: BG_TOP }}> | ||||
| <Stack direction="row" alignItems="center" justifyContent="space-between" flexWrap="wrap" gap={2}> | <Stack direction="row" alignItems="center" justifyContent="space-between" flexWrap="wrap" gap={2}> | ||||