feat(ofsted): add Report Card system support alongside legacy OEIF grades
All checks were successful
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 47s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m11s
Build and Push Docker Images / Build Integrator (push) Successful in 58s
Build and Push Docker Images / Build Kestra Init (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
All checks were successful
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 47s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m11s
Build and Push Docker Images / Build Integrator (push) Successful in 58s
Build and Push Docker Images / Build Kestra Init (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Ofsted replaced single overall grades with Report Cards from Nov 2025. Both systems are retained during the transition period. - DB: new framework + 9 RC columns on ofsted_inspections (schema v4) - Integrator: auto-detect OEIF vs Report Card from CSV column headers; parse 5-level RC grades and safeguarding met/not-met - API: expose all new fields in the ofsted response dict - Frontend: branch on framework='ReportCard' to show safeguarding badge + 8-category grid; fall back to legacy OEIF layout otherwise; always show inspection date in both layouts - CSS: rcGrade1–5 and safeguardingMet/NotMet classes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -575,6 +575,10 @@ def get_supplementary_data(db: Session, urn: int) -> dict:
|
|||||||
# Ofsted inspection
|
# Ofsted inspection
|
||||||
o = safe_query(OfstedInspection, "urn")
|
o = safe_query(OfstedInspection, "urn")
|
||||||
result["ofsted"] = {
|
result["ofsted"] = {
|
||||||
|
"framework": o.framework,
|
||||||
|
"inspection_date": o.inspection_date.isoformat() if o.inspection_date else None,
|
||||||
|
"inspection_type": o.inspection_type,
|
||||||
|
# OEIF fields (old framework)
|
||||||
"overall_effectiveness": o.overall_effectiveness,
|
"overall_effectiveness": o.overall_effectiveness,
|
||||||
"quality_of_education": o.quality_of_education,
|
"quality_of_education": o.quality_of_education,
|
||||||
"behaviour_attitudes": o.behaviour_attitudes,
|
"behaviour_attitudes": o.behaviour_attitudes,
|
||||||
@@ -582,8 +586,16 @@ def get_supplementary_data(db: Session, urn: int) -> dict:
|
|||||||
"leadership_management": o.leadership_management,
|
"leadership_management": o.leadership_management,
|
||||||
"early_years_provision": o.early_years_provision,
|
"early_years_provision": o.early_years_provision,
|
||||||
"previous_overall": o.previous_overall,
|
"previous_overall": o.previous_overall,
|
||||||
"inspection_date": o.inspection_date.isoformat() if o.inspection_date else None,
|
# Report Card fields (new framework, from Nov 2025)
|
||||||
"inspection_type": o.inspection_type,
|
"rc_safeguarding_met": o.rc_safeguarding_met,
|
||||||
|
"rc_inclusion": o.rc_inclusion,
|
||||||
|
"rc_curriculum_teaching": o.rc_curriculum_teaching,
|
||||||
|
"rc_achievement": o.rc_achievement,
|
||||||
|
"rc_attendance_behaviour": o.rc_attendance_behaviour,
|
||||||
|
"rc_personal_development": o.rc_personal_development,
|
||||||
|
"rc_leadership_governance": o.rc_leadership_governance,
|
||||||
|
"rc_early_years": o.rc_early_years,
|
||||||
|
"rc_sixth_form": o.rc_sixth_form,
|
||||||
} if o else None
|
} if o else None
|
||||||
|
|
||||||
# Parent View
|
# Parent View
|
||||||
|
|||||||
@@ -171,6 +171,10 @@ class OfstedInspection(Base):
|
|||||||
inspection_date = Column(Date)
|
inspection_date = Column(Date)
|
||||||
publication_date = Column(Date)
|
publication_date = Column(Date)
|
||||||
inspection_type = Column(String(100)) # Section 5 / Section 8 etc.
|
inspection_type = Column(String(100)) # Section 5 / Section 8 etc.
|
||||||
|
# Which inspection framework was used: 'OEIF' or 'ReportCard'
|
||||||
|
framework = Column(String(20))
|
||||||
|
|
||||||
|
# --- OEIF grades (old framework, pre-Nov 2025) ---
|
||||||
# 1=Outstanding 2=Good 3=Requires improvement 4=Inadequate
|
# 1=Outstanding 2=Good 3=Requires improvement 4=Inadequate
|
||||||
overall_effectiveness = Column(Integer)
|
overall_effectiveness = Column(Integer)
|
||||||
quality_of_education = Column(Integer)
|
quality_of_education = Column(Integer)
|
||||||
@@ -180,8 +184,20 @@ class OfstedInspection(Base):
|
|||||||
early_years_provision = Column(Integer) # nullable — not all schools
|
early_years_provision = Column(Integer) # nullable — not all schools
|
||||||
previous_overall = Column(Integer) # for trend display
|
previous_overall = Column(Integer) # for trend display
|
||||||
|
|
||||||
|
# --- Report Card grades (new framework, from Nov 2025) ---
|
||||||
|
# 1=Exceptional 2=Strong 3=Expected standard 4=Needs attention 5=Urgent improvement
|
||||||
|
rc_safeguarding_met = Column(Boolean) # True=Met, False=Not met
|
||||||
|
rc_inclusion = Column(Integer)
|
||||||
|
rc_curriculum_teaching = Column(Integer)
|
||||||
|
rc_achievement = Column(Integer)
|
||||||
|
rc_attendance_behaviour = Column(Integer)
|
||||||
|
rc_personal_development = Column(Integer)
|
||||||
|
rc_leadership_governance = Column(Integer)
|
||||||
|
rc_early_years = Column(Integer) # nullable — not all schools
|
||||||
|
rc_sixth_form = Column(Integer) # nullable — secondary only
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<OfstedInspection(urn={self.urn}, overall={self.overall_effectiveness})>"
|
return f"<OfstedInspection(urn={self.urn}, framework={self.framework}, overall={self.overall_effectiveness})>"
|
||||||
|
|
||||||
|
|
||||||
class OfstedParentView(Base):
|
class OfstedParentView(Base):
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ WHEN TO BUMP:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Current schema version - increment when models change
|
# Current schema version - increment when models change
|
||||||
SCHEMA_VERSION = 3
|
SCHEMA_VERSION = 4
|
||||||
|
|
||||||
# Changelog for documentation
|
# Changelog for documentation
|
||||||
SCHEMA_CHANGELOG = {
|
SCHEMA_CHANGELOG = {
|
||||||
1: "Initial schema with School and SchoolResult tables",
|
1: "Initial schema with School and SchoolResult tables",
|
||||||
2: "Added pupil absence fields (reading, maths, gps, writing, science)",
|
2: "Added pupil absence fields (reading, maths, gps, writing, science)",
|
||||||
3: "Added supplementary data tables: ofsted, parent_view, census, admissions, sen_detail, phonics, deprivation, finance; GIAS columns on schools",
|
3: "Added supplementary data tables: ofsted, parent_view, census, admissions, sen_detail, phonics, deprivation, finance; GIAS columns on schools",
|
||||||
|
4: "Added Ofsted Report Card columns to ofsted_inspections (new framework from Nov 2025)",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,65 @@ GRADE_MAP = {
|
|||||||
"Inadequate": 4, "4": 4, 4: 4,
|
"Inadequate": 4, "4": 4, 4: 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Report Card grade text → integer (1=Exceptional … 5=Urgent improvement)
|
||||||
|
RC_GRADE_MAP = {
|
||||||
|
"exceptional": 1,
|
||||||
|
"strong standard": 2,
|
||||||
|
"strong": 2,
|
||||||
|
"expected standard": 3,
|
||||||
|
"expected": 3,
|
||||||
|
"needs attention": 4,
|
||||||
|
"urgent improvement": 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Column name priority for Report Card fields (best-guess names; Ofsted may vary)
|
||||||
|
RC_COLUMN_PRIORITY = {
|
||||||
|
"rc_safeguarding": [
|
||||||
|
"Safeguarding",
|
||||||
|
"safeguarding",
|
||||||
|
"Safeguarding standards",
|
||||||
|
],
|
||||||
|
"rc_inclusion": [
|
||||||
|
"Inclusion",
|
||||||
|
"inclusion",
|
||||||
|
],
|
||||||
|
"rc_curriculum_teaching": [
|
||||||
|
"Curriculum and teaching",
|
||||||
|
"curriculum_and_teaching",
|
||||||
|
"Curriculum & teaching",
|
||||||
|
],
|
||||||
|
"rc_achievement": [
|
||||||
|
"Achievement",
|
||||||
|
"achievement",
|
||||||
|
],
|
||||||
|
"rc_attendance_behaviour": [
|
||||||
|
"Attendance and behaviour",
|
||||||
|
"attendance_and_behaviour",
|
||||||
|
"Attendance & behaviour",
|
||||||
|
],
|
||||||
|
"rc_personal_development": [
|
||||||
|
"Personal development and well-being",
|
||||||
|
"Personal development and wellbeing",
|
||||||
|
"personal_development_and_wellbeing",
|
||||||
|
"Personal development & well-being",
|
||||||
|
],
|
||||||
|
"rc_leadership_governance": [
|
||||||
|
"Leadership and governance",
|
||||||
|
"leadership_and_governance",
|
||||||
|
"Leadership & governance",
|
||||||
|
],
|
||||||
|
"rc_early_years": [
|
||||||
|
"Early years",
|
||||||
|
"early_years",
|
||||||
|
"Early years provision",
|
||||||
|
],
|
||||||
|
"rc_sixth_form": [
|
||||||
|
"Sixth form",
|
||||||
|
"sixth_form",
|
||||||
|
"Sixth form in schools",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
DEST_DIR = SUPPLEMENTARY_DIR / "ofsted"
|
DEST_DIR = SUPPLEMENTARY_DIR / "ofsted"
|
||||||
|
|
||||||
|
|
||||||
@@ -137,6 +196,26 @@ def _parse_grade(val) -> int | None:
|
|||||||
return GRADE_MAP.get(key)
|
return GRADE_MAP.get(key)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_rc_grade(val) -> int | None:
|
||||||
|
"""Parse a Report Card grade text to integer 1–5."""
|
||||||
|
if pd.isna(val):
|
||||||
|
return None
|
||||||
|
key = str(val).strip().lower()
|
||||||
|
return RC_GRADE_MAP.get(key)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_safeguarding(val) -> bool | None:
|
||||||
|
"""Parse safeguarding 'Met'/'Not met' to boolean."""
|
||||||
|
if pd.isna(val):
|
||||||
|
return None
|
||||||
|
s = str(val).strip().lower()
|
||||||
|
if s == "met":
|
||||||
|
return True
|
||||||
|
if s in ("not met", "not_met"):
|
||||||
|
return False
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _parse_date(val) -> date | None:
|
def _parse_date(val) -> date | None:
|
||||||
if pd.isna(val):
|
if pd.isna(val):
|
||||||
return None
|
return None
|
||||||
@@ -148,6 +227,19 @@ def _parse_date(val) -> date | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_framework(df: pd.DataFrame) -> str:
|
||||||
|
"""Return 'ReportCard' if new-format columns are present, else 'OEIF'."""
|
||||||
|
rc_indicators = [
|
||||||
|
"inclusion", "curriculum and teaching", "achievement",
|
||||||
|
"attendance and behaviour", "safeguarding standards", "safeguarding",
|
||||||
|
]
|
||||||
|
cols_lower = {c.lower() for c in df.columns}
|
||||||
|
for indicator in rc_indicators:
|
||||||
|
if any(indicator in c for c in cols_lower):
|
||||||
|
return "ReportCard"
|
||||||
|
return "OEIF"
|
||||||
|
|
||||||
|
|
||||||
def load(path: Path | None = None, data_dir: Path | None = None) -> dict:
|
def load(path: Path | None = None, data_dir: Path | None = None) -> dict:
|
||||||
if path is None:
|
if path is None:
|
||||||
dest = (data_dir / "supplementary" / "ofsted") if data_dir else DEST_DIR
|
dest = (data_dir / "supplementary" / "ofsted") if data_dir else DEST_DIR
|
||||||
@@ -186,7 +278,11 @@ def load(path: Path | None = None, data_dir: Path | None = None) -> dict:
|
|||||||
hdr = _find_header_row(path)
|
hdr = _find_header_row(path)
|
||||||
df = pd.read_csv(path, encoding="latin-1", low_memory=False, header=hdr)
|
df = pd.read_csv(path, encoding="latin-1", low_memory=False, header=hdr)
|
||||||
|
|
||||||
# Normalise column names: for each target field pick the first source column present
|
# Detect which framework the CSV represents BEFORE any renaming
|
||||||
|
framework = _detect_framework(df)
|
||||||
|
print(f" Ofsted: detected framework '{framework}'")
|
||||||
|
|
||||||
|
# Normalise OEIF column names: for each target field pick the first source column present
|
||||||
available = set(df.columns)
|
available = set(df.columns)
|
||||||
for target, sources in COLUMN_PRIORITY.items():
|
for target, sources in COLUMN_PRIORITY.items():
|
||||||
for src in sources:
|
for src in sources:
|
||||||
@@ -194,6 +290,14 @@ def load(path: Path | None = None, data_dir: Path | None = None) -> dict:
|
|||||||
df.rename(columns={src: target}, inplace=True)
|
df.rename(columns={src: target}, inplace=True)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Normalise Report Card column names (if present)
|
||||||
|
available = set(df.columns)
|
||||||
|
for target, sources in RC_COLUMN_PRIORITY.items():
|
||||||
|
for src in sources:
|
||||||
|
if src in available:
|
||||||
|
df.rename(columns={src: target}, inplace=True)
|
||||||
|
break
|
||||||
|
|
||||||
if "urn" not in df.columns:
|
if "urn" not in df.columns:
|
||||||
raise ValueError(f"URN column not found. Available: {list(df.columns)[:20]}")
|
raise ValueError(f"URN column not found. Available: {list(df.columns)[:20]}")
|
||||||
|
|
||||||
@@ -210,14 +314,18 @@ def load(path: Path | None = None, data_dir: Path | None = None) -> dict:
|
|||||||
df["_date_parsed"] = df["inspection_date"].apply(_parse_date)
|
df["_date_parsed"] = df["inspection_date"].apply(_parse_date)
|
||||||
df = df.sort_values("_date_parsed", ascending=False).groupby("urn").first().reset_index()
|
df = df.sort_values("_date_parsed", ascending=False).groupby("urn").first().reset_index()
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
for _, row in df.iterrows():
|
for _, row in df.iterrows():
|
||||||
urn = int(row["urn"])
|
urn = int(row["urn"])
|
||||||
|
|
||||||
record = {
|
record = {
|
||||||
"urn": urn,
|
"urn": urn,
|
||||||
|
"framework": framework,
|
||||||
"inspection_date": _parse_date(row.get("inspection_date")),
|
"inspection_date": _parse_date(row.get("inspection_date")),
|
||||||
"publication_date": _parse_date(row.get("publication_date")),
|
"publication_date": _parse_date(row.get("publication_date")),
|
||||||
"inspection_type": str(row.get("inspection_type", "")).strip() or None,
|
"inspection_type": str(row.get("inspection_type", "")).strip() or None,
|
||||||
|
# OEIF fields
|
||||||
"overall_effectiveness": _parse_grade(row.get("overall_effectiveness")),
|
"overall_effectiveness": _parse_grade(row.get("overall_effectiveness")),
|
||||||
"quality_of_education": _parse_grade(row.get("quality_of_education")),
|
"quality_of_education": _parse_grade(row.get("quality_of_education")),
|
||||||
"behaviour_attitudes": _parse_grade(row.get("behaviour_attitudes")),
|
"behaviour_attitudes": _parse_grade(row.get("behaviour_attitudes")),
|
||||||
@@ -225,32 +333,57 @@ def load(path: Path | None = None, data_dir: Path | None = None) -> dict:
|
|||||||
"leadership_management": _parse_grade(row.get("leadership_management")),
|
"leadership_management": _parse_grade(row.get("leadership_management")),
|
||||||
"early_years_provision": _parse_grade(row.get("early_years_provision")),
|
"early_years_provision": _parse_grade(row.get("early_years_provision")),
|
||||||
"previous_overall": None,
|
"previous_overall": None,
|
||||||
|
# Report Card fields
|
||||||
|
"rc_safeguarding_met": _parse_safeguarding(row.get("rc_safeguarding")),
|
||||||
|
"rc_inclusion": _parse_rc_grade(row.get("rc_inclusion")),
|
||||||
|
"rc_curriculum_teaching": _parse_rc_grade(row.get("rc_curriculum_teaching")),
|
||||||
|
"rc_achievement": _parse_rc_grade(row.get("rc_achievement")),
|
||||||
|
"rc_attendance_behaviour": _parse_rc_grade(row.get("rc_attendance_behaviour")),
|
||||||
|
"rc_personal_development": _parse_rc_grade(row.get("rc_personal_development")),
|
||||||
|
"rc_leadership_governance": _parse_rc_grade(row.get("rc_leadership_governance")),
|
||||||
|
"rc_early_years": _parse_rc_grade(row.get("rc_early_years")),
|
||||||
|
"rc_sixth_form": _parse_rc_grade(row.get("rc_sixth_form")),
|
||||||
}
|
}
|
||||||
|
|
||||||
from sqlalchemy import text
|
|
||||||
session.execute(
|
session.execute(
|
||||||
text("""
|
text("""
|
||||||
INSERT INTO ofsted_inspections
|
INSERT INTO ofsted_inspections
|
||||||
(urn, inspection_date, publication_date, inspection_type,
|
(urn, framework, inspection_date, publication_date, inspection_type,
|
||||||
overall_effectiveness, quality_of_education, behaviour_attitudes,
|
overall_effectiveness, quality_of_education, behaviour_attitudes,
|
||||||
personal_development, leadership_management, early_years_provision,
|
personal_development, leadership_management, early_years_provision,
|
||||||
previous_overall)
|
previous_overall,
|
||||||
|
rc_safeguarding_met, rc_inclusion, rc_curriculum_teaching,
|
||||||
|
rc_achievement, rc_attendance_behaviour, rc_personal_development,
|
||||||
|
rc_leadership_governance, rc_early_years, rc_sixth_form)
|
||||||
VALUES
|
VALUES
|
||||||
(:urn, :inspection_date, :publication_date, :inspection_type,
|
(:urn, :framework, :inspection_date, :publication_date, :inspection_type,
|
||||||
:overall_effectiveness, :quality_of_education, :behaviour_attitudes,
|
:overall_effectiveness, :quality_of_education, :behaviour_attitudes,
|
||||||
:personal_development, :leadership_management, :early_years_provision,
|
:personal_development, :leadership_management, :early_years_provision,
|
||||||
:previous_overall)
|
:previous_overall,
|
||||||
|
:rc_safeguarding_met, :rc_inclusion, :rc_curriculum_teaching,
|
||||||
|
:rc_achievement, :rc_attendance_behaviour, :rc_personal_development,
|
||||||
|
:rc_leadership_governance, :rc_early_years, :rc_sixth_form)
|
||||||
ON CONFLICT (urn) DO UPDATE SET
|
ON CONFLICT (urn) DO UPDATE SET
|
||||||
previous_overall = ofsted_inspections.overall_effectiveness,
|
previous_overall = ofsted_inspections.overall_effectiveness,
|
||||||
inspection_date = EXCLUDED.inspection_date,
|
framework = EXCLUDED.framework,
|
||||||
publication_date = EXCLUDED.publication_date,
|
inspection_date = EXCLUDED.inspection_date,
|
||||||
inspection_type = EXCLUDED.inspection_type,
|
publication_date = EXCLUDED.publication_date,
|
||||||
|
inspection_type = EXCLUDED.inspection_type,
|
||||||
overall_effectiveness = EXCLUDED.overall_effectiveness,
|
overall_effectiveness = EXCLUDED.overall_effectiveness,
|
||||||
quality_of_education = EXCLUDED.quality_of_education,
|
quality_of_education = EXCLUDED.quality_of_education,
|
||||||
behaviour_attitudes = EXCLUDED.behaviour_attitudes,
|
behaviour_attitudes = EXCLUDED.behaviour_attitudes,
|
||||||
personal_development = EXCLUDED.personal_development,
|
personal_development = EXCLUDED.personal_development,
|
||||||
leadership_management = EXCLUDED.leadership_management,
|
leadership_management = EXCLUDED.leadership_management,
|
||||||
early_years_provision = EXCLUDED.early_years_provision
|
early_years_provision = EXCLUDED.early_years_provision,
|
||||||
|
rc_safeguarding_met = EXCLUDED.rc_safeguarding_met,
|
||||||
|
rc_inclusion = EXCLUDED.rc_inclusion,
|
||||||
|
rc_curriculum_teaching = EXCLUDED.rc_curriculum_teaching,
|
||||||
|
rc_achievement = EXCLUDED.rc_achievement,
|
||||||
|
rc_attendance_behaviour = EXCLUDED.rc_attendance_behaviour,
|
||||||
|
rc_personal_development = EXCLUDED.rc_personal_development,
|
||||||
|
rc_leadership_governance = EXCLUDED.rc_leadership_governance,
|
||||||
|
rc_early_years = EXCLUDED.rc_early_years,
|
||||||
|
rc_sixth_form = EXCLUDED.rc_sixth_form
|
||||||
"""),
|
"""),
|
||||||
record,
|
record,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ export function Footer() {
|
|||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<h3 className={styles.title}>SchoolCompare</h3>
|
<h3 className={styles.title}>SchoolCompare</h3>
|
||||||
<p className={styles.description}>
|
<p className={styles.description}>
|
||||||
Compare primary school KS2 performance across England. Data sourced from UK Government
|
Compare primary schools across England.
|
||||||
Compare School Performance.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -24,14 +23,6 @@ export function Footer() {
|
|||||||
<h4 className={styles.sectionTitle}>About</h4>
|
<h4 className={styles.sectionTitle}>About</h4>
|
||||||
<ul className={styles.links}>
|
<ul className={styles.links}>
|
||||||
<li>
|
<li>
|
||||||
<a
|
|
||||||
href="https://www.compare-school-performance.service.gov.uk/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className={styles.link}
|
|
||||||
>
|
|
||||||
Data Source
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -475,6 +475,44 @@
|
|||||||
.ofstedGrade3 { background: rgba(201, 162, 39, 0.15); color: #b8920e; }
|
.ofstedGrade3 { background: rgba(201, 162, 39, 0.15); color: #b8920e; }
|
||||||
.ofstedGrade4 { background: rgba(224, 114, 86, 0.15); color: var(--accent-coral, #e07256); }
|
.ofstedGrade4 { background: rgba(224, 114, 86, 0.15); color: var(--accent-coral, #e07256); }
|
||||||
|
|
||||||
|
/* Report Card grade colours (5-level scale, lower = better) */
|
||||||
|
.rcGrade1 { background: rgba(45, 125, 125, 0.12); color: var(--accent-teal, #2d7d7d); } /* Exceptional */
|
||||||
|
.rcGrade2 { background: rgba(60, 140, 60, 0.12); color: #3c8c3c; } /* Strong */
|
||||||
|
.rcGrade3 { background: rgba(201, 162, 39, 0.15); color: #b8920e; } /* Expected standard */
|
||||||
|
.rcGrade4 { background: rgba(249, 115, 22, 0.12); color: #c2410c; } /* Needs attention */
|
||||||
|
.rcGrade5 { background: rgba(224, 114, 86, 0.15); color: var(--accent-coral, #e07256); } /* Urgent improvement */
|
||||||
|
|
||||||
|
/* Safeguarding badge */
|
||||||
|
.safeguardingRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.safeguardingLabel {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary, #1a1612);
|
||||||
|
}
|
||||||
|
.safeguardingMet {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background: rgba(45, 125, 125, 0.12);
|
||||||
|
color: var(--accent-teal, #2d7d7d);
|
||||||
|
}
|
||||||
|
.safeguardingNotMet {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background: rgba(224, 114, 86, 0.15);
|
||||||
|
color: var(--accent-coral, #e07256);
|
||||||
|
}
|
||||||
|
|
||||||
.ofstedDisclaimer {
|
.ofstedDisclaimer {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--text-muted, #8a847a);
|
color: var(--text-muted, #8a847a);
|
||||||
|
|||||||
@@ -22,6 +22,21 @@ const OFSTED_LABELS: Record<number, string> = {
|
|||||||
1: 'Outstanding', 2: 'Good', 3: 'Requires Improvement', 4: 'Inadequate',
|
1: 'Outstanding', 2: 'Good', 3: 'Requires Improvement', 4: 'Inadequate',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RC_LABELS: Record<number, string> = {
|
||||||
|
1: 'Exceptional', 2: 'Strong', 3: 'Expected standard', 4: 'Needs attention', 5: 'Urgent improvement',
|
||||||
|
};
|
||||||
|
|
||||||
|
const RC_CATEGORIES = [
|
||||||
|
{ key: 'rc_inclusion' as const, label: 'Inclusion' },
|
||||||
|
{ key: 'rc_curriculum_teaching' as const, label: 'Curriculum & Teaching' },
|
||||||
|
{ key: 'rc_achievement' as const, label: 'Achievement' },
|
||||||
|
{ key: 'rc_attendance_behaviour' as const, label: 'Attendance & Behaviour' },
|
||||||
|
{ key: 'rc_personal_development' as const, label: 'Personal Development' },
|
||||||
|
{ key: 'rc_leadership_governance' as const, label: 'Leadership & Governance' },
|
||||||
|
{ key: 'rc_early_years' as const, label: 'Early Years' },
|
||||||
|
{ key: 'rc_sixth_form' as const, label: 'Sixth Form' },
|
||||||
|
];
|
||||||
|
|
||||||
// 2023 national averages for context
|
// 2023 national averages for context
|
||||||
const NATIONAL_AVG = {
|
const NATIONAL_AVG = {
|
||||||
rwm_expected: 60,
|
rwm_expected: 60,
|
||||||
@@ -179,11 +194,11 @@ export function SchoolDetailView({
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Ofsted Rating */}
|
{/* Ofsted Rating / Report Card */}
|
||||||
{ofsted && (
|
{ofsted && (
|
||||||
<section id="ofsted" className={styles.card}>
|
<section id="ofsted" className={styles.card}>
|
||||||
<h2 className={styles.sectionTitle}>
|
<h2 className={styles.sectionTitle}>
|
||||||
Ofsted Rating
|
{ofsted.framework === 'ReportCard' ? 'Ofsted Report Card' : 'Ofsted Rating'}
|
||||||
{ofsted.inspection_date && (
|
{ofsted.inspection_date && (
|
||||||
<span className={styles.ofstedDate}>
|
<span className={styles.ofstedDate}>
|
||||||
Inspected {new Date(ofsted.inspection_date).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })}
|
Inspected {new Date(ofsted.inspection_date).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })}
|
||||||
@@ -198,43 +213,82 @@ export function SchoolDetailView({
|
|||||||
Full report ↗
|
Full report ↗
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
<div className={styles.ofstedHeader}>
|
|
||||||
<span className={`${styles.ofstedGrade} ${styles[`ofstedGrade${ofsted.overall_effectiveness}`]}`}>
|
{ofsted.framework === 'ReportCard' ? (
|
||||||
{ofsted.overall_effectiveness ? OFSTED_LABELS[ofsted.overall_effectiveness] : 'Not rated'}
|
/* ── New Report Card layout ── */
|
||||||
</span>
|
<>
|
||||||
{ofsted.previous_overall != null &&
|
<p className={styles.ofstedDisclaimer}>
|
||||||
ofsted.previous_overall !== ofsted.overall_effectiveness && (
|
From November 2025, Ofsted replaced single overall grades with Report Cards rating schools across several areas.
|
||||||
<span className={styles.ofstedPrevious}>
|
</p>
|
||||||
Previously: {OFSTED_LABELS[ofsted.previous_overall]}
|
{ofsted.rc_safeguarding_met != null && (
|
||||||
</span>
|
<div className={styles.safeguardingRow}>
|
||||||
)}
|
<span className={styles.safeguardingLabel}>Safeguarding</span>
|
||||||
</div>
|
<span className={ofsted.rc_safeguarding_met ? styles.safeguardingMet : styles.safeguardingNotMet}>
|
||||||
<p className={styles.ofstedDisclaimer}>
|
{ofsted.rc_safeguarding_met ? 'Met' : 'Not met'}
|
||||||
From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections of state-funded schools.
|
</span>
|
||||||
</p>
|
|
||||||
{parentView?.q_recommend_pct != null && parentView.total_responses != null && parentView.total_responses > 0 && (
|
|
||||||
<p className={styles.parentRecommendLine}>
|
|
||||||
<strong>{Math.round(parentView.q_recommend_pct)}%</strong> of parents would recommend this school ({parentView.total_responses.toLocaleString()} responses)
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div className={styles.metricsGrid}>
|
|
||||||
{[
|
|
||||||
{ label: 'Quality of Teaching', value: ofsted.quality_of_education },
|
|
||||||
{ label: 'Behaviour in School', value: ofsted.behaviour_attitudes },
|
|
||||||
{ label: 'Pupils\' Wider Development', value: ofsted.personal_development },
|
|
||||||
{ label: 'School Leadership', value: ofsted.leadership_management },
|
|
||||||
...(ofsted.early_years_provision != null
|
|
||||||
? [{ label: 'Early Years (Reception)', value: ofsted.early_years_provision }]
|
|
||||||
: []),
|
|
||||||
].map(({ label, value }) => value != null && (
|
|
||||||
<div key={label} className={styles.metricCard}>
|
|
||||||
<div className={styles.metricLabel}>{label}</div>
|
|
||||||
<div className={`${styles.metricValue} ${styles[`ofstedGrade${value}`]}`}>
|
|
||||||
{OFSTED_LABELS[value]}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.metricsGrid}>
|
||||||
|
{RC_CATEGORIES.map(({ key, label }) => {
|
||||||
|
const value = ofsted[key] as number | null;
|
||||||
|
return value != null ? (
|
||||||
|
<div key={key} className={styles.metricCard}>
|
||||||
|
<div className={styles.metricLabel}>{label}</div>
|
||||||
|
<div className={`${styles.metricValue} ${styles[`rcGrade${value}`]}`}>
|
||||||
|
{RC_LABELS[value]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
{parentView?.q_recommend_pct != null && parentView.total_responses != null && parentView.total_responses > 0 && (
|
||||||
</div>
|
<p className={styles.parentRecommendLine}>
|
||||||
|
<strong>{Math.round(parentView.q_recommend_pct)}%</strong> of parents would recommend this school ({parentView.total_responses.toLocaleString()} responses)
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
/* ── Old OEIF layout ── */
|
||||||
|
<>
|
||||||
|
<div className={styles.ofstedHeader}>
|
||||||
|
<span className={`${styles.ofstedGrade} ${styles[`ofstedGrade${ofsted.overall_effectiveness}`]}`}>
|
||||||
|
{ofsted.overall_effectiveness ? OFSTED_LABELS[ofsted.overall_effectiveness] : 'Not rated'}
|
||||||
|
</span>
|
||||||
|
{ofsted.previous_overall != null &&
|
||||||
|
ofsted.previous_overall !== ofsted.overall_effectiveness && (
|
||||||
|
<span className={styles.ofstedPrevious}>
|
||||||
|
Previously: {OFSTED_LABELS[ofsted.previous_overall]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className={styles.ofstedDisclaimer}>
|
||||||
|
From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections of state-funded schools.
|
||||||
|
</p>
|
||||||
|
{parentView?.q_recommend_pct != null && parentView.total_responses != null && parentView.total_responses > 0 && (
|
||||||
|
<p className={styles.parentRecommendLine}>
|
||||||
|
<strong>{Math.round(parentView.q_recommend_pct)}%</strong> of parents would recommend this school ({parentView.total_responses.toLocaleString()} responses)
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className={styles.metricsGrid}>
|
||||||
|
{[
|
||||||
|
{ label: 'Quality of Teaching', value: ofsted.quality_of_education },
|
||||||
|
{ label: 'Behaviour in School', value: ofsted.behaviour_attitudes },
|
||||||
|
{ label: 'Pupils\' Wider Development', value: ofsted.personal_development },
|
||||||
|
{ label: 'School Leadership', value: ofsted.leadership_management },
|
||||||
|
...(ofsted.early_years_provision != null
|
||||||
|
? [{ label: 'Early Years (Reception)', value: ofsted.early_years_provision }]
|
||||||
|
: []),
|
||||||
|
].map(({ label, value }) => value != null && (
|
||||||
|
<div key={label} className={styles.metricCard}>
|
||||||
|
<div className={styles.metricLabel}>{label}</div>
|
||||||
|
<div className={`${styles.metricValue} ${styles[`ofstedGrade${value}`]}`}>
|
||||||
|
{OFSTED_LABELS[value]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
nextjs:
|
nextjs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ export interface School {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export interface OfstedInspection {
|
export interface OfstedInspection {
|
||||||
|
framework: 'OEIF' | 'ReportCard' | null;
|
||||||
|
inspection_date: string | null;
|
||||||
|
inspection_type: string | null;
|
||||||
|
// OEIF fields (old framework, pre-Nov 2025)
|
||||||
overall_effectiveness: 1 | 2 | 3 | 4 | null;
|
overall_effectiveness: 1 | 2 | 3 | 4 | null;
|
||||||
quality_of_education: number | null;
|
quality_of_education: number | null;
|
||||||
behaviour_attitudes: number | null;
|
behaviour_attitudes: number | null;
|
||||||
@@ -72,8 +76,17 @@ export interface OfstedInspection {
|
|||||||
leadership_management: number | null;
|
leadership_management: number | null;
|
||||||
early_years_provision: number | null;
|
early_years_provision: number | null;
|
||||||
previous_overall: number | null;
|
previous_overall: number | null;
|
||||||
inspection_date: string | null;
|
// Report Card fields (new framework, from Nov 2025)
|
||||||
inspection_type: string | null;
|
// 1=Exceptional 2=Strong 3=Expected standard 4=Needs attention 5=Urgent improvement
|
||||||
|
rc_safeguarding_met: boolean | null;
|
||||||
|
rc_inclusion: number | null;
|
||||||
|
rc_curriculum_teaching: number | null;
|
||||||
|
rc_achievement: number | null;
|
||||||
|
rc_attendance_behaviour: number | null;
|
||||||
|
rc_personal_development: number | null;
|
||||||
|
rc_leadership_governance: number | null;
|
||||||
|
rc_early_years: number | null;
|
||||||
|
rc_sixth_form: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OfstedParentView {
|
export interface OfstedParentView {
|
||||||
|
|||||||
Reference in New Issue
Block a user