diff --git a/src/app/(main)/testing/page.tsx b/src/app/(main)/testing/page.tsx index c6bc427..e8458b2 100644 --- a/src/app/(main)/testing/page.tsx +++ b/src/app/(main)/testing/page.tsx @@ -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(""); + // --- 4. OnPack NGPCL (same job-order → ZIP logic as /bagPrint) --- + const [onpackPlanDate, setOnpackPlanDate] = useState(() => dayjs().format("YYYY-MM-DD")); + const [onpackJobOrders, setOnpackJobOrders] = useState([]); + const [onpackLoading, setOnpackLoading] = useState(false); + const [onpackLoadError, setOnpackLoadError] = useState(null); + const [onpackLemonDownloading, setOnpackLemonDownloading] = useState(false); + const [onpackPushLoading, setOnpackPushLoading] = useState(false); + const [onpackPushResult, setOnpackPushResult] = useState(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 }) => ( @@ -152,6 +256,7 @@ export default function TestingPage() { + @@ -244,6 +349,102 @@ export default function TestingPage() { ) : null} + + +
+ + Uses GET /py/job-orders?planStart= for the day, then the same jobOrders payload as{" "} + Bag Print → 下載 OnPack2023檸檬機. The ZIP contains loose .job / .image / BMPs — extract + before sending to NGE; the ZIP itself is only a transport bundle. + + + Distinct item codes in the list produce one label set each (backend groups by code). Configure ngpcl.push-url on the server + to POST the same lemon ZIP bytes to your NGPCL HTTP gateway; otherwise use download only. + + + + setOnpackPlanDate(e.target.value)} + InputLabelProps={{ shrink: true }} + /> + + {onpackLoading ? ( + <> + + Loading job orders… + + ) : ( + `${onpackJobOrders.length} job order(s), ${onpackPayload.length} row(s) with item code → ZIP` + )} + + + {onpackLoadError ? ( + + {onpackLoadError} + + ) : null} + + + + + JO id + Code + Item code + Lot + + + + {onpackJobOrders.length === 0 && !onpackLoading ? ( + + + + No rows for this date (or still loading). + + + + ) : ( + onpackJobOrders.map((jo) => ( + + {jo.id} + {jo.code ?? "—"} + {jo.itemCode ?? "—"} + {jo.lotNo ?? "—"} + + )) + )} + +
+ + + + + + + + {onpackPushResult ? ( + + ) : null} + + POST /plastic/download-onpack-qr-text · POST /plastic/ngpcl/push-onpack-qr-text (same body) + +
+
); } diff --git a/src/app/api/bagPrint/actions.ts b/src/app/api/bagPrint/actions.ts index c1ddd11..3749ee3 100644 --- a/src/app/api/bagPrint/actions.ts +++ b/src/app/api/bagPrint/actions.ts @@ -39,6 +39,45 @@ export interface OnPackQrDownloadRequest { }[]; } +/** Same mapping as Bag Print download buttons: one entry per row with a non-empty item code. */ +export function buildOnPackJobOrdersPayload(jobOrders: JobOrderListItem[]): { + jobOrderId: number; + itemCode: string; +}[] { + return jobOrders + .map((jobOrder) => ({ + jobOrderId: jobOrder.id, + itemCode: jobOrder.itemCode?.trim() || "", + })) + .filter((jobOrder) => jobOrder.itemCode.length > 0); +} + +export interface NgpclPushResponse { + pushed: boolean; + message: string; +} + +/** + * POST the same lemon OnPack ZIP bytes as download-onpack-qr-text to the server-configured NGPCL HTTP endpoint (ngpcl.push-url). + * When the URL is not configured, response has pushed=false — use download ZIP instead. + */ +export async function pushOnPackTextQrZipToNgpcl(request: OnPackQrDownloadRequest): Promise { + const url = `${NEXT_PUBLIC_API_URL}/plastic/ngpcl/push-onpack-qr-text`; + const res = await clientAuthFetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); + if (res.status === 401 || res.status === 403) { + return { pushed: false, message: "Session expired or unauthorized." }; + } + const data = (await res.json()) as NgpclPushResponse; + if (!res.ok) { + throw new Error(data.message || `HTTP ${res.status}`); + } + return data; +} + /** Readable message when ZIP download returns non-OK (plain text, JSON error body, or generic). */ async function zipDownloadError(res: Response): Promise { const text = await res.text(); diff --git a/src/components/BagPrint/BagPrintSearch.tsx b/src/components/BagPrint/BagPrintSearch.tsx index 8b63294..63e7579 100644 --- a/src/components/BagPrint/BagPrintSearch.tsx +++ b/src/components/BagPrint/BagPrintSearch.tsx @@ -26,6 +26,7 @@ import Settings from "@mui/icons-material/Settings"; import Print from "@mui/icons-material/Print"; import Download from "@mui/icons-material/Download"; import { + buildOnPackJobOrdersPayload, checkPrinterStatus, downloadOnPackQrZip, downloadOnPackTextQrZip, @@ -274,12 +275,7 @@ const BagPrintSearch: React.FC = () => { }; const handleDownloadOnPackQr = async () => { - const onPackJobOrders = jobOrders - .map((jobOrder) => ({ - jobOrderId: jobOrder.id, - itemCode: jobOrder.itemCode?.trim() || "", - })) - .filter((jobOrder) => jobOrder.itemCode.length > 0); + const onPackJobOrders = buildOnPackJobOrdersPayload(jobOrders); if (onPackJobOrders.length === 0) { setSnackbar({ open: true, message: "當日沒有可下載的 job order", severity: "error" }); @@ -314,12 +310,7 @@ const BagPrintSearch: React.FC = () => { }; const handleDownloadOnPackTextQr = async () => { - const onPackJobOrders = jobOrders - .map((jobOrder) => ({ - jobOrderId: jobOrder.id, - itemCode: jobOrder.itemCode?.trim() || "", - })) - .filter((jobOrder) => jobOrder.itemCode.length > 0); + const onPackJobOrders = buildOnPackJobOrdersPayload(jobOrders); if (onPackJobOrders.length === 0) { setSnackbar({ open: true, message: "當日沒有可下載的 job order", severity: "error" });