Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 128 additions & 1 deletion client/src/pages/EstadisticasNacional.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Comment on lines +92 to +96
}
};

if (loading) {
return (
<div className="container mt-5">
Expand Down Expand Up @@ -189,6 +224,98 @@ export default function Estadisticas() {
<LastMatchsNacional ordenEquipos={estadisticas.map(e => e.equipo)} />
</div>
</div>

{/* Tabla de Posiciones Virtual - según mis propios pronósticos */}
<div className="row g-4 mt-1">
<div className="col-lg-6">
<div className="card shadow-sm">
<div className="card-header bg-success text-white">
<h5 className="mb-0">🔮 Mi Tabla de Posiciones Virtual</h5>
</div>
<div className="card-body p-0">
{loadingVirtual ? (
<div className="text-center py-4">
<div className="spinner-border text-success" role="status">
<span className="visually-hidden">Cargando...</span>
</div>
</div>
) : errorVirtual ? (
<div className="alert alert-warning m-3 mb-0" role="alert">
{errorVirtual}
</div>
) : tablaVirtual.length === 0 ? (
<div className="alert alert-info m-3 mb-0" role="alert">
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.
</div>
) : (
<div className="table-responsive">
<table className="table table-hover table-striped mb-0">
<thead className="table-light">
<tr>
<th className="text-center" style={{ width: '50px' }}>Pos</th>
<th style={{ minWidth: '180px' }}>Equipo</th>
<th className="text-center" title="Partidos Pronosticados">PJ</th>
<th className="text-center" title="Partidos Ganados">PG</th>
<th className="text-center" title="Partidos Empatados">PE</th>
<th className="text-center" title="Partidos Perdidos">PP</th>
<th className="text-center" title="Goles a Favor">GF</th>
<th className="text-center" title="Goles en Contra">GC</th>
<th className="text-center" title="Diferencia de Goles">DIF</th>
<th className="text-center fw-bold" title="Puntos">PTS</th>
</tr>
</thead>
<tbody>
{tablaVirtual.map((equipo, index) => {
const logo = getLogoEquipo(equipo.equipo);
const diferencia = equipo.goles_favor - equipo.goles_contra;

return (
<tr key={equipo.equipo}>
<td className="text-center align-middle fw-bold">{index + 1}</td>
<td className="align-middle">
<div className="d-flex align-items-center gap-2">
{logo && (
<img
src={logo}
alt={equipo.equipo}
style={{ width: '28px', height: '28px', objectFit: 'contain' }}
onError={(e) => { e.target.style.display = 'none'; }}
/>
)}
<span className="fw-semibold" style={{ fontSize: '0.9rem' }}>{equipo.equipo}</span>
</div>
</td>
<td className="text-center align-middle">{equipo.partidos_jugados}</td>
<td className="text-center align-middle text-success fw-semibold">{equipo.ganados}</td>
<td className="text-center align-middle text-warning fw-semibold">{equipo.empatados}</td>
<td className="text-center align-middle text-danger fw-semibold">{equipo.perdidos}</td>
<td className="text-center align-middle">{equipo.goles_favor}</td>
<td className="text-center align-middle">{equipo.goles_contra}</td>
<td className={`text-center align-middle fw-semibold ${diferencia > 0 ? 'text-success' : diferencia < 0 ? 'text-danger' : ''}`}>
{diferencia > 0 ? '+' : ''}{diferencia}
</td>
<td className="text-center align-middle">
<span className="badge bg-success fs-6 px-3 py-2">{equipo.puntos}</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>

{!loadingVirtual && !errorVirtual && virtualInfo.total_partidos_jugados > 0 && (
<div className="alert alert-info mt-3">
<small>
Esta tabla se calcula con <strong>tus propios pronósticos</strong> (goles que ingresaste), considerando solo los partidos que ya se jugaron: <strong>{virtualInfo.partidos_pronosticados} de {virtualInfo.total_partidos_jugados}</strong> partidos disputados tienen pronóstico tuyo registrado.
</small>
</div>
)}
</div>
</div>
</div>
);
}
107 changes: 107 additions & 0 deletions server/routes/estadisticas_nacional.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import express from 'express';
import { pool } from '../db/pool.js';
import { verifyToken } from '../middleware/verifyToken.js';

const router = express.Router();

Expand Down Expand Up @@ -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
});
Comment on lines +272 to +275
}
});

// GET: Obtener solo equipos empatados que requieren desempate manual
router.get('/empates-pendientes', async (req, res) => {
try {
Expand Down