Moves ~1,300 lines of section markup out of the two client views into components/school/, mirroring the components/compare/ layout. Twelve section components plus shared primitives, all server components. The only client file is AdmissionsViewToggle, which owns the hidden/aria-pressed state and receives both views as server-rendered children. JSX was extracted mechanically rather than retyped, so the markup the CSS modules depend on is verbatim. Sharing follows measured similarity, not assumption: - Finances (91%) shared. The secondary premises-cost card is gated behind a prop so primary pages are unchanged; enabling it is a one-line follow-up. - Ofsted (80%) shared, but behind a variant prop. The headline similarity hid a real fork: on a school with no overall grade the primary page shows a "Not rated" badge while the secondary shows a four-area OEIF panel, and the disclaimer copy differs. Both preserved exactly; reconciling them is a human decision, not a side effect of a move. - Admissions (14%) and History (40%) kept separate. Not yet wired up -- the old views still render. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123 lines
5.4 KiB
TypeScript
123 lines
5.4 KiB
TypeScript
/**
|
|
* WellbeingSection — SEN, gender split and local-area deprivation.
|
|
* Secondary pages. Server component.
|
|
*/
|
|
|
|
import type { School, SchoolCensus, SchoolResult, SchoolDeprivation } from '@/lib/types';
|
|
import { formatPercentage } from '@/lib/utils';
|
|
import { MetricTooltip } from '../MetricTooltip';
|
|
import { Section, sectionStyles as styles } from './sectionShared';
|
|
|
|
// Moved with this section from SecondarySchoolDetailView, its only consumer.
|
|
function deprivationDesc(decile: number) {
|
|
if (decile <= 3) return `This school is in one of England's most deprived areas (decile ${decile}/10). Many pupils may face additional challenges at home.`;
|
|
if (decile <= 7) return `This school is in an area with average levels of deprivation (decile ${decile}/10).`;
|
|
return `This school is in one of England's less deprived areas (decile ${decile}/10).`;
|
|
}
|
|
|
|
export function WellbeingSection({
|
|
latestResults, census, schoolInfo, deprivation, hasDeprivation,
|
|
}: {
|
|
latestResults: SchoolResult | null;
|
|
census: SchoolCensus | null;
|
|
schoolInfo: School;
|
|
deprivation: SchoolDeprivation | null;
|
|
hasDeprivation: boolean;
|
|
}) {
|
|
return (
|
|
<section id="wellbeing" className={styles.card}>
|
|
<h2 className={styles.sectionTitle}>Wellbeing & Context</h2>
|
|
|
|
{/* SEN */}
|
|
{(latestResults?.sen_support_pct != null || latestResults?.sen_ehcp_pct != null) && (
|
|
<>
|
|
<h3 className={styles.subSectionTitle}>Special Educational Needs (SEN)</h3>
|
|
<div className={styles.heroStatGrid}>
|
|
{latestResults?.sen_support_pct != null && (
|
|
<div className={styles.heroStatCard}>
|
|
<div className={styles.heroStatLabel}>
|
|
SEN support
|
|
<MetricTooltip metricKey="sen_support_pct" />
|
|
</div>
|
|
<div className={styles.heroStatValue}>{formatPercentage(latestResults.sen_support_pct)}</div>
|
|
<div className={styles.heroStatHint}>Without an EHCP</div>
|
|
</div>
|
|
)}
|
|
{latestResults?.sen_ehcp_pct != null && (
|
|
<div className={styles.heroStatCard}>
|
|
<div className={styles.heroStatLabel}>
|
|
Pupils with EHCP
|
|
<MetricTooltip metricKey="sen_ehcp_pct" />
|
|
</div>
|
|
<div className={styles.heroStatValue}>{formatPercentage(latestResults.sen_ehcp_pct)}</div>
|
|
<div className={styles.heroStatHint}>Education, Health and Care Plan</div>
|
|
</div>
|
|
)}
|
|
{(() => {
|
|
const total = census?.total_pupils ?? schoolInfo.total_pupils ?? latestResults?.total_pupils ?? null;
|
|
if (total == null) return null;
|
|
const female = census?.female_pupils ?? null;
|
|
const male = census?.male_pupils ?? null;
|
|
const isMixed = schoolInfo.gender === 'Mixed' || schoolInfo.gender == null;
|
|
const hasSplit = isMixed && female != null && male != null && female + male > 0;
|
|
const sum = hasSplit ? female! + male! : 0;
|
|
const girlsPct = hasSplit ? Math.round((female! / sum) * 100) : 0;
|
|
const boysPct = hasSplit ? 100 - girlsPct : 0;
|
|
return (
|
|
<div className={styles.heroStatCard}>
|
|
<div className={styles.heroStatLabel}>Total pupils</div>
|
|
<div className={styles.heroStatValue}>{total.toLocaleString()}</div>
|
|
{hasSplit && (
|
|
<>
|
|
<div
|
|
className={styles.genderBar}
|
|
role="img"
|
|
aria-label={`Gender split: ${girlsPct}% girls, ${boysPct}% boys`}
|
|
>
|
|
<span className={styles.genderBarGirls} style={{ width: `${girlsPct}%` }} />
|
|
<span className={styles.genderBarBoys} style={{ width: `${boysPct}%` }} />
|
|
</div>
|
|
<div className={styles.genderSplitHint}>
|
|
<span className={styles.genderSplitGirls}>{girlsPct}% girls</span>
|
|
<span className={styles.genderSplitSep}> · </span>
|
|
<span className={styles.genderSplitBoys}>{boysPct}% boys</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
{schoolInfo.capacity != null && !hasSplit && (
|
|
<div className={styles.heroStatHint}>Capacity: {schoolInfo.capacity}</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Deprivation */}
|
|
{hasDeprivation && deprivation && (
|
|
<>
|
|
<h3 className={styles.subSectionTitle} style={{ marginTop: '1.25rem' }}>
|
|
Local Area Context
|
|
<MetricTooltip metricKey="idaci_decile" />
|
|
</h3>
|
|
<div className={styles.deprivationDots}>
|
|
{Array.from({ length: 10 }, (_, i) => (
|
|
<div
|
|
key={i}
|
|
className={`${styles.deprivationDot} ${i < deprivation.idaci_decile! ? styles.deprivationDotFilled : ''}`}
|
|
title={`Decile ${i + 1}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className={styles.deprivationScaleLabel}>
|
|
<span>Most deprived</span>
|
|
<span>Least deprived</span>
|
|
</div>
|
|
<p className={styles.deprivationDesc}>{deprivationDesc(deprivation.idaci_decile!)}</p>
|
|
</>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|