import type { FacilityStatus } from "@dci/core"; import Link from "next/link"; import { cn } from "@/components/ui/cn"; import { StatusDot } from "@/components/ui/status-dot"; import { fmtInt, splitMw } from "@/lib/format"; import { routes } from "@/lib/routes"; export interface FunnelInput { /** project counts + published MW by status */ byStatus: Array<{ status: FacilityStatus; count: number; mw: number | null }>; /** facilities opened in the last 12 months (from a pulse / velocity window) — null when the API does not expose it */ openedLast12m?: { count: number; mw: number | null } | null; } const STEPS: Array<{ key: string; label: string; statuses: FacilityStatus[]; tone: FacilityStatus }> = [ { key: "announced", label: "Announced", statuses: ["rumored", "proposed", "announced", "expansion"], tone: "announced" }, { key: "permitting", label: "Permitting", statuses: ["permitting"], tone: "permitting" }, { key: "approved", label: "Approved", statuses: ["approved"], tone: "approved" }, { key: "construction", label: "Construction", statuses: ["under_construction", "partially_operational"], tone: "under_construction" }, ]; /** * Construction pipeline funnel (§33): ANNOUNCED → PERMITTING → APPROVED → CONSTRUCTION → OPENED, each with project * count and the sum of published MW (GW when ≥ 1 000). Bars are proportional to the count; MW are labelled * "known published MW only" because most projects do not publish a figure. */ export function PipelineFunnel({ data, className, compact }: { data: FunnelInput; className?: string; compact?: boolean }) { const agg = STEPS.map((s) => { const rows = data.byStatus.filter((r) => s.statuses.includes(r.status)); return { ...s, count: rows.reduce((a, r) => a + r.count, 0), mw: rows.some((r) => r.mw !== null) ? rows.reduce((a, r) => a + (r.mw ?? 0), 0) : null }; }); const opened = data.openedLast12m ?? null; const delayed = data.byStatus.filter((r) => r.status === "delayed" || r.status === "cancelled"); const max = Math.max(1, ...agg.map((a) => a.count), opened?.count ?? 0); const cells = [...agg.map((a) => ({ key: a.key, label: a.label, count: a.count, mw: a.mw, tone: a.tone as FacilityStatus, href: routes.projects(`status=${a.statuses.join(",")}`) })), { key: "opened", label: "Opened · 12 m", count: opened?.count ?? null, mw: opened?.mw ?? null, tone: "operational" as FacilityStatus, href: routes.datacenters("status=operational&sort=opened") }]; return (
    {cells.map((c) => { const s = splitMw(c.mw); const pct = c.count === null ? 0 : Math.max(2, (c.count / max) * 100); return (
  1. {c.label}
    {c.count === null ? — : fmtInt(c.count)}
    {c.count === null ? "not tracked as projects" : s ? `${s.value} ${s.unit}` : c.count > 0 ? "no MW published" : "—"}
  2. ); })}
Known published MW only {delayed.length > 0 && ( {delayed.map((d, i) => ( {i > 0 && " · "} {fmtInt(d.count)} {d.status} ))} )}
); }