ソースを参照

update

MergeProblem1
CANCERYS\kw093 5日前
コミット
d2854953a8
2個のファイルの変更204行の追加65行の削除
  1. +2
    -0
      src/app/api/stockTake/actions.ts
  2. +202
    -65
      src/components/StockTakeManagement/ApproverStockTakeAll.tsx

+ 2
- 0
src/app/api/stockTake/actions.ts ファイルの表示

@@ -41,6 +41,7 @@ export interface InventoryLotDetailResponse {
approverBadQty: number | null; approverBadQty: number | null;
finalQty: number | null; finalQty: number | null;
bookQty: number | null; bookQty: number | null;
lastSelect?: number | null;
stockTakeSection?: string | null; stockTakeSection?: string | null;
stockTakeSectionDescription?: string | null; stockTakeSectionDescription?: string | null;
stockTakerName?: string | null; stockTakerName?: string | null;
@@ -286,6 +287,7 @@ export interface SaveApproverStockTakeRecordRequest {
approverId?: number | null; approverId?: number | null;
approverQty?: number | null; approverQty?: number | null;
approverBadQty?: number | null; approverBadQty?: number | null;
lastSelect?: number | null;
} }


export interface BatchSaveApproverStockTakeRecordRequest { export interface BatchSaveApproverStockTakeRecordRequest {


+ 202
- 65
src/components/StockTakeManagement/ApproverStockTakeAll.tsx ファイルの表示

@@ -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>


読み込み中…
キャンセル
保存