|
- "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<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,
- }
-
- /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */
- 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);
- /** Tracks user/nav selection so query patches via history.replaceState stay authoritative. */
- const selectedPolIdRef = useRef<number | null>(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<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 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<PrinterCombo | undefined>(
- 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<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 ?? 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<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} sx={{ width: "100%" }}>
- {/* Area1: title */}
- <Grid container 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={{ width: "100%" }} 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} sx={{ minWidth: 0 }}>
- <Grid container spacing={3} sx={{ width: "100%" }}>
- <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>
- <PrinterSelect
- label={t("Label Printer")}
- printers={labelPrinters}
- value={selectedPrinter ?? null}
- onChange={(p) => setSelectedPrinter(p ?? undefined)}
- placeholder={t("Label Printer")}
- fullWidth
- disabled={labelPrinters.length <= 0}
- />
- <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={labelPrinters.length <= 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 justifyContent="start" sx={{ width: "100%" }}>
- <Grid item xs={12} sx={{ width: "100%", minWidth: 0 }}>
- <TableContainer component={Paper} sx={{ width: "100%", overflow: "auto" }}>
- <Table aria-label="collapsible table" stickyHeader sx={{ width: "100%" }}>
- <TableHead>
- <TableRow>
- <TableCell align="center" sx={{ width: '60px' }}></TableCell>
- <TableCell sx={{ width: '88px', maxWidth: '88px', px: 1 }}>{t("itemNo")}</TableCell>
- <TableCell align="left" sx={{ width: '100px', maxWidth: '100px', px: 1 }}>{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: 360, minWidth: 360 }}>
- {t("dnQty")}
- <br />
- ({t("dnQtyOrderUnitHint")})
- </TableCell>
- ) : undefined}
- <TableCell align="center" sx={{ width: '100px' }}></TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {rows.map((row) => (
- <PoDetailRow
- key={row.id}
- row={row}
- selected={selectedRow?.id === row.id}
- canSeeStockInReminders={canSeeStockInReminders}
- showDnQty={renderFieldCondition(SECOND_IN_FIELD)}
- savedLotNo={polInputList[row.id]?.lotNo ?? ""}
- savedDnQty={polInputList[row.id]?.dnQty ?? ""}
- onSelect={handleSelectPol}
- onInputBlur={handleRowInputBlur}
- getDnValues={getDnValues}
- formatReceiptDate={formatReceiptDate}
- onSubmitted={handleRowSubmitted}
- />
- ))}
- </TableBody>
- </Table>
- </TableContainer>
- </Grid>
- </Grid>
-
- {/* area5: selected item info */}
- <Grid container justifyContent="start" sx={{ width: "100%" }}>
- <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} sx={{ width: "100%", minWidth: 0 }}>
- {selectedRow && (
- <PoInputGrid
- setRows={setRows}
- stockInLine={stockInLine}
- setStockInLine={setStockInLine}
- setProcessedQty={setProcessedQty}
- itemDetail={selectedRow}
- warehouse={warehouse}
- fetchPoDetail={fetchPoDetail}
- handleMailTemplateForStockInLine={handleMailTemplateForStockInLine}
- printerCombo={printerCombo}
- />
- )}
- </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;
|