|
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
|
|
formatPercentage, formatProgress, formatAcademicYear, isProposedToClose, ofstedLegacyAreas,
|
|
|
|
|
} from '@/lib/utils';
|
|
|
|
|
import { DeltaChip } from './DeltaChip';
|
|
|
|
|
import { summariseAdmissions } from '@/lib/compareLogic';
|
|
|
|
|
|
|
|
|
|
const PerformanceChart = dynamic(
|
|
|
|
|
() => import('./PerformanceChart').then((m) => m.PerformanceChart),
|
|
|
|
@@ -85,6 +86,10 @@ export function SchoolDetailView({
|
|
|
|
|
// Trend toggle only appears with ≥2 years carrying an offer rate.
|
|
|
|
|
const admissionsOfferYears = admissionsHistory.filter((h) => h.first_preference_offer_pct != null).length;
|
|
|
|
|
const showAdmissionsTrend = admissionsOfferYears >= 2;
|
|
|
|
|
// Banded interpretation of the first-choice offer rate ("More than half of
|
|
|
|
|
// first choices missed out" etc.) — the same banding the compare screen
|
|
|
|
|
// uses, so a low offer rate reads as how severe it actually is.
|
|
|
|
|
const admissionsSummary = summariseAdmissions(admissions);
|
|
|
|
|
// Only the section links scroll horizontally; Back and "All" stay pinned.
|
|
|
|
|
const sectionLinksRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
|
const [sectionNavAtEnd, setSectionNavAtEnd] = useState(false);
|
|
|
|
@@ -153,10 +158,16 @@ export function SchoolDetailView({
|
|
|
|
|
|
|
|
|
|
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
|
|
|
|
|
|
|
|
|
|
// Phase detection
|
|
|
|
|
// 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 (phonics,
|
|
|
|
|
// KS2 trend) back on and switches phase-specific copy to an all-ages framing.
|
|
|
|
|
const phase = schoolInfo.phase ?? '';
|
|
|
|
|
const isSecondary = phase.toLowerCase().includes('secondary') || phase.toLowerCase() === 'all-through';
|
|
|
|
|
const isAllThrough = phase.toLowerCase() === 'all-through';
|
|
|
|
|
const isSecondary = phase.toLowerCase().includes('secondary') || isAllThrough;
|
|
|
|
|
const isPrimary = !isSecondary;
|
|
|
|
|
// Primary-stage content shows for pure-primary AND all-through schools.
|
|
|
|
|
const showPrimaryContent = isPrimary || isAllThrough;
|
|
|
|
|
|
|
|
|
|
// National averages (fetched dynamically so they stay current)
|
|
|
|
|
const [nationalAvg, setNationalAvg] = useState<NationalAverages | null>(null);
|
|
|
|
@@ -228,11 +239,11 @@ export function SchoolDetailView({
|
|
|
|
|
// 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: isSecondary ? 'GCSEs' : 'SATs' });
|
|
|
|
|
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 (hasPhonics && isPrimary) navItems.push({ id: 'phonics', label: 'Phonics' });
|
|
|
|
|
if (hasPhonics && showPrimaryContent) navItems.push({ id: 'phonics', label: 'Phonics' });
|
|
|
|
|
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' });
|
|
|
|
@@ -319,6 +330,9 @@ export function SchoolDetailView({
|
|
|
|
|
{schoolInfo.school_type && (
|
|
|
|
|
<span className={styles.metaItem}>{schoolInfo.school_type}</span>
|
|
|
|
|
)}
|
|
|
|
|
{isAllThrough && (
|
|
|
|
|
<span className={styles.metaItem}>All-through (primary & secondary)</span>
|
|
|
|
|
)}
|
|
|
|
|
{schoolInfo.gender && schoolInfo.gender !== 'Mixed' && (
|
|
|
|
|
<span className={styles.metaItem}>{schoolInfo.gender}'s school</span>
|
|
|
|
|
)}
|
|
|
|
@@ -607,17 +621,22 @@ export function SchoolDetailView({
|
|
|
|
|
{hasAnyResults && latestResults && (
|
|
|
|
|
<section id="results" className={styles.card}>
|
|
|
|
|
<h2 className={styles.sectionTitle}>
|
|
|
|
|
{isSecondary ? 'GCSE Results' : 'SATs Results'} ({formatAcademicYear(latestResults.year)})
|
|
|
|
|
{isAllThrough ? 'SATs & GCSE Results' : isSecondary ? 'GCSE Results' : 'SATs Results'} ({formatAcademicYear(latestResults.year)})
|
|
|
|
|
</h2>
|
|
|
|
|
<p className={styles.sectionSubtitle}>
|
|
|
|
|
{isSecondary
|
|
|
|
|
? 'GCSE results for Year 11 pupils. England averages shown for comparison.'
|
|
|
|
|
: 'End-of-primary-school tests taken by Year 6 pupils. England averages shown for comparison.'}
|
|
|
|
|
{isAllThrough
|
|
|
|
|
? 'KS2 SATs (end of Year 6) and GCSE results (Year 11) — this school covers both. England averages shown for comparison.'
|
|
|
|
|
: isSecondary
|
|
|
|
|
? 'GCSE results for Year 11 pupils. England averages shown for comparison.'
|
|
|
|
|
: 'End-of-primary-school tests taken by Year 6 pupils. England averages shown for comparison.'}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
{/* ── Primary / KS2 content ── */}
|
|
|
|
|
{hasKS2Results && (
|
|
|
|
|
<>
|
|
|
|
|
{isAllThrough && (
|
|
|
|
|
<h3 className={styles.subSectionTitle}>Primary — KS2 SATs (Year 6)</h3>
|
|
|
|
|
)}
|
|
|
|
|
<div className={styles.heroStatGrid}>
|
|
|
|
|
{latestResults.rwm_expected_pct !== null && (
|
|
|
|
|
<div className={styles.heroStatCard}>
|
|
|
|
@@ -756,6 +775,9 @@ export function SchoolDetailView({
|
|
|
|
|
{/* ── Secondary / KS4 content ── */}
|
|
|
|
|
{hasKS4Results && (
|
|
|
|
|
<>
|
|
|
|
|
{isAllThrough && (
|
|
|
|
|
<h3 className={styles.subSectionTitle} style={{ marginTop: '1.5rem' }}>Secondary — GCSEs (Year 11)</h3>
|
|
|
|
|
)}
|
|
|
|
|
<div className={styles.metricsGrid}>
|
|
|
|
|
{latestResults.attainment_8_score !== null && (
|
|
|
|
|
<div className={styles.metricCard}>
|
|
|
|
@@ -865,6 +887,15 @@ export function SchoolDetailView({
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{/* All-through admissions data covers a single entry point (usually the
|
|
|
|
|
Year 7 secondary intake), not reception — say so, or a parent could
|
|
|
|
|
read these as the whole-school figures. */}
|
|
|
|
|
{isAllThrough && admissions.school_phase && (
|
|
|
|
|
<p className={styles.sectionSubtitle}>
|
|
|
|
|
These figures are for {admissions.school_phase.toLowerCase()} entry
|
|
|
|
|
{/secondary/i.test(admissions.school_phase) ? ' (Year 7)' : /primary/i.test(admissions.school_phase) ? ' (Reception)' : ''}.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className={styles.admissionsViewport}>
|
|
|
|
|
{/* This-year Q&A */}
|
|
|
|
@@ -906,6 +937,9 @@ export function SchoolDetailView({
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</dl>
|
|
|
|
|
{admissionsSummary.chip && (
|
|
|
|
|
<p className={styles.admissionsTrendSummary}>{admissionsSummary.chip.text}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Multi-year trend */}
|
|
|
|
@@ -941,7 +975,7 @@ export function SchoolDetailView({
|
|
|
|
|
<DeltaChip value={latestResults.disadvantaged_pct} baseline={primaryAvg.disadvantaged_pct} unit="pts" size="sm" />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className={styles.heroStatHint}>Pupils from disadvantaged backgrounds{primaryAvg.disadvantaged_pct != null ? ` · national avg: ${primaryAvg.disadvantaged_pct.toFixed(0)}%` : ''}</div>
|
|
|
|
|
<div className={styles.heroStatHint}>Pupils from disadvantaged backgrounds{primaryAvg.disadvantaged_pct != null ? ` · England avg: ${primaryAvg.disadvantaged_pct.toFixed(0)}%` : ''}</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{latestResults?.eal_pct != null && (
|
|
|
|
@@ -1038,16 +1072,52 @@ export function SchoolDetailView({
|
|
|
|
|
{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>
|
|
|
|
|
{isAllThrough ? (
|
|
|
|
|
// All-through: KS2 and KS4 trends are on different scales and have
|
|
|
|
|
// different gap stories, so render them as two stacked charts
|
|
|
|
|
// rather than crowding 8+ series onto one axis.
|
|
|
|
|
<>
|
|
|
|
|
{hasKS2Results && (
|
|
|
|
|
<>
|
|
|
|
|
<h3 className={styles.subSectionTitle}>Primary — KS2 SATs</h3>
|
|
|
|
|
<div className={styles.chartContainer}>
|
|
|
|
|
<PerformanceChart
|
|
|
|
|
data={yearlyData}
|
|
|
|
|
schoolName={schoolInfo.school_name}
|
|
|
|
|
isSecondary={false}
|
|
|
|
|
nationalRwmAvg={primaryAvg.rwm_expected_pct ?? null}
|
|
|
|
|
nationalByYear={nationalAvg?.by_year}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{hasKS4Results && (
|
|
|
|
|
<>
|
|
|
|
|
<h3 className={styles.subSectionTitle} style={{ marginTop: '1.5rem' }}>Secondary — GCSEs</h3>
|
|
|
|
|
<div className={styles.chartContainer}>
|
|
|
|
|
<PerformanceChart
|
|
|
|
|
data={yearlyData}
|
|
|
|
|
schoolName={schoolInfo.school_name}
|
|
|
|
|
isSecondary={true}
|
|
|
|
|
nationalAtt8Avg={secondaryAvg.attainment_8_score ?? null}
|
|
|
|
|
nationalByYear={nationalAvg?.by_year}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<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>
|
|
|
|
@@ -1056,7 +1126,15 @@ export function SchoolDetailView({
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>Year</th>
|
|
|
|
|
{isSecondary ? (
|
|
|
|
|
{isAllThrough ? (
|
|
|
|
|
<>
|
|
|
|
|
<th>RWM (expected %)</th>
|
|
|
|
|
<th>Exceeding (%)</th>
|
|
|
|
|
<th>Attainment 8</th>
|
|
|
|
|
<th>Progress 8</th>
|
|
|
|
|
<th>English & Maths Grade 4+</th>
|
|
|
|
|
</>
|
|
|
|
|
) : isSecondary ? (
|
|
|
|
|
<>
|
|
|
|
|
<th>Attainment 8</th>
|
|
|
|
|
<th>Progress 8</th>
|
|
|
|
@@ -1078,7 +1156,15 @@ export function SchoolDetailView({
|
|
|
|
|
{yearlyData.map((result) => (
|
|
|
|
|
<tr key={result.year}>
|
|
|
|
|
<td className={styles.yearCell}>{formatAcademicYear(result.year)}</td>
|
|
|
|
|
{isSecondary ? (
|
|
|
|
|
{isAllThrough ? (
|
|
|
|
|
<>
|
|
|
|
|
<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.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>
|
|
|
|
|
</>
|
|
|
|
|
) : 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>
|
|
|
|
@@ -1103,8 +1189,8 @@ export function SchoolDetailView({
|
|
|
|
|
)}
|
|
|
|
|
</section>
|
|
|
|
|
)}
|
|
|
|
|
{/* Year 1 Phonics — primary only */}
|
|
|
|
|
{hasPhonics && isPrimary && phonics && (
|
|
|
|
|
{/* Year 1 Phonics — primary-stage metric (pure primary + all-through) */}
|
|
|
|
|
{hasPhonics && showPrimaryContent && phonics && (
|
|
|
|
|
<section id="phonics" className={styles.card}>
|
|
|
|
|
<h2 className={styles.sectionTitle}>Year 1 Phonics ({formatAcademicYear(phonics.year)})</h2>
|
|
|
|
|
<p className={styles.sectionSubtitle}>
|
|
|
|
|