124 lines
4.5 KiB
JavaScript
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>
|
|
);
|
|
}
|