Actualizar frontend/src/pages/Vendedores.jsx
This commit is contained in:
@@ -14,6 +14,11 @@ export default function Vendedores() {
|
||||
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
|
||||
// Pop-up de detalle al hacer click en un gráfico
|
||||
const [detDia, setDetDia] = useState(null);
|
||||
const [detData, setDetData] = useState(null);
|
||||
const [detLoading, setDetLoading] = useState(false);
|
||||
const [detModo, setDetModo] = useState("asignacion");
|
||||
|
||||
const [filtros, setFiltros] = useState({ anos: [2026] });
|
||||
const [data, setData] = useState(null);
|
||||
@@ -103,6 +108,16 @@ export default function Vendedores() {
|
||||
return data.tiempo_resp_r_total || [];
|
||||
}, [data, vend]);
|
||||
|
||||
const abrirDetalle = (diaNum, modoForzado) => {
|
||||
if (!diaNum) return;
|
||||
const modo = modoForzado || (modoTR === "resp" ? "respuesta" : "asignacion");
|
||||
setDetDia(diaNum); setDetModo(modo); setDetData(null); setDetLoading(true);
|
||||
api.tiempoRespuestaDetalle(diaNum, modo, vend, ano, mes)
|
||||
.then((r) => setDetData(r))
|
||||
.catch(() => setDetData({ asesores: [], total: { cantidad: 0, promedio_min: 0 } }))
|
||||
.finally(() => setDetLoading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="page-title">🧑💼 Vendedores</h1>
|
||||
@@ -222,7 +237,9 @@ export default function Vendedores() {
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={serie} margin={{ top: 12, right: 24, left: -6, bottom: 8 }}>
|
||||
<LineChart data={serie} margin={{ top: 12, right: 24, left: -6, bottom: 8 }}
|
||||
onClick={(p) => abrirDetalle(p && p.activePayload && p.activePayload[0] && p.activePayload[0].payload.dia)}
|
||||
style={{ cursor: "pointer" }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" vertical={false} />
|
||||
<XAxis dataKey="dia" tick={{ fontSize: 10, fill: "#94a3b8" }} interval={0}
|
||||
axisLine={{ stroke: "#e2e8f0" }} tickLine={false} />
|
||||
@@ -247,7 +264,7 @@ export default function Vendedores() {
|
||||
<Line type="linear" dataKey="promedio_min" name="Promedio (min)" stroke={color}
|
||||
strokeWidth={2.5}
|
||||
dot={{ r: 3, fill: color, stroke: "#fff", strokeWidth: 1.5 }}
|
||||
activeDot={{ r: 6, fill: colorSoft, stroke: "#fff", strokeWidth: 2 }} />
|
||||
activeDot={{ r: 7, fill: colorSoft, stroke: "#fff", strokeWidth: 2, cursor: "pointer" }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@@ -278,7 +295,9 @@ export default function Vendedores() {
|
||||
}
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={datosNR} margin={{ top: 12, right: 24, left: -6, bottom: 8 }}>
|
||||
<BarChart data={datosNR} margin={{ top: 12, right: 24, left: -6, bottom: 8 }}
|
||||
onClick={(p) => abrirDetalle(p && p.activePayload && p.activePayload[0] && p.activePayload[0].payload.dia, "sin_responder")}
|
||||
style={{ cursor: "pointer" }}>
|
||||
<defs>
|
||||
<linearGradient id="gradNoResp" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#fb7185" stopOpacity={1} />
|
||||
@@ -357,6 +376,73 @@ export default function Vendedores() {
|
||||
</div>
|
||||
|
||||
</div>}
|
||||
|
||||
{/* Pop-up: detalle del día (leads por asesor con T1, T2 y minutos) */}
|
||||
{detDia != null && (
|
||||
<div onClick={() => setDetDia(null)}
|
||||
style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,0.45)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000, padding: 20 }}>
|
||||
<div onClick={(e) => e.stopPropagation()}
|
||||
style={{ background: "#fff", borderRadius: 14, width: "100%", maxWidth: 820,
|
||||
maxHeight: "85vh", overflow: "auto", boxShadow: "0 20px 60px rgba(0,0,0,0.3)" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center",
|
||||
padding: "16px 20px", borderBottom: "1px solid #f1f5f9", position: "sticky", top: 0, background: "#fff" }}>
|
||||
<div style={{ fontWeight: 800, fontSize: 16, color: "#0f172a" }}>
|
||||
{detModo === "sin_responder" ? "🚫" : "⏱️"} Detalle Día {detDia} · {
|
||||
detModo === "sin_responder" ? "Sin Responder"
|
||||
: detModo === "respuesta" ? "Según Respuesta" : "Según Asignación"}
|
||||
{detData && <span style={{ fontWeight: 600, color: "#64748b", fontSize: 13 }}>
|
||||
{" · "}{detModo === "sin_responder"
|
||||
? `${detData.total?.cantidad || 0} sin responder`
|
||||
: `Promedio: ${detData.total?.promedio_min || 0} min (${detData.total?.cantidad || 0} leads)`}</span>}
|
||||
</div>
|
||||
<button onClick={() => setDetDia(null)}
|
||||
style={{ border: "none", background: "#f1f5f9", borderRadius: 8, width: 32, height: 32,
|
||||
cursor: "pointer", fontSize: 16, color: "#334155" }}>✕</button>
|
||||
</div>
|
||||
<div style={{ padding: "14px 20px" }}>
|
||||
{detLoading ? (
|
||||
<div style={{ padding: 30, textAlign: "center", color: "#94a3b8" }}>Cargando…</div>
|
||||
) : !detData || (detData.asesores || []).length === 0 ? (
|
||||
<div style={{ padding: 30, textAlign: "center", color: "#94a3b8" }}>Sin leads para este día.</div>
|
||||
) : (
|
||||
(detData.asesores || []).map((a) => (
|
||||
<div key={a.asesor} style={{ marginBottom: 18 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center",
|
||||
background: "#eff6ff", borderRadius: 8, padding: "8px 12px", marginBottom: 6 }}>
|
||||
<span style={{ fontWeight: 800, color: "#1e40af", fontSize: 13.5 }}>👤 {a.asesor}</span>
|
||||
<span style={{ fontSize: 12.5, color: "#334155", fontWeight: 700 }}>
|
||||
{detModo === "sin_responder"
|
||||
? `${a.cantidad} sin responder`
|
||||
: `Promedio: ${a.promedio_min} min · ${a.cantidad} leads`}</span>
|
||||
</div>
|
||||
<table className="cob-table" style={{ width: "100%" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: "center" }}>TELÉFONO</th>
|
||||
<th style={{ textAlign: "center" }}>ASIGNACIÓN (T1)</th>
|
||||
<th style={{ textAlign: "center" }}>RESPUESTA (T2)</th>
|
||||
<th style={{ textAlign: "center" }}>MINUTOS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{a.leads.map((l, i) => (
|
||||
<tr key={i}>
|
||||
<td style={{ textAlign: "center", whiteSpace: "nowrap", color: "#334155" }}>{l.telefono || "-"}</td>
|
||||
<td style={{ textAlign: "center", whiteSpace: "nowrap" }}>{l.t1}</td>
|
||||
<td style={{ textAlign: "center", whiteSpace: "nowrap" }}>{l.t2}</td>
|
||||
<td style={{ textAlign: "center", fontWeight: 700, color: "#b45309" }}>{l.minutos}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user