mirror of
https://github.com/temetro/temetro.git
synced 2026-08-27 19:06:52 +00:00
Collapse sidebar footer into the user menu; tweak sub-nav & appointments
- Footer is now a single user row. The ⌘K command hint moved into the user menu below Theme, and the clinic switcher became a hover submenu below that (revealing the active clinic's name/slug/count, the switch list, and Create clinic). Removed SidebarCommandButton and deleted team-switcher.tsx. - Patients sub-nav (Patients / Appointments & Schedule): no icons, no background change on hover/active — only the text color changes. - Appointments & Schedule: added an "Add" button that opens the create-patient dialog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { CalendarClock, Clock, Stethoscope, Users } from "lucide-react";
|
import { CalendarClock, Clock, Plus, Stethoscope, Users } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import { type ReactNode, useState } from "react";
|
||||||
|
|
||||||
|
import { PatientFormDialog } from "@/components/chat/patient-form-dialog";
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
|
|
||||||
// All figures here are mock/placeholder data — there is no scheduling backend.
|
// All figures here are mock/placeholder data — there is no scheduling backend.
|
||||||
@@ -199,15 +201,32 @@ function Section({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AppointmentsView() {
|
export function AppointmentsView() {
|
||||||
|
const [addOpen, setAddOpen] = useState(false);
|
||||||
|
// Bumped on open so the create dialog remounts with a fresh file # / form.
|
||||||
|
const [addKey, setAddKey] = useState(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-10 px-6 py-10">
|
<div className="mx-auto flex w-full max-w-5xl flex-col gap-10 px-6 py-10">
|
||||||
<div>
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<h1 className="font-semibold text-2xl tracking-tight">
|
<div>
|
||||||
Appointments & Schedule
|
<h1 className="font-semibold text-2xl tracking-tight">
|
||||||
</h1>
|
Appointments & Schedule
|
||||||
<p className="text-muted-foreground text-sm">
|
</h1>
|
||||||
Today's clinic schedule and what's coming up. Sample data.
|
<p className="text-muted-foreground text-sm">
|
||||||
</p>
|
Today's clinic schedule and what's coming up. Sample data.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="rounded-3xl"
|
||||||
|
onClick={() => {
|
||||||
|
setAddKey((k) => k + 1);
|
||||||
|
setAddOpen(true);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Plus className="size-4" />
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||||
@@ -225,6 +244,14 @@ export function AppointmentsView() {
|
|||||||
<ScheduleList items={group.items} />
|
<ScheduleList items={group.items} />
|
||||||
</Section>
|
</Section>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<PatientFormDialog
|
||||||
|
key={addKey}
|
||||||
|
mode="create"
|
||||||
|
onCreated={() => setAddOpen(false)}
|
||||||
|
onOpenChange={setAddOpen}
|
||||||
|
open={addOpen}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { ArrowDownIcon, ArrowUpIcon, CornerDownLeftIcon } from "lucide-react";
|
||||||
ArrowDownIcon,
|
|
||||||
ArrowUpIcon,
|
|
||||||
CornerDownLeftIcon,
|
|
||||||
Search,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
@@ -32,7 +27,6 @@ import {
|
|||||||
CommandPanel,
|
CommandPanel,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
import { Kbd, KbdGroup } from "@/components/ui/kbd";
|
import { Kbd, KbdGroup } from "@/components/ui/kbd";
|
||||||
import { useSidebar } from "@/components/ui/sidebar";
|
|
||||||
import { navItems } from "@/lib/nav";
|
import { navItems } from "@/lib/nav";
|
||||||
|
|
||||||
type CommandPaletteContextValue = { open: () => void };
|
type CommandPaletteContextValue = { open: () => void };
|
||||||
@@ -171,28 +165,3 @@ export function CommandPaletteProvider({ children }: { children: ReactNode }) {
|
|||||||
</CommandPaletteContext.Provider>
|
</CommandPaletteContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sidebar-footer affordance: shows the ⌘K hint and opens the palette on click.
|
|
||||||
// Hidden when the sidebar is collapsed to its icon rail.
|
|
||||||
export function SidebarCommandButton() {
|
|
||||||
const { open } = useCommandPalette();
|
|
||||||
const { state } = useSidebar();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
if (state === "collapsed") return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-sidebar-muted hover:text-foreground"
|
|
||||||
onClick={open}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Search className="size-4" />
|
|
||||||
<span>{t("nav.quickNav")}</span>
|
|
||||||
<KbdGroup className="ml-auto">
|
|
||||||
<Kbd>⌘</Kbd>
|
|
||||||
<Kbd>K</Kbd>
|
|
||||||
</KbdGroup>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarSeparator,
|
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
@@ -20,11 +19,9 @@ import { motion } from "framer-motion";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { Route } from "./nav-main";
|
import type { Route } from "./nav-main";
|
||||||
import { SidebarCommandButton } from "@/components/command-palette";
|
|
||||||
import DashboardNavigation from "@/components/sidebar-02/nav-main";
|
import DashboardNavigation from "@/components/sidebar-02/nav-main";
|
||||||
import { NotificationsPopover } from "@/components/sidebar-02/nav-notifications";
|
import { NotificationsPopover } from "@/components/sidebar-02/nav-notifications";
|
||||||
import { NavUser } from "@/components/sidebar-02/nav-user";
|
import { NavUser } from "@/components/sidebar-02/nav-user";
|
||||||
import { OrgSwitcher } from "@/components/sidebar-02/team-switcher";
|
|
||||||
|
|
||||||
const sampleNotifications = [
|
const sampleNotifications = [
|
||||||
{
|
{
|
||||||
@@ -117,22 +114,7 @@ export function DashboardSidebar() {
|
|||||||
<DashboardNavigation routes={dashboardRoutes} />
|
<DashboardNavigation routes={dashboardRoutes} />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter className="p-2">
|
<SidebarFooter className="p-2">
|
||||||
<div
|
<NavUser />
|
||||||
className={cn(
|
|
||||||
"flex flex-col gap-1 rounded-xl border border-sidebar-border bg-sidebar-accent/30 p-1",
|
|
||||||
isCollapsed && "border-0 bg-transparent p-0"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SidebarCommandButton />
|
|
||||||
<SidebarSeparator
|
|
||||||
className={cn("mx-0 my-0.5", isCollapsed && "hidden")}
|
|
||||||
/>
|
|
||||||
<OrgSwitcher />
|
|
||||||
<SidebarSeparator
|
|
||||||
className={cn("mx-0 my-0.5", isCollapsed && "hidden")}
|
|
||||||
/>
|
|
||||||
<NavUser />
|
|
||||||
</div>
|
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -89,11 +89,10 @@ export default function DashboardNavigation({ routes }: { routes: Route[] }) {
|
|||||||
{route.subs?.map((subRoute) => (
|
{route.subs?.map((subRoute) => (
|
||||||
<SidebarMenuSubItem key={`${route.id}-${subRoute.link}`}>
|
<SidebarMenuSubItem key={`${route.id}-${subRoute.link}`}>
|
||||||
<SidebarMenuSubButton
|
<SidebarMenuSubButton
|
||||||
className="text-muted-foreground"
|
className="text-muted-foreground hover:bg-transparent hover:text-foreground active:bg-transparent data-[active=true]:bg-transparent data-[active=true]:font-medium data-[active=true]:text-foreground"
|
||||||
isActive={isActive(subRoute.link)}
|
isActive={isActive(subRoute.link)}
|
||||||
render={<Link href={subRoute.link} prefetch={true} />}
|
render={<Link href={subRoute.link} prefetch={true} />}
|
||||||
>
|
>
|
||||||
{subRoute.icon}
|
|
||||||
<span className="truncate">{subRoute.title}</span>
|
<span className="truncate">{subRoute.title}</span>
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSubItem>
|
||||||
|
|||||||
@@ -1,17 +1,33 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Building2,
|
||||||
|
Check,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
LogOut,
|
LogOut,
|
||||||
Moon,
|
Moon,
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
Settings as SettingsIcon,
|
Settings as SettingsIcon,
|
||||||
Sun,
|
Sun,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useCommandPalette } from "@/components/command-palette";
|
||||||
|
import { CreateClinicForm } from "@/components/clinic/create-clinic-form";
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogPanel,
|
||||||
|
DialogPopup,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Kbd, KbdGroup } from "@/components/ui/kbd";
|
||||||
import {
|
import {
|
||||||
Menu,
|
Menu,
|
||||||
MenuGroup,
|
MenuGroup,
|
||||||
@@ -20,6 +36,9 @@ import {
|
|||||||
MenuPopup,
|
MenuPopup,
|
||||||
MenuSeparator,
|
MenuSeparator,
|
||||||
MenuShortcut,
|
MenuShortcut,
|
||||||
|
MenuSub,
|
||||||
|
MenuSubPopup,
|
||||||
|
MenuSubTrigger,
|
||||||
MenuTrigger,
|
MenuTrigger,
|
||||||
} from "@/components/ui/menu";
|
} from "@/components/ui/menu";
|
||||||
import {
|
import {
|
||||||
@@ -57,6 +76,9 @@ function GitHubIcon({ className }: { className?: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The sidebar's single footer row. Its menu also hosts the command-palette
|
||||||
|
// shortcut hint (below Theme) and a clinic switcher submenu (below that) whose
|
||||||
|
// popup reveals the active clinic's details on hover.
|
||||||
export function NavUser() {
|
export function NavUser() {
|
||||||
const { isMobile, state } = useSidebar();
|
const { isMobile, state } = useSidebar();
|
||||||
const isCollapsed = state === "collapsed";
|
const isCollapsed = state === "collapsed";
|
||||||
@@ -64,10 +86,21 @@ export function NavUser() {
|
|||||||
const { resolvedTheme, setTheme } = useTheme();
|
const { resolvedTheme, setTheme } = useTheme();
|
||||||
const isDark = resolvedTheme === "dark";
|
const isDark = resolvedTheme === "dark";
|
||||||
const { data } = authClient.useSession();
|
const { data } = authClient.useSession();
|
||||||
|
const { open: openCommand } = useCommandPalette();
|
||||||
|
const { data: orgs } = authClient.useListOrganizations();
|
||||||
|
const { data: activeOrg } = authClient.useActiveOrganization();
|
||||||
|
|
||||||
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
|
||||||
const name = data?.user?.name ?? "Clinician";
|
const name = data?.user?.name ?? "Clinician";
|
||||||
const email = data?.user?.email ?? "";
|
const email = data?.user?.email ?? "";
|
||||||
const initials = initialsFromName(name);
|
const initials = initialsFromName(name);
|
||||||
|
const activeName = activeOrg?.name ?? "Select clinic";
|
||||||
|
|
||||||
|
const setActive = async (organizationId: string) => {
|
||||||
|
if (organizationId === activeOrg?.id) return;
|
||||||
|
await authClient.organization.setActive({ organizationId });
|
||||||
|
};
|
||||||
|
|
||||||
const signOut = async () => {
|
const signOut = async () => {
|
||||||
const { error } = await authClient.signOut();
|
const { error } = await authClient.signOut();
|
||||||
@@ -99,7 +132,7 @@ export function NavUser() {
|
|||||||
<>
|
<>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span className="truncate font-medium">{name}</span>
|
<span className="truncate font-medium">{name}</span>
|
||||||
<span className="truncate text-xs text-muted-foreground">
|
<span className="truncate text-muted-foreground text-xs">
|
||||||
{email}
|
{email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +142,7 @@ export function NavUser() {
|
|||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
<MenuPopup
|
<MenuPopup
|
||||||
align="start"
|
align="start"
|
||||||
className="min-w-56"
|
className="min-w-60"
|
||||||
side={isMobile ? "bottom" : isCollapsed ? "right" : "top"}
|
side={isMobile ? "bottom" : isCollapsed ? "right" : "top"}
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
>
|
>
|
||||||
@@ -120,7 +153,7 @@ export function NavUser() {
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span className="truncate font-medium">{name}</span>
|
<span className="truncate font-medium">{name}</span>
|
||||||
<span className="truncate text-xs text-muted-foreground">
|
<span className="truncate text-muted-foreground text-xs">
|
||||||
{email}
|
{email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,6 +178,61 @@ export function NavUser() {
|
|||||||
Theme
|
Theme
|
||||||
<MenuShortcut>{isDark ? "Dark" : "Light"}</MenuShortcut>
|
<MenuShortcut>{isDark ? "Dark" : "Light"}</MenuShortcut>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
|
{/* Command palette: hint + shortcut, sits below Theme. */}
|
||||||
|
<MenuItem onClick={openCommand}>
|
||||||
|
<Search />
|
||||||
|
Quick nav
|
||||||
|
<KbdGroup className="ms-auto">
|
||||||
|
<Kbd>⌘</Kbd>
|
||||||
|
<Kbd>K</Kbd>
|
||||||
|
</KbdGroup>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
{/* Clinic switcher: hover reveals the active clinic's details. */}
|
||||||
|
<MenuSub>
|
||||||
|
<MenuSubTrigger>
|
||||||
|
<Building2 />
|
||||||
|
<span className="truncate">{activeName}</span>
|
||||||
|
</MenuSubTrigger>
|
||||||
|
<MenuSubPopup className="min-w-64">
|
||||||
|
<div className="px-2 py-1.5">
|
||||||
|
<p className="truncate font-medium text-foreground text-sm">
|
||||||
|
{activeOrg?.name ?? "No clinic selected"}
|
||||||
|
</p>
|
||||||
|
<p className="truncate text-muted-foreground text-xs">
|
||||||
|
{activeOrg?.slug ? `/${activeOrg.slug}` : "—"} ·{" "}
|
||||||
|
{orgs?.length ?? 0}{" "}
|
||||||
|
{orgs?.length === 1 ? "clinic" : "clinics"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<MenuSeparator />
|
||||||
|
<MenuGroup>
|
||||||
|
<MenuGroupLabel className="text-muted-foreground text-xs">
|
||||||
|
Switch clinic
|
||||||
|
</MenuGroupLabel>
|
||||||
|
{(orgs ?? []).map((org) => (
|
||||||
|
<MenuItem
|
||||||
|
className="gap-2"
|
||||||
|
key={org.id}
|
||||||
|
onClick={() => setActive(org.id)}
|
||||||
|
>
|
||||||
|
<div className="flex size-6 items-center justify-center rounded-sm border">
|
||||||
|
<Building2 className="size-4 shrink-0" />
|
||||||
|
</div>
|
||||||
|
<span className="flex-1 truncate">{org.name}</span>
|
||||||
|
{org.id === activeOrg?.id && <Check className="size-4" />}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</MenuGroup>
|
||||||
|
<MenuSeparator />
|
||||||
|
<MenuItem className="gap-2" onClick={() => setCreateOpen(true)}>
|
||||||
|
<Plus />
|
||||||
|
Create clinic
|
||||||
|
</MenuItem>
|
||||||
|
</MenuSubPopup>
|
||||||
|
</MenuSub>
|
||||||
|
|
||||||
<MenuSeparator />
|
<MenuSeparator />
|
||||||
<MenuItem onClick={signOut} variant="destructive">
|
<MenuItem onClick={signOut} variant="destructive">
|
||||||
<LogOut />
|
<LogOut />
|
||||||
@@ -153,6 +241,21 @@ export function NavUser() {
|
|||||||
</MenuPopup>
|
</MenuPopup>
|
||||||
</Menu>
|
</Menu>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
|
||||||
|
{/* Create a new clinic */}
|
||||||
|
<Dialog onOpenChange={setCreateOpen} open={createOpen}>
|
||||||
|
<DialogPopup className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create clinic</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Add a new clinic and switch to it.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogPanel>
|
||||||
|
<CreateClinicForm onCreated={() => setCreateOpen(false)} />
|
||||||
|
</DialogPanel>
|
||||||
|
</DialogPopup>
|
||||||
|
</Dialog>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Building2, ChevronsUpDown, Info, Plus } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { CreateClinicForm } from "@/components/clinic/create-clinic-form";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogPanel,
|
|
||||||
DialogPopup,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Menu,
|
|
||||||
MenuGroup,
|
|
||||||
MenuGroupLabel,
|
|
||||||
MenuItem,
|
|
||||||
MenuPopup,
|
|
||||||
MenuSeparator,
|
|
||||||
MenuTrigger,
|
|
||||||
} from "@/components/ui/menu";
|
|
||||||
import {
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
useSidebar,
|
|
||||||
} from "@/components/ui/sidebar";
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
|
|
||||||
function InfoRow({ label, value }: { label: string; value: string }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<span className="text-muted-foreground">{label}</span>
|
|
||||||
<span className="truncate font-medium text-foreground">{value}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switches the active clinic (organization). Scopes every subsequent patient
|
|
||||||
// API call. Lives in the sidebar footer; its menu also opens dialogs to view
|
|
||||||
// clinic info or create a new clinic.
|
|
||||||
export function OrgSwitcher() {
|
|
||||||
const { isMobile, state } = useSidebar();
|
|
||||||
const isCollapsed = state === "collapsed";
|
|
||||||
const { data: orgs } = authClient.useListOrganizations();
|
|
||||||
const { data: activeOrg } = authClient.useActiveOrganization();
|
|
||||||
|
|
||||||
const [infoOpen, setInfoOpen] = useState(false);
|
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
|
||||||
|
|
||||||
const setActive = async (organizationId: string) => {
|
|
||||||
if (organizationId === activeOrg?.id) return;
|
|
||||||
await authClient.organization.setActive({ organizationId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeName = activeOrg?.name ?? "Select clinic";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<Menu>
|
|
||||||
<MenuTrigger
|
|
||||||
render={
|
|
||||||
<SidebarMenuButton
|
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
||||||
size="lg"
|
|
||||||
tooltip={activeName}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-background text-foreground">
|
|
||||||
<Building2 className="size-4" />
|
|
||||||
</div>
|
|
||||||
{!isCollapsed && (
|
|
||||||
<>
|
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
||||||
<span className="truncate font-semibold">{activeName}</span>
|
|
||||||
<span className="truncate text-muted-foreground text-xs">
|
|
||||||
Clinic
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ChevronsUpDown className="ml-auto size-4" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuPopup
|
|
||||||
align="start"
|
|
||||||
className="min-w-56 rounded-lg"
|
|
||||||
side={isMobile ? "bottom" : isCollapsed ? "right" : "top"}
|
|
||||||
sideOffset={4}
|
|
||||||
>
|
|
||||||
<MenuGroup>
|
|
||||||
<MenuGroupLabel className="text-muted-foreground text-xs">
|
|
||||||
Clinics
|
|
||||||
</MenuGroupLabel>
|
|
||||||
{(orgs ?? []).map((org) => (
|
|
||||||
<MenuItem
|
|
||||||
className="gap-2 p-2"
|
|
||||||
key={org.id}
|
|
||||||
onClick={() => setActive(org.id)}
|
|
||||||
>
|
|
||||||
<div className="flex size-6 items-center justify-center rounded-sm border">
|
|
||||||
<Building2 className="size-4 shrink-0" />
|
|
||||||
</div>
|
|
||||||
<span className="truncate">{org.name}</span>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</MenuGroup>
|
|
||||||
<MenuSeparator />
|
|
||||||
<MenuItem
|
|
||||||
className="gap-2 p-2"
|
|
||||||
disabled={!activeOrg}
|
|
||||||
onClick={() => setInfoOpen(true)}
|
|
||||||
>
|
|
||||||
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
|
|
||||||
<Info className="size-4" />
|
|
||||||
</div>
|
|
||||||
<div className="font-medium text-muted-foreground">
|
|
||||||
Clinic info
|
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem className="gap-2 p-2" onClick={() => setCreateOpen(true)}>
|
|
||||||
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
|
|
||||||
<Plus className="size-4" />
|
|
||||||
</div>
|
|
||||||
<div className="font-medium text-muted-foreground">
|
|
||||||
Create clinic
|
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
</MenuPopup>
|
|
||||||
</Menu>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
|
|
||||||
{/* Read-only clinic details */}
|
|
||||||
<Dialog onOpenChange={setInfoOpen} open={infoOpen}>
|
|
||||||
<DialogPopup className="sm:max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{activeOrg?.name ?? "Clinic"}</DialogTitle>
|
|
||||||
<DialogDescription>Clinic information</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogPanel className="flex flex-col gap-3 text-sm">
|
|
||||||
<InfoRow label="Name" value={activeOrg?.name ?? "—"} />
|
|
||||||
<InfoRow label="URL slug" value={activeOrg?.slug ?? "—"} />
|
|
||||||
<InfoRow
|
|
||||||
label="Clinics you belong to"
|
|
||||||
value={String(orgs?.length ?? 0)}
|
|
||||||
/>
|
|
||||||
</DialogPanel>
|
|
||||||
</DialogPopup>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Create a new clinic (replaces the old /onboarding redirect) */}
|
|
||||||
<Dialog onOpenChange={setCreateOpen} open={createOpen}>
|
|
||||||
<DialogPopup className="sm:max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Create clinic</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Add a new clinic and switch to it.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogPanel>
|
|
||||||
<CreateClinicForm onCreated={() => setCreateOpen(false)} />
|
|
||||||
</DialogPanel>
|
|
||||||
</DialogPopup>
|
|
||||||
</Dialog>
|
|
||||||
</SidebarMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user