"use client"; import { format, formatDistanceToNow } from "date-fns"; import { AlertCircle, CheckCircle, Clock, Download, Eye, MoreHorizontal, Plus, Search, Shield, Target, Trash2, Upload, Users, UserX, } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useState } from "react"; import { Alert, AlertDescription } from "~/components/ui/alert"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "~/components/ui/dropdown-menu"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { Textarea } from "~/components/ui/textarea"; import { api } from "~/trpc/react"; interface Participant { id: string; participantCode: string; email: string | null; name: string | null; demographics: Record; consentGiven: boolean; consentDate: Date | null; notes: string | null; createdAt: Date; updatedAt: Date; studyId: string; trialCount: number; _count?: { trials: number; }; } export function ParticipantsView() { const router = useRouter(); const [searchQuery, setSearchQuery] = useState(""); const [studyFilter, setStudyFilter] = useState("all"); const [consentFilter, setConsentFilter] = useState("all"); const [sortBy, setSortBy] = useState("createdAt"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); const [showNewParticipantDialog, setShowNewParticipantDialog] = useState(false); const [showConsentDialog, setShowConsentDialog] = useState(false); const [selectedParticipant, setSelectedParticipant] = useState(null); const [newParticipant, setNewParticipant] = useState<{ participantCode: string; email: string; name: string; studyId: string; demographics: Record; notes: string; }>({ participantCode: "", email: "", name: "", studyId: "", demographics: {}, notes: "", }); // Get current user's studies const { data: userStudies } = api.studies.list.useQuery({ memberOnly: true, limit: 100, }); // Get participants with filtering const { data: participantsData, isLoading: participantsLoading, refetch, } = api.participants.list.useQuery( { studyId: studyFilter === "all" ? (userStudies?.studies?.[0]?.id ?? "") : studyFilter, search: searchQuery ?? undefined, limit: 100, }, { enabled: !!userStudies?.studies?.length, }, ); // Mutations const createParticipantMutation = api.participants.create.useMutation({ onSuccess: () => { void refetch(); setShowNewParticipantDialog(false); resetNewParticipantForm(); }, }); const updateConsentMutation = api.participants.update.useMutation({ onSuccess: () => { void refetch(); setShowConsentDialog(false); setSelectedParticipant(null); }, }); const deleteParticipantMutation = api.participants.delete.useMutation({ onSuccess: () => { void refetch(); }, }); const resetNewParticipantForm = () => { setNewParticipant({ participantCode: "", email: "", name: "", studyId: "", demographics: {}, notes: "", }); }; const handleCreateParticipant = useCallback(async () => { if (!newParticipant.participantCode || !newParticipant.studyId) return; try { await createParticipantMutation.mutateAsync({ participantCode: newParticipant.participantCode, studyId: newParticipant.studyId, email: newParticipant.email ? newParticipant.email : undefined, name: newParticipant.name ? newParticipant.name : undefined, demographics: newParticipant.demographics, }); } catch (_error) { console.error("Failed to create participant:", _error); } }, [createParticipantMutation, newParticipant]); const handleUpdateConsent = useCallback(async () => { if (!selectedParticipant) return; try { await updateConsentMutation.mutateAsync({ id: selectedParticipant.id, }); } catch (_error) { console.error("Failed to update consent:", _error); } }, [selectedParticipant, updateConsentMutation]); const handleDeleteParticipant = useCallback( async (participantId: string) => { if ( !confirm( "Are you sure you want to delete this participant? This action cannot be undone.", ) ) { return; } try { await deleteParticipantMutation.mutateAsync({ id: participantId }); } catch (_error) { console.error("Failed to delete participant:", _error); } }, [deleteParticipantMutation], ); const getConsentStatusBadge = (participant: Participant) => { if (participant.consentGiven) { return ( Consented ); } else { return ( Pending ); } }; const getTrialsBadge = (trialCount: number) => { if (trialCount === 0) { return No trials; } else if (trialCount === 1) { return 1 trial; } else { return ( {trialCount} trials ); } }; const filteredParticipants: Participant[] = (participantsData?.participants?.filter((participant) => { if (consentFilter === "consented" && !participant.consentGiven) { return false; } if (consentFilter === "pending" && participant.consentGiven) { return false; } return true; }) as Participant[] | undefined) ?? []; return (
{/* Header Actions */}
Participant Management

Manage participant registration, consent, and trial assignments

{/* Filters and Search */}
setSearchQuery(e.target.value)} className="pl-10" />
{/* Statistics */}

{participantsData?.pagination?.total ?? 0}

Total Participants

{ filteredParticipants.filter( (p: Participant) => p.consentGiven, ).length }

Consented

{ filteredParticipants.filter( (p: Participant) => !p.consentGiven, ).length }

Pending Consent

{filteredParticipants.reduce( (sum: number, p: Participant) => sum + (p.trialCount ?? 0), 0, )}

Total Trials

{/* Participants Table */} {participantsLoading ? (

Loading participants...

) : filteredParticipants.length === 0 ? (

No participants found

{searchQuery || studyFilter !== "all" || consentFilter !== "all" ? "Try adjusting your filters" : "Add your first participant to get started"}

) : ( Participant Study Consent Status Trials Registered {filteredParticipants.map((participant) => (
{participant.participantCode .slice(0, 2) .toUpperCase()}

{participant.participantCode}

{participant.name && (

{participant.name}

)} {participant.email && (

{participant.email}

)}
{userStudies?.studies?.find( (s) => s.id === participant.studyId, )?.name ?? "Unknown Study"}
{getConsentStatusBadge(participant)} {participant.consentDate && (

{format( new Date(participant.consentDate), "MMM d, yyyy", )}

)}
{getTrialsBadge(participant.trialCount ?? 0)}
{formatDistanceToNow(new Date(participant.createdAt), { addSuffix: true, })}
Actions router.push(`/participants/${participant.id}`) } > View Details { setSelectedParticipant(participant); setShowConsentDialog(true); }} > Manage Consent handleDeleteParticipant(participant.id) } className="text-red-600" > Delete
))}
)}
{/* New Participant Dialog */} Add New Participant Register a new participant for study enrollment
setNewParticipant((prev) => ({ ...prev, participantCode: e.target.value, })) } placeholder="P001, SUBJ_01, etc." className="mt-1" />
setNewParticipant((prev) => ({ ...prev, name: e.target.value, })) } placeholder="Participant's name" className="mt-1" />
setNewParticipant((prev) => ({ ...prev, email: e.target.value, })) } placeholder="participant@example.com" className="mt-1" />