import { useCallback, useEffect, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { Bar, BarChart, CartesianGrid, Cell, Funnel, FunnelChart, LabelList, Legend, Pie, PieChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from 'recharts' import { axisTextColor, gridStroke, tooltipProps } from '../lib/chart-theme' import KpiCard from './KpiCard' import Perfilamiento from './Perfilamiento' import SeccionEquipo from './SeccionEquipo' import { EstadoVacio, TarjetaGrafica, type AccionesTablero } from './comunes' import { datosCobranza, datosFunnel, datosPipeline, formatoMoneda, formatoPorcentaje, unidadesApiladas, unidadesEnEtapa, type ResumenReportes, } from '../lib/reportes' import { fetchResumen } from '../services/reportes' // Paleta NovaSpark: acento azul + semánticos (sin gradientes) const COLORES_STATUS: Record = { DISPONIBLE: '#2563EB', APARTADO: '#F59E0B', VENDIDO: '#525252', BLOQUEADO: '#EF4444', OTROS: '#d4d4d4', } const COLORES_FUNNEL = ['#94A3B8', '#64748B', '#3B82F6', '#2563EB'] const COLORES_COBRANZA = ['#2563EB', '#F59E0B'] /** Rangos de fecha disponibles para filtrar las gráficas (días hacia atrás) */ const RANGOS_DIAS = [7, 30, 90] as const /** Rutas de drill-down a otras apps del ecosistema (NGINX las sirve como SPAs aparte) */ const RUTA_PLANES_VENCIDOS = '/pay/?vista=todos&status=ACTIVO&sort=atraso&dir=desc' const RUTA_PLANES_ACTIVOS = '/pay/?vista=todos&status=ACTIVO' const RUTA_MI_DIA = '/hoy/' /** * Drill-down cruzado de apps: /pay (cobranza) y /hoy (mi-dia) son SPAs * distintas detrás de NGINX; navigate() de react-router no puede salir del * basename de esta app, así que el salto es a nivel document. */ function irAApp(ruta: string): void { window.location.assign(ruta) } /** * Vista "General": el tablero comercial de todo el ecosistema (KPIs, inventario, * pipeline, funnel y cobranza). Con `mostrarEquipo` (coordinador/corporativo/ * admin) agrega la sección Equipo con leads por vendedor. * El rango de fechas (?dias=7|30|90) vive en la URL y filtra leads, * oportunidades y perfilamiento; inventario y cobranza son posición actual. */ export default function TableroGeneral({ mostrarEquipo, alCambiarAcciones, }: { mostrarEquipo: boolean alCambiarAcciones: (acciones: AccionesTablero) => void }) { const [resumen, setResumen] = useState(null) const [error, setError] = useState(null) const [cargando, setCargando] = useState(true) // Rango de fechas persistido en la URL: ?dias=7|30|90 (default 30) const [searchParams, setSearchParams] = useSearchParams() const paramDias = Number(searchParams.get('dias')) const dias = (RANGOS_DIAS as readonly number[]).includes(paramDias) ? paramDias : 30 const cargar = useCallback(async () => { setCargando(true) try { setResumen(await fetchResumen(dias)) setError(null) } catch (err) { setError(err instanceof Error ? err.message : 'No se pudo cargar el tablero') } finally { setCargando(false) } }, [dias]) useEffect(() => { void cargar() }, [cargar]) /** Cambia el rango y lo persiste en la URL (compartible) */ const cambiarRango = (nuevo: number) => { setSearchParams( (prev) => { const next = new URLSearchParams(prev) next.set('dias', String(nuevo)) return next }, { replace: true }, ) } // Expone "Actualizar" al header sticky de DashboardPage useEffect(() => { alCambiarAcciones({ actualizar: () => void cargar(), cargando }) }, [alCambiarAcciones, cargar, cargando]) const unidades = resumen ? unidadesApiladas(resumen) : [] const funnel = resumen ? datosFunnel(resumen) : [] const cobranza = resumen ? datosCobranza(resumen) : [] const pipeline = resumen ? datosPipeline(resumen) : [] const sinPipeline = pipeline.every((e) => e.valor === 0) const sinCobranza = resumen !== null && resumen.cobranza.planesAlCorriente + resumen.cobranza.planesConVencidos === 0 return (

Tablero comercial

{resumen && (

Actualizado: {new Date(resumen.generadoEn).toLocaleString('es-MX')}

)}
Rango: {RANGOS_DIAS.map((r) => ( ))}
{error && (

Sin datos del API — {error}

)} {!resumen && !error && cargando && } {!resumen && !error && !cargando && } {resumen && ( <>
irAApp(RUTA_MI_DIA)} /> irAApp(RUTA_PLANES_VENCIDOS)} /> irAApp(RUTA_PLANES_ACTIVOS)} />
{unidades.length === 0 ? ( ) : (
{Object.keys(COLORES_STATUS).map((status) => ( ))}
)}
{sinPipeline ? ( ) : (
)}
{funnel.every((e) => e.valor === 0) ? ( ) : (
{funnel.map((etapa, i) => ( ))}
)}
{sinCobranza ? ( ) : (
`${name}: ${value}`} fontSize={11} > {cobranza.map((rebanada, i) => ( ))}
)}
{mostrarEquipo && } )}
) }