Se elimino rol de usuario y consolido admin

This commit is contained in:
luis.aguilar 2025-05-08 21:30:57 -06:00
parent 6b1d644adc
commit 7f4aa87aa4
4 changed files with 746 additions and 746 deletions

View File

@ -9,31 +9,14 @@ if (!is_logged_in()) {
}
$method = $_SERVER['REQUEST_METHOD'];
$user = $_SESSION['user'];
switch ($method) {
case 'GET':
if ($user['rol'] === 'admin') {
$stmt = $pdo->query("SELECT * FROM cursos");
} else {
$stmt = $pdo->prepare("
SELECT c.*, uc.estado, uc.fecha_inicio, uc.fecha_fin, uc.profesor
FROM usuario_cursos uc
JOIN cursos c ON uc.curso_id = c.id
WHERE uc.usuario_id = ?
");
$stmt->execute([$user['id']]);
}
$stmt = $pdo->query("SELECT * FROM cursos");
echo json_encode($stmt->fetchAll());
break;
case 'POST':
if ($user['rol'] !== 'admin') {
http_response_code(403);
echo json_encode(['error' => 'Acceso no autorizado']);
exit;
}
$data = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare("
@ -52,5 +35,4 @@ switch ($method) {
default:
http_response_code(405);
echo json_encode(['error' => 'Método no permitido']);
}
?>
}

View File

@ -1,4 +1,21 @@
/* Reset y estilos base */
/* --- Nueva paleta de colores --- */
:root {
--primary-dark: #002b5c;
--primary-main: #003f7d;
--primary-light: #0066cc;
--primary-lighter: #e1f0ff;
--accent-main: #4facfe;
--accent-light: #00f2fe;
--text-primary: #1a1a1a;
--text-secondary: #4a5568;
--background: #f8fafc;
--surface: #ffffff;
--border: #e2e8f0;
--error: #e53e3e;
--success: #38a169;
}
/* --- Reset y tipografía --- */
* {
box-sizing: border-box;
margin: 0;
@ -7,30 +24,34 @@
body {
font-family: 'Inter', sans-serif;
color: var(--text-primary);
background-color: var(--background);
line-height: 1.6;
height: 100vh;
overflow: hidden;
}
/* Estilos para el contenedor del login */
/* --- Login - Versión elegante --- */
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
background: linear-gradient(135deg, #003f7d 0%, #0066cc 100%);
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-main) 100%);
padding: 20px;
}
.login-card {
background: white;
border-radius: 16px;
background: var(--surface);
border-radius: 12px;
padding: 2.5rem;
width: 100%;
max-width: 380px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
max-width: 420px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.login-card::before {
@ -39,35 +60,37 @@ body {
top: 0;
left: 0;
width: 100%;
height: 5px;
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
height: 6px;
background: linear-gradient(90deg, var(--accent-main) 0%, var(--accent-light) 100%);
}
.logo-container {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
text-align: center;
margin-bottom: 2rem;
}
.logo-icon {
width: 40px;
height: 40px;
color: #2563eb;
margin-right: 10px;
width: 48px;
height: 48px;
color: var(--primary-light);
margin: 0 auto 1rem;
display: block;
}
.login-card h1 {
color: #1e293b;
color: var(--primary-dark);
margin: 0;
font-size: 1.8rem;
font-size: 2rem;
font-weight: 600;
letter-spacing: -0.5px;
}
.welcome-text {
color: #64748b;
color: var(--text-secondary);
text-align: center;
margin-bottom: 2rem;
font-size: 0.95rem;
font-size: 1rem;
font-weight: 400;
}
.input-group {
@ -77,76 +100,58 @@ body {
.input-icon {
position: absolute;
left: 12px;
left: 16px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
color: #94a3b8;
color: var(--text-secondary);
z-index: 2;
}
.login-card input {
width: 100%;
padding: 0.75rem 0.75rem 0.75rem 40px;
border: 1px solid #e2e8f0;
padding: 0.85rem 0.85rem 0.85rem 48px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 0.95rem;
transition: all 0.3s ease;
background-color: #f8fafc;
font-size: 1rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background-color: var(--surface);
color: var(--text-primary);
}
.login-card input:focus {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
background-color: white;
border-color: var(--primary-light);
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.15);
outline: none;
}
.login-btn {
width: 100%;
padding: 0.85rem;
padding: 1rem;
font-size: 1rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 1rem;
background: linear-gradient(90deg, #2563eb 0%, #1d4ed8 100%);
gap: 10px;
margin-top: 1.5rem;
background: linear-gradient(90deg, var(--primary-main) 0%, var(--primary-light) 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 6px rgba(0, 63, 125, 0.1);
}
.login-btn:hover {
background: linear-gradient(90deg, #1d4ed8 0%, #1e40af 100%);
transform: translateY(-1px);
background: linear-gradient(90deg, var(--primary-dark) 0%, var(--primary-main) 100%);
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 63, 125, 0.15);
}
.btn-icon {
width: 20px;
height: 20px;
}
.footer-links {
display: flex;
justify-content: space-between;
margin-top: 1.5rem;
font-size: 0.85rem;
}
.footer-links a {
color: #64748b;
text-decoration: none;
transition: color 0.2s ease;
}
.footer-links a:hover {
color: #2563eb;
}
/* Estilos para la aplicación principal */
/* --- Dashboard - Versión elegante --- */
#app-content {
display: flex;
flex-direction: column;
@ -158,67 +163,39 @@ header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background-color: #003f7d;
padding: 1rem 2.5rem;
background-color: var(--primary-dark);
color: white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
position: relative;
z-index: 10;
}
header h1 {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
header h1 {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-weight: 600;
font-size: 1.5rem;
letter-spacing: 0.5px;
}
#user-info {
order: -1;
margin-left: auto;
display: flex;
align-items: center;
gap: 1rem;
gap: 1.5rem;
}
/* Estilos para la tabla en el dashboard */
.stats {
margin-bottom: 1.5rem;
}
.stats p {
margin: 0.5rem 0;
}
/* Paginación */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-top: 1rem;
}
.pagination-btn {
padding: 0.5rem 1rem;
min-width: 100px;
}
#page-info {
font-size: 0.9rem;
color: #64748b;
}
#current-user {
font-weight: 600;
font-weight: 500;
background: rgba(255, 255, 255, 0.1);
padding: 0.5rem 1rem;
padding: 0.5rem 1.25rem;
border-radius: 20px;
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.main-container {
@ -228,11 +205,12 @@ header h1 {
}
.sidebar {
width: 250px;
background-color: #f8fafc;
padding: 1.5rem 1rem;
border-right: 1px solid #e2e8f0;
width: 280px;
background-color: var(--surface);
padding: 1.5rem 0;
border-right: 1px solid var(--border);
overflow-y: auto;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.03);
}
.sidebar-menu {
@ -242,207 +220,179 @@ header h1 {
}
.sidebar-menu li {
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
border-radius: 6px;
padding: 0.85rem 1.5rem;
margin: 0.25rem 1rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.75rem;
gap: 0.85rem;
font-weight: 500;
color: var(--text-secondary);
}
.sidebar-menu li:hover {
background-color: #e0e7ff;
background-color: var(--primary-lighter);
color: var(--primary-main);
}
.sidebar-menu li.active {
background-color: #2563eb;
background-color: var(--primary-main);
color: white;
}
.sidebar-menu li svg {
width: 20px;
height: 20px;
}
.content {
flex: 1;
padding: 2rem;
padding: 2.5rem;
overflow-y: auto;
background-color: #f9fafb;
}
.content-section {
display: none;
}
.content-section.active {
display: block;
background-color: var(--background);
}
.card {
background: white;
border-left: 4px solid #2563eb;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
padding: 1.5rem;
margin-bottom: 1.5rem;
background: var(--surface);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 2rem;
margin-bottom: 2rem;
border-left: none;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
.card h2 {
margin-top: 0;
color: #003f7d;
font-size: 1.25rem;
}
.btn {
background-color: #2563eb;
color: white;
padding: 0.6rem 1.25rem;
border: none;
border-radius: 6px;
cursor: pointer;
margin-bottom: 1.5rem;
color: var(--primary-dark);
font-size: 1.5rem;
font-weight: 600;
font-family: 'Inter', sans-serif;
transition: background-color 0.2s ease;
}
.btn:hover {
background-color: #1d4ed8;
}
input,
select {
width: 100%;
padding: 0.75rem;
margin-top: 0.5rem;
margin-bottom: 1rem;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-family: 'Inter', sans-serif;
}
input:focus,
select:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
}
.oculto {
display: none;
}
/* Estilos para el contenido específico */
.course-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.course-card {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 1.25rem;
transition: transform 0.2s ease;
}
.course-card:hover {
transform: translateY(-2px);
}
.diploma-preview {
max-width: 600px;
margin: 0 auto;
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
text-align: center;
}
.diploma-preview img {
max-width: 100%;
height: auto;
margin: 1rem 0;
}
/* Estilos para el buscador y tabla */
.search-container {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.search-input {
flex: 1;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-family: 'Inter', sans-serif;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border);
}
/* --- Tablas mejoradas --- */
.table-container {
overflow-x: auto;
margin-top: 20px;
margin-top: 1.5rem;
border-radius: 8px;
border: 1px solid var(--border);
}
.courses-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.courses-table th, .courses-table td {
padding: 12px 15px;
padding: 1rem 1.25rem;
text-align: left;
border-bottom: 1px solid #e2e8f0;
border-bottom: 1px solid var(--border);
}
.courses-table th {
background-color: #f8fafc;
background-color: var(--primary-lighter);
font-weight: 600;
color: #003f7d;
color: var(--primary-dark);
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 0.5px;
}
.courses-table tr:last-child td {
border-bottom: none;
}
.courses-table tr:hover {
background-color: #f5f7fa;
background-color: var(--primary-lighter);
}
/* --- Botones mejorados --- */
.btn {
background-color: var(--primary-main);
color: white;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
font-family: 'Inter', sans-serif;
transition: all 0.2s ease;
font-size: 0.95rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn:hover {
background-color: var(--primary-dark);
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.btn svg {
width: 16px;
height: 16px;
}
.download-btn {
padding: 0.4rem 0.8rem;
font-size: 0.9rem;
background-color: var(--success);
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin-top: 20px;
.download-btn:hover {
background-color: #2f855a;
}
.pagination-btn {
padding: 0.5rem 1rem;
}
/* Mantén todos tus estilos anteriores y añade estos */
/* Estilos para el dashboard */
.admin .sidebar-menu li[data-section="courses"],
.admin .sidebar-menu li[data-section="students"],
.user .sidebar-menu li[data-section="my-courses"],
.user .sidebar-menu li[data-section="diplomas"] {
display: none;
/* --- Formularios mejorados --- */
input, select, textarea {
width: 100%;
padding: 0.85rem;
margin-top: 0.5rem;
margin-bottom: 1.25rem;
border: 1px solid var(--border);
border-radius: 8px;
font-family: 'Inter', sans-serif;
font-size: 1rem;
transition: all 0.2s ease;
background-color: var(--surface);
}
.admin .sidebar-menu li[data-section="courses"],
.admin .sidebar-menu li[data-section="students"] {
display: flex;
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--primary-light);
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
.user .sidebar-menu li[data-section="my-courses"],
.user .sidebar-menu li[data-section="diplomas"] {
display: flex;
label {
font-weight: 500;
color: var(--text-secondary);
font-size: 0.95rem;
}
/* --- Efectos de transición --- */
.content-section {
transition: opacity 0.3s ease;
}
/* --- Responsive --- */
@media (max-width: 1024px) {
.sidebar {
width: 240px;
}
.content {
padding: 1.5rem;
}
}
/* Mejoras de responsive */
@media (max-width: 768px) {
.main-container {
flex-direction: column;
@ -451,118 +401,98 @@ select:focus {
.sidebar {
width: 100%;
height: auto;
padding: 1rem 0;
}
.login-card {
width: 90%;
padding: 1.5rem;
.sidebar-menu {
display: flex;
overflow-x: auto;
padding: 0 1rem;
}
.sidebar-menu li {
white-space: nowrap;
}
header h1 {
position: static;
transform: none;
margin-right: auto;
}
#user-info {
margin-left: 0;
}
}
/* Estilos adicionales */
.loader {
text-align: center;
padding: 2rem;
font-style: italic;
color: #64748b;
@media (max-width: 480px) {
.login-card {
padding: 1.75rem;
}
.card {
padding: 1.5rem;
}
.content {
padding: 1.25rem;
}
}
.course-card {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 1.25rem;
margin-bottom: 1rem;
transition: transform 0.2s ease;
.error-card {
border-left: 4px solid var(--error);
}
.course-card:hover {
transform: translateY(-2px);
.error-card h2 {
color: var(--error);
}
.course-card h3 {
margin-top: 0;
color: #003f7d;
.hidden {
display: none;
}
.diploma-preview {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
margin-bottom: 1.5rem;
text-align: center;
.loading-spinner {
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top: 2px solid white;
width: 16px;
height: 16px;
animation: spin 1s linear infinite;
display: inline-block;
vertical-align: middle;
}
.diploma-preview h3 {
color: #003f7d;
.loading-spinner.small {
width: 12px;
height: 12px;
border-width: 1.5px;
}
.search-container {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.search-input {
flex: 1;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 6px;
.badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: capitalize;
}
/* Añadir al final del archivo */
/* Estilos para el header */
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
.badge.pildora {
background-color: #DBEAFE;
color: #1E40AF;
}
header h1 {
position: static;
transform: none;
margin: 0;
.badge.inyeccion {
background-color: #D1FAE5;
color: #065F46;
}
#user-info {
order: 1;
display: flex;
align-items: center;
gap: 1rem;
}
/* Estilos para el enlace de cerrar sesión */
.logout-link {
color: #333;
text-decoration: none;
padding: 0.75rem 1rem;
display: block;
transition: all 0.2s ease;
}
.logout-link:hover {
color: #e53e3e;
background-color: #fee2e2;
}
/* Estilos para la columna de acciones */
.courses-table td:last-child {
text-align: center;
}
.download-btn {
padding: 0.4rem 0.8rem;
font-size: 0.9rem;
background-color: #38a169;
}
.download-btn:hover {
background-color: #2f855a;
}
.download-btn:disabled {
background-color: #a0aec0;
cursor: not-allowed;
}
.badge.tratamiento {
background-color: #E0E7FF;
color: #3730A3;
}

View File

@ -1,16 +1,31 @@
document.addEventListener('DOMContentLoaded', function() {
// Manejo del formulario de login
const loginForm = document.getElementById('loginForm');
if (loginForm) {
loginForm.addEventListener('submit', handleLogin);
}
// Configuración inicial del dashboard
if (document.body.classList.contains('admin') || document.body.classList.contains('user')) {
initializeDashboard();
} else {
initDashboard(); // Nombre de función consistente
}
});
function initDashboard() {
// Configuración inicial
setupSidebarNavigation();
loadInitialData();
// Mostrar sección activa
const activeSection = document.querySelector('.sidebar-menu li.active');
if (activeSection) {
const sectionId = activeSection.dataset.section;
showSection(sectionId, true);
}
// Configurar tooltips para botones
initTooltips();
// Configurar eventos globales
document.addEventListener('click', handleGlobalEvents);
}
function handleLogin(e) {
e.preventDefault();
const formData = new FormData(this);
@ -30,160 +45,59 @@ function handleLogin(e) {
.catch(error => console.error('Error:', error));
}
function initializeDashboard() {
// Configurar eventos del menú lateral
setupSidebarNavigation();
// Cargar datos iniciales
loadInitialData();
// Mostrar la sección activa inicial
const activeSection = document.querySelector('.sidebar-menu li.active');
if (activeSection) {
const sectionId = activeSection.getAttribute('data-section');
showSection(sectionId, true); // true indica que es la carga inicial
}
}
function setupSidebarNavigation() {
document.querySelectorAll('.sidebar-menu li').forEach(item => {
const menuItems = document.querySelectorAll('.sidebar-menu li:not(.logout-link)');
menuItems.forEach(item => {
item.addEventListener('click', function() {
const section = this.getAttribute('data-section');
// Agregar efecto visual al hacer clic
this.classList.add('click-feedback');
setTimeout(() => this.classList.remove('click-feedback'), 200);
const section = this.dataset.section;
showSection(section);
});
});
}
function loadInitialData() {
fetch('api/cursos.php')
.then(response => response.json())
.then(data => {
if (document.body.classList.contains('admin')) {
updateAdminStats(data);
} else {
updateUserStats(data);
}
})
.catch(error => console.error('Error:', error));
async function loadInitialData() {
try {
// Mostrar skeleton loading
document.querySelectorAll('.stats p span').forEach(span => {
span.innerHTML = '<span class="skeleton-loader"></span>';
});
const [courses, users] = await Promise.all([
fetchData('api/cursos.php'),
fetchData('api/usuarios.php')
]);
updateStats(courses, users);
} catch (error) {
console.error('Error loading initial data:', error);
showToast('Error al cargar datos iniciales', 'error');
}
}
function updateAdminStats(courses) {
document.getElementById('active-courses-count').textContent = courses.length;
// Aquí puedes agregar más llamadas para estudiantes y diplomas
// fetch('api/estudiantes.php')...
// fetch('api/diplomas.php')...
}
function updateUserStats(courses) {
document.getElementById('user-courses-count').textContent = courses.length;
function updateStats(courses = [], users = []) {
// Efecto de conteo animado
animateValue('active-courses-count', 0, courses.length, 1000);
animateValue('students-count', 0, users.length, 1000);
const approvedCourses = courses.filter(c => c.estado === 'Aprobado');
document.getElementById('user-diplomas-count').textContent = approvedCourses.length;
window.userCourses = courses;
// Mostrar los primeros 5 cursos en el dashboard
renderDashboardCourses(courses.slice(0, 5));
setupDashboardPagination(courses);
setupDashboardSearch();
}
function renderDashboardCourses(courses) {
const tbody = document.getElementById('dashboard-courses-body');
if (!tbody) return;
tbody.innerHTML = courses.map(course => `
<tr>
<td>${course.nombre}</td>
<td>${course.fecha_inicio || '-'}</td>
<td>${course.fecha_fin || '-'}</td>
<td>
${course.estado === 'Aprobado' ?
`<button class="btn download-btn" data-course-id="${course.id}">Descargar</button>` :
'No disponible'}
</td>
</tr>
`).join('');
// Configurar eventos de descarga
document.querySelectorAll('.download-btn').forEach(btn => {
btn.addEventListener('click', function() {
const courseId = this.getAttribute('data-course-id');
window.open(`certificado.php?course_id=${courseId}`, '_blank');
});
});
animateValue('diplomas-count', 0, approvedCourses.length, 1000);
}
function setupDashboardPagination(allCourses) {
let currentPage = 1;
const perPage = 5;
const totalPages = Math.ceil(allCourses.length / perPage);
function updatePagination() {
const start = (currentPage - 1) * perPage;
const end = start + perPage;
renderDashboardCourses(allCourses.slice(start, end));
document.getElementById('page-info').textContent = `Página ${currentPage} de ${totalPages}`;
document.getElementById('prev-page').disabled = currentPage === 1;
document.getElementById('next-page').disabled = currentPage === totalPages || totalPages === 0;
}
document.getElementById('prev-page')?.addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
updatePagination();
}
});
document.getElementById('next-page')?.addEventListener('click', () => {
if (currentPage < totalPages) {
currentPage++;
updatePagination();
}
});
updatePagination();
}
function setupDashboardSearch() {
const searchBtn = document.getElementById('dashboard-search-btn');
const searchInput = document.getElementById('dashboard-course-search');
if (searchBtn && searchInput) {
searchBtn.addEventListener('click', () => {
const term = searchInput.value.toLowerCase();
const filtered = window.userCourses.filter(course =>
course.nombre.toLowerCase().includes(term) ||
course.tipo.toLowerCase().includes(term) ||
course.estado.toLowerCase().includes(term)
);
setupDashboardPagination(filtered);
});
// Permitir búsqueda al presionar Enter
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
searchBtn.click();
}
});
}
}
function showSection(sectionId, isInitialLoad = false) {
// Actualizar menú activo
async function showSection(sectionId, isInitialLoad = false) {
updateActiveMenu(sectionId);
// Mostrar la sección correspondiente
const sectionElement = getSectionElement(sectionId);
if (sectionElement) {
toggleSections(sectionElement);
// Solo cargar contenido dinámico si no es la carga inicial del dashboard
if (!(isInitialLoad && sectionId === 'dashboard')) {
if (sectionId === 'dashboard') {
// Recargar los datos del dashboard
loadInitialData();
} else {
loadDynamicContent(sectionId, sectionElement);
}
await loadDynamicContent(sectionId, sectionElement);
}
}
}
@ -192,13 +106,13 @@ function updateActiveMenu(sectionId) {
document.querySelectorAll('.sidebar-menu li').forEach(li => {
li.classList.remove('active');
});
document.querySelector(`.sidebar-menu li[data-section="${sectionId}"]`).classList.add('active');
const activeItem = document.querySelector(`.sidebar-menu li[data-section="${sectionId}"]`);
if (activeItem) {
activeItem.classList.add('active');
}
}
function getSectionElement(sectionId) {
if (sectionId === 'dashboard') {
return document.getElementById('dashboard-content');
}
return document.getElementById(`${sectionId}-content`);
}
@ -209,217 +123,405 @@ function toggleSections(activeSection) {
activeSection.classList.add('active');
}
function loadDynamicContent(sectionId, container) {
// Mostrar loader mientras se carga
container.innerHTML = '<div class="loader">Cargando...</div>';
switch(sectionId) {
case 'courses':
loadAdminCourses(container);
break;
case 'students':
loadAdminStudents(container);
break;
case 'my-courses':
loadUserCourses(container);
break;
case 'diplomas':
loadUserDiplomas(container);
break;
case 'profile':
loadProfileSection(container);
break;
default:
container.innerHTML = '<div class="card"><h2>Sección no implementada</h2></div>';
async function loadDynamicContent(sectionId, container) {
// Mostrar skeleton loading
container.innerHTML = `
<div class="skeleton-card">
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
</div>
`;
try {
switch(sectionId) {
case 'courses':
await loadCoursesSection(container);
break;
case 'students':
await loadStudentsSection(container);
break;
case 'diplomas':
await loadDiplomasSection(container);
break;
default:
container.innerHTML = '<div class="card"><h2>Sección no implementada</h2></div>';
}
} catch (error) {
console.error(`Error loading ${sectionId} section:`, error);
container.innerHTML = `
<div class="card error-card">
<h2>Error al cargar contenido</h2>
<p>No se pudo cargar la información solicitada.</p>
<button class="btn retry-btn" onclick="showSection('${sectionId}')">
Reintentar
</button>
</div>
`;
}
}
// Funciones para cargar contenido específico
function loadAdminCourses(container) {
fetch('api/cursos.php')
.then(response => response.json())
.then(courses => {
container.innerHTML = `
<div class="card">
<h2>Gestión de Cursos</h2>
<form id="courseForm">
async function loadCoursesSection(container) {
try {
const courses = await fetchData('api/cursos.php');
// Verificar si courses es un array
if (!Array.isArray(courses)) {
throw new Error('Formato de datos inválido');
}
container.innerHTML = `
<div class="card">
<h2>Gestión de Cursos</h2>
<form id="courseForm" class="elegant-form">
<div class="form-group">
<label>Nombre del Curso</label>
<input type="text" name="nombre" placeholder="Ej. Seguridad Informática" required>
</div>
<div class="form-group">
<label>Tipo de Curso</label>
<select id="courseType" name="tipo" required>
<option value="">Seleccionar</option>
<option value="">Seleccionar tipo</option>
<option value="pildora">Píldora</option>
<option value="inyeccion">Inyección</option>
<option value="tratamiento">Tratamiento</option>
</select>
<div id="competencesField" class="oculto">
<label>Competencias Asociadas</label>
<input type="text" name="competencias" placeholder="Ej. Análisis de datos, Comunicación efectiva">
</div>
<button class="btn" type="submit">Guardar Curso</button>
</form>
</div>
<div id="competencesField" class="form-group hidden">
<label>Competencias Asociadas</label>
<input type="text" name="competencias" placeholder="Ej. Análisis de datos, Comunicación efectiva">
</div>
<div class="form-actions">
<button class="btn" type="submit">
<svg class="btn-icon" viewBox="0 0 24 24">
<path fill="currentColor" d="M17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V7L17 3M19 19H5V5H16.17L19 7.83V19M12 12C10.34 12 9 13.34 9 15S10.34 18 12 18 15 16.66 15 15 13.66 12 12 12M6 6H15V10H6V6Z"/>
</svg>
Guardar Curso
</button>
</div>
</form>
</div>
<div class="card">
<h2>Lista de Cursos</h2>
<div class="table-actions">
<div class="search-container">
<input type="text" id="courseSearch" placeholder="Buscar cursos..." class="search-input">
<button class="btn search-btn">
<svg class="btn-icon" viewBox="0 0 24 24">
<path fill="currentColor" d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"/>
</svg>
</button>
</div>
</div>
<div class="table-container">
<table class="elegant-table">
<thead>
<tr>
<th>Nombre</th>
<th>Tipo</th>
<th>Competencias</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="courses-list-body">
${generateCoursesTableRows(courses)}
</tbody>
</table>
</div>
</div>
`;
setupCourseForm();
} catch (error) {
console.error('Error loading courses:', error);
container.innerHTML = `
<div class="card error-card">
<h2>Error al cargar los cursos</h2>
<p>${error.message || 'No se pudieron cargar los cursos'}</p>
<button class="btn retry-btn" onclick="loadCoursesSection(this.closest('.content-section'))">
Reintentar
</button>
</div>
`;
}
}
function formatCourseType(type) {
const types = {
'pildora': 'Píldora',
'inyeccion': 'Inyección',
'tratamiento': 'Tratamiento'
};
return types[type] || type;
}
function loadStudentsSection(container) {
fetch('api/usuarios.php')
.then(response => response.json())
.then(users => {
container.innerHTML = `
<div class="card">
<h2>Lista de Cursos</h2>
<div class="course-list">
${generateCoursesList(courses)}
<h2>Gestión de Estudiantes</h2>
<div class="search-container">
<input type="text" id="studentSearch" placeholder="Buscar estudiantes..." class="search-input">
<button class="btn" id="searchStudentButton">Buscar</button>
</div>
<div class="table-container">
<table class="courses-table">
<thead>
<tr>
<th>Nombre</th>
<th>Usuario</th>
<th>Email</th>
<th>Cursos Inscritos</th>
</tr>
</thead>
<tbody id="students-list-body">
${generateStudentsTableRows(users)}
</tbody>
</table>
</div>
</div>`;
setupCourseForm();
setupStudentSearch();
})
.catch(error => {
container.innerHTML = '<div class="card"><h2>Error al cargar los cursos</h2></div>';
container.innerHTML = '<div class="card"><h2>Error al cargar los estudiantes</h2></div>';
console.error('Error:', error);
});
}
function loadUserCourses(container) {
const courses = window.userCourses || [];
container.innerHTML = `
<div class="card">
<h2>Mis Cursos</h2>
<div class="search-container">
<input type="text" id="courseSearch" placeholder="Buscar cursos..." class="search-input">
<button class="btn" id="searchButton">Buscar</button>
</div>
<div class="course-list">
${courses.length > 0 ?
courses.map(course => generateCourseCard(course)).join('') :
'<p>No tienes cursos registrados.</p>'}
</div>
</div>`;
setupCourseSearch();
function loadDiplomasSection(container) {
fetch('api/cursos.php')
.then(response => response.json())
.then(courses => {
const approvedCourses = courses.filter(c => c.estado === 'Aprobado');
container.innerHTML = `
<div class="card">
<h2>Diplomas Emitidos</h2>
<div class="search-container">
<input type="text" id="diplomaSearch" placeholder="Buscar diplomas..." class="search-input">
<button class="btn" id="searchDiplomaButton">Buscar</button>
</div>
<div class="table-container">
<table class="courses-table">
<thead>
<tr>
<th>Curso</th>
<th>Estudiante</th>
<th>Fecha de Aprobación</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="diplomas-list-body">
${generateDiplomasTableRows(approvedCourses)}
</tbody>
</table>
</div>
</div>`;
setupDiplomaSearch();
})
.catch(error => {
container.innerHTML = '<div class="card"><h2>Error al cargar los diplomas</h2></div>';
console.error('Error:', error);
});
}
function loadUserDiplomas(container) {
const courses = (window.userCourses || []).filter(c => c.estado === 'Aprobado');
container.innerHTML = `
<div class="card">
<h2>Mis Diplomas</h2>
${courses.length > 0 ?
courses.map(course => generateDiplomaCard(course)).join('') :
'<p>No tienes diplomas disponibles todavía.</p>'}
</div>`;
setupDiplomaDownloads();
}
// Funciones auxiliares
function generateCoursesList(courses) {
// Funciones auxiliares para generar contenido
function generateCoursesTableRows(courses = []) {
return courses.map(course => `
<div class="course-card">
<h3>${course.nombre}</h3>
<p>Tipo: ${course.tipo}</p>
<p>ID: ${course.id}</p>
</div>
<tr>
<td>${course.nombre}</td>
<td><span class="badge ${course.tipo}">${formatCourseType(course.tipo)}</span></td>
<td>${course.competencias || '-'}</td>
<td class="actions-cell">
<button class="btn-icon-btn edit-btn" data-id="${course.id}" title="Editar">
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z"/>
</svg>
</button>
<button class="btn-icon-btn delete-btn" data-id="${course.id}" title="Eliminar">
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"/>
</svg>
</button>
</td>
</tr>
`).join('');
}
function generateCourseCard(course) {
return `
<div class="course-card">
<h3>${course.nombre}</h3>
<p>Tipo: ${course.tipo}</p>
<p>Estado: ${course.estado}</p>
${course.competencias ? `<p>Competencias: ${course.competencias}</p>` : ''}
<p>Profesor: ${course.profesor || 'No asignado'}</p>
<p>Fecha: ${course.fecha_inicio} a ${course.fecha_fin}</p>
</div>`;
/**
* Muestra un toast notification
*/
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast-notification ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('fade-out');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
function generateDiplomaCard(course) {
return `
<div class="diploma-preview">
<h3>Diploma de ${course.nombre}</h3>
<p>Fecha: ${course.fecha_fin || 'Completado'}</p>
<p>Profesor: ${course.profesor || 'No especificado'}</p>
<button class="btn download-btn"
data-course-id="${course.id}"
data-course-name="${course.nombre}">
Descargar Diploma
</button>
</div>`;
function generateStudentsTableRows(users) {
return users.map(user => `
<tr>
<td>${user.nombre}</td>
<td>${user.username}</td>
<td>${user.email}</td>
<td>${user.cursos ? user.cursos.length : 0}</td>
</tr>
`).join('');
}
function generateDiplomasTableRows(courses) {
return courses.map(course => `
<tr>
<td>${course.nombre}</td>
<td>${course.estudiante || 'N/A'}</td>
<td>${course.fecha_fin || 'N/A'}</td>
<td>
<button class="btn download-btn" data-course-id="${course.id}">
Descargar Diploma
</button>
</td>
</tr>
`).join('');
}
function animateValue(id, start, end, duration) {
const element = document.getElementById(id);
if (!element) return;
const range = end - start;
const startTime = Date.now();
const endTime = startTime + duration;
const update = () => {
const now = Date.now();
const progress = Math.min((now - startTime) / duration, 1);
const value = Math.floor(start + progress * range);
element.textContent = value.toLocaleString();
if (now < endTime) {
requestAnimationFrame(update);
}
};
update();
}
// Configuración de eventos dinámicos
function setupCourseForm() {
const courseTypeSelect = document.getElementById('courseType');
if (courseTypeSelect) {
courseTypeSelect.addEventListener('change', function() {
const competencesField = document.getElementById('competencesField');
if (competencesField) {
competencesField.classList.toggle('oculto', this.value !== 'tratamiento');
}
competencesField.classList.toggle('hidden', this.value !== 'tratamiento');
});
}
const courseForm = document.getElementById('courseForm');
if (courseForm) {
courseForm.addEventListener('submit', function(e) {
courseForm.addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const jsonData = {};
formData.forEach((value, key) => jsonData[key] = value);
fetch('api/cursos.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(jsonData)
})
.then(response => response.json())
.then(data => {
const submitBtn = this.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<span class="loading-spinner"></span>';
submitBtn.disabled = true;
try {
const formData = new FormData(this);
const jsonData = Object.fromEntries(formData.entries());
const response = await fetch('api/cursos.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(jsonData)
});
const data = await response.json();
if (data.success) {
alert('Curso creado exitosamente');
showSection('courses');
showToast('Curso creado exitosamente', 'success');
await loadCoursesSection(document.getElementById('courses-content'));
} else {
alert('Error al crear el curso');
showToast('Error al crear el curso', 'error');
}
})
.catch(error => console.error('Error:', error));
});
}
}
function setupCourseSearch() {
const searchButton = document.getElementById('searchButton');
if (searchButton) {
searchButton.addEventListener('click', function() {
const searchTerm = document.getElementById('courseSearch').value.toLowerCase();
const courses = window.userCourses || [];
const filteredCourses = courses.filter(course =>
course.nombre.toLowerCase().includes(searchTerm) ||
(course.profesor && course.profesor.toLowerCase().includes(searchTerm))
);
const courseList = document.querySelector('.course-list');
if (courseList) {
courseList.innerHTML = filteredCourses.length > 0 ?
filteredCourses.map(course => generateCourseCard(course)).join('') :
'<p>No se encontraron cursos que coincidan con la búsqueda.</p>';
} catch (error) {
console.error('Error:', error);
showToast('Error en la conexión', 'error');
} finally {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
});
}
}
function setupDiplomaDownloads() {
function setupStudentSearch() {
const searchButton = document.getElementById('searchStudentButton');
if (searchButton) {
searchButton.addEventListener('click', function() {
const searchTerm = document.getElementById('studentSearch').value.toLowerCase();
const tableBody = document.getElementById('students-list-body');
fetch('api/usuarios.php')
.then(response => response.json())
.then(users => {
const filteredUsers = users.filter(user =>
user.nombre.toLowerCase().includes(searchTerm) ||
user.username.toLowerCase().includes(searchTerm) ||
user.email.toLowerCase().includes(searchTerm)
);
tableBody.innerHTML = generateStudentsTableRows(filteredUsers);
});
});
}
}
function setupDiplomaSearch() {
const searchButton = document.getElementById('searchDiplomaButton');
if (searchButton) {
searchButton.addEventListener('click', function() {
const searchTerm = document.getElementById('diplomaSearch').value.toLowerCase();
const tableBody = document.getElementById('diplomas-list-body');
fetch('api/cursos.php')
.then(response => response.json())
.then(courses => {
const approvedCourses = courses.filter(c => c.estado === 'Aprobado');
const filteredCourses = approvedCourses.filter(course =>
course.nombre.toLowerCase().includes(searchTerm) ||
(course.estudiante && course.estudiante.toLowerCase().includes(searchTerm))
);
tableBody.innerHTML = generateDiplomasTableRows(filteredCourses);
});
});
}
// Configurar eventos de descarga de diplomas
document.querySelectorAll('.download-btn').forEach(btn => {
btn.addEventListener('click', function() {
const courseId = this.getAttribute('data-course-id');
// Mostrar mensaje de carga
const originalText = this.innerHTML;
this.innerHTML = '<span class="loading-text">Generando diploma...</span>';
this.disabled = true;
// Abrir en nueva pestaña en lugar de usar fetch
window.open(`certificado.php`, '_blank');
// Abrir en nueva pestaña
window.open(`certificado.php?course_id=${courseId}`, '_blank');
// Restaurar botón después de un breve retraso
setTimeout(() => {
@ -430,11 +532,56 @@ function setupDiplomaDownloads() {
});
}
// Funciones de secciones no implementadas (para completar)
function loadAdminStudents(container) {
container.innerHTML = '<div class="card"><h2>Gestión de Estudiantes</h2><p>Sección en desarrollo</p></div>';
function handleGlobalEvents(e) {
// Delegación de eventos para mejor performance
if (e.target.closest('.edit-btn')) {
const courseId = e.target.closest('.edit-btn').dataset.id;
handleEditCourse(courseId);
}
if (e.target.closest('.delete-btn')) {
const courseId = e.target.closest('.delete-btn').dataset.id;
handleDeleteCourse(courseId);
}
if (e.target.closest('.download-btn')) {
const courseId = e.target.closest('.download-btn').dataset.id;
handleDownloadDiploma(courseId);
}
}
function loadProfileSection(container) {
container.innerHTML = '<div class="card"><h2>Perfil de Usuario</h2><p>Sección en desarrollo</p></div>';
function handleDownloadDiploma(courseId) {
const btn = document.querySelector(`.download-btn[data-course-id="${courseId}"]`);
const originalText = btn.innerHTML;
btn.innerHTML = '<span class="loading-spinner small"></span>';
btn.disabled = true;
// Simular generación de diploma
setTimeout(() => {
window.open(`certificado.php?course_id=${courseId}`, '_blank');
btn.innerHTML = originalText;
btn.disabled = false;
showToast('Diploma generado con éxito', 'success');
}, 1500);
}
async function fetchData(url, options = {}) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`Error HTTP: ${response.status}`);
}
const data = await response.json();
// Verificar si la respuesta es válida
if (data === null || data === undefined) {
throw new Error('Respuesta vacía del servidor');
}
return data;
} catch (error) {
console.error(`Error fetching data from ${url}:`, error);
throw error; // Re-lanzamos el error para manejarlo en el nivel superior
}
}

View File

@ -13,7 +13,7 @@ $user = $_SESSION['user'];
<link rel="stylesheet" href="assets/css/styles.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
</head>
<body class="<?= $user['rol'] ?>">
<body>
<div id="app-content">
<header>
<h1>DiploMaster</h1>
@ -25,96 +25,37 @@ $user = $_SESSION['user'];
<div class="main-container">
<div class="sidebar" id="sidebar">
<ul class="sidebar-menu">
<?php if ($user['rol'] === 'admin'): ?>
<li class="active" data-section="dashboard"><span>🏠 Inicio</span></li>
<li data-section="courses"><span>📚 Gestión de Cursos</span></li>
<li data-section="students"><span>👨‍🎓 Gestión de Estudiantes</span></li>
<?php else: ?>
<li class="active" data-section="dashboard"><span>🏠 Inicio</span></li>
<li data-section="my-courses"><span>📚 Mis Cursos</span></li>
<li data-section="diplomas"><span>🎓 Mis Diplomas</span></li>
<?php endif; ?>
<li data-section="profile"><span>👤 Perfil</span></li>
<li class="active" data-section="dashboard"><span>Inicio</span></li>
<li data-section="courses"><span>Gestión de Cursos</span></li>
<li data-section="students"><span>Gestión de Estudiantes</span></li>
<li data-section="diplomas"><span>Diplomas</span></li>
<li><a href="api/logout.php" class="logout-link">Cerrar sesión</a></li>
</ul>
</div>
<div class="content" id="main-content">
<?php if ($user['rol'] === 'admin'): ?>
<!-- Contenido para Administrador -->
<div id="dashboard-content" class="content-section active">
<div class="card">
<h2>Panel de Administración</h2>
<p>Bienvenido al sistema de gestión de DiploMaster</p>
<div class="stats">
<p><strong>Estadísticas:</strong></p>
<p> <span id="active-courses-count">0</span> cursos activos</p>
<p> <span id="students-count">0</span> estudiantes registrados</p>
<p> <span id="diplomas-count">0</span> diplomas emitidos</p>
</div>
<div id="dashboard-content" class="content-section active">
<div class="card">
<h2>Panel de Administración</h2>
<p>Bienvenido al sistema de gestión de DiploMaster</p>
<div class="stats">
<p><strong>Estadísticas:</strong></p>
<p> <span id="active-courses-count">0</span> cursos activos</p>
<p> <span id="students-count">0</span> estudiantes registrados</p>
<p> <span id="diplomas-count">0</span> diplomas emitidos</p>
</div>
</div>
<div id="courses-content" class="content-section">
<!-- Se cargará dinámicamente -->
</div>
<div id="students-content" class="content-section">
<!-- Se cargará dinámicamente -->
</div>
<?php else: ?>
<!-- Contenido para Usuario Normal -->
<div id="dashboard-content" class="content-section active">
<div class="card">
<h2>Bienvenido <?= htmlspecialchars($user['nombre']) ?></h2>
<p>Este es tu panel de control en DiploMaster</p>
<div class="stats">
<p><strong>Resumen:</strong></p>
<p> <span id="user-courses-count">0</span> cursos registrados</p>
<p> <span id="user-diplomas-count">0</span> diplomas disponibles</p>
</div>
</div>
<div class="card">
<h2>Mis Cursos Recientes</h2>
<div class="search-container">
<input type="text" id="dashboard-course-search" placeholder="Buscar mis cursos..." class="search-input">
<button class="btn" id="dashboard-search-btn">Buscar</button>
</div>
<div class="table-container">
<table class="courses-table" id="dashboard-courses-table">
<thead>
<tr>
<th>Nombre</th>
<th>Fecha Inicio</th>
<th>Fecha Fin</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="dashboard-courses-body">
<!-- Se llenará dinámicamente -->
</tbody>
</table>
<div class="pagination" id="dashboard-pagination">
<button class="btn pagination-btn" id="prev-page">Anterior</button>
<span id="page-info">Página 1</span>
<button class="btn pagination-btn" id="next-page">Siguiente</button>
</div>
</div>
</div>
</div>
<div id="my-courses-content" class="content-section">
<!-- Se cargará dinámicamente -->
</div>
<div id="diplomas-content" class="content-section">
<!-- Se cargará dinámicamente -->
</div>
<?php endif; ?>
</div>
<!-- Sección común para todos los usuarios -->
<div id="profile-content" class="content-section">
<div id="courses-content" class="content-section">
<!-- Se cargará dinámicamente -->
</div>
<div id="students-content" class="content-section">
<!-- Se cargará dinámicamente -->
</div>
<div id="diplomas-content" class="content-section">
<!-- Se cargará dinámicamente -->
</div>
</div>