"use client"; import { Box, Button, Stack, Typography, Chip, CircularProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, TextField, TablePagination, Select, // Add this MenuItem, // Add this FormControl, // Add this InputLabel, } from "@mui/material"; import { SelectChangeEvent } from "@mui/material/Select"; import { useState, useCallback, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { AllPickedStockTakeListReponse, getInventoryLotDetailsBySection, InventoryLotDetailResponse, saveStockTakeRecord, SaveStockTakeRecordRequest, BatchSaveStockTakeRecordRequest, batchSaveStockTakeRecords, } from "@/app/api/stockTake/actions"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; interface PickerStockTakeProps { selectedSession: AllPickedStockTakeListReponse; onBack: () => void; onSnackbar: (message: string, severity: "success" | "error" | "warning") => void; } const PickerStockTake: React.FC = ({ selectedSession, onBack, onSnackbar, }) => { const { t } = useTranslation(["inventory", "common"]); const { data: session } = useSession() as { data: SessionWithTokens | null }; const [inventoryLotDetails, setInventoryLotDetails] = useState([]); const [loadingDetails, setLoadingDetails] = useState(false); const [recordInputs, setRecordInputs] = useState>({}); const [savingRecordId, setSavingRecordId] = useState(null); const [remark, setRemark] = useState(""); const [saving, setSaving] = useState(false); 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 totalPages = pageSize === "all" ? 1 : Math.ceil(total / (pageSize as number)); const currentUserId = session?.id ? parseInt(session.id) : undefined; const handleBatchSubmitAllRef = useRef<() => Promise>(); const handleChangePage = useCallback((event: unknown, newPage: number) => { setPage(newPage); }, []); const handlePageSelectChange = useCallback((event: SelectChangeEvent) => { const newPage = parseInt(event.target.value as string, 10) - 1; // Convert to 0-indexed setPage(Math.max(0, Math.min(newPage, totalPages - 1))); }, [totalPages]); 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, options?: { silent?: boolean } ) => { console.log('loadDetails called with:', { pageNum, size, selectedSessionTotal: selectedSession.totalInventoryLotNumber }); setLoadingDetails(true); try { let actualSize: number; if (size === "all") { // Use totalInventoryLotNumber from selectedSession if available if (selectedSession.totalInventoryLotNumber > 0) { actualSize = selectedSession.totalInventoryLotNumber; console.log('Using "all" - actualSize set to totalInventoryLotNumber:', actualSize); } else if (total > 0) { // Fallback to total from previous response actualSize = total; console.log('Using "all" - actualSize set to total from state:', actualSize); } else { // Last resort: use a large number actualSize = 10000; console.log('Using "all" - actualSize set to default 10000'); } } else { actualSize = typeof size === 'string' ? parseInt(size, 10) : size; console.log('Using specific size - actualSize set to:', actualSize); } console.log('Calling getInventoryLotDetailsBySection with actualSize:', actualSize); const response = await getInventoryLotDetailsBySection( 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(() => { 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.stockTakeRecordId || !detail.firstStockTakeQty; const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; // 现在用户输入的是 total 和 bad,需要算 available = total - bad const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty; const badQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstBadQty : recordInputs[detail.id]?.secondBadQty; // 只檢查 totalQty,Bad Qty 未輸入時預設為 0 if (!totalQtyStr) { onSnackbar( isFirstSubmit ? t("Please enter QTY") : t("Please enter Second QTY"), "error" ); return; } const totalQty = parseFloat(totalQtyStr); const badQty = parseFloat(badQtyStr || "0") || 0; // 空字串時為 0 if (Number.isNaN(totalQty)) { onSnackbar(t("Invalid QTY"), "error"); return; } const availableQty = totalQty - badQty; if (availableQty < 0) { onSnackbar(t("Available QTY cannot be negative"), "error"); return; } setSaving(true); try { const request: SaveStockTakeRecordRequest = { stockTakeRecordId: detail.stockTakeRecordId || null, inventoryLotLineId: detail.id, qty: availableQty, // 保存 available qty badQty: badQty, // 保存 bad qty remark: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : null, }; console.log("handleSaveStockTake: request:", request); console.log("handleSaveStockTake: selectedSession.stockTakeId:", selectedSession.stockTakeId); console.log("handleSaveStockTake: currentUserId:", currentUserId); await saveStockTakeRecord(request, selectedSession.stockTakeId, currentUserId); onSnackbar(t("Stock take record saved successfully"), "success"); //await loadDetails(page, pageSize, { silent: true }); setInventoryLotDetails((prev) => prev.map((d) => d.id === detail.id ? { ...d, stockTakeRecordId: d.stockTakeRecordId ?? null, // 首次儲存後可從 response 取得,此處先保留 firstStockTakeQty: isFirstSubmit ? availableQty : d.firstStockTakeQty, firstBadQty: isFirstSubmit ? badQty : d.firstBadQty ?? null, secondStockTakeQty: isSecondSubmit ? availableQty : d.secondStockTakeQty, secondBadQty: isSecondSubmit ? badQty : 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, remark, t, currentUserId, onSnackbar, ] ); const handleBatchSubmitAll = useCallback( async () => { if (!selectedSession || !currentUserId) { console.log("handleBatchSubmitAll: Missing selectedSession or currentUserId"); return; } console.log("handleBatchSubmitAll: Starting batch save..."); setBatchSaving(true); try { const request: BatchSaveStockTakeRecordRequest = { stockTakeId: selectedSession.stockTakeId, stockTakeSection: selectedSession.stockTakeSession, stockTakerId: currentUserId, }; const result = await batchSaveStockTakeRecords(request); console.log("handleBatchSubmitAll: Result:", result); 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: any) { console.error("handleBatchSubmitAll: Error:", e); let errorMessage = t("Failed to batch save stock take records"); 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 { setBatchSaving(false); } }, [selectedSession, t, currentUserId, onSnackbar] ); useEffect(() => { handleBatchSubmitAllRef.current = handleBatchSubmitAll; }, [handleBatchSubmitAll]); 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}") { console.log("✅ Shortcut {2fitestall} detected!"); setTimeout(() => { if (handleBatchSubmitAllRef.current) { console.log("Calling handleBatchSubmitAll..."); handleBatchSubmitAllRef.current().catch((err) => { console.error("Error in handleBatchSubmitAll:", err); }); } else { console.error("handleBatchSubmitAllRef.current is null"); } }, 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 blockNonIntegerKeys = (e: React.KeyboardEvent) => { // 禁止小数点、逗号、科学计数、正负号 if ([".", ",", "e", "E", "+", "-"].includes(e.key)) { e.preventDefault(); } }; const sanitizeIntegerInput = (value: string) => { // 只保留数字 return value.replace(/[^\d]/g, ""); }; const isIntegerString = (value: string) => /^\d+$/.test(value); 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 handleSubmitAllInputted = useCallback(async () => { if (!selectedSession || !currentUserId) return; const toSave = inventoryLotDetails.filter((detail) => { const submitDisabled = isSubmitDisabled(detail); if (submitDisabled) return false; const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty; return !!totalQtyStr && totalQtyStr.trim() !== ""; }); if (toSave.length === 0) { onSnackbar(t("No valid input to submit"), "warning"); return; } setBatchSaving(true); let successCount = 0; let errorCount = 0; for (const detail of toSave) { try { await handleSaveStockTake(detail); successCount++; } catch { errorCount++; } } setBatchSaving(false); onSnackbar( t("Submit completed: {{success}} success, {{errors}} errors", { success: successCount, errors: errorCount }), errorCount > 0 ? "warning" : "success" ); }, [inventoryLotDetails, recordInputs, isSubmitDisabled, handleSaveStockTake, selectedSession, currentUserId, onSnackbar, t]); const uniqueWarehouses = Array.from( new Set( inventoryLotDetails .map((detail) => detail.warehouse) .filter((warehouse) => warehouse && warehouse.trim() !== "") ) ).join(", "); return ( {t("Stock Take Section")}: {selectedSession.stockTakeSession} {uniqueWarehouses && ( <> {t("Warehouse")}: {uniqueWarehouses} )} {/* */} {/* 如果需要显示快捷键输入,可以把这块注释打开 */} {/* {shortcutInput && ( {t("Shortcut Input")}:{" "} {shortcutInput} )} */} {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.stockTakeRecordId || !detail.firstStockTakeQty; const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; return ( {detail.warehouseArea || "-"} {detail.warehouseSlot || "-"} {detail.itemCode || "-"} {detail.itemName || "-"} {detail.lotNo || "-"} {detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"} {detail.uom || "-"} {/* Qty + Bad Qty 合并显示/输入 */} {/* First */} {!submitDisabled && isFirstSubmit ? ( {t("First")}: { const clean = sanitizeIntegerInput(e.target.value); const val = clean; if (val.includes("-")) return; setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], firstQty: val } })); }} sx={{ width: 130, minWidth: 130, "& .MuiInputBase-input": { height: "1.4375em", padding: "4px 8px", }, "& .MuiInputBase-input::placeholder": { color: "grey.400", // MUI light grey opacity: 1, }, }} placeholder={t("Stock Take Qty")} /> { const clean = sanitizeIntegerInput(e.target.value); const val = clean; if (val.includes("-")) return; setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], firstBadQty: val } })); }} sx={{ width: 130, minWidth: 130, "& .MuiInputBase-input": { height: "1.4375em", padding: "4px 8px", }, "& .MuiInputBase-input::placeholder": { color: "grey.400", // MUI light grey opacity: 1, }, }} placeholder={t("Bad Qty")} /> = {formatNumber( parseFloat(recordInputs[detail.id]?.firstQty || "0") - parseFloat(recordInputs[detail.id]?.firstBadQty || "0") )} ) : detail.firstStockTakeQty != null ? ( {t("First")}:{" "} {formatNumber( (detail.firstStockTakeQty ?? 0) + (detail.firstBadQty ?? 0) )}{" "} ( {formatNumber( detail.firstBadQty ?? 0 )} ) ={" "} {formatNumber(detail.firstStockTakeQty ?? 0)} ) : null} {/* Second */} {!submitDisabled && isSecondSubmit ? ( {t("Second")}: { const clean = sanitizeIntegerInput(e.target.value); const val = clean; if (val.includes("-")) return; setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], secondQty: val } })); }} sx={{ width: 130, minWidth: 130, "& .MuiInputBase-input": { height: "1.4375em", padding: "4px 8px", }, }} placeholder={t("Stock Take Qty")} /> { const clean = sanitizeIntegerInput(e.target.value); const val = clean; if (val.includes("-")) return; setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], secondBadQty: val } })); }} sx={{ width: 130, minWidth: 130, "& .MuiInputBase-input": { height: "1.4375em", padding: "4px 8px", }, }} placeholder={t("Bad Qty")} /> = {formatNumber( parseFloat(recordInputs[detail.id]?.secondQty || "0") - parseFloat(recordInputs[detail.id]?.secondBadQty || "0") )} ) : detail.secondStockTakeQty != null ? ( {t("Second")}:{" "} {formatNumber( (detail.secondStockTakeQty ?? 0) + (detail.secondBadQty ?? 0) )}{" "} ( {formatNumber( detail.secondBadQty ?? 0 )} ) ={" "} {formatNumber(detail.secondStockTakeQty ?? 0)} ) : null} {!detail.firstStockTakeQty && !detail.secondStockTakeQty && !submitDisabled && ( - )} {/* Remark */} {!submitDisabled && isSecondSubmit ? ( <> {t("Remark")} { // const clean = sanitizeIntegerInput(e.target.value); setRecordInputs(prev => ({ ...prev, [detail.id]: { ...(prev[detail.id] ?? { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" }), remark: e.target.value } })); }} sx={{ width: 150 }} /> ) : ( {detail.remarks || "-"} )} {detail.stockTakeRecordStatus === "completed" ? ( ) : detail.stockTakeRecordStatus === "pass" ? ( ) : detail.stockTakeRecordStatus === "notMatch" ? ( ) : ( )} ); }) )}
)}
); }; export default PickerStockTake;