Ver a proveniência

adding onpack 2nd machine zip download, added DO syn test for single DO code

MergeProblem1
PC-20260115JRSN\Administrator há 5 dias
ascendente
cometimento
548548f453
3 ficheiros alterados com 159 adições e 449 eliminações
  1. +82
    -446
      src/app/(main)/testing/page.tsx
  2. +18
    -0
      src/app/api/bagPrint/actions.ts
  3. +59
    -3
      src/components/BagPrint/BagPrintSearch.tsx

+ 82
- 446
src/app/(main)/testing/page.tsx Ver ficheiro

@@ -1,19 +1,12 @@
"use client";

import React, { useState } from "react";
import {
Box, Grid, Paper, Typography, Button, Dialog, DialogTitle,
DialogContent, DialogActions, TextField, Stack, Table,
TableBody, TableCell, TableContainer, TableHead, TableRow,
Tabs, Tab // ← Added for tabs
} from "@mui/material";
import { FileDownload, Print, SettingsEthernet, Lan, Router } from "@mui/icons-material";
import dayjs from "dayjs";
import { Box, Paper, Typography, Button, TextField, Stack, Tabs, Tab } from "@mui/material";
import { FileDownload } from "@mui/icons-material";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
import * as XLSX from "xlsx";

// Simple TabPanel component for conditional rendering
interface TabPanelProps {
children?: React.ReactNode;
index: number;
@@ -30,192 +23,29 @@ function TabPanel(props: TabPanelProps) {
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
{children}
</Box>
)}
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}

export default function TestingPage() {
// Tab state
const [tabValue, setTabValue] = useState(0);

const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};

// --- 1. TSC Section States ---
const [tscConfig, setTscConfig] = useState({ ip: '192.168.1.100', port: '9100' });
const [tscItems, setTscItems] = useState([
{ id: 1, itemCode: 'FG-001', itemName: 'Yellow Curry Sauce', lotNo: 'LOT-TSC-01', expiryDate: '2025-12-01' },
{ id: 2, itemCode: 'FG-002', itemName: 'Red Curry Paste', lotNo: 'LOT-TSC-02', expiryDate: '2025-12-05' },
]);

// --- 2. DataFlex Section States ---
const [dfConfig, setDfConfig] = useState({ ip: '192.168.1.101', port: '9100' });
const [dfItems, setDfItems] = useState([
{ id: 1, itemCode: 'DF-101', itemName: 'Instant Noodle A', lotNo: 'LOT-DF-01', expiryDate: '2026-01-10' },
{ id: 2, itemCode: 'DF-102', itemName: 'Instant Noodle B', lotNo: 'LOT-DF-02', expiryDate: '2026-01-15' },
]);

// --- 3. OnPack Section States ---
const [isPrinterModalOpen, setIsPrinterModalOpen] = useState(false);
const [printerFormData, setPrinterFormData] = useState({
itemCode: '',
lotNo: '',
expiryDate: dayjs().format('YYYY-MM-DD'),
productName: ''
});

// --- 4. Laser Section States ---
const [laserConfig, setLaserConfig] = useState({ ip: '192.168.1.102', port: '8080' });
const [laserItems, setLaserItems] = useState([
{ id: 1, templateId: 'JOB_001', lotNo: 'L-LASER-01', expiryDate: '2025-12-31', power: '50' },
]);

// --- 5. HANS600S-M Section States ---
const [hansConfig, setHansConfig] = useState({ ip: '192.168.76.10', port: '45678' });
const [hansItems, setHansItems] = useState([
{
id: 1,
textChannel3: 'SN-HANS-001-20260117', // channel 3 (e.g. serial / text1)
textChannel4: 'BATCH-HK-TEST-OK', // channel 4 (e.g. batch / text2)
text3ObjectName: 'Text3', // EZCAD object name for channel 3
text4ObjectName: 'Text4' // EZCAD object name for channel 4
},
]);

// --- 6. GRN Preview (M18) ---
// --- 1. GRN Preview (M18) ---
const [grnPreviewReceiptDate, setGrnPreviewReceiptDate] = useState("2026-03-16");
// --- 7. M18 PO Sync by Code ---
// --- 2. M18 PO Sync by Code ---
const [m18PoCode, setM18PoCode] = useState("");
const [isSyncingM18Po, setIsSyncingM18Po] = useState(false);
const [m18PoSyncResult, setM18PoSyncResult] = useState<string>("");
// --- 3. M18 DO Sync by Code ---
const [m18DoCode, setM18DoCode] = useState("");
const [isSyncingM18Do, setIsSyncingM18Do] = useState(false);
const [m18DoSyncResult, setM18DoSyncResult] = useState<string>("");

// Generic handler for inline table edits
const handleItemChange = (setter: any, id: number, field: string, value: string) => {
setter((prev: any[]) => prev.map(item =>
item.id === id ? { ...item, [field]: value } : item
));
};

// --- API CALLS ---

// TSC Print (Section 1)
const handleTscPrint = async (row: any) => {
const payload = { ...row, printerIp: tscConfig.ip, printerPort: tscConfig.port };
try {
const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-tsc`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (response.status === 401 || response.status === 403) return;
if (response.ok) alert(`TSC Print Command Sent for ${row.itemCode}!`);
else alert("TSC Print Failed");
} catch (e) { console.error("TSC Error:", e); }
};

// DataFlex Print (Section 2)
const handleDfPrint = async (row: any) => {
const payload = { ...row, printerIp: dfConfig.ip, printerPort: dfConfig.port };
try {
const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-dataflex`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (response.status === 401 || response.status === 403) return;
if (response.ok) alert(`DataFlex Print Command Sent for ${row.itemCode}!`);
else alert("DataFlex Print Failed");
} catch (e) { console.error("DataFlex Error:", e); }
};

// OnPack Zip Download (Section 3)
const handleDownloadPrintJob = async () => {
const params = new URLSearchParams(printerFormData);
try {
const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/get-printer6?${params.toString()}`, {
method: 'GET',
});

if (response.status === 401 || response.status === 403) return;
if (!response.ok) throw new Error('Download failed');

const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `${printerFormData.lotNo || 'OnPack'}.zip`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
setIsPrinterModalOpen(false);
} catch (e) { console.error("OnPack Error:", e); }
};

// Laser Print (Section 4 - original)
const handleLaserPrint = async (row: any) => {
const payload = { ...row, printerIp: laserConfig.ip, printerPort: laserConfig.port };
try {
const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (response.status === 401 || response.status === 403) return;
if (response.ok) alert(`Laser Command Sent: ${row.templateId}`);
} catch (e) { console.error(e); }
};

const handleLaserPreview = async (row: any) => {
const payload = { ...row, printerIp: laserConfig.ip, printerPort: parseInt(laserConfig.port) };
try {
const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/preview-laser`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (response.status === 401 || response.status === 403) return;
if (response.ok) alert("Red light preview active!");
} catch (e) { console.error("Preview Error:", e); }
};

// HANS600S-M TCP Print (Section 5)
const handleHansPrint = async (row: any) => {
const payload = {
printerIp: hansConfig.ip,
printerPort: hansConfig.port,
textChannel3: row.textChannel3,
textChannel4: row.textChannel4,
text3ObjectName: row.text3ObjectName,
text4ObjectName: row.text4ObjectName
};
try {
const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser-tcp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (response.status === 401 || response.status === 403) return;
const result = await response.text();
if (response.ok) {
alert(`HANS600S-M Mark Success: ${result}`);
} else {
alert(`HANS600S-M Failed: ${result}`);
}
} catch (e) {
console.error("HANS600S-M Error:", e);
alert("HANS600S-M Connection Error");
}
};

// GRN Preview CSV Download (Section 6)
const handleDownloadGrnPreviewXlsx = async () => {
try {
const response = await clientAuthFetch(
@@ -251,7 +81,6 @@ export default function TestingPage() {
}
};

// M18 PO Sync By Code (Section 7)
const handleSyncM18PoByCode = async () => {
if (!m18PoCode.trim()) {
alert("Please enter PO code.");
@@ -278,258 +107,55 @@ export default function TestingPage() {
}
};

// Layout Helper
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 handleSyncM18DoByCode = async () => {
if (!m18DoCode.trim()) {
alert("Please enter DO / shop PO code.");
return;
}
setIsSyncingM18Do(true);
setM18DoSyncResult("");
try {
const response = await clientAuthFetch(
`${NEXT_PUBLIC_API_URL}/m18/test/do-by-code?code=${encodeURIComponent(m18DoCode.trim())}`,
{ method: "GET" },
);
if (response.status === 401 || response.status === 403) return;
const text = await response.text();
setM18DoSyncResult(text);
if (!response.ok) {
alert(`Sync failed: ${response.status}`);
}
} catch (e) {
console.error("M18 DO Sync By Code Error:", e);
alert("M18 DO sync failed. Check console/network.");
} finally {
setIsSyncingM18Do(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 }}>
{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>
);

return (
<Box sx={{ p: 4 }}>
<Typography variant="h4" sx={{ mb: 4, fontWeight: 'bold' }}>Printer Testing</Typography>
<Tabs value={tabValue} onChange={handleTabChange} aria-label="printer sections tabs" centered variant="fullWidth">
<Tab label="1. TSC" />
<Tab label="2. DataFlex" />
<Tab label="3. OnPack" />
<Tab label="4. Laser" />
<Tab label="5. HANS600S-M" />
<Tab label="6. GRN Preview" />
<Tab label="7. M18 PO Sync" />
<Typography variant="h4" sx={{ mb: 4, fontWeight: "bold" }}>
Testing
</Typography>

<Tabs value={tabValue} onChange={handleTabChange} aria-label="testing sections tabs" centered variant="fullWidth">
<Tab label="1. GRN Preview" />
<Tab label="2. M18 PO Sync" />
<Tab label="3. M18 DO Sync" />
</Tabs>

<TabPanel value={tabValue} index={0}>
<Section title="1. TSC">
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
<TextField size="small" label="Printer IP" value={tscConfig.ip} onChange={e => setTscConfig({...tscConfig, ip: e.target.value})} />
<TextField size="small" label="Port" value={tscConfig.port} onChange={e => setTscConfig({...tscConfig, port: e.target.value})} />
<SettingsEthernet color="action" />
</Stack>
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: 300 }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>Code</TableCell>
<TableCell>Name</TableCell>
<TableCell>Lot</TableCell>
<TableCell>Expiry</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{tscItems.map(row => (
<TableRow key={row.id}>
<TableCell><TextField variant="standard" value={row.itemCode} onChange={e => handleItemChange(setTscItems, row.id, 'itemCode', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" value={row.itemName} onChange={e => handleItemChange(setTscItems, row.id, 'itemName', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" value={row.lotNo} onChange={e => handleItemChange(setTscItems, row.id, 'lotNo', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" type="date" value={row.expiryDate} onChange={e => handleItemChange(setTscItems, row.id, 'expiryDate', e.target.value)} /></TableCell>
<TableCell align="center"><Button variant="contained" size="small" startIcon={<Print />} onClick={() => handleTscPrint(row)}>Print</Button></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Section>
</TabPanel>

<TabPanel value={tabValue} index={1}>
<Section title="2. DataFlex">
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
<TextField size="small" label="Printer IP" value={dfConfig.ip} onChange={e => setDfConfig({...dfConfig, ip: e.target.value})} />
<TextField size="small" label="Port" value={dfConfig.port} onChange={e => setDfConfig({...dfConfig, port: e.target.value})} />
<Lan color="action" />
</Stack>
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: 300 }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>Code</TableCell>
<TableCell>Name</TableCell>
<TableCell>Lot</TableCell>
<TableCell>Expiry</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{dfItems.map(row => (
<TableRow key={row.id}>
<TableCell><TextField variant="standard" value={row.itemCode} onChange={e => handleItemChange(setDfItems, row.id, 'itemCode', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" value={row.itemName} onChange={e => handleItemChange(setDfItems, row.id, 'itemName', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" value={row.lotNo} onChange={e => handleItemChange(setDfItems, row.id, 'lotNo', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" type="date" value={row.expiryDate} onChange={e => handleItemChange(setDfItems, row.id, 'expiryDate', e.target.value)} /></TableCell>
<TableCell align="center"><Button variant="contained" color="secondary" size="small" startIcon={<Print />} onClick={() => handleDfPrint(row)}>Print</Button></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Section>
</TabPanel>

<TabPanel value={tabValue} index={2}>
<Section title="3. OnPack">
<Box sx={{ m: 'auto', textAlign: 'center' }}>
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
Calls /plastic/get-printer6 to generate CoLOS .job bundle.
</Typography>
<Button variant="contained" color="success" size="large" startIcon={<FileDownload />} onClick={() => setIsPrinterModalOpen(true)}>
Generate CoLOS Files
</Button>
</Box>
</Section>
</TabPanel>

<TabPanel value={tabValue} index={3}>
<Section title="4. Laser">
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
<TextField size="small" label="Laser IP" value={laserConfig.ip} onChange={e => setLaserConfig({...laserConfig, ip: e.target.value})} />
<TextField size="small" label="Port" value={laserConfig.port} onChange={e => setLaserConfig({...laserConfig, port: e.target.value})} />
<Router color="action" />
</Stack>
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: 300 }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>Template</TableCell>
<TableCell>Lot</TableCell>
<TableCell>Exp</TableCell>
<TableCell>Pwr%</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{laserItems.map(row => (
<TableRow key={row.id}>
<TableCell><TextField variant="standard" value={row.templateId} onChange={e => handleItemChange(setLaserItems, row.id, 'templateId', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" value={row.lotNo} onChange={e => handleItemChange(setLaserItems, row.id, 'lotNo', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" type="date" value={row.expiryDate} onChange={e => handleItemChange(setLaserItems, row.id, 'expiryDate', e.target.value)} /></TableCell>
<TableCell><TextField variant="standard" value={row.power} sx={{ width: 40 }} onChange={e => handleItemChange(setLaserItems, row.id, 'power', e.target.value)} /></TableCell>
<TableCell align="center">
<Stack direction="row" spacing={1} justifyContent="center">
<Button
variant="outlined"
color="info"
size="small"
onClick={() => handleLaserPreview(row)}
>
Preview
</Button>
<Button
variant="contained"
color="warning"
size="small"
startIcon={<Print />}
onClick={() => handleLaserPrint(row)}
>
Mark
</Button>
</Stack>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Typography variant="caption" sx={{ mt: 2, display: 'block', color: 'text.secondary' }}>
Note: HANS Laser requires pre-saved templates on the controller.
</Typography>
</Section>
</TabPanel>

<TabPanel value={tabValue} index={4}>
<Section title="5. HANS600S-M">
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
<TextField
size="small"
label="Laser IP"
value={hansConfig.ip}
onChange={e => setHansConfig({...hansConfig, ip: e.target.value})}
/>
<TextField
size="small"
label="Port"
value={hansConfig.port}
onChange={e => setHansConfig({...hansConfig, port: e.target.value})}
/>
<Router color="action" sx={{ ml: 'auto' }} />
</Stack>
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: 300 }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>Ch3 Text (SN)</TableCell>
<TableCell>Ch4 Text (Batch)</TableCell>
<TableCell>Obj3 Name</TableCell>
<TableCell>Obj4 Name</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{hansItems.map(row => (
<TableRow key={row.id}>
<TableCell>
<TextField
variant="standard"
value={row.textChannel3}
onChange={e => handleItemChange(setHansItems, row.id, 'textChannel3', e.target.value)}
sx={{ minWidth: 180 }}
/>
</TableCell>
<TableCell>
<TextField
variant="standard"
value={row.textChannel4}
onChange={e => handleItemChange(setHansItems, row.id, 'textChannel4', e.target.value)}
sx={{ minWidth: 140 }}
/>
</TableCell>
<TableCell>
<TextField
variant="standard"
value={row.text3ObjectName}
onChange={e => handleItemChange(setHansItems, row.id, 'text3ObjectName', e.target.value)}
size="small"
/>
</TableCell>
<TableCell>
<TextField
variant="standard"
value={row.text4ObjectName}
onChange={e => handleItemChange(setHansItems, row.id, 'text4ObjectName', e.target.value)}
size="small"
/>
</TableCell>
<TableCell align="center">
<Button
variant="contained"
color="error"
size="small"
startIcon={<Print />}
onClick={() => handleHansPrint(row)}
sx={{ minWidth: 80 }}
>
TCP Mark
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Typography variant="caption" sx={{ mt: 2, display: 'block', color: 'text.secondary', fontSize: '0.75rem' }}>
TCP Push to EZCAD3 (Ch3/Ch4 via E3_SetTextObject) | IP:192.168.76.10:45678 | Backend: /print-laser-tcp
</Typography>
</Section>
</TabPanel>

<TabPanel value={tabValue} index={5}>
<Section title="6. GRN Preview (M18)">
<Section title="1. GRN Preview (M18)">
<Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}>
<TextField
size="small"
@@ -555,8 +181,8 @@ export default function TestingPage() {
</Section>
</TabPanel>

<TabPanel value={tabValue} index={6}>
<Section title="7. M18 PO Sync by Code">
<TabPanel value={tabValue} index={1}>
<Section title="2. M18 PO Sync by Code">
<Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}>
<TextField
size="small"
@@ -566,12 +192,7 @@ export default function TestingPage() {
placeholder="e.g. PFP002PO26030341"
sx={{ minWidth: 320 }}
/>
<Button
variant="contained"
color="primary"
onClick={handleSyncM18PoByCode}
disabled={isSyncingM18Po}
>
<Button variant="contained" color="primary" onClick={handleSyncM18PoByCode} disabled={isSyncingM18Po}>
{isSyncingM18Po ? "Syncing..." : "Sync PO from M18"}
</Button>
</Stack>
@@ -592,22 +213,37 @@ export default function TestingPage() {
</Section>
</TabPanel>

{/* Dialog for OnPack */}
<Dialog open={isPrinterModalOpen} onClose={() => setIsPrinterModalOpen(false)} fullWidth maxWidth="sm">
<DialogTitle sx={{ bgcolor: 'success.main', color: 'white' }}>OnPack Printer Job Details</DialogTitle>
<DialogContent sx={{ mt: 2 }}>
<Stack spacing={3}>
<TextField label="Item Code" fullWidth value={printerFormData.itemCode} onChange={(e) => setPrinterFormData({ ...printerFormData, itemCode: e.target.value })} />
<TextField label="Lot Number" fullWidth value={printerFormData.lotNo} onChange={(e) => setPrinterFormData({ ...printerFormData, lotNo: e.target.value })} />
<TextField label="Product Name" fullWidth value={printerFormData.productName} onChange={(e) => setPrinterFormData({ ...printerFormData, productName: e.target.value })} />
<TextField label="Expiry Date" type="date" fullWidth InputLabelProps={{ shrink: true }} value={printerFormData.expiryDate} onChange={(e) => setPrinterFormData({ ...printerFormData, expiryDate: e.target.value })} />
<TabPanel value={tabValue} index={2}>
<Section title="3. M18 DO Sync by Code">
<Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}>
<TextField
size="small"
label="DO / Shop PO Code"
value={m18DoCode}
onChange={(e) => setM18DoCode(e.target.value)}
placeholder="e.g. same document code as M18 shop PO"
sx={{ minWidth: 320 }}
/>
<Button variant="contained" color="primary" onClick={handleSyncM18DoByCode} disabled={isSyncingM18Do}>
{isSyncingM18Do ? "Syncing..." : "Sync DO from M18"}
</Button>
</Stack>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button onClick={() => setIsPrinterModalOpen(false)} variant="outlined" color="inherit">Cancel</Button>
<Button variant="contained" color="success" onClick={handleDownloadPrintJob}>Generate & Download</Button>
</DialogActions>
</Dialog>
<Typography variant="body2" color="textSecondary">
Backend endpoint: <code>/m18/test/do-by-code?code=YOUR_CODE</code>
</Typography>
{m18DoSyncResult ? (
<TextField
fullWidth
multiline
minRows={4}
margin="normal"
label="Sync Result"
value={m18DoSyncResult}
InputProps={{ readOnly: true }}
/>
) : null}
</Section>
</TabPanel>
</Box>
);
}
}

+ 18
- 0
src/app/api/bagPrint/actions.ts Ver ficheiro

@@ -80,3 +80,21 @@ export async function downloadOnPackQrZip(

return res.blob();
}

/** OnPack2023 檸檬機 — text QR template (`onpack2030_2`), no separate .bmp */
export async function downloadOnPackTextQrZip(
request: OnPackQrDownloadRequest,
): Promise<Blob> {
const url = `${NEXT_PUBLIC_API_URL}/plastic/download-onpack-qr-text`;
const res = await clientAuthFetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});

if (!res.ok) {
throw new Error((await res.text()) || "Download failed");
}

return res.blob();
}

+ 59
- 3
src/components/BagPrint/BagPrintSearch.tsx Ver ficheiro

@@ -25,7 +25,13 @@ import ChevronRight from "@mui/icons-material/ChevronRight";
import Settings from "@mui/icons-material/Settings";
import Print from "@mui/icons-material/Print";
import Download from "@mui/icons-material/Download";
import { checkPrinterStatus, downloadOnPackQrZip, fetchJobOrders, JobOrderListItem } from "@/app/api/bagPrint/actions";
import {
checkPrinterStatus,
downloadOnPackQrZip,
downloadOnPackTextQrZip,
fetchJobOrders,
JobOrderListItem,
} from "@/app/api/bagPrint/actions";
import dayjs from "dayjs";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
@@ -107,6 +113,7 @@ const BagPrintSearch: React.FC = () => {
const [printerConnected, setPrinterConnected] = useState(false);
const [printerMessage, setPrinterMessage] = useState("列印機未連接");
const [downloadingOnPack, setDownloadingOnPack] = useState(false);
const [downloadingOnPackText, setDownloadingOnPackText] = useState(false);

useEffect(() => {
setSettings(loadSettings());
@@ -306,6 +313,46 @@ 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);

if (onPackJobOrders.length === 0) {
setSnackbar({ open: true, message: "當日沒有可下載的 job order", severity: "error" });
return;
}

setDownloadingOnPackText(true);
try {
const blob = await downloadOnPackTextQrZip({
jobOrders: onPackJobOrders,
});

const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", `onpack2023_lemon_qr_${planDate}.zip`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);

setSnackbar({ open: true, message: "OnPack2023檸檬機 ZIP 已下載", severity: "success" });
} catch (e) {
setSnackbar({
open: true,
message: e instanceof Error ? e.message : "下載 OnPack2023檸檬機 失敗",
severity: "error",
});
} finally {
setDownloadingOnPackText(false);
}
};

return (
<Box sx={{ minHeight: "70vh", display: "flex", flexDirection: "column" }}>
{/* Top: date nav + printer + settings */}
@@ -360,15 +407,24 @@ const BagPrintSearch: React.FC = () => {
<Typography variant="body2" sx={{ mt: 1, color: "text.secondary" }}>
{printerMessage}
</Typography>
<Stack direction="row" sx={{ mt: 2 }}>
<Stack direction="row" sx={{ mt: 2 }} spacing={2} flexWrap="wrap" useFlexGap>
<Button
variant="contained"
startIcon={<Download />}
onClick={handleDownloadOnPackQr}
disabled={loading || downloadingOnPack || jobOrders.length === 0}
disabled={loading || downloadingOnPack || downloadingOnPackText || jobOrders.length === 0}
>
{downloadingOnPack ? "下載中..." : "下載 OnPack 汁水機 QR code"}
</Button>
<Button
variant="contained"
color="secondary"
startIcon={<Download />}
onClick={handleDownloadOnPackTextQr}
disabled={loading || downloadingOnPack || downloadingOnPackText || jobOrders.length === 0}
>
{downloadingOnPackText ? "下載中..." : "下載 OnPack2023檸檬機"}
</Button>
</Stack>
</Paper>



Carregando…
Cancelar
Guardar