diff --git a/src/components/PoDetail/PoDetail.tsx b/src/components/PoDetail/PoDetail.tsx index c8ded3a..24a46e5 100644 --- a/src/components/PoDetail/PoDetail.tsx +++ b/src/components/PoDetail/PoDetail.tsx @@ -31,6 +31,11 @@ import { CardContent, Radio, alpha, + Autocomplete, + Dialog, + DialogActions, + DialogContent, + DialogTitle, } from "@mui/material"; import { useTranslation } from "react-i18next"; import { submitDialogWithWarning } from "../Swal/CustomAlerts"; @@ -69,8 +74,8 @@ 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 PoInfoCard from "./PoInfoCard"; 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"; @@ -81,6 +86,7 @@ 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"; @@ -155,7 +161,16 @@ const PoSearchList: React.FC<{ }, [poList, searchTerm, t]); return ( - + {t("Purchase Order")} @@ -175,51 +190,53 @@ const PoSearchList: React.FC<{ ), }} /> - {loading ? ( - - ) : filteredPoList.length > 0 ? ( - - {filteredPoList.map((poItem, index) => ( -
- - onSelect(poItem)} - sx={{ - width: "100%", - "&.Mui-selected": { - backgroundColor: "primary.light", - "&:hover": { + + {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: "沒有可顯示的採購單" })} - - )} + }} + > + + {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")}`} @@ -311,6 +328,86 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { receiptDate: dayjsToDateString(dayjs()) } }) + + const [selectedPrinter, setSelectedPrinter] = useState( + 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>( + () => 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; @@ -836,6 +933,21 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { } }, []) + 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 ( <> @@ -850,10 +962,10 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { {/* area2: dn info */} - + {/* left side select po */} - - + + = ({ po, warehouse, printerCombo }) => { - - - - {true ? ( - - - - - + + + + + + + + - - - + + + + + + + ( - - { - handleDatePickerChange(newValue, field.onChange) - }} - slotProps={{ textField: { fullWidth: true }}} - /> + { + handleDatePickerChange(newValue, field.onChange); + }} + slotProps={{ textField: { fullWidth: true } }} + /> )} - /> - - - {/* */} + /> + + - {/* */} + + + + + + + + + + + 列印 + setSelectedPrinter(value)} + renderInput={(params) => ( + + )} + /> + { + const cleaned = String(event.target.value).replace(/[^0-9]/g, ""); + setPrintQty(Number(cleaned || 0)); + }} + fullWidth + /> + + + 只會顯示「待上架 / 已上架」的來貨記錄 + - - - ) : undefined} + + + + + + 列印 + + + + + + @@ -1016,6 +1188,128 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { /> */} + + 列印標籤 + + + + + 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 && ( <> = ({ } else { closeHandler({}, "backdropClick"); } - msg("已更新來貨狀態"); + msg("已更新來貨狀態", { + position: + typeof window !== "undefined" && + window.location.pathname.startsWith("/po/edit") + ? "top-end" + : "bottom-end", + }); return ; }, diff --git a/src/components/Qc/QcStockInModal.tsx b/src/components/Qc/QcStockInModal.tsx index 6d93615..2efea80 100644 --- a/src/components/Qc/QcStockInModal.tsx +++ b/src/components/Qc/QcStockInModal.tsx @@ -548,7 +548,13 @@ const QcStockInModal: React.FC = ({ closeWithResult(qcRes); } setIsSubmitting(false); - msg("已更新來貨狀態"); + msg("已更新來貨狀態", { + position: + typeof window !== "undefined" && + window.location.pathname.startsWith("/po/edit") + ? "top-end" + : "bottom-end", + }); return ; }, diff --git a/src/components/Swal/CustomAlerts.tsx b/src/components/Swal/CustomAlerts.tsx index 823f879..9aedc26 100644 --- a/src/components/Swal/CustomAlerts.tsx +++ b/src/components/Swal/CustomAlerts.tsx @@ -8,12 +8,28 @@ export type SweetAlertConfirmButtonText = string | undefined; type Transaction = TFunction<["translation", ...string[]], undefined>; -export const msg = (title: SweetAlertTitle) => { +type ToastPosition = + | "top" + | "top-start" + | "top-end" + | "center" + | "center-start" + | "center-end" + | "bottom" + | "bottom-start" + | "bottom-end"; + +type MsgOptions = { + position?: ToastPosition; + timer?: number; +}; + +export const msg = (title: SweetAlertTitle, options?: MsgOptions) => { Swal.mixin({ toast: true, - position: "bottom-end", + position: options?.position ?? "bottom-end", showConfirmButton: false, - timer: 3000, + timer: options?.timer ?? 3000, timerProgressBar: true, didOpen: (toast) => { toast.onmouseenter = Swal.stopTimer; @@ -28,12 +44,12 @@ export const msg = (title: SweetAlertTitle) => { }); }; -export const msgError = (title: SweetAlertTitle) => { +export const msgError = (title: SweetAlertTitle, options?: MsgOptions) => { Swal.mixin({ toast: true, - position: "bottom-end", + position: options?.position ?? "bottom-end", showConfirmButton: false, - timer: 3000, + timer: options?.timer ?? 3000, timerProgressBar: true, didOpen: (toast) => { toast.onmouseenter = Swal.stopTimer;