Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21c4add408 |
@@ -172,35 +172,15 @@ test('an all-through school shows BOTH its KS2 SATs and its GCSE results, not ju
|
|||||||
// Labelled as all-through in the hero meta.
|
// Labelled as all-through in the hero meta.
|
||||||
await expect(page.getByText(/All-through/i).first()).toBeVisible();
|
await expect(page.getByText(/All-through/i).first()).toBeVisible();
|
||||||
|
|
||||||
// The combined results section carries both phases.
|
// The combined results section carries both phases, each under its own
|
||||||
|
// all-through sub-heading (a unique visible node — unlike "Attainment 8",
|
||||||
|
// which also appears in hidden MetricTooltip labels).
|
||||||
const results = page.locator('#results');
|
const results = page.locator('#results');
|
||||||
await expect(results.getByText(/SATs & GCSE Results/)).toBeVisible();
|
await expect(results.getByText(/SATs & GCSE Results/)).toBeVisible();
|
||||||
await expect(results.getByRole('heading', { name: /Primary.*KS2 SATs/ })).toBeVisible(); // KS2 block
|
await expect(results.getByRole('heading', { name: /Primary.*KS2 SATs/ })).toBeVisible(); // KS2 block
|
||||||
await expect(results.getByRole('heading', { name: /Secondary.*GCSEs/ })).toBeVisible(); // KS4 block
|
await expect(results.getByRole('heading', { name: /Secondary.*GCSEs/ })).toBeVisible(); // KS4 block
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a special school is not shown as failing against the mainstream England average', async ({ page }) => {
|
|
||||||
// Special schools sit the same tests but very few pupils reach the mainstream
|
|
||||||
// "expected standard", so a "0.0% · −62 pts below England average" rendering
|
|
||||||
// portrays them as failing against a benchmark that doesn't fit. The results
|
|
||||||
// section drops the England comparison and explains the context instead.
|
|
||||||
const SP_URN = 101099; // Greenmead School — a community special school
|
|
||||||
const res = await page.request.get(`/api/schools/${SP_URN}`);
|
|
||||||
expect(res.ok()).toBeTruthy();
|
|
||||||
const detail = await res.json();
|
|
||||||
test.skip(
|
|
||||||
!/special|pupil referral|alternative provision/i.test(detail.school_info?.school_type ?? ''),
|
|
||||||
'precondition: chosen URN must currently be a special school',
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.goto(`/school/${SP_URN}`);
|
|
||||||
const results = page.locator('#results');
|
|
||||||
// The special-school context note is shown…
|
|
||||||
await expect(results.getByText(/This is a special school/i)).toBeVisible({ timeout: 15_000 });
|
|
||||||
// …and the mainstream England-average comparison is dropped entirely.
|
|
||||||
await expect(results.getByText(/England avg/i)).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,33 +0,0 @@
|
|||||||
/**
|
|
||||||
* SpecialSchoolNote Component Tests
|
|
||||||
*/
|
|
||||||
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import { SpecialSchoolNote } from '@/components/SpecialSchoolNote';
|
|
||||||
|
|
||||||
describe('SpecialSchoolNote', () => {
|
|
||||||
it('renders nothing for a mainstream school', () => {
|
|
||||||
const { container } = render(<SpecialSchoolNote school={{ school_type: 'Academy converter' }} />);
|
|
||||||
expect(container).toBeEmptyDOMElement();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('describes a special school as teaching pupils with SEND', () => {
|
|
||||||
render(<SpecialSchoolNote school={{ school_type: 'Community special school' }} />);
|
|
||||||
expect(screen.getByText(/This is a special school/i)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(/special educational needs/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does NOT call PRU pupils SEND — they are educated outside a mainstream school', () => {
|
|
||||||
render(<SpecialSchoolNote school={{ school_type: 'Pupil referral unit' }} />);
|
|
||||||
expect(screen.getByText(/This is a pupil referral unit/i)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(/educated outside a mainstream school/i)).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText(/special educational needs/i)).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does NOT call alternative-provision pupils SEND', () => {
|
|
||||||
render(<SpecialSchoolNote school={{ school_type: 'Academy alternative provision converter' }} />);
|
|
||||||
expect(screen.getByText(/This is an alternative provision setting/i)).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText(/special educational needs/i)).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -269,44 +269,6 @@ describe('isProposedToClose', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isSpecialSchool', () => {
|
|
||||||
const { isSpecialSchool } = require('@/lib/utils');
|
|
||||||
|
|
||||||
it('matches every DfE special-school establishment type', () => {
|
|
||||||
for (const t of [
|
|
||||||
'Community special school',
|
|
||||||
'Foundation special school',
|
|
||||||
'Academy special converter',
|
|
||||||
'Academy special sponsor led',
|
|
||||||
'Non-maintained special school',
|
|
||||||
'Free schools special',
|
|
||||||
'Other independent special school',
|
|
||||||
]) {
|
|
||||||
expect(isSpecialSchool({ school_type: t })).toBe(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('matches pupil referral units and alternative provision', () => {
|
|
||||||
expect(isSpecialSchool({ school_type: 'Pupil referral unit' })).toBe(true);
|
|
||||||
expect(isSpecialSchool({ school_type: 'Academy alternative provision converter' })).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not match mainstream types (no false positives)', () => {
|
|
||||||
for (const t of [
|
|
||||||
'Academy converter',
|
|
||||||
'Community school',
|
|
||||||
'Voluntary aided school',
|
|
||||||
'Free schools',
|
|
||||||
'University technical college',
|
|
||||||
'Other independent school',
|
|
||||||
null,
|
|
||||||
undefined,
|
|
||||||
]) {
|
|
||||||
expect(isSpecialSchool({ school_type: t })).toBe(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('shortName', () => {
|
describe('shortName', () => {
|
||||||
it('drops the trailing establishment-type words', () => {
|
it('drops the trailing establishment-type words', () => {
|
||||||
expect(shortName('Barclay Primary School')).toBe('Barclay');
|
expect(shortName('Barclay Primary School')).toBe('Barclay');
|
||||||
|
|||||||
@@ -1040,41 +1040,6 @@
|
|||||||
color: var(--accent-coral-dark, #b04a2e);
|
color: var(--accent-coral-dark, #b04a2e);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Ofsted grade grids (Report Card + OEIF) ──
|
|
||||||
Uniform, vertically-aligned grade chips. Labels reserve two lines so
|
|
||||||
single- and double-line labels put their chips on the same baseline;
|
|
||||||
every chip (Met, Strong, Expected standard, …) shares one font size,
|
|
||||||
padding and min-height regardless of how many lines its text wraps to. */
|
|
||||||
.gradeGrid .metricCard {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.85rem 0.75rem;
|
|
||||||
}
|
|
||||||
.gradeGrid .metricLabel {
|
|
||||||
min-height: 2.6em;
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.gradeGrid .metricValue {
|
|
||||||
margin-top: auto;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
max-width: 100%;
|
|
||||||
min-height: 2.6em;
|
|
||||||
padding: 0.3rem 0.7rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.25;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ofstedDisclaimer {
|
.ofstedDisclaimer {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--text-muted, #8a847a);
|
color: var(--text-muted, #8a847a);
|
||||||
|
|||||||
@@ -18,10 +18,9 @@ import type {
|
|||||||
SchoolDeprivation, SchoolFinance, NationalAverages,
|
SchoolDeprivation, SchoolFinance, NationalAverages,
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
import {
|
import {
|
||||||
formatPercentage, formatProgress, formatAcademicYear, isProposedToClose, ofstedLegacyAreas, isSpecialSchool,
|
formatPercentage, formatProgress, formatAcademicYear, isProposedToClose, ofstedLegacyAreas,
|
||||||
} from '@/lib/utils';
|
} from '@/lib/utils';
|
||||||
import { DeltaChip } from './DeltaChip';
|
import { DeltaChip } from './DeltaChip';
|
||||||
import { SpecialSchoolNote } from './SpecialSchoolNote';
|
|
||||||
import { summariseAdmissions } from '@/lib/compareLogic';
|
import { summariseAdmissions } from '@/lib/compareLogic';
|
||||||
|
|
||||||
const PerformanceChart = dynamic(
|
const PerformanceChart = dynamic(
|
||||||
@@ -234,27 +233,6 @@ export function SchoolDetailView({
|
|||||||
const hasKS4Results = latestResults != null && latestResults.attainment_8_score != null;
|
const hasKS4Results = latestResults != null && latestResults.attainment_8_score != null;
|
||||||
const hasAnyResults = hasKS2Results || hasKS4Results;
|
const hasAnyResults = hasKS2Results || hasKS4Results;
|
||||||
|
|
||||||
// Special schools / PRUs / AP: their pupils sit the same tests but very few
|
|
||||||
// reach the mainstream "expected standard", so a 0% headline and an England
|
|
||||||
// comparison portray them as failing against a benchmark that doesn't fit.
|
|
||||||
const isSpecial = isSpecialSchool(schoolInfo);
|
|
||||||
// Belt-and-braces for KS2: a whole-row zero attainment (every subject 0 — a
|
|
||||||
// special/suppressed signature) is a placeholder, not a real result. This
|
|
||||||
// needs ALL of RWM + reading + writing + maths to be 0, so a genuine 0%
|
|
||||||
// combined (some pupils met individual subjects but not all three) stays
|
|
||||||
// comparable. Attainment 8 is a single 0–80 score with no subject breakdown
|
|
||||||
// to form such a signature, so KS4 keys off establishment type only — a
|
|
||||||
// genuine (if extreme) 0.0 still shows its real figure and comparison.
|
|
||||||
const ks2Placeholder = latestResults != null
|
|
||||||
&& latestResults.rwm_expected_pct === 0
|
|
||||||
&& (latestResults.reading_expected_pct ?? 0) === 0
|
|
||||||
&& (latestResults.writing_expected_pct ?? 0) === 0
|
|
||||||
&& (latestResults.maths_expected_pct ?? 0) === 0;
|
|
||||||
// Whether to drop the England-average deltas / national markers / "below"
|
|
||||||
// framing on the attainment measures.
|
|
||||||
const suppressKs2Comparison = isSpecial || ks2Placeholder;
|
|
||||||
const suppressKs4Comparison = isSpecial;
|
|
||||||
|
|
||||||
// Build section nav items dynamically — only sections with data.
|
// Build section nav items dynamically — only sections with data.
|
||||||
// Order is engagement-led (from section_nav_used analytics): the most-sought
|
// Order is engagement-led (from section_nav_used analytics): the most-sought
|
||||||
// sections — results, admissions, inclusion, history — sit near the top,
|
// sections — results, admissions, inclusion, history — sit near the top,
|
||||||
@@ -577,7 +555,7 @@ export function SchoolDetailView({
|
|||||||
<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.
|
||||||
</p>
|
</p>
|
||||||
<div className={`${styles.metricsGrid} ${styles.gradeGrid}`}>
|
<div className={styles.metricsGrid}>
|
||||||
{ofsted.rc_safeguarding_met != null && (
|
{ofsted.rc_safeguarding_met != null && (
|
||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>Safeguarding</div>
|
<div className={styles.metricLabel}>Safeguarding</div>
|
||||||
@@ -623,7 +601,7 @@ export function SchoolDetailView({
|
|||||||
Rated <strong>{OFSTED_LABELS[ofsted.overall_effectiveness!]}</strong> across all inspected areas — Quality of Teaching, Behaviour, Pupils' Development and Leadership.
|
Rated <strong>{OFSTED_LABELS[ofsted.overall_effectiveness!]}</strong> across all inspected areas — Quality of Teaching, Behaviour, Pupils' Development and Leadership.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className={`${styles.metricsGrid} ${styles.gradeGrid}`}>
|
<div className={styles.metricsGrid}>
|
||||||
{oeifAreas.map(({ label, value }) => (
|
{oeifAreas.map(({ label, value }) => (
|
||||||
<div key={label} className={styles.metricCard}>
|
<div key={label} className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>{label}</div>
|
<div className={styles.metricLabel}>{label}</div>
|
||||||
@@ -646,22 +624,13 @@ export function SchoolDetailView({
|
|||||||
{isAllThrough ? 'SATs & GCSE Results' : isSecondary ? 'GCSE Results' : 'SATs Results'} ({formatAcademicYear(latestResults.year)})
|
{isAllThrough ? 'SATs & GCSE Results' : isSecondary ? 'GCSE Results' : 'SATs Results'} ({formatAcademicYear(latestResults.year)})
|
||||||
</h2>
|
</h2>
|
||||||
<p className={styles.sectionSubtitle}>
|
<p className={styles.sectionSubtitle}>
|
||||||
{isSpecial
|
{isAllThrough
|
||||||
? (isSecondary
|
? 'KS2 SATs (end of Year 6) and GCSE results (Year 11) — this school covers both. England averages shown for comparison.'
|
||||||
? 'GCSE results for Year 11 pupils.'
|
: isSecondary
|
||||||
: 'End-of-primary-school tests taken by Year 6 pupils.')
|
? 'GCSE results for Year 11 pupils. England averages shown for comparison.'
|
||||||
: isAllThrough
|
: 'End-of-primary-school tests taken by Year 6 pupils. England averages shown for comparison.'}
|
||||||
? 'KS2 SATs (end of Year 6) and GCSE results (Year 11) — this school covers both. England averages shown for comparison.'
|
|
||||||
: isSecondary
|
|
||||||
? 'GCSE results for Year 11 pupils. England averages shown for comparison.'
|
|
||||||
: 'End-of-primary-school tests taken by Year 6 pupils. England averages shown for comparison.'}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Explains up front why the England comparison is dropped below, so
|
|
||||||
a 0% headline never reads as a failing grade against a benchmark
|
|
||||||
that doesn't fit. Type-aware copy (special vs PRU vs AP). */}
|
|
||||||
<SpecialSchoolNote school={schoolInfo} />
|
|
||||||
|
|
||||||
{/* ── Primary / KS2 content ── */}
|
{/* ── Primary / KS2 content ── */}
|
||||||
{hasKS2Results && (
|
{hasKS2Results && (
|
||||||
<>
|
<>
|
||||||
@@ -677,7 +646,7 @@ export function SchoolDetailView({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.heroStatValue}>
|
<div className={styles.heroStatValue}>
|
||||||
{formatPercentage(latestResults.rwm_expected_pct)}
|
{formatPercentage(latestResults.rwm_expected_pct)}
|
||||||
{!suppressKs2Comparison && primaryAvg.rwm_expected_pct != null && (
|
{primaryAvg.rwm_expected_pct != null && (
|
||||||
<DeltaChip
|
<DeltaChip
|
||||||
value={latestResults.rwm_expected_pct}
|
value={latestResults.rwm_expected_pct}
|
||||||
baseline={primaryAvg.rwm_expected_pct}
|
baseline={primaryAvg.rwm_expected_pct}
|
||||||
@@ -686,7 +655,7 @@ export function SchoolDetailView({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!suppressKs2Comparison && 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}>England avg: {primaryAvg.rwm_expected_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -699,7 +668,7 @@ export function SchoolDetailView({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.heroStatValue}>
|
<div className={styles.heroStatValue}>
|
||||||
{formatPercentage(latestResults.rwm_high_pct)}
|
{formatPercentage(latestResults.rwm_high_pct)}
|
||||||
{!suppressKs2Comparison && primaryAvg.rwm_high_pct != null && (
|
{primaryAvg.rwm_high_pct != null && (
|
||||||
<DeltaChip
|
<DeltaChip
|
||||||
value={latestResults.rwm_high_pct}
|
value={latestResults.rwm_high_pct}
|
||||||
baseline={primaryAvg.rwm_high_pct}
|
baseline={primaryAvg.rwm_high_pct}
|
||||||
@@ -708,15 +677,14 @@ export function SchoolDetailView({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!suppressKs2Comparison && 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}>England avg: {primaryAvg.rwm_high_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!suppressKs2Comparison &&
|
{latestResults.rwm_expected_pct != null &&
|
||||||
latestResults.rwm_expected_pct != null &&
|
|
||||||
latestResults.reading_expected_pct != null &&
|
latestResults.reading_expected_pct != null &&
|
||||||
latestResults.writing_expected_pct != null &&
|
latestResults.writing_expected_pct != null &&
|
||||||
latestResults.maths_expected_pct != null && (
|
latestResults.maths_expected_pct != null && (
|
||||||
@@ -740,34 +708,28 @@ export function SchoolDetailView({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* All-zero placeholder rows (special / suppressed) would render as
|
<SatsChart
|
||||||
three empty bars against the national markers — misleading, so
|
subjects={[
|
||||||
skip the chart. For a special school with some non-zero
|
{
|
||||||
subjects, keep the bars but drop the national markers. */}
|
name: 'Reading',
|
||||||
{!ks2Placeholder && (
|
expectedPct: latestResults.reading_expected_pct,
|
||||||
<SatsChart
|
exceedingPct: latestResults.reading_high_pct,
|
||||||
subjects={[
|
nationalExpectedPct: primaryAvg.reading_expected_pct,
|
||||||
{
|
},
|
||||||
name: 'Reading',
|
{
|
||||||
expectedPct: latestResults.reading_expected_pct,
|
name: 'Writing',
|
||||||
exceedingPct: latestResults.reading_high_pct,
|
expectedPct: latestResults.writing_expected_pct,
|
||||||
nationalExpectedPct: suppressKs2Comparison ? null : primaryAvg.reading_expected_pct,
|
exceedingPct: latestResults.writing_high_pct,
|
||||||
},
|
nationalExpectedPct: primaryAvg.writing_expected_pct,
|
||||||
{
|
},
|
||||||
name: 'Writing',
|
{
|
||||||
expectedPct: latestResults.writing_expected_pct,
|
name: 'Maths',
|
||||||
exceedingPct: latestResults.writing_high_pct,
|
expectedPct: latestResults.maths_expected_pct,
|
||||||
nationalExpectedPct: suppressKs2Comparison ? null : primaryAvg.writing_expected_pct,
|
exceedingPct: latestResults.maths_high_pct,
|
||||||
},
|
nationalExpectedPct: primaryAvg.maths_expected_pct,
|
||||||
{
|
},
|
||||||
name: 'Maths',
|
]}
|
||||||
expectedPct: latestResults.maths_expected_pct,
|
/>
|
||||||
exceedingPct: latestResults.maths_high_pct,
|
|
||||||
nationalExpectedPct: suppressKs2Comparison ? null : primaryAvg.maths_expected_pct,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Progress scores row */}
|
{/* Progress scores row */}
|
||||||
{(latestResults.reading_progress != null || latestResults.writing_progress != null || latestResults.maths_progress != null) && (
|
{(latestResults.reading_progress != null || latestResults.writing_progress != null || latestResults.maths_progress != null) && (
|
||||||
@@ -824,7 +786,7 @@ export function SchoolDetailView({
|
|||||||
<MetricTooltip metricKey="attainment_8_score" />
|
<MetricTooltip metricKey="attainment_8_score" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.metricValue}>{latestResults.attainment_8_score.toFixed(1)}</div>
|
<div className={styles.metricValue}>{latestResults.attainment_8_score.toFixed(1)}</div>
|
||||||
{!suppressKs4Comparison && 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}>England avg: {secondaryAvg.attainment_8_score.toFixed(1)}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -848,7 +810,7 @@ export function SchoolDetailView({
|
|||||||
<MetricTooltip metricKey="english_maths_standard_pass_pct" />
|
<MetricTooltip metricKey="english_maths_standard_pass_pct" />
|
||||||
</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>
|
||||||
{!suppressKs4Comparison && 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}>England avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -860,7 +822,7 @@ export function SchoolDetailView({
|
|||||||
<MetricTooltip metricKey="english_maths_strong_pass_pct" />
|
<MetricTooltip metricKey="english_maths_strong_pass_pct" />
|
||||||
</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>
|
||||||
{!suppressKs4Comparison && 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}>England avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1123,8 +1085,8 @@ export function SchoolDetailView({
|
|||||||
data={yearlyData}
|
data={yearlyData}
|
||||||
schoolName={schoolInfo.school_name}
|
schoolName={schoolInfo.school_name}
|
||||||
isSecondary={false}
|
isSecondary={false}
|
||||||
nationalRwmAvg={suppressKs2Comparison ? null : (primaryAvg.rwm_expected_pct ?? null)}
|
nationalRwmAvg={primaryAvg.rwm_expected_pct ?? null}
|
||||||
nationalByYear={suppressKs2Comparison ? undefined : nationalAvg?.by_year}
|
nationalByYear={nationalAvg?.by_year}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -1137,8 +1099,8 @@ export function SchoolDetailView({
|
|||||||
data={yearlyData}
|
data={yearlyData}
|
||||||
schoolName={schoolInfo.school_name}
|
schoolName={schoolInfo.school_name}
|
||||||
isSecondary={true}
|
isSecondary={true}
|
||||||
nationalAtt8Avg={suppressKs4Comparison ? null : (secondaryAvg.attainment_8_score ?? null)}
|
nationalAtt8Avg={secondaryAvg.attainment_8_score ?? null}
|
||||||
nationalByYear={suppressKs4Comparison ? undefined : nationalAvg?.by_year}
|
nationalByYear={nationalAvg?.by_year}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -1150,9 +1112,9 @@ export function SchoolDetailView({
|
|||||||
data={yearlyData}
|
data={yearlyData}
|
||||||
schoolName={schoolInfo.school_name}
|
schoolName={schoolInfo.school_name}
|
||||||
isSecondary={isSecondary}
|
isSecondary={isSecondary}
|
||||||
nationalRwmAvg={isPrimary && !suppressKs2Comparison ? (primaryAvg.rwm_expected_pct ?? null) : null}
|
nationalRwmAvg={isPrimary ? (primaryAvg.rwm_expected_pct ?? null) : null}
|
||||||
nationalAtt8Avg={isSecondary && !suppressKs4Comparison ? (secondaryAvg.attainment_8_score ?? null) : null}
|
nationalAtt8Avg={isSecondary ? (secondaryAvg.attainment_8_score ?? null) : null}
|
||||||
nationalByYear={(isPrimary ? suppressKs2Comparison : suppressKs4Comparison) ? undefined : nationalAvg?.by_year}
|
nationalByYear={nationalAvg?.by_year}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { School } from '@/lib/types';
|
import type { School } from '@/lib/types';
|
||||||
import { formatPercentage, calculateTrend, getPhaseStyle, schoolUrl, buildOfstedListBadge, formatAgeRange, isProposedToClose, isSpecialSchool } from '@/lib/utils';
|
import { formatPercentage, calculateTrend, getPhaseStyle, schoolUrl, buildOfstedListBadge, formatAgeRange, isProposedToClose } from '@/lib/utils';
|
||||||
import styles from './SchoolRow.module.css';
|
import styles from './SchoolRow.module.css';
|
||||||
|
|
||||||
interface SchoolRowProps {
|
interface SchoolRowProps {
|
||||||
@@ -38,23 +38,10 @@ export function SchoolRow({
|
|||||||
school.religious_denomination &&
|
school.religious_denomination &&
|
||||||
school.religious_denomination !== 'Does not apply';
|
school.religious_denomination !== 'Does not apply';
|
||||||
|
|
||||||
// A placeholder all-zero row (every subject 0 — a special/suppressed
|
// vs-national delta
|
||||||
// signature, matching SchoolDetailView's ks2Placeholder) isn't a real score,
|
|
||||||
// so its figure is hidden. A genuine 0% combined (some pupils met individual
|
|
||||||
// subjects but not all three) is NOT all-zero and stays shown.
|
|
||||||
const rwmPlaceholder =
|
|
||||||
school.rwm_expected_pct === 0 &&
|
|
||||||
(school.reading_expected_pct ?? 0) === 0 &&
|
|
||||||
(school.writing_expected_pct ?? 0) === 0 &&
|
|
||||||
(school.maths_expected_pct ?? 0) === 0;
|
|
||||||
// The school's OWN figure and its year-over-year trend are same-school
|
|
||||||
// measures — shown whenever there's a real value, special schools included.
|
|
||||||
const showRwmValue = school.rwm_expected_pct != null && !rwmPlaceholder;
|
|
||||||
// The vs-England delta is a mainstream benchmark: additionally dropped for
|
|
||||||
// special schools / PRUs / AP, whose pupils aren't measured against it fairly.
|
|
||||||
const rwmDelta =
|
const rwmDelta =
|
||||||
showRwmValue && !isSpecialSchool(school) && nationalAvgRwm != null
|
school.rwm_expected_pct != null && nationalAvgRwm != null
|
||||||
? Math.round((school.rwm_expected_pct as number) - nationalAvgRwm)
|
? Math.round(school.rwm_expected_pct - nationalAvgRwm)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const handleCompareClick = () => {
|
const handleCompareClick = () => {
|
||||||
@@ -100,9 +87,9 @@ export function SchoolRow({
|
|||||||
<div className={styles.line3}>
|
<div className={styles.line3}>
|
||||||
<span className={styles.stat}>
|
<span className={styles.stat}>
|
||||||
<strong className={styles.statValue}>
|
<strong className={styles.statValue}>
|
||||||
{showRwmValue ? formatPercentage(school.rwm_expected_pct, 0) : '—'}
|
{school.rwm_expected_pct != null ? formatPercentage(school.rwm_expected_pct, 0) : '—'}
|
||||||
</strong>
|
</strong>
|
||||||
{showRwmValue && school.prev_rwm_expected_pct != null && (
|
{school.prev_rwm_expected_pct != null && (
|
||||||
<span
|
<span
|
||||||
className={`${styles.trend} ${styles[`trend${trend.charAt(0).toUpperCase() + trend.slice(1)}`]}`}
|
className={`${styles.trend} ${styles[`trend${trend.charAt(0).toUpperCase() + trend.slice(1)}`]}`}
|
||||||
title={`Previous year: ${formatPercentage(school.prev_rwm_expected_pct)}`}
|
title={`Previous year: ${formatPercentage(school.prev_rwm_expected_pct)}`}
|
||||||
|
|||||||
@@ -623,41 +623,6 @@
|
|||||||
color: var(--accent-coral-dark, #b04a2e);
|
color: var(--accent-coral-dark, #b04a2e);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Ofsted grade grids (Report Card + OEIF) ──
|
|
||||||
Uniform, vertically-aligned grade chips. Labels reserve two lines so
|
|
||||||
single- and double-line labels put their chips on the same baseline;
|
|
||||||
every chip (Met, Strong, Expected standard, …) shares one font size,
|
|
||||||
padding and min-height regardless of how many lines its text wraps to. */
|
|
||||||
.gradeGrid .metricCard {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.85rem 0.75rem;
|
|
||||||
}
|
|
||||||
.gradeGrid .metricLabel {
|
|
||||||
min-height: 2.6em;
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.gradeGrid .metricValue {
|
|
||||||
margin-top: auto;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
max-width: 100%;
|
|
||||||
min-height: 2.6em;
|
|
||||||
padding: 0.3rem 0.7rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.25;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ofstedDisclaimer {
|
.ofstedDisclaimer {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--text-muted, #8a847a);
|
color: var(--text-muted, #8a847a);
|
||||||
|
|||||||
@@ -23,9 +23,8 @@ import type {
|
|||||||
SchoolAdmissions, SenDetail, Phonics,
|
SchoolAdmissions, SenDetail, Phonics,
|
||||||
SchoolDeprivation, SchoolFinance, NationalAverages,
|
SchoolDeprivation, SchoolFinance, NationalAverages,
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
import { formatPercentage, formatProgress, formatAcademicYear, formatAgeRange, isProposedToClose, ofstedLegacyAreas, isSpecialSchool } from '@/lib/utils';
|
import { formatPercentage, formatProgress, formatAcademicYear, formatAgeRange, isProposedToClose, ofstedLegacyAreas } from '@/lib/utils';
|
||||||
import { DeltaChip } from './DeltaChip';
|
import { DeltaChip } from './DeltaChip';
|
||||||
import { SpecialSchoolNote } from './SpecialSchoolNote';
|
|
||||||
import { track, getNavigationSource } from '@/lib/analytics';
|
import { track, getNavigationSource } from '@/lib/analytics';
|
||||||
import styles from './SecondarySchoolDetailView.module.css';
|
import styles from './SecondarySchoolDetailView.module.css';
|
||||||
|
|
||||||
@@ -108,16 +107,6 @@ export function SecondarySchoolDetailView({
|
|||||||
const p8Suspended = latestResults != null && latestResults.year >= 202425;
|
const p8Suspended = latestResults != null && latestResults.year >= 202425;
|
||||||
const hasResults = latestResults?.attainment_8_score != null;
|
const hasResults = latestResults?.attainment_8_score != null;
|
||||||
|
|
||||||
// Special schools / PRUs / AP sit the same GCSEs but teach pupils with SEND,
|
|
||||||
// so their headline attainment is far below the mainstream average by design.
|
|
||||||
// Drop the England comparison + "below" framing so the page doesn't portray
|
|
||||||
// them as failing against a benchmark that doesn't fit. Attainment 8 is a
|
|
||||||
// single 0–80 score with no subject breakdown to test for a placeholder, so
|
|
||||||
// this keys off establishment type only — a genuine (if extreme) 0.0 at a
|
|
||||||
// mainstream school still shows its real value and comparison.
|
|
||||||
const isSpecial = isSpecialSchool(schoolInfo);
|
|
||||||
const suppressComparison = isSpecial;
|
|
||||||
|
|
||||||
const admissionsTag = (() => {
|
const admissionsTag = (() => {
|
||||||
const policy = schoolInfo.admissions_policy?.toLowerCase() ?? '';
|
const policy = schoolInfo.admissions_policy?.toLowerCase() ?? '';
|
||||||
if (policy.includes('selective')) return 'Selective';
|
if (policy.includes('selective')) return 'Selective';
|
||||||
@@ -374,7 +363,7 @@ export function SecondarySchoolDetailView({
|
|||||||
<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.
|
||||||
</p>
|
</p>
|
||||||
<div className={`${styles.metricsGrid} ${styles.gradeGrid}`}>
|
<div className={styles.metricsGrid}>
|
||||||
{ofsted.rc_safeguarding_met != null && (
|
{ofsted.rc_safeguarding_met != null && (
|
||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>Safeguarding</div>
|
<div className={styles.metricLabel}>Safeguarding</div>
|
||||||
@@ -419,7 +408,7 @@ export function SecondarySchoolDetailView({
|
|||||||
Rated <strong>{OFSTED_LABELS[ofsted.overall_effectiveness]}</strong> across all inspected areas — Quality of Teaching, Behaviour, Pupils' Development and Leadership.
|
Rated <strong>{OFSTED_LABELS[ofsted.overall_effectiveness]}</strong> across all inspected areas — Quality of Teaching, Behaviour, Pupils' Development and Leadership.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className={`${styles.metricsGrid} ${styles.gradeGrid}`}>
|
<div className={styles.metricsGrid}>
|
||||||
{oeifAreas.map(({ label, value }) => (
|
{oeifAreas.map(({ label, value }) => (
|
||||||
<div key={label} className={styles.metricCard}>
|
<div key={label} className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>{label}</div>
|
<div className={styles.metricLabel}>{label}</div>
|
||||||
@@ -436,7 +425,7 @@ export function SecondarySchoolDetailView({
|
|||||||
<p className={styles.sectionSubtitle}>
|
<p className={styles.sectionSubtitle}>
|
||||||
From September 2024, Ofsted no longer gives a single overall grade.
|
From September 2024, Ofsted no longer gives a single overall grade.
|
||||||
</p>
|
</p>
|
||||||
<div className={`${styles.metricsGrid} ${styles.gradeGrid}`}>
|
<div className={styles.metricsGrid}>
|
||||||
{[
|
{[
|
||||||
{ label: 'Quality of Education', value: ofsted.quality_of_education },
|
{ label: 'Quality of Education', value: ofsted.quality_of_education },
|
||||||
{ label: 'Behaviour & Attitudes', value: ofsted.behaviour_attitudes },
|
{ label: 'Behaviour & Attitudes', value: ofsted.behaviour_attitudes },
|
||||||
@@ -463,11 +452,9 @@ 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.{!suppressComparison && ' England averages shown for comparison.'}
|
GCSE results for Year 11 pupils. England averages shown for comparison.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<SpecialSchoolNote school={schoolInfo} />
|
|
||||||
|
|
||||||
{p8Suspended && (
|
{p8Suspended && (
|
||||||
<div className={styles.p8Banner}>
|
<div className={styles.p8Banner}>
|
||||||
Progress 8 isn't published for 2024/25: this GCSE year group sat no KS2 tests
|
Progress 8 isn't published for 2024/25: this GCSE year group sat no KS2 tests
|
||||||
@@ -485,7 +472,7 @@ export function SecondarySchoolDetailView({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.heroStatValue}>
|
<div className={styles.heroStatValue}>
|
||||||
{latestResults.attainment_8_score.toFixed(1)}
|
{latestResults.attainment_8_score.toFixed(1)}
|
||||||
{!suppressComparison && secondaryAvg.attainment_8_score != null && (
|
{secondaryAvg.attainment_8_score != null && (
|
||||||
<DeltaChip
|
<DeltaChip
|
||||||
value={latestResults.attainment_8_score}
|
value={latestResults.attainment_8_score}
|
||||||
baseline={secondaryAvg.attainment_8_score}
|
baseline={secondaryAvg.attainment_8_score}
|
||||||
@@ -494,7 +481,7 @@ export function SecondarySchoolDetailView({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!suppressComparison && 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}>England avg: {secondaryAvg.attainment_8_score.toFixed(1)}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -525,7 +512,7 @@ export function SecondarySchoolDetailView({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.heroStatValue}>
|
<div className={styles.heroStatValue}>
|
||||||
{formatPercentage(latestResults.english_maths_strong_pass_pct)}
|
{formatPercentage(latestResults.english_maths_strong_pass_pct)}
|
||||||
{!suppressComparison && secondaryAvg.english_maths_strong_pass_pct != null && (
|
{secondaryAvg.english_maths_strong_pass_pct != null && (
|
||||||
<DeltaChip
|
<DeltaChip
|
||||||
value={latestResults.english_maths_strong_pass_pct}
|
value={latestResults.english_maths_strong_pass_pct}
|
||||||
baseline={secondaryAvg.english_maths_strong_pass_pct}
|
baseline={secondaryAvg.english_maths_strong_pass_pct}
|
||||||
@@ -534,7 +521,7 @@ export function SecondarySchoolDetailView({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!suppressComparison && 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}>England avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -547,7 +534,7 @@ export function SecondarySchoolDetailView({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.heroStatValue}>
|
<div className={styles.heroStatValue}>
|
||||||
{formatPercentage(latestResults.english_maths_standard_pass_pct)}
|
{formatPercentage(latestResults.english_maths_standard_pass_pct)}
|
||||||
{!suppressComparison && secondaryAvg.english_maths_standard_pass_pct != null && (
|
{secondaryAvg.english_maths_standard_pass_pct != null && (
|
||||||
<DeltaChip
|
<DeltaChip
|
||||||
value={latestResults.english_maths_standard_pass_pct}
|
value={latestResults.english_maths_standard_pass_pct}
|
||||||
baseline={secondaryAvg.english_maths_standard_pass_pct}
|
baseline={secondaryAvg.english_maths_standard_pass_pct}
|
||||||
@@ -556,17 +543,15 @@ export function SecondarySchoolDetailView({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!suppressComparison && 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}>England avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Attainment 8 visual bar (0–80 scale). This viz is explicitly
|
{/* Attainment 8 visual bar (0–80 scale) */}
|
||||||
"school vs national", so it's dropped for special schools where
|
{latestResults.attainment_8_score != null && (
|
||||||
that comparison isn't meaningful. */}
|
|
||||||
{!suppressComparison && latestResults.attainment_8_score != null && (
|
|
||||||
<div className={styles.att8Viz}>
|
<div className={styles.att8Viz}>
|
||||||
<div className={styles.att8VizLabel}>Attainment 8 — school vs national</div>
|
<div className={styles.att8VizLabel}>Attainment 8 — school vs national</div>
|
||||||
<div className={styles.att8VizTrack}>
|
<div className={styles.att8VizTrack}>
|
||||||
@@ -758,8 +743,8 @@ export function SecondarySchoolDetailView({
|
|||||||
data={yearlyData}
|
data={yearlyData}
|
||||||
schoolName={schoolInfo.school_name}
|
schoolName={schoolInfo.school_name}
|
||||||
isSecondary={true}
|
isSecondary={true}
|
||||||
nationalAtt8Avg={suppressComparison ? null : heroAtt8Nat}
|
nationalAtt8Avg={heroAtt8Nat}
|
||||||
nationalByYear={suppressComparison ? undefined : nationalAvg?.by_year}
|
nationalByYear={nationalAvg?.by_year}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { School } from '@/lib/types';
|
import type { School } from '@/lib/types';
|
||||||
import { buildOfstedListBadge, getPhaseStyle, schoolUrl, formatAgeRange, isProposedToClose, isSpecialSchool } from '@/lib/utils';
|
import { buildOfstedListBadge, getPhaseStyle, schoolUrl, formatAgeRange, isProposedToClose } from '@/lib/utils';
|
||||||
import styles from './SecondarySchoolRow.module.css';
|
import styles from './SecondarySchoolRow.module.css';
|
||||||
|
|
||||||
function detectAdmissionsTag(school: School): string | null {
|
function detectAdmissionsTag(school: School): string | null {
|
||||||
@@ -55,14 +55,8 @@ export function SecondarySchoolRow({
|
|||||||
const ofstedBadge = buildOfstedListBadge(school);
|
const ofstedBadge = buildOfstedListBadge(school);
|
||||||
const phase = getPhaseStyle(school.phase);
|
const phase = getPhaseStyle(school.phase);
|
||||||
const att8 = school.attainment_8_score;
|
const att8 = school.attainment_8_score;
|
||||||
// The school's own Attainment 8 is a same-school figure — shown whenever it
|
|
||||||
// exists (special schools included; their type tag on line 2 gives context).
|
|
||||||
// Only the vs-LA-average delta, a benchmark comparison, is dropped for
|
|
||||||
// special schools / PRUs / AP, whose pupils aren't measured against it fairly.
|
|
||||||
const laDelta =
|
const laDelta =
|
||||||
att8 != null && !isSpecialSchool(school) && laAvgAttainment8 != null
|
att8 != null && laAvgAttainment8 != null ? att8 - laAvgAttainment8 : null;
|
||||||
? att8 - laAvgAttainment8
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const admissionsTag = detectAdmissionsTag(school);
|
const admissionsTag = detectAdmissionsTag(school);
|
||||||
const sixthForm = hasSixthForm(school);
|
const sixthForm = hasSixthForm(school);
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
/* Special-school / PRU / AP context note: soft, informational (teal), not a
|
|
||||||
warning. Shared by both detail views so the styling can't drift. */
|
|
||||||
.note {
|
|
||||||
background: var(--bg-secondary, #f3ede4);
|
|
||||||
border-left: 4px solid var(--accent-teal, #2d7d7d);
|
|
||||||
border-radius: 0 6px 6px 0;
|
|
||||||
padding: 0.7rem 1rem;
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: var(--text-secondary, #5c564d);
|
|
||||||
}
|
|
||||||
.note strong {
|
|
||||||
color: var(--text-primary, #1a1612);
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/**
|
|
||||||
* SpecialSchoolNote — the context note shown on special-school / PRU / AP
|
|
||||||
* detail pages explaining why the mainstream England-average comparison is
|
|
||||||
* dropped. Renders nothing for mainstream schools.
|
|
||||||
*
|
|
||||||
* The copy is type-aware: only genuine special schools have pupils with special
|
|
||||||
* educational needs. Pupil referral units and alternative provision teach
|
|
||||||
* pupils educated outside a mainstream setting (e.g. after exclusion, or for
|
|
||||||
* medical reasons) who are not necessarily SEND — so their note says so rather
|
|
||||||
* than mischaracterising them. Suppressing the England comparison is reasonable
|
|
||||||
* for all three.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { isSpecialSchool } from '@/lib/utils';
|
|
||||||
import styles from './SpecialSchoolNote.module.css';
|
|
||||||
|
|
||||||
type SpecialKind = 'special' | 'pru' | 'ap';
|
|
||||||
|
|
||||||
function specialKind(schoolType: string | null | undefined): SpecialKind {
|
|
||||||
const t = (schoolType ?? '').toLowerCase();
|
|
||||||
if (/pupil referral/.test(t)) return 'pru';
|
|
||||||
if (/alternative provision/.test(t)) return 'ap';
|
|
||||||
return 'special';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SpecialSchoolNote({ school }: { school: { school_type?: string | null } }) {
|
|
||||||
if (!isSpecialSchool(school)) return null;
|
|
||||||
const kind = specialKind(school.school_type);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.note} role="note">
|
|
||||||
{kind === 'special' && (
|
|
||||||
<>
|
|
||||||
<strong>This is a special school.</strong> Its pupils have special educational needs and
|
|
||||||
work towards individual targets. They sit the same national assessments, but very few
|
|
||||||
reach the mainstream “expected standard” these measures report — so a
|
|
||||||
comparison with the England average isn’t a meaningful guide to the school.
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{kind === 'pru' && (
|
|
||||||
<>
|
|
||||||
<strong>This is a pupil referral unit.</strong> It teaches pupils educated outside a
|
|
||||||
mainstream school — for example after exclusion, or for medical or behavioural reasons.
|
|
||||||
The mainstream “expected standard” and the England-average comparison
|
|
||||||
aren’t a meaningful guide to the school.
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{kind === 'ap' && (
|
|
||||||
<>
|
|
||||||
<strong>This is an alternative provision setting.</strong> It teaches pupils educated
|
|
||||||
outside a mainstream school. The mainstream “expected standard” and the
|
|
||||||
England-average comparison aren’t a meaningful guide to the school.
|
|
||||||
</>
|
|
||||||
)}{' '}
|
|
||||||
Where available, the progress its pupils make is a fairer measure.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { latestValues, verdict } from '@/lib/compareLogic';
|
import { latestValues, verdict } from '@/lib/compareLogic';
|
||||||
import { isSpecialSchool } from '@/lib/utils';
|
|
||||||
import type { Benchmarks, ComparisonData, NationalAverages, School } from '@/lib/types';
|
import type { Benchmarks, ComparisonData, NationalAverages, School } from '@/lib/types';
|
||||||
import { DotStrip } from '@/components/DotStrip';
|
import { DotStrip } from '@/components/DotStrip';
|
||||||
import { Cell, Chip, RowLabel, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
import { Cell, Chip, RowLabel, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||||
@@ -91,19 +90,15 @@ function Strip({
|
|||||||
urns,
|
urns,
|
||||||
schoolNames,
|
schoolNames,
|
||||||
national,
|
national,
|
||||||
special,
|
|
||||||
}: {
|
}: {
|
||||||
spec: StripSpec;
|
spec: StripSpec;
|
||||||
data: Record<string, ComparisonData>;
|
data: Record<string, ComparisonData>;
|
||||||
urns: number[];
|
urns: number[];
|
||||||
schoolNames: string[];
|
schoolNames: string[];
|
||||||
national: Record<string, number> | undefined;
|
national: Record<string, number> | undefined;
|
||||||
/** Per-school special-school flag; special schools' mainstream attainment is
|
|
||||||
* not a fair comparison, so it's dropped from the strip (no dot). */
|
|
||||||
special: boolean[];
|
|
||||||
}) {
|
}) {
|
||||||
const values = latestValues(data, urns, spec.metric).map((v, i) =>
|
const values = latestValues(data, urns, spec.metric).map((v) =>
|
||||||
v != null && !special[i] ? Math.round(v) : null,
|
v != null ? Math.round(v) : null,
|
||||||
);
|
);
|
||||||
const anchorValue = spec.anchorKey ? national?.[spec.anchorKey] : undefined;
|
const anchorValue = spec.anchorKey ? national?.[spec.anchorKey] : undefined;
|
||||||
const anchor =
|
const anchor =
|
||||||
@@ -140,19 +135,12 @@ export function CompareAcademics({
|
|||||||
}) {
|
}) {
|
||||||
const urns = schools.map((school) => school.urn);
|
const urns = schools.map((school) => school.urn);
|
||||||
const schoolNames = schools.map((school) => school.school_name);
|
const schoolNames = schools.map((school) => school.school_name);
|
||||||
// Special schools / PRUs / AP: their pupils sit the same assessments but very
|
|
||||||
// few reach the mainstream standard, so their attainment isn't a fair
|
|
||||||
// like-for-like comparison — drop it (progress banding, which IS meaningful,
|
|
||||||
// is kept).
|
|
||||||
const specialFlags = schools.map((school) => isSpecialSchool(school));
|
|
||||||
const dropSpecial = (vals: Array<number | null>) =>
|
|
||||||
vals.map((v, i) => (specialFlags[i] ? null : v));
|
|
||||||
const isSecondary = propIsSecondary !== undefined ? propIsSecondary : schools.some(
|
const isSecondary = propIsSecondary !== undefined ? propIsSecondary : schools.some(
|
||||||
(school) => data[String(school.urn)]?.school_info?.attainment_8_score != null,
|
(school) => data[String(school.urn)]?.school_info?.attainment_8_score != null,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSecondary) {
|
if (isSecondary) {
|
||||||
const att8 = dropSpecial(latestValues(data, urns, 'attainment_8_score'));
|
const att8 = latestValues(data, urns, 'attainment_8_score');
|
||||||
const banding = urns.map((urn) => {
|
const banding = urns.map((urn) => {
|
||||||
const rows = data[String(urn)]?.yearly_data ?? [];
|
const rows = data[String(urn)]?.yearly_data ?? [];
|
||||||
for (let i = rows.length - 1; i >= 0; i--) {
|
for (let i = rows.length - 1; i >= 0; i--) {
|
||||||
@@ -171,8 +159,8 @@ export function CompareAcademics({
|
|||||||
const y = rows.length ? Math.trunc(rows[rows.length - 1].year) : 0;
|
const y = rows.length ? Math.trunc(rows[rows.length - 1].year) : 0;
|
||||||
return y >= 202425;
|
return y >= 202425;
|
||||||
});
|
});
|
||||||
const grade5 = dropSpecial(latestValues(data, urns, 'english_maths_strong_pass_pct'));
|
const grade5 = latestValues(data, urns, 'english_maths_strong_pass_pct');
|
||||||
const ebacc = dropSpecial(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 grade5Anchor = nationalAverages?.secondary?.english_maths_strong_pass_pct;
|
||||||
const ebaccAnchor = nationalAverages?.secondary?.ebacc_entry_pct;
|
const ebaccAnchor = nationalAverages?.secondary?.ebacc_entry_pct;
|
||||||
@@ -284,7 +272,7 @@ export function CompareAcademics({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const national = nationalAverages?.primary;
|
const national = nationalAverages?.primary;
|
||||||
const disadvantaged = dropSpecial(latestValues(data, urns, 'rwm_expected_disadvantaged_pct'));
|
const disadvantaged = latestValues(data, urns, 'rwm_expected_disadvantaged_pct');
|
||||||
const disadvantagedAnchor = benchmarks?.primary?.disadvantaged_rwm_expected_pct ?? null;
|
const disadvantagedAnchor = benchmarks?.primary?.disadvantaged_rwm_expected_pct ?? null;
|
||||||
// Cohort size behind the disadvantaged figure (spec §8.5): these are small
|
// Cohort size behind the disadvantaged figure (spec §8.5): these are small
|
||||||
// groups where single pupils move the percentage — show roughly how many
|
// groups where single pupils move the percentage — show roughly how many
|
||||||
@@ -319,7 +307,6 @@ export function CompareAcademics({
|
|||||||
urns={urns}
|
urns={urns}
|
||||||
schoolNames={schoolNames}
|
schoolNames={schoolNames}
|
||||||
national={national}
|
national={national}
|
||||||
special={specialFlags}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -333,7 +320,6 @@ export function CompareAcademics({
|
|||||||
urns={urns}
|
urns={urns}
|
||||||
schoolNames={schoolNames}
|
schoolNames={schoolNames}
|
||||||
national={national}
|
national={national}
|
||||||
special={specialFlags}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<p className={styles.stripNote}>
|
<p className={styles.stripNote}>
|
||||||
|
|||||||
@@ -795,24 +795,3 @@ export const PROPOSED_TO_CLOSE_STATUS = 'Open, but proposed to close';
|
|||||||
export function isProposedToClose(school: { status?: string | null }): boolean {
|
export function isProposedToClose(school: { status?: string | null }): boolean {
|
||||||
return school.status === PROPOSED_TO_CLOSE_STATUS;
|
return school.status === PROPOSED_TO_CLOSE_STATUS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Special schools, pupil referral units and alternative provision teach pupils
|
|
||||||
* with SEND or outside mainstream settings. Their pupils sit the same KS2/KS4
|
|
||||||
* assessments but very few reach the mainstream "expected standard", so the
|
|
||||||
* headline attainment measures — and any comparison to the England average —
|
|
||||||
* are not a fair judgement of the school. Callers use this to drop the
|
|
||||||
* mainstream-benchmark framing (deltas, "below England", national markers)
|
|
||||||
* rather than portray these schools as failing.
|
|
||||||
*
|
|
||||||
* Detection is by establishment type: every DfE special-school type contains
|
|
||||||
* "special" (e.g. "Community special school", "Academy special converter/sponsor
|
|
||||||
* led", "Foundation special school", "Non-maintained special school", "Free
|
|
||||||
* schools special", "Other independent special school"); PRUs and alternative
|
|
||||||
* provision are matched by name. Special schools carry a mainstream `phase`
|
|
||||||
* (Primary/Secondary/All-through), so `phase` alone can't identify them.
|
|
||||||
*/
|
|
||||||
export function isSpecialSchool(school: { school_type?: string | null }): boolean {
|
|
||||||
const t = (school.school_type ?? '').toLowerCase();
|
|
||||||
return /\bspecial\b/.test(t) || /pupil referral/.test(t) || /alternative provision/.test(t);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user