diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 6572efc..248ea07 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -587,6 +587,7 @@ export interface LotDetailResponse { pickOrderConsoCode: string | null; pickOrderLineId: number | null; stockOutLineId: number | null; + stockInLineId: number | null; suggestedPickLotId: number | null; stockOutLineQty: number | null; stockOutLineStatus: string | null; diff --git a/src/app/api/stockIssue/actions.ts b/src/app/api/stockIssue/actions.ts index 3e2e9bd..790c4fe 100644 --- a/src/app/api/stockIssue/actions.ts +++ b/src/app/api/stockIssue/actions.ts @@ -167,4 +167,60 @@ export async function submitMissItem(issueId: number, handler: number) { body: JSON.stringify({ lotLineIds, handler }), }, ); + } + + + export interface LotIssueDetailResponse { + lotId: number | null; + lotNo: string | null; + itemId: number; + itemCode: string | null; + itemDescription: string | null; + storeLocation: string | null; + issues: IssueDetailItem[]; + } + + export interface IssueDetailItem { + issueId: number; + pickerName: string | null; + missQty: number | null; + issueQty: number | null; + pickOrderCode: string; + doOrderCode: string | null; + joOrderCode: string | null; + issueRemark: string | null; + } + + export async function getLotIssueDetails( + lotId: number, + itemId: number, + issueType: "miss" | "bad" + ) { + return serverFetchJson( + `${BASE_API_URL}/pickExecution/lotIssueDetails?lotId=${lotId}&itemId=${itemId}&issueType=${issueType}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + } + ); + } + + export async function submitIssueWithQty( + lotId: number, + itemId: number, + issueType: "miss" | "bad", + submitQty: number, + handler: number + ){return serverFetchJson( + `${BASE_API_URL}/pickExecution/submitIssueWithQty`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ lotId, itemId, issueType, submitQty, handler }), + } + ); } \ No newline at end of file diff --git a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx index 3251015..549164f 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx @@ -19,7 +19,6 @@ import { TablePagination, Modal, Chip, - LinearProgress, } from "@mui/material"; import dayjs from 'dayjs'; import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider'; @@ -74,38 +73,13 @@ import { SessionWithTokens } from "@/config/authConfig"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; import GoodPickExecutionForm from "./GoodPickExecutionForm"; import FGPickOrderCard from "./FGPickOrderCard"; +import LinearProgressWithLabel from "../common/LinearProgressWithLabel"; +import ScanStatusAlert from "../common/ScanStatusAlert"; interface Props { filterArgs: Record; onSwitchToRecordTab?: () => void; onRefreshReleasedOrderCount?: () => void; } -const LinearProgressWithLabel: React.FC<{ completed: number; total: number }> = ({ completed, total }) => { - const { t } = useTranslation(["pickOrder", "do"]); - const progress = total > 0 ? (completed / total) * 100 : 0; - - return ( - - - - - - - - {t("Progress")}: {completed}/{total} - - - - - - ); -}; // QR Code Modal Component (from LotTable) const QrCodeModal: React.FC<{ open: boolean; @@ -542,6 +516,7 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); const [qrScanInput, setQrScanInput] = useState(''); const [qrScanError, setQrScanError] = useState(false); + const [qrScanErrorMsg, setQrScanErrorMsg] = useState(''); const [qrScanSuccess, setQrScanSuccess] = useState(false); const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false); const [pickQtyData, setPickQtyData] = useState>({}); @@ -1550,15 +1525,86 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); // ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed) const lookupStartTime = performance.now(); const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || []; + // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected + const allLotsForItem = indexes.byItemId.get(scannedItemId) || []; const lookupTime = performance.now() - lookupStartTime; - console.log(`⏱️ [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${activeSuggestedLots.length} active lots`); + console.log(`⏱️ [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${activeSuggestedLots.length} active lots, ${allLotsForItem.length} total lots`); + + // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots + // This allows users to scan other lots even when all suggested lots are rejected + const scannedLot = allLotsForItem.find( + (lot: any) => lot.stockInLineId === scannedStockInLineId + ); + + if (scannedLot) { + const isRejected = + scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' || + scannedLot.lotAvailability === 'rejected' || + scannedLot.lotAvailability === 'status_unavailable'; + + if (isRejected) { + console.warn(`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + setQrScanErrorMsg( + `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。` + ); + }); + // Mark as processed to prevent re-processing + setProcessedQrCombinations(prev => { + const newMap = new Map(prev); + if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); + newMap.get(scannedItemId)!.add(scannedStockInLineId); + return newMap; + }); + return; + } + } + // ✅ If no active suggested lots, but scanned lot is not rejected, allow lot switching if (activeSuggestedLots.length === 0) { - console.error("No active suggested lots found for this item"); - startTransition(() => { - setQrScanError(true); - setQrScanSuccess(false); - }); + // Check if there are any lots for this item (even if all are rejected) + if (allLotsForItem.length === 0) { + console.error("No lots found for this item"); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + setQrScanErrorMsg("当前订单中没有此物品的批次信息"); + }); + return; + } + + // ✅ Allow lot switching: find a rejected lot as expected lot, or use first lot + // This allows users to switch to a new lot even when all suggested lots are rejected + console.log(`⚠️ [QR PROCESS] No active suggested lots, but allowing lot switching.`); + + // Find a rejected lot as expected lot (the one that was rejected) + const rejectedLot = allLotsForItem.find((lot: any) => + lot.stockOutLineStatus?.toLowerCase() === 'rejected' || + lot.lotAvailability === 'rejected' || + lot.lotAvailability === 'status_unavailable' + ); + const expectedLot = rejectedLot || allLotsForItem[0]; // Use rejected lot if exists, otherwise first lot + + // ✅ Always open confirmation modal when no active lots (user needs to confirm switching) + // handleLotMismatch will fetch lotNo from backend using stockInLineId if needed + console.log(`⚠️ [QR PROCESS] Opening confirmation modal for lot switch (no active lots)`); + setSelectedLotForQr(expectedLot); + handleLotMismatch( + { + lotNo: expectedLot.lotNo, + itemCode: expectedLot.itemCode, + itemName: expectedLot.itemName + }, + { + lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null + itemCode: expectedLot.itemCode, + itemName: expectedLot.itemName, + inventoryLotLineId: scannedLot?.lotId || null, + stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo + } + ); return; } @@ -1577,6 +1623,37 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); const matchTime = performance.now() - matchStartTime; console.log(`⏱️ [PERF] Find exact match time: ${matchTime.toFixed(2)}ms, found: ${exactMatch ? 'yes' : 'no'}`); + // ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots + // This handles the case where Lot A is rejected and user scans Lot B + // Also handle case where scanned lot is not in allLotsForItem (scannedLot is undefined) + if (!exactMatch) { + // Scanned lot is not in active suggested lots, open confirmation modal + const expectedLot = activeSuggestedLots[0] || allLotsForItem[0]; // Use first active lot or first lot as expected + if (expectedLot) { + // Check if scanned lot is different from expected, or if scannedLot is undefined (not in allLotsForItem) + const shouldOpenModal = !scannedLot || (scannedLot.stockInLineId !== expectedLot.stockInLineId); + if (shouldOpenModal) { + console.log(`⚠️ [QR PROCESS] Opening confirmation modal (scanned lot ${scannedLot?.lotNo || 'not in data'} is not in active suggested lots)`); + setSelectedLotForQr(expectedLot); + handleLotMismatch( + { + lotNo: expectedLot.lotNo, + itemCode: expectedLot.itemCode, + itemName: expectedLot.itemName + }, + { + lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null + itemCode: expectedLot.itemCode, + itemName: expectedLot.itemName, + inventoryLotLineId: scannedLot?.lotId || null, + stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo + } + ); + return; + } + } + } + if (exactMatch) { // ✅ Case 1: stockInLineId 匹配 - 直接处理,不需要确认 console.log(`✅ Exact stockInLineId match found for lot: ${exactMatch.lotNo}`); @@ -1748,7 +1825,7 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); }); return; } - }, [lotDataIndexes, handleLotMismatch, processedQrCombinations]); + }, [lotDataIndexes, handleLotMismatch, processedQrCombinations, combinedLotData, fetchStockInLineInfoCached]); // Store processOutsideQrCode in ref for immediate access (update on every render) processOutsideQrCodeRef.current = processOutsideQrCode; @@ -2797,23 +2874,33 @@ const handleSubmitAllScanned = useCallback(async () => { - - + sx={{ + position: 'fixed', + top: 0, + left: 0, + right: 0, + zIndex: 1100, // Higher than other elements + backgroundColor: 'background.paper', + pt: 2, + pb: 1, + px: 2, + borderBottom: '1px solid', + borderColor: 'divider', + boxShadow: '0 2px 4px rgba(0,0,0,0.1)', + }} + > + + + {/* DO Header */} @@ -2821,7 +2908,7 @@ const handleSubmitAllScanned = useCallback(async () => { {/* 保留:Combined Lot Table - 包含所有 QR 扫描功能 */} - + {t("All Pick Order Lots")} diff --git a/src/components/Jodetail/FInishedJobOrderRecord.tsx b/src/components/Jodetail/FInishedJobOrderRecord.tsx index a4900b1..e076661 100644 --- a/src/components/Jodetail/FInishedJobOrderRecord.tsx +++ b/src/components/Jodetail/FInishedJobOrderRecord.tsx @@ -339,11 +339,11 @@ const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { {t("Index")} - {t("Route")} + {t("Location")} {t("Item Code")} {t("Item Name")} {t("Lot No")} - {t("Location")} + {t("Required Qty")} {t("Actual Pick Qty")} {t("Processing Status")} @@ -375,7 +375,7 @@ const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { {lot.itemCode} {lot.itemName} {lot.lotNo} - {lot.location} + {lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc}) diff --git a/src/components/Jodetail/completeJobOrderRecord.tsx b/src/components/Jodetail/completeJobOrderRecord.tsx index 53f0f0b..f842630 100644 --- a/src/components/Jodetail/completeJobOrderRecord.tsx +++ b/src/components/Jodetail/completeJobOrderRecord.tsx @@ -463,7 +463,7 @@ const CompleteJobOrderRecord: React.FC = ({ {t("Item Code")} {t("Item Name")} {t("Lot No")} - {t("Location")} + {t("Required Qty")} {t("Actual Pick Qty")} {t("Processing Status")} @@ -495,7 +495,7 @@ const CompleteJobOrderRecord: React.FC = ({ {lot.itemCode} {lot.itemName} {lot.lotNo} - {lot.location} + {lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc}) diff --git a/src/components/Jodetail/newJobPickExecution.tsx b/src/components/Jodetail/newJobPickExecution.tsx index b59ac0e..9c4c618 100644 --- a/src/components/Jodetail/newJobPickExecution.tsx +++ b/src/components/Jodetail/newJobPickExecution.tsx @@ -63,6 +63,8 @@ import { fetchStockInLineInfo } from "@/app/api/po/actions"; import GoodPickExecutionForm from "./JobPickExecutionForm"; import FGPickOrderCard from "./FGPickOrderCard"; import LotConfirmationModal from "./LotConfirmationModal"; +import LinearProgressWithLabel from "../common/LinearProgressWithLabel"; +import ScanStatusAlert from "../common/ScanStatusAlert"; interface Props { filterArgs: Record; //onSwitchToRecordTab: () => void; @@ -1113,11 +1115,84 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const indexes = lotDataIndexes; const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || []; + // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected + const allLotsForItem = indexes.byItemId.get(scannedItemId) || []; + + // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots + // This allows users to scan other lots even when all suggested lots are rejected + const scannedLot = allLotsForItem.find( + (lot: any) => lot.stockInLineId === scannedStockInLineId + ); + + if (scannedLot) { + const isRejected = + scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' || + scannedLot.lotAvailability === 'rejected' || + scannedLot.lotAvailability === 'status_unavailable'; + + if (isRejected) { + console.warn(`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + setQrScanErrorMsg( + `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。` + ); + }); + // Mark as processed to prevent re-processing + setProcessedQrCombinations(prev => { + const newMap = new Map(prev); + if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); + newMap.get(scannedItemId)!.add(scannedStockInLineId); + return newMap; + }); + return; + } + } + + // ✅ If no active suggested lots, but scanned lot is not rejected, allow lot switching if (activeSuggestedLots.length === 0) { - startTransition(() => { - setQrScanError(true); - setQrScanSuccess(false); - }); + // Check if there are any lots for this item (even if all are rejected) + if (allLotsForItem.length === 0) { + console.error("No lots found for this item"); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + setQrScanErrorMsg("当前订单中没有此物品的批次信息"); + }); + return; + } + + // ✅ Allow lot switching: find a rejected lot as expected lot, or use first lot + // This allows users to switch to a new lot even when all suggested lots are rejected + console.log(`⚠️ [QR PROCESS] No active suggested lots, but allowing lot switching. Scanned lot is not rejected.`); + + // Find a rejected lot as expected lot (the one that was rejected) + const rejectedLot = allLotsForItem.find((lot: any) => + lot.stockOutLineStatus?.toLowerCase() === 'rejected' || + lot.lotAvailability === 'rejected' || + lot.lotAvailability === 'status_unavailable' + ); + const expectedLot = rejectedLot || allLotsForItem[0]; // Use rejected lot if exists, otherwise first lot + + // ✅ Always open confirmation modal when no active lots (user needs to confirm switching) + // handleLotMismatch will fetch lotNo from backend using stockInLineId if needed + console.log(`⚠️ [QR PROCESS] Opening confirmation modal for lot switch (no active lots)`); + setSelectedLotForQr(expectedLot); + handleLotMismatch( + { + lotNo: expectedLot.lotNo, + itemCode: expectedLot.itemCode, + itemName: expectedLot.itemName + }, + { + lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null + itemCode: expectedLot.itemCode, + itemName: expectedLot.itemName, + inventoryLotLineId: scannedLot?.lotId || null, + stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo + } + ); return; } @@ -1136,6 +1211,32 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { console.log(`🔍 [QR PROCESS] Found ${stockInLineLots.length} lots with stockInLineId ${scannedStockInLineId}`); console.log(`🔍 [QR PROCESS] Exact match found: ${exactMatch ? `YES (lotNo: ${exactMatch.lotNo}, stockOutLineId: ${exactMatch.stockOutLineId})` : 'NO'}`); + // ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots + // This handles the case where Lot A is rejected and user scans Lot B + if (!exactMatch && scannedLot && !activeSuggestedLots.includes(scannedLot)) { + // Scanned lot is not in active suggested lots, open confirmation modal + const expectedLot = activeSuggestedLots[0] || allLotsForItem[0]; // Use first active lot or first lot as expected + if (expectedLot && scannedLot.stockInLineId !== expectedLot.stockInLineId) { + console.log(`⚠️ [QR PROCESS] Scanned lot ${scannedLot.lotNo} is not in active suggested lots, opening confirmation modal`); + setSelectedLotForQr(expectedLot); + handleLotMismatch( + { + lotNo: expectedLot.lotNo, + itemCode: expectedLot.itemCode, + itemName: expectedLot.itemName + }, + { + lotNo: scannedLot.lotNo || null, + itemCode: expectedLot.itemCode, + itemName: expectedLot.itemName, + inventoryLotLineId: scannedLot.lotId || null, + stockInLineId: scannedStockInLineId + } + ); + return; + } + } + if (exactMatch) { if (!exactMatch.stockOutLineId) { console.error(`❌ [QR PROCESS] Exact match found but no stockOutLineId`); @@ -1216,7 +1317,38 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const stockInLineInfo = await fetchStockInLineInfoCached(scannedStockInLineId); console.log(`✅ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} exists, lotNo: ${stockInLineInfo.lotNo}`); - // ✅ stockInLineId exists, open confirmation modal + // ✅ 检查扫描的批次是否已被拒绝 + const scannedLot = combinedLotData.find( + (lot: any) => lot.stockInLineId === scannedStockInLineId && lot.itemId === scannedItemId + ); + + if (scannedLot) { + const isRejected = + scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' || + scannedLot.lotAvailability === 'rejected' || + scannedLot.lotAvailability === 'status_unavailable'; + + if (isRejected) { + console.warn(`⚠️ [QR PROCESS] Scanned lot ${stockInLineInfo.lotNo} (stockInLineId: ${scannedStockInLineId}) is rejected or unavailable`); + startTransition(() => { + setQrScanError(true); + setQrScanSuccess(false); + setQrScanErrorMsg( + `此批次(${stockInLineInfo.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。` + ); + }); + // Mark as processed to prevent re-processing + setProcessedQrCombinations(prev => { + const newMap = new Map(prev); + if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set()); + newMap.get(scannedItemId)!.add(scannedStockInLineId); + return newMap; + }); + return; + } + } + + // ✅ stockInLineId exists and is not rejected, open confirmation modal console.log(`⚠️ [QR PROCESS] Opening confirmation modal - user must confirm before any lot is marked as scanned`); setSelectedLotForQr(expectedLot); handleLotMismatch( @@ -1251,7 +1383,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { return newMap; }); } - }, [filterArgs?.pickOrderId, fetchJobOrderData, handleLotMismatch, lotDataIndexes, processedQrCombinations]); + }, [filterArgs?.pickOrderId, fetchJobOrderData, handleLotMismatch, lotDataIndexes, processedQrCombinations, combinedLotData, fetchStockInLineInfoCached]); // Store in refs for immediate access in qrValues effect processOutsideQrCodeRef.current = processOutsideQrCode; @@ -1730,6 +1862,23 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const scannedItemsCount = useMemo(() => { return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length; }, [combinedLotData]); + + // Progress bar data (align with Finished Good execution detail) + const progress = useMemo(() => { + if (combinedLotData.length === 0) { + return { completed: 0, total: 0 }; + } + + const nonPendingCount = combinedLotData.filter((lot) => { + const status = lot.stockOutLineStatus?.toLowerCase(); + return status !== 'pending'; + }).length; + + return { + completed: nonPendingCount, + total: combinedLotData.length, + }; + }, [combinedLotData]); // Handle reject lot const handleRejectLot = useCallback(async (lot: any) => { if (!lot.stockOutLineId) { @@ -1944,16 +2093,46 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { return ( ( - lot.lotAvailability !== 'rejected' && - lot.stockOutLineStatus !== 'rejected' && - lot.stockOutLineStatus !== 'completed' - )} - > - - + lotData={combinedLotData} + onScanLot={handleQrCodeSubmit} + filterActive={(lot) => ( + lot.lotAvailability !== 'rejected' && + lot.stockOutLineStatus !== 'rejected' && + lot.stockOutLineStatus !== 'completed' + )} + > + + + {/* Progress bar + scan status fixed at top */} + + + + + {/* Job Order Header */} {jobOrderData && ( @@ -1974,7 +2153,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { {/* Combined Lot Table */} - + @@ -2020,60 +2199,6 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { - - {qrScanError && !qrScanSuccess && ( - - {qrScanErrorMsg || t("QR code does not match any item in current orders.")} - - )} - {qrScanSuccess && ( - - {t("QR code verified.")} - - )} diff --git a/src/components/ProductionProcess/BagConsumptionForm.tsx b/src/components/ProductionProcess/BagConsumptionForm.tsx index 8d5483c..afee258 100644 --- a/src/components/ProductionProcess/BagConsumptionForm.tsx +++ b/src/components/ProductionProcess/BagConsumptionForm.tsx @@ -38,7 +38,7 @@ interface BagConsumptionFormProps { jobOrderId: number; lineId: number; bomDescription?: string; - isLastLine: boolean; + processName?: string; submitedBagRecord?: boolean; onRefresh?: () => void; } @@ -47,7 +47,7 @@ const BagConsumptionForm: React.FC = ({ jobOrderId, lineId, bomDescription, - isLastLine, + processName, submitedBagRecord, onRefresh, }) => { @@ -65,8 +65,8 @@ const BagConsumptionForm: React.FC = ({ if (submitedBagRecord === true) { return false; } - return bomDescription === "FG" && isLastLine; - }, [bomDescription, isLastLine, submitedBagRecord]); + return processName === "包裝"; + }, [processName, submitedBagRecord]); // 加载 Bag 列表 useEffect(() => { diff --git a/src/components/ProductionProcess/ProductionProcessStepExecution.tsx b/src/components/ProductionProcess/ProductionProcessStepExecution.tsx index fb9eca6..dd293c1 100644 --- a/src/components/ProductionProcess/ProductionProcessStepExecution.tsx +++ b/src/components/ProductionProcess/ProductionProcessStepExecution.tsx @@ -102,20 +102,10 @@ const ProductionProcessStepExecution: React.FC { - if (!processData || !allLines || !lineDetail) return false; - - // 检查 BOM description 是否为 "FG" - const bomDescription = processData.bomDescription; - if (bomDescription !== "FG") return false; - - // 检查是否是最后一个 process line(按 seqNo 排序) - const sortedLines = [...allLines].sort((a, b) => (a.seqNo || 0) - (b.seqNo || 0)); - const maxSeqNo = sortedLines[sortedLines.length - 1]?.seqNo; - const isLastLine = lineDetail.seqNo === maxSeqNo; - - return isLastLine; - }, [processData, allLines, lineDetail]); + const isPackagingProcess = useMemo(() => { + if (!lineDetail) return false; + return lineDetail.name === "包裝"; + }, [lineDetail]) // ✅ 添加:刷新 line detail 的函数 const handleRefreshLineDetail = useCallback(async () => { @@ -981,12 +971,12 @@ const ProductionProcessStepExecution: React.FC diff --git a/src/components/StockIssue/SearchPage.tsx b/src/components/StockIssue/SearchPage.tsx index dad5252..d14b622 100644 --- a/src/components/StockIssue/SearchPage.tsx +++ b/src/components/StockIssue/SearchPage.tsx @@ -18,6 +18,7 @@ import { } from "@/app/api/stockIssue/actions"; import { Box, Button, Tab, Tabs } from "@mui/material"; import { useSession } from "next-auth/react"; +import SubmitIssueForm from "./SubmitIssueForm"; interface Props { dataList: StockIssueLists; @@ -34,6 +35,11 @@ const SearchPage: React.FC = ({ dataList }) => { const [search, setSearch] = useState({ lotNo: "" }); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; + const [formOpen, setFormOpen] = useState(false); + const [selectedLotId, setSelectedLotId] = useState(null); + const [selectedItemId, setSelectedItemId] = useState(0); + const [selectedIssueType, setSelectedIssueType] = useState<"miss" | "bad">("miss"); + const [missItems, setMissItems] = useState( dataList.missItems, ); @@ -76,34 +82,47 @@ const SearchPage: React.FC = ({ dataList }) => { return; } - setSubmittingIds((prev) => new Set(prev).add(id)); - try { - if (tab === "miss") { - await submitMissItem(id, currentUserId); - setMissItems((prev) => prev.filter((i) => i.id !== id)); - } else if (tab === "bad") { - await submitBadItem(id, currentUserId); - setBadItems((prev) => prev.filter((i) => i.id !== id)); - } else { - await submitExpiryItem(id, currentUserId); - setExpiryItems((prev) => prev.filter((i) => i.id !== id)); + // Find the item to get lotId + let lotId: number | null = null; + let itemId = 0; + + if (tab === "miss") { + const item = missItems.find((i) => i.id === id); + if (item) { + lotId = item.lotId; + itemId = item.itemId; + } + } else if (tab === "bad") { + const item = badItems.find((i) => i.id === id); + if (item) { + lotId = item.lotId; + itemId = item.itemId; } - // Remove from selectedIds if it was selected - setSelectedIds((prev) => prev.filter((selectedId) => selectedId !== id)); - } catch (error) { - console.error("Failed to submit item:", error); - alert(`Failed to submit: ${error instanceof Error ? error.message : "Unknown error"}`); - } finally { - setSubmittingIds((prev) => { - const newSet = new Set(prev); - newSet.delete(id); - return newSet; - }); + } + + if (lotId && itemId) { + setSelectedLotId(lotId); + setSelectedItemId(itemId); + setSelectedIssueType(tab === "miss" ? "miss" : "bad"); + setFormOpen(true); + } else { + alert(t("Item not found")); } }, - [tab, currentUserId, t], + [tab, currentUserId, t, missItems, badItems] ); + const handleFormSuccess = useCallback(() => { + // Refresh the lists + if (tab === "miss") { + // Reload miss items - you may need to add a refresh function + window.location.reload(); // Or use a proper refresh mechanism + } else if (tab === "bad") { + // Reload bad items + window.location.reload(); // Or use a proper refresh mechanism + } + }, [tab]); + const handleSubmitSelected = useCallback(async () => { if (!currentUserId) return; @@ -299,6 +318,15 @@ const SearchPage: React.FC = ({ dataList }) => { {renderCurrentTab()} + setFormOpen(false)} + lotId={selectedLotId} + itemId={selectedItemId} + issueType={selectedIssueType} + currentUserId={currentUserId || 0} + onSuccess={handleFormSuccess} + /> ); }; diff --git a/src/components/StockIssue/SubmitIssueForm.tsx b/src/components/StockIssue/SubmitIssueForm.tsx new file mode 100644 index 0000000..4ee5c22 --- /dev/null +++ b/src/components/StockIssue/SubmitIssueForm.tsx @@ -0,0 +1,187 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Box, + Typography, +} from "@mui/material"; +import { + getLotIssueDetails, + submitIssueWithQty, + LotIssueDetailResponse, +} from "@/app/api/stockIssue/actions"; +import { useTranslation } from "react-i18next"; + +interface Props { + open: boolean; + onClose: () => void; + lotId: number | null; + itemId: number; + issueType: "miss" | "bad"; + currentUserId: number; + onSuccess: () => void; +} + +const SubmitIssueForm: React.FC = ({ + open, + onClose, + lotId, + itemId, + issueType, + currentUserId, + onSuccess, +}) => { + const { t } = useTranslation("inventory"); + const [loading, setLoading] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [details, setDetails] = useState(null); + const [submitQty, setSubmitQty] = useState(""); + + useEffect(() => { + if (open && lotId) { + loadDetails(); + } + }, [open, lotId, itemId, issueType]); + + const loadDetails = async () => { + if (!lotId) return; + setLoading(true); + try { + const data = await getLotIssueDetails(lotId, itemId, issueType); + setDetails(data); + // Set default qty to sum of issueQty (for bad) or missQty (for miss) + const defaultQty = issueType === "bad" + ? data.issues.reduce((sum, issue) => sum + (issue.issueQty || 0), 0) + : data.issues.reduce((sum, issue) => sum + (issue.missQty || 0), 0); + setSubmitQty(defaultQty.toString()); + } catch (error) { + console.error("Failed to load details:", error); + alert("Failed to load issue details"); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async () => { + if (!lotId || !submitQty || parseFloat(submitQty) <= 0) { + alert(t("Please enter a valid quantity")); + return; + } + + setSubmitting(true); + try { + await submitIssueWithQty( + lotId, + itemId, + issueType, + parseFloat(submitQty), + currentUserId + ); + onSuccess(); + onClose(); + } catch (error) { + console.error("Failed to submit:", error); + alert(`Failed to submit: ${error instanceof Error ? error.message : "Unknown error"}`); + } finally { + setSubmitting(false); + } + }; + + if (!details) { + return null; + } + + return ( + + + {issueType === "miss" ? t("Submit Miss Item") : t("Submit Bad Item")} + + + + + {t("Item Code")}: {details.itemCode} + + + {t("Item")}: {details.itemDescription} + + + {t("Lot No.")}: {details.lotNo} + + + {t("Location")}: {details.storeLocation} + + + + +
+ + + {t("Picker Name")} + + {issueType === "miss" ? t("Miss Qty") : t("Issue Qty")} + + {t("Pick Order Code")} + {t("DO Order Code")} + {t("JO Order Code")} + {t("Remark")} + + + + {details.issues.map((issue) => ( + + {issue.pickerName || "-"} + + {issueType === "miss" + ? issue.missQty?.toFixed(2) || "0" + : issue.issueQty?.toFixed(2) || "0"} + + {issue.pickOrderCode} + {issue.doOrderCode || "-"} + {issue.joOrderCode || "-"} + {issue.issueRemark || "-"} + + ))} + +
+
+ + setSubmitQty(e.target.value)} + inputProps={{ min: 0, step: 0.01 }} + sx={{ mt: 2 }} + /> + + + + + + + ); +}; + +export default SubmitIssueForm; \ No newline at end of file diff --git a/src/components/common/LinearProgressWithLabel.tsx b/src/components/common/LinearProgressWithLabel.tsx new file mode 100644 index 0000000..5c8b82d --- /dev/null +++ b/src/components/common/LinearProgressWithLabel.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { Box, LinearProgress, Typography } from "@mui/material"; +import React from "react"; + +interface LinearProgressWithLabelProps { + completed: number; + total: number; + label: string; +} + +const LinearProgressWithLabel: React.FC = ({ + completed, + total, + label, +}) => { + const progress = total > 0 ? (completed / total) * 100 : 0; + + return ( + + + + + + + + + {label}: {completed}/{total} + + + + + + ); +}; + +export default LinearProgressWithLabel; + + + + + diff --git a/src/components/common/ScanStatusAlert.tsx b/src/components/common/ScanStatusAlert.tsx new file mode 100644 index 0000000..4aab110 --- /dev/null +++ b/src/components/common/ScanStatusAlert.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { Alert } from "@mui/material"; +import React, { ReactNode } from "react"; + +interface ScanStatusAlertProps { + error: boolean; + success: boolean; + errorMessage?: ReactNode; + successMessage?: ReactNode; +} + +const ScanStatusAlert: React.FC = ({ + error, + success, + errorMessage, + successMessage, +}) => { + if (error && !success) { + return ( + + {errorMessage} + + ); + } + + if (success) { + return ( + + {successMessage} + + ); + } + + return null; +}; + +export default ScanStatusAlert; + + + + +