diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 058c217..f59de5c 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -524,6 +524,7 @@ export interface PickOrderLineWithLotsResponse { uomCode: string | null; uomDesc: string | null; status: string | null; + handler: string | null; lots: LotDetailResponse[]; } @@ -868,9 +869,9 @@ export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => ); }); // 获取已完成的 Job Order pick orders -export const fetchCompletedJobOrderPickOrdersrecords = cache(async (userId: number) => { +export const fetchCompletedJobOrderPickOrdersrecords = cache(async () => { return serverFetchJson( - `${BASE_API_URL}/jo/completed-job-order-pick-orders-only/${userId}`, + `${BASE_API_URL}/jo/completed-job-order-pick-orders-only`, { method: "GET", next: { tags: ["jo-completed"] }, diff --git a/src/components/Jodetail/JobPickExecutionForm.tsx b/src/components/Jodetail/JobPickExecutionForm.tsx index 15007b3..3cf7b27 100644 --- a/src/components/Jodetail/JobPickExecutionForm.tsx +++ b/src/components/Jodetail/JobPickExecutionForm.tsx @@ -196,16 +196,19 @@ useEffect(() => { if (verifiedQty === undefined || verifiedQty < 0) { newErrors.actualPickQty = t('Qty is required'); } - + const totalQty = verifiedQty + badItemQty + missQty; const hasAnyValue = verifiedQty > 0 || badItemQty > 0 || missQty > 0; - + + // ✅ 新增:必须至少有一个 > 0 + if (!hasAnyValue) { + newErrors.actualPickQty = t('At least one of Verified / Missing / Bad must be greater than 0'); + } + if (hasAnyValue && totalQty !== requiredQty) { newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity'); } - - - + setErrors(newErrors); return Object.keys(newErrors).length === 0; }; @@ -214,9 +217,10 @@ useEffect(() => { return; } - // Handle normal pick submission: verifiedQty > 0 with no issues, OR all zeros (verifiedQty=0, missQty=0, badItemQty=0) - const isNormalPick = (verifiedQty > 0 || (verifiedQty === 0 && formData.missQty == 0 && formData.badItemQty == 0)) - && formData.missQty == 0 && formData.badItemQty == 0; + // ✅ 只允许 Verified>0 且没有问题时,走 normal pick + const isNormalPick = verifiedQty > 0 + && formData.missQty == 0 + && formData.badItemQty == 0; if (isNormalPick) { if (onNormalPickSubmit) { @@ -235,11 +239,12 @@ useEffect(() => { } return; } - + + // ❌ 有问题(或全部为 0)才进入 Issue 提报流程 if (!validateForm() || !formData.pickOrderId) { return; } - + setLoading(true); try { const submissionData = { diff --git a/src/components/Jodetail/JobPickExecutionsecondscan.tsx b/src/components/Jodetail/JobPickExecutionsecondscan.tsx index d43a661..458a372 100644 --- a/src/components/Jodetail/JobPickExecutionsecondscan.tsx +++ b/src/components/Jodetail/JobPickExecutionsecondscan.tsx @@ -487,7 +487,8 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { matchStatus: lot.matchStatus, routerArea: lot.routerArea, routerRoute: lot.routerRoute, - uomShortDesc: lot.uomShortDesc + uomShortDesc: lot.uomShortDesc, + handler: lot.handler, }); }); } @@ -1173,6 +1174,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { {t("Index")} {t("Route")} + {t("Handler")} {t("Item Code")} {t("Item Name")} {t("Lot No")} @@ -1212,6 +1214,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { {lot.routerRoute || '-'} + {lot.handler || '-'} {lot.itemCode} {lot.itemName+'('+lot.uomDesc+')'} diff --git a/src/components/Jodetail/JodetailSearch.tsx b/src/components/Jodetail/JodetailSearch.tsx index b68616f..22165c8 100644 --- a/src/components/Jodetail/JodetailSearch.tsx +++ b/src/components/Jodetail/JodetailSearch.tsx @@ -15,7 +15,7 @@ import { import { arrayToDayjs, } from "@/app/utils/formatUtil"; -import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography, Box } from "@mui/material"; +import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography, Box, TextField, Autocomplete } from "@mui/material"; import Jodetail from "./Jodetail" import PickExecution from "./JobPickExecution"; import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; @@ -63,12 +63,18 @@ const JodetailSearch: React.FC = ({ pickOrders, printerCombo }) => { const [totalCount, setTotalCount] = useState(); const [isAssigning, setIsAssigning] = useState(false); const [unassignedOrders, setUnassignedOrders] = useState([]); -const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); -const [hasAssignedJobOrders, setHasAssignedJobOrders] = useState(false); -const [hasDataTab0, setHasDataTab0] = useState(false); -const [hasDataTab1, setHasDataTab1] = useState(false); -const hasAnyAssignedData = hasDataTab0 || hasDataTab1; -//const [printers, setPrinters] = useState([]); + const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); + const [hasAssignedJobOrders, setHasAssignedJobOrders] = useState(false); + const [hasDataTab0, setHasDataTab0] = useState(false); + const [hasDataTab1, setHasDataTab1] = useState(false); + const hasAnyAssignedData = hasDataTab0 || hasDataTab1; + + // Add printer selection state + const [selectedPrinter, setSelectedPrinter] = useState( + printerCombo && printerCombo.length > 0 ? printerCombo[0] : null + ); + const [printQty, setPrintQty] = useState(1); + const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState( typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' ); @@ -98,21 +104,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; window.removeEventListener('jobOrderDataStatus', handleJobOrderDataChange as EventListener); }; }, []); - /* - useEffect(() => { - const fetchPrinters = async () => { - try { - // 需要创建一个客户端版本的 fetchPrinterCombo - // 或者使用 API 路由 - // const printersData = await fetch('/api/printers/combo').then(r => r.json()); - // setPrinters(printersData); - } catch (error) { - console.error("Error fetching printers:", error); - } - }; - fetchPrinters(); - }, []); - */ + useEffect(() => { const onAssigned = () => { localStorage.removeItem('hideCompletedUntilNext'); @@ -121,7 +113,6 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; window.addEventListener('pickOrderAssigned', onAssigned); return () => window.removeEventListener('pickOrderAssigned', onAssigned); }, []); - // ... existing code ... useEffect(() => { const handleCompletionStatusChange = (event: CustomEvent) => { @@ -139,7 +130,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; return () => { window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); }; - }, [tabIndex]); // 添加 tabIndex 依赖 + }, [tabIndex]); // 新增:处理标签页切换时的打印按钮状态重置 useEffect(() => { @@ -150,7 +141,6 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; } }, [tabIndex]); -// ... existing code ... const handleAssignByStore = async (storeId: "2/F" | "4/F") => { if (!currentUserId) { console.error("Missing user id in session"); @@ -430,71 +420,89 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; return ( - {/* Header section */} - - - - - - -{/* Last 2 buttons aligned right - - - - {!hasAnyAssignedData && unassignedOrders && unassignedOrders.length > 0 && ( - - - {t("Unassigned Job Orders")} ({unassignedOrders.length}) - - - {unassignedOrders.map((order) => ( - - ))} - - -)} - -*/} + {/* Header section with printer selection */} + + {/* Left side - Title */} - - - + {/* Right side - Printer selection (only show on tab 1) */} + {tabIndex === 1 && ( + + + {t("Select Printer")}: + + + option.name || option.label || option.code || `Printer ${option.id}` + } + value={selectedPrinter} + onChange={(_, newValue) => setSelectedPrinter(newValue)} + sx={{ minWidth: 200 }} + size="small" + renderInput={(params) => ( + + )} + /> + + {t("Print Quantity")}: + + { + const value = parseInt(e.target.value) || 1; + setPrintQty(Math.max(1, value)); + }} + inputProps={{ min: 1, step: 1 }} + sx={{ width: 120 }} + size="small" + /> + + )} + - {/* Tabs section - Move the click handler here */} + {/* Tabs section */} - {/* */} - - - - {/* */} - {/* */} + + - - {/* Content section - NO overflow: 'auto' here */} - - {/* {tabIndex === 0 && } */} - {tabIndex === 1 && } + {/* Content section */} + {tabIndex === 0 && } - {/* {tabIndex === 2 && } */} - {/* {tabIndex === 3 && } */} + {tabIndex === 1 && ( + + )} ); diff --git a/src/components/Jodetail/completeJobOrderRecord.tsx b/src/components/Jodetail/completeJobOrderRecord.tsx index f54d545..ea6cc22 100644 --- a/src/components/Jodetail/completeJobOrderRecord.tsx +++ b/src/components/Jodetail/completeJobOrderRecord.tsx @@ -49,6 +49,8 @@ import { PrinterCombo } from "@/app/api/settings/printer"; interface Props { filterArgs: Record; printerCombo: PrinterCombo[]; + selectedPrinter?: PrinterCombo | null; + printQty?: number; } // 修改:已完成的 Job Order Pick Order 接口 @@ -101,7 +103,12 @@ interface LotDetail { uomDesc: string; } -const CompleteJobOrderRecord: React.FC = ({ filterArgs ,printerCombo}) => { +const CompleteJobOrderRecord: React.FC = ({ + filterArgs, + printerCombo, + selectedPrinter: selectedPrinterProp, + printQty: printQtyProp +}) => { const { t } = useTranslation("jo"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -121,25 +128,11 @@ const CompleteJobOrderRecord: React.FC = ({ filterArgs ,printerCombo}) => // 修改:搜索状态 const [searchQuery, setSearchQuery] = useState>({}); const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState([]); - //const [selectedPrinter, setSelectedPrinter] = useState(printerCombo[0]); - const defaultDemoPrinter: PrinterCombo = { - id: 2, - value: 2, - name: "2fi", - label: "2fi", - code: "2fi" - }; - const availablePrinters = useMemo(() => { - if (printerCombo.length === 0) { - console.log("No printers available, using default demo printer"); - return [defaultDemoPrinter]; - } - return printerCombo; - }, [printerCombo]); - const [selectedPrinter, setSelectedPrinter] = useState( - printerCombo && printerCombo.length > 0 ? printerCombo[0] : null - ); - const [printQty, setPrintQty] = useState(1); + + // Use props with fallback + const selectedPrinter = selectedPrinterProp ?? (printerCombo && printerCombo.length > 0 ? printerCombo[0] : null); + const printQty = printQtyProp ?? 1; + // 修改:分页状态 const [paginationController, setPaginationController] = useState({ pageNum: 0, @@ -157,7 +150,7 @@ const CompleteJobOrderRecord: React.FC = ({ filterArgs ,printerCombo}) => try { console.log("🔍 Fetching completed Job Order pick orders (pick completed only)..."); - const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersrecords(currentUserId); + const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersrecords(); // Fix: Ensure the data is always an array const safeData = Array.isArray(completedJobOrderPickOrders) ? completedJobOrderPickOrders : []; @@ -226,7 +219,19 @@ const CompleteJobOrderRecord: React.FC = ({ filterArgs ,printerCombo}) => setFilteredJobOrderPickOrders(filtered); console.log("Filtered Job Order pick orders count:", filtered.length); }, [completedJobOrderPickOrders]); - + const formatDateTime = (value: any) => { + if (!value) return "-"; + + // 后端发来的是 [yyyy, MM, dd, HH, mm, ss] + if (Array.isArray(value)) { + const [year, month, day, hour = 0, minute = 0, second = 0] = value; + return new Date(year, month - 1, day, hour, minute, second).toLocaleString(); + } + + // 如果以后改成字符串/ISO,也兼容 + const d = new Date(value); + return isNaN(d.getTime()) ? "-" : d.toLocaleString(); + }; // 修改:重置搜索 const handleSearchReset = useCallback(() => { setSearchQuery({}); @@ -433,18 +438,6 @@ const CompleteJobOrderRecord: React.FC = ({ filterArgs ,printerCombo}) => {t("Required Qty")}: {selectedJobOrderPickOrder.reqQty} {selectedJobOrderPickOrder.uom} - {/* - - - - */} @@ -600,37 +593,7 @@ const CompleteJobOrderRecord: React.FC = ({ filterArgs ,printerCombo}) => {t("Total")}: {filteredJobOrderPickOrders.length} {t("completed Job Order pick orders with matching")} - - - - {t("Select Printer")}: - - option.name || option.label || option.code || `Printer ${option.id}`} - value={selectedPrinter} - onChange={(_, newValue) => setSelectedPrinter(newValue)} - sx={{ minWidth: 250 }} - size="small" - renderInput={(params) => } - /> - - {t("Print Quantity")}: - - { - const value = parseInt(e.target.value) || 1; - setPrintQty(Math.max(1, value)); - }} - inputProps={{ min: 1, step: 1 }} - sx={{ width: 120 }} - size="small" - /> - - + {/* 列表 */} {filteredJobOrderPickOrders.length === 0 ? ( @@ -652,7 +615,7 @@ const CompleteJobOrderRecord: React.FC = ({ filterArgs ,printerCombo}) => {jobOrderPickOrder.jobOrderName} - {jobOrderPickOrder.pickOrderCode} - {t("Completed")}: {new Date(jobOrderPickOrder.completedDate).toLocaleString()} + {t("Completed")}: {formatDateTime(jobOrderPickOrder.planEnd)} {t("Target Date")}: {jobOrderPickOrder.pickOrderTargetDate} diff --git a/src/components/Jodetail/newJobPickExecution.tsx b/src/components/Jodetail/newJobPickExecution.tsx index 2ba0e66..dc5c0c9 100644 --- a/src/components/Jodetail/newJobPickExecution.tsx +++ b/src/components/Jodetail/newJobPickExecution.tsx @@ -42,8 +42,6 @@ import { } from "@/app/api/pickOrder/actions"; // 修改:使用 Job Order API import { - //fetchJobOrderLotsHierarchical, - //fetchUnassignedJobOrderPickOrders, assignJobOrderPickOrder, fetchJobOrderLotsHierarchicalByPickOrderId, updateJoPickOrderHandledBy, @@ -412,6 +410,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) pickOrderType: data.pickOrder.type, pickOrderStatus: data.pickOrder.status, pickOrderAssignTo: data.pickOrder.assignTo, + handler: line.handler, }); }); } @@ -537,6 +536,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) setCombinedDataLoading(false); } }, [getAllLotsFromHierarchical]); + const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => { if (!currentUserId || !pickOrderId || !itemId) { return; @@ -901,11 +901,9 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) // Use the first active suggested lot as the "expected" lot const expectedLot = activeSuggestedLots[0]; - // 2) Check if the scanned lot matches exactly if (scanned?.lotNo === expectedLot.lotNo) { - // ✅ Case 1: 使用 updateStockOutLineStatusByQRCodeAndLotNo API(更快) console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`); - + if (!expectedLot.stockOutLineId) { console.warn("No stockOutLineId on expectedLot, cannot update status by QR."); setQrScanError(true); @@ -922,24 +920,33 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) status: "checked", }); - if (res.code === "checked" || res.code === "SUCCESS") { - setQrScanError(false); - setQrScanSuccess(true); + const updateOk = + res?.type === "checked" || + typeof res?.id === "number" || + (res?.message && res.message.includes("success")); + + if (updateOk) { + setQrScanError(false); + setQrScanSuccess(true); - // ✅ 刷新数据而不是直接更新 state - const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; - await fetchJobOrderData(pickOrderId); - console.log("✅ Status updated, data refreshed"); - } else if (res.code === "LOT_NUMBER_MISMATCH") { - console.warn("Backend reported LOT_NUMBER_MISMATCH:", res.message); - setQrScanError(true); - setQrScanSuccess(false); - } else if (res.code === "ITEM_MISMATCH") { - console.warn("Backend reported ITEM_MISMATCH:", res.message); + + if ( + expectedLot.pickOrderId && + expectedLot.itemId && + (expectedLot.stockOutLineStatus?.toLowerCase?.() === "pending" || + !expectedLot.stockOutLineStatus) && + !expectedLot.handler + ) { + await updateHandledBy(expectedLot.pickOrderId, expectedLot.itemId); + } + + const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + await fetchJobOrderData(pickOrderId); + } else if (res?.code === "LOT_NUMBER_MISMATCH" || res?.code === "ITEM_MISMATCH") { setQrScanError(true); setQrScanSuccess(false); } else { - console.warn("Unexpected response code from backend:", res.code); + console.warn("Unexpected response from backend:", res); setQrScanError(true); setQrScanSuccess(false); } @@ -949,7 +956,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) setQrScanSuccess(false); } - return; // ✅ 直接返回,不再调用 handleQrCodeSubmit + return; // ✅ 直接返回,不再调用后面的分支 } // Case 2: Same item, different lot - show confirmation modal @@ -977,7 +984,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) setQrScanSuccess(false); return; } - }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen]); + }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen, updateHandledBy]); const handleManualInputSubmit = useCallback(() => { @@ -1310,6 +1317,14 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) console.error("Error submitting pick quantity:", error); } }, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]); + const handleSkip = useCallback(async (lot: any) => { + try { + console.log("Skip clicked, submit 0 qty for lot:", lot.lotNo); + await handleSubmitPickQtyWithQty(lot, 0); + } catch (err) { + console.error("Error in Skip:", err); + } + }, [handleSubmitPickQtyWithQty]); const handleSubmitAllScanned = useCallback(async () => { const scannedLots = combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked' @@ -1544,7 +1559,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) }, [startScan]); const handleStopScan = useCallback(() => { - console.log("⏹️ Stopping manual QR scan..."); + console.log(" Stopping manual QR scan..."); setIsManualScanning(false); setQrScanError(false); setQrScanSuccess(false); @@ -1563,7 +1578,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) }, [isManualScanning, stopScan, resetScan]); useEffect(() => { if (isManualScanning && combinedLotData.length === 0) { - console.log("⏹️ No data available, auto-stopping QR scan..."); + console.log(" No data available, auto-stopping QR scan..."); handleStopScan(); } }, [combinedLotData.length, isManualScanning, handleStopScan]); @@ -1677,16 +1692,59 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) - {qrScanError && !qrScanSuccess && ( - + {qrScanError && !qrScanSuccess && ( + {t("QR code does not match any item in current orders.")} )} - {qrScanSuccess && ( - - {t("QR code verified.")} - - )} + {qrScanSuccess && ( + + {t("QR code verified.")} + + )} @@ -1694,6 +1752,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) {t("Index")} {t("Route")} + {t("Handler")} {t("Item Code")} {t("Item Name")} {t("Lot No")} @@ -1733,6 +1792,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) {lot.routerRoute || '-'} + {lot.handler || '-'} {lot.itemCode} {lot.itemName+'('+lot.uomDesc+')'} @@ -1837,6 +1897,15 @@ const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) > {t("Issue")} + diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx index 166382c..1fc6f08 100644 --- a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -173,7 +173,8 @@ const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => { const response = await deleteJobOrder(jobOrderId) if (response) { //setProcessData(response.entity); - await fetchData(); + //await fetchData(); + onBack(); } }, [jobOrderId]); const handleRelease = useCallback(async ( jobOrderId: number) => { diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index db94557..7fa7091 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -221,6 +221,7 @@ "View Details": "查看詳情", "view stockin": "品檢", "No completed Job Order pick orders with matching found": "沒有相關記錄", + "Handler": "提料員", "Completed Step": "完成步驟", "Continue": "繼續", "Executing": "執行中", diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index 1bbc7f1..8c1bc89 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -86,6 +86,8 @@ "Job Order Item Name": "工單物料名稱", "Job Order Code": "工單編號", "View Details": "查看詳情", + "Skip": "跳過", + "Handler": "提料員", "Required Qty": "需求數量", "completed Job Order pick orders with Matching": "工單已完成提料和對料", "No completed Job Order pick orders with matching found": "沒有相關記錄",