feat(final-docs): admin manual reminder button
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -96,6 +96,7 @@ import { MentoringProjectsTable } from '@/components/admin/round/mentoring-proje
|
|||||||
import { FinalistSlotsCard } from '@/components/admin/grand-finale/finalist-slots-card'
|
import { FinalistSlotsCard } from '@/components/admin/grand-finale/finalist-slots-card'
|
||||||
import { WaitlistCard } from '@/components/admin/grand-finale/waitlist-card'
|
import { WaitlistCard } from '@/components/admin/grand-finale/waitlist-card'
|
||||||
import { FinalistEnrollmentCard } from '@/components/admin/grand-finale/finalist-enrollment-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 { RankingDashboard } from '@/components/admin/round/ranking-dashboard'
|
||||||
import { CoverageReport } from '@/components/admin/assignment/coverage-report'
|
import { CoverageReport } from '@/components/admin/assignment/coverage-report'
|
||||||
import { AssignmentPreviewSheet } from '@/components/admin/assignment/assignment-preview-sheet'
|
import { AssignmentPreviewSheet } from '@/components/admin/assignment/assignment-preview-sheet'
|
||||||
@@ -1529,6 +1530,9 @@ export default function RoundDetailPage() {
|
|||||||
{isGrandFinale && programId && (
|
{isGrandFinale && programId && (
|
||||||
<>
|
<>
|
||||||
<FinalistEnrollmentCard programId={programId} roundId={roundId} />
|
<FinalistEnrollmentCard programId={programId} roundId={roundId} />
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<FinalDocsReminderButton programId={programId} />
|
||||||
|
</div>
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<FinalistSlotsCard programId={programId} />
|
<FinalistSlotsCard programId={programId} />
|
||||||
<WaitlistCard programId={programId} />
|
<WaitlistCard programId={programId} />
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -119,6 +119,11 @@ const NOTIFICATION_SAMPLE_DATA: Record<string, Record<string, unknown>> = {
|
|||||||
FINALIST_EXPIRED: { projectTitle: 'Ocean Cleanup Initiative', category: 'STARTUP' },
|
FINALIST_EXPIRED: { projectTitle: 'Ocean Cleanup Initiative', category: 'STARTUP' },
|
||||||
FINALIST_WAITLIST_PROMOTED: { projectTitle: 'Reef Guardians', 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() },
|
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' },
|
FINALIST_WITHDRAWN: { projectTitle: 'Ocean Cleanup Initiative', reason: 'Schedule conflict' },
|
||||||
TRAVEL_CONFIRMED: {
|
TRAVEL_CONFIRMED: {
|
||||||
projectTitle: 'Ocean Cleanup Initiative',
|
projectTitle: 'Ocean Cleanup Initiative',
|
||||||
|
|||||||
Reference in New Issue
Block a user