"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { TestTube } from "lucide-react"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider"; import { EntityForm, FormField, FormSection, Tips, } from "~/components/ui/entity-form"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { Textarea } from "~/components/ui/textarea"; import { useStudyContext } from "~/lib/study-context"; import { api } from "~/trpc/react"; import { Calendar as CalendarIcon, Clock } from "lucide-react"; import { format } from "date-fns"; import { cn } from "~/lib/utils"; import { Button } from "~/components/ui/button"; import { Calendar } from "~/components/ui/calendar"; import { Popover, PopoverContent, PopoverTrigger, } from "~/components/ui/popover"; import { Controller } from "react-hook-form"; // Custom DatePickerTime component based on user request function DateTimePicker({ value, onChange, }: { value: Date | undefined; onChange: (date: Date | undefined) => void; }) { const [open, setOpen] = useState(false); // Parse time from value or default const timeValue = value ? format(value, "HH:mm") : "12:00"; const onDateSelect = (newDate: Date | undefined) => { if (!newDate) { onChange(undefined); setOpen(false); return; } // Preserve existing time or use default const [hours, minutes] = timeValue.split(":").map(Number); const updatedDate = new Date(newDate); updatedDate.setHours(hours || 0); updatedDate.setMinutes(minutes || 0); updatedDate.setSeconds(0); onChange(updatedDate); setOpen(false); }; const onTimeChange = (e: React.ChangeEvent) => { const newTime = e.target.value; if (!value) return; // Can't set time without date const [hours, minutes] = newTime.split(":").map(Number); const updatedDate = new Date(value); updatedDate.setHours(hours || 0); updatedDate.setMinutes(minutes || 0); updatedDate.setSeconds(0); onChange(updatedDate); }; return (
); } const trialSchema = z.object({ experimentId: z.string().uuid("Please select an experiment"), participantId: z.string().uuid("Please select a participant"), scheduledAt: z.date(), wizardId: z.string().uuid().optional(), notes: z.string().max(1000, "Notes cannot exceed 1000 characters").optional(), sessionNumber: z .number() .min(1, "Session number must be at least 1") .optional(), }); type TrialFormData = z.infer; interface TrialFormProps { mode: "create" | "edit"; trialId?: string; studyId?: string; } export function TrialForm({ mode, trialId, studyId }: TrialFormProps) { const router = useRouter(); const { selectedStudyId } = useStudyContext(); const contextStudyId = studyId ?? selectedStudyId; const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const form = useForm({ resolver: zodResolver(trialSchema), defaultValues: { sessionNumber: 1, }, }); // Fetch trial data for edit mode const { data: trial, isLoading, error: fetchError, } = api.trials.get.useQuery( { id: trialId! }, { enabled: mode === "edit" && !!trialId }, ); // Fetch experiments for the selected study const { data: experimentsData, isLoading: experimentsLoading } = api.experiments.list.useQuery( { studyId: contextStudyId! }, { enabled: !!contextStudyId }, ); // Fetch participants for the selected study const { data: participantsData, isLoading: participantsLoading } = api.participants.list.useQuery( { studyId: contextStudyId!, limit: 100 }, { enabled: !!contextStudyId }, ); // Fetch users who can be wizards const { data: usersData, isLoading: usersLoading } = api.users.getWizards.useQuery(); // Auto-increment session number const selectedParticipantId = form.watch("participantId"); const { data: latestSession } = api.trials.getLatestSession.useQuery( { participantId: selectedParticipantId }, { enabled: !!selectedParticipantId && mode === "create", refetchOnWindowFocus: false, }, ); useEffect(() => { if (latestSession !== undefined && mode === "create") { form.setValue("sessionNumber", latestSession + 1); } }, [latestSession, mode, form]); // Set breadcrumbs const breadcrumbs = [ { label: "Dashboard", href: "/dashboard" }, { label: "Studies", href: "/studies" }, ...(contextStudyId ? [ { label: "Study", href: `/studies/${contextStudyId}`, }, { label: "Trials", href: `/studies/${contextStudyId}/trials` }, ...(mode === "edit" && trial ? [ { label: `Trial ${trial.sessionNumber || trial.id.slice(-8)}`, href: `/studies/${contextStudyId}/trials/${trial.id}`, }, { label: "Edit" }, ] : [{ label: "New Trial" }]), ] : [ { label: "Trials", href: `/studies/${contextStudyId}/trials` }, ...(mode === "edit" && trial ? [ { label: `Trial ${trial.sessionNumber || trial.id.slice(-8)}`, href: `/studies/${contextStudyId}/trials/${trial.id}`, }, { label: "Edit" }, ] : [{ label: "New Trial" }]), ]), ]; useBreadcrumbsEffect(breadcrumbs); // Populate form with existing data in edit mode useEffect(() => { if (mode === "edit" && trial) { form.reset({ experimentId: trial.experimentId, participantId: trial?.participantId ?? "", scheduledAt: trial.scheduledAt ? new Date(trial.scheduledAt) : undefined, wizardId: trial.wizardId ?? undefined, notes: trial.notes ?? "", sessionNumber: trial.sessionNumber ?? 1, }); } }, [trial, mode, form]); const createTrialMutation = api.trials.create.useMutation(); const updateTrialMutation = api.trials.update.useMutation(); // Form submission const onSubmit = async (data: TrialFormData) => { setIsSubmitting(true); setError(null); try { if (mode === "create") { await createTrialMutation.mutateAsync({ experimentId: data.experimentId, participantId: data.participantId, scheduledAt: data.scheduledAt, wizardId: data.wizardId, sessionNumber: data.sessionNumber ?? 1, notes: data.notes ?? undefined, }); // Redirect to trials table instead of detail page router.push(`/studies/${contextStudyId}/trials`); } else { await updateTrialMutation.mutateAsync({ id: trialId!, scheduledAt: data.scheduledAt, wizardId: data.wizardId, sessionNumber: data.sessionNumber ?? 1, notes: data.notes ?? undefined, }); // Redirect to trials table on update too router.push(`/studies/${contextStudyId}/trials`); } } catch (error) { setError( `Failed to ${mode} trial: ${error instanceof Error ? error.message : "Unknown error"}`, ); } finally { setIsSubmitting(false); } }; // Loading state for edit mode if (mode === "edit" && isLoading) { return
Loading trial...
; } // Error state for edit mode if (mode === "edit" && fetchError) { return
Error loading trial: {fetchError.message}
; } return (
{/* Left Column: Main Info (Spans 2) */}
{form.formState.errors.experimentId && (

{form.formState.errors.experimentId.message}

)} {mode === "edit" && (

Experiment cannot be changed after creation

)}
{form.formState.errors.participantId && (

{form.formState.errors.participantId.message}

)} {mode === "edit" && (

Participant cannot be changed after creation

)}
( )} /> {form.formState.errors.scheduledAt && (

{form.formState.errors.scheduledAt.message}

)}

When should this trial be conducted?

{form.formState.errors.sessionNumber && (

{form.formState.errors.sessionNumber.message}

)}

Auto-incremented based on participant history

{/* Right Column: Assignment & Notes (Spans 1) */}

Who will operate the robot?