From 16d1d0f1930c85c63e931c010f97bd271543e439 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Wed, 29 Apr 2026 22:06:06 +0800 Subject: [PATCH] update stock take and stock take report --- src/app/(main)/report/page.tsx | 8 + src/app/api/stockTake/actions.ts | 14 +- src/app/api/warehouse/index.ts | 1 + .../JoWorkbench/newJobPickExecution.tsx | 94 ------ .../StockTakeManagement/ApproverStockTake.tsx | 2 +- .../ApproverStockTakeAll.tsx | 280 ++++++++++++------ .../StockTakeManagement/PickerCardList.tsx | 260 +++++++++++----- .../StockTakeManagement/PickerReStockTake.tsx | 10 +- .../StockTakeManagement/PickerStockTake.tsx | 25 +- src/config/reportConfig.ts | 28 +- src/i18n/zh/common.json | 8 +- src/i18n/zh/inventory.json | 11 +- 12 files changed, 442 insertions(+), 299 deletions(-) diff --git a/src/app/(main)/report/page.tsx b/src/app/(main)/report/page.tsx index 95540de..b9add6a 100644 --- a/src/app/(main)/report/page.tsx +++ b/src/app/(main)/report/page.tsx @@ -141,6 +141,14 @@ export default function ReportPage() { } // Clear dynamic options when report changes setDynamicOptions({}); + + // Default "All" (no filter) for stock take variance report conditions. + if (selectedReportId === 'rep-012') { + setCriteria({ + store_id: 'All', + status: 'All', + }); + } }, [selectedReportId]); // React 18 Strict Mode (dev) mounts → unmounts → remounts, so effects with [] run twice. diff --git a/src/app/api/stockTake/actions.ts b/src/app/api/stockTake/actions.ts index 22399d7..3b09bf5 100644 --- a/src/app/api/stockTake/actions.ts +++ b/src/app/api/stockTake/actions.ts @@ -29,6 +29,7 @@ export interface InventoryLotDetailResponse { warehouseSlot: string; warehouseArea: string; warehouse: string; + storeId?: string | null; varianceQty: number | null; status: string; remarks: string | null; @@ -137,6 +138,8 @@ export interface AllPickedStockTakeListReponse { endTime: string | null; planStartDate: string | null; stockTakeSectionDescription: string | null; + warehouseArea: string | null; + storeId: string | null; reStockTakeTrueFalse: boolean; } @@ -254,7 +257,7 @@ export const getStockTakeRecords = async () => { export const getStockTakeRecordsPaged = async ( pageNum: number, pageSize: number, - params?: { sectionDescription?: string; stockTakeSections?: string } + params?: { sectionDescription?: string; stockTakeSections?: string; status?: string; area?: string; storeId?: string } ) => { const searchParams = new URLSearchParams(); searchParams.set("pageNum", String(pageNum)); @@ -265,6 +268,15 @@ export const getStockTakeRecordsPaged = async ( if (params?.stockTakeSections?.trim()) { searchParams.set("stockTakeSections", params.stockTakeSections.trim()); } + if (params?.status && params.status !== "All") { + searchParams.set("status", params.status); + } + if (params?.area?.trim()) { + searchParams.set("area", params.area.trim()); + } + if (params?.storeId && params.storeId !== "All") { + searchParams.set("storeId", params.storeId); + } const url = `${BASE_API_URL}/stockTakeRecord/AllPickedStockOutRecordList?${searchParams.toString()}`; const res = await serverFetchJson>(url, { method: "GET" }); return res; diff --git a/src/app/api/warehouse/index.ts b/src/app/api/warehouse/index.ts index 705e52e..cc1b185 100644 --- a/src/app/api/warehouse/index.ts +++ b/src/app/api/warehouse/index.ts @@ -39,5 +39,6 @@ export interface StockTakeSectionInfo { id: string; stockTakeSection: string; stockTakeSectionDescription: string | null; + storeId?: string | null; warehouseCount: number; } \ No newline at end of file diff --git a/src/components/JoWorkbench/newJobPickExecution.tsx b/src/components/JoWorkbench/newJobPickExecution.tsx index 48d0f48..92fa403 100644 --- a/src/components/JoWorkbench/newJobPickExecution.tsx +++ b/src/components/JoWorkbench/newJobPickExecution.tsx @@ -44,9 +44,6 @@ import { checkAndCompletePickOrderByConsoCode, confirmLotSubstitution, updateStockOutLineStatusByQRCodeAndLotNo, // ✅ 添加 - batchSubmitList, // ✅ 添加 - batchSubmitListRequest, // ✅ 添加 - batchSubmitListLineRequest, } from "@/app/api/pickOrder/actions"; // 修改:使用 Job Order API import { @@ -3783,92 +3780,6 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { }); } - // Hold-only:與整批相同規則,只送一筆 batch,把實揈/完成狀態寫回 DB - if (useHoldOnlyApi && pickOrderIdEarly && solId > 0) { - const freshData = - await fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench(pickOrderIdEarly); - const flatLots = getAllLotsFromHierarchical(freshData); - const lotRow = flatLots.find( - (l: any) => Number(l.stockOutLineId) === solId, - ); - if (!lotRow) { - throw new Error( - "Could not find lot row after refresh for batch submit", - ); - } - - const requiredQty = Number( - lotRow.requiredQty || lotRow.pickOrderLineRequiredQty || 0, - ); - const issuePickedVal = picked; - const currentActualPickQty = Number( - issuePickedVal ?? lotRow.actualPickQty ?? 0, - ); - const onlyComplete = - lotRow.stockOutLineStatus === "partially_completed" || - lotRow.stockOutLineStatus === "PARTIALLY_COMPLETE" || - issuePickedVal !== undefined; - const expired = isLotAvailabilityExpired(lotRow); - const unavailable = isInventoryLotLineUnavailable(lotRow); - - let targetActual: number; - let newStatus: string; - - if (unavailable) { - targetActual = currentActualPickQty; - newStatus = "completed"; - } else if (expired && issuePickedVal === undefined) { - targetActual = 0; - newStatus = "completed"; - } else if (onlyComplete) { - targetActual = currentActualPickQty; - newStatus = "completed"; - } else { - const remainingQty = Math.max( - 0, - requiredQty - currentActualPickQty, - ); - const cumulativeQty = currentActualPickQty + remainingQty; - targetActual = cumulativeQty; - newStatus = "partially_completed"; - if (requiredQty > 0 && cumulativeQty >= requiredQty) { - newStatus = "completed"; - } - } - - const line: batchSubmitListLineRequest = { - stockOutLineId: solId, - pickOrderLineId: Number(lotRow.pickOrderLineId), - inventoryLotLineId: lotRow.lotId ? Number(lotRow.lotId) : null, - requiredQty, - actualPickQty: targetActual, - stockOutLineStatus: newStatus, - pickOrderConsoCode: String(lotRow.pickOrderConsoCode || ""), - noLot: Boolean(lotRow.noLot === true), - }; - - const batchResult = await batchSubmitList({ - userId: currentUserId || 0, - lines: [line], - }); - - if (!batchResult || batchResult.code !== "SUCCESS") { - throw new Error( - batchResult?.message || - "Batch submit failed after hold adjustment", - ); - } - - const conso = String(lotRow.pickOrderConsoCode || "").trim(); - if (conso) { - try { - await checkAndCompletePickOrderByConsoCode(conso); - } catch (e) { - console.error("❌ completion check after single batch:", e); - } - } - } - setPickExecutionFormOpen(false); setSelectedLotForExecutionForm(null); @@ -3880,16 +3791,11 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { }, [ fetchJobOrderData, - getAllLotsFromHierarchical, currentUserId, selectedLotForExecutionForm, updateHandledBy, filterArgs, session?.user?.name, - batchSubmitList, - checkAndCompletePickOrderByConsoCode, - isLotAvailabilityExpired, - isInventoryLotLineUnavailable, ], ); // Calculate remaining required quantity diff --git a/src/components/StockTakeManagement/ApproverStockTake.tsx b/src/components/StockTakeManagement/ApproverStockTake.tsx index f677f73..700fd13 100644 --- a/src/components/StockTakeManagement/ApproverStockTake.tsx +++ b/src/components/StockTakeManagement/ApproverStockTake.tsx @@ -689,7 +689,7 @@ const ApproverStockTake: React.FC = ({ variant="outlined" color="warning" onClick={() => handleUpdateStatusToNotMatch(detail)} - disabled={updatingStatus || detail.stockTakeRecordStatus === "completed"} + disabled={updatingStatus || detail.stockTakeRecordStatus === "completed"||hasSecond} > {t("ReStockTake")} diff --git a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx index d10f1c4..16d9be8 100644 --- a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx +++ b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx @@ -18,6 +18,15 @@ import { Radio, TablePagination, TableSortLabel, + Card, + CardContent, + CardActions, + Grid, + FormControl, + InputLabel, + Select, + MenuItem, + Autocomplete, } from "@mui/material"; import { useState, useCallback, useEffect, useMemo } from "react"; import { Collapse } from "@mui/material"; @@ -37,7 +46,6 @@ import { 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"; @@ -66,13 +74,12 @@ type ApprovedSortKey = | "stockTakerName" | "variance"; -type ApproverSearchKey = "sectionDescription" | "stockTakeSession" | "itemKeyword" | "warehouseKeyword"|"status"; - type ApproverSearchFilters = { sectionDescription: string; stockTakeSession: string; itemKeyword: string; warehouseKeyword: string; + storeId: string; status: string; }; @@ -85,15 +92,6 @@ function buildApproverInventoryQuery(filters: ApproverSearchFilters): ApproverIn }; } -function hasAnyApproverSearchCriterion(f: ApproverSearchFilters): boolean { - return ( - (f.sectionDescription && f.sectionDescription !== "All") || - f.stockTakeSession.trim() !== "" || - f.itemKeyword.trim() !== "" || - f.warehouseKeyword.trim() !== "" - ); -} - function isBlankApproverField(value: string | undefined): boolean { return value == null || String(value).trim() === ""; } @@ -202,9 +200,15 @@ 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 [sectionDescriptionOptions, setSectionDescriptionOptions] = useState([]); + const [stockTakeSectionOptions, setStockTakeSectionOptions] = useState([]); + const [storeIdOptions, setStoreIdOptions] = useState(["2F", "4F"]); + const [searchSectionDescription, setSearchSectionDescription] = useState("All"); + const [searchStockTakeSession, setSearchStockTakeSession] = useState(""); + const [searchItemKeyword, setSearchItemKeyword] = useState(""); + const [searchWarehouseKeyword, setSearchWarehouseKeyword] = useState(""); + const [searchStoreId, setSearchStoreId] = useState("All"); + const [searchStatus, setSearchStatus] = useState(mode === "pending" ? "pass" : "All"); const [showFilters, setShowFilters] = useState(true) const [appliedFilters, setAppliedFilters] = useState(null); @@ -227,76 +231,32 @@ const ApproverStockTakeAll: React.FC = ({ [] ); - const handleApproverSearchBoxSearch = useCallback( - (inputs: Record) => { - const next: ApproverSearchFilters = { - sectionDescription: inputs.sectionDescription || "All", - stockTakeSession: inputs.stockTakeSession || "", - itemKeyword: inputs.itemKeyword || "", - warehouseKeyword: inputs.warehouseKeyword || "", - status: inputs.status || "All", - }; - /* - if (!hasAnyApproverSearchCriterion(next)) { - onSnackbar(t("Please set at least one search criterion"), "warning"); - return; - } - */ - setAppliedFilters(next); - setPage(0); - }, - [onSnackbar, t] - ); - - const handleApproverSearchBoxReset = useCallback(() => { + const handleSearch = useCallback(() => { + const next: ApproverSearchFilters = { + sectionDescription: searchSectionDescription || "All", + stockTakeSession: searchStockTakeSession || "", + itemKeyword: searchItemKeyword || "", + warehouseKeyword: searchWarehouseKeyword || "", + storeId: searchStoreId || "All", + status: mode === "pending" ? (searchStatus || "pass") : "All", + }; + setAppliedFilters(next); + setPage(0); + }, [searchSectionDescription, searchStockTakeSession, searchItemKeyword, searchWarehouseKeyword, searchStoreId, searchStatus, mode]); + + const handleResetSearch = useCallback(() => { + const defaultStatus = mode === "pending" ? "pass" : "All"; + setSearchSectionDescription("All"); + setSearchStockTakeSession(""); + setSearchItemKeyword(""); + setSearchWarehouseKeyword(""); + setSearchStoreId("All"); + setSearchStatus(defaultStatus); 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"), - paramName: "itemKeyword", - placeholder: "", - }, - - { - type: "text", - label: t("Warehouse"), - paramName: "warehouseKeyword", - placeholder: "", - }, - { - type: "select-labelled", - label: t("Record Status"), - paramName: "status", - options: [ - { label: t("All"), value: "All" }, - { label: t("Pending"), value: "pending" }, - { label: t("Not Match"), value: "notMatch" }, - { label: t("Pass"), value: "pass" }, // UI=Pass,值=completed - ], - } - ], - [t, sectionDescriptionAutocompleteOptions] - ); + }, [mode]); const loadDetails = useCallback(async (filters: ApproverSearchFilters) => { setLoadingDetails(true); @@ -342,13 +302,19 @@ const ApproverStockTakeAll: React.FC = ({ fetchStockTakeSections() .then((sections) => { const descSet = new Set(); + const sectionSet = new Set(); + const storeSet = new Set(["2F", "4F"]); sections.forEach((s) => { + const section = s.stockTakeSection?.trim(); + if (section) sectionSet.add(section); const desc = s.stockTakeSectionDescription?.trim(); if (desc) descSet.add(desc); + const storeId = s.storeId?.trim(); + if (storeId) storeSet.add(storeId); }); - setSectionDescriptionAutocompleteOptions( - Array.from(descSet).map((desc) => ({ value: desc, label: desc })) - ); + setStockTakeSectionOptions(Array.from(sectionSet).sort((a, b) => a.localeCompare(b))); + setSectionDescriptionOptions(Array.from(descSet).sort((a, b) => a.localeCompare(b))); + setStoreIdOptions(Array.from(storeSet).sort((a, b) => a.localeCompare(b))); }) .catch((e) => { console.error("Failed to load section descriptions for approver search:", e); @@ -361,6 +327,7 @@ const ApproverStockTakeAll: React.FC = ({ setApproverQty({}); setApproverBadQty({}); setAppliedFilters(null); + setSearchStatus(mode === "pending" ? "pass" : "All"); setPage(0); setInventoryLotDetails([]); setTotal(0); @@ -491,8 +458,14 @@ const ApproverStockTakeAll: React.FC = ({ const percent = parseFloat(variancePercentTolerance || "0"); const thresholdPercent = isNaN(percent) || percent < 0 ? 0 : percent; - const statusFilter = appliedFilters?.status ?? "All"; + const statusFilter = mode === "pending" ? (appliedFilters?.status ?? "pass") : "All"; + const storeIdFilter = appliedFilters?.storeId ?? "All"; return inventoryLotDetails.filter((detail) => { + if (storeIdFilter !== "All") { + if ((detail.storeId || "").trim().toLowerCase() !== storeIdFilter.trim().toLowerCase()) { + return false; + } + } if (statusFilter !== "All") { const rowStatus = normalizeStatus(detail.stockTakeRecordStatus); const wanted = normalizeStatus(statusFilter); @@ -526,6 +499,8 @@ const ApproverStockTakeAll: React.FC = ({ variancePercentTolerance, qtySelection, calculateDifference, + appliedFilters, + mode, ]); const sortedDetails = useMemo(() => { @@ -890,7 +865,7 @@ const ApproverStockTakeAll: React.FC = ({ { field: "qtyBlock", headerName: t("Stock Take Qty(include Bad Qty)= Available Qty"), - minWidth: 420, + minWidth: 320, flex: 3, sortable: false, renderCell: (params: GridRenderCellParams) => { @@ -949,7 +924,8 @@ const ApproverStockTakeAll: React.FC = ({ {formatNumber( (detail.firstStockTakeQty ?? 0) + (detail.firstBadQty ?? 0) )}{" "} - ({detail.firstBadQty ?? 0}) ={" "} + {/* ({detail.firstBadQty ?? 0}) */} + ={" "} {formatNumber(detail.firstStockTakeQty ?? 0)} @@ -974,7 +950,8 @@ const ApproverStockTakeAll: React.FC = ({ (detail.secondStockTakeQty ?? 0) + (detail.secondBadQty ?? 0) )}{" "} - ({detail.secondBadQty ?? 0}) ={" "} + {/* ({detail.secondBadQty ?? 0}) */} + ={" "} {formatNumber(detail.secondStockTakeQty ?? 0)} @@ -1013,6 +990,7 @@ const ApproverStockTakeAll: React.FC = ({ inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} /> + {/* = ({ disabled={mode === "approved" || selection !== "approver"} inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} /> + */ + } = {formatNumber(approverGoodQty)} @@ -1124,8 +1104,7 @@ const ApproverStockTakeAll: React.FC = ({ sortable: false, renderCell: (params) => { const detail = params.row; - const hasSecond = - detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0; + const hasSecond = detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0; const selection = qtySelection[detail.id] || (hasSecond ? "second" : "first"); @@ -1149,7 +1128,7 @@ const ApproverStockTakeAll: React.FC = ({ variant="outlined" color="warning" onClick={() => handleUpdateStatusToNotMatch(detail)} - disabled={updatingStatus} + disabled={updatingStatus||hasSecond} > {t("Not Match")} @@ -1189,6 +1168,25 @@ const ApproverStockTakeAll: React.FC = ({ ]); const effectivePageSize = pageSize === "all" ? Math.max(total || 0, 0) : (pageSize as number); + useEffect(() => { + setStockTakeSectionOptions((prev) => { + const sectionSet = new Set(prev); + inventoryLotDetails.forEach((item) => { + const section = item.stockTakeSection?.trim(); + if (section) sectionSet.add(section); + }); + return Array.from(sectionSet).sort((a, b) => a.localeCompare(b)); + }); + setStoreIdOptions((prev) => { + const storeSet = new Set([...prev, "2F", "4F"]); + inventoryLotDetails.forEach((item) => { + const storeId = item.storeId?.trim(); + if (storeId) storeSet.add(storeId); + }); + return Array.from(storeSet).sort((a, b) => a.localeCompare(b)); + }); + }, [inventoryLotDetails]); + return ( {onBack && ( @@ -1260,11 +1258,101 @@ const ApproverStockTakeAll: React.FC = ({ - - criteria={approverSearchCriteria} - onSearch={handleApproverSearchBoxSearch} - onReset={handleApproverSearchBoxReset} - /> + + + + + + {t("Stock Take Section")} + + + + + setSearchStockTakeSession(newValue)} + renderInput={(params) => ( + + )} + /> + + + setSearchItemKeyword(e.target.value)} + /> + + + setSearchWarehouseKeyword(e.target.value)} + /> + + + + {t("Store ID")} + + + + {mode === "pending" && ( + + + {t("Record Status")} + + + + )} + + + + + + + diff --git a/src/components/StockTakeManagement/PickerCardList.tsx b/src/components/StockTakeManagement/PickerCardList.tsx index 4559c0b..b24a629 100644 --- a/src/components/StockTakeManagement/PickerCardList.tsx +++ b/src/components/StockTakeManagement/PickerCardList.tsx @@ -12,21 +12,24 @@ import { CircularProgress, TablePagination, Grid, - LinearProgress, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, + TextField, + FormControl, + InputLabel, + Select, + MenuItem, + Autocomplete, } from "@mui/material"; import { SessionWithTokens } from "@/config/authConfig"; import { useSession } from "next-auth/react"; -import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox"; import { useState, useCallback, useEffect } from "react"; import { useTranslation } from "react-i18next"; import duration from "dayjs/plugin/duration"; import { - getStockTakeRecords, AllPickedStockTakeListReponse, createStockTakeForSections, getStockTakeRecordsPaged, @@ -36,7 +39,6 @@ import { fetchStockTakeSections } from "@/app/api/warehouse/actions"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import { AUTH } from "@/authorities"; -const PER_PAGE = 6; interface PickerCardListProps { /** 由父層保存,從明細返回時仍回到同一頁 */ @@ -57,7 +59,6 @@ const PickerCardList: React.FC = ({ const { t } = useTranslation(["inventory", "common"]); dayjs.extend(duration); - const PER_PAGE = 6; const [loading, setLoading] = useState(false); const [stockTakeSessions, setStockTakeSessions] = useState([]); const [total, setTotal] = useState(0); @@ -68,66 +69,45 @@ const PickerCardList: React.FC = ({ const [listRefreshNonce, setListRefreshNonce] = useState(0); const [creating, setCreating] = useState(false); const [openConfirmDialog, setOpenConfirmDialog] = useState(false); - const [filterSectionDescription, setFilterSectionDescription] = useState("All"); -const [filterStockTakeSession, setFilterStockTakeSession] = useState(""); -const [sectionDescriptionAutocompleteOptions, setSectionDescriptionAutocompleteOptions] = useState<{ value: string; label: string }[]>([]); -type PickerSearchKey = "sectionDescription" | "stockTakeSession"; -const sectionDescriptionOptions = Array.from( - new Set( - stockTakeSessions - .map((s) => s.stockTakeSectionDescription) - .filter((v): v is string => !!v) - ) -); -/* -// 按 description + section 双条件过滤 -const filteredSessions = stockTakeSessions.filter((s) => { - const matchDesc = - filterSectionDescription === "All" || - s.stockTakeSectionDescription === filterSectionDescription; + const [sectionDescriptionOptions, setSectionDescriptionOptions] = useState([]); + const [stockTakeSectionOptions, setStockTakeSectionOptions] = useState([]); + const [storeIdOptions, setStoreIdOptions] = useState(["2F", "4F"]); + const [searchSectionDescription, setSearchSectionDescription] = useState("All"); + const [searchStockTakeSession, setSearchStockTakeSession] = useState(""); + const [searchStatus, setSearchStatus] = useState("All"); + const [searchArea, setSearchArea] = useState(""); + const [searchStoreId, setSearchStoreId] = useState("All"); - const sessionParts = (filterStockTakeSession ?? "") - .split(",") - .map((p) => p.trim().toLowerCase()) - .filter(Boolean); - - const matchSession = - sessionParts.length === 0 || - sessionParts.some((part) => - (s.stockTakeSession ?? "").toString().toLowerCase().includes(part) - ); + const [filterSectionDescription, setFilterSectionDescription] = useState("All"); + const [filterStockTakeSession, setFilterStockTakeSession] = useState(""); + const [filterStatus, setFilterStatus] = useState("All"); + const [filterArea, setFilterArea] = useState(""); + const [filterStoreId, setFilterStoreId] = useState("All"); - return matchDesc && matchSession; -}); -*/ + const statusOptions = ["pending", "stockTaking", "approving", "completed"]; -// SearchBox 的条件配置 -const criteria: Criterion[] = [ - { - 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: "", - }, -]; + const handleSearch = () => { + setFilterSectionDescription(searchSectionDescription || "All"); + setFilterStockTakeSession(searchStockTakeSession || ""); + setFilterStatus(searchStatus || "All"); + setFilterArea(searchArea || ""); + setFilterStoreId(searchStoreId || "All"); + onListPageChange(0); + }; -const handleSearch = (inputs: Record) => { - setFilterSectionDescription(inputs.sectionDescription || "All"); - setFilterStockTakeSession(inputs.stockTakeSession || ""); - onListPageChange(0); -}; -const handleResetSearch = () => { - setFilterSectionDescription("All"); - setFilterStockTakeSession(""); - onListPageChange(0); -}; + const handleResetSearch = () => { + setSearchSectionDescription("All"); + setSearchStockTakeSession(""); + setSearchStatus("All"); + setSearchArea(""); + setSearchStoreId("All"); + setFilterSectionDescription("All"); + setFilterStockTakeSession(""); + setFilterStatus("All"); + setFilterArea(""); + setFilterStoreId("All"); + onListPageChange(0); + }; useEffect(() => { let cancelled = false; @@ -135,6 +115,9 @@ const handleResetSearch = () => { getStockTakeRecordsPaged(page, pageSize, { sectionDescription: filterSectionDescription, stockTakeSections: filterStockTakeSession, + status: filterStatus, + area: filterArea, + storeId: filterStoreId, }) .then((res) => { if (cancelled) return; @@ -154,7 +137,7 @@ const handleResetSearch = () => { return () => { cancelled = true; }; - }, [page, pageSize, filterSectionDescription, filterStockTakeSession, listRefreshNonce]); + }, [page, pageSize, filterSectionDescription, filterStockTakeSession, filterStatus, filterArea, filterStoreId, listRefreshNonce]); //const startIdx = page * PER_PAGE; //const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE); @@ -187,18 +170,44 @@ const handleResetSearch = () => { fetchStockTakeSections() .then((sections) => { const descSet = new Set(); + const sectionSet = new Set(); + const storeIdSet = new Set(["2F", "4F"]); sections.forEach((s) => { + const section = s.stockTakeSection?.trim(); + if (section) sectionSet.add(section); const desc = s.stockTakeSectionDescription?.trim(); if (desc) descSet.add(desc); + const storeId = s.storeId?.trim(); + if (storeId) storeIdSet.add(storeId); }); - setSectionDescriptionAutocompleteOptions( - Array.from(descSet).map((desc) => ({ value: desc, label: desc })) - ); + setStockTakeSectionOptions(Array.from(sectionSet).sort((a, b) => a.localeCompare(b))); + setSectionDescriptionOptions(Array.from(descSet).sort((a, b) => a.localeCompare(b))); + setStoreIdOptions(Array.from(storeIdSet).sort((a, b) => a.localeCompare(b))); }) .catch((e) => { console.error("Failed to load section descriptions for filter:", e); }); }, []); + useEffect(() => { + setStockTakeSectionOptions((prev) => { + const sectionSet = new Set(prev); + stockTakeSessions.forEach((item) => { + const section = item.stockTakeSession?.trim(); + if (section) sectionSet.add(section); + }); + return Array.from(sectionSet).sort((a, b) => a.localeCompare(b)); + }); + }, [stockTakeSessions]); + useEffect(() => { + setStoreIdOptions((prev) => { + const storeIdSet = new Set([...prev, "2F", "4F"]); + stockTakeSessions.forEach((item) => { + const storeId = item.storeId?.trim(); + if (storeId) storeIdSet.add(storeId); + }); + return Array.from(storeIdSet).sort((a, b) => a.localeCompare(b)); + }); + }, [stockTakeSessions]); const getStatusColor = (status: string) => { const statusLower = status.toLowerCase(); if (statusLower === "completed") return "success"; @@ -277,23 +286,99 @@ const handleResetSearch = () => { if (!first?.planStartDate) return null; return dayjs(first.planStartDate).format(OUTPUT_DATE_FORMAT); })(); - if (loading) { - return ( - - - - ); - } - return ( - - - criteria={criteria} - onSearch={handleSearch} - onReset={handleResetSearch} - /> - + + + + {t("Search Criteria")} + + + + + {t("Stock Take Section")} + + + + + setSearchStockTakeSession(newValue)} + renderInput={(params) => ( + + )} + /> + + + + {t("Status")} + + + + + setSearchArea(e.target.value)} + /> + + + + {t("Store ID")} + + + + + + + + + + @@ -314,13 +399,23 @@ const handleResetSearch = () => { + {loading ? ( + + + + ) : ( - {stockTakeSessions.map((session: AllPickedStockTakeListReponse) => { + {stockTakeSessions.map((session: AllPickedStockTakeListReponse) => { const statusColor = getStatusColor(session.status || ""); const lastStockTakeDate = session.lastStockTakeDate ? dayjs(session.lastStockTakeDate).format(OUTPUT_DATE_FORMAT) : "-"; const completionRate = getCompletionRate(session); + const sectionMeta = [ + session.stockTakeSectionDescription, + session.warehouseArea, + session.storeId, + ].filter((v): v is string => Boolean(v && v.trim())).join(" / "); return ( @@ -337,7 +432,7 @@ const handleResetSearch = () => { {t("Section")}: {session.stockTakeSession} - {session.stockTakeSectionDescription ? ` (${session.stockTakeSectionDescription})` : null} + {sectionMeta ? ` (${sectionMeta})` : null} @@ -383,6 +478,7 @@ const handleResetSearch = () => { ); })} + )} {total > 0 && ( = ({ }} placeholder={t("Stock Take Qty")} /> + {/* = ({ }} placeholder={t("Bad Qty")} /> + */} = {formatNumber(parseFloat(inputs.firstQty || "0") - parseFloat(inputs.firstBadQty || "0"))} @@ -528,7 +530,8 @@ const PickerReStockTake: React.FC = ({ {t("First")}:{" "} {formatNumber((detail.firstStockTakeQty ?? 0) + (detail.firstBadQty ?? 0))}{" "} - ({formatNumber(detail.firstBadQty ?? 0)}) ={" "} + {/* ({formatNumber(detail.firstBadQty ?? 0)}) */} + ={" "} {formatNumber(detail.firstStockTakeQty ?? 0)} ) : null} @@ -562,6 +565,7 @@ const PickerReStockTake: React.FC = ({ }} placeholder={t("Stock Take Qty")} /> + {/* = ({ }} placeholder={t("Bad Qty")} /> + */} = {formatNumber(parseFloat(inputs.secondQty || "0") - parseFloat(inputs.secondBadQty || "0"))} @@ -595,7 +600,8 @@ const PickerReStockTake: React.FC = ({ {t("Second")}:{" "} {formatNumber((detail.secondStockTakeQty ?? 0) + (detail.secondBadQty ?? 0))}{" "} - ({formatNumber(detail.secondBadQty ?? 0)}) ={" "} + {/* ({formatNumber(detail.secondBadQty ?? 0)}) */} + ={" "} {formatNumber(detail.secondStockTakeQty ?? 0)} ) : null} diff --git a/src/components/StockTakeManagement/PickerStockTake.tsx b/src/components/StockTakeManagement/PickerStockTake.tsx index 3984bf2..7abbcf7 100644 --- a/src/components/StockTakeManagement/PickerStockTake.tsx +++ b/src/components/StockTakeManagement/PickerStockTake.tsx @@ -191,18 +191,9 @@ const PickerStockTake: React.FC = ({ const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty; const badQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstBadQty : recordInputs[detail.id]?.secondBadQty; - // 只檢查 totalQty,Bad Qty 未輸入時預設為 0 - if (!totalQtyStr) { - onSnackbar( - isFirstSubmit - ? t("Please enter QTY") - : t("Please enter Second QTY"), - "error" - ); - return; - } + - const totalQty = parseFloat(totalQtyStr); + const totalQty = parseFloat(totalQtyStr || "0") || 0; const badQty = parseFloat(badQtyStr || "0") || 0; // 空字串時為 0 if (Number.isNaN(totalQty)) { @@ -617,6 +608,7 @@ const PickerStockTake: React.FC = ({ }} placeholder={t("Stock Take Qty")} /> + {/* = ({ }} placeholder={t("Bad Qty")} /> + */} = {formatNumber( @@ -658,11 +651,13 @@ const PickerStockTake: React.FC = ({ (detail.firstStockTakeQty ?? 0) + (detail.firstBadQty ?? 0) )}{" "} + {/* ( {formatNumber( detail.firstBadQty ?? 0 )} - ) ={" "} + */} + ={" "} {formatNumber(detail.firstStockTakeQty ?? 0)} ) : null} @@ -693,6 +688,7 @@ const PickerStockTake: React.FC = ({ }} placeholder={t("Stock Take Qty")} /> + {/* = ({ }} placeholder={t("Bad Qty")} /> + */} = {formatNumber( @@ -730,11 +727,13 @@ const PickerStockTake: React.FC = ({ (detail.secondStockTakeQty ?? 0) + (detail.secondBadQty ?? 0) )}{" "} + {/* ( {formatNumber( detail.secondBadQty ?? 0 )} - ) ={" "} + */} + ={" "} {formatNumber(detail.secondStockTakeQty ?? 0)} ) : null} diff --git a/src/config/reportConfig.ts b/src/config/reportConfig.ts index 95e332c..7bccf41 100644 --- a/src/config/reportConfig.ts +++ b/src/config/reportConfig.ts @@ -110,7 +110,7 @@ export const REPORTS: ReportDefinition[] = [ apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-take-variance-v2`, fields: [ { - label: "盤點輪次 Stock Take Round", + label: "盤點輪次", name: "stockTakeRoundId", type: "select", required: true, @@ -118,7 +118,31 @@ export const REPORTS: ReportDefinition[] = [ dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/stock-take-rounds`, options: [] }, - { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, + { label: "物料編號", name: "itemCode", type: "text", required: false}, + { + label: "倉庫樓層", + name: "store_id", + type: "select", + required: false, + options: [ + { label: "全部", value: "All" }, + { label: "1F", value: "1F" }, + { label: "2F", value: "2F" }, + { label: "3F", value: "3F" }, + { label: "4F", value: "4F" } + ], + }, + { + label: "狀態", + name: "status", + type: "select", + required: false, + options: [ + { label: "全部", value: "All" }, + { label: "待盤點", value: "pending" }, + { label: "已審核", value: "completed" } + ], + }, ] }, { id: "rep-011", diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index 07de0c8..6384f4e 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -15,8 +15,6 @@ "Floor": "樓層", "Job Order Type": "工單類型", - "FG": "成品", - "WIP": "半成品", "BOM Type": "BOM 類型", "No Lot": "沒有批號", "Select All": "全選", @@ -110,7 +108,7 @@ "code": "編號", "Name": "名稱", "Assignment successful": "分配成功", - "Pass": "通過", + "Unable to get user ID": "無法獲取用戶ID", "Unknown error: ": "未知錯誤: ", "Please try again later.": "請稍後重試。", @@ -468,8 +466,8 @@ "Powder_Mixture": "箱料粉", "Powder Mixture": "箱料粉", "Not Match": "數值不符", - "Pass": "通過", - "pass": "通過", + "Pass": "已盤點", + "pass": "已盤點", "Actions": "操作", "Insert": "插入", "Move to order": "移動到指定順序", diff --git a/src/i18n/zh/inventory.json b/src/i18n/zh/inventory.json index e9fb839..e175eac 100644 --- a/src/i18n/zh/inventory.json +++ b/src/i18n/zh/inventory.json @@ -50,11 +50,13 @@ "Batch Save All": "批量保存所有", "not match": "數值不符", "Not Match": "數值不符", - "Pass": "通過", + "Pass": "已盤點", + "Area": "區域", + "Selected Qty": "選擇數量", "Show Search Filters": "顯示搜索器", "Hide Search Filters": "隱藏搜索器", - "Stock Take Qty(include Bad Qty)= Available Qty": "盤點數(含壞品)= 可用數", + "Stock Take Qty(include Bad Qty)= Available Qty": "盤點數= 可用數", "View ReStockTake": "查看重新盤點", "Stock Take Qty": "盤點數", "variance Percentage": "差異百分比", @@ -97,9 +99,11 @@ "book qty": "帳面庫存", "start time": "開始時間", "end time": "結束時間", + "notmatch": "數值不符", "Only Variance": "僅差異", "Control Time": "操作時間", - "pass": "通過", + "Batch approver save completed: {{success}} success, {{errors}} errors": "批次審核儲存完成:成功 {{success}} 筆,錯誤 {{errors}} 筆", + "pass": "已盤點", "not pass": "不通過", "Available": "可用", "approving": "審核中", @@ -126,6 +130,7 @@ "Create Stock Take for All Sections": "為所有區域創建盤點", "section": "區域", "Stock Take Section": "盤點區域", + "Store ID":"樓層", "Warehouse Location": "倉庫位置", "UOM": "單位", "First Qty": "第一次盤點數量",