From 446862291ff3b527afb64d7b8ab218d6aa88315d Mon Sep 17 00:00:00 2001 From: EidanThen Date: Thu, 17 Sep 2026 13:33:06 -0400 Subject: [PATCH] feat: Dashboard Documentos RRHH --- src/App.jsx | 9 ++ src/components/NavBar.jsx | 6 + src/screens/DashboardRRHHDocs.jsx | 229 ++++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 src/screens/DashboardRRHHDocs.jsx diff --git a/src/App.jsx b/src/App.jsx index a143968..8751f20 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,7 @@ import Dashboard from './screens/DashboardTerminacion'; import DashboardCarnet from './screens/DashboardCarnet'; import DashboardGLMWAY from './screens/DashboardGLMWAY'; import DashboardENPS from './screens/DashboardENPS'; +import DashboardRRHHDocs from './screens/DashboardRRHHDocs'; import Login from './screens/Login'; import ProtectedRoute from './components/ProtectedRoute'; @@ -44,6 +45,14 @@ export default function App() { } /> + + + + } + /> } /> } /> eNPS + `${linkBase} ${isActive ? linkActive : linkInactive}`} + > + Documentos RRHH + ); diff --git a/src/screens/DashboardRRHHDocs.jsx b/src/screens/DashboardRRHHDocs.jsx new file mode 100644 index 0000000..75bd0f2 --- /dev/null +++ b/src/screens/DashboardRRHHDocs.jsx @@ -0,0 +1,229 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Bar, BarChart, CartesianGrid, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; +import NavBar from '../components/NavBar'; +import { supabaseData } from '../services/supabaseClient'; + +const CHART_PALETTE = ['#6CC24A', '#4F758B', '#A4D65E', '#5B7F95', '#C4D600', '#6B8FA3', '#3E6B8A', '#8FBF5A']; +const MONTHS = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']; + +function getDateParts(value) { + const date = value ? new Date(value) : null; + if (!date || Number.isNaN(date.getTime())) return null; + return { year: date.getFullYear(), month: date.getMonth() + 1 }; +} + +function formatDuration(seconds) { + const totalSeconds = Math.max(0, Math.round(seconds)); + const minutes = Math.floor(totalSeconds / 60); + return `${minutes}:${String(totalSeconds % 60).padStart(2, '0')}`; +} + +export default function DashboardRRHHDocs() { + const [rawData, setRawData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filtroPais, setFiltroPais] = useState(''); + const [filtroAnio, setFiltroAnio] = useState(''); + const [filtroMes, setFiltroMes] = useState(''); + + const fetchData = async () => { + setLoading(true); + setError(null); + + const { data, error: fetchError } = await supabaseData + .from('docassist_kpi') + .select('id, nombre_archivo, pais, tiempo_generacion, cantidad_archivos_generados_en_ejecucion, created_at') + .order('created_at', { ascending: false }); + + if (fetchError) { + console.error('[DashboardRRHHDocs] Error de Supabase:', fetchError); + setError(fetchError.message || 'Error al cargar los datos de documentos RRHH.'); + } else { + setRawData(data ?? []); + } + + setLoading(false); + }; + + useEffect(() => { + fetchData(); + }, []); + + const paises = useMemo(() => [ + ...new Set(rawData + .map(item => (item.pais ?? '').toString().trim()) + .filter(Boolean)), + ].sort((a, b) => a.localeCompare(b, 'es')), [rawData]); + + const anios = useMemo(() => [ + ...new Set(rawData + .map(item => getDateParts(item.created_at)?.year) + .filter(Boolean)), + ].sort((a, b) => b - a), [rawData]); + + const meses = useMemo(() => [ + ...new Set(rawData + .map(item => getDateParts(item.created_at)?.month) + .filter(Boolean)), + ].sort((a, b) => a - b), [rawData]); + + const datosFiltrados = useMemo(() => rawData.filter(item => { + const dateParts = getDateParts(item.created_at); + const pais = (item.pais ?? '').toString().trim(); + return (!filtroPais || pais === filtroPais) + && (!filtroAnio || dateParts?.year === Number(filtroAnio)) + && (!filtroMes || dateParts?.month === Number(filtroMes)); + }), [rawData, filtroPais, filtroAnio, filtroMes]); + + const chartData = useMemo(() => { + const totals = datosFiltrados.reduce((acc, item) => { + const pais = (item.pais ?? '').toString().trim() || 'Sin país'; + const cantidad = Number(item.cantidad_archivos_generados_en_ejecucion); + acc[pais] = (acc[pais] || 0) + (Number.isFinite(cantidad) ? cantidad : 0); + return acc; + }, {}); + + return Object.entries(totals) + .map(([name, value], index) => ({ name, value, fill: CHART_PALETTE[index % CHART_PALETTE.length] })) + .sort((a, b) => b.value - a.value); + }, [datosFiltrados]); + + const tiemposPromedio = useMemo(() => { + const groups = datosFiltrados.reduce((acc, item) => { + const cantidad = Number(item.cantidad_archivos_generados_en_ejecucion); + const tiempo = Number(item.tiempo_generacion); + if (!Number.isInteger(cantidad) || cantidad < 1 || !Number.isFinite(tiempo)) return acc; + + if (!acc[cantidad]) acc[cantidad] = { total: 0, ejecuciones: 0 }; + acc[cantidad].total += tiempo; + acc[cantidad].ejecuciones += 1; + return acc; + }, {}); + + return Object.entries(groups) + .map(([cantidad, values]) => ({ + cantidad: Number(cantidad), + promedio: values.total / values.ejecuciones, + ejecuciones: values.ejecuciones, + })) + .sort((a, b) => a.cantidad - b.cantidad); + }, [datosFiltrados]); + + const totalArchivos = chartData.reduce((total, item) => total + item.value, 0); + const hayFiltrosActivos = filtroPais || filtroAnio || filtroMes; + + const limpiarFiltros = () => { + setFiltroPais(''); + setFiltroAnio(''); + setFiltroMes(''); + }; + + if (loading) return ( +
+
+

Cargando datos...

+
+ ); + + if (error) return ( +
+
+

Error al cargar datos

+

{error}

+ +
+
+ ); + + return ( +
+
+
+ Logo GLM +
+
+

Documentos RRHH

+

Generación de documentos por país y tiempo de proceso

+
+
+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+

Total de archivos creados

+

{totalArchivos}

+
+
+

Países con actividad

+

{chartData.length}

+
+
+ +
+

Tiempo promedio de generación de archivos

+ {tiemposPromedio.length === 0 ?

No hay tiempos de generación válidos para los filtros seleccionados.

: ( +
+ {tiemposPromedio.map(({ cantidad, promedio, ejecuciones }) => ( +
+

{cantidad} {cantidad === 1 ? 'archivo' : 'archivos'}

+

{formatDuration(promedio)}

+

Promedio de {ejecuciones} {ejecuciones === 1 ? 'ejecución' : 'ejecuciones'}

+
+ ))} +
+ )} +
+ +
+

Archivos creados por país

+
+ {chartData.length === 0 ?

No hay datos para los filtros seleccionados.

: ( + + + + + + [value, 'Archivos creados']} /> + + {chartData.map(item => )} + + + + )} +
+
+
+ ); +}