'use client'; import { InventoryLotLineResult, InventoryResult } from '@/app/api/inventory'; import { useTranslation } from 'react-i18next'; import SearchBox, { Criterion } from '../SearchBox'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { uniq, uniqBy } from 'lodash'; import InventoryTable from './InventoryTable'; import { defaultPagingController } from '../SearchResults/SearchResults'; import InventoryLotLineTable from './InventoryLotLineTable'; import { useQrCodeScannerContext } from '@/components/QrCodeScannerProvider/QrCodeScannerProvider'; import { analyzeQrCode, SearchInventory, SearchInventoryLotLine, fetchInventories, fetchInventoryLotLines, } from '@/app/api/inventory/actions'; import { PrinterCombo } from '@/app/api/settings/printer'; import { ItemCombo, fetchItemsWithDetails } from '@/app/api/settings/item/actions'; import { Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField, Box, CircularProgress, Table, TableBody, TableCell, TableHead, TableRow, Radio, } from '@mui/material'; interface Props { inventories: InventoryResult[]; printerCombo?: PrinterCombo[]; } type SearchQuery = Partial< Omit< InventoryResult, | "id" | "qty" | "uomCode" | "uomUdfudesc" | "germPerSmallestUnit" | "qtyPerSmallestUnit" | "itemSmallestUnit" | "price" | "description" | "category" > >; type SearchParamNames = keyof SearchQuery; const InventorySearch: React.FC = ({ inventories, printerCombo }) => { const { t, i18n } = useTranslation(['inventory', 'common', 'item']); // #region agent log useEffect(() => { fetch('http://127.0.0.1:7242/ingest/8c332e19-29c4-4171-9317-8619340856a2', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': 'aca475', }, body: JSON.stringify({ sessionId: 'aca475', runId: 'run1', hypothesisId: 'H1', location: 'src/components/InventorySearch/InventorySearch.tsx:63', message: 'Check current stock translation lookup', data: { language: i18n.language, translatedCurrentStock: t('Current Stock'), translatedHardcodedLabel: t('現有庫存'), }, timestamp: Date.now(), }), }).catch(() => {}); }, [i18n.language, t]); // #endregion // Inventory const [filteredInventories, setFilteredInventories] = useState([]); const [inventoriesPagingController, setInventoriesPagingController] = useState(defaultPagingController) const [inventoriesTotalCount, setInventoriesTotalCount] = useState(0) const [selectedInventory, setSelectedInventory] = useState(null) // Inventory Lot Line const [filteredInventoryLotLines, setFilteredInventoryLotLines] = useState([]); const [inventoryLotLinesPagingController, setInventoryLotLinesPagingController] = useState(defaultPagingController) const [inventoryLotLinesTotalCount, setInventoryLotLinesTotalCount] = useState(0) // Scan-mode UI (hardware QR scanner via QrCodeScannerProvider) const qrScanner = useQrCodeScannerContext(); const [scanUiMode, setScanUiMode] = useState<'idle' | 'scanning'>('idle'); const [scanHoverCancel, setScanHoverCancel] = useState(false); // Resolved lot no for filtering const [lotNoFilter, setLotNoFilter] = useState(''); const [scannedItemId, setScannedItemId] = useState(null); // Opening inventory (pure opening stock for items without existing inventory) const [openingItems, setOpeningItems] = useState([]); const [openingModalOpen, setOpeningModalOpen] = useState(false); const [openingSelectedItem, setOpeningSelectedItem] = useState(null); const [openingLoading, setOpeningLoading] = useState(false); const [openingSearchText, setOpeningSearchText] = useState(''); const defaultInputs = useMemo( () => ({ itemId: '', itemCode: '', itemName: '', itemType: '', onHandQty: '', onHoldQty: '', unavailableQty: '', availableQty: '', currencyName: '', status: '', baseUom: '', uomShortDesc: '', latestMarketUnitPrice: '', latestMupUpdatedDate: '', }), [], ); const [inputs, setInputs] = useState>(defaultInputs); const searchCriteria: Criterion[] = useMemo( () => [ { label: t('Code'), paramName: 'itemCode', type: 'text' }, { label: t('Name'), paramName: 'itemName', type: 'text' }, { label: t('Type'), paramName: 'itemType', type: 'select', options: uniq(inventories.map((i) => i.itemType)), }, // { // label: t("Status"), // paramName: "status", // type: "select", // options: uniq(inventories.map((i) => i.status)), // }, ], [t, inventories], ); // Inventory const refetchInventoryData = useCallback( async ( query: Record, actionType: 'reset' | 'search' | 'paging' | 'init', pagingController: typeof defaultPagingController, lotNo: string, ) => { console.log('%c Action Type 1.', 'color:red', actionType); // Avoid loading data again if (actionType === 'paging' && pagingController === defaultPagingController) { return; } console.log('%c Action Type 2.', 'color:blue', actionType); const params: SearchInventory = { code: query?.itemCode ?? '', name: query?.itemName ?? '', type: query?.itemType.toLowerCase() === 'all' ? '' : query?.itemType ?? '', lotNo: lotNo?.trim() ? lotNo.trim() : undefined, pageNum: pagingController.pageNum - 1, pageSize: pagingController.pageSize, }; const response = await fetchInventories(params); if (response) { setInventoriesTotalCount(response.total); switch (actionType) { case 'init': case 'reset': case 'search': setFilteredInventories(() => response.records); break; case 'paging': setFilteredInventories((fi) => uniqBy([...fi, ...response.records], 'id'), ); } } return response; }, [], ); useEffect(() => { refetchInventoryData(defaultInputs, 'init', defaultPagingController, ''); }, [defaultInputs, refetchInventoryData]); useEffect(() => { // if (!isEqual(inventoriesPagingController, defaultPagingController)) { refetchInventoryData(inputs, 'paging', inventoriesPagingController, lotNoFilter) // } }, [inventoriesPagingController, inputs, lotNoFilter, refetchInventoryData]) // Inventory Lot Line const refetchInventoryLotLineData = useCallback( async ( itemId: number | null, actionType: 'reset' | 'search' | 'paging', pagingController: typeof defaultPagingController, ) => { if (!itemId) { setSelectedInventory(null); setInventoryLotLinesTotalCount(0); setFilteredInventoryLotLines([]); return; } // Avoid loading data again if (actionType === 'paging' && pagingController === defaultPagingController) { return; } const params: SearchInventoryLotLine = { itemId, pageNum: pagingController.pageNum - 1, pageSize: pagingController.pageSize, }; const response = await fetchInventoryLotLines(params); if (response) { setInventoryLotLinesTotalCount(response.total); switch (actionType) { case 'reset': case 'search': setFilteredInventoryLotLines(() => response.records); break; case 'paging': setFilteredInventoryLotLines((fi) => uniqBy([...fi, ...response.records], 'id')); } } }, [], ); useEffect(() => { // if (!isEqual(inventoryLotLinesPagingController, defaultPagingController)) { refetchInventoryLotLineData(selectedInventory?.itemId ?? null, 'paging', inventoryLotLinesPagingController) // } }, [inventoryLotLinesPagingController]) // Reset const onReset = useCallback(() => { refetchInventoryData(defaultInputs, 'reset', defaultPagingController, ''); refetchInventoryLotLineData(null, 'reset', defaultPagingController); // setFilteredInventories(inventories); setLotNoFilter(''); setScannedItemId(null); setScanUiMode('idle'); setScanHoverCancel(false); qrScanner.stopScan(); qrScanner.resetScan(); setInputs(() => defaultInputs) setInventoriesPagingController(() => defaultPagingController) setInventoryLotLinesPagingController(() => defaultPagingController) }, [defaultInputs, qrScanner, refetchInventoryData, refetchInventoryLotLineData]); // Click Row const onInventoryRowClick = useCallback( (item: InventoryResult) => { refetchInventoryLotLineData(item.itemId, 'search', defaultPagingController); setSelectedInventory(item); setInventoryLotLinesPagingController(() => defaultPagingController); }, [refetchInventoryLotLineData], ); // On Search const onSearch = useCallback( (query: Record) => { setLotNoFilter(''); setScannedItemId(null); setScanUiMode('idle'); setScanHoverCancel(false); qrScanner.stopScan(); qrScanner.resetScan(); refetchInventoryData(query, 'search', defaultPagingController, ''); refetchInventoryLotLineData(null, 'search', defaultPagingController); setInputs(() => query); setInventoriesPagingController(() => defaultPagingController); setInventoryLotLinesPagingController(() => defaultPagingController); }, [qrScanner, refetchInventoryData, refetchInventoryLotLineData], ); const startLotScan = useCallback(() => { setScanHoverCancel(false); setScanUiMode('scanning'); qrScanner.resetScan(); qrScanner.startScan(); }, [qrScanner]); const cancelLotScan = useCallback(() => { qrScanner.stopScan(); qrScanner.resetScan(); setScanUiMode('idle'); setScanHoverCancel(false); }, [qrScanner]); useEffect(() => { if (scanUiMode !== 'scanning') return; const itemId = qrScanner.result?.itemId; const stockInLineId = qrScanner.result?.stockInLineId; if (!itemId || !stockInLineId) return; (async () => { try { const res = await analyzeQrCode({ itemId: Number(itemId), stockInLineId: Number(stockInLineId), }); const resolvedLotNo = res?.scanned?.lotNo?.trim?.() ? res.scanned.lotNo.trim() : ''; if (!resolvedLotNo) return; setLotNoFilter(resolvedLotNo); setScannedItemId(res?.itemId ?? Number(itemId)); const invRes = await refetchInventoryData(inputs, 'search', defaultPagingController, resolvedLotNo); const records = invRes?.records ?? []; const target = records.find((r) => r.itemId === (res?.itemId ?? Number(itemId))) ?? null; if (target) { onInventoryRowClick(target); } else { refetchInventoryLotLineData(null, 'search', defaultPagingController); setSelectedInventory(null); } setInventoriesPagingController(() => defaultPagingController); setInventoryLotLinesPagingController(() => defaultPagingController); } catch (e) { console.error('Failed to analyze QR code:', e); } finally { // Always go back to initial state after a scan attempt cancelLotScan(); } })(); }, [ cancelLotScan, inputs, onInventoryRowClick, qrScanner.result, refetchInventoryData, refetchInventoryLotLineData, scanUiMode, ]); console.log('', 'color: #666', inventoriesPagingController); const handleOpenOpeningInventoryModal = useCallback(() => { setOpeningSelectedItem(null); setOpeningItems([]); setOpeningSearchText(''); setOpeningModalOpen(true); }, []); const handleOpeningSearch = useCallback(async () => { const trimmed = openingSearchText.trim(); if (!trimmed) { setOpeningItems([]); return; } setOpeningLoading(true); try { const searchParams: Record = { pageSize: 50, pageNum: 1, }; // Heuristic: if input contains space, treat as name; otherwise treat as code. if (trimmed.includes(' ')) { searchParams.name = trimmed; } else { searchParams.code = trimmed; } const response = await fetchItemsWithDetails(searchParams); let records: any[] = []; if (response && typeof response === 'object') { const anyRes = response as any; if (Array.isArray(anyRes.records)) { records = anyRes.records; } else if (Array.isArray(anyRes)) { records = anyRes; } } const combos: ItemCombo[] = records.map((item: any) => ({ id: item.id, label: `${item.code} - ${item.name}`, uomId: item.uomId, uom: item.uom, uomDesc: item.uomDesc, group: item.group, currentStockBalance: item.currentStockBalance, })); setOpeningItems(combos); } catch (e) { console.error('Failed to search items for opening inventory:', e); setOpeningItems([]); } finally { setOpeningLoading(false); } }, [openingSearchText]); const handleConfirmOpeningInventory = useCallback(() => { if (!openingSelectedItem) { setOpeningModalOpen(false); return; } // Try to split label into code and name if possible: "CODE - Name" const rawLabel = openingSelectedItem.label ?? ''; const [codePart, ...nameParts] = rawLabel.split(' - '); const itemCode = codePart?.trim() || rawLabel; const itemName = nameParts.join(' - ').trim() || itemCode; const syntheticInventory: InventoryResult = { id: 0, itemId: Number(openingSelectedItem.id), itemCode, itemName, itemType: 'Material', onHandQty: 0, onHoldQty: 0, unavailableQty: 0, availableQty: 0, uomCode: openingSelectedItem.uom, uomUdfudesc: openingSelectedItem.uomDesc, uomShortDesc: openingSelectedItem.uom, qtyPerSmallestUnit: 1, baseUom: openingSelectedItem.uom, price: 0, currencyName: '', status: 'active', latestMarketUnitPrice: undefined, latestMupUpdatedDate: undefined, }; // Use this synthetic inventory to drive the stock adjustment UI setSelectedInventory(syntheticInventory); setFilteredInventoryLotLines([]); setInventoryLotLinesPagingController(() => defaultPagingController); setOpeningModalOpen(false); }, [openingSelectedItem]); return ( <> { onSearch(query); }} onReset={onReset} extraActions={ {scanUiMode === 'idle' ? ( ) : ( <> )} } /> refetchInventoryLotLineData( selectedInventory?.itemId ?? null, 'search', inventoryLotLinesPagingController, ) } onStockAdjustmentSuccess={() => refetchInventoryLotLineData( selectedInventory?.itemId ?? null, 'search', inventoryLotLinesPagingController, ) } /> setOpeningModalOpen(false)} fullWidth maxWidth="md" > {t('Add entry for items without inventory')} setOpeningSearchText(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleOpeningSearch(); } }} sx={{ flex: 2 }} /> {openingItems.length === 0 && !openingLoading ? ( {openingSearchText ? t('No data') : t('Enter item code or name to search')} ) : ( {t('Code')} {t('Name')} {t('UoM')} {t('Current Stock')} {openingItems.map((it) => { const [code, ...nameParts] = (it.label ?? '').split(' - '); const name = nameParts.join(' - '); const selected = openingSelectedItem?.id === it.id; return ( setOpeningSelectedItem(it)} sx={{ cursor: 'pointer' }} > {code} {name} {it.uomDesc || it.uom} {it.currentStockBalance != null ? it.currentStockBalance : '-'} ); })}
)}
); }; export default InventorySearch;