Compare commits
4
Commits
59b5f3f163
...
8cf2ac47c1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cf2ac47c1 | ||
|
|
a102508ef1 | ||
|
|
684da611ff | ||
|
|
0186227ced |
@@ -606,17 +606,21 @@ async def get_school_details(request: Request, urn: int):
|
|||||||
"religious_denomination": latest.get("religious_denomination", ""),
|
"religious_denomination": latest.get("religious_denomination", ""),
|
||||||
"age_range": latest.get("age_range", ""),
|
"age_range": latest.get("age_range", ""),
|
||||||
"has_sixth_form": latest.get("has_sixth_form"),
|
"has_sixth_form": latest.get("has_sixth_form"),
|
||||||
|
"nursery_provision": latest.get("nursery_provision"),
|
||||||
"status": latest.get("status"),
|
"status": latest.get("status"),
|
||||||
"latitude": latest.get("latitude"),
|
"latitude": latest.get("latitude"),
|
||||||
"longitude": latest.get("longitude"),
|
"longitude": latest.get("longitude"),
|
||||||
"phase": latest.get("phase"),
|
"phase": latest.get("phase"),
|
||||||
# GIAS fields
|
# GIAS fields
|
||||||
"website": latest.get("website"),
|
"website": latest.get("website"),
|
||||||
|
"telephone": latest.get("telephone"),
|
||||||
"headteacher_name": latest.get("headteacher_name"),
|
"headteacher_name": latest.get("headteacher_name"),
|
||||||
"capacity": latest.get("capacity"),
|
"capacity": latest.get("capacity"),
|
||||||
"total_pupils": latest.get("gias_total_pupils"),
|
"total_pupils": latest.get("gias_total_pupils"),
|
||||||
"trust_name": latest.get("trust_name"),
|
"trust_name": latest.get("trust_name"),
|
||||||
"gender": latest.get("gender"),
|
"gender": latest.get("gender"),
|
||||||
|
"county": latest.get("county"),
|
||||||
|
"parliamentary_constituency": latest.get("parliamentary_constituency"),
|
||||||
}.items()
|
}.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+47
-1
@@ -169,6 +169,8 @@ _MAIN_QUERY = text("""
|
|||||||
s.total_pupils AS gias_total_pupils,
|
s.total_pupils AS gias_total_pupils,
|
||||||
s.headteacher_name,
|
s.headteacher_name,
|
||||||
s.website,
|
s.website,
|
||||||
|
s.telephone,
|
||||||
|
s.nursery_provision,
|
||||||
foi.ofsted_grade,
|
foi.ofsted_grade,
|
||||||
foi.ofsted_date,
|
foi.ofsted_date,
|
||||||
foi.ofsted_framework,
|
foi.ofsted_framework,
|
||||||
@@ -178,6 +180,8 @@ _MAIN_QUERY = text("""
|
|||||||
l.address_line1 AS address1,
|
l.address_line1 AS address1,
|
||||||
l.address_line2 AS address2,
|
l.address_line2 AS address2,
|
||||||
l.town,
|
l.town,
|
||||||
|
l.county,
|
||||||
|
l.parliamentary_constituency,
|
||||||
l.postcode,
|
l.postcode,
|
||||||
l.latitude,
|
l.latitude,
|
||||||
l.longitude,
|
l.longitude,
|
||||||
@@ -301,6 +305,32 @@ for _old, _new in _LEGACY_NAME_REPLACEMENTS:
|
|||||||
_MAIN_QUERY_LEGACY_NAMES = _MAIN_QUERY_LEGACY_NAMES.replace(_old, _new)
|
_MAIN_QUERY_LEGACY_NAMES = _MAIN_QUERY_LEGACY_NAMES.replace(_old, _new)
|
||||||
_MAIN_QUERY_LEGACY_NAMES = text(_MAIN_QUERY_LEGACY_NAMES)
|
_MAIN_QUERY_LEGACY_NAMES = text(_MAIN_QUERY_LEGACY_NAMES)
|
||||||
|
|
||||||
|
# Fallback used when marts predate the optional GIAS/location detail columns
|
||||||
|
# (telephone, nursery provision, county, parliamentary constituency) — i.e. the
|
||||||
|
# nightly dbt pipeline hasn't rebuilt the mart yet on this DB. Keeps each column
|
||||||
|
# present as NULL so the whole data load degrades gracefully instead of failing.
|
||||||
|
_EXTRA_OPTIONAL_COLUMNS = (
|
||||||
|
"telephone",
|
||||||
|
"nursery_provision",
|
||||||
|
"county",
|
||||||
|
"parliamentary_constituency",
|
||||||
|
)
|
||||||
|
_MAIN_QUERY_NO_EXTRA_COLS = str(_MAIN_QUERY)
|
||||||
|
for _col, _tbl in (
|
||||||
|
("telephone", "s"),
|
||||||
|
("nursery_provision", "s"),
|
||||||
|
("county", "l"),
|
||||||
|
("parliamentary_constituency", "l"),
|
||||||
|
):
|
||||||
|
_src = f"{_tbl}.{_col},"
|
||||||
|
assert _src in _MAIN_QUERY_NO_EXTRA_COLS, (
|
||||||
|
f"expected {_src!r} to be present in _MAIN_QUERY before replacement"
|
||||||
|
)
|
||||||
|
_MAIN_QUERY_NO_EXTRA_COLS = _MAIN_QUERY_NO_EXTRA_COLS.replace(
|
||||||
|
_src, f"NULL AS {_col},"
|
||||||
|
)
|
||||||
|
_MAIN_QUERY_NO_EXTRA_COLS = text(_MAIN_QUERY_NO_EXTRA_COLS)
|
||||||
|
|
||||||
_GIAS_CODE_COLUMN_NAMES = (
|
_GIAS_CODE_COLUMN_NAMES = (
|
||||||
"phase_code",
|
"phase_code",
|
||||||
"school_type_code",
|
"school_type_code",
|
||||||
@@ -309,7 +339,11 @@ _GIAS_CODE_COLUMN_NAMES = (
|
|||||||
"admissions_policy_code",
|
"admissions_policy_code",
|
||||||
)
|
)
|
||||||
|
|
||||||
_MISSING_COLUMN_RE = re.compile(r'column "?(?:s\.)?(\w+)"? does not exist')
|
# Strip any single table alias prefix (s., l., p., foi., …) — Postgres reports
|
||||||
|
# an undefined *qualified* column unquoted as "column l.county does not exist",
|
||||||
|
# so matching only the s. alias would miss dim_location columns (county,
|
||||||
|
# parliamentary_constituency) and defeat the graceful-degradation fallback.
|
||||||
|
_MISSING_COLUMN_RE = re.compile(r'column "?(?:\w+\.)?(\w+)"? does not exist')
|
||||||
|
|
||||||
|
|
||||||
def _missing_column_name(exc: Exception) -> Optional[str]:
|
def _missing_column_name(exc: Exception) -> Optional[str]:
|
||||||
@@ -352,6 +386,18 @@ def load_school_data_as_dataframe() -> pd.DataFrame:
|
|||||||
except Exception as exc2:
|
except Exception as exc2:
|
||||||
print(f"Warning: Could not load school data from marts: {exc2}")
|
print(f"Warning: Could not load school data from marts: {exc2}")
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
elif missing in _EXTRA_OPTIONAL_COLUMNS:
|
||||||
|
logging.getLogger(__name__).warning(
|
||||||
|
"marts.dim_school/dim_location is missing %s (pipeline hasn't "
|
||||||
|
"rebuilt the mart yet on this DB) — retrying without the "
|
||||||
|
"optional detail columns: %s",
|
||||||
|
missing, exc,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
df = pd.read_sql(_MAIN_QUERY_NO_EXTRA_COLS, engine)
|
||||||
|
except Exception as exc2:
|
||||||
|
print(f"Warning: Could not load school data from marts: {exc2}")
|
||||||
|
return pd.DataFrame()
|
||||||
else:
|
else:
|
||||||
print(f"Warning: Could not load school data from marts: {exc}")
|
print(f"Warning: Could not load school data from marts: {exc}")
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|||||||
@@ -73,6 +73,24 @@ def test_missing_column_name_table_prefixed():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_column_name_location_alias_prefixed():
|
||||||
|
# dim_location columns are selected via the `l.` alias; Postgres reports a
|
||||||
|
# missing qualified column unquoted (e.g. "column l.county does not exist").
|
||||||
|
# The matcher must strip any alias, not just `s.`, or the county /
|
||||||
|
# parliamentary_constituency fallback never triggers and the whole data
|
||||||
|
# load degrades to an empty DataFrame (zero schools) instead of NULLs.
|
||||||
|
assert (
|
||||||
|
_missing_column_name(_fake_exc("column l.county does not exist"))
|
||||||
|
== "county"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
_missing_column_name(
|
||||||
|
_fake_exc("column l.parliamentary_constituency does not exist")
|
||||||
|
)
|
||||||
|
== "parliamentary_constituency"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_missing_column_name_no_match_returns_none():
|
def test_missing_column_name_no_match_returns_none():
|
||||||
assert _missing_column_name(_fake_exc("relation \"marts.dim_school\" does not exist")) is None
|
assert _missing_column_name(_fake_exc("relation \"marts.dim_school\" does not exist")) is None
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,33 @@ test('school detail page renders name and performance data', async ({ page }) =>
|
|||||||
await expect(page.locator('canvas:visible').first()).toBeVisible({ timeout: 15_000 });
|
await expect(page.locator('canvas:visible').first()).toBeVisible({ timeout: 15_000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('school detail page shows GIAS identity/contact details and drops the unwired Phonics section', async ({ page }) => {
|
||||||
|
const [urn] = await twoPrimaryUrns(page);
|
||||||
|
const res = await page.request.get(`/api/schools/${urn}`);
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
const info = (await res.json()).school_info;
|
||||||
|
|
||||||
|
await page.goto(`/school/${urn}`);
|
||||||
|
await expect(page.locator('h1').first()).toBeVisible({ timeout: 15_000 });
|
||||||
|
|
||||||
|
// Phonics, SEN-type breakdown and average class size were never populated by
|
||||||
|
// the backend — the sections have been removed, so the Phonics section (its
|
||||||
|
// own #phonics anchor) must no longer exist.
|
||||||
|
await expect(page.locator('#phonics')).toHaveCount(0);
|
||||||
|
|
||||||
|
// Newly surfaced GIAS/location fields render when the record carries them.
|
||||||
|
const ageMatch = String(info.age_range ?? '').match(/^\s*(\d+)\s*[-–]\s*(\d+)\s*$/);
|
||||||
|
if (ageMatch) {
|
||||||
|
await expect(page.getByText(`Ages ${ageMatch[1]}–${ageMatch[2]}`).first()).toBeVisible();
|
||||||
|
}
|
||||||
|
if (info.telephone) {
|
||||||
|
await expect(page.locator('a[href^="tel:"]').first()).toBeVisible();
|
||||||
|
}
|
||||||
|
if (info.parliamentary_constituency) {
|
||||||
|
await expect(page.getByText('Constituency:').first()).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('a report-card school shows its report card, dated to the report-card inspection', async ({ page }) => {
|
test('a report-card school shows its report card, dated to the report-card inspection', async ({ page }) => {
|
||||||
// Detail views detected report cards via `framework`, which the API never
|
// Detail views detected report cards via `framework`, which the API never
|
||||||
// sets to "ReportCard" — so report-card schools rendered as legacy ratings
|
// sets to "ReportCard" — so report-card schools rendered as legacy ratings
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { school_info, yearly_data, absence_data, ofsted, census, admissions, admissions_history, sen_detail, phonics, deprivation, finance } = data;
|
const { school_info, yearly_data, absence_data, ofsted, census, admissions, admissions_history, deprivation, finance } = data;
|
||||||
|
|
||||||
// Redirect bare URN to canonical slug URL
|
// Redirect bare URN to canonical slug URL
|
||||||
const canonicalSlug = schoolUrl(urn, school_info.school_name).replace('/school/', '');
|
const canonicalSlug = schoolUrl(urn, school_info.school_name).replace('/school/', '');
|
||||||
@@ -191,8 +191,6 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
|
|||||||
ofsted={ofsted ?? null}
|
ofsted={ofsted ?? null}
|
||||||
census={census ?? null}
|
census={census ?? null}
|
||||||
admissions={admissions ?? null}
|
admissions={admissions ?? null}
|
||||||
senDetail={sen_detail ?? null}
|
|
||||||
phonics={phonics ?? null}
|
|
||||||
deprivation={deprivation ?? null}
|
deprivation={deprivation ?? null}
|
||||||
finance={finance ?? null}
|
finance={finance ?? null}
|
||||||
/>
|
/>
|
||||||
@@ -205,8 +203,6 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
|
|||||||
census={census ?? null}
|
census={census ?? null}
|
||||||
admissions={admissions ?? null}
|
admissions={admissions ?? null}
|
||||||
admissionsHistory={admissions_history ?? []}
|
admissionsHistory={admissions_history ?? []}
|
||||||
senDetail={sen_detail ?? null}
|
|
||||||
phonics={phonics ?? null}
|
|
||||||
deprivation={deprivation ?? null}
|
deprivation={deprivation ?? null}
|
||||||
finance={finance ?? null}
|
finance={finance ?? null}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import { MetricTooltip } from './MetricTooltip';
|
|||||||
import type {
|
import type {
|
||||||
School, SchoolResult, AbsenceData,
|
School, SchoolResult, AbsenceData,
|
||||||
OfstedInspection, SchoolCensus,
|
OfstedInspection, SchoolCensus,
|
||||||
SchoolAdmissions, SenDetail, Phonics,
|
SchoolAdmissions,
|
||||||
SchoolDeprivation, SchoolFinance, NationalAverages,
|
SchoolDeprivation, SchoolFinance, NationalAverages,
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
import {
|
import {
|
||||||
formatPercentage, formatProgress, formatAcademicYear, isProposedToClose, ofstedLegacyAreas, isSpecialSchool,
|
formatPercentage, formatProgress, formatAcademicYear, formatAgeRange, isProposedToClose, ofstedLegacyAreas, isSpecialSchool,
|
||||||
} from '@/lib/utils';
|
} from '@/lib/utils';
|
||||||
import { DeltaChip } from './DeltaChip';
|
import { DeltaChip } from './DeltaChip';
|
||||||
import { SpecialSchoolNote } from './SpecialSchoolNote';
|
import { SpecialSchoolNote } from './SpecialSchoolNote';
|
||||||
@@ -68,15 +68,13 @@ interface SchoolDetailViewProps {
|
|||||||
census: SchoolCensus | null;
|
census: SchoolCensus | null;
|
||||||
admissions: SchoolAdmissions | null;
|
admissions: SchoolAdmissions | null;
|
||||||
admissionsHistory: SchoolAdmissions[];
|
admissionsHistory: SchoolAdmissions[];
|
||||||
senDetail: SenDetail | null;
|
|
||||||
phonics: Phonics | null;
|
|
||||||
deprivation: SchoolDeprivation | null;
|
deprivation: SchoolDeprivation | null;
|
||||||
finance: SchoolFinance | null;
|
finance: SchoolFinance | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SchoolDetailView({
|
export function SchoolDetailView({
|
||||||
schoolInfo, yearlyData, absenceData,
|
schoolInfo, yearlyData, absenceData,
|
||||||
ofsted, census, admissions, admissionsHistory, senDetail, phonics, deprivation, finance,
|
ofsted, census, admissions, admissionsHistory, deprivation, finance,
|
||||||
}: SchoolDetailViewProps) {
|
}: SchoolDetailViewProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { addSchool, removeSchool, isSelected } = useComparison();
|
const { addSchool, removeSchool, isSelected } = useComparison();
|
||||||
@@ -161,14 +159,12 @@ export function SchoolDetailView({
|
|||||||
|
|
||||||
// Phase detection. All-through schools cover BOTH key stages, so they are
|
// Phase detection. All-through schools cover BOTH key stages, so they are
|
||||||
// neither "pure primary" nor "pure secondary": isSecondary stays true (they
|
// neither "pure primary" nor "pure secondary": isSecondary stays true (they
|
||||||
// have KS4 data) but isAllThrough gates the primary-only content (phonics,
|
// have KS4 data) but isAllThrough gates the primary-only content (KS2 SATs,
|
||||||
// KS2 trend) back on and switches phase-specific copy to an all-ages framing.
|
// KS2 trend) back on and switches phase-specific copy to an all-ages framing.
|
||||||
const phase = schoolInfo.phase ?? '';
|
const phase = schoolInfo.phase ?? '';
|
||||||
const isAllThrough = phase.toLowerCase() === 'all-through';
|
const isAllThrough = phase.toLowerCase() === 'all-through';
|
||||||
const isSecondary = phase.toLowerCase().includes('secondary') || isAllThrough;
|
const isSecondary = phase.toLowerCase().includes('secondary') || isAllThrough;
|
||||||
const isPrimary = !isSecondary;
|
const isPrimary = !isSecondary;
|
||||||
// Primary-stage content shows for pure-primary AND all-through schools.
|
|
||||||
const showPrimaryContent = isPrimary || isAllThrough;
|
|
||||||
|
|
||||||
// National averages (fetched dynamically so they stay current)
|
// National averages (fetched dynamically so they stay current)
|
||||||
const [nationalAvg, setNationalAvg] = useState<NationalAverages | null>(null);
|
const [nationalAvg, setNationalAvg] = useState<NationalAverages | null>(null);
|
||||||
@@ -220,11 +216,9 @@ export function SchoolDetailView({
|
|||||||
const hasInclusionData = (latestResults?.disadvantaged_pct != null)
|
const hasInclusionData = (latestResults?.disadvantaged_pct != null)
|
||||||
|| (latestResults?.eal_pct != null)
|
|| (latestResults?.eal_pct != null)
|
||||||
|| (latestResults?.sen_support_pct != null)
|
|| (latestResults?.sen_support_pct != null)
|
||||||
|| senDetail != null
|
|
||||||
|| hasGenderSplit;
|
|| hasGenderSplit;
|
||||||
|
|
||||||
const hasSchoolLife = absenceData != null || census?.class_size_avg != null;
|
const hasSchoolLife = absenceData != null;
|
||||||
const hasPhonics = phonics != null && phonics.year1_phonics_pct != null;
|
|
||||||
const hasDeprivation = deprivation != null && deprivation.idaci_decile != null;
|
const hasDeprivation = deprivation != null && deprivation.idaci_decile != null;
|
||||||
const hasFinance = finance != null && finance.per_pupil_spend != null;
|
const hasFinance = finance != null && finance.per_pupil_spend != null;
|
||||||
const hasLocation = schoolInfo.latitude != null && schoolInfo.longitude != null;
|
const hasLocation = schoolInfo.latitude != null && schoolInfo.longitude != null;
|
||||||
@@ -265,7 +259,6 @@ export function SchoolDetailView({
|
|||||||
if (admissions) navItems.push({ id: 'admissions', label: 'Admissions' });
|
if (admissions) navItems.push({ id: 'admissions', label: 'Admissions' });
|
||||||
if (hasInclusionData) navItems.push({ id: 'inclusion', label: 'Pupils' });
|
if (hasInclusionData) navItems.push({ id: 'inclusion', label: 'Pupils' });
|
||||||
if (yearlyData.length > 0) navItems.push({ id: 'history', label: 'History' });
|
if (yearlyData.length > 0) navItems.push({ id: 'history', label: 'History' });
|
||||||
if (hasPhonics && showPrimaryContent) navItems.push({ id: 'phonics', label: 'Phonics' });
|
|
||||||
if (hasSchoolLife) navItems.push({ id: 'school-life', label: 'School Life' });
|
if (hasSchoolLife) navItems.push({ id: 'school-life', label: 'School Life' });
|
||||||
if (hasDeprivation) navItems.push({ id: 'local-area', label: 'Local Area' });
|
if (hasDeprivation) navItems.push({ id: 'local-area', label: 'Local Area' });
|
||||||
if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' });
|
if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' });
|
||||||
@@ -358,6 +351,22 @@ export function SchoolDetailView({
|
|||||||
{schoolInfo.gender && schoolInfo.gender !== 'Mixed' && (
|
{schoolInfo.gender && schoolInfo.gender !== 'Mixed' && (
|
||||||
<span className={styles.metaItem}>{schoolInfo.gender}'s school</span>
|
<span className={styles.metaItem}>{schoolInfo.gender}'s school</span>
|
||||||
)}
|
)}
|
||||||
|
{schoolInfo.age_range && (
|
||||||
|
<span className={styles.metaItem}>{formatAgeRange(schoolInfo.age_range)}</span>
|
||||||
|
)}
|
||||||
|
{schoolInfo.religious_denomination && (
|
||||||
|
<span className={styles.metaItem}>
|
||||||
|
{['Does not apply', 'None'].includes(schoolInfo.religious_denomination)
|
||||||
|
? 'None'
|
||||||
|
: schoolInfo.religious_denomination}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{schoolInfo.nursery_provision && (
|
||||||
|
<span className={styles.metaItem}>Nursery</span>
|
||||||
|
)}
|
||||||
|
{schoolInfo.has_sixth_form && (
|
||||||
|
<span className={styles.metaItem}>Sixth form</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isProposedToClose(schoolInfo) && (
|
{isProposedToClose(schoolInfo) && (
|
||||||
<div className={styles.closingStrip} role="note">
|
<div className={styles.closingStrip} role="note">
|
||||||
@@ -416,6 +425,24 @@ export function SchoolDetailView({
|
|||||||
Part of <strong>{schoolInfo.trust_name}</strong>
|
Part of <strong>{schoolInfo.trust_name}</strong>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{schoolInfo.telephone && (
|
||||||
|
<span className={styles.headerDetail}>
|
||||||
|
<strong>Phone:</strong>{' '}
|
||||||
|
<a href={`tel:${schoolInfo.telephone.replace(/\s+/g, '')}`}>
|
||||||
|
{schoolInfo.telephone}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{schoolInfo.county && (
|
||||||
|
<span className={styles.headerDetail}>
|
||||||
|
<strong>County:</strong> {schoolInfo.county}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{schoolInfo.parliamentary_constituency && (
|
||||||
|
<span className={styles.headerDetail}>
|
||||||
|
<strong>Constituency:</strong> {schoolInfo.parliamentary_constituency}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.actions} ref={heroActionsRef}>
|
<div className={styles.actions} ref={heroActionsRef}>
|
||||||
@@ -1084,29 +1111,6 @@ export function SchoolDetailView({
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
{senDetail && (
|
|
||||||
<>
|
|
||||||
<h3 className={styles.subSectionTitle}>Types of additional needs supported</h3>
|
|
||||||
<p className={styles.sectionSubtitle}>
|
|
||||||
What proportion of pupils with additional needs have each type of support need.
|
|
||||||
</p>
|
|
||||||
<div className={styles.metricsGrid}>
|
|
||||||
{[
|
|
||||||
{ label: 'Speech & Language', pct: senDetail.primary_need_speech_pct },
|
|
||||||
{ label: 'Autism (ASD)', pct: senDetail.primary_need_autism_pct },
|
|
||||||
{ label: 'Learning Difficulties', pct: senDetail.primary_need_mld_pct },
|
|
||||||
{ label: 'Specific Learning (e.g. Dyslexia)', pct: senDetail.primary_need_spld_pct },
|
|
||||||
{ label: 'Social, Emotional & Mental Health', pct: senDetail.primary_need_semh_pct },
|
|
||||||
{ label: 'Physical / Sensory', pct: senDetail.primary_need_physical_pct },
|
|
||||||
].filter(n => n.pct != null).map(({ label, pct }) => (
|
|
||||||
<div key={label} className={styles.metricCard}>
|
|
||||||
<div className={styles.metricLabel}>{label}</div>
|
|
||||||
<div className={styles.metricValue}>{pct}%</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1231,41 +1235,11 @@ export function SchoolDetailView({
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{/* Year 1 Phonics — primary-stage metric (pure primary + all-through) */}
|
|
||||||
{hasPhonics && showPrimaryContent && phonics && (
|
|
||||||
<section id="phonics" className={styles.card}>
|
|
||||||
<h2 className={styles.sectionTitle}>Year 1 Phonics ({formatAcademicYear(phonics.year)})</h2>
|
|
||||||
<p className={styles.sectionSubtitle}>
|
|
||||||
Phonics is a key early reading skill. Children are tested at the end of Year 1.
|
|
||||||
</p>
|
|
||||||
<div className={styles.metricsGrid}>
|
|
||||||
<div className={styles.metricCard}>
|
|
||||||
<div className={styles.metricLabel}>Passed the phonics check</div>
|
|
||||||
<div className={styles.metricValue}>{formatPercentage(phonics.year1_phonics_pct)}</div>
|
|
||||||
<div className={styles.metricHint}>Phonics is a key early reading skill tested at end of Year 1</div>
|
|
||||||
</div>
|
|
||||||
{phonics.year2_phonics_pct != null && (
|
|
||||||
<div className={styles.metricCard}>
|
|
||||||
<div className={styles.metricLabel}>Year 2 pupils who retook and passed</div>
|
|
||||||
<div className={styles.metricValue}>{formatPercentage(phonics.year2_phonics_pct)}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* School Life */}
|
{/* School Life */}
|
||||||
{hasSchoolLife && (
|
{hasSchoolLife && (
|
||||||
<section id="school-life" className={styles.card}>
|
<section id="school-life" className={styles.card}>
|
||||||
<h2 className={styles.sectionTitle}>School Life</h2>
|
<h2 className={styles.sectionTitle}>School Life</h2>
|
||||||
<div className={styles.metricsGrid}>
|
<div className={styles.metricsGrid}>
|
||||||
{census?.class_size_avg != null && (
|
|
||||||
<div className={styles.metricCard}>
|
|
||||||
<div className={styles.metricLabel}>Average class size</div>
|
|
||||||
<div className={styles.metricValue}>{census.class_size_avg.toFixed(1)}</div>
|
|
||||||
<div className={styles.metricHint}>Average number of pupils per class</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{absenceData?.overall_absence_rate != null && (
|
{absenceData?.overall_absence_rate != null && (
|
||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>
|
<div className={styles.metricLabel}>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const PerformanceChart = dynamic(
|
|||||||
import type {
|
import type {
|
||||||
School, SchoolResult, AbsenceData,
|
School, SchoolResult, AbsenceData,
|
||||||
OfstedInspection, SchoolCensus,
|
OfstedInspection, SchoolCensus,
|
||||||
SchoolAdmissions, SenDetail, Phonics,
|
SchoolAdmissions,
|
||||||
SchoolDeprivation, SchoolFinance, NationalAverages,
|
SchoolDeprivation, SchoolFinance, NationalAverages,
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
import { formatPercentage, formatProgress, formatAcademicYear, formatAgeRange, isProposedToClose, ofstedLegacyAreas, isSpecialSchool } from '@/lib/utils';
|
import { formatPercentage, formatProgress, formatAcademicYear, formatAgeRange, isProposedToClose, ofstedLegacyAreas, isSpecialSchool } from '@/lib/utils';
|
||||||
@@ -68,15 +68,13 @@ interface SecondarySchoolDetailViewProps {
|
|||||||
ofsted: OfstedInspection | null;
|
ofsted: OfstedInspection | null;
|
||||||
census: SchoolCensus | null;
|
census: SchoolCensus | null;
|
||||||
admissions: SchoolAdmissions | null;
|
admissions: SchoolAdmissions | null;
|
||||||
senDetail: SenDetail | null;
|
|
||||||
phonics: Phonics | null;
|
|
||||||
deprivation: SchoolDeprivation | null;
|
deprivation: SchoolDeprivation | null;
|
||||||
finance: SchoolFinance | null;
|
finance: SchoolFinance | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SecondarySchoolDetailView({
|
export function SecondarySchoolDetailView({
|
||||||
schoolInfo, yearlyData,
|
schoolInfo, yearlyData,
|
||||||
ofsted, census, admissions, senDetail, deprivation, finance, absenceData,
|
ofsted, census, admissions, deprivation, finance, absenceData,
|
||||||
}: SecondarySchoolDetailViewProps) {
|
}: SecondarySchoolDetailViewProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// Hero map — the "View on map" link opens its fullscreen view.
|
// Hero map — the "View on map" link opens its fullscreen view.
|
||||||
@@ -248,6 +246,16 @@ export function SecondarySchoolDetailView({
|
|||||||
{schoolInfo.age_range && (
|
{schoolInfo.age_range && (
|
||||||
<span className={styles.badge}>{formatAgeRange(schoolInfo.age_range)}</span>
|
<span className={styles.badge}>{formatAgeRange(schoolInfo.age_range)}</span>
|
||||||
)}
|
)}
|
||||||
|
{schoolInfo.religious_denomination && (
|
||||||
|
<span className={styles.badge}>
|
||||||
|
{['Does not apply', 'None'].includes(schoolInfo.religious_denomination)
|
||||||
|
? 'None'
|
||||||
|
: schoolInfo.religious_denomination}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{schoolInfo.nursery_provision && (
|
||||||
|
<span className={styles.badge}>Nursery</span>
|
||||||
|
)}
|
||||||
{hasSixthForm && (
|
{hasSixthForm && (
|
||||||
<span className={styles.badge}>Sixth form</span>
|
<span className={styles.badge}>Sixth form</span>
|
||||||
)}
|
)}
|
||||||
@@ -310,6 +318,24 @@ export function SecondarySchoolDetailView({
|
|||||||
Part of <strong>{schoolInfo.trust_name}</strong>
|
Part of <strong>{schoolInfo.trust_name}</strong>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{schoolInfo.telephone && (
|
||||||
|
<span className={styles.headerDetail}>
|
||||||
|
<strong>Phone:</strong>{' '}
|
||||||
|
<a href={`tel:${schoolInfo.telephone.replace(/\s+/g, '')}`}>
|
||||||
|
{schoolInfo.telephone}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{schoolInfo.county && (
|
||||||
|
<span className={styles.headerDetail}>
|
||||||
|
<strong>County:</strong> {schoolInfo.county}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{schoolInfo.parliamentary_constituency && (
|
||||||
|
<span className={styles.headerDetail}>
|
||||||
|
<strong>Constituency:</strong> {schoolInfo.parliamentary_constituency}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
|
|||||||
+4
-19
@@ -18,6 +18,7 @@ export interface School {
|
|||||||
religious_denomination: string | null;
|
religious_denomination: string | null;
|
||||||
age_range: string | null;
|
age_range: string | null;
|
||||||
has_sixth_form?: boolean | null;
|
has_sixth_form?: boolean | null;
|
||||||
|
nursery_provision?: boolean | null;
|
||||||
status?: string | null; // GIAS establishment status ("Open" / "Open, but proposed to close")
|
status?: string | null; // GIAS establishment status ("Open" / "Open, but proposed to close")
|
||||||
|
|
||||||
// Address
|
// Address
|
||||||
@@ -60,11 +61,14 @@ export interface School {
|
|||||||
|
|
||||||
// GIAS enrichment fields
|
// GIAS enrichment fields
|
||||||
website?: string | null;
|
website?: string | null;
|
||||||
|
telephone?: string | null;
|
||||||
headteacher_name?: string | null;
|
headteacher_name?: string | null;
|
||||||
capacity?: number | null;
|
capacity?: number | null;
|
||||||
trust_name?: string | null;
|
trust_name?: string | null;
|
||||||
gender?: string | null;
|
gender?: string | null;
|
||||||
admissions_policy?: string | null;
|
admissions_policy?: string | null;
|
||||||
|
county?: string | null;
|
||||||
|
parliamentary_constituency?: string | null;
|
||||||
|
|
||||||
// Ofsted (for list view — summary only)
|
// Ofsted (for list view — summary only)
|
||||||
ofsted_grade?: 1 | 2 | 3 | 4 | null;
|
ofsted_grade?: 1 | 2 | 3 | 4 | null;
|
||||||
@@ -130,7 +134,6 @@ export interface SchoolCensus {
|
|||||||
male_pupils: number | null;
|
male_pupils: number | null;
|
||||||
fsm_pct: number | null;
|
fsm_pct: number | null;
|
||||||
eal_pct: number | null;
|
eal_pct: number | null;
|
||||||
class_size_avg?: number | null;
|
|
||||||
ethnicity_white_pct?: number | null;
|
ethnicity_white_pct?: number | null;
|
||||||
ethnicity_asian_pct?: number | null;
|
ethnicity_asian_pct?: number | null;
|
||||||
ethnicity_black_pct?: number | null;
|
ethnicity_black_pct?: number | null;
|
||||||
@@ -159,22 +162,6 @@ export interface SchoolAdmissions {
|
|||||||
cross_la_offers?: number | null;
|
cross_la_offers?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SenDetail {
|
|
||||||
year: number;
|
|
||||||
primary_need_speech_pct: number | null;
|
|
||||||
primary_need_autism_pct: number | null;
|
|
||||||
primary_need_mld_pct: number | null;
|
|
||||||
primary_need_spld_pct: number | null;
|
|
||||||
primary_need_semh_pct: number | null;
|
|
||||||
primary_need_physical_pct: number | null;
|
|
||||||
primary_need_other_pct: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Phonics {
|
|
||||||
year: number;
|
|
||||||
year1_phonics_pct: number | null;
|
|
||||||
year2_phonics_pct: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SchoolDeprivation {
|
export interface SchoolDeprivation {
|
||||||
lsoa_code: string | null;
|
lsoa_code: string | null;
|
||||||
@@ -343,8 +330,6 @@ export interface SchoolDetailsResponse {
|
|||||||
admissions: SchoolAdmissions | null;
|
admissions: SchoolAdmissions | null;
|
||||||
/** All available admissions years, oldest first. Drives the multi-year trend view. */
|
/** All available admissions years, oldest first. Drives the multi-year trend view. */
|
||||||
admissions_history: SchoolAdmissions[];
|
admissions_history: SchoolAdmissions[];
|
||||||
sen_detail: SenDetail | null;
|
|
||||||
phonics: Phonics | null;
|
|
||||||
deprivation: SchoolDeprivation | null;
|
deprivation: SchoolDeprivation | null;
|
||||||
finance: SchoolFinance | null;
|
finance: SchoolFinance | null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user