SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
4.9 KB · 98 lines tsx
Raw Blame History
1import type { ClaimDTO } from "@dci/core";2import { ExternalLink } from "lucide-react";3import { ConfidenceBadge, EstimateMark, ScopeBadge, SemanticsLabel, TierMark } from "@/components/ui/badge";4import { SourceBadge } from "@/components/ui/source-badge";5import { TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";6import { fmtDate, fmtValue } from "@/lib/format";7import { CLAIM_STATUS_LABEL, isSiteScopeLabel } from "@/lib/labels";8import { Evidence } from "./evidence-context";910/**11 * Every capacity / investment claim on a record: predicate · scope · value · source · published · confidence · tier.12 * Site-scoped claims come first; non-site claims (portfolio, company, metro…) are grouped under13 * "Figures not assigned to this site" so they are never read as the record's own MW.14 */15export function ClaimsTable({ claims, kind = "capacity", emptyNote }: { claims: ClaimDTO[]; kind?: "capacity" | "investment" | "all"; emptyNote?: string }) {16  const rows = claims.filter((c) => (kind === "all" ? true : kind === "capacity" ? /_mw$/.test(c.predicate) : /_usd$/.test(c.predicate)));17  if (!rows.length) return <p className="rounded-sm border border-dashed border-hair-2 px-3 py-2.5 text-[12.5px] text-ink-3">{emptyNote ?? `No ${kind === "all" ? "" : `${kind} `}claim has been extracted for this record yet — the figures above come from structured source fields (see Source evidence).`}</p>;18  const site = rows.filter((c) => isSiteScopeLabel(c.scope));19  const other = rows.filter((c) => !isSiteScopeLabel(c.scope));20  return (21    <div className="flex flex-col gap-4">22      {site.length > 0 && <Rows rows={site} />}23      {other.length > 0 && (24        <div>25          <div className="mb-1.5 flex flex-wrap items-baseline gap-x-2">26            <h3 className="text-[13px] font-semibold text-ink">Figures not assigned to this site</h3>27            <span className="text-[11.5px] text-ink-3">portfolio, company, metro, country or unknown scope — shown as evidence, never summed into this record</span>28          </div>29          <Rows rows={other} />30        </div>31      )}32    </div>33  );34}3536function Rows({ rows }: { rows: ClaimDTO[] }) {37  const sorted = [...rows].sort((a, b) => Number(b.isWinner) - Number(a.isWinner) || a.predicate.localeCompare(b.predicate) || ((a.publishedAt ?? a.retrievedAt) < (b.publishedAt ?? b.retrievedAt) ? 1 : -1));38  return (39    <Table dense>40      <THead>41        <tr>42          <Th>Figure</Th>43          <Th>Scope</Th>44          <Th numeric>Value</Th>45          <Th>Source</Th>46          <Th className="hidden lg:table-cell">Published</Th>47          <Th>Confidence</Th>48          <Th className="hidden md:table-cell">Tier</Th>49          <Th className="hidden md:table-cell">Status</Th>50        </tr>51      </THead>52      <TBody>53        {sorted.map((c) => (54          <Tr key={c.id} className={c.status !== "current" ? "text-ink-3" : undefined}>55            <Td primary>56              <span className="inline-flex flex-wrap items-center gap-1.5">57                <SemanticsLabel semantics={c.predicate} className="text-[12.5px] text-ink" />58                {c.isWinner && <span className="inline-flex h-[16px] items-center rounded-xs border border-accent/40 bg-accent-soft px-1 text-[9.5px] font-medium uppercase tracking-[0.06em] text-accent-ink">shown</span>}59              </span>60            </Td>61            <Td label="Scope">62              <ScopeBadge scope={c.scope} />63            </Td>64            <Td label="Value" numeric lead>65              <Evidence field={c.predicate} label={c.label}>66                <span className="figure">67                  {c.isEstimate && <EstimateMark className="mr-0.5" />}68                  {c.value !== null ? fmtValue(c.value, c.unit === "USD" ? "usd" : "mw") : (c.valueText ?? "—")}69                </span>70              </Evidence>71            </Td>72            <Td label="Source">73              <span className="inline-flex flex-wrap items-center gap-1.5">74                <SourceBadge kind={c.sourceKind} name={c.sourceName} url={c.url} publishedAt={c.publishedAt} retrievedAt={c.retrievedAt} documentId={c.documentId} />75                <a href={c.url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-[12px] text-ink-2 hover:text-accent">76                  {c.sourceName} <ExternalLink className="size-3" />77                </a>78              </span>79            </Td>80            <Td label="Published" className="figure hidden text-[12px] text-ink-3 lg:table-cell">81              {c.publishedAt ? fmtDate(c.publishedAt) : "—"}82            </Td>83            <Td label="Confidence">84              <ConfidenceBadge level={c.confidence} size="xs" />85            </Td>86            <Td label="Tier" className="hidden md:table-cell">87              <TierMark tier={c.authorityTier} />88            </Td>89            <Td label="Status" className="hidden text-[12px] md:table-cell">90              {CLAIM_STATUS_LABEL[c.status]}91            </Td>92          </Tr>93        ))}94      </TBody>95    </Table>96  );97}98