feat: initialize project with Supabase authentication, routing, and dashboard screens for project data visualization

This commit is contained in:
2026-07-13 15:36:58 -04:00
parent 2e4031e14a
commit 210063198b
10 changed files with 590 additions and 106 deletions
+38 -67
View File
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { supabase } from '../services/supabaseClient';
import { supabaseData } from '../services/supabaseClient';
import { BarChart, Bar, Cell, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, CartesianGrid } from 'recharts';
import NavBar from '../components/NavBar';
@@ -8,31 +8,40 @@ const CHART_PALETTE = ['#6CC24A', '#4F758B', '#A4D65E', '#5B7F95', '#C4D600', '#
export default function DashboardCarnet() {
const [rawData, setRawData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [clientes, setClientes] = useState([]);
const [filtroCliente, setFiltroCliente] = useState('');
const [paginaActual, setPaginaActual] = useState(1);
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
setPaginaActual(1);
const uniqueClientes = [...new Set(rawData.map(item => item.cliente_proyecto).filter(Boolean))];
setClientes(uniqueClientes);
}, [rawData]);
const fetchData = async () => {
setLoading(true);
const { data, error } = await supabase
setError(null);
console.log('[DashboardCarnet] Iniciando fetch...');
const { data, error: fetchError } = await supabaseData
.from('carnet_empleados_creados_glm')
.select('*')
.order('created_at', { ascending: false });
if (error) {
console.error('Error fetching data:', error);
if (fetchError) {
console.error('[DashboardCarnet] Error de Supabase:', {
message: fetchError.message,
details: fetchError.details,
hint: fetchError.hint,
code: fetchError.code,
});
setError(fetchError.message || 'Error al cargar los datos de carnets.');
} else {
setRawData(data);
console.log('[DashboardCarnet] Datos recibidos:', data?.length ?? 0, 'registros');
setRawData(data ?? []);
}
setLoading(false);
};
@@ -55,12 +64,29 @@ export default function DashboardCarnet() {
fill: CHART_PALETTE[index % CHART_PALETTE.length]
}));
const filasPorPagina = 20;
const totalPaginas = Math.ceil(datosFiltrados.length / filasPorPagina) || 1;
const indiceUltimoItem = paginaActual * filasPorPagina;
const indicePrimerItem = indiceUltimoItem - filasPorPagina;
if (loading) return (
<div className="p-10 text-center">
<div className="inline-block w-10 h-10 border-4 border-verde-glm border-t-transparent rounded-full animate-spin mb-4" />
<p className="text-xl font-semibold text-azul-glm">Cargando datos...</p>
</div>
);
if (loading) return <div className="p-10 text-center text-xl font-semibold text-azul-glm">Cargando datos...</div>;
if (error) return (
<div className="p-10 max-w-xl mx-auto min-h-screen flex items-center justify-center">
<div className="bg-white p-8 rounded-2xl border border-red-200 shadow-sm text-center w-full">
<div className="text-red-500 text-4xl mb-4"></div>
<h2 className="text-xl font-bold text-red-700 mb-2">Error al cargar datos</h2>
<p className="text-sm text-gray-600 mb-1">{error}</p>
<p className="text-xs text-gray-400 mb-6">Revisa la consola del navegador (F12) para más detalles.</p>
<button
onClick={fetchData}
className="bg-azul-glm hover:bg-azul-medio text-white font-semibold py-2.5 px-6 rounded-lg transition-colors"
>
Reintentar
</button>
</div>
</div>
);
return (
<div className="p-6 max-w-7xl mx-auto min-h-screen">
@@ -148,61 +174,6 @@ export default function DashboardCarnet() {
</ResponsiveContainer>
</div>
</div>
{/* TABLA GLM */}
<div className="bg-white rounded-xl border border-gris-medio/30 shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gris-medio/30 bg-verde-bg">
<h3 className="text-base font-bold text-azul-glm">Detalle de Carnets</h3>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gris-medio/50 text-sm">
<thead className="bg-azul-glm">
<tr>
<th className="px-6 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Fecha</th>
<th className="px-6 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Cliente / Proyecto</th>
<th className="px-6 py-3 text-left text-xs font-bold text-white uppercase tracking-wider">Carnets</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gris-medio/30">
{chartDataCliente.map((row, idx) => (
<tr key={idx} className={`${idx % 2 === 0 ? 'bg-white' : 'bg-gris-claro'} hover:bg-verde-bg/50 transition-colors`}>
<td className="px-6 py-4 whitespace-nowrap text-acero" colSpan={2}>{row.name}</td>
<td className="px-6 py-4 whitespace-nowrap font-semibold text-azul-glm">{row.value}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Controles de Pagina */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 px-6 py-4 border-t border-gris-medio/30 bg-gris-claro/50">
<div className="text-xs text-acero font-medium">
{datosFiltrados.length > 0
? `Mostrando registros ${indicePrimerItem + 1} - ${Math.min(indiceUltimoItem, datosFiltrados.length)} de ${datosFiltrados.length}`
: 'No hay registros para mostrar'
}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setPaginaActual(prev => Math.max(prev - 1, 1))}
disabled={paginaActual === 1}
className="px-3 py-1.5 rounded-lg border border-gris-medio text-xs font-semibold text-azul-glm bg-white hover:bg-gris-claro disabled:opacity-50 disabled:cursor-not-allowed transition-all cursor-pointer"
>
Atrás
</button>
<span className="text-xs text-gris-oscuro font-bold">
Página {paginaActual} de {totalPaginas}
</span>
<button
onClick={() => setPaginaActual(prev => Math.min(prev + 1, totalPaginas))}
disabled={paginaActual === totalPaginas}
className="px-3 py-1.5 rounded-lg border border-gris-medio text-xs font-semibold text-azul-glm bg-white hover:bg-gris-claro disabled:opacity-50 disabled:cursor-not-allowed transition-all cursor-pointer"
>
Siguiente
</button>
</div>
</div>
</div>
</div>
);
}