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.
 
 

1078 lines
39 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. Grid,
  12. Paper,
  13. Stack,
  14. Tab,
  15. Table,
  16. TableBody,
  17. TableCell,
  18. TableContainer,
  19. TableHead,
  20. TableRow,
  21. Tabs,
  22. TabsProps,
  23. TextField,
  24. Typography,
  25. Checkbox,
  26. FormControlLabel,
  27. Card,
  28. CardContent,
  29. Dialog,
  30. DialogActions,
  31. DialogContent,
  32. DialogTitle,
  33. } from "@mui/material";
  34. import { useTranslation } from "react-i18next";
  35. import PrinterSelect from "@/components/common/PrinterSelect";
  36. import { PoDetailRow } from "./PoDetailRow";
  37. // import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid";
  38. import {
  39. GridColDef,
  40. GridRowId,
  41. GridRowModel,
  42. useGridApiRef,
  43. } from "@mui/x-data-grid";
  44. import {
  45. checkPolAndCompletePo,
  46. fetchPoInClient,
  47. fetchPoSummariesClient,
  48. startPo,
  49. } from "@/app/api/po/actions";
  50. import {
  51. useCallback,
  52. useContext,
  53. useEffect,
  54. useMemo,
  55. useRef,
  56. useState,
  57. } from "react";
  58. import PoInputGrid from "./PoInputGrid";
  59. // import { QcItemWithChecks } from "@/app/api/qc";
  60. import { useRouter, useSearchParams, usePathname } from "next/navigation";
  61. import { WarehouseResult } from "@/app/api/warehouse";
  62. import { dateStringToDayjs, dayjsToDateString, OUTPUT_DATE_FORMAT, outputDateStringToInputDateString, decimalFormatter, arrayToDateString } from "@/app/utils/formatUtil";
  63. import { CameraContext } from "../Cameras/CameraProvider";
  64. import QrModal from "./QrModal";
  65. import { PlayArrow } from "@mui/icons-material";
  66. import DoneIcon from "@mui/icons-material/Done";
  67. import { downloadFile, getCustomWidth } from "@/app/utils/commonUtil";
  68. import { List, ListItem, ListItemButton, ListItemText, Divider } from "@mui/material";
  69. import { Controller, FormProvider, useForm } from "react-hook-form";
  70. import dayjs, { Dayjs } from "dayjs";
  71. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  72. import { DatePicker, LocalizationProvider, zhHK } from "@mui/x-date-pickers";
  73. import LoadingComponent from "../General/LoadingComponent";
  74. import { getMailTemplatePdfForStockInLine } from "@/app/api/mailTemplate/actions";
  75. import { PrinterCombo } from "@/app/api/settings/printer";
  76. import { EscalationCombo } from "@/app/api/user";
  77. import { StockInLine } from "@/app/api/stockIn";
  78. import { printQrCodeForSil } from "@/app/api/stockIn/actions";
  79. import { useSession } from "next-auth/react";
  80. import { AUTH } from "@/authorities";
  81. //import { useRouter } from "next/navigation";
  82. type Props = {
  83. po: PoResult;
  84. // qc: QcItemWithChecks[];
  85. warehouse: WarehouseResult[];
  86. printerCombo: PrinterCombo[];
  87. };
  88. type EntryError =
  89. | {
  90. [field in keyof StockInLine]?: string;
  91. }
  92. | undefined;
  93. // type PolRow = TableRow<Partial<StockInLine>, EntryError>;
  94. const PoSearchList: React.FC<{
  95. poList: PoResult[];
  96. selectedPoId: number;
  97. onSelect: (po: PoResult) => void;
  98. loading?: boolean;
  99. }> = ({ poList, selectedPoId, onSelect, loading = false }) => {
  100. const { t } = useTranslation(["purchaseOrder", "dashboard"]);
  101. const [searchTerm, setSearchTerm] = useState('');
  102. const filteredPoList = useMemo(() => {
  103. if (searchTerm.trim() === '') {
  104. return poList;
  105. }
  106. return poList.filter(poItem =>
  107. poItem.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
  108. poItem.supplier?.toLowerCase().includes(searchTerm.toLowerCase()) ||
  109. t(`${poItem.status.toLowerCase()}`).toLowerCase().includes(searchTerm.toLowerCase())
  110. );
  111. }, [poList, searchTerm, t]);
  112. return (
  113. <Paper
  114. sx={{
  115. p: 2,
  116. minWidth: "300px",
  117. height: "100%",
  118. display: "flex",
  119. flexDirection: "column",
  120. overflow: "hidden",
  121. }}
  122. >
  123. <Typography variant="h6" gutterBottom>
  124. {t("Purchase Order")}
  125. </Typography>
  126. <TextField
  127. label={t("Search")}
  128. variant="outlined"
  129. size="small"
  130. fullWidth
  131. value={searchTerm}
  132. onChange={(e) => setSearchTerm(e.target.value)}
  133. sx={{ mb: 2 }}
  134. InputProps={{
  135. startAdornment: (
  136. <Typography variant="body2" color="text.secondary" sx={{ mr: 1 }}>
  137. </Typography>
  138. ),
  139. }}
  140. />
  141. <Box sx={{ flex: 1, overflow: "auto" }}>
  142. {loading ? (
  143. <LoadingComponent />
  144. ) : filteredPoList.length > 0 ? (
  145. <List dense sx={{ width: "100%" }}>
  146. {filteredPoList.map((poItem, index) => (
  147. <div key={poItem.id}>
  148. <ListItem disablePadding sx={{ width: "100%" }}>
  149. <ListItemButton
  150. selected={selectedPoId === poItem.id}
  151. onClick={() => onSelect(poItem)}
  152. sx={{
  153. width: "100%",
  154. "&.Mui-selected": {
  155. backgroundColor: "primary.light",
  156. "&:hover": {
  157. backgroundColor: "primary.light",
  158. },
  159. },
  160. }}
  161. >
  162. <ListItemText
  163. primary={
  164. <Typography variant="body2" sx={{ wordBreak: "break-all" }}>
  165. {poItem.code}
  166. </Typography>
  167. }
  168. secondary={
  169. <Typography variant="caption" color="text.secondary">
  170. {t(`${poItem.status.toLowerCase()}`)}
  171. </Typography>
  172. }
  173. />
  174. </ListItemButton>
  175. </ListItem>
  176. {index < filteredPoList.length - 1 && <Divider />}
  177. </div>
  178. ))}
  179. </List>
  180. ) : (
  181. <Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
  182. {searchTerm.trim()
  183. ? t("No purchase orders match your search", { defaultValue: "沒有符合搜索的採購單" })
  184. : t("No purchase orders to show", { defaultValue: "沒有可顯示的採購單" })}
  185. </Typography>
  186. )}
  187. </Box>
  188. {searchTerm && (
  189. <Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: "block" }}>
  190. {`${t("Found")} ${filteredPoList.length} ${t("Purchase Order")}`}
  191. {/* {`${t("Found")} ${filteredPoList.length} of ${poList.length} ${t("Item")}`} */}
  192. </Typography>
  193. )}
  194. </Paper>
  195. );
  196. };
  197. interface PolInputResult {
  198. lotNo: string,
  199. dnQty: string,
  200. }
  201. /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */
  202. const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
  203. const cameras = useContext(CameraContext);
  204. const { data: session } = useSession();
  205. const canSeeStockInReminders = useMemo(() => {
  206. const set = new Set((session?.user?.abilities ?? []).map((a) => String(a).trim()));
  207. return set.has(AUTH.TESTING) || set.has(AUTH.ADMIN) || set.has(AUTH.STOCK);
  208. }, [session?.user?.abilities]);
  209. // console.log(cameras);
  210. const { t } = useTranslation("purchaseOrder");
  211. const apiRef = useGridApiRef();
  212. const [purchaseOrder, setPurchaseOrder] = useState({ ...po });
  213. const [rows, setRows] = useState<PurchaseOrderLine[]>(
  214. purchaseOrder.pol || [],
  215. );
  216. const [polInputList, setPolInputList] = useState<Record<number, PolInputResult>>({})
  217. const PO_DETAIL_SELECTION_KEY = "po-detail-selection";
  218. useEffect(() => {
  219. setPolInputList((prev) => {
  220. const next: Record<number, PolInputResult> = {};
  221. (purchaseOrder.pol ?? []).forEach((pol) => {
  222. next[pol.id] = prev[pol.id] ?? {
  223. lotNo: "",
  224. dnQty: "",
  225. };
  226. });
  227. return next;
  228. });
  229. }, [purchaseOrder.pol]);
  230. useEffect(() => {
  231. try {
  232. const raw = sessionStorage.getItem("po-detail-selection");
  233. if (raw) {
  234. const parsed = JSON.parse(raw) as { id: number; code: string; status: string; supplier: string | null }[];
  235. if (Array.isArray(parsed) && parsed.length > 0) {
  236. setPoList(parsed as PoResult[]);
  237. sessionStorage.removeItem("po-detail-selection"); // 可选:用一次就删,避免下次从别处进还看到旧数据
  238. }
  239. }
  240. } catch (e) {
  241. console.warn("sessionStorage getItem/parse failed", e);
  242. }
  243. }, []);
  244. const pathname = usePathname()
  245. const searchParams = useSearchParams();
  246. const [selectedRow, setSelectedRow] = useState<PurchaseOrderLine | null>(null);
  247. const [stockInLine, setStockInLine] = useState<StockInLine[]>([]);
  248. const [processedQty, setProcessedQty] = useState(0);
  249. /** Tracks user/nav selection so query patches via history.replaceState stay authoritative. */
  250. const selectedPolIdRef = useRef<number | null>(null);
  251. /** Patch PO edit query without Next soft-navigation (avoids scroll-to-top). */
  252. const patchPoEditQuery = useCallback(
  253. (mutate: (params: URLSearchParams) => void) => {
  254. if (typeof window === "undefined") return;
  255. const params = new URLSearchParams(window.location.search);
  256. mutate(params);
  257. const qs = params.toString();
  258. window.history.replaceState(
  259. window.history.state,
  260. "",
  261. qs ? `${pathname}?${qs}` : pathname,
  262. );
  263. },
  264. [pathname],
  265. );
  266. /** Keep selection + bottom stock-in grid in sync with selected / URL `polId`. */
  267. useEffect(() => {
  268. if (rows.length === 0) return;
  269. const urlPolId = searchParams.get("polId");
  270. const preferredId =
  271. selectedPolIdRef.current ??
  272. (urlPolId != null ? Number(urlPolId) : null);
  273. if (preferredId == null || Number.isNaN(preferredId)) return;
  274. const match =
  275. rows.find((r) => r.id === preferredId) ??
  276. (urlPolId != null
  277. ? rows.find((r) => r.id.toString() === urlPolId)
  278. : undefined);
  279. if (!match) return;
  280. selectedPolIdRef.current = match.id;
  281. setSelectedRow(match);
  282. setStockInLine(match.stockInLine ?? []);
  283. setProcessedQty(match.processed);
  284. }, [rows, searchParams]);
  285. const router = useRouter();
  286. const [poList, setPoList] = useState<PoResult[]>(() => [po]);
  287. const [isPoListLoading, setIsPoListLoading] = useState(false);
  288. const [selectedPoId, setSelectedPoId] = useState(po.id);
  289. const [focusField, setFocusField] = useState<HTMLInputElement>();
  290. const currentPoId = searchParams.get('id');
  291. const selectedIdsParam = searchParams.get('selectedIds');
  292. // const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
  293. const dnFormProps = useForm({
  294. defaultValues: {
  295. dnNo: '',
  296. receiptDate: dayjsToDateString(dayjs())
  297. }
  298. })
  299. const labelPrinters = useMemo(() => {
  300. return (printerCombo ?? []).filter((p) => {
  301. const typeText = String(p.type ?? "").trim().toLowerCase();
  302. if (typeText === "label") return true;
  303. // Backward compatibility for legacy rows without a reliable type value.
  304. const text = `${p.label ?? ""} ${p.name ?? ""} ${p.code ?? ""}`.toLowerCase();
  305. return text.includes("label") || text.includes("標籤");
  306. });
  307. }, [printerCombo]);
  308. const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | undefined>(
  309. labelPrinters?.[0],
  310. );
  311. useEffect(() => {
  312. // If options change, keep selection valid and prefer first Label printer.
  313. if (!selectedPrinter || !labelPrinters.some((p) => p.id === selectedPrinter.id)) {
  314. setSelectedPrinter(labelPrinters[0]);
  315. }
  316. }, [labelPrinters, selectedPrinter]);
  317. const [printQty, setPrintQty] = useState(1);
  318. const [printDialogOpen, setPrintDialogOpen] = useState(false);
  319. const [isBulkPrinting, setIsBulkPrinting] = useState(false);
  320. const [printStatusFilter, setPrintStatusFilter] = useState({
  321. received: true,
  322. completed: false,
  323. });
  324. const [selectedPrintSilIds, setSelectedPrintSilIds] = useState<Set<number>>(
  325. () => new Set(),
  326. );
  327. const eligiblePrintSils = useMemo(() => {
  328. const statusSet = new Set<string>();
  329. if (printStatusFilter.received) statusSet.add("received");
  330. if (printStatusFilter.completed) statusSet.add("completed");
  331. const pols = purchaseOrder.pol ?? [];
  332. return pols
  333. .flatMap((pol) => pol.stockInLine ?? [])
  334. .filter((sil) => statusSet.has((sil.status ?? "").toLowerCase().trim()));
  335. }, [purchaseOrder.pol, printStatusFilter.completed, printStatusFilter.received]);
  336. const openPrintDialog = useCallback(() => {
  337. setSelectedPrintSilIds(new Set());
  338. setPrintDialogOpen(true);
  339. }, []);
  340. const closePrintDialog = useCallback(() => {
  341. if (isBulkPrinting) return;
  342. setPrintDialogOpen(false);
  343. }, [isBulkPrinting]);
  344. const togglePrintSilSelection = useCallback((id: number, checked: boolean) => {
  345. setSelectedPrintSilIds((prev) => {
  346. const next = new Set(prev);
  347. if (checked) next.add(id);
  348. else next.delete(id);
  349. return next;
  350. });
  351. }, []);
  352. const setAllVisiblePrintSilsSelected = useCallback((checked: boolean) => {
  353. setSelectedPrintSilIds(() => {
  354. if (!checked) return new Set();
  355. return new Set(eligiblePrintSils.map((s) => s.id));
  356. });
  357. }, [eligiblePrintSils]);
  358. const handleBulkPrint = useCallback(async () => {
  359. if (!selectedPrinter) {
  360. alert("請先選擇印表機");
  361. return;
  362. }
  363. if (!Number.isFinite(printQty) || printQty <= 0) {
  364. alert("列印數量必須大於 0");
  365. return;
  366. }
  367. const ids = Array.from(selectedPrintSilIds.values());
  368. if (ids.length <= 0) {
  369. alert("請先選擇要列印的項目");
  370. return;
  371. }
  372. setIsBulkPrinting(true);
  373. try {
  374. for (const id of ids) {
  375. await printQrCodeForSil({
  376. stockInLineId: id,
  377. printerId: selectedPrinter.id,
  378. printQty,
  379. });
  380. }
  381. setPrintDialogOpen(false);
  382. } finally {
  383. setIsBulkPrinting(false);
  384. }
  385. }, [printQty, selectedPrinter, selectedPrintSilIds]);
  386. /** Only loads sidebar list when `selectedIds` is in the URL; otherwise show current PO only (no /po/list fetch). */
  387. const fetchPoList = useCallback(async () => {
  388. if (!selectedIdsParam) return;
  389. setIsPoListLoading(true);
  390. try {
  391. const MAX_IDS = 20; // 一次最多加载 20 个,防止卡死
  392. const allIds = selectedIdsParam
  393. .split(',')
  394. .map((id) => parseInt(id))
  395. .filter((id) => !Number.isNaN(id));
  396. const limitedIds = allIds.slice(0, MAX_IDS);
  397. if (allIds.length > MAX_IDS) {
  398. console.warn(`selectedIds too many (${allIds.length}), only loading first ${MAX_IDS}.`);
  399. }
  400. const result = await fetchPoSummariesClient(limitedIds);
  401. setPoList(result as any);
  402. } catch (error) {
  403. console.error("Failed to fetch PO list:", error);
  404. } finally {
  405. setIsPoListLoading(false);
  406. }
  407. }, [selectedIdsParam]);
  408. const fetchPoDetail = useCallback(async (poId: string, preserveDnNo: boolean = false, preferredPolId?: number) => {
  409. try {
  410. const result = await fetchPoInClient(parseInt(poId));
  411. if (result) {
  412. console.log("%c Fetched PO:", "color:orange", result);
  413. setPurchaseOrder(result);
  414. const currentDnNo = preserveDnNo ? dnFormProps.getValues("dnNo") : "";
  415. dnFormProps.reset({
  416. dnNo: currentDnNo,
  417. receiptDate: dayjsToDateString(dayjs()),
  418. });
  419. setRows(result.pol || []);
  420. if (result.pol && result.pol.length > 0) {
  421. const targetPolId = preferredPolId ?? selectedPolIdRef.current ?? selectedRow?.id;
  422. const targetPol =
  423. result.pol.find((p) => p.id === targetPolId) ?? result.pol[0];
  424. selectedPolIdRef.current = targetPol.id;
  425. setSelectedRow(targetPol);
  426. setStockInLine(targetPol.stockInLine);
  427. setProcessedQty(targetPol.processed);
  428. }
  429. // if (focusField) {console.log(focusField);focusField.focus();}
  430. }
  431. } catch (error) {
  432. console.error("Failed to fetch PO detail:", error);
  433. }
  434. }, [selectedRow, selectedPoId]);
  435. const handlePoSelect = useCallback(
  436. async (selectedPo: PoResult) => {
  437. if (selectedPo.id === selectedPoId) return;
  438. selectedPolIdRef.current = null;
  439. setSelectedPoId(selectedPo.id);
  440. await fetchPoDetail(selectedPo.id.toString());
  441. const newSelectedIds = selectedIdsParam || selectedPo.id.toString();
  442. const newUrl = `/po/edit?id=${selectedPo.id}&start=true&selectedIds=${newSelectedIds}`;
  443. if (pathname + searchParams.toString() !== newUrl) {
  444. router.replace(newUrl, { scroll: false });
  445. }
  446. },
  447. [selectedPoId, fetchPoDetail, selectedIdsParam, pathname, searchParams, router]
  448. );
  449. useEffect(() => {
  450. if (currentPoId && currentPoId !== selectedPoId.toString()) {
  451. setSelectedPoId(parseInt(currentPoId));
  452. fetchPoDetail(currentPoId);
  453. }
  454. }, [currentPoId, fetchPoDetail]);
  455. useEffect(() => {
  456. if (selectedIdsParam) {
  457. void fetchPoList();
  458. }
  459. }, [selectedIdsParam, fetchPoList]);
  460. useEffect(() => {
  461. if (selectedIdsParam) return;
  462. setPoList([purchaseOrder]);
  463. }, [selectedIdsParam, purchaseOrder]);
  464. useEffect(() => {
  465. if (currentPoId) {
  466. setSelectedPoId(parseInt(currentPoId));
  467. }
  468. }, [currentPoId]);
  469. const removeParam = (paramToRemove: string) => {
  470. const newParams = new URLSearchParams(searchParams.toString());
  471. newParams.delete(paramToRemove);
  472. window.history.replaceState({}, '', `${window.location.pathname}?${newParams}`);
  473. };
  474. const handleCompletePo = useCallback(async () => {
  475. const checkRes = await checkPolAndCompletePo(purchaseOrder.id);
  476. console.log(checkRes);
  477. const newPo = await fetchPoInClient(purchaseOrder.id);
  478. setPurchaseOrder(newPo);
  479. }, [purchaseOrder.id]);
  480. const handleStartPo = useCallback(async () => {
  481. const startRes = await startPo(purchaseOrder.id);
  482. console.log(startRes);
  483. const newPo = await fetchPoInClient(purchaseOrder.id);
  484. setPurchaseOrder(newPo);
  485. }, [purchaseOrder.id]);
  486. const getDnValues = useCallback(() => dnFormProps.getValues(), [dnFormProps]);
  487. const formatReceiptDate = useCallback((receiptDate?: string) => {
  488. return outputDateStringToInputDateString(receiptDate ?? "");
  489. }, []);
  490. const handleSelectPol = useCallback((row: PurchaseOrderLine) => {
  491. selectedPolIdRef.current = row.id;
  492. setSelectedRow(row);
  493. setStockInLine(row.stockInLine ?? []);
  494. setProcessedQty(row.processed);
  495. patchPoEditQuery((params) => {
  496. params.set("polId", String(row.id));
  497. params.delete("stockInLineId");
  498. });
  499. }, [patchPoEditQuery]);
  500. const handleRowInputBlur = useCallback((rowId: number, lotNo: string, dnQty: string) => {
  501. setPolInputList((prev) => {
  502. const current = prev[rowId] ?? { lotNo: "", dnQty: "" };
  503. if (current.lotNo === lotNo && current.dnQty === dnQty) return prev;
  504. return {
  505. ...prev,
  506. [rowId]: { lotNo, dnQty },
  507. };
  508. });
  509. }, []);
  510. const handleRowSubmitted = useCallback((row: PurchaseOrderLine) => {
  511. setPolInputList((prev) => ({
  512. ...prev,
  513. [row.id]: { lotNo: "", dnQty: "" },
  514. }));
  515. selectedPolIdRef.current = row.id;
  516. setSelectedRow(row);
  517. patchPoEditQuery((params) => {
  518. params.set("polId", String(row.id));
  519. params.delete("stockInLineId");
  520. });
  521. fetchPoDetail(selectedPoId.toString(), true, row.id);
  522. }, [fetchPoDetail, patchPoEditQuery, selectedPoId]);
  523. const handleMailTemplateForStockInLine = useCallback(async (stockInLineId: number) => {
  524. const response = await getMailTemplatePdfForStockInLine(stockInLineId)
  525. if (response) {
  526. downloadFile(new Uint8Array(response.blobValue), response.filename);
  527. }
  528. }, [])
  529. useEffect(() => {
  530. setRows(purchaseOrder.pol || []);
  531. }, [purchaseOrder]);
  532. // useEffect(() => {
  533. // setStockInLine([])
  534. // }, []);
  535. const [tabIndex, setTabIndex] = useState(0);
  536. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  537. (_e, newValue) => {
  538. setTabIndex(newValue);
  539. },
  540. [],
  541. );
  542. const [isOpenScanner, setOpenScanner] = useState(false);
  543. // const testing = useCallback(() => {
  544. // // setOpenScanner(true);
  545. // const newParams = new URLSearchParams(searchParams.toString());
  546. // console.log(pathname)
  547. // }, [pathname, router, searchParams]);
  548. const onOpenScanner = useCallback(() => {
  549. setOpenScanner(true);
  550. }, []);
  551. const onCloseScanner = useCallback(() => {
  552. setOpenScanner(false);
  553. }, []);
  554. const [itemInfo, setItemInfo] = useState<
  555. StockInLine & { warehouseId?: number }
  556. >();
  557. const [putAwayOpen, setPutAwayOpen] = useState(false);
  558. // const [scannedInfo, setScannedInfo] = useState<QrCodeInfo>({} as QrCodeInfo);
  559. const closePutAwayModal = useCallback(() => {
  560. setPutAwayOpen(false);
  561. setItemInfo(undefined);
  562. }, []);
  563. const openPutAwayModal = useCallback(() => {
  564. setPutAwayOpen(true);
  565. }, []);
  566. const buttonData = useMemo(() => {
  567. switch (purchaseOrder.status.toLowerCase()) {
  568. case "pending":
  569. return {
  570. buttonName: "start",
  571. title: t("Do you want to start?"),
  572. confirmButtonText: t("Start"),
  573. successTitle: t("Start Success"),
  574. errorTitle: t("Start Fail"),
  575. buttonText: t("Start PO"),
  576. buttonIcon: <PlayArrow />,
  577. buttonColor: "success",
  578. disabled: false,
  579. onClick: handleStartPo,
  580. };
  581. case "receiving":
  582. return {
  583. buttonName: "complete",
  584. title: t("Do you want to complete?"),
  585. confirmButtonText: t("Complete"),
  586. successTitle: t("Complete Success"),
  587. errorTitle: t("Complete Fail"),
  588. buttonText: t("Complete PO"),
  589. buttonIcon: <DoneIcon />,
  590. buttonColor: "info",
  591. disabled: false,
  592. onClick: handleCompletePo,
  593. };
  594. default:
  595. return {
  596. buttonName: "complete",
  597. title: t("Do you want to complete?"),
  598. confirmButtonText: t("Complete"),
  599. successTitle: t("Complete Success"),
  600. errorTitle: t("Complete Fail"),
  601. buttonText: t("Complete PO"),
  602. buttonIcon: <DoneIcon />,
  603. buttonColor: "info",
  604. disabled: true,
  605. };
  606. // break;
  607. }
  608. }, [purchaseOrder.status, t, handleStartPo, handleCompletePo]);
  609. const FIRST_IN_FIELD = "firstInQty"
  610. const SECOND_IN_FIELD = "secondInQty"
  611. const renderFieldCondition = useCallback((field: "firstInQty" | "secondInQty"): boolean => {
  612. switch (field) {
  613. case FIRST_IN_FIELD:
  614. return true;
  615. case SECOND_IN_FIELD:
  616. return true;
  617. default:
  618. return false; // Default case
  619. }
  620. }, []);
  621. const handleDatePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => {
  622. if (value != null) {
  623. const updatedValue = dayjsToDateString(value)
  624. onChange(updatedValue)
  625. } else {
  626. onChange(value)
  627. }
  628. }, [])
  629. const fillTodayLotNo = useCallback(() => {
  630. const today = dayjs().format("YYYYMMDD");
  631. setPolInputList((prev) => {
  632. const next: Record<number, PolInputResult> = { ...prev };
  633. (rows ?? []).forEach((r) => {
  634. const current = next[r.id] ?? { lotNo: "", dnQty: "" };
  635. const lotNo = (current.lotNo ?? "").trim();
  636. if (!lotNo) {
  637. next[r.id] = { ...current, lotNo: today };
  638. }
  639. });
  640. return next;
  641. });
  642. }, [rows]);
  643. return (
  644. <>
  645. <Stack spacing={2} sx={{ width: "100%" }}>
  646. {/* Area1: title */}
  647. <Grid container justifyContent="start">
  648. <Grid item>
  649. <Typography mb={2} variant="h4">
  650. {purchaseOrder.code} -{" "}
  651. {t(`${purchaseOrder.status.toLowerCase()}`)}
  652. </Typography>
  653. </Grid>
  654. </Grid>
  655. {/* area2: dn info */}
  656. <Grid container spacing={3} sx={{ width: "100%" }} alignItems="stretch">
  657. {/* left side select po */}
  658. <Grid item xs={4} sx={{ display: "flex" }}>
  659. <Stack spacing={1} sx={{ flex: 1 }}>
  660. <PoSearchList
  661. poList={poList}
  662. selectedPoId={selectedPoId}
  663. onSelect={handlePoSelect}
  664. loading={isPoListLoading}
  665. />
  666. </Stack>
  667. </Grid>
  668. {/* right side po info */}
  669. <Grid item xs={8} sx={{ minWidth: 0 }}>
  670. <Grid container spacing={3} sx={{ width: "100%" }}>
  671. <Grid item xs={12}>
  672. <FormProvider {...dnFormProps}>
  673. <Card sx={{ display: "block" }}>
  674. <CardContent component={Stack} spacing={2}>
  675. <TextField
  676. label={t("Supplier")}
  677. fullWidth
  678. disabled={true}
  679. value={purchaseOrder.supplier ?? ""}
  680. />
  681. <Grid container spacing={2}>
  682. <Grid item xs={6}>
  683. <Stack spacing={2}>
  684. <TextField
  685. label={t("Order Date")}
  686. fullWidth
  687. disabled={true}
  688. value={arrayToDateString(purchaseOrder.orderDate as any)}
  689. />
  690. <TextField
  691. {...dnFormProps.register("dnNo")}
  692. label={t("dnNo")}
  693. type="text"
  694. variant="outlined"
  695. fullWidth
  696. />
  697. </Stack>
  698. </Grid>
  699. <Grid item xs={6}>
  700. <Stack spacing={2}>
  701. <TextField
  702. label={t("ETA")}
  703. fullWidth
  704. disabled={true}
  705. value={arrayToDateString(purchaseOrder.estimatedArrivalDate as any)}
  706. />
  707. <LocalizationProvider
  708. dateAdapter={AdapterDayjs}
  709. adapterLocale="zh-hk"
  710. localeText={zhHK.components.MuiLocalizationProvider.defaultProps.localeText}
  711. >
  712. <Controller
  713. control={dnFormProps.control}
  714. name="receiptDate"
  715. render={({ field }) => (
  716. <DatePicker
  717. label={t("receiptDate")}
  718. format={`${OUTPUT_DATE_FORMAT}`}
  719. defaultValue={dateStringToDayjs(field.value)}
  720. onChange={(newValue: Dayjs | null) => {
  721. handleDatePickerChange(newValue, field.onChange);
  722. }}
  723. slotProps={{ textField: { fullWidth: true } }}
  724. />
  725. )}
  726. />
  727. </LocalizationProvider>
  728. </Stack>
  729. </Grid>
  730. </Grid>
  731. </CardContent>
  732. </Card>
  733. </FormProvider>
  734. </Grid>
  735. <Grid item xs={12}>
  736. <Grid container spacing={2} alignItems="stretch">
  737. <Grid item xs={6} sx={{ display: "flex" }}>
  738. <Card sx={{ display: "block", flex: 1 }}>
  739. <CardContent component={Stack} spacing={2}>
  740. <Typography variant="h6">列印</Typography>
  741. <PrinterSelect
  742. label={t("Label Printer")}
  743. printers={labelPrinters}
  744. value={selectedPrinter ?? null}
  745. onChange={(p) => setSelectedPrinter(p ?? undefined)}
  746. placeholder={t("Label Printer")}
  747. fullWidth
  748. disabled={labelPrinters.length <= 0}
  749. />
  750. <TextField
  751. variant="outlined"
  752. label={t("Print Qty")}
  753. value={printQty}
  754. onChange={(event) => {
  755. const cleaned = String(event.target.value).replace(/[^0-9]/g, "");
  756. setPrintQty(Number(cleaned || 0));
  757. }}
  758. fullWidth
  759. />
  760. <Button
  761. variant="contained"
  762. onClick={openPrintDialog}
  763. disabled={labelPrinters.length <= 0}
  764. >
  765. 選擇列印項目
  766. </Button>
  767. <Typography variant="caption" color="text.secondary">
  768. 只會顯示「待上架 / 已上架」的來貨記錄
  769. </Typography>
  770. </CardContent>
  771. </Card>
  772. </Grid>
  773. <Grid item xs={6} sx={{ display: "flex" }}>
  774. <Card sx={{ display: "block", flex: 1 }}>
  775. <CardContent component={Stack} spacing={2} sx={{ height: "100%" }}>
  776. <Typography variant="h6" sx={{ visibility: "hidden" }}>
  777. 列印
  778. </Typography>
  779. <Button
  780. variant="outlined"
  781. onClick={fillTodayLotNo}
  782. sx={{ flex: 1 }}
  783. >
  784. 一鍵填入來貨編號(今日)
  785. </Button>
  786. </CardContent>
  787. </Card>
  788. </Grid>
  789. </Grid>
  790. </Grid>
  791. </Grid>
  792. </Grid>
  793. </Grid>
  794. {/* Area4: Main Table */}
  795. <Grid container justifyContent="start" sx={{ width: "100%" }}>
  796. <Grid item xs={12} sx={{ width: "100%", minWidth: 0 }}>
  797. <TableContainer component={Paper} sx={{ width: "100%", overflow: "auto" }}>
  798. <Table aria-label="collapsible table" stickyHeader sx={{ width: "100%" }}>
  799. <TableHead>
  800. <TableRow>
  801. <TableCell align="center" sx={{ width: '60px' }}></TableCell>
  802. <TableCell sx={{ width: '88px', maxWidth: '88px', px: 1 }}>{t("itemNo")}</TableCell>
  803. <TableCell align="left" sx={{ width: '100px', maxWidth: '100px', px: 1 }}>{t("itemName")}</TableCell>
  804. <TableCell align="right">{t("qty")}</TableCell>
  805. <TableCell align="right">{t("processedQty")}</TableCell>
  806. <TableCell align="left">{t("uom")}</TableCell>
  807. <TableCell align="right">{t("receivedTotal")}</TableCell>
  808. <TableCell align="left">{t("Stock UoM")}</TableCell>
  809. {/* <TableCell align="right">{t("total weight")}</TableCell> */}
  810. {/* <TableCell align="right">{`${t("price")} (HKD)`}</TableCell> */}
  811. <TableCell align="left" sx={{ width: '75px' }}>{t("status")}</TableCell>
  812. {/* {renderFieldCondition(FIRST_IN_FIELD) ? <TableCell align="right">{t("receivedQty")}</TableCell> : undefined} */}
  813. <TableCell align="center" sx={{ width: '150px' }}>{t("productLotNo")}</TableCell>
  814. {renderFieldCondition(SECOND_IN_FIELD) ? (
  815. <TableCell align="center" sx={{ width: 360, minWidth: 360 }}>
  816. {t("dnQty")}
  817. <br />
  818. ({t("dnQtyOrderUnitHint")})
  819. </TableCell>
  820. ) : undefined}
  821. <TableCell align="center" sx={{ width: '100px' }}></TableCell>
  822. </TableRow>
  823. </TableHead>
  824. <TableBody>
  825. {rows.map((row) => (
  826. <PoDetailRow
  827. key={row.id}
  828. row={row}
  829. selected={selectedRow?.id === row.id}
  830. canSeeStockInReminders={canSeeStockInReminders}
  831. showDnQty={renderFieldCondition(SECOND_IN_FIELD)}
  832. savedLotNo={polInputList[row.id]?.lotNo ?? ""}
  833. savedDnQty={polInputList[row.id]?.dnQty ?? ""}
  834. onSelect={handleSelectPol}
  835. onInputBlur={handleRowInputBlur}
  836. getDnValues={getDnValues}
  837. formatReceiptDate={formatReceiptDate}
  838. onSubmitted={handleRowSubmitted}
  839. />
  840. ))}
  841. </TableBody>
  842. </Table>
  843. </TableContainer>
  844. </Grid>
  845. </Grid>
  846. {/* area5: selected item info */}
  847. <Grid container justifyContent="start" sx={{ width: "100%" }}>
  848. <Grid item xs={12}>
  849. <Typography variant="h6">
  850. {selectedRow ? `已選擇貨品: ${selectedRow?.itemNo ? selectedRow.itemNo : 'N/A'} - ${selectedRow?.itemName ? selectedRow?.itemName : 'N/A'}` : "未選擇貨品"}
  851. </Typography>
  852. </Grid>
  853. <Grid item xs={12} sx={{ width: "100%", minWidth: 0 }}>
  854. {selectedRow && (
  855. <PoInputGrid
  856. setRows={setRows}
  857. stockInLine={stockInLine}
  858. setStockInLine={setStockInLine}
  859. setProcessedQty={setProcessedQty}
  860. itemDetail={selectedRow}
  861. warehouse={warehouse}
  862. fetchPoDetail={fetchPoDetail}
  863. handleMailTemplateForStockInLine={handleMailTemplateForStockInLine}
  864. printerCombo={printerCombo}
  865. />
  866. )}
  867. </Grid>
  868. </Grid>
  869. {/* tab 2 */}
  870. <Grid sx={{ display: tabIndex === 1 ? "block" : "none" }}>
  871. {/* <StyledDataGrid
  872. /> */}
  873. </Grid>
  874. </Stack>
  875. <Dialog open={printDialogOpen} onClose={closePrintDialog} fullWidth maxWidth="md">
  876. <DialogTitle>列印標籤</DialogTitle>
  877. <DialogContent>
  878. <Stack spacing={1.5} sx={{ mt: 1 }}>
  879. <Stack direction="row" spacing={2} alignItems="center" flexWrap="wrap">
  880. <FormControlLabel
  881. control={
  882. <Checkbox
  883. checked={printStatusFilter.received}
  884. onChange={(e) =>
  885. setPrintStatusFilter((p) => ({ ...p, received: e.target.checked }))
  886. }
  887. />
  888. }
  889. label="待上架"
  890. />
  891. <FormControlLabel
  892. control={
  893. <Checkbox
  894. checked={printStatusFilter.completed}
  895. onChange={(e) =>
  896. setPrintStatusFilter((p) => ({ ...p, completed: e.target.checked }))
  897. }
  898. />
  899. }
  900. label="已上架"
  901. />
  902. <FormControlLabel
  903. control={
  904. <Checkbox
  905. checked={
  906. eligiblePrintSils.length > 0 &&
  907. selectedPrintSilIds.size === eligiblePrintSils.length
  908. }
  909. indeterminate={
  910. selectedPrintSilIds.size > 0 &&
  911. selectedPrintSilIds.size < eligiblePrintSils.length
  912. }
  913. onChange={(e) => setAllVisiblePrintSilsSelected(e.target.checked)}
  914. />
  915. }
  916. label="全選(目前篩選結果)"
  917. />
  918. <Typography variant="caption" color="text.secondary">
  919. 已選擇 {selectedPrintSilIds.size} / {eligiblePrintSils.length}
  920. </Typography>
  921. </Stack>
  922. <TableContainer component={Paper} variant="outlined">
  923. <Table size="small" stickyHeader>
  924. <TableHead>
  925. <TableRow>
  926. <TableCell padding="checkbox"></TableCell>
  927. <TableCell>貨品編號</TableCell>
  928. <TableCell>貨品名稱</TableCell>
  929. <TableCell align="right">換算庫存數量</TableCell>
  930. <TableCell>庫存單位</TableCell>
  931. <TableCell>收貨日期</TableCell>
  932. <TableCell>來貨批號</TableCell>
  933. <TableCell>來貨狀態</TableCell>
  934. </TableRow>
  935. </TableHead>
  936. <TableBody>
  937. {eligiblePrintSils.map((sil) => {
  938. const status = (sil.status ?? "").toLowerCase().trim();
  939. const statusText =
  940. status === "received" ? "待上架" : status === "completed" ? "已上架" : sil.status;
  941. const receiptText = sil.receiptDate
  942. ? Array.isArray(sil.receiptDate)
  943. ? arrayToDateString(sil.receiptDate)
  944. : String(sil.receiptDate)
  945. : "-";
  946. const stockQty = Number(sil.acceptedQty ?? 0);
  947. const stockQtyText =
  948. Number.isFinite(stockQty) && stockQty > 0
  949. ? decimalFormatter.format(stockQty)
  950. : decimalFormatter.format(0);
  951. return (
  952. <TableRow key={sil.id} hover>
  953. <TableCell padding="checkbox">
  954. <Checkbox
  955. checked={selectedPrintSilIds.has(sil.id)}
  956. onChange={(e) => togglePrintSilSelection(sil.id, e.target.checked)}
  957. />
  958. </TableCell>
  959. <TableCell>{sil.itemNo}</TableCell>
  960. <TableCell>{sil.itemName}</TableCell>
  961. <TableCell align="right">{stockQtyText}</TableCell>
  962. <TableCell>{sil.stockUomDesc || "-"}</TableCell>
  963. <TableCell>{receiptText}</TableCell>
  964. <TableCell>{sil.productLotNo || "-"}</TableCell>
  965. <TableCell>{statusText}</TableCell>
  966. </TableRow>
  967. );
  968. })}
  969. {eligiblePrintSils.length === 0 && (
  970. <TableRow>
  971. <TableCell colSpan={8}>
  972. <Typography variant="body2" color="text.secondary">
  973. 沒有符合條件的項目
  974. </Typography>
  975. </TableCell>
  976. </TableRow>
  977. )}
  978. </TableBody>
  979. </Table>
  980. </TableContainer>
  981. </Stack>
  982. </DialogContent>
  983. <DialogActions>
  984. <Button onClick={closePrintDialog} disabled={isBulkPrinting}>
  985. 取消
  986. </Button>
  987. <Button
  988. variant="contained"
  989. onClick={handleBulkPrint}
  990. disabled={isBulkPrinting || selectedPrintSilIds.size <= 0 || !selectedPrinter}
  991. >
  992. {isBulkPrinting ? "列印中..." : "列印"}
  993. </Button>
  994. </DialogActions>
  995. </Dialog>
  996. {/* {itemInfo !== undefined && (
  997. <>
  998. <PoQcStockInModal
  999. type={"putaway"}
  1000. open={putAwayOpen}
  1001. warehouse={warehouse}
  1002. setItemDetail={setItemInfo}
  1003. onClose={closePutAwayModal}
  1004. itemDetail={itemInfo}
  1005. />
  1006. </>
  1007. )} */}
  1008. </>
  1009. );
  1010. };
  1011. export default PoDetail;