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

@@ -20,17 +20,18 @@ import {
getDistrictMuseumMap,
getMuseumsForDistrict
} from '../services/dataService';
import type { DashboardProps, Filters, MuseumRecord } from '../types';
const defaultFilters = {
const defaultFilters: Filters = {
year: 'all',
district: 'all',
museum: 'all',
quarter: 'all'
};
const filterKeys = ['year', 'district', 'museum', 'quarter'];
const filterKeys: (keyof Filters)[] = ['year', 'district', 'museum', 'quarter'];
function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setIncludeVAT }) {
function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setIncludeVAT }: DashboardProps) {
const { t } = useLanguage();
const [searchParams, setSearchParams] = useSearchParams();
const [pilgrimLoaded, setPilgrimLoaded] = useState(false);
@@ -51,7 +52,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
});
// Update both state and URL
const setFilters = (newFilters) => {
const setFilters = (newFilters: Filters | ((prev: Filters) => Filters)) => {
const updated = typeof newFilters === 'function' ? newFilters(filters) : newFilters;
setFiltersState(updated);
@@ -97,7 +98,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
const yoyChange = useMemo(() => {
if (filters.year === 'all') return null;
const prevYear = String(parseInt(filters.year) - 1);
const prevData = data.filter(row => row.year === prevYear);
const prevData = data.filter((row: MuseumRecord) => row.year === prevYear);
if (prevData.length === 0) return null;
const prevMetrics = calculateMetrics(prevData, includeVAT);
return prevMetrics.revenue > 0 ? ((metrics.revenue - prevMetrics.revenue) / prevMetrics.revenue * 100) : null;
@@ -106,7 +107,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
// Revenue trend data (weekly or daily)
const trendData = useMemo(() => {
const revenueField = includeVAT ? 'revenue_gross' : 'revenue_net';
const formatLabel = (dateStr) => {
const formatLabel = (dateStr: string) => {
if (!dateStr) return '';
const [year, month, day] = dateStr.split('-').map(Number);
const d = new Date(year, month - 1, day);
@@ -166,7 +167,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
filteredData.forEach(row => {
const date = row.date;
if (!dailyData[date]) dailyData[date] = 0;
dailyData[date] += Number(row[revenueField] || row.revenue_incl_tax || 0);
dailyData[date] += Number((row as unknown as Record<string, unknown>)[revenueField] || row.revenue_incl_tax || 0);
});
const days = Object.keys(dailyData).sort();
const revenueValues = days.map(d => dailyData[d]);
@@ -228,21 +229,21 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
// Quarterly YoY
const quarterlyYoYData = useMemo(() => {
const revenueField = includeVAT ? 'revenue_gross' : 'revenue_net';
const d2024 = data.filter(row => row.year === '2024');
const d2025 = data.filter(row => row.year === '2025');
const d2024 = data.filter((row: MuseumRecord) => row.year === '2024');
const d2025 = data.filter((row: MuseumRecord) => row.year === '2025');
const quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
return {
labels: quarters,
datasets: [
{
label: '2024',
data: quarters.map(q => d2024.filter(r => r.quarter === q.slice(1)).reduce((s, r) => s + parseFloat(r[revenueField] || r.revenue_incl_tax || 0), 0)),
data: quarters.map(q => d2024.filter((r: MuseumRecord) => r.quarter === q.slice(1)).reduce((s: number, r: MuseumRecord) => s + parseFloat(String(r[revenueField as keyof MuseumRecord] || r.revenue_incl_tax || 0)), 0)),
backgroundColor: chartColors.muted,
borderRadius: 4
},
{
label: '2025',
data: quarters.map(q => d2025.filter(r => r.quarter === q.slice(1)).reduce((s, r) => s + parseFloat(r[revenueField] || r.revenue_incl_tax || 0), 0)),
data: quarters.map(q => d2025.filter((r: MuseumRecord) => r.quarter === q.slice(1)).reduce((s: number, r: MuseumRecord) => s + parseFloat(String(r[revenueField as keyof MuseumRecord] || r.revenue_incl_tax || 0)), 0)),
backgroundColor: chartColors.primary,
borderRadius: 4
}
@@ -252,17 +253,17 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
// Capture rate
const captureRateData = useMemo(() => {
const labels = [];
const rates = [];
const pilgrimCounts = [];
const labels: string[] = [];
const rates: number[] = [];
const pilgrimCounts: number[] = [];
[2024, 2025].forEach(year => {
[1, 2, 3, 4].forEach(q => {
const pilgrims = umrahData[year]?.[q];
if (!pilgrims) return;
let qData = data.filter(r => r.year === String(year) && r.quarter === String(q));
if (filters.district !== 'all') qData = qData.filter(r => r.district === filters.district);
if (filters.museum !== 'all') qData = qData.filter(r => r.museum_name === filters.museum);
const visitors = qData.reduce((s, r) => s + parseInt(r.visits || 0), 0);
let qData = data.filter((r: MuseumRecord) => r.year === String(year) && r.quarter === String(q));
if (filters.district !== 'all') qData = qData.filter((r: MuseumRecord) => r.district === filters.district);
if (filters.museum !== 'all') qData = qData.filter((r: MuseumRecord) => r.museum_name === filters.museum);
const visitors = qData.reduce((s: number, r: MuseumRecord) => s + parseInt(String(r.visits || 0)), 0);
labels.push(`Q${q} ${year}`);
rates.push((visitors / pilgrims * 100));
pilgrimCounts.push(pilgrims);
@@ -286,7 +287,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
yAxisID: 'y',
datalabels: {
display: showDataLabels,
formatter: (value) => value.toFixed(2) + '%',
formatter: (value: number) => value.toFixed(2) + '%',
color: '#1e293b',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderRadius: 3,
@@ -312,7 +313,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
order: 1,
datalabels: {
display: showDataLabels,
formatter: (value) => (value / 1000000).toFixed(2) + 'M',
formatter: (value: number) => (value / 1000000).toFixed(2) + 'M',
color: '#1e293b',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderRadius: 3,
@@ -329,23 +330,23 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
// Quarterly table
const quarterlyTable = useMemo(() => {
const revenueField = includeVAT ? 'revenue_gross' : 'revenue_net';
const d2024 = data.filter(row => row.year === '2024');
const d2025 = data.filter(row => row.year === '2025');
const d2024 = data.filter((row: MuseumRecord) => row.year === '2024');
const d2025 = data.filter((row: MuseumRecord) => row.year === '2025');
return [1, 2, 3, 4].map(q => {
let q2024 = d2024.filter(r => r.quarter === String(q));
let q2025 = d2025.filter(r => r.quarter === String(q));
let q2024 = d2024.filter((r: MuseumRecord) => r.quarter === String(q));
let q2025 = d2025.filter((r: MuseumRecord) => r.quarter === String(q));
if (filters.district !== 'all') {
q2024 = q2024.filter(r => r.district === filters.district);
q2025 = q2025.filter(r => r.district === filters.district);
q2024 = q2024.filter((r: MuseumRecord) => r.district === filters.district);
q2025 = q2025.filter((r: MuseumRecord) => r.district === filters.district);
}
if (filters.museum !== 'all') {
q2024 = q2024.filter(r => r.museum_name === filters.museum);
q2025 = q2025.filter(r => r.museum_name === filters.museum);
q2024 = q2024.filter((r: MuseumRecord) => r.museum_name === filters.museum);
q2025 = q2025.filter((r: MuseumRecord) => r.museum_name === filters.museum);
}
const rev24 = q2024.reduce((s, r) => s + parseFloat(r[revenueField] || r.revenue_incl_tax || 0), 0);
const rev25 = q2025.reduce((s, r) => s + parseFloat(r[revenueField] || r.revenue_incl_tax || 0), 0);
const vis24 = q2024.reduce((s, r) => s + parseInt(r.visits || 0), 0);
const vis25 = q2025.reduce((s, r) => s + parseInt(r.visits || 0), 0);
const rev24 = q2024.reduce((s: number, r: MuseumRecord) => s + parseFloat(String(r[revenueField as keyof MuseumRecord] || r.revenue_incl_tax || 0)), 0);
const rev25 = q2025.reduce((s: number, r: MuseumRecord) => s + parseFloat(String(r[revenueField as keyof MuseumRecord] || r.revenue_incl_tax || 0)), 0);
const vis24 = q2024.reduce((s: number, r: MuseumRecord) => s + parseInt(String(r.visits || 0)), 0);
const vis25 = q2025.reduce((s: number, r: MuseumRecord) => s + parseInt(String(r.visits || 0)), 0);
const revChg = rev24 > 0 ? ((rev25 - rev24) / rev24 * 100) : 0;
const visChg = vis24 > 0 ? ((vis25 - vis24) / vis24 * 100) : 0;
const cap24 = umrahData[2024][q] ? (vis24 / umrahData[2024][q] * 100) : null;
@@ -545,7 +546,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
tooltip: {
...baseOptions.plugins.tooltip,
callbacks: {
label: (ctx) => {
label: (ctx: { dataset: { label?: string }; parsed: { y: number } }) => {
if (ctx.dataset.label === 'Capture Rate (%)') {
return `Capture Rate: ${ctx.parsed.y.toFixed(2)}%`;
}
@@ -560,7 +561,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
type: 'linear',
position: 'left',
grid: { color: chartColors.grid },
ticks: { font: { size: 12 }, color: '#94a3b8', callback: (v) => v.toFixed(1) + '%' },
ticks: { font: { size: 12 }, color: '#94a3b8', callback: (v: number | string) => Number(v).toFixed(1) + '%' },
border: { display: false },
title: { display: true, text: 'Capture Rate (%)', font: { size: 12 }, color: chartColors.secondary }
},
@@ -568,7 +569,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
type: 'linear',
position: 'right',
grid: { drawOnChartArea: false },
ticks: { font: { size: 12 }, color: '#94a3b8', callback: (v) => (v / 1000000).toFixed(0) + 'M' },
ticks: { font: { size: 12 }, color: '#94a3b8', callback: (v: number | string) => (Number(v) / 1000000).toFixed(0) + 'M' },
border: { display: false },
title: { display: true, text: 'Pilgrims', font: { size: 12 }, color: chartColors.tertiary }
}
@@ -651,7 +652,7 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
tooltip: {
...baseOptions.plugins.tooltip,
callbacks: {
label: (ctx) => {
label: (ctx: { dataset: { label?: string }; parsed: { y: number } }) => {
if (ctx.dataset.label === 'Capture Rate (%)') {
return `Capture Rate: ${ctx.parsed.y.toFixed(2)}%`;
}
@@ -666,14 +667,14 @@ function Dashboard({ data, showDataLabels, setShowDataLabels, includeVAT, setInc
type: 'linear',
position: 'left',
grid: { color: chartColors.grid },
ticks: { font: { size: 13 }, color: '#94a3b8', callback: (v) => v.toFixed(1) + '%' },
ticks: { font: { size: 13 }, color: '#94a3b8', callback: (v: number | string) => Number(v).toFixed(1) + '%' },
border: { display: false }
},
y1: {
type: 'linear',
position: 'right',
grid: { drawOnChartArea: false },
ticks: { font: { size: 13 }, color: '#94a3b8', callback: (v) => (v / 1000000).toFixed(0) + 'M' },
ticks: { font: { size: 13 }, color: '#94a3b8', callback: (v: number | string) => (Number(v) / 1000000).toFixed(0) + 'M' },
border: { display: false }
}
}