fix(compare): expert sign-off must-fixes — phase-matched admissions, Ofsted sentinel codes, selective-school copy
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
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
This commit is contained in:
@@ -422,7 +422,11 @@ export function ComparisonView({
|
||||
benchmarks={benchmarks}
|
||||
isSecondary={!isPrimary}
|
||||
/>
|
||||
<CompareAdmissions schools={activeSchools} data={activeComparisonData} />
|
||||
<CompareAdmissions
|
||||
schools={activeSchools}
|
||||
data={activeComparisonData}
|
||||
isSecondary={!isPrimary}
|
||||
/>
|
||||
<CompareCommunity
|
||||
schools={activeSchools}
|
||||
data={activeComparisonData}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { summariseAdmissions } from '@/lib/compareLogic';
|
||||
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';
|
||||
@@ -15,11 +15,17 @@ import { Cell, Chip, Measure, Section, SectionGrid, sectionStyles as s } from '.
|
||||
export function CompareAdmissions({
|
||||
schools,
|
||||
data,
|
||||
isSecondary = false,
|
||||
}: {
|
||||
schools: School[];
|
||||
data: Record<string, ComparisonData>;
|
||||
isSecondary?: boolean;
|
||||
}) {
|
||||
const rows = schools.map((school) => data[String(school.urn)]?.admissions ?? null);
|
||||
// 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
|
||||
@@ -28,7 +34,10 @@ export function CompareAdmissions({
|
||||
|
||||
if (!anyData) {
|
||||
return (
|
||||
<Section title="Getting a place" how="No admissions data is available for these schools yet.">
|
||||
<Section
|
||||
title="Getting a place"
|
||||
how={`No ${roundLabel} admissions data is available for these schools yet.`}
|
||||
>
|
||||
<></>
|
||||
</Section>
|
||||
);
|
||||
@@ -63,7 +72,9 @@ export function CompareAdmissions({
|
||||
<strong>{a.places_offered.toLocaleString('en-GB')}</strong> places
|
||||
</>
|
||||
) : (
|
||||
<span className={s.small}>No data</span>
|
||||
<span className={s.small}>
|
||||
We don't hold {roundLabel} admissions data for this school
|
||||
</span>
|
||||
)}
|
||||
</Cell>
|
||||
);
|
||||
@@ -104,15 +115,28 @@ export function CompareAdmissions({
|
||||
{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 (summary.firstPrefPct >= 100) {
|
||||
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 =
|
||||
'More first-choice applications than places — check the school’s admission criteria (for most non-faith primaries, distance decides).';
|
||||
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.`;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
admissionsForPhase,
|
||||
latestValues,
|
||||
ofstedDisplay,
|
||||
summariseAdmissions,
|
||||
@@ -145,7 +146,11 @@ export function CompareAtAGlance({
|
||||
|
||||
<Measure label="Getting a place">
|
||||
{schools.map((school, i) => {
|
||||
const summary = summariseAdmissions(data[String(school.urn)]?.admissions);
|
||||
// Phase-matched round only — an all-through school's Year 7 round
|
||||
// must not masquerade as Reception odds on the primary tab.
|
||||
const summary = summariseAdmissions(
|
||||
admissionsForPhase(data[String(school.urn)], isSecondary),
|
||||
);
|
||||
return (
|
||||
<Cell key={school.urn} school={school} index={i}>
|
||||
{summary.chip ? (
|
||||
|
||||
@@ -108,14 +108,21 @@ function JudgementDetailCell({
|
||||
);
|
||||
}
|
||||
|
||||
const legacyAreas: Array<[string, number | null]> = [
|
||||
const legacyAreas: Array<[string, number | null | undefined]> = [
|
||||
['Quality of education', ofsted.quality_of_education],
|
||||
['Behaviour & attitudes', ofsted.behaviour_attitudes],
|
||||
['Personal development', ofsted.personal_development],
|
||||
['Leadership & management', ofsted.leadership_management],
|
||||
['Early years provision', ofsted.early_years_provision],
|
||||
['Sixth form provision', ofsted.sixth_form_provision],
|
||||
];
|
||||
const published = legacyAreas.filter(([, grade]) => grade != null);
|
||||
// Only real Ofsted grades (1–4) are judgements. The MI file uses sentinel
|
||||
// codes for "not applicable / no judgement" (9, and 0/8 variants) — those
|
||||
// must never render as a rating chip.
|
||||
const published = legacyAreas.filter(
|
||||
(entry): entry is [string, number] =>
|
||||
entry[1] != null && entry[1] >= 1 && entry[1] <= 4,
|
||||
);
|
||||
|
||||
if (published.length === 0) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user