Files
DentalManagementMH07/apps/Frontend/src/components/layout/sidebar.tsx
ff 089e94ba88 feat: add AI-driven phone calls to patient connection
Adds an "AI Call" option alongside Call/SMS/Chat that places an outbound
call where Lisa converses live with the patient via Twilio speech
gather + TTS, using the same conversational AI as chat. Includes a
separate, user-editable Call Template (Settings > AI Chat/Call Settings)
so the call greeting can be customized independently of SMS templates.

Also fixes the outbound-call webhook URL to always use the fixed public
Twilio hostname (CLOUDFLARE_HOST) instead of deriving it from the
triggering request, since staff-browser requests arrive over the
LAN-only hostname, which Twilio can never reach.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 20:14:09 -04:00

441 lines
14 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,
MonitorCog,
GraduationCap,
ShoppingCart,
Search,
KeyRound,
Copy,
Keyboard,
} 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");
if (location.startsWith("/dental-shopping")) s.add("/dental-shopping");
if (location.startsWith("/ai-input-agent")) s.add("/ai-input-agent");
return s;
});
useEffect(() => {
if (location.startsWith("/chart")) {
setExpandedPaths((prev) => new Set([...prev, "/chart"]));
}
if (location.startsWith("/settings")) {
setExpandedPaths((prev) => new Set([...prev, "/settings"]));
}
if (location.startsWith("/dental-shopping")) {
setExpandedPaths((prev) => new Set([...prev, "/dental-shopping"]));
}
if (location.startsWith("/ai-input-agent")) {
setExpandedPaths((prev) => new Set([...prev, "/ai-input-agent"]));
}
}, [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: "Dental Education",
path: "/dental-education",
icon: <GraduationCap className="h-5 w-5 text-lime-600" />,
},
{
name: "AI Copy/Type Agent",
path: "/ai-input-agent",
icon: <Bot className="h-5 w-5 text-violet-500" />,
children: [
{
name: "Copy Agent",
path: "/ai-input-agent/copy-agent",
icon: <Copy className="h-4 w-4 text-violet-400" />,
},
{
name: "Type Agent",
path: "/ai-input-agent/type-agent",
icon: <Keyboard className="h-4 w-4 text-violet-400" />,
},
{
name: "Settings",
path: "/ai-input-agent/settings",
icon: <Settings className="h-4 w-4 text-violet-400" />,
},
],
},
{
name: "AI Dental Shopping",
path: "/dental-shopping",
icon: <ShoppingCart className="h-5 w-5 text-cyan-500" />,
children: [
{
name: "Search / Tag",
path: "/dental-shopping/search-tag",
icon: <Search className="h-4 w-4 text-cyan-400" />,
},
{
name: "Login Info",
path: "/dental-shopping/login-info",
icon: <KeyRound className="h-4 w-4 text-cyan-400" />,
},
],
},
{
name: "Activation",
path: "/activation",
icon: <KeyRound className="h-5 w-5 text-amber-500" />,
adminOnly: true,
},
{
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: "AI API Setting",
path: "/settings/ai",
icon: <Bot className="h-4 w-4 text-gray-400" />,
},
{
name: "AI Chat/Call Settings",
path: "/settings/aichat",
icon: <Bot className="h-4 w-4 text-gray-400" />,
},
{
name: "Selenium Settings",
path: "/settings/selenium",
icon: <MonitorCog 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>
);
}