- 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.
151 lines
5.3 KiB
TypeScript
151 lines
5.3 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { toast } from 'sonner';
|
|
import { api } from '../lib/api';
|
|
import type { ApiResponse } from '../lib/api';
|
|
|
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
|
|
export type InventoryStatus = 'in_stock' | 'low_stock' | 'out_of_stock';
|
|
|
|
export interface InventoryItem {
|
|
uuid: string;
|
|
name: string;
|
|
consumable: string | null;
|
|
category: string | null;
|
|
unit: string;
|
|
price: number; // Rial
|
|
stock: number;
|
|
alertThreshold: number;
|
|
status: InventoryStatus;
|
|
}
|
|
|
|
/** Backend-owned option lists (GET /api/v1/inventory-meta) — never hardcode client-side. */
|
|
export interface InventoryMeta {
|
|
units: string[];
|
|
categories: string[];
|
|
}
|
|
|
|
export interface InventoryStats {
|
|
total: number;
|
|
low: number;
|
|
inStock: number;
|
|
outOfStock: number;
|
|
}
|
|
|
|
export interface PackageLine {
|
|
itemUuid: string;
|
|
name: string;
|
|
unit: string;
|
|
price: number; // Rial
|
|
amount: number;
|
|
}
|
|
|
|
export interface InventoryPackage {
|
|
uuid: string;
|
|
title: string;
|
|
items: PackageLine[];
|
|
total: number; // Rial
|
|
available: boolean;
|
|
}
|
|
|
|
export interface ItemPayload {
|
|
name: string;
|
|
consumable?: string | null;
|
|
category?: string | null;
|
|
unit?: string;
|
|
price?: number;
|
|
stock?: number;
|
|
alertThreshold?: number;
|
|
}
|
|
|
|
export interface PackagePayload {
|
|
title: string;
|
|
items: { itemUuid: string; amount: number }[];
|
|
}
|
|
|
|
const EMPTY_STATS: InventoryStats = { total: 0, low: 0, inStock: 0, outOfStock: 0 };
|
|
const EMPTY_ITEMS: InventoryItem[] = [];
|
|
const EMPTY_PACKAGES: InventoryPackage[] = [];
|
|
const EMPTY_CATS: string[] = [];
|
|
const EMPTY_META: InventoryMeta = { units: [], categories: [] };
|
|
|
|
// ── Hook ─────────────────────────────────────────────────────────────────────
|
|
|
|
/** Per-tenant inventory: items (+stats), packages and category filter options. */
|
|
export function useInventory() {
|
|
const qc = useQueryClient();
|
|
|
|
const itemsQuery = useQuery<ApiResponse<{ items: InventoryItem[]; stats: InventoryStats }>>({
|
|
queryKey: ['inventory-items'],
|
|
queryFn: () => api.get('/api/v1/inventory-items'),
|
|
});
|
|
|
|
const packagesQuery = useQuery<ApiResponse<InventoryPackage[]>>({
|
|
queryKey: ['inventory-packages'],
|
|
queryFn: () => api.get('/api/v1/inventory-packages'),
|
|
});
|
|
|
|
const categoriesQuery = useQuery<ApiResponse<string[]>>({
|
|
queryKey: ['inventory-categories'],
|
|
queryFn: () => api.get('/api/v1/inventory-categories'),
|
|
});
|
|
|
|
// Option lists are effectively static — fetch once, never refetch.
|
|
const metaQuery = useQuery<ApiResponse<InventoryMeta>>({
|
|
queryKey: ['inventory-meta'],
|
|
queryFn: () => api.get('/api/v1/inventory-meta'),
|
|
staleTime: Infinity,
|
|
});
|
|
|
|
const invalidateItems = () => {
|
|
qc.invalidateQueries({ queryKey: ['inventory-items'] });
|
|
qc.invalidateQueries({ queryKey: ['inventory-categories'] });
|
|
qc.invalidateQueries({ queryKey: ['inventory-packages'] });
|
|
};
|
|
const invalidatePackages = () => qc.invalidateQueries({ queryKey: ['inventory-packages'] });
|
|
|
|
const createItem = useMutation({
|
|
mutationFn: (d: ItemPayload) => api.post('/api/v1/inventory-item', d),
|
|
onSuccess: () => { invalidateItems(); toast.success('کالا اضافه شد'); },
|
|
onError: (e: any) => toast.error(e.message),
|
|
});
|
|
const updateItem = useMutation({
|
|
mutationFn: ({ uuid, d }: { uuid: string; d: ItemPayload }) => api.patch(`/api/v1/inventory-item/${uuid}`, d),
|
|
onSuccess: () => { invalidateItems(); toast.success('کالا ویرایش شد'); },
|
|
onError: (e: any) => toast.error(e.message),
|
|
});
|
|
const deleteItem = useMutation({
|
|
mutationFn: (uuid: string) => api.delete(`/api/v1/inventory-item/${uuid}`),
|
|
onSuccess: () => { invalidateItems(); toast.success('کالا حذف شد'); },
|
|
onError: (e: any) => toast.error(e.message),
|
|
});
|
|
|
|
const createPackage = useMutation({
|
|
mutationFn: (d: PackagePayload) => api.post('/api/v1/inventory-package', d),
|
|
onSuccess: () => { invalidatePackages(); toast.success('پکیج اضافه شد'); },
|
|
onError: (e: any) => toast.error(e.message),
|
|
});
|
|
const updatePackage = useMutation({
|
|
mutationFn: ({ uuid, d }: { uuid: string; d: PackagePayload }) => api.patch(`/api/v1/inventory-package/${uuid}`, d),
|
|
onSuccess: () => { invalidatePackages(); toast.success('پکیج ویرایش شد'); },
|
|
onError: (e: any) => toast.error(e.message),
|
|
});
|
|
const deletePackage = useMutation({
|
|
mutationFn: (uuid: string) => api.delete(`/api/v1/inventory-package/${uuid}`),
|
|
onSuccess: () => { invalidatePackages(); toast.success('پکیج حذف شد'); },
|
|
onError: (e: any) => toast.error(e.message),
|
|
});
|
|
|
|
return {
|
|
items: itemsQuery.data?.data?.items ?? EMPTY_ITEMS,
|
|
stats: itemsQuery.data?.data?.stats ?? EMPTY_STATS,
|
|
packages: packagesQuery.data?.data ?? EMPTY_PACKAGES,
|
|
categories: categoriesQuery.data?.data ?? EMPTY_CATS,
|
|
meta: metaQuery.data?.data ?? EMPTY_META,
|
|
itemsLoading: itemsQuery.isLoading,
|
|
packagesLoading: packagesQuery.isLoading,
|
|
createItem, updateItem, deleteItem,
|
|
createPackage, updatePackage, deletePackage,
|
|
};
|
|
}
|