ADD new frontend
This commit is contained in:
321
frontend/src/hooks/api/useAuth.ts
Normal file
321
frontend/src/hooks/api/useAuth.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
568
frontend/src/hooks/api/useForecasting.ts
Normal file
568
frontend/src/hooks/api/useForecasting.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
475
frontend/src/hooks/api/useInventory.ts
Normal file
475
frontend/src/hooks/api/useInventory.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
650
frontend/src/hooks/api/useProduction.ts
Normal file
650
frontend/src/hooks/api/useProduction.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
329
frontend/src/hooks/api/useSSE.ts
Normal file
329
frontend/src/hooks/api/useSSE.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
507
frontend/src/hooks/api/useSales.ts
Normal file
507
frontend/src/hooks/api/useSales.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
423
frontend/src/hooks/api/useWebSocket.ts
Normal file
423
frontend/src/hooks/api/useWebSocket.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user