'use client' import { use, useState } from 'react' import Link from 'next/link' import type { Route } from 'next' import { useRouter } from 'next/navigation' import { trpc } from '@/lib/trpc/client' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Textarea } from '@/components/ui/textarea' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { toast } from 'sonner' import { cn } from '@/lib/utils' import { ArrowLeft, Plus, Loader2, Trash2, Users, Settings, Search, } from 'lucide-react' const capModeLabels = { HARD: 'Hard Cap', SOFT: 'Soft Cap', NONE: 'No Cap', } const capModeColors = { HARD: 'bg-red-100 text-red-700', SOFT: 'bg-amber-100 text-amber-700', NONE: 'bg-gray-100 text-gray-600', } type JuryGroupDetailPageProps = { params: Promise<{ groupId: string }> } export default function JuryGroupDetailPage({ params }: JuryGroupDetailPageProps) { const resolvedParams = use(params) const groupId = resolvedParams.groupId const router = useRouter() const utils = trpc.useUtils() const [addMemberDialogOpen, setAddMemberDialogOpen] = useState(false) const [userSearch, setUserSearch] = useState('') const [selectedUserId, setSelectedUserId] = useState('') const [selectedRole, setSelectedRole] = useState<'CHAIR' | 'MEMBER' | 'OBSERVER'>('MEMBER') const [maxAssignmentsOverride, setMaxAssignmentsOverride] = useState('') const { data: group, isLoading: loadingGroup } = trpc.juryGroup.getById.useQuery( { id: groupId }, { enabled: !!groupId } ) const { data: competition, isLoading: loadingCompetition } = trpc.competition.getById.useQuery( { id: group?.competitionId ?? '' }, { enabled: !!group?.competitionId } ) const { data: userSearchResults, isLoading: loadingUsers } = trpc.user.list.useQuery( { role: 'JURY_MEMBER', search: userSearch, page: 1, perPage: 20, }, { enabled: addMemberDialogOpen } ) const { data: selfServiceData } = trpc.juryGroup.reviewSelfServiceValues.useQuery( { juryGroupId: groupId }, { enabled: !!groupId } ) const addMemberMutation = trpc.juryGroup.addMember.useMutation({ onSuccess: () => { utils.juryGroup.getById.invalidate({ id: groupId }) utils.juryGroup.reviewSelfServiceValues.invalidate({ juryGroupId: groupId }) toast.success('Member added') setAddMemberDialogOpen(false) setSelectedUserId('') setUserSearch('') setMaxAssignmentsOverride('') }, onError: (err) => toast.error(err.message), }) const removeMemberMutation = trpc.juryGroup.removeMember.useMutation({ onSuccess: () => { utils.juryGroup.getById.invalidate({ id: groupId }) utils.juryGroup.reviewSelfServiceValues.invalidate({ juryGroupId: groupId }) toast.success('Member removed') }, onError: (err) => toast.error(err.message), }) const updateMemberMutation = trpc.juryGroup.updateMember.useMutation({ onSuccess: () => { utils.juryGroup.getById.invalidate({ id: groupId }) toast.success('Member updated') }, onError: (err) => toast.error(err.message), }) const updateGroupMutation = trpc.juryGroup.update.useMutation({ onSuccess: () => { utils.juryGroup.getById.invalidate({ id: groupId }) utils.juryGroup.list.invalidate() toast.success('Jury group updated') }, onError: (err) => toast.error(err.message), }) const handleAddMember = () => { if (!selectedUserId) { toast.error('Please select a user') return } addMemberMutation.mutate({ juryGroupId: groupId, userId: selectedUserId, role: selectedRole, maxAssignmentsOverride: maxAssignmentsOverride ? parseInt(maxAssignmentsOverride, 10) : null, }) } const handleRemoveMember = (memberId: string) => { if (!confirm('Remove this member from the jury group?')) return removeMemberMutation.mutate({ id: memberId }) } const handleRoleChange = (memberId: string, role: 'CHAIR' | 'MEMBER' | 'OBSERVER') => { updateMemberMutation.mutate({ id: memberId, role }) } if (loadingGroup || loadingCompetition) { return (
) } if (!group) { return (

Jury Group Not Found

The requested jury group could not be found.

) } return (
{/* Header */}

{group.name}

{capModeLabels[group.defaultCapMode as keyof typeof capModeLabels]}

{competition?.name ?? 'Loading...'}

{/* Tabs */} Members Settings {/* Members Tab */}
Members {group.members.length} member{group.members.length === 1 ? '' : 's'}
{group.members.length === 0 ? (

No members yet. Add jury members to this group.

) : ( Name Email Role Cap Override Availability Actions {group.members.map((member) => ( {member.user.name || 'Unnamed'} {member.user.email} {member.maxAssignmentsOverride ?? ( )} {member.availabilityNotes ? ( {member.availabilityNotes} ) : ( )} ))}
)}
{/* Settings Tab */} updateGroupMutation.mutate({ id: groupId, ...data })} isPending={updateGroupMutation.isPending} /> {/* Self-Service Review Section */} {selfServiceData && selfServiceData.members.length > 0 && ( Self-Service Values Members who set their own capacity or ratio during onboarding Member Role Admin Cap Self-Service Cap Self-Service Ratio Preferred Ratio {selfServiceData.members.map((m) => (
{m.userName}
{m.userEmail}
{m.role} {m.adminCap} {m.selfServiceCap ?? } {m.selfServiceRatio !== null ? ( {(m.selfServiceRatio * 100).toFixed(0)}% ) : ( )} {m.preferredStartupRatio !== null ? ( {(m.preferredStartupRatio * 100).toFixed(0)}% ) : ( )}
))}
)}
{/* Add Member Dialog */} Add Member Search for a jury member to add to this group
setUserSearch(e.target.value)} className="pl-9" />
{loadingUsers ? (
) : userSearchResults?.users && userSearchResults.users.length > 0 ? (
{userSearchResults.users.map((user) => (
setSelectedUserId(user.id)} >
{user.name || 'Unnamed'}
{user.email}
))}
) : (

No users found. Try a different search.

)}
setMaxAssignmentsOverride(e.target.value)} />
) } // ─── Settings Form Component ───────────────────────────────────────────────── type SettingsFormProps = { group: any onSave: (data: any) => void isPending: boolean } function SettingsForm({ group, onSave, isPending }: SettingsFormProps) { const [formData, setFormData] = useState({ name: group.name, description: group.description || '', defaultMaxAssignments: group.defaultMaxAssignments, }) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() onSave(formData) } return ( General Settings Configure jury group defaults and permissions
setFormData({ ...formData, name: e.target.value })} placeholder="Jury group name" />