2DAM — Proyecto Grupal — 2026

doli-front

SPA Vanilla JS · Vite · Sin frameworks

El frontend que Dolibarr nunca tuvo

Dolibarr tiene todo. Menos una interfaz decente.

El ERP funciona, pero su interfaz tiene 20 años. El objetivo: dar una experiencia moderna sin tocar el backend del ERP.

Antes

Interfaz PHP de 2004
Sin dark mode
Sin búsqueda en tiempo real
Sin notificaciones
Imposible de personalizar

Ahora

SPA moderna con dark mode
Búsqueda y filtros reactivos
Notificaciones Teams / Slack
Asistente de voz integrado
Soporte VeriFactu

Sin React. Sin Vue. Sin Angular.

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.

Control total

Cada elemento del DOM lo creamos nosotros. Sin magia, sin diffing invisible.

Dependencias mínimas

Solo 3: Chart.js (gráficas), @huggingface/transformers (voz) y node-forge (cifrado RSA).

Aprendizaje real

Entendemos qué hace el navegador. Un framework encima de esto es trivial; al revés, no tanto.

Estructura del proyecto

src/ ├── pages/ ← render*() → HTMLElement │ ├── DashboardPage.js │ ├── FacturasPage.js │ └── pagesRegistry.js ← auto-glob ├── components/ ← reutilizables │ ├── InvoiceModal.js │ └── Sidebar.js ├── services/ ← lógica │ ├── apiClient.js ← fetch+JWT │ └── pagesConfig.js ├── styles/ ├── main.js ← entrada └── router.js ← hash routing

Una página = una función

render*() devuelve un HTMLElement.
Estado local en closure.
El router la monta y destruye.

Sin estado global

No hay store ni contexto.
Cada página habla con el BFF
de forma independiente.

Routing sin servidor

El router escucha el hash de la URL. Navegar a #invoices desmonta la página actual y monta la nueva.

// 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);
});

cleanup() evita memory leaks. Si una página tiene un setInterval (ej.: countdown del JWT en Configuración), se cancela al navegar.

Añadir una página es crear un archivo

Con import.meta.glob de Vite, cualquier *Page.js en /pages se registra solo en el sidebar.

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

Páginas explícitas

En pagesConfig.js: ruta, nombre, icono, voice patterns.

Páginas nuevas

Crear MiPaginaPage.js → aparece en el sidebar con ruta #mi-pagina automáticamente.

Lo que puede hacer el usuario

Dashboard

KPIs, gráfica trimestral, últimos movimientos, tabla con colores.

Facturas

CRUD completo. Plantillas, líneas editables, pagos, cambio de estado.

Fact. Proveedores

Gestión de compras. Total por línea en tiempo real.

Terceros

Clientes y proveedores con rol. Contactos anidados.

Banco

Cuentas bancarias y movimientos. Alta con selector de país.

Configuración

Tema, JWT countdown, webhook Teams/Slack, VeriFactu.

Whisper en el navegador

El asistente de voz ejecuta Whisper base (Xenova/whisper-base) directamente en el navegador con @huggingface/transformers. Sin API key, sin servidor de voz.

Micrófono
Web Audio API
Whisper (WASM)
Navegación

"Ir a facturas", "abrir banco", "dashboard" → el router navega sin tocar el teclado. Los patrones de voz se definen en pagesConfig.js junto a la ruta.

Firma de facturas: VeriFactu

Flujo de tres pasos que ocurre en el navegador antes de llegar al servidor.

1 — Certificado

El usuario selecciona su .p12. La FileReader API lo convierte a base64 en el navegador. Nunca toca el disco del servidor.

2 — Contraseña cifrada

El BFF expone una clave pública RSA. El frontend cifra la contraseña con ella. Nunca viaja en claro.

3 — Registro

El BFF reenvía al microservicio Go, que valida el .p12, lo almacena y devuelve un token de sesión.

Una sola función para hablar con el backend

Todo el tráfico HTTP pasa por apiClient.js. JWT automático, 401 redirige al login, errores normalizados.

// 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();
}

Las páginas nunca manejan tokens ni status HTTP. Solo llaman a apiGet(), apiPost()… y reciben datos o un error.

Decisiones que valió la pena tomar

Hash routing sin servidor

Archivos estáticos. Funciona en cualquier CDN sin configurar rutas.

Cleanup en cada página

container.cleanup() cancela intervals al navegar. Sin memory leaks.

Vite + import.meta.glob

HMR en desarrollo. Cada página es un chunk en producción. Registro dinámico.

FileReader + RSA en cliente

Certificado y contraseña procesados en el navegador. Nunca viajan en claro.

Preguntas

doli-front · Vanilla JS · Vite · 2026