From 2d6e39eebc9742bac1992b970b31748b777b6088 Mon Sep 17 00:00:00 2001 From: Tudor Sitaru Date: Wed, 8 Apr 2026 10:44:37 +0100 Subject: [PATCH] fix(school-detail): hero Ofsted chip mislabels OEIF schools as Report Card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The API returns ``framework`` as the literal string "NULL" for older OEIF inspections (it comes from the upstream ``event_type_grouping`` column), not real null. The original render path checks ``=== 'ReportCard'`` and correctly treats anything else as OEIF — but buildOfstedHeroChip inverted that and treated anything not exactly equal to ``'OEIF'`` as Report Card, so OLQH (inspected Nov 2023, Outstanding) was being labelled as a Report Card school in the hero strip and the at-a-glance tile. - Invert the helper: only branch into Report Card when framework is explicitly ``'ReportCard'``; treat OEIF / null / "NULL" / anything else as OEIF, and require ``overall_effectiveness`` to render the grade word. - Replace the toneClass field (which reused .ofstedGrade{N} / .rcGrade{N} badge classes and dragged in their backgrounds) with a clean tone enum ``teal | green | gold | coral | neutral``. The serif Ofsted heroStat picked up the badge background and rendered as a green box around "Report Card" — gone now. - Hero chip backgrounds use color-mix() against the tone variable so all five tones share one rule. Co-Authored-By: Claude Opus 4.6 --- .../components/SchoolDetailView.module.css | 62 +++++++---------- nextjs-app/components/SchoolDetailView.tsx | 10 +-- nextjs-app/lib/utils.ts | 68 +++++++++++-------- 3 files changed, 70 insertions(+), 70 deletions(-) diff --git a/nextjs-app/components/SchoolDetailView.module.css b/nextjs-app/components/SchoolDetailView.module.css index 779695a..4456fb4 100644 --- a/nextjs-app/components/SchoolDetailView.module.css +++ b/nextjs-app/components/SchoolDetailView.module.css @@ -756,38 +756,26 @@ margin-top: 0.15rem; } -/* Tone variants — keep consistent with .ofstedGrade{N} / .rcGrade{N} */ -.heroChip.ofstedGrade1, -.heroChip.rcGrade1, -.heroChip.rcGrade2, -.heroChipGood { - background: var(--accent-teal-bg, rgba(45, 125, 125, 0.08)); - border-left-color: var(--accent-teal, #2d7d7d); +/* Hero tone scheme — independent of the .ofstedGrade{N} / .rcGrade{N} badges + so the same tone class can be applied to a chip (background tint + border) + or a serif number (colour only) without one bleeding into the other. */ +.tone-teal { --hero-tone: var(--accent-teal, #2d7d7d); } +.tone-green { --hero-tone: #3c8c3c; } +.tone-gold { --hero-tone: var(--accent-gold, #c9a227); } +.tone-coral { --hero-tone: var(--accent-coral, #e07256); } +.tone-neutral { --hero-tone: var(--text-muted, #8a847a); } + +.heroChip.tone-teal, +.heroChip.tone-green, +.heroChip.tone-gold, +.heroChip.tone-coral, +.heroChip.tone-neutral { + border-left-color: var(--hero-tone); + background: color-mix(in srgb, var(--hero-tone) 10%, var(--bg-card, white)); } -.heroChip.ofstedGrade2 { - background: rgba(60, 140, 60, 0.08); - border-left-color: #3c8c3c; -} - -.heroChip.ofstedGrade3, -.heroChip.rcGrade3, -.heroChipWarn { - background: var(--accent-gold-bg, rgba(201, 162, 39, 0.10)); - border-left-color: var(--accent-gold, #c9a227); -} - -.heroChip.ofstedGrade4, -.heroChip.rcGrade4, -.heroChip.rcGrade5 { - background: var(--accent-coral-bg, rgba(224, 114, 86, 0.10)); - border-left-color: var(--accent-coral, #e07256); -} - -.heroChip.heroChipNeutral { +.heroChip.tone-neutral { background: var(--bg-secondary, #f3ede4); - border-left-color: var(--border-color, #e5dfd5); - color: var(--text-muted, #8a847a); } /* ── Hero at-a-glance stats (A3) ─────────────────────────────────────── */ @@ -824,15 +812,13 @@ color: var(--text-primary, #1a1612); } -.heroStatNumberSerif.ofstedGrade1 { color: var(--accent-teal, #2d7d7d); } -.heroStatNumberSerif.ofstedGrade2 { color: #3c8c3c; } -.heroStatNumberSerif.ofstedGrade3 { color: #b8920e; } -.heroStatNumberSerif.ofstedGrade4 { color: var(--accent-coral, #e07256); } -.heroStatNumberSerif.rcGrade1 { color: var(--accent-teal, #2d7d7d); } -.heroStatNumberSerif.rcGrade2 { color: #3c8c3c; } -.heroStatNumberSerif.rcGrade3 { color: #b8920e; } -.heroStatNumberSerif.rcGrade4 { color: #c2410c; } -.heroStatNumberSerif.rcGrade5 { color: var(--accent-coral, #e07256); } +.heroStatNumberSerif.tone-teal, +.heroStatNumberSerif.tone-green, +.heroStatNumberSerif.tone-gold, +.heroStatNumberSerif.tone-coral, +.heroStatNumberSerif.tone-neutral { + color: var(--hero-tone); +} .heroStatLabel { font-size: 0.6875rem; diff --git a/nextjs-app/components/SchoolDetailView.tsx b/nextjs-app/components/SchoolDetailView.tsx index 596305c..5d9b910 100644 --- a/nextjs-app/components/SchoolDetailView.tsx +++ b/nextjs-app/components/SchoolDetailView.tsx @@ -213,7 +213,7 @@ export function SchoolDetailView({ {/* Hero signal chip strip */}
-
+
{ofstedHeroChip.title}
{ofstedHeroChip.subtitle}
{ofstedHeroChip.detail && ( @@ -222,7 +222,7 @@ export function SchoolDetailView({
{admissions?.oversubscribed && ( -
+
Oversubscribed
{admissions.first_preference_offer_pct != null @@ -233,7 +233,7 @@ export function SchoolDetailView({ )} {isPrimary && heroRwm != null && heroRwmNat != null && heroRwm > heroRwmNat && ( -
+
Above national average
Reading, Writing & Maths · {Math.round(heroRwm)} vs {Math.round(heroRwmNat)} @@ -242,7 +242,7 @@ export function SchoolDetailView({ )} {isSecondary && heroAtt8 != null && heroAtt8Nat != null && heroAtt8 > heroAtt8Nat && ( -
+
Above national average
Attainment 8 · {heroAtt8.toFixed(1)} vs {heroAtt8Nat.toFixed(1)} @@ -276,7 +276,7 @@ export function SchoolDetailView({ {ofsted && (
-
+
{ofstedHeroChip.state === 'oeif' ? ofstedHeroChip.title.replace(/^Ofsted\s+/, '') : ofstedHeroChip.state === 'reportCard' diff --git a/nextjs-app/lib/utils.ts b/nextjs-app/lib/utils.ts index 9c64ba8..2291399 100644 --- a/nextjs-app/lib/utils.ts +++ b/nextjs-app/lib/utils.ts @@ -423,59 +423,73 @@ function formatOfstedMonth(date: string | null | undefined): string { return d.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' }); } +export type HeroTone = 'teal' | 'green' | 'gold' | 'coral' | 'neutral'; + export interface OfstedHeroChip { state: 'oeif' | 'reportCard' | 'none'; title: string; // Main label (e.g. "Ofsted Outstanding", "Ofsted Report Card") subtitle: string; // Context line (e.g. "Inspected November 2023") detail?: string; // Optional extra line (e.g. "Safeguarding: Met") - toneClass: string; // CSS class key — matches .ofstedGrade{N} / .rcGrade{N} / neutral + tone: HeroTone; // Maps to dedicated hero tone classes (not badge classes) } /** * Build the hero-strip Ofsted chip, branching on the inspection framework. * Never synthesises a single overall grade for ReportCard schools. + * + * Note: the API may return ``framework`` as a literal string ``"NULL"`` for + * older inspections, so we explicitly only branch into the ReportCard layout + * when the value is exactly ``"ReportCard"``. Anything else with an + * ``overall_effectiveness`` score is treated as OEIF. */ export function buildOfstedHeroChip(ofsted: OfstedInspection | null | undefined): OfstedHeroChip { - if (!ofsted || !ofsted.framework) { + if (!ofsted) { return { state: 'none', title: 'Ofsted pending', subtitle: 'No inspection on record', - toneClass: 'heroChipNeutral', + tone: 'neutral', }; } const when = formatOfstedMonth(ofsted.inspection_date); - if (ofsted.framework === 'OEIF') { - const grade = ofsted.overall_effectiveness; - if (grade && OFSTED_OEIF_WORDS[grade]) { - return { - state: 'oeif', - title: `Ofsted ${OFSTED_OEIF_WORDS[grade]}`, - subtitle: when ? `Inspected ${when}` : 'Inspected', - toneClass: `ofstedGrade${grade}`, - }; - } + // ReportCard branch — only if the API explicitly says so + if (ofsted.framework === 'ReportCard') { + const safeguarding = ofsted.rc_safeguarding_met; return { - state: 'oeif', - title: 'Ofsted inspected', - subtitle: when ? `Inspected ${when}` : 'Inspection on record', - toneClass: 'heroChipNeutral', + state: 'reportCard', + title: 'Ofsted Report Card', + subtitle: when ? `Inspected ${when}` : 'New framework inspection', + detail: + safeguarding == null + ? undefined + : safeguarding ? 'Safeguarding: Met' : 'Safeguarding: Not met', + tone: safeguarding === false ? 'coral' : 'green', + }; + } + + // Otherwise treat as OEIF (covers framework === 'OEIF', null, "NULL", etc.) + const grade = ofsted.overall_effectiveness; + if (grade && OFSTED_OEIF_WORDS[grade]) { + const oeifTone: HeroTone = + grade === 1 ? 'teal' : + grade === 2 ? 'green' : + grade === 3 ? 'gold' : + 'coral'; + return { + state: 'oeif', + title: `Ofsted ${OFSTED_OEIF_WORDS[grade]}`, + subtitle: when ? `Inspected ${when}` : 'Inspected', + tone: oeifTone, }; } - // ReportCard — never fabricate an overall grade - const safeguarding = ofsted.rc_safeguarding_met; return { - state: 'reportCard', - title: 'Ofsted Report Card', - subtitle: when ? `Inspected ${when}` : 'New framework inspection', - detail: - safeguarding == null - ? undefined - : safeguarding ? 'Safeguarding: Met' : 'Safeguarding: Not met', - toneClass: safeguarding === false ? 'rcGrade5' : 'rcGrade2', + state: 'oeif', + title: 'Ofsted inspected', + subtitle: when ? `Inspected ${when}` : 'Inspection on record', + tone: 'neutral', }; }