first commit
This commit is contained in:
706
frontend/src/pages/dashboard/index.tsx
Normal file
706
frontend/src/pages/dashboard/index.tsx
Normal file
@@ -0,0 +1,706 @@
|
||||
// frontend/src/pages/dashboard/index.tsx (Fixed version)
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import ForecastChart from '../../components/charts/ForecastChart';
|
||||
import dashboardApi from '../../api/dashboardApi';
|
||||
import {
|
||||
ChartBarIcon,
|
||||
CalendarIcon,
|
||||
CloudIcon,
|
||||
TruckIcon,
|
||||
ExclamationTriangleIcon,
|
||||
CheckCircleIcon,
|
||||
ArrowTrendingUpIcon,
|
||||
ArrowTrendingDownIcon,
|
||||
CogIcon,
|
||||
BellIcon,
|
||||
UserCircleIcon,
|
||||
Bars3Icon,
|
||||
XMarkIcon,
|
||||
ArrowPathIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarElement,
|
||||
} from 'chart.js';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import { format, subDays } from 'date-fns';
|
||||
import { es } from 'date-fns/locale';
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarElement
|
||||
);
|
||||
|
||||
interface SalesRecord {
|
||||
id: string;
|
||||
product_name: string;
|
||||
quantity_sold: number;
|
||||
revenue: number;
|
||||
sale_date: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface ForecastRecord {
|
||||
date: string;
|
||||
product_name: string;
|
||||
predicted_quantity: number;
|
||||
confidence_lower: number;
|
||||
confidence_upper: number;
|
||||
}
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const { user, tenant, logout } = useAuth();
|
||||
const [currentDate] = useState(new Date());
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
|
||||
const [salesHistory, setSalesHistory] = useState<SalesRecord[]>([]);
|
||||
const [panForecast, setPanForecast] = useState<ForecastRecord[]>([]);
|
||||
const [croissantForecast, setCroissantForecast] = useState<ForecastRecord[]>([]);
|
||||
const [cafeForecast, setCafeForecast] = useState<ForecastRecord[]>([]);
|
||||
const [bocadilloForecast, setBocadilloForecast] = useState<ForecastRecord[]>([]);
|
||||
|
||||
const fetchDashboardData = async (skipLoading = false) => {
|
||||
if (!skipLoading) {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
console.log('Fetching dashboard data...');
|
||||
|
||||
const endDate = format(currentDate, 'yyyy-MM-dd');
|
||||
const startDate = format(subDays(currentDate, 30), 'yyyy-MM-dd'); // Fetch 30 days instead of 7
|
||||
|
||||
console.log('Fetching sales history from', startDate, 'to', endDate);
|
||||
|
||||
// Fetch sales history
|
||||
const fetchedSales = await dashboardApi.getSalesHistory(startDate, endDate);
|
||||
console.log('Fetched sales:', fetchedSales);
|
||||
setSalesHistory(fetchedSales || []);
|
||||
|
||||
// Fetch forecasts for each product
|
||||
const products = ['Pan', 'Croissant', 'Cafe', 'Bocadillo'];
|
||||
|
||||
for (const product of products) {
|
||||
try {
|
||||
console.log(`Fetching forecast for ${product}`);
|
||||
const forecast = await dashboardApi.getProductForecast(product, 14);
|
||||
console.log(`Forecast for ${product}:`, forecast);
|
||||
|
||||
switch (product) {
|
||||
case 'Pan':
|
||||
setPanForecast(forecast || []);
|
||||
break;
|
||||
case 'Croissant':
|
||||
setCroissantForecast(forecast || []);
|
||||
break;
|
||||
case 'Cafe':
|
||||
setCafeForecast(forecast || []);
|
||||
break;
|
||||
case 'Bocadillo':
|
||||
setBocadilloForecast(forecast || []);
|
||||
break;
|
||||
}
|
||||
} catch (forecastError) {
|
||||
console.error(`Error fetching forecast for ${product}:`, forecastError);
|
||||
// Continue with other products if one fails
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Dashboard data loaded successfully');
|
||||
setRetryCount(0);
|
||||
|
||||
} catch (err: any) {
|
||||
console.error("Error fetching dashboard data:", err);
|
||||
|
||||
let errorMessage = "No se pudieron cargar los datos del dashboard.";
|
||||
|
||||
if (err.response?.status === 401) {
|
||||
errorMessage = "Sesión expirada. Por favor, inicia sesión nuevamente.";
|
||||
// Optionally logout the user
|
||||
// logout();
|
||||
} else if (err.response?.status === 403) {
|
||||
errorMessage = "No tienes permisos para acceder a estos datos.";
|
||||
} else if (err.response?.status === 404) {
|
||||
errorMessage = "No se encontraron datos. Esto puede ser normal para nuevos usuarios.";
|
||||
} else if (err.response?.status >= 500) {
|
||||
errorMessage = "Error del servidor. Por favor, inténtalo más tarde.";
|
||||
} else if (err.code === 'NETWORK_ERROR' || !err.response) {
|
||||
errorMessage = "Error de conexión. Verifica tu conexión a internet.";
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
|
||||
// Auto-retry logic for temporary network errors
|
||||
if (retryCount < 3 && (!err.response || err.response.status >= 500)) {
|
||||
console.log(`Retrying in 2 seconds... (attempt ${retryCount + 1}/3)`);
|
||||
setTimeout(() => {
|
||||
setRetryCount(prev => prev + 1);
|
||||
fetchDashboardData(true);
|
||||
}, 2000);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
setRetryCount(0);
|
||||
fetchDashboardData();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user && tenant) {
|
||||
console.log('User and tenant loaded, fetching dashboard data');
|
||||
fetchDashboardData();
|
||||
} else {
|
||||
console.log('User or tenant not available yet');
|
||||
}
|
||||
}, [user, tenant]);
|
||||
|
||||
const salesChartData = useMemo(() => {
|
||||
if (!salesHistory.length) {
|
||||
return {
|
||||
labels: [],
|
||||
datasets: []
|
||||
};
|
||||
}
|
||||
|
||||
const salesByDateAndProduct: { [date: string]: { [product: string]: number } } = {};
|
||||
salesHistory.forEach(sale => {
|
||||
const date = format(new Date(sale.sale_date), 'yyyy-MM-dd');
|
||||
if (!salesByDateAndProduct[date]) {
|
||||
salesByDateAndProduct[date] = {};
|
||||
}
|
||||
salesByDateAndProduct[date][sale.product_name] = (salesByDateAndProduct[date][sale.product_name] || 0) + sale.quantity_sold;
|
||||
});
|
||||
|
||||
const uniqueDates = Object.keys(salesByDateAndProduct).sort();
|
||||
const allProductNames = Array.from(new Set(salesHistory.map(s => s.product_name)));
|
||||
|
||||
const datasets = allProductNames.map(productName => {
|
||||
const productData = uniqueDates.map(date => salesByDateAndProduct[date][productName] || 0);
|
||||
|
||||
let borderColor = '';
|
||||
let backgroundColor = '';
|
||||
switch(productName.toLowerCase()) {
|
||||
case 'pan':
|
||||
borderColor = 'rgb(255, 99, 132)';
|
||||
backgroundColor = 'rgba(255, 99, 132, 0.5)';
|
||||
break;
|
||||
case 'croissant':
|
||||
borderColor = 'rgb(53, 162, 235)';
|
||||
backgroundColor = 'rgba(53, 162, 235, 0.5)';
|
||||
break;
|
||||
case 'cafe':
|
||||
borderColor = 'rgb(75, 192, 192)';
|
||||
backgroundColor = 'rgba(75, 192, 192, 0.5)';
|
||||
break;
|
||||
case 'bocadillo':
|
||||
borderColor = 'rgb(153, 102, 255)';
|
||||
backgroundColor = 'rgba(153, 102, 255, 0.5)';
|
||||
break;
|
||||
default:
|
||||
borderColor = `hsl(${Math.random() * 360}, 70%, 50%)`;
|
||||
backgroundColor = `hsla(${Math.random() * 360}, 70%, 50%, 0.5)`;
|
||||
}
|
||||
|
||||
return {
|
||||
label: `Ventas de ${productName}`,
|
||||
data: productData,
|
||||
borderColor,
|
||||
backgroundColor,
|
||||
tension: 0.1,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
labels: uniqueDates.map(d => format(new Date(d), 'dd MMM', { locale: es })),
|
||||
datasets: datasets,
|
||||
};
|
||||
}, [salesHistory]);
|
||||
|
||||
const demoForecastData = useMemo(() => {
|
||||
if (!panForecast.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return panForecast.map(f => ({
|
||||
date: format(new Date(f.date), 'yyyy-MM-dd'),
|
||||
predicted_quantity: f.predicted_quantity,
|
||||
confidence_lower: f.confidence_lower,
|
||||
confidence_upper: f.confidence_upper,
|
||||
}));
|
||||
}, [panForecast]);
|
||||
|
||||
const productPredictions = useMemo(() => {
|
||||
const today = format(currentDate, 'yyyy-MM-dd');
|
||||
const todaySales = salesHistory.filter(s => format(new Date(s.sale_date), 'yyyy-MM-dd') === today);
|
||||
|
||||
const currentSalesByProduct: { [product: string]: number } = {};
|
||||
todaySales.forEach(sale => {
|
||||
currentSalesByProduct[sale.product_name] = (currentSalesByProduct[sale.product_name] || 0) + sale.quantity_sold;
|
||||
});
|
||||
|
||||
const allForecasts = [
|
||||
...panForecast,
|
||||
...croissantForecast,
|
||||
...cafeForecast,
|
||||
...bocadilloForecast
|
||||
];
|
||||
|
||||
if (!allForecasts.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const uniqueProductsInForecasts = Array.from(new Set(allForecasts.map(f => f.product_name)));
|
||||
|
||||
const predictions = uniqueProductsInForecasts.map((productName, index) => {
|
||||
const productTodayForecast = allForecasts.find(f =>
|
||||
f.product_name === productName && format(new Date(f.date), 'yyyy-MM-dd') === today
|
||||
);
|
||||
|
||||
const predicted = productTodayForecast?.predicted_quantity || 0;
|
||||
const current = currentSalesByProduct[productName] || 0;
|
||||
|
||||
let status: 'good' | 'warning' | 'bad' = 'good';
|
||||
if (predicted > 0) {
|
||||
const percentageAchieved = (current / predicted) * 100;
|
||||
if (percentageAchieved < 50) {
|
||||
status = 'bad';
|
||||
} else if (percentageAchieved < 90) {
|
||||
status = 'warning';
|
||||
}
|
||||
} else if (current > 0) {
|
||||
status = 'good';
|
||||
}
|
||||
|
||||
return {
|
||||
id: index + 1,
|
||||
product: productName,
|
||||
predicted: Math.round(predicted),
|
||||
current: current,
|
||||
status: status,
|
||||
};
|
||||
}).filter(p => p.predicted > 0 || p.current > 0);
|
||||
|
||||
return predictions;
|
||||
}, [salesHistory, panForecast, croissantForecast, cafeForecast, bocadilloForecast, currentDate]);
|
||||
|
||||
const kpiData = useMemo(() => {
|
||||
if (!salesHistory.length) {
|
||||
return {
|
||||
totalSalesToday: 0,
|
||||
salesChange: 0,
|
||||
totalProductsSoldToday: 0,
|
||||
productsSoldChange: 0,
|
||||
wasteToday: 0,
|
||||
wasteChange: 0,
|
||||
totalPredictedValueToday: 0,
|
||||
predictedValueChange: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const today = format(currentDate, 'yyyy-MM-dd');
|
||||
const yesterday = format(subDays(currentDate, 1), 'yyyy-MM-dd');
|
||||
|
||||
const salesToday = salesHistory.filter(s => format(new Date(s.sale_date), 'yyyy-MM-dd') === today);
|
||||
const salesYesterday = salesHistory.filter(s => format(new Date(s.sale_date), 'yyyy-MM-dd') === yesterday);
|
||||
|
||||
const totalSalesToday = salesToday.reduce((sum, s) => sum + s.revenue, 0);
|
||||
const totalProductsSoldToday = salesToday.reduce((sum, s) => sum + s.quantity_sold, 0);
|
||||
const totalSalesYesterday = salesYesterday.reduce((sum, s) => sum + s.revenue, 0);
|
||||
const totalProductsSoldYesterday = salesYesterday.reduce((sum, s) => sum + s.quantity_sold, 0);
|
||||
|
||||
const wasteToday = 15; // Mock data
|
||||
const wasteLastWeek = 15.3; // Mock data
|
||||
|
||||
const salesChange = totalSalesYesterday > 0 ? ((totalSalesToday - totalSalesYesterday) / totalSalesYesterday) * 100 : (totalSalesToday > 0 ? 100 : 0);
|
||||
const productsSoldChange = totalProductsSoldYesterday > 0 ? ((totalProductsSoldToday - totalProductsSoldYesterday) / totalProductsSoldYesterday) * 100 : (totalProductsSoldToday > 0 ? 100 : 0);
|
||||
const wasteChange = wasteLastWeek > 0 ? ((wasteToday - wasteLastWeek) / wasteLastWeek) * 100 : (wasteToday > 0 ? 100 : 0);
|
||||
|
||||
const totalPredictedValueToday = productPredictions.reduce((sum, p) => sum + p.predicted, 0) * 1.5;
|
||||
const predictedValueChange = totalPredictedValueToday > 0 ? ((totalSalesToday - totalPredictedValueToday) / totalPredictedValueToday) * 100 : (totalSalesToday > 0 ? 100 : 0);
|
||||
|
||||
return {
|
||||
totalSalesToday,
|
||||
salesChange,
|
||||
totalProductsSoldToday,
|
||||
productsSoldChange,
|
||||
wasteToday,
|
||||
wasteChange,
|
||||
totalPredictedValueToday,
|
||||
predictedValueChange,
|
||||
};
|
||||
}, [salesHistory, productPredictions, currentDate]);
|
||||
|
||||
const salesChartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top' as const,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Histórico de Ventas Recientes',
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Cantidad Vendida (uds)',
|
||||
},
|
||||
},
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Fecha',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-orange-600 mx-auto"></div>
|
||||
<p className="mt-4 text-lg text-gray-700">Cargando datos del dashboard...</p>
|
||||
{retryCount > 0 && (
|
||||
<p className="mt-2 text-sm text-gray-500">Reintentando... ({retryCount}/3)</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="text-center bg-white p-8 rounded-lg shadow-md max-w-md mx-4">
|
||||
<ExclamationTriangleIcon className="h-12 w-12 text-red-500 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">Error al cargar datos</h2>
|
||||
<p className="text-gray-600 mb-6">{error}</p>
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={handleRetry}
|
||||
className="w-full bg-orange-600 text-white px-4 py-2 rounded-lg hover:bg-orange-700 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<ArrowPathIcon className="h-5 w-5 mr-2" />
|
||||
Reintentar
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="w-full bg-gray-200 text-gray-800 px-4 py-2 rounded-lg hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
Recargar página
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 flex">
|
||||
{/* Overlay for mobile sidebar */}
|
||||
{isSidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-30 lg:hidden"
|
||||
onClick={() => setIsSidebarOpen(false)}
|
||||
></div>
|
||||
)}
|
||||
|
||||
{/* Sidebar - Made responsive */}
|
||||
<aside
|
||||
className={`fixed inset-y-0 left-0 z-40 bg-white shadow-md overflow-y-auto transform transition-transform duration-300 ease-in-out
|
||||
${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'} lg:translate-x-0 lg:static lg:inset-0 w-64`}
|
||||
>
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-gray-900">PanIA</h2>
|
||||
<button
|
||||
className="lg:hidden p-2"
|
||||
onClick={() => setIsSidebarOpen(false)}
|
||||
>
|
||||
<XMarkIcon className="h-6 w-6 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-1">{tenant?.name}</p>
|
||||
</div>
|
||||
|
||||
<nav className="mt-6">
|
||||
<div className="px-6 py-3">
|
||||
<div className="flex items-center text-orange-600">
|
||||
<ChartBarIcon className="h-5 w-5 mr-3" />
|
||||
<span className="font-medium">Dashboard</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 px-6">
|
||||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
||||
Herramientas
|
||||
</h3>
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="flex items-center text-gray-700 hover:bg-gray-100 rounded-lg p-2 cursor-pointer">
|
||||
<CalendarIcon className="h-5 w-5 mr-3" />
|
||||
<span className="text-sm">Pronósticos</span>
|
||||
</div>
|
||||
<div className="flex items-center text-gray-700 hover:bg-gray-100 rounded-lg p-2 cursor-pointer">
|
||||
<CloudIcon className="h-5 w-5 mr-3" />
|
||||
<span className="text-sm">Datos Climáticos</span>
|
||||
</div>
|
||||
<div className="flex items-center text-gray-700 hover:bg-gray-100 rounded-lg p-2 cursor-pointer">
|
||||
<TruckIcon className="h-5 w-5 mr-3" />
|
||||
<span className="text-sm">Pedidos</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 px-6">
|
||||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
||||
Configuración
|
||||
</h3>
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="flex items-center text-gray-700 hover:bg-gray-100 rounded-lg p-2 cursor-pointer">
|
||||
<CogIcon className="h-5 w-5 mr-3" />
|
||||
<span className="text-sm">Ajustes</span>
|
||||
</div>
|
||||
<div className="flex items-center text-gray-700 hover:bg-gray-100 rounded-lg p-2 cursor-pointer">
|
||||
<UserCircleIcon className="h-5 w-5 mr-3" />
|
||||
<span className="text-sm">Perfil</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="absolute bottom-0 w-full p-6 border-t border-gray-200">
|
||||
<div className="flex items-center">
|
||||
<div className="w-8 h-8 bg-orange-600 rounded-full flex items-center justify-center">
|
||||
<span className="text-white text-sm font-medium">
|
||||
{user?.full_name?.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium text-gray-900">{user?.full_name}</p>
|
||||
<p className="text-xs text-gray-500">{user?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="mt-3 w-full text-left text-sm text-red-600 hover:text-red-700"
|
||||
>
|
||||
Cerrar sesión
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm border-b border-gray-200 flex items-center justify-between px-4 py-3">
|
||||
<button
|
||||
className="lg:hidden p-2"
|
||||
onClick={() => setIsSidebarOpen(true)}
|
||||
>
|
||||
<Bars3Icon className="h-6 w-6 text-gray-500" />
|
||||
</button>
|
||||
<h1 className="text-xl font-semibold text-gray-900">Dashboard</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-gray-600 text-sm">
|
||||
{format(currentDate, 'dd MMMM yyyy', { locale: es })}
|
||||
</span>
|
||||
<button onClick={handleRetry} className="p-2 text-gray-500 hover:text-gray-700">
|
||||
<ArrowPathIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<BellIcon className="h-6 w-6 text-gray-500 cursor-pointer" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
||||
{/* Show a notice if no data is available */}
|
||||
{!salesHistory.length && !loading && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-6">
|
||||
<div className="flex items-center">
|
||||
<ExclamationTriangleIcon className="h-6 w-6 text-blue-600 mr-3" />
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-blue-900">
|
||||
No hay datos disponibles
|
||||
</h3>
|
||||
<p className="text-blue-700 mt-1">
|
||||
Parece que aún no tienes datos de ventas. Los datos se generarán automáticamente
|
||||
después de completar el proceso de configuración.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium text-gray-500">Ventas Hoy</h3>
|
||||
<ChartBarIcon className="h-6 w-6 text-orange-500" />
|
||||
</div>
|
||||
<p className="mt-1 text-3xl font-semibold text-gray-900">
|
||||
€ {kpiData.totalSalesToday.toFixed(2)}
|
||||
</p>
|
||||
<p className={`mt-2 text-sm flex items-center ${kpiData.salesChange >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{kpiData.salesChange >= 0 ? <ArrowTrendingUpIcon className="h-4 w-4 mr-1" /> : <ArrowTrendingDownIcon className="h-4 w-4 mr-1" />}
|
||||
{kpiData.salesChange.toFixed(1)}% desde ayer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium text-gray-500">Productos Vendidos</h3>
|
||||
<CheckCircleIcon className="h-6 w-6 text-green-500" />
|
||||
</div>
|
||||
<p className="mt-1 text-3xl font-semibold text-gray-900">
|
||||
{kpiData.totalProductsSoldToday} uds.
|
||||
</p>
|
||||
<p className={`mt-2 text-sm flex items-center ${kpiData.productsSoldChange >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{kpiData.productsSoldChange >= 0 ? <ArrowTrendingUpIcon className="h-4 w-4 mr-1" /> : <ArrowTrendingDownIcon className="h-4 w-4 mr-1" />}
|
||||
{kpiData.productsSoldChange.toFixed(1)}% desde ayer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium text-gray-500">Desperdicio</h3>
|
||||
<ExclamationTriangleIcon className="h-6 w-6 text-red-500" />
|
||||
</div>
|
||||
<p className="mt-1 text-3xl font-semibold text-gray-900">
|
||||
{kpiData.wasteToday} kg
|
||||
</p>
|
||||
<p className={`mt-2 text-sm flex items-center ${kpiData.wasteChange <= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{kpiData.wasteChange <= 0 ? <ArrowTrendingDownIcon className="h-4 w-4 mr-1" /> : <ArrowTrendingUpIcon className="h-4 w-4 mr-1" />}
|
||||
{kpiData.wasteChange.toFixed(1)}% desde la semana pasada
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium text-gray-500">Valor Pronosticado</h3>
|
||||
<ChartBarIcon className="h-6 w-6 text-blue-500" />
|
||||
</div>
|
||||
<p className="mt-1 text-3xl font-semibold text-gray-900">
|
||||
€ {kpiData.totalPredictedValueToday.toFixed(2)}
|
||||
</p>
|
||||
<p className={`mt-2 text-sm flex items-center ${kpiData.predictedValueChange >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{kpiData.predictedValueChange >= 0 ? <ArrowTrendingUpIcon className="h-4 w-4 mr-1" /> : <ArrowTrendingDownIcon className="h-4 w-4 mr-1" />}
|
||||
{kpiData.predictedValueChange.toFixed(1)}% sobre predicción
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts Section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Ventas por Día</h2>
|
||||
{salesHistory.length > 0 ? (
|
||||
<div style={{ height: '350px' }}>
|
||||
<Line data={salesChartData} options={salesChartOptions} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-80 flex items-center justify-center text-gray-500">
|
||||
<div className="text-center">
|
||||
<ChartBarIcon className="h-12 w-12 mx-auto mb-4 text-gray-400" />
|
||||
<p>No hay datos de ventas disponibles</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Pronóstico de Demanda (Pan)</h2>
|
||||
{demoForecastData.length > 0 ? (
|
||||
<div style={{ height: '350px' }}>
|
||||
<ForecastChart data={demoForecastData} productName="Pan" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-80 flex items-center justify-center text-gray-500">
|
||||
<div className="text-center">
|
||||
<CalendarIcon className="h-12 w-12 mx-auto mb-4 text-gray-400" />
|
||||
<p>No hay pronósticos disponibles</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Predictions */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Predicciones de Productos Clave</h2>
|
||||
{productPredictions.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{productPredictions.map((prediction) => (
|
||||
<div key={prediction.id} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-sm font-medium text-gray-900">{prediction.product}</h4>
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
prediction.status === 'good' ? 'bg-green-400' :
|
||||
prediction.status === 'warning' ? 'bg-yellow-400' : 'bg-red-400'
|
||||
}`}></div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">Predicción:</span>
|
||||
<span className="font-medium">{prediction.predicted} uds</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">Actual:</span>
|
||||
<span className="font-medium">{prediction.current} uds</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${
|
||||
prediction.status === 'good' ? 'bg-green-400' :
|
||||
prediction.status === 'warning' ? 'bg-yellow-400' : 'bg-red-400'
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.min((prediction.current / prediction.predicted) * 100, 100)}%`
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<div className="text-center">
|
||||
<TruckIcon className="h-12 w-12 mx-auto mb-4 text-gray-400" />
|
||||
<p>No hay predicciones disponibles</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
Reference in New Issue
Block a user