correciones atleta

This commit is contained in:
Fernando Escobar Robles 2025-06-06 21:04:14 -06:00
parent db12eda039
commit c0a40b82f2
6 changed files with 279 additions and 122 deletions

View File

@ -1,47 +1,52 @@
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", async () => {
const atletaId = sessionStorage.getItem("userId"); const atletaId = sessionStorage.getItem("userId");
const contenedor = document.getElementById("rutinas-list");
if (!atletaId) { if (!atletaId) {
document.getElementById("rutinas-list").innerHTML = contenedor.innerHTML = "<p>No se encontró tu sesión. Inicia sesión nuevamente.</p>";
"<p>No se encontró tu sesión. Inicia sesión nuevamente.</p>";
return; return;
} }
fetch(`/api/rutinas/atleta/${atletaId}`) try {
.then(res => { const res = await fetch(`/api/rutinas/atleta/${atletaId}/formaciones`);
if (!res.ok) { if (!res.ok) throw new Error("Error al obtener formaciones del atleta");
throw new Error("Error al obtener rutinas");
} const formaciones = await res.json();
return res.json(); if (formaciones.length === 0) {
}) contenedor.innerHTML = "<p>No estás asignado a ninguna formación aún.</p>";
.then(rutinas => {
const contenedor = document.getElementById("rutinas-list");
if (rutinas.length === 0) {
contenedor.innerHTML = "<p>No tienes rutinas asignadas todavía.</p>";
return; return;
} }
rutinas.forEach(rutina => {
formaciones.forEach(f => {
const card = document.createElement("div"); const card = document.createElement("div");
card.className = "card my-3"; card.className = "card text-start my-3 shadow-sm";
card.innerHTML = ` card.innerHTML = `
<div class="card-body"> <div class="card-body">
<h5 class="card-title">${rutina.title}</h5> <h5 class="card-title text-primary fw-bold">${f.rutinaNombre || "Rutina sin nombre"}</h5>
<p class="card-text">${rutina.nombreCompetencia || "Sin descripción"}</p> <h6 class="card-subtitle mb-2 text-muted">Formación: ${f.nombreColoquial || "(sin nombre)"}</h6>
<button class="btn btn-success" onclick="verRutina('${rutina._id}')">Ver rutina</button> <p class="card-text small mb-1">
<strong>Duración:</strong> ${f.duracion || "?"}s<br>
<strong>Notas:</strong> ${f.notasTacticas || "Sin notas"}<br>
<strong>Rol:</strong> ${f.atleta.rol || "N/A"} |
<strong>ID:</strong> ${f.atleta.idPersonalizado || "N/A"} |
<strong>Figura:</strong> ${f.atleta.figura || ""}
</p>
<button class="btn btn-sm btn-outline-success" onclick="verSimulador('${f.rutinaId}', ${f.index})">
Ver simulación
</button>
</div> </div>
`; `;
contenedor.appendChild(card); contenedor.appendChild(card);
}); });
}) } catch (err) {
.catch(err => { console.error("❌ Error al obtener formaciones:", err);
console.error(err); contenedor.innerHTML = "<p>Error al cargar tus asignaciones.</p>";
document.getElementById("rutinas-list").innerHTML = }
"<p>Error al cargar tus rutinas.</p>";
});
}); });
function verRutina(id) { function verSimulador(rutinaId, index) {
window.location.href = `simulador.html?routineId=${id}`; window.location.href = `simulador.html?routineId=${rutinaId}&formationIndex=${index}`;
} }
function logout() { function logout() {
@ -50,15 +55,11 @@ function logout() {
window.location.href = "../index.html"; window.location.href = "../index.html";
} }
// Mostrar nombre del usuario logueado en el header // Mostrar nombre de usuario
window.addEventListener('DOMContentLoaded', async () => { window.addEventListener("DOMContentLoaded", async () => {
const userId = sessionStorage.getItem("userId"); const userId = sessionStorage.getItem("userId");
if (!userId) { if (!userId) return;
alert("Sesión expirada, inicia sesión de nuevo.");
window.location.href = "index.html";
return;
}
try { try {
const res = await fetch(`/api/users/${userId}`); const res = await fetch(`/api/users/${userId}`);
@ -66,9 +67,8 @@ window.addEventListener('DOMContentLoaded', async () => {
if (user?.name) { if (user?.name) {
document.getElementById("nombreUsuarioHeader").textContent = user.name; document.getElementById("nombreUsuarioHeader").textContent = user.name;
document.getElementById("nombreUsuarioDropdown").textContent = "Coach " + user.name;
} }
} catch (err) { } catch (err) {
console.error("❌ Error al obtener datos del usuario:", err); console.error("❌ Error al obtener usuario:", err);
} }
}); });

View File

@ -298,15 +298,32 @@ tipoPiscinaSelect.addEventListener('change', async () => {
circle.on('dblclick', () => { circle.on('dblclick', () => {
const i = formacionActual.findIndex(a => a.idPersonalizado === atleta.idPersonalizado); const i = formacionActual.findIndex(a => a.idPersonalizado === atleta.idPersonalizado);
const html = ` const html = `
<div class="p-2" style="min-width:200px;"> <div class="p-2" style="min-width:200px;">
<h6 class="mb-1">${atleta.figura || 'Sin figura'}</h6> <h6 class="mb-1">${atleta.figura || 'Sin figura'}</h6>
<button class="btn btn-sm btn-outline-primary w-100 mb-2" onclick="activarModoDireccion(${i})"> Añadir Dirección</button> <button class="btn btn-sm btn-outline-primary w-100 mb-2" onclick="activarModoDireccion(${i})"> Añadir Dirección</button>
<button class="btn btn-sm btn-outline-secondary w-100" onclick="alert('Función editar aún no implementada')"> Editar/Eliminar</button> <button class="btn btn-sm btn-outline-danger w-100 mb-2" onclick="eliminarDireccion(${i})"> Eliminar Dirección</button>
</div> </div>
`; `;
showPopover(circle.x(), circle.y(), html); showPopover(circle.x(), circle.y(), html);
}); });
window.eliminarDireccion = function(index) {
const atleta = formacionActual[index];
if (!atleta) return;
atleta.direccion = null;
const visual = atletasKonva[atleta.idPersonalizado];
if (visual?.flecha) {
visual.flecha.destroy();
visual.flecha = null;
layer.draw();
}
document.getElementById('popoverAtleta')?.remove();
};
circle.on('dragend', () => { circle.on('dragend', () => {
const newX = circle.x(); const newX = circle.x();
@ -644,6 +661,40 @@ formaciones.forEach((f, i) => {
alert('Selecciona una formación desde la línea de tiempo primero.'); alert('Selecciona una formación desde la línea de tiempo primero.');
} }
}); });
const btnEliminarFormacion = document.getElementById('btnEliminarFormacion');
btnEliminarFormacion.addEventListener('click', async () => {
if (indexSeleccionado === null || !formaciones[indexSeleccionado]) {
return alert('Selecciona una formación primero');
}
const confirm = await Swal.fire({
title: '¿Eliminar formación?',
text: 'Esta acción no se puede deshacer',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Sí, eliminar',
cancelButtonText: 'Cancelar'
});
if (!confirm.isConfirmed) return;
try {
const res = await fetch(`/api/rutinas/${rutinaId}/formations/${indexSeleccionado}`, {
method: 'DELETE'
});
if (res.ok) {
Swal.fire('Eliminada', 'La formación ha sido eliminada.', 'success');
window.location.reload();
} else {
Swal.fire('Error', 'No se pudo eliminar la formación.', 'error');
}
} catch (err) {
console.error(err);
Swal.fire('Error', 'Ocurrió un error de red.', 'error');
}
});

View File

@ -1,11 +1,16 @@
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
const rutinaId = new URLSearchParams(window.location.search).get("routineId"); const rutinaId = new URLSearchParams(window.location.search).get("routineId");
if (!rutinaId) return alert("No se proporcionó ID de rutina."); const formationIndex = parseInt(new URLSearchParams(window.location.search).get("formationIndex"));
if (!rutinaId || isNaN(formationIndex)) {
return alert("No se proporcionó ID de rutina o índice de formación.");
}
const tituloRutina = document.getElementById("tituloRutina"); const tituloRutina = document.getElementById("tituloRutina");
const tipoRutina = document.getElementById("tipoRutina"); const tipoRutina = document.getElementById("tipoRutina");
const modalidadRutina = document.getElementById("modalidadRutina"); const modalidadRutina = document.getElementById("modalidadRutina");
const audioPlayer = document.getElementById("audioPlayer"); const audioPlayer = document.getElementById("audioPlayer");
const playBtn = document.getElementById("playPauseBtn");
const timeline = document.getElementById("lineaTiempo"); const timeline = document.getElementById("lineaTiempo");
const rutina = await fetch(`/api/rutinas/${rutinaId}`).then(res => res.json()); const rutina = await fetch(`/api/rutinas/${rutinaId}`).then(res => res.json());
@ -14,8 +19,20 @@ document.addEventListener("DOMContentLoaded", async () => {
tituloRutina.textContent = rutina.title || rutina.nombreCompetencia; tituloRutina.textContent = rutina.title || rutina.nombreCompetencia;
tipoRutina.textContent = rutina.tipoCompetencia; tipoRutina.textContent = rutina.tipoCompetencia;
modalidadRutina.textContent = modalidad; modalidadRutina.textContent = modalidad;
if (rutina.musicUrl) { if (rutina.musicUrl) {
audioPlayer.src = rutina.musicUrl; audioPlayer.src = rutina.musicUrl;
playBtn.onclick = () => {
if (audioPlayer.paused) {
audioPlayer.play();
playBtn.textContent = "⏸ Pausar";
} else {
audioPlayer.pause();
playBtn.textContent = "▶ Reproducir";
}
};
} else {
playBtn.style.display = "none";
} }
const piscinaWidth = rutina.piscina?.width || 850; const piscinaWidth = rutina.piscina?.width || 850;
@ -41,6 +58,7 @@ document.addEventListener("DOMContentLoaded", async () => {
const divPiscina = document.getElementById("piscina"); const divPiscina = document.getElementById("piscina");
divPiscina.style.width = `${piscinaWidth}px`; divPiscina.style.width = `${piscinaWidth}px`;
divPiscina.style.height = `${piscinaHeight}px`; divPiscina.style.height = `${piscinaHeight}px`;
divPiscina.style.background = "#d4f0ff"; // Azul claro
document.getElementById("piscinaContainer").style.width = `${piscinaWidth + 20}px`; document.getElementById("piscinaContainer").style.width = `${piscinaWidth + 20}px`;
function dibujarCuadricula(layer, ancho, alto, tamano = 45) { function dibujarCuadricula(layer, ancho, alto, tamano = 45) {
@ -79,38 +97,38 @@ document.addEventListener("DOMContentLoaded", async () => {
x: atleta.x - 10, x: atleta.x - 10,
y: atleta.y - 7, y: atleta.y - 7,
text: atleta.idPersonalizado, text: atleta.idPersonalizado,
fontSize: 12, fontSize: 14,
fontStyle: 'bold',
fill: 'white' fill: 'white'
}); });
const figuraText = new Konva.Text({ const figuraText = new Konva.Text({
x: atleta.x - 25, x: atleta.x - 30,
y: atleta.y + 18, y: atleta.y + 20,
text: atleta.figura || '', text: atleta.figura || '',
fontSize: 10, fontSize: 11,
fill: '#333', fill: '#333',
fontStyle: 'italic' fontStyle: 'italic'
}); });
const coordText = new Konva.Text({ const coordText = new Konva.Text({
x: atleta.x - 35, x: atleta.x - 38,
y: atleta.y + 30, y: atleta.y + 34,
text: `${metros.metrosX}m, ${metros.metrosY}m`, text: `${metros.metrosX}m, ${metros.metrosY}m`,
fontSize: 9, fontSize: 10,
fill: '#666' fill: '#666'
}); });
layer.add(circle); layer.add(circle, text, figuraText, coordText);
layer.add(text);
layer.add(figuraText);
layer.add(coordText);
if (atleta.direccion) { if (atleta.direccion) {
const dir = new Konva.Line({ const dir = new Konva.Line({
points: [atleta.direccion.x1, atleta.direccion.y1, atleta.direccion.x2, atleta.direccion.y2], points: [atleta.direccion.x1, atleta.direccion.y1, atleta.direccion.x2, atleta.direccion.y2],
stroke: 'black', stroke: 'black',
strokeWidth: 2, strokeWidth: 2,
dash: [4, 4] dash: [4, 4],
pointerLength: 10,
pointerWidth: 10
}); });
layer.add(dir); layer.add(dir);
} }
@ -119,27 +137,21 @@ document.addEventListener("DOMContentLoaded", async () => {
} }
const formaciones = await fetch(`/api/rutinas/${rutinaId}/formations`).then(res => res.json()); const formaciones = await fetch(`/api/rutinas/${rutinaId}/formations`).then(res => res.json());
const formacion = formaciones[formationIndex];
if (Array.isArray(formaciones)) { if (!formacion) {
formaciones.forEach((f, i) => { alert("La formación solicitada no existe.");
const block = document.createElement("button"); return;
block.className = "btn btn-outline-primary btn-sm me-2 step";
block.textContent = `${f.nombreColoquial || `Formación ${i + 1}`} (${f.duracion || '?'}s)`;
block.dataset.index = i;
block.title = f.notasTacticas || '';
timeline.appendChild(block);
});
} }
timeline.addEventListener("click", (e) => { // Pintar solo esta formación
if (!e.target.classList.contains("step")) return; formacion.atletas.forEach(dibujarAtleta);
const index = parseInt(e.target.dataset.index); // Mostrar título visual
const formacion = formaciones[index]; const block = document.createElement("button");
if (!formacion) return; block.className = "btn btn-outline-primary btn-sm me-2 step";
block.textContent = `${formacion.nombreColoquial || `Formación ${formationIndex + 1}`} (${formacion.duracion || '?'}s)`;
layer.destroyChildren(); block.dataset.index = formationIndex;
dibujarCuadricula(layer, stage.width(), stage.height()); block.title = formacion.notasTacticas || '';
formacion.atletas.forEach(a => dibujarAtleta(a)); timeline.appendChild(block);
});
}); });

View File

@ -81,8 +81,7 @@
<button id="btnAgregarAtleta" class="btn btn-primary mt-auto">Añadir Atleta</button> <button id="btnAgregarAtleta" class="btn btn-primary mt-auto">Añadir Atleta</button>
</div> </div>
</div> </div>
<div class="col-lg-8 d-flex flex-column gap-3">
<div class="col-lg-8 d-flex flex-column gap-3">
<div class="card p-4"> <div class="card p-4">
<h5 class="fw-semibold mb-3 text-primary">Piscina</h5> <h5 class="fw-semibold mb-3 text-primary">Piscina</h5>
@ -99,21 +98,24 @@
<div id="piscina"></div> <div id="piscina"></div>
</div> </div>
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3"> <div class="d-flex align-items-center gap-2 flex-wrap mb-3">
<button id="btnEditarFormacion" class="btn btn-outline-secondary btn-sm">Editar formación</button> <button id="btnEditarFormacion" class="btn btn-outline-secondary btn-sm">✏ Editar formación</button>
<div class="flex-grow-1"> <button id="btnEliminarFormacion" class="btn btn-outline-danger btn-sm">🗑 Eliminar Formación</button>
<div id="waveform" style="height: 80px;"></div>
<div class="text-end">
<button id="playPauseBtn" class="btn btn-outline-primary btn-sm">Reproducir</button>
</div>
</div>
</div> </div>
<label class="form-label fw-semibold mt-3">Línea de Tiempo</label> <label class="form-label fw-semibold">Línea de Tiempo</label>
<div id="lineaTiempo" class="timeline-placeholder d-flex flex-wrap gap-2"></div> <div id="lineaTiempo" class="timeline-placeholder d-flex flex-wrap gap-2 mb-3"></div>
<!-- Reproductor abajo -->
<div class="mb-3">
<div id="waveform" style="height: 80px;"></div>
<div class="text-end mt-2">
<button id="playPauseBtn" class="btn btn-outline-primary btn-sm">▶ Reproducir</button>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="card p-4 mt-4"> <div class="card p-4 mt-4">
<h5 class="fw-semibold mb-3 text-primary">Datos de Formación</h5> <h5 class="fw-semibold mb-3 text-primary">Datos de Formación</h5>

View File

@ -49,18 +49,24 @@
</nav> </nav>
<div class="container mt-4"> <div class="container mt-4">
<h2 id="tituloRutina" class="text-center text-primary mb-3">Cargando rutina...</h2> <h2 id="tituloRutina" class="text-center text-primary mb-3">Cargando rutina...</h2>
<p class="text-center"><span id="tipoRutina" class="badge bg-info"></span> | <span id="modalidadRutina" class="badge bg-secondary"></span></p> <p class="text-center">
<span id="tipoRutina" class="badge bg-info"></span> |
<span id="modalidadRutina" class="badge bg-secondary"></span>
</p>
<audio id="audioPlayer" controls class="d-block mx-auto mb-4"></audio> <audio id="audioPlayer" controls class="d-block mx-auto mb-2"></audio>
<div class="text-center">
<button id="playPauseBtn" class="btn btn-sm btn-outline-success">▶ Reproducir</button>
</div>
<div id="piscinaContainer" class="mx-auto mb-4"> <div id="piscinaContainer" class="mx-auto mb-4">
<div id="piscina" class="border bg-white" style="margin: 0 auto;"></div> <div id="piscina" class="border" style="margin: 0 auto; background: #d4f0ff;"></div>
</div> </div>
<h5 class="mt-3">Línea de Tiempo</h5>
<div id="lineaTiempo" class="d-flex flex-wrap gap-2"></div>
<h5 class="mt-3">Línea de Tiempo</h5>
<div id="lineaTiempo" class="d-flex flex-wrap gap-2"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/konva@8.3.12/konva.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/konva@8.3.12/konva.min.js"></script>

View File

@ -258,11 +258,97 @@ router.get('/atleta/:id', async (req, res) => {
const rutinas = await Routine.find({ participantes: atletaId }) const rutinas = await Routine.find({ participantes: atletaId })
.populate('participantes', 'name') .populate('participantes', 'name')
.populate('formaciones.atletas.atletaId', 'name'); .populate('formaciones.atletas.atletaId', 'name');
res.json(rutinas);
// Filtrar solo rutinas donde realmente aparece en alguna formación
const filtradas = rutinas.filter(rutina =>
rutina.formaciones.some(f =>
f.atletas.some(a => a.atletaId?.toString() === atletaId)
)
);
res.json(filtradas);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
res.status(500).json({ mensaje: "Error al obtener rutinas del atleta" }); res.status(500).json({ mensaje: "Error al obtener rutinas del atleta" });
} }
}); });
// DELETE eliminar una formación específica por índice
router.delete('/:id/formations/:index', async (req, res) => {
const { id, index } = req.params;
if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).json({ message: 'ID de rutina inválido' });
}
const i = parseInt(index);
if (isNaN(i) || i < 0) {
return res.status(400).json({ message: 'Índice de formación inválido' });
}
try {
const rutina = await Routine.findById(id);
if (!rutina) {
return res.status(404).json({ message: 'Rutina no encontrada' });
}
if (i >= rutina.formaciones.length) {
return res.status(400).json({ message: 'Índice fuera de rango' });
}
rutina.formaciones.splice(i, 1);
await rutina.save();
const nuevosParticipantes = new Set();
rutina.formaciones.forEach(f => {
f.atletas.forEach(a => {
if (a.atletaId) nuevosParticipantes.add(a.atletaId.toString());
});
});
rutina.participantes = [...nuevosParticipantes];
res.status(200).json({ message: 'Formación eliminada exitosamente' });
} catch (error) {
console.error('❌ Error al eliminar formación:', error);
res.status(500).json({ message: 'Error del servidor', error });
}
});
router.get('/atleta/:id/formaciones', async (req, res) => {
const atletaId = req.params.id;
try {
const rutinas = await Routine.find({ participantes: atletaId });
const resultado = [];
for (const rutina of rutinas) {
const formacionesAtleta = rutina.formaciones
.map((f, i) => {
const match = f.atletas.find(a => a.atletaId?.toString() === atletaId);
return match
? {
rutinaId: rutina._id,
rutinaNombre: rutina.title || rutina.nombreCompetencia,
index: i,
nombreColoquial: f.nombreColoquial,
notasTacticas: f.notasTacticas,
duracion: f.duracion,
atleta: match
}
: null;
})
.filter(Boolean);
if (formacionesAtleta.length > 0) {
resultado.push(...formacionesAtleta);
}
}
res.json(resultado);
} catch (err) {
console.error(err);
res.status(500).json({ mensaje: "Error al obtener formaciones del atleta" });
}
});
module.exports = router; module.exports = router;