Просмотр исходного кода

stock take input max limit

production
CANCERYS\kw093 3 недель назад
Родитель
Сommit
92675215dd
6 измененных файлов: 120 добавлений и 77 удалений
  1. +34
    -0
      src/app/utils/formatUtil.ts
  2. +37
    -48
      src/components/StockTakeManagement/ApproverStockTakeAll.tsx
  3. +29
    -23
      src/components/StockTakeManagement/PickerStockTake.tsx
  4. +15
    -5
      src/components/StockTakeManagement/buildPickerBatchSaveRequests.ts
  5. +3
    -1
      src/i18n/en/inventory.json
  6. +2
    -0
      src/i18n/zh/inventory.json

+ 34
- 0
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";


+ 37
- 48
src/components/StockTakeManagement/ApproverStockTakeAll.tsx Просмотреть файл

@@ -1424,54 +1424,43 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
</Grid>
)}
<Grid item xs={12} md={4}>
<TextField
fullWidth
size="small"
type="number"
label={t("Variance %")}
value={searchVariancePercentTolerance}
onChange={(e) =>
setSearchVariancePercentTolerance(sanitizePositiveDecimalInput(e.target.value))
}
inputProps={{ min: 0, step: 0.1 }}
/>
</Grid>
<Grid item xs={12} md={4} sx={{ display: "flex", alignItems: "center" }}>
<FormControlLabel
control={
<Checkbox
checked={searchVarianceFilterInclusive}
onChange={(e) => setSearchVarianceFilterInclusive(e.target.checked)}
/>
}
label={t("Variance filter inclusive only")}
/>
</Grid>
{/*
<Grid item xs={12} md={4} sx={{ display: "flex", alignItems: "center" }}>
<FormControlLabel
control={
<Checkbox
checked={searchVarianceFilterStrict}
onChange={(e) => setSearchVarianceFilterStrict(e.target.checked)}
/>
}
label={t("Variance filter strict bounds")}
/>
</Grid>
*/}
<Grid item xs={12}>
<Typography variant="caption" color="text.secondary">
{searchVarianceFilterInclusive
? t("Variance filter inclusive range hint", {
value: searchVariancePercentTolerance || "0",
op: searchVarianceFilterStrict ? "<" : "≤",
})
: t("Variance filter exclusive range hint", {
value: searchVariancePercentTolerance || "0",
op: searchVarianceFilterStrict ? ">" : "≥",
})}
</Typography>
<Stack spacing={0.5}>
<TextField
fullWidth
size="small"
type="number"
label={t("Variance %")}
value={searchVariancePercentTolerance}
onChange={(e) =>
setSearchVariancePercentTolerance(sanitizePositiveDecimalInput(e.target.value))
}
inputProps={{ min: 0, step: 0.1 }}
/>
<FormControlLabel
sx={{ ml: 0, mr: 0 }}
control={
<Checkbox
size="small"
checked={searchVarianceFilterInclusive}
onChange={(e) => setSearchVarianceFilterInclusive(e.target.checked)}
/>
}
label={
<Typography variant="body2">{t("Variance filter inclusive only")}</Typography>
}
/>
<Typography variant="caption" color="text.secondary" sx={{ pl: 0.5 }}>
{searchVarianceFilterInclusive
? t("Variance filter inclusive range hint", {
value: searchVariancePercentTolerance || "0",
op: searchVarianceFilterStrict ? "<" : "≤",
})
: t("Variance filter exclusive range hint", {
value: searchVariancePercentTolerance || "0",
op: searchVarianceFilterStrict ? ">" : "≥",
})}
</Typography>
</Stack>
</Grid>
</Grid>
<CardActions sx={{ px: 0, pt: 2, gap: 1 }}>


+ 29
- 23
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<PickerStockTakeProps> = ({

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<PickerStockTakeProps> = ({
? {
...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<PickerStockTakeProps> = ({
}
};
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<PickerStockTakeProps> = ({
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<PickerStockTakeProps> = ({
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<PickerStockTakeProps> = ({
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<PickerStockTakeProps> = ({
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 } }));


+ 15
- 5
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,


+ 3
- 1
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"
}

+ 2
- 0
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": "盤點中",


Загрузка…
Отмена
Сохранить