'use client' import React, { useState, useMemo } from 'react' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' import { Checkbox } from '@/components/ui/checkbox' import { Skeleton } from '@/components/ui/skeleton' import { Clock, CheckCircle2, AlertTriangle, ArrowRight, Loader2, Search, ChevronDown, ChevronRight, Mail, Send, Eye, } from 'lucide-react' import { cn } from '@/lib/utils' import { projectStateConfig } from '@/lib/round-config' import { EmailPreviewDialog } from './email-preview-dialog' // ── Types ────────────────────────────────────────────────────────────────── interface FinalizationTabProps { roundId: string roundStatus: string } type ProposedOutcome = 'PASSED' | 'REJECTED' const stateColors: Record = Object.fromEntries( Object.entries(projectStateConfig).map(([k, v]) => [k, v.bg]) ) const stateLabelColors: Record = { PENDING: 'bg-gray-100 text-gray-700', IN_PROGRESS: 'bg-blue-100 text-blue-700', COMPLETED: 'bg-indigo-100 text-indigo-700', PASSED: 'bg-green-100 text-green-700', REJECTED: 'bg-red-100 text-red-700', WITHDRAWN: 'bg-yellow-100 text-yellow-700', } // ── Main Component ───────────────────────────────────────────────────────── export function FinalizationTab({ roundId, roundStatus }: FinalizationTabProps) { const utils = trpc.useUtils() const { data: summary, isLoading } = trpc.roundEngine.getFinalizationSummary.useQuery( { roundId }, ) const [search, setSearch] = useState('') const [filterOutcome, setFilterOutcome] = useState<'all' | 'PASSED' | 'REJECTED' | 'none'>('all') const [filterCategory, setFilterCategory] = useState<'all' | 'STARTUP' | 'BUSINESS_CONCEPT'>('all') const [selectedIds, setSelectedIds] = useState>(new Set()) const [emailSectionOpen, setEmailSectionOpen] = useState(false) const [advancementMessage, setAdvancementMessage] = useState('') const [rejectionMessage, setRejectionMessage] = useState('') const [advancePreviewOpen, setAdvancePreviewOpen] = useState(false) const [rejectPreviewOpen, setRejectPreviewOpen] = useState(false) const [advancePreviewMsg, setAdvancePreviewMsg] = useState() const [rejectPreviewMsg, setRejectPreviewMsg] = useState() // Mutations const updateOutcome = trpc.roundEngine.updateProposedOutcome.useMutation({ onSuccess: () => utils.roundEngine.getFinalizationSummary.invalidate({ roundId }), }) const batchUpdate = trpc.roundEngine.batchUpdateProposedOutcomes.useMutation({ onSuccess: () => { utils.roundEngine.getFinalizationSummary.invalidate({ roundId }) setSelectedIds(new Set()) toast.success('Proposed outcomes updated') }, }) const confirmMutation = trpc.roundEngine.confirmFinalization.useMutation({ onSuccess: (data) => { utils.roundEngine.getFinalizationSummary.invalidate({ roundId }) toast.success( `Finalized: ${data.advanced} advanced, ${data.rejected} rejected, ${data.emailsSent} emails sent`, ) }, onError: (err) => toast.error(err.message), }) const endGraceMutation = trpc.roundEngine.endGracePeriod.useMutation({ onSuccess: () => { utils.roundEngine.getFinalizationSummary.invalidate({ roundId }) toast.success('Grace period ended, projects processed') }, onError: (err) => toast.error(err.message), }) const processProjectsMutation = trpc.roundEngine.processRoundProjects.useMutation({ onSuccess: (data) => { utils.roundEngine.getFinalizationSummary.invalidate({ roundId }) toast.success(`Processed ${data.processed} projects — review proposed outcomes below`) }, onError: (err) => toast.error(err.message), }) // Email preview queries const advancePreview = trpc.roundEngine.previewFinalizationAdvancementEmail.useQuery( { roundId, customMessage: advancePreviewMsg }, { enabled: advancePreviewOpen } ) const rejectPreview = trpc.roundEngine.previewFinalizationRejectionEmail.useQuery( { roundId, customMessage: rejectPreviewMsg }, { enabled: rejectPreviewOpen } ) // Filtered projects const filteredProjects = useMemo(() => { if (!summary) return [] return summary.projects.filter((p) => { const matchesSearch = !search || p.title.toLowerCase().includes(search.toLowerCase()) || p.teamName?.toLowerCase().includes(search.toLowerCase()) || p.country?.toLowerCase().includes(search.toLowerCase()) const matchesFilter = filterOutcome === 'all' || (filterOutcome === 'none' && !p.proposedOutcome) || p.proposedOutcome === filterOutcome const matchesCategory = filterCategory === 'all' || p.category === filterCategory return matchesSearch && matchesFilter && matchesCategory }) }, [summary, search, filterOutcome, filterCategory]) // Check if we have multiple categories (to decide whether to group) const hasMultipleCategories = useMemo(() => { if (!summary) return false const cats = new Set(summary.projects.map((p) => p.category).filter(Boolean)) return cats.size > 1 }, [summary]) // Group filtered projects by category, sorted by rank within each group const groupedProjects = useMemo(() => { if (!hasMultipleCategories || filterCategory !== 'all') return null const groups: { category: string; label: string; projects: typeof filteredProjects }[] = [] const startups = filteredProjects.filter((p) => p.category === 'STARTUP') const concepts = filteredProjects.filter((p) => p.category === 'BUSINESS_CONCEPT') const other = filteredProjects.filter((p) => p.category !== 'STARTUP' && p.category !== 'BUSINESS_CONCEPT') if (startups.length > 0) groups.push({ category: 'STARTUP', label: 'Startups', projects: startups }) if (concepts.length > 0) groups.push({ category: 'BUSINESS_CONCEPT', label: 'Business Concepts', projects: concepts }) if (other.length > 0) groups.push({ category: 'OTHER', label: 'Other', projects: other }) return groups }, [filteredProjects, hasMultipleCategories, filterCategory]) // Counts const passedCount = summary?.projects.filter((p) => p.proposedOutcome === 'PASSED').length ?? 0 const rejectedCount = summary?.projects.filter((p) => p.proposedOutcome === 'REJECTED').length ?? 0 const undecidedCount = summary?.projects.filter((p) => !p.proposedOutcome).length ?? 0 // Select all toggle const allSelected = filteredProjects.length > 0 && filteredProjects.every((p) => selectedIds.has(p.id)) const toggleSelectAll = () => { if (allSelected) { setSelectedIds(new Set()) } else { setSelectedIds(new Set(filteredProjects.map((p) => p.id))) } } // Bulk set outcome const handleBulkSetOutcome = (outcome: ProposedOutcome) => { const outcomes: Record = {} for (const id of selectedIds) { outcomes[id] = outcome } batchUpdate.mutate({ roundId, outcomes }) } // Column count for colSpan const colCount = (summary?.isFinalized ? 0 : 1) + 4 + (summary?.roundType === 'EVALUATION' ? 1 : 0) + 1 // Shared row renderer const renderProjectRow = (project: (typeof filteredProjects)[number]) => ( {!summary?.isFinalized && ( { const next = new Set(selectedIds) if (checked) next.add(project.id) else next.delete(project.id) setSelectedIds(next) }} aria-label={`Select ${project.title}`} /> )}
{project.title}
{project.teamName && (
{project.teamName}
)} {project.category === 'STARTUP' ? 'Startup' : project.category === 'BUSINESS_CONCEPT' ? 'Concept' : project.category ?? '-'} {project.country ?? '-'} {project.currentState.replace('_', ' ')} {summary?.roundType === 'EVALUATION' && ( {project.evaluationScore != null ? `${project.evaluationScore.toFixed(1)} (#${project.rankPosition ?? '-'})` : '-'} )} {summary?.isFinalized ? ( {project.proposedOutcome === 'PASSED' ? 'Advanced' : 'Rejected'} ) : ( )} ) if (isLoading) { return (
) } if (!summary) return null return (
{/* Grace Period Banner */} {summary.isGracePeriodActive && (

Grace Period Active

Applicants can still submit until{' '} {summary.gracePeriodEndsAt ? new Date(summary.gracePeriodEndsAt).toLocaleString() : 'the grace period ends'}

)} {/* Finalized Banner */} {summary.isFinalized && (

Round Finalized

Finalized on{' '} {summary.finalizedAt ? new Date(summary.finalizedAt).toLocaleString() : 'unknown date'}

)} {/* Needs Processing Banner */} {!summary.isFinalized && !summary.isGracePeriodActive && summary.projects.length > 0 && summary.projects.every((p) => !p.proposedOutcome) && (

Projects Need Processing

{summary.projects.length} project{summary.projects.length !== 1 ? 's' : ''} in this round have no proposed outcome. Click "Process" to auto-assign outcomes based on round type and project activity.

)} {/* Summary Stats Bar */}
{([ ['Pending', summary.stats.pending, 'bg-gray-100 text-gray-700'], ['In Progress', summary.stats.inProgress, 'bg-blue-100 text-blue-700'], ['Completed', summary.stats.completed, 'bg-indigo-100 text-indigo-700'], ['Passed', summary.stats.passed, 'bg-green-100 text-green-700'], ['Rejected', summary.stats.rejected, 'bg-red-100 text-red-700'], ['Withdrawn', summary.stats.withdrawn, 'bg-yellow-100 text-yellow-700'], ] as const).map(([label, count, cls]) => (
{count}
{label}
))}
{/* Category Target Progress */} {(summary.categoryTargets.startupTarget != null || summary.categoryTargets.conceptTarget != null) && ( Advancement Targets {summary.categoryTargets.startupTarget != null && (
Startup {summary.categoryTargets.startupProposed} / {summary.categoryTargets.startupTarget}
summary.categoryTargets.startupTarget ? 'bg-amber-500' : 'bg-green-500', )} style={{ width: `${Math.min(100, (summary.categoryTargets.startupProposed / summary.categoryTargets.startupTarget) * 100)}%`, }} />
)} {summary.categoryTargets.conceptTarget != null && (
Business Concept {summary.categoryTargets.conceptProposed} / {summary.categoryTargets.conceptTarget}
summary.categoryTargets.conceptTarget ? 'bg-amber-500' : 'bg-green-500', )} style={{ width: `${Math.min(100, (summary.categoryTargets.conceptProposed / summary.categoryTargets.conceptTarget) * 100)}%`, }} />
)} )} {/* Proposed Outcomes Table */}
Proposed Outcomes
{passedCount} advancing {rejectedCount} rejected {undecidedCount > 0 && ( {undecidedCount} undecided )}
{/* Search + Filter */}
setSearch(e.target.value)} className="pl-9" />
{hasMultipleCategories && ( )}
{/* Bulk actions */} {selectedIds.size > 0 && !summary.isFinalized && (
{selectedIds.size} selected
)} {/* Table */}
{!summary.isFinalized && ( )} {summary.roundType === 'EVALUATION' && ( )} {groupedProjects ? ( groupedProjects.map((group) => ( {group.projects.map((project) => renderProjectRow(project))} )) ) : ( filteredProjects.map((project) => renderProjectRow(project)) )} {filteredProjects.length === 0 && ( )}
Project Category Country Current StateScore / RankProposed Outcome
{group.label} ({group.projects.filter((p) => p.proposedOutcome === 'PASSED').length} pass, {group.projects.filter((p) => p.proposedOutcome === 'REJECTED').length} reject, {group.projects.length} total)
No projects match your search/filter
{/* Email Customization + Confirm */} {!summary.isFinalized && !summary.isGracePeriodActive && ( {emailSectionOpen && ( {/* Account stats */} {(summary.accountStats.needsInvite > 0 || summary.accountStats.hasAccount > 0) && (
{summary.accountStats.needsInvite > 0 && ( {summary.accountStats.needsInvite} project{summary.accountStats.needsInvite !== 1 ? 's' : ''} will receive a{' '} Create Account{' '} invite link )} {summary.accountStats.hasAccount > 0 && ( {summary.accountStats.hasAccount} project{summary.accountStats.hasAccount !== 1 ? 's' : ''} will receive a{' '} View Dashboard{' '} link )}
)}