refactor(detail): extract derived flags and nav items to lib
Pure data-shape logic moves out of the client components so page.tsx can compute the section list on the server without importing them. The secondary page is not a variant of the primary one -- different section ids (gcse, wellbeing), different flags, and History gated on more than one year rather than at least one -- so it gets its own computeSecondaryFlags and buildSecondaryNavItems rather than bending a shared function. Adds 16 unit tests covering the all-through and special-school branches, previously reachable only through a full component render. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,8 +18,9 @@ import type {
|
||||
SchoolDeprivation, SchoolFinance, NationalAverages,
|
||||
} from '@/lib/types';
|
||||
import {
|
||||
formatPercentage, formatProgress, formatAcademicYear, formatAgeRange, isProposedToClose, ofstedLegacyAreas, isSpecialSchool,
|
||||
formatPercentage, formatProgress, formatAcademicYear, formatAgeRange, isProposedToClose, ofstedLegacyAreas,
|
||||
} from '@/lib/utils';
|
||||
import { computeSchoolFlags, buildNavItems } from '@/lib/schoolSections';
|
||||
import { DeltaChip } from './DeltaChip';
|
||||
import { SpecialSchoolNote } from './SpecialSchoolNote';
|
||||
import { summariseAdmissions } from '@/lib/compareLogic';
|
||||
@@ -162,16 +163,18 @@ export function SchoolDetailView({
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [sectionsOpen]);
|
||||
|
||||
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
|
||||
|
||||
// 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 (KS2 SATs,
|
||||
// KS2 trend) back on and switches phase-specific copy to an all-ages framing.
|
||||
// Derived data-shape logic lives in lib/schoolSections so the server route
|
||||
// can compute the section list without importing this client component.
|
||||
const flags = computeSchoolFlags({
|
||||
schoolInfo, yearlyData, absenceData, census, deprivation, finance,
|
||||
});
|
||||
const {
|
||||
latestResults, isAllThrough, isSecondary, isPrimary,
|
||||
hasGenderSplit, hasInclusionData, hasSchoolLife, hasDeprivation,
|
||||
hasFinance, hasLocation, hasKS2Results, hasKS4Results, hasAnyResults,
|
||||
isSpecial, ks2Placeholder, suppressKs2Comparison, suppressKs4Comparison,
|
||||
} = flags;
|
||||
const phase = schoolInfo.phase ?? '';
|
||||
const isAllThrough = phase.toLowerCase() === 'all-through';
|
||||
const isSecondary = phase.toLowerCase().includes('secondary') || isAllThrough;
|
||||
const isPrimary = !isSecondary;
|
||||
|
||||
const primaryAvg = nationalAvg?.primary ?? {};
|
||||
const secondaryAvg = nationalAvg?.secondary ?? {};
|
||||
@@ -203,63 +206,9 @@ export function SchoolDetailView({
|
||||
return `This school is in one of England's less deprived areas (decile ${decile}/10).`;
|
||||
};
|
||||
|
||||
// Gender split availability (only meaningful for Mixed schools with census data)
|
||||
const isMixedSchool = schoolInfo.gender === 'Mixed' || schoolInfo.gender == null;
|
||||
const hasGenderSplit = isMixedSchool
|
||||
&& census?.female_pupils != null
|
||||
&& census?.male_pupils != null
|
||||
&& (census.female_pupils + census.male_pupils) > 0;
|
||||
|
||||
// Guard for Pupils & Inclusion — only show if at least one metric is available
|
||||
const hasInclusionData = (latestResults?.disadvantaged_pct != null)
|
||||
|| (latestResults?.eal_pct != null)
|
||||
|| (latestResults?.sen_support_pct != null)
|
||||
|| hasGenderSplit;
|
||||
|
||||
const hasSchoolLife = absenceData != null;
|
||||
const hasDeprivation = deprivation != null && deprivation.idaci_decile != null;
|
||||
const hasFinance = finance != null && finance.per_pupil_spend != null;
|
||||
const hasLocation = schoolInfo.latitude != null && schoolInfo.longitude != null;
|
||||
|
||||
// Determine whether this school has KS2 or KS4 results to show
|
||||
const hasKS2Results = latestResults != null && latestResults.rwm_expected_pct != null;
|
||||
const hasKS4Results = latestResults != null && latestResults.attainment_8_score != null;
|
||||
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.
|
||||
// Order is engagement-led (from section_nav_used analytics): the most-sought
|
||||
// sections — results, admissions, inclusion, history — sit near the top,
|
||||
// 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: 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 (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' });
|
||||
const navItems = buildNavItems(flags, {
|
||||
ofsted, admissions, yearlyDataLength: yearlyData.length,
|
||||
});
|
||||
|
||||
// Track active section as user scrolls
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user