Start integrating the onboarding flow with backend 6

This commit is contained in:
Urtzi Alfaro
2025-09-05 17:49:48 +02:00
parent 236c3a32ae
commit 069954981a
131 changed files with 5217 additions and 22838 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,621 +0,0 @@
/**
* Alerts hook for managing bakery alerts and notifications
*/
import { useState, useEffect, useCallback } from 'react';
import { InventoryService } from '../../services/api/inventory.service';
import { ProductionService } from '../../services/api/production.service';
import { NotificationService } from '../../services/api/notification.service';
export type AlertType = 'inventory' | 'production' | 'quality' | 'maintenance' | 'safety' | 'system' | 'custom';
export type AlertPriority = 'low' | 'medium' | 'high' | 'critical';
export type AlertStatus = 'active' | 'acknowledged' | 'resolved' | 'dismissed';
export interface Alert {
id: string;
type: AlertType;
priority: AlertPriority;
status: AlertStatus;
title: string;
message: string;
details?: Record<string, any>;
source: string;
sourceId?: string;
createdAt: Date;
updatedAt?: Date;
acknowledgedAt?: Date;
acknowledgedBy?: string;
resolvedAt?: Date;
resolvedBy?: string;
dismissedAt?: Date;
dismissedBy?: string;
expiresAt?: Date;
actions?: {
id: string;
label: string;
action: string;
parameters?: Record<string, any>;
}[];
metadata?: Record<string, any>;
}
export interface AlertRule {
id: string;
name: string;
type: AlertType;
priority: AlertPriority;
condition: {
field: string;
operator: 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'greater_or_equal' | 'less_or_equal' | 'contains' | 'not_contains';
value: any;
}[];
actions: string[];
enabled: boolean;
cooldownPeriod?: number; // in minutes
lastTriggered?: Date;
}
interface AlertsState {
alerts: Alert[];
rules: AlertRule[];
unreadCount: number;
criticalCount: number;
isLoading: boolean;
error: string | null;
filters: {
types: AlertType[];
priorities: AlertPriority[];
statuses: AlertStatus[];
sources: string[];
};
}
interface AlertsActions {
// Alert Management
fetchAlerts: (filters?: Partial<AlertsState['filters']>) => Promise<void>;
acknowledgeAlert: (id: string) => Promise<boolean>;
resolveAlert: (id: string, resolution?: string) => Promise<boolean>;
dismissAlert: (id: string, reason?: string) => Promise<boolean>;
bulkAcknowledge: (ids: string[]) => Promise<boolean>;
bulkResolve: (ids: string[], resolution?: string) => Promise<boolean>;
bulkDismiss: (ids: string[], reason?: string) => Promise<boolean>;
// Alert Creation
createAlert: (alert: Omit<Alert, 'id' | 'createdAt' | 'status'>) => Promise<boolean>;
createCustomAlert: (title: string, message: string, priority?: AlertPriority, details?: Record<string, any>) => Promise<boolean>;
// Alert Rules
fetchAlertRules: () => Promise<void>;
createAlertRule: (rule: Omit<AlertRule, 'id'>) => Promise<boolean>;
updateAlertRule: (id: string, rule: Partial<AlertRule>) => Promise<boolean>;
deleteAlertRule: (id: string) => Promise<boolean>;
testAlertRule: (rule: AlertRule) => Promise<boolean>;
// Monitoring and Checks
checkInventoryAlerts: () => Promise<void>;
checkProductionAlerts: () => Promise<void>;
checkQualityAlerts: () => Promise<void>;
checkMaintenanceAlerts: () => Promise<void>;
checkSystemAlerts: () => Promise<void>;
// Analytics
getAlertAnalytics: (period: string) => Promise<any>;
getAlertTrends: (days: number) => Promise<any>;
getMostFrequentAlerts: (period: string) => Promise<any>;
// Filters and Search
setFilters: (filters: Partial<AlertsState['filters']>) => void;
searchAlerts: (query: string) => Promise<Alert[]>;
// Real-time Updates
subscribeToAlerts: (callback: (alert: Alert) => void) => () => void;
// Utilities
clearError: () => void;
refresh: () => Promise<void>;
}
export const useAlerts = (): AlertsState & AlertsActions => {
const [state, setState] = useState<AlertsState>({
alerts: [],
rules: [],
unreadCount: 0,
criticalCount: 0,
isLoading: false,
error: null,
filters: {
types: [],
priorities: [],
statuses: [],
sources: [],
},
});
const inventoryService = new InventoryService();
const productionService = new ProductionService();
const notificationService = new NotificationService();
// Fetch alerts
const fetchAlerts = useCallback(async (filters?: Partial<AlertsState['filters']>) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Combine filters
const activeFilters = { ...state.filters, ...filters };
// Fetch alerts from different sources
const [inventoryAlerts, productionAlerts, systemAlerts] = await Promise.all([
getInventoryAlerts(activeFilters),
getProductionAlerts(activeFilters),
getSystemAlerts(activeFilters),
]);
const allAlerts = [...inventoryAlerts, ...productionAlerts, ...systemAlerts]
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
// Calculate counts
const unreadCount = allAlerts.filter(alert => alert.status === 'active').length;
const criticalCount = allAlerts.filter(alert => alert.priority === 'critical' && alert.status === 'active').length;
setState(prev => ({
...prev,
alerts: allAlerts,
unreadCount,
criticalCount,
isLoading: false,
filters: activeFilters,
}));
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al cargar alertas',
}));
}
}, [state.filters]);
// Get inventory alerts
const getInventoryAlerts = async (filters: Partial<AlertsState['filters']>): Promise<Alert[]> => {
try {
const response = await inventoryService.getAlerts();
if (response.success && response.data) {
return response.data.map((alert: any) => convertToAlert(alert, 'inventory'));
}
} catch (error) {
console.error('Error fetching inventory alerts:', error);
}
return [];
};
// Get production alerts
const getProductionAlerts = async (filters: Partial<AlertsState['filters']>): Promise<Alert[]> => {
try {
const response = await productionService.getAlerts?.();
if (response?.success && response.data) {
return response.data.map((alert: any) => convertToAlert(alert, 'production'));
}
} catch (error) {
console.error('Error fetching production alerts:', error);
}
return [];
};
// Get system alerts
const getSystemAlerts = async (filters: Partial<AlertsState['filters']>): Promise<Alert[]> => {
try {
const response = await notificationService.getNotifications();
if (response.success && response.data) {
return response.data
.filter((notif: any) => notif.type === 'alert')
.map((alert: any) => convertToAlert(alert, 'system'));
}
} catch (error) {
console.error('Error fetching system alerts:', error);
}
return [];
};
// Convert API response to Alert format
const convertToAlert = (apiAlert: any, source: string): Alert => {
return {
id: apiAlert.id,
type: apiAlert.type || (source as AlertType),
priority: mapPriority(apiAlert.priority || apiAlert.severity),
status: mapStatus(apiAlert.status),
title: apiAlert.title || apiAlert.message?.substring(0, 50) || 'Alert',
message: apiAlert.message || apiAlert.description || '',
details: apiAlert.details || apiAlert.data,
source,
sourceId: apiAlert.source_id,
createdAt: new Date(apiAlert.created_at || Date.now()),
updatedAt: apiAlert.updated_at ? new Date(apiAlert.updated_at) : undefined,
acknowledgedAt: apiAlert.acknowledged_at ? new Date(apiAlert.acknowledged_at) : undefined,
acknowledgedBy: apiAlert.acknowledged_by,
resolvedAt: apiAlert.resolved_at ? new Date(apiAlert.resolved_at) : undefined,
resolvedBy: apiAlert.resolved_by,
dismissedAt: apiAlert.dismissed_at ? new Date(apiAlert.dismissed_at) : undefined,
dismissedBy: apiAlert.dismissed_by,
expiresAt: apiAlert.expires_at ? new Date(apiAlert.expires_at) : undefined,
actions: apiAlert.actions || [],
metadata: apiAlert.metadata || {},
};
};
// Map priority from different sources
const mapPriority = (priority: string): AlertPriority => {
const priorityMap: Record<string, AlertPriority> = {
'low': 'low',
'medium': 'medium',
'high': 'high',
'critical': 'critical',
'urgent': 'critical',
'warning': 'medium',
'error': 'high',
'info': 'low',
};
return priorityMap[priority?.toLowerCase()] || 'medium';
};
// Map status from different sources
const mapStatus = (status: string): AlertStatus => {
const statusMap: Record<string, AlertStatus> = {
'active': 'active',
'new': 'active',
'open': 'active',
'acknowledged': 'acknowledged',
'ack': 'acknowledged',
'resolved': 'resolved',
'closed': 'resolved',
'dismissed': 'dismissed',
'ignored': 'dismissed',
};
return statusMap[status?.toLowerCase()] || 'active';
};
// Acknowledge alert
const acknowledgeAlert = useCallback(async (id: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const alert = state.alerts.find(a => a.id === id);
if (!alert) return false;
// Call appropriate service based on source
let success = false;
if (alert.source === 'inventory') {
const response = await inventoryService.markAlertAsRead(id);
success = response.success;
}
if (success) {
setState(prev => ({
...prev,
alerts: prev.alerts.map(a =>
a.id === id
? { ...a, status: 'acknowledged', acknowledgedAt: new Date() }
: a
),
unreadCount: Math.max(0, prev.unreadCount - 1),
}));
}
return success;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al confirmar alerta' }));
return false;
}
}, [state.alerts, inventoryService]);
// Resolve alert
const resolveAlert = useCallback(async (id: string, resolution?: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const alert = state.alerts.find(a => a.id === id);
if (!alert) return false;
// Call appropriate service based on source
let success = false;
if (alert.source === 'inventory') {
const response = await inventoryService.dismissAlert(id);
success = response.success;
}
if (success) {
setState(prev => ({
...prev,
alerts: prev.alerts.map(a =>
a.id === id
? { ...a, status: 'resolved', resolvedAt: new Date(), metadata: { ...a.metadata, resolution } }
: a
),
unreadCount: a.status === 'active' ? Math.max(0, prev.unreadCount - 1) : prev.unreadCount,
criticalCount: a.priority === 'critical' && a.status === 'active' ? Math.max(0, prev.criticalCount - 1) : prev.criticalCount,
}));
}
return success;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al resolver alerta' }));
return false;
}
}, [state.alerts, inventoryService]);
// Dismiss alert
const dismissAlert = useCallback(async (id: string, reason?: string): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const alert = state.alerts.find(a => a.id === id);
if (!alert) return false;
// Call appropriate service based on source
let success = false;
if (alert.source === 'inventory') {
const response = await inventoryService.dismissAlert(id);
success = response.success;
}
if (success) {
setState(prev => ({
...prev,
alerts: prev.alerts.map(a =>
a.id === id
? { ...a, status: 'dismissed', dismissedAt: new Date(), metadata: { ...a.metadata, dismissReason: reason } }
: a
),
}));
}
return success;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al descartar alerta' }));
return false;
}
}, [state.alerts, inventoryService]);
// Bulk acknowledge
const bulkAcknowledge = useCallback(async (ids: string[]): Promise<boolean> => {
const results = await Promise.all(ids.map(id => acknowledgeAlert(id)));
return results.every(result => result);
}, [acknowledgeAlert]);
// Bulk resolve
const bulkResolve = useCallback(async (ids: string[], resolution?: string): Promise<boolean> => {
const results = await Promise.all(ids.map(id => resolveAlert(id, resolution)));
return results.every(result => result);
}, [resolveAlert]);
// Bulk dismiss
const bulkDismiss = useCallback(async (ids: string[], reason?: string): Promise<boolean> => {
const results = await Promise.all(ids.map(id => dismissAlert(id, reason)));
return results.every(result => result);
}, [dismissAlert]);
// Create alert
const createAlert = useCallback(async (alert: Omit<Alert, 'id' | 'createdAt' | 'status'>): Promise<boolean> => {
setState(prev => ({ ...prev, error: null }));
try {
const newAlert: Alert = {
...alert,
id: generateAlertId(),
status: 'active',
createdAt: new Date(),
};
setState(prev => ({
...prev,
alerts: [newAlert, ...prev.alerts],
unreadCount: prev.unreadCount + 1,
criticalCount: newAlert.priority === 'critical' ? prev.criticalCount + 1 : prev.criticalCount,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al crear alerta' }));
return false;
}
}, []);
// Create custom alert
const createCustomAlert = useCallback(async (
title: string,
message: string,
priority: AlertPriority = 'medium',
details?: Record<string, any>
): Promise<boolean> => {
return createAlert({
type: 'custom',
priority,
title,
message,
details,
source: 'user',
});
}, [createAlert]);
// Check inventory alerts
const checkInventoryAlerts = useCallback(async () => {
try {
// Check for low stock
const stockResponse = await inventoryService.getStockLevels();
if (stockResponse.success && stockResponse.data) {
const lowStockItems = stockResponse.data.filter((item: any) =>
item.current_quantity <= item.reorder_point
);
for (const item of lowStockItems) {
await createAlert({
type: 'inventory',
priority: item.current_quantity === 0 ? 'critical' : 'high',
title: 'Stock bajo',
message: `Stock bajo para ${item.ingredient?.name}: ${item.current_quantity} ${item.unit}`,
source: 'inventory',
sourceId: item.id,
details: { item },
});
}
}
// Check for expiring items
const expirationResponse = await inventoryService.getExpirationReport();
if (expirationResponse?.success && expirationResponse.data) {
const expiringItems = expirationResponse.data.expiring_soon || [];
for (const item of expiringItems) {
await createAlert({
type: 'inventory',
priority: 'medium',
title: 'Producto próximo a expirar',
message: `${item.name} expira el ${item.expiration_date}`,
source: 'inventory',
sourceId: item.id,
details: { item },
});
}
}
} catch (error) {
console.error('Error checking inventory alerts:', error);
}
}, [inventoryService, createAlert]);
// Generate unique ID
const generateAlertId = (): string => {
return `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
};
// Set filters
const setFilters = useCallback((filters: Partial<AlertsState['filters']>) => {
setState(prev => ({
...prev,
filters: { ...prev.filters, ...filters },
}));
}, []);
// Clear error
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
// Refresh alerts
const refresh = useCallback(async () => {
await fetchAlerts(state.filters);
}, [fetchAlerts, state.filters]);
// Placeholder implementations for remaining functions
const fetchAlertRules = useCallback(async () => {
setState(prev => ({ ...prev, rules: [] }));
}, []);
const createAlertRule = useCallback(async (rule: Omit<AlertRule, 'id'>): Promise<boolean> => {
return true;
}, []);
const updateAlertRule = useCallback(async (id: string, rule: Partial<AlertRule>): Promise<boolean> => {
return true;
}, []);
const deleteAlertRule = useCallback(async (id: string): Promise<boolean> => {
return true;
}, []);
const testAlertRule = useCallback(async (rule: AlertRule): Promise<boolean> => {
return true;
}, []);
const checkProductionAlerts = useCallback(async () => {
// Implementation for production alerts
}, []);
const checkQualityAlerts = useCallback(async () => {
// Implementation for quality alerts
}, []);
const checkMaintenanceAlerts = useCallback(async () => {
// Implementation for maintenance alerts
}, []);
const checkSystemAlerts = useCallback(async () => {
// Implementation for system alerts
}, []);
const getAlertAnalytics = useCallback(async (period: string): Promise<any> => {
return {};
}, []);
const getAlertTrends = useCallback(async (days: number): Promise<any> => {
return {};
}, []);
const getMostFrequentAlerts = useCallback(async (period: string): Promise<any> => {
return {};
}, []);
const searchAlerts = useCallback(async (query: string): Promise<Alert[]> => {
return state.alerts.filter(alert =>
alert.title.toLowerCase().includes(query.toLowerCase()) ||
alert.message.toLowerCase().includes(query.toLowerCase())
);
}, [state.alerts]);
const subscribeToAlerts = useCallback((callback: (alert: Alert) => void): (() => void) => {
// Implementation would set up real-time subscription
return () => {
// Cleanup subscription
};
}, []);
// Initialize alerts on mount
useEffect(() => {
fetchAlerts();
}, []);
// Set up periodic checks
useEffect(() => {
const interval = setInterval(() => {
checkInventoryAlerts();
}, 5 * 60 * 1000); // Check every 5 minutes
return () => clearInterval(interval);
}, [checkInventoryAlerts]);
return {
...state,
fetchAlerts,
acknowledgeAlert,
resolveAlert,
dismissAlert,
bulkAcknowledge,
bulkResolve,
bulkDismiss,
createAlert,
createCustomAlert,
fetchAlertRules,
createAlertRule,
updateAlertRule,
deleteAlertRule,
testAlertRule,
checkInventoryAlerts,
checkProductionAlerts,
checkQualityAlerts,
checkMaintenanceAlerts,
checkSystemAlerts,
getAlertAnalytics,
getAlertTrends,
getMostFrequentAlerts,
setFilters,
searchAlerts,
subscribeToAlerts,
clearError,
refresh,
};
};

View File

@@ -1,520 +0,0 @@
/**
* Bakery workflow hook for managing daily bakery operations and processes
*/
import { useState, useEffect, useCallback } from 'react';
import { ProductionService } from '../../services/api/production.service';
import { InventoryService } from '../../services/api/inventory.service';
import { SalesService } from '../../services/api/sales.service';
import { OrderService } from '../../services/api/order.service';
export interface WorkflowStep {
id: string;
name: string;
description: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
priority: 'low' | 'medium' | 'high' | 'urgent';
estimatedDuration: number; // in minutes
actualDuration?: number;
dependencies?: string[];
assignedTo?: string;
startTime?: Date;
endTime?: Date;
notes?: string;
}
export interface DailyWorkflow {
date: string;
steps: WorkflowStep[];
totalEstimatedTime: number;
totalActualTime: number;
completionRate: number;
status: 'not_started' | 'in_progress' | 'completed' | 'behind_schedule';
}
interface BakeryWorkflowState {
currentWorkflow: DailyWorkflow | null;
workflowHistory: DailyWorkflow[];
activeStep: WorkflowStep | null;
isLoading: boolean;
error: string | null;
}
interface BakeryWorkflowActions {
// Workflow Management
initializeDailyWorkflow: (date: string) => Promise<void>;
generateWorkflowFromForecast: (date: string) => Promise<void>;
updateWorkflow: (workflow: Partial<DailyWorkflow>) => Promise<boolean>;
// Step Management
startStep: (stepId: string) => Promise<boolean>;
completeStep: (stepId: string, notes?: string) => Promise<boolean>;
failStep: (stepId: string, reason: string) => Promise<boolean>;
skipStep: (stepId: string, reason: string) => Promise<boolean>;
updateStepProgress: (stepId: string, progress: Partial<WorkflowStep>) => Promise<boolean>;
// Workflow Templates
createWorkflowTemplate: (name: string, steps: Omit<WorkflowStep, 'id' | 'status'>[]) => Promise<boolean>;
loadWorkflowTemplate: (templateId: string, date: string) => Promise<boolean>;
getWorkflowTemplates: () => Promise<any[]>;
// Analytics and Optimization
getWorkflowAnalytics: (startDate: string, endDate: string) => Promise<any>;
getBottleneckAnalysis: (period: string) => Promise<any>;
getEfficiencyReport: (date: string) => Promise<any>;
// Real-time Updates
subscribeToWorkflowUpdates: (callback: (workflow: DailyWorkflow) => void) => () => void;
// Utilities
clearError: () => void;
refresh: () => Promise<void>;
}
export const useBakeryWorkflow = (): BakeryWorkflowState & BakeryWorkflowActions => {
const [state, setState] = useState<BakeryWorkflowState>({
currentWorkflow: null,
workflowHistory: [],
activeStep: null,
isLoading: false,
error: null,
});
const productionService = new ProductionService();
const inventoryService = new InventoryService();
const salesService = new SalesService();
const orderService = new OrderService();
// Initialize daily workflow
const initializeDailyWorkflow = useCallback(async (date: string) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Get existing workflow or create new one
const existingWorkflow = await getWorkflowForDate(date);
if (existingWorkflow) {
setState(prev => ({
...prev,
currentWorkflow: existingWorkflow,
activeStep: findActiveStep(existingWorkflow.steps),
isLoading: false,
}));
} else {
await generateWorkflowFromForecast(date);
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al inicializar flujo de trabajo diario',
}));
}
}, []);
// Generate workflow from forecast and orders
const generateWorkflowFromForecast = useCallback(async (date: string) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Get daily forecast and orders
const [forecastData, ordersData, inventoryData] = await Promise.all([
productionService.getDailyForecast?.(date),
orderService.getOrdersByDate?.(date),
inventoryService.getStockLevels(),
]);
const workflow = generateWorkflowSteps(forecastData, ordersData, inventoryData);
setState(prev => ({
...prev,
currentWorkflow: workflow,
activeStep: workflow.steps.find(step => step.status === 'pending') || null,
isLoading: false,
}));
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al generar flujo de trabajo desde predicción',
}));
}
}, [productionService, orderService, inventoryService]);
// Generate workflow steps based on data
const generateWorkflowSteps = (forecastData: any, ordersData: any, inventoryData: any): DailyWorkflow => {
const steps: WorkflowStep[] = [
// Morning prep
{
id: 'morning_prep',
name: 'Preparación matutina',
description: 'Verificar equipos, ingredientes y planificación del día',
status: 'pending',
priority: 'high',
estimatedDuration: 30,
},
// Inventory check
{
id: 'inventory_check',
name: 'Control de inventario',
description: 'Verificar niveles de stock y calidad de ingredientes',
status: 'pending',
priority: 'high',
estimatedDuration: 20,
dependencies: ['morning_prep'],
},
// Production preparation
{
id: 'production_prep',
name: 'Preparación de producción',
description: 'Preparar ingredientes y configurar equipos',
status: 'pending',
priority: 'high',
estimatedDuration: 45,
dependencies: ['inventory_check'],
},
// Production batches (generated from forecast)
...generateProductionSteps(forecastData),
// Quality control
{
id: 'quality_control',
name: 'Control de calidad',
description: 'Verificar calidad de productos terminados',
status: 'pending',
priority: 'medium',
estimatedDuration: 30,
dependencies: ['production_prep'],
},
// Order fulfillment
...generateOrderSteps(ordersData),
// End of day cleanup
{
id: 'cleanup',
name: 'Limpieza final',
description: 'Limpieza de equipos y área de trabajo',
status: 'pending',
priority: 'medium',
estimatedDuration: 45,
},
// Daily reporting
{
id: 'daily_report',
name: 'Reporte diario',
description: 'Completar reportes de producción y ventas',
status: 'pending',
priority: 'low',
estimatedDuration: 15,
dependencies: ['cleanup'],
},
];
const totalEstimatedTime = steps.reduce((total, step) => total + step.estimatedDuration, 0);
return {
date: new Date().toISOString().split('T')[0],
steps,
totalEstimatedTime,
totalActualTime: 0,
completionRate: 0,
status: 'not_started',
};
};
// Generate production steps from forecast
const generateProductionSteps = (forecastData: any): WorkflowStep[] => {
if (!forecastData || !forecastData.products) return [];
return forecastData.products.map((product: any, index: number) => ({
id: `production_${product.id}`,
name: `Producir ${product.name}`,
description: `Producir ${product.estimated_quantity} unidades de ${product.name}`,
status: 'pending' as const,
priority: product.priority || 'medium' as const,
estimatedDuration: product.production_time || 60,
dependencies: ['production_prep'],
}));
};
// Generate order fulfillment steps
const generateOrderSteps = (ordersData: any): WorkflowStep[] => {
if (!ordersData || !ordersData.length) return [];
const specialOrders = ordersData.filter((order: any) => order.type === 'special' || order.priority === 'high');
return specialOrders.map((order: any) => ({
id: `order_${order.id}`,
name: `Preparar pedido especial`,
description: `Preparar pedido #${order.id} - ${order.items?.length || 0} items`,
status: 'pending' as const,
priority: order.priority || 'medium' as const,
estimatedDuration: order.preparation_time || 30,
dependencies: ['production_prep'],
}));
};
// Find active step
const findActiveStep = (steps: WorkflowStep[]): WorkflowStep | null => {
return steps.find(step => step.status === 'in_progress') ||
steps.find(step => step.status === 'pending') ||
null;
};
// Get workflow for specific date
const getWorkflowForDate = async (date: string): Promise<DailyWorkflow | null> => {
// This would typically fetch from an API
// For now, return null to force generation
return null;
};
// Start a workflow step
const startStep = useCallback(async (stepId: string): Promise<boolean> => {
if (!state.currentWorkflow) return false;
try {
const updatedWorkflow = {
...state.currentWorkflow,
steps: state.currentWorkflow.steps.map(step =>
step.id === stepId
? { ...step, status: 'in_progress' as const, startTime: new Date() }
: step
),
};
setState(prev => ({
...prev,
currentWorkflow: updatedWorkflow,
activeStep: updatedWorkflow.steps.find(step => step.id === stepId) || null,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al iniciar paso del flujo de trabajo' }));
return false;
}
}, [state.currentWorkflow]);
// Complete a workflow step
const completeStep = useCallback(async (stepId: string, notes?: string): Promise<boolean> => {
if (!state.currentWorkflow) return false;
try {
const step = state.currentWorkflow.steps.find(s => s.id === stepId);
if (!step || !step.startTime) return false;
const endTime = new Date();
const actualDuration = Math.round((endTime.getTime() - step.startTime.getTime()) / 60000);
const updatedWorkflow = {
...state.currentWorkflow,
steps: state.currentWorkflow.steps.map(s =>
s.id === stepId
? {
...s,
status: 'completed' as const,
endTime,
actualDuration,
notes: notes || s.notes,
}
: s
),
};
// Update completion rate
const completedSteps = updatedWorkflow.steps.filter(s => s.status === 'completed');
updatedWorkflow.completionRate = (completedSteps.length / updatedWorkflow.steps.length) * 100;
updatedWorkflow.totalActualTime = updatedWorkflow.steps
.filter(s => s.actualDuration)
.reduce((total, s) => total + (s.actualDuration || 0), 0);
setState(prev => ({
...prev,
currentWorkflow: updatedWorkflow,
activeStep: findActiveStep(updatedWorkflow.steps),
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al completar paso del flujo de trabajo' }));
return false;
}
}, [state.currentWorkflow]);
// Fail a workflow step
const failStep = useCallback(async (stepId: string, reason: string): Promise<boolean> => {
if (!state.currentWorkflow) return false;
try {
const updatedWorkflow = {
...state.currentWorkflow,
steps: state.currentWorkflow.steps.map(step =>
step.id === stepId
? { ...step, status: 'failed' as const, notes: reason }
: step
),
};
setState(prev => ({
...prev,
currentWorkflow: updatedWorkflow,
activeStep: findActiveStep(updatedWorkflow.steps),
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al marcar paso como fallido' }));
return false;
}
}, [state.currentWorkflow]);
// Skip a workflow step
const skipStep = useCallback(async (stepId: string, reason: string): Promise<boolean> => {
if (!state.currentWorkflow) return false;
try {
const updatedWorkflow = {
...state.currentWorkflow,
steps: state.currentWorkflow.steps.map(step =>
step.id === stepId
? { ...step, status: 'completed' as const, notes: `Omitido: ${reason}` }
: step
),
};
setState(prev => ({
...prev,
currentWorkflow: updatedWorkflow,
activeStep: findActiveStep(updatedWorkflow.steps),
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al omitir paso del flujo de trabajo' }));
return false;
}
}, [state.currentWorkflow]);
// Update workflow
const updateWorkflow = useCallback(async (workflow: Partial<DailyWorkflow>): Promise<boolean> => {
if (!state.currentWorkflow) return false;
try {
const updatedWorkflow = { ...state.currentWorkflow, ...workflow };
setState(prev => ({
...prev,
currentWorkflow: updatedWorkflow,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al actualizar flujo de trabajo' }));
return false;
}
}, [state.currentWorkflow]);
// Update step progress
const updateStepProgress = useCallback(async (stepId: string, progress: Partial<WorkflowStep>): Promise<boolean> => {
if (!state.currentWorkflow) return false;
try {
const updatedWorkflow = {
...state.currentWorkflow,
steps: state.currentWorkflow.steps.map(step =>
step.id === stepId ? { ...step, ...progress } : step
),
};
setState(prev => ({
...prev,
currentWorkflow: updatedWorkflow,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al actualizar progreso del paso' }));
return false;
}
}, [state.currentWorkflow]);
// Placeholder implementations for template and analytics functions
const createWorkflowTemplate = useCallback(async (name: string, steps: Omit<WorkflowStep, 'id' | 'status'>[]): Promise<boolean> => {
// Implementation would save template to backend
return true;
}, []);
const loadWorkflowTemplate = useCallback(async (templateId: string, date: string): Promise<boolean> => {
// Implementation would load template from backend
return true;
}, []);
const getWorkflowTemplates = useCallback(async (): Promise<any[]> => {
// Implementation would fetch templates from backend
return [];
}, []);
const getWorkflowAnalytics = useCallback(async (startDate: string, endDate: string): Promise<any> => {
// Implementation would fetch analytics from backend
return {};
}, []);
const getBottleneckAnalysis = useCallback(async (period: string): Promise<any> => {
// Implementation would analyze bottlenecks
return {};
}, []);
const getEfficiencyReport = useCallback(async (date: string): Promise<any> => {
// Implementation would generate efficiency report
return {};
}, []);
const subscribeToWorkflowUpdates = useCallback((callback: (workflow: DailyWorkflow) => void): (() => void) => {
// Implementation would set up real-time subscription
return () => {
// Cleanup subscription
};
}, []);
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
const refresh = useCallback(async () => {
if (state.currentWorkflow) {
await initializeDailyWorkflow(state.currentWorkflow.date);
}
}, [state.currentWorkflow, initializeDailyWorkflow]);
// Initialize workflow for today on mount
useEffect(() => {
const today = new Date().toISOString().split('T')[0];
initializeDailyWorkflow(today);
}, [initializeDailyWorkflow]);
return {
...state,
initializeDailyWorkflow,
generateWorkflowFromForecast,
updateWorkflow,
startStep,
completeStep,
failStep,
skipStep,
updateStepProgress,
createWorkflowTemplate,
loadWorkflowTemplate,
getWorkflowTemplates,
getWorkflowAnalytics,
getBottleneckAnalysis,
getEfficiencyReport,
subscribeToWorkflowUpdates,
clearError,
refresh,
};
};

View File

@@ -4,23 +4,26 @@
import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { inventoryService } from '../../services/api/inventory.service';
import { salesService } from '../../services/api/sales.service';
import { authService } from '../../services/api/auth.service';
import { tenantService } from '../../services/api/tenant.service';
import { useAuthUser } from '../../stores/auth.store';
import { useAlertActions } from '../../stores/alerts.store';
import {
ProductSuggestion,
ProductSuggestionsResponse,
InventoryCreationResponse
} from '../../types/inventory.types';
import {
BusinessModelGuide,
BusinessModelType,
TemplateData
} from '../../types/sales.types';
import { OnboardingStatus } from '../../types/auth.types';
import {
// Auth hooks
useAuthProfile,
// Tenant hooks
useRegisterBakery,
// Sales hooks
useValidateSalesRecord,
// Inventory hooks
useClassifyProductsBatch,
useCreateIngredient,
// Classification hooks
useBusinessModelAnalysis,
// Types
type User,
type BakeryRegistration,
type ProductSuggestionResponse,
type BusinessModelAnalysisResponse,
type ProductClassificationRequest,
} from '../../api';
export interface OnboardingStep {
id: string;
@@ -33,16 +36,7 @@ export interface OnboardingStep {
export interface OnboardingData {
// Step 1: Setup
bakery?: {
name: string;
business_model: BusinessModelType;
address: string;
city: string;
postal_code: string;
phone: string;
email?: string;
description?: string;
};
bakery?: BakeryRegistration;
// Step 2: Data Processing
files?: {
@@ -64,8 +58,8 @@ export interface OnboardingData {
};
// Step 3: Review
suggestions?: ProductSuggestion[];
approvedSuggestions?: ProductSuggestion[];
suggestions?: ProductSuggestionResponse[];
approvedSuggestions?: ProductSuggestionResponse[];
reviewCompleted?: boolean;
// Step 4: Inventory
@@ -99,7 +93,7 @@ interface OnboardingState {
isLoading: boolean;
error: string | null;
isInitialized: boolean;
onboardingStatus: OnboardingStatus | null;
onboardingStatus: User['onboarding_status'] | null;
}
interface OnboardingActions {
@@ -113,12 +107,12 @@ interface OnboardingActions {
validateCurrentStep: () => string | null;
// Step-specific Actions
createTenant: (bakeryData: OnboardingData['bakery']) => Promise<boolean>;
createTenant: (bakeryData: BakeryRegistration) => Promise<boolean>;
processSalesFile: (file: File, onProgress: (progress: number, stage: string, message: string) => void) => Promise<boolean>;
generateInventorySuggestions: (productList: string[]) => Promise<ProductSuggestionsResponse | null>;
createInventoryFromSuggestions: (suggestions: ProductSuggestion[]) => Promise<InventoryCreationResponse | null>;
getBusinessModelGuide: (model: BusinessModelType) => Promise<BusinessModelGuide | null>;
downloadTemplate: (templateData: TemplateData, filename: string, format?: 'csv' | 'json') => void;
generateInventorySuggestions: (productList: string[]) => Promise<ProductSuggestionResponse[] | null>;
createInventoryFromSuggestions: (suggestions: ProductSuggestionResponse[]) => Promise<any | null>;
getBusinessModelGuide: (model: string) => Promise<BusinessModelAnalysisResponse | null>;
downloadTemplate: (templateData: any, filename: string, format?: 'csv' | 'json') => void;
// Completion
completeOnboarding: () => Promise<boolean>;
@@ -138,7 +132,7 @@ const DEFAULT_STEPS: OnboardingStep[] = [
isCompleted: false,
validation: (data: OnboardingData) => {
if (!data.bakery?.name) return 'El nombre de la panadería es requerido';
if (!data.bakery?.business_model) return 'El modelo de negocio es requerido';
if (!data.bakery?.business_type) return 'El tipo de negocio es requerido';
if (!data.bakery?.address) return 'La dirección es requerida';
if (!data.bakery?.city) return 'La ciudad es requerida';
if (!data.bakery?.postal_code) return 'El código postal es requerido';
@@ -221,7 +215,13 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
const navigate = useNavigate();
const user = useAuthUser();
const { createAlert } = useAlertActions();
// React Query hooks
const { data: profile } = useAuthProfile();
const registerBakeryMutation = useRegisterBakery();
const validateSalesMutation = useValidateSalesRecord();
const classifyProductsMutation = useClassifyProductsBatch();
const businessModelMutation = useBusinessModelAnalysis();
// Initialize onboarding status
useEffect(() => {
@@ -236,14 +236,7 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
const validation = validateCurrentStep();
if (validation) {
createAlert({
type: 'error',
category: 'validation',
priority: 'high',
title: 'Validación fallida',
message: validation,
source: 'onboarding'
});
setState(prev => ({ ...prev, error: validation }));
return false;
}
@@ -302,54 +295,25 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
}, [state.currentStep, state.steps, state.data]);
// Step-specific Actions
const createTenant = useCallback(async (bakeryData: OnboardingData['bakery']): Promise<boolean> => {
const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise<boolean> => {
if (!bakeryData) return false;
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await tenantService.createTenant({
name: bakeryData.name,
description: bakeryData.description || '',
business_type: bakeryData.business_model,
settings: {
address: bakeryData.address,
city: bakeryData.city,
postal_code: bakeryData.postal_code,
phone: bakeryData.phone,
email: bakeryData.email,
}
await registerBakeryMutation.mutateAsync({
bakeryData
});
if (response.success) {
updateStepData('setup', { bakery: bakeryData });
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Tenant creado',
message: 'Tu panadería ha sido configurada exitosamente',
source: 'onboarding'
});
setState(prev => ({ ...prev, isLoading: false }));
return true;
} else {
throw new Error(response.error || 'Error creating tenant');
}
updateStepData('setup', { bakery: bakeryData });
setState(prev => ({ ...prev, isLoading: false }));
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al crear tenant',
message: errorMessage,
source: 'onboarding'
});
return false;
}
}, [updateStepData, createAlert]);
}, [updateStepData, registerBakeryMutation]);
const processSalesFile = useCallback(async (
file: File,
@@ -360,16 +324,19 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
try {
// Stage 1: Validate file
onProgress(20, 'validating', 'Validando estructura del archivo...');
const validationResult = await salesService.validateSalesData(file);
const validationResult = await validateSalesMutation.mutateAsync({
// Convert file to the expected format for validation
data: {
// This would need to be adapted based on the actual API structure
file_data: file
}
});
onProgress(40, 'validating', 'Verificando integridad de datos...');
if (!validationResult.is_valid) {
throw new Error('Archivo de datos inválido');
}
if (!validationResult.product_list || validationResult.product_list.length === 0) {
throw new Error('No se encontraron productos en el archivo');
if (!validationResult || !validationResult.product_list || validationResult.product_list.length === 0) {
throw new Error('No se encontraron productos válidos en el archivo');
}
// Stage 2: Generate AI suggestions
@@ -384,7 +351,7 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
files: { salesData: file },
processingStage: 'completed',
processingResults: validationResult,
suggestions: suggestions?.suggestions || []
suggestions: suggestions || []
});
setState(prev => ({ ...prev, isLoading: false }));
@@ -402,36 +369,48 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
}));
return false;
}
}, [updateStepData]);
}, [updateStepData, validateSalesMutation]);
const generateInventorySuggestions = useCallback(async (productList: string[]): Promise<ProductSuggestionsResponse | null> => {
const generateInventorySuggestions = useCallback(async (productList: string[]): Promise<ProductSuggestionResponse[] | null> => {
try {
const response = await inventoryService.generateInventorySuggestions(productList);
return response.success ? response.data : null;
const response = await classifyProductsMutation.mutateAsync({
products: productList.map(name => ({ name, description: '' }))
});
return response.suggestions || [];
} catch (error) {
console.error('Error generating inventory suggestions:', error);
return null;
}
}, []);
}, [classifyProductsMutation]);
const createInventoryFromSuggestions = useCallback(async (suggestions: ProductSuggestion[]): Promise<InventoryCreationResponse | null> => {
const createInventoryFromSuggestions = useCallback(async (suggestions: ProductSuggestionResponse[]): Promise<any | null> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await inventoryService.createInventoryFromSuggestions(suggestions);
// Create ingredients from approved suggestions
const createdItems = [];
const inventoryMapping: { [key: string]: string } = {};
if (response.success) {
updateStepData('inventory', {
inventoryItems: response.data.created_items,
inventoryMapping: response.data.inventory_mapping,
inventoryConfigured: true
for (const suggestion of suggestions) {
// This would need to be adapted based on actual API structure
const createdItem = await useCreateIngredient().mutateAsync({
name: suggestion.name,
category: suggestion.category,
// Map other suggestion properties to ingredient properties
});
setState(prev => ({ ...prev, isLoading: false }));
return response.data;
} else {
throw new Error(response.error || 'Error creating inventory');
createdItems.push(createdItem);
inventoryMapping[suggestion.name] = createdItem.id;
}
updateStepData('inventory', {
inventoryItems: createdItems,
inventoryMapping,
inventoryConfigured: true
});
setState(prev => ({ ...prev, isLoading: false }));
return { created_items: createdItems, inventory_mapping: inventoryMapping };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error creating inventory';
setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
@@ -439,28 +418,49 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
}
}, [updateStepData]);
const getBusinessModelGuide = useCallback(async (model: BusinessModelType): Promise<BusinessModelGuide | null> => {
const getBusinessModelGuide = useCallback(async (model: string): Promise<BusinessModelAnalysisResponse | null> => {
try {
const response = await salesService.getBusinessModelGuide(model);
return response.success ? response.data : null;
const response = await businessModelMutation.mutateAsync({
business_model: model,
// Include any other required parameters
});
return response;
} catch (error) {
console.error('Error getting business model guide:', error);
return null;
}
}, [businessModelMutation]);
const downloadTemplate = useCallback((templateData: any, filename: string, format: 'csv' | 'json' = 'csv') => {
// Create and download template file
const content = format === 'json' ? JSON.stringify(templateData, null, 2) : convertToCSV(templateData);
const blob = new Blob([content], { type: format === 'json' ? 'application/json' : 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${filename}.${format}`;
a.click();
URL.revokeObjectURL(url);
}, []);
const downloadTemplate = useCallback((templateData: TemplateData, filename: string, format: 'csv' | 'json' = 'csv') => {
salesService.downloadTemplate(templateData, filename, format);
}, []);
const convertToCSV = (data: any): string => {
// Simple CSV conversion - this should be adapted based on the actual data structure
if (Array.isArray(data)) {
const headers = Object.keys(data[0] || {}).join(',');
const rows = data.map(item => Object.values(item).join(',')).join('\n');
return `${headers}\n${rows}`;
}
return JSON.stringify(data);
};
const checkOnboardingStatus = useCallback(async () => {
setState(prev => ({ ...prev, isLoading: true }));
try {
const response = await authService.checkOnboardingStatus();
// Use the profile data to get onboarding status
setState(prev => ({
...prev,
onboardingStatus: response.success ? response.data : null,
onboardingStatus: profile?.onboarding_status || null,
isInitialized: true,
isLoading: false
}));
@@ -471,58 +471,33 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
isLoading: false
}));
}
}, []);
}, [profile]);
const completeOnboarding = useCallback(async (): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await authService.completeOnboarding({
completedAt: new Date().toISOString(),
data: state.data
});
// Mark onboarding as completed - this would typically involve an API call
// For now, we'll simulate success and navigate to dashboard
setState(prev => ({
...prev,
isLoading: false,
steps: prev.steps.map(step => ({ ...step, isCompleted: true }))
}));
if (response.success) {
createAlert({
type: 'success',
category: 'system',
priority: 'high',
title: '¡Onboarding completado!',
message: 'Has completado exitosamente la configuración inicial',
source: 'onboarding'
});
// Navigate to dashboard after a short delay
setTimeout(() => {
navigate('/app/dashboard');
}, 2000);
setState(prev => ({
...prev,
isLoading: false,
steps: prev.steps.map(step => ({ ...step, isCompleted: true }))
}));
// Navigate to dashboard after a short delay
setTimeout(() => {
navigate('/app/dashboard');
}, 2000);
return true;
} else {
throw new Error(response.error || 'Error completing onboarding');
}
return true;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Error completing onboarding';
setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al completar onboarding',
message: errorMessage,
source: 'onboarding'
});
return false;
}
}, [state.data, createAlert, navigate]);
}, [state.data, navigate]);
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));

View File

@@ -1,655 +0,0 @@
/**
* Production schedule hook for managing bakery production scheduling and capacity planning
*/
import { useState, useEffect, useCallback } from 'react';
import { ProductionService } from '../../services/api/production.service';
import { InventoryService } from '../../services/api/inventory.service';
import { ForecastingService } from '../../services/api/forecasting.service';
export interface ScheduleItem {
id: string;
recipeId: string;
recipeName: string;
quantity: number;
priority: 'low' | 'medium' | 'high' | 'urgent';
estimatedStartTime: Date;
estimatedEndTime: Date;
actualStartTime?: Date;
actualEndTime?: Date;
status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'delayed';
assignedEquipment?: string[];
assignedStaff?: string[];
requiredIngredients: {
ingredientId: string;
ingredientName: string;
requiredQuantity: number;
availableQuantity: number;
unit: string;
}[];
notes?: string;
dependencies?: string[];
}
export interface ProductionSlot {
startTime: Date;
endTime: Date;
isAvailable: boolean;
assignedItems: ScheduleItem[];
capacity: number;
utilizationRate: number;
}
export interface DailySchedule {
date: string;
items: ScheduleItem[];
slots: ProductionSlot[];
totalCapacity: number;
totalUtilization: number;
efficiency: number;
bottlenecks: string[];
}
interface ProductionScheduleState {
currentSchedule: DailySchedule | null;
scheduleHistory: DailySchedule[];
availableRecipes: any[];
equipmentStatus: any[];
staffAvailability: any[];
isLoading: boolean;
error: string | null;
constraints: {
maxDailyCapacity: number;
workingHours: { start: string; end: string };
equipmentLimitations: Record<string, number>;
staffLimitations: Record<string, number>;
};
}
interface ProductionScheduleActions {
// Schedule Management
loadSchedule: (date: string) => Promise<void>;
createSchedule: (date: string) => Promise<void>;
updateSchedule: (schedule: Partial<DailySchedule>) => Promise<boolean>;
// Schedule Items
addScheduleItem: (item: Omit<ScheduleItem, 'id'>) => Promise<boolean>;
updateScheduleItem: (id: string, item: Partial<ScheduleItem>) => Promise<boolean>;
removeScheduleItem: (id: string) => Promise<boolean>;
moveScheduleItem: (id: string, newStartTime: Date) => Promise<boolean>;
// Automatic Scheduling
autoSchedule: (date: string, items: Omit<ScheduleItem, 'id'>[]) => Promise<boolean>;
optimizeSchedule: (date: string) => Promise<boolean>;
generateFromForecast: (date: string) => Promise<boolean>;
// Capacity Management
checkCapacity: (date: string, newItem: Omit<ScheduleItem, 'id'>) => Promise<{ canSchedule: boolean; suggestedTime?: Date; conflicts?: string[] }>;
getAvailableSlots: (date: string, duration: number) => Promise<ProductionSlot[]>;
calculateUtilization: (date: string) => Promise<number>;
// Resource Management
checkIngredientAvailability: (items: ScheduleItem[]) => Promise<{ available: boolean; shortages: any[] }>;
checkEquipmentAvailability: (date: string, equipment: string[], timeSlot: { start: Date; end: Date }) => Promise<boolean>;
checkStaffAvailability: (date: string, staff: string[], timeSlot: { start: Date; end: Date }) => Promise<boolean>;
// Analytics and Optimization
getScheduleAnalytics: (startDate: string, endDate: string) => Promise<any>;
getBottleneckAnalysis: (date: string) => Promise<any>;
getEfficiencyReport: (period: string) => Promise<any>;
predictDelays: (date: string) => Promise<any>;
// Templates and Presets
saveScheduleTemplate: (name: string, template: Omit<ScheduleItem, 'id'>[]) => Promise<boolean>;
loadScheduleTemplate: (templateId: string, date: string) => Promise<boolean>;
getScheduleTemplates: () => Promise<any[]>;
// Utilities
clearError: () => void;
refresh: () => Promise<void>;
}
export const useProductionSchedule = (): ProductionScheduleState & ProductionScheduleActions => {
const [state, setState] = useState<ProductionScheduleState>({
currentSchedule: null,
scheduleHistory: [],
availableRecipes: [],
equipmentStatus: [],
staffAvailability: [],
isLoading: false,
error: null,
constraints: {
maxDailyCapacity: 8 * 60, // 8 hours in minutes
workingHours: { start: '06:00', end: '20:00' },
equipmentLimitations: {},
staffLimitations: {},
},
});
const productionService = new ProductionService();
const inventoryService = new InventoryService();
const forecastingService = new ForecastingService();
// Load schedule for specific date
const loadSchedule = useCallback(async (date: string) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Get schedule data from API
const scheduleData = await getScheduleFromAPI(date);
if (scheduleData) {
setState(prev => ({
...prev,
currentSchedule: scheduleData,
isLoading: false,
}));
} else {
// Create new schedule if none exists
await createSchedule(date);
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al cargar programación de producción',
}));
}
}, []);
// Create new schedule
const createSchedule = useCallback(async (date: string) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const workingHours = generateWorkingHours(date, state.constraints.workingHours);
const slots = generateTimeSlots(workingHours, 30); // 30-minute slots
const newSchedule: DailySchedule = {
date,
items: [],
slots,
totalCapacity: state.constraints.maxDailyCapacity,
totalUtilization: 0,
efficiency: 0,
bottlenecks: [],
};
setState(prev => ({
...prev,
currentSchedule: newSchedule,
isLoading: false,
}));
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al crear nueva programación',
}));
}
}, [state.constraints]);
// Generate working hours for a date
const generateWorkingHours = (date: string, workingHours: { start: string; end: string }) => {
const startTime = new Date(`${date}T${workingHours.start}`);
const endTime = new Date(`${date}T${workingHours.end}`);
return { startTime, endTime };
};
// Generate time slots
const generateTimeSlots = (workingHours: { startTime: Date; endTime: Date }, slotDuration: number): ProductionSlot[] => {
const slots: ProductionSlot[] = [];
const current = new Date(workingHours.startTime);
while (current < workingHours.endTime) {
const slotEnd = new Date(current.getTime() + slotDuration * 60000);
slots.push({
startTime: new Date(current),
endTime: slotEnd,
isAvailable: true,
assignedItems: [],
capacity: 1,
utilizationRate: 0,
});
current.setTime(slotEnd.getTime());
}
return slots;
};
// Add schedule item
const addScheduleItem = useCallback(async (item: Omit<ScheduleItem, 'id'>): Promise<boolean> => {
if (!state.currentSchedule) return false;
setState(prev => ({ ...prev, error: null }));
try {
// Check capacity and resources
const capacityCheck = await checkCapacity(state.currentSchedule.date, item);
if (!capacityCheck.canSchedule) {
setState(prev => ({
...prev,
error: `No se puede programar: ${capacityCheck.conflicts?.join(', ')}`,
}));
return false;
}
const newItem: ScheduleItem = {
...item,
id: generateScheduleItemId(),
};
const updatedSchedule = {
...state.currentSchedule,
items: [...state.currentSchedule.items, newItem],
};
// Recalculate utilization and efficiency
recalculateScheduleMetrics(updatedSchedule);
setState(prev => ({
...prev,
currentSchedule: updatedSchedule,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al agregar item a la programación' }));
return false;
}
}, [state.currentSchedule]);
// Update schedule item
const updateScheduleItem = useCallback(async (id: string, item: Partial<ScheduleItem>): Promise<boolean> => {
if (!state.currentSchedule) return false;
try {
const updatedSchedule = {
...state.currentSchedule,
items: state.currentSchedule.items.map(scheduleItem =>
scheduleItem.id === id ? { ...scheduleItem, ...item } : scheduleItem
),
};
recalculateScheduleMetrics(updatedSchedule);
setState(prev => ({
...prev,
currentSchedule: updatedSchedule,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al actualizar item de programación' }));
return false;
}
}, [state.currentSchedule]);
// Remove schedule item
const removeScheduleItem = useCallback(async (id: string): Promise<boolean> => {
if (!state.currentSchedule) return false;
try {
const updatedSchedule = {
...state.currentSchedule,
items: state.currentSchedule.items.filter(item => item.id !== id),
};
recalculateScheduleMetrics(updatedSchedule);
setState(prev => ({
...prev,
currentSchedule: updatedSchedule,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al eliminar item de programación' }));
return false;
}
}, [state.currentSchedule]);
// Move schedule item to new time
const moveScheduleItem = useCallback(async (id: string, newStartTime: Date): Promise<boolean> => {
if (!state.currentSchedule) return false;
const item = state.currentSchedule.items.find(item => item.id === id);
if (!item) return false;
const duration = item.estimatedEndTime.getTime() - item.estimatedStartTime.getTime();
const newEndTime = new Date(newStartTime.getTime() + duration);
return updateScheduleItem(id, {
estimatedStartTime: newStartTime,
estimatedEndTime: newEndTime,
});
}, [state.currentSchedule, updateScheduleItem]);
// Auto-schedule items
const autoSchedule = useCallback(async (date: string, items: Omit<ScheduleItem, 'id'>[]): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Sort items by priority and estimated duration
const sortedItems = [...items].sort((a, b) => {
const priorityOrder = { urgent: 4, high: 3, medium: 2, low: 1 };
return priorityOrder[b.priority] - priorityOrder[a.priority];
});
const schedule = await createOptimalSchedule(date, sortedItems);
setState(prev => ({
...prev,
currentSchedule: schedule,
isLoading: false,
}));
return true;
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al programar automáticamente',
}));
return false;
}
}, []);
// Create optimal schedule
const createOptimalSchedule = async (date: string, items: Omit<ScheduleItem, 'id'>[]): Promise<DailySchedule> => {
const workingHours = generateWorkingHours(date, state.constraints.workingHours);
const slots = generateTimeSlots(workingHours, 30);
const scheduledItems: ScheduleItem[] = [];
let currentTime = new Date(workingHours.startTime);
for (const item of items) {
const duration = item.estimatedEndTime.getTime() - item.estimatedStartTime.getTime();
const endTime = new Date(currentTime.getTime() + duration);
// Check if item fits in remaining time
if (endTime <= workingHours.endTime) {
scheduledItems.push({
...item,
id: generateScheduleItemId(),
estimatedStartTime: new Date(currentTime),
estimatedEndTime: endTime,
});
currentTime = endTime;
}
}
const schedule: DailySchedule = {
date,
items: scheduledItems,
slots,
totalCapacity: state.constraints.maxDailyCapacity,
totalUtilization: 0,
efficiency: 0,
bottlenecks: [],
};
recalculateScheduleMetrics(schedule);
return schedule;
};
// Check capacity for new item
const checkCapacity = useCallback(async (date: string, newItem: Omit<ScheduleItem, 'id'>) => {
const conflicts: string[] = [];
let canSchedule = true;
// Check time conflicts
if (state.currentSchedule) {
const hasTimeConflict = state.currentSchedule.items.some(item => {
return (newItem.estimatedStartTime < item.estimatedEndTime &&
newItem.estimatedEndTime > item.estimatedStartTime);
});
if (hasTimeConflict) {
conflicts.push('Conflicto de horario');
canSchedule = false;
}
}
// Check ingredient availability
const ingredientCheck = await checkIngredientAvailability([newItem as ScheduleItem]);
if (!ingredientCheck.available) {
conflicts.push('Ingredientes insuficientes');
canSchedule = false;
}
return { canSchedule, conflicts };
}, [state.currentSchedule]);
// Get available slots
const getAvailableSlots = useCallback(async (date: string, duration: number): Promise<ProductionSlot[]> => {
if (!state.currentSchedule) return [];
return state.currentSchedule.slots.filter(slot => {
const slotDuration = slot.endTime.getTime() - slot.startTime.getTime();
return slot.isAvailable && slotDuration >= duration * 60000;
});
}, [state.currentSchedule]);
// Check ingredient availability
const checkIngredientAvailability = useCallback(async (items: ScheduleItem[]) => {
try {
const stockLevels = await inventoryService.getStockLevels();
const shortages: any[] = [];
for (const item of items) {
for (const ingredient of item.requiredIngredients) {
const stock = stockLevels.data?.find((s: any) => s.ingredient_id === ingredient.ingredientId);
if (!stock || stock.current_quantity < ingredient.requiredQuantity) {
shortages.push({
ingredientName: ingredient.ingredientName,
required: ingredient.requiredQuantity,
available: stock?.current_quantity || 0,
});
}
}
}
return { available: shortages.length === 0, shortages };
} catch (error) {
return { available: false, shortages: [] };
}
}, [inventoryService]);
// Generate from forecast
const generateFromForecast = useCallback(async (date: string): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Get forecast data
const forecast = await forecastingService.generateDemandForecast('default', 1);
if (!forecast) {
setState(prev => ({
...prev,
isLoading: false,
error: 'No se pudo obtener predicción de demanda',
}));
return false;
}
// Convert forecast to schedule items
const items = convertForecastToScheduleItems(forecast);
// Auto-schedule the items
return await autoSchedule(date, items);
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al generar programación desde predicción',
}));
return false;
}
}, [forecastingService, autoSchedule]);
// Convert forecast to schedule items
const convertForecastToScheduleItems = (forecast: any): Omit<ScheduleItem, 'id'>[] => {
if (!forecast.products) return [];
return forecast.products.map((product: any) => ({
recipeId: product.recipe_id || `recipe_${product.id}`,
recipeName: product.name,
quantity: product.estimated_quantity || 1,
priority: 'medium' as const,
estimatedStartTime: new Date(),
estimatedEndTime: new Date(Date.now() + (product.production_time || 60) * 60000),
status: 'scheduled' as const,
requiredIngredients: product.ingredients || [],
}));
};
// Recalculate schedule metrics
const recalculateScheduleMetrics = (schedule: DailySchedule) => {
const totalScheduledTime = schedule.items.reduce((total, item) => {
return total + (item.estimatedEndTime.getTime() - item.estimatedStartTime.getTime());
}, 0);
schedule.totalUtilization = (totalScheduledTime / (schedule.totalCapacity * 60000)) * 100;
schedule.efficiency = calculateEfficiency(schedule.items);
schedule.bottlenecks = identifyBottlenecks(schedule.items);
};
// Calculate efficiency
const calculateEfficiency = (items: ScheduleItem[]): number => {
if (items.length === 0) return 0;
const completedItems = items.filter(item => item.status === 'completed');
return (completedItems.length / items.length) * 100;
};
// Identify bottlenecks
const identifyBottlenecks = (items: ScheduleItem[]): string[] => {
const bottlenecks: string[] = [];
// Check for equipment conflicts
const equipmentUsage: Record<string, number> = {};
items.forEach(item => {
item.assignedEquipment?.forEach(equipment => {
equipmentUsage[equipment] = (equipmentUsage[equipment] || 0) + 1;
});
});
Object.entries(equipmentUsage).forEach(([equipment, usage]) => {
if (usage > 1) {
bottlenecks.push(`Conflicto de equipamiento: ${equipment}`);
}
});
return bottlenecks;
};
// Generate unique ID
const generateScheduleItemId = (): string => {
return `schedule_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
};
// Get schedule from API (placeholder)
const getScheduleFromAPI = async (date: string): Promise<DailySchedule | null> => {
// This would fetch from actual API
return null;
};
// Placeholder implementations for remaining functions
const updateSchedule = useCallback(async (schedule: Partial<DailySchedule>): Promise<boolean> => {
return true;
}, []);
const optimizeSchedule = useCallback(async (date: string): Promise<boolean> => {
return true;
}, []);
const calculateUtilization = useCallback(async (date: string): Promise<number> => {
return state.currentSchedule?.totalUtilization || 0;
}, [state.currentSchedule]);
const checkEquipmentAvailability = useCallback(async (date: string, equipment: string[], timeSlot: { start: Date; end: Date }): Promise<boolean> => {
return true;
}, []);
const checkStaffAvailability = useCallback(async (date: string, staff: string[], timeSlot: { start: Date; end: Date }): Promise<boolean> => {
return true;
}, []);
const getScheduleAnalytics = useCallback(async (startDate: string, endDate: string): Promise<any> => {
return {};
}, []);
const getBottleneckAnalysis = useCallback(async (date: string): Promise<any> => {
return {};
}, []);
const getEfficiencyReport = useCallback(async (period: string): Promise<any> => {
return {};
}, []);
const predictDelays = useCallback(async (date: string): Promise<any> => {
return {};
}, []);
const saveScheduleTemplate = useCallback(async (name: string, template: Omit<ScheduleItem, 'id'>[]): Promise<boolean> => {
return true;
}, []);
const loadScheduleTemplate = useCallback(async (templateId: string, date: string): Promise<boolean> => {
return true;
}, []);
const getScheduleTemplates = useCallback(async (): Promise<any[]> => {
return [];
}, []);
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
const refresh = useCallback(async () => {
if (state.currentSchedule) {
await loadSchedule(state.currentSchedule.date);
}
}, [state.currentSchedule, loadSchedule]);
// Load today's schedule on mount
useEffect(() => {
const today = new Date().toISOString().split('T')[0];
loadSchedule(today);
}, [loadSchedule]);
return {
...state,
loadSchedule,
createSchedule,
updateSchedule,
addScheduleItem,
updateScheduleItem,
removeScheduleItem,
moveScheduleItem,
autoSchedule,
optimizeSchedule,
generateFromForecast,
checkCapacity,
getAvailableSlots,
calculateUtilization,
checkIngredientAvailability,
checkEquipmentAvailability,
checkStaffAvailability,
getScheduleAnalytics,
getBottleneckAnalysis,
getEfficiencyReport,
predictDelays,
saveScheduleTemplate,
loadScheduleTemplate,
getScheduleTemplates,
clearError,
refresh,
};
};

View File

@@ -1,18 +0,0 @@
// Mock auth hook for testing
export const useAuth = () => {
return {
user: {
id: 'user_123',
tenant_id: 'tenant_456',
email: 'user@example.com',
name: 'Usuario Demo'
},
isAuthenticated: true,
login: async (credentials: any) => {
console.log('Mock login:', credentials);
},
logout: () => {
console.log('Mock logout');
}
};
};

View File

@@ -1,27 +0,0 @@
import { useEffect } from 'react';
import { useIsAuthenticated } from '../stores/auth.store';
import { useTenantActions, useAvailableTenants } from '../stores/tenant.store';
/**
* Hook to automatically initialize tenant data when user is authenticated
* This should be used at the app level to ensure tenant data is loaded
*/
export const useTenantInitializer = () => {
const isAuthenticated = useIsAuthenticated();
const availableTenants = useAvailableTenants();
const { loadUserTenants } = useTenantActions();
useEffect(() => {
if (isAuthenticated && !availableTenants) {
// Load user's available tenants when authenticated and not already loaded
loadUserTenants();
}
}, [isAuthenticated, availableTenants, loadUserTenants]);
// Also load tenants when user becomes authenticated (e.g., after login)
useEffect(() => {
if (isAuthenticated && availableTenants === null) {
loadUserTenants();
}
}, [isAuthenticated, availableTenants, loadUserTenants]);
};