|
|
|
@@ -22,6 +22,7 @@ import { FileDownload } from "@mui/icons-material"; |
|
|
|
import dayjs from "dayjs"; |
|
|
|
import { NEXT_PUBLIC_API_URL } from "@/config/api"; |
|
|
|
import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; |
|
|
|
import LotLabelPrintModal from "@/components/InventorySearch/LotLabelPrintModal"; |
|
|
|
import { |
|
|
|
buildOnPackJobOrdersPayload, |
|
|
|
downloadOnPackTextQrZip, |
|
|
|
@@ -60,30 +61,43 @@ function TabPanel(props: TabPanelProps) { |
|
|
|
|
|
|
|
export default function TestingPage() { |
|
|
|
const [tabValue, setTabValue] = useState(0); |
|
|
|
const [lotLabelModalOpen, setLotLabelModalOpen] = useState(false); |
|
|
|
|
|
|
|
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { |
|
|
|
setTabValue(newValue); |
|
|
|
}; |
|
|
|
|
|
|
|
// --- 1. GRN Preview (M18) --- |
|
|
|
const [grnPreviewReceiptDate, setGrnPreviewReceiptDate] = useState("2026-03-16"); |
|
|
|
const [grnPreviewReceiptDate, setGrnPreviewReceiptDate] = |
|
|
|
useState("2026-03-16"); |
|
|
|
// --- 2. OnPack NGPCL (same job-order → ZIP logic as /bagPrint) --- |
|
|
|
const [onpackPlanDate, setOnpackPlanDate] = useState(() => dayjs().format("YYYY-MM-DD")); |
|
|
|
const [onpackJobOrders, setOnpackJobOrders] = useState<JobOrderListItem[]>([]); |
|
|
|
const [onpackPlanDate, setOnpackPlanDate] = useState(() => |
|
|
|
dayjs().format("YYYY-MM-DD"), |
|
|
|
); |
|
|
|
const [onpackJobOrders, setOnpackJobOrders] = useState<JobOrderListItem[]>( |
|
|
|
[], |
|
|
|
); |
|
|
|
const [onpackLoading, setOnpackLoading] = useState(false); |
|
|
|
const [onpackLoadError, setOnpackLoadError] = useState<string | null>(null); |
|
|
|
const [onpackLemonDownloading, setOnpackLemonDownloading] = useState(false); |
|
|
|
const [onpackPushLoading, setOnpackPushLoading] = useState(false); |
|
|
|
const [onpackPushResult, setOnpackPushResult] = useState<string | null>(null); |
|
|
|
// --- 3. Laser Bag2 auto-send (same as /laserPrint + DB LASER_PRINT.*) --- |
|
|
|
const [laserAutoPlanDate, setLaserAutoPlanDate] = useState(() => dayjs().format("YYYY-MM-DD")); |
|
|
|
const [laserAutoPlanDate, setLaserAutoPlanDate] = useState(() => |
|
|
|
dayjs().format("YYYY-MM-DD"), |
|
|
|
); |
|
|
|
const [laserAutoLimit, setLaserAutoLimit] = useState("1"); |
|
|
|
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 [laserLastReceive, setLaserLastReceive] = useState<LaserLastReceiveSuccess | null>(null); |
|
|
|
const [laserLastReceive, setLaserLastReceive] = |
|
|
|
useState<LaserLastReceiveSuccess | null>(null); |
|
|
|
|
|
|
|
const onpackPayload = useMemo(() => buildOnPackJobOrdersPayload(onpackJobOrders), [onpackJobOrders]); |
|
|
|
const onpackPayload = useMemo( |
|
|
|
() => buildOnPackJobOrdersPayload(onpackJobOrders), |
|
|
|
[onpackJobOrders], |
|
|
|
); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (tabValue !== 1) return; |
|
|
|
@@ -96,7 +110,9 @@ export default function TestingPage() { |
|
|
|
if (!cancelled) setOnpackJobOrders(data); |
|
|
|
} catch (e) { |
|
|
|
if (!cancelled) { |
|
|
|
setOnpackLoadError(e instanceof Error ? e.message : "Failed to load job orders"); |
|
|
|
setOnpackLoadError( |
|
|
|
e instanceof Error ? e.message : "Failed to load job orders", |
|
|
|
); |
|
|
|
setOnpackJobOrders([]); |
|
|
|
} |
|
|
|
} finally { |
|
|
|
@@ -127,7 +143,9 @@ export default function TestingPage() { |
|
|
|
const handleDownloadGrnPreviewXlsx = async () => { |
|
|
|
try { |
|
|
|
const response = await clientAuthFetch( |
|
|
|
`${NEXT_PUBLIC_API_URL}/report/grn-preview-m18?receiptDate=${encodeURIComponent(grnPreviewReceiptDate)}`, |
|
|
|
`${NEXT_PUBLIC_API_URL}/report/grn-preview-m18?receiptDate=${encodeURIComponent( |
|
|
|
grnPreviewReceiptDate, |
|
|
|
)}`, |
|
|
|
{ method: "GET" }, |
|
|
|
); |
|
|
|
if (response.status === 401 || response.status === 403) return; |
|
|
|
@@ -140,7 +158,10 @@ export default function TestingPage() { |
|
|
|
const wb = XLSX.utils.book_new(); |
|
|
|
XLSX.utils.book_append_sheet(wb, ws, "GRN Preview"); |
|
|
|
|
|
|
|
const xlsxArrayBuffer = XLSX.write(wb, { bookType: "xlsx", type: "array" }); |
|
|
|
const xlsxArrayBuffer = XLSX.write(wb, { |
|
|
|
bookType: "xlsx", |
|
|
|
type: "array", |
|
|
|
}); |
|
|
|
const blob = new Blob([xlsxArrayBuffer], { |
|
|
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", |
|
|
|
}); |
|
|
|
@@ -148,7 +169,10 @@ export default function TestingPage() { |
|
|
|
const url = window.URL.createObjectURL(blob); |
|
|
|
const link = document.createElement("a"); |
|
|
|
link.href = url; |
|
|
|
link.setAttribute("download", `grn-preview-m18-${grnPreviewReceiptDate}.xlsx`); |
|
|
|
link.setAttribute( |
|
|
|
"download", |
|
|
|
`grn-preview-m18-${grnPreviewReceiptDate}.xlsx`, |
|
|
|
); |
|
|
|
document.body.appendChild(link); |
|
|
|
link.click(); |
|
|
|
link.remove(); |
|
|
|
@@ -172,7 +196,9 @@ export default function TestingPage() { |
|
|
|
|
|
|
|
const handleOnpackDownloadLemonZip = async () => { |
|
|
|
if (onpackPayload.length === 0) { |
|
|
|
alert("No job orders with item code for this plan date (same rule as Bag Print)."); |
|
|
|
alert( |
|
|
|
"No job orders with item code for this plan date (same rule as Bag Print).", |
|
|
|
); |
|
|
|
return; |
|
|
|
} |
|
|
|
setOnpackLemonDownloading(true); |
|
|
|
@@ -220,7 +246,9 @@ export default function TestingPage() { |
|
|
|
setOnpackPushResult(null); |
|
|
|
try { |
|
|
|
const r = await pushOnPackTextQrZipToNgpcl({ jobOrders: onpackPayload }); |
|
|
|
setOnpackPushResult(`${r.pushed ? "Pushed" : "Not pushed"}: ${r.message}`); |
|
|
|
setOnpackPushResult( |
|
|
|
`${r.pushed ? "Pushed" : "Not pushed"}: ${r.message}`, |
|
|
|
); |
|
|
|
} catch (e) { |
|
|
|
const msg = e instanceof Error ? e.message : String(e); |
|
|
|
setOnpackPushResult(`Error: ${msg}`); |
|
|
|
@@ -230,12 +258,34 @@ export default function TestingPage() { |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const Section = ({ title, children }: { title: string; children?: React.ReactNode }) => ( |
|
|
|
<Paper sx={{ p: 3, minHeight: "450px", display: "flex", flexDirection: "column" }}> |
|
|
|
<Typography variant="h5" gutterBottom color="primary" sx={{ borderBottom: "2px solid #f0f0f0", pb: 1, mb: 2 }}> |
|
|
|
const Section = ({ |
|
|
|
title, |
|
|
|
children, |
|
|
|
}: { |
|
|
|
title: string; |
|
|
|
children?: React.ReactNode; |
|
|
|
}) => ( |
|
|
|
<Paper |
|
|
|
sx={{ |
|
|
|
p: 3, |
|
|
|
minHeight: "450px", |
|
|
|
display: "flex", |
|
|
|
flexDirection: "column", |
|
|
|
}} |
|
|
|
> |
|
|
|
<Typography |
|
|
|
variant="h5" |
|
|
|
gutterBottom |
|
|
|
color="primary" |
|
|
|
sx={{ borderBottom: "2px solid #f0f0f0", pb: 1, mb: 2 }} |
|
|
|
> |
|
|
|
{title} |
|
|
|
</Typography> |
|
|
|
{children || <Typography color="textSecondary" sx={{ m: "auto" }}>Waiting for implementation...</Typography>} |
|
|
|
{children || ( |
|
|
|
<Typography color="textSecondary" sx={{ m: "auto" }}> |
|
|
|
Waiting for implementation... |
|
|
|
</Typography> |
|
|
|
)} |
|
|
|
</Paper> |
|
|
|
); |
|
|
|
|
|
|
|
@@ -245,15 +295,26 @@ export default function TestingPage() { |
|
|
|
Testing |
|
|
|
</Typography> |
|
|
|
|
|
|
|
<Tabs value={tabValue} onChange={handleTabChange} aria-label="testing sections tabs" centered variant="fullWidth"> |
|
|
|
<Tabs |
|
|
|
value={tabValue} |
|
|
|
onChange={handleTabChange} |
|
|
|
aria-label="testing sections tabs" |
|
|
|
centered |
|
|
|
variant="fullWidth" |
|
|
|
> |
|
|
|
<Tab label="1. GRN Preview" /> |
|
|
|
<Tab label="2. OnPack NGPCL" /> |
|
|
|
<Tab label="3. Laser Bag2 自動送" /> |
|
|
|
<Tab label="4. 批號標籤列印" /> |
|
|
|
</Tabs> |
|
|
|
|
|
|
|
<TabPanel value={tabValue} index={0}> |
|
|
|
<Section title="1. GRN Preview (M18)"> |
|
|
|
<Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}> |
|
|
|
<Stack |
|
|
|
direction="row" |
|
|
|
spacing={2} |
|
|
|
sx={{ mb: 2, alignItems: "center" }} |
|
|
|
> |
|
|
|
<TextField |
|
|
|
size="small" |
|
|
|
label="Receipt Date" |
|
|
|
@@ -273,7 +334,8 @@ export default function TestingPage() { |
|
|
|
</Button> |
|
|
|
</Stack> |
|
|
|
<Typography variant="body2" color="textSecondary"> |
|
|
|
Backend endpoint: <code>/report/grn-preview-m18?receiptDate=YYYY-MM-DD</code> |
|
|
|
Backend endpoint:{" "} |
|
|
|
<code>/report/grn-preview-m18?receiptDate=YYYY-MM-DD</code> |
|
|
|
</Typography> |
|
|
|
</Section> |
|
|
|
</TabPanel> |
|
|
|
@@ -281,16 +343,24 @@ export default function TestingPage() { |
|
|
|
<TabPanel value={tabValue} index={1}> |
|
|
|
<Section title="2. OnPack NGPCL (same logic as /bagPrint)"> |
|
|
|
<Alert severity="info" sx={{ mb: 2 }}> |
|
|
|
Uses <strong>GET /py/job-orders?planStart=</strong> for the day, then the same <code>jobOrders</code> payload as{" "} |
|
|
|
<strong>Bag Print → 下載 OnPack2023檸檬機</strong>. The ZIP contains loose <code>.job</code> / <code>.image</code> / BMPs — extract |
|
|
|
Uses <strong>GET /py/job-orders?planStart=</strong> for the day, |
|
|
|
then the same <code>jobOrders</code> payload as{" "} |
|
|
|
<strong>Bag Print → 下載 OnPack2023檸檬機</strong>. The ZIP contains |
|
|
|
loose <code>.job</code> / <code>.image</code> / BMPs — extract |
|
|
|
before sending to NGE; the ZIP itself is only a transport bundle. |
|
|
|
</Alert> |
|
|
|
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}> |
|
|
|
Distinct item codes in the list produce one label set each (backend groups by code). Configure <code>ngpcl.push-url</code> on the server |
|
|
|
to POST the same lemon ZIP bytes to your NGPCL HTTP gateway; otherwise use download only. |
|
|
|
Distinct item codes in the list produce one label set each (backend |
|
|
|
groups by code). Configure <code>ngpcl.push-url</code> on the server |
|
|
|
to POST the same lemon ZIP bytes to your NGPCL HTTP gateway; |
|
|
|
otherwise use download only. |
|
|
|
</Typography> |
|
|
|
|
|
|
|
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} sx={{ mb: 2, alignItems: "center", flexWrap: "wrap" }}> |
|
|
|
<Stack |
|
|
|
direction={{ xs: "column", sm: "row" }} |
|
|
|
spacing={2} |
|
|
|
sx={{ mb: 2, alignItems: "center", flexWrap: "wrap" }} |
|
|
|
> |
|
|
|
<TextField |
|
|
|
size="small" |
|
|
|
label="Plan date (planStart)" |
|
|
|
@@ -302,7 +372,10 @@ export default function TestingPage() { |
|
|
|
<Typography variant="body2" color="textSecondary"> |
|
|
|
{onpackLoading ? ( |
|
|
|
<> |
|
|
|
<CircularProgress size={16} sx={{ mr: 1, verticalAlign: "middle" }} /> |
|
|
|
<CircularProgress |
|
|
|
size={16} |
|
|
|
sx={{ mr: 1, verticalAlign: "middle" }} |
|
|
|
/> |
|
|
|
Loading job orders… |
|
|
|
</> |
|
|
|
) : ( |
|
|
|
@@ -357,19 +430,44 @@ export default function TestingPage() { |
|
|
|
sx={{ mb: 2, fontFamily: "monospace" }} |
|
|
|
/> |
|
|
|
|
|
|
|
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} sx={{ mb: 2, flexWrap: "wrap" }}> |
|
|
|
<Button variant="contained" color="success" onClick={handleOnpackDownloadLemonZip} disabled={onpackLemonDownloading || onpackLoading}> |
|
|
|
{onpackLemonDownloading ? "Downloading…" : "Download lemon OnPack ZIP"} |
|
|
|
<Stack |
|
|
|
direction={{ xs: "column", sm: "row" }} |
|
|
|
spacing={2} |
|
|
|
sx={{ mb: 2, flexWrap: "wrap" }} |
|
|
|
> |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
color="success" |
|
|
|
onClick={handleOnpackDownloadLemonZip} |
|
|
|
disabled={onpackLemonDownloading || onpackLoading} |
|
|
|
> |
|
|
|
{onpackLemonDownloading |
|
|
|
? "Downloading…" |
|
|
|
: "Download lemon OnPack ZIP"} |
|
|
|
</Button> |
|
|
|
<Button variant="outlined" onClick={handleOnpackPushNgpcl} disabled={onpackPushLoading || onpackLoading}> |
|
|
|
{onpackPushLoading ? "Pushing…" : "Push to NGPCL (server → ngpcl.push-url)"} |
|
|
|
<Button |
|
|
|
variant="outlined" |
|
|
|
onClick={handleOnpackPushNgpcl} |
|
|
|
disabled={onpackPushLoading || onpackLoading} |
|
|
|
> |
|
|
|
{onpackPushLoading |
|
|
|
? "Pushing…" |
|
|
|
: "Push to NGPCL (server → ngpcl.push-url)"} |
|
|
|
</Button> |
|
|
|
</Stack> |
|
|
|
{onpackPushResult ? ( |
|
|
|
<TextField fullWidth multiline minRows={2} label="Last NGPCL push result" value={onpackPushResult} InputProps={{ readOnly: true }} /> |
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
multiline |
|
|
|
minRows={2} |
|
|
|
label="Last NGPCL push result" |
|
|
|
value={onpackPushResult} |
|
|
|
InputProps={{ readOnly: true }} |
|
|
|
/> |
|
|
|
) : null} |
|
|
|
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}> |
|
|
|
<code>POST /plastic/download-onpack-qr-text</code> · <code>POST /plastic/ngpcl/push-onpack-qr-text</code> (same body) |
|
|
|
<code>POST /plastic/download-onpack-qr-text</code> ·{" "} |
|
|
|
<code>POST /plastic/ngpcl/push-onpack-qr-text</code> (same body) |
|
|
|
</Typography> |
|
|
|
</Section> |
|
|
|
</TabPanel> |
|
|
|
@@ -382,27 +480,46 @@ export default function TestingPage() { |
|
|
|
上次印表機已確認(receive)的工單(資料庫) |
|
|
|
</Typography> |
|
|
|
<Typography variant="body2" sx={{ mt: 0.5 }}> |
|
|
|
工單號:{laserLastReceive.jobOrderNo ?? "—"} Lot:{laserLastReceive.lotNo ?? "—"} |
|
|
|
工單號:{laserLastReceive.jobOrderNo ?? "—"} Lot: |
|
|
|
{laserLastReceive.lotNo ?? "—"} |
|
|
|
</Typography> |
|
|
|
<Typography variant="body2" sx={{ mt: 0.5, fontFamily: "monospace" }}> |
|
|
|
<Typography |
|
|
|
variant="body2" |
|
|
|
sx={{ mt: 0.5, fontFamily: "monospace" }} |
|
|
|
> |
|
|
|
JSON:{" "} |
|
|
|
{laserLastReceive.itemId != null && laserLastReceive.stockInLineId != null |
|
|
|
{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 }}> |
|
|
|
<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)。 |
|
|
|
依資料庫 <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)。 |
|
|
|
</Alert> |
|
|
|
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} sx={{ mb: 2, alignItems: "center", flexWrap: "wrap" }}> |
|
|
|
<Stack |
|
|
|
direction={{ xs: "column", sm: "row" }} |
|
|
|
spacing={2} |
|
|
|
sx={{ mb: 2, alignItems: "center", flexWrap: "wrap" }} |
|
|
|
> |
|
|
|
<TextField |
|
|
|
size="small" |
|
|
|
label="Plan date (planStart)" |
|
|
|
@@ -419,8 +536,15 @@ export default function TestingPage() { |
|
|
|
sx={{ width: 200 }} |
|
|
|
helperText="手動測試建議 1;排程預設每分鐘最多 1 筆" |
|
|
|
/> |
|
|
|
<Button variant="contained" color="primary" onClick={() => void handleLaserBag2AutoSend()} disabled={laserAutoLoading}> |
|
|
|
{laserAutoLoading ? "送出中…" : "執行 POST /plastic/laser-bag2-auto-send"} |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
color="primary" |
|
|
|
onClick={() => void handleLaserBag2AutoSend()} |
|
|
|
disabled={laserAutoLoading} |
|
|
|
> |
|
|
|
{laserAutoLoading |
|
|
|
? "送出中…" |
|
|
|
: "執行 POST /plastic/laser-bag2-auto-send"} |
|
|
|
</Button> |
|
|
|
</Stack> |
|
|
|
{laserAutoError ? ( |
|
|
|
@@ -440,10 +564,42 @@ export default function TestingPage() { |
|
|
|
/> |
|
|
|
) : null} |
|
|
|
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}> |
|
|
|
<code>POST /api/plastic/laser-bag2-auto-send?planStart=YYYY-MM-DD&limitPerRun=N</code> |
|
|
|
<code> |
|
|
|
POST |
|
|
|
/api/plastic/laser-bag2-auto-send?planStart=YYYY-MM-DD&limitPerRun=N |
|
|
|
</code> |
|
|
|
</Typography> |
|
|
|
</Section> |
|
|
|
</TabPanel> |
|
|
|
|
|
|
|
<TabPanel value={tabValue} index={3}> |
|
|
|
<Section title="4. 批號標籤列印(掃碼 → 查同品批號 → 選印表機 → 列印)"> |
|
|
|
<Alert severity="info" sx={{ mb: 2 }}> |
|
|
|
此工具會呼叫後端 <code>/inventoryLotLine/analyze-qr-code</code>{" "} |
|
|
|
找同品可用批號,再用 <code>/inventoryLotLine/print-label</code>(需 |
|
|
|
printerId)送出列印。 |
|
|
|
</Alert> |
|
|
|
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}> |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
onClick={() => setLotLabelModalOpen(true)} |
|
|
|
> |
|
|
|
開啟列印視窗 |
|
|
|
</Button> |
|
|
|
<Typography |
|
|
|
variant="body2" |
|
|
|
color="text.secondary" |
|
|
|
sx={{ alignSelf: "center" }} |
|
|
|
> |
|
|
|
掃碼格式:<code>{'{"itemId":16431,"stockInLineId":10381'}</code> |
|
|
|
</Typography> |
|
|
|
</Stack> |
|
|
|
<LotLabelPrintModal |
|
|
|
open={lotLabelModalOpen} |
|
|
|
onClose={() => setLotLabelModalOpen(false)} |
|
|
|
/> |
|
|
|
</Section> |
|
|
|
</TabPanel> |
|
|
|
</Box> |
|
|
|
); |
|
|
|
} |