Files
school_compare/nextjs-app/components/compare/CompareAdmissions.tsx
T

155 lines
6.2 KiB
TypeScript
Raw Normal View History

/**
* Getting a place — admissions framed the way the expert review requires:
* total applications are "named on N forms" (any preference rank, not
* head-to-head), one consistent chip metric (first-preference success),
* equal-preference and offers-vs-intake explanations up front.
*/
'use client';
import { admissionsForPhase, summariseAdmissions } from '@/lib/compareLogic';
import type { ComparisonData, School } from '@/lib/types';
import { CHART_COLORS } from '@/lib/utils';
import { Cell, Chip, Measure, Section, SectionGrid, sectionStyles as s } from './sectionShared';
export function CompareAdmissions({
schools,
data,
isSecondary = false,
}: {
schools: School[];
data: Record<string, ComparisonData>;
isSecondary?: boolean;
}) {
// Admissions rounds are phase-specific: an all-through school's Year 7
// round must never stand in for Reception on the primary tab (and vice
// versa) — beside pure primaries it reads as Reception odds.
const rows = schools.map((school) => admissionsForPhase(data[String(school.urn)], isSecondary));
const roundLabel = isSecondary ? 'Year 7' : 'Reception';
const anyData = rows.some(Boolean);
const entryYear = rows.find(Boolean)?.year;
const entryLabel = entryYear
? `September ${String(entryYear).slice(0, 4)} entry`
: 'the most recent admissions round';
if (!anyData) {
return (
<Section
title="Getting a place"
how={`No ${roundLabel} admissions data is available for these schools yet.`}
>
<></>
</Section>
);
}
return (
<Section
title="Getting a place"
how={
<>
From the most recent admissions round ({entryLabel}). &quot;First choice&quot; means
families who ranked the school top of their application form officially a &quot;first
preference&quot;. Schools never see your ranking: places are decided only by the
school&apos;s admission criteria, so listing a school lower down never hurts your chances.
These are National Offer Day offers waiting lists and appeals can change the final
intake.
</>
}
>
<SectionGrid schools={schools}>
<Measure
tip="How many application forms named the school at any preference rank — not the number of families competing head-to-head for a place."
label="Interest in the school"
>
{schools.map((school, i) => {
const a = rows[i];
return (
<Cell key={school.urn} school={school} index={i}>
{a?.total_applications != null && a?.places_offered != null ? (
<>
Named on <strong>{a.total_applications.toLocaleString('en-GB')}</strong> forms ·{' '}
<strong>{a.places_offered.toLocaleString('en-GB')}</strong> places
</>
) : (
<span className={s.small}>
We don&apos;t hold {roundLabel} admissions data for this school
</span>
)}
</Cell>
);
})}
</Measure>
<Measure label="First-choice families offered a place">
{schools.map((school, i) => {
const summary = summariseAdmissions(rows[i]);
return (
<Cell key={school.urn} school={school} index={i}>
{summary.firstPrefPct != null ? (
<>
<strong>{summary.firstPrefPct}%</strong>{' '}
{summary.chip && summary.chip.tone === 'warn' && (
<Chip tone="warn">{summary.chip.text}</Chip>
)}
<span className={s.barMini}>
<i
style={{
width: `${summary.firstPrefPct}%`,
background: CHART_COLORS[i % CHART_COLORS.length],
}}
/>
</span>
</>
) : (
<span className={s.small}>No data</span>
)}
</Cell>
);
})}
</Measure>
<Measure label="What this means">
{schools.map((school, i) => {
const a = rows[i];
const summary = summariseAdmissions(a);
const info = data[String(school.urn)]?.school_info;
const selective = (info?.admissions_policy ?? '').toLowerCase() === 'selective';
const faith =
!!info?.religious_denomination &&
!/^(none|does not apply|not applicable)$/i.test(info.religious_denomination);
let text: string | null = null;
if (summary.firstPrefPct != null) {
if (selective) {
// Selective schools: the entrance test decides, whatever the
// offer percentage looks like — never the distance template.
text =
'Entry is by entrance test — the school is selective; distance and preference rank dont decide places.';
} else if (summary.firstPrefPct >= 100) {
text = `Every family who put ${school.school_name} first got a place.`;
} else if (summary.firstPrefPct >= 90) {
text = `Nearly every family who put ${school.school_name} first got a place.`;
} else if (a?.oversubscribed) {
text = isSecondary
? faith
? 'More first-choice applications than places — check the schools admission criteria (faith-based criteria may apply).'
: 'More first-choice applications than places — check the schools admission criteria (catchment or distance often decides, but criteria vary).'
: 'More first-choice applications than places — check the schools admission criteria (for most non-faith primaries, distance decides).';
} else {
text = `${summary.firstPrefPct}% of first-choice families received an offer.`;
}
}
return (
<Cell key={school.urn} school={school} index={i}>
{text ? <span className={s.small}>{text}</span> : <span className={s.small}></span>}
</Cell>
);
})}
</Measure>
</SectionGrid>
</Section>
);
}