| @@ -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 ( | |||
| <> | |||
| <PageTitleBar title={t("Job Order Pickexcution", { defaultValue: "Job Order Pickexcution" })} className="mb-4" /> | |||
| <I18nProvider namespaces={["jo", "common", "pickOrder", "purchaseOrder", "dashboard"]}> | |||
| <Suspense fallback={<GeneralLoading />}> | |||
| <JoPickOrderList /> | |||
| <JoPickOrderList printerCombo={printerCombo ?? []} /> | |||
| </Suspense> | |||
| </I18nProvider> | |||
| </> | |||
| @@ -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<JobOrderLotsHierarchicalResponse>( | |||
| return serverFetchJson<JobOrderLotsHierarchicalWorkbenchResponse>( | |||
| `${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order-workbench/${pickOrderId}`, | |||
| { | |||
| method: "GET", | |||
| @@ -40,8 +40,8 @@ const DoDetail: React.FC<Props> = ({ | |||
| 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<DoDetailType>({ | |||
| defaultValues: defaultValues | |||
| }) | |||
| @@ -43,7 +43,7 @@ type Props = { | |||
| searchQuery?: Record<string, any>; | |||
| 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<Partial<DoResult>, EntryError>; | |||
| /** 已填車線但未選預計送貨日:後端會掃全量再篩,需擋下。 */ | |||
| function isTruckLaneSearchMissingEta(truckLanceCode: string, estimatedArrivalDate: string): boolean { | |||
| return truckLanceCode.trim() !== "" && estimatedArrivalDate.trim() === ""; | |||
| } | |||
| const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSearch }) => { | |||
| const apiRef = useGridApiRef(); | |||
| @@ -70,8 +74,8 @@ const DoSearch: React.FC<Props> = ({ 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<NodeJS.Timeout | null>(null); | |||
| /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜尋結果視為「已選」以便跨頁記憶 */ | |||
| const [excludedRowIds, setExcludedRowIds] = useState<number[]>([]); | |||
| @@ -93,6 +97,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||
| shopName: "", | |||
| deliveryOrderLines: "", | |||
| truckLanceCode: "", // 添加这个字段 | |||
| floor: "All", | |||
| codeTo: "", | |||
| statusTo: "", | |||
| estimatedArrivalDateTo: "", | |||
| @@ -100,7 +105,8 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||
| supplierNameTo: "", | |||
| shopNameTo: "", | |||
| deliveryOrderLinesTo: "", | |||
| truckLanceCodeTo: "" // 这个字段已经存在,但需要确保在类型定义中 | |||
| truckLanceCodeTo: "", | |||
| floorTo: "", | |||
| }); | |||
| const [hasSearched, setHasSearched] = useState(false); | |||
| @@ -143,7 +149,14 @@ const DoSearch: React.FC<Props> = ({ 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<SearchParamNames>[] = useMemo( | |||
| @@ -151,6 +164,15 @@ const DoSearch: React.FC<Props> = ({ 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<Props> = ({ 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<HTMLInputElement>) => { | |||
| 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(); | |||
| @@ -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<Partial<DoResult>, EntryError>; | |||
| function isTruckLaneSearchMissingEta(truckLanceCode: string, estimatedArrivalDate: string): boolean { | |||
| return truckLanceCode.trim() !== "" && estimatedArrivalDate.trim() === ""; | |||
| } | |||
| const DoSearchWorkbench: React.FC<Props> = ({ | |||
| filterArgs, | |||
| @@ -77,8 +80,8 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||
| 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<NodeJS.Timeout | null>(null); | |||
| /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜尋結果視為「已選」以便跨頁記憶 */ | |||
| const [excludedRowIds, setExcludedRowIds] = useState<number[]>([]); | |||
| @@ -100,6 +103,7 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||
| shopName: "", | |||
| deliveryOrderLines: "", | |||
| truckLanceCode: "", // 添加这个字段 | |||
| floor: "All", | |||
| codeTo: "", | |||
| statusTo: "", | |||
| estimatedArrivalDateTo: "", | |||
| @@ -107,7 +111,8 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||
| supplierNameTo: "", | |||
| shopNameTo: "", | |||
| deliveryOrderLinesTo: "", | |||
| truckLanceCodeTo: "" // 这个字段已经存在,但需要确保在类型定义中 | |||
| truckLanceCodeTo: "", | |||
| floorTo: "", | |||
| }); | |||
| const [hasSearched, setHasSearched] = useState(false); | |||
| @@ -150,7 +155,14 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||
| ...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<SearchParamNames>[] = useMemo( | |||
| @@ -158,6 +170,15 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||
| { 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<Props> = ({ | |||
| //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<HTMLInputElement>) => { | |||
| 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(); | |||
| @@ -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<Props> = () => { | |||
| const JoPickOrderList: React.FC<Props> = ({ 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<Props> = () => { | |||
| <JobPickExecution | |||
| filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} | |||
| onBackToList={handleBackToList} | |||
| printerCombo={printerCombo} | |||
| /> | |||
| </Box> | |||
| ); | |||
| @@ -51,7 +51,7 @@ const JoWorkbenchTabs: React.FC<JoWorkbenchTabsProps> = ({ | |||
| </TabPanel> | |||
| <TabPanel value={tab} index={1}> | |||
| <JoPickOrderList /> | |||
| <JoPickOrderList printerCombo={printerCombo} /> | |||
| </TabPanel> | |||
| </Box> | |||
| ); | |||
| @@ -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<string, any>; | |||
| //onSwitchToRecordTab: () => void; | |||
| onBackToList?: () => void; | |||
| printerCombo?: PrinterCombo[]; | |||
| } | |||
| /** 過期批號:與 noLot 類似——單筆/批量預設 0,除非 Issue 改數(對齊 GoodPickExecutiondetail) */ | |||
| @@ -608,7 +613,7 @@ const QrCodeModal: React.FC<{ | |||
| ); | |||
| }; | |||
| const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||
| const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCombo = [] }) => { | |||
| const workbenchMode = true; | |||
| const { t } = useTranslation("jo"); | |||
| const { t: tPick } = useTranslation("pickOrder"); | |||
| @@ -640,7 +645,82 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||
| const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>(""); | |||
| const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false); | |||
| const [jobOrderData, setJobOrderData] = | |||
| useState<JobOrderLotsHierarchicalResponse | null>(null); | |||
| useState<JobOrderLotsHierarchicalWorkbenchResponse | null>(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<PrinterCombo | null>( | |||
| printerOptions.length > 0 ? printerOptions[0] : null, | |||
| ); | |||
| const [printQty, setPrintQty] = useState<number>(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<Props> = ({ 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<Props> = ({ filterArgs, onBackToList }) => { | |||
| return Math.max(0, requiredQty - stockOutLineQty); | |||
| }, []); | |||
| // Search criteria | |||
| const searchCriteria: Criterion<any>[] = [ | |||
| { | |||
| 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<Props> = ({ filterArgs, onBackToList }) => { | |||
| {floor} | |||
| </Button> | |||
| ))} | |||
| <Box sx={{ flex: 1 }} /> | |||
| <Typography variant="body2" sx={{ minWidth: "fit-content", mr: 1 }}> | |||
| {t("Select Printer")}: | |||
| </Typography> | |||
| <Autocomplete | |||
| options={printerOptions} | |||
| getOptionLabel={(option) => | |||
| option.name || option.label || option.code || `Printer ${option.id}` | |||
| } | |||
| value={selectedPrinter} | |||
| onChange={(_, newValue) => setSelectedPrinter(newValue)} | |||
| sx={{ minWidth: 220 }} | |||
| size="small" | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| placeholder={t("Printer")} | |||
| inputProps={{ ...params.inputProps, readOnly: true }} | |||
| /> | |||
| )} | |||
| /> | |||
| <Typography variant="body2" sx={{ minWidth: "fit-content", ml: 1 }}> | |||
| {t("Print Quantity")}: | |||
| </Typography> | |||
| <TextField | |||
| type="number" | |||
| label={t("Print Quantity")} | |||
| value={printQty} | |||
| onChange={(e) => { | |||
| const value = parseInt(e.target.value) || 1; | |||
| setPrintQty(Math.max(1, value)); | |||
| }} | |||
| inputProps={{ min: 1, step: 1 }} | |||
| sx={{ width: 120 }} | |||
| size="small" | |||
| /> | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| size="small" | |||
| onClick={() => handlePickRecord("ALL")} | |||
| > | |||
| {t("Print Pick Record")} ALL | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| size="small" | |||
| onClick={() => handlePickRecord("2F")} | |||
| > | |||
| {t("Print Pick Record")} 2F | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| size="small" | |||
| onClick={() => handlePickRecord("3F")} | |||
| > | |||
| {t("Print Pick Record")} 3F | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| size="small" | |||
| onClick={() => handlePickRecord("4F")} | |||
| > | |||
| {t("Print Pick Record")} 4F | |||
| </Button> | |||
| {isPrinterComboMissing && ( | |||
| <Alert severity="warning" sx={{ py: 0, px: 1 }}> | |||
| {t("Printer list is empty")} | |||
| </Alert> | |||
| )} | |||
| </Box> | |||
| {/* Job Order Header */} | |||
| {jobOrderData && ( | |||
| <Paper sx={{ p: 2 }}> | |||
| <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap"> | |||
| <Typography variant="subtitle1"> | |||
| <strong>{t("Item Name")}:</strong>{" "} | |||
| {jobOrderData.pickOrder.jobOrder.itemCode || "-"}{" "}{jobOrderData.pickOrder.jobOrder.itemName || "-"} | |||
| </Typography> | |||
| <Typography variant="subtitle1"> | |||
| <strong>{t("Job Order")}:</strong>{" "} | |||
| {jobOrderData.pickOrder?.jobOrder?.code || "-"} | |||
| @@ -504,7 +504,7 @@ const JodetailSearch: React.FC<Props> = ({ printerCombo }) => { | |||
| {/* Content section */} | |||
| <Box sx={{ p: 2 }}> | |||
| {tabIndex === 0 && <JoPickOrderList onSwitchToRecordTab={handleSwitchToRecordTab} />} | |||
| {tabIndex === 0 && <JoPickOrderList onSwitchToRecordTab={handleSwitchToRecordTab} printerCombo={printerCombo} />} | |||
| {tabIndex === 1 && ( | |||
| <CompleteJobOrderRecord | |||
| filterArgs={filterArgs} | |||
| @@ -15,6 +15,9 @@ | |||
| "do workbench": "新版成品出倉", | |||
| "Do Workbench": "新版成品出倉", | |||
| "Delivery Order Code": "送貨訂單編號", | |||
| "Floor": "樓層", | |||
| "Truck lane search requires date title": "需選擇預計送貨日期", | |||
| "Truck lane search requires date message": "已填寫車線號碼時,請一併選擇預計送貨日期後再搜尋。", | |||
| "Truck Lance Code": "車線號碼", | |||
| "Select Remark": "選擇備註", | |||
| "Confirm Assignment": "確認分配", | |||
| @@ -157,6 +157,7 @@ | |||
| "Target Date": "需求日期", | |||
| "Lot Required Pick Qty": "批號需求數", | |||
| "Available Qty": "可用數量", | |||
| "Job Order Match": "工單對料", | |||
| "Lot No": "批號", | |||
| "Submit Required Pick Qty": "提交需求數", | |||