diff --git a/client/src/pages/EstadisticasNacional.jsx b/client/src/pages/EstadisticasNacional.jsx index e9a0cb5..a50ab3d 100644 --- a/client/src/pages/EstadisticasNacional.jsx +++ b/client/src/pages/EstadisticasNacional.jsx @@ -38,15 +38,21 @@ export default function Estadisticas() { const [empatesPendientes, setEmpatesPendientes] = useState(null); const [loading, setLoading] = useState(true); + const [tablaVirtual, setTablaVirtual] = useState([]); + const [virtualInfo, setVirtualInfo] = useState({ partidos_pronosticados: 0, total_partidos_jugados: 0 }); + const [loadingVirtual, setLoadingVirtual] = useState(true); + const [errorVirtual, setErrorVirtual] = useState(null); + useEffect(() => { cargarEstadisticas(); + cargarTablaVirtual(); }, []); const cargarEstadisticas = async () => { try { setLoading(true); const response = await axios.get(`${API_BASE_URL}/api/estadisticas-nacional/tabla-posiciones`); - + // Manejar nueva estructura de respuesta if (response.data.tabla) { setEstadisticas(response.data.tabla); @@ -62,6 +68,35 @@ export default function Estadisticas() { } }; + const cargarTablaVirtual = async () => { + try { + setLoadingVirtual(true); + setErrorVirtual(null); + const token = localStorage.getItem('token'); + + if (!token) { + setErrorVirtual('Debes iniciar sesi贸n para ver tu tabla de posiciones virtual.'); + return; + } + + const response = await axios.get( + `${API_BASE_URL}/api/estadisticas-nacional/tabla-posiciones-virtual`, + { headers: { Authorization: `Bearer ${token}` } } + ); + + setTablaVirtual(response.data.tabla || []); + setVirtualInfo({ + partidos_pronosticados: response.data.partidos_pronosticados || 0, + total_partidos_jugados: response.data.total_partidos_jugados || 0 + }); + } catch (error) { + console.error('Error cargando tabla virtual:', error); + setErrorVirtual('No se pudo cargar tu tabla de posiciones virtual.'); + } finally { + setLoadingVirtual(false); + } + }; + if (loading) { return (
@@ -189,6 +224,98 @@ export default function Estadisticas() { e.equipo)} />
+ + {/* Tabla de Posiciones Virtual - seg煤n mis propios pron贸sticos */} +
+
+
+
+
馃敭 Mi Tabla de Posiciones Virtual
+
+
+ {loadingVirtual ? ( +
+
+ Cargando... +
+
+ ) : errorVirtual ? ( +
+ {errorVirtual} +
+ ) : tablaVirtual.length === 0 ? ( +
+ A煤n no tienes pron贸sticos de partidos ya jugados. Cuando ingreses tus pron贸sticos y se disputen los partidos, aqu铆 ver谩s c贸mo quedar铆a la tabla seg煤n tus propios resultados. +
+ ) : ( +
+ + + + + + + + + + + + + + + + + {tablaVirtual.map((equipo, index) => { + const logo = getLogoEquipo(equipo.equipo); + const diferencia = equipo.goles_favor - equipo.goles_contra; + + return ( + + + + + + + + + + + + + ); + })} + +
PosEquipoPJPGPEPPGFGCDIFPTS
{index + 1} +
+ {logo && ( + {equipo.equipo} { e.target.style.display = 'none'; }} + /> + )} + {equipo.equipo} +
+
{equipo.partidos_jugados}{equipo.ganados}{equipo.empatados}{equipo.perdidos}{equipo.goles_favor}{equipo.goles_contra} 0 ? 'text-success' : diferencia < 0 ? 'text-danger' : ''}`}> + {diferencia > 0 ? '+' : ''}{diferencia} + + {equipo.puntos} +
+
+ )} +
+
+ + {!loadingVirtual && !errorVirtual && virtualInfo.total_partidos_jugados > 0 && ( +
+ + Esta tabla se calcula con tus propios pron贸sticos (goles que ingresaste), considerando solo los partidos que ya se jugaron: {virtualInfo.partidos_pronosticados} de {virtualInfo.total_partidos_jugados} partidos disputados tienen pron贸stico tuyo registrado. + +
+ )} +
+
); } diff --git a/server/routes/estadisticas_nacional.js b/server/routes/estadisticas_nacional.js index aaf26a3..56bc953 100644 --- a/server/routes/estadisticas_nacional.js +++ b/server/routes/estadisticas_nacional.js @@ -1,5 +1,6 @@ import express from 'express'; import { pool } from '../db/pool.js'; +import { verifyToken } from '../middleware/verifyToken.js'; const router = express.Router(); @@ -169,6 +170,112 @@ router.get('/tabla-posiciones', async (req, res) => { } }); +// GET: Obtener tabla de posiciones VIRTUAL del usuario logueado, +// calculada con sus propios pron贸sticos (solo partidos ya jugados) +router.get('/tabla-posiciones-virtual', verifyToken, async (req, res) => { + const usuarioId = req.usuario.id; + + try { + // Solo se consideran partidos que ya tienen resultado real Y para los + // cuales el usuario efectivamente entreg贸 un pron贸stico + const query = ` + WITH mis_pronosticos AS ( + SELECT + pa.nombre_local, + pa.nombre_visita, + p.goles_local, + p.goles_visita + FROM pronosticos p + JOIN partidos pa ON p.partido_id = pa.id + WHERE p.usuario_id = $1 + AND p.goles_local IS NOT NULL + AND p.goles_visita IS NOT NULL + AND pa.goles_local IS NOT NULL + AND pa.goles_visita IS NOT NULL + ), + equipos_locales AS ( + SELECT + nombre_local as equipo, + COUNT(*) as partidos_local, + SUM(CASE WHEN goles_local > goles_visita THEN 1 ELSE 0 END) as ganados_local, + SUM(CASE WHEN goles_local = goles_visita THEN 1 ELSE 0 END) as empatados_local, + SUM(CASE WHEN goles_local < goles_visita THEN 1 ELSE 0 END) as perdidos_local, + COALESCE(SUM(goles_local), 0) as gf_local, + COALESCE(SUM(goles_visita), 0) as gc_local + FROM mis_pronosticos + GROUP BY nombre_local + ), + equipos_visitas AS ( + SELECT + nombre_visita as equipo, + COUNT(*) as partidos_visita, + SUM(CASE WHEN goles_visita > goles_local THEN 1 ELSE 0 END) as ganados_visita, + SUM(CASE WHEN goles_visita = goles_local THEN 1 ELSE 0 END) as empatados_visita, + SUM(CASE WHEN goles_visita < goles_local THEN 1 ELSE 0 END) as perdidos_visita, + COALESCE(SUM(goles_visita), 0) as gf_visita, + COALESCE(SUM(goles_local), 0) as gc_visita + FROM mis_pronosticos + GROUP BY nombre_visita + ), + todos_equipos AS ( + SELECT DISTINCT nombre_local as equipo FROM mis_pronosticos + UNION + SELECT DISTINCT nombre_visita as equipo FROM mis_pronosticos + ) + SELECT + te.equipo, + COALESCE(el.partidos_local, 0) + COALESCE(ev.partidos_visita, 0) as partidos_jugados, + COALESCE(el.ganados_local, 0) + COALESCE(ev.ganados_visita, 0) as ganados, + COALESCE(el.empatados_local, 0) + COALESCE(ev.empatados_visita, 0) as empatados, + COALESCE(el.perdidos_local, 0) + COALESCE(ev.perdidos_visita, 0) as perdidos, + COALESCE(el.gf_local, 0) + COALESCE(ev.gf_visita, 0) as goles_favor, + COALESCE(el.gc_local, 0) + COALESCE(ev.gc_visita, 0) as goles_contra, + (COALESCE(el.ganados_local, 0) + COALESCE(ev.ganados_visita, 0)) * 3 + + (COALESCE(el.empatados_local, 0) + COALESCE(ev.empatados_visita, 0)) as puntos + FROM todos_equipos te + LEFT JOIN equipos_locales el ON te.equipo = el.equipo + LEFT JOIN equipos_visitas ev ON te.equipo = ev.equipo + `; + + const result = await pool.query(query, [usuarioId]); + + // Aplicar los mismos 4 criterios autom谩ticos de desempate que la tabla real + // (los criterios 5-8, manuales, no aplican aqu铆: son espec铆ficos del torneo real) + result.rows.sort((a, b) => { + if (b.puntos !== a.puntos) return b.puntos - a.puntos; + + const difA = a.goles_favor - a.goles_contra; + const difB = b.goles_favor - b.goles_contra; + if (difB !== difA) return difB - difA; + + if (b.ganados !== a.ganados) return b.ganados - a.ganados; + if (b.goles_favor !== a.goles_favor) return b.goles_favor - a.goles_favor; + + return a.equipo.localeCompare(b.equipo); + }); + + // Cantidad de partidos ya jugados en el torneo (para dar contexto de cobertura) + const totalPartidosJugadosResult = await pool.query( + `SELECT COUNT(*) as total FROM partidos WHERE goles_local IS NOT NULL AND goles_visita IS NOT NULL` + ); + const totalPartidosJugados = parseInt(totalPartidosJugadosResult.rows[0].total, 10); + const partidosPronosticados = result.rows.reduce((acc, r) => acc + parseInt(r.partidos_jugados, 10), 0) / 2; + + res.json({ + tabla: result.rows, + partidos_pronosticados: partidosPronosticados, + total_partidos_jugados: totalPartidosJugados + }); + + } catch (error) { + console.error('Error obteniendo tabla de posiciones virtual:', error); + res.status(500).json({ + error: 'Error obteniendo tabla de posiciones virtual', + details: error.message + }); + } +}); + // GET: Obtener solo equipos empatados que requieren desempate manual router.get('/empates-pendientes', async (req, res) => { try {