feat: add district filter (Hiraa/AsSaffiyah) from static mapping

- ETL writes District column to NocoDB DailySales
- Museums mapped: Hiraa (Revelation, Holy Quraan, Trail, Makkah, VIP)
  AsSaffiyah (Creation Story, Best of Creation)
- District filter added to Dashboard and Comparison (cascades to museum)
- District Performance chart added (desktop + mobile)
- Locale keys added for both EN and AR

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-31 14:08:16 +03:00
parent 4f4559023b
commit 219680fb5e
9 changed files with 120 additions and 9 deletions

View File

@@ -13,6 +13,8 @@ import {
umrahData,
getUniqueChannels,
getUniqueMuseums,
getUniqueDistricts,
getMuseumsForDistrict,
getLatestYear
} from '../services/dataService';
import type { MuseumRecord, ComparisonProps, DateRangeFilters } from '../types';
@@ -106,6 +108,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn
return searchParams.get('to') || `${year}-01-31`;
});
const [filters, setFiltersState] = useState(() => ({
district: searchParams.get('district') || 'all',
channel: searchParams.get('channel') || 'all',
museum: searchParams.get('museum') || 'all'
}));
@@ -124,6 +127,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn
if (newFrom) params.set('from', newFrom);
if (newTo) params.set('to', newTo);
}
if (newFilters?.district && newFilters.district !== 'all') params.set('district', newFilters.district);
if (newFilters?.channel && newFilters.channel !== 'all') params.set('channel', newFilters.channel);
if (newFilters?.museum && newFilters.museum !== 'all') params.set('museum', newFilters.museum);
setSearchParams(params, { replace: true });
@@ -219,7 +223,8 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn
// Dynamic lists from data
const channels = useMemo(() => getUniqueChannels(data), [data]);
const availableMuseums = useMemo(() => getUniqueMuseums(data), [data]);
const districts = useMemo(() => getUniqueDistricts(data), [data]);
const availableMuseums = useMemo(() => getMuseumsForDistrict(data, filters.district), [data, filters.district]);
// Year-over-year comparison: same dates, previous year
const ranges = useMemo(() => ({
@@ -244,7 +249,7 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn
const currMetrics = useMemo(() => calculateMetrics(currData, includeVAT), [currData, includeVAT]);
const hasData = prevData.length > 0 || currData.length > 0;
const resetFilters = () => setFilters({ channel: 'all', museum: 'all' });
const resetFilters = () => setFilters({ district: 'all', channel: 'all', museum: 'all' });
const calcChange = (prev: number, curr: number) => prev === 0 ? (curr > 0 ? Infinity : 0) : ((curr - prev) / prev * 100);
@@ -575,6 +580,12 @@ function Comparison({ data, showDataLabels, setShowDataLabels, includeVAT, setIn
</FilterControls.Group>
</>
)}
<FilterControls.Group label={t('filters.district')}>
<select value={filters.district} onChange={e => setFilters({...filters, district: e.target.value, museum: 'all'})}>
<option value="all">{t('filters.allDistricts')}</option>
{districts.map(d => <option key={d} value={d}>{d}</option>)}
</select>
</FilterControls.Group>
<FilterControls.Group label={t('filters.channel')}>
<select value={filters.channel} onChange={e => setFilters({...filters, channel: e.target.value})}>
<option value="all">{t('filters.allChannels')}</option>