Browse Source

update

MergeProblem1
CANCERYS\kw093 1 day ago
parent
commit
87d32c728a
5 changed files with 170 additions and 52 deletions
  1. +12
    -3
      src/app/api/jo/actions.ts
  2. +138
    -46
      src/components/ProductionProcess/ProductionProcessList.tsx
  3. +14
    -1
      src/components/ProductionProcess/ProductionProcessPage.tsx
  4. +4
    -2
      src/components/SearchBox/SearchBox.tsx
  5. +2
    -0
      src/i18n/zh/common.json

+ 12
- 3
src/app/api/jo/actions.ts View File

@@ -779,7 +779,10 @@ export const fetchAllJoborderProductProcessInfo = cache(async (isDrink?: boolean
}); });


export const fetchJoborderProductProcessesPage = cache(async (params: { 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; itemCode?: string | null;
jobOrderCode?: string | null; jobOrderCode?: string | null;
bomIds?: number[] | null; bomIds?: number[] | null;
@@ -789,7 +792,8 @@ export const fetchJoborderProductProcessesPage = cache(async (params: {
size?: number; size?: number;
}) => { }) => {
const { const {
date,
planStartFrom,
planStartTo,
itemCode, itemCode,
jobOrderCode, jobOrderCode,
bomIds, bomIds,
@@ -800,7 +804,12 @@ export const fetchJoborderProductProcessesPage = cache(async (params: {
} = params; } = params;


const queryParts: string[] = []; 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 (itemCode) queryParts.push(`itemCode=${encodeURIComponent(itemCode)}`);
if (jobOrderCode) queryParts.push(`jobOrderCode=${encodeURIComponent(jobOrderCode)}`); if (jobOrderCode) queryParts.push(`jobOrderCode=${encodeURIComponent(jobOrderCode)}`);
if (bomIds && bomIds.length > 0) queryParts.push(`bomIds=${bomIds.join(",")}`); if (bomIds && bomIds.length > 0) queryParts.push(`bomIds=${bomIds.join(",")}`);


+ 138
- 46
src/components/ProductionProcess/ProductionProcessList.tsx View File

@@ -45,43 +45,91 @@ import {
import { StockInLineInput } from "@/app/api/stockIn"; import { StockInLineInput } from "@/app/api/stockIn";
import { PrinterCombo } from "@/app/api/settings/printer"; import { PrinterCombo } from "@/app/api/settings/printer";
import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; 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 { interface ProductProcessListProps {
onSelectProcess: (jobOrderId: number|undefined, productProcessId: number|undefined) => void; onSelectProcess: (jobOrderId: number|undefined, productProcessId: number|undefined) => void;
onSelectMatchingStock: (jobOrderId: number|undefined, productProcessId: number|undefined,pickOrderId: number|undefined) => void; onSelectMatchingStock: (jobOrderId: number|undefined, productProcessId: number|undefined,pickOrderId: number|undefined) => void;
printerCombo: PrinterCombo[]; printerCombo: PrinterCombo[];
qcReady: boolean; qcReady: boolean;
/** 由父層保存,進入工單詳情再返回時可還原同一組搜尋/分頁 */
listPersistedState: ProductionProcessListPersistedState;
onListPersistedStateChange: React.Dispatch<
React.SetStateAction<ProductionProcessListPersistedState>
>;
} }


type SearchParam = "date" | "itemCode" | "jobOrderCode" | "processType"; type SearchParam = "date" | "itemCode" | "jobOrderCode" | "processType";


const PAGE_SIZE = 50; const PAGE_SIZE = 50;


const ProductProcessList: React.FC<ProductProcessListProps> = ({ 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<ProductProcessListProps> = ({
onSelectProcess,
printerCombo,
onSelectMatchingStock,
qcReady,
listPersistedState,
onListPersistedStateChange,
}) => {
const { t } = useTranslation( ["common", "production","purchaseOrder","dashboard"]); const { t } = useTranslation( ["common", "production","purchaseOrder","dashboard"]);
const { data: session } = useSession() as { data: SessionWithTokens | null }; const { data: session } = useSession() as { data: SessionWithTokens | null };
const sessionToken = session as SessionWithTokens | null; const sessionToken = session as SessionWithTokens | null;
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [processes, setProcesses] = useState<AllJoborderProductProcessInfoResponse[]>([]); const [processes, setProcesses] = useState<AllJoborderProductProcessInfoResponse[]>([]);
const [page, setPage] = useState(0);
const [openModal, setOpenModal] = useState<boolean>(false); const [openModal, setOpenModal] = useState<boolean>(false);
const [modalInfo, setModalInfo] = useState<StockInLineInput>(); const [modalInfo, setModalInfo] = useState<StockInLineInput>();
const currentUserId = session?.id ? parseInt(session.id) : undefined; const currentUserId = session?.id ? parseInt(session.id) : undefined;
type ProcessFilter = "all" | "drink" | "other"; type ProcessFilter = "all" | "drink" | "other";
const [filter, setFilter] = useState<ProcessFilter>("all");
const [suggestedLocationCode, setSuggestedLocationCode] = useState<string | null>(null); const [suggestedLocationCode, setSuggestedLocationCode] = useState<string | null>(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 [totalJobOrders, setTotalJobOrders] = useState(0);
const [selectedItemCodes, setSelectedItemCodes] = useState<string[]>([]);


// Generic confirm dialog for actions (update job order / etc.) // Generic confirm dialog for actions (update job order / etc.)
const [confirmOpen, setConfirmOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false);
@@ -172,28 +220,42 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
setOpenModal(true); setOpenModal(true);
}, [t]); }, [t]);


const handleApplySearch = useCallback((inputs: Record<SearchParam | `${SearchParam}To`, string>) => {
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<SearchParam | `${SearchParam}To`, string>) => {
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(() => { 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, itemCode: null,
jobOrderCode: null, jobOrderCode: null,
});
setSelectedItemCodes([]);
setPage(0);
}, []);
selectedItemCodes: [],
page: 0,
}));
}, [onListPersistedStateChange]);


const fetchProcesses = useCallback(async () => { const fetchProcesses = useCallback(async () => {
setLoading(true); setLoading(true);
@@ -202,7 +264,8 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
filter === "all" ? undefined : filter === "drink" ? true : false; filter === "all" ? undefined : filter === "drink" ? true : false;


const data = await fetchJoborderProductProcessesPage({ const data = await fetchJoborderProductProcessesPage({
date: appliedSearch.date,
planStartFrom: appliedSearch.planStartFrom,
planStartTo: appliedSearch.planStartTo,
itemCode: appliedSearch.itemCode, itemCode: appliedSearch.itemCode,
jobOrderCode: appliedSearch.jobOrderCode, jobOrderCode: appliedSearch.jobOrderCode,
qcReady, qcReady,
@@ -220,7 +283,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [filter, appliedSearch, qcReady, page]);
}, [listPersistedState, qcReady]);


useEffect(() => { useEffect(() => {
fetchProcesses(); fetchProcesses();
@@ -308,39 +371,65 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
return processes.filter((p) => selectedItemCodes.includes(p.itemCode)); return processes.filter((p) => selectedItemCodes.includes(p.itemCode));
}, [processes, selectedItemCodes]); }, [processes, selectedItemCodes]);


const searchCriteria: Criterion<SearchParam>[] = useMemo(
() => [
/** Reset 用 ±3 天;preFilled 用目前已套用的條件(與列表查詢一致) */
const searchCriteria: Criterion<SearchParam>[] = useMemo(() => {
const r = defaultPlanStartRange();
return [
{ {
type: "date",
label: "Production date",
type: "dateRange",
label: t("Plan start (from)"),
label2: t("Plan start (to)"),
paramName: "date", paramName: "date",
preFilledValue: dayjs().format("YYYY-MM-DD"),
defaultValue: r.from,
defaultValueTo: r.to,
preFilledValue: {
from: appliedSearch.planStartFrom,
to: appliedSearch.planStartTo,
},
}, },
{ {
type: "text", type: "text",
label: "Item Code", label: "Item Code",
paramName: "itemCode", paramName: "itemCode",
preFilledValue: appliedSearch.itemCode ?? "",
}, },
{ {
type: "text", type: "text",
label: "Job Order Code", label: "Job Order Code",
paramName: "jobOrderCode", paramName: "jobOrderCode",
preFilledValue: appliedSearch.jobOrderCode ?? "",
}, },
{ {
type: "select", type: "select",
label: "Type", label: "Type",
paramName: "processType", paramName: "processType",
options: ["all", "drink", "other"], 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<string[]>) => {
const nextValue = e.target.value;
setSelectedItemCodes(typeof nextValue === "string" ? nextValue.split(",") : nextValue);
}, []);
const handleSelectedItemCodesChange = useCallback(
(e: SelectChangeEvent<string[]>) => {
const nextValue = e.target.value;
const codes = typeof nextValue === "string" ? nextValue.split(",") : nextValue;
onListPersistedStateChange((prev) => ({ ...prev, selectedItemCodes: codes }));
},
[onListPersistedStateChange],
);


return ( return (
<Box> <Box>
@@ -351,6 +440,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
) : ( ) : (
<Box> <Box>
<SearchBox<SearchParam> <SearchBox<SearchParam>
key={searchBoxKey}
criteria={searchCriteria} criteria={searchCriteria}
onSearch={handleApplySearch} onSearch={handleApplySearch}
onReset={handleResetSearch} onReset={handleResetSearch}
@@ -573,7 +663,9 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
count={totalJobOrders} count={totalJobOrders}
page={page} page={page}
rowsPerPage={PAGE_SIZE} rowsPerPage={PAGE_SIZE}
onPageChange={(e, p) => setPage(p)}
onPageChange={(e, p) =>
onListPersistedStateChange((prev) => ({ ...prev, page: p }))
}
rowsPerPageOptions={[PAGE_SIZE]} rowsPerPageOptions={[PAGE_SIZE]}
/> />
)} )}


+ 14
- 1
src/components/ProductionProcess/ProductionProcessPage.tsx View File

@@ -3,7 +3,9 @@ import React, { useState, useEffect, useCallback } from "react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig"; import { SessionWithTokens } from "@/config/authConfig";
import { Box, Tabs, Tab, Stack, Typography, Autocomplete, TextField } from "@mui/material"; 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 ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail";
import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail"; import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail";
import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan"; import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan";
@@ -43,6 +45,13 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
pickOrderId: number; pickOrderId: number;
} | null>(null); } | null>(null);
const [tabIndex, setTabIndex] = useState(0); const [tabIndex, setTabIndex] = useState(0);
/** 列表搜尋/分頁:保留在切換工單詳情時,返回後仍為同一條件 */
const [productionListState, setProductionListState] = useState(
createDefaultProductionProcessListPersistedState,
);
const [finishedQcListState, setFinishedQcListState] = useState(
createDefaultProductionProcessListPersistedState,
);
const { data: session } = useSession() as { data: SessionWithTokens | null }; const { data: session } = useSession() as { data: SessionWithTokens | null };
const currentUserId = session?.id ? parseInt(session.id) : undefined; const currentUserId = session?.id ? parseInt(session.id) : undefined;


@@ -180,6 +189,8 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
<ProductionProcessList <ProductionProcessList
printerCombo={printerCombo} printerCombo={printerCombo}
qcReady={false} qcReady={false}
listPersistedState={productionListState}
onListPersistedStateChange={setProductionListState}
onSelectProcess={(jobOrderId) => { onSelectProcess={(jobOrderId) => {
const id = jobOrderId ?? null; const id = jobOrderId ?? null;
if (id !== null) { if (id !== null) {
@@ -200,6 +211,8 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
<ProductionProcessList <ProductionProcessList
printerCombo={printerCombo} printerCombo={printerCombo}
qcReady={true} qcReady={true}
listPersistedState={finishedQcListState}
onListPersistedStateChange={setFinishedQcListState}
onSelectProcess={(jobOrderId) => { onSelectProcess={(jobOrderId) => {
const id = jobOrderId ?? null; const id = jobOrderId ?? null;
if (id !== null) { if (id !== null) {


+ 4
- 2
src/components/SearchBox/SearchBox.tsx View File

@@ -40,6 +40,8 @@ interface BaseCriterion<T extends string> {
paramName2?: T; paramName2?: T;
// options?: T[] | string[]; // options?: T[] | string[];
defaultValue?: string; defaultValue?: string;
/** 與 `defaultValue` 配對,用於 dateRange / datetimeRange 重置時的結束值 */
defaultValueTo?: string;
preFilledValue?: string | { from?: string; to?: string }; preFilledValue?: string | { from?: string; to?: string };
filterObj?: T; filterObj?: T;
handleSelectionChange?: (selectedOptions: T[]) => void; handleSelectionChange?: (selectedOptions: T[]) => void;
@@ -159,7 +161,7 @@ function SearchBox<T extends string>({
tempCriteria = { tempCriteria = {
...tempCriteria, ...tempCriteria,
[c.paramName]: c.defaultValue ?? "", [c.paramName]: c.defaultValue ?? "",
[`${c.paramName}To`]: "",
[`${c.paramName}To`]: c.defaultValueTo ?? "",
}; };
} }
return tempCriteria; return tempCriteria;
@@ -188,7 +190,7 @@ function SearchBox<T extends string>({
{} as Record<T | `${T}To`, string>, {} as Record<T | `${T}To`, string>,
); );
return {...defaultInputs, ...preFilledCriteria} return {...defaultInputs, ...preFilledCriteria}
}, [defaultInputs])
}, [defaultInputs, criteria])


const [inputs, setInputs] = useState(preFilledInputs); const [inputs, setInputs] = useState(preFilledInputs);
const [isReset, setIsReset] = useState(false); const [isReset, setIsReset] = useState(false);


+ 2
- 0
src/i18n/zh/common.json View File

@@ -17,6 +17,8 @@
"Process & Equipment": "製程與設備", "Process & Equipment": "製程與設備",
"Sequence": "順序", "Sequence": "順序",
"Process Name": "製程名稱", "Process Name": "製程名稱",
"Plan start (from)": "開始日期(從)",
"Plan start (to)": "開始日期(至)",
"Process Description": "說明", "Process Description": "說明",
"Confirm to Pass this Process?": "確認要通過此工序嗎?", "Confirm to Pass this Process?": "確認要通過此工序嗎?",
"Equipment Name": "設備", "Equipment Name": "設備",


Loading…
Cancel
Save