Преглед на файлове

make the stock take qty can be edit before 完成

production
PC-20260115JRSN\Administrator преди 6 часа
родител
ревизия
be499f5d90
променени са 5 файла, в които са добавени 150 реда и са изтрити 32 реда
  1. +4
    -0
      src/app/api/stockTake/actions.ts
  2. +126
    -28
      src/components/StockTakeManagement/PickerStockTake.tsx
  3. +16
    -4
      src/components/StockTakeManagement/StockTakeTab.tsx
  4. +2
    -0
      src/i18n/en/stockTake.json
  5. +2
    -0
      src/i18n/zh/stockTake.json

+ 4
- 0
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;


+ 126
- 28
src/components/StockTakeManagement/PickerStockTake.tsx Целия файл

@@ -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 */}


+ 16
- 4
src/components/StockTakeManagement/StockTakeTab.tsx Целия файл

@@ -145,9 +145,15 @@ const StockTakeTab: React.FC = () => {
<Snackbar
open={snackbar.open}
autoHideDuration={6000}
onClose={() => setSnackbar({ ...snackbar, open: false })}
onClose={(_event, reason) => {
if (reason === "clickaway") return;
setSnackbar((prev) => ({ ...prev, open: false }));
}}
>
<Alert onClose={() => setSnackbar({ ...snackbar, open: false })} severity={snackbar.severity}>
<Alert
onClose={() => setSnackbar((prev) => ({ ...prev, open: false }))}
severity={snackbar.severity}
>
{snackbar.message}
</Alert>
</Snackbar>
@@ -268,9 +274,15 @@ const StockTakeTab: React.FC = () => {
<Snackbar
open={snackbar.open}
autoHideDuration={6000}
onClose={() => setSnackbar({ ...snackbar, open: false })}
onClose={(_event, reason) => {
if (reason === "clickaway") return;
setSnackbar((prev) => ({ ...prev, open: false }));
}}
>
<Alert onClose={() => setSnackbar({ ...snackbar, open: false })} severity={snackbar.severity}>
<Alert
onClose={() => setSnackbar((prev) => ({ ...prev, open: false }))}
severity={snackbar.severity}
>
{snackbar.message}
</Alert>
</Snackbar>


+ 2
- 0
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)",


+ 2
- 0
src/i18n/zh/stockTake.json Целия файл

@@ -8,6 +8,8 @@
"Remark": "備註",
"Action": "操作",
"Save": "保存",
"Change": "更改",
"oneChangeLeft": "只剩 1 次更改機會",
"Selected Qty": "選擇數量",
"Not Match": "要求重點",
"Search Criteria": "搜索條件",


Зареждане…
Отказ
Запис