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

@@ -61,9 +61,10 @@ function SettingsSkeleton() {
interface SettingsContentProps {
initialSettings: Record<string, string>
isSuperAdmin?: boolean
}
export function SettingsContent({ initialSettings }: SettingsContentProps) {
export function SettingsContent({ initialSettings, isSuperAdmin = true }: SettingsContentProps) {
// We use the initial settings passed from the server
// Forms will refetch on mutation success
@@ -168,10 +169,12 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
<Globe className="h-4 w-4" />
Locale
</TabsTrigger>
<TabsTrigger value="email" className="gap-2 shrink-0">
<Mail className="h-4 w-4" />
Email
</TabsTrigger>
{isSuperAdmin && (
<TabsTrigger value="email" className="gap-2 shrink-0">
<Mail className="h-4 w-4" />
Email
</TabsTrigger>
)}
<TabsTrigger value="notifications" className="gap-2 shrink-0">
<Bell className="h-4 w-4" />
Notif.
@@ -180,18 +183,22 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
<Newspaper className="h-4 w-4" />
Digest
</TabsTrigger>
<TabsTrigger value="security" className="gap-2 shrink-0">
<Shield className="h-4 w-4" />
Security
</TabsTrigger>
{isSuperAdmin && (
<TabsTrigger value="security" className="gap-2 shrink-0">
<Shield className="h-4 w-4" />
Security
</TabsTrigger>
)}
<TabsTrigger value="audit" className="gap-2 shrink-0">
<ShieldAlert className="h-4 w-4" />
Audit
</TabsTrigger>
<TabsTrigger value="ai" className="gap-2 shrink-0">
<Bot className="h-4 w-4" />
AI
</TabsTrigger>
{isSuperAdmin && (
<TabsTrigger value="ai" className="gap-2 shrink-0">
<Bot className="h-4 w-4" />
AI
</TabsTrigger>
)}
<TabsTrigger value="tags" className="gap-2 shrink-0">
<Tags className="h-4 w-4" />
Tags
@@ -200,10 +207,12 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
<BarChart3 className="h-4 w-4" />
Analytics
</TabsTrigger>
<TabsTrigger value="storage" className="gap-2 shrink-0">
<HardDrive className="h-4 w-4" />
Storage
</TabsTrigger>
{isSuperAdmin && (
<TabsTrigger value="storage" className="gap-2 shrink-0">
<HardDrive className="h-4 w-4" />
Storage
</TabsTrigger>
)}
</TabsList>
<div className="lg:flex lg:gap-8">
@@ -230,10 +239,12 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
<div>
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Communication</p>
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
<TabsTrigger value="email" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Mail className="h-4 w-4" />
Email
</TabsTrigger>
{isSuperAdmin && (
<TabsTrigger value="email" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Mail className="h-4 w-4" />
Email
</TabsTrigger>
)}
<TabsTrigger value="notifications" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Bell className="h-4 w-4" />
Notifications
@@ -247,10 +258,12 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
<div>
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Security</p>
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
<TabsTrigger value="security" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Shield className="h-4 w-4" />
Security
</TabsTrigger>
{isSuperAdmin && (
<TabsTrigger value="security" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Shield className="h-4 w-4" />
Security
</TabsTrigger>
)}
<TabsTrigger value="audit" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<ShieldAlert className="h-4 w-4" />
Audit
@@ -260,10 +273,12 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
<div>
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Features</p>
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
<TabsTrigger value="ai" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Bot className="h-4 w-4" />
AI
</TabsTrigger>
{isSuperAdmin && (
<TabsTrigger value="ai" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Bot className="h-4 w-4" />
AI
</TabsTrigger>
)}
<TabsTrigger value="tags" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Tags className="h-4 w-4" />
Tags
@@ -274,35 +289,39 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
</TabsTrigger>
</TabsList>
</div>
<div>
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Infrastructure</p>
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
<TabsTrigger value="storage" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<HardDrive className="h-4 w-4" />
Storage
</TabsTrigger>
</TabsList>
</div>
{isSuperAdmin && (
<div>
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Infrastructure</p>
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
<TabsTrigger value="storage" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<HardDrive className="h-4 w-4" />
Storage
</TabsTrigger>
</TabsList>
</div>
)}
</nav>
</div>
{/* Content area */}
<div className="flex-1 min-w-0">
<TabsContent value="ai" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>AI Configuration</CardTitle>
<CardDescription>
Configure AI-powered features like smart jury assignment
</CardDescription>
</CardHeader>
<CardContent>
<AISettingsForm settings={aiSettings} />
</CardContent>
</Card>
<AIUsageCard />
</TabsContent>
{isSuperAdmin && (
<TabsContent value="ai" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>AI Configuration</CardTitle>
<CardDescription>
Configure AI-powered features like smart jury assignment
</CardDescription>
</CardHeader>
<CardContent>
<AISettingsForm settings={aiSettings} />
</CardContent>
</Card>
<AIUsageCard />
</TabsContent>
)}
<TabsContent value="tags">
<Card>
@@ -350,19 +369,21 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
</Card>
</TabsContent>
<TabsContent value="email">
<Card>
<CardHeader>
<CardTitle>Email Configuration</CardTitle>
<CardDescription>
Configure email settings for notifications and magic links
</CardDescription>
</CardHeader>
<CardContent>
<EmailSettingsForm settings={emailSettings} />
</CardContent>
</Card>
</TabsContent>
{isSuperAdmin && (
<TabsContent value="email">
<Card>
<CardHeader>
<CardTitle>Email Configuration</CardTitle>
<CardDescription>
Configure email settings for notifications and magic links
</CardDescription>
</CardHeader>
<CardContent>
<EmailSettingsForm settings={emailSettings} />
</CardContent>
</Card>
</TabsContent>
)}
<TabsContent value="notifications">
<Card>
@@ -378,33 +399,37 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
</Card>
</TabsContent>
<TabsContent value="storage">
<Card>
<CardHeader>
<CardTitle>File Storage</CardTitle>
<CardDescription>
Configure file upload limits and allowed types
</CardDescription>
</CardHeader>
<CardContent>
<StorageSettingsForm settings={storageSettings} />
</CardContent>
</Card>
</TabsContent>
{isSuperAdmin && (
<TabsContent value="storage">
<Card>
<CardHeader>
<CardTitle>File Storage</CardTitle>
<CardDescription>
Configure file upload limits and allowed types
</CardDescription>
</CardHeader>
<CardContent>
<StorageSettingsForm settings={storageSettings} />
</CardContent>
</Card>
</TabsContent>
)}
<TabsContent value="security">
<Card>
<CardHeader>
<CardTitle>Security Settings</CardTitle>
<CardDescription>
Configure security and access control settings
</CardDescription>
</CardHeader>
<CardContent>
<SecuritySettingsForm settings={securitySettings} />
</CardContent>
</Card>
</TabsContent>
{isSuperAdmin && (
<TabsContent value="security">
<Card>
<CardHeader>
<CardTitle>Security Settings</CardTitle>
<CardDescription>
Configure security and access control settings
</CardDescription>
</CardHeader>
<CardContent>
<SecuritySettingsForm settings={securitySettings} />
</CardContent>
</Card>
</TabsContent>
)}
<TabsContent value="defaults">
<Card>
@@ -502,26 +527,28 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Webhook className="h-4 w-4" />
Webhooks
</CardTitle>
<CardDescription>
Configure webhook endpoints for platform events
</CardDescription>
</CardHeader>
<CardContent>
<Button asChild>
<Link href="/admin/settings/webhooks">
<Webhook className="mr-2 h-4 w-4" />
Manage Webhooks
<ExternalLink className="ml-2 h-3 w-3" />
</Link>
</Button>
</CardContent>
</Card>
{isSuperAdmin && (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Webhook className="h-4 w-4" />
Webhooks
</CardTitle>
<CardDescription>
Configure webhook endpoints for platform events
</CardDescription>
</CardHeader>
<CardContent>
<Button asChild>
<Link href="/admin/settings/webhooks">
<Webhook className="mr-2 h-4 w-4" />
Manage Webhooks
<ExternalLink className="ml-2 h-3 w-3" />
</Link>
</Button>
</CardContent>
</Card>
)}
</div>
</>
)