Browse Source

better UX in inventory search

MergeProblem1
kelvin.yau 3 days ago
parent
commit
6fe4889b02
1 changed files with 100 additions and 11 deletions
  1. +100
    -11
      src/components/InventorySearch/InventorySearch.tsx

+ 100
- 11
src/components/InventorySearch/InventorySearch.tsx View File

@@ -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


Loading…
Cancel
Save