informe-comercial-alfa/app/tuberia/page.js
2026-09-21 17:11:53 -06:00

124 lines
4.5 KiB
JavaScript

import { auth } from "../lib/auth";
import { ownerFilter } from "../lib/authz";
import { getPipeline, getSystemUsers } from "../lib/queries";
import LoginPrompt from "../components/LoginPrompt";
import OwnerControl from "../components/OwnerControl";
import ErrorNotice from "../components/ErrorNotice";
import { DYN, ACT_COLOR, money, money2 } from "../lib/constants";
import { EJECUTIVOS_VENTA } from "../lib/derive";
export const dynamic = "force-dynamic";
export default async function TuberiaPage({ searchParams }) {
const session = await auth();
if (!session?.email) return <LoginPrompt />;
const { owner } = await searchParams;
const scope = ownerFilter(session, owner);
let rows, allUsers;
try {
[rows, allUsers] = await Promise.all([
getPipeline(session.accessToken, scope),
session.isAdmin ? getSystemUsers(session.accessToken) : Promise.resolve([]),
]);
} catch (error) {
return <ErrorNotice error={error} />;
}
const owners = allUsers.filter((u) => EJECUTIVOS_VENTA.includes(u.name));
const sorted = [...rows].sort((a, b) => b.est - a.est);
const totalEst = rows.reduce((a, r) => a + r.est, 0);
const totalFc = rows.reduce((a, r) => a + r.fc, 0);
const estancadas = rows.filter((r) => r.estanc).length;
return (
<div style={{ padding: 24 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 12 }}>
<h2>Tubería Activa</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">{rows.length}</span>
<span className="l">Oportunidades activas</span>
</div>
<div className="kpi">
<span className="n">{money(totalEst)}</span>
<span className="l">Estimado total</span>
</div>
<div className="kpi">
<span className="n">{money(totalFc)}</span>
<span className="l">Forecast ponderado</span>
</div>
<div className="kpi">
<span className="n pierde">{estancadas}</span>
<span className="l">Estancadas</span>
</div>
</div>
{sorted.length === 0 ? (
<p>No hay oportunidades activas para este filtro.</p>
) : (
<div className="tw">
<div className="ts">
<table>
<thead>
<tr>
<th>Cliente</th>
<th>Oportunidad</th>
<th>Seg.</th>
<th>Plan cierre</th>
<th>Línea de Negocio</th>
<th>Fase actual</th>
<th className="num">Días en fase</th>
<th>Actividad</th>
{!scope.locked && <th>Ejecutivo</th>}
<th className="num">MRR est.</th>
<th className="num">Forecast</th>
<th>Cierre est.</th>
</tr>
</thead>
<tbody>
{sorted.map((o) => (
<tr key={o.id}>
<td>{o.cliente || "—"}</td>
<td>
<a className="opp" href={`${DYN}${o.id}`} target="_blank" rel="noopener">
{o.name}
</a>
</td>
<td>
<span className={`pill ${o.segmento === "Cartera Activa" ? "cartera" : "nuevo"}`}>
{o.segmento === "Cartera Activa" ? "CA" : "CN"}
</span>
</td>
<td>{(o.plan || "").replace(" - ", " ")}</td>
<td>{o.linea || "—"}</td>
<td>{o.faseActual || "—"}</td>
<td className="num">{o.diasEnFase == null ? "—" : o.diasEnFase}</td>
<td>
<span style={{ color: ACT_COLOR[o.actividad] || "#9db4c9", fontWeight: 700 }}>
{o.actividad}
</span>
</td>
{!scope.locked && <td>{o.owner}</td>}
<td className="num">{money2(o.est)}</td>
<td className="num">{money2(o.fc)}</td>
<td>{o.close || "—"}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
);
}