13.1 📊 Estado visual actual

El sistema tiene una identidad visual consistente y bien lograda:

  • ✅ Paleta dark/light unificada con clave za-theme en localStorage.
  • ✅ Fuente Outfit (300-800) en todos los módulos.
  • ✅ Header sticky con glass effect (backdrop-filter).
  • ✅ Cards con sombras consistentes (--shadow-sm/md/lg).
  • ✅ Border-radius consistente (8/14 px).
  • ✅ Anti-flash de tema (script en primera línea del head).
  • ✅ Iconografía emoji (sin dependencia de icon font).

Oportunidades de mejora

  • ⚠️ Cada módulo redefine variables CSS — riesgo de divergencia.
  • ⚠️ No hay design tokens formalizados (espaciado, radius, sombras).
  • ⚠️ Componentes (cards, badges, callouts) repetidos sin abstracción.
  • ⚠️ Sin librería de componentes documentada (style guide).
  • ⚠️ Empty/loading/error states inconsistentes entre módulos.

13.2 🎨 Design system unificado

Tokens propuestos

/* /reportes/_shared/css/tokens.css */
:root {
  /* SPACING (4px base) */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;

  /* RADIUS */
  --radius-xs: 4px;
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-xl: 24px;
  --radius-full: 9999px;

  /* SHADOWS */
  --shadow-xs: 0 1px 2px rgba(0,0,0,.05);
  --shadow-sm: 0 2px 4px rgba(0,0,0,.04);
  --shadow-md: 0 10px 25px -5px rgba(0,0,0,.08);
  --shadow-lg: 0 20px 40px -12px rgba(0,0,0,.12);
  --shadow-xl: 0 30px 60px -20px rgba(0,0,0,.15);

  /* COLORS (semantic) */
  --c-ok:     #22c55e;
  --c-warn:   #f59e0b;
  --c-err:    #ef4444;
  --c-info:   #38bdf8;
  --c-violet: #a855f7;
  --c-accent: #3b82f6;

  /* DURATION */
  --d-fast:   150ms;
  --d-base:   250ms;
  --d-slow:   400ms;

  /* EASING */
  --e-out:    cubic-bezier(.165, .84, .44, 1);
  --e-bounce: cubic-bezier(.34, 1.56, .64, 1);
}

Componentes propuestos

/reportes/_shared/css/components/
├── _buttons.css       ← .btn, .btn-primary, .btn-secondary, sizes
├── _cards.css         ← .card, .card-header, .card-body
├── _badges.css        ← .badge-ok, .badge-warn, .badge-err
├── _kpis.css          ← .kpi, .kpi.ok, .kpi.warn
├── _callouts.css      ← .callout, .callout-info, etc
├── _tables.css        ← reset + estilos base
├── _forms.css         ← inputs, selects, switches
├── _modals.css        ← overlay + content + close
├── _tooltips.css      ← .tooltip con flecha CSS
├── _skeletons.css     ← shimmer animations
└── _theme.css         ← dark/light tokens

Style guide interactivo

// /reportes/_shared/styleguide.html
// Una página con todos los componentes en uso, lado a lado
// Para que cualquier dev nuevo vea "así se ve un .btn-primary"
// Inspirado en Storybook (versión simple, sin build)

13.3 📭 Empty states consistentes

Hoy, cuando un período no tiene datos, se ve "0,00" o tablas vacías. Mejora: componente <EmptyState> con ícono + mensaje + CTA.

Patrón propuesto

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│                          📭                                  │
│                                                              │
│           Aún no hay ventas para este período                │
│                                                              │
│      Sincronizá las ventas desde ChessERP para ver           │
│            KPIs, márgenes y rentabilidad.                    │
│                                                              │
│              [⚡ Sincronizar ahora]                          │
│                                                              │
└──────────────────────────────────────────────────────────────┘

HTML/CSS

<div class="empty-state">
  <div class="empty-icon">📭</div>
  <h3>Aún no hay ventas para este período</h3>
  <p>Sincronizá las ventas desde ChessERP para ver KPIs, márgenes y rentabilidad.</p>
  <a href="../sync_api.php?mes=<?=$mes?>&anio=<?=$anio?>" class="btn btn-primary">
    ⚡ Sincronizar ahora
  </a>
</div>
.empty-state {
  text-align: center;
  padding: var(--space-12) var(--space-6);
  color: var(--text-muted);
}
.empty-state .empty-icon {
  font-size: 4rem;
  margin-bottom: var(--space-4);
  opacity: .6;
}
.empty-state h3 {
  font-size: 1.15rem;
  color: var(--text-strong);
  margin-bottom: var(--space-2);
}
.empty-state p {
  max-width: 40ch;
  margin: 0 auto var(--space-6);
  font-size: .92rem;
}

Variantes por contexto

ContextoÍconoMensaje sugerido
Sin períodos cargados📭"Aún no hay datos. Sincronizá tu primer mes para empezar."
Sin resultados en filtro🔍"No encontramos coincidencias. Probá con otros filtros."
Sin permisos🔒"No tenés permiso para ver este reporte. Hablalo con un admin."
Error de carga⚠️"No pudimos cargar los datos. [Reintentar]"
Catálogo vacío📦"El catálogo de artículos está vacío. Subí el Excel ahora."
Sin cheques en cartera🧾"No hay cheques registrados. Subí la cartera de cheques."

13.4 ⏳ Loading states con skeletons

Hoy: spinners genéricos o nada. Mejora: skeletons que respetan el layout del componente final.

Patrón skeleton

.skeleton {
  background: linear-gradient(
    90deg,
    var(--bg-card-alt) 0%,
    var(--accent-soft) 50%,
    var(--bg-card-alt) 100%
  );
  background-size: 200% 100%;
  animation: shimmer 1.4s ease-in-out infinite;
  border-radius: var(--radius-sm);
}
@keyframes shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* Variantes por uso */
.skeleton-text-sm   { height: 12px; width: 60%; }
.skeleton-text-lg   { height: 24px; width: 80%; }
.skeleton-kpi-value { height: 36px; width: 70%; margin: 8px 0; }
.skeleton-row       { height: 40px; width: 100%; margin-bottom: 6px; }
.skeleton-circle    { width: 40px; height: 40px; border-radius: 50%; }

Skeleton de KPI card

┌────────────────────────────┐
│ ░░░░░░░░░░░░░░░░░          │   ← label skeleton
│                            │
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓              │   ← value skeleton (shimmer)
│                            │
│ ░░░░░░░░░░░░░░░░░░░░░░░    │   ← delta skeleton
└────────────────────────────┘

13.5 💬 Feedback visual (toasts y alerts)

Hoy: alert() nativos o errores silentes. Mejora: sistema de toasts no bloqueantes.

Toast component

// /reportes/_shared/js/toast.js
const Toast = {
  show(msg, type = 'info', duration = 4000) {
    const el = document.createElement('div');
    el.className = `toast toast-${type}`;
    el.innerHTML = `<span>${this.icon(type)}</span> ${msg}`;
    document.body.appendChild(el);
    setTimeout(() => el.classList.add('show'), 10);
    setTimeout(() => {
      el.classList.remove('show');
      setTimeout(() => el.remove(), 300);
    }, duration);
  },
  ok(msg)   { this.show(msg, 'ok'); },
  warn(msg) { this.show(msg, 'warn'); },
  err(msg)  { this.show(msg, 'err', 6000); },
  info(msg) { this.show(msg, 'info'); },
  icon(t) {
    return {ok:'✅', warn:'⚠️', err:'❌', info:'💡'}[t] || '';
  }
};

// Uso:
Toast.ok('Sincronización completada: 22.456 filas insertadas');
Toast.err('Error de conexión con ChessERP. Reintentando…');
.toast {
  position: fixed;
  top: 1.5rem;
  right: 1.5rem;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-left: 4px solid;
  border-radius: var(--radius-sm);
  padding: 1rem 1.5rem;
  box-shadow: var(--shadow-lg);
  display: flex;
  gap: .75rem;
  align-items: center;
  transform: translateX(120%);
  transition: transform .3s var(--e-out);
  z-index: 9999;
  max-width: 420px;
}
.toast.show { transform: translateX(0); }
.toast-ok   { border-left-color: var(--c-ok); }
.toast-warn { border-left-color: var(--c-warn); }
.toast-err  { border-left-color: var(--c-err); }
.toast-info { border-left-color: var(--c-info); }

13.6 📈 Gráficos mejorados

Chart.js — tweaks para mejor lectura

// Config global para todos los charts
Chart.defaults.font.family = "'Outfit', sans-serif";
Chart.defaults.font.size = 12;
Chart.defaults.color = getCSS('--text-muted');
Chart.defaults.borderColor = getCSS('--border');

Chart.defaults.plugins.tooltip = {
  backgroundColor: getCSS('--bg-card'),
  titleColor:    getCSS('--text-strong'),
  bodyColor:     getCSS('--text-main'),
  borderColor:   getCSS('--border'),
  borderWidth:   1,
  padding:       12,
  cornerRadius:  8,
  displayColors: true,
  boxPadding:    4,
  titleFont:     { weight: 700, size: 13 },
  bodyFont:      { size: 12 },
  // Formato de números argentinos
  callbacks: {
    label(ctx) {
      const v = ctx.parsed.y;
      const fmt = new Intl.NumberFormat('es-AR', {
        style: 'currency', currency: 'ARS', maximumFractionDigits: 0
      });
      return `${ctx.dataset.label}: ${fmt.format(v)}`;
    }
  }
};

Anotaciones (chartjs-plugin-annotation)

Marcar eventos importantes en el chart de "Evolución 12 meses":

// Anotaciones útiles:
// · Línea horizontal: promedio histórico
// · Banda: rango ±1 stddev
// · Línea vertical: cambio de gerente / promo importante
// · Punto: anomalía detectada (z-score >2)

plugins: {
  annotation: {
    annotations: {
      promedio: {
        type: 'line',
        scaleID: 'y',
        value: ventasPromedio,
        borderColor: getCSS('--c-info'),
        borderDash: [6, 6],
        label: { content: 'Promedio 12m', enabled: true, position: 'end' }
      },
      promo: {
        type: 'line',
        scaleID: 'x',
        value: 'Ene 26',
        borderColor: getCSS('--c-violet'),
        label: { content: '🎁 Promo Año Nuevo', enabled: true }
      }
    }
  }
}

Sparklines en KPI cards

Cada KPI numérico podría tener mini-chart (~80×30px) con tendencia 12 meses:

┌────────────────────────────┐
│ VENTAS NETAS               │
│                            │
│ $ 591.307.925              │
│                            │
│      ╱╲    ╱╲              │  ← sparkline
│  ╲  ╱  ╲  ╱  ╲   ╱         │
│   ╲╱    ╲╱    ╲ ╱          │
│                ╲╱           │
│ +12.3 % vs anterior         │
└────────────────────────────┘

Heatmaps para patrones temporales

Útil para:

  • Gastos por mes × rubro (cuándo se concentra cada gasto).
  • Ventas por día de semana × mes (estacionalidad fina).
  • Cheques recibidos por día × cliente (concentración).

13.7 📋 Informes mejorados

1 · Tarjetas KPI con tendencia y meta

┌───────────────────────────────────────────────────────┐
│ 💰 MARGEN GENERAL                                      │
│                                                       │
│ 20,34 %      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━░░░░       │
│              (barra de progreso vs meta 25%)          │
│                                                       │
│ ↑ +1,2 pp vs Mar 26                                   │
│ ⚠ -4,7 pp vs meta (25 %)                              │
│                                                       │
│ ▁▂▃▅▆▆▅▄▃▄▅▆ (12 meses)                              │
└───────────────────────────────────────────────────────┘

2 · Drill-down universal con breadcrumbs

// Al hacer drill-down desde el dashboard:
// breadcrumb visual: "Resumen / Supervisor: LAR | A. SANCHEZ / Top proveedores"
// con flechas ← clicables para volver atrás

3 · Comparativa lado a lado

┌─────────────────────────────────────────────────────────────┐
│ Comparar:  [Abril 2026 ▼]   vs   [Marzo 2026 ▼]             │
├─────────────────────────────────────────────────────────────┤
│                          │   ABR 2026   │   MAR 2026  │ Δ   │
│ Vta                      │ $591.3 M ━━━│ $564.5 M ━━ │+4.7%│
│ Margen                   │ 20.34 %     │ 19.12 %     │+1.2 │
│ Comprobantes             │ 22.546      │ 21.864      │+3.1%│
│ Clientes únicos          │ 1.234       │ 1.198       │+3.0%│
│ Bultos                   │ 14.230      │ 13.890      │+2.4%│
│ Ticket promedio          │ $26.224     │ $25.821     │+1.6%│
└─────────────────────────────────────────────────────────────┘

4 · Mensajes en lenguaje natural

// En vez de tabla fría, generar "insights" automáticos:

📊 Resumen del mes:
   Abril 2026 vendió $591.3M (+4.7% vs Marzo).
   El margen mejoró a 20.34% (+1.2pp) gracias a:
     · Reducción de gastos logísticos en SGO (-12%)
     · Recuperación del proveedor PURINA (+18% margen)

⚠️ Atención:
   El supervisor LAR | A. SANCHEZ tuvo el peor desempeño:
   ventas -8% vs su promedio. Sugerencia: revisar zona.

✅ Mejores resultados:
   Cliente nuevo "Veterinaria Del Sur" generó $1.2M con margen 28%.
   Es candidato a aumentar línea de crédito (sugerencia: $2M).

5 · Filtros guardables como "vistas"

┌─ Mis Vistas ─────────────────────────────────────────┐
│ ★ Cierre mensual ZA (actual sync + ZA)               │
│ ★ Top deudores +60d (snapshot reciente)              │
│ ★ Combustible 2026 (rubro filtrado año)              │
│ ★ Performance cobranza JM (vendedor)                  │
│                                                       │
│ [+ Guardar vista actual]                              │
└──────────────────────────────────────────────────────┘

13.8 ♿ Accesibilidad

Quick wins

  • ARIA labels en todos los botones-ícono (theme toggle, fullscreen, close).
  • Contraste WCAG AA: verificar con axe-core. La paleta dark actual cumple en general.
  • Focus visible: outline azul claro en cada elemento focuseable.
  • Skip link: "Saltar al contenido principal" para teclado.
  • Keyboard navigation: Tab debe llegar a cada control.
  • Semántica: usar <nav>, <main>, <aside>, <article> en vez de divs.

Tablas accesibles

<table>
  <caption>Top 10 proveedores por ventas</caption>
  <thead>
    <tr>
      <th scope="col">Proveedor</th>
      <th scope="col">Ventas</th>
      <th scope="col">Margen</th>
    </tr>
  </thead>
  ...
</table>

Reducir motion para usuarios sensibles

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

13.9 ✨ Microinteracciones

  • Hover en filas Tabulator: ya existe, mejorar con leve elevation.
  • Click feedback: ripple sutil al clicar botones (200ms).
  • Tooltip en KPIs: "Click para ver detalle por entidad".
  • Animar números: countUp.js para que KPIs cuenten al cargar.
  • Skeleton → real: fade-in suave (300ms) cuando llega el dato.
  • Tab switching: ya tiene transición; agregar slide horizontal sutil.
  • Drag para reordenar: cards de KPI reordenables y persistir orden.
  • Modo presentación: F11 maximiza dashboard, oculta sidebar (para reuniones).

13.10 🖨️ Print-friendly

Para imprimir reportes en papel o exportar como PDF nativo del browser:

@media print {
  /* Quitar elementos no esenciales */
  .site-header, .sidebar, .tab-buttons,
  .filter-card, .back-top, button { display: none !important; }

  /* Expandir todo */
  .main { grid-template-columns: 1fr !important; padding: 0 !important; }
  .content { max-width: 100% !important; }

  /* Páginas A4 */
  @page {
    size: A4;
    margin: 1.5cm;
  }

  /* Forzar page break antes de cada sección grande */
  .section { page-break-inside: avoid; }
  h2 { page-break-after: avoid; }

  /* Color print */
  body { background: white !important; color: black !important; }
  .kpi, .card { box-shadow: none; border: 1px solid #ccc; }
  pre  { border: 1px solid #ccc; page-break-inside: avoid; }

  /* Mostrar URL al lado de links */
  a[href^="http"]:after { content: " (" attr(href) ")"; font-size: .8em; }
}

Botón "Imprimir reporte"

En cada dashboard, botón visible que ejecuta window.print() + sugerencia "Guardar como PDF" en el diálogo.