From 83eaa0c399cb0c26c8092445ed1837589ecb92c4 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Sun, 28 Sep 2025 21:43:18 +0800 Subject: [PATCH] update --- src/app/api/jo/actions.ts | 122 ++ ...nRecord.tsx => FInishedJobOrderRecord.tsx} | 6 +- ...PickExecution.tsx => JobPickExecution.tsx} | 776 ++++++++---- ...utionForm.tsx => JobPickExecutionForm.tsx} | 67 +- ...ail.tsx => JobPickExecutionsecondscan.tsx} | 1095 +++++------------ src/components/Jodetail/JodetailSearch.tsx | 159 +-- 6 files changed, 1102 insertions(+), 1123 deletions(-) rename src/components/Jodetail/{GoodPickExecutionRecord.tsx => FInishedJobOrderRecord.tsx} (98%) rename src/components/Jodetail/{GoodPickExecution.tsx => JobPickExecution.tsx} (63%) rename src/components/Jodetail/{GoodPickExecutionForm.tsx => JobPickExecutionForm.tsx} (86%) rename src/components/Jodetail/{GoodPickExecutiondetail.tsx => JobPickExecutionsecondscan.tsx} (52%) diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 135f2d9..0955a79 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -76,7 +76,129 @@ export interface JobOrderDetail { pickLines: any[]; status: string; } +export interface UnassignedJobOrderPickOrder { + pickOrderId: number; + pickOrderCode: string; + pickOrderConsoCode: string; + pickOrderTargetDate: string; + pickOrderStatus: string; + jobOrderId: number; + jobOrderCode: string; + jobOrderName: string; + reqQty: number; + uom: string; + planStart: string; + planEnd: string; +} + +export interface AssignJobOrderResponse { + id: number | null; + code: string | null; + name: string | null; + type: string | null; + message: string | null; + errorPosition: string | null; +} +export const recordSecondScanIssue = cache(async ( + pickOrderId: number, + itemId: number, + data: { + qty: number; + isMissing: boolean; + isBad: boolean; + reason: string; + createdBy: number; + } +) => { + return serverFetchJson( + `${BASE_API_URL}/jo/second-scan-issue/${pickOrderId}/${itemId}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + next: { tags: ["jo-second-scan"] }, + }, + ); +}); +export const updateSecondQrScanStatus = cache(async (pickOrderId: number, itemId: number) => { + return serverFetchJson( + `${BASE_API_URL}/jo/second-scan-qr/${pickOrderId}/${itemId}`, + { + method: "POST", + next: { tags: ["jo-second-scan"] }, + }, + ); +}); + +export const submitSecondScanQuantity = cache(async ( + pickOrderId: number, + itemId: number, + data: { qty: number; isMissing?: boolean; isBad?: boolean; reason?: string } +) => { + return serverFetchJson( + `${BASE_API_URL}/jo/second-scan-submit/${pickOrderId}/${itemId}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + next: { tags: ["jo-second-scan"] }, + }, + ); +}); +// 获取未分配的 Job Order pick orders +export const fetchUnassignedJobOrderPickOrders = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/jo/unassigned-job-order-pick-orders`, + { + method: "GET", + next: { tags: ["jo-unassigned"] }, + }, + ); +}); +// 分配 Job Order pick order 给用户 +export const assignJobOrderPickOrder = async (pickOrderId: number, userId: number) => { + return serverFetchJson( + `${BASE_API_URL}/jo/assign-job-order-pick-order/${pickOrderId}/${userId}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + } + ); +}; + +// 获取 Job Order 分层数据 +export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => { + return serverFetchJson( + `${BASE_API_URL}/jo/all-lots-hierarchical/${userId}`, + { + method: "GET", + next: { tags: ["jo-hierarchical"] }, + }, + ); +}); + +// 获取已完成的 Job Order pick orders +export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => { + return serverFetchJson( + `${BASE_API_URL}/jo/completed-job-order-pick-orders/${userId}`, + { + method: "GET", + next: { tags: ["jo-completed"] }, + }, + ); +}); + +// 获取已完成的 Job Order pick order records +export const fetchCompletedJobOrderPickOrderRecords = cache(async (userId: number) => { + return serverFetchJson( + `${BASE_API_URL}/jo/completed-job-order-pick-order-records/${userId}`, + { + method: "GET", + next: { tags: ["jo-records"] }, + }, + ); +}); export const fetchJobOrderDetailByCode = cache(async (code: string) => { return serverFetchJson( `${BASE_API_URL}/jo/detailByCode/${code}`, diff --git a/src/components/Jodetail/GoodPickExecutionRecord.tsx b/src/components/Jodetail/FInishedJobOrderRecord.tsx similarity index 98% rename from src/components/Jodetail/GoodPickExecutionRecord.tsx rename to src/components/Jodetail/FInishedJobOrderRecord.tsx index 2bde71e..4756e95 100644 --- a/src/components/Jodetail/GoodPickExecutionRecord.tsx +++ b/src/components/Jodetail/FInishedJobOrderRecord.tsx @@ -59,7 +59,7 @@ import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerP import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; -import GoodPickExecutionForm from "./GoodPickExecutionForm"; +import GoodPickExecutionForm from "./JobPickExecutionForm"; import FGPickOrderCard from "./FGPickOrderCard"; interface Props { @@ -99,7 +99,7 @@ interface PickOrderData { lots: any[]; } -const GoodPickExecutionRecord: React.FC = ({ filterArgs }) => { +const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { const { t } = useTranslation("pickOrder"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -437,4 +437,4 @@ const GoodPickExecutionRecord: React.FC = ({ filterArgs }) => { ); }; -export default GoodPickExecutionRecord; \ No newline at end of file +export default FInishedJobOrderRecord; \ No newline at end of file diff --git a/src/components/Jodetail/GoodPickExecution.tsx b/src/components/Jodetail/JobPickExecution.tsx similarity index 63% rename from src/components/Jodetail/GoodPickExecution.tsx rename to src/components/Jodetail/JobPickExecution.tsx index 1bd52b8..87c6bf2 100644 --- a/src/components/Jodetail/GoodPickExecution.tsx +++ b/src/components/Jodetail/JobPickExecution.tsx @@ -15,6 +15,7 @@ import { TableHead, TableRow, Paper, + Checkbox, TablePagination, Modal, } from "@mui/material"; @@ -22,11 +23,10 @@ import { useCallback, useEffect, useState, useRef, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/navigation"; import { - fetchALLPickOrderLineLotDetails, updateStockOutLineStatus, createStockOutLine, recordPickExecutionIssue, - fetchFGPickOrders, // ✅ Add this import + fetchFGPickOrders, FGPickOrderResponse, autoAssignAndReleasePickOrder, AutoAssignReleaseResponse, @@ -34,6 +34,12 @@ import { PickOrderCompletionResponse, checkAndCompletePickOrderByConsoCode } from "@/app/api/pickOrder/actions"; +// ✅ 修改:使用 Job Order API +import { + fetchJobOrderLotsHierarchical, + fetchUnassignedJobOrderPickOrders, + assignJobOrderPickOrder +} from "@/app/api/jo/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; import { FormProvider, @@ -47,19 +53,20 @@ import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerP import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; -import GoodPickExecutionForm from "./GoodPickExecutionForm"; +import GoodPickExecutionForm from "./JobPickExecutionForm"; import FGPickOrderCard from "./FGPickOrderCard"; + interface Props { filterArgs: Record; } -// ✅ QR Code Modal Component (from LotTable) +// ✅ QR Code Modal Component (from GoodPickExecution) const QrCodeModal: React.FC<{ open: boolean; onClose: () => void; lot: any | null; onQrCodeSubmit: (lotNo: string) => void; - combinedLotData: any[]; // ✅ Add this prop + combinedLotData: any[]; }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => { const { t } = useTranslation("pickOrder"); const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); @@ -73,7 +80,7 @@ const QrCodeModal: React.FC<{ const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [scannedQrResult, setScannedQrResult] = useState(''); - const [fgPickOrder, setFgPickOrder] = useState(null); + // Process scanned QR codes useEffect(() => { if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) { @@ -131,7 +138,7 @@ const QrCodeModal: React.FC<{ onClose(); resetScan(); } else { - setQrScanFailed(true); + setQrScanFailed(true); setManualInputError(true); setManualInputSubmitted(true); } @@ -147,7 +154,7 @@ const QrCodeModal: React.FC<{ onClose(); resetScan(); } else { - setQrScanFailed(true); + setQrScanFailed(true); setManualInputError(true); setManualInputSubmitted(true); } @@ -162,23 +169,23 @@ const QrCodeModal: React.FC<{ setManualInputSubmitted(false); setManualInputError(false); setIsProcessingQr(false); - setQrScanFailed(false); - setQrScanSuccess(false); + setQrScanFailed(false); + setQrScanSuccess(false); setScannedQrResult(''); setProcessedQrCodes(new Set()); - } + } }, [open]); useEffect(() => { if (lot) { - setManualInput(''); - setManualInputSubmitted(false); - setManualInputError(false); + setManualInput(''); + setManualInputSubmitted(false); + setManualInputError(false); setIsProcessingQr(false); - setQrScanFailed(false); - setQrScanSuccess(false); - setScannedQrResult(''); - setProcessedQrCodes(new Set()); + setQrScanFailed(false); + setQrScanSuccess(false); + setScannedQrResult(''); + setProcessedQrCodes(new Set()); } }, [lot]); @@ -190,7 +197,7 @@ const QrCodeModal: React.FC<{ const timer = setTimeout(() => { setQrScanSuccess(true); onQrCodeSubmit(lot.lotNo); - onClose(); + onClose(); setManualInput(''); setManualInputError(false); setManualInputSubmitted(false); @@ -238,7 +245,7 @@ const QrCodeModal: React.FC<{ {isProcessingQr && ( - {t("Processing QR code...")} + {t("Processing QR code...")} )} @@ -267,8 +274,8 @@ const QrCodeModal: React.FC<{ : '' } /> - + ) : ( + + )} + + {isManualScanning && ( + + + + {t("Scanning...")} + + + )} + + + + {qrScanError && !qrScanSuccess && ( + + {t("QR code does not match any item in current orders.")} + + )} + {qrScanSuccess && ( + + {t("QR code verified.")} + + )} - - - - + +
+ + {t("Index")} {t("Route")} - {t("Item Name")} + {t("Item Code")} + {t("Item Name")} {t("Lot#")} - {t("Lot Required Pick Qty")} - - {t("Lot Actual Pick Qty")} - - {t("Action")} - - - + {t("Scan Result")} + {t("Submit Required Pick Qty")} + + + {paginatedData.length === 0 ? ( - + {t("No data available")} @@ -1037,7 +1330,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { > - {lot.routerIndex || index + 1} + {index + 1} @@ -1045,7 +1338,8 @@ const PickExecution: React.FC = ({ filterArgs }) => { {lot.routerRoute || '-'} - {lot.itemName} + {lot.itemCode} + {lot.itemName+'('+lot.uomDesc+')'} = ({ filterArgs }) => { - {(() => { - const inQty = lot.inQty || 0; - const outQty = lot.outQty || 0; - const result = inQty - outQty; - return result.toLocaleString(); + const requiredQty = lot.requiredQty || 0; + return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; })()} + + {lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? ( + + + + ) : null} + - {!lot.stockOutLineId ? ( - - ) : ( - // ✅ When stockOutLineId exists, show TextField + Issue button + + - { + - - - )} - - - - - - - + + )) )} - - -
+ +
-*/} - {/* - `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` } - /> + /> - + {/* ✅ QR Code Modal */} { @@ -1210,19 +1477,19 @@ const PickExecution: React.FC = ({ filterArgs }) => { resetScan(); }} lot={selectedLotForQr} - combinedLotData={combinedLotData} // ✅ Add this prop + combinedLotData={combinedLotData} onQrCodeSubmit={handleQrCodeSubmitFromModal} /> - + {/* ✅ Pick Execution Form Modal */} {pickExecutionFormOpen && selectedLotForExecutionForm && ( - { setPickExecutionFormOpen(false); setSelectedLotForExecutionForm(null); }} - onSubmit={handlePickExecutionFormSubmit} + onSubmit={handlePickExecutionFormSubmit} selectedLot={selectedLotForExecutionForm} selectedPickOrderLine={{ id: selectedLotForExecutionForm.pickOrderLineId, @@ -1242,9 +1509,8 @@ const PickExecution: React.FC = ({ filterArgs }) => { pickOrderCreateDate={new Date()} /> )} - */} ); }; -export default PickExecution; \ No newline at end of file +export default JobPickExecution \ No newline at end of file diff --git a/src/components/Jodetail/GoodPickExecutionForm.tsx b/src/components/Jodetail/JobPickExecutionForm.tsx similarity index 86% rename from src/components/Jodetail/GoodPickExecutionForm.tsx rename to src/components/Jodetail/JobPickExecutionForm.tsx index b7fe86d..7af42b7 100644 --- a/src/components/Jodetail/GoodPickExecutionForm.tsx +++ b/src/components/Jodetail/JobPickExecutionForm.tsx @@ -81,7 +81,7 @@ const PickExecutionForm: React.FC = ({ const [errors, setErrors] = useState({}); const [loading, setLoading] = useState(false); const [handlers, setHandlers] = useState>([]); - + const [verifiedQty, setVerifiedQty] = useState(0); // 计算剩余可用数量 const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { const remainingQty = lot.inQty - lot.outQty; @@ -123,17 +123,15 @@ const PickExecutionForm: React.FC = ({ } }; - // 计算剩余可用数量 - const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); - const requiredQty = calculateRequiredQty(selectedLot); + // ✅ Initialize verified quantity to the received quantity (actualPickQty) + const initialVerifiedQty = selectedLot.actualPickQty || 0; + setVerifiedQty(initialVerifiedQty); + console.log("=== PickExecutionForm Debug ==="); console.log("selectedLot:", selectedLot); - console.log("inQty:", selectedLot.inQty); - console.log("outQty:", selectedLot.outQty); - console.log("holdQty:", selectedLot.holdQty); - console.log("availableQty:", selectedLot.availableQty); - console.log("calculated remainingAvailableQty:", remainingAvailableQty); + console.log("initialVerifiedQty:", initialVerifiedQty); console.log("=== End Debug ==="); + setFormData({ pickOrderId: pickOrderId, pickOrderCode: selectedPickOrderLine.pickOrderCode, @@ -147,18 +145,24 @@ const PickExecutionForm: React.FC = ({ lotNo: selectedLot.lotNo, storeLocation: selectedLot.location, requiredQty: selectedLot.requiredQty, - actualPickQty: selectedLot.actualPickQty || 0, + actualPickQty: initialVerifiedQty, // ✅ Use the initial value missQty: 0, - badItemQty: 0, // 初始化为 0,用户需要手动输入 + badItemQty: 0, issueRemark: '', pickerName: '', handledBy: undefined, }); } - }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]); + }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate]); const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => { setFormData(prev => ({ ...prev, [field]: value })); + + // ✅ Update verified quantity state when actualPickQty changes + if (field === 'actualPickQty') { + setVerifiedQty(value); + } + // 清除错误 if (errors[field as keyof FormErrors]) { setErrors(prev => ({ ...prev, [field]: undefined })); @@ -169,21 +173,21 @@ const PickExecutionForm: React.FC = ({ const validateForm = (): boolean => { const newErrors: FormErrors = {}; - if (formData.actualPickQty === undefined || formData.actualPickQty < 0) { + if (verifiedQty === undefined || verifiedQty < 0) { newErrors.actualPickQty = t('Qty is required'); } - // ✅ FIXED: Check if actual pick qty exceeds remaining available qty - if (formData.actualPickQty && formData.actualPickQty > remainingAvailableQty) { - newErrors.actualPickQty = t('Qty is not allowed to be greater than remaining available qty'); + // ✅ Check if verified qty exceeds received qty + if (verifiedQty > (selectedLot?.actualPickQty || 0)) { + newErrors.actualPickQty = t('Verified quantity cannot exceed received quantity'); } - // ✅ FIXED: Check if actual pick qty exceeds required qty (use original required qty) - if (formData.actualPickQty && formData.actualPickQty > (selectedLot?.requiredQty || 0)) { + // ✅ Check if verified qty exceeds required qty + if (verifiedQty > (selectedLot?.requiredQty || 0)) { newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty'); } - // ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported) + // ✅ Require either missQty > 0 OR badItemQty > 0 const hasMissQty = formData.missQty && formData.missQty > 0; const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0; @@ -203,7 +207,13 @@ const PickExecutionForm: React.FC = ({ setLoading(true); try { - await onSubmit(formData as PickExecutionIssueData); + // ✅ Use the verified quantity in the submission + const submissionData = { + ...formData, + actualPickQty: verifiedQty + } as PickExecutionIssueData; + + await onSubmit(submissionData); onClose(); } catch (error) { console.error('Error submitting pick execution issue:', error); @@ -215,6 +225,7 @@ const PickExecutionForm: React.FC = ({ const handleClose = () => { setFormData({}); setErrors({}); + setVerifiedQty(0); onClose(); }; @@ -257,8 +268,8 @@ const PickExecutionForm: React.FC = ({ = ({ handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)} + value={verifiedQty} // ✅ Use the separate state + onChange={(e) => { + const newValue = parseFloat(e.target.value) || 0; + setVerifiedQty(newValue); + handleInputChange('actualPickQty', newValue); + }} error={!!errors.actualPickQty} - helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`} + helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(selectedLot?.actualPickQty || 0, selectedLot?.requiredQty || 0)}`} variant="outlined" /> diff --git a/src/components/Jodetail/GoodPickExecutiondetail.tsx b/src/components/Jodetail/JobPickExecutionsecondscan.tsx similarity index 52% rename from src/components/Jodetail/GoodPickExecutiondetail.tsx rename to src/components/Jodetail/JobPickExecutionsecondscan.tsx index 1af86fc..fbc2122 100644 --- a/src/components/Jodetail/GoodPickExecutiondetail.tsx +++ b/src/components/Jodetail/JobPickExecutionsecondscan.tsx @@ -19,31 +19,20 @@ import { TablePagination, Modal, } from "@mui/material"; -import { fetchLotDetail } from "@/app/api/inventory/actions"; import { useCallback, useEffect, useState, useRef, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/navigation"; -import { - fetchALLPickOrderLineLotDetails, - updateStockOutLineStatus, - createStockOutLine, - updateStockOutLine, - recordPickExecutionIssue, - fetchFGPickOrders, // ✅ Add this import - FGPickOrderResponse, - autoAssignAndReleasePickOrder, - AutoAssignReleaseResponse, - checkPickOrderCompletion, - fetchAllPickOrderLotsHierarchical, - PickOrderCompletionResponse, - checkAndCompletePickOrderByConsoCode, - updateSuggestedLotLineId, - confirmLotSubstitution -} from "@/app/api/pickOrder/actions"; -import LotConfirmationModal from "./LotConfirmationModal"; -//import { fetchItem } from "@/app/api/settings/item"; -import { updateInventoryLotLineStatus, analyzeQrCode } from "@/app/api/inventory/actions"; +// ✅ 修改:使用 Job Order API +import { + fetchCompletedJobOrderPickOrders, + fetchUnassignedJobOrderPickOrders, + assignJobOrderPickOrder, + updateSecondQrScanStatus, + submitSecondScanQuantity, + recordSecondScanIssue + +} from "@/app/api/jo/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; import { FormProvider, @@ -57,19 +46,20 @@ import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerP import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; -import GoodPickExecutionForm from "./GoodPickExecutionForm"; +import GoodPickExecutionForm from "./JobPickExecutionForm"; import FGPickOrderCard from "./FGPickOrderCard"; + interface Props { filterArgs: Record; } -// ✅ QR Code Modal Component (from LotTable) +// ✅ QR Code Modal Component (from GoodPickExecution) const QrCodeModal: React.FC<{ open: boolean; onClose: () => void; lot: any | null; onQrCodeSubmit: (lotNo: string) => void; - combinedLotData: any[]; // ✅ Add this prop + combinedLotData: any[]; }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => { const { t } = useTranslation("pickOrder"); const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); @@ -83,7 +73,7 @@ const QrCodeModal: React.FC<{ const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [scannedQrResult, setScannedQrResult] = useState(''); - const [fgPickOrder, setFgPickOrder] = useState(null); + // Process scanned QR codes useEffect(() => { if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) { @@ -317,17 +307,23 @@ const QrCodeModal: React.FC<{ ); }; -const PickExecution: React.FC = ({ filterArgs }) => { +const JobPickExecution: React.FC = ({ filterArgs }) => { const { t } = useTranslation("pickOrder"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; - const [allLotsCompleted, setAllLotsCompleted] = useState(false); + + // ✅ 修改:使用 Job Order 数据结构 + const [jobOrderData, setJobOrderData] = useState(null); const [combinedLotData, setCombinedLotData] = useState([]); const [combinedDataLoading, setCombinedDataLoading] = useState(false); const [originalCombinedData, setOriginalCombinedData] = useState([]); + // ✅ 添加未分配订单状态 + const [unassignedOrders, setUnassignedOrders] = useState([]); + const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); + const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); const [qrScanInput, setQrScanInput] = useState(''); @@ -353,131 +349,99 @@ const PickExecution: React.FC = ({ filterArgs }) => { // ✅ Add QR modal states const [qrModalOpen, setQrModalOpen] = useState(false); const [selectedLotForQr, setSelectedLotForQr] = useState(null); - const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false); -const [expectedLotData, setExpectedLotData] = useState(null); -const [scannedLotData, setScannedLotData] = useState(null); -const [isConfirmingLot, setIsConfirmingLot] = useState(false); + // ✅ Add GoodPickExecutionForm states const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState(null); - const [fgPickOrders, setFgPickOrders] = useState([]); - const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false); - // ✅ Add these missing state variables after line 352 + // ✅ Add these missing state variables const [isManualScanning, setIsManualScanning] = useState(false); const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [lastProcessedQr, setLastProcessedQr] = useState(''); const [isRefreshingData, setIsRefreshingData] = useState(false); - const fetchFgPickOrdersData = useCallback(async () => { - if (!currentUserId) return; - - setFgPickOrdersLoading(true); + // ✅ 修改:加载未分配的 Job Order 订单 + const loadUnassignedOrders = useCallback(async () => { + setIsLoadingUnassigned(true); try { - // Get all pick order IDs from combinedLotData - const pickOrderIds = Array.from(new Set(combinedLotData.map(lot => lot.pickOrderId))); - - if (pickOrderIds.length === 0) { - setFgPickOrders([]); - return; - } - - // Fetch FG pick orders for each pick order ID - const fgPickOrdersPromises = pickOrderIds.map(pickOrderId => - fetchFGPickOrders(pickOrderId) - ); - - const fgPickOrdersResults = await Promise.all(fgPickOrdersPromises); - - // Flatten the results (each fetchFGPickOrders returns an array) - const allFgPickOrders = fgPickOrdersResults.flat(); - - setFgPickOrders(allFgPickOrders); - console.log("✅ Fetched FG pick orders:", allFgPickOrders); + const orders = await fetchUnassignedJobOrderPickOrders(); + setUnassignedOrders(orders); } catch (error) { - console.error("❌ Error fetching FG pick orders:", error); - setFgPickOrders([]); + console.error("Error loading unassigned orders:", error); } finally { - setFgPickOrdersLoading(false); + setIsLoadingUnassigned(false); } - }, [currentUserId, combinedLotData]); - useEffect(() => { - if (combinedLotData.length > 0) { - fetchFgPickOrdersData(); + }, []); + + // ✅ 修改:分配订单给当前用户 + const handleAssignOrder = useCallback(async (pickOrderId: number) => { + if (!currentUserId) { + console.error("Missing user id in session"); + return; } - }, [combinedLotData, fetchFgPickOrdersData]); - + + try { + const result = await assignJobOrderPickOrder(pickOrderId, currentUserId); + if (result.message === "Successfully assigned") { + console.log("✅ Successfully assigned pick order"); + // 刷新数据 + window.dispatchEvent(new CustomEvent('pickOrderAssigned')); + // 重新加载未分配订单列表 + loadUnassignedOrders(); + } else { + console.warn("⚠️ Assignment failed:", result.message); + alert(`Assignment failed: ${result.message}`); + } + } catch (error) { + console.error("❌ Error assigning order:", error); + alert("Error occurred during assignment"); + } + }, [currentUserId, loadUnassignedOrders]); + + // ✅ Handle QR code button click const handleQrCodeClick = (pickOrderId: number) => { console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); // TODO: Implement QR code functionality }; - const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => { - console.log("Lot mismatch detected:", { expectedLot, scannedLot }); - setExpectedLotData(expectedLot); - setScannedLotData(scannedLot); - setLotConfirmationOpen(true); - }, []); - const checkAllLotsCompleted = useCallback((lotData: any[]) => { - if (lotData.length === 0) { - setAllLotsCompleted(false); - return false; - } - - // Filter out rejected lots - const nonRejectedLots = lotData.filter(lot => - lot.lotAvailability !== 'rejected' && - lot.stockOutLineStatus !== 'rejected' - ); - - if (nonRejectedLots.length === 0) { - setAllLotsCompleted(false); - return false; - } - - // Check if all non-rejected lots are completed - const allCompleted = nonRejectedLots.every(lot => - lot.stockOutLineStatus === 'completed' - ); - - setAllLotsCompleted(allCompleted); - return allCompleted; - }, []); - const fetchAllCombinedLotData = useCallback(async (userId?: number) => { + // ✅ 修改:使用 Job Order API 获取数据 + const fetchJobOrderData = useCallback(async (userId?: number) => { setCombinedDataLoading(true); try { const userIdToUse = userId || currentUserId; - console.log(" fetchAllCombinedLotData called with userId:", userIdToUse); + console.log(" fetchJobOrderData called with userId:", userIdToUse); if (!userIdToUse) { console.warn("⚠️ No userId available, skipping API call"); + setJobOrderData(null); setCombinedLotData([]); setOriginalCombinedData([]); - setAllLotsCompleted(false); return; } - // ✅ Use the hierarchical endpoint that includes rejected lots - const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse); - console.log("✅ Hierarchical lot details:", hierarchicalData); + // ✅ 使用 Job Order API + const jobOrderData = await fetchCompletedJobOrderPickOrders(userIdToUse); + console.log("✅ Job Order data:", jobOrderData); + + setJobOrderData(jobOrderData); // ✅ Transform hierarchical data to flat structure for the table const flatLotData: any[] = []; - if (hierarchicalData.pickOrder && hierarchicalData.pickOrderLines) { - hierarchicalData.pickOrderLines.forEach((line: any) => { + if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) { + jobOrderData.pickOrderLines.forEach((line: any) => { if (line.lots && line.lots.length > 0) { line.lots.forEach((lot: any) => { flatLotData.push({ // Pick order info - pickOrderId: hierarchicalData.pickOrder.id, - pickOrderCode: hierarchicalData.pickOrder.code, - pickOrderConsoCode: hierarchicalData.pickOrder.consoCode, - pickOrderTargetDate: hierarchicalData.pickOrder.targetDate, - pickOrderType: hierarchicalData.pickOrder.type, - pickOrderStatus: hierarchicalData.pickOrder.status, - pickOrderAssignTo: hierarchicalData.pickOrder.assignTo, + pickOrderId: jobOrderData.pickOrder.id, + pickOrderCode: jobOrderData.pickOrder.code, + pickOrderConsoCode: jobOrderData.pickOrder.consoCode, + pickOrderTargetDate: jobOrderData.pickOrder.targetDate, + pickOrderType: jobOrderData.pickOrder.type, + pickOrderStatus: jobOrderData.pickOrder.status, + pickOrderAssignTo: jobOrderData.pickOrder.assignTo, // Pick order line info pickOrderLineId: line.id, @@ -485,38 +449,33 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); pickOrderLineStatus: line.status, // Item info - itemId: line.item.id, - itemCode: line.item.code, - itemName: line.item.name, - uomCode: line.item.uomCode, - uomDesc: line.item.uomDesc, + itemId: line.itemId, + itemCode: line.itemCode, + itemName: line.itemName, + uomCode: line.uomCode, + uomDesc: line.uomDesc, // Lot info - lotId: lot.id, + lotId: lot.lotId, lotNo: lot.lotNo, expiryDate: lot.expiryDate, location: lot.location, - stockUnit: lot.stockUnit, availableQty: lot.availableQty, requiredQty: lot.requiredQty, actualPickQty: lot.actualPickQty, - inQty: lot.inQty, - outQty: lot.outQty, - holdQty: lot.holdQty, lotStatus: lot.lotStatus, lotAvailability: lot.lotAvailability, processingStatus: lot.processingStatus, - suggestedPickLotId: lot.suggestedPickLotId, stockOutLineId: lot.stockOutLineId, stockOutLineStatus: lot.stockOutLineStatus, stockOutLineQty: lot.stockOutLineQty, // Router info - routerId: lot.router?.id, - routerIndex: lot.router?.index, - routerRoute: lot.router?.route, - routerArea: lot.router?.area, - uomShortDesc: lot.router?.uomId + routerIndex: lot.routerIndex, + secondQrScanStatus: lot.secondQrScanStatus, + routerArea: lot.routerArea, + routerRoute: lot.routerRoute, + uomShortDesc: lot.uomShortDesc }); }); } @@ -527,89 +486,76 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); setCombinedLotData(flatLotData); setOriginalCombinedData(flatLotData); - // ✅ Check completion status - checkAllLotsCompleted(flatLotData); + // ✅ 计算完成状态并发送事件 + const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) => + lot.processingStatus === 'completed' + ); + + // ✅ 发送完成状态事件,包含标签页信息 + window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { + detail: { + allLotsCompleted: allCompleted, + tabIndex: 0 // ✅ 明确指定这是来自标签页 0 的事件 + } + })); + } catch (error) { - console.error("❌ Error fetching combined lot data:", error); + console.error("❌ Error fetching job order data:", error); + setJobOrderData(null); setCombinedLotData([]); setOriginalCombinedData([]); - setAllLotsCompleted(false); + + // ✅ 如果加载失败,禁用打印按钮 + window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { + detail: { + allLotsCompleted: false, + tabIndex: 0 + } + })); } finally { setCombinedDataLoading(false); } - }, [currentUserId, checkAllLotsCompleted]); + }, [currentUserId]); - // ✅ Add effect to check completion when lot data changes + // ✅ 修改:初始化时加载数据 useEffect(() => { - if (combinedLotData.length > 0) { - checkAllLotsCompleted(combinedLotData); + if (session && currentUserId && !initializationRef.current) { + console.log("✅ Session loaded, initializing job order..."); + initializationRef.current = true; + + // 加载 Job Order 数据 + fetchJobOrderData(); + // 加载未分配订单 + loadUnassignedOrders(); } - }, [combinedLotData, checkAllLotsCompleted]); + }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]); - // ✅ Add function to expose completion status to parent - const getCompletionStatus = useCallback(() => { - return allLotsCompleted; - }, [allLotsCompleted]); - - // ✅ Expose completion status to parent component + // ✅ Add event listener for manual assignment useEffect(() => { - // Dispatch custom event with completion status - const event = new CustomEvent('pickOrderCompletionStatus', { - detail: { - allLotsCompleted, - tabIndex: 1 // ✅ 明确指定这是来自标签页 1 的事件 - } - }); - window.dispatchEvent(event); - }, [allLotsCompleted]); - const handleLotConfirmation = useCallback(async () => { - if (!expectedLotData || !scannedLotData || !selectedLotForQr) return; - setIsConfirmingLot(true); - try { - let newLotLineId = scannedLotData?.inventoryLotLineId; - if (!newLotLineId && scannedLotData?.stockInLineId) { - const ld = await fetchLotDetail(scannedLotData.stockInLineId); - newLotLineId = ld.inventoryLotLineId; - } - if (!newLotLineId) { - console.error("No inventory lot line id for scanned lot"); - return; - } - - await confirmLotSubstitution({ - pickOrderLineId: selectedLotForQr.pickOrderLineId, - stockOutLineId: selectedLotForQr.stockOutLineId, - originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId, - newInventoryLotLineId: newLotLineId - }); - - setQrScanError(false); - setQrScanSuccess(false); - setQrScanInput(''); - setIsManualScanning(false); - stopScan(); - resetScan(); - setProcessedQrCodes(new Set()); - setLastProcessedQr(''); + const handlePickOrderAssigned = () => { + console.log("🔄 Pick order assigned event received, refreshing data..."); + fetchJobOrderData(); + }; - setLotConfirmationOpen(false); - setExpectedLotData(null); - setScannedLotData(null); - setSelectedLotForQr(null); - await fetchAllCombinedLotData(); - } catch (error) { - console.error("Error confirming lot substitution:", error); - } finally { - setIsConfirmingLot(false); - } - }, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData]); + window.addEventListener('pickOrderAssigned', handlePickOrderAssigned); + + return () => { + window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned); + }; + }, [fetchJobOrderData]); + + // ✅ Handle QR code submission for matched lot (external scanning) const handleQrCodeSubmit = useCallback(async (lotNo: string) => { - console.log(`✅ Processing QR Code for lot: ${lotNo}`); + console.log(`✅ Processing Second QR Code for lot: ${lotNo}`); - // ✅ Use current data without refreshing to avoid infinite loop - const currentLotData = combinedLotData; - console.log(` Available lots:`, currentLotData.map(lot => lot.lotNo)); + // ✅ Check if this lot was already processed recently + const lotKey = `${lotNo}_${Date.now()}`; + if (processedQrCodes.has(lotNo)) { + console.log(`⏭️ Lot ${lotNo} already processed, skipping...`); + return; + } + const currentLotData = combinedLotData; const matchingLots = currentLotData.filter(lot => lot.lotNo === lotNo || lot.lotNo?.toLowerCase() === lotNo.toLowerCase() @@ -619,305 +565,97 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); console.error(`❌ Lot not found: ${lotNo}`); setQrScanError(true); setQrScanSuccess(false); - const availableLotNos = currentLotData.map(lot => lot.lotNo).join(', '); - console.log(`❌ QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`); return; } - console.log(`✅ Found ${matchingLots.length} matching lots:`, matchingLots); - setQrScanError(false); - try { let successCount = 0; - let errorCount = 0; for (const matchingLot of matchingLots) { - console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`); + // ✅ Check if this specific item was already processed + const itemKey = `${matchingLot.pickOrderId}_${matchingLot.itemId}`; + if (processedQrCodes.has(itemKey)) { + console.log(`⏭️ Item ${matchingLot.itemId} already processed, skipping...`); + continue; + } - if (matchingLot.stockOutLineId) { - const stockOutLineUpdate = await updateStockOutLineStatus({ - id: matchingLot.stockOutLineId, - status: 'checked', - qty: 0 - }); - console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate); - - // Treat multiple backend shapes as success (type-safe via any) - const r: any = stockOutLineUpdate as any; - const updateOk = - r?.code === 'SUCCESS' || - typeof r?.id === 'number' || - r?.type === 'checked' || - r?.status === 'checked' || - typeof r?.entity?.id === 'number' || - r?.entity?.status === 'checked'; - - if (updateOk) { - successCount++; - } else { - errorCount++; - } + // ✅ Use the new second scan API + const result = await updateSecondQrScanStatus( + matchingLot.pickOrderId, + matchingLot.itemId + ); + + if (result.code === "SUCCESS") { + successCount++; + // ✅ Mark this item as processed + setProcessedQrCodes(prev => new Set(prev).add(itemKey)); + console.log(`✅ Second QR scan status updated for item ${matchingLot.itemId}`); } else { - const createStockOutLineData = { - consoCode: matchingLot.pickOrderConsoCode, - pickOrderLineId: matchingLot.pickOrderLineId, - inventoryLotLineId: matchingLot.lotId, - qty: 0 - }; - - const createResult = await createStockOutLine(createStockOutLineData); - console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult); - - if (createResult && createResult.code === "SUCCESS") { - // Immediately set status to checked for new line - let newSolId: number | undefined; - const anyRes: any = createResult as any; - if (typeof anyRes?.id === 'number') { - newSolId = anyRes.id; - } else if (anyRes?.entity) { - newSolId = Array.isArray(anyRes.entity) ? anyRes.entity[0]?.id : anyRes.entity?.id; - } - - if (newSolId) { - const setChecked = await updateStockOutLineStatus({ - id: newSolId, - status: 'checked', - qty: 0 - }); - if (setChecked && setChecked.code === "SUCCESS") { - successCount++; - } else { - errorCount++; - } - } else { - console.warn("Created stock out line but no ID returned; cannot set to checked"); - errorCount++; - } - } else { - errorCount++; - } + console.error(`❌ Failed to update second QR scan status: ${result.message}`); } } - // ✅ FIXED: Set refresh flag before refreshing data - setIsRefreshingData(true); - console.log("🔄 Refreshing data after QR code processing..."); - await fetchAllCombinedLotData(); - if (successCount > 0) { - console.log(`✅ QR Code processing completed: ${successCount} updated/created`); setQrScanSuccess(true); setQrScanError(false); - setQrScanInput(''); // Clear input after successful processing - setIsManualScanning(false); - stopScan(); - resetScan(); - // ✅ Clear success state after a delay - - //setTimeout(() => { - //setQrScanSuccess(false); - //}, 2000); + await fetchJobOrderData(); // Refresh data } else { - console.error(`❌ QR Code processing failed: ${errorCount} errors`); setQrScanError(true); setQrScanSuccess(false); - - // ✅ Clear error state after a delay - // setTimeout(() => { - // setQrScanError(false); - //}, 3000); } } catch (error) { - console.error("❌ Error processing QR code:", error); + console.error("❌ Error processing second QR code:", error); setQrScanError(true); setQrScanSuccess(false); - - // ✅ Still refresh data even on error - setIsRefreshingData(true); - await fetchAllCombinedLotData(); - - // ✅ Clear error state after a delay - setTimeout(() => { - setQrScanError(false); - }, 3000); - } finally { - // ✅ Clear refresh flag after a short delay - setTimeout(() => { - setIsRefreshingData(false); - }, 1000); } - }, [combinedLotData, fetchAllCombinedLotData]); - const processOutsideQrCode = useCallback(async (latestQr: string) => { - // 1) Parse JSON safely - let qrData: any = null; - try { - qrData = JSON.parse(latestQr); - } catch { - console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches."); - setQrScanError(true); - setQrScanSuccess(false); - return; - } - - try { - // Only use the new API when we have JSON with stockInLineId + itemId - if (!(qrData?.stockInLineId && qrData?.itemId)) { - console.log("QR JSON missing required fields (itemId, stockInLineId)."); - setQrScanError(true); - setQrScanSuccess(false); - return; - } + }, [combinedLotData, fetchJobOrderData, processedQrCodes]); - // Call new analyze-qr-code API - const analysis = await analyzeQrCode({ - itemId: qrData.itemId, - stockInLineId: qrData.stockInLineId - }); - - if (!analysis) { - console.error("analyzeQrCode returned no data"); - setQrScanError(true); - setQrScanSuccess(false); - return; - } - - const { - itemId: analyzedItemId, - itemCode: analyzedItemCode, - itemName: analyzedItemName, - scanned, - } = analysis || {}; - - // 1) Find all lots for the same item from current expected list - const sameItemLotsInExpected = combinedLotData.filter(l => - (l.itemId && analyzedItemId && l.itemId === analyzedItemId) || - (l.itemCode && analyzedItemCode && l.itemCode === analyzedItemCode) - ); - - if (!sameItemLotsInExpected || sameItemLotsInExpected.length === 0) { - // Case 3: No item code match - console.error("No item match in expected lots for scanned code"); - setQrScanError(true); - setQrScanSuccess(false); + useEffect(() => { + if (qrValues.length > 0 && combinedLotData.length > 0) { + const latestQr = qrValues[qrValues.length - 1]; + + // ✅ Check if this QR was already processed recently + if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { + console.log("⏭️ QR code already processed, skipping..."); return; } - // ✅ FIXED: Find the ACTIVE suggested lot (not rejected lots) - const activeSuggestedLots = sameItemLotsInExpected.filter(lot => - lot.lotAvailability !== 'rejected' && - lot.stockOutLineStatus !== 'rejected' && - lot.processingStatus !== 'rejected' - ); + // ✅ Mark as processed + setProcessedQrCodes(prev => new Set(prev).add(latestQr)); + setLastProcessedQr(latestQr); - if (activeSuggestedLots.length === 0) { - console.error("No active suggested lots found for this item"); - setQrScanError(true); - setQrScanSuccess(false); - return; + // Extract lot number from QR code + let lotNo = ''; + try { + const qrData = JSON.parse(latestQr); + if (qrData.stockInLineId && qrData.itemId) { + // For JSON QR codes, we need to fetch the lot number + fetchStockInLineInfo(qrData.stockInLineId) + .then((stockInLineInfo) => { + console.log("Outside QR scan - Stock in line info:", stockInLineInfo); + const extractedLotNo = stockInLineInfo.lotNo; + if (extractedLotNo) { + console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`); + handleQrCodeSubmit(extractedLotNo); + } + }) + .catch((error) => { + console.error("Outside QR scan - Error fetching stock in line info:", error); + }); + return; // Exit early for JSON QR codes + } + } catch (error) { + // Not JSON format, treat as direct lot number + lotNo = latestQr.replace(/[{}]/g, ''); } - // 2) Check if scanned lot is exactly in active suggested lots - const exactLotMatch = activeSuggestedLots.find(l => - (scanned?.inventoryLotLineId && l.lotId === scanned.inventoryLotLineId) || - (scanned?.lotNo && l.lotNo === scanned.lotNo) - ); - - if (exactLotMatch && scanned?.lotNo) { - // Case 1: Normal case - item matches AND lot matches -> proceed - console.log(`Exact lot match found for ${scanned.lotNo}, submitting QR`); - handleQrCodeSubmit(scanned.lotNo); - return; + // For direct lot number QR codes + if (lotNo) { + console.log(`Outside QR scan detected (direct): ${lotNo}`); + handleQrCodeSubmit(lotNo); } - - // Case 2: Item matches but lot number differs -> open confirmation modal - // ✅ FIXED: Use the first ACTIVE suggested lot, not just any lot - const expectedLot = activeSuggestedLots[0]; - if (!expectedLot) { - console.error("Could not determine expected lot for confirmation"); - setQrScanError(true); - setQrScanSuccess(false); - return; - } - - // ✅ Check if the expected lot is already the scanned lot (after substitution) - if (expectedLot.lotNo === scanned?.lotNo) { - console.log(`Lot already substituted, proceeding with ${scanned.lotNo}`); - handleQrCodeSubmit(scanned.lotNo); - return; - } - - console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); - setSelectedLotForQr(expectedLot); - handleLotMismatch( - { - lotNo: expectedLot.lotNo, - itemCode: analyzedItemCode || expectedLot.itemCode, - itemName: analyzedItemName || expectedLot.itemName - }, - { - lotNo: scanned?.lotNo || '', - itemCode: analyzedItemCode || expectedLot.itemCode, - itemName: analyzedItemName || expectedLot.itemName, - inventoryLotLineId: scanned?.inventoryLotLineId, - stockInLineId: qrData.stockInLineId - } - ); - } catch (error) { - console.error("Error during analyzeQrCode flow:", error); - setQrScanError(true); - setQrScanSuccess(false); - return; - } - }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch]); - // ✅ Update the outside QR scanning effect to use enhanced processing -// ✅ Update the outside QR scanning effect to use enhanced processing -useEffect(() => { - if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) { - return; - } - - const latestQr = qrValues[qrValues.length - 1]; - - if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { - console.log("QR code already processed, skipping..."); - return; - } - - if (latestQr && latestQr !== lastProcessedQr) { - console.log(`🔍 Processing new QR code with enhanced validation: ${latestQr}`); - setLastProcessedQr(latestQr); - setProcessedQrCodes(prev => new Set(prev).add(latestQr)); - - processOutsideQrCode(latestQr); - } -}, [qrValues, isManualScanning, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode, combinedLotData]); - // ✅ Only fetch existing data when session is ready, no auto-assignment - useEffect(() => { - if (session && currentUserId && !initializationRef.current) { - console.log("✅ Session loaded, initializing pick order..."); - initializationRef.current = true; - - // ✅ Only fetch existing data, no auto-assignment - fetchAllCombinedLotData(); } - }, [session, currentUserId, fetchAllCombinedLotData]); - - // ✅ Add event listener for manual assignment - useEffect(() => { - const handlePickOrderAssigned = () => { - console.log("🔄 Pick order assigned event received, refreshing data..."); - fetchAllCombinedLotData(); - }; - - window.addEventListener('pickOrderAssigned', handlePickOrderAssigned); - - return () => { - window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned); - }; - }, [fetchAllCombinedLotData]); - - - + }, [qrValues, combinedLotData, handleQrCodeSubmit, processedQrCodes, lastProcessedQr]); const handleManualInputSubmit = useCallback(() => { if (qrScanInput.trim() !== '') { handleQrCodeSubmit(qrScanInput.trim()); @@ -933,17 +671,16 @@ useEffect(() => { const lotId = selectedLotForQr.lotId; // Create stock out line - + const stockOutLineData: CreateStockOutLine = { + consoCode: selectedLotForQr.pickOrderConsoCode, + pickOrderLineId: selectedLotForQr.pickOrderLineId, + inventoryLotLineId: selectedLotForQr.lotId, + qty: 0.0 + }; try { - const stockOutLineUpdate = await updateStockOutLineStatus({ - id: selectedLotForQr.stockOutLineId, - status: 'checked', - qty: selectedLotForQr.stockOutLineQty || 0 - }); - console.log("Stock out line updated successfully!"); - setQrScanSuccess(true); - setQrScanError(false); + + // Close modal setQrModalOpen(false); @@ -960,13 +697,50 @@ useEffect(() => { }, 500); // Refresh data - await fetchAllCombinedLotData(); + await fetchJobOrderData(); } catch (error) { console.error("Error creating stock out line:", error); } } - }, [selectedLotForQr, fetchAllCombinedLotData]); + }, [selectedLotForQr, fetchJobOrderData]); + // ✅ Outside QR scanning - process QR codes from outside the page automatically + useEffect(() => { + if (qrValues.length > 0 && combinedLotData.length > 0) { + const latestQr = qrValues[qrValues.length - 1]; + + // Extract lot number from QR code + let lotNo = ''; + try { + const qrData = JSON.parse(latestQr); + if (qrData.stockInLineId && qrData.itemId) { + // For JSON QR codes, we need to fetch the lot number + fetchStockInLineInfo(qrData.stockInLineId) + .then((stockInLineInfo) => { + console.log("Outside QR scan - Stock in line info:", stockInLineInfo); + const extractedLotNo = stockInLineInfo.lotNo; + if (extractedLotNo) { + console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`); + handleQrCodeSubmit(extractedLotNo); + } + }) + .catch((error) => { + console.error("Outside QR scan - Error fetching stock in line info:", error); + }); + return; // Exit early for JSON QR codes + } + } catch (error) { + // Not JSON format, treat as direct lot number + lotNo = latestQr.replace(/[{}]/g, ''); + } + + // For direct lot number QR codes + if (lotNo) { + console.log(`Outside QR scan detected (direct): ${lotNo}`); + handleQrCodeSubmit(lotNo); + } + } + }, [qrValues, combinedLotData, handleQrCodeSubmit]); const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { if (value === '' || value === null || value === undefined) { @@ -995,131 +769,36 @@ useEffect(() => { const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle'); const [autoAssignMessage, setAutoAssignMessage] = useState(''); - const [completionStatus, setCompletionStatus] = useState(null); - const checkAndAutoAssignNext = useCallback(async () => { - if (!currentUserId) return; - - try { - const completionResponse = await checkPickOrderCompletion(currentUserId); - - if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) { - console.log("Found completed pick orders, auto-assigning next..."); - // ✅ 移除前端的自动分配逻辑,因为后端已经处理了 - // await handleAutoAssignAndRelease(); // 删除这个函数 - } - } catch (error) { - console.error("Error checking pick order completion:", error); - } - }, [currentUserId]); - // ✅ Handle submit pick quantity - const handleSubmitPickQty = useCallback(async (lot: any) => { - const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; - const newQty = pickQtyData[lotKey] || 0; - - if (!lot.stockOutLineId) { - console.error("No stock out line found for this lot"); - return; - } - + + + const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => { try { - // ✅ FIXED: Calculate cumulative quantity correctly - const currentActualPickQty = lot.actualPickQty || 0; - const cumulativeQty = currentActualPickQty + newQty; + // ✅ Use the new second scan submit API + const result = await submitSecondScanQuantity( + lot.pickOrderId, + lot.itemId, + { + qty: submitQty, + isMissing: false, + isBad: false, + reason: undefined // ✅ Fix TypeScript error + } + ); - // ✅ FIXED: Determine status based on cumulative quantity vs required quantity - let newStatus = 'partially_completed'; - if (cumulativeQty >= lot.requiredQty) { - newStatus = 'completed'; - } else if (cumulativeQty > 0) { - newStatus = 'partially_completed'; + if (result.code === "SUCCESS") { + console.log(`✅ Second scan quantity submitted: ${submitQty}`); + await fetchJobOrderData(); // Refresh data } else { - newStatus = 'checked'; // QR scanned but no quantity submitted yet + console.error(`❌ Failed to submit second scan quantity: ${result.message}`); } - - console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`); - console.log(`Lot: ${lot.lotNo}`); - console.log(`Required Qty: ${lot.requiredQty}`); - console.log(`Current Actual Pick Qty: ${currentActualPickQty}`); - console.log(`New Submitted Qty: ${newQty}`); - console.log(`Cumulative Qty: ${cumulativeQty}`); - console.log(`New Status: ${newStatus}`); - console.log(`=====================================`); - - await updateStockOutLineStatus({ - id: lot.stockOutLineId, - status: newStatus, - qty: cumulativeQty // ✅ Use cumulative quantity - }); - - if (newQty > 0) { - await updateInventoryLotLineQuantities({ - inventoryLotLineId: lot.lotId, - qty: newQty, - status: 'available', - operation: 'pick' - }); - } - - // ✅ Check if pick order is completed when lot status becomes 'completed' - if (newStatus === 'completed' && lot.pickOrderConsoCode) { - console.log(`✅ Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`); - - try { - const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode); - console.log(`✅ Pick order completion check result:`, completionResponse); - - if (completionResponse.code === "SUCCESS") { - console.log(`�� Pick order ${lot.pickOrderConsoCode} completed successfully!`); - } else if (completionResponse.message === "not completed") { - console.log(`⏳ Pick order not completed yet, more lines remaining`); - } else { - console.error(`❌ Error checking completion: ${completionResponse.message}`); - } - } catch (error) { - console.error("Error checking pick order completion:", error); - } - } - - await fetchAllCombinedLotData(); - console.log("Pick quantity submitted successfully!"); - - setTimeout(() => { - checkAndAutoAssignNext(); - }, 1000); - } catch (error) { - console.error("Error submitting pick quantity:", error); + console.error("Error submitting second scan quantity:", error); } - }, [pickQtyData, fetchAllCombinedLotData, checkAndAutoAssignNext]); - + }, [fetchJobOrderData]); // ✅ Handle reject lot - const handleRejectLot = useCallback(async (lot: any) => { - if (!lot.stockOutLineId) { - console.error("No stock out line found for this lot"); - return; - } - - try { - await updateStockOutLineStatus({ - id: lot.stockOutLineId, - status: 'rejected', - qty: 0 - }); - - await fetchAllCombinedLotData(); - console.log("Lot rejected successfully!"); - - setTimeout(() => { - checkAndAutoAssignNext(); - }, 1000); - - } catch (error) { - console.error("Error rejecting lot:", error); - } - }, [fetchAllCombinedLotData, checkAndAutoAssignNext]); // ✅ Handle pick execution form const handlePickExecutionForm = useCallback((lot: any) => { @@ -1142,8 +821,21 @@ useEffect(() => { const handlePickExecutionFormSubmit = useCallback(async (data: any) => { try { console.log("Pick execution form submitted:", data); - - const result = await recordPickExecutionIssue(data); + if (!currentUserId) { + console.error("❌ No current user ID available"); + return; + } + const result = await recordSecondScanIssue( + selectedLotForExecutionForm.pickOrderId, + selectedLotForExecutionForm.itemId, + { + qty: data.actualPickQty, + isMissing: data.missQty > 0, + isBad: data.badItemQty > 0, + reason: data.issueRemark || '', + createdBy: currentUserId + } + ); console.log("Pick execution issue recorded:", result); if (result && result.code === "SUCCESS") { @@ -1154,19 +846,12 @@ useEffect(() => { setPickExecutionFormOpen(false); setSelectedLotForExecutionForm(null); - setQrScanError(false); - setQrScanSuccess(false); - setQrScanInput(''); - setIsManualScanning(false); - stopScan(); - resetScan(); - setProcessedQrCodes(new Set()); - setLastProcessedQr(''); - await fetchAllCombinedLotData(); + + await fetchJobOrderData(); } catch (error) { console.error("Error submitting pick execution form:", error); } - }, [fetchAllCombinedLotData]); + }, [currentUserId, selectedLotForExecutionForm, fetchJobOrderData,]); // ✅ Calculate remaining required quantity const calculateRemainingRequiredQty = useCallback((lot: any) => { @@ -1248,92 +933,32 @@ useEffect(() => { }, []); // Pagination data with sorting by routerIndex - // Remove the sorting logic and just do pagination const paginatedData = useMemo(() => { - const startIndex = paginationController.pageNum * paginationController.pageSize; - const endIndex = startIndex + paginationController.pageSize; - return combinedLotData.slice(startIndex, endIndex); // ✅ No sorting needed -}, [combinedLotData, paginationController]); -const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => { - if (!lot.stockOutLineId) { - console.error("No stock out line found for this lot"); - return; - } - - try { - // ✅ FIXED: Calculate cumulative quantity correctly - const currentActualPickQty = lot.actualPickQty || 0; - const cumulativeQty = currentActualPickQty + submitQty; - - // ✅ FIXED: Determine status based on cumulative quantity vs required quantity - let newStatus = 'partially_completed'; - - if (cumulativeQty >= lot.requiredQty) { - newStatus = 'completed'; - } else if (cumulativeQty > 0) { - newStatus = 'partially_completed'; - } else { - newStatus = 'checked'; // QR scanned but no quantity submitted yet - } - - console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`); - console.log(`Lot: ${lot.lotNo}`); - console.log(`Required Qty: ${lot.requiredQty}`); - console.log(`Current Actual Pick Qty: ${currentActualPickQty}`); - console.log(`New Submitted Qty: ${submitQty}`); - console.log(`Cumulative Qty: ${cumulativeQty}`); - console.log(`New Status: ${newStatus}`); - console.log(`=====================================`); - - await updateStockOutLineStatus({ - id: lot.stockOutLineId, - status: newStatus, - qty: cumulativeQty // ✅ Use cumulative quantity - }); - - if (submitQty > 0) { - await updateInventoryLotLineQuantities({ - inventoryLotLineId: lot.lotId, - qty: submitQty, - status: 'available', - operation: 'pick' - }); - } - - // ✅ Check if pick order is completed when lot status becomes 'completed' - if (newStatus === 'completed' && lot.pickOrderConsoCode) { - console.log(`✅ Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`); + // ✅ Sort by routerIndex first, then by other criteria + const sortedData = [...combinedLotData].sort((a, b) => { + const aIndex = a.routerIndex || 0; + const bIndex = b.routerIndex || 0; - try { - const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode); - console.log(`✅ Pick order completion check result:`, completionResponse); - - if (completionResponse.code === "SUCCESS") { - console.log(`�� Pick order ${lot.pickOrderConsoCode} completed successfully!`); - } else if (completionResponse.message === "not completed") { - console.log(`⏳ Pick order not completed yet, more lines remaining`); - } else { - console.error(`❌ Error checking completion: ${completionResponse.message}`); - } - } catch (error) { - console.error("Error checking pick order completion:", error); + // Primary sort: by routerIndex + if (aIndex !== bIndex) { + return aIndex - bIndex; } - } - - await fetchAllCombinedLotData(); - console.log("Pick quantity submitted successfully!"); - - setTimeout(() => { - checkAndAutoAssignNext(); - }, 1000); + + // Secondary sort: by pickOrderCode if routerIndex is the same + if (a.pickOrderCode !== b.pickOrderCode) { + return a.pickOrderCode.localeCompare(b.pickOrderCode); + } + + // Tertiary sort: by lotNo if everything else is the same + return (a.lotNo || '').localeCompare(b.lotNo || ''); + }); - } catch (error) { - console.error("Error submitting pick quantity:", error); - } -}, [fetchAllCombinedLotData, checkAndAutoAssignNext]); + const startIndex = paginationController.pageNum * paginationController.pageSize; + const endIndex = startIndex + paginationController.pageSize; + return sortedData.slice(startIndex, endIndex); + }, [combinedLotData, paginationController]); - - // ✅ Add these functions after line 395 + // ✅ Add these functions for manual scanning const handleStartScan = useCallback(() => { console.log(" Starting manual QR scan..."); setIsManualScanning(true); @@ -1352,6 +977,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe stopScan(); resetScan(); }, [stopScan, resetScan]); + const getStatusMessage = useCallback((lot: any) => { switch (lot.stockOutLineStatus?.toLowerCase()) { case 'pending': @@ -1370,43 +996,32 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe return t("Please finish QR code scan and pick order."); } }, [t]); + return ( - - - - {/* DO Header */} - {fgPickOrdersLoading ? ( - - - - ) : ( - fgPickOrders.length > 0 && ( + {/* Job Order Header */} + {jobOrderData && ( - {t("Shop Name")}: {fgPickOrders[0].shopName || '-'} - - - {t("Pick Order Code")}:{fgPickOrders[0].pickOrderCode || '-'} + {t("Job Order")}: {jobOrderData.pickOrder?.jobOrder?.name || '-'} - {t("Store ID")}: {fgPickOrders[0].storeId || '-'} + {t("Pick Order Code")}: {jobOrderData.pickOrder?.code || '-'} - {t("Ticket No.")}: {fgPickOrders[0].ticketNo || '-'} + {t("Target Date")}: {jobOrderData.pickOrder?.targetDate || '-'} - {t("Departure Time")}: {fgPickOrders[0].DepartureTime || '-'} + {t("Status")}: {jobOrderData.pickOrder?.status || '-'} - - ) )} + {/* Combined Lot Table */} @@ -1445,7 +1060,6 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe )} - @@ -1469,21 +1083,15 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe {t("Item Code")} {t("Item Name")} {t("Lot#")} - {/* {t("Target Date")} */} - {/* {t("Lot Location")} */} {t("Lot Required Pick Qty")} - {/* {t("Original Available Qty")} */} {t("Scan Result")} {t("Submit Required Pick Qty")} - {/* {t("Remaining Available Qty")} */} - - {/* {t("Action")} */} {paginatedData.length === 0 ? ( - + {t("No data available")} @@ -1512,7 +1120,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe {lot.itemCode} - {lot.itemName+'('+lot.stockUnit+')'} + {lot.itemName+'('+lot.uomDesc+')'} - {/* {lot.pickOrderTargetDate} */} - {/* {lot.location} */} - {/* {calculateRemainingRequiredQty(lot).toLocaleString()} */} {(() => { - const inQty = lot.inQty || 0; const requiredQty = lot.requiredQty || 0; - const actualPickQty = lot.actualPickQty || 0; - const outQty = lot.outQty || 0; - const result = requiredQty; - return result.toLocaleString()+'('+lot.uomShortDesc+')'; + return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; })()} @@ -1549,12 +1150,12 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe height: '100%' }}> - )) )} - {/* ✅ Status Messages Display - Move here, outside the table */} - {/* -{paginatedData.length > 0 && ( - - {paginatedData.map((lot, index) => ( - - - {t("Lot")} {lot.lotNo}: {getStatusMessage(lot)} - - - ))} - -)} -*/} + - {/* ✅ Lot Confirmation Modal */} - {lotConfirmationOpen && expectedLotData && scannedLotData && ( - { - setLotConfirmationOpen(false); - setExpectedLotData(null); - setScannedLotData(null); - }} - onConfirm={handleLotConfirmation} - expectedLot={expectedLotData} - scannedLot={scannedLotData} - isLoading={isConfirmingLot} - /> - )} - {/* ✅ Good Pick Execution Form Modal */} + + {/* ✅ Pick Execution Form Modal */} {pickExecutionFormOpen && selectedLotForExecutionForm && ( = ({ pickOrders }) => { const [tabIndex, setTabIndex] = useState(0); const [totalCount, setTotalCount] = useState(); const [isAssigning, setIsAssigning] = useState(false); + const [unassignedOrders, setUnassignedOrders] = useState([]); +const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState( typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' ); @@ -125,7 +135,47 @@ const JodetailSearch: React.FC = ({ pickOrders }) => { } }; // ✅ Manual assignment handler - uses the action function - + const loadUnassignedOrders = useCallback(async () => { + setIsLoadingUnassigned(true); + try { + const orders = await fetchUnassignedJobOrderPickOrders(); + setUnassignedOrders(orders); + } catch (error) { + console.error("Error loading unassigned orders:", error); + } finally { + setIsLoadingUnassigned(false); + } + }, []); + + // 分配订单给当前用户 + const handleAssignOrder = useCallback(async (pickOrderId: number) => { + if (!currentUserId) { + console.error("Missing user id in session"); + return; + } + + try { + const result = await assignJobOrderPickOrder(pickOrderId, currentUserId); + if (result.message === "Successfully assigned") { + console.log("✅ Successfully assigned pick order"); + // 刷新数据 + window.dispatchEvent(new CustomEvent('pickOrderAssigned')); + // 重新加载未分配订单列表 + loadUnassignedOrders(); + } else { + console.warn("⚠️ Assignment failed:", result.message); + alert(`Assignment failed: ${result.message}`); + } + } catch (error) { + console.error("❌ Error assigning order:", error); + alert("Error occurred during assignment"); + } + }, [currentUserId, loadUnassignedOrders]); + + // 在组件加载时获取未分配订单 + useEffect(() => { + loadUnassignedOrders(); + }, [loadUnassignedOrders]); const handleTabChange = useCallback>( (_e, newValue) => { @@ -333,80 +383,33 @@ const JodetailSearch: React.FC = ({ pickOrders }) => { - - - {t("Finished Good Order")} - - + {/* Last 2 buttons aligned right */} - - - - + {/* Unassigned Job Orders */} +{unassignedOrders.length > 0 && ( + + + {t("Unassigned Job Orders")} ({unassignedOrders.length}) + + + {unassignedOrders.map((order) => ( + + ))} + + +)} - {/* ✅ Updated print buttons with completion status */} - - -{/* - - */} - - - - - - @@ -419,8 +422,8 @@ const JodetailSearch: React.FC = ({ pickOrders }) => { }}> - - + + @@ -429,9 +432,9 @@ const JodetailSearch: React.FC = ({ pickOrders }) => { - {tabIndex === 0 && } - {tabIndex === 1 && } - {tabIndex === 2 && } + {tabIndex === 0 && } + {tabIndex === 1 && } + {tabIndex === 2 && } );