| @@ -16,7 +16,7 @@ import { | |||||
| fetchInventoryLotLines, | fetchInventoryLotLines, | ||||
| } from '@/app/api/inventory/actions'; | } from '@/app/api/inventory/actions'; | ||||
| import { PrinterCombo } from '@/app/api/settings/printer'; | 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 { | import { | ||||
| Button, | Button, | ||||
| Dialog, | Dialog, | ||||
| @@ -59,6 +59,38 @@ type SearchParamNames = keyof SearchQuery; | |||||
| 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']); | ||||
| 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 | // Inventory | ||||
| const [filteredInventories, setFilteredInventories] = useState<InventoryResult[]>([]); | const [filteredInventories, setFilteredInventories] = useState<InventoryResult[]>([]); | ||||
| @@ -260,21 +292,48 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||||
| // On Search | // On Search | ||||
| const onSearch = useCallback( | const onSearch = useCallback( | ||||
| (query: Record<SearchParamNames, string>) => { | |||||
| async (query: Record<SearchParamNames, string>) => { | |||||
| setLotNoFilter(''); | setLotNoFilter(''); | ||||
| setScannedItemId(null); | setScannedItemId(null); | ||||
| setScanUiMode('idle'); | setScanUiMode('idle'); | ||||
| setScanHoverCancel(false); | setScanHoverCancel(false); | ||||
| qrScanner.stopScan(); | qrScanner.stopScan(); | ||||
| qrScanner.resetScan(); | qrScanner.resetScan(); | ||||
| refetchInventoryData(query, 'search', defaultPagingController, ''); | |||||
| refetchInventoryLotLineData(null, 'search', defaultPagingController); | |||||
| const invRes = await refetchInventoryData(query, 'search', defaultPagingController, ''); | |||||
| await refetchInventoryLotLineData(null, 'search', defaultPagingController); | |||||
| setInputs(() => query); | setInputs(() => query); | ||||
| setInventoriesPagingController(() => defaultPagingController); | setInventoriesPagingController(() => defaultPagingController); | ||||
| setInventoryLotLinesPagingController(() => 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(() => { | const startLotScan = useCallback(() => { | ||||
| @@ -319,7 +378,16 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||||
| onInventoryRowClick(target); | onInventoryRowClick(target); | ||||
| } else { | } else { | ||||
| refetchInventoryLotLineData(null, 'search', defaultPagingController); | 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); | setInventoriesPagingController(() => defaultPagingController); | ||||
| @@ -338,6 +406,8 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||||
| qrScanner.result, | qrScanner.result, | ||||
| refetchInventoryData, | refetchInventoryData, | ||||
| refetchInventoryLotLineData, | refetchInventoryLotLineData, | ||||
| buildSyntheticInventory, | |||||
| getFirstItemRecord, | |||||
| scanUiMode, | scanUiMode, | ||||
| ]); | ]); | ||||
| @@ -471,6 +541,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||||
| variant="outlined" | variant="outlined" | ||||
| color="secondary" | color="secondary" | ||||
| onClick={handleOpenOpeningInventoryModal} | onClick={handleOpenOpeningInventoryModal} | ||||
| sx={{ display: 'none' }} | |||||
| > | > | ||||
| {t('Add entry for items without inventory')} | {t('Add entry for items without inventory')} | ||||
| </Button> | </Button> | ||||
| @@ -499,13 +570,31 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||||
| inventoryLotLinesPagingController, | 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', | 'search', | ||||
| inventoryLotLinesPagingController, | 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 | <Dialog | ||||