FPSMS-frontend
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

1331 lignes
49 KiB

  1. "use client";
  2. import {
  3. fetchPoWithStockInLines,
  4. PoResult,
  5. PurchaseOrderLine,
  6. } from "@/app/api/po";
  7. import {
  8. Box,
  9. Button,
  10. ButtonProps,
  11. Collapse,
  12. Grid,
  13. IconButton,
  14. Paper,
  15. Stack,
  16. Tab,
  17. Table,
  18. TableBody,
  19. TableCell,
  20. TableContainer,
  21. TableHead,
  22. TableRow,
  23. Tabs,
  24. TabsProps,
  25. TextField,
  26. Typography,
  27. Checkbox,
  28. FormControlLabel,
  29. Card,
  30. CardContent,
  31. Radio,
  32. alpha,
  33. Dialog,
  34. DialogActions,
  35. DialogContent,
  36. DialogTitle,
  37. } from "@mui/material";
  38. import { useTranslation } from "react-i18next";
  39. import { submitDialogWithWarning } from "../Swal/CustomAlerts";
  40. import PrinterSelect from "@/components/common/PrinterSelect";
  41. // import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid";
  42. import {
  43. GridColDef,
  44. GridRowId,
  45. GridRowModel,
  46. useGridApiRef,
  47. } from "@mui/x-data-grid";
  48. import {
  49. checkPolAndCompletePo,
  50. fetchPoInClient,
  51. fetchPoSummariesClient,
  52. startPo,
  53. } from "@/app/api/po/actions";
  54. import {
  55. createStockInLine
  56. } from "@/app/api/stockIn/actions";
  57. import {
  58. useCallback,
  59. useContext,
  60. useEffect,
  61. useMemo,
  62. useState,
  63. } from "react";
  64. import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
  65. import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
  66. import PoInputGrid from "./PoInputGrid";
  67. // import { QcItemWithChecks } from "@/app/api/qc";
  68. import { useRouter, useSearchParams, usePathname } from "next/navigation";
  69. import { WarehouseResult } from "@/app/api/warehouse";
  70. import { calculateWeight, dateStringToDayjs, dayjsToDateString, OUTPUT_DATE_FORMAT, outputDateStringToInputDateString, returnWeightUnit } from "@/app/utils/formatUtil";
  71. import { CameraContext } from "../Cameras/CameraProvider";
  72. import QrModal from "./QrModal";
  73. import { PlayArrow } from "@mui/icons-material";
  74. import DoneIcon from "@mui/icons-material/Done";
  75. import { downloadFile, getCustomWidth } from "@/app/utils/commonUtil";
  76. import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
  77. import { arrayToDateString } from "@/app/utils/formatUtil";
  78. import { List, ListItem, ListItemButton, ListItemText, Divider } from "@mui/material";
  79. import { Controller, FormProvider, useForm } from "react-hook-form";
  80. import dayjs, { Dayjs } from "dayjs";
  81. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  82. import { DatePicker, LocalizationProvider, zhHK } from "@mui/x-date-pickers";
  83. import LoadingComponent from "../General/LoadingComponent";
  84. import { getMailTemplatePdfForStockInLine } from "@/app/api/mailTemplate/actions";
  85. import { PrinterCombo } from "@/app/api/settings/printer";
  86. import { EscalationCombo } from "@/app/api/user";
  87. import { StockInLine } from "@/app/api/stockIn";
  88. import { printQrCodeForSil } from "@/app/api/stockIn/actions";
  89. import { useSession } from "next-auth/react";
  90. import { AUTH } from "@/authorities";
  91. //import { useRouter } from "next/navigation";
  92. type Props = {
  93. po: PoResult;
  94. // qc: QcItemWithChecks[];
  95. warehouse: WarehouseResult[];
  96. printerCombo: PrinterCombo[];
  97. };
  98. /** PO stock-in lines still in pre-complete workflow (align with nav alert: pending / receiving). */
  99. const PURCHASE_STOCK_IN_ALERT_STATUSES = new Set(["pending", "receiving"]);
  100. /** Sum of put-away in stock units (matches StockInForm「已上架數量」stockQty). */
  101. function totalPutAwayStockQtyForPol(row: PurchaseOrderLine): number {
  102. return row.stockInLine
  103. .filter((sil) => sil.purchaseOrderLineId === row.id)
  104. .reduce((acc, sil) => {
  105. const lineSum =
  106. sil.putAwayLines?.reduce(
  107. (s, p) => s + Number(p.stockQty ?? p.qty ?? 0),
  108. 0,
  109. ) ?? 0;
  110. return acc + lineSum;
  111. }, 0);
  112. }
  113. /** POL order demand in stock units (same basis as PoDetail processed / backend PO detail). */
  114. function polOrderStockQty(row: PurchaseOrderLine): number {
  115. return Number(row.stockUom?.stockQty ?? row.qty ?? 0);
  116. }
  117. function purchaseOrderLineHasIncompleteStockIn(row: PurchaseOrderLine): boolean {
  118. const orderStock = polOrderStockQty(row);
  119. const putAway = totalPutAwayStockQtyForPol(row);
  120. if (orderStock > 0 && putAway >= orderStock) {
  121. return false;
  122. }
  123. return row.stockInLine
  124. .filter((sil) => sil.purchaseOrderLineId === row.id)
  125. .some((sil) =>
  126. PURCHASE_STOCK_IN_ALERT_STATUSES.has((sil.status ?? "").toLowerCase().trim()),
  127. );
  128. }
  129. type EntryError =
  130. | {
  131. [field in keyof StockInLine]?: string;
  132. }
  133. | undefined;
  134. // type PolRow = TableRow<Partial<StockInLine>, EntryError>;
  135. const PoSearchList: React.FC<{
  136. poList: PoResult[];
  137. selectedPoId: number;
  138. onSelect: (po: PoResult) => void;
  139. loading?: boolean;
  140. }> = ({ poList, selectedPoId, onSelect, loading = false }) => {
  141. const { t } = useTranslation(["purchaseOrder", "dashboard"]);
  142. const [searchTerm, setSearchTerm] = useState('');
  143. const filteredPoList = useMemo(() => {
  144. if (searchTerm.trim() === '') {
  145. return poList;
  146. }
  147. return poList.filter(poItem =>
  148. poItem.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
  149. poItem.supplier?.toLowerCase().includes(searchTerm.toLowerCase()) ||
  150. t(`${poItem.status.toLowerCase()}`).toLowerCase().includes(searchTerm.toLowerCase())
  151. );
  152. }, [poList, searchTerm, t]);
  153. return (
  154. <Paper
  155. sx={{
  156. p: 2,
  157. minWidth: "300px",
  158. height: "100%",
  159. display: "flex",
  160. flexDirection: "column",
  161. overflow: "hidden",
  162. }}
  163. >
  164. <Typography variant="h6" gutterBottom>
  165. {t("Purchase Order")}
  166. </Typography>
  167. <TextField
  168. label={t("Search")}
  169. variant="outlined"
  170. size="small"
  171. fullWidth
  172. value={searchTerm}
  173. onChange={(e) => setSearchTerm(e.target.value)}
  174. sx={{ mb: 2 }}
  175. InputProps={{
  176. startAdornment: (
  177. <Typography variant="body2" color="text.secondary" sx={{ mr: 1 }}>
  178. </Typography>
  179. ),
  180. }}
  181. />
  182. <Box sx={{ flex: 1, overflow: "auto" }}>
  183. {loading ? (
  184. <LoadingComponent />
  185. ) : filteredPoList.length > 0 ? (
  186. <List dense sx={{ width: "100%" }}>
  187. {filteredPoList.map((poItem, index) => (
  188. <div key={poItem.id}>
  189. <ListItem disablePadding sx={{ width: "100%" }}>
  190. <ListItemButton
  191. selected={selectedPoId === poItem.id}
  192. onClick={() => onSelect(poItem)}
  193. sx={{
  194. width: "100%",
  195. "&.Mui-selected": {
  196. backgroundColor: "primary.light",
  197. "&:hover": {
  198. backgroundColor: "primary.light",
  199. },
  200. },
  201. }}
  202. >
  203. <ListItemText
  204. primary={
  205. <Typography variant="body2" sx={{ wordBreak: "break-all" }}>
  206. {poItem.code}
  207. </Typography>
  208. }
  209. secondary={
  210. <Typography variant="caption" color="text.secondary">
  211. {t(`${poItem.status.toLowerCase()}`)}
  212. </Typography>
  213. }
  214. />
  215. </ListItemButton>
  216. </ListItem>
  217. {index < filteredPoList.length - 1 && <Divider />}
  218. </div>
  219. ))}
  220. </List>
  221. ) : (
  222. <Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
  223. {searchTerm.trim()
  224. ? t("No purchase orders match your search", { defaultValue: "沒有符合搜索的採購單" })
  225. : t("No purchase orders to show", { defaultValue: "沒有可顯示的採購單" })}
  226. </Typography>
  227. )}
  228. </Box>
  229. {searchTerm && (
  230. <Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: "block" }}>
  231. {`${t("Found")} ${filteredPoList.length} ${t("Purchase Order")}`}
  232. {/* {`${t("Found")} ${filteredPoList.length} of ${poList.length} ${t("Item")}`} */}
  233. </Typography>
  234. )}
  235. </Paper>
  236. );
  237. };
  238. interface PolInputResult {
  239. lotNo: string,
  240. dnQty: string,
  241. }
  242. /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.0 | 2026-08-06 */
  243. const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
  244. const cameras = useContext(CameraContext);
  245. const { data: session } = useSession();
  246. const canSeeStockInReminders = useMemo(() => {
  247. const set = new Set((session?.user?.abilities ?? []).map((a) => String(a).trim()));
  248. return set.has(AUTH.TESTING) || set.has(AUTH.ADMIN) || set.has(AUTH.STOCK);
  249. }, [session?.user?.abilities]);
  250. // console.log(cameras);
  251. const { t } = useTranslation("purchaseOrder");
  252. const apiRef = useGridApiRef();
  253. const [purchaseOrder, setPurchaseOrder] = useState({ ...po });
  254. const [rows, setRows] = useState<PurchaseOrderLine[]>(
  255. purchaseOrder.pol || [],
  256. );
  257. const [polInputList, setPolInputList] = useState<Record<number, PolInputResult>>({})
  258. const PO_DETAIL_SELECTION_KEY = "po-detail-selection";
  259. useEffect(() => {
  260. setPolInputList((prev) => {
  261. const next: Record<number, PolInputResult> = {};
  262. (purchaseOrder.pol ?? []).forEach((pol) => {
  263. next[pol.id] = prev[pol.id] ?? {
  264. lotNo: "",
  265. dnQty: "",
  266. };
  267. });
  268. return next;
  269. });
  270. }, [purchaseOrder.pol]);
  271. useEffect(() => {
  272. try {
  273. const raw = sessionStorage.getItem("po-detail-selection");
  274. if (raw) {
  275. const parsed = JSON.parse(raw) as { id: number; code: string; status: string; supplier: string | null }[];
  276. if (Array.isArray(parsed) && parsed.length > 0) {
  277. setPoList(parsed as PoResult[]);
  278. sessionStorage.removeItem("po-detail-selection"); // 可选:用一次就删,避免下次从别处进还看到旧数据
  279. }
  280. }
  281. } catch (e) {
  282. console.warn("sessionStorage getItem/parse failed", e);
  283. }
  284. }, []);
  285. const pathname = usePathname()
  286. const searchParams = useSearchParams();
  287. const [selectedRow, setSelectedRow] = useState<PurchaseOrderLine | null>(null);
  288. const [stockInLine, setStockInLine] = useState<StockInLine[]>([]);
  289. const [processedQty, setProcessedQty] = useState(0);
  290. useEffect(() => {
  291. const polIdParam = searchParams.get("polId");
  292. if (!polIdParam || rows.length === 0) return;
  293. const match = rows.find((r) => r.id.toString() === polIdParam);
  294. if (match) {
  295. setSelectedRow(match);
  296. setStockInLine(match.stockInLine);
  297. setProcessedQty(match.processed);
  298. }
  299. }, [rows, searchParams]);
  300. const router = useRouter();
  301. const [poList, setPoList] = useState<PoResult[]>(() => [po]);
  302. const [isPoListLoading, setIsPoListLoading] = useState(false);
  303. const [selectedPoId, setSelectedPoId] = useState(po.id);
  304. const [focusField, setFocusField] = useState<HTMLInputElement>();
  305. const currentPoId = searchParams.get('id');
  306. const selectedIdsParam = searchParams.get('selectedIds');
  307. // const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
  308. const dnFormProps = useForm({
  309. defaultValues: {
  310. dnNo: '',
  311. receiptDate: dayjsToDateString(dayjs())
  312. }
  313. })
  314. const labelPrinters = useMemo(() => {
  315. return (printerCombo ?? []).filter((p) => {
  316. const typeText = String(p.type ?? "").trim().toLowerCase();
  317. if (typeText === "label") return true;
  318. // Backward compatibility for legacy rows without a reliable type value.
  319. const text = `${p.label ?? ""} ${p.name ?? ""} ${p.code ?? ""}`.toLowerCase();
  320. return text.includes("label") || text.includes("標籤");
  321. });
  322. }, [printerCombo]);
  323. const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | undefined>(
  324. labelPrinters?.[0],
  325. );
  326. useEffect(() => {
  327. // If options change, keep selection valid and prefer first Label printer.
  328. if (!selectedPrinter || !labelPrinters.some((p) => p.id === selectedPrinter.id)) {
  329. setSelectedPrinter(labelPrinters[0]);
  330. }
  331. }, [labelPrinters, selectedPrinter]);
  332. const [printQty, setPrintQty] = useState(1);
  333. const [printDialogOpen, setPrintDialogOpen] = useState(false);
  334. const [isBulkPrinting, setIsBulkPrinting] = useState(false);
  335. const [printStatusFilter, setPrintStatusFilter] = useState({
  336. received: true,
  337. completed: false,
  338. });
  339. const [selectedPrintSilIds, setSelectedPrintSilIds] = useState<Set<number>>(
  340. () => new Set(),
  341. );
  342. const eligiblePrintSils = useMemo(() => {
  343. const statusSet = new Set<string>();
  344. if (printStatusFilter.received) statusSet.add("received");
  345. if (printStatusFilter.completed) statusSet.add("completed");
  346. const pols = purchaseOrder.pol ?? [];
  347. return pols
  348. .flatMap((pol) => pol.stockInLine ?? [])
  349. .filter((sil) => statusSet.has((sil.status ?? "").toLowerCase().trim()));
  350. }, [purchaseOrder.pol, printStatusFilter.completed, printStatusFilter.received]);
  351. const openPrintDialog = useCallback(() => {
  352. setSelectedPrintSilIds(new Set());
  353. setPrintDialogOpen(true);
  354. }, []);
  355. const closePrintDialog = useCallback(() => {
  356. if (isBulkPrinting) return;
  357. setPrintDialogOpen(false);
  358. }, [isBulkPrinting]);
  359. const togglePrintSilSelection = useCallback((id: number, checked: boolean) => {
  360. setSelectedPrintSilIds((prev) => {
  361. const next = new Set(prev);
  362. if (checked) next.add(id);
  363. else next.delete(id);
  364. return next;
  365. });
  366. }, []);
  367. const setAllVisiblePrintSilsSelected = useCallback((checked: boolean) => {
  368. setSelectedPrintSilIds(() => {
  369. if (!checked) return new Set();
  370. return new Set(eligiblePrintSils.map((s) => s.id));
  371. });
  372. }, [eligiblePrintSils]);
  373. const handleBulkPrint = useCallback(async () => {
  374. if (!selectedPrinter) {
  375. alert("請先選擇印表機");
  376. return;
  377. }
  378. if (!Number.isFinite(printQty) || printQty <= 0) {
  379. alert("列印數量必須大於 0");
  380. return;
  381. }
  382. const ids = Array.from(selectedPrintSilIds.values());
  383. if (ids.length <= 0) {
  384. alert("請先選擇要列印的項目");
  385. return;
  386. }
  387. setIsBulkPrinting(true);
  388. try {
  389. for (const id of ids) {
  390. await printQrCodeForSil({
  391. stockInLineId: id,
  392. printerId: selectedPrinter.id,
  393. printQty,
  394. });
  395. }
  396. setPrintDialogOpen(false);
  397. } finally {
  398. setIsBulkPrinting(false);
  399. }
  400. }, [printQty, selectedPrinter, selectedPrintSilIds]);
  401. /** Only loads sidebar list when `selectedIds` is in the URL; otherwise show current PO only (no /po/list fetch). */
  402. const fetchPoList = useCallback(async () => {
  403. if (!selectedIdsParam) return;
  404. setIsPoListLoading(true);
  405. try {
  406. const MAX_IDS = 20; // 一次最多加载 20 个,防止卡死
  407. const allIds = selectedIdsParam
  408. .split(',')
  409. .map((id) => parseInt(id))
  410. .filter((id) => !Number.isNaN(id));
  411. const limitedIds = allIds.slice(0, MAX_IDS);
  412. if (allIds.length > MAX_IDS) {
  413. console.warn(`selectedIds too many (${allIds.length}), only loading first ${MAX_IDS}.`);
  414. }
  415. const result = await fetchPoSummariesClient(limitedIds);
  416. setPoList(result as any);
  417. } catch (error) {
  418. console.error("Failed to fetch PO list:", error);
  419. } finally {
  420. setIsPoListLoading(false);
  421. }
  422. }, [selectedIdsParam]);
  423. const fetchPoDetail = useCallback(async (poId: string, preserveDnNo: boolean = false, preferredPolId?: number) => {
  424. try {
  425. const result = await fetchPoInClient(parseInt(poId));
  426. if (result) {
  427. console.log("%c Fetched PO:", "color:orange", result);
  428. setPurchaseOrder(result);
  429. const currentDnNo = preserveDnNo ? dnFormProps.getValues("dnNo") : "";
  430. dnFormProps.reset({
  431. dnNo: currentDnNo,
  432. receiptDate: dayjsToDateString(dayjs()),
  433. });
  434. setRows(result.pol || []);
  435. if (result.pol && result.pol.length > 0) {
  436. const targetPolId = preferredPolId ?? selectedRow?.id;
  437. const targetPol =
  438. result.pol.find((p) => p.id === targetPolId) ?? result.pol[0];
  439. setSelectedRow(targetPol);
  440. setStockInLine(targetPol.stockInLine);
  441. setProcessedQty(targetPol.processed);
  442. }
  443. // if (focusField) {console.log(focusField);focusField.focus();}
  444. }
  445. } catch (error) {
  446. console.error("Failed to fetch PO detail:", error);
  447. }
  448. }, [selectedRow, selectedPoId]);
  449. const handlePoSelect = useCallback(
  450. async (selectedPo: PoResult) => {
  451. if (selectedPo.id === selectedPoId) return;
  452. setSelectedPoId(selectedPo.id);
  453. await fetchPoDetail(selectedPo.id.toString());
  454. const newSelectedIds = selectedIdsParam || selectedPo.id.toString();
  455. const newUrl = `/po/edit?id=${selectedPo.id}&start=true&selectedIds=${newSelectedIds}`;
  456. if (pathname + searchParams.toString() !== newUrl) {
  457. router.replace(newUrl, { scroll: false });
  458. }
  459. },
  460. [selectedPoId, fetchPoDetail, selectedIdsParam, pathname, searchParams, router]
  461. );
  462. useEffect(() => {
  463. if (currentPoId && currentPoId !== selectedPoId.toString()) {
  464. setSelectedPoId(parseInt(currentPoId));
  465. fetchPoDetail(currentPoId);
  466. }
  467. }, [currentPoId, fetchPoDetail]);
  468. useEffect(() => {
  469. if (selectedIdsParam) {
  470. void fetchPoList();
  471. }
  472. }, [selectedIdsParam, fetchPoList]);
  473. useEffect(() => {
  474. if (selectedIdsParam) return;
  475. setPoList([purchaseOrder]);
  476. }, [selectedIdsParam, purchaseOrder]);
  477. useEffect(() => {
  478. if (currentPoId) {
  479. setSelectedPoId(parseInt(currentPoId));
  480. }
  481. }, [currentPoId]);
  482. const removeParam = (paramToRemove: string) => {
  483. const newParams = new URLSearchParams(searchParams.toString());
  484. newParams.delete(paramToRemove);
  485. window.history.replaceState({}, '', `${window.location.pathname}?${newParams}`);
  486. };
  487. const handleCompletePo = useCallback(async () => {
  488. const checkRes = await checkPolAndCompletePo(purchaseOrder.id);
  489. console.log(checkRes);
  490. const newPo = await fetchPoInClient(purchaseOrder.id);
  491. setPurchaseOrder(newPo);
  492. }, [purchaseOrder.id]);
  493. const handleStartPo = useCallback(async () => {
  494. const startRes = await startPo(purchaseOrder.id);
  495. console.log(startRes);
  496. const newPo = await fetchPoInClient(purchaseOrder.id);
  497. setPurchaseOrder(newPo);
  498. }, [purchaseOrder.id]);
  499. const handleMailTemplateForStockInLine = useCallback(async (stockInLineId: number) => {
  500. const response = await getMailTemplatePdfForStockInLine(stockInLineId)
  501. if (response) {
  502. downloadFile(new Uint8Array(response.blobValue), response.filename);
  503. }
  504. }, [])
  505. useEffect(() => {
  506. setRows(purchaseOrder.pol || []);
  507. }, [purchaseOrder]);
  508. // useEffect(() => {
  509. // setStockInLine([])
  510. // }, []);
  511. function Row(props: { row: PurchaseOrderLine }) {
  512. const { row } = props;
  513. // const [firstReceiveQty, setFirstReceiveQty] = useState<number>()
  514. // const [secondReceiveQty, setSecondReceiveQty] = useState<number>()
  515. // const [open, setOpen] = useState(false);
  516. const [processedQty, setProcessedQty] = useState(row.processed);
  517. const [currStatus, setCurrStatus] = useState(row.status);
  518. const [lotNoInput, setLotNoInput] = useState(polInputList[row.id]?.lotNo ?? "");
  519. const [dnQtyInput, setDnQtyInput] = useState(polInputList[row.id]?.dnQty ?? "");
  520. // const [stockInLine, setStockInLine] = useState(row.stockInLine);
  521. const totalWeight = useMemo(
  522. () => calculateWeight(row.qty, row.uom),
  523. [row.qty, row.uom],
  524. );
  525. const weightUnit = useMemo(
  526. () => returnWeightUnit(row.uom),
  527. [row.uom],
  528. );
  529. useEffect(() => {
  530. const polId = searchParams.get("polId") != null ? parseInt(searchParams.get("polId")!) : null
  531. if (polId) {
  532. setStockInLine(rows.find((r) => r.id == polId)!.stockInLine)
  533. }
  534. }, []);
  535. useEffect(() => {
  536. // `processedQty` comes from putAwayLines (stock unit).
  537. // After the fix, `row.qty` is qtyM18 (M18 unit), so compare using stockUom demand.
  538. const targetStockQty = Number(row.stockUom?.stockQty ?? row.qty ?? 0);
  539. if (targetStockQty > 0 && processedQty >= targetStockQty) {
  540. setCurrStatus("completed".toUpperCase());
  541. } else if (processedQty > 0) {
  542. setCurrStatus("receiving".toUpperCase());
  543. } else {
  544. setCurrStatus("pending".toUpperCase());
  545. }
  546. }, [processedQty, row.qty, row.stockUom?.stockQty]);
  547. useEffect(() => {
  548. setLotNoInput(polInputList[row.id]?.lotNo ?? "");
  549. setDnQtyInput(polInputList[row.id]?.dnQty ?? "");
  550. }, [polInputList, row.id]);
  551. const handleRowSelect = () => {
  552. // setSelectedRowId(row.id);
  553. setSelectedRow(row);
  554. setStockInLine(row.stockInLine);
  555. setProcessedQty(row.processed);
  556. };
  557. const changeStockInLines = useCallback(
  558. (id: number) => {
  559. //rows = purchaseOrderLine
  560. const target = rows.find((r) => r.id === id)
  561. const stockInLine = target!.stockInLine
  562. setStockInLine(stockInLine)
  563. setSelectedRow(target!)
  564. // console.log(pathname)
  565. // router.replace(`/po/edit?id=${item.poId}&polId=${item.polId}&stockInLineId=${item.stockInLineId}`);
  566. },
  567. [rows]
  568. );
  569. const handleStart = useCallback(
  570. () => {
  571. const orderQty = Number(row?.qty) ?? 0;
  572. const acceptedQty = Number(dnQtyInput.trim());
  573. if (isNaN(acceptedQty) || acceptedQty <= 0) {
  574. alert("來貨數量必須大於0!");
  575. return;
  576. }
  577. const doSubmit = () => {
  578. setTimeout(async () => {
  579. const currentDnNo = dnFormProps.watch("dnNo");
  580. const postData = {
  581. dnNo: dnFormProps.watch("dnNo"),
  582. receiptDate: outputDateStringToInputDateString(dnFormProps.watch("receiptDate")),
  583. itemId: row.itemId,
  584. itemNo: row.itemNo,
  585. itemName: row.itemName,
  586. purchaseOrderLineId: row.id,
  587. acceptedQty: acceptedQty,
  588. productLotNo: lotNoInput || "",
  589. };
  590. const res = await createStockInLine(postData);
  591. if (res) {
  592. setLotNoInput("");
  593. setDnQtyInput("");
  594. setPolInputList((prev) => ({
  595. ...prev,
  596. [row.id]: { lotNo: "", dnQty: "" },
  597. }));
  598. setSelectedRow(row);
  599. fetchPoDetail(selectedPoId.toString(), true, row.id);
  600. }
  601. console.log(res);
  602. }, 200);
  603. };
  604. const exceedOrderBy10Percent = orderQty > 0 && acceptedQty > orderQty * 1.1;
  605. if (exceedOrderBy10Percent) {
  606. submitDialogWithWarning(doSubmit, t, {
  607. title: t("Confirm submit"),
  608. html: t("This batch quantity exceeds order quantity. Do you still want to submit?"),
  609. confirmButtonText: t("Submit"),
  610. });
  611. } else {
  612. doSubmit();
  613. }
  614. },
  615. [dnQtyInput, row, dnFormProps, selectedPoId, fetchPoDetail, t, lotNoInput],
  616. );
  617. const syncRowInputToParent = useCallback((lotNo: string, dnQty: string) => {
  618. setPolInputList((prev) => {
  619. const current = prev[row.id] ?? { lotNo: "", dnQty: "" };
  620. if (current.lotNo === lotNo && current.dnQty === dnQty) return prev;
  621. return {
  622. ...prev,
  623. [row.id]: { lotNo, dnQty },
  624. };
  625. });
  626. }, [row.id]);
  627. // const [focusField, setFocusField] = useState<HTMLInputElement>();
  628. // 本批收貨數量(訂單單位): 使用者在該行輸入的 dnQty
  629. const batchPurchaseQty = Number(dnQtyInput.trim()) || 0;
  630. // 已來貨總數(庫存單位): 同一 POL 底下所有 stock_in_line.acceptedQty 的合計
  631. const totalStockReceived = row.stockInLine
  632. .filter((sil) => sil.purchaseOrderLineId === row.id)
  633. .reduce((acc, cur) => acc + (cur.acceptedQty ?? 0), 0);
  634. const receivedTotalText = decimalFormatter.format(totalStockReceived);
  635. const highlightColor =
  636. Number(receivedTotalText.replace(/,/g, "")) <= 0 ? "red" : "inherit";
  637. const needsStockInAttention =
  638. canSeeStockInReminders && purchaseOrderLineHasIncompleteStockIn(row);
  639. return (
  640. <>
  641. <TableRow
  642. hover
  643. title={
  644. needsStockInAttention
  645. ? "採購入庫未完成:此採購明細尚有入庫單為「待處理」或「收貨中」,請於下方完成入庫。"
  646. : undefined
  647. }
  648. sx={{
  649. "& > *": { borderBottom: "unset" },
  650. color: "black",
  651. ...(needsStockInAttention
  652. ? (theme) => ({
  653. boxShadow: `inset 4px 0 0 ${theme.palette.error.main}`,
  654. backgroundColor: alpha(theme.palette.error.main, 0.07),
  655. })
  656. : {}),
  657. }}
  658. onClick={() => changeStockInLines(row.id)}
  659. >
  660. {/* <TableCell>
  661. <IconButton
  662. disabled={purchaseOrder.status.toLowerCase() === "pending"}
  663. aria-label="expand row"
  664. size="small"
  665. onClick={() => setOpen(!open)}
  666. >
  667. {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
  668. </IconButton>
  669. </TableCell> */}
  670. <TableCell align="center" sx={{ width: "60px", position: "relative" }}>
  671. {needsStockInAttention && (
  672. <Box
  673. component="span"
  674. aria-hidden
  675. sx={{
  676. position: "absolute",
  677. top: 6,
  678. left: 8,
  679. width: 10,
  680. height: 10,
  681. borderRadius: "50%",
  682. bgcolor: "error.main",
  683. border: "2px solid",
  684. borderColor: "background.paper",
  685. boxShadow: (theme) => `0 0 0 1px ${alpha(theme.palette.error.main, 0.45)}`,
  686. zIndex: 1,
  687. }}
  688. />
  689. )}
  690. <Radio
  691. checked={selectedRow?.id === row.id}
  692. // onChange={handleRowSelect}
  693. // onClick={(e) => e.stopPropagation()}
  694. />
  695. </TableCell>
  696. <TableCell align="left" sx={{ width: 88, maxWidth: 88, px: 1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} title={row.itemNo}>
  697. {row.itemNo}
  698. </TableCell>
  699. <TableCell align="left" sx={{ width: 100, maxWidth: 100, px: 1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} title={row.itemName}>
  700. {row.itemName}
  701. </TableCell>
  702. <TableCell align="right">{integerFormatter.format(row.qty)}</TableCell>
  703. <TableCell align="right">{integerFormatter.format(row.processed)}</TableCell>
  704. <TableCell align="left">{row.uom?.udfudesc}</TableCell>
  705. {/* <TableCell align="right">{decimalFormatter.format(row.stockUom.stockQty)}</TableCell> */}
  706. {/* <TableCell sx={{ color: highlightColor}} align="right">{receivedTotal}</TableCell> */}
  707. <TableCell sx={{ color: highlightColor }} align="right">
  708. {decimalFormatter.format(totalStockReceived)}
  709. </TableCell>
  710. <TableCell sx={{ color: highlightColor}} align="left">{row.stockUom.stockUomDesc}</TableCell>
  711. {/* <TableCell align="right">
  712. {decimalFormatter.format(totalWeight)} {weightUnit}
  713. </TableCell> */}
  714. {/* <TableCell align="left">{weightUnit}</TableCell> */}
  715. {/* <TableCell align="right">{decimalFormatter.format(row.price)}</TableCell> */}
  716. {/* <TableCell align="left">{row.expiryDate}</TableCell> */}
  717. <TableCell sx={{ color: highlightColor}} align="left">{t(`${row.status.toLowerCase()}`)}</TableCell>
  718. {/* <TableCell sx={{ color: highlightColor}} align="left">{t(`${currStatus.toLowerCase()}`)}</TableCell> */}
  719. {/* <TableCell align="right">{integerFormatter.format(row.receivedQty)}</TableCell> */}
  720. <TableCell align="center">
  721. <TextField
  722. id="lotNo"
  723. label="輸入貨品批號"
  724. type="text" // Use type="text" to allow validation in the change handler
  725. variant="outlined"
  726. value={lotNoInput}
  727. onChange={(e) => setLotNoInput(e.target.value)}
  728. onBlur={() => syncRowInputToParent(lotNoInput, dnQtyInput)}
  729. onClick={(e) => e.stopPropagation()}
  730. // onFocus={(e) => {setFocusField(e.target as HTMLInputElement);}}
  731. />
  732. </TableCell>
  733. <TableCell align="center">
  734. <TextField
  735. id="dnQty"
  736. label="此批送貨數量"
  737. type="text" // Use type="text" to allow validation in the change handler
  738. variant="outlined"
  739. value={dnQtyInput}
  740. onChange={(e) => setDnQtyInput(e.target.value)}
  741. onBlur={() => syncRowInputToParent(lotNoInput, dnQtyInput)}
  742. onClick={(e) => e.stopPropagation()}
  743. InputProps={{
  744. inputProps: {
  745. min: 0, // Optional: set a minimum value
  746. step: "any",
  747. inputMode: "decimal",
  748. }
  749. }}
  750. />
  751. </TableCell>
  752. <TableCell align="center">
  753. <Button
  754. variant="contained"
  755. onClick={(e) => {
  756. e.stopPropagation();
  757. handleStart();
  758. }}
  759. >
  760. {t("submit")}
  761. </Button>
  762. </TableCell>
  763. </TableRow>
  764. {/* <TableRow> */}
  765. {/* <TableCell /> */}
  766. {/* <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={12}> */}
  767. {/* <Collapse in={true} timeout="auto" unmountOnExit> */}
  768. {/* <Collapse in={open} timeout="auto" unmountOnExit> */}
  769. {/* <Table>
  770. <TableBody>
  771. <TableRow>
  772. <TableCell align="right">
  773. <Box>
  774. <PoInputGrid
  775. qc={qc}
  776. setRows={setRows}
  777. stockInLine={stockInLine}
  778. setStockInLine={setStockInLine}
  779. setProcessedQty={setProcessedQty}
  780. itemDetail={row}
  781. warehouse={warehouse}
  782. />
  783. </Box>
  784. </TableCell>
  785. </TableRow>
  786. </TableBody>
  787. </Table> */}
  788. {/* </Collapse> */}
  789. {/* </TableCell> */}
  790. {/* </TableRow> */}
  791. </>
  792. );
  793. }
  794. // ROW END
  795. const [tabIndex, setTabIndex] = useState(0);
  796. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  797. (_e, newValue) => {
  798. setTabIndex(newValue);
  799. },
  800. [],
  801. );
  802. const [isOpenScanner, setOpenScanner] = useState(false);
  803. // const testing = useCallback(() => {
  804. // // setOpenScanner(true);
  805. // const newParams = new URLSearchParams(searchParams.toString());
  806. // console.log(pathname)
  807. // }, [pathname, router, searchParams]);
  808. const onOpenScanner = useCallback(() => {
  809. setOpenScanner(true);
  810. }, []);
  811. const onCloseScanner = useCallback(() => {
  812. setOpenScanner(false);
  813. }, []);
  814. const [itemInfo, setItemInfo] = useState<
  815. StockInLine & { warehouseId?: number }
  816. >();
  817. const [putAwayOpen, setPutAwayOpen] = useState(false);
  818. // const [scannedInfo, setScannedInfo] = useState<QrCodeInfo>({} as QrCodeInfo);
  819. const closePutAwayModal = useCallback(() => {
  820. setPutAwayOpen(false);
  821. setItemInfo(undefined);
  822. }, []);
  823. const openPutAwayModal = useCallback(() => {
  824. setPutAwayOpen(true);
  825. }, []);
  826. const buttonData = useMemo(() => {
  827. switch (purchaseOrder.status.toLowerCase()) {
  828. case "pending":
  829. return {
  830. buttonName: "start",
  831. title: t("Do you want to start?"),
  832. confirmButtonText: t("Start"),
  833. successTitle: t("Start Success"),
  834. errorTitle: t("Start Fail"),
  835. buttonText: t("Start PO"),
  836. buttonIcon: <PlayArrow />,
  837. buttonColor: "success",
  838. disabled: false,
  839. onClick: handleStartPo,
  840. };
  841. case "receiving":
  842. return {
  843. buttonName: "complete",
  844. title: t("Do you want to complete?"),
  845. confirmButtonText: t("Complete"),
  846. successTitle: t("Complete Success"),
  847. errorTitle: t("Complete Fail"),
  848. buttonText: t("Complete PO"),
  849. buttonIcon: <DoneIcon />,
  850. buttonColor: "info",
  851. disabled: false,
  852. onClick: handleCompletePo,
  853. };
  854. default:
  855. return {
  856. buttonName: "complete",
  857. title: t("Do you want to complete?"),
  858. confirmButtonText: t("Complete"),
  859. successTitle: t("Complete Success"),
  860. errorTitle: t("Complete Fail"),
  861. buttonText: t("Complete PO"),
  862. buttonIcon: <DoneIcon />,
  863. buttonColor: "info",
  864. disabled: true,
  865. };
  866. // break;
  867. }
  868. }, [purchaseOrder.status, t, handleStartPo, handleCompletePo]);
  869. const FIRST_IN_FIELD = "firstInQty"
  870. const SECOND_IN_FIELD = "secondInQty"
  871. const renderFieldCondition = useCallback((field: "firstInQty" | "secondInQty"): boolean => {
  872. switch (field) {
  873. case FIRST_IN_FIELD:
  874. return true;
  875. case SECOND_IN_FIELD:
  876. return true;
  877. default:
  878. return false; // Default case
  879. }
  880. }, []);
  881. const handleDatePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => {
  882. if (value != null) {
  883. const updatedValue = dayjsToDateString(value)
  884. onChange(updatedValue)
  885. } else {
  886. onChange(value)
  887. }
  888. }, [])
  889. const fillTodayLotNo = useCallback(() => {
  890. const today = dayjs().format("YYYYMMDD");
  891. setPolInputList((prev) => {
  892. const next: Record<number, PolInputResult> = { ...prev };
  893. (rows ?? []).forEach((r) => {
  894. const current = next[r.id] ?? { lotNo: "", dnQty: "" };
  895. const lotNo = (current.lotNo ?? "").trim();
  896. if (!lotNo) {
  897. next[r.id] = { ...current, lotNo: today };
  898. }
  899. });
  900. return next;
  901. });
  902. }, [rows]);
  903. return (
  904. <>
  905. <Stack spacing={2}>
  906. {/* Area1: title */}
  907. <Grid container xs={12} justifyContent="start">
  908. <Grid item>
  909. <Typography mb={2} variant="h4">
  910. {purchaseOrder.code} -{" "}
  911. {t(`${purchaseOrder.status.toLowerCase()}`)}
  912. </Typography>
  913. </Grid>
  914. </Grid>
  915. {/* area2: dn info */}
  916. <Grid container spacing={3} sx={{ maxWidth: 'fit-content' }} alignItems="stretch">
  917. {/* left side select po */}
  918. <Grid item xs={4} sx={{ display: "flex" }}>
  919. <Stack spacing={1} sx={{ flex: 1 }}>
  920. <PoSearchList
  921. poList={poList}
  922. selectedPoId={selectedPoId}
  923. onSelect={handlePoSelect}
  924. loading={isPoListLoading}
  925. />
  926. </Stack>
  927. </Grid>
  928. {/* right side po info */}
  929. <Grid item xs={8}>
  930. <Grid container spacing={3} sx={{ maxWidth: 'fit-content' }}>
  931. <Grid item xs={12}>
  932. <FormProvider {...dnFormProps}>
  933. <Card sx={{ display: "block" }}>
  934. <CardContent component={Stack} spacing={2}>
  935. <TextField
  936. label={t("Supplier")}
  937. fullWidth
  938. disabled={true}
  939. value={purchaseOrder.supplier ?? ""}
  940. />
  941. <Grid container spacing={2}>
  942. <Grid item xs={6}>
  943. <Stack spacing={2}>
  944. <TextField
  945. label={t("Order Date")}
  946. fullWidth
  947. disabled={true}
  948. value={arrayToDateString(purchaseOrder.orderDate as any)}
  949. />
  950. <TextField
  951. {...dnFormProps.register("dnNo")}
  952. label={t("dnNo")}
  953. type="text"
  954. variant="outlined"
  955. fullWidth
  956. />
  957. </Stack>
  958. </Grid>
  959. <Grid item xs={6}>
  960. <Stack spacing={2}>
  961. <TextField
  962. label={t("ETA")}
  963. fullWidth
  964. disabled={true}
  965. value={arrayToDateString(purchaseOrder.estimatedArrivalDate as any)}
  966. />
  967. <LocalizationProvider
  968. dateAdapter={AdapterDayjs}
  969. adapterLocale="zh-hk"
  970. localeText={zhHK.components.MuiLocalizationProvider.defaultProps.localeText}
  971. >
  972. <Controller
  973. control={dnFormProps.control}
  974. name="receiptDate"
  975. render={({ field }) => (
  976. <DatePicker
  977. label={t("receiptDate")}
  978. format={`${OUTPUT_DATE_FORMAT}`}
  979. defaultValue={dateStringToDayjs(field.value)}
  980. onChange={(newValue: Dayjs | null) => {
  981. handleDatePickerChange(newValue, field.onChange);
  982. }}
  983. slotProps={{ textField: { fullWidth: true } }}
  984. />
  985. )}
  986. />
  987. </LocalizationProvider>
  988. </Stack>
  989. </Grid>
  990. </Grid>
  991. </CardContent>
  992. </Card>
  993. </FormProvider>
  994. </Grid>
  995. <Grid item xs={12}>
  996. <Grid container spacing={2} alignItems="stretch">
  997. <Grid item xs={6} sx={{ display: "flex" }}>
  998. <Card sx={{ display: "block", flex: 1 }}>
  999. <CardContent component={Stack} spacing={2}>
  1000. <Typography variant="h6">列印</Typography>
  1001. <PrinterSelect
  1002. label={t("Label Printer")}
  1003. printers={labelPrinters}
  1004. value={selectedPrinter ?? null}
  1005. onChange={(p) => setSelectedPrinter(p ?? undefined)}
  1006. placeholder={t("Label Printer")}
  1007. fullWidth
  1008. disabled={labelPrinters.length <= 0}
  1009. />
  1010. <TextField
  1011. variant="outlined"
  1012. label={t("Print Qty")}
  1013. value={printQty}
  1014. onChange={(event) => {
  1015. const cleaned = String(event.target.value).replace(/[^0-9]/g, "");
  1016. setPrintQty(Number(cleaned || 0));
  1017. }}
  1018. fullWidth
  1019. />
  1020. <Button
  1021. variant="contained"
  1022. onClick={openPrintDialog}
  1023. disabled={labelPrinters.length <= 0}
  1024. >
  1025. 選擇列印項目
  1026. </Button>
  1027. <Typography variant="caption" color="text.secondary">
  1028. 只會顯示「待上架 / 已上架」的來貨記錄
  1029. </Typography>
  1030. </CardContent>
  1031. </Card>
  1032. </Grid>
  1033. <Grid item xs={6} sx={{ display: "flex" }}>
  1034. <Card sx={{ display: "block", flex: 1 }}>
  1035. <CardContent component={Stack} spacing={2} sx={{ height: "100%" }}>
  1036. <Typography variant="h6" sx={{ visibility: "hidden" }}>
  1037. 列印
  1038. </Typography>
  1039. <Button
  1040. variant="outlined"
  1041. onClick={fillTodayLotNo}
  1042. sx={{ flex: 1 }}
  1043. >
  1044. 一鍵填入來貨編號(今日)
  1045. </Button>
  1046. </CardContent>
  1047. </Card>
  1048. </Grid>
  1049. </Grid>
  1050. </Grid>
  1051. </Grid>
  1052. </Grid>
  1053. </Grid>
  1054. {/* Area4: Main Table */}
  1055. <Grid container xs={12} justifyContent="start">
  1056. <Grid item xs={12}>
  1057. <TableContainer component={Paper} sx={{ width: 'fit-content', overflow: 'auto' }}>
  1058. <Table aria-label="collapsible table" stickyHeader>
  1059. <TableHead>
  1060. <TableRow>
  1061. <TableCell align="center" sx={{ width: '60px' }}></TableCell>
  1062. <TableCell sx={{ width: '88px', maxWidth: '88px', px: 1 }}>{t("itemNo")}</TableCell>
  1063. <TableCell align="left" sx={{ width: '100px', maxWidth: '100px', px: 1 }}>{t("itemName")}</TableCell>
  1064. <TableCell align="right">{t("qty")}</TableCell>
  1065. <TableCell align="right">{t("processedQty")}</TableCell>
  1066. <TableCell align="left">{t("uom")}</TableCell>
  1067. <TableCell align="right">{t("receivedTotal")}</TableCell>
  1068. <TableCell align="left">{t("Stock UoM")}</TableCell>
  1069. {/* <TableCell align="right">{t("total weight")}</TableCell> */}
  1070. {/* <TableCell align="right">{`${t("price")} (HKD)`}</TableCell> */}
  1071. <TableCell align="left" sx={{ width: '75px' }}>{t("status")}</TableCell>
  1072. {/* {renderFieldCondition(FIRST_IN_FIELD) ? <TableCell align="right">{t("receivedQty")}</TableCell> : undefined} */}
  1073. <TableCell align="center" sx={{ width: '150px' }}>{t("productLotNo")}</TableCell>
  1074. {renderFieldCondition(SECOND_IN_FIELD) ? <TableCell align="center" sx={{ width: '150px' }}>{t("dnQty")}<br/>(以訂單單位計算)</TableCell> : undefined}
  1075. <TableCell align="center" sx={{ width: '100px' }}></TableCell>
  1076. </TableRow>
  1077. </TableHead>
  1078. <TableBody>
  1079. {rows.map((row) => (
  1080. <Row key={row.id} row={row} />
  1081. ))}
  1082. </TableBody>
  1083. </Table>
  1084. </TableContainer>
  1085. </Grid>
  1086. </Grid>
  1087. {/* area5: selected item info */}
  1088. <Grid container xs={12} justifyContent="start">
  1089. <Grid item xs={12}>
  1090. <Typography variant="h6">
  1091. {selectedRow ? `已選擇貨品: ${selectedRow?.itemNo ? selectedRow.itemNo : 'N/A'} - ${selectedRow?.itemName ? selectedRow?.itemName : 'N/A'}` : "未選擇貨品"}
  1092. </Typography>
  1093. </Grid>
  1094. <Grid item xs={12} sx={{ minWidth: 0 }}>
  1095. {selectedRow && (
  1096. <PoInputGrid
  1097. setRows={setRows}
  1098. stockInLine={stockInLine}
  1099. setStockInLine={setStockInLine}
  1100. setProcessedQty={setProcessedQty}
  1101. itemDetail={selectedRow}
  1102. warehouse={warehouse}
  1103. fetchPoDetail={fetchPoDetail}
  1104. handleMailTemplateForStockInLine={handleMailTemplateForStockInLine}
  1105. printerCombo={printerCombo}
  1106. />
  1107. )}
  1108. </Grid>
  1109. </Grid>
  1110. {/* tab 2 */}
  1111. <Grid sx={{ display: tabIndex === 1 ? "block" : "none" }}>
  1112. {/* <StyledDataGrid
  1113. /> */}
  1114. </Grid>
  1115. </Stack>
  1116. <Dialog open={printDialogOpen} onClose={closePrintDialog} fullWidth maxWidth="md">
  1117. <DialogTitle>列印標籤</DialogTitle>
  1118. <DialogContent>
  1119. <Stack spacing={1.5} sx={{ mt: 1 }}>
  1120. <Stack direction="row" spacing={2} alignItems="center" flexWrap="wrap">
  1121. <FormControlLabel
  1122. control={
  1123. <Checkbox
  1124. checked={printStatusFilter.received}
  1125. onChange={(e) =>
  1126. setPrintStatusFilter((p) => ({ ...p, received: e.target.checked }))
  1127. }
  1128. />
  1129. }
  1130. label="待上架"
  1131. />
  1132. <FormControlLabel
  1133. control={
  1134. <Checkbox
  1135. checked={printStatusFilter.completed}
  1136. onChange={(e) =>
  1137. setPrintStatusFilter((p) => ({ ...p, completed: e.target.checked }))
  1138. }
  1139. />
  1140. }
  1141. label="已上架"
  1142. />
  1143. <FormControlLabel
  1144. control={
  1145. <Checkbox
  1146. checked={
  1147. eligiblePrintSils.length > 0 &&
  1148. selectedPrintSilIds.size === eligiblePrintSils.length
  1149. }
  1150. indeterminate={
  1151. selectedPrintSilIds.size > 0 &&
  1152. selectedPrintSilIds.size < eligiblePrintSils.length
  1153. }
  1154. onChange={(e) => setAllVisiblePrintSilsSelected(e.target.checked)}
  1155. />
  1156. }
  1157. label="全選(目前篩選結果)"
  1158. />
  1159. <Typography variant="caption" color="text.secondary">
  1160. 已選擇 {selectedPrintSilIds.size} / {eligiblePrintSils.length}
  1161. </Typography>
  1162. </Stack>
  1163. <TableContainer component={Paper} variant="outlined">
  1164. <Table size="small" stickyHeader>
  1165. <TableHead>
  1166. <TableRow>
  1167. <TableCell padding="checkbox"></TableCell>
  1168. <TableCell>貨品編號</TableCell>
  1169. <TableCell>貨品名稱</TableCell>
  1170. <TableCell align="right">換算庫存數量</TableCell>
  1171. <TableCell>庫存單位</TableCell>
  1172. <TableCell>收貨日期</TableCell>
  1173. <TableCell>來貨批號</TableCell>
  1174. <TableCell>來貨狀態</TableCell>
  1175. </TableRow>
  1176. </TableHead>
  1177. <TableBody>
  1178. {eligiblePrintSils.map((sil) => {
  1179. const status = (sil.status ?? "").toLowerCase().trim();
  1180. const statusText =
  1181. status === "received" ? "待上架" : status === "completed" ? "已上架" : sil.status;
  1182. const receiptText = sil.receiptDate
  1183. ? Array.isArray(sil.receiptDate)
  1184. ? arrayToDateString(sil.receiptDate)
  1185. : String(sil.receiptDate)
  1186. : "-";
  1187. const stockQty = Number(sil.acceptedQty ?? 0);
  1188. const stockQtyText =
  1189. Number.isFinite(stockQty) && stockQty > 0
  1190. ? decimalFormatter.format(stockQty)
  1191. : decimalFormatter.format(0);
  1192. return (
  1193. <TableRow key={sil.id} hover>
  1194. <TableCell padding="checkbox">
  1195. <Checkbox
  1196. checked={selectedPrintSilIds.has(sil.id)}
  1197. onChange={(e) => togglePrintSilSelection(sil.id, e.target.checked)}
  1198. />
  1199. </TableCell>
  1200. <TableCell>{sil.itemNo}</TableCell>
  1201. <TableCell>{sil.itemName}</TableCell>
  1202. <TableCell align="right">{stockQtyText}</TableCell>
  1203. <TableCell>{sil.stockUomDesc || "-"}</TableCell>
  1204. <TableCell>{receiptText}</TableCell>
  1205. <TableCell>{sil.productLotNo || "-"}</TableCell>
  1206. <TableCell>{statusText}</TableCell>
  1207. </TableRow>
  1208. );
  1209. })}
  1210. {eligiblePrintSils.length === 0 && (
  1211. <TableRow>
  1212. <TableCell colSpan={8}>
  1213. <Typography variant="body2" color="text.secondary">
  1214. 沒有符合條件的項目
  1215. </Typography>
  1216. </TableCell>
  1217. </TableRow>
  1218. )}
  1219. </TableBody>
  1220. </Table>
  1221. </TableContainer>
  1222. </Stack>
  1223. </DialogContent>
  1224. <DialogActions>
  1225. <Button onClick={closePrintDialog} disabled={isBulkPrinting}>
  1226. 取消
  1227. </Button>
  1228. <Button
  1229. variant="contained"
  1230. onClick={handleBulkPrint}
  1231. disabled={isBulkPrinting || selectedPrintSilIds.size <= 0 || !selectedPrinter}
  1232. >
  1233. {isBulkPrinting ? "列印中..." : "列印"}
  1234. </Button>
  1235. </DialogActions>
  1236. </Dialog>
  1237. {/* {itemInfo !== undefined && (
  1238. <>
  1239. <PoQcStockInModal
  1240. type={"putaway"}
  1241. open={putAwayOpen}
  1242. warehouse={warehouse}
  1243. setItemDetail={setItemInfo}
  1244. onClose={closePutAwayModal}
  1245. itemDetail={itemInfo}
  1246. />
  1247. </>
  1248. )} */}
  1249. </>
  1250. );
  1251. };
  1252. export default PoDetail;