spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
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