"use client"; import { Box, Button, Stack, Typography, Chip, CircularProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, TextField, TablePagination, } from "@mui/material"; import { useState, useCallback, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { AllPickedStockTakeListReponse, InventoryLotDetailResponse, saveStockTakeRecord, SaveStockTakeRecordRequest, BatchSaveStockTakeRecordRequest, batchSaveStockTakeRecords, batchSavePickerStockTakeInputs, getInventoryLotDetailsBySectionNotMatch } from "@/app/api/stockTake/actions"; import { buildPickerBatchSaveRequests } from "./buildPickerBatchSaveRequests"; import { stockTakeQtyEndAdornment, StockTakeQtyWithUnit } from "./stockTakeQtyAdornment"; import StockTakeQtyGapHint from "./StockTakeQtyGapHint"; import { stockTakeHiddenOnHand, stockTakeQtyGapWarnText } from "./stockTakeQtyGapWarning"; import { useStockTakeQtyGapWarnPercent } from "./useStockTakeQtyGapWarnPercent"; import PickerBatchSaveFab from "./PickerBatchSaveFab"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT, sanitizeStockTakeQtyInput, validateStockTakeQtyString, } from "@/app/utils/formatUtil"; interface PickerReStockTakeProps { selectedSession: AllPickedStockTakeListReponse; onBack: () => void; onSnackbar: (message: string, severity: "success" | "error" | "warning") => void; } const PickerReStockTake: React.FC = ({ selectedSession, onBack, onSnackbar, }) => { const { t } = useTranslation(["stockTake", "common"]); const qtyGapWarnPercent = useStockTakeQtyGapWarnPercent(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const [inventoryLotDetails, setInventoryLotDetails] = useState([]); const [loadingDetails, setLoadingDetails] = useState(false); const [recordInputs, setRecordInputs] = useState>({}); const [saving, setSaving] = useState(false); const [gapCheckOpen, setGapCheckOpen] = useState>({}); const [batchSaving, setBatchSaving] = useState(false); const [shortcutInput, setShortcutInput] = useState(""); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState("all"); const [total, setTotal] = useState(0); const currentUserId = session?.id ? parseInt(session.id) : undefined; const handleBatchTestAllRef = useRef<() => Promise>(); const batchInFlightRef = useRef(false); const isSessionCompleted = selectedSession?.status?.toLowerCase() === "completed"; const handleChangePage = useCallback((event: unknown, newPage: number) => { setPage(newPage); }, []); const blockNonIntegerKeys = (e: React.KeyboardEvent) => { // 禁止小数点、逗号、科学计数、正负号 if ([".", ",", "e", "E", "+", "-"].includes(e.key)) { e.preventDefault(); } }; const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent) => { const newSize = parseInt(event.target.value, 10); if (newSize === -1) { setPageSize("all"); } else if (!isNaN(newSize)) { setPageSize(newSize); } setPage(0); }, []); const loadDetails = useCallback(async (pageNum: number, size: number | string) => { setLoadingDetails(true); try { let actualSize: number; if (size === "all") { if (selectedSession.totalInventoryLotNumber > 0) { actualSize = selectedSession.totalInventoryLotNumber; } else if (total > 0) { actualSize = total; } else { actualSize = 10000; } } else { actualSize = typeof size === 'string' ? parseInt(size, 10) : size; } const response = await getInventoryLotDetailsBySectionNotMatch( selectedSession.stockTakeSession, selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, pageNum, actualSize, selectedSession.stockTakeRoundId != null && selectedSession.stockTakeRoundId > 0 ? selectedSession.stockTakeRoundId : null ); setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); setTotal(response.total || 0); } catch (e) { console.error(e); setInventoryLotDetails([]); setTotal(0); } finally { setLoadingDetails(false); } }, [selectedSession, total]); {/* useEffect(() => { const inputs: Record = {}; inventoryLotDetails.forEach((detail) => { const firstTotal = detail.firstStockTakeQty != null ? (detail.firstStockTakeQty + (detail.firstBadQty ?? 0)).toString() : ""; const secondTotal = detail.secondStockTakeQty != null ? (detail.secondStockTakeQty + (detail.secondBadQty ?? 0)).toString() : ""; inputs[detail.id] = { firstQty: firstTotal, secondQty: secondTotal, firstBadQty: detail.firstBadQty?.toString() || "", secondBadQty: detail.secondBadQty?.toString() || "", remark: detail.remarks || "", }; }); setRecordInputs(inputs); }, [inventoryLotDetails]); */} useEffect(() => { setRecordInputs((prev) => { const next: Record = {}; inventoryLotDetails.forEach((detail) => { const hasServerFirst = detail.firstStockTakeQty != null; const hasServerSecond = detail.secondStockTakeQty != null; const firstTotal = hasServerFirst ? (detail.firstStockTakeQty! + (detail.firstBadQty ?? 0)).toString() : ""; const secondTotal = hasServerSecond ? (detail.secondStockTakeQty! + (detail.secondBadQty ?? 0)).toString() : ""; const existing = prev[detail.id]; next[detail.id] = { firstQty: hasServerFirst ? firstTotal : (existing?.firstQty ?? firstTotal), secondQty: hasServerSecond ? secondTotal : (existing?.secondQty ?? secondTotal), firstBadQty: hasServerFirst ? (detail.firstBadQty?.toString() || "") : (existing?.firstBadQty ?? ""), secondBadQty: hasServerSecond ? (detail.secondBadQty?.toString() || "") : (existing?.secondBadQty ?? ""), remark: hasServerSecond ? (detail.remarks || "") : (existing?.remark ?? detail.remarks ?? ""), }; }); return next; }); }, [inventoryLotDetails]); useEffect(() => { loadDetails(page, pageSize); }, [page, pageSize, loadDetails]); const formatNumber = (num: number | null | undefined): string => { if (num == null || Number.isNaN(num)) return "0"; return num.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 0, }); }; const handleSaveStockTake = useCallback(async (detail: InventoryLotDetailResponse) => { if (!selectedSession || !currentUserId) { return; } const isFirstSubmit = detail.firstStockTakeQty == null; const isSecondSubmit = detail.firstStockTakeQty != null && detail.secondStockTakeQty == null; // 用戶輸入為 total 和 bad,需計算 available = total - bad(與 PickerStockTake 一致) const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty; const badQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstBadQty : recordInputs[detail.id]?.secondBadQty; if (!totalQtyStr) { onSnackbar( isFirstSubmit ? t("Please enter QTY") : t("Please enter Second QTY"), "error" ); return; } const totalValidated = validateStockTakeQtyString(totalQtyStr); if (!totalValidated.ok) { onSnackbar(t(totalValidated.errorKey), "error"); return; } const badValidated = validateStockTakeQtyString(badQtyStr, { allowEmpty: true }); if (!badValidated.ok) { onSnackbar(t(badValidated.errorKey), "error"); return; } const availableQty = totalValidated.qty - badValidated.qty; if (availableQty < 0) { onSnackbar(t("Available QTY cannot be negative"), "error"); return; } const availableValidated = validateStockTakeQtyString(String(availableQty)); if (!availableValidated.ok) { onSnackbar(t(availableValidated.errorKey), "error"); return; } setSaving(true); try { const request: SaveStockTakeRecordRequest = { stockTakeRecordId: detail.stockTakeRecordId || null, inventoryLotLineId: detail.id, qty: availableValidated.qty, badQty: badValidated.qty, remark: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : null, }; const result = await saveStockTakeRecord( request, selectedSession.stockTakeId, currentUserId ); const gapText = stockTakeQtyGapWarnText( t, totalQtyStr ?? "", stockTakeHiddenOnHand(detail), qtyGapWarnPercent, ); setGapCheckOpen((prev) => ({ ...prev, [`${detail.id}:${isFirstSubmit ? "first" : "second"}`]: true })); onSnackbar( gapText ? `${t("Stock take record saved successfully")} ${gapText}` : t("Stock take record saved successfully"), gapText ? "warning" : "success", ); const savedId = result?.id ?? detail.stockTakeRecordId; setInventoryLotDetails((prev) => prev.map((d) => d.id === detail.id ? { ...d, stockTakeRecordId: savedId ?? d.stockTakeRecordId, firstStockTakeQty: isFirstSubmit ? availableQty : d.firstStockTakeQty, firstBadQty: isFirstSubmit ? badValidated.qty : d.firstBadQty ?? null, secondStockTakeQty: isSecondSubmit ? availableQty : d.secondStockTakeQty, secondBadQty: isSecondSubmit ? badValidated.qty : d.secondBadQty ?? null, remarks: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : d.remarks, stockTakeRecordStatus: "pass", } : d ) ); } catch (e: any) { console.error("Save stock take record error:", e); let errorMessage = t("Failed to save stock take record"); if (e?.message) { errorMessage = e.message; } else if (e?.response) { try { const errorData = await e.response.json(); errorMessage = errorData.message || errorData.error || errorMessage; } catch { // ignore } } onSnackbar(errorMessage, "error"); } finally { setSaving(false); } }, [selectedSession, recordInputs, t, currentUserId, onSnackbar, page, pageSize, loadDetails, qtyGapWarnPercent]); const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { if (selectedSession?.status?.toLowerCase() === "completed") { return true; } const recordStatus = detail.stockTakeRecordStatus?.toLowerCase(); if (recordStatus === "pass" || recordStatus === "completed") { return true; } return false; }, [selectedSession?.status]); const handleBatchTestAutoFill = useCallback(async () => { if (!selectedSession || !currentUserId || batchInFlightRef.current) { return; } batchInFlightRef.current = true; setBatchSaving(true); try { const request: BatchSaveStockTakeRecordRequest = { stockTakeId: selectedSession.stockTakeId, stockTakeSection: selectedSession.stockTakeSession, stockTakerId: currentUserId, }; const result = await batchSaveStockTakeRecords(request); onSnackbar( t("Batch save completed: {{success}} success, {{errors}} errors", { success: result.successCount, errors: result.errorCount, }), result.errorCount > 0 ? "warning" : "success" ); await loadDetails(page, pageSize); } catch (e: unknown) { console.error("handleBatchTestAutoFill:", e); let errorMessage = t("Failed to batch save stock take records"); if (e instanceof Error && e.message) { errorMessage = e.message; } onSnackbar(errorMessage, "error"); } finally { setBatchSaving(false); batchInFlightRef.current = false; } }, [selectedSession, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); const handleBatchSaveInputted = useCallback(async () => { if (!selectedSession || !currentUserId || batchInFlightRef.current) return; const built = buildPickerBatchSaveRequests( inventoryLotDetails, recordInputs, isSubmitDisabled ); if (!built.ok) { onSnackbar(t(built.message), "error"); return; } if (built.records.length === 0) { onSnackbar(t("No valid input to submit"), "warning"); return; } batchInFlightRef.current = true; setBatchSaving(true); try { const result = await batchSavePickerStockTakeInputs({ stockTakeId: selectedSession.stockTakeId, stockTakeSection: selectedSession.stockTakeSession, stockTakerId: currentUserId, records: built.records, }); onSnackbar( t("Batch save completed: {{success}} success, {{errors}} errors", { success: result.successCount, errors: result.errorCount, }), result.errorCount > 0 ? "warning" : "success" ); await loadDetails(page, pageSize); } catch (e: unknown) { console.error("handleBatchSaveInputted:", e); let errorMessage = t("Failed to batch save stock take records"); if (e instanceof Error && e.message) { errorMessage = e.message; } onSnackbar(errorMessage, "error"); } finally { setBatchSaving(false); batchInFlightRef.current = false; } }, [ selectedSession, currentUserId, inventoryLotDetails, recordInputs, isSubmitDisabled, t, onSnackbar, page, pageSize, loadDetails, ]); useEffect(() => { handleBatchTestAllRef.current = handleBatchTestAutoFill; }, [handleBatchTestAutoFill]); useEffect(() => { const handleKeyPress = (e: KeyboardEvent) => { const target = e.target as HTMLElement; if (target && ( target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable )) { return; } if (e.ctrlKey || e.metaKey || e.altKey) { return; } if (e.key.length === 1) { setShortcutInput(prev => { const newInput = prev + e.key; if (newInput === '{2fitestall}') { setTimeout(() => { handleBatchTestAllRef.current?.().catch((err) => { console.error("Error in handleBatchTestAutoFill:", err); }); }, 0); return ""; } if (newInput.length > 15) return ""; if (newInput.length > 0 && !newInput.startsWith('{')) return ""; if (newInput.length > 5 && !newInput.startsWith('{2fi')) return ""; return newInput; }); } else if (e.key === 'Backspace') { setShortcutInput(prev => prev.slice(0, -1)); } else if (e.key === 'Escape') { setShortcutInput(""); } }; window.addEventListener('keydown', handleKeyPress); return () => { window.removeEventListener('keydown', handleKeyPress); }; }, []); const uniqueWarehouses = Array.from( new Set( inventoryLotDetails .map(detail => detail.warehouse) .filter(warehouse => warehouse && warehouse.trim() !== "") ) ).join(", "); const defaultInputs = { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" }; return ( {t("Stock Take Section")}: {selectedSession.stockTakeSession} {uniqueWarehouses && ( <> {t("Warehouse")}: {uniqueWarehouses} )} {loadingDetails ? ( ) : ( <> {t("Warehouse Location")} {t("Item-lotNo-ExpiryDate")} {t("UOM")} {t("Stock Take Qty(include Bad Qty)= Available Qty")} {t("Action")} {/*{t("Remark")}*/} {t("Record Status")} {inventoryLotDetails.length === 0 ? ( {t("No data")} ) : ( inventoryLotDetails.map((detail) => { const submitDisabled = isSubmitDisabled(detail); const isFirstSubmit = detail.firstStockTakeQty == null; const isSecondSubmit = detail.firstStockTakeQty != null && detail.secondStockTakeQty == null; const inputs = recordInputs[detail.id] ?? defaultInputs; return ( {detail.warehouseArea || "-"}{detail.warehouseSlot || "-"} {detail.itemCode || "-"} {detail.itemName || "-"} {detail.lotNo || "-"} {detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"} {detail.uom || "-"} {/* First */} {!submitDisabled && isFirstSubmit ? ( {t("First")}: setGapCheckOpen((prev) => ({ ...prev, [`${detail.id}:first`]: false })) } onBlur={() => setGapCheckOpen((prev) => ({ ...prev, [`${detail.id}:first`]: true })) } inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} onKeyDown={blockNonIntegerKeys} onChange={(e) => { const clean = sanitizeStockTakeQtyInput(e.target.value); const val = clean; if (val.includes("-")) return; setRecordInputs(prev => ({ ...prev, [detail.id]: { ...(prev[detail.id] ?? defaultInputs), firstQty: val } })); }} InputProps={{ endAdornment: stockTakeQtyEndAdornment(detail.uomShortDesc), }} sx={{ width: 148, minWidth: 148, "& .MuiInputBase-input": { height: "1.4375em", padding: "4px 8px", }, }} placeholder={t("Stock Take Qty")} /> {/* { const clean = sanitizeStockTakeQtyInput(e.target.value); const val = clean; if (val.includes("-")) return; setRecordInputs(prev => ({ ...prev, [detail.id]: { ...(prev[detail.id] ?? defaultInputs), firstBadQty: val } })); }} sx={{ width: 130, minWidth: 130, "& .MuiInputBase-input": { height: "1.4375em", padding: "4px 8px", }, }} placeholder={t("Bad Qty")} /> */} ) : detail.firstStockTakeQty != null ? ( {t("First")}:{" "} ) : null} {/* Second */} {!submitDisabled && isSecondSubmit ? ( {t("Second")}: setGapCheckOpen((prev) => ({ ...prev, [`${detail.id}:second`]: false })) } onBlur={() => setGapCheckOpen((prev) => ({ ...prev, [`${detail.id}:second`]: true })) } inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} onKeyDown={blockNonIntegerKeys} onChange={(e) => { const clean = sanitizeStockTakeQtyInput(e.target.value); const val = clean; if (val.includes("-")) return; setRecordInputs(prev => ({ ...prev, [detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondQty: clean } })); }} InputProps={{ endAdornment: stockTakeQtyEndAdornment(detail.uomShortDesc), }} sx={{ width: 148, minWidth: 148, "& .MuiInputBase-input": { height: "1.4375em", padding: "4px 8px", }, }} placeholder={t("Stock Take Qty")} /> {/* { const clean = sanitizeStockTakeQtyInput(e.target.value); const val = clean; if (val.includes("-")) return; setRecordInputs(prev => ({ ...prev, [detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondBadQty: clean } })); }} sx={{ width: 130, minWidth: 130, "& .MuiInputBase-input": { height: "1.4375em", padding: "4px 8px", }, }} placeholder={t("Bad Qty")} /> */} ) : detail.secondStockTakeQty != null ? ( {t("Second")}:{" "} ) : null} {!detail.firstStockTakeQty && !detail.secondStockTakeQty && !submitDisabled && ( - )} {/* {!submitDisabled && isSecondSubmit ? ( <> {t("Remark")} { // const clean = sanitizeIntegerInput(e.target.value); setRecordInputs(prev => ({ ...prev, [detail.id]: { ...(prev[detail.id] ?? defaultInputs), remark: e.target.value } })); }} sx={{ width: 150 }} /> ) : ( {detail.remarks || "-"} )} */} {detail.stockTakeRecordStatus === "completed" ? ( ) : detail.stockTakeRecordStatus === "pass" ? ( ) : detail.stockTakeRecordStatus === "notMatch" ? ( ) : ( )} ); }) )}
)}
); }; export default PickerReStockTake;