'use client' import { useState, useMemo } from 'react' import Link from 'next/link' import { trpc } from '@/lib/trpc/client' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Checkbox } from '@/components/ui/checkbox' import { Switch } from '@/components/ui/switch' import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Tabs, TabsContent, TabsList, TabsTrigger, } from '@/components/ui/tabs' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' import { Send, Mail, Bell, Clock, Loader2, LayoutTemplate, AlertCircle, Inbox, CheckCircle2, Eye, Users, FolderOpen, XCircle, ArrowRight, ChevronDown, ChevronRight, } from 'lucide-react' import { toast } from 'sonner' import { formatDate } from '@/lib/utils' type RecipientType = 'ALL' | 'ROLE' | 'ROUND_JURY' | 'ROUND_APPLICANTS' | 'PROGRAM_TEAM' | 'USER' const RECIPIENT_TYPE_OPTIONS: { value: RecipientType; label: string }[] = [ { value: 'ALL', label: 'All Users' }, { value: 'ROLE', label: 'By Role' }, { value: 'ROUND_JURY', label: 'Round Jury' }, { value: 'ROUND_APPLICANTS', label: 'Round Applicants' }, { value: 'PROGRAM_TEAM', label: 'Program Team' }, { value: 'USER', label: 'Specific User' }, ] const ROLES = ['JURY_MEMBER', 'MENTOR', 'OBSERVER', 'APPLICANT', 'PROGRAM_ADMIN'] const STATE_LABELS: Record = { PENDING: 'Pending', IN_PROGRESS: 'Active', COMPLETED: 'Completed', PASSED: 'Passed', REJECTED: 'Rejected', WITHDRAWN: 'Withdrawn', } const STATE_BADGE_VARIANT: Record = { PENDING: 'secondary', IN_PROGRESS: 'default', COMPLETED: 'default', PASSED: 'success', REJECTED: 'destructive', WITHDRAWN: 'secondary', } export default function MessagesPage() { const [recipientType, setRecipientType] = useState('ALL') const [selectedRole, setSelectedRole] = useState('') const [roundIds, setRoundIds] = useState([]) const [selectedProgramId, setSelectedProgramId] = useState('') const [selectedUserId, setSelectedUserId] = useState('') const [subject, setSubject] = useState('') const [body, setBody] = useState('') const [selectedTemplateId, setSelectedTemplateId] = useState('') const [deliveryChannels, setDeliveryChannels] = useState(['EMAIL', 'IN_APP']) const [linkType, setLinkType] = useState<'NONE' | 'MESSAGES' | 'LOGIN' | 'INVITE'>('MESSAGES') const [isScheduled, setIsScheduled] = useState(false) const [scheduledAt, setScheduledAt] = useState('') const [showPreview, setShowPreview] = useState(false) const [excludeRejected, setExcludeRejected] = useState(true) const [excludeWithdrawn, setExcludeWithdrawn] = useState(true) const utils = trpc.useUtils() // Fetch supporting data const { data: programs } = trpc.program.list.useQuery({ includeStages: true }) const rounds = programs?.flatMap((p) => ((p.stages ?? []) as Array<{ id: string; name: string }>).map((s: { id: string; name: string }) => ({ ...s, program: { name: p.name } })) ) || [] const { data: templates } = trpc.message.listTemplates.useQuery() const { data: users } = trpc.user.list.useQuery( { page: 1, perPage: 100 }, { enabled: recipientType === 'USER' } ) // Fetch sent messages for history const { data: sentMessages, isLoading: loadingSent } = trpc.message.sent.useQuery( { page: 1, pageSize: 50 }, { refetchInterval: 30_000 } ) // Compute exclude states list const excludeStates = useMemo(() => { const states: string[] = [] if (excludeRejected) states.push('REJECTED') if (excludeWithdrawn) states.push('WITHDRAWN') return states }, [excludeRejected, excludeWithdrawn]) // Live recipient preview — fetches whenever selection changes const recipientPreview = trpc.message.previewRecipients.useQuery( { recipientType, recipientFilter: buildRecipientFilterValue(), roundIds: roundIds.length > 0 ? roundIds : undefined, excludeStates: recipientType === 'ROUND_APPLICANTS' ? excludeStates : undefined, }, { enabled: recipientType === 'ROUND_APPLICANTS' ? roundIds.length > 0 : recipientType === 'ROUND_JURY' ? roundIds.length > 0 : recipientType === 'ROLE' ? !!selectedRole : recipientType === 'USER' ? !!selectedUserId : recipientType === 'PROGRAM_TEAM' ? !!selectedProgramId : recipientType === 'ALL', } ) // Detailed recipient list (fetched on-demand when user expands section) const [showRecipientDetails, setShowRecipientDetails] = useState(false) const recipientDetails = trpc.message.listRecipientDetails.useQuery( { recipientType, recipientFilter: buildRecipientFilterValue(), roundIds: roundIds.length > 0 ? roundIds : undefined, excludeStates: recipientType === 'ROUND_APPLICANTS' ? excludeStates : undefined, }, { enabled: showRecipientDetails && ( recipientType === 'ROUND_APPLICANTS' ? roundIds.length > 0 : recipientType === 'ROUND_JURY' ? roundIds.length > 0 : recipientType === 'ROLE' ? !!selectedRole : recipientType === 'USER' ? !!selectedUserId : recipientType === 'PROGRAM_TEAM' ? !!selectedProgramId : recipientType === 'ALL' ), } ) const emailPreview = trpc.message.previewEmail.useQuery( { subject, body }, { enabled: showPreview && subject.length > 0 && body.length > 0 } ) const sendTestMutation = trpc.message.sendTest.useMutation({ onSuccess: (data) => toast.success(`Test email sent to ${data.to}`), onError: (e) => toast.error(e.message), }) const sendMutation = trpc.message.send.useMutation({ onSuccess: (data) => { const count = (data as Record)?.recipientCount || '' toast.success(`Message sent successfully${count ? ` to ${count} recipients` : ''}`) resetForm() utils.message.sent.invalidate() }, onError: (e) => toast.error(e.message), }) const resetForm = () => { setSubject('') setBody('') setSelectedTemplateId('') setSelectedRole('') setRoundIds([]) setSelectedProgramId('') setSelectedUserId('') setIsScheduled(false) setScheduledAt('') setLinkType('MESSAGES') setShowRecipientDetails(false) } const handleTemplateSelect = (templateId: string) => { setSelectedTemplateId(templateId) if (templateId && templateId !== '__none__' && templates) { const template = (templates as Array>).find( (t) => String(t.id) === templateId ) if (template) { setSubject(String(template.subject || '')) setBody(String(template.body || '')) } } } const toggleChannel = (channel: string) => { setDeliveryChannels((prev) => prev.includes(channel) ? prev.filter((c) => c !== channel) : [...prev, channel] ) } function buildRecipientFilterValue(): unknown { switch (recipientType) { case 'ROLE': return selectedRole ? { role: selectedRole } : undefined case 'USER': return selectedUserId ? { userId: selectedUserId } : undefined case 'PROGRAM_TEAM': return selectedProgramId ? { programId: selectedProgramId } : undefined default: return undefined } } const getRecipientDescription = (): string => { switch (recipientType) { case 'ALL': return 'All platform users' case 'ROLE': { const roleLabel = selectedRole ? selectedRole.replace(/_/g, ' ') : '' return roleLabel ? `All ${roleLabel}s` : 'By Role (none selected)' } case 'ROUND_JURY': { if (roundIds.length === 0) return 'Stage Jury (none selected)' const selectedJuryRounds = rounds?.filter((r) => roundIds.includes(r.id)) if (!selectedJuryRounds?.length) return 'Stage Jury' if (selectedJuryRounds.length === 1) { const s = selectedJuryRounds[0] return `Jury of ${s.program ? `${s.program.name} - ` : ''}${s.name}` } return `Jury across ${selectedJuryRounds.length} rounds` } case 'ROUND_APPLICANTS': { if (roundIds.length === 0) return 'Round Applicants (none selected)' const selectedAppRounds = rounds?.filter((r) => roundIds.includes(r.id)) if (!selectedAppRounds?.length) return 'Round Applicants' if (selectedAppRounds.length === 1) { const ar = selectedAppRounds[0] return `Applicants in ${ar.program ? `${ar.program.name} - ` : ''}${ar.name}` } return `Applicants across ${selectedAppRounds.length} rounds` } case 'PROGRAM_TEAM': { if (!selectedProgramId) return 'Program Team (none selected)' const program = (programs as Array<{ id: string; name: string }> | undefined)?.find( (p) => p.id === selectedProgramId ) return program ? `Team of ${program.name}` : 'Program Team' } case 'USER': { if (!selectedUserId) return 'Specific User (none selected)' const userList = (users as { users: Array<{ id: string; name: string | null; email: string }> } | undefined)?.users const user = userList?.find((u) => u.id === selectedUserId) return user ? (user.name || user.email) : 'Specific User' } default: return 'Unknown' } } const validateForm = (): boolean => { if (!subject.trim()) { toast.error('Subject is required') return false } if (!body.trim()) { toast.error('Message body is required') return false } if (deliveryChannels.length === 0) { toast.error('Select at least one delivery channel') return false } if (recipientType === 'ROLE' && !selectedRole) { toast.error('Please select a role') return false } if ((recipientType === 'ROUND_JURY' || recipientType === 'ROUND_APPLICANTS') && roundIds.length === 0) { toast.error('Please select at least one round') return false } if (recipientType === 'PROGRAM_TEAM' && !selectedProgramId) { toast.error('Please select a program') return false } if (recipientType === 'USER' && !selectedUserId) { toast.error('Please select a user') return false } return true } const handlePreview = () => { if (!validateForm()) return setShowPreview(true) } const handleActualSend = () => { sendMutation.mutate({ recipientType, recipientFilter: buildRecipientFilterValue(), roundIds: roundIds.length > 0 ? roundIds : undefined, excludeStates: recipientType === 'ROUND_APPLICANTS' ? excludeStates : undefined, subject: subject.trim(), body: body.trim(), deliveryChannels, scheduledAt: isScheduled && scheduledAt ? new Date(scheduledAt).toISOString() : undefined, templateId: selectedTemplateId && selectedTemplateId !== '__none__' ? selectedTemplateId : undefined, linkType, }) setShowPreview(false) } const preview = recipientPreview.data return (
{/* Header */}

Communication Hub

Send messages and notifications to platform users

Compose Sent History
{/* Left: Compose form */}
Compose Message Send a message via email, in-app notifications, or both {/* Recipient type */}
{/* Conditional sub-filters */} {recipientType === 'ROLE' && (
)} {(recipientType === 'ROUND_JURY' || recipientType === 'ROUND_APPLICANTS') && (
{rounds?.length === 0 && (

No rounds available

)} {rounds?.map((round) => { const label = round.program ? `${round.program.name} - ${round.name}` : round.name const isChecked = roundIds.includes(round.id) return (
{ setRoundIds((prev) => checked ? [...prev, round.id] : prev.filter((id) => id !== round.id) ) }} />
) })}
{roundIds.length > 0 && (

{roundIds.length} round{roundIds.length > 1 ? 's' : ''} selected

)}
)} {/* Exclude filters for Round Applicants */} {recipientType === 'ROUND_APPLICANTS' && roundIds.length > 0 && (
setExcludeRejected(!!v)} />
setExcludeWithdrawn(!!v)} />
)} {recipientType === 'PROGRAM_TEAM' && (
)} {recipientType === 'USER' && (
)} {recipientType === 'ALL' && (

This message will be sent to all platform users.

)} {/* Template selector */} {templates && (templates as unknown[]).length > 0 && (
)} {/* Subject */}
setSubject(e.target.value)} />
{/* Body */}
Variables: {'{{projectName}}'}, {'{{userName}}'}, {'{{deadline}}'}, {'{{roundName}}'}, {'{{programName}}'}