Add file requirements per round and super admin promotion via UI

Part A: File Requirements per Round
- New FileRequirement model with name, description, accepted MIME types, max size, required flag, sort order
- Added requirementId FK to ProjectFile for linking uploads to requirements
- Backend CRUD (create/update/delete/reorder) in file router with audit logging
- Mime type validation and team member upload authorization in applicant router
- Admin UI: FileRequirementsEditor component in round edit page
- Applicant UI: RequirementUploadSlot/List components in submission detail and team pages
- Viewer UI: RequirementChecklist with fulfillment status in file-viewer

Part B: Super Admin Promotion
- Added SUPER_ADMIN to role enums in user create/update/bulkCreate with guards
- Member detail page: SUPER_ADMIN dropdown option with AlertDialog confirmation
- Invite page: SUPER_ADMIN option visible only to super admins

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 23:01:33 +01:00
parent e73a676412
commit 829acf8d4e
12 changed files with 1229 additions and 62 deletions

View File

@@ -69,7 +69,7 @@ import {
import { cn } from '@/lib/utils'
type Step = 'input' | 'preview' | 'sending' | 'complete'
type Role = 'PROGRAM_ADMIN' | 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER'
type Role = 'SUPER_ADMIN' | 'PROGRAM_ADMIN' | 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER'
interface Assignment {
projectId: string
@@ -99,6 +99,7 @@ interface ParsedUser {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const ROLE_LABELS: Record<Role, string> = {
SUPER_ADMIN: 'Super Admin',
PROGRAM_ADMIN: 'Program Admin',
JURY_MEMBER: 'Jury Member',
MENTOR: 'Mentor',
@@ -399,16 +400,18 @@ export default function MemberInvitePage() {
const name = nameKey ? row[nameKey]?.trim() : undefined
const rawRole = roleKey ? row[roleKey]?.trim().toUpperCase() : ''
const role: Role =
rawRole === 'PROGRAM_ADMIN'
? 'PROGRAM_ADMIN'
: rawRole === 'MENTOR'
? 'MENTOR'
: rawRole === 'OBSERVER'
? 'OBSERVER'
: 'JURY_MEMBER'
rawRole === 'SUPER_ADMIN'
? 'SUPER_ADMIN'
: rawRole === 'PROGRAM_ADMIN'
? 'PROGRAM_ADMIN'
: rawRole === 'MENTOR'
? 'MENTOR'
: rawRole === 'OBSERVER'
? 'OBSERVER'
: 'JURY_MEMBER'
const isValidFormat = emailRegex.test(email)
const isDuplicate = email ? seenEmails.has(email) : false
const isUnauthorizedAdmin = role === 'PROGRAM_ADMIN' && !isSuperAdmin
const isUnauthorizedAdmin = (role === 'PROGRAM_ADMIN' || role === 'SUPER_ADMIN') && !isSuperAdmin
if (isValidFormat && !isDuplicate && email) seenEmails.add(email)
return {
email,
@@ -646,6 +649,11 @@ export default function MemberInvitePage() {
<SelectValue />
</SelectTrigger>
<SelectContent>
{isSuperAdmin && (
<SelectItem value="SUPER_ADMIN">
Super Admin
</SelectItem>
)}
{isSuperAdmin && (
<SelectItem value="PROGRAM_ADMIN">
Program Admin