100 lines
3.5 KiB
JavaScript
100 lines
3.5 KiB
JavaScript
|
|
import { auth } from "./lib/auth";
|
||
|
|
import { ownerFilter } from "./lib/authz";
|
||
|
|
import { getClosed, getSystemUsers } from "./lib/queries";
|
||
|
|
import { TIPOS_META, 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 { 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")}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default async function Home({ searchParams }) {
|
||
|
|
const session = await auth();
|
||
|
|
if (!session?.email) return <LoginPrompt />;
|
||
|
|
|
||
|
|
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 <ErrorNotice error={error} />;
|
||
|
|
}
|
||
|
|
const owners = allUsers.filter((u) => EJECUTIVOS_VENTA.includes(u.name));
|
||
|
|
|
||
|
|
const sumW = won.reduce((a, o) => a + (o.actual || 0), 0);
|
||
|
|
const sumWMeta = won
|
||
|
|
.filter((o) => TIPOS_META.includes(o.tipo))
|
||
|
|
.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 metas = getMetas();
|
||
|
|
const viewedEmail = scope.locked ? session.email : scope.email;
|
||
|
|
const metaDen = viewedEmail ? metas[viewedEmail.toLowerCase()] || null : getMetaGlobal();
|
||
|
|
const cumpl = metaDen ? Math.round((sumWMeta / metaDen) * 100) : null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div style={{ padding: 24 }}>
|
||
|
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 12 }}>
|
||
|
|
<h2>Resumen — {mes}</h2>
|
||
|
|
<OwnerControl
|
||
|
|
owners={owners}
|
||
|
|
current={scope.locked ? session.email : scope.email}
|
||
|
|
locked={scope.locked}
|
||
|
|
lockedLabel={session.email}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="kpis">
|
||
|
|
<div className="kpi">
|
||
|
|
<span className="n">{money2(sumW)}</span>
|
||
|
|
<span className="l">Ingresos reales (ganadas)</span>
|
||
|
|
</div>
|
||
|
|
<div className="kpi">
|
||
|
|
<span className="n">{won.length}</span>
|
||
|
|
<span className="l">Oportunidades ganadas</span>
|
||
|
|
</div>
|
||
|
|
<div className="kpi">
|
||
|
|
<span className="n">{money2(arpu)}</span>
|
||
|
|
<span className="l">ARPU logrado</span>
|
||
|
|
</div>
|
||
|
|
<div className="kpi">
|
||
|
|
<span className="n pierde">{lost.length}</span>
|
||
|
|
<span className="l">Oportunidades perdidas</span>
|
||
|
|
</div>
|
||
|
|
<div className="kpi">
|
||
|
|
<span className="n">{conv}%</span>
|
||
|
|
<span className="l">% Conversión (ganadas/total)</span>
|
||
|
|
</div>
|
||
|
|
<div className="kpi">
|
||
|
|
<span className={cumpl != null && cumpl < 70 ? "n pierde" : "n"}>
|
||
|
|
{cumpl != null ? cumpl + "%" : "n/d"}
|
||
|
|
</span>
|
||
|
|
<span className="l">
|
||
|
|
Cumplimiento de meta{metaDen ? ` (${money2(metaDen)})` : " (sin meta asignada)"}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<p className="hint">
|
||
|
|
Cumplimiento de meta cuenta solo Venta - Cliente Nuevo, Up-Selling, Cross-Selling y Plan de Cuenta (se
|
||
|
|
excluyen renovaciones y SAC).
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|