diff --git a/src/App.tsx b/src/App.tsx index dded7ba..0bb72e0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ const Dashboard = lazy(() => import('./components/Dashboard')); const Report = lazy(() => import('./components/Report')); import Login from './components/Login'; import LoadingSkeleton from './components/shared/LoadingSkeleton'; -import { fetchData, getCacheStatus, refreshData, getUniqueMuseums, getUniqueChannels } from './services/dataService'; +import { fetchData, getCacheStatus, refreshData, getUniqueMuseums, getUniqueChannels, fetchMuseumTranslations } from './services/dataService'; import { fetchSeasons } from './services/seasonsService'; import { parseAllowed } from './services/usersService'; import { useLanguage } from './contexts/LanguageContext'; @@ -59,6 +59,7 @@ function App() { const [includeVAT, setIncludeVAT] = useState(true); const [dataSource, setDataSource] = useState('museums'); const [seasons, setSeasons] = useState([]); + const [museumTranslations, setMuseumTranslations] = useState>({}); const [theme, setTheme] = useState(() => { if (typeof window !== 'undefined') { return localStorage.getItem('hihala_theme') || 'light'; @@ -118,6 +119,11 @@ function App() { setSeasons(s); }, []); + const loadMuseumTranslations = useCallback(async () => { + const t = await fetchMuseumTranslations(); + setMuseumTranslations(t); + }, []); + // Check auth on mount useEffect(() => { fetch('/auth/check', { credentials: 'include' }) @@ -131,6 +137,7 @@ function App() { setAllowedChannels(parseAllowed(d.allowedChannels)); loadData(); loadSeasons(); + loadMuseumTranslations(); } }) .catch(() => setAuthenticated(false)); @@ -145,6 +152,7 @@ function App() { setAllowedChannels(parseAllowed(rawChannels)); loadData(); loadSeasons(); + loadMuseumTranslations(); }; const handleRefresh = () => { @@ -327,8 +335,8 @@ function App() {
}> - } /> - } /> + } /> + } /> {userRole === 'admin' && } />} {userRole === 'admin' && } />} diff --git a/src/components/Comparison.tsx b/src/components/Comparison.tsx index 92d3e44..c592ba0 100644 --- a/src/components/Comparison.tsx +++ b/src/components/Comparison.tsx @@ -22,6 +22,7 @@ interface Props { includeVAT: boolean; allowedMuseums: string[] | null; allowedChannels: string[] | null; + museumTranslations?: Record; lang?: 'en' | 'ar'; } @@ -65,8 +66,9 @@ function PeriodCard({ role, hint, start, end, variant, onChange, availableYears, } // ─── main page ──────────────────────────────────────────────────── -export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedMuseums, allowedChannels }: Props) { +export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedMuseums, allowedChannels, museumTranslations = {} }: Props) { const { lang: activeLang, setLanguage } = useLanguage(); + const tr = (name: string) => (activeLang === 'ar' && museumTranslations[name]) ? museumTranslations[name] : name; const L = activeLang === 'ar' ? AR : EN; const curr = currentMonth(); const [currStart, setCurrStart] = useState(curr.start); @@ -167,7 +169,7 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM const museumDatasets = museumList.map((museum, idx) => { const mg = group(currData.filter(r => r.museum_name === museum), currStart); return { - label: museum, + label: tr(museum), data: labels.map((_,i) => mg[i+1]||0), borderColor: chartPalette[idx % chartPalette.length], backgroundColor: 'transparent', @@ -200,13 +202,13 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM all.forEach(m => { pb[m]=getVal(prevData.filter(r => r.museum_name===m), metric); cb[m]=getVal(currData.filter(r => r.museum_name===m), metric); }); const active = all.filter(m => pb[m]>0 || cb[m]>0); return { - labels: active, + labels: active.map(tr), datasets: [ { label:periodLabel(prevStart,prevEnd), data:active.map(m => pb[m]), backgroundColor:chartColors.muted+'cc', borderRadius:4 }, { label:periodLabel(currStart,currEnd), data:active.map(m => cb[m]), backgroundColor:chartColors.primary, borderRadius:4 }, ] }; - }, [data, prevData, currData, prevStart, prevEnd, currStart, currEnd, metric, getVal]); + }, [data, prevData, currData, prevStart, prevEnd, currStart, currEnd, metric, getVal, activeLang, museumTranslations]); const baseOpts = useMemo(() => createBaseOptions(showLabels), [showLabels]); const { chartOpts } = useMemo(() => { @@ -326,7 +328,7 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM
- + {hasFilters && }
diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 7a2c57d..7b4575b 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -22,12 +22,14 @@ interface Props { setIncludeVAT: (v: boolean) => void; allowedMuseums: string[] | null; allowedChannels: string[] | null; + museumTranslations?: Record; lang?: 'en' | 'ar'; } // ─── main page ──────────────────────────────────────────────────── -export default function DashboardDemo({ data, seasons: _seasons, includeVAT, setIncludeVAT, allowedMuseums, allowedChannels }: Props) { +export default function DashboardDemo({ data, seasons: _seasons, includeVAT, setIncludeVAT, allowedMuseums, allowedChannels, museumTranslations = {} }: Props) { const { lang: activeLang, setLanguage } = useLanguage(); + const tr = (name: string) => (activeLang === 'ar' && museumTranslations[name]) ? museumTranslations[name] : name; const L = activeLang === 'ar' ? AR : EN; const curr = currentMonth(); const [start, setStart] = useState(curr.start); @@ -128,7 +130,7 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set const museumDatasets = museumList.map((museum, idx) => { const mg = group(filteredData.filter(r => r.museum_name === museum), start); return { - label: museum, + label: tr(museum), data: labels.map((_,i) => mg[i+1]||0), borderColor: chartPalette[idx % chartPalette.length], backgroundColor: 'transparent', @@ -159,8 +161,8 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set const g = groupByMuseum(filteredData, includeVAT); const getM = (d: typeof g[string]) => metric==='visitors' ? d.visitors : metric==='tickets' ? d.tickets : d.revenue; const entries = Object.entries(g).sort((a,b) => getM(b[1]) - getM(a[1])); - return { labels:entries.map(([k]) => k), datasets:[{ label:metric, data:entries.map(([,v]) => getM(v)), backgroundColor:chartPalette, borderRadius:4 }] }; - }, [filteredData, includeVAT, metric]); + return { labels:entries.map(([k]) => tr(k)), datasets:[{ label:metric, data:entries.map(([,v]) => getM(v)), backgroundColor:chartPalette, borderRadius:4 }] }; + }, [filteredData, includeVAT, metric, activeLang, museumTranslations]); const channelData = useMemo(() => { const g = groupByChannel(filteredData, includeVAT); @@ -296,7 +298,7 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set
- + {hasFilters && }
diff --git a/src/components/shared/AltMultiSelect.tsx b/src/components/shared/AltMultiSelect.tsx index 43b7e1b..9269af9 100644 --- a/src/components/shared/AltMultiSelect.tsx +++ b/src/components/shared/AltMultiSelect.tsx @@ -1,11 +1,13 @@ import React, { useState, useRef, useEffect } from 'react'; // ─── multi-select ───────────────────────────────────────────────── -export default function AltMultiSelect({ value, options, onChange, allLabel, countLabel, clearLabel }: { +export default function AltMultiSelect({ value, options, onChange, allLabel, countLabel, clearLabel, labelFn }: { value: string[]; options: string[]; onChange: (vals: string[]) => void; allLabel: string; countLabel: (n: number) => string; clearLabel: string; + labelFn?: (opt: string) => string; }) { + const display = labelFn ?? ((opt: string) => opt); const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { @@ -15,7 +17,7 @@ export default function AltMultiSelect({ value, options, onChange, allLabel, cou }, [open]); const toggle = (opt: string) => onChange(value.includes(opt) ? value.filter(v => v !== opt) : [...value, opt]); - const label = value.length === 0 ? allLabel : value.length === 1 ? value[0] : countLabel(value.length); + const label = value.length === 0 ? allLabel : value.length === 1 ? display(value[0]) : countLabel(value.length); return (
@@ -32,7 +34,7 @@ export default function AltMultiSelect({ value, options, onChange, allLabel, cou ))}
diff --git a/src/services/dataService.ts b/src/services/dataService.ts index f10fbef..b4e6f00 100644 --- a/src/services/dataService.ts +++ b/src/services/dataService.ts @@ -75,6 +75,21 @@ export let umrahData: UmrahData = { 2025: { 1: 15222497, 2: 5443393, 3: 26643148, 4: 31591871 } }; +export async function fetchMuseumTranslations(): Promise> { + try { + const tables = await discoverTableIds(); + if (!tables['Museums']) return {}; + const rows = await fetchNocoDBTable<{ Name: string; NameAr: string }>(tables['Museums']); + const map: Record = {}; + for (const r of rows) { + if (r.Name && r.NameAr) map[r.Name] = r.NameAr; + } + return map; + } catch { + return {}; + } +} + export async function fetchPilgrimStats(): Promise { try { const tables = await discoverTableIds();