PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m4s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 17s
PR Checks / Build Frontend (no push) (pull_request) Successful in 46s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m53s
M1: admissions rounds are now selected by the active phase tab (admissionsForPhase) — an all-through school's Year 7 round no longer masquerades as Reception odds beside pure primaries; honest per-cell and section fallbacks name the round (Reception / Year 7). M2: Ofsted sentinel codes (9 = not applicable) never render as judgement chips, and the sixth-form judgement — previously dropped — now renders for schools that have one. M3: 'What this means' is phase- and type-aware: selective schools get entrance-test framing, secondary faith schools a faith-criteria note, and the primaries' distance template never appears on the secondary tab (admissions_policy now exposed in compare school_info). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
155 lines
6.2 KiB
TypeScript
155 lines
6.2 KiB
TypeScript
/**
|
||
* 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}). "First choice" means
|
||
families who ranked the school top of their application form — officially a "first
|
||
preference". Schools never see your ranking: places are decided only by the
|
||
school'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'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 don’t 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 school’s admission criteria (faith-based criteria may apply).'
|
||
: 'More first-choice applications than places — check the school’s admission criteria (catchment or distance often decides, but criteria vary).'
|
||
: 'More first-choice applications than places — check the school’s 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>
|
||
);
|
||
}
|