feat(school-detail): reorder sections by parent demand (analytics-led)
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 13s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 55s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 13s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 55s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Reorders the detail-page sections so the most-sought content leads, based on
30-day section_nav_used analytics (nav clicks over-count buried sections, so a
low, high-demand section is a strong "surface me" signal):
Primary: Ofsted → SATs Results → Admissions → Pupils & Inclusion → History →
Phonics → What Parents Say → School Life → Location → Local Area → Finances
Secondary: Ofsted → GCSEs → Admissions → History → Parents → Wellbeing →
Location → Finances
Key moves: Admissions (23%, most-clicked) lifted from #6→#3; Pupils & Inclusion
(18%) from #7→#4; History (13%) off the bottom. Ofsted stays first (its low click
rate is positional — everyone reaches it first). Low-demand context sections
(Local Area, Finances) stay last. Brings the primary page in line with secondary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lh7Js5xSetKNzLVr9ArXLF
This commit is contained in:
@@ -218,20 +218,23 @@ export function SchoolDetailView({
|
|||||||
const hasKS4Results = latestResults != null && latestResults.attainment_8_score != null;
|
const hasKS4Results = latestResults != null && latestResults.attainment_8_score != null;
|
||||||
const hasAnyResults = hasKS2Results || hasKS4Results;
|
const hasAnyResults = hasKS2Results || hasKS4Results;
|
||||||
|
|
||||||
// Build section nav items dynamically — only sections with data
|
// Build section nav items dynamically — only sections with data.
|
||||||
|
// Order is engagement-led (from section_nav_used analytics): the most-sought
|
||||||
|
// sections — results, admissions, inclusion, history — sit near the top,
|
||||||
|
// after the recognised Ofsted badge; low-demand context sections stay last.
|
||||||
const navItems: { id: string; label: string }[] = [];
|
const navItems: { id: string; label: string }[] = [];
|
||||||
if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' });
|
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 (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 (admissions) navItems.push({ id: 'admissions', label: 'Admissions' });
|
||||||
if (hasInclusionData) navItems.push({ id: 'inclusion', label: 'Pupils' });
|
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 (parentView && parentView.total_responses != null && parentView.total_responses > 0)
|
||||||
|
navItems.push({ id: 'parents', label: 'Parents' });
|
||||||
|
if (hasSchoolLife) navItems.push({ id: 'school-life', label: 'School Life' });
|
||||||
if (hasLocation) navItems.push({ id: 'location', label: 'Location' });
|
if (hasLocation) navItems.push({ id: 'location', label: 'Location' });
|
||||||
if (hasDeprivation) navItems.push({ id: 'local-area', label: 'Local Area' });
|
if (hasDeprivation) navItems.push({ id: 'local-area', label: 'Local Area' });
|
||||||
if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' });
|
if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' });
|
||||||
if (yearlyData.length > 0) navItems.push({ id: 'history', label: 'History' });
|
|
||||||
|
|
||||||
// Track active section as user scrolls
|
// Track active section as user scrolls
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -670,42 +673,6 @@ export function SchoolDetailView({
|
|||||||
</section>
|
</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) */}
|
{/* Results Section (SATs for primary, GCSEs for secondary) */}
|
||||||
{hasAnyResults && latestResults && (
|
{hasAnyResults && latestResults && (
|
||||||
<section id="results" className={styles.card}>
|
<section id="results" className={styles.card}>
|
||||||
@@ -950,69 +917,6 @@ export function SchoolDetailView({
|
|||||||
</section>
|
</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 */}
|
{/* How Hard to Get In */}
|
||||||
{admissions && (
|
{admissions && (
|
||||||
<section id="admissions" className={styles.card}>
|
<section id="admissions" className={styles.card}>
|
||||||
@@ -1200,73 +1104,6 @@ export function SchoolDetailView({
|
|||||||
</section>
|
</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) */}
|
{/* Results Over Time (merged: chart + historical table) */}
|
||||||
{yearlyData.length > 0 && (
|
{yearlyData.length > 0 && (
|
||||||
<section id="history" className={styles.card}>
|
<section id="history" className={styles.card}>
|
||||||
@@ -1336,6 +1173,172 @@ export function SchoolDetailView({
|
|||||||
)}
|
)}
|
||||||
</section>
|
</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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,16 +134,19 @@ export function SecondarySchoolDetailView({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [schoolInfo.urn]);
|
}, [schoolInfo.urn]);
|
||||||
|
|
||||||
// Build nav items dynamically based on available data
|
// Build nav items dynamically based on available data.
|
||||||
|
// Engagement-led order (matches the primary page): recognised Ofsted badge,
|
||||||
|
// then the most-sought sections — results, admissions, history — with the
|
||||||
|
// experience and context sections following.
|
||||||
const navItems: { id: string; label: string }[] = [];
|
const navItems: { id: string; label: string }[] = [];
|
||||||
if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' });
|
if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' });
|
||||||
if (hasParents) navItems.push({ id: 'parents', label: 'Parents' });
|
|
||||||
if (hasResults) navItems.push({ id: 'gcse', label: 'GCSEs' });
|
if (hasResults) navItems.push({ id: 'gcse', label: 'GCSEs' });
|
||||||
if (admissions) navItems.push({ id: 'admissions', label: 'Admissions' });
|
if (admissions) navItems.push({ id: 'admissions', label: 'Admissions' });
|
||||||
|
if (yearlyData.length > 1) navItems.push({ id: 'history', label: 'History' });
|
||||||
|
if (hasParents) navItems.push({ id: 'parents', label: 'Parents' });
|
||||||
if (hasWellbeing) navItems.push({ id: 'wellbeing', label: 'Wellbeing' });
|
if (hasWellbeing) navItems.push({ id: 'wellbeing', label: 'Wellbeing' });
|
||||||
if (hasLocation) navItems.push({ id: 'location', label: 'Location' });
|
if (hasLocation) navItems.push({ id: 'location', label: 'Location' });
|
||||||
if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' });
|
if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' });
|
||||||
if (yearlyData.length > 1) navItems.push({ id: 'history', label: 'History' });
|
|
||||||
|
|
||||||
// Track active section as user scrolls
|
// Track active section as user scrolls
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -469,42 +472,6 @@ export function SecondarySchoolDetailView({
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Parent View ────────────────────────────────── */}
|
|
||||||
{hasParents && parentView && (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ── GCSE Results ───────────────────────────────── */}
|
{/* ── GCSE Results ───────────────────────────────── */}
|
||||||
{hasResults && latestResults && (
|
{hasResults && latestResults && (
|
||||||
<section id="gcse" className={styles.card}>
|
<section id="gcse" className={styles.card}>
|
||||||
@@ -790,6 +757,89 @@ export function SecondarySchoolDetailView({
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ── History table ──────────────────────────────── */}
|
||||||
|
{yearlyData.length > 1 && (
|
||||||
|
<section id="history" className={styles.card}>
|
||||||
|
<h2 className={styles.sectionTitle}>Historical Results</h2>
|
||||||
|
{yearlyData.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h3 className={styles.subSectionTitle} style={{ marginTop: '1.25rem' }}>Results Over Time</h3>
|
||||||
|
<div className={styles.chartContainer}>
|
||||||
|
<PerformanceChart
|
||||||
|
data={yearlyData}
|
||||||
|
schoolName={schoolInfo.school_name}
|
||||||
|
isSecondary={true}
|
||||||
|
nationalAtt8Avg={heroAtt8Nat}
|
||||||
|
nationalByYear={nationalAvg?.by_year}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<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>
|
||||||
|
<th>Attainment 8</th>
|
||||||
|
<th>Progress 8</th>
|
||||||
|
<th>Eng & Maths 4+</th>
|
||||||
|
<th>EBacc entry %</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{yearlyData.map((result) => (
|
||||||
|
<tr key={result.year}>
|
||||||
|
<td className={styles.yearCell}>{formatAcademicYear(result.year)}</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>
|
||||||
|
<td>{result.ebacc_entry_pct != null ? formatPercentage(result.ebacc_entry_pct) : '-'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{/* ── Parent View ────────────────────────────────── */}
|
||||||
|
{hasParents && parentView && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ── Wellbeing ──────────────────────────────────── */}
|
{/* ── Wellbeing ──────────────────────────────────── */}
|
||||||
{hasWellbeing && (
|
{hasWellbeing && (
|
||||||
<section id="wellbeing" className={styles.card}>
|
<section id="wellbeing" className={styles.card}>
|
||||||
@@ -936,53 +986,6 @@ export function SecondarySchoolDetailView({
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── History table ──────────────────────────────── */}
|
|
||||||
{yearlyData.length > 1 && (
|
|
||||||
<section id="history" className={styles.card}>
|
|
||||||
<h2 className={styles.sectionTitle}>Historical Results</h2>
|
|
||||||
{yearlyData.length > 0 && (
|
|
||||||
<>
|
|
||||||
<h3 className={styles.subSectionTitle} style={{ marginTop: '1.25rem' }}>Results Over Time</h3>
|
|
||||||
<div className={styles.chartContainer}>
|
|
||||||
<PerformanceChart
|
|
||||||
data={yearlyData}
|
|
||||||
schoolName={schoolInfo.school_name}
|
|
||||||
isSecondary={true}
|
|
||||||
nationalAtt8Avg={heroAtt8Nat}
|
|
||||||
nationalByYear={nationalAvg?.by_year}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<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>
|
|
||||||
<th>Attainment 8</th>
|
|
||||||
<th>Progress 8</th>
|
|
||||||
<th>Eng & Maths 4+</th>
|
|
||||||
<th>EBacc entry %</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{yearlyData.map((result) => (
|
|
||||||
<tr key={result.year}>
|
|
||||||
<td className={styles.yearCell}>{formatAcademicYear(result.year)}</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>
|
|
||||||
<td>{result.ebacc_entry_pct != null ? formatPercentage(result.ebacc_entry_pct) : '-'}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user