applayout added, sidebar updated
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Switch, Route } from "wouter";
|
import { Switch, Route } from "wouter";
|
||||||
import React, { Suspense, lazy } from "react";
|
import React, { lazy } from "react";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { store } from "./redux/store";
|
import { store } from "./redux/store";
|
||||||
import { queryClient } from "./lib/queryClient";
|
import { queryClient } from "./lib/queryClient";
|
||||||
@@ -9,7 +9,6 @@ import { TooltipProvider } from "./components/ui/tooltip";
|
|||||||
import { ProtectedRoute } from "./lib/protected-route";
|
import { ProtectedRoute } from "./lib/protected-route";
|
||||||
import { AuthProvider } from "./hooks/use-auth";
|
import { AuthProvider } from "./hooks/use-auth";
|
||||||
import Dashboard from "./pages/dashboard";
|
import Dashboard from "./pages/dashboard";
|
||||||
import LoadingScreen from "./components/ui/LoadingScreen";
|
|
||||||
|
|
||||||
const AuthPage = lazy(() => import("./pages/auth-page"));
|
const AuthPage = lazy(() => import("./pages/auth-page"));
|
||||||
const AppointmentsPage = lazy(() => import("./pages/appointments-page"));
|
const AppointmentsPage = lazy(() => import("./pages/appointments-page"));
|
||||||
@@ -24,6 +23,7 @@ const DocumentPage = lazy(() => import("./pages/documents-page"));
|
|||||||
const DatabaseManagementPage = lazy(
|
const DatabaseManagementPage = lazy(
|
||||||
() => import("./pages/database-management-page")
|
() => import("./pages/database-management-page")
|
||||||
);
|
);
|
||||||
|
const ReportsPage = lazy(() => import("./pages/reports-page"));
|
||||||
const NotFound = lazy(() => import("./pages/not-found"));
|
const NotFound = lazy(() => import("./pages/not-found"));
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
@@ -47,6 +47,7 @@ function Router() {
|
|||||||
path="/database-management"
|
path="/database-management"
|
||||||
component={() => <DatabaseManagementPage />}
|
component={() => <DatabaseManagementPage />}
|
||||||
/>
|
/>
|
||||||
|
<ProtectedRoute path="/reports/" component={() => <ReportsPage />} />
|
||||||
<Route path="/auth" component={() => <AuthPage />} />
|
<Route path="/auth" component={() => <AuthPage />} />
|
||||||
<Route component={() => <NotFound />} />
|
<Route component={() => <NotFound />} />
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -60,9 +61,7 @@ function App() {
|
|||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<Suspense fallback={<LoadingScreen />}>
|
<Router />
|
||||||
<Router />
|
|
||||||
</Suspense>
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
25
apps/Frontend/src/components/layout/app-layout.tsx
Normal file
25
apps/Frontend/src/components/layout/app-layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||||
|
import { Sidebar } from "@/components/layout/sidebar";
|
||||||
|
import { TopAppBar } from "@/components/layout/top-app-bar";
|
||||||
|
|
||||||
|
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<SidebarProvider defaultOpen>
|
||||||
|
<div className="flex flex-col h-screen">
|
||||||
|
{/* Fixed top bar */}
|
||||||
|
<TopAppBar />
|
||||||
|
|
||||||
|
{/* Main content area */}
|
||||||
|
<div className="flex flex-1 pt-16 min-h-0 bg-gray-100">
|
||||||
|
{/* Sidebar (collapsible on mobile) */}
|
||||||
|
<Sidebar />
|
||||||
|
|
||||||
|
{/* Page content */}
|
||||||
|
<main className="flex-1 min-w-0 min-h-0 overflow-y-auto p-4">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SidebarProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,96 +9,115 @@ import {
|
|||||||
CreditCard,
|
CreditCard,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Database,
|
Database,
|
||||||
|
FileText,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useSidebar } from "@/components/ui/sidebar";
|
||||||
|
|
||||||
interface SidebarProps {
|
const WIDTH_ANIM_MS = 100;
|
||||||
isMobileOpen: boolean;
|
|
||||||
setIsMobileOpen: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Sidebar({ isMobileOpen, setIsMobileOpen }: SidebarProps) {
|
export function Sidebar() {
|
||||||
const [location] = useLocation();
|
const [location] = useLocation();
|
||||||
|
const { state, openMobile, setOpenMobile } = useSidebar(); // "expanded" | "collapsed"
|
||||||
|
|
||||||
const navItems = [
|
// Delay label visibility until the width animation completes
|
||||||
{
|
const [showLabels, setShowLabels] = useState(state !== "collapsed");
|
||||||
name: "Dashboard",
|
useEffect(() => {
|
||||||
path: "/",
|
let timer: number | undefined;
|
||||||
icon: <LayoutDashboard className="h-5 w-5" />,
|
|
||||||
},
|
if (state === "expanded") {
|
||||||
{
|
timer = window.setTimeout(() => setShowLabels(true), WIDTH_ANIM_MS);
|
||||||
name: "Appointments",
|
} else {
|
||||||
path: "/appointments",
|
setShowLabels(false);
|
||||||
icon: <Calendar className="h-5 w-5" />,
|
}
|
||||||
},
|
|
||||||
{
|
return () => {
|
||||||
name: "Patients",
|
if (timer !== undefined) {
|
||||||
path: "/patients",
|
window.clearTimeout(timer);
|
||||||
icon: <Users className="h-5 w-5" />,
|
}
|
||||||
},
|
};
|
||||||
{
|
}, [state]);
|
||||||
name: "Insurance Eligibility",
|
|
||||||
path: "/insurance-eligibility",
|
const navItems = useMemo(
|
||||||
icon: <Shield className="h-5 w-5" />,
|
() => [
|
||||||
},
|
{
|
||||||
{
|
name: "Dashboard",
|
||||||
name: "Claims",
|
path: "/",
|
||||||
path: "/claims",
|
icon: <LayoutDashboard className="h-5 w-5" />,
|
||||||
icon: <FileCheck className="h-5 w-5" />,
|
},
|
||||||
},
|
{
|
||||||
{
|
name: "Appointments",
|
||||||
name: "Payments",
|
path: "/appointments",
|
||||||
path: "/payments",
|
icon: <Calendar className="h-5 w-5" />,
|
||||||
icon: <CreditCard className="h-5 w-5" />,
|
},
|
||||||
},
|
{
|
||||||
{
|
name: "Patients",
|
||||||
name: "Documents",
|
path: "/patients",
|
||||||
path: "/documents",
|
icon: <Users className="h-5 w-5" />,
|
||||||
icon: <FolderOpen className="h-5 w-5" />,
|
},
|
||||||
},
|
{
|
||||||
{
|
name: "Insurance Eligibility",
|
||||||
name: "Backup Database",
|
path: "/insurance-eligibility",
|
||||||
path: "/database-management",
|
icon: <Shield className="h-5 w-5" />,
|
||||||
icon: <Database className="h-5 w-5" />,
|
},
|
||||||
},
|
{
|
||||||
{
|
name: "Claims",
|
||||||
name: "Settings",
|
path: "/claims",
|
||||||
path: "/settings",
|
icon: <FileCheck className="h-5 w-5" />,
|
||||||
icon: <Settings className="h-5 w-5" />,
|
},
|
||||||
},
|
{
|
||||||
];
|
name: "Payments",
|
||||||
|
path: "/payments",
|
||||||
|
icon: <CreditCard className="h-5 w-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Documents",
|
||||||
|
path: "/documents",
|
||||||
|
icon: <FolderOpen className="h-5 w-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Reports",
|
||||||
|
path: "/reports",
|
||||||
|
icon: <FileText className="h-5 w-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Backup Database",
|
||||||
|
path: "/database-management",
|
||||||
|
icon: <Database className="h-5 w-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Settings",
|
||||||
|
path: "/settings",
|
||||||
|
icon: <Settings className="h-5 w-5" />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-white w-64 border-r border-gray-200 shadow-sm z-10 fixed h-full md:static",
|
// original look
|
||||||
isMobileOpen ? "block" : "hidden md:block"
|
"bg-white border-r border-gray-200 shadow-sm z-20",
|
||||||
|
// clip during width animation to avoid text peeking
|
||||||
|
"overflow-hidden will-change-[width]",
|
||||||
|
// animate width only
|
||||||
|
"transition-[width] duration-200 ease-in-out",
|
||||||
|
// MOBILE: overlay below topbar (h = 100vh - 4rem)
|
||||||
|
openMobile
|
||||||
|
? "fixed top-16 left-0 h-[calc(100vh-4rem)] w-64 block md:hidden"
|
||||||
|
: "hidden md:block",
|
||||||
|
// DESKTOP: participates in row layout
|
||||||
|
"md:static md:top-auto md:h-auto md:flex-shrink-0",
|
||||||
|
state === "collapsed" ? "md:w-16" : "md:w-64"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="p-4 border-b border-gray-200 flex items-center space-x-2">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className="h-5 w-5 text-primary"
|
|
||||||
>
|
|
||||||
<path d="M12 14c-1.65 0-3-1.35-3-3V5c0-1.65 1.35-3 3-3s3 1.35 3 3v6c0 1.65-1.35 3-3 3Z" />
|
|
||||||
<path d="M19 14v-4a7 7 0 0 0-14 0v4" />
|
|
||||||
<path d="M12 19c-5 0-8-2-9-5.5m18 0c-1 3.5-4 5.5-9 5.5Z" />
|
|
||||||
</svg>
|
|
||||||
<h1 className="text-lg font-medium text-primary">DentalConnect</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<nav>
|
<nav role="navigation" aria-label="Main">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<div key={item.path}>
|
<div key={item.path}>
|
||||||
<Link to={item.path} onClick={() => setIsMobileOpen(false)}>
|
<Link to={item.path} onClick={() => setOpenMobile(false)}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center space-x-3 p-2 rounded-md pl-3 mb-1 transition-colors cursor-pointer",
|
"flex items-center space-x-3 p-2 rounded-md pl-3 mb-1 transition-colors cursor-pointer",
|
||||||
@@ -108,7 +127,12 @@ export function Sidebar({ isMobileOpen, setIsMobileOpen }: SidebarProps) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
<span>{item.name}</span>
|
{/* show label only after expand animation completes */}
|
||||||
|
{showLabels && (
|
||||||
|
<span className="whitespace-nowrap select-none">
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Bell, Menu } from "lucide-react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
@@ -11,47 +10,45 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { NotificationsBell } from "@/components/layout/notification-bell";
|
import { NotificationsBell } from "@/components/layout/notification-bell";
|
||||||
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
|
|
||||||
interface TopAppBarProps {
|
export function TopAppBar() {
|
||||||
toggleMobileMenu: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TopAppBar({ toggleMobileMenu }: TopAppBarProps) {
|
|
||||||
const { user, logoutMutation } = useAuth();
|
const { user, logoutMutation } = useAuth();
|
||||||
const [location, setLocation] = useLocation();
|
const [location, setLocation] = useLocation();
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => logoutMutation.mutate();
|
||||||
logoutMutation.mutate();
|
const getInitials = (username: string) =>
|
||||||
};
|
username.substring(0, 2).toUpperCase();
|
||||||
|
|
||||||
const getInitials = (username: string) => {
|
|
||||||
return username.substring(0, 2).toUpperCase();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-white shadow-sm z-10">
|
<header className="bg-white shadow-sm z-30 fixed top-0 left-0 right-0">
|
||||||
<div className="flex items-center justify-between h-16 px-4">
|
<div className="flex items-center justify-between h-16 px-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
{/* both desktop + mobile triggers */}
|
||||||
variant="ghost"
|
<SidebarTrigger className="mr-2" />
|
||||||
size="icon"
|
|
||||||
className="md:hidden mr-2"
|
|
||||||
onClick={toggleMobileMenu}
|
|
||||||
>
|
|
||||||
<Menu className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
<h1 className="md:hidden text-lg font-medium text-primary">
|
|
||||||
DentalConnect
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="hidden md:flex md:flex-1 items-center justify-center">
|
<div className="p-4 border-gray-200 flex items-center space-x-2">
|
||||||
{/* Search bar removed */}
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="h-5 w-5 text-primary"
|
||||||
|
>
|
||||||
|
<path d="M12 14c-1.65 0-3-1.35-3-3V5c0-1.65 1.35-3 3-3s3 1.35 3 3v6c0 1.65-1.35 3-3 3Z" />
|
||||||
|
<path d="M19 14v-4a7 7 0 0 0-14 0v4" />
|
||||||
|
<path d="M12 19c-5 0-8-2-9-5.5m18 0c-1 3.5-4 5.5-9 5.5Z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<h1 className="text-lg font-medium text-primary">DentalConnect</h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<NotificationsBell />
|
<NotificationsBell />
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,62 +1,69 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { VariantProps, cva } from "class-variance-authority"
|
import { VariantProps, cva } from "class-variance-authority";
|
||||||
import { PanelLeft } from "lucide-react"
|
import { PanelLeft } from "lucide-react";
|
||||||
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
import { useIsMobile } from "@/hooks/use-mobile"
|
import { cn } from "@/lib/utils";
|
||||||
import { cn } from "@/lib/utils"
|
import { Button } from "@/components/ui/button";
|
||||||
import { Button } from "@/components/ui/button"
|
import { Input } from "@/components/ui/input";
|
||||||
import { Input } from "@/components/ui/input"
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
SheetDescription,
|
SheetDescription,
|
||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
} from "@/components/ui/sheet"
|
} from "@/components/ui/sheet";
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip"
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||||
const SIDEBAR_WIDTH = "16rem"
|
const SIDEBAR_WIDTH = "16rem";
|
||||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||||
|
|
||||||
type SidebarContextProps = {
|
type SidebarContextProps = {
|
||||||
state: "expanded" | "collapsed"
|
state: "expanded" | "collapsed";
|
||||||
open: boolean
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void
|
setOpen: (open: boolean) => void;
|
||||||
openMobile: boolean
|
openMobile: boolean;
|
||||||
setOpenMobile: (open: boolean) => void
|
setOpenMobile: (open: boolean) => void;
|
||||||
isMobile: boolean
|
isMobile: boolean;
|
||||||
toggleSidebar: () => void
|
toggleSidebar: () => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||||
|
|
||||||
function useSidebar() {
|
function useSidebar() {
|
||||||
const context = React.useContext(SidebarContext)
|
const context = React.useContext(SidebarContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
throw new Error("useSidebar must be used within a SidebarProvider.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSidebarCookie(): boolean | undefined {
|
||||||
|
if (typeof document === "undefined") return undefined; // SSR safety
|
||||||
|
const m = document.cookie.match(
|
||||||
|
new RegExp(`(?:^|; )${SIDEBAR_COOKIE_NAME}=([^;]*)`)
|
||||||
|
);
|
||||||
|
return m ? m[1] === "true" : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SidebarProvider = React.forwardRef<
|
const SidebarProvider = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> & {
|
React.ComponentProps<"div"> & {
|
||||||
defaultOpen?: boolean
|
defaultOpen?: boolean;
|
||||||
open?: boolean
|
open?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void
|
onOpenChange?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
@@ -71,54 +78,35 @@ const SidebarProvider = React.forwardRef<
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile();
|
||||||
const [openMobile, setOpenMobile] = React.useState(false)
|
const [openMobile, setOpenMobile] = React.useState(false);
|
||||||
|
|
||||||
// This is the internal state of the sidebar.
|
const cookieOpen = readSidebarCookie();
|
||||||
// We use openProp and setOpenProp for control from outside the component.
|
const [_open, _setOpen] = React.useState<boolean>(
|
||||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
cookieOpen !== undefined ? cookieOpen : defaultOpen
|
||||||
const open = openProp ?? _open
|
);
|
||||||
|
|
||||||
|
const open = openProp ?? _open;
|
||||||
const setOpen = React.useCallback(
|
const setOpen = React.useCallback(
|
||||||
(value: boolean | ((value: boolean) => boolean)) => {
|
(value: boolean | ((value: boolean) => boolean)) => {
|
||||||
const openState = typeof value === "function" ? value(open) : value
|
const openState = typeof value === "function" ? value(open) : value;
|
||||||
if (setOpenProp) {
|
if (setOpenProp) {
|
||||||
setOpenProp(openState)
|
setOpenProp(openState);
|
||||||
} else {
|
} else {
|
||||||
_setOpen(openState)
|
_setOpen(openState);
|
||||||
}
|
}
|
||||||
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||||
// This sets the cookie to keep the sidebar state.
|
|
||||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
|
||||||
},
|
},
|
||||||
[setOpenProp, open]
|
[setOpenProp, open]
|
||||||
)
|
);
|
||||||
|
|
||||||
// Helper to toggle the sidebar.
|
|
||||||
const toggleSidebar = React.useCallback(() => {
|
const toggleSidebar = React.useCallback(() => {
|
||||||
return isMobile
|
return isMobile
|
||||||
? setOpenMobile((open) => !open)
|
? setOpenMobile((open) => !open)
|
||||||
: setOpen((open) => !open)
|
: setOpen((open) => !open);
|
||||||
}, [isMobile, setOpen, setOpenMobile])
|
}, [isMobile, setOpen, setOpenMobile]);
|
||||||
|
|
||||||
// Adds a keyboard shortcut to toggle the sidebar.
|
const state = open ? "expanded" : "collapsed";
|
||||||
React.useEffect(() => {
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (
|
|
||||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
|
||||||
(event.metaKey || event.ctrlKey)
|
|
||||||
) {
|
|
||||||
event.preventDefault()
|
|
||||||
toggleSidebar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown)
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
|
||||||
}, [toggleSidebar])
|
|
||||||
|
|
||||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
|
||||||
// This makes it easier to style the sidebar with Tailwind classes.
|
|
||||||
const state = open ? "expanded" : "collapsed"
|
|
||||||
|
|
||||||
const contextValue = React.useMemo<SidebarContextProps>(
|
const contextValue = React.useMemo<SidebarContextProps>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -131,11 +119,12 @@ const SidebarProvider = React.forwardRef<
|
|||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
}),
|
}),
|
||||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarContext.Provider value={contextValue}>
|
<SidebarContext.Provider value={contextValue}>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
|
{/* FIX: remove flex + w-full wrapper */}
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
@@ -144,10 +133,7 @@ const SidebarProvider = React.forwardRef<
|
|||||||
...style,
|
...style,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn("group/sidebar-wrapper min-h-svh", className)}
|
||||||
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -155,17 +141,18 @@ const SidebarProvider = React.forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</SidebarContext.Provider>
|
</SidebarContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
SidebarProvider.displayName = "SidebarProvider"
|
|
||||||
|
SidebarProvider.displayName = "SidebarProvider";
|
||||||
|
|
||||||
const Sidebar = React.forwardRef<
|
const Sidebar = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> & {
|
React.ComponentProps<"div"> & {
|
||||||
side?: "left" | "right"
|
side?: "left" | "right";
|
||||||
variant?: "sidebar" | "floating" | "inset"
|
variant?: "sidebar" | "floating" | "inset";
|
||||||
collapsible?: "offcanvas" | "icon" | "none"
|
collapsible?: "offcanvas" | "icon" | "none";
|
||||||
}
|
}
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
@@ -179,7 +166,7 @@ const Sidebar = React.forwardRef<
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||||
|
|
||||||
if (collapsible === "none") {
|
if (collapsible === "none") {
|
||||||
return (
|
return (
|
||||||
@@ -193,7 +180,7 @@ const Sidebar = React.forwardRef<
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
@@ -217,7 +204,7 @@ const Sidebar = React.forwardRef<
|
|||||||
<div className="flex h-full w-full flex-col">{children}</div>
|
<div className="flex h-full w-full flex-col">{children}</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -262,16 +249,16 @@ const Sidebar = React.forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
Sidebar.displayName = "Sidebar"
|
Sidebar.displayName = "Sidebar";
|
||||||
|
|
||||||
const SidebarTrigger = React.forwardRef<
|
const SidebarTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof Button>,
|
React.ElementRef<typeof Button>,
|
||||||
React.ComponentProps<typeof Button>
|
React.ComponentProps<typeof Button>
|
||||||
>(({ className, onClick, ...props }, ref) => {
|
>(({ className, onClick, ...props }, ref) => {
|
||||||
const { toggleSidebar } = useSidebar()
|
const { toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -281,23 +268,23 @@ const SidebarTrigger = React.forwardRef<
|
|||||||
size="icon"
|
size="icon"
|
||||||
className={cn("h-7 w-7", className)}
|
className={cn("h-7 w-7", className)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
onClick?.(event)
|
onClick?.(event);
|
||||||
toggleSidebar()
|
toggleSidebar();
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<PanelLeft />
|
<PanelLeft />
|
||||||
<span className="sr-only">Toggle Sidebar</span>
|
<span className="sr-only">Toggle Sidebar</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarTrigger.displayName = "SidebarTrigger"
|
SidebarTrigger.displayName = "SidebarTrigger";
|
||||||
|
|
||||||
const SidebarRail = React.forwardRef<
|
const SidebarRail = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
React.ComponentProps<"button">
|
React.ComponentProps<"button">
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const { toggleSidebar } = useSidebar()
|
const { toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -318,9 +305,9 @@ const SidebarRail = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarRail.displayName = "SidebarRail"
|
SidebarRail.displayName = "SidebarRail";
|
||||||
|
|
||||||
const SidebarInset = React.forwardRef<
|
const SidebarInset = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@@ -336,9 +323,9 @@ const SidebarInset = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarInset.displayName = "SidebarInset"
|
SidebarInset.displayName = "SidebarInset";
|
||||||
|
|
||||||
const SidebarInput = React.forwardRef<
|
const SidebarInput = React.forwardRef<
|
||||||
React.ElementRef<typeof Input>,
|
React.ElementRef<typeof Input>,
|
||||||
@@ -354,9 +341,9 @@ const SidebarInput = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarInput.displayName = "SidebarInput"
|
SidebarInput.displayName = "SidebarInput";
|
||||||
|
|
||||||
const SidebarHeader = React.forwardRef<
|
const SidebarHeader = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@@ -369,9 +356,9 @@ const SidebarHeader = React.forwardRef<
|
|||||||
className={cn("flex flex-col gap-2 p-2", className)}
|
className={cn("flex flex-col gap-2 p-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarHeader.displayName = "SidebarHeader"
|
SidebarHeader.displayName = "SidebarHeader";
|
||||||
|
|
||||||
const SidebarFooter = React.forwardRef<
|
const SidebarFooter = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@@ -384,9 +371,9 @@ const SidebarFooter = React.forwardRef<
|
|||||||
className={cn("flex flex-col gap-2 p-2", className)}
|
className={cn("flex flex-col gap-2 p-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarFooter.displayName = "SidebarFooter"
|
SidebarFooter.displayName = "SidebarFooter";
|
||||||
|
|
||||||
const SidebarSeparator = React.forwardRef<
|
const SidebarSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof Separator>,
|
React.ElementRef<typeof Separator>,
|
||||||
@@ -399,9 +386,9 @@ const SidebarSeparator = React.forwardRef<
|
|||||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarSeparator.displayName = "SidebarSeparator"
|
SidebarSeparator.displayName = "SidebarSeparator";
|
||||||
|
|
||||||
const SidebarContent = React.forwardRef<
|
const SidebarContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@@ -417,9 +404,9 @@ const SidebarContent = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarContent.displayName = "SidebarContent"
|
SidebarContent.displayName = "SidebarContent";
|
||||||
|
|
||||||
const SidebarGroup = React.forwardRef<
|
const SidebarGroup = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@@ -432,15 +419,15 @@ const SidebarGroup = React.forwardRef<
|
|||||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarGroup.displayName = "SidebarGroup"
|
SidebarGroup.displayName = "SidebarGroup";
|
||||||
|
|
||||||
const SidebarGroupLabel = React.forwardRef<
|
const SidebarGroupLabel = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> & { asChild?: boolean }
|
React.ComponentProps<"div"> & { asChild?: boolean }
|
||||||
>(({ className, asChild = false, ...props }, ref) => {
|
>(({ className, asChild = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "div"
|
const Comp = asChild ? Slot : "div";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
@@ -453,15 +440,15 @@ const SidebarGroupLabel = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarGroupLabel.displayName = "SidebarGroupLabel"
|
SidebarGroupLabel.displayName = "SidebarGroupLabel";
|
||||||
|
|
||||||
const SidebarGroupAction = React.forwardRef<
|
const SidebarGroupAction = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
React.ComponentProps<"button"> & { asChild?: boolean }
|
React.ComponentProps<"button"> & { asChild?: boolean }
|
||||||
>(({ className, asChild = false, ...props }, ref) => {
|
>(({ className, asChild = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
@@ -476,9 +463,9 @@ const SidebarGroupAction = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarGroupAction.displayName = "SidebarGroupAction"
|
SidebarGroupAction.displayName = "SidebarGroupAction";
|
||||||
|
|
||||||
const SidebarGroupContent = React.forwardRef<
|
const SidebarGroupContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@@ -490,8 +477,8 @@ const SidebarGroupContent = React.forwardRef<
|
|||||||
className={cn("w-full text-sm", className)}
|
className={cn("w-full text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SidebarGroupContent.displayName = "SidebarGroupContent"
|
SidebarGroupContent.displayName = "SidebarGroupContent";
|
||||||
|
|
||||||
const SidebarMenu = React.forwardRef<
|
const SidebarMenu = React.forwardRef<
|
||||||
HTMLUListElement,
|
HTMLUListElement,
|
||||||
@@ -503,8 +490,8 @@ const SidebarMenu = React.forwardRef<
|
|||||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SidebarMenu.displayName = "SidebarMenu"
|
SidebarMenu.displayName = "SidebarMenu";
|
||||||
|
|
||||||
const SidebarMenuItem = React.forwardRef<
|
const SidebarMenuItem = React.forwardRef<
|
||||||
HTMLLIElement,
|
HTMLLIElement,
|
||||||
@@ -516,8 +503,8 @@ const SidebarMenuItem = React.forwardRef<
|
|||||||
className={cn("group/menu-item relative", className)}
|
className={cn("group/menu-item relative", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SidebarMenuItem.displayName = "SidebarMenuItem"
|
SidebarMenuItem.displayName = "SidebarMenuItem";
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
const sidebarMenuButtonVariants = cva(
|
||||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
@@ -539,14 +526,14 @@ const sidebarMenuButtonVariants = cva(
|
|||||||
size: "default",
|
size: "default",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
const SidebarMenuButton = React.forwardRef<
|
const SidebarMenuButton = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
React.ComponentProps<"button"> & {
|
React.ComponentProps<"button"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
isActive?: boolean
|
isActive?: boolean;
|
||||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
@@ -561,8 +548,8 @@ const SidebarMenuButton = React.forwardRef<
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button";
|
||||||
const { isMobile, state } = useSidebar()
|
const { isMobile, state } = useSidebar();
|
||||||
|
|
||||||
const button = (
|
const button = (
|
||||||
<Comp
|
<Comp
|
||||||
@@ -573,16 +560,16 @@ const SidebarMenuButton = React.forwardRef<
|
|||||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
|
|
||||||
if (!tooltip) {
|
if (!tooltip) {
|
||||||
return button
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof tooltip === "string") {
|
if (typeof tooltip === "string") {
|
||||||
tooltip = {
|
tooltip = {
|
||||||
children: tooltip,
|
children: tooltip,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -595,19 +582,19 @@ const SidebarMenuButton = React.forwardRef<
|
|||||||
{...tooltip}
|
{...tooltip}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
SidebarMenuButton.displayName = "SidebarMenuButton"
|
SidebarMenuButton.displayName = "SidebarMenuButton";
|
||||||
|
|
||||||
const SidebarMenuAction = React.forwardRef<
|
const SidebarMenuAction = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
React.ComponentProps<"button"> & {
|
React.ComponentProps<"button"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
showOnHover?: boolean
|
showOnHover?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
@@ -627,9 +614,9 @@ const SidebarMenuAction = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarMenuAction.displayName = "SidebarMenuAction"
|
SidebarMenuAction.displayName = "SidebarMenuAction";
|
||||||
|
|
||||||
const SidebarMenuBadge = React.forwardRef<
|
const SidebarMenuBadge = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@@ -649,19 +636,19 @@ const SidebarMenuBadge = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SidebarMenuBadge.displayName = "SidebarMenuBadge"
|
SidebarMenuBadge.displayName = "SidebarMenuBadge";
|
||||||
|
|
||||||
const SidebarMenuSkeleton = React.forwardRef<
|
const SidebarMenuSkeleton = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> & {
|
React.ComponentProps<"div"> & {
|
||||||
showIcon?: boolean
|
showIcon?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, showIcon = false, ...props }, ref) => {
|
>(({ className, showIcon = false, ...props }, ref) => {
|
||||||
// Random width between 50 to 90%.
|
// Random width between 50 to 90%.
|
||||||
const width = React.useMemo(() => {
|
const width = React.useMemo(() => {
|
||||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -686,9 +673,9 @@ const SidebarMenuSkeleton = React.forwardRef<
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
|
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
|
||||||
|
|
||||||
const SidebarMenuSub = React.forwardRef<
|
const SidebarMenuSub = React.forwardRef<
|
||||||
HTMLUListElement,
|
HTMLUListElement,
|
||||||
@@ -704,24 +691,24 @@ const SidebarMenuSub = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SidebarMenuSub.displayName = "SidebarMenuSub"
|
SidebarMenuSub.displayName = "SidebarMenuSub";
|
||||||
|
|
||||||
const SidebarMenuSubItem = React.forwardRef<
|
const SidebarMenuSubItem = React.forwardRef<
|
||||||
HTMLLIElement,
|
HTMLLIElement,
|
||||||
React.ComponentProps<"li">
|
React.ComponentProps<"li">
|
||||||
>(({ ...props }, ref) => <li ref={ref} {...props} />)
|
>(({ ...props }, ref) => <li ref={ref} {...props} />);
|
||||||
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
|
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
|
||||||
|
|
||||||
const SidebarMenuSubButton = React.forwardRef<
|
const SidebarMenuSubButton = React.forwardRef<
|
||||||
HTMLAnchorElement,
|
HTMLAnchorElement,
|
||||||
React.ComponentProps<"a"> & {
|
React.ComponentProps<"a"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
size?: "sm" | "md"
|
size?: "sm" | "md";
|
||||||
isActive?: boolean
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "a"
|
const Comp = asChild ? Slot : "a";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
@@ -739,9 +726,9 @@ const SidebarMenuSubButton = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
|
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
@@ -768,4 +755,4 @@ export {
|
|||||||
SidebarSeparator,
|
SidebarSeparator,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,33 +1,37 @@
|
|||||||
|
import AppLayout from "@/components/layout/app-layout";
|
||||||
|
import LoadingScreen from "@/components/ui/LoadingScreen";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Suspense } from "react";
|
||||||
import { Redirect, Route } from "wouter";
|
import { Redirect, Route } from "wouter";
|
||||||
|
|
||||||
|
type ComponentLike = React.ComponentType; // works for both lazy() and regular components
|
||||||
|
|
||||||
export function ProtectedRoute({
|
export function ProtectedRoute({
|
||||||
path,
|
path,
|
||||||
component: Component,
|
component: Component,
|
||||||
}: {
|
}: {
|
||||||
path: string;
|
path: string;
|
||||||
component: () => React.JSX.Element;
|
component: ComponentLike;
|
||||||
}) {
|
}) {
|
||||||
const { user, isLoading } = useAuth();
|
const { user, isLoading } = useAuth();
|
||||||
|
|
||||||
if (isLoading) {
|
return (
|
||||||
return (
|
<Route path={path}>
|
||||||
<Route path={path}>
|
{/* While auth is resolving: keep layout visible and show a small spinner in the content area */}
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
{isLoading ? (
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-border" />
|
<AppLayout>
|
||||||
</div>
|
<LoadingScreen />
|
||||||
</Route>
|
</AppLayout>
|
||||||
);
|
) : !user ? (
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return (
|
|
||||||
<Route path={path}>
|
|
||||||
<Redirect to="/auth" />
|
<Redirect to="/auth" />
|
||||||
</Route>
|
) : (
|
||||||
);
|
// Authenticated: render page inside layout. Lazy pages load with an in-layout spinner.
|
||||||
}
|
<AppLayout>
|
||||||
|
<Suspense fallback={<LoadingScreen />}>
|
||||||
return <Route path={path} component={Component} />;
|
<Component />
|
||||||
|
</Suspense>
|
||||||
|
</AppLayout>
|
||||||
|
)}
|
||||||
|
</Route>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { useState, useEffect } from "react";
|
|||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { format, addDays, startOfToday, addMinutes } from "date-fns";
|
import { format, addDays, startOfToday, addMinutes } from "date-fns";
|
||||||
import { parseLocalDate, formatLocalDate } from "@/utils/dateUtils";
|
import { parseLocalDate, formatLocalDate } from "@/utils/dateUtils";
|
||||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
|
||||||
import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal";
|
import { AddAppointmentModal } from "@/components/appointments/add-appointment-modal";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -625,231 +623,212 @@ export default function AppointmentsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
<div className="">
|
||||||
<Sidebar
|
<div className="container mx-auto">
|
||||||
isMobileOpen={isMobileMenuOpen}
|
<div className="flex justify-between items-center mb-6">
|
||||||
setIsMobileOpen={setIsMobileMenuOpen}
|
<div>
|
||||||
/>
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
|
Appointment Schedule
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
View and manage the dental practice schedule
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setEditingAppointment(undefined);
|
||||||
|
setIsAddModalOpen(true);
|
||||||
|
}}
|
||||||
|
className="gap-1"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
New Appointment
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
{/* Context Menu */}
|
||||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
<Menu id={APPOINTMENT_CONTEXT_MENU_ID} animation="fade">
|
||||||
|
<Item
|
||||||
|
onClick={({ props }) => {
|
||||||
|
const fullAppointment = appointments.find(
|
||||||
|
(a) => a.id === props.appointmentId
|
||||||
|
);
|
||||||
|
if (fullAppointment) {
|
||||||
|
handleEditAppointment(fullAppointment);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<CalendarIcon className="h-4 w-4" />
|
||||||
|
Edit Appointment
|
||||||
|
</span>
|
||||||
|
</Item>
|
||||||
|
<Item
|
||||||
|
onClick={({ props }) =>
|
||||||
|
handleDeleteAppointment(props.appointmentId)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2 text-red-600">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
Delete Appointment
|
||||||
|
</span>
|
||||||
|
</Item>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto p-4">
|
{/* Main Content - Split into Schedule and Calendar */}
|
||||||
<div className="container mx-auto">
|
<div className="flex flex-col lg:flex-row gap-6">
|
||||||
<div className="flex justify-between items-center mb-6">
|
{/* Left side - Schedule Grid */}
|
||||||
<div>
|
<div className="w-full lg:w-3/4 overflow-x-auto bg-white rounded-md shadow">
|
||||||
<h1 className="text-3xl font-bold tracking-tight">
|
<div className="p-4 border-b">
|
||||||
Appointment Schedule
|
<div className="flex items-center justify-between">
|
||||||
</h1>
|
<div className="flex items-center space-x-2">
|
||||||
<p className="text-muted-foreground">
|
<Button
|
||||||
View and manage the dental practice schedule
|
variant="outline"
|
||||||
</p>
|
size="icon"
|
||||||
|
onClick={() => setSelectedDate(addDays(selectedDate, -1))}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<h2 className="text-xl font-semibold">{formattedDate}</h2>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedDate(addDays(selectedDate, 1))}
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setEditingAppointment(undefined);
|
|
||||||
setIsAddModalOpen(true);
|
|
||||||
}}
|
|
||||||
className="gap-1"
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
New Appointment
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Context Menu */}
|
{/* Schedule Grid with Drag and Drop */}
|
||||||
<Menu id={APPOINTMENT_CONTEXT_MENU_ID} animation="fade">
|
<DndProvider backend={HTML5Backend}>
|
||||||
<Item
|
<div className="overflow-x-auto">
|
||||||
onClick={({ props }) => {
|
<table className="w-full border-collapse min-w-[800px]">
|
||||||
const fullAppointment = appointments.find(
|
<thead>
|
||||||
(a) => a.id === props.appointmentId
|
<tr>
|
||||||
);
|
<th className="p-2 border bg-gray-50 w-[100px]">Time</th>
|
||||||
if (fullAppointment) {
|
{staffMembers.map((staff) => (
|
||||||
handleEditAppointment(fullAppointment);
|
<th
|
||||||
}
|
key={staff.id}
|
||||||
}}
|
className={`p-2 border bg-gray-50 ${staff.role === "doctor" ? "font-bold" : ""}`}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
{staff.name}
|
||||||
<CalendarIcon className="h-4 w-4" />
|
<div className="text-xs text-gray-500">
|
||||||
Edit Appointment
|
{staff.role}
|
||||||
</span>
|
</div>
|
||||||
</Item>
|
</th>
|
||||||
<Item
|
))}
|
||||||
onClick={({ props }) =>
|
</tr>
|
||||||
handleDeleteAppointment(props.appointmentId)
|
</thead>
|
||||||
}
|
<tbody>
|
||||||
>
|
{timeSlots.map((timeSlot) => (
|
||||||
<span className="flex items-center gap-2 text-red-600">
|
<tr key={timeSlot.time}>
|
||||||
<Trash2 className="h-4 w-4" />
|
<td className="border px-2 py-1 text-xs text-gray-600 font-medium">
|
||||||
Delete Appointment
|
{timeSlot.displayTime}
|
||||||
</span>
|
</td>
|
||||||
</Item>
|
{staffMembers.map((staff) => (
|
||||||
</Menu>
|
<DroppableTimeSlot
|
||||||
|
key={`${timeSlot.time}-${staff.id}`}
|
||||||
|
timeSlot={timeSlot}
|
||||||
|
staffId={Number(staff.id)}
|
||||||
|
appointment={getAppointmentAtSlot(
|
||||||
|
timeSlot,
|
||||||
|
Number(staff.id)
|
||||||
|
)}
|
||||||
|
staff={staff}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</DndProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Main Content - Split into Schedule and Calendar */}
|
{/* Right side - Calendar and Stats */}
|
||||||
<div className="flex flex-col lg:flex-row gap-6">
|
<div className="w-full lg:w-1/4 space-y-6">
|
||||||
{/* Left side - Schedule Grid */}
|
{/* Calendar Card */}
|
||||||
<div className="w-full lg:w-3/4 overflow-x-auto bg-white rounded-md shadow">
|
<Card>
|
||||||
<div className="p-4 border-b">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex items-center justify-between">
|
<CardTitle>Calendar</CardTitle>
|
||||||
<div className="flex items-center space-x-2">
|
<CardDescription>
|
||||||
<Button
|
Select a date to view or schedule appointments
|
||||||
variant="outline"
|
</CardDescription>
|
||||||
size="icon"
|
</CardHeader>
|
||||||
onClick={() =>
|
<CardContent>
|
||||||
setSelectedDate(addDays(selectedDate, -1))
|
<Calendar
|
||||||
}
|
mode="single"
|
||||||
>
|
selected={selectedDate}
|
||||||
<ChevronLeft className="h-4 w-4" />
|
onSelect={(date) => {
|
||||||
</Button>
|
if (date) setSelectedDate(date);
|
||||||
<h2 className="text-xl font-semibold">{formattedDate}</h2>
|
}}
|
||||||
<Button
|
/>
|
||||||
variant="outline"
|
</CardContent>
|
||||||
size="icon"
|
</Card>
|
||||||
onClick={() =>
|
|
||||||
setSelectedDate(addDays(selectedDate, 1))
|
{/* Statistics Card */}
|
||||||
}
|
<Card>
|
||||||
>
|
<CardHeader className="pb-2">
|
||||||
<ChevronRight className="h-4 w-4" />
|
<CardTitle className="flex items-center justify-between">
|
||||||
</Button>
|
<span>Appointments</span>
|
||||||
</div>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => refetchAppointments()}
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Statistics for {formattedDate}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
Total appointments:
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold">
|
||||||
|
{selectedDateAppointments.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-500">With doctors:</span>
|
||||||
|
<span className="font-semibold">
|
||||||
|
{
|
||||||
|
processedAppointments.filter(
|
||||||
|
(apt) =>
|
||||||
|
staffMembers.find(
|
||||||
|
(s) => Number(s.id) === apt.staffId
|
||||||
|
)?.role === "doctor"
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
With hygienists:
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold">
|
||||||
|
{
|
||||||
|
processedAppointments.filter(
|
||||||
|
(apt) =>
|
||||||
|
staffMembers.find(
|
||||||
|
(s) => Number(s.id) === apt.staffId
|
||||||
|
)?.role === "hygienist"
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</CardContent>
|
||||||
{/* Schedule Grid with Drag and Drop */}
|
</Card>
|
||||||
<DndProvider backend={HTML5Backend}>
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full border-collapse min-w-[800px]">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="p-2 border bg-gray-50 w-[100px]">
|
|
||||||
Time
|
|
||||||
</th>
|
|
||||||
{staffMembers.map((staff) => (
|
|
||||||
<th
|
|
||||||
key={staff.id}
|
|
||||||
className={`p-2 border bg-gray-50 ${staff.role === "doctor" ? "font-bold" : ""}`}
|
|
||||||
>
|
|
||||||
{staff.name}
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
{staff.role}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{timeSlots.map((timeSlot) => (
|
|
||||||
<tr key={timeSlot.time}>
|
|
||||||
<td className="border px-2 py-1 text-xs text-gray-600 font-medium">
|
|
||||||
{timeSlot.displayTime}
|
|
||||||
</td>
|
|
||||||
{staffMembers.map((staff) => (
|
|
||||||
<DroppableTimeSlot
|
|
||||||
key={`${timeSlot.time}-${staff.id}`}
|
|
||||||
timeSlot={timeSlot}
|
|
||||||
staffId={Number(staff.id)}
|
|
||||||
appointment={getAppointmentAtSlot(
|
|
||||||
timeSlot,
|
|
||||||
Number(staff.id)
|
|
||||||
)}
|
|
||||||
staff={staff}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</DndProvider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right side - Calendar and Stats */}
|
|
||||||
<div className="w-full lg:w-1/4 space-y-6">
|
|
||||||
{/* Calendar Card */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle>Calendar</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Select a date to view or schedule appointments
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Calendar
|
|
||||||
mode="single"
|
|
||||||
selected={selectedDate}
|
|
||||||
onSelect={(date) => {
|
|
||||||
if (date) setSelectedDate(date);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Statistics Card */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="flex items-center justify-between">
|
|
||||||
<span>Appointments</span>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => refetchAppointments()}
|
|
||||||
>
|
|
||||||
<RefreshCw className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Statistics for {formattedDate}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm text-gray-500">
|
|
||||||
Total appointments:
|
|
||||||
</span>
|
|
||||||
<span className="font-semibold">
|
|
||||||
{selectedDateAppointments.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm text-gray-500">
|
|
||||||
With doctors:
|
|
||||||
</span>
|
|
||||||
<span className="font-semibold">
|
|
||||||
{
|
|
||||||
processedAppointments.filter(
|
|
||||||
(apt) =>
|
|
||||||
staffMembers.find(
|
|
||||||
(s) => Number(s.id) === apt.staffId
|
|
||||||
)?.role === "doctor"
|
|
||||||
).length
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm text-gray-500">
|
|
||||||
With hygienists:
|
|
||||||
</span>
|
|
||||||
<span className="font-semibold">
|
|
||||||
{
|
|
||||||
processedAppointments.filter(
|
|
||||||
(apt) =>
|
|
||||||
staffMembers.find(
|
|
||||||
(s) => Number(s.id) === apt.staffId
|
|
||||||
)?.role === "hygienist"
|
|
||||||
).length
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add/Edit Appointment Modal */}
|
{/* Add/Edit Appointment Modal */}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
@@ -410,58 +408,47 @@ export default function ClaimsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
<div>
|
||||||
<Sidebar
|
<SeleniumTaskBanner
|
||||||
isMobileOpen={isMobileMenuOpen}
|
status={status}
|
||||||
setIsMobileOpen={setIsMobileMenuOpen}
|
message={message}
|
||||||
|
show={show}
|
||||||
|
onClear={() => dispatch(clearTaskStatus())}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="container mx-auto space-y-6">
|
||||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
<SeleniumTaskBanner
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
status={status}
|
Insurance Claims
|
||||||
message={message}
|
</h1>
|
||||||
show={show}
|
<p className="text-muted-foreground">
|
||||||
onClear={() => dispatch(clearTaskStatus())}
|
Manage and submit insurance claims for patients
|
||||||
/>
|
</p>
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto p-4">
|
|
||||||
<div className="container mx-auto space-y-6">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">
|
|
||||||
Insurance Claims
|
|
||||||
</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Manage and submit insurance claims for patients
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Recent Claims by Patients also handles new claims */}
|
|
||||||
<ClaimsOfPatientModal onNewClaim={handleNewClaim} />
|
|
||||||
|
|
||||||
{/* Recent Claims Section */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Recently Submitted Claims</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
View and manage all recent claims information
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ClaimsRecentTable
|
|
||||||
allowEdit={true}
|
|
||||||
allowView={true}
|
|
||||||
allowDelete={true}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Claims by Patients also handles new claims */}
|
||||||
|
<ClaimsOfPatientModal onNewClaim={handleNewClaim} />
|
||||||
|
|
||||||
|
{/* Recent Claims Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Recently Submitted Claims</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
View and manage all recent claims information
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ClaimsRecentTable
|
||||||
|
allowEdit={true}
|
||||||
|
allowView={true}
|
||||||
|
allowDelete={true}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Claim Form Modal */}
|
{/* Claim Form Modal */}
|
||||||
{isClaimFormOpen && selectedPatientId !== null && (
|
{isClaimFormOpen && selectedPatientId !== null && (
|
||||||
<ClaimForm
|
<ClaimForm
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { format, parse, isValid, parseISO } from "date-fns";
|
import { format, parse, isValid, parseISO } from "date-fns";
|
||||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
|
||||||
import { StatCard } from "@/components/ui/stat-card";
|
import { StatCard } from "@/components/ui/stat-card";
|
||||||
import { PatientTable } from "@/components/patients/patient-table";
|
import { PatientTable } from "@/components/patients/patient-table";
|
||||||
import { AddPatientModal } from "@/components/patients/add-patient-modal";
|
import { AddPatientModal } from "@/components/patients/add-patient-modal";
|
||||||
@@ -14,8 +12,6 @@ import { useAuth } from "@/hooks/use-auth";
|
|||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { AppointmentsByDay } from "@/components/analytics/appointments-by-day";
|
import { AppointmentsByDay } from "@/components/analytics/appointments-by-day";
|
||||||
import { NewPatients } from "@/components/analytics/new-patients";
|
import { NewPatients } from "@/components/analytics/new-patients";
|
||||||
import { AppointmentUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
|
||||||
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
|
||||||
import {
|
import {
|
||||||
Users,
|
Users,
|
||||||
Calendar,
|
Calendar,
|
||||||
@@ -25,7 +21,6 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
import { z } from "zod";
|
|
||||||
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
|
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
|
||||||
import {
|
import {
|
||||||
Appointment,
|
Appointment,
|
||||||
@@ -220,116 +215,107 @@ export default function Dashboard() {
|
|||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
<div>
|
||||||
<Sidebar
|
{/* Quick Stats */}
|
||||||
isMobileOpen={isMobileMenuOpen}
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||||
setIsMobileOpen={setIsMobileMenuOpen}
|
<StatCard
|
||||||
/>
|
title="Total Patients"
|
||||||
|
value={patients.length}
|
||||||
|
icon={Users}
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Today's Appointments"
|
||||||
|
value={todaysAppointments.length}
|
||||||
|
icon={Calendar}
|
||||||
|
color="secondary"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Completed Today"
|
||||||
|
value={completedTodayCount}
|
||||||
|
icon={CheckCircle}
|
||||||
|
color="success"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Pending Payments"
|
||||||
|
value={0}
|
||||||
|
icon={CreditCard}
|
||||||
|
color="warning"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
{/* Today's Appointments Section */}
|
||||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
<div className="flex flex-col space-y-4 mb-6">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||||
|
<h2 className="text-xl font-medium text-gray-800">
|
||||||
|
Today's Appointments
|
||||||
|
</h2>
|
||||||
|
<Button
|
||||||
|
className="mt-2 md:mt-0"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedAppointment(undefined);
|
||||||
|
setIsAddAppointmentOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
New Appointment
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto p-4">
|
<Card>
|
||||||
{/* Quick Stats */}
|
<CardContent className="p-0">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
{todaysAppointments.length > 0 ? (
|
||||||
<StatCard
|
<div className="divide-y">
|
||||||
title="Total Patients"
|
{todaysAppointments.map((appointment) => {
|
||||||
value={patients.length}
|
const patient = patients.find(
|
||||||
icon={Users}
|
(p) => p.id === appointment.patientId
|
||||||
color="blue"
|
);
|
||||||
/>
|
return (
|
||||||
<StatCard
|
<div
|
||||||
title="Today's Appointments"
|
key={appointment.id}
|
||||||
value={todaysAppointments.length}
|
className="p-4 flex items-center justify-between"
|
||||||
icon={Calendar}
|
>
|
||||||
color="secondary"
|
<div className="flex items-center space-x-4">
|
||||||
/>
|
<div className="h-10 w-10 rounded-full bg-opacity-10 text-primary flex items-center justify-center">
|
||||||
<StatCard
|
<Clock className="h-5 w-5" />
|
||||||
title="Completed Today"
|
</div>
|
||||||
value={completedTodayCount}
|
<div>
|
||||||
icon={CheckCircle}
|
<h3 className="font-medium">
|
||||||
color="success"
|
{patient
|
||||||
/>
|
? `${patient.firstName} ${patient.lastName}`
|
||||||
<StatCard
|
: "Unknown Patient"}
|
||||||
title="Pending Payments"
|
</h3>
|
||||||
value={0}
|
<div className="text-sm text-gray-500 flex items-center space-x-2">
|
||||||
icon={CreditCard}
|
<span>
|
||||||
color="warning"
|
<span>
|
||||||
/>
|
{`${format(
|
||||||
</div>
|
parse(
|
||||||
|
`${format(new Date(appointment.date), "yyyy-MM-dd")} ${appointment.startTime}`,
|
||||||
{/* Today's Appointments Section */}
|
"yyyy-MM-dd HH:mm",
|
||||||
<div className="flex flex-col space-y-4 mb-6">
|
new Date()
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
|
),
|
||||||
<h2 className="text-xl font-medium text-gray-800">
|
"hh:mm a"
|
||||||
Today's Appointments
|
)} - ${format(
|
||||||
</h2>
|
parse(
|
||||||
<Button
|
`${format(new Date(appointment.date), "yyyy-MM-dd")} ${appointment.endTime}`,
|
||||||
className="mt-2 md:mt-0"
|
"yyyy-MM-dd HH:mm",
|
||||||
onClick={() => {
|
new Date()
|
||||||
setSelectedAppointment(undefined);
|
),
|
||||||
setIsAddAppointmentOpen(true);
|
"hh:mm a"
|
||||||
}}
|
)}`}
|
||||||
>
|
</span>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
</span>
|
||||||
New Appointment
|
<span>•</span>
|
||||||
</Button>
|
<span>
|
||||||
</div>
|
{appointment.type.charAt(0).toUpperCase() +
|
||||||
|
appointment.type.slice(1)}
|
||||||
<Card>
|
</span>
|
||||||
<CardContent className="p-0">
|
|
||||||
{todaysAppointments.length > 0 ? (
|
|
||||||
<div className="divide-y">
|
|
||||||
{todaysAppointments.map((appointment) => {
|
|
||||||
const patient = patients.find(
|
|
||||||
(p) => p.id === appointment.patientId
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={appointment.id}
|
|
||||||
className="p-4 flex items-center justify-between"
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div className="h-10 w-10 rounded-full bg-opacity-10 text-primary flex items-center justify-center">
|
|
||||||
<Clock className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium">
|
|
||||||
{patient
|
|
||||||
? `${patient.firstName} ${patient.lastName}`
|
|
||||||
: "Unknown Patient"}
|
|
||||||
</h3>
|
|
||||||
<div className="text-sm text-gray-500 flex items-center space-x-2">
|
|
||||||
<span>
|
|
||||||
<span>
|
|
||||||
{`${format(
|
|
||||||
parse(
|
|
||||||
`${format(new Date(appointment.date), "yyyy-MM-dd")} ${appointment.startTime}`,
|
|
||||||
"yyyy-MM-dd HH:mm",
|
|
||||||
new Date()
|
|
||||||
),
|
|
||||||
"hh:mm a"
|
|
||||||
)} - ${format(
|
|
||||||
parse(
|
|
||||||
`${format(new Date(appointment.date), "yyyy-MM-dd")} ${appointment.endTime}`,
|
|
||||||
"yyyy-MM-dd HH:mm",
|
|
||||||
new Date()
|
|
||||||
),
|
|
||||||
"hh:mm a"
|
|
||||||
)}`}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>
|
|
||||||
{appointment.type.charAt(0).toUpperCase() +
|
|
||||||
appointment.type.slice(1)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
</div>
|
||||||
<span
|
</div>
|
||||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
<div className="flex items-center space-x-2">
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||||
${
|
${
|
||||||
appointment.status === "completed"
|
appointment.status === "completed"
|
||||||
? "bg-green-100 text-green-800"
|
? "bg-green-100 text-green-800"
|
||||||
@@ -339,81 +325,75 @@ export default function Dashboard() {
|
|||||||
? "bg-blue-100 text-blue-800"
|
? "bg-blue-100 text-blue-800"
|
||||||
: "bg-yellow-100 text-yellow-800"
|
: "bg-yellow-100 text-yellow-800"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{appointment.status
|
{appointment.status
|
||||||
? appointment.status.charAt(0).toUpperCase() +
|
? appointment.status.charAt(0).toUpperCase() +
|
||||||
appointment.status.slice(1)
|
appointment.status.slice(1)
|
||||||
: "Scheduled"}
|
: "Scheduled"}
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
to="/appointments"
|
to="/appointments"
|
||||||
className="text-primary hover:text-primary/80 text-sm"
|
className="text-primary hover:text-primary/80 text-sm"
|
||||||
>
|
>
|
||||||
View All
|
View All
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-6 text-center">
|
<div className="p-6 text-center">
|
||||||
<Calendar className="h-12 w-12 mx-auto text-gray-400 mb-2" />
|
<Calendar className="h-12 w-12 mx-auto text-gray-400 mb-2" />
|
||||||
<h3 className="text-lg font-medium text-gray-900">
|
<h3 className="text-lg font-medium text-gray-900">
|
||||||
No appointments today
|
No appointments today
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-1 text-gray-500">
|
<p className="mt-1 text-gray-500">
|
||||||
You don't have any appointments scheduled for today.
|
You don't have any appointments scheduled for today.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedAppointment(undefined);
|
setSelectedAppointment(undefined);
|
||||||
setIsAddAppointmentOpen(true);
|
setIsAddAppointmentOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Schedule an Appointment
|
Schedule an Appointment
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Analytics Dashboard Section */}
|
{/* Analytics Dashboard Section */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
<AppointmentsByDay appointments={appointments} />
|
<AppointmentsByDay appointments={appointments} />
|
||||||
<NewPatients patients={patients} />
|
<NewPatients patients={patients} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Patient Management Section */}
|
{/* Patient Management Section */}
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
{/* Patient Header */}
|
{/* Patient Header */}
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||||
<h2 className="text-xl font-medium text-gray-800">
|
<h2 className="text-xl font-medium text-gray-800">
|
||||||
Patient Management
|
Patient Management
|
||||||
</h2>
|
</h2>
|
||||||
<Button
|
<Button
|
||||||
className="mt-2 md:mt-0"
|
className="mt-2 md:mt-0"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentPatient(undefined);
|
setCurrentPatient(undefined);
|
||||||
setIsAddPatientOpen(true);
|
setIsAddPatientOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
Add Patient
|
Add Patient
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Patient Table */}
|
{/* Patient Table */}
|
||||||
<PatientTable
|
<PatientTable allowDelete={true} allowEdit={true} allowView={true} />
|
||||||
allowDelete={true}
|
|
||||||
allowEdit={true}
|
|
||||||
allowView={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add/Edit Patient Modal */}
|
{/* Add/Edit Patient Modal */}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@@ -76,128 +74,110 @@ export default function DatabaseManagementPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
<div>
|
||||||
<Sidebar
|
<div className="container mx-auto space-y-6">
|
||||||
isMobileOpen={isMobileMenuOpen}
|
{/* Page Header */}
|
||||||
setIsMobileOpen={setIsMobileMenuOpen}
|
<div className="bg-white rounded-lg shadow-sm p-6 border">
|
||||||
/>
|
<h1 className="text-2xl font-bold text-gray-900 flex items-center space-x-3">
|
||||||
|
<Database className="h-8 w-8 text-blue-600" />
|
||||||
|
<span>Database Management</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 mt-2">
|
||||||
|
Manage your dental practice database with backup, export
|
||||||
|
capabilities
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
{/* Database Backup Section */}
|
||||||
<TopAppBar
|
<Card>
|
||||||
toggleMobileMenu={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
<CardHeader>
|
||||||
/>
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<HardDrive className="h-5 w-5" />
|
||||||
|
<span>Database Backup</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Create a complete backup of your dental practice database
|
||||||
|
including patients, appointments, claims, and all related data.
|
||||||
|
</p>
|
||||||
|
|
||||||
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-100 p-6">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="max-w-6xl mx-auto space-y-6">
|
<Button
|
||||||
{/* Page Header */}
|
onClick={() => backupMutation.mutate()}
|
||||||
<div className="bg-white rounded-lg shadow-sm p-6 border">
|
disabled={backupMutation.isPending}
|
||||||
<h1 className="text-2xl font-bold text-gray-900 flex items-center space-x-3">
|
className="flex items-center space-x-2"
|
||||||
<Database className="h-8 w-8 text-blue-600" />
|
>
|
||||||
<span>Database Management</span>
|
{backupMutation.isPending ? (
|
||||||
</h1>
|
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||||
<p className="text-gray-600 mt-2">
|
|
||||||
Manage your dental practice database with backup, export
|
|
||||||
capabilities
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Database Backup Section */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center space-x-2">
|
|
||||||
<HardDrive className="h-5 w-5" />
|
|
||||||
<span>Database Backup</span>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<p className="text-gray-600">
|
|
||||||
Create a complete backup of your dental practice database
|
|
||||||
including patients, appointments, claims, and all related
|
|
||||||
data.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<Button
|
|
||||||
onClick={() => backupMutation.mutate()}
|
|
||||||
disabled={backupMutation.isPending}
|
|
||||||
className="flex items-center space-x-2"
|
|
||||||
>
|
|
||||||
{backupMutation.isPending ? (
|
|
||||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<FileArchive className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
<span>
|
|
||||||
{backupMutation.isPending
|
|
||||||
? "Creating Backup..."
|
|
||||||
: "Create Backup"}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
Last backup:{" "}
|
|
||||||
{dbStatus?.lastBackup
|
|
||||||
? formatDateToHumanReadable(dbStatus.lastBackup)
|
|
||||||
: "Never"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Database Status Section */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center space-x-2">
|
|
||||||
<Database className="h-5 w-5" />
|
|
||||||
<span>Database Status</span>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{isLoadingStatus ? (
|
|
||||||
<p className="text-gray-500">Loading status...</p>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<FileArchive className="h-4 w-4" />
|
||||||
<div className="p-4 bg-green-50 rounded-lg">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
|
||||||
<span className="font-medium text-green-800">
|
|
||||||
Status
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-green-600 mt-1">
|
|
||||||
{dbStatus?.connected ? "Connected" : "Disconnected"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-blue-50 rounded-lg">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Database className="h-4 w-4 text-blue-500" />
|
|
||||||
<span className="font-medium text-blue-800">Size</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-blue-600 mt-1">
|
|
||||||
{dbStatus?.size ?? "Unknown"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-purple-50 rounded-lg">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Cloud className="h-4 w-4 text-purple-500" />
|
|
||||||
<span className="font-medium text-purple-800">
|
|
||||||
Records
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-purple-600 mt-1">
|
|
||||||
{dbStatus?.patients
|
|
||||||
? `${dbStatus.patients} patients`
|
|
||||||
: "N/A"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
<span>
|
||||||
</Card>
|
{backupMutation.isPending
|
||||||
</div>
|
? "Creating Backup..."
|
||||||
</main>
|
: "Create Backup"}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Last backup:{" "}
|
||||||
|
{dbStatus?.lastBackup
|
||||||
|
? formatDateToHumanReadable(dbStatus.lastBackup)
|
||||||
|
: "Never"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Database Status Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<Database className="h-5 w-5" />
|
||||||
|
<span>Database Status</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoadingStatus ? (
|
||||||
|
<p className="text-gray-500">Loading status...</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="p-4 bg-green-50 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||||
|
<span className="font-medium text-green-800">Status</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-green-600 mt-1">
|
||||||
|
{dbStatus?.connected ? "Connected" : "Disconnected"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-blue-50 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Database className="h-4 w-4 text-blue-500" />
|
||||||
|
<span className="font-medium text-blue-800">Size</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-blue-600 mt-1">
|
||||||
|
{dbStatus?.size ?? "Unknown"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-purple-50 rounded-lg">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Cloud className="h-4 w-4 text-purple-500" />
|
||||||
|
<span className="font-medium text-purple-800">Records</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-purple-600 mt-1">
|
||||||
|
{dbStatus?.patients
|
||||||
|
? `${dbStatus.patients} patients`
|
||||||
|
: "N/A"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import { apiRequest, queryClient } from "@/lib/queryClient";
|
|||||||
import { Eye, Trash, Download, FolderOpen } from "lucide-react";
|
import { Eye, Trash, Download, FolderOpen } from "lucide-react";
|
||||||
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
|
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
|
||||||
import { PatientTable } from "@/components/patients/patient-table";
|
import { PatientTable } from "@/components/patients/patient-table";
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
|
||||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
|
||||||
import { Patient, PdfFile } from "@repo/db/types";
|
import { Patient, PdfFile } from "@repo/db/types";
|
||||||
|
|
||||||
export default function DocumentsPage() {
|
export default function DocumentsPage() {
|
||||||
@@ -117,165 +115,152 @@ export default function DocumentsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
<div>
|
||||||
<Sidebar
|
<div className="container mx-auto space-y-6">
|
||||||
isMobileOpen={isMobileMenuOpen}
|
<div className="flex justify-between items-center">
|
||||||
setIsMobileOpen={setIsMobileMenuOpen}
|
<div>
|
||||||
/>
|
<h1 className="text-3xl font-bold tracking-tight">Documents</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
View and manage recent uploaded claim PDFs
|
||||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
</p>
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto p-4">
|
|
||||||
<div className="container mx-auto space-y-6">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Documents</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
View and manage recent uploaded claim PDFs
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedPatient && (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>
|
|
||||||
Document Groups for {selectedPatient.firstName}{" "}
|
|
||||||
{selectedPatient.lastName}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>Select a group to view PDFs</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-2">
|
|
||||||
{groups.length === 0 ? (
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
No groups found for this patient.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
groups.map((group: any) => (
|
|
||||||
<Button
|
|
||||||
key={group.id}
|
|
||||||
variant={
|
|
||||||
group.id === selectedGroupId ? "default" : "outline"
|
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
setSelectedGroupId((prevId) =>
|
|
||||||
prevId === group.id ? null : group.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FolderOpen className="w-4 h-4 mr-2" />
|
|
||||||
Group - {group.title}
|
|
||||||
</Button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedGroupId && (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>PDFs in Group</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-2">
|
|
||||||
{groupPdfs.length === 0 ? (
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
No PDFs found in this group.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
groupPdfs.map((pdf: any) => (
|
|
||||||
<div
|
|
||||||
key={pdf.id}
|
|
||||||
className="flex justify-between items-center border p-2 rounded"
|
|
||||||
>
|
|
||||||
<span className="text-sm">{pdf.filename}</span>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleViewPdf(pdf.id)}
|
|
||||||
>
|
|
||||||
<Eye className="w-4 h-4 text-gray-600" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
handleDownloadPdf(pdf.id, pdf.filename)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Download className="w-4 h-4 text-blue-600" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentPdf(pdf);
|
|
||||||
setIsDeletePdfOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash className="w-4 h-4 text-red-600" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{fileBlobUrl && (
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex justify-between items-center">
|
|
||||||
<CardTitle>Viewing PDF</CardTitle>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="ml-auto text-red-600 border-red-500 hover:bg-red-100 hover:border-red-600"
|
|
||||||
onClick={() => {
|
|
||||||
setFileBlobUrl(null);
|
|
||||||
setSelectedPdfId(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
✕ Close
|
|
||||||
</Button>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<iframe
|
|
||||||
src={fileBlobUrl}
|
|
||||||
className="w-full h-[80vh] border rounded"
|
|
||||||
title="PDF Viewer"
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Patient Records</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Select a patient to view document groups
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<PatientTable
|
|
||||||
allowView
|
|
||||||
allowDelete
|
|
||||||
allowCheckbox
|
|
||||||
allowEdit
|
|
||||||
onSelectPatient={setSelectedPatient}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<DeleteConfirmationDialog
|
|
||||||
isOpen={isDeletePdfOpen}
|
|
||||||
onConfirm={handleConfirmDeletePdf}
|
|
||||||
onCancel={() => setIsDeletePdfOpen(false)}
|
|
||||||
entityName={`PDF #${currentPdf?.id}`}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
|
{selectedPatient && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
Document Groups for {selectedPatient.firstName}{" "}
|
||||||
|
{selectedPatient.lastName}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Select a group to view PDFs</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
{groups.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
No groups found for this patient.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
groups.map((group: any) => (
|
||||||
|
<Button
|
||||||
|
key={group.id}
|
||||||
|
variant={
|
||||||
|
group.id === selectedGroupId ? "default" : "outline"
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
setSelectedGroupId((prevId) =>
|
||||||
|
prevId === group.id ? null : group.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FolderOpen className="w-4 h-4 mr-2" />
|
||||||
|
Group - {group.title}
|
||||||
|
</Button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedGroupId && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>PDFs in Group</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
{groupPdfs.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
No PDFs found in this group.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
groupPdfs.map((pdf: any) => (
|
||||||
|
<div
|
||||||
|
key={pdf.id}
|
||||||
|
className="flex justify-between items-center border p-2 rounded"
|
||||||
|
>
|
||||||
|
<span className="text-sm">{pdf.filename}</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleViewPdf(pdf.id)}
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4 text-gray-600" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDownloadPdf(pdf.id, pdf.filename)}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 text-blue-600" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentPdf(pdf);
|
||||||
|
setIsDeletePdfOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash className="w-4 h-4 text-red-600" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fileBlobUrl && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex justify-between items-center">
|
||||||
|
<CardTitle>Viewing PDF</CardTitle>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="ml-auto text-red-600 border-red-500 hover:bg-red-100 hover:border-red-600"
|
||||||
|
onClick={() => {
|
||||||
|
setFileBlobUrl(null);
|
||||||
|
setSelectedPdfId(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✕ Close
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<iframe
|
||||||
|
src={fileBlobUrl}
|
||||||
|
className="w-full h-[80vh] border rounded"
|
||||||
|
title="PDF Viewer"
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Patient Records</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Select a patient to view document groups
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<PatientTable
|
||||||
|
allowView
|
||||||
|
allowDelete
|
||||||
|
allowCheckbox
|
||||||
|
allowEdit
|
||||||
|
onSelectPatient={setSelectedPatient}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<DeleteConfirmationDialog
|
||||||
|
isOpen={isDeletePdfOpen}
|
||||||
|
onConfirm={handleConfirmDeletePdf}
|
||||||
|
onCancel={() => setIsDeletePdfOpen(false)}
|
||||||
|
entityName={`PDF #${currentPdf?.id}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -210,125 +208,114 @@ export default function InsuranceEligibilityPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
<div>
|
||||||
<Sidebar
|
<SeleniumTaskBanner
|
||||||
isMobileOpen={isMobileMenuOpen}
|
status={status}
|
||||||
setIsMobileOpen={setIsMobileMenuOpen}
|
message={message}
|
||||||
|
show={show}
|
||||||
|
onClear={() => dispatch(clearTaskStatus())}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="container mx-auto space-y-6">
|
||||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
|
Insurance Eligibility
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Check insurance eligibility and view patient information
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SeleniumTaskBanner
|
{/* Insurance Eligibility Check Form */}
|
||||||
status={status}
|
<Card className="mb-6">
|
||||||
message={message}
|
<CardHeader>
|
||||||
show={show}
|
<CardTitle>Check Insurance Eligibility</CardTitle>
|
||||||
onClear={() => dispatch(clearTaskStatus())}
|
</CardHeader>
|
||||||
/>
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-4 md:grid-cols-4 gap-4 mb-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="memberId">Member ID</Label>
|
||||||
|
<Input
|
||||||
|
id="memberId"
|
||||||
|
placeholder="Enter member ID"
|
||||||
|
value={memberId}
|
||||||
|
onChange={(e) => setMemberId(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto p-4">
|
<div className="space-y-2">
|
||||||
<div className="container mx-auto space-y-6">
|
<DateInput
|
||||||
<div className="flex justify-between items-center">
|
label="Date of Birth"
|
||||||
<div>
|
value={dateOfBirth}
|
||||||
<h1 className="text-3xl font-bold tracking-tight">
|
onChange={setDateOfBirth}
|
||||||
Insurance Eligibility
|
disableFuture
|
||||||
</h1>
|
/>
|
||||||
<p className="text-muted-foreground">
|
</div>
|
||||||
Check insurance eligibility and view patient information
|
|
||||||
</p>
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="firstName">First Name</Label>
|
||||||
|
<Input
|
||||||
|
id="firstName"
|
||||||
|
placeholder="Enter first name"
|
||||||
|
value={firstName}
|
||||||
|
onChange={(e) => setFirstName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="lastName">Last Name</Label>
|
||||||
|
<Input
|
||||||
|
id="lastName"
|
||||||
|
placeholder="Enter last name"
|
||||||
|
value={lastName}
|
||||||
|
onChange={(e) => setLastName(e.target.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Insurance Eligibility Check Form */}
|
<div>
|
||||||
<Card className="mb-6">
|
<Button
|
||||||
<CardHeader>
|
onClick={() => handleMHButton()}
|
||||||
<CardTitle>Check Insurance Eligibility</CardTitle>
|
className="w-full"
|
||||||
</CardHeader>
|
disabled={isCheckingEligibility}
|
||||||
<CardContent>
|
>
|
||||||
<div className="grid grid-cols-4 md:grid-cols-4 gap-4 mb-4">
|
{isCheckingEligibility ? (
|
||||||
<div className="space-y-2">
|
<>
|
||||||
<Label htmlFor="memberId">Member ID</Label>
|
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||||
<Input
|
Processing...
|
||||||
id="memberId"
|
</>
|
||||||
placeholder="Enter member ID"
|
) : (
|
||||||
value={memberId}
|
<>
|
||||||
onChange={(e) => setMemberId(e.target.value)}
|
<CheckCircle className="h-4 w-4 mr-2" />
|
||||||
/>
|
MH
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div className="space-y-2">
|
{/* Patients Table */}
|
||||||
<DateInput
|
<Card>
|
||||||
label="Date of Birth"
|
<CardHeader>
|
||||||
value={dateOfBirth}
|
<CardTitle>Patient Records</CardTitle>
|
||||||
onChange={setDateOfBirth}
|
<CardDescription>
|
||||||
disableFuture
|
Select Patients and Check Their Eligibility
|
||||||
/>
|
</CardDescription>
|
||||||
</div>
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
<div className="space-y-2">
|
<PatientTable
|
||||||
<Label htmlFor="firstName">First Name</Label>
|
allowView={true}
|
||||||
<Input
|
allowDelete={true}
|
||||||
id="firstName"
|
allowCheckbox={true}
|
||||||
placeholder="Enter first name"
|
allowEdit={true}
|
||||||
value={firstName}
|
onSelectPatient={setSelectedPatient}
|
||||||
onChange={(e) => setFirstName(e.target.value)}
|
onPageChange={setCurrentTablePage}
|
||||||
/>
|
onSearchChange={setCurrentTableSearchTerm}
|
||||||
</div>
|
/>
|
||||||
<div className="space-y-2">
|
</CardContent>
|
||||||
<Label htmlFor="lastName">Last Name</Label>
|
</Card>
|
||||||
<Input
|
|
||||||
id="lastName"
|
|
||||||
placeholder="Enter last name"
|
|
||||||
value={lastName}
|
|
||||||
onChange={(e) => setLastName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
onClick={() => handleMHButton()}
|
|
||||||
className="w-full"
|
|
||||||
disabled={isCheckingEligibility}
|
|
||||||
>
|
|
||||||
{isCheckingEligibility ? (
|
|
||||||
<>
|
|
||||||
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
|
||||||
Processing...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CheckCircle className="h-4 w-4 mr-2" />
|
|
||||||
MH
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Patients Table */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Patient Records</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Select Patients and Check Their Eligibility
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<PatientTable
|
|
||||||
allowView={true}
|
|
||||||
allowDelete={true}
|
|
||||||
allowCheckbox={true}
|
|
||||||
allowEdit={true}
|
|
||||||
onSelectPatient={setSelectedPatient}
|
|
||||||
onPageChange={setCurrentTablePage}
|
|
||||||
onSearchChange={setCurrentTableSearchTerm}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
|
||||||
import { PatientTable } from "@/components/patients/patient-table";
|
import { PatientTable } from "@/components/patients/patient-table";
|
||||||
import { AddPatientModal } from "@/components/patients/add-patient-modal";
|
import { AddPatientModal } from "@/components/patients/add-patient-modal";
|
||||||
import { FileUploadZone } from "@/components/file-upload/file-upload-zone";
|
import { FileUploadZone } from "@/components/file-upload/file-upload-zone";
|
||||||
@@ -137,107 +135,96 @@ export default function PatientsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
<div>
|
||||||
<Sidebar
|
<div className="container mx-auto space-y-6">
|
||||||
isMobileOpen={isMobileMenuOpen}
|
<div className="flex justify-between items-center">
|
||||||
setIsMobileOpen={setIsMobileMenuOpen}
|
<div>
|
||||||
/>
|
<h1 className="text-3xl font-bold tracking-tight">Patients</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Manage patient records and information
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentPatient(undefined);
|
||||||
|
setIsAddPatientOpen(true);
|
||||||
|
}}
|
||||||
|
className="gap-1"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
New Patient
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
{/* File Upload Zone */}
|
||||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
<div className="grid gap-4 md:grid-cols-4">
|
||||||
|
<div className="md:col-span-3">
|
||||||
<main className="flex-1 overflow-y-auto p-4">
|
|
||||||
<div className="container mx-auto space-y-6">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Patients</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Manage patient records and information
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentPatient(undefined);
|
|
||||||
setIsAddPatientOpen(true);
|
|
||||||
}}
|
|
||||||
className="gap-1"
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
New Patient
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* File Upload Zone */}
|
|
||||||
<div className="grid gap-4 md:grid-cols-4">
|
|
||||||
<div className="md:col-span-3">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
Upload Patient Document
|
|
||||||
</CardTitle>
|
|
||||||
<File className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<FileUploadZone
|
|
||||||
onFileUpload={handleFileUpload}
|
|
||||||
isUploading={isUploading}
|
|
||||||
acceptedFileTypes="application/pdf"
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div className="md:col-span-1 flex items-end">
|
|
||||||
<Button
|
|
||||||
className="w-full h-12 gap-2"
|
|
||||||
disabled={!uploadedFile || isExtracting}
|
|
||||||
onClick={handleExtract}
|
|
||||||
>
|
|
||||||
{isExtracting ? (
|
|
||||||
<>
|
|
||||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
|
||||||
Processing...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FilePlus className="h-4 w-4" />
|
|
||||||
Extract Info And Claim
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Patients Table */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle>Patient Records</CardTitle>
|
<CardTitle className="text-sm font-medium">
|
||||||
<CardDescription>
|
Upload Patient Document
|
||||||
View and manage all patient information
|
</CardTitle>
|
||||||
</CardDescription>
|
<File className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<PatientTable
|
<FileUploadZone
|
||||||
allowDelete={true}
|
onFileUpload={handleFileUpload}
|
||||||
allowEdit={true}
|
isUploading={isUploading}
|
||||||
allowView={true}
|
acceptedFileTypes="application/pdf"
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Add/Edit Patient Modal */}
|
|
||||||
<AddPatientModal
|
|
||||||
ref={addPatientModalRef}
|
|
||||||
open={isAddPatientOpen}
|
|
||||||
onOpenChange={setIsAddPatientOpen}
|
|
||||||
onSubmit={handleAddPatient}
|
|
||||||
isLoading={isLoading}
|
|
||||||
patient={currentPatient}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
<div className="md:col-span-1 flex items-end">
|
||||||
|
<Button
|
||||||
|
className="w-full h-12 gap-2"
|
||||||
|
disabled={!uploadedFile || isExtracting}
|
||||||
|
onClick={handleExtract}
|
||||||
|
>
|
||||||
|
{isExtracting ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||||
|
Processing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FilePlus className="h-4 w-4" />
|
||||||
|
Extract Info And Claim
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Patients Table */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Patient Records</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
View and manage all patient information
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<PatientTable
|
||||||
|
allowDelete={true}
|
||||||
|
allowEdit={true}
|
||||||
|
allowView={true}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Add/Edit Patient Modal */}
|
||||||
|
<AddPatientModal
|
||||||
|
ref={addPatientModalRef}
|
||||||
|
open={isAddPatientOpen}
|
||||||
|
onOpenChange={setIsAddPatientOpen}
|
||||||
|
onSubmit={handleAddPatient}
|
||||||
|
isLoading={isLoading}
|
||||||
|
patient={currentPatient}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
@@ -10,12 +8,7 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import {
|
import { DollarSign, Upload, Image, X } from "lucide-react";
|
||||||
DollarSign,
|
|
||||||
Upload,
|
|
||||||
Image,
|
|
||||||
X,
|
|
||||||
} from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -116,228 +109,217 @@ export default function PaymentsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
<div>
|
||||||
<Sidebar
|
{/* Header */}
|
||||||
isMobileOpen={isMobileMenuOpen}
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
|
||||||
setIsMobileOpen={setIsMobileMenuOpen}
|
<div>
|
||||||
/>
|
<h1 className="text-2xl font-semibold text-gray-800">Payments</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Manage patient payments and outstanding balances
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="mt-4 md:mt-0 flex items-center space-x-2">
|
||||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
<Select
|
||||||
|
defaultValue="all-time"
|
||||||
|
onValueChange={(value) => setPaymentPeriod(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue placeholder="Select period" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all-time">All Time</SelectItem>
|
||||||
|
<SelectItem value="this-month">This Month</SelectItem>
|
||||||
|
<SelectItem value="last-month">Last Month</SelectItem>
|
||||||
|
<SelectItem value="last-90-days">Last 90 Days</SelectItem>
|
||||||
|
<SelectItem value="this-year">This Year</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto p-4">
|
{/* Payment Summary Cards */}
|
||||||
{/* Header */}
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
|
<Card>
|
||||||
<div>
|
<CardHeader className="pb-2">
|
||||||
<h1 className="text-2xl font-semibold text-gray-800">Payments</h1>
|
<CardTitle className="text-sm font-medium text-gray-500">
|
||||||
<p className="text-gray-600">
|
Outstanding Balance
|
||||||
Manage patient payments and outstanding balances
|
</CardTitle>
|
||||||
</p>
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DollarSign className="h-5 w-5 text-yellow-500 mr-2" />
|
||||||
|
<div className="text-2xl font-bold">$0</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
From 0 outstanding invoices
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div className="mt-4 md:mt-0 flex items-center space-x-2">
|
<Card>
|
||||||
<Select
|
<CardHeader className="pb-2">
|
||||||
defaultValue="all-time"
|
<CardTitle className="text-sm font-medium text-gray-500">
|
||||||
onValueChange={(value) => setPaymentPeriod(value)}
|
Payments Collected
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DollarSign className="h-5 w-5 text-green-500 mr-2" />
|
||||||
|
<div className="text-2xl font-bold">${0}</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
From 0 completed payments
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-500">
|
||||||
|
Pending Payments
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DollarSign className="h-5 w-5 text-blue-500 mr-2" />
|
||||||
|
<div className="text-2xl font-bold">$0</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
From 0 pending transactions
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* OCR Image Upload Section - not working rn*/}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-xl font-medium text-gray-800">
|
||||||
|
Payment Document OCR
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div
|
||||||
|
className={`flex-1 border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
||||||
|
isDragging
|
||||||
|
? "border-blue-400 bg-blue-50"
|
||||||
|
: uploadedImage
|
||||||
|
? "border-green-400 bg-green-50"
|
||||||
|
: "border-gray-300 bg-gray-50 hover:border-gray-400"
|
||||||
|
}`}
|
||||||
|
onDrop={handleImageDrop}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
}}
|
||||||
|
onDragLeave={() => setIsDragging(false)}
|
||||||
|
onClick={() =>
|
||||||
|
document.getElementById("image-upload-input")?.click()
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[180px]">
|
{uploadedImage ? (
|
||||||
<SelectValue placeholder="Select period" />
|
<div className="space-y-4">
|
||||||
</SelectTrigger>
|
<div className="flex items-center justify-center space-x-4">
|
||||||
<SelectContent>
|
<Image className="h-8 w-8 text-green-500" />
|
||||||
<SelectItem value="all-time">All Time</SelectItem>
|
<div className="text-left">
|
||||||
<SelectItem value="this-month">This Month</SelectItem>
|
<p className="font-medium text-green-700">
|
||||||
<SelectItem value="last-month">Last Month</SelectItem>
|
{uploadedImage.name}
|
||||||
<SelectItem value="last-90-days">Last 90 Days</SelectItem>
|
</p>
|
||||||
<SelectItem value="this-year">This Year</SelectItem>
|
<p className="text-sm text-gray-500">
|
||||||
</SelectContent>
|
{(uploadedImage.size / 1024 / 1024).toFixed(2)} MB
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Payment Summary Cards */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium text-gray-500">
|
|
||||||
Outstanding Balance
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<DollarSign className="h-5 w-5 text-yellow-500 mr-2" />
|
|
||||||
<div className="text-2xl font-bold">$0</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
From 0 outstanding invoices
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium text-gray-500">
|
|
||||||
Payments Collected
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<DollarSign className="h-5 w-5 text-green-500 mr-2" />
|
|
||||||
<div className="text-2xl font-bold">${0}</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
From 0 completed payments
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium text-gray-500">
|
|
||||||
Pending Payments
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<DollarSign className="h-5 w-5 text-blue-500 mr-2" />
|
|
||||||
<div className="text-2xl font-bold">$0</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
From 0 pending transactions
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* OCR Image Upload Section - not working rn*/}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h2 className="text-xl font-medium text-gray-800">
|
|
||||||
Payment Document OCR
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<div
|
|
||||||
className={`flex-1 border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
|
||||||
isDragging
|
|
||||||
? "border-blue-400 bg-blue-50"
|
|
||||||
: uploadedImage
|
|
||||||
? "border-green-400 bg-green-50"
|
|
||||||
: "border-gray-300 bg-gray-50 hover:border-gray-400"
|
|
||||||
}`}
|
|
||||||
onDrop={handleImageDrop}
|
|
||||||
onDragOver={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsDragging(true);
|
|
||||||
}}
|
|
||||||
onDragLeave={() => setIsDragging(false)}
|
|
||||||
onClick={() =>
|
|
||||||
document.getElementById("image-upload-input")?.click()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{uploadedImage ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-center space-x-4">
|
|
||||||
<Image className="h-8 w-8 text-green-500" />
|
|
||||||
<div className="text-left">
|
|
||||||
<p className="font-medium text-green-700">
|
|
||||||
{uploadedImage.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
{(uploadedImage.size / 1024 / 1024).toFixed(2)} MB
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
removeUploadedImage();
|
|
||||||
}}
|
|
||||||
className="ml-auto"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{isExtracting && (
|
|
||||||
<div className="text-sm text-blue-600">
|
|
||||||
Extracting payment information...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Upload className="h-12 w-12 text-gray-400 mx-auto" />
|
|
||||||
<div>
|
|
||||||
<p className="text-lg font-medium text-gray-700 mb-2">
|
|
||||||
Upload Payment Document
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500 mb-4">
|
|
||||||
Drag and drop an image or click to browse
|
|
||||||
</p>
|
|
||||||
<Button variant="outline" type="button">
|
|
||||||
Choose Image
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-400">
|
|
||||||
Supported formats: JPG, PNG, GIF • Max size: 10MB
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeUploadedImage();
|
||||||
|
}}
|
||||||
|
className="ml-auto"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isExtracting && (
|
||||||
|
<div className="text-sm text-blue-600">
|
||||||
|
Extracting payment information...
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<input
|
|
||||||
id="image-upload-input"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={handleImageSelect}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
<div className="flex flex-col justify-center">
|
<div className="space-y-4">
|
||||||
<Button
|
<Upload className="h-12 w-12 text-gray-400 mx-auto" />
|
||||||
onClick={() => {
|
<div>
|
||||||
if (!uploadedImage) {
|
<p className="text-lg font-medium text-gray-700 mb-2">
|
||||||
toast({
|
Upload Payment Document
|
||||||
title: "No Image Selected",
|
</p>
|
||||||
description:
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
"Please upload an image first before extracting information",
|
Drag and drop an image or click to browse
|
||||||
variant: "destructive",
|
</p>
|
||||||
});
|
<Button variant="outline" type="button">
|
||||||
return;
|
Choose Image
|
||||||
}
|
</Button>
|
||||||
handleOCRExtraction(uploadedImage);
|
</div>
|
||||||
}}
|
<p className="text-xs text-gray-400">
|
||||||
disabled={!uploadedImage || isExtracting}
|
Supported formats: JPG, PNG, GIF • Max size: 10MB
|
||||||
className="min-w-32"
|
</p>
|
||||||
>
|
|
||||||
{isExtracting ? "Extracting..." : "Extract Info"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Recent Payments table */}
|
<input
|
||||||
<Card>
|
id="image-upload-input"
|
||||||
<CardHeader>
|
type="file"
|
||||||
<CardTitle>Payment's Records</CardTitle>
|
accept="image/*"
|
||||||
<CardDescription>
|
onChange={handleImageSelect}
|
||||||
View and manage all recents patient's claims payments
|
className="hidden"
|
||||||
</CardDescription>
|
/>
|
||||||
</CardHeader>
|
</div>
|
||||||
<CardContent>
|
|
||||||
<PaymentsRecentTable allowEdit allowDelete />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Recent Payments by Patients*/}
|
<div className="flex flex-col justify-center">
|
||||||
<PaymentsOfPatientModal/>
|
<Button
|
||||||
</main>
|
onClick={() => {
|
||||||
|
if (!uploadedImage) {
|
||||||
|
toast({
|
||||||
|
title: "No Image Selected",
|
||||||
|
description:
|
||||||
|
"Please upload an image first before extracting information",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleOCRExtraction(uploadedImage);
|
||||||
|
}}
|
||||||
|
disabled={!uploadedImage || isExtracting}
|
||||||
|
className="min-w-32"
|
||||||
|
>
|
||||||
|
{isExtracting ? "Extracting..." : "Extract Info"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Payments table */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Payment's Records</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
View and manage all recents patient's claims payments
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<PaymentsRecentTable allowEdit allowDelete />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Recent Payments by Patients*/}
|
||||||
|
<PaymentsOfPatientModal />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@@ -104,13 +102,7 @@ export default function PreAuthorizationsPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
<div>
|
||||||
<Sidebar isMobileOpen={isMobileMenuOpen} setIsMobileOpen={setIsMobileMenuOpen} />
|
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
|
||||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto p-4">
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-semibold text-gray-800">Pre-authorizations</h1>
|
<h1 className="text-2xl font-semibold text-gray-800">Pre-authorizations</h1>
|
||||||
@@ -342,8 +334,6 @@ export default function PreAuthorizationsPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
304
apps/Frontend/src/pages/reports-page.tsx
Normal file
304
apps/Frontend/src/pages/reports-page.tsx
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
Edit,
|
||||||
|
Eye,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Settings,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Patient } from "@repo/db/types";
|
||||||
|
import { formatDateToHumanReadable } from "@/utils/dateUtils";
|
||||||
|
|
||||||
|
export default function ReportsPage() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [searchField, setSearchField] = useState("all");
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const itemsPerPage = 5;
|
||||||
|
|
||||||
|
// Fetch patients
|
||||||
|
const { data: patients = [], isLoading: isLoadingPatients } = useQuery<
|
||||||
|
Patient[]
|
||||||
|
>({
|
||||||
|
queryKey: ["/api/patients"],
|
||||||
|
enabled: !!user,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter patients based on search
|
||||||
|
const filteredPatients = patients.filter((patient) => {
|
||||||
|
if (!searchTerm) return true;
|
||||||
|
|
||||||
|
const searchLower = searchTerm.toLowerCase();
|
||||||
|
const fullName = `${patient.firstName} ${patient.lastName}`.toLowerCase();
|
||||||
|
const patientId = `PID-${patient?.id?.toString().padStart(4, "0")}`;
|
||||||
|
|
||||||
|
switch (searchField) {
|
||||||
|
case "name":
|
||||||
|
return fullName.includes(searchLower);
|
||||||
|
case "id":
|
||||||
|
return patientId.toLowerCase().includes(searchLower);
|
||||||
|
case "phone":
|
||||||
|
return patient.phone?.toLowerCase().includes(searchLower) || false;
|
||||||
|
case "all":
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
fullName.includes(searchLower) ||
|
||||||
|
patientId.toLowerCase().includes(searchLower) ||
|
||||||
|
patient.phone?.toLowerCase().includes(searchLower) ||
|
||||||
|
patient.email?.toLowerCase().includes(searchLower) ||
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
const totalPages = Math.ceil(filteredPatients.length / itemsPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
const currentPatients = filteredPatients.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
const toggleMobileMenu = () => {
|
||||||
|
setIsMobileMenuOpen(!isMobileMenuOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPatientInitials = (firstName: string, lastName: string) => {
|
||||||
|
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-2xl font-semibold text-gray-900 mb-2">Reports</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
View and manage all patient information
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search and Filters */}
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search patients..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select value={searchField} onValueChange={setSearchField}>
|
||||||
|
<SelectTrigger className="w-32">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Fields</SelectItem>
|
||||||
|
<SelectItem value="name">Name</SelectItem>
|
||||||
|
<SelectItem value="id">Patient ID</SelectItem>
|
||||||
|
<SelectItem value="phone">Phone</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Settings className="h-4 w-4 mr-2" />
|
||||||
|
Advanced
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Patient List */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
{isLoadingPatients ? (
|
||||||
|
<div className="text-center py-8">Loading patients...</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Table Header */}
|
||||||
|
<div className="grid grid-cols-12 gap-4 p-4 bg-gray-50 border-b text-sm font-medium text-gray-600">
|
||||||
|
<div className="col-span-3">Patient</div>
|
||||||
|
<div className="col-span-2">DOB / Gender</div>
|
||||||
|
<div className="col-span-2">Contact</div>
|
||||||
|
<div className="col-span-2">Insurance</div>
|
||||||
|
<div className="col-span-2">Status</div>
|
||||||
|
<div className="col-span-1">Actions</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table Rows */}
|
||||||
|
{currentPatients.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
{searchTerm
|
||||||
|
? "No patients found matching your search."
|
||||||
|
: "No patients available."}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
currentPatients.map((patient) => (
|
||||||
|
<div
|
||||||
|
key={patient.id}
|
||||||
|
className="grid grid-cols-12 gap-4 p-4 border-b hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
{/* Patient Info */}
|
||||||
|
<div className="col-span-3 flex items-center space-x-3">
|
||||||
|
<div className="w-10 h-10 bg-gray-200 rounded-full flex items-center justify-center text-sm font-medium text-gray-600">
|
||||||
|
{getPatientInitials(
|
||||||
|
patient.firstName,
|
||||||
|
patient.lastName
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-gray-900">
|
||||||
|
{patient.firstName} {patient.lastName}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
PID-{patient.id?.toString().padStart(4, "0")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* DOB / Gender */}
|
||||||
|
<div className="col-span-2">
|
||||||
|
<div className="text-sm text-gray-900">
|
||||||
|
{formatDateToHumanReadable(patient.dateOfBirth)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500 capitalize">
|
||||||
|
{patient.gender}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact */}
|
||||||
|
<div className="col-span-2">
|
||||||
|
<div className="text-sm text-gray-900">
|
||||||
|
{patient.phone || "Not provided"}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{patient.email || "No email"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Insurance */}
|
||||||
|
<div className="col-span-2">
|
||||||
|
<div className="text-sm text-gray-900">
|
||||||
|
{patient.insuranceProvider
|
||||||
|
? `${patient.insuranceProvider.charAt(0).toUpperCase()}${patient.insuranceProvider.slice(1)}`
|
||||||
|
: "Not specified"}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
ID: {patient.insuranceId || "N/A"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status */}
|
||||||
|
<div className="col-span-2">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
|
||||||
|
patient.status === "active"
|
||||||
|
? "bg-green-100 text-green-800"
|
||||||
|
: "bg-gray-100 text-gray-800"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{patient.status === "active" ? "Active" : "Inactive"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="col-span-1">
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4 text-blue-600" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4 text-gray-600" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-between p-4 border-t bg-gray-50">
|
||||||
|
<div className="text-sm text-gray-700">
|
||||||
|
Showing {startIndex + 1} to{" "}
|
||||||
|
{Math.min(endIndex, filteredPatients.length)} of{" "}
|
||||||
|
{filteredPatients.length} results
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
setCurrentPage(Math.max(1, currentPage - 1))
|
||||||
|
}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Page Numbers */}
|
||||||
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
|
||||||
|
(page) => (
|
||||||
|
<Button
|
||||||
|
key={page}
|
||||||
|
variant={
|
||||||
|
currentPage === page ? "default" : "outline"
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage(page)}
|
||||||
|
className="w-8 h-8 p-0"
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
setCurrentPage(Math.min(totalPages, currentPage + 1))
|
||||||
|
}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<ChevronRight className="h-4 w-4 ml-1" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { TopAppBar } from "@/components/layout/top-app-bar";
|
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
|
||||||
import { StaffTable } from "@/components/staffs/staff-table";
|
import { StaffTable } from "@/components/staffs/staff-table";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
@@ -259,14 +257,7 @@ export default function SettingsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden bg-gray-100">
|
<div>
|
||||||
<Sidebar
|
|
||||||
isMobileOpen={isMobileMenuOpen}
|
|
||||||
setIsMobileOpen={setIsMobileMenuOpen}
|
|
||||||
/>
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
|
||||||
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
|
||||||
<main className="flex-1 overflow-y-auto p-2">
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
@@ -371,8 +362,6 @@ export default function SettingsPage() {
|
|||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<CredentialTable />
|
<CredentialTable />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user