feat(final-docs): admin manual reminder button

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-06-09 15:31:18 +02:00
parent 26709e2c9b
commit 61bf5a4032
3 changed files with 91 additions and 0 deletions

View File

@@ -96,6 +96,7 @@ import { MentoringProjectsTable } from '@/components/admin/round/mentoring-proje
import { FinalistSlotsCard } from '@/components/admin/grand-finale/finalist-slots-card'
import { WaitlistCard } from '@/components/admin/grand-finale/waitlist-card'
import { FinalistEnrollmentCard } from '@/components/admin/grand-finale/finalist-enrollment-card'
import { FinalDocsReminderButton } from '@/components/admin/grand-finale/final-docs-reminder-button'
import { RankingDashboard } from '@/components/admin/round/ranking-dashboard'
import { CoverageReport } from '@/components/admin/assignment/coverage-report'
import { AssignmentPreviewSheet } from '@/components/admin/assignment/assignment-preview-sheet'
@@ -1529,6 +1530,9 @@ export default function RoundDetailPage() {
{isGrandFinale && programId && (
<>
<FinalistEnrollmentCard programId={programId} roundId={roundId} />
<div className="flex justify-end">
<FinalDocsReminderButton programId={programId} />
</div>
<div className="grid gap-4 md:grid-cols-2">
<FinalistSlotsCard programId={programId} />
<WaitlistCard programId={programId} />

View File

@@ -0,0 +1,82 @@
'use client'
import { useState } from 'react'
import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { EmailPreviewDialog } from '@/components/admin/round/email-preview-dialog'
import { Eye, Mail, Send } from 'lucide-react'
import { toast } from 'sonner'
const REMINDER_TYPE = 'GRAND_FINAL_DOCS_REMINDER'
export function FinalDocsReminderButton({ programId }: { programId: string }) {
const [open, setOpen] = useState(false)
const [previewOpen, setPreviewOpen] = useState(false)
const preview = trpc.notification.previewEmailTemplate.useQuery(
{ notificationType: REMINDER_TYPE },
{ enabled: previewOpen },
)
const send = trpc.finalist.sendDocumentReminders.useMutation({
onSuccess: (r) => {
toast.success(`Reminder sent to ${r.sent} team${r.sent === 1 ? '' : 's'}`)
setOpen(false)
},
onError: (e) => toast.error(e.message),
})
return (
<>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<Mail className="mr-2 h-4 w-4" /> Remind teams to upload final documents
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Remind finalist teams</DialogTitle>
<DialogDescription>
Sends an in-app + email reminder to every finalist team with missing required
documents.
</DialogDescription>
</DialogHeader>
<DialogFooter className="sm:justify-between">
<Button variant="ghost" size="sm" onClick={() => setPreviewOpen(true)}>
<Eye className="mr-2 h-4 w-4" /> Preview email
</Button>
<Button onClick={() => send.mutate({ programId })} disabled={send.isPending}>
<Send className="mr-2 h-4 w-4" /> {send.isPending ? 'Sending…' : 'Send reminders'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Self-contained preview dialog — rendered as a sibling so it is not nested
inside the confirm dialog's content. */}
<EmailPreviewDialog
open={previewOpen}
onOpenChange={setPreviewOpen}
title="Final Documents Reminder"
description="Preview of the email finalist teams receive."
recipientCount={0}
previewHtml={preview.data?.html}
isPreviewLoading={preview.isLoading}
onSend={() => {}}
isSending={false}
previewOnly
showCustomMessage={false}
/>
</>
)
}

View File

@@ -119,6 +119,11 @@ const NOTIFICATION_SAMPLE_DATA: Record<string, Record<string, unknown>> = {
FINALIST_EXPIRED: { projectTitle: 'Ocean Cleanup Initiative', category: 'STARTUP' },
FINALIST_WAITLIST_PROMOTED: { projectTitle: 'Reef Guardians', category: 'STARTUP' },
FINALIST_REMINDER: { projectTitle: 'Ocean Cleanup Initiative', deadline: new Date(Date.now() + 86_400_000).toISOString() },
GRAND_FINAL_DOCS_REMINDER: {
projectTitle: 'Ocean Cleanup Initiative',
deadline: new Date(Date.now() + 5 * 86_400_000).toISOString(),
missing: ['Pitch deck', 'Final video', 'Team photo'],
},
FINALIST_WITHDRAWN: { projectTitle: 'Ocean Cleanup Initiative', reason: 'Schedule conflict' },
TRAVEL_CONFIRMED: {
projectTitle: 'Ocean Cleanup Initiative',