diff --git a/src/app/api/stockTake/actions.ts b/src/app/api/stockTake/actions.ts index 676bdc3..a991294 100644 --- a/src/app/api/stockTake/actions.ts +++ b/src/app/api/stockTake/actions.ts @@ -41,6 +41,7 @@ export interface InventoryLotDetailResponse { approverBadQty: number | null; finalQty: number | null; bookQty: number | null; + lastSelect?: number | null; stockTakeSection?: string | null; stockTakeSectionDescription?: string | null; stockTakerName?: string | null; @@ -286,6 +287,7 @@ export interface SaveApproverStockTakeRecordRequest { approverId?: number | null; approverQty?: number | null; approverBadQty?: number | null; + lastSelect?: number | null; } export interface BatchSaveApproverStockTakeRecordRequest { diff --git a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx index c754d5e..1be6453 100644 --- a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx +++ b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx @@ -153,16 +153,76 @@ const ApproverStockTakeAll: React.FC = ({ loadDetails(page, pageSize); }, [page, pageSize, loadDetails]); + // 切换模式时,清空用户先前的选择与输入,approved 模式需要以后端结果为准。 useEffect(() => { - const newSelections: Record = {}; - inventoryLotDetails.forEach((detail) => { - if (!qtySelection[detail.id]) { - if (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0) { - newSelections[detail.id] = "second"; - } else { - newSelections[detail.id] = "first"; + setQtySelection({}); + setApproverQty({}); + setApproverBadQty({}); + }, [mode, selectedSession.stockTakeId]); + + useEffect(() => { + const inferSelection = ( + detail: InventoryLotDetailResponse + ): QtySelectionType => { + // 优先使用后端记录的 lastSelect(1=First, 2=Second, 3=Approver Input) + if (detail.lastSelect != null) { + if (detail.lastSelect === 1) return "first"; + if (detail.lastSelect === 2) return "second"; + if (detail.lastSelect === 3) return "approver"; + } + + // 目标:在 approved 模式下,即使后端把 approver 字段也回填了, + // 只要 finalQty 来自 first/second(picker 结果),就优先勾选 first/second。 + // 只有匹配不到 first/second 时,才推断为 approver。 + if (detail.finalQty != null) { + const eps = 1e-6; + const firstAvailable = detail.firstStockTakeQty; + const secondAvailable = detail.secondStockTakeQty; + + // 如果这一行确实有 approver 结果,那么 approved 时应该优先显示为 approver + // (尤其是:picker first 后又手动改 approver input 的情况) + if (detail.approverQty != null) { + const approverAvailable = + detail.approverQty - (detail.approverBadQty ?? 0); + if (Math.abs(approverAvailable - detail.finalQty) <= eps) { + return "approver"; + } + } + + if (secondAvailable != null && Math.abs(secondAvailable - detail.finalQty) <= eps) { + return "second"; + } + + if (firstAvailable != null && Math.abs(firstAvailable - detail.finalQty) <= eps) { + return "first"; + } + + // approver 字段口径可能是「available」或「total+bad」两种之一,这里同时尝试两种。 + if (detail.approverQty != null) { + const approverAvailable = detail.approverQty; + const approverAvailable2 = + detail.approverQty - (detail.approverBadQty ?? 0); + + if ( + Math.abs(approverAvailable - detail.finalQty) <= eps || + Math.abs(approverAvailable2 - detail.finalQty) <= eps + ) { + return "approver"; + } } } + + // pending/无法反推时:second 存在则默认 second,否则 first + if (detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0) { + return "second"; + } + return "first"; + }; + + const newSelections: Record = {}; + inventoryLotDetails.forEach((detail) => { + if (qtySelection[detail.id]) return; + newSelections[detail.id] = inferSelection(detail); }); if (Object.keys(newSelections).length > 0) { @@ -170,6 +230,33 @@ const ApproverStockTakeAll: React.FC = ({ } }, [inventoryLotDetails, qtySelection]); + // approved 模式下:把已保存的 approver 输入值回填到 TextField,避免“radio 显示了但输入框为空” + useEffect(() => { + if (mode !== "approved") return; + + const newApproverQty: Record = {}; + const newApproverBadQty: Record = {}; + + inventoryLotDetails.forEach((detail) => { + if (detail.approverQty != null && approverQty[detail.id] == null) { + newApproverQty[detail.id] = String(detail.approverQty); + } + if ( + detail.approverBadQty != null && + approverBadQty[detail.id] == null + ) { + newApproverBadQty[detail.id] = String(detail.approverBadQty); + } + }); + + if (Object.keys(newApproverQty).length > 0) { + setApproverQty((prev) => ({ ...prev, ...newApproverQty })); + } + if (Object.keys(newApproverBadQty).length > 0) { + setApproverBadQty((prev) => ({ ...prev, ...newApproverBadQty })); + } + }, [mode, inventoryLotDetails, approverQty, approverBadQty]); + const calculateDifference = useCallback( (detail: InventoryLotDetailResponse, selection: QtySelectionType): number => { let selectedQty = 0; @@ -200,7 +287,7 @@ const ApproverStockTakeAll: React.FC = ({ } const selection = qtySelection[detail.id] ?? - (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0 + (detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0 ? "second" : "first"); // 避免 Approver 手动输入过程中被 variance 过滤掉,导致“输入后行消失无法提交” @@ -322,6 +409,8 @@ const ApproverStockTakeAll: React.FC = ({ approverId: currentUserId, approverQty: selection === "approver" ? finalQty : null, approverBadQty: selection === "approver" ? finalBadQty : null, + // lastSelect: 1=First, 2=Second, 3=Approver Input + lastSelect: selection === "first" ? 1 : selection === "second" ? 2 : 3, }; await saveApproverStockTakeRecord(request, selectedSession.stockTakeId); @@ -678,6 +767,18 @@ const ApproverStockTakeAll: React.FC = ({ const selection = qtySelection[detail.id] || (hasSecond ? "second" : "first"); + // approved 视图下,只有存在已保存的 approver 结果才显示 approver 输入区块 + const canApprover = + mode === "pending" + ? true + : selection === "approver" && + (detail.approverQty != null || + detail.approverBadQty != null); + + // approved 模式下:即使 finalQty 已存在,也需要展示 radio 用于查看选择 + const showRadioBlock = + mode === "approved" || detail.finalQty == null; + return ( {mode === "approved" && ( @@ -719,35 +820,7 @@ const ApproverStockTakeAll: React.FC = ({ {detail.uom || "-"} - {detail.finalQty != null ? ( - - {(() => { - const bookQtyToUse = - detail.bookQty != null - ? detail.bookQty - : detail.availableQty || 0; - const finalDifference = - (detail.finalQty || 0) - bookQtyToUse; - const differenceColor = - detail.stockTakeRecordStatus === "completed" - ? "text.secondary" - : finalDifference !== 0 - ? "error.main" - : "success.main"; - - return ( - - {t("Difference")}: {formatNumber(detail.finalQty)} -{" "} - {formatNumber(bookQtyToUse)} ={" "} - {formatNumber(finalDifference)} - - ); - })()} - - ) : ( + {showRadioBlock ? ( {hasFirst && ( = ({ checked={selection === "first"} disabled={mode === "approved"} onChange={() => - setQtySelection({ ...qtySelection, [detail.id]: "first", @@ -808,7 +880,7 @@ const ApproverStockTakeAll: React.FC = ({ )} - {hasSecond && ( + {canApprover && ( = ({ }, }} placeholder={t("Stock Take Qty")} - disabled={selection !== "approver"} + disabled={mode === "approved" || selection !== "approver"} inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} /> @@ -874,7 +946,7 @@ const ApproverStockTakeAll: React.FC = ({ }, }} placeholder={t("Bad Qty")} - disabled={selection !== "approver"} + disabled={mode === "approved" || selection !== "approver"} inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} /> @@ -889,30 +961,98 @@ const ApproverStockTakeAll: React.FC = ({ )} + {detail.finalQty != null ? ( + + {(() => { + const bookQtyToUse = + detail.bookQty != null + ? detail.bookQty + : detail.availableQty || 0; + const finalDifference = + (detail.finalQty || 0) - bookQtyToUse; + const differenceColor = + detail.stockTakeRecordStatus === "completed" + ? "text.secondary" + : finalDifference !== 0 + ? "error.main" + : "success.main"; + + return ( + + {t("Difference")}:{" "} + {formatNumber(detail.finalQty)} -{" "} + {formatNumber(bookQtyToUse)} ={" "} + {formatNumber(finalDifference)} + + ); + })()} + + ) : ( + (() => { + let selectedQty = 0; + + if (selection === "first") { + selectedQty = detail.firstStockTakeQty || 0; + } else if (selection === "second") { + selectedQty = detail.secondStockTakeQty || 0; + } else if (selection === "approver") { + selectedQty = + (parseFloat(approverQty[detail.id] || "0") - + parseFloat( + approverBadQty[detail.id] || "0" + )) || 0; + } + + const bookQty = + detail.bookQty != null + ? detail.bookQty + : detail.availableQty || 0; + const difference = selectedQty - bookQty; + const differenceColor = + detail.stockTakeRecordStatus === "completed" + ? "text.secondary" + : difference !== 0 + ? "error.main" + : "success.main"; + + return ( + + {t("Difference")}:{" "} + {t("selected stock take qty")}( + {formatNumber(selectedQty)}) -{" "} + {t("book qty")}( + {formatNumber(bookQty)}) ={" "} + {formatNumber(difference)} + + ); + })() + )} + + ) : ( + {(() => { - let selectedQty = 0; - - if (selection === "first") { - selectedQty = detail.firstStockTakeQty || 0; - } else if (selection === "second") { - selectedQty = detail.secondStockTakeQty || 0; - } else if (selection === "approver") { - selectedQty = - (parseFloat(approverQty[detail.id] || "0") - - parseFloat( - approverBadQty[detail.id] || "0" - )) || 0; - } - - const bookQty = + const bookQtyToUse = detail.bookQty != null ? detail.bookQty : detail.availableQty || 0; - const difference = selectedQty - bookQty; + const finalDifference = + (detail.finalQty || 0) - bookQtyToUse; const differenceColor = detail.stockTakeRecordStatus === "completed" ? "text.secondary" - : difference !== 0 + : finalDifference !== 0 ? "error.main" : "success.main"; @@ -921,12 +1061,9 @@ const ApproverStockTakeAll: React.FC = ({ variant="body2" sx={{ fontWeight: "bold", color: differenceColor }} > - {t("Difference")}:{" "} - {t("selected stock take qty")}( - {formatNumber(selectedQty)}) -{" "} - {t("book qty")}( - {formatNumber(bookQty)}) ={" "} - {formatNumber(difference)} + {t("Difference")}: {formatNumber(detail.finalQty)} -{" "} + {formatNumber(bookQtyToUse)} ={" "} + {formatNumber(finalDifference)} ); })()} @@ -1003,7 +1140,7 @@ const ApproverStockTakeAll: React.FC = ({ size="small" variant="contained" onClick={() => handleSaveApproverStockTake(detail)} - disabled={saving} + disabled={saving ||detail.stockTakeRecordStatus === "notMatch"} > {t("Save")}