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 {