diff --git a/src/components/Comparison.tsx b/src/components/Comparison.tsx index 90b602b..be0eb32 100644 --- a/src/components/Comparison.tsx +++ b/src/components/Comparison.tsx @@ -16,9 +16,31 @@ import { getMuseumsForDistrict, getLatestYear } from '../services/dataService'; +import type { MuseumRecord, ComparisonProps, DateRangeFilters } 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) => ({ +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` }, @@ -40,15 +62,15 @@ const generatePresetDates = (year) => ({ 'full': { start: `${year}-01-01`, end: `${year}-12-31` } }); -function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIncludeVAT }) { +function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIncludeVAT }: ComparisonProps) { const { t } = useLanguage(); const [searchParams, setSearchParams] = useSearchParams(); // Get available years from data - const latestYear = useMemo(() => getLatestYear(data), [data]); + const latestYear = useMemo(() => parseInt(getLatestYear(data)), [data]); const availableYears = useMemo((): number[] => { const yearsSet = new Set(); - data.forEach(r => { + data.forEach((r: MuseumRecord) => { const d = r.date || (r as any).Date; if (d) yearsSet.add(new Date(d).getFullYear()); }); @@ -57,7 +79,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn }, [data]); // Initialize state from URL or defaults - const [selectedYear, setSelectedYearState] = useState(() => { + const [selectedYear, setSelectedYearState] = useState(() => { const urlYear = searchParams.get('year'); return urlYear ? parseInt(urlYear) : latestYear; }); @@ -66,7 +88,8 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn const [preset, setPresetState] = useState(() => searchParams.get('preset') || 'jan'); const [startDate, setStartDateState] = useState(() => { const urlPreset = searchParams.get('preset'); - const year = searchParams.get('year') ? parseInt(searchParams.get('year')) : latestYear; + 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; @@ -75,7 +98,8 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn }); const [endDate, setEndDateState] = useState(() => { const urlPreset = searchParams.get('preset'); - const year = searchParams.get('year') ? parseInt(searchParams.get('year')) : latestYear; + 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; @@ -93,7 +117,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn const [activeCard, setActiveCard] = useState(0); // Update URL with current state - const updateUrl = useCallback((newPreset, newFrom, newTo, newFilters, newYear) => { + 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()); @@ -106,7 +130,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn setSearchParams(params, { replace: true }); }, [setSearchParams, latestYear]); - const setSelectedYear = (year) => { + const setSelectedYear = (year: number) => { setSelectedYearState(year); const newDates = generatePresetDates(year); if (preset !== 'custom' && newDates[preset]) { @@ -116,7 +140,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn updateUrl(preset, null, null, filters, year); }; - const setPreset = (value) => { + const setPreset = (value: string) => { setPresetState(value); if (value !== 'custom' && presetDates[value]) { setStartDateState(presetDates[value].start); @@ -125,19 +149,19 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn } }; - const setStartDate = (value) => { + const setStartDate = (value: string) => { setStartDateState(value); setPresetState('custom'); updateUrl('custom', value, endDate, filters, selectedYear); }; - const setEndDate = (value) => { + const setEndDate = (value: string) => { setEndDateState(value); setPresetState('custom'); updateUrl('custom', startDate, value, filters, selectedYear); }; - const setFilters = (newFilters) => { + const setFilters = (newFilters: DateRangeFilters | ((prev: DateRangeFilters) => DateRangeFilters)) => { const updated = typeof newFilters === 'function' ? newFilters(filters) : newFilters; setFiltersState(updated); updateUrl(preset, startDate, endDate, updated, selectedYear); @@ -149,13 +173,13 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn ]; // Touch swipe handlers - const touchStartChart = useRef(null); - const touchStartCard = useRef(null); + const touchStartChart = useRef(null); + const touchStartCard = useRef(null); - const handleChartTouchStart = (e) => { + const handleChartTouchStart = (e: React.TouchEvent) => { touchStartChart.current = e.touches[0].clientX; }; - const handleChartTouchEnd = (e) => { + const handleChartTouchEnd = (e: React.TouchEvent) => { if (!touchStartChart.current) return; const diff = touchStartChart.current - e.changedTouches[0].clientX; if (Math.abs(diff) > 50) { @@ -183,15 +207,15 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn { value: 'avgRevenue', label: t('metrics.avgRevenue'), field: null, format: 'currency' } ]; - const getMetricValue = useCallback((rows, metric) => { + const getMetricValue = useCallback((rows: MuseumRecord[], metric: string) => { if (metric === 'avgRevenue') { - const revenue = rows.reduce((s, r) => s + parseFloat(r[revenueField] || r.revenue_incl_tax || 0), 0); - const visitors = rows.reduce((s, r) => s + parseInt(r.visits || 0), 0); + const revenue = rows.reduce((s: number, r: MuseumRecord) => s + parseFloat(String((r as any)[revenueField] || r.revenue_incl_tax || 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 = { revenue: revenueField, visitors: 'visits', tickets: 'tickets' }; + const fieldMap: Record = { revenue: revenueField, visitors: 'visits', tickets: 'tickets' }; const field = fieldMap[metric]; - return rows.reduce((s, r) => s + parseFloat(r[field] || r.revenue_incl_tax || 0), 0); + return rows.reduce((s: number, r: MuseumRecord) => s + parseFloat(String((r as any)[field] || r.revenue_incl_tax || 0)), 0); }, [revenueField]); // Dynamic lists from data @@ -203,8 +227,8 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn const ranges = useMemo(() => ({ curr: { start: startDate, end: endDate }, prev: { - start: startDate.replace(/^(\d{4})/, (_, y) => parseInt(y) - 1), - end: endDate.replace(/^(\d{4})/, (_, y) => parseInt(y) - 1) + start: startDate.replace(/^(\d{4})/, (_: string, y: string) => String(parseInt(y) - 1)), + end: endDate.replace(/^(\d{4})/, (_: string, y: string) => String(parseInt(y) - 1)) } }), [startDate, endDate]); @@ -224,11 +248,11 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn const hasData = prevData.length > 0 || currData.length > 0; const resetFilters = () => setFilters({ district: 'all', museum: 'all' }); - const calcChange = (prev, curr) => prev === 0 ? (curr > 0 ? Infinity : 0) : ((curr - prev) / prev * 100); + 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, end) => { - const quarterRanges = { + 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' }, @@ -331,10 +355,10 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn return cards; }, [prevMetrics, currMetrics, pilgrimCounts, captureRates, t]); - const handleCardTouchStart = (e) => { + const handleCardTouchStart = (e: React.TouchEvent) => { touchStartCard.current = e.touches[0].clientX; }; - const handleCardTouchEnd = (e) => { + const handleCardTouchEnd = (e: React.TouchEvent) => { if (!touchStartCard.current) return; const diff = touchStartCard.current - e.changedTouches[0].clientX; if (Math.abs(diff) > 50) { @@ -347,7 +371,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn touchStartCard.current = null; }; - const formatDate = (dateStr) => { + const formatDate = (dateStr: string) => { if (!dateStr) return ''; const [year, month, day] = dateStr.split('-').map(Number); const d = new Date(year, month - 1, day); @@ -355,7 +379,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn }; // Generate period label - shows year if same year, or "MMM YY–MMM YY" if spans years - const getPeriodLabel = useCallback((startDate, endDate) => { + const getPeriodLabel = useCallback((startDate: string, endDate: string) => { if (!startDate || !endDate) return ''; const startYear = startDate.substring(0, 4); const endYear = endDate.substring(0, 4); @@ -374,11 +398,11 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn // Time series chart (daily or weekly) const timeSeriesChart = useMemo(() => { - const groupByPeriod = (periodData, periodStart, metric, granularity) => { + const groupByPeriod = (periodData: MuseumRecord[], periodStart: string, metric: string, granularity: string) => { const start = new Date(periodStart); - const groupedRows = {}; - - periodData.forEach(row => { + 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)); @@ -398,9 +422,9 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn groupedRows[key].push(row); }); - const result = {}; + const result: Record = {}; Object.keys(groupedRows).forEach(key => { - result[key] = getMetricValue(groupedRows[key], metric); + result[Number(key)] = getMetricValue(groupedRows[Number(key)], metric); }); return result; }; @@ -454,7 +478,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn 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 => r.museum_name))].filter(Boolean) as string[]; + const allMuseums = [...new Set(data.map((r: MuseumRecord) => r.museum_name))].filter(Boolean) as string[]; const prevByMuseum: Record = {}; const currByMuseum: Record = {}; allMuseums.forEach(m => { @@ -802,12 +826,12 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn ); } -function MetricCard({ title, prev, curr, change, isCurrency, isPercent, pendingMessage, prevYear, currYear }) { +function MetricCard({ title, prev, curr, change, isCurrency, isPercent, pendingMessage, prevYear, currYear }: MetricCardProps) { const hasPending = prev === null || curr === null; - const isPositive = change >= 0; + const isPositive = (change ?? 0) >= 0; const changeText = (hasPending && pendingMessage) ? null : (change === Infinity || change === null ? '—' : `${isPositive ? '+' : ''}${change.toFixed(1)}%`); - const formatValue = (val) => { + const formatValue = (val: number | null | undefined) => { if (val === null || val === undefined) return '—'; if (isPercent) return val.toFixed(2) + '%'; if (isCurrency) return formatCompactCurrency(val); diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index ce6f937..be52f9b 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -20,17 +20,18 @@ import { getDistrictMuseumMap, getMuseumsForDistrict } from '../services/dataService'; +import type { DashboardProps, Filters, MuseumRecord } from '../types'; -const defaultFilters = { +const defaultFilters: Filters = { year: 'all', district: 'all', museum: 'all', quarter: 'all' }; -const filterKeys = ['year', 'district', 'museum', 'quarter']; +const filterKeys: (keyof Filters)[] = ['year', 'district', 'museum', 'quarter']; -function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setIncludeVAT }) { +function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setIncludeVAT }: DashboardProps) { const { t } = useLanguage(); const [searchParams, setSearchParams] = useSearchParams(); const [pilgrimLoaded, setPilgrimLoaded] = useState(false); @@ -51,7 +52,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc }); // Update both state and URL - const setFilters = (newFilters) => { + const setFilters = (newFilters: Filters | ((prev: Filters) => Filters)) => { const updated = typeof newFilters === 'function' ? newFilters(filters) : newFilters; setFiltersState(updated); @@ -97,7 +98,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc const yoyChange = useMemo(() => { if (filters.year === 'all') return null; const prevYear = String(parseInt(filters.year) - 1); - const prevData = data.filter(row => row.year === prevYear); + const prevData = data.filter((row: MuseumRecord) => 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; @@ -106,7 +107,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc // Revenue trend data (weekly or daily) const trendData = useMemo(() => { const revenueField = includeVAT ? 'revenue_gross' : 'revenue_net'; - const formatLabel = (dateStr) => { + const formatLabel = (dateStr: string) => { if (!dateStr) return ''; const [year, month, day] = dateStr.split('-').map(Number); const d = new Date(year, month - 1, day); @@ -166,7 +167,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc filteredData.forEach(row => { const date = row.date; if (!dailyData[date]) dailyData[date] = 0; - dailyData[date] += Number(row[revenueField] || row.revenue_incl_tax || 0); + dailyData[date] += Number((row as unknown as Record)[revenueField] || row.revenue_incl_tax || 0); }); const days = Object.keys(dailyData).sort(); const revenueValues = days.map(d => dailyData[d]); @@ -228,21 +229,21 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc // 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 d2024 = data.filter((row: MuseumRecord) => row.year === '2024'); + const d2025 = data.filter((row: MuseumRecord) => 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)), + data: quarters.map(q => d2024.filter((r: MuseumRecord) => r.quarter === q.slice(1)).reduce((s: number, r: MuseumRecord) => s + parseFloat(String(r[revenueField as keyof MuseumRecord] || 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)), + data: quarters.map(q => d2025.filter((r: MuseumRecord) => r.quarter === q.slice(1)).reduce((s: number, r: MuseumRecord) => s + parseFloat(String(r[revenueField as keyof MuseumRecord] || r.revenue_incl_tax || 0)), 0)), backgroundColor: chartColors.primary, borderRadius: 4 } @@ -252,17 +253,17 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc // Capture rate const captureRateData = useMemo(() => { - const labels = []; - const rates = []; - const pilgrimCounts = []; + const labels: string[] = []; + const rates: number[] = []; + const pilgrimCounts: number[] = []; [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); + let qData = data.filter((r: MuseumRecord) => r.year === String(year) && r.quarter === String(q)); + if (filters.district !== 'all') qData = qData.filter((r: MuseumRecord) => r.district === filters.district); + if (filters.museum !== 'all') qData = qData.filter((r: MuseumRecord) => r.museum_name === filters.museum); + const visitors = qData.reduce((s: number, r: MuseumRecord) => s + parseInt(String(r.visits || 0)), 0); labels.push(`Q${q} ${year}`); rates.push((visitors / pilgrims * 100)); pilgrimCounts.push(pilgrims); @@ -286,7 +287,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc yAxisID: 'y', datalabels: { display: showDataLabels, - formatter: (value) => value.toFixed(2) + '%', + formatter: (value: number) => value.toFixed(2) + '%', color: '#1e293b', backgroundColor: 'rgba(255, 255, 255, 0.9)', borderRadius: 3, @@ -312,7 +313,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc order: 1, datalabels: { display: showDataLabels, - formatter: (value) => (value / 1000000).toFixed(2) + 'M', + formatter: (value: number) => (value / 1000000).toFixed(2) + 'M', color: '#1e293b', backgroundColor: 'rgba(255, 255, 255, 0.9)', borderRadius: 3, @@ -329,23 +330,23 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc // 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'); + const d2024 = data.filter((row: MuseumRecord) => row.year === '2024'); + const d2025 = data.filter((row: MuseumRecord) => 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)); + let q2024 = d2024.filter((r: MuseumRecord) => r.quarter === String(q)); + let q2025 = d2025.filter((r: MuseumRecord) => r.quarter === String(q)); if (filters.district !== 'all') { - q2024 = q2024.filter(r => r.district === filters.district); - q2025 = q2025.filter(r => r.district === filters.district); + q2024 = q2024.filter((r: MuseumRecord) => r.district === filters.district); + q2025 = q2025.filter((r: MuseumRecord) => 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); + q2024 = q2024.filter((r: MuseumRecord) => r.museum_name === filters.museum); + q2025 = q2025.filter((r: MuseumRecord) => 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 rev24 = q2024.reduce((s: number, r: MuseumRecord) => s + parseFloat(String(r[revenueField as keyof MuseumRecord] || r.revenue_incl_tax || 0)), 0); + const rev25 = q2025.reduce((s: number, r: MuseumRecord) => s + parseFloat(String(r[revenueField as keyof MuseumRecord] || r.revenue_incl_tax || 0)), 0); + const vis24 = q2024.reduce((s: number, r: MuseumRecord) => s + parseInt(String(r.visits || 0)), 0); + const vis25 = q2025.reduce((s: number, r: MuseumRecord) => s + parseInt(String(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; @@ -545,7 +546,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc tooltip: { ...baseOptions.plugins.tooltip, callbacks: { - label: (ctx) => { + label: (ctx: { dataset: { label?: string }; parsed: { y: number } }) => { if (ctx.dataset.label === 'Capture Rate (%)') { return `Capture Rate: ${ctx.parsed.y.toFixed(2)}%`; } @@ -560,7 +561,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc type: 'linear', position: 'left', grid: { color: chartColors.grid }, - ticks: { font: { size: 12 }, color: '#94a3b8', callback: (v) => v.toFixed(1) + '%' }, + ticks: { font: { size: 12 }, color: '#94a3b8', callback: (v: number | string) => Number(v).toFixed(1) + '%' }, border: { display: false }, title: { display: true, text: 'Capture Rate (%)', font: { size: 12 }, color: chartColors.secondary } }, @@ -568,7 +569,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc type: 'linear', position: 'right', grid: { drawOnChartArea: false }, - ticks: { font: { size: 12 }, color: '#94a3b8', callback: (v) => (v / 1000000).toFixed(0) + 'M' }, + ticks: { font: { size: 12 }, color: '#94a3b8', callback: (v: number | string) => (Number(v) / 1000000).toFixed(0) + 'M' }, border: { display: false }, title: { display: true, text: 'Pilgrims', font: { size: 12 }, color: chartColors.tertiary } } @@ -651,7 +652,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc tooltip: { ...baseOptions.plugins.tooltip, callbacks: { - label: (ctx) => { + label: (ctx: { dataset: { label?: string }; parsed: { y: number } }) => { if (ctx.dataset.label === 'Capture Rate (%)') { return `Capture Rate: ${ctx.parsed.y.toFixed(2)}%`; } @@ -666,14 +667,14 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc type: 'linear', position: 'left', grid: { color: chartColors.grid }, - ticks: { font: { size: 13 }, color: '#94a3b8', callback: (v) => v.toFixed(1) + '%' }, + ticks: { font: { size: 13 }, color: '#94a3b8', callback: (v: number | string) => Number(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' }, + ticks: { font: { size: 13 }, color: '#94a3b8', callback: (v: number | string) => (Number(v) / 1000000).toFixed(0) + 'M' }, border: { display: false } } } diff --git a/src/components/Slides.tsx b/src/components/Slides.tsx index 2a32876..3926898 100644 --- a/src/components/Slides.tsx +++ b/src/components/Slides.tsx @@ -12,31 +12,69 @@ import { getMuseumsForDistrict } from '../services/dataService'; import JSZip from 'jszip'; +import type { + MuseumRecord, + DistrictMuseumMap, + SlideConfig, + ChartTypeOption, + MetricOption, + MetricFieldInfo, + SlidesProps +} from '../types'; -function Slides({ data }) { +interface SlideEditorProps { + slide: SlideConfig; + onUpdate: (updates: Partial) => void; + districts: string[]; + districtMuseumMap: DistrictMuseumMap; + data: MuseumRecord[]; + chartTypes: ChartTypeOption[]; + metrics: MetricOption[]; +} + +interface SlidePreviewProps { + slide: SlideConfig; + data: MuseumRecord[]; + districts: string[]; + districtMuseumMap: DistrictMuseumMap; + metrics: MetricOption[]; +} + +interface PreviewModeProps { + slides: SlideConfig[]; + data: MuseumRecord[]; + districts: string[]; + districtMuseumMap: DistrictMuseumMap; + currentSlide: number; + setCurrentSlide: React.Dispatch>; + onExit: () => void; + metrics: MetricOption[]; +} + +function Slides({ data }: SlidesProps) { const { t } = useLanguage(); - - const CHART_TYPES = useMemo(() => [ + + const CHART_TYPES: ChartTypeOption[] = useMemo(() => [ { id: 'trend', label: t('slides.revenueTrend'), icon: '📈' }, { id: 'museum-bar', label: t('slides.byMuseum'), icon: '📊' }, { id: 'kpi-cards', label: t('slides.kpiSummary'), icon: '🎯' }, { id: 'comparison', label: t('slides.yoyComparison'), icon: '⚖️' } ], [t]); - const METRICS = useMemo(() => [ + const METRICS: MetricOption[] = useMemo(() => [ { id: 'revenue', label: t('metrics.revenue'), field: 'revenue_incl_tax' }, { id: 'visitors', label: t('metrics.visitors'), field: 'visits' }, { id: 'tickets', label: t('metrics.tickets'), field: 'tickets' } ], [t]); - const [slides, setSlides] = useState([]); - const [editingSlide, setEditingSlide] = useState(null); + const [slides, setSlides] = useState([]); + const [editingSlide, setEditingSlide] = useState(null); const [previewMode, setPreviewMode] = useState(false); const [currentPreviewSlide, setCurrentPreviewSlide] = useState(0); const districts = useMemo(() => getUniqueDistricts(data), [data]); const districtMuseumMap = useMemo(() => getDistrictMuseumMap(data), [data]); - const defaultSlideConfig = { + const defaultSlideConfig: Omit = { title: 'Slide Title', chartType: 'trend', metric: 'revenue', @@ -48,7 +86,7 @@ function Slides({ data }) { }; const addSlide = () => { - const newSlide = { + const newSlide: SlideConfig = { id: Date.now(), ...defaultSlideConfig, title: `Slide ${slides.length + 1}` @@ -57,16 +95,16 @@ function Slides({ data }) { setEditingSlide(newSlide.id); }; - const updateSlide = (id, updates) => { + const updateSlide = (id: number, updates: Partial) => { setSlides(slides.map(s => s.id === id ? { ...s, ...updates } : s)); }; - const removeSlide = (id) => { + const removeSlide = (id: number) => { setSlides(slides.filter(s => s.id !== id)); if (editingSlide === id) setEditingSlide(null); }; - const moveSlide = (id, direction) => { + const moveSlide = (id: number, direction: number) => { const index = slides.findIndex(s => s.id === id); if ((direction === -1 && index === 0) || (direction === 1 && index === slides.length - 1)) return; const newSlides = [...slides]; @@ -74,10 +112,10 @@ function Slides({ data }) { setSlides(newSlides); }; - const duplicateSlide = (id) => { + const duplicateSlide = (id: number) => { const slide = slides.find(s => s.id === id); if (slide) { - const newSlide = { ...slide, id: Date.now(), title: `${slide.title} (copy)` }; + const newSlide: SlideConfig = { ...slide, id: Date.now(), title: `${slide.title} (copy)` }; const index = slides.findIndex(s => s.id === id); const newSlides = [...slides]; newSlides.splice(index + 1, 0, newSlide); @@ -87,7 +125,7 @@ function Slides({ data }) { const exportAsHTML = async () => { const zip = new JSZip(); - + // Generate HTML for each slide const slidesHTML = slides.map((slide, index) => { return generateSlideHTML(slide, index, data, districts, districtMuseumMap); @@ -103,21 +141,21 @@ function Slides({ data }) {