"use client"; import { fetchPoWithStockInLines, PoResult, PurchaseOrderLine, } from "@/app/api/po"; import { Box, Button, ButtonProps, Grid, Paper, Stack, Tab, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tabs, TabsProps, TextField, Typography, Checkbox, FormControlLabel, Card, CardContent, Dialog, DialogActions, DialogContent, DialogTitle, } from "@mui/material"; import { useTranslation } from "react-i18next"; import PrinterSelect from "@/components/common/PrinterSelect"; import { PoDetailRow } from "./PoDetailRow"; // 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 { useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import PoInputGrid from "./PoInputGrid"; // import { QcItemWithChecks } from "@/app/api/qc"; import { useRouter, useSearchParams, usePathname } from "next/navigation"; import { WarehouseResult } from "@/app/api/warehouse"; import { dateStringToDayjs, dayjsToDateString, OUTPUT_DATE_FORMAT, outputDateStringToInputDateString, decimalFormatter, arrayToDateString } 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 { 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[]; }; type EntryError = | { [field in keyof StockInLine]?: string; } | undefined; // type PolRow = TableRow, 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 ( {t("Purchase Order")} setSearchTerm(e.target.value)} sx={{ mb: 2 }} InputProps={{ startAdornment: ( ), }} /> {loading ? ( ) : filteredPoList.length > 0 ? ( {filteredPoList.map((poItem, index) => (
onSelect(poItem)} sx={{ width: "100%", "&.Mui-selected": { backgroundColor: "primary.light", "&:hover": { backgroundColor: "primary.light", }, }, }} > {poItem.code} } secondary={ {t(`${poItem.status.toLowerCase()}`)} } /> {index < filteredPoList.length - 1 && }
))}
) : ( {searchTerm.trim() ? t("No purchase orders match your search", { defaultValue: "沒有符合搜索的採購單" }) : t("No purchase orders to show", { defaultValue: "沒有可顯示的採購單" })} )}
{searchTerm && ( {`${t("Found")} ${filteredPoList.length} ${t("Purchase Order")}`} {/* {`${t("Found")} ${filteredPoList.length} of ${poList.length} ${t("Item")}`} */} )}
); }; interface PolInputResult { lotNo: string, dnQty: string, } /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */ const PoDetail: React.FC = ({ 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( purchaseOrder.pol || [], ); const [polInputList, setPolInputList] = useState>({}) const PO_DETAIL_SELECTION_KEY = "po-detail-selection"; useEffect(() => { setPolInputList((prev) => { const next: Record = {}; (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(null); const [stockInLine, setStockInLine] = useState([]); const [processedQty, setProcessedQty] = useState(0); /** Tracks user/nav selection so query patches via history.replaceState stay authoritative. */ const selectedPolIdRef = useRef(null); /** Patch PO edit query without Next soft-navigation (avoids scroll-to-top). */ const patchPoEditQuery = useCallback( (mutate: (params: URLSearchParams) => void) => { if (typeof window === "undefined") return; const params = new URLSearchParams(window.location.search); mutate(params); const qs = params.toString(); window.history.replaceState( window.history.state, "", qs ? `${pathname}?${qs}` : pathname, ); }, [pathname], ); /** Keep selection + bottom stock-in grid in sync with selected / URL `polId`. */ useEffect(() => { if (rows.length === 0) return; const urlPolId = searchParams.get("polId"); const preferredId = selectedPolIdRef.current ?? (urlPolId != null ? Number(urlPolId) : null); if (preferredId == null || Number.isNaN(preferredId)) return; const match = rows.find((r) => r.id === preferredId) ?? (urlPolId != null ? rows.find((r) => r.id.toString() === urlPolId) : undefined); if (!match) return; selectedPolIdRef.current = match.id; setSelectedRow(match); setStockInLine(match.stockInLine ?? []); setProcessedQty(match.processed); }, [rows, searchParams]); const router = useRouter(); const [poList, setPoList] = useState(() => [po]); const [isPoListLoading, setIsPoListLoading] = useState(false); const [selectedPoId, setSelectedPoId] = useState(po.id); const [focusField, setFocusField] = useState(); const currentPoId = searchParams.get('id'); const selectedIdsParam = searchParams.get('selectedIds'); // const [selectedRowId, setSelectedRowId] = useState(null); const dnFormProps = useForm({ defaultValues: { dnNo: '', receiptDate: dayjsToDateString(dayjs()) } }) const labelPrinters = useMemo(() => { return (printerCombo ?? []).filter((p) => { const typeText = String(p.type ?? "").trim().toLowerCase(); if (typeText === "label") return true; // Backward compatibility for legacy rows without a reliable type value. const text = `${p.label ?? ""} ${p.name ?? ""} ${p.code ?? ""}`.toLowerCase(); return text.includes("label") || text.includes("標籤"); }); }, [printerCombo]); const [selectedPrinter, setSelectedPrinter] = useState( labelPrinters?.[0], ); useEffect(() => { // If options change, keep selection valid and prefer first Label printer. if (!selectedPrinter || !labelPrinters.some((p) => p.id === selectedPrinter.id)) { setSelectedPrinter(labelPrinters[0]); } }, [labelPrinters, selectedPrinter]); 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>( () => new Set(), ); const eligiblePrintSils = useMemo(() => { const statusSet = new Set(); 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 ?? selectedPolIdRef.current ?? selectedRow?.id; const targetPol = result.pol.find((p) => p.id === targetPolId) ?? result.pol[0]; selectedPolIdRef.current = targetPol.id; 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; selectedPolIdRef.current = null; 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 getDnValues = useCallback(() => dnFormProps.getValues(), [dnFormProps]); const formatReceiptDate = useCallback((receiptDate?: string) => { return outputDateStringToInputDateString(receiptDate ?? ""); }, []); const handleSelectPol = useCallback((row: PurchaseOrderLine) => { selectedPolIdRef.current = row.id; setSelectedRow(row); setStockInLine(row.stockInLine ?? []); setProcessedQty(row.processed); patchPoEditQuery((params) => { params.set("polId", String(row.id)); params.delete("stockInLineId"); }); }, [patchPoEditQuery]); const handleRowInputBlur = useCallback((rowId: number, lotNo: string, dnQty: string) => { setPolInputList((prev) => { const current = prev[rowId] ?? { lotNo: "", dnQty: "" }; if (current.lotNo === lotNo && current.dnQty === dnQty) return prev; return { ...prev, [rowId]: { lotNo, dnQty }, }; }); }, []); const handleRowSubmitted = useCallback((row: PurchaseOrderLine) => { setPolInputList((prev) => ({ ...prev, [row.id]: { lotNo: "", dnQty: "" }, })); selectedPolIdRef.current = row.id; setSelectedRow(row); patchPoEditQuery((params) => { params.set("polId", String(row.id)); params.delete("stockInLineId"); }); fetchPoDetail(selectedPoId.toString(), true, row.id); }, [fetchPoDetail, patchPoEditQuery, selectedPoId]); 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([]) // }, []); const [tabIndex, setTabIndex] = useState(0); const handleTabChange = useCallback>( (_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({} 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: , 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: , 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: , 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 = { ...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 ( <> {/* Area1: title */} {purchaseOrder.code} -{" "} {t(`${purchaseOrder.status.toLowerCase()}`)} {/* area2: dn info */} {/* left side select po */} {/* right side po info */} ( { handleDatePickerChange(newValue, field.onChange); }} slotProps={{ textField: { fullWidth: true } }} /> )} /> 列印 setSelectedPrinter(p ?? undefined)} placeholder={t("Label Printer")} fullWidth disabled={labelPrinters.length <= 0} /> { const cleaned = String(event.target.value).replace(/[^0-9]/g, ""); setPrintQty(Number(cleaned || 0)); }} fullWidth /> 只會顯示「待上架 / 已上架」的來貨記錄 列印 {/* Area4: Main Table */} {t("itemNo")} {t("itemName")} {t("qty")} {t("processedQty")} {t("uom")} {t("receivedTotal")} {t("Stock UoM")} {/* {t("total weight")} */} {/* {`${t("price")} (HKD)`} */} {t("status")} {/* {renderFieldCondition(FIRST_IN_FIELD) ? {t("receivedQty")} : undefined} */} {t("productLotNo")} {renderFieldCondition(SECOND_IN_FIELD) ? ( {t("dnQty")}
({t("dnQtyOrderUnitHint")})
) : undefined}
{rows.map((row) => ( ))}
{/* area5: selected item info */} {selectedRow ? `已選擇貨品: ${selectedRow?.itemNo ? selectedRow.itemNo : 'N/A'} - ${selectedRow?.itemName ? selectedRow?.itemName : 'N/A'}` : "未選擇貨品"} {selectedRow && ( )} {/* tab 2 */} {/* */}
列印標籤 setPrintStatusFilter((p) => ({ ...p, received: e.target.checked })) } /> } label="待上架" /> setPrintStatusFilter((p) => ({ ...p, completed: e.target.checked })) } /> } label="已上架" /> 0 && selectedPrintSilIds.size === eligiblePrintSils.length } indeterminate={ selectedPrintSilIds.size > 0 && selectedPrintSilIds.size < eligiblePrintSils.length } onChange={(e) => setAllVisiblePrintSilsSelected(e.target.checked)} /> } label="全選(目前篩選結果)" /> 已選擇 {selectedPrintSilIds.size} / {eligiblePrintSils.length} 貨品編號 貨品名稱 換算庫存數量 庫存單位 收貨日期 來貨批號 來貨狀態 {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 ( togglePrintSilSelection(sil.id, e.target.checked)} /> {sil.itemNo} {sil.itemName} {stockQtyText} {sil.stockUomDesc || "-"} {receiptText} {sil.productLotNo || "-"} {statusText} ); })} {eligiblePrintSils.length === 0 && ( 沒有符合條件的項目 )}
{/* {itemInfo !== undefined && ( <> )} */} ); }; export default PoDetail;