feat(compare): at-a-glance, Ofsted, admissions and community sections
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* Ofsted section — one visual grammar for inspection detail across all
|
||||
* three regimes (legacy graded, interim carried-forward, renewed-framework
|
||||
* report card). Copy comes verbatim from the reviewed mockups.
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import {
|
||||
OFSTED_LEGACY_GRADES,
|
||||
ofstedDisplay,
|
||||
rcAreaLabel,
|
||||
type OfstedDisplay,
|
||||
} from '@/lib/compareLogic';
|
||||
import type { ComparisonData, OfstedInspection, School } from '@/lib/types';
|
||||
import { Cell, Chip, RowLabel, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||
|
||||
const GRADE_TONE: Record<number, 'good' | 'warn' | 'bad'> = {
|
||||
1: 'good',
|
||||
2: 'good',
|
||||
3: 'warn',
|
||||
4: 'bad',
|
||||
};
|
||||
|
||||
const RC_CODE_TONE = (code: number): 'good' | 'warn' | 'bad' | 'neutral' =>
|
||||
code <= 2 ? 'good' : code === 3 ? 'neutral' : code === 4 ? 'warn' : 'bad';
|
||||
|
||||
function formatInspectionDate(iso: string | null): string {
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return '—';
|
||||
return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
}
|
||||
|
||||
function yearsSince(iso: string | null): number | null {
|
||||
if (!iso) return null;
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return null;
|
||||
return (Date.now() - d.getTime()) / (365.25 * 24 * 3600 * 1000);
|
||||
}
|
||||
|
||||
function ResultCell({ display }: { display: OfstedDisplay }) {
|
||||
if (display.kind === 'none') {
|
||||
return <span className={s.small}>No inspection outcome in our dataset</span>;
|
||||
}
|
||||
if (display.kind === 'report_card') {
|
||||
return (
|
||||
<>
|
||||
<strong style={{ fontSize: '0.9rem' }}>Report card</strong>
|
||||
<span className={s.small}>New-style inspection — no overall grade is given</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<span className={`${s.badge} ${display.grade <= 2 ? s.badgeGood : display.grade === 3 ? s.badgeWarn : s.badgeBad}`}>
|
||||
{display.gradeLabel}
|
||||
</span>
|
||||
<span className={s.small}>
|
||||
{display.carriedForward
|
||||
? 'Grade carried forward from an earlier inspection (ungraded visit since)'
|
||||
: 'Overall grade (older-style inspection)'}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function JudgementDetailCell({
|
||||
ofsted,
|
||||
display,
|
||||
schoolName,
|
||||
}: {
|
||||
ofsted: OfstedInspection;
|
||||
display: OfstedDisplay;
|
||||
schoolName: string;
|
||||
}) {
|
||||
if (display.kind === 'report_card') {
|
||||
const entries = Object.entries(ofsted.report_card ?? {});
|
||||
return (
|
||||
<div className={s.rcList}>
|
||||
{entries.map(([key, entry]) => (
|
||||
<div key={key} className={s.rcRow}>
|
||||
<span className={s.rcArea}>{rcAreaLabel(key)}</span>
|
||||
<Chip tone={RC_CODE_TONE(entry.code)}>{entry.label}</Chip>
|
||||
</div>
|
||||
))}
|
||||
{ofsted.rc_safeguarding_met != null && (
|
||||
<div className={s.rcRow}>
|
||||
<span className={s.rcArea}>Safeguarding</span>
|
||||
<Chip tone={ofsted.rc_safeguarding_met ? 'good' : 'bad'}>
|
||||
{ofsted.rc_safeguarding_met ? 'Met' : 'Not met'}
|
||||
</Chip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const legacyAreas: Array<[string, number | null]> = [
|
||||
['Quality of education', ofsted.quality_of_education],
|
||||
['Behaviour & attitudes', ofsted.behaviour_attitudes],
|
||||
['Personal development', ofsted.personal_development],
|
||||
['Leadership & management', ofsted.leadership_management],
|
||||
['Early years provision', ofsted.early_years_provision],
|
||||
];
|
||||
const published = legacyAreas.filter(([, grade]) => grade != null);
|
||||
|
||||
if (published.length === 0) {
|
||||
return (
|
||||
<span className={s.small}>
|
||||
We don't hold area-by-area detail for this inspection — see {schoolName}'s
|
||||
Ofsted page for the full report.
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={s.rcList}>
|
||||
{published.map(([label, grade]) => (
|
||||
<div key={label} className={s.rcRow}>
|
||||
<span className={s.rcArea}>{label}</span>
|
||||
<Chip tone={GRADE_TONE[grade as number] ?? 'neutral'}>
|
||||
{OFSTED_LEGACY_GRADES[grade as number] ?? String(grade)}
|
||||
</Chip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CompareOfsted({
|
||||
schools,
|
||||
data,
|
||||
}: {
|
||||
schools: School[];
|
||||
data: Record<string, ComparisonData>;
|
||||
}) {
|
||||
const displays = schools.map((school) => ofstedDisplay(data[String(school.urn)]?.ofsted));
|
||||
const kinds = new Set(displays.map((d) => d.kind).filter((k) => k !== 'none'));
|
||||
const mixedRegimes = kinds.size > 1;
|
||||
|
||||
return (
|
||||
<Section
|
||||
title="Ofsted inspection"
|
||||
how={
|
||||
<>
|
||||
Ofsted is the schools inspectorate. It stopped giving a single overall grade in{' '}
|
||||
<strong>September 2024</strong>; inspections between then and November 2025 kept the
|
||||
area-by-area judgements without an overall grade, and from <strong>November 2025</strong>{' '}
|
||||
new inspections produce a <strong>report card</strong> rating each area of school life on
|
||||
a five-point scale.
|
||||
{mixedRegimes && (
|
||||
<> A report card and an older overall grade aren't directly comparable.</>
|
||||
)}{' '}
|
||||
(Ofsted's "Expected standard" rating is unrelated to the KS2 "expected
|
||||
standard" test measure further down this page.)
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SectionGrid schools={schools}>
|
||||
<RowLabel>Result</RowLabel>
|
||||
{schools.map((school, i) => (
|
||||
<Cell key={school.urn} school={school} index={i}>
|
||||
<ResultCell display={displays[i]} />
|
||||
</Cell>
|
||||
))}
|
||||
|
||||
<RowLabel>Inspected</RowLabel>
|
||||
{schools.map((school, i) => {
|
||||
const ofsted = data[String(school.urn)]?.ofsted;
|
||||
const age = yearsSince(ofsted?.inspection_date ?? null);
|
||||
return (
|
||||
<Cell key={school.urn} school={school} index={i}>
|
||||
{formatInspectionDate(ofsted?.inspection_date ?? null)}{' '}
|
||||
{age != null && age > 4 && <Chip tone="neutral">4+ years ago</Chip>}
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
|
||||
<RowLabel tip="Older-style inspections: one rating per judgement area, where published. New-style inspections: the full report card, one rating per area of school life.">
|
||||
Judgement detail
|
||||
</RowLabel>
|
||||
{schools.map((school, i) => {
|
||||
const ofsted = data[String(school.urn)]?.ofsted;
|
||||
return (
|
||||
<Cell key={school.urn} school={school} index={i}>
|
||||
{ofsted ? (
|
||||
<JudgementDetailCell
|
||||
ofsted={ofsted}
|
||||
display={displays[i]}
|
||||
schoolName={school.school_name}
|
||||
/>
|
||||
) : (
|
||||
<span className={s.small}>No inspection in our dataset</span>
|
||||
)}
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
|
||||
<RowLabel tip="Links to the school's page on ofsted.gov.uk, where all its inspection reports are listed.">
|
||||
Ofsted page
|
||||
</RowLabel>
|
||||
{schools.map((school, i) => {
|
||||
const url =
|
||||
data[String(school.urn)]?.ofsted?.ofsted_page_url ??
|
||||
`https://reports.ofsted.gov.uk/provider/21/${school.urn}`;
|
||||
return (
|
||||
<Cell key={school.urn} school={school} index={i}>
|
||||
<a className={s.link} href={url} target="_blank" rel="noopener noreferrer">
|
||||
{school.school_name}'s Ofsted page →
|
||||
</a>
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</SectionGrid>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user