116 lines
3.9 KiB
JavaScript
116 lines
3.9 KiB
JavaScript
|
|
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>
|
||
|
|
);
|
||
|
|
}
|