From 0c0a9b7eb5aec22efafed950910ec9bdfc5a519e Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 30 Jan 2026 19:28:57 +0100 Subject: [PATCH] Add round delete with confirmation dialog - Add delete procedure to round tRPC router with cascade and audit log - Add delete option to rounds list dropdown menu - Show confirmation dialog with project/assignment counts before deletion Co-Authored-By: Claude Opus 4.5 --- src/app/(admin)/admin/rounds/page.tsx | 60 ++++++++++++++++++++++++++- src/server/routers/round.ts | 39 +++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/app/(admin)/admin/rounds/page.tsx b/src/app/(admin)/admin/rounds/page.tsx index 8257d99..a041c6c 100644 --- a/src/app/(admin)/admin/rounds/page.tsx +++ b/src/app/(admin)/admin/rounds/page.tsx @@ -1,8 +1,9 @@ 'use client' -import { Suspense } from 'react' +import { Suspense, useState } from 'react' import Link from 'next/link' import { trpc } from '@/lib/trpc/client' +import { toast } from 'sonner' import { Card, CardContent, @@ -28,6 +29,16 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' import { Plus, MoreHorizontal, @@ -39,6 +50,8 @@ import { CheckCircle2, Clock, Archive, + Trash2, + Loader2, } from 'lucide-react' import { format, isPast, isFuture } from 'date-fns' @@ -122,6 +135,7 @@ function RoundsContent() { function RoundRow({ round }: { round: any }) { const utils = trpc.useUtils() + const [showDeleteDialog, setShowDeleteDialog] = useState(false) const updateStatus = trpc.round.updateStatus.useMutation({ onSuccess: () => { @@ -129,6 +143,16 @@ function RoundRow({ round }: { round: any }) { }, }) + const deleteRound = trpc.round.delete.useMutation({ + onSuccess: () => { + toast.success('Round deleted successfully') + utils.program.list.invalidate() + }, + onError: (error) => { + toast.error(error.message || 'Failed to delete round') + }, + }) + const getStatusBadge = () => { const now = new Date() const isVotingOpen = @@ -284,8 +308,42 @@ function RoundRow({ round }: { round: any }) { Archive Round )} + + setShowDeleteDialog(true)} + > + + Delete Round + + + + + + Delete Round + + Are you sure you want to delete "{round.name}"? This will + permanently delete all {round._count?.projects || 0} projects,{' '} + {round._count?.assignments || 0} assignments, and all evaluations + in this round. This action cannot be undone. + + + + Cancel + deleteRound.mutate({ id: round.id })} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + {deleteRound.isPending ? ( + + ) : null} + Delete + + + + ) diff --git a/src/server/routers/round.ts b/src/server/routers/round.ts index aaf3396..2e1e752 100644 --- a/src/server/routers/round.ts +++ b/src/server/routers/round.ts @@ -341,6 +341,45 @@ export const roundRouter = router({ }) }), + /** + * Delete a round (admin only) + * Cascades to projects, assignments, evaluations, etc. + */ + delete: adminProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ ctx, input }) => { + const round = await ctx.prisma.round.findUniqueOrThrow({ + where: { id: input.id }, + include: { + _count: { select: { projects: true, assignments: true } }, + }, + }) + + await ctx.prisma.round.delete({ + where: { id: input.id }, + }) + + // Audit log + await ctx.prisma.auditLog.create({ + data: { + userId: ctx.user.id, + action: 'DELETE', + entityType: 'Round', + entityId: input.id, + detailsJson: { + name: round.name, + status: round.status, + projectsDeleted: round._count.projects, + assignmentsDeleted: round._count.assignments, + }, + ipAddress: ctx.ip, + userAgent: ctx.userAgent, + }, + }) + + return round + }), + /** * Check if a round has any submitted evaluations */