feat: Implement category and unit selection for inventory items
- Added a new 'category' field to the InventoryItem entity and updated the database schema. - Replaced free-text input for 'unit' and 'category' with select dropdowns in the AddItemModal. - Introduced a new API endpoint to fetch metadata for units and categories. - Updated inventory filtering logic to use the new 'category' field instead of 'consumable'. - Enhanced validation for item creation and updates to ensure valid unit and category values. - Updated tests to cover new functionality and ensure proper validation.
This commit is contained in:
@@ -14,18 +14,20 @@ import InventoryPage from './InventoryPage';
|
||||
const get = api.get as ReturnType<typeof vi.fn>;
|
||||
|
||||
const ITEM = {
|
||||
uuid: 'i-1', name: 'دستکش جراحی', consumable: 'جراحی', unit: 'عدد',
|
||||
uuid: 'i-1', name: 'دستکش جراحی', consumable: 'جراحی', category: 'لوازم مصرفی و تزریقات', unit: 'عدد',
|
||||
price: 250000, stock: 150, alertThreshold: 20, status: 'in_stock',
|
||||
};
|
||||
const META = { units: ['عدد', 'بسته', 'ویال'], categories: ['دارو', 'لوازم مصرفی و تزریقات', 'سایر'] };
|
||||
const STATS = { total: 1, low: 0, inStock: 1, outOfStock: 0 };
|
||||
const PKG = {
|
||||
uuid: 'p-1', title: 'پکیج شماره یک', total: 2400000, available: true,
|
||||
items: [{ itemUuid: 'i-1', name: 'ژل', unit: 'سیسی', price: 1200000, amount: 2 }],
|
||||
};
|
||||
|
||||
function mockApi(opts: { items?: any[]; stats?: any; packages?: any[]; categories?: string[] } = {}) {
|
||||
function mockApi(opts: { items?: any[]; stats?: any; packages?: any[]; categories?: string[]; meta?: any } = {}) {
|
||||
get.mockImplementation((url: string = '') => {
|
||||
if (url.includes('/inventory-packages')) return Promise.resolve({ success: true, data: opts.packages ?? [] });
|
||||
if (url.includes('/inventory-meta')) return Promise.resolve({ success: true, data: opts.meta ?? META });
|
||||
if (url.includes('/inventory-categories')) return Promise.resolve({ success: true, data: opts.categories ?? [] });
|
||||
if (url.includes('/inventory-items')) return Promise.resolve({ success: true, data: { items: opts.items ?? [], stats: opts.stats ?? { total: 0, low: 0, inStock: 0, outOfStock: 0 } } });
|
||||
return Promise.resolve({ success: true, data: { items: [], stats: { total: 0, low: 0, inStock: 0, outOfStock: 0 } } });
|
||||
@@ -69,6 +71,21 @@ describe('InventoryPage', () => {
|
||||
expect(price.value).toBe('1,200,000');
|
||||
});
|
||||
|
||||
it('blocks submit when category is missing and unit is a picker, not free text', async () => {
|
||||
mockApi({ items: [], stats: { total: 0, low: 0, inStock: 0, outOfStock: 0 } });
|
||||
renderWithProviders(<InventoryPage />, { route: '/admin/inventory' });
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /افزودن کالا/ }));
|
||||
await screen.findByText('افزودن کالای جدید');
|
||||
|
||||
// unit is now a select — no free-text input with the old placeholder
|
||||
expect(screen.queryByPlaceholderText('عدد')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('نام کالا'), { target: { value: 'ماسک' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'اضافه کردن کالا' }));
|
||||
expect(await screen.findByText('دستهبندی کالا الزامی است')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('switches to the packages tab and lists a package with its price', async () => {
|
||||
mockApi({ packages: [PKG] });
|
||||
renderWithProviders(<InventoryPage />, { route: '/admin/inventory' });
|
||||
|
||||
@@ -14,7 +14,7 @@ type Tab = 'stock' | 'packages';
|
||||
/** انبارداری — consumable stock items + packages. Ported from clinic-pro-tauri /inventory. */
|
||||
export default function InventoryPage() {
|
||||
const {
|
||||
items, stats, packages, categories, itemsLoading, packagesLoading,
|
||||
items, stats, packages, categories, meta, itemsLoading, packagesLoading,
|
||||
createItem, updateItem, deleteItem, createPackage, updatePackage, deletePackage,
|
||||
} = useInventory();
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function InventoryPage() {
|
||||
const q = search.trim();
|
||||
return items.filter((it) =>
|
||||
(q === '' || it.name.includes(q)) &&
|
||||
(category === '' || it.consumable === category)
|
||||
(category === '' || it.category === category)
|
||||
);
|
||||
}, [items, search, category]);
|
||||
|
||||
@@ -120,6 +120,7 @@ export default function InventoryPage() {
|
||||
<AddItemModal
|
||||
open={itemModal.open}
|
||||
editing={itemModal.editing}
|
||||
meta={meta}
|
||||
saving={createItem.isPending || updateItem.isPending}
|
||||
onClose={() => setItemModal({ open: false, editing: null })}
|
||||
onSave={saveItem}
|
||||
|
||||
Reference in New Issue
Block a user