Selaa lähdekoodia

stocktakeALL

reset-do-picking-order
CANCERYS\kw093 1 viikko sitten
vanhempi
commit
9b5d1306d9
5 muutettua tiedostoa jossa 1077 lisäystä ja 21 poistoa
  1. +197
    -0
      src/components/StockTakeManagement/ApproverAllCardList.tsx
  2. +1
    -1
      src/components/StockTakeManagement/ApproverCardList.tsx
  3. +808
    -0
      src/components/StockTakeManagement/ApproverStockTakeAll.tsx
  4. +70
    -20
      src/components/StockTakeManagement/StockTakeTab.tsx
  5. +1
    -0
      src/i18n/zh/inventory.json

+ 197
- 0
src/components/StockTakeManagement/ApproverAllCardList.tsx Näytä tiedosto

@@ -0,0 +1,197 @@
"use client";

import {
Box,
Card,
CardContent,
CardActions,
Typography,
CircularProgress,
Grid,
Chip,
Button,
TablePagination,
} from "@mui/material";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import {
AllPickedStockTakeListReponse,
getApproverStockTakeRecords,
} from "@/app/api/stockTake/actions";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";

const PER_PAGE = 6;

interface ApproverAllCardListProps {
onCardClick: (session: AllPickedStockTakeListReponse) => void;
}

const ApproverAllCardList: React.FC<ApproverAllCardListProps> = ({
onCardClick,
}) => {
const { t } = useTranslation(["inventory", "common"]);
const [loading, setLoading] = useState(false);
const [sessions, setSessions] = useState<AllPickedStockTakeListReponse[]>([]);
const [page, setPage] = useState(0);

const fetchSessions = useCallback(async () => {
setLoading(true);
try {
const data = await getApproverStockTakeRecords();
const list = Array.isArray(data) ? data : [];

// 找出最新一轮的 planStartDate
const withPlanStart = list.filter((s) => s.planStartDate);
if (withPlanStart.length === 0) {
setSessions([]);
setPage(0);
return;
}

const latestPlanStart = withPlanStart
.map((s) => s.planStartDate as string)
.sort((a, b) => dayjs(b).valueOf() - dayjs(a).valueOf())[0];

// 这一轮下所有 section 的卡片
const roundSessions = list.filter((s) => s.planStartDate === latestPlanStart);

// 汇总这一轮的总 item / lot 数
const totalItems = roundSessions.reduce(
(sum, s) => sum + (s.totalItemNumber || 0),
0
);
const totalLots = roundSessions.reduce(
(sum, s) => sum + (s.totalInventoryLotNumber || 0),
0
);

// 用这一轮里的第一条作为代表,覆盖汇总数字
const representative = roundSessions[0];
const mergedRound: AllPickedStockTakeListReponse = {
...representative,
totalItemNumber: totalItems,
totalInventoryLotNumber: totalLots,
};

// UI 上只展示这一轮一张卡
setSessions([mergedRound]);
setPage(0);
} catch (e) {
console.error(e);
setSessions([]);
} finally {
setLoading(false);
}
}, []);

useEffect(() => {
fetchSessions();
}, [fetchSessions]);

const getStatusColor = (status: string | null) => {
if (!status) return "default";
const statusLower = status.toLowerCase();
if (statusLower === "completed") return "success";
if (statusLower === "approving") return "info";
return "warning";
};

const paged = useMemo(() => {
const startIdx = page * PER_PAGE;
return sessions.slice(startIdx, startIdx + PER_PAGE);
}, [page, sessions]);

if (loading) {
return (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
);
}

return (
<Box>


<Grid container spacing={2}>
{paged.map((session) => {
const statusColor = getStatusColor(session.status);
const planStart = session.planStartDate
? dayjs(session.planStartDate).format(OUTPUT_DATE_FORMAT)
: "-";

return (
<Grid key={session.stockTakeId} item xs={12} sm={6} md={4}>
<Card
sx={{
minHeight: 180,
display: "flex",
flexDirection: "column",
border: "1px solid",
borderColor:
statusColor === "success" ? "success.main" : "primary.main",
cursor: "pointer",
"&:hover": {
boxShadow: 4,
},
}}
onClick={() => onCardClick(session)}
>
<CardContent sx={{ pb: 1, flexGrow: 1 }}>
<Typography variant="subtitle1" fontWeight={600} sx={{ mb: 0.5 }}>
{t("Stock Take Round")}: {planStart}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Plan Start Date")}: {planStart}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Total Items")}: {session.totalItemNumber}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Total Lots")}: {session.totalInventoryLotNumber}
</Typography>
</CardContent>
<CardActions sx={{ pt: 0.5, justifyContent: "space-between" }}>
<Button
size="small"
variant="contained"
onClick={(e) => {
e.stopPropagation();
onCardClick(session);
}}
>
{t("View Details")}
</Button>
{session.status ? (
<Chip
size="small"
label={t(session.status)}
color={statusColor as any}
/>
) : (
<Chip size="small" label={t(" ")} color="default" />
)}
</CardActions>
</Card>
</Grid>
);
})}
</Grid>

{sessions.length > 0 && (
<TablePagination
component="div"
count={sessions.length}
page={page}
rowsPerPage={PER_PAGE}
onPageChange={(_, p) => setPage(p)}
rowsPerPageOptions={[PER_PAGE]}
/>
)}
</Box>
);
};

export default ApproverAllCardList;


+ 1
- 1
src/components/StockTakeManagement/ApproverCardList.tsx Näytä tiedosto

@@ -23,7 +23,7 @@ import {
} from "@/app/api/stockTake/actions";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { I18nProvider, getServerI18n } from "@/i18n";
const PER_PAGE = 6;

interface ApproverCardListProps {


+ 808
- 0
src/components/StockTakeManagement/ApproverStockTakeAll.tsx Näytä tiedosto

@@ -0,0 +1,808 @@
"use client";

import {
Box,
Button,
Stack,
Typography,
Chip,
CircularProgress,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
TextField,
Radio,
TablePagination,
} from "@mui/material";
import { useState, useCallback, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
AllPickedStockTakeListReponse,
InventoryLotDetailResponse,
SaveApproverStockTakeRecordRequest,
saveApproverStockTakeRecord,
getApproverInventoryLotDetailsAll,
BatchSaveApproverStockTakeAllRequest,
batchSaveApproverStockTakeRecordsAll,
updateStockTakeRecordStatusToNotMatch,
} from "@/app/api/stockTake/actions";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";

interface ApproverStockTakeAllProps {
selectedSession: AllPickedStockTakeListReponse;
onBack: () => void;
onSnackbar: (message: string, severity: "success" | "error" | "warning") => void;
}

type QtySelectionType = "first" | "second" | "approver";

const ApproverStockTakeAll: React.FC<ApproverStockTakeAllProps> = ({
selectedSession,
onBack,
onSnackbar,
}) => {
const { t } = useTranslation(["inventory", "common"]);
const { data: session } = useSession() as { data: SessionWithTokens | null };

const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]);
const [loadingDetails, setLoadingDetails] = useState(false);
const [variancePercentTolerance, setVariancePercentTolerance] = useState<string>("5");
const [qtySelection, setQtySelection] = useState<Record<number, QtySelectionType>>({});
const [approverQty, setApproverQty] = useState<Record<number, string>>({});
const [approverBadQty, setApproverBadQty] = useState<Record<number, string>>({});
const [saving, setSaving] = useState(false);
const [batchSaving, setBatchSaving] = useState(false);
const [updatingStatus, setUpdatingStatus] = useState(false);
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState<number | string>("all");
const [total, setTotal] = useState(0);

const currentUserId = session?.id ? parseInt(session.id) : undefined;

const handleChangePage = useCallback((_: unknown, newPage: number) => {
setPage(newPage);
}, []);

const handleChangeRowsPerPage = useCallback(
(event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const newSize = parseInt(event.target.value, 10);
if (newSize === -1) {
setPageSize("all");
} else if (!isNaN(newSize)) {
setPageSize(newSize);
}
setPage(0);
},
[]
);

const loadDetails = useCallback(
async (pageNum: number, size: number | string) => {
setLoadingDetails(true);
try {
let actualSize: number;
if (size === "all") {
if (total > 0) {
actualSize = total;
} else if (selectedSession.totalInventoryLotNumber > 0) {
actualSize = selectedSession.totalInventoryLotNumber;
} else {
actualSize = 10000;
}
} else {
actualSize = typeof size === "string" ? parseInt(size, 10) : size;
}

const response = await getApproverInventoryLotDetailsAll(
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null,
pageNum,
actualSize
);
setInventoryLotDetails(Array.isArray(response.records) ? response.records : []);
setTotal(response.total || 0);
} catch (e) {
console.error(e);
setInventoryLotDetails([]);
setTotal(0);
} finally {
setLoadingDetails(false);
}
},
[selectedSession, total]
);

useEffect(() => {
loadDetails(page, pageSize);
}, [page, pageSize, loadDetails]);

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";
}
}
});

if (Object.keys(newSelections).length > 0) {
setQtySelection((prev) => ({ ...prev, ...newSelections }));
}
}, [inventoryLotDetails, qtySelection]);

const calculateDifference = useCallback(
(detail: InventoryLotDetailResponse, selection: QtySelectionType): number => {
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;
return selectedQty - bookQty;
},
[approverQty, approverBadQty]
);

const filteredDetails = useMemo(() => {
const percent = parseFloat(variancePercentTolerance || "0");
const thresholdPercent = isNaN(percent) || percent < 0 ? 0 : percent;
return inventoryLotDetails.filter((detail) => {
if (detail.finalQty != null || detail.stockTakeRecordStatus === "completed") {
return true;
}
const selection =
qtySelection[detail.id] ??
(detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0
? "second"
: "first");
const difference = calculateDifference(detail, selection);
const bookQty =
detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0);
if (bookQty === 0) return difference !== 0;
const threshold = Math.abs(bookQty) * (thresholdPercent / 100);
return Math.abs(difference) > threshold;
});
}, [
inventoryLotDetails,
variancePercentTolerance,
qtySelection,
calculateDifference,
]);

const handleSaveApproverStockTake = useCallback(
async (detail: InventoryLotDetailResponse) => {
if (!selectedSession || !currentUserId) {
return;
}

const selection = qtySelection[detail.id] || "first";
let finalQty: number;
let finalBadQty: number;

if (selection === "first") {
if (detail.firstStockTakeQty == null) {
onSnackbar(t("First QTY is not available"), "error");
return;
}
finalQty = detail.firstStockTakeQty;
finalBadQty = detail.firstBadQty || 0;
} else if (selection === "second") {
if (detail.secondStockTakeQty == null) {
onSnackbar(t("Second QTY is not available"), "error");
return;
}

finalQty = detail.secondStockTakeQty;
finalBadQty = detail.secondBadQty || 0;
} else {
const approverQtyValue = approverQty[detail.id];
const approverBadQtyValue = approverBadQty[detail.id];

if (
approverQtyValue === undefined ||
approverQtyValue === null ||
approverQtyValue === ""
) {
onSnackbar(t("Please enter Approver QTY"), "error");
return;
}
if (
approverBadQtyValue === undefined ||
approverBadQtyValue === null ||
approverBadQtyValue === ""
) {
onSnackbar(t("Please enter Approver Bad QTY"), "error");
return;
}

finalQty = parseFloat(approverQtyValue) || 0;
finalBadQty = parseFloat(approverBadQtyValue) || 0;
}

setSaving(true);
try {
const request: SaveApproverStockTakeRecordRequest = {
stockTakeRecordId: detail.stockTakeRecordId || null,
qty: finalQty,
badQty: finalBadQty,
approverId: currentUserId,
approverQty: selection === "approver" ? finalQty : null,
approverBadQty: selection === "approver" ? finalBadQty : null,
};

await saveApproverStockTakeRecord(request, selectedSession.stockTakeId);

onSnackbar(t("Approver stock take record saved successfully"), "success");

const goodQty = finalQty - finalBadQty;

setInventoryLotDetails((prev) =>
prev.map((d) =>
d.id === detail.id
? {
...d,
finalQty: goodQty,
approverQty: selection === "approver" ? finalQty : d.approverQty,
approverBadQty: selection === "approver" ? finalBadQty : d.approverBadQty,
stockTakeRecordStatus: "completed",
}
: d
)
);
} catch (e: any) {
console.error("Save approver stock take record error:", e);
let errorMessage = t("Failed to save approver stock take record");

if (e?.message) {
errorMessage = e.message;
} else if (e?.response) {
try {
const errorData = await e.response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch {
}
}

onSnackbar(errorMessage, "error");
} finally {
setSaving(false);
}
},
[selectedSession, currentUserId, qtySelection, approverQty, approverBadQty, t, onSnackbar]
);

const handleUpdateStatusToNotMatch = useCallback(
async (detail: InventoryLotDetailResponse) => {
if (!detail.stockTakeRecordId) {
onSnackbar(t("Stock take record ID is required"), "error");
return;
}

setUpdatingStatus(true);
try {
await updateStockTakeRecordStatusToNotMatch(detail.stockTakeRecordId);

onSnackbar(t("Stock take record status updated to not match"), "success");
setInventoryLotDetails((prev) =>
prev.map((d) =>
d.id === detail.id ? { ...d, stockTakeRecordStatus: "notMatch" } : d
)
);
} catch (e: any) {
console.error("Update stock take record status error:", e);
let errorMessage = t("Failed to update stock take record status");

if (e?.message) {
errorMessage = e.message;
} else if (e?.response) {
try {
const errorData = await e.response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch {
}
}

onSnackbar(errorMessage, "error");
} finally {
setUpdatingStatus(false);
}
},
[t, onSnackbar]
);

const handleBatchSubmitAll = useCallback(async () => {
if (!selectedSession || !currentUserId) {
return;
}

setBatchSaving(true);
try {
const request: BatchSaveApproverStockTakeAllRequest = {
stockTakeId: selectedSession.stockTakeId,
approverId: currentUserId,
variancePercentTolerance: parseFloat(variancePercentTolerance || "0") || undefined,
};

const result = await batchSaveApproverStockTakeRecordsAll(request);

onSnackbar(
t("Batch approver save completed: {{success}} success, {{errors}} errors", {
success: result.successCount,
errors: result.errorCount,
}),
result.errorCount > 0 ? "warning" : "success"
);

await loadDetails(page, pageSize);
} catch (e: any) {
console.error("handleBatchSubmitAll (all): Error:", e);
let errorMessage = t("Failed to batch save approver stock take records");

if (e?.message) {
errorMessage = e.message;
} else if (e?.response) {
try {
const errorData = await e.response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch {
}
}

onSnackbar(errorMessage, "error");
} finally {
setBatchSaving(false);
}
}, [selectedSession, currentUserId, variancePercentTolerance, t, onSnackbar, loadDetails, page, pageSize]);

const formatNumber = (num: number | null | undefined): string => {
if (num == null) return "0";
return num.toLocaleString("en-US", {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
};

const uniqueWarehouses = useMemo(
() =>
Array.from(
new Set(
inventoryLotDetails
.map((detail) => detail.warehouse)
.filter((warehouse) => warehouse && warehouse.trim() !== "")
)
).join(", "),
[inventoryLotDetails]
);

return (
<Box>
<Button
onClick={onBack}
sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}
>
{t("Back to List")}
</Button>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
sx={{ mb: 2 }}
>
<Typography variant="h6" sx={{ mb: 2 }}>
{uniqueWarehouses && (
<> {t("Warehouse")}: {uniqueWarehouses}</>
)}
</Typography>

<Stack direction="row" spacing={2} alignItems="center">
<TextField
size="small"
type="number"
value={variancePercentTolerance}
onChange={(e) => setVariancePercentTolerance(e.target.value)}
label={t("Variance %")}
sx={{ width: 100 }}
inputProps={{ min: 0, max: 100, step: 0.1 }}
/>
<Button
variant="contained"
color="primary"
onClick={handleBatchSubmitAll}
disabled={batchSaving}
>
{t("Batch Save All")}
</Button>
</Stack>
</Stack>
{loadingDetails ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : (
<>
<TablePagination
component="div"
count={total}
page={page}
onPageChange={handleChangePage}
rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
labelRowsPerPage={t("Rows per page")}
/>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>
{t("Stock Take Qty(include Bad Qty)= Available Qty")}
</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("Record Status")}</TableCell>
<TableCell>{t("Action")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredDetails.length === 0 ? (
<TableRow>
<TableCell colSpan={7} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
</TableCell>
</TableRow>
) : (
filteredDetails.map((detail) => {
const hasFirst =
detail.firstStockTakeQty != null && detail.firstStockTakeQty >= 0;
const hasSecond =
detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0;
const selection =
qtySelection[detail.id] || (hasSecond ? "second" : "first");

return (
<TableRow key={detail.id}>
<TableCell>
{detail.warehouseArea || "-"}
{detail.warehouseSlot || "-"}
</TableCell>
<TableCell
sx={{
maxWidth: 150,
wordBreak: "break-word",
whiteSpace: "normal",
lineHeight: 1.5,
}}
>
<Stack spacing={0.5}>
<Box>
{detail.itemCode || "-"} {detail.itemName || "-"}
</Box>
<Box>{detail.lotNo || "-"}</Box>
<Box>
{detail.expiryDate
? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT)
: "-"}
</Box>
</Stack>
</TableCell>
<TableCell>{detail.uom || "-"}</TableCell>
<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>
) : (
<Stack spacing={1}>
{hasFirst && (
<Stack
direction="row"
spacing={1}
alignItems="center"
>
<Radio
size="small"
checked={selection === "first"}
onChange={() =>
setQtySelection({
...qtySelection,
[detail.id]: "first",
})
}
/>
<Typography variant="body2">
{t("First")}:{" "}
{formatNumber(
(detail.firstStockTakeQty ?? 0) +
(detail.firstBadQty ?? 0)
)}{" "}
({detail.firstBadQty ?? 0}) ={" "}
{formatNumber(detail.firstStockTakeQty ?? 0)}
</Typography>
</Stack>
)}

{hasSecond && (
<Stack
direction="row"
spacing={1}
alignItems="center"
>
<Radio
size="small"
checked={selection === "second"}
onChange={() =>
setQtySelection({
...qtySelection,
[detail.id]: "second",
})
}
/>
<Typography variant="body2">
{t("Second")}:{" "}
{formatNumber(
(detail.secondStockTakeQty ?? 0) +
(detail.secondBadQty ?? 0)
)}{" "}
({detail.secondBadQty ?? 0}) ={" "}
{formatNumber(detail.secondStockTakeQty ?? 0)}
</Typography>
</Stack>
)}

{hasSecond && (
<Stack
direction="row"
spacing={1}
alignItems="center"
>
<Radio
size="small"
checked={selection === "approver"}
onChange={() =>
setQtySelection({
...qtySelection,
[detail.id]: "approver",
})
}
/>
<Typography variant="body2">
{t("Approver Input")}:
</Typography>
<TextField
size="small"
type="number"
value={approverQty[detail.id] || ""}
onChange={(e) =>
setApproverQty({
...approverQty,
[detail.id]: e.target.value,
})
}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Stock Take Qty")}
disabled={selection !== "approver"}
/>

<TextField
size="small"
type="number"
value={approverBadQty[detail.id] || ""}
onChange={(e) =>
setApproverBadQty({
...approverBadQty,
[detail.id]: e.target.value,
})
}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Bad Qty")}
disabled={selection !== "approver"}
/>
<Typography variant="body2">
={" "}
{formatNumber(
parseFloat(approverQty[detail.id] || "0") -
parseFloat(
approverBadQty[detail.id] || "0"
)
)}
</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>
)}
</TableCell>

<TableCell>
<Typography variant="body2">
{detail.remarks || "-"}
</Typography>
</TableCell>

<TableCell>
{detail.stockTakeRecordStatus === "completed" ? (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus)}
color="success"
/>
) : detail.stockTakeRecordStatus === "pass" ? (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus)}
color="default"
/>
) : detail.stockTakeRecordStatus === "notMatch" ? (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus)}
color="warning"
/>
) : (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus || "")}
color="default"
/>
)}
</TableCell>
<TableCell>
{detail.stockTakeRecordId &&
detail.stockTakeRecordStatus !== "notMatch" && (
<Box>
<Button
size="small"
variant="outlined"
color="warning"
onClick={() =>
handleUpdateStatusToNotMatch(detail)
}
disabled={
updatingStatus ||
detail.stockTakeRecordStatus === "completed"
}
>
{t("ReStockTake")}
</Button>
</Box>
)}
<br />
{detail.finalQty == null && (
<Box>
<Button
size="small"
variant="contained"
onClick={() => handleSaveApproverStockTake(detail)}
disabled={saving}
>
{t("Save")}
</Button>
</Box>
)}
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={total}
page={page}
onPageChange={handleChangePage}
rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
labelRowsPerPage={t("Rows per page")}
/>
</>
)}
</Box>
);
};

export default ApproverStockTakeAll;


+ 70
- 20
src/components/StockTakeManagement/StockTakeTab.tsx Näytä tiedosto

@@ -9,12 +9,17 @@ import ApproverCardList from "./ApproverCardList";
import PickerStockTake from "./PickerStockTake";
import PickerReStockTake from "./PickerReStockTake";
import ApproverStockTake from "./ApproverStockTake";
import ApproverAllCardList from "./ApproverAllCardList";
import ApproverStockTakeAll from "./ApproverStockTakeAll";

type ViewScope = "picker" | "approver-by-section" | "approver-all";

const StockTakeTab: React.FC = () => {
const { t } = useTranslation(["inventory", "common"]);
const [tabValue, setTabValue] = useState(0);
const [selectedSession, setSelectedSession] = useState<AllPickedStockTakeListReponse | null>(null);
const [viewMode, setViewMode] = useState<"details" | "reStockTake">("details");
const [viewScope, setViewScope] = useState<ViewScope>("picker");
const [snackbar, setSnackbar] = useState<{
open: boolean;
message: string;
@@ -30,9 +35,16 @@ const StockTakeTab: React.FC = () => {
setViewMode("details");
}, []);

const handleApproverAllCardClick = useCallback((session: AllPickedStockTakeListReponse) => {
setSelectedSession(session);
setViewMode("details");
setViewScope("approver-all");
}, []);

const handleReStockTakeClick = useCallback((session: AllPickedStockTakeListReponse) => {
setSelectedSession(session);
setViewMode("reStockTake");
setViewScope("picker");
}, []);

const handleBackToList = useCallback(() => {
@@ -51,27 +63,37 @@ const StockTakeTab: React.FC = () => {
if (selectedSession) {
return (
<Box>
{tabValue === 0 ? (
viewMode === "reStockTake" ? (
<PickerReStockTake
selectedSession={selectedSession}
onBack={handleBackToList}
onSnackbar={handleSnackbar}
/>
) : (
<PickerStockTake
selectedSession={selectedSession}
onBack={handleBackToList}
onSnackbar={handleSnackbar}
/>
)
) : (
{viewScope === "picker" && (
tabValue === 0 ? (
viewMode === "reStockTake" ? (
<PickerReStockTake
selectedSession={selectedSession}
onBack={handleBackToList}
onSnackbar={handleSnackbar}
/>
) : (
<PickerStockTake
selectedSession={selectedSession}
onBack={handleBackToList}
onSnackbar={handleSnackbar}
/>
)
) : null
)}
{viewScope === "approver-by-section" && tabValue === 1 && (
<ApproverStockTake
selectedSession={selectedSession}
onBack={handleBackToList}
onSnackbar={handleSnackbar}
/>
)}
{viewScope === "approver-all" && tabValue === 2 && (
<ApproverStockTakeAll
selectedSession={selectedSession}
onBack={handleBackToList}
onSnackbar={handleSnackbar}
/>
)}
<Snackbar
open={snackbar.open}
autoHideDuration={6000}
@@ -87,18 +109,46 @@ const StockTakeTab: React.FC = () => {

return (
<Box>
<Tabs value={tabValue} onChange={(e, newValue) => setTabValue(newValue)} sx={{ mb: 2 }}>
<Tabs
value={tabValue}
onChange={(e, newValue) => {
setTabValue(newValue);
if (newValue === 0) {
setViewScope("picker");
} else if (newValue === 1) {
setViewScope("approver-by-section");
} else {
setViewScope("approver-all");
}
}}
sx={{ mb: 2 }}
>
<Tab label={t("Picker")} />
<Tab label={t("Approver")} />
<Tab label={t("Approver All")} />
</Tabs>

{tabValue === 0 ? (
{tabValue === 0 && (
<PickerCardList
onCardClick={handleCardClick}
onCardClick={(session) => {
setViewScope("picker");
handleCardClick(session);
}}
onReStockTakeClick={handleReStockTakeClick}
/>
) : (
<ApproverCardList onCardClick={handleCardClick} />
)}
{tabValue === 1 && (
<ApproverCardList
onCardClick={(session) => {
setViewScope("approver-by-section");
handleCardClick(session);
}}
/>
)}
{tabValue === 2 && (
<ApproverAllCardList
onCardClick={handleApproverAllCardClick}
/>
)}

<Snackbar


+ 1
- 0
src/i18n/zh/inventory.json Näytä tiedosto

@@ -14,6 +14,7 @@
"Stock Take Round": "盤點輪次",
"ApproverAll": "審核員",
"Stock Take Section (can use , to search multiple sections)": "盤點區域(可使用逗號搜索多個區域)",
"Approver All": "審核員全部盤點",
"Variance %": "差異百分比",
"fg": "成品",
"Back to List": "返回列表",


Ladataan…
Peruuta
Tallenna