"use client"; import type { ClaimDTO, HistoryPoint, ProvenanceDTO } from "@dci/core"; import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react"; import { cn } from "@/components/ui/cn"; import { fieldLabel } from "@/lib/field-labels"; import { EvidenceDrawer } from "./evidence-drawer"; /* ------------------------------------------------------------------------------------------ Evidence context: one provider per entity page holding claims + provenance + history; any figure wrapped in opens the drawer on that field. Server pages render the provider with serialisable props; the wrapper and the drawer are the only client pieces. ------------------------------------------------------------------------------------------ */ export interface EvidenceData { entityName: string; entityKind: "facility" | "project" | "operator"; claims: ClaimDTO[]; provenance: ProvenanceDTO[]; history: HistoryPoint[]; } export interface EvidenceTarget { field: string; fields: string[]; label: string; display?: string; mode: "evidence" | "history"; } interface Ctx extends EvidenceData { open: (t: Omit & { mode?: EvidenceTarget["mode"] }) => void; close: () => void; target: EvidenceTarget | null; hasEvidence: (fields: string[]) => boolean; } const EvidenceCtx = createContext(null); /** Field name → every alias it may appear under in claims (predicates) and provenance (camel / snake). */ export function fieldAliases(field: string): string[] { const snake = field.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`); const camel = field.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase()); const set = new Set([field, snake, camel]); const preds = PREDICATES_BY_FIELD[camel] ?? PREDICATES_BY_FIELD[field]; if (preds) for (const p of preds) set.add(p); return Array.from(set); } /** Column ↔ claim predicates (mirrors CAPACITY_COLUMN in @dci/core, plus investment / status). */ export const PREDICATES_BY_FIELD: Record = { itCapacityMw: ["it_capacity_mw", "critical_power_mw"], totalPowerMw: ["current_power_mw"], plannedPowerMw: ["planned_power_mw", "phase_mw"], plannedMw: ["planned_power_mw", "phase_mw"], utilityCapacityMw: ["utility_capacity_mw"], gridConnectionMw: ["grid_connection_mw"], ultimateCampusMw: ["ultimate_campus_mw"], investmentUsd: ["project_investment_usd", "campus_investment_usd", "company_investment_usd", "country_program_usd", "multi_year_capex_usd", "deal_value_usd"], status: ["status"], }; export function EvidenceProvider({ data, children }: { data: EvidenceData; children: ReactNode }) { const [target, setTarget] = useState(null); const open = useCallback((t: Omit & { mode?: EvidenceTarget["mode"] }) => setTarget({ ...t, mode: t.mode ?? "evidence" }), []); const close = useCallback(() => setTarget(null), []); const index = useMemo(() => { const s = new Set(); for (const c of data.claims) s.add(c.predicate); for (const p of data.provenance) s.add(p.field); for (const h of data.history) { s.add(h.field); if (h.predicate) s.add(h.predicate); } return s; }, [data]); const hasEvidence = useCallback((fields: string[]) => fields.some((f) => fieldAliases(f).some((a) => index.has(a))), [index]); const value = useMemo(() => ({ ...data, open, close, target, hasEvidence }), [data, open, close, target, hasEvidence]); return ( {children} ); } export function useEvidence(): Ctx | null { return useContext(EvidenceCtx); } export interface EvidenceProps { /** primary field (camelCase API name) */ field: string; /** extra aliases (e.g. ["plannedMw", "planned_power_mw"]) */ fields?: string[]; /** label shown in the drawer header (defaults from the field) */ label?: string; /** preformatted display of the current value for the drawer header */ display?: string; /** open straight on the history list */ history?: boolean; children: ReactNode; className?: string; /** render as block (metric cells) rather than inline */ block?: boolean; /** show the wrapper even when no evidence exists (renders children plain, no button) */ plainWhenEmpty?: boolean; } /** * Click target around an important figure. Renders a real ); }