| @@ -417,6 +417,8 @@ export async function fetchWorkbenchCompletedLotDetails( | |||||
| export type WorkbenchScanPayload = { | export type WorkbenchScanPayload = { | ||||
| itemId: number; | itemId: number; | ||||
| stockInLineId: number; | stockInLineId: number; | ||||
| /** POL UomConversion id — when set, sameItemLots only include matching lot UOM */ | |||||
| uomId?: number | null; | |||||
| }; | }; | ||||
| export async function fetchWorkbenchPrinters() { | export async function fetchWorkbenchPrinters() { | ||||
| return serverFetchJson<any[]>(`${BASE_API_URL}/printers`, { | return serverFetchJson<any[]>(`${BASE_API_URL}/printers`, { | ||||
| @@ -433,9 +435,16 @@ export async function analyzeWorkbenchQrCode(payload: WorkbenchScanPayload) { | |||||
| }); | }); | ||||
| } | } | ||||
| export async function fetchWorkbenchAvailableLotsByItem(itemId: number) { | |||||
| export async function fetchWorkbenchAvailableLotsByItem( | |||||
| itemId: number, | |||||
| uomId?: number | null, | |||||
| ) { | |||||
| const qs = | |||||
| uomId != null && Number.isFinite(Number(uomId)) && Number(uomId) > 0 | |||||
| ? `?uomId=${Number(uomId)}` | |||||
| : ""; | |||||
| return serverFetchJson<any>( | return serverFetchJson<any>( | ||||
| `${BASE_API_URL}/inventoryLotLine/workbench/available-lots-by-item/${itemId}`, | |||||
| `${BASE_API_URL}/inventoryLotLine/workbench/available-lots-by-item/${itemId}${qs}`, | |||||
| { | { | ||||
| method: "GET", | method: "GET", | ||||
| cache: "no-store", | cache: "no-store", | ||||
| @@ -23,6 +23,7 @@ export interface LotLineInfo { | |||||
| lotNo: string; | lotNo: string; | ||||
| remainingQty: number; | remainingQty: number; | ||||
| uom: string; | uom: string; | ||||
| uomId: number; | |||||
| } | } | ||||
| export interface SearchInventoryLotLine extends Pageable { | export interface SearchInventoryLotLine extends Pageable { | ||||
| @@ -170,7 +171,10 @@ async function fetchInventoriesLatestImpl(data: SearchInventory) { | |||||
| export const fetchInventories = cache(fetchInventoriesImpl); | export const fetchInventories = cache(fetchInventoriesImpl); | ||||
| /** Inventory search page: latest inventory row per item (no baseUnit/uomId filter). */ | |||||
| /** | |||||
| * FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.0 | 2026-07-17 | |||||
| * Inventory search page: latest inventory row per item (no baseUnit/uomId filter). | |||||
| */ | |||||
| export const fetchInventoriesLatest = cache(fetchInventoriesLatestImpl); | export const fetchInventoriesLatest = cache(fetchInventoriesLatestImpl); | ||||
| /** Bypass React cache() after mutations so lists show fresh qty. */ | /** Bypass React cache() after mutations so lists show fresh qty. */ | ||||
| @@ -95,6 +95,30 @@ export interface ItemWithDetails { | |||||
| uomDesc: string; | uomDesc: string; | ||||
| currentStockBalance: number; | currentStockBalance: number; | ||||
| } | } | ||||
| export interface PickUomOption { | |||||
| uomId: number; | |||||
| uomCode: string; | |||||
| uomDesc: string; | |||||
| } | |||||
| export interface AvailablePickUomsResponse { | |||||
| itemId: number; | |||||
| stockUom: PickUomOption | null; | |||||
| options: PickUomOption[]; | |||||
| } | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 35 | v1.0.1 | 2026-07-22 */ | |||||
| export const fetchAvailablePickUoms = cache(async (itemId: number) => { | |||||
| return serverFetchJson<AvailablePickUomsResponse>( | |||||
| `${BASE_API_URL}/items/${itemId}/available-pick-uoms`, | |||||
| { | |||||
| method: "GET", | |||||
| next: { tags: ["items"] }, | |||||
| }, | |||||
| ); | |||||
| }); | |||||
| export const fetchItemsWithDetails = cache(async (searchParams?: Record<string, any>) => { | export const fetchItemsWithDetails = cache(async (searchParams?: Record<string, any>) => { | ||||
| if (searchParams) { | if (searchParams) { | ||||
| const queryString = new URLSearchParams(searchParams).toString(); | const queryString = new URLSearchParams(searchParams).toString(); | ||||
| @@ -551,6 +551,7 @@ function saveIssuePickedMap(doPickOrderId: number, map: Record<number, number>) | |||||
| } | } | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 29 | v1.0.1 | 2026-07-22 */ | |||||
| const WorkbenchGoodPickExecutionDetail: React.FC<Props> = ({ | const WorkbenchGoodPickExecutionDetail: React.FC<Props> = ({ | ||||
| filterArgs, | filterArgs, | ||||
| onSwitchToRecordTab, | onSwitchToRecordTab, | ||||
| @@ -1076,6 +1077,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| itemId: line.item.id, | itemId: line.item.id, | ||||
| itemCode: line.item.code, | itemCode: line.item.code, | ||||
| itemName: line.item.name, | itemName: line.item.name, | ||||
| uomId: line.item.uomId ?? null, | |||||
| uomDesc: line.item.uomDesc, | uomDesc: line.item.uomDesc, | ||||
| uomShortDesc: line.item.uomShortDesc, | uomShortDesc: line.item.uomShortDesc, | ||||
| @@ -1147,6 +1149,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| itemId: line.item.id, | itemId: line.item.id, | ||||
| itemCode: line.item.code, | itemCode: line.item.code, | ||||
| itemName: line.item.name, | itemName: line.item.name, | ||||
| uomId: line.item.uomId ?? null, | |||||
| uomDesc: line.item.uomDesc, | uomDesc: line.item.uomDesc, | ||||
| uomShortDesc: line.item.uomShortDesc, | uomShortDesc: line.item.uomShortDesc, | ||||
| @@ -1688,14 +1691,91 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| }); | }); | ||||
| return; | return; | ||||
| } | } | ||||
| // ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed) | // ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed) | ||||
| const lookupStartTime = performance.now(); | const lookupStartTime = performance.now(); | ||||
| const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || []; | |||||
| let 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 | // ✅ 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) || []; | |||||
| let allLotsForItem = indexes.byItemId.get(scannedItemId) || []; | |||||
| const lookupTime = performance.now() - lookupStartTime; | const lookupTime = performance.now() - lookupStartTime; | ||||
| console.log(` [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${activeSuggestedLots.length} active lots, ${allLotsForItem.length} total lots`); | console.log(` [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${activeSuggestedLots.length} active lots, ${allLotsForItem.length} total lots`); | ||||
| // Case A: reject only if scanned lot UOM matches none of this item's POL UOMs | |||||
| // (same item may have multiple lines with different UOMs on one pick order) | |||||
| let scannedUomId = 0; | |||||
| const allowedUomIds = new Set( | |||||
| allLotsForItem | |||||
| .map((l: any) => Number(l?.uomId)) | |||||
| .filter((id: number) => Number.isFinite(id) && id > 0), | |||||
| ); | |||||
| const hasLineUoms = allowedUomIds.size > 0; | |||||
| try { | |||||
| const lotDetail = await fetchLotDetail(scannedStockInLineId); | |||||
| scannedUomId = Number(lotDetail?.uomId) || 0; | |||||
| } catch (e) { | |||||
| if (hasLineUoms) { | |||||
| console.warn( | |||||
| "[QR PROCESS] lot-detail UOM check failed; rejecting scan", | |||||
| e, | |||||
| ); | |||||
| startTransition(() => { | |||||
| setQrScanError(true); | |||||
| setQrScanSuccess(false); | |||||
| setQrScanErrorMsg( | |||||
| t( | |||||
| "This lot UOM does not match the pick line. Please scan another lot.", | |||||
| ), | |||||
| ); | |||||
| }); | |||||
| lastProcessedQrRef.current = ""; | |||||
| processedQrCodesRef.current.delete(latestQr); | |||||
| return; | |||||
| } | |||||
| } | |||||
| if ( | |||||
| hasLineUoms && | |||||
| (!(scannedUomId > 0) || !allowedUomIds.has(scannedUomId)) | |||||
| ) { | |||||
| console.warn( | |||||
| ` [QR PROCESS] UOM mismatch: allowed=[${Array.from(allowedUomIds).join(",")}], scannedLot=${scannedUomId}, stockInLineId=${scannedStockInLineId}`, | |||||
| ); | |||||
| startTransition(() => { | |||||
| setQrScanError(true); | |||||
| setQrScanSuccess(false); | |||||
| setQrScanErrorMsg( | |||||
| t( | |||||
| "This lot UOM does not match the pick line. Please scan another lot.", | |||||
| ), | |||||
| ); | |||||
| }); | |||||
| lastProcessedQrRef.current = ""; | |||||
| processedQrCodesRef.current.delete(latestQr); | |||||
| return; | |||||
| } | |||||
| // Strict UOM filter — never fall back to other UOM lines | |||||
| if (scannedUomId > 0 && hasLineUoms) { | |||||
| allLotsForItem = allLotsForItem.filter( | |||||
| (l: any) => Number(l?.uomId) === scannedUomId, | |||||
| ); | |||||
| activeSuggestedLots = activeSuggestedLots.filter( | |||||
| (l: any) => Number(l?.uomId) === scannedUomId, | |||||
| ); | |||||
| if (allLotsForItem.length === 0) { | |||||
| startTransition(() => { | |||||
| setQrScanError(true); | |||||
| setQrScanSuccess(false); | |||||
| setQrScanErrorMsg( | |||||
| t( | |||||
| "This lot UOM does not match the pick line. Please scan another lot.", | |||||
| ), | |||||
| ); | |||||
| }); | |||||
| lastProcessedQrRef.current = ""; | |||||
| processedQrCodesRef.current.delete(latestQr); | |||||
| return; | |||||
| } | |||||
| } | |||||
| // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots | // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots | ||||
| // This allows users to scan other lots even when all suggested lots are rejected | // This allows users to scan other lots even when all suggested lots are rejected | ||||
| @@ -2646,7 +2726,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| const qrDetectionStartTime = performance.now(); | const qrDetectionStartTime = performance.now(); | ||||
| console.log(` [QR DETECTION] Latest QR detected: ${latestQr?.substring(0, 50)}...`); | console.log(` [QR DETECTION] Latest QR detected: ${latestQr?.substring(0, 50)}...`); | ||||
| console.log(` [QR DETECTION] Detection time: ${new Date().toISOString()}`); | |||||
| //console.log(` [QR DETECTION] Detection time: ${new Date().toISOString()}`); | |||||
| console.log(` [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`); | console.log(` [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`); | ||||
| const qrPayload = parseWorkbenchScanQrPayload(latestQr); | const qrPayload = parseWorkbenchScanQrPayload(latestQr); | ||||
| @@ -3457,7 +3537,7 @@ const handleStartScan = useCallback(() => { | |||||
| resetScan(); | resetScan(); | ||||
| lastConsumedQrValuesLengthRef.current = 0; | lastConsumedQrValuesLengthRef.current = 0; | ||||
| }, [stopScan, resetScan]); | }, [stopScan, resetScan]); | ||||
| // ... existing code around line 1469 ... | |||||
| const handlelotnull = useCallback(async (lot: any) => { | const handlelotnull = useCallback(async (lot: any) => { | ||||
| // 优先使用 stockouts 中的 id,如果没有则使用 stockOutLineId | // 优先使用 stockouts 中的 id,如果没有则使用 stockOutLineId | ||||
| const stockOutLineId = lot.stockOutLineId; | const stockOutLineId = lot.stockOutLineId; | ||||
| @@ -4106,7 +4186,7 @@ paginatedData.map((row, index) => { | |||||
| ? Number(fromPickRow) | ? Number(fromPickRow) | ||||
| : lockedSubmitQtyDisplay; | : lockedSubmitQtyDisplay; | ||||
| // 检查是否是 issue lot | |||||
| const isIssueLot = lot.stockOutLineStatus === 'rejected' || !lot.lotNo; | const isIssueLot = lot.stockOutLineStatus === 'rejected' || !lot.lotNo; | ||||
| const rejectDisplay = buildLotRejectDisplayMessage(lot, scanRejectMessageBySolId, t); | const rejectDisplay = buildLotRejectDisplayMessage(lot, scanRejectMessageBySolId, t); | ||||
| const solSt = String(lot.stockOutLineStatus || "").toLowerCase(); | const solSt = String(lot.stockOutLineStatus || "").toLowerCase(); | ||||
| @@ -4131,14 +4211,20 @@ paginatedData.map((row, index) => { | |||||
| }} | }} | ||||
| > | > | ||||
| <TableCell> | <TableCell> | ||||
| <Typography variant="body2" fontWeight="bold"> | |||||
| <Typography variant="body1" fontWeight="bold"> | |||||
| {row.isGroupFirst ? row.groupDisplayIndex : ""} | {row.isGroupFirst ? row.groupDisplayIndex : ""} | ||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell>{row.isGroupFirst ? lot.itemCode : ""}</TableCell> | |||||
| <TableCell> | <TableCell> | ||||
| <Typography variant="body1"> | |||||
| {row.isGroupFirst ? lot.itemCode : ""} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Typography variant="body1"> | |||||
| {row.isGroupFirst ? lot.itemName + '(' + lot.stockUnit + ')' : ""} | {row.isGroupFirst ? lot.itemName + '(' + lot.stockUnit + ')' : ""} | ||||
| </Typography> | |||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | <TableCell> | ||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| @@ -4146,7 +4232,7 @@ paginatedData.map((row, index) => { | |||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | <TableCell> | ||||
| <Stack direction="row" spacing={1} alignItems="flex-start"> | |||||
| <Stack direction="row" spacing={1} alignItems="center"> | |||||
| <Box sx={{ flex: 1, minWidth: 0 }}> | <Box sx={{ flex: 1, minWidth: 0 }}> | ||||
| {(() => { | {(() => { | ||||
| const hasLotNo = Boolean(lot.lotNo); | const hasLotNo = Boolean(lot.lotNo); | ||||
| @@ -4165,7 +4251,7 @@ paginatedData.map((row, index) => { | |||||
| : 'inherit'; | : 'inherit'; | ||||
| return ( | return ( | ||||
| <Typography | <Typography | ||||
| variant={isIssueText ? "body2" : "body1"} | |||||
| variant={isIssueText ? "body2" : "body2"} | |||||
| sx={{ | sx={{ | ||||
| color: textColor, | color: textColor, | ||||
| whiteSpace: 'pre-wrap', | whiteSpace: 'pre-wrap', | ||||
| @@ -4213,7 +4299,7 @@ paginatedData.map((row, index) => { | |||||
| sx={{ | sx={{ | ||||
| flexShrink: 0, | flexShrink: 0, | ||||
| fontSize: "0.75rem", | fontSize: "0.75rem", | ||||
| py: 0.25, | |||||
| //py: 0.25, | |||||
| minWidth: "auto", | minWidth: "auto", | ||||
| px: 1, | px: 1, | ||||
| whiteSpace: "nowrap", | whiteSpace: "nowrap", | ||||
| @@ -4556,6 +4642,12 @@ paginatedData.map((row, index) => { | |||||
| ).trim() || null | ).trim() || null | ||||
| : null | : null | ||||
| } | } | ||||
| expectedUomId={ | |||||
| workbenchLotLabelContextLot != null && | |||||
| Number(workbenchLotLabelContextLot.uomId) > 0 | |||||
| ? Number(workbenchLotLabelContextLot.uomId) | |||||
| : null | |||||
| } | |||||
| disableScanPick={workbenchLotLabelScanPickDisabled} | disableScanPick={workbenchLotLabelScanPickDisabled} | ||||
| onWorkbenchScanPick={handleWorkbenchLotLabelScanPick} | onWorkbenchScanPick={handleWorkbenchLotLabelScanPick} | ||||
| submitQty={workbenchLotLabelSubmitQty} | submitQty={workbenchLotLabelSubmitQty} | ||||
| @@ -63,6 +63,8 @@ type QrCodeAnalysisResponse = { | |||||
| inventoryLotLineId: number; | inventoryLotLineId: number; | ||||
| warehouseCode?: string | null; | warehouseCode?: string | null; | ||||
| warehouseName?: string | null; | warehouseName?: string | null; | ||||
| uom?: string | null; | |||||
| uomId?: number | null; | |||||
| } | null; | } | null; | ||||
| sameItemLots: Array<{ | sameItemLots: Array<{ | ||||
| lotNo: string; | lotNo: string; | ||||
| @@ -70,6 +72,7 @@ type QrCodeAnalysisResponse = { | |||||
| stockInLineId?: number | null; | stockInLineId?: number | null; | ||||
| availableQty: number; | availableQty: number; | ||||
| uom: string; | uom: string; | ||||
| uomId?: number | null; | |||||
| warehouseCode?: string | null; | warehouseCode?: string | null; | ||||
| warehouseName?: string | null; | warehouseName?: string | null; | ||||
| }>; | }>; | ||||
| @@ -95,6 +98,8 @@ export interface WorkbenchLotLabelPrintModalProps { | |||||
| /** 提貨台表格列上的可用量/單位(API 的 sameItemLots 不含掃描行,需補上才能顯示「目前這筆」) */ | /** 提貨台表格列上的可用量/單位(API 的 sameItemLots 不含掃描行,需補上才能顯示「目前這筆」) */ | ||||
| triggerLotAvailableQty?: number | null; | triggerLotAvailableQty?: number | null; | ||||
| triggerLotUom?: string | null; | triggerLotUom?: string | null; | ||||
| /** POL UomConversion id — workbench lot list only returns matching UOM */ | |||||
| expectedUomId?: number | null; | |||||
| /** 此出庫行已掃碼/已完成時為 true,停用所有「掃碼提貨」(仍可列印標籤) */ | /** 此出庫行已掃碼/已完成時為 true,停用所有「掃碼提貨」(仍可列印標籤) */ | ||||
| disableScanPick?: boolean; | disableScanPick?: boolean; | ||||
| /** | /** | ||||
| @@ -141,6 +146,7 @@ function isLabelPrinter(p: Printer): boolean { | |||||
| return s.includes("label") && !s.includes("a4"); | return s.includes("label") && !s.includes("a4"); | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 30 | v1.0.1 | 2026-07-22 */ | |||||
| const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = ({ | const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = ({ | ||||
| open, | open, | ||||
| onClose, | onClose, | ||||
| @@ -155,6 +161,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| hideTriggeredLot = false, | hideTriggeredLot = false, | ||||
| triggerLotAvailableQty = null, | triggerLotAvailableQty = null, | ||||
| triggerLotUom = null, | triggerLotUom = null, | ||||
| expectedUomId = null, | |||||
| disableScanPick = false, | disableScanPick = false, | ||||
| onWorkbenchScanPick, | onWorkbenchScanPick, | ||||
| submitQty = null, | submitQty = null, | ||||
| @@ -257,13 +264,22 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| if (id != null) setSelectedPrinterId(id); | if (id != null) setSelectedPrinterId(id); | ||||
| }, [open, printers, selectedPrinterId, pickDefaultPrinterId]); | }, [open, printers, selectedPrinterId, pickDefaultPrinterId]); | ||||
| const resolveExpectedUomId = useCallback((): number | null => { | |||||
| const n = Number(expectedUomId); | |||||
| return Number.isFinite(n) && n > 0 ? n : null; | |||||
| }, [expectedUomId]); | |||||
| const analyzePayload = useCallback( | const analyzePayload = useCallback( | ||||
| async (payload: ScanPayload) => { | async (payload: ScanPayload) => { | ||||
| setLastPayload(payload); | setLastPayload(payload); | ||||
| setScanError(null); | setScanError(null); | ||||
| setAnalysisLoading(true); | setAnalysisLoading(true); | ||||
| try { | try { | ||||
| const data = (await analyzeWorkbenchQrCode(payload)) as QrCodeAnalysisResponse; | |||||
| const uomId = resolveExpectedUomId(); | |||||
| const data = (await analyzeWorkbenchQrCode({ | |||||
| ...payload, | |||||
| ...(uomId != null ? { uomId } : {}), | |||||
| })) as QrCodeAnalysisResponse; | |||||
| setAnalysis(data); | setAnalysis(data); | ||||
| setSnackbar({ | setSnackbar({ | ||||
| open: true, | open: true, | ||||
| @@ -277,7 +293,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| setAnalysisLoading(false); | setAnalysisLoading(false); | ||||
| } | } | ||||
| }, | }, | ||||
| [], | |||||
| [resolveExpectedUomId], | |||||
| ); | ); | ||||
| const analyzeByItem = useCallback( | const analyzeByItem = useCallback( | ||||
| @@ -290,7 +306,11 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| setScanError(null); | setScanError(null); | ||||
| setAnalysisLoading(true); | setAnalysisLoading(true); | ||||
| try { | try { | ||||
| const data = (await fetchWorkbenchAvailableLotsByItem(itemId)) as { | |||||
| const uomId = resolveExpectedUomId(); | |||||
| const data = (await fetchWorkbenchAvailableLotsByItem( | |||||
| itemId, | |||||
| uomId, | |||||
| )) as { | |||||
| itemId: number; | itemId: number; | ||||
| itemCode: string; | itemCode: string; | ||||
| itemName: string; | itemName: string; | ||||
| @@ -315,7 +335,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| setAnalysisLoading(false); | setAnalysisLoading(false); | ||||
| } | } | ||||
| }, | }, | ||||
| [], | |||||
| [resolveExpectedUomId], | |||||
| ); | ); | ||||
| const handleAnalyze = useCallback(async () => { | const handleAnalyze = useCallback(async () => { | ||||
| @@ -381,30 +401,51 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = | |||||
| Number.isFinite(tableQty) && tableQty >= 0 ? tableQty : 0; | Number.isFinite(tableQty) && tableQty >= 0 ? tableQty : 0; | ||||
| const fromApi = Number(scannedRow?.availableQty ?? 0); | const fromApi = Number(scannedRow?.availableQty ?? 0); | ||||
| const scanned = analysis.scanned; | const scanned = analysis.scanned; | ||||
| const scannedLot = scannedLotLineId | |||||
| ? { | |||||
| lotNo: scanned?.lotNo ?? "", | |||||
| inventoryLotLineId: scannedLotLineId, | |||||
| stockInLineId: Number(scanned?.stockInLineId ?? 0) || null, | |||||
| availableQty: Math.max(fromApi, fromTable) as number, | |||||
| uom: (scannedRow?.uom ?? triggerLotUom ?? "") as string, | |||||
| warehouseCode: | |||||
| scanned?.warehouseCode ?? scannedRow?.warehouseCode, | |||||
| warehouseName: | |||||
| scanned?.warehouseName ?? scannedRow?.warehouseName, | |||||
| _scanned: true as const, | |||||
| } | |||||
| : null; | |||||
| const expectUom = Number(expectedUomId); | |||||
| const hasExpectUom = Number.isFinite(expectUom) && expectUom > 0; | |||||
| const scannedUomId = Number(scanned?.uomId ?? scannedRow?.uomId ?? 0); | |||||
| const scannedUomOk = | |||||
| !hasExpectUom || | |||||
| !Number.isFinite(scannedUomId) || | |||||
| scannedUomId <= 0 || | |||||
| scannedUomId === expectUom; | |||||
| const scannedLot = | |||||
| scannedLotLineId && scannedUomOk | |||||
| ? { | |||||
| lotNo: scanned?.lotNo ?? "", | |||||
| inventoryLotLineId: scannedLotLineId, | |||||
| stockInLineId: Number(scanned?.stockInLineId ?? 0) || null, | |||||
| availableQty: Math.max(fromApi, fromTable) as number, | |||||
| uom: (scanned?.uom ?? scannedRow?.uom ?? triggerLotUom ?? "") as string, | |||||
| uomId: scannedUomId > 0 ? scannedUomId : null, | |||||
| warehouseCode: | |||||
| scanned?.warehouseCode ?? scannedRow?.warehouseCode, | |||||
| warehouseName: | |||||
| scanned?.warehouseName ?? scannedRow?.warehouseName, | |||||
| _scanned: true as const, | |||||
| } | |||||
| : null; | |||||
| const merged = [ | const merged = [ | ||||
| ...(!hideTriggeredLot && scannedLot ? [scannedLot] : []), | ...(!hideTriggeredLot && scannedLot ? [scannedLot] : []), | ||||
| ...list | ...list | ||||
| .filter((x) => x.inventoryLotLineId !== scannedLotLineId) | .filter((x) => x.inventoryLotLineId !== scannedLotLineId) | ||||
| .filter((x) => { | |||||
| if (!hasExpectUom) return true; | |||||
| const id = Number(x.uomId); | |||||
| return !Number.isFinite(id) || id <= 0 || id === expectUom; | |||||
| }) | |||||
| .map((x) => ({ ...x, _scanned: false as const })), | .map((x) => ({ ...x, _scanned: false as const })), | ||||
| ]; | ]; | ||||
| return merged; | return merged; | ||||
| }, [analysis, hideTriggeredLot, triggerLotAvailableQty, triggerLotUom]); | |||||
| }, [ | |||||
| analysis, | |||||
| hideTriggeredLot, | |||||
| triggerLotAvailableQty, | |||||
| triggerLotUom, | |||||
| expectedUomId, | |||||
| ]); | |||||
| const filteredLots = useMemo(() => { | const filteredLots = useMemo(() => { | ||||
| const prefix = String(warehouseCodePrefixFilter ?? "").trim(); | const prefix = String(warehouseCodePrefixFilter ?? "").trim(); | ||||
| @@ -56,6 +56,7 @@ type SearchQuery = Partial< | |||||
| >; | >; | ||||
| type SearchParamNames = keyof SearchQuery; | type SearchParamNames = keyof SearchQuery; | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.0 | 2026-07-17 */ | |||||
| const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | ||||
| const { t } = useTranslation(['inventory', 'common', 'item']); | const { t } = useTranslation(['inventory', 'common', 'item']); | ||||
| @@ -50,7 +50,9 @@ interface ItemRow { | |||||
| id: string; | id: string; | ||||
| pickOrderId: number; | pickOrderId: number; | ||||
| pickOrderCode: string; | pickOrderCode: string; | ||||
| pickOrderLineId?: number; | |||||
| itemId: number; | itemId: number; | ||||
| uomId?: number; | |||||
| itemCode: string; | itemCode: string; | ||||
| itemName: string; | itemName: string; | ||||
| requiredQty: number; | requiredQty: number; | ||||
| @@ -85,6 +87,7 @@ const style = { | |||||
| width: { xs: "100%", sm: "100%", md: "100%" }, | width: { xs: "100%", sm: "100%", md: "100%" }, | ||||
| }; | }; | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 31 | v1.0.1 | 2026-07-22 */ | |||||
| const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | ||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| const { setIsUploading } = useUploadContext(); | const { setIsUploading } = useUploadContext(); | ||||
| @@ -167,11 +170,13 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| // const filteredRecords = res.records.filter( | // const filteredRecords = res.records.filter( | ||||
| // (item: any) => (item.status || "").toLowerCase() === "pending" | // (item: any) => (item.status || "").toLowerCase() === "pending" | ||||
| //); | //); | ||||
| const itemRows: ItemRow[] = res.records.map((item: any) => ({ | |||||
| id: item.id, | |||||
| const itemRows: ItemRow[] = res.records.map((item: any, index: number) => ({ | |||||
| id: item.id ?? `${item.pickOrderId}-${item.pickOrderLineId ?? item.itemId}-${index}`, | |||||
| pickOrderId: item.pickOrderId, | pickOrderId: item.pickOrderId, | ||||
| pickOrderCode: item.pickOrderCode, | pickOrderCode: item.pickOrderCode, | ||||
| pickOrderLineId: item.pickOrderLineId, | |||||
| itemId: item.itemId, | itemId: item.itemId, | ||||
| uomId: item.uomId, | |||||
| itemCode: item.itemCode, | itemCode: item.itemCode, | ||||
| itemName: item.itemName, | itemName: item.itemName, | ||||
| requiredQty: item.requiredQty, | requiredQty: item.requiredQty, | ||||
| @@ -493,7 +498,7 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| ) : ( | ) : ( | ||||
| groupedItems.map((group) => ( | groupedItems.map((group) => ( | ||||
| group.items.map((item, index) => ( | group.items.map((item, index) => ( | ||||
| <TableRow key={item.id}> | |||||
| <TableRow key={item.id || `${group.pickOrderId}-${item.pickOrderLineId ?? item.itemId}-${index}`}> | |||||
| {/* Checkbox - 只在第一个项目显示,按 pick order 选择 */} | {/* Checkbox - 只在第一个项目显示,按 pick order 选择 */} | ||||
| <TableCell> | <TableCell> | ||||
| {index === 0 ? ( | {index === 0 ? ( | ||||
| @@ -1,6 +1,5 @@ | |||||
| import React, { useCallback } from 'react'; | import React, { useCallback } from 'react'; | ||||
| import { | import { | ||||
| Box, | |||||
| Typography, | Typography, | ||||
| Table, | Table, | ||||
| TableBody, | TableBody, | ||||
| @@ -43,15 +42,16 @@ interface Group { | |||||
| interface CreatedItemsTableProps { | interface CreatedItemsTableProps { | ||||
| items: CreatedItem[]; | items: CreatedItem[]; | ||||
| groups: Group[]; | groups: Group[]; | ||||
| onItemSelect: (itemId: number, checked: boolean) => void; | |||||
| onQtyChange: (itemId: number, qty: number) => void; | |||||
| onGroupChange: (itemId: number, groupId: string) => void; | |||||
| onItemSelect: (itemId: number, checked: boolean, uomId?: number) => void; | |||||
| onQtyChange: (itemId: number, qty: number, uomId?: number) => void; | |||||
| onGroupChange: (itemId: number, groupId: string, uomId?: number) => void; | |||||
| pageNum: number; | pageNum: number; | ||||
| pageSize: number; | pageSize: number; | ||||
| onPageChange: (event: unknown, newPage: number) => void; | onPageChange: (event: unknown, newPage: number) => void; | ||||
| onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void; | onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void; | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 31 | v1.0.1 | 2026-07-22 */ | |||||
| const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({ | const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({ | ||||
| items, | items, | ||||
| groups, | groups, | ||||
| @@ -65,15 +65,14 @@ const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({ | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| // Calculate pagination | |||||
| const startIndex = (pageNum - 1) * pageSize; | const startIndex = (pageNum - 1) * pageSize; | ||||
| const endIndex = startIndex + pageSize; | const endIndex = startIndex + pageSize; | ||||
| const paginatedItems = items.slice(startIndex, endIndex); | const paginatedItems = items.slice(startIndex, endIndex); | ||||
| const handleQtyChange = useCallback((itemId: number, value: string) => { | |||||
| const handleQtyChange = useCallback((itemId: number, uomId: number, value: string) => { | |||||
| const numValue = Number(value); | const numValue = Number(value); | ||||
| if (!isNaN(numValue) && numValue >= 1) { | if (!isNaN(numValue) && numValue >= 1) { | ||||
| onQtyChange(itemId, numValue); | |||||
| onQtyChange(itemId, numValue, uomId); | |||||
| } | } | ||||
| }, [onQtyChange]); | }, [onQtyChange]); | ||||
| @@ -117,11 +116,11 @@ const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({ | |||||
| </TableRow> | </TableRow> | ||||
| ) : ( | ) : ( | ||||
| paginatedItems.map((item) => ( | paginatedItems.map((item) => ( | ||||
| <TableRow key={item.itemId}> | |||||
| <TableRow key={`${item.itemId}_${item.uomId}`}> | |||||
| <TableCell padding="checkbox"> | <TableCell padding="checkbox"> | ||||
| <Checkbox | <Checkbox | ||||
| checked={item.isSelected} | checked={item.isSelected} | ||||
| onChange={(e) => onItemSelect(item.itemId, e.target.checked)} | |||||
| onChange={(e) => onItemSelect(item.itemId, e.target.checked, item.uomId)} | |||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | <TableCell> | ||||
| @@ -134,7 +133,7 @@ const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({ | |||||
| <FormControl size="small" sx={{ minWidth: 120 }}> | <FormControl size="small" sx={{ minWidth: 120 }}> | ||||
| <Select | <Select | ||||
| value={item.groupId?.toString() || ""} | value={item.groupId?.toString() || ""} | ||||
| onChange={(e) => onGroupChange(item.itemId, e.target.value)} | |||||
| onChange={(e) => onGroupChange(item.itemId, e.target.value, item.uomId)} | |||||
| displayEmpty | displayEmpty | ||||
| > | > | ||||
| <MenuItem value=""> | <MenuItem value=""> | ||||
| @@ -157,14 +156,14 @@ const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({ | |||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <Typography variant="body2">{item.uomDesc}</Typography> | |||||
| <Typography variant="body2">{item.uomDesc || item.uom || "-"}</Typography> | |||||
| </TableCell> | </TableCell> | ||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <TextField | <TextField | ||||
| type="number" | type="number" | ||||
| size="small" | size="small" | ||||
| value={item.qty || ""} | value={item.qty || ""} | ||||
| onChange={(e) => handleQtyChange(item.itemId, e.target.value)} | |||||
| onChange={(e) => handleQtyChange(item.itemId, item.uomId, e.target.value)} | |||||
| inputProps={{ | inputProps={{ | ||||
| min: 1, | min: 1, | ||||
| step: 1, | step: 1, | ||||
| @@ -208,4 +207,4 @@ const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({ | |||||
| ); | ); | ||||
| }; | }; | ||||
| export default CreatedItemsTable; | |||||
| export default CreatedItemsTable; | |||||
| @@ -1,6 +1,5 @@ | |||||
| import React, { useCallback } from 'react'; | import React, { useCallback } from 'react'; | ||||
| import { | import { | ||||
| Box, | |||||
| Typography, | Typography, | ||||
| Table, | Table, | ||||
| TableBody, | TableBody, | ||||
| @@ -22,8 +21,12 @@ import dayjs from 'dayjs'; | |||||
| interface SearchItemWithQty { | interface SearchItemWithQty { | ||||
| id: number; | id: number; | ||||
| /** Unique search-row key: itemId + uomId */ | |||||
| rowKey: string; | |||||
| label: string; | label: string; | ||||
| qty: number | null; | qty: number | null; | ||||
| uomId?: number; | |||||
| uom?: string; | |||||
| currentStockBalance?: number; | currentStockBalance?: number; | ||||
| uomDesc?: string; | uomDesc?: string; | ||||
| targetDate?: string | null; | targetDate?: string | null; | ||||
| @@ -40,17 +43,18 @@ interface SearchResultsTableProps { | |||||
| items: SearchItemWithQty[]; | items: SearchItemWithQty[]; | ||||
| selectedItemIds: (string | number)[]; | selectedItemIds: (string | number)[]; | ||||
| groups: Group[]; | groups: Group[]; | ||||
| onItemSelect: (itemId: number, checked: boolean) => void; | |||||
| onQtyChange: (itemId: number, qty: number | null) => void; | |||||
| onQtyBlur: (itemId: number) => void; | |||||
| onGroupChange: (itemId: number, groupId: string) => void; | |||||
| isItemInCreated: (itemId: number) => boolean; | |||||
| onItemSelect: (rowKey: string, checked: boolean) => void; | |||||
| onQtyChange: (rowKey: string, qty: number | null) => void; | |||||
| onQtyBlur: (rowKey: string) => void; | |||||
| onGroupChange: (rowKey: string, groupId: string) => void; | |||||
| isItemInCreated: (itemId: number, uomId?: number) => boolean; | |||||
| pageNum: number; | pageNum: number; | ||||
| pageSize: number; | pageSize: number; | ||||
| onPageChange: (event: unknown, newPage: number) => void; | onPageChange: (event: unknown, newPage: number) => void; | ||||
| onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void; | onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void; | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 31 | v1.0.1 | 2026-07-22 */ | |||||
| const SearchResultsTable: React.FC<SearchResultsTableProps> = ({ | const SearchResultsTable: React.FC<SearchResultsTableProps> = ({ | ||||
| items, | items, | ||||
| selectedItemIds, | selectedItemIds, | ||||
| @@ -67,16 +71,14 @@ const SearchResultsTable: React.FC<SearchResultsTableProps> = ({ | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| // Calculate pagination | |||||
| const startIndex = (pageNum - 1) * pageSize; | const startIndex = (pageNum - 1) * pageSize; | ||||
| const endIndex = startIndex + pageSize; | const endIndex = startIndex + pageSize; | ||||
| const paginatedResults = items.slice(startIndex, endIndex); | const paginatedResults = items.slice(startIndex, endIndex); | ||||
| const handleQtyChange = useCallback((itemId: number, value: string) => { | |||||
| // Only allow numbers | |||||
| const handleQtyChange = useCallback((rowKey: string, value: string) => { | |||||
| if (value === "" || /^\d*\.?\d+$/.test(value)) { | if (value === "" || /^\d*\.?\d+$/.test(value)) { | ||||
| const numValue = value === "" ? null : Number(value); | const numValue = value === "" ? null : Number(value); | ||||
| onQtyChange(itemId, numValue); | |||||
| onQtyChange(rowKey, numValue); | |||||
| } | } | ||||
| }, [onQtyChange]); | }, [onQtyChange]); | ||||
| @@ -119,50 +121,45 @@ const SearchResultsTable: React.FC<SearchResultsTableProps> = ({ | |||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| ) : ( | ) : ( | ||||
| paginatedResults.map((item) => ( | |||||
| <TableRow key={item.id}> | |||||
| paginatedResults.map((item) => { | |||||
| const rowKey = item.rowKey || `${item.id}_${item.uomId || 0}`; | |||||
| const inCreated = isItemInCreated(item.id, item.uomId); | |||||
| return ( | |||||
| <TableRow key={rowKey}> | |||||
| <TableCell padding="checkbox"> | <TableCell padding="checkbox"> | ||||
| <Checkbox | <Checkbox | ||||
| checked={selectedItemIds.includes(item.id)} | |||||
| onChange={(e) => onItemSelect(item.id, e.target.checked)} | |||||
| disabled={isItemInCreated(item.id)} | |||||
| checked={selectedItemIds.includes(rowKey) || inCreated} | |||||
| onChange={(e) => onItemSelect(rowKey, e.target.checked)} | |||||
| disabled={inCreated} | |||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| {/* Item */} | |||||
| <TableCell> | <TableCell> | ||||
| <Box> | |||||
| <Typography variant="body2"> | |||||
| {item.label.split(' - ')[1] || item.label} | |||||
| </Typography> | |||||
| <Typography variant="caption" color="textSecondary"> | |||||
| {item.label.split(' - ')[0] || ''} | |||||
| </Typography> | |||||
| </Box> | |||||
| <Typography variant="body2"> | |||||
| {item.label.split(' - ')[1] || item.label} | |||||
| </Typography> | |||||
| <Typography variant="caption" color="textSecondary"> | |||||
| {item.label.split(' - ')[0] || ''} | |||||
| </Typography> | |||||
| </TableCell> | </TableCell> | ||||
| {/* Group */} | |||||
| <TableCell> | <TableCell> | ||||
| <FormControl size="small" sx={{ minWidth: 120 }}> | <FormControl size="small" sx={{ minWidth: 120 }}> | ||||
| <Select | |||||
| <Select | |||||
| value={item.groupId?.toString() || ""} | value={item.groupId?.toString() || ""} | ||||
| onChange={(e) => onGroupChange(item.id, e.target.value)} | |||||
| onChange={(e) => onGroupChange(rowKey, e.target.value)} | |||||
| displayEmpty | displayEmpty | ||||
| disabled={isItemInCreated(item.id)} | |||||
| > | |||||
| disabled={inCreated} | |||||
| > | |||||
| <MenuItem value=""> | <MenuItem value=""> | ||||
| <em>{t("No Group")}</em> | |||||
| <em>{t("No Group")}</em> | |||||
| </MenuItem> | </MenuItem> | ||||
| {groups.map((group) => ( | {groups.map((group) => ( | ||||
| <MenuItem key={group.id} value={group.id.toString()}> | |||||
| <MenuItem key={group.id} value={group.id.toString()}> | |||||
| {group.name} | {group.name} | ||||
| </MenuItem> | |||||
| </MenuItem> | |||||
| ))} | ))} | ||||
| </Select> | |||||
| </Select> | |||||
| </FormControl> | </FormControl> | ||||
| </TableCell> | |||||
| {/* Current Stock */} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <Typography | <Typography | ||||
| variant="body2" | variant="body2" | ||||
| @@ -172,54 +169,42 @@ const SearchResultsTable: React.FC<SearchResultsTableProps> = ({ | |||||
| {item.currentStockBalance?.toLocaleString()||0} | {item.currentStockBalance?.toLocaleString()||0} | ||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| {/* Stock Unit */} | |||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| {item.uomDesc || "-"} | |||||
| {item.uomDesc || item.uom || "-"} | |||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| {/* Order Quantity */} | |||||
| <TextField | |||||
| type="number" | |||||
| size="small" | |||||
| value={item.qty || ""} | |||||
| onChange={(e) => { | |||||
| const value = e.target.value; | |||||
| // Only allow numbers | |||||
| if (value === "" || /^\d+$/.test(value)) { | |||||
| const numValue = value === "" ? null : Number(value); | |||||
| onQtyChange(item.id, numValue); | |||||
| } | |||||
| }} | |||||
| onBlur={() => { | |||||
| // Trigger auto-add check when user finishes input (clicks elsewhere) | |||||
| onQtyBlur(item.id); // ← Change this to call onQtyBlur instead! | |||||
| }} | |||||
| inputProps={{ | |||||
| <TextField | |||||
| type="number" | |||||
| size="small" | |||||
| value={item.qty || ""} | |||||
| onChange={(e) => handleQtyChange(rowKey, e.target.value)} | |||||
| onBlur={() => onQtyBlur(rowKey)} | |||||
| disabled={inCreated} | |||||
| inputProps={{ | |||||
| min: 1, | |||||
| max: item.currentStockBalance || 0, | |||||
| step: 1, | |||||
| style: { textAlign: 'center' } | style: { textAlign: 'center' } | ||||
| }} | |||||
| sx={{ | |||||
| }} | |||||
| sx={{ | |||||
| width: '80px', | width: '80px', | ||||
| '& .MuiInputBase-input': { | '& .MuiInputBase-input': { | ||||
| textAlign: 'center', | |||||
| cursor: 'text' | |||||
| textAlign: 'center', | |||||
| cursor: 'text' | |||||
| } | } | ||||
| }} | |||||
| disabled={isItemInCreated(item.id)} | |||||
| }} | |||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| {/* Target Date */} | |||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| {item.targetDate ? dayjs(item.targetDate).format(OUTPUT_DATE_FORMAT) : "-"} | {item.targetDate ? dayjs(item.targetDate).format(OUTPUT_DATE_FORMAT) : "-"} | ||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| )) | |||||
| ); | |||||
| }) | |||||
| )} | )} | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -242,4 +227,4 @@ const SearchResultsTable: React.FC<SearchResultsTableProps> = ({ | |||||
| ); | ); | ||||
| }; | }; | ||||
| export default SearchResultsTable; | |||||
| export default SearchResultsTable; | |||||
| @@ -35,6 +35,7 @@ import { | |||||
| } from "@/app/api/pickOrder/actions"; | } from "@/app/api/pickOrder/actions"; | ||||
| import { workbenchScanPick } from "@/app/api/doworkbench/actions"; | import { workbenchScanPick } from "@/app/api/doworkbench/actions"; | ||||
| import { fetchStockInLineInfo } from "@/app/api/po/actions"; | import { fetchStockInLineInfo } from "@/app/api/po/actions"; | ||||
| import { fetchLotDetail } from "@/app/api/inventory/actions"; | |||||
| import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; | import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; | ||||
| import TestQrCodeProvider from "../QrCodeScannerProvider/TestQrCodeProvider"; | import TestQrCodeProvider from "../QrCodeScannerProvider/TestQrCodeProvider"; | ||||
| import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | ||||
| @@ -66,6 +67,7 @@ type LineRow = { | |||||
| requiredQty: number; | requiredQty: number; | ||||
| pickedQty: number; | pickedQty: number; | ||||
| stockUnit: string; | stockUnit: string; | ||||
| uomId?: number; | |||||
| status: string; | status: string; | ||||
| lotsRaw: unknown[]; | lotsRaw: unknown[]; | ||||
| }; | }; | ||||
| @@ -89,6 +91,7 @@ type LotRow = { | |||||
| itemCode: string; | itemCode: string; | ||||
| itemName: string; | itemName: string; | ||||
| uomDesc: string; | uomDesc: string; | ||||
| uomId?: number; | |||||
| requiredQty: number; | requiredQty: number; | ||||
| pickOrderLineRequiredQty?: number; | pickOrderLineRequiredQty?: number; | ||||
| availableQty: number; | availableQty: number; | ||||
| @@ -273,6 +276,7 @@ function flattenLotsFromPickOrder(po: PickOrderTopRow): LotRow[] { | |||||
| itemCode: line.itemCode, | itemCode: line.itemCode, | ||||
| itemName: line.itemName, | itemName: line.itemName, | ||||
| uomDesc: toStr(lot.stockUnit) || line.stockUnit, | uomDesc: toStr(lot.stockUnit) || line.stockUnit, | ||||
| uomId: line.uomId, | |||||
| requiredQty: toNum(lot.requiredQty, line.requiredQty), | requiredQty: toNum(lot.requiredQty, line.requiredQty), | ||||
| pickOrderLineRequiredQty: line.requiredQty, | pickOrderLineRequiredQty: line.requiredQty, | ||||
| availableQty: toNum(lot.availableQty), | availableQty: toNum(lot.availableQty), | ||||
| @@ -333,6 +337,7 @@ function mapHierarchicalToPickOrders(data: unknown): PickOrderTopRow[] { | |||||
| requiredQty: toNum(line?.requiredQty), | requiredQty: toNum(line?.requiredQty), | ||||
| pickedQty: sumLinePickedQtyFromLots(lots), | pickedQty: sumLinePickedQtyFromLots(lots), | ||||
| stockUnit: toStr(item?.uomDesc ?? item?.uomCode), | stockUnit: toStr(item?.uomDesc ?? item?.uomCode), | ||||
| uomId: toNum(item?.uomId) > 0 ? toNum(item?.uomId) : undefined, | |||||
| status: toStr(line?.status), | status: toStr(line?.status), | ||||
| lotsRaw: lots, | lotsRaw: lots, | ||||
| }; | }; | ||||
| @@ -351,6 +356,7 @@ function mapHierarchicalToPickOrders(data: unknown): PickOrderTopRow[] { | |||||
| }); | }); | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 28 | v1.0.1 | 2026-07-22 */ | |||||
| const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | ||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| @@ -770,6 +776,16 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| processedQrCodesRef.current.add(latestQr); | processedQrCodesRef.current.add(latestQr); | ||||
| }, []); | }, []); | ||||
| /** Soft-fail / switch-PO: allow the same QR to be processed again. */ | |||||
| const releaseProcessedQr = useCallback((qr: string) => { | |||||
| const key = String(qr || "").trim(); | |||||
| if (!key) return; | |||||
| processedQrCodesRef.current.delete(key); | |||||
| if (lastProcessedQrRef.current === key) { | |||||
| lastProcessedQrRef.current = ""; | |||||
| } | |||||
| }, []); | |||||
| const openUnpickableScanLotLabelModal = useCallback( | const openUnpickableScanLotLabelModal = useCallback( | ||||
| ( | ( | ||||
| pickRow: LotRow, | pickRow: LotRow, | ||||
| @@ -1177,16 +1193,102 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| return; | return; | ||||
| } | } | ||||
| const expectedPool = activeSuggestedLots.length > 0 ? activeSuggestedLots : allLotsForItem; | |||||
| const expectedRow = pickExpectedRowForSubstitution(expectedPool) || allLotsForItem[0]; | |||||
| // Case A: reject only if scanned lot UOM matches none of this item's POL UOMs | |||||
| // (same item may have multiple lines with different UOMs on one pick order) | |||||
| let scannedUomId = 0; | |||||
| const allowedUomIds = new Set( | |||||
| allLotsForItem | |||||
| .map((l) => Number(l?.uomId)) | |||||
| .filter((id) => Number.isFinite(id) && id > 0), | |||||
| ); | |||||
| const hasLineUoms = allowedUomIds.size > 0; | |||||
| try { | |||||
| const lotDetail = await fetchLotDetail(scannedStockInLineId); | |||||
| scannedUomId = Number(lotDetail?.uomId) || 0; | |||||
| } catch (e) { | |||||
| if (hasLineUoms) { | |||||
| console.warn( | |||||
| "[QR PROCESS] lot-detail UOM check failed; rejecting scan", | |||||
| e, | |||||
| ); | |||||
| const msg = t( | |||||
| "This lot UOM does not match the pick line. Please scan another lot.", | |||||
| ); | |||||
| setError(msg); | |||||
| startTransition(() => { | |||||
| setQrScanError(true); | |||||
| setQrScanSuccess(false); | |||||
| setQrScanErrorMsg(msg); | |||||
| }); | |||||
| releaseProcessedQr(latest); | |||||
| return; | |||||
| } | |||||
| } | |||||
| if ( | |||||
| hasLineUoms && | |||||
| (!(scannedUomId > 0) || !allowedUomIds.has(scannedUomId)) | |||||
| ) { | |||||
| const msg = t( | |||||
| "This lot UOM does not match the pick line. Please scan another lot.", | |||||
| ); | |||||
| setError(msg); | |||||
| startTransition(() => { | |||||
| setQrScanError(true); | |||||
| setQrScanSuccess(false); | |||||
| setQrScanErrorMsg(msg); | |||||
| }); | |||||
| releaseProcessedQr(latest); | |||||
| return; | |||||
| } | |||||
| // Strict UOM filter — never fall back to other UOM lines | |||||
| const filterByScannedUom = (rows: LotRow[]) => { | |||||
| if (!(scannedUomId > 0) || !hasLineUoms) return rows; | |||||
| return rows.filter((r) => Number(r.uomId) === scannedUomId); | |||||
| }; | |||||
| const activeForUom = filterByScannedUom(activeSuggestedLots); | |||||
| const allForUom = filterByScannedUom(allLotsForItem); | |||||
| if (hasLineUoms && scannedUomId > 0 && allForUom.length === 0) { | |||||
| const msg = t( | |||||
| "This lot UOM does not match the pick line. Please scan another lot.", | |||||
| ); | |||||
| setError(msg); | |||||
| startTransition(() => { | |||||
| setQrScanError(true); | |||||
| setQrScanSuccess(false); | |||||
| setQrScanErrorMsg(msg); | |||||
| }); | |||||
| releaseProcessedQr(latest); | |||||
| return; | |||||
| } | |||||
| const expectedPool = activeForUom.length > 0 ? activeForUom : allForUom; | |||||
| let expectedRow = pickExpectedRowForSubstitution(expectedPool) || allForUom[0]; | |||||
| if (!expectedRow) { | |||||
| setError(t("Scanned item is not found in current line")); | |||||
| startTransition(() => { | |||||
| setQrScanError(true); | |||||
| setQrScanSuccess(false); | |||||
| setQrScanErrorMsg(t("Scanned item is not found in current line")); | |||||
| }); | |||||
| releaseProcessedQr(latest); | |||||
| return; | |||||
| } | |||||
| const scannedRows = lotRowIndexes.byStockInLineId.get(scannedStockInLineId) || []; | const scannedRows = lotRowIndexes.byStockInLineId.get(scannedStockInLineId) || []; | ||||
| const scannedRowInItem = | |||||
| let scannedRowInItem = | |||||
| scannedRows.find( | scannedRows.find( | ||||
| (r) => | (r) => | ||||
| Number(r.itemId) === scannedItemId && | Number(r.itemId) === scannedItemId && | ||||
| r.stockOutLineId > 0, | |||||
| r.stockOutLineId > 0 && | |||||
| (!(scannedUomId > 0) || !hasLineUoms || Number(r.uomId) === scannedUomId), | |||||
| ) || | ) || | ||||
| (!(scannedUomId > 0) || !hasLineUoms | |||||
| ? scannedRows.find( | |||||
| (r) => | |||||
| Number(r.itemId) === scannedItemId && | |||||
| r.stockOutLineId > 0, | |||||
| ) | |||||
| : undefined) || | |||||
| null; | null; | ||||
| if (scannedRowInItem && isRejectedStatus(scannedRowInItem.status)) { | if (scannedRowInItem && isRejectedStatus(scannedRowInItem.status)) { | ||||
| @@ -1231,14 +1333,46 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| return; | return; | ||||
| } | } | ||||
| if (scannedRowInItem && (isCompletedStatus(scannedRowInItem.status) || isCheckedStatus(scannedRowInItem.status))) { | |||||
| setError(t("Scanned lot is already completed or checked")); | |||||
| startTransition(() => { | |||||
| setQrScanError(true); | |||||
| setQrScanSuccess(false); | |||||
| setQrScanErrorMsg(t("Scanned lot is already completed or checked")); | |||||
| // Same lot already bound to a completed/checked SOL: only reuse for another | |||||
| // pending line with the SAME uomId (never cross UOM A ↔ B). | |||||
| if ( | |||||
| scannedRowInItem && | |||||
| (isCompletedStatus(scannedRowInItem.status) || isCheckedStatus(scannedRowInItem.status)) | |||||
| ) { | |||||
| const completedPolId = Number(scannedRowInItem.pickOrderLineId); | |||||
| const completedSolId = Number(scannedRowInItem.stockOutLineId); | |||||
| const pendingPoolBase = activeForUom.length > 0 ? activeForUom : allForUom; | |||||
| const pendingSameUom = pendingPoolBase.filter((r) => { | |||||
| if (!(r.stockOutLineId > 0)) return false; | |||||
| if (isCompletedStatus(r.status) || isCheckedStatus(r.status) || isRejectedStatus(r.status)) { | |||||
| return false; | |||||
| } | |||||
| if (Number(r.stockOutLineId) === completedSolId) return false; | |||||
| if (completedPolId > 0 && Number(r.pickOrderLineId) === completedPolId) return false; | |||||
| // Require explicit same UOM — do not reuse across different UOM lines | |||||
| if (scannedUomId > 0 && hasLineUoms) { | |||||
| return Number(r.uomId) === scannedUomId; | |||||
| } | |||||
| // Without line UOM metadata, do not auto-reuse (avoids A/B cross-pick) | |||||
| return false; | |||||
| }); | }); | ||||
| return; | |||||
| const nextExpected = | |||||
| pickExpectedRowForSubstitution(pendingSameUom) || pendingSameUom[0] || null; | |||||
| if (nextExpected) { | |||||
| expectedRow = nextExpected; | |||||
| // Force substitution path using scanned SIL, not the completed SOL row. | |||||
| scannedRowInItem = null; | |||||
| } else { | |||||
| const msg = t("This lot has already been picked. Please scan another lot."); | |||||
| setError(msg); | |||||
| startTransition(() => { | |||||
| setQrScanError(true); | |||||
| setQrScanSuccess(false); | |||||
| setQrScanErrorMsg(msg); | |||||
| }); | |||||
| releaseProcessedQr(latest); | |||||
| return; | |||||
| } | |||||
| } | } | ||||
| let scannedState: ConfirmLotState | null = null; | let scannedState: ConfirmLotState | null = null; | ||||
| @@ -1292,6 +1426,7 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| pickExpectedRowForSubstitution, | pickExpectedRowForSubstitution, | ||||
| lotRowIndexes, | lotRowIndexes, | ||||
| openUnpickableScanLotLabelModal, | openUnpickableScanLotLabelModal, | ||||
| releaseProcessedQr, | |||||
| resetScan, | resetScan, | ||||
| submitRow, | submitRow, | ||||
| t, | t, | ||||
| @@ -1306,10 +1441,9 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| }, [isScanning, startScan, userId]); | }, [isScanning, startScan, userId]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (!selectedPickOrderId) { | |||||
| lastProcessedQrRef.current = ""; | |||||
| processedQrCodesRef.current.clear(); | |||||
| } | |||||
| // Clear on any PO selection change (including PO1 → PO2), so the same lot QR can be scanned again. | |||||
| lastProcessedQrRef.current = ""; | |||||
| processedQrCodesRef.current.clear(); | |||||
| }, [selectedPickOrderId]); | }, [selectedPickOrderId]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -1779,6 +1913,12 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| statusTitleSeverity={workbenchLotLabelStatusBanner.severity} | statusTitleSeverity={workbenchLotLabelStatusBanner.severity} | ||||
| triggerLotAvailableQty={workbenchLotLabelContextLot?.availableQty ?? null} | triggerLotAvailableQty={workbenchLotLabelContextLot?.availableQty ?? null} | ||||
| triggerLotUom={workbenchLotLabelContextLot?.uomDesc ?? null} | triggerLotUom={workbenchLotLabelContextLot?.uomDesc ?? null} | ||||
| expectedUomId={ | |||||
| workbenchLotLabelContextLot != null && | |||||
| Number(workbenchLotLabelContextLot.uomId) > 0 | |||||
| ? Number(workbenchLotLabelContextLot.uomId) | |||||
| : null | |||||
| } | |||||
| submitQty={ | submitQty={ | ||||
| workbenchLotLabelContextLot?.stockOutLineId | workbenchLotLabelContextLot?.stockOutLineId | ||||
| ? Number(resolveLockedSubmitQtyDisplay(workbenchLotLabelContextLot)) | ? Number(resolveLockedSubmitQtyDisplay(workbenchLotLabelContextLot)) | ||||
| @@ -70,6 +70,8 @@ interface CreatedItem { | |||||
| // Add interface for search items with quantity | // Add interface for search items with quantity | ||||
| interface SearchItemWithQty extends ItemCombo { | interface SearchItemWithQty extends ItemCombo { | ||||
| /** Unique per item+uom search row */ | |||||
| rowKey: string; | |||||
| qty: number | null; // Changed from number to number | null | qty: number | null; // Changed from number to number | null | ||||
| jobOrderCode?: string; | jobOrderCode?: string; | ||||
| jobOrderId?: number; | jobOrderId?: number; | ||||
| @@ -77,6 +79,10 @@ interface SearchItemWithQty extends ItemCombo { | |||||
| targetDate?: string | null; // Allow null values | targetDate?: string | null; // Allow null values | ||||
| groupId?: number | null; // Allow null values | groupId?: number | null; // Allow null values | ||||
| } | } | ||||
| const toSearchRowKey = (itemId: number, uomId?: number | null) => | |||||
| `${itemId}_${Number(uomId) > 0 ? Number(uomId) : 0}`; | |||||
| interface JobOrderDetailPickLine { | interface JobOrderDetailPickLine { | ||||
| id: number; | id: number; | ||||
| code: string; | code: string; | ||||
| @@ -95,8 +101,9 @@ interface Group { | |||||
| } | } | ||||
| // Move the counter outside the component to persist across re-renders | // Move the counter outside the component to persist across re-renders | ||||
| let checkboxChangeCallCount = 0; | let checkboxChangeCallCount = 0; | ||||
| let processingItems = new Set<number>(); | |||||
| let processingItems = new Set<string>(); | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 31 | v1.0.1 | 2026-07-22 */ | |||||
| const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCreated }) => { | const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCreated }) => { | ||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| const [items, setItems] = useState<ItemCombo[]>([]); | const [items, setItems] = useState<ItemCombo[]>([]); | ||||
| @@ -174,6 +181,7 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| uom: item.uom, | uom: item.uom, | ||||
| uomId: 0, | uomId: 0, | ||||
| uomDesc: item.uomDesc || "", // Add missing uomDesc | uomDesc: item.uomDesc || "", // Add missing uomDesc | ||||
| rowKey: toSearchRowKey(item.id, 0), | |||||
| jobOrderCode: jobOrderDetail.code, | jobOrderCode: jobOrderDetail.code, | ||||
| jobOrderId: jobOrderDetail.id, | jobOrderId: jobOrderDetail.id, | ||||
| currentStockBalance: 0, // Add default value | currentStockBalance: 0, // Add default value | ||||
| @@ -263,49 +271,50 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| ); | ); | ||||
| }, []); | }, []); | ||||
| // Modified handler for search item selection | |||||
| // Modified handler for search item selection (legacy filteredItems path) | |||||
| const handleSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => { | const handleSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => { | ||||
| if (isSelected) { | if (isSelected) { | ||||
| const item = filteredItems.find(i => i.id === itemId); | const item = filteredItems.find(i => i.id === itemId); | ||||
| if (!item) return; | if (!item) return; | ||||
| const existingItem = createdItems.find(created => created.itemId === item.id); | |||||
| if (existingItem) { | |||||
| const uomId = Number(item.uomId) || 0; | |||||
| if (createdItems.some((c) => c.itemId === item.id && Number(c.uomId) === uomId)) { | |||||
| alert(t("Item already exists in created items")); | alert(t("Item already exists in created items")); | ||||
| return; | return; | ||||
| } | } | ||||
| // Fix the newCreatedItem creation - add missing uomDesc | |||||
| const newCreatedItem: CreatedItem = { | const newCreatedItem: CreatedItem = { | ||||
| itemId: item.id, | itemId: item.id, | ||||
| itemName: item.label, | itemName: item.label, | ||||
| itemCode: item.label, | itemCode: item.label, | ||||
| qty: item.qty || 1, | qty: item.qty || 1, | ||||
| uom: item.uom || "", | uom: item.uom || "", | ||||
| uomId: item.uomId || 0, | |||||
| uomDesc: item.uomDesc || "", // Add missing uomDesc | |||||
| uomId, | |||||
| uomDesc: item.uomDesc || "", | |||||
| isSelected: true, | isSelected: true, | ||||
| currentStockBalance: item.currentStockBalance, | currentStockBalance: item.currentStockBalance, | ||||
| targetDate: item.targetDate || targetDate, // Use item's targetDate or fallback to form's targetDate | |||||
| groupId: item.groupId || undefined, // Handle null values | |||||
| targetDate: item.targetDate || targetDate, | |||||
| groupId: item.groupId || undefined, | |||||
| }; | }; | ||||
| setCreatedItems(prev => [...prev, newCreatedItem]); | setCreatedItems(prev => [...prev, newCreatedItem]); | ||||
| } | } | ||||
| }, [filteredItems, createdItems, t, targetDate]); | }, [filteredItems, createdItems, t, targetDate]); | ||||
| // Handler for created item selection | // Handler for created item selection | ||||
| const handleCreatedItemSelect = useCallback((itemId: number, isSelected: boolean) => { | |||||
| const handleCreatedItemSelect = useCallback((itemId: number, isSelected: boolean, uomId?: number) => { | |||||
| setCreatedItems(prev => | setCreatedItems(prev => | ||||
| prev.map(item => | prev.map(item => | ||||
| item.itemId === itemId ? { ...item, isSelected } : item | |||||
| item.itemId === itemId && | |||||
| (uomId == null || Number(item.uomId) === Number(uomId)) | |||||
| ? { ...item, isSelected } | |||||
| : item | |||||
| ) | ) | ||||
| ); | ); | ||||
| }, []); | }, []); | ||||
| const handleQtyChange = useCallback((itemId: number, newQty: number) => { | |||||
| const handleQtyChange = useCallback((itemId: number, newQty: number, uomId?: number) => { | |||||
| setCreatedItems(prev => | setCreatedItems(prev => | ||||
| prev.map(item => | prev.map(item => | ||||
| item.itemId === itemId | |||||
| item.itemId === itemId && | |||||
| (uomId == null || Number(item.uomId) === Number(uomId)) | |||||
| ? { | ? { | ||||
| ...item, | ...item, | ||||
| qty: Math.max(1, Math.min(newQty, Math.max(0, item.currentStockBalance ?? 0))), | qty: Math.max(1, Math.min(newQty, Math.max(0, item.currentStockBalance ?? 0))), | ||||
| @@ -315,18 +324,23 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| ); | ); | ||||
| }, []); | }, []); | ||||
| // Check if item is already in created items | |||||
| const isItemInCreated = useCallback((itemId: number) => { | |||||
| return createdItems.some(item => item.itemId === itemId); | |||||
| // Check if item (+ optional uom) is already in created items | |||||
| const isItemInCreated = useCallback((itemId: number, uomId?: number) => { | |||||
| return createdItems.some( | |||||
| (item) => | |||||
| item.itemId === itemId && | |||||
| (uomId == null || Number(item.uomId) === Number(uomId)), | |||||
| ); | |||||
| }, [createdItems]); | }, [createdItems]); | ||||
| // 1) Created Items 行内改组:只改这一行的 groupId,并把该行 targetDate 同步为该组日期 | // 1) Created Items 行内改组:只改这一行的 groupId,并把该行 targetDate 同步为该组日期 | ||||
| const handleCreatedItemGroupChange = useCallback((itemId: number, newGroupId: string) => { | |||||
| const handleCreatedItemGroupChange = useCallback((itemId: number, newGroupId: string, uomId?: number) => { | |||||
| const gid = newGroupId ? Number(newGroupId) : undefined; | const gid = newGroupId ? Number(newGroupId) : undefined; | ||||
| const group = groups.find(g => g.id === gid); | const group = groups.find(g => g.id === gid); | ||||
| setCreatedItems(prev => | setCreatedItems(prev => | ||||
| prev.map(it => | prev.map(it => | ||||
| it.itemId === itemId | |||||
| it.itemId === itemId && | |||||
| (uomId == null || Number(it.uomId) === Number(uomId)) | |||||
| ? { | ? { | ||||
| ...it, | ...it, | ||||
| groupId: gid, | groupId: gid, | ||||
| @@ -410,27 +424,23 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| } | } | ||||
| }, [t]); | }, [t]); | ||||
| const checkAndAutoAddItem = useCallback((itemId: number) => { | |||||
| const item = secondSearchResults.find(i => i.id === itemId); | |||||
| const checkAndAutoAddItem = useCallback((rowKey: string) => { | |||||
| const item = secondSearchResults.find((i) => (i.rowKey || toSearchRowKey(i.id, i.uomId)) === rowKey); | |||||
| if (!item) return; | if (!item) return; | ||||
| // Check if item has ALL 3 conditions: | |||||
| // 1. Item is selected (checkbox checked) | |||||
| const isSelected = selectedSecondSearchItemIds.includes(itemId); | |||||
| // 2. Group is assigned | |||||
| const key = item.rowKey || toSearchRowKey(item.id, item.uomId); | |||||
| const isSelected = selectedSecondSearchItemIds.includes(key); | |||||
| const hasGroup = item.groupId !== undefined && item.groupId !== null; | const hasGroup = item.groupId !== undefined && item.groupId !== null; | ||||
| // 3. Quantity is entered | |||||
| const hasQty = item.qty !== null && item.qty !== undefined && item.qty > 0; | const hasQty = item.qty !== null && item.qty !== undefined && item.qty > 0; | ||||
| if (isSelected && hasGroup && hasQty && !isItemInCreated(item.id)) { | |||||
| // Auto-add to created items | |||||
| if (isSelected && hasGroup && hasQty && !isItemInCreated(item.id, item.uomId)) { | |||||
| const newCreatedItem: CreatedItem = { | const newCreatedItem: CreatedItem = { | ||||
| itemId: item.id, | itemId: item.id, | ||||
| itemName: item.label, | itemName: item.label, | ||||
| itemCode: item.label, | itemCode: item.label, | ||||
| qty: item.qty || 1, | qty: item.qty || 1, | ||||
| uom: item.uom || "", | uom: item.uom || "", | ||||
| uomId: item.uomId || 0, | |||||
| uomId: Number(item.uomId) || 0, | |||||
| uomDesc: item.uomDesc || "", | uomDesc: item.uomDesc || "", | ||||
| isSelected: true, | isSelected: true, | ||||
| currentStockBalance: item.currentStockBalance, | currentStockBalance: item.currentStockBalance, | ||||
| @@ -438,33 +448,29 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| groupId: item.groupId || undefined, | groupId: item.groupId || undefined, | ||||
| }; | }; | ||||
| setCreatedItems(prev => [...prev, newCreatedItem]); | setCreatedItems(prev => [...prev, newCreatedItem]); | ||||
| // Remove from search results since it's now in created items | |||||
| setSecondSearchResults(prev => prev.filter(searchItem => searchItem.id !== itemId)); | |||||
| setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId)); | |||||
| setSecondSearchResults(prev => prev.filter((searchItem) => (searchItem.rowKey || toSearchRowKey(searchItem.id, searchItem.uomId)) !== key)); | |||||
| setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== key)); | |||||
| } | } | ||||
| }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); | }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); | ||||
| // Add this function after checkAndAutoAddItem | |||||
| // Add this function after checkAndAutoAddItem | |||||
| const handleQtyBlur = useCallback((itemId: number) => { | |||||
| // Only auto-add if item is already selected (scenario 1: select first, then enter quantity) | |||||
| const handleQtyBlur = useCallback((rowKey: string) => { | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| const currentItem = secondSearchResults.find(i => i.id === itemId); | |||||
| const currentItem = secondSearchResults.find((i) => (i.rowKey || toSearchRowKey(i.id, i.uomId)) === rowKey); | |||||
| if (!currentItem) return; | if (!currentItem) return; | ||||
| const isSelected = selectedSecondSearchItemIds.includes(itemId); | |||||
| const key = currentItem.rowKey || toSearchRowKey(currentItem.id, currentItem.uomId); | |||||
| const isSelected = selectedSecondSearchItemIds.includes(key); | |||||
| const hasGroup = currentItem.groupId !== undefined && currentItem.groupId !== null; | const hasGroup = currentItem.groupId !== undefined && currentItem.groupId !== null; | ||||
| const hasQty = currentItem.qty !== null && currentItem.qty !== undefined && currentItem.qty > 0; | const hasQty = currentItem.qty !== null && currentItem.qty !== undefined && currentItem.qty > 0; | ||||
| // Only auto-add if item is already selected (scenario 1: select first, then enter quantity) | |||||
| if (isSelected && hasGroup && hasQty && !isItemInCreated(currentItem.id)) { | |||||
| if (isSelected && hasGroup && hasQty && !isItemInCreated(currentItem.id, currentItem.uomId)) { | |||||
| const newCreatedItem: CreatedItem = { | const newCreatedItem: CreatedItem = { | ||||
| itemId: currentItem.id, | itemId: currentItem.id, | ||||
| itemName: currentItem.label, | itemName: currentItem.label, | ||||
| itemCode: currentItem.label, | itemCode: currentItem.label, | ||||
| qty: currentItem.qty || 1, | qty: currentItem.qty || 1, | ||||
| uom: currentItem.uom || "", | uom: currentItem.uom || "", | ||||
| uomId: currentItem.uomId || 0, | |||||
| uomId: Number(currentItem.uomId) || 0, | |||||
| uomDesc: currentItem.uomDesc || "", | uomDesc: currentItem.uomDesc || "", | ||||
| isSelected: true, | isSelected: true, | ||||
| currentStockBalance: currentItem.currentStockBalance, | currentStockBalance: currentItem.currentStockBalance, | ||||
| @@ -472,18 +478,18 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||||
| groupId: currentItem.groupId || undefined, | groupId: currentItem.groupId || undefined, | ||||
| }; | }; | ||||
| setCreatedItems(prev => [...prev, newCreatedItem]); | setCreatedItems(prev => [...prev, newCreatedItem]); | ||||
| setSecondSearchResults(prev => prev.filter(searchItem => searchItem.id !== itemId)); | |||||
| setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId)); | |||||
| setSecondSearchResults(prev => prev.filter((searchItem) => (searchItem.rowKey || toSearchRowKey(searchItem.id, searchItem.uomId)) !== key)); | |||||
| setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== key)); | |||||
| } | } | ||||
| }, 0); | }, 0); | ||||
| }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); | }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); | ||||
| const handleSearchItemGroupChange = useCallback((itemId: number, groupId: string) => { | |||||
| const handleSearchItemGroupChange = useCallback((rowKey: string, groupId: string) => { | |||||
| const gid = groupId ? Number(groupId) : undefined; | const gid = groupId ? Number(groupId) : undefined; | ||||
| const group = groups.find(g => g.id === gid); | const group = groups.find(g => g.id === gid); | ||||
| setSecondSearchResults(prev => prev.map(item => | setSecondSearchResults(prev => prev.map(item => | ||||
| item.id === itemId | |||||
| (item.rowKey || toSearchRowKey(item.id, item.uomId)) === rowKey | |||||
| ? { | ? { | ||||
| ...item, | ...item, | ||||
| groupId: gid, | groupId: gid, | ||||
| @@ -492,9 +498,8 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||||
| : item | : item | ||||
| )); | )); | ||||
| // Check auto-add after group assignment | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| checkAndAutoAddItem(itemId); | |||||
| checkAndAutoAddItem(rowKey); | |||||
| }, 0); | }, 0); | ||||
| }, [groups, checkAndAutoAddItem]); | }, [groups, checkAndAutoAddItem]); | ||||
| // 5) 选中新增的待选项:依然按“当前 Group”赋 groupId + targetDate(新加入的应随 Group) | // 5) 选中新增的待选项:依然按“当前 Group”赋 groupId + targetDate(新加入的应随 Group) | ||||
| @@ -550,6 +555,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||||
| uomId: item.uomId, | uomId: item.uomId, | ||||
| uom: item.uom, | uom: item.uom, | ||||
| uomDesc: item.uomDesc, | uomDesc: item.uomDesc, | ||||
| rowKey: toSearchRowKey(item.id, item.uomId), | |||||
| currentStockBalance: item.currentStockBalance, | currentStockBalance: item.currentStockBalance, | ||||
| qty: null, | qty: null, | ||||
| targetDate: targetDate, | targetDate: targetDate, | ||||
| @@ -1017,7 +1023,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||||
| name: "id", | name: "id", | ||||
| label: "", | label: "", | ||||
| type: "checkbox", | type: "checkbox", | ||||
| disabled: (item) => isItemInCreated(item.id), // Disable if already in created items | |||||
| disabled: (item) => isItemInCreated(item.id, item.uomId), // Disable if already in created items | |||||
| }, | }, | ||||
| { | { | ||||
| @@ -1141,7 +1147,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||||
| }, [formProps]); | }, [formProps]); | ||||
| // 添加数量变更处理函数 | // 添加数量变更处理函数 | ||||
| const handleSecondSearchQtyChange = useCallback((itemId: number, newQty: number | null) => { | |||||
| const handleSecondSearchQtyChange = useCallback((rowKey: string, newQty: number | null) => { | |||||
| const getClampedQty = (qty: number | null, stock?: number) => { | const getClampedQty = (qty: number | null, stock?: number) => { | ||||
| if (qty === null) return null; | if (qty === null) return null; | ||||
| const maxQty = Math.max(0, stock ?? 0); | const maxQty = Math.max(0, stock ?? 0); | ||||
| @@ -1150,7 +1156,9 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||||
| setSecondSearchResults(prev => | setSecondSearchResults(prev => | ||||
| prev.map(item => | prev.map(item => | ||||
| item.id === itemId ? { ...item, qty: getClampedQty(newQty, item.currentStockBalance) } : item | |||||
| (item.rowKey || toSearchRowKey(item.id, item.uomId)) === rowKey | |||||
| ? { ...item, qty: getClampedQty(newQty, item.currentStockBalance) } | |||||
| : item | |||||
| ) | ) | ||||
| ); | ); | ||||
| @@ -1163,24 +1171,24 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||||
| const newIds = ids(selectedSecondSearchItemIds); | const newIds = ids(selectedSecondSearchItemIds); | ||||
| setSelectedSecondSearchItemIds(newIds); | setSelectedSecondSearchItemIds(newIds); | ||||
| // 处理全选逻辑 - 选择所有搜索结果,不仅仅是当前页面 | |||||
| if (newIds.length === secondSearchResults.length) { | if (newIds.length === secondSearchResults.length) { | ||||
| // 全选:将所有搜索结果添加到创建项目 | |||||
| secondSearchResults.forEach(item => { | secondSearchResults.forEach(item => { | ||||
| if (!isItemInCreated(item.id)) { | |||||
| if (!isItemInCreated(item.id, item.uomId)) { | |||||
| handleSearchItemSelect(item.id, true); | handleSearchItemSelect(item.id, true); | ||||
| } | } | ||||
| }); | }); | ||||
| } else { | } else { | ||||
| // 部分选择:只处理当前页面的选择 | |||||
| secondSearchResults.forEach(item => { | secondSearchResults.forEach(item => { | ||||
| const isSelected = newIds.includes(item.id); | |||||
| const isCurrentlyInCreated = isItemInCreated(item.id); | |||||
| const key = item.rowKey || toSearchRowKey(item.id, item.uomId); | |||||
| const isSelected = newIds.includes(key); | |||||
| const isCurrentlyInCreated = isItemInCreated(item.id, item.uomId); | |||||
| if (isSelected && !isCurrentlyInCreated) { | if (isSelected && !isCurrentlyInCreated) { | ||||
| handleSearchItemSelect(item.id, true); | handleSearchItemSelect(item.id, true); | ||||
| } else if (!isSelected && isCurrentlyInCreated) { | } else if (!isSelected && isCurrentlyInCreated) { | ||||
| setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id)); | |||||
| setCreatedItems(prev => prev.filter(createdItem => | |||||
| !(createdItem.itemId === item.id && Number(createdItem.uomId) === Number(item.uomId)) | |||||
| )); | |||||
| } | } | ||||
| }); | }); | ||||
| } | } | ||||
| @@ -1192,13 +1200,19 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||||
| const newlyDeselected = previousIds.filter(id => !ids.includes(id)); | const newlyDeselected = previousIds.filter(id => !ids.includes(id)); | ||||
| newlySelected.forEach(id => { | newlySelected.forEach(id => { | ||||
| if (!isItemInCreated(id as number)) { | |||||
| handleSearchItemSelect(id as number, true); | |||||
| const row = secondSearchResults.find(item => (item.rowKey || toSearchRowKey(item.id, item.uomId)) === String(id)); | |||||
| if (row && !isItemInCreated(row.id, row.uomId)) { | |||||
| handleSearchItemSelect(row.id, true); | |||||
| } | } | ||||
| }); | }); | ||||
| newlyDeselected.forEach(id => { | newlyDeselected.forEach(id => { | ||||
| setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id)); | |||||
| const row = secondSearchResults.find(item => (item.rowKey || toSearchRowKey(item.id, item.uomId)) === String(id)); | |||||
| if (row) { | |||||
| setCreatedItems(prev => prev.filter(createdItem => | |||||
| !(createdItem.itemId === row.id && Number(createdItem.uomId) === Number(row.uomId)) | |||||
| )); | |||||
| } | |||||
| }); | }); | ||||
| } | } | ||||
| }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSearchItemSelect]); | }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSearchItemSelect]); | ||||
| @@ -1209,7 +1223,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||||
| name: "id", | name: "id", | ||||
| label: "", | label: "", | ||||
| type: "checkbox", | type: "checkbox", | ||||
| disabled: (item) => isItemInCreated(item.id), | |||||
| disabled: (item) => isItemInCreated(item.id, item.uomId), | |||||
| }, | }, | ||||
| { | { | ||||
| name: "label", | name: "label", | ||||
| @@ -1260,7 +1274,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||||
| renderCell: (item) => ( | renderCell: (item) => ( | ||||
| <Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%' }}> | <Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%' }}> | ||||
| <Typography sx={{ textAlign: 'right' }}> {/* Add right alignment for the value */} | <Typography sx={{ textAlign: 'right' }}> {/* Add right alignment for the value */} | ||||
| {item.uom || "-"} | |||||
| {item.uomDesc || item.uom || "-"} | |||||
| </Typography> | </Typography> | ||||
| </Box> | </Box> | ||||
| ), | ), | ||||
| @@ -1280,7 +1294,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||||
| // Only allow numbers | // Only allow numbers | ||||
| if (value === "" || /^\d+$/.test(value)) { | if (value === "" || /^\d+$/.test(value)) { | ||||
| const numValue = value === "" ? null : Number(value); | const numValue = value === "" ? null : Number(value); | ||||
| handleSecondSearchQtyChange(item.id, numValue); | |||||
| handleSecondSearchQtyChange(item.rowKey || toSearchRowKey(item.id, item.uomId), numValue); | |||||
| } | } | ||||
| }} | }} | ||||
| inputProps={{ | inputProps={{ | ||||
| @@ -1299,7 +1313,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||||
| const value = e.target.value; | const value = e.target.value; | ||||
| const numValue = value === "" ? null : Number(value); | const numValue = value === "" ? null : Number(value); | ||||
| if (numValue !== null && numValue < 1) { | if (numValue !== null && numValue < 1) { | ||||
| handleSecondSearchQtyChange(item.id, 1); // Enforce min value | |||||
| handleSecondSearchQtyChange(item.rowKey || toSearchRowKey(item.id, item.uomId), 1); // Enforce min value | |||||
| } | } | ||||
| }} | }} | ||||
| /> | /> | ||||
| @@ -1368,6 +1382,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||||
| uomId: item.uomId, | uomId: item.uomId, | ||||
| uom: item.uom, | uom: item.uom, | ||||
| uomDesc: item.uomDesc, | uomDesc: item.uomDesc, | ||||
| rowKey: toSearchRowKey(item.id, item.uomId), | |||||
| currentStockBalance: item.currentStockBalance, | currentStockBalance: item.currentStockBalance, | ||||
| qty: null, | qty: null, | ||||
| targetDate: undefined, | targetDate: undefined, | ||||
| @@ -1493,7 +1508,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||||
| }, []); | }, []); | ||||
| const getValidationMessage = useCallback(() => { | const getValidationMessage = useCallback(() => { | ||||
| const selectedItems = secondSearchResults.filter(item => | const selectedItems = secondSearchResults.filter(item => | ||||
| selectedSecondSearchItemIds.includes(item.id) | |||||
| selectedSecondSearchItemIds.includes(item.rowKey || toSearchRowKey(item.id, item.uomId)) | |||||
| ); | ); | ||||
| const itemsWithoutGroup = selectedItems.filter(item => | const itemsWithoutGroup = selectedItems.filter(item => | ||||
| @@ -1517,42 +1532,42 @@ const getValidationMessage = useCallback(() => { | |||||
| // Fix the handleAddSelectedToCreatedItems function to properly clear selections | // Fix the handleAddSelectedToCreatedItems function to properly clear selections | ||||
| const handleAddSelectedToCreatedItems = useCallback(() => { | const handleAddSelectedToCreatedItems = useCallback(() => { | ||||
| const selectedItems = secondSearchResults.filter(item => | const selectedItems = secondSearchResults.filter(item => | ||||
| selectedSecondSearchItemIds.includes(item.id) | |||||
| selectedSecondSearchItemIds.includes(item.rowKey || toSearchRowKey(item.id, item.uomId)) | |||||
| ); | ); | ||||
| // Add selected items to created items with their own group info | |||||
| selectedItems.forEach(item => { | |||||
| if (!isItemInCreated(item.id)) { | |||||
| const newCreatedItem: CreatedItem = { | |||||
| itemId: item.id, | |||||
| itemName: item.label, | |||||
| itemCode: item.label, | |||||
| qty: item.qty || 1, | |||||
| uom: item.uom || "", | |||||
| uomId: item.uomId || 0, | |||||
| uomDesc: item.uomDesc || "", | |||||
| isSelected: true, | |||||
| currentStockBalance: item.currentStockBalance, | |||||
| targetDate: item.targetDate || targetDate, | |||||
| groupId: item.groupId || undefined, | |||||
| }; | |||||
| setCreatedItems(prev => [...prev, newCreatedItem]); | |||||
| } | |||||
| }); | |||||
| // Clear the selection | |||||
| const toAdd: CreatedItem[] = []; | |||||
| const addedKeys: string[] = []; | |||||
| for (const item of selectedItems) { | |||||
| const key = item.rowKey || toSearchRowKey(item.id, item.uomId); | |||||
| if (isItemInCreated(item.id, item.uomId)) continue; | |||||
| toAdd.push({ | |||||
| itemId: item.id, | |||||
| itemName: item.label, | |||||
| itemCode: item.label, | |||||
| qty: item.qty || 1, | |||||
| uom: item.uom || "", | |||||
| uomId: Number(item.uomId) || 0, | |||||
| uomDesc: item.uomDesc || "", | |||||
| isSelected: true, | |||||
| currentStockBalance: item.currentStockBalance, | |||||
| targetDate: item.targetDate || targetDate, | |||||
| groupId: item.groupId || undefined, | |||||
| }); | |||||
| addedKeys.push(key); | |||||
| } | |||||
| if (toAdd.length > 0) { | |||||
| setCreatedItems((prev) => [...prev, ...toAdd]); | |||||
| } | |||||
| setSelectedSecondSearchItemIds([]); | setSelectedSecondSearchItemIds([]); | ||||
| // Remove the selected/added items from search results entirely | |||||
| setSecondSearchResults(prev => prev.filter(item => | |||||
| !selectedSecondSearchItemIds.includes(item.id) | |||||
| )); | |||||
| setSecondSearchResults((prev) => | |||||
| prev.filter((item) => !addedKeys.includes(item.rowKey || toSearchRowKey(item.id, item.uomId))), | |||||
| ); | |||||
| }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); | }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); | ||||
| // Add a validation function to check if selected items are valid | // Add a validation function to check if selected items are valid | ||||
| const areSelectedItemsValid = useCallback(() => { | const areSelectedItemsValid = useCallback(() => { | ||||
| const selectedItems = secondSearchResults.filter(item => | const selectedItems = secondSearchResults.filter(item => | ||||
| selectedSecondSearchItemIds.includes(item.id) | |||||
| selectedSecondSearchItemIds.includes(item.rowKey || toSearchRowKey(item.id, item.uomId)) | |||||
| ); | ); | ||||
| return selectedItems.every(item => | return selectedItems.every(item => | ||||
| @@ -1566,17 +1581,17 @@ const getValidationMessage = useCallback(() => { | |||||
| // Move these handlers to the component level (outside of CustomSearchResultsTable) | // Move these handlers to the component level (outside of CustomSearchResultsTable) | ||||
| // Handle individual checkbox change - ONLY select, don't add to created items | // Handle individual checkbox change - ONLY select, don't add to created items | ||||
| const handleIndividualCheckboxChange = useCallback((itemId: number, checked: boolean) => { | |||||
| const handleIndividualCheckboxChange = useCallback((rowKey: string, checked: boolean) => { | |||||
| checkboxChangeCallCount++; | checkboxChangeCallCount++; | ||||
| if (checked) { | if (checked) { | ||||
| // Add to selected IDs | // Add to selected IDs | ||||
| setSelectedSecondSearchItemIds(prev => [...prev, itemId]); | |||||
| setSelectedSecondSearchItemIds(prev => [...prev, rowKey]); | |||||
| // Set the item's group and targetDate to current group when selected | // Set the item's group and targetDate to current group when selected | ||||
| setSecondSearchResults(prev => { | setSecondSearchResults(prev => { | ||||
| const updatedResults = prev.map(item => | const updatedResults = prev.map(item => | ||||
| item.id === itemId | |||||
| (item.rowKey || toSearchRowKey(item.id, item.uomId)) === rowKey | |||||
| ? { | ? { | ||||
| ...item, | ...item, | ||||
| groupId: selectedGroup?.id || undefined, | groupId: selectedGroup?.id || undefined, | ||||
| @@ -1588,29 +1603,27 @@ const getValidationMessage = useCallback(() => { | |||||
| // Check if should auto-add after state update | // Check if should auto-add after state update | ||||
| setTimeout(() => { | setTimeout(() => { | ||||
| // Check if we're already processing this item | // Check if we're already processing this item | ||||
| if (processingItems.has(itemId)) { | |||||
| //alert(`Item ${itemId} is already being processed, skipping duplicate auto-add`); | |||||
| if (processingItems.has(rowKey)) { | |||||
| return; | return; | ||||
| } | } | ||||
| const updatedItem = updatedResults.find(i => i.id === itemId); | |||||
| const updatedItem = updatedResults.find(i => (i.rowKey || toSearchRowKey(i.id, i.uomId)) === rowKey); | |||||
| if (updatedItem) { | if (updatedItem) { | ||||
| const isSelected = true; // We just selected it | const isSelected = true; // We just selected it | ||||
| const hasGroup = updatedItem.groupId !== undefined && updatedItem.groupId !== null; | const hasGroup = updatedItem.groupId !== undefined && updatedItem.groupId !== null; | ||||
| const hasQty = updatedItem.qty !== null && updatedItem.qty !== undefined && updatedItem.qty > 0; | const hasQty = updatedItem.qty !== null && updatedItem.qty !== undefined && updatedItem.qty > 0; | ||||
| // Only auto-add if item has quantity (scenario 2: enter quantity first, then select) | // Only auto-add if item has quantity (scenario 2: enter quantity first, then select) | ||||
| if (isSelected && hasGroup && hasQty && !isItemInCreated(updatedItem.id)) { | |||||
| // Mark this item as being processed | |||||
| processingItems.add(itemId); | |||||
| if (isSelected && hasGroup && hasQty && !isItemInCreated(updatedItem.id, updatedItem.uomId)) { | |||||
| processingItems.add(rowKey); | |||||
| const newCreatedItem: CreatedItem = { | const newCreatedItem: CreatedItem = { | ||||
| itemId: updatedItem.id, | itemId: updatedItem.id, | ||||
| itemName: updatedItem.label, | itemName: updatedItem.label, | ||||
| itemCode: updatedItem.label, | itemCode: updatedItem.label, | ||||
| qty: updatedItem.qty || 1, | qty: updatedItem.qty || 1, | ||||
| uom: updatedItem.uom || "", | uom: updatedItem.uom || "", | ||||
| uomId: updatedItem.uomId || 0, | |||||
| uomId: Number(updatedItem.uomId) || 0, | |||||
| uomDesc: updatedItem.uomDesc || "", | uomDesc: updatedItem.uomDesc || "", | ||||
| isSelected: true, | isSelected: true, | ||||
| currentStockBalance: updatedItem.currentStockBalance, | currentStockBalance: updatedItem.currentStockBalance, | ||||
| @@ -1618,29 +1631,16 @@ const getValidationMessage = useCallback(() => { | |||||
| groupId: updatedItem.groupId || undefined, | groupId: updatedItem.groupId || undefined, | ||||
| }; | }; | ||||
| setCreatedItems(prev => [...prev, newCreatedItem]); | setCreatedItems(prev => [...prev, newCreatedItem]); | ||||
| setSecondSearchResults(current => current.filter(searchItem => searchItem.id !== itemId)); | |||||
| setSelectedSecondSearchItemIds(current => current.filter(id => id !== itemId)); | |||||
| // Remove from processing set after a short delay | |||||
| setSecondSearchResults(current => current.filter(searchItem => | |||||
| (searchItem.rowKey || toSearchRowKey(searchItem.id, searchItem.uomId)) !== rowKey | |||||
| )); | |||||
| setSelectedSecondSearchItemIds(current => current.filter(id => id !== rowKey)); | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| processingItems.delete(itemId); | |||||
| processingItems.delete(rowKey); | |||||
| }, 100); | }, 100); | ||||
| } | } | ||||
| // Show final debug info in one alert | |||||
| /* | |||||
| alert(`FINAL DEBUG INFO for item ${itemId}: | |||||
| Function called ${checkboxChangeCallCount} times | |||||
| Is Selected: ${isSelected} | |||||
| Has Group: ${hasGroup} | |||||
| Has Quantity: ${hasQty} | |||||
| Quantity: ${updatedItem.qty} | |||||
| Group ID: ${updatedItem.groupId} | |||||
| Is Item In Created: ${isItemInCreated(updatedItem.id)} | |||||
| Auto-add triggered: ${isSelected && hasGroup && hasQty && !isItemInCreated(updatedItem.id)} | |||||
| Processing items: ${Array.from(processingItems).join(', ')}`); | |||||
| */ | |||||
| } | } | ||||
| }, 0); | }, 0); | ||||
| @@ -1648,11 +1648,11 @@ Processing items: ${Array.from(processingItems).join(', ')}`); | |||||
| }); | }); | ||||
| } else { | } else { | ||||
| // Remove from selected IDs | // Remove from selected IDs | ||||
| setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId)); | |||||
| setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== rowKey)); | |||||
| // Clear the item's group and targetDate when deselected | // Clear the item's group and targetDate when deselected | ||||
| setSecondSearchResults(prev => prev.map(item => | setSecondSearchResults(prev => prev.map(item => | ||||
| item.id === itemId | |||||
| (item.rowKey || toSearchRowKey(item.id, item.uomId)) === rowKey | |||||
| ? { | ? { | ||||
| ...item, | ...item, | ||||
| groupId: undefined, | groupId: undefined, | ||||
| @@ -1668,17 +1668,19 @@ const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: S | |||||
| if (checked) { | if (checked) { | ||||
| // Select all items on current page that are not already in created items | // Select all items on current page that are not already in created items | ||||
| const newSelectedIds = paginatedResults | const newSelectedIds = paginatedResults | ||||
| .filter(item => !isItemInCreated(item.id)) | |||||
| .map(item => item.id); | |||||
| .filter(item => !isItemInCreated(item.id, item.uomId)) | |||||
| .map(item => item.rowKey || toSearchRowKey(item.id, item.uomId)); | |||||
| setSelectedSecondSearchItemIds(prev => { | setSelectedSecondSearchItemIds(prev => { | ||||
| const existingIds = prev.filter(id => !paginatedResults.some(item => item.id === id)); | |||||
| const existingIds = prev.filter(id => !paginatedResults.some(item => | |||||
| (item.rowKey || toSearchRowKey(item.id, item.uomId)) === id | |||||
| )); | |||||
| return [...existingIds, ...newSelectedIds]; | return [...existingIds, ...newSelectedIds]; | ||||
| }); | }); | ||||
| // Set group and targetDate for all selected items on current page | // Set group and targetDate for all selected items on current page | ||||
| setSecondSearchResults(prev => prev.map(item => | setSecondSearchResults(prev => prev.map(item => | ||||
| newSelectedIds.includes(item.id) | |||||
| newSelectedIds.includes(item.rowKey || toSearchRowKey(item.id, item.uomId)) | |||||
| ? { | ? { | ||||
| ...item, | ...item, | ||||
| groupId: selectedGroup?.id || undefined, | groupId: selectedGroup?.id || undefined, | ||||
| @@ -1688,12 +1690,12 @@ const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: S | |||||
| )); | )); | ||||
| } else { | } else { | ||||
| // Deselect all items on current page | // Deselect all items on current page | ||||
| const pageItemIds = paginatedResults.map(item => item.id); | |||||
| setSelectedSecondSearchItemIds(prev => prev.filter(id => !pageItemIds.includes(id as number))); | |||||
| const pageItemIds = paginatedResults.map(item => item.rowKey || toSearchRowKey(item.id, item.uomId)); | |||||
| setSelectedSecondSearchItemIds(prev => prev.filter(id => !pageItemIds.includes(String(id)))); | |||||
| // Clear group and targetDate for all deselected items on current page | // Clear group and targetDate for all deselected items on current page | ||||
| setSecondSearchResults(prev => prev.map(item => | setSecondSearchResults(prev => prev.map(item => | ||||
| pageItemIds.includes(item.id) | |||||
| pageItemIds.includes(item.rowKey || toSearchRowKey(item.id, item.uomId)) | |||||
| ? { | ? { | ||||
| ...item, | ...item, | ||||
| groupId: undefined, | groupId: undefined, | ||||
| @@ -497,6 +497,10 @@ | |||||
| "Tomorrow": "Tomorrow", | "Tomorrow": "Tomorrow", | ||||
| "packaging": "packaging", | "packaging": "packaging", | ||||
| "This lot is not available, please scan another lot.": "This lot is not available, please scan another lot.", | "This lot is not available, please scan another lot.": "This lot is not available, please scan another lot.", | ||||
| "This lot UOM does not match the pick line. Please scan another lot.": "This lot UOM does not match the pick line. Please scan another lot.", | |||||
| "Scanned lot UOM does not match pick line UOM": "This lot UOM does not match the pick line. Please scan another lot.", | |||||
| "This lot has already been picked. Please scan another lot.": "This lot has already been picked. Please scan another lot.", | |||||
| "Scanned lot is already completed or checked": "This lot has already been picked. Please scan another lot.", | |||||
| "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.", | "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.", | ||||
| "Lot is expired (expiry={{expiry}})": "Lot is expired (expiry={{expiry}})", | "Lot is expired (expiry={{expiry}})": "Lot is expired (expiry={{expiry}})", | ||||
| "Day After Tomorrow": "Day After Tomorrow", | "Day After Tomorrow": "Day After Tomorrow", | ||||
| @@ -507,6 +507,10 @@ | |||||
| "packaging": "提料中", | "packaging": "提料中", | ||||
| "No Stock Available": "沒有庫存可用", | "No Stock Available": "沒有庫存可用", | ||||
| "This lot is not available, please scan another lot.": "此批號不可用,請掃描其他批號。", | "This lot is not available, please scan another lot.": "此批號不可用,請掃描其他批號。", | ||||
| "This lot UOM does not match the pick line. Please scan another lot.": "此批號單位與提貨行不符,請掃描其他批號。", | |||||
| "Scanned lot UOM does not match pick line UOM": "此批號單位與提貨行不符,請掃描其他批號。", | |||||
| "This lot has already been picked. Please scan another lot.": "此批號已提貨,請掃描其他批號。", | |||||
| "Scanned lot is already completed or checked": "此物料已提貨,請掃描其他批號。", | |||||
| "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "請檢查周圍是否有其他可用 QR 碼。", | "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "請檢查周圍是否有其他可用 QR 碼。", | ||||
| "Lot is expired (expiry={{expiry}})": "掃描批號已過期(到期日={{expiry}})", | "Lot is expired (expiry={{expiry}})": "掃描批號已過期(到期日={{expiry}})", | ||||
| "Day After Tomorrow": "後日", | "Day After Tomorrow": "後日", | ||||