Revisé botones en cursos y alumnos

This commit is contained in:
Christopher Alessandro Rodriguez Salazar 2025-06-19 16:44:41 -06:00
parent 64b4a2b075
commit 20c618ab73
3 changed files with 177 additions and 71 deletions

View File

@ -1013,3 +1013,38 @@ textarea[name="descripcion"] {
resize: none;
overflow-y: hidden;
}
.tooltip-csv {
visibility: hidden;
opacity: 0;
position: absolute;
top: 140%;
left: 50%;
transform: translateX(-50%);
background-color: #2563eb;
color: #fff;
padding: 6px 10px;
border-radius: 4px;
white-space: nowrap;
font-size: 0.75rem;
transition: opacity 0.2s ease;
pointer-events: none;
z-index: 10;
}
.tooltip-csv::after {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
margin-left: -6px;
border-width: 6px;
border-style: solid;
border-color: transparent transparent #2563eb transparent;
}
svg:hover + .tooltip-csv,
.tooltip-csv:hover {
visibility: visible;
opacity: 1;
}

View File

@ -0,0 +1,2 @@
"nombre,email,telefono,tipoCurso,nombreCurso"
"Rafael,rafael1@mail.com,10987654321,pildora,Curso Avanzado"
1 nombre,email,telefono,tipoCurso,nombreCurso
2 Rafael,rafael1@mail.com,10987654321,pildora,Curso Avanzado

View File

@ -373,12 +373,15 @@ function loadProfessorCourses(container) {
<option value="tratamiento">Tratamiento</option>
</select>
<div id="competencesField" name="competencias" class="oculto">
<div id="competencesField" class="oculto">
<label>Competencias Asociadas *</label>
<input type="text" name="competencias" placeholder="Ej. Análisis de datos, Comunicación efectiva">
<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>Estado</label>
<label style="margin-top: 1rem; display: block;">Estado</label>
<select name="estado">
<option value="activo">Activo</option>
<option value="archivado">Archivado</option>
@ -569,8 +572,15 @@ function setupCourseForm() {
const courseTypeSelect = document.getElementById('courseType');
const competencesField = document.getElementById('competencesField');
const competenciasInput = competencesField
? competencesField.querySelector('input[name="competencias"]')
: null;
? 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 () {
@ -718,7 +728,7 @@ window.editCourse = function (id) {
// 🟦 Mostrar/ocultar campo de competencias según tipo
const competencesField = document.getElementById("competencesField");
const competenciasInput = competencesField.querySelector('input[name="competencias"]');
const competenciasInput = competencesField.querySelector('textarea[name="competencias"]');
if (course.tipo === "tratamiento") {
competencesField.classList.remove("oculto");
competenciasInput.value = course.competencias || "";
@ -818,38 +828,73 @@ function loadStudentsManagement(container) {
throw new Error(data.error || "Error al obtener alumnos");
container.innerHTML = `
<div class="students-management">
${renderStudentForm()}
${renderStudentsTable(data.data || [])}
</div>
`;
<div class="students-management">
${renderStudentForm()}
${renderStudentsTable(data.data || [])}
</div>
`;
setupStudentForm();
// Cargar cursos por alumno
(data.data || []).forEach((alumno) => loadStudentCourses(alumno.id));
// Activar filtro por estado de curso
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";
});
});
// Aplicar inmediatamente al cargar
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>`;
<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>
<label class="btn btn-primary" style="margin: 0;">
Importar CSV
<input type="file" accept=".csv" onchange="handleCSVUpload(event)" style="display: none;">
</label>
<div style="display: flex; align-items: center; gap: 0.5rem; position: relative;">
<label class="btn btn-primary" style="margin: 0;">
Importar CSV
<input type="file" accept=".csv" onchange="handleCSVUpload(event)" style="display: none;">
</label>
<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>
</div>
</div>
<form id="studentForm">
<div class="form-grid">
@ -1140,11 +1185,13 @@ function filterStudents(searchTerm) {
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(); // ✅ aquí
if (
nombre.includes(searchTerm) ||
email.includes(searchTerm) ||
telefono.includes(searchTerm)
telefono.includes(searchTerm) ||
cursos.includes(searchTerm) // ✅ permite buscar tipo o nombre de curso
) {
row.style.display = "";
} else {
@ -1174,15 +1221,13 @@ function renderStudentsTable(alumnos) {
return `
<div class="card">
<div class="card-header">
<h2>Lista de Alumnos</h2>
<div class="header-actions">
<div class="search-box">
<input type="text" id="studentSearch" placeholder="Buscar alumno...">
</div>
<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>
@ -1198,31 +1243,23 @@ function renderStudentsTable(alumnos) {
${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>
`
<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>
@ -1237,27 +1274,41 @@ function renderCourseBadges(alumnoId) {
async function loadStudentCourses(alumnoId) {
try {
const response = await fetch(
`api/alumnos-cursos.php?alumno_id=${alumnoId}`
);
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
.map(
(curso) => `
<span class="badge course-badge">
${curso.nombre}
<button class="badge-remove" onclick="unassignStudent(${alumnoId}, ${curso.id})">
&times;
</button>
</span>
`
)
.join("") || '<span class="text-muted">Sin cursos</span>';
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>';
}
// ✅ Aplicar filtro visual si está activo
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) {
@ -1623,5 +1674,23 @@ window.handleCSVUpload = function (event) {
};
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); // requerido en algunos navegadores
link.click();
document.body.removeChild(link);
}