250 lines
11 KiB
TypeScript
250 lines
11 KiB
TypeScript
/**
|
||||
|
|
* GcseSection — KS4 headline results. Secondary pages. Server component.
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import type { School, SchoolResult } from '@/lib/types';
|
|||
|
|
import { formatPercentage, formatProgress, formatAcademicYear } from '@/lib/utils';
|
|||
|
|
import { MetricTooltip } from '../MetricTooltip';
|
|||
|
|
import { DeltaChip } from '../DeltaChip';
|
|||
|
|
import { SpecialSchoolNote } from '../SpecialSchoolNote';
|
|||
|
|
import { Section, sectionStyles as styles, progressClass } from './sectionShared';
|
|||
|
|
|
|||
|
|
export function GcseSection({
|
|||
|
|
latestResults, schoolInfo, secondaryAvg, p8Suspended, suppressComparison,
|
|||
|
|
}: {
|
|||
|
|
latestResults: SchoolResult;
|
|||
|
|
schoolInfo: School;
|
|||
|
|
secondaryAvg: Record<string, number>;
|
|||
|
|
p8Suspended: boolean;
|
|||
|
|
suppressComparison: boolean;
|
|||
|
|
}) {
|
|||
|
|
return (
|
|||
|
|
<section id="gcse" className={styles.card}>
|
|||
|
|
<h2 className={styles.sectionTitle}>
|
|||
|
|
GCSE Results ({formatAcademicYear(latestResults.year)})
|
|||
|
|
</h2>
|
|||
|
|
<p className={styles.sectionSubtitle}>
|
|||
|
|
GCSE results for Year 11 pupils.{!suppressComparison && ' England averages shown for comparison.'}
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
<SpecialSchoolNote school={schoolInfo} />
|
|||
|
|
|
|||
|
|
{p8Suspended && (
|
|||
|
|
<div className={styles.p8Banner}>
|
|||
|
|
Progress 8 isn't published for 2024/25: this GCSE year group sat no KS2 tests
|
|||
|
|
(COVID), so DfE has no starting point to measure their progress from.
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Hero stat cards — top GCSE metrics */}
|
|||
|
|
<div className={styles.heroStatGrid}>
|
|||
|
|
{latestResults.attainment_8_score != null && (
|
|||
|
|
<div className={styles.heroStatCard}>
|
|||
|
|
<div className={styles.heroStatLabel}>
|
|||
|
|
Attainment 8 score
|
|||
|
|
<MetricTooltip metricKey="attainment_8_score" />
|
|||
|
|
</div>
|
|||
|
|
<div className={styles.heroStatValue}>
|
|||
|
|
{latestResults.attainment_8_score.toFixed(1)}
|
|||
|
|
{!suppressComparison && secondaryAvg.attainment_8_score != null && (
|
|||
|
|
<DeltaChip
|
|||
|
|
value={latestResults.attainment_8_score}
|
|||
|
|
baseline={secondaryAvg.attainment_8_score}
|
|||
|
|
unit="pts"
|
|||
|
|
size="sm"
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
{!suppressComparison && secondaryAvg.attainment_8_score != null && (
|
|||
|
|
<div className={styles.heroStatHint}>England avg: {secondaryAvg.attainment_8_score.toFixed(1)}</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
{latestResults.progress_8_score != null && (
|
|||
|
|
<div className={styles.heroStatCard}>
|
|||
|
|
<div className={styles.heroStatLabel}>
|
|||
|
|
Progress 8 score
|
|||
|
|
<MetricTooltip metricKey="progress_8_score" />
|
|||
|
|
</div>
|
|||
|
|
<div className={`${styles.heroStatValue} ${progressClass(latestResults.progress_8_score)}`}>
|
|||
|
|
{formatProgress(latestResults.progress_8_score)}
|
|||
|
|
</div>
|
|||
|
|
{(latestResults.progress_8_lower_ci != null && latestResults.progress_8_upper_ci != null) ? (
|
|||
|
|
<div className={styles.heroStatHint}>
|
|||
|
|
CI: {latestResults.progress_8_lower_ci.toFixed(2)} to {latestResults.progress_8_upper_ci.toFixed(2)}
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className={styles.heroStatHint}>National baseline: 0.0</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
{latestResults.english_maths_strong_pass_pct != null && (
|
|||
|
|
<div className={styles.heroStatCard}>
|
|||
|
|
<div className={styles.heroStatLabel}>
|
|||
|
|
English & Maths Grade 5+
|
|||
|
|
<MetricTooltip metricKey="english_maths_strong_pass_pct" />
|
|||
|
|
</div>
|
|||
|
|
<div className={styles.heroStatValue}>
|
|||
|
|
{formatPercentage(latestResults.english_maths_strong_pass_pct)}
|
|||
|
|
{!suppressComparison && secondaryAvg.english_maths_strong_pass_pct != null && (
|
|||
|
|
<DeltaChip
|
|||
|
|
value={latestResults.english_maths_strong_pass_pct}
|
|||
|
|
baseline={secondaryAvg.english_maths_strong_pass_pct}
|
|||
|
|
unit="pts"
|
|||
|
|
size="sm"
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
{!suppressComparison && secondaryAvg.english_maths_strong_pass_pct != null && (
|
|||
|
|
<div className={styles.heroStatHint}>England avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
{latestResults.english_maths_standard_pass_pct != null && (
|
|||
|
|
<div className={styles.heroStatCard}>
|
|||
|
|
<div className={styles.heroStatLabel}>
|
|||
|
|
English & Maths Grade 4+
|
|||
|
|
<MetricTooltip metricKey="english_maths_standard_pass_pct" />
|
|||
|
|
</div>
|
|||
|
|
<div className={styles.heroStatValue}>
|
|||
|
|
{formatPercentage(latestResults.english_maths_standard_pass_pct)}
|
|||
|
|
{!suppressComparison && secondaryAvg.english_maths_standard_pass_pct != null && (
|
|||
|
|
<DeltaChip
|
|||
|
|
value={latestResults.english_maths_standard_pass_pct}
|
|||
|
|
baseline={secondaryAvg.english_maths_standard_pass_pct}
|
|||
|
|
unit="pts"
|
|||
|
|
size="sm"
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
{!suppressComparison && secondaryAvg.english_maths_standard_pass_pct != null && (
|
|||
|
|
<div className={styles.heroStatHint}>England avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Attainment 8 visual bar (0–80 scale). This viz is explicitly
|
|||
|
|
"school vs national", so it's dropped for special schools where
|
|||
|
|
that comparison isn't meaningful. */}
|
|||
|
|
{!suppressComparison && latestResults.attainment_8_score != null && (
|
|||
|
|
<div className={styles.att8Viz}>
|
|||
|
|
<div className={styles.att8VizLabel}>Attainment 8 — school vs national</div>
|
|||
|
|
<div className={styles.att8VizTrack}>
|
|||
|
|
<div
|
|||
|
|
className={styles.att8VizFill}
|
|||
|
|
style={{ width: `${Math.min((latestResults.attainment_8_score / 80) * 100, 100)}%` }}
|
|||
|
|
/>
|
|||
|
|
{secondaryAvg.attainment_8_score != null && (
|
|||
|
|
<div
|
|||
|
|
className={styles.att8VizNatLine}
|
|||
|
|
style={{ left: `${(secondaryAvg.attainment_8_score / 80) * 100}%` }}
|
|||
|
|
>
|
|||
|
|
<div className={styles.att8VizNatPill}>
|
|||
|
|
Nat avg {secondaryAvg.attainment_8_score.toFixed(1)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div className={styles.att8VizTicks}>
|
|||
|
|
<span>0</span><span>20</span><span>40</span><span>60</span><span>80</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Progress 8 number line with CI */}
|
|||
|
|
{latestResults.progress_8_score != null && !p8Suspended && (
|
|||
|
|
<div className={styles.p8Viz}>
|
|||
|
|
<div className={styles.p8VizLabel}>Progress 8 — relative to national baseline (0)</div>
|
|||
|
|
{(() => {
|
|||
|
|
const p8 = latestResults.progress_8_score!;
|
|||
|
|
const lo = latestResults.progress_8_lower_ci ?? p8;
|
|||
|
|
const hi = latestResults.progress_8_upper_ci ?? p8;
|
|||
|
|
const range = 6; // −3 to +3
|
|||
|
|
const toX = (v: number) => `${Math.min(Math.max(((v + 3) / range) * 100, 0), 100)}%`;
|
|||
|
|
return (
|
|||
|
|
<div className={styles.p8VizTrack}>
|
|||
|
|
{/* CI band */}
|
|||
|
|
<div
|
|||
|
|
className={styles.p8VizCi}
|
|||
|
|
style={{ left: toX(lo), width: `calc(${toX(hi)} - ${toX(lo)})` }}
|
|||
|
|
/>
|
|||
|
|
{/* Zero line */}
|
|||
|
|
<div className={styles.p8VizZero} style={{ left: toX(0) }} />
|
|||
|
|
{/* Score dot */}
|
|||
|
|
<div
|
|||
|
|
className={`${styles.p8VizDot} ${p8 < 0 ? styles.p8VizDotNeg : ''}`}
|
|||
|
|
style={{ left: toX(p8) }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})()}
|
|||
|
|
<div className={styles.p8VizTicks}>
|
|||
|
|
<span>−3</span><span>−2</span><span>−1</span><span>0</span><span>+1</span><span>+2</span><span>+3</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Progress 8 component breakdown */}
|
|||
|
|
{(latestResults.progress_8_english != null || latestResults.progress_8_maths != null ||
|
|||
|
|
latestResults.progress_8_ebacc != null || latestResults.progress_8_open != null) && (
|
|||
|
|
<>
|
|||
|
|
<h3 className={styles.subSectionTitle}>Attainment 8 Components (Progress 8 contribution)</h3>
|
|||
|
|
<div className={styles.metricTable}>
|
|||
|
|
{[
|
|||
|
|
{ label: 'English', val: latestResults.progress_8_english },
|
|||
|
|
{ label: 'Maths', val: latestResults.progress_8_maths },
|
|||
|
|
{ label: 'EBacc subjects', val: latestResults.progress_8_ebacc },
|
|||
|
|
{ label: 'Open (other GCSEs)', val: latestResults.progress_8_open },
|
|||
|
|
].filter(r => r.val != null).map(({ label, val }) => (
|
|||
|
|
<div key={label} className={styles.metricRow}>
|
|||
|
|
<span className={styles.metricName}>{label}</span>
|
|||
|
|
<span className={`${styles.metricValue} ${progressClass(val)}`}>
|
|||
|
|
{formatProgress(val!)}
|
|||
|
|
</span>
|
|||
|
|
</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+</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+</span>
|
|||
|
|
<span className={styles.metricValue}>{formatPercentage(latestResults.ebacc_strong_pass_pct)}</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
{latestResults.ebacc_avg_score != null && (
|
|||
|
|
<div className={styles.metricRow}>
|
|||
|
|
<span className={styles.metricName}>EBacc average point score</span>
|
|||
|
|
<span className={styles.metricValue}>{latestResults.ebacc_avg_score.toFixed(2)}</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
</section>
|
|||
|
|
);
|
|||
|
|
}
|