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

140 lines
4.7 KiB
JavaScript
Raw Normal View History

2026-09-21 23:11:53 +00:00
import Link from "next/link";
import { auth } from "../lib/auth";
import { ownerFilter } from "../lib/authz";
import { getPipeline, getClosed } from "../lib/queries";
import LoginPrompt from "../components/LoginPrompt";
import ErrorNotice from "../components/ErrorNotice";
import { DYN, ACT_COLOR, 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")}`;
}
const SETS = [
{ key: "pipe", label: "Tubería activa" },
{ key: "ganadas", label: "Ganadas" },
{ key: "perdidas", label: "Perdidas" },
];
export default async function DetallePage({ searchParams }) {
const session = await auth();
if (!session?.email) return <LoginPrompt />;
const { owner, set } = await searchParams;
const scope = ownerFilter(session, owner);
const detSet = SETS.some((s) => s.key === set) ? set : "pipe";
const mes = currentMonth();
let rows;
try {
if (detSet === "pipe") rows = await getPipeline(session.accessToken, scope);
else if (detSet === "ganadas") rows = await getClosed(session.accessToken, scope, 1, mes);
else rows = await getClosed(session.accessToken, scope, 2, mes);
} catch (error) {
return <ErrorNotice error={error} />;
}
const sorted = [...rows].sort((a, b) => (b.est ?? b.actual ?? 0) - (a.est ?? a.actual ?? 0));
const qs = (key) => {
const p = new URLSearchParams();
if (owner) p.set("owner", owner);
p.set("set", key);
return `/detalle?${p.toString()}`;
};
return (
<div style={{ padding: 24 }}>
<h2>Detalle</h2>
<div className="seg-tabs" style={{ marginBottom: 16 }}>
{SETS.map((s) => (
<Link key={s.key} href={qs(s.key)} className={detSet === s.key ? "on" : ""}>
<button className={detSet === s.key ? "on" : ""} type="button">
{s.label}
</button>
</Link>
))}
</div>
<p className="hint" style={{ marginBottom: 12 }}>
{rows.length} oportunidades{detSet !== "pipe" ? ` (${mes})` : ""} · clic en el nombre abre la oportunidad en
Dynamics 365.
</p>
{sorted.length === 0 ? (
<p>No hay oportunidades para este filtro.</p>
) : (
<div className="tw">
<div className="ts">
<table>
<thead>
<tr>
<th>Cliente</th>
<th>Oportunidad</th>
<th>Seg.</th>
{!scope.locked && <th>Ejecutivo</th>}
{detSet === "pipe" ? (
<>
<th>Plan cierre</th>
<th>Fase actual</th>
<th>Actividad</th>
<th className="num">MRR est.</th>
<th className="num">Forecast</th>
</>
) : (
<>
<th>Tipo</th>
<th className="num">MRR</th>
<th>Cierre</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>
{!scope.locked && <td>{o.owner}</td>}
{detSet === "pipe" ? (
<>
<td>{(o.plan || "").replace(" - ", " ")}</td>
<td>{o.faseActual || "—"}</td>
<td>
<span style={{ color: ACT_COLOR[o.actividad] || "#9db4c9", fontWeight: 700 }}>
{o.actividad}
</span>
</td>
<td className="num">{money2(o.est)}</td>
<td className="num">{money2(o.fc)}</td>
</>
) : (
<>
<td>{o.tipo}</td>
<td className="num">{money2(o.actual)}</td>
<td>{o.actualClose || "—"}</td>
</>
)}
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
);
}