fix(school-detail): hero Ofsted chip mislabels OEIF schools as Report Card
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 13s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 45s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 13s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 45s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -756,38 +756,26 @@
|
|||||||
margin-top: 0.15rem;
|
margin-top: 0.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tone variants — keep consistent with .ofstedGrade{N} / .rcGrade{N} */
|
/* Hero tone scheme — independent of the .ofstedGrade{N} / .rcGrade{N} badges
|
||||||
.heroChip.ofstedGrade1,
|
so the same tone class can be applied to a chip (background tint + border)
|
||||||
.heroChip.rcGrade1,
|
or a serif number (colour only) without one bleeding into the other. */
|
||||||
.heroChip.rcGrade2,
|
.tone-teal { --hero-tone: var(--accent-teal, #2d7d7d); }
|
||||||
.heroChipGood {
|
.tone-green { --hero-tone: #3c8c3c; }
|
||||||
background: var(--accent-teal-bg, rgba(45, 125, 125, 0.08));
|
.tone-gold { --hero-tone: var(--accent-gold, #c9a227); }
|
||||||
border-left-color: var(--accent-teal, #2d7d7d);
|
.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 {
|
.heroChip.tone-neutral {
|
||||||
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 {
|
|
||||||
background: var(--bg-secondary, #f3ede4);
|
background: var(--bg-secondary, #f3ede4);
|
||||||
border-left-color: var(--border-color, #e5dfd5);
|
|
||||||
color: var(--text-muted, #8a847a);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Hero at-a-glance stats (A3) ─────────────────────────────────────── */
|
/* ── Hero at-a-glance stats (A3) ─────────────────────────────────────── */
|
||||||
@@ -824,15 +812,13 @@
|
|||||||
color: var(--text-primary, #1a1612);
|
color: var(--text-primary, #1a1612);
|
||||||
}
|
}
|
||||||
|
|
||||||
.heroStatNumberSerif.ofstedGrade1 { color: var(--accent-teal, #2d7d7d); }
|
.heroStatNumberSerif.tone-teal,
|
||||||
.heroStatNumberSerif.ofstedGrade2 { color: #3c8c3c; }
|
.heroStatNumberSerif.tone-green,
|
||||||
.heroStatNumberSerif.ofstedGrade3 { color: #b8920e; }
|
.heroStatNumberSerif.tone-gold,
|
||||||
.heroStatNumberSerif.ofstedGrade4 { color: var(--accent-coral, #e07256); }
|
.heroStatNumberSerif.tone-coral,
|
||||||
.heroStatNumberSerif.rcGrade1 { color: var(--accent-teal, #2d7d7d); }
|
.heroStatNumberSerif.tone-neutral {
|
||||||
.heroStatNumberSerif.rcGrade2 { color: #3c8c3c; }
|
color: var(--hero-tone);
|
||||||
.heroStatNumberSerif.rcGrade3 { color: #b8920e; }
|
}
|
||||||
.heroStatNumberSerif.rcGrade4 { color: #c2410c; }
|
|
||||||
.heroStatNumberSerif.rcGrade5 { color: var(--accent-coral, #e07256); }
|
|
||||||
|
|
||||||
.heroStatLabel {
|
.heroStatLabel {
|
||||||
font-size: 0.6875rem;
|
font-size: 0.6875rem;
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ export function SchoolDetailView({
|
|||||||
|
|
||||||
{/* Hero signal chip strip */}
|
{/* Hero signal chip strip */}
|
||||||
<div className={styles.heroChips}>
|
<div className={styles.heroChips}>
|
||||||
<div className={`${styles.heroChip} ${styles[ofstedHeroChip.toneClass] ?? ''}`}>
|
<div className={`${styles.heroChip} ${styles[`tone-${ofstedHeroChip.tone}`]}`}>
|
||||||
<div className={styles.heroChipTitle}>{ofstedHeroChip.title}</div>
|
<div className={styles.heroChipTitle}>{ofstedHeroChip.title}</div>
|
||||||
<div className={styles.heroChipSub}>{ofstedHeroChip.subtitle}</div>
|
<div className={styles.heroChipSub}>{ofstedHeroChip.subtitle}</div>
|
||||||
{ofstedHeroChip.detail && (
|
{ofstedHeroChip.detail && (
|
||||||
@@ -222,7 +222,7 @@ export function SchoolDetailView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{admissions?.oversubscribed && (
|
{admissions?.oversubscribed && (
|
||||||
<div className={`${styles.heroChip} ${styles.heroChipWarn}`}>
|
<div className={`${styles.heroChip} ${styles['tone-gold']}`}>
|
||||||
<div className={styles.heroChipTitle}>Oversubscribed</div>
|
<div className={styles.heroChipTitle}>Oversubscribed</div>
|
||||||
<div className={styles.heroChipSub}>
|
<div className={styles.heroChipSub}>
|
||||||
{admissions.first_preference_offer_pct != null
|
{admissions.first_preference_offer_pct != null
|
||||||
@@ -233,7 +233,7 @@ export function SchoolDetailView({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isPrimary && heroRwm != null && heroRwmNat != null && heroRwm > heroRwmNat && (
|
{isPrimary && heroRwm != null && heroRwmNat != null && heroRwm > heroRwmNat && (
|
||||||
<div className={`${styles.heroChip} ${styles.heroChipGood}`}>
|
<div className={`${styles.heroChip} ${styles['tone-teal']}`}>
|
||||||
<div className={styles.heroChipTitle}>Above national average</div>
|
<div className={styles.heroChipTitle}>Above national average</div>
|
||||||
<div className={styles.heroChipSub}>
|
<div className={styles.heroChipSub}>
|
||||||
Reading, Writing & Maths · {Math.round(heroRwm)} vs {Math.round(heroRwmNat)}
|
Reading, Writing & Maths · {Math.round(heroRwm)} vs {Math.round(heroRwmNat)}
|
||||||
@@ -242,7 +242,7 @@ export function SchoolDetailView({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isSecondary && heroAtt8 != null && heroAtt8Nat != null && heroAtt8 > heroAtt8Nat && (
|
{isSecondary && heroAtt8 != null && heroAtt8Nat != null && heroAtt8 > heroAtt8Nat && (
|
||||||
<div className={`${styles.heroChip} ${styles.heroChipGood}`}>
|
<div className={`${styles.heroChip} ${styles['tone-teal']}`}>
|
||||||
<div className={styles.heroChipTitle}>Above national average</div>
|
<div className={styles.heroChipTitle}>Above national average</div>
|
||||||
<div className={styles.heroChipSub}>
|
<div className={styles.heroChipSub}>
|
||||||
Attainment 8 · {heroAtt8.toFixed(1)} vs {heroAtt8Nat.toFixed(1)}
|
Attainment 8 · {heroAtt8.toFixed(1)} vs {heroAtt8Nat.toFixed(1)}
|
||||||
@@ -276,7 +276,7 @@ export function SchoolDetailView({
|
|||||||
|
|
||||||
{ofsted && (
|
{ofsted && (
|
||||||
<div className={styles.heroStat}>
|
<div className={styles.heroStat}>
|
||||||
<div className={`${styles.heroStatNumberSerif} ${styles[ofstedHeroChip.toneClass] ?? ''}`}>
|
<div className={`${styles.heroStatNumberSerif} ${styles[`tone-${ofstedHeroChip.tone}`]}`}>
|
||||||
{ofstedHeroChip.state === 'oeif'
|
{ofstedHeroChip.state === 'oeif'
|
||||||
? ofstedHeroChip.title.replace(/^Ofsted\s+/, '')
|
? ofstedHeroChip.title.replace(/^Ofsted\s+/, '')
|
||||||
: ofstedHeroChip.state === 'reportCard'
|
: ofstedHeroChip.state === 'reportCard'
|
||||||
|
|||||||
+41
-27
@@ -423,59 +423,73 @@ function formatOfstedMonth(date: string | null | undefined): string {
|
|||||||
return d.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' });
|
return d.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HeroTone = 'teal' | 'green' | 'gold' | 'coral' | 'neutral';
|
||||||
|
|
||||||
export interface OfstedHeroChip {
|
export interface OfstedHeroChip {
|
||||||
state: 'oeif' | 'reportCard' | 'none';
|
state: 'oeif' | 'reportCard' | 'none';
|
||||||
title: string; // Main label (e.g. "Ofsted Outstanding", "Ofsted Report Card")
|
title: string; // Main label (e.g. "Ofsted Outstanding", "Ofsted Report Card")
|
||||||
subtitle: string; // Context line (e.g. "Inspected November 2023")
|
subtitle: string; // Context line (e.g. "Inspected November 2023")
|
||||||
detail?: string; // Optional extra line (e.g. "Safeguarding: Met")
|
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.
|
* Build the hero-strip Ofsted chip, branching on the inspection framework.
|
||||||
* Never synthesises a single overall grade for ReportCard schools.
|
* 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 {
|
export function buildOfstedHeroChip(ofsted: OfstedInspection | null | undefined): OfstedHeroChip {
|
||||||
if (!ofsted || !ofsted.framework) {
|
if (!ofsted) {
|
||||||
return {
|
return {
|
||||||
state: 'none',
|
state: 'none',
|
||||||
title: 'Ofsted pending',
|
title: 'Ofsted pending',
|
||||||
subtitle: 'No inspection on record',
|
subtitle: 'No inspection on record',
|
||||||
toneClass: 'heroChipNeutral',
|
tone: 'neutral',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const when = formatOfstedMonth(ofsted.inspection_date);
|
const when = formatOfstedMonth(ofsted.inspection_date);
|
||||||
|
|
||||||
if (ofsted.framework === 'OEIF') {
|
// ReportCard branch — only if the API explicitly says so
|
||||||
const grade = ofsted.overall_effectiveness;
|
if (ofsted.framework === 'ReportCard') {
|
||||||
if (grade && OFSTED_OEIF_WORDS[grade]) {
|
const safeguarding = ofsted.rc_safeguarding_met;
|
||||||
return {
|
|
||||||
state: 'oeif',
|
|
||||||
title: `Ofsted ${OFSTED_OEIF_WORDS[grade]}`,
|
|
||||||
subtitle: when ? `Inspected ${when}` : 'Inspected',
|
|
||||||
toneClass: `ofstedGrade${grade}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
state: 'oeif',
|
state: 'reportCard',
|
||||||
title: 'Ofsted inspected',
|
title: 'Ofsted Report Card',
|
||||||
subtitle: when ? `Inspected ${when}` : 'Inspection on record',
|
subtitle: when ? `Inspected ${when}` : 'New framework inspection',
|
||||||
toneClass: 'heroChipNeutral',
|
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 {
|
return {
|
||||||
state: 'reportCard',
|
state: 'oeif',
|
||||||
title: 'Ofsted Report Card',
|
title: 'Ofsted inspected',
|
||||||
subtitle: when ? `Inspected ${when}` : 'New framework inspection',
|
subtitle: when ? `Inspected ${when}` : 'Inspection on record',
|
||||||
detail:
|
tone: 'neutral',
|
||||||
safeguarding == null
|
|
||||||
? undefined
|
|
||||||
: safeguarding ? 'Safeguarding: Met' : 'Safeguarding: Not met',
|
|
||||||
toneClass: safeguarding === false ? 'rcGrade5' : 'rcGrade2',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user