From 92675215dd60f5dd28bb3362c36f30658c2152e4 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 26 May 2026 23:26:16 +0800 Subject: [PATCH] stock take input max limit --- src/app/utils/formatUtil.ts | 34 ++++++++ .../ApproverStockTakeAll.tsx | 85 ++++++++----------- .../StockTakeManagement/PickerStockTake.tsx | 52 +++++++----- .../buildPickerBatchSaveRequests.ts | 20 +++-- src/i18n/en/inventory.json | 4 +- src/i18n/zh/inventory.json | 2 + 6 files changed, 120 insertions(+), 77 deletions(-) diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index 672ddb5..9d1d149 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -39,6 +39,40 @@ export function roundDownPercent(value: number): number { return Math.trunc(value); } +/** 與 stocktakerecord DECIMAL(14,2) 整數部分上限一致 */ +export const MAX_STOCK_TAKE_QTY = 999_999_999_999; +const MAX_STOCK_TAKE_QTY_DIGITS = 12; + +/** 盤點數量輸入:僅數字,最多 12 位 */ +export function sanitizeStockTakeQtyInput(value: string): string { + return value.replace(/[^\d]/g, "").slice(0, MAX_STOCK_TAKE_QTY_DIGITS); +} + +export type StockTakeQtyValidation = + | { ok: true; qty: number } + | { ok: false; errorKey: "Invalid QTY" | "Stock take qty exceeds maximum" }; + +export function validateStockTakeQtyString( + value: string | undefined, + options?: { allowEmpty?: boolean }, +): StockTakeQtyValidation { + const trimmed = (value ?? "").trim(); + if (!trimmed) { + return options?.allowEmpty ? { ok: true, qty: 0 } : { ok: false, errorKey: "Invalid QTY" }; + } + if (!/^\d+$/.test(trimmed)) { + return { ok: false, errorKey: "Invalid QTY" }; + } + if (trimmed.length > MAX_STOCK_TAKE_QTY_DIGITS) { + return { ok: false, errorKey: "Stock take qty exceeds maximum" }; + } + const qty = Number(trimmed); + if (!Number.isFinite(qty) || qty > MAX_STOCK_TAKE_QTY) { + return { ok: false, errorKey: "Stock take qty exceeds maximum" }; + } + return { ok: true, qty }; +} + export const INPUT_DATE_FORMAT = "YYYY-MM-DD"; export const OUTPUT_DATE_FORMAT = "YYYY-MM-DD"; diff --git a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx index 0c8022c..c7dd011 100644 --- a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx +++ b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx @@ -1424,54 +1424,43 @@ const ApproverStockTakeAll: React.FC = ({ )} - - setSearchVariancePercentTolerance(sanitizePositiveDecimalInput(e.target.value)) - } - inputProps={{ min: 0, step: 0.1 }} - /> - - - setSearchVarianceFilterInclusive(e.target.checked)} - /> - } - label={t("Variance filter inclusive only")} - /> - - {/* - - setSearchVarianceFilterStrict(e.target.checked)} - /> - } - label={t("Variance filter strict bounds")} - /> - - */} - - - {searchVarianceFilterInclusive - ? t("Variance filter inclusive range hint", { - value: searchVariancePercentTolerance || "0", - op: searchVarianceFilterStrict ? "<" : "≤", - }) - : t("Variance filter exclusive range hint", { - value: searchVariancePercentTolerance || "0", - op: searchVarianceFilterStrict ? ">" : "≥", - })} - + + + setSearchVariancePercentTolerance(sanitizePositiveDecimalInput(e.target.value)) + } + inputProps={{ min: 0, step: 0.1 }} + /> + setSearchVarianceFilterInclusive(e.target.checked)} + /> + } + label={ + {t("Variance filter inclusive only")} + } + /> + + {searchVarianceFilterInclusive + ? t("Variance filter inclusive range hint", { + value: searchVariancePercentTolerance || "0", + op: searchVarianceFilterStrict ? "<" : "≤", + }) + : t("Variance filter exclusive range hint", { + value: searchVariancePercentTolerance || "0", + op: searchVarianceFilterStrict ? ">" : "≥", + })} + + diff --git a/src/components/StockTakeManagement/PickerStockTake.tsx b/src/components/StockTakeManagement/PickerStockTake.tsx index 428bc4d..76ca5d7 100644 --- a/src/components/StockTakeManagement/PickerStockTake.tsx +++ b/src/components/StockTakeManagement/PickerStockTake.tsx @@ -39,7 +39,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 PickerStockTakeProps { selectedSession: AllPickedStockTakeListReponse; @@ -198,28 +202,36 @@ const PickerStockTake: React.FC = ({ - const totalQty = parseFloat(totalQtyStr || "0") || 0; - const badQty = parseFloat(badQtyStr || "0") || 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, // 保存 available qty - badQty: badQty, // 保存 bad qty + qty: availableValidated.qty, + badQty: badValidated.qty, remark: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : null, }; console.log("handleSaveStockTake: request:", request); @@ -237,10 +249,10 @@ const PickerStockTake: React.FC = ({ ? { ...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, + firstStockTakeQty: isFirstSubmit ? availableValidated.qty : d.firstStockTakeQty, + firstBadQty: isFirstSubmit ? badValidated.qty : d.firstBadQty ?? null, + secondStockTakeQty: isSecondSubmit ? availableValidated.qty : d.secondStockTakeQty, + secondBadQty: isSecondSubmit ? badValidated.qty : d.secondBadQty ?? null, remarks: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : d.remarks, stockTakeRecordStatus: "pass", } @@ -382,12 +394,6 @@ const PickerStockTake: React.FC = ({ } }; - 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; @@ -599,7 +605,7 @@ const PickerStockTake: 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 => ({ ...prev, [detail.id]: { ...prev[detail.id], firstQty: val } })); @@ -626,7 +632,7 @@ const PickerStockTake: 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 => ({ ...prev, [detail.id]: { ...prev[detail.id], firstBadQty: val } })); @@ -683,7 +689,7 @@ const PickerStockTake: 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 => ({ ...prev, [detail.id]: { ...prev[detail.id], secondQty: val } })); @@ -706,7 +712,7 @@ const PickerStockTake: 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 => ({ ...prev, [detail.id]: { ...prev[detail.id], secondBadQty: val } })); diff --git a/src/components/StockTakeManagement/buildPickerBatchSaveRequests.ts b/src/components/StockTakeManagement/buildPickerBatchSaveRequests.ts index bec7cf0..55e29f7 100644 --- a/src/components/StockTakeManagement/buildPickerBatchSaveRequests.ts +++ b/src/components/StockTakeManagement/buildPickerBatchSaveRequests.ts @@ -2,6 +2,7 @@ import type { InventoryLotDetailResponse, SaveStockTakeRecordRequest, } from "@/app/api/stockTake/actions"; +import { validateStockTakeQtyString } from "@/app/utils/formatUtil"; export type PickerRecordInputs = Record< number, @@ -47,17 +48,26 @@ export function buildPickerBatchSaveRequests( if (!totalQtyStr || totalQtyStr.trim() === "") continue; - const totalQty = parseFloat(totalQtyStr); - const badQty = parseFloat(badQtyStr || "0") || 0; - - if (Number.isNaN(totalQty)) { - return { ok: false, message: "Invalid QTY" }; + const totalValidated = validateStockTakeQtyString(totalQtyStr); + if (!totalValidated.ok) { + return { ok: false, message: totalValidated.errorKey }; + } + const badValidated = validateStockTakeQtyString(badQtyStr, { allowEmpty: true }); + if (!badValidated.ok) { + return { ok: false, message: badValidated.errorKey }; } + const totalQty = totalValidated.qty; + const badQty = badValidated.qty; + const availableQty = totalQty - badQty; if (availableQty < 0) { return { ok: false, message: "Available QTY cannot be negative" }; } + const availableValidated = validateStockTakeQtyString(String(availableQty)); + if (!availableValidated.ok) { + return { ok: false, message: availableValidated.errorKey }; + } records.push({ stockTakeRecordId: detail.stockTakeRecordId ?? null, diff --git a/src/i18n/en/inventory.json b/src/i18n/en/inventory.json index deeaa5f..81e3feb 100644 --- a/src/i18n/en/inventory.json +++ b/src/i18n/en/inventory.json @@ -50,5 +50,7 @@ "Warehouse missing stock take section drawer hint": "These locations have no stock take section (ST-xxx) and cannot be included in stock take. Fix in warehouse settings.", "Warehouse missing stock take section go settings": "Go to warehouse settings", "Warehouse missing stock take section showing": "Showing {{shown}} of {{count}}", - "Warehouse missing stock take section empty": "No warehouses missing stock take section" + "Warehouse missing stock take section empty": "No warehouses missing stock take section", + "Invalid QTY": "Invalid quantity", + "Stock take qty exceeds maximum": "Stock take quantity cannot exceed 999,999,999,999" } \ No newline at end of file diff --git a/src/i18n/zh/inventory.json b/src/i18n/zh/inventory.json index 6a5e9c3..8cd4dd7 100644 --- a/src/i18n/zh/inventory.json +++ b/src/i18n/zh/inventory.json @@ -96,6 +96,8 @@ "Total Item Kind Number": "貨品種類數量", "Please enter QTY and Bad QTY": "請輸入盤點數量和不良數量", "Available QTY cannot be negative": "可用數量不能為負數", + "Invalid QTY": "無效的數量", + "Stock take qty exceeds maximum": "盤點數量不可超過 999,999,999,999", "Start Time": "開始時間", "Difference": "差異", "stockTaking": "盤點中",