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) => (
{r.k}
{r.n} {money(r.v)}
))}
); } 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.

) : (
{!scope.locked && } {[...won].sort((a, b) => b.actual - a.actual).map((o) => ( {!scope.locked && } ))}
Cliente Oportunidad Seg.EjecutivoTipo Línea de Negocio Origen MRR Cierre
{o.cliente || "—"} {o.name} {o.segmento === "Cartera Activa" ? "CA" : "CN"} {o.owner}{o.tipo} {o.linea} {o.origen} {money2(o.actual)} {o.actualClose || "—"}
)}
); }