diff --git a/src/app/api/stockTake/actions.ts b/src/app/api/stockTake/actions.ts index 98f3233..676bdc3 100644 --- a/src/app/api/stockTake/actions.ts +++ b/src/app/api/stockTake/actions.ts @@ -44,6 +44,10 @@ export interface InventoryLotDetailResponse { stockTakeSection?: string | null; stockTakeSectionDescription?: string | null; stockTakerName?: string | null; + /** ISO string or backend LocalDateTime array */ + stockTakeEndTime?: string | string[] | null; + /** ISO string or backend LocalDateTime array */ + approverTime?: string | string[] | null; } export const getInventoryLotDetailsBySection = async ( diff --git a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx index 8b59227..c754d5e 100644 --- a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx +++ b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx @@ -17,6 +17,7 @@ import { TextField, Radio, TablePagination, + TableSortLabel, } from "@mui/material"; import { useState, useCallback, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; @@ -45,6 +46,25 @@ interface ApproverStockTakeAllProps { type QtySelectionType = "first" | "second" | "approver"; +type ApprovedSortKey = + | "stockTakeEndTime" + | "stockTakeSection" + | "item" + | "stockTakerName" + | "variance"; + +function parseDateTimeMs( + v: string | string[] | null | undefined +): number { + if (v == null) return 0; + if (Array.isArray(v)) { + const arr = v as unknown as number[]; + const [y, m, d, h = 0, min = 0, s = 0] = arr; + return dayjs(`${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")} ${h}:${min}:${s}`).valueOf(); + } + return dayjs(v as string).valueOf(); +} + const ApproverStockTakeAll: React.FC = ({ selectedSession, mode, @@ -66,6 +86,8 @@ const ApproverStockTakeAll: React.FC = ({ const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(50); const [total, setTotal] = useState(0); + const [approvedSortKey, setApprovedSortKey] = useState(null); + const [approvedSortDir, setApprovedSortDir] = useState<"asc" | "desc">("asc"); const currentUserId = session?.id ? parseInt(session.id) : undefined; @@ -200,12 +222,62 @@ const ApproverStockTakeAll: React.FC = ({ ]); const sortedDetails = useMemo(() => { - return [...filteredDetails].sort((a, b) => { - const sectionA = (a.stockTakeSection || "").trim(); - const sectionB = (b.stockTakeSection || "").trim(); - return sectionA.localeCompare(sectionB, undefined, { numeric: true, sensitivity: "base" }); + const list = [...filteredDetails]; + if (mode !== "approved") { + return list.sort((a, b) => + (a.stockTakeSection || "").localeCompare(b.stockTakeSection || "", undefined, { + numeric: true, + sensitivity: "base", + }) + ); + } + const key = approvedSortKey ?? "stockTakeSection"; + const mul = approvedSortDir === "asc" ? 1 : -1; + return list.sort((a, b) => { + let cmp = 0; + switch (key) { + case "stockTakeEndTime": + cmp = + parseDateTimeMs(a.approverTime ?? a.stockTakeEndTime) - + parseDateTimeMs(b.approverTime ?? b.stockTakeEndTime); + break; + case "stockTakeSection": + cmp = (a.stockTakeSection || "").localeCompare(b.stockTakeSection || "", undefined, { + numeric: true, + sensitivity: "base", + }); + break; + case "item": + cmp = `${a.itemCode || ""} ${a.itemName || ""}`.localeCompare( + `${b.itemCode || ""} ${b.itemName || ""}`, + undefined, + { numeric: true, sensitivity: "base" } + ); + break; + case "stockTakerName": + cmp = (a.stockTakerName || "").localeCompare(b.stockTakerName || "", undefined, { + numeric: true, + sensitivity: "base", + }); + break; + case "variance": + cmp = Number(a.varianceQty ?? 0) - Number(b.varianceQty ?? 0); + break; + default: + cmp = 0; + } + return cmp * mul; }); - }, [filteredDetails]); + }, [filteredDetails, mode, approvedSortKey, approvedSortDir]); + + const handleApprovedSort = useCallback((property: ApprovedSortKey) => { + if (approvedSortKey === property) { + setApprovedSortDir((d) => (d === "asc" ? "desc" : "asc")); + } else { + setApprovedSortKey(property); + setApprovedSortDir("asc"); + } + }, [approvedSortKey]); const handleSaveApproverStockTake = useCallback( async (detail: InventoryLotDetailResponse) => { @@ -410,6 +482,12 @@ const ApproverStockTakeAll: React.FC = ({ [inventoryLotDetails] ); + const formatRecordEndTime = (detail: InventoryLotDetailResponse) => { + const ms = parseDateTimeMs(detail.approverTime ?? detail.stockTakeEndTime); + if (!ms) return "-"; + return dayjs(ms).format("YYYY-MM-DD HH:mm"); + }; + return ( {onBack && ( @@ -482,23 +560,110 @@ const ApproverStockTakeAll: React.FC = ({ - {t("Warehouse Location")} - {t("Item-lotNo-ExpiryDate")} + {mode === "approved" && ( + + handleApprovedSort("stockTakeEndTime")} + > + {t("Approver Time")} + + + )} + + {mode === "approved" ? ( + handleApprovedSort("stockTakeSection")} + > + {t("Warehouse Location")} + + ) : ( + t("Warehouse Location") + )} + + + {mode === "approved" ? ( + handleApprovedSort("item")} + > + {t("Item-lotNo-ExpiryDate")} + + ) : ( + t("Item-lotNo-ExpiryDate") + )} + {t("UOM")} {t("Stock Take Qty(include Bad Qty)= Available Qty")} - + {mode === "approved" && ( + + handleApprovedSort("variance")} + > + {t("Variance")} + + + )} {t("Remark")} {t("Record Status")} - {t("Picker")} + + {mode === "approved" ? ( + handleApprovedSort("stockTakerName")} + > + {t("Picker")} + + ) : ( + t("Picker") + )} + {t("Action")} {sortedDetails.length === 0 ? ( - + {t("No data")} @@ -515,6 +680,16 @@ const ApproverStockTakeAll: React.FC = ({ return ( + {mode === "approved" && ( + + + + + {formatRecordEndTime(detail)} + + + + )} {detail.stockTakeSection || "-"} {detail.stockTakeSectionDescription || "-"} @@ -759,6 +934,14 @@ const ApproverStockTakeAll: React.FC = ({ )} + {mode === "approved" && ( + + + {formatNumber(detail.varianceQty)} + + + )} + {detail.remarks || "-"} diff --git a/src/components/StockTakeManagement/PickerCardList.tsx b/src/components/StockTakeManagement/PickerCardList.tsx index f1a766d..06980d8 100644 --- a/src/components/StockTakeManagement/PickerCardList.tsx +++ b/src/components/StockTakeManagement/PickerCardList.tsx @@ -37,20 +37,30 @@ import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; const PER_PAGE = 6; interface PickerCardListProps { + /** 由父層保存,從明細返回時仍回到同一頁 */ + page: number; + pageSize: number; + onListPageChange: (page: number) => void; onCardClick: (session: AllPickedStockTakeListReponse) => void; onReStockTakeClick: (session: AllPickedStockTakeListReponse) => void; } -const PickerCardList: React.FC = ({ onCardClick, onReStockTakeClick }) => { +const PickerCardList: React.FC = ({ + page, + pageSize, + onListPageChange, + onCardClick, + onReStockTakeClick, +}) => { const { t } = useTranslation(["inventory", "common"]); dayjs.extend(duration); const PER_PAGE = 6; const [loading, setLoading] = useState(false); const [stockTakeSessions, setStockTakeSessions] = useState([]); - const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState(6); // 每页 6 条 -const [total, setTotal] = useState(0); + const [total, setTotal] = useState(0); + /** 建立盤點後若仍在 page 0,仍強制重新載入 */ + const [listRefreshNonce, setListRefreshNonce] = useState(0); const [creating, setCreating] = useState(false); const [openConfirmDialog, setOpenConfirmDialog] = useState(false); const [filterSectionDescription, setFilterSectionDescription] = useState("All"); @@ -106,41 +116,40 @@ const criteria: Criterion[] = [ const handleSearch = (inputs: Record) => { setFilterSectionDescription(inputs.sectionDescription || "All"); setFilterStockTakeSession(inputs.stockTakeSession || ""); - fetchStockTakeSessions(0, pageSize, { - sectionDescription: inputs.sectionDescription || "All", - stockTakeSections: inputs.stockTakeSession ?? "", - }); + onListPageChange(0); }; const handleResetSearch = () => { setFilterSectionDescription("All"); setFilterStockTakeSession(""); - fetchStockTakeSessions(0, pageSize, { - sectionDescription: "All", - stockTakeSections: "", - }); + onListPageChange(0); }; - const fetchStockTakeSessions = useCallback( - async (pageNum: number, size: number, filterOverrides?: { sectionDescription: string; stockTakeSections: string }) => { - setLoading(true); - try { - const res = await getStockTakeRecordsPaged(pageNum, size, filterOverrides); + + useEffect(() => { + let cancelled = false; + setLoading(true); + getStockTakeRecordsPaged(page, pageSize, { + sectionDescription: filterSectionDescription, + stockTakeSections: filterStockTakeSession, + }) + .then((res) => { + if (cancelled) return; setStockTakeSessions(Array.isArray(res.records) ? res.records : []); setTotal(res.total || 0); - setPage(pageNum); - } catch (e) { + }) + .catch((e) => { console.error(e); - setStockTakeSessions([]); - setTotal(0); - } finally { - setLoading(false); - } - }, - [] - ); - - useEffect(() => { - fetchStockTakeSessions(0, pageSize); - }, [fetchStockTakeSessions, pageSize]); + if (!cancelled) { + setStockTakeSessions([]); + setTotal(0); + } + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, [page, pageSize, filterSectionDescription, filterStockTakeSession, listRefreshNonce]); //const startIdx = page * PER_PAGE; //const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE); @@ -161,13 +170,14 @@ const handleResetSearch = () => { console.log(message); - await fetchStockTakeSessions(0, pageSize); + onListPageChange(0); + setListRefreshNonce((n) => n + 1); } catch (e) { console.error(e); } finally { setCreating(false); } - }, [fetchStockTakeSessions, t]); + }, [onListPageChange, t]); useEffect(() => { fetchStockTakeSections() .then((sections) => { @@ -376,7 +386,7 @@ const handleResetSearch = () => { page={page} rowsPerPage={pageSize} onPageChange={(e, newPage) => { - fetchStockTakeSessions(newPage, pageSize); + onListPageChange(newPage); }} rowsPerPageOptions={[pageSize]} // 如果暂时不让用户改 pageSize,就写死 /> diff --git a/src/components/StockTakeManagement/PickerReStockTake.tsx b/src/components/StockTakeManagement/PickerReStockTake.tsx index f8353e1..896b67f 100644 --- a/src/components/StockTakeManagement/PickerReStockTake.tsx +++ b/src/components/StockTakeManagement/PickerReStockTake.tsx @@ -599,13 +599,13 @@ const PickerReStockTake: React.FC = ({ { - const clean = sanitizeIntegerInput(e.target.value); + // const clean = sanitizeIntegerInput(e.target.value); setRecordInputs(prev => ({ ...prev, - [detail.id]: { ...(prev[detail.id] ?? defaultInputs), remark: clean } + [detail.id]: { ...(prev[detail.id] ?? defaultInputs), remark: e.target.value } })); }} sx={{ width: 150 }} diff --git a/src/components/StockTakeManagement/PickerStockTake.tsx b/src/components/StockTakeManagement/PickerStockTake.tsx index 4050919..53bd8c0 100644 --- a/src/components/StockTakeManagement/PickerStockTake.tsx +++ b/src/components/StockTakeManagement/PickerStockTake.tsx @@ -771,15 +771,15 @@ const PickerStockTake: React.FC = ({ { - const clean = sanitizeIntegerInput(e.target.value); + // const clean = sanitizeIntegerInput(e.target.value); setRecordInputs(prev => ({ ...prev, [detail.id]: { ...(prev[detail.id] ?? { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" }), - remark: clean + remark: e.target.value } })); }} diff --git a/src/components/StockTakeManagement/StockTakeTab.tsx b/src/components/StockTakeManagement/StockTakeTab.tsx index fb371f5..bbb128b 100644 --- a/src/components/StockTakeManagement/StockTakeTab.tsx +++ b/src/components/StockTakeManagement/StockTakeTab.tsx @@ -19,6 +19,9 @@ const StockTakeTab: React.FC = () => { const [viewScope, setViewScope] = useState("picker"); const [approverSession, setApproverSession] = useState(null); const [approverLoading, setApproverLoading] = useState(false); + /** 從卡片列表進入明細後返回時保留分頁 */ + const [pickerListPage, setPickerListPage] = useState(0); + const [pickerListPageSize] = useState(6); const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; @@ -120,7 +123,10 @@ const StockTakeTab: React.FC = () => { {tabValue === 0 && ( - { setViewScope("picker"); handleCardClick(session); diff --git a/src/i18n/zh/inventory.json b/src/i18n/zh/inventory.json index 554090c..40bb45c 100644 --- a/src/i18n/zh/inventory.json +++ b/src/i18n/zh/inventory.json @@ -8,6 +8,7 @@ "UoM": "單位", "Approver Pending": "審核待處理", "Approver Approved": "審核通過", + "Approver Time": "審核時間", "mat": "物料", "variance": "差異", "Plan Start Date": "計劃開始日期",