main
js 223 lines 9.54 KB
Raw
1 /**
2 * Mi Ruta MAPS - Certificados del modelo académico.
3 * Contrato esperado de GET /estudiantes/ruta:
4 * { estudiante: { id, matricula, semestre, carrera },
5 * ruta: [{ id, nombre, descripcion, semestreMin, semestreMax, materias[], enRuta }],
6 * catalogo: [ { id, nombre, descripcion, semestreMin, semestreMax, materias[], enRuta } ] }
7 * ASIGNACION: POST /certificados/asignar { idEstudiante, idCertificado }.
8 */
9 document.addEventListener('DOMContentLoaded', async () => {
10 Layout.init('certificados.html');
11 Layout.setPageTitle('Mi Ruta MAPS');
12
13 const content = document.getElementById('main-content');
14 if (!content) return;
15
16 const user = Auth.getUser();
17 if (!user || user.rol === 'PROFESOR') {
18 renderVistaProfesor(content);
19 return;
20 }
21
22 content.innerHTML = '<div class="ruta-container"><p class="loading-message">Cargando tu ruta MAPS…</p></div>';
23 content.addEventListener('click', handlerAgregar);
24
25 let data = {};
26 try {
27 data = await API.request('/estudiantes/ruta');
28 } catch (error) {
29 const sinPerfil = /completa tu perfil|perfil de estudiante aún no está completo/i.test(error?.message || '');
30 renderError(content, sinPerfil);
31 return;
32 }
33
34 renderRuta(content, data || {});
35 });
36
37 function renderVistaProfesor(content) {
38 content.innerHTML = `
39 <div class="ruta-container">
40 <header class="module-heading"><h1>Mi Ruta MAPS</h1><p>Panel de la trayectoria de certificados de los estudiantes.</p></header>
41 <section class="card empty-state-box">
42 <h2>Vista exclusiva para estudiantes</h2>
43 <p>La ruta MAPS agrupa los certificados y sus materias por semestre sugerido. Como docente, puedes orientar a tus estudiantes desde tu vista de docencia.</p>
44 <a class="btn-clean" href="inicio.html">Volver a inicio</a>
45 </section>
46 </div>
47 `;
48 }
49
50 function renderError(content, sinPerfil) {
51 content.innerHTML = `
52 <div class="ruta-container">
53 <header class="module-heading"><h1>Mi Ruta MAPS</h1><p>Explora y arma la ruta de certificados de tu carrera.</p></header>
54 <section class="card empty-state-box">
55 <h2>${sinPerfil ? 'Completa tu perfil de estudiante' : 'No se pudo cargar tu ruta'}</h2>
56 <p>${sinPerfil
57 ? 'Para ver tu ruta MAPS primero debes completar tu perfil de estudiante y elegir tu carrera.'
58 : 'Ocurrió un problema al consultar tu ruta. Inténtalo de nuevo en un momento.'}</p>
59 ${sinPerfil ? '<a class="btn-solid" href="onboarding.html">Completar mi perfil</a>' : ''}
60 </section>
61 </div>
62 `;
63 }
64
65 let estadoRuta = { estudiante: null, ruta: [], catalogo: [] };
66
67 function renderRuta(content, data) {
68 estadoRuta = {
69 estudiante: data.estudiante || null,
70 ruta: Array.isArray(data.ruta) ? data.ruta : [],
71 catalogo: Array.isArray(data.catalogo) ? data.catalogo : []
72 };
73
74 const estudiante = estadoRuta.estudiante;
75 const semestreActual = Number(estudiante?.semestre) || 0;
76 const maxCertificados = 3;
77 const enRuta = estadoRuta.ruta.length;
78
79 content.innerHTML = `
80 <div class="ruta-container">
81 <header class="module-heading">
82 <h1>Mi Ruta MAPS</h1>
83 <p>Tu plan de certificados ${estudiante?.carrera ? `en <strong>${escapeHtml(estudiante.carrera)}</strong>` : ''}, ordenado por el semestre sugerido de sus materias.</p>
84 </header>
85 ${estudiante ? renderBanner(estudiante, semestreActual, enRuta, maxCertificados) : ''}
86 <section id="ruta-section" class="ruta-section" aria-labelledby="ruta-title">
87 <h2 class="ruta-section-title" id="ruta-title">Tu ruta ${enRuta ? `<small>${enRuta} de ${maxCertificados} certificados</small>` : ''}</h2>
88 ${enRuta ? estadoRuta.ruta.map(renderRutaBlock).join('') : renderRutaVacia()}
89 </section>
90 <section class="ruta-section" aria-labelledby="catalogo-title">
91 <h2 class="ruta-section-title" id="catalogo-title">Catálogo de certificados</h2>
92 ${estadoRuta.catalogo.map(cert => renderCatalogoBlock(cert, semestreActual, enRuta, maxCertificados)).join('')}
93 </section>
94 <p class="ruta-footer-note">Sugerencia: arma tu ruta con hasta ${maxCertificados} certificados según los intereses de tu carrera.</p>
95 </div>
96 `;
97 }
98
99 function renderBanner(estudiante, semestreActual, enRuta, maxCertificados) {
100 const bloque = semestreActual
101 ? `Semestre vigente: <strong>${semestreActual}°</strong>`
102 : 'Sin semestre asignado';
103 return `
104 <section class="card ruta-banner" aria-labelledby="ruta-banner-title">
105 <h2 id="ruta-banner-title">${escapeHtml(estudiante.matricula || 'Estudiante')}</h2>
106 <p>${bloque} • Certificados seleccionados: <strong>${enRuta} de ${maxCertificados}</strong></p>
107 </section>
108 `;
109 }
110
111 function renderRutaVacia() {
112 return `
113 <section class="card empty-state-box">
114 <h2>Aún no tienes certificados en tu ruta</h2>
115 <p>Explora el catálogo de abajo y añade hasta 3 certificados relacionados con tu carrera. Seguirán su semestre sugerido en el plan de estudios.</p>
116 </section>
117 `;
118 }
119
120 function renderRutaBlock(cert) {
121 return renderCertificadoCard(cert, true);
122 }
123
124 function renderCatalogoBlock(cert, semestreActual, enRuta, maxCertificados) {
125 const lleno = enRuta >= maxCertificados && !cert.enRuta;
126 return renderCertificadoCard(cert, false, lleno);
127 }
128
129 function renderCertificadoCard(cert, enRuta, lleno) {
130 const rango = rangoSemestres(cert);
131 const estado = estadoCertificado(cert, enRuta);
132 const materias = Array.isArray(cert.materias) ? cert.materias : [];
133 const chips = materias.map(materia =>
134 `<span class="materia-chip">${escapeHtml(materia.nombre || 'Materia')}${Number.isFinite(Number(materia.semestre)) ? ` · S.${materia.semestre}` : ''}</span>`
135 ).join('');
136
137 const cuerpo = `
138 <div class="ruta-block-head">
139 <div>
140 <h3 class="ruta-block-title">${escapeHtml(cert.nombre || 'Certificado')}</h3>
141 ${rango ? `<span class="ruta-block-range">${escapeHtml(rango)}</span>` : ''}
142 </div>
143 <div class="ruta-badges">${estado.badges}</div>
144 </div>
145 ${cert.descripcion ? `<p class="ruta-block-desc">${escapeHtml(cert.descripcion)}</p>` : ''}
146 ${chips ? `<div class="ruta-materias">${chips}</div>` : ''}
147 `;
148
149 const acciones = enRuta
150 ? `<div class="ruta-actions"><button class="btn-clean" type="button" disabled>En tu ruta</button></div>`
151 : `<div class="ruta-actions"><button class="btn-solid" type="button" data-action="agregar" data-cert-id="${escapeHtml(cert.id ?? '')}"${lleno ? ' disabled' : ''}>${lleno ? 'Ruta completa' : 'Añadir a mi ruta'}</button></div>`;
152
153 return `
154 <article class="card ruta-block">
155 ${cuerpo}
156 ${acciones}
157 </article>
158 `;
159 }
160
161 function rangoSemestres(cert) {
162 const min = Number.isFinite(Number(cert.semestreMin)) ? Number(cert.semestreMin) : null;
163 const max = Number.isFinite(Number(cert.semestreMax)) ? Number(cert.semestreMax) : null;
164 if (!min && !max) return '';
165 if (!min || !max) return `Semestre ${min || max}`;
166 return min === max ? `Semestre ${min}` : `Semestres ${min}${max}`;
167 }
168
169 function estadoCertificado(cert, enRuta) {
170 const semestreActual = Number(estadoRuta.estudiante?.semestre) || 0;
171 const min = Number.isFinite(Number(cert.semestreMin)) ? Number(cert.semestreMin) : null;
172 const max = Number.isFinite(Number(cert.semestreMax)) ? Number(cert.semestreMax) : null;
173
174 let badge = '';
175 if (enRuta) {
176 badge = '<span class="badge-ruta">En tu ruta</span>';
177 }
178
179 if (min == null && max == null) {
180 badge += ' <span class="badge-proximo">Sin semestre asociado</span>';
181 return { badges: badge };
182 }
183
184 if (max != null && semestreActual > max) {
185 badge += ' <span class="badge-cursado">Cursado</span>';
186 } else if (semestreActual >= (min ?? 1) && (max == null || semestreActual <= max)) {
187 badge += ' <span class="badge-encurso">En curso</span>';
188 } else {
189 badge += ' <span class="badge-proximo">Próximo</span>';
190 }
191 return { badges: badge };
192 }
193
194 async function handlerAgregar(event) {
195 const btn = event.target.closest('button[data-action="agregar"]');
196 if (!btn) return;
197
198 const estudianteId = estadoRuta.estudiante?.id;
199 const certificadoId = btn.dataset.certId;
200 if (!estudianteId || !certificadoId) return;
201
202 btn.disabled = true;
203 try {
204 await API.request('/certificados/asignar', {
205 method: 'POST',
206 body: JSON.stringify({ idEstudiante: Number(estudianteId), idCertificado: Number(certificadoId) })
207 });
208 Utils.toast('Certificado añadido a tu ruta MAPS.', 'success');
209 const content = document.getElementById('main-content');
210 if (content) {
211 content.innerHTML = '<div class="ruta-container"><p class="loading-message">Actualizando tu ruta…</p></div>';
212 const data = await API.request('/estudiantes/ruta');
213 renderRuta(content, data || {});
214 }
215 } catch (error) {
216 btn.disabled = false;
217 Utils.toast(error.message || 'No se pudo añadir el certificado.', 'error');
218 }
219 }
220
221 function escapeHtml(value) {
222 return String(value ?? '').replace(/[&<>'"]/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' })[character]);
223 }