From 9ef48da4b10bf2a18eb7b1eeb4690fb74d0614ff Mon Sep 17 00:00:00 2001 From: Tudor Date: Mon, 5 Oct 2026 16:31:26 +0100 Subject: [PATCH] fix(school): the Ofsted section shows the grade still in force and the latest visit The section is dated by the latest visit and headlines the grade still in force with the inspection that awarded or confirmed it, or 'No overall grade'. A later visit that isn't the grade's source gets its own line, with an ungraded outcome such as 'Standards maintained'. The primary and secondary no-grade branches merge, so the secondary page no longer drops the sixth-form and early-years judgements (audit M2), and 'Not rated' goes. Co-Authored-By: Claude Opus 5.5 --- .../components/OfstedSection.test.tsx | 84 +++++++++++++ .../__tests__/support/schoolFixtures.ts | 6 +- .../components/school/OfstedSection.tsx | 113 ++++++++---------- .../school/PrimarySchoolSections.tsx | 9 +- .../school/SecondarySchoolSections.tsx | 4 +- 5 files changed, 147 insertions(+), 69 deletions(-) create mode 100644 nextjs-app/__tests__/components/OfstedSection.test.tsx diff --git a/nextjs-app/__tests__/components/OfstedSection.test.tsx b/nextjs-app/__tests__/components/OfstedSection.test.tsx new file mode 100644 index 0000000..3f16072 --- /dev/null +++ b/nextjs-app/__tests__/components/OfstedSection.test.tsx @@ -0,0 +1,84 @@ +import { render, screen } from '@testing-library/react'; +import { OfstedSection } from '@/components/school/OfstedSection'; +import { ofstedLegacyAreas } from '@/lib/utils'; +import type { OfstedInspection } from '@/lib/types'; + +const empty = { + framework: null, inspection_date: null, inspection_type: null, overall_effectiveness: null, + quality_of_education: null, behaviour_attitudes: null, personal_development: null, + leadership_management: null, early_years_provision: null, sixth_form_provision: null, + previous_overall: null, rc_safeguarding_met: null, rc_inclusion: null, rc_curriculum_teaching: null, + rc_achievement: null, rc_attendance_behaviour: null, rc_personal_development: null, + rc_leadership_governance: null, rc_early_years: null, rc_sixth_form: null, report_url: null, + report_card: {}, +} as OfstedInspection; + +// `page` mimics how each school page used to mount the section; the secondary +// page's branch hard-coded four areas and dropped the sixth form (audit M2). +function renderSection(o: Partial, page: Record = {}) { + const ofsted = { ...empty, ...o } as OfstedInspection; + render( + , + ); +} + +it('shows no overall grade, not an older one, after a no-grade inspection (Rabbsfarm)', () => { + renderSection({ + inspection_date: '2025-06-17', quality_of_education: 3, behaviour_attitudes: 3, + personal_development: 2, leadership_management: 3, early_years_provision: 2, + current_grade: null, latest_visit: { date: '2025-06-17', kind: 'graded', outcome: null }, + }); + expect(screen.getByText(/Inspected 17 June 2025/)).toBeInTheDocument(); + expect(screen.getByText('No overall grade')).toBeInTheDocument(); + expect(screen.queryByText('Good', { selector: 'span' })).not.toBeInTheDocument(); + expect(screen.queryByText('Not rated')).not.toBeInTheDocument(); + expect(screen.getAllByText('Requires Improvement')).toHaveLength(3); +}); + +it('keeps the sixth-form judgement when there is no overall grade (audit M2)', () => { + renderSection({ + inspection_date: '2025-04-01', quality_of_education: 1, behaviour_attitudes: 1, + personal_development: 1, leadership_management: 1, sixth_form_provision: 2, + current_grade: null, latest_visit: { date: '2025-04-01', kind: 'graded', outcome: null }, + }, { variant: 'secondary' }); + expect(screen.getByText(/sixth form/i)).toBeInTheDocument(); + expect(screen.getByText('Good')).toBeInTheDocument(); +}); + +it('dates a grade by its source and prints a later visit separately (Washwood Heath)', () => { + renderSection({ + inspection_date: '2020-03-03', overall_effectiveness: 2, quality_of_education: 2, + current_grade: { grade: 2, date: '2020-03-03', basis: 'graded' }, + latest_visit: { date: '2025-05-21', kind: 'ungraded', outcome: 'Standards maintained' }, + }); + expect(screen.getByText(/Inspected 21 May 2025/)).toBeInTheDocument(); + expect(screen.getByText('Graded inspection, 3 March 2020')).toBeInTheDocument(); + expect(screen.getByText(/Latest visit: Ungraded inspection, 21 May 2025: Standards maintained/)).toBeInTheDocument(); +}); + +it('says a grade was confirmed at an ungraded visit (Robins Lane)', () => { + renderSection({ + inspection_date: '2020-01-07', overall_effectiveness: 2, + current_grade: { grade: 2, date: '2024-07-18', basis: 'confirmed' }, + latest_visit: { date: '2024-07-18', kind: 'ungraded', outcome: 'School remains Good' }, + }); + expect(screen.getByText('Confirmed at an ungraded inspection, 18 July 2024')).toBeInTheDocument(); + expect(screen.queryByText(/Latest visit/)).not.toBeInTheDocument(); +}); + +it('prints an ungraded outcome when there is no grade (Oakgrove)', () => { + renderSection({ + current_grade: null, + latest_visit: { date: '2024-11-13', kind: 'ungraded', outcome: 'Standards maintained' }, + }); + expect(screen.getByText('No overall grade')).toBeInTheDocument(); + expect(screen.getByText(/Ungraded inspection, 13 November 2024: Standards maintained/)).toBeInTheDocument(); +}); diff --git a/nextjs-app/__tests__/support/schoolFixtures.ts b/nextjs-app/__tests__/support/schoolFixtures.ts index 7eb4c55..332a3cf 100644 --- a/nextjs-app/__tests__/support/schoolFixtures.ts +++ b/nextjs-app/__tests__/support/schoolFixtures.ts @@ -191,7 +191,8 @@ export const primaryFixture = { personal_development: 2, leadership_management: 2, previous_overall: 3, - grade_source: 'graded', + current_grade: { grade: 2, date: '2023-05-17', basis: 'graded' }, + latest_visit: { date: '2023-05-17', kind: 'graded', outcome: null }, }), census, admissions: makeAdmissions({ year: 2024 }), @@ -295,7 +296,8 @@ export const allThroughFixture = { behaviour_attitudes: 1, personal_development: 1, leadership_management: 1, - grade_source: 'graded', + current_grade: { grade: 1, date: '2022-10-04', basis: 'graded' }, + latest_visit: { date: '2022-10-04', kind: 'graded', outcome: null }, }), census, admissions: makeAdmissions({ year: 2024, school_phase: 'Secondary' }), diff --git a/nextjs-app/components/school/OfstedSection.tsx b/nextjs-app/components/school/OfstedSection.tsx index 7d2d202..e393ca3 100644 --- a/nextjs-app/components/school/OfstedSection.tsx +++ b/nextjs-app/components/school/OfstedSection.tsx @@ -1,16 +1,18 @@ /** * OfstedSection — shared between the primary and secondary detail pages. * - * The two versions were ~80% identical, but that figure masked a real fork in - * the no-overall-grade case: the primary page shows a "Not rated" badge, while - * the secondary page shows a four-area OEIF panel. The disclaimer copy also - * differs slightly. Both are preserved exactly via the `variant` prop rather - * than reconciled, because this refactor must not change either page. Merging - * them is a follow-up decision for a human, not a side effect of a move. + * The headline is the school's current Ofsted status (backend: + * fact_ofsted_latest): the report card; or the overall grade still in force, + * with the inspection that awarded or confirmed it; or "No overall grade". + * A later visit that is not the grade's source gets its own line. Both pages + * render the same branches, so neither drops an area judgement (audit M2). + * Rule: docs/superpowers/specs/2026-10-05-ofsted-current-status-design.md * * Server component. */ +import { ofstedDisplay } from '@/lib/compareLogic'; +import { formatOfstedDate, gradeSourceLine, latestVisitLine, showLatestVisitLine } from '@/lib/ofstedStatus'; import type { OfstedInspection } from '@/lib/types'; import { Section, sectionStyles as styles } from './sectionShared'; @@ -40,14 +42,13 @@ export interface OfstedSectionProps { ofstedInspectedDate: string | null; oeifAllSameGrade: boolean; oeifAreas: { label: string; value: number }[]; - variant?: 'primary' | 'secondary'; } export function OfstedSection({ ofsted, urn, isReportCard, ofstedInspectedDate, - oeifAllSameGrade, oeifAreas, variant = 'primary', + oeifAllSameGrade, oeifAreas, }: OfstedSectionProps) { - const isSecondary = variant === 'secondary'; + const display = ofstedDisplay(ofsted); return (
@@ -55,7 +56,7 @@ export function OfstedSection({ {isReportCard ? 'Ofsted Report Card' : 'Ofsted Rating'} {ofstedInspectedDate && ( - {isSecondary && ' '}Inspected {new Date(ofstedInspectedDate).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })} + Inspected {new Date(ofstedInspectedDate).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })} )} - ) : (!isSecondary || ofsted.overall_effectiveness) ? ( - /* ── Old OEIF layout ── */ + ) : ( <>
- - {ofsted.overall_effectiveness ? OFSTED_LABELS[ofsted.overall_effectiveness] : 'Not rated'} - - {ofsted.previous_overall != null && - ofsted.previous_overall !== ofsted.overall_effectiveness && ( - - Previously: {OFSTED_LABELS[ofsted.previous_overall]} - + {display.kind === 'graded' || display.kind === 'confirmed' ? ( + <> + + {display.gradeLabel} + + {ofsted.previous_overall != null && ofsted.previous_overall !== display.grade && ( + + Previously: {OFSTED_LABELS[ofsted.previous_overall]} + + )} + + ) : ( + No overall grade )}

- {ofsted.grade_source === 'ungraded_carried_forward' - ? 'This overall grade is carried forward from an earlier inspection. Ofsted has since visited without issuing a new overall grade. From September 2024, Ofsted no longer makes an overall effectiveness judgement.' - : isSecondary - ? 'From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections.' - : 'From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections of state-funded schools.'} + {ofsted.current_grade + ? gradeSourceLine(ofsted.current_grade) + : 'Ofsted stopped giving overall grades in September 2024.'}

- {oeifAllSameGrade ? ( -

- Rated {OFSTED_LABELS[ofsted.overall_effectiveness!]} across all inspected areas: Quality of Teaching, Behaviour, Pupils' Development and Leadership. -

- ) : ( -
- {oeifAreas.map(({ label, value }) => ( -
-
{label}
-
- {OFSTED_LABELS[value]} -
-
- ))} -
+ {showLatestVisitLine(ofsted) && ofsted.latest_visit && ( +

Latest visit: {latestVisitLine(ofsted.latest_visit)}

)} - - ) : ( - /* ── Secondary only: inspected since Sept 2024, no overall grade ── */ - <> -

- From September 2024, Ofsted no longer gives a single overall grade. -

-
- {[ - { label: 'Quality of Education', value: ofsted.quality_of_education }, - { label: 'Behaviour & Attitudes', value: ofsted.behaviour_attitudes }, - { label: 'Personal Development', value: ofsted.personal_development }, - { label: 'Leadership & Management', value: ofsted.leadership_management }, - ].filter(({ value }) => value != null).map(({ label, value }) => ( -
-
{label}
-
- {OFSTED_LABELS[value!]} -
+ {oeifAllSameGrade && (display.kind === 'graded' || display.kind === 'confirmed') ? ( +

+ Rated {display.gradeLabel} across all inspected areas: Quality of Teaching, Behaviour, Pupils' Development and Leadership. +

+ ) : oeifAreas.length > 0 ? ( + <> + {ofsted.inspection_date && ofsted.inspection_date !== ofsted.latest_visit?.date && ( +

+ Area judgements from the graded inspection, {formatOfstedDate(ofsted.inspection_date)}. +

+ )} +
+ {oeifAreas.map(({ label, value }) => ( +
+
{label}
+
+ {OFSTED_LABELS[value]} +
+
+ ))}
- ))} -
+ + ) : null} )}
diff --git a/nextjs-app/components/school/PrimarySchoolSections.tsx b/nextjs-app/components/school/PrimarySchoolSections.tsx index 319ee86..623027b 100644 --- a/nextjs-app/components/school/PrimarySchoolSections.tsx +++ b/nextjs-app/components/school/PrimarySchoolSections.tsx @@ -55,15 +55,17 @@ export function PrimarySchoolSections({ const secondaryAvg = nationalAvg?.secondary ?? {}; const isReportCard = !!(ofsted?.report_card && Object.keys(ofsted.report_card).length > 0); - // Report cards are dated by their own inspection (rc_inspection_date), never - // the legacy inspection_date (report cards exist only from Nov 2025). + // Report cards are dated by their own inspection (rc_inspection_date); + // anything else by the school's latest visit, never the older inspection a + // grade may come from. const ofstedInspectedDate = isReportCard ? ofsted?.rc_inspection_date ?? null - : ofsted?.inspection_date ?? null; + : ofsted?.latest_visit?.date ?? null; const oeifAreas = ofsted ? ofstedLegacyAreas(ofsted) : []; const oeifAllSameGrade = !!ofsted && !isReportCard && + ofsted.overall_effectiveness != null && oeifAreas.length >= 3 && oeifAreas.every((a) => a.value === ofsted.overall_effectiveness); @@ -77,7 +79,6 @@ export function PrimarySchoolSections({ ofstedInspectedDate={ofstedInspectedDate} oeifAllSameGrade={oeifAllSameGrade} oeifAreas={oeifAreas} - variant="primary" /> )} diff --git a/nextjs-app/components/school/SecondarySchoolSections.tsx b/nextjs-app/components/school/SecondarySchoolSections.tsx index 7fcbe98..9526e8d 100644 --- a/nextjs-app/components/school/SecondarySchoolSections.tsx +++ b/nextjs-app/components/school/SecondarySchoolSections.tsx @@ -63,11 +63,12 @@ export function SecondarySchoolSections({ const isReportCard = !!(ofsted?.report_card && Object.keys(ofsted.report_card).length > 0); const ofstedInspectedDate = isReportCard ? ofsted?.rc_inspection_date ?? null - : ofsted?.inspection_date ?? null; + : ofsted?.latest_visit?.date ?? null; const oeifAreas = ofsted ? ofstedLegacyAreas(ofsted) : []; const oeifAllSameGrade = !!ofsted && !isReportCard && + ofsted.overall_effectiveness != null && oeifAreas.length >= 3 && oeifAreas.every((a) => a.value === ofsted.overall_effectiveness); @@ -81,7 +82,6 @@ export function SecondarySchoolSections({ ofstedInspectedDate={ofstedInspectedDate} oeifAllSameGrade={oeifAllSameGrade} oeifAreas={oeifAreas} - variant="secondary" /> )}