| @@ -199,6 +199,12 @@ export const getApproverStockTakeRecords = async () => { | |||
| ); | |||
| return stockTakeRecords; | |||
| } | |||
| export const getLatestApproverStockTakeHeader = async () => { | |||
| return serverFetchJson<AllPickedStockTakeListReponse>( | |||
| `${BASE_API_URL}/stockTakeRecord/LatestApproverStockTakeHeader`, | |||
| { method: "GET" } | |||
| ); | |||
| } | |||
| export const createStockTakeForSections = async () => { | |||
| const createStockTakeForSections = await serverFetchJson<Map<string, string>>( | |||
| `${BASE_API_URL}/stockTake/createForSections`, | |||
| @@ -64,7 +64,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| const [batchSaving, setBatchSaving] = useState(false); | |||
| const [updatingStatus, setUpdatingStatus] = useState(false); | |||
| const [page, setPage] = useState(0); | |||
| const [pageSize, setPageSize] = useState<number | string>("all"); | |||
| const [pageSize, setPageSize] = useState<number | string>(50); | |||
| const [total, setTotal] = useState(0); | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| @@ -337,7 +337,19 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| }, | |||
| [t, onSnackbar] | |||
| ); | |||
| const blockNonIntegerKeys = (e: React.KeyboardEvent<HTMLInputElement>) => { | |||
| // 禁止小数点、逗号、科学计数、正负号 | |||
| 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 handleBatchSubmitAll = useCallback(async () => { | |||
| if (mode === "approved") return; | |||
| if (!selectedSession || !currentUserId) { | |||
| @@ -431,9 +443,16 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| size="small" | |||
| type="number" | |||
| value={variancePercentTolerance} | |||
| onChange={(e) => setVariancePercentTolerance(e.target.value)} | |||
| onKeyDown={blockNonIntegerKeys} | |||
| onChange={(e) => { | |||
| const clean = sanitizeIntegerInput(e.target.value); | |||
| setVariancePercentTolerance(clean); | |||
| }} | |||
| label={t("Variance %")} | |||
| sx={{ width: 100 }} | |||
| inputProps={{ min: 0, max: 100, step: 0.1 }} | |||
| /> | |||
| {mode === "pending" && ( | |||
| @@ -564,7 +583,9 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| <Radio | |||
| size="small" | |||
| checked={selection === "first"} | |||
| disabled={mode === "approved"} | |||
| onChange={() => | |||
| setQtySelection({ | |||
| ...qtySelection, | |||
| [detail.id]: "first", | |||
| @@ -592,6 +613,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| <Radio | |||
| size="small" | |||
| checked={selection === "second"} | |||
| disabled={mode === "approved"} | |||
| onChange={() => | |||
| setQtySelection({ | |||
| ...qtySelection, | |||
| @@ -620,6 +642,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| <Radio | |||
| size="small" | |||
| checked={selection === "approver"} | |||
| disabled={mode === "approved"} | |||
| onChange={() => | |||
| setQtySelection({ | |||
| ...qtySelection, | |||
| @@ -634,12 +657,14 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| size="small" | |||
| type="number" | |||
| value={approverQty[detail.id] || ""} | |||
| onChange={(e) => | |||
| onKeyDown={blockNonIntegerKeys} | |||
| onChange={(e) => { | |||
| const clean = sanitizeIntegerInput(e.target.value); | |||
| setApproverQty({ | |||
| ...approverQty, | |||
| [detail.id]: e.target.value, | |||
| }) | |||
| } | |||
| [detail.id]: clean, | |||
| }); | |||
| }} | |||
| sx={{ | |||
| width: 130, | |||
| minWidth: 130, | |||
| @@ -650,18 +675,21 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| }} | |||
| placeholder={t("Stock Take Qty")} | |||
| disabled={selection !== "approver"} | |||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||
| /> | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={approverBadQty[detail.id] || ""} | |||
| onChange={(e) => | |||
| onKeyDown={blockNonIntegerKeys} | |||
| onChange={(e) => { | |||
| const clean = sanitizeIntegerInput(e.target.value); | |||
| setApproverBadQty({ | |||
| ...approverBadQty, | |||
| [detail.id]: e.target.value, | |||
| }) | |||
| } | |||
| [detail.id]: clean, | |||
| }); | |||
| }} | |||
| sx={{ | |||
| width: 130, | |||
| minWidth: 130, | |||
| @@ -672,6 +700,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ | |||
| }} | |||
| placeholder={t("Bad Qty")} | |||
| disabled={selection !== "approver"} | |||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||
| /> | |||
| <Typography variant="body2"> | |||
| ={" "} | |||
| @@ -70,7 +70,17 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| const handleChangePage = useCallback((event: unknown, newPage: number) => { | |||
| setPage(newPage); | |||
| }, []); | |||
| const blockNonIntegerKeys = (e: React.KeyboardEvent<HTMLInputElement>) => { | |||
| // 禁止小数点、逗号、科学计数、正负号 | |||
| 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<HTMLInputElement | HTMLTextAreaElement>) => { | |||
| const newSize = parseInt(event.target.value, 10); | |||
| if (newSize === -1) { | |||
| @@ -437,9 +447,11 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| size="small" | |||
| type="number" | |||
| value={inputs.firstQty} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||
| onKeyDown={blockNonIntegerKeys} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| const clean = sanitizeIntegerInput(e.target.value); | |||
| const val = clean; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ | |||
| ...prev, | |||
| @@ -460,9 +472,11 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| size="small" | |||
| type="number" | |||
| value={inputs.firstBadQty} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||
| onKeyDown={blockNonIntegerKeys} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| const clean = sanitizeIntegerInput(e.target.value); | |||
| const val = clean; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ | |||
| ...prev, | |||
| @@ -500,13 +514,15 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| size="small" | |||
| type="number" | |||
| value={inputs.secondQty} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||
| onKeyDown={blockNonIntegerKeys} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| const clean = sanitizeIntegerInput(e.target.value); | |||
| const val = clean; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ | |||
| ...prev, | |||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondQty: val } | |||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondQty: clean } | |||
| })); | |||
| }} | |||
| sx={{ | |||
| @@ -523,13 +539,15 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| size="small" | |||
| type="number" | |||
| value={inputs.secondBadQty} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||
| onKeyDown={blockNonIntegerKeys} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| const clean = sanitizeIntegerInput(e.target.value); | |||
| const val = clean; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ | |||
| ...prev, | |||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondBadQty: val } | |||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), secondBadQty: clean } | |||
| })); | |||
| }} | |||
| sx={{ | |||
| @@ -581,10 +599,15 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||
| <TextField | |||
| size="small" | |||
| value={inputs.remark} | |||
| onChange={(e) => setRecordInputs(prev => ({ | |||
| onKeyDown={blockNonIntegerKeys} | |||
| inputProps={{ inputMode: "text", pattern: "[0-9]*" }} | |||
| onChange={(e) => { | |||
| const clean = sanitizeIntegerInput(e.target.value); | |||
| setRecordInputs(prev => ({ | |||
| ...prev, | |||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), remark: e.target.value } | |||
| }))} | |||
| [detail.id]: { ...(prev[detail.id] ?? defaultInputs), remark: clean } | |||
| })); | |||
| }} | |||
| sx={{ width: 150 }} | |||
| /> | |||
| </> | |||
| @@ -394,7 +394,19 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| window.removeEventListener("keydown", handleKeyPress); | |||
| }; | |||
| }, []); | |||
| const blockNonIntegerKeys = (e: React.KeyboardEvent<HTMLInputElement>) => { | |||
| // 禁止小数点、逗号、科学计数、正负号 | |||
| 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 isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { | |||
| if (selectedSession?.status?.toLowerCase() === "completed") { | |||
| return true; | |||
| @@ -580,9 +592,11 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| size="small" | |||
| type="number" | |||
| value={recordInputs[detail.id]?.firstQty || ""} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||
| onKeyDown={blockNonIntegerKeys} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| const clean = sanitizeIntegerInput(e.target.value); | |||
| const val = clean; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], firstQty: val } })); | |||
| }} | |||
| @@ -604,9 +618,11 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| size="small" | |||
| type="number" | |||
| value={recordInputs[detail.id]?.firstBadQty || ""} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||
| onKeyDown={blockNonIntegerKeys} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| const clean = sanitizeIntegerInput(e.target.value); | |||
| const val = clean; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], firstBadQty: val } })); | |||
| }} | |||
| @@ -656,9 +672,11 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| size="small" | |||
| type="number" | |||
| value={recordInputs[detail.id]?.secondQty || ""} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||
| onKeyDown={blockNonIntegerKeys} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| const clean = sanitizeIntegerInput(e.target.value); | |||
| const val = clean; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], secondQty: val } })); | |||
| }} | |||
| @@ -676,9 +694,11 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| size="small" | |||
| type="number" | |||
| value={recordInputs[detail.id]?.secondBadQty || ""} | |||
| inputProps={{ min: 0, step: "any" }} | |||
| inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} | |||
| onKeyDown={blockNonIntegerKeys} | |||
| onChange={(e) => { | |||
| const val = e.target.value; | |||
| const clean = sanitizeIntegerInput(e.target.value); | |||
| const val = clean; | |||
| if (val.includes("-")) return; | |||
| setRecordInputs(prev => ({ ...prev, [detail.id]: { ...prev[detail.id], secondBadQty: val } })); | |||
| }} | |||
| @@ -751,13 +771,18 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| <TextField | |||
| size="small" | |||
| value={recordInputs[detail.id]?.remark || ""} | |||
| onChange={(e) => setRecordInputs(prev => ({ | |||
| ...prev, | |||
| [detail.id]: { | |||
| ...(prev[detail.id] ?? { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" }), | |||
| remark: e.target.value | |||
| } | |||
| }))} | |||
| onKeyDown={blockNonIntegerKeys} | |||
| inputProps={{ inputMode: "text", pattern: "[0-9]*" }} | |||
| onChange={(e) => { | |||
| const clean = sanitizeIntegerInput(e.target.value); | |||
| setRecordInputs(prev => ({ | |||
| ...prev, | |||
| [detail.id]: { | |||
| ...(prev[detail.id] ?? { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" }), | |||
| remark: clean | |||
| } | |||
| })); | |||
| }} | |||
| sx={{ width: 150 }} | |||
| /> | |||
| </> | |||
| @@ -3,7 +3,7 @@ | |||
| import { Box, Tab, Tabs, Snackbar, Alert, CircularProgress, Typography } from "@mui/material"; | |||
| import { useState, useCallback, useEffect } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { AllPickedStockTakeListReponse, getApproverStockTakeRecords } from "@/app/api/stockTake/actions"; | |||
| import { AllPickedStockTakeListReponse, getLatestApproverStockTakeHeader } from "@/app/api/stockTake/actions"; | |||
| import PickerCardList from "./PickerCardList"; | |||
| import PickerStockTake from "./PickerStockTake"; | |||
| import PickerReStockTake from "./PickerReStockTake"; | |||
| @@ -54,12 +54,11 @@ const StockTakeTab: React.FC = () => { | |||
| }, []); | |||
| useEffect(() => { | |||
| if (tabValue !== 1) return; | |||
| if (tabValue !== 1 && tabValue !== 2) return; | |||
| setApproverLoading(true); | |||
| getApproverStockTakeRecords() | |||
| .then((records) => { | |||
| const list = Array.isArray(records) ? records : []; | |||
| setApproverSession(list[0] ?? null); | |||
| getLatestApproverStockTakeHeader() | |||
| .then((header) => { | |||
| setApproverSession(header ?? null); | |||
| }) | |||
| .catch((e) => { | |||
| console.error(e); | |||
| @@ -150,11 +149,7 @@ const StockTakeTab: React.FC = () => { | |||
| )} | |||
| {tabValue === 2 && ( | |||
| <Box> | |||
| {approverLoading ? ( | |||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : approverSession ? ( | |||
| {approverSession ? ( | |||
| <ApproverStockTakeAll | |||
| selectedSession={approverSession} | |||
| mode="approved" | |||
| @@ -6,6 +6,8 @@ | |||
| "Status": "來貨狀態", | |||
| "Qty": "盤點數量", | |||
| "UoM": "單位", | |||
| "Approver Pending": "審核待處理", | |||
| "Approver Approved": "審核通過", | |||
| "mat": "物料", | |||
| "variance": "差異", | |||
| "Plan Start Date": "計劃開始日期", | |||