import { auth } from "../lib/auth"; import { ownerFilter } from "../lib/authz"; import { getClosed, getMotivosCierre, getSystemUsers } from "../lib/queries"; import { agg, EJECUTIVOS_VENTA } from "../lib/derive"; 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 }) { 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 PerdemosPage({ searchParams }) { const session = await auth(); if (!session?.email) return ; const { owner } = await searchParams; const scope = ownerFilter(session, owner); const mes = currentMonth(); let lost, allUsers; try { [lost, allUsers] = await Promise.all([ getClosed(session.accessToken, scope, 2, mes), getSystemUsers(session.accessToken), ]); const motivos = await getMotivosCierre(lost.map((o) => o.id), session.accessToken); lost = lost.map((o) => ({ ...o, motivo: motivos[o.id]?.motivo || "Sin registrar", competidor: motivos[o.id]?.competidor })); } catch (error) { return ; } const owners = allUsers.filter((u) => EJECUTIVOS_VENTA.includes(u.name)); const mrrPerdido = lost.reduce((a, o) => a + (o.est || 0), 0); const clientesNuevos = lost.filter((o) => o.segmento === "Clientes Nuevos").length; const carteraActiva = lost.filter((o) => o.segmento === "Cartera Activa").length; return (

Por qué Perdemos — {mes}

{lost.length} Oportunidades perdidas
{money(mrrPerdido)} MRR estimado perdido
{clientesNuevos} Clientes Nuevos
{carteraActiva} Cartera Activa

No hay seguimiento registrado en el CRM ≠ no hubo seguimiento — la mayoría del contacto comercial se hace por WhatsApp, que no está integrado a Dynamics (ver CLAUDE.md).

o.est)} /> o.competidor && o.competidor !== "Ninguno"), "competidor", (o) => o.est)} /> o.est)} /> o.est)} />

Detalle de oportunidades perdidas

Clic en el nombre abre la oportunidad en Dynamics 365

{lost.length === 0 ? (

No hay oportunidades perdidas en este período para este filtro.

) : (
{!scope.locked && } {[...lost].sort((a, b) => (b.est || 0) - (a.est || 0)).map((o) => ( {!scope.locked && } ))}
Cliente Oportunidad Seg. Motivo de cierre Línea de Negocio CompetidorEjecutivoMRR est.
{o.cliente || "—"} {o.name} {o.segmento === "Cartera Activa" ? "CA" : "CN"} {o.motivo} {o.linea || "—"} {o.competidor || "—"}{o.owner}{o.est ? money2(o.est) : "—"}
)}
); }