"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, getInventoryLotDetailsBySectionNotMatch } 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 PickerReStockTakeProps { selectedSession: AllPickedStockTakeListReponse; onBack: () => void; onSnackbar: (message: string, severity: "success" | "error" | "warning") => void; } const PickerReStockTake: 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 [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 currentUserId = session?.id ? parseInt(session.id) : undefined; const handleBatchSubmitAllRef = useRef<() => Promise>(); const handleChangePage = useCallback((event: unknown, newPage: number) => { setPage(newPage); }, []); 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 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 ); 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(() => { 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(與 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 totalQty = parseFloat(totalQtyStr); const badQty = parseFloat(badQtyStr || "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, badQty: badQty, remark: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : null, }; const result = await saveStockTakeRecord( request, selectedSession.stockTakeId, currentUserId ); onSnackbar(t("Stock take record saved successfully"), "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 ? 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, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); const handleBatchSubmitAll = useCallback(async () => { if (!selectedSession || !currentUserId) { return; } 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: 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, page, pageSize, loadDetails]); 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}') { setTimeout(() => { if (handleBatchSubmitAllRef.current) { handleBatchSubmitAllRef.current().catch(err => { console.error('Error in handleBatchSubmitAll:', 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 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 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.stockTakeRecordId || !detail.firstStockTakeQty; const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; 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")}: { const clean = sanitizeIntegerInput(e.target.value); const val = clean; if (val.includes("-")) return; setRecordInputs(prev => ({ ...prev, [detail.id]: { ...(prev[detail.id] ?? defaultInputs), firstQty: 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] ?? defaultInputs), firstBadQty: val } })); }} sx={{ width: 130, minWidth: 130, "& .MuiInputBase-input": { height: "1.4375em", padding: "4px 8px", }, }} placeholder={t("Bad Qty")} /> = {formatNumber(parseFloat(inputs.firstQty || "0") - parseFloat(inputs.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] ?? defaultInputs), secondQty: clean } })); }} 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] ?? defaultInputs), secondBadQty: clean } })); }} sx={{ width: 130, minWidth: 130, "& .MuiInputBase-input": { height: "1.4375em", padding: "4px 8px", }, }} placeholder={t("Bad Qty")} /> = {formatNumber(parseFloat(inputs.secondQty || "0") - parseFloat(inputs.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 && ( - )} {!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;