Enable TypeScript strict mode and fix all type errors
All checks were successful
Deploy HiHala Dashboard / deploy (push) Successful in 6s

- Enable strict: true in tsconfig.json (was false)
- Add proper interfaces for all component props (Dashboard, Comparison, Slides)
- Add SlideConfig, ChartTypeOption, MetricOption types
- Type all function parameters, callbacks, and state variables
- Fix dynamic property access with proper keyof assertions
- 233 type errors resolved across 5 files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-25 18:17:09 +03:00
parent 30ea4b6ecb
commit c8567da75f
7 changed files with 254 additions and 163 deletions

View File

@@ -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<SlideConfig>) => 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<React.SetStateAction<number>>;
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<SlideConfig[]>([]);
const [editingSlide, setEditingSlide] = useState<number | null>(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<SlideConfig, 'id'> = {
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<SlideConfig>) => {
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 }) {
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0f172a; }
.slide {
width: 100vw; height: 100vh;
.slide {
width: 100vw; height: 100vh;
display: flex; flex-direction: column;
justify-content: center; align-items: center;
padding: 60px; background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
page-break-after: always;
}
.slide-title {
color: #f8fafc; font-size: 2.5rem; font-weight: 600;
.slide-title {
color: #f8fafc; font-size: 2.5rem; font-weight: 600;
margin-bottom: 40px; text-align: center;
}
.slide-subtitle {
color: #94a3b8; font-size: 1.1rem; margin-bottom: 30px;
}
.chart-container {
.chart-container {
width: 100%; max-width: 900px; height: 400px;
background: rgba(255,255,255,0.03); border-radius: 16px;
padding: 30px;
@@ -134,8 +172,8 @@ function Slides({ data }) {
.kpi-label { color: #94a3b8; font-size: 1rem; margin-top: 8px; }
.logo { position: absolute; bottom: 30px; right: 40px; opacity: 0.6; }
.logo svg { height: 30px; }
.slide-number {
position: absolute; bottom: 30px; left: 40px;
.slide-number {
position: absolute; bottom: 30px; left: 40px;
color: #475569; font-size: 0.9rem;
}
@media print {
@@ -153,7 +191,7 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)}
</html>`;
zip.file('presentation.html', fullHTML);
const blob = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
@@ -165,7 +203,7 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)}
if (previewMode) {
return (
<PreviewMode
<PreviewMode
slides={slides}
data={data}
districts={districts}
@@ -221,8 +259,8 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)}
) : (
<div className="slides-thumbnails">
{slides.map((slide, index) => (
<div
key={slide.id}
<div
key={slide.id}
className={`slide-thumbnail ${editingSlide === slide.id ? 'active' : ''}`}
onClick={() => setEditingSlide(slide.id)}
>
@@ -243,7 +281,7 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)}
{editingSlide && (
<SlideEditor
slide={slides.find(s => s.id === editingSlide)}
slide={slides.find(s => s.id === editingSlide)!}
onUpdate={(updates) => updateSlide(editingSlide, updates)}
districts={districts}
districtMuseumMap={districtMuseumMap}
@@ -257,9 +295,9 @@ ${generateChartScripts(slides, data, districts, districtMuseumMap)}
);
}
function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data, chartTypes, metrics }) {
function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data, chartTypes, metrics }: SlideEditorProps) {
const { t } = useLanguage();
const availableMuseums = useMemo(() =>
const availableMuseums = useMemo(() =>
getMuseumsForDistrict(districtMuseumMap, slide.district),
[districtMuseumMap, slide.district]
);
@@ -268,9 +306,9 @@ function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data, char
<div className="slide-editor">
<div className="editor-section">
<label>{t('slides.slideTitle')}</label>
<input
type="text"
value={slide.title}
<input
type="text"
value={slide.title}
onChange={e => onUpdate({ title: e.target.value })}
placeholder={t('slides.slideTitle')}
/>
@@ -279,7 +317,7 @@ function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data, char
<div className="editor-section">
<label>{t('slides.chartType')}</label>
<div className="chart-type-grid">
{chartTypes.map(type => (
{chartTypes.map((type: ChartTypeOption) => (
<button
key={type.id}
className={`chart-type-btn ${slide.chartType === type.id ? 'active' : ''}`}
@@ -295,7 +333,7 @@ function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data, char
<div className="editor-section">
<label>{t('slides.metric')}</label>
<select value={slide.metric} onChange={e => onUpdate({ metric: e.target.value })}>
{metrics.map(m => <option key={m.id} value={m.id}>{m.label}</option>)}
{metrics.map((m: MetricOption) => <option key={m.id} value={m.id}>{m.label}</option>)}
</select>
</div>
@@ -315,14 +353,14 @@ function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data, char
<label>{t('filters.district')}</label>
<select value={slide.district} onChange={e => onUpdate({ district: e.target.value, museum: 'all' })}>
<option value="all">{t('filters.allDistricts')}</option>
{districts.map(d => <option key={d} value={d}>{d}</option>)}
{districts.map((d: string) => <option key={d} value={d}>{d}</option>)}
</select>
</div>
<div className="editor-section">
<label>{t('filters.museum')}</label>
<select value={slide.museum} onChange={e => onUpdate({ museum: e.target.value })}>
<option value="all">{t('filters.allMuseums')}</option>
{availableMuseums.map(m => <option key={m} value={m}>{m}</option>)}
{availableMuseums.map((m: string) => <option key={m} value={m}>{m}</option>)}
</select>
</div>
</div>
@@ -330,9 +368,9 @@ function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data, char
{slide.chartType === 'comparison' && (
<div className="editor-section">
<label>
<input
type="checkbox"
checked={slide.showComparison}
<input
type="checkbox"
checked={slide.showComparison}
onChange={e => onUpdate({ showComparison: e.target.checked })}
/>
{t('slides.showYoY')}
@@ -349,15 +387,15 @@ function SlideEditor({ slide, onUpdate, districts, districtMuseumMap, data, char
}
// Static field mapping for charts (Chart.js labels don't need i18n)
const METRIC_FIELDS = {
const METRIC_FIELDS: Record<string, MetricFieldInfo> = {
revenue: { field: 'revenue_incl_tax', label: 'Revenue' },
visitors: { field: 'visits', label: 'Visitors' },
tickets: { field: 'tickets', label: 'Tickets' }
};
function SlidePreview({ slide, data, districts, districtMuseumMap, metrics }) {
function SlidePreview({ slide, data, districts, districtMuseumMap, metrics }: SlidePreviewProps) {
const { t } = useLanguage();
const filteredData = useMemo(() =>
const filteredData = useMemo(() =>
filterDataByDateRange(data, slide.startDate, slide.endDate, {
district: slide.district,
museum: slide.museum
@@ -368,22 +406,22 @@ function SlidePreview({ slide, data, districts, districtMuseumMap, metrics }) {
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 getMetricValue = useCallback((rows: MuseumRecord[], metric: string) => {
const fieldMap: Record<string, string> = { revenue: 'revenue_incl_tax', visitors: 'visits', tickets: 'tickets' };
return rows.reduce((s: number, r: MuseumRecord) => s + parseFloat(String((r as unknown as Record<string, unknown>)[fieldMap[metric]] || 0)), 0);
}, []);
const trendData = useMemo(() => {
const grouped = {};
const grouped: Record<string, MuseumRecord[]> = {};
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;
const metricLabel = metrics?.find((m: MetricOption) => m.id === slide.metric)?.label || METRIC_FIELDS[slide.metric]?.label || slide.metric;
return {
labels: sortedDates.map(d => d.substring(5)),
datasets: [{
@@ -398,15 +436,15 @@ function SlidePreview({ slide, data, districts, districtMuseumMap, metrics }) {
}, [filteredData, slide.metric, getMetricValue, metrics]);
const museumData = useMemo(() => {
const byMuseum = {};
const byMuseum: Record<string, MuseumRecord[]> = {};
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;
const metricLabel = metrics?.find((m: MetricOption) => m.id === slide.metric)?.label || METRIC_FIELDS[slide.metric]?.label || slide.metric;
return {
labels: museums,
datasets: [{
@@ -452,13 +490,13 @@ function SlidePreview({ slide, data, districts, districtMuseumMap, metrics }) {
);
}
function PreviewMode({ slides, data, districts, districtMuseumMap, currentSlide, setCurrentSlide, onExit, metrics }) {
function PreviewMode({ slides, data, districts, districtMuseumMap, currentSlide, setCurrentSlide, onExit, metrics }: PreviewModeProps) {
const { t } = useLanguage();
const handleKeyDown = useCallback((e) => {
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === 'ArrowRight' || e.key === ' ') {
setCurrentSlide(prev => Math.min(prev + 1, slides.length - 1));
setCurrentSlide((prev: number) => Math.min(prev + 1, slides.length - 1));
} else if (e.key === 'ArrowLeft') {
setCurrentSlide(prev => Math.max(prev - 1, 0));
setCurrentSlide((prev: number) => Math.max(prev - 1, 0));
} else if (e.key === 'Escape') {
onExit();
}
@@ -483,8 +521,8 @@ function PreviewMode({ slides, data, districts, districtMuseumMap, currentSlide,
</div>
</div>
<div className="preview-controls">
<button onClick={() => setCurrentSlide(prev => Math.max(prev - 1, 0))} disabled={currentSlide === 0}></button>
<button onClick={() => setCurrentSlide(prev => Math.min(prev + 1, slides.length - 1))} disabled={currentSlide === slides.length - 1}></button>
<button onClick={() => setCurrentSlide((prev: number) => Math.max(prev - 1, 0))} disabled={currentSlide === 0}></button>
<button onClick={() => setCurrentSlide((prev: number) => Math.min(prev + 1, slides.length - 1))} disabled={currentSlide === slides.length - 1}></button>
<button onClick={onExit}>{t('slides.exit')}</button>
</div>
</div>
@@ -492,10 +530,10 @@ function PreviewMode({ slides, data, districts, districtMuseumMap, currentSlide,
}
// Helper functions for HTML export
function generateSlideHTML(slide, index, data, districts, districtMuseumMap) {
function generateSlideHTML(slide: SlideConfig, index: number, data: MuseumRecord[], districts: string[], districtMuseumMap: DistrictMuseumMap): string {
const chartType = slide.chartType;
const canvasId = `chart-${index}`;
return `
<div class="slide" id="slide-${index}">
<h1 class="slide-title">${slide.title}</h1>
@@ -510,13 +548,13 @@ function generateSlideHTML(slide, index, data, districts, districtMuseumMap) {
</div>`;
}
function generateKPIHTML(slide, data) {
function generateKPIHTML(slide: SlideConfig, data: MuseumRecord[]): string {
const filtered = filterDataByDateRange(data, slide.startDate, slide.endDate, {
district: slide.district,
museum: slide.museum
});
const metrics = calculateMetrics(filtered);
return `
<div class="kpi-grid">
<div class="kpi-card">
@@ -534,40 +572,40 @@ function generateKPIHTML(slide, data) {
</div>`;
}
function generateChartScripts(slides, data, districts, districtMuseumMap) {
return slides.map((slide, index) => {
function generateChartScripts(slides: SlideConfig[], data: MuseumRecord[], districts: string[], districtMuseumMap: DistrictMuseumMap): string {
return slides.map((slide: SlideConfig, index: number) => {
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' };
function generateChartConfig(slide: SlideConfig, data: MuseumRecord[]): object {
const fieldMap: Record<string, keyof MuseumRecord> = { revenue: 'revenue_incl_tax', visitors: 'visits', tickets: 'tickets' };
const field = fieldMap[slide.metric];
if (slide.chartType === 'museum-bar') {
const byMuseum = {};
data.forEach(row => {
const byMuseum: Record<string, number> = {};
data.forEach((row: MuseumRecord) => {
if (!row.museum_name) return;
byMuseum[row.museum_name] = (byMuseum[row.museum_name] || 0) + parseFloat(row[field] || 0);
byMuseum[row.museum_name] = (byMuseum[row.museum_name] || 0) + parseFloat(String(row[field] || 0));
});
const museums = Object.keys(byMuseum).sort();
return {
type: 'bar',
data: {
labels: museums,
datasets: [{
datasets: [{
data: museums.map(m => byMuseum[m]),
backgroundColor: '#3b82f6',
borderRadius: 6
@@ -576,15 +614,15 @@ function generateChartConfig(slide, data) {
options: { indexAxis: 'y', plugins: { legend: { display: false } } }
};
}
// Default: trend line
const grouped = {};
data.forEach(row => {
const grouped: Record<string, number> = {};
data.forEach((row: MuseumRecord) => {
if (!row.date) return;
grouped[row.date] = (grouped[row.date] || 0) + parseFloat(row[field] || 0);
grouped[row.date] = (grouped[row.date] || 0) + parseFloat(String(row[field] || 0));
});
const dates = Object.keys(grouped).sort();
return {
type: 'line',
data: {