- Add floating chat window Hand-off to AI toggle (per-patient) and after-hours AI toggle (global) - Add LangGraph-powered appointment reminder flow: AI introduces itself, classifies YES/NO, handles confirmation with appointment date/time - Add multi-step rescheduling flow: ASAP vs next week, tomorrow offer, Mon/Tue/Wed picker, morning/afternoon time slot — automatically updates appointment in DB - Add new patient / after-hours flow: new vs existing patient, dental insurance check, MassHealth Selenium eligibility check (auto-uses saved member ID + DOB for existing patients), self-pay fallback - Add AI Chat Settings page (Settings → Advanced) with editable greeting templates and LangGraph flow diagrams for both reminder and new-patient flows - Add Schedule a New Patient template option in chat window, starts new-patient conversation flow - Add GET/PUT endpoints for AI handoff, after-hours handoff, and AI chat templates - Add multilingual support (7 languages) across all AI reply nodes with LLM generation and hardcoded fallbacks - Add pending reschedule in-memory store and conversation stage tracking across all flows Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
371 lines
12 KiB
TypeScript
Executable File
371 lines
12 KiB
TypeScript
Executable File
import { Link, useLocation } from "wouter";
|
|
import {
|
|
LayoutDashboard,
|
|
Users,
|
|
Calendar,
|
|
Settings,
|
|
FileCheck,
|
|
Shield,
|
|
CreditCard,
|
|
FolderOpen,
|
|
Database,
|
|
FileText,
|
|
Cloud,
|
|
Phone,
|
|
Activity,
|
|
ClipboardList,
|
|
LayoutGrid,
|
|
ListChecks,
|
|
Pill,
|
|
Microscope,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
UserCog,
|
|
User,
|
|
ShieldCheck,
|
|
Stethoscope,
|
|
Workflow,
|
|
Bot,
|
|
Clock,
|
|
Building2,
|
|
Timer,
|
|
BookOpen,
|
|
} from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { useMemo, useState, useEffect } from "react";
|
|
import { useSidebar } from "@/components/ui/sidebar";
|
|
import { useAuth } from "@/hooks/use-auth";
|
|
|
|
type NavChild = {
|
|
name: string;
|
|
path: string;
|
|
icon: React.ReactNode;
|
|
adminOnly?: boolean;
|
|
groupLabel?: string; // renders a group heading before this item
|
|
};
|
|
|
|
type NavItem = {
|
|
name: string;
|
|
path: string;
|
|
icon: React.ReactNode;
|
|
adminOnly?: boolean;
|
|
children?: NavChild[];
|
|
};
|
|
|
|
export function Sidebar() {
|
|
const [location] = useLocation();
|
|
const { state, openMobile, setOpenMobile } = useSidebar();
|
|
const { user } = useAuth();
|
|
const isAdmin = user?.username === "admin";
|
|
|
|
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => {
|
|
const s = new Set<string>();
|
|
if (location.startsWith("/chart")) s.add("/chart");
|
|
if (location.startsWith("/settings")) s.add("/settings");
|
|
return s;
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (location.startsWith("/chart")) {
|
|
setExpandedPaths((prev) => new Set([...prev, "/chart"]));
|
|
}
|
|
if (location.startsWith("/settings")) {
|
|
setExpandedPaths((prev) => new Set([...prev, "/settings"]));
|
|
}
|
|
}, [location]);
|
|
|
|
const togglePath = (path: string) => {
|
|
setExpandedPaths((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(path)) next.delete(path);
|
|
else next.add(path);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const navItems: NavItem[] = useMemo(
|
|
() => [
|
|
{
|
|
name: "Dashboard",
|
|
path: "/dashboard",
|
|
icon: <LayoutDashboard className="h-5 w-5 text-violet-500" />,
|
|
},
|
|
{
|
|
name: "Patient Connection",
|
|
path: "/patient-connection",
|
|
icon: <Phone className="h-5 w-5 text-green-500" />,
|
|
},
|
|
{
|
|
name: "Schedule",
|
|
path: "/appointments",
|
|
icon: <Calendar className="h-5 w-5 text-blue-500" />,
|
|
},
|
|
{
|
|
name: "Patient Management",
|
|
path: "/patients",
|
|
icon: <Users className="h-5 w-5 text-indigo-500" />,
|
|
},
|
|
{
|
|
name: "Chart",
|
|
path: "/chart",
|
|
icon: <ClipboardList className="h-5 w-5 text-teal-500" />,
|
|
children: [
|
|
{
|
|
name: "Charting Map",
|
|
path: "/chart/charting",
|
|
icon: <LayoutGrid className="h-4 w-4 text-teal-400" />,
|
|
},
|
|
{
|
|
name: "Treatment Plan",
|
|
path: "/chart/treatment-plan",
|
|
icon: <ListChecks className="h-4 w-4 text-orange-400" />,
|
|
},
|
|
{
|
|
name: "Prescription",
|
|
path: "/chart/prescription",
|
|
icon: <Pill className="h-4 w-4 text-rose-400" />,
|
|
},
|
|
{
|
|
name: "Lab Management",
|
|
path: "/chart/lab-management",
|
|
icon: <Microscope className="h-4 w-4 text-purple-400" />,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Insurance Eligibility",
|
|
path: "/insurance-status",
|
|
icon: <Shield className="h-5 w-5 text-emerald-500" />,
|
|
},
|
|
{
|
|
name: "Claims/PreAuth",
|
|
path: "/claims",
|
|
icon: <FileCheck className="h-5 w-5 text-orange-500" />,
|
|
},
|
|
{
|
|
name: "Accounts/Payments",
|
|
path: "/payments",
|
|
icon: <CreditCard className="h-5 w-5 text-amber-500" />,
|
|
},
|
|
{
|
|
name: "Documents",
|
|
path: "/documents",
|
|
icon: <FolderOpen className="h-5 w-5 text-yellow-500" />,
|
|
},
|
|
{
|
|
name: "Reports",
|
|
path: "/reports",
|
|
icon: <FileText className="h-5 w-5 text-red-400" />,
|
|
},
|
|
{
|
|
name: "Cloud storage",
|
|
path: "/cloud-storage",
|
|
icon: <Cloud className="h-5 w-5 text-sky-500" />,
|
|
},
|
|
{
|
|
name: "Database Management",
|
|
path: "/database-management",
|
|
icon: <Database className="h-5 w-5 text-slate-500" />,
|
|
adminOnly: true,
|
|
},
|
|
{
|
|
name: "Job Monitor",
|
|
path: "/job-monitor",
|
|
icon: <Activity className="h-5 w-5 text-rose-500" />,
|
|
adminOnly: true,
|
|
},
|
|
{
|
|
name: "Settings",
|
|
path: "/settings",
|
|
icon: <Settings className="h-5 w-5 text-gray-400" />,
|
|
adminOnly: true,
|
|
children: [
|
|
// ── General ──────────────────────────────────────────
|
|
{
|
|
groupLabel: "General",
|
|
name: "Staff Management",
|
|
path: "/settings/staff",
|
|
icon: <Users className="h-4 w-4 text-gray-400" />,
|
|
},
|
|
{
|
|
name: "Manage Users",
|
|
path: "/settings/users",
|
|
icon: <UserCog className="h-4 w-4 text-gray-400" />,
|
|
adminOnly: true,
|
|
},
|
|
{
|
|
name: "Account Settings",
|
|
path: "/settings/account",
|
|
icon: <User className="h-4 w-4 text-gray-400" />,
|
|
},
|
|
{
|
|
name: "Insurance Credentials",
|
|
path: "/settings/credentials",
|
|
icon: <ShieldCheck className="h-4 w-4 text-gray-400" />,
|
|
},
|
|
{
|
|
name: "NPI Providers",
|
|
path: "/settings/npi",
|
|
icon: <Stethoscope className="h-4 w-4 text-gray-400" />,
|
|
},
|
|
{
|
|
name: "Program Bridge",
|
|
path: "/settings/programs",
|
|
icon: <Workflow className="h-4 w-4 text-gray-400" />,
|
|
},
|
|
// ── Advanced ─────────────────────────────────────────
|
|
{
|
|
groupLabel: "Advanced",
|
|
name: "Office Hours",
|
|
path: "/settings/officehours",
|
|
icon: <Clock className="h-4 w-4 text-gray-400" />,
|
|
},
|
|
{
|
|
name: "Office Contact",
|
|
path: "/settings/officecontact",
|
|
icon: <Building2 className="h-4 w-4 text-gray-400" />,
|
|
},
|
|
{
|
|
name: "Insurance/Transportation Contact",
|
|
path: "/settings/insurancecontact",
|
|
icon: <BookOpen className="h-4 w-4 text-gray-400" />,
|
|
},
|
|
{
|
|
name: "Procedure Duration/Time Slot",
|
|
path: "/settings/proceduretimeslot",
|
|
icon: <Timer className="h-4 w-4 text-gray-400" />,
|
|
},
|
|
{
|
|
name: "Twilio Settings",
|
|
path: "/settings/twilio",
|
|
icon: <Phone className="h-4 w-4 text-gray-400" />,
|
|
},
|
|
{
|
|
name: "Google AI Settings",
|
|
path: "/settings/ai",
|
|
icon: <Bot className="h-4 w-4 text-gray-400" />,
|
|
},
|
|
{
|
|
name: "AI Chat Settings",
|
|
path: "/settings/aichat",
|
|
icon: <Bot className="h-4 w-4 text-gray-400" />,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
[]
|
|
);
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"bg-white border-r border-gray-200 shadow-sm z-20",
|
|
"overflow-x-hidden will-change-[width]",
|
|
"transition-[width] duration-200 ease-in-out",
|
|
openMobile
|
|
? "fixed top-16 left-0 h-[calc(100vh-4rem)] w-64 block md:hidden"
|
|
: "hidden md:block",
|
|
"md:static md:top-auto md:h-full md:flex-shrink-0",
|
|
state === "collapsed" ? "md:w-0 overflow-hidden" : "md:w-64"
|
|
)}
|
|
>
|
|
<div className="p-2 h-full overflow-y-auto">
|
|
<nav role="navigation" aria-label="Main">
|
|
{navItems
|
|
.filter((item) => !item.adminOnly || isAdmin)
|
|
.map((item) => {
|
|
if (item.children) {
|
|
const isParentActive = location.startsWith(item.path);
|
|
const isExpanded = expandedPaths.has(item.path);
|
|
const visibleChildren = item.children.filter(
|
|
(c) => !c.adminOnly || isAdmin
|
|
);
|
|
|
|
return (
|
|
<div key={item.path}>
|
|
<div
|
|
className={cn(
|
|
"flex items-center justify-between p-2 rounded-md pl-3 mb-1 transition-colors cursor-pointer select-none",
|
|
isParentActive
|
|
? "text-primary font-medium bg-primary/5"
|
|
: "text-gray-600 hover:bg-gray-100"
|
|
)}
|
|
onClick={() => togglePath(item.path)}
|
|
>
|
|
<div className="flex items-center space-x-3">
|
|
{item.icon}
|
|
<span className="whitespace-nowrap">{item.name}</span>
|
|
</div>
|
|
{isExpanded ? (
|
|
<ChevronDown className="h-4 w-4 flex-shrink-0 opacity-60" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4 flex-shrink-0 opacity-60" />
|
|
)}
|
|
</div>
|
|
|
|
{isExpanded && (
|
|
<div className="ml-4 border-l border-gray-200 pl-2 mb-1">
|
|
{visibleChildren.map((child) => {
|
|
const isActive =
|
|
location === child.path ||
|
|
location.startsWith(child.path + "/");
|
|
return (
|
|
<div key={child.path}>
|
|
{child.groupLabel && (
|
|
<p className="text-[10px] font-semibold uppercase tracking-wider text-gray-400 px-2 pt-2 pb-0.5">
|
|
{child.groupLabel}
|
|
</p>
|
|
)}
|
|
<Link
|
|
to={child.path}
|
|
onClick={() => setOpenMobile(false)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"flex items-center space-x-2 p-2 rounded-md pl-2 mb-0.5 transition-colors cursor-pointer",
|
|
isActive
|
|
? "text-primary font-medium bg-primary/5 border-l-2 border-primary"
|
|
: "text-gray-500 hover:bg-gray-100 hover:text-gray-700"
|
|
)}
|
|
>
|
|
{child.icon}
|
|
<span className="whitespace-nowrap select-none text-sm">
|
|
{child.name}
|
|
</span>
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div key={item.path}>
|
|
<Link to={item.path} onClick={() => setOpenMobile(false)}>
|
|
<div
|
|
className={cn(
|
|
"flex items-center space-x-3 p-2 rounded-md pl-3 mb-1 transition-colors cursor-pointer",
|
|
location === item.path
|
|
? "text-primary font-medium border-l-2 border-primary"
|
|
: "text-gray-600 hover:bg-gray-100"
|
|
)}
|
|
>
|
|
{item.icon}
|
|
<span className="whitespace-nowrap select-none">
|
|
{item.name}
|
|
</span>
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
);
|
|
})}
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|