| @@ -1,6 +1,7 @@ | |||||
| import GeneralLoading from "@/components/General/GeneralLoading"; | import GeneralLoading from "@/components/General/GeneralLoading"; | ||||
| import PageTitleBar from "@/components/PageTitleBar"; | import PageTitleBar from "@/components/PageTitleBar"; | ||||
| import JoPickOrderList from "@/components/JoWorkbench/JoPickOrderList"; | import JoPickOrderList from "@/components/JoWorkbench/JoPickOrderList"; | ||||
| import { fetchPrinterCombo } from "@/app/api/settings/printer"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | import { I18nProvider, getServerI18n } from "@/i18n"; | ||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| import React, { Suspense } from "react"; | import React, { Suspense } from "react"; | ||||
| @@ -11,13 +12,15 @@ export const metadata: Metadata = { | |||||
| const JoWorkbenchPage = async () => { | const JoWorkbenchPage = async () => { | ||||
| const { t } = await getServerI18n("jo"); | const { t } = await getServerI18n("jo"); | ||||
| const printerCombo = await fetchPrinterCombo(); | |||||
| //console.log("[JO Workbench Page] printerCombo count:", printerCombo?.length ?? 0); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <PageTitleBar title={t("Job Order Pickexcution", { defaultValue: "Job Order Pickexcution" })} className="mb-4" /> | <PageTitleBar title={t("Job Order Pickexcution", { defaultValue: "Job Order Pickexcution" })} className="mb-4" /> | ||||
| <I18nProvider namespaces={["jo", "common", "pickOrder", "purchaseOrder", "dashboard"]}> | <I18nProvider namespaces={["jo", "common", "pickOrder", "purchaseOrder", "dashboard"]}> | ||||
| <Suspense fallback={<GeneralLoading />}> | <Suspense fallback={<GeneralLoading />}> | ||||
| <JoPickOrderList /> | |||||
| <JoPickOrderList printerCombo={printerCombo ?? []} /> | |||||
| </Suspense> | </Suspense> | ||||
| </I18nProvider> | </I18nProvider> | ||||
| </> | </> | ||||
| @@ -567,6 +567,12 @@ export interface JobOrderLotsHierarchicalResponse { | |||||
| pickOrderLines: PickOrderLineWithLotsResponse[]; | 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 { | export interface PickOrderInfoResponse { | ||||
| id: number | null; | id: number | null; | ||||
| code: string | null; | code: string | null; | ||||
| @@ -578,12 +584,32 @@ export interface PickOrderInfoResponse { | |||||
| jobOrder: JobOrderBasicInfoResponse; | 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 { | export interface JobOrderBasicInfoResponse { | ||||
| id: number; | id: number; | ||||
| code: string; | code: string; | ||||
| name: 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 { | export interface PickOrderLineWithLotsResponse { | ||||
| id: number; | id: number; | ||||
| itemId: number | null; | itemId: number | null; | ||||
| @@ -724,7 +750,7 @@ export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrder | |||||
| export const fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench = cache( | export const fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench = cache( | ||||
| async (pickOrderId: number) => { | async (pickOrderId: number) => { | ||||
| return serverFetchJson<JobOrderLotsHierarchicalResponse>( | |||||
| return serverFetchJson<JobOrderLotsHierarchicalWorkbenchResponse>( | |||||
| `${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order-workbench/${pickOrderId}`, | `${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order-workbench/${pickOrderId}`, | ||||
| { | { | ||||
| method: "GET", | method: "GET", | ||||
| @@ -40,8 +40,8 @@ const DoDetail: React.FC<Props> = ({ | |||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; // Use correct session type | 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 | 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>({ | const formProps = useForm<DoDetailType>({ | ||||
| defaultValues: defaultValues | defaultValues: defaultValues | ||||
| }) | }) | ||||
| @@ -43,7 +43,7 @@ type Props = { | |||||
| searchQuery?: Record<string, any>; | searchQuery?: Record<string, any>; | ||||
| onDeliveryOrderSearch?: () => void; | 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; | type SearchParamNames = keyof SearchBoxInputs; | ||||
| // put all this into a new component | // put all this into a new component | ||||
| @@ -55,6 +55,10 @@ type EntryError = | |||||
| | undefined; | | undefined; | ||||
| type DoRow = TableRow<Partial<DoResult>, EntryError>; | 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 DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSearch }) => { | ||||
| const apiRef = useGridApiRef(); | const apiRef = useGridApiRef(); | ||||
| @@ -70,8 +74,8 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| const router = useRouter(); | const router = useRouter(); | ||||
| 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; | ||||
| 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); | const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null); | ||||
| /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜尋結果視為「已選」以便跨頁記憶 */ | /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜尋結果視為「已選」以便跨頁記憶 */ | ||||
| const [excludedRowIds, setExcludedRowIds] = useState<number[]>([]); | const [excludedRowIds, setExcludedRowIds] = useState<number[]>([]); | ||||
| @@ -93,6 +97,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| shopName: "", | shopName: "", | ||||
| deliveryOrderLines: "", | deliveryOrderLines: "", | ||||
| truckLanceCode: "", // 添加这个字段 | truckLanceCode: "", // 添加这个字段 | ||||
| floor: "All", | |||||
| codeTo: "", | codeTo: "", | ||||
| statusTo: "", | statusTo: "", | ||||
| estimatedArrivalDateTo: "", | estimatedArrivalDateTo: "", | ||||
| @@ -100,7 +105,8 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| supplierNameTo: "", | supplierNameTo: "", | ||||
| shopNameTo: "", | shopNameTo: "", | ||||
| deliveryOrderLinesTo: "", | deliveryOrderLinesTo: "", | ||||
| truckLanceCodeTo: "" // 这个字段已经存在,但需要确保在类型定义中 | |||||
| truckLanceCodeTo: "", | |||||
| floorTo: "", | |||||
| }); | }); | ||||
| const [hasSearched, setHasSearched] = useState(false); | const [hasSearched, setHasSearched] = useState(false); | ||||
| @@ -143,7 +149,14 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| ...p, | ...p, | ||||
| pageNum: 1, | 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( | 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("Code"), paramName: "code", type: "text" }, | ||||
| { label: t("Shop Name"), paramName: "shopName", type: "text" }, | { label: t("Shop Name"), paramName: "shopName", type: "text" }, | ||||
| { label: t("Truck Lance Code"), paramName: "truckLanceCode", 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"), | label: t("Estimated Arrival"), | ||||
| paramName: "estimatedArrivalDate", | paramName: "estimatedArrivalDate", | ||||
| @@ -297,6 +319,16 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea | |||||
| //SEARCH FUNCTION | //SEARCH FUNCTION | ||||
| const handleSearch = useCallback(async (query: SearchBoxInputs) => { | const handleSearch = useCallback(async (query: SearchBoxInputs) => { | ||||
| try { | 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); | setCurrentSearchParams(query); | ||||
| let estArrStartDate = query.estimatedArrivalDate; | let estArrStartDate = query.estimatedArrivalDate; | ||||
| @@ -313,6 +345,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| else{ | else{ | ||||
| status = query.status; | status = query.status; | ||||
| } | } | ||||
| const floorParam = query.floor === "All" || !query.floor ? null : query.floor; | |||||
| // 调用新的 API,传入分页参数和 truckLanceCode | // 调用新的 API,传入分页参数和 truckLanceCode | ||||
| const response = await fetchDoSearch( | const response = await fetchDoSearch( | ||||
| @@ -325,7 +359,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| "", // estArrEndDate - 不再使用 | "", // estArrEndDate - 不再使用 | ||||
| pagingController.pageNum, // 传入当前页码 | pagingController.pageNum, // 传入当前页码 | ||||
| pagingController.pageSize, // 传入每页大小 | pagingController.pageSize, // 传入每页大小 | ||||
| query.truckLanceCode || "" // 添加 truckLanceCode 参数 | |||||
| query.truckLanceCode || "", | |||||
| floorParam, | |||||
| ); | ); | ||||
| setSearchAllDos(response.records); | setSearchAllDos(response.records); | ||||
| @@ -342,7 +377,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| setHasResults(false); | setHasResults(false); | ||||
| setExcludedRowIds([]); | setExcludedRowIds([]); | ||||
| } | } | ||||
| }, [pagingController]); | |||||
| }, [pagingController, t]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (typeof window !== 'undefined') { | if (typeof window !== 'undefined') { | ||||
| @@ -402,6 +437,20 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| // 使用新的分页参数重新搜索 | // 使用新的分页参数重新搜索 | ||||
| const searchWithNewPage = async () => { | const searchWithNewPage = async () => { | ||||
| try { | 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; | let estArrStartDate = currentSearchParams.estimatedArrivalDate; | ||||
| const time = "T00:00:00"; | const time = "T00:00:00"; | ||||
| @@ -416,6 +465,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| else{ | else{ | ||||
| status = currentSearchParams.status; | status = currentSearchParams.status; | ||||
| } | } | ||||
| const floorParam = | |||||
| currentSearchParams.floor === "All" || !currentSearchParams.floor | |||||
| ? null | |||||
| : currentSearchParams.floor; | |||||
| const response = await fetchDoSearch( | const response = await fetchDoSearch( | ||||
| currentSearchParams.code || "", | currentSearchParams.code || "", | ||||
| @@ -427,7 +481,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| "", | "", | ||||
| newPagingController.pageNum, | newPagingController.pageNum, | ||||
| newPagingController.pageSize, | newPagingController.pageSize, | ||||
| currentSearchParams.truckLanceCode || "" // 添加这个参数 | |||||
| currentSearchParams.truckLanceCode || "", | |||||
| floorParam, | |||||
| ); | ); | ||||
| setSearchAllDos(response.records); | setSearchAllDos(response.records); | ||||
| @@ -438,7 +493,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| }; | }; | ||||
| searchWithNewPage(); | searchWithNewPage(); | ||||
| } | } | ||||
| }, [pagingController, hasSearched, currentSearchParams]); | |||||
| }, [pagingController, hasSearched, currentSearchParams, t]); | |||||
| const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | ||||
| const newPageSize = parseInt(event.target.value, 10); | const newPageSize = parseInt(event.target.value, 10); | ||||
| @@ -451,6 +506,20 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| if (hasSearched && currentSearchParams) { | if (hasSearched && currentSearchParams) { | ||||
| const searchWithNewPageSize = async () => { | const searchWithNewPageSize = async () => { | ||||
| try { | 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; | let estArrStartDate = currentSearchParams.estimatedArrivalDate; | ||||
| const time = "T00:00:00"; | const time = "T00:00:00"; | ||||
| @@ -465,6 +534,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| else{ | else{ | ||||
| status = currentSearchParams.status; | status = currentSearchParams.status; | ||||
| } | } | ||||
| const floorParam = | |||||
| currentSearchParams.floor === "All" || !currentSearchParams.floor | |||||
| ? null | |||||
| : currentSearchParams.floor; | |||||
| const response = await fetchDoSearch( | const response = await fetchDoSearch( | ||||
| currentSearchParams.code || "", | currentSearchParams.code || "", | ||||
| @@ -476,7 +550,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| "", | "", | ||||
| 1, // 重置到第一页 | 1, // 重置到第一页 | ||||
| newPageSize, | newPageSize, | ||||
| currentSearchParams.truckLanceCode || "" // 添加这个参数 | |||||
| currentSearchParams.truckLanceCode || "", | |||||
| floorParam, | |||||
| ); | ); | ||||
| setSearchAllDos(response.records); | setSearchAllDos(response.records); | ||||
| @@ -487,10 +562,24 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| }; | }; | ||||
| searchWithNewPageSize(); | searchWithNewPageSize(); | ||||
| } | } | ||||
| }, [hasSearched, currentSearchParams]); | |||||
| }, [hasSearched, currentSearchParams, t]); | |||||
| const handleBatchRelease = useCallback(async (isWorkbench: boolean) => { | const handleBatchRelease = useCallback(async (isWorkbench: boolean) => { | ||||
| try { | 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; | let estArrStartDate = currentSearchParams.estimatedArrivalDate; | ||||
| const time = "T00:00:00"; | const time = "T00:00:00"; | ||||
| @@ -506,6 +595,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| else{ | else{ | ||||
| status = currentSearchParams.status; | status = currentSearchParams.status; | ||||
| } | } | ||||
| const floorParam = | |||||
| currentSearchParams.floor === "All" || !currentSearchParams.floor | |||||
| ? null | |||||
| : currentSearchParams.floor; | |||||
| // 显示加载提示 | // 显示加载提示 | ||||
| const loadingSwal = Swal.fire({ | const loadingSwal = Swal.fire({ | ||||
| @@ -525,7 +619,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| currentSearchParams.shopName || "", | currentSearchParams.shopName || "", | ||||
| status, | status, | ||||
| estArrStartDate, | estArrStartDate, | ||||
| currentSearchParams.truckLanceCode || "" | |||||
| currentSearchParams.truckLanceCode || "", | |||||
| floorParam, | |||||
| ); | ); | ||||
| Swal.close(); | Swal.close(); | ||||
| @@ -45,7 +45,7 @@ type Props = { | |||||
| /** 明細頁路由前綴,預設 `/doworkbench`;在 `/do copy 2` 等別名頁面請傳對應 base */ | /** 明細頁路由前綴,預設 `/doworkbench`;在 `/do copy 2` 等別名頁面請傳對應 base */ | ||||
| workbenchHrefBase?: string; | 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; | type SearchParamNames = keyof SearchBoxInputs; | ||||
| // put all this into a new component | // put all this into a new component | ||||
| @@ -57,6 +57,9 @@ type EntryError = | |||||
| | undefined; | | undefined; | ||||
| type DoRow = TableRow<Partial<DoResult>, EntryError>; | type DoRow = TableRow<Partial<DoResult>, EntryError>; | ||||
| function isTruckLaneSearchMissingEta(truckLanceCode: string, estimatedArrivalDate: string): boolean { | |||||
| return truckLanceCode.trim() !== "" && estimatedArrivalDate.trim() === ""; | |||||
| } | |||||
| const DoSearchWorkbench: React.FC<Props> = ({ | const DoSearchWorkbench: React.FC<Props> = ({ | ||||
| filterArgs, | filterArgs, | ||||
| @@ -77,8 +80,8 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||||
| const router = useRouter(); | const router = useRouter(); | ||||
| 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; | ||||
| 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); | const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null); | ||||
| /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜尋結果視為「已選」以便跨頁記憶 */ | /** 使用者明確取消勾選的送貨單 id;未在此集合中的搜尋結果視為「已選」以便跨頁記憶 */ | ||||
| const [excludedRowIds, setExcludedRowIds] = useState<number[]>([]); | const [excludedRowIds, setExcludedRowIds] = useState<number[]>([]); | ||||
| @@ -100,6 +103,7 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||||
| shopName: "", | shopName: "", | ||||
| deliveryOrderLines: "", | deliveryOrderLines: "", | ||||
| truckLanceCode: "", // 添加这个字段 | truckLanceCode: "", // 添加这个字段 | ||||
| floor: "All", | |||||
| codeTo: "", | codeTo: "", | ||||
| statusTo: "", | statusTo: "", | ||||
| estimatedArrivalDateTo: "", | estimatedArrivalDateTo: "", | ||||
| @@ -107,7 +111,8 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||||
| supplierNameTo: "", | supplierNameTo: "", | ||||
| shopNameTo: "", | shopNameTo: "", | ||||
| deliveryOrderLinesTo: "", | deliveryOrderLinesTo: "", | ||||
| truckLanceCodeTo: "" // 这个字段已经存在,但需要确保在类型定义中 | |||||
| truckLanceCodeTo: "", | |||||
| floorTo: "", | |||||
| }); | }); | ||||
| const [hasSearched, setHasSearched] = useState(false); | const [hasSearched, setHasSearched] = useState(false); | ||||
| @@ -150,7 +155,14 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||||
| ...p, | ...p, | ||||
| pageNum: 1, | 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( | const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | ||||
| @@ -158,6 +170,15 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||||
| { label: t("Code"), paramName: "code", type: "text" }, | { label: t("Code"), paramName: "code", type: "text" }, | ||||
| { label: t("Shop Name"), paramName: "shopName", type: "text" }, | { label: t("Shop Name"), paramName: "shopName", type: "text" }, | ||||
| { label: t("Truck Lance Code"), paramName: "truckLanceCode", 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"), | label: t("Estimated Arrival"), | ||||
| paramName: "estimatedArrivalDate", | paramName: "estimatedArrivalDate", | ||||
| @@ -299,6 +320,16 @@ const DoSearchWorkbench: React.FC<Props> = ({ | |||||
| //SEARCH FUNCTION | //SEARCH FUNCTION | ||||
| const handleSearch = useCallback(async (query: SearchBoxInputs) => { | const handleSearch = useCallback(async (query: SearchBoxInputs) => { | ||||
| try { | 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); | setCurrentSearchParams(query); | ||||
| let estArrStartDate = query.estimatedArrivalDate; | let estArrStartDate = query.estimatedArrivalDate; | ||||
| @@ -315,6 +346,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| else{ | else{ | ||||
| status = query.status; | status = query.status; | ||||
| } | } | ||||
| const floorParam = query.floor === "All" || !query.floor ? null : query.floor; | |||||
| // 调用新的 API,传入分页参数和 truckLanceCode | // 调用新的 API,传入分页参数和 truckLanceCode | ||||
| const response = await fetchDoSearch( | const response = await fetchDoSearch( | ||||
| @@ -327,7 +360,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| "", // estArrEndDate - 不再使用 | "", // estArrEndDate - 不再使用 | ||||
| pagingController.pageNum, // 传入当前页码 | pagingController.pageNum, // 传入当前页码 | ||||
| pagingController.pageSize, // 传入每页大小 | pagingController.pageSize, // 传入每页大小 | ||||
| query.truckLanceCode || "" // 添加 truckLanceCode 参数 | |||||
| query.truckLanceCode || "", | |||||
| floorParam, | |||||
| ); | ); | ||||
| setSearchAllDos(response.records); | setSearchAllDos(response.records); | ||||
| @@ -344,7 +378,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| setHasResults(false); | setHasResults(false); | ||||
| setExcludedRowIds([]); | setExcludedRowIds([]); | ||||
| } | } | ||||
| }, [pagingController]); | |||||
| }, [pagingController, t]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (typeof window !== 'undefined') { | if (typeof window !== 'undefined') { | ||||
| @@ -404,6 +438,20 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| // 使用新的分页参数重新搜索 | // 使用新的分页参数重新搜索 | ||||
| const searchWithNewPage = async () => { | const searchWithNewPage = async () => { | ||||
| try { | 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; | let estArrStartDate = currentSearchParams.estimatedArrivalDate; | ||||
| const time = "T00:00:00"; | const time = "T00:00:00"; | ||||
| @@ -418,6 +466,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| else{ | else{ | ||||
| status = currentSearchParams.status; | status = currentSearchParams.status; | ||||
| } | } | ||||
| const floorParam = | |||||
| currentSearchParams.floor === "All" || !currentSearchParams.floor | |||||
| ? null | |||||
| : currentSearchParams.floor; | |||||
| const response = await fetchDoSearch( | const response = await fetchDoSearch( | ||||
| currentSearchParams.code || "", | currentSearchParams.code || "", | ||||
| @@ -429,7 +482,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| "", | "", | ||||
| newPagingController.pageNum, | newPagingController.pageNum, | ||||
| newPagingController.pageSize, | newPagingController.pageSize, | ||||
| currentSearchParams.truckLanceCode || "" // 添加这个参数 | |||||
| currentSearchParams.truckLanceCode || "", | |||||
| floorParam, | |||||
| ); | ); | ||||
| setSearchAllDos(response.records); | setSearchAllDos(response.records); | ||||
| @@ -440,7 +494,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| }; | }; | ||||
| searchWithNewPage(); | searchWithNewPage(); | ||||
| } | } | ||||
| }, [pagingController, hasSearched, currentSearchParams]); | |||||
| }, [pagingController, hasSearched, currentSearchParams, t]); | |||||
| const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | ||||
| const newPageSize = parseInt(event.target.value, 10); | const newPageSize = parseInt(event.target.value, 10); | ||||
| @@ -453,6 +507,20 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| if (hasSearched && currentSearchParams) { | if (hasSearched && currentSearchParams) { | ||||
| const searchWithNewPageSize = async () => { | const searchWithNewPageSize = async () => { | ||||
| try { | 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; | let estArrStartDate = currentSearchParams.estimatedArrivalDate; | ||||
| const time = "T00:00:00"; | const time = "T00:00:00"; | ||||
| @@ -467,6 +535,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| else{ | else{ | ||||
| status = currentSearchParams.status; | status = currentSearchParams.status; | ||||
| } | } | ||||
| const floorParam = | |||||
| currentSearchParams.floor === "All" || !currentSearchParams.floor | |||||
| ? null | |||||
| : currentSearchParams.floor; | |||||
| const response = await fetchDoSearch( | const response = await fetchDoSearch( | ||||
| currentSearchParams.code || "", | currentSearchParams.code || "", | ||||
| @@ -478,7 +551,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| "", | "", | ||||
| 1, // 重置到第一页 | 1, // 重置到第一页 | ||||
| newPageSize, | newPageSize, | ||||
| currentSearchParams.truckLanceCode || "" // 添加这个参数 | |||||
| currentSearchParams.truckLanceCode || "", | |||||
| floorParam, | |||||
| ); | ); | ||||
| setSearchAllDos(response.records); | setSearchAllDos(response.records); | ||||
| @@ -489,10 +563,24 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| }; | }; | ||||
| searchWithNewPageSize(); | searchWithNewPageSize(); | ||||
| } | } | ||||
| }, [hasSearched, currentSearchParams]); | |||||
| }, [hasSearched, currentSearchParams, t]); | |||||
| const handleBatchRelease = useCallback(async () => { | const handleBatchRelease = useCallback(async () => { | ||||
| try { | 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; | let estArrStartDate = currentSearchParams.estimatedArrivalDate; | ||||
| const time = "T00:00:00"; | const time = "T00:00:00"; | ||||
| @@ -508,6 +596,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| else{ | else{ | ||||
| status = currentSearchParams.status; | status = currentSearchParams.status; | ||||
| } | } | ||||
| const floorParam = | |||||
| currentSearchParams.floor === "All" || !currentSearchParams.floor | |||||
| ? null | |||||
| : currentSearchParams.floor; | |||||
| // 显示加载提示 | // 显示加载提示 | ||||
| const loadingSwal = Swal.fire({ | const loadingSwal = Swal.fire({ | ||||
| @@ -527,7 +620,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||||
| currentSearchParams.shopName || "", | currentSearchParams.shopName || "", | ||||
| status, | status, | ||||
| estArrStartDate, | estArrStartDate, | ||||
| currentSearchParams.truckLanceCode || "" | |||||
| currentSearchParams.truckLanceCode || "", | |||||
| floorParam, | |||||
| ); | ); | ||||
| Swal.close(); | Swal.close(); | ||||
| @@ -18,14 +18,16 @@ import { fetchAllJoPickOrders, AllJoPickOrderResponse } from "@/app/api/jo/actio | |||||
| import JobPickExecution from "./newJobPickExecution"; | import JobPickExecution from "./newJobPickExecution"; | ||||
| import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import type { PrinterCombo } from "@/app/api/settings/printer"; | |||||
| interface Props { | interface Props { | ||||
| /** Reserved for tabs parity with Jodetail; not used in workbench list yet. */ | /** Reserved for tabs parity with Jodetail; not used in workbench list yet. */ | ||||
| onSwitchToRecordTab?: () => void; | onSwitchToRecordTab?: () => void; | ||||
| printerCombo: PrinterCombo[]; | |||||
| } | } | ||||
| /** Jo workbench: same list + detail flow as Jodetail `JoPickOrderList`, detail uses `JoWorkbench/newJobPickExecution`. */ | /** 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 { t } = useTranslation(["common", "jo"]); | ||||
| const today = dayjs().format("YYYY-MM-DD"); | const today = dayjs().format("YYYY-MM-DD"); | ||||
| const [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||||
| @@ -153,6 +155,7 @@ const JoPickOrderList: React.FC<Props> = () => { | |||||
| <JobPickExecution | <JobPickExecution | ||||
| filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} | filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} | ||||
| onBackToList={handleBackToList} | onBackToList={handleBackToList} | ||||
| printerCombo={printerCombo} | |||||
| /> | /> | ||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| @@ -51,7 +51,7 @@ const JoWorkbenchTabs: React.FC<JoWorkbenchTabsProps> = ({ | |||||
| </TabPanel> | </TabPanel> | ||||
| <TabPanel value={tab} index={1}> | <TabPanel value={tab} index={1}> | ||||
| <JoPickOrderList /> | |||||
| <JoPickOrderList printerCombo={printerCombo} /> | |||||
| </TabPanel> | </TabPanel> | ||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| @@ -8,6 +8,7 @@ import { | |||||
| Typography, | Typography, | ||||
| Alert, | Alert, | ||||
| CircularProgress, | CircularProgress, | ||||
| Autocomplete, | |||||
| Table, | Table, | ||||
| TableBody, | TableBody, | ||||
| TableCell, | TableCell, | ||||
| @@ -48,8 +49,9 @@ import { | |||||
| import { | import { | ||||
| fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench, | fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench, | ||||
| updateJoPickOrderHandledBy, | updateJoPickOrderHandledBy, | ||||
| JobOrderLotsHierarchicalResponse, | |||||
| JobOrderLotsHierarchicalWorkbenchResponse, | |||||
| applyPickExecutionHoldAndChecked, | applyPickExecutionHoldAndChecked, | ||||
| PrintPickRecord, | |||||
| } from "@/app/api/jo/actions"; | } from "@/app/api/jo/actions"; | ||||
| import { assignJobOrderPickOrderForWorkbench } from "@/app/api/jo/workbenchActions"; | import { assignJobOrderPickOrderForWorkbench } from "@/app/api/jo/workbenchActions"; | ||||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | import { fetchNameList, NameList } from "@/app/api/user/actions"; | ||||
| @@ -75,10 +77,13 @@ import { | |||||
| workbenchBatchScanPick, | workbenchBatchScanPick, | ||||
| workbenchScanPick, | workbenchScanPick, | ||||
| } from "@/app/api/doworkbench/actions"; | } from "@/app/api/doworkbench/actions"; | ||||
| import type { PrinterCombo } from "@/app/api/settings/printer"; | |||||
| import { msg, msgError } from "@/components/Swal/CustomAlerts"; | |||||
| interface Props { | interface Props { | ||||
| filterArgs: Record<string, any>; | filterArgs: Record<string, any>; | ||||
| //onSwitchToRecordTab: () => void; | //onSwitchToRecordTab: () => void; | ||||
| onBackToList?: () => void; | onBackToList?: () => void; | ||||
| printerCombo?: PrinterCombo[]; | |||||
| } | } | ||||
| /** 過期批號:與 noLot 類似——單筆/批量預設 0,除非 Issue 改數(對齊 GoodPickExecutiondetail) */ | /** 過期批號:與 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 workbenchMode = true; | ||||
| const { t } = useTranslation("jo"); | const { t } = useTranslation("jo"); | ||||
| const { t: tPick } = useTranslation("pickOrder"); | const { t: tPick } = useTranslation("pickOrder"); | ||||
| @@ -640,7 +645,82 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>(""); | const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>(""); | ||||
| const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false); | const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false); | ||||
| const [jobOrderData, setJobOrderData] = | 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 workbenchStoreId = useMemo(() => { | ||||
| const po = jobOrderData?.pickOrder as | const po = jobOrderData?.pickOrder as | ||||
| | { storeId?: string | null } | | { storeId?: string | null } | ||||
| @@ -772,7 +852,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = | const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = | ||||
| useState(false); | useState(false); | ||||
| const getAllLotsFromHierarchical = useCallback( | const getAllLotsFromHierarchical = useCallback( | ||||
| (data: JobOrderLotsHierarchicalResponse | null): any[] => { | |||||
| (data: JobOrderLotsHierarchicalWorkbenchResponse | null): any[] => { | |||||
| if (!data || !data.pickOrder || !data.pickOrderLines) { | if (!data || !data.pickOrder || !data.pickOrderLines) { | ||||
| return []; | return []; | ||||
| } | } | ||||
| @@ -3573,29 +3653,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| return Math.max(0, requiredQty - stockOutLineQty); | 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) => { | const handlePageChange = useCallback((event: unknown, newPage: number) => { | ||||
| setPaginationController((prev) => ({ | setPaginationController((prev) => ({ | ||||
| @@ -3829,11 +3887,91 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| {floor} | {floor} | ||||
| </Button> | </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> | </Box> | ||||
| {/* Job Order Header */} | {/* Job Order Header */} | ||||
| {jobOrderData && ( | {jobOrderData && ( | ||||
| <Paper sx={{ p: 2 }}> | <Paper sx={{ p: 2 }}> | ||||
| <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap"> | <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"> | <Typography variant="subtitle1"> | ||||
| <strong>{t("Job Order")}:</strong>{" "} | <strong>{t("Job Order")}:</strong>{" "} | ||||
| {jobOrderData.pickOrder?.jobOrder?.code || "-"} | {jobOrderData.pickOrder?.jobOrder?.code || "-"} | ||||
| @@ -504,7 +504,7 @@ const JodetailSearch: React.FC<Props> = ({ printerCombo }) => { | |||||
| {/* Content section */} | {/* Content section */} | ||||
| <Box sx={{ p: 2 }}> | <Box sx={{ p: 2 }}> | ||||
| {tabIndex === 0 && <JoPickOrderList onSwitchToRecordTab={handleSwitchToRecordTab} />} | |||||
| {tabIndex === 0 && <JoPickOrderList onSwitchToRecordTab={handleSwitchToRecordTab} printerCombo={printerCombo} />} | |||||
| {tabIndex === 1 && ( | {tabIndex === 1 && ( | ||||
| <CompleteJobOrderRecord | <CompleteJobOrderRecord | ||||
| filterArgs={filterArgs} | filterArgs={filterArgs} | ||||
| @@ -15,6 +15,9 @@ | |||||
| "do workbench": "新版成品出倉", | "do workbench": "新版成品出倉", | ||||
| "Do Workbench": "新版成品出倉", | "Do Workbench": "新版成品出倉", | ||||
| "Delivery Order Code": "送貨訂單編號", | "Delivery Order Code": "送貨訂單編號", | ||||
| "Floor": "樓層", | |||||
| "Truck lane search requires date title": "需選擇預計送貨日期", | |||||
| "Truck lane search requires date message": "已填寫車線號碼時,請一併選擇預計送貨日期後再搜尋。", | |||||
| "Truck Lance Code": "車線號碼", | "Truck Lance Code": "車線號碼", | ||||
| "Select Remark": "選擇備註", | "Select Remark": "選擇備註", | ||||
| "Confirm Assignment": "確認分配", | "Confirm Assignment": "確認分配", | ||||
| @@ -157,6 +157,7 @@ | |||||
| "Target Date": "需求日期", | "Target Date": "需求日期", | ||||
| "Lot Required Pick Qty": "批號需求數", | "Lot Required Pick Qty": "批號需求數", | ||||
| "Available Qty": "可用數量", | "Available Qty": "可用數量", | ||||
| "Job Order Match": "工單對料", | "Job Order Match": "工單對料", | ||||
| "Lot No": "批號", | "Lot No": "批號", | ||||
| "Submit Required Pick Qty": "提交需求數", | "Submit Required Pick Qty": "提交需求數", | ||||