Begin plugins system

This commit is contained in:
2025-08-07 01:12:58 -04:00
parent 544207e9a2
commit 3a443d1727
53 changed files with 5873 additions and 2547 deletions

View File

@@ -27,6 +27,20 @@ import {
import { useStudyContext } from "~/lib/study-context";
import { api } from "~/trpc/react";
type DemographicsData = {
age?: number;
gender?: string;
occupation?: string;
education?: string;
primaryLanguage?: string;
language?: string;
location?: string;
city?: string;
robotExperience?: string;
experience?: string;
grade?: number;
};
const participantSchema = z.object({
participantCode: z
.string()
@@ -67,7 +81,7 @@ export function ParticipantForm({
}: ParticipantFormProps) {
const router = useRouter();
const { selectedStudyId } = useStudyContext();
const contextStudyId = studyId || selectedStudyId;
const contextStudyId = studyId ?? selectedStudyId;
const [isSubmitting, setIsSubmitting] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -76,7 +90,7 @@ export function ParticipantForm({
resolver: zodResolver(participantSchema),
defaultValues: {
consentGiven: false,
studyId: contextStudyId || "",
studyId: contextStudyId ?? "",
},
});
@@ -97,16 +111,39 @@ export function ParticipantForm({
// Set breadcrumbs
const breadcrumbs = [
{ label: "Dashboard", href: "/dashboard" },
{ label: "Participants", href: "/participants" },
...(mode === "edit" && participant
{ label: "Studies", href: "/studies" },
...(contextStudyId
? [
{
label: participant.name || participant.participantCode,
href: `/participants/${participant.id}`,
label: participant?.study?.name ?? "Study",
href: `/studies/${contextStudyId}`,
},
{ label: "Edit" },
{
label: "Participants",
href: `/studies/${contextStudyId}/participants`,
},
...(mode === "edit" && participant
? [
{
label: participant.name ?? participant.participantCode,
href: `/participants/${participant.id}`,
},
{ label: "Edit" },
]
: [{ label: "New Participant" }]),
]
: [{ label: "New Participant" }]),
: [
{ label: "Participants", href: "/participants" },
...(mode === "edit" && participant
? [
{
label: participant.name ?? participant.participantCode,
href: `/participants/${participant.id}`,
},
{ label: "Edit" },
]
: [{ label: "New Participant" }]),
]),
];
useBreadcrumbsEffect(breadcrumbs);
@@ -116,11 +153,18 @@ export function ParticipantForm({
if (mode === "edit" && participant) {
form.reset({
participantCode: participant.participantCode,
name: participant.name || "",
email: participant.email || "",
name: participant.name ?? "",
email: participant.email ?? "",
studyId: participant.studyId,
age: (participant.demographics as any)?.age || undefined,
gender: (participant.demographics as any)?.gender || undefined,
age: (participant.demographics as DemographicsData)?.age ?? undefined,
gender:
((participant.demographics as DemographicsData)?.gender as
| "male"
| "female"
| "non_binary"
| "prefer_not_to_say"
| "other"
| undefined) ?? undefined,
consentGiven: true, // Assume consent was given if participant exists
});
}
@@ -144,16 +188,16 @@ export function ParticipantForm({
try {
const demographics = {
age: data.age || null,
gender: data.gender || null,
age: data.age ?? null,
gender: data.gender ?? null,
};
if (mode === "create") {
const newParticipant = await createParticipantMutation.mutateAsync({
studyId: data.studyId,
participantCode: data.participantCode,
name: data.name || undefined,
email: data.email || undefined,
name: data.name ?? undefined,
email: data.email ?? undefined,
demographics,
});
router.push(`/participants/${newParticipant.id}`);
@@ -161,8 +205,8 @@ export function ParticipantForm({
const updatedParticipant = await updateParticipantMutation.mutateAsync({
id: participantId!,
participantCode: data.participantCode,
name: data.name || undefined,
email: data.email || undefined,
name: data.name ?? undefined,
email: data.email ?? undefined,
demographics,
});
router.push(`/participants/${updatedParticipant.id}`);
@@ -333,7 +377,7 @@ export function ParticipantForm({
<FormField>
<Label htmlFor="gender">Gender</Label>
<Select
value={form.watch("gender") || ""}
value={form.watch("gender") ?? ""}
onValueChange={(value) =>
form.setValue(
"gender",
@@ -444,7 +488,7 @@ export function ParticipantForm({
title={
mode === "create"
? "Register New Participant"
: `Edit ${participant?.name || participant?.participantCode || "Participant"}`
: `Edit ${participant?.name ?? participant?.participantCode ?? "Participant"}`
}
description={
mode === "create"

View File

@@ -177,7 +177,7 @@ export const participantsColumns: ColumnDef<Participant>[] = [
<DataTableColumnHeader column={column} title="Name" />
),
cell: ({ row }) => {
const name = row.getValue("name") as string | null;
const name = row.original.name;
const email = row.original.email;
return (
<div className="max-w-[160px] space-y-1">
@@ -193,8 +193,8 @@ export const participantsColumns: ColumnDef<Participant>[] = [
{email && (
<div className="text-muted-foreground flex items-center space-x-1 text-xs">
<Mail className="h-3 w-3 flex-shrink-0" />
<span className="truncate" title={email}>
{email}
<span className="truncate" title={email ?? ""}>
{email ?? ""}
</span>
</div>
)}
@@ -237,7 +237,7 @@ export const participantsColumns: ColumnDef<Participant>[] = [
);
},
filterFn: (row, id, value) => {
const consentGiven = row.getValue(id) as boolean;
const consentGiven = row.getValue(id);
if (value === "consented") return !!consentGiven;
if (value === "pending") return !consentGiven;
return true;
@@ -249,12 +249,12 @@ export const participantsColumns: ColumnDef<Participant>[] = [
<DataTableColumnHeader column={column} title="Trials" />
),
cell: ({ row }) => {
const trialCount = row.getValue("trialCount") as number;
const trialCount = row.original.trialCount;
return (
<div className="flex items-center space-x-1 text-sm whitespace-nowrap">
<TestTube className="text-muted-foreground h-3 w-3" />
<span>{trialCount as number}</span>
<span>{trialCount ?? 0}</span>
</div>
);
},
@@ -265,10 +265,10 @@ export const participantsColumns: ColumnDef<Participant>[] = [
<DataTableColumnHeader column={column} title="Created" />
),
cell: ({ row }) => {
const date = row.getValue("createdAt") as Date;
const date = row.original.createdAt;
return (
<div className="text-sm whitespace-nowrap">
{formatDistanceToNow(date, { addSuffix: true })}
{formatDistanceToNow(date ?? new Date(), { addSuffix: true })}
</div>
);
},

View File

@@ -15,11 +15,13 @@ import {
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { useStudyContext } from "~/lib/study-context";
import { api } from "~/trpc/react";
import { participantsColumns, type Participant } from "./participants-columns";
export function ParticipantsDataTable() {
const [consentFilter, setConsentFilter] = React.useState("all");
const { selectedStudyId } = useStudyContext();
const {
data: participantsData,
@@ -45,10 +47,22 @@ export function ParticipantsDataTable() {
return () => clearInterval(interval);
}, [refetch]);
// Get study data for breadcrumbs
const { data: studyData } = api.studies.get.useQuery(
{ id: selectedStudyId! },
{ enabled: !!selectedStudyId },
);
// Set breadcrumbs
useBreadcrumbsEffect([
{ label: "Dashboard", href: "/dashboard" },
{ label: "Participants" },
{ label: "Studies", href: "/studies" },
...(selectedStudyId && studyData
? [
{ label: studyData.name, href: `/studies/${selectedStudyId}` },
{ label: "Participants" },
]
: [{ label: "Participants" }]),
]);
// Transform participants data to match the Participant type expected by columns
@@ -60,12 +74,18 @@ export function ParticipantsDataTable() {
participantCode: p.participantCode,
email: p.email,
name: p.name,
consentGiven: (p as any).hasConsent || false,
consentDate: (p as any).latestConsent?.signedAt
? new Date((p as any).latestConsent.signedAt as unknown as string)
consentGiven:
(p as unknown as { hasConsent?: boolean }).hasConsent ?? false,
consentDate: (p as unknown as { latestConsent?: { signedAt: string } })
.latestConsent?.signedAt
? new Date(
(
p as unknown as { latestConsent: { signedAt: string } }
).latestConsent.signedAt,
)
: null,
createdAt: p.createdAt,
trialCount: (p as any).trialCount || 0,
trialCount: (p as unknown as { trialCount?: number }).trialCount ?? 0,
userRole: undefined,
canEdit: true,
canDelete: true,
@@ -92,7 +112,7 @@ export function ParticipantsDataTable() {
const filters = (
<div className="flex items-center space-x-2">
<Select value={consentFilter} onValueChange={setConsentFilter}>
<SelectTrigger className="w-[160px]">
<SelectTrigger className="h-8 w-[160px]">
<SelectValue placeholder="Consent Status" />
</SelectTrigger>
<SelectContent>