mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-23 19:27:51 -04:00
refactor: restructure study and participant forms into logical sections with separators and enhance EntityForm's layout flexibility for sidebar presence.
This commit is contained in:
@@ -256,14 +256,15 @@ export function ParticipantForm({
|
|||||||
<>
|
<>
|
||||||
<FormSection
|
<FormSection
|
||||||
title="Participant Information"
|
title="Participant Information"
|
||||||
description="Basic information about the research participant."
|
description="Basic identity and study association."
|
||||||
>
|
>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<FormField>
|
<FormField>
|
||||||
<Label htmlFor="participantCode">Participant Code *</Label>
|
<Label htmlFor="participantCode">Participant Code *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="participantCode"
|
id="participantCode"
|
||||||
{...form.register("participantCode")}
|
{...form.register("participantCode")}
|
||||||
placeholder="e.g., P001, SUBJ_01, etc."
|
placeholder="e.g., P001"
|
||||||
className={
|
className={
|
||||||
form.formState.errors.participantCode ? "border-red-500" : ""
|
form.formState.errors.participantCode ? "border-red-500" : ""
|
||||||
}
|
}
|
||||||
@@ -273,9 +274,6 @@ export function ParticipantForm({
|
|||||||
{form.formState.errors.participantCode.message}
|
{form.formState.errors.participantCode.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
Unique identifier for this participant within the study
|
|
||||||
</p>
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField>
|
<FormField>
|
||||||
@@ -283,7 +281,7 @@ export function ParticipantForm({
|
|||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
{...form.register("name")}
|
{...form.register("name")}
|
||||||
placeholder="Optional: Participant's full name"
|
placeholder="Optional name"
|
||||||
className={form.formState.errors.name ? "border-red-500" : ""}
|
className={form.formState.errors.name ? "border-red-500" : ""}
|
||||||
/>
|
/>
|
||||||
{form.formState.errors.name && (
|
{form.formState.errors.name && (
|
||||||
@@ -291,9 +289,6 @@ export function ParticipantForm({
|
|||||||
{form.formState.errors.name.message}
|
{form.formState.errors.name.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
Optional: Real name for contact purposes
|
|
||||||
</p>
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField>
|
<FormField>
|
||||||
@@ -310,11 +305,17 @@ export function ParticipantForm({
|
|||||||
{form.formState.errors.email.message}
|
{form.formState.errors.email.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
Optional: For scheduling and communication
|
|
||||||
</p>
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<div className="my-6" />
|
||||||
|
|
||||||
|
<FormSection
|
||||||
|
title="Demographics & Study"
|
||||||
|
description="study association and demographic details."
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<FormField>
|
<FormField>
|
||||||
<Label htmlFor="studyId">Study *</Label>
|
<Label htmlFor="studyId">Study *</Label>
|
||||||
<Select
|
<Select
|
||||||
@@ -323,11 +324,13 @@ export function ParticipantForm({
|
|||||||
disabled={studiesLoading || mode === "edit"}
|
disabled={studiesLoading || mode === "edit"}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
className={form.formState.errors.studyId ? "border-red-500" : ""}
|
className={
|
||||||
|
form.formState.errors.studyId ? "border-red-500" : ""
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={
|
placeholder={
|
||||||
studiesLoading ? "Loading studies..." : "Select a study"
|
studiesLoading ? "Loading..." : "Select study"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -344,18 +347,8 @@ export function ParticipantForm({
|
|||||||
{form.formState.errors.studyId.message}
|
{form.formState.errors.studyId.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{mode === "edit" && (
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
Study cannot be changed after registration
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</FormField>
|
</FormField>
|
||||||
</FormSection>
|
|
||||||
|
|
||||||
<FormSection
|
|
||||||
title="Demographics"
|
|
||||||
description="Optional demographic information for research purposes."
|
|
||||||
>
|
|
||||||
<FormField>
|
<FormField>
|
||||||
<Label htmlFor="age">Age</Label>
|
<Label htmlFor="age">Age</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -372,9 +365,6 @@ export function ParticipantForm({
|
|||||||
{form.formState.errors.age.message}
|
{form.formState.errors.age.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
Optional: Age in years (minimum 18)
|
|
||||||
</p>
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField>
|
<FormField>
|
||||||
@@ -394,7 +384,7 @@ export function ParticipantForm({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select gender (optional)" />
|
<SelectValue placeholder="Select gender" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="male">Male</SelectItem>
|
<SelectItem value="male">Male</SelectItem>
|
||||||
@@ -406,10 +396,8 @@ export function ParticipantForm({
|
|||||||
<SelectItem value="other">Other</SelectItem>
|
<SelectItem value="other">Other</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
Optional: Gender identity for demographic analysis
|
|
||||||
</p>
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
{mode === "create" && (
|
{mode === "create" && (
|
||||||
@@ -505,8 +493,7 @@ export function ParticipantForm({
|
|||||||
error={error}
|
error={error}
|
||||||
onDelete={mode === "edit" ? onDelete : undefined}
|
onDelete={mode === "edit" ? onDelete : undefined}
|
||||||
isDeleting={isDeleting}
|
isDeleting={isDeleting}
|
||||||
|
sidebar={mode === "create" ? sidebar : undefined}
|
||||||
// sidebar={sidebar} // Removed for cleaner UI per user request
|
|
||||||
submitText={mode === "create" ? "Register Participant" : "Save Changes"}
|
submitText={mode === "create" ? "Register Participant" : "Save Changes"}
|
||||||
>
|
>
|
||||||
{formFields}
|
{formFields}
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ import {
|
|||||||
} from "~/components/ui/dropdown-menu";
|
} from "~/components/ui/dropdown-menu";
|
||||||
import { useStudyContext } from "~/lib/study-context";
|
import { useStudyContext } from "~/lib/study-context";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "~/components/ui/tooltip";
|
||||||
|
|
||||||
export type Participant = {
|
export type Participant = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -101,16 +107,32 @@ export const columns: ColumnDef<Participant>[] = [
|
|||||||
const name = row.getValue("name");
|
const name = row.getValue("name");
|
||||||
const email = row.original.email;
|
const email = row.original.email;
|
||||||
return (
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
<div>
|
<div>
|
||||||
<div className="truncate font-medium">
|
<div className="truncate font-medium max-w-[200px]">
|
||||||
{String(name) || "No name provided"}
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span>{String(name) || "No name provided"}</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{String(name) || "No name provided"}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{email && (
|
{email && (
|
||||||
<div className="text-muted-foreground truncate text-sm">
|
<div className="text-muted-foreground truncate text-sm max-w-[200px]">
|
||||||
{email}
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span>{email}</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{email}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -120,11 +142,30 @@ export const columns: ColumnDef<Participant>[] = [
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const consentGiven = row.getValue("consentGiven");
|
const consentGiven = row.getValue("consentGiven");
|
||||||
|
|
||||||
if (consentGiven) {
|
return (
|
||||||
return <Badge className="bg-green-100 text-green-800">Consented</Badge>;
|
<TooltipProvider>
|
||||||
}
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
return <Badge className="bg-red-100 text-red-800">Pending</Badge>;
|
{consentGiven ? (
|
||||||
|
<Badge className="bg-green-100 text-green-800 hover:bg-green-200">
|
||||||
|
Consented
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge className="bg-red-100 text-red-800 hover:bg-red-200">
|
||||||
|
Pending
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
{consentGiven
|
||||||
|
? "Participant has signed the consent form."
|
||||||
|
: "Consent form has not been recorded."}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "~/components/ui/select";
|
} from "~/components/ui/select";
|
||||||
|
import { Separator } from "~/components/ui/separator";
|
||||||
import { Textarea } from "~/components/ui/textarea";
|
import { Textarea } from "~/components/ui/textarea";
|
||||||
import {
|
import {
|
||||||
EntityForm,
|
EntityForm,
|
||||||
@@ -165,10 +166,12 @@ export function StudyForm({ mode, studyId }: StudyFormProps) {
|
|||||||
|
|
||||||
// Form fields
|
// Form fields
|
||||||
const formFields = (
|
const formFields = (
|
||||||
|
<div className="space-y-6">
|
||||||
<FormSection
|
<FormSection
|
||||||
title="Study Details"
|
title="Study Details"
|
||||||
description="Basic information about your research study."
|
description="Basic information and status of your research study."
|
||||||
>
|
>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<FormField>
|
<FormField>
|
||||||
<Label htmlFor="tour-study-name">Study Name *</Label>
|
<Label htmlFor="tour-study-name">Study Name *</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -184,6 +187,36 @@ export function StudyForm({ mode, studyId }: StudyFormProps) {
|
|||||||
)}
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField>
|
||||||
|
<Label htmlFor="status">Status</Label>
|
||||||
|
<Select
|
||||||
|
value={form.watch("status")}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
form.setValue(
|
||||||
|
"status",
|
||||||
|
value as "draft" | "active" | "completed" | "archived",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="draft">Draft - Study in preparation</SelectItem>
|
||||||
|
<SelectItem value="active">
|
||||||
|
Active - Currently recruiting/running
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="completed">
|
||||||
|
Completed - Data collection finished
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="archived">
|
||||||
|
Archived - Study concluded
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
<FormField>
|
<FormField>
|
||||||
<Label htmlFor="tour-study-description">Description *</Label>
|
<Label htmlFor="tour-study-description">Description *</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -191,7 +224,9 @@ export function StudyForm({ mode, studyId }: StudyFormProps) {
|
|||||||
{...form.register("description")}
|
{...form.register("description")}
|
||||||
placeholder="Describe the research objectives, methodology, and expected outcomes..."
|
placeholder="Describe the research objectives, methodology, and expected outcomes..."
|
||||||
rows={4}
|
rows={4}
|
||||||
className={form.formState.errors.description ? "border-red-500" : ""}
|
className={
|
||||||
|
form.formState.errors.description ? "border-red-500" : ""
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{form.formState.errors.description && (
|
{form.formState.errors.description && (
|
||||||
<p className="text-sm text-red-600">
|
<p className="text-sm text-red-600">
|
||||||
@@ -199,14 +234,26 @@ export function StudyForm({ mode, studyId }: StudyFormProps) {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<FormSection
|
||||||
|
title="Configuration"
|
||||||
|
description="Institutional details and ethics approval."
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<FormField>
|
<FormField>
|
||||||
<Label htmlFor="institution">Institution *</Label>
|
<Label htmlFor="institution">Institution *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="institution"
|
id="institution"
|
||||||
{...form.register("institution")}
|
{...form.register("institution")}
|
||||||
placeholder="e.g., University of Technology"
|
placeholder="e.g., University of Technology"
|
||||||
className={form.formState.errors.institution ? "border-red-500" : ""}
|
className={
|
||||||
|
form.formState.errors.institution ? "border-red-500" : ""
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{form.formState.errors.institution && (
|
{form.formState.errors.institution && (
|
||||||
<p className="text-sm text-red-600">
|
<p className="text-sm text-red-600">
|
||||||
@@ -234,34 +281,9 @@ export function StudyForm({ mode, studyId }: StudyFormProps) {
|
|||||||
Optional: Institutional Review Board protocol number if applicable
|
Optional: Institutional Review Board protocol number if applicable
|
||||||
</p>
|
</p>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
</div>
|
||||||
<FormField>
|
|
||||||
<Label htmlFor="status">Status</Label>
|
|
||||||
<Select
|
|
||||||
value={form.watch("status")}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
form.setValue(
|
|
||||||
"status",
|
|
||||||
value as "draft" | "active" | "completed" | "archived",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select status" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="draft">Draft - Study in preparation</SelectItem>
|
|
||||||
<SelectItem value="active">
|
|
||||||
Active - Currently recruiting/running
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="completed">
|
|
||||||
Completed - Data collection finished
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="archived">Archived - Study concluded</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormField>
|
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sidebar content
|
// Sidebar content
|
||||||
@@ -324,7 +346,7 @@ export function StudyForm({ mode, studyId }: StudyFormProps) {
|
|||||||
error={error}
|
error={error}
|
||||||
onDelete={mode === "edit" ? onDelete : undefined}
|
onDelete={mode === "edit" ? onDelete : undefined}
|
||||||
isDeleting={isDeleting}
|
isDeleting={isDeleting}
|
||||||
sidebar={sidebar}
|
sidebar={mode === "create" ? sidebar : undefined}
|
||||||
submitButtonId="tour-study-submit"
|
submitButtonId="tour-study-submit"
|
||||||
extraActions={
|
extraActions={
|
||||||
<Button variant="ghost" size="sm" onClick={() => startTour("study_creation")}>
|
<Button variant="ghost" size="sm" onClick={() => startTour("study_creation")}>
|
||||||
|
|||||||
@@ -127,12 +127,14 @@ export function EntityForm<T extends FieldValues = FieldValues>({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid gap-8 w-full",
|
"grid gap-8 w-full",
|
||||||
layout === "default" && "grid-cols-1 lg:grid-cols-3", // Keep the column split but remove max-width
|
// If sidebar exists, use 2-column layout. If not, use full width (max-w-7xl centered).
|
||||||
layout === "full-width" && "grid-cols-1",
|
sidebar && layout === "default"
|
||||||
|
? "grid-cols-1 lg:grid-cols-3"
|
||||||
|
: "grid-cols-1 max-w-7xl mx-auto",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Main Form */}
|
{/* Main Form */}
|
||||||
<div className={layout === "default" ? "lg:col-span-2" : "col-span-1"}>
|
<div className={sidebar && layout === "default" ? "lg:col-span-2" : "col-span-1"}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
|
|||||||
Reference in New Issue
Block a user