FPSMS-frontend
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 

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