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:
@@ -63,15 +63,24 @@ const generatePresetDates = (year: number): PresetDates => ({
|
||||
'full': { start: `${year}-01-01`, end: `${year}-12-31` }
|
||||
});
|
||||
|
||||
function Comparison({ data, seasons, showDataLabels, setShowDataLabels, includeVAT, setIncludeVAT }: ComparisonProps) {
|
||||
function Comparison({ data, seasons, showDataLabels, setShowDataLabels, includeVAT, setIncludeVAT, allowedMuseums, allowedChannels }: ComparisonProps) {
|
||||
const { t } = useLanguage();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
|
||||
// Permission base filter — applied before any user-facing filter
|
||||
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]);
|
||||
|
||||
// Get available years from data
|
||||
const latestYear = useMemo(() => parseInt(getLatestYear(data)), [data]);
|
||||
const latestYear = useMemo(() => parseInt(getLatestYear(permissionFilteredData)), [permissionFilteredData]);
|
||||
const availableYears = useMemo((): number[] => {
|
||||
const yearsSet = new Set<number>();
|
||||
data.forEach((r: MuseumRecord) => {
|
||||
permissionFilteredData.forEach((r: MuseumRecord) => {
|
||||
const d = r.date || (r as any).Date;
|
||||
if (d) yearsSet.add(new Date(d).getFullYear());
|
||||
});
|
||||
@@ -236,9 +245,9 @@ function Comparison({ data, seasons, showDataLabels, setShowDataLabels, includeV
|
||||
}, [revenueField]);
|
||||
|
||||
// Dynamic lists from data
|
||||
const channels = useMemo(() => getUniqueChannels(data), [data]);
|
||||
const districts = useMemo(() => getUniqueDistricts(data), [data]);
|
||||
const availableMuseums = useMemo(() => getMuseumsForDistrict(data, filters.district), [data, filters.district]);
|
||||
const channels = useMemo(() => getUniqueChannels(permissionFilteredData), [permissionFilteredData]);
|
||||
const districts = useMemo(() => getUniqueDistricts(permissionFilteredData), [permissionFilteredData]);
|
||||
const availableMuseums = useMemo(() => getMuseumsForDistrict(permissionFilteredData, filters.district), [permissionFilteredData, filters.district]);
|
||||
|
||||
// Year-over-year comparison: same dates, previous year
|
||||
// For season presets, try to find the same season name from the previous hijri year
|
||||
@@ -265,14 +274,14 @@ function Comparison({ data, seasons, showDataLabels, setShowDataLabels, includeV
|
||||
return { curr, prev };
|
||||
}, [startDate, endDate, preset, seasons]);
|
||||
|
||||
const prevData = useMemo(() =>
|
||||
filterDataByDateRange(data, ranges.prev.start, ranges.prev.end, filters),
|
||||
[data, ranges.prev, filters]
|
||||
const prevData = useMemo(() =>
|
||||
filterDataByDateRange(permissionFilteredData, ranges.prev.start, ranges.prev.end, filters),
|
||||
[permissionFilteredData, ranges.prev, filters]
|
||||
);
|
||||
|
||||
const currData = useMemo(() =>
|
||||
filterDataByDateRange(data, ranges.curr.start, ranges.curr.end, filters),
|
||||
[data, ranges.curr, filters]
|
||||
|
||||
const currData = useMemo(() =>
|
||||
filterDataByDateRange(permissionFilteredData, ranges.curr.start, ranges.curr.end, filters),
|
||||
[permissionFilteredData, ranges.curr, filters]
|
||||
);
|
||||
|
||||
const prevMetrics = useMemo(() => calculateMetrics(prevData, includeVAT), [prevData, includeVAT]);
|
||||
|
||||
Reference in New Issue
Block a user