475 lines
13 KiB
TypeScript
475 lines
13 KiB
TypeScript
|
|
/**
|
||
|
|
* Inventory hook for managing inventory state and operations
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { useState, useEffect, useCallback } from 'react';
|
||
|
|
import { InventoryService } from '../../services/api/inventory.service';
|
||
|
|
import {
|
||
|
|
Ingredient,
|
||
|
|
IngredientCreate,
|
||
|
|
IngredientUpdate,
|
||
|
|
StockLevel,
|
||
|
|
StockMovement,
|
||
|
|
StockMovementCreate,
|
||
|
|
InventoryAlert,
|
||
|
|
QualityCheckCreate,
|
||
|
|
QualityCheck
|
||
|
|
} from '../../types/inventory.types';
|
||
|
|
import { ApiResponse, PaginatedResponse, QueryParams } from '../../types/api.types';
|
||
|
|
|
||
|
|
interface InventoryState {
|
||
|
|
ingredients: Ingredient[];
|
||
|
|
stockLevels: StockLevel[];
|
||
|
|
stockMovements: StockMovement[];
|
||
|
|
alerts: InventoryAlert[];
|
||
|
|
qualityChecks: QualityCheck[];
|
||
|
|
isLoading: boolean;
|
||
|
|
error: string | null;
|
||
|
|
pagination: {
|
||
|
|
total: number;
|
||
|
|
page: number;
|
||
|
|
pages: number;
|
||
|
|
limit: number;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
interface InventoryActions {
|
||
|
|
// Ingredients
|
||
|
|
fetchIngredients: (params?: QueryParams) => Promise<void>;
|
||
|
|
createIngredient: (data: IngredientCreate) => Promise<boolean>;
|
||
|
|
updateIngredient: (id: string, data: IngredientUpdate) => Promise<boolean>;
|
||
|
|
deleteIngredient: (id: string) => Promise<boolean>;
|
||
|
|
getIngredient: (id: string) => Promise<Ingredient | null>;
|
||
|
|
|
||
|
|
// Stock Levels
|
||
|
|
fetchStockLevels: (params?: QueryParams) => Promise<void>;
|
||
|
|
updateStockLevel: (ingredientId: string, quantity: number, reason?: string) => Promise<boolean>;
|
||
|
|
|
||
|
|
// Stock Movements
|
||
|
|
fetchStockMovements: (params?: QueryParams) => Promise<void>;
|
||
|
|
createStockMovement: (data: StockMovementCreate) => Promise<boolean>;
|
||
|
|
|
||
|
|
// Alerts
|
||
|
|
fetchAlerts: (params?: QueryParams) => Promise<void>;
|
||
|
|
markAlertAsRead: (id: string) => Promise<boolean>;
|
||
|
|
dismissAlert: (id: string) => Promise<boolean>;
|
||
|
|
|
||
|
|
// Quality Checks
|
||
|
|
fetchQualityChecks: (params?: QueryParams) => Promise<void>;
|
||
|
|
createQualityCheck: (data: QualityCheckCreate) => Promise<boolean>;
|
||
|
|
|
||
|
|
// Analytics
|
||
|
|
getInventoryAnalytics: (startDate?: string, endDate?: string) => Promise<any>;
|
||
|
|
getExpirationReport: () => Promise<any>;
|
||
|
|
|
||
|
|
// Utilities
|
||
|
|
clearError: () => void;
|
||
|
|
refresh: () => Promise<void>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const useInventory = (): InventoryState & InventoryActions => {
|
||
|
|
const [state, setState] = useState<InventoryState>({
|
||
|
|
ingredients: [],
|
||
|
|
stockLevels: [],
|
||
|
|
stockMovements: [],
|
||
|
|
alerts: [],
|
||
|
|
qualityChecks: [],
|
||
|
|
isLoading: false,
|
||
|
|
error: null,
|
||
|
|
pagination: {
|
||
|
|
total: 0,
|
||
|
|
page: 1,
|
||
|
|
pages: 1,
|
||
|
|
limit: 20,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const inventoryService = new InventoryService();
|
||
|
|
|
||
|
|
// Fetch ingredients
|
||
|
|
const fetchIngredients = useCallback(async (params?: QueryParams) => {
|
||
|
|
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.getIngredients(params);
|
||
|
|
|
||
|
|
if (response.success && response.data) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
ingredients: Array.isArray(response.data) ? response.data : response.data.items || [],
|
||
|
|
pagination: response.data.pagination || prev.pagination,
|
||
|
|
isLoading: false,
|
||
|
|
}));
|
||
|
|
} else {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
isLoading: false,
|
||
|
|
error: response.error || 'Error al cargar ingredientes',
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
isLoading: false,
|
||
|
|
error: 'Error de conexión al servidor',
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}, [inventoryService]);
|
||
|
|
|
||
|
|
// Create ingredient
|
||
|
|
const createIngredient = useCallback(async (data: IngredientCreate): Promise<boolean> => {
|
||
|
|
setState(prev => ({ ...prev, error: null }));
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.createIngredient(data);
|
||
|
|
|
||
|
|
if (response.success) {
|
||
|
|
await fetchIngredients();
|
||
|
|
return true;
|
||
|
|
} else {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
error: response.error || 'Error al crear ingrediente',
|
||
|
|
}));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
error: 'Error de conexión al servidor',
|
||
|
|
}));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}, [inventoryService, fetchIngredients]);
|
||
|
|
|
||
|
|
// Update ingredient
|
||
|
|
const updateIngredient = useCallback(async (id: string, data: IngredientUpdate): Promise<boolean> => {
|
||
|
|
setState(prev => ({ ...prev, error: null }));
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.updateIngredient(id, data);
|
||
|
|
|
||
|
|
if (response.success) {
|
||
|
|
await fetchIngredients();
|
||
|
|
return true;
|
||
|
|
} else {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
error: response.error || 'Error al actualizar ingrediente',
|
||
|
|
}));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
error: 'Error de conexión al servidor',
|
||
|
|
}));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}, [inventoryService, fetchIngredients]);
|
||
|
|
|
||
|
|
// Delete ingredient
|
||
|
|
const deleteIngredient = useCallback(async (id: string): Promise<boolean> => {
|
||
|
|
setState(prev => ({ ...prev, error: null }));
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.deleteIngredient(id);
|
||
|
|
|
||
|
|
if (response.success) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
ingredients: prev.ingredients.filter(ingredient => ingredient.id !== id),
|
||
|
|
}));
|
||
|
|
return true;
|
||
|
|
} else {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
error: response.error || 'Error al eliminar ingrediente',
|
||
|
|
}));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
error: 'Error de conexión al servidor',
|
||
|
|
}));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}, [inventoryService]);
|
||
|
|
|
||
|
|
// Get single ingredient
|
||
|
|
const getIngredient = useCallback(async (id: string): Promise<Ingredient | null> => {
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.getIngredient(id);
|
||
|
|
return response.success ? response.data : null;
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error fetching ingredient:', error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}, [inventoryService]);
|
||
|
|
|
||
|
|
// Fetch stock levels
|
||
|
|
const fetchStockLevels = useCallback(async (params?: QueryParams) => {
|
||
|
|
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.getStockLevels(params);
|
||
|
|
|
||
|
|
if (response.success && response.data) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
stockLevels: Array.isArray(response.data) ? response.data : response.data.items || [],
|
||
|
|
isLoading: false,
|
||
|
|
}));
|
||
|
|
} else {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
isLoading: false,
|
||
|
|
error: response.error || 'Error al cargar niveles de stock',
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
isLoading: false,
|
||
|
|
error: 'Error de conexión al servidor',
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}, [inventoryService]);
|
||
|
|
|
||
|
|
// Update stock level
|
||
|
|
const updateStockLevel = useCallback(async (ingredientId: string, quantity: number, reason?: string): Promise<boolean> => {
|
||
|
|
setState(prev => ({ ...prev, error: null }));
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.updateStockLevel(ingredientId, {
|
||
|
|
quantity,
|
||
|
|
reason: reason || 'Manual adjustment'
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.success) {
|
||
|
|
await fetchStockLevels();
|
||
|
|
return true;
|
||
|
|
} else {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
error: response.error || 'Error al actualizar stock',
|
||
|
|
}));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
error: 'Error de conexión al servidor',
|
||
|
|
}));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}, [inventoryService, fetchStockLevels]);
|
||
|
|
|
||
|
|
// Fetch stock movements
|
||
|
|
const fetchStockMovements = useCallback(async (params?: QueryParams) => {
|
||
|
|
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.getStockMovements(params);
|
||
|
|
|
||
|
|
if (response.success && response.data) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
stockMovements: Array.isArray(response.data) ? response.data : response.data.items || [],
|
||
|
|
isLoading: false,
|
||
|
|
}));
|
||
|
|
} else {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
isLoading: false,
|
||
|
|
error: response.error || 'Error al cargar movimientos de stock',
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
isLoading: false,
|
||
|
|
error: 'Error de conexión al servidor',
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}, [inventoryService]);
|
||
|
|
|
||
|
|
// Create stock movement
|
||
|
|
const createStockMovement = useCallback(async (data: StockMovementCreate): Promise<boolean> => {
|
||
|
|
setState(prev => ({ ...prev, error: null }));
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.createStockMovement(data);
|
||
|
|
|
||
|
|
if (response.success) {
|
||
|
|
await fetchStockMovements();
|
||
|
|
await fetchStockLevels();
|
||
|
|
return true;
|
||
|
|
} else {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
error: response.error || 'Error al crear movimiento de stock',
|
||
|
|
}));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
error: 'Error de conexión al servidor',
|
||
|
|
}));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}, [inventoryService, fetchStockMovements, fetchStockLevels]);
|
||
|
|
|
||
|
|
// Fetch alerts
|
||
|
|
const fetchAlerts = useCallback(async (params?: QueryParams) => {
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.getAlerts(params);
|
||
|
|
|
||
|
|
if (response.success && response.data) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
alerts: Array.isArray(response.data) ? response.data : response.data.items || [],
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error fetching alerts:', error);
|
||
|
|
}
|
||
|
|
}, [inventoryService]);
|
||
|
|
|
||
|
|
// Mark alert as read
|
||
|
|
const markAlertAsRead = useCallback(async (id: string): Promise<boolean> => {
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.markAlertAsRead(id);
|
||
|
|
|
||
|
|
if (response.success) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
alerts: prev.alerts.map(alert =>
|
||
|
|
alert.id === id ? { ...alert, is_read: true } : alert
|
||
|
|
),
|
||
|
|
}));
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error marking alert as read:', error);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}, [inventoryService]);
|
||
|
|
|
||
|
|
// Dismiss alert
|
||
|
|
const dismissAlert = useCallback(async (id: string): Promise<boolean> => {
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.dismissAlert(id);
|
||
|
|
|
||
|
|
if (response.success) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
alerts: prev.alerts.filter(alert => alert.id !== id),
|
||
|
|
}));
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error dismissing alert:', error);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}, [inventoryService]);
|
||
|
|
|
||
|
|
// Fetch quality checks
|
||
|
|
const fetchQualityChecks = useCallback(async (params?: QueryParams) => {
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.getQualityChecks(params);
|
||
|
|
|
||
|
|
if (response.success && response.data) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
qualityChecks: Array.isArray(response.data) ? response.data : response.data.items || [],
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error fetching quality checks:', error);
|
||
|
|
}
|
||
|
|
}, [inventoryService]);
|
||
|
|
|
||
|
|
// Create quality check
|
||
|
|
const createQualityCheck = useCallback(async (data: QualityCheckCreate): Promise<boolean> => {
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.createQualityCheck(data);
|
||
|
|
|
||
|
|
if (response.success) {
|
||
|
|
await fetchQualityChecks();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error creating quality check:', error);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}, [inventoryService, fetchQualityChecks]);
|
||
|
|
|
||
|
|
// Get inventory analytics
|
||
|
|
const getInventoryAnalytics = useCallback(async (startDate?: string, endDate?: string) => {
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.getAnalytics(startDate, endDate);
|
||
|
|
return response.success ? response.data : null;
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error fetching inventory analytics:', error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}, [inventoryService]);
|
||
|
|
|
||
|
|
// Get expiration report
|
||
|
|
const getExpirationReport = useCallback(async () => {
|
||
|
|
try {
|
||
|
|
const response = await inventoryService.getExpirationReport();
|
||
|
|
return response.success ? response.data : null;
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error fetching expiration report:', error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}, [inventoryService]);
|
||
|
|
|
||
|
|
// Clear error
|
||
|
|
const clearError = useCallback(() => {
|
||
|
|
setState(prev => ({ ...prev, error: null }));
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// Refresh all data
|
||
|
|
const refresh = useCallback(async () => {
|
||
|
|
await Promise.all([
|
||
|
|
fetchIngredients(),
|
||
|
|
fetchStockLevels(),
|
||
|
|
fetchAlerts(),
|
||
|
|
]);
|
||
|
|
}, [fetchIngredients, fetchStockLevels, fetchAlerts]);
|
||
|
|
|
||
|
|
// Initialize data on mount
|
||
|
|
useEffect(() => {
|
||
|
|
refresh();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
return {
|
||
|
|
...state,
|
||
|
|
fetchIngredients,
|
||
|
|
createIngredient,
|
||
|
|
updateIngredient,
|
||
|
|
deleteIngredient,
|
||
|
|
getIngredient,
|
||
|
|
fetchStockLevels,
|
||
|
|
updateStockLevel,
|
||
|
|
fetchStockMovements,
|
||
|
|
createStockMovement,
|
||
|
|
fetchAlerts,
|
||
|
|
markAlertAsRead,
|
||
|
|
dismissAlert,
|
||
|
|
fetchQualityChecks,
|
||
|
|
createQualityCheck,
|
||
|
|
getInventoryAnalytics,
|
||
|
|
getExpirationReport,
|
||
|
|
clearError,
|
||
|
|
refresh,
|
||
|
|
};
|
||
|
|
};
|