Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c1df77961 | ||
|
|
5e370d09f1 | ||
|
|
f6bb037c47 | ||
|
|
9c93c3d9c2 | ||
|
|
31ae13451a | ||
|
|
4e6be0ce65 |
@@ -148,6 +148,37 @@ test('a report-card school shows its report card, dated to the report-card inspe
|
||||
}
|
||||
});
|
||||
|
||||
test('an all-through school shows BOTH its KS2 SATs and its GCSE results, not just one phase', async ({ page }) => {
|
||||
// All-through schools carry both KS2 and KS4 data in the same yearly rows.
|
||||
// The detail view used to flip them to isSecondary and render GCSE-only,
|
||||
// hiding the primary phase. It now renders both phases and labels the school
|
||||
// "All-through".
|
||||
const AT_URN = 137306; // Hessle High School and Penshurst Primary — all-through
|
||||
const res = await page.request.get(`/api/schools/${AT_URN}`);
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const detail = await res.json();
|
||||
const rows: Array<{ rwm_expected_pct: number | null; attainment_8_score: number | null }> =
|
||||
detail.yearly_data ?? [];
|
||||
const hasKS2 = rows.some((r) => r.rwm_expected_pct != null);
|
||||
const hasKS4 = rows.some((r) => r.attainment_8_score != null);
|
||||
test.skip(
|
||||
(detail.school_info?.phase ?? '').toLowerCase() !== 'all-through' || !hasKS2 || !hasKS4,
|
||||
'precondition: chosen URN must currently be all-through with both KS2 and KS4 results',
|
||||
);
|
||||
|
||||
await page.goto(`/school/${AT_URN}`);
|
||||
await expect(page.locator('h1').first()).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Labelled as all-through in the hero meta.
|
||||
await expect(page.getByText(/All-through/i).first()).toBeVisible();
|
||||
|
||||
// The combined results section carries both phases.
|
||||
const results = page.locator('#results');
|
||||
await expect(results.getByText(/SATs & GCSE Results/)).toBeVisible();
|
||||
await expect(results.getByText('Reading, Writing & Maths combined')).toBeVisible(); // KS2
|
||||
await expect(results.getByText('Attainment 8').first()).toBeVisible(); // KS4
|
||||
});
|
||||
|
||||
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
|
||||
// centres, PRUs) used to 500 in the API — NaN GIAS fields broke JSON
|
||||
|
||||
@@ -1040,6 +1040,41 @@
|
||||
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 {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted, #8a847a);
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
formatPercentage, formatProgress, formatAcademicYear, isProposedToClose, ofstedLegacyAreas,
|
||||
} from '@/lib/utils';
|
||||
import { DeltaChip } from './DeltaChip';
|
||||
import { summariseAdmissions } from '@/lib/compareLogic';
|
||||
|
||||
const PerformanceChart = dynamic(
|
||||
() => import('./PerformanceChart').then((m) => m.PerformanceChart),
|
||||
@@ -85,6 +86,10 @@ export function SchoolDetailView({
|
||||
// Trend toggle only appears with ≥2 years carrying an offer rate.
|
||||
const admissionsOfferYears = admissionsHistory.filter((h) => h.first_preference_offer_pct != null).length;
|
||||
const showAdmissionsTrend = admissionsOfferYears >= 2;
|
||||
// Banded interpretation of the first-choice offer rate ("More than half of
|
||||
// first choices missed out" etc.) — the same banding the compare screen
|
||||
// uses, so a low offer rate reads as how severe it actually is.
|
||||
const admissionsSummary = summariseAdmissions(admissions);
|
||||
// Only the section links scroll horizontally; Back and "All" stay pinned.
|
||||
const sectionLinksRef = useRef<HTMLDivElement | null>(null);
|
||||
const [sectionNavAtEnd, setSectionNavAtEnd] = useState(false);
|
||||
@@ -153,10 +158,16 @@ export function SchoolDetailView({
|
||||
|
||||
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
|
||||
|
||||
// Phase detection
|
||||
// Phase detection. All-through schools cover BOTH key stages, so they are
|
||||
// neither "pure primary" nor "pure secondary": isSecondary stays true (they
|
||||
// have KS4 data) but isAllThrough gates the primary-only content (phonics,
|
||||
// KS2 trend) back on and switches phase-specific copy to an all-ages framing.
|
||||
const phase = schoolInfo.phase ?? '';
|
||||
const isSecondary = phase.toLowerCase().includes('secondary') || phase.toLowerCase() === 'all-through';
|
||||
const isAllThrough = phase.toLowerCase() === 'all-through';
|
||||
const isSecondary = phase.toLowerCase().includes('secondary') || isAllThrough;
|
||||
const isPrimary = !isSecondary;
|
||||
// Primary-stage content shows for pure-primary AND all-through schools.
|
||||
const showPrimaryContent = isPrimary || isAllThrough;
|
||||
|
||||
// National averages (fetched dynamically so they stay current)
|
||||
const [nationalAvg, setNationalAvg] = useState<NationalAverages | null>(null);
|
||||
@@ -228,11 +239,11 @@ export function SchoolDetailView({
|
||||
// after the recognised Ofsted badge; low-demand context sections stay last.
|
||||
const navItems: { id: string; label: string }[] = [];
|
||||
if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' });
|
||||
if (hasAnyResults) navItems.push({ id: 'results', label: isSecondary ? 'GCSEs' : 'SATs' });
|
||||
if (hasAnyResults) navItems.push({ id: 'results', label: isAllThrough ? 'Results' : isSecondary ? 'GCSEs' : 'SATs' });
|
||||
if (admissions) navItems.push({ id: 'admissions', label: 'Admissions' });
|
||||
if (hasInclusionData) navItems.push({ id: 'inclusion', label: 'Pupils' });
|
||||
if (yearlyData.length > 0) navItems.push({ id: 'history', label: 'History' });
|
||||
if (hasPhonics && isPrimary) navItems.push({ id: 'phonics', label: 'Phonics' });
|
||||
if (hasPhonics && showPrimaryContent) navItems.push({ id: 'phonics', label: 'Phonics' });
|
||||
if (hasSchoolLife) navItems.push({ id: 'school-life', label: 'School Life' });
|
||||
if (hasDeprivation) navItems.push({ id: 'local-area', label: 'Local Area' });
|
||||
if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' });
|
||||
@@ -319,6 +330,9 @@ export function SchoolDetailView({
|
||||
{schoolInfo.school_type && (
|
||||
<span className={styles.metaItem}>{schoolInfo.school_type}</span>
|
||||
)}
|
||||
{isAllThrough && (
|
||||
<span className={styles.metaItem}>All-through (primary & secondary)</span>
|
||||
)}
|
||||
{schoolInfo.gender && schoolInfo.gender !== 'Mixed' && (
|
||||
<span className={styles.metaItem}>{schoolInfo.gender}'s school</span>
|
||||
)}
|
||||
@@ -541,7 +555,7 @@ export function SchoolDetailView({
|
||||
<p className={styles.ofstedDisclaimer}>
|
||||
From November 2025, Ofsted replaced single overall grades with Report Cards rating schools across several areas.
|
||||
</p>
|
||||
<div className={styles.metricsGrid}>
|
||||
<div className={`${styles.metricsGrid} ${styles.gradeGrid}`}>
|
||||
{ofsted.rc_safeguarding_met != null && (
|
||||
<div className={styles.metricCard}>
|
||||
<div className={styles.metricLabel}>Safeguarding</div>
|
||||
@@ -587,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.
|
||||
</p>
|
||||
) : (
|
||||
<div className={styles.metricsGrid}>
|
||||
<div className={`${styles.metricsGrid} ${styles.gradeGrid}`}>
|
||||
{oeifAreas.map(({ label, value }) => (
|
||||
<div key={label} className={styles.metricCard}>
|
||||
<div className={styles.metricLabel}>{label}</div>
|
||||
@@ -607,10 +621,12 @@ export function SchoolDetailView({
|
||||
{hasAnyResults && latestResults && (
|
||||
<section id="results" className={styles.card}>
|
||||
<h2 className={styles.sectionTitle}>
|
||||
{isSecondary ? 'GCSE Results' : 'SATs Results'} ({formatAcademicYear(latestResults.year)})
|
||||
{isAllThrough ? 'SATs & GCSE Results' : isSecondary ? 'GCSE Results' : 'SATs Results'} ({formatAcademicYear(latestResults.year)})
|
||||
</h2>
|
||||
<p className={styles.sectionSubtitle}>
|
||||
{isSecondary
|
||||
{isAllThrough
|
||||
? '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>
|
||||
@@ -618,6 +634,9 @@ export function SchoolDetailView({
|
||||
{/* ── Primary / KS2 content ── */}
|
||||
{hasKS2Results && (
|
||||
<>
|
||||
{isAllThrough && (
|
||||
<h3 className={styles.subSectionTitle}>Primary — KS2 SATs (Year 6)</h3>
|
||||
)}
|
||||
<div className={styles.heroStatGrid}>
|
||||
{latestResults.rwm_expected_pct !== null && (
|
||||
<div className={styles.heroStatCard}>
|
||||
@@ -756,6 +775,9 @@ export function SchoolDetailView({
|
||||
{/* ── Secondary / KS4 content ── */}
|
||||
{hasKS4Results && (
|
||||
<>
|
||||
{isAllThrough && (
|
||||
<h3 className={styles.subSectionTitle} style={{ marginTop: '1.5rem' }}>Secondary — GCSEs (Year 11)</h3>
|
||||
)}
|
||||
<div className={styles.metricsGrid}>
|
||||
{latestResults.attainment_8_score !== null && (
|
||||
<div className={styles.metricCard}>
|
||||
@@ -906,6 +928,9 @@ export function SchoolDetailView({
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
{admissionsSummary.chip && (
|
||||
<p className={styles.admissionsTrendSummary}>{admissionsSummary.chip.text}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Multi-year trend */}
|
||||
@@ -941,7 +966,7 @@ export function SchoolDetailView({
|
||||
<DeltaChip value={latestResults.disadvantaged_pct} baseline={primaryAvg.disadvantaged_pct} unit="pts" size="sm" />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.heroStatHint}>Pupils from disadvantaged backgrounds{primaryAvg.disadvantaged_pct != null ? ` · national avg: ${primaryAvg.disadvantaged_pct.toFixed(0)}%` : ''}</div>
|
||||
<div className={styles.heroStatHint}>Pupils from disadvantaged backgrounds{primaryAvg.disadvantaged_pct != null ? ` · England avg: ${primaryAvg.disadvantaged_pct.toFixed(0)}%` : ''}</div>
|
||||
</div>
|
||||
)}
|
||||
{latestResults?.eal_pct != null && (
|
||||
@@ -1038,6 +1063,41 @@ export function SchoolDetailView({
|
||||
{yearlyData.length > 0 && (
|
||||
<section id="history" className={styles.card}>
|
||||
<h2 className={styles.sectionTitle}>Results Over Time</h2>
|
||||
{isAllThrough ? (
|
||||
// All-through: KS2 and KS4 trends are on different scales and have
|
||||
// different gap stories, so render them as two stacked charts
|
||||
// rather than crowding 8+ series onto one axis.
|
||||
<>
|
||||
{hasKS2Results && (
|
||||
<>
|
||||
<h3 className={styles.subSectionTitle}>Primary — KS2 SATs</h3>
|
||||
<div className={styles.chartContainer}>
|
||||
<PerformanceChart
|
||||
data={yearlyData}
|
||||
schoolName={schoolInfo.school_name}
|
||||
isSecondary={false}
|
||||
nationalRwmAvg={primaryAvg.rwm_expected_pct ?? null}
|
||||
nationalByYear={nationalAvg?.by_year}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{hasKS4Results && (
|
||||
<>
|
||||
<h3 className={styles.subSectionTitle} style={{ marginTop: '1.5rem' }}>Secondary — GCSEs</h3>
|
||||
<div className={styles.chartContainer}>
|
||||
<PerformanceChart
|
||||
data={yearlyData}
|
||||
schoolName={schoolInfo.school_name}
|
||||
isSecondary={true}
|
||||
nationalAtt8Avg={secondaryAvg.attainment_8_score ?? null}
|
||||
nationalByYear={nationalAvg?.by_year}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.chartContainer}>
|
||||
<PerformanceChart
|
||||
data={yearlyData}
|
||||
@@ -1048,6 +1108,7 @@ export function SchoolDetailView({
|
||||
nationalByYear={nationalAvg?.by_year}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{yearlyData.length > 1 && (
|
||||
<details className={styles.historyDisclosure}>
|
||||
<summary className={styles.historyToggle}>View raw year-by-year data</summary>
|
||||
@@ -1056,7 +1117,15 @@ export function SchoolDetailView({
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Year</th>
|
||||
{isSecondary ? (
|
||||
{isAllThrough ? (
|
||||
<>
|
||||
<th>RWM (expected %)</th>
|
||||
<th>Exceeding (%)</th>
|
||||
<th>Attainment 8</th>
|
||||
<th>Progress 8</th>
|
||||
<th>English & Maths Grade 4+</th>
|
||||
</>
|
||||
) : isSecondary ? (
|
||||
<>
|
||||
<th>Attainment 8</th>
|
||||
<th>Progress 8</th>
|
||||
@@ -1078,7 +1147,15 @@ export function SchoolDetailView({
|
||||
{yearlyData.map((result) => (
|
||||
<tr key={result.year}>
|
||||
<td className={styles.yearCell}>{formatAcademicYear(result.year)}</td>
|
||||
{isSecondary ? (
|
||||
{isAllThrough ? (
|
||||
<>
|
||||
<td>{result.rwm_expected_pct !== null ? formatPercentage(result.rwm_expected_pct) : '-'}</td>
|
||||
<td>{result.rwm_high_pct !== null ? formatPercentage(result.rwm_high_pct) : '-'}</td>
|
||||
<td>{result.attainment_8_score !== null ? result.attainment_8_score.toFixed(1) : '-'}</td>
|
||||
<td>{result.progress_8_score !== null ? formatProgress(result.progress_8_score) : '-'}</td>
|
||||
<td>{result.english_maths_standard_pass_pct !== null ? formatPercentage(result.english_maths_standard_pass_pct) : '-'}</td>
|
||||
</>
|
||||
) : isSecondary ? (
|
||||
<>
|
||||
<td>{result.attainment_8_score !== null ? result.attainment_8_score.toFixed(1) : '-'}</td>
|
||||
<td>{result.progress_8_score !== null ? formatProgress(result.progress_8_score) : '-'}</td>
|
||||
@@ -1103,8 +1180,8 @@ export function SchoolDetailView({
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
{/* Year 1 Phonics — primary only */}
|
||||
{hasPhonics && isPrimary && phonics && (
|
||||
{/* Year 1 Phonics — primary-stage metric (pure primary + all-through) */}
|
||||
{hasPhonics && showPrimaryContent && phonics && (
|
||||
<section id="phonics" className={styles.card}>
|
||||
<h2 className={styles.sectionTitle}>Year 1 Phonics ({formatAcademicYear(phonics.year)})</h2>
|
||||
<p className={styles.sectionSubtitle}>
|
||||
|
||||
@@ -623,6 +623,41 @@
|
||||
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 {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted, #8a847a);
|
||||
|
||||
@@ -363,7 +363,7 @@ export function SecondarySchoolDetailView({
|
||||
<p className={styles.ofstedDisclaimer}>
|
||||
From November 2025, Ofsted replaced single overall grades with Report Cards rating schools across several areas.
|
||||
</p>
|
||||
<div className={styles.metricsGrid}>
|
||||
<div className={`${styles.metricsGrid} ${styles.gradeGrid}`}>
|
||||
{ofsted.rc_safeguarding_met != null && (
|
||||
<div className={styles.metricCard}>
|
||||
<div className={styles.metricLabel}>Safeguarding</div>
|
||||
@@ -408,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.
|
||||
</p>
|
||||
) : (
|
||||
<div className={styles.metricsGrid}>
|
||||
<div className={`${styles.metricsGrid} ${styles.gradeGrid}`}>
|
||||
{oeifAreas.map(({ label, value }) => (
|
||||
<div key={label} className={styles.metricCard}>
|
||||
<div className={styles.metricLabel}>{label}</div>
|
||||
@@ -425,7 +425,7 @@ export function SecondarySchoolDetailView({
|
||||
<p className={styles.sectionSubtitle}>
|
||||
From September 2024, Ofsted no longer gives a single overall grade.
|
||||
</p>
|
||||
<div className={styles.metricsGrid}>
|
||||
<div className={`${styles.metricsGrid} ${styles.gradeGrid}`}>
|
||||
{[
|
||||
{ label: 'Quality of Education', value: ofsted.quality_of_education },
|
||||
{ label: 'Behaviour & Attitudes', value: ofsted.behaviour_attitudes },
|
||||
@@ -457,7 +457,8 @@ export function SecondarySchoolDetailView({
|
||||
|
||||
{p8Suspended && (
|
||||
<div className={styles.p8Banner}>
|
||||
Progress 8 scores for 2024/25 are not used for accountability purposes following the KS2 assessment disruption. Treat with caution.
|
||||
Progress 8 isn't published for 2024/25: this GCSE year group sat no KS2 tests
|
||||
(COVID), so DfE has no starting point to measure their progress from.
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user