Create new services: inventory, recipes, suppliers
This commit is contained in:
510
frontend/src/api/hooks/useInventory.ts
Normal file
510
frontend/src/api/hooks/useInventory.ts
Normal file
@@ -0,0 +1,510 @@
|
||||
// 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<string, StockLevel>;
|
||||
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<void>;
|
||||
loadItem: (itemId: string) => Promise<InventoryItem | null>;
|
||||
createItem: (data: CreateInventoryItemRequest) => Promise<InventoryItem | null>;
|
||||
updateItem: (itemId: string, data: UpdateInventoryItemRequest) => Promise<InventoryItem | null>;
|
||||
deleteItem: (itemId: string) => Promise<boolean>;
|
||||
|
||||
// Stock operations
|
||||
loadStockLevels: () => Promise<void>;
|
||||
adjustStock: (itemId: string, adjustment: StockAdjustmentRequest) => Promise<StockMovement | null>;
|
||||
loadMovements: (params?: any) => Promise<void>;
|
||||
|
||||
// Alerts
|
||||
loadAlerts: () => Promise<void>;
|
||||
acknowledgeAlert: (alertId: string) => Promise<boolean>;
|
||||
|
||||
// Dashboard
|
||||
loadDashboard: () => Promise<void>;
|
||||
|
||||
// Utility
|
||||
searchItems: (query: string) => Promise<InventoryItem[]>;
|
||||
refresh: () => Promise<void>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
interface UseInventoryDashboardReturn {
|
||||
dashboardData: InventoryDashboardData | null;
|
||||
alerts: StockAlert[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface UseInventoryItemReturn {
|
||||
item: InventoryItem | null;
|
||||
stockLevel: StockLevel | null;
|
||||
recentMovements: StockMovement[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
updateItem: (data: UpdateInventoryItemRequest) => Promise<boolean>;
|
||||
adjustStock: (adjustment: StockAdjustmentRequest) => Promise<boolean>;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
// ========== MAIN INVENTORY HOOK ==========
|
||||
|
||||
export const useInventory = (autoLoad = true): UseInventoryReturn => {
|
||||
const tenantId = useTenantId();
|
||||
|
||||
// State
|
||||
const [items, setItems] = useState<InventoryItem[]>([]);
|
||||
const [stockLevels, setStockLevels] = useState<Record<string, StockLevel>>({});
|
||||
const [movements, setMovements] = useState<StockMovement[]>([]);
|
||||
const [alerts, setAlerts] = useState<StockAlert[]>([]);
|
||||
const [dashboardData, setDashboardData] = useState<InventoryDashboardData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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<InventoryItem | null> => {
|
||||
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<InventoryItem | null> => {
|
||||
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<InventoryItem | null> => {
|
||||
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<boolean> => {
|
||||
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<string, StockLevel>);
|
||||
setStockLevels(levelMap);
|
||||
} catch (err: any) {
|
||||
console.error('Error loading stock levels:', err);
|
||||
}
|
||||
}, [tenantId]);
|
||||
|
||||
// Adjust stock
|
||||
const adjustStock = useCallback(async (
|
||||
itemId: string,
|
||||
adjustment: StockAdjustmentRequest
|
||||
): Promise<StockMovement | null> => {
|
||||
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<boolean> => {
|
||||
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<InventoryItem[]> => {
|
||||
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<InventoryDashboardData | null>(null);
|
||||
const [alerts, setAlerts] = useState<StockAlert[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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<InventoryItem | null>(null);
|
||||
const [stockLevel, setStockLevel] = useState<StockLevel | null>(null);
|
||||
const [recentMovements, setRecentMovements] = useState<StockMovement[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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<boolean> => {
|
||||
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<boolean> => {
|
||||
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
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user