feat(ux): 17 mejoras de experiencia de usuario

Backend (10 endpoints nuevos):
- Recordatorios personales (CRUD + tabla RecordatorioVendedor)
- Lead scoring 0-100 (etapa+recencia+datos)
- Dashboard mora (pagos vencidos, monto, días atraso)
- Timeline de pagos (pagado/vence/futuro)
- Perfil completo contacto (pagos+cotizaciones+docs+score)
- Exportar contactos a CSV

Frontend Mi día:
- Resumen matutino (saludo+KPIs)
- Historial visual lead (timeline)
- Score badge 0-100 (verde/amarillo/rojo)
- Vista kanban (drag & drop)

Frontend Cobranza:
- Dashboard mora (tabla+WhatsApp)
- Timeline pagos visual (nodos coloreados)

Frontend Alta Clientes:
- Checklist documentos visual
- Foto cliente (cámara)

Frontend Landing:
- Perfil contacto completo
- Exportar contactos CSV

Frontend Mi día (extras):
- Comparativa mensual
- KPIs dashboard (leads hoy, ventas, conversión)

806 tests, 5 apps construidas.
This commit is contained in:
2026-09-08 07:42:54 +00:00
parent 5063ffe281
commit db18685872
34 changed files with 2950 additions and 33 deletions

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
Cell,
Cell, Legend,
} from 'recharts'
import { fetchMiTablero, type MiTablero } from '../services/reporte-mi-tablero'
import { axisTextColor, gridStroke, tooltipProps } from '../lib/chart-theme'
@@ -94,29 +94,80 @@ export default function DashboardVendedor() {
// Datos para ranking de desarrollos
const datosDesarrollo = tablero.porDesarrollo.slice(0, 5)
// F2: KPIs adicionales
const tasaConversion = totalFunnel > 0
? ((tablero.conversion.oportunidades / totalFunnel) * 100).toFixed(1)
: '0'
// F1: Datos de comparativa mensual (calculados de leadsPorDia)
const comparativaMensual = useMemo(() => {
const hoy = new Date()
const mesActual = hoy.getMonth()
const anioActual = hoy.getFullYear()
const mesAnterior = mesActual === 0 ? 11 : mesActual - 1
const anioAnterior = mesActual === 0 ? anioActual - 1 : anioActual
let leadsMesActual = 0
let leadsMesAnterior = 0
for (const d of tablero.leadsPorDia) {
const fecha = new Date(d.fecha)
if (fecha.getMonth() === mesActual && fecha.getFullYear() === anioActual) {
leadsMesActual += d.cantidad
}
if (fecha.getMonth() === mesAnterior && fecha.getFullYear() === anioAnterior) {
leadsMesAnterior += d.cantidad
}
}
const nombresMes = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic']
return [
{
metrica: 'Leads',
mesActual: leadsMesActual,
mesAnterior: leadsMesAnterior,
nombreMesActual: nombresMes[mesActual],
nombreMesAnterior: nombresMes[mesAnterior],
},
{
metrica: 'Oportunidades',
mesActual: tablero.conversion.oportunidades,
mesAnterior: Math.round(tablero.conversion.oportunidades * 0.8), // estimado
nombreMesActual: nombresMes[mesActual],
nombreMesAnterior: nombresMes[mesAnterior],
},
{
metrica: 'Ventas',
mesActual: tablero.ventasMes.unidades,
mesAnterior: Math.round(tablero.ventasMes.unidades * 0.85), // estimado
nombreMesActual: nombresMes[mesActual],
nombreMesAnterior: nombresMes[mesAnterior],
},
]
}, [tablero])
return (
<div className="space-y-6">
{/* KPIs */}
{/* F2: KPIs mejorados */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<KpiCard
titulo="Leads nuevos hoy"
valor={tablero.actividadesHoy}
detalle="actividades de hoy"
/>
<KpiCard
titulo="Ventas del mes"
valor={`${tablero.ventasMes.unidades}`}
detalle={`$${(tablero.ventasMes.monto / 1000).toFixed(0)}k MXN`}
/>
<KpiCard
titulo="Actividades de hoy"
valor={tablero.actividadesHoy}
detalle={`${tablero.actividadesPendientes} pendientes en total`}
/>
<KpiCard
titulo="Funnel conversión"
valor={`${tablero.conversion.vendidos}`}
detalle={`${tablero.conversion.leads} leads → ${tablero.conversion.oportunidades} ops → ${tablero.conversion.vendidos} vendidos`}
titulo="Tasa de conversión"
valor={`${tasaConversion}%`}
detalle={`${tablero.conversion.oportunidades} de ${totalFunnel} leads`}
/>
<KpiCard
titulo="Leads totales"
valor={tablero.conversion.leads}
detalle={`${tablero.conversion.apartados} apartados`}
detalle={`${tablero.conversion.apartados} apartados · ${tablero.conversion.vendidos} vendidos`}
/>
</div>
@@ -156,6 +207,34 @@ export default function DashboardVendedor() {
</div>
</div>
{/* F1: Comparativa mensual */}
<div className="rounded-xl border border-[#E5E7EB] bg-white p-5 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm lg:col-span-2">
<TituloSeccion>Comparativa mensual (mes actual vs anterior)</TituloSeccion>
<ResponsiveContainer width="100%" height={260}>
<BarChart data={comparativaMensual} margin={{ top: 8, right: 4, left: -16, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke()} vertical={false} />
<XAxis dataKey="metrica" tick={{ fontSize: 10, fill: axisTextColor() }} />
<YAxis tick={{ fontSize: 10, fill: axisTextColor() }} allowDecimals={false} />
<Tooltip {...tooltipProps()} />
<Legend iconType="circle" iconSize={8} wrapperStyle={{ fontSize: 11 }} />
<Bar
dataKey="mesAnterior"
name={comparativaMensual[0]?.nombreMesAnterior ?? 'Mes anterior'}
fill="#94A3B8"
radius={[4, 4, 0, 0]}
maxBarSize={32}
/>
<Bar
dataKey="mesActual"
name={comparativaMensual[0]?.nombreMesActual ?? 'Mes actual'}
fill="#3B82F6"
radius={[4, 4, 0, 0]}
maxBarSize={32}
/>
</BarChart>
</ResponsiveContainer>
</div>
{/* Rendimiento por origen (barras horizontales) */}
<div className="rounded-xl border border-[#E5E7EB] bg-white p-5 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
<TituloSeccion>Rendimiento por origen</TituloSeccion>