Dental Care
This commit is contained in:
235
components/layout/app-sidebar.tsx
Normal file
235
components/layout/app-sidebar.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
IconChartBar,
|
||||
IconDashboard,
|
||||
IconDatabase,
|
||||
IconFileDescription,
|
||||
IconHelp,
|
||||
IconListDetails,
|
||||
IconReport,
|
||||
IconSettings,
|
||||
IconUsers,
|
||||
IconStethoscope,
|
||||
IconCalendar,
|
||||
IconMedicalCross,
|
||||
IconUserCog,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { NavDocuments } from "@/components/layout/nav-documents";
|
||||
import { NavMain } from "@/components/layout/nav-main";
|
||||
import { NavSecondary } from "@/components/layout/nav-secondary";
|
||||
import { NavUser } from "@/components/layout/nav-user";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
const adminData = {
|
||||
navMain: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "/admin",
|
||||
icon: IconDashboard,
|
||||
},
|
||||
{
|
||||
title: "Appointments",
|
||||
url: "/admin/appointment-management",
|
||||
icon: IconCalendar,
|
||||
},
|
||||
{
|
||||
title: "Patients",
|
||||
url: "/admin/patient-management",
|
||||
icon: IconUsers,
|
||||
},
|
||||
{
|
||||
title: "Dentists",
|
||||
url: "/admin/dentist-management",
|
||||
icon: IconStethoscope,
|
||||
},
|
||||
{
|
||||
title: "Services",
|
||||
url: "/admin/service-management",
|
||||
icon: IconMedicalCross,
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
url: "/admin/user-management",
|
||||
icon: IconUserCog,
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: "Settings",
|
||||
url: "/admin/settings",
|
||||
icon: IconSettings,
|
||||
},
|
||||
{
|
||||
title: "Help & Support",
|
||||
url: "/admin/help-support",
|
||||
icon: IconHelp,
|
||||
},
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
name: "Analytics",
|
||||
url: "/Dashboard",
|
||||
icon: IconChartBar,
|
||||
},
|
||||
{
|
||||
name: "Reports",
|
||||
url: "/Reports",
|
||||
icon: IconReport,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const patientData = {
|
||||
navMain: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "/patient",
|
||||
icon: IconDashboard,
|
||||
},
|
||||
{
|
||||
title: "Book Appointment",
|
||||
url: "/patient/book-appointment",
|
||||
icon: IconCalendar,
|
||||
},
|
||||
{
|
||||
title: "My Appointments",
|
||||
url: "/patient/appointments",
|
||||
icon: IconListDetails,
|
||||
},
|
||||
{
|
||||
title: "Payments",
|
||||
url: "/patient/payments",
|
||||
icon: IconFileDescription,
|
||||
},
|
||||
{
|
||||
title: "Health Records",
|
||||
url: "/patient/health-records",
|
||||
icon: IconDatabase,
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: "Settings",
|
||||
url: "/patient/settings",
|
||||
icon: IconSettings,
|
||||
},
|
||||
{
|
||||
title: "Help & Support",
|
||||
url: "#",
|
||||
icon: IconHelp,
|
||||
},
|
||||
],
|
||||
documents: [],
|
||||
};
|
||||
|
||||
const dentistData = {
|
||||
navMain: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "/dentist",
|
||||
icon: IconDashboard,
|
||||
},
|
||||
{
|
||||
title: "My Appointments",
|
||||
url: "/dentist/appointments",
|
||||
icon: IconCalendar,
|
||||
},
|
||||
{
|
||||
title: "My Patients",
|
||||
url: "/dentist/patients",
|
||||
icon: IconUsers,
|
||||
},
|
||||
{
|
||||
title: "Schedule",
|
||||
url: "/dentist/schedule",
|
||||
icon: IconListDetails,
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: "Settings",
|
||||
url: "/dentist/settings",
|
||||
icon: IconSettings,
|
||||
},
|
||||
{
|
||||
title: "Help & Support",
|
||||
url: "#",
|
||||
icon: IconHelp,
|
||||
},
|
||||
],
|
||||
documents: [],
|
||||
};
|
||||
|
||||
type AppSidebarProps = React.ComponentProps<typeof Sidebar> & {
|
||||
user?: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image?: string | null;
|
||||
role?: string | null;
|
||||
} | null;
|
||||
isAdmin?: boolean;
|
||||
};
|
||||
|
||||
export function AppSidebar({ user, isAdmin, ...props }: AppSidebarProps) {
|
||||
// Determine which data to use based on user role
|
||||
const role = user?.role || "patient";
|
||||
const data =
|
||||
role === "admin"
|
||||
? adminData
|
||||
: role === "dentist"
|
||||
? dentistData
|
||||
: patientData;
|
||||
const homeUrl =
|
||||
role === "admin" ? "/admin" : role === "dentist" ? "/dentist" : "/";
|
||||
return (
|
||||
<>
|
||||
<Sidebar collapsible="offcanvas" {...props}>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
className="data-[slot=sidebar-menu-button]:!p-1.5"
|
||||
>
|
||||
<Link href={homeUrl} className="flex items-center gap-2">
|
||||
<Image
|
||||
src="/tooth.svg"
|
||||
alt="Dental U Care"
|
||||
width={24}
|
||||
height={24}
|
||||
className="!size-6"
|
||||
/>
|
||||
<span className="text-base font-semibold bg-gradient-to-r from-blue-600 to-pink-800 bg-clip-text text-transparent">
|
||||
Dental U-Care
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
{data.documents.length > 0 && <NavDocuments items={data.documents} />}
|
||||
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
{user && <NavUser user={user} isAdmin={isAdmin} />}
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
components/layout/auth-layout-redirect.tsx
Normal file
38
components/layout/auth-layout-redirect.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { authClient } from "@/lib/auth-session/auth-client";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
/**
|
||||
* Client-side redirect for authenticated users visiting auth pages
|
||||
* This runs after page load to avoid SSR/client hydration conflicts
|
||||
*/
|
||||
export function AuthLayoutRedirect() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// Check session on client side only
|
||||
const checkSession = async () => {
|
||||
const { data: session, error } = await authClient.getSession();
|
||||
|
||||
if (!error && session?.user) {
|
||||
const user = session.user as { role?: string };
|
||||
const role = user.role;
|
||||
|
||||
// Redirect based on role
|
||||
if (role === "admin") {
|
||||
router.replace("/admin");
|
||||
} else if (role === "dentist") {
|
||||
router.replace("/dentist");
|
||||
} else if (role === "patient") {
|
||||
router.replace("/patient");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkSession();
|
||||
}, [router]);
|
||||
|
||||
return null; // This component doesn't render anything
|
||||
}
|
||||
51
components/layout/dashboard-layout.tsx
Normal file
51
components/layout/dashboard-layout.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { AppSidebar } from "@/components/layout/app-sidebar";
|
||||
import { SiteHeader } from "@/components/layout/site-header";
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type UserRole = "admin" | "dentist" | "patient";
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string | null | undefined;
|
||||
image?: string | null;
|
||||
};
|
||||
role: UserRole;
|
||||
children: ReactNode;
|
||||
variant?: "inset" | "sidebar";
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared dashboard layout component that wraps pages with SidebarProvider,
|
||||
* AppSidebar, and SiteHeader to reduce code duplication across pages.
|
||||
*/
|
||||
export function DashboardLayout({
|
||||
user,
|
||||
role,
|
||||
children,
|
||||
variant = "inset",
|
||||
}: DashboardLayoutProps) {
|
||||
return (
|
||||
<SidebarProvider
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
||||
"--header-height": "calc(var(--spacing) * 12)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<AppSidebar variant={variant} user={user} />
|
||||
<SidebarInset>
|
||||
<SiteHeader role={role} />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
93
components/layout/nav-documents.tsx
Normal file
93
components/layout/nav-documents.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
IconDots,
|
||||
IconFolder,
|
||||
IconShare3,
|
||||
IconTrash,
|
||||
type Icon,
|
||||
} from "@tabler/icons-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
||||
export function NavDocuments({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
name: string
|
||||
url: string
|
||||
icon: Icon
|
||||
}[]
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Documents</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.name}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction
|
||||
showOnHover
|
||||
className="data-[state=open]:bg-accent rounded-sm"
|
||||
>
|
||||
<IconDots />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-24 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align={isMobile ? "end" : "start"}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<IconFolder />
|
||||
<span>Open</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconShare3 />
|
||||
<span>Share</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<IconTrash />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||
<IconDots className="text-sidebar-foreground/70" />
|
||||
<span>More</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
41
components/layout/nav-main.tsx
Normal file
41
components/layout/nav-main.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { type Icon } from "@tabler/icons-react";
|
||||
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import Link from "next/link";
|
||||
|
||||
export function NavMain({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
title: string;
|
||||
url: string;
|
||||
icon?: Icon;
|
||||
}[];
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent className="flex flex-col gap-2">
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton tooltip={item.title} asChild>
|
||||
<Link href={item.url}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
43
components/layout/nav-secondary.tsx
Normal file
43
components/layout/nav-secondary.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type Icon } from "@tabler/icons-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
||||
export function NavSecondary({
|
||||
items,
|
||||
...props
|
||||
}: {
|
||||
items: {
|
||||
title: string
|
||||
url: string
|
||||
icon: Icon
|
||||
}[]
|
||||
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
||||
return (
|
||||
<SidebarGroup {...props}>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
131
components/layout/nav-user.tsx
Normal file
131
components/layout/nav-user.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
IconCreditCard,
|
||||
IconDotsVertical,
|
||||
IconLogout,
|
||||
} from "@tabler/icons-react";
|
||||
import Link from "next/link";
|
||||
import { authClient } from "@/lib/auth-session/auth-client";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
isAdmin,
|
||||
}: {
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
image?: string | null;
|
||||
role?: string | null;
|
||||
};
|
||||
isAdmin?: boolean;
|
||||
}) {
|
||||
const { isMobile } = useSidebar();
|
||||
const router = useRouter();
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await authClient.signOut();
|
||||
toast.success("Signed out successfully");
|
||||
router.push("/sign-in");
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-lg ">
|
||||
<AvatarImage src={user.image ?? undefined} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{user.name?.[0] ?? "U"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
<IconDotsVertical className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.image ?? undefined} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{user.name?.[0] ?? "U"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{isAdmin && (
|
||||
<>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/admin" className="cursor-pointer">
|
||||
<IconCreditCard className="mr-2" />
|
||||
<span>Dashboard</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{user?.role !== "admin" && (
|
||||
<>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/profile" className="cursor-pointer">
|
||||
<IconCreditCard className="mr-2" />
|
||||
<span>Profile</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuGroup />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleSignOut}>
|
||||
<IconLogout />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
117
components/layout/section-cards.tsx
Normal file
117
components/layout/section-cards.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { IconTrendingUp, IconTrendingDown } from "@tabler/icons-react"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
|
||||
type DashboardStats = {
|
||||
totalAppointments: number
|
||||
appointmentChange: number
|
||||
newPatients: number
|
||||
patientChange: number
|
||||
revenue: number
|
||||
revenueChange: number
|
||||
satisfactionRate: number
|
||||
satisfactionChange: number
|
||||
}
|
||||
|
||||
export function SectionCards({ stats }: { stats: DashboardStats }) {
|
||||
return (
|
||||
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>Total Appointments</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
{stats.totalAppointments.toLocaleString()}
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
{stats.appointmentChange >= 0 ? <IconTrendingUp /> : <IconTrendingDown />}
|
||||
{stats.appointmentChange >= 0 ? '+' : ''}{stats.appointmentChange.toFixed(1)}%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
{stats.appointmentChange >= 0 ? 'Bookings up this month' : 'Bookings down this month'}{' '}
|
||||
{stats.appointmentChange >= 0 ? <IconTrendingUp className="size-4" /> : <IconTrendingDown className="size-4" />}
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
Appointments for the last 30 days
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>New Patients</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
{stats.newPatients.toLocaleString()}
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
{stats.patientChange >= 0 ? <IconTrendingUp /> : <IconTrendingDown />}
|
||||
{stats.patientChange >= 0 ? '+' : ''}{stats.patientChange.toFixed(1)}%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
{stats.patientChange >= 0 ? 'Growing patient base' : 'Patient growth slowing'}{' '}
|
||||
{stats.patientChange >= 0 ? <IconTrendingUp className="size-4" /> : <IconTrendingDown className="size-4" />}
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
New registrations this month
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>Revenue This Month</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
₱{stats.revenue.toLocaleString('en-PH', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
{stats.revenueChange >= 0 ? <IconTrendingUp /> : <IconTrendingDown />}
|
||||
{stats.revenueChange >= 0 ? '+' : ''}{stats.revenueChange.toFixed(1)}%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
{stats.revenueChange >= 0 ? 'Strong financial performance' : 'Revenue needs attention'}{' '}
|
||||
{stats.revenueChange >= 0 ? <IconTrendingUp className="size-4" /> : <IconTrendingDown className="size-4" />}
|
||||
</div>
|
||||
<div className="text-muted-foreground">Total revenue collected</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className="@container/card">
|
||||
<CardHeader>
|
||||
<CardDescription>Patient Satisfaction</CardDescription>
|
||||
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
|
||||
{stats.satisfactionRate.toFixed(1)}%
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Badge variant="outline">
|
||||
{stats.satisfactionChange >= 0 ? <IconTrendingUp /> : <IconTrendingDown />}
|
||||
{stats.satisfactionChange >= 0 ? '+' : ''}{stats.satisfactionChange.toFixed(1)}%
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-sm">
|
||||
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||
{stats.satisfactionRate >= 95 ? 'Excellent patient feedback' : 'Good patient feedback'}{' '}
|
||||
{stats.satisfactionChange >= 0 ? <IconTrendingUp className="size-4" /> : <IconTrendingDown className="size-4" />}
|
||||
</div>
|
||||
<div className="text-muted-foreground">Based on completed appointments</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
37
components/layout/site-header.tsx
Normal file
37
components/layout/site-header.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar"
|
||||
import { ModeToggle } from "../ui/mode-toggle"
|
||||
|
||||
type SiteHeaderProps = {
|
||||
role?: string | null
|
||||
}
|
||||
|
||||
export function SiteHeader({ role }: SiteHeaderProps = {}) {
|
||||
const getTitle = () => {
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
return 'Dental U-Care Admin Dashboard'
|
||||
case 'dentist':
|
||||
return 'Dental U-Care Dentist Portal'
|
||||
case 'patient':
|
||||
return 'Dental U-Care Patient Portal'
|
||||
default:
|
||||
return 'Dental U-Care'
|
||||
}
|
||||
}
|
||||
return (
|
||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
<h1 className="text-base font-medium">{getTitle()}</h1>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user