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
+25
View File
@@ -128,6 +128,7 @@ async function fetchFromNocoDB(): Promise<MuseumRecord[]> {
return {
date,
district: row.District,
museum_name: row.MuseumName,
channel: row.Channel,
visits: row.Visits,
@@ -273,6 +274,7 @@ export async function refreshData(): Promise<FetchResult> {
export function filterData(data: MuseumRecord[], filters: Filters): MuseumRecord[] {
return data.filter(row => {
if (filters.year && filters.year !== 'all' && row.year !== filters.year) return false;
if (filters.district && filters.district !== 'all' && row.district !== filters.district) return false;
if (filters.channel && filters.channel !== 'all' && row.channel !== filters.channel) return false;
if (filters.museum && filters.museum !== 'all' && row.museum_name !== filters.museum) return false;
if (filters.quarter && filters.quarter !== 'all' && row.quarter !== filters.quarter) return false;
@@ -289,6 +291,7 @@ export function filterDataByDateRange(
return data.filter(row => {
if (!row.date) return false;
if (row.date < startDate || row.date > endDate) return false;
if (filters.district && filters.district !== 'all' && row.district !== filters.district) return false;
if (filters.channel && filters.channel !== 'all' && row.channel !== filters.channel) return false;
if (filters.museum && filters.museum !== 'all' && row.museum_name !== filters.museum) return false;
return true;
@@ -408,6 +411,28 @@ export function getUniqueYears(data: MuseumRecord[]): string[] {
return years.sort((a, b) => parseInt(a) - parseInt(b));
}
export function getUniqueDistricts(data: MuseumRecord[]): string[] {
return [...new Set(data.map(r => r.district).filter(Boolean))].sort();
}
export function groupByDistrict(data: MuseumRecord[], includeVAT: boolean = true): Record<string, GroupedData> {
const revenueField = includeVAT ? 'revenue_gross' : 'revenue_net';
const grouped: Record<string, GroupedData> = {};
data.forEach(row => {
if (!row.district) return;
if (!grouped[row.district]) grouped[row.district] = { revenue: 0, visitors: 0, tickets: 0 };
grouped[row.district].revenue += row[revenueField] || 0;
grouped[row.district].visitors += row.visits || 0;
grouped[row.district].tickets += row.tickets || 0;
});
return grouped;
}
export function getMuseumsForDistrict(data: MuseumRecord[], district: string): string[] {
if (district === 'all') return getUniqueMuseums(data);
return [...new Set(data.filter(r => r.district === district).map(r => r.museum_name).filter(Boolean))].sort();
}
export function getUniqueChannels(data: MuseumRecord[]): string[] {
return [...new Set(data.map(r => r.channel).filter(Boolean))].sort();
}