D1 πŸ”„ Sync mensual de ventas (lote Γ— lote)

Hostinger mata HTTP largas a los ~4 minutos β†’ AJAX lote Γ— lote orquestado desde el browser.

 Usuario        Browser (sync_api.php)     Server (sync_api_process.php)        ChessERP
   β”‚                       β”‚                              β”‚                              β”‚
   β”‚ click "Sync"          β”‚                              β”‚                              β”‚
   β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚                              β”‚                              β”‚
   β”‚                       β”‚ POST action=init             β”‚                              β”‚
   β”‚                       β”‚ {mes, anio}                  β”‚                              β”‚
   β”‚                       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚                              β”‚
   β”‚                       β”‚                              β”‚ DELETE sync previo           β”‚
   β”‚                       β”‚                              β”‚ INSERT sincronizaciones      β”‚
   β”‚                       β”‚                              β”‚   estado='procesando'        β”‚
   β”‚                       │◄────────────────────────────── {ok, sync_id}                β”‚
   β”‚                       β”‚                              β”‚                              β”‚
   β”‚                       β”‚ ╔═══════ LOOP lote=1..N ═════╗                              β”‚
   β”‚                       β”‚ β•‘                            β•‘                              β”‚
   β”‚                       β”‚ β•‘ POST action=lote           β•‘                              β”‚
   β”‚                       β”‚ β•‘ {sync_id, lote, mes, anio} β•‘                              β”‚
   β”‚                       β”‚ ║───────────────────────────►║                              β”‚
   β”‚                       β”‚ β•‘                            β•‘ login() (1er lote)           β”‚
   β”‚                       β”‚ β•‘                            ║──────────────────────────────►│
   β”‚                       β”‚ β•‘                            ║◄─── sessionId ────────────────
   β”‚                       β”‚ β•‘                            β•‘ GET /ventas?nroLote=N        β”‚
   β”‚                       β”‚ β•‘                            ║──────────────────────────────►│
   β”‚                       β”‚ β•‘                            ║◄─── rows[] (200-2000) ────────
   β”‚                       β”‚ β•‘                            β•‘ INSERT ventas (prepared)    β”‚
   β”‚                       β”‚ ║◄───────────────────────────║ {ok, filas, finalizado}      β”‚
   β”‚                       β”‚ β•‘                            β•‘                              β”‚
   β”‚                       β”‚ β•‘ progress bar update        β•‘                              β”‚
   β”‚                       β”‚ β•‘ si finalizado==false β†’     β•‘                              β”‚
   β”‚                       β”‚ β•‘   lote++ y repite          β•‘                              β”‚
   β”‚                       β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•                              β”‚
   β”‚                       β”‚                              β”‚                              β”‚
   β”‚                       β”‚ POST action=finalizar        β”‚                              β”‚
   β”‚                       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚                              β”‚
   β”‚                       β”‚                              β”‚ UPDATE estado='completado'   β”‚
   β”‚                       β”‚                              β”‚       fecha_fin=NOW()        β”‚
   β”‚                       │◄────────────────────────────── {ok}                         β”‚
   β”‚ βœ… "Mes cargado"      β”‚                              β”‚                              β”‚
   │◄───────────────────────                              β”‚                              β”‚

Tiempo tΓ­pico: 25-40 segundos para ~20K filas (3-8 lotes).

D2 ⛓️ Pipeline 3-tier de clasificaciΓ³n (caja gastos)

   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  ENTRADA: 6 archivos Excel del ERP                              β”‚
   β”‚  ────────────────────────────────────────                       β”‚
   β”‚   1. EgresosCaja.xlsx                                           β”‚
   β”‚   2. ConceptosDeGastos.xlsx        (catΓ‘logo persistente)       β”‚
   β”‚   3. EgresosDetalle.xlsx           ← FUENTE de los gastos       β”‚
   β”‚   4. GastosTipificados.xlsx        ← Nivel 1 PRIMARIA           β”‚
   β”‚   5. MovimientoDetalle.xlsx        ← Nivel 2 FALLBACK           β”‚
   β”‚   6. MovimientoResumido.xlsx       (opcional, para IVA)         β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                        β”‚
                                        β”‚ SheetJS parsea cliente
                                        β”‚ Filtro por operaciΓ³n:
                                        β”‚   'compras / gastos - caja' Γ³
                                        β”‚   'gastos - liquidaciones'
                                        β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  Por cada fila de EgresosDetalle filtrada:                      β”‚
   β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
   β”‚   β”‚  Extrae nΒΊ ME de col "Observaciones" via regex:  β”‚          β”‚
   β”‚   β”‚    /ME\s+0*(\d+)-0*(\d+)/i                       β”‚          β”‚
   β”‚   β”‚  β†’ (serie, numero, empresa)                      β”‚          β”‚
   β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β–Ό
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚  ΒΏMatch en gastosMap?  β”‚
                β”‚  key = (s, n, e)       β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                       SI         NO
                        β”‚         β”‚
                        β–Ό         β–Ό
      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
      β”‚ NIVEL 1: GT          β”‚   β”‚  ΒΏMatch en movDetMap?β”‚
      β”‚ rubro_id  = GT.rubro β”‚   β”‚  key = (s, n, e)     β”‚
      β”‚ concepto  = GT.conc  β”‚   β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       SI          NO
                 β”‚                    β”‚           β”‚
                 β”‚                    β–Ό           β–Ό
                 β”‚     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                 β”‚     β”‚ NIVEL 2: MD + Conc   β”‚  β”‚ NIVEL 3: SIN CLASIFICAR  β”‚
                 β”‚     β”‚ MD β†’ concepto_id     β”‚  β”‚ rubro = 'SIN CLASIFICAR' β”‚
                 β”‚     β”‚ Conc[id] β†’ rubro     β”‚  β”‚ concepto_libre = obs[:200]β”‚
                 β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β”‚                β”‚                              β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                 β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  Mapeo de provincia                                             β”‚
   β”‚  cajero = cajaMap[`${empresa}-${cajaNro}`]                      β”‚
   β”‚  provincia = CAJERO_PROV[cajero.toLowerCase().trim()] || 'OTRAS'β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                 β”‚
                                                 β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  INSERT caja_gastos (sync_id, mes, anio, provincia,             β”‚
   β”‚                      rubro_id, rubro, concepto_id,              β”‚
   β”‚                      concepto_libre, tipo_comprobante,          β”‚
   β”‚                      serie, numero, empresa, caja_nro,          β”‚
   β”‚                      cajero, fecha, monto)                      β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Bug histΓ³rico fix v2.0.2: el parser leΓ­a col 6 (Tipo) en vez de col 5 (Comprobante). Ver Β§10 hallazgo CAJ-bug-1.

D3 πŸ“¦ Sync bulk multi-mes (caja)

   Usuario sube N archivos (cada uno con M meses)
   ────────────────────────────────────────────────
        β”‚
        β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  SheetJS parsea TODOS los archivos                         β”‚
   β”‚  β†’ cajaRowsGlobal, detalleRowsGlobal, tipificadosRowsGlobalβ”‚
   β”‚    movDetalleRowsGlobal, movResumidoRowsGlobal,            β”‚
   β”‚    conceptosRowsGlobal                                     β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                                β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  JS escanea fechas β†’ detecta meses ΓΊnicos                  β”‚
   β”‚  meses = [{mes:1, anio:2025}, {mes:2, anio:2025}, ...]     β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                                β–Ό
              ╔═════════════════════════════════════╗
              β•‘  Para cada mes en la cola:          β•‘
              β•‘                                     β•‘
              β•‘  1. POST init.php β†’ sync_id         β•‘
              β•‘                                     β•‘
              β•‘  2. Filtra cada Excel por fecha     β•‘
              β•‘     que coincida con (mes, anio)    β•‘
              β•‘                                     β•‘
              β•‘  3. Ejecuta pipeline 3-tier         β•‘
              β•‘     sobre las filas filtradas       β•‘
              β•‘                                     β•‘
              β•‘  4. POST save_raw.php Γ— 5-6 tablas  β•‘
              β•‘     (linkeadas a este sync_id)      β•‘
              β•‘                                     β•‘
              β•‘  5. POST save.php Γ— N (caja_gastos) β•‘
              β•‘                                     β•‘
              β•‘  6. POST save.php {finalizar:true}  β•‘
              β•‘     β†’ caja_rebuild_cajeros()        β•‘
              β•‘     β†’ caja_audit_log()              β•‘
              β•‘                                     β•‘
              β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
                                β”‚
                                β–Ό
                        βœ… Todos los meses
                           self-contained en BD

Antes de v2.0.1: los archivos compartidos quedaban con el sync_id del primer mes. Eliminar ese mes borraba la raw histΓ³rica de todos los demΓ‘s.

D4 πŸ’Έ Cashflow data.php (composiciΓ³n del JSON)

   GET /reportes/cashflow/api/data.php?corte=2026-05-20[&sucursal=...]
        β”‚
        β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  1) Validar corte (regex YYYY-MM-DD) o usar MAX(fecha_corte)     β”‚
   β”‚  2) cashflow_ensure_schema()                                     β”‚
   β”‚  3) Listar sucursales disponibles (para selector)                β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Bloque         β–Ό                                            β”‚
   β”‚ CLIENTES       cartera_clientes                             β”‚
   β”‚                                                             β”‚
   β”‚  β”œβ”€ KPI:     COUNT, SUM(saldo_total), SUM(d7/d15/d30/d60)   β”‚
   β”‚  β”œβ”€ Aging:   8 tramos del antiguedad_dias                   β”‚
   β”‚  β”œβ”€ Top 50 deudores                                         β”‚
   β”‚  β”œβ”€ Por vendedor                                            β”‚
   β”‚  └─ Por provincia                                           β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Bloque         β–Ό                                            β”‚
   β”‚ PROVEEDORES    cartera_proveedores                          β”‚
   β”‚                                                             β”‚
   β”‚  β”œβ”€ KPI:     a_pagar, vencido, anticipos                    β”‚
   β”‚  β”œβ”€ Aging por dias_atraso                                   β”‚
   β”‚  β”œβ”€ Top proveedores                                         β”‚
   β”‚  β”œβ”€ Pagos futuros (prΓ³ximos vencimientos)                   β”‚
   β”‚  └─ Vencidos (ya vencidos)                                  β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Bloque         β–Ό                                            β”‚
   β”‚ CHEQUES        cheques_cartera                              β”‚
   β”‚                                                             β”‚
   β”‚  β”œβ”€ KPI:    n_cartera, monto_cartera, monto_vencidos,       β”‚
   β”‚  β”‚          monto_endosados, monto_rechazados               β”‚
   β”‚  β”œβ”€ Lista completa con dias_a_presentar                     β”‚
   β”‚  β”œβ”€ Por banco                                               β”‚
   β”‚  └─ Por cliente emisor                                      β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Bloque         β–Ό                                            β”‚
   β”‚ CRUCE COMERCIAL                                             β”‚
   β”‚                                                             β”‚
   β”‚  Para cada cliente con saldo > 0:                          β”‚
   β”‚   β”œβ”€ GROUP BY idCliente en ventas (ΓΊltimos 6 syncs)         β”‚
   β”‚   β”œβ”€ Calcula: venta_6m, ultima_compra, dias_sin_comprar     β”‚
   β”‚   β”œβ”€ Calcula: ndcons_n, ndcons_monto                        β”‚
   β”‚   └─ Clasifica: critico/fantasma/riesgo/sano                β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Bloque         β–Ό                                            β”‚
   β”‚ SALDO CAJA HOY                                              β”‚
   β”‚                                                             β”‚
   β”‚  SELECT total_saldo_final FROM caja_raw_egresos_caja        β”‚
   β”‚  ORDER BY fecha_caja DESC LIMIT 1                           β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Bloque         β–Ό                                            β”‚
   β”‚ CALENDARIO SEMANAL (4 semanas)                              β”‚
   β”‚                                                             β”‚
   β”‚  Por cada semana w en [0..3]:                               β”‚
   β”‚   β”œβ”€ cobranzas = heurΓ­stica sobre aging                     β”‚
   β”‚   β”œβ”€ cheques   = SUM(importe) cuyo fecha_pres ∈ semana      β”‚
   β”‚   β”œβ”€ pagos     = SUM(saldo)   cuyo fecha_vto  ∈ semana      β”‚
   β”‚   └─ neto      = cobranzas + cheques - pagos                β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
                    β–Ό
            { ok, corte, clientes, proveedores, cheques,
              calendario, saldo_caja_hoy, alertas, ... }

D5 πŸ”— Cross-module bootstrap del schema

   Cualquier endpoint del mΓ³dulo VENTA
   ────────────────────────────────────
        β”‚
        β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  require_once 'includes/db_reportes.php'     β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό (al primer require, sΓ³lo una vez por proceso)
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  db_reportes.php carga al inicio:                       β”‚
   β”‚   require_once '../../articulos/api/_schema.php'        β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  Cuando llamΓ‘s getApiDB() por primera vez:              β”‚
   β”‚                                                         β”‚
   β”‚   1. Crear PDO singleton                                β”‚
   β”‚   2. Llamar articulos_ensure_schema($pdo)               β”‚
   β”‚      β”œβ”€ CREATE TABLE IF NOT EXISTS articulos_raw        β”‚
   β”‚      β”œβ”€ CREATE TABLE IF NOT EXISTS articulos_raw_prov…  β”‚
   β”‚      β”œβ”€ CREATE TABLE IF NOT EXISTS … (5 tablas)         β”‚
   β”‚      └─ ALTER TABLE defensivos en try/catch             β”‚
   β”‚                                                         β”‚
   β”‚   3. Return PDO                                         β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  Queries de venta hacen JOIN seguro con articulos_raw   β”‚
   β”‚  (la tabla siempre existe, aunque estΓ© vacΓ­a)           β”‚
   β”‚                                                         β”‚
   β”‚  Si estΓ‘ vacΓ­a β†’ IFNULL hace fallback a pesoTotal       β”‚
   β”‚  Si estΓ‘ cargada β†’ usa cat_peso Γ— cantidadesTotal       β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Beneficio: nunca falla con "Table 'articulos_raw' doesn't exist" aunque el mΓ³dulo ArtΓ­culos no se haya inicializado.

D6 🎯 Cross-filter (drill-down) β€” request lifecycle

   Usuario en dashboard.php click en una fila de la tabla "Supervisores"
   ────────────────────────────────────────────────────────────────────
        β”‚
        β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  JS captura el click + lee dataset del row:              β”‚
   β”‚   tipo  = 'supervisor'                                   β”‚
   β”‚   valor = 'LAR | ANDREA SANCHEZ (SUPERVISOR)'            β”‚
   β”‚   id    = sync_id actual                                 β”‚
   β”‚   sucursal = filtro actual                               β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  fetch('/reportes/venta/api/filter.php?                  β”‚
   β”‚        id=28&tipo=supervisor&valor=...&sucursal=...')    β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  api/filter.php                                          β”‚
   β”‚  ────────────────                                        β”‚
   β”‚  1. Validar:                                             β”‚
   β”‚     Β· id INT > 0                                        β”‚
   β”‚     Β· tipo ∈ whitelist                                   β”‚
   β”‚     Β· valor != ''                                        β”‚
   β”‚  2. Verificar sync existe y estado='completado'          β”‚
   β”‚  3. switch(tipo):                                        β”‚
   β”‚     case 'supervisor':                                   β”‚
   β”‚       query principal (KPIs filtrados)                   β”‚
   β”‚       query 2: top 15 proveedores                        β”‚
   β”‚     case 'deposito': …                                   β”‚
   β”‚     case 'provincia': …                                  β”‚
   β”‚     case 'vendedor': …                                   β”‚
   β”‚  4. Devolver JSON                                        β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  JS recibe el JSON y abre modal con:                     β”‚
   β”‚   Β· 6 KPI cards filtrados                                β”‚
   β”‚   Β· tabla top 15 proveedores                             β”‚
   β”‚   Β· breadcrumb "Volver" β†’ cierra modal                   β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

D7 πŸ—ΊοΈ Mapa de dependencias completo

                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                          β”‚    u120688891_chess (BD)  β”‚
                          β”‚    33 tablas Β· ~915K filas β”‚
                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                       β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚              β”‚            β”‚          β”‚            β”‚               β”‚
        β–Ό              β–Ό            β–Ό          β–Ό            β–Ό               β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ VENTA  β”‚    β”‚  CAJA  β”‚   β”‚CASHFLOWβ”‚  β”‚ARTICULOS β”‚ β”‚ LISTAS β”‚    β”‚  /hub/   β”‚
    β”‚ v2.4.0 β”‚    β”‚ v2.0.2 β”‚   β”‚ v1.1.0 β”‚  β”‚ (no v)   β”‚ β”‚ (no v) β”‚    β”‚ /reportesβ”‚
    β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
        β”‚             β”‚            β”‚            β”‚            β”‚              β”‚
        β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚            β”‚            β”‚              β”‚
        β”‚   β”‚                      β”‚            β”‚            β”‚              β”‚
        β”‚   β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚            β”‚              β”‚
        β”‚   β”‚   β”‚                               β”‚            β”‚              β”‚
        β–Ό   β–Ό   β–Ό                               β–Ό            β–Ό              β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ JOINS Y LECTURAS CRUZADAS β”‚          β”‚ SYNC DELEGADO       β”‚
   β”‚ ───────────────────────── β”‚          β”‚ ──────────────────  β”‚
   β”‚ β€’ venta β†’ articulos_raw   β”‚          β”‚ listas/sync.php     β”‚
   β”‚   (peso fallback)         β”‚          β”‚   β†’ /catalogo/admin β”‚
   β”‚                           β”‚          β”‚       /api_sync.php β”‚
   β”‚ β€’ caja/finanzas β†’ ventas  β”‚          β”‚  (fuera de reportes)β”‚
   β”‚   (cruce rentabilidad)    β”‚          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   β”‚                           β”‚
   β”‚ β€’ cashflow β†’ ventas       β”‚
   β”‚   (cruce comercial 6m)    β”‚
   β”‚                           β”‚
   β”‚ β€’ cashflow β†’ caja_raw_    β”‚
   β”‚   egresos_caja (saldo)    β”‚
   β”‚                           β”‚
   β”‚ β€’ /hub β†’ ventas + caja +  β”‚
   β”‚   articulos_raw (stats)   β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

                   APIs externas
                   ─────────────
                       β”‚
                       β”‚ HTTPS
                       β–Ό
                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                   β”‚ ChessERP                     β”‚
                   β”‚ /AR1185/web/api/chess/v1     β”‚
                   β”‚  β€’ /auth/login               β”‚
                   β”‚  β€’ /ventas?fechaDesde=…      β”‚
                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

D8 🎨 Theme switching (anti-flash)

   Usuario navega a /reportes/venta/dashboard.php
        β”‚
        β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  <head>                                                          β”‚
   β”‚   β˜… PRIMERA lΓ­nea del head (anti-flash):                         β”‚
   β”‚   <script>                                                       β”‚
   β”‚   (function(){                                                   β”‚
   β”‚     var t = localStorage.getItem('za-theme') || 'dark';          β”‚
   β”‚     document.documentElement.setAttribute('data-theme', t);      β”‚
   β”‚   })();                                                          β”‚
   β”‚   </script>                                                      β”‚
   β”‚                                                                  β”‚
   β”‚  <link rel="stylesheet" href="style.css">                        β”‚
   β”‚   ↑ Variables CSS leen :root o [data-theme='dark']               β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
                             β”‚ β˜… NO HAY FLASH: el atributo se setea
                             β”‚   antes de que el CSS pinte
                             β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  <body> renderiza con tema correcto desde el primer pixel       β”‚
   β”‚                                                                  β”‚
   β”‚  Toggle button:                                                  β”‚
   β”‚   click β†’ cur = dark ? 'light' : 'dark'                          β”‚
   β”‚           localStorage.setItem('za-theme', cur)                  β”‚
   β”‚           html.dataset.theme = cur                               β”‚
   β”‚           (transiciΓ³n CSS de 0.35s)                              β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Clave za-theme compartida por todos los mΓ³dulos del sitio.

D9 ⚑ Lifecycle de una request al dashboard

   GET /reportes/venta/dashboard.php?sync=28&sucursal=ZONAS+ARIDAS
        β”‚
        β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ 1. PHP carga incluyes:                                        β”‚
   β”‚    require config/database.php  β†’ constantes DB + API         β”‚
   β”‚    require includes/db_reportes.php                           β”‚
   β”‚       └─► require articulos/api/_schema.php                   β”‚
   β”‚    require includes/data_api.php                              β”‚
   β”‚    require includes/functions.php                             β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ 2. Validar params:                                            β”‚
   β”‚    $syncId    = (int)$_GET['sync']                            β”‚
   β”‚    $sucursal  = trim($_GET['sucursal'])                       β”‚
   β”‚    Validar sync existe + estado='completado'                  β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ 3. loadDashboardDataFromApi($syncId, $sucursal)               β”‚
   β”‚    Devuelve bundle con ~18 keys:                              β”‚
   β”‚     kpis, diarias, supervisores, provincias,                  β”‚
   β”‚     prov_sup, prov_dep, rankings, empresas,                   β”‚
   β”‚     localidades, devoluciones, anulados, evolucion_12m,       β”‚
   β”‚     margen_vend, margen_sup, margen_dep,                      β”‚
   β”‚     margen_cli, margen_art, margen_prov,                      β”‚
   β”‚     merc_sc, abc, clientes_comp, sin_articulo                 β”‚
   β”‚                                                               β”‚
   β”‚    β˜… ~20 queries SQL ejecutadas secuencialmente               β”‚
   β”‚    Tiempo tΓ­pico: 800ms - 2s segΓΊn mes/sucursal               β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ 4. Renderiza HTML inline:                                     β”‚
   β”‚    Β· Header sticky + filtros                                  β”‚
   β”‚    Β· Resumen Ejecutivo                                        β”‚
   β”‚    Β· Las 14 secciones con datos pre-renderizados              β”‚
   β”‚    Β· Inyecta datos JSON en variables JS:                      β”‚
   β”‚       const RENT_VEND_DATA = <?= json_encode(...) ?>          β”‚
   β”‚       const RENT_SUP_DATA  = <?= json_encode(...) ?>          β”‚
   β”‚       ... etc                                                 β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ 5. Browser ejecuta JS:                                        β”‚
   β”‚    Β· Crea instancias Tabulator (6 tabs Rentabilidad)          β”‚
   β”‚    Β· Crea instancias Chart.js (ventas diarias, evoluciΓ³n 12m) β”‚
   β”‚    Β· Setup event listeners (openTab, fullscreen, filter)      β”‚
   β”‚                                                               β”‚
   β”‚  Drill-downs son requests AJAX adicionales a filter.php       β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

D10 πŸ“Š Tabulator render lifecycle (con tabs)

   El usuario llega al dashboard con tab "Por Vendedor" activo
        β”‚
        β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Cada tab inicializa su Tabulator AL CARGAR la pΓ‘gina:        β”‚
   β”‚                                                              β”‚
   β”‚  tabRentVend = new Tabulator("#tabulator-rent-vend", {       β”‚
   β”‚    data: RENT_VEND_DATA,                                     β”‚
   β”‚    columns: [...],                                           β”‚
   β”‚    rowFormatter: makeRentRowFormatter(...)                   β”‚
   β”‚  });                                                         β”‚
   β”‚                                                              β”‚
   β”‚  β˜… Tabulator NO mide bien containers con display:none       β”‚
   β”‚    β†’ Los tabs no activos renderean con dimensiones errΓ³neas  β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Usuario hace click en otro tab (ej. "Por Cliente"):          β”‚
   β”‚                                                              β”‚
   β”‚  openTab(event, 'rent-tab-cli') {                            β”‚
   β”‚    1. Quitar .active a todos los .tab-content                β”‚
   β”‚    2. Quitar .active a todos los .tab-btn                    β”‚
   β”‚    3. Agregar .active al nuevo tab-content (display:block)   β”‚
   β”‚    4. setTimeout(()=> {                                      β”‚
   β”‚         tabRentCli.redraw(true)  // β˜… recalcula dimensiones  β”‚
   β”‚       }, 50);                                                β”‚
   β”‚  }                                                           β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Cada vez que se entra a un tab por primera vez:              β”‚
   β”‚  Β· Tabulator recalcula widths/heights con el container       β”‚
   β”‚    ya visible                                                β”‚
   β”‚  Β· Renderiza virtual scroll (sΓ³lo filas visibles)            β”‚
   β”‚  Β· bottomCalc se ejecuta con todos los datos                 β”‚
   β”‚  Β· rowFormatter aplica .rent-row-top / .rent-row-bot         β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

El setTimeout(50ms) es necesario porque display:block no es inmediato (espera al prΓ³ximo reflow).