DESKTOP-064TTA1\Fai LUK 3 дней назад
Родитель
Сommit
519a2fb82c
3 измененных файлов: 115 добавлений и 1 удалений
  1. +47
    -0
      src/app/(main)/testing/page.tsx
  2. +23
    -0
      src/app/api/laserPrint/actions.ts
  3. +45
    -1
      src/components/LaserPrint/LaserPrintSearch.tsx

+ 47
- 0
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<LaserBag2AutoSendReport | null>(null);
const [laserAutoError, setLaserAutoError] = useState<string | null>(null);
const [laserLastReceive, setLaserLastReceive] = useState<LaserLastReceiveSuccess | null>(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() {

<TabPanel value={tabValue} index={2}>
<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 }}>
依資料庫 <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)。


+ 23
- 0
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 {


+ 45
- 1
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<LaserLastReceiveSuccess | null>(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 (
<Box sx={{ minHeight: "70vh", display: "flex", flexDirection: "column" }}>
{successSignal && (
@@ -245,6 +272,23 @@ const LaserPrintSearch: React.FC = () => {
{successSignal}
</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 }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" flexWrap="wrap" gap={2}>


Загрузка…
Отмена
Сохранить