/** * ComparisonView Component * Client-side comparison interface with phase tabs, charts, and tables */ 'use client'; import { useEffect, useState } from 'react'; import { useRouter, usePathname, useSearchParams } from 'next/navigation'; import { useComparison } from '@/hooks/useComparison'; import { ComparisonChart } from './ComparisonChart'; import { SchoolSearchModal } from './SchoolSearchModal'; import { EmptyState } from './EmptyState'; import { LoadingSkeleton } from './LoadingSkeleton'; import type { ComparisonData, MetricDefinition, School } from '@/lib/types'; import { formatPercentage, formatProgress, CHART_COLORS, schoolUrl } from '@/lib/utils'; import { fetchComparison } from '@/lib/api'; import styles from './ComparisonView.module.css'; const PRIMARY_CATEGORIES = ['expected', 'higher', 'progress', 'average', 'gender', 'equity', 'context', 'absence', 'trends']; const SECONDARY_CATEGORIES = ['gcse']; const PRIMARY_OPTGROUPS: { label: string; category: string }[] = [ { label: 'Expected Standard', category: 'expected' }, { label: 'Higher Standard', category: 'higher' }, { label: 'Progress Scores', category: 'progress' }, { label: 'Average Scores', category: 'average' }, { label: 'Gender Performance', category: 'gender' }, { label: 'Equity (Disadvantaged)', category: 'equity' }, { label: 'School Context', category: 'context' }, { label: 'Absence', category: 'absence' }, { label: '3-Year Trends', category: 'trends' }, ]; const SECONDARY_OPTGROUPS: { label: string; category: string }[] = [ { label: 'GCSE Performance', category: 'gcse' }, ]; interface ComparisonViewProps { initialData: Record | null; initialUrns: number[]; metrics: MetricDefinition[]; selectedMetric: string; } export function ComparisonView({ initialData, initialUrns, metrics, selectedMetric: initialMetric, }: ComparisonViewProps) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const { selectedSchools, removeSchool, addSchool, isInitialized } = useComparison(); const [selectedMetric, setSelectedMetric] = useState(initialMetric); const [isModalOpen, setIsModalOpen] = useState(false); const [comparisonData, setComparisonData] = useState(initialData); const [shareConfirm, setShareConfirm] = useState(false); const [comparePhase, setComparePhase] = useState<'primary' | 'secondary'>('primary'); // Seed context from initialData when component mounts and localStorage is empty useEffect(() => { if (!isInitialized) return; if (selectedSchools.length === 0 && initialUrns.length > 0 && initialData) { initialUrns.forEach(urn => { const data = initialData[String(urn)]; if (data?.school_info) { addSchool(data.school_info); } }); } }, [isInitialized]); // eslint-disable-line react-hooks/exhaustive-deps // Sync URL with selected schools useEffect(() => { const urns = selectedSchools.map((s) => s.urn).join(','); const params = new URLSearchParams(searchParams); if (urns) { params.set('urns', urns); } else { params.delete('urns'); } params.set('metric', selectedMetric); const newUrl = `${pathname}?${params.toString()}`; router.replace(newUrl, { scroll: false }); // Fetch comparison data if (selectedSchools.length > 0) { fetchComparison(urns, { cache: 'no-store' }) .then((data) => { setComparisonData(data.comparison); }) .catch((err) => { console.error('Failed to fetch comparison:', err); setComparisonData(null); }); } else { setComparisonData(null); } }, [selectedSchools, selectedMetric, pathname, searchParams, router]); // Classify schools by phase using comparison data const classifySchool = (school: School): 'primary' | 'secondary' => { const info = comparisonData?.[school.urn]?.school_info; if (info?.attainment_8_score != null) return 'secondary'; if (info?.rwm_expected_pct != null) return 'primary'; // Fallback: check yearly data const yearlyData = comparisonData?.[school.urn]?.yearly_data; if (yearlyData?.some((d: any) => d.attainment_8_score != null)) return 'secondary'; return 'primary'; }; const primarySchools = selectedSchools.filter(s => classifySchool(s) === 'primary'); const secondarySchools = selectedSchools.filter(s => classifySchool(s) === 'secondary'); // Auto-select tab with more schools useEffect(() => { if (comparisonData && selectedSchools.length > 0) { if (secondarySchools.length > primarySchools.length) { setComparePhase('secondary'); } else { setComparePhase('primary'); } } }, [comparisonData]); // eslint-disable-line react-hooks/exhaustive-deps const handlePhaseChange = (phase: 'primary' | 'secondary') => { setComparePhase(phase); const defaultMetric = phase === 'secondary' ? 'attainment_8_score' : 'rwm_expected_pct'; setSelectedMetric(defaultMetric); }; const handleMetricChange = (metric: string) => { setSelectedMetric(metric); }; const handleRemoveSchool = (urn: number) => { removeSchool(urn); }; const handleShare = async () => { try { await navigator.clipboard.writeText(window.location.href); setShareConfirm(true); setTimeout(() => setShareConfirm(false), 2000); } catch { /* fallback: do nothing */ } }; const isPrimary = comparePhase === 'primary'; const allowedCategories = isPrimary ? PRIMARY_CATEGORIES : SECONDARY_CATEGORIES; const optgroups = isPrimary ? PRIMARY_OPTGROUPS : SECONDARY_OPTGROUPS; const filteredMetrics = metrics.filter(m => allowedCategories.includes(m.category)); const activeSchools = isPrimary ? primarySchools : secondarySchools; // Get metric definition const currentMetricDef = metrics.find((m) => m.key === selectedMetric); const metricLabel = currentMetricDef?.label || selectedMetric; // No schools selected if (selectedSchools.length === 0) { return (

Compare Schools

Add schools to your comparison basket to see side-by-side performance data

setIsModalOpen(true), }} /> setIsModalOpen(false)} />
); } // Build filtered comparison data for active phase const activeComparisonData: Record = {}; if (comparisonData) { activeSchools.forEach(s => { if (comparisonData[s.urn]) { activeComparisonData[s.urn] = comparisonData[s.urn]; } }); } // Get years for table const years = Object.keys(activeComparisonData).length > 0 ? activeComparisonData[Object.keys(activeComparisonData)[0]].yearly_data.map((d) => d.year) : []; return (
{/* Header */}

Compare Schools

Comparing {selectedSchools.length} school{selectedSchools.length !== 1 ? 's' : ''}

{/* Phase Tabs */}
{activeSchools.length === 0 ? ( setIsModalOpen(true), }} /> ) : ( <> {/* Metric Selector */}
{currentMetricDef?.description && (

{currentMetricDef.description}

)}
{/* Progress score explanation */} {selectedMetric.includes('progress') && (

Progress scores measure pupils' progress from KS1 to KS2. A score of 0 equals the national average; positive scores are above average.

)} {/* School Cards */}
{activeSchools.map((school, index) => (

{school.school_name}

{school.local_authority && ( {school.local_authority} )} {school.school_type && ( {school.school_type} )}
{/* Latest metric value */} {activeComparisonData[school.urn] && (
{metricLabel}
{(() => { const yearlyData = activeComparisonData[school.urn].yearly_data; if (yearlyData.length === 0) return '-'; const latestData = yearlyData[yearlyData.length - 1]; const value = latestData[selectedMetric as keyof typeof latestData]; if (value === null || value === undefined) return '-'; if (selectedMetric.includes('progress')) { return formatProgress(value as number); } else if (selectedMetric.includes('pct') || selectedMetric.includes('rate')) { return formatPercentage(value as number); } else { return typeof value === 'number' ? value.toFixed(1) : String(value); } })()}
)}
))}
{/* Comparison Chart */} {Object.keys(activeComparisonData).length > 0 ? (

Performance Over Time

) : activeSchools.length > 0 ? (
) : null} {/* Comparison Table */} {Object.keys(activeComparisonData).length > 0 && years.length > 0 && (

Detailed Comparison

{activeSchools.map((school) => ( ))} {years.map((year) => ( {activeSchools.map((school) => { const schoolData = activeComparisonData[school.urn]; if (!schoolData) return ; const yearData = schoolData.yearly_data.find((d) => d.year === year); if (!yearData) return ; const value = yearData[selectedMetric as keyof typeof yearData]; if (value === null || value === undefined) { return ; } let displayValue: string; if (selectedMetric.includes('progress')) { displayValue = formatProgress(value as number); } else if (selectedMetric.includes('pct') || selectedMetric.includes('rate')) { displayValue = formatPercentage(value as number); } else { displayValue = typeof value === 'number' ? value.toFixed(1) : String(value); } return ; })} ))}
Year{school.school_name}
{year}---{displayValue}
)} )} {/* School Search Modal */} setIsModalOpen(false)} />
); }