Mi día: cancelación con motivo, conteo escrituración real, cita sin retroceder, calendario completo Cobranza: MoraDashboard respeta alcance, hoy dinámico, cancelar con motivo Landing: contactos paginados, error vs vacío diferenciado Dashboards: KPI renombrado, drill-down planes activos Chat: historial recarga, preguntas rotativas, error solo toast Transversal: shadow-sm corrupto corregido en 9 archivos 553 API tests + 7 apps construidas
129 lines
5.1 KiB
TypeScript
129 lines
5.1 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react'
|
|
import { Cell, Funnel, FunnelChart, LabelList, ResponsiveContainer, Tooltip } from 'recharts'
|
|
import { tooltipProps } from '../lib/chart-theme'
|
|
import KpiCard from './KpiCard'
|
|
import { EstadoVacio, TarjetaGrafica, type AccionesTablero } from './comunes'
|
|
import { funnelMiTablero, type MiTablero } from '../lib/mi-tablero'
|
|
import { fetchMiTablero } from '../services/reportes'
|
|
|
|
// 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 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 [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 {
|
|
setTablero(await fetchMiTablero())
|
|
setError(null)
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'No se pudo cargar el tablero')
|
|
} finally {
|
|
setCargando(false)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
void cargar()
|
|
}, [cargar])
|
|
|
|
// Expone "Actualizar" al header sticky de DashboardPage
|
|
useEffect(() => {
|
|
alCambiarAcciones({ actualizar: () => void cargar(), cargando })
|
|
}, [alCambiarAcciones, cargar, cargando])
|
|
|
|
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>
|
|
{tablero && (
|
|
<p className="mt-1 text-sm text-[#64748B] dark:text-[#94A3B8]">
|
|
Actualizado: {new Date(tablero.generadoEn).toLocaleString('es-MX')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{error && (
|
|
<p role="alert" className="mb-8 rounded-lg bg-amber-50 px-4 py-3 text-xs font-medium text-amber-700">
|
|
Sin datos del API — {error}
|
|
</p>
|
|
)}
|
|
|
|
{!tablero && !error && cargando && <EstadoVacio mensaje="Cargando…" />}
|
|
{!tablero && !error && !cargando && <EstadoVacio mensaje="Sin datos" />}
|
|
|
|
{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="Leads totales"
|
|
valor={conversion.leads}
|
|
detalle={`${conversion.oportunidades} oportunidades · ${conversion.vendidos} vendidos`}
|
|
/>
|
|
<KpiCard
|
|
titulo="Tasa de conversión"
|
|
valor={tasaConversion}
|
|
detalle={`${conversion.oportunidades} de ${conversion.leads} leads`}
|
|
/>
|
|
<KpiCard
|
|
titulo="Cotizaciones del mes"
|
|
valor={tablero.ventasMes.unidades}
|
|
detalle={`$${(tablero.ventasMes.monto / 1000).toFixed(0)}k MXN cotizados`}
|
|
/>
|
|
<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="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%">
|
|
<FunnelChart>
|
|
<Tooltip {...tooltipProps()} />
|
|
<Funnel dataKey="valor" data={funnel} nameKey="etapa" isAnimationActive={false}>
|
|
{funnel.map((etapa, i) => (
|
|
<Cell key={etapa.etapa} fill={COLORES_FUNNEL[i % COLORES_FUNNEL.length]} />
|
|
))}
|
|
<LabelList position="right" dataKey="etapa" stroke="none" fontSize={11} />
|
|
<LabelList position="center" dataKey="valor" stroke="none" fill="#fff" fontSize={12} />
|
|
</Funnel>
|
|
</FunnelChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
)}
|
|
</TarjetaGrafica>
|
|
</section>
|
|
</>
|
|
)}
|
|
</main>
|
|
)
|
|
}
|