diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 73a0caa..ca5bdad 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -779,6 +779,7 @@ export const fetchAllJoborderProductProcessInfo = cache(async (isDrink?: boolean }); export const fetchJoborderProductProcessesPage = cache(async (params: { + /** Job order planStart 區間起(YYYY-MM-DD,含當日) */ date?: string | null; itemCode?: string | null; jobOrderCode?: string | null; @@ -800,7 +801,9 @@ export const fetchJoborderProductProcessesPage = cache(async (params: { } = params; const queryParts: string[] = []; - if (date) queryParts.push(`date=${encodeURIComponent(date)}`); + if (date) { + queryParts.push(`date=${encodeURIComponent(date)}`); + } 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 b00c5e7..0fb150e 100644 --- a/src/components/ProductionProcess/ProductionProcessList.tsx +++ b/src/components/ProductionProcess/ProductionProcessList.tsx @@ -32,14 +32,7 @@ import { SessionWithTokens } from "@/config/authConfig"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox"; -export type ProductionProcessListPersistedState = { - date: string; - itemCode: string | null; - jobOrderCode: string | null; - filter: "all" | "drink" | "other"; - page: number; - selectedItemCodes: string[]; -}; + import { AllJoborderProductProcessInfoResponse, @@ -52,6 +45,15 @@ import { import { StockInLineInput } from "@/app/api/stockIn"; import { PrinterCombo } from "@/app/api/settings/printer"; import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; +export type ProductionProcessListPersistedState = { + date: 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; @@ -62,6 +64,18 @@ interface ProductProcessListProps { React.SetStateAction >; } +export type SearchParam = "date" | "itemCode" | "jobOrderCode" | "processType"; + +const PAGE_SIZE = 50; + +/** 預設依 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 { return { date: dayjs().format("YYYY-MM-DD"), @@ -72,36 +86,43 @@ export function createDefaultProductionProcessListPersistedState(): ProductionPr selectedItemCodes: [], }; } -type SearchParam = "date" | "itemCode" | "jobOrderCode" | "processType"; -const PAGE_SIZE = 50; - -const ProductProcessList: React.FC = ({ onSelectProcess, printerCombo ,onSelectMatchingStock, qcReady}) => { +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( + () => ({ + date: listPersistedState.date, + itemCode: listPersistedState.itemCode, + jobOrderCode: listPersistedState.jobOrderCode, + }), + [ + listPersistedState.date, + 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); @@ -192,28 +213,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); @@ -240,7 +275,7 @@ const ProductProcessList: React.FC = ({ onSelectProcess } finally { setLoading(false); } - }, [filter, appliedSearch, qcReady, page]); + }, [listPersistedState, qcReady]); useEffect(() => { fetchProcesses(); @@ -328,39 +363,64 @@ 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.date, + to: appliedSearch.date, + }, }, { 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.date, + 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 ( @@ -371,6 +431,7 @@ const ProductProcessList: React.FC = ({ onSelectProcess ) : ( + key={searchBoxKey} criteria={searchCriteria} onSearch={handleApplySearch} onReset={handleResetSearch} @@ -599,7 +660,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]} /> )}