'use client' import { useMemo, useState } from 'react' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Button } from '@/components/ui/button' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' import { Textarea } from '@/components/ui/textarea' import { Loader2 } from 'lucide-react' import { formatEnumLabel } from '@/lib/utils' import type { FinalistConfirmationStatus } from '@prisma/client' import { AdminAttendanceDialog, type AttendanceMode } from './admin-attendance-dialog' interface Props { programId: string } type StatusFilter = 'all' | FinalistConfirmationStatus const STATUS_BADGE: Record< FinalistConfirmationStatus, { label: string; variant: 'default' | 'secondary' | 'destructive' | 'outline' } > = { PENDING: { label: 'Pending', variant: 'secondary' }, CONFIRMED: { label: 'Confirmed', variant: 'default' }, DECLINED: { label: 'Declined', variant: 'destructive' }, EXPIRED: { label: 'Expired', variant: 'outline' }, SUPERSEDED: { label: 'Superseded', variant: 'outline' }, } function formatDeadline(d: Date): string { return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short', }).format(d) } function relativeFromNow(d: Date): string { const ms = d.getTime() - Date.now() if (ms <= 0) return 'past deadline' const hours = Math.floor(ms / 3_600_000) const days = Math.floor(hours / 24) if (days >= 1) return `in ${days}d` return `in ${hours}h` } export function ConfirmationsTab({ programId }: Props) { const utils = trpc.useUtils() const [statusFilter, setStatusFilter] = useState('all') const [dialogState, setDialogState] = useState<{ open: boolean mode: AttendanceMode confirmationId: string | null }>({ open: false, mode: 'confirm', confirmationId: null }) const [unconfirmState, setUnconfirmState] = useState<{ open: boolean confirmationId: string | null projectTitle: string reason: string }>({ open: false, confirmationId: null, projectTitle: '', reason: '' }) const { data, isLoading } = trpc.logistics.listConfirmations.useQuery( { programId }, { refetchInterval: 60_000 }, ) // Get liveFinalRoundId for re-invite action const { data: candidatesData } = trpc.finalist.listEnrollmentCandidates.useQuery({ programId }) const liveFinalRoundId = candidatesData?.liveFinalRoundId ?? null const unconfirmMutation = trpc.finalist.unconfirm.useMutation({ onSuccess: () => { toast.success('Finalist un-confirmed') utils.logistics.listConfirmations.invalidate({ programId }) utils.finalist.listEnrollmentCandidates.invalidate({ programId }) setUnconfirmState((prev) => ({ ...prev, open: false, confirmationId: null, reason: '' })) }, onError: (err) => toast.error(err.message), }) const reinviteMutation = trpc.finalist.enrollFinalists.useMutation({ onSuccess: (result) => { if (result.skipped.length > 0) { toast.info('Re-invite skipped — team is already confirmed') } else { toast.success('Re-invite sent') } utils.logistics.listConfirmations.invalidate({ programId }) utils.finalist.listEnrollmentCandidates.invalidate({ programId }) }, onError: (err) => toast.error(err.message), }) const filtered = useMemo(() => { if (!data) return [] return statusFilter === 'all' ? data : data.filter((r) => r.status === statusFilter) }, [data, statusFilter]) const totals = useMemo(() => { const counts: Record = { PENDING: 0, CONFIRMED: 0, DECLINED: 0, EXPIRED: 0, SUPERSEDED: 0, } for (const r of data ?? []) counts[r.status]++ return counts }, [data]) const StatusPill = ({ value, label, count }: { value: StatusFilter; label: string; count: number }) => ( ) return (
All confirmations
{isLoading ? (
{[1, 2, 3].map((i) => ( ))}
) : filtered.length === 0 ? (

{statusFilter === 'all' ? 'No finalists have been selected yet. Enroll finalists from the Grand Final round\'s Overview tab to start confirmations.' : 'No confirmations match this filter.'}

) : (
Project Status Deadline Attendees Notes Actions {filtered.map((r) => { const badge = STATUS_BADGE[r.status] const isPending = r.status === 'PENDING' return (
{r.project.title}
{formatEnumLabel(r.category)} {r.project.country && ( <> {' · '} {r.project.country} )}
{badge.label} {r.promotedFromWaitlistEntryId && ( Waitlist )}
{formatDeadline(new Date(r.deadline))}
{r.status === 'PENDING' && (
{relativeFromNow(new Date(r.deadline))}
)}
{r.attendeeCount} {r.status === 'DECLINED' && r.declineReason ? `Reason: ${r.declineReason}` : r.status === 'CONFIRMED' && r.confirmedAt ? `Confirmed ${formatDeadline(new Date(r.confirmedAt))}` : r.status === 'EXPIRED' && r.expiredAt ? `Expired ${formatDeadline(new Date(r.expiredAt))}` : '—'} {isPending ? (
) : r.status === 'CONFIRMED' ? ( ) : r.status === 'DECLINED' || r.status === 'EXPIRED' ? ( ) : ( )}
) })}
)}
setDialogState((prev) => ({ ...prev, open: next })) } /> {/* Un-confirm AlertDialog (needs a reason — min 5 chars per the server) */} { if (!unconfirmMutation.isPending) setUnconfirmState((prev) => ({ ...prev, open: next })) }} > Un-confirm this finalist? {unconfirmState.projectTitle} will be moved back to Superseded. Any active mentor assignment will be dropped and the mentor notified. This action is audit-logged.