feat(mejoras): 25 mejoras UX en todos los módulos + purga Monday
Backend: mi-dia siempre local, vocabulario canónico, AplicacionPago con usuario Mi día: modal español real, cita desde modal, score falso eliminado Cotizador: barra sticky, persistencia local con folio Alta Clientes: redirect eliminado, foto conectada, asesor desde sesión, refs dinámicas Cobranza: contrato Mora arreglado, link a Plan, abono próximo pago, WhatsApp Landing: contactos locales, ModalContacto eliminado, ModoTrabajo eliminado Dashboards: MiTablero unificado, funnel con descartados, drill-down, rango fechas Chat FRASA: corpus 5 desarrollos, 6 herramientas, historial conversaciones 69 archivos, 7 apps construidas
This commit is contained in:
@@ -3,26 +3,28 @@ import { Cell, Funnel, FunnelChart, LabelList, ResponsiveContainer, Tooltip } fr
|
||||
import { tooltipProps } from '../lib/chart-theme'
|
||||
import KpiCard from './KpiCard'
|
||||
import { EstadoVacio, TarjetaGrafica, type AccionesTablero } from './comunes'
|
||||
import { funnelMisLeads, type ResumenMiDia } from '../lib/mi-tablero'
|
||||
import { fetchResumenMiDia } from '../services/mi-dia'
|
||||
import { funnelMiTablero, type MiTablero } from '../lib/mi-tablero'
|
||||
import { fetchMiTablero } from '../services/reportes'
|
||||
|
||||
// Misma paleta del funnel del tablero general (NovaSpark: azules + neutros)
|
||||
const COLORES_FUNNEL = ['#94A3B8', '#64748B', '#3B82F6', '#2563EB']
|
||||
// Misma paleta del funnel del tablero general (NovaSpark: azules + neutros);
|
||||
// rojo para la etapa final "Descartados"
|
||||
const COLORES_FUNNEL = ['#94A3B8', '#64748B', '#3B82F6', '#2563EB', '#EF4444']
|
||||
|
||||
/**
|
||||
* "Mi tablero": KPIs personales del vendedor logueado (mis leads, actividades,
|
||||
* apartados por vencer) y un funnel simple de sus leads por estado.
|
||||
* Fuente: GET /api/mi-dia/resumen (filtra por su mondayUserId en el API).
|
||||
* "Mi tablero": KPIs personales del vendedor logueado y su funnel de conversión
|
||||
* (Leads → Oportunidades → Apartados → Vendidos → Descartados).
|
||||
* Fuente: GET /api/reportes/mi-tablero, el mismo endpoint que usa el panel
|
||||
* "Mi día" (DashboardVendedor).
|
||||
*/
|
||||
export default function MiTablero({ alCambiarAcciones }: { alCambiarAcciones: (acciones: AccionesTablero) => void }) {
|
||||
const [resumen, setResumen] = useState<ResumenMiDia | null>(null)
|
||||
const [tablero, setTablero] = useState<MiTablero | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [cargando, setCargando] = useState(true)
|
||||
|
||||
const cargar = useCallback(async () => {
|
||||
setCargando(true)
|
||||
try {
|
||||
setResumen(await fetchResumenMiDia())
|
||||
setTablero(await fetchMiTablero())
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'No se pudo cargar el tablero')
|
||||
@@ -40,15 +42,20 @@ export default function MiTablero({ alCambiarAcciones }: { alCambiarAcciones: (a
|
||||
alCambiarAcciones({ actualizar: () => void cargar(), cargando })
|
||||
}, [alCambiarAcciones, cargar, cargando])
|
||||
|
||||
const funnel = resumen ? funnelMisLeads(resumen.misLeads) : []
|
||||
const funnel = tablero ? funnelMiTablero(tablero) : []
|
||||
const conversion = tablero?.conversion
|
||||
const tasaConversion =
|
||||
conversion && conversion.leads > 0
|
||||
? `${((conversion.oportunidades / conversion.leads) * 100).toFixed(1)}%`
|
||||
: '—'
|
||||
|
||||
return (
|
||||
<main className="px-4 py-8 sm:px-6 lg:px-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-xl font-semibold text-[#111827] dark:text-[#E2E8F0]">Mi tablero</h1>
|
||||
{resumen && (
|
||||
{tablero && (
|
||||
<p className="mt-1 text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Actualizado: {new Date(resumen.generadoEn).toLocaleString('es-MX')}
|
||||
Actualizado: {new Date(tablero.generadoEn).toLocaleString('es-MX')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -59,39 +66,43 @@ export default function MiTablero({ alCambiarAcciones }: { alCambiarAcciones: (a
|
||||
</p>
|
||||
)}
|
||||
|
||||
{resumen?.sinMapeoMonday && (
|
||||
<p className="mb-8 rounded-lg bg-amber-50 px-4 py-3 text-xs font-medium text-amber-700">
|
||||
Tu usuario aún no está ligado a Monday: las listas personales aparecen vacías
|
||||
</p>
|
||||
)}
|
||||
{!tablero && !error && cargando && <EstadoVacio mensaje="Cargando…" />}
|
||||
{!tablero && !error && !cargando && <EstadoVacio mensaje="Sin datos" />}
|
||||
|
||||
{!resumen && !error && cargando && <EstadoVacio mensaje="Cargando…" />}
|
||||
{!resumen && !error && !cargando && <EstadoVacio mensaje="Sin datos" />}
|
||||
|
||||
{resumen && (
|
||||
{tablero && conversion && (
|
||||
<>
|
||||
<section aria-label="Mis indicadores" className="grid gap-4 grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
<KpiCard
|
||||
titulo="Mis leads activos"
|
||||
valor={resumen.misLeads.length}
|
||||
detalle={`${resumen.conteos.leadsNuevosSemana} nuevo(s) esta semana`}
|
||||
titulo="Leads totales"
|
||||
valor={conversion.leads}
|
||||
detalle={`${conversion.oportunidades} oportunidades · ${conversion.vendidos} vendidos`}
|
||||
/>
|
||||
<KpiCard
|
||||
titulo="Mis actividades abiertas"
|
||||
valor={resumen.conteos.actividadesAbiertas}
|
||||
detalle={`${resumen.misActividadesHoy.length} con fecha de hoy o antes`}
|
||||
titulo="Tasa de conversión"
|
||||
valor={tasaConversion}
|
||||
detalle={`${conversion.oportunidades} de ${conversion.leads} leads`}
|
||||
/>
|
||||
<KpiCard
|
||||
titulo="Apartados por vencer"
|
||||
valor={resumen.apartadosPorVencer.length}
|
||||
detalle="próximos 7 días (pipeline completo)"
|
||||
titulo="Ventas del mes"
|
||||
valor={conversion.vendidos}
|
||||
detalle={`$${(tablero.ventasMes.monto / 1000).toFixed(0)}k MXN · ${tablero.ventasMes.unidades} cotizada(s)`}
|
||||
/>
|
||||
<KpiCard
|
||||
titulo="Actividades abiertas"
|
||||
valor={tablero.actividadesPendientes}
|
||||
detalle={`${tablero.actividadesHoy} con fecha de hoy o antes`}
|
||||
/>
|
||||
<KpiCard
|
||||
titulo="Leads descartados"
|
||||
valor={conversion.descartados}
|
||||
detalle="etapa terminal DESCARTADO"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section aria-label="Mis leads por estado" className="mt-10">
|
||||
<TarjetaGrafica titulo="Mis leads por estado">
|
||||
{funnel.length === 0 ? (
|
||||
<EstadoVacio mensaje="Sin leads asignados" />
|
||||
<section aria-label="Funnel de conversión" className="mt-10">
|
||||
<TarjetaGrafica titulo="Funnel de conversión (incluye descartados)">
|
||||
{funnel.every((e) => e.valor === 0) ? (
|
||||
<EstadoVacio mensaje="Sin movimiento comercial" />
|
||||
) : (
|
||||
<div className="h-72">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
Reference in New Issue
Block a user