Fix new Frontend 2

This commit is contained in:
Urtzi Alfaro
2025-08-03 19:49:57 +02:00
parent 11ab73ae97
commit 5b16545a0d
7 changed files with 782 additions and 274 deletions

View File

@@ -1,8 +1,4 @@
const [appState, setAppState] = useState<AppState>({ import React, { useState, useEffect } from 'react';
isAuthenticated: false,
isLoading: true,
user: null,
currentPage: 'landing' // 👈 Startimport React, { useState, useEffect } from 'react';
import { Toaster } from 'react-hot-toast'; import { Toaster } from 'react-hot-toast';
// Components // Components

View File

@@ -1,10 +1,10 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App.tsx' 👈 Imports from ./App.tsx import App from './App.tsx'
import './index.css' import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<App /> 👈 Renders the App component <App />
</React.StrictMode>, </React.StrictMode>,
) )

View File

@@ -1,247 +1,3 @@
{/* Register Form */}
<div className="bg-white py-8 px-6 shadow-strong rounded-3xl">
<div className="space-y-6">
{/* Full Name Field */}
<div>
<label htmlFor="fullName" className="block text-sm font-medium text-gray-700 mb-2">
Nombre completo
</label>
<input
id="fullName"
name="fullName"
type="text"
autoComplete="name"
required
value={formData.fullName}
onChange={handleInputChange}
className={`
appearance-none relative block w-full px-4 py-3 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.fullName
? 'border-red-300 bg-red-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="Tu nombre completo"
/>
{errors.fullName && (
<p className="mt-1 text-sm text-red-600">{errors.fullName}</p>
)}
</div>
{/* Email Field */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
Correo electrónico
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={formData.email}
onChange={handleInputChange}
className={`
appearance-none relative block w-full px-4 py-3 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.email
? 'border-red-300 bg-red-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="tu@panaderia.com"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div>
{/* Password Field */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
Contraseña
</label>
<div className="relative">
<input
id="password"
name="password"
type={showPassword ? 'text' : 'password'}
autoComplete="new-password"
required
value={formData.password}
onChange={handleInputChange}
className={`
appearance-none relative block w-full px-4 py-3 pr-12 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.password
? 'border-red-300 bg-red-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="••••••••"
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-5 w-5 text-gray-400 hover:text-gray-600" />
) : (
<Eye className="h-5 w-5 text-gray-400 hover:text-gray-600" />
)}
</button>
</div>
{/* Password Strength Indicator */}
{formData.password && (
<div className="mt-2">
<div className="flex space-x-1">
{[...Array(5)].map((_, i) => (
<div
key={i}
className={`h-1 flex-1 rounded ${
i < passwordStrength ? strengthColors[passwordStrength - 1] : 'bg-gray-200'
}`}
/>
))}
</div>
<p className="text-xs text-gray-600 mt-1">
Seguridad: {strengthLabels[passwordStrength - 1] || 'Muy débil'}
</p>
</div>
)}
{errors.password && (
<p className="mt-1 text-sm text-red-600">{errors.password}</p>
)}
</div>
{/* Confirm Password Field */}
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-2">
Confirmar contraseña
</label>
<div className="relative">
<input
id="confirmPassword"
name="confirmPassword"
type={showConfirmPassword ? 'text' : 'password'}
autoComplete="new-password"
required
value={formData.confirmPassword}
onChange={handleInputChange}
className={`
appearance-none relative block w-full px-4 py-3 pr-12 border rounded-xl
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:border-primary-500 focus:z-10
transition-all duration-200
${errors.confirmPassword
? 'border-red-300 bg-red-50'
: formData.confirmPassword && formData.password === formData.confirmPassword
? 'border-green-300 bg-green-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="••••••••"
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? (
<EyeOff className="h-5 w-5 text-gray-400 hover:text-gray-600" />
) : (
<Eye className="h-5 w-5 text-gray-400 hover:text-gray-600" />
)}
</button>
{formData.confirmPassword && formData.password === formData.confirmPassword && (
<div className="absolute inset-y-0 right-10 flex items-center">
<Check className="h-5 w-5 text-green-500" />
</div>
)}
</div>
{errors.confirmPassword && (
<p className="mt-1 text-sm text-red-600">{errors.confirmPassword}</p>
)}
</div>
{/* Terms and Conditions */}
<div>
<div className="flex items-start">
<input
id="acceptTerms"
name="acceptTerms"
type="checkbox"
checked={formData.acceptTerms}
onChange={handleInputChange}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded mt-0.5"
/>
<label htmlFor="acceptTerms" className="ml-2 block text-sm text-gray-700">
Acepto los{' '}
<a href="#" className="font-medium text-primary-600 hover:text-primary-500">
términos y condiciones
</a>{' '}
y la{' '}
<a href="#" className="font-medium text-primary-600 hover:text-primary-500">
política de privacidad
</a>
</label>
</div>
{errors.acceptTerms && (
<p className="mt-1 text-sm text-red-600">{errors.acceptTerms}</p>
)}
</div>
{/* Submit Button */}
<div>
<button
type="button"
onClick={handleSubmit}
disabled={isLoading}
className={`
group relative w-full flex justify-center py-3 px-4 border border-transparent
text-sm font-medium rounded-xl text-white transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500
${isLoading
? 'bg-gray-400 cursor-not-allowed'
: 'bg-primary-500 hover:bg-primary-600 hover:shadow-lg transform hover:-translate-y-0.5'
}
`}
>
{isLoading ? (
<>
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
Creando cuenta...
</>
) : (
'Crear cuenta gratis'
)}
</button>
</div>
</div>
{/* Login Link */}
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
¿Ya tienes una cuenta?{' '}
<button
onClick={onNavigateToLogin}
className="font-medium text-primary-600 hover:text-primary-500 transition-colors"
>
Inicia sesión aquí
</button>
</p>
</div>
</div>import React, { useState } from 'react';
import { Eye, EyeOff, Loader2, Check } from 'lucide-react'; import { Eye, EyeOff, Loader2, Check } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';

View File

@@ -27,6 +27,27 @@ interface MetricsData {
} }
const DashboardPage: React.FC<DashboardPageProps> = ({ user }) => { const DashboardPage: React.FC<DashboardPageProps> = ({ user }) => {
const [isLoading, setIsLoading] = useState(true);
const [weather, setWeather] = useState<WeatherData | null>(null);
const [todayForecasts, setTodayForecasts] = useState<ForecastData[]>([]);
const [metrics, setMetrics] = useState<MetricsData>({
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 = [ const topProducts = [
{ name: 'Croissants', quantity: 45, trend: 'up' }, { name: 'Croissants', quantity: 45, trend: 'up' },
{ name: 'Pan de molde', quantity: 32, trend: 'up' }, { name: 'Pan de molde', quantity: 32, trend: 'up' },
@@ -405,25 +426,4 @@ const DashboardPage: React.FC<DashboardPageProps> = ({ user }) => {
); );
}; };
export default DashboardPage; [isLoading, setIsLoading] = useState(true); export default DashboardPage;
const [weather, setWeather] = useState<WeatherData | null>(null);
const [todayForecasts, setTodayForecasts] = useState<ForecastData[]>([]);
const [metrics, setMetrics] = useState<MetricsData>({
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

View File

@@ -1,4 +1,3 @@
/ src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import authSlice from './slices/authSlice'; import authSlice from './slices/authSlice';
import tenantSlice from './slices/tenantSlice'; import tenantSlice from './slices/tenantSlice';

View File

@@ -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<string, any>;
}
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<string>) => {
state.selectedDate = action.payload;
},
setSelectedProduct: (state, action: PayloadAction<string>) => {
state.selectedProduct = action.payload;
},
setSelectedLocation: (state, action: PayloadAction<string>) => {
state.selectedLocation = action.payload;
},
addForecast: (state, action: PayloadAction<Forecast>) => {
state.forecasts.unshift(action.payload);
},
updateModelAccuracy: (state, action: PayloadAction<number>) => {
state.modelAccuracy = action.payload;
},
setCurrentWeather: (state, action: PayloadAction<ForecastState['currentWeather']>) => {
state.currentWeather = action.payload;
},
markAlertAsRead: (state, action: PayloadAction<string>) => {
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);

View File

@@ -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<Tenant>) => {
state.currentTenant = action.payload;
},
updateTenantSettings: (state, action: PayloadAction<Partial<Tenant['settings']>>) => {
if (state.currentTenant) {
state.currentTenant.settings = {
...state.currentTenant.settings,
...action.payload,
};
}
},
setUploadProgress: (state, action: PayloadAction<number>) => {
state.uploadProgress = action.payload;
},
setTrainingProgress: (state, action: PayloadAction<number>) => {
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,
});