Enable TypeScript strict mode and fix all type errors
All checks were successful
Deploy HiHala Dashboard / deploy (push) Successful in 6s
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:
@@ -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<number>();
|
||||
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<number>(() => {
|
||||
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<number | null>(null);
|
||||
const touchStartCard = useRef<number | null>(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<string, string> = { 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<number, { start: string; end: string }> = {
|
||||
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<number, MuseumRecord[]> = {};
|
||||
|
||||
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<number, number> = {};
|
||||
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<string, number> = {};
|
||||
const currByMuseum: Record<string, number> = {};
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user