From b826baabe2e6c22bbfe8ffccd32fcdafbe6446f0 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 31 Mar 2026 18:42:36 +0800 Subject: [PATCH] update --- src/app/api/stockTake/actions.ts | 43 ++++- .../ApproverStockTakeAll.tsx | 148 ++++++++++++++++-- src/i18n/en/inventory.json | 4 +- src/i18n/zh/inventory.json | 4 +- 4 files changed, 185 insertions(+), 14 deletions(-) diff --git a/src/app/api/stockTake/actions.ts b/src/app/api/stockTake/actions.ts index bf9b425..2712b2c 100644 --- a/src/app/api/stockTake/actions.ts +++ b/src/app/api/stockTake/actions.ts @@ -135,10 +135,42 @@ export interface AllPickedStockTakeListReponse { reStockTakeTrueFalse: boolean; } +/** 與 Picker 列表一致:區域描述、盤點區域、貨品編號/名稱(逗號多關鍵字由後端解析) */ +export type ApproverInventoryLotDetailsQuery = { + itemCode?: string | null; + itemName?: string | null; + sectionDescription?: string | null; + stockTakeSections?: string | null; +}; + +function appendApproverInventoryLotQueryParams( + params: URLSearchParams, + query?: ApproverInventoryLotDetailsQuery | null +) { + if (!query) return; + if (query.itemCode != null && query.itemCode.trim() !== "") { + params.append("itemCode", query.itemCode.trim()); + } + if (query.itemName != null && query.itemName.trim() !== "") { + params.append("itemName", query.itemName.trim()); + } + if ( + query.sectionDescription != null && + query.sectionDescription !== "" && + query.sectionDescription !== "All" + ) { + params.append("sectionDescription", query.sectionDescription.trim()); + } + if (query.stockTakeSections != null && query.stockTakeSections.trim() !== "") { + params.append("stockTakeSections", query.stockTakeSections.trim()); + } +} + export const getApproverInventoryLotDetailsAll = async ( stockTakeId?: number | null, pageNum: number = 0, - pageSize: number = 100 + pageSize: number = 100, + query?: ApproverInventoryLotDetailsQuery | null ) => { const params = new URLSearchParams(); params.append("pageNum", String(pageNum)); @@ -146,6 +178,7 @@ export const getApproverInventoryLotDetailsAll = async ( if (stockTakeId != null && stockTakeId > 0) { params.append("stockTakeId", String(stockTakeId)); } + appendApproverInventoryLotQueryParams(params, query); const url = `${BASE_API_URL}/stockTakeRecord/approverInventoryLotDetailsAll?${params.toString()}`; const response = await serverFetchJson( @@ -159,7 +192,8 @@ export const getApproverInventoryLotDetailsAll = async ( export const getApproverInventoryLotDetailsAllPending = async ( stockTakeId?: number | null, pageNum: number = 0, - pageSize: number = 2147483647 + pageSize: number = 50, + query?: ApproverInventoryLotDetailsQuery | null ) => { const params = new URLSearchParams(); params.append("pageNum", String(pageNum)); @@ -167,6 +201,7 @@ export const getApproverInventoryLotDetailsAllPending = async ( if (stockTakeId != null && stockTakeId > 0) { params.append("stockTakeId", String(stockTakeId)); } + appendApproverInventoryLotQueryParams(params, query); const url = `${BASE_API_URL}/stockTakeRecord/approverInventoryLotDetailsAllPending?${params.toString()}`; const response = await serverFetchJson(url, { method: "GET" }); return normalizeApproverInventoryLotDetailsRes(response); @@ -174,7 +209,8 @@ export const getApproverInventoryLotDetailsAllPending = async ( export const getApproverInventoryLotDetailsAllApproved = async ( stockTakeId?: number | null, pageNum: number = 0, - pageSize: number = 50 + pageSize: number = 50, + query?: ApproverInventoryLotDetailsQuery | null ) => { const params = new URLSearchParams(); params.append("pageNum", String(pageNum)); @@ -182,6 +218,7 @@ export const getApproverInventoryLotDetailsAllApproved = async ( if (stockTakeId != null && stockTakeId > 0) { params.append("stockTakeId", String(stockTakeId)); } + appendApproverInventoryLotQueryParams(params, query); const url = `${BASE_API_URL}/stockTakeRecord/approverInventoryLotDetailsAllApproved?${params.toString()}`; const response = await serverFetchJson(url, { method: "GET" }); return normalizeApproverInventoryLotDetailsRes(response); diff --git a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx index 734649b..cd8f76e 100644 --- a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx +++ b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx @@ -31,7 +31,10 @@ import { BatchSaveApproverStockTakeAllRequest, batchSaveApproverStockTakeRecordsAll, updateStockTakeRecordStatusToNotMatch, + type ApproverInventoryLotDetailsQuery, } from "@/app/api/stockTake/actions"; +import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox"; +import { fetchStockTakeSections } from "@/app/api/warehouse/actions"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import dayjs from "dayjs"; @@ -53,6 +56,33 @@ type ApprovedSortKey = | "stockTakerName" | "variance"; +type ApproverSearchKey = "sectionDescription" | "stockTakeSession" | "itemCode" | "itemName"; + +type ApproverSearchFilters = { + sectionDescription: string; + stockTakeSession: string; + itemCode: string; + itemName: string; +}; + +function buildApproverInventoryQuery(filters: ApproverSearchFilters): ApproverInventoryLotDetailsQuery { + return { + sectionDescription: filters.sectionDescription !== "All" ? filters.sectionDescription : undefined, + stockTakeSections: filters.stockTakeSession.trim() ? filters.stockTakeSession.trim() : undefined, + itemCode: filters.itemCode.trim() ? filters.itemCode.trim() : undefined, + itemName: filters.itemName.trim() ? filters.itemName.trim() : undefined, + }; +} + +function hasAnyApproverSearchCriterion(f: ApproverSearchFilters): boolean { + return ( + (f.sectionDescription && f.sectionDescription !== "All") || + f.stockTakeSession.trim() !== "" || + f.itemCode.trim() !== "" || + f.itemName.trim() !== "" + ); +} + function parseDateTimeMs( v: string | string[] | null | undefined ): number { @@ -88,6 +118,10 @@ const ApproverStockTakeAll: React.FC = ({ const [total, setTotal] = useState(0); const [approvedSortKey, setApprovedSortKey] = useState(null); const [approvedSortDir, setApprovedSortDir] = useState<"asc" | "desc">("asc"); + const [sectionDescriptionAutocompleteOptions, setSectionDescriptionAutocompleteOptions] = useState< + { value: string; label: string }[] + >([]); + const [appliedFilters, setAppliedFilters] = useState(null); const currentUserId = session?.id ? parseInt(session.id) : undefined; @@ -108,8 +142,64 @@ const ApproverStockTakeAll: React.FC = ({ [] ); + const handleApproverSearchBoxSearch = useCallback( + (inputs: Record) => { + const next: ApproverSearchFilters = { + sectionDescription: inputs.sectionDescription || "All", + stockTakeSession: inputs.stockTakeSession || "", + itemCode: inputs.itemCode || "", + itemName: inputs.itemName || "", + }; + if (!hasAnyApproverSearchCriterion(next)) { + onSnackbar(t("Please set at least one search criterion"), "warning"); + return; + } + setAppliedFilters(next); + setPage(0); + }, + [onSnackbar, t] + ); + + const handleApproverSearchBoxReset = useCallback(() => { + setAppliedFilters(null); + setPage(0); + setInventoryLotDetails([]); + setTotal(0); + }, []); + + const approverSearchCriteria: Criterion[] = useMemo( + () => [ + { + type: "autocomplete", + label: t("Stock Take Section Description"), + paramName: "sectionDescription", + options: sectionDescriptionAutocompleteOptions, + needAll: true, + }, + { + type: "text", + label: t("Stock Take Section (can use , to search multiple sections)"), + paramName: "stockTakeSession", + placeholder: "", + }, + { + type: "text", + label: t("Item Code"), + paramName: "itemCode", + placeholder: "", + }, + { + type: "text", + label: t("Item Name"), + paramName: "itemName", + placeholder: "", + }, + ], + [t, sectionDescriptionAutocompleteOptions] + ); + const loadDetails = useCallback( - async (pageNum: number, size: number | string) => { + async (pageNum: number, size: number | string, filters: ApproverSearchFilters) => { setLoadingDetails(true); try { let actualSize: number; @@ -125,16 +215,19 @@ const ApproverStockTakeAll: React.FC = ({ actualSize = typeof size === "string" ? parseInt(size, 10) : size; } + const searchQuery: ApproverInventoryLotDetailsQuery = buildApproverInventoryQuery(filters); const response = mode === "approved" ? await getApproverInventoryLotDetailsAllApproved( selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, pageNum, - actualSize + actualSize, + searchQuery ) : await getApproverInventoryLotDetailsAllPending( selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, pageNum, - actualSize + actualSize, + searchQuery ); setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); setTotal(response.total || 0); @@ -150,14 +243,38 @@ const ApproverStockTakeAll: React.FC = ({ ); useEffect(() => { - loadDetails(page, pageSize); - }, [page, pageSize, loadDetails]); + if (!appliedFilters) { + return; + } + loadDetails(page, pageSize, appliedFilters); + }, [page, pageSize, appliedFilters, loadDetails]); - // 切换模式时,清空用户先前的选择与输入,approved 模式需要以后端结果为准。 + useEffect(() => { + fetchStockTakeSections() + .then((sections) => { + const descSet = new Set(); + sections.forEach((s) => { + const desc = s.stockTakeSectionDescription?.trim(); + if (desc) descSet.add(desc); + }); + setSectionDescriptionAutocompleteOptions( + Array.from(descSet).map((desc) => ({ value: desc, label: desc })) + ); + }) + .catch((e) => { + console.error("Failed to load section descriptions for approver search:", e); + }); + }, []); + + // 切换模式或盘次时,清空选择与搜索;approved 模式需要以后端结果为准。 useEffect(() => { setQtySelection({}); setApproverQty({}); setApproverBadQty({}); + setAppliedFilters(null); + setPage(0); + setInventoryLotDetails([]); + setTotal(0); }, [mode, selectedSession.stockTakeId]); useEffect(() => { @@ -530,7 +647,9 @@ const ApproverStockTakeAll: React.FC = ({ result.errorCount > 0 ? "warning" : "success" ); - await loadDetails(page, pageSize); + if (appliedFilters) { + await loadDetails(page, pageSize, appliedFilters); + } } catch (e: any) { console.error("handleBatchSubmitAll (all): Error:", e); let errorMessage = t("Failed to batch save approver stock take records"); @@ -549,7 +668,7 @@ const ApproverStockTakeAll: React.FC = ({ } finally { setBatchSaving(false); } - }, [selectedSession, currentUserId, variancePercentTolerance, t, onSnackbar, loadDetails, page, pageSize, mode]); + }, [selectedSession, currentUserId, variancePercentTolerance, t, onSnackbar, loadDetails, page, pageSize, mode, appliedFilters]); const formatNumber = (num: number | null | undefined): string => { if (num == null) return "0"; @@ -629,6 +748,15 @@ const ApproverStockTakeAll: React.FC = ({ )} + + + + criteria={approverSearchCriteria} + onSearch={handleApproverSearchBoxSearch} + onReset={handleApproverSearchBoxReset} + /> + + {loadingDetails ? ( @@ -754,7 +882,9 @@ const ApproverStockTakeAll: React.FC = ({ - {t("No data")} + {!appliedFilters + ? t("Approver search empty hint") + : t("No data")} diff --git a/src/i18n/en/inventory.json b/src/i18n/en/inventory.json index 07041bf..0c57fb6 100644 --- a/src/i18n/en/inventory.json +++ b/src/i18n/en/inventory.json @@ -3,5 +3,7 @@ "Latest market unit price": "Latest market unit price", "Add entry for items without inventory": "Add entry for items without inventory", "Enter item code or name to search": "Enter item code or name to search", - "Current Stock": "Current Stock" + "Current Stock": "Current Stock", + "Please set at least one search criterion": "Please set at least one search criterion", + "Approver search empty hint": "Set search criteria and click Search" } \ No newline at end of file diff --git a/src/i18n/zh/inventory.json b/src/i18n/zh/inventory.json index 80a4ab6..337fcf3 100644 --- a/src/i18n/zh/inventory.json +++ b/src/i18n/zh/inventory.json @@ -281,6 +281,8 @@ "Search lot by QR code": "尋找批次(掃描二維碼)", "Please scan...": "請掃描...", "Stop QR Scan": "停止掃碼", - "No Data": "沒有數據" + "No Data": "沒有數據", + "Please set at least one search criterion": "請至少設定一項搜索條件", + "Approver search empty hint": "請設定搜索條件後點擊搜索" }