瀏覽代碼

no message

MergeProblem1
PC-20260115JRSN\Administrator 6 天之前
父節點
當前提交
991cfa72d0
共有 3 個文件被更改,包括 245 次插入14 次删除
  1. +203
    -2
      src/app/(main)/testing/page.tsx
  2. +39
    -0
      src/app/api/bagPrint/actions.ts
  3. +3
    -12
      src/components/BagPrint/BagPrintSearch.tsx

+ 203
- 2
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<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>
);
}

+ 39
- 0
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<NgpclPushResponse> {
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<Error> {
const text = await res.text();


+ 3
- 12
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" });


Loading…
取消
儲存