Files
school_compare/nextjs-app/components/SchoolDetailView.tsx

654 lines
30 KiB
TypeScript
Raw Normal View History

/**
* SchoolDetailView Component
* Displays comprehensive school information with performance charts
*/
'use client';
feat(ux): implement comprehensive UX audit fixes across all pages Addresses 28 issues identified in UX audit (P0–P3 severity): P0 — Critical: - Fix compare URL sharing: seed ComparisonContext from SSR initialData when localStorage is empty, making /compare?urns=... links shareable - Remove permanently broken "Avg. Scaled Score" column from school detail historical data table P1 — High priority: - Add radius selector (0.5–10 mi) to postcode search in FilterBar - Make Add to Compare a toggle (remove) on SchoolCards - Hide hero title/description once a search is active - Show school count + quick-search prompts on empty landing page - Compare empty state opens in-page school search modal directly - Remove URN from school detail header (irrelevant to end users) - Move map above performance chart in school detail page - Add ← Back navigation to school detail page - Add sort controls to search results (RWM%, distance, A–Z) - Show metric descriptions below metric selector - Expand ComparisonToast to list school names with per-school remove - Add progress score explainer (0 = national average) throughout P2 — Medium: - Remove console.log statements from ComparisonView - Colour-code comparison school cards to match chart line colours - Replace plain loading text with LoadingSkeleton in ComparisonView - Rankings empty state uses shared EmptyState component - Rankings year filter shows actual year e.g. "2023 (Latest)" - Rankings subtitle shows top-N count - Add View link alongside Add button in rankings table - Remove placeholder Privacy Policy / Terms links from footer - Replace untappable 10px info icons with visible metric hint text - Show active filter chips in search results header P3 — Polish: - Remove redundant "Home" nav link (logo already links home) - Add / and Ctrl+K keyboard shortcut to focus search input - Add Share button to compare page (copies URL to clipboard) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 21:31:28 +00:00
import { useRouter } from 'next/navigation';
import { useComparison } from '@/hooks/useComparison';
import { PerformanceChart } from './PerformanceChart';
import { SchoolMap } from './SchoolMap';
import type {
School, SchoolResult, AbsenceData,
OfstedInspection, OfstedParentView, SchoolCensus,
SchoolAdmissions, SenDetail, Phonics,
SchoolDeprivation, SchoolFinance,
} from '@/lib/types';
import { formatPercentage, formatProgress } from '@/lib/utils';
import styles from './SchoolDetailView.module.css';
const OFSTED_LABELS: Record<number, string> = {
1: 'Outstanding', 2: 'Good', 3: 'Requires Improvement', 4: 'Inadequate',
};
// 2023 national averages for context
const NATIONAL_AVG = {
rwm_expected: 60,
rwm_high: 8,
reading_expected: 73,
writing_expected: 71,
maths_expected: 73,
phonics_yr1: 79,
overall_absence: 6.7,
persistent_absence: 22,
class_size: 27,
per_pupil_spend: 6000,
};
function progressClass(val: number | null | undefined): string {
if (val == null) return '';
if (val > 0) return styles.progressPositive;
if (val < 0) return styles.progressNegative;
return '';
}
interface SchoolDetailViewProps {
schoolInfo: School;
yearlyData: SchoolResult[];
absenceData: AbsenceData | null;
ofsted: OfstedInspection | null;
parentView: OfstedParentView | null;
census: SchoolCensus | null;
admissions: SchoolAdmissions | null;
senDetail: SenDetail | null;
phonics: Phonics | null;
deprivation: SchoolDeprivation | null;
finance: SchoolFinance | null;
}
export function SchoolDetailView({
schoolInfo, yearlyData, absenceData,
ofsted, parentView, census, admissions, senDetail, phonics, deprivation, finance,
}: SchoolDetailViewProps) {
feat(ux): implement comprehensive UX audit fixes across all pages Addresses 28 issues identified in UX audit (P0–P3 severity): P0 — Critical: - Fix compare URL sharing: seed ComparisonContext from SSR initialData when localStorage is empty, making /compare?urns=... links shareable - Remove permanently broken "Avg. Scaled Score" column from school detail historical data table P1 — High priority: - Add radius selector (0.5–10 mi) to postcode search in FilterBar - Make Add to Compare a toggle (remove) on SchoolCards - Hide hero title/description once a search is active - Show school count + quick-search prompts on empty landing page - Compare empty state opens in-page school search modal directly - Remove URN from school detail header (irrelevant to end users) - Move map above performance chart in school detail page - Add ← Back navigation to school detail page - Add sort controls to search results (RWM%, distance, A–Z) - Show metric descriptions below metric selector - Expand ComparisonToast to list school names with per-school remove - Add progress score explainer (0 = national average) throughout P2 — Medium: - Remove console.log statements from ComparisonView - Colour-code comparison school cards to match chart line colours - Replace plain loading text with LoadingSkeleton in ComparisonView - Rankings empty state uses shared EmptyState component - Rankings year filter shows actual year e.g. "2023 (Latest)" - Rankings subtitle shows top-N count - Add View link alongside Add button in rankings table - Remove placeholder Privacy Policy / Terms links from footer - Replace untappable 10px info icons with visible metric hint text - Show active filter chips in search results header P3 — Polish: - Remove redundant "Home" nav link (logo already links home) - Add / and Ctrl+K keyboard shortcut to focus search input - Add Share button to compare page (copies URL to clipboard) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 21:31:28 +00:00
const router = useRouter();
const { addSchool, removeSchool, isSelected } = useComparison();
const isInComparison = isSelected(schoolInfo.urn);
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
const handleComparisonToggle = () => {
if (isInComparison) {
removeSchool(schoolInfo.urn);
} else {
addSchool(schoolInfo);
}
};
const deprivationDesc = (decile: number) => {
if (decile <= 3) return `This school is in one of England's most deprived areas (decile ${decile}/10). Many pupils may face additional challenges at home.`;
if (decile <= 7) return `This school is in an area with average levels of deprivation (decile ${decile}/10).`;
return `This school is in one of England's less deprived areas (decile ${decile}/10).`;
};
// 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)
|| senDetail != null;
const hasSchoolLife = absenceData != null || census?.class_size_avg != null;
const hasPhonics = phonics != null && phonics.year1_phonics_pct != 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;
// Build section nav items dynamically — only sections with data
const navItems: { id: string; label: string }[] = [];
if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' });
if (parentView && parentView.total_responses != null && parentView.total_responses > 0)
navItems.push({ id: 'parents', label: 'Parents' });
if (latestResults) navItems.push({ id: 'sats', label: 'SATs' });
if (hasPhonics) navItems.push({ id: 'phonics', label: 'Phonics' });
if (hasSchoolLife) navItems.push({ id: 'school-life', label: 'School Life' });
if (admissions) navItems.push({ id: 'admissions', label: 'Admissions' });
if (hasInclusionData) navItems.push({ id: 'inclusion', label: 'Pupils' });
if (hasLocation) navItems.push({ id: 'location', label: 'Location' });
if (hasDeprivation) navItems.push({ id: 'local-area', label: 'Local Area' });
if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' });
if (yearlyData.length > 0) navItems.push({ id: 'history', label: 'History' });
return (
<div className={styles.container}>
feat(ux): implement comprehensive UX audit fixes across all pages Addresses 28 issues identified in UX audit (P0–P3 severity): P0 — Critical: - Fix compare URL sharing: seed ComparisonContext from SSR initialData when localStorage is empty, making /compare?urns=... links shareable - Remove permanently broken "Avg. Scaled Score" column from school detail historical data table P1 — High priority: - Add radius selector (0.5–10 mi) to postcode search in FilterBar - Make Add to Compare a toggle (remove) on SchoolCards - Hide hero title/description once a search is active - Show school count + quick-search prompts on empty landing page - Compare empty state opens in-page school search modal directly - Remove URN from school detail header (irrelevant to end users) - Move map above performance chart in school detail page - Add ← Back navigation to school detail page - Add sort controls to search results (RWM%, distance, A–Z) - Show metric descriptions below metric selector - Expand ComparisonToast to list school names with per-school remove - Add progress score explainer (0 = national average) throughout P2 — Medium: - Remove console.log statements from ComparisonView - Colour-code comparison school cards to match chart line colours - Replace plain loading text with LoadingSkeleton in ComparisonView - Rankings empty state uses shared EmptyState component - Rankings year filter shows actual year e.g. "2023 (Latest)" - Rankings subtitle shows top-N count - Add View link alongside Add button in rankings table - Remove placeholder Privacy Policy / Terms links from footer - Replace untappable 10px info icons with visible metric hint text - Show active filter chips in search results header P3 — Polish: - Remove redundant "Home" nav link (logo already links home) - Add / and Ctrl+K keyboard shortcut to focus search input - Add Share button to compare page (copies URL to clipboard) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 21:31:28 +00:00
{/* Back Navigation */}
<div className={styles.backNav}>
<button onClick={() => router.back()} className={styles.backButton}> Back</button>
</div>
{/* Header */}
<header className={styles.header}>
<div className={styles.headerContent}>
<div className={styles.titleSection}>
<h1 className={styles.schoolName}>{schoolInfo.school_name}</h1>
<div className={styles.meta}>
{schoolInfo.local_authority && (
<span className={styles.metaItem}>{schoolInfo.local_authority}</span>
)}
{schoolInfo.school_type && (
<span className={styles.metaItem}>{schoolInfo.school_type}</span>
)}
{schoolInfo.gender && schoolInfo.gender !== 'Mixed' && (
<span className={styles.metaItem}>{schoolInfo.gender}&apos;s school</span>
)}
</div>
{schoolInfo.address && (
<p className={styles.address}>
{schoolInfo.address}{schoolInfo.postcode && `, ${schoolInfo.postcode}`}
</p>
)}
<div className={styles.headerDetails}>
{schoolInfo.headteacher_name && (
<span className={styles.headerDetail}>
<strong>Headteacher:</strong> {schoolInfo.headteacher_name}
</span>
)}
{schoolInfo.website && (
<span className={styles.headerDetail}>
<a href={schoolInfo.website} target="_blank" rel="noopener noreferrer">
School website
</a>
</span>
)}
{latestResults?.total_pupils != null && (
<span className={styles.headerDetail}>
<strong>Pupils:</strong> {latestResults.total_pupils.toLocaleString()}
{schoolInfo.capacity != null && ` (capacity: ${schoolInfo.capacity})`}
</span>
)}
{schoolInfo.trust_name && (
<span className={styles.headerDetail}>
Part of <strong>{schoolInfo.trust_name}</strong>
</span>
)}
</div>
</div>
<div className={styles.actions}>
<button
onClick={handleComparisonToggle}
className={isInComparison ? styles.btnRemove : styles.btnAdd}
>
{isInComparison ? '✓ In Comparison' : '+ Add to Compare'}
</button>
</div>
</div>
</header>
{/* Sticky Section Navigation */}
{navItems.length > 0 && (
<nav className={styles.sectionNav} aria-label="Page sections">
<div className={styles.sectionNavInner}>
{navItems.map(({ id, label }) => (
<a key={id} href={`#${id}`} className={styles.sectionNavLink}>{label}</a>
))}
</div>
</nav>
)}
{/* Ofsted Rating */}
{ofsted && (
<section id="ofsted" className={styles.card}>
<h2 className={styles.sectionTitle}>
Ofsted Rating
{ofsted.inspection_date && (
<span className={styles.ofstedDate}>
Inspected {new Date(ofsted.inspection_date).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })}
</span>
)}
<a
href={`https://reports.ofsted.gov.uk/provider/21/${schoolInfo.urn}`}
target="_blank"
rel="noopener noreferrer"
className={styles.ofstedReportLink}
>
Full report
</a>
</h2>
<div className={styles.ofstedHeader}>
<span className={`${styles.ofstedGrade} ${styles[`ofstedGrade${ofsted.overall_effectiveness}`]}`}>
{ofsted.overall_effectiveness ? OFSTED_LABELS[ofsted.overall_effectiveness] : 'Not rated'}
</span>
{ofsted.previous_overall != null &&
ofsted.previous_overall !== ofsted.overall_effectiveness && (
<span className={styles.ofstedPrevious}>
Previously: {OFSTED_LABELS[ofsted.previous_overall]}
</span>
)}
</div>
{parentView?.q_recommend_pct != null && parentView.total_responses != null && parentView.total_responses > 0 && (
<p className={styles.parentRecommendLine}>
<strong>{Math.round(parentView.q_recommend_pct)}%</strong> of parents would recommend this school ({parentView.total_responses.toLocaleString()} responses)
</p>
)}
<div className={styles.metricsGrid}>
{[
{ label: 'Quality of Teaching', value: ofsted.quality_of_education },
{ label: 'Behaviour in School', value: ofsted.behaviour_attitudes },
{ label: 'Pupils\' Wider Development', value: ofsted.personal_development },
{ label: 'School Leadership', value: ofsted.leadership_management },
...(ofsted.early_years_provision != null
? [{ label: 'Early Years (Reception)', value: ofsted.early_years_provision }]
: []),
].map(({ label, value }) => value != null && (
<div key={label} className={styles.metricCard}>
<div className={styles.metricLabel}>{label}</div>
<div className={`${styles.metricValue} ${styles[`ofstedGrade${value}`]}`}>
{OFSTED_LABELS[value]}
</div>
</div>
))}
</div>
</section>
)}
{/* What Parents Say */}
{parentView && parentView.total_responses != null && parentView.total_responses > 0 && (
<section id="parents" className={styles.card}>
<h2 className={styles.sectionTitle}>
What Parents Say
<span className={styles.responseBadge}>
{parentView.total_responses.toLocaleString()} responses
</span>
</h2>
<p className={styles.sectionSubtitle}>
From the Ofsted Parent View survey parents share their experience of this school.
</p>
<div className={styles.parentViewGrid}>
{[
{ label: 'Would recommend this school', pct: parentView.q_recommend_pct },
{ label: 'My child is happy here', pct: parentView.q_happy_pct },
{ label: 'My child feels safe here', pct: parentView.q_safe_pct },
{ label: 'Teaching is good', pct: parentView.q_teaching_pct },
{ label: 'My child makes good progress', pct: parentView.q_progress_pct },
{ label: 'School looks after pupils\' wellbeing', pct: parentView.q_wellbeing_pct },
{ label: 'Behaviour is well managed', pct: parentView.q_behaviour_pct },
{ label: 'School deals well with bullying', pct: parentView.q_bullying_pct },
{ label: 'Communicates well with parents', pct: parentView.q_communication_pct },
].filter(q => q.pct != null).map(({ label, pct }) => (
<div key={label} className={styles.parentViewRow}>
<span className={styles.parentViewLabel}>{label}</span>
<div className={styles.parentViewBar}>
<div className={styles.parentViewFill} style={{ width: `${pct}%` }} />
</div>
<span className={styles.parentViewPct}>{Math.round(pct!)}%</span>
</div>
))}
</div>
</section>
)}
{/* SATs Results (merged with Subject Breakdown) */}
{latestResults && (
<section id="sats" className={styles.card}>
<h2 className={styles.sectionTitle}>SATs Results ({latestResults.year})</h2>
<p className={styles.sectionSubtitle}>
End-of-primary-school tests taken by Year 6 pupils. National averages shown for comparison.
</p>
{/* Headline numbers: RWM combined */}
<div className={styles.metricsGrid}>
{latestResults.rwm_expected_pct !== null && (
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Reading, Writing & Maths combined</div>
<div className={styles.metricValue}>{formatPercentage(latestResults.rwm_expected_pct)}</div>
<div className={styles.metricHint}>National avg: {NATIONAL_AVG.rwm_expected}%</div>
</div>
)}
{latestResults.rwm_high_pct !== null && (
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Exceeding expected level (RWM)</div>
<div className={styles.metricValue}>{formatPercentage(latestResults.rwm_high_pct)}</div>
<div className={styles.metricHint}>National avg: {NATIONAL_AVG.rwm_high}%</div>
</div>
)}
</div>
feat(ux): implement comprehensive UX audit fixes across all pages Addresses 28 issues identified in UX audit (P0–P3 severity): P0 — Critical: - Fix compare URL sharing: seed ComparisonContext from SSR initialData when localStorage is empty, making /compare?urns=... links shareable - Remove permanently broken "Avg. Scaled Score" column from school detail historical data table P1 — High priority: - Add radius selector (0.5–10 mi) to postcode search in FilterBar - Make Add to Compare a toggle (remove) on SchoolCards - Hide hero title/description once a search is active - Show school count + quick-search prompts on empty landing page - Compare empty state opens in-page school search modal directly - Remove URN from school detail header (irrelevant to end users) - Move map above performance chart in school detail page - Add ← Back navigation to school detail page - Add sort controls to search results (RWM%, distance, A–Z) - Show metric descriptions below metric selector - Expand ComparisonToast to list school names with per-school remove - Add progress score explainer (0 = national average) throughout P2 — Medium: - Remove console.log statements from ComparisonView - Colour-code comparison school cards to match chart line colours - Replace plain loading text with LoadingSkeleton in ComparisonView - Rankings empty state uses shared EmptyState component - Rankings year filter shows actual year e.g. "2023 (Latest)" - Rankings subtitle shows top-N count - Add View link alongside Add button in rankings table - Remove placeholder Privacy Policy / Terms links from footer - Replace untappable 10px info icons with visible metric hint text - Show active filter chips in search results header P3 — Polish: - Remove redundant "Home" nav link (logo already links home) - Add / and Ctrl+K keyboard shortcut to focus search input - Add Share button to compare page (copies URL to clipboard) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 21:31:28 +00:00
{/* Per-subject detail table */}
<div className={styles.metricGroupsGrid} style={{ marginTop: '1rem' }}>
<div className={styles.metricGroup}>
<h3 className={styles.metricGroupTitle}>Reading</h3>
<div className={styles.metricTable}>
{latestResults.reading_expected_pct !== null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Expected level</span>
<span className={styles.metricValue}>{formatPercentage(latestResults.reading_expected_pct)}</span>
</div>
)}
{latestResults.reading_high_pct !== null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Exceeding</span>
<span className={styles.metricValue}>{formatPercentage(latestResults.reading_high_pct)}</span>
</div>
)}
{latestResults.reading_progress !== null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Progress score</span>
<span className={`${styles.metricValue} ${progressClass(latestResults.reading_progress)}`}>
{formatProgress(latestResults.reading_progress)}
</span>
</div>
)}
{latestResults.reading_avg_score !== null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Average score</span>
<span className={styles.metricValue}>{latestResults.reading_avg_score.toFixed(1)}</span>
</div>
)}
</div>
</div>
<div className={styles.metricGroup}>
<h3 className={styles.metricGroupTitle}>Writing</h3>
<div className={styles.metricTable}>
{latestResults.writing_expected_pct !== null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Expected level</span>
<span className={styles.metricValue}>{formatPercentage(latestResults.writing_expected_pct)}</span>
</div>
)}
{latestResults.writing_high_pct !== null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Exceeding</span>
<span className={styles.metricValue}>{formatPercentage(latestResults.writing_high_pct)}</span>
</div>
)}
{latestResults.writing_progress !== null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Progress score</span>
<span className={`${styles.metricValue} ${progressClass(latestResults.writing_progress)}`}>
{formatProgress(latestResults.writing_progress)}
</span>
</div>
)}
</div>
</div>
<div className={styles.metricGroup}>
<h3 className={styles.metricGroupTitle}>Maths</h3>
<div className={styles.metricTable}>
{latestResults.maths_expected_pct !== null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Expected level</span>
<span className={styles.metricValue}>{formatPercentage(latestResults.maths_expected_pct)}</span>
</div>
)}
{latestResults.maths_high_pct !== null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Exceeding</span>
<span className={styles.metricValue}>{formatPercentage(latestResults.maths_high_pct)}</span>
</div>
)}
{latestResults.maths_progress !== null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Progress score</span>
<span className={`${styles.metricValue} ${progressClass(latestResults.maths_progress)}`}>
{formatProgress(latestResults.maths_progress)}
</span>
</div>
)}
{latestResults.maths_avg_score !== null && (
<div className={styles.metricRow}>
<span className={styles.metricName}>Average score</span>
<span className={styles.metricValue}>{latestResults.maths_avg_score.toFixed(1)}</span>
</div>
)}
</div>
</div>
</div>
{(latestResults.reading_progress !== null || latestResults.writing_progress !== null || latestResults.maths_progress !== null) && (
<p className={styles.progressNote}>
Progress scores measure how much pupils improved compared to similar schools nationally. Above 0 = better than average, below 0 = below average.
</p>
)}
</section>
)}
{/* Year 1 Phonics */}
{hasPhonics && phonics && (
<section id="phonics" className={styles.card}>
<h2 className={styles.sectionTitle}>Year 1 Phonics ({phonics.year})</h2>
<p className={styles.sectionSubtitle}>
Phonics is a key early reading skill. Children are tested at the end of Year 1.
</p>
<div className={styles.metricsGrid}>
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Passed the phonics check</div>
<div className={styles.metricValue}>{formatPercentage(phonics.year1_phonics_pct)}</div>
<div className={styles.metricHint}>National avg: ~{NATIONAL_AVG.phonics_yr1}%</div>
</div>
{phonics.year2_phonics_pct != null && (
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Year 2 pupils who retook and passed</div>
<div className={styles.metricValue}>{formatPercentage(phonics.year2_phonics_pct)}</div>
</div>
)}
</div>
</section>
)}
{/* School Life */}
{hasSchoolLife && (
<section id="school-life" className={styles.card}>
<h2 className={styles.sectionTitle}>School Life</h2>
<div className={styles.metricsGrid}>
{census?.class_size_avg != null && (
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Average class size</div>
<div className={styles.metricValue}>{census.class_size_avg.toFixed(1)}</div>
<div className={styles.metricHint}>National avg: ~{NATIONAL_AVG.class_size} pupils</div>
</div>
)}
{absenceData?.overall_absence_rate != null && (
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Days missed (overall absence)</div>
<div className={styles.metricValue}>{formatPercentage(absenceData.overall_absence_rate)}</div>
<div className={styles.metricHint}>National avg: ~{NATIONAL_AVG.overall_absence}%</div>
</div>
)}
{absenceData?.persistent_absence_rate != null && (
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Regularly missing school</div>
<div className={styles.metricValue}>{formatPercentage(absenceData.persistent_absence_rate)}</div>
<div className={styles.metricHint}>National avg: ~{NATIONAL_AVG.persistent_absence}%. Missing 10%+ of sessions.</div>
</div>
)}
</div>
</section>
)}
{/* How Hard to Get In */}
{admissions && (
<section id="admissions" className={styles.card}>
<h2 className={styles.sectionTitle}>How Hard to Get Into This School ({admissions.year})</h2>
<div className={styles.metricsGrid}>
{admissions.published_admission_number != null && (
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Year 3 places per year</div>
<div className={styles.metricValue}>{admissions.published_admission_number}</div>
</div>
)}
{admissions.total_applications != null && (
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Applications received</div>
<div className={styles.metricValue}>{admissions.total_applications.toLocaleString()}</div>
</div>
)}
{admissions.first_preference_offers_pct != null && (
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Families who got their first-choice</div>
<div className={styles.metricValue}>{admissions.first_preference_offers_pct}%</div>
</div>
)}
</div>
{admissions.oversubscribed != null && (
<div className={`${styles.admissionsBadge} ${admissions.oversubscribed ? styles.statusWarn : styles.statusGood}`}>
{admissions.oversubscribed
? '⚠ More applications than places last year'
: '✓ Places were available last year'}
</div>
)}
</section>
)}
{/* Pupils & Inclusion */}
{hasInclusionData && (
<section id="inclusion" className={styles.card}>
<h2 className={styles.sectionTitle}>Pupils &amp; Inclusion</h2>
<div className={styles.metricsGrid}>
{latestResults?.disadvantaged_pct != null && (
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Eligible for pupil premium</div>
<div className={styles.metricValue}>{formatPercentage(latestResults.disadvantaged_pct)}</div>
<div className={styles.metricHint}>Pupils from disadvantaged backgrounds</div>
</div>
)}
{latestResults?.eal_pct != null && (
<div className={styles.metricCard}>
<div className={styles.metricLabel}>English as an additional language</div>
<div className={styles.metricValue}>{formatPercentage(latestResults.eal_pct)}</div>
</div>
)}
{latestResults?.sen_support_pct != null && (
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Pupils with additional needs (SEN support)</div>
<div className={styles.metricValue}>{formatPercentage(latestResults.sen_support_pct)}</div>
</div>
)}
</div>
{senDetail && (
<>
<h3 className={styles.subSectionTitle}>Types of additional needs supported</h3>
<p className={styles.sectionSubtitle}>
What proportion of pupils with additional needs have each type of support need.
</p>
<div className={styles.metricsGrid}>
{[
{ label: 'Speech & Language', pct: senDetail.primary_need_speech_pct },
{ label: 'Autism (ASD)', pct: senDetail.primary_need_autism_pct },
{ label: 'Learning Difficulties', pct: senDetail.primary_need_mld_pct },
{ label: 'Specific Learning (e.g. Dyslexia)', pct: senDetail.primary_need_spld_pct },
{ label: 'Social, Emotional & Mental Health', pct: senDetail.primary_need_semh_pct },
{ label: 'Physical / Sensory', pct: senDetail.primary_need_physical_pct },
].filter(n => n.pct != null).map(({ label, pct }) => (
<div key={label} className={styles.metricCard}>
<div className={styles.metricLabel}>{label}</div>
<div className={styles.metricValue}>{pct}%</div>
</div>
))}
</div>
</>
)}
</section>
)}
{/* Location */}
{hasLocation && (
<section id="location" className={styles.card}>
<h2 className={styles.sectionTitle}>Location</h2>
<div className={styles.mapContainer}>
<SchoolMap
schools={[schoolInfo]}
center={[schoolInfo.latitude!, schoolInfo.longitude!]}
zoom={15}
/>
</div>
</section>
)}
{/* Local Area Context */}
{hasDeprivation && deprivation && (
<section id="local-area" className={styles.card}>
<h2 className={styles.sectionTitle}>Local Area Context</h2>
<div className={styles.deprivationDots}>
{Array.from({ length: 10 }, (_, i) => (
<div
key={i}
className={`${styles.deprivationDot} ${i < deprivation.idaci_decile! ? styles.deprivationDotFilled : ''}`}
title={`Decile ${i + 1}`}
/>
))}
</div>
<div className={styles.deprivationScaleLabel}>
<span>Most deprived</span>
<span>Least deprived</span>
</div>
<p className={styles.deprivationDesc}>{deprivationDesc(deprivation.idaci_decile!)}</p>
</section>
)}
{/* Finances */}
{hasFinance && finance && (
<section id="finances" className={styles.card}>
<h2 className={styles.sectionTitle}>School Finances ({finance.year})</h2>
<p className={styles.sectionSubtitle}>
Per-pupil spending shows how much the school has to spend on each child&apos;s education.
</p>
<div className={styles.metricsGrid}>
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Total spend per pupil per year</div>
<div className={styles.metricValue}>£{Math.round(finance.per_pupil_spend!).toLocaleString()}</div>
<div className={styles.metricHint}>National avg: ~£{NATIONAL_AVG.per_pupil_spend.toLocaleString()}</div>
</div>
{finance.teacher_cost_pct != null && (
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Share of budget spent on teachers</div>
<div className={styles.metricValue}>{finance.teacher_cost_pct.toFixed(1)}%</div>
</div>
)}
{finance.staff_cost_pct != null && (
<div className={styles.metricCard}>
<div className={styles.metricLabel}>Share of budget spent on all staff</div>
<div className={styles.metricValue}>{finance.staff_cost_pct.toFixed(1)}%</div>
</div>
)}
</div>
</section>
)}
{/* Results Over Time (merged: chart + historical table) */}
{yearlyData.length > 0 && (
<section id="history" className={styles.card}>
<h2 className={styles.sectionTitle}>Results Over Time</h2>
<div className={styles.chartContainer}>
<PerformanceChart
data={yearlyData}
schoolName={schoolInfo.school_name}
/>
</div>
{yearlyData.length > 1 && (
<>
<p className={styles.historicalSubtitle}>Detailed year-by-year figures</p>
<div className={styles.tableWrapper}>
<table className={styles.dataTable}>
<thead>
<tr>
<th>Year</th>
<th>Reading, Writing & Maths (expected %)</th>
<th>Exceeding expected (%)</th>
<th>Reading Progress</th>
<th>Writing Progress</th>
<th>Maths Progress</th>
</tr>
</thead>
<tbody>
{yearlyData.map((result) => (
<tr key={result.year}>
<td className={styles.yearCell}>{result.year}</td>
<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.reading_progress !== null ? formatProgress(result.reading_progress) : '-'}</td>
<td>{result.writing_progress !== null ? formatProgress(result.writing_progress) : '-'}</td>
<td>{result.maths_progress !== null ? formatProgress(result.maths_progress) : '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
</section>
)}
</div>
);
}