139 lines
4.7 KiB
JavaScript
139 lines
4.7 KiB
JavaScript
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>
|
|
);
|
|
}
|