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}` : ""}
| Estado/Objeción |
Leads Asignados |
{matrizEtiq.map((e) => (
| {e.estado} |
{num(e.cantidad)} |
))}
{matrizEtiq.length > 0 && (
| 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}` : ""}
| Tipo de Programa |
Total Matrículas |
Retiradas |
% Retirados |
{matrizProg.map((p) => (
| {p.programa} |
{num(p.total)} |
{num(p.retiradas)} |
{Number(p.pct_retiradas || 0).toLocaleString("es-PE")}% |
))}
{matrizProg.length > 0 && (
| 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}
))}
))}
);
}