| 1 | /** |
| 2 | * MAPS Connect - Ajustes |
| 3 | * Permite: personalizar el color de la página, elegir un estilo visual, |
| 4 | * activar/desactivar el modo oscuro, administrar la cuenta (nombre, contraseña) |
| 5 | * y consultar el aviso de privacidad. |
| 6 | */ |
| 7 | const Ajustes = { |
| 8 | init() { |
| 9 | Layout.init('ajustes.html'); |
| 10 | Layout.setPageTitle('Ajustes'); |
| 11 | |
| 12 | this.initColor(); |
| 13 | this.initEstilos(); |
| 14 | this.initTema(); |
| 15 | this.initCuenta(); |
| 16 | document.getElementById('btn-ver-aviso')?.addEventListener('click', () => AvisoPrivacidad.mostrar()); |
| 17 | }, |
| 18 | |
| 19 | /** Personalización del color de la página (predefinidos + color a medida). */ |
| 20 | initColor() { |
| 21 | const swatches = document.getElementById('swatches-colores'); |
| 22 | const customInput = document.getElementById('color-custom'); |
| 23 | const customTexto = document.getElementById('color-custom-texto'); |
| 24 | const btnRestaurar = document.getElementById('btn-restaurar-color'); |
| 25 | |
| 26 | // --- Swatches predefinidos --- |
| 27 | const current = Colores.guardado(); |
| 28 | const activarSwatch = (hex, boton) => { |
| 29 | if (!swatches) return; |
| 30 | swatches.querySelectorAll('.color-swatch').forEach((el) => { |
| 31 | el.classList.toggle('activo', el === boton); |
| 32 | el.setAttribute('aria-pressed', el === boton ? 'true' : 'false'); |
| 33 | }); |
| 34 | if (customInput) customInput.value = hex; |
| 35 | if (customTexto) customTexto.textContent = hex; |
| 36 | }; |
| 37 | |
| 38 | if (swatches) { |
| 39 | Colores.PRESETS.forEach(({ id, hex, nombre }) => { |
| 40 | const btn = document.createElement('button'); |
| 41 | btn.type = 'button'; |
| 42 | btn.className = 'color-swatch'; |
| 43 | btn.dataset.id = id; |
| 44 | btn.dataset.hex = hex; |
| 45 | btn.style.setProperty('--swatch', hex); |
| 46 | btn.title = nombre; |
| 47 | btn.setAttribute('aria-label', `Color ${nombre}`); |
| 48 | btn.setAttribute('aria-pressed', 'false'); |
| 49 | if (current === hex) { |
| 50 | btn.classList.add('activo'); |
| 51 | btn.setAttribute('aria-pressed', 'true'); |
| 52 | if (customInput) customInput.value = hex; |
| 53 | if (customTexto) customTexto.textContent = hex; |
| 54 | } |
| 55 | btn.addEventListener('click', () => { |
| 56 | Colores.set(hex); |
| 57 | activarSwatch(hex, btn); |
| 58 | Utils.toast(`Color ${nombre} aplicado.`, 'success'); |
| 59 | }); |
| 60 | swatches.appendChild(btn); |
| 61 | }); |
| 62 | } |
| 63 | |
| 64 | // --- Color a medida --- |
| 65 | if (customInput) { |
| 66 | customInput.addEventListener('input', () => { |
| 67 | const hex = customInput.value.toLowerCase(); |
| 68 | Colores.set(hex); |
| 69 | if (customTexto) customTexto.textContent = hex; |
| 70 | swatches?.querySelectorAll('.color-swatch').forEach((el) => el.classList.remove('activo')); |
| 71 | }); |
| 72 | } |
| 73 | |
| 74 | // --- Restablecer color por defecto --- |
| 75 | btnRestaurar?.addEventListener('click', () => { |
| 76 | Colores.limpiar(); |
| 77 | swatches?.querySelectorAll('.color-swatch').forEach((el) => el.classList.remove('activo')); |
| 78 | // Marca el verde (por defecto) como activo. |
| 79 | const verde = swatches?.querySelector('.color-swatch[data-id="verde"]'); |
| 80 | if (verde) { |
| 81 | verde.classList.add('activo'); |
| 82 | if (customInput) customInput.value = verde.dataset.hex; |
| 83 | if (customTexto) customTexto.textContent = verde.dataset.hex; |
| 84 | } |
| 85 | Utils.toast('Color por defecto restablecido.', 'success'); |
| 86 | }); |
| 87 | }, |
| 88 | |
| 89 | /** Selector de estilos de la página (Estilos.STYLES). */ |
| 90 | initEstilos() { |
| 91 | const lista = document.getElementById('estilos-lista'); |
| 92 | if (!lista) return; |
| 93 | |
| 94 | const renderizar = () => { |
| 95 | const activo = Estilos.activo(); |
| 96 | lista.querySelectorAll('.estilo-opcion').forEach((op) => { |
| 97 | const esActivo = op.dataset.estilo === activo; |
| 98 | op.classList.toggle('activo', esActivo); |
| 99 | op.setAttribute('aria-pressed', esActivo ? 'true' : 'false'); |
| 100 | }); |
| 101 | this._bloquearPersonalizaColor(); |
| 102 | }; |
| 103 | |
| 104 | Estilos.STYLES.forEach(({ id, nombre, descripcion, preview }) => { |
| 105 | const op = document.createElement('button'); |
| 106 | op.type = 'button'; |
| 107 | op.className = 'estilo-opcion'; |
| 108 | op.dataset.estilo = id; |
| 109 | op.setAttribute('aria-pressed', 'false'); |
| 110 | op.innerHTML = ` |
| 111 | <span class="estilo-preview" style="background: ${preview}" aria-hidden="true"></span> |
| 112 | <span class="estilo-nombre">${nombre}</span> |
| 113 | <span class="estilo-desc">${descripcion}</span> |
| 114 | <span class="estilo-check" aria-hidden="true">✓</span>`; |
| 115 | op.addEventListener('click', () => { |
| 116 | const activado = Estilos.set(id); |
| 117 | renderizar(); |
| 118 | const e = Estilos.STYLES.find((s) => s.id === activado); |
| 119 | Utils.toast(`Estilo "${e ? e.nombre : activado}" aplicado.`, 'success'); |
| 120 | }); |
| 121 | lista.appendChild(op); |
| 122 | }); |
| 123 | |
| 124 | renderizar(); |
| 125 | }, |
| 126 | |
| 127 | /** |
| 128 | * La personalización de color solo aplica al estilo "Clásico". Con otro |
| 129 | * estilo activo se deshabilita para evitar mezclar paletas. |
| 130 | */ |
| 131 | _bloquearPersonalizaColor() { |
| 132 | const personaliza = document.querySelector('.personaliza-seccion'); |
| 133 | if (!personaliza) return; |
| 134 | const bloqueado = Estilos.activo() !== 'clasico'; |
| 135 | personaliza.querySelectorAll('button, input').forEach((el) => { el.disabled = bloqueado; }); |
| 136 | personaliza.classList.toggle('personaliza-bloqueado', bloqueado); |
| 137 | }, |
| 138 | |
| 139 | /** Modo oscuro / claro. */ |
| 140 | initTema() { |
| 141 | const toggle = document.getElementById('tema-toggle'); |
| 142 | if (toggle) { |
| 143 | toggle.checked = Tema.activo(); |
| 144 | toggle.addEventListener('change', () => { |
| 145 | const oscuro = Tema.alternar(); |
| 146 | // Después de alternar la clase real, sincronizamos el switch. |
| 147 | toggle.checked = oscuro; |
| 148 | Utils.toast(oscuro ? 'Modo oscuro activado.' : 'Modo claro activado.', 'success'); |
| 149 | }); |
| 150 | } |
| 151 | }, |
| 152 | |
| 153 | /** Mi cuenta: cambiar nombre y contraseña. */ |
| 154 | initCuenta() { |
| 155 | const user = Auth.getUser(); |
| 156 | const cuentaEmail = document.getElementById('cuenta-email'); |
| 157 | if (cuentaEmail) cuentaEmail.textContent = user?.email || '—'; |
| 158 | |
| 159 | const formNombre = document.getElementById('form-nombre'); |
| 160 | const inputNombre = document.getElementById('input-nombre'); |
| 161 | if (inputNombre) inputNombre.value = user?.nombre || ''; |
| 162 | |
| 163 | formNombre?.addEventListener('submit', async (e) => { |
| 164 | e.preventDefault(); |
| 165 | const nombre = inputNombre.value.trim(); |
| 166 | if (nombre.length < 2) { |
| 167 | return Utils.toast('Escribe tu nombre completo (mínimo 2 caracteres).', 'error'); |
| 168 | } |
| 169 | try { |
| 170 | const data = await API.request('/usuarios/cuenta/nombre', { |
| 171 | method: 'PUT', |
| 172 | body: JSON.stringify({ nombre }) |
| 173 | }); |
| 174 | Auth.actualizar({ nombre: data.nombre }); |
| 175 | inputNombre.value = data.nombre; |
| 176 | Utils.toast('Nombre actualizado.', 'success'); |
| 177 | } catch (err) { |
| 178 | Utils.toast(err.message || 'No se pudo actualizar el nombre.', 'error'); |
| 179 | } |
| 180 | }); |
| 181 | |
| 182 | const formContrasena = document.getElementById('form-contrasena'); |
| 183 | const actual = document.getElementById('input-contrasena-actual'); |
| 184 | const nueva = document.getElementById('input-contrasena-nueva'); |
| 185 | const confirmar = document.getElementById('input-contrasena-confirmar'); |
| 186 | |
| 187 | formContrasena?.addEventListener('submit', async (e) => { |
| 188 | e.preventDefault(); |
| 189 | const a = actual.value; |
| 190 | const n = nueva.value; |
| 191 | const c = confirmar.value; |
| 192 | |
| 193 | if (!a || !n || !c) { |
| 194 | return Utils.toast('Completa todos los campos.', 'error'); |
| 195 | } |
| 196 | if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(n)) { |
| 197 | return Utils.toast('La contraseña nueva debe tener al menos 8 caracteres y combinar mayúscula, minúscula y número.', 'error'); |
| 198 | } |
| 199 | if (n !== c) { |
| 200 | return Utils.toast('La confirmación no coincide.', 'error'); |
| 201 | } |
| 202 | if (n === a) { |
| 203 | return Utils.toast('La nueva contraseña no puede ser igual a la actual.', 'error'); |
| 204 | } |
| 205 | |
| 206 | try { |
| 207 | await API.request('/usuarios/cuenta/contrasena', { |
| 208 | method: 'PUT', |
| 209 | body: JSON.stringify({ contrasenaActual: a, contrasenaNueva: n }) |
| 210 | }); |
| 211 | actual.value = ''; |
| 212 | nueva.value = ''; |
| 213 | confirmar.value = ''; |
| 214 | Utils.toast('Contraseña actualizada.', 'success'); |
| 215 | } catch (err) { |
| 216 | Utils.toast(err.message || 'No se pudo cambiar la contraseña.', 'error'); |
| 217 | } |
| 218 | }); |
| 219 | } |
| 220 | }; |
| 221 | |
| 222 | document.addEventListener('DOMContentLoaded', () => Ajustes.init()); |