"use client"; import React, { useCallback, useEffect, useState } from "react"; import { Box, Button, FormControl, InputLabel, MenuItem, Select, Stack, Typography, Paper, CircularProgress, SelectChangeEvent, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Snackbar, } from "@mui/material"; import ChevronLeft from "@mui/icons-material/ChevronLeft"; 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, 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"; // Light blue theme (matching Python Bag1) const BG_TOP = "#E8F4FC"; const BG_LIST = "#D4E8F7"; const BG_ROW = "#C5E1F5"; const BG_ROW_SELECTED = "#6BB5FF"; const BG_STATUS_ERROR = "#FFCCCB"; const BG_STATUS_OK = "#90EE90"; const FG_STATUS_ERROR = "#B22222"; const FG_STATUS_OK = "#006400"; const PRINTER_OPTIONS = [ { value: "dataflex", label: "打袋機 DataFlex" }, { value: "laser", label: "激光機" }, ]; const REFRESH_MS = 60 * 1000; const PRINTER_CHECK_MS = 60 * 1000; const PRINTER_RETRY_MS = 30 * 1000; const SETTINGS_KEY = "bagPrint_settings"; const DEFAULT_SETTINGS = { dabag_ip: "", dabag_port: "3008", laser_ip: "192.168.17.10", laser_port: "45678", }; function loadSettings(): typeof DEFAULT_SETTINGS { if (typeof window === "undefined") return DEFAULT_SETTINGS; try { const s = localStorage.getItem(SETTINGS_KEY); if (s) return { ...DEFAULT_SETTINGS, ...JSON.parse(s) }; } catch {} return DEFAULT_SETTINGS; } function saveSettings(s: typeof DEFAULT_SETTINGS) { if (typeof window === "undefined") return; try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(s)); } catch {} } function formatQty(val: number | null | undefined): string { if (val == null) return "—"; try { const n = Number(val); if (Number.isInteger(n)) return n.toLocaleString(); return n.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 }).replace(/\.?0+$/, ""); } catch { return String(val); } } function getBatch(jo: JobOrderListItem): string { return (jo.lotNo || "—").trim() || "—"; } const BagPrintSearch: React.FC = () => { const [planDate, setPlanDate] = useState(() => dayjs().format("YYYY-MM-DD")); const [jobOrders, setJobOrders] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [connected, setConnected] = useState(false); const [printer, setPrinter] = useState("dataflex"); const [selectedId, setSelectedId] = useState(null); const [printDialogOpen, setPrintDialogOpen] = useState(false); const [printTarget, setPrintTarget] = useState(null); const [printCount, setPrintCount] = useState(0); const [printContinuous, setPrintContinuous] = useState(false); const [printing, setPrinting] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity?: "success" | "info" | "error" }>({ open: false, message: "" }); const [settings, setSettings] = useState(DEFAULT_SETTINGS); const [printerConnected, setPrinterConnected] = useState(false); const [printerMessage, setPrinterMessage] = useState("列印機未連接"); const [downloadingOnPack, setDownloadingOnPack] = useState(false); const [downloadingOnPackText, setDownloadingOnPackText] = useState(false); useEffect(() => { setSettings(loadSettings()); }, []); const loadJobOrders = useCallback(async (fromUserChange = false) => { setLoading(true); setError(null); try { const data = await fetchJobOrders(planDate); setJobOrders(data); setConnected(true); if (fromUserChange) setSelectedId(null); } catch (e) { setError(e instanceof Error ? e.message : "連接不到服務器"); setConnected(false); setJobOrders([]); } finally { setLoading(false); } }, [planDate]); useEffect(() => { loadJobOrders(true); }, [planDate]); useEffect(() => { if (!connected) return; const id = setInterval(() => loadJobOrders(false), REFRESH_MS); return () => clearInterval(id); }, [connected, loadJobOrders]); const checkCurrentPrinter = useCallback(async () => { try { const request = printer === "dataflex" ? { printerType: "dataflex" as const, printerIp: settings.dabag_ip, printerPort: Number(settings.dabag_port || 3008), } : { printerType: "laser" as const, printerIp: settings.laser_ip, printerPort: Number(settings.laser_port || 45678), }; const result = await checkPrinterStatus(request); setPrinterConnected(result.connected); setPrinterMessage(result.message); } catch (e) { setPrinterConnected(false); setPrinterMessage(e instanceof Error ? e.message : "列印機狀態檢查失敗"); } }, [printer, settings]); useEffect(() => { checkCurrentPrinter(); }, [checkCurrentPrinter]); useEffect(() => { const intervalMs = printerConnected ? PRINTER_CHECK_MS : PRINTER_RETRY_MS; const id = setInterval(() => { checkCurrentPrinter(); }, intervalMs); return () => clearInterval(id); }, [printerConnected, checkCurrentPrinter]); const goPrevDay = () => { setPlanDate((d) => dayjs(d).subtract(1, "day").format("YYYY-MM-DD")); }; const goNextDay = () => { setPlanDate((d) => dayjs(d).add(1, "day").format("YYYY-MM-DD")); }; const handlePrinterChange = (e: SelectChangeEvent) => { setPrinter(e.target.value); }; const handleRowClick = (jo: JobOrderListItem) => { setSelectedId(jo.id); const batch = getBatch(jo); const itemCode = jo.itemCode || "—"; const itemName = jo.itemName || "—"; setSnackbar({ open: true, message: `已點選:批次 ${batch} 品號 ${itemCode} ${itemName}`, severity: "info" }); // Align with Bag2.py "click row -> ask bag count -> print" for DataFlex. if (printer === "dataflex") { setPrintTarget(jo); setPrintCount(0); setPrintContinuous(false); setPrintDialogOpen(true); } }; const confirmPrintDataFlex = async () => { if (!printTarget) return; if (printer !== "dataflex") { setSnackbar({ open: true, message: "此頁目前只支援打袋機 DataFlex 列印", severity: "error" }); return; } if (!printContinuous && printCount < 1) { setSnackbar({ open: true, message: "請先按 +50、+10、+5 或 +1 選擇數量。", severity: "error" }); return; } const qty = printContinuous ? -1 : printCount; const printerIp = settings.dabag_ip; const printerPort = Number(settings.dabag_port || 3008); if (!printerIp) { setSnackbar({ open: true, message: "請先在設定中填寫打袋機 DataFlex 的 IP。", severity: "error" }); return; } setPrinting(true); try { const resp = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-dataflex`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ itemCode: printTarget.itemCode || "—", itemName: printTarget.itemName || "—", lotNo: printTarget.lotNo || "—", // DataFlex zpl (Bag2.py) only needs itemId + stockInLineId for QR payload (optional). itemId: printTarget.itemId, stockInLineId: printTarget.stockInLineId, printerIp, printerPort, printQty: qty, }), }); if (resp.status === 401 || resp.status === 403) return; if (!resp.ok) { const msg = await resp.text().catch(() => ""); setSnackbar({ open: true, message: `DataFlex 列印失敗(狀態碼 ${resp.status})。${msg ? msg.slice(0, 120) : ""}`, severity: "error", }); return; } const batch = getBatch(printTarget); const printedText = qty === -1 ? "連續 (C)" : `${qty}`; setSnackbar({ open: true, message: `已送出列印:批次 ${batch} x ${printedText}`, severity: "success" }); setPrintDialogOpen(false); } catch (e) { setSnackbar({ open: true, message: e instanceof Error ? e.message : "DataFlex 列印失敗", severity: "error" }); } finally { setPrinting(false); } }; const handleDownloadOnPackQr = 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; } setDownloadingOnPack(true); try { const blob = await downloadOnPackQrZip({ jobOrders: onPackJobOrders, }); const url = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.setAttribute("download", `onpack_qr_${planDate}.zip`); document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url); setSnackbar({ open: true, message: "OnPack QR code ZIP 已下載", severity: "success" }); } catch (e) { setSnackbar({ open: true, message: e instanceof Error ? e.message : "下載 OnPack QR code 失敗", severity: "error", }); } finally { setDownloadingOnPack(false); } }; 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 ( {/* Top: date nav + printer + settings */} setPlanDate(e.target.value)} size="small" sx={{ width: 160 }} InputLabelProps={{ shrink: true }} /> 列印機: 列印機 {printerMessage} {/* Job orders list */} {loading ? ( ) : jobOrders.length === 0 ? ( 當日無工單 ) : ( {jobOrders.map((jo) => { const batch = getBatch(jo); const qtyStr = formatQty(jo.reqQty); const isSelected = selectedId === jo.id; return ( handleRowClick(jo)} > {batch} {qtyStr !== "—" && ( 數量:{qtyStr} )} {jo.code || "—"} {jo.itemCode || "—"} {jo.itemName || "—"} ); })} )} {/* Print count dialog (DataFlex) */} (printing ? null : setPrintDialogOpen(false))} maxWidth="xs" fullWidth> 打袋機 DataFlex 列印數量 列印多少個袋? {printContinuous ? "連續 (C)" : `數量: ${printCount}`} {/* Settings dialog */} setSettingsOpen(false)} maxWidth="sm" fullWidth> 設定 打袋機 DataFlex setSettings((s) => ({ ...s, dabag_ip: e.target.value }))} fullWidth /> setSettings((s) => ({ ...s, dabag_port: e.target.value }))} fullWidth /> 激光機 setSettings((s) => ({ ...s, laser_ip: e.target.value }))} fullWidth /> setSettings((s) => ({ ...s, laser_port: e.target.value }))} fullWidth /> setSnackbar((s) => ({ ...s, open: false }))} message={snackbar.message} anchorOrigin={{ vertical: "bottom", horizontal: "center" }} /> ); }; export default BagPrintSearch;