diff --git a/nextjs-app/__tests__/lib/utils.test.ts b/nextjs-app/__tests__/lib/utils.test.ts index 7a6a406..f657d1b 100644 --- a/nextjs-app/__tests__/lib/utils.test.ts +++ b/nextjs-app/__tests__/lib/utils.test.ts @@ -9,6 +9,7 @@ import { isValidPostcode, debounce, buildOfstedListBadge, + ofstedLegacyAreas, metricKind, shortName, computeYBounds, @@ -111,6 +112,35 @@ describe('debounce', () => { }); }); +describe('ofstedLegacyAreas', () => { + it('returns only real grades (1-4) and drops sentinel/null values', () => { + const areas = ofstedLegacyAreas({ + quality_of_education: 1, + behaviour_attitudes: 2, + personal_development: null, + leadership_management: 9, // sentinel "not applicable" — must be dropped + early_years_provision: 9, // secondary school — not applicable + sixth_form_provision: 2, + }); + expect(areas.map((a) => a.label)).toEqual([ + 'Quality of Teaching', + 'Behaviour in School', + 'Sixth Form', + ]); + expect(areas.every((a) => a.value >= 1 && a.value <= 4)).toBe(true); + }); + + it('includes the sixth-form judgement where present (was previously dropped)', () => { + const areas = ofstedLegacyAreas({ sixth_form_provision: 1 }); + expect(areas).toEqual([{ label: 'Sixth Form', value: 1 }]); + }); + + it('includes Early Years for a primary school', () => { + const areas = ofstedLegacyAreas({ quality_of_education: 2, early_years_provision: 1 }); + expect(areas.map((a) => a.label)).toContain('Early Years (Reception)'); + }); +}); + describe('buildOfstedListBadge', () => { it('returns grade word + year for OEIF Outstanding', () => { const badge = buildOfstedListBadge({ ofsted_grade: 1, ofsted_date: '2023-11-15', ofsted_framework: 'OEIF' }); diff --git a/nextjs-app/components/SchoolDetailView.tsx b/nextjs-app/components/SchoolDetailView.tsx index 796efa6..5fd7be7 100644 --- a/nextjs-app/components/SchoolDetailView.tsx +++ b/nextjs-app/components/SchoolDetailView.tsx @@ -18,7 +18,7 @@ import type { SchoolDeprivation, SchoolFinance, NationalAverages, } from '@/lib/types'; import { - formatPercentage, formatProgress, formatAcademicYear, isProposedToClose, + formatPercentage, formatProgress, formatAcademicYear, isProposedToClose, ofstedLegacyAreas, } from '@/lib/utils'; import { DeltaChip } from './DeltaChip'; @@ -285,17 +285,12 @@ export function SchoolDetailView({ : ofsted?.inspection_date ?? null; // ── Ofsted: detect if all OEIF sub-grades match the overall ─────────── - const oeifAllSameGrade = (() => { - if (!ofsted || isReportCard) return false; - const subs = [ - ofsted.quality_of_education, - ofsted.behaviour_attitudes, - ofsted.personal_development, - ofsted.leadership_management, - ...(ofsted.early_years_provision != null ? [ofsted.early_years_provision] : []), - ].filter((v): v is number => v != null); - return subs.length >= 3 && subs.every(v => v === ofsted.overall_effectiveness); - })(); + const oeifAreas = ofsted ? ofstedLegacyAreas(ofsted) : []; + const oeifAllSameGrade = + !!ofsted && + !isReportCard && + oeifAreas.length >= 3 && + oeifAreas.every((a) => a.value === ofsted.overall_effectiveness); // Label shown in the mobile "section" menu button — the section in view. const activeNavLabel = (navItems.find((n) => n.id === activeSection) ?? navItems[0])?.label ?? ''; @@ -583,7 +578,9 @@ export function SchoolDetailView({ )}

- From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections of state-funded schools. + {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.' + : 'From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections of state-funded schools.'}

{oeifAllSameGrade ? (

@@ -591,15 +588,7 @@ export function SchoolDetailView({

) : (
- {[ - { label: 'Quality of Teaching', value: ofsted.quality_of_education }, - { label: 'Behaviour in School', value: ofsted.behaviour_attitudes }, - { label: 'Pupils\' Wider Development', value: ofsted.personal_development }, - { label: 'School Leadership', value: ofsted.leadership_management }, - ...(ofsted.early_years_provision != null - ? [{ label: 'Early Years (Reception)', value: ofsted.early_years_provision }] - : []), - ].map(({ label, value }) => value != null && ( + {oeifAreas.map(({ label, value }) => (
{label}
diff --git a/nextjs-app/components/SecondarySchoolDetailView.tsx b/nextjs-app/components/SecondarySchoolDetailView.tsx index 9b8ee98..bde7ac9 100644 --- a/nextjs-app/components/SecondarySchoolDetailView.tsx +++ b/nextjs-app/components/SecondarySchoolDetailView.tsx @@ -23,7 +23,7 @@ import type { SchoolAdmissions, SenDetail, Phonics, SchoolDeprivation, SchoolFinance, NationalAverages, } from '@/lib/types'; -import { formatPercentage, formatProgress, formatAcademicYear, formatAgeRange, isProposedToClose } from '@/lib/utils'; +import { formatPercentage, formatProgress, formatAcademicYear, formatAgeRange, isProposedToClose, ofstedLegacyAreas } from '@/lib/utils'; import { DeltaChip } from './DeltaChip'; import { track, getNavigationSource } from '@/lib/analytics'; import styles from './SecondarySchoolDetailView.module.css'; @@ -201,17 +201,12 @@ export function SecondarySchoolDetailView({ : ofsted?.inspection_date ?? null; // ── Ofsted: detect if all OEIF sub-grades match the overall ─────────── - const oeifAllSameGrade = (() => { - if (!ofsted || isReportCard) return false; - const subs = [ - ofsted.quality_of_education, - ofsted.behaviour_attitudes, - ofsted.personal_development, - ofsted.leadership_management, - ...(ofsted.early_years_provision != null ? [ofsted.early_years_provision] : []), - ].filter((v): v is number => v != null); - return subs.length >= 3 && subs.every(v => v === ofsted.overall_effectiveness); - })(); + const oeifAreas = ofsted ? ofstedLegacyAreas(ofsted) : []; + const oeifAllSameGrade = + !!ofsted && + !isReportCard && + oeifAreas.length >= 3 && + oeifAreas.every((a) => a.value === ofsted.overall_effectiveness); // National Attainment 8 baseline for the "Results Over Time" chart. const heroAtt8Nat = secondaryAvg.attainment_8_score ?? null; @@ -404,7 +399,9 @@ export function SecondarySchoolDetailView({ )}

- From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections. + {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.' + : 'From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections.'}

{oeifAllSameGrade ? (

@@ -412,15 +409,7 @@ export function SecondarySchoolDetailView({

) : (
- {[ - { label: 'Quality of Teaching', value: ofsted.quality_of_education }, - { label: 'Behaviour in School', value: ofsted.behaviour_attitudes }, - { label: 'Pupils\' Wider Development', value: ofsted.personal_development }, - { label: 'School Leadership', value: ofsted.leadership_management }, - ...(ofsted.early_years_provision != null - ? [{ label: 'Early Years (Reception)', value: ofsted.early_years_provision }] - : []), - ].map(({ label, value }) => value != null && ( + {oeifAreas.map(({ label, value }) => (
{label}
diff --git a/nextjs-app/lib/utils.ts b/nextjs-app/lib/utils.ts index febe5d1..41b7090 100644 --- a/nextjs-app/lib/utils.ts +++ b/nextjs-app/lib/utils.ts @@ -680,6 +680,42 @@ export function buildSchoolSummary( return parts.join(', ') + '.'; } +// ─── Legacy (OEIF) sub-judgement areas ──────────────────────────────────────── + +export interface OfstedLegacyArea { + label: string; + value: number; +} + +/** + * The published OEIF sub-judgement areas for the legacy Ofsted layout, in + * display order. Only real grades (1–4) are returned: Ofsted's sentinel + * codes for "not applicable / no judgement" (9, and any 0/8 variants) and + * nulls are filtered out, so a cryptic "9" never renders as a rating. + * Sixth Form provision is included where a school has one — it was + * previously dropped from the detail grid entirely. + */ +export function ofstedLegacyAreas(ofsted: { + quality_of_education?: number | null; + behaviour_attitudes?: number | null; + personal_development?: number | null; + leadership_management?: number | null; + early_years_provision?: number | null; + sixth_form_provision?: number | null; +}): OfstedLegacyArea[] { + const candidates: Array<[string, number | null | undefined]> = [ + ['Quality of Teaching', ofsted.quality_of_education], + ['Behaviour in School', ofsted.behaviour_attitudes], + ["Pupils' Wider Development", ofsted.personal_development], + ['School Leadership', ofsted.leadership_management], + ['Early Years (Reception)', ofsted.early_years_provision], + ['Sixth Form', ofsted.sixth_form_provision], + ]; + return candidates + .filter((c): c is [string, number] => c[1] != null && c[1] >= 1 && c[1] <= 4) + .map(([label, value]) => ({ label, value })); +} + // ─── List-level Ofsted badge ────────────────────────────────────────────────── export interface OfstedListBadge {