5abab067a1
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 13s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 50s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 1m4s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
The "How Hard to Get Into This School" tile mixed a progress bar (places vs first-choice) with three text metric cards, making the data feel fragmented and hiding the real narrative. The progress bar also broke visually when undersubscribed and didn't scale to different school sizes. Replace with a typographic Q&A list that answers the questions parents actually ask — "How many places were offered?", "How many families wanted this school first?", "How many got their first choice?", "How many applied in total?" — with a verdict footer (Oversubscribed / Not oversubscribed + one-sentence explanation). The third row now uses first_preference_offers (already in the API response) to show "27 of 42 (64.3%)" instead of just the percentage, giving the raw count parents actually want. Each row is independently null-gated; rows stack vertically under 480px so the Playfair numeral stays legible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1059 lines
49 KiB
TypeScript
1059 lines
49 KiB
TypeScript
/**
|
|
* SchoolDetailView Component
|
|
* Displays comprehensive school information with performance charts
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useComparison } from '@/hooks/useComparison';
|
|
import { PerformanceChart } from './PerformanceChart';
|
|
import { SchoolMap } from './SchoolMap';
|
|
import { MetricTooltip } from './MetricTooltip';
|
|
import type {
|
|
School, SchoolResult, AbsenceData,
|
|
OfstedInspection, OfstedParentView, SchoolCensus,
|
|
SchoolAdmissions, SenDetail, Phonics,
|
|
SchoolDeprivation, SchoolFinance, NationalAverages,
|
|
} from '@/lib/types';
|
|
import {
|
|
formatPercentage, formatProgress, formatAcademicYear,
|
|
buildOfstedHeroChip,
|
|
} from '@/lib/utils';
|
|
import { DeltaChip } from './DeltaChip';
|
|
import SatsChart from './SatsChart';
|
|
import styles from './SchoolDetailView.module.css';
|
|
|
|
const OFSTED_LABELS: Record<number, string> = {
|
|
1: 'Outstanding', 2: 'Good', 3: 'Requires Improvement', 4: 'Inadequate',
|
|
};
|
|
|
|
const RC_LABELS: Record<number, string> = {
|
|
1: 'Exceptional', 2: 'Strong', 3: 'Expected standard', 4: 'Needs attention', 5: 'Urgent improvement',
|
|
};
|
|
|
|
const RC_CATEGORIES = [
|
|
{ key: 'rc_inclusion' as const, label: 'Inclusion' },
|
|
{ key: 'rc_curriculum_teaching' as const, label: 'Curriculum & Teaching' },
|
|
{ key: 'rc_achievement' as const, label: 'Achievement' },
|
|
{ key: 'rc_attendance_behaviour' as const, label: 'Attendance & Behaviour' },
|
|
{ key: 'rc_personal_development' as const, label: 'Personal Development' },
|
|
{ key: 'rc_leadership_governance' as const, label: 'Leadership & Governance' },
|
|
{ key: 'rc_early_years' as const, label: 'Early Years' },
|
|
{ key: 'rc_sixth_form' as const, label: 'Sixth Form' },
|
|
];
|
|
|
|
|
|
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) {
|
|
const router = useRouter();
|
|
const { addSchool, removeSchool, isSelected } = useComparison();
|
|
const isInComparison = isSelected(schoolInfo.urn);
|
|
|
|
const [activeSection, setActiveSection] = useState<string>('');
|
|
|
|
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
|
|
|
|
// Phase detection
|
|
const phase = schoolInfo.phase ?? '';
|
|
const isSecondary = phase.toLowerCase().includes('secondary') || phase.toLowerCase() === 'all-through';
|
|
const isPrimary = !isSecondary;
|
|
|
|
// National averages (fetched dynamically so they stay current)
|
|
const [nationalAvg, setNationalAvg] = useState<NationalAverages | null>(null);
|
|
useEffect(() => {
|
|
fetch('/api/national-averages')
|
|
.then(r => r.ok ? r.json() : null)
|
|
.then(data => { if (data) setNationalAvg(data); })
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
const primaryAvg = nationalAvg?.primary ?? {};
|
|
const secondaryAvg = nationalAvg?.secondary ?? {};
|
|
|
|
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;
|
|
|
|
// 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;
|
|
|
|
// 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 (hasAnyResults) navItems.push({ id: 'results', label: isSecondary ? 'GCSEs' : 'SATs' });
|
|
if (hasPhonics && isPrimary) 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' });
|
|
|
|
// Track active section as user scrolls
|
|
useEffect(() => {
|
|
const ids = navItems.map(n => n.id);
|
|
if (!ids.length) return;
|
|
|
|
const observers: IntersectionObserver[] = [];
|
|
const ratioMap: Record<string, number> = {};
|
|
|
|
const pickActive = () => {
|
|
const top = Object.entries(ratioMap).sort((a, b) => b[1] - a[1])[0];
|
|
setActiveSection(top?.[1] > 0 ? top[0] : '');
|
|
};
|
|
|
|
ids.forEach(id => {
|
|
const el = document.getElementById(id);
|
|
if (!el) return;
|
|
ratioMap[id] = 0;
|
|
const obs = new IntersectionObserver(
|
|
([entry]) => {
|
|
ratioMap[id] = entry.intersectionRatio;
|
|
pickActive();
|
|
},
|
|
{ threshold: [0, 0.1, 0.25, 0.5, 0.75, 1.0], rootMargin: '-56px 0px 0px 0px' },
|
|
);
|
|
obs.observe(el);
|
|
observers.push(obs);
|
|
});
|
|
|
|
return () => observers.forEach(o => o.disconnect());
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [navItems.map(n => n.id).join(',')]);
|
|
|
|
// ── Ofsted: detect if all OEIF sub-grades match the overall ───────────
|
|
const oeifAllSameGrade = (() => {
|
|
if (!ofsted || ofsted.framework === 'ReportCard') return false;
|
|
const subs = [
|
|
ofsted.quality_of_education,
|
|
ofsted.behaviour_attitudes,
|
|
ofsted.personal_development,
|
|
ofsted.leadership_management,
|
|
...(ofsted.early_years_provision != null ? [ofsted.early_years_provision] : []),
|
|
].filter((v): v is number => v != null);
|
|
return subs.length >= 3 && subs.every(v => v === ofsted.overall_effectiveness);
|
|
})();
|
|
|
|
// ── Hero: framework-aware signal chip + narrative summary ─────────────
|
|
const ofstedHeroChip = buildOfstedHeroChip(ofsted);
|
|
|
|
// KS2 headline numbers for the at-a-glance row
|
|
const heroRwm = isPrimary ? latestResults?.rwm_expected_pct ?? null : null;
|
|
const heroRwmNat = primaryAvg.rwm_expected_pct ?? null;
|
|
|
|
// KS4 headline number for secondary/all-through schools
|
|
const heroAtt8 = isSecondary ? latestResults?.attainment_8_score ?? null : null;
|
|
const heroAtt8Nat = secondaryAvg.attainment_8_score ?? null;
|
|
|
|
const heroAcademicYear = latestResults ? formatAcademicYear(latestResults.year) : '';
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
{/* 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}'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={/^https?:\/\//i.test(schoolInfo.website) ? schoolInfo.website : `https://${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>
|
|
|
|
{/* Hero signal chip strip */}
|
|
<div className={styles.heroChips}>
|
|
<div className={`${styles.heroChip} ${styles[`tone-${ofstedHeroChip.tone}`]}`}>
|
|
<div className={styles.heroChipTitle}>{ofstedHeroChip.title}</div>
|
|
<div className={styles.heroChipSub}>{ofstedHeroChip.subtitle}</div>
|
|
{ofstedHeroChip.detail && (
|
|
<div className={styles.heroChipDetail}>{ofstedHeroChip.detail}</div>
|
|
)}
|
|
</div>
|
|
|
|
{admissions?.oversubscribed && (
|
|
<div className={`${styles.heroChip} ${styles['tone-coral']}`}>
|
|
<div className={styles.heroChipTitle}>Oversubscribed</div>
|
|
<div className={styles.heroChipSub}>
|
|
{admissions.first_preference_offer_pct != null
|
|
? `${Math.round(admissions.first_preference_offer_pct)}% of first-choice applicants offered a place`
|
|
: 'More applicants than places'}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
|
|
{/* At-a-glance stats row */}
|
|
{latestResults && (
|
|
<div className={styles.heroStats}>
|
|
{isPrimary && heroRwm != null && (
|
|
<div className={styles.heroStat}>
|
|
<div className={styles.heroStatNumber}>{Math.round(heroRwm)}%</div>
|
|
<div className={styles.heroStatLabel}>Reading, Writing & Maths</div>
|
|
{heroRwmNat != null && (
|
|
<DeltaChip value={heroRwm} baseline={heroRwmNat} unit="pts" suffix="vs national" />
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{isSecondary && heroAtt8 != null && (
|
|
<div className={styles.heroStat}>
|
|
<div className={styles.heroStatNumber}>{heroAtt8.toFixed(1)}</div>
|
|
<div className={styles.heroStatLabel}>Attainment 8 score</div>
|
|
{heroAtt8Nat != null && (
|
|
<DeltaChip value={heroAtt8} baseline={heroAtt8Nat} unit="pts" suffix="vs national" />
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{ofsted && (
|
|
<div className={styles.heroStat}>
|
|
<div className={`${styles.heroStatNumberSerif} ${styles[`tone-${ofstedHeroChip.tone}`]}`}>
|
|
{ofstedHeroChip.state === 'oeif'
|
|
? ofstedHeroChip.title.replace(/^Ofsted\s+/, '')
|
|
: ofstedHeroChip.state === 'reportCard'
|
|
? 'Report Card'
|
|
: '—'}
|
|
</div>
|
|
<div className={styles.heroStatLabel}>
|
|
{ofstedHeroChip.subtitle}
|
|
</div>
|
|
{ofstedHeroChip.detail && (
|
|
<div className={styles.heroStatFoot}>{ofstedHeroChip.detail}</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{admissions?.first_preference_offer_pct != null && (
|
|
<div className={styles.heroStat}>
|
|
<div className={styles.heroStatNumber}>
|
|
{Math.round(admissions.first_preference_offer_pct)}%
|
|
</div>
|
|
<div className={styles.heroStatLabel}>First-choice offer rate</div>
|
|
{admissions.oversubscribed && (
|
|
<div className={styles.heroStatFoot}>Oversubscribed</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{heroAcademicYear && (
|
|
<p className={styles.heroDataNote}>
|
|
Latest data: {heroAcademicYear}
|
|
</p>
|
|
)}
|
|
</header>
|
|
|
|
{/* Sticky Section Navigation */}
|
|
<nav className={styles.sectionNav} aria-label="Page sections">
|
|
<div className={styles.sectionNavInner}>
|
|
<button onClick={() => router.back()} className={styles.sectionNavBack}>← Back</button>
|
|
{navItems.length > 0 && <div className={styles.sectionNavDivider} />}
|
|
{navItems.map(({ id, label }) => (
|
|
<a
|
|
key={id}
|
|
href={`#${id}`}
|
|
className={`${styles.sectionNavLink}${activeSection === id ? ` ${styles.sectionNavLinkActive}` : ''}`}
|
|
>
|
|
{label}
|
|
</a>
|
|
))}
|
|
</div>
|
|
</nav>
|
|
|
|
{/* Ofsted Rating / Report Card */}
|
|
{ofsted && (
|
|
<section id="ofsted" className={styles.card}>
|
|
<h2 className={styles.sectionTitle}>
|
|
{ofsted.framework === 'ReportCard' ? 'Ofsted Report Card' : '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>
|
|
|
|
{ofsted.framework === 'ReportCard' ? (
|
|
/* ── New Report Card layout ── */
|
|
<>
|
|
<p className={styles.ofstedDisclaimer}>
|
|
From November 2025, Ofsted replaced single overall grades with Report Cards rating schools across several areas.
|
|
</p>
|
|
<div className={styles.metricsGrid}>
|
|
{ofsted.rc_safeguarding_met != null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>Safeguarding</div>
|
|
<div className={`${styles.metricValue} ${ofsted.rc_safeguarding_met ? styles.safeguardingMet : styles.safeguardingNotMet}`}>
|
|
{ofsted.rc_safeguarding_met ? 'Met' : 'Not met'}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{RC_CATEGORIES.map(({ key, label }) => {
|
|
const value = ofsted[key] as number | null;
|
|
return value != null ? (
|
|
<div key={key} className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>{label}</div>
|
|
<div className={`${styles.metricValue} ${styles[`rcGrade${value}`]}`}>
|
|
{RC_LABELS[value]}
|
|
</div>
|
|
</div>
|
|
) : null;
|
|
})}
|
|
</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>
|
|
)}
|
|
</>
|
|
) : (
|
|
/* ── Old OEIF layout ── */
|
|
<>
|
|
<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>
|
|
<p className={styles.ofstedDisclaimer}>
|
|
From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections of state-funded schools.
|
|
</p>
|
|
{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>
|
|
)}
|
|
{oeifAllSameGrade ? (
|
|
<p className={styles.ofstedAllSame}>
|
|
Rated <strong>{OFSTED_LABELS[ofsted.overall_effectiveness!]}</strong> across all inspected areas — Quality of Teaching, Behaviour, Pupils' Development and Leadership.
|
|
</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>
|
|
)}
|
|
|
|
{/* Results Section (SATs for primary, GCSEs for secondary) */}
|
|
{hasAnyResults && latestResults && (
|
|
<section id="results" className={styles.card}>
|
|
<h2 className={styles.sectionTitle}>
|
|
{isSecondary ? 'GCSE Results' : 'SATs Results'} ({formatAcademicYear(latestResults.year)})
|
|
</h2>
|
|
<p className={styles.sectionSubtitle}>
|
|
{isSecondary
|
|
? 'GCSE results for Year 11 pupils. National averages shown for comparison.'
|
|
: 'End-of-primary-school tests taken by Year 6 pupils. National averages shown for comparison.'}
|
|
</p>
|
|
|
|
{/* ── Primary / KS2 content ── */}
|
|
{hasKS2Results && (
|
|
<>
|
|
<div className={styles.metricsGrid}>
|
|
{latestResults.rwm_expected_pct !== null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
Reading, Writing & Maths combined
|
|
<MetricTooltip metricKey="rwm_expected_pct" />
|
|
</div>
|
|
<div className={styles.metricValue}>
|
|
{formatPercentage(latestResults.rwm_expected_pct)}
|
|
{primaryAvg.rwm_expected_pct != null && (
|
|
<DeltaChip
|
|
value={latestResults.rwm_expected_pct}
|
|
baseline={primaryAvg.rwm_expected_pct}
|
|
unit="pts"
|
|
size="sm"
|
|
/>
|
|
)}
|
|
</div>
|
|
{primaryAvg.rwm_expected_pct != null && (
|
|
<div className={styles.metricHint}>National avg: {primaryAvg.rwm_expected_pct.toFixed(0)}%</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
{latestResults.rwm_high_pct !== null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
Exceeding expected level (Reading, Writing & Maths)
|
|
<MetricTooltip metricKey="rwm_high_pct" />
|
|
</div>
|
|
<div className={styles.metricValue}>
|
|
{formatPercentage(latestResults.rwm_high_pct)}
|
|
{primaryAvg.rwm_high_pct != null && (
|
|
<DeltaChip
|
|
value={latestResults.rwm_high_pct}
|
|
baseline={primaryAvg.rwm_high_pct}
|
|
unit="pts"
|
|
size="sm"
|
|
/>
|
|
)}
|
|
</div>
|
|
{primaryAvg.rwm_high_pct != null && (
|
|
<div className={styles.metricHint}>National avg: {primaryAvg.rwm_high_pct.toFixed(0)}%</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<SatsChart
|
|
subjects={[
|
|
{
|
|
name: 'Reading',
|
|
expectedPct: latestResults.reading_expected_pct,
|
|
exceedingPct: latestResults.reading_high_pct,
|
|
nationalExpectedPct: primaryAvg.reading_expected_pct,
|
|
},
|
|
{
|
|
name: 'Writing',
|
|
expectedPct: latestResults.writing_expected_pct,
|
|
exceedingPct: latestResults.writing_high_pct,
|
|
nationalExpectedPct: primaryAvg.writing_expected_pct,
|
|
},
|
|
{
|
|
name: 'Maths',
|
|
expectedPct: latestResults.maths_expected_pct,
|
|
exceedingPct: latestResults.maths_high_pct,
|
|
nationalExpectedPct: primaryAvg.maths_expected_pct,
|
|
},
|
|
]}
|
|
/>
|
|
|
|
{/* Progress scores row */}
|
|
{(latestResults.reading_progress != null || latestResults.writing_progress != null || latestResults.maths_progress != null) && (
|
|
<div className={styles.progressScoresRow}>
|
|
<h3 className={styles.subSectionTitle}>Progress Scores</h3>
|
|
<div className={styles.progressScoresGrid}>
|
|
{latestResults.reading_progress != null && (
|
|
<div className={styles.progressScoreItem}>
|
|
<span className={styles.progressScoreLabel}>Reading</span>
|
|
<span className={`${styles.progressScoreValue} ${progressClass(latestResults.reading_progress)}`}>
|
|
{formatProgress(latestResults.reading_progress)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{latestResults.writing_progress != null && (
|
|
<div className={styles.progressScoreItem}>
|
|
<span className={styles.progressScoreLabel}>Writing</span>
|
|
<span className={`${styles.progressScoreValue} ${progressClass(latestResults.writing_progress)}`}>
|
|
{formatProgress(latestResults.writing_progress)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{latestResults.maths_progress != null && (
|
|
<div className={styles.progressScoreItem}>
|
|
<span className={styles.progressScoreLabel}>Maths</span>
|
|
<span className={`${styles.progressScoreValue} ${progressClass(latestResults.maths_progress)}`}>
|
|
{formatProgress(latestResults.maths_progress)}
|
|
</span>
|
|
</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>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* ── Secondary / KS4 content ── */}
|
|
{hasKS4Results && (
|
|
<>
|
|
<div className={styles.metricsGrid}>
|
|
{latestResults.attainment_8_score !== null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
Attainment 8
|
|
<MetricTooltip metricKey="attainment_8_score" />
|
|
</div>
|
|
<div className={styles.metricValue}>{latestResults.attainment_8_score.toFixed(1)}</div>
|
|
{secondaryAvg.attainment_8_score != null && (
|
|
<div className={styles.metricHint}>National avg: {secondaryAvg.attainment_8_score.toFixed(1)}</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
{latestResults.progress_8_score !== null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
Progress 8
|
|
<MetricTooltip metricKey="progress_8_score" />
|
|
</div>
|
|
<div className={`${styles.metricValue} ${progressClass(latestResults.progress_8_score)}`}>
|
|
{formatProgress(latestResults.progress_8_score)}
|
|
</div>
|
|
<div className={styles.metricHint}>0 = national average</div>
|
|
</div>
|
|
)}
|
|
{latestResults.english_maths_standard_pass_pct !== null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
English & Maths Grade 4+
|
|
<MetricTooltip metricKey="english_maths_standard_pass_pct" />
|
|
</div>
|
|
<div className={styles.metricValue}>{formatPercentage(latestResults.english_maths_standard_pass_pct)}</div>
|
|
{secondaryAvg.english_maths_standard_pass_pct != null && (
|
|
<div className={styles.metricHint}>National avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
{latestResults.english_maths_strong_pass_pct !== null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
English & Maths Grade 5+
|
|
<MetricTooltip metricKey="english_maths_strong_pass_pct" />
|
|
</div>
|
|
<div className={styles.metricValue}>{formatPercentage(latestResults.english_maths_strong_pass_pct)}</div>
|
|
{secondaryAvg.english_maths_strong_pass_pct != null && (
|
|
<div className={styles.metricHint}>National avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* EBacc */}
|
|
{(latestResults.ebacc_entry_pct !== null || latestResults.ebacc_standard_pass_pct !== null) && (
|
|
<>
|
|
<h3 className={styles.subSectionTitle} style={{ marginTop: '1rem' }}>
|
|
English Baccalaureate (EBacc)
|
|
<MetricTooltip metricKey="ebacc_entry_pct" />
|
|
</h3>
|
|
<div className={styles.metricTable}>
|
|
{latestResults.ebacc_entry_pct !== null && (
|
|
<div className={styles.metricRow}>
|
|
<span className={styles.metricName}>Pupils entered for EBacc</span>
|
|
<span className={styles.metricValue}>{formatPercentage(latestResults.ebacc_entry_pct)}</span>
|
|
</div>
|
|
)}
|
|
{latestResults.ebacc_standard_pass_pct !== null && (
|
|
<div className={styles.metricRow}>
|
|
<span className={styles.metricName}>
|
|
EBacc Grade 4+
|
|
<MetricTooltip metricKey="ebacc_standard_pass_pct" />
|
|
</span>
|
|
<span className={styles.metricValue}>{formatPercentage(latestResults.ebacc_standard_pass_pct)}</span>
|
|
</div>
|
|
)}
|
|
{latestResults.ebacc_strong_pass_pct !== null && (
|
|
<div className={styles.metricRow}>
|
|
<span className={styles.metricName}>
|
|
EBacc Grade 5+
|
|
<MetricTooltip metricKey="ebacc_strong_pass_pct" />
|
|
</span>
|
|
<span className={styles.metricValue}>{formatPercentage(latestResults.ebacc_strong_pass_pct)}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
{/* Year 1 Phonics — primary only */}
|
|
{hasPhonics && isPrimary && phonics && (
|
|
<section id="phonics" className={styles.card}>
|
|
<h2 className={styles.sectionTitle}>Year 1 Phonics ({formatAcademicYear(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}>Phonics is a key early reading skill tested at end of Year 1</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}>Average number of pupils per class</div>
|
|
</div>
|
|
)}
|
|
{absenceData?.overall_absence_rate != null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
Days missed (overall absence)
|
|
<MetricTooltip metricKey="overall_absence_pct" />
|
|
</div>
|
|
<div className={styles.metricValue}>{formatPercentage(absenceData.overall_absence_rate)}</div>
|
|
{primaryAvg.overall_absence_pct != null && (
|
|
<div className={styles.metricHint}>National avg: ~{primaryAvg.overall_absence_pct.toFixed(1)}%</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
{absenceData?.persistent_absence_rate != null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
Regularly missing school
|
|
<MetricTooltip metricKey="persistent_absence_pct" />
|
|
</div>
|
|
<div className={styles.metricValue}>{formatPercentage(absenceData.persistent_absence_rate)}</div>
|
|
{primaryAvg.persistent_absence_pct != null && (
|
|
<div className={styles.metricHint}>National avg: ~{primaryAvg.persistent_absence_pct.toFixed(0)}%</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 ({formatAcademicYear(admissions.year)})</h2>
|
|
|
|
<dl className={styles.admissionsQa}>
|
|
{admissions.places_offered != null && (
|
|
<div className={styles.admissionsQaRow}>
|
|
<dt className={styles.admissionsQaQuestion}>How many places were offered?</dt>
|
|
<dd className={styles.admissionsQaAnswer}>{admissions.places_offered}</dd>
|
|
</div>
|
|
)}
|
|
{admissions.first_preference_applications != null && (
|
|
<div className={styles.admissionsQaRow}>
|
|
<dt className={styles.admissionsQaQuestion}>How many families wanted this school first?</dt>
|
|
<dd className={styles.admissionsQaAnswer}>{admissions.first_preference_applications}</dd>
|
|
</div>
|
|
)}
|
|
{admissions.first_preference_offer_pct != null && (
|
|
<div className={styles.admissionsQaRow}>
|
|
<dt className={styles.admissionsQaQuestion}>How many got their first choice?</dt>
|
|
<dd className={styles.admissionsQaAnswer}>
|
|
{admissions.first_preference_offers != null && admissions.first_preference_applications != null ? (
|
|
<>
|
|
{admissions.first_preference_offers}
|
|
<span className={styles.admissionsQaAnswerSub}>
|
|
of {admissions.first_preference_applications} ({formatPercentage(admissions.first_preference_offer_pct)})
|
|
</span>
|
|
</>
|
|
) : (
|
|
formatPercentage(admissions.first_preference_offer_pct)
|
|
)}
|
|
</dd>
|
|
</div>
|
|
)}
|
|
{admissions.total_applications != null && (
|
|
<div className={styles.admissionsQaRow}>
|
|
<dt className={styles.admissionsQaQuestion}>How many applied in total?</dt>
|
|
<dd className={styles.admissionsQaAnswer}>{admissions.total_applications.toLocaleString()}</dd>
|
|
</div>
|
|
)}
|
|
</dl>
|
|
|
|
{admissions.oversubscribed != null && (
|
|
<div className={styles.admissionsVerdict}>
|
|
<span className={`${styles.admissionsBadge} ${admissions.oversubscribed ? styles.statusWarn : styles.statusGood}`}>
|
|
{admissions.oversubscribed ? 'Oversubscribed' : 'Not oversubscribed'}
|
|
</span>
|
|
<span className={styles.admissionsVerdictText}>
|
|
{admissions.oversubscribed ? 'Demand exceeds capacity.' : 'Supply meets demand.'}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
{/* Pupils & Inclusion */}
|
|
{hasInclusionData && (
|
|
<section id="inclusion" className={styles.card}>
|
|
<h2 className={styles.sectionTitle}>Pupils & 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)}
|
|
{primaryAvg.disadvantaged_pct != null && (
|
|
<DeltaChip value={latestResults.disadvantaged_pct} baseline={primaryAvg.disadvantaged_pct} unit="pts" size="sm" />
|
|
)}
|
|
</div>
|
|
<div className={styles.metricHint}>Pupils from disadvantaged backgrounds{primaryAvg.disadvantaged_pct != null ? ` · national avg: ${primaryAvg.disadvantaged_pct.toFixed(0)}%` : ''}</div>
|
|
</div>
|
|
)}
|
|
{latestResults?.eal_pct != null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
English as an additional language
|
|
<MetricTooltip metricKey="eal_pct" />
|
|
</div>
|
|
<div className={styles.metricValue}>
|
|
{formatPercentage(latestResults.eal_pct)}
|
|
{primaryAvg.eal_pct != null && (
|
|
<DeltaChip value={latestResults.eal_pct} baseline={primaryAvg.eal_pct} unit="pts" size="sm" />
|
|
)}
|
|
</div>
|
|
{primaryAvg.eal_pct != null && (
|
|
<div className={styles.metricHint}>National avg: {primaryAvg.eal_pct.toFixed(0)}%</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
{latestResults?.sen_support_pct != null && (
|
|
<div className={styles.metricCard}>
|
|
<div className={styles.metricLabel}>
|
|
Pupils receiving SEN support
|
|
<MetricTooltip metricKey="sen_support_pct" />
|
|
</div>
|
|
<div className={styles.metricValue}>
|
|
{formatPercentage(latestResults.sen_support_pct)}
|
|
{primaryAvg.sen_support_pct != null && (
|
|
<DeltaChip value={latestResults.sen_support_pct} baseline={primaryAvg.sen_support_pct} unit="pts" size="sm" />
|
|
)}
|
|
</div>
|
|
{primaryAvg.sen_support_pct != null && (
|
|
<div className={styles.metricHint}>National avg: {primaryAvg.sen_support_pct.toFixed(0)}%</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
|
|
<MetricTooltip metricKey="idaci_decile" />
|
|
</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 ({formatAcademicYear(finance.year)})</h2>
|
|
<p className={styles.sectionSubtitle}>
|
|
Per-pupil spending shows how much the school has to spend on each child'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}>How much the school has to spend on each pupil annually</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}
|
|
isSecondary={isSecondary}
|
|
nationalRwmAvg={isPrimary ? (primaryAvg.rwm_expected_pct ?? null) : null}
|
|
nationalAtt8Avg={isSecondary ? (secondaryAvg.attainment_8_score ?? null) : null}
|
|
nationalByYear={nationalAvg?.by_year}
|
|
/>
|
|
</div>
|
|
{yearlyData.length > 1 && (
|
|
<details className={styles.historyDisclosure}>
|
|
<summary className={styles.historyToggle}>View raw year-by-year data</summary>
|
|
<div className={styles.tableWrapper}>
|
|
<table className={styles.dataTable}>
|
|
<thead>
|
|
<tr>
|
|
<th>Year</th>
|
|
{isSecondary ? (
|
|
<>
|
|
<th>Attainment 8</th>
|
|
<th>Progress 8</th>
|
|
<th>English & Maths Grade 4+</th>
|
|
<th>English & Maths Grade 5+</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}>{formatAcademicYear(result.year)}</td>
|
|
{isSecondary ? (
|
|
<>
|
|
<td>{result.attainment_8_score !== null ? result.attainment_8_score.toFixed(1) : '-'}</td>
|
|
<td>{result.progress_8_score !== null ? formatProgress(result.progress_8_score) : '-'}</td>
|
|
<td>{result.english_maths_standard_pass_pct !== null ? formatPercentage(result.english_maths_standard_pass_pct) : '-'}</td>
|
|
<td>{result.english_maths_strong_pass_pct !== null ? formatPercentage(result.english_maths_strong_pass_pct) : '-'}</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>
|
|
</details>
|
|
)}
|
|
</section>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|