Fix new Frontend 2
This commit is contained in:
@@ -1,8 +1,4 @@
|
||||
const [appState, setAppState] = useState<AppState>({
|
||||
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
|
||||
|
||||
@@ -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(
|
||||
<React.StrictMode>
|
||||
<App /> 👈 Renders the App component
|
||||
<App />
|
||||
</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 toast from 'react-hot-toast';
|
||||
|
||||
|
||||
@@ -27,6 +27,27 @@ interface MetricsData {
|
||||
}
|
||||
|
||||
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 = [
|
||||
{ name: 'Croissants', quantity: 45, 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);
|
||||
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
|
||||
export default DashboardPage;
|
||||
@@ -1,4 +1,3 @@
|
||||
/ src/store/index.ts
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import authSlice from './slices/authSlice';
|
||||
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