|
|
|
@@ -1,9 +1,10 @@ |
|
|
|
"use client"; |
|
|
|
|
|
|
|
import React, { useCallback, useEffect, useState } from "react"; |
|
|
|
import React, { useCallback, useEffect, useRef, useState } from "react"; |
|
|
|
import { |
|
|
|
Box, |
|
|
|
Button, |
|
|
|
Chip, |
|
|
|
FormControl, |
|
|
|
InputLabel, |
|
|
|
MenuItem, |
|
|
|
@@ -29,11 +30,22 @@ import { |
|
|
|
buildOnPackJobOrdersPayload, |
|
|
|
checkPrinterStatus, |
|
|
|
downloadOnPackQrZip, |
|
|
|
downloadOnPackQrZipWithExpiry, |
|
|
|
downloadOnPackTextQrZip, |
|
|
|
downloadOnPackTextQrZipWithExpiry, |
|
|
|
deleteOnPackTemplate, |
|
|
|
fetchJobOrders, |
|
|
|
fetchOnPackSupportedCatalog, |
|
|
|
listOnPackTemplates, |
|
|
|
uploadOnPackTemplates, |
|
|
|
JobOrderListItem, |
|
|
|
OnPackMachine, |
|
|
|
OnPackSupportedCatalogDto, |
|
|
|
OnPackTemplateFileDto, |
|
|
|
} from "@/app/api/bagPrint/actions"; |
|
|
|
import dayjs from "dayjs"; |
|
|
|
import { useSession } from "next-auth/react"; |
|
|
|
import { SessionWithTokens } from "@/config/authConfig"; |
|
|
|
import { NEXT_PUBLIC_API_URL } from "@/config/api"; |
|
|
|
import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; |
|
|
|
|
|
|
|
@@ -56,6 +68,28 @@ const REFRESH_MS = 60 * 1000; |
|
|
|
const PRINTER_CHECK_MS = 60 * 1000; |
|
|
|
const PRINTER_RETRY_MS = 30 * 1000; |
|
|
|
const SETTINGS_KEY = "bagPrint_settings"; |
|
|
|
const ONPACK_ADMIN_USERNAME = "2fi"; |
|
|
|
|
|
|
|
/** Login username from backend JWT `sub` (UserDetails.username). */ |
|
|
|
function loginUsernameFromSession(session: SessionWithTokens | null | undefined): string { |
|
|
|
const token = session?.accessToken?.trim(); |
|
|
|
if (token) { |
|
|
|
try { |
|
|
|
const parts = token.split("."); |
|
|
|
if (parts.length >= 2) { |
|
|
|
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/"); |
|
|
|
const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4); |
|
|
|
const payload = JSON.parse(atob(padded)) as { sub?: unknown }; |
|
|
|
if (typeof payload.sub === "string" && payload.sub.trim()) { |
|
|
|
return payload.sub.trim(); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch { |
|
|
|
// fall through to display name |
|
|
|
} |
|
|
|
} |
|
|
|
return (session?.user?.name ?? "").trim(); |
|
|
|
} |
|
|
|
|
|
|
|
const DEFAULT_SETTINGS = { |
|
|
|
dabag_ip: "", |
|
|
|
@@ -95,7 +129,21 @@ function getBatch(jo: JobOrderListItem): string { |
|
|
|
return (jo.lotNo || "—").trim() || "—"; |
|
|
|
} |
|
|
|
|
|
|
|
function printableCodes(items: { itemCode: string; printable: boolean }[] | undefined): Set<string> { |
|
|
|
return new Set((items ?? []).filter((r) => r.printable).map((r) => r.itemCode.toUpperCase())); |
|
|
|
} |
|
|
|
|
|
|
|
function supportLabel(row: { inDatabase: boolean; builtin: boolean; printable: boolean }): string { |
|
|
|
if (!row.printable) return "缺模板"; |
|
|
|
if (row.inDatabase && row.builtin) return "資料庫+內建"; |
|
|
|
if (row.inDatabase) return "資料庫"; |
|
|
|
return "內建"; |
|
|
|
} |
|
|
|
|
|
|
|
const BagPrintSearch: React.FC = () => { |
|
|
|
const { data: session } = useSession() as { data: SessionWithTokens | null }; |
|
|
|
const canSeeOnPackAdmin = |
|
|
|
loginUsernameFromSession(session).toLowerCase() === ONPACK_ADMIN_USERNAME; |
|
|
|
const [planDate, setPlanDate] = useState(() => dayjs().format("YYYY-MM-DD")); |
|
|
|
const [jobOrders, setJobOrders] = useState<JobOrderListItem[]>([]); |
|
|
|
const [loading, setLoading] = useState(true); |
|
|
|
@@ -109,12 +157,28 @@ const BagPrintSearch: React.FC = () => { |
|
|
|
const [printContinuous, setPrintContinuous] = useState(false); |
|
|
|
const [printing, setPrinting] = useState(false); |
|
|
|
const [settingsOpen, setSettingsOpen] = useState(false); |
|
|
|
const [templatesOpen, setTemplatesOpen] = useState(false); |
|
|
|
const [templateMachine, setTemplateMachine] = useState<OnPackMachine>("juice"); |
|
|
|
const [templateItemCode, setTemplateItemCode] = useState(""); |
|
|
|
const [templateFiles, setTemplateFiles] = useState<OnPackTemplateFileDto[]>([]); |
|
|
|
const [templateLoading, setTemplateLoading] = useState(false); |
|
|
|
const [templateUploading, setTemplateUploading] = useState(false); |
|
|
|
const [supportedCatalog, setSupportedCatalog] = useState<OnPackSupportedCatalogDto | null>(null); |
|
|
|
const templateUploadRef = useRef(false); |
|
|
|
const templateDeleteRef = useRef(false); |
|
|
|
const templateFileInputRef = useRef<HTMLInputElement | null>(null); |
|
|
|
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 [downloadingOnPackExp, setDownloadingOnPackExp] = useState(false); |
|
|
|
const [downloadingOnPackText, setDownloadingOnPackText] = useState(false); |
|
|
|
const [downloadingOnPackTextExp, setDownloadingOnPackTextExp] = useState(false); |
|
|
|
const downloadingOnPackRef = useRef(false); |
|
|
|
const downloadingOnPackExpRef = useRef(false); |
|
|
|
const downloadingOnPackTextRef = useRef(false); |
|
|
|
const downloadingOnPackTextExpRef = useRef(false); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
setSettings(loadSettings()); |
|
|
|
@@ -275,6 +339,7 @@ const BagPrintSearch: React.FC = () => { |
|
|
|
}; |
|
|
|
|
|
|
|
const handleDownloadOnPackQr = async () => { |
|
|
|
if (downloadingOnPackRef.current) return; |
|
|
|
const onPackJobOrders = buildOnPackJobOrdersPayload(jobOrders); |
|
|
|
|
|
|
|
if (onPackJobOrders.length === 0) { |
|
|
|
@@ -282,6 +347,7 @@ const BagPrintSearch: React.FC = () => { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
downloadingOnPackRef.current = true; |
|
|
|
setDownloadingOnPack(true); |
|
|
|
try { |
|
|
|
const blob = await downloadOnPackQrZip({ |
|
|
|
@@ -306,10 +372,51 @@ const BagPrintSearch: React.FC = () => { |
|
|
|
}); |
|
|
|
} finally { |
|
|
|
setDownloadingOnPack(false); |
|
|
|
downloadingOnPackRef.current = false; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const handleDownloadOnPackQrWithExpiry = async () => { |
|
|
|
if (downloadingOnPackExpRef.current) return; |
|
|
|
const onPackJobOrders = buildOnPackJobOrdersPayload(jobOrders); |
|
|
|
|
|
|
|
if (onPackJobOrders.length === 0) { |
|
|
|
setSnackbar({ open: true, message: "當日沒有可下載的 job order", severity: "error" }); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
downloadingOnPackExpRef.current = true; |
|
|
|
setDownloadingOnPackExp(true); |
|
|
|
try { |
|
|
|
const blob = await downloadOnPackQrZipWithExpiry({ |
|
|
|
jobOrders: onPackJobOrders, |
|
|
|
planDate, |
|
|
|
}); |
|
|
|
|
|
|
|
const url = window.URL.createObjectURL(blob); |
|
|
|
const link = document.createElement("a"); |
|
|
|
link.href = url; |
|
|
|
link.setAttribute("download", `onpack_qr_exp_${planDate}.zip`); |
|
|
|
document.body.appendChild(link); |
|
|
|
link.click(); |
|
|
|
link.remove(); |
|
|
|
window.URL.revokeObjectURL(url); |
|
|
|
|
|
|
|
setSnackbar({ open: true, message: "OnPack 汁水機(含到期日)ZIP 已下載", severity: "success" }); |
|
|
|
} catch (e) { |
|
|
|
setSnackbar({ |
|
|
|
open: true, |
|
|
|
message: e instanceof Error ? e.message : "下載 OnPack 汁水機(含到期日)失敗", |
|
|
|
severity: "error", |
|
|
|
}); |
|
|
|
} finally { |
|
|
|
setDownloadingOnPackExp(false); |
|
|
|
downloadingOnPackExpRef.current = false; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const handleDownloadOnPackTextQr = async () => { |
|
|
|
if (downloadingOnPackTextRef.current) return; |
|
|
|
const onPackJobOrders = buildOnPackJobOrdersPayload(jobOrders); |
|
|
|
|
|
|
|
if (onPackJobOrders.length === 0) { |
|
|
|
@@ -317,6 +424,7 @@ const BagPrintSearch: React.FC = () => { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
downloadingOnPackTextRef.current = true; |
|
|
|
setDownloadingOnPackText(true); |
|
|
|
try { |
|
|
|
const blob = await downloadOnPackTextQrZip({ |
|
|
|
@@ -341,6 +449,140 @@ const BagPrintSearch: React.FC = () => { |
|
|
|
}); |
|
|
|
} finally { |
|
|
|
setDownloadingOnPackText(false); |
|
|
|
downloadingOnPackTextRef.current = false; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const handleDownloadOnPackTextQrWithExpiry = async () => { |
|
|
|
if (downloadingOnPackTextExpRef.current) return; |
|
|
|
const onPackJobOrders = buildOnPackJobOrdersPayload(jobOrders); |
|
|
|
|
|
|
|
if (onPackJobOrders.length === 0) { |
|
|
|
setSnackbar({ open: true, message: "當日沒有可下載的 job order", severity: "error" }); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
downloadingOnPackTextExpRef.current = true; |
|
|
|
setDownloadingOnPackTextExp(true); |
|
|
|
try { |
|
|
|
const blob = await downloadOnPackTextQrZipWithExpiry({ |
|
|
|
jobOrders: onPackJobOrders, |
|
|
|
planDate, |
|
|
|
}); |
|
|
|
|
|
|
|
const url = window.URL.createObjectURL(blob); |
|
|
|
const link = document.createElement("a"); |
|
|
|
link.href = url; |
|
|
|
link.setAttribute("download", `onpack2023_lemon_qr_exp_${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 { |
|
|
|
setDownloadingOnPackTextExp(false); |
|
|
|
downloadingOnPackTextExpRef.current = false; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const loadTemplateFiles = useCallback(async (machine: OnPackMachine) => { |
|
|
|
setTemplateLoading(true); |
|
|
|
try { |
|
|
|
const rows = await listOnPackTemplates(machine); |
|
|
|
setTemplateFiles(rows); |
|
|
|
} catch (e) { |
|
|
|
setSnackbar({ |
|
|
|
open: true, |
|
|
|
message: e instanceof Error ? e.message : "讀取 OnPack 模板失敗", |
|
|
|
severity: "error", |
|
|
|
}); |
|
|
|
} finally { |
|
|
|
setTemplateLoading(false); |
|
|
|
} |
|
|
|
}, []); |
|
|
|
|
|
|
|
const loadSupportedCatalog = useCallback(async (notify = false) => { |
|
|
|
try { |
|
|
|
setSupportedCatalog(await fetchOnPackSupportedCatalog()); |
|
|
|
} catch (e) { |
|
|
|
if (notify) { |
|
|
|
setSnackbar({ |
|
|
|
open: true, |
|
|
|
message: e instanceof Error ? e.message : "讀取 OnPack 支援清單失敗", |
|
|
|
severity: "error", |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
}, []); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
void loadSupportedCatalog(); |
|
|
|
}, [loadSupportedCatalog]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (!templatesOpen) return; |
|
|
|
void loadTemplateFiles(templateMachine); |
|
|
|
void loadSupportedCatalog(true); |
|
|
|
}, [templatesOpen, templateMachine, loadTemplateFiles, loadSupportedCatalog]); |
|
|
|
|
|
|
|
const handleUploadTemplates = async (fileList: FileList | null) => { |
|
|
|
if (templateUploadRef.current) return; |
|
|
|
const itemCode = templateItemCode.trim(); |
|
|
|
if (!itemCode) { |
|
|
|
setSnackbar({ open: true, message: "請先填寫品號", severity: "error" }); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!fileList || fileList.length === 0) { |
|
|
|
setSnackbar({ open: true, message: "請選擇 .image / .bmp / .job 檔案", severity: "error" }); |
|
|
|
return; |
|
|
|
} |
|
|
|
templateUploadRef.current = true; |
|
|
|
setTemplateUploading(true); |
|
|
|
try { |
|
|
|
const result = await uploadOnPackTemplates(templateMachine, itemCode, Array.from(fileList)); |
|
|
|
setSnackbar({ |
|
|
|
open: true, |
|
|
|
message: `已儲存 ${result.itemCode}:${result.saved.join("、")}`, |
|
|
|
severity: "success", |
|
|
|
}); |
|
|
|
if (templateFileInputRef.current) templateFileInputRef.current.value = ""; |
|
|
|
await loadTemplateFiles(templateMachine); |
|
|
|
await loadSupportedCatalog(); |
|
|
|
} catch (e) { |
|
|
|
setSnackbar({ |
|
|
|
open: true, |
|
|
|
message: e instanceof Error ? e.message : "上傳 OnPack 模板失敗", |
|
|
|
severity: "error", |
|
|
|
}); |
|
|
|
} finally { |
|
|
|
setTemplateUploading(false); |
|
|
|
templateUploadRef.current = false; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const handleDeleteTemplate = async (row: OnPackTemplateFileDto) => { |
|
|
|
if (templateDeleteRef.current) return; |
|
|
|
templateDeleteRef.current = true; |
|
|
|
try { |
|
|
|
await deleteOnPackTemplate(row.id); |
|
|
|
setSnackbar({ open: true, message: `已刪除 ${row.itemCode} / ${row.fileName}`, severity: "success" }); |
|
|
|
await loadTemplateFiles(templateMachine); |
|
|
|
await loadSupportedCatalog(); |
|
|
|
} catch (e) { |
|
|
|
setSnackbar({ |
|
|
|
open: true, |
|
|
|
message: e instanceof Error ? e.message : "刪除失敗", |
|
|
|
severity: "error", |
|
|
|
}); |
|
|
|
} finally { |
|
|
|
templateDeleteRef.current = false; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
@@ -369,6 +611,11 @@ const BagPrintSearch: React.FC = () => { |
|
|
|
<Button variant="outlined" startIcon={<Settings />} onClick={() => setSettingsOpen(true)}> |
|
|
|
設定 |
|
|
|
</Button> |
|
|
|
{canSeeOnPackAdmin && ( |
|
|
|
<Button variant="outlined" onClick={() => setTemplatesOpen(true)}> |
|
|
|
OnPack 模板 |
|
|
|
</Button> |
|
|
|
)} |
|
|
|
<Box |
|
|
|
sx={{ |
|
|
|
px: 1.5, |
|
|
|
@@ -403,19 +650,66 @@ const BagPrintSearch: React.FC = () => { |
|
|
|
variant="contained" |
|
|
|
startIcon={<Download />} |
|
|
|
onClick={handleDownloadOnPackQr} |
|
|
|
disabled={loading || downloadingOnPack || downloadingOnPackText || jobOrders.length === 0} |
|
|
|
disabled={ |
|
|
|
loading || |
|
|
|
downloadingOnPack || |
|
|
|
downloadingOnPackExp || |
|
|
|
downloadingOnPackText || |
|
|
|
downloadingOnPackTextExp || |
|
|
|
jobOrders.length === 0 |
|
|
|
} |
|
|
|
> |
|
|
|
{downloadingOnPack ? "下載中..." : "下載 OnPack 汁水機 QR code"} |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
startIcon={<Download />} |
|
|
|
onClick={handleDownloadOnPackQrWithExpiry} |
|
|
|
disabled={ |
|
|
|
loading || |
|
|
|
downloadingOnPack || |
|
|
|
downloadingOnPackExp || |
|
|
|
downloadingOnPackText || |
|
|
|
downloadingOnPackTextExp || |
|
|
|
jobOrders.length === 0 |
|
|
|
} |
|
|
|
> |
|
|
|
{downloadingOnPackExp ? "下載中..." : "下載 OnPack 汁水機(含到期日)"} |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
color="secondary" |
|
|
|
startIcon={<Download />} |
|
|
|
onClick={handleDownloadOnPackTextQr} |
|
|
|
disabled={loading || downloadingOnPack || downloadingOnPackText || jobOrders.length === 0} |
|
|
|
disabled={ |
|
|
|
loading || |
|
|
|
downloadingOnPack || |
|
|
|
downloadingOnPackExp || |
|
|
|
downloadingOnPackText || |
|
|
|
downloadingOnPackTextExp || |
|
|
|
jobOrders.length === 0 |
|
|
|
} |
|
|
|
> |
|
|
|
{downloadingOnPackText ? "下載中..." : "下載 OnPack2023檸檬機"} |
|
|
|
</Button> |
|
|
|
{canSeeOnPackAdmin && ( |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
color="secondary" |
|
|
|
startIcon={<Download />} |
|
|
|
onClick={handleDownloadOnPackTextQrWithExpiry} |
|
|
|
disabled={ |
|
|
|
loading || |
|
|
|
downloadingOnPack || |
|
|
|
downloadingOnPackExp || |
|
|
|
downloadingOnPackText || |
|
|
|
downloadingOnPackTextExp || |
|
|
|
jobOrders.length === 0 |
|
|
|
} |
|
|
|
> |
|
|
|
{downloadingOnPackTextExp ? "下載中..." : "下載 OnPack2023檸檬機(含到期日)"} |
|
|
|
</Button> |
|
|
|
)} |
|
|
|
</Stack> |
|
|
|
</Paper> |
|
|
|
|
|
|
|
@@ -436,6 +730,9 @@ const BagPrintSearch: React.FC = () => { |
|
|
|
const batch = getBatch(jo); |
|
|
|
const qtyStr = formatQty(jo.reqQty); |
|
|
|
const isSelected = selectedId === jo.id; |
|
|
|
const codeKey = (jo.itemCode || "").trim().toUpperCase(); |
|
|
|
const juiceOk = printableCodes(supportedCatalog?.juice).has(codeKey); |
|
|
|
const lemonOk = printableCodes(supportedCatalog?.lemon).has(codeKey); |
|
|
|
return ( |
|
|
|
<Paper |
|
|
|
key={jo.id} |
|
|
|
@@ -471,6 +768,10 @@ const BagPrintSearch: React.FC = () => { |
|
|
|
<Typography variant="h6" sx={{ fontSize: "1.35rem" }}> |
|
|
|
{jo.itemCode || "—"} |
|
|
|
</Typography> |
|
|
|
<Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap sx={{ mt: 0.5 }}> |
|
|
|
{juiceOk ? <Chip size="small" label="汁水機" color="primary" /> : null} |
|
|
|
{lemonOk ? <Chip size="small" label="檸檬機" color="secondary" /> : null} |
|
|
|
</Stack> |
|
|
|
</Box> |
|
|
|
<Box sx={{ flex: 1, minWidth: 0 }}> |
|
|
|
<Typography variant="h6" sx={{ fontSize: "1.35rem", wordBreak: "break-word" }}> |
|
|
|
@@ -632,6 +933,113 @@ const BagPrintSearch: React.FC = () => { |
|
|
|
</DialogActions> |
|
|
|
</Dialog> |
|
|
|
|
|
|
|
<Dialog |
|
|
|
open={templatesOpen && canSeeOnPackAdmin} |
|
|
|
onClose={() => setTemplatesOpen(false)} |
|
|
|
maxWidth="md" |
|
|
|
fullWidth |
|
|
|
> |
|
|
|
<DialogTitle>OnPack 支援品號 / 模板</DialogTitle> |
|
|
|
<DialogContent> |
|
|
|
<Stack spacing={2} sx={{ mt: 1 }}> |
|
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
|
下列為目前可做 OnPack 列印的品號(內建只列 PP 品號)。上傳新套模板後會自動加入清單。下方「刪除」只作用於資料庫檔,內建檔無法從畫面移除。 |
|
|
|
</Typography> |
|
|
|
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} alignItems="flex-start"> |
|
|
|
<Box sx={{ flex: 1, minWidth: 0 }}> |
|
|
|
<Typography variant="subtitle2" color="primary" sx={{ mb: 0.5 }}> |
|
|
|
汁水機 OnPack({printableCodes(supportedCatalog?.juice).size}) |
|
|
|
</Typography> |
|
|
|
<Box sx={{ maxHeight: 180, overflow: "auto", pr: 1 }}> |
|
|
|
{(supportedCatalog?.juice ?? []).filter((r) => r.printable).map((row) => ( |
|
|
|
<Typography key={`j-${row.itemCode}`} variant="body2" sx={{ fontFamily: "monospace" }}> |
|
|
|
{row.itemCode} · {supportLabel(row)} |
|
|
|
</Typography> |
|
|
|
))} |
|
|
|
</Box> |
|
|
|
</Box> |
|
|
|
<Box sx={{ flex: 1, minWidth: 0 }}> |
|
|
|
<Typography variant="subtitle2" color="secondary" sx={{ mb: 0.5 }}> |
|
|
|
檸檬機 OnPack({printableCodes(supportedCatalog?.lemon).size}) |
|
|
|
</Typography> |
|
|
|
<Box sx={{ maxHeight: 180, overflow: "auto", pr: 1 }}> |
|
|
|
{(supportedCatalog?.lemon ?? []).filter((r) => r.printable).map((row) => ( |
|
|
|
<Typography key={`l-${row.itemCode}`} variant="body2" sx={{ fontFamily: "monospace" }}> |
|
|
|
{row.itemCode} · {supportLabel(row)} |
|
|
|
</Typography> |
|
|
|
))} |
|
|
|
</Box> |
|
|
|
</Box> |
|
|
|
</Stack> |
|
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
|
新增品號:上傳該套 <code>.image</code> / <code>.bmp</code> / <code>.job</code>。汁水機請上傳{" "} |
|
|
|
<code>品號.image</code>;檸檬機請一併上傳模板裡引用的 BMP。 |
|
|
|
</Typography> |
|
|
|
<FormControl size="small" sx={{ minWidth: 200 }}> |
|
|
|
<InputLabel>機台</InputLabel> |
|
|
|
<Select |
|
|
|
value={templateMachine} |
|
|
|
label="機台" |
|
|
|
onChange={(e) => setTemplateMachine(e.target.value as OnPackMachine)} |
|
|
|
> |
|
|
|
<MenuItem value="juice">汁水機</MenuItem> |
|
|
|
<MenuItem value="lemon">檸檬機</MenuItem> |
|
|
|
</Select> |
|
|
|
</FormControl> |
|
|
|
<TextField |
|
|
|
label="品號" |
|
|
|
size="small" |
|
|
|
placeholder="例如 PP2211" |
|
|
|
value={templateItemCode} |
|
|
|
onChange={(e) => setTemplateItemCode(e.target.value)} |
|
|
|
/> |
|
|
|
<Button variant="contained" component="label" disabled={templateUploading}> |
|
|
|
{templateUploading ? "上傳中..." : "選擇檔案並上傳"} |
|
|
|
<input |
|
|
|
ref={templateFileInputRef} |
|
|
|
type="file" |
|
|
|
hidden |
|
|
|
multiple |
|
|
|
accept=".image,.bmp,.job" |
|
|
|
onChange={(e) => { |
|
|
|
void handleUploadTemplates(e.target.files); |
|
|
|
}} |
|
|
|
/> |
|
|
|
</Button> |
|
|
|
{templateLoading ? ( |
|
|
|
<Box sx={{ display: "flex", justifyContent: "center", py: 2 }}> |
|
|
|
<CircularProgress size={24} /> |
|
|
|
</Box> |
|
|
|
) : templateFiles.length === 0 ? ( |
|
|
|
<Typography color="text.secondary">資料庫尚無此機台的上傳檔</Typography> |
|
|
|
) : ( |
|
|
|
<Stack spacing={0.5} sx={{ maxHeight: 320, overflow: "auto" }}> |
|
|
|
{templateFiles.map((row) => ( |
|
|
|
<Stack |
|
|
|
key={row.id} |
|
|
|
direction="row" |
|
|
|
alignItems="center" |
|
|
|
justifyContent="space-between" |
|
|
|
spacing={1} |
|
|
|
sx={{ py: 0.5, borderBottom: "1px solid #e0e0e0" }} |
|
|
|
> |
|
|
|
<Typography variant="body2" sx={{ fontFamily: "monospace" }}> |
|
|
|
{row.itemCode} · {row.fileName} · {Math.max(1, Math.round(row.byteSize / 1024))} KB |
|
|
|
</Typography> |
|
|
|
<Button size="small" color="error" onClick={() => void handleDeleteTemplate(row)}> |
|
|
|
刪除 |
|
|
|
</Button> |
|
|
|
</Stack> |
|
|
|
))} |
|
|
|
</Stack> |
|
|
|
)} |
|
|
|
</Stack> |
|
|
|
</DialogContent> |
|
|
|
<DialogActions> |
|
|
|
<Button onClick={() => setTemplatesOpen(false)}>關閉</Button> |
|
|
|
</DialogActions> |
|
|
|
</Dialog> |
|
|
|
|
|
|
|
<Snackbar |
|
|
|
open={snackbar.open} |
|
|
|
autoHideDuration={3000} |
|
|
|
|