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

@@ -35,6 +35,16 @@ import {
import { toast } from 'sonner'
import { TagInput } from '@/components/shared/tag-input'
import { UserActivityLog } from '@/components/shared/user-activity-log'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import {
ArrowLeft,
Save,
@@ -51,6 +61,8 @@ export default function MemberDetailPage() {
const userId = params.id as string
const { data: user, isLoading, error, refetch } = trpc.user.get.useQuery({ id: userId })
const { data: currentUser } = trpc.user.me.useQuery()
const isSuperAdmin = currentUser?.role === 'SUPER_ADMIN'
const updateUser = trpc.user.update.useMutation()
const sendInvitation = trpc.user.sendInvitation.useMutation()
@@ -65,6 +77,8 @@ export default function MemberDetailPage() {
const [status, setStatus] = useState<string>('INVITED')
const [expertiseTags, setExpertiseTags] = useState<string[]>([])
const [maxAssignments, setMaxAssignments] = useState<string>('')
const [showSuperAdminConfirm, setShowSuperAdminConfirm] = useState(false)
const [pendingSuperAdminRole, setPendingSuperAdminRole] = useState(false)
useEffect(() => {
if (user) {
@@ -81,7 +95,7 @@ export default function MemberDetailPage() {
await updateUser.mutateAsync({
id: userId,
name: name || null,
role: role as 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER' | 'PROGRAM_ADMIN',
role: role as 'SUPER_ADMIN' | 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER' | 'PROGRAM_ADMIN',
status: status as 'INVITED' | 'ACTIVE' | 'SUSPENDED',
expertiseTags,
maxAssignments: maxAssignments ? parseInt(maxAssignments) : null,
@@ -211,15 +225,28 @@ export default function MemberDetailPage() {
</div>
<div className="space-y-2">
<Label htmlFor="role">Role</Label>
<Select value={role} onValueChange={setRole}>
<Select
value={role}
onValueChange={(v) => {
if (v === 'SUPER_ADMIN') {
setPendingSuperAdminRole(true)
setShowSuperAdminConfirm(true)
} else {
setRole(v)
}
}}
>
<SelectTrigger id="role">
<SelectValue />
</SelectTrigger>
<SelectContent>
{isSuperAdmin && (
<SelectItem value="SUPER_ADMIN">Super Admin</SelectItem>
)}
<SelectItem value="PROGRAM_ADMIN">Program Admin</SelectItem>
<SelectItem value="JURY_MEMBER">Jury Member</SelectItem>
<SelectItem value="MENTOR">Mentor</SelectItem>
<SelectItem value="OBSERVER">Observer</SelectItem>
<SelectItem value="PROGRAM_ADMIN">Program Admin</SelectItem>
</SelectContent>
</Select>
</div>
@@ -377,6 +404,39 @@ export default function MemberDetailPage() {
Save Changes
</Button>
</div>
{/* Super Admin Confirmation Dialog */}
<AlertDialog open={showSuperAdminConfirm} onOpenChange={setShowSuperAdminConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Grant Super Admin Access?</AlertDialogTitle>
<AlertDialogDescription>
This will grant <strong>{name || user?.name || 'this user'}</strong> full Super Admin
access, including user management, system settings, and all administrative
capabilities. This action should only be performed for trusted administrators.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
onClick={() => {
setPendingSuperAdminRole(false)
}}
>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
setRole('SUPER_ADMIN')
setPendingSuperAdminRole(false)
setShowSuperAdminConfirm(false)
}}
className="bg-red-600 hover:bg-red-700"
>
Confirm Super Admin
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
}