app.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. document.addEventListener('DOMContentLoaded', () => {
  2. const state = {
  3. dashboard: null,
  4. courses: [],
  5. tasks: [],
  6. grades: [],
  7. schedule: [],
  8. projects: [],
  9. exams: []
  10. };
  11. const UI = {
  12. loading: document.getElementById('loading'),
  13. tabs: document.querySelectorAll('.nav-link'),
  14. tabContents: document.querySelectorAll('.tab-content'),
  15. title: document.getElementById('topbar-title')
  16. };
  17. async function init() {
  18. try {
  19. const res = await fetch(`/api/student/dashboard`);
  20. if (res.status === 401) {
  21. window.location.href = '/login';
  22. return;
  23. }
  24. if (!res.ok) throw new Error('API not reachable');
  25. state.dashboard = await res.json();
  26. updateHeader();
  27. await switchTab('inicio');
  28. UI.loading.style.display = 'none';
  29. UI.tabs.forEach(btn => {
  30. btn.addEventListener('click', () => {
  31. switchTab(btn.getAttribute('data-tab'));
  32. });
  33. });
  34. } catch (err) {
  35. UI.loading.textContent = 'Error al cargar los datos. Verifica la API.';
  36. UI.loading.className = 'alert alert-error';
  37. }
  38. }
  39. function updateHeader() {
  40. const d = state.dashboard;
  41. // Topbar
  42. document.getElementById('user-name-display').textContent = d.name;
  43. document.getElementById('user-level-display').textContent = d.level_name;
  44. document.getElementById('user-avatar-initial').textContent = d.name.charAt(0);
  45. // Profile Header (Gamification)
  46. const phName = document.getElementById('ph-name');
  47. if (phName) {
  48. phName.textContent = d.name;
  49. document.getElementById('ph-avatar').textContent = d.name.charAt(0);
  50. document.getElementById('ph-xp').textContent = `${d.xp} XP`;
  51. document.getElementById('ph-level').textContent = d.level;
  52. document.getElementById('ph-streak').textContent = d.streak;
  53. document.getElementById('ph-xp-bar').style.width = `${(d.xp % 1000) / 10}%`;
  54. }
  55. }
  56. async function switchTab(tabId) {
  57. UI.tabs.forEach(t => t.classList.remove('active'));
  58. document.querySelector(`[data-tab="${tabId}"]`).classList.add('active');
  59. UI.tabContents.forEach(c => {
  60. c.style.display = 'none';
  61. c.innerHTML = '';
  62. });
  63. const container = document.getElementById(`tab-${tabId}`);
  64. const template = document.getElementById(`tmpl-${tabId}`);
  65. if (!template) return;
  66. container.appendChild(template.content.cloneNode(true));
  67. container.style.display = 'block';
  68. if (tabId === 'inicio') {
  69. UI.title.textContent = 'Inicio';
  70. document.getElementById('kpi-gpa').textContent = state.dashboard.gpa || 7.5;
  71. document.getElementById('kpi-cursos').textContent = state.dashboard.active_courses || 0;
  72. document.getElementById('kpi-tareas').textContent = state.dashboard.pending_tasks || 0;
  73. document.getElementById('kpi-streak-card').textContent = state.dashboard.streak;
  74. if (!state.schedule.length) {
  75. const schRes = await fetch(`/api/student/schedule`);
  76. if (schRes.ok) state.schedule = await schRes.json();
  77. }
  78. if (!state.exams.length) {
  79. const exRes = await fetch(`/api/student/exams`);
  80. if (exRes.ok) state.exams = await exRes.json();
  81. }
  82. renderInicioLists();
  83. }
  84. else if (tabId === 'clases') {
  85. UI.title.textContent = 'Mis Clases';
  86. if (!state.schedule.length) {
  87. const schRes = await fetch(`/api/student/schedule`);
  88. if (schRes.ok) state.schedule = await schRes.json();
  89. }
  90. renderSchedule();
  91. }
  92. else if (tabId === 'tareas') {
  93. UI.title.textContent = 'Tareas';
  94. if (!state.tasks.length) {
  95. const tsRes = await fetch(`/api/student/tasks`);
  96. if (tsRes.ok) state.tasks = await tsRes.json();
  97. }
  98. renderTasks();
  99. }
  100. else if (tabId === 'calificaciones') {
  101. UI.title.textContent = 'Calificaciones';
  102. if (!state.grades.length) {
  103. const grRes = await fetch(`/api/student/grades`);
  104. if (grRes.ok) state.grades = await grRes.json();
  105. }
  106. renderGrades();
  107. }
  108. else if (tabId === 'proyectos') {
  109. UI.title.textContent = 'Proyectos';
  110. if (!state.projects.length) {
  111. const prRes = await fetch(`/api/student/projects`);
  112. if (prRes.ok) state.projects = await prRes.json();
  113. }
  114. renderProjects();
  115. }
  116. else if (tabId === 'examenes') {
  117. UI.title.textContent = 'Exámenes';
  118. if (!state.exams.length) {
  119. const exRes = await fetch(`/api/student/exams`);
  120. if (exRes.ok) state.exams = await exRes.json();
  121. }
  122. renderExams();
  123. }
  124. }
  125. function renderInicioLists() {
  126. const sContainer = document.getElementById('inicio-upcoming-sessions');
  127. if (state.schedule.length) {
  128. sContainer.innerHTML = state.schedule.slice(0,3).map(s => `
  129. <div style="display:flex; justify-content:space-between; padding: 12px 0; border-bottom: 1px solid var(--border);">
  130. <div>
  131. <div style="font-weight: 600; color: var(--ink);">${s.course}</div>
  132. <div style="font-size: 0.85rem; color: var(--muted);">${s.start} - ${s.end} | ${s.room}</div>
  133. </div>
  134. <button class="btn btn-outline" style="padding: 4px 12px; font-size: 0.8rem;" onclick="window.joinClass('${s.course}')">Entrar</button>
  135. </div>
  136. `).join('');
  137. }
  138. const eContainer = document.getElementById('inicio-upcoming-exams');
  139. if (state.exams.length) {
  140. eContainer.innerHTML = state.exams.slice(0,2).map(e => `
  141. <div style="display:flex; justify-content:space-between; padding: 12px 0; border-bottom: 1px solid var(--border);">
  142. <div>
  143. <div style="font-weight: 600; color: var(--rose);">${e.title}</div>
  144. <div style="font-size: 0.85rem; color: var(--muted);">${new Date(e.date).toLocaleDateString()} | ${e.course}</div>
  145. </div>
  146. </div>
  147. `).join('');
  148. }
  149. }
  150. function renderSchedule() {
  151. if(state.schedule.length > 0) {
  152. const next = state.schedule[0];
  153. document.getElementById('next-class-name').textContent = next.course;
  154. document.getElementById('next-class-time').textContent = next.start;
  155. document.getElementById('next-class-room').textContent = next.room;
  156. }
  157. const grid = document.getElementById('schedule-grid');
  158. const days = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes'];
  159. let html = `<div style="display:grid; grid-template-columns: repeat(5, 1fr); gap: 16px;">`;
  160. for(let d=1; d<=5; d++) {
  161. const dayClasses = state.schedule.filter(s => s.day === d).sort((a,b) => a.start.localeCompare(b.start));
  162. html += `<div class="schedule-col">
  163. <h4 style="text-align:center; padding-bottom: 8px; border-bottom: 2px solid var(--border);">${days[d-1]}</h4>
  164. <div style="margin-top: 16px; display:flex; flex-direction:column; gap:12px;">
  165. ${dayClasses.map(c => `
  166. <div class="card" style="padding: 12px; border-left: 4px solid var(--azure);">
  167. <div style="font-size: 0.75rem; color: var(--muted); font-weight:700;">${c.start} - ${c.end}</div>
  168. <div style="font-weight: 600; font-size: 0.9rem; margin: 4px 0;">${c.course}</div>
  169. <div style="font-size: 0.8rem; color: var(--muted);">📍 ${c.room}</div>
  170. </div>
  171. `).join('') || '<div style="color:var(--muted); text-align:center; font-size:0.85rem;">Libre</div>'}
  172. </div>
  173. </div>`;
  174. }
  175. html += `</div>`;
  176. grid.innerHTML = html;
  177. }
  178. function renderTasks() {
  179. const c = document.getElementById('tasks-container');
  180. c.innerHTML = state.tasks.map(t => {
  181. let badge = '';
  182. if (t.status === 'graded') badge = `<span class="badge badge-ok">Calificada (${t.score}/${t.max_score})</span>`;
  183. else if (t.status === 'submitted') badge = '<span class="badge badge-teal">Entregada</span>';
  184. else if (t.status === 'overdue') badge = '<span class="badge badge-rose">Atrasada</span>';
  185. else badge = '<span class="badge badge-amber">Pendiente</span>';
  186. const dateStr = t.due_date ? new Date(t.due_date).toLocaleDateString() : 'Sin fecha límite';
  187. const btn = (t.status === 'pending' || t.status === 'overdue')
  188. ? `<button class="btn btn-primary" style="width:100%;" onclick="window.openTaskModal(${t.id}, '${t.title}')">Entregar Tarea</button>`
  189. : `<button class="btn btn-outline" style="width:100%;" disabled>Completada</button>`;
  190. return `
  191. <div class="card" style="display:flex; flex-direction:column; justify-content:space-between;">
  192. <div>
  193. <div style="display:flex; justify-content:space-between; margin-bottom: 12px;">
  194. <span class="badge badge-outline">${t.course}</span>
  195. ${badge}
  196. </div>
  197. <h3 style="font-size: 1.1rem; margin-bottom: 8px;">${t.title}</h3>
  198. <p style="font-size: 0.85rem; color: var(--muted); margin-bottom: 16px;">Vence: ${dateStr}</p>
  199. </div>
  200. ${btn}
  201. </div>
  202. `;
  203. }).join('');
  204. }
  205. function renderGrades() {
  206. const tb = document.getElementById('grades-table-body');
  207. // Group grades by course ideally, but here we just list them.
  208. tb.innerHTML = state.grades.map(g => {
  209. const pct = (g.score / g.max_score) * 100;
  210. let color = pct >= 80 ? 'var(--teal)' : pct >= 50 ? 'var(--amber)' : 'var(--rose)';
  211. return `
  212. <tr style="border-bottom: 1px solid var(--border);">
  213. <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>
  214. <td style="padding: 16px;">--</td>
  215. <td style="padding: 16px;">${g.score}/${g.max_score}</td>
  216. <td style="padding: 16px;">--</td>
  217. <td style="padding: 16px;">
  218. <div style="display:flex; align-items:center; gap: 8px;">
  219. <span style="font-weight: 800; font-family: 'Sora'; color: ${color};">${g.score}</span>
  220. <div class="progress" style="flex: 1; height: 6px;"><div class="progress-fill" style="width: ${pct}%; background: ${color};"></div></div>
  221. </div>
  222. </td>
  223. </tr>
  224. `;
  225. }).join('');
  226. }
  227. function renderProjects() {
  228. const c = document.getElementById('projects-container');
  229. c.innerHTML = state.projects.map(p => {
  230. 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('');
  231. const badge = p.status === 'entregado' ? '<span class="badge badge-teal">Entregado</span>' : '<span class="badge badge-amber">En Curso</span>';
  232. return `
  233. <div class="card">
  234. <div style="display:flex; justify-content:space-between; margin-bottom: 12px;">
  235. <span style="font-size: 0.8rem; color: var(--muted);">${p.course}</span>
  236. ${badge}
  237. </div>
  238. <h3 style="margin-bottom: 8px;">${p.title}</h3>
  239. <p style="font-size: 0.9rem; color: var(--muted); margin-bottom: 16px;">${p.description}</p>
  240. <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 16px;">
  241. <div style="display:flex; padding-left: 8px;">${teamHtml}</div>
  242. <div style="font-size: 0.8rem; font-weight: 600;">Progreso: ${p.progress}%</div>
  243. </div>
  244. <div class="progress" style="margin-bottom: 16px;"><div class="progress-fill" style="width: ${p.progress}%;"></div></div>
  245. <button class="btn btn-outline" style="width:100%;" onclick="window.openProjectModal(${p.id})">Ver detalles</button>
  246. </div>
  247. `;
  248. }).join('');
  249. }
  250. function renderExams() {
  251. const c = document.getElementById('exams-container');
  252. c.innerHTML = state.exams.map(e => `
  253. <div class="card" style="border-top: 4px solid var(--rose);">
  254. <div style="display:flex; justify-content:space-between; margin-bottom: 8px;">
  255. <span class="badge badge-outline">${e.mode}</span>
  256. <span style="font-size: 0.8rem; font-weight: bold;">${e.duration} min</span>
  257. </div>
  258. <h3 style="margin-bottom: 4px;">${e.title}</h3>
  259. <p style="font-size: 0.8rem; color: var(--muted); margin-bottom: 16px;">${e.course}</p>
  260. <div style="background: var(--surface-alt); padding: 12px; border-radius: 8px; margin-bottom: 16px;">
  261. <div style="font-size: 0.85rem; font-weight: 600; margin-bottom: 4px;">Temario:</div>
  262. <div style="font-size: 0.8rem; color: var(--muted);">${e.syllabus}</div>
  263. </div>
  264. <div style="font-weight: 600; color: var(--rose); text-align: center; font-size: 1.1rem;">
  265. 📅 ${new Date(e.date).toLocaleDateString()}
  266. </div>
  267. </div>
  268. `).join('');
  269. }
  270. if (document.getElementById('sidebar-nav')) {
  271. init();
  272. // Setup Logout
  273. const logoutBtn = document.getElementById('logout-btn');
  274. if (logoutBtn) {
  275. logoutBtn.addEventListener('click', async (e) => {
  276. e.preventDefault();
  277. await fetch('/api/auth/logout', { method: 'POST' });
  278. window.location.href = '/login';
  279. });
  280. }
  281. }
  282. // --- Modal Logic ---
  283. let activeTaskId = null;
  284. let activeProjectId = null;
  285. window.closeModals = function() {
  286. document.querySelectorAll('.modal-overlay').forEach(m => m.classList.remove('active'));
  287. activeTaskId = null;
  288. activeProjectId = null;
  289. };
  290. window.openTaskModal = function(taskId, taskTitle) {
  291. activeTaskId = taskId;
  292. document.getElementById('task-modal-title').textContent = `Entregar Tarea: ${taskTitle}`;
  293. document.getElementById('task-content').value = '';
  294. document.getElementById('task-modal').classList.add('active');
  295. };
  296. window.submitTask = async function() {
  297. if (!activeTaskId) return;
  298. const content = document.getElementById('task-content').value.trim();
  299. if (!content) return alert('Por favor introduce el contenido o enlace de la tarea.');
  300. const btn = document.querySelector('#task-modal .btn-primary');
  301. btn.disabled = true; btn.textContent = 'Enviando...';
  302. try {
  303. const res = await fetch(`/api/student/tasks/${activeTaskId}/submit`, {
  304. method: 'POST',
  305. headers: { 'Content-Type': 'application/json' },
  306. body: JSON.stringify({ contenido: content })
  307. });
  308. if (res.ok) {
  309. window.closeModals();
  310. // Refresh tasks
  311. state.tasks = await (await fetch(`/api/student/tasks`)).json();
  312. renderTasks();
  313. } else {
  314. alert('Error al enviar la tarea.');
  315. }
  316. } catch(err) {
  317. alert('Error de red.');
  318. }
  319. btn.disabled = false; btn.textContent = 'Enviar Tarea';
  320. };
  321. window.openProjectModal = function(projectId) {
  322. const p = state.projects.find(x => x.id === projectId);
  323. if (!p) return;
  324. activeProjectId = projectId;
  325. document.getElementById('project-modal-title').textContent = p.title;
  326. document.getElementById('project-modal-desc').textContent = p.description;
  327. const rng = document.getElementById('project-modal-progress');
  328. rng.value = p.progress;
  329. document.getElementById('project-modal-progress-text').textContent = p.progress;
  330. document.getElementById('project-modal').classList.add('active');
  331. };
  332. window.updateProjectProgress = async function() {
  333. if (!activeProjectId) return;
  334. const progress = parseInt(document.getElementById('project-modal-progress').value, 10);
  335. const btn = document.querySelector('#project-modal .btn-primary');
  336. btn.disabled = true; btn.textContent = 'Guardando...';
  337. try {
  338. const res = await fetch(`/api/student/projects/${activeProjectId}/progress`, {
  339. method: 'POST',
  340. headers: { 'Content-Type': 'application/json' },
  341. body: JSON.stringify({ progress: progress })
  342. });
  343. if (res.ok) {
  344. window.closeModals();
  345. state.projects = await (await fetch(`/api/student/projects`)).json();
  346. renderProjects();
  347. } else {
  348. alert('Error al actualizar el proyecto.');
  349. }
  350. } catch(err) {
  351. alert('Error de red.');
  352. }
  353. btn.disabled = false; btn.textContent = 'Guardar Cambios';
  354. };
  355. window.joinClass = function(courseName) {
  356. document.getElementById('class-modal-text').textContent = `Conectando con el aula virtual de ${courseName}...`;
  357. document.getElementById('class-modal').classList.add('active');
  358. setTimeout(() => {
  359. document.getElementById('class-modal-text').textContent = 'El profesor aún no ha iniciado la sesión. Por favor, espera.';
  360. }, 2000);
  361. };
  362. const fakeNotifs = [
  363. { title: "Nueva Tarea", desc: "Se ha publicado la tarea de Matemáticas Avanzadas.", time: "Hace 10 min", icon: "📚" },
  364. { title: "Calificación Publicada", desc: "Has recibido un 9.5 en Física.", time: "Hace 2 horas", icon: "⭐" }
  365. ];
  366. window.toggleNotifications = function(e) {
  367. if (e) e.stopPropagation();
  368. const drop = document.getElementById('notifications-dropdown');
  369. const badge = document.getElementById('notif-badge');
  370. if (!drop) return;
  371. if (!drop.classList.contains('active')) {
  372. const list = document.getElementById('notifications-list');
  373. list.innerHTML = fakeNotifs.map(n => `
  374. <div class="notif-item">
  375. <div class="notif-icon">${n.icon}</div>
  376. <div>
  377. <div class="notif-title">${n.title}</div>
  378. <div class="notif-desc">${n.desc}</div>
  379. <div class="notif-time">${n.time}</div>
  380. </div>
  381. </div>
  382. `).join('');
  383. drop.classList.add('active');
  384. if (badge) badge.style.display = 'none'; // clear badge
  385. } else {
  386. drop.classList.remove('active');
  387. }
  388. };
  389. // Close dropdown when clicking outside
  390. document.addEventListener('click', (e) => {
  391. const drop = document.getElementById('notifications-dropdown');
  392. if (drop && drop.classList.contains('active') && !e.target.closest('.notification-bell')) {
  393. drop.classList.remove('active');
  394. }
  395. });
  396. });