feat: add multi-role editor to member detail page
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m42s

Adds an "Additional Roles" checkbox section below the primary Role
dropdown. Admins can now grant a user multiple dashboard views (e.g.,
Observer + Jury Member) without changing their primary role. The roles
array is saved alongside the primary role and used by the role switcher.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-04-07 21:08:32 -04:00
parent 2d6cee394f
commit a51241f7ff
2 changed files with 32 additions and 1 deletions

View File

@@ -49,6 +49,7 @@ import {
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { UserAvatar } from '@/components/shared/user-avatar'
import { Checkbox } from '@/components/ui/checkbox'
import {
ArrowLeft,
Save,
@@ -137,6 +138,7 @@ export default function MemberDetailPage() {
const [maxAssignments, setMaxAssignments] = useState<string>('')
const [showSuperAdminConfirm, setShowSuperAdminConfirm] = useState(false)
const [pendingSuperAdminRole, setPendingSuperAdminRole] = useState(false)
const [additionalRoles, setAdditionalRoles] = useState<string[]>([])
useEffect(() => {
if (user) {
@@ -146,16 +148,19 @@ export default function MemberDetailPage() {
setStatus(user.status)
setExpertiseTags(user.expertiseTags || [])
setMaxAssignments(user.maxAssignments?.toString() || '')
setAdditionalRoles(user.roles?.filter((r: string) => r !== user.role) || [])
}
}, [user])
const handleSave = async () => {
try {
const allRoles = [role, ...additionalRoles.filter((r) => r !== role)] as Array<'SUPER_ADMIN' | 'PROGRAM_ADMIN' | 'AWARD_MASTER' | 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER' | 'APPLICANT' | 'AUDIENCE'>
await updateUser.mutateAsync({
id: userId,
email: email || undefined,
name: name || null,
role: role as 'SUPER_ADMIN' | 'PROGRAM_ADMIN' | 'AWARD_MASTER' | 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER' | 'APPLICANT' | 'AUDIENCE',
role: role as typeof allRoles[number],
roles: allRoles,
status: status as 'NONE' | 'INVITED' | 'ACTIVE' | 'SUSPENDED',
expertiseTags,
maxAssignments: maxAssignments ? parseInt(maxAssignments) : null,
@@ -622,6 +627,31 @@ export default function MemberDetailPage() {
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Additional Roles</Label>
<p className="text-xs text-muted-foreground">
Grant additional dashboard access beyond the primary role
</p>
<div className="grid grid-cols-2 gap-2">
{(['JURY_MEMBER', 'OBSERVER', 'MENTOR', 'AWARD_MASTER'] as const)
.filter((r) => r !== role)
.map((r) => (
<label key={r} className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox
checked={additionalRoles.includes(r)}
onCheckedChange={(checked) => {
if (checked) {
setAdditionalRoles((prev) => [...prev, r])
} else {
setAdditionalRoles((prev) => prev.filter((x) => x !== r))
}
}}
/>
{r.replace(/_/g, ' ')}
</label>
))}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select value={status} onValueChange={setStatus}>

View File

@@ -547,6 +547,7 @@ export const userRouter = router({
email: z.string().email().optional(),
name: z.string().optional().nullable(),
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER', 'APPLICANT', 'AUDIENCE']).optional(),
roles: z.array(z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER', 'APPLICANT', 'AUDIENCE'])).optional(),
status: z.enum(['NONE', 'INVITED', 'ACTIVE', 'SUSPENDED']).optional(),
expertiseTags: z.array(z.string()).optional(),
maxAssignments: z.number().int().min(1).max(100).optional().nullable(),