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 ? (
+
+ ) : 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.
+
+ ) : (
+
+
+
+
+ | Pos |
+ Equipo |
+ PJ |
+ PG |
+ PE |
+ PP |
+ GF |
+ GC |
+ DIF |
+ PTS |
+
+
+
+ {tablaVirtual.map((equipo, index) => {
+ const logo = getLogoEquipo(equipo.equipo);
+ const diferencia = equipo.goles_favor - equipo.goles_contra;
+
+ return (
+
+ | {index + 1} |
+
+
+ {logo && (
+  { 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 {