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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user