ASSISTANT
Claro, aquí tienes el código completo de tu componente `Dashboard` con las correcciones necesarias para asegurarse de que cada elemento en la lista de `recentOrders` tenga la propiedad `key` única. Esto se logra modificando cómo se obtiene `ordersList` al incluir `id` de los documentos de Firebase y luego usándolo correctamente en la tabla.
### Código completo corregido
```tsx
import React, { useState, useEffect } from 'react';
import { db } from '@/services/firebase';
import { collection, getDocs } from 'firebase/firestore';
import { Line, Bar, Doughnut } from 'react-chartjs-2';
import 'chart.js/auto';
const Dashboard: React.FC = () => {
const [userGrowthData, setUserGrowthData] = useState<any>(null);
const [monthlySalesData, setMonthlySalesData] = useState<any>(null);
const [userRolesData, setUserRolesData] = useState<any>(null);
const [siteTrafficData, setSiteTrafficData] = useState<any>(null);
const [recentOrders, setRecentOrders] = useState<any[]>([]);
const [kpis, setKpis] = useState<any>({});
useEffect(() => {
const fetchData = async () => {
// Fetch user growth data
const usersCollection = collection(db, 'usuarios');
const usersSnapshot = await getDocs(usersCollection);
const usersList = usersSnapshot.docs.map(doc => doc.data());
const userGrowth = usersList.reduce((acc, user) => {
if (user.fechaRegistro && user.fechaRegistro.seconds) {
const date = new Date(user.fechaRegistro.seconds * 1000).toLocaleDateString();
acc[date] = (acc[date] || 0) + 1;
}
return acc;
}, {});
setUserGrowthData({
labels: Object.keys(userGrowth),
datasets: [
{
label: 'Crecimiento de Usuarios',
data: Object.values(userGrowth),
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
},
],
});
// Fetch monthly sales data
const ordersCollection = collection(db, 'HistorialGeneralPedidos');
const ordersSnapshot = await getDocs(ordersCollection);
const ordersList = ordersSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); // Incluye los IDs únicos
const monthlySales = ordersList.reduce((acc, order) => {
if (order.fecha && order.fecha.seconds) {
const month = new Date(order.fecha.seconds * 1000).toLocaleString('default', { month: 'long' });
acc[month] = (acc[month] || 0) + order.total;
}
return acc;
}, {});
setMonthlySalesData({
labels: Object.keys(monthlySales),
datasets: [
{
label: 'Ventas Mensuales',
data: Object.values(monthlySales),
backgroundColor: 'rgba(153, 102, 255, 0.2)',
borderColor: 'rgba(153, 102, 255, 1)',
borderWidth: 1,
},
],
});
// Fetch user roles data
const userRoles = usersList.reduce((acc, user) => {
acc[user.rol] = (acc[user.rol] || 0) + 1;
return acc;
}, {});
setUserRolesData({
labels: Object.keys(userRoles),
datasets: [
{
label: 'Distribución de Roles de Usuarios',
data: Object.values(userRoles),
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)',
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)',
],
borderWidth: 1,
},
],
});
// Fetch site traffic data
const siteTraffic = ordersList.reduce((acc, order) => {
if (order.fecha && order.fecha.seconds) {
const date = new Date(order.fecha.seconds * 1000).toLocaleDateString();
acc[date] = (acc[date] || 0) + 1;
}
return acc;
}, {});
setSiteTrafficData({
labels: Object.keys(siteTraffic),
datasets: [
{
label: 'Tráfico del Sitio Web',
data: Object.values(siteTraffic),
borderColor: 'rgba(255, 159, 64, 1)',
backgroundColor: 'rgba(255, 159, 64, 0.2)',
},
],
});
// Fetch recent orders
setRecentOrders(ordersList.slice(0, 5));
// Fetch KPIs
setKpis({
totalUsers: usersList.length,
totalSales: ordersList.reduce((acc, order) => acc + order.total, 0),
pendingOrders: ordersList.filter(order => order.estado === 'Pendiente').length,
});
};
fetchData();
}, []);
return (
<div className="container mx-auto p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-4 text-gray-800">Dashboard</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<div className="bg-blue-100 p-4 rounded-lg shadow-md">
<h3 className="text-lg font-semibold text-blue-800">Usuarios Totales</h3>
<p className="text-2xl font-bold text-blue-800">{kpis.totalUsers}</p>
</div>
<div className="bg-green-100 p-4 rounded-lg shadow-md">
<h3 className="text-lg font-semibold text-green-800">Ventas Totales</h3>
<p className="text-2xl font-bold text-green-800">{kpis.totalSales}</p>
</div>
<div className="bg-yellow-100 p-4 rounded-lg shadow-md">
<h3 className="text-lg font-semibold text-yellow-800">Pedidos Pendientes</h3>
<p className="text-2xl font-bold text-yellow-800">{kpis.pendingOrders}</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div className="bg-white p-4 rounded-lg shadow-md">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Crecimiento de Usuarios</h3>
{userGrowthData && <Line data={userGrowthData} />}
</div>
<div className="bg-white p-4 rounded-lg shadow-md">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Ventas Mensuales</h3>
{monthlySalesData && <Bar data={monthlySalesData} />}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div className="bg-white p-4 rounded-lg shadow-md">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Distribución de Roles de Usuarios</h3>
{userRolesData && <Doughnut data={userRolesData} />}
</div>
<div className="bg-white p-4 rounded-lg shadow-md">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Tráfico del Sitio Web</h3>
{siteTrafficData && <Line data={siteTrafficData} />}
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow-md">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Pedidos Recientes</h3>
<table className="min-w-full bg-white rounded-lg shadow-md">
<thead>
<tr className="bg-gray-100 text-gray-600 uppercase text-sm leading-normal">
<th className="py-3 px-6 text-left">Nro Pedido</th>
<th className="py-3 px-6 text-left">Cliente</th>
<th className="py-3 px-6 text-left">Fecha</th>
<th className="py-3 px-6 text-left">Estado</th>
<th className="py-3 px-6 text-left">Total</th>
</tr>
</thead>
<tbody className="text-gray-600 text-sm font-light">
{recentOrders.map(order => (
<tr key={order.id} className="border-b border-gray-200 hover:bg-gray-50">
<td className="py-3 px-6 text-left">{String(order.numeroPedido || '').padStart(8, '0')}</td>
<td className="py-3 px-6 text-left">{`${order.nombre} ${order.apellido}`}</td>
<td className="py-3 px-6 text-left">{order.fecha && order.fecha.seconds ? new Date(order.fecha.seconds * 1000).toLocaleDateString() : 'Fecha no disponible'}</td>
<td className="py-3 px-6 text-left">{order.estado}</td>
<td className="py-3 px-6 text-left">{order.total}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
export default Dashboard;
```
---
### Cambios realizados:
1. En la obtención de datos para `ordersList` a partir de Firestore, agregué los `doc.id` como identificadores únicos.
```js
const ordersList = ordersSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
```
2. En la tabla `recentOrders`, ahora uso `key={order.id}` y elimino la posibilidad de que React genere advertencias sobre claves únicas. De este modo:
```js
{recentOrders.map(order => (
<tr key={order.id} className="border-b border-gray-200 hover:bg-gray-50">
...
</tr>
))}
```
Esto debería eliminar cualquier advertencia relacionada con las claves únicas (`key`) en el renderizado. 😊