PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 41s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m57s
The grid sections (At a glance, Ofsted, Getting a place, Who goes there) collapsed generically on mobile — grey label pills, full names wrapping to 3 lines, no dots — making the page ~2x the mockup's height and 'significantly different' from the mobile design. Each measure is now wrapped in a <Measure> that is display:contents on desktop (so the label + cells still flow into the shared aligned grid, unchanged) and a white card on mobile with compact [dot][short name] [value] rows — matching the mobile mockup. The sticky school bar becomes scrollable short-name pills on mobile too. Adds a shortName() util. Desktop layout is unchanged (display:contents dissolves the wrapper). Validated the card mechanism and real content shapes (report-card cell, badges, %+chip rows) via static previews at both widths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
226 lines
7.4 KiB
TypeScript
226 lines
7.4 KiB
TypeScript
/**
|
|
* 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, Measure, 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}>
|
|
<Measure label="Result">
|
|
{schools.map((school, i) => (
|
|
<Cell key={school.urn} school={school} index={i}>
|
|
<ResultCell display={displays[i]} />
|
|
</Cell>
|
|
))}
|
|
</Measure>
|
|
|
|
<Measure label="Inspected">
|
|
{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>
|
|
);
|
|
})}
|
|
|
|
</Measure>
|
|
|
|
<Measure
|
|
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."
|
|
label="Judgement detail"
|
|
>
|
|
{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>
|
|
);
|
|
})}
|
|
|
|
</Measure>
|
|
|
|
<Measure
|
|
tip="Links to the school's page on ofsted.gov.uk, where all its inspection reports are listed."
|
|
label="Ofsted page"
|
|
>
|
|
{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>
|
|
);
|
|
})}
|
|
</Measure>
|
|
</SectionGrid>
|
|
</Section>
|
|
);
|
|
}
|