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

@@ -8,15 +8,22 @@ import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { SettingsContent } from '@/components/settings/settings-content'
async function SettingsLoader() {
// Categories that only super admins can access
const SUPER_ADMIN_CATEGORIES = new Set(['AI', 'EMAIL', 'STORAGE', 'SECURITY'])
async function SettingsLoader({ isSuperAdmin }: { isSuperAdmin: boolean }) {
const settings = await prisma.systemSettings.findMany({
orderBy: [{ category: 'asc' }, { key: 'asc' }],
})
// Convert settings array to key-value map
// For secrets, pass a marker but not the actual value
// For non-super-admins, filter out infrastructure categories
const settingsMap: Record<string, string> = {}
settings.forEach((setting) => {
if (!isSuperAdmin && SUPER_ADMIN_CATEGORIES.has(setting.category)) {
return
}
if (setting.isSecret && setting.value) {
// Pass marker for UI to show "existing" state
settingsMap[setting.key] = '********'
@@ -25,7 +32,7 @@ async function SettingsLoader() {
}
})
return <SettingsContent initialSettings={settingsMap} />
return <SettingsContent initialSettings={settingsMap} isSuperAdmin={isSuperAdmin} />
}
function SettingsSkeleton() {
@@ -52,11 +59,13 @@ function SettingsSkeleton() {
export default async function SettingsPage() {
const session = await auth()
// Only super admins can access settings
if (session?.user?.role !== 'SUPER_ADMIN') {
// Only admins (super admin + program admin) can access settings
if (session?.user?.role !== 'SUPER_ADMIN' && session?.user?.role !== 'PROGRAM_ADMIN') {
redirect('/admin')
}
const isSuperAdmin = session?.user?.role === 'SUPER_ADMIN'
return (
<div className="space-y-6">
{/* Header */}
@@ -69,7 +78,7 @@ export default async function SettingsPage() {
{/* Content */}
<Suspense fallback={<SettingsSkeleton />}>
<SettingsLoader />
<SettingsLoader isSuperAdmin={isSuperAdmin} />
</Suspense>
</div>
)