feat: add multi-role editor to member detail page
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m42s
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:
@@ -49,6 +49,7 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
import { UserAvatar } from '@/components/shared/user-avatar'
|
import { UserAvatar } from '@/components/shared/user-avatar'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Save,
|
Save,
|
||||||
@@ -137,6 +138,7 @@ export default function MemberDetailPage() {
|
|||||||
const [maxAssignments, setMaxAssignments] = useState<string>('')
|
const [maxAssignments, setMaxAssignments] = useState<string>('')
|
||||||
const [showSuperAdminConfirm, setShowSuperAdminConfirm] = useState(false)
|
const [showSuperAdminConfirm, setShowSuperAdminConfirm] = useState(false)
|
||||||
const [pendingSuperAdminRole, setPendingSuperAdminRole] = useState(false)
|
const [pendingSuperAdminRole, setPendingSuperAdminRole] = useState(false)
|
||||||
|
const [additionalRoles, setAdditionalRoles] = useState<string[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
@@ -146,16 +148,19 @@ export default function MemberDetailPage() {
|
|||||||
setStatus(user.status)
|
setStatus(user.status)
|
||||||
setExpertiseTags(user.expertiseTags || [])
|
setExpertiseTags(user.expertiseTags || [])
|
||||||
setMaxAssignments(user.maxAssignments?.toString() || '')
|
setMaxAssignments(user.maxAssignments?.toString() || '')
|
||||||
|
setAdditionalRoles(user.roles?.filter((r: string) => r !== user.role) || [])
|
||||||
}
|
}
|
||||||
}, [user])
|
}, [user])
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
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({
|
await updateUser.mutateAsync({
|
||||||
id: userId,
|
id: userId,
|
||||||
email: email || undefined,
|
email: email || undefined,
|
||||||
name: name || null,
|
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',
|
status: status as 'NONE' | 'INVITED' | 'ACTIVE' | 'SUSPENDED',
|
||||||
expertiseTags,
|
expertiseTags,
|
||||||
maxAssignments: maxAssignments ? parseInt(maxAssignments) : null,
|
maxAssignments: maxAssignments ? parseInt(maxAssignments) : null,
|
||||||
@@ -622,6 +627,31 @@ export default function MemberDetailPage() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="status">Status</Label>
|
<Label htmlFor="status">Status</Label>
|
||||||
<Select value={status} onValueChange={setStatus}>
|
<Select value={status} onValueChange={setStatus}>
|
||||||
|
|||||||
@@ -547,6 +547,7 @@ export const userRouter = router({
|
|||||||
email: z.string().email().optional(),
|
email: z.string().email().optional(),
|
||||||
name: z.string().optional().nullable(),
|
name: z.string().optional().nullable(),
|
||||||
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER', 'APPLICANT', 'AUDIENCE']).optional(),
|
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(),
|
status: z.enum(['NONE', 'INVITED', 'ACTIVE', 'SUSPENDED']).optional(),
|
||||||
expertiseTags: z.array(z.string()).optional(),
|
expertiseTags: z.array(z.string()).optional(),
|
||||||
maxAssignments: z.number().int().min(1).max(100).optional().nullable(),
|
maxAssignments: z.number().int().min(1).max(100).optional().nullable(),
|
||||||
|
|||||||
Reference in New Issue
Block a user