diff --git a/src/App.jsx b/src/App.jsx index 2b84eb6..8ed641d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,23 +2,40 @@ import React from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import Dashboard from './screens/DashboardTerminacion'; import DashboardCarnet from './screens/DashboardCarnet'; +import Login from './screens/Login'; +import ProtectedRoute from './components/ProtectedRoute'; export default function App() { return ( + } /> + + + + } + /> + + + + } + /> } /> - } /> - } />

404

-

Página no encontrada

+

Pagina no encontrada

- La página que buscas no existe o ha sido movida. + La pagina que buscas no existe o ha sido movida.

+ ); } diff --git a/src/components/ProtectedRoute.jsx b/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..fdc97a7 --- /dev/null +++ b/src/components/ProtectedRoute.jsx @@ -0,0 +1,23 @@ +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; + +export default function ProtectedRoute({ children }) { + const { user, loading } = useAuth(); + + if (loading) { + return ( +
+
+
+

Cargando...

+
+
+ ); + } + + if (!user) { + return ; + } + + return children; +} diff --git a/src/contexts/AuthContext.jsx b/src/contexts/AuthContext.jsx new file mode 100644 index 0000000..2cdfe4a --- /dev/null +++ b/src/contexts/AuthContext.jsx @@ -0,0 +1,73 @@ +import { createContext, useContext, useEffect, useState } from 'react'; +import { supabase } from '../services/supabaseClient'; + +const AuthContext = createContext(null); + +const ALLOWED_DOMAIN = 'gomezleemarketing.com'; + +export function AuthProvider({ children }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + supabase.auth.getSession().then(({ data: { session } }) => { + if (session?.user) { + const email = session.user.email || ''; + if (email.endsWith(`@${ALLOWED_DOMAIN}`)) { + setUser(session.user); + } else { + supabase.auth.signOut(); + setUser(null); + } + } + setLoading(false); + }); + + const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { + if (session?.user) { + const email = session.user.email || ''; + if (email.endsWith(`@${ALLOWED_DOMAIN}`)) { + setUser(session.user); + } else { + supabase.auth.signOut(); + setUser(null); + } + } else { + setUser(null); + } + setLoading(false); + }); + + return () => subscription.unsubscribe(); + }, []); + + const signInWithGoogle = async () => { + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: `${window.location.origin}/dashboard/encuesta-terminacion`, + queryParams: { + hd: ALLOWED_DOMAIN, + }, + }, + }); + if (error) throw error; + }; + + const signOut = async () => { + await supabase.auth.signOut(); + setUser(null); + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) throw new Error('useAuth must be used within AuthProvider'); + return context; +} diff --git a/src/main.jsx b/src/main.jsx index b9a1a6d..298afc9 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -2,9 +2,12 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' +import { AuthProvider } from './contexts/AuthContext' createRoot(document.getElementById('root')).render( - + + + , ) diff --git a/src/screens/DashboardCarnet.jsx b/src/screens/DashboardCarnet.jsx index be46272..91fd1f9 100644 --- a/src/screens/DashboardCarnet.jsx +++ b/src/screens/DashboardCarnet.jsx @@ -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 ( +
+
+

Cargando datos...

+
+ ); - if (loading) return
Cargando datos...
; + if (error) return ( +
+
+
⚠️
+

Error al cargar datos

+

{error}

+

Revisa la consola del navegador (F12) para más detalles.

+ +
+
+ ); return (
@@ -148,61 +174,6 @@ export default function DashboardCarnet() {
- - {/* TABLA GLM */} -
-
-

Detalle de Carnets

-
-
- - - - - - - - - - {chartDataCliente.map((row, idx) => ( - - - - - ))} - -
FechaCliente / ProyectoCarnets
{row.name}{row.value}
-
- - {/* Controles de Pagina */} -
-
- {datosFiltrados.length > 0 - ? `Mostrando registros ${indicePrimerItem + 1} - ${Math.min(indiceUltimoItem, datosFiltrados.length)} de ${datosFiltrados.length}` - : 'No hay registros para mostrar' - } -
-
- - - Página {paginaActual} de {totalPaginas} - - -
-
-
); } diff --git a/src/screens/DashboardTerminacion.jsx b/src/screens/DashboardTerminacion.jsx index 0339663..aa8888a 100644 --- a/src/screens/DashboardTerminacion.jsx +++ b/src/screens/DashboardTerminacion.jsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from 'react'; -import { supabase } from '../services/supabaseClient'; -import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from 'recharts'; +import React, { useState, useEffect, useMemo } from 'react'; +import { supabaseData } from '../services/supabaseClient'; +import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid } from 'recharts'; import NavBar from '../components/NavBar'; const GLM_COLORS = { positivo: '#6CC24A', neutro: '#F0C75E', negativo: '#FF6A13' }; @@ -9,15 +9,30 @@ const CHART_PALETTE = ['#6CC24A', '#4F758B', '#A4D65E', '#5B7F95', '#C4D600', '# export default function Dashboard() { const [rawData, setRawData] = useState([]); const [filteredData, setFilteredData] = useState([]); + const [encuestasEnviadas, setEncuestasEnviadas] = useState(0); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - const [posiciones, setPosiciones] = useState([]); - const [homologaciones, setHomologaciones] = useState([]); - const [paises, setPaises] = useState([]); + const opcionesUnicas = (campo) => [ + ...new Set( + rawData + .map(item => (item?.[campo] ?? '').toString().trim()) + .filter(v => v !== '') + ), + ].sort((a, b) => a.localeCompare(b, 'es')); + + const posiciones = useMemo(() => opcionesUnicas('posicion'), [rawData]); + const homologaciones = useMemo(() => opcionesUnicas('homologacion_ia'), [rawData]); + const paises = useMemo(() => opcionesUnicas('pais'), [rawData]); + const cuentas = useMemo(() => opcionesUnicas('cuenta_proyecto'), [rawData]); + const jefes = useMemo(() => opcionesUnicas('jefe_directo'), [rawData]); const [filtroPosicion, setFiltroPosicion] = useState(''); const [filtroHomologacion, setFiltroHomologacion] = useState(''); const [filtroPais, setFiltroPais] = useState(''); + const [filtroCuenta, setFiltroCuenta] = useState(''); + const [filtroJefe, setFiltroJefe] = useState(''); + const [busquedaNombre, setBusquedaNombre] = useState(''); const [paginaActual, setPaginaActual] = useState(1); @@ -28,26 +43,45 @@ export default function Dashboard() { useEffect(() => { aplicarFiltros(); setPaginaActual(1); - }, [filtroPosicion, filtroHomologacion, filtroPais, rawData]); + }, [filtroPosicion, filtroHomologacion, filtroPais, filtroCuenta, filtroJefe, rawData]); const fetchData = async () => { setLoading(true); - const { data, error } = await supabase + setError(null); + console.log('[DashboardTerminacion] Iniciando fetch...'); + + const { data, error: fetchError } = await supabaseData .from('encuesta_terminacion_glm') .select('*') .order('timestamp', { ascending: false }); - if (error) { - console.error('Error fetching data:', error); + if (fetchError) { + console.error('[DashboardTerminacion] Error de Supabase:', { + message: fetchError.message, + details: fetchError.details, + hint: fetchError.hint, + code: fetchError.code, + }); + setError(fetchError.message || 'Error al cargar los datos de encuestas.'); } else { - setRawData(data); - const uniquePosiciones = [...new Set(data.map(item => item.posicion).filter(Boolean))]; - const uniqueHomologaciones = [...new Set(data.map(item => item.homologacion_ia).filter(Boolean))]; - const uniquePaises = [...new Set(data.map(item => item.pais).filter(Boolean))]; - setPosiciones(uniquePosiciones); - setHomologaciones(uniqueHomologaciones); - setPaises(uniquePaises); + console.log('[DashboardTerminacion] Datos recibidos:', data?.length ?? 0, 'registros'); + setRawData(data ?? []); } + + const { data: kpiData, error: kpiError } = await supabaseData + .from('encuesta_terminacion_kpi_glm') + .select('encuestas_enviadas'); + + if (kpiError) { + console.error('[DashboardTerminacion] Error al cargar KPI:', kpiError.message); + } else { + const totalEnviadas = (kpiData ?? []).reduce( + (acc, curr) => acc + (Number(curr.encuestas_enviadas) || 0), + 0 + ); + setEncuestasEnviadas(totalEnviadas); + } + setLoading(false); }; @@ -56,6 +90,8 @@ export default function Dashboard() { if (filtroPosicion) result = result.filter(item => item.posicion === filtroPosicion); if (filtroHomologacion) result = result.filter(item => item.homologacion_ia === filtroHomologacion); if (filtroPais) result = result.filter(item => item.pais === filtroPais); + if (filtroCuenta) result = result.filter(item => item.cuenta_proyecto === filtroCuenta); + if (filtroJefe) result = result.filter(item => item.jefe_directo === filtroJefe); // Ordenar por fecha de más reciente a más vieja result = [...result].sort((a, b) => { @@ -67,15 +103,26 @@ export default function Dashboard() { setFilteredData(result); }; - const filasPorPagina = 20; + const filasPorPagina = 10; const totalPaginas = Math.ceil(filteredData.length / filasPorPagina) || 1; const indiceUltimoItem = paginaActual * filasPorPagina; const indicePrimerItem = indiceUltimoItem - filasPorPagina; - const datosPaginados = filteredData.slice(indicePrimerItem, indiceUltimoItem); + const datosFiltradosTabla = busquedaNombre.trim() + ? filteredData.filter(item => + (item?.nombre ?? '').toString().toLowerCase().includes(busquedaNombre.trim().toLowerCase()) + ) + : filteredData; + + const totalRegistrosTabla = datosFiltradosTabla.length; + const datosPaginados = datosFiltradosTabla.slice(indicePrimerItem, indiceUltimoItem); const totalRegistros = filteredData.length; - const regresarian = filteredData.filter(d => d.considerar_regresar).length; - const recomendarian = filteredData.filter(d => d.siento_comodo_recomendando).length; + const porcentajeAprobacion = encuestasEnviadas > 0 + ? Math.round((totalRegistros / encuestasEnviadas) * 100) + : 0; + const esVerdadero = (v) => v === true || String(v).toLowerCase().trim() === 'true'; + const regresarian = filteredData.filter(d => esVerdadero(d.considerar_regresar)).length; + const recomendarian = filteredData.filter(d => esVerdadero(d.siento_comodo_recomendando)).length; const sentimientoCount = filteredData.reduce((acc, curr) => { const sent = curr.sentimiento_ia || 'neutro'; @@ -88,7 +135,66 @@ export default function Dashboard() { value: sentimientoCount[key] })); - if (loading) return
Cargando datos...
; + const pendientes = Math.max(encuestasEnviadas - totalRegistros, 0); + const chartDataAprobacion = [ + { name: 'Completadas', value: totalRegistros }, + { name: 'Pendientes', value: pendientes }, + ]; + + const esFalso = (v) => v === false || String(v).toLowerCase().trim() === 'false'; + + const chartDataRegresar = [ + { name: 'Sí', value: filteredData.filter(d => esVerdadero(d.considerar_regresar)).length }, + { name: 'No', value: filteredData.filter(d => esFalso(d.considerar_regresar)).length }, + ]; + + const chartDataRecomendar = [ + { name: 'Sí', value: filteredData.filter(d => esVerdadero(d.siento_comodo_recomendando)).length }, + { name: 'No', value: filteredData.filter(d => esFalso(d.siento_comodo_recomendando)).length }, + ]; + + const contarPorCampo = (campo) => { + const conteo = filteredData.reduce((acc, curr) => { + const valor = (curr?.[campo] ?? '').toString().trim() || 'Sin especificar'; + acc[valor] = (acc[valor] || 0) + 1; + return acc; + }, {}); + return Object.keys(conteo) + .sort((a, b) => conteo[b] - conteo[a]) + .map((key, index) => ({ + name: key, + value: conteo[key], + fill: CHART_PALETTE[index % CHART_PALETTE.length], + })); + }; + + const chartDataMotivoSalida = contarPorCampo('homologacion_ia'); + const chartDataPorPais = contarPorCampo('pais'); + const chartDataPorProyecto = contarPorCampo('cuenta_proyecto'); + + if (loading) return ( +
+
+

Cargando datos...

+
+ ); + + if (error) return ( +
+
+
⚠️
+

Error al cargar datos

+

{error}

+

Revisa la consola del navegador (F12) para más detalles.

+ +
+
+ ); return (
@@ -112,7 +218,7 @@ export default function Dashboard() { {/* FILTROS GLM */} -
+
setFiltroCuenta(e.target.value)} + > + + {cuentas.map((cuenta, idx) => ( + + ))} + +
+ +
+ + +
+
@@ -166,9 +300,13 @@ export default function Dashboard() {
{/* KPIs GLM */} -
+
+
+

Total Encuestas Enviadas

+

{encuestasEnviadas}

+
-

Total Registros

+

Total Encuestas Completadas

{totalRegistros}

@@ -213,20 +351,165 @@ export default function Dashboard() {
+ +
+

% de Aprobación

+
+ + + + + + + + + + +
+ {porcentajeAprobacion}% + {totalRegistros} de {encuestasEnviadas} +
+
+
+
+ + {/* GRÁFICAS SÍ / NO */} +
+
+

¿Regresaría a la organización en el futuro?

+
+ + + + + + + + {chartDataRegresar.map((entry) => ( + + ))} + + + +
+
+ +
+

Recomendaría a otra persona para trabajar en la organización

+
+ + + + + + + + {chartDataRecomendar.map((entry) => ( + + ))} + + + +
+
+
+ + {/* GRÁFICAS DE SALIDA LABORAL */} +
+
+

Motivo de Salida Laboral

+
+ + + + + + + + {chartDataMotivoSalida.map((entry, index) => ( + + ))} + + + +
+
+ +
+

Salida Laboral Según País

+
+ + + + + + + + {chartDataPorPais.map((entry, index) => ( + + ))} + + + +
+
+ +
+

Salida Laboral Según Proyecto

+
+ + + + + + + + {chartDataPorProyecto.map((entry, index) => ( + + ))} + + + +
+
{/* TABLA GLM */}
-
+

Detalle de Registros

+
+ + { setBusquedaNombre(e.target.value); setPaginaActual(1); }} + placeholder="Escriba el nombre..." + className="w-56 rounded-lg p-2.5 border text-sm text-gris-oscuro bg-amarillo-input focus:bg-white focus:ring-2 focus:ring-verde-glm focus:outline-none" + /> +
+ + + + + @@ -237,8 +520,13 @@ export default function Dashboard() { - + + + + + +
FechaNombre PaísCliente/Proyecto PosiciónJefe DirectoPDVTrade Partner Motivo Sentimiento
{new Date(item.timestamp).toLocaleDateString()} {item.pais}{item.nombre}{item.pais}{item.cuenta_proyecto} {item.posicion}{item.jefe_directo}{item.pdv}{item.trade_partner} {item.homologacion_ia} {(() => { diff --git a/src/screens/Login.jsx b/src/screens/Login.jsx new file mode 100644 index 0000000..4612366 --- /dev/null +++ b/src/screens/Login.jsx @@ -0,0 +1,85 @@ +import { useState } from 'react'; +import { useAuth } from '../contexts/AuthContext'; +import { Navigate } from 'react-router-dom'; + +export default function Login() { + const { user, loading, signInWithGoogle } = useAuth(); + const [error, setError] = useState(null); + const [signingIn, setSigningIn] = useState(false); + + if (loading) { + return ( +
+
+
+ ); + } + + if (user) { + return ; + } + + const handleLogin = async () => { + setError(null); + setSigningIn(true); + try { + await signInWithGoogle(); + } catch (err) { + setError('Error al iniciar sesion. Intenta de nuevo.'); + setSigningIn(false); + } + }; + + return ( +
+
+ GomezLee Marketing + + + + {error && ( +

+ {error} +

+ )} + +

+ Solo se permiten correos corporativos
+ @gomezleemarketing.com +

+
+
+ ); +} diff --git a/src/services/supabaseClient.js b/src/services/supabaseClient.js index dc9dfb5..8e0116d 100644 --- a/src/services/supabaseClient.js +++ b/src/services/supabaseClient.js @@ -1,7 +1,27 @@ -// src/supabaseClient.js import { createClient } from '@supabase/supabase-js'; const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; -const supabaseAnonKey = import.meta.env.VITE_SUPABASE_SERVICE_KEY; +const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY; -export const supabase = createClient(supabaseUrl, supabaseAnonKey); \ No newline at end of file +if (!supabaseUrl || !supabaseKey) { + console.error('[Supabase] Faltan variables de entorno:', { + url: !!supabaseUrl, + key: !!supabaseKey, + }); +} + +export const supabase = createClient(supabaseUrl, supabaseKey, { + auth: { + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: true, + storageKey: 'glm-dashboard-auth', + }, + global: { + headers: { + 'x-application-name': 'glm-dashboard', + }, + }, +}); + +export const supabaseData = supabase; diff --git a/vite.config.js b/vite.config.js index 0d2cabd..071af14 100644 --- a/vite.config.js +++ b/vite.config.js @@ -5,5 +5,5 @@ import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ plugins: [tailwindcss(), react()], - base: '/dashboard/' + base: '/dashboard/encuesta-terminacion' })