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

636 行
20 KiB

  1. "use client";
  2. import {
  3. fetchPoWithStockInLines,
  4. PoResult,
  5. PurchaseOrderLine,
  6. StockInLine,
  7. } from "@/app/api/po";
  8. import {
  9. Box,
  10. Button,
  11. ButtonProps,
  12. Collapse,
  13. Grid,
  14. IconButton,
  15. Paper,
  16. Stack,
  17. Tab,
  18. Table,
  19. TableBody,
  20. TableCell,
  21. TableContainer,
  22. TableHead,
  23. TableRow,
  24. Tabs,
  25. TabsProps,
  26. TextField,
  27. Typography,
  28. } from "@mui/material";
  29. import { useTranslation } from "react-i18next";
  30. // import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid";
  31. import {
  32. GridColDef,
  33. GridRowId,
  34. GridRowModel,
  35. useGridApiRef,
  36. } from "@mui/x-data-grid";
  37. import {
  38. checkPolAndCompletePo,
  39. fetchPoInClient,
  40. fetchStockInLineInfo,
  41. PurchaseQcResult,
  42. startPo,
  43. } from "@/app/api/po/actions";
  44. import {
  45. useCallback,
  46. useContext,
  47. useEffect,
  48. useMemo,
  49. useState,
  50. } from "react";
  51. import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
  52. import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
  53. import PoInputGrid from "./PoInputGrid";
  54. import { QcItemWithChecks } from "@/app/api/qc";
  55. import { useSearchParams } from "next/navigation";
  56. import { WarehouseResult } from "@/app/api/warehouse";
  57. import { calculateWeight, returnWeightUnit } from "@/app/utils/formatUtil";
  58. import { CameraContext } from "../Cameras/CameraProvider";
  59. import PoQcStockInModal from "./PoQcStockInModal";
  60. import QrModal from "./QrModal";
  61. import { PlayArrow } from "@mui/icons-material";
  62. import DoneIcon from "@mui/icons-material/Done";
  63. import { getCustomWidth } from "@/app/utils/commonUtil";
  64. import PoInfoCard from "./PoInfoCard";
  65. import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
  66. import { fetchPoListClient } from "@/app/api/po/actions";
  67. import { List, ListItem, ListItemButton, ListItemText, Divider } from "@mui/material";
  68. import { useRouter } from "next/navigation";
  69. type Props = {
  70. po: PoResult;
  71. qc: QcItemWithChecks[];
  72. warehouse: WarehouseResult[];
  73. };
  74. type EntryError =
  75. | {
  76. [field in keyof StockInLine]?: string;
  77. }
  78. | undefined;
  79. // type PolRow = TableRow<Partial<StockInLine>, EntryError>;
  80. const PoSearchList: React.FC<{
  81. poList: PoResult[];
  82. selectedPoId: number;
  83. onSelect: (po: PoResult) => void;
  84. }> = ({ poList, selectedPoId, onSelect }) => {
  85. const { t } = useTranslation("purchaseOrder");
  86. const [searchTerm, setSearchTerm] = useState('');
  87. const filteredPoList = useMemo(() => {
  88. if (searchTerm.trim() === '') {
  89. return poList;
  90. }
  91. return poList.filter(poItem =>
  92. poItem.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
  93. poItem.supplier?.toLowerCase().includes(searchTerm.toLowerCase()) ||
  94. t(`${poItem.status.toLowerCase()}`).toLowerCase().includes(searchTerm.toLowerCase())
  95. );
  96. }, [poList, searchTerm, t]);
  97. return (
  98. <Paper sx={{ p: 2, maxHeight: "400px", overflow: "auto" }}>
  99. <Typography variant="h6" gutterBottom>
  100. {t("Purchase Orders")}
  101. </Typography>
  102. <TextField
  103. label={t("Search")}
  104. variant="outlined"
  105. size="small"
  106. fullWidth
  107. value={searchTerm}
  108. onChange={(e) => setSearchTerm(e.target.value)}
  109. sx={{ mb: 2 }}
  110. InputProps={{
  111. startAdornment: (
  112. <Typography variant="body2" color="text.secondary" sx={{ mr: 1 }}>
  113. </Typography>
  114. ),
  115. }}
  116. />
  117. <List dense>
  118. {filteredPoList.map((poItem, index) => (
  119. <div key={poItem.id}>
  120. <ListItem disablePadding>
  121. <ListItemButton
  122. selected={selectedPoId === poItem.id}
  123. onClick={() => onSelect(poItem)}
  124. sx={{
  125. "&.Mui-selected": {
  126. backgroundColor: "primary.light",
  127. "&:hover": {
  128. backgroundColor: "primary.light",
  129. },
  130. },
  131. }}
  132. >
  133. <ListItemText
  134. primary={
  135. <Typography variant="body2" noWrap>
  136. {poItem.code}
  137. </Typography>
  138. }
  139. secondary={
  140. <Typography variant="caption" color="text.secondary">
  141. {t(`${poItem.status.toLowerCase()}`)}
  142. </Typography>
  143. }
  144. />
  145. </ListItemButton>
  146. </ListItem>
  147. {index < filteredPoList.length - 1 && <Divider />}
  148. </div>
  149. ))}
  150. </List>
  151. {searchTerm && (
  152. <Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: "block" }}>
  153. {t("Found")} {filteredPoList.length} {t("of")} {poList.length} {t("items")}
  154. </Typography>
  155. )}
  156. </Paper>
  157. );
  158. };
  159. const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
  160. const cameras = useContext(CameraContext);
  161. console.log(cameras);
  162. const { t } = useTranslation("purchaseOrder");
  163. const apiRef = useGridApiRef();
  164. const [purchaseOrder, setPurchaseOrder] = useState({ ...po });
  165. const [rows, setRows] = useState<PurchaseOrderLine[]>(
  166. purchaseOrder.pol || [],
  167. );
  168. const [row, setRow] = useState(rows[0]);
  169. const [stockInLine, setStockInLine] = useState(rows[0].stockInLine);
  170. const [processedQty, setProcessedQty] = useState(rows[0].processed);
  171. const searchParams = useSearchParams();
  172. // const [currPoStatus, setCurrPoStatus] = useState(purchaseOrder.status);
  173. const router = useRouter();
  174. const [poList, setPoList] = useState<PoResult[]>([]);
  175. const [selectedPoId, setSelectedPoId] = useState(po.id);
  176. const currentPoId = searchParams.get('id');
  177. const [searchTerm, setSearchTerm] = useState('');
  178. const filteredPoList = useMemo(() => {
  179. if (searchTerm.trim() === '') {
  180. return poList;
  181. }
  182. return poList.filter(poItem =>
  183. poItem.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
  184. poItem.supplier?.toLowerCase().includes(searchTerm.toLowerCase()) ||
  185. t(`${poItem.status.toLowerCase()}`).toLowerCase().includes(searchTerm.toLowerCase())
  186. );
  187. }, [poList, searchTerm, t]);
  188. const fetchPoList = useCallback(async () => {
  189. try {
  190. const result = await fetchPoListClient({ limit: 20, offset: 0 });
  191. if (result && result.records) {
  192. setPoList(result.records);
  193. }
  194. } catch (error) {
  195. console.error("Failed to fetch PO list:", error);
  196. }
  197. }, []);
  198. const handlePoSelect = useCallback(
  199. (selectedPo: PoResult) => {
  200. setSelectedPoId(selectedPo.id);
  201. router.push(`/po/edit?id=${selectedPo.id}&start=true`, { scroll: false });
  202. },
  203. [router]
  204. );
  205. const fetchPoDetail = useCallback(async (poId: string) => {
  206. try {
  207. const result = await fetchPoInClient(parseInt(poId));
  208. if (result) {
  209. setPurchaseOrder(result);
  210. setRows(result.pol || []);
  211. if (result.pol && result.pol.length > 0) {
  212. setRow(result.pol[0]);
  213. setStockInLine(result.pol[0].stockInLine);
  214. setProcessedQty(result.pol[0].processed);
  215. }
  216. }
  217. } catch (error) {
  218. console.error("Failed to fetch PO detail:", error);
  219. }
  220. }, []);
  221. useEffect(() => {
  222. if (currentPoId && currentPoId !== selectedPoId.toString()) {
  223. setSelectedPoId(parseInt(currentPoId));
  224. fetchPoDetail(currentPoId);
  225. }
  226. }, [currentPoId, selectedPoId, fetchPoDetail]);
  227. useEffect(() => {
  228. fetchPoList();
  229. }, [fetchPoList]);
  230. useEffect(() => {
  231. if (currentPoId) {
  232. setSelectedPoId(parseInt(currentPoId));
  233. }
  234. }, [currentPoId]);
  235. const removeParam = (paramToRemove: string) => {
  236. const newParams = new URLSearchParams(searchParams.toString());
  237. newParams.delete(paramToRemove);
  238. window.history.replaceState({}, '', `${window.location.pathname}?${newParams}`);
  239. };
  240. const handleCompletePo = useCallback(async () => {
  241. const checkRes = await checkPolAndCompletePo(purchaseOrder.id);
  242. console.log(checkRes);
  243. const newPo = await fetchPoInClient(purchaseOrder.id);
  244. setPurchaseOrder(newPo);
  245. }, [purchaseOrder.id]);
  246. const handleStartPo = useCallback(async () => {
  247. const startRes = await startPo(purchaseOrder.id);
  248. console.log(startRes);
  249. const newPo = await fetchPoInClient(purchaseOrder.id);
  250. setPurchaseOrder(newPo);
  251. }, [purchaseOrder.id]);
  252. useEffect(() => {
  253. setRows(purchaseOrder.pol || []);
  254. }, [purchaseOrder]);
  255. function Row(props: { row: PurchaseOrderLine }) {
  256. const { row } = props;
  257. const [firstReceiveQty, setFirstReceiveQty] = useState<number>()
  258. const [secondReceiveQty, setSecondReceiveQty] = useState<number>()
  259. const [open, setOpen] = useState(false);
  260. const [processedQty, setProcessedQty] = useState(row.processed);
  261. const [currStatus, setCurrStatus] = useState(row.status);
  262. const [stockInLine, setStockInLine] = useState(row.stockInLine);
  263. const totalWeight = useMemo(
  264. () => calculateWeight(row.qty, row.uom),
  265. [row.qty, row.uom],
  266. );
  267. const weightUnit = useMemo(
  268. () => returnWeightUnit(row.uom),
  269. [row.uom],
  270. );
  271. useEffect(() => {
  272. if (processedQty === row.qty) {
  273. setCurrStatus("completed".toUpperCase());
  274. } else if (processedQty > 0) {
  275. setCurrStatus("receiving".toUpperCase());
  276. } else {
  277. setCurrStatus("pending".toUpperCase());
  278. }
  279. }, [processedQty, row.qty]);
  280. return (
  281. <>
  282. <TableRow sx={{ "& > *": { borderBottom: "unset" }, color: "black" }}>
  283. {/* <TableCell>
  284. <IconButton
  285. disabled={purchaseOrder.status.toLowerCase() === "pending"}
  286. aria-label="expand row"
  287. size="small"
  288. onClick={() => setOpen(!open)}
  289. >
  290. {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
  291. </IconButton>
  292. </TableCell> */}
  293. <TableCell align="left">{row.itemNo}</TableCell>
  294. <TableCell align="left">{row.itemName}</TableCell>
  295. <TableCell align="right">{integerFormatter.format(row.qty)}</TableCell>
  296. <TableCell align="right">{integerFormatter.format(processedQty)}</TableCell>
  297. <TableCell align="left">{row.uom?.code}</TableCell>
  298. <TableCell align="right">
  299. {decimalFormatter.format(totalWeight)} {weightUnit}
  300. </TableCell>
  301. {/* <TableCell align="left">{weightUnit}</TableCell> */}
  302. <TableCell align="right">{decimalFormatter.format(row.price)}</TableCell>
  303. {/* <TableCell align="left">{row.expiryDate}</TableCell> */}
  304. <TableCell align="left">{t(`${currStatus.toLowerCase()}`)}</TableCell>
  305. <TableCell align="right">
  306. 0
  307. </TableCell>
  308. <TableCell align="center">
  309. <TextField
  310. label="輸入來貨數量"
  311. type="text" // Use type="text" to allow validation in the change handler
  312. variant="outlined"
  313. value={secondReceiveQty}
  314. // onChange={handleChange}
  315. InputProps={{
  316. inputProps: {
  317. min: 0, // Optional: set a minimum value
  318. step: 1 // Optional: set the step for the number input
  319. }
  320. }}
  321. />
  322. </TableCell>
  323. <TableCell align="center">
  324. <Button variant="contained">
  325. 提交
  326. </Button>
  327. </TableCell>
  328. </TableRow>
  329. {/* <TableRow> */}
  330. {/* <TableCell /> */}
  331. {/* <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={12}> */}
  332. {/* <Collapse in={true} timeout="auto" unmountOnExit> */}
  333. {/* <Collapse in={open} timeout="auto" unmountOnExit> */}
  334. {/* <Table>
  335. <TableBody>
  336. <TableRow>
  337. <TableCell align="right">
  338. <Box>
  339. <PoInputGrid
  340. qc={qc}
  341. setRows={setRows}
  342. stockInLine={stockInLine}
  343. setStockInLine={setStockInLine}
  344. setProcessedQty={setProcessedQty}
  345. itemDetail={row}
  346. warehouse={warehouse}
  347. />
  348. </Box>
  349. </TableCell>
  350. </TableRow>
  351. </TableBody>
  352. </Table> */}
  353. {/* </Collapse> */}
  354. {/* </TableCell> */}
  355. {/* </TableRow> */}
  356. </>
  357. );
  358. }
  359. const [tabIndex, setTabIndex] = useState(0);
  360. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  361. (_e, newValue) => {
  362. setTabIndex(newValue);
  363. },
  364. [],
  365. );
  366. const [isOpenScanner, setOpenScanner] = useState(false);
  367. const onOpenScanner = useCallback(() => {
  368. setOpenScanner(true);
  369. }, []);
  370. const onCloseScanner = useCallback(() => {
  371. setOpenScanner(false);
  372. }, []);
  373. const [itemInfo, setItemInfo] = useState<
  374. StockInLine & { warehouseId?: number }
  375. >();
  376. const [putAwayOpen, setPutAwayOpen] = useState(false);
  377. // const [scannedInfo, setScannedInfo] = useState<QrCodeInfo>({} as QrCodeInfo);
  378. const closePutAwayModal = useCallback(() => {
  379. setPutAwayOpen(false);
  380. setItemInfo(undefined);
  381. }, []);
  382. const openPutAwayModal = useCallback(() => {
  383. setPutAwayOpen(true);
  384. }, []);
  385. const buttonData = useMemo(() => {
  386. switch (purchaseOrder.status.toLowerCase()) {
  387. case "pending":
  388. return {
  389. buttonName: "start",
  390. title: t("Do you want to start?"),
  391. confirmButtonText: t("Start"),
  392. successTitle: t("Start Success"),
  393. errorTitle: t("Start Fail"),
  394. buttonText: t("Start PO"),
  395. buttonIcon: <PlayArrow />,
  396. buttonColor: "success",
  397. disabled: false,
  398. onClick: handleStartPo,
  399. };
  400. case "receiving":
  401. return {
  402. buttonName: "complete",
  403. title: t("Do you want to complete?"),
  404. confirmButtonText: t("Complete"),
  405. successTitle: t("Complete Success"),
  406. errorTitle: t("Complete Fail"),
  407. buttonText: t("Complete PO"),
  408. buttonIcon: <DoneIcon />,
  409. buttonColor: "info",
  410. disabled: false,
  411. onClick: handleCompletePo,
  412. };
  413. default:
  414. return {
  415. buttonName: "complete",
  416. title: t("Do you want to complete?"),
  417. confirmButtonText: t("Complete"),
  418. successTitle: t("Complete Success"),
  419. errorTitle: t("Complete Fail"),
  420. buttonText: t("Complete PO"),
  421. buttonIcon: <DoneIcon />,
  422. buttonColor: "info",
  423. disabled: true,
  424. };
  425. // break;
  426. }
  427. }, [purchaseOrder.status, t, handleStartPo, handleCompletePo]);
  428. const FIRST_IN_FIELD = "firstInQty"
  429. const SECOND_IN_FIELD = "secondInQty"
  430. const renderFieldCondition = useCallback((field: "firstInQty" | "secondInQty"): boolean => {
  431. switch (field) {
  432. case FIRST_IN_FIELD:
  433. return true;
  434. case SECOND_IN_FIELD:
  435. return true;
  436. default:
  437. return false; // Default case
  438. }
  439. }, []);
  440. return (
  441. <>
  442. <Stack spacing={2}>
  443. {/* Area1: title */}
  444. <Grid container xs={12} justifyContent="start">
  445. <Grid item>
  446. <Typography mb={2} variant="h4">
  447. {purchaseOrder.code} -{" "}
  448. {t(`${purchaseOrder.status.toLowerCase()}`)}
  449. </Typography>
  450. </Grid>
  451. </Grid>
  452. {/* area2: dn info */}
  453. <Grid container spacing={2}>
  454. {/* left side select po */}
  455. <Grid item xs={3}>
  456. <PoSearchList
  457. poList={poList}
  458. selectedPoId={selectedPoId}
  459. onSelect={handlePoSelect}
  460. />
  461. </Grid>
  462. {/* right side po info */}
  463. <Grid item xs={9}>
  464. <PoInfoCard po={purchaseOrder} />
  465. {true ? (
  466. <Stack spacing={2}>
  467. <TextField
  468. label={t("dnNo")}
  469. type="text"
  470. variant="outlined"
  471. fullWidth
  472. InputProps={{
  473. inputProps: {
  474. min: 0,
  475. step: 1
  476. }
  477. }}
  478. />
  479. <TextField
  480. label={t("dnDate")}
  481. type="text"
  482. variant="outlined"
  483. defaultValue={"11/08/2025"}
  484. fullWidth
  485. InputProps={{
  486. inputProps: {
  487. min: 0,
  488. step: 1
  489. }
  490. }}
  491. />
  492. <Button variant="contained" onClick={onOpenScanner} fullWidth>
  493. 提交
  494. </Button>
  495. </Stack>
  496. ) : undefined}
  497. </Grid>
  498. </Grid>
  499. {/* Area4: Main Table */}
  500. <Grid container xs={12} justifyContent="start">
  501. <Grid item xs={12}>
  502. <TableContainer component={Paper} sx={{ width: 'fit-content', overflow: 'auto' }}>
  503. <Table aria-label="collapsible table" stickyHeader>
  504. <TableHead>
  505. <TableRow>
  506. <TableCell sx={{ width: '125px' }}>{t("itemNo")}</TableCell>
  507. <TableCell align="left" sx={{ width: '125px' }}>{t("itemName")}</TableCell>
  508. <TableCell align="right">{t("qty")}</TableCell>
  509. <TableCell align="right">{t("processed")}</TableCell>
  510. <TableCell align="left">{t("uom")}</TableCell>
  511. <TableCell align="right">{t("total weight")}</TableCell>
  512. <TableCell align="right">{`${t("price")} (HKD)`}</TableCell>
  513. <TableCell align="left" sx={{ width: '75px' }}>{t("status")}</TableCell>
  514. {renderFieldCondition(FIRST_IN_FIELD) ? <TableCell align="right">{t("receivedQty")}</TableCell> : undefined}
  515. {renderFieldCondition(SECOND_IN_FIELD) ? <TableCell align="center" sx={{ width: '150px' }}>{t("dnQty")}(以訂單單位計算)</TableCell> : undefined}
  516. <TableCell align="center" sx={{ width: '100px' }}></TableCell>
  517. </TableRow>
  518. </TableHead>
  519. <TableBody>
  520. {rows.map((row) => (
  521. <Row key={row.id} row={row} />
  522. ))}
  523. </TableBody>
  524. </Table>
  525. </TableContainer>
  526. </Grid>
  527. </Grid>
  528. {/* area5: selected item info */}
  529. <Grid container xs={12} justifyContent="start">
  530. <Grid item xs={12}>
  531. <Typography variant="h6">已選擇: {row.itemNo}-{row.itemName}</Typography>
  532. </Grid>
  533. <Grid item xs={12}>
  534. <TableContainer component={Paper} sx={{ width: 'fit-content', overflow: 'auto' }}>
  535. <Table>
  536. <TableBody>
  537. <TableRow>
  538. <TableCell align="right">
  539. <Box>
  540. <PoInputGrid
  541. qc={qc}
  542. setRows={setRows}
  543. stockInLine={stockInLine}
  544. setStockInLine={setStockInLine}
  545. setProcessedQty={setProcessedQty}
  546. itemDetail={row}
  547. warehouse={warehouse}
  548. />
  549. </Box>
  550. </TableCell>
  551. </TableRow>
  552. </TableBody>
  553. </Table>
  554. </TableContainer>
  555. </Grid>
  556. </Grid>
  557. {/* tab 2 */}
  558. <Grid sx={{ display: tabIndex === 1 ? "block" : "none" }}>
  559. {/* <StyledDataGrid
  560. /> */}
  561. </Grid>
  562. </Stack>
  563. {itemInfo !== undefined && (
  564. <>
  565. <PoQcStockInModal
  566. type={"putaway"}
  567. open={putAwayOpen}
  568. warehouse={warehouse}
  569. setItemDetail={setItemInfo}
  570. onClose={closePutAwayModal}
  571. itemDetail={itemInfo}
  572. />
  573. </>
  574. )}
  575. </>
  576. );
  577. };
  578. export default PoDetail;