"use client"; import { Box, Button, Stack, Typography, Chip, CircularProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Checkbox, TextField, FormControlLabel, Radio, TablePagination, ToggleButton } from "@mui/material"; import { useState, useCallback, useEffect, useRef, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { AllPickedStockTakeListReponse, getInventoryLotDetailsBySection, InventoryLotDetailResponse, saveApproverStockTakeRecord, SaveApproverStockTakeRecordRequest, BatchSaveApproverStockTakeRecordRequest, batchSaveApproverStockTakeRecords, updateStockTakeRecordStatusToNotMatch, } 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 ApproverStockTakeProps { selectedSession: AllPickedStockTakeListReponse; onBack: () => void; onSnackbar: (message: string, severity: "success" | "error" | "warning") => void; } type QtySelectionType = "first" | "second" | "approver"; const ApproverStockTake: 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 [showOnlyWithDifference, setShowOnlyWithDifference] = useState(false); // 每个记录的选择状态,key 为 detail.id const [qtySelection, setQtySelection] = useState>({}); const [approverQty, setApproverQty] = useState>({}); const [approverBadQty, setApproverBadQty] = useState>({}); const [saving, setSaving] = useState(false); const [batchSaving, setBatchSaving] = useState(false); const [updatingStatus, setUpdatingStatus] = useState(false); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState("all"); const [total, setTotal] = useState(0); const currentUserId = session?.id ? parseInt(session.id) : undefined; const handleBatchSubmitAllRef = useRef<() => Promise>(); const handleChangePage = useCallback((event: unknown, newPage: number) => { setPage(newPage); }, []); 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 getInventoryLotDetailsBySection( selectedSession.stockTakeSession, selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, pageNum, actualSize ); 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(() => { loadDetails(page, pageSize); }, [page, pageSize, loadDetails]); const calculateDifference = useCallback((detail: InventoryLotDetailResponse, selection: QtySelectionType): number => { let selectedQty = 0; if (selection === "first") { selectedQty = detail.firstStockTakeQty || 0; } else if (selection === "second") { selectedQty = detail.secondStockTakeQty || 0; } else if (selection === "approver") { selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0")) || 0; } const bookQty = detail.availableQty || 0; return selectedQty - bookQty; }, [approverQty, approverBadQty]); // 3. 修改默认选择逻辑(在 loadDetails 的 useEffect 中,或创建一个新的 useEffect) useEffect(() => { // 初始化默认选择:如果 second 存在则选择 second,否则选择 first const newSelections: Record = {}; inventoryLotDetails.forEach(detail => { if (!qtySelection[detail.id]) { // 如果 second 不为 null 且大于 0,默认选择 second,否则选择 first if (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0) { newSelections[detail.id] = "second"; } else { newSelections[detail.id] = "first"; } } }); if (Object.keys(newSelections).length > 0) { setQtySelection(prev => ({ ...prev, ...newSelections })); } }, [inventoryLotDetails]); // 4. 添加过滤逻辑(在渲染表格之前) const filteredDetails = useMemo(() => { if (!showOnlyWithDifference) { return inventoryLotDetails; } return inventoryLotDetails.filter(detail => { const selection = qtySelection[detail.id] || (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0 ? "second" : "first"); const difference = calculateDifference(detail, selection); return difference !== 0; }); }, [inventoryLotDetails, showOnlyWithDifference, qtySelection, calculateDifference]); const handleSaveApproverStockTake = useCallback(async (detail: InventoryLotDetailResponse) => { if (!selectedSession || !currentUserId) { return; } const selection = qtySelection[detail.id] || "first"; let finalQty: number; let finalBadQty: number; if (selection === "first") { if (detail.firstStockTakeQty == null) { onSnackbar(t("First QTY is not available"), "error"); return; } finalQty = detail.firstStockTakeQty; finalBadQty = detail.firstBadQty || 0; } else if (selection === "second") { if (detail.secondStockTakeQty == null) { onSnackbar(t("Second QTY is not available"), "error"); return; } finalQty = detail.secondStockTakeQty; finalBadQty = detail.secondBadQty || 0; } else { // Approver input const approverQtyValue = approverQty[detail.id]; const approverBadQtyValue = approverBadQty[detail.id]; if (approverQtyValue === undefined || approverQtyValue === null || approverQtyValue === "") { onSnackbar(t("Please enter Approver QTY"), "error"); return; } if (approverBadQtyValue === undefined || approverBadQtyValue === null || approverBadQtyValue === "") { onSnackbar(t("Please enter Approver Bad QTY"), "error"); return; } finalQty = parseFloat(approverQtyValue) || 0; finalBadQty = parseFloat(approverBadQtyValue) || 0; } setSaving(true); try { const request: SaveApproverStockTakeRecordRequest = { stockTakeRecordId: detail.stockTakeRecordId || null, qty: finalQty, badQty: finalBadQty, approverId: currentUserId, approverQty: selection === "approver" ? finalQty : null, approverBadQty: selection === "approver" ? finalBadQty : null, }; await saveApproverStockTakeRecord( request, selectedSession.stockTakeId ); onSnackbar(t("Approver stock take record saved successfully"), "success"); await loadDetails(page, pageSize); } catch (e: any) { console.error("Save approver stock take record error:", e); let errorMessage = t("Failed to save approver 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, qtySelection, approverQty, approverBadQty, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); const handleUpdateStatusToNotMatch = useCallback(async (detail: InventoryLotDetailResponse) => { if (!detail.stockTakeRecordId) { onSnackbar(t("Stock take record ID is required"), "error"); return; } setUpdatingStatus(true); try { await updateStockTakeRecordStatusToNotMatch(detail.stockTakeRecordId); onSnackbar(t("Stock take record status updated to not match"), "success"); } catch (e: any) { console.error("Update stock take record status error:", e); let errorMessage = t("Failed to update stock take record status"); 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 { setUpdatingStatus(false); // Reload after status update - the useEffect will handle it with current page/pageSize // Or explicitly reload: setPage((currentPage) => { setPageSize((currentPageSize) => { setTimeout(() => { loadDetails(currentPage, currentPageSize); }, 0); return currentPageSize; }); return currentPage; }); } }, [selectedSession, t, onSnackbar, loadDetails]); const handleBatchSubmitAll = useCallback(async () => { if (!selectedSession || !currentUserId) { console.log('handleBatchSubmitAll: Missing selectedSession or currentUserId'); return; } console.log('handleBatchSubmitAll: Starting batch approver save...'); setBatchSaving(true); try { const request: BatchSaveApproverStockTakeRecordRequest = { stockTakeId: selectedSession.stockTakeId, stockTakeSection: selectedSession.stockTakeSession, approverId: currentUserId, }; const result = await batchSaveApproverStockTakeRecords(request); console.log('handleBatchSubmitAll: Result:', result); onSnackbar( t("Batch approver 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 approver 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, page, pageSize, loadDetails]); useEffect(() => { handleBatchSubmitAllRef.current = handleBatchSubmitAll; }, [handleBatchSubmitAll]); const formatNumber = (num: number | null | undefined): string => { if (num == null) return "0.00"; return num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }; const uniqueWarehouses = Array.from( new Set( inventoryLotDetails .map(detail => detail.warehouse) .filter(warehouse => warehouse && warehouse.trim() !== "") ) ).join(", "); const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { // 如果已经有 finalQty(已完成审批),不允许再次编辑 if (detail.finalQty != null) { return true; } // 获取当前选择模式 const hasFirst = detail.firstStockTakeQty != null && detail.firstStockTakeQty >= 0; const hasSecond = detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0; const selection = qtySelection[detail.id] || (hasSecond ? "second" : "first"); // 如果选择了 "approver" 模式,检查用户是否已经输入了值 if (selection === "approver") { const approverQtyValue = approverQty[detail.id]; const approverBadQtyValue = approverBadQty[detail.id]; // 如果用户已经输入了值(包括0),允许保存 if (approverQtyValue !== undefined && approverQtyValue !== null && approverQtyValue !== "" && approverBadQtyValue !== undefined && approverBadQtyValue !== null && approverBadQtyValue !== "") { return false; // 允许保存 } // 如果用户还没有输入值,禁用按钮 return true; } // 对于 first 或 second 模式,需要检查是否有有效的数量(允许0) // 只要 firstStockTakeQty 不为 null,就允许保存(即使为0) if (detail.firstStockTakeQty == null) { return true; // 如果 firstStockTakeQty 为 null,禁用 } return false; // 允许保存 }, [qtySelection, approverQty, approverBadQty]); return ( {t("Stock Take Section")}: {selectedSession.stockTakeSession} {uniqueWarehouses && ( <> {t("Warehouse")}: {uniqueWarehouses} )} {loadingDetails ? ( ) : ( <> {t("Warehouse Location")} {t("Item-lotNo-ExpiryDate")} {t("Stock Take Qty(include Bad Qty)= Available Qty")} {t("Remark")} {t("UOM")} {t("Record Status")} {t("Action")} {filteredDetails.length === 0 ? ( {t("No data")} ) : ( filteredDetails.map((detail) => { // const submitDisabled = isSubmitDisabled(detail); const hasFirst = detail.firstStockTakeQty != null && detail.firstStockTakeQty >= 0; const hasSecond = detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0; // 改为 >= 0,允许0值 const selection = qtySelection[detail.id] || (hasSecond ? "second" : "first"); return ( {detail.warehouseArea || "-"}{detail.warehouseSlot || "-"} {detail.itemCode || "-"} {detail.itemName || "-"} {detail.lotNo || "-"} {detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"} {detail.finalQty != null ? ( {(() => { const finalDifference = (detail.finalQty || 0) - (detail.availableQty || 0); const differenceColor = finalDifference > 0 ? 'error.main' : finalDifference < 0 ? 'error.main' : 'success.main'; return ( {t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(detail.availableQty)} = {formatNumber(finalDifference)} ); })()} ) : ( {hasFirst && ( setQtySelection({ ...qtySelection, [detail.id]: "first" })} /> {t("First")}: {formatNumber((detail.firstStockTakeQty??0)+(detail.firstBadQty??0))} ({detail.firstBadQty??0}) = {formatNumber(detail.firstStockTakeQty??0)} )} {hasSecond && ( setQtySelection({ ...qtySelection, [detail.id]: "second" })} /> {t("Second")}: {formatNumber((detail.secondStockTakeQty??0)+(detail.secondBadQty??0))} ({detail.secondBadQty??0}) = {formatNumber(detail.secondStockTakeQty??0)} )} {hasSecond && ( setQtySelection({ ...qtySelection, [detail.id]: "approver" })} /> {t("Approver Input")}: setApproverQty({ ...approverQty, [detail.id]: e.target.value })} sx={{ width: 130, minWidth: 130, '& .MuiInputBase-input': { height: '1.4375em', padding: '4px 8px' } }} placeholder={t("Stock Take Qty") } disabled={selection !== "approver"} /> setApproverBadQty({ ...approverBadQty, [detail.id]: e.target.value })} sx={{ width: 130, minWidth: 130, '& .MuiInputBase-input': { height: '1.4375em', padding: '4px 8px' } }} placeholder={t("Bad Qty")} disabled={selection !== "approver"} /> ={(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))} )} {(() => { let selectedQty = 0; if (selection === "first") { selectedQty = detail.firstStockTakeQty || 0; } else if (selection === "second") { selectedQty = detail.secondStockTakeQty || 0; } else if (selection === "approver") { selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))|| 0; } const bookQty = detail.availableQty || 0; const difference = selectedQty - bookQty; const differenceColor = difference > 0 ? 'error.main' : difference < 0 ? 'error.main' : 'success.main'; return ( {t("Difference")}: {t("selected stock take qty")}({formatNumber(selectedQty)}) - {t("book qty")}({formatNumber(bookQty)}) = {formatNumber(difference)} ); })()} )} {detail.remarks || "-"} {detail.uom || "-"} {detail.stockTakeRecordStatus === "pass" ? ( ) : detail.stockTakeRecordStatus === "notMatch" ? ( ) : ( )} {detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && ( )}
{detail.finalQty == null && ( )}
); }) )}
)}
); }; export default ApproverStockTake;