Jury management: create, delete, add/remove members from round detail page
- Added Jury tab to round detail page with full jury management inline - Create new jury groups (auto-assigns to current round) - Delete jury groups with confirmation dialog - Add/remove members with inline member list - Assign/switch jury groups via dropdown selector - Added delete endpoint to juryGroup router (unlinks rounds, deletes members) - Removed CHAIR/OBSERVER role selectors from add-member dialog (all members) - Removed role column from jury-members-table Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
86fa542371
commit
763b2ef0f5
|
|
@ -88,6 +88,7 @@ import { CoverageReport } from '@/components/admin/assignment/coverage-report'
|
|||
import { AssignmentPreviewSheet } from '@/components/admin/assignment/assignment-preview-sheet'
|
||||
import { CsvExportDialog } from '@/components/shared/csv-export-dialog'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import { AddMemberDialog } from '@/components/admin/jury/add-member-dialog'
|
||||
import { motion } from 'motion/react'
|
||||
|
||||
// ── Status & type config maps ──────────────────────────────────────────────
|
||||
|
|
@ -156,6 +157,9 @@ export default function RoundDetailPage() {
|
|||
BUSINESS_CONCEPT: Array<{ projectId: string; rank: number; score: number; category: string; strengths: string[]; concerns: string[]; recommendation: string }>
|
||||
} | null>(null)
|
||||
const [shortlistDialogOpen, setShortlistDialogOpen] = useState(false)
|
||||
const [createJuryOpen, setCreateJuryOpen] = useState(false)
|
||||
const [newJuryName, setNewJuryName] = useState('')
|
||||
const [addMemberOpen, setAddMemberOpen] = useState(false)
|
||||
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
|
|
@ -249,11 +253,48 @@ export default function RoundDetailPage() {
|
|||
const assignJuryMutation = trpc.round.update.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.round.getById.invalidate({ id: roundId })
|
||||
utils.juryGroup.list.invalidate({ competitionId })
|
||||
toast.success('Jury group updated')
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
// Jury group detail query (for the assigned group)
|
||||
const juryGroupId = round?.juryGroupId ?? ''
|
||||
const { data: juryGroupDetail } = trpc.juryGroup.getById.useQuery(
|
||||
{ id: juryGroupId },
|
||||
{ enabled: !!juryGroupId, refetchInterval: 10_000 },
|
||||
)
|
||||
|
||||
const createJuryMutation = trpc.juryGroup.create.useMutation({
|
||||
onSuccess: (newGroup) => {
|
||||
utils.juryGroup.list.invalidate({ competitionId })
|
||||
// Auto-assign the new jury group to this round
|
||||
assignJuryMutation.mutate({ id: roundId, juryGroupId: newGroup.id })
|
||||
toast.success(`Jury "${newGroup.name}" created and assigned`)
|
||||
setCreateJuryOpen(false)
|
||||
setNewJuryName('')
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const deleteJuryMutation = trpc.juryGroup.delete.useMutation({
|
||||
onSuccess: (result) => {
|
||||
utils.juryGroup.list.invalidate({ competitionId })
|
||||
utils.round.getById.invalidate({ id: roundId })
|
||||
toast.success(`Jury "${result.name}" deleted`)
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const removeJuryMemberMutation = trpc.juryGroup.removeMember.useMutation({
|
||||
onSuccess: () => {
|
||||
if (juryGroupId) utils.juryGroup.getById.invalidate({ id: juryGroupId })
|
||||
toast.success('Member removed')
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const advanceMutation = trpc.round.advanceProjects.useMutation({
|
||||
onSuccess: (data) => {
|
||||
utils.round.getById.invalidate({ id: roundId })
|
||||
|
|
@ -653,6 +694,7 @@ export default function RoundDetailPage() {
|
|||
{ value: 'projects', label: 'Projects', icon: Layers },
|
||||
...(isFiltering ? [{ value: 'filtering', label: 'Filtering', icon: Shield }] : []),
|
||||
...(isEvaluation ? [{ value: 'assignments', label: 'Assignments', icon: ClipboardList }] : []),
|
||||
{ value: 'jury', label: 'Jury', icon: Users },
|
||||
{ value: 'config', label: 'Config', icon: Settings },
|
||||
{ value: 'windows', label: 'Submissions', icon: Clock },
|
||||
{ value: 'awards', label: 'Awards', icon: Trophy },
|
||||
|
|
@ -1128,6 +1170,248 @@ export default function RoundDetailPage() {
|
|||
</TabsContent>
|
||||
)}
|
||||
|
||||
{/* ═══════════ JURY TAB ═══════════ */}
|
||||
<TabsContent value="jury" className="space-y-6">
|
||||
{/* Jury Group Selector + Create */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-base">Jury Group</CardTitle>
|
||||
<CardDescription>
|
||||
Select or create a jury group for this round
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => setCreateJuryOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-1.5" />
|
||||
New Jury
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{juryGroups && juryGroups.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
<Select
|
||||
value={round.juryGroupId ?? '__none__'}
|
||||
onValueChange={(value) => {
|
||||
assignJuryMutation.mutate({
|
||||
id: roundId,
|
||||
juryGroupId: value === '__none__' ? null : value,
|
||||
})
|
||||
}}
|
||||
disabled={assignJuryMutation.isPending}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-80">
|
||||
<SelectValue placeholder="Select jury group..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">No jury assigned</SelectItem>
|
||||
{juryGroups.map((jg: any) => (
|
||||
<SelectItem key={jg.id} value={jg.id}>
|
||||
{jg.name} ({jg._count?.members ?? 0} members)
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Delete button for currently selected jury group */}
|
||||
{round.juryGroupId && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="sm" variant="ghost" className="text-destructive hover:text-destructive">
|
||||
<Trash2 className="h-4 w-4 mr-1.5" />
|
||||
Delete "{juryGroup?.name}"
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete jury group?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete "{juryGroup?.name}" and remove all its members.
|
||||
Rounds using this jury group will be unlinked. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => deleteJuryMutation.mutate({ id: round.juryGroupId! })}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
disabled={deleteJuryMutation.isPending}
|
||||
>
|
||||
{deleteJuryMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
||||
Delete Jury
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-10 text-center">
|
||||
<div className="rounded-full bg-purple-50 p-4 mb-4">
|
||||
<Users className="h-8 w-8 text-purple-400" />
|
||||
</div>
|
||||
<p className="text-sm font-medium">No Jury Groups</p>
|
||||
<p className="text-xs text-muted-foreground mt-1 max-w-sm">
|
||||
Create a jury group to assign members who will evaluate projects in this round.
|
||||
</p>
|
||||
<Button size="sm" className="mt-4" onClick={() => setCreateJuryOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-1.5" />
|
||||
Create First Jury
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Members list (only if a jury group is assigned) */}
|
||||
{juryGroupDetail && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-base">
|
||||
Members — {juryGroupDetail.name}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{juryGroupDetail.members.length} member{juryGroupDetail.members.length !== 1 ? 's' : ''}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button size="sm" onClick={() => setAddMemberOpen(true)}>
|
||||
<UserPlus className="h-4 w-4 mr-1.5" />
|
||||
Add Member
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{juryGroupDetail.members.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-10 text-center">
|
||||
<div className="rounded-full bg-muted p-4 mb-4">
|
||||
<UserPlus className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-sm font-medium">No Members Yet</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Add jury members to start assigning projects for evaluation.
|
||||
</p>
|
||||
<Button size="sm" variant="outline" className="mt-4" onClick={() => setAddMemberOpen(true)}>
|
||||
<UserPlus className="h-4 w-4 mr-1.5" />
|
||||
Add First Member
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{juryGroupDetail.members.map((member: any, idx: number) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className={cn(
|
||||
'flex items-center justify-between py-2.5 px-3 rounded-md transition-colors',
|
||||
idx % 2 === 1 && 'bg-muted/30',
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{member.user.name || 'Unnamed User'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">{member.user.email}</p>
|
||||
</div>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive hover:text-destructive shrink-0"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Remove member?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Remove {member.user.name || member.user.email} from {juryGroupDetail.name}?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => removeJuryMemberMutation.mutate({ id: member.id })}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Remove
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Create Jury Dialog */}
|
||||
<Dialog open={createJuryOpen} onOpenChange={setCreateJuryOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Jury Group</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new jury group for this competition. It will be automatically assigned to this round.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Name</label>
|
||||
<Input
|
||||
placeholder="e.g. Round 1 Jury, Expert Panel, Final Jury"
|
||||
value={newJuryName}
|
||||
onChange={(e) => setNewJuryName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && newJuryName.trim()) {
|
||||
createJuryMutation.mutate({
|
||||
competitionId,
|
||||
name: newJuryName.trim(),
|
||||
slug: newJuryName.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''),
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setCreateJuryOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
createJuryMutation.mutate({
|
||||
competitionId,
|
||||
name: newJuryName.trim(),
|
||||
slug: newJuryName.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''),
|
||||
})
|
||||
}}
|
||||
disabled={createJuryMutation.isPending || !newJuryName.trim()}
|
||||
>
|
||||
{createJuryMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Add Member Dialog */}
|
||||
{juryGroupId && (
|
||||
<AddMemberDialog
|
||||
juryGroupId={juryGroupId}
|
||||
open={addMemberOpen}
|
||||
onOpenChange={(open) => {
|
||||
setAddMemberOpen(open)
|
||||
if (!open) utils.juryGroup.getById.invalidate({ id: juryGroupId })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* ═══════════ ASSIGNMENTS TAB (Evaluation rounds) ═══════════ */}
|
||||
{isEvaluation && (
|
||||
<TabsContent value="assignments" className="space-y-6">
|
||||
|
|
|
|||
|
|
@ -36,14 +36,12 @@ export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDi
|
|||
// Search existing user state
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedUserId, setSelectedUserId] = useState<string>('')
|
||||
const [role, setRole] = useState<'CHAIR' | 'MEMBER' | 'OBSERVER'>('MEMBER')
|
||||
const [maxAssignments, setMaxAssignments] = useState<string>('')
|
||||
const [capMode, setCapMode] = useState<string>('')
|
||||
|
||||
// Invite new user state
|
||||
const [inviteName, setInviteName] = useState('')
|
||||
const [inviteEmail, setInviteEmail] = useState('')
|
||||
const [inviteRole, setInviteRole] = useState<'CHAIR' | 'MEMBER' | 'OBSERVER'>('MEMBER')
|
||||
const [inviteMaxAssignments, setInviteMaxAssignments] = useState<string>('')
|
||||
const [inviteCapMode, setInviteCapMode] = useState<string>('')
|
||||
const [inviteExpertise, setInviteExpertise] = useState('')
|
||||
|
|
@ -75,7 +73,7 @@ export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDi
|
|||
addMember({
|
||||
juryGroupId,
|
||||
userId: newUser.id,
|
||||
role: inviteRole,
|
||||
role: 'MEMBER',
|
||||
maxAssignmentsOverride: inviteMaxAssignments ? parseInt(inviteMaxAssignments, 10) : null,
|
||||
capModeOverride: inviteCapMode && inviteCapMode !== 'DEFAULT' ? (inviteCapMode as 'HARD' | 'SOFT' | 'NONE') : null,
|
||||
})
|
||||
|
|
@ -100,12 +98,10 @@ export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDi
|
|||
const resetForm = () => {
|
||||
setSearchQuery('')
|
||||
setSelectedUserId('')
|
||||
setRole('MEMBER')
|
||||
setMaxAssignments('')
|
||||
setCapMode('')
|
||||
setInviteName('')
|
||||
setInviteEmail('')
|
||||
setInviteRole('MEMBER')
|
||||
setInviteMaxAssignments('')
|
||||
setInviteCapMode('')
|
||||
setInviteExpertise('')
|
||||
|
|
@ -122,7 +118,7 @@ export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDi
|
|||
addMember({
|
||||
juryGroupId,
|
||||
userId: selectedUserId,
|
||||
role,
|
||||
role: 'MEMBER',
|
||||
maxAssignmentsOverride: maxAssignments ? parseInt(maxAssignments, 10) : null,
|
||||
capModeOverride: capMode && capMode !== 'DEFAULT' ? (capMode as 'HARD' | 'SOFT' | 'NONE') : null,
|
||||
})
|
||||
|
|
@ -215,20 +211,6 @@ export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDi
|
|||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="role">Group Role</Label>
|
||||
<Select value={role} onValueChange={(val) => setRole(val as typeof role)}>
|
||||
<SelectTrigger id="role">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="MEMBER">Member</SelectItem>
|
||||
<SelectItem value="CHAIR">Chair</SelectItem>
|
||||
<SelectItem value="OBSERVER">Observer</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="capMode">Cap Mode</Label>
|
||||
<Select value={capMode || 'DEFAULT'} onValueChange={setCapMode}>
|
||||
|
|
@ -298,20 +280,6 @@ export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDi
|
|||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="inviteGroupRole">Group Role</Label>
|
||||
<Select value={inviteRole} onValueChange={(val) => setInviteRole(val as typeof inviteRole)}>
|
||||
<SelectTrigger id="inviteGroupRole">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="MEMBER">Member</SelectItem>
|
||||
<SelectItem value="CHAIR">Chair</SelectItem>
|
||||
<SelectItem value="OBSERVER">Observer</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="inviteCapMode">Cap Mode</Label>
|
||||
<Select value={inviteCapMode || 'DEFAULT'} onValueChange={setInviteCapMode}>
|
||||
|
|
|
|||
|
|
@ -29,15 +29,15 @@ import { AddMemberDialog } from './add-member-dialog'
|
|||
interface JuryMember {
|
||||
id: string
|
||||
userId: string
|
||||
role: string
|
||||
role?: string
|
||||
user: {
|
||||
id: string
|
||||
name: string | null
|
||||
email: string
|
||||
}
|
||||
maxAssignmentsOverride: number | null
|
||||
capModeOverride: string | null
|
||||
preferredStartupRatio: number | null
|
||||
maxAssignmentsOverride?: number | null
|
||||
capModeOverride?: string | null
|
||||
preferredStartupRatio?: number | null
|
||||
}
|
||||
|
||||
interface JuryMembersTableProps {
|
||||
|
|
@ -81,7 +81,6 @@ export function JuryMembersTable({ juryGroupId, members }: JuryMembersTableProps
|
|||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead className="hidden md:table-cell">Role</TableHead>
|
||||
<TableHead className="hidden sm:table-cell">Max Assignments</TableHead>
|
||||
<TableHead className="hidden lg:table-cell">Cap Mode</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
|
|
@ -90,7 +89,7 @@ export function JuryMembersTable({ juryGroupId, members }: JuryMembersTableProps
|
|||
<TableBody>
|
||||
{members.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-muted-foreground">
|
||||
<TableCell colSpan={5} className="text-center text-muted-foreground">
|
||||
No members yet. Add members to get started.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -103,11 +102,6 @@ export function JuryMembersTable({ juryGroupId, members }: JuryMembersTableProps
|
|||
<TableCell className="text-sm text-muted-foreground">
|
||||
{member.user.email}
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
<Badge variant={member.role === 'CHAIR' ? 'default' : 'secondary'}>
|
||||
{member.role}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
{member.maxAssignmentsOverride ?? '—'}
|
||||
</TableCell>
|
||||
|
|
|
|||
|
|
@ -249,6 +249,50 @@ export const juryGroupRouter = router({
|
|||
return existing
|
||||
}),
|
||||
|
||||
/**
|
||||
* Delete a jury group entirely
|
||||
*/
|
||||
delete: adminProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const group = await ctx.prisma.juryGroup.findUniqueOrThrow({
|
||||
where: { id: input.id },
|
||||
include: {
|
||||
_count: { select: { assignments: true, rounds: true } },
|
||||
},
|
||||
})
|
||||
|
||||
// Unlink any rounds that reference this jury group
|
||||
await ctx.prisma.round.updateMany({
|
||||
where: { juryGroupId: input.id },
|
||||
data: { juryGroupId: null },
|
||||
})
|
||||
|
||||
// Delete all members first (cascade should handle this, but be explicit)
|
||||
await ctx.prisma.juryGroupMember.deleteMany({
|
||||
where: { juryGroupId: input.id },
|
||||
})
|
||||
|
||||
await ctx.prisma.juryGroup.delete({ where: { id: input.id } })
|
||||
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'DELETE',
|
||||
entityType: 'JuryGroup',
|
||||
entityId: input.id,
|
||||
detailsJson: {
|
||||
name: group.name,
|
||||
competitionId: group.competitionId,
|
||||
memberCount: group._count.assignments,
|
||||
},
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
return { success: true, name: group.name }
|
||||
}),
|
||||
|
||||
/**
|
||||
* Update a jury group member's role/overrides
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue