|
|
|
@@ -80,6 +80,8 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ |
|
|
|
/** Qty fields the user has left, or just saved. Warning stays hidden while typing. */ |
|
|
|
const [gapCheckOpen, setGapCheckOpen] = useState<Record<string, boolean>>({}); |
|
|
|
const [batchSaving, setBatchSaving] = useState(false); |
|
|
|
/** 已盤點列:按「更改」後才打開數量欄,再按「保存」。 */ |
|
|
|
const [editingFirstIds, setEditingFirstIds] = useState<Record<number, boolean>>({}); |
|
|
|
const [shortcutInput, setShortcutInput] = useState<string>(""); |
|
|
|
const [page, setPage] = useState(0); |
|
|
|
const [pageSize, setPageSize] = useState<number | string>("all"); |
|
|
|
@@ -90,6 +92,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ |
|
|
|
const currentUserId = session?.id ? parseInt(session.id) : undefined; |
|
|
|
const handleBatchTestAllRef = useRef<() => Promise<void>>(); |
|
|
|
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<PickerStockTakeProps> = ({ |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
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<PickerStockTakeProps> = ({ |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
saveInFlightRef.current = true; |
|
|
|
setSaving(true); |
|
|
|
try { |
|
|
|
const request: SaveStockTakeRecordRequest = { |
|
|
|
@@ -240,6 +271,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ |
|
|
|
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<PickerStockTakeProps> = ({ |
|
|
|
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<PickerStockTakeProps> = ({ |
|
|
|
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<PickerStockTakeProps> = ({ |
|
|
|
onSnackbar(errorMessage, "error"); |
|
|
|
} finally { |
|
|
|
setSaving(false); |
|
|
|
saveInFlightRef.current = false; |
|
|
|
} |
|
|
|
}, |
|
|
|
[ |
|
|
|
@@ -306,6 +343,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ |
|
|
|
currentUserId, |
|
|
|
onSnackbar, |
|
|
|
qtyGapWarnPercent, |
|
|
|
canReviseFirstCount, |
|
|
|
] |
|
|
|
); |
|
|
|
|
|
|
|
@@ -579,10 +617,28 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ |
|
|
|
) : ( |
|
|
|
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 ( |
|
|
|
<TableRow key={detail.id}> |
|
|
|
@@ -618,7 +674,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ |
|
|
|
<TableCell sx={{ width: 250, minWidth: 250 }}> |
|
|
|
<Stack spacing={1}> |
|
|
|
{/* First */} |
|
|
|
{!submitDisabled && isFirstSubmit ? ( |
|
|
|
{showFirstInput ? ( |
|
|
|
<Stack spacing={0.5} alignItems="flex-start"> |
|
|
|
<Stack direction="row" spacing={1} alignItems="center"> |
|
|
|
<Typography variant="body2">{t("First")}:</Typography> |
|
|
|
@@ -694,13 +750,21 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ |
|
|
|
/> |
|
|
|
</Stack> |
|
|
|
) : detail.firstStockTakeQty != null ? ( |
|
|
|
<Typography variant="body2"> |
|
|
|
{t("First")}:{" "} |
|
|
|
<StockTakeQtyWithUnit |
|
|
|
qty={formatNumber(detail.firstStockTakeQty ?? 0)} |
|
|
|
uomShortDesc={detail.uomShortDesc} |
|
|
|
<Stack spacing={0.5} alignItems="flex-start"> |
|
|
|
<Typography variant="body2"> |
|
|
|
{t("First")}:{" "} |
|
|
|
<StockTakeQtyWithUnit |
|
|
|
qty={formatNumber(detail.firstStockTakeQty ?? 0)} |
|
|
|
uomShortDesc={detail.uomShortDesc} |
|
|
|
/> |
|
|
|
</Typography> |
|
|
|
<StockTakeQtyGapHint |
|
|
|
open |
|
|
|
entered={String(Math.trunc(Number(detail.firstStockTakeQty)))} |
|
|
|
currentQty={stockTakeHiddenOnHand(detail)} |
|
|
|
threshold={qtyGapWarnPercent} |
|
|
|
/> |
|
|
|
</Typography> |
|
|
|
</Stack> |
|
|
|
) : null} |
|
|
|
|
|
|
|
{/* Second */} |
|
|
|
@@ -772,13 +836,21 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ |
|
|
|
/> |
|
|
|
</Stack> |
|
|
|
) : detail.secondStockTakeQty != null ? ( |
|
|
|
<Typography variant="body2"> |
|
|
|
{t("Second")}:{" "} |
|
|
|
<StockTakeQtyWithUnit |
|
|
|
qty={formatNumber(detail.secondStockTakeQty ?? 0)} |
|
|
|
uomShortDesc={detail.uomShortDesc} |
|
|
|
<Stack spacing={0.5} alignItems="flex-start"> |
|
|
|
<Typography variant="body2"> |
|
|
|
{t("Second")}:{" "} |
|
|
|
<StockTakeQtyWithUnit |
|
|
|
qty={formatNumber(detail.secondStockTakeQty ?? 0)} |
|
|
|
uomShortDesc={detail.uomShortDesc} |
|
|
|
/> |
|
|
|
</Typography> |
|
|
|
<StockTakeQtyGapHint |
|
|
|
open |
|
|
|
entered={String(Math.trunc(Number(detail.secondStockTakeQty)))} |
|
|
|
currentQty={stockTakeHiddenOnHand(detail)} |
|
|
|
threshold={qtyGapWarnPercent} |
|
|
|
/> |
|
|
|
</Typography> |
|
|
|
</Stack> |
|
|
|
) : null} |
|
|
|
|
|
|
|
{!detail.firstStockTakeQty && |
|
|
|
@@ -799,13 +871,39 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ |
|
|
|
<Button |
|
|
|
size="small" |
|
|
|
variant="contained" |
|
|
|
onClick={() => handleSaveStockTake(detail)} |
|
|
|
disabled={saving || submitDisabled} |
|
|
|
color={showChangeButton ? "warning" : "primary"} |
|
|
|
onClick={() => { |
|
|
|
if (changeUsedUp) return; |
|
|
|
if (revisingFirst && !editingFirst) { |
|
|
|
if ((detail.pickerReviseCount ?? 0) === 1) { |
|
|
|
onSnackbar(t("oneChangeLeft"), "warning"); |
|
|
|
} |
|
|
|
setEditingFirstIds((prev) => ({ ...prev, [detail.id]: true })); |
|
|
|
return; |
|
|
|
} |
|
|
|
handleSaveStockTake(detail); |
|
|
|
}} |
|
|
|
disabled={ |
|
|
|
saving || |
|
|
|
batchSaving || |
|
|
|
changeUsedUp || |
|
|
|
(submitDisabled && !revisingFirst) || |
|
|
|
(editingFirst && !firstQtyChanged) |
|
|
|
} |
|
|
|
> |
|
|
|
{t("Save")} |
|
|
|
{showChangeButton ? t("Change") : t("Save")} |
|
|
|
</Button> |
|
|
|
|
|
|
|
</Stack> |
|
|
|
{oneChanceLeft && ( |
|
|
|
<Typography |
|
|
|
variant="caption" |
|
|
|
component="div" |
|
|
|
role="status" |
|
|
|
sx={{ color: "warning.dark", fontWeight: 700, mt: 0.5, maxWidth: 140, lineHeight: 1.35 }} |
|
|
|
> |
|
|
|
{t("oneChangeLeft")} |
|
|
|
</Typography> |
|
|
|
)} |
|
|
|
|
|
|
|
</TableCell> |
|
|
|
{/* Remark */} |
|
|
|
|