Add round delete with confirmation dialog
Build and Push Docker Image / build (push) Successful in 7m57s Details

- 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 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-01-30 19:28:57 +01:00
parent 4c5a49cede
commit 0c0a9b7eb5
2 changed files with 98 additions and 1 deletions

View File

@ -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
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => setShowDeleteDialog(true)}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete Round
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Round</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete &quot;{round.name}&quot;? 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.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteRound.mutate({ id: round.id })}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{deleteRound.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : null}
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</TableCell>
</TableRow>
)

View File

@ -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
*/