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:
Tudor
2026-08-01 21:14:35 +01:00
co-authored by Claude Opus 5
parent d74cc95034
commit 5044c24895
4 changed files with 398 additions and 99 deletions
@@ -23,7 +23,8 @@ import type {
SchoolAdmissions,
SchoolDeprivation, SchoolFinance, NationalAverages,
} 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 { computeSecondaryFlags, buildSecondaryNavItems } from '@/lib/schoolSections';
import { DeltaChip } from './DeltaChip';
import { SpecialSchoolNote } from './SpecialSchoolNote';
import { track, getNavigationSource } from '@/lib/analytics';
@@ -90,29 +91,16 @@ export function SecondarySchoolDetailView({
// Header details collapse behind a "Show all details" link on mobile/tablet.
const [detailsOpen, setDetailsOpen] = useState(false);
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
// Derived data-shape logic lives in lib/schoolSections so the server route
// can compute the section list without importing this client component.
const flags = computeSecondaryFlags({ schoolInfo, yearlyData, deprivation, finance });
const {
latestResults, hasSixthForm, hasFinance, hasDeprivation, hasLocation,
hasWellbeing, hasResults, p8Suspended, isSpecial, suppressComparison,
} = flags;
const secondaryAvg = nationalAvg?.secondary ?? {};
// GIAS OfficialSixthForm flag; missing (pipeline not yet re-run) => false.
const hasSixthForm = schoolInfo.has_sixth_form ?? false;
const hasFinance = finance != null && finance.per_pupil_spend != null;
const hasDeprivation = deprivation != null && deprivation.idaci_decile != null;
const hasLocation = schoolInfo.latitude != null && schoolInfo.longitude != null;
const hasWellbeing = (latestResults?.sen_support_pct != null || latestResults?.sen_ehcp_pct != null) || hasDeprivation;
const p8Suspended = latestResults != null && latestResults.year >= 202425;
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 080 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 policy = schoolInfo.admissions_policy?.toLowerCase() ?? '';
if (policy.includes('selective')) return 'Selective';
@@ -155,17 +143,9 @@ export function SecondarySchoolDetailView({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [schoolInfo.urn]);
// Build nav items dynamically based on available data.
// Engagement-led order (matches the primary page): recognised Ofsted badge,
// then the most-sought sections — results, admissions, history — with the
// experience and context sections following.
const navItems: { id: string; label: string }[] = [];
if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' });
if (hasResults) navItems.push({ id: 'gcse', label: 'GCSEs' });
if (admissions) navItems.push({ id: 'admissions', label: 'Admissions' });
if (yearlyData.length > 1) navItems.push({ id: 'history', label: 'History' });
if (hasWellbeing) navItems.push({ id: 'wellbeing', label: 'Wellbeing' });
if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' });
const navItems = buildSecondaryNavItems(flags, {
ofsted, admissions, yearlyDataLength: yearlyData.length,
});
// Track active section as user scrolls
useEffect(() => {