import { useState, useEffect, useMemo } from "react"; import { api } from "../lib/api"; import { Loader, ErrorBox, Filters, Select } from "../components/UI"; import { AreaChart, Area, BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, } from "recharts"; const MESES = ["ENERO","FEBRERO","MARZO","ABRIL","MAYO","JUNIO","JULIO","AGOSTO","SEPTIEMBRE","OCTUBRE","NOVIEMBRE","DICIEMBRE"]; export default function Vendedores() { const _hoy = new Date(); const [vend, setVend] = useState("TODOS"); const [ano, setAno] = useState(String(_hoy.getFullYear())); // año actual const [mes, setMes] = useState(String(_hoy.getMonth() + 1)); // mes actual const [dia, setDia] = useState("TODOS"); const [modoTR, setModoTR] = useState("asig"); // "asig" | "resp" para el grafico de tiempo const [filtros, setFiltros] = useState({ anos: [2026] }); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [recargando, setRecargando] = useState(false); const [error, setError] = useState(null); const [reload, setReload] = useState(0); useEffect(() => { api.leadsFiltros().then(setFiltros).catch(() => {}); }, []); useEffect(() => { const id = setInterval(() => setReload((n) => n + 1), 15 * 60 * 1000); const onActualizar = () => setReload((n) => n + 1); // recarga al guardar en Config window.addEventListener("datos-actualizar", onActualizar); return () => { clearInterval(id); window.removeEventListener("datos-actualizar", onActualizar); }; }, []); useEffect(() => { let activo = true; if (!data) setLoading(true); setRecargando(true); // atenuar contenido mientras se recalcula setError(null); api.vendedores(ano, mes, dia) .then((res) => { if (activo) { setData(res); setLoading(false); setRecargando(false); } }) .catch((e) => { if (activo) { setError(e.message); setLoading(false); setRecargando(false); } }); return () => { activo = false; }; }, [ano, mes, dia, reload]); const optVend = useMemo(() => ["TODOS", ...((data && data.vendedores) || [])], [data]); const optAnos = useMemo(() => { const anos = (filtros.anos || []).map(String); const actual = String(_hoy.getFullYear()); if (!anos.includes(actual)) anos.push(actual); // asegura que el año actual sea opcion return ["TODOS", ...anos]; }, [filtros]); const optMes = useMemo(() => [{ value: "TODOS", label: "TODOS" }, ...MESES.map((m,i)=>({ value: String(i+1), label: m }))], []); const optDia = useMemo(() => ["TODOS", ...Array.from({length:31}, (_,i)=>String(i+1))], []); // Filas visibles: si hay vendedor filtrado, solo esa; si no, todas const filas = useMemo(() => { if (!data) return []; if (vend === "TODOS") return data.filas; return data.filas.filter((f) => f.vendedor === vend); }, [data, vend]); const num = (v) => Number(v || 0).toLocaleString("es-PE"); // Resumen para las tarjetas: si hay 1 vendedor filtrado -> esa fila; si TODOS -> el total const resumen = useMemo(() => { if (!data) return null; if (vend !== "TODOS") return data.filas.find((f) => f.vendedor === vend) || null; return data.total || null; }, [data, vend]); // Matriz por programa: si hay 1 vendedor -> la suya; si TODOS -> el total const matrizProg = useMemo(() => { if (!data) return []; if (vend !== "TODOS") return (data.matriz_prog && data.matriz_prog[vend]) || []; return data.matriz_prog_total || []; }, [data, vend]); // Matriz Estado/Objeción (Ultima_Etiqueta): por vendedor o total const matrizEtiq = useMemo(() => { if (!data) return []; if (vend !== "TODOS") return (data.matriz_etiq && data.matriz_etiq[vend]) || []; return data.matriz_etiq_total || []; }, [data, vend]); // Serie diaria (asignados + matriculas): por vendedor o total const serieDia = useMemo(() => { if (!data) return []; if (vend !== "TODOS") return (data.serie_dia && data.serie_dia[vend]) || []; return data.serie_dia_total || []; }, [data, vend]); // Serie tiempo de respuesta (promedio min por dia de asignacion) const serieTR = useMemo(() => { if (!data) return []; if (vend !== "TODOS") return (data.tiempo_resp && data.tiempo_resp[vend]) || []; return data.tiempo_resp_total || []; }, [data, vend]); // Serie tiempo de respuesta (promedio min por dia de RESPUESTA) const serieTRr = useMemo(() => { if (!data) return []; if (vend !== "TODOS") return (data.tiempo_resp_r && data.tiempo_resp_r[vend]) || []; return data.tiempo_resp_r_total || []; }, [data, vend]); return (

🧑‍💼 Vendedores

{loading ? : error ? : !data ? :
{resumen && } {/* Gráfico día a día + Estado/Objeción lado a lado */}
{/* Gráfico día a día: Leads Procesados vs Matrículas */}
Leads Asignados y Matrículas por Día{vend !== "TODOS" ? ` — ${vend}` : ""}
{/* Matriz Estado/Objeción (Ultima_Etiqueta) — al costado del gráfico */}
Estado/Objeción{vend !== "TODOS" ? ` — ${vend}` : ""}
{matrizEtiq.map((e) => ( ))} {matrizEtiq.length > 0 && ( )}
Estado/Objeción Leads Asignados
{e.estado} {num(e.cantidad)}
Total {num(matrizEtiq.reduce((s, e) => s + (e.cantidad || 0), 0))}
{/* Tiempo de respuesta + Sin responder, lado a lado */}
{(() => { const esResp = modoTR === "resp"; const serie = esResp ? serieTRr : serieTR; const color = esResp ? "#2563eb" : "#f59e0b"; const colorSoft = esResp ? "#3b82f6" : "#fbbf24"; return (
Tiempo de Respuesta por Día (min){vend !== "TODOS" ? ` — ${vend}` : ""}
{ if (!active || !payload || !payload.length) return null; const d = payload[0].payload || {}; return (
Día {label}
Promedio: {d.promedio_min || 0} min
Respondidos{esResp ? " ese día" : ""}: {d.respondidos || 0}
{esResp ?
⚠ Respuesta tardía (+2 días): {d.tardios || 0}
:
Sin responder: {d.no_respondidos || 0}
}
); }} />
); })()} {/* Leads SIN responder por día (fecha de asignación) — barras */}
Leads Sin Responder por Día{vend !== "TODOS" ? ` — ${vend}` : ""}
{(() => { const datosNR = serieTR.filter((d) => (d.no_respondidos || 0) >= 1); if (datosNR.length === 0) { return (
✅
Sin leads pendientes
Todos los leads del periodo fueron respondidos.
); } return ( { if (!active || !payload || !payload.length) return null; const d = payload[0].payload || {}; return (
Día {label}
Sin responder: {d.no_respondidos || 0}
Respondidos: {d.respondidos || 0}
); }} />
); })()}
{/* Matriz por tipo de programa (al final) */}
Matrículas por Tipo de Programa{vend !== "TODOS" ? ` — ${vend}` : ""}
{matrizProg.map((p) => ( ))} {matrizProg.length > 0 && ( )}
Tipo de Programa Total Matrículas Retiradas % Retirados
{p.programa} {num(p.total)} {num(p.retiradas)} {Number(p.pct_retiradas || 0).toLocaleString("es-PE")}%
Total {num(matrizProg.reduce((s, p) => s + (p.total || 0), 0))} {num(matrizProg.reduce((s, p) => s + (p.retiradas || 0), 0))} {(() => { const t = matrizProg.reduce((s, p) => s + (p.total || 0), 0); const r = matrizProg.reduce((s, p) => s + (p.retiradas || 0), 0); return `${(t ? Math.round(r / t * 1000) / 10 : 0).toLocaleString("es-PE")}%`; })()}
}
); } // ── Tarjetas de resumen: 3 grupos (Cartera / Matrículas / Leads) en lista ───── function ResumenTarjetas({ r, num }) { const pct = Number(r.pct_retiradas || 0); const pctCont = Number(r.pct_contactados || 0); const grupos = [ { titulo: "Cartera", acento: "#2563eb", filas: [ { label: "Total", valor: num(r.cartera_total) }, { label: "Copito", valor: num(r.cartera_copito) }, { label: "Otros", valor: num(r.cartera_otros) }, ], }, { titulo: "Matrículas", acento: "#0f766e", filas: [ { label: "Total", valor: num(r.mat_total) }, { label: "Retiradas", valor: num(r.mat_retiradas) }, { label: "% Retirados", valor: `${pct.toLocaleString("es-PE")}%`, alerta: true }, ], }, { titulo: "Leads", acento: "#7c3aed", filas: [ { label: "Asignados", valor: num(r.leads_asignados) }, { label: "Contactados", valor: num(r.leads_contactados) }, { label: "% Contactados", valor: `${pctCont.toLocaleString("es-PE")}%`, resalta: "#7c3aed" }, ], }, ]; return (
{grupos.map((g) => (
{g.titulo}
{g.filas.map((f, i) => (
{f.label} {f.valor}
))}
))}
); }