diff --git a/src/app/api/stockTake/actions.ts b/src/app/api/stockTake/actions.ts index 4118c777..e588dc4c 100644 --- a/src/app/api/stockTake/actions.ts +++ b/src/app/api/stockTake/actions.ts @@ -53,6 +53,8 @@ export interface InventoryLotDetailResponse { stockTakeEndTime?: string | string[] | null; /** ISO string or backend LocalDateTime array */ approverTime?: string | string[] | null; + /** Times this saved first count has been changed. */ + pickerReviseCount?: number | null; } /** @@ -124,6 +126,8 @@ export interface SaveStockTakeRecordRequest { badQty: number; //stockTakerName: string; remark?: string | null; + /** Overwrite the saved first count while the section is still stock-taking. */ + reviseFirstQty?: boolean; } export interface AllPickedStockTakeListReponse { id: number; diff --git a/src/components/StockTakeManagement/PickerStockTake.tsx b/src/components/StockTakeManagement/PickerStockTake.tsx index 6d63b6b0..98a510a4 100644 --- a/src/components/StockTakeManagement/PickerStockTake.tsx +++ b/src/components/StockTakeManagement/PickerStockTake.tsx @@ -80,6 +80,8 @@ const PickerStockTake: React.FC = ({ /** Qty fields the user has left, or just saved. Warning stays hidden while typing. */ const [gapCheckOpen, setGapCheckOpen] = useState>({}); const [batchSaving, setBatchSaving] = useState(false); + /** 已盤點列:按「更改」後才打開數量欄,再按「保存」。 */ + const [editingFirstIds, setEditingFirstIds] = useState>({}); const [shortcutInput, setShortcutInput] = useState(""); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState("all"); @@ -90,6 +92,7 @@ const PickerStockTake: React.FC = ({ const currentUserId = session?.id ? parseInt(session.id) : undefined; const handleBatchTestAllRef = useRef<() => Promise>(); const batchInFlightRef = useRef(false); + const saveInFlightRef = useRef(false); const isSessionCompleted = selectedSession?.status?.toLowerCase() === "completed"; const handleChangePage = useCallback((event: unknown, newPage: number) => { setPage(newPage); @@ -193,15 +196,42 @@ const PickerStockTake: React.FC = ({ }); }; + const canReviseFirstCount = useCallback((detail: InventoryLotDetailResponse): boolean => { + const sessionStatus = selectedSession?.status?.toLowerCase() ?? ""; + if (sessionStatus === "completed") { + return false; + } + const recordStatus = detail.stockTakeRecordStatus?.toLowerCase(); + if (recordStatus === "completed" || recordStatus !== "pass") { + return false; + } + if (detail.approverQty != null) { + return false; + } + if (detail.stockTakeRecordId == null) { + return false; + } + if (detail.firstStockTakeQty == null || detail.secondStockTakeQty != null) { + return false; + } + if ((detail.pickerReviseCount ?? 0) >= 2) { + return false; + } + return true; + }, [selectedSession?.status]); + const handleSaveStockTake = useCallback( async (detail: InventoryLotDetailResponse) => { - if (!selectedSession || !currentUserId) { + if (!selectedSession || !currentUserId || saveInFlightRef.current) { return; } - const isFirstSubmit = detail.firstStockTakeQty == null; + const revisingFirst = canReviseFirstCount(detail); + const isFirstSubmit = detail.firstStockTakeQty == null || revisingFirst; const isSecondSubmit = - detail.firstStockTakeQty != null && detail.secondStockTakeQty == null; + !revisingFirst && + detail.firstStockTakeQty != null && + detail.secondStockTakeQty == null; // 现在用户输入的是 total 和 bad,需要算 available = total - bad const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty; @@ -232,6 +262,7 @@ const PickerStockTake: React.FC = ({ return; } + saveInFlightRef.current = true; setSaving(true); try { const request: SaveStockTakeRecordRequest = { @@ -240,6 +271,7 @@ const PickerStockTake: React.FC = ({ qty: availableValidated.qty, badQty: badValidated.qty, remark: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : null, + reviseFirstQty: revisingFirst, }; console.log("handleSaveStockTake: request:", request); console.log("handleSaveStockTake: selectedSession.stockTakeId:", selectedSession.stockTakeId); @@ -254,14 +286,17 @@ const PickerStockTake: React.FC = ({ qtyGapWarnPercent, ); setGapCheckOpen((prev) => ({ ...prev, [`${detail.id}:${isFirstSubmit ? "first" : "second"}`]: true })); - onSnackbar( - gapText - ? `${t("Stock take record saved successfully")} ${gapText}` - : t("Stock take record saved successfully"), - gapText ? "warning" : "success", - ); + const oneChangeLeftText = + revisingFirst && (detail.pickerReviseCount ?? 0) === 0 ? t("oneChangeLeft") : ""; + const saveMessage = [t("Stock take record saved successfully"), gapText, oneChangeLeftText] + .filter(Boolean) + .join(" "); + onSnackbar(saveMessage, gapText || oneChangeLeftText ? "warning" : "success"); //await loadDetails(page, pageSize, { silent: true }); + if (revisingFirst) { + setEditingFirstIds((prev) => ({ ...prev, [detail.id]: false })); + } setInventoryLotDetails((prev) => prev.map((d) => d.id === detail.id @@ -270,10 +305,11 @@ const PickerStockTake: React.FC = ({ stockTakeRecordId: d.stockTakeRecordId ?? null, // 首次儲存後可從 response 取得,此處先保留 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, + secondStockTakeQty: revisingFirst ? null : (isSecondSubmit ? availableValidated.qty : d.secondStockTakeQty), + secondBadQty: revisingFirst ? null : (isSecondSubmit ? badValidated.qty : d.secondBadQty ?? null), remarks: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : d.remarks, stockTakeRecordStatus: "pass", + pickerReviseCount: revisingFirst ? (d.pickerReviseCount ?? 0) + 1 : d.pickerReviseCount, } : d ) @@ -296,6 +332,7 @@ const PickerStockTake: React.FC = ({ onSnackbar(errorMessage, "error"); } finally { setSaving(false); + saveInFlightRef.current = false; } }, [ @@ -306,6 +343,7 @@ const PickerStockTake: React.FC = ({ currentUserId, onSnackbar, qtyGapWarnPercent, + canReviseFirstCount, ] ); @@ -579,10 +617,28 @@ const PickerStockTake: React.FC = ({ ) : ( inventoryLotDetails.map((detail) => { const submitDisabled = isSubmitDisabled(detail); + const revisingFirst = canReviseFirstCount(detail); + const editingFirst = revisingFirst && !!editingFirstIds[detail.id]; + const sessionOpen = selectedSession?.status?.toLowerCase() !== "completed"; + const changeUsedUp = + sessionOpen && + detail.stockTakeRecordStatus?.toLowerCase() === "pass" && + detail.approverQty == null && + detail.firstStockTakeQty != null && + detail.secondStockTakeQty == null && + (detail.pickerReviseCount ?? 0) >= 2; + const showChangeButton = (revisingFirst || changeUsedUp) && !editingFirst; + const oneChanceLeft = revisingFirst && (detail.pickerReviseCount ?? 0) === 1; const isFirstSubmit = detail.firstStockTakeQty == null; const isSecondSubmit = detail.firstStockTakeQty != null && detail.secondStockTakeQty == null; + const showFirstInput = (!submitDisabled && isFirstSubmit) || editingFirst; + const savedFirstQty = sanitizeStockTakeQtyInput( + String(Math.trunc(Number(detail.firstStockTakeQty ?? 0))) + ); + const editedFirstQty = sanitizeStockTakeQtyInput(recordInputs[detail.id]?.firstQty || ""); + const firstQtyChanged = editedFirstQty !== "" && editedFirstQty !== savedFirstQty; return ( @@ -618,7 +674,7 @@ const PickerStockTake: React.FC = ({ {/* First */} - {!submitDisabled && isFirstSubmit ? ( + {showFirstInput ? ( {t("First")}: @@ -694,13 +750,21 @@ const PickerStockTake: React.FC = ({ /> ) : detail.firstStockTakeQty != null ? ( - - {t("First")}:{" "} - + + {t("First")}:{" "} + + + - + ) : null} {/* Second */} @@ -772,13 +836,21 @@ const PickerStockTake: React.FC = ({ /> ) : detail.secondStockTakeQty != null ? ( - - {t("Second")}:{" "} - + + {t("Second")}:{" "} + + + - + ) : null} {!detail.firstStockTakeQty && @@ -799,13 +871,39 @@ const PickerStockTake: React.FC = ({ - + {oneChanceLeft && ( + + {t("oneChangeLeft")} + + )} {/* Remark */} diff --git a/src/components/StockTakeManagement/StockTakeTab.tsx b/src/components/StockTakeManagement/StockTakeTab.tsx index 957e1901..2ef820ba 100644 --- a/src/components/StockTakeManagement/StockTakeTab.tsx +++ b/src/components/StockTakeManagement/StockTakeTab.tsx @@ -145,9 +145,15 @@ const StockTakeTab: React.FC = () => { setSnackbar({ ...snackbar, open: false })} + onClose={(_event, reason) => { + if (reason === "clickaway") return; + setSnackbar((prev) => ({ ...prev, open: false })); + }} > - setSnackbar({ ...snackbar, open: false })} severity={snackbar.severity}> + setSnackbar((prev) => ({ ...prev, open: false }))} + severity={snackbar.severity} + > {snackbar.message} @@ -268,9 +274,15 @@ const StockTakeTab: React.FC = () => { setSnackbar({ ...snackbar, open: false })} + onClose={(_event, reason) => { + if (reason === "clickaway") return; + setSnackbar((prev) => ({ ...prev, open: false })); + }} > - setSnackbar({ ...snackbar, open: false })} severity={snackbar.severity}> + setSnackbar((prev) => ({ ...prev, open: false }))} + severity={snackbar.severity} + > {snackbar.message} diff --git a/src/i18n/en/stockTake.json b/src/i18n/en/stockTake.json index 64f04baf..a866d2bb 100644 --- a/src/i18n/en/stockTake.json +++ b/src/i18n/en/stockTake.json @@ -98,6 +98,8 @@ "Return": "Return", "Rows per page": "Rows per page", "Save": "Save", + "Change": "Change", + "oneChangeLeft": "Only 1 change left", "Search": "Search", "Search Criteria": "Search Criteria", "Search section code or name": "Search code or name (e.g. ST-042 or drinks)", diff --git a/src/i18n/zh/stockTake.json b/src/i18n/zh/stockTake.json index 53745a40..3287df0f 100644 --- a/src/i18n/zh/stockTake.json +++ b/src/i18n/zh/stockTake.json @@ -8,6 +8,8 @@ "Remark": "備註", "Action": "操作", "Save": "保存", + "Change": "更改", + "oneChangeLeft": "只剩 1 次更改機會", "Selected Qty": "選擇數量", "Not Match": "要求重點", "Search Criteria": "搜索條件",