FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 

793 行
26 KiB

  1. "use client";
  2. /**
  3. * Workbench copy of `LotLabelPrintModal`: same label-print flow, plus optional
  4. * 「掃碼提貨」 per listed lot row (parent calls `workbenchScanPick` with `inventoryLotLineId`).
  5. */
  6. import React, {
  7. useCallback,
  8. useEffect,
  9. useMemo,
  10. useRef,
  11. useState,
  12. } from "react";
  13. import {
  14. Alert,
  15. Box,
  16. Button,
  17. CircularProgress,
  18. Dialog,
  19. DialogActions,
  20. DialogContent,
  21. DialogTitle,
  22. FormControl,
  23. InputLabel,
  24. MenuItem,
  25. Select,
  26. Snackbar,
  27. Stack,
  28. TextField,
  29. Typography,
  30. } from "@mui/material";
  31. import {
  32. analyzeWorkbenchQrCode,
  33. fetchWorkbenchAvailableLotsByItem,
  34. fetchWorkbenchPrinters,
  35. printWorkbenchLotLabel,
  36. } from "@/app/api/doworkbench/actions";
  37. import { QRCodeSVG } from "qrcode.react";
  38. type ScanPayload = {
  39. itemId: number;
  40. stockInLineId: number;
  41. };
  42. type Printer = {
  43. id: number;
  44. name?: string;
  45. description?: string;
  46. ip?: string;
  47. port?: number;
  48. type?: string;
  49. brand?: string;
  50. };
  51. type QrCodeAnalysisResponse = {
  52. itemId: number;
  53. itemCode: string;
  54. itemName: string;
  55. scanned?: {
  56. stockInLineId: number;
  57. lotNo: string;
  58. inventoryLotLineId: number;
  59. warehouseCode?: string | null;
  60. warehouseName?: string | null;
  61. } | null;
  62. sameItemLots: Array<{
  63. lotNo: string;
  64. inventoryLotLineId: number;
  65. stockInLineId?: number | null;
  66. availableQty: number;
  67. uom: string;
  68. warehouseCode?: string | null;
  69. warehouseName?: string | null;
  70. }>;
  71. };
  72. export interface WorkbenchLotLabelPrintModalProps {
  73. open: boolean;
  74. onClose: () => void;
  75. initialPayload?: ScanPayload | null;
  76. initialItemId?: number | null;
  77. defaultPrinterName?: string;
  78. hideScanSection?: boolean;
  79. reminderText?: string;
  80. statusTitleText?: string;
  81. /** 與 statusTitleText 搭配;預設 error(舊版固定紅字) */
  82. statusTitleSeverity?: "success" | "warning" | "error";
  83. warehouseCodePrefixFilter?: string;
  84. /**
  85. * When true, omit the API 「scanned」 lot from the merged list (legacy FG-style).
  86. * Workbench should leave false so the current row’s lot appears for label print / scan-pick.
  87. */
  88. hideTriggeredLot?: boolean;
  89. /** 提貨台表格列上的可用量/單位(API 的 sameItemLots 不含掃描行,需補上才能顯示「目前這筆」) */
  90. triggerLotAvailableQty?: number | null;
  91. triggerLotUom?: string | null;
  92. /** 此出庫行已掃碼/已完成時為 true,停用所有「掃碼提貨」(仍可列印標籤) */
  93. disableScanPick?: boolean;
  94. /**
  95. * When set, each lot row shows 「掃碼提貨」. Parent should call `workbenchScanPick`
  96. * with `inventoryLotLineId` and throw on failure.
  97. */
  98. onWorkbenchScanPick?: (args: {
  99. inventoryLotLineId: number;
  100. lotNo: string;
  101. qty?: number;
  102. }) => Promise<void>;
  103. /** Global submit qty shared with outer "Qty will submit". */
  104. submitQty?: number | null;
  105. onSubmitQtyChange?: (qty: number) => void;
  106. }
  107. function safeParseScanPayload(raw: string): ScanPayload | null {
  108. try {
  109. const obj = JSON.parse(raw);
  110. const itemId = Number(obj?.itemId);
  111. const stockInLineId = Number(obj?.stockInLineId);
  112. if (!Number.isFinite(itemId) || !Number.isFinite(stockInLineId))
  113. return null;
  114. return { itemId, stockInLineId };
  115. } catch {
  116. return null;
  117. }
  118. }
  119. function formatPrinterLabel(p: Printer): string {
  120. const name = (p.name || "").trim();
  121. if (name) return name;
  122. const desc = (p.description || "").trim();
  123. if (desc) return desc;
  124. const code = (p as { code?: string }).code?.trim?.() ?? "";
  125. if (code) return code;
  126. return `#${p.id}`;
  127. }
  128. function isLabelPrinter(p: Printer): boolean {
  129. const s = `${p.name ?? ""} ${p.description ?? ""} ${
  130. (p as { code?: string }).code ?? ""
  131. } ${p.type ?? ""} ${p.brand ?? ""}`.toLowerCase();
  132. return s.includes("label") && !s.includes("a4");
  133. }
  134. const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = ({
  135. open,
  136. onClose,
  137. initialPayload = null,
  138. initialItemId = null,
  139. defaultPrinterName,
  140. hideScanSection,
  141. reminderText,
  142. statusTitleText,
  143. statusTitleSeverity = "error",
  144. warehouseCodePrefixFilter,
  145. hideTriggeredLot = false,
  146. triggerLotAvailableQty = null,
  147. triggerLotUom = null,
  148. disableScanPick = false,
  149. onWorkbenchScanPick,
  150. submitQty = null,
  151. onSubmitQtyChange,
  152. }) => {
  153. const scanInputRef = useRef<HTMLInputElement | null>(null);
  154. const [scanInput, setScanInput] = useState("");
  155. const [scanError, setScanError] = useState<string | null>(null);
  156. const [printers, setPrinters] = useState<Printer[]>([]);
  157. const [printersLoading, setPrintersLoading] = useState(false);
  158. const [selectedPrinterId, setSelectedPrinterId] = useState<number | "">("");
  159. const [analysisLoading, setAnalysisLoading] = useState(false);
  160. const [analysis, setAnalysis] = useState<QrCodeAnalysisResponse | null>(null);
  161. const [lastPayload, setLastPayload] = useState<ScanPayload | null>(null);
  162. const [lastItemId, setLastItemId] = useState<number | null>(null);
  163. const [printQty, setPrintQty] = useState(1);
  164. const [printingLotLineId, setPrintingLotLineId] = useState<number | null>(
  165. null,
  166. );
  167. const [qrVisibleLotLineId, setQrVisibleLotLineId] = useState<number | null>(
  168. null,
  169. );
  170. const [snackbar, setSnackbar] = useState<{
  171. open: boolean;
  172. message: string;
  173. severity?: "success" | "info" | "error";
  174. }>({
  175. open: false,
  176. message: "",
  177. severity: "info",
  178. });
  179. const resetAll = useCallback(() => {
  180. setScanInput("");
  181. setScanError(null);
  182. setAnalysis(null);
  183. setPrintQty(1);
  184. setPrintingLotLineId(null);
  185. setQrVisibleLotLineId(null);
  186. }, []);
  187. useEffect(() => {
  188. if (!open) return;
  189. resetAll();
  190. const t = setTimeout(() => scanInputRef.current?.focus(), 50);
  191. return () => clearTimeout(t);
  192. }, [open, resetAll]);
  193. const loadPrinters = useCallback(async () => {
  194. setPrintersLoading(true);
  195. try {
  196. const data = (await fetchWorkbenchPrinters()) as Printer[];
  197. const list = Array.isArray(data) ? data : [];
  198. setPrinters(list.filter(isLabelPrinter));
  199. } catch (e) {
  200. setPrinters([]);
  201. setSnackbar({
  202. open: true,
  203. message: e instanceof Error ? e.message : "載入印表機清單失敗",
  204. severity: "error",
  205. });
  206. } finally {
  207. setPrintersLoading(false);
  208. }
  209. }, []);
  210. useEffect(() => {
  211. if (!open) return;
  212. void loadPrinters();
  213. }, [open, loadPrinters]);
  214. const effectiveHideScanSection = hideScanSection ?? initialPayload != null;
  215. const pickDefaultPrinterId = useCallback(
  216. (list: Printer[]): number | null => {
  217. if (!defaultPrinterName) return null;
  218. const target = defaultPrinterName.trim().toLowerCase();
  219. if (!target) return null;
  220. const byExact = list.find(
  221. (p) => formatPrinterLabel(p).trim().toLowerCase() === target,
  222. );
  223. if (byExact) return byExact.id;
  224. const byIncludes = list.find((p) =>
  225. formatPrinterLabel(p).trim().toLowerCase().includes(target),
  226. );
  227. return byIncludes?.id ?? null;
  228. },
  229. [defaultPrinterName],
  230. );
  231. useEffect(() => {
  232. if (!open) return;
  233. if (selectedPrinterId !== "") return;
  234. if (printers.length === 0) return;
  235. const id = pickDefaultPrinterId(printers);
  236. if (id != null) setSelectedPrinterId(id);
  237. }, [open, printers, selectedPrinterId, pickDefaultPrinterId]);
  238. const analyzePayload = useCallback(
  239. async (payload: ScanPayload) => {
  240. setLastPayload(payload);
  241. setScanError(null);
  242. setAnalysisLoading(true);
  243. try {
  244. const data = (await analyzeWorkbenchQrCode(payload)) as QrCodeAnalysisResponse;
  245. setAnalysis(data);
  246. setSnackbar({
  247. open: true,
  248. message: "已載入同品可用批號清單",
  249. severity: "success",
  250. });
  251. } catch (e) {
  252. setAnalysis(null);
  253. setScanError(e instanceof Error ? e.message : "分析失敗");
  254. } finally {
  255. setAnalysisLoading(false);
  256. }
  257. },
  258. [],
  259. );
  260. const analyzeByItem = useCallback(
  261. async (itemId: number) => {
  262. if (!Number.isFinite(itemId) || itemId <= 0) {
  263. setScanError("無效 itemId,無法載入批號清單。");
  264. return;
  265. }
  266. setLastItemId(itemId);
  267. setScanError(null);
  268. setAnalysisLoading(true);
  269. try {
  270. const data = (await fetchWorkbenchAvailableLotsByItem(itemId)) as {
  271. itemId: number;
  272. itemCode: string;
  273. itemName: string;
  274. sameItemLots: QrCodeAnalysisResponse["sameItemLots"];
  275. };
  276. setAnalysis({
  277. itemId: data.itemId,
  278. itemCode: data.itemCode,
  279. itemName: data.itemName,
  280. scanned: null,
  281. sameItemLots: data.sameItemLots ?? [],
  282. });
  283. setSnackbar({
  284. open: true,
  285. message: "已載入同品可用批號清單",
  286. severity: "success",
  287. });
  288. } catch (e) {
  289. setAnalysis(null);
  290. setScanError(e instanceof Error ? e.message : "分析失敗");
  291. } finally {
  292. setAnalysisLoading(false);
  293. }
  294. },
  295. [],
  296. );
  297. const handleAnalyze = useCallback(async () => {
  298. const raw = scanInput.trim();
  299. const payload = safeParseScanPayload(raw);
  300. if (!payload) {
  301. setScanError(
  302. '掃碼內容格式錯誤,請重新掃碼',
  303. );
  304. setAnalysis(null);
  305. return;
  306. }
  307. await analyzePayload(payload);
  308. }, [scanInput, analyzePayload]);
  309. const handleRefreshLots = useCallback(async () => {
  310. const payload = lastPayload ?? safeParseScanPayload(scanInput.trim());
  311. if (payload) {
  312. await analyzePayload(payload);
  313. return;
  314. }
  315. const candidateItemId =
  316. (Number.isFinite(lastItemId ?? NaN) && (lastItemId ?? 0) > 0
  317. ? (lastItemId as number)
  318. : Number(initialItemId));
  319. if (Number.isFinite(candidateItemId) && candidateItemId > 0) {
  320. await analyzeByItem(candidateItemId);
  321. return;
  322. }
  323. if (!payload) {
  324. setSnackbar({
  325. open: true,
  326. message: "請先掃碼或查詢一次,才可刷新批號清單。",
  327. severity: "info",
  328. });
  329. return;
  330. }
  331. }, [analyzeByItem, analyzePayload, initialItemId, lastItemId, lastPayload, scanInput]);
  332. useEffect(() => {
  333. if (!open) return;
  334. if (initialPayload) {
  335. setScanInput(JSON.stringify(initialPayload));
  336. void analyzePayload(initialPayload);
  337. return;
  338. }
  339. if (Number.isFinite(Number(initialItemId)) && Number(initialItemId) > 0) {
  340. void analyzeByItem(Number(initialItemId));
  341. }
  342. }, [open, initialPayload, initialItemId, analyzePayload, analyzeByItem]);
  343. const availableLots = useMemo(() => {
  344. if (!analysis) return [];
  345. const list = (analysis.sameItemLots ?? []).filter(
  346. (x) => Number(x.availableQty) > 0 && !!String(x.lotNo || "").trim(),
  347. );
  348. const scannedLotLineId = analysis.scanned?.inventoryLotLineId;
  349. const scannedRow = scannedLotLineId
  350. ? list.find((x) => x.inventoryLotLineId === scannedLotLineId)
  351. : undefined;
  352. const tableQty = Number(triggerLotAvailableQty);
  353. const fromTable =
  354. Number.isFinite(tableQty) && tableQty >= 0 ? tableQty : 0;
  355. const fromApi = Number(scannedRow?.availableQty ?? 0);
  356. const scanned = analysis.scanned;
  357. const scannedLot = scannedLotLineId
  358. ? {
  359. lotNo: scanned?.lotNo ?? "",
  360. inventoryLotLineId: scannedLotLineId,
  361. stockInLineId: Number(scanned?.stockInLineId ?? 0) || null,
  362. availableQty: Math.max(fromApi, fromTable) as number,
  363. uom: (scannedRow?.uom ?? triggerLotUom ?? "") as string,
  364. warehouseCode:
  365. scanned?.warehouseCode ?? scannedRow?.warehouseCode,
  366. warehouseName:
  367. scanned?.warehouseName ?? scannedRow?.warehouseName,
  368. _scanned: true as const,
  369. }
  370. : null;
  371. const merged = [
  372. ...(!hideTriggeredLot && scannedLot ? [scannedLot] : []),
  373. ...list
  374. .filter((x) => x.inventoryLotLineId !== scannedLotLineId)
  375. .map((x) => ({ ...x, _scanned: false as const })),
  376. ];
  377. return merged;
  378. }, [analysis, hideTriggeredLot, triggerLotAvailableQty, triggerLotUom]);
  379. const filteredLots = useMemo(() => {
  380. const prefix = String(warehouseCodePrefixFilter ?? "").trim();
  381. if (!prefix) return availableLots;
  382. return availableLots.filter((lot) => {
  383. // 使用者從本列開啟視窗:即使 API 未帶 warehouseCode,仍應顯示目前這筆批號
  384. if (lot._scanned) return true;
  385. return String(lot.warehouseCode ?? "").startsWith(prefix);
  386. });
  387. }, [availableLots, warehouseCodePrefixFilter]);
  388. const selectedPrinter = useMemo(() => {
  389. if (selectedPrinterId === "") return null;
  390. return printers.find((p) => p.id === selectedPrinterId) ?? null;
  391. }, [printers, selectedPrinterId]);
  392. const canPrint =
  393. !!analysis && selectedPrinterId !== "" && printQty >= 1 && !analysisLoading;
  394. const handlePrintOne = useCallback(
  395. async (inventoryLotLineId: number, lotNo: string) => {
  396. if (selectedPrinterId === "") {
  397. setSnackbar({
  398. open: true,
  399. message: "請先選擇印表機",
  400. severity: "error",
  401. });
  402. return;
  403. }
  404. if (printQty < 1 || !Number.isFinite(printQty)) {
  405. setSnackbar({
  406. open: true,
  407. message: "列印張數需為大於等於 1 的整數",
  408. severity: "error",
  409. });
  410. return;
  411. }
  412. setPrintingLotLineId(inventoryLotLineId);
  413. try {
  414. await printWorkbenchLotLabel({
  415. inventoryLotLineId,
  416. printerId: selectedPrinterId,
  417. printQty: Math.floor(printQty),
  418. });
  419. setSnackbar({
  420. open: true,
  421. message: `已送出列印:Lot ${lotNo}`,
  422. severity: "success",
  423. });
  424. } catch (e) {
  425. setSnackbar({
  426. open: true,
  427. message: e instanceof Error ? e.message : "列印失敗",
  428. severity: "error",
  429. });
  430. } finally {
  431. setPrintingLotLineId(null);
  432. }
  433. },
  434. [selectedPrinterId, printQty],
  435. );
  436. return (
  437. <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
  438. <DialogTitle>批號標籤列印(提貨台)</DialogTitle>
  439. <DialogContent>
  440. <Stack spacing={2} sx={{ mt: 1 }}>
  441. {statusTitleText ? (
  442. <Typography
  443. variant="h6"
  444. sx={{
  445. fontWeight: 800,
  446. color:
  447. statusTitleSeverity === "success"
  448. ? "success.main"
  449. : statusTitleSeverity === "warning"
  450. ? "warning.main"
  451. : "error.main",
  452. }}
  453. >
  454. {statusTitleText}
  455. </Typography>
  456. ) : null}
  457. {reminderText ? (
  458. <Alert severity="warning">{reminderText}</Alert>
  459. ) : null}
  460. {effectiveHideScanSection ? null : (
  461. <>
  462. {/*
  463. <Alert severity="info">
  464. 請掃描條碼(JSON 格式),例如{" "}
  465. <code>{'{"itemId":16431,"stockInLineId":10381'}</code>。
  466. </Alert>
  467. */}
  468. <Stack
  469. direction={{ xs: "column", md: "row" }}
  470. spacing={2}
  471. alignItems={{ xs: "stretch", md: "center" }}
  472. >
  473. <TextField
  474. inputRef={scanInputRef}
  475. label="掃碼內容"
  476. value={scanInput}
  477. onChange={(e) => setScanInput(e.target.value)}
  478. fullWidth
  479. size="small"
  480. error={!!scanError}
  481. helperText={scanError || "掃描後按 Enter 或點「查詢」"}
  482. onKeyDown={(e) => {
  483. if (e.key === "Enter") {
  484. e.preventDefault();
  485. void handleAnalyze();
  486. }
  487. }}
  488. disabled={analysisLoading}
  489. />
  490. <Button
  491. variant="contained"
  492. onClick={() => void handleAnalyze()}
  493. disabled={analysisLoading || !scanInput.trim()}
  494. >
  495. {analysisLoading ? <CircularProgress size={18} /> : "查詢"}
  496. </Button>
  497. <Button
  498. variant="outlined"
  499. onClick={() => {
  500. resetAll();
  501. scanInputRef.current?.focus();
  502. }}
  503. disabled={analysisLoading}
  504. >
  505. 清除
  506. </Button>
  507. </Stack>
  508. </>
  509. )}
  510. <Stack
  511. direction={{ xs: "column", md: "row" }}
  512. spacing={2}
  513. alignItems={{ xs: "stretch", md: "center" }}
  514. >
  515. <FormControl
  516. size="small"
  517. sx={{ minWidth: 260 }}
  518. disabled={printersLoading}
  519. >
  520. <InputLabel>印表機</InputLabel>
  521. <Select
  522. label="印表機"
  523. value={selectedPrinterId}
  524. onChange={(e) =>
  525. setSelectedPrinterId((e.target.value as number) ?? "")
  526. }
  527. >
  528. <MenuItem value="">
  529. <em>{printersLoading ? "載入中..." : "請選擇"}</em>
  530. </MenuItem>
  531. {printers.map((p) => (
  532. <MenuItem key={p.id} value={p.id}>
  533. {formatPrinterLabel(p)}
  534. </MenuItem>
  535. ))}
  536. </Select>
  537. </FormControl>
  538. <TextField
  539. label="列印張數"
  540. size="small"
  541. type="number"
  542. inputProps={{ min: 1, step: 1 }}
  543. value={printQty}
  544. onChange={(e) => setPrintQty(Number(e.target.value))}
  545. sx={{ width: 140 }}
  546. disabled={analysisLoading}
  547. />
  548. {onWorkbenchScanPick ? (
  549. <TextField
  550. label="提交數量"
  551. size="small"
  552. type="number"
  553. inputProps={{ min: 0, step: 1 }}
  554. value={
  555. Number.isFinite(Number(submitQty)) ? Number(submitQty) : 0
  556. }
  557. onChange={(e) => {
  558. const n = Number(e.target.value);
  559. if (!Number.isFinite(n) || n < 0) return;
  560. onSubmitQtyChange?.(n);
  561. }}
  562. sx={{ width: 140 }}
  563. disabled={analysisLoading}
  564. />
  565. ) : null}
  566. <Button
  567. variant="outlined"
  568. onClick={() => void handleRefreshLots()}
  569. disabled={analysisLoading}
  570. >
  571. {analysisLoading ? (
  572. <CircularProgress size={18} />
  573. ) : (
  574. "刷新批號清單"
  575. )}
  576. </Button>
  577. {selectedPrinter && (
  578. <Typography
  579. variant="body2"
  580. color="text.secondary"
  581. sx={{ ml: { md: "auto" } }}
  582. >
  583. 已選:{formatPrinterLabel(selectedPrinter)}
  584. </Typography>
  585. )}
  586. </Stack>
  587. {analysis && (
  588. <Box>
  589. <Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 1 }}>
  590. 品號:{analysis.itemCode} {analysis.itemName}
  591. </Typography>
  592. {filteredLots.length === 0 ? (
  593. <Alert severity="warning">
  594. 找不到該樓層有可用批號(availableQty &gt; 0)。
  595. </Alert>
  596. ) : (
  597. <Stack spacing={1}>
  598. {filteredLots.map((lot) => {
  599. const isPrinting =
  600. printingLotLineId === lot.inventoryLotLineId;
  601. const loc = String(lot.warehouseCode ?? "").trim();
  602. const canShowLotQr =
  603. !!onWorkbenchScanPick &&
  604. !!analysis &&
  605. !analysisLoading &&
  606. !disableScanPick;
  607. const lotQrPayload =
  608. Number.isFinite(Number(analysis?.itemId)) &&
  609. Number.isFinite(Number(lot.stockInLineId))
  610. ? {
  611. itemId: Number(analysis?.itemId),
  612. stockInLineId: Number(lot.stockInLineId),
  613. }
  614. : null;
  615. return (
  616. <Box
  617. key={lot.inventoryLotLineId}
  618. sx={{
  619. p: 1.25,
  620. borderRadius: 1,
  621. border: "1px solid",
  622. borderColor: "divider",
  623. display: "flex",
  624. alignItems: "center",
  625. gap: 2,
  626. backgroundColor: lot._scanned
  627. ? "rgba(25, 118, 210, 0.08)"
  628. : "transparent",
  629. }}
  630. >
  631. <Box sx={{ minWidth: 220 }}>
  632. <Typography
  633. variant="body1"
  634. sx={{ fontWeight: lot._scanned ? 800 : 600 }}
  635. >
  636. Lot:{lot.lotNo}
  637. {lot._scanned ? "(當前批次)" : ""}
  638. </Typography>
  639. <Typography variant="body2" color="text.secondary">
  640. 位置:{loc || "—"}
  641. </Typography>
  642. <Typography variant="body2" color="text.secondary">
  643. 可用量:{Number(lot.availableQty).toLocaleString()}{" "}
  644. 單位:{lot.uom || ""}
  645. </Typography>
  646. </Box>
  647. <Stack
  648. direction="row"
  649. spacing={1}
  650. sx={{ ml: "auto" }}
  651. flexWrap="wrap"
  652. useFlexGap
  653. >
  654. <Button
  655. variant="contained"
  656. disabled={!canPrint || isPrinting}
  657. onClick={() =>
  658. void handlePrintOne(
  659. lot.inventoryLotLineId,
  660. lot.lotNo,
  661. )
  662. }
  663. >
  664. {isPrinting ? (
  665. <CircularProgress size={18} />
  666. ) : (
  667. "列印標籤"
  668. )}
  669. </Button>
  670. {onWorkbenchScanPick ? (
  671. <Button
  672. variant="outlined"
  673. color="secondary"
  674. title={
  675. !lotQrPayload
  676. ? "此列無法取得 QR payload(需 stockInLineId)"
  677. : disableScanPick
  678. ? "此出庫行已掃碼或已完成,無法顯示 QR"
  679. : undefined
  680. }
  681. disabled={
  682. !canShowLotQr || !lotQrPayload || isPrinting
  683. }
  684. onClick={() =>
  685. setQrVisibleLotLineId((prev) =>
  686. prev === lot.inventoryLotLineId
  687. ? null
  688. : lot.inventoryLotLineId,
  689. )
  690. }
  691. >
  692. 顯示 QR
  693. </Button>
  694. ) : null}
  695. </Stack>
  696. {qrVisibleLotLineId === lot.inventoryLotLineId &&
  697. lotQrPayload ? (
  698. <Box
  699. sx={{
  700. mt: 1.5,
  701. ml: "auto",
  702. p: 1.5,
  703. borderRadius: 1,
  704. border: "1px dashed",
  705. borderColor: "divider",
  706. textAlign: "center",
  707. minWidth: 220,
  708. }}
  709. >
  710. <QRCodeSVG
  711. value={JSON.stringify(lotQrPayload)}
  712. size={160}
  713. includeMargin
  714. />
  715. </Box>
  716. ) : null}
  717. </Box>
  718. );
  719. })}
  720. </Stack>
  721. )}
  722. </Box>
  723. )}
  724. {!analysis && !analysisLoading && (
  725. <Typography variant="body2" color="text.secondary">
  726. {onWorkbenchScanPick
  727. ? "沒有任何批號可列印標籤"
  728. : ""}
  729. </Typography>
  730. )}
  731. </Stack>
  732. </DialogContent>
  733. <DialogActions>
  734. <Button onClick={onClose}>關閉</Button>
  735. </DialogActions>
  736. <Snackbar
  737. open={snackbar.open}
  738. autoHideDuration={3500}
  739. onClose={() => setSnackbar((s) => ({ ...s, open: false }))}
  740. message={snackbar.message}
  741. anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
  742. />
  743. </Dialog>
  744. );
  745. };
  746. export default WorkbenchLotLabelPrintModal;