"use client"; import { type ColumnDef } from "@tanstack/react-table"; import { ArrowUpDown, ChevronDown, MoreHorizontal, Play, Gamepad2, LineChart, Ban } from "lucide-react"; import * as React from "react"; import { format, formatDistanceToNow } from "date-fns"; import { AlertCircle } from "lucide-react"; import Link from "next/link"; import { useEffect } from "react"; import { Alert, AlertDescription } from "~/components/ui/alert"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { Card, CardContent } from "~/components/ui/card"; import { Checkbox } from "~/components/ui/checkbox"; import { DataTable } from "~/components/ui/data-table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "~/components/ui/dropdown-menu"; import { useStudyContext } from "~/lib/study-context"; import { api } from "~/trpc/react"; export type Trial = { id: string; sessionNumber: number; status: "scheduled" | "in_progress" | "completed" | "aborted" | "failed"; scheduledAt: Date | null; startedAt: Date | null; completedAt: Date | null; createdAt: Date; experimentName: string; experimentId: string; studyName: string; studyId: string; participantCode: string | null; participantName: string | null; participantId: string | null; wizardName: string | null; wizardId: string | null; eventCount: number; mediaCount: number; latestEventAt: Date | null; }; const statusConfig = { scheduled: { label: "Scheduled", className: "bg-blue-100 text-blue-800", }, in_progress: { label: "In Progress", className: "bg-yellow-100 text-yellow-800", }, completed: { label: "Completed", className: "bg-green-100 text-green-800", }, aborted: { label: "Aborted", className: "bg-gray-100 text-gray-800", }, failed: { label: "Failed", className: "bg-red-100 text-red-800", }, }; export const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} aria-label="Select all" /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} aria-label="Select row" /> ), enableSorting: false, enableHiding: false, }, { accessorKey: "sessionNumber", header: ({ column }) => { return ( ); }, cell: ({ row }) => { const sessionNumber = row.getValue("sessionNumber"); return (
#{Number(sessionNumber)}
); }, }, { accessorKey: "experimentName", header: ({ column }) => { return ( ); }, cell: ({ row }) => { const experimentName = row.getValue("experimentName"); const experimentId = row.original.experimentId; return (
{String(experimentName)}
); }, }, { accessorKey: "participantCode", header: "Participant", cell: ({ row }) => { const participantCode = row.getValue("participantCode"); const participantName = row.original?.participantName; const participantId = row.original?.participantId; if (!participantCode && !participantName) { return ( No participant ); } return (
{participantId ? ( {(participantCode ?? "Unknown") as string} ) : ( {(participantCode ?? "Unknown") as string} )} {participantName && (
{participantName}
)}
); }, }, { accessorKey: "wizardName", header: "Wizard", cell: ({ row }) => { const wizardName = row.getValue("wizardName"); if (!wizardName) { return ( No wizard ); } return (
{wizardName as string}
); }, }, { accessorKey: "status", header: "Status", cell: ({ row }) => { const status = row.getValue("status"); const statusInfo = statusConfig[status as keyof typeof statusConfig]; if (!statusInfo) { return ( Unknown ); } return ( {statusInfo.label} ); }, }, { accessorKey: "scheduledAt", header: ({ column }) => { return ( ); }, cell: ({ row }) => { const scheduledAt = row.getValue("scheduledAt"); const startedAt = row.original?.startedAt; const completedAt = row.original?.completedAt; if (completedAt) { return (
Completed
{formatDistanceToNow(new Date(completedAt), { addSuffix: true })}
); } if (startedAt) { return (
Started
{formatDistanceToNow(new Date(startedAt), { addSuffix: true })}
); } if (scheduledAt) { const scheduleDate = scheduledAt != null ? new Date(scheduledAt as string | number | Date) : null; const isUpcoming = scheduleDate && scheduleDate > new Date(); return (
{isUpcoming ? "Upcoming" : "Overdue"}
{scheduleDate ? format(scheduleDate, "MMM d, h:mm a") : "Unknown"}
); } return ( Not scheduled ); }, }, { accessorKey: "eventCount", header: "Data", cell: ({ row }) => { const eventCount = row.getValue("eventCount") ?? 0; const mediaCount = row.original?.mediaCount ?? 0; const latestEventAt = row.original?.latestEventAt ? new Date(row.original.latestEventAt) : null; return (
{Number(eventCount)} events {mediaCount > 0 && ( {mediaCount} media )}
{latestEventAt && (
Last evt:{" "} {latestEventAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", })}
)}
); }, }, { id: "actions", enableHiding: false, cell: ({ row }) => , }, ]; function ActionsCell({ row }: { row: { original: Trial } }) { const trial = row.original; // ActionsCell is a component rendered by the table. // importing useRouter is fine. const utils = api.useUtils(); const duplicateMutation = api.trials.duplicate.useMutation({ onSuccess: () => { utils.trials.list.invalidate(); // toast.success("Trial duplicated"); // We need toast }, }); if (!trial?.id) { return No actions; } return ( Actions {trial.status === "scheduled" && ( Start Trial )} {trial.status === "in_progress" && ( Control Trial )} {trial.status === "completed" && ( Analysis )} {(trial.status === "scheduled" || trial.status === "failed") && ( Cancel )} ); } interface TrialsTableProps { studyId?: string; } export function TrialsTable({ studyId }: TrialsTableProps = {}) { const { selectedStudyId } = useStudyContext(); const [statusFilter, setStatusFilter] = React.useState("all"); const { data: trialsData, isLoading, error, refetch, } = api.trials.list.useQuery( { studyId: studyId ?? selectedStudyId ?? "", limit: 50, }, { refetchOnWindowFocus: false, enabled: !!(studyId ?? selectedStudyId), }, ); // Refetch when active study changes useEffect(() => { if (selectedStudyId || studyId) { void refetch(); } }, [selectedStudyId, studyId, refetch]); // Adapt trials.list payload (no wizard, counts, sessionNumber, scheduledAt in list response) const data: Trial[] = React.useMemo(() => { if (!Array.isArray(trialsData)) return []; interface ListTrial { id: string; participantId: string | null; experimentId: string; status: Trial["status"]; sessionNumber: number | null; scheduledAt: Date | null; startedAt: Date | null; completedAt: Date | null; duration: number | null; notes: string | null; createdAt: Date; updatedAt: Date; experiment: { id: string; name: string; studyId: string }; participant?: { id: string; participantCode: string } | null; wizard?: { id: string | null; name: string | null; email: string | null; } | null; eventCount?: number; mediaCount?: number; latestEventAt?: Date | null; userRole: string; canAccess: boolean; } const mapped = (trialsData as ListTrial[]).map((t) => ({ id: t.id, sessionNumber: t.sessionNumber ?? 0, status: t.status, scheduledAt: t.scheduledAt ?? null, startedAt: t.startedAt ?? null, completedAt: t.completedAt ?? null, createdAt: t.createdAt, experimentName: t.experiment.name, experimentId: t.experiment.id, studyName: "Active Study", studyId: t.experiment.studyId, participantCode: t.participant?.participantCode ?? null, participantName: null, participantId: t.participant?.id ?? null, wizardName: t.wizard?.name ?? null, wizardId: t.wizard?.id ?? null, eventCount: t.eventCount ?? 0, mediaCount: t.mediaCount ?? 0, latestEventAt: t.latestEventAt ?? null, })); // Apply status filter (if not "all") if (statusFilter !== "all") { return mapped.filter((t) => t.status === statusFilter); } return mapped; }, [trialsData, statusFilter]); if (!selectedStudyId && !studyId) { return ( Please select a study to view trials. ); } if (error) { return ( Failed to load trials: {error.message} ); } const statusFilterComponent = ( setStatusFilter("all")}> All Status setStatusFilter("scheduled")}> Scheduled setStatusFilter("in_progress")}> In Progress setStatusFilter("completed")}> Completed setStatusFilter("aborted")}> Aborted setStatusFilter("failed")}> Failed ); return ( ); }