1944 lines
64 KiB
JavaScript
1944 lines
64 KiB
JavaScript
document.addEventListener("DOMContentLoaded", function () {
|
|
const loginForm = document.getElementById("loginForm");
|
|
if (loginForm) {
|
|
loginForm.addEventListener("submit", handleLogin);
|
|
}
|
|
|
|
if (document.body.classList.contains("admin")) {
|
|
initializeDashboard();
|
|
}
|
|
});
|
|
|
|
function handleLogin(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(this);
|
|
|
|
fetch(this.action, {
|
|
method: "POST",
|
|
body: formData,
|
|
})
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (data.success) {
|
|
window.location.href = data.redirect;
|
|
} else {
|
|
alert(data.message);
|
|
}
|
|
})
|
|
.catch((error) => console.error("Error:", error));
|
|
}
|
|
|
|
function initializeDashboard() {
|
|
setupSidebarNavigation();
|
|
showSection("dashboard", true);
|
|
}
|
|
|
|
function setupSidebarNavigation() {
|
|
document.querySelectorAll(".sidebar-menu li").forEach((item) => {
|
|
if (item.getAttribute("data-section")) {
|
|
item.addEventListener("click", function () {
|
|
const section = this.getAttribute("data-section");
|
|
showSection(section);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadInitialData() {
|
|
const profesorId = getProfesorId();
|
|
|
|
if (!profesorId) {
|
|
console.error("No se pudo obtener el ID del profesor");
|
|
return;
|
|
}
|
|
|
|
fetch(`api/cursos.php?profesor_id=${profesorId}`)
|
|
.then((response) => response.json())
|
|
.then((courses) => {
|
|
Promise.all([
|
|
fetch(`api/alumnos.php?profesor_id=${profesorId}`).then((res) =>
|
|
res.json()
|
|
),
|
|
fetch(`api/diplomas.php?profesor_id=${profesorId}`).then((res) =>
|
|
res.json()
|
|
),
|
|
]).then(([students, diplomas]) => {
|
|
updateProfessorStats(courses, students, diplomas);
|
|
});
|
|
})
|
|
.catch((error) => console.error("Error:", error));
|
|
}
|
|
|
|
function updateProfessorStats(courses, students, diplomas) {
|
|
document.getElementById("active-courses-count").textContent = courses.length;
|
|
document.getElementById("students-count").textContent = students.length;
|
|
document.getElementById("diplomas-count").textContent = diplomas.length || 0;
|
|
}
|
|
|
|
function getProfesorId() {
|
|
try {
|
|
const profesorElement = document.getElementById("current-profesor");
|
|
if (profesorElement && profesorElement.dataset.id) {
|
|
return profesorElement.dataset.id;
|
|
}
|
|
|
|
const storedUser =
|
|
sessionStorage.getItem("currentUser") ||
|
|
localStorage.getItem("currentUser");
|
|
if (storedUser) {
|
|
const user = JSON.parse(storedUser);
|
|
if (user && user.profesor_id) {
|
|
return user.profesor_id;
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
"No se encontró el ID del profesor en el DOM o en el almacenamiento"
|
|
);
|
|
return null;
|
|
} catch (error) {
|
|
console.error("Error en getProfesorId:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function renderCursosGenerablesPorTipo(tipo) {
|
|
const container = document.getElementById("cursos-generar-diplomas");
|
|
container.innerHTML = `<div class="loader">Cargando cursos...</div>`;
|
|
|
|
try {
|
|
const profesorId = getProfesorId();
|
|
const resCursos = await fetch(`api/cursos.php?profesor_id=${profesorId}`);
|
|
const cursosData = await resCursos.json();
|
|
if (!cursosData.success) throw new Error("Error al cargar cursos");
|
|
|
|
const cursosFiltrados = cursosData.data.filter(
|
|
(c) => c.estado === "completado" && c.tipo === tipo
|
|
);
|
|
|
|
if (cursosFiltrados.length === 0) {
|
|
container.innerHTML = `<div class="no-data"><h3>No hay cursos completados de este tipo</h3></div>`;
|
|
return;
|
|
}
|
|
|
|
const allAlumnos = await fetch("api/alumnos.php").then((r) => r.json());
|
|
const vinculos = await fetch("api/alumnos-cursos.php").then((r) => r.json());
|
|
const diplomasData = await fetch(`api/diplomas.php?profesor_id=${profesorId}`).then(r => r.json());
|
|
const diplomas = diplomasData.success ? diplomasData.data : [];
|
|
|
|
const cursosConAlumnos = cursosFiltrados.map((curso) => {
|
|
const alumnosDelCurso = vinculos.data
|
|
.filter((ac) => ac.curso_id == curso.id)
|
|
.map((ac) => {
|
|
const alumno = allAlumnos.data.find((a) => a.id == ac.alumno_id);
|
|
const diploma = diplomas.find((d) => d.alumno_curso_id == ac.id);
|
|
console.log("Diploma encontrado para AC", ac.id, diploma);
|
|
console.log("VINCULO:", ac.id, "→ DIPLOMA:", diploma);
|
|
return alumno ? {
|
|
...alumno,
|
|
alumno_curso_id: ac.id,
|
|
codigo_unico: diploma?.codigo_unico || null
|
|
} : null;
|
|
})
|
|
.filter(Boolean);
|
|
|
|
return { ...curso, alumnos: alumnosDelCurso };
|
|
});
|
|
|
|
container.innerHTML = cursosConAlumnos.map((curso) => {
|
|
const alumnosHtml = curso.alumnos.length
|
|
? curso.alumnos
|
|
.map((al) => {
|
|
const safeNombre = al.nombre.replace(/'/g, "\\'").replace(/"/g, """);
|
|
const safeEmail = al.email.replace(/'/g, "\\'").replace(/"/g, """);
|
|
const enviarBtn = al.codigo_unico
|
|
? `<button class="btn btn-sm btn-send-diploma" data-codigo="${al.codigo_unico}">Enviar</button>`
|
|
: `<button class="btn btn-sm btn-send-diploma" disabled title="Genera el diploma primero">Enviar</button>`;
|
|
|
|
return `
|
|
<tr>
|
|
<td class="description-cell">
|
|
<span class="truncated-text" id="nombre-${al.id}">${al.nombre}</span>
|
|
<span class="read-more-container" id="nombre-readmore-${al.id}"></span>
|
|
</td>
|
|
<td class="description-cell">
|
|
<span class="truncated-text" id="email-${al.id}">${al.email}</span>
|
|
<span class="read-more-container" id="email-readmore-${al.id}"></span>
|
|
</td>
|
|
<td>
|
|
<div class="action-buttons" style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
<button class="btn btn-sm" onclick="generateDiploma(${al.alumno_curso_id})">Generar Diploma</button>
|
|
${enviarBtn}
|
|
</div>
|
|
</td>
|
|
</tr>`;
|
|
})
|
|
.join("")
|
|
: `<tr><td colspan="3" class="text-muted">No hay alumnos inscritos</td></tr>`;
|
|
|
|
return `
|
|
<div class="card" style="padding: 1.5rem 2rem; border-radius: 8px; margin-bottom: 1.5rem; box-shadow: 0 2px 6px rgba(0,0,0,0.05);">
|
|
<h3 style="margin-bottom: 1rem;">${curso.nombre}
|
|
<span class="badge ${getCourseTypeClass(curso.tipo)}">${formatCourseType(curso.tipo)}</span>
|
|
</h3>
|
|
<div class="table-container">
|
|
<table class="table" style="width: 100%;">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 35%;">Alumno</th>
|
|
<th style="width: 35%;">Email</th>
|
|
<th style="width: 30%;">Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>${alumnosHtml}</tbody>
|
|
</table>
|
|
</div>
|
|
</div>`;
|
|
}).join("");
|
|
|
|
setTimeout(() => {
|
|
cursosConAlumnos.forEach(curso => {
|
|
curso.alumnos.forEach(al => {
|
|
["nombre", "email"].forEach(campo => {
|
|
const span = document.getElementById(`${campo}-${al.id}`);
|
|
const leerMas = document.getElementById(`${campo}-readmore-${al.id}`);
|
|
if (span && leerMas && span.scrollWidth > span.clientWidth) {
|
|
const safeContent = (al[campo] || "")
|
|
.replace(/'/g, "\\'")
|
|
.replace(/"/g, """)
|
|
.replace(/\n/g, "\\n");
|
|
const titulo = campo === "nombre" ? "Nombre completo" : "Correo electrónico completo";
|
|
leerMas.innerHTML = `<a href="#" class="read-more" onclick="showFullDescriptionWithTitle('${titulo}', '${safeContent}'); return false;">Leer más</a>`;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}, 0);
|
|
} catch (err) {
|
|
container.innerHTML = `<div class="card error-card"><p>${err.message}</p></div>`;
|
|
}
|
|
}
|
|
|
|
function showModal(title, content, buttons = []) {
|
|
const existing = document.getElementById("modal-overlay");
|
|
if (existing) existing.remove();
|
|
|
|
const modalOverlay = document.createElement("div");
|
|
modalOverlay.className = "modal-overlay";
|
|
modalOverlay.id = "modal-overlay";
|
|
|
|
const modal = document.createElement("div");
|
|
modal.className = "modal";
|
|
|
|
modal.innerHTML = `
|
|
<div class="modal-header">
|
|
<h3>${title}</h3>
|
|
<button class="close-btn" id="modal-close-btn">×</button>
|
|
</div>
|
|
<div class="modal-body">${content}</div>
|
|
<div class="modal-footer" id="modal-footer"></div>
|
|
`;
|
|
|
|
modalOverlay.appendChild(modal);
|
|
document.body.appendChild(modalOverlay);
|
|
document.body.style.overflow = "hidden";
|
|
|
|
document.getElementById("modal-close-btn").addEventListener("click", closeModal);
|
|
|
|
const footer = document.getElementById("modal-footer");
|
|
|
|
buttons.forEach((btn) => {
|
|
const button = document.createElement("button");
|
|
button.className = "btn " + btn.class;
|
|
button.innerText = btn.text;
|
|
|
|
button.addEventListener("click", () => {
|
|
if (typeof btn.handler === "function") {
|
|
btn.handler();
|
|
} else if (typeof btn.handler === "string") {
|
|
if (btn.handler.endsWith(")")) {
|
|
eval(btn.handler);
|
|
} else if (typeof window[btn.handler] === "function") {
|
|
window[btn.handler](); // closeModal
|
|
}
|
|
}
|
|
});
|
|
|
|
footer.appendChild(button);
|
|
});
|
|
}
|
|
|
|
window.showFullDescription = function (descripcion) {
|
|
showModal("Descripción del Curso", `<div class="modal-description">${descripcion}</div>`);
|
|
};
|
|
|
|
window.showFullDescriptionWithTitle = function (titulo, contenido) {
|
|
showModal(titulo, `<div class="modal-description">${contenido}</div>`);
|
|
};
|
|
|
|
window.closeModal = function () {
|
|
const modal = document.getElementById("modal-overlay");
|
|
if (modal) {
|
|
modal.remove();
|
|
document.body.style.overflow = "";
|
|
}
|
|
};
|
|
|
|
function showToast(type, message) {
|
|
const toast = document.createElement("div");
|
|
toast.className = `toast toast-${type}`;
|
|
toast.textContent = message;
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.classList.add("fade-out");
|
|
setTimeout(() => toast.remove(), 300);
|
|
}, 3000);
|
|
}
|
|
|
|
function showSection(sectionId, isInitialLoad = false) {
|
|
updateActiveMenu(sectionId);
|
|
|
|
const sectionElement = document.getElementById(`${sectionId}-content`);
|
|
if (!sectionElement) return;
|
|
|
|
document.querySelectorAll(".content-section").forEach((s) => {
|
|
s.classList.remove("active");
|
|
});
|
|
sectionElement.classList.add("active");
|
|
|
|
if (sectionId === "dashboard") {
|
|
} else {
|
|
loadDynamicContent(sectionId, sectionElement);
|
|
}
|
|
}
|
|
|
|
function updateActiveMenu(sectionId) {
|
|
document.querySelectorAll(".sidebar-menu li").forEach((li) => {
|
|
li.classList.remove("active");
|
|
});
|
|
|
|
const activeItem = document.querySelector(
|
|
`.sidebar-menu li[data-section="${sectionId}"]`
|
|
);
|
|
if (activeItem) activeItem.classList.add("active");
|
|
}
|
|
|
|
function loadDynamicContent(sectionId, container) {
|
|
container.innerHTML = '<div class="loader">Cargando...</div>';
|
|
|
|
switch (sectionId) {
|
|
case "dashboard":
|
|
loadDashboardContent(container);
|
|
break;
|
|
case "courses":
|
|
loadProfessorCourses(container);
|
|
break;
|
|
case "students":
|
|
loadStudentsManagement(container);
|
|
break;
|
|
case "diplomas":
|
|
loadDiplomasSection(container);
|
|
break;
|
|
default:
|
|
container.innerHTML =
|
|
'<div class="card"><h2>Sección no implementada</h2></div>';
|
|
}
|
|
}
|
|
|
|
async function loadDashboardContent(container, forceReload = false) {
|
|
try {
|
|
container.innerHTML = '<div class="loader">Cargando datos...</div>';
|
|
|
|
const profesorId = getProfesorId();
|
|
if (!profesorId) {
|
|
throw new Error("Debes iniciar sesión nuevamente");
|
|
}
|
|
|
|
const cacheKey = `dashboardData-${profesorId}`;
|
|
if (!forceReload && sessionStorage.getItem(cacheKey)) {
|
|
const cachedData = JSON.parse(sessionStorage.getItem(cacheKey));
|
|
renderDashboard(container, cachedData);
|
|
return;
|
|
}
|
|
|
|
const [coursesRes, studentsRes, diplomasRes] = await Promise.all([
|
|
fetch(`api/cursos.php?profesor_id=${profesorId}&t=${Date.now()}`),
|
|
fetch(`api/alumnos.php?t=${Date.now()}`),
|
|
fetch(`api/diplomas.php?profesor_id=${profesorId}&t=${Date.now()}`),
|
|
]);
|
|
|
|
const [coursesData, studentsData, diplomasData] = await Promise.all([
|
|
coursesRes.json(),
|
|
studentsRes.json(),
|
|
diplomasRes.json(),
|
|
]);
|
|
|
|
if (
|
|
!coursesData.success ||
|
|
!studentsData.success ||
|
|
!diplomasData.success
|
|
) {
|
|
throw new Error("Error en los datos recibidos");
|
|
}
|
|
|
|
const dashboardData = {
|
|
cursos: coursesData.data,
|
|
alumnos: studentsData.data,
|
|
diplomas: diplomasData.data,
|
|
lastUpdated: new Date().toLocaleTimeString(),
|
|
};
|
|
|
|
sessionStorage.setItem(cacheKey, JSON.stringify(dashboardData));
|
|
renderDashboard(container, dashboardData);
|
|
} catch (error) {
|
|
console.error("Error:", error);
|
|
container.innerHTML = `
|
|
<div class="card error-card">
|
|
<h2>Error al cargar datos</h2>
|
|
<p>${error.message}</p>
|
|
<button class="btn" onclick="loadDashboardContent(this.parentElement.parentElement, true)">
|
|
Reintentar
|
|
</button>
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
function renderDashboard(container, data) {
|
|
const cursos = data.cursos;
|
|
const activos = cursos.filter(c => c.estado === "activo");
|
|
const inyeccion = cursos.filter(c => c.tipo === "inyeccion");
|
|
const pildora = cursos.filter(c => c.tipo === "pildora");
|
|
const tratamiento = cursos.filter(c => c.tipo === "tratamiento");
|
|
|
|
container.innerHTML = `
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2>Resumen General</h2>
|
|
<small class="text-muted">Actualizado: ${data.lastUpdated}</small>
|
|
<button class="btn btn-sm" onclick="loadDashboardContent(this.closest('.content-section'), true)">
|
|
🔄 Actualizar
|
|
</button>
|
|
</div>
|
|
<div class="stats">
|
|
<p>• Total de cursos: <strong>${cursos.length}</strong></p>
|
|
<p>• Cursos activos: <strong id="active-courses-count">${activos.length}</strong></p>
|
|
<p>• Tipo Inyección: <strong>${inyeccion.length}</strong></p>
|
|
<p>• Tipo Píldora: <strong>${pildora.length}</strong></p>
|
|
<p>• Tipo Tratamiento: <strong>${tratamiento.length}</strong></p>
|
|
<p>• Alumnos registrados: <strong id="students-count">${data.alumnos.length}</strong></p>
|
|
<p>• Diplomas emitidos: <strong id="diplomas-count">${data.diplomas.length}</strong></p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function loadProfessorCourses(container) {
|
|
fetch(`api/cursos.php?profesor_id=${getProfesorId()}`)
|
|
.then((response) => response.json())
|
|
.then((res) => {
|
|
if (!res.success) throw new Error("No se pudieron cargar los cursos");
|
|
const courses = res.data;
|
|
|
|
container.innerHTML = `
|
|
<div class="card">
|
|
<h2>Mis Cursos</h2>
|
|
<form id="courseForm">
|
|
<label>Nombre del Curso *</label>
|
|
<input type="text" name="nombre" required>
|
|
|
|
<label>Descripción</label>
|
|
<textarea name="descripcion" maxlength="250" rows="4" style="width: 100%; padding: 0.75rem; margin: 0.5rem 0 1rem 0; border: 1px solid #e2e8f0; border-radius: 6px; font-family: 'Inter', sans-serif;"></textarea>
|
|
|
|
<label>Tipo de Curso *</label>
|
|
<select id="courseType" name="tipo" required>
|
|
<option value="inyeccion">Inyección</option>
|
|
<option value="pildora">Píldora</option>
|
|
<option value="tratamiento">Tratamiento</option>
|
|
</select>
|
|
|
|
<label>Docente que imparte (Opcional)</label>
|
|
<input type="text" name="docente" style="margin-bottom: 1rem;">
|
|
|
|
<label>Horas trabajadas (Opcional)</label>
|
|
<input type="number" name="horas_trabajadas" min="0" step="1" style="margin-bottom: 1rem;">
|
|
|
|
<div id="competencesField" class="oculto">
|
|
<label>Competencias Asociadas *</label>
|
|
<small style="display:block; margin-bottom: 0.5rem; color: #64748b;">
|
|
Escribe <strong>una competencia por línea</strong>. Estas se mostrarán individualmente en el diploma.
|
|
</small>
|
|
<textarea name="competencias" rows="4" style="width: 100%; padding: 0.75rem; border: 1px solid #e2e8f0; border-radius: 6px; font-family: 'Inter', sans-serif;"></textarea>
|
|
</div>
|
|
|
|
<label style="margin-top: 1rem; display: block;">Estado</label>
|
|
<select name="estado">
|
|
<option value="activo">Activo</option>
|
|
<option value="archivado">Archivado</option>
|
|
</select>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn">
|
|
<span id="submitCourseText">Guardar</span>
|
|
<span class="spinner-border spinner-border-sm" id="submitCourseSpinner" style="display:none;"></span>
|
|
</button>
|
|
<button type="button" class="btn btn-outline" id="cancelCourseBtn" style="display:none;">
|
|
Cancelar
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
|
|
<h2 style="margin: 0;">Lista de Cursos</h2>
|
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
<label for="courseFilter"><strong>Mostrar:</strong></label>
|
|
<select id="courseFilter" class="form-control" style="padding: 0.3rem 0.6rem; border-radius: 6px;">
|
|
<option value="todos">Todos</option>
|
|
<option value="activo">Activos</option>
|
|
<option value="completado">Completados</option>
|
|
<option value="archivado">Archivados</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-container" style="margin-top: 1rem;">
|
|
<table class="courses-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Nombre</th>
|
|
<th>Descripción</th>
|
|
<th>Tipo</th>
|
|
<th>Docente</th>
|
|
<th>Horas</th>
|
|
<th>Estado</th>
|
|
<th>Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${courses
|
|
.map((course) => {
|
|
const desc = course.descripcion || "-";
|
|
const safeDesc = desc.replace(/"/g, """).replace(/'/g, "\\'");
|
|
const docente = course.docente || "-";
|
|
const horas = course.horas_trabajadas || "-";
|
|
return `
|
|
<tr>
|
|
<td>${course.nombre}</td>
|
|
<td class="description-cell">
|
|
<span class="truncated-text" id="desc-${course.id}">
|
|
${desc}
|
|
</span>
|
|
<span class="read-more-container" id="readmore-${course.id}"></span>
|
|
</td>
|
|
<td><span class="badge ${getCourseTypeClass(course.tipo)}">${formatCourseType(course.tipo)}</span></td>
|
|
<td>${docente}</td>
|
|
<td>${horas}</td>
|
|
<td>
|
|
<span class="badge ${
|
|
course.estado === "activo"
|
|
? "active"
|
|
: course.estado === "completado"
|
|
? "completed"
|
|
: course.estado === "archivado"
|
|
? "archived"
|
|
: "inactive"
|
|
}">${course.estado}</span>
|
|
</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button class="btn btn-sm" onclick="editCourse(${course.id})">Editar</button>
|
|
<button class="btn btn-sm btn-danger" onclick="confirmDeleteCourse(${course.id})">Eliminar</button>
|
|
</div>
|
|
</td>
|
|
</tr>`;
|
|
})
|
|
.join("")}
|
|
</tbody>
|
|
</table>
|
|
<div id="emptyCourseMessage" style="display: none; padding: 1rem; text-align: center; color: #64748b;">
|
|
Cuando haya cursos en esta categoría, se mostrarán aquí.
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
setTimeout(() => {
|
|
courses.forEach(course => {
|
|
const span = document.getElementById(`desc-${course.id}`);
|
|
const leerMas = document.getElementById(`readmore-${course.id}`);
|
|
if (span && leerMas && span.scrollWidth > span.clientWidth) {
|
|
const safeDesc = (course.descripcion || "")
|
|
.replace(/'/g, "\\'")
|
|
.replace(/"/g, """)
|
|
.replace(/\n/g, "\\n");
|
|
leerMas.innerHTML = `
|
|
<a href="#" class="read-more" onclick="showFullDescription('${safeDesc}'); return false;">Leer más</a>`;
|
|
}
|
|
});
|
|
}, 0);
|
|
|
|
const filtro = document.getElementById("courseFilter");
|
|
if (filtro) {
|
|
filtro.addEventListener("change", function () {
|
|
const estadoSeleccionado = this.value;
|
|
const filas = document.querySelectorAll(".courses-table tbody tr");
|
|
let visibles = 0;
|
|
|
|
filas.forEach(fila => {
|
|
const badgeEstado = fila.querySelector("td:nth-child(6) .badge");
|
|
const estado = badgeEstado?.textContent.toLowerCase() || "";
|
|
if (estadoSeleccionado === "todos" || estado === estadoSeleccionado) {
|
|
fila.style.display = "";
|
|
visibles++;
|
|
} else {
|
|
fila.style.display = "none";
|
|
}
|
|
});
|
|
|
|
const mensajeVacio = document.getElementById("emptyCourseMessage");
|
|
if (mensajeVacio) {
|
|
mensajeVacio.style.display = visibles === 0 ? "block" : "none";
|
|
}
|
|
});
|
|
|
|
filtro.dispatchEvent(new Event("change"));
|
|
}
|
|
|
|
setupCourseForm();
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error al cargar cursos:", err);
|
|
container.innerHTML = `<div class="card error-card">No se pudieron cargar los cursos</div>`;
|
|
});
|
|
}
|
|
|
|
function formatCourseType(type) {
|
|
const types = {
|
|
pildora: "Píldora",
|
|
inyeccion: "Inyección",
|
|
tratamiento: "Tratamiento"
|
|
};
|
|
return types[type] || type;
|
|
}
|
|
|
|
window.createCourse = function (data, callback) {
|
|
fetch("api/cursos.php", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
})
|
|
.then((response) => response.json())
|
|
.then((res) => {
|
|
if (res.success) {
|
|
callback(true, "Curso creado correctamente");
|
|
} else {
|
|
callback(false, res.error || "Error al crear curso");
|
|
}
|
|
})
|
|
.catch(() => {
|
|
callback(false, "No se pudo conectar con el servidor");
|
|
});
|
|
};
|
|
|
|
window.updateCourse = function (id, data, callback) {
|
|
data.id = id;
|
|
|
|
fetch("api/cursos.php", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
})
|
|
.then((response) => response.json())
|
|
.then((res) => {
|
|
if (res.success) {
|
|
callback(true, "Curso actualizado correctamente");
|
|
} else {
|
|
callback(false, res.error || "Error al actualizar curso");
|
|
}
|
|
})
|
|
.catch(() => {
|
|
callback(false, "No se pudo conectar con el servidor");
|
|
});
|
|
};
|
|
|
|
function setupCourseForm() {
|
|
const courseTypeSelect = document.getElementById('courseType');
|
|
const competencesField = document.getElementById('competencesField');
|
|
const competenciasInput = competencesField
|
|
? competencesField.querySelector('textarea[name="competencias"]')
|
|
: null;
|
|
|
|
if (competenciasInput) {
|
|
competenciasInput.addEventListener("keydown", function (e) {
|
|
if (e.key === "Enter") {
|
|
e.stopPropagation();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (courseTypeSelect && competenciasInput) {
|
|
courseTypeSelect.addEventListener('change', function () {
|
|
const isTratamiento = this.value === 'tratamiento';
|
|
competencesField.classList.toggle('oculto', !isTratamiento);
|
|
competenciasInput.required = isTratamiento;
|
|
if (!isTratamiento) {
|
|
competenciasInput.value = '';
|
|
}
|
|
});
|
|
|
|
const isTratamiento = courseTypeSelect.value === 'tratamiento';
|
|
competencesField.classList.toggle('oculto', !isTratamiento);
|
|
competenciasInput.required = isTratamiento;
|
|
}
|
|
|
|
const form = document.getElementById("courseForm");
|
|
if (!form) return;
|
|
|
|
const cancelBtn = document.getElementById("cancelCourseBtn");
|
|
const submitBtn = form.querySelector("button[type='submit']");
|
|
const submitText = document.getElementById("submitCourseText");
|
|
const spinner = document.getElementById("submitCourseSpinner");
|
|
|
|
const estadoSelect = form.querySelector('select[name="estado"]');
|
|
if (estadoSelect) {
|
|
estadoSelect.innerHTML = "";
|
|
const opcionesBase = [
|
|
{ value: "activo", label: "Activo" },
|
|
{ value: "archivado", label: "Archivado" }
|
|
];
|
|
|
|
const isEdit = form.dataset.editing === "true";
|
|
if (isEdit) {
|
|
opcionesBase.push({ value: "completado", label: "Completado" });
|
|
}
|
|
|
|
opcionesBase.forEach(opt => {
|
|
const option = document.createElement("option");
|
|
option.value = opt.value;
|
|
option.textContent = opt.label;
|
|
estadoSelect.appendChild(option);
|
|
});
|
|
}
|
|
|
|
if (cancelBtn) {
|
|
cancelBtn.addEventListener("click", function () {
|
|
form.reset();
|
|
form.dataset.editing = "false";
|
|
delete form.dataset.courseId;
|
|
submitText.textContent = "Guardar";
|
|
spinner.style.display = "none";
|
|
submitBtn.disabled = false;
|
|
cancelBtn.style.display = "none";
|
|
|
|
if (estadoSelect) {
|
|
estadoSelect.innerHTML = "";
|
|
["activo", "archivado"].forEach(val => {
|
|
const opt = document.createElement("option");
|
|
opt.value = val;
|
|
opt.textContent = val.charAt(0).toUpperCase() + val.slice(1);
|
|
estadoSelect.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
const tipoCurso = document.getElementById("courseType");
|
|
if (tipoCurso) {
|
|
tipoCurso.value = "inyeccion";
|
|
tipoCurso.dispatchEvent(new Event("change"));
|
|
}
|
|
|
|
const actions = form.querySelector(".form-actions");
|
|
if (actions) actions.appendChild(cancelBtn);
|
|
});
|
|
}
|
|
|
|
form.addEventListener("submit", function (e) {
|
|
e.preventDefault();
|
|
|
|
submitText.textContent = "Procesando...";
|
|
spinner.style.display = "inline-block";
|
|
submitBtn.disabled = true;
|
|
|
|
const formData = new FormData(this);
|
|
const jsonData = {
|
|
nombre: formData.get("nombre"),
|
|
descripcion: formData.get("descripcion"),
|
|
competencias: formData.get("competencias"),
|
|
tipo: formData.get("tipo"),
|
|
estado: formData.get("estado"),
|
|
docente: formData.get("docente") || null,
|
|
horas_trabajadas: formData.get("horas_trabajadas") || null,
|
|
profesor_id: getProfesorId(),
|
|
};
|
|
|
|
const isEdit = form.dataset.editing === "true";
|
|
const courseId = form.dataset.courseId;
|
|
|
|
const callback = (success, message) => {
|
|
if (success) {
|
|
showToast("success", message);
|
|
showSection("courses");
|
|
} else {
|
|
showToast("error", message);
|
|
}
|
|
submitText.textContent = "Guardar";
|
|
spinner.style.display = "none";
|
|
submitBtn.disabled = false;
|
|
};
|
|
|
|
if (isEdit) {
|
|
updateCourse(courseId, jsonData, callback);
|
|
} else {
|
|
createCourse(jsonData, callback);
|
|
}
|
|
});
|
|
|
|
const descripcionTextarea = form.querySelector('textarea[name="descripcion"]');
|
|
if (descripcionTextarea) {
|
|
const autoResize = () => {
|
|
descripcionTextarea.style.height = "auto";
|
|
descripcionTextarea.style.height = descripcionTextarea.scrollHeight + "px";
|
|
};
|
|
descripcionTextarea.addEventListener("input", autoResize);
|
|
autoResize();
|
|
}
|
|
}
|
|
|
|
window.editCourse = function (id) {
|
|
fetch(`api/cursos.php?profesor_id=${getProfesorId()}`)
|
|
.then((response) => response.json())
|
|
.then((res) => {
|
|
const course = res.data.find((c) => c.id == id);
|
|
if (!course) return;
|
|
|
|
const form = document.getElementById("courseForm");
|
|
form.nombre.value = course.nombre;
|
|
form.descripcion.value = course.descripcion || "";
|
|
form.tipo.value = course.tipo;
|
|
form.docente.value = course.docente || "";
|
|
form.horas_trabajadas.value = course.horas_trabajadas || "";
|
|
|
|
const competencesField = document.getElementById("competencesField");
|
|
const competenciasInput = competencesField.querySelector('textarea[name="competencias"]');
|
|
if (course.tipo === "tratamiento") {
|
|
competencesField.classList.remove("oculto");
|
|
competenciasInput.value = course.competencias || "";
|
|
competenciasInput.required = true;
|
|
} else {
|
|
competencesField.classList.add("oculto");
|
|
competenciasInput.value = "";
|
|
competenciasInput.required = false;
|
|
}
|
|
|
|
const estadoSelect = form.querySelector('select[name="estado"]');
|
|
if (estadoSelect) {
|
|
estadoSelect.innerHTML = "";
|
|
["activo", "archivado", "completado"].forEach((estado) => {
|
|
const option = document.createElement("option");
|
|
option.value = estado;
|
|
option.textContent = estado.charAt(0).toUpperCase() + estado.slice(1);
|
|
estadoSelect.appendChild(option);
|
|
});
|
|
estadoSelect.value = course.estado || "activo";
|
|
}
|
|
|
|
form.dataset.editing = "true";
|
|
form.dataset.courseId = id;
|
|
|
|
const submitText = document.getElementById("submitCourseText");
|
|
if (submitText) submitText.textContent = "Actualizar";
|
|
|
|
const cancelBtn = document.getElementById("cancelCourseBtn");
|
|
if (cancelBtn) cancelBtn.style.display = "inline-block";
|
|
|
|
form.scrollIntoView({ behavior: "smooth" });
|
|
});
|
|
};
|
|
|
|
window.confirmDeleteCourse = function (id) {
|
|
showModal(
|
|
"Confirmar eliminación",
|
|
"¿Estás seguro de eliminar este curso? Esta acción no se puede deshacer.",
|
|
[
|
|
{
|
|
text: "Cancelar",
|
|
class: "btn-outline",
|
|
handler: "closeModal",
|
|
},
|
|
{
|
|
text: "Eliminar",
|
|
class: "btn-danger",
|
|
handler: `deleteCourse(${id})`,
|
|
},
|
|
]
|
|
);
|
|
};
|
|
|
|
window.deleteCourse = function (id) {
|
|
fetch(`api/cursos.php?id=${id}`, {
|
|
method: "DELETE",
|
|
})
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (data.success) {
|
|
showToast("success", "Curso eliminado correctamente");
|
|
closeModal();
|
|
loadProfessorCourses(document.querySelector("#courses-content"));
|
|
} else {
|
|
showToast("error", data.error || "No se pudo eliminar el curso");
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
showToast("error", "Error al eliminar el curso");
|
|
});
|
|
};
|
|
|
|
// Gestión de alumnos
|
|
function loadStudentsManagement(container) {
|
|
container.innerHTML = '<div class="loader">Cargando alumnos...</div>';
|
|
|
|
fetch("api/alumnos.php")
|
|
.then((response) => {
|
|
if (!response.ok) throw new Error("Error en la respuesta del servidor");
|
|
return response.json();
|
|
})
|
|
.then((data) => {
|
|
if (!data.success)
|
|
throw new Error(data.error || "Error al obtener alumnos");
|
|
|
|
container.innerHTML = `
|
|
<div class="students-management">
|
|
${renderStudentForm()}
|
|
${renderStudentsTable(data.data || [])}
|
|
</div>
|
|
`;
|
|
|
|
setupStudentForm();
|
|
|
|
(data.data || []).forEach((alumno) => loadStudentCourses(alumno.id));
|
|
|
|
const filtro = document.getElementById("studentCourseFilter");
|
|
if (filtro) {
|
|
filtro.addEventListener("change", () => {
|
|
document.querySelectorAll(".students-table tbody tr").forEach((fila) => {
|
|
const estadoSeleccionado = filtro.value;
|
|
const badges = fila.querySelectorAll(".course-badge");
|
|
const visible = Array.from(badges).some((badge) => {
|
|
const estado = (badge.dataset.estado || "").toLowerCase();
|
|
return estadoSeleccionado === "todos" || estado === estadoSeleccionado;
|
|
});
|
|
fila.style.display = visible ? "" : "none";
|
|
});
|
|
});
|
|
|
|
filtro.dispatchEvent(new Event("change"));
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error:", error);
|
|
container.innerHTML = `
|
|
<div class="card error-card">
|
|
<h2>Error al cargar alumnos</h2>
|
|
<p>${error.message}</p>
|
|
<button class="btn" onclick="loadStudentsManagement(this.closest('.content-section'))">
|
|
Reintentar
|
|
</button>
|
|
</div>`;
|
|
});
|
|
}
|
|
|
|
function renderStudentForm() {
|
|
return `
|
|
<div class="card">
|
|
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
|
|
<h2>Gestión de Alumnos</h2>
|
|
<div style="display: flex; align-items: center; gap: 0.5rem; position: relative;">
|
|
<div style="position: relative;">
|
|
<svg onclick="downloadCSVTemplate()"
|
|
xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
|
fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
style="cursor: pointer;">
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
<path d="M12 16v-1"></path>
|
|
<path d="M12 12a2 2 0 1 0-2-2"></path>
|
|
</svg>
|
|
<div class="tooltip-csv">
|
|
Descargar formato CSV
|
|
</div>
|
|
</div>
|
|
|
|
<label class="btn btn-primary" style="margin: 0;">
|
|
Importar CSV
|
|
<input type="file" accept=".csv" onchange="handleCSVUpload(event)" style="display: none;">
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<form id="studentForm">
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="nombre">Nombre*</label>
|
|
<input type="text" id="nombre" name="nombre" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="email">Email*</label>
|
|
<input type="email" id="email" name="email" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="telefono">Teléfono</label>
|
|
<input type="tel" id="telefono" name="telefono">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="tipoCurso">Tipo de Curso*</label>
|
|
<select id="tipoCurso" required>
|
|
<option value="">Seleccionar tipo</option>
|
|
<option value="inyeccion">Inyección</option>
|
|
<option value="pildora">Píldora</option>
|
|
<option value="tratamiento">Tratamiento</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="curso">Curso*</label>
|
|
<select id="curso" required>
|
|
<option value="">Selecciona primero un tipo</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions" id="studentFormActions">
|
|
<button type="submit" class="btn">
|
|
<span id="submitText">Guardar</span>
|
|
<span class="spinner-border spinner-border-sm" id="submitSpinner" style="display:none;"></span>
|
|
</button>
|
|
<button type="button" class="btn btn-outline" onclick="resetStudentForm()" id="cancelBtn" style="display:none;">
|
|
Cancelar
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>`;
|
|
}
|
|
|
|
function setupStudentForm() {
|
|
const form = document.getElementById("studentForm");
|
|
if (!form) return;
|
|
|
|
const tipoCurso = document.getElementById("tipoCurso");
|
|
const curso = document.getElementById("curso");
|
|
|
|
tipoCurso.addEventListener("change", () => {
|
|
const tipo = tipoCurso.value;
|
|
curso.innerHTML = '<option value="">Cargando...</option>';
|
|
fetch(`api/cursos.php?tipo=${tipo}`)
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
if (data.success) {
|
|
curso.innerHTML = '<option value="">Seleccionar curso</option>';
|
|
data.data.forEach((c) => {
|
|
const opt = document.createElement("option");
|
|
opt.value = c.id;
|
|
opt.textContent = c.nombre;
|
|
curso.appendChild(opt);
|
|
});
|
|
} else {
|
|
curso.innerHTML = '<option value="">Sin resultados</option>';
|
|
}
|
|
})
|
|
.catch(() => {
|
|
curso.innerHTML = '<option value="">Error al cargar</option>';
|
|
});
|
|
});
|
|
|
|
form.addEventListener("submit", function (e) {
|
|
e.preventDefault();
|
|
submitStudentForm();
|
|
});
|
|
|
|
const searchInput = document.getElementById("studentSearch");
|
|
if (searchInput) {
|
|
searchInput.addEventListener("input", function () {
|
|
filterStudents(this.value.toLowerCase());
|
|
});
|
|
}
|
|
}
|
|
|
|
function submitStudentForm() {
|
|
const form = document.getElementById("studentForm");
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
const submitText = document.getElementById("submitText");
|
|
const spinner = document.getElementById("submitSpinner");
|
|
|
|
submitText.textContent = "Procesando...";
|
|
spinner.style.display = "inline-block";
|
|
submitBtn.disabled = true;
|
|
|
|
const formData = new FormData(form);
|
|
const jsonData = {
|
|
nombre: formData.get("nombre"),
|
|
email: formData.get("email"),
|
|
telefono: formData.get("telefono"),
|
|
curso_id: document.getElementById("curso").value,
|
|
};
|
|
|
|
const isEdit = form.dataset.editing === "true";
|
|
const studentId = form.dataset.studentId;
|
|
const url = "api/alumnos.php";
|
|
const method = isEdit ? "PUT" : "POST";
|
|
|
|
if (isEdit) {
|
|
jsonData.id = studentId;
|
|
}
|
|
|
|
fetch(url, {
|
|
method: method,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(jsonData),
|
|
})
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (!data.success)
|
|
throw new Error(data.error || "Error al guardar alumno");
|
|
|
|
showToast(
|
|
"success",
|
|
data.message || (isEdit ? "Alumno actualizado" : "Alumno creado")
|
|
);
|
|
resetStudentForm();
|
|
loadStudentsManagement(document.querySelector("#students-content"));
|
|
})
|
|
.catch((error) => {
|
|
showToast("error", error.message || "Error en el servidor");
|
|
})
|
|
.finally(() => {
|
|
submitText.textContent = "Guardar";
|
|
spinner.style.display = "none";
|
|
submitBtn.disabled = false;
|
|
});
|
|
}
|
|
|
|
window.createStudent = function (data) {
|
|
fetch("api/alumnos.php", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
})
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (data.success) {
|
|
showSection("students");
|
|
}
|
|
})
|
|
.catch((error) => console.error("Error:", error));
|
|
};
|
|
|
|
window.updateStudent = function (id, data) {
|
|
data.id = id;
|
|
|
|
fetch("api/alumnos.php", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
})
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (data.success) {
|
|
showSection("students");
|
|
}
|
|
})
|
|
.catch((error) => console.error("Error:", error));
|
|
};
|
|
|
|
window.editStudent = function (id) {
|
|
fetch(`api/alumnos.php`)
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (!data.success)
|
|
throw new Error(data.error || "Error al obtener alumnos");
|
|
|
|
const alumno = data.data.find((a) => a.id == id);
|
|
if (!alumno) throw new Error("Alumno no encontrado");
|
|
|
|
const form = document.getElementById("studentForm");
|
|
form.nombre.value = alumno.nombre;
|
|
form.email.value = alumno.email;
|
|
form.telefono.value = alumno.telefono || "";
|
|
|
|
form.dataset.editing = "true";
|
|
form.dataset.studentId = id;
|
|
|
|
document.getElementById("submitText").textContent = "Actualizar";
|
|
const formActions = document.getElementById("studentFormActions");
|
|
const submitBtn = formActions.querySelector("button[type='submit']");
|
|
const cancelBtn = document.getElementById("cancelBtn");
|
|
|
|
if (submitBtn && cancelBtn) {
|
|
formActions.innerHTML = "";
|
|
formActions.appendChild(submitBtn);
|
|
formActions.appendChild(cancelBtn);
|
|
}
|
|
|
|
document.getElementById("cancelBtn").style.display = "inline-block";
|
|
|
|
fetch(`api/alumnos-cursos.php?alumno_id=${id}`)
|
|
.then((res) => res.json())
|
|
.then((asignaciones) => {
|
|
if (asignaciones.success && asignaciones.data.length > 0) {
|
|
const curso = asignaciones.data[0];
|
|
const tipoCursoSelect = document.getElementById("tipoCurso");
|
|
const cursoSelect = document.getElementById("curso");
|
|
|
|
tipoCursoSelect.value = curso.tipo;
|
|
tipoCursoSelect.dispatchEvent(new Event("change"));
|
|
|
|
setTimeout(() => {
|
|
cursoSelect.value = curso.id;
|
|
}, 300);
|
|
}
|
|
});
|
|
|
|
form.scrollIntoView({ behavior: "smooth" });
|
|
})
|
|
.catch((error) => {
|
|
showToast("error", error.message || "Error al cargar alumno");
|
|
});
|
|
};
|
|
|
|
window.deleteStudent = function (id) {
|
|
if (!id) {
|
|
showToast("error", "ID de alumno no proporcionado");
|
|
return;
|
|
}
|
|
|
|
fetch(`api/alumnos.php?id=${id}`, {
|
|
method: "DELETE",
|
|
})
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
if (!data.success)
|
|
throw new Error(data.error || "Error al eliminar alumno");
|
|
|
|
showToast("success", data.message || "Alumno eliminado");
|
|
closeModal();
|
|
loadStudentsManagement(document.querySelector("#students-content"));
|
|
})
|
|
.catch((error) => {
|
|
showToast("error", error.message || "Error al eliminar alumno");
|
|
});
|
|
};
|
|
|
|
|
|
window.confirmDeleteStudent = function (id) {
|
|
showModal(
|
|
"Confirmar eliminación",
|
|
"¿Estás seguro de eliminar este alumno? Esta acción no se puede deshacer.",
|
|
[
|
|
{
|
|
text: "Cancelar",
|
|
class: "btn-outline",
|
|
handler: "closeModal",
|
|
},
|
|
{
|
|
text: "Eliminar",
|
|
class: "btn-danger",
|
|
handler: "deleteStudent(" + id + ")"
|
|
},
|
|
]
|
|
);
|
|
};
|
|
|
|
window.resetStudentForm = function () {
|
|
const form = document.getElementById("studentForm");
|
|
form.reset();
|
|
form.dataset.editing = "false";
|
|
delete form.dataset.studentId;
|
|
|
|
const submitText = document.getElementById("submitText");
|
|
const cancelBtn = document.getElementById("cancelBtn");
|
|
const formActions = document.getElementById("studentFormActions");
|
|
const submitBtn = formActions.querySelector("button[type='submit']");
|
|
|
|
if (submitText) submitText.textContent = "Guardar";
|
|
if (cancelBtn) cancelBtn.style.display = "none";
|
|
|
|
if (submitBtn && cancelBtn) {
|
|
formActions.innerHTML = "";
|
|
formActions.appendChild(submitBtn);
|
|
formActions.appendChild(cancelBtn);
|
|
}
|
|
|
|
const tipoCurso = document.getElementById("tipoCurso");
|
|
const curso = document.getElementById("curso");
|
|
if (tipoCurso) tipoCurso.selectedIndex = 0;
|
|
if (curso) curso.innerHTML = '<option value="">Selecciona primero un tipo</option>';
|
|
};
|
|
|
|
function filterStudents(searchTerm) {
|
|
const rows = document.querySelectorAll(".students-table tbody tr");
|
|
|
|
rows.forEach((row) => {
|
|
const nombre = row.cells[0].textContent.toLowerCase();
|
|
const email = row.cells[1].textContent.toLowerCase();
|
|
const telefono = row.cells[2].textContent.toLowerCase();
|
|
const cursos = row.cells[3].textContent.toLowerCase();
|
|
|
|
if (
|
|
nombre.includes(searchTerm) ||
|
|
email.includes(searchTerm) ||
|
|
telefono.includes(searchTerm) ||
|
|
cursos.includes(searchTerm)
|
|
) {
|
|
row.style.display = "";
|
|
} else {
|
|
row.style.display = "none";
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderStudentsTable(alumnos) {
|
|
if (alumnos.length === 0) {
|
|
return `
|
|
<div class="card">
|
|
<div class="no-data">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#64748b">
|
|
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
|
<circle cx="8.5" cy="7" r="4"></circle>
|
|
<line x1="18" y1="8" x2="23" y2="13"></line>
|
|
<line x1="23" y1="8" x2="18" y2="13"></line>
|
|
</svg>
|
|
<h3>No hay alumnos registrados</h3>
|
|
<p>Comienza agregando tu primer alumno</p>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
return `
|
|
<div class="card">
|
|
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
|
|
<h2 style="margin: 0;">Lista de Alumnos</h2>
|
|
<div class="search-box">
|
|
<input type="text" id="studentSearch" placeholder="Buscar alumno...">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="students-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Nombre</th>
|
|
<th>Email</th>
|
|
<th>Teléfono</th>
|
|
<th>Cursos</th>
|
|
<th>Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${alumnos
|
|
.map(
|
|
(alumno) => `
|
|
<tr>
|
|
<td>${alumno.nombre}</td>
|
|
<td>${alumno.email}</td>
|
|
<td>${alumno.telefono || "-"}</td>
|
|
<td>
|
|
<div class="course-badges" id="courses-${alumno.id}">
|
|
${renderCourseBadges(alumno.id)}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button class="btn btn-sm btn-edit" onclick="editStudent(${alumno.id})">Editar</button>
|
|
<button class="btn btn-sm btn-danger" onclick="confirmDeleteStudent(${alumno.id})">Eliminar</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`
|
|
)
|
|
.join("")}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
function renderCourseBadges(alumnoId) {
|
|
return '<div class="loader-sm"></div>';
|
|
}
|
|
|
|
async function loadStudentCourses(alumnoId) {
|
|
try {
|
|
const response = await fetch(`api/alumnos-cursos.php?alumno_id=${alumnoId}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
const container = document.getElementById(`courses-${alumnoId}`);
|
|
if (container) {
|
|
container.innerHTML =
|
|
data.data.length > 0
|
|
? data.data
|
|
.map(
|
|
(curso) => `
|
|
<span class="badge course-badge" data-estado="${curso.estado}">
|
|
<span style="display: inline-block; font-weight: 500; color: #0f62fe;">
|
|
${formatCourseType(curso.tipo)} /
|
|
</span>
|
|
${curso.nombre}
|
|
</span>`
|
|
)
|
|
.join("")
|
|
: '<span class="text-muted">Sin cursos</span>';
|
|
}
|
|
|
|
const filtro = document.getElementById("studentCourseFilter");
|
|
if (filtro) {
|
|
const estadoSeleccionado = filtro.value;
|
|
|
|
document.querySelectorAll(".students-table tbody tr").forEach((fila) => {
|
|
const badges = fila.querySelectorAll(".course-badge");
|
|
const visible = Array.from(badges).some((badge) => {
|
|
const estado = (badge.dataset.estado || "").toLowerCase();
|
|
return estadoSeleccionado === "todos" || estado === estadoSeleccionado;
|
|
});
|
|
fila.style.display = visible ? "" : "none";
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading student courses:", error);
|
|
}
|
|
}
|
|
|
|
window.showAssignStudentModal = async function () {
|
|
try {
|
|
console.log("Mostrando modal de vinculación");
|
|
|
|
const profesorId = getProfesorId();
|
|
if (!profesorId) {
|
|
console.error("No se pudo obtener el ID del profesor");
|
|
showToast("error", "No se pudo identificar al profesor");
|
|
return;
|
|
}
|
|
|
|
const tempModalContent = `<div class="loader">Cargando datos...</div>`;
|
|
showModal("Vincular Alumnos a Curso", tempModalContent);
|
|
|
|
const [cursosRes, alumnosRes] = await Promise.all([
|
|
fetch(`api/cursos.php?profesor_id=${profesorId}`),
|
|
fetch("api/alumnos.php"),
|
|
]);
|
|
|
|
const [cursosData, alumnosData] = await Promise.all([
|
|
cursosRes.json(),
|
|
alumnosRes.json(),
|
|
]);
|
|
|
|
if (!cursosData || !alumnosData || !alumnosData.success) {
|
|
closeModal();
|
|
throw new Error("Error al cargar datos necesarios");
|
|
}
|
|
|
|
const modalHtml = `
|
|
<div class="form-group">
|
|
<label>Seleccionar Curso</label>
|
|
<select id="selectCurso" class="form-control">
|
|
${cursosData
|
|
.map(
|
|
(curso) => `<option value="${curso.id}">${curso.nombre}</option>`
|
|
)
|
|
.join("")}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Seleccionar Alumnos</label>
|
|
<div class="checkbox-group">
|
|
${
|
|
alumnosData.data.length > 0
|
|
? alumnosData.data
|
|
.map(
|
|
(alumno) => `
|
|
<label>
|
|
<input type="checkbox" name="alumnos" value="${alumno.id}">
|
|
${alumno.nombre} (${alumno.email})
|
|
</label>
|
|
`
|
|
)
|
|
.join("")
|
|
: "<p>No hay alumnos disponibles</p>"
|
|
}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const modalBody = document.querySelector(".modal-body");
|
|
if (modalBody) {
|
|
modalBody.innerHTML = modalHtml;
|
|
}
|
|
|
|
let modalFooter = document.querySelector(".modal-footer");
|
|
if (!modalFooter) {
|
|
modalFooter = document.createElement("div");
|
|
modalFooter.className = "modal-footer";
|
|
document.querySelector(".modal").appendChild(modalFooter);
|
|
}
|
|
|
|
modalFooter.innerHTML = `
|
|
<button class="btn btn-secondary" onclick="closeModal()">Cancelar</button>
|
|
<button class="btn btn-primary" onclick="assignStudentsToCourse()">Confirmar Vinculación</button>
|
|
`;
|
|
|
|
window.assignStudentsToCourse = async function () {
|
|
const cursoId = document.getElementById("selectCurso").value;
|
|
const selectedAlumnos = Array.from(
|
|
document.querySelectorAll('input[name="alumnos"]:checked')
|
|
).map((input) => input.value);
|
|
|
|
if (selectedAlumnos.length === 0) {
|
|
showToast("warning", "Selecciona al menos un alumno");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch("api/alumnos-cursos.php", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
curso_id: cursoId,
|
|
alumnos: selectedAlumnos,
|
|
}),
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
showToast("success", "Alumnos vinculados exitosamente");
|
|
closeModal();
|
|
loadStudentsManagement(document.querySelector("#students-content"));
|
|
} else {
|
|
showToast("error", result.error || "Error al vincular");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error al vincular:", error);
|
|
showToast("error", "Error al realizar la vinculación");
|
|
}
|
|
};
|
|
} catch (error) {
|
|
console.error("Error en showAssignStudentModal:", error);
|
|
closeModal();
|
|
showToast("error", "Error al cargar datos para vinculación");
|
|
}
|
|
};
|
|
|
|
window.unassignStudent = async function (alumnoId, cursoId) {
|
|
if (confirm("¿Desvincular este alumno del curso?")) {
|
|
try {
|
|
const response = await fetch(
|
|
`api/alumnos-cursos.php?alumno_id=${alumnoId}&curso_id=${cursoId}`,
|
|
{
|
|
method: "DELETE",
|
|
}
|
|
);
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showToast("success", data.message);
|
|
loadStudentCourses(alumnoId);
|
|
} else {
|
|
showToast("error", data.error || "Error al desvincular");
|
|
}
|
|
} catch (error) {
|
|
showToast("error", "Error en la conexión");
|
|
}
|
|
}
|
|
};
|
|
|
|
function loadDiplomasSection(container) {
|
|
container.innerHTML = `
|
|
<div class="card">
|
|
<div class="tab-header" style="display: flex; gap: 1rem; border-bottom: 2px solid #e2e8f0;">
|
|
<button class="btn btn-outline active tab-btn" data-tab="generar" onclick="switchDiplomaTab('generar')">📝 Generar Diplomas</button>
|
|
<button class="btn btn-outline tab-btn" data-tab="emitidos" onclick="switchDiplomaTab('emitidos')">📋 Emitidos</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="diploma-tab-generar" class="diploma-tab-content active">
|
|
<div class="card">
|
|
<h2 style="margin-bottom: 1rem;">Generar Diplomas</h2>
|
|
|
|
<div class="form-group" style="margin-bottom: 1rem;">
|
|
<label for="tipoCursoSelector"><strong>Tipo de Curso:</strong></label>
|
|
<select id="tipoCursoSelector" class="form-control" style="margin-top: 0.5rem; padding: 0.5rem; border-radius: 6px;">
|
|
<option value="">Selecciona un tipo...</option>
|
|
<option value="inyeccion">Inyección</option>
|
|
<option value="pildora">Píldora</option>
|
|
<option value="tratamiento">Tratamiento</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div id="cursos-generar-diplomas">
|
|
<div class="no-data">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#64748b">
|
|
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
|
<circle cx="8.5" cy="7" r="4"></circle>
|
|
<line x1="18" y1="8" x2="23" y2="13"></line>
|
|
<line x1="23" y1="8" x2="18" y2="13"></line>
|
|
</svg>
|
|
<h3>Selecciona un tipo de curso</h3>
|
|
<p>Los cursos completados con alumnos aparecerán aquí</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="diploma-tab-emitidos" class="diploma-tab-content" style="display:none;">
|
|
<div class="card">
|
|
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
|
|
<h2 style="margin: 0;">Gestión de Diplomas</h2>
|
|
</div>
|
|
|
|
<div class="loader">Cargando diplomas...</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById("tipoCursoSelector").addEventListener("change", (e) => {
|
|
const tipoSeleccionado = e.target.value;
|
|
if (tipoSeleccionado) {
|
|
renderCursosGenerablesPorTipo(tipoSeleccionado);
|
|
} else {
|
|
document.getElementById("cursos-generar-diplomas").innerHTML = `
|
|
<div class="no-data">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#64748b">
|
|
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
|
<circle cx="8.5" cy="7" r="4"></circle>
|
|
<line x1="18" y1="8" x2="23" y2="13"></line>
|
|
<line x1="23" y1="8" x2="18" y2="13"></line>
|
|
</svg>
|
|
<h3>Selecciona un tipo de curso</h3>
|
|
<p>Los cursos completados con alumnos aparecerán aquí</p>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
|
|
fetch(`api/diplomas.php?profesor_id=${getProfesorId()}`)
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
const emitidosContainer = document.querySelector("#diploma-tab-emitidos .card");
|
|
if (!data.success || data.data.length === 0) {
|
|
emitidosContainer.innerHTML = `
|
|
<div class="card-header">
|
|
<h2>Gestión de Diplomas</h2>
|
|
</div>
|
|
<p>No hay diplomas registrados aún</p>`;
|
|
return;
|
|
}
|
|
|
|
emitidosContainer.innerHTML = `
|
|
<div class="card-header">
|
|
<h2 style="margin: 0;">Gestión de Diplomas</h2>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="students-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Alumno</th>
|
|
<th>Email</th>
|
|
<th>Curso</th>
|
|
<th>Tipo</th>
|
|
<th>Fecha</th>
|
|
<th>Código</th>
|
|
<th>Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.data
|
|
.map((diploma) => `
|
|
<tr>
|
|
<td>${diploma.alumno_nombre}</td>
|
|
<td>${diploma.alumno_email}</td>
|
|
<td>${diploma.curso_nombre}</td>
|
|
<td><span class="badge ${getCourseTypeClass(diploma.curso_tipo)}">${formatCourseType(diploma.curso_tipo)}</span></td>
|
|
<td>${diploma.fecha_formateada}</td>
|
|
<td class="code">${diploma.codigo_unico}</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button class="btn btn-sm" onclick="downloadDiploma('${diploma.codigo_unico}')">Descargar</button>
|
|
<button class="btn btn-sm" onclick="resendDiploma('${diploma.codigo_unico}')">Reenviar</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`)
|
|
.join("")}
|
|
</tbody>
|
|
</table>
|
|
</div>`;
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error:", error);
|
|
const emitidosContainer = document.querySelector("#diploma-tab-emitidos .card");
|
|
emitidosContainer.innerHTML = `
|
|
<h2>Error al cargar diplomas</h2>
|
|
<p>${error.message}</p>`;
|
|
});
|
|
}
|
|
|
|
function switchDiplomaTab(tabName) {
|
|
document.querySelectorAll(".tab-btn").forEach((btn) => {
|
|
btn.classList.toggle("active", btn.dataset.tab === tabName);
|
|
});
|
|
|
|
document.querySelectorAll(".diploma-tab-content").forEach((tab) => {
|
|
tab.style.display = "none";
|
|
});
|
|
|
|
const activeTab = document.getElementById(`diploma-tab-${tabName}`);
|
|
if (activeTab) activeTab.style.display = "block";
|
|
}
|
|
|
|
function generateCoursesPreview(courses) {
|
|
if (!courses.length) return "<p>No tienes cursos activos</p>";
|
|
|
|
return `
|
|
<div class="table-container">
|
|
<table class="preview-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Nombre</th>
|
|
<th>Tipo</th>
|
|
<th>Estado</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${courses
|
|
.map(
|
|
(course) => `
|
|
<tr>
|
|
<td>${course.nombre || "Sin nombre"}</td>
|
|
<td><span class="badge ${getCourseTypeClass(course.tipo)}">${
|
|
course.tipo || "N/A"
|
|
}</span></td>
|
|
<td><span class="badge ${
|
|
course.estado === "activo" ? "active" : "inactive"
|
|
}">${course.estado || "N/A"}</span></td>
|
|
</tr>
|
|
`
|
|
)
|
|
.join("")}
|
|
</tbody>
|
|
</table>
|
|
</div>`;
|
|
}
|
|
|
|
function getCourseTypeClass(type) {
|
|
const types = {
|
|
inyeccion: "type-inyeccion",
|
|
pildora: "type-pildora",
|
|
tratamiento: "type-tratamiento",
|
|
};
|
|
return types[type] || "";
|
|
}
|
|
|
|
window.downloadDiploma = function (codigo) {
|
|
window.open(`certificado.php?codigo=${codigo}`, "_blank");
|
|
};
|
|
|
|
window.resendDiploma = function (codigo) {
|
|
if (!codigo) {
|
|
showToast("error", "Código de diploma no disponible.");
|
|
return;
|
|
}
|
|
|
|
const btn = event.target;
|
|
btn.disabled = true;
|
|
const originalText = btn.textContent;
|
|
btn.textContent = "Enviando...";
|
|
|
|
fetch(`api/enviar_diploma.php?codigo=${codigo}`, {
|
|
method: "GET"
|
|
})
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
if (data.success) {
|
|
showToast("success", "Diploma reenviado correctamente.");
|
|
} else {
|
|
showToast("error", data.error || "Error al reenviar el diploma.");
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error(error);
|
|
showToast("error", "Error al conectar con el servidor.");
|
|
})
|
|
.finally(() => {
|
|
btn.disabled = false;
|
|
btn.textContent = originalText;
|
|
});
|
|
};
|
|
|
|
window.handleCSVUpload = function (event) {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
const resultadoDiv = document.getElementById("resultadoCSV");
|
|
if (resultadoDiv) resultadoDiv.innerHTML = "";
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = function (e) {
|
|
const lines = e.target.result.split("\n").map(line => line.trim()).filter(Boolean);
|
|
const alumnos = [];
|
|
|
|
for (let i = 1; i < lines.length; i++) {
|
|
const [nombre, email, telefono, tipoCurso, nombreCurso] = lines[i].split(",").map(s => s.trim());
|
|
|
|
if (!nombre || !email || !telefono || !tipoCurso || !nombreCurso) {
|
|
continue;
|
|
}
|
|
|
|
alumnos.push({ nombre, email, telefono, tipoCurso, nombreCurso });
|
|
}
|
|
|
|
if (alumnos.length === 0) {
|
|
showToast("error", "El archivo CSV no contiene datos válidos");
|
|
if (resultadoDiv) resultadoDiv.innerHTML = `<div class="alert alert-danger">No se encontraron alumnos válidos en el archivo.</div>`;
|
|
return;
|
|
}
|
|
|
|
fetch("api/importar_csv.php", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ alumnos }),
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showToast("success", data.message || "Alumnos importados correctamente");
|
|
if (resultadoDiv) resultadoDiv.innerHTML = `<div class="alert alert-success">${data.message}</div>`;
|
|
loadStudentsManagement(document.querySelector("#students-content"));
|
|
} else {
|
|
if (data.conflicts && data.conflicts.length > 0) {
|
|
const list = `<ul>${data.conflicts.map(msg => `<li>${msg}</li>`).join("")}</ul>`;
|
|
showModal("Errores en Importación CSV", `<p>No se importaron alumnos por los siguientes errores:</p>${list}`);
|
|
if (resultadoDiv) resultadoDiv.innerHTML = `<div class="alert alert-danger">${list}</div>`;
|
|
} else {
|
|
showToast("error", data.error || "Error al importar alumnos");
|
|
if (resultadoDiv) resultadoDiv.innerHTML = `<div class="alert alert-danger">${data.error || "Error desconocido"}</div>`;
|
|
}
|
|
}
|
|
})
|
|
.catch(() => {
|
|
showToast("error", "Error al procesar el archivo");
|
|
if (resultadoDiv) resultadoDiv.innerHTML = `<div class="alert alert-danger">Error al procesar el archivo.</div>`;
|
|
});
|
|
};
|
|
|
|
reader.readAsText(file);
|
|
|
|
function formatCourseType(type) {
|
|
const types = {
|
|
pildora: "Píldora",
|
|
inyeccion: "Inyección",
|
|
tratamiento: "Tratamiento"
|
|
};
|
|
return types[type] || type;
|
|
}
|
|
|
|
};
|
|
|
|
function downloadCSVTemplate() {
|
|
const link = document.createElement("a");
|
|
link.href = "assets/formato_alumnos.csv";
|
|
link.download = "formato_alumnos.csv";
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
|
|
window.generateDiploma = function (alumnoCursoId) {
|
|
if (!alumnoCursoId) {
|
|
showToast("error", "ID de alumno-curso no válido");
|
|
return;
|
|
}
|
|
|
|
fetch("api/diplomas.php", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ alumno_curso_id: alumnoCursoId }),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
if (data.success && data.codigo_unico) {
|
|
showToast("success", "Diploma generado correctamente");
|
|
window.open(`certificado.php?codigo=${data.codigo_unico}`, "_blank");
|
|
|
|
const sendButtons = document.querySelectorAll(".btn-send-diploma");
|
|
sendButtons.forEach((btn) => {
|
|
|
|
if (btn.disabled && !btn.dataset.codigo) {
|
|
btn.disabled = false;
|
|
btn.title = "Enviar diploma por correo";
|
|
btn.dataset.codigo = data.codigo_unico;
|
|
btn.textContent = "Enviar";
|
|
}
|
|
});
|
|
|
|
loadDiplomasSection(document.querySelector("#diplomas-content"));
|
|
} else {
|
|
showToast("error", data.error || "Error al generar diploma");
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error:", err);
|
|
showToast("error", "Error en el servidor");
|
|
});
|
|
};
|
|
|
|
document.addEventListener('click', function (e) {
|
|
if (e.target.classList.contains('btn-send-diploma')) {
|
|
const btn = e.target;
|
|
const codigo = btn.dataset.codigo;
|
|
|
|
if (!codigo) {
|
|
showToast("error", "Código de diploma no disponible.");
|
|
return;
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = "Enviando...";
|
|
|
|
fetch(`api/enviar_diploma.php?codigo=${codigo}`, {
|
|
method: "GET"
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showToast("success", "Diploma enviado con éxito.");
|
|
} else {
|
|
showToast("error", data.error || "Error al enviar el diploma.");
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
showToast("error", "Fallo de conexión.");
|
|
})
|
|
.finally(() => {
|
|
btn.disabled = false;
|
|
btn.textContent = "Enviar";
|
|
});
|
|
}
|
|
});
|
|
|
|
function loadDashboardContent(container, force = false) {
|
|
fetch('api/dashboard_data.php')
|
|
.then(res => res.json())
|
|
.then(data => renderDashboard(container, data))
|
|
.catch(err => {
|
|
container.innerHTML = `<p>Error al cargar estadísticas.</p>`;
|
|
console.error(err);
|
|
});
|
|
}
|
|
|