diff --git a/backend/data_loader.py b/backend/data_loader.py index 41f698d..503b22b 100644 --- a/backend/data_loader.py +++ b/backend/data_loader.py @@ -15,7 +15,7 @@ from .database import SessionLocal, engine from .models import ( DimSchool, DimLocation, KS2Performance, FactOfstedInspection, FactParentView, FactAdmissions, - FactDeprivation, FactFinance, + FactDeprivation, FactFinance, FactPupilCharacteristics, ) from .schemas import SCHOOL_TYPE_MAP @@ -462,8 +462,20 @@ def get_supplementary_data(db: Session, urn: int) -> dict: else None ) - # Census (fact_pupil_characteristics — minimal until census columns are verified) - result["census"] = None + # Census (latest year of fact_pupil_characteristics) + pc = safe_query(FactPupilCharacteristics, "urn", "year") + result["census"] = ( + { + "year": pc.year, + "total_pupils": pc.total_pupils, + "female_pupils": pc.female_pupils, + "male_pupils": pc.male_pupils, + "fsm_pct": pc.fsm_pct, + "eal_pct": pc.eal_pct, + } + if pc + else None + ) # Admissions (latest year) a = safe_query(FactAdmissions, "urn", "year") diff --git a/backend/models.py b/backend/models.py index eef03ae..94b9d2e 100644 --- a/backend/models.py +++ b/backend/models.py @@ -189,6 +189,24 @@ class FactAdmissions(Base): admissions_policy = Column(String(100)) +class FactPupilCharacteristics(Base): + """School pupil composition from EES census — one row per URN per year.""" + __tablename__ = "fact_pupil_characteristics" + __table_args__ = ( + Index("ix_pupil_chars_urn_year", "urn", "year"), + MARTS, + ) + + urn = Column(Integer, primary_key=True) + year = Column(Integer, primary_key=True) + phase_type_grouping = Column(String(50)) + total_pupils = Column(Integer) + female_pupils = Column(Integer) + male_pupils = Column(Integer) + fsm_pct = Column(Float) + eal_pct = Column(Float) + + class FactDeprivation(Base): """IDACI deprivation index — one row per URN.""" __tablename__ = "fact_deprivation" diff --git a/nextjs-app/components/SchoolDetailView.module.css b/nextjs-app/components/SchoolDetailView.module.css index addf1d6..7a548d8 100644 --- a/nextjs-app/components/SchoolDetailView.module.css +++ b/nextjs-app/components/SchoolDetailView.module.css @@ -83,6 +83,95 @@ text-decoration: underline; } +/* Pupils block — the "Pupils: N" inline text is upgraded to a richer + display whenever we have census gender data and the school is Mixed. */ +.pupilsBlock { + display: inline-flex; + align-items: center; + gap: 0.7rem; + padding: 0.3rem 0.65rem 0.3rem 0.4rem; + background: rgba(243, 237, 228, 0.5); + border-radius: 10px; +} + +.pupilsFigure { + font-family: var(--font-playfair), 'Playfair Display', serif; + font-size: 1.6rem; + font-weight: 700; + line-height: 1; + letter-spacing: -0.01em; + color: var(--text-primary, #1a1612); +} + +.pupilsInfo { + display: flex; + flex-direction: column; + gap: 0.15rem; + min-width: 140px; +} + +.pupilsLegend { + font-size: 0.72rem; + color: var(--text-muted, #6d685f); + font-weight: 500; + line-height: 1; +} + +.pupilsCapacity { + color: var(--text-muted, #6d685f); +} + +.pupilsSplitRow { + display: flex; + align-items: baseline; + gap: 0.35rem; + font-size: 0.75rem; + font-weight: 600; + line-height: 1; +} + +.pupilsSplitGirls { + color: #b45778; +} + +.pupilsSplitBoys { + color: var(--accent-teal, #2d7d7d); +} + +.pupilsSplitSep { + color: var(--border-color, #e5dfd5); + font-weight: 400; +} + +.pupilsBar { + display: flex; + height: 4px; + border-radius: 999px; + overflow: hidden; + background: var(--border-color, #e5dfd5); + width: 100%; + min-width: 140px; +} + +.pupilsBarGirls { + background: #b45778; +} + +.pupilsBarBoys { + background: var(--accent-teal, #2d7d7d); +} + +@media (max-width: 640px) { + .pupilsBlock { + display: flex; + width: 100%; + } + .pupilsInfo { + flex: 1; + min-width: 0; + } +} + .actions { display: flex; gap: 0.5rem; diff --git a/nextjs-app/components/SchoolDetailView.tsx b/nextjs-app/components/SchoolDetailView.tsx index 497fefa..5a3baf0 100644 --- a/nextjs-app/components/SchoolDetailView.tsx +++ b/nextjs-app/components/SchoolDetailView.tsx @@ -235,12 +235,56 @@ export function SchoolDetailView({ )} - {latestResults?.total_pupils != null && ( - - Pupils: {latestResults.total_pupils.toLocaleString()} - {schoolInfo.capacity != null && ` (capacity: ${schoolInfo.capacity})`} - - )} + {(() => { + const censusTotal = census?.total_pupils ?? null; + const female = census?.female_pupils ?? null; + const male = census?.male_pupils ?? null; + const total = censusTotal ?? latestResults?.total_pupils ?? null; + const isMixed = schoolInfo.gender === 'Mixed' || schoolInfo.gender == null; + const hasSplit = isMixed && female != null && male != null && female + male > 0; + + if (hasSplit && total != null) { + const sum = female! + male!; + const girlsPct = Math.round((female! / sum) * 100); + const boysPct = 100 - girlsPct; + return ( +