Quellcode durchsuchen

ui update

stockTakeUpdate
CANCERYS\kw093 vor 1 Woche
Ursprung
Commit
63abd5b22c
8 geänderte Dateien mit 266 neuen und 149 gelöschten Zeilen
  1. +92
    -49
      src/components/StockTakeManagement/ApproverStockTakeAll.tsx
  2. +11
    -27
      src/components/StockTakeManagement/PickerCardList.tsx
  3. +68
    -31
      src/components/StockTakeManagement/PickerReStockTake.tsx
  4. +72
    -34
      src/components/StockTakeManagement/PickerStockTake.tsx
  5. +12
    -3
      src/components/StockTakeManagement/StockTakeRoundCardList.tsx
  6. +6
    -0
      src/components/StockTakeManagement/StockTakeTab.tsx
  7. +2
    -2
      src/i18n/en/stockTake.json
  8. +3
    -3
      src/i18n/zh/stockTake.json

+ 92
- 49
src/components/StockTakeManagement/ApproverStockTakeAll.tsx Datei anzeigen

@@ -77,6 +77,8 @@ interface ApproverStockTakeAllProps {
onSnackbar: (message: string, severity: "success" | "error" | "warning") => void;
/** 審核待處理時,把批量保存按鈕掛到分頁列右側 */
batchButtonHost?: HTMLElement | null;
/** History / closed round — disable save / batch save */
roundReadOnly?: boolean;
}

type QtySelectionType = "first" | "second" | "approver";
@@ -368,6 +370,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
onBack,
onSnackbar,
batchButtonHost = null,
roundReadOnly = false,
}) => {
const { t } = useTranslation(["stockTake", "common"]);
const qtyGapWarnPercent = useStockTakeQtyGapWarnPercent();
@@ -375,6 +378,8 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
const isAdmin = (session?.abilities ?? session?.user?.abilities ?? []).some(
(ability) => String(ability).trim() === AUTH.ADMIN,
);
const isRoundClosed =
roundReadOnly || selectedSession?.status?.toLowerCase() === "completed";

const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]);
const [loadingDetails, setLoadingDetails] = useState(false);
@@ -896,7 +901,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({

const handleSaveApproverStockTake = useCallback(
async (detail: InventoryLotDetailResponse) => {
if (mode === "approved") return;
if (mode === "approved" || isRoundClosed) return;
if (!selectedSession || !currentUserId) {
return;
}
@@ -975,7 +980,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({

const handleUpdateStatusToNotMatch = useCallback(
async (detail: InventoryLotDetailResponse) => {
if (mode === "approved") return;
if (mode === "approved" || isRoundClosed) return;
if (!detail.stockTakeRecordId) {
onSnackbar(t("Stock take record ID is required"), "error");
return;
@@ -1056,7 +1061,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
}, [sortedDetails]);

const handleOpenBatchSaveConfirm = useCallback(() => {
if (mode === "approved") return;
if (mode === "approved" || isRoundClosed) return;
if (!selectedSession || !currentUserId) return;
if (inventoryLotDetails.length === 0) {
onSnackbar(t("No rows loaded; set search criteria and search first"), "warning");
@@ -1079,7 +1084,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({

const handleBatchSubmitAll = useCallback(async () => {
if (batchSaveInFlightRef.current) return;
if (mode === "approved") return;
if (mode === "approved" || isRoundClosed) return;
if (!selectedSession || !currentUserId) return;

const records = buildBatchApproverSaveRequests(
@@ -1169,7 +1174,30 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
if (!ms) return "-";
return dayjs(ms).format("YYYY-MM-DD HH:mm");
};
const rows = sortedDetails;
/** Word-style: same location grouped under a heading (picker-like). */
const locationGroups = useMemo(() => {
const groups: { key: string; label: string; items: InventoryLotDetailResponse[] }[] = [];
const indexByKey = new Map<string, number>();
for (const detail of sortedDetails) {
const area = detail.warehouseArea?.trim() || "";
const slot = detail.warehouseSlot?.trim() || "";
const code = detail.warehouseCode?.trim() || "";
const key = code || `${area}|${slot}` || String(detail.id);
const label =
code ||
(area || slot ? `${area}${slot}` : "") ||
detail.stockTakeSection?.trim() ||
"-";
let idx = indexByKey.get(key);
if (idx == null) {
idx = groups.length;
indexByKey.set(key, idx);
groups.push({ key, label, items: [] });
}
groups[idx].items.push(detail);
}
return groups;
}, [sortedDetails]);

const columns = useMemo<GridColDef<InventoryLotDetailResponse>[]>(() => {
const cols: GridColDef<InventoryLotDetailResponse>[] = [];
@@ -1190,29 +1218,11 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
}
cols.push(
{
field: "warehouseLocation",
headerName: t("Warehouse Location"),
minWidth: 220,
flex: 1,
sortable: false,
renderCell: (params) => (
<Stack spacing={0.5}>
<Typography variant="body2">
<strong>
{params.row.stockTakeSection || "-"}{" "}
{params.row.stockTakeSectionDescription || "-"}
</strong>
</Typography>
<Typography variant="body2">{params.row.warehouseCode || "-"}</Typography>
</Stack>
),
},
{
field: "itemLot",
headerName: t("Item-lotNo-ExpiryDate"),
minWidth: 160,
flex: 1,
minWidth: 220,
flex: 1.6,
sortable: false,
renderCell: (params) => (
<Stack spacing={0.5} sx={{ lineHeight: 1.5, py: 0.5 }}>
@@ -1222,12 +1232,20 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
>
{params.row.itemCode || "-"} {params.row.itemName || "-"}
</Typography>
<Box>{params.row.lotNo || "-"}</Box>
<Box>
<Typography
component="div"
sx={{ fontWeight: 800, fontSize: "1.15rem", lineHeight: 1.3, color: "text.primary" }}
>
{params.row.lotNo || "-"}
</Typography>
<Typography
component="div"
sx={{ fontWeight: 800, fontSize: "1.15rem", lineHeight: 1.3, color: "text.primary" }}
>
{params.row.expiryDate
? dayjs(params.row.expiryDate).format(OUTPUT_DATE_FORMAT)
: "-"}
</Box>
</Typography>
</Stack>
),
},
@@ -1577,7 +1595,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
size="small"
variant="contained"
onClick={() => handleSaveApproverStockTake(detail)}
disabled={!isAdmin || saving}
disabled={!isAdmin || saving || isRoundClosed}
>
{t("Save")}
</Button>
@@ -1588,7 +1606,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
variant="outlined"
color="warning"
onClick={() => handleUpdateStatusToNotMatch(detail)}
disabled={!isAdmin || updatingStatus || hasSecond}
disabled={!isAdmin || updatingStatus || hasSecond || isRoundClosed}
>
{t("Not Match")}
</Button>
@@ -1626,6 +1644,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
handleSaveApproverStockTake,
handleUpdateStatusToNotMatch,
formatRecordEndTime,
isRoundClosed,
]);
const effectivePageSize =
pageSize === "all" ? Math.max(total || 0, 0) : (pageSize as number);
@@ -1635,7 +1654,7 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
variant="contained"
color="primary"
onClick={handleOpenBatchSaveConfirm}
disabled={!isAdmin || batchSaving}
disabled={!isAdmin || batchSaving || isRoundClosed}
>
{t("Batch Save All")}
</Button>
@@ -2133,24 +2152,48 @@ const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
</Typography>
{varianceRangeBar}
</Stack>
<Box sx={{ width: "100%", height: 700 }}>
<StyledDataGrid
rows={sortedDetails}
columns={columns}
getRowId={(row) => row.id}
loading={loadingDetails}
disableRowSelectionOnClick
paginationMode="client" // ← important: client-side
pageSizeOptions={[50, 100, 200, 500]}
getRowHeight={() => "auto"}
sx={{
backgroundColor: "transparent",
"& .MuiDataGrid-columnHeaders": { backgroundColor: "#fff" },
"& .MuiDataGrid-cell": { py: 1, alignItems: "flex-start" },
"& .MuiDataGrid-row": { minHeight: 96 },
}}
/>
</Box>
<Stack spacing={2} sx={{ width: "100%" }}>
{loadingDetails && sortedDetails.length === 0 ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : sortedDetails.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
{t("No data")}
</Typography>
) : (
locationGroups.map((group) => (
<Box key={group.key} sx={{ width: "100%" }}>
<Typography variant="subtitle1" fontWeight={700} sx={{ mb: 1, px: 0.5 }}>
{t("Warehouse Location")}: {group.label}
</Typography>
<Box
sx={{
width: "100%",
minHeight: 120,
height: Math.min(700, 72 + group.items.length * 110),
}}
>
<StyledDataGrid
rows={group.items}
columns={columns}
getRowId={(row) => row.id}
loading={loadingDetails}
disableRowSelectionOnClick
hideFooter
getRowHeight={() => "auto"}
sx={{
backgroundColor: "transparent",
"& .MuiDataGrid-columnHeaders": { backgroundColor: "#fff" },
"& .MuiDataGrid-cell": { py: 1, alignItems: "flex-start" },
"& .MuiDataGrid-row": { minHeight: 96 },
}}
/>
</Box>
</Box>
))
)}
</Stack>

<Dialog
open={openBatchSaveConfirmDialog}


+ 11
- 27
src/components/StockTakeManagement/PickerCardList.tsx Datei anzeigen

@@ -1958,11 +1958,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
/>
</LocalizationProvider>
</Box>
{createMode === "item" ? (
<Typography variant="body2" color="text.secondary">
{t("Create by item hint")}
</Typography>
) : sortedFloorKeys.length === 0 && !sectionsLoading ? (
{createMode === "item" ? null : sortedFloorKeys.length === 0 && !sectionsLoading ? (
<Typography variant="body2" color="text.secondary">
{t("No stock take sections from warehouse")}
</Typography>
@@ -2141,9 +2137,6 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
</Select>
</FormControl>
</Stack>
<Typography variant="caption" color="text.secondary">
{t("Create by item hint")}
</Typography>
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.75 }}>
{createDialogSelectedItems.map((it) => (
<Chip
@@ -2167,22 +2160,19 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
}}
>
<Box sx={{ minHeight: 0, overflowY: "auto", border: 1, borderColor: "divider", borderRadius: 1, p: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ mb: 0.5, display: "block" }}>
{t("Create by item search hint")}
</Typography>
{createDialogItemSearching ? (
<Box sx={{ display: "flex", justifyContent: "center", py: 3 }}>
<CircularProgress size={24} />
</Box>
) : createDialogItemHits.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
{createDialogItemCode.trim() ||
createDialogItemName.trim() ||
(createDialogLotOrigin.trim() !== "" &&
createDialogLotOrigin.trim().toLowerCase() !== "all")
? t("No data")
: t("Create by item search hint")}
</Typography>
createDialogItemCode.trim() ||
createDialogItemName.trim() ||
(createDialogLotOrigin.trim() !== "" &&
createDialogLotOrigin.trim().toLowerCase() !== "all") ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
{t("No data")}
</Typography>
) : null
) : (
<Stack>
{createDialogItemHits.map((it) => {
@@ -2244,11 +2234,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
<Box sx={{ display: "flex", justifyContent: "center", py: 3 }}>
<CircularProgress size={24} />
</Box>
) : createDialogSelectedItems.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
{t("Create by item select item first")}
</Typography>
) : createDialogLotOptions.length === 0 ? (
) : createDialogSelectedItems.length === 0 ? null : createDialogLotOptions.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
{t("No available lots for selected items")}
</Typography>
@@ -2671,9 +2657,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({
<Typography variant="h6" fontWeight={700}>
{t("Warehouse missing stock take section warn title")}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Warehouse missing stock take section drawer hint")}
</Typography>

{missingSectionCount > 0 ? (
<Typography variant="caption" color="text.secondary" display="block" sx={{ mt: 1 }}>
{t("Warehouse missing stock take section showing", {


+ 68
- 31
src/components/StockTakeManagement/PickerReStockTake.tsx Datei anzeigen

@@ -17,7 +17,7 @@ import {
TextField,
TablePagination,
} from "@mui/material";
import { useState, useCallback, useEffect, useRef } from "react";
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
AllPickedStockTakeListReponse,
@@ -49,12 +49,15 @@ interface PickerReStockTakeProps {
selectedSession: AllPickedStockTakeListReponse;
onBack: () => void;
onSnackbar: (message: string, severity: "success" | "error" | "warning") => void;
/** History / closed round — disable all save actions */
roundReadOnly?: boolean;
}

const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
selectedSession,
onBack,
onSnackbar,
roundReadOnly = false,
}) => {
const { t } = useTranslation(["stockTake", "common"]);
const qtyGapWarnPercent = useStockTakeQtyGapWarnPercent();
@@ -81,7 +84,8 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
const currentUserId = session?.id ? parseInt(session.id) : undefined;
const handleBatchTestAllRef = useRef<() => Promise<void>>();
const batchInFlightRef = useRef(false);
const isSessionCompleted = selectedSession?.status?.toLowerCase() === "completed";
const isSessionCompleted =
roundReadOnly || selectedSession?.status?.toLowerCase() === "completed";
const handleChangePage = useCallback((event: unknown, newPage: number) => {
setPage(newPage);
@@ -204,7 +208,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
});
};
const handleSaveStockTake = useCallback(async (detail: InventoryLotDetailResponse) => {
if (!selectedSession || !currentUserId) {
if (!selectedSession || !currentUserId || isSessionCompleted) {
return;
}
@@ -319,7 +323,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
}, [selectedSession, recordInputs, t, currentUserId, onSnackbar, page, pageSize, loadDetails, qtyGapWarnPercent]);

const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => {
if (selectedSession?.status?.toLowerCase() === "completed") {
if (roundReadOnly || selectedSession?.status?.toLowerCase() === "completed") {
return true;
}
const recordStatus = detail.stockTakeRecordStatus?.toLowerCase();
@@ -327,10 +331,10 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
return true;
}
return false;
}, [selectedSession?.status]);
}, [roundReadOnly, selectedSession?.status]);

const handleBatchTestAutoFill = useCallback(async () => {
if (!selectedSession || !currentUserId || batchInFlightRef.current) {
if (!selectedSession || !currentUserId || batchInFlightRef.current || isSessionCompleted) {
return;
}

@@ -368,7 +372,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
}, [selectedSession, t, currentUserId, onSnackbar, page, pageSize, loadDetails]);

const handleBatchSaveInputted = useCallback(async () => {
if (!selectedSession || !currentUserId || batchInFlightRef.current) return;
if (!selectedSession || !currentUserId || batchInFlightRef.current || isSessionCompleted) return;

const built = buildPickerBatchSaveRequests(
inventoryLotDetails,
@@ -478,6 +482,31 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
};
}, []);

const locationGroups = useMemo(() => {
const groups: { key: string; label: string; items: InventoryLotDetailResponse[] }[] = [];
const indexByKey = new Map<string, number>();
const itemMode = selectedSession.listMode === "item";
for (const detail of inventoryLotDetails) {
const area = detail.warehouseArea?.trim() || "";
const slot = detail.warehouseSlot?.trim() || "";
const code = detail.warehouseCode?.trim() || "";
const key = itemMode ? `${code}|${area}|${slot}` : `${area}|${slot}`;
const label = itemMode
? [code, [area, slot].filter(Boolean).join(" ")].filter(Boolean).join(" · ") || "-"
: area || slot
? `${area}${slot}`
: code || "-";
let idx = indexByKey.get(key);
if (idx == null) {
idx = groups.length;
indexByKey.set(key, idx);
groups.push({ key, label, items: [] });
}
groups[idx].items.push(detail);
}
return groups;
}, [inventoryLotDetails, selectedSession.listMode]);

const uniqueWarehouses = Array.from(
new Set(
inventoryLotDetails
@@ -523,9 +552,6 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
<Table>
<TableHead>
<TableRow>
<TableCell>
{selectedSession.listMode === "item" ? t("Warehouse code") : t("Warehouse Location")}
</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell sx={{ width: 250, minWidth: 250 }}>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
@@ -537,14 +563,30 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
<TableBody>
{inventoryLotDetails.length === 0 ? (
<TableRow>
<TableCell colSpan={7} align="center">
<TableCell colSpan={5} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
</TableCell>
</TableRow>
) : (
inventoryLotDetails.map((detail) => {
locationGroups.flatMap((group) => [
<TableRow key={`loc-${group.key}`}>
<TableCell
colSpan={5}
sx={{
bgcolor: "grey.50",
borderBottom: 1,
borderColor: "divider",
py: 1.25,
}}
>
<Typography variant="subtitle1" fontWeight={700}>
{t("Warehouse Location")}: {group.label}
</Typography>
</TableCell>
</TableRow>,
...group.items.map((detail) => {
const submitDisabled = isSubmitDisabled(detail);
const isFirstSubmit = detail.firstStockTakeQty == null;
const isSecondSubmit =
@@ -554,23 +596,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({

return (
<TableRow key={detail.id}>
<TableCell>
{selectedSession.listMode === "item" ? (
<Stack spacing={0.25}>
<Typography fontWeight={700}>{detail.warehouseCode || "-"}</Typography>
<Typography variant="body2" color="text.secondary">
{[detail.warehouseArea, detail.warehouseSlot].filter((v) => v?.trim()).join(" ") || "-"}
</Typography>
</Stack>
) : (
<>
{detail.warehouseArea || "-"}
{detail.warehouseSlot || "-"}
</>
)}
</TableCell>
<TableCell sx={{
maxWidth: 280,
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.5
@@ -582,8 +608,18 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
>
{detail.itemCode || "-"} {detail.itemName || "-"}
</Typography>
<Box>{detail.lotNo || "-"}</Box>
<Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box>
<Typography
component="div"
sx={{ fontWeight: 800, fontSize: "1.15rem", lineHeight: 1.3, color: "text.primary" }}
>
{detail.lotNo || "-"}
</Typography>
<Typography
component="div"
sx={{ fontWeight: 800, fontSize: "1.15rem", lineHeight: 1.3, color: "text.primary" }}
>
{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}
</Typography>
</Stack>
</TableCell>
<TableCell>{detail.uom || "-"}</TableCell>
@@ -810,7 +846,8 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
</TableRow>
);
})
}),
])
)}
</TableBody>
</Table>


+ 72
- 34
src/components/StockTakeManagement/PickerStockTake.tsx Datei anzeigen

@@ -22,7 +22,7 @@ import {
InputLabel,
} from "@mui/material";
import { SelectChangeEvent } from "@mui/material/Select";
import { useState, useCallback, useEffect, useRef } from "react";
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
AllPickedStockTakeListReponse,
@@ -67,12 +67,15 @@ interface PickerStockTakeProps {
selectedSession: AllPickedStockTakeListReponse;
onBack: () => void;
onSnackbar: (message: string, severity: "success" | "error" | "warning") => void;
/** History / closed round — disable all save actions */
roundReadOnly?: boolean;
}

const PickerStockTake: React.FC<PickerStockTakeProps> = ({
selectedSession,
onBack,
onSnackbar,
roundReadOnly = false,
}) => {
const { t } = useTranslation(["stockTake", "common"]);
const qtyGapWarnPercent = useStockTakeQtyGapWarnPercent();
@@ -107,7 +110,8 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
const handleBatchTestAllRef = useRef<() => Promise<void>>();
const batchInFlightRef = useRef(false);
const saveInFlightRef = useRef(false);
const isSessionCompleted = selectedSession?.status?.toLowerCase() === "completed";
const isSessionCompleted =
roundReadOnly || selectedSession?.status?.toLowerCase() === "completed";
const handleChangePage = useCallback((event: unknown, newPage: number) => {
setPage(newPage);
}, []);
@@ -215,6 +219,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
};

const canReviseFirstCount = useCallback((detail: InventoryLotDetailResponse): boolean => {
if (roundReadOnly) return false;
const sessionStatus = selectedSession?.status?.toLowerCase() ?? "";
if (sessionStatus === "completed") {
return false;
@@ -236,11 +241,11 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
return false;
}
return true;
}, [selectedSession?.status]);
}, [roundReadOnly, selectedSession?.status]);

const handleSaveStockTake = useCallback(
async (detail: InventoryLotDetailResponse) => {
if (!selectedSession || !currentUserId || saveInFlightRef.current) {
if (!selectedSession || !currentUserId || saveInFlightRef.current || isSessionCompleted) {
return;
}

@@ -361,6 +366,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
remark,
t,
currentUserId,
isSessionCompleted,
onSnackbar,
qtyGapWarnPercent,
canReviseFirstCount,
@@ -369,7 +375,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({

/** 测试快捷键:按账面自动建记录(非用户输入批量) */
const handleBatchTestAutoFill = useCallback(async () => {
if (!selectedSession || !currentUserId || batchInFlightRef.current) {
if (!selectedSession || !currentUserId || batchInFlightRef.current || isSessionCompleted) {
return;
}

@@ -502,7 +508,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
};
const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => {
if (selectedSession?.status?.toLowerCase() === "completed") {
if (roundReadOnly || selectedSession?.status?.toLowerCase() === "completed") {
return true;
}
const recordStatus = detail.stockTakeRecordStatus?.toLowerCase();
@@ -510,12 +516,12 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
return true;
}
return false;
}, [selectedSession?.status]);
}, [roundReadOnly, selectedSession?.status]);
editableQtyIdsRef.current = inventoryLotDetails
.filter((detail) => !isSubmitDisabled(detail) && (detail.firstStockTakeQty == null || detail.secondStockTakeQty == null))
.map((detail) => detail.id);
const handleBatchSaveInputted = useCallback(async () => {
if (!selectedSession || !currentUserId || batchInFlightRef.current) return;
if (!selectedSession || !currentUserId || batchInFlightRef.current || isSessionCompleted) return;

const built = buildPickerBatchSaveRequests(
inventoryLotDetails,
@@ -564,6 +570,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
}, [
selectedSession,
currentUserId,
isSessionCompleted,
inventoryLotDetails,
recordInputs,
isSubmitDisabled,
@@ -573,6 +580,31 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
pageSize,
loadDetails,
]);
const locationGroups = useMemo(() => {
const groups: { key: string; label: string; items: InventoryLotDetailResponse[] }[] = [];
const indexByKey = new Map<string, number>();
const itemMode = selectedSession.listMode === "item";
for (const detail of inventoryLotDetails) {
const area = detail.warehouseArea?.trim() || "";
const slot = detail.warehouseSlot?.trim() || "";
const code = detail.warehouseCode?.trim() || "";
const key = itemMode ? `${code}|${area}|${slot}` : `${area}|${slot}`;
const label = itemMode
? [code, [area, slot].filter(Boolean).join(" ")].filter(Boolean).join(" · ") || "-"
: area || slot
? `${area}${slot}`
: code || "-";
let idx = indexByKey.get(key);
if (idx == null) {
idx = groups.length;
indexByKey.set(key, idx);
groups.push({ key, label, items: [] });
}
groups[idx].items.push(detail);
}
return groups;
}, [inventoryLotDetails, selectedSession.listMode]);

const uniqueWarehouses = Array.from(
new Set(
inventoryLotDetails
@@ -651,9 +683,6 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
<Table>
<TableHead>
<TableRow>
<TableCell>
{selectedSession.listMode === "item" ? t("Warehouse code") : t("Warehouse Location")}
</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell sx={{ width: 250, minWidth: 250 }}>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
@@ -666,18 +695,34 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
<TableBody>
{inventoryLotDetails.length === 0 ? (
<TableRow>
<TableCell colSpan={7} align="center">
<TableCell colSpan={5} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
</TableCell>
</TableRow>
) : (
inventoryLotDetails.map((detail) => {
locationGroups.flatMap((group) => [
<TableRow key={`loc-${group.key}`}>
<TableCell
colSpan={5}
sx={{
bgcolor: "grey.50",
borderBottom: 1,
borderColor: "divider",
py: 1.25,
}}
>
<Typography variant="subtitle1" fontWeight={700}>
{t("Warehouse Location")}: {group.label}
</Typography>
</TableCell>
</TableRow>,
...group.items.map((detail) => {
const submitDisabled = isSubmitDisabled(detail);
const revisingFirst = canReviseFirstCount(detail);
const editingFirst = revisingFirst && !!editingFirstIds[detail.id];
const sessionOpen = selectedSession?.status?.toLowerCase() !== "completed";
const sessionOpen = !isSessionCompleted;
const changeUsedUp =
sessionOpen &&
detail.stockTakeRecordStatus?.toLowerCase() === "pass" &&
@@ -710,24 +755,8 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
...(canEditQty ? { cursor: "pointer" } : {}),
}}
>
<TableCell>
{selectedSession.listMode === "item" ? (
<Stack spacing={0.25}>
<Typography fontWeight={700}>{detail.warehouseCode || "-"}</Typography>
<Typography variant="body2" color="text.secondary">
{[detail.warehouseArea, detail.warehouseSlot].filter((v) => v?.trim()).join(" ") || "-"}
</Typography>
</Stack>
) : (
<>
{detail.warehouseArea || "-"}
{detail.warehouseSlot || "-"}
</>
)}
</TableCell>
<TableCell
sx={{
maxWidth: 280,
wordBreak: "break-word",
whiteSpace: "normal",
lineHeight: 1.5,
@@ -740,12 +769,20 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
>
{detail.itemCode || "-"} {detail.itemName || "-"}
</Typography>
<Box>{detail.lotNo || "-"}</Box>
<Box>
<Typography
component="div"
sx={{ fontWeight: 800, fontSize: "1.15rem", lineHeight: 1.3, color: "text.primary" }}
>
{detail.lotNo || "-"}
</Typography>
<Typography
component="div"
sx={{ fontWeight: 800, fontSize: "1.15rem", lineHeight: 1.3, color: "text.primary" }}
>
{detail.expiryDate
? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT)
: "-"}
</Box>
</Typography>
</Stack>
</TableCell>
<TableCell>{detail.uom || "-"}</TableCell>
@@ -1040,7 +1077,8 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({

</TableRow>
);
})
}),
])
)}
</TableBody>
</Table>


+ 12
- 3
src/components/StockTakeManagement/StockTakeRoundCardList.tsx Datei anzeigen

@@ -348,9 +348,18 @@ const StockTakeRoundCardList: React.FC<StockTakeRoundCardListProps> = ({
<Typography variant="subtitle1" fontWeight={600} noWrap>
{roundLabel(card)}
</Typography>
<Typography variant="body2" color="text.secondary">
{floor ? `${modeLabel} · ${floor}` : modeLabel}
</Typography>
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ mt: 0.25, flexWrap: "wrap" }}>
<Chip
size="small"
label={modeLabel}
color={isItemRound ? "info" : "primary"}
/>
{floor ? (
<Typography variant="body2" color="text.secondary">
{floor}
</Typography>
) : null}
</Stack>
</Box>
<Chip
size="small"


+ 6
- 0
src/components/StockTakeManagement/StockTakeTab.tsx Datei anzeigen

@@ -212,6 +212,8 @@ const StockTakeTab: React.FC = () => {

const showingPickerDetail = Boolean(selectedSession) && viewScope === "picker" && tabValue === 0;
const roundId = selectedRound?.stockTakeRoundId ?? 0;
/** History / closed round: disable picker & approver save actions */
const roundReadOnly = Boolean(selectedRound && !selectedRound.open);
const roundLabel = (card: StockTakeRoundCard) => {
const name = card.stockTakeRoundName?.trim();
const title = name || `${t("Stock Take Round")} #${card.stockTakeRoundId}`;
@@ -228,12 +230,14 @@ const StockTakeTab: React.FC = () => {
selectedSession={selectedSession}
onBack={handleBackToList}
onSnackbar={handleSnackbar}
roundReadOnly={roundReadOnly}
/>
) : (
<PickerStockTake
selectedSession={selectedSession}
onBack={handleBackToList}
onSnackbar={handleSnackbar}
roundReadOnly={roundReadOnly}
/>
)
) : null}
@@ -413,6 +417,7 @@ const StockTakeTab: React.FC = () => {
mode="pending"
onSnackbar={handleSnackbar}
batchButtonHost={tabValue === 1 ? batchButtonHost : null}
roundReadOnly={roundReadOnly}
/>
) : (
<Typography variant="body2" color="text.secondary">
@@ -426,6 +431,7 @@ const StockTakeTab: React.FC = () => {
selectedSession={approverSession}
mode="approved"
onSnackbar={handleSnackbar}
roundReadOnly={roundReadOnly}
/>
) : !approverLoading ? (
<Typography variant="body2" color="text.secondary">


+ 2
- 2
src/i18n/en/stockTake.json Datei anzeigen

@@ -65,8 +65,8 @@
"Section create item overlap title": "These items are already in an open item stock-take round; this round will skip them",
"Section create item overlap line": "Item {{item}} is in round #{{round}}{{roundName}}; this round will not include it (section {{section}})",
"Control Time": "Time spent",
"Create by item": "By item",
"Create by section": "By section",
"Create by item": "Item stock-take",
"Create by section": "Location stock-take",
"Cannot create while both section and item rounds are open": "Cannot create a new round while both section and item rounds are open",
"Lot Origin": "Lot origin",
"Create by item hint": "Only items with available lots are listed. Pick origin (PP / PF / Other) to load the list, optionally narrow by code/name, then tick lots on the right.",


+ 3
- 3
src/i18n/zh/stockTake.json Datei anzeigen

@@ -146,9 +146,9 @@
"Select area first": "請先選擇倉位",
"Slot": "位置",
"Start Stock Take Date": "盤點日期",
"Create by item": "依貨品",
"Create by section": "依區域",
"Cannot create while both section and item rounds are open": "目前同時有開著的依區域與依貨品盤點輪,無法再建立新輪",
"Create by item": "貨品盤點",
"Create by section": "區域盤點",
"Cannot create while both section and item rounds are open": "目前同時有開著的區域盤點與貨品盤點輪,無法再建立新輪",
"Lot Origin": "來源",
"Create by item hint": "只列出有可用庫存的貨品。選來源(PP/PF/其他)即可列出;也可再輸入編號/名稱縮小。先勾選貨品,再於右側勾選批號/位置。",
"Create by item search hint": "可只選來源列出;或再輸入編號/名稱縮小。兩個關鍵字都填時要同時符合",


Laden…
Abbrechen
Speichern