From f6bb037c471553e8195b5a8b147467ce0d07a688 Mon Sep 17 00:00:00 2001 From: Tudor Date: Mon, 20 Jul 2026 12:53:07 +0100 Subject: [PATCH] fix(detail): render both phases for all-through schools (Batch E) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Batch E of applying the compare-screen learnings to the school detail page — all-through handling (point 11). An all-through school carries both KS2 and KS4 figures in the same yearly rows, but SchoolDetailView flipped it to isSecondary and rendered GCSE-only, hiding the entire primary phase (SATs, phonics, KS2 trend). The Results snapshot already gated its KS2/KS4 blocks purely on data availability, so both already appeared there — but the section title, the trend chart, phonics, the nav label and the history table all still assumed a single phase. - Add an explicit `isAllThrough` flag (+ `showPrimaryContent = isPrimary || isAllThrough`); pure-secondary behaviour is unchanged. - Hero: an "All-through (primary & secondary)" meta chip for all-ages framing. - Results section: title "SATs & GCSE Results", a combined subtitle, and "Primary — KS2 SATs (Year 6)" / "Secondary — GCSEs (Year 11)" sub-headings separating the two blocks. - Results Over Time: render two stacked PerformanceCharts (KS2 SATs + GCSEs) rather than crowding both stages' series — on different scales with different gap stories — onto one axis. Each gets its correct England overlay. - Phonics section + nav item now show for all-through (primary-stage metric). - History table: an all-through column set covering both phases (RWM, Exceeding, Attainment 8, Progress 8, Eng & Maths 4+). - Nav "Results" label for all-through instead of "GCSEs". e2e: a new journey asserts an all-through school (Hessle, 137306) shows both the KS2 and KS4 results and the All-through label, with a data-driven precondition skip if staging data drifts. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB --- e2e/tests/journeys.spec.ts | 31 ++++++ nextjs-app/components/SchoolDetailView.tsx | 113 +++++++++++++++++---- 2 files changed, 122 insertions(+), 22 deletions(-) diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts index 548a060..4df77d7 100644 --- a/e2e/tests/journeys.spec.ts +++ b/e2e/tests/journeys.spec.ts @@ -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 diff --git a/nextjs-app/components/SchoolDetailView.tsx b/nextjs-app/components/SchoolDetailView.tsx index 229b71e..e353795 100644 --- a/nextjs-app/components/SchoolDetailView.tsx +++ b/nextjs-app/components/SchoolDetailView.tsx @@ -158,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(null); @@ -233,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' }); @@ -324,6 +330,9 @@ export function SchoolDetailView({ {schoolInfo.school_type && ( {schoolInfo.school_type} )} + {isAllThrough && ( + All-through (primary & secondary) + )} {schoolInfo.gender && schoolInfo.gender !== 'Mixed' && ( {schoolInfo.gender}'s school )} @@ -612,17 +621,22 @@ export function SchoolDetailView({ {hasAnyResults && latestResults && (

- {isSecondary ? 'GCSE Results' : 'SATs Results'} ({formatAcademicYear(latestResults.year)}) + {isAllThrough ? 'SATs & GCSE Results' : isSecondary ? 'GCSE Results' : 'SATs Results'} ({formatAcademicYear(latestResults.year)})

- {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.'} + {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.'}

{/* ── Primary / KS2 content ── */} {hasKS2Results && ( <> + {isAllThrough && ( +

Primary — KS2 SATs (Year 6)

+ )}
{latestResults.rwm_expected_pct !== null && (
@@ -761,6 +775,9 @@ export function SchoolDetailView({ {/* ── Secondary / KS4 content ── */} {hasKS4Results && ( <> + {isAllThrough && ( +

Secondary — GCSEs (Year 11)

+ )}
{latestResults.attainment_8_score !== null && (
@@ -1046,16 +1063,52 @@ export function SchoolDetailView({ {yearlyData.length > 0 && (

Results Over Time

-
- -
+ {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 && ( + <> +

Primary — KS2 SATs

+
+ +
+ + )} + {hasKS4Results && ( + <> +

Secondary — GCSEs

+
+ +
+ + )} + + ) : ( +
+ +
+ )} {yearlyData.length > 1 && (
View raw year-by-year data @@ -1064,7 +1117,15 @@ export function SchoolDetailView({ Year - {isSecondary ? ( + {isAllThrough ? ( + <> + RWM (expected %) + Exceeding (%) + Attainment 8 + Progress 8 + English & Maths Grade 4+ + + ) : isSecondary ? ( <> Attainment 8 Progress 8 @@ -1086,7 +1147,15 @@ export function SchoolDetailView({ {yearlyData.map((result) => ( {formatAcademicYear(result.year)} - {isSecondary ? ( + {isAllThrough ? ( + <> + {result.rwm_expected_pct !== null ? formatPercentage(result.rwm_expected_pct) : '-'} + {result.rwm_high_pct !== null ? formatPercentage(result.rwm_high_pct) : '-'} + {result.attainment_8_score !== null ? result.attainment_8_score.toFixed(1) : '-'} + {result.progress_8_score !== null ? formatProgress(result.progress_8_score) : '-'} + {result.english_maths_standard_pass_pct !== null ? formatPercentage(result.english_maths_standard_pass_pct) : '-'} + + ) : isSecondary ? ( <> {result.attainment_8_score !== null ? result.attainment_8_score.toFixed(1) : '-'} {result.progress_8_score !== null ? formatProgress(result.progress_8_score) : '-'} @@ -1111,8 +1180,8 @@ export function SchoolDetailView({ )}
)} - {/* Year 1 Phonics — primary only */} - {hasPhonics && isPrimary && phonics && ( + {/* Year 1 Phonics — primary-stage metric (pure primary + all-through) */} + {hasPhonics && showPrimaryContent && phonics && (

Year 1 Phonics ({formatAcademicYear(phonics.year)})

-- 2.54.0