// frontend/src/api/hooks/useInventory.ts /** * Inventory Management React Hook * Provides comprehensive state management for inventory operations */ import { useState, useEffect, useCallback } from 'react'; import toast from 'react-hot-toast'; import { inventoryService, InventoryItem, StockLevel, StockMovement, StockAlert, InventorySearchParams, CreateInventoryItemRequest, UpdateInventoryItemRequest, StockAdjustmentRequest, PaginatedResponse, InventoryDashboardData } from '../services/inventory.service'; import { useTenantId } from '../../hooks/useTenantId'; // ========== HOOK INTERFACES ========== interface UseInventoryReturn { // State items: InventoryItem[]; stockLevels: Record; movements: StockMovement[]; alerts: StockAlert[]; dashboardData: InventoryDashboardData | null; isLoading: boolean; error: string | null; pagination: { page: number; limit: number; total: number; totalPages: number; }; // Actions loadItems: (params?: InventorySearchParams) => Promise; loadItem: (itemId: string) => Promise; createItem: (data: CreateInventoryItemRequest) => Promise; updateItem: (itemId: string, data: UpdateInventoryItemRequest) => Promise; deleteItem: (itemId: string) => Promise; // Stock operations loadStockLevels: () => Promise; adjustStock: (itemId: string, adjustment: StockAdjustmentRequest) => Promise; loadMovements: (params?: any) => Promise; // Alerts loadAlerts: () => Promise; acknowledgeAlert: (alertId: string) => Promise; // Dashboard loadDashboard: () => Promise; // Utility searchItems: (query: string) => Promise; refresh: () => Promise; clearError: () => void; } interface UseInventoryDashboardReturn { dashboardData: InventoryDashboardData | null; alerts: StockAlert[]; isLoading: boolean; error: string | null; refresh: () => Promise; } interface UseInventoryItemReturn { item: InventoryItem | null; stockLevel: StockLevel | null; recentMovements: StockMovement[]; isLoading: boolean; error: string | null; updateItem: (data: UpdateInventoryItemRequest) => Promise; adjustStock: (adjustment: StockAdjustmentRequest) => Promise; refresh: () => Promise; } // ========== MAIN INVENTORY HOOK ========== export const useInventory = (autoLoad = true): UseInventoryReturn => { const tenantId = useTenantId(); // State const [items, setItems] = useState([]); const [stockLevels, setStockLevels] = useState>({}); const [movements, setMovements] = useState([]); const [alerts, setAlerts] = useState([]); const [dashboardData, setDashboardData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [pagination, setPagination] = useState({ page: 1, limit: 20, total: 0, totalPages: 0 }); // Clear error const clearError = useCallback(() => setError(null), []); // Load inventory items const loadItems = useCallback(async (params?: InventorySearchParams) => { if (!tenantId) return; setIsLoading(true); setError(null); try { const response = await inventoryService.getInventoryItems(tenantId, params); setItems(response.items); setPagination({ page: response.page, limit: response.limit, total: response.total, totalPages: response.total_pages }); } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Error loading inventory items'; setError(errorMessage); toast.error(errorMessage); } finally { setIsLoading(false); } }, [tenantId]); // Load single item const loadItem = useCallback(async (itemId: string): Promise => { if (!tenantId) return null; try { const item = await inventoryService.getInventoryItem(tenantId, itemId); // Update in local state if it exists setItems(prev => prev.map(i => i.id === itemId ? item : i)); return item; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Error loading item'; setError(errorMessage); return null; } }, [tenantId]); // Create item const createItem = useCallback(async (data: CreateInventoryItemRequest): Promise => { if (!tenantId) return null; setIsLoading(true); try { const newItem = await inventoryService.createInventoryItem(tenantId, data); setItems(prev => [newItem, ...prev]); toast.success(`Created ${newItem.name} successfully`); return newItem; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Error creating item'; setError(errorMessage); toast.error(errorMessage); return null; } finally { setIsLoading(false); } }, [tenantId]); // Update item const updateItem = useCallback(async ( itemId: string, data: UpdateInventoryItemRequest ): Promise => { if (!tenantId) return null; try { const updatedItem = await inventoryService.updateInventoryItem(tenantId, itemId, data); setItems(prev => prev.map(i => i.id === itemId ? updatedItem : i)); toast.success(`Updated ${updatedItem.name} successfully`); return updatedItem; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Error updating item'; setError(errorMessage); toast.error(errorMessage); return null; } }, [tenantId]); // Delete item const deleteItem = useCallback(async (itemId: string): Promise => { if (!tenantId) return false; try { await inventoryService.deleteInventoryItem(tenantId, itemId); setItems(prev => prev.filter(i => i.id !== itemId)); toast.success('Item deleted successfully'); return true; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Error deleting item'; setError(errorMessage); toast.error(errorMessage); return false; } }, [tenantId]); // Load stock levels const loadStockLevels = useCallback(async () => { if (!tenantId) return; try { const levels = await inventoryService.getAllStockLevels(tenantId); const levelMap = levels.reduce((acc, level) => { acc[level.item_id] = level; return acc; }, {} as Record); setStockLevels(levelMap); } catch (err: any) { console.error('Error loading stock levels:', err); } }, [tenantId]); // Adjust stock const adjustStock = useCallback(async ( itemId: string, adjustment: StockAdjustmentRequest ): Promise => { if (!tenantId) return null; try { const movement = await inventoryService.adjustStock(tenantId, itemId, adjustment); // Update local movements setMovements(prev => [movement, ...prev.slice(0, 49)]); // Keep last 50 // Reload stock level for this item const updatedLevel = await inventoryService.getStockLevel(tenantId, itemId); setStockLevels(prev => ({ ...prev, [itemId]: updatedLevel })); toast.success('Stock adjusted successfully'); return movement; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Error adjusting stock'; setError(errorMessage); toast.error(errorMessage); return null; } }, [tenantId]); // Load movements const loadMovements = useCallback(async (params?: any) => { if (!tenantId) return; try { const response = await inventoryService.getStockMovements(tenantId, params); setMovements(response.items); } catch (err: any) { console.error('Error loading movements:', err); } }, [tenantId]); // Load alerts const loadAlerts = useCallback(async () => { if (!tenantId) return; try { const alertsData = await inventoryService.getStockAlerts(tenantId); setAlerts(alertsData); } catch (err: any) { console.error('Error loading alerts:', err); } }, [tenantId]); // Acknowledge alert const acknowledgeAlert = useCallback(async (alertId: string): Promise => { if (!tenantId) return false; try { await inventoryService.acknowledgeAlert(tenantId, alertId); setAlerts(prev => prev.map(a => a.id === alertId ? { ...a, is_acknowledged: true, acknowledged_at: new Date().toISOString() } : a )); return true; } catch (err: any) { toast.error('Error acknowledging alert'); return false; } }, [tenantId]); // Load dashboard const loadDashboard = useCallback(async () => { if (!tenantId) return; try { const data = await inventoryService.getDashboardData(tenantId); setDashboardData(data); } catch (err: any) { console.error('Error loading dashboard:', err); } }, [tenantId]); // Search items const searchItems = useCallback(async (query: string): Promise => { if (!tenantId || !query.trim()) return []; try { return await inventoryService.searchItems(tenantId, query); } catch (err: any) { console.error('Error searching items:', err); return []; } }, [tenantId]); // Refresh all data const refresh = useCallback(async () => { await Promise.all([ loadItems(), loadStockLevels(), loadAlerts(), loadDashboard() ]); }, [loadItems, loadStockLevels, loadAlerts, loadDashboard]); // Auto-load on mount useEffect(() => { if (autoLoad && tenantId) { refresh(); } }, [autoLoad, tenantId, refresh]); return { // State items, stockLevels, movements, alerts, dashboardData, isLoading, error, pagination, // Actions loadItems, loadItem, createItem, updateItem, deleteItem, // Stock operations loadStockLevels, adjustStock, loadMovements, // Alerts loadAlerts, acknowledgeAlert, // Dashboard loadDashboard, // Utility searchItems, refresh, clearError }; }; // ========== DASHBOARD HOOK ========== export const useInventoryDashboard = (): UseInventoryDashboardReturn => { const tenantId = useTenantId(); const [dashboardData, setDashboardData] = useState(null); const [alerts, setAlerts] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const refresh = useCallback(async () => { if (!tenantId) return; setIsLoading(true); setError(null); try { const [dashboard, alertsData] = await Promise.all([ inventoryService.getDashboardData(tenantId), inventoryService.getStockAlerts(tenantId) ]); setDashboardData(dashboard); setAlerts(alertsData); } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Error loading dashboard'; setError(errorMessage); } finally { setIsLoading(false); } }, [tenantId]); useEffect(() => { if (tenantId) { refresh(); } }, [tenantId, refresh]); return { dashboardData, alerts, isLoading, error, refresh }; }; // ========== SINGLE ITEM HOOK ========== export const useInventoryItem = (itemId: string): UseInventoryItemReturn => { const tenantId = useTenantId(); const [item, setItem] = useState(null); const [stockLevel, setStockLevel] = useState(null); const [recentMovements, setRecentMovements] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const refresh = useCallback(async () => { if (!tenantId || !itemId) return; setIsLoading(true); setError(null); try { const [itemData, stockData, movementsData] = await Promise.all([ inventoryService.getInventoryItem(tenantId, itemId), inventoryService.getStockLevel(tenantId, itemId), inventoryService.getStockMovements(tenantId, { item_id: itemId, limit: 10 }) ]); setItem(itemData); setStockLevel(stockData); setRecentMovements(movementsData.items); } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Error loading item'; setError(errorMessage); } finally { setIsLoading(false); } }, [tenantId, itemId]); const updateItem = useCallback(async (data: UpdateInventoryItemRequest): Promise => { if (!tenantId || !itemId) return false; try { const updatedItem = await inventoryService.updateInventoryItem(tenantId, itemId, data); setItem(updatedItem); toast.success('Item updated successfully'); return true; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Error updating item'; setError(errorMessage); toast.error(errorMessage); return false; } }, [tenantId, itemId]); const adjustStock = useCallback(async (adjustment: StockAdjustmentRequest): Promise => { if (!tenantId || !itemId) return false; try { const movement = await inventoryService.adjustStock(tenantId, itemId, adjustment); // Refresh data const [updatedStock, updatedMovements] = await Promise.all([ inventoryService.getStockLevel(tenantId, itemId), inventoryService.getStockMovements(tenantId, { item_id: itemId, limit: 10 }) ]); setStockLevel(updatedStock); setRecentMovements(updatedMovements.items); toast.success('Stock adjusted successfully'); return true; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Error adjusting stock'; setError(errorMessage); toast.error(errorMessage); return false; } }, [tenantId, itemId]); useEffect(() => { if (tenantId && itemId) { refresh(); } }, [tenantId, itemId, refresh]); return { item, stockLevel, recentMovements, isLoading, error, updateItem, adjustStock, refresh }; };