feat(compare): at-a-glance, Ofsted, admissions and community sections
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Who goes there — the school's community from the latest census plus GIAS
|
||||
* facts. Benchmark chips use the computed state-school averages and must
|
||||
* carry their provenance wording (never "England average" for computed
|
||||
* figures). Copy verbatim from the reviewed mockups.
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { verdict } from '@/lib/compareLogic';
|
||||
import type { Benchmarks, ComparisonData, School } from '@/lib/types';
|
||||
import { Cell, Chip, RowLabel, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||
|
||||
function pctSplit(part: number | null | undefined, total: number | null | undefined): string | null {
|
||||
if (part == null || total == null || total === 0) return null;
|
||||
return `${Math.round((part / total) * 100)}%`;
|
||||
}
|
||||
|
||||
export function CompareCommunity({
|
||||
schools,
|
||||
data,
|
||||
benchmarks,
|
||||
}: {
|
||||
schools: School[];
|
||||
data: Record<string, ComparisonData>;
|
||||
benchmarks?: Benchmarks;
|
||||
}) {
|
||||
const isSecondary = schools.some(
|
||||
(school) => data[String(school.urn)]?.school_info?.attainment_8_score != null,
|
||||
);
|
||||
const bench = isSecondary ? benchmarks?.secondary : benchmarks?.primary;
|
||||
|
||||
const fsmChip = (value: number | null) => {
|
||||
if (value == null || bench?.disadvantaged_pct == null) return null;
|
||||
const v = verdict(value, bench.disadvantaged_pct, 3);
|
||||
return (
|
||||
<Chip tone="neutral">
|
||||
{v === 'above' && 'Above the state-school average'}
|
||||
{v === 'close' && 'About the state-school average'}
|
||||
{v === 'below' && 'Below the state-school average'}
|
||||
</Chip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Section
|
||||
title="Who goes there"
|
||||
how="The school's community, from the latest school census. State-school averages are computed from our dataset and shown for context — there's no “right” number here."
|
||||
>
|
||||
<SectionGrid schools={schools}>
|
||||
<RowLabel>Pupils on roll</RowLabel>
|
||||
{schools.map((school, i) => {
|
||||
const info = data[String(school.urn)]?.school_info as (School & { gias_total_pupils?: number | null; capacity?: number | null }) | undefined;
|
||||
const census = data[String(school.urn)]?.census;
|
||||
const pupils = census?.total_pupils ?? info?.gias_total_pupils ?? null;
|
||||
const capacity = info?.capacity ?? null;
|
||||
let capNote: string | null = null;
|
||||
if (pupils != null && capacity != null && capacity > 0) {
|
||||
capNote =
|
||||
pupils >= capacity
|
||||
? `${capacity.toLocaleString('en-GB')} places — at or above capacity`
|
||||
: `of ${capacity.toLocaleString('en-GB')} places (${Math.round((pupils / capacity) * 100)}% full)`;
|
||||
}
|
||||
return (
|
||||
<Cell key={school.urn} school={school} index={i}>
|
||||
{pupils != null ? (
|
||||
<>
|
||||
{pupils.toLocaleString('en-GB')}
|
||||
{capNote && <span className={s.small}>{capNote}</span>}
|
||||
</>
|
||||
) : (
|
||||
<span className={s.small}>No data</span>
|
||||
)}
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
|
||||
<RowLabel>Girls / boys</RowLabel>
|
||||
{schools.map((school, i) => {
|
||||
const census = data[String(school.urn)]?.census;
|
||||
const girls = pctSplit(census?.female_pupils, census?.total_pupils);
|
||||
const boys = pctSplit(census?.male_pupils, census?.total_pupils);
|
||||
return (
|
||||
<Cell key={school.urn} school={school} index={i}>
|
||||
{girls && boys ? `${girls} / ${boys}` : <span className={s.small}>No data</span>}
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
|
||||
<RowLabel tip="% of pupils eligible for free school meals — a common measure of how many pupils come from lower-income families. Benchmark computed across state schools in our dataset.">
|
||||
Free school meals
|
||||
</RowLabel>
|
||||
{schools.map((school, i) => {
|
||||
const fsm = data[String(school.urn)]?.census?.fsm_pct ?? null;
|
||||
return (
|
||||
<Cell key={school.urn} school={school} index={i}>
|
||||
{fsm != null ? (
|
||||
<>
|
||||
{Math.round(fsm)}% {fsmChip(fsm)}
|
||||
</>
|
||||
) : (
|
||||
<span className={s.small}>No data</span>
|
||||
)}
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
|
||||
<RowLabel tip="% of pupils whose first language is known or believed to be other than English. State-school average computed from our dataset.">
|
||||
English as an additional language
|
||||
</RowLabel>
|
||||
{schools.map((school, i) => {
|
||||
const eal = data[String(school.urn)]?.census?.eal_pct ?? null;
|
||||
return (
|
||||
<Cell key={school.urn} school={school} index={i}>
|
||||
{eal != null ? `${Math.round(eal)}%` : <span className={s.small}>No data</span>}
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
|
||||
<RowLabel tip="% of pupils receiving SEN support (not including EHC plans). A high figure can mean the school hosts specialist provision — often a strength, not a warning sign. State-school average computed from our dataset.">
|
||||
Extra learning support (SEN)
|
||||
</RowLabel>
|
||||
{schools.map((school, i) => {
|
||||
const rows = data[String(school.urn)]?.yearly_data ?? [];
|
||||
let sen: number | null = null;
|
||||
for (let r = rows.length - 1; r >= 0; r--) {
|
||||
if (rows[r].sen_support_pct != null) {
|
||||
sen = rows[r].sen_support_pct;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const high =
|
||||
sen != null && bench?.sen_support_pct != null && sen >= bench.sen_support_pct * 1.75;
|
||||
return (
|
||||
<Cell key={school.urn} school={school} index={i}>
|
||||
{sen != null ? (
|
||||
<>
|
||||
{Math.round(sen)}% {high && <Chip tone="neutral">Well above average</Chip>}
|
||||
</>
|
||||
) : (
|
||||
<span className={s.small}>No data</span>
|
||||
)}
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
|
||||
<RowLabel>Faith character</RowLabel>
|
||||
{schools.map((school, i) => {
|
||||
const info = data[String(school.urn)]?.school_info;
|
||||
const faith = info?.religious_denomination;
|
||||
const none = !faith || faith === 'Does not apply' || faith === 'None';
|
||||
return (
|
||||
<Cell key={school.urn} school={school} index={i}>
|
||||
{none ? 'None' : faith}
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
|
||||
<RowLabel>Ages</RowLabel>
|
||||
{schools.map((school, i) => {
|
||||
const info = data[String(school.urn)]?.school_info;
|
||||
return (
|
||||
<Cell key={school.urn} school={school} index={i}>
|
||||
{info?.age_range || <span className={s.small}>No data</span>}
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
|
||||
<RowLabel>Run by</RowLabel>
|
||||
{schools.map((school, i) => {
|
||||
const info = data[String(school.urn)]?.school_info;
|
||||
const trust = info?.trust_name;
|
||||
const la = info?.local_authority ?? school.local_authority;
|
||||
return (
|
||||
<Cell key={school.urn} school={school} index={i}>
|
||||
{trust ? trust : la ? `${la} council` : <span className={s.small}>No data</span>}
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</SectionGrid>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user