FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

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