From 6159a435b8841a42c5bac03e479d7bc1cf36cee0 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 5 May 2026 20:51:16 +0800 Subject: [PATCH] update translate, do finish jump page, fix jo expiry can not submit, --- src/app/api/pickOrder/actions.ts | 8 ++- .../DoWorkbench/DoWorkbenchTabs.tsx | 61 +++++++++++++++++-- .../GoodPickExecutionWorkbenchRecord.tsx | 28 +++++++-- .../WorkbenchGoodPickExecutionDetail.tsx | 38 ++++++++++-- .../JoWorkbench/JoPickOrderList.tsx | 6 +- .../JoWorkbench/newJobPickExecution.tsx | 37 +++++++---- .../Jodetail/FinishedGoodSearchWrapper.tsx | 2 +- .../ProductionProcessDetail.tsx | 2 +- src/i18n/zh/common.json | 6 +- src/i18n/zh/jo.json | 2 + src/i18n/zh/pickOrder.json | 1 + 11 files changed, 159 insertions(+), 32 deletions(-) diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts index c2bce38..17f0401 100644 --- a/src/app/api/pickOrder/actions.ts +++ b/src/app/api/pickOrder/actions.ts @@ -381,6 +381,7 @@ export interface CompletedDoPickOrderSearchParams { deliveryNoteCode?: string; /** 卡車/車道(後端 truckLanceCode 模糊匹配) */ truckLanceCode?: string; + ticketNo?: string; } export interface PickExecutionIssue { id: number; @@ -713,6 +714,9 @@ export const fetchCompletedDoPickOrdersWorkbench = async ( if (searchParams?.truckLanceCode) { params.append("truckLanceCode", searchParams.truckLanceCode); } + if (searchParams?.ticketNo) { + params.append("ticketNo", searchParams.ticketNo); + } const queryString = params.toString(); const url = `${BASE_API_URL}/pickOrder/completed-do-pick-orders-workbench/${userId}${ @@ -742,7 +746,9 @@ export const fetchCompletedDoPickOrdersWorkbenchAll = async ( if (searchParams?.truckLanceCode) { params.append("truckLanceCode", searchParams.truckLanceCode); } - + if (searchParams?.ticketNo) { + params.append("ticketNo", searchParams.ticketNo); + } const queryString = params.toString(); const url = `${BASE_API_URL}/pickOrder/completed-do-pick-orders-workbench-all${ queryString ? `?${queryString}` : "" diff --git a/src/components/DoWorkbench/DoWorkbenchTabs.tsx b/src/components/DoWorkbench/DoWorkbenchTabs.tsx index 83ef702..ae44f60 100644 --- a/src/components/DoWorkbench/DoWorkbenchTabs.tsx +++ b/src/components/DoWorkbench/DoWorkbenchTabs.tsx @@ -1,7 +1,8 @@ "use client"; -import { Autocomplete, Box, Tab, Tabs, TextField, Typography } from "@mui/material"; -import React from "react"; +import { Autocomplete, Box, CircularProgress, Tab, Tabs, TextField, Typography } from "@mui/material"; +import React, { Suspense } from "react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; import DoWorkbenchPickShell from "./DoWorkbenchPickShell"; import type { PrinterCombo } from "@/app/api/settings/printer"; import GoodPickExecutionWorkbenchRecord from "./GoodPickExecutionWorkbenchRecord"; @@ -14,6 +15,9 @@ import { fetchWorkbenchReleasedDoPickOrdersForSelectionToday } from "@/app/api/d import { Button } from "@mui/material"; import FinishedGoodCartonDashboardTab from "../FinishedGoodSearch/FinishedGoodCartonDashboardTab"; import TruckRoutingSummaryTabWorkbench from "./TruckRoutingSummaryTabWorkbench"; + +const ALLOWED_WORKBENCH_TABS = new Set([0, 1, 2, 3, 5, 6]); + type Props = { defaultTabIndex?: 0 | 1; printerCombo?: PrinterCombo[]; @@ -25,7 +29,18 @@ function TabPanel(props: { value: number; index: number; children: React.ReactNo return {children}; } -const DoWorkbenchTabs: React.FC = ({ defaultTabIndex = 0, printerCombo = [] }) => { +const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCombo = [] }) => { + const searchParams = useSearchParams(); + const router = useRouter(); + const pathname = usePathname(); + + const urlTabStr = searchParams.get("tab"); + const urlTicketRaw = searchParams.get("ticketNo"); + const urlTicketNo = + urlTicketRaw && urlTicketRaw.trim() !== "" + ? decodeURIComponent(urlTicketRaw.trim()) + : null; + const [tab, setTab] = React.useState(defaultTabIndex); const [a4Printer, setA4Printer] = React.useState(null); const [labelPrinter, setLabelPrinter] = React.useState(null); @@ -54,6 +69,28 @@ const DoWorkbenchTabs: React.FC = ({ defaultTabIndex = 0, printerCombo = void fetchReleasedOrderCount(); }, [fetchReleasedOrderCount]); + React.useEffect(() => { + if (urlTabStr == null || urlTabStr === "") return; + const n = parseInt(urlTabStr, 10); + if (!Number.isNaN(n) && ALLOWED_WORKBENCH_TABS.has(n)) { + setTab(n); + } + }, [urlTabStr]); + + const handleTabChange = React.useCallback( + (_: React.SyntheticEvent, newTab: number) => { + setTab(newTab); + const params = new URLSearchParams(searchParams.toString()); + params.set("tab", String(newTab)); + if (newTab !== 1) { + params.delete("ticketNo"); + } + const qs = params.toString(); + router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false }); + }, + [pathname, router, searchParams], + ); + const handleAllDraft = React.useCallback(async () => { try { if (!a4Printer) { @@ -186,7 +223,7 @@ const DoWorkbenchTabs: React.FC = ({ defaultTabIndex = 0, printerCombo = {`${t("Print All Draft")} (${releasedOrderCount})`} - setTab(v)} sx={{ borderBottom: 1, borderColor: "divider" }}> + @@ -200,18 +237,22 @@ const DoWorkbenchTabs: React.FC = ({ defaultTabIndex = 0, printerCombo = @@ -228,5 +269,17 @@ const DoWorkbenchTabs: React.FC = ({ defaultTabIndex = 0, printerCombo = ); }; +const DoWorkbenchTabs: React.FC = (props) => ( + + + + } + > + + +); + export default DoWorkbenchTabs; diff --git a/src/components/DoWorkbench/GoodPickExecutionWorkbenchRecord.tsx b/src/components/DoWorkbench/GoodPickExecutionWorkbenchRecord.tsx index 7c2e9fd..ec5eb8b 100644 --- a/src/components/DoWorkbench/GoodPickExecutionWorkbenchRecord.tsx +++ b/src/components/DoWorkbench/GoodPickExecutionWorkbenchRecord.tsx @@ -44,6 +44,7 @@ type Props = { listScope?: "mine" | "all"; a4Printer: PrinterCombo | null; labelPrinter: PrinterCombo | null; + initialTicketNo?: string | null; }; const GoodPickExecutionWorkbenchRecord: React.FC = ({ @@ -51,6 +52,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC = ({ listScope = "mine", a4Printer, labelPrinter, + initialTicketNo, }) => { const { t } = useTranslation("pickOrder"); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -70,6 +72,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC = ({ shopName?: string; deliveryNoteCode?: string; truckLanceCode?: string; + ticketNo?: string; }) => { setLoading(true); try { @@ -89,8 +92,13 @@ const GoodPickExecutionWorkbenchRecord: React.FC = ({ }, [currentUserId, listScope]); useEffect(() => { - void loadData({ targetDate: dayjs().format("YYYY-MM-DD") }); - }, [loadData]); + const today = dayjs().format("YYYY-MM-DD"); + const tn = initialTicketNo?.trim() || undefined; + void loadData({ + targetDate: today, + ...(tn ? { ticketNo: tn } : {}), + }); + }, [loadData, initialTicketNo]); const searchCriteria: Criterion[] = useMemo( () => [ @@ -115,8 +123,16 @@ const GoodPickExecutionWorkbenchRecord: React.FC = ({ type: "date", defaultValue: dayjs().format("YYYY-MM-DD"), }, + { + label: t("Ticket No"), + paramName: "ticketNo", + type: "text", + ...(initialTicketNo?.trim() + ? { preFilledValue: initialTicketNo.trim() } + : {}), + }, ], - [t], + [t, initialTicketNo], ); const handleSearch = useCallback((query: Record) => { @@ -126,6 +142,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC = ({ shopName: query.shopName || undefined, deliveryNoteCode: query.deliveryNoteCode || undefined, truckLanceCode: query.truckLanceCode || undefined, + ticketNo: query.ticketNo || undefined, }); }, [loadData]); @@ -582,10 +599,10 @@ const GoodPickExecutionWorkbenchRecord: React.FC = ({ = ({ {row.deliveryNoteCode || "-"} + + {row.ticketNo || "-"} + {row.shopName} diff --git a/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx b/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx index 178edbd..0d7a1a5 100644 --- a/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx +++ b/src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx @@ -25,7 +25,7 @@ import TestQrCodeProvider from "@/components/QrCodeScannerProvider/TestQrCodePro import { fetchLotDetail } from "@/app/api/inventory/actions"; import React, { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react"; import { useTranslation } from "react-i18next"; -import { useRouter } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { updateStockOutLineStatus, createStockOutLine, @@ -383,6 +383,7 @@ const WorkbenchGoodPickExecutionDetail: React.FC = ({ const workbenchMode = true; const { t } = useTranslation("pickOrder"); const router = useRouter(); + const pathname = usePathname(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const [doPickOrderDetail, setDoPickOrderDetail] = useState(null); const [selectedPickOrderId, setSelectedPickOrderId] = useState(null); @@ -519,6 +520,10 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); const autoAssignRef = useRef(false); /** 曾成功載入過 workbench 階層資料;避免「列表仍有單但階層暫空」時對外層重複觸發造成迴圈 */ const workbenchHierarchicalReadyRef = useRef(false); + /** 最後一筆 workbench 票號(階層清空或完成後仍可用於導向完成紀錄) */ + const lastWorkbenchTicketNoRef = useRef(null); + /** 同一筆揀貨完成後只導向「完成紀錄」分頁一次 */ + const workbenchFinishNavigateDoneRef = useRef(false); const formProps = useForm(); const errors = formProps.formState.errors; @@ -718,15 +723,31 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO // 检查数据结构 if (!hierarchicalData?.fgInfo || !hierarchicalData.pickOrders?.length) { console.warn("⚠️ No FG info or pick orders found"); + const hadWorkbenchData = workbenchHierarchicalReadyRef.current; + const ticketForRedirect = + String(hierarchicalData?.fgInfo?.ticketNo ?? "").trim() || + lastWorkbenchTicketNoRef.current || + ""; setCombinedLotData([]); setOriginalCombinedData([]); setAllLotsCompleted(false); setIssuePickedQtyBySolId({}); setFgPickOrders([]); - if (workbenchHierarchicalReadyRef.current) { - workbenchHierarchicalReadyRef.current = false; + workbenchHierarchicalReadyRef.current = false; + if (hadWorkbenchData) { onWorkbenchHierarchyEmpty?.(); } + if ( + hadWorkbenchData && + ticketForRedirect && + !workbenchFinishNavigateDoneRef.current + ) { + workbenchFinishNavigateDoneRef.current = true; + router.replace( + `${pathname}?tab=1&ticketNo=${encodeURIComponent(ticketForRedirect)}`, + { scroll: false }, + ); + } return; } @@ -779,6 +800,9 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO setFgPickOrders([fgOrder]); workbenchHierarchicalReadyRef.current = true; + lastWorkbenchTicketNoRef.current = + String(fgOrder.ticketNo ?? "").trim() || null; + workbenchFinishNavigateDoneRef.current = false; console.log(" DEBUG fgOrder.lineCountsPerPickOrder:", fgOrder.lineCountsPerPickOrder); console.log(" DEBUG fgOrder.pickOrderCodes:", fgOrder.pickOrderCodes); console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); @@ -967,7 +991,13 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO } finally { setCombinedDataLoading(false); } -}, [currentUserId, checkAllLotsCompleted, onWorkbenchHierarchyEmpty]); // 移除 selectedPickOrderId 依赖 +}, [ + currentUserId, + checkAllLotsCompleted, + onWorkbenchHierarchyEmpty, + router, + pathname, +]); // 移除 selectedPickOrderId 依赖 /** After workbench scan-pick (incl. split → new stock_out_line), reload hierarchical rows. */ const refreshWorkbenchAfterScanPick = useCallback(async () => { diff --git a/src/components/JoWorkbench/JoPickOrderList.tsx b/src/components/JoWorkbench/JoPickOrderList.tsx index 01803ba..5f3a0be 100644 --- a/src/components/JoWorkbench/JoPickOrderList.tsx +++ b/src/components/JoWorkbench/JoPickOrderList.tsx @@ -45,7 +45,7 @@ const JoPickOrderList: React.FC = () => { paramName: "BOM Description", type: "select-labelled", options: [ - { label: t("All"), value: "All" }, + //{ label: t("All"), value: "All" }, { label: t("FG"), value: "FG" }, { label: t("WIP"), value: "WIP" }, ], @@ -56,7 +56,7 @@ const JoPickOrderList: React.FC = () => { paramName: "bomType", type: "select-labelled", options: [ - { label: t("All"), value: "All" }, + //{ label: t("All"), value: "All" }, { label: t("Drink"), value: "drink" }, { label: t("Powder Mixture"), value: "Powder_Mixture" }, { label: t("Other"), value: "other" }, @@ -67,7 +67,7 @@ const JoPickOrderList: React.FC = () => { paramName: "floor", type: "select-labelled", options: [ - { label: t("All"), value: "ALL" }, + //{ label: t("All"), value: "ALL" }, { label: "2F", value: "2F" }, { label: "3F", value: "3F" }, { label: "4F", value: "4F" }, diff --git a/src/components/JoWorkbench/newJobPickExecution.tsx b/src/components/JoWorkbench/newJobPickExecution.tsx index f3f8a31..5c09d51 100644 --- a/src/components/JoWorkbench/newJobPickExecution.tsx +++ b/src/components/JoWorkbench/newJobPickExecution.tsx @@ -3002,6 +3002,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const canPostScanPick = // unavailable lot: Just Completed must always submit qty=0, even without lotNo isUnavailableForJustComplete || + isLotAvailabilityExpired(canonicalLotForSol) || // noLot row: Just Completed always submit qty=0 isNoLotForJustComplete || (canonicalLotForSol.lotNo && @@ -3014,6 +3015,8 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { if (canPostScanPick) { const qtyToSend = isUnavailableForJustComplete + ? 0 + : isLotAvailabilityExpired(canonicalLotForSol) ? 0 : isNoLotForJustComplete ? 0 @@ -3060,7 +3063,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { ); return; } - const justCompleteErr = tPick( + const justCompleteErr = t( "Just Completed (workbench): requires valid quantity; expired rows must not use this button.", ); if (solId > 0) { @@ -3964,7 +3967,8 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { !Number.isNaN(Number(fromPickRow)) ? Number(fromPickRow) : lockedSubmitQtyDisplay; - + const totalAvail = Number(lot.itemTotalAvailableQty ?? 0); + const isLastLotUnavailable = Number.isFinite(totalAvail) && totalAvail === 0; return ( = ({ filterArgs, onBackToList }) => { - {row.isGroupFirst ? ( - <> - {lot.itemCode}
- {lot.itemName}
- {lot.uomDesc} - - ) : ""} -
+ {row.isGroupFirst ? ( + <> + {lot.itemCode}
+ {lot.itemName}
+ {lot.uomDesc} + + ) : ""} + @@ -4043,7 +4047,16 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { {t( "is expired. Please check around have available QR code or not.", )} +
+ {isLastLotUnavailable && ( + + {t("This is last lot, so no available lot.")} + + )} ) : isInventoryLotLineUnavailable(lot) && !( @@ -4093,7 +4106,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { openWorkbenchLotLabelModalForLot(lot) } disabled={ - lot.lotAvailability === "expired" || + (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[ Number(lot.stockOutLineId) @@ -4457,7 +4470,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { "partially_completed" || lot.stockOutLineStatus === "partially_complete" || - isUnavailableLot || + // isUnavailableLot || (Number(lot.stockOutLineId) > 0 && issuePickedQtyBySolId[ Number(lot.stockOutLineId) diff --git a/src/components/Jodetail/FinishedGoodSearchWrapper.tsx b/src/components/Jodetail/FinishedGoodSearchWrapper.tsx index 8784982..c4c0896 100644 --- a/src/components/Jodetail/FinishedGoodSearchWrapper.tsx +++ b/src/components/Jodetail/FinishedGoodSearchWrapper.tsx @@ -23,7 +23,7 @@ const JodetailSearchWrapper: React.FC & SubComponents = async () => { */ fetchPrinterCombo(), ]); - console.log("%c printerCombo:", "color:green", printerCombo); + //console.log("%c printerCombo:", "color:green", printerCombo); return ; }; diff --git a/src/components/ProductionProcess/ProductionProcessDetail.tsx b/src/components/ProductionProcess/ProductionProcessDetail.tsx index 5e28a49..53ae52d 100644 --- a/src/components/ProductionProcess/ProductionProcessDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessDetail.tsx @@ -770,7 +770,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { ) : isPaused ? ( ) : isPass ? ( - + ) : ( ) diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index 6384f4e..8276d71 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -284,8 +284,9 @@ "Finished Good Management": "成品出倉管理", "提料順序": "提料順序", "Filter": "過濾", - "Item Code": "材料編號", - "Item Name": "材料名稱", + "Item Code": "物料編號", + "Item Name": "物料名稱", + "Just Completed (workbench): requires valid quantity; expired rows must not use this button.": "工單對料:需要有效數量;過期項目不能使用此按鈕。", "Search & Jump": "搜尋並跳轉", "Enter to jump to item": "按 Enter 直接跳到品項位置", "Jump": "跳轉", @@ -533,6 +534,7 @@ "Edit departure time": "編輯出發時間", "Failed to load truck lane detail": "載入車線詳情失敗", "Shop Detail": "店鋪詳情", + "Just Pass": "已完成", "Truck Lane Detail": "車線詳情", "Filter by Status": "按狀態篩選", "All": "全部", diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index e59eae9..e900278 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -151,6 +151,8 @@ "Issue": "問題", "Location": "位置", "Scan Result": "掃碼結果", + "Just Completed (workbench): requires valid quantity; expired rows must not use this button.": "工單對料:需要有效數量;過期項目不能使用此按鈕。", + "This is last lot, so no available lot.": "這是最後一個批次,所以沒有可用批次。", "Expiry Date": "有效期", "Target Date": "需求日期", "Lot Required Pick Qty": "批號需求數", diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index a1c2d5a..963a3c4 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -501,6 +501,7 @@ "label Printer" : "標籤打印機", "A4 Printer" : "A4 打印機", "Loading Sequence": "裝載序", + "Ticket No": "提票號碼", "The scanned lot inventory line is unavailable. Cannot switch or bind; pick line was not updated.": "掃描的庫存批行為「不可用」,無法換批或綁定;揀貨行未更新。", "is unavable. Please check around have available QR code or not.": "此批號不可用,請檢查周圍是否有可用的 QR 碼。", "Lot switch failed; pick line was not marked as checked.": "換批失敗;揀貨行未標為已核對。",