|
|
|
@@ -1,10 +1,34 @@ |
|
|
|
"use client"; |
|
|
|
|
|
|
|
import React, { useState } from "react"; |
|
|
|
import { Box, Paper, Typography, Button, TextField, Stack, Tabs, Tab } from "@mui/material"; |
|
|
|
import React, { useEffect, useMemo, useState } from "react"; |
|
|
|
import { |
|
|
|
Alert, |
|
|
|
Box, |
|
|
|
Button, |
|
|
|
CircularProgress, |
|
|
|
Paper, |
|
|
|
Stack, |
|
|
|
Tab, |
|
|
|
Table, |
|
|
|
TableBody, |
|
|
|
TableCell, |
|
|
|
TableHead, |
|
|
|
TableRow, |
|
|
|
Tabs, |
|
|
|
TextField, |
|
|
|
Typography, |
|
|
|
} from "@mui/material"; |
|
|
|
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 { |
|
|
|
buildOnPackJobOrdersPayload, |
|
|
|
downloadOnPackTextQrZip, |
|
|
|
fetchJobOrders, |
|
|
|
pushOnPackTextQrZipToNgpcl, |
|
|
|
type JobOrderListItem, |
|
|
|
} from "@/app/api/bagPrint/actions"; |
|
|
|
import * as XLSX from "xlsx"; |
|
|
|
|
|
|
|
interface TabPanelProps { |
|
|
|
@@ -45,6 +69,39 @@ export default function TestingPage() { |
|
|
|
const [m18DoCode, setM18DoCode] = useState(""); |
|
|
|
const [isSyncingM18Do, setIsSyncingM18Do] = useState(false); |
|
|
|
const [m18DoSyncResult, setM18DoSyncResult] = useState<string>(""); |
|
|
|
// --- 4. OnPack NGPCL (same job-order → ZIP logic as /bagPrint) --- |
|
|
|
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); |
|
|
|
|
|
|
|
const onpackPayload = useMemo(() => buildOnPackJobOrdersPayload(onpackJobOrders), [onpackJobOrders]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (tabValue !== 3) return; |
|
|
|
let cancelled = false; |
|
|
|
(async () => { |
|
|
|
setOnpackLoading(true); |
|
|
|
setOnpackLoadError(null); |
|
|
|
try { |
|
|
|
const data = await fetchJobOrders(onpackPlanDate); |
|
|
|
if (!cancelled) setOnpackJobOrders(data); |
|
|
|
} catch (e) { |
|
|
|
if (!cancelled) { |
|
|
|
setOnpackLoadError(e instanceof Error ? e.message : "Failed to load job orders"); |
|
|
|
setOnpackJobOrders([]); |
|
|
|
} |
|
|
|
} finally { |
|
|
|
if (!cancelled) setOnpackLoading(false); |
|
|
|
} |
|
|
|
})(); |
|
|
|
return () => { |
|
|
|
cancelled = true; |
|
|
|
}; |
|
|
|
}, [tabValue, onpackPlanDate]); |
|
|
|
|
|
|
|
const handleDownloadGrnPreviewXlsx = async () => { |
|
|
|
try { |
|
|
|
@@ -133,6 +190,53 @@ export default function TestingPage() { |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const downloadBlob = (blob: Blob, filename: string) => { |
|
|
|
const url = window.URL.createObjectURL(blob); |
|
|
|
const link = document.createElement("a"); |
|
|
|
link.href = url; |
|
|
|
link.setAttribute("download", filename); |
|
|
|
document.body.appendChild(link); |
|
|
|
link.click(); |
|
|
|
link.remove(); |
|
|
|
window.URL.revokeObjectURL(url); |
|
|
|
}; |
|
|
|
|
|
|
|
const handleOnpackDownloadLemonZip = async () => { |
|
|
|
if (onpackPayload.length === 0) { |
|
|
|
alert("No job orders with item code for this plan date (same rule as Bag Print)."); |
|
|
|
return; |
|
|
|
} |
|
|
|
setOnpackLemonDownloading(true); |
|
|
|
try { |
|
|
|
const blob = await downloadOnPackTextQrZip({ jobOrders: onpackPayload }); |
|
|
|
downloadBlob(blob, `onpack2023_lemon_qr_${onpackPlanDate}.zip`); |
|
|
|
} catch (e) { |
|
|
|
console.error("Lemon OnPack ZIP download error:", e); |
|
|
|
alert(e instanceof Error ? e.message : "Lemon OnPack ZIP failed"); |
|
|
|
} finally { |
|
|
|
setOnpackLemonDownloading(false); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const handleOnpackPushNgpcl = async () => { |
|
|
|
if (onpackPayload.length === 0) { |
|
|
|
alert("No job orders with item code for this plan date."); |
|
|
|
return; |
|
|
|
} |
|
|
|
setOnpackPushLoading(true); |
|
|
|
setOnpackPushResult(null); |
|
|
|
try { |
|
|
|
const r = await pushOnPackTextQrZipToNgpcl({ jobOrders: onpackPayload }); |
|
|
|
setOnpackPushResult(`${r.pushed ? "Pushed" : "Not pushed"}: ${r.message}`); |
|
|
|
} catch (e) { |
|
|
|
const msg = e instanceof Error ? e.message : String(e); |
|
|
|
setOnpackPushResult(`Error: ${msg}`); |
|
|
|
alert(msg); |
|
|
|
} finally { |
|
|
|
setOnpackPushLoading(false); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
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 }}> |
|
|
|
@@ -152,6 +256,7 @@ export default function TestingPage() { |
|
|
|
<Tab label="1. GRN Preview" /> |
|
|
|
<Tab label="2. M18 PO Sync" /> |
|
|
|
<Tab label="3. M18 DO Sync" /> |
|
|
|
<Tab label="4. OnPack NGPCL" /> |
|
|
|
</Tabs> |
|
|
|
|
|
|
|
<TabPanel value={tabValue} index={0}> |
|
|
|
@@ -244,6 +349,102 @@ export default function TestingPage() { |
|
|
|
) : null} |
|
|
|
</Section> |
|
|
|
</TabPanel> |
|
|
|
|
|
|
|
<TabPanel value={tabValue} index={3}> |
|
|
|
<Section title="4. 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 |
|
|
|
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. |
|
|
|
</Typography> |
|
|
|
|
|
|
|
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} sx={{ mb: 2, alignItems: "center", flexWrap: "wrap" }}> |
|
|
|
<TextField |
|
|
|
size="small" |
|
|
|
label="Plan date (planStart)" |
|
|
|
type="date" |
|
|
|
value={onpackPlanDate} |
|
|
|
onChange={(e) => setOnpackPlanDate(e.target.value)} |
|
|
|
InputLabelProps={{ shrink: true }} |
|
|
|
/> |
|
|
|
<Typography variant="body2" color="textSecondary"> |
|
|
|
{onpackLoading ? ( |
|
|
|
<> |
|
|
|
<CircularProgress size={16} sx={{ mr: 1, verticalAlign: "middle" }} /> |
|
|
|
Loading job orders… |
|
|
|
</> |
|
|
|
) : ( |
|
|
|
`${onpackJobOrders.length} job order(s), ${onpackPayload.length} row(s) with item code → ZIP` |
|
|
|
)} |
|
|
|
</Typography> |
|
|
|
</Stack> |
|
|
|
{onpackLoadError ? ( |
|
|
|
<Alert severity="error" sx={{ mb: 2 }}> |
|
|
|
{onpackLoadError} |
|
|
|
</Alert> |
|
|
|
) : null} |
|
|
|
|
|
|
|
<Table size="small" sx={{ mb: 2, maxWidth: 900 }}> |
|
|
|
<TableHead> |
|
|
|
<TableRow> |
|
|
|
<TableCell>JO id</TableCell> |
|
|
|
<TableCell>Code</TableCell> |
|
|
|
<TableCell>Item code</TableCell> |
|
|
|
<TableCell>Lot</TableCell> |
|
|
|
</TableRow> |
|
|
|
</TableHead> |
|
|
|
<TableBody> |
|
|
|
{onpackJobOrders.length === 0 && !onpackLoading ? ( |
|
|
|
<TableRow> |
|
|
|
<TableCell colSpan={4}> |
|
|
|
<Typography variant="body2" color="textSecondary"> |
|
|
|
No rows for this date (or still loading). |
|
|
|
</Typography> |
|
|
|
</TableCell> |
|
|
|
</TableRow> |
|
|
|
) : ( |
|
|
|
onpackJobOrders.map((jo) => ( |
|
|
|
<TableRow key={jo.id}> |
|
|
|
<TableCell>{jo.id}</TableCell> |
|
|
|
<TableCell>{jo.code ?? "—"}</TableCell> |
|
|
|
<TableCell>{jo.itemCode ?? "—"}</TableCell> |
|
|
|
<TableCell>{jo.lotNo ?? "—"}</TableCell> |
|
|
|
</TableRow> |
|
|
|
)) |
|
|
|
)} |
|
|
|
</TableBody> |
|
|
|
</Table> |
|
|
|
|
|
|
|
<TextField |
|
|
|
fullWidth |
|
|
|
multiline |
|
|
|
minRows={3} |
|
|
|
label="Resolved POST body (download-onpack-qr-text / NGPCL push)" |
|
|
|
value={JSON.stringify({ jobOrders: onpackPayload }, null, 2)} |
|
|
|
InputProps={{ readOnly: true }} |
|
|
|
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"} |
|
|
|
</Button> |
|
|
|
<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 }} /> |
|
|
|
) : 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) |
|
|
|
</Typography> |
|
|
|
</Section> |
|
|
|
</TabPanel> |
|
|
|
</Box> |
|
|
|
); |
|
|
|
} |