Inline filtering results, select-all across pages, country flags, settings RBAC, and inline role changes

- Round detail: add skeleton loading for filtering stats, inline results table
  with expandable rows, pagination, override/reinstate, CSV export, and tooltip
  on AI summaries button (removes need for separate results page)
- Projects: add select-all-across-pages with Gmail-style banner, show country
  flags with tooltip instead of country codes (table + card views), add listAllIds
  backend endpoint
- Settings: allow PROGRAM_ADMIN access to settings page, restrict infrastructure
  tabs (AI, Email, Storage, Security, Webhooks) to SUPER_ADMIN only
- Members: add inline role change via dropdown submenu in user actions, enforce
  role hierarchy (only super admins can modify admin/super-admin roles) in both
  backend and UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 23:07:38 +01:00
parent 5cae78fe0c
commit 5c8d22ac11
9 changed files with 1257 additions and 197 deletions

View File

@@ -74,7 +74,7 @@ export default function MemberDetailPage() {
const [name, setName] = useState('')
const [role, setRole] = useState<string>('JURY_MEMBER')
const [status, setStatus] = useState<string>('INVITED')
const [status, setStatus] = useState<string>('NONE')
const [expertiseTags, setExpertiseTags] = useState<string[]>([])
const [maxAssignments, setMaxAssignments] = useState<string>('')
const [showSuperAdminConfirm, setShowSuperAdminConfirm] = useState(false)
@@ -96,7 +96,7 @@ export default function MemberDetailPage() {
id: userId,
name: name || null,
role: role as 'SUPER_ADMIN' | 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER' | 'PROGRAM_ADMIN',
status: status as 'INVITED' | 'ACTIVE' | 'SUSPENDED',
status: status as 'NONE' | 'INVITED' | 'ACTIVE' | 'SUSPENDED',
expertiseTags,
maxAssignments: maxAssignments ? parseInt(maxAssignments) : null,
})
@@ -180,11 +180,11 @@ export default function MemberDetailPage() {
<div className="flex items-center gap-2 mt-1">
<p className="text-muted-foreground">{user.email}</p>
<Badge variant={user.status === 'ACTIVE' ? 'success' : user.status === 'SUSPENDED' ? 'destructive' : 'secondary'}>
{user.status}
{user.status === 'NONE' ? 'Not Invited' : user.status}
</Badge>
</div>
</div>
{user.status === 'INVITED' && (
{(user.status === 'NONE' || user.status === 'INVITED') && (
<Button
variant="outline"
onClick={handleSendInvitation}
@@ -235,6 +235,7 @@ export default function MemberDetailPage() {
setRole(v)
}
}}
disabled={!isSuperAdmin && (user.role === 'SUPER_ADMIN' || user.role === 'PROGRAM_ADMIN')}
>
<SelectTrigger id="role">
<SelectValue />
@@ -243,7 +244,9 @@ export default function MemberDetailPage() {
{isSuperAdmin && (
<SelectItem value="SUPER_ADMIN">Super Admin</SelectItem>
)}
<SelectItem value="PROGRAM_ADMIN">Program Admin</SelectItem>
{isSuperAdmin && (
<SelectItem value="PROGRAM_ADMIN">Program Admin</SelectItem>
)}
<SelectItem value="JURY_MEMBER">Jury Member</SelectItem>
<SelectItem value="MENTOR">Mentor</SelectItem>
<SelectItem value="OBSERVER">Observer</SelectItem>
@@ -257,6 +260,7 @@ export default function MemberDetailPage() {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="NONE">Not Invited</SelectItem>
<SelectItem value="INVITED">Invited</SelectItem>
<SelectItem value="ACTIVE">Active</SelectItem>
<SelectItem value="SUSPENDED">Suspended</SelectItem>
@@ -379,6 +383,16 @@ export default function MemberDetailPage() {
<UserActivityLog userId={userId} />
{/* Status Alert */}
{user.status === 'NONE' && (
<Alert>
<Mail className="h-4 w-4" />
<AlertTitle>Not Yet Invited</AlertTitle>
<AlertDescription>
This member was added to the platform via project import but hasn&apos;t been
invited yet. Send them an invitation using the button above.
</AlertDescription>
</Alert>
)}
{user.status === 'INVITED' && (
<Alert>
<Mail className="h-4 w-4" />