|
|
@@ -153,16 +153,76 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
loadDetails(page, pageSize); |
|
|
loadDetails(page, pageSize); |
|
|
}, [page, pageSize, loadDetails]); |
|
|
}, [page, pageSize, loadDetails]); |
|
|
|
|
|
|
|
|
|
|
|
// 切换模式时,清空用户先前的选择与输入,approved 模式需要以后端结果为准。 |
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
const newSelections: Record<number, QtySelectionType> = {}; |
|
|
|
|
|
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<number, QtySelectionType> = {}; |
|
|
|
|
|
inventoryLotDetails.forEach((detail) => { |
|
|
|
|
|
if (qtySelection[detail.id]) return; |
|
|
|
|
|
newSelections[detail.id] = inferSelection(detail); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
if (Object.keys(newSelections).length > 0) { |
|
|
if (Object.keys(newSelections).length > 0) { |
|
|
@@ -170,6 +230,33 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
} |
|
|
} |
|
|
}, [inventoryLotDetails, qtySelection]); |
|
|
}, [inventoryLotDetails, qtySelection]); |
|
|
|
|
|
|
|
|
|
|
|
// approved 模式下:把已保存的 approver 输入值回填到 TextField,避免“radio 显示了但输入框为空” |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (mode !== "approved") return; |
|
|
|
|
|
|
|
|
|
|
|
const newApproverQty: Record<number, string> = {}; |
|
|
|
|
|
const newApproverBadQty: Record<number, string> = {}; |
|
|
|
|
|
|
|
|
|
|
|
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( |
|
|
const calculateDifference = useCallback( |
|
|
(detail: InventoryLotDetailResponse, selection: QtySelectionType): number => { |
|
|
(detail: InventoryLotDetailResponse, selection: QtySelectionType): number => { |
|
|
let selectedQty = 0; |
|
|
let selectedQty = 0; |
|
|
@@ -200,7 +287,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
} |
|
|
} |
|
|
const selection = |
|
|
const selection = |
|
|
qtySelection[detail.id] ?? |
|
|
qtySelection[detail.id] ?? |
|
|
(detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0 |
|
|
|
|
|
|
|
|
(detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0 |
|
|
? "second" |
|
|
? "second" |
|
|
: "first"); |
|
|
: "first"); |
|
|
// 避免 Approver 手动输入过程中被 variance 过滤掉,导致“输入后行消失无法提交” |
|
|
// 避免 Approver 手动输入过程中被 variance 过滤掉,导致“输入后行消失无法提交” |
|
|
@@ -322,6 +409,8 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
approverId: currentUserId, |
|
|
approverId: currentUserId, |
|
|
approverQty: selection === "approver" ? finalQty : null, |
|
|
approverQty: selection === "approver" ? finalQty : null, |
|
|
approverBadQty: selection === "approver" ? finalBadQty : 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); |
|
|
await saveApproverStockTakeRecord(request, selectedSession.stockTakeId); |
|
|
@@ -678,6 +767,18 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
const selection = |
|
|
const selection = |
|
|
qtySelection[detail.id] || (hasSecond ? "second" : "first"); |
|
|
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 ( |
|
|
return ( |
|
|
<TableRow key={detail.id}> |
|
|
<TableRow key={detail.id}> |
|
|
{mode === "approved" && ( |
|
|
{mode === "approved" && ( |
|
|
@@ -719,35 +820,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
</TableCell> |
|
|
</TableCell> |
|
|
<TableCell>{detail.uom || "-"}</TableCell> |
|
|
<TableCell>{detail.uom || "-"}</TableCell> |
|
|
<TableCell sx={{ minWidth: 300 }}> |
|
|
<TableCell sx={{ minWidth: 300 }}> |
|
|
{detail.finalQty != null ? ( |
|
|
|
|
|
<Stack spacing={0.5}> |
|
|
|
|
|
{(() => { |
|
|
|
|
|
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 ( |
|
|
|
|
|
<Typography |
|
|
|
|
|
variant="body2" |
|
|
|
|
|
sx={{ fontWeight: "bold", color: differenceColor }} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Difference")}: {formatNumber(detail.finalQty)} -{" "} |
|
|
|
|
|
{formatNumber(bookQtyToUse)} ={" "} |
|
|
|
|
|
{formatNumber(finalDifference)} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
); |
|
|
|
|
|
})()} |
|
|
|
|
|
</Stack> |
|
|
|
|
|
) : ( |
|
|
|
|
|
|
|
|
{showRadioBlock ? ( |
|
|
<Stack spacing={1}> |
|
|
<Stack spacing={1}> |
|
|
{hasFirst && ( |
|
|
{hasFirst && ( |
|
|
<Stack |
|
|
<Stack |
|
|
@@ -760,7 +833,6 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
checked={selection === "first"} |
|
|
checked={selection === "first"} |
|
|
disabled={mode === "approved"} |
|
|
disabled={mode === "approved"} |
|
|
onChange={() => |
|
|
onChange={() => |
|
|
|
|
|
|
|
|
setQtySelection({ |
|
|
setQtySelection({ |
|
|
...qtySelection, |
|
|
...qtySelection, |
|
|
[detail.id]: "first", |
|
|
[detail.id]: "first", |
|
|
@@ -808,7 +880,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
</Stack> |
|
|
</Stack> |
|
|
)} |
|
|
)} |
|
|
|
|
|
|
|
|
{hasSecond && ( |
|
|
|
|
|
|
|
|
{canApprover && ( |
|
|
<Stack |
|
|
<Stack |
|
|
direction="row" |
|
|
direction="row" |
|
|
spacing={1} |
|
|
spacing={1} |
|
|
@@ -849,7 +921,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
}, |
|
|
}, |
|
|
}} |
|
|
}} |
|
|
placeholder={t("Stock Take Qty")} |
|
|
placeholder={t("Stock Take Qty")} |
|
|
disabled={selection !== "approver"} |
|
|
|
|
|
|
|
|
disabled={mode === "approved" || selection !== "approver"} |
|
|
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} |
|
|
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} |
|
|
/> |
|
|
/> |
|
|
|
|
|
|
|
|
@@ -874,7 +946,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
}, |
|
|
}, |
|
|
}} |
|
|
}} |
|
|
placeholder={t("Bad Qty")} |
|
|
placeholder={t("Bad Qty")} |
|
|
disabled={selection !== "approver"} |
|
|
|
|
|
|
|
|
disabled={mode === "approved" || selection !== "approver"} |
|
|
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} |
|
|
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} |
|
|
/> |
|
|
/> |
|
|
<Typography variant="body2"> |
|
|
<Typography variant="body2"> |
|
|
@@ -889,30 +961,98 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
</Stack> |
|
|
</Stack> |
|
|
)} |
|
|
)} |
|
|
|
|
|
|
|
|
|
|
|
{detail.finalQty != null ? ( |
|
|
|
|
|
<Stack spacing={0.5}> |
|
|
|
|
|
{(() => { |
|
|
|
|
|
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 ( |
|
|
|
|
|
<Typography |
|
|
|
|
|
variant="body2" |
|
|
|
|
|
sx={{ |
|
|
|
|
|
fontWeight: "bold", |
|
|
|
|
|
color: differenceColor, |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Difference")}:{" "} |
|
|
|
|
|
{formatNumber(detail.finalQty)} -{" "} |
|
|
|
|
|
{formatNumber(bookQtyToUse)} ={" "} |
|
|
|
|
|
{formatNumber(finalDifference)} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
); |
|
|
|
|
|
})()} |
|
|
|
|
|
</Stack> |
|
|
|
|
|
) : ( |
|
|
|
|
|
(() => { |
|
|
|
|
|
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 ( |
|
|
|
|
|
<Typography |
|
|
|
|
|
variant="body2" |
|
|
|
|
|
sx={{ |
|
|
|
|
|
fontWeight: "bold", |
|
|
|
|
|
color: differenceColor, |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Difference")}:{" "} |
|
|
|
|
|
{t("selected stock take qty")}( |
|
|
|
|
|
{formatNumber(selectedQty)}) -{" "} |
|
|
|
|
|
{t("book qty")}( |
|
|
|
|
|
{formatNumber(bookQty)}) ={" "} |
|
|
|
|
|
{formatNumber(difference)} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
); |
|
|
|
|
|
})() |
|
|
|
|
|
)} |
|
|
|
|
|
</Stack> |
|
|
|
|
|
) : ( |
|
|
|
|
|
<Stack spacing={0.5}> |
|
|
{(() => { |
|
|
{(() => { |
|
|
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 != null |
|
|
? detail.bookQty |
|
|
? detail.bookQty |
|
|
: detail.availableQty || 0; |
|
|
: detail.availableQty || 0; |
|
|
const difference = selectedQty - bookQty; |
|
|
|
|
|
|
|
|
const finalDifference = |
|
|
|
|
|
(detail.finalQty || 0) - bookQtyToUse; |
|
|
const differenceColor = |
|
|
const differenceColor = |
|
|
detail.stockTakeRecordStatus === "completed" |
|
|
detail.stockTakeRecordStatus === "completed" |
|
|
? "text.secondary" |
|
|
? "text.secondary" |
|
|
: difference !== 0 |
|
|
|
|
|
|
|
|
: finalDifference !== 0 |
|
|
? "error.main" |
|
|
? "error.main" |
|
|
: "success.main"; |
|
|
: "success.main"; |
|
|
|
|
|
|
|
|
@@ -921,12 +1061,9 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
variant="body2" |
|
|
variant="body2" |
|
|
sx={{ fontWeight: "bold", color: differenceColor }} |
|
|
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)} |
|
|
</Typography> |
|
|
</Typography> |
|
|
); |
|
|
); |
|
|
})()} |
|
|
})()} |
|
|
@@ -1003,7 +1140,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({ |
|
|
size="small" |
|
|
size="small" |
|
|
variant="contained" |
|
|
variant="contained" |
|
|
onClick={() => handleSaveApproverStockTake(detail)} |
|
|
onClick={() => handleSaveApproverStockTake(detail)} |
|
|
disabled={saving} |
|
|
|
|
|
|
|
|
disabled={saving ||detail.stockTakeRecordStatus === "notMatch"} |
|
|
> |
|
|
> |
|
|
{t("Save")} |
|
|
{t("Save")} |
|
|
</Button> |
|
|
</Button> |
|
|
|