From c8fe2cb962ec59a55c7f196388ec1d4f99c9f82f Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Wed, 6 May 2026 16:33:26 +0800 Subject: [PATCH] update do search and jo bom name coe --- src/app/(main)/jo/workbench/page.tsx | 5 +- src/app/api/jo/actions.ts | 28 ++- src/components/DoDetail/DoDetail.tsx | 4 +- src/components/DoSearch/DoSearch.tsx | 119 +++++++++-- .../DoSearchWorkbench/DoSearchWorkbench.tsx | 118 +++++++++-- .../JoWorkbench/JoPickOrderList.tsx | 5 +- .../JoWorkbench/JoWorkbenchTabs.tsx | 2 +- .../JoWorkbench/newJobPickExecution.tsx | 192 +++++++++++++++--- src/components/Jodetail/JodetailSearch.tsx | 2 +- src/i18n/zh/do.json | 3 + src/i18n/zh/jo.json | 1 + 11 files changed, 421 insertions(+), 58 deletions(-) diff --git a/src/app/(main)/jo/workbench/page.tsx b/src/app/(main)/jo/workbench/page.tsx index 2140658..d66d7b7 100644 --- a/src/app/(main)/jo/workbench/page.tsx +++ b/src/app/(main)/jo/workbench/page.tsx @@ -1,6 +1,7 @@ import GeneralLoading from "@/components/General/GeneralLoading"; import PageTitleBar from "@/components/PageTitleBar"; import JoPickOrderList from "@/components/JoWorkbench/JoPickOrderList"; +import { fetchPrinterCombo } from "@/app/api/settings/printer"; import { I18nProvider, getServerI18n } from "@/i18n"; import { Metadata } from "next"; import React, { Suspense } from "react"; @@ -11,13 +12,15 @@ export const metadata: Metadata = { const JoWorkbenchPage = async () => { const { t } = await getServerI18n("jo"); + const printerCombo = await fetchPrinterCombo(); + //console.log("[JO Workbench Page] printerCombo count:", printerCombo?.length ?? 0); return ( <> }> - + diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index e2805fc..b08ac05 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -567,6 +567,12 @@ export interface JobOrderLotsHierarchicalResponse { pickOrderLines: PickOrderLineWithLotsResponse[]; } +/** JO Workbench: same shape as [JobOrderLotsHierarchicalResponse] but `pickOrder.jobOrder` includes BOM code/name. */ +export interface JobOrderLotsHierarchicalWorkbenchResponse { + pickOrder: PickOrderInfoWorkbenchResponse; + pickOrderLines: PickOrderLineWithLotsResponse[]; +} + export interface PickOrderInfoResponse { id: number | null; code: string | null; @@ -578,12 +584,32 @@ export interface PickOrderInfoResponse { jobOrder: JobOrderBasicInfoResponse; } +export interface PickOrderInfoWorkbenchResponse { + id: number | null; + code: string | null; + consoCode: string | null; + targetDate: string | null; + type: string | null; + status: string | null; + assignTo: number | null; + jobOrder: JobOrderBasicInfoWorkbenchResponse; +} + export interface JobOrderBasicInfoResponse { id: number; code: string; name: string; } +/** BOM header code/name from job order's BOM (workbench hierarchical API only). */ +export interface JobOrderBasicInfoWorkbenchResponse { + id: number; + code: string; + name: string; + itemCode: string | null; + itemName: string | null; +} + export interface PickOrderLineWithLotsResponse { id: number; itemId: number | null; @@ -724,7 +750,7 @@ export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrder export const fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench = cache( async (pickOrderId: number) => { - return serverFetchJson( + return serverFetchJson( `${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order-workbench/${pickOrderId}`, { method: "GET", diff --git a/src/components/DoDetail/DoDetail.tsx b/src/components/DoDetail/DoDetail.tsx index 0801d81..496d8ec 100644 --- a/src/components/DoDetail/DoDetail.tsx +++ b/src/components/DoDetail/DoDetail.tsx @@ -40,8 +40,8 @@ const DoDetail: React.FC = ({ const { data: session } = useSession() as { data: SessionWithTokens | null }; // Use correct session type const currentUserId = session?.id ? parseInt(session.id) : undefined; // Get user ID from session.id - console.log("🔍 DoSearch - session:", session); -console.log("🔍 DoSearch - currentUserId:", currentUserId); + //console.log("🔍 DoSearch - session:", session); +//console.log("🔍 DoSearch - currentUserId:", currentUserId); const formProps = useForm({ defaultValues: defaultValues }) diff --git a/src/components/DoSearch/DoSearch.tsx b/src/components/DoSearch/DoSearch.tsx index 18a0a79..fe3799c 100644 --- a/src/components/DoSearch/DoSearch.tsx +++ b/src/components/DoSearch/DoSearch.tsx @@ -43,7 +43,7 @@ type Props = { searchQuery?: Record; onDeliveryOrderSearch?: () => void; }; -type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "truckLanceCode" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" | "truckLanceCodeTo", string>; +type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "truckLanceCode" | "floor" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" | "truckLanceCodeTo" | "floorTo", string>; type SearchParamNames = keyof SearchBoxInputs; // put all this into a new component @@ -55,6 +55,10 @@ type EntryError = | undefined; type DoRow = TableRow, EntryError>; +/** 已填車線但未選預計送貨日:後端會掃全量再篩,需擋下。 */ +function isTruckLaneSearchMissingEta(truckLanceCode: string, estimatedArrivalDate: string): boolean { + return truckLanceCode.trim() !== "" && estimatedArrivalDate.trim() === ""; +} const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSearch }) => { const apiRef = useGridApiRef(); @@ -70,8 +74,8 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; - console.log("🔍 DoSearch - session:", session); - console.log("🔍 DoSearch - currentUserId:", currentUserId); + //console.log("🔍 DoSearch - session:", session); + //console.log("🔍 DoSearch - currentUserId:", currentUserId); const [searchTimeout, setSearchTimeout] = useState(null); /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜尋結果視為「已選」以便跨頁記憶 */ const [excludedRowIds, setExcludedRowIds] = useState([]); @@ -93,6 +97,7 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea shopName: "", deliveryOrderLines: "", truckLanceCode: "", // 添加这个字段 + floor: "All", codeTo: "", statusTo: "", estimatedArrivalDateTo: "", @@ -100,7 +105,8 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea supplierNameTo: "", shopNameTo: "", deliveryOrderLinesTo: "", - truckLanceCodeTo: "" // 这个字段已经存在,但需要确保在类型定义中 + truckLanceCodeTo: "", + floorTo: "", }); const [hasSearched, setHasSearched] = useState(false); @@ -143,7 +149,14 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea ...p, pageNum: 1, })); - }, [currentSearchParams.code, currentSearchParams.shopName, currentSearchParams.status, currentSearchParams.estimatedArrivalDate]); + }, [ + currentSearchParams.code, + currentSearchParams.shopName, + currentSearchParams.status, + currentSearchParams.estimatedArrivalDate, + currentSearchParams.truckLanceCode, + currentSearchParams.floor, + ]); const searchCriteria: Criterion[] = useMemo( @@ -151,6 +164,15 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea { label: t("Code"), paramName: "code", type: "text" }, { label: t("Shop Name"), paramName: "shopName", type: "text" }, { label: t("Truck Lance Code"), paramName: "truckLanceCode", type: "text" }, + { + label: t("Floor"), + paramName: "floor", + type: "select-labelled", + options: [ + { label: "2F", value: "2F" }, + { label: "4F", value: "4F" }, + ], + }, { label: t("Estimated Arrival"), paramName: "estimatedArrivalDate", @@ -297,6 +319,16 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea //SEARCH FUNCTION const handleSearch = useCallback(async (query: SearchBoxInputs) => { try { + if (isTruckLaneSearchMissingEta(query.truckLanceCode ?? "", query.estimatedArrivalDate ?? "")) { + await Swal.fire({ + icon: "warning", + title: t("Truck lane search requires date title"), + text: t("Truck lane search requires date message"), + confirmButtonText: t("Confirm"), + }); + return; + } + setCurrentSearchParams(query); let estArrStartDate = query.estimatedArrivalDate; @@ -313,6 +345,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { else{ status = query.status; } + + const floorParam = query.floor === "All" || !query.floor ? null : query.floor; // 调用新的 API,传入分页参数和 truckLanceCode const response = await fetchDoSearch( @@ -325,7 +359,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { "", // estArrEndDate - 不再使用 pagingController.pageNum, // 传入当前页码 pagingController.pageSize, // 传入每页大小 - query.truckLanceCode || "" // 添加 truckLanceCode 参数 + query.truckLanceCode || "", + floorParam, ); setSearchAllDos(response.records); @@ -342,7 +377,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { setHasResults(false); setExcludedRowIds([]); } -}, [pagingController]); +}, [pagingController, t]); useEffect(() => { if (typeof window !== 'undefined') { @@ -402,6 +437,20 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { // 使用新的分页参数重新搜索 const searchWithNewPage = async () => { try { + if ( + isTruckLaneSearchMissingEta( + currentSearchParams.truckLanceCode ?? "", + currentSearchParams.estimatedArrivalDate ?? "", + ) + ) { + await Swal.fire({ + icon: "warning", + title: t("Truck lane search requires date title"), + text: t("Truck lane search requires date message"), + confirmButtonText: t("Confirm"), + }); + return; + } let estArrStartDate = currentSearchParams.estimatedArrivalDate; const time = "T00:00:00"; @@ -416,6 +465,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { else{ status = currentSearchParams.status; } + + const floorParam = + currentSearchParams.floor === "All" || !currentSearchParams.floor + ? null + : currentSearchParams.floor; const response = await fetchDoSearch( currentSearchParams.code || "", @@ -427,7 +481,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { "", newPagingController.pageNum, newPagingController.pageSize, - currentSearchParams.truckLanceCode || "" // 添加这个参数 + currentSearchParams.truckLanceCode || "", + floorParam, ); setSearchAllDos(response.records); @@ -438,7 +493,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { }; searchWithNewPage(); } - }, [pagingController, hasSearched, currentSearchParams]); + }, [pagingController, hasSearched, currentSearchParams, t]); const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { const newPageSize = parseInt(event.target.value, 10); @@ -451,6 +506,20 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { if (hasSearched && currentSearchParams) { const searchWithNewPageSize = async () => { try { + if ( + isTruckLaneSearchMissingEta( + currentSearchParams.truckLanceCode ?? "", + currentSearchParams.estimatedArrivalDate ?? "", + ) + ) { + await Swal.fire({ + icon: "warning", + title: t("Truck lane search requires date title"), + text: t("Truck lane search requires date message"), + confirmButtonText: t("Confirm"), + }); + return; + } let estArrStartDate = currentSearchParams.estimatedArrivalDate; const time = "T00:00:00"; @@ -465,6 +534,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { else{ status = currentSearchParams.status; } + + const floorParam = + currentSearchParams.floor === "All" || !currentSearchParams.floor + ? null + : currentSearchParams.floor; const response = await fetchDoSearch( currentSearchParams.code || "", @@ -476,7 +550,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { "", 1, // 重置到第一页 newPageSize, - currentSearchParams.truckLanceCode || "" // 添加这个参数 + currentSearchParams.truckLanceCode || "", + floorParam, ); setSearchAllDos(response.records); @@ -487,10 +562,24 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { }; searchWithNewPageSize(); } - }, [hasSearched, currentSearchParams]); + }, [hasSearched, currentSearchParams, t]); const handleBatchRelease = useCallback(async (isWorkbench: boolean) => { try { + if ( + isTruckLaneSearchMissingEta( + currentSearchParams.truckLanceCode ?? "", + currentSearchParams.estimatedArrivalDate ?? "", + ) + ) { + await Swal.fire({ + icon: "warning", + title: t("Truck lane search requires date title"), + text: t("Truck lane search requires date message"), + confirmButtonText: t("Confirm"), + }); + return; + } // 根据当前搜索条件获取所有匹配的记录(不分页) let estArrStartDate = currentSearchParams.estimatedArrivalDate; const time = "T00:00:00"; @@ -506,6 +595,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { else{ status = currentSearchParams.status; } + + const floorParam = + currentSearchParams.floor === "All" || !currentSearchParams.floor + ? null + : currentSearchParams.floor; // 显示加载提示 const loadingSwal = Swal.fire({ @@ -525,7 +619,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { currentSearchParams.shopName || "", status, estArrStartDate, - currentSearchParams.truckLanceCode || "" + currentSearchParams.truckLanceCode || "", + floorParam, ); Swal.close(); diff --git a/src/components/DoSearchWorkbench/DoSearchWorkbench.tsx b/src/components/DoSearchWorkbench/DoSearchWorkbench.tsx index 4e2301b..7ffba63 100644 --- a/src/components/DoSearchWorkbench/DoSearchWorkbench.tsx +++ b/src/components/DoSearchWorkbench/DoSearchWorkbench.tsx @@ -45,7 +45,7 @@ type Props = { /** 明細頁路由前綴,預設 `/doworkbench`;在 `/do copy 2` 等別名頁面請傳對應 base */ workbenchHrefBase?: string; }; -type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "truckLanceCode" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" | "truckLanceCodeTo", string>; +type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "truckLanceCode" | "floor" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" | "truckLanceCodeTo" | "floorTo", string>; type SearchParamNames = keyof SearchBoxInputs; // put all this into a new component @@ -57,6 +57,9 @@ type EntryError = | undefined; type DoRow = TableRow, EntryError>; +function isTruckLaneSearchMissingEta(truckLanceCode: string, estimatedArrivalDate: string): boolean { + return truckLanceCode.trim() !== "" && estimatedArrivalDate.trim() === ""; +} const DoSearchWorkbench: React.FC = ({ filterArgs, @@ -77,8 +80,8 @@ const DoSearchWorkbench: React.FC = ({ const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; - console.log("🔍 DoSearch - session:", session); - console.log("🔍 DoSearch - currentUserId:", currentUserId); + //console.log("🔍 DoSearch - session:", session); + //console.log("🔍 DoSearch - currentUserId:", currentUserId); const [searchTimeout, setSearchTimeout] = useState(null); /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜尋結果視為「已選」以便跨頁記憶 */ const [excludedRowIds, setExcludedRowIds] = useState([]); @@ -100,6 +103,7 @@ const DoSearchWorkbench: React.FC = ({ shopName: "", deliveryOrderLines: "", truckLanceCode: "", // 添加这个字段 + floor: "All", codeTo: "", statusTo: "", estimatedArrivalDateTo: "", @@ -107,7 +111,8 @@ const DoSearchWorkbench: React.FC = ({ supplierNameTo: "", shopNameTo: "", deliveryOrderLinesTo: "", - truckLanceCodeTo: "" // 这个字段已经存在,但需要确保在类型定义中 + truckLanceCodeTo: "", + floorTo: "", }); const [hasSearched, setHasSearched] = useState(false); @@ -150,7 +155,14 @@ const DoSearchWorkbench: React.FC = ({ ...p, pageNum: 1, })); - }, [currentSearchParams.code, currentSearchParams.shopName, currentSearchParams.status, currentSearchParams.estimatedArrivalDate]); + }, [ + currentSearchParams.code, + currentSearchParams.shopName, + currentSearchParams.status, + currentSearchParams.estimatedArrivalDate, + currentSearchParams.truckLanceCode, + currentSearchParams.floor, + ]); const searchCriteria: Criterion[] = useMemo( @@ -158,6 +170,15 @@ const DoSearchWorkbench: React.FC = ({ { label: t("Code"), paramName: "code", type: "text" }, { label: t("Shop Name"), paramName: "shopName", type: "text" }, { label: t("Truck Lance Code"), paramName: "truckLanceCode", type: "text" }, + { + label: t("Floor"), + paramName: "floor", + type: "select-labelled", + options: [ + { label: "2F", value: "2F" }, + { label: "4F", value: "4F" }, + ], + }, { label: t("Estimated Arrival"), paramName: "estimatedArrivalDate", @@ -299,6 +320,16 @@ const DoSearchWorkbench: React.FC = ({ //SEARCH FUNCTION const handleSearch = useCallback(async (query: SearchBoxInputs) => { try { + if (isTruckLaneSearchMissingEta(query.truckLanceCode ?? "", query.estimatedArrivalDate ?? "")) { + await Swal.fire({ + icon: "warning", + title: t("Truck lane search requires date title"), + text: t("Truck lane search requires date message"), + confirmButtonText: t("Confirm"), + }); + return; + } + setCurrentSearchParams(query); let estArrStartDate = query.estimatedArrivalDate; @@ -315,6 +346,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { else{ status = query.status; } + + const floorParam = query.floor === "All" || !query.floor ? null : query.floor; // 调用新的 API,传入分页参数和 truckLanceCode const response = await fetchDoSearch( @@ -327,7 +360,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { "", // estArrEndDate - 不再使用 pagingController.pageNum, // 传入当前页码 pagingController.pageSize, // 传入每页大小 - query.truckLanceCode || "" // 添加 truckLanceCode 参数 + query.truckLanceCode || "", + floorParam, ); setSearchAllDos(response.records); @@ -344,7 +378,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { setHasResults(false); setExcludedRowIds([]); } -}, [pagingController]); +}, [pagingController, t]); useEffect(() => { if (typeof window !== 'undefined') { @@ -404,6 +438,20 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { // 使用新的分页参数重新搜索 const searchWithNewPage = async () => { try { + if ( + isTruckLaneSearchMissingEta( + currentSearchParams.truckLanceCode ?? "", + currentSearchParams.estimatedArrivalDate ?? "", + ) + ) { + await Swal.fire({ + icon: "warning", + title: t("Truck lane search requires date title"), + text: t("Truck lane search requires date message"), + confirmButtonText: t("Confirm"), + }); + return; + } let estArrStartDate = currentSearchParams.estimatedArrivalDate; const time = "T00:00:00"; @@ -418,6 +466,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { else{ status = currentSearchParams.status; } + + const floorParam = + currentSearchParams.floor === "All" || !currentSearchParams.floor + ? null + : currentSearchParams.floor; const response = await fetchDoSearch( currentSearchParams.code || "", @@ -429,7 +482,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { "", newPagingController.pageNum, newPagingController.pageSize, - currentSearchParams.truckLanceCode || "" // 添加这个参数 + currentSearchParams.truckLanceCode || "", + floorParam, ); setSearchAllDos(response.records); @@ -440,7 +494,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { }; searchWithNewPage(); } - }, [pagingController, hasSearched, currentSearchParams]); + }, [pagingController, hasSearched, currentSearchParams, t]); const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { const newPageSize = parseInt(event.target.value, 10); @@ -453,6 +507,20 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { if (hasSearched && currentSearchParams) { const searchWithNewPageSize = async () => { try { + if ( + isTruckLaneSearchMissingEta( + currentSearchParams.truckLanceCode ?? "", + currentSearchParams.estimatedArrivalDate ?? "", + ) + ) { + await Swal.fire({ + icon: "warning", + title: t("Truck lane search requires date title"), + text: t("Truck lane search requires date message"), + confirmButtonText: t("Confirm"), + }); + return; + } let estArrStartDate = currentSearchParams.estimatedArrivalDate; const time = "T00:00:00"; @@ -467,6 +535,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { else{ status = currentSearchParams.status; } + + const floorParam = + currentSearchParams.floor === "All" || !currentSearchParams.floor + ? null + : currentSearchParams.floor; const response = await fetchDoSearch( currentSearchParams.code || "", @@ -478,7 +551,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { "", 1, // 重置到第一页 newPageSize, - currentSearchParams.truckLanceCode || "" // 添加这个参数 + currentSearchParams.truckLanceCode || "", + floorParam, ); setSearchAllDos(response.records); @@ -489,10 +563,24 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { }; searchWithNewPageSize(); } - }, [hasSearched, currentSearchParams]); + }, [hasSearched, currentSearchParams, t]); const handleBatchRelease = useCallback(async () => { try { + if ( + isTruckLaneSearchMissingEta( + currentSearchParams.truckLanceCode ?? "", + currentSearchParams.estimatedArrivalDate ?? "", + ) + ) { + await Swal.fire({ + icon: "warning", + title: t("Truck lane search requires date title"), + text: t("Truck lane search requires date message"), + confirmButtonText: t("Confirm"), + }); + return; + } // 根据当前搜索条件获取所有匹配的记录(不分页) let estArrStartDate = currentSearchParams.estimatedArrivalDate; const time = "T00:00:00"; @@ -508,6 +596,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { else{ status = currentSearchParams.status; } + + const floorParam = + currentSearchParams.floor === "All" || !currentSearchParams.floor + ? null + : currentSearchParams.floor; // 显示加载提示 const loadingSwal = Swal.fire({ @@ -527,7 +620,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { currentSearchParams.shopName || "", status, estArrStartDate, - currentSearchParams.truckLanceCode || "" + currentSearchParams.truckLanceCode || "", + floorParam, ); Swal.close(); diff --git a/src/components/JoWorkbench/JoPickOrderList.tsx b/src/components/JoWorkbench/JoPickOrderList.tsx index 5f3a0be..f40abef 100644 --- a/src/components/JoWorkbench/JoPickOrderList.tsx +++ b/src/components/JoWorkbench/JoPickOrderList.tsx @@ -18,14 +18,16 @@ import { fetchAllJoPickOrders, AllJoPickOrderResponse } from "@/app/api/jo/actio import JobPickExecution from "./newJobPickExecution"; import SearchBox, { Criterion } from "../SearchBox"; import dayjs from "dayjs"; +import type { PrinterCombo } from "@/app/api/settings/printer"; interface Props { /** Reserved for tabs parity with Jodetail; not used in workbench list yet. */ onSwitchToRecordTab?: () => void; + printerCombo: PrinterCombo[]; } /** Jo workbench: same list + detail flow as Jodetail `JoPickOrderList`, detail uses `JoWorkbench/newJobPickExecution`. */ -const JoPickOrderList: React.FC = () => { +const JoPickOrderList: React.FC = ({ printerCombo }) => { const { t } = useTranslation(["common", "jo"]); const today = dayjs().format("YYYY-MM-DD"); const [loading, setLoading] = useState(false); @@ -153,6 +155,7 @@ const JoPickOrderList: React.FC = () => { ); diff --git a/src/components/JoWorkbench/JoWorkbenchTabs.tsx b/src/components/JoWorkbench/JoWorkbenchTabs.tsx index 4d07254..caa662a 100644 --- a/src/components/JoWorkbench/JoWorkbenchTabs.tsx +++ b/src/components/JoWorkbench/JoWorkbenchTabs.tsx @@ -51,7 +51,7 @@ const JoWorkbenchTabs: React.FC = ({ - + ); diff --git a/src/components/JoWorkbench/newJobPickExecution.tsx b/src/components/JoWorkbench/newJobPickExecution.tsx index 5c09d51..1579fb9 100644 --- a/src/components/JoWorkbench/newJobPickExecution.tsx +++ b/src/components/JoWorkbench/newJobPickExecution.tsx @@ -8,6 +8,7 @@ import { Typography, Alert, CircularProgress, + Autocomplete, Table, TableBody, TableCell, @@ -48,8 +49,9 @@ import { import { fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench, updateJoPickOrderHandledBy, - JobOrderLotsHierarchicalResponse, + JobOrderLotsHierarchicalWorkbenchResponse, applyPickExecutionHoldAndChecked, + PrintPickRecord, } from "@/app/api/jo/actions"; import { assignJobOrderPickOrderForWorkbench } from "@/app/api/jo/workbenchActions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; @@ -75,10 +77,13 @@ import { workbenchBatchScanPick, workbenchScanPick, } from "@/app/api/doworkbench/actions"; +import type { PrinterCombo } from "@/app/api/settings/printer"; +import { msg, msgError } from "@/components/Swal/CustomAlerts"; interface Props { filterArgs: Record; //onSwitchToRecordTab: () => void; onBackToList?: () => void; + printerCombo?: PrinterCombo[]; } /** 過期批號:與 noLot 類似——單筆/批量預設 0,除非 Issue 改數(對齊 GoodPickExecutiondetail) */ @@ -608,7 +613,7 @@ const QrCodeModal: React.FC<{ ); }; -const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { +const JobPickExecution: React.FC = ({ filterArgs, onBackToList, printerCombo = [] }) => { const workbenchMode = true; const { t } = useTranslation("jo"); const { t: tPick } = useTranslation("pickOrder"); @@ -640,7 +645,82 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const [qrScanErrorMsg, setQrScanErrorMsg] = useState(""); const [qrScanSuccess, setQrScanSuccess] = useState(false); const [jobOrderData, setJobOrderData] = - useState(null); + useState(null); + const a4Printers = useMemo( + () => + (printerCombo || []).filter((p) => + String(p.type || "") + .trim() + .toUpperCase() + .includes("A4"), + ), + [printerCombo], + ); + const printerOptions = useMemo( + () => (a4Printers.length > 0 ? a4Printers : printerCombo || []), + [a4Printers, printerCombo], + ); + const isPrinterComboMissing = printerCombo.length === 0; + const [selectedPrinter, setSelectedPrinter] = useState( + printerOptions.length > 0 ? printerOptions[0] : null, + ); + const [printQty, setPrintQty] = useState(1); + + useEffect(() => { + // Keep selected printer valid when combo list changes. + if (!printerOptions.length) { + setSelectedPrinter(null); + return; + } + setSelectedPrinter((prev) => { + if (!prev) return printerOptions[0]; + const stillExists = printerOptions.some((p) => p.id === prev.id); + return stillExists ? prev : printerOptions[0]; + }); + }, [printerOptions]); + + useEffect(() => { + console.log("[JO Workbench] printerCombo:", printerCombo); + console.log("[JO Workbench] a4Printers:", a4Printers); + console.log("[JO Workbench] printerOptions:", printerOptions); + }, [printerCombo, a4Printers, printerOptions]); + + const handlePickRecord = useCallback( + async (floor: "2F" | "3F" | "4F" | "ALL") => { + try { + const pickOrderId = jobOrderData?.pickOrder?.id; + if (!pickOrderId) { + msgError(t("Pick Order not found")); + return; + } + if (!selectedPrinter) { + msgError(t("Please select a printer first")); + return; + } + if (!printQty || printQty < 1) { + msgError(t("Please enter a valid print quantity (at least 1)")); + return; + } + + const response = await PrintPickRecord({ + pickOrderId, + printerId: selectedPrinter.id, + printQty, + floor, + }); + + if (response?.success) { + msg(t("Printed Successfully.")); + } else { + msgError(response?.message || t("Print failed")); + } + } catch (e) { + console.error(e); + msgError(t("An error occurred while printing")); + } + }, + [jobOrderData, printQty, selectedPrinter, t], + ); const workbenchStoreId = useMemo(() => { const po = jobOrderData?.pickOrder as | { storeId?: string | null } @@ -772,7 +852,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false); const getAllLotsFromHierarchical = useCallback( - (data: JobOrderLotsHierarchicalResponse | null): any[] => { + (data: JobOrderLotsHierarchicalWorkbenchResponse | null): any[] => { if (!data || !data.pickOrder || !data.pickOrderLines) { return []; } @@ -3573,29 +3653,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { return Math.max(0, requiredQty - stockOutLineQty); }, []); - // Search criteria - const searchCriteria: Criterion[] = [ - { - label: t("Pick Order Code"), - paramName: "pickOrderCode", - type: "text", - }, - { - label: t("Item Code"), - paramName: "itemCode", - type: "text", - }, - { - label: t("Item Name"), - paramName: "itemName", - type: "text", - }, - { - label: t("Lot No"), - paramName: "lotNo", - type: "text", - }, - ]; + const handlePageChange = useCallback((event: unknown, newPage: number) => { setPaginationController((prev) => ({ @@ -3829,11 +3887,91 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { {floor} ))} + + + + + {t("Select Printer")}: + + + option.name || option.label || option.code || `Printer ${option.id}` + } + value={selectedPrinter} + onChange={(_, newValue) => setSelectedPrinter(newValue)} + sx={{ minWidth: 220 }} + size="small" + renderInput={(params) => ( + + )} + /> + + {t("Print Quantity")}: + + { + const value = parseInt(e.target.value) || 1; + setPrintQty(Math.max(1, value)); + }} + inputProps={{ min: 1, step: 1 }} + sx={{ width: 120 }} + size="small" + /> + + + + + + {isPrinterComboMissing && ( + + {t("Printer list is empty")} + + )} {/* Job Order Header */} {jobOrderData && ( + + {t("Item Name")}:{" "} + {jobOrderData.pickOrder.jobOrder.itemCode || "-"}{" "}{jobOrderData.pickOrder.jobOrder.itemName || "-"} + {t("Job Order")}:{" "} {jobOrderData.pickOrder?.jobOrder?.code || "-"} diff --git a/src/components/Jodetail/JodetailSearch.tsx b/src/components/Jodetail/JodetailSearch.tsx index 83c9749..be37cf9 100644 --- a/src/components/Jodetail/JodetailSearch.tsx +++ b/src/components/Jodetail/JodetailSearch.tsx @@ -504,7 +504,7 @@ const JodetailSearch: React.FC = ({ printerCombo }) => { {/* Content section */} - {tabIndex === 0 && } + {tabIndex === 0 && } {tabIndex === 1 && (