informe-comercial-alfa/app/tendencia/page.js

116 lines
3.9 KiB
JavaScript
Raw Permalink Normal View History

2026-09-21 23:11:53 +00:00
import { auth } from "../lib/auth";
import { ownerFilter } from "../lib/authz";
import { getClosed, resolveScopeOwnerId } from "../lib/queries";
import LoginPrompt from "../components/LoginPrompt";
import ErrorNotice from "../components/ErrorNotice";
import { money, money2 } from "../lib/constants";
import { getMetaGlobal } from "../lib/metas";
export const dynamic = "force-dynamic";
const MESES_A_MOSTRAR = 6;
function lastMonths(n) {
const out = [];
const d = new Date();
for (let i = n - 1; i >= 0; i--) {
const dt = new Date(d.getFullYear(), d.getMonth() - i, 1);
out.push(`${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, "0")}`);
}
return out;
}
export default async function TendenciaPage({ searchParams }) {
const session = await auth();
if (!session?.email) return <LoginPrompt />;
const { owner } = await searchParams;
const scope = ownerFilter(session, owner);
const meses = lastMonths(MESES_A_MOSTRAR);
const metaGlobal = getMetaGlobal();
let porMes;
try {
const { ownerId } = await resolveScopeOwnerId(scope, session.accessToken);
porMes = await Promise.all(
meses.map(async (m) => {
const [won, lost] = await Promise.all([
getClosed(session.accessToken, scope, 1, m, ownerId),
getClosed(session.accessToken, scope, 2, m, ownerId),
]);
const sumW = won.reduce((a, o) => a + (o.actual || 0), 0);
return {
mes: m,
won: won.length,
lost: lost.length,
sumW,
arpu: won.length ? sumW / won.length : 0,
conv: won.length + lost.length ? Math.round((won.length / (won.length + lost.length)) * 100) : 0,
cumpl: metaGlobal ? Math.round((sumW / metaGlobal) * 100) : null,
};
})
);
} catch (error) {
return <ErrorNotice error={error} />;
}
const maxSumW = Math.max(...porMes.map((m) => m.sumW), 1);
return (
<div style={{ padding: 24 }}>
<h2>Tendencia</h2>
<p className="hint" style={{ marginBottom: 16 }}>
Últimos {MESES_A_MOSTRAR} meses, calculado en vivo desde Dynamics. No incluye el análisis narrativo del
tablero anterior (era específico de un semestre puntual) solo las cifras.
</p>
<div className="panel">
<h3>Ganadas ($) por mes</h3>
{porMes.map((m) => (
<div className="row static" key={m.mes}>
<div className="bl">
<span className="nm">{m.mes}</span>
</div>
<div className="bar-cell">
<div className="bar-fill" style={{ width: `${((m.sumW / maxSumW) * 100).toFixed(1)}%` }} />
</div>
<span className="v">{m.won}</span>
<span className="c">{money(m.sumW)}</span>
</div>
))}
</div>
<div className="tw" style={{ marginTop: 18 }}>
<div className="ts">
<table>
<thead>
<tr>
<th>Mes</th>
<th className="num">Ganadas #</th>
<th className="num">Ganadas $</th>
<th className="num">ARPU</th>
<th className="num">Perdidas #</th>
<th className="num">Conversión</th>
<th className="num">Cumpl. (meta {money(metaGlobal)})</th>
</tr>
</thead>
<tbody>
{porMes.map((m) => (
<tr key={m.mes}>
<td>{m.mes}</td>
<td className="num">{m.won}</td>
<td className="num">{money2(m.sumW)}</td>
<td className="num">{money2(m.arpu)}</td>
<td className="num">{m.lost}</td>
<td className="num">{m.conv}%</td>
<td className="num">{m.cumpl != null ? `${m.cumpl}%` : "n/d"}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}