import React, { useState, useMemo, useCallback } from 'react'; import { Line, Bar } from 'react-chartjs-2'; import { chartColors, createBaseOptions } from '../config/chartConfig'; import { useLanguage } from '../contexts/LanguageContext'; import { filterDataByDateRange, calculateMetrics, formatCompact, formatCompactCurrency, getUniqueDistricts, getDistrictMuseumMap, getMuseumsForDistrict } from '../services/dataService'; import JSZip from 'jszip'; function Slides({ data }) { const { t } = useLanguage(); const CHART_TYPES = 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(() => [ { 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 [previewMode, setPreviewMode] = useState(false); const [currentPreviewSlide, setCurrentPreviewSlide] = useState(0); const districts = useMemo(() => getUniqueDistricts(data), [data]); const districtMuseumMap = useMemo(() => getDistrictMuseumMap(data), [data]); const defaultSlideConfig = { title: 'Slide Title', chartType: 'trend', metric: 'revenue', startDate: '2026-01-01', endDate: '2026-01-31', district: 'all', museum: 'all', showComparison: false }; const addSlide = () => { const newSlide = { id: Date.now(), ...defaultSlideConfig, title: `Slide ${slides.length + 1}` }; setSlides([...slides, newSlide]); setEditingSlide(newSlide.id); }; const updateSlide = (id, updates) => { setSlides(slides.map(s => s.id === id ? { ...s, ...updates } : s)); }; const removeSlide = (id) => { setSlides(slides.filter(s => s.id !== id)); if (editingSlide === id) setEditingSlide(null); }; const moveSlide = (id, direction) => { const index = slides.findIndex(s => s.id === id); if ((direction === -1 && index === 0) || (direction === 1 && index === slides.length - 1)) return; const newSlides = [...slides]; [newSlides[index], newSlides[index + direction]] = [newSlides[index + direction], newSlides[index]]; setSlides(newSlides); }; const duplicateSlide = (id) => { const slide = slides.find(s => s.id === id); if (slide) { const newSlide = { ...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); setSlides(newSlides); } }; 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); }).join('\n'); const fullHTML = ` HiHala Data Presentation ${slidesHTML} `; zip.file('presentation.html', fullHTML); const blob = await zip.generateAsync({ type: 'blob' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'hihala-presentation.zip'; a.click(); URL.revokeObjectURL(url); }; if (previewMode) { return ( setPreviewMode(false)} metrics={METRICS} /> ); } return (

{t('slides.title')}

{t('slides.subtitle')}

{slides.length > 0 && ( <> )}

{t('slides.slidesCount')} ({slides.length})

{slides.length === 0 ? (

{t('slides.noSlides')}

) : (
{slides.map((slide, index) => (
setEditingSlide(slide.id)} >
{index + 1}
{CHART_TYPES.find(c => c.id === slide.chartType)?.icon}
{slide.title}
))}
)}
{editingSlide && ( s.id === editingSlide)} onUpdate={(updates) => updateSlide(editingSlide, updates)} districts={districts} districtMuseumMap={districtMuseumMap} data={data} chartTypes={CHART_TYPES} metrics={METRICS} /> )}
); } function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data, chartTypes, metrics }) { const { t } = useLanguage(); const availableMuseums = useMemo(() => getMuseumsForDistrict(districtMuseumMap, slide.district), [districtMuseumMap, slide.district] ); return (
onUpdate({ title: e.target.value })} placeholder={t('slides.slideTitle')} />
{chartTypes.map(type => ( ))}
onUpdate({ startDate: e.target.value })} />
onUpdate({ endDate: e.target.value })} />
{slide.chartType === 'comparison' && (
)}

{t('slides.preview')}

); } // Static field mapping for charts (Chart.js labels don't need i18n) const METRIC_FIELDS = { revenue: { field: 'revenue_incl_tax', label: 'Revenue' }, visitors: { field: 'visits', label: 'Visitors' }, tickets: { field: 'tickets', label: 'Tickets' } }; function SlidePreview({ slide, data, districts, districtMuseumMap, metrics }) { const { t } = useLanguage(); const filteredData = useMemo(() => filterDataByDateRange(data, slide.startDate, slide.endDate, { district: slide.district, museum: slide.museum }), [data, slide.startDate, slide.endDate, slide.district, slide.museum] ); const metricsData = useMemo(() => calculateMetrics(filteredData), [filteredData]); const baseOptions = useMemo(() => createBaseOptions(false), []); const getMetricValue = useCallback((rows, metric) => { const fieldMap = { revenue: 'revenue_incl_tax', visitors: 'visits', tickets: 'tickets' }; return rows.reduce((s, r) => s + parseFloat(r[fieldMap[metric]] || 0), 0); }, []); const trendData = useMemo(() => { const grouped = {}; filteredData.forEach(row => { if (!row.date) return; const weekStart = row.date.substring(0, 10); if (!grouped[weekStart]) grouped[weekStart] = []; grouped[weekStart].push(row); }); const sortedDates = Object.keys(grouped).sort(); const metricLabel = metrics?.find(m => m.id === slide.metric)?.label || METRIC_FIELDS[slide.metric]?.label || slide.metric; return { labels: sortedDates.map(d => d.substring(5)), datasets: [{ label: metricLabel, data: sortedDates.map(d => getMetricValue(grouped[d], slide.metric)), borderColor: chartColors.primary, backgroundColor: chartColors.primary + '20', fill: true, tension: 0.4 }] }; }, [filteredData, slide.metric, getMetricValue, metrics]); const museumData = useMemo(() => { const byMuseum = {}; filteredData.forEach(row => { if (!row.museum_name) return; if (!byMuseum[row.museum_name]) byMuseum[row.museum_name] = []; byMuseum[row.museum_name].push(row); }); const museums = Object.keys(byMuseum).sort(); const metricLabel = metrics?.find(m => m.id === slide.metric)?.label || METRIC_FIELDS[slide.metric]?.label || slide.metric; return { labels: museums, datasets: [{ label: metricLabel, data: museums.map(m => getMetricValue(byMuseum[m], slide.metric)), backgroundColor: chartColors.primary, borderRadius: 6 }] }; }, [filteredData, slide.metric, getMetricValue, metrics]); if (slide.chartType === 'kpi-cards') { return (
{formatCompactCurrency(metricsData.revenue)}
{t('metrics.revenue')}
{formatCompact(metricsData.visitors)}
{t('metrics.visitors')}
{formatCompact(metricsData.tickets)}
{t('metrics.tickets')}
); } if (slide.chartType === 'museum-bar') { return (
); } return (
); } function PreviewMode({ slides, data, districts, districtMuseumMap, currentSlide, setCurrentSlide, onExit, metrics }) { const { t } = useLanguage(); const handleKeyDown = useCallback((e) => { if (e.key === 'ArrowRight' || e.key === ' ') { setCurrentSlide(prev => Math.min(prev + 1, slides.length - 1)); } else if (e.key === 'ArrowLeft') { setCurrentSlide(prev => Math.max(prev - 1, 0)); } else if (e.key === 'Escape') { onExit(); } }, [slides.length, setCurrentSlide, onExit]); React.useEffect(() => { window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [handleKeyDown]); const slide = slides[currentSlide]; return (

{slide?.title}

{slide && }
{currentSlide + 1} / {slides.length}
); } // Helper functions for HTML export function generateSlideHTML(slide, index, data, districts, districtMuseumMap) { const chartType = slide.chartType; const canvasId = `chart-${index}`; return `

${slide.title}

${formatDateRange(slide.startDate, slide.endDate)}

${chartType === 'kpi-cards' ? generateKPIHTML(slide, data) : `
`}
Slide ${index + 1}
`; } function generateKPIHTML(slide, data) { const filtered = filterDataByDateRange(data, slide.startDate, slide.endDate, { district: slide.district, museum: slide.museum }); const metrics = calculateMetrics(filtered); return `
${formatCompactCurrency(metrics.revenue)}
Revenue
${formatCompact(metrics.visitors)}
Visitors
${formatCompact(metrics.tickets)}
Tickets
`; } function generateChartScripts(slides, data, districts, districtMuseumMap) { return slides.map((slide, index) => { if (slide.chartType === 'kpi-cards') return ''; const filtered = filterDataByDateRange(data, slide.startDate, slide.endDate, { district: slide.district, museum: slide.museum }); const chartConfig = generateChartConfig(slide, filtered); return ` new Chart(document.getElementById('chart-${index}'), ${JSON.stringify(chartConfig)}); `; }).join('\n'); } function generateChartConfig(slide, data) { const fieldMap = { revenue: 'revenue_incl_tax', visitors: 'visits', tickets: 'tickets' }; const field = fieldMap[slide.metric]; if (slide.chartType === 'museum-bar') { const byMuseum = {}; data.forEach(row => { if (!row.museum_name) return; byMuseum[row.museum_name] = (byMuseum[row.museum_name] || 0) + parseFloat(row[field] || 0); }); const museums = Object.keys(byMuseum).sort(); return { type: 'bar', data: { labels: museums, datasets: [{ data: museums.map(m => byMuseum[m]), backgroundColor: '#3b82f6', borderRadius: 6 }] }, options: { indexAxis: 'y', plugins: { legend: { display: false } } } }; } // Default: trend line const grouped = {}; data.forEach(row => { if (!row.date) return; grouped[row.date] = (grouped[row.date] || 0) + parseFloat(row[field] || 0); }); const dates = Object.keys(grouped).sort(); return { type: 'line', data: { labels: dates.map(d => d.substring(5)), datasets: [{ data: dates.map(d => grouped[d]), borderColor: '#3b82f6', backgroundColor: 'rgba(59,130,246,0.1)', fill: true, tension: 0.4 }] }, options: { plugins: { legend: { display: false } } } }; } function formatDateRange(start, end) { const s = new Date(start); const e = new Date(end); const opts = { month: 'short', day: 'numeric', year: 'numeric' }; return `${s.toLocaleDateString('en-US', opts)} – ${e.toLocaleDateString('en-US', opts)}`; } export default Slides;