SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
4 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
4.1 KB · 76 lines tsx
Raw Blame History
1import type { DataQualitySummary } from "@dci/core";2import { AlertTriangle, Info, ShieldAlert } from "lucide-react";3import Link from "next/link";4import { cn } from "@/components/ui/cn";5import { fmtDate, fmtInt } from "@/lib/format";6import { fieldLabel } from "@/lib/field-labels";7import { routes } from "@/lib/routes";89function Cell({ label, value, note }: { label: string; value: string; note?: string }) {10  return (11    <div className="min-w-0 border-b border-hair py-2 pr-3 sm:border-b-0 sm:border-r sm:last:border-r-0">12      <div className="figure text-[18px] font-medium leading-none text-ink">{value}</div>13      <div className="label-caps mt-1">{label}</div>14      {note && <div className="text-[11px] text-ink-4">{note}</div>}15    </div>16  );17}1819const SEV_ICON = { info: Info, warn: AlertTriangle, critical: ShieldAlert } as const;2021/** DATA QUALITY block: sources, completeness, claims (current / unscoped / review), open flags, duplicate review. */22export function DataQualityPanel({ q, completenessNote }: { q: DataQualitySummary; completenessNote?: string }) {23  return (24    <div className="flex flex-col gap-4">25      <div className="grid grid-cols-2 gap-x-3 sm:grid-cols-6">26        <Cell label="Sources" value={fmtInt(q.sourceCount)} note={`${fmtInt(q.primarySourceCount)} primary`} />27        <Cell label="Completeness" value={`${q.completeness}%`} note={completenessNote ?? `${fmtInt(q.fieldsWithProvenance)} fields sourced`} />28        <Cell label="Claims current" value={fmtInt(q.claimsCurrent)} note={`${fmtInt(q.claimsTotal)} total`} />29        <Cell label="Not site-scoped" value={fmtInt(q.claimsUnscoped)} note="stored, never summed" />30        <Cell label="In review" value={fmtInt(q.claimsInReview)} note="sanity flags" />31        <Cell label="Last verified" value={q.lastVerified ? fmtDate(q.lastVerified) : "—"} note={q.lastVerified ? "primary source" : "no primary verification"} />32      </div>33      <div>34        <div className="mb-1.5 flex items-baseline justify-between">35          <h3 className="text-[13px] font-semibold text-ink">Open quality flags</h3>36          <span className="figure text-[11.5px] text-ink-3">{fmtInt(q.openFlags.length)}</span>37        </div>38        {q.openFlags.length === 0 && !q.pendingDuplicate ? (39          <p className="text-[12.5px] text-ink-3">No open flag. Automated sanity checks (scope, density, magnitude, 5× change) passed for every current figure.</p>40        ) : (41          <ul className="divide-y divide-hair">42            {q.pendingDuplicate && (43              <li className="flex items-start gap-2 py-2 text-[12.5px]">44                <Info className="mt-0.5 size-3.5 shrink-0 text-ink-3" aria-hidden />45                <span className="text-ink-2">A possible duplicate of this record is pending human review; figures are not merged until it is resolved.</span>46              </li>47            )}48            {q.openFlags.map((f, i) => {49              const Icon = SEV_ICON[f.severity];50              return (51                <li key={`${f.code}-${i}`} className="flex items-start gap-2 py-2 text-[12.5px]">52                  <Icon className={cn("mt-0.5 size-3.5 shrink-0", f.severity === "critical" ? "text-danger" : f.severity === "warn" ? "text-warn" : "text-ink-3")} aria-label={f.severity} />53                  <div className="min-w-0">54                    <span className="text-ink-2">{f.message}</span>55                    <div className="mt-0.5 flex flex-wrap gap-x-2 text-[11px] text-ink-4">56                      <span className="figure">{f.code}</span>57                      {f.field && <span>· {fieldLabel(f.field)}</span>}58                      <span>· {f.severity}</span>59                    </div>60                  </div>61                </li>62              );63            })}64          </ul>65        )}66        <p className="mt-2 text-[11px] text-ink-4">67          Flags are opened automatically by the sanity engines and closed by review; a critical flag keeps a figure visible but marked.{" "}68          <Link href={`${routes.methodology()}#quality`} className="underline decoration-hair-2 hover:text-ink">69            How quality is scored70          </Link>71        </p>72      </div>73    </div>74  );75}76