import React, { useState, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Line, Doughnut, Bar } from 'react-chartjs-2'; import { Carousel, EmptyState, FilterControls, StatCard } from './shared'; import { ExportableChart } from './ChartExport'; import { chartColors, createBaseOptions } from '../config/chartConfig'; import { useLanguage } from '../contexts/LanguageContext'; import { filterData, calculateMetrics, formatCurrency, formatNumber, groupByWeek, groupByMuseum, groupByDistrict, umrahData, getUniqueYears, getUniqueDistricts, getDistrictMuseumMap, getMuseumsForDistrict } from '../services/dataService'; const defaultFilters = { year: 'all', district: 'all', museum: 'all', quarter: 'all' }; const filterKeys = ['year', 'district', 'museum', 'quarter']; function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setIncludeVAT }) { const { t } = useLanguage(); const [searchParams, setSearchParams] = useSearchParams(); // Initialize filters from URL or defaults const [filters, setFiltersState] = useState(() => { const initial = { ...defaultFilters }; filterKeys.forEach(key => { const value = searchParams.get(key); if (value) initial[key] = value; }); return initial; }); // Update both state and URL const setFilters = (newFilters) => { const updated = typeof newFilters === 'function' ? newFilters(filters) : newFilters; setFiltersState(updated); const params = new URLSearchParams(); filterKeys.forEach(key => { if (updated[key] && updated[key] !== 'all') { params.set(key, updated[key]); } }); setSearchParams(params, { replace: true }); }; const [activeStatCard, setActiveStatCard] = useState(0); const [activeChart, setActiveChart] = useState(0); const [trendGranularity, setTrendGranularity] = useState('week'); const filteredData = useMemo(() => filterData(data, filters), [data, filters]); const metrics = useMemo(() => calculateMetrics(filteredData, includeVAT), [filteredData, includeVAT]); const hasData = filteredData.length > 0; const resetFilters = () => setFilters(defaultFilters); // Stat cards for carousel const statCards = useMemo(() => [ { title: t('metrics.totalRevenue'), value: formatCurrency(metrics.revenue), hasYoy: true }, { title: t('metrics.totalVisitors'), value: formatNumber(metrics.visitors) }, { title: t('metrics.totalTickets'), value: formatNumber(metrics.tickets) }, { title: t('metrics.avgRevenue'), value: formatCurrency(metrics.avgRevPerVisitor) } ], [metrics, t]); // Chart carousel labels const chartLabels = useMemo(() => { const labels = [t('charts.revenueTrend'), t('charts.visitors'), t('charts.revenue'), t('charts.quarterly'), t('charts.district'), t('charts.captureRate')]; return filters.museum === 'all' ? labels : labels.filter((_, i) => i !== 1 && i !== 2); }, [filters.museum, t]); // Dynamic lists from data const years = useMemo(() => getUniqueYears(data), [data]); const districts = useMemo(() => getUniqueDistricts(data), [data]); const districtMuseumMap = useMemo(() => getDistrictMuseumMap(data), [data]); const availableMuseums = useMemo(() => getMuseumsForDistrict(districtMuseumMap, filters.district), [districtMuseumMap, filters.district]); const yoyChange = useMemo(() => { if (filters.year === 'all') return null; const prevYear = String(parseInt(filters.year) - 1); const prevData = data.filter(row => row.year === prevYear); if (prevData.length === 0) return null; const prevMetrics = calculateMetrics(prevData, includeVAT); return prevMetrics.revenue > 0 ? ((metrics.revenue - prevMetrics.revenue) / prevMetrics.revenue * 100) : null; }, [data, filters.year, metrics.revenue, includeVAT]); // Revenue trend data (weekly or daily) const trendData = useMemo(() => { const revenueField = includeVAT ? 'revenue_gross' : 'revenue_net'; const formatLabel = (dateStr) => { 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' }); }; if (trendGranularity === 'week') { const grouped = groupByWeek(filteredData, includeVAT); const weeks = Object.keys(grouped).filter(w => w).sort(); return { labels: weeks.map(formatLabel), datasets: [{ label: includeVAT ? 'Revenue (incl. VAT)' : 'Revenue (excl. VAT)', data: weeks.map(w => grouped[w].revenue), borderColor: chartColors.primary, backgroundColor: chartColors.primary + '10', borderWidth: 2, tension: 0.4, fill: true, pointRadius: 0, pointHoverRadius: 4 }] }; } else { // Daily granularity const dailyData: Record = {}; filteredData.forEach(row => { const date = row.date; if (!dailyData[date]) dailyData[date] = 0; dailyData[date] += Number(row[revenueField] || row.revenue_incl_tax || 0); }); const days = Object.keys(dailyData).sort(); return { labels: days.map(formatLabel), datasets: [{ label: includeVAT ? 'Revenue (incl. VAT)' : 'Revenue (excl. VAT)', data: days.map(d => dailyData[d]), borderColor: chartColors.primary, backgroundColor: chartColors.primary + '10', borderWidth: 1.5, tension: 0.4, fill: true, pointRadius: 0, pointHoverRadius: 3 }] }; } }, [filteredData, trendGranularity, includeVAT]); // Museum data const museumData = useMemo(() => { const grouped = groupByMuseum(filteredData, includeVAT); const museums = Object.keys(grouped); return { visitors: { labels: museums, datasets: [{ data: museums.map(m => grouped[m].visitors), backgroundColor: [chartColors.primary + 'cc', chartColors.secondary + 'cc', chartColors.tertiary + 'cc'], borderWidth: 0 }] }, revenue: { labels: museums, datasets: [{ data: museums.map(m => grouped[m].revenue), backgroundColor: [chartColors.primary + 'cc', chartColors.secondary + 'cc', chartColors.tertiary + 'cc'], borderRadius: 4 }] } }; }, [filteredData, includeVAT]); // District data const districtData = useMemo(() => { const grouped = groupByDistrict(filteredData, includeVAT); const districts = Object.keys(grouped); return { labels: districts, datasets: [{ data: districts.map(d => grouped[d].revenue), backgroundColor: [chartColors.secondary + 'cc', chartColors.tertiary + 'cc'], borderRadius: 4 }] }; }, [filteredData, includeVAT]); // Quarterly YoY const quarterlyYoYData = useMemo(() => { const revenueField = includeVAT ? 'revenue_gross' : 'revenue_net'; const d2024 = data.filter(row => row.year === '2024'); const d2025 = data.filter(row => row.year === '2025'); const quarters = ['Q1', 'Q2', 'Q3', 'Q4']; return { labels: quarters, datasets: [ { label: '2024', data: quarters.map(q => d2024.filter(r => r.quarter === q.slice(1)).reduce((s, r) => s + parseFloat(r[revenueField] || r.revenue_incl_tax || 0), 0)), backgroundColor: chartColors.muted, borderRadius: 4 }, { label: '2025', data: quarters.map(q => d2025.filter(r => r.quarter === q.slice(1)).reduce((s, r) => s + parseFloat(r[revenueField] || r.revenue_incl_tax || 0), 0)), backgroundColor: chartColors.primary, borderRadius: 4 } ] }; }, [data, includeVAT]); // Capture rate const captureRateData = useMemo(() => { const labels = []; const rates = []; const pilgrimCounts = []; [2024, 2025].forEach(year => { [1, 2, 3, 4].forEach(q => { const pilgrims = umrahData[year]?.[q]; if (!pilgrims) return; let qData = data.filter(r => r.year === String(year) && r.quarter === String(q)); if (filters.district !== 'all') qData = qData.filter(r => r.district === filters.district); if (filters.museum !== 'all') qData = qData.filter(r => r.museum_name === filters.museum); const visitors = qData.reduce((s, r) => s + parseInt(r.visits || 0), 0); labels.push(`Q${q} ${year}`); rates.push((visitors / pilgrims * 100)); pilgrimCounts.push(pilgrims); }); }); return { labels, datasets: [ { label: 'Capture Rate (%)', data: rates, borderColor: chartColors.secondary, backgroundColor: chartColors.secondary + '10', borderWidth: 2, tension: 0.4, fill: true, pointRadius: 4, pointBackgroundColor: '#fff', pointBorderColor: chartColors.secondary, pointBorderWidth: 2, yAxisID: 'y', datalabels: { display: showDataLabels, formatter: (value) => value.toFixed(2) + '%', color: '#1e293b', backgroundColor: 'rgba(255, 255, 255, 0.9)', borderRadius: 3, font: { size: 10, weight: 600 }, anchor: 'end', align: 'top', offset: 6 } }, { label: 'Pilgrims', data: pilgrimCounts, borderColor: chartColors.tertiary, backgroundColor: chartColors.tertiary + '10', borderWidth: 2, tension: 0.4, fill: true, pointRadius: 4, pointBackgroundColor: '#fff', pointBorderColor: chartColors.tertiary, pointBorderWidth: 2, yAxisID: 'y1', order: 1, datalabels: { display: showDataLabels, formatter: (value) => (value / 1000000).toFixed(2) + 'M', color: '#1e293b', backgroundColor: 'rgba(255, 255, 255, 0.9)', borderRadius: 3, font: { size: 10, weight: 600 }, anchor: 'start', align: 'bottom', offset: 6 } } ] }; }, [data, filters.district, filters.museum, showDataLabels]); // Quarterly table const quarterlyTable = useMemo(() => { const revenueField = includeVAT ? 'revenue_gross' : 'revenue_net'; const d2024 = data.filter(row => row.year === '2024'); const d2025 = data.filter(row => row.year === '2025'); return [1, 2, 3, 4].map(q => { let q2024 = d2024.filter(r => r.quarter === String(q)); let q2025 = d2025.filter(r => r.quarter === String(q)); if (filters.district !== 'all') { q2024 = q2024.filter(r => r.district === filters.district); q2025 = q2025.filter(r => r.district === filters.district); } if (filters.museum !== 'all') { q2024 = q2024.filter(r => r.museum_name === filters.museum); q2025 = q2025.filter(r => r.museum_name === filters.museum); } const rev24 = q2024.reduce((s, r) => s + parseFloat(r[revenueField] || r.revenue_incl_tax || 0), 0); const rev25 = q2025.reduce((s, r) => s + parseFloat(r[revenueField] || r.revenue_incl_tax || 0), 0); const vis24 = q2024.reduce((s, r) => s + parseInt(r.visits || 0), 0); const vis25 = q2025.reduce((s, r) => s + parseInt(r.visits || 0), 0); const revChg = rev24 > 0 ? ((rev25 - rev24) / rev24 * 100) : 0; const visChg = vis24 > 0 ? ((vis25 - vis24) / vis24 * 100) : 0; const cap24 = umrahData[2024][q] ? (vis24 / umrahData[2024][q] * 100) : null; const cap25 = umrahData[2025][q] ? (vis25 / umrahData[2025][q] * 100) : null; return { q, rev24, rev25, revChg, vis24, vis25, visChg, cap24, cap25 }; }); }, [data, filters.district, filters.museum, includeVAT]); const baseOptions = useMemo(() => createBaseOptions(showDataLabels), [showDataLabels]); return (

{t('dashboard.title')}

{t('dashboard.subtitle')}

{t('nav.vat') || 'VAT'}
{t('nav.labels')}
{/* Desktop: Grid */}
{/* Mobile: Stats Carousel */}
c.title.replace('Total ', '').replace('Avg ', ''))} > {statCards.map((card, i) => ( ))}
{!hasData ? ( ) : ( <>

{t('dashboard.quarterlyComparison')}

{quarterlyTable.map(row => ( ))}
{t('table.quarter')} {t('table.rev2024')} {t('table.rev2025')} {t('table.change')} {t('table.visitors2024')} {t('table.visitors2025')} {t('table.change')} {t('table.capture2024')} {t('table.capture2025')}
Q{row.q} {formatCurrency(row.rev24)} {formatCurrency(row.rev25)} = 0 ? 'positive' : 'negative'}> {row.revChg >= 0 ? '+' : ''}{row.revChg.toFixed(1)}% {formatNumber(row.vis24)} {formatNumber(row.vis25)} = 0 ? 'positive' : 'negative'}> {row.visChg >= 0 ? '+' : ''}{row.visChg.toFixed(1)}% {row.cap24 ? row.cap24.toFixed(2) + '%' : '—'} {row.cap25 ? row.cap25.toFixed(2) + '%' : '—'}
{/* Desktop: Charts Grid */}
} >
{filters.museum === 'all' && (
)} {filters.museum === 'all' && (
)}
{ if (ctx.dataset.label === 'Capture Rate (%)') { return `Capture Rate: ${ctx.parsed.y.toFixed(2)}%`; } return `Pilgrims: ${ctx.parsed.y.toLocaleString()}`; } } } }, scales: { x: baseOptions.scales.x, y: { type: 'linear', position: 'left', grid: { color: chartColors.grid }, ticks: { font: { size: 12 }, color: '#94a3b8', callback: (v) => v.toFixed(1) + '%' }, border: { display: false }, title: { display: true, text: 'Capture Rate (%)', font: { size: 12 }, color: chartColors.secondary } }, y1: { type: 'linear', position: 'right', grid: { drawOnChartArea: false }, ticks: { font: { size: 12 }, color: '#94a3b8', callback: (v) => (v / 1000000).toFixed(0) + 'M' }, border: { display: false }, title: { display: true, text: 'Pilgrims', font: { size: 12 }, color: chartColors.tertiary } } } }} />
{/* Mobile: Charts Carousel */}

{t('dashboard.revenueTrends')}

{filters.museum === 'all' && (

{t('dashboard.visitorsByMuseum')}

)} {filters.museum === 'all' && (

{t('dashboard.revenueByMuseum')}

)}

{t('dashboard.quarterlyRevenue')}

{t('dashboard.districtPerformance')}

{t('dashboard.captureRateChart')}

{ if (ctx.dataset.label === 'Capture Rate (%)') { return `Capture Rate: ${ctx.parsed.y.toFixed(2)}%`; } return `Pilgrims: ${ctx.parsed.y.toLocaleString()}`; } } } }, scales: { x: baseOptions.scales.x, y: { type: 'linear', position: 'left', grid: { color: chartColors.grid }, ticks: { font: { size: 13 }, color: '#94a3b8', callback: (v) => v.toFixed(1) + '%' }, border: { display: false } }, y1: { type: 'linear', position: 'right', grid: { drawOnChartArea: false }, ticks: { font: { size: 13 }, color: '#94a3b8', callback: (v) => (v / 1000000).toFixed(0) + 'M' }, border: { display: false } } } }} />
{chartLabels.map((label, i) => ( ))}
)} ); } export default Dashboard;