ProyectoGrupal/doli-front/presentacion/index.html

333 lines
17 KiB
HTML
Raw Permalink Normal View History

<!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>