feat: mis clientes en Mi dia + perfilamiento de la afluencia en dashboards

- Cliente.capturadoPorId: el alta (directa y progresiva) registra al
  usuario que la inicio; el panel Mi dia muestra 'Mis clientes' con su
  estado (En captura / Cliente) sin depender de Monday.
- LeadMonday ahora lee las 11 columnas de perfil del tablero Leads
  (estado civil, edad, rango salarial, tipo de empleo/economia,
  califica, credito, modelo, etc.).
- GET /api/reportes/resumen agrega leads.perfil con conteos por
  dimension (valores vacios se omiten; edad en buckets).
- /dash/ gana la seccion 'Perfilamiento de la afluencia' con
  mini-graficas de barras por dimension.
This commit is contained in:
2026-09-02 02:39:19 +00:00
parent 895a831ff8
commit 4030faee25
18 changed files with 551 additions and 14 deletions

View File

@@ -0,0 +1,59 @@
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
import { EstadoVacio, TituloSeccion } from './comunes'
import { rebanadasDe, type ResumenReportes } from '../lib/reportes'
// Dimensiones del perfilamiento de la afluencia (board Leads) en el orden del tablero
const DIMENSIONES: { titulo: string; registro: (perfil: ResumenReportes['leads']['perfil']) => Record<string, number> }[] = [
{ titulo: 'Estado civil', registro: (p) => p.porEstadoCivil },
{ titulo: 'Rango salarial', registro: (p) => p.porRangoSalarial },
{ titulo: 'Edad', registro: (p) => p.porEdad },
{ titulo: 'Tipo de empleo', registro: (p) => p.porTipoEmpleo },
{ titulo: 'Tipo de economía', registro: (p) => p.porTipoEconomia },
{ titulo: 'Califica', registro: (p) => p.porCalifica },
{ titulo: 'Crédito', registro: (p) => p.porCredito },
{ titulo: 'Desarrollo', registro: (p) => p.porDesarrollo },
{ titulo: 'Modelo de interés', registro: (p) => p.porModelo },
{ titulo: 'Medio de contacto', registro: (p) => p.porMedioContacto },
{ titulo: 'Lo quiere para', registro: (p) => p.porLoQuierePara },
]
function TarjetaDimension({ titulo, datos }: { titulo: string; datos: Record<string, number> }) {
const rebanadas = rebanadasDe(datos)
return (
<div className="border border-neutral-200 p-4">
<p className="mb-2 text-xs font-bold uppercase tracking-widest text-neutral-500">{titulo}</p>
{rebanadas.length === 0 ? (
<EstadoVacio mensaje="Sin datos" />
) : (
<div className="h-48">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={rebanadas} layout="vertical" margin={{ top: 4, right: 24, left: 24, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" horizontal={false} />
<XAxis type="number" allowDecimals={false} tick={{ fontSize: 11 }} />
<YAxis type="category" dataKey="nombre" width={90} tick={{ fontSize: 11 }} />
<Tooltip />
<Bar dataKey="valor" name="Leads" fill="#1B4332" isAnimationActive={false} />
</BarChart>
</ResponsiveContainer>
</div>
)}
</div>
)
}
/**
* Sección "Perfilamiento de la afluencia": una mini-gráfica de barras
* horizontales por dimensión capturada en el board Leads.
*/
export default function Perfilamiento({ perfil }: { perfil: ResumenReportes['leads']['perfil'] }) {
return (
<section aria-label="Perfilamiento de la afluencia" className="mt-12">
<TituloSeccion>Perfilamiento de la afluencia</TituloSeccion>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{DIMENSIONES.map((d) => (
<TarjetaDimension key={d.titulo} titulo={d.titulo} datos={d.registro(perfil)} />
))}
</div>
</section>
)
}

View File

@@ -16,6 +16,7 @@ import {
YAxis,
} from 'recharts'
import KpiCard from './KpiCard'
import Perfilamiento from './Perfilamiento'
import SeccionEquipo from './SeccionEquipo'
import { EstadoVacio, TituloSeccion } from './comunes'
import {
@@ -244,6 +245,8 @@ export default function TableroGeneral({ mostrarEquipo }: { mostrarEquipo: boole
</section>
</div>
<Perfilamiento perfil={resumen.leads.perfil} />
{mostrarEquipo && <SeccionEquipo />}
</>
)}