From 3bcbf72ad6e4c6ce7c35e3df684f0b3b711dbcfe Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 7 May 2026 18:27:15 +0200 Subject: [PATCH] fix(members): replace flat role checkbox grid with assigned-only dropdown + confirm modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous Additional Roles grid laid every role option out as a row of checkboxes regardless of assignment, which made unchecked roles look like roles the user already had — admins almost toggled the wrong role on the wrong user (e.g. nearly granting JURY_MEMBER when looking at an AWARD_MASTER). New layout shows only the roles a user actually has, as removable badges with an X. A "Manage roles" dropdown next to them surfaces the full role list as DropdownMenuCheckboxItems (assigned ones are checked, the primary role is excluded). Toggling any item opens an AlertDialog with add/remove-specific copy that names the user and the dashboard being granted/revoked, so the click is impossible to misread. The change is staged into local additionalRoles state — same flow as before — and persisted on Save. Modal copy spells this out so the admin knows the action isn't applied until they click Save below. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/(admin)/admin/members/[id]/page.tsx | 149 +++++++++++++++++--- 1 file changed, 131 insertions(+), 18 deletions(-) diff --git a/src/app/(admin)/admin/members/[id]/page.tsx b/src/app/(admin)/admin/members/[id]/page.tsx index f3f502c..6c581f3 100644 --- a/src/app/(admin)/admin/members/[id]/page.tsx +++ b/src/app/(admin)/admin/members/[id]/page.tsx @@ -56,6 +56,14 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' import { UserAvatar } from '@/components/shared/user-avatar' import { Checkbox } from '@/components/ui/checkbox' import { @@ -80,6 +88,8 @@ import { Link as LinkIcon, Copy, Check, + Plus, + X, } from 'lucide-react' import { getCountryName, getCountryFlag } from '@/lib/countries' import { formatRelativeTime } from '@/lib/utils' @@ -185,6 +195,10 @@ export default function MemberDetailPage() { const [maxAssignments, setMaxAssignments] = useState('') const [showSuperAdminConfirm, setShowSuperAdminConfirm] = useState(false) const [pendingSuperAdminRole, setPendingSuperAdminRole] = useState(false) + const [pendingAdditionalRole, setPendingAdditionalRole] = useState<{ + role: 'JURY_MEMBER' | 'OBSERVER' | 'MENTOR' + action: 'add' | 'remove' + } | null>(null) const [additionalRoles, setAdditionalRoles] = useState([]) useEffect(() => { @@ -691,26 +705,72 @@ export default function MemberDetailPage() {

- Grant additional dashboard access beyond the primary role + Grant additional dashboard access beyond the primary role. + Click the menu to add or remove a role — you'll be + asked to confirm each change.

-
- {(['JURY_MEMBER', 'OBSERVER', 'MENTOR'] as const) - .filter((r) => r !== role) - .map((r) => ( -
@@ -882,6 +942,59 @@ export default function MemberDetailPage() { {/* Super Admin Confirmation Dialog */} + { if (!open) setPendingAdditionalRole(null) }} + > + + + + {pendingAdditionalRole?.action === 'add' ? 'Add' : 'Remove'}{' '} + {pendingAdditionalRole?.role.replace(/_/g, ' ')} role? + + + {pendingAdditionalRole?.action === 'add' ? ( + <> + This will give {name || user?.name || 'this user'}{' '} + the {pendingAdditionalRole.role.replace(/_/g, ' ')} dashboard + in addition to their primary role. They'll be able to + switch between dashboards from the role switcher. Click + “Save changes” below to apply. + + ) : ( + <> + This will revoke the {pendingAdditionalRole?.role.replace(/_/g, ' ')} + {' '}dashboard from {name || user?.name || 'this user'}. + They'll keep their primary role and any other additional + roles. Click “Save changes” below to apply. + + )} + + + + setPendingAdditionalRole(null)}> + Cancel + + { + if (!pendingAdditionalRole) return + const { role: r, action } = pendingAdditionalRole + if (action === 'add') { + setAdditionalRoles((prev) => + prev.includes(r) ? prev : [...prev, r] + ) + } else { + setAdditionalRoles((prev) => prev.filter((x) => x !== r)) + } + setPendingAdditionalRole(null) + }} + > + {pendingAdditionalRole?.action === 'add' ? 'Add role' : 'Remove role'} + + + + +