|
|
|
@@ -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]} |
|
|
|
/> |
|
|
|
)} |
|
|
|
|