feat: per-user museum and channel access control
- PATCH /api/users/:id route to update user permissions - Auth session stores and returns allowedMuseums/allowedChannels - User type gains AllowedMuseums/AllowedChannels (JSON string fields) - parseAllowed() with fail-closed semantics (empty string → null → no data) - Dashboard/Comparison apply permission base filter before user filters - Filter dropdowns (museums, channels, years, districts) derived from permission-filtered data — restricted users only see their allowed options - Settings UserRow component with inline checkbox pickers for access config - Access badges in users table showing current restriction summary Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,7 @@ const defaultFilters: Filters = {
|
||||
|
||||
const filterKeys: (keyof Filters)[] = ['year', 'district', 'quarter'];
|
||||
|
||||
function Dashboard({ data, seasons, userRole, showDataLabels, setShowDataLabels, includeVAT, setIncludeVAT }: DashboardProps) {
|
||||
function Dashboard({ data, seasons, userRole, showDataLabels, setShowDataLabels, includeVAT, setIncludeVAT, allowedMuseums, allowedChannels }: DashboardProps) {
|
||||
const { t } = useLanguage();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [pilgrimLoaded, setPilgrimLoaded] = useState(false);
|
||||
@@ -87,7 +87,17 @@ function Dashboard({ data, seasons, userRole, showDataLabels, setShowDataLabels,
|
||||
const [districtChartType, setDistrictChartType] = useState<'bar' | 'pie'>('pie');
|
||||
const [districtDisplayMode, setDistrictDisplayMode] = useState<'absolute' | 'percent'>('absolute');
|
||||
|
||||
const filteredData = useMemo(() => filterData(data, filters), [data, filters]);
|
||||
// Permission base filter — applied before any user-facing filter
|
||||
// null = corrupted value → fail-closed (show nothing)
|
||||
const permissionFilteredData = useMemo(() => {
|
||||
if (allowedMuseums === null || allowedChannels === null) return [];
|
||||
let d = data;
|
||||
if (allowedMuseums.length > 0) d = d.filter(r => allowedMuseums.includes(r.museum_name));
|
||||
if (allowedChannels.length > 0) d = d.filter(r => allowedChannels.includes(r.channel));
|
||||
return d;
|
||||
}, [data, allowedMuseums, allowedChannels]);
|
||||
|
||||
const filteredData = useMemo(() => filterData(permissionFilteredData, filters), [permissionFilteredData, filters]);
|
||||
|
||||
const seasonFilteredData = useMemo(() => {
|
||||
if (!selectedSeason) return filteredData;
|
||||
@@ -118,19 +128,19 @@ function Dashboard({ data, seasons, userRole, showDataLabels, setShowDataLabels,
|
||||
}, [t]);
|
||||
|
||||
// Dynamic lists from data
|
||||
const years = useMemo(() => getUniqueYears(data), [data]);
|
||||
const districts = useMemo(() => getUniqueDistricts(data), [data]);
|
||||
const channels = useMemo(() => getUniqueChannels(data), [data]);
|
||||
const availableMuseums = useMemo(() => getMuseumsForDistrict(data, filters.district), [data, filters.district]);
|
||||
const years = useMemo(() => getUniqueYears(permissionFilteredData), [permissionFilteredData]);
|
||||
const districts = useMemo(() => getUniqueDistricts(permissionFilteredData), [permissionFilteredData]);
|
||||
const channels = useMemo(() => getUniqueChannels(permissionFilteredData), [permissionFilteredData]);
|
||||
const availableMuseums = useMemo(() => getMuseumsForDistrict(permissionFilteredData, filters.district), [permissionFilteredData, filters.district]);
|
||||
|
||||
const yoyChange = useMemo(() => {
|
||||
if (filters.year === 'all') return null;
|
||||
const prevYear = String(parseInt(filters.year) - 1);
|
||||
const prevData = data.filter((row: MuseumRecord) => row.year === prevYear);
|
||||
const prevData = permissionFilteredData.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;
|
||||
}, [data, filters.year, metrics.revenue, includeVAT]);
|
||||
}, [permissionFilteredData, filters.year, metrics.revenue, includeVAT]);
|
||||
|
||||
// Revenue trend data (weekly or daily)
|
||||
const trendData = useMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user