feat(school-detail): page-wide improvements across 5 sections
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 12s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 49s
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 12s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 49s
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
History chart
- Strip the redundant school-name title (already the page heading)
- Default to 2 visible lines: Reading, Writing & Maths expected % (teal,
bold) + Exceeding (gold, lighter); progress score lines hidden by
default, togglable via legend
- Add dashed national average reference line for RWM (primary) or
Attainment 8 (secondary) so the school's trajectory is always in
context
- Add trend summary chip above the chart computed from the data
("↓ Peaked at 90% (2016/17), currently 70%")
- Add COVID footnote when 2019/20 and 2020/21 data is absent
Ofsted section
- Collapse the four identical "Outstanding / Outstanding / Outstanding /
Outstanding" boxes into a single prose line when all sub-grades match
the overall verdict; show individual cards only when grades differ
SATs sub-metrics
- DeltaChip vs national average on Expected level row for Reading,
Writing and Maths (national averages already in the API response)
Admissions
- Fix label: "Year 3 places per year" → "Reception places per year" for
primary schools
Pupils & Inclusion
- DeltaChip + national avg hint on Eligible for pupil premium and
Pupils receiving SEN support (both keys present in /api/national-averages)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,34 @@
|
|||||||
|
.chartOuter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trendSummary {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary, #5c564d);
|
||||||
|
padding: 0.5rem 0.875rem;
|
||||||
|
background: var(--bg-secondary, #f3ede4);
|
||||||
|
border-left: 3px solid var(--accent-teal, #2d7d7d);
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.chartWrapper {
|
.chartWrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.covidNote,
|
||||||
|
.chartHint {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted, #8a847a);
|
||||||
|
margin: 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.chartWrapper {
|
.chartWrapper {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
|||||||
@@ -16,217 +16,257 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Legend,
|
Legend,
|
||||||
ChartOptions,
|
ChartOptions,
|
||||||
|
ChartDataset,
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import type { SchoolResult } from '@/lib/types';
|
import type { SchoolResult } from '@/lib/types';
|
||||||
import { formatAcademicYear } from '@/lib/utils';
|
import { formatAcademicYear } from '@/lib/utils';
|
||||||
import styles from './PerformanceChart.module.css';
|
import styles from './PerformanceChart.module.css';
|
||||||
|
|
||||||
// Register Chart.js components
|
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
||||||
ChartJS.register(
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend
|
|
||||||
);
|
|
||||||
|
|
||||||
interface PerformanceChartProps {
|
interface PerformanceChartProps {
|
||||||
data: SchoolResult[];
|
data: SchoolResult[];
|
||||||
schoolName: string;
|
schoolName: string;
|
||||||
isSecondary?: boolean;
|
isSecondary?: boolean;
|
||||||
|
/** National average RWM expected % — rendered as a dashed reference line */
|
||||||
|
nationalRwmAvg?: number | null;
|
||||||
|
/** National average Attainment 8 — rendered as a dashed reference line */
|
||||||
|
nationalAtt8Avg?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PerformanceChart({ data, schoolName, isSecondary = false }: PerformanceChartProps) {
|
// Academic years when SATs/GCSEs were cancelled due to COVID
|
||||||
// Sort data by year
|
const COVID_YEARS = new Set([201920, 202021]);
|
||||||
|
|
||||||
|
export function PerformanceChart({
|
||||||
|
data,
|
||||||
|
isSecondary = false,
|
||||||
|
nationalRwmAvg,
|
||||||
|
nationalAtt8Avg,
|
||||||
|
}: PerformanceChartProps) {
|
||||||
const sortedData = [...data].sort((a, b) => a.year - b.year);
|
const sortedData = [...data].sort((a, b) => a.year - b.year);
|
||||||
const years = sortedData.map(d => formatAcademicYear(d.year));
|
const years = sortedData.map(d => formatAcademicYear(d.year));
|
||||||
|
|
||||||
// Prepare datasets — phase-aware
|
// ── Trend summary (primary only) ──────────────────────────────────────
|
||||||
const datasets = isSecondary ? [
|
const trendSummary = (() => {
|
||||||
|
if (isSecondary) return null;
|
||||||
|
const rwm = sortedData.filter(d => d.rwm_expected_pct != null);
|
||||||
|
if (rwm.length < 2) return null;
|
||||||
|
const latest = rwm[rwm.length - 1];
|
||||||
|
const prev = rwm[rwm.length - 2];
|
||||||
|
const best = rwm.reduce((a, b) => (b.rwm_expected_pct! > a.rwm_expected_pct! ? b : a));
|
||||||
|
const latestPct = Math.round(latest.rwm_expected_pct!);
|
||||||
|
const bestPct = Math.round(best.rwm_expected_pct!);
|
||||||
|
const delta = latest.rwm_expected_pct! - prev.rwm_expected_pct!;
|
||||||
|
const arrow = delta > 1 ? '↑' : delta < -1 ? '↓' : '→';
|
||||||
|
if (best.year === latest.year) {
|
||||||
|
return `${arrow} Best year on record — ${latestPct}% Reading, Writing & Maths`;
|
||||||
|
}
|
||||||
|
return `${arrow} Peaked at ${bestPct}% (${formatAcademicYear(best.year)}), currently ${latestPct}%`;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ── COVID gap note ─────────────────────────────────────────────────────
|
||||||
|
const hasCovidGap = isSecondary
|
||||||
|
? false
|
||||||
|
: COVID_YEARS.size > 0 &&
|
||||||
|
[...COVID_YEARS].some(y => !sortedData.find(d => d.year === y));
|
||||||
|
|
||||||
|
// ── Datasets ──────────────────────────────────────────────────────────
|
||||||
|
const refLineStyle = {
|
||||||
|
borderColor: 'rgba(90,80,70,0.35)',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderDash: [6, 4] as number[],
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0,
|
||||||
|
order: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const datasets: ChartDataset<'line'>[] = isSecondary ? [
|
||||||
{
|
{
|
||||||
label: 'Attainment 8',
|
label: 'Attainment 8',
|
||||||
data: sortedData.map(d => d.attainment_8_score),
|
data: sortedData.map(d => d.attainment_8_score),
|
||||||
borderColor: 'rgb(59, 130, 246)',
|
borderColor: '#2d7d7d',
|
||||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
backgroundColor: 'rgba(45,125,125,0.08)',
|
||||||
|
borderWidth: 2.5,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
|
pointRadius: 4,
|
||||||
|
pointHoverRadius: 6,
|
||||||
yAxisID: 'y',
|
yAxisID: 'y',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'English & Maths Grade 4+',
|
label: 'English & Maths Grade 4+',
|
||||||
data: sortedData.map(d => d.english_maths_standard_pass_pct),
|
data: sortedData.map(d => d.english_maths_standard_pass_pct),
|
||||||
borderColor: 'rgb(16, 185, 129)',
|
borderColor: '#c9a227',
|
||||||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
backgroundColor: 'rgba(201,162,39,0.08)',
|
||||||
|
borderWidth: 1.5,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
|
pointRadius: 3,
|
||||||
yAxisID: 'y',
|
yAxisID: 'y',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Progress 8',
|
label: 'Progress 8',
|
||||||
data: sortedData.map(d => d.progress_8_score),
|
data: sortedData.map(d => d.progress_8_score),
|
||||||
borderColor: 'rgb(245, 158, 11)',
|
borderColor: 'rgb(139,92,246)',
|
||||||
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
backgroundColor: 'rgba(139,92,246,0.08)',
|
||||||
|
borderWidth: 1.5,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
|
pointRadius: 3,
|
||||||
|
hidden: true,
|
||||||
yAxisID: 'y1',
|
yAxisID: 'y1',
|
||||||
},
|
},
|
||||||
|
...(nationalAtt8Avg != null ? [{
|
||||||
|
...refLineStyle,
|
||||||
|
label: 'National average',
|
||||||
|
data: sortedData.map(() => nationalAtt8Avg!),
|
||||||
|
yAxisID: 'y',
|
||||||
|
} as ChartDataset<'line'>] : []),
|
||||||
] : [
|
] : [
|
||||||
{
|
{
|
||||||
label: 'Reading, Writing & Maths Expected %',
|
label: 'Reading, Writing & Maths expected %',
|
||||||
data: sortedData.map(d => d.rwm_expected_pct),
|
data: sortedData.map(d => d.rwm_expected_pct),
|
||||||
borderColor: 'rgb(59, 130, 246)',
|
borderColor: '#2d7d7d',
|
||||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
backgroundColor: 'rgba(45,125,125,0.08)',
|
||||||
|
borderWidth: 2.5,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
|
pointRadius: 4,
|
||||||
|
pointHoverRadius: 6,
|
||||||
|
yAxisID: 'y',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Reading, Writing & Maths Higher %',
|
label: 'Exceeding expected level',
|
||||||
data: sortedData.map(d => d.rwm_high_pct),
|
data: sortedData.map(d => d.rwm_high_pct),
|
||||||
borderColor: 'rgb(16, 185, 129)',
|
borderColor: '#c9a227',
|
||||||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
backgroundColor: 'rgba(201,162,39,0.08)',
|
||||||
|
borderWidth: 1.5,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
|
pointRadius: 3,
|
||||||
|
yAxisID: 'y',
|
||||||
},
|
},
|
||||||
|
...(nationalRwmAvg != null ? [{
|
||||||
|
...refLineStyle,
|
||||||
|
label: 'National average',
|
||||||
|
data: sortedData.map(() => nationalRwmAvg!),
|
||||||
|
yAxisID: 'y',
|
||||||
|
} as ChartDataset<'line'>] : []),
|
||||||
{
|
{
|
||||||
label: 'Reading Progress',
|
label: 'Reading progress',
|
||||||
data: sortedData.map(d => d.reading_progress),
|
data: sortedData.map(d => d.reading_progress),
|
||||||
borderColor: 'rgb(245, 158, 11)',
|
borderColor: 'rgb(59,130,246)',
|
||||||
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
backgroundColor: 'rgba(59,130,246,0.08)',
|
||||||
|
borderWidth: 1.5,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
|
pointRadius: 3,
|
||||||
|
hidden: true,
|
||||||
yAxisID: 'y1',
|
yAxisID: 'y1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Writing Progress',
|
label: 'Writing progress',
|
||||||
data: sortedData.map(d => d.writing_progress),
|
data: sortedData.map(d => d.writing_progress),
|
||||||
borderColor: 'rgb(139, 92, 246)',
|
borderColor: 'rgb(139,92,246)',
|
||||||
backgroundColor: 'rgba(139, 92, 246, 0.1)',
|
backgroundColor: 'rgba(139,92,246,0.08)',
|
||||||
|
borderWidth: 1.5,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
|
pointRadius: 3,
|
||||||
|
hidden: true,
|
||||||
yAxisID: 'y1',
|
yAxisID: 'y1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Maths Progress',
|
label: 'Maths progress',
|
||||||
data: sortedData.map(d => d.maths_progress),
|
data: sortedData.map(d => d.maths_progress),
|
||||||
borderColor: 'rgb(236, 72, 153)',
|
borderColor: 'rgb(236,72,153)',
|
||||||
backgroundColor: 'rgba(236, 72, 153, 0.1)',
|
backgroundColor: 'rgba(236,72,153,0.08)',
|
||||||
|
borderWidth: 1.5,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
|
pointRadius: 3,
|
||||||
|
hidden: true,
|
||||||
yAxisID: 'y1',
|
yAxisID: 'y1',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const chartData = {
|
|
||||||
labels: years,
|
|
||||||
datasets,
|
|
||||||
};
|
|
||||||
|
|
||||||
const options: ChartOptions<'line'> = {
|
const options: ChartOptions<'line'> = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
interaction: {
|
interaction: { mode: 'index', intersect: false },
|
||||||
mode: 'index' as const,
|
|
||||||
intersect: false,
|
|
||||||
},
|
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
position: 'top' as const,
|
position: 'top',
|
||||||
labels: {
|
labels: {
|
||||||
usePointStyle: true,
|
usePointStyle: true,
|
||||||
padding: 15,
|
padding: 14,
|
||||||
font: {
|
font: { size: 12 },
|
||||||
size: 12,
|
filter: item => item.text !== 'National average' || true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'rgba(26,22,18,0.92)',
|
||||||
|
padding: 12,
|
||||||
|
titleFont: { size: 13 },
|
||||||
|
bodyFont: { size: 12 },
|
||||||
|
callbacks: {
|
||||||
|
label: ctx => {
|
||||||
|
const label = ctx.dataset.label ?? '';
|
||||||
|
if (ctx.parsed.y == null) return label;
|
||||||
|
const isProgress = ctx.dataset.yAxisID === 'y1';
|
||||||
|
const suffix = isProgress ? '' : '%';
|
||||||
|
const val = ctx.parsed.y.toFixed(1);
|
||||||
|
return `${label}: ${val}${suffix}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: `${schoolName} - Performance Over Time`,
|
|
||||||
font: {
|
|
||||||
size: 16,
|
|
||||||
weight: 'bold',
|
|
||||||
},
|
|
||||||
padding: {
|
|
||||||
bottom: 20,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
||||||
padding: 12,
|
|
||||||
titleFont: {
|
|
||||||
size: 14,
|
|
||||||
},
|
|
||||||
bodyFont: {
|
|
||||||
size: 13,
|
|
||||||
},
|
|
||||||
callbacks: {
|
|
||||||
label: function(context) {
|
|
||||||
let label = context.dataset.label || '';
|
|
||||||
if (label) {
|
|
||||||
label += ': ';
|
|
||||||
}
|
|
||||||
if (context.parsed.y !== null) {
|
|
||||||
if (context.dataset.yAxisID === 'y1') {
|
|
||||||
// Progress scores
|
|
||||||
label += context.parsed.y.toFixed(1);
|
|
||||||
} else {
|
|
||||||
// Percentages
|
|
||||||
label += context.parsed.y.toFixed(1) + '%';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
type: 'linear' as const,
|
type: 'linear',
|
||||||
display: true,
|
display: true,
|
||||||
position: 'left' as const,
|
position: 'left',
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: isSecondary ? 'Score / Percentage (%)' : 'Percentage (%)',
|
text: isSecondary ? 'Score / %' : 'Percentage (%)',
|
||||||
font: {
|
font: { size: 11 },
|
||||||
size: 12,
|
|
||||||
weight: 'bold',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
min: 0,
|
min: 0,
|
||||||
max: isSecondary ? undefined : 100,
|
max: isSecondary ? undefined : 100,
|
||||||
grid: {
|
grid: { color: 'rgba(0,0,0,0.05)' },
|
||||||
color: 'rgba(0, 0, 0, 0.05)',
|
ticks: { font: { size: 11 } },
|
||||||
},
|
|
||||||
},
|
},
|
||||||
y1: {
|
y1: {
|
||||||
type: 'linear' as const,
|
type: 'linear',
|
||||||
display: true,
|
display: true,
|
||||||
position: 'right' as const,
|
position: 'right',
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: isSecondary ? 'Progress 8 Score' : 'Progress Score',
|
text: isSecondary ? 'Progress 8' : 'Progress score',
|
||||||
font: {
|
font: { size: 11 },
|
||||||
size: 12,
|
|
||||||
weight: 'bold',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
drawOnChartArea: false,
|
|
||||||
},
|
},
|
||||||
|
grid: { drawOnChartArea: false },
|
||||||
|
ticks: { font: { size: 11 } },
|
||||||
},
|
},
|
||||||
x: {
|
x: {
|
||||||
grid: {
|
grid: { display: false },
|
||||||
display: false,
|
ticks: { font: { size: 11 } },
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Year',
|
|
||||||
font: {
|
|
||||||
size: 12,
|
|
||||||
weight: 'bold',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.chartWrapper}>
|
<div className={styles.chartOuter}>
|
||||||
<Line data={chartData} options={options} />
|
{trendSummary && (
|
||||||
|
<div className={styles.trendSummary}>{trendSummary}</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.chartWrapper}>
|
||||||
|
<Line data={{ labels: years, datasets }} options={options} />
|
||||||
|
</div>
|
||||||
|
{hasCovidGap && (
|
||||||
|
<p className={styles.covidNote}>
|
||||||
|
* No data for 2019/20 or 2020/21 — national assessments were cancelled due to COVID-19.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!isSecondary && (
|
||||||
|
<p className={styles.chartHint}>
|
||||||
|
Progress scores (Reading, Writing, Maths) are hidden by default — click them in the legend to show.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,6 +635,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Progress note */
|
/* Progress note */
|
||||||
|
.ofstedAllSame {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: var(--text-secondary, #5c564d);
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ofstedAllSame strong {
|
||||||
|
color: var(--text-primary, #1a1612);
|
||||||
|
}
|
||||||
|
|
||||||
.progressNote {
|
.progressNote {
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
|||||||
@@ -138,6 +138,19 @@ export function SchoolDetailView({
|
|||||||
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' });
|
if (yearlyData.length > 0) navItems.push({ id: 'history', label: 'History' });
|
||||||
|
|
||||||
|
// ── 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 ─────────────
|
// ── Hero: framework-aware signal chip + narrative summary ─────────────
|
||||||
const ofstedHeroChip = buildOfstedHeroChip(ofsted);
|
const ofstedHeroChip = buildOfstedHeroChip(ofsted);
|
||||||
const heroSummary = buildSchoolSummary(schoolInfo, ofsted, admissions, latestResults);
|
const heroSummary = buildSchoolSummary(schoolInfo, ofsted, admissions, latestResults);
|
||||||
@@ -385,24 +398,30 @@ export function SchoolDetailView({
|
|||||||
<strong>{Math.round(parentView.q_recommend_pct)}%</strong> of parents would recommend this school ({parentView.total_responses.toLocaleString()} responses)
|
<strong>{Math.round(parentView.q_recommend_pct)}%</strong> of parents would recommend this school ({parentView.total_responses.toLocaleString()} responses)
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className={styles.metricsGrid}>
|
{oeifAllSameGrade ? (
|
||||||
{[
|
<p className={styles.ofstedAllSame}>
|
||||||
{ label: 'Quality of Teaching', value: ofsted.quality_of_education },
|
Rated <strong>{OFSTED_LABELS[ofsted.overall_effectiveness!]}</strong> across all inspected areas — Quality of Teaching, Behaviour, Pupils' Development and Leadership.
|
||||||
{ label: 'Behaviour in School', value: ofsted.behaviour_attitudes },
|
</p>
|
||||||
{ label: 'Pupils\' Wider Development', value: ofsted.personal_development },
|
) : (
|
||||||
{ label: 'School Leadership', value: ofsted.leadership_management },
|
<div className={styles.metricsGrid}>
|
||||||
...(ofsted.early_years_provision != null
|
{[
|
||||||
? [{ label: 'Early Years (Reception)', value: ofsted.early_years_provision }]
|
{ label: 'Quality of Teaching', value: ofsted.quality_of_education },
|
||||||
: []),
|
{ label: 'Behaviour in School', value: ofsted.behaviour_attitudes },
|
||||||
].map(({ label, value }) => value != null && (
|
{ label: 'Pupils\' Wider Development', value: ofsted.personal_development },
|
||||||
<div key={label} className={styles.metricCard}>
|
{ label: 'School Leadership', value: ofsted.leadership_management },
|
||||||
<div className={styles.metricLabel}>{label}</div>
|
...(ofsted.early_years_provision != null
|
||||||
<div className={`${styles.metricValue} ${styles[`ofstedGrade${value}`]}`}>
|
? [{ label: 'Early Years (Reception)', value: ofsted.early_years_provision }]
|
||||||
{OFSTED_LABELS[value]}
|
: []),
|
||||||
|
].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>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
@@ -513,7 +532,12 @@ export function SchoolDetailView({
|
|||||||
{latestResults.reading_expected_pct !== null && (
|
{latestResults.reading_expected_pct !== null && (
|
||||||
<div className={styles.metricRow}>
|
<div className={styles.metricRow}>
|
||||||
<span className={styles.metricName}>Expected level</span>
|
<span className={styles.metricName}>Expected level</span>
|
||||||
<span className={styles.metricValue}>{formatPercentage(latestResults.reading_expected_pct)}</span>
|
<span className={styles.metricValue}>
|
||||||
|
{formatPercentage(latestResults.reading_expected_pct)}
|
||||||
|
{primaryAvg.reading_expected_pct != null && (
|
||||||
|
<DeltaChip value={latestResults.reading_expected_pct} baseline={primaryAvg.reading_expected_pct} unit="pts" size="sm" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{latestResults.reading_high_pct !== null && (
|
{latestResults.reading_high_pct !== null && (
|
||||||
@@ -551,7 +575,12 @@ export function SchoolDetailView({
|
|||||||
{latestResults.writing_expected_pct !== null && (
|
{latestResults.writing_expected_pct !== null && (
|
||||||
<div className={styles.metricRow}>
|
<div className={styles.metricRow}>
|
||||||
<span className={styles.metricName}>Expected level</span>
|
<span className={styles.metricName}>Expected level</span>
|
||||||
<span className={styles.metricValue}>{formatPercentage(latestResults.writing_expected_pct)}</span>
|
<span className={styles.metricValue}>
|
||||||
|
{formatPercentage(latestResults.writing_expected_pct)}
|
||||||
|
{primaryAvg.writing_expected_pct != null && (
|
||||||
|
<DeltaChip value={latestResults.writing_expected_pct} baseline={primaryAvg.writing_expected_pct} unit="pts" size="sm" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{latestResults.writing_high_pct !== null && (
|
{latestResults.writing_high_pct !== null && (
|
||||||
@@ -580,7 +609,12 @@ export function SchoolDetailView({
|
|||||||
{latestResults.maths_expected_pct !== null && (
|
{latestResults.maths_expected_pct !== null && (
|
||||||
<div className={styles.metricRow}>
|
<div className={styles.metricRow}>
|
||||||
<span className={styles.metricName}>Expected level</span>
|
<span className={styles.metricName}>Expected level</span>
|
||||||
<span className={styles.metricValue}>{formatPercentage(latestResults.maths_expected_pct)}</span>
|
<span className={styles.metricValue}>
|
||||||
|
{formatPercentage(latestResults.maths_expected_pct)}
|
||||||
|
{primaryAvg.maths_expected_pct != null && (
|
||||||
|
<DeltaChip value={latestResults.maths_expected_pct} baseline={primaryAvg.maths_expected_pct} unit="pts" size="sm" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{latestResults.maths_high_pct !== null && (
|
{latestResults.maths_high_pct !== null && (
|
||||||
@@ -792,7 +826,7 @@ export function SchoolDetailView({
|
|||||||
<div className={styles.metricsGrid}>
|
<div className={styles.metricsGrid}>
|
||||||
{admissions.published_admission_number != null && (
|
{admissions.published_admission_number != null && (
|
||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>{isSecondary ? 'Year 7' : 'Year 3'} places per year</div>
|
<div className={styles.metricLabel}>{isSecondary ? 'Year 7' : 'Reception'} places per year</div>
|
||||||
<div className={styles.metricValue}>{admissions.published_admission_number}</div>
|
<div className={styles.metricValue}>{admissions.published_admission_number}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -820,8 +854,13 @@ export function SchoolDetailView({
|
|||||||
{latestResults?.disadvantaged_pct != null && (
|
{latestResults?.disadvantaged_pct != null && (
|
||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>Eligible for pupil premium</div>
|
<div className={styles.metricLabel}>Eligible for pupil premium</div>
|
||||||
<div className={styles.metricValue}>{formatPercentage(latestResults.disadvantaged_pct)}</div>
|
<div className={styles.metricValue}>
|
||||||
<div className={styles.metricHint}>Pupils from disadvantaged backgrounds</div>
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{latestResults?.eal_pct != null && (
|
{latestResults?.eal_pct != null && (
|
||||||
@@ -839,7 +878,15 @@ export function SchoolDetailView({
|
|||||||
Pupils receiving SEN support
|
Pupils receiving SEN support
|
||||||
<MetricTooltip metricKey="sen_support_pct" />
|
<MetricTooltip metricKey="sen_support_pct" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.metricValue}>{formatPercentage(latestResults.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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -945,6 +992,8 @@ export function SchoolDetailView({
|
|||||||
data={yearlyData}
|
data={yearlyData}
|
||||||
schoolName={schoolInfo.school_name}
|
schoolName={schoolInfo.school_name}
|
||||||
isSecondary={isSecondary}
|
isSecondary={isSecondary}
|
||||||
|
nationalRwmAvg={isPrimary ? (primaryAvg.rwm_expected_pct ?? null) : null}
|
||||||
|
nationalAtt8Avg={isSecondary ? (secondaryAvg.attainment_8_score ?? null) : null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{yearlyData.length > 1 && (
|
{yearlyData.length > 1 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user