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:
@@ -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);
|
||||
@@ -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,
|
||||
@@ -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 {
|
||||
@@ -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)}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user