mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-24 03:37:51 -04:00
Enhance development standards and UI components
- Updated .rules to enforce stricter UI/UX standards, including exclusive use of Lucide icons and consistent patterns for entity view pages. - Added new UI components for entity views, including headers, sections, and quick actions to improve layout and reusability. - Refactored existing pages (experiments, participants, studies, trials) to utilize the new entity view components, enhancing consistency across the dashboard. - Improved accessibility and user experience by implementing loading states and error boundaries in async operations. - Updated package dependencies to ensure compatibility and performance improvements. Features: - Comprehensive guidelines for component reusability and visual consistency. - Enhanced user interface with new entity view components for better organization and navigation. Breaking Changes: None - existing functionality remains intact.
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import {
|
||||
AlertCircle,
|
||||
ArrowLeft,
|
||||
Calendar,
|
||||
CheckCircle,
|
||||
Edit,
|
||||
FileText,
|
||||
Mail,
|
||||
@@ -10,21 +13,26 @@ import {
|
||||
Shield,
|
||||
Trash2,
|
||||
Users,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { useEffect, 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,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { auth } from "~/server/auth";
|
||||
import { api } from "~/trpc/server";
|
||||
EntityView,
|
||||
EntityViewHeader,
|
||||
EntityViewSection,
|
||||
EntityViewSidebar,
|
||||
EmptyState,
|
||||
InfoGrid,
|
||||
QuickActions,
|
||||
} from "~/components/ui/entity-view";
|
||||
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
interface ParticipantDetailPageProps {
|
||||
params: Promise<{
|
||||
@@ -32,415 +40,372 @@ interface ParticipantDetailPageProps {
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function ParticipantDetailPage({
|
||||
export default function ParticipantDetailPage({
|
||||
params,
|
||||
}: ParticipantDetailPageProps) {
|
||||
const resolvedParams = await params;
|
||||
const session = await auth();
|
||||
const { data: session } = useSession();
|
||||
const [participant, setParticipant] = useState<any>(null);
|
||||
const [trials, setTrials] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [resolvedParams, setResolvedParams] = useState<{ id: string } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
async function resolveParams() {
|
||||
const resolved = await params;
|
||||
setResolvedParams(resolved);
|
||||
}
|
||||
resolveParams();
|
||||
}, [params]);
|
||||
|
||||
const { data: participantData } = api.participants.get.useQuery(
|
||||
{ id: resolvedParams?.id ?? "" },
|
||||
{ enabled: !!resolvedParams?.id },
|
||||
);
|
||||
|
||||
const { data: trialsData } = api.trials.list.useQuery(
|
||||
{ participantId: resolvedParams?.id ?? "", limit: 10 },
|
||||
{ enabled: !!resolvedParams?.id },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (participantData) {
|
||||
setParticipant(participantData);
|
||||
}
|
||||
if (trialsData) {
|
||||
setTrials(trialsData);
|
||||
}
|
||||
if (participantData !== undefined) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [participantData, trialsData]);
|
||||
|
||||
// Set breadcrumbs
|
||||
useBreadcrumbsEffect([
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
{ label: "Participants", href: "/participants" },
|
||||
{
|
||||
label: participant?.name || participant?.participantCode || "Participant",
|
||||
},
|
||||
]);
|
||||
|
||||
if (!session?.user) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
try {
|
||||
const participant = await api.participants.get({ id: resolvedParams.id });
|
||||
if (loading || !participant) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!participant) {
|
||||
return notFound();
|
||||
}
|
||||
const userRole = session.user.roles?.[0]?.role ?? "observer";
|
||||
const canEdit = ["administrator", "researcher"].includes(userRole);
|
||||
|
||||
const userRole = session.user.roles?.[0]?.role ?? "observer";
|
||||
const canEdit = ["administrator", "researcher"].includes(userRole);
|
||||
// canDelete removed - not used in component
|
||||
return (
|
||||
<EntityView>
|
||||
{/* Header */}
|
||||
<EntityViewHeader
|
||||
title={participant.name ?? participant.participantCode}
|
||||
subtitle={
|
||||
participant.name
|
||||
? `Code: ${participant.participantCode}`
|
||||
: "Participant"
|
||||
}
|
||||
icon="Users"
|
||||
actions={
|
||||
canEdit && (
|
||||
<>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href={`/participants/${resolvedParams.id}/edit`}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
// Get participant's trials
|
||||
const trials = await api.trials.list({
|
||||
participantId: resolvedParams.id,
|
||||
limit: 10,
|
||||
});
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{/* Main Content */}
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{/* Participant Information */}
|
||||
<EntityViewSection title="Participant Information" icon="FileText">
|
||||
<InfoGrid
|
||||
items={[
|
||||
{
|
||||
label: "Participant Code",
|
||||
value: (
|
||||
<code className="bg-muted rounded px-2 py-1 font-mono text-sm">
|
||||
{participant.participantCode}
|
||||
</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Name",
|
||||
value: participant.name || "Not provided",
|
||||
},
|
||||
{
|
||||
label: "Email",
|
||||
value: participant.email ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="h-4 w-4" />
|
||||
<a
|
||||
href={`mailto:${participant.email}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{participant.email}
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
"Not provided"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Study",
|
||||
value: participant.study ? (
|
||||
<Link
|
||||
href={`/studies/${participant.study.id}`}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{participant.study.name}
|
||||
</Link>
|
||||
) : (
|
||||
"No study assigned"
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-6xl px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="mb-4 flex items-center gap-4">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/participants">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Participants
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
{/* Demographics */}
|
||||
{participant.demographics &&
|
||||
typeof participant.demographics === "object" &&
|
||||
participant.demographics !== null &&
|
||||
Object.keys(participant.demographics).length > 0 && (
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="text-muted-foreground mb-3 text-sm font-medium">
|
||||
Demographics
|
||||
</h4>
|
||||
<InfoGrid
|
||||
items={(() => {
|
||||
const demo = participant.demographics as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
return [
|
||||
demo.age && {
|
||||
label: "Age",
|
||||
value:
|
||||
typeof demo.age === "number"
|
||||
? demo.age.toString()
|
||||
: String(demo.age),
|
||||
},
|
||||
demo.gender && {
|
||||
label: "Gender",
|
||||
value: String(demo.gender),
|
||||
},
|
||||
].filter(Boolean) as Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>;
|
||||
})()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="bg-primary text-primary-foreground flex h-16 w-16 items-center justify-center rounded-lg">
|
||||
<Users className="h-8 w-8" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-foreground text-3xl font-bold">
|
||||
{participant.name ?? participant.participantCode}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
{participant.name
|
||||
? `Code: ${participant.participantCode}`
|
||||
: "Participant"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canEdit && (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" asChild>
|
||||
<Link href={`/participants/${resolvedParams.id}/edit`}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
{/* Notes */}
|
||||
{participant.notes && (
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="text-muted-foreground mb-2 text-sm font-medium">
|
||||
Notes
|
||||
</h4>
|
||||
<div className="bg-muted rounded p-3 text-sm whitespace-pre-wrap">
|
||||
{participant.notes}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</EntityViewSection>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{/* Main Content */}
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{/* Participant Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
Participant Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<h4 className="text-muted-foreground text-sm font-medium">
|
||||
Participant Code
|
||||
</h4>
|
||||
<p className="bg-muted rounded px-2 py-1 font-mono text-sm">
|
||||
{participant.participantCode}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{participant.name && (
|
||||
<div>
|
||||
<h4 className="text-muted-foreground text-sm font-medium">
|
||||
Name
|
||||
</h4>
|
||||
<p className="text-sm">{participant.name}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{participant.email && (
|
||||
<div>
|
||||
<h4 className="text-muted-foreground text-sm font-medium">
|
||||
Email
|
||||
</h4>
|
||||
<p className="flex items-center gap-2 text-sm">
|
||||
<Mail className="h-4 w-4" />
|
||||
<a
|
||||
href={`mailto:${participant.email}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{participant.email}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h4 className="text-muted-foreground text-sm font-medium">
|
||||
Study
|
||||
</h4>
|
||||
<p className="text-sm">
|
||||
{/* Trial History */}
|
||||
<EntityViewSection
|
||||
title="Trial History"
|
||||
icon="Play"
|
||||
description="Experimental sessions for this participant"
|
||||
actions={
|
||||
canEdit && (
|
||||
<Button size="sm" asChild>
|
||||
<Link href={`/trials/new?participantId=${resolvedParams.id}`}>
|
||||
Schedule Trial
|
||||
</Link>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
{trials.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{trials.map((trial) => (
|
||||
<div
|
||||
key={trial.id}
|
||||
className="hover:bg-muted/50 rounded-lg border p-4 transition-colors"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Link
|
||||
href={`/studies/${participant.study?.id}`}
|
||||
className="text-primary hover:underline"
|
||||
href={`/trials/${trial.id}`}
|
||||
className="font-medium hover:underline"
|
||||
>
|
||||
{participant.study?.name}
|
||||
{trial.experiment?.name || "Trial"}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{participant.demographics &&
|
||||
typeof participant.demographics === "object" &&
|
||||
participant.demographics !== null &&
|
||||
Object.keys(participant.demographics).length > 0 ? (
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="text-muted-foreground mb-2 text-sm font-medium">
|
||||
Demographics
|
||||
</h4>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{(() => {
|
||||
const demo = participant.demographics as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
return (
|
||||
<>
|
||||
{demo.age && (
|
||||
<div>
|
||||
<span className="text-sm font-medium">
|
||||
Age:
|
||||
</span>{" "}
|
||||
<span className="text-sm">
|
||||
{typeof demo.age === "number"
|
||||
? demo.age.toString()
|
||||
: String(demo.age)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{demo.gender && (
|
||||
<div>
|
||||
<span className="text-sm font-medium">
|
||||
Gender:
|
||||
</span>{" "}
|
||||
<span className="text-sm">
|
||||
{String(demo.gender)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
<Badge
|
||||
variant={
|
||||
trial.status === "completed"
|
||||
? "default"
|
||||
: trial.status === "in_progress"
|
||||
? "secondary"
|
||||
: trial.status === "failed"
|
||||
? "destructive"
|
||||
: "outline"
|
||||
}
|
||||
>
|
||||
{trial.status.replace("_", " ")}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-muted-foreground flex items-center gap-4 text-sm">
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{trial.createdAt
|
||||
? formatDistanceToNow(new Date(trial.createdAt), {
|
||||
addSuffix: true,
|
||||
})
|
||||
: "Not scheduled"}
|
||||
</span>
|
||||
{trial.duration && (
|
||||
<span>{Math.round(trial.duration / 60)} minutes</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Notes */}
|
||||
{participant.notes && (
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="text-muted-foreground mb-2 text-sm font-medium">
|
||||
Notes
|
||||
</h4>
|
||||
<p className="bg-muted rounded p-3 text-sm whitespace-pre-wrap">
|
||||
{participant.notes}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Trial History */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Play className="h-5 w-5" />
|
||||
Trial History
|
||||
</CardTitle>
|
||||
{canEdit && (
|
||||
<Button size="sm" asChild>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
icon="Play"
|
||||
title="No Trials Yet"
|
||||
description="This participant hasn't been assigned to any trials."
|
||||
action={
|
||||
canEdit && (
|
||||
<Button asChild>
|
||||
<Link
|
||||
href={`/trials/new?participantId=${resolvedParams.id}`}
|
||||
>
|
||||
Schedule Trial
|
||||
Schedule First Trial
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<CardDescription>
|
||||
Experimental sessions for this participant
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{trials.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{trials.map((trial) => (
|
||||
<div
|
||||
key={trial.id}
|
||||
className="hover:bg-muted/50 rounded-lg border p-4 transition-colors"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Link
|
||||
href={`/trials/${trial.id}`}
|
||||
className="font-medium hover:underline"
|
||||
>
|
||||
{trial.experiment?.name || "Trial"}
|
||||
</Link>
|
||||
<Badge
|
||||
variant={
|
||||
trial.status === "completed"
|
||||
? "default"
|
||||
: trial.status === "in_progress"
|
||||
? "secondary"
|
||||
: trial.status === "failed"
|
||||
? "destructive"
|
||||
: "outline"
|
||||
}
|
||||
>
|
||||
{trial.status.replace("_", " ")}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-muted-foreground flex items-center gap-4 text-sm">
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{trial.createdAt
|
||||
? formatDistanceToNow(new Date(trial.createdAt), {
|
||||
addSuffix: true,
|
||||
})
|
||||
: "Not scheduled"}
|
||||
</span>
|
||||
{trial.duration && (
|
||||
<span>
|
||||
{Math.round(trial.duration / 60)} minutes
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-8 text-center">
|
||||
<Play className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
|
||||
<h3 className="mb-2 font-medium">No Trials Yet</h3>
|
||||
<p className="text-muted-foreground mb-4 text-sm">
|
||||
This participant hasn't been assigned to any trials.
|
||||
</p>
|
||||
{canEdit && (
|
||||
<Button asChild>
|
||||
<Link
|
||||
href={`/trials/new?participantId=${resolvedParams.id}`}
|
||||
>
|
||||
Schedule First Trial
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Consent Status */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Shield className="h-4 w-4" />
|
||||
Consent Status
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Informed Consent</span>
|
||||
<Badge
|
||||
variant={
|
||||
participant.consentGiven ? "default" : "destructive"
|
||||
}
|
||||
>
|
||||
{participant.consentGiven ? "Given" : "Not Given"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{participant.consentDate && (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Consented:{" "}
|
||||
{formatDistanceToNow(participant.consentDate, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!participant.consentGiven && (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription className="text-sm">
|
||||
Consent required before trials can be conducted.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Registration Details */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
Registration Details
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div>
|
||||
<h4 className="text-muted-foreground text-sm font-medium">
|
||||
Registered
|
||||
</h4>
|
||||
<p className="text-sm">
|
||||
{formatDistanceToNow(participant.createdAt, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{participant.updatedAt &&
|
||||
participant.updatedAt !== participant.createdAt && (
|
||||
<div>
|
||||
<h4 className="text-muted-foreground text-sm font-medium">
|
||||
Last Updated
|
||||
</h4>
|
||||
<p className="text-sm">
|
||||
{formatDistanceToNow(participant.updatedAt, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions */}
|
||||
{canEdit && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Quick Actions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href={`/trials/new?participantId=${resolvedParams.id}`}
|
||||
>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Schedule Trial
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
asChild
|
||||
>
|
||||
<Link href={`/participants/${resolvedParams.id}/edit`}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit Information
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" className="w-full justify-start">
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
Export Data
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</EntityViewSection>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<EntityViewSidebar>
|
||||
{/* Consent Status */}
|
||||
<EntityViewSection title="Consent Status" icon="Shield">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Informed Consent</span>
|
||||
<Badge
|
||||
variant={participant.consentGiven ? "default" : "destructive"}
|
||||
>
|
||||
{participant.consentGiven ? (
|
||||
<>
|
||||
<CheckCircle className="mr-1 h-3 w-3" />
|
||||
Given
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XCircle className="mr-1 h-3 w-3" />
|
||||
Not Given
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{participant.consentDate && (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Consented:{" "}
|
||||
{formatDistanceToNow(participant.consentDate, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!participant.consentGiven && (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription className="text-sm">
|
||||
Consent required before trials can be conducted.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</EntityViewSection>
|
||||
|
||||
{/* Registration Details */}
|
||||
<EntityViewSection title="Registration Details" icon="Calendar">
|
||||
<InfoGrid
|
||||
columns={1}
|
||||
items={[
|
||||
{
|
||||
label: "Registered",
|
||||
value: formatDistanceToNow(participant.createdAt, {
|
||||
addSuffix: true,
|
||||
}),
|
||||
},
|
||||
...(participant.updatedAt &&
|
||||
participant.updatedAt !== participant.createdAt
|
||||
? [
|
||||
{
|
||||
label: "Last Updated",
|
||||
value: formatDistanceToNow(participant.updatedAt, {
|
||||
addSuffix: true,
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</EntityViewSection>
|
||||
|
||||
{/* Quick Actions */}
|
||||
{canEdit && (
|
||||
<EntityViewSection title="Quick Actions" icon="Edit">
|
||||
<QuickActions
|
||||
actions={[
|
||||
{
|
||||
label: "Schedule Trial",
|
||||
icon: "Play",
|
||||
href: `/trials/new?participantId=${resolvedParams.id}`,
|
||||
},
|
||||
{
|
||||
label: "Edit Information",
|
||||
icon: "Edit",
|
||||
href: `/participants/${resolvedParams.id}/edit`,
|
||||
},
|
||||
{
|
||||
label: "Export Data",
|
||||
icon: "FileText",
|
||||
href: `/participants/${resolvedParams.id}/export`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EntityViewSection>
|
||||
)}
|
||||
</EntityViewSidebar>
|
||||
</div>
|
||||
);
|
||||
} catch {
|
||||
return notFound();
|
||||
}
|
||||
</EntityView>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user