"use client"; import React, { useEffect, useRef } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { signOut, useSession } from "next-auth/react"; import { toast } from "sonner"; import { BarChart3, BookOpen, Building, ChevronDown, FlaskConical, Home, LogOut, MoreHorizontal, PlayCircle, Puzzle, Settings, TestTube, User, UserCheck, Users, FileText, } from "lucide-react"; import { useSidebar } from "~/components/ui/sidebar"; import { useTour } from "~/components/onboarding/TourProvider"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "~/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "~/components/ui/tooltip"; import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail, } from "~/components/ui/sidebar"; import { Avatar, AvatarImage, AvatarFallback } from "~/components/ui/avatar"; import { Logo } from "~/components/ui/logo"; import { useStudyManagement } from "~/hooks/useStudyManagement"; import { handleAuthError, isAuthError } from "~/lib/auth-error-handler"; import { api } from "~/trpc/react"; // Global items - always available const globalItems = [ { title: "Overview", url: "/dashboard", icon: Home, }, { title: "Studies", url: "/studies", icon: Building, }, { title: "Profile", url: "/profile", icon: User, }, ]; // Current Study Work section - only shown when study is selected const studyWorkItems = [ { title: "Participants", url: "/participants", icon: Users, }, { title: "Trials", url: "/trials", icon: TestTube, }, { title: "Experiments", url: "/experiments", icon: FlaskConical, }, { title: "Forms", url: "/forms", icon: FileText, }, { title: "Analytics", url: "/analytics", icon: BarChart3, }, { title: "Plugins", url: "/plugins", icon: Puzzle, }, ]; const adminItems = [ { title: "Administration", url: "/admin", icon: UserCheck, }, ]; const helpItems = [ { title: "Help Center", url: "/help", icon: BookOpen, }, { title: "Interactive Tour", url: "#tour", icon: PlayCircle, action: "tour", }, ]; interface AppSidebarProps extends React.ComponentProps { userRole?: string; } export function AppSidebar({ userRole = "researcher", ...props }: AppSidebarProps) { const { data: session } = useSession(); const pathname = usePathname(); const isAdmin = userRole === "administrator"; const { state: sidebarState } = useSidebar(); const isCollapsed = sidebarState === "collapsed"; const { selectedStudyId, userStudies, selectStudy, refreshStudyData, isLoadingUserStudies, } = useStudyManagement(); const { startTour, isTourActive } = useTour(); // Reference to track if we've already attempted auto-selection to avoid fighting with manual clearing const hasAutoSelected = useRef(false); // Auto-select most recently touched study if none selected useEffect(() => { // Only run if not loading, no study selected, and we have studies available // And only run once per session (using ref) to allow user to clear selection if desired if ( !isLoadingUserStudies && !selectedStudyId && userStudies.length > 0 && !hasAutoSelected.current ) { // userStudies is sorted by updatedAt desc from the API, so the first one is the most recent // userStudies is sorted by updatedAt desc from the API, so the first one is the most recent const mostRecent = userStudies[0]; if (mostRecent) { console.log("Auto-selecting most recent study:", mostRecent.name); void selectStudy(mostRecent.id); hasAutoSelected.current = true; } } }, [isLoadingUserStudies, selectedStudyId, userStudies, selectStudy]); // Debug API call const { data: debugData } = api.dashboard.debug.useQuery(undefined, { enabled: process.env.NODE_ENV === "development", staleTime: 1000 * 30, // 30 seconds }); type Study = { id: string; name: string; }; // Build study work items with proper URLs when study is selected const studyWorkItemsWithUrls = selectedStudyId ? studyWorkItems.map((item) => ({ ...item, url: `/studies/${selectedStudyId}${item.url}`, })) : []; const handleSignOut = async () => { await signOut({ callbackUrl: "/" }); }; const handleStudySelect = async (studyId: string) => { try { await selectStudy(studyId); } catch (error) { console.error("Failed to select study:", error); // Handle auth errors first if (isAuthError(error)) { await handleAuthError(error, "Session expired while selecting study"); return; } // If study selection fails (e.g., study not found), clear the selection await selectStudy(null); } }; const handleClearStudy = async (event: React.MouseEvent) => { try { event.preventDefault(); event.stopPropagation(); console.log("Clearing study selection..."); await selectStudy(null); console.log("Study selection cleared successfully"); toast.success("Study selection cleared"); } catch (error) { console.error("Failed to clear study:", error); // Handle auth errors first if (isAuthError(error)) { await handleAuthError(error, "Session expired while clearing study"); return; } toast.error("Failed to clear study selection"); } }; const selectedStudy = userStudies.find( (study: Study) => study.id === selectedStudyId, ); // Debug logging for study data React.useEffect(() => { console.log("Sidebar debug - User studies:", { count: userStudies.length, studies: userStudies.map((s) => ({ id: s.id, name: s.name })), selectedStudyId, selectedStudy: selectedStudy ? { id: selectedStudy.id, name: selectedStudy.name } : null, }); }, [userStudies, selectedStudyId, selectedStudy]); // If we have a selectedStudyId but can't find the study, clear the selection React.useEffect(() => { if (selectedStudyId && userStudies.length > 0 && !selectedStudy) { console.warn( "Selected study not found in user studies, clearing selection", ); void selectStudy(null); } }, [selectedStudyId, userStudies, selectedStudy, selectStudy]); // Auto-refresh studies list when component mounts to catch external changes useEffect(() => { const interval = setInterval(() => { void (async () => { try { await refreshStudyData(); } catch (error) { console.error("Failed to refresh study data:", error); if (isAuthError(error)) { void handleAuthError(error, "Session expired during data refresh"); } } })(); }, 30000); // Refresh every 30 seconds return () => clearInterval(interval); }, [refreshStudyData]); // Show debug info in development const showDebug = process.env.NODE_ENV === "development"; const [mounted, setMounted] = React.useState(false); React.useEffect(() => { setMounted(true); }, []); if (!mounted) { return ( ); } return ( {isTourActive && !isCollapsed && (
Tutorial Active
)}
{/* Study Selector */} Active Study {isCollapsed ? ( {selectedStudy?.name ?? "Select Study"} Studies {userStudies.map((study: Study) => ( handleStudySelect(study.id)} className="cursor-pointer" > {study.name} ))} {selectedStudyId && ( Clear selection )} Create study {selectedStudy?.name ?? "Select Study"} ) : ( {selectedStudy?.name ?? "Select Study"} Studies {userStudies.map((study: Study) => ( handleStudySelect(study.id)} className="cursor-pointer" > {study.name} ))} {selectedStudyId && ( Clear selection )} Create study )} {/* Main Navigation */} {/* Global Section */} Platform {globalItems.map((item) => { const isActive = pathname === item.url || (item.url !== "/dashboard" && pathname.startsWith(item.url)); const menuButton = ( {item.title} ); return ( {isCollapsed ? ( {menuButton} {item.title} ) : ( menuButton )} ); })} {/* Current Study Work Section */} {selectedStudyId && selectedStudy ? ( Current Study Work {studyWorkItemsWithUrls.map((item) => { const isActive = pathname === item.url || (item.url !== "/dashboard" && pathname.startsWith(item.url)); const menuButton = ( {item.title} ); return ( {isCollapsed ? ( {menuButton} {item.title} ) : ( menuButton )} ); })} ) : ( !isCollapsed && ( Current Study Work
Select a study to access participants, trials, experiments, and analytics.
) )} {/* Admin Section */} {isAdmin && ( Administration {adminItems.map((item) => { const isActive = pathname.startsWith(item.url); const menuButton = ( {item.title} ); return ( {isCollapsed ? ( {menuButton} {item.title} ) : ( menuButton )} ); })} )}
{/* Help Section */} Support {helpItems.map((item) => { const isActive = pathname.startsWith(item.url); const menuButton = item.action === "tour" ? ( startTour("full_platform")} isActive={false} > {item.title} ) : ( {item.title} ); return ( {isCollapsed ? ( {menuButton} {item.title} ) : ( menuButton )} ); })} {/* Debug info moved to footer tooltip button */} {showDebug && ( {isCollapsed ? (
Session: {session?.user?.email ?? "No session"}
Role: {userRole ?? "No role"}
Studies: {userStudies.length}
Selected: {selectedStudy?.name ?? "None"}
Auth: {session ? "✓" : "✗"}
{debugData && ( <>
DB User: {debugData.user?.email ?? "None"}
System Roles:{" "} {debugData.systemRoles.join(", ") || "None"}
Memberships: {debugData.studyMemberships.length}
All Studies: {debugData.allStudies.length}
Session ID: {debugData.session.userId.slice(0, 8)} ...
)}
) : ( Debug Debug Info
Session: {session?.user?.email ?? "No session"}
Role: {userRole ?? "No role"}
Studies: {userStudies.length}
Selected: {selectedStudy?.name ?? "None"}
Auth: {session ? "✓" : "✗"}
{debugData && ( <>
DB User: {debugData.user?.email ?? "None"}
System Roles:{" "} {debugData.systemRoles.join(", ") || "None"}
Memberships: {debugData.studyMemberships.length}
All Studies: {debugData.allStudies.length}
Session ID: {debugData.session.userId.slice(0, 8)} ...
)}
)}
)} {isCollapsed ? ( {( session?.user?.name ?? session?.user?.email ?? "U" ) .charAt(0) .toUpperCase()}
{session?.user?.name ?? "User"} {session?.user?.email ?? ""}
{( session?.user?.name ?? session?.user?.email ?? "U" ) .charAt(0) .toUpperCase()}
{session?.user?.name ?? "User"} {session?.user?.email ?? ""}
Profile & Settings Sign out
{session?.user?.name ?? "User Menu"}
) : ( {(session?.user?.name ?? session?.user?.email ?? "U") .charAt(0) .toUpperCase()}
{session?.user?.name ?? "User"} {session?.user?.email ?? ""}
{(session?.user?.name ?? session?.user?.email ?? "U") .charAt(0) .toUpperCase()}
{session?.user?.name ?? "User"} {session?.user?.email ?? ""}
Profile & Settings Sign out
)}
); }