333 lines
17 KiB
HTML
333 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>doli-front — Frontend SPA para Dolibarr</title>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/reveal.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/theme/black.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/plugin/highlight/monokai.css">
|
|
<style>
|
|
.reveal h1,.reveal h2,.reveal h3{text-transform:none;letter-spacing:0}
|
|
.reveal section{padding:18px 42px;box-sizing:border-box;overflow:hidden}
|
|
.reveal h1{font-size:1.6em;font-weight:800;margin-bottom:.1em}
|
|
.reveal h2{font-size:1.05em;margin-bottom:.4em;color:#fff;font-weight:700}
|
|
.reveal p{font-size:.76em;line-height:1.5;margin:.3em 0}
|
|
.reveal ul{font-size:.74em;line-height:1.6;margin:.2em 0 .2em 1.2em}
|
|
.reveal pre{width:100%;margin:.4em 0;border-radius:5px}
|
|
.reveal pre code{font-size:.58em;line-height:1.2}
|
|
|
|
body,.reveal{background:#111;color:#eee}
|
|
|
|
.cols{display:flex;gap:12px;align-items:stretch}
|
|
.cols>*{flex:1}
|
|
.center{text-align:center}
|
|
|
|
.card{
|
|
background:#181818;
|
|
border:1px solid #333;
|
|
border-radius:6px;
|
|
padding:10px 14px;
|
|
margin:3px 0;
|
|
}
|
|
|
|
.callout{
|
|
background:#121c14;
|
|
border:1px solid #3fb95044;
|
|
padding:10px 16px;
|
|
border-radius:6px;
|
|
margin:.5em 0;
|
|
}
|
|
.callout p{margin:0;font-size:.73em;line-height:1.45}
|
|
|
|
.flow{display:flex;align-items:center;justify-content:center;gap:12px;margin:.7em 0;font-size:.8em}
|
|
.flow-box{padding:10px 18px;border-radius:6px;font-weight:700;text-align:center;font-size:.9em}
|
|
.flow-arrow{color:#3b82f6;font-size:1.3em;font-weight:800}
|
|
|
|
.sub{font-size:.63em;color:#888;line-height:1.45}
|
|
|
|
.accent{color:#3b82f6}
|
|
.green{color:#4ade80}
|
|
.red{color:#f87171}
|
|
.amber{color:#fbbf24}
|
|
|
|
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:6px 16px}
|
|
.grid1{display:grid;grid-template-columns:1fr;gap:5px}
|
|
|
|
.file-tree{
|
|
font-family:'SF Mono',Consolas,monospace;
|
|
font-size:.44em;line-height:1.6;
|
|
color:#ccc;background:#181818;
|
|
border-radius:6px;padding:10px 14px;
|
|
border:1px solid #333;
|
|
white-space:pre;
|
|
overflow:hidden
|
|
}
|
|
.file-tree .dir{color:#3b82f6;font-weight:700}
|
|
.file-tree .comment{color:#555}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="reveal"><div class="slides">
|
|
|
|
<!-- PORTADA -->
|
|
<section class="center">
|
|
<p style="font-size:.62em;color:#888;margin-bottom:.5em">2DAM — Proyecto Grupal — 2026</p>
|
|
<h1 style="color:#fff">doli-front</h1>
|
|
<p style="font-size:.9em;color:#3b82f6;margin:.2em 0 .4em">SPA Vanilla JS · Vite · Sin frameworks</p>
|
|
<p class="sub" style="margin-top:.8em">El frontend que Dolibarr nunca tuvo</p>
|
|
</section>
|
|
|
|
<!-- EL PROBLEMA -->
|
|
<section>
|
|
<h2>Dolibarr tiene todo. Menos una interfaz decente.</h2>
|
|
<p>El ERP funciona, pero su interfaz tiene 20 años. El objetivo: dar una experiencia moderna sin tocar el backend del ERP.</p>
|
|
<div class="cols" style="margin-top:.7em">
|
|
<div class="card" style="border-top:3px solid #f87171">
|
|
<p style="font-size:.75em;color:#f87171;font-weight:700;margin:0 0 6px">Antes</p>
|
|
<div class="sub" style="line-height:1.75">
|
|
Interfaz PHP de 2004<br>Sin dark mode<br>Sin búsqueda en tiempo real<br>Sin notificaciones<br>Imposible de personalizar
|
|
</div>
|
|
</div>
|
|
<div class="card" style="border-top:3px solid #4ade80">
|
|
<p style="font-size:.75em;color:#4ade80;font-weight:700;margin:0 0 6px">Ahora</p>
|
|
<div class="sub" style="line-height:1.75">
|
|
SPA moderna con dark mode<br>Búsqueda y filtros reactivos<br>Notificaciones Teams / Slack<br>Asistente de voz integrado<br>Soporte VeriFactu
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ¿POR QUÉ VANILLA JS? -->
|
|
<section>
|
|
<h2>Sin React. Sin Vue. Sin Angular.</h2>
|
|
<p>Una SPA no necesita un framework. Necesita un router, componentes y control del DOM. El proyecto tiene ~7.700 líneas de JS puro sin contar dependencias.</p>
|
|
<div class="cols" style="margin-top:.7em">
|
|
<div class="card" style="border-top:3px solid #3b82f6">
|
|
<p style="font-size:.74em;color:#3b82f6;font-weight:700;margin:0 0 3px">Control total</p>
|
|
<p class="sub">Cada elemento del DOM lo creamos nosotros. Sin magia, sin diffing invisible.</p>
|
|
</div>
|
|
<div class="card" style="border-top:3px solid #3b82f6">
|
|
<p style="font-size:.74em;color:#3b82f6;font-weight:700;margin:0 0 3px">Dependencias mínimas</p>
|
|
<p class="sub">Solo 3: Chart.js (gráficas), @huggingface/transformers (voz) y node-forge (cifrado RSA).</p>
|
|
</div>
|
|
<div class="card" style="border-top:3px solid #3b82f6">
|
|
<p style="font-size:.74em;color:#3b82f6;font-weight:700;margin:0 0 3px">Aprendizaje real</p>
|
|
<p class="sub">Entendemos qué hace el navegador. Un framework encima de esto es trivial; al revés, no tanto.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ARQUITECTURA -->
|
|
<section>
|
|
<h2>Estructura del proyecto</h2>
|
|
<div class="cols" style="align-items:flex-start;margin-top:.4em;gap:14px">
|
|
<div class="file-tree"><span class="dir">src/</span>
|
|
├── <span class="dir">pages/</span> <span class="comment">← render*() → HTMLElement</span>
|
|
│ ├── DashboardPage.js
|
|
│ ├── FacturasPage.js
|
|
│ └── pagesRegistry.js <span class="comment">← auto-glob</span>
|
|
├── <span class="dir">components/</span> <span class="comment">← reutilizables</span>
|
|
│ ├── InvoiceModal.js
|
|
│ └── Sidebar.js
|
|
├── <span class="dir">services/</span> <span class="comment">← lógica</span>
|
|
│ ├── apiClient.js <span class="comment">← fetch+JWT</span>
|
|
│ └── pagesConfig.js
|
|
├── <span class="dir">styles/</span>
|
|
├── main.js <span class="comment">← entrada</span>
|
|
└── router.js <span class="comment">← hash routing</span></div>
|
|
<div>
|
|
<div class="card" style="border-top:3px solid #3b82f6;margin-bottom:7px">
|
|
<p style="font-size:.72em;color:#3b82f6;font-weight:700;margin:0 0 3px">Una página = una función</p>
|
|
<p class="sub"><code>render*()</code> devuelve un <code>HTMLElement</code>.<br>Estado local en closure.<br>El router la monta y destruye.</p>
|
|
</div>
|
|
<div class="card" style="border-top:3px solid #4ade80">
|
|
<p style="font-size:.72em;color:#4ade80;font-weight:700;margin:0 0 3px">Sin estado global</p>
|
|
<p class="sub">No hay store ni contexto.<br>Cada página habla con el BFF<br>de forma independiente.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ROUTER -->
|
|
<section>
|
|
<h2>Routing sin servidor</h2>
|
|
<p>El router escucha el hash de la URL. Navegar a <code>#invoices</code> desmonta la página actual y monta la nueva.</p>
|
|
<pre><code class="javascript">// router.js — lo esencial
|
|
window.addEventListener('hashchange', () => {
|
|
const route = location.hash.replace('#', '') || 'dashboard';
|
|
const page = getPageByRoute(route);
|
|
|
|
currentContainer?.cleanup?.(); // cancela timers e intervals
|
|
currentContainer = page.render(); // monta la nueva página
|
|
appRoot.replaceChildren(currentContainer);
|
|
});</code></pre>
|
|
<div class="callout" style="margin-top:.5em">
|
|
<p><code>cleanup()</code> evita memory leaks. Si una página tiene un <code>setInterval</code> (ej.: countdown del JWT en Configuración), se cancela al navegar.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- REGISTRO DINÁMICO -->
|
|
<section>
|
|
<h2>Añadir una página es crear un archivo</h2>
|
|
<p>Con <code>import.meta.glob</code> de Vite, cualquier <code>*Page.js</code> en <code>/pages</code> se registra solo en el sidebar.</p>
|
|
<div class="cols" style="margin-top:.6em;align-items:flex-start">
|
|
<pre><code class="javascript">// pagesRegistry.js
|
|
const modules = import.meta.glob(
|
|
'./*Page.js', { eager: true }
|
|
);
|
|
|
|
// filtra las declaradas en pagesConfig
|
|
// el resto se auto-registran con ruta
|
|
// derivada del nombre del archivo</code></pre>
|
|
<div>
|
|
<div class="card" style="border-top:3px solid #3b82f6;margin-bottom:7px">
|
|
<p style="font-size:.72em;color:#3b82f6;font-weight:700;margin:0 0 3px">Páginas explícitas</p>
|
|
<p class="sub">En <code>pagesConfig.js</code>: ruta, nombre, icono, voice patterns.</p>
|
|
</div>
|
|
<div class="card" style="border-top:3px solid #4ade80">
|
|
<p style="font-size:.72em;color:#4ade80;font-weight:700;margin:0 0 3px">Páginas nuevas</p>
|
|
<p class="sub">Crear <code>MiPaginaPage.js</code> → aparece en el sidebar con ruta <code>#mi-pagina</code> automáticamente.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- PÁGINAS PRINCIPALES -->
|
|
<section>
|
|
<h2>Lo que puede hacer el usuario</h2>
|
|
<div class="grid2" style="margin-top:.35em;gap:5px 12px">
|
|
<div class="card" style="border-left:3px solid #3b82f6;padding:7px 12px">
|
|
<p style="font-size:.65em;color:#3b82f6;font-weight:700;margin:0 0 2px">Dashboard</p>
|
|
<p style="font-size:.55em;color:#888;line-height:1.4;margin:0">KPIs, gráfica trimestral, últimos movimientos, tabla con colores.</p>
|
|
</div>
|
|
<div class="card" style="border-left:3px solid #3b82f6;padding:7px 12px">
|
|
<p style="font-size:.65em;color:#3b82f6;font-weight:700;margin:0 0 2px">Facturas</p>
|
|
<p style="font-size:.55em;color:#888;line-height:1.4;margin:0">CRUD completo. Plantillas, líneas editables, pagos, cambio de estado.</p>
|
|
</div>
|
|
<div class="card" style="border-left:3px solid #3b82f6;padding:7px 12px">
|
|
<p style="font-size:.65em;color:#3b82f6;font-weight:700;margin:0 0 2px">Fact. Proveedores</p>
|
|
<p style="font-size:.55em;color:#888;line-height:1.4;margin:0">Gestión de compras. Total por línea en tiempo real.</p>
|
|
</div>
|
|
<div class="card" style="border-left:3px solid #3b82f6;padding:7px 12px">
|
|
<p style="font-size:.65em;color:#3b82f6;font-weight:700;margin:0 0 2px">Terceros</p>
|
|
<p style="font-size:.55em;color:#888;line-height:1.4;margin:0">Clientes y proveedores con rol. Contactos anidados.</p>
|
|
</div>
|
|
<div class="card" style="border-left:3px solid #3b82f6;padding:7px 12px">
|
|
<p style="font-size:.65em;color:#3b82f6;font-weight:700;margin:0 0 2px">Banco</p>
|
|
<p style="font-size:.55em;color:#888;line-height:1.4;margin:0">Cuentas bancarias y movimientos. Alta con selector de país.</p>
|
|
</div>
|
|
<div class="card" style="border-left:3px solid #3b82f6;padding:7px 12px">
|
|
<p style="font-size:.65em;color:#3b82f6;font-weight:700;margin:0 0 2px">Configuración</p>
|
|
<p style="font-size:.55em;color:#888;line-height:1.4;margin:0">Tema, JWT countdown, webhook Teams/Slack, VeriFactu.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ASISTENTE DE VOZ -->
|
|
<section>
|
|
<h2>Whisper en el navegador</h2>
|
|
<p>El asistente de voz ejecuta <strong>Whisper base</strong> (<code>Xenova/whisper-base</code>) directamente en el navegador con <code>@huggingface/transformers</code>. Sin API key, sin servidor de voz.</p>
|
|
<div class="flow" style="margin:.8em 0">
|
|
<div class="flow-box" style="background:#1a1a2e;color:#a78bfa;border:2px solid #a78bfa88">Micrófono</div>
|
|
<span class="flow-arrow">⟶</span>
|
|
<div class="flow-box" style="background:#1a1a2e;color:#a78bfa;border:2px solid #a78bfa88">Web Audio API</div>
|
|
<span class="flow-arrow">⟶</span>
|
|
<div class="flow-box" style="background:#1a1a2e;color:#a78bfa;border:2px solid #a78bfa88">Whisper (WASM)</div>
|
|
<span class="flow-arrow">⟶</span>
|
|
<div class="flow-box" style="background:#1a2e1a;color:#4ade80;border:2px solid #4ade8088">Navegación</div>
|
|
</div>
|
|
<div class="callout">
|
|
<p>"Ir a facturas", "abrir banco", "dashboard" → el router navega sin tocar el teclado. Los patrones de voz se definen en <code>pagesConfig.js</code> junto a la ruta.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- VERIFACTU -->
|
|
<section>
|
|
<h2>Firma de facturas: VeriFactu</h2>
|
|
<p>Flujo de tres pasos que ocurre en el navegador antes de llegar al servidor.</p>
|
|
<div class="cols" style="margin-top:.6em">
|
|
<div class="card" style="border-top:3px solid #fbbf24">
|
|
<p style="font-size:.72em;color:#fbbf24;font-weight:700;margin:0 0 3px">1 — Certificado</p>
|
|
<p class="sub">El usuario selecciona su <code>.p12</code>. La <strong>FileReader API</strong> lo convierte a base64 en el navegador. Nunca toca el disco del servidor.</p>
|
|
</div>
|
|
<div class="card" style="border-top:3px solid #fbbf24">
|
|
<p style="font-size:.72em;color:#fbbf24;font-weight:700;margin:0 0 3px">2 — Contraseña cifrada</p>
|
|
<p class="sub">El BFF expone una clave pública RSA. El frontend cifra la contraseña con ella. Nunca viaja en claro.</p>
|
|
</div>
|
|
<div class="card" style="border-top:3px solid #4ade80">
|
|
<p style="font-size:.72em;color:#4ade80;font-weight:700;margin:0 0 3px">3 — Registro</p>
|
|
<p class="sub">El BFF reenvía al microservicio Go, que valida el <code>.p12</code>, lo almacena y devuelve un token de sesión.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- APICLIENT + JWT -->
|
|
<section>
|
|
<h2>Una sola función para hablar con el backend</h2>
|
|
<p>Todo el tráfico HTTP pasa por <code>apiClient.js</code>. JWT automático, 401 redirige al login, errores normalizados.</p>
|
|
<pre><code class="javascript">// services/apiClient.js
|
|
export async function apiGet(endpoint) {
|
|
const token = localStorage.getItem('token');
|
|
const res = await fetch(BASE_URL + endpoint, {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
});
|
|
if (res.status === 401) { navigate('login'); return; }
|
|
if (!res.ok) throw new Error(await res.text());
|
|
return res.json();
|
|
}</code></pre>
|
|
<div class="callout" style="margin-top:.5em">
|
|
<p>Las páginas nunca manejan tokens ni status HTTP. Solo llaman a <code>apiGet()</code>, <code>apiPost()</code>… y reciben datos o un error.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- DECISIONES TÉCNICAS -->
|
|
<section>
|
|
<h2>Decisiones que valió la pena tomar</h2>
|
|
<div class="grid2" style="margin-top:.35em;gap:5px 12px">
|
|
<div class="card" style="border-left:3px solid #3b82f6;padding:7px 12px">
|
|
<p style="font-size:.65em;color:#3b82f6;font-weight:700;margin:0 0 2px">Hash routing sin servidor</p>
|
|
<p style="font-size:.55em;color:#888;line-height:1.4;margin:0">Archivos estáticos. Funciona en cualquier CDN sin configurar rutas.</p>
|
|
</div>
|
|
<div class="card" style="border-left:3px solid #4ade80;padding:7px 12px">
|
|
<p style="font-size:.65em;color:#4ade80;font-weight:700;margin:0 0 2px">Cleanup en cada página</p>
|
|
<p style="font-size:.55em;color:#888;line-height:1.4;margin:0"><code>container.cleanup()</code> cancela intervals al navegar. Sin memory leaks.</p>
|
|
</div>
|
|
<div class="card" style="border-left:3px solid #a78bfa;padding:7px 12px">
|
|
<p style="font-size:.65em;color:#a78bfa;font-weight:700;margin:0 0 2px">Vite + import.meta.glob</p>
|
|
<p style="font-size:.55em;color:#888;line-height:1.4;margin:0">HMR en desarrollo. Cada página es un chunk en producción. Registro dinámico.</p>
|
|
</div>
|
|
<div class="card" style="border-left:3px solid #fbbf24;padding:7px 12px">
|
|
<p style="font-size:.65em;color:#fbbf24;font-weight:700;margin:0 0 2px">FileReader + RSA en cliente</p>
|
|
<p style="font-size:.55em;color:#888;line-height:1.4;margin:0">Certificado y contraseña procesados en el navegador. Nunca viajan en claro.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- CIERRE -->
|
|
<section class="center">
|
|
<h1 style="font-size:2em">Preguntas</h1>
|
|
<p style="margin-top:.8em;color:#555">doli-front · Vanilla JS · Vite · 2026</p>
|
|
</section>
|
|
|
|
</div></div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/reveal.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/plugin/highlight/highlight.js"></script>
|
|
<script>
|
|
Reveal.initialize({
|
|
hash: true,
|
|
slideNumber: 'c/t',
|
|
transition: 'fade',
|
|
transitionSpeed: 'fast',
|
|
plugins: [RevealHighlight],
|
|
width: 1100,
|
|
height: 680,
|
|
margin: 0.04,
|
|
center: false,
|
|
disableLayout: false,
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|