Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
993a6133cf |
@@ -67,12 +67,9 @@ jobs:
|
|||||||
echo "Promoting $FULL_SHA (images tagged $SHORT_SHA)"
|
echo "Promoting $FULL_SHA (images tagged $SHORT_SHA)"
|
||||||
|
|
||||||
- name: Verify the staging E2E gate passed for this commit
|
- name: Verify the staging E2E gate passed for this commit
|
||||||
# Use the built-in Actions token (GITHUB_TOKEN is the documented name;
|
|
||||||
# it carries repository read scope), NOT REGISTRY_TOKEN — the registry
|
|
||||||
# token has no repo scope, so the commit-status API returns 403.
|
|
||||||
run: |
|
run: |
|
||||||
STATUS_JSON=$(curl -fsS \
|
STATUS_JSON=$(curl -fsS \
|
||||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.REGISTRY_TOKEN }}" \
|
||||||
"https://${REGISTRY}/api/v1/repos/${{ gitea.repository }}/commits/${{ steps.resolve.outputs.full }}/status")
|
"https://${REGISTRY}/api/v1/repos/${{ gitea.repository }}/commits/${{ steps.resolve.outputs.full }}/status")
|
||||||
echo "$STATUS_JSON" | python3 -c "
|
echo "$STATUS_JSON" | python3 -c "
|
||||||
import json, sys
|
import json, sys
|
||||||
|
|||||||
@@ -172,7 +172,6 @@ _MAIN_QUERY = text("""
|
|||||||
foi.ofsted_grade,
|
foi.ofsted_grade,
|
||||||
foi.ofsted_date,
|
foi.ofsted_date,
|
||||||
foi.ofsted_framework,
|
foi.ofsted_framework,
|
||||||
foi.ofsted_rc_date,
|
|
||||||
l.local_authority_name AS local_authority,
|
l.local_authority_name AS local_authority,
|
||||||
l.local_authority_code,
|
l.local_authority_code,
|
||||||
l.address_line1 AS address1,
|
l.address_line1 AS address1,
|
||||||
@@ -257,12 +256,7 @@ _MAIN_QUERY = text("""
|
|||||||
-- Fall back to the ungraded-inspection grade when no graded grade exists.
|
-- Fall back to the ungraded-inspection grade when no graded grade exists.
|
||||||
COALESCE(overall_effectiveness, ungraded_grade) AS ofsted_grade,
|
COALESCE(overall_effectiveness, ungraded_grade) AS ofsted_grade,
|
||||||
inspection_date AS ofsted_date,
|
inspection_date AS ofsted_date,
|
||||||
framework AS ofsted_framework,
|
framework AS ofsted_framework
|
||||||
-- Report-card signal for list/map badges: non-null only when the
|
|
||||||
-- latest inspection carries report-card grades. framework is the
|
|
||||||
-- raw event grouping ("Schools - S5"), never "ReportCard", so it
|
|
||||||
-- can't be used to detect report cards.
|
|
||||||
rc_inspection_date AS ofsted_rc_date
|
|
||||||
FROM marts.fact_ofsted_inspection
|
FROM marts.fact_ofsted_inspection
|
||||||
ORDER BY urn, inspection_date DESC NULLS LAST
|
ORDER BY urn, inspection_date DESC NULLS LAST
|
||||||
) foi ON s.urn = foi.urn
|
) foi ON s.urn = foi.urn
|
||||||
|
|||||||
@@ -550,7 +550,6 @@ SCHOOL_COLUMNS = [
|
|||||||
"ofsted_grade",
|
"ofsted_grade",
|
||||||
"ofsted_date",
|
"ofsted_date",
|
||||||
"ofsted_framework",
|
"ofsted_framework",
|
||||||
"ofsted_rc_date",
|
|
||||||
"latitude",
|
"latitude",
|
||||||
"longitude",
|
"longitude",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -80,33 +80,6 @@ test('searching by postcode returns nearby schools', async ({ page }) => {
|
|||||||
await expect(schoolLinks(page).first()).toBeVisible({ timeout: 15_000 });
|
await expect(schoolLinks(page).first()).toBeVisible({ timeout: 15_000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a report-card school shows a Report Card badge in search results, not its old grade', async ({ page }) => {
|
|
||||||
// List/map badges keyed off ofsted_grade (the carried-forward legacy grade)
|
|
||||||
// and never reached the report-card branch, so report-card schools were
|
|
||||||
// labelled by their old grade (e.g. "Outstanding · 2021"). The list now
|
|
||||||
// carries ofsted_rc_date and the badge treats a report card as winning.
|
|
||||||
const RC_URN = 138690; // Barclay Primary — has a Nov-2025+ report card
|
|
||||||
const res = await page.request.get(`/api/schools?search=Barclay%20Primary&page_size=5`);
|
|
||||||
expect(res.ok()).toBeTruthy();
|
|
||||||
const barclay = ((await res.json()).schools ?? []).find(
|
|
||||||
(s: { urn: number }) => s.urn === RC_URN,
|
|
||||||
);
|
|
||||||
// Hard assertions, not test.skip: if the backend stops exposing
|
|
||||||
// ofsted_rc_date for this report-card school, that IS the regression this
|
|
||||||
// test exists to catch, so it must fail loudly rather than skip.
|
|
||||||
expect(barclay, 'Barclay must appear in the search results').toBeTruthy();
|
|
||||||
expect(
|
|
||||||
barclay.ofsted_rc_date,
|
|
||||||
'the list must expose ofsted_rc_date for a report-card school',
|
|
||||||
).toBeTruthy();
|
|
||||||
|
|
||||||
await searchByName(page, 'Barclay Primary');
|
|
||||||
// The Barclay row must be present…
|
|
||||||
await expect(page.locator(`a[href*="${RC_URN}"]`).first()).toBeVisible({ timeout: 15_000 });
|
|
||||||
// …badged as a Report Card, not its carried-forward "Outstanding" grade.
|
|
||||||
await expect(page.getByText(/Report Card ·/).first()).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('school detail page renders name and performance data', async ({ page }) => {
|
test('school detail page renders name and performance data', async ({ page }) => {
|
||||||
await searchByName(page, 'primary');
|
await searchByName(page, 'primary');
|
||||||
const firstSchool = schoolLinks(page).first();
|
const firstSchool = schoolLinks(page).first();
|
||||||
@@ -121,33 +94,6 @@ test('school detail page renders name and performance data', async ({ page }) =>
|
|||||||
await expect(page.locator('canvas:visible').first()).toBeVisible({ timeout: 15_000 });
|
await expect(page.locator('canvas:visible').first()).toBeVisible({ timeout: 15_000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a report-card school shows its report card, dated to the report-card inspection', async ({ page }) => {
|
|
||||||
// Detail views detected report cards via `framework`, which the API never
|
|
||||||
// sets to "ReportCard" — so report-card schools rendered as legacy ratings
|
|
||||||
// dated to a pre-Nov-2025 inspection. Detection now keys off the report_card
|
|
||||||
// object and dates it with rc_inspection_date.
|
|
||||||
const RC_URN = 138690; // Barclay Primary — has a Nov-2025+ report card
|
|
||||||
const res = await page.request.get(`/api/schools/${RC_URN}`);
|
|
||||||
expect(res.ok()).toBeTruthy();
|
|
||||||
const ofsted = (await res.json()).ofsted;
|
|
||||||
test.skip(
|
|
||||||
!ofsted?.report_card || Object.keys(ofsted.report_card).length === 0,
|
|
||||||
'precondition: chosen URN must currently have a report card',
|
|
||||||
);
|
|
||||||
const rcYear = new Date(ofsted.rc_inspection_date).getFullYear();
|
|
||||||
const legacyYear = new Date(ofsted.inspection_date).getFullYear();
|
|
||||||
|
|
||||||
await page.goto(`/school/${RC_URN}`);
|
|
||||||
const ofstedSection = page.locator('#ofsted');
|
|
||||||
// Detection fixed: rendered as a Report Card, not a legacy "Ofsted Rating".
|
|
||||||
await expect(ofstedSection.getByText('Ofsted Report Card')).toBeVisible({ timeout: 15_000 });
|
|
||||||
// Dating fixed: dated to the report-card inspection, never the legacy one.
|
|
||||||
await expect(ofstedSection.getByText(new RegExp(`Inspected .*${rcYear}`))).toBeVisible();
|
|
||||||
if (legacyYear !== rcYear) {
|
|
||||||
await expect(ofstedSection.getByText(new RegExp(`Inspected .*${legacyYear}`))).toHaveCount(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('school with no performance data still gets a working detail page', async ({ page }) => {
|
test('school with no performance data still gets a working detail page', async ({ page }) => {
|
||||||
// Schools without KS2/KS4 results (special post-16 institutions, sixth-form
|
// Schools without KS2/KS4 results (special post-16 institutions, sixth-form
|
||||||
// centres, PRUs) used to 500 in the API — NaN GIAS fields broke JSON
|
// centres, PRUs) used to 500 in the API — NaN GIAS fields broke JSON
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
/**
|
|
||||||
* Secondary academics: every headline number carries its England anchor and a
|
|
||||||
* verdict chip (expert sign-off SF1 — the grade-5 and EBacc rows previously
|
|
||||||
* rendered as bare numbers, breaking the "anchored against England" promise).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { render, screen, within } from '@testing-library/react';
|
|
||||||
|
|
||||||
import { CompareAcademics } from '@/components/compare/CompareAcademics';
|
|
||||||
import type { ComparisonData, NationalAverages, School } from '@/lib/types';
|
|
||||||
|
|
||||||
function school(urn: number, name: string): School {
|
|
||||||
return { urn, school_name: name, attainment_8_score: 58.7 } as School;
|
|
||||||
}
|
|
||||||
|
|
||||||
function data(urn: number): ComparisonData {
|
|
||||||
return {
|
|
||||||
school_info: school(urn, 'Test High'),
|
|
||||||
yearly_data: [
|
|
||||||
{
|
|
||||||
year: 202425,
|
|
||||||
attainment_8_score: 58.7,
|
|
||||||
english_maths_strong_pass_pct: 30,
|
|
||||||
ebacc_entry_pct: 10,
|
|
||||||
},
|
|
||||||
] as ComparisonData['yearly_data'],
|
|
||||||
ofsted: null,
|
|
||||||
census: null,
|
|
||||||
admissions: null,
|
|
||||||
admissions_history: [],
|
|
||||||
deprivation: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const NATIONAL: NationalAverages = {
|
|
||||||
year: 202425,
|
|
||||||
primary: {},
|
|
||||||
secondary: {
|
|
||||||
attainment_8_score: 46.0,
|
|
||||||
english_maths_strong_pass_pct: 45.4,
|
|
||||||
ebacc_entry_pct: 40.5,
|
|
||||||
},
|
|
||||||
by_year: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
test('grade-5 and EBacc rows show the England anchor and a Below chip when under it', () => {
|
|
||||||
const s = school(137086, 'Bishop Stopford School');
|
|
||||||
render(
|
|
||||||
<CompareAcademics
|
|
||||||
schools={[s]}
|
|
||||||
data={{ '137086': data(137086) }}
|
|
||||||
nationalAverages={NATIONAL}
|
|
||||||
isSecondary
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// The official anchors appear (45.4% and 40.5%), not just the school numbers.
|
|
||||||
expect(screen.getByText(/England average 45%/)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(/England average 41%/)).toBeInTheDocument();
|
|
||||||
|
|
||||||
// 30% grade-5 and 10% EBacc are both well below their anchors → Below chips.
|
|
||||||
// Attainment 8 (58.7 vs 46.0) is above → at least one "Above" chip too.
|
|
||||||
expect(screen.getAllByText(/Below England average/).length).toBeGreaterThanOrEqual(2);
|
|
||||||
expect(screen.getAllByText(/Above England average/).length).toBeGreaterThanOrEqual(1);
|
|
||||||
});
|
|
||||||
@@ -130,23 +130,9 @@ describe('buildOfstedListBadge', () => {
|
|||||||
expect(badge.cssClass).toBe('ofsted2');
|
expect(badge.cssClass).toBe('ofsted2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a Report Card badge when ofsted_rc_date is present', () => {
|
it('returns Report Card badge when framework is ReportCard', () => {
|
||||||
const badge = buildOfstedListBadge({ ofsted_grade: null, ofsted_rc_date: '2026-02-03' });
|
const badge = buildOfstedListBadge({ ofsted_grade: null, ofsted_date: '2025-11-01', ofsted_framework: 'ReportCard' });
|
||||||
expect(badge.label).toBe('Report Card · 2026');
|
expect(badge.label).toBe('Report Card · 2025');
|
||||||
expect(badge.cssClass).toBe('ofstedRc');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('a report card wins over a carried-forward legacy grade', () => {
|
|
||||||
// The production bug: a report-card school (e.g. Barclay) also carries a
|
|
||||||
// carried-forward legacy grade (ofsted_grade), which used to win and label
|
|
||||||
// the pin "Outstanding · 2021" instead of "Report Card · 2026".
|
|
||||||
const badge = buildOfstedListBadge({
|
|
||||||
ofsted_grade: 1,
|
|
||||||
ofsted_date: '2021-10-07',
|
|
||||||
ofsted_framework: 'Schools - S5',
|
|
||||||
ofsted_rc_date: '2026-02-03',
|
|
||||||
});
|
|
||||||
expect(badge.label).toBe('Report Card · 2026');
|
|
||||||
expect(badge.cssClass).toBe('ofstedRc');
|
expect(badge.cssClass).toBe('ofstedRc');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -183,15 +183,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Secondary: teal outline — supporting actions (+ Compare) */
|
/* Secondary: teal outline — supporting actions (+ Compare) */
|
||||||
/* NOTE: a duplicate `.btn` block further down this file sets `border: none`,
|
|
||||||
which wins over the base `.btn`'s `1px solid transparent`. The outline
|
|
||||||
variants below therefore declare the full `border` shorthand explicitly so
|
|
||||||
they don't depend on the base border-width — otherwise `border-color` alone
|
|
||||||
has no width and the outline never renders (buttons read as plain text). */
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--accent-teal);
|
color: var(--accent-teal);
|
||||||
border: 1px solid var(--accent-teal);
|
border-color: var(--accent-teal);
|
||||||
}
|
}
|
||||||
.btn-secondary:hover:not(:disabled) {
|
.btn-secondary:hover:not(:disabled) {
|
||||||
background: var(--accent-teal-bg);
|
background: var(--accent-teal-bg);
|
||||||
@@ -201,7 +196,7 @@ body {
|
|||||||
.btn-tertiary {
|
.btn-tertiary {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
border: 1px solid var(--border-color);
|
border-color: var(--border-color);
|
||||||
}
|
}
|
||||||
.btn-tertiary:hover:not(:disabled) {
|
.btn-tertiary:hover:not(:disabled) {
|
||||||
background: var(--border-color);
|
background: var(--border-color);
|
||||||
@@ -212,7 +207,7 @@ body {
|
|||||||
.btn-active {
|
.btn-active {
|
||||||
background: var(--accent-teal-bg);
|
background: var(--accent-teal-bg);
|
||||||
color: var(--accent-teal);
|
color: var(--accent-teal);
|
||||||
border: 1px solid var(--accent-teal);
|
border-color: var(--accent-teal);
|
||||||
}
|
}
|
||||||
.btn-active:hover:not(:disabled) {
|
.btn-active:hover:not(:disabled) {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|||||||
@@ -43,13 +43,6 @@ interface PopupBadge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildPopupBadge(school: School): PopupBadge {
|
function buildPopupBadge(school: School): PopupBadge {
|
||||||
// A report card wins over any carried-forward legacy grade — its presence is
|
|
||||||
// signalled by ofsted_rc_date (the list has no full report_card object, and
|
|
||||||
// ofsted_framework is the raw event grouping, never "ReportCard").
|
|
||||||
if (school.ofsted_rc_date) {
|
|
||||||
const rcYear = new Date(school.ofsted_rc_date).getFullYear();
|
|
||||||
return { label: `Report Card · ${rcYear}`, style: 'background:#5a3a6e;color:#fff' };
|
|
||||||
}
|
|
||||||
const year = school.ofsted_date ? new Date(school.ofsted_date).getFullYear() : null;
|
const year = school.ofsted_date ? new Date(school.ofsted_date).getFullYear() : null;
|
||||||
const yearStr = year ? ` · ${year}` : '';
|
const yearStr = year ? ` · ${year}` : '';
|
||||||
if (school.ofsted_grade) {
|
if (school.ofsted_grade) {
|
||||||
@@ -62,6 +55,9 @@ function buildPopupBadge(school: School): PopupBadge {
|
|||||||
};
|
};
|
||||||
return { label: `${labels[school.ofsted_grade]}${yearStr}`, style: colours[school.ofsted_grade] };
|
return { label: `${labels[school.ofsted_grade]}${yearStr}`, style: colours[school.ofsted_grade] };
|
||||||
}
|
}
|
||||||
|
if (school.ofsted_framework === 'ReportCard') {
|
||||||
|
return { label: `Report Card${yearStr}`, style: 'background:#5a3a6e;color:#fff' };
|
||||||
|
}
|
||||||
return { label: 'Not yet inspected', style: 'background:#e0e0e0;color:#666' };
|
return { label: 'Not yet inspected', style: 'background:#e0e0e0;color:#666' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,13 +123,7 @@
|
|||||||
.modal {
|
.modal {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
/* Bottom sheet sized against the overlay (which tracks the visual
|
max-height: 95vh;
|
||||||
viewport), NOT vh: when the keyboard is open the overlay is short, so
|
|
||||||
max-height:100% keeps the whole sheet — input and results — above the
|
|
||||||
keyboard. min-height gives a comfortable default without a tiny stub,
|
|
||||||
but is capped at 100% so it never exceeds the visible area. */
|
|
||||||
min-height: min(55vh, 100%);
|
|
||||||
max-height: 100%;
|
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
animation: slideUp 0.3s ease;
|
animation: slideUp 0.3s ease;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useCallback, useRef } from 'react';
|
import { useEffect, useCallback } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import styles from './Modal.module.css';
|
import styles from './Modal.module.css';
|
||||||
|
|
||||||
@@ -18,8 +18,6 @@ interface ModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Modal({ isOpen, onClose, children, title, size = 'medium' }: ModalProps) {
|
export function Modal({ isOpen, onClose, children, title, size = 'medium' }: ModalProps) {
|
||||||
const overlayRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const handleEscape = useCallback((e: KeyboardEvent) => {
|
const handleEscape = useCallback((e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
onClose();
|
onClose();
|
||||||
@@ -41,32 +39,6 @@ export function Modal({ isOpen, onClose, children, title, size = 'medium' }: Mod
|
|||||||
};
|
};
|
||||||
}, [isOpen, handleEscape]);
|
}, [isOpen, handleEscape]);
|
||||||
|
|
||||||
// Pin the overlay to the VISUAL viewport, not the layout viewport. On mobile
|
|
||||||
// the on-screen keyboard shrinks the visual viewport but not the layout one,
|
|
||||||
// so a `position: fixed; inset: 0` overlay keeps full height — leaving the
|
|
||||||
// bottom-anchored sheet (and the dim backdrop's lower half) hidden behind
|
|
||||||
// the keyboard. Tracking visualViewport.height/offsetTop keeps the whole
|
|
||||||
// overlay — backdrop and sheet — inside the visible area, above the keyboard.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) return;
|
|
||||||
const vv = typeof window !== 'undefined' ? window.visualViewport : null;
|
|
||||||
const el = overlayRef.current;
|
|
||||||
if (!vv || !el) return;
|
|
||||||
|
|
||||||
const sync = () => {
|
|
||||||
el.style.top = `${vv.offsetTop}px`;
|
|
||||||
el.style.height = `${vv.height}px`;
|
|
||||||
el.style.bottom = 'auto';
|
|
||||||
};
|
|
||||||
sync();
|
|
||||||
vv.addEventListener('resize', sync);
|
|
||||||
vv.addEventListener('scroll', sync);
|
|
||||||
return () => {
|
|
||||||
vv.removeEventListener('resize', sync);
|
|
||||||
vv.removeEventListener('scroll', sync);
|
|
||||||
};
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
if (!isOpen || typeof window === 'undefined') return null;
|
if (!isOpen || typeof window === 'undefined') return null;
|
||||||
|
|
||||||
const handleOverlayClick = (e: React.MouseEvent) => {
|
const handleOverlayClick = (e: React.MouseEvent) => {
|
||||||
@@ -76,7 +48,7 @@ export function Modal({ isOpen, onClose, children, title, size = 'medium' }: Mod
|
|||||||
};
|
};
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div ref={overlayRef} className={styles.overlay} onClick={handleOverlayClick}>
|
<div className={styles.overlay} onClick={handleOverlayClick}>
|
||||||
<div className={`${styles.modal} ${styles[size]}`}>
|
<div className={`${styles.modal} ${styles[size]}`}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
{title && <h2 className={styles.title}>{title}</h2>}
|
{title && <h2 className={styles.title}>{title}</h2>}
|
||||||
|
|||||||
@@ -269,24 +269,9 @@ export function SchoolDetailView({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [navItems.map(n => n.id).join(',')]);
|
}, [navItems.map(n => n.id).join(',')]);
|
||||||
|
|
||||||
// A report card is identified by the presence of report-card area
|
|
||||||
// judgements, NOT by `framework` — the API sets `framework` to the raw
|
|
||||||
// event grouping (e.g. "Schools - S5") even for report-card schools, so
|
|
||||||
// the old `framework === 'ReportCard'` test never matched and report cards
|
|
||||||
// were rendered as legacy ratings dated to a pre-Nov-2025 inspection.
|
|
||||||
const isReportCard = !!(
|
|
||||||
ofsted?.report_card && Object.keys(ofsted.report_card).length > 0
|
|
||||||
);
|
|
||||||
// A report card is dated by its own inspection (rc_inspection_date); the
|
|
||||||
// legacy inspection_date belongs to an older inspection and must never
|
|
||||||
// date a report card (report cards exist only from Nov 2025).
|
|
||||||
const ofstedInspectedDate = isReportCard
|
|
||||||
? ofsted?.rc_inspection_date ?? null
|
|
||||||
: ofsted?.inspection_date ?? null;
|
|
||||||
|
|
||||||
// ── Ofsted: detect if all OEIF sub-grades match the overall ───────────
|
// ── Ofsted: detect if all OEIF sub-grades match the overall ───────────
|
||||||
const oeifAllSameGrade = (() => {
|
const oeifAllSameGrade = (() => {
|
||||||
if (!ofsted || isReportCard) return false;
|
if (!ofsted || ofsted.framework === 'ReportCard') return false;
|
||||||
const subs = [
|
const subs = [
|
||||||
ofsted.quality_of_education,
|
ofsted.quality_of_education,
|
||||||
ofsted.behaviour_attitudes,
|
ofsted.behaviour_attitudes,
|
||||||
@@ -522,10 +507,10 @@ export function SchoolDetailView({
|
|||||||
{ofsted && (
|
{ofsted && (
|
||||||
<section id="ofsted" className={styles.card}>
|
<section id="ofsted" className={styles.card}>
|
||||||
<h2 className={styles.sectionTitle}>
|
<h2 className={styles.sectionTitle}>
|
||||||
{isReportCard ? 'Ofsted Report Card' : 'Ofsted Rating'}
|
{ofsted.framework === 'ReportCard' ? 'Ofsted Report Card' : 'Ofsted Rating'}
|
||||||
{ofstedInspectedDate && (
|
{ofsted.inspection_date && (
|
||||||
<span className={styles.ofstedDate}>
|
<span className={styles.ofstedDate}>
|
||||||
Inspected {new Date(ofstedInspectedDate).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })}
|
Inspected {new Date(ofsted.inspection_date).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<a
|
<a
|
||||||
@@ -540,7 +525,7 @@ export function SchoolDetailView({
|
|||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{isReportCard ? (
|
{ofsted.framework === 'ReportCard' ? (
|
||||||
/* ── New Report Card layout ── */
|
/* ── New Report Card layout ── */
|
||||||
<>
|
<>
|
||||||
<p className={styles.ofstedDisclaimer}>
|
<p className={styles.ofstedDisclaimer}>
|
||||||
@@ -622,8 +607,8 @@ export function SchoolDetailView({
|
|||||||
</h2>
|
</h2>
|
||||||
<p className={styles.sectionSubtitle}>
|
<p className={styles.sectionSubtitle}>
|
||||||
{isSecondary
|
{isSecondary
|
||||||
? 'GCSE results for Year 11 pupils. England averages shown for comparison.'
|
? 'GCSE results for Year 11 pupils. National averages shown for comparison.'
|
||||||
: 'End-of-primary-school tests taken by Year 6 pupils. England averages shown for comparison.'}
|
: 'End-of-primary-school tests taken by Year 6 pupils. National averages shown for comparison.'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* ── Primary / KS2 content ── */}
|
{/* ── Primary / KS2 content ── */}
|
||||||
@@ -648,7 +633,7 @@ export function SchoolDetailView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{primaryAvg.rwm_expected_pct != null && (
|
{primaryAvg.rwm_expected_pct != null && (
|
||||||
<div className={styles.heroStatHint}>England avg: {primaryAvg.rwm_expected_pct.toFixed(0)}%</div>
|
<div className={styles.heroStatHint}>National avg: {primaryAvg.rwm_expected_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -670,7 +655,7 @@ export function SchoolDetailView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{primaryAvg.rwm_high_pct != null && (
|
{primaryAvg.rwm_high_pct != null && (
|
||||||
<div className={styles.heroStatHint}>England avg: {primaryAvg.rwm_high_pct.toFixed(0)}%</div>
|
<div className={styles.heroStatHint}>National avg: {primaryAvg.rwm_high_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -776,7 +761,7 @@ export function SchoolDetailView({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.metricValue}>{latestResults.attainment_8_score.toFixed(1)}</div>
|
<div className={styles.metricValue}>{latestResults.attainment_8_score.toFixed(1)}</div>
|
||||||
{secondaryAvg.attainment_8_score != null && (
|
{secondaryAvg.attainment_8_score != null && (
|
||||||
<div className={styles.metricHint}>England avg: {secondaryAvg.attainment_8_score.toFixed(1)}</div>
|
<div className={styles.metricHint}>National avg: {secondaryAvg.attainment_8_score.toFixed(1)}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -800,7 +785,7 @@ export function SchoolDetailView({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.metricValue}>{formatPercentage(latestResults.english_maths_standard_pass_pct)}</div>
|
<div className={styles.metricValue}>{formatPercentage(latestResults.english_maths_standard_pass_pct)}</div>
|
||||||
{secondaryAvg.english_maths_standard_pass_pct != null && (
|
{secondaryAvg.english_maths_standard_pass_pct != null && (
|
||||||
<div className={styles.metricHint}>England avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%</div>
|
<div className={styles.metricHint}>National avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -812,7 +797,7 @@ export function SchoolDetailView({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.metricValue}>{formatPercentage(latestResults.english_maths_strong_pass_pct)}</div>
|
<div className={styles.metricValue}>{formatPercentage(latestResults.english_maths_strong_pass_pct)}</div>
|
||||||
{secondaryAvg.english_maths_strong_pass_pct != null && (
|
{secondaryAvg.english_maths_strong_pass_pct != null && (
|
||||||
<div className={styles.metricHint}>England avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%</div>
|
<div className={styles.metricHint}>National avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -968,7 +953,7 @@ export function SchoolDetailView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{primaryAvg.eal_pct != null && (
|
{primaryAvg.eal_pct != null && (
|
||||||
<div className={styles.heroStatHint}>England avg: {primaryAvg.eal_pct.toFixed(0)}%</div>
|
<div className={styles.heroStatHint}>National avg: {primaryAvg.eal_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -985,7 +970,7 @@ export function SchoolDetailView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{primaryAvg.sen_support_pct != null && (
|
{primaryAvg.sen_support_pct != null && (
|
||||||
<div className={styles.heroStatHint}>England avg: {primaryAvg.sen_support_pct.toFixed(0)}%</div>
|
<div className={styles.heroStatHint}>National avg: {primaryAvg.sen_support_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1157,7 +1142,7 @@ export function SchoolDetailView({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.metricValue}>{formatPercentage(absenceData.overall_absence_rate)}</div>
|
<div className={styles.metricValue}>{formatPercentage(absenceData.overall_absence_rate)}</div>
|
||||||
{primaryAvg.overall_absence_pct != null && (
|
{primaryAvg.overall_absence_pct != null && (
|
||||||
<div className={styles.metricHint}>England avg: ~{primaryAvg.overall_absence_pct.toFixed(1)}%</div>
|
<div className={styles.metricHint}>National avg: ~{primaryAvg.overall_absence_pct.toFixed(1)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1169,7 +1154,7 @@ export function SchoolDetailView({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.metricValue}>{formatPercentage(absenceData.persistent_absence_rate)}</div>
|
<div className={styles.metricValue}>{formatPercentage(absenceData.persistent_absence_rate)}</div>
|
||||||
{primaryAvg.persistent_absence_pct != null && (
|
{primaryAvg.persistent_absence_pct != null && (
|
||||||
<div className={styles.metricHint}>England avg: ~{primaryAvg.persistent_absence_pct.toFixed(0)}%</div>
|
<div className={styles.metricHint}>National avg: ~{primaryAvg.persistent_absence_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary, #1a1612);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-family: var(--font-playfair), 'Playfair Display', serif;
|
||||||
|
}
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
background: var(--accent-gold-bg);
|
background: var(--accent-gold-bg);
|
||||||
border: 1px solid var(--accent-gold, #c9a227);
|
border: 1px solid var(--accent-gold, #c9a227);
|
||||||
@@ -111,16 +119,12 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary, #1a1612);
|
color: var(--text-primary, #1a1612);
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.5rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resultButton {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schoolMeta {
|
.schoolMeta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -155,30 +159,21 @@
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* One scroll container on mobile: the modal content itself scrolls, so the
|
.title {
|
||||||
results list must not add its own inner scroll (double scrollbars, and
|
font-size: 1.25rem;
|
||||||
the input would be trapped above a short 400px window when the keyboard
|
|
||||||
shrinks the sheet). */
|
|
||||||
.results {
|
|
||||||
max-height: none;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compact stacked card: name + meta, then a full-width action so the tap
|
|
||||||
target is obvious and the card doesn't waste vertical space. */
|
|
||||||
.resultItem {
|
.resultItem {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 0.625rem;
|
|
||||||
padding: 0.875rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.resultButton {
|
.addButton {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.schoolMeta {
|
.schoolMeta {
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
gap: 0.25rem 1rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,8 +81,10 @@ export function SchoolSearchModal({ isOpen, onClose }: SchoolSearchModalProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={handleClose} title="Add School to Comparison">
|
<Modal isOpen={isOpen} onClose={handleClose}>
|
||||||
<div className={styles.modalContent}>
|
<div className={styles.modalContent}>
|
||||||
|
<h2 className={styles.title}>Add School to Comparison</h2>
|
||||||
|
|
||||||
{!canAddMore && (
|
{!canAddMore && (
|
||||||
<div className={styles.warning}>
|
<div className={styles.warning}>
|
||||||
Maximum 5 schools can be compared. Remove a school to add another.
|
Maximum 5 schools can be compared. Remove a school to add another.
|
||||||
@@ -127,9 +129,9 @@ export function SchoolSearchModal({ isOpen, onClose }: SchoolSearchModalProps) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleAddSchool(school)}
|
onClick={() => handleAddSchool(school)}
|
||||||
disabled={alreadySelected || !canAddMore}
|
disabled={alreadySelected || !canAddMore}
|
||||||
className={`${styles.resultButton} ${
|
className={
|
||||||
alreadySelected ? "btn btn-active" : "btn btn-secondary"
|
alreadySelected ? "btn btn-active" : "btn btn-secondary"
|
||||||
}`}
|
}
|
||||||
>
|
>
|
||||||
{alreadySelected ? "✓ Comparing" : "+ Compare"}
|
{alreadySelected ? "✓ Comparing" : "+ Compare"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -186,23 +186,9 @@ export function SecondarySchoolDetailView({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [navItems.map(n => n.id).join(',')]);
|
}, [navItems.map(n => n.id).join(',')]);
|
||||||
|
|
||||||
// A report card is identified by the presence of report-card area
|
|
||||||
// judgements, NOT by `framework` — the API sets `framework` to the raw
|
|
||||||
// event grouping (e.g. "Schools - S5") even for report-card schools, so
|
|
||||||
// the old `framework === 'ReportCard'` test never matched and report cards
|
|
||||||
// were rendered as legacy ratings dated to a pre-Nov-2025 inspection.
|
|
||||||
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).
|
|
||||||
const ofstedInspectedDate = isReportCard
|
|
||||||
? ofsted?.rc_inspection_date ?? null
|
|
||||||
: ofsted?.inspection_date ?? null;
|
|
||||||
|
|
||||||
// ── Ofsted: detect if all OEIF sub-grades match the overall ───────────
|
// ── Ofsted: detect if all OEIF sub-grades match the overall ───────────
|
||||||
const oeifAllSameGrade = (() => {
|
const oeifAllSameGrade = (() => {
|
||||||
if (!ofsted || isReportCard) return false;
|
if (!ofsted || ofsted.framework === 'ReportCard') return false;
|
||||||
const subs = [
|
const subs = [
|
||||||
ofsted.quality_of_education,
|
ofsted.quality_of_education,
|
||||||
ofsted.behaviour_attitudes,
|
ofsted.behaviour_attitudes,
|
||||||
@@ -346,10 +332,10 @@ export function SecondarySchoolDetailView({
|
|||||||
{ofsted && (
|
{ofsted && (
|
||||||
<section id="ofsted" className={styles.card}>
|
<section id="ofsted" className={styles.card}>
|
||||||
<h2 className={styles.sectionTitle}>
|
<h2 className={styles.sectionTitle}>
|
||||||
{isReportCard ? 'Ofsted Report Card' : 'Ofsted Rating'}
|
{ofsted.framework === 'ReportCard' ? 'Ofsted Report Card' : 'Ofsted Rating'}
|
||||||
{ofstedInspectedDate && (
|
{ofsted.inspection_date && (
|
||||||
<span className={styles.ofstedDate}>
|
<span className={styles.ofstedDate}>
|
||||||
{' '}Inspected {new Date(ofstedInspectedDate).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })}
|
{' '}Inspected {new Date(ofsted.inspection_date).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<a
|
<a
|
||||||
@@ -363,7 +349,7 @@ export function SecondarySchoolDetailView({
|
|||||||
Ofsted reports ↗
|
Ofsted reports ↗
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
{isReportCard ? (
|
{ofsted.framework === 'ReportCard' ? (
|
||||||
<>
|
<>
|
||||||
<p className={styles.ofstedDisclaimer}>
|
<p className={styles.ofstedDisclaimer}>
|
||||||
From November 2025, Ofsted replaced single overall grades with Report Cards rating schools across several areas.
|
From November 2025, Ofsted replaced single overall grades with Report Cards rating schools across several areas.
|
||||||
@@ -463,7 +449,7 @@ export function SecondarySchoolDetailView({
|
|||||||
GCSE Results ({formatAcademicYear(latestResults.year)})
|
GCSE Results ({formatAcademicYear(latestResults.year)})
|
||||||
</h2>
|
</h2>
|
||||||
<p className={styles.sectionSubtitle}>
|
<p className={styles.sectionSubtitle}>
|
||||||
GCSE results for Year 11 pupils. England averages shown for comparison.
|
GCSE results for Year 11 pupils. National averages shown for comparison.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{p8Suspended && (
|
{p8Suspended && (
|
||||||
@@ -492,7 +478,7 @@ export function SecondarySchoolDetailView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{secondaryAvg.attainment_8_score != null && (
|
{secondaryAvg.attainment_8_score != null && (
|
||||||
<div className={styles.heroStatHint}>England avg: {secondaryAvg.attainment_8_score.toFixed(1)}</div>
|
<div className={styles.heroStatHint}>National avg: {secondaryAvg.attainment_8_score.toFixed(1)}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -532,7 +518,7 @@ export function SecondarySchoolDetailView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{secondaryAvg.english_maths_strong_pass_pct != null && (
|
{secondaryAvg.english_maths_strong_pass_pct != null && (
|
||||||
<div className={styles.heroStatHint}>England avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%</div>
|
<div className={styles.heroStatHint}>National avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -554,7 +540,7 @@ export function SecondarySchoolDetailView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{secondaryAvg.english_maths_standard_pass_pct != null && (
|
{secondaryAvg.english_maths_standard_pass_pct != null && (
|
||||||
<div className={styles.heroStatHint}>England avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%</div>
|
<div className={styles.heroStatHint}>National avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -162,21 +162,6 @@ export function CompareAcademics({
|
|||||||
const grade5 = latestValues(data, urns, 'english_maths_strong_pass_pct');
|
const grade5 = latestValues(data, urns, 'english_maths_strong_pass_pct');
|
||||||
const ebacc = latestValues(data, urns, 'ebacc_entry_pct');
|
const ebacc = latestValues(data, urns, 'ebacc_entry_pct');
|
||||||
const att8Anchor = nationalAverages?.secondary?.attainment_8_score;
|
const att8Anchor = nationalAverages?.secondary?.attainment_8_score;
|
||||||
const grade5Anchor = nationalAverages?.secondary?.english_maths_strong_pass_pct;
|
|
||||||
const ebaccAnchor = nationalAverages?.secondary?.ebacc_entry_pct;
|
|
||||||
|
|
||||||
// Every headline number gets its England anchor + verdict chip, so the
|
|
||||||
// "anchored against the England average" promise holds for the grade-5
|
|
||||||
// and EBacc rows too, not just Attainment 8.
|
|
||||||
const anchorChip = (value: number | null, anchor: number | null | undefined, tol: number) => {
|
|
||||||
if (value == null || anchor == null) return null;
|
|
||||||
const v = verdict(value, anchor, tol);
|
|
||||||
return (
|
|
||||||
<Chip tone={v === 'above' ? 'good' : v === 'below' ? 'warn' : 'neutral'}>
|
|
||||||
{v === 'above' ? 'Above' : v === 'below' ? 'Below' : 'Close to'} England average
|
|
||||||
</Chip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section
|
<Section
|
||||||
@@ -189,8 +174,7 @@ export function CompareAcademics({
|
|||||||
<Cell key={school.urn} school={school} index={i}>
|
<Cell key={school.urn} school={school} index={i}>
|
||||||
{att8[i] != null ? (
|
{att8[i] != null ? (
|
||||||
<>
|
<>
|
||||||
<span className={s.big}>{(att8[i] as number).toFixed(1)}</span>{' '}
|
<span className={s.big}>{(att8[i] as number).toFixed(1)}</span>
|
||||||
{anchorChip(att8[i], att8Anchor, 2)}
|
|
||||||
{att8Anchor != null && (
|
{att8Anchor != null && (
|
||||||
<span className={s.small}>England average {att8Anchor.toFixed(1)}</span>
|
<span className={s.small}>England average {att8Anchor.toFixed(1)}</span>
|
||||||
)}
|
)}
|
||||||
@@ -232,38 +216,14 @@ export function CompareAcademics({
|
|||||||
</RowLabel>
|
</RowLabel>
|
||||||
{schools.map((school, i) => (
|
{schools.map((school, i) => (
|
||||||
<Cell key={school.urn} school={school} index={i}>
|
<Cell key={school.urn} school={school} index={i}>
|
||||||
{grade5[i] != null ? (
|
{grade5[i] != null ? `${Math.round(grade5[i] as number)}%` : <span className={s.small}>No data</span>}
|
||||||
<>
|
|
||||||
<span className={s.big} style={{ fontSize: '1.1rem' }}>
|
|
||||||
{Math.round(grade5[i] as number)}%
|
|
||||||
</span>{' '}
|
|
||||||
{anchorChip(grade5[i], grade5Anchor, 3)}
|
|
||||||
{grade5Anchor != null && (
|
|
||||||
<span className={s.small}>England average {Math.round(grade5Anchor)}%</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className={s.small}>No data</span>
|
|
||||||
)}
|
|
||||||
</Cell>
|
</Cell>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<RowLabel tip="% entering the English Baccalaureate subject combination.">EBacc entry</RowLabel>
|
<RowLabel tip="% entering the English Baccalaureate subject combination.">EBacc entry</RowLabel>
|
||||||
{schools.map((school, i) => (
|
{schools.map((school, i) => (
|
||||||
<Cell key={school.urn} school={school} index={i}>
|
<Cell key={school.urn} school={school} index={i}>
|
||||||
{ebacc[i] != null ? (
|
{ebacc[i] != null ? `${Math.round(ebacc[i] as number)}%` : <span className={s.small}>No data</span>}
|
||||||
<>
|
|
||||||
<span className={s.big} style={{ fontSize: '1.1rem' }}>
|
|
||||||
{Math.round(ebacc[i] as number)}%
|
|
||||||
</span>{' '}
|
|
||||||
{anchorChip(ebacc[i], ebaccAnchor, 3)}
|
|
||||||
{ebaccAnchor != null && (
|
|
||||||
<span className={s.small}>England average {Math.round(ebaccAnchor)}%</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className={s.small}>No data</span>
|
|
||||||
)}
|
|
||||||
</Cell>
|
</Cell>
|
||||||
))}
|
))}
|
||||||
</SectionGrid>
|
</SectionGrid>
|
||||||
|
|||||||
@@ -239,17 +239,8 @@ export function CompareOfsted({
|
|||||||
`https://reports.ofsted.gov.uk/provider/21/${school.urn}`;
|
`https://reports.ofsted.gov.uk/provider/21/${school.urn}`;
|
||||||
return (
|
return (
|
||||||
<Cell key={school.urn} school={school} index={i}>
|
<Cell key={school.urn} school={school} index={i}>
|
||||||
{/* Short visible label to save space on mobile (the coloured
|
<a className={s.link} href={url} target="_blank" rel="noopener noreferrer">
|
||||||
school name already leads the row); the full name stays in
|
{school.school_name}'s Ofsted page →
|
||||||
aria-label so screen readers can tell the links apart. */}
|
|
||||||
<a
|
|
||||||
className={s.link}
|
|
||||||
href={url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label={`${school.school_name}'s Ofsted page`}
|
|
||||||
>
|
|
||||||
Ofsted page →
|
|
||||||
</a>
|
</a>
|
||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -72,18 +72,15 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
/* Slightly larger than the values below it so the school each row belongs
|
font-size: 0.8rem;
|
||||||
to is easy to read on mobile (hidden on desktop, where the column header
|
|
||||||
names the school). */
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--sc, var(--text-secondary));
|
color: var(--sc, var(--text-secondary));
|
||||||
margin-bottom: 0.2rem;
|
margin-bottom: 0.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cellDot {
|
.cellDot {
|
||||||
width: 10px;
|
width: 9px;
|
||||||
height: 10px;
|
height: 9px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--dot, var(--text-muted));
|
background: var(--dot, var(--text-muted));
|
||||||
flex: none;
|
flex: none;
|
||||||
|
|||||||
@@ -68,9 +68,6 @@ export interface School {
|
|||||||
|
|
||||||
// Ofsted (for list view — summary only)
|
// Ofsted (for list view — summary only)
|
||||||
ofsted_grade?: 1 | 2 | 3 | 4 | null;
|
ofsted_grade?: 1 | 2 | 3 | 4 | null;
|
||||||
/** Report-card inspection date (Nov 2025+); non-null identifies a report
|
|
||||||
* card in the list/map, where the full report_card object isn't available. */
|
|
||||||
ofsted_rc_date?: string | null;
|
|
||||||
ofsted_date?: string | null;
|
ofsted_date?: string | null;
|
||||||
ofsted_framework?: string | null;
|
ofsted_framework?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-14
@@ -691,12 +691,9 @@ export interface OfstedListBadge {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the Ofsted badge for a school card in the list/map view.
|
* Build the Ofsted badge for a school card in the list/map view.
|
||||||
* States, in priority order:
|
* Three states:
|
||||||
* - Report Card school (ofsted_rc_date set): "Report Card · YYYY" in purple.
|
|
||||||
* Checked FIRST so it wins over any carried-forward legacy grade — the
|
|
||||||
* list has no full report_card object, and ofsted_framework is the raw
|
|
||||||
* event grouping ("Schools - S5"), never "ReportCard".
|
|
||||||
* - OEIF school (ofsted_grade set): grade word + year, colour-keyed
|
* - OEIF school (ofsted_grade set): grade word + year, colour-keyed
|
||||||
|
* - ReportCard school (ofsted_framework === 'ReportCard'): "Report Card · YYYY" in purple
|
||||||
* - Inspected without an overall grade (OEIF post-Sept-2024, where Ofsted no
|
* - Inspected without an overall grade (OEIF post-Sept-2024, where Ofsted no
|
||||||
* longer issues an overall judgement): "Inspected · YYYY" — mirrors the
|
* longer issues an overall judgement): "Inspected · YYYY" — mirrors the
|
||||||
* detail page's hero chip so a school never reads as both inspected and
|
* detail page's hero chip so a school never reads as both inspected and
|
||||||
@@ -707,16 +704,7 @@ export function buildOfstedListBadge(school: {
|
|||||||
ofsted_grade?: 1 | 2 | 3 | 4 | null;
|
ofsted_grade?: 1 | 2 | 3 | 4 | null;
|
||||||
ofsted_date?: string | null;
|
ofsted_date?: string | null;
|
||||||
ofsted_framework?: string | null;
|
ofsted_framework?: string | null;
|
||||||
ofsted_rc_date?: string | null;
|
|
||||||
}): OfstedListBadge {
|
}): OfstedListBadge {
|
||||||
// A report card wins over any carried-forward legacy grade — signalled by
|
|
||||||
// ofsted_rc_date. ofsted_framework is the raw event grouping ("Schools -
|
|
||||||
// S5"), never "ReportCard", so it can't detect report cards.
|
|
||||||
if (school.ofsted_rc_date) {
|
|
||||||
const rcYear = new Date(school.ofsted_rc_date).getFullYear();
|
|
||||||
return { label: `Report Card · ${rcYear}`, cssClass: 'ofstedRc' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const year = school.ofsted_date
|
const year = school.ofsted_date
|
||||||
? new Date(school.ofsted_date).getFullYear()
|
? new Date(school.ofsted_date).getFullYear()
|
||||||
: null;
|
: null;
|
||||||
@@ -735,6 +723,10 @@ export function buildOfstedListBadge(school: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (school.ofsted_framework === 'ReportCard') {
|
||||||
|
return { label: `Report Card${yearStr}`, cssClass: 'ofstedRc' };
|
||||||
|
}
|
||||||
|
|
||||||
// An inspection is on record (date or framework present) but carries no
|
// An inspection is on record (date or framework present) but carries no
|
||||||
// overall grade — a post-Sept-2024 OEIF inspection. Distinct from a school
|
// overall grade — a post-Sept-2024 OEIF inspection. Distinct from a school
|
||||||
// that has genuinely never been inspected.
|
// that has genuinely never been inspected.
|
||||||
|
|||||||
Reference in New Issue
Block a user