applayout added, sidebar updated

This commit is contained in:
2025-08-26 20:30:00 +05:30
parent 09873596dc
commit ca59f647a2
17 changed files with 1665 additions and 1479 deletions

View File

@@ -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>

View 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>
);
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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,
} };

View File

@@ -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>
);
} }

View File

@@ -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 */}

View File

@@ -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

View File

@@ -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 */}

View File

@@ -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>
); );

View File

@@ -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>
); );

View File

@@ -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>
); );

View File

@@ -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>
); );

View File

@@ -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>
); );
} }

View File

@@ -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>
); );
} }

View 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>
);
}

View File

@@ -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>
); );
} }