import { auth } from "../lib/auth";
import { ownerFilter } from "../lib/authz";
import { getClosed, getSystemUsers } from "../lib/queries";
import { agg, EJECUTIVOS_VENTA } from "../lib/derive";
import { getMetas, getMetaGlobal } from "../lib/metas";
import LoginPrompt from "../components/LoginPrompt";
import OwnerControl from "../components/OwnerControl";
import ErrorNotice from "../components/ErrorNotice";
import { DYN, money, money2 } from "../lib/constants";
export const dynamic = "force-dynamic";
function currentMonth() {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
}
function Panel({ title, hint, rows, cls }) {
const maxN = Math.max(...rows.map((r) => r.n), 1);
return (
{title}
{hint &&
{hint}
}
{rows.map((r) => (
))}
);
}
export default async function GanadasPage({ searchParams }) {
const session = await auth();
if (!session?.email) return ;
const { owner } = await searchParams;
const scope = ownerFilter(session, owner);
const mes = currentMonth();
let won, lost, allUsers;
try {
[won, lost, allUsers] = await Promise.all([
getClosed(session.accessToken, scope, 1, mes),
getClosed(session.accessToken, scope, 2, mes),
getSystemUsers(session.accessToken),
]);
} catch (error) {
return ;
}
const owners = allUsers.filter((u) => EJECUTIVOS_VENTA.includes(u.name));
const metas = getMetas();
const metaGlobal = getMetaGlobal();
const sumW = won.reduce((a, o) => a + (o.actual || 0), 0);
const arpu = won.length ? sumW / won.length : 0;
const conv = won.length + lost.length ? Math.round((won.length / (won.length + lost.length)) * 100) : 0;
const cumplGlobal = metaGlobal ? Math.round((sumW / metaGlobal) * 100) : null;
const porEjecutivo = EJECUTIVOS_VENTA.map((nombre) => {
const w = won.filter((o) => o.owner === nombre);
const logrado = w.reduce((a, o) => a + (o.actual || 0), 0);
const email = allUsers.find((u) => u.name === nombre)?.email;
const meta = email ? metas[email] : null;
const pct = meta ? Math.round((logrado / meta) * 100) : null;
return { nombre, logrado, meta, pct };
}).sort((a, b) => (b.pct ?? -1) - (a.pct ?? -1));
return (
Ventas Ganadas — {mes}
{money2(sumW)}
Suma ingresos reales
{won.length}
Oportunidades ganadas
{money2(arpu)}
ARPU logrado
{conv}%
% Conversión
{cumplGlobal != null ? `${cumplGlobal}%` : "n/d"}
Cumplimiento equipo
Cumplimiento de meta por ejecutivo
Ingresos reales ÷ meta mensual · verde ≥70% · ámbar 50-69% · rojo <50%
{porEjecutivo.map((r) => (
{r.nombre}
= 70 ? "" : r.pct >= 50 ? "alerta" : "pierde"}`}
style={{ width: `${Math.min(100, r.pct ?? 0)}%` }}
/>
{r.pct == null ? "n/d" : `${r.pct}%`}
{money(r.logrado)}
{r.meta ? `/${money(r.meta)}` : ""}
))}
o.actual)} />
o.actual)} />
o.actual)} />
o.actual)} />
Detalle de oportunidades ganadas
Clic en el nombre abre la oportunidad en Dynamics 365
{won.length === 0 ? (
No hay oportunidades ganadas en este período para este filtro.
) : (
| Cliente |
Oportunidad |
Seg. |
{!scope.locked && Ejecutivo | }
Tipo |
Línea de Negocio |
Origen |
MRR |
Cierre |
{[...won].sort((a, b) => b.actual - a.actual).map((o) => (
| {o.cliente || "—"} |
{o.name}
|
{o.segmento === "Cartera Activa" ? "CA" : "CN"}
|
{!scope.locked && {o.owner} | }
{o.tipo} |
{o.linea} |
{o.origen} |
{money2(o.actual)} |
{o.actualClose || "—"} |
))}
)}
);
}