feat(school-detail): editorial hero with signal chips, at-a-glance stats, summary
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 15s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 51s
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 0s
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 15s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 51s
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 0s
Elevates the primary school detail hero from a flat report header into a scannable editorial block. Parents can read the headline signal in seconds. - A1: bump .schoolName to clamp(2rem, 5vw, 3.25rem) Playfair. - A2: framework-aware signal chip strip via new buildOfstedHeroChip() helper. Branches on ofsted.framework so Report Card schools never show a fake overall grade — they get "Ofsted Report Card" + inspection date + Safeguarding: Met/Not met. OEIF schools keep the grade word. - A3: oversized Playfair stats — Reading, Writing & Maths % (primary) or Attainment 8 (secondary) with inline DeltaChip vs national, Ofsted verdict with tone colouring, and first-choice offer rate. - B1: italic serif one-sentence summary via buildSchoolSummary() helper, also framework-aware so Report Card schools are described by framework, not a synthetic grade. - C1: new DeltaChip component reused in the two headline KS2 metric cards (rwm_expected_pct, rwm_high_pct). All copy uses "Reading, Writing & Maths" in full. Secondary detail view untouched in this slice. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+152
-1
@@ -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<number, string> = {
|
||||
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(', ') + '.';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user