feat: initialize project with Supabase authentication, routing, and dashboard screens for project data visualization
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user