Fix new Frontend 2
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>,
|
||||||
)
|
)
|
||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user