Files
bakery-ia/frontend/src/api/hooks/useInventory.ts
2025-08-13 17:39:35 +02:00

510 lines
14 KiB
TypeScript

// 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
};
};