feat(destinations): types and secondary section flags
Destinations are secondary-only, so the flags go on computeSecondaryFlags rather than computeSchoolFlags. A phase counts as present only when some pupil group carries categories — an empty block would otherwise open a nav entry pointing at a section that never renders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvdDKvFFSZuMVDH5fEyTob
This commit is contained in:
1 parent
c5719ef362
commit
68b6417149
3 files changed
+134
-1
No files matched your search
@@ -10,6 +10,7 @@
|
||||
import type {
|
||||
School, SchoolResult, AbsenceData, SchoolCensus,
|
||||
OfstedInspection, SchoolAdmissions, SchoolAdmissionDistance, SchoolDeprivation, SchoolFinance,
|
||||
SchoolDestinations, DestinationPhase,
|
||||
} from './types';
|
||||
import { isSpecialSchool } from './utils';
|
||||
|
||||
@@ -20,6 +21,7 @@ export interface SchoolFlagsInput {
|
||||
census: SchoolCensus | null;
|
||||
deprivation: SchoolDeprivation | null;
|
||||
finance: SchoolFinance | null;
|
||||
destinations?: SchoolDestinations | null;
|
||||
}
|
||||
|
||||
export interface SchoolFlags {
|
||||
@@ -183,10 +185,22 @@ export interface SecondaryFlags {
|
||||
p8Suspended: boolean;
|
||||
isSpecial: boolean;
|
||||
suppressComparison: boolean;
|
||||
/** Whether a destinations phase has anything to render. A block can exist
|
||||
* with empty groups when the pipeline has run but the school has no rows,
|
||||
* and a nav entry for a section that never renders links to nothing. */
|
||||
hasKs4Destinations: boolean;
|
||||
hasKs5Destinations: boolean;
|
||||
}
|
||||
|
||||
/** A phase is renderable only if some pupil group actually carries categories. */
|
||||
function phaseHasContent(phase: DestinationPhase | null | undefined): boolean {
|
||||
if (!phase) return false;
|
||||
return Object.values(phase.groups ?? {})
|
||||
.some(group => (group?.categories?.length ?? 0) > 0);
|
||||
}
|
||||
|
||||
export function computeSecondaryFlags({
|
||||
schoolInfo, yearlyData, deprivation, finance,
|
||||
schoolInfo, yearlyData, deprivation, finance, destinations,
|
||||
}: Omit<SchoolFlagsInput, 'absenceData' | 'census'>): SecondaryFlags {
|
||||
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
|
||||
|
||||
@@ -212,6 +226,8 @@ export function computeSecondaryFlags({
|
||||
return {
|
||||
latestResults, hasSixthForm, hasFinance, hasDeprivation, hasLocation,
|
||||
hasWellbeing, hasResults: !!hasResults, p8Suspended, isSpecial, suppressComparison,
|
||||
hasKs4Destinations: phaseHasContent(destinations?.ks4),
|
||||
hasKs5Destinations: phaseHasContent(destinations?.ks5),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -228,6 +244,12 @@ export function buildSecondaryNavItems(
|
||||
const navItems: NavItem[] = [];
|
||||
if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' });
|
||||
if (flags.hasResults) navItems.push({ id: 'gcse', label: 'GCSEs' });
|
||||
// Destinations sit straight after attainment: they answer "and then what
|
||||
// happened", which only makes sense once the results are in view.
|
||||
if (flags.hasKs4Destinations) navItems.push({ id: 'destinations', label: 'After Year 11' });
|
||||
if (flags.hasKs5Destinations) {
|
||||
navItems.push({ id: 'post16-destinations', label: 'After sixth form' });
|
||||
}
|
||||
if (admissions || admissionDistance) navItems.push({ id: 'admissions', label: 'Admissions' });
|
||||
if (admissionDistance?.distance_m != null && hasLocation) {
|
||||
navItems.push({ id: 'distance', label: 'Distance' });
|
||||
|
||||
@@ -598,3 +598,30 @@ export interface SortConfig {
|
||||
key: string;
|
||||
direction: SortDirection;
|
||||
}
|
||||
|
||||
// ── Destination measures ────────────────────────────────────────────────────
|
||||
// Shaped by backend/data_loader.py::_destinations_block. `status` is the field
|
||||
// that matters: 'suppressed' is DfE withholding a figure it judged disclosive
|
||||
// and must render as "withheld"; 'not_applicable' must render as nothing.
|
||||
// `pupils` is null for both, so a null check alone loses the difference.
|
||||
|
||||
import type { DestinationCell, PupilGroup } from './destinations';
|
||||
|
||||
export interface DestinationGroupPayload {
|
||||
cohort: number | null;
|
||||
categories: DestinationCell[];
|
||||
/** Totals DfE published itself. Never computed here — see lib/destinations.ts. */
|
||||
aggregates: Partial<Record<'sustained_education' | 'sustained_all', DestinationCell>>;
|
||||
}
|
||||
|
||||
export interface DestinationPhase {
|
||||
/** e.g. "2022/23" — the section dates its own cohort, which runs about two
|
||||
* GCSE years behind the results shown above it. */
|
||||
cohort_year: string | null;
|
||||
groups: Partial<Record<PupilGroup, DestinationGroupPayload>>;
|
||||
}
|
||||
|
||||
export interface SchoolDestinations {
|
||||
ks4: DestinationPhase | null;
|
||||
ks5: DestinationPhase | null;
|
||||
}
|
||||
Reference in new issue
Block a user