= {
@@ -134,6 +138,20 @@ export function SchoolDetailView({
if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' });
if (yearlyData.length > 0) navItems.push({ id: 'history', label: 'History' });
+ // ── Hero: framework-aware signal chip + narrative summary ─────────────
+ const ofstedHeroChip = buildOfstedHeroChip(ofsted);
+ const heroSummary = buildSchoolSummary(schoolInfo, ofsted, admissions, latestResults);
+
+ // KS2 headline numbers for the at-a-glance row
+ const heroRwm = isPrimary ? latestResults?.rwm_expected_pct ?? null : null;
+ const heroRwmNat = primaryAvg.rwm_expected_pct ?? null;
+
+ // KS4 headline number for secondary/all-through schools
+ const heroAtt8 = isSecondary ? latestResults?.attainment_8_score ?? null : null;
+ const heroAtt8Nat = secondaryAvg.attainment_8_score ?? null;
+
+ const heroAcademicYear = latestResults ? formatAcademicYear(latestResults.year) : '';
+
return (
{/* Header */}
@@ -192,6 +210,110 @@ export function SchoolDetailView({
+
+ {/* Hero signal chip strip */}
+
+
+
{ofstedHeroChip.title}
+
{ofstedHeroChip.subtitle}
+ {ofstedHeroChip.detail && (
+
{ofstedHeroChip.detail}
+ )}
+
+
+ {admissions?.oversubscribed && (
+
+
Oversubscribed
+
+ {admissions.first_preference_offer_pct != null
+ ? `${Math.round(admissions.first_preference_offer_pct)}% got first choice`
+ : 'More applicants than places'}
+
+
+ )}
+
+ {isPrimary && heroRwm != null && heroRwmNat != null && heroRwm > heroRwmNat && (
+
+
Above national average
+
+ Reading, Writing & Maths · {Math.round(heroRwm)} vs {Math.round(heroRwmNat)}
+
+
+ )}
+
+ {isSecondary && heroAtt8 != null && heroAtt8Nat != null && heroAtt8 > heroAtt8Nat && (
+
+
Above national average
+
+ Attainment 8 · {heroAtt8.toFixed(1)} vs {heroAtt8Nat.toFixed(1)}
+
+
+ )}
+
+
+ {/* At-a-glance stats row */}
+ {latestResults && (
+
+ {isPrimary && heroRwm != null && (
+
+
{Math.round(heroRwm)}%
+
Reading, Writing & Maths
+ {heroRwmNat != null && (
+
+ )}
+
+ )}
+
+ {isSecondary && heroAtt8 != null && (
+
+
{heroAtt8.toFixed(1)}
+
Attainment 8 score
+ {heroAtt8Nat != null && (
+
+ )}
+
+ )}
+
+ {ofsted && (
+
+
+ {ofstedHeroChip.state === 'oeif'
+ ? ofstedHeroChip.title.replace(/^Ofsted\s+/, '')
+ : ofstedHeroChip.state === 'reportCard'
+ ? 'Report Card'
+ : '—'}
+
+
+ {ofstedHeroChip.subtitle}
+
+ {ofstedHeroChip.detail && (
+
{ofstedHeroChip.detail}
+ )}
+
+ )}
+
+ {admissions?.first_preference_offer_pct != null && (
+
+
+ {Math.round(admissions.first_preference_offer_pct)}%
+
+
First-choice offer rate
+ {admissions.oversubscribed && (
+
Oversubscribed
+ )}
+
+ )}
+
+ )}
+
+ {heroSummary && (
+ {heroSummary}
+ )}
+ {heroAcademicYear && (
+
+ Latest data: {heroAcademicYear}
+
+ )}
{/* Sticky Section Navigation */}
@@ -361,7 +483,17 @@ export function SchoolDetailView({
Reading, Writing & Maths combined
- {formatPercentage(latestResults.rwm_expected_pct)}
+
+ {formatPercentage(latestResults.rwm_expected_pct)}
+ {primaryAvg.rwm_expected_pct != null && (
+
+ )}
+
{primaryAvg.rwm_expected_pct != null && (
National avg: {primaryAvg.rwm_expected_pct.toFixed(0)}%
)}
@@ -373,7 +505,17 @@ export function SchoolDetailView({
Exceeding expected level (Reading, Writing & Maths)
- {formatPercentage(latestResults.rwm_high_pct)}
+
+ {formatPercentage(latestResults.rwm_high_pct)}
+ {primaryAvg.rwm_high_pct != null && (
+
+ )}
+
{primaryAvg.rwm_high_pct != null && (
National avg: {primaryAvg.rwm_high_pct.toFixed(0)}%
)}
diff --git a/nextjs-app/lib/utils.ts b/nextjs-app/lib/utils.ts
index f052b41..9c64ba8 100644
--- a/nextjs-app/lib/utils.ts
+++ b/nextjs-app/lib/utils.ts
@@ -2,7 +2,7 @@
* Utility functions for SchoolCompare
*/
-import type { School, MetricDefinition } from './types';
+import type { School, MetricDefinition, OfstedInspection, SchoolAdmissions, SchoolResult } from './types';
// ============================================================================
// String Utilities
@@ -404,3 +404,154 @@ export function getCurrentAcademicYear(): number {
// Academic year starts in September (month 8)
return month >= 8 ? year : year - 1;
}
+
+// ============================================================================
+// School Detail Hero Helpers
+// ============================================================================
+
+const OFSTED_OEIF_WORDS: Record = {
+ 1: 'Outstanding', 2: 'Good', 3: 'Requires Improvement', 4: 'Inadequate',
+};
+
+/**
+ * Format an Ofsted inspection date as "Month YYYY" (e.g. "November 2023").
+ */
+function formatOfstedMonth(date: string | null | undefined): string {
+ if (!date) return '';
+ const d = new Date(date);
+ if (Number.isNaN(d.getTime())) return '';
+ return d.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' });
+}
+
+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
+}
+
+/**
+ * Build the hero-strip Ofsted chip, branching on the inspection framework.
+ * Never synthesises a single overall grade for ReportCard schools.
+ */
+export function buildOfstedHeroChip(ofsted: OfstedInspection | null | undefined): OfstedHeroChip {
+ if (!ofsted || !ofsted.framework) {
+ return {
+ state: 'none',
+ title: 'Ofsted pending',
+ subtitle: 'No inspection on record',
+ toneClass: 'heroChipNeutral',
+ };
+ }
+
+ 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}`,
+ };
+ }
+ return {
+ state: 'oeif',
+ title: 'Ofsted inspected',
+ subtitle: when ? `Inspected ${when}` : 'Inspection on record',
+ toneClass: 'heroChipNeutral',
+ };
+ }
+
+ // 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',
+ };
+}
+
+/**
+ * Build a one-sentence editorial summary for the school detail hero.
+ * Branches on Ofsted framework so Report Card schools are never described
+ * with an overall grade they do not have.
+ */
+export function buildSchoolSummary(
+ schoolInfo: School,
+ ofsted: OfstedInspection | null | undefined,
+ admissions: SchoolAdmissions | null | undefined,
+ latestResults: SchoolResult | null | undefined,
+): string {
+ const parts: string[] = [];
+
+ // Size descriptor
+ const pupils = latestResults?.total_pupils ?? schoolInfo.total_pupils ?? null;
+ const sizeWord =
+ pupils == null ? '' :
+ pupils < 200 ? 'Small' :
+ pupils < 500 ? 'Mid-sized' :
+ 'Large';
+
+ // Phase descriptor — avoid the raw code
+ const phase = (schoolInfo.phase ?? '').toLowerCase();
+ const phaseWord =
+ phase.includes('secondary') ? 'secondary' :
+ phase === 'all-through' ? 'all-through' :
+ phase.includes('primary') ? 'primary' :
+ 'school';
+
+ // Religious character
+ const religion = schoolInfo.religious_denomination;
+ const religionWord =
+ !religion || /none|does not apply/i.test(religion) ? '' :
+ /roman catholic|catholic/i.test(religion) ? 'Catholic ' :
+ /church of england|ce|anglican/i.test(religion) ? 'Church of England ' :
+ /jewish/i.test(religion) ? 'Jewish ' :
+ /muslim|islam/i.test(religion) ? 'Muslim ' :
+ /hindu/i.test(religion) ? 'Hindu ' :
+ /sikh/i.test(religion) ? 'Sikh ' :
+ '';
+
+ // Locality — prefer town from address parsing (fallback to LA)
+ const locality = schoolInfo.town || schoolInfo.local_authority || '';
+
+ const lead = [sizeWord, religionWord + phaseWord].filter(Boolean).join(' ');
+ let opening = lead || 'School';
+ if (locality) opening += ` in ${locality}`;
+ parts.push(opening);
+
+ // Ofsted clause (framework-aware)
+ if (ofsted?.framework === 'OEIF' && ofsted.overall_effectiveness) {
+ parts.push(`rated ${OFSTED_OEIF_WORDS[ofsted.overall_effectiveness]} by Ofsted`);
+ } else if (ofsted?.framework === 'ReportCard') {
+ const when = formatOfstedMonth(ofsted.inspection_date);
+ parts.push(
+ when
+ ? `most recently inspected under Ofsted's Report Card framework in ${when}`
+ : "recently inspected under Ofsted's new Report Card framework",
+ );
+ }
+
+ // Admissions clause
+ if (admissions?.oversubscribed) {
+ if (admissions.first_preference_offer_pct != null) {
+ parts.push(
+ `heavily oversubscribed — just ${Math.round(admissions.first_preference_offer_pct)}% of applicants get a first-choice offer`,
+ );
+ } else {
+ parts.push('heavily oversubscribed');
+ }
+ } else if (admissions?.first_preference_offer_pct != null && admissions.first_preference_offer_pct >= 90) {
+ parts.push('most families get their first-choice offer');
+ }
+
+ return parts.join(', ') + '.';
+}