import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react'; import { Link } from 'react-router-dom'; import { Line, Bar } from 'react-chartjs-2'; import { filterDataByDateRange, calculateMetrics, getUniqueChannels, getUniqueMuseums, getUniqueDistricts, umrahData } from '../services/dataService'; import { chartColors, createBaseOptions } from '../config/chartConfig'; import type { MuseumRecord, Season } from '../types'; import { useLanguage } from '../contexts/LanguageContext'; import type { LC } from '../lib/locale'; import { EN, AR } from '../lib/locale'; import { currentMonth, shiftYear, periodNameL, dateRangeTextL } from '../lib/dateHelpers'; import { InlinePicker } from './shared/PeriodPicker'; import AltMultiSelect from './shared/AltMultiSelect'; import MetricCard from './shared/MetricCard'; interface Props { data: MuseumRecord[]; seasons: Season[]; includeVAT: boolean; allowedMuseums: string[] | null; allowedChannels: string[] | null; lang?: 'en' | 'ar'; } // ─── period card ────────────────────────────────────────────────── function PeriodCard({ role, hint, start, end, variant, onChange, availableYears, L }: { role: string; hint: string; start: string; end: string; variant: 'current'|'previous'; onChange: (s: string, e: string) => void; availableYears: number[]; L: LC; }) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { if (!open) return; const onM = (e: MouseEvent) => { if(ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; const onK = (e: KeyboardEvent) => { if(e.key==='Escape') setOpen(false); }; document.addEventListener('mousedown', onM); document.addEventListener('keydown', onK); return () => { document.removeEventListener('mousedown', onM); document.removeEventListener('keydown', onK); }; }, [open]); return (
{role} {hint}
{periodNameL(start, end, L)}
{dateRangeTextL(start, end, L)}
{open && setOpen(false)} availableYears={availableYears} L={L} />}
); } // ─── main page ──────────────────────────────────────────────────── export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedMuseums, allowedChannels }: Props) { const { lang: activeLang, setLanguage } = useLanguage(); const L = activeLang === 'ar' ? AR : EN; const curr = currentMonth(); const [currStart, setCurrStart] = useState(curr.start); const [currEnd, setCurrEnd] = useState(curr.end); const [prevStart, setPrevStart] = useState(() => shiftYear(curr.start)); const [prevEnd, setPrevEnd] = useState(() => shiftYear(curr.end)); const [selDistricts, setSelDistricts] = useState([]); const [selChannels, setSelChannels] = useState([]); const [selMuseums, setSelMuseums] = useState([]); const [metric, setMetric] = useState('revenue'); const [gran, setGran] = useState('week'); const perm = useMemo(() => { if (!allowedMuseums || !allowedChannels) return []; let d = data; if (allowedMuseums.length) d = d.filter(r => allowedMuseums.includes(r.museum_name)); if (allowedChannels.length) d = d.filter(r => allowedChannels.includes(r.channel)); return d; }, [data, allowedMuseums, allowedChannels]); const availableYears = useMemo(() => { const s = new Set(); perm.forEach(r => r.date && s.add(parseInt(r.date.slice(0,4)))); const a = Array.from(s).sort((a,b) => b-a); return a.length ? a : [new Date().getFullYear()]; }, [perm]); const handleCurr = (s: string, e: string) => { setCurrStart(s); setCurrEnd(e); setPrevStart(shiftYear(s)); setPrevEnd(shiftYear(e)); }; const applyFilters = useCallback((rows: MuseumRecord[]) => { let d = rows; if (selChannels.length) d = d.filter(r => selChannels.includes(r.channel)); if (selMuseums.length) d = d.filter(r => selMuseums.includes(r.museum_name)); if (selDistricts.length) d = d.filter(r => selDistricts.includes(r.district)); return d; }, [selDistricts, selChannels, selMuseums]); const currData = useMemo(() => applyFilters(filterDataByDateRange(perm, currStart, currEnd, {})), [perm, currStart, currEnd, applyFilters]); const prevData = useMemo(() => applyFilters(filterDataByDateRange(perm, prevStart, prevEnd, {})), [perm, prevStart, prevEnd, applyFilters]); const currM = useMemo(() => calculateMetrics(currData, includeVAT), [currData, includeVAT]); const prevM = useMemo(() => calculateMetrics(prevData, includeVAT), [prevData, includeVAT]); const revenueField = includeVAT ? 'revenue_gross' : 'revenue_net'; const getVal = useCallback((rows: MuseumRecord[], m: string) => { if (m==='avgRevenue') { const rev = rows.reduce((s,r) => s + parseFloat(String((r as any)[revenueField]||0)), 0); const vis = rows.reduce((s,r) => s + parseInt(String(r.visits||0)), 0); return vis>0 ? rev/vis : 0; } const f: Record = { revenue:revenueField, visitors:'visits', tickets:'tickets' }; return rows.reduce((s,r) => s + parseFloat(String((r as any)[f[m]]||0)), 0); }, [revenueField]); const channels = useMemo(() => getUniqueChannels(perm), [perm]); const districts = useMemo(() => getUniqueDistricts(perm), [perm]); const museums = useMemo(() => getUniqueMuseums(perm), [perm]); const periodLabel = (s: string, e: string) => { const sy=s.slice(0,4), ey=e.slice(0,4); return sy===ey ? sy : `${L.monthShort[parseInt(s.slice(5,7))-1]} '${sy.slice(-2)}–${L.monthShort[parseInt(e.slice(5,7))-1]} '${ey.slice(-2)}`; }; const trendData = useMemo(() => { const group = (rows: MuseumRecord[], ps: string) => { const s=new Date(ps); const acc: Record = {}; rows.forEach(r => { if (!r.date) return; const diff = Math.floor((new Date(r.date).getTime() - s.getTime()) / 86400000); const key = gran==='month' ? Math.floor(diff/30)+1 : gran==='week' ? Math.floor(diff/7)+1 : diff+1; if (!acc[key]) acc[key]=[]; acc[key].push(r); }); const res: Record = {}; Object.entries(acc).forEach(([k,v]) => res[Number(k)] = getVal(v, metric)); return res; }; const pg = group(prevData, prevStart), cg = group(currData, currStart); const maxK = Math.max(...Object.keys(pg).map(Number), ...Object.keys(cg).map(Number), 1); const labels = Array.from({length:maxK}, (_,i) => gran==='week' ? `W${i+1}` : gran==='month' ? L.monthShort[(new Date(currStart).getMonth()+i)%12] : `D${i+1}` ); return { labels, datasets: [ { label:periodLabel(prevStart,prevEnd), data:labels.map((_,i) => pg[i+1]||0), borderColor:chartColors.muted, backgroundColor:'transparent', borderWidth:2, tension:0.4, pointRadius:gran==='week'?3:1, pointBackgroundColor:chartColors.muted }, { label:periodLabel(currStart,currEnd), data:labels.map((_,i) => cg[i+1]||0), borderColor:chartColors.primary, backgroundColor:chartColors.primary+'15', borderWidth:2, tension:0.4, fill:true, pointRadius:gran==='week'?4:2, pointBackgroundColor:chartColors.primary }, ] }; }, [prevData, currData, prevStart, currStart, prevEnd, currEnd, metric, gran, getVal, L]); const museumData = useMemo(() => { const all = [...new Set(data.map(r => r.museum_name))].filter(Boolean) as string[]; const pb: Record={}, cb: Record={}; all.forEach(m => { pb[m]=getVal(prevData.filter(r => r.museum_name===m), metric); cb[m]=getVal(currData.filter(r => r.museum_name===m), metric); }); const active = all.filter(m => pb[m]>0 || cb[m]>0); return { labels: active, datasets: [ { label:periodLabel(prevStart,prevEnd), data:active.map(m => pb[m]), backgroundColor:chartColors.muted+'cc', borderRadius:4 }, { label:periodLabel(currStart,currEnd), data:active.map(m => cb[m]), backgroundColor:chartColors.primary, borderRadius:4 }, ] }; }, [data, prevData, currData, prevStart, prevEnd, currStart, currEnd, metric, getVal]); const baseOpts = useMemo(() => createBaseOptions(false), []); const { chartOpts } = useMemo(() => { const chartOpts: any = { ...baseOpts, plugins:{ ...baseOpts.plugins, legend:{ position:'top', align:'end', labels:{ boxWidth:12, padding:12 } } } }; return { chartOpts }; }, [baseOpts]); const metricOpts = [ { value:'revenue', label:L.revenue }, { value:'visitors', label:L.visitors }, { value:'tickets', label:L.tickets }, { value:'avgRevenue', label:L.avgRev }, ]; const granOpts = [{ value:'day', label:L.daily }, { value:'week', label:L.weekly }, { value:'month', label:L.monthly }]; const estimatePilgrims = useCallback((s: string, e: string) => { const sd=new Date(s), ed=new Date(e); let total=0, has=false; for (let y=sd.getFullYear(); y<=ed.getFullYear(); y++) { for (let q=1; q<=4; q++) { const qs=new Date(y,(q-1)*3,1), qe=new Date(y,q*3,0); if (qeed) continue; const p=umrahData[y]?.[q]; if(!p) continue; const os=new Date(Math.max(qs.getTime(),sd.getTime())), oe=new Date(Math.min(qe.getTime(),ed.getTime())); total+=p*((oe.getTime()-os.getTime())/86400000+1)/((qe.getTime()-qs.getTime())/86400000+1); has=true; } } return has ? Math.round(total) : null; }, []); const currPilgrims = useMemo(() => estimatePilgrims(currStart, currEnd), [currStart, currEnd, estimatePilgrims]); const prevPilgrims = useMemo(() => estimatePilgrims(prevStart, prevEnd), [prevStart, prevEnd, estimatePilgrims]); const currCapture = currPilgrims ? currM.visitors/currPilgrims*100 : null; const prevCapture = prevPilgrims ? prevM.visitors/prevPilgrims*100 : null; const hasFilters = selDistricts.length>0 || selChannels.length>0 || selMuseums.length>0; const activeFilterCount = selDistricts.length + selChannels.length + selMuseums.length; const [filtersOpen, setFiltersOpen] = useState(false); return (
{L.backLink}

{L.pageTitle}

{L.pageSub}

{L.vs}
{ setPrevStart(s); setPrevEnd(e); }} availableYears={availableYears} L={L} />
{L.filter} {activeFilterCount > 0 && {activeFilterCount}}
{hasFilters && }

{L.keyMetrics}

{currPilgrims!==null && prevPilgrims!==null && } {currCapture!==null && prevCapture!==null && }

{L.trendTitle}

{metricOpts.map(o => )}
{granOpts.map(o => )}

{L.museumTitle}

{metricOpts.map(o => )}
); }