| @@ -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; | |||
| @@ -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<LotIssueDetailResponse>( | |||
| `${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<MessageResponse>( | |||
| `${BASE_API_URL}/pickExecution/submitIssueWithQty`, | |||
| { | |||
| method: "POST", | |||
| headers: { | |||
| "Content-Type": "application/json", | |||
| }, | |||
| body: JSON.stringify({ lotId, itemId, issueType, submitQty, handler }), | |||
| } | |||
| ); | |||
| } | |||
| @@ -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<string, any>; | |||
| 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 ( | |||
| <Box sx={{ width: '100%', mb: 2 }}> | |||
| <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> | |||
| <Box sx={{ width: '100%', mr: 1 }}> | |||
| <LinearProgress | |||
| variant="determinate" | |||
| value={progress} | |||
| sx={{ | |||
| height: 30, // ✅ Increase height from default (4px) to 10px | |||
| borderRadius: 5, // ✅ Add rounded corners | |||
| }} | |||
| /> | |||
| </Box> | |||
| <Box sx={{ minWidth: 80 }}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| <strong>{t("Progress")}: {completed}/{total}</strong> | |||
| </Typography> | |||
| </Box> | |||
| </Box> | |||
| </Box> | |||
| ); | |||
| }; | |||
| // 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<string>(''); | |||
| const [qrScanError, setQrScanError] = useState<boolean>(false); | |||
| const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>(''); | |||
| const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false); | |||
| const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false); | |||
| const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({}); | |||
| @@ -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 () => { | |||
| <FormProvider {...formProps}> | |||
| <Stack spacing={2}> | |||
| <Box | |||
| 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)', | |||
| }} | |||
| > | |||
| <LinearProgressWithLabel completed={progress.completed} total={progress.total} /> | |||
| </Box> | |||
| 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)', | |||
| }} | |||
| > | |||
| <LinearProgressWithLabel | |||
| completed={progress.completed} | |||
| total={progress.total} | |||
| label={t("Progress")} | |||
| /> | |||
| <ScanStatusAlert | |||
| error={qrScanError} | |||
| success={qrScanSuccess} | |||
| errorMessage={t("QR code does not match any item in current orders.")} | |||
| successMessage={t("QR code verified.")} | |||
| /> | |||
| </Box> | |||
| {/* DO Header */} | |||
| @@ -2821,7 +2908,7 @@ const handleSubmitAllScanned = useCallback(async () => { | |||
| {/* 保留:Combined Lot Table - 包含所有 QR 扫描功能 */} | |||
| <Box> | |||
| <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> | |||
| <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, mt: 10 }}> | |||
| <Typography variant="h6" gutterBottom sx={{ mb: 0 }}> | |||
| {t("All Pick Order Lots")} | |||
| </Typography> | |||
| @@ -339,11 +339,11 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell>{t("Index")}</TableCell> | |||
| <TableCell>{t("Route")}</TableCell> | |||
| <TableCell>{t("Location")}</TableCell> | |||
| <TableCell>{t("Item Code")}</TableCell> | |||
| <TableCell>{t("Item Name")}</TableCell> | |||
| <TableCell>{t("Lot No")}</TableCell> | |||
| <TableCell>{t("Location")}</TableCell> | |||
| <TableCell align="right">{t("Required Qty")}</TableCell> | |||
| <TableCell align="right">{t("Actual Pick Qty")}</TableCell> | |||
| <TableCell align="center">{t("Processing Status")}</TableCell> | |||
| @@ -375,7 +375,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| <TableCell>{lot.itemCode}</TableCell> | |||
| <TableCell>{lot.itemName}</TableCell> | |||
| <TableCell>{lot.lotNo}</TableCell> | |||
| <TableCell>{lot.location}</TableCell> | |||
| <TableCell align="right"> | |||
| {lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc}) | |||
| </TableCell> | |||
| @@ -463,7 +463,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||
| <TableCell>{t("Item Code")}</TableCell> | |||
| <TableCell>{t("Item Name")}</TableCell> | |||
| <TableCell>{t("Lot No")}</TableCell> | |||
| <TableCell>{t("Location")}</TableCell> | |||
| <TableCell align="right">{t("Required Qty")}</TableCell> | |||
| <TableCell align="right">{t("Actual Pick Qty")}</TableCell> | |||
| <TableCell align="center">{t("Processing Status")}</TableCell> | |||
| @@ -495,7 +495,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||
| <TableCell>{lot.itemCode}</TableCell> | |||
| <TableCell>{lot.itemName}</TableCell> | |||
| <TableCell>{lot.lotNo}</TableCell> | |||
| <TableCell>{lot.location}</TableCell> | |||
| <TableCell align="right"> | |||
| {lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc}) | |||
| </TableCell> | |||
| @@ -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<string, any>; | |||
| //onSwitchToRecordTab: () => void; | |||
| @@ -1113,11 +1115,84 @@ const JobPickExecution: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ filterArgs, onBackToList }) => { | |||
| return ( | |||
| <TestQrCodeProvider | |||
| lotData={combinedLotData} | |||
| onScanLot={handleQrCodeSubmit} | |||
| filterActive={(lot) => ( | |||
| lot.lotAvailability !== 'rejected' && | |||
| lot.stockOutLineStatus !== 'rejected' && | |||
| lot.stockOutLineStatus !== 'completed' | |||
| )} | |||
| > | |||
| <FormProvider {...formProps}> | |||
| <Stack spacing={2}> | |||
| lotData={combinedLotData} | |||
| onScanLot={handleQrCodeSubmit} | |||
| filterActive={(lot) => ( | |||
| lot.lotAvailability !== 'rejected' && | |||
| lot.stockOutLineStatus !== 'rejected' && | |||
| lot.stockOutLineStatus !== 'completed' | |||
| )} | |||
| > | |||
| <FormProvider {...formProps}> | |||
| <Stack spacing={2}> | |||
| {/* Progress bar + scan status fixed at top */} | |||
| <Box | |||
| sx={{ | |||
| position: 'fixed', | |||
| top: 0, | |||
| left: 0, | |||
| right: 0, | |||
| zIndex: 1100, | |||
| backgroundColor: 'background.paper', | |||
| pt: 2, | |||
| pb: 1, | |||
| px: 2, | |||
| borderBottom: '1px solid', | |||
| borderColor: 'divider', | |||
| boxShadow: '0 2px 4px rgba(0,0,0,0.1)', | |||
| }} | |||
| > | |||
| <LinearProgressWithLabel | |||
| completed={progress.completed} | |||
| total={progress.total} | |||
| label={t("Progress")} | |||
| /> | |||
| <ScanStatusAlert | |||
| error={qrScanError} | |||
| success={qrScanSuccess} | |||
| errorMessage={qrScanErrorMsg || t("QR code does not match any item in current orders.")} | |||
| successMessage={t("QR code verified.")} | |||
| /> | |||
| </Box> | |||
| {/* Job Order Header */} | |||
| {jobOrderData && ( | |||
| <Paper sx={{ p: 2 }}> | |||
| @@ -1974,7 +2153,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||
| {/* Combined Lot Table */} | |||
| <Box> | |||
| <Box sx={{ mt: 10 }}> | |||
| <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> | |||
| @@ -2020,60 +2199,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||
| </Box> | |||
| </Box> | |||
| {qrScanError && !qrScanSuccess && ( | |||
| <Alert | |||
| severity="error" | |||
| sx={{ | |||
| mb: 2, | |||
| display: "flex", | |||
| justifyContent: "center", | |||
| alignItems: "center", | |||
| fontWeight: "bold", | |||
| fontSize: "1rem", | |||
| color: "error.main", // ✅ 整个 Alert 文字用错误红 | |||
| "& .MuiAlert-message": { | |||
| width: "100%", | |||
| textAlign: "center", | |||
| // color: "error.main", // ✅ 明确指定 message 文字颜色 | |||
| }, | |||
| "& .MuiSvgIcon-root": { | |||
| color: "error.main", // 图标继续红色(可选) | |||
| }, | |||
| backgroundColor: "error.light", | |||
| }} | |||
| > | |||
| {qrScanErrorMsg || t("QR code does not match any item in current orders.")} | |||
| </Alert> | |||
| )} | |||
| {qrScanSuccess && ( | |||
| <Alert | |||
| severity="success" | |||
| sx={{ | |||
| mb: 2, | |||
| display: "flex", | |||
| justifyContent: "center", | |||
| alignItems: "center", | |||
| fontWeight: "bold", | |||
| fontSize: "1rem", | |||
| // 背景用很浅的绿色 | |||
| bgcolor: "rgba(76, 175, 80, 0.08)", | |||
| // 文字用主题 success 绿 | |||
| color: "success.main", | |||
| // 去掉默认强烈的色块感 | |||
| "& .MuiAlert-icon": { | |||
| color: "success.main", | |||
| }, | |||
| "& .MuiAlert-message": { | |||
| width: "100%", | |||
| textAlign: "center", | |||
| color: "success.main", | |||
| }, | |||
| }} | |||
| > | |||
| {t("QR code verified.")} | |||
| </Alert> | |||
| )} | |||
| <TableContainer component={Paper}> | |||
| <Table> | |||
| @@ -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<BagConsumptionFormProps> = ({ | |||
| jobOrderId, | |||
| lineId, | |||
| bomDescription, | |||
| isLastLine, | |||
| processName, | |||
| submitedBagRecord, | |||
| onRefresh, | |||
| }) => { | |||
| @@ -65,8 +65,8 @@ const BagConsumptionForm: React.FC<BagConsumptionFormProps> = ({ | |||
| if (submitedBagRecord === true) { | |||
| return false; | |||
| } | |||
| return bomDescription === "FG" && isLastLine; | |||
| }, [bomDescription, isLastLine, submitedBagRecord]); | |||
| return processName === "包裝"; | |||
| }, [processName, submitedBagRecord]); | |||
| // 加载 Bag 列表 | |||
| useEffect(() => { | |||
| @@ -102,20 +102,10 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| const [pauseReason, setPauseReason] = useState(""); | |||
| // ✅ 添加:判断是否显示 Bag 表单的条件 | |||
| const shouldShowBagForm = useMemo(() => { | |||
| 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<ProductionProcessStepExecutionPro | |||
| )} | |||
| {/* ========== Bag Consumption Form ========== */} | |||
| {((showOutputTable || isCompleted) && shouldShowBagForm && jobOrderId && lineId) && ( | |||
| {((showOutputTable || isCompleted) && isPackagingProcess && jobOrderId && lineId) && ( | |||
| <BagConsumptionForm | |||
| jobOrderId={jobOrderId} | |||
| lineId={lineId} | |||
| bomDescription={processData?.bomDescription} | |||
| isLastLine={shouldShowBagForm} | |||
| processName={lineDetail?.name} | |||
| submitedBagRecord={lineDetail?.submitedBagRecord} | |||
| onRefresh={handleRefreshLineDetail} | |||
| /> | |||
| @@ -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<Props> = ({ dataList }) => { | |||
| const [search, setSearch] = useState<SearchQuery>({ 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<number | null>(null); | |||
| const [selectedItemId, setSelectedItemId] = useState<number>(0); | |||
| const [selectedIssueType, setSelectedIssueType] = useState<"miss" | "bad">("miss"); | |||
| const [missItems, setMissItems] = useState<StockIssueResult[]>( | |||
| dataList.missItems, | |||
| ); | |||
| @@ -76,34 +82,47 @@ const SearchPage: React.FC<Props> = ({ 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<Props> = ({ dataList }) => { | |||
| </Box> | |||
| {renderCurrentTab()} | |||
| <SubmitIssueForm | |||
| open={formOpen} | |||
| onClose={() => setFormOpen(false)} | |||
| lotId={selectedLotId} | |||
| itemId={selectedItemId} | |||
| issueType={selectedIssueType} | |||
| currentUserId={currentUserId || 0} | |||
| onSuccess={handleFormSuccess} | |||
| /> | |||
| </Box> | |||
| ); | |||
| }; | |||
| @@ -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<Props> = ({ | |||
| 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<LotIssueDetailResponse | null>(null); | |||
| const [submitQty, setSubmitQty] = useState<string>(""); | |||
| 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 ( | |||
| <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth> | |||
| <DialogTitle> | |||
| {issueType === "miss" ? t("Submit Miss Item") : t("Submit Bad Item")} | |||
| </DialogTitle> | |||
| <DialogContent> | |||
| <Box sx={{ mb: 2 }}> | |||
| <Typography variant="body2" sx={{ mb: 1 }}> | |||
| <strong>{t("Item Code")}:</strong> {details.itemCode} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ mb: 1 }}> | |||
| <strong>{t("Item")}:</strong> {details.itemDescription} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ mb: 1 }}> | |||
| <strong>{t("Lot No.")}:</strong> {details.lotNo} | |||
| </Typography> | |||
| <Typography variant="body2" sx={{ mb: 2 }}> | |||
| <strong>{t("Location")}:</strong> {details.storeLocation} | |||
| </Typography> | |||
| </Box> | |||
| <TableContainer component={Paper} sx={{ mb: 2 }}> | |||
| <Table size="small"> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell>{t("Picker Name")}</TableCell> | |||
| <TableCell align="right"> | |||
| {issueType === "miss" ? t("Miss Qty") : t("Issue Qty")} | |||
| </TableCell> | |||
| <TableCell>{t("Pick Order Code")}</TableCell> | |||
| <TableCell>{t("DO Order Code")}</TableCell> | |||
| <TableCell>{t("JO Order Code")}</TableCell> | |||
| <TableCell>{t("Remark")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {details.issues.map((issue) => ( | |||
| <TableRow key={issue.issueId}> | |||
| <TableCell>{issue.pickerName || "-"}</TableCell> | |||
| <TableCell align="right"> | |||
| {issueType === "miss" | |||
| ? issue.missQty?.toFixed(2) || "0" | |||
| : issue.issueQty?.toFixed(2) || "0"} | |||
| </TableCell> | |||
| <TableCell>{issue.pickOrderCode}</TableCell> | |||
| <TableCell>{issue.doOrderCode || "-"}</TableCell> | |||
| <TableCell>{issue.joOrderCode || "-"}</TableCell> | |||
| <TableCell>{issue.issueRemark || "-"}</TableCell> | |||
| </TableRow> | |||
| ))} | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| <TextField | |||
| fullWidth | |||
| label={t("Submit Quantity")} | |||
| type="number" | |||
| value={submitQty} | |||
| onChange={(e) => setSubmitQty(e.target.value)} | |||
| inputProps={{ min: 0, step: 0.01 }} | |||
| sx={{ mt: 2 }} | |||
| /> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| <Button onClick={onClose} disabled={submitting}> | |||
| {t("Cancel")} | |||
| </Button> | |||
| <Button | |||
| onClick={handleSubmit} | |||
| variant="contained" | |||
| disabled={submitting || !submitQty || parseFloat(submitQty) <= 0} | |||
| > | |||
| {submitting ? t("Submitting...") : t("Submit")} | |||
| </Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| ); | |||
| }; | |||
| export default SubmitIssueForm; | |||
| @@ -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<LinearProgressWithLabelProps> = ({ | |||
| completed, | |||
| total, | |||
| label, | |||
| }) => { | |||
| const progress = total > 0 ? (completed / total) * 100 : 0; | |||
| return ( | |||
| <Box sx={{ width: "100%", mb: 2 }}> | |||
| <Box sx={{ display: "flex", alignItems: "center", mb: 1 }}> | |||
| <Box sx={{ width: "100%", mr: 1 }}> | |||
| <LinearProgress | |||
| variant="determinate" | |||
| value={progress} | |||
| sx={{ | |||
| height: 30, | |||
| borderRadius: 5, | |||
| }} | |||
| /> | |||
| </Box> | |||
| <Box sx={{ minWidth: 80 }}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| <strong> | |||
| {label}: {completed}/{total} | |||
| </strong> | |||
| </Typography> | |||
| </Box> | |||
| </Box> | |||
| </Box> | |||
| ); | |||
| }; | |||
| export default LinearProgressWithLabel; | |||
| @@ -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<ScanStatusAlertProps> = ({ | |||
| error, | |||
| success, | |||
| errorMessage, | |||
| successMessage, | |||
| }) => { | |||
| if (error && !success) { | |||
| return ( | |||
| <Alert | |||
| severity="error" | |||
| sx={{ | |||
| mb: 2, | |||
| display: "flex", | |||
| justifyContent: "center", | |||
| alignItems: "center", | |||
| fontWeight: "bold", | |||
| fontSize: "1rem", | |||
| color: "error.main", | |||
| "& .MuiAlert-message": { | |||
| width: "100%", | |||
| textAlign: "center", | |||
| }, | |||
| "& .MuiSvgIcon-root": { | |||
| color: "error.main", | |||
| }, | |||
| backgroundColor: "error.light", | |||
| }} | |||
| > | |||
| {errorMessage} | |||
| </Alert> | |||
| ); | |||
| } | |||
| if (success) { | |||
| return ( | |||
| <Alert | |||
| severity="success" | |||
| sx={{ | |||
| mb: 2, | |||
| display: "flex", | |||
| justifyContent: "center", | |||
| alignItems: "center", | |||
| fontWeight: "bold", | |||
| fontSize: "1rem", | |||
| bgcolor: "rgba(76, 175, 80, 0.08)", | |||
| color: "success.main", | |||
| "& .MuiAlert-icon": { | |||
| color: "success.main", | |||
| }, | |||
| "& .MuiAlert-message": { | |||
| width: "100%", | |||
| textAlign: "center", | |||
| color: "success.main", | |||
| }, | |||
| }} | |||
| > | |||
| {successMessage} | |||
| </Alert> | |||
| ); | |||
| } | |||
| return null; | |||
| }; | |||
| export default ScanStatusAlert; | |||