Start integrating the onboarding flow with backend 6
This commit is contained in:
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
Reference in New Issue
Block a user