ADD new frontend

This commit is contained in:
Urtzi Alfaro
2025-08-28 10:41:04 +02:00
parent 9c247a5f99
commit 0fd273cfce
492 changed files with 114979 additions and 1632 deletions

View File

@@ -0,0 +1,321 @@
/**
* Authentication hook for managing user authentication state
*/
import { useState, useEffect, useCallback } from 'react';
import { authService } from '../../services/api/auth.service';
import { storageService } from '../../services/utils/storage.service';
import { User, UserLogin, UserRegistration, TokenResponse } from '../../types/auth.types';
import { ApiResponse } from '../../types/api.types';
interface AuthState {
user: User | null;
tenant_id: string | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
}
interface AuthActions {
login: (credentials: UserLogin) => Promise<boolean>;
register: (data: UserRegistration) => Promise<boolean>;
logout: () => Promise<void>;
refreshToken: () => Promise<boolean>;
requestPasswordReset: (email: string) => Promise<boolean>;
resetPassword: (token: string, password: string) => Promise<boolean>;
verifyEmail: (token: string) => Promise<boolean>;
updateProfile: (data: Partial<User>) => Promise<boolean>;
switchTenant: (tenantId: string) => Promise<boolean>;
clearError: () => void;
}
export const useAuth = (): AuthState & AuthActions => {
const [state, setState] = useState<AuthState>({
user: null,
tenant_id: null,
isAuthenticated: false,
isLoading: true,
error: null,
});
// Initialize authentication state
useEffect(() => {
const initializeAuth = async () => {
try {
const access_token = storageService.getItem<string>('access_token');
const user_data = storageService.getItem('user_data');
const tenant_id = storageService.getItem<string>('tenant_id');
if (access_token) {
// Try to get current user profile
const profileResponse = await authService.getCurrentUser();
if (profileResponse.success && profileResponse.data) {
setState(prev => ({
...prev,
user: profileResponse.data,
tenant_id: tenant_id || null,
isAuthenticated: true,
isLoading: false,
}));
return;
}
}
// No valid authentication found
setState(prev => ({
...prev,
isLoading: false,
}));
} catch (error) {
console.error('Auth initialization error:', error);
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al inicializar la autenticación',
}));
}
};
initializeAuth();
}, []);
const login = useCallback(async (credentials: UserLogin): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await authService.login(credentials);
if (response.success && response.data) {
const profileResponse = await authService.getCurrentUser();
if (profileResponse.success && profileResponse.data) {
setState(prev => ({
...prev,
user: profileResponse.data,
tenant_id: response.data.user?.tenant_id || null,
isAuthenticated: true,
isLoading: false,
}));
return true;
}
}
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al iniciar sesión',
}));
return false;
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
return false;
}
}, []);
const register = useCallback(async (data: UserRegistration): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await authService.register(data);
setState(prev => ({
...prev,
isLoading: false,
error: response.success ? null : (response.error || 'Error al registrar usuario'),
}));
return response.success;
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
return false;
}
}, []);
const logout = useCallback(async (): Promise<void> => {
try {
await authService.logout();
} catch (error) {
console.error('Logout error:', error);
} finally {
setState({
user: null,
tenant_id: null,
isAuthenticated: false,
isLoading: false,
error: null,
});
}
}, []);
const refreshToken = useCallback(async (): Promise<boolean> => {
try {
const response = await authService.refreshToken();
return response.success;
} catch (error) {
console.error('Token refresh error:', error);
await logout();
return false;
}
}, [logout]);
const requestPasswordReset = useCallback(async (email: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await authService.resetPassword(email);
if (!response.success) {
setState(prev => ({
...prev,
error: response.error || 'Error al solicitar restablecimiento de contraseña',
}));
}
return response.success;
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, []);
const resetPassword = useCallback(async (token: string, password: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await authService.confirmPasswordReset({ token, new_password: password });
if (!response.success) {
setState(prev => ({
...prev,
error: response.error || 'Error al restablecer contraseña',
}));
}
return response.success;
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, []);
const verifyEmail = useCallback(async (token: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await authService.confirmEmailVerification(token);
if (!response.success) {
setState(prev => ({
...prev,
error: response.error || 'Error al verificar email',
}));
}
return response.success;
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, []);
const updateProfile = useCallback(async (data: Partial<User>): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await authService.updateProfile(data);
if (response.success && response.data) {
setState(prev => ({
...prev,
user: response.data,
isLoading: false,
}));
return true;
}
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al actualizar perfil',
}));
return false;
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
return false;
}
}, []);
const switchTenant = useCallback(async (tenantId: string): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// switchTenant method doesn't exist in AuthService, implement tenant switching logic here
// For now, just update the local state
storageService.setItem('tenant_id', tenantId);
const response = { success: true };
if (response.success) {
setState(prev => ({
...prev,
tenant_id: tenantId,
isLoading: false,
}));
return true;
}
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al cambiar organización',
}));
return false;
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
return false;
}
}, []);
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
return {
...state,
login,
register,
logout,
refreshToken,
requestPasswordReset,
resetPassword,
verifyEmail,
updateProfile,
switchTenant,
clearError,
};
};

View File

@@ -0,0 +1,568 @@
/**
* Forecasting hook for managing demand forecasting and ML models
*/
import { useState, useEffect, useCallback } from 'react';
import { ForecastingService } from '../../services/api/forecasting.service';
import {
ForecastModel,
ForecastModelCreate,
ForecastModelUpdate,
ForecastPrediction,
ForecastPredictionCreate,
ForecastBatch,
ModelTraining,
ModelEvaluation
} from '../../types/forecasting.types';
import { ApiResponse, PaginatedResponse, QueryParams } from '../../types/api.types';
interface ForecastingState {
models: ForecastModel[];
predictions: ForecastPrediction[];
batches: ForecastBatch[];
trainings: ModelTraining[];
evaluations: ModelEvaluation[];
isLoading: boolean;
error: string | null;
pagination: {
total: number;
page: number;
pages: number;
limit: number;
};
}
interface ForecastingActions {
// Models
fetchModels: (params?: QueryParams) => Promise<void>;
createModel: (data: ForecastModelCreate) => Promise<boolean>;
updateModel: (id: string, data: ForecastModelUpdate) => Promise<boolean>;
deleteModel: (id: string) => Promise<boolean>;
getModel: (id: string) => Promise<ForecastModel | null>;
trainModel: (id: string, parameters?: any) => Promise<boolean>;
deployModel: (id: string) => Promise<boolean>;
// Predictions
fetchPredictions: (params?: QueryParams) => Promise<void>;
createPrediction: (data: ForecastPredictionCreate) => Promise<boolean>;
getPrediction: (id: string) => Promise<ForecastPrediction | null>;
generateDemandForecast: (modelId: string, horizon: number, parameters?: any) => Promise<any>;
// Batch Predictions
fetchBatches: (params?: QueryParams) => Promise<void>;
createBatch: (modelId: string, data: any) => Promise<boolean>;
getBatch: (id: string) => Promise<ForecastBatch | null>;
downloadBatchResults: (id: string) => Promise<boolean>;
// Model Training
fetchTrainings: (modelId?: string) => Promise<void>;
getTraining: (id: string) => Promise<ModelTraining | null>;
cancelTraining: (id: string) => Promise<boolean>;
// Model Evaluation
fetchEvaluations: (modelId?: string) => Promise<void>;
createEvaluation: (modelId: string, testData: any) => Promise<boolean>;
getEvaluation: (id: string) => Promise<ModelEvaluation | null>;
// Analytics
getModelPerformance: (modelId: string, period?: string) => Promise<any>;
getAccuracyReport: (modelId: string, startDate?: string, endDate?: string) => Promise<any>;
getFeatureImportance: (modelId: string) => Promise<any>;
// Utilities
clearError: () => void;
refresh: () => Promise<void>;
}
export const useForecasting = (): ForecastingState & ForecastingActions => {
const [state, setState] = useState<ForecastingState>({
models: [],
predictions: [],
batches: [],
trainings: [],
evaluations: [],
isLoading: false,
error: null,
pagination: {
total: 0,
page: 1,
pages: 1,
limit: 20,
},
});
const forecastingService = new ForecastingService();
// Fetch forecast models
const fetchModels = useCallback(async (params?: QueryParams) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await forecastingService.getModels(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
models: 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 modelos de predicción',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [forecastingService]);
// Create forecast model
const createModel = useCallback(async (data: ForecastModelCreate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await forecastingService.createModel(data);
if (response.success) {
await fetchModels();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al crear modelo de predicción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [forecastingService, fetchModels]);
// Update forecast model
const updateModel = useCallback(async (id: string, data: ForecastModelUpdate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await forecastingService.updateModel(id, data);
if (response.success) {
await fetchModels();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al actualizar modelo de predicción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [forecastingService, fetchModels]);
// Delete forecast model
const deleteModel = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await forecastingService.deleteModel(id);
if (response.success) {
setState(prev => ({
...prev,
models: prev.models.filter(model => model.id !== id),
}));
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al eliminar modelo de predicción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [forecastingService]);
// Get single forecast model
const getModel = useCallback(async (id: string): Promise<ForecastModel | null> => {
try {
const response = await forecastingService.getModel(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching model:', error);
return null;
}
}, [forecastingService]);
// Train forecast model
const trainModel = useCallback(async (id: string, parameters?: any): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await forecastingService.trainModel(id, parameters);
if (response.success) {
await fetchModels();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al entrenar modelo',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [forecastingService, fetchModels]);
// Deploy forecast model
const deployModel = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await forecastingService.deployModel(id);
if (response.success) {
await fetchModels();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al desplegar modelo',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [forecastingService, fetchModels]);
// Fetch predictions
const fetchPredictions = useCallback(async (params?: QueryParams) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await forecastingService.getPredictions(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
predictions: Array.isArray(response.data) ? response.data : response.data.items || [],
isLoading: false,
}));
} else {
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al cargar predicciones',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [forecastingService]);
// Create prediction
const createPrediction = useCallback(async (data: ForecastPredictionCreate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await forecastingService.createPrediction(data);
if (response.success) {
await fetchPredictions();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al crear predicción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [forecastingService, fetchPredictions]);
// Get single prediction
const getPrediction = useCallback(async (id: string): Promise<ForecastPrediction | null> => {
try {
const response = await forecastingService.getPrediction(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching prediction:', error);
return null;
}
}, [forecastingService]);
// Generate demand forecast
const generateDemandForecast = useCallback(async (modelId: string, horizon: number, parameters?: any) => {
try {
const response = await forecastingService.generateDemandForecast(modelId, horizon, parameters);
return response.success ? response.data : null;
} catch (error) {
console.error('Error generating demand forecast:', error);
return null;
}
}, [forecastingService]);
// Fetch batch predictions
const fetchBatches = useCallback(async (params?: QueryParams) => {
try {
const response = await forecastingService.getBatches(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
batches: Array.isArray(response.data) ? response.data : response.data.items || [],
}));
}
} catch (error) {
console.error('Error fetching batches:', error);
}
}, [forecastingService]);
// Create batch prediction
const createBatch = useCallback(async (modelId: string, data: any): Promise<boolean> => {
try {
const response = await forecastingService.createBatch(modelId, data);
if (response.success) {
await fetchBatches();
return true;
}
return false;
} catch (error) {
console.error('Error creating batch:', error);
return false;
}
}, [forecastingService, fetchBatches]);
// Get single batch
const getBatch = useCallback(async (id: string): Promise<ForecastBatch | null> => {
try {
const response = await forecastingService.getBatch(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching batch:', error);
return null;
}
}, [forecastingService]);
// Download batch results
const downloadBatchResults = useCallback(async (id: string): Promise<boolean> => {
try {
const response = await forecastingService.downloadBatchResults(id);
return response.success;
} catch (error) {
console.error('Error downloading batch results:', error);
return false;
}
}, [forecastingService]);
// Fetch model trainings
const fetchTrainings = useCallback(async (modelId?: string) => {
try {
const response = await forecastingService.getTrainings(modelId);
if (response.success && response.data) {
setState(prev => ({
...prev,
trainings: Array.isArray(response.data) ? response.data : response.data.items || [],
}));
}
} catch (error) {
console.error('Error fetching trainings:', error);
}
}, [forecastingService]);
// Get single training
const getTraining = useCallback(async (id: string): Promise<ModelTraining | null> => {
try {
const response = await forecastingService.getTraining(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching training:', error);
return null;
}
}, [forecastingService]);
// Cancel model training
const cancelTraining = useCallback(async (id: string): Promise<boolean> => {
try {
const response = await forecastingService.cancelTraining(id);
if (response.success) {
await fetchTrainings();
return true;
}
return false;
} catch (error) {
console.error('Error canceling training:', error);
return false;
}
}, [forecastingService, fetchTrainings]);
// Fetch model evaluations
const fetchEvaluations = useCallback(async (modelId?: string) => {
try {
const response = await forecastingService.getEvaluations(modelId);
if (response.success && response.data) {
setState(prev => ({
...prev,
evaluations: Array.isArray(response.data) ? response.data : response.data.items || [],
}));
}
} catch (error) {
console.error('Error fetching evaluations:', error);
}
}, [forecastingService]);
// Create model evaluation
const createEvaluation = useCallback(async (modelId: string, testData: any): Promise<boolean> => {
try {
const response = await forecastingService.createEvaluation(modelId, testData);
if (response.success) {
await fetchEvaluations(modelId);
return true;
}
return false;
} catch (error) {
console.error('Error creating evaluation:', error);
return false;
}
}, [forecastingService, fetchEvaluations]);
// Get single evaluation
const getEvaluation = useCallback(async (id: string): Promise<ModelEvaluation | null> => {
try {
const response = await forecastingService.getEvaluation(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching evaluation:', error);
return null;
}
}, [forecastingService]);
// Get model performance
const getModelPerformance = useCallback(async (modelId: string, period?: string) => {
try {
const response = await forecastingService.getModelPerformance(modelId, period);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching model performance:', error);
return null;
}
}, [forecastingService]);
// Get accuracy report
const getAccuracyReport = useCallback(async (modelId: string, startDate?: string, endDate?: string) => {
try {
const response = await forecastingService.getAccuracyReport(modelId, startDate, endDate);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching accuracy report:', error);
return null;
}
}, [forecastingService]);
// Get feature importance
const getFeatureImportance = useCallback(async (modelId: string) => {
try {
const response = await forecastingService.getFeatureImportance(modelId);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching feature importance:', error);
return null;
}
}, [forecastingService]);
// Clear error
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
// Refresh all data
const refresh = useCallback(async () => {
await Promise.all([
fetchModels(),
fetchPredictions(),
fetchBatches(),
]);
}, [fetchModels, fetchPredictions, fetchBatches]);
// Initialize data on mount
useEffect(() => {
refresh();
}, []);
return {
...state,
fetchModels,
createModel,
updateModel,
deleteModel,
getModel,
trainModel,
deployModel,
fetchPredictions,
createPrediction,
getPrediction,
generateDemandForecast,
fetchBatches,
createBatch,
getBatch,
downloadBatchResults,
fetchTrainings,
getTraining,
cancelTraining,
fetchEvaluations,
createEvaluation,
getEvaluation,
getModelPerformance,
getAccuracyReport,
getFeatureImportance,
clearError,
refresh,
};
};

View File

@@ -0,0 +1,475 @@
/**
* 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,
};
};

View File

@@ -0,0 +1,650 @@
/**
* Production hook for managing production batches, recipes, and scheduling
*/
import { useState, useEffect, useCallback } from 'react';
import { ProductionService } from '../../services/api/production.service';
import {
ProductionBatch,
ProductionBatchCreate,
ProductionBatchUpdate,
Recipe,
RecipeCreate,
RecipeUpdate,
ProductionSchedule,
ProductionScheduleCreate,
QualityControl,
QualityControlCreate
} from '../../types/production.types';
import { ApiResponse, PaginatedResponse, QueryParams } from '../../types/api.types';
interface ProductionState {
batches: ProductionBatch[];
recipes: Recipe[];
schedules: ProductionSchedule[];
qualityControls: QualityControl[];
isLoading: boolean;
error: string | null;
pagination: {
total: number;
page: number;
pages: number;
limit: number;
};
}
interface ProductionActions {
// Production Batches
fetchBatches: (params?: QueryParams) => Promise<void>;
createBatch: (data: ProductionBatchCreate) => Promise<boolean>;
updateBatch: (id: string, data: ProductionBatchUpdate) => Promise<boolean>;
deleteBatch: (id: string) => Promise<boolean>;
getBatch: (id: string) => Promise<ProductionBatch | null>;
startBatch: (id: string) => Promise<boolean>;
completeBatch: (id: string) => Promise<boolean>;
cancelBatch: (id: string, reason: string) => Promise<boolean>;
// Recipes
fetchRecipes: (params?: QueryParams) => Promise<void>;
createRecipe: (data: RecipeCreate) => Promise<boolean>;
updateRecipe: (id: string, data: RecipeUpdate) => Promise<boolean>;
deleteRecipe: (id: string) => Promise<boolean>;
getRecipe: (id: string) => Promise<Recipe | null>;
duplicateRecipe: (id: string, name: string) => Promise<boolean>;
// Production Scheduling
fetchSchedules: (params?: QueryParams) => Promise<void>;
createSchedule: (data: ProductionScheduleCreate) => Promise<boolean>;
updateSchedule: (id: string, data: Partial<ProductionScheduleCreate>) => Promise<boolean>;
deleteSchedule: (id: string) => Promise<boolean>;
getCapacityAnalysis: (date: string) => Promise<any>;
// Quality Control
fetchQualityControls: (params?: QueryParams) => Promise<void>;
createQualityControl: (data: QualityControlCreate) => Promise<boolean>;
updateQualityControl: (id: string, data: Partial<QualityControlCreate>) => Promise<boolean>;
// Analytics
getProductionAnalytics: (startDate?: string, endDate?: string) => Promise<any>;
getEfficiencyReport: (period: string) => Promise<any>;
getRecipePerformance: (recipeId?: string) => Promise<any>;
// Utilities
clearError: () => void;
refresh: () => Promise<void>;
}
export const useProduction = (): ProductionState & ProductionActions => {
const [state, setState] = useState<ProductionState>({
batches: [],
recipes: [],
schedules: [],
qualityControls: [],
isLoading: false,
error: null,
pagination: {
total: 0,
page: 1,
pages: 1,
limit: 20,
},
});
const productionService = new ProductionService();
// Fetch production batches
const fetchBatches = useCallback(async (params?: QueryParams) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await productionService.getBatches(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
batches: 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 lotes de producción',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [productionService]);
// Create production batch
const createBatch = useCallback(async (data: ProductionBatchCreate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.createBatch(data);
if (response.success) {
await fetchBatches();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al crear lote de producción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchBatches]);
// Update production batch
const updateBatch = useCallback(async (id: string, data: ProductionBatchUpdate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.updateBatch(id, data);
if (response.success) {
await fetchBatches();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al actualizar lote de producción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchBatches]);
// Delete production batch
const deleteBatch = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.deleteBatch(id);
if (response.success) {
setState(prev => ({
...prev,
batches: prev.batches.filter(batch => batch.id !== id),
}));
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al eliminar lote de producción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService]);
// Get single production batch
const getBatch = useCallback(async (id: string): Promise<ProductionBatch | null> => {
try {
const response = await productionService.getBatch(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching batch:', error);
return null;
}
}, [productionService]);
// Start production batch
const startBatch = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.startBatch(id);
if (response.success) {
await fetchBatches();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al iniciar lote de producción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchBatches]);
// Complete production batch
const completeBatch = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.completeBatch(id);
if (response.success) {
await fetchBatches();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al completar lote de producción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchBatches]);
// Cancel production batch
const cancelBatch = useCallback(async (id: string, reason: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.cancelBatch(id, reason);
if (response.success) {
await fetchBatches();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al cancelar lote de producción',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchBatches]);
// Fetch recipes
const fetchRecipes = useCallback(async (params?: QueryParams) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await productionService.getRecipes(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
recipes: Array.isArray(response.data) ? response.data : response.data.items || [],
isLoading: false,
}));
} else {
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al cargar recetas',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [productionService]);
// Create recipe
const createRecipe = useCallback(async (data: RecipeCreate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.createRecipe(data);
if (response.success) {
await fetchRecipes();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al crear receta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchRecipes]);
// Update recipe
const updateRecipe = useCallback(async (id: string, data: RecipeUpdate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.updateRecipe(id, data);
if (response.success) {
await fetchRecipes();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al actualizar receta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchRecipes]);
// Delete recipe
const deleteRecipe = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.deleteRecipe(id);
if (response.success) {
setState(prev => ({
...prev,
recipes: prev.recipes.filter(recipe => recipe.id !== id),
}));
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al eliminar receta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService]);
// Get single recipe
const getRecipe = useCallback(async (id: string): Promise<Recipe | null> => {
try {
const response = await productionService.getRecipe(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching recipe:', error);
return null;
}
}, [productionService]);
// Duplicate recipe
const duplicateRecipe = useCallback(async (id: string, name: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await productionService.duplicateRecipe(id, name);
if (response.success) {
await fetchRecipes();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al duplicar receta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [productionService, fetchRecipes]);
// Fetch production schedules
const fetchSchedules = useCallback(async (params?: QueryParams) => {
try {
const response = await productionService.getSchedules(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
schedules: Array.isArray(response.data) ? response.data : response.data.items || [],
}));
}
} catch (error) {
console.error('Error fetching schedules:', error);
}
}, [productionService]);
// Create production schedule
const createSchedule = useCallback(async (data: ProductionScheduleCreate): Promise<boolean> => {
try {
const response = await productionService.createSchedule(data);
if (response.success) {
await fetchSchedules();
return true;
}
return false;
} catch (error) {
console.error('Error creating schedule:', error);
return false;
}
}, [productionService, fetchSchedules]);
// Update production schedule
const updateSchedule = useCallback(async (id: string, data: Partial<ProductionScheduleCreate>): Promise<boolean> => {
try {
const response = await productionService.updateSchedule(id, data);
if (response.success) {
await fetchSchedules();
return true;
}
return false;
} catch (error) {
console.error('Error updating schedule:', error);
return false;
}
}, [productionService, fetchSchedules]);
// Delete production schedule
const deleteSchedule = useCallback(async (id: string): Promise<boolean> => {
try {
const response = await productionService.deleteSchedule(id);
if (response.success) {
setState(prev => ({
...prev,
schedules: prev.schedules.filter(schedule => schedule.id !== id),
}));
return true;
}
return false;
} catch (error) {
console.error('Error deleting schedule:', error);
return false;
}
}, [productionService]);
// Get capacity analysis
const getCapacityAnalysis = useCallback(async (date: string) => {
try {
const response = await productionService.getCapacityAnalysis(date);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching capacity analysis:', error);
return null;
}
}, [productionService]);
// Fetch quality controls
const fetchQualityControls = useCallback(async (params?: QueryParams) => {
try {
const response = await productionService.getQualityControls(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
qualityControls: Array.isArray(response.data) ? response.data : response.data.items || [],
}));
}
} catch (error) {
console.error('Error fetching quality controls:', error);
}
}, [productionService]);
// Create quality control
const createQualityControl = useCallback(async (data: QualityControlCreate): Promise<boolean> => {
try {
const response = await productionService.createQualityControl(data);
if (response.success) {
await fetchQualityControls();
return true;
}
return false;
} catch (error) {
console.error('Error creating quality control:', error);
return false;
}
}, [productionService, fetchQualityControls]);
// Update quality control
const updateQualityControl = useCallback(async (id: string, data: Partial<QualityControlCreate>): Promise<boolean> => {
try {
const response = await productionService.updateQualityControl(id, data);
if (response.success) {
await fetchQualityControls();
return true;
}
return false;
} catch (error) {
console.error('Error updating quality control:', error);
return false;
}
}, [productionService, fetchQualityControls]);
// Get production analytics
const getProductionAnalytics = useCallback(async (startDate?: string, endDate?: string) => {
try {
const response = await productionService.getAnalytics(startDate, endDate);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching production analytics:', error);
return null;
}
}, [productionService]);
// Get efficiency report
const getEfficiencyReport = useCallback(async (period: string) => {
try {
const response = await productionService.getEfficiencyReport(period);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching efficiency report:', error);
return null;
}
}, [productionService]);
// Get recipe performance
const getRecipePerformance = useCallback(async (recipeId?: string) => {
try {
const response = await productionService.getRecipePerformance(recipeId);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching recipe performance:', error);
return null;
}
}, [productionService]);
// Clear error
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
// Refresh all data
const refresh = useCallback(async () => {
await Promise.all([
fetchBatches(),
fetchRecipes(),
fetchSchedules(),
]);
}, [fetchBatches, fetchRecipes, fetchSchedules]);
// Initialize data on mount
useEffect(() => {
refresh();
}, []);
return {
...state,
fetchBatches,
createBatch,
updateBatch,
deleteBatch,
getBatch,
startBatch,
completeBatch,
cancelBatch,
fetchRecipes,
createRecipe,
updateRecipe,
deleteRecipe,
getRecipe,
duplicateRecipe,
fetchSchedules,
createSchedule,
updateSchedule,
deleteSchedule,
getCapacityAnalysis,
fetchQualityControls,
createQualityControl,
updateQualityControl,
getProductionAnalytics,
getEfficiencyReport,
getRecipePerformance,
clearError,
refresh,
};
};

View File

@@ -0,0 +1,329 @@
/**
* Server-Sent Events (SSE) hook for real-time notifications and updates
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import { StorageService } from '../../services/api/utils/storage.service';
export interface SSEMessage {
id?: string;
event?: string;
data: any;
timestamp: number;
}
interface SSEState {
isConnected: boolean;
isConnecting: boolean;
error: string | null;
messages: SSEMessage[];
lastEventId: string | null;
}
interface SSEOptions {
withCredentials?: boolean;
reconnectInterval?: number;
maxReconnectAttempts?: number;
bufferSize?: number;
autoConnect?: boolean;
eventFilters?: string[];
}
interface SSEActions {
connect: (url: string) => void;
disconnect: () => void;
reconnect: () => void;
clearMessages: () => void;
clearError: () => void;
addEventListener: (eventType: string, handler: (data: any) => void) => () => void;
}
const DEFAULT_OPTIONS: Required<SSEOptions> = {
withCredentials: true,
reconnectInterval: 3000,
maxReconnectAttempts: 10,
bufferSize: 100,
autoConnect: true,
eventFilters: [],
};
export const useSSE = (
initialUrl?: string,
options: SSEOptions = {}
): SSEState & SSEActions => {
const [state, setState] = useState<SSEState>({
isConnected: false,
isConnecting: false,
error: null,
messages: [],
lastEventId: null,
});
const eventSourceRef = useRef<EventSource | null>(null);
const urlRef = useRef<string | null>(initialUrl || null);
const reconnectTimeoutRef = useRef<number | null>(null);
const reconnectAttemptsRef = useRef<number>(0);
const eventHandlersRef = useRef<Map<string, Set<(data: any) => void>>>(new Map());
const config = { ...DEFAULT_OPTIONS, ...options };
const storageService = new StorageService();
// Helper function to get auth headers
const getAuthHeaders = useCallback(() => {
const authData = storageService.getAuthData();
if (authData?.access_token) {
return `Bearer ${authData.access_token}`;
}
return null;
}, [storageService]);
// Helper function to build URL with auth token
const buildUrlWithAuth = useCallback((baseUrl: string) => {
const url = new URL(baseUrl);
const authToken = getAuthHeaders();
if (authToken) {
url.searchParams.set('Authorization', authToken);
}
if (state.lastEventId) {
url.searchParams.set('Last-Event-ID', state.lastEventId);
}
return url.toString();
}, [getAuthHeaders, state.lastEventId]);
// Add event listener for specific event types
const addEventListener = useCallback((eventType: string, handler: (data: any) => void) => {
if (!eventHandlersRef.current.has(eventType)) {
eventHandlersRef.current.set(eventType, new Set());
}
eventHandlersRef.current.get(eventType)!.add(handler);
// Return cleanup function
return () => {
const handlers = eventHandlersRef.current.get(eventType);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
eventHandlersRef.current.delete(eventType);
}
}
};
}, []);
// Process incoming message
const processMessage = useCallback((event: MessageEvent, eventType: string = 'message') => {
try {
let data: any;
try {
data = JSON.parse(event.data);
} catch {
data = event.data;
}
const message: SSEMessage = {
id: event.lastEventId || undefined,
event: eventType,
data,
timestamp: Date.now(),
};
// Filter messages if eventFilters is specified
if (config.eventFilters.length > 0 && !config.eventFilters.includes(eventType)) {
return;
}
setState(prev => ({
...prev,
messages: [...prev.messages.slice(-(config.bufferSize - 1)), message],
lastEventId: event.lastEventId || prev.lastEventId,
}));
// Call registered event handlers
const handlers = eventHandlersRef.current.get(eventType);
if (handlers) {
handlers.forEach(handler => {
try {
handler(data);
} catch (error) {
console.error('Error in SSE event handler:', error);
}
});
}
// Call generic message handlers
const messageHandlers = eventHandlersRef.current.get('message');
if (messageHandlers && eventType !== 'message') {
messageHandlers.forEach(handler => {
try {
handler(message);
} catch (error) {
console.error('Error in SSE message handler:', error);
}
});
}
} catch (error) {
console.error('Error processing SSE message:', error);
}
}, [config.eventFilters, config.bufferSize]);
// Connect to SSE endpoint
const connect = useCallback((url: string) => {
if (eventSourceRef.current) {
disconnect();
}
urlRef.current = url;
setState(prev => ({ ...prev, isConnecting: true, error: null }));
try {
const fullUrl = buildUrlWithAuth(url);
const eventSource = new EventSource(fullUrl, {
withCredentials: config.withCredentials,
});
eventSourceRef.current = eventSource;
eventSource.onopen = () => {
setState(prev => ({
...prev,
isConnected: true,
isConnecting: false,
error: null,
}));
reconnectAttemptsRef.current = 0;
// Clear reconnect timeout if it exists
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
};
eventSource.onmessage = (event) => {
processMessage(event, 'message');
};
eventSource.onerror = (error) => {
console.error('SSE error:', error);
setState(prev => ({
...prev,
isConnected: false,
isConnecting: false,
error: 'Error de conexión con el servidor',
}));
// Attempt reconnection if within limits
if (reconnectAttemptsRef.current < config.maxReconnectAttempts) {
reconnectAttemptsRef.current += 1;
reconnectTimeoutRef.current = window.setTimeout(() => {
if (urlRef.current) {
connect(urlRef.current);
}
}, config.reconnectInterval);
} else {
setState(prev => ({
...prev,
error: `Máximo número de intentos de reconexión alcanzado (${config.maxReconnectAttempts})`,
}));
}
};
// Add listeners for custom event types
const commonEventTypes = ['notification', 'alert', 'update', 'heartbeat'];
commonEventTypes.forEach(eventType => {
eventSource.addEventListener(eventType, (event) => {
processMessage(event as MessageEvent, eventType);
});
});
} catch (error) {
setState(prev => ({
...prev,
isConnecting: false,
error: 'Error al establecer conexión SSE',
}));
}
}, [buildUrlWithAuth, config.withCredentials, config.maxReconnectAttempts, config.reconnectInterval, processMessage]);
// Disconnect from SSE
const disconnect = useCallback(() => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
setState(prev => ({
...prev,
isConnected: false,
isConnecting: false,
}));
reconnectAttemptsRef.current = 0;
}, []);
// Reconnect to SSE
const reconnect = useCallback(() => {
if (urlRef.current) {
disconnect();
setTimeout(() => {
connect(urlRef.current!);
}, 100);
}
}, [connect, disconnect]);
// Clear messages buffer
const clearMessages = useCallback(() => {
setState(prev => ({ ...prev, messages: [] }));
}, []);
// Clear error
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
// Auto-connect on mount if URL provided
useEffect(() => {
if (initialUrl && config.autoConnect) {
connect(initialUrl);
}
return () => {
disconnect();
};
}, [initialUrl, config.autoConnect]);
// Cleanup on unmount
useEffect(() => {
return () => {
disconnect();
eventHandlersRef.current.clear();
};
}, [disconnect]);
// Auto-reconnect when auth token changes
useEffect(() => {
if (state.isConnected && urlRef.current) {
reconnect();
}
}, [storageService.getAuthData()?.access_token]);
return {
...state,
connect,
disconnect,
reconnect,
clearMessages,
clearError,
addEventListener,
};
};

View File

@@ -0,0 +1,507 @@
/**
* Sales hook for managing sales data, analytics, and reporting
*/
import { useState, useEffect, useCallback } from 'react';
import { salesService } from '../../services/api/sales.service';
import {
SalesData,
SalesDataCreate,
SalesDataUpdate,
SalesAnalytics,
OnboardingAnalysis,
WeatherCorrelation
} from '../../types/sales.types';
import { ApiResponse, PaginatedResponse, QueryParams } from '../../types/api.types';
interface SalesState {
salesData: SalesData[];
analytics: SalesAnalytics | null;
onboardingAnalysis: OnboardingAnalysis | null;
weatherCorrelation: WeatherCorrelation | null;
isLoading: boolean;
error: string | null;
pagination: {
total: number;
page: number;
pages: number;
limit: number;
};
}
interface SalesActions {
// Sales Data
fetchSalesData: (params?: QueryParams) => Promise<void>;
createSalesData: (data: SalesDataCreate) => Promise<boolean>;
updateSalesData: (id: string, data: SalesDataUpdate) => Promise<boolean>;
deleteSalesData: (id: string) => Promise<boolean>;
getSalesData: (id: string) => Promise<SalesData | null>;
bulkCreateSalesData: (data: SalesDataCreate[]) => Promise<boolean>;
// Analytics
fetchAnalytics: (startDate?: string, endDate?: string, filters?: any) => Promise<void>;
getRevenueAnalytics: (period: string, groupBy?: string) => Promise<any>;
getProductAnalytics: (startDate?: string, endDate?: string) => Promise<any>;
getCustomerAnalytics: (startDate?: string, endDate?: string) => Promise<any>;
getTimeAnalytics: (period: string) => Promise<any>;
// Reports
getDailyReport: (date: string) => Promise<any>;
getWeeklyReport: (startDate: string) => Promise<any>;
getMonthlyReport: (year: number, month: number) => Promise<any>;
getPerformanceReport: (startDate: string, endDate: string) => Promise<any>;
// Weather Correlation
fetchWeatherCorrelation: (startDate?: string, endDate?: string) => Promise<void>;
updateWeatherData: (startDate: string, endDate: string) => Promise<boolean>;
// Onboarding Analysis
fetchOnboardingAnalysis: () => Promise<void>;
generateOnboardingReport: () => Promise<any>;
// Import/Export
importSalesData: (file: File, format: string) => Promise<boolean>;
exportSalesData: (startDate: string, endDate: string, format: string) => Promise<boolean>;
getImportTemplates: () => Promise<any>;
// Utilities
clearError: () => void;
refresh: () => Promise<void>;
}
export const useSales = (): SalesState & SalesActions => {
const [state, setState] = useState<SalesState>({
salesData: [],
analytics: null,
onboardingAnalysis: null,
weatherCorrelation: null,
isLoading: false,
error: null,
pagination: {
total: 0,
page: 1,
pages: 1,
limit: 20,
},
});
// Fetch sales data
const fetchSalesData = useCallback(async (params?: QueryParams) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await salesService.getSalesData(params);
if (response.success && response.data) {
setState(prev => ({
...prev,
salesData: 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 datos de ventas',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [salesService]);
// Create sales data
const createSalesData = useCallback(async (data: SalesDataCreate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await salesService.createSalesData(data);
if (response.success) {
await fetchSalesData();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al crear dato de venta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [salesService, fetchSalesData]);
// Update sales data
const updateSalesData = useCallback(async (id: string, data: SalesDataUpdate): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await salesService.updateSalesData(id, data);
if (response.success) {
await fetchSalesData();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al actualizar dato de venta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [salesService, fetchSalesData]);
// Delete sales data
const deleteSalesData = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await salesService.deleteSalesData(id);
if (response.success) {
setState(prev => ({
...prev,
salesData: prev.salesData.filter(data => data.id !== id),
}));
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al eliminar dato de venta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [salesService]);
// Get single sales data
const getSalesData = useCallback(async (id: string): Promise<SalesData | null> => {
try {
const response = await salesService.getSalesDataById(id);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching sales data:', error);
return null;
}
}, [salesService]);
// Bulk create sales data
const bulkCreateSalesData = useCallback(async (data: SalesDataCreate[]): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await salesService.bulkCreateSalesData(data);
if (response.success) {
await fetchSalesData();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al crear datos de venta en lote',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [salesService, fetchSalesData]);
// Fetch analytics
const fetchAnalytics = useCallback(async (startDate?: string, endDate?: string, filters?: any) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await salesService.getAnalytics(startDate, endDate, filters);
if (response.success && response.data) {
setState(prev => ({
...prev,
analytics: response.data,
isLoading: false,
}));
} else {
setState(prev => ({
...prev,
isLoading: false,
error: response.error || 'Error al cargar analytics de ventas',
}));
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error de conexión al servidor',
}));
}
}, [salesService]);
// Get revenue analytics
const getRevenueAnalytics = useCallback(async (period: string, groupBy?: string) => {
try {
const response = await salesService.getRevenueAnalytics(period, groupBy);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching revenue analytics:', error);
return null;
}
}, [salesService]);
// Get product analytics
const getProductAnalytics = useCallback(async (startDate?: string, endDate?: string) => {
try {
const response = await salesService.getProductAnalytics(startDate, endDate);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching product analytics:', error);
return null;
}
}, [salesService]);
// Get customer analytics
const getCustomerAnalytics = useCallback(async (startDate?: string, endDate?: string) => {
try {
const response = await salesService.getCustomerAnalytics(startDate, endDate);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching customer analytics:', error);
return null;
}
}, [salesService]);
// Get time analytics
const getTimeAnalytics = useCallback(async (period: string) => {
try {
const response = await salesService.getTimeAnalytics(period);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching time analytics:', error);
return null;
}
}, [salesService]);
// Get daily report
const getDailyReport = useCallback(async (date: string) => {
try {
const response = await salesService.getDailyReport(date);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching daily report:', error);
return null;
}
}, [salesService]);
// Get weekly report
const getWeeklyReport = useCallback(async (startDate: string) => {
try {
const response = await salesService.getWeeklyReport(startDate);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching weekly report:', error);
return null;
}
}, [salesService]);
// Get monthly report
const getMonthlyReport = useCallback(async (year: number, month: number) => {
try {
const response = await salesService.getMonthlyReport(year, month);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching monthly report:', error);
return null;
}
}, [salesService]);
// Get performance report
const getPerformanceReport = useCallback(async (startDate: string, endDate: string) => {
try {
const response = await salesService.getPerformanceReport(startDate, endDate);
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching performance report:', error);
return null;
}
}, [salesService]);
// Fetch weather correlation
const fetchWeatherCorrelation = useCallback(async (startDate?: string, endDate?: string) => {
try {
const response = await salesService.getWeatherCorrelation(startDate, endDate);
if (response.success && response.data) {
setState(prev => ({
...prev,
weatherCorrelation: response.data,
}));
}
} catch (error) {
console.error('Error fetching weather correlation:', error);
}
}, [salesService]);
// Update weather data
const updateWeatherData = useCallback(async (startDate: string, endDate: string): Promise<boolean> => {
try {
const response = await salesService.updateWeatherData(startDate, endDate);
if (response.success) {
await fetchWeatherCorrelation(startDate, endDate);
return true;
}
return false;
} catch (error) {
console.error('Error updating weather data:', error);
return false;
}
}, [salesService, fetchWeatherCorrelation]);
// Fetch onboarding analysis
const fetchOnboardingAnalysis = useCallback(async () => {
try {
const response = await salesService.getOnboardingAnalysis();
if (response.success && response.data) {
setState(prev => ({
...prev,
onboardingAnalysis: response.data,
}));
}
} catch (error) {
console.error('Error fetching onboarding analysis:', error);
}
}, [salesService]);
// Generate onboarding report
const generateOnboardingReport = useCallback(async () => {
try {
const response = await salesService.generateOnboardingReport();
return response.success ? response.data : null;
} catch (error) {
console.error('Error generating onboarding report:', error);
return null;
}
}, [salesService]);
// Import sales data
const importSalesData = useCallback(async (file: File, format: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const response = await salesService.importSalesData(file, format);
if (response.success) {
await fetchSalesData();
return true;
} else {
setState(prev => ({
...prev,
error: response.error || 'Error al importar datos de venta',
}));
return false;
}
} catch (error) {
setState(prev => ({
...prev,
error: 'Error de conexión al servidor',
}));
return false;
}
}, [salesService, fetchSalesData]);
// Export sales data
const exportSalesData = useCallback(async (startDate: string, endDate: string, format: string): Promise<boolean> => {
try {
const response = await salesService.exportSalesData(startDate, endDate, format);
return response.success;
} catch (error) {
console.error('Error exporting sales data:', error);
return false;
}
}, [salesService]);
// Get import templates
const getImportTemplates = useCallback(async () => {
try {
const response = await salesService.getImportTemplates();
return response.success ? response.data : null;
} catch (error) {
console.error('Error fetching import templates:', error);
return null;
}
}, [salesService]);
// Clear error
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
// Refresh all data
const refresh = useCallback(async () => {
await Promise.all([
fetchSalesData(),
fetchAnalytics(),
fetchOnboardingAnalysis(),
]);
}, [fetchSalesData, fetchAnalytics, fetchOnboardingAnalysis]);
// Initialize data on mount
useEffect(() => {
refresh();
}, []);
return {
...state,
fetchSalesData,
createSalesData,
updateSalesData,
deleteSalesData,
getSalesData,
bulkCreateSalesData,
fetchAnalytics,
getRevenueAnalytics,
getProductAnalytics,
getCustomerAnalytics,
getTimeAnalytics,
getDailyReport,
getWeeklyReport,
getMonthlyReport,
getPerformanceReport,
fetchWeatherCorrelation,
updateWeatherData,
fetchOnboardingAnalysis,
generateOnboardingReport,
importSalesData,
exportSalesData,
getImportTemplates,
clearError,
refresh,
};
};

View File

@@ -0,0 +1,423 @@
/**
* WebSocket hook for real-time bidirectional communication
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import { StorageService } from '../../services/api/utils/storage.service';
export interface WebSocketMessage {
id?: string;
type: string;
data: any;
timestamp: number;
}
interface WebSocketState {
isConnected: boolean;
isConnecting: boolean;
error: string | null;
messages: WebSocketMessage[];
readyState: number;
}
interface WebSocketOptions {
protocols?: string | string[];
reconnectInterval?: number;
maxReconnectAttempts?: number;
bufferSize?: number;
autoConnect?: boolean;
heartbeatInterval?: number;
messageFilters?: string[];
}
interface WebSocketActions {
connect: (url: string) => void;
disconnect: () => void;
reconnect: () => void;
send: (data: any, type?: string) => boolean;
sendMessage: (message: WebSocketMessage) => boolean;
clearMessages: () => void;
clearError: () => void;
addEventListener: (messageType: string, handler: (data: any) => void) => () => void;
}
const DEFAULT_OPTIONS: Required<WebSocketOptions> = {
protocols: [],
reconnectInterval: 3000,
maxReconnectAttempts: 10,
bufferSize: 100,
autoConnect: true,
heartbeatInterval: 30000,
messageFilters: [],
};
export const useWebSocket = (
initialUrl?: string,
options: WebSocketOptions = {}
): WebSocketState & WebSocketActions => {
const [state, setState] = useState<WebSocketState>({
isConnected: false,
isConnecting: false,
error: null,
messages: [],
readyState: WebSocket.CLOSED,
});
const webSocketRef = useRef<WebSocket | null>(null);
const urlRef = useRef<string | null>(initialUrl || null);
const reconnectTimeoutRef = useRef<number | null>(null);
const heartbeatTimeoutRef = useRef<number | null>(null);
const reconnectAttemptsRef = useRef<number>(0);
const messageHandlersRef = useRef<Map<string, Set<(data: any) => void>>>(new Map());
const messageQueueRef = useRef<WebSocketMessage[]>([]);
const config = { ...DEFAULT_OPTIONS, ...options };
const storageService = new StorageService();
// Helper function to get auth token
const getAuthToken = useCallback(() => {
const authData = storageService.getAuthData();
return authData?.access_token || null;
}, [storageService]);
// Add event listener for specific message types
const addEventListener = useCallback((messageType: string, handler: (data: any) => void) => {
if (!messageHandlersRef.current.has(messageType)) {
messageHandlersRef.current.set(messageType, new Set());
}
messageHandlersRef.current.get(messageType)!.add(handler);
// Return cleanup function
return () => {
const handlers = messageHandlersRef.current.get(messageType);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
messageHandlersRef.current.delete(messageType);
}
}
};
}, []);
// Process incoming message
const processMessage = useCallback((event: MessageEvent) => {
try {
let messageData: WebSocketMessage;
try {
const parsed = JSON.parse(event.data);
messageData = {
id: parsed.id,
type: parsed.type || 'message',
data: parsed.data || parsed,
timestamp: parsed.timestamp || Date.now(),
};
} catch {
messageData = {
type: 'message',
data: event.data,
timestamp: Date.now(),
};
}
// Filter messages if messageFilters is specified
if (config.messageFilters.length > 0 && !config.messageFilters.includes(messageData.type)) {
return;
}
setState(prev => ({
...prev,
messages: [...prev.messages.slice(-(config.bufferSize - 1)), messageData],
}));
// Call registered message handlers
const handlers = messageHandlersRef.current.get(messageData.type);
if (handlers) {
handlers.forEach(handler => {
try {
handler(messageData.data);
} catch (error) {
console.error('Error in WebSocket message handler:', error);
}
});
}
// Call generic message handlers
const messageHandlers = messageHandlersRef.current.get('message');
if (messageHandlers && messageData.type !== 'message') {
messageHandlers.forEach(handler => {
try {
handler(messageData);
} catch (error) {
console.error('Error in WebSocket message handler:', error);
}
});
}
} catch (error) {
console.error('Error processing WebSocket message:', error);
}
}, [config.messageFilters, config.bufferSize]);
// Send heartbeat/ping message
const sendHeartbeat = useCallback(() => {
if (webSocketRef.current && webSocketRef.current.readyState === WebSocket.OPEN) {
const heartbeatMessage: WebSocketMessage = {
type: 'ping',
data: { timestamp: Date.now() },
timestamp: Date.now(),
};
webSocketRef.current.send(JSON.stringify(heartbeatMessage));
}
}, []);
// Setup heartbeat interval
const setupHeartbeat = useCallback(() => {
if (config.heartbeatInterval > 0) {
heartbeatTimeoutRef.current = window.setInterval(sendHeartbeat, config.heartbeatInterval);
}
}, [config.heartbeatInterval, sendHeartbeat]);
// Clear heartbeat interval
const clearHeartbeat = useCallback(() => {
if (heartbeatTimeoutRef.current) {
window.clearInterval(heartbeatTimeoutRef.current);
heartbeatTimeoutRef.current = null;
}
}, []);
// Process queued messages
const processMessageQueue = useCallback(() => {
while (messageQueueRef.current.length > 0) {
const message = messageQueueRef.current.shift();
if (message && webSocketRef.current && webSocketRef.current.readyState === WebSocket.OPEN) {
webSocketRef.current.send(JSON.stringify(message));
}
}
}, []);
// Connect to WebSocket endpoint
const connect = useCallback((url: string) => {
if (webSocketRef.current) {
disconnect();
}
urlRef.current = url;
setState(prev => ({ ...prev, isConnecting: true, error: null }));
try {
// Build URL with auth token if available
const wsUrl = new URL(url);
const authToken = getAuthToken();
if (authToken) {
wsUrl.searchParams.set('token', authToken);
}
const webSocket = new WebSocket(
wsUrl.toString(),
Array.isArray(config.protocols) ? config.protocols : [config.protocols].filter(Boolean)
);
webSocketRef.current = webSocket;
webSocket.onopen = () => {
setState(prev => ({
...prev,
isConnected: true,
isConnecting: false,
error: null,
readyState: WebSocket.OPEN,
}));
reconnectAttemptsRef.current = 0;
// Clear reconnect timeout if it exists
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
// Setup heartbeat
setupHeartbeat();
// Process any queued messages
processMessageQueue();
};
webSocket.onmessage = processMessage;
webSocket.onclose = (event) => {
setState(prev => ({
...prev,
isConnected: false,
isConnecting: false,
readyState: WebSocket.CLOSED,
}));
clearHeartbeat();
// Attempt reconnection if not a clean close and within limits
if (!event.wasClean && reconnectAttemptsRef.current < config.maxReconnectAttempts) {
reconnectAttemptsRef.current += 1;
setState(prev => ({
...prev,
error: `Conexión perdida. Reintentando... (${reconnectAttemptsRef.current}/${config.maxReconnectAttempts})`,
}));
reconnectTimeoutRef.current = window.setTimeout(() => {
if (urlRef.current) {
connect(urlRef.current);
}
}, config.reconnectInterval);
} else if (reconnectAttemptsRef.current >= config.maxReconnectAttempts) {
setState(prev => ({
...prev,
error: `Máximo número de intentos de reconexión alcanzado (${config.maxReconnectAttempts})`,
}));
}
};
webSocket.onerror = (error) => {
console.error('WebSocket error:', error);
setState(prev => ({
...prev,
error: 'Error de conexión WebSocket',
readyState: webSocket.readyState,
}));
};
// Update ready state periodically
const stateInterval = setInterval(() => {
if (webSocketRef.current) {
setState(prev => ({
...prev,
readyState: webSocketRef.current!.readyState,
}));
} else {
clearInterval(stateInterval);
}
}, 1000);
} catch (error) {
setState(prev => ({
...prev,
isConnecting: false,
error: 'Error al establecer conexión WebSocket',
readyState: WebSocket.CLOSED,
}));
}
}, [getAuthToken, config.protocols, config.maxReconnectAttempts, config.reconnectInterval, processMessage, setupHeartbeat, clearHeartbeat, processMessageQueue]);
// Disconnect from WebSocket
const disconnect = useCallback(() => {
if (webSocketRef.current) {
webSocketRef.current.close(1000, 'Manual disconnect');
webSocketRef.current = null;
}
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
clearHeartbeat();
setState(prev => ({
...prev,
isConnected: false,
isConnecting: false,
readyState: WebSocket.CLOSED,
}));
reconnectAttemptsRef.current = 0;
}, [clearHeartbeat]);
// Reconnect to WebSocket
const reconnect = useCallback(() => {
if (urlRef.current) {
disconnect();
setTimeout(() => {
connect(urlRef.current!);
}, 100);
}
}, [connect, disconnect]);
// Send raw data
const send = useCallback((data: any, type: string = 'message'): boolean => {
const message: WebSocketMessage = {
type,
data,
timestamp: Date.now(),
};
return sendMessage(message);
}, []);
// Send structured message
const sendMessage = useCallback((message: WebSocketMessage): boolean => {
if (webSocketRef.current && webSocketRef.current.readyState === WebSocket.OPEN) {
try {
webSocketRef.current.send(JSON.stringify(message));
return true;
} catch (error) {
console.error('Error sending WebSocket message:', error);
return false;
}
} else {
// Queue message for later if not connected
messageQueueRef.current.push(message);
return false;
}
}, []);
// Clear messages buffer
const clearMessages = useCallback(() => {
setState(prev => ({ ...prev, messages: [] }));
}, []);
// Clear error
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
// Auto-connect on mount if URL provided
useEffect(() => {
if (initialUrl && config.autoConnect) {
connect(initialUrl);
}
return () => {
disconnect();
};
}, [initialUrl, config.autoConnect]);
// Cleanup on unmount
useEffect(() => {
return () => {
disconnect();
messageHandlersRef.current.clear();
messageQueueRef.current = [];
};
}, [disconnect]);
// Auto-reconnect when auth token changes
useEffect(() => {
if (state.isConnected && urlRef.current) {
reconnect();
}
}, [getAuthToken()]);
return {
...state,
connect,
disconnect,
reconnect,
send,
sendMessage,
clearMessages,
clearError,
addEventListener,
};
};