| 1 | /** |
| 2 | * MAPS Connect - Perfil público de otro usuario (alumno o docente). |
| 3 | * Contrato esperado de GET /usuarios/{id}: |
| 4 | * { id, nombre, foto, rol, esYo, reputacion, seguidores, siguiendo, loSigo } |
| 5 | * ESTUDIANTE: { carrera, semestre, proposito, certificados[{titulo,descripcion}], |
| 6 | * dudas, respuestas, aportes[{titulo,detalle,tipo,meta}] } |
| 7 | * PROFESOR: { especialidad, biografia, horarioAsesorias, disponibleChat, |
| 8 | * materias[], respuestas, aportes[{titulo,detalle,tipo,meta}] } |
| 9 | */ |
| 10 | |
| 11 | const UsuarioPub = { |
| 12 | data: null, |
| 13 | |
| 14 | async init() { |
| 15 | Layout.init('usuario.html'); |
| 16 | Layout.setPageTitle('Perfil'); |
| 17 | |
| 18 | const root = document.getElementById('usuario-root'); |
| 19 | if (!root) return; |
| 20 | |
| 21 | const id = new URLSearchParams(window.location.search).get('id'); |
| 22 | if (!id) { |
| 23 | this.renderError(root, 'No se indicó un usuario.'); |
| 24 | return; |
| 25 | } |
| 26 | |
| 27 | try { |
| 28 | const data = await API.request(`/usuarios/${encodeURIComponent(id)}`); |
| 29 | if (!data) { |
| 30 | this.renderError(root, 'No se pudo cargar el perfil.'); |
| 31 | return; |
| 32 | } |
| 33 | this.data = data; |
| 34 | this.render(); |
| 35 | } catch (e) { |
| 36 | this.renderError(root, (e && e.message) || 'No se pudo cargar el perfil.'); |
| 37 | } |
| 38 | }, |
| 39 | |
| 40 | num(v) { |
| 41 | const n = Number(v); |
| 42 | return Number.isFinite(n) ? n : null; |
| 43 | }, |
| 44 | |
| 45 | /* ---------- Render principal ---------- */ |
| 46 | |
| 47 | render() { |
| 48 | const d = this.data; |
| 49 | const esDocente = d.rol === 'PROFESOR'; |
| 50 | |
| 51 | document.getElementById('usuario-nombre').textContent = d.nombre || 'Usuario'; |
| 52 | const trigger = document.getElementById('avatarTrigger'); |
| 53 | trigger.innerHTML = Utils.avatarHtml(d.nombre, d.foto, 'profile-avatar trigger', d.nombre); |
| 54 | |
| 55 | const sub = document.getElementById('usuario-sub'); |
| 56 | const sub2 = document.getElementById('usuario-sub2'); |
| 57 | if (esDocente) { |
| 58 | sub.innerHTML = d.especialidad |
| 59 | ? `Docente • <strong>${Utils.esc(d.especialidad)}</strong>` |
| 60 | : 'Docente'; |
| 61 | sub2.innerHTML = `Reputación: <strong>${this.neto(d.reputacion)}</strong>`; |
| 62 | } else { |
| 63 | sub.innerHTML = d.carrera |
| 64 | ? `Estudiante • <strong>${Utils.esc(d.carrera)}</strong>` |
| 65 | : 'Estudiante'; |
| 66 | sub2.innerHTML = (d.semestre |
| 67 | ? `Semestre Vigente: <strong>${this.neto(d.semestre)}° Semestre</strong> • ` |
| 68 | : '') |
| 69 | + `Reputación: <strong>${this.neto(d.reputacion)}</strong>`; |
| 70 | } |
| 71 | |
| 72 | this.renderActions(d); |
| 73 | this.renderIzquierda(d); |
| 74 | this.renderDerecha(d); |
| 75 | }, |
| 76 | |
| 77 | neto(v) { |
| 78 | const n = this.num(v); |
| 79 | return n == null ? '—' : n; |
| 80 | }, |
| 81 | |
| 82 | renderActions(d) { |
| 83 | const actions = document.getElementById('usuario-actions'); |
| 84 | if (!actions) return; |
| 85 | |
| 86 | if (d.esYo) { |
| 87 | actions.innerHTML = '<a class="btn-clean" href="perfil.html">Este es tu perfil</a>'; |
| 88 | return; |
| 89 | } |
| 90 | |
| 91 | const loSigo = d.loSigo === true || d.loSigo === 'true'; |
| 92 | actions.innerHTML = ` |
| 93 | <button class="follow-btn${loSigo ? ' siguiendo' : ''}" type="button" id="usuario-follow" data-siguiendo="${loSigo}">${loSigo ? 'Siguiendo' : 'Seguir'}</button> |
| 94 | <button class="btn-clean" type="button" id="usuario-invitar">Invitar a Círculo</button> |
| 95 | <button class="btn-solid" type="button" id="usuario-mensaje">Enviar Mensaje</button> |
| 96 | `; |
| 97 | |
| 98 | document.getElementById('usuario-follow')?.addEventListener('click', async e => { |
| 99 | const siguiente = await Utils.alternarSeguir(e.target, d.id); |
| 100 | const box = document.getElementById('stat-seg'); |
| 101 | if (box) { |
| 102 | const base = this.num(box.dataset.base); |
| 103 | if (base != null) box.textContent = String(base + (siguiente ? 1 : -1)); |
| 104 | } |
| 105 | }); |
| 106 | |
| 107 | document.getElementById('usuario-invitar')?.addEventListener('click', () => { |
| 108 | Utils.toast('La invitación a círculos de estudio se habilita próximamente.', 'info'); |
| 109 | }); |
| 110 | |
| 111 | document.getElementById('usuario-mensaje')?.addEventListener('click', async () => { |
| 112 | try { |
| 113 | await API.request(`/mensajes/nuevo/${encodeURIComponent(d.id)}`, { method: 'POST' }); |
| 114 | } catch (e) { |
| 115 | // La conversación suele existir; se navega igual. |
| 116 | } |
| 117 | window.location.href = 'mensajes.html'; |
| 118 | }); |
| 119 | }, |
| 120 | |
| 121 | renderIzquierda(d) { |
| 122 | const host = document.getElementById('col-izquierda'); |
| 123 | if (!host) return; |
| 124 | |
| 125 | if (d.rol === 'ESTUDIANTE') { |
| 126 | const certs = Array.isArray(d.certificados) ? d.certificados : []; |
| 127 | const proposito = d.proposito |
| 128 | ? `<div class="purpose-box"><strong>Propósito Declarado</strong><p>${Utils.esc(d.proposito)}</p></div>` |
| 129 | : ''; |
| 130 | const listaCerts = certs.length ? certs.map((c, i) => ` |
| 131 | <div class="certificate-item${i === 0 ? ' active' : ''}"> |
| 132 | <div class="cert-header"> |
| 133 | <span class="cert-title">${Utils.esc(c.titulo || 'Certificado MAPS')}</span> |
| 134 | <span class="badge badge-green">En Curso</span> |
| 135 | </div> |
| 136 | <p class="cert-desc">${Utils.esc(c.descripcion)}</p> |
| 137 | </div>`).join('') |
| 138 | : '<p class="empty-note">Aún no ha seleccionado certificados MAPS.</p>'; |
| 139 | |
| 140 | host.innerHTML = ` |
| 141 | <h3 class="section-heading">Ruta Académica MAPS</h3> |
| 142 | ${proposito} |
| 143 | <h4 class="subsection-heading">Certificados en Curso y Obtenidos</h4> |
| 144 | ${listaCerts}`; |
| 145 | return; |
| 146 | } |
| 147 | |
| 148 | if (d.rol === 'PROFESOR') { |
| 149 | const bio = d.biografia |
| 150 | ? `<div class="purpose-box"><strong>Sobre ${Utils.esc(d.nombre)}</strong><p>${Utils.esc(d.biografia)}</p></div>` |
| 151 | : ''; |
| 152 | const materias = (Array.isArray(d.materias) && d.materias.length) |
| 153 | ? `<li><strong>Materias asignadas:</strong><span style="display:block;margin-top:4px;">${d.materias.map(m => `<span class="materia-tag">${Utils.esc(m)}</span>`).join('')}</span></li>` |
| 154 | : ''; |
| 155 | |
| 156 | host.innerHTML = ` |
| 157 | <h3 class="section-heading">Perfil Docente</h3> |
| 158 | ${bio} |
| 159 | <ul class="prof-detail-list"> |
| 160 | ${d.especialidad ? `<li><strong>Especialidad:</strong> ${Utils.esc(d.especialidad)}</li>` : ''} |
| 161 | ${materias} |
| 162 | ${d.horarioAsesorias ? `<li><strong>Horario de asesorías:</strong> ${Utils.esc(d.horarioAsesorias)}</li>` : ''} |
| 163 | <li><strong>Disponible en chat:</strong> <span class="${d.disponibleChat ? 'ok' : 'off'}">${d.disponibleChat ? 'Sí' : 'No'}</span></li> |
| 164 | </ul>`; |
| 165 | return; |
| 166 | } |
| 167 | |
| 168 | host.innerHTML = '<p class="empty-note">Perfil sin información académica adicional.</p>'; |
| 169 | }, |
| 170 | |
| 171 | renderDerecha(d) { |
| 172 | const esDocente = d.rol === 'PROFESOR'; |
| 173 | const stats = esDocente ? [ |
| 174 | { key: 'seg', label: 'Seguidores', val: d.seguidores }, |
| 175 | { key: 'sig', label: 'Siguiendo', val: d.siguiendo }, |
| 176 | { key: 'res', label: 'Respuestas Aportadas', val: d.respuestas }, |
| 177 | { key: 'mat', label: 'Materias', val: Array.isArray(d.materias) ? d.materias.length : 0 } |
| 178 | ] : [ |
| 179 | { key: 'seg', label: 'Seguidores', val: d.seguidores }, |
| 180 | { key: 'sig', label: 'Siguiendo', val: d.siguiendo }, |
| 181 | { key: 'dud', label: 'Dudas en el Foro', val: d.dudas }, |
| 182 | { key: 'res', label: 'Respuestas Aportadas', val: d.respuestas } |
| 183 | ]; |
| 184 | |
| 185 | const statsHost = document.getElementById('usuario-stats'); |
| 186 | if (statsHost) { |
| 187 | statsHost.innerHTML = stats.map(s => ` |
| 188 | <div class="stat-box"> |
| 189 | <span class="number" id="stat-${s.key}"${s.key === 'seg' ? ` data-base="${this.neto(s.val)}"` : ''}>${this.neto(s.val)}</span> |
| 190 | <span class="label">${Utils.esc(s.label)}</span> |
| 191 | </div>`).join(''); |
| 192 | } |
| 193 | |
| 194 | const aportes = Array.isArray(d.aportes) ? d.aportes : []; |
| 195 | const aportesHost = document.getElementById('usuario-aportes'); |
| 196 | if (aportesHost) { |
| 197 | aportesHost.innerHTML = aportes.length ? aportes.map(a => ` |
| 198 | <div class="public-item"> |
| 199 | <strong>${Utils.esc(a.titulo || 'Aporte')}</strong> |
| 200 | <p>${Utils.esc(a.detalle)}</p> |
| 201 | <div class="item-meta"> |
| 202 | <span>${a.tipo === 'respuesta' ? 'Respuesta en foro' : 'Duda publicada'}</span> |
| 203 | <span>${Utils.esc(a.meta)}</span> |
| 204 | </div> |
| 205 | </div>`).join('') |
| 206 | : '<p class="empty-note">Aún no hay aportaciones públicas.</p>'; |
| 207 | } |
| 208 | }, |
| 209 | |
| 210 | /* ---------- Modal para ampliar avatar ---------- */ |
| 211 | |
| 212 | renderError(root, message) { |
| 213 | root.innerHTML = `<div class="card empty-state"><h2>Perfil no disponible</h2><p>${Utils.esc(message)}</p><a class="side-link" href="inicio.html">Volver al inicio →</a></div>`; |
| 214 | } |
| 215 | }; |
| 216 | |
| 217 | document.addEventListener('DOMContentLoaded', () => { |
| 218 | |
| 219 | const backdrop = document.getElementById('avatarModal'); |
| 220 | const trigger = document.getElementById('avatarTrigger'); |
| 221 | const closeBtn = document.getElementById('closeModalBtn'); |
| 222 | const expanded = document.getElementById('avatarExpanded'); |
| 223 | |
| 224 | function abrirModal() { |
| 225 | const d = UsuarioPub.data; |
| 226 | if (!d || !backdrop || !expanded) return; |
| 227 | if (d.foto) { |
| 228 | expanded.innerHTML = `<img class="avatar-expanded-box" src="${Utils.esc(d.foto)}" alt="Foto de ${Utils.esc(d.nombre)}">`; |
| 229 | } else { |
| 230 | expanded.innerHTML = `<span class="avatar-expanded-box">${Utils.esc(Utils.iniciales(d.nombre))}</span>`; |
| 231 | } |
| 232 | backdrop.classList.add('show'); |
| 233 | } |
| 234 | |
| 235 | function cerrarModal() { |
| 236 | if (backdrop) backdrop.classList.remove('show'); |
| 237 | } |
| 238 | |
| 239 | trigger?.addEventListener('click', abrirModal); |
| 240 | closeBtn?.addEventListener('click', cerrarModal); |
| 241 | backdrop?.addEventListener('click', e => { if (e.target === backdrop) cerrarModal(); }); |
| 242 | document.addEventListener('keydown', e => { |
| 243 | if (e.key === 'Escape' && backdrop.classList.contains('show')) cerrarModal(); |
| 244 | }); |
| 245 | |
| 246 | UsuarioPub.init(); |
| 247 | }); |