Преглед изворни кода

added for expiry date for bag printing

fix負數倉
PC-20260115JRSN\Administrator пре 4 недеља
родитељ
комит
55ef2bfc00
4 измењених фајлова са 547 додато и 5 уклоњено
  1. +130
    -2
      src/app/api/bagPrint/actions.ts
  2. +5
    -0
      src/app/api/laserPrint/actions.ts
  3. +411
    -3
      src/components/BagPrint/BagPrintSearch.tsx
  4. +1
    -0
      src/components/LaserPrint/LaserPrintSearch.tsx

+ 130
- 2
src/app/api/bagPrint/actions.ts Прегледај датотеку

@@ -13,6 +13,12 @@ export interface JobOrderListItem {
stockInLineId: number | null;
itemId: number | null;
lotNo: string | null;
/** Effective shelf life days used for print (chilled or -18, according to useMinus18). */
defaultShelfLifeDays?: number | null;
/** True when expiry is computed from -18 warehouse days. */
useMinus18?: boolean | null;
/** Print date + effective shelf life days (yyyy-MM-dd). */
expiryDate?: string | null;
/** 打袋機 DataFlex cumulative printed qty */
bagPrintedQty?: number;
/** 標簽機 cumulative printed qty */
@@ -37,6 +43,8 @@ export interface OnPackQrDownloadRequest {
jobOrderId: number;
itemCode: string;
}[];
/** /bagPrint filter date (YYYY-MM-DD). Used by expiry ZIP for production date. */
planDate?: string;
}

/** Same mapping as Bag Print download buttons: one entry per row with a non-empty item code. */
@@ -80,17 +88,27 @@ export async function pushOnPackTextQrZipToNgpcl(request: OnPackQrDownloadReques

/** Readable message when ZIP download returns non-OK (plain text, JSON error body, or generic). */
async function zipDownloadError(res: Response): Promise<Error> {
return parseBagPrintApiError(res, "下載");
}

/** Backend ErrorRes is `{ timestamp, traceId }` with no message; avoid calling that a ZIP download failure. */
async function parseBagPrintApiError(res: Response, action: string): Promise<Error> {
const text = await res.text();
const ct = res.headers.get("content-type") ?? "";
if (ct.includes("application/json")) {
try {
const j = JSON.parse(text) as { message?: string; error?: string };
const j = JSON.parse(text) as { message?: string; error?: string; traceId?: string };
if (typeof j.message === "string" && j.message.length > 0) {
return new Error(j.message);
}
if (typeof j.error === "string" && j.error.length > 0) {
return new Error(j.error);
}
if (typeof j.traceId === "string" && j.traceId.length > 0) {
return new Error(
`${action}失敗(HTTP ${res.status})。請重啟後端以執行 Liquibase,或查看日誌 traceId ${j.traceId}。`,
);
}
} catch {
/* ignore parse */
}
@@ -98,7 +116,7 @@ async function zipDownloadError(res: Response): Promise<Error> {
if (text && text.length > 0 && text.length < 800 && !text.trim().startsWith("{")) {
return new Error(text);
}
return new Error(`下載失敗(HTTP ${res.status})。請查看後端日誌或確認資料庫已執行 Liquibase 更新。`);
return new Error(`${action}失敗(HTTP ${res.status})。請查看後端日誌或確認資料庫已執行 Liquibase 更新。`);
}

/**
@@ -149,6 +167,24 @@ export async function downloadOnPackQrZip(
return res.blob();
}

/** 汁水機 OnPack — same as QR ZIP, plus LOGO_EXP BMP from item_default_shelf_life. */
export async function downloadOnPackQrZipWithExpiry(
request: OnPackQrDownloadRequest,
): Promise<Blob> {
const url = `${NEXT_PUBLIC_API_URL}/plastic/download-onpack-qr-with-expiry`;
const res = await clientAuthFetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});

if (!res.ok) {
throw await zipDownloadError(res);
}

return res.blob();
}

/** OnPack2023 檸檬機 — text QR template (`onpack2030_2`), no separate .bmp */
export async function downloadOnPackTextQrZip(
request: OnPackQrDownloadRequest,
@@ -166,3 +202,95 @@ export async function downloadOnPackTextQrZip(

return res.blob();
}

/** OnPack2023 檸檬機 — same as text ZIP, plus TEXT_EXP from item_default_shelf_life. */
export async function downloadOnPackTextQrZipWithExpiry(
request: OnPackQrDownloadRequest,
): Promise<Blob> {
const url = `${NEXT_PUBLIC_API_URL}/plastic/download-onpack-qr-text-with-expiry`;
const res = await clientAuthFetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});

if (!res.ok) {
throw await zipDownloadError(res);
}

return res.blob();
}

export type OnPackMachine = "juice" | "lemon";

export interface OnPackTemplateFileDto {
id: number;
machine: OnPackMachine | string;
itemCode: string;
fileName: string;
byteSize: number;
modified?: string | null;
}

export interface OnPackTemplateUploadResponse {
machine: string;
itemCode: string;
saved: string[];
}

export interface OnPackSupportedItemDto {
itemCode: string;
printable: boolean;
inDatabase: boolean;
builtin: boolean;
registered: boolean;
}

export interface OnPackSupportedCatalogDto {
juice: OnPackSupportedItemDto[];
lemon: OnPackSupportedItemDto[];
}

export async function fetchOnPackSupportedCatalog(): Promise<OnPackSupportedCatalogDto> {
const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates/supported`;
const res = await clientAuthFetch(url, { method: "GET" });
if (!res.ok) {
throw await parseBagPrintApiError(res, "讀取 OnPack 支援清單");
}
return (await res.json()) as OnPackSupportedCatalogDto;
}

export async function listOnPackTemplates(machine?: OnPackMachine): Promise<OnPackTemplateFileDto[]> {
const q = machine ? `?machine=${encodeURIComponent(machine)}` : "";
const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates${q}`;
const res = await clientAuthFetch(url, { method: "GET" });
if (!res.ok) {
throw await parseBagPrintApiError(res, "讀取 OnPack 模板");
}
return (await res.json()) as OnPackTemplateFileDto[];
}

export async function uploadOnPackTemplates(
machine: OnPackMachine,
itemCode: string,
files: File[],
): Promise<OnPackTemplateUploadResponse> {
const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates`;
const body = new FormData();
body.append("machine", machine);
body.append("itemCode", itemCode);
files.forEach((f) => body.append("files", f));
const res = await clientAuthFetch(url, { method: "POST", body });
if (!res.ok) {
throw await parseBagPrintApiError(res, "上傳 OnPack 模板");
}
return (await res.json()) as OnPackTemplateUploadResponse;
}

export async function deleteOnPackTemplate(id: number): Promise<void> {
const url = `${NEXT_PUBLIC_API_URL}/plastic/onpack-templates/${id}`;
const res = await clientAuthFetch(url, { method: "DELETE" });
if (!res.ok && res.status !== 204) {
throw await parseBagPrintApiError(res, "刪除 OnPack 模板");
}
}

+ 5
- 0
src/app/api/laserPrint/actions.ts Прегледај датотеку

@@ -13,6 +13,9 @@ export interface JobOrderListItem {
stockInLineId: number | null;
itemId: number | null;
lotNo: string | null;
defaultShelfLifeDays?: number | null;
useMinus18?: boolean | null;
expiryDate?: string | null;
bagPrintedQty?: number;
labelPrintedQty?: number;
laserPrintedQty?: number;
@@ -48,6 +51,8 @@ export interface LaserBag2SendRequest {
jobOrderId?: number | null;
jobOrderNo?: string | null;
lotNo?: string | null;
/** Print-time expiry from job list (`yyyy-MM-dd`); backend sends as 4th TCP field. */
expiryDate?: string | null;
source?: string | null;
}



+ 411
- 3
src/components/BagPrint/BagPrintSearch.tsx Прегледај датотеку

@@ -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}


+ 1
- 0
src/components/LaserPrint/LaserPrintSearch.tsx Прегледај датотеку

@@ -191,6 +191,7 @@ const LaserPrintSearch: React.FC = () => {
jobOrderId: jo.id,
jobOrderNo: jo.code,
lotNo: jo.lotNo,
expiryDate: jo.expiryDate ?? null,
source: "MANUAL",
});



Loading…
Откажи
Сачувај