diff --git a/frontend/src/components/AdminScopeToggle.js b/frontend/src/components/AdminScopeToggle.js
index 03feb8a..d098bb3 100644
--- a/frontend/src/components/AdminScopeToggle.js
+++ b/frontend/src/components/AdminScopeToggle.js
@@ -1,59 +1,164 @@
// AdminScopeToggle.js
-// Two-state toggle for Admin users: "My Teams" vs "All BUs"
-// Controls whether data on Reporting, Compliance, and Exports pages
-// is scoped to the admin's assigned teams or shows everything.
+// Multi-select BU scope picker for Admin users.
+// Allows selecting any combination of teams to filter Reporting/Compliance/Exports.
+// "My Teams" preset selects the admin's assigned bu_teams.
+// "All BUs" preset selects everything.
+// Custom selections are persisted in localStorage.
-import React from 'react';
+import React, { useState, useRef, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';
function AdminScopeToggle() {
- const { isAdmin, adminScope, toggleAdminScope, hasTeams } = useAuth();
+ const { isAdmin, user, adminScope, setAdminScopeTeams, KNOWN_TEAMS, hasTeams } = useAuth();
+ const [open, setOpen] = useState(false);
+ const ref = useRef(null);
- // Only render for Admin users who have teams assigned
- // (if no teams assigned, both modes are identical — no toggle needed)
- if (!isAdmin() || !hasTeams()) return null;
+ // Close dropdown on outside click
+ useEffect(() => {
+ if (!open) return;
+ const handler = (e) => {
+ if (ref.current && !ref.current.contains(e.target)) setOpen(false);
+ };
+ document.addEventListener('mousedown', handler);
+ return () => document.removeEventListener('mousedown', handler);
+ }, [open]);
- const isAllMode = adminScope === 'all';
+ // Only render for Admin users
+ if (!isAdmin()) return null;
+
+ const selectedTeams = adminScope || [];
+ const myTeams = user?.teams || [];
+ const allSelected = selectedTeams.length === KNOWN_TEAMS.length;
+ const isMyTeams = myTeams.length > 0 &&
+ selectedTeams.length === myTeams.length &&
+ myTeams.every(t => selectedTeams.includes(t));
+
+ const toggleTeam = (team) => {
+ const next = selectedTeams.includes(team)
+ ? selectedTeams.filter(t => t !== team)
+ : [...selectedTeams, team];
+ setAdminScopeTeams(next);
+ };
+
+ const selectAll = () => setAdminScopeTeams([...KNOWN_TEAMS]);
+ const selectMyTeams = () => setAdminScopeTeams([...myTeams]);
+
+ // Label for the button
+ let label;
+ if (allSelected || selectedTeams.length === 0) {
+ label = 'All BUs';
+ } else if (isMyTeams) {
+ label = 'My Teams';
+ } else {
+ label = selectedTeams.join(', ');
+ }
return (
-
-
Scope:
+
+
+ {open && (
+
+ {/* Presets */}
+
+
+ {myTeams.length > 0 && (
+
+ )}
+
+
+ {/* Individual team checkboxes */}
+
+ {KNOWN_TEAMS.map(team => {
+ const checked = selectedTeams.includes(team);
+ return (
+
+ );
+ })}
+
+
+ )}
);
}
diff --git a/frontend/src/contexts/AuthContext.js b/frontend/src/contexts/AuthContext.js
index e4e3ffe..b752168 100644
--- a/frontend/src/contexts/AuthContext.js
+++ b/frontend/src/contexts/AuthContext.js
@@ -7,15 +7,33 @@ const KNOWN_TEAMS = ['STEAM', 'ACCESS-ENG', 'ACCESS-OPS', 'INTELDEV'];
const AuthContext = createContext(null);
+// Load admin scope from localStorage — returns array of selected teams
+function loadAdminScope() {
+ try {
+ const saved = localStorage.getItem('admin_bu_scope');
+ if (saved) {
+ const parsed = JSON.parse(saved);
+ if (Array.isArray(parsed)) return parsed;
+ }
+ } catch { /* ignore */ }
+ // Default: null means "not yet initialized" — will be set to user's teams on first load
+ return null;
+}
+
+function saveAdminScope(teams) {
+ try {
+ localStorage.setItem('admin_bu_scope', JSON.stringify(teams));
+ } catch { /* ignore */ }
+}
+
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
- // Admin scope toggle — persisted in localStorage
- const [adminScope, setAdminScope] = useState(
- () => localStorage.getItem('admin_bu_scope') || 'my-teams'
- );
+ // Admin scope — array of currently selected teams for filtering
+ // null = not initialized yet (will default to user's teams after login)
+ const [adminScope, setAdminScope] = useState(loadAdminScope);
// Check if user is authenticated on mount
const checkAuth = useCallback(async () => {
@@ -27,6 +45,12 @@ export function AuthProvider({ children }) {
if (response.ok) {
const data = await response.json();
setUser(data.user);
+ // Initialize admin scope to user's teams if not yet set
+ if (adminScope === null && data.user?.teams?.length > 0) {
+ const initial = data.user.teams;
+ setAdminScope(initial);
+ saveAdminScope(initial);
+ }
} else {
setUser(null);
}
@@ -36,7 +60,7 @@ export function AuthProvider({ children }) {
} finally {
setLoading(false);
}
- }, []);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
checkAuth();
@@ -60,6 +84,12 @@ export function AuthProvider({ children }) {
}
setUser(data.user);
+ // Initialize scope to user's teams on login
+ if (data.user?.teams?.length > 0 && adminScope === null) {
+ const initial = data.user.teams;
+ setAdminScope(initial);
+ saveAdminScope(initial);
+ }
return { success: true };
} catch (err) {
setError(err.message);
@@ -87,7 +117,6 @@ export function AuthProvider({ children }) {
const canWrite = () => isInGroup('Admin', 'Standard_User');
// Check if user can delete a resource
- // Admin: always true; Standard_User: only if they own the resource; others: false
const canDelete = (resource) => {
if (!user) return false;
if (isInGroup('Admin')) return true;
@@ -108,36 +137,51 @@ export function AuthProvider({ children }) {
// Whether the user has any BU teams assigned
const hasTeams = () => (user?.teams?.length ?? 0) > 0;
- // Whether the user is a member of a specific team (or is Admin in "All BUs" mode)
+ // Whether the user is a member of a specific team
const isTeamMember = (team) => {
if (!user) return false;
- if (isInGroup('Admin') && adminScope === 'all') return true;
+ if (isInGroup('Admin')) {
+ // Admin: check against current scope selection
+ const scope = adminScope || [];
+ return scope.length === 0 || scope.includes(team);
+ }
return (user.teams || []).includes(team);
};
- // Toggle admin scope between 'my-teams' and 'all'
- const toggleAdminScope = () => {
- setAdminScope(prev => {
- const next = prev === 'my-teams' ? 'all' : 'my-teams';
- localStorage.setItem('admin_bu_scope', next);
- return next;
- });
+ // Set the admin scope to a specific set of teams
+ const setAdminScopeTeams = (teams) => {
+ setAdminScope(teams);
+ saveAdminScope(teams);
};
// Returns the comma-joined teams string for API query params.
// Empty string means "no filter" (show all).
const getActiveTeamsParam = () => {
if (!user) return '';
- if (isInGroup('Admin') && adminScope === 'all') return '';
+
+ if (isInGroup('Admin')) {
+ const scope = adminScope || [];
+ // If all teams selected or empty, no filter
+ if (scope.length === 0 || scope.length === KNOWN_TEAMS.length) return '';
+ return scope.join(',');
+ }
+
+ // Non-admin: always use their assigned teams
const teams = user.teams || [];
return teams.join(',');
};
// Returns the list of teams available for UI selectors (compliance team picker, etc.)
- // Admin in "All BUs" mode sees all known teams; otherwise scoped to user's teams.
const getAvailableTeams = () => {
if (!user) return [];
- if (isInGroup('Admin') && adminScope === 'all') return KNOWN_TEAMS;
+
+ if (isInGroup('Admin')) {
+ const scope = adminScope || [];
+ // If all selected or empty, show all known teams
+ if (scope.length === 0 || scope.length === KNOWN_TEAMS.length) return KNOWN_TEAMS;
+ return scope;
+ }
+
return user.teams || [];
};
@@ -158,7 +202,7 @@ export function AuthProvider({ children }) {
hasTeams,
isTeamMember,
adminScope,
- toggleAdminScope,
+ setAdminScopeTeams,
getActiveTeamsParam,
getAvailableTeams,
KNOWN_TEAMS,