|
- "use client";
-
- import {
- fetchPoWithStockInLines,
- PoResult,
- PurchaseOrderLine,
- StockInLine,
- } from "@/app/api/po";
- import {
- Box,
- Button,
- ButtonProps,
- Collapse,
- Grid,
- IconButton,
- Paper,
- Stack,
- Tab,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Tabs,
- TabsProps,
- TextField,
- Typography,
- } from "@mui/material";
- import { useTranslation } from "react-i18next";
- // import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid";
- import {
- GridColDef,
- GridRowId,
- GridRowModel,
- useGridApiRef,
- } from "@mui/x-data-grid";
- import {
- checkPolAndCompletePo,
- fetchPoInClient,
- fetchStockInLineInfo,
- PurchaseQcResult,
- startPo,
- } from "@/app/api/po/actions";
- import {
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useState,
- } from "react";
- import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
- import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
- import PoInputGrid from "./PoInputGrid";
- import { QcItemWithChecks } from "@/app/api/qc";
- import { useSearchParams } from "next/navigation";
- import { WarehouseResult } from "@/app/api/warehouse";
- import { calculateWeight, returnWeightUnit } from "@/app/utils/formatUtil";
- import { CameraContext } from "../Cameras/CameraProvider";
- import PoQcStockInModal from "./PoQcStockInModal";
- import QrModal from "./QrModal";
- import { PlayArrow } from "@mui/icons-material";
- import DoneIcon from "@mui/icons-material/Done";
- import { getCustomWidth } from "@/app/utils/commonUtil";
- import PoInfoCard from "./PoInfoCard";
- import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
-
-
-
- import { fetchPoListClient } from "@/app/api/po/actions";
- import { List, ListItem, ListItemButton, ListItemText, Divider } from "@mui/material";
- import { useRouter } from "next/navigation";
-
-
-
- type Props = {
- po: PoResult;
- qc: QcItemWithChecks[];
- warehouse: WarehouseResult[];
- };
-
- type EntryError =
- | {
- [field in keyof StockInLine]?: string;
- }
- | undefined;
- // type PolRow = TableRow<Partial<StockInLine>, EntryError>;
- const PoSearchList: React.FC<{
- poList: PoResult[];
- selectedPoId: number;
- onSelect: (po: PoResult) => void;
- }> = ({ poList, selectedPoId, onSelect }) => {
- const { t } = useTranslation("purchaseOrder");
- const [searchTerm, setSearchTerm] = useState('');
-
- const filteredPoList = useMemo(() => {
- if (searchTerm.trim() === '') {
- return poList;
- }
- return poList.filter(poItem =>
- poItem.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
- poItem.supplier?.toLowerCase().includes(searchTerm.toLowerCase()) ||
- t(`${poItem.status.toLowerCase()}`).toLowerCase().includes(searchTerm.toLowerCase())
- );
- }, [poList, searchTerm, t]);
-
- return (
- <Paper sx={{ p: 2, maxHeight: "400px", overflow: "auto" }}>
- <Typography variant="h6" gutterBottom>
- {t("Purchase Orders")}
- </Typography>
- <TextField
- label={t("Search")}
- variant="outlined"
- size="small"
- fullWidth
- value={searchTerm}
- onChange={(e) => setSearchTerm(e.target.value)}
- sx={{ mb: 2 }}
- InputProps={{
- startAdornment: (
- <Typography variant="body2" color="text.secondary" sx={{ mr: 1 }}>
-
- </Typography>
- ),
- }}
- />
- <List dense>
- {filteredPoList.map((poItem, index) => (
- <div key={poItem.id}>
- <ListItem disablePadding>
- <ListItemButton
- selected={selectedPoId === poItem.id}
- onClick={() => onSelect(poItem)}
- sx={{
- "&.Mui-selected": {
- backgroundColor: "primary.light",
- "&:hover": {
- backgroundColor: "primary.light",
- },
- },
- }}
- >
- <ListItemText
- primary={
- <Typography variant="body2" noWrap>
- {poItem.code}
- </Typography>
- }
- secondary={
- <Typography variant="caption" color="text.secondary">
- {t(`${poItem.status.toLowerCase()}`)}
- </Typography>
- }
- />
- </ListItemButton>
- </ListItem>
- {index < filteredPoList.length - 1 && <Divider />}
- </div>
- ))}
- </List>
- {searchTerm && (
- <Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: "block" }}>
- {t("Found")} {filteredPoList.length} {t("of")} {poList.length} {t("items")}
- </Typography>
- )}
- </Paper>
- );
- };
-
- const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
- const cameras = useContext(CameraContext);
- console.log(cameras);
- const { t } = useTranslation("purchaseOrder");
- const apiRef = useGridApiRef();
- const [purchaseOrder, setPurchaseOrder] = useState({ ...po });
- const [rows, setRows] = useState<PurchaseOrderLine[]>(
- purchaseOrder.pol || [],
- );
- const [row, setRow] = useState(rows[0]);
- const [stockInLine, setStockInLine] = useState(rows[0].stockInLine);
- const [processedQty, setProcessedQty] = useState(rows[0].processed);
-
- const searchParams = useSearchParams();
- // const [currPoStatus, setCurrPoStatus] = useState(purchaseOrder.status);
-
-
-
-
-
- const router = useRouter();
- const [poList, setPoList] = useState<PoResult[]>([]);
- const [selectedPoId, setSelectedPoId] = useState(po.id);
- const currentPoId = searchParams.get('id');
- const [searchTerm, setSearchTerm] = useState('');
-
-
- const filteredPoList = useMemo(() => {
- if (searchTerm.trim() === '') {
- return poList;
- }
- return poList.filter(poItem =>
- poItem.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
- poItem.supplier?.toLowerCase().includes(searchTerm.toLowerCase()) ||
- t(`${poItem.status.toLowerCase()}`).toLowerCase().includes(searchTerm.toLowerCase())
- );
- }, [poList, searchTerm, t]);
-
-
- const fetchPoList = useCallback(async () => {
- try {
- const result = await fetchPoListClient({ limit: 20, offset: 0 });
- if (result && result.records) {
- setPoList(result.records);
- }
- } catch (error) {
- console.error("Failed to fetch PO list:", error);
- }
- }, []);
-
- const handlePoSelect = useCallback(
- (selectedPo: PoResult) => {
- setSelectedPoId(selectedPo.id);
- router.push(`/po/edit?id=${selectedPo.id}&start=true`, { scroll: false });
- },
- [router]
- );
-
- const fetchPoDetail = useCallback(async (poId: string) => {
- try {
- const result = await fetchPoInClient(parseInt(poId));
- if (result) {
- setPurchaseOrder(result);
- setRows(result.pol || []);
- if (result.pol && result.pol.length > 0) {
- setRow(result.pol[0]);
- setStockInLine(result.pol[0].stockInLine);
- setProcessedQty(result.pol[0].processed);
- }
- }
- } catch (error) {
- console.error("Failed to fetch PO detail:", error);
- }
- }, []);
-
-
- useEffect(() => {
- if (currentPoId && currentPoId !== selectedPoId.toString()) {
- setSelectedPoId(parseInt(currentPoId));
- fetchPoDetail(currentPoId);
- }
- }, [currentPoId, selectedPoId, fetchPoDetail]);
-
- useEffect(() => {
- fetchPoList();
- }, [fetchPoList]);
-
- useEffect(() => {
- if (currentPoId) {
- setSelectedPoId(parseInt(currentPoId));
- }
- }, [currentPoId]);
-
-
-
-
-
-
-
-
- const removeParam = (paramToRemove: string) => {
- const newParams = new URLSearchParams(searchParams.toString());
- newParams.delete(paramToRemove);
- window.history.replaceState({}, '', `${window.location.pathname}?${newParams}`);
- };
-
- const handleCompletePo = useCallback(async () => {
- const checkRes = await checkPolAndCompletePo(purchaseOrder.id);
- console.log(checkRes);
- const newPo = await fetchPoInClient(purchaseOrder.id);
- setPurchaseOrder(newPo);
- }, [purchaseOrder.id]);
-
- const handleStartPo = useCallback(async () => {
- const startRes = await startPo(purchaseOrder.id);
- console.log(startRes);
- const newPo = await fetchPoInClient(purchaseOrder.id);
- setPurchaseOrder(newPo);
- }, [purchaseOrder.id]);
-
- useEffect(() => {
- setRows(purchaseOrder.pol || []);
- }, [purchaseOrder]);
-
- function Row(props: { row: PurchaseOrderLine }) {
- const { row } = props;
- const [firstReceiveQty, setFirstReceiveQty] = useState<number>()
- const [secondReceiveQty, setSecondReceiveQty] = useState<number>()
- const [open, setOpen] = useState(false);
- const [processedQty, setProcessedQty] = useState(row.processed);
- const [currStatus, setCurrStatus] = useState(row.status);
- const [stockInLine, setStockInLine] = useState(row.stockInLine);
- const totalWeight = useMemo(
- () => calculateWeight(row.qty, row.uom),
- [row.qty, row.uom],
- );
- const weightUnit = useMemo(
- () => returnWeightUnit(row.uom),
- [row.uom],
- );
-
- useEffect(() => {
- if (processedQty === row.qty) {
- setCurrStatus("completed".toUpperCase());
- } else if (processedQty > 0) {
- setCurrStatus("receiving".toUpperCase());
- } else {
- setCurrStatus("pending".toUpperCase());
- }
- }, [processedQty, row.qty]);
-
- return (
- <>
- <TableRow sx={{ "& > *": { borderBottom: "unset" }, color: "black" }}>
- {/* <TableCell>
- <IconButton
- disabled={purchaseOrder.status.toLowerCase() === "pending"}
- aria-label="expand row"
- size="small"
- onClick={() => setOpen(!open)}
- >
- {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
- </IconButton>
- </TableCell> */}
- <TableCell align="left">{row.itemNo}</TableCell>
- <TableCell align="left">{row.itemName}</TableCell>
- <TableCell align="right">{integerFormatter.format(row.qty)}</TableCell>
- <TableCell align="right">{integerFormatter.format(processedQty)}</TableCell>
- <TableCell align="left">{row.uom?.code}</TableCell>
- <TableCell align="right">
- {decimalFormatter.format(totalWeight)} {weightUnit}
- </TableCell>
- {/* <TableCell align="left">{weightUnit}</TableCell> */}
- <TableCell align="right">{decimalFormatter.format(row.price)}</TableCell>
- {/* <TableCell align="left">{row.expiryDate}</TableCell> */}
- <TableCell align="left">{t(`${currStatus.toLowerCase()}`)}</TableCell>
- <TableCell align="right">
- 0
- </TableCell>
- <TableCell align="center">
- <TextField
- label="輸入來貨數量"
- type="text" // Use type="text" to allow validation in the change handler
- variant="outlined"
- value={secondReceiveQty}
- // onChange={handleChange}
- InputProps={{
- inputProps: {
- min: 0, // Optional: set a minimum value
- step: 1 // Optional: set the step for the number input
- }
- }}
- />
- </TableCell>
- <TableCell align="center">
- <Button variant="contained">
- 提交
- </Button>
- </TableCell>
- </TableRow>
- {/* <TableRow> */}
- {/* <TableCell /> */}
- {/* <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={12}> */}
- {/* <Collapse in={true} timeout="auto" unmountOnExit> */}
- {/* <Collapse in={open} timeout="auto" unmountOnExit> */}
- {/* <Table>
- <TableBody>
- <TableRow>
- <TableCell align="right">
- <Box>
- <PoInputGrid
- qc={qc}
- setRows={setRows}
- stockInLine={stockInLine}
- setStockInLine={setStockInLine}
- setProcessedQty={setProcessedQty}
- itemDetail={row}
- warehouse={warehouse}
- />
- </Box>
- </TableCell>
- </TableRow>
- </TableBody>
- </Table> */}
- {/* </Collapse> */}
- {/* </TableCell> */}
- {/* </TableRow> */}
- </>
- );
- }
- const [tabIndex, setTabIndex] = useState(0);
- const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
- (_e, newValue) => {
- setTabIndex(newValue);
- },
- [],
- );
-
- const [isOpenScanner, setOpenScanner] = useState(false);
- const onOpenScanner = useCallback(() => {
- setOpenScanner(true);
- }, []);
-
- const onCloseScanner = useCallback(() => {
- setOpenScanner(false);
- }, []);
-
- const [itemInfo, setItemInfo] = useState<
- StockInLine & { warehouseId?: number }
- >();
- const [putAwayOpen, setPutAwayOpen] = useState(false);
- // const [scannedInfo, setScannedInfo] = useState<QrCodeInfo>({} as QrCodeInfo);
-
- const closePutAwayModal = useCallback(() => {
- setPutAwayOpen(false);
- setItemInfo(undefined);
- }, []);
- const openPutAwayModal = useCallback(() => {
- setPutAwayOpen(true);
- }, []);
-
- const buttonData = useMemo(() => {
- switch (purchaseOrder.status.toLowerCase()) {
- case "pending":
- return {
- buttonName: "start",
- title: t("Do you want to start?"),
- confirmButtonText: t("Start"),
- successTitle: t("Start Success"),
- errorTitle: t("Start Fail"),
- buttonText: t("Start PO"),
- buttonIcon: <PlayArrow />,
- buttonColor: "success",
- disabled: false,
- onClick: handleStartPo,
- };
- case "receiving":
- return {
- buttonName: "complete",
- title: t("Do you want to complete?"),
- confirmButtonText: t("Complete"),
- successTitle: t("Complete Success"),
- errorTitle: t("Complete Fail"),
- buttonText: t("Complete PO"),
- buttonIcon: <DoneIcon />,
- buttonColor: "info",
- disabled: false,
- onClick: handleCompletePo,
- };
- default:
- return {
- buttonName: "complete",
- title: t("Do you want to complete?"),
- confirmButtonText: t("Complete"),
- successTitle: t("Complete Success"),
- errorTitle: t("Complete Fail"),
- buttonText: t("Complete PO"),
- buttonIcon: <DoneIcon />,
- buttonColor: "info",
- disabled: true,
- };
- // break;
- }
- }, [purchaseOrder.status, t, handleStartPo, handleCompletePo]);
-
- const FIRST_IN_FIELD = "firstInQty"
- const SECOND_IN_FIELD = "secondInQty"
-
- const renderFieldCondition = useCallback((field: "firstInQty" | "secondInQty"): boolean => {
- switch (field) {
- case FIRST_IN_FIELD:
-
- return true;
- case SECOND_IN_FIELD:
- return true;
- default:
- return false; // Default case
- }
- }, []);
-
- return (
- <>
- <Stack spacing={2}>
- {/* Area1: title */}
- <Grid container xs={12} justifyContent="start">
- <Grid item>
- <Typography mb={2} variant="h4">
- {purchaseOrder.code} -{" "}
- {t(`${purchaseOrder.status.toLowerCase()}`)}
- </Typography>
- </Grid>
- </Grid>
-
- {/* area2: dn info */}
- <Grid container spacing={2}>
- {/* left side select po */}
- <Grid item xs={3}>
- <PoSearchList
- poList={poList}
- selectedPoId={selectedPoId}
- onSelect={handlePoSelect}
- />
- </Grid>
-
- {/* right side po info */}
- <Grid item xs={9}>
- <PoInfoCard po={purchaseOrder} />
- {true ? (
- <Stack spacing={2}>
- <TextField
- label={t("dnNo")}
- type="text"
- variant="outlined"
- fullWidth
- InputProps={{
- inputProps: {
- min: 0,
- step: 1
- }
- }}
- />
- <TextField
- label={t("dnDate")}
- type="text"
- variant="outlined"
- defaultValue={"11/08/2025"}
- fullWidth
- InputProps={{
- inputProps: {
- min: 0,
- step: 1
- }
- }}
- />
- <Button variant="contained" onClick={onOpenScanner} fullWidth>
- 提交
- </Button>
- </Stack>
- ) : undefined}
- </Grid>
- </Grid>
-
-
-
- {/* Area4: Main Table */}
- <Grid container xs={12} justifyContent="start">
- <Grid item xs={12}>
- <TableContainer component={Paper} sx={{ width: 'fit-content', overflow: 'auto' }}>
- <Table aria-label="collapsible table" stickyHeader>
- <TableHead>
- <TableRow>
- <TableCell sx={{ width: '125px' }}>{t("itemNo")}</TableCell>
- <TableCell align="left" sx={{ width: '125px' }}>{t("itemName")}</TableCell>
- <TableCell align="right">{t("qty")}</TableCell>
- <TableCell align="right">{t("processed")}</TableCell>
- <TableCell align="left">{t("uom")}</TableCell>
- <TableCell align="right">{t("total weight")}</TableCell>
- <TableCell align="right">{`${t("price")} (HKD)`}</TableCell>
- <TableCell align="left" sx={{ width: '75px' }}>{t("status")}</TableCell>
- {renderFieldCondition(FIRST_IN_FIELD) ? <TableCell align="right">{t("receivedQty")}</TableCell> : undefined}
- {renderFieldCondition(SECOND_IN_FIELD) ? <TableCell align="center" sx={{ width: '150px' }}>{t("dnQty")}(以訂單單位計算)</TableCell> : undefined}
- <TableCell align="center" sx={{ width: '100px' }}></TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {rows.map((row) => (
- <Row key={row.id} row={row} />
- ))}
- </TableBody>
- </Table>
- </TableContainer>
- </Grid>
- </Grid>
-
- {/* area5: selected item info */}
- <Grid container xs={12} justifyContent="start">
- <Grid item xs={12}>
- <Typography variant="h6">已選擇: {row.itemNo}-{row.itemName}</Typography>
- </Grid>
- <Grid item xs={12}>
- <TableContainer component={Paper} sx={{ width: 'fit-content', overflow: 'auto' }}>
- <Table>
- <TableBody>
- <TableRow>
- <TableCell align="right">
- <Box>
- <PoInputGrid
- qc={qc}
- setRows={setRows}
- stockInLine={stockInLine}
- setStockInLine={setStockInLine}
- setProcessedQty={setProcessedQty}
- itemDetail={row}
- warehouse={warehouse}
- />
- </Box>
- </TableCell>
- </TableRow>
- </TableBody>
- </Table>
- </TableContainer>
- </Grid>
- </Grid>
- {/* tab 2 */}
- <Grid sx={{ display: tabIndex === 1 ? "block" : "none" }}>
- {/* <StyledDataGrid
-
- /> */}
- </Grid>
- </Stack>
- {itemInfo !== undefined && (
- <>
- <PoQcStockInModal
- type={"putaway"}
- open={putAwayOpen}
- warehouse={warehouse}
- setItemDetail={setItemInfo}
- onClose={closePutAwayModal}
- itemDetail={itemInfo}
- />
- </>
- )}
- </>
- );
- };
- export default PoDetail;
|