import React, { useState, useMemo, useCallback, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Line, Bar } from 'react-chartjs-2'; import { EmptyState, FilterControls, MultiSelect } from './shared'; import { ExportableChart } from './ChartExport'; import { chartColors, createBaseOptions } from '../config/chartConfig'; import { useLanguage } from '../contexts/LanguageContext'; import { filterDataByDateRange, calculateMetrics, formatCompact, formatCompactCurrency, umrahData, getUniqueChannels, getUniqueMuseums, getUniqueDistricts, getMuseumsForDistrict, getLatestYear } from '../services/dataService'; import type { MuseumRecord, ComparisonProps, DateRangeFilters, Season } from '../types'; interface PresetDateRange { start: string; end: string; } interface PresetDates { [key: string]: PresetDateRange; } interface MetricCardProps { title: string; prev: number | null; curr: number | null; change: number | null; isCurrency?: boolean; isPercent?: boolean; pendingMessage?: string; prevYear: string; currYear: string; } // Generate preset dates for a given year const generatePresetDates = (year: number): PresetDates => ({ 'jan': { start: `${year}-01-01`, end: `${year}-01-31` }, 'feb': { start: `${year}-02-01`, end: `${year}-02-28` }, 'mar': { start: `${year}-03-01`, end: `${year}-03-31` }, 'apr': { start: `${year}-04-01`, end: `${year}-04-30` }, 'may': { start: `${year}-05-01`, end: `${year}-05-31` }, 'jun': { start: `${year}-06-01`, end: `${year}-06-30` }, 'jul': { start: `${year}-07-01`, end: `${year}-07-31` }, 'aug': { start: `${year}-08-01`, end: `${year}-08-31` }, 'sep': { start: `${year}-09-01`, end: `${year}-09-30` }, 'oct': { start: `${year}-10-01`, end: `${year}-10-31` }, 'nov': { start: `${year}-11-01`, end: `${year}-11-30` }, 'dec': { start: `${year}-12-01`, end: `${year}-12-31` }, 'q1': { start: `${year}-01-01`, end: `${year}-03-31` }, 'q2': { start: `${year}-04-01`, end: `${year}-06-30` }, 'q3': { start: `${year}-07-01`, end: `${year}-09-30` }, 'q4': { start: `${year}-10-01`, end: `${year}-12-31` }, 'h1': { start: `${year}-01-01`, end: `${year}-06-30` }, 'h2': { start: `${year}-07-01`, end: `${year}-12-31` }, 'full': { start: `${year}-01-01`, end: `${year}-12-31` } }); function Comparison({ data, seasons, showDataLabels, setShowDataLabels, includeVAT, setIncludeVAT }: ComparisonProps) { const { t } = useLanguage(); const [searchParams, setSearchParams] = useSearchParams(); // Get available years from data const latestYear = useMemo(() => parseInt(getLatestYear(data)), [data]); const availableYears = useMemo((): number[] => { const yearsSet = new Set(); data.forEach((r: MuseumRecord) => { const d = r.date || (r as any).Date; if (d) yearsSet.add(new Date(d).getFullYear()); }); const years = Array.from(yearsSet).sort((a, b) => b - a); return years.length ? years : [new Date().getFullYear()]; }, [data]); // Initialize state from URL or defaults const [selectedYear, setSelectedYearState] = useState(() => { const urlYear = searchParams.get('year'); return urlYear ? parseInt(urlYear) : latestYear; }); const presetDates = useMemo(() => generatePresetDates(selectedYear), [selectedYear]); const [preset, setPresetState] = useState(() => searchParams.get('preset') || 'jan'); const [startDate, setStartDateState] = useState(() => { const urlPreset = searchParams.get('preset'); const yearParam = searchParams.get('year'); const year = yearParam ? parseInt(yearParam) : latestYear; const dates = generatePresetDates(year); if (urlPreset && urlPreset !== 'custom' && dates[urlPreset]) { return dates[urlPreset].start; } // Season presets store from/to in URL const fromParam = searchParams.get('from'); if (fromParam) return fromParam; return `${year}-01-01`; }); const [endDate, setEndDateState] = useState(() => { const urlPreset = searchParams.get('preset'); const yearParam = searchParams.get('year'); const year = yearParam ? parseInt(yearParam) : latestYear; const dates = generatePresetDates(year); if (urlPreset && urlPreset !== 'custom' && dates[urlPreset]) { return dates[urlPreset].end; } // Season presets store from/to in URL const toParam = searchParams.get('to'); if (toParam) return toParam; return `${year}-01-31`; }); const [filters, setFiltersState] = useState(() => ({ district: searchParams.get('district') || 'all', channel: searchParams.get('channel')?.split(',').filter(Boolean) || [], museum: searchParams.get('museum')?.split(',').filter(Boolean) || [] })); const [chartMetric, setChartMetric] = useState('revenue'); const [chartGranularity, setChartGranularity] = useState('week'); const [activeChart, setActiveChart] = useState(0); const [activeCard, setActiveCard] = useState(0); // Update URL with current state const updateUrl = useCallback((newPreset: string, newFrom: string | null, newTo: string | null, newFilters: DateRangeFilters | null, newYear: number) => { const params = new URLSearchParams(); if (newPreset && newPreset !== 'jan') params.set('preset', newPreset); if (newYear && newYear !== latestYear) params.set('year', newYear.toString()); if (newPreset === 'custom' || newPreset.startsWith('season-')) { if (newFrom) params.set('from', newFrom); if (newTo) params.set('to', newTo); } if (newFilters?.district && newFilters.district !== 'all') params.set('district', newFilters.district); if (newFilters?.channel && newFilters.channel.length > 0) params.set('channel', newFilters.channel.join(',')); if (newFilters?.museum && newFilters.museum.length > 0) params.set('museum', newFilters.museum.join(',')); setSearchParams(params, { replace: true }); }, [setSearchParams, latestYear]); const setSelectedYear = (year: number) => { setSelectedYearState(year); const newDates = generatePresetDates(year); if (preset !== 'custom' && !preset.startsWith('season-') && newDates[preset]) { setStartDateState(newDates[preset].start); setEndDateState(newDates[preset].end); } updateUrl(preset, null, null, filters, year); }; const setPreset = (value: string) => { setPresetState(value); if (value.startsWith('season-')) { const seasonId = parseInt(value.replace('season-', '')); const season = seasons.find(s => s.Id === seasonId); if (season) { setStartDateState(season.StartDate); setEndDateState(season.EndDate); updateUrl(value, season.StartDate, season.EndDate, filters, selectedYear); } } else if (value !== 'custom' && presetDates[value]) { setStartDateState(presetDates[value].start); setEndDateState(presetDates[value].end); updateUrl(value, null, null, filters, selectedYear); } }; const setStartDate = (value: string) => { setStartDateState(value); setPresetState('custom'); updateUrl('custom', value, endDate, filters, selectedYear); }; const setEndDate = (value: string) => { setEndDateState(value); setPresetState('custom'); updateUrl('custom', startDate, value, filters, selectedYear); }; const setFilters = (newFilters: DateRangeFilters | ((prev: DateRangeFilters) => DateRangeFilters)) => { const updated = typeof newFilters === 'function' ? newFilters(filters) : newFilters; setFiltersState(updated); updateUrl(preset, startDate, endDate, updated, selectedYear); }; const charts = [ { id: 'timeseries', label: t('comparison.trend') }, { id: 'museum', label: t('comparison.byMuseum') } ]; // Touch swipe handlers const touchStartChart = useRef(null); const touchStartCard = useRef(null); const handleChartTouchStart = (e: React.TouchEvent) => { touchStartChart.current = e.touches[0].clientX; }; const handleChartTouchEnd = (e: React.TouchEvent) => { if (!touchStartChart.current) return; const diff = touchStartChart.current - e.changedTouches[0].clientX; if (Math.abs(diff) > 50) { if (diff > 0 && activeChart < charts.length - 1) { setActiveChart(activeChart + 1); } else if (diff < 0 && activeChart > 0) { setActiveChart(activeChart - 1); } } touchStartChart.current = null; }; const granularityOptions = [ { value: 'day', label: t('time.daily') }, { value: 'week', label: t('time.weekly') }, { value: 'month', label: t('time.monthly') } ]; const revenueField = includeVAT ? 'revenue_gross' : 'revenue_net'; const metricOptions = [ { value: 'revenue', label: t('metrics.revenue'), field: revenueField, format: 'currency' }, { value: 'visitors', label: t('metrics.visitors'), field: 'visits', format: 'number' }, { value: 'tickets', label: t('metrics.tickets'), field: 'tickets', format: 'number' }, { value: 'avgRevenue', label: t('metrics.avgRevenue'), field: null, format: 'currency' } ]; const getMetricValue = useCallback((rows: MuseumRecord[], metric: string) => { if (metric === 'avgRevenue') { const revenue = rows.reduce((s: number, r: MuseumRecord) => s + parseFloat(String((r as any)[revenueField] || 0)), 0); const visitors = rows.reduce((s: number, r: MuseumRecord) => s + parseInt(String(r.visits || 0)), 0); return visitors > 0 ? revenue / visitors : 0; } const fieldMap: Record = { revenue: revenueField, visitors: 'visits', tickets: 'tickets' }; const field = fieldMap[metric]; return rows.reduce((s: number, r: MuseumRecord) => s + parseFloat(String((r as any)[field] || 0)), 0); }, [revenueField]); // Dynamic lists from data const channels = useMemo(() => getUniqueChannels(data), [data]); const districts = useMemo(() => getUniqueDistricts(data), [data]); const availableMuseums = useMemo(() => getMuseumsForDistrict(data, filters.district), [data, filters.district]); // Year-over-year comparison: same dates, previous year // For season presets, try to find the same season name from the previous hijri year const ranges = useMemo(() => { const curr = { start: startDate, end: endDate }; let prev = { start: startDate.replace(/^(\d{4})/, (_: string, y: string) => String(parseInt(y) - 1)), end: endDate.replace(/^(\d{4})/, (_: string, y: string) => String(parseInt(y) - 1)) }; if (preset.startsWith('season-')) { const seasonId = parseInt(preset.replace('season-', '')); const currentSeason = seasons.find(s => s.Id === seasonId); if (currentSeason) { const prevSeason = seasons.find( s => s.Name === currentSeason.Name && s.HijriYear === currentSeason.HijriYear - 1 ); if (prevSeason) { prev = { start: prevSeason.StartDate, end: prevSeason.EndDate }; } } } return { curr, prev }; }, [startDate, endDate, preset, seasons]); const prevData = useMemo(() => filterDataByDateRange(data, ranges.prev.start, ranges.prev.end, filters), [data, ranges.prev, filters] ); const currData = useMemo(() => filterDataByDateRange(data, ranges.curr.start, ranges.curr.end, filters), [data, ranges.curr, filters] ); const prevMetrics = useMemo(() => calculateMetrics(prevData, includeVAT), [prevData, includeVAT]); const currMetrics = useMemo(() => calculateMetrics(currData, includeVAT), [currData, includeVAT]); const hasData = prevData.length > 0 || currData.length > 0; const resetFilters = () => setFilters({ district: 'all', channel: [], museum: [] }); const calcChange = (prev: number, curr: number) => prev === 0 ? (curr > 0 ? Infinity : 0) : ((curr - prev) / prev * 100); // Get quarter from date range (returns null if not a clean quarter) const getQuarterFromRange = (start: string, end: string) => { const quarterRanges: Record = { 1: { start: '-01-01', end: '-03-31' }, 2: { start: '-04-01', end: '-06-30' }, 3: { start: '-07-01', end: '-09-30' }, 4: { start: '-10-01', end: '-12-31' } }; for (let q = 1; q <= 4; q++) { if (start.endsWith(quarterRanges[q].start) && end.endsWith(quarterRanges[q].end)) { return q; } } return null; }; // Estimate pilgrims for any date range by prorating quarterly data const estimatePilgrims = useCallback((start: string, end: string): number | null => { const startDate = new Date(start); const endDate = new Date(end); let total = 0; let hasData = false; // Iterate through each quarter that overlaps with the range const startYear = startDate.getFullYear(); const endYear = endDate.getFullYear(); for (let year = startYear; year <= endYear; year++) { for (let q = 1; q <= 4; q++) { const qStart = new Date(year, (q - 1) * 3, 1); const qEnd = new Date(year, q * 3, 0); // last day of quarter // Check overlap if (qEnd < startDate || qStart > endDate) continue; const pilgrims = umrahData[year]?.[q]; if (!pilgrims) continue; // Calculate overlap fraction const overlapStart = new Date(Math.max(qStart.getTime(), startDate.getTime())); const overlapEnd = new Date(Math.min(qEnd.getTime(), endDate.getTime())); const overlapDays = (overlapEnd.getTime() - overlapStart.getTime()) / (1000 * 60 * 60 * 24) + 1; const quarterDays = (qEnd.getTime() - qStart.getTime()) / (1000 * 60 * 60 * 24) + 1; total += pilgrims * (overlapDays / quarterDays); hasData = true; } } return hasData ? Math.round(total) : null; }, []); // Calculate capture rate and pilgrim data for any date range const quarterData = useMemo(() => { const prevPilgrims = estimatePilgrims(ranges.prev.start, ranges.prev.end); const currPilgrims = estimatePilgrims(ranges.curr.start, ranges.curr.end); if (!prevPilgrims && !currPilgrims) return null; const prevRate = prevPilgrims ? (prevMetrics.visitors / prevPilgrims * 100) : null; const currRate = currPilgrims ? (currMetrics.visitors / currPilgrims * 100) : null; return { pilgrims: { prev: prevPilgrims, curr: currPilgrims }, captureRate: { prev: prevRate, curr: currRate } }; }, [ranges, prevMetrics.visitors, currMetrics.visitors, estimatePilgrims]); const captureRates = quarterData?.captureRate || null; const pilgrimCounts = quarterData?.pilgrims || null; // Build cards array dynamically interface CardData { title: string; prev: number | null; curr: number | null; change: number | null; isCurrency?: boolean; isPercent?: boolean; pendingMessage?: string; } const metricCards = useMemo((): CardData[] => { const revenueChange = calcChange(prevMetrics.revenue, currMetrics.revenue); const visitorsChange = calcChange(prevMetrics.visitors, currMetrics.visitors); const ticketsChange = calcChange(prevMetrics.tickets, currMetrics.tickets); const avgRevChange = calcChange(prevMetrics.avgRevPerVisitor, currMetrics.avgRevPerVisitor); const pilgrimsChange = pilgrimCounts ? calcChange(pilgrimCounts.prev || 0, pilgrimCounts.curr || 0) : null; const captureRateChange = captureRates ? calcChange(captureRates.prev || 0, captureRates.curr || 0) : null; const cards: CardData[] = [ { title: t('metrics.revenue'), prev: prevMetrics.revenue, curr: currMetrics.revenue, change: revenueChange, isCurrency: true }, { title: t('metrics.visitors'), prev: prevMetrics.visitors, curr: currMetrics.visitors, change: visitorsChange }, { title: t('metrics.tickets'), prev: prevMetrics.tickets, curr: currMetrics.tickets, change: ticketsChange }, { title: t('metrics.avgRevenue'), prev: prevMetrics.avgRevPerVisitor, curr: currMetrics.avgRevPerVisitor, change: avgRevChange, isCurrency: true } ]; if (pilgrimCounts) { cards.push({ title: t('metrics.pilgrims'), prev: pilgrimCounts.prev, curr: pilgrimCounts.curr, change: pilgrimsChange, pendingMessage: t('comparison.pendingData') }); } if (captureRates) { cards.push({ title: t('metrics.captureRate'), prev: captureRates.prev, curr: captureRates.curr, change: captureRateChange, isPercent: true, pendingMessage: t('comparison.pendingData') }); } return cards; }, [prevMetrics, currMetrics, pilgrimCounts, captureRates, t]); const handleCardTouchStart = (e: React.TouchEvent) => { touchStartCard.current = e.touches[0].clientX; }; const handleCardTouchEnd = (e: React.TouchEvent) => { if (!touchStartCard.current) return; const diff = touchStartCard.current - e.changedTouches[0].clientX; if (Math.abs(diff) > 50) { if (diff > 0 && activeCard < metricCards.length - 1) { setActiveCard(activeCard + 1); } else if (diff < 0 && activeCard > 0) { setActiveCard(activeCard - 1); } } touchStartCard.current = null; }; const formatDate = (dateStr: string) => { if (!dateStr) return ''; const [year, month, day] = dateStr.split('-').map(Number); const d = new Date(year, month - 1, day); return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }; // Generate period label - shows year if same year, or "MMM YY–MMM YY" if spans years const getPeriodLabel = useCallback((startDate: string, endDate: string) => { if (!startDate || !endDate) return ''; const startYear = startDate.substring(0, 4); const endYear = endDate.substring(0, 4); if (startYear === endYear) { return startYear; } // Spans multiple years - show abbreviated range const [sy, sm] = startDate.split('-').map(Number); const [ey, em] = endDate.split('-').map(Number); const startMonth = new Date(sy, sm - 1, 1).toLocaleDateString('en-US', { month: 'short' }); const endMonth = new Date(ey, em - 1, 1).toLocaleDateString('en-US', { month: 'short' }); return `${startMonth} ${String(sy).slice(-2)}–${endMonth} ${String(ey).slice(-2)}`; }, []); // Time series chart (daily or weekly) const timeSeriesChart = useMemo(() => { const groupByPeriod = (periodData: MuseumRecord[], periodStart: string, metric: string, granularity: string) => { const start = new Date(periodStart); const groupedRows: Record = {}; periodData.forEach((row: MuseumRecord) => { if (!row.date) return; const rowDate = new Date(row.date); const daysDiff = Math.floor((rowDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); let key; if (granularity === 'month') { // Group by month number (relative to start) const monthsDiff = (rowDate.getFullYear() - start.getFullYear()) * 12 + (rowDate.getMonth() - start.getMonth()); key = monthsDiff + 1; } else if (granularity === 'week') { key = Math.floor(daysDiff / 7) + 1; } else { key = daysDiff + 1; // day number from start } if (!groupedRows[key]) groupedRows[key] = []; groupedRows[key].push(row); }); const result: Record = {}; Object.keys(groupedRows).forEach(key => { result[Number(key)] = getMetricValue(groupedRows[Number(key)], metric); }); return result; }; const prevGrouped = groupByPeriod(prevData, ranges.prev.start, chartMetric, chartGranularity); const currGrouped = groupByPeriod(currData, ranges.curr.start, chartMetric, chartGranularity); const maxKey = Math.max(...Object.keys(prevGrouped).map(Number), ...Object.keys(currGrouped).map(Number), 1); const labels = Array.from({ length: maxKey }, (_, i) => { if (chartGranularity === 'month') { const startDate = new Date(ranges.curr.start); const monthNum = ((startDate.getMonth() + i) % 12) + 1; return String(monthNum); } if (chartGranularity === 'week') return `W${i + 1}`; return `D${i + 1}`; }); const prevLabel = getPeriodLabel(ranges.prev.start, ranges.prev.end); const currLabel = getPeriodLabel(ranges.curr.start, ranges.curr.end); return { labels, datasets: [ { label: prevLabel, data: labels.map((_, i) => prevGrouped[i + 1] || 0), borderColor: chartColors.muted, backgroundColor: 'transparent', borderWidth: 2, tension: 0.4, pointRadius: chartGranularity === 'week' ? 3 : 1, pointBackgroundColor: chartColors.muted }, { label: currLabel, data: labels.map((_, i) => currGrouped[i + 1] || 0), borderColor: chartColors.primary, backgroundColor: chartColors.primary + '10', borderWidth: 2, tension: 0.4, fill: true, pointRadius: chartGranularity === 'week' ? 4 : 2, pointBackgroundColor: chartColors.primary } ] }; }, [prevData, currData, ranges, chartMetric, chartGranularity, getMetricValue, getPeriodLabel]); // Museum chart - only show museums with data const museumChart = useMemo(() => { const prevLabel = getPeriodLabel(ranges.prev.start, ranges.prev.end); const currLabel = getPeriodLabel(ranges.curr.start, ranges.curr.end); const allMuseums = [...new Set(data.map((r: MuseumRecord) => r.museum_name))].filter(Boolean) as string[]; const prevByMuseum: Record = {}; const currByMuseum: Record = {}; allMuseums.forEach(m => { const prevRows = prevData.filter(r => r.museum_name === m); const currRows = currData.filter(r => r.museum_name === m); prevByMuseum[m] = getMetricValue(prevRows, chartMetric); currByMuseum[m] = getMetricValue(currRows, chartMetric); }); // Only include museums that have data in either period const museums = allMuseums.filter(m => prevByMuseum[m] > 0 || currByMuseum[m] > 0); return { labels: museums, datasets: [ { label: prevLabel, data: museums.map(m => prevByMuseum[m]), backgroundColor: chartColors.muted, borderRadius: 4 }, { label: currLabel, data: museums.map(m => currByMuseum[m]), backgroundColor: chartColors.primary, borderRadius: 4 } ] }; }, [data, prevData, currData, ranges, chartMetric, getMetricValue, getPeriodLabel]); const baseOptions = useMemo(() => createBaseOptions(showDataLabels), [showDataLabels]); // Map seasons to annotation bands on the current period's timeline const seasonAnnotations = useMemo(() => { if (!seasons.length) return {}; const currStart = new Date(ranges.curr.start); const currEnd = new Date(ranges.curr.end); const annotations: Record = {}; const msPerDay = 1000 * 60 * 60 * 24; const granDivisor = chartGranularity === 'month' ? 30 : chartGranularity === 'week' ? 7 : 1; seasons.forEach((s, i) => { const sStart = new Date(s.StartDate); const sEnd = new Date(s.EndDate); // Check overlap with current period if (sEnd < currStart || sStart > currEnd) return; const clampedStart = sStart < currStart ? currStart : sStart; const clampedEnd = sEnd > currEnd ? currEnd : sEnd; const startIdx = Math.floor((clampedStart.getTime() - currStart.getTime()) / msPerDay / granDivisor); const endIdx = Math.floor((clampedEnd.getTime() - currStart.getTime()) / msPerDay / granDivisor); annotations[`season${i}`] = { type: 'box', xMin: startIdx - 0.5, xMax: endIdx + 0.5, backgroundColor: s.Color + '20', borderColor: s.Color + '40', borderWidth: 1, label: { display: true, content: `${s.Name} ${s.HijriYear}`, position: 'start', color: s.Color, font: { size: 10, weight: '600' }, padding: 4 } }; }); return annotations; }, [seasons, ranges.curr, chartGranularity]); const chartOptions: any = { ...baseOptions, plugins: { ...baseOptions.plugins, legend: { position: 'top', align: 'end', labels: { boxWidth: 12, padding: 12, font: { size: 13 } } }, annotation: { annotations: seasonAnnotations } } }; return (

{t('comparison.title')}

{t('comparison.subtitle')}

{t('nav.vat') || 'VAT'}
{t('nav.labels')}
{preset !== 'custom' && !preset.startsWith('season-') && ( )} {(preset === 'custom' || preset.startsWith('season-')) && ( <> setStartDate(e.target.value)} /> setEndDate(e.target.value)} /> )} setFilters({...filters, channel: selected})} allLabel={t('filters.allChannels')} /> setFilters({...filters, museum: selected})} allLabel={t('filters.allMuseums')} />
{t('comparison.previousPeriod')}
{getPeriodLabel(ranges.prev.start, ranges.prev.end)}
{formatDate(ranges.prev.start)} → {formatDate(ranges.prev.end)}
{t('comparison.vs')}
{t('comparison.currentPeriod')}
{getPeriodLabel(ranges.curr.start, ranges.curr.end)}
{formatDate(ranges.curr.start)} → {formatDate(ranges.curr.end)}
{!hasData ? ( ) : ( <> {/* Desktop: Grid layout */}
{metricCards.map((card, i) => ( ))}
{/* Mobile: Carousel layout */}
{metricCards.map((card, i) => (
))}
{metricCards.map((card, i) => ( ))}
{/* Desktop: Show both charts */}
m.value === chartMetric)?.label} - ${t('comparison.trend')}`} className="chart-container" controls={ <>
{granularityOptions.map(opt => ( ))}
{metricOptions.map(opt => ( ))}
} >
m.value === chartMetric)?.label} - ${t('comparison.byMuseum')}`} className="chart-container" controls={
{metricOptions.map(opt => ( ))}
} >
{/* Mobile: Carousel */}

{metricOptions.find(m => m.value === chartMetric)?.label} - {t('comparison.trend')}

{granularityOptions.map(opt => ( ))}
{metricOptions.map(opt => ( ))}

{metricOptions.find(m => m.value === chartMetric)?.label} - {t('comparison.byMuseum')}

{metricOptions.map(opt => ( ))}
{charts.map((chart, i) => ( ))}
)}
); } function MetricCard({ title, prev, curr, change, isCurrency, isPercent, pendingMessage, prevYear, currYear }: MetricCardProps) { const hasPending = prev === null || curr === null; const isPositive = (change ?? 0) >= 0; const changeText = (hasPending && pendingMessage) ? null : (change === Infinity || change === null ? '—' : `${isPositive ? '+' : ''}${change.toFixed(1)}%`); const formatValue = (val: number | null | undefined) => { if (val === null || val === undefined) return '—'; if (isPercent) return val.toFixed(2) + '%'; if (isCurrency) return formatCompactCurrency(val); return formatCompact(val); }; const diff = (curr || 0) - (prev || 0); const diffText = (hasPending && pendingMessage) ? pendingMessage : (isPercent ? (diff >= 0 ? '+' : '') + diff.toFixed(2) + 'pp' : (isCurrency ? formatCompactCurrency(diff) : formatCompact(diff))); return (

{title}

{prevYear}
{formatValue(prev)}
{hasPending && pendingMessage ? (
{pendingMessage}
) : ( <>
{changeText}
{diff >= 0 ? '+' : ''}{diffText}
)}
{currYear}
{formatValue(curr)}
); } export default Comparison;