SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
8.7 KB · 158 lines tsx
Raw Blame History
1import Link from 'next/link';2import { Section, Note } from '@/components/ui/section';3import { EmptyState } from '@/components/ui/empty-state';4import { Freshness } from '@/components/ui/freshness';5import { Badge, ClaimBadge, StatusBadge } from '@/components/ui/badge';6import { SourceBadge } from '@/components/ui/source-badge';7import { countryTrialTotals, countrySitesByCancer, countrySitesByPhase, jurisdictionApprovalSummary, latestJurisdictionApprovals } from '@/lib/queries/country-activity';8import { fmtDate, fmtInt, fmtPct, phaseLabel, truncate } from '@/lib/format';910/** ISO 3166-1 alpha-2 codes that are also `drug_approvals.jurisdiction` codes. */11const JURISDICTIONS: Record<string, string> = { US: 'US', CA: 'CA', GB: 'UK', AU: 'AU', JP: 'JP', CH: 'CH' };1213/**14 * Country research & regulatory activity (SPEC §22): trial sites in the country from ClinicalTrials.gov15 * locations (derived, formula ci-trial-sites-v1) and approval records for the country's regulator.16 * Rendered on every country page, including those without epidemiology observations.17 */18export async function CountryActivity({ iso2, iso3, name, kind }: { iso2: string | null; iso3: string | null; name: string; kind: string }) {19  if (kind !== 'country' || !iso3) return null;20  const jurisdiction = iso2 ? JURISDICTIONS[iso2] ?? null : null;21  const [totals, byCancer, byPhase, approvalSummary, latest] = await Promise.all([countryTrialTotals(iso3), countrySitesByCancer(iso3, 25), countrySitesByPhase(iso3), jurisdiction ? jurisdictionApprovalSummary(jurisdiction) : Promise.resolve([]), jurisdiction ? latestJurisdictionApprovals(jurisdiction, 10) : Promise.resolve([])]);2223  return (24    <>25      <Section id="trial-activity" kicker="Clinical research" title={`Clinical trial activity in ${name}`} description="Registrant-entered study locations on ClinicalTrials.gov mapped to this country (any study type, any phase). A site is one location; a multi-site study weighs by its number of sites. Recruiting = location status RECRUITING, or the study status when the site has none." actions={<Link href={`/trials/map?recruiting=1`} className="ci-link">Trial map →</Link>}>26        {totals ? (27          <>28            <dl className="mb-3 grid grid-cols-2 gap-x-6 gap-y-2 text-[13.5px] sm:grid-cols-4">29              {[30                ['Sites', totals.sites],31                ['Studies with a site here', totals.trials],32                ['Recruiting sites', totals.recruiting_sites],33                ['Recruiting studies', totals.recruiting_trials],34              ].map(([k, v]) => (35                <div key={String(k)}>36                  <dt className="ci-kicker">{k}</dt>37                  <dd className="ci-num text-left font-display text-2xl">{fmtInt(v as number)}</dd>38                </div>39              ))}40            </dl>41            <div className="grid gap-6 lg:grid-cols-[3fr_2fr]">42              <div className="ci-table-wrap">43                <table className="ci-table">44                  <caption className="pb-1 text-[12.5px] text-ink-3">Top-level cancers by sites in {name} (descendants included)</caption>45                  <thead>46                    <tr>47                      <th>Cancer</th>48                      <th className="num">Sites</th>49                      <th className="num">Studies</th>50                      <th className="num">Recruiting studies</th>51                      <th className="num">Share of sites</th>52                    </tr>53                  </thead>54                  <tbody>55                    {byCancer.map((r) => (56                      <tr key={r.cancer_id}>57                        <td>58                          <Link href={`/cancer/${r.slug}/trials`} className="ci-link">59                            {r.canonical_name}60                          </Link>61                        </td>62                        <td className="num">{fmtInt(r.sites)}</td>63                        <td className="num">{fmtInt(r.trials)}</td>64                        <td className="num">{fmtInt(r.recruiting_trials)}</td>65                        <td className="num text-ink-3">{fmtPct(r.sites / totals.sites, 1)}</td>66                      </tr>67                    ))}68                  </tbody>69                </table>70              </div>71              <div className="ci-table-wrap">72                <table className="ci-table">73                  <caption className="pb-1 text-[12.5px] text-ink-3">By phase (a Phase II/III study counts in both)</caption>74                  <thead>75                    <tr>76                      <th>Phase</th>77                      <th className="num">Sites</th>78                      <th className="num">Studies</th>79                      <th className="num">Recruiting</th>80                    </tr>81                  </thead>82                  <tbody>83                    {byPhase.map((p) => (84                      <tr key={p.phase}>85                        <td>86                          <Link href={`/trials/map?phase=${p.phase}`} className="ci-link">87                            {phaseLabel(p.phase)}88                          </Link>89                        </td>90                        <td className="num">{fmtInt(p.sites)}</td>91                        <td className="num">{fmtInt(p.trials)}</td>92                        <td className="num">{fmtInt(p.recruiting_trials)}</td>93                      </tr>94                    ))}95                  </tbody>96                </table>97              </div>98            </div>99            <p className="mt-2 flex flex-wrap items-center gap-1.5 text-[11.5px] text-ink-3">100              <ClaimBadge kind="computed" /> <span>formula ci-trial-sites-v1 · source: clinicaltrials · shares are of sites, not of studies</span>101            </p>102            <Freshness dataUpdatedAt={totals.computed_at} extra="trial_site_country_counts" />103          </>104        ) : (105          <EmptyState compact title="No study location mapped to this country">Country aggregates exist for every country named in ClinicalTrials.gov locations; none names {name} yet, or the aggregate has not been computed.</EmptyState>106        )}107      </Section>108109      {jurisdiction ? (110        <Section id="approvals" kicker="Regulatory" title={`Oncology approval records · ${jurisdiction}`} description="Records of the country's regulator as ingested (one record = one decision for one application or product identifier). Absence of a record here is not evidence of absence." actions={<Link href={`/approvals?jurisdiction=${jurisdiction}`} className="ci-link">All records →</Link>}>111          {approvalSummary.length ? (112            <>113              <p className="mb-2 flex flex-wrap gap-1.5 text-[12px]">114                {approvalSummary.map((s) => (115                  <Badge key={`${s.authority}-${s.status}`} tone="outline" title={`${s.drugs} distinct drugs · latest dated ${s.latest ?? '—'}`}>116                    {s.authority} · {s.status}: {fmtInt(s.n)} records / {fmtInt(s.drugs)} drugs117                  </Badge>118                ))}119              </p>120              <ul className="divide-y divide-rule text-[13.5px]">121                {latest.map((a) => (122                  <li key={a.id} className="grid gap-x-3 py-1.5 sm:grid-cols-[92px_1fr]">123                    <span className="ci-mono text-[12px] text-ink-3">{a.approval_date ? fmtDate(a.approval_date) : 'undated'}</span>124                    <span className="min-w-0">125                      <Link href={`/drug/${a.drug_slug}`} className="ci-link font-medium">126                        {a.drug_name}127                      </Link>{' '}128                      <StatusBadge status={a.status} />{' '}129                      {a.cancer_slug ? (130                        <Link href={`/cancer/${a.cancer_slug}`} className="ci-link">131                          {a.cancer_name}132                        </Link>133                      ) : (134                        <span className="text-ink-3">cancer not stated in this record</span>135                      )}136                      <span className="mt-0.5 block text-[12.5px] text-ink-2" title={a.indication}>137                        {truncate(a.indication, 140)}138                      </span>139                      <SourceBadge p={{ sourceSlug: a.source_slug }} compact />140                    </span>141                  </li>142                ))}143              </ul>144              <p className="mt-2">145                <ClaimBadge kind="regulatory" />146              </p>147            </>148          ) : (149            <EmptyState compact title="No approval record for this jurisdiction yet">Regulatory connectors are live for the FDA (US), Health Canada (CA) and the EMA (EU). Other regulators are not yet ingested.</EmptyState>150          )}151        </Section>152      ) : (153        <Note>Regulatory records are ingested per jurisdiction (US, CA, EU so far); {name} has no connected regulator yet.</Note>154      )}155    </>156  );157}158