/** * Derived data-shape logic for the school detail pages. * * Pure functions with no React dependency, so the server route can decide * which sections exist without pulling in a client component. Extracted from * SchoolDetailView, which owned this logic inline while it was a client * component. */ import type { School, SchoolResult, AbsenceData, SchoolCensus, OfstedInspection, SchoolAdmissions, SchoolDeprivation, SchoolFinance, } from './types'; import { isSpecialSchool } from './utils'; export interface SchoolFlagsInput { schoolInfo: School; yearlyData: SchoolResult[]; absenceData: AbsenceData | null; census: SchoolCensus | null; deprivation: SchoolDeprivation | null; finance: SchoolFinance | null; } export interface SchoolFlags { latestResults: SchoolResult | null; isAllThrough: boolean; isSecondary: boolean; isPrimary: boolean; hasGenderSplit: boolean; hasInclusionData: boolean; hasSchoolLife: boolean; hasDeprivation: boolean; hasFinance: boolean; hasLocation: boolean; hasKS2Results: boolean; hasKS4Results: boolean; hasAnyResults: boolean; isSpecial: boolean; ks2Placeholder: boolean; suppressKs2Comparison: boolean; suppressKs4Comparison: boolean; } export interface NavItem { id: string; label: string; } export function computeSchoolFlags({ schoolInfo, yearlyData, absenceData, census, deprivation, finance, }: SchoolFlagsInput): SchoolFlags { 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. const phase = schoolInfo.phase ?? ''; const isAllThrough = phase.toLowerCase() === 'all-through'; const isSecondary = phase.toLowerCase().includes('secondary') || isAllThrough; const isPrimary = !isSecondary; // 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; return { latestResults, isAllThrough, isSecondary, isPrimary, hasGenderSplit, hasInclusionData, hasSchoolLife, hasDeprivation, hasFinance, hasLocation, hasKS2Results, hasKS4Results, hasAnyResults, isSpecial, ks2Placeholder, suppressKs2Comparison, suppressKs4Comparison, }; } export interface NavItemsInput { ofsted: OfstedInspection | null; admissions: SchoolAdmissions | null; yearlyDataLength: number; } /** * 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. * * These conditions MUST match the conditions the section composers use to * render, or the nav will link to sections that do not exist. */ export function buildNavItems( flags: SchoolFlags, { ofsted, admissions, yearlyDataLength }: NavItemsInput, ): NavItem[] { const navItems: NavItem[] = []; if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' }); if (flags.hasAnyResults) { navItems.push({ id: 'results', label: flags.isAllThrough ? 'Results' : flags.isSecondary ? 'GCSEs' : 'SATs', }); } if (admissions) navItems.push({ id: 'admissions', label: 'Admissions' }); if (flags.hasInclusionData) navItems.push({ id: 'inclusion', label: 'Pupils' }); if (yearlyDataLength > 0) navItems.push({ id: 'history', label: 'History' }); if (flags.hasSchoolLife) navItems.push({ id: 'school-life', label: 'School Life' }); if (flags.hasDeprivation) navItems.push({ id: 'local-area', label: 'Local Area' }); if (flags.hasFinance) navItems.push({ id: 'finances', label: 'Finances' }); return navItems; } // --------------------------------------------------------------------------- // Secondary pages // // The secondary view is not a variant of the primary one: it has its own // section ids ('gcse', 'wellbeing'), its own flags, and gates History on // MORE THAN ONE year rather than at least one. Kept as separate functions so // neither phase's behaviour bends to accommodate the other. // --------------------------------------------------------------------------- export interface SecondaryFlags { latestResults: SchoolResult | null; hasSixthForm: boolean; hasFinance: boolean; hasDeprivation: boolean; hasLocation: boolean; hasWellbeing: boolean; hasResults: boolean; /** Progress 8 was suspended from the 2024/25 cohort onwards. */ p8Suspended: boolean; isSpecial: boolean; suppressComparison: boolean; } export function computeSecondaryFlags({ schoolInfo, yearlyData, deprivation, finance, }: Omit): SecondaryFlags { const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null; // 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 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; return { latestResults, hasSixthForm, hasFinance, hasDeprivation, hasLocation, hasWellbeing, hasResults: !!hasResults, p8Suspended, isSpecial, suppressComparison, }; } /** * Build nav items for a secondary page. * 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. */ export function buildSecondaryNavItems( flags: SecondaryFlags, { ofsted, admissions, yearlyDataLength }: NavItemsInput, ): NavItem[] { const navItems: NavItem[] = []; if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' }); if (flags.hasResults) navItems.push({ id: 'gcse', label: 'GCSEs' }); if (admissions) navItems.push({ id: 'admissions', label: 'Admissions' }); if (yearlyDataLength > 1) navItems.push({ id: 'history', label: 'History' }); if (flags.hasWellbeing) navItems.push({ id: 'wellbeing', label: 'Wellbeing' }); if (flags.hasFinance) navItems.push({ id: 'finances', label: 'Finances' }); return navItems; }