feat(modo-local): fallback completo sin Monday + cola de sincronización

- Circuit breaker saludMonday (3 fallos → modo local; ping cada 60s)
- Endpoint GET /api/sistema/estado {mondayOk, pendientesSync}
- LeadCaptura extendida (board, etapa, origen, motivo, pendienteSync)
- Tablas Unidad (espejo inventario, 288 seed) + SyncPendiente
- crmLocal: crearLead, afluencia, cambio etapa, descarte, conversión, apartar
- Ruteo en controllers: fallback local ante 502 o Monday caído
- Mi día local: resumen completo desde SQLite + unión con Monday
- Fallbacks: unidades, contactos, reportes, prefill, buscador
- Sync worker cada 2min + handlers 15 tipos (con DependenciaPendienteError)
- Gate automatizaciones: skip si !mondayOk
- Banner 'Modo local' en 5 apps + badge pendiente en mi-dia
- 597 API tests + 263 front tests verdes

Env MODO_LOCAL_FORZADO=true para testing sin Monday.
This commit is contained in:
2026-09-06 08:08:56 +00:00
parent af2fb10956
commit 60649890eb
70 changed files with 8763 additions and 240 deletions

View File

@@ -1,5 +1,7 @@
import { useCallback, useEffect, useState, type FormEvent } from 'react'
import KpiCard from '../components/KpiCard'
import BannerModoLocal from '../components/BannerModoLocal'
import ChipPendienteSync from '../components/ChipPendienteSync'
import type { AccionesSesion } from '../components/AuthGate'
import {
esActividadVencida,
@@ -187,7 +189,10 @@ function FilaOportunidad({
<li className="rounded-xl border border-[#E5E7EB] bg-white p-4 shadow-sm">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-sm font-medium">{op.nombre}</p>
<p className="flex items-center gap-2 text-sm font-medium">
{op.nombre}
{op.pendienteSync === true && <ChipPendienteSync />}
</p>
<p className="mt-1 text-xs text-[#64748B]">
{op.telefono || 'Sin teléfono'}
{op.desarrollo ? ` · ${op.desarrollo}` : ''}
@@ -285,10 +290,11 @@ function FilaApartado({
<li className="rounded-xl border border-[#E5E7EB] bg-white p-4 shadow-sm">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-sm font-medium">
<p className="flex items-center gap-2 text-sm font-medium">
{apartado.nombre}
{apartado.pendienteSync === true && <ChipPendienteSync />}
{apartado.unidadCodigo ? (
<span className="ml-2 text-xs font-normal text-[#94A3B8]">{apartado.unidadCodigo}</span>
<span className="text-xs font-normal text-[#94A3B8]">{apartado.unidadCodigo}</span>
) : null}
</p>
<p className="mt-1 text-xs text-[#64748B]">
@@ -510,6 +516,7 @@ function FilaLead({
<div>
<p className="flex items-center gap-2 text-sm font-medium">
{lead.nombre}
{lead.pendienteSync === true && <ChipPendienteSync />}
{/* Insignia de procedencia: gris = tablero Leads, azul = Oportunidades */}
<span
className={
@@ -757,6 +764,8 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
</div>
</header>
<BannerModoLocal />
<main className="mx-auto max-w-[1280px] px-4 py-8 sm:px-6">
<div className="mb-6">
<h1 className="text-xl font-semibold text-[#111827]">{nombreVendedor}</h1>
@@ -884,7 +893,10 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm"
>
<div>
<p className="text-sm font-medium">{c.nombre}</p>
<p className="flex items-center gap-2 text-sm font-medium">
{c.nombre}
{c.pendienteSync === true && <ChipPendienteSync />}
</p>
<p className="mt-1 text-xs text-[#64748B]">
{c.telefono || 'Sin teléfono'}
{c.origen ? ` · ${c.origen}` : ''}