Procházet zdrojové kódy

update

MergeProblem1
CANCERYS\kw093 před 1 dnem
rodič
revize
87d32c728a
5 změnil soubory, kde provedl 170 přidání a 52 odebrání
  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 Zobrazit soubor

@@ -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(",")}`);


+ 138
- 46
src/components/ProductionProcess/ProductionProcessList.tsx Zobrazit soubor

@@ -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<ProductionProcessListPersistedState>
>;
}

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

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 { data: session } = useSession() as { data: SessionWithTokens | null };
const sessionToken = session as SessionWithTokens | null;
const [loading, setLoading] = useState(false);
const [processes, setProcesses] = useState<AllJoborderProductProcessInfoResponse[]>([]);
const [page, setPage] = useState(0);
const [openModal, setOpenModal] = useState<boolean>(false);
const [modalInfo, setModalInfo] = useState<StockInLineInput>();
const currentUserId = session?.id ? parseInt(session.id) : undefined;
type ProcessFilter = "all" | "drink" | "other";
const [filter, setFilter] = useState<ProcessFilter>("all");
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 [selectedItemCodes, setSelectedItemCodes] = useState<string[]>([]);

// Generic confirm dialog for actions (update job order / etc.)
const [confirmOpen, setConfirmOpen] = useState(false);
@@ -172,28 +220,42 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
setOpenModal(true);
}, [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(() => {
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<ProductProcessListProps> = ({ 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<ProductProcessListProps> = ({ onSelectProcess
} finally {
setLoading(false);
}
}, [filter, appliedSearch, qcReady, page]);
}, [listPersistedState, qcReady]);

useEffect(() => {
fetchProcesses();
@@ -308,39 +371,65 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
return processes.filter((p) => selectedItemCodes.includes(p.itemCode));
}, [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",
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<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 (
<Box>
@@ -351,6 +440,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
) : (
<Box>
<SearchBox<SearchParam>
key={searchBoxKey}
criteria={searchCriteria}
onSearch={handleApplySearch}
onReset={handleResetSearch}
@@ -573,7 +663,9 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
count={totalJobOrders}
page={page}
rowsPerPage={PAGE_SIZE}
onPageChange={(e, p) => setPage(p)}
onPageChange={(e, p) =>
onListPersistedStateChange((prev) => ({ ...prev, page: p }))
}
rowsPerPageOptions={[PAGE_SIZE]}
/>
)}


+ 14
- 1
src/components/ProductionProcess/ProductionProcessPage.tsx Zobrazit soubor

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


+ 4
- 2
src/components/SearchBox/SearchBox.tsx Zobrazit soubor

@@ -40,6 +40,8 @@ interface BaseCriterion<T extends string> {
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<T extends string>({
tempCriteria = {
...tempCriteria,
[c.paramName]: c.defaultValue ?? "",
[`${c.paramName}To`]: "",
[`${c.paramName}To`]: c.defaultValueTo ?? "",
};
}
return tempCriteria;
@@ -188,7 +190,7 @@ function SearchBox<T extends string>({
{} as Record<T | `${T}To`, string>,
);
return {...defaultInputs, ...preFilledCriteria}
}, [defaultInputs])
}, [defaultInputs, criteria])

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


+ 2
- 0
src/i18n/zh/common.json Zobrazit soubor

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


Načítá se…
Zrušit
Uložit