mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-24 03:37:51 -04:00
Consolidate all study-dependent routes and UI
- Remove global experiments and plugins routes; redirect to study-scoped pages - Update sidebar navigation to separate platform-level and study-level items - Add study filter to dashboard and stats queries - Refactor participants, trials, analytics pages to use new header and breadcrumbs - Update documentation for new route architecture and migration guide - Remove duplicate experiment creation route - Upgrade Next.js to 15.5.4 in package.json and bun.lock
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
import { ExperimentForm } from "~/components/experiments/ExperimentForm";
|
||||
|
||||
export default function NewExperimentPage() {
|
||||
return <ExperimentForm mode="create" />;
|
||||
}
|
||||
@@ -1,10 +1,65 @@
|
||||
import { ExperimentsDataTable } from "~/components/experiments/experiments-data-table";
|
||||
import { StudyGuard } from "~/components/dashboard/study-guard";
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { FlaskConical, ArrowRight } from "lucide-react";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { useStudyContext } from "~/lib/study-context";
|
||||
|
||||
export default function ExperimentsRedirect() {
|
||||
const router = useRouter();
|
||||
const { selectedStudyId } = useStudyContext();
|
||||
|
||||
useEffect(() => {
|
||||
// If user has a selected study, redirect to study experiments
|
||||
if (selectedStudyId) {
|
||||
router.replace(`/studies/${selectedStudyId}/experiments`);
|
||||
}
|
||||
}, [selectedStudyId, router]);
|
||||
|
||||
export default function ExperimentsPage() {
|
||||
return (
|
||||
<StudyGuard>
|
||||
<ExperimentsDataTable />
|
||||
</StudyGuard>
|
||||
<div className="flex min-h-[60vh] items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-blue-50">
|
||||
<FlaskConical className="h-8 w-8 text-blue-500" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">Experiments Moved</CardTitle>
|
||||
<CardDescription>
|
||||
Experiment management is now organized by study for better
|
||||
workflow organization.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="text-muted-foreground space-y-2 text-center text-sm">
|
||||
<p>To manage experiments:</p>
|
||||
<ul className="space-y-1 text-left">
|
||||
<li>• Select a study from your studies list</li>
|
||||
<li>• Navigate to that study's experiments page</li>
|
||||
<li>• Create and manage experiment protocols for that specific study</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 pt-4">
|
||||
<Button asChild className="w-full">
|
||||
<Link href="/studies">
|
||||
<ArrowRight className="mr-2 h-4 w-4" />
|
||||
Browse Studies
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="w-full">
|
||||
<Link href="/dashboard">Go to Dashboard</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,67 @@
|
||||
import { PluginStoreBrowse } from "~/components/plugins/plugin-store-browse";
|
||||
"use client";
|
||||
|
||||
export default function PluginStoreBrowsePage() {
|
||||
return <PluginStoreBrowse />;
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, Store } from "lucide-react";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { useStudyContext } from "~/lib/study-context";
|
||||
|
||||
export default function PluginBrowseRedirect() {
|
||||
const router = useRouter();
|
||||
const { selectedStudyId } = useStudyContext();
|
||||
|
||||
useEffect(() => {
|
||||
// If user has a selected study, redirect to study plugin browse
|
||||
if (selectedStudyId) {
|
||||
router.replace(`/studies/${selectedStudyId}/plugins/browse`);
|
||||
}
|
||||
}, [selectedStudyId, router]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-purple-50">
|
||||
<Store className="h-8 w-8 text-purple-500" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">Plugin Store Moved</CardTitle>
|
||||
<CardDescription>
|
||||
Plugin browsing is now organized by study for better robot
|
||||
capability management.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="text-muted-foreground space-y-2 text-center text-sm">
|
||||
<p>To browse and install plugins:</p>
|
||||
<ul className="space-y-1 text-left">
|
||||
<li>• Select a study from your studies list</li>
|
||||
<li>• Navigate to that study's plugin store</li>
|
||||
<li>
|
||||
• Browse and install robot capabilities for that specific study
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 pt-4">
|
||||
<Button asChild className="w-full">
|
||||
<Link href="/studies">
|
||||
<ArrowRight className="mr-2 h-4 w-4" />
|
||||
Browse Studies
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="w-full">
|
||||
<Link href="/dashboard">Go to Dashboard</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,68 @@
|
||||
import { PluginsDataTable } from "~/components/plugins/plugins-data-table";
|
||||
"use client";
|
||||
|
||||
export default function PluginsPage() {
|
||||
return <PluginsDataTable />;
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Puzzle, ArrowRight } from "lucide-react";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { useStudyContext } from "~/lib/study-context";
|
||||
|
||||
export default function PluginsRedirect() {
|
||||
const router = useRouter();
|
||||
const { selectedStudyId } = useStudyContext();
|
||||
|
||||
useEffect(() => {
|
||||
// If user has a selected study, redirect to study plugins
|
||||
if (selectedStudyId) {
|
||||
router.replace(`/studies/${selectedStudyId}/plugins`);
|
||||
}
|
||||
}, [selectedStudyId, router]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-purple-50">
|
||||
<Puzzle className="h-8 w-8 text-purple-500" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">Plugins Moved</CardTitle>
|
||||
<CardDescription>
|
||||
Plugin management is now organized by study for better robot
|
||||
capability management.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="text-muted-foreground space-y-2 text-center text-sm">
|
||||
<p>To manage plugins:</p>
|
||||
<ul className="space-y-1 text-left">
|
||||
<li>• Select a study from your studies list</li>
|
||||
<li>• Navigate to that study's plugins page</li>
|
||||
<li>
|
||||
• Install and configure robot capabilities for that specific
|
||||
study
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 pt-4">
|
||||
<Button asChild className="w-full">
|
||||
<Link href="/studies">
|
||||
<ArrowRight className="mr-2 h-4 w-4" />
|
||||
Browse Studies
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="w-full">
|
||||
<Link href="/dashboard">Go to Dashboard</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,229 @@
|
||||
import Link from "next/link";
|
||||
"use client";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
import { PasswordChangeForm } from "~/components/profile/password-change-form";
|
||||
import { ProfileEditForm } from "~/components/profile/profile-edit-form";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import { PageHeader } from "~/components/ui/page-header";
|
||||
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||
import { formatRole, getRoleDescription } from "~/lib/auth-client";
|
||||
import { auth } from "~/server/auth";
|
||||
import { User, Shield, Download, Trash2, ExternalLink } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
export default async function ProfilePage() {
|
||||
const session = await auth();
|
||||
interface ProfileUser {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
image: string | null;
|
||||
roles?: Array<{
|
||||
role: "administrator" | "researcher" | "wizard" | "observer";
|
||||
grantedAt: string | Date;
|
||||
}>;
|
||||
}
|
||||
|
||||
function ProfileContent({ user }: { user: ProfileUser }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Profile"
|
||||
description="Manage your account settings and preferences"
|
||||
icon={User}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* Profile Information */}
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{/* Basic Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Basic Information</CardTitle>
|
||||
<CardDescription>
|
||||
Your personal account information
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ProfileEditForm
|
||||
user={{
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Password Change */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Password</CardTitle>
|
||||
<CardDescription>Change your account password</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PasswordChangeForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Account Actions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Actions</CardTitle>
|
||||
<CardDescription>Manage your account settings</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">Export Data</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Download all your research data and account information
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" disabled>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Export Data
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-destructive text-sm font-medium">
|
||||
Delete Account
|
||||
</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Permanently delete your account and all associated data
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="destructive" disabled>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* User Summary */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-primary/10 flex h-12 w-12 items-center justify-center rounded-full">
|
||||
<span className="text-primary text-lg font-semibold">
|
||||
{(user.name ?? user.email ?? "U").charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{user.name ?? "Unnamed User"}</p>
|
||||
<p className="text-muted-foreground text-sm">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-medium">User ID</p>
|
||||
<p className="text-muted-foreground bg-muted rounded p-2 font-mono text-xs break-all">
|
||||
{user.id}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* System Roles */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="h-4 w-4" />
|
||||
System Roles
|
||||
</CardTitle>
|
||||
<CardDescription>Your current system permissions</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{user.roles && user.roles.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{user.roles.map((roleInfo, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start justify-between"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<Badge variant="secondary">
|
||||
{formatRole(roleInfo.role)}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{getRoleDescription(roleInfo.role)}
|
||||
</p>
|
||||
<p className="text-muted-foreground/80 mt-1 text-xs">
|
||||
Granted{" "}
|
||||
{new Date(roleInfo.grantedAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Need additional permissions?{" "}
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="h-auto p-0 text-xs"
|
||||
>
|
||||
Contact an administrator
|
||||
<ExternalLink className="ml-1 h-3 w-3" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-6 text-center">
|
||||
<div className="bg-muted mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-lg">
|
||||
<Shield className="text-muted-foreground h-6 w-6" />
|
||||
</div>
|
||||
<p className="mb-1 text-sm font-medium">No Roles Assigned</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
You don't have any system roles yet. Contact an
|
||||
administrator to get access to HRIStudio features.
|
||||
</p>
|
||||
<Button size="sm" variant="outline">
|
||||
Request Access
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { data: session } = useSession();
|
||||
|
||||
useBreadcrumbsEffect([
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
{ label: "Profile" },
|
||||
]);
|
||||
|
||||
if (!session?.user) {
|
||||
redirect("/auth/signin");
|
||||
@@ -24,295 +231,5 @@ export default async function ProfilePage() {
|
||||
|
||||
const user = session.user;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-900">Profile</h1>
|
||||
<p className="text-slate-600">
|
||||
Manage your account settings and preferences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-slate-600">
|
||||
Welcome, {user.name ?? user.email}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/auth/signout">Sign Out</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/">← Back to Home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* Profile Information */}
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{/* Basic Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Basic Information</CardTitle>
|
||||
<CardDescription>
|
||||
Your personal account information
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ProfileEditForm
|
||||
user={{
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Password Change */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Password</CardTitle>
|
||||
<CardDescription>Change your account password</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PasswordChangeForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Account Actions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Actions</CardTitle>
|
||||
<CardDescription>Manage your account settings</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">Export Data</h4>
|
||||
<p className="text-sm text-slate-600">
|
||||
Download all your research data and account information
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" disabled>
|
||||
Export Data
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-red-700">
|
||||
Delete Account
|
||||
</h4>
|
||||
<p className="text-sm text-slate-600">
|
||||
Permanently delete your account and all associated data
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="destructive" disabled>
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* User Summary */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-blue-100">
|
||||
<span className="text-lg font-semibold text-blue-600">
|
||||
{(user.name ?? user.email ?? "U").charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{user.name ?? "Unnamed User"}</p>
|
||||
<p className="text-sm text-slate-600">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-medium">User ID</p>
|
||||
<p className="rounded bg-slate-100 p-2 font-mono text-xs break-all text-slate-600">
|
||||
{user.id}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* System Roles */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>System Roles</CardTitle>
|
||||
<CardDescription>
|
||||
Your current system permissions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{user.roles && user.roles.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{user.roles.map((roleInfo, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start justify-between"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<Badge variant="secondary">
|
||||
{formatRole(roleInfo.role)}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-slate-600">
|
||||
{getRoleDescription(roleInfo.role)}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
Granted {roleInfo.grantedAt.toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-slate-500">
|
||||
Need additional permissions?{" "}
|
||||
<Link
|
||||
href="/contact"
|
||||
className="text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
Contact an administrator
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-6 text-center">
|
||||
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-lg bg-yellow-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-yellow-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.728-.833-2.498 0L4.316 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="mb-1 text-sm font-medium text-slate-900">
|
||||
No Roles Assigned
|
||||
</p>
|
||||
<p className="mb-3 text-xs text-slate-600">
|
||||
You don't have any system roles yet. Contact an
|
||||
administrator to get access to HRIStudio features.
|
||||
</p>
|
||||
<Button asChild size="sm" variant="outline">
|
||||
<Link href="/contact">Request Access</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Quick Actions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
>
|
||||
<Link href="/studies">
|
||||
<svg
|
||||
className="mr-2 h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
My Studies
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
disabled
|
||||
>
|
||||
<Link href="/experiments">
|
||||
<svg
|
||||
className="mr-2 h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
|
||||
/>
|
||||
</svg>
|
||||
Experiments
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
disabled
|
||||
>
|
||||
<Link href="/wizard">
|
||||
<svg
|
||||
className="mr-2 h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
Wizard Interface
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <ProfileContent user={user} />;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/ui/select";
|
||||
import { ManagementPageLayout } from "~/components/ui/page-layout";
|
||||
import { PageHeader } from "~/components/ui/page-header";
|
||||
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||
import { useStudyContext } from "~/lib/study-context";
|
||||
import { useSelectedStudyDetails } from "~/hooks/useSelectedStudyDetails";
|
||||
|
||||
@@ -303,6 +304,14 @@ export default function StudyAnalyticsPage() {
|
||||
const { setSelectedStudyId, selectedStudyId } = useStudyContext();
|
||||
const { study } = useSelectedStudyDetails();
|
||||
|
||||
// Set breadcrumbs
|
||||
useBreadcrumbsEffect([
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
{ label: "Studies", href: "/studies" },
|
||||
{ label: study?.name ?? "Study", href: `/studies/${studyId}` },
|
||||
{ label: "Analytics" },
|
||||
]);
|
||||
|
||||
// Set the active study if it doesn't match the current route
|
||||
useEffect(() => {
|
||||
if (studyId && selectedStudyId !== studyId) {
|
||||
@@ -311,19 +320,16 @@ export default function StudyAnalyticsPage() {
|
||||
}, [studyId, selectedStudyId, setSelectedStudyId]);
|
||||
|
||||
return (
|
||||
<ManagementPageLayout
|
||||
title="Analytics"
|
||||
description="Insights and data analysis for this study"
|
||||
breadcrumb={[
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
{ label: "Studies", href: "/studies" },
|
||||
{ label: study?.name ?? "Study", href: `/studies/${studyId}` },
|
||||
{ label: "Analytics" },
|
||||
]}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Analytics"
|
||||
description="Insights and data analysis for this study"
|
||||
icon={BarChart3}
|
||||
/>
|
||||
|
||||
<Suspense fallback={<div>Loading analytics...</div>}>
|
||||
<AnalyticsContent studyId={studyId} />
|
||||
</Suspense>
|
||||
</ManagementPageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { Users, Plus } from "lucide-react";
|
||||
import { ParticipantsTable } from "~/components/participants/ParticipantsTable";
|
||||
import { ManagementPageLayout } from "~/components/ui/page-layout";
|
||||
import { PageHeader } from "~/components/ui/page-header";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||
import { useStudyContext } from "~/lib/study-context";
|
||||
import { useSelectedStudyDetails } from "~/hooks/useSelectedStudyDetails";
|
||||
|
||||
@@ -13,6 +16,14 @@ export default function StudyParticipantsPage() {
|
||||
const { setSelectedStudyId, selectedStudyId } = useStudyContext();
|
||||
const { study } = useSelectedStudyDetails();
|
||||
|
||||
// Set breadcrumbs
|
||||
useBreadcrumbsEffect([
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
{ label: "Studies", href: "/studies" },
|
||||
{ label: study?.name ?? "Study", href: `/studies/${studyId}` },
|
||||
{ label: "Participants" },
|
||||
]);
|
||||
|
||||
// Sync selected study (unified study-context)
|
||||
useEffect(() => {
|
||||
if (studyId && selectedStudyId !== studyId) {
|
||||
@@ -21,23 +32,24 @@ export default function StudyParticipantsPage() {
|
||||
}, [studyId, selectedStudyId, setSelectedStudyId]);
|
||||
|
||||
return (
|
||||
<ManagementPageLayout
|
||||
title="Participants"
|
||||
description="Manage participant registration, consent, and trial assignments for this study"
|
||||
breadcrumb={[
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
{ label: "Studies", href: "/studies" },
|
||||
{ label: study?.name ?? "Study", href: `/studies/${studyId}` },
|
||||
{ label: "Participants" },
|
||||
]}
|
||||
createButton={{
|
||||
label: "Add Participant",
|
||||
href: `/studies/${studyId}/participants/new`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Participants"
|
||||
description="Manage participant registration, consent, and trial assignments for this study"
|
||||
icon={Users}
|
||||
actions={
|
||||
<Button asChild>
|
||||
<a href={`/studies/${studyId}/participants/new`}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Participant
|
||||
</a>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Suspense fallback={<div>Loading participants...</div>}>
|
||||
<ParticipantsTable studyId={studyId} />
|
||||
</Suspense>
|
||||
</ManagementPageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { TestTube, Plus } from "lucide-react";
|
||||
import { TrialsTable } from "~/components/trials/TrialsTable";
|
||||
import { ManagementPageLayout } from "~/components/ui/page-layout";
|
||||
import { PageHeader } from "~/components/ui/page-header";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||
import { useStudyContext } from "~/lib/study-context";
|
||||
import { useSelectedStudyDetails } from "~/hooks/useSelectedStudyDetails";
|
||||
|
||||
@@ -13,6 +16,14 @@ export default function StudyTrialsPage() {
|
||||
const { setSelectedStudyId, selectedStudyId } = useStudyContext();
|
||||
const { study } = useSelectedStudyDetails();
|
||||
|
||||
// Set breadcrumbs
|
||||
useBreadcrumbsEffect([
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
{ label: "Studies", href: "/studies" },
|
||||
{ label: study?.name ?? "Study", href: `/studies/${studyId}` },
|
||||
{ label: "Trials" },
|
||||
]);
|
||||
|
||||
// Set the active study if it doesn't match the current route
|
||||
useEffect(() => {
|
||||
if (studyId && selectedStudyId !== studyId) {
|
||||
@@ -21,23 +32,24 @@ export default function StudyTrialsPage() {
|
||||
}, [studyId, selectedStudyId, setSelectedStudyId]);
|
||||
|
||||
return (
|
||||
<ManagementPageLayout
|
||||
title="Trials"
|
||||
description="Schedule, execute, and monitor HRI experiment trials with real-time wizard control for this study"
|
||||
breadcrumb={[
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
{ label: "Studies", href: "/studies" },
|
||||
{ label: study?.name ?? "Study", href: `/studies/${studyId}` },
|
||||
{ label: "Trials" },
|
||||
]}
|
||||
createButton={{
|
||||
label: "Schedule Trial",
|
||||
href: `/studies/${studyId}/trials/new`,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Trials"
|
||||
description="Manage trial execution, scheduling, and data collection for this study"
|
||||
icon={TestTube}
|
||||
actions={
|
||||
<Button asChild>
|
||||
<a href={`/studies/${studyId}/trials/new`}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Schedule Trial
|
||||
</a>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Suspense fallback={<div>Loading trials...</div>}>
|
||||
<TrialsTable studyId={studyId} />
|
||||
</Suspense>
|
||||
</ManagementPageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,11 +24,20 @@ import {
|
||||
} from "~/components/ui/card";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Progress } from "~/components/ui/progress";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/ui/select";
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
// Dashboard Overview Cards
|
||||
function OverviewCards() {
|
||||
const { data: stats, isLoading } = api.dashboard.getStats.useQuery();
|
||||
function OverviewCards({ studyFilter }: { studyFilter: string | null }) {
|
||||
const { data: stats, isLoading } = api.dashboard.getStats.useQuery({
|
||||
studyId: studyFilter ?? undefined,
|
||||
});
|
||||
|
||||
const cards = [
|
||||
{
|
||||
@@ -105,10 +114,11 @@ function OverviewCards() {
|
||||
}
|
||||
|
||||
// Recent Activity Component
|
||||
function RecentActivity() {
|
||||
function RecentActivity({ studyFilter }: { studyFilter: string | null }) {
|
||||
const { data: activities = [], isLoading } =
|
||||
api.dashboard.getRecentActivity.useQuery({
|
||||
limit: 8,
|
||||
studyId: studyFilter ?? undefined,
|
||||
});
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
@@ -236,10 +246,11 @@ function QuickActions() {
|
||||
}
|
||||
|
||||
// Study Progress Component
|
||||
function StudyProgress() {
|
||||
function StudyProgress({ studyFilter }: { studyFilter: string | null }) {
|
||||
const { data: studies = [], isLoading } =
|
||||
api.dashboard.getStudyProgress.useQuery({
|
||||
limit: 5,
|
||||
studyId: studyFilter ?? undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -313,17 +324,59 @@ function StudyProgress() {
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [studyFilter, setStudyFilter] = React.useState<string | null>(null);
|
||||
|
||||
// Get user studies for filter dropdown
|
||||
const { data: userStudiesData } = api.studies.list.useQuery({
|
||||
memberOnly: true,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
const userStudies = userStudiesData?.studies ?? [];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
Dashboard
|
||||
{studyFilter && (
|
||||
<Badge variant="secondary" className="ml-2">
|
||||
{userStudies.find((s) => s.id === studyFilter)?.name}
|
||||
</Badge>
|
||||
)}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Welcome to your HRI Studio research platform
|
||||
{studyFilter
|
||||
? "Study-specific dashboard view"
|
||||
: "Welcome to your HRI Studio research platform"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
Filter by study:
|
||||
</span>
|
||||
<Select
|
||||
value={studyFilter ?? "all"}
|
||||
onValueChange={(value) =>
|
||||
setStudyFilter(value === "all" ? null : value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="All Studies" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Studies</SelectItem>
|
||||
{userStudies.map((study) => (
|
||||
<SelectItem key={study.id} value={study.id}>
|
||||
{study.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Calendar className="mr-1 h-3 w-3" />
|
||||
{new Date().toLocaleDateString()}
|
||||
@@ -332,13 +385,13 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
|
||||
{/* Overview Cards */}
|
||||
<OverviewCards />
|
||||
<OverviewCards studyFilter={studyFilter} />
|
||||
|
||||
{/* Main Content Grid */}
|
||||
<div className="grid gap-4 lg:grid-cols-7">
|
||||
<StudyProgress />
|
||||
<StudyProgress studyFilter={studyFilter} />
|
||||
<div className="col-span-4 space-y-4">
|
||||
<RecentActivity />
|
||||
<RecentActivity studyFilter={studyFilter} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user