| @@ -16,7 +16,7 @@ import { | |||
| fetchInventoryLotLines, | |||
| } from '@/app/api/inventory/actions'; | |||
| import { PrinterCombo } from '@/app/api/settings/printer'; | |||
| import { ItemCombo, fetchItemsWithDetails } from '@/app/api/settings/item/actions'; | |||
| import { ItemCombo, fetchItemsWithDetails, ItemWithDetails } from '@/app/api/settings/item/actions'; | |||
| import { | |||
| Button, | |||
| Dialog, | |||
| @@ -59,6 +59,38 @@ type SearchParamNames = keyof SearchQuery; | |||
| const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||
| const { t } = useTranslation(['inventory', 'common', 'item']); | |||
| const buildSyntheticInventory = useCallback( | |||
| (item: ItemWithDetails): InventoryResult => ({ | |||
| id: 0, | |||
| itemId: item.id, | |||
| itemCode: item.code, | |||
| itemName: item.name, | |||
| itemType: 'Material', | |||
| onHandQty: 0, | |||
| onHoldQty: 0, | |||
| unavailableQty: 0, | |||
| availableQty: 0, | |||
| uomCode: item.uom, | |||
| uomUdfudesc: item.uomDesc, | |||
| uomShortDesc: item.uom, | |||
| qtyPerSmallestUnit: 1, | |||
| baseUom: item.uom, | |||
| price: 0, | |||
| currencyName: '', | |||
| status: 'active', | |||
| latestMarketUnitPrice: undefined, | |||
| latestMupUpdatedDate: undefined, | |||
| }), | |||
| [], | |||
| ); | |||
| const getFirstItemRecord = useCallback((res: any): ItemWithDetails | null => { | |||
| if (!res) return null; | |||
| if (Array.isArray(res)) return (res[0] as ItemWithDetails) ?? null; | |||
| if (Array.isArray(res?.records)) return (res.records[0] as ItemWithDetails) ?? null; | |||
| return null; | |||
| }, []); | |||
| // Inventory | |||
| const [filteredInventories, setFilteredInventories] = useState<InventoryResult[]>([]); | |||
| @@ -260,21 +292,48 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||
| // On Search | |||
| const onSearch = useCallback( | |||
| (query: Record<SearchParamNames, string>) => { | |||
| async (query: Record<SearchParamNames, string>) => { | |||
| setLotNoFilter(''); | |||
| setScannedItemId(null); | |||
| setScanUiMode('idle'); | |||
| setScanHoverCancel(false); | |||
| qrScanner.stopScan(); | |||
| qrScanner.resetScan(); | |||
| refetchInventoryData(query, 'search', defaultPagingController, ''); | |||
| refetchInventoryLotLineData(null, 'search', defaultPagingController); | |||
| const invRes = await refetchInventoryData(query, 'search', defaultPagingController, ''); | |||
| await refetchInventoryLotLineData(null, 'search', defaultPagingController); | |||
| setInputs(() => query); | |||
| setInventoriesPagingController(() => defaultPagingController); | |||
| setInventoryLotLinesPagingController(() => defaultPagingController); | |||
| // If there are no inventory rows, render a synthetic inventory so the "Stock Adjustment" chip can be used. | |||
| if (invRes?.records?.length === 0) { | |||
| try { | |||
| const code = query.itemCode?.trim?.(); | |||
| const name = query.itemName?.trim?.(); | |||
| const lookupParams = code ? { code } : name ? { name } : null; | |||
| if (lookupParams) { | |||
| const itemRes = await fetchItemsWithDetails(lookupParams); | |||
| const firstItem = getFirstItemRecord(itemRes); | |||
| if (firstItem) { | |||
| setSelectedInventory(buildSyntheticInventory(firstItem)); | |||
| setFilteredInventoryLotLines([]); | |||
| setInventoryLotLinesPagingController(() => defaultPagingController); | |||
| } | |||
| } | |||
| } catch (e) { | |||
| console.error('Failed to build synthetic inventory:', e); | |||
| } | |||
| } | |||
| }, | |||
| [qrScanner, refetchInventoryData, refetchInventoryLotLineData], | |||
| [ | |||
| qrScanner, | |||
| refetchInventoryData, | |||
| refetchInventoryLotLineData, | |||
| buildSyntheticInventory, | |||
| getFirstItemRecord, | |||
| ], | |||
| ); | |||
| const startLotScan = useCallback(() => { | |||
| @@ -319,7 +378,16 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||
| onInventoryRowClick(target); | |||
| } else { | |||
| refetchInventoryLotLineData(null, 'search', defaultPagingController); | |||
| setSelectedInventory(null); | |||
| // No inventory rows for this scanned item => show synthetic inventory with the existing chip workflow. | |||
| const itemRes = await fetchItemsWithDetails({ code: res?.itemCode }); | |||
| const firstItem = getFirstItemRecord(itemRes); | |||
| if (firstItem) { | |||
| setSelectedInventory(buildSyntheticInventory(firstItem)); | |||
| setFilteredInventoryLotLines([]); | |||
| setInventoryLotLinesPagingController(() => defaultPagingController); | |||
| } else { | |||
| setSelectedInventory(null); | |||
| } | |||
| } | |||
| setInventoriesPagingController(() => defaultPagingController); | |||
| @@ -338,6 +406,8 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||
| qrScanner.result, | |||
| refetchInventoryData, | |||
| refetchInventoryLotLineData, | |||
| buildSyntheticInventory, | |||
| getFirstItemRecord, | |||
| scanUiMode, | |||
| ]); | |||
| @@ -471,6 +541,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||
| variant="outlined" | |||
| color="secondary" | |||
| onClick={handleOpenOpeningInventoryModal} | |||
| sx={{ display: 'none' }} | |||
| > | |||
| {t('Add entry for items without inventory')} | |||
| </Button> | |||
| @@ -499,13 +570,31 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||
| inventoryLotLinesPagingController, | |||
| ) | |||
| } | |||
| onStockAdjustmentSuccess={() => | |||
| refetchInventoryLotLineData( | |||
| selectedInventory?.itemId ?? null, | |||
| onStockAdjustmentSuccess={async () => { | |||
| const itemId = selectedInventory?.itemId ?? null; | |||
| // Refresh both blocks: | |||
| // - middle: InventoryTable (inventories list) | |||
| // - bottom: InventoryLotLineTable (lot lines for selected item) | |||
| const invRes = await refetchInventoryData( | |||
| inputs, | |||
| 'search', | |||
| inventoriesPagingController, | |||
| lotNoFilter, | |||
| ); | |||
| await refetchInventoryLotLineData( | |||
| itemId, | |||
| 'search', | |||
| inventoryLotLinesPagingController, | |||
| ) | |||
| } | |||
| ); | |||
| // If inventory becomes available again after OPEN/ADJ, sync selected row. | |||
| if (itemId != null && invRes?.records?.length) { | |||
| const target = invRes.records.find((r) => r.itemId === itemId); | |||
| if (target) setSelectedInventory(target); | |||
| } | |||
| }} | |||
| /> | |||
| <Dialog | |||