diff --git a/src/app/(dashboard)/studies/[id]/page.tsx b/src/app/(dashboard)/studies/[id]/page.tsx index 0e0278a..452a4f7 100755 --- a/src/app/(dashboard)/studies/[id]/page.tsx +++ b/src/app/(dashboard)/studies/[id]/page.tsx @@ -20,6 +20,7 @@ import { PageHeader } from "~/components/ui/page-header"; import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider"; import { useSession } from "~/lib/auth-client"; import { api } from "~/trpc/react"; +import { AddMemberDialog } from "~/components/studies/add-member-dialog"; interface StudyDetailPageProps { params: Promise<{ @@ -391,10 +392,12 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) { icon="Users" description={`${members.length} team member${members.length !== 1 ? "s" : ""}`} actions={ - + + + } >
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index b8735d8..65522eb 100755 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -15,20 +15,13 @@ import { ChevronRight, Bot, Radio, - BarChart3, - Settings, + Building, } from "lucide-react"; import { Button } from "~/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "~/components/ui/card"; import { Badge } from "~/components/ui/badge"; import { ScrollArea } from "~/components/ui/scroll-area"; +import { PageHeader } from "~/components/ui/page-layout"; import { api } from "~/trpc/react"; export default function DashboardPage() { @@ -42,7 +35,6 @@ export default function DashboardPage() { const { data: recentTrials } = api.trials.list.useQuery({ limit: 5, - status: undefined, }); const { data: liveTrials } = api.dashboard.getLiveTrials.useQuery( @@ -59,62 +51,57 @@ export default function DashboardPage() { return "Good evening"; })(); - return ( -
- {/* Header */} -
-
-

- {greeting}, {userName.split(" ")[0]} -

-

- Ready to run your next session? -

-
+ const firstStudy = userStudies?.studies?.[0]; -
- - -
-
+ return ( +
+ {/* Header */} + + + +
+ } + /> {/* Live Trials Banner */} {liveTrials && liveTrials.length > 0 && ( - - -
- - -
-
-

- {liveTrials.length} Active Session{liveTrials.length > 1 ? "s" : ""} -

-

- {liveTrials.map((t) => t.participantCode).join(", ")} -

-
- -
-
+
+
+ +
+
+

+ {liveTrials.length} Active Session{liveTrials.length > 1 ? "s" : ""} +

+

+ {liveTrials.map((t) => t.participantCode).join(", ")} +

+
+ +
)} {/* Stats Row */} -
+
@@ -144,22 +131,19 @@ export default function DashboardPage() { {/* Main Grid */}
{/* Studies List */} - - -
- - - Your Studies - - Recent studies and quick access +
+
+
+ +

Your Studies

- - +
+
{userStudies?.studies?.slice(0, 5).map((study) => ( )}
- - +
+
{/* Quick Links & Recent */}
{/* Quick Actions */} - - - Quick Actions - - +
+
+

Quick Actions

+
+
- - +
+
{/* Recent Trials */} - - - - - Recent Trials - - - +
+
+
+ +

Recent Trials

+
+
+
{recentTrials?.slice(0, 5).map((trial) => ( @@ -292,8 +276,8 @@ export default function DashboardPage() { )}
- - +
+
@@ -312,15 +296,15 @@ function StatCard({ color: "teal" | "emerald" | "blue" | "violet"; }) { const colorClasses = { - teal: "bg-teal-500/10 text-teal-600 dark:text-teal-400", + teal: "bg-primary/10 text-primary", emerald: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400", blue: "bg-blue-500/10 text-blue-600 dark:text-blue-400", violet: "bg-violet-500/10 text-violet-600 dark:text-violet-400", }; return ( - - +
+
@@ -328,7 +312,7 @@ function StatCard({

{value}

{label}

- - +
+
); } diff --git a/src/components/studies/add-member-dialog.tsx b/src/components/studies/add-member-dialog.tsx new file mode 100644 index 0000000..4fc17d5 --- /dev/null +++ b/src/components/studies/add-member-dialog.tsx @@ -0,0 +1,195 @@ +"use client"; + +import * as React from "react"; +import { useSession } from "~/lib/auth-client"; +import { api } from "~/trpc/react"; +import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "~/components/ui/dialog"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; +import { Plus, Loader2, Trash2, Shield, UserMinus } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu"; + +interface Member { + id: string; + userId: string; + role: string; + user: { + name: string | null; + email: string; + }; +} + +interface AddMemberDialogProps { + studyId: string; + children?: React.ReactNode; +} + +export function AddMemberDialog({ studyId, children }: AddMemberDialogProps) { + const utils = api.useUtils(); + const [open, setOpen] = React.useState(false); + const [email, setEmail] = React.useState(""); + const [role, setRole] = React.useState("researcher"); + + const { data: membersData } = api.studies.getMembers.useQuery({ studyId }); + + const addMember = api.studies.addMember.useMutation({ + onSuccess: () => { + toast.success("Member added successfully"); + void utils.studies.getMembers.invalidate(); + setEmail(""); + setRole("researcher"); + setOpen(false); + }, + onError: (error) => { + toast.error(error.message || "Failed to add member"); + }, + }); + + const removeMember = api.studies.removeMember.useMutation({ + onSuccess: () => { + toast.success("Member removed"); + void utils.studies.getMembers.invalidate(); + }, + onError: (error) => { + toast.error(error.message || "Failed to remove member"); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!email || !role) return; + addMember.mutate({ studyId, email, role: role as "researcher" | "wizard" | "observer" }); + }; + + const handleRemove = (memberId: string, memberName: string | null) => { + if (confirm(`Remove ${memberName ?? memberId} from this study?`)) { + removeMember.mutate({ studyId, memberId }); + } + }; + + const members = membersData ?? []; + const currentUser = members.find((m) => m.userId); + const isOwner = currentUser?.role === "owner"; + + return ( + + + {children ?? ( + + )} + + + + Manage Team Members + + Add researchers, wizards, or observers to collaborate on this study. + + + + {/* Current Members */} +
+ {members.map((member) => ( +
+
+
+ + {(member.user.name ?? member.user.email).charAt(0).toUpperCase()} + +
+
+

+ {member.user.name ?? member.user.email} + {member.role === "owner" && ( + + )} +

+

{member.role}

+
+
+ {isOwner && member.role !== "owner" && ( + + )} +
+ ))} +
+ +
+
+ + setEmail(e.target.value)} + required + /> +
+ +
+ + +

+ Researchers can design experiments, Wizards execute trials, Observers have read-only access. +

+
+ + + + + +
+
+
+ ); +}