feat: add email preview to award notification and finalization tab
- Award "Notify Pool" dialog now uses EmailPreviewDialog with live preview - Button shows eligible project count: "Notify Pool (38)" - Finalization tab email section has "Preview" buttons for both advancement and rejection messages - EmailPreviewDialog supports previewOnly mode (close button, no send) - Backend: previewAwardSelectionEmail, previewFinalizationAdvancementEmail, previewFinalizationRejectionEmail queries Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -53,11 +53,11 @@ import {
|
||||
} from '@/components/ui/dialog'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { UserAvatar } from '@/components/shared/user-avatar'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import { Pagination } from '@/components/shared/pagination'
|
||||
import { EmailPreviewDialog } from '@/components/admin/round/email-preview-dialog'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -158,7 +158,7 @@ export default function AwardDetailPage({
|
||||
const [addRoundOpen, setAddRoundOpen] = useState(false)
|
||||
const [roundForm, setRoundForm] = useState({ name: '', roundType: 'EVALUATION' as string })
|
||||
const [notifyDialogOpen, setNotifyDialogOpen] = useState(false)
|
||||
const [notifyCustomMessage, setNotifyCustomMessage] = useState('')
|
||||
const [notifyCustomMessage, setNotifyCustomMessage] = useState<string | undefined>()
|
||||
|
||||
// Pagination for eligibility list
|
||||
const [eligibilityPage, setEligibilityPage] = useState(1)
|
||||
@@ -287,15 +287,15 @@ export default function AwardDetailPage({
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const { data: notifyStats } = trpc.specialAward.getNotificationStats.useQuery(
|
||||
{ awardId },
|
||||
const notifyPreview = trpc.specialAward.previewAwardSelectionEmail.useQuery(
|
||||
{ awardId, customMessage: notifyCustomMessage },
|
||||
{ enabled: notifyDialogOpen }
|
||||
)
|
||||
const notifyEligible = trpc.specialAward.notifyEligibleProjects.useMutation({
|
||||
onSuccess: (result) => {
|
||||
toast.success(`Notified ${result.notified} projects (${result.emailsSent} emails sent${result.emailsFailed ? `, ${result.emailsFailed} failed` : ''})`)
|
||||
setNotifyDialogOpen(false)
|
||||
setNotifyCustomMessage('')
|
||||
setNotifyCustomMessage(undefined)
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
@@ -486,63 +486,22 @@ export default function AwardDetailPage({
|
||||
)}
|
||||
{award.status === 'NOMINATIONS_OPEN' && (
|
||||
<>
|
||||
<Dialog open={notifyDialogOpen} onOpenChange={setNotifyDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" disabled={award.eligibleCount === 0}>
|
||||
<Mail className="mr-2 h-4 w-4" />
|
||||
Notify Pool
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Notify Eligible Projects</DialogTitle>
|
||||
<DialogDescription>
|
||||
Send "Selected for {award.name}" emails to all {award.eligibleCount} eligible projects.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
{notifyStats && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{notifyStats.needsInvite > 0 && (
|
||||
<Badge variant="outline" className="border-amber-300 bg-amber-50 text-amber-700">
|
||||
{notifyStats.needsInvite} will receive Create Account link
|
||||
</Badge>
|
||||
)}
|
||||
{notifyStats.hasAccount > 0 && (
|
||||
<Badge variant="outline" className="border-emerald-300 bg-emerald-50 text-emerald-700">
|
||||
{notifyStats.hasAccount} will receive Dashboard link
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label>Custom message (optional)</Label>
|
||||
<Textarea
|
||||
placeholder="Add a personal message to include in the email..."
|
||||
value={notifyCustomMessage}
|
||||
onChange={(e) => setNotifyCustomMessage(e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setNotifyDialogOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={() => notifyEligible.mutate({
|
||||
awardId,
|
||||
customMessage: notifyCustomMessage.trim() || undefined,
|
||||
})}
|
||||
disabled={notifyEligible.isPending}
|
||||
>
|
||||
{notifyEligible.isPending ? (
|
||||
<><Loader2 className="mr-2 h-4 w-4 animate-spin" />Sending...</>
|
||||
) : (
|
||||
<><Mail className="mr-2 h-4 w-4" />Send {award.eligibleCount} Emails</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button variant="outline" disabled={award.eligibleCount === 0} onClick={() => setNotifyDialogOpen(true)}>
|
||||
<Mail className="mr-2 h-4 w-4" />
|
||||
Notify Pool ({award.eligibleCount})
|
||||
</Button>
|
||||
<EmailPreviewDialog
|
||||
open={notifyDialogOpen}
|
||||
onOpenChange={setNotifyDialogOpen}
|
||||
title="Notify Eligible Projects"
|
||||
description={`Send "Selected for ${award.name}" emails to all ${award.eligibleCount} eligible projects.`}
|
||||
recipientCount={notifyPreview.data?.recipientCount ?? 0}
|
||||
previewHtml={notifyPreview.data?.html}
|
||||
isPreviewLoading={notifyPreview.isLoading}
|
||||
onSend={(msg) => notifyEligible.mutate({ awardId, customMessage: msg })}
|
||||
isSending={notifyEligible.isPending}
|
||||
onRefreshPreview={(msg) => setNotifyCustomMessage(msg)}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => handleStatusChange('VOTING_OPEN')}
|
||||
disabled={updateStatus.isPending}
|
||||
|
||||
Reference in New Issue
Block a user