1<!DOCTYPE html> 2<html lang="es"> 3 4<head> 5
5<script> 6 /* âââ Tema: FOUC prevention â se ejecuta antes del render âââââââââââââââââââ 7 Prioridad al abrir la app: 8 1. Regla horaria: 19:00-08:59 â dark (siempre, override de manual) 9 2. Preferencia manual del usuario (cashflow_theme_manual) 10 3. Preferencia del sistema operativo (prefers-color-scheme) 11 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 12 (function() { 13 var h = new Date().getHours(); 14 var esNoche = (h >= 19 || h < 9); 15 var theme; 16 if (esNoche) { 17 theme = 'dark'; 18 } else { 19 var manual = localStorage.getItem('cashflow_theme_manual') 20 || localStorage.getItem('cashflow_theme'); /* backward compat */ 21 theme = manual 22 ? manual 23 : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); 24 } 25 document.documentElement.setAttribute('data-theme', theme); 26 })(); 27 28 /* Helper global de colores theme-aware para gráficos (canvas no entiende var()). 29 Los gráficos leen estos valores al construirse y se re-tematizan vÃa 'themechange'. */ 30 window.cfChartColors = function () { 31 var cs = getComputedStyle(document.documentElement); 32 var dark = document.documentElement.getAttribute('data-theme') === 'dark'; 33 var get = function (name, fallback) { 34 var v = cs.getPropertyValue(name); 35 return (v && v.trim()) ? v.trim() : fallback; 36 }; 37 return { 38 text: get('--text-secondary', dark ? '#A8BAD0' : '#4B5563'), 39 heading: get('--text-heading', dark ? '#F5F7FA' : '#0A0A0A'), 40 surface: get('--bg-surface', dark ? '#18202F' : '#F8F9FA'), 41 border: get('--border-color', dark ? 'rgba(255,255,255,0.08)' : '#E5E7EB'), 42 grid: dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)', 43 tooltipTheme: dark ? 'dark' : 'light' 44 }; 45 }; 46 47 /* Re-tematiza una instancia de Chart.js (mutación de paths estándar + update). */ 48 window.cfRetintChart = function (ch) { 49 if (!ch || !ch.options) return; 50 var c = window.cfChartColors(); 51 var o = ch.options; 52 if (o.plugins && o.plugins.legend && o.plugins.legend.labels) o.plugins.legend.labels.color = c.heading; 53 if (o.scales) { 54 Object.keys(o.scales).forEach(function (k) { 55 var s = o.scales[k]; 56 if (!s) return; 57 if (s.ticks) s.ticks.color = c.text; 58 if (s.title && 'color' in s.title) s.title.color = c.text; 59 if (s.grid && s.grid.color) s.grid.color = c.grid; 60 }); 61 } 62 ch.update('none'); 63 }; 64 65 /* Registro de callbacks de re-tematización; cada gráfico se anota acá. */ 66 window.cfChartThemers = window.cfChartThemers || []; 67 window.addEventListener('themechange', function () { 68 (window.cfChartThemers || []).forEach(function (fn) { try { fn(); } catch (e) {} }); 69 }); 70 </script>
70 71 <meta charset="utf-8"> 72 <!-- viewport-fit=cover: habilita env(safe-area-inset-*) en iPhone (PWA/notch) 73 maximum-scale=1 + user-scalable=no: bloquea el zoom (pinch/doble-tap) para 74 que la PWA se sienta como una app nativa. --> 75 <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"> 76 <!-- ââ PWA (instalable + comportamiento app-like) ââââââââââââââââââââââââââââââ --> 77 <link rel="manifest" href="/manifest.json"> 78 <meta name="theme-color" content="#0F1420"> 79 <meta name="mobile-web-app-capable" content="yes"> 80 <meta name="apple-mobile-web-app-capable" content="yes"> 81 <!-- 'default' (no 'black-translucent'): en modo standalone iOS reserva el espacio 82 de la barra de estado, asà el header NO queda tapado por el reloj/hora. --> 83 <meta name="apple-mobile-web-app-status-bar-style" content="default"> 84 <meta name="apple-mobile-web-app-title" content="CashFlow"> 85 <style> 86 /* App-like: sin overscroll/bounce ni tirón de recarga que dispare navegación. */
87 html { overscroll-behavior: none; } 88 body { 89 overscroll-behavior: none; 90 touch-action: manipulation; /* elimina el zoom por doble-tap */ 91 -webkit-tap-highlight-color: transparent; 92 } 93 /* App-like: sin selección de texto (las apps nativas no tienen "seleccionar 94 texto" salvo en campos editables). El clic derecho se bloquea en pwa.js; 95 queda reservado para el menú contextual propio de CashFlow (próximamente). */ 96 html, body { 97 -webkit-user-select: none; 98 -moz-user-select: none; 99 -ms-user-select: none; 100 user-select: none; 101 } 102 input, textarea, select, 103 [contenteditable="true"], [contenteditable=""] { 104 -webkit-user-select: text; 105 -moz-user-select: text; 106 -ms-user-select: text; 107 user-select: text; 108 } 109 </style> 110 <!-- Nombre - Autoria - Descripcion --> 111 <title>CashFlow</title> 112 <meta name="author" content="CortexDev | Acosta, Simón MartÃn." /> 113 <meta name="copyright" content="Acosta, Simón MartÃn. | Cortex Digital" /> 114 <meta 115 content="Sistema de gestión financiera integral que permite registrar usuarios, agendar pagos, registrar salarios y gestionar diversas operaciones financieras. Ideal para organizar y controlar tus finanzas personales y empresariales." 116 name="description"> 117 <meta 118 content="gestión financiera, registro de usuarios, agendar pagos, registrar salarios, operaciones financieras, categorÃas, bancos, autenticación de usuarios, finanzas personales, finanzas empresariales" 119 name="keywords"> 120 <meta name="robots" content="index, follow"> 121 <!-- Open Graph Meta Tags (para redes sociales) --> 122 <meta property="og:title" content="CashFlow by Cortex" /> 123 <meta property="og:description" content="Sistema integral para gestionar finanzas personales y empresariales de manera eficiente." /> 124 <meta property="og:image" content="/assets/img/favicon.png" /> 125 <meta property="og:url" content="https://cortexdigital.com.ar/" /> 126 <meta property="og:type" content="website" /> 127 <!-- Twitter Card Meta Tags --> 128 <meta name="twitter:card" content="summary_large_image"> 129 <meta name="twitter:title" content="CashFlow by Cortex"> 130 <meta name="twitter:description" content="Sistema integral para la gestión de finanzas personales y empresariales."> 131 <meta name="twitter:image" content="/assets/img/favicon.png"> 132 <!-- Canonical URL --> 133 <link rel="canonical" href="https://cortexdigital.com.ar/" /> 134 <!-- Favicons --> 135 <link href="/assets/img/favicon.png" rel="icon"> 136 <!-- Ãcono de pantalla de inicio iOS: cuadrado 180x180 (el navegador lo recorta redondeado) --> 137 <link href="/assets/img/pwa-apple-touch-180.png" rel="apple-touch-icon" sizes="180x180"> 138 <!-- Google Fonts â Inter (loaded via variables.css @import) --> 139 <link rel="preconnect" href="https://fonts.googleapis.com"> 140 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 141 <!-- Vendor CSS Files --> 142 <link href="/assets/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet"> 143 <link href="/assets/vendor/bootstrap-icons/bootstrap-icons.css" rel="stylesheet"> 144 <!-- simple-datatables CSS se carga via CDN abajo --> 145 <!-- Variables CSS (Design Tokens) --> 146 <link href="/assets/css/variables.css" rel="stylesheet"> 147 <!-- Template Main CSS File --> 148 <link href="/assets/css/style.css" rel="stylesheet"> 149 <!-- Lógica de chat compartida (chat-ia + widget flotante) --> 150
150<script src="/m/cortexia/js/cortex-chat.js"></script>
150 151 <!-- Render de los gráficos que devuelve CortexIA como datos (Chart.js / ApexCharts). 152 Las librerÃas se cargan al final del body: este archivo solo las usa al dibujar. --> 153
153<script src="/m/cortexia/js/cortex-viz.js"></script>
153 154 <!-- Estilos globales del layout: cards, dropdowns, breadcrumbs, ApexCharts --> 155 <link href="/assets/css/pages/layout.css" rel="stylesheet"> 156 <!-- Chip de modelo/lÃmites que va pegado a los inputs de IA (window.AiModelo) --> 157 <link href="/assets/css/ai-modelo.css" rel="stylesheet"> 158 <!-- CSS especÃfico de cada página se carga via <link> al inicio de cada HBS --> 159 <!-- Toasts (Sileo, port vanilla) â estilos de las notificaciones --> 160 <link href="/assets/vendor/sileo/styles.css" rel="stylesheet"> 161 <link rel="stylesheet" href="https://cdn.datatables.net/2.1.4/css/dataTables.dataTables.css" /> 162 163
163<script src=" 164 https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/simple-datatables.min.js 165 "></script>
165 166 <link href=" 167 https://cdn.jsdelivr.net/npm/[email protected]/dist/style.min.css 168 " rel="stylesheet"> 169 170 <!-- Google tag (gtag.js) -->
vendor: 66 bytes, lines 170-171
170 171 <script async src="https://www.googletagmanager.com/gtag/js?id=
171G-7NZP6V6WLB
vendor: 14 bytes, lines 171-172
171"></script> 172
172<script> 173
vendor: 142 bytes, lines 173-177
173window.dataLayer = window.dataLayer || []; 174 function gtag(){dataLayer.push(arguments);} 175 gtag('js', new Date()); 176 177 gtag('config', '
177G-7NZP6V6WLB
vendor: 6 bytes, lines 177-178
177'); 178
178</script>
178 179</head> 180 181<body> 182 183 <link rel="stylesheet" href="/assets/css/pages/pages-register.css"> 184<main id="pages-register" class="pagesViews"> 185 <!-- ð¬ Fondo de video --> 186 <video autoplay muted loop id="background-video"> 187 <source src="/assets/videos/video2.mp4" type="video/mp4"> 188 Tu navegador no soporta videos HTML5. 189 </video> 190 <div class="overlay"></div> 191 192 <div class="container"> 193 <section class="section register min-vh-100 d-flex flex-column align-items-center justify-content-center py-4"> 194 <div class="container"> 195 <div class="row justify-content-center"> 196 <div class="col-lg-4 col-md-6 d-flex flex-column align-items-center justify-content-center"> 197 198 <div class="card mb-3 register-card"> 199 <div class="card-body"> 200 201 <!-- Logo + tÃtulo --> 202 <div class="pt-4 pb-2 text-center"> 203 <div class="d-flex justify-content-center mb-3"> 204 <a href="https://cortexdigital.com.ar/web/" class="logo d-flex align-items-center w-auto register-logo"> 205 <img src="https://cortexdigital.com.ar/acoslegacy/media/wirtschaft.png" alt="Logo CashFlow"> 206 <span>CashFlow</span> 207 </a> 208 </div> 209 210 <h5 class="card-title register-title">Registro de usuario</h5> 211 <p class="register-subtitle">Ingresa tus datos para crear una cuenta nueva.</p> 212 <div class="register-trial"> 213 ð Al registrarte te regalamos <strong>30 dÃas de Premium</strong><br> 214 <span>Todos los módulos desbloqueados. Sin tarjeta, sin compromiso.</span> 215 </div> 216 </div> 217 218 <!-- ð§¾ Formulario de registro --> 219 <form action="/auth/create" method="post" class="row g-3 needs-validation" novalidate> 220 <!-- ð¡ï¸ Honeypot (campo invisible, debe quedar vacÃo) --> 221 <div class="hp-field" aria-hidden="true"> 222 <label>Dejar en blanco</label> 223 <input type="text" name="website_hp" tabindex="-1" autocomplete="off" /> 224 </div> 225 226 <!-- ð Timestamp para validación de tiempo mÃnimo --> 227 <input type="hidden" name="formStartedAt" id="formStartedAt" value="" /> 228 229 <div class="col-6"> 230 <label for="name" class="form-label register-label"> 231 Nombre <span class="text-danger">*</span> 232 </label> 233 <input 234 type="text" 235 name="nombre" 236 class="form-control register-input" 237 placeholder="Tu nombre" 238 id="name" 239 required 240 > 241 <div class="invalid-feedback">Por favor, ingresa tu nombre.</div> 242 </div> 243 244 <div class="col-6"> 245 <label for="surname" class="form-label register-label"> 246 Apellido <span class="text-danger">*</span> 247 </label> 248 <input 249 type="text" 250 name="apellido" 251 placeholder="Tu apellido" 252 class="form-control register-input" 253 id="surname" 254 required 255 > 256 <div class="invalid-feedback">Por favor, ingresa tu apellido.</div> 257 </div> 258 259 <div class="col-12"> 260 <label for="nacimiento" class="form-label register-label"> 261 Nacimiento <span class="text-danger">*</span> 262 </label> 263 <input 264 type="date" 265 name="nacimiento" 266 class="form-control register-input" 267 id="nacimiento" 268 required 269 > 270 <div class="invalid-feedback">Por favor, ingresa una fecha válida.</div> 271 </div> 272 273 <div class="col-12"> 274 <label for="telefono" class="form-label register-label"> 275 Celular <span class="text-danger">*</span> 276 </label> 277 <input 278 type="tel" 279 placeholder="+54 341 201-3655" 280 name="telefono" 281 class="form-control register-input" 282 id="telefono" 283 autocomplete="tel" 284 inputmode="tel" 285 pattern="[\d\+\-\s\(\)]{6,}" 286 required 287 > 288 <div class="invalid-feedback">
288Por favor, ingresa un número de celular válido.</div> 289 </div> 290 291 <div class="col-12"> 292 <label for="yourEmail" class="form-label register-label"> 293 Correo electrónico <span class="text-danger">*</span> 294 </label> 295 <input 296 type="email" 297 placeholder="[email protected]" 298 name="email" 299 class="form-control register-input" 300 id="yourEmail" 301 required 302 > 303 <div class="invalid-feedback">Por favor, ingresa un correo electrónico válido.</div> 304 </div> 305 306 <div class="col-12"> 307 <label for="yourPassword" class="form-label register-label"> 308 Contraseña <span class="text-danger">*</span> 309 </label> 310 <input 311 type="password" 312 placeholder="********" 313 name="password" 314 class="form-control register-input" 315 id="yourPassword" 316 required 317 minlength="8" 318 autocomplete="new-password" 319 > 320 <div class="invalid-feedback">La contraseña debe tener al menos 8 caracteres.</div> 321 </div> 322 323 <div class="col-12"> 324 <label for="yourConfirmPassword" class="form-label register-label"> 325 Confirmar contraseña <span class="text-danger">*</span> 326 </label> 327 <input 328 type="password" 329 placeholder="********" 330 name="yourConfirmPassword" 331 class="form-control register-input" 332 id="yourConfirmPassword" 333 required 334 autocomplete="new-password" 335 > 336 <div class="invalid-feedback">Por favor, confirmá la contraseña.</div> 337 </div> 338 339 <div class="col-12"> 340 <div class="form-check register-check"> 341 <input 342 class="form-check-input" 343 name="acceptTerms" 344 type="checkbox" 345 value="true" 346 id="acceptTerms" 347 required 348 > 349 <label class="form-check-label" for="acceptTerms"> 350 Acepto todos los 351 <a href="https://cortexdigital.com.ar/web/privacy-policy/" target="_blank"> 352 términos y condiciones 353 </a>. 354 <span class="text-danger">*</span> 355 </label> 356 <div class="invalid-feedback"> 357 Para crear una cuenta debes aceptar los términos y condiciones. 358 </div> 359 </div> 360 </div> 361 362 <div class="col-12 mt-1"> 363 <button class="btn btn-primary w-100 register-btn" type="submit" id="btnSubmit"> 364 Crear cuenta 365 </button> 366 </div> 367 368 <div class="col-12 text-center register-links"> 369 <p class="small mb-0"> 370 ¿Tu cuenta ya existe? 371 <a href="pages-login">Inicia sesión</a> 372 </p> 373 </div> 374 375 <!-- Separador --> 376 <div class="col-12"> 377 <div class="register-divider"> 378 <span>¿Problemas para registrarte?</span> 379 </div> 380 </div> 381 382 <!-- Botón WhatsApp registro manual --> 383 <div class="col-12"> 384 <a 385 href="https://wa.me/5493412013655?text=Hola%20equipo%20de%20CashFlow%2C%20necesito%20registrarme%20de%20forma%20manual.%20Mis%20datos%3A%20%0ANombre%3A%20%0AApellido%3A%20%0ACorreo%3A%20%0ACelular%3A%20" 386 target="_blank" 387 class="btn register-btn-whatsapp w-100" 388 > 389 <i class="bi bi-whatsapp me-2"></i> 390 Registrarme por WhatsApp 391 </a> 392 <p class="register-whatsapp-hint"> 393 El equipo te asistirá y activará tu cuenta manualmente. 394 </p> 395 </div> 396 </form> 397 </div> 398 </div> 399 400 <!-- <div class="copyright"> 401 ©
401 Copyright <strong><a href="https://cortexdigital.com.ar/" target="_blank"><span>AcosLegacy</span></a></strong> 402 </div>--> 403 <div class="credits"> 404 Cortex.Digital 405 <a href="https://cortexdigital.com.ar/" target="_blank">Más info.</a> 406 </div> </div> 407 </div> 408 </div> 409 </section> 410 </div> 411</main> 412 413<a href="#" class="back-to-top d-flex align-items-center justify-content-center"><i 414 class="bi bi-arrow-up-short"></i></a> 415 416<!-- /vendor JS Files -->
417<script src="/assets/vendor/apexcharts/apexcharts.min.js"></script>
vendor: 1 bytes, line 417
417
418<script src="/assets/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
vendor: 1 bytes, line 418
418
419<script src="/assets/vendor/chart.js/chart.min.js"></script>
vendor: 1 bytes, line 419
419
420<script src="/assets/vendor/echarts/echarts.min.js"></script>
420 421<!-- simple-datatables se carga via CDN en head.hbs -->
422<script src="/assets/vendor/tinymce/tinymce.min.js"></script>
vendor: 1 bytes, line 422
422
423<script src="/assets/vendor/php-email-form/validate.js"></script>
423 424<!-- gravitec worker -->
425<script src="https://cdn.gravitec.net/storage/6eeb6c904a22d41fa69f564770a20f0a/client.js" async></script>
425 426 427 428<!-- Modules 429
429<script src="../js/login.js"></script>
429 430
430<script src="../js/register.js"></script>
430 431 --> 432<!-- Motion One (vanilla) â motor de animaciones de UI (global window.Motion) -->
433<script src="/assets/vendor/motion/motion.min.js"></script>
433 434<!-- Sileo (port vanilla) â toasts con fÃsica de resorte (global window.sileo) -->
435<script src="/assets/vendor/sileo/sileo.iife.js"></script>
435 436<!-- Capa propia: notificar.* (toasts) y CFAnim (animaciones). Van después de 437 sus librerÃas y antes del resto, para estar disponibles en toda la app. --> 438<!-- Libreria compartida (src/shared/js/lib): formato es-AR unificado, wrapper de 439 fetch con manejo de 401/403, y helper de modales. Va ANTES del resto para 440 que window.fmt / window.http / window.modal esten disponibles en toda la app. -->
441<script src="/assets/js/lib/format.js"></script>
vendor: 1 bytes, line 441
441
442<script src="/assets/js/lib/modal.js"></script>
442 443<!-- Diagramas Mermaid (window.cfDiagramas): figuras con pantalla completa, editor y 444 versión papel para los PDF. Liviano: el motor Mermaid se baja recién cuando 445 una pantalla tiene un diagrama. -->
446<script src="/assets/js/lib/diagramas.js"></script>
vendor: 1 bytes, line 446
446
447<script src="/assets/js/notify.js"></script>
vendor: 1 bytes, line 447
447
448<script src="/assets/js/lib/http.js"></script>
448 449<!-- Dictado por voz reutilizable: cuelga un microfono de cualquier input de IA -->
450<script src="/assets/js/lib/voz.js"></script>
vendor: 1 bytes, line 450
450
451<script src="/assets/js/anim.js"></script>
451 452<!-- Overlay de carga GLOBAL: spinner bloqueante durante operaciones (evita cortar 453 procesos sin querer, ej. borrado masivo). Intercepta fetch/$.ajax de mutación. -->
454<script src="/assets/js/cargando.js"></script>
454 455<!-- Lee los avisos flash (cookie cf_flash + query params) y los muestra como toast -->
456<script src="/assets/js/flash.js"></script>
456 457<!-- Template Main JS File -->
458<script src="/assets/js/main.js"></script>
vendor: 1 bytes, line 458
458
459<script src="/assets/js/header.js"></script>
459 460<!-- Selector de modelo de IA (window.AiModelo): el chip que va pegado a los 461 inputs de IA con el LLM en uso, sus lÃmites y el cambio de modelo. Va ANTES 462 del panel del header, que lo usa para las listas de modelos. -->
463<script src="/assets/js/ai-modelo.js"></script>
463 464<!-- Panel "Disponibilidad de IA" del header: una barra por LLM conectado, con 465 estado de cuota, orden de uso editable y test de conexión. -->
466<script src="/assets/js/ai-estado.js"></script>
466 467<!-- PWA: instalación, bloqueo de zoom y del gesto de back (app-like) -->
468<script src="/assets/js/pwa.js"></script>
468 469<!-- Caché offline: capa de acceso al service worker (botón Sincronizar del aside, 470 auto-sync cada 24 h, estado online/offline). Va después de pwa.js, que registra el SW. -->
471<script src="/assets/js/cache.js"></script>
vendor: 1 bytes, line 471
471
472<script src="/assets/js/sound.js"></script>
472 473<!-- Feedback de UI: sonidos de interacción (cuelume) + animación de contadores -->
474<script type="module" src="/assets/js/cuelume-init.js"></script>
vendor: 1 bytes, line 474
474
475<script src="/assets/js/count-up.js" defer></script>
475 476<!-- JS Paypal: TEST -->
477<script 478 src="https://www.paypal.com/sdk/js?client-id=AWeQ5E_4amzccWiMXexpFJcPRTsxPOKLQOdqGhmiugCcue6h3Y5jGDzRoJZfWJwBNy5EEjzO1TgeFYXJ&components=buttons"></script>
478 479 480 <!-- Incluir jQuery -->
481<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
481 482<!-- Incluir DataTables después de jQuery -->
483<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
vendor: 1 bytes, line 483
483
484<script> 485// ââ Selects del sistema: orden alfabético + ancho ADAPTATIVO al contenido ââââââ 486// Cada <select> se ordena (placeholders arriba, orden natural es+numeric) y se le 487// fija el ancho de su opción MÃS LARGA â sin saltos de lÃnea y sin ocupar de más 488// (cada dropdown mide distinto según su contenido). Cubre selects dinámicos 489// (cargados por fetch). Opt-out: data-no-sort (orden) y data-no-width (ancho). 490document.addEventListener("DOMContentLoaded", function () { 491 'use strict'; 492 function ordenarSelect(sel) { 493 if (!sel || sel.dataset.noSort !== undefined || sel.dataset.sorting !== undefined) return; 494 if (sel.querySelector('optgroup')) return; // no tocamos selects con grupos 495 var opts = Array.prototype.slice.call(sel.options); 496 if (opts.length < 2) return; 497 var fijos = opts.filter(function (o) { return o.value === ''; }); // placeholders arriba 498 var resto = opts.filter(function (o) { return o.value !== ''; }); 499 resto.sort(function (a, b) { return a.text.toLowerCase().localeCompare(b.text.toLowerCase(), 'es', { numeric: true }); }); 500 var objetivo = fijos.concat(resto); 501 if (objetivo.every(function (o, i) { return o === opts[i]; })) return; // ya ordenado â no tocar 502 var valor = sel.value; 503 sel.dataset.sorting = '1'; 504 objetivo.forEach(function (o) { sel.appendChild(o); }); // mueve nodos (conserva estado) 505 sel.value = valor; 506 delete sel.dataset.sorting; 507 } 508 // Mide texto sin tocar el DOM (canvas), con la tipografÃa real del select. 509 var _ctx = null; 510 function medirTexto(texto, font) { 511 if (!_ctx) { _ctx = document.createElement('canvas').getContext('2d'); } 512 _ctx.font = font; 513 return _ctx.measureText(texto || '').width; 514 } 515 // Fija el ancho del select al de su opción MÃS LARGA (adaptativo por select): 516 // ningún salto de lÃnea y sin ocupar de más (cada uno mide distinto según su 517 // contenido). Opt-out: data-no-width. 518 function ajustarAncho(sel) { 519 if (!sel || sel.dataset.noWidth !== undefined || !sel.options.length) return; 520 var cs = getComputedStyle(sel); 521 var font = (cs.fontWeight || '400') + ' ' + (cs.fontSize || '14px') + ' ' + (cs.fontFamily || 'sans-serif'); 522 var max = 0; 523 for (var i = 0; i < sel.options.length; i++) { var w = medirTexto(sel.options[i].text, font); if (w > max) max = w; } 524 if (!max) return; 525 var ancho = max + 26; // 26px â flecha del select + respiro 526 if (cs.boxSizing === 'border-box') { 527 ancho += (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0) 528 + (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0); 529 } 530 ancho = Math.ceil(ancho); 531 if (ancho < 56) ancho = 56; // piso para selects muy cortos 532 sel.style.width = ancho + 'px'; 533 sel.style.maxWidth = '100%'; // nunca desborda el contenedor 534 } 535 function procesarSelect(sel) { ordenarSelect(sel); ajustarAncho(sel); } 536 function procesarTodos(root) { (root || document).querySelectorAll('select').forEach(procesarSelect); } 537 window.ordenarSelects = procesarTodos; // por si un módulo quiere forzarlo tras poblar opciones 538 539 procesarTodos(document); 540 // Con web fonts el ancho exacto se conoce recién cuando cargan â recalculamos. 541 if (document.fonts && document.fonts.ready) document.fonts.ready.then(function () { procesarTodos(document); }); 542 543 // Selects que aparecen/cambian después (cargados por fetch en los módulos). 544 if (window.MutationObserver) { 545 var t = null; 546 var obs = new MutationObserver(function (muts) { 547 var relevante = muts.some(function (m) { 548 return Array.prototype.some.call(m.addedNodes, function (n) { 549 return n.nodeType === 1 && (n.tagName === 'SELECT' || n.tagName === 'OPTION' || (n.querySelector && n.querySelector('select'))); 550 }); 551 }); 552 if (!relevante) return; 553 clearTimeout(t); 554 t = setTimeout(function () { obs.disconnect(); procesarTodos(document); obs.observe(document.body, { childList: true, subtree: true }); }, 120); 555 }); 556 obs.observe(document.body, { childList: true, subtree: true }); 557 } 558}); 559</script>
vendor: 1 bytes, line 559
559
560<script src="./assets/js/register.js"></script>
560 561 562 <footer id="footer" class="footer px-3 py-2"> 563 <p class="small mb-1">© 2024 Cortex.Digital - <a href="https://cortexdigital.com.ar/" target="_blank">Más info</a> ⢠<a target="_blank" href="https://www.linkedin.com/in/acostasimon/">Acosta Simón</a></p> 564 </footer> 565 <style> 566 /* ===== Footer CashFlow / Cortex Digital â Tema Claro ===== */ 567 568 footer.footer { 569 background-color: var(--bg-base); 570 border-top: 1px solid var(--border-color); 571 box-shadow: 0 -1px 0 var(--border-color); 572 color: var(--text-secondary); 573 text-align: center; 574 padding: 1rem 1rem; 575 font-size: 0.85rem; 576 } 577 578 footer.footer p { 579 color: var(--text-secondary); 580 margin: 0.25rem 0; 581 } 582 583 footer.footer a { 584 color: var(--color-lime-dark) !important; 585 font-weight: 600; 586 text-decoration: none; 587 transition: color 0.2s ease; 588 } 589 590 footer.footer a:hover { 591 color: var(--color-lime) !important; 592 } 593 594 @media (max-width: 768px) { 595 footer.footer { 596 font-size: 0.78rem; 597 padding: 0.8rem 0.5rem; 598 } 599 } 600 601 </style> <a href="#" class="back-to-top d-flex align-items-center justify-content-center"><i 602 class="bi bi-arrow-up-short"></i></a> 603 604 <!-- /vendor JS Files --> 605
605<script src="/assets/vendor/apexcharts/apexcharts.min.js"></script>
605 606
606<script src="/assets/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
606 607
607<script src="/assets/vendor/chart.js/chart.min.js"></script>
607 608
608<script src="/assets/vendor/echarts/echarts.min.js"></script>
608 609 <!-- simple-datatables se carga via CDN en head.hbs --> 610
610<script src="/assets/vendor/tinymce/tinymce.min.js"></script>
610 611
611<script src="/assets/vendor/php-email-form/validate.js"></script>
611 612 <!-- gravitec worker --> 613
613<script src="https://cdn.gravitec.net/storage/6eeb6c904a22d41fa69f564770a20f0a/client.js" async></script>
613 614 615 616 <!-- Modules 617
617<script src="../js/login.js"></script>
617 618
618<script src="../js/register.js"></script>
618 619 --> 620 <!-- Motion One (vanilla) â motor de animaciones de UI (global window.Motion) --> 621
621<script src="/assets/vendor/motion/motion.min.js"></script>
621 622 <!-- Sileo (port vanilla) â toasts con fÃsica de resorte (global window.sileo) --> 623
623<script src="/assets/vendor/sileo/sileo.iife.js"></script>
623 624 <!-- Capa propia: notificar.* (toasts) y CFAnim (animaciones). Van después de 625 sus librerÃas y antes del resto, para estar disponibles en toda la app. --> 626 <!-- Libreria compartida (src/shared/js/lib): formato es-AR unificado, wrapper de 627 fetch con manejo de 401/403, y helper de modales. Va ANTES del resto para 628 que window.fmt / window.http / window.modal esten disponibles en toda la app. --> 629
629<script src="/assets/js/lib/format.js"></script>
629 630
630<script src="/assets/js/lib/modal.js"></script>
630 631 <!-- Diagramas Mermaid (window.cfDiagramas): figuras con pantalla completa, editor y 632 versión papel para los PDF. Liviano: el motor Mermaid se baja recién cuando 633 una pantalla tiene un diagrama. --> 634
634<script src="/assets/js/lib/diagramas.js"></script>
634 635
635<script src="/assets/js/notify.js"></script>
635 636
636<script src="/assets/js/lib/http.js"></script>
636 637 <!-- Dictado por voz reutilizable: cuelga un microfono de cualquier input de IA --> 638
638<script src="/assets/js/lib/voz.js"></script>
638 639
639<script src="/assets/js/anim.js"></script>
639 640 <!-- Overlay de carga GLOBAL: spinner bloqueante durante operaciones (evita cortar 641 procesos sin querer, ej. borrado masivo). Intercepta fetch/$.ajax de mutación. --> 642
642<script src="/assets/js/cargando.js"></script>
642 643 <!-- Lee los avisos flash (cookie cf_flash + query params) y los muestra como toast --> 644
644<script src="/assets/js/flash.js"></script>
644 645 <!-- Template Main JS File --> 646
646<script src="/assets/js/main.js"></script>
646 647
647<script src="/assets/js/header.js"></script>
647 648 <!-- Selector de modelo de IA (window.AiModelo): el chip que va pegado a los 649 inputs de IA con el LLM en uso, sus lÃmites y el cambio de modelo. Va ANTES 650 del panel del header, que lo usa para las listas de modelos. --> 651
651<script src="/assets/js/ai-modelo.js"></script>
651 652 <!-- Panel "Disponibilidad de IA" del header: una barra por LLM conectado, con 653 estado de cuota, orden de uso editable y test de conexión. --> 654
654<script src="/assets/js/ai-estado.js"></script>
654 655 <!-- PWA: instalación, bloqueo de zoom y del gesto de back (app-like) --> 656
656<script src="/assets/js/pwa.js"></script>
656 657 <!-- Caché offline: capa de acceso al service worker (botón Sincronizar del aside, 658 auto-sync cada 24 h, estado online/offline). Va después de pwa.js, que registra el SW. --> 659
659<script src="/assets/js/cache.js"></script>
659 660
660<script src="/assets/js/sound.js"></script>
660 661 <!-- Feedback de UI: sonidos de interacción (cuelume) + animación de contadores --> 662
662<script type="module" src="/assets/js/cuelume-init.js"></script>
662 663
663<script src="/assets/js/count-up.js" defer></script>
663 664 <!-- JS Paypal: TEST --> 665
665<script 666 src="https://www.paypal.com/sdk/js?client-id=AWeQ5E_4amzccWiMXexpFJcPRTsxPOKLQOdqGhmiugCcue6h3Y5jGDzRoJZfWJwBNy5EEjzO1TgeFYXJ&components=buttons"></script>
666 667 668 <!-- Incluir jQuery --> 669
669<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
669 670 <!-- Incluir DataTables después de jQuery --> 671
671<script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
671 672
672<script> 673 // ââ Selects del sistema: orden alfabético + ancho ADAPTATIVO al contenido ââââââ 674 // Cada <select> se ordena (placeholders arriba, orden natural es+numeric) y se le 675 // fija el ancho de su opción MÃS LARGA â sin saltos de lÃnea y sin ocupar de más 676 // (cada dropdown mide distinto según su contenido). Cubre selects dinámicos 677 // (cargados por fetch). Opt-out: data-no-sort (orden) y data-no-width (ancho). 678 document.addEventListener("DOMContentLoaded", function () { 679 'use strict'; 680 function ordenarSelect(sel) { 681 if (!sel || sel.dataset.noSort !== undefined || sel.dataset.sorting !== undefined) return; 682 if (sel.querySelector('optgroup')) return; // no tocamos selects con grupos 683 var opts = Array.prototype.slice.call(sel.options); 684 if (opts.length < 2) return; 685 var fijos = opts.filter(function (o) { return o.value === ''; }); // placeholders arriba 686 var resto = opts.filter(function (o) { return o.value !== ''; }); 687 resto.sort(function (a, b) { return a.text.toLowerCase().localeCompare(b.text.toLowerCase(), 'es', { numeric: true }); }); 688 var objetivo = fijos.concat(resto); 689 if (objetivo.every(function (o, i) { return o === opts[i]; })) return; // ya ordenado â no tocar 690 var valor = sel.value; 691 sel.dataset.sorting = '1'; 692 objetivo.forEach(function (o) { sel.appendChild(o); }); // mueve nodos (conserva estado) 693 sel.value = valor; 694 delete sel.dataset.sorting; 695 } 696 // Mide texto sin tocar el DOM (canvas), con la tipografÃa real del select. 697 var _ctx = null; 698 function medirTexto(texto, font) { 699 if (!_ctx) { _ctx = document.createElement('canvas').getContext('2d'); } 700 _ctx.font = font; 701 return _ctx.measureText(texto || '').width; 702 } 703 // Fija el ancho del select al de su opción MÃS LARGA (adaptativo por select): 704 // ningún salto de lÃnea y sin ocupar de más (cada uno mide distinto según su 705 // contenido). Opt-out: data-no-width. 706 function ajustarAncho(sel) { 707 if (!sel || sel.dataset.noWidth !== undefined || !sel.options.length) return; 708 var cs = getComputedStyle(sel); 709 var font = (cs.fontWeight || '400') + ' ' + (cs.fontSize || '14px') + ' ' + (cs.fontFamily || 'sans-serif'); 710 var max = 0; 711 for (var i = 0; i < sel.options.length; i++) { var w = medirTexto(sel.options[i].text, font); if (w > max) max = w; } 712 if (!max) return; 713 var ancho = max + 26; // 26px â flecha del select + respiro 714 if (cs.boxSizing === 'border-box') { 715 ancho += (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0) 716 + (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0); 717 } 718 ancho = Math.ceil(ancho); 719 if (ancho < 56) ancho = 56; // piso para selects muy cortos 720 sel.style.width = ancho + 'px'; 721 sel.style.maxWidth = '100%'; // nunca desborda el contenedor 722 } 723 function procesarSelect(sel) { ordenarSelect(sel); ajustarAncho(sel); } 724 function procesarTodos(root) { (root || document).querySelectorAll('select').forEach(procesarSelect); } 725 window.ordenarSelects = procesarTodos; // por si un módulo quiere forzarlo tras poblar opciones 726 727 procesarTodos(document); 728 // Con web fonts el ancho exacto se conoce recién cuando cargan â recalculamos. 729 if (document.fonts && document.fonts.ready) document.fonts.ready.then(function () { procesarTodos(document); }); 730 731 // Selects que aparecen/cambian después (cargados por fetch en los módulos). 732 if (window.MutationObserver) { 733 var t = null; 734 var obs = new MutationObserver(function (muts) { 735 var relevante = muts.some(function (m) { 736 return Array.prototype.some.call(m.addedNodes, function (n) { 737 return n.nodeType === 1 && (n.tagName === 'SELECT' || n.tagName === 'OPTION' || (n.querySelector && n.querySelector('select'))); 738 }); 739 }); 740 if (!relevante) return; 741 clearTimeout(t); 742 t = setTimeout(function () { obs.disconnect(); procesarTodos(document); obs.observe(document.body, { childList: true, subtree: true }); }, 120); 743 }); 744 obs.observe(document.body, { childList: true, subtree: true }); 745 } 746 }); 747 </script>
747 748 749
749<script> 750 /** 751 * ============================================================================ 752 * CashFlow Sound System v1.0.0 753 * ============================================================================ 754 * Sistema universal de sonidos para feedback de usuario 755 * 756 * Autor: Acosta Simón MartÃn - [email protected] 757 * Proyecto: CashFlow v2.5.0+ 758 * 759 * UBICACIÃN: /views/assets/js/sounds.js 760 * 761 * CARACTERÃSTICAS: 762 * - Reproducción automática en respuestas exitosas de formularios 763 * - Soporte para AJAX/Fetch nativo 764 * - Configuración via data-attributes 765 * - Excepciones personalizables (ej: chat IA) 766 * - API pública para llamadas manuales 767 * - Precarga de sonidos para mejor rendimiento 768 * - Control de volumen global 769 * - Respeto por preferencias de usuario (reducción de movimiento) 770 * ============================================================================ 771 */ 772console.log('Sound.js cargado'); 773 774(function(window) { 775 'use strict'; 776 777 // ========================================================================= 778 // CONFIGURACIÃN PRINCIPAL 779 // ========================================================================= 780 781 const CONFIG = { 782 // Ruta base de los archivos de sonido 783 basePath: '/assets/sounds/', 784 785 // Catálogo de sonidos disponibles 786 sounds: { 787 success: 'correcto.mp3', // Formularios exitosos 788 chat: 'chat.mp3', // Chat IA 789 pop: 'pop.mp3', // Acciones generales (notificaciones, etc) 790 error: 'pop.mp3' // Errores (usa pop por defecto, personalizable) 791 }, 792 793 // Volumen global (0.0 a 1.0) 794 volume: 0.5, 795 796 // Habilitar/deshabilitar sonidos globalmente 797 enabled: true, 798 799 // Selectores para identificar el chat IA (excepción) 800 chatSelectors: [ 801 '[data-sound="chat"]', 802 '#chat-ia-form', 803 '#gemini-chat-form', 804 '.chat-ia-input', 805 '[data-module="chat-ia"]' 806 ], 807 808 // Selectores a ignorar (no reproducir sonido) 809 ignoreSelectors: [ 810 '[data-sound="none"]', 811 '[data-sound-disabled]', 812 '.no-sound' 813 ], 814 815 // Tiempo mÃnimo entre sonidos (evita spam) 816 debounceMs: 300 817 }; 818 819 // ========================================================================= 820 // CLASE PRINCIPAL: CashFlowSounds 821 // ========================================================================= 822 823 class CashFlowSounds { 824 constructor(options = {}) { 825 // Merge configuración personalizada 826 this.config = { ...CONFIG, ...options }; 827 828 // Cache de elementos de audio 829 this.audioCache = new Map(); 830 831 // Control de debounce 832 this.lastPlayTime = 0; 833 834 // Estado de inicialización 835 this.initialized = false; 836 837 // Verificar preferencias de usuario 838 this.checkUserPreferences(); 839 840 // Inicializar 841 this.init(); 842 } 843 844 // ===================================================================== 845 // INICIALIZACIÃN 846 // ===================================================================== 847 848 init() { 849 if (this.initialized) return; 850 851 // Precargar sonidos 852 this.preloadSounds(); 853 854 // Interceptar respuestas de formularios 855 this.interceptFormSubmissions(); 856 857 // Interceptar llamadas fetch/axios 858 this.interceptAjaxCalls(); 859 860 // Escuchar eventos personalizados 861 this.listenCustomEvents(); 862 863 this.initialized = true; 864 865 console.log('ð CashFlow Sound System inicializado'); 866 } 867 868 /** 869 * Verificar preferencias de accesibilidad del usuario 870 */ 871 checkUserPreferences() { 872 // Respetar preferencia de reducción de movimiento/sonido 873 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 874 this.config.volume = 0.2; // Reducir volumen 875 } 876 877 // Verificar localStorage para preferencias guardadas 878 const savedPrefs = localStorage.getItem('cashflow_sound_prefs'); 879 if (savedPrefs) { 880 try { 881 const prefs = JSON.parse(savedPrefs); 882 this.config.enabled = prefs.enabled ?? this.config.enabled; 883 this.config.volume = prefs.volume ?? this.config.volume; 884 } catch (e) { 885 console.warn('Error al cargar preferencias de sonido:', e); 886 } 887 } 888 } 889 890 /** 891 * Precargar todos los sonidos en memoria 892 */ 893 preloadSounds() {
894 Object.entries(this.config.sounds).forEach(([key, filename]) => { 895 const audio = new Audio(this.config.basePath + filename); 896 audio.preload = 'auto'; 897 audio.volume = this.config.volume; 898 this.audioCache.set(key, audio); 899 }); 900 } 901 902 // ===================================================================== 903 // REPRODUCCIÃN DE SONIDOS 904 // ===================================================================== 905 906 /** 907 * Reproducir un sonido especÃfico 908 * @param {string} soundKey - Clave del sonido (success, chat, pop, error) 909 * @param {object} options - Opciones adicionales 910 */ 911 play(soundKey = 'success', options = {}) { 912 // Verificar si está habilitado 913 if (!this.config.enabled) return; 914 915 // Control de debounce 916 const now = Date.now(); 917 if (now - this.lastPlayTime < this.config.debounceMs) return; 918 this.lastPlayTime = now; 919 920 // Obtener audio del cache o crear nuevo 921 let audio = this.audioCache.get(soundKey); 922 923 if (!audio) { 924 // Si no existe la clave, usar 'success' por defecto 925 audio = this.audioCache.get('success'); 926 console.warn(`Sonido '${soundKey}' no encontrado, usando 'success'`); 927 } 928 929 if (!audio) return; 930 931 // Configurar volumen (permite override) 932 audio.volume = options.volume ?? this.config.volume; 933 934 // Reiniciar y reproducir 935 audio.currentTime = 0; 936 937 // Usar promesa para manejar errores de autoplay 938 const playPromise = audio.play(); 939 940 if (playPromise !== undefined) { 941 playPromise.catch(error => { 942 // Autoplay bloqueado por el navegador 943 console.debug('Reproducción de sonido bloqueada:', error.message); 944 }); 945 } 946 } 947 948 /** 949 * Atajos para sonidos especÃficos 950 */ 951 success(options) { this.play('success', options); } 952 chat(options) { this.play('chat', options); } 953 pop(options) { this.play('pop', options); } 954 error(options) { this.play('error', options); } 955 956 // ===================================================================== 957 // INTERCEPCIÃN DE FORMULARIOS 958 // ===================================================================== 959 960 /** 961 * Interceptar envÃos de formularios tradicionales 962 */ 963 interceptFormSubmissions() { 964 document.addEventListener('submit', (e) => { 965 const form = e.target; 966 967 // Verificar si debe ignorarse 968 if (this.shouldIgnore(form)) return; 969 970 // Determinar qué sonido usar 971 const soundKey = this.getSoundKeyForElement(form); 972 973 // Guardar referencia para reproducir después del éxito 974 form._cashflowSound = soundKey; 975 }); 976 } 977 978 /** 979 * Interceptar llamadas AJAX (fetch y axios) 980 */ 981 interceptAjaxCalls() { 982 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 983 // Interceptor de Fetch nativo 984 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 985 const originalFetch = window.fetch; 986 const self = this; 987 988 window.fetch = function(...args) { 989 // Opt-out por request: fetch(url, { sonido: false }) (refrescos automáticos) 990 if (args[1] && args[1].sonido === false) return originalFetch.apply(this, args); 991 return originalFetch.apply(this, args).then(response => { 992 // Solo procesar si la respuesta es exitosa 993 if (response.ok) { 994 // Clonar respuesta para poder leerla 995 const clonedResponse = response.clone(); 996 997 // Intentar determinar el contexto 998 clonedResponse.json().then(data => { 999 // Verificar si la respuesta indica éxito 1000 if (self.isSuccessResponse(data)) { 1001 // Determinar sonido basado en URL o contexto 1002 const soundKey = self.getSoundKeyFromUrl(args[0]); 1003 self.play(soundKey); 1004 } 1005 }).catch(() => { 1006 // No es JSON, ignorar 1007 }); 1008 } 1009 return response; 1010 }); 1011 }; 1012 1013 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1014 // Interceptor de Axios (si está disponible) 1015 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1016 if (window.axios) { 1017 window.axios.interceptors.response.use( 1018 (response) => { 1019 if (this.isSuccessResponse(response.data)) {
1020 const soundKey = this.getSoundKeyFromUrl(response.config.url); 1021 this.play(soundKey); 1022 } 1023 return response; 1024 }, 1025 (error) => { 1026 // Opcionalmente reproducir sonido de error 1027 // this.play('error'); 1028 return Promise.reject(error); 1029 } 1030 ); 1031 } 1032 } 1033 1034 /** 1035 * Verificar si una respuesta indica éxito 1036 * @param {object} data - Datos de la respuesta 1037 */ 1038 isSuccessResponse(data) { 1039 if (!data || typeof data !== 'object') return false; 1040 1041 // Patrones comunes de respuesta exitosa en CashFlow 1042 return ( 1043 data.success === true || 1044 data.status === 'success' || 1045 data.ok === true || 1046 data.created === true || 1047 data.updated === true || 1048 data.deleted === true || 1049 data.saved === true 1050 ); 1051 } 1052 1053 /** 1054 * Determinar sonido basado en URL 1055 * @param {string} url - URL de la petición 1056 */ 1057 getSoundKeyFromUrl(url) { 1058 if (!url) return 'success'; 1059 1060 const urlStr = url.toString().toLowerCase(); 1061 1062 // Detectar rutas del chat IA 1063 const chatPatterns = [ 1064 '/chat', '/ia', '/gemini', '/assistant', 1065 '/ai', '/mensaje', '/message' 1066 ]; 1067 1068 if (chatPatterns.some(pattern => urlStr.includes(pattern))) { 1069 return 'chat'; 1070 } 1071 1072 return 'success'; 1073 } 1074 1075 // ===================================================================== 1076 // DETECCIÃN DE CONTEXTO 1077 // ===================================================================== 1078 1079 /** 1080 * Verificar si un elemento debe ignorarse 1081 * @param {Element} element - Elemento DOM 1082 */ 1083 shouldIgnore(element) { 1084 return this.config.ignoreSelectors.some(selector => 1085 element.matches(selector) || element.closest(selector) 1086 ); 1087 } 1088 1089 /** 1090 * Determinar qué sonido usar para un elemento 1091 * @param {Element} element - Elemento DOM 1092 */ 1093 getSoundKeyForElement(element) { 1094 // 1. Verificar data-sound explÃcito 1095 const explicitSound = element.dataset.sound; 1096 if (explicitSound && this.audioCache.has(explicitSound)) { 1097 return explicitSound; 1098 } 1099 1100 // 2. Verificar si es contexto de chat IA 1101 const isChat = this.config.chatSelectors.some(selector => 1102 element.matches(selector) || element.closest(selector) 1103 ); 1104 1105 if (isChat) return 'chat'; 1106 1107 // 3. Default: success 1108 return 'success'; 1109 } 1110 1111 // ===================================================================== 1112 // EVENTOS PERSONALIZADOS 1113 // ===================================================================== 1114 1115 /** 1116 * Escuchar eventos personalizados para reproducir sonidos 1117 */ 1118 listenCustomEvents() { 1119 // Evento genérico para reproducir sonido 1120 document.addEventListener('cashflow:sound', (e) => { 1121 const soundKey = e.detail?.sound || 'success'; 1122 const options = e.detail?.options || {}; 1123 this.play(soundKey, options); 1124 }); 1125 1126 // Evento de éxito 1127 document.addEventListener('cashflow:success', () => { 1128 this.play('success'); 1129 }); 1130 1131 // Evento de chat 1132 document.addEventListener('cashflow:chat', () => { 1133 this.play('chat'); 1134 }); 1135 1136 // Evento de notificación 1137 document.addEventListener('cashflow:notification', () => { 1138 this.play('pop'); 1139 }); 1140 } 1141 1142 // ===================================================================== 1143 // CONFIGURACIÃN EN TIEMPO DE EJECUCIÃN 1144 // ===================================================================== 1145 1146 /** 1147 * Habilitar/deshabilitar sonidos 1148 * @param {boolean} enabled 1149 */ 1150 setEnabled(enabled) { 1151 this.config.enabled = enabled; 1152 this.savePreferences(); 1153 } 1154 1155 /** 1156 * Cambiar volumen global 1157 * @param {number} volume - Valor entre 0.0 y 1.0 1158 */ 1159 setVolume(volume) { 1160 this.config.volume = Math.max(0, Math.min(1, volume)); 1161 1162 // Actualizar volumen en todos los audios cacheados 1163 this.audioCache.forEach(audio => { 1164 audio.volume = this.config.volume; 1165 }); 1166 1167 this.savePreferences(); 1168 } 1169 1170 /** 1171 * Guardar preferencias en localStorage 1172 */ 1173 savePreferences() { 1174 localStorage.setItem('cashflow_sound_prefs', JSON.stringify({ 1175 enabled: this.config.enabled, 1176 volume: this.config.volume 1177 })); 1178 } 1179 1180 /** 1181 * Toggle sonidos on/off 1182 */ 1183 toggle() { 1184 this.setEnabled(!this.config.enabled); 1185 return this.config.enabled; 1186 } 1187 1188 /** 1189 * Obtener estado actual 1190 */ 1191 getStatus() { 1192 return { 1193 enabled: this.config.enabled, 1194 volume: this.config.volume, 1195 soundsLoaded: this.audioCache.size 1196 }; 1197 } 1198 } 1199 1200 // ========================================================================= 1201 // FUNCIONES HELPER GLOBALES 1202 // ========================================================================= 1203 1204 /** 1205 * Función helper para reproducir sonido de éxito 1206 * Uso: playSuccessSound() 1207 */ 1208 window.playSuccessSound = function(options) { 1209 if (window.CashFlowAudio) { 1210 window.CashFlowAudio.success(options); 1211 } 1212 }; 1213 1214 /** 1215 * Función helper para reproducir sonido de chat 1216 * Uso: playChatSound() 1217 */ 1218 window.playChatSound = function(options) { 1219 if (window.CashFlowAudio) { 1220 window.CashFlowAudio.chat(options); 1221 } 1222 }; 1223 1224 /** 1225 * Función helper para reproducir sonido pop 1226 * Uso: playPopSound() 1227 */ 1228 window.playPopSound = function(options) { 1229 if (window.CashFlowAudio) { 1230 window.CashFlowAudio.pop(options); 1231 } 1232 }; 1233 1234 /**
1235 * Disparar evento de sonido personalizado 1236 * Uso: triggerSound('success') o triggerSound('chat', { volume: 0.8 }) 1237 */ 1238 window.triggerSound = function(soundKey, options = {}) { 1239 document.dispatchEvent(new CustomEvent('cashflow:sound', { 1240 detail: { sound: soundKey, options } 1241 })); 1242 }; 1243 1244 // ========================================================================= 1245 // INICIALIZACIÃN AUTOMÃTICA 1246 // ========================================================================= 1247 1248 // Crear instancia global cuando el DOM esté listo 1249 if (document.readyState === 'loading') { 1250 document.addEventListener('DOMContentLoaded', () => { 1251 window.CashFlowAudio = new CashFlowSounds(); 1252 }); 1253 } else { 1254 window.CashFlowAudio = new CashFlowSounds(); 1255 } 1256 1257 // Exponer clase para uso avanzado 1258 window.CashFlowSounds = CashFlowSounds; 1259 1260})(window); 1261 </script>
1261 1262</body> 1263 1264</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.