diff --git a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx index c7dd011..a889443 100644 --- a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx +++ b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx @@ -178,6 +178,35 @@ function buildApproverSaveRequest( return { ok: true, request, finalQty, finalBadQty, goodQty, selection }; } +function buildBatchApproverSaveRequests( + details: InventoryLotDetailResponse[], + qtySelection: Record, + approverQty: Record, + approverBadQty: Record, + currentUserId: number, + t: (key: string) => string +): SaveApproverStockTakeRecordRequest[] { + const records: SaveApproverStockTakeRecordRequest[] = []; + for (const detail of details) { + if (!detail.stockTakeRecordId || detail.stockTakeRecordId <= 0) { + continue; + } + const built = buildApproverSaveRequest( + detail, + qtySelection, + approverQty, + approverBadQty, + currentUserId, + t + ); + if (!built.ok) { + continue; + } + records.push(built.request); + } + return records; +} + function parseDateTimeMs( v: string | string[] | null | undefined ): number { @@ -720,12 +749,19 @@ const ApproverStockTakeAll: React.FC = ({ }; const isIntegerString = (value: string) => /^\d+$/.test(value); - const batchSaveRecordIds = useMemo( + const batchApproverSaveRecords = useMemo( () => - sortedDetails - .map((d) => d.stockTakeRecordId) - .filter((id): id is number => typeof id === "number" && id > 0), - [sortedDetails], + currentUserId + ? buildBatchApproverSaveRequests( + sortedDetails, + qtySelection, + approverQty, + approverBadQty, + currentUserId, + t + ) + : [], + [sortedDetails, qtySelection, approverQty, approverBadQty, currentUserId, t] ); const batchSaveSectionLabels = useMemo(() => { @@ -744,7 +780,7 @@ const ApproverStockTakeAll: React.FC = ({ onSnackbar(t("No rows loaded; set search criteria and search first"), "warning"); return; } - if (batchSaveRecordIds.length === 0) { + if (batchApproverSaveRecords.length === 0) { onSnackbar(t("No valid records to batch save"), "warning"); return; } @@ -754,7 +790,7 @@ const ApproverStockTakeAll: React.FC = ({ selectedSession, currentUserId, inventoryLotDetails.length, - batchSaveRecordIds.length, + batchApproverSaveRecords.length, onSnackbar, t, ]); @@ -763,7 +799,16 @@ const ApproverStockTakeAll: React.FC = ({ if (batchSaveInFlightRef.current) return; if (mode === "approved") return; if (!selectedSession || !currentUserId) return; - if (batchSaveRecordIds.length === 0) { + + const records = buildBatchApproverSaveRequests( + sortedDetails, + qtySelection, + approverQty, + approverBadQty, + currentUserId, + t + ); + if (records.length === 0) { onSnackbar(t("No valid records to batch save"), "warning"); return; } @@ -776,7 +821,10 @@ const ApproverStockTakeAll: React.FC = ({ const result = await batchSaveApproverStockTakeRecordsByIds({ stockTakeId: selectedSession.stockTakeId, approverId: currentUserId, - recordIds: batchSaveRecordIds, + recordIds: records + .map((r) => r.stockTakeRecordId) + .filter((id): id is number => typeof id === "number" && id > 0), + records, }); onSnackbar( @@ -805,7 +853,10 @@ const ApproverStockTakeAll: React.FC = ({ mode, selectedSession, currentUserId, - batchSaveRecordIds, + sortedDetails, + qtySelection, + approverQty, + approverBadQty, t, onSnackbar, loadDetails, @@ -1515,7 +1566,7 @@ const ApproverStockTakeAll: React.FC = ({ {t("Confirm batch save approver")} - {t("Batch save confirm message", { count: batchSaveRecordIds.length })} + {t("Batch save confirm message", { count: batchApproverSaveRecords.length })} {batchSaveSectionLabels.length > 0 ? ( diff --git a/src/components/StockTakeManagement/PickerReStockTake.tsx b/src/components/StockTakeManagement/PickerReStockTake.tsx index 6a7db22..7757c93 100644 --- a/src/components/StockTakeManagement/PickerReStockTake.tsx +++ b/src/components/StockTakeManagement/PickerReStockTake.tsx @@ -34,7 +34,11 @@ import PickerBatchSaveFab from "./PickerBatchSaveFab"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import dayjs from "dayjs"; -import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; +import { + OUTPUT_DATE_FORMAT, + sanitizeStockTakeQtyInput, + validateStockTakeQtyString, +} from "@/app/utils/formatUtil"; interface PickerReStockTakeProps { selectedSession: AllPickedStockTakeListReponse; @@ -81,11 +85,6 @@ const PickerReStockTake: React.FC = ({ 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) { @@ -209,28 +208,36 @@ const PickerReStockTake: React.FC = ({ return; } - const totalQty = parseFloat(totalQtyStr); - const badQty = parseFloat(badQtyStr || "0") || 0; - - if (Number.isNaN(totalQty)) { - onSnackbar(t("Invalid QTY"), "error"); + 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 = totalQty - badQty; + 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: availableQty, - badQty: badQty, + qty: availableValidated.qty, + badQty: badValidated.qty, remark: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : null, }; const result = await saveStockTakeRecord( @@ -536,7 +543,7 @@ const PickerReStockTake: React.FC = ({ inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} onKeyDown={blockNonIntegerKeys} onChange={(e) => { - const clean = sanitizeIntegerInput(e.target.value); + const clean = sanitizeStockTakeQtyInput(e.target.value); const val = clean; if (val.includes("-")) return; setRecordInputs(prev => ({ @@ -562,7 +569,7 @@ const PickerReStockTake: React.FC = ({ inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} onKeyDown={blockNonIntegerKeys} onChange={(e) => { - const clean = sanitizeIntegerInput(e.target.value); + const clean = sanitizeStockTakeQtyInput(e.target.value); const val = clean; if (val.includes("-")) return; setRecordInputs(prev => ({ @@ -606,7 +613,7 @@ const PickerReStockTake: React.FC = ({ inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} onKeyDown={blockNonIntegerKeys} onChange={(e) => { - const clean = sanitizeIntegerInput(e.target.value); + const clean = sanitizeStockTakeQtyInput(e.target.value); const val = clean; if (val.includes("-")) return; setRecordInputs(prev => ({ @@ -632,7 +639,7 @@ const PickerReStockTake: React.FC = ({ inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} onKeyDown={blockNonIntegerKeys} onChange={(e) => { - const clean = sanitizeIntegerInput(e.target.value); + const clean = sanitizeStockTakeQtyInput(e.target.value); const val = clean; if (val.includes("-")) return; setRecordInputs(prev => ({