From 87d32c728a6466a2b37aaa2ed98b5311352f2f77 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Sat, 21 Mar 2026 10:10:32 +0800 Subject: [PATCH] update --- src/app/api/jo/actions.ts | 15 +- .../ProductionProcessList.tsx | 184 +++++++++++++----- .../ProductionProcessPage.tsx | 15 +- src/components/SearchBox/SearchBox.tsx | 6 +- src/i18n/zh/common.json | 2 + 5 files changed, 170 insertions(+), 52 deletions(-) diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 73a0caa..8cc5a11 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -779,7 +779,10 @@ export const fetchAllJoborderProductProcessInfo = cache(async (isDrink?: boolean }); export const fetchJoborderProductProcessesPage = cache(async (params: { - date?: string | null; + /** Job order planStart 區間起(YYYY-MM-DD,含當日) */ + planStartFrom?: string | null; + /** Job order planStart 區間迄(YYYY-MM-DD,含當日) */ + planStartTo?: string | null; itemCode?: string | null; jobOrderCode?: string | null; bomIds?: number[] | null; @@ -789,7 +792,8 @@ export const fetchJoborderProductProcessesPage = cache(async (params: { size?: number; }) => { const { - date, + planStartFrom, + planStartTo, itemCode, jobOrderCode, bomIds, @@ -800,7 +804,12 @@ export const fetchJoborderProductProcessesPage = cache(async (params: { } = params; const queryParts: string[] = []; - if (date) queryParts.push(`date=${encodeURIComponent(date)}`); + if (planStartFrom) { + queryParts.push(`planStartFrom=${encodeURIComponent(planStartFrom)}`); + } + if (planStartTo) { + queryParts.push(`planStartTo=${encodeURIComponent(planStartTo)}`); + } if (itemCode) queryParts.push(`itemCode=${encodeURIComponent(itemCode)}`); if (jobOrderCode) queryParts.push(`jobOrderCode=${encodeURIComponent(jobOrderCode)}`); if (bomIds && bomIds.length > 0) queryParts.push(`bomIds=${bomIds.join(",")}`); diff --git a/src/components/ProductionProcess/ProductionProcessList.tsx b/src/components/ProductionProcess/ProductionProcessList.tsx index f713ac6..f8b9651 100644 --- a/src/components/ProductionProcess/ProductionProcessList.tsx +++ b/src/components/ProductionProcess/ProductionProcessList.tsx @@ -45,43 +45,91 @@ import { import { StockInLineInput } from "@/app/api/stockIn"; import { PrinterCombo } from "@/app/api/settings/printer"; import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; +export type ProductionProcessListPersistedState = { + planStartFrom: string; + planStartTo: string; + itemCode: string | null; + jobOrderCode: string | null; + filter: "all" | "drink" | "other"; + page: number; + selectedItemCodes: string[]; +}; + interface ProductProcessListProps { onSelectProcess: (jobOrderId: number|undefined, productProcessId: number|undefined) => void; onSelectMatchingStock: (jobOrderId: number|undefined, productProcessId: number|undefined,pickOrderId: number|undefined) => void; printerCombo: PrinterCombo[]; qcReady: boolean; + /** 由父層保存,進入工單詳情再返回時可還原同一組搜尋/分頁 */ + listPersistedState: ProductionProcessListPersistedState; + onListPersistedStateChange: React.Dispatch< + React.SetStateAction + >; } type SearchParam = "date" | "itemCode" | "jobOrderCode" | "processType"; const PAGE_SIZE = 50; -const ProductProcessList: React.FC = ({ onSelectProcess, printerCombo ,onSelectMatchingStock, qcReady}) => { +/** 預設依 JobOrder.planStart 搜尋:今天往前 3 天~往後 3 天(含當日) */ +function defaultPlanStartRange() { + return { + from: dayjs().subtract(0, "day").format("YYYY-MM-DD"), + to: dayjs().add(0, "day").format("YYYY-MM-DD"), + }; +} + +export function createDefaultProductionProcessListPersistedState(): ProductionProcessListPersistedState { + const r = defaultPlanStartRange(); + return { + planStartFrom: r.from, + planStartTo: r.to, + itemCode: null, + jobOrderCode: null, + filter: "all", + page: 0, + selectedItemCodes: [], + }; +} + +const ProductProcessList: React.FC = ({ + onSelectProcess, + printerCombo, + onSelectMatchingStock, + qcReady, + listPersistedState, + onListPersistedStateChange, +}) => { const { t } = useTranslation( ["common", "production","purchaseOrder","dashboard"]); const { data: session } = useSession() as { data: SessionWithTokens | null }; const sessionToken = session as SessionWithTokens | null; const [loading, setLoading] = useState(false); const [processes, setProcesses] = useState([]); - const [page, setPage] = useState(0); const [openModal, setOpenModal] = useState(false); const [modalInfo, setModalInfo] = useState(); const currentUserId = session?.id ? parseInt(session.id) : undefined; type ProcessFilter = "all" | "drink" | "other"; - const [filter, setFilter] = useState("all"); const [suggestedLocationCode, setSuggestedLocationCode] = useState(null); - const [appliedSearch, setAppliedSearch] = useState<{ - date: string; - itemCode: string | null; - jobOrderCode: string | null; - }>(() => ({ - date: dayjs().format("YYYY-MM-DD"), - itemCode: null, - jobOrderCode: null, - })); + const appliedSearch = useMemo( + () => ({ + planStartFrom: listPersistedState.planStartFrom, + planStartTo: listPersistedState.planStartTo, + itemCode: listPersistedState.itemCode, + jobOrderCode: listPersistedState.jobOrderCode, + }), + [ + listPersistedState.planStartFrom, + listPersistedState.planStartTo, + listPersistedState.itemCode, + listPersistedState.jobOrderCode, + ], + ); + const filter = listPersistedState.filter; + const page = listPersistedState.page; + const selectedItemCodes = listPersistedState.selectedItemCodes; const [totalJobOrders, setTotalJobOrders] = useState(0); - const [selectedItemCodes, setSelectedItemCodes] = useState([]); // Generic confirm dialog for actions (update job order / etc.) const [confirmOpen, setConfirmOpen] = useState(false); @@ -172,28 +220,42 @@ const ProductProcessList: React.FC = ({ onSelectProcess setOpenModal(true); }, [t]); - const handleApplySearch = useCallback((inputs: Record) => { - const selectedProcessType = (inputs.processType || "all") as ProcessFilter; - setFilter(selectedProcessType); - setAppliedSearch({ - date: inputs.date || dayjs().format("YYYY-MM-DD"), - itemCode: inputs.itemCode?.trim() ? inputs.itemCode.trim() : null, - jobOrderCode: inputs.jobOrderCode?.trim() ? inputs.jobOrderCode.trim() : null, - }); - setSelectedItemCodes([]); - setPage(0); - }, []); + const handleApplySearch = useCallback( + (inputs: Record) => { + const selectedProcessType = (inputs.processType || "all") as ProcessFilter; + const fallback = defaultPlanStartRange(); + let from = (inputs.date || "").trim() || fallback.from; + let to = (inputs.dateTo || "").trim() || fallback.to; + if (dayjs(from).isAfter(dayjs(to), "day")) { + [from, to] = [to, from]; + } + onListPersistedStateChange((prev) => ({ + ...prev, + filter: selectedProcessType, + planStartFrom: from, + planStartTo: to, + itemCode: inputs.itemCode?.trim() ? inputs.itemCode.trim() : null, + jobOrderCode: inputs.jobOrderCode?.trim() ? inputs.jobOrderCode.trim() : null, + selectedItemCodes: [], + page: 0, + })); + }, + [onListPersistedStateChange], + ); const handleResetSearch = useCallback(() => { - setFilter("all"); - setAppliedSearch({ - date: dayjs().format("YYYY-MM-DD"), + const r = defaultPlanStartRange(); + onListPersistedStateChange((prev) => ({ + ...prev, + filter: "all", + planStartFrom: r.from, + planStartTo: r.to, itemCode: null, jobOrderCode: null, - }); - setSelectedItemCodes([]); - setPage(0); - }, []); + selectedItemCodes: [], + page: 0, + })); + }, [onListPersistedStateChange]); const fetchProcesses = useCallback(async () => { setLoading(true); @@ -202,7 +264,8 @@ const ProductProcessList: React.FC = ({ onSelectProcess filter === "all" ? undefined : filter === "drink" ? true : false; const data = await fetchJoborderProductProcessesPage({ - date: appliedSearch.date, + planStartFrom: appliedSearch.planStartFrom, + planStartTo: appliedSearch.planStartTo, itemCode: appliedSearch.itemCode, jobOrderCode: appliedSearch.jobOrderCode, qcReady, @@ -220,7 +283,7 @@ const ProductProcessList: React.FC = ({ onSelectProcess } finally { setLoading(false); } - }, [filter, appliedSearch, qcReady, page]); + }, [listPersistedState, qcReady]); useEffect(() => { fetchProcesses(); @@ -308,39 +371,65 @@ const ProductProcessList: React.FC = ({ onSelectProcess return processes.filter((p) => selectedItemCodes.includes(p.itemCode)); }, [processes, selectedItemCodes]); - const searchCriteria: Criterion[] = useMemo( - () => [ + /** Reset 用 ±3 天;preFilled 用目前已套用的條件(與列表查詢一致) */ + const searchCriteria: Criterion[] = useMemo(() => { + const r = defaultPlanStartRange(); + return [ { - type: "date", - label: "Production date", + type: "dateRange", + label: t("Plan start (from)"), + label2: t("Plan start (to)"), paramName: "date", - preFilledValue: dayjs().format("YYYY-MM-DD"), + defaultValue: r.from, + defaultValueTo: r.to, + preFilledValue: { + from: appliedSearch.planStartFrom, + to: appliedSearch.planStartTo, + }, }, { type: "text", label: "Item Code", paramName: "itemCode", + preFilledValue: appliedSearch.itemCode ?? "", }, { type: "text", label: "Job Order Code", paramName: "jobOrderCode", + preFilledValue: appliedSearch.jobOrderCode ?? "", }, { type: "select", label: "Type", paramName: "processType", options: ["all", "drink", "other"], - preFilledValue: "all", + preFilledValue: filter, }, - ], - [], + ]; + }, [appliedSearch, filter, t]); + + /** SearchBox 內部 state 只在掛載時讀 preFilled;套用搜尋後需 remount 才會與 appliedSearch 一致 */ + const searchBoxKey = useMemo( + () => + [ + appliedSearch.planStartFrom, + appliedSearch.planStartTo, + appliedSearch.itemCode ?? "", + appliedSearch.jobOrderCode ?? "", + filter, + ].join("|"), + [appliedSearch, filter], ); - const handleSelectedItemCodesChange = useCallback((e: SelectChangeEvent) => { - const nextValue = e.target.value; - setSelectedItemCodes(typeof nextValue === "string" ? nextValue.split(",") : nextValue); - }, []); + const handleSelectedItemCodesChange = useCallback( + (e: SelectChangeEvent) => { + const nextValue = e.target.value; + const codes = typeof nextValue === "string" ? nextValue.split(",") : nextValue; + onListPersistedStateChange((prev) => ({ ...prev, selectedItemCodes: codes })); + }, + [onListPersistedStateChange], + ); return ( @@ -351,6 +440,7 @@ const ProductProcessList: React.FC = ({ onSelectProcess ) : ( + key={searchBoxKey} criteria={searchCriteria} onSearch={handleApplySearch} onReset={handleResetSearch} @@ -573,7 +663,9 @@ const ProductProcessList: React.FC = ({ onSelectProcess count={totalJobOrders} page={page} rowsPerPage={PAGE_SIZE} - onPageChange={(e, p) => setPage(p)} + onPageChange={(e, p) => + onListPersistedStateChange((prev) => ({ ...prev, page: p })) + } rowsPerPageOptions={[PAGE_SIZE]} /> )} diff --git a/src/components/ProductionProcess/ProductionProcessPage.tsx b/src/components/ProductionProcess/ProductionProcessPage.tsx index 5ffa8f9..ef34f7c 100644 --- a/src/components/ProductionProcess/ProductionProcessPage.tsx +++ b/src/components/ProductionProcess/ProductionProcessPage.tsx @@ -3,7 +3,9 @@ import React, { useState, useEffect, useCallback } from "react"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { Box, Tabs, Tab, Stack, Typography, Autocomplete, TextField } from "@mui/material"; -import ProductionProcessList from "@/components/ProductionProcess/ProductionProcessList"; +import ProductionProcessList, { + createDefaultProductionProcessListPersistedState, +} from "@/components/ProductionProcess/ProductionProcessList"; import ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail"; import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail"; import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan"; @@ -43,6 +45,13 @@ const ProductionProcessPage: React.FC = ({ printerCo pickOrderId: number; } | null>(null); const [tabIndex, setTabIndex] = useState(0); + /** 列表搜尋/分頁:保留在切換工單詳情時,返回後仍為同一條件 */ + const [productionListState, setProductionListState] = useState( + createDefaultProductionProcessListPersistedState, + ); + const [finishedQcListState, setFinishedQcListState] = useState( + createDefaultProductionProcessListPersistedState, + ); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; @@ -180,6 +189,8 @@ const ProductionProcessPage: React.FC = ({ printerCo { const id = jobOrderId ?? null; if (id !== null) { @@ -200,6 +211,8 @@ const ProductionProcessPage: React.FC = ({ printerCo { const id = jobOrderId ?? null; if (id !== null) { diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index ccd10c5..848fd14 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -40,6 +40,8 @@ interface BaseCriterion { paramName2?: T; // options?: T[] | string[]; defaultValue?: string; + /** 與 `defaultValue` 配對,用於 dateRange / datetimeRange 重置時的結束值 */ + defaultValueTo?: string; preFilledValue?: string | { from?: string; to?: string }; filterObj?: T; handleSelectionChange?: (selectedOptions: T[]) => void; @@ -159,7 +161,7 @@ function SearchBox({ tempCriteria = { ...tempCriteria, [c.paramName]: c.defaultValue ?? "", - [`${c.paramName}To`]: "", + [`${c.paramName}To`]: c.defaultValueTo ?? "", }; } return tempCriteria; @@ -188,7 +190,7 @@ function SearchBox({ {} as Record, ); return {...defaultInputs, ...preFilledCriteria} - }, [defaultInputs]) + }, [defaultInputs, criteria]) const [inputs, setInputs] = useState(preFilledInputs); const [isReset, setIsReset] = useState(false); diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index 9ee5daf..b2a2354 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -17,6 +17,8 @@ "Process & Equipment": "製程與設備", "Sequence": "順序", "Process Name": "製程名稱", + "Plan start (from)": "開始日期(從)", + "Plan start (to)": "開始日期(至)", "Process Description": "說明", "Confirm to Pass this Process?": "確認要通過此工序嗎?", "Equipment Name": "設備",