chore: migrate to TypeScript

- Convert all .js files to .tsx/.ts
- Add types for data structures (MuseumRecord, Metrics, etc.)
- Add type declarations for react-chartjs-2
- Configure tsconfig with relaxed strictness for gradual adoption
- All components now use TypeScript
This commit is contained in:
fahed
2026-02-04 13:45:50 +03:00
parent e98bebd60b
commit 868f46fc6e
18 changed files with 484 additions and 121 deletions

View File

@@ -1,9 +1,23 @@
import React, { useRef } from 'react';
import React, { useRef, useState, ReactNode } from 'react';
import JSZip from 'jszip';
interface ExportableChartProps {
children: ReactNode;
filename?: string;
title?: string;
className?: string;
controls?: ReactNode;
}
// Wrapper component that adds PNG export to any chart
export function ExportableChart({ children, filename = 'chart', title = '', className = '', controls = null }) {
const chartRef = useRef(null);
export function ExportableChart({
children,
filename = 'chart',
title = '',
className = '',
controls = null
}: ExportableChartProps) {
const chartRef = useRef<HTMLDivElement>(null);
const exportAsPNG = () => {
const chartContainer = chartRef.current;
@@ -15,6 +29,7 @@ export function ExportableChart({ children, filename = 'chart', title = '', clas
// Create a new canvas with white background and title
const exportCanvas = document.createElement('canvas');
const ctx = exportCanvas.getContext('2d');
if (!ctx) return;
// Set dimensions with padding and title space
const padding = 24;
@@ -75,7 +90,7 @@ export function ExportableChart({ children, filename = 'chart', title = '', clas
}
// Utility function to export all charts from a container as a ZIP
export async function exportAllCharts(containerSelector, zipFilename = 'charts') {
export async function exportAllCharts(containerSelector: string, zipFilename: string = 'charts'): Promise<void> {
const container = document.querySelector(containerSelector);
if (!container) return;
@@ -93,6 +108,7 @@ export async function exportAllCharts(containerSelector, zipFilename = 'charts')
// Create export canvas with white background and title
const exportCanvas = document.createElement('canvas');
const ctx = exportCanvas.getContext('2d');
if (!ctx) continue;
const padding = 32;
const titleHeight = 56;
@@ -129,9 +145,16 @@ export async function exportAllCharts(containerSelector, zipFilename = 'charts')
URL.revokeObjectURL(url);
}
interface ExportAllButtonProps {
containerSelector: string;
zipFilename?: string;
label: string;
loadingLabel: string;
}
// Button component for exporting all charts
export function ExportAllButton({ containerSelector, zipFilename, label, loadingLabel }) {
const [exporting, setExporting] = React.useState(false);
export function ExportAllButton({ containerSelector, zipFilename = 'charts', label, loadingLabel }: ExportAllButtonProps) {
const [exporting, setExporting] = useState(false);
const handleExport = async () => {
setExporting(true);

View File

@@ -46,11 +46,13 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn
// Get available years from data
const latestYear = useMemo(() => getLatestYear(data), [data]);
const availableYears = useMemo(() => {
const years = [...new Set(data.map(r => {
const d = r.date || r.Date;
return d ? new Date(d).getFullYear() : null;
}).filter(Boolean))].sort((a, b) => b - a);
const availableYears = useMemo((): number[] => {
const yearsSet = new Set<number>();
data.forEach(r => {
const d = r.date || (r as any).Date;
if (d) yearsSet.add(new Date(d).getFullYear());
});
const years = Array.from(yearsSet).sort((a, b) => b - a);
return years.length ? years : [new Date().getFullYear()];
}, [data]);
@@ -267,7 +269,17 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn
const pilgrimCounts = quarterData?.pilgrims || null;
// Build cards array dynamically
const metricCards = useMemo(() => {
interface CardData {
title: string;
prev: number | null;
curr: number | null;
change: number | null;
isCurrency?: boolean;
isPercent?: boolean;
pendingMessage?: string;
}
const metricCards = useMemo((): CardData[] => {
const revenueChange = calcChange(prevMetrics.revenue, currMetrics.revenue);
const visitorsChange = calcChange(prevMetrics.visitors, currMetrics.visitors);
const ticketsChange = calcChange(prevMetrics.tickets, currMetrics.tickets);
@@ -275,7 +287,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn
const pilgrimsChange = pilgrimCounts ? calcChange(pilgrimCounts.prev || 0, pilgrimCounts.curr || 0) : null;
const captureRateChange = captureRates ? calcChange(captureRates.prev || 0, captureRates.curr || 0) : null;
const cards = [
const cards: CardData[] = [
{ title: t('metrics.revenue'), prev: prevMetrics.revenue, curr: currMetrics.revenue, change: revenueChange, isCurrency: true },
{ title: t('metrics.visitors'), prev: prevMetrics.visitors, curr: currMetrics.visitors, change: visitorsChange },
{ title: t('metrics.tickets'), prev: prevMetrics.tickets, curr: currMetrics.tickets, change: ticketsChange },
@@ -340,7 +352,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn
periodData.forEach(row => {
if (!row.date) return;
const rowDate = new Date(row.date);
const daysDiff = Math.floor((rowDate - start) / (1000 * 60 * 60 * 24));
const daysDiff = Math.floor((rowDate.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
let key;
if (granularity === 'month') {
@@ -413,9 +425,9 @@ 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);
const prevByMuseum = {};
const currByMuseum = {};
const allMuseums = [...new Set(data.map(r => r.museum_name))].filter(Boolean) as string[];
const prevByMuseum: Record<string, number> = {};
const currByMuseum: Record<string, number> = {};
allMuseums.forEach(m => {
const prevRows = prevData.filter(r => r.museum_name === m);
const currRows = currData.filter(r => r.museum_name === m);
@@ -434,7 +446,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn
}, [data, prevData, currData, ranges, chartMetric, getMetricValue, getPeriodLabel]);
const baseOptions = useMemo(() => createBaseOptions(showDataLabels), [showDataLabels]);
const chartOptions = {
const chartOptions: any = {
...baseOptions,
plugins: {
...baseOptions.plugins,

View File

@@ -125,11 +125,11 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
};
} else {
// Daily granularity
const dailyData = {};
const dailyData: Record<string, number> = {};
filteredData.forEach(row => {
const date = row.date;
if (!dailyData[date]) dailyData[date] = 0;
dailyData[date] += parseFloat(row[revenueField] || row.revenue_incl_tax || 0);
dailyData[date] += Number(row[revenueField] || row.revenue_incl_tax || 0);
});
const days = Object.keys(dailyData).sort();
return {

View File

@@ -601,10 +601,10 @@ function generateChartConfig(slide, data) {
};
}
function formatDateRange(start, end) {
function formatDateRange(start: string, end: string): string {
const s = new Date(start);
const e = new Date(end);
const opts = { month: 'short', day: 'numeric', year: 'numeric' };
const opts: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' };
return `${s.toLocaleDateString('en-US', opts)} ${e.toLocaleDateString('en-US', opts)}`;
}