diff --git a/src/app/(dashboard)/studies/[id]/forms/[formId]/page.tsx b/src/app/(dashboard)/studies/[id]/forms/[formId]/page.tsx new file mode 100644 index 0000000..2917caa --- /dev/null +++ b/src/app/(dashboard)/studies/[id]/forms/[formId]/page.tsx @@ -0,0 +1,506 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { useSession } from "~/lib/auth-client"; +import { notFound } from "next/navigation"; +import Link from "next/link"; +import { + FileText, + ArrowLeft, + Plus, + Trash2, + GripVertical, + FileSignature, + ClipboardList, + FileQuestion, + Save, + Eye, + Edit2, + Users, + CheckCircle, +} from "lucide-react"; +import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Textarea } from "~/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; +import { Badge } from "~/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; +import { api } from "~/trpc/react"; +import { toast } from "sonner"; + +interface Field { + id: string; + type: string; + label: string; + required: boolean; + options?: string[]; + settings?: Record; +} + +const fieldTypes = [ + { value: "text", label: "Text (short)", icon: "📝" }, + { value: "textarea", label: "Text (long)", icon: "📄" }, + { value: "multiple_choice", label: "Multiple Choice", icon: "☑️" }, + { value: "checkbox", label: "Checkbox", icon: "✅" }, + { value: "rating", label: "Rating Scale", icon: "⭐" }, + { value: "yes_no", label: "Yes/No", icon: "✔️" }, + { value: "date", label: "Date", icon: "📅" }, + { value: "signature", label: "Signature", icon: "✍️" }, +]; + +const formTypeIcons = { + consent: FileSignature, + survey: ClipboardList, + questionnaire: FileQuestion, +}; + +const statusColors = { + pending: "bg-yellow-100 text-yellow-700", + completed: "bg-green-100 text-green-700", + rejected: "bg-red-100 text-red-700", +}; + +interface FormViewPageProps { + params: Promise<{ + id: string; + formId: string; + }>; +} + +export default function FormViewPage({ params }: FormViewPageProps) { + const { data: session } = useSession(); + const router = useRouter(); + const utils = api.useUtils(); + const [resolvedParams, setResolvedParams] = useState<{ id: string; formId: string } | null>(null); + const [isEditing, setIsEditing] = useState(false); + + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [fields, setFields] = useState([]); + + useEffect(() => { + const resolveParams = async () => { + const resolved = await params; + setResolvedParams(resolved); + }; + void resolveParams(); + }, [params]); + + const { data: study } = api.studies.get.useQuery( + { id: resolvedParams?.id ?? "" }, + { enabled: !!resolvedParams?.id }, + ); + + const { data: form, isLoading } = api.forms.get.useQuery( + { id: resolvedParams?.formId ?? "" }, + { enabled: !!resolvedParams?.formId }, + ); + + const { data: responsesData } = api.forms.getResponses.useQuery( + { formId: resolvedParams?.formId ?? "", limit: 50 }, + { enabled: !!resolvedParams?.formId }, + ); + + const userRole = (study as any)?.userRole; + const canManage = userRole === "owner" || userRole === "researcher"; + + const updateForm = api.forms.update.useMutation({ + onSuccess: () => { + toast.success("Form updated successfully!"); + setIsEditing(false); + void utils.forms.get.invalidate({ id: resolvedParams?.formId }); + }, + onError: (error) => { + toast.error("Failed to update form", { description: error.message }); + }, + }); + + useEffect(() => { + if (form) { + setTitle(form.title); + setDescription(form.description || ""); + setFields((form.fields as Field[]) || []); + } + }, [form]); + + useBreadcrumbsEffect([ + { label: "Dashboard", href: "/dashboard" }, + { label: "Studies", href: "/studies" }, + { label: study?.name ?? "Study", href: `/studies/${resolvedParams?.id}` }, + { label: "Forms", href: `/studies/${resolvedParams?.id}/forms` }, + { label: form?.title ?? "Form" }, + ]); + + if (!session?.user) { + return notFound(); + } + + if (isLoading || !form) return
Loading...
; + + const TypeIcon = formTypeIcons[form.type as keyof typeof formTypeIcons] || FileText; + const responses = responsesData?.responses ?? []; + + const addField = (type: string) => { + const newField: Field = { + id: crypto.randomUUID(), + type, + label: `New ${fieldTypes.find(f => f.value === type)?.label || "Field"}`, + required: false, + options: type === "multiple_choice" ? ["Option 1", "Option 2"] : undefined, + }; + setFields([...fields, newField]); + }; + + const removeField = (id: string) => { + setFields(fields.filter(f => f.id !== id)); + }; + + const updateField = (id: string, updates: Partial) => { + setFields(fields.map(f => f.id === id ? { ...f, ...updates } : f)); + }; + + const handleSave = () => { + updateForm.mutate({ + id: form.id, + title, + description, + fields, + settings: form.settings as Record, + }); + }; + + return ( +
+
+
+ +
+
+ +

{form.title}

+ {form.active && ( + Active + )} +
+

+ {form.type} • Version {form.version} +

+
+
+ {canManage && ( +
+ {isEditing ? ( + <> + + + + ) : ( + + )} +
+ )} +
+ + + + Fields + Preview + + Responses ({responses.length}) + + + + + {isEditing ? ( + + + Form Fields + + + + {fields.length === 0 ? ( +
+ +

No fields added yet

+
+ ) : ( +
+ {fields.map((field) => ( +
+
+ +
+
+
+ + {fieldTypes.find(f => f.value === field.type)?.icon}{" "} + {fieldTypes.find(f => f.value === field.type)?.label} + + updateField(field.id, { label: e.target.value })} + placeholder="Field label" + className="flex-1" + /> + +
+ {field.type === "multiple_choice" && ( +
+ + {field.options?.map((opt, i) => ( +
+ { + const newOptions = [...(field.options || [])]; + newOptions[i] = e.target.value; + updateField(field.id, { options: newOptions }); + }} + placeholder={`Option ${i + 1}`} + className="flex-1" + /> + +
+ ))} + +
+ )} +
+ +
+ ))} +
+ )} +
+
+ ) : ( + + + Form Fields + + + {fields.length === 0 ? ( +

No fields defined

+ ) : ( +
+ {fields.map((field, index) => ( +
+ + {index + 1} + +
+

{field.label}

+

+ {fieldTypes.find(f => f.value === field.type)?.label} + {field.required && " • Required"} + {field.type === "multiple_choice" && ` • ${field.options?.length} options`} +

+
+
+ ))} +
+ )} +
+
+ )} +
+ + + + + Form Preview + + +
+

{title}

+ {description &&

{description}

} +
+ {fields.length === 0 ? ( +

No fields to preview

+ ) : ( +
+ {fields.map((field, index) => ( +
+ + {field.type === "text" && ( + + )} + {field.type === "textarea" && ( +