|
- "use client";
-
- import {
- fetchPoWithStockInLines,
- PoResult,
- PurchaseOrderLine,
- } from "@/app/api/po";
- import {
- Box,
- Button,
- ButtonProps,
- Collapse,
- Grid,
- IconButton,
- Paper,
- Stack,
- Tab,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Tabs,
- TabsProps,
- TextField,
- Typography,
- Checkbox,
- FormControlLabel,
- Card,
- CardContent,
- Radio,
- alpha,
- Autocomplete,
- Dialog,
- DialogActions,
- DialogContent,
- DialogTitle,
- } from "@mui/material";
- import { useTranslation } from "react-i18next";
- import { submitDialogWithWarning } from "../Swal/CustomAlerts";
- // import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid";
- import {
- GridColDef,
- GridRowId,
- GridRowModel,
- useGridApiRef,
- } from "@mui/x-data-grid";
- import {
- checkPolAndCompletePo,
- fetchPoInClient,
- fetchPoSummariesClient,
- startPo,
- } from "@/app/api/po/actions";
- import {
- createStockInLine
- } from "@/app/api/stockIn/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 { useRouter, useSearchParams, usePathname } from "next/navigation";
- import { WarehouseResult } from "@/app/api/warehouse";
- import { calculateWeight, dateStringToDayjs, dayjsToDateString, OUTPUT_DATE_FORMAT, outputDateStringToInputDateString, returnWeightUnit } from "@/app/utils/formatUtil";
- import { CameraContext } from "../Cameras/CameraProvider";
- import QrModal from "./QrModal";
- import { PlayArrow } from "@mui/icons-material";
- import DoneIcon from "@mui/icons-material/Done";
- import { downloadFile, getCustomWidth } from "@/app/utils/commonUtil";
- import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
- import { arrayToDateString } from "@/app/utils/formatUtil";
- import { List, ListItem, ListItemButton, ListItemText, Divider } from "@mui/material";
- import { Controller, FormProvider, useForm } from "react-hook-form";
- import dayjs, { Dayjs } from "dayjs";
- import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
- import { DatePicker, LocalizationProvider, zhHK } from "@mui/x-date-pickers";
- import LoadingComponent from "../General/LoadingComponent";
- import { getMailTemplatePdfForStockInLine } from "@/app/api/mailTemplate/actions";
- import { PrinterCombo } from "@/app/api/settings/printer";
- import { EscalationCombo } from "@/app/api/user";
- import { StockInLine } from "@/app/api/stockIn";
- import { printQrCodeForSil } from "@/app/api/stockIn/actions";
- import { useSession } from "next-auth/react";
- import { AUTH } from "@/authorities";
- //import { useRouter } from "next/navigation";
-
-
- type Props = {
- po: PoResult;
- // qc: QcItemWithChecks[];
- warehouse: WarehouseResult[];
- printerCombo: PrinterCombo[];
- };
-
- /** PO stock-in lines still in pre-complete workflow (align with nav alert: pending / receiving). */
- const PURCHASE_STOCK_IN_ALERT_STATUSES = new Set(["pending", "receiving"]);
-
- /** Sum of put-away in stock units (matches StockInForm「已上架數量」stockQty). */
- function totalPutAwayStockQtyForPol(row: PurchaseOrderLine): number {
- return row.stockInLine
- .filter((sil) => sil.purchaseOrderLineId === row.id)
- .reduce((acc, sil) => {
- const lineSum =
- sil.putAwayLines?.reduce(
- (s, p) => s + Number(p.stockQty ?? p.qty ?? 0),
- 0,
- ) ?? 0;
- return acc + lineSum;
- }, 0);
- }
-
- /** POL order demand in stock units (same basis as PoDetail processed / backend PO detail). */
- function polOrderStockQty(row: PurchaseOrderLine): number {
- return Number(row.stockUom?.stockQty ?? row.qty ?? 0);
- }
-
- function purchaseOrderLineHasIncompleteStockIn(row: PurchaseOrderLine): boolean {
- const orderStock = polOrderStockQty(row);
- const putAway = totalPutAwayStockQtyForPol(row);
- if (orderStock > 0 && putAway >= orderStock) {
- return false;
- }
- return row.stockInLine
- .filter((sil) => sil.purchaseOrderLineId === row.id)
- .some((sil) =>
- PURCHASE_STOCK_IN_ALERT_STATUSES.has((sil.status ?? "").toLowerCase().trim()),
- );
- }
-
- 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;
- loading?: boolean;
- }> = ({ poList, selectedPoId, onSelect, loading = false }) => {
- const { t } = useTranslation(["purchaseOrder", "dashboard"]);
- 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,
- minWidth: "300px",
- height: "100%",
- display: "flex",
- flexDirection: "column",
- overflow: "hidden",
- }}
- >
- <Typography variant="h6" gutterBottom>
- {t("Purchase Order")}
- </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>
- ),
- }}
- />
- <Box sx={{ flex: 1, overflow: "auto" }}>
- {loading ? (
- <LoadingComponent />
- ) : filteredPoList.length > 0 ? (
- <List dense sx={{ width: "100%" }}>
- {filteredPoList.map((poItem, index) => (
- <div key={poItem.id}>
- <ListItem disablePadding sx={{ width: "100%" }}>
- <ListItemButton
- selected={selectedPoId === poItem.id}
- onClick={() => onSelect(poItem)}
- sx={{
- width: "100%",
- "&.Mui-selected": {
- backgroundColor: "primary.light",
- "&:hover": {
- backgroundColor: "primary.light",
- },
- },
- }}
- >
- <ListItemText
- primary={
- <Typography variant="body2" sx={{ wordBreak: "break-all" }}>
- {poItem.code}
- </Typography>
- }
- secondary={
- <Typography variant="caption" color="text.secondary">
- {t(`${poItem.status.toLowerCase()}`)}
- </Typography>
- }
- />
- </ListItemButton>
- </ListItem>
- {index < filteredPoList.length - 1 && <Divider />}
- </div>
- ))}
- </List>
- ) : (
- <Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
- {searchTerm.trim()
- ? t("No purchase orders match your search", { defaultValue: "沒有符合搜尋的採購單" })
- : t("No purchase orders to show", { defaultValue: "沒有可顯示的採購單" })}
- </Typography>
- )}
- </Box>
- {searchTerm && (
- <Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: "block" }}>
- {`${t("Found")} ${filteredPoList.length} ${t("Purchase Order")}`}
- {/* {`${t("Found")} ${filteredPoList.length} of ${poList.length} ${t("Item")}`} */}
- </Typography>
- )}
- </Paper>
- );
- };
-
- interface PolInputResult {
- lotNo: string,
- dnQty: string,
- }
-
- const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
- const cameras = useContext(CameraContext);
- const { data: session } = useSession();
- const canSeeStockInReminders = useMemo(() => {
- const set = new Set((session?.user?.abilities ?? []).map((a) => String(a).trim()));
- return set.has(AUTH.TESTING) || set.has(AUTH.ADMIN) || set.has(AUTH.STOCK);
- }, [session?.user?.abilities]);
- // console.log(cameras);
- const { t } = useTranslation("purchaseOrder");
- const apiRef = useGridApiRef();
- const [purchaseOrder, setPurchaseOrder] = useState({ ...po });
- const [rows, setRows] = useState<PurchaseOrderLine[]>(
- purchaseOrder.pol || [],
- );
- const [polInputList, setPolInputList] = useState<Record<number, PolInputResult>>({})
- const PO_DETAIL_SELECTION_KEY = "po-detail-selection";
- useEffect(() => {
- setPolInputList((prev) => {
- const next: Record<number, PolInputResult> = {};
- (purchaseOrder.pol ?? []).forEach((pol) => {
- next[pol.id] = prev[pol.id] ?? {
- lotNo: "",
- dnQty: "",
- };
- });
- return next;
- });
- }, [purchaseOrder.pol]);
- useEffect(() => {
- try {
- const raw = sessionStorage.getItem("po-detail-selection");
- if (raw) {
- const parsed = JSON.parse(raw) as { id: number; code: string; status: string; supplier: string | null }[];
- if (Array.isArray(parsed) && parsed.length > 0) {
- setPoList(parsed as PoResult[]);
-
- sessionStorage.removeItem("po-detail-selection"); // 可选:用一次就删,避免下次从别处进还看到旧数据
- }
- }
- } catch (e) {
- console.warn("sessionStorage getItem/parse failed", e);
- }
- }, []);
- const pathname = usePathname()
- const searchParams = useSearchParams();
-
- const [selectedRow, setSelectedRow] = useState<PurchaseOrderLine | null>(null);
- const [stockInLine, setStockInLine] = useState<StockInLine[]>([]);
- const [processedQty, setProcessedQty] = useState(0);
-
- useEffect(() => {
- const polIdParam = searchParams.get("polId");
- if (!polIdParam || rows.length === 0) return;
- const match = rows.find((r) => r.id.toString() === polIdParam);
- if (match) {
- setSelectedRow(match);
- setStockInLine(match.stockInLine);
- setProcessedQty(match.processed);
- }
- }, [rows, searchParams]);
-
- const router = useRouter();
- const [poList, setPoList] = useState<PoResult[]>(() => [po]);
- const [isPoListLoading, setIsPoListLoading] = useState(false);
- const [selectedPoId, setSelectedPoId] = useState(po.id);
- const [focusField, setFocusField] = useState<HTMLInputElement>();
-
- const currentPoId = searchParams.get('id');
- const selectedIdsParam = searchParams.get('selectedIds');
- // const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
- const dnFormProps = useForm({
- defaultValues: {
- dnNo: '',
- receiptDate: dayjsToDateString(dayjs())
- }
- })
-
- const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | undefined>(
- printerCombo?.[0],
- );
- const [printQty, setPrintQty] = useState(1);
- const [printDialogOpen, setPrintDialogOpen] = useState(false);
- const [isBulkPrinting, setIsBulkPrinting] = useState(false);
- const [printStatusFilter, setPrintStatusFilter] = useState({
- received: true,
- completed: false,
- });
- const [selectedPrintSilIds, setSelectedPrintSilIds] = useState<Set<number>>(
- () => new Set(),
- );
-
- const eligiblePrintSils = useMemo(() => {
- const statusSet = new Set<string>();
- if (printStatusFilter.received) statusSet.add("received");
- if (printStatusFilter.completed) statusSet.add("completed");
- const pols = purchaseOrder.pol ?? [];
- return pols
- .flatMap((pol) => pol.stockInLine ?? [])
- .filter((sil) => statusSet.has((sil.status ?? "").toLowerCase().trim()));
- }, [purchaseOrder.pol, printStatusFilter.completed, printStatusFilter.received]);
-
- const openPrintDialog = useCallback(() => {
- setSelectedPrintSilIds(new Set());
- setPrintDialogOpen(true);
- }, []);
-
- const closePrintDialog = useCallback(() => {
- if (isBulkPrinting) return;
- setPrintDialogOpen(false);
- }, [isBulkPrinting]);
-
- const togglePrintSilSelection = useCallback((id: number, checked: boolean) => {
- setSelectedPrintSilIds((prev) => {
- const next = new Set(prev);
- if (checked) next.add(id);
- else next.delete(id);
- return next;
- });
- }, []);
-
- const setAllVisiblePrintSilsSelected = useCallback((checked: boolean) => {
- setSelectedPrintSilIds(() => {
- if (!checked) return new Set();
- return new Set(eligiblePrintSils.map((s) => s.id));
- });
- }, [eligiblePrintSils]);
-
- const handleBulkPrint = useCallback(async () => {
- if (!selectedPrinter) {
- alert("請先選擇印表機");
- return;
- }
- if (!Number.isFinite(printQty) || printQty <= 0) {
- alert("列印數量必須大於 0");
- return;
- }
- const ids = Array.from(selectedPrintSilIds.values());
- if (ids.length <= 0) {
- alert("請先選擇要列印的項目");
- return;
- }
- setIsBulkPrinting(true);
- try {
- for (const id of ids) {
- await printQrCodeForSil({
- stockInLineId: id,
- printerId: selectedPrinter.id,
- printQty,
- });
- }
- setPrintDialogOpen(false);
- } finally {
- setIsBulkPrinting(false);
- }
- }, [printQty, selectedPrinter, selectedPrintSilIds]);
-
- /** Only loads sidebar list when `selectedIds` is in the URL; otherwise show current PO only (no /po/list fetch). */
- const fetchPoList = useCallback(async () => {
- if (!selectedIdsParam) return;
- setIsPoListLoading(true);
- try {
- const MAX_IDS = 20; // 一次最多加载 20 个,防止卡死
-
- const allIds = selectedIdsParam
- .split(',')
- .map((id) => parseInt(id))
- .filter((id) => !Number.isNaN(id));
-
- const limitedIds = allIds.slice(0, MAX_IDS);
-
- if (allIds.length > MAX_IDS) {
- console.warn(`selectedIds too many (${allIds.length}), only loading first ${MAX_IDS}.`);
- }
- const result = await fetchPoSummariesClient(limitedIds);
- setPoList(result as any);
- } catch (error) {
- console.error("Failed to fetch PO list:", error);
- } finally {
- setIsPoListLoading(false);
- }
- }, [selectedIdsParam]);
-
-
- const fetchPoDetail = useCallback(async (poId: string, preserveDnNo: boolean = false, preferredPolId?: number) => {
- try {
- const result = await fetchPoInClient(parseInt(poId));
- if (result) {
- console.log("%c Fetched PO:", "color:orange", result);
- setPurchaseOrder(result);
- const currentDnNo = preserveDnNo ? dnFormProps.getValues("dnNo") : "";
- dnFormProps.reset({
- dnNo: currentDnNo,
- receiptDate: dayjsToDateString(dayjs()),
- });
- setRows(result.pol || []);
- if (result.pol && result.pol.length > 0) {
- const targetPolId = preferredPolId ?? selectedRow?.id;
- const targetPol =
- result.pol.find((p) => p.id === targetPolId) ?? result.pol[0];
- setSelectedRow(targetPol);
- setStockInLine(targetPol.stockInLine);
- setProcessedQty(targetPol.processed);
- }
- // if (focusField) {console.log(focusField);focusField.focus();}
- }
- } catch (error) {
- console.error("Failed to fetch PO detail:", error);
- }
- }, [selectedRow, selectedPoId]);
-
- const handlePoSelect = useCallback(
- async (selectedPo: PoResult) => {
- if (selectedPo.id === selectedPoId) return;
- setSelectedPoId(selectedPo.id);
- await fetchPoDetail(selectedPo.id.toString());
- const newSelectedIds = selectedIdsParam || selectedPo.id.toString();
- const newUrl = `/po/edit?id=${selectedPo.id}&start=true&selectedIds=${newSelectedIds}`;
- if (pathname + searchParams.toString() !== newUrl) {
- router.replace(newUrl, { scroll: false });
- }
- },
- [selectedPoId, fetchPoDetail, selectedIdsParam, pathname, searchParams, router]
- );
-
- useEffect(() => {
- if (currentPoId && currentPoId !== selectedPoId.toString()) {
- setSelectedPoId(parseInt(currentPoId));
- fetchPoDetail(currentPoId);
- }
- }, [currentPoId, fetchPoDetail]);
-
- useEffect(() => {
- if (selectedIdsParam) {
- void fetchPoList();
- }
- }, [selectedIdsParam, fetchPoList]);
-
- useEffect(() => {
- if (selectedIdsParam) return;
- setPoList([purchaseOrder]);
- }, [selectedIdsParam, purchaseOrder]);
-
- 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]);
-
- const handleMailTemplateForStockInLine = useCallback(async (stockInLineId: number) => {
- const response = await getMailTemplatePdfForStockInLine(stockInLineId)
- if (response) {
- downloadFile(new Uint8Array(response.blobValue), response.filename);
- }
- }, [])
-
- useEffect(() => {
- setRows(purchaseOrder.pol || []);
- }, [purchaseOrder]);
-
- // useEffect(() => {
- // setStockInLine([])
- // }, []);
-
- 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 [lotNoInput, setLotNoInput] = useState(polInputList[row.id]?.lotNo ?? "");
- const [dnQtyInput, setDnQtyInput] = useState(polInputList[row.id]?.dnQty ?? "");
- // 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(() => {
- const polId = searchParams.get("polId") != null ? parseInt(searchParams.get("polId")!) : null
- if (polId) {
- setStockInLine(rows.find((r) => r.id == polId)!.stockInLine)
- }
- }, []);
-
- useEffect(() => {
- // `processedQty` comes from putAwayLines (stock unit).
- // After the fix, `row.qty` is qtyM18 (M18 unit), so compare using stockUom demand.
- const targetStockQty = Number(row.stockUom?.stockQty ?? row.qty ?? 0);
- if (targetStockQty > 0 && processedQty >= targetStockQty) {
- setCurrStatus("completed".toUpperCase());
- } else if (processedQty > 0) {
- setCurrStatus("receiving".toUpperCase());
- } else {
- setCurrStatus("pending".toUpperCase());
- }
- }, [processedQty, row.qty, row.stockUom?.stockQty]);
-
- useEffect(() => {
- setLotNoInput(polInputList[row.id]?.lotNo ?? "");
- setDnQtyInput(polInputList[row.id]?.dnQty ?? "");
- }, [polInputList, row.id]);
-
- const handleRowSelect = () => {
- // setSelectedRowId(row.id);
- setSelectedRow(row);
- setStockInLine(row.stockInLine);
- setProcessedQty(row.processed);
- };
- const changeStockInLines = useCallback(
- (id: number) => {
- //rows = purchaseOrderLine
- const target = rows.find((r) => r.id === id)
- const stockInLine = target!.stockInLine
- setStockInLine(stockInLine)
- setSelectedRow(target!)
- // console.log(pathname)
- // router.replace(`/po/edit?id=${item.poId}&polId=${item.polId}&stockInLineId=${item.stockInLineId}`);
- },
- [rows]
- );
-
- const handleStart = useCallback(
- () => {
- const orderQty = Number(row?.qty) ?? 0;
- const acceptedQty = Number(dnQtyInput.trim());
-
- if (isNaN(acceptedQty) || acceptedQty <= 0) {
- alert("來貨數量必須大於0!");
- return;
- }
- const doSubmit = () => {
- setTimeout(async () => {
- const currentDnNo = dnFormProps.watch("dnNo");
- const postData = {
- dnNo: dnFormProps.watch("dnNo"),
- receiptDate: outputDateStringToInputDateString(dnFormProps.watch("receiptDate")),
- itemId: row.itemId,
- itemNo: row.itemNo,
- itemName: row.itemName,
- purchaseOrderLineId: row.id,
- acceptedQty: acceptedQty,
- productLotNo: lotNoInput || "",
- };
- const res = await createStockInLine(postData);
- if (res) {
- setLotNoInput("");
- setDnQtyInput("");
- setPolInputList((prev) => ({
- ...prev,
- [row.id]: { lotNo: "", dnQty: "" },
- }));
- setSelectedRow(row);
- fetchPoDetail(selectedPoId.toString(), true, row.id);
- }
- console.log(res);
- }, 200);
- };
-
- const exceedOrderBy10Percent = orderQty > 0 && acceptedQty > orderQty * 1.1;
- if (exceedOrderBy10Percent) {
- submitDialogWithWarning(doSubmit, t, {
- title: t("Confirm submit"),
- html: t("This batch quantity exceeds order quantity. Do you still want to submit?"),
- confirmButtonText: t("Submit"),
- });
- } else {
- doSubmit();
- }
- },
- [dnQtyInput, row, dnFormProps, selectedPoId, fetchPoDetail, t, lotNoInput],
- );
-
- const syncRowInputToParent = useCallback((lotNo: string, dnQty: string) => {
- setPolInputList((prev) => {
- const current = prev[row.id] ?? { lotNo: "", dnQty: "" };
- if (current.lotNo === lotNo && current.dnQty === dnQty) return prev;
- return {
- ...prev,
- [row.id]: { lotNo, dnQty },
- };
- });
- }, [row.id]);
-
- // const [focusField, setFocusField] = useState<HTMLInputElement>();
-
- // 本批收貨數量(訂單單位): 使用者在該行輸入的 dnQty
- const batchPurchaseQty = Number(dnQtyInput.trim()) || 0;
-
- // 已來貨總數(庫存單位): 同一 POL 底下所有 stock_in_line.acceptedQty 的合計
- const totalStockReceived = row.stockInLine
- .filter((sil) => sil.purchaseOrderLineId === row.id)
- .reduce((acc, cur) => acc + (cur.acceptedQty ?? 0), 0);
- const receivedTotalText = decimalFormatter.format(totalStockReceived);
- const highlightColor =
- Number(receivedTotalText.replace(/,/g, "")) <= 0 ? "red" : "inherit";
- const needsStockInAttention =
- canSeeStockInReminders && purchaseOrderLineHasIncompleteStockIn(row);
- return (
- <>
-
-
- <TableRow
- hover
- title={
- needsStockInAttention
- ? "採購入庫未完成:此採購明細尚有入庫單為「待處理」或「收貨中」,請於下方完成入庫。"
- : undefined
- }
- sx={{
- "& > *": { borderBottom: "unset" },
- color: "black",
- ...(needsStockInAttention
- ? (theme) => ({
- boxShadow: `inset 4px 0 0 ${theme.palette.error.main}`,
- backgroundColor: alpha(theme.palette.error.main, 0.07),
- })
- : {}),
- }}
- onClick={() => changeStockInLines(row.id)}
- >
-
- {/* <TableCell>
- <IconButton
- disabled={purchaseOrder.status.toLowerCase() === "pending"}
- aria-label="expand row"
- size="small"
- onClick={() => setOpen(!open)}
- >
- {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
- </IconButton>
- </TableCell> */}
- <TableCell align="center" sx={{ width: "60px", position: "relative" }}>
- {needsStockInAttention && (
- <Box
- component="span"
- aria-hidden
- sx={{
- position: "absolute",
- top: 6,
- left: 8,
- width: 10,
- height: 10,
- borderRadius: "50%",
- bgcolor: "error.main",
- border: "2px solid",
- borderColor: "background.paper",
- boxShadow: (theme) => `0 0 0 1px ${alpha(theme.palette.error.main, 0.45)}`,
- zIndex: 1,
- }}
- />
- )}
- <Radio
- checked={selectedRow?.id === row.id}
- // onChange={handleRowSelect}
- // onClick={(e) => e.stopPropagation()}
- />
- </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(row.processed)}</TableCell>
- <TableCell align="left">{row.uom?.udfudesc}</TableCell>
- {/* <TableCell align="right">{decimalFormatter.format(row.stockUom.stockQty)}</TableCell> */}
- {/* <TableCell sx={{ color: highlightColor}} align="right">{receivedTotal}</TableCell> */}
- <TableCell sx={{ color: highlightColor }} align="right">
- {decimalFormatter.format(totalStockReceived)}
- </TableCell>
- <TableCell sx={{ color: highlightColor}} align="left">{row.stockUom.stockUomDesc}</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 sx={{ color: highlightColor}} align="left">{t(`${row.status.toLowerCase()}`)}</TableCell>
- {/* <TableCell sx={{ color: highlightColor}} align="left">{t(`${currStatus.toLowerCase()}`)}</TableCell> */}
- {/* <TableCell align="right">{integerFormatter.format(row.receivedQty)}</TableCell> */}
- <TableCell align="center">
- <TextField
- id="lotNo"
- label="輸入貨品批號"
- type="text" // Use type="text" to allow validation in the change handler
- variant="outlined"
- value={lotNoInput}
- onChange={(e) => setLotNoInput(e.target.value)}
- onBlur={() => syncRowInputToParent(lotNoInput, dnQtyInput)}
- onClick={(e) => e.stopPropagation()}
- // onFocus={(e) => {setFocusField(e.target as HTMLInputElement);}}
- />
- </TableCell>
- <TableCell align="center">
- <TextField
- id="dnQty"
- label="此批送貨數量"
- type="text" // Use type="text" to allow validation in the change handler
- variant="outlined"
- value={dnQtyInput}
- onChange={(e) => setDnQtyInput(e.target.value)}
- onBlur={() => syncRowInputToParent(lotNoInput, dnQtyInput)}
- onClick={(e) => e.stopPropagation()}
- InputProps={{
- inputProps: {
- min: 0, // Optional: set a minimum value
- step: "any",
- inputMode: "decimal",
- }
- }}
- />
- </TableCell>
- <TableCell align="center">
- <Button
- variant="contained"
- onClick={(e) => {
- e.stopPropagation();
- handleStart();
- }}
- >
- {t("submit")}
- </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> */}
- </>
- );
- }
- // ROW END
-
- const [tabIndex, setTabIndex] = useState(0);
- const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
- (_e, newValue) => {
- setTabIndex(newValue);
- },
- [],
- );
-
- const [isOpenScanner, setOpenScanner] = useState(false);
- // const testing = useCallback(() => {
- // // setOpenScanner(true);
- // const newParams = new URLSearchParams(searchParams.toString());
- // console.log(pathname)
- // }, [pathname, router, searchParams]);
-
- 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
- }
- }, []);
-
- const handleDatePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => {
- if (value != null) {
- const updatedValue = dayjsToDateString(value)
- onChange(updatedValue)
- } else {
- onChange(value)
- }
- }, [])
-
- const fillTodayLotNo = useCallback(() => {
- const today = dayjs().format("YYYYMMDD");
- setPolInputList((prev) => {
- const next: Record<number, PolInputResult> = { ...prev };
- (rows ?? []).forEach((r) => {
- const current = next[r.id] ?? { lotNo: "", dnQty: "" };
- const lotNo = (current.lotNo ?? "").trim();
- if (!lotNo) {
- next[r.id] = { ...current, lotNo: today };
- }
- });
- return next;
- });
- }, [rows]);
-
- 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={3} sx={{ maxWidth: 'fit-content' }} alignItems="stretch">
- {/* left side select po */}
- <Grid item xs={4} sx={{ display: "flex" }}>
- <Stack spacing={1} sx={{ flex: 1 }}>
- <PoSearchList
- poList={poList}
- selectedPoId={selectedPoId}
- onSelect={handlePoSelect}
- loading={isPoListLoading}
- />
- </Stack>
- </Grid>
-
- {/* right side po info */}
- <Grid item xs={8}>
- <Grid container spacing={3} sx={{ maxWidth: 'fit-content' }}>
- <Grid item xs={12}>
- <FormProvider {...dnFormProps}>
- <Card sx={{ display: "block" }}>
- <CardContent component={Stack} spacing={2}>
- <TextField
- label={t("Supplier")}
- fullWidth
- disabled={true}
- value={purchaseOrder.supplier ?? ""}
- />
-
- <Grid container spacing={2}>
- <Grid item xs={6}>
- <Stack spacing={2}>
- <TextField
- label={t("Order Date")}
- fullWidth
- disabled={true}
- value={arrayToDateString(purchaseOrder.orderDate as any)}
- />
- <TextField
- {...dnFormProps.register("dnNo")}
- label={t("dnNo")}
- type="text"
- variant="outlined"
- fullWidth
- />
- </Stack>
- </Grid>
-
- <Grid item xs={6}>
- <Stack spacing={2}>
- <TextField
- label={t("ETA")}
- fullWidth
- disabled={true}
- value={arrayToDateString(purchaseOrder.estimatedArrivalDate as any)}
- />
- <LocalizationProvider
- dateAdapter={AdapterDayjs}
- adapterLocale="zh-hk"
- localeText={zhHK.components.MuiLocalizationProvider.defaultProps.localeText}
- >
- <Controller
- control={dnFormProps.control}
- name="receiptDate"
- render={({ field }) => (
- <DatePicker
- label={t("receiptDate")}
- format={`${OUTPUT_DATE_FORMAT}`}
- defaultValue={dateStringToDayjs(field.value)}
- onChange={(newValue: Dayjs | null) => {
- handleDatePickerChange(newValue, field.onChange);
- }}
- slotProps={{ textField: { fullWidth: true } }}
- />
- )}
- />
- </LocalizationProvider>
- </Stack>
- </Grid>
- </Grid>
- </CardContent>
- </Card>
- </FormProvider>
- </Grid>
- <Grid item xs={12}>
- <Grid container spacing={2} alignItems="stretch">
- <Grid item xs={6} sx={{ display: "flex" }}>
- <Card sx={{ display: "block", flex: 1 }}>
- <CardContent component={Stack} spacing={2}>
- <Typography variant="h6">列印</Typography>
- <Autocomplete
- disableClearable
- options={printerCombo}
- value={selectedPrinter}
- onChange={(_event, value) => setSelectedPrinter(value)}
- renderInput={(params) => (
- <TextField
- {...params}
- variant="outlined"
- label={t("Printer")}
- fullWidth
- />
- )}
- />
- <TextField
- variant="outlined"
- label={t("Print Qty")}
- value={printQty}
- onChange={(event) => {
- const cleaned = String(event.target.value).replace(/[^0-9]/g, "");
- setPrintQty(Number(cleaned || 0));
- }}
- fullWidth
- />
- <Button
- variant="contained"
- onClick={openPrintDialog}
- disabled={(printerCombo?.length ?? 0) <= 0}
- >
- 選擇列印項目
- </Button>
- <Typography variant="caption" color="text.secondary">
- 只會顯示「待上架 / 已上架」的來貨記錄
- </Typography>
- </CardContent>
- </Card>
- </Grid>
- <Grid item xs={6} sx={{ display: "flex" }}>
- <Card sx={{ display: "block", flex: 1 }}>
- <CardContent component={Stack} spacing={2} sx={{ height: "100%" }}>
- <Typography variant="h6" sx={{ visibility: "hidden" }}>
- 列印
- </Typography>
- <Button
- variant="outlined"
- onClick={fillTodayLotNo}
- sx={{ flex: 1 }}
- >
- 一鍵填入來貨編號(今日)
- </Button>
- </CardContent>
- </Card>
- </Grid>
- </Grid>
- </Grid>
- </Grid>
- </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 align="center" sx={{ width: '60px' }}></TableCell>
- <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("processedQty")}</TableCell>
- <TableCell align="left">{t("uom")}</TableCell>
- <TableCell align="right">{t("receivedTotal")}</TableCell>
- <TableCell align="left">{t("Stock 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} */}
- <TableCell align="center" sx={{ width: '150px' }}>{t("productLotNo")}</TableCell>
- {renderFieldCondition(SECOND_IN_FIELD) ? <TableCell align="center" sx={{ width: '150px' }}>{t("dnQty")}<br/>(以訂單單位計算)</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">
- {selectedRow ? `已選擇貨品: ${selectedRow?.itemNo ? selectedRow.itemNo : 'N/A'} - ${selectedRow?.itemName ? selectedRow?.itemName : 'N/A'}` : "未選擇貨品"}
- </Typography>
- </Grid>
- <Grid item xs={12}>
- {selectedRow && (
- <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={selectedRow}
- warehouse={warehouse}
- fetchPoDetail={fetchPoDetail}
- handleMailTemplateForStockInLine={handleMailTemplateForStockInLine}
- printerCombo={printerCombo}
- />
- </Box>
- </TableCell>
- </TableRow>
- </TableBody>
- </Table>
- </TableContainer>
- )}
- </Grid>
- </Grid>
- {/* tab 2 */}
- <Grid sx={{ display: tabIndex === 1 ? "block" : "none" }}>
- {/* <StyledDataGrid
-
- /> */}
- </Grid>
- </Stack>
- <Dialog open={printDialogOpen} onClose={closePrintDialog} fullWidth maxWidth="md">
- <DialogTitle>列印標籤</DialogTitle>
- <DialogContent>
- <Stack spacing={1.5} sx={{ mt: 1 }}>
- <Stack direction="row" spacing={2} alignItems="center" flexWrap="wrap">
- <FormControlLabel
- control={
- <Checkbox
- checked={printStatusFilter.received}
- onChange={(e) =>
- setPrintStatusFilter((p) => ({ ...p, received: e.target.checked }))
- }
- />
- }
- label="待上架"
- />
- <FormControlLabel
- control={
- <Checkbox
- checked={printStatusFilter.completed}
- onChange={(e) =>
- setPrintStatusFilter((p) => ({ ...p, completed: e.target.checked }))
- }
- />
- }
- label="已上架"
- />
- <FormControlLabel
- control={
- <Checkbox
- checked={
- eligiblePrintSils.length > 0 &&
- selectedPrintSilIds.size === eligiblePrintSils.length
- }
- indeterminate={
- selectedPrintSilIds.size > 0 &&
- selectedPrintSilIds.size < eligiblePrintSils.length
- }
- onChange={(e) => setAllVisiblePrintSilsSelected(e.target.checked)}
- />
- }
- label="全選(目前篩選結果)"
- />
- <Typography variant="caption" color="text.secondary">
- 已選擇 {selectedPrintSilIds.size} / {eligiblePrintSils.length}
- </Typography>
- </Stack>
-
- <TableContainer component={Paper} variant="outlined">
- <Table size="small" stickyHeader>
- <TableHead>
- <TableRow>
- <TableCell padding="checkbox"></TableCell>
- <TableCell>貨品編號</TableCell>
- <TableCell>貨品名稱</TableCell>
- <TableCell align="right">換算庫存數量</TableCell>
- <TableCell>庫存單位</TableCell>
- <TableCell>收貨日期</TableCell>
- <TableCell>來貨批號</TableCell>
- <TableCell>來貨狀態</TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {eligiblePrintSils.map((sil) => {
- const status = (sil.status ?? "").toLowerCase().trim();
- const statusText =
- status === "received" ? "待上架" : status === "completed" ? "已上架" : sil.status;
- const receiptText = sil.receiptDate
- ? Array.isArray(sil.receiptDate)
- ? arrayToDateString(sil.receiptDate)
- : String(sil.receiptDate)
- : "-";
- const stockQty = Number(sil.acceptedQty ?? 0);
- const stockQtyText =
- Number.isFinite(stockQty) && stockQty > 0
- ? decimalFormatter.format(stockQty)
- : decimalFormatter.format(0);
- return (
- <TableRow key={sil.id} hover>
- <TableCell padding="checkbox">
- <Checkbox
- checked={selectedPrintSilIds.has(sil.id)}
- onChange={(e) => togglePrintSilSelection(sil.id, e.target.checked)}
- />
- </TableCell>
- <TableCell>{sil.itemNo}</TableCell>
- <TableCell>{sil.itemName}</TableCell>
- <TableCell align="right">{stockQtyText}</TableCell>
- <TableCell>{sil.stockUomDesc || "-"}</TableCell>
- <TableCell>{receiptText}</TableCell>
- <TableCell>{sil.productLotNo || "-"}</TableCell>
- <TableCell>{statusText}</TableCell>
- </TableRow>
- );
- })}
- {eligiblePrintSils.length === 0 && (
- <TableRow>
- <TableCell colSpan={8}>
- <Typography variant="body2" color="text.secondary">
- 沒有符合條件的項目
- </Typography>
- </TableCell>
- </TableRow>
- )}
- </TableBody>
- </Table>
- </TableContainer>
- </Stack>
- </DialogContent>
- <DialogActions>
- <Button onClick={closePrintDialog} disabled={isBulkPrinting}>
- 取消
- </Button>
- <Button
- variant="contained"
- onClick={handleBulkPrint}
- disabled={isBulkPrinting || selectedPrintSilIds.size <= 0 || !selectedPrinter}
- >
- {isBulkPrinting ? "列印中..." : "列印"}
- </Button>
- </DialogActions>
- </Dialog>
- {/* {itemInfo !== undefined && (
- <>
- <PoQcStockInModal
- type={"putaway"}
- open={putAwayOpen}
- warehouse={warehouse}
- setItemDetail={setItemInfo}
- onClose={closePutAwayModal}
- itemDetail={itemInfo}
- />
- </>
- )} */}
- </>
- );
- };
- export default PoDetail;
|