| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442 |
- document.addEventListener('DOMContentLoaded', () => {
- const state = {
- dashboard: null,
- courses: [],
- tasks: [],
- grades: [],
- schedule: [],
- projects: [],
- exams: []
- };
- const UI = {
- loading: document.getElementById('loading'),
- tabs: document.querySelectorAll('.nav-link'),
- tabContents: document.querySelectorAll('.tab-content'),
- title: document.getElementById('topbar-title')
- };
- async function init() {
- try {
- const res = await fetch(`/api/student/dashboard`);
- if (res.status === 401) {
- window.location.href = '/login';
- return;
- }
- if (!res.ok) throw new Error('API not reachable');
- state.dashboard = await res.json();
-
- updateHeader();
- await switchTab('inicio');
- UI.loading.style.display = 'none';
-
- UI.tabs.forEach(btn => {
- btn.addEventListener('click', () => {
- switchTab(btn.getAttribute('data-tab'));
- });
- });
- } catch (err) {
- UI.loading.textContent = 'Error al cargar los datos. Verifica la API.';
- UI.loading.className = 'alert alert-error';
- }
- }
- function updateHeader() {
- const d = state.dashboard;
- // Topbar
- document.getElementById('user-name-display').textContent = d.name;
- document.getElementById('user-level-display').textContent = d.level_name;
- document.getElementById('user-avatar-initial').textContent = d.name.charAt(0);
-
- // Profile Header (Gamification)
- const phName = document.getElementById('ph-name');
- if (phName) {
- phName.textContent = d.name;
- document.getElementById('ph-avatar').textContent = d.name.charAt(0);
- document.getElementById('ph-xp').textContent = `${d.xp} XP`;
- document.getElementById('ph-level').textContent = d.level;
- document.getElementById('ph-streak').textContent = d.streak;
- document.getElementById('ph-xp-bar').style.width = `${(d.xp % 1000) / 10}%`;
- }
- }
- async function switchTab(tabId) {
- UI.tabs.forEach(t => t.classList.remove('active'));
- document.querySelector(`[data-tab="${tabId}"]`).classList.add('active');
-
- UI.tabContents.forEach(c => {
- c.style.display = 'none';
- c.innerHTML = '';
- });
-
- const container = document.getElementById(`tab-${tabId}`);
- const template = document.getElementById(`tmpl-${tabId}`);
-
- if (!template) return;
- container.appendChild(template.content.cloneNode(true));
- container.style.display = 'block';
- if (tabId === 'inicio') {
- UI.title.textContent = 'Inicio';
- document.getElementById('kpi-gpa').textContent = state.dashboard.gpa || 7.5;
- document.getElementById('kpi-cursos').textContent = state.dashboard.active_courses || 0;
- document.getElementById('kpi-tareas').textContent = state.dashboard.pending_tasks || 0;
- document.getElementById('kpi-streak-card').textContent = state.dashboard.streak;
-
- if (!state.schedule.length) {
- const schRes = await fetch(`/api/student/schedule`);
- if (schRes.ok) state.schedule = await schRes.json();
- }
- if (!state.exams.length) {
- const exRes = await fetch(`/api/student/exams`);
- if (exRes.ok) state.exams = await exRes.json();
- }
- renderInicioLists();
- }
- else if (tabId === 'clases') {
- UI.title.textContent = 'Mis Clases';
- if (!state.schedule.length) {
- const schRes = await fetch(`/api/student/schedule`);
- if (schRes.ok) state.schedule = await schRes.json();
- }
- renderSchedule();
- }
- else if (tabId === 'tareas') {
- UI.title.textContent = 'Tareas';
- if (!state.tasks.length) {
- const tsRes = await fetch(`/api/student/tasks`);
- if (tsRes.ok) state.tasks = await tsRes.json();
- }
- renderTasks();
- }
- else if (tabId === 'calificaciones') {
- UI.title.textContent = 'Calificaciones';
- if (!state.grades.length) {
- const grRes = await fetch(`/api/student/grades`);
- if (grRes.ok) state.grades = await grRes.json();
- }
- renderGrades();
- }
- else if (tabId === 'proyectos') {
- UI.title.textContent = 'Proyectos';
- if (!state.projects.length) {
- const prRes = await fetch(`/api/student/projects`);
- if (prRes.ok) state.projects = await prRes.json();
- }
- renderProjects();
- }
- else if (tabId === 'examenes') {
- UI.title.textContent = 'Exámenes';
- if (!state.exams.length) {
- const exRes = await fetch(`/api/student/exams`);
- if (exRes.ok) state.exams = await exRes.json();
- }
- renderExams();
- }
- }
- function renderInicioLists() {
- const sContainer = document.getElementById('inicio-upcoming-sessions');
- if (state.schedule.length) {
- sContainer.innerHTML = state.schedule.slice(0,3).map(s => `
- <div style="display:flex; justify-content:space-between; padding: 12px 0; border-bottom: 1px solid var(--border);">
- <div>
- <div style="font-weight: 600; color: var(--ink);">${s.course}</div>
- <div style="font-size: 0.85rem; color: var(--muted);">${s.start} - ${s.end} | ${s.room}</div>
- </div>
- <button class="btn btn-outline" style="padding: 4px 12px; font-size: 0.8rem;" onclick="window.joinClass('${s.course}')">Entrar</button>
- </div>
- `).join('');
- }
- const eContainer = document.getElementById('inicio-upcoming-exams');
- if (state.exams.length) {
- eContainer.innerHTML = state.exams.slice(0,2).map(e => `
- <div style="display:flex; justify-content:space-between; padding: 12px 0; border-bottom: 1px solid var(--border);">
- <div>
- <div style="font-weight: 600; color: var(--rose);">${e.title}</div>
- <div style="font-size: 0.85rem; color: var(--muted);">${new Date(e.date).toLocaleDateString()} | ${e.course}</div>
- </div>
- </div>
- `).join('');
- }
- }
- function renderSchedule() {
- if(state.schedule.length > 0) {
- const next = state.schedule[0];
- document.getElementById('next-class-name').textContent = next.course;
- document.getElementById('next-class-time').textContent = next.start;
- document.getElementById('next-class-room').textContent = next.room;
- }
- const grid = document.getElementById('schedule-grid');
- const days = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes'];
-
- let html = `<div style="display:grid; grid-template-columns: repeat(5, 1fr); gap: 16px;">`;
- for(let d=1; d<=5; d++) {
- const dayClasses = state.schedule.filter(s => s.day === d).sort((a,b) => a.start.localeCompare(b.start));
- html += `<div class="schedule-col">
- <h4 style="text-align:center; padding-bottom: 8px; border-bottom: 2px solid var(--border);">${days[d-1]}</h4>
- <div style="margin-top: 16px; display:flex; flex-direction:column; gap:12px;">
- ${dayClasses.map(c => `
- <div class="card" style="padding: 12px; border-left: 4px solid var(--azure);">
- <div style="font-size: 0.75rem; color: var(--muted); font-weight:700;">${c.start} - ${c.end}</div>
- <div style="font-weight: 600; font-size: 0.9rem; margin: 4px 0;">${c.course}</div>
- <div style="font-size: 0.8rem; color: var(--muted);">📍 ${c.room}</div>
- </div>
- `).join('') || '<div style="color:var(--muted); text-align:center; font-size:0.85rem;">Libre</div>'}
- </div>
- </div>`;
- }
- html += `</div>`;
- grid.innerHTML = html;
- }
- function renderTasks() {
- const c = document.getElementById('tasks-container');
- c.innerHTML = state.tasks.map(t => {
- let badge = '';
- if (t.status === 'graded') badge = `<span class="badge badge-ok">Calificada (${t.score}/${t.max_score})</span>`;
- else if (t.status === 'submitted') badge = '<span class="badge badge-teal">Entregada</span>';
- else if (t.status === 'overdue') badge = '<span class="badge badge-rose">Atrasada</span>';
- else badge = '<span class="badge badge-amber">Pendiente</span>';
-
- const dateStr = t.due_date ? new Date(t.due_date).toLocaleDateString() : 'Sin fecha límite';
- const btn = (t.status === 'pending' || t.status === 'overdue')
- ? `<button class="btn btn-primary" style="width:100%;" onclick="window.openTaskModal(${t.id}, '${t.title}')">Entregar Tarea</button>`
- : `<button class="btn btn-outline" style="width:100%;" disabled>Completada</button>`;
- return `
- <div class="card" style="display:flex; flex-direction:column; justify-content:space-between;">
- <div>
- <div style="display:flex; justify-content:space-between; margin-bottom: 12px;">
- <span class="badge badge-outline">${t.course}</span>
- ${badge}
- </div>
- <h3 style="font-size: 1.1rem; margin-bottom: 8px;">${t.title}</h3>
- <p style="font-size: 0.85rem; color: var(--muted); margin-bottom: 16px;">Vence: ${dateStr}</p>
- </div>
- ${btn}
- </div>
- `;
- }).join('');
- }
- function renderGrades() {
- const tb = document.getElementById('grades-table-body');
- // Group grades by course ideally, but here we just list them.
- tb.innerHTML = state.grades.map(g => {
- const pct = (g.score / g.max_score) * 100;
- let color = pct >= 80 ? 'var(--teal)' : pct >= 50 ? 'var(--amber)' : 'var(--rose)';
-
- return `
- <tr style="border-bottom: 1px solid var(--border);">
- <td style="padding: 16px; font-weight: 600;">${g.course}<br><span style="font-size:0.8rem; color:var(--muted); font-weight:normal;">${g.task_title}</span></td>
- <td style="padding: 16px;">--</td>
- <td style="padding: 16px;">${g.score}/${g.max_score}</td>
- <td style="padding: 16px;">--</td>
- <td style="padding: 16px;">
- <div style="display:flex; align-items:center; gap: 8px;">
- <span style="font-weight: 800; font-family: 'Sora'; color: ${color};">${g.score}</span>
- <div class="progress" style="flex: 1; height: 6px;"><div class="progress-fill" style="width: ${pct}%; background: ${color};"></div></div>
- </div>
- </td>
- </tr>
- `;
- }).join('');
- }
- function renderProjects() {
- const c = document.getElementById('projects-container');
- c.innerHTML = state.projects.map(p => {
- const teamHtml = p.team.map(m => `<div class="user-avatar" style="width: 28px; height: 28px; font-size: 0.7rem; border: 2px solid white; margin-left: -8px;">${m.initial}</div>`).join('');
- const badge = p.status === 'entregado' ? '<span class="badge badge-teal">Entregado</span>' : '<span class="badge badge-amber">En Curso</span>';
-
- return `
- <div class="card">
- <div style="display:flex; justify-content:space-between; margin-bottom: 12px;">
- <span style="font-size: 0.8rem; color: var(--muted);">${p.course}</span>
- ${badge}
- </div>
- <h3 style="margin-bottom: 8px;">${p.title}</h3>
- <p style="font-size: 0.9rem; color: var(--muted); margin-bottom: 16px;">${p.description}</p>
-
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 16px;">
- <div style="display:flex; padding-left: 8px;">${teamHtml}</div>
- <div style="font-size: 0.8rem; font-weight: 600;">Progreso: ${p.progress}%</div>
- </div>
-
- <div class="progress" style="margin-bottom: 16px;"><div class="progress-fill" style="width: ${p.progress}%;"></div></div>
- <button class="btn btn-outline" style="width:100%;" onclick="window.openProjectModal(${p.id})">Ver detalles</button>
- </div>
- `;
- }).join('');
- }
- function renderExams() {
- const c = document.getElementById('exams-container');
- c.innerHTML = state.exams.map(e => `
- <div class="card" style="border-top: 4px solid var(--rose);">
- <div style="display:flex; justify-content:space-between; margin-bottom: 8px;">
- <span class="badge badge-outline">${e.mode}</span>
- <span style="font-size: 0.8rem; font-weight: bold;">${e.duration} min</span>
- </div>
- <h3 style="margin-bottom: 4px;">${e.title}</h3>
- <p style="font-size: 0.8rem; color: var(--muted); margin-bottom: 16px;">${e.course}</p>
-
- <div style="background: var(--surface-alt); padding: 12px; border-radius: 8px; margin-bottom: 16px;">
- <div style="font-size: 0.85rem; font-weight: 600; margin-bottom: 4px;">Temario:</div>
- <div style="font-size: 0.8rem; color: var(--muted);">${e.syllabus}</div>
- </div>
-
- <div style="font-weight: 600; color: var(--rose); text-align: center; font-size: 1.1rem;">
- 📅 ${new Date(e.date).toLocaleDateString()}
- </div>
- </div>
- `).join('');
- }
- if (document.getElementById('sidebar-nav')) {
- init();
- // Setup Logout
- const logoutBtn = document.getElementById('logout-btn');
- if (logoutBtn) {
- logoutBtn.addEventListener('click', async (e) => {
- e.preventDefault();
- await fetch('/api/auth/logout', { method: 'POST' });
- window.location.href = '/login';
- });
- }
- }
- // --- Modal Logic ---
- let activeTaskId = null;
- let activeProjectId = null;
- window.closeModals = function() {
- document.querySelectorAll('.modal-overlay').forEach(m => m.classList.remove('active'));
- activeTaskId = null;
- activeProjectId = null;
- };
- window.openTaskModal = function(taskId, taskTitle) {
- activeTaskId = taskId;
- document.getElementById('task-modal-title').textContent = `Entregar Tarea: ${taskTitle}`;
- document.getElementById('task-content').value = '';
- document.getElementById('task-modal').classList.add('active');
- };
- window.submitTask = async function() {
- if (!activeTaskId) return;
- const content = document.getElementById('task-content').value.trim();
- if (!content) return alert('Por favor introduce el contenido o enlace de la tarea.');
- const btn = document.querySelector('#task-modal .btn-primary');
- btn.disabled = true; btn.textContent = 'Enviando...';
- try {
- const res = await fetch(`/api/student/tasks/${activeTaskId}/submit`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ contenido: content })
- });
- if (res.ok) {
- window.closeModals();
- // Refresh tasks
- state.tasks = await (await fetch(`/api/student/tasks`)).json();
- renderTasks();
- } else {
- alert('Error al enviar la tarea.');
- }
- } catch(err) {
- alert('Error de red.');
- }
- btn.disabled = false; btn.textContent = 'Enviar Tarea';
- };
- window.openProjectModal = function(projectId) {
- const p = state.projects.find(x => x.id === projectId);
- if (!p) return;
- activeProjectId = projectId;
- document.getElementById('project-modal-title').textContent = p.title;
- document.getElementById('project-modal-desc').textContent = p.description;
- const rng = document.getElementById('project-modal-progress');
- rng.value = p.progress;
- document.getElementById('project-modal-progress-text').textContent = p.progress;
- document.getElementById('project-modal').classList.add('active');
- };
- window.updateProjectProgress = async function() {
- if (!activeProjectId) return;
- const progress = parseInt(document.getElementById('project-modal-progress').value, 10);
- const btn = document.querySelector('#project-modal .btn-primary');
- btn.disabled = true; btn.textContent = 'Guardando...';
- try {
- const res = await fetch(`/api/student/projects/${activeProjectId}/progress`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ progress: progress })
- });
- if (res.ok) {
- window.closeModals();
- state.projects = await (await fetch(`/api/student/projects`)).json();
- renderProjects();
- } else {
- alert('Error al actualizar el proyecto.');
- }
- } catch(err) {
- alert('Error de red.');
- }
- btn.disabled = false; btn.textContent = 'Guardar Cambios';
- };
- window.joinClass = function(courseName) {
- document.getElementById('class-modal-text').textContent = `Conectando con el aula virtual de ${courseName}...`;
- document.getElementById('class-modal').classList.add('active');
- setTimeout(() => {
- document.getElementById('class-modal-text').textContent = 'El profesor aún no ha iniciado la sesión. Por favor, espera.';
- }, 2000);
- };
- const fakeNotifs = [
- { title: "Nueva Tarea", desc: "Se ha publicado la tarea de Matemáticas Avanzadas.", time: "Hace 10 min", icon: "📚" },
- { title: "Calificación Publicada", desc: "Has recibido un 9.5 en Física.", time: "Hace 2 horas", icon: "⭐" }
- ];
- window.toggleNotifications = function(e) {
- if (e) e.stopPropagation();
- const drop = document.getElementById('notifications-dropdown');
- const badge = document.getElementById('notif-badge');
- if (!drop) return;
-
- if (!drop.classList.contains('active')) {
- const list = document.getElementById('notifications-list');
- list.innerHTML = fakeNotifs.map(n => `
- <div class="notif-item">
- <div class="notif-icon">${n.icon}</div>
- <div>
- <div class="notif-title">${n.title}</div>
- <div class="notif-desc">${n.desc}</div>
- <div class="notif-time">${n.time}</div>
- </div>
- </div>
- `).join('');
- drop.classList.add('active');
- if (badge) badge.style.display = 'none'; // clear badge
- } else {
- drop.classList.remove('active');
- }
- };
- // Close dropdown when clicking outside
- document.addEventListener('click', (e) => {
- const drop = document.getElementById('notifications-dropdown');
- if (drop && drop.classList.contains('active') && !e.target.closest('.notification-bell')) {
- drop.classList.remove('active');
- }
- });
- });
|