diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5a1e12fc..d37efe58 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,8 +1,4 @@ -const [appState, setAppState] = useState({ - isAuthenticated: false, - isLoading: true, - user: null, - currentPage: 'landing' // 👈 Startimport React, { useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import { Toaster } from 'react-hot-toast'; // Components diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 825a0c6e..cbe1cdf3 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,10 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import App from './App.tsx' 👈 Imports from ./App.tsx +import App from './App.tsx' import './index.css' ReactDOM.createRoot(document.getElementById('root')!).render( - 👈 Renders the App component + , ) \ No newline at end of file diff --git a/frontend/src/pages/auth/RegisterPage.tsx b/frontend/src/pages/auth/RegisterPage.tsx index 943c86b5..dc362679 100644 --- a/frontend/src/pages/auth/RegisterPage.tsx +++ b/frontend/src/pages/auth/RegisterPage.tsx @@ -1,247 +1,3 @@ -{/* Register Form */} -
-
- {/* Full Name Field */} -
- - - {errors.fullName && ( -

{errors.fullName}

- )} -
- - {/* Email Field */} -
- - - {errors.email && ( -

{errors.email}

- )} -
- - {/* Password Field */} -
- -
- - -
- - {/* Password Strength Indicator */} - {formData.password && ( -
-
- {[...Array(5)].map((_, i) => ( -
- ))} -
-

- Seguridad: {strengthLabels[passwordStrength - 1] || 'Muy débil'} -

-
- )} - - {errors.password && ( -

{errors.password}

- )} -
- - {/* Confirm Password Field */} -
- -
- - - {formData.confirmPassword && formData.password === formData.confirmPassword && ( -
- -
- )} -
- {errors.confirmPassword && ( -

{errors.confirmPassword}

- )} -
- - {/* Terms and Conditions */} -
-
- - -
- {errors.acceptTerms && ( -

{errors.acceptTerms}

- )} -
- - {/* Submit Button */} -
- -
-
- - {/* Login Link */} -
-

- ¿Ya tienes una cuenta?{' '} - -

-
-
import React, { useState } from 'react'; import { Eye, EyeOff, Loader2, Check } from 'lucide-react'; import toast from 'react-hot-toast'; diff --git a/frontend/src/pages/dashboard/DashboardPage.tsx b/frontend/src/pages/dashboard/DashboardPage.tsx index 33134882..99901445 100644 --- a/frontend/src/pages/dashboard/DashboardPage.tsx +++ b/frontend/src/pages/dashboard/DashboardPage.tsx @@ -27,6 +27,27 @@ interface MetricsData { } const DashboardPage: React.FC = ({ user }) => { + const [isLoading, setIsLoading] = useState(true); + const [weather, setWeather] = useState(null); + const [todayForecasts, setTodayForecasts] = useState([]); + const [metrics, setMetrics] = useState({ + totalSales: 0, + wasteReduction: 0, + accuracy: 0, + stockouts: 0 + }); + + // Sample historical data for charts + const salesHistory = [ + { date: '2024-10-28', ventas: 145, prediccion: 140 }, + { date: '2024-10-29', ventas: 128, prediccion: 135 }, + { date: '2024-10-30', ventas: 167, prediccion: 160 }, + { date: '2024-10-31', ventas: 143, prediccion: 145 }, + { date: '2024-11-01', ventas: 156, prediccion: 150 }, + { date: '2024-11-02', ventas: 189, prediccion: 185 }, + { date: '2024-11-03', ventas: 134, prediccion: 130 }, + ]; + const topProducts = [ { name: 'Croissants', quantity: 45, trend: 'up' }, { name: 'Pan de molde', quantity: 32, trend: 'up' }, @@ -405,25 +426,4 @@ const DashboardPage: React.FC = ({ user }) => { ); }; -export default DashboardPage; [isLoading, setIsLoading] = useState(true); - const [weather, setWeather] = useState(null); - const [todayForecasts, setTodayForecasts] = useState([]); - const [metrics, setMetrics] = useState({ - totalSales: 0, - wasteReduction: 0, - accuracy: 0, - stockouts: 0 - }); - - // Sample historical data for charts - const salesHistory = [ - { date: '2024-10-28', ventas: 145, prediccion: 140 }, - { date: '2024-10-29', ventas: 128, prediccion: 135 }, - { date: '2024-10-30', ventas: 167, prediccion: 160 }, - { date: '2024-10-31', ventas: 143, prediccion: 145 }, - { date: '2024-11-01', ventas: 156, prediccion: 150 }, - { date: '2024-11-02', ventas: 189, prediccion: 185 }, - { date: '2024-11-03', ventas: 134, prediccion: 130 }, - ]; - - const \ No newline at end of file +export default DashboardPage; \ No newline at end of file diff --git a/frontend/src/store/index.ts b/frontend/src/store/index.ts index 3526332a..d51fbb14 100644 --- a/frontend/src/store/index.ts +++ b/frontend/src/store/index.ts @@ -1,4 +1,3 @@ -/ src/store/index.ts import { configureStore } from '@reduxjs/toolkit'; import authSlice from './slices/authSlice'; import tenantSlice from './slices/tenantSlice'; diff --git a/frontend/src/store/slices/forecastSlice.ts b/frontend/src/store/slices/forecastSlice.ts index e69de29b..b536b6fb 100644 --- a/frontend/src/store/slices/forecastSlice.ts +++ b/frontend/src/store/slices/forecastSlice.ts @@ -0,0 +1,439 @@ +// src/store/slices/forecastSlice.ts +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; + +export interface Forecast { + id: string; + tenant_id: string; + product_name: string; + location: string; + forecast_date: string; + created_at: string; + + // Prediction results + predicted_demand: number; + confidence_lower: number; + confidence_upper: number; + confidence_level: number; + + // Model information + model_id: string; + model_version: string; + algorithm: string; + + // Business context + business_type: string; + day_of_week: number; + is_holiday: boolean; + is_weekend: boolean; + + // External factors + weather_temperature?: number; + weather_precipitation?: number; + weather_description?: string; + traffic_volume?: number; + + // Metadata + processing_time_ms?: number; + features_used?: Record; +} + +export interface ForecastAlert { + id: string; + tenant_id: string; + type: 'high_demand' | 'low_demand' | 'stockout_risk' | 'overproduction'; + product_name: string; + message: string; + severity: 'low' | 'medium' | 'high'; + created_at: string; + acknowledged: boolean; + forecast_id?: string; +} + +export interface QuickForecast { + product: string; + predicted: number; + confidence: 'high' | 'medium' | 'low'; + change: number; + factors: string[]; + weatherImpact?: string; +} + +interface ForecastState { + forecasts: Forecast[]; + todayForecasts: QuickForecast[]; + alerts: ForecastAlert[]; + + // Loading states + isLoading: boolean; + isGeneratingForecast: boolean; + isFetchingAlerts: boolean; + + // Selected filters + selectedDate: string; + selectedProduct: string; + selectedLocation: string; + + // Errors + error: string | null; + + // Weather context + currentWeather: { + temperature: number; + description: string; + precipitation: number; + } | null; + + // Performance metrics + modelAccuracy: number; + lastUpdated: string | null; +} + +const initialState: ForecastState = { + forecasts: [], + todayForecasts: [], + alerts: [], + isLoading: false, + isGeneratingForecast: false, + isFetchingAlerts: false, + selectedDate: new Date().toISOString().split('T')[0], + selectedProduct: 'all', + selectedLocation: '', + error: null, + currentWeather: null, + modelAccuracy: 0, + lastUpdated: null, +}; + +// Async thunks for API calls +export const generateForecast = createAsyncThunk( + 'forecast/generate', + async ({ + tenantId, + productName, + forecastDate, + forecastDays = 1, + location + }: { + tenantId: string; + productName: string; + forecastDate: string; + forecastDays?: number; + location: string; + }) => { + const response = await fetch(`/api/v1/tenants/${tenantId}/forecasts/single`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`, + }, + body: JSON.stringify({ + product_name: productName, + forecast_date: forecastDate, + forecast_days: forecastDays, + location, + }), + }); + + if (!response.ok) { + throw new Error('Failed to generate forecast'); + } + + return response.json(); + } +); + +export const generateBatchForecast = createAsyncThunk( + 'forecast/generateBatch', + async ({ + tenantId, + products, + forecastDays = 7 + }: { + tenantId: string; + products: string[]; + forecastDays?: number; + }) => { + const response = await fetch(`/api/v1/tenants/${tenantId}/forecasts/batch`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`, + }, + body: JSON.stringify({ + products, + forecast_days: forecastDays, + batch_name: `Batch_${new Date().toISOString()}`, + }), + }); + + if (!response.ok) { + throw new Error('Failed to generate batch forecast'); + } + + return response.json(); + } +); + +export const fetchForecasts = createAsyncThunk( + 'forecast/fetchAll', + async (tenantId: string) => { + const response = await fetch(`/api/v1/tenants/${tenantId}/forecasts/list`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch forecasts'); + } + + return response.json(); + } +); + +export const fetchTodayForecasts = createAsyncThunk( + 'forecast/fetchToday', + async (tenantId: string) => { + const today = new Date().toISOString().split('T')[0]; + const response = await fetch(`/api/v1/tenants/${tenantId}/forecasts/list?date=${today}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch today\'s forecasts'); + } + + return response.json(); + } +); + +export const fetchForecastAlerts = createAsyncThunk( + 'forecast/fetchAlerts', + async (tenantId: string) => { + const response = await fetch(`/api/v1/tenants/${tenantId}/forecasts/alerts`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch forecast alerts'); + } + + return response.json(); + } +); + +export const acknowledgeAlert = createAsyncThunk( + 'forecast/acknowledgeAlert', + async ({ tenantId, alertId }: { tenantId: string; alertId: string }) => { + const response = await fetch(`/api/v1/tenants/${tenantId}/forecasts/alerts/${alertId}/acknowledge`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to acknowledge alert'); + } + + return { alertId }; + } +); + +export const fetchWeatherContext = createAsyncThunk( + 'forecast/fetchWeather', + async (location: string) => { + // This would typically call a weather API or your backend's weather endpoint + const response = await fetch(`/api/v1/data/weather?location=${encodeURIComponent(location)}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch weather data'); + } + + return response.json(); + } +); + +const forecastSlice = createSlice({ + name: 'forecast', + initialState, + reducers: { + clearError: (state) => { + state.error = null; + }, + setSelectedDate: (state, action: PayloadAction) => { + state.selectedDate = action.payload; + }, + setSelectedProduct: (state, action: PayloadAction) => { + state.selectedProduct = action.payload; + }, + setSelectedLocation: (state, action: PayloadAction) => { + state.selectedLocation = action.payload; + }, + addForecast: (state, action: PayloadAction) => { + state.forecasts.unshift(action.payload); + }, + updateModelAccuracy: (state, action: PayloadAction) => { + state.modelAccuracy = action.payload; + }, + setCurrentWeather: (state, action: PayloadAction) => { + state.currentWeather = action.payload; + }, + markAlertAsRead: (state, action: PayloadAction) => { + const alert = state.alerts.find(a => a.id === action.payload); + if (alert) { + alert.acknowledged = true; + } + }, + clearForecasts: (state) => { + state.forecasts = []; + state.todayForecasts = []; + }, + }, + extraReducers: (builder) => { + // Generate single forecast + builder + .addCase(generateForecast.pending, (state) => { + state.isGeneratingForecast = true; + state.error = null; + }) + .addCase(generateForecast.fulfilled, (state, action) => { + state.isGeneratingForecast = false; + state.forecasts.unshift(action.payload.forecast); + state.lastUpdated = new Date().toISOString(); + }) + .addCase(generateForecast.rejected, (state, action) => { + state.isGeneratingForecast = false; + state.error = action.error.message || 'Failed to generate forecast'; + }) + + // Generate batch forecast + builder + .addCase(generateBatchForecast.pending, (state) => { + state.isGeneratingForecast = true; + state.error = null; + }) + .addCase(generateBatchForecast.fulfilled, (state, action) => { + state.isGeneratingForecast = false; + if (action.payload.forecasts) { + state.forecasts = [...action.payload.forecasts, ...state.forecasts]; + } + state.lastUpdated = new Date().toISOString(); + }) + .addCase(generateBatchForecast.rejected, (state, action) => { + state.isGeneratingForecast = false; + state.error = action.error.message || 'Failed to generate batch forecast'; + }) + + // Fetch all forecasts + builder + .addCase(fetchForecasts.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(fetchForecasts.fulfilled, (state, action) => { + state.isLoading = false; + state.forecasts = action.payload.forecasts || []; + state.lastUpdated = new Date().toISOString(); + }) + .addCase(fetchForecasts.rejected, (state, action) => { + state.isLoading = false; + state.error = action.error.message || 'Failed to fetch forecasts'; + }) + + // Fetch today's forecasts + builder + .addCase(fetchTodayForecasts.pending, (state) => { + state.isLoading = true; + }) + .addCase(fetchTodayForecasts.fulfilled, (state, action) => { + state.isLoading = false; + // Convert API forecasts to QuickForecast format + state.todayForecasts = (action.payload.forecasts || []).map((forecast: Forecast) => ({ + product: forecast.product_name, + predicted: Math.round(forecast.predicted_demand), + confidence: forecast.confidence_level > 0.8 ? 'high' : + forecast.confidence_level > 0.6 ? 'medium' : 'low', + change: 0, // Would need historical data to calculate + factors: forecast.features_used ? Object.keys(forecast.features_used) : [], + weatherImpact: forecast.weather_description, + })); + }) + .addCase(fetchTodayForecasts.rejected, (state, action) => { + state.isLoading = false; + state.error = action.error.message || 'Failed to fetch today\'s forecasts'; + }) + + // Fetch alerts + builder + .addCase(fetchForecastAlerts.pending, (state) => { + state.isFetchingAlerts = true; + }) + .addCase(fetchForecastAlerts.fulfilled, (state, action) => { + state.isFetchingAlerts = false; + state.alerts = action.payload.alerts || []; + }) + .addCase(fetchForecastAlerts.rejected, (state, action) => { + state.isFetchingAlerts = false; + state.error = action.error.message || 'Failed to fetch alerts'; + }) + + // Acknowledge alert + builder + .addCase(acknowledgeAlert.fulfilled, (state, action) => { + const alert = state.alerts.find(a => a.id === action.payload.alertId); + if (alert) { + alert.acknowledged = true; + } + }) + + // Fetch weather context + builder + .addCase(fetchWeatherContext.fulfilled, (state, action) => { + state.currentWeather = action.payload.weather; + }) + .addCase(fetchWeatherContext.rejected, (state, action) => { + console.warn('Failed to fetch weather data:', action.error.message); + // Don't set error state for weather as it's non-critical + }); + }, +}); + +export const { + clearError, + setSelectedDate, + setSelectedProduct, + setSelectedLocation, + addForecast, + updateModelAccuracy, + setCurrentWeather, + markAlertAsRead, + clearForecasts, +} = forecastSlice.actions; + +export default forecastSlice.reducer; + +// Selectors +export const selectForecasts = (state: { forecast: ForecastState }) => state.forecast.forecasts; +export const selectTodayForecasts = (state: { forecast: ForecastState }) => state.forecast.todayForecasts; +export const selectForecastAlerts = (state: { forecast: ForecastState }) => state.forecast.alerts; +export const selectForecastLoading = (state: { forecast: ForecastState }) => state.forecast.isLoading; +export const selectForecastGenerating = (state: { forecast: ForecastState }) => state.forecast.isGeneratingForecast; +export const selectForecastError = (state: { forecast: ForecastState }) => state.forecast.error; +export const selectForecastFilters = (state: { forecast: ForecastState }) => ({ + selectedDate: state.forecast.selectedDate, + selectedProduct: state.forecast.selectedProduct, + selectedLocation: state.forecast.selectedLocation, +}); +export const selectCurrentWeather = (state: { forecast: ForecastState }) => state.forecast.currentWeather; +export const selectModelAccuracy = (state: { forecast: ForecastState }) => state.forecast.modelAccuracy; +export const selectUnacknowledgedAlerts = (state: { forecast: ForecastState }) => + state.forecast.alerts.filter(alert => !alert.acknowledged); \ No newline at end of file diff --git a/frontend/src/store/slices/tenantSlice.ts b/frontend/src/store/slices/tenantSlice.ts index e69de29b..1b1596f3 100644 --- a/frontend/src/store/slices/tenantSlice.ts +++ b/frontend/src/store/slices/tenantSlice.ts @@ -0,0 +1,318 @@ +// src/store/slices/tenantSlice.ts +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; + +export interface Tenant { + id: string; + name: string; + address: string; + business_type: 'individual' | 'central_workshop'; + coordinates?: { + lat: number; + lng: number; + }; + products: string[]; + created_at: string; + updated_at: string; + owner_id: string; + settings?: { + operating_hours?: { + open: string; + close: string; + }; + operating_days?: number[]; + timezone?: string; + currency?: string; + }; +} + +export interface TenantMember { + id: string; + user_id: string; + tenant_id: string; + role: 'owner' | 'manager' | 'employee'; + user: { + id: string; + email: string; + full_name: string; + }; +} + +interface TenantState { + currentTenant: Tenant | null; + tenants: Tenant[]; + members: TenantMember[]; + isLoading: boolean; + error: string | null; + + // Upload states + isUploadingData: boolean; + uploadProgress: number; + + // Training states + isTraining: boolean; + trainingStatus: 'idle' | 'starting' | 'in_progress' | 'completed' | 'failed'; + trainingProgress: number; +} + +const initialState: TenantState = { + currentTenant: null, + tenants: [], + members: [], + isLoading: false, + error: null, + isUploadingData: false, + uploadProgress: 0, + isTraining: false, + trainingStatus: 'idle', + trainingProgress: 0, +}; + +// Async thunks for API calls +export const createTenant = createAsyncThunk( + 'tenant/create', + async (tenantData: { + name: string; + address: string; + business_type: 'individual' | 'central_workshop'; + coordinates?: { lat: number; lng: number }; + products: string[]; + }) => { + const response = await fetch('/api/v1/tenants/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`, + }, + body: JSON.stringify(tenantData), + }); + + if (!response.ok) { + throw new Error('Failed to create tenant'); + } + + return response.json(); + } +); + +export const fetchCurrentTenant = createAsyncThunk( + 'tenant/fetchCurrent', + async (tenantId: string) => { + const response = await fetch(`/api/v1/tenants/${tenantId}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch tenant'); + } + + return response.json(); + } +); + +export const uploadSalesData = createAsyncThunk( + 'tenant/uploadSalesData', + async ({ tenantId, file }: { tenantId: string; file: File }, { dispatch }) => { + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch(`/api/v1/tenants/${tenantId}/data/upload`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`, + }, + body: formData, + }); + + if (!response.ok) { + throw new Error('Failed to upload sales data'); + } + + return response.json(); + } +); + +export const startTraining = createAsyncThunk( + 'tenant/startTraining', + async ({ tenantId, products }: { tenantId: string; products: string[] }) => { + const response = await fetch(`/api/v1/tenants/${tenantId}/training/start`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`, + }, + body: JSON.stringify({ products }), + }); + + if (!response.ok) { + throw new Error('Failed to start training'); + } + + return response.json(); + } +); + +export const fetchTenantMembers = createAsyncThunk( + 'tenant/fetchMembers', + async (tenantId: string) => { + const response = await fetch(`/api/v1/tenants/${tenantId}/members`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch tenant members'); + } + + return response.json(); + } +); + +const tenantSlice = createSlice({ + name: 'tenant', + initialState, + reducers: { + clearError: (state) => { + state.error = null; + }, + setCurrentTenant: (state, action: PayloadAction) => { + state.currentTenant = action.payload; + }, + updateTenantSettings: (state, action: PayloadAction>) => { + if (state.currentTenant) { + state.currentTenant.settings = { + ...state.currentTenant.settings, + ...action.payload, + }; + } + }, + setUploadProgress: (state, action: PayloadAction) => { + state.uploadProgress = action.payload; + }, + setTrainingProgress: (state, action: PayloadAction) => { + state.trainingProgress = action.payload; + }, + resetUploadState: (state) => { + state.isUploadingData = false; + state.uploadProgress = 0; + }, + resetTrainingState: (state) => { + state.isTraining = false; + state.trainingStatus = 'idle'; + state.trainingProgress = 0; + }, + }, + extraReducers: (builder) => { + // Create tenant + builder + .addCase(createTenant.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(createTenant.fulfilled, (state, action) => { + state.isLoading = false; + state.currentTenant = action.payload.tenant; + state.tenants.push(action.payload.tenant); + }) + .addCase(createTenant.rejected, (state, action) => { + state.isLoading = false; + state.error = action.error.message || 'Failed to create tenant'; + }) + + // Fetch current tenant + builder + .addCase(fetchCurrentTenant.pending, (state) => { + state.isLoading = true; + state.error = null; + }) + .addCase(fetchCurrentTenant.fulfilled, (state, action) => { + state.isLoading = false; + state.currentTenant = action.payload.tenant; + }) + .addCase(fetchCurrentTenant.rejected, (state, action) => { + state.isLoading = false; + state.error = action.error.message || 'Failed to fetch tenant'; + }) + + // Upload sales data + builder + .addCase(uploadSalesData.pending, (state) => { + state.isUploadingData = true; + state.uploadProgress = 0; + state.error = null; + }) + .addCase(uploadSalesData.fulfilled, (state, action) => { + state.isUploadingData = false; + state.uploadProgress = 100; + // Update tenant with upload info if needed + }) + .addCase(uploadSalesData.rejected, (state, action) => { + state.isUploadingData = false; + state.uploadProgress = 0; + state.error = action.error.message || 'Failed to upload sales data'; + }) + + // Start training + builder + .addCase(startTraining.pending, (state) => { + state.isTraining = true; + state.trainingStatus = 'starting'; + state.trainingProgress = 0; + state.error = null; + }) + .addCase(startTraining.fulfilled, (state, action) => { + state.trainingStatus = 'in_progress'; + state.trainingProgress = 10; // Initial progress + // Note: Training completion should be handled via WebSocket or polling + }) + .addCase(startTraining.rejected, (state, action) => { + state.isTraining = false; + state.trainingStatus = 'failed'; + state.error = action.error.message || 'Failed to start training'; + }) + + // Fetch tenant members + builder + .addCase(fetchTenantMembers.pending, (state) => { + state.isLoading = true; + }) + .addCase(fetchTenantMembers.fulfilled, (state, action) => { + state.isLoading = false; + state.members = action.payload.members; + }) + .addCase(fetchTenantMembers.rejected, (state, action) => { + state.isLoading = false; + state.error = action.error.message || 'Failed to fetch members'; + }); + }, +}); + +export const { + clearError, + setCurrentTenant, + updateTenantSettings, + setUploadProgress, + setTrainingProgress, + resetUploadState, + resetTrainingState, +} = tenantSlice.actions; + +export default tenantSlice.reducer; + +// Selectors +export const selectCurrentTenant = (state: { tenant: TenantState }) => state.tenant.currentTenant; +export const selectTenants = (state: { tenant: TenantState }) => state.tenant.tenants; +export const selectTenantMembers = (state: { tenant: TenantState }) => state.tenant.members; +export const selectTenantLoading = (state: { tenant: TenantState }) => state.tenant.isLoading; +export const selectTenantError = (state: { tenant: TenantState }) => state.tenant.error; +export const selectUploadStatus = (state: { tenant: TenantState }) => ({ + isUploading: state.tenant.isUploadingData, + progress: state.tenant.uploadProgress, +}); +export const selectTrainingStatus = (state: { tenant: TenantState }) => ({ + isTraining: state.tenant.isTraining, + status: state.tenant.trainingStatus, + progress: state.tenant.trainingProgress, +}); \ No newline at end of file