From 8a6758b5912a02934a08362729a14c37da57fd77 Mon Sep 17 00:00:00 2001 From: Tudor Sitaru Date: Mon, 13 Apr 2026 13:52:05 +0100 Subject: [PATCH] feat(utils): add buildOfstedListBadge helper and fetchNationalAverages - Add ofsted_framework field to School type - Add OfstedListBadge interface and buildOfstedListBadge pure function to utils.ts - Add fetchNationalAverages API function that calls GET /api/national-averages - Add test suite for buildOfstedListBadge (all 6 new tests pass) Co-Authored-By: Claude Sonnet 4.6 --- nextjs-app/__tests__/lib/utils.test.ts | 39 ++++++++++++++++++++++ nextjs-app/lib/api.ts | 21 ++++++++++++ nextjs-app/lib/types.ts | 1 + nextjs-app/lib/utils.ts | 46 ++++++++++++++++++++++++++ 4 files changed, 107 insertions(+) diff --git a/nextjs-app/__tests__/lib/utils.test.ts b/nextjs-app/__tests__/lib/utils.test.ts index 2258d2d..44afec9 100644 --- a/nextjs-app/__tests__/lib/utils.test.ts +++ b/nextjs-app/__tests__/lib/utils.test.ts @@ -8,6 +8,7 @@ import { calculateTrend, isValidPostcode, debounce, + buildOfstedListBadge, } from '@/lib/utils'; describe('formatPercentage', () => { @@ -102,3 +103,41 @@ describe('debounce', () => { jest.useRealTimers(); }); + +describe('buildOfstedListBadge', () => { + it('returns grade word + year for OEIF Outstanding', () => { + const badge = buildOfstedListBadge({ ofsted_grade: 1, ofsted_date: '2023-11-15', ofsted_framework: 'OEIF' }); + expect(badge.label).toBe('Outstanding · 2023'); + expect(badge.cssClass).toBe('ofsted1'); + }); + + it('returns grade word for each OEIF grade', () => { + expect(buildOfstedListBadge({ ofsted_grade: 2, ofsted_date: '2022-05-01' }).label).toBe('Good · 2022'); + expect(buildOfstedListBadge({ ofsted_grade: 3, ofsted_date: '2021-01-01' }).label).toBe('Req. Improvement · 2021'); + expect(buildOfstedListBadge({ ofsted_grade: 4, ofsted_date: '2020-03-01' }).label).toBe('Inadequate · 2020'); + }); + + it('returns grade word without year when date is missing', () => { + const badge = buildOfstedListBadge({ ofsted_grade: 2, ofsted_date: null }); + expect(badge.label).toBe('Good'); + expect(badge.cssClass).toBe('ofsted2'); + }); + + it('returns Report Card badge when framework is ReportCard', () => { + const badge = buildOfstedListBadge({ ofsted_grade: null, ofsted_date: '2025-11-01', ofsted_framework: 'ReportCard' }); + expect(badge.label).toBe('Report Card · 2025'); + expect(badge.cssClass).toBe('ofstedRc'); + }); + + it('returns pending badge when no grade and no ReportCard framework', () => { + const badge = buildOfstedListBadge({ ofsted_grade: null, ofsted_date: null, ofsted_framework: null }); + expect(badge.label).toBe('Not yet inspected'); + expect(badge.cssClass).toBe('ofstedPending'); + }); + + it('returns pending badge when all fields are undefined', () => { + const badge = buildOfstedListBadge({}); + expect(badge.label).toBe('Not yet inspected'); + expect(badge.cssClass).toBe('ofstedPending'); + }); +}); diff --git a/nextjs-app/lib/api.ts b/nextjs-app/lib/api.ts index 476d460..4fb0929 100644 --- a/nextjs-app/lib/api.ts +++ b/nextjs-app/lib/api.ts @@ -15,6 +15,7 @@ import type { RankingsParams, APIError, LAaveragesResponse, + NationalAverages, } from './types'; // ============================================================================ @@ -261,6 +262,26 @@ export async function fetchLAaverages( return handleResponse(response); } +/** + * Fetch official DfE KS2 national averages (primary) and computed KS4 secondary averages. + * Returns latest year snapshot plus per-year history for chart reference lines. + */ +export async function fetchNationalAverages( + options: RequestInit = {} +): Promise { + const url = `${API_BASE_URL}/national-averages`; + + const response = await fetch(url, { + ...options, + next: { + revalidate: 3600, + ...options.next, + }, + }); + + return handleResponse(response); +} + /** * Fetch database statistics and info */ diff --git a/nextjs-app/lib/types.ts b/nextjs-app/lib/types.ts index eddb1b1..1b7efa4 100644 --- a/nextjs-app/lib/types.ts +++ b/nextjs-app/lib/types.ts @@ -67,6 +67,7 @@ export interface School { // Ofsted (for list view — summary only) ofsted_grade?: 1 | 2 | 3 | 4 | null; ofsted_date?: string | null; + ofsted_framework?: string | null; } // ============================================================================ diff --git a/nextjs-app/lib/utils.ts b/nextjs-app/lib/utils.ts index 8309100..18fdd10 100644 --- a/nextjs-app/lib/utils.ts +++ b/nextjs-app/lib/utils.ts @@ -570,3 +570,49 @@ export function buildSchoolSummary( return parts.join(', ') + '.'; } + +// ─── List-level Ofsted badge ────────────────────────────────────────────────── + +export interface OfstedListBadge { + /** Display text for the badge (e.g. "Outstanding · 2023", "Report Card · 2025") */ + label: string; + /** CSS module class key — one of: ofsted1 | ofsted2 | ofsted3 | ofsted4 | ofstedRc | ofstedPending */ + cssClass: string; +} + +/** + * Build the Ofsted badge for a school card in the list/map view. + * Three states: + * - OEIF school (ofsted_grade set): grade word + year, colour-keyed + * - ReportCard school (ofsted_framework === 'ReportCard'): "Report Card · YYYY" in purple + * - No inspection: "Not yet inspected" in grey + */ +export function buildOfstedListBadge(school: { + ofsted_grade?: number | null; + ofsted_date?: string | null; + ofsted_framework?: string | null; +}): OfstedListBadge { + const year = school.ofsted_date + ? new Date(school.ofsted_date).getFullYear() + : null; + const yearStr = year ? ` · ${year}` : ''; + + if (school.ofsted_grade) { + const labels: Record = { + 1: 'Outstanding', + 2: 'Good', + 3: 'Req. Improvement', + 4: 'Inadequate', + }; + return { + label: `${labels[school.ofsted_grade]}${yearStr}`, + cssClass: `ofsted${school.ofsted_grade}`, + }; + } + + if (school.ofsted_framework === 'ReportCard') { + return { label: `Report Card${yearStr}`, cssClass: 'ofstedRc' }; + } + + return { label: 'Not yet inspected', cssClass: 'ofstedPending' }; +}