feat: improve backup management, settings UI, and Twilio webhooks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -105,7 +105,10 @@ export function BackupDestinationManager() {
|
||||
const backupNowMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await apiRequest("POST", "/api/database-management/backup-path");
|
||||
if (!res.ok) throw new Error((await res.json()).error || "Backup failed");
|
||||
if (!res.ok) {
|
||||
const body = await res.json();
|
||||
throw new Error(body.details || body.error || "Backup failed");
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
|
||||
@@ -25,7 +25,7 @@ interface FolderBrowserModalProps {
|
||||
|
||||
export function FolderBrowserModal({ open, onClose, onSelect }: FolderBrowserModalProps) {
|
||||
const [browsePath, setBrowsePath] = useState("/");
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [selected, setSelected] = useState<string>("/");
|
||||
|
||||
const { data, isLoading, isError } = useQuery<BrowseResult>({
|
||||
queryKey: ["/db/browse", browsePath],
|
||||
@@ -41,15 +41,13 @@ export function FolderBrowserModal({ open, onClose, onSelect }: FolderBrowserMod
|
||||
});
|
||||
|
||||
const handleNavigate = (path: string) => {
|
||||
setSelected(null);
|
||||
setSelected(path);
|
||||
setBrowsePath(path);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (selected) {
|
||||
onSelect(selected);
|
||||
onClose();
|
||||
}
|
||||
onSelect(selected);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -120,7 +118,7 @@ export function FolderBrowserModal({ open, onClose, onSelect }: FolderBrowserMod
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} disabled={!selected}>
|
||||
<Button onClick={handleConfirm}>
|
||||
Select Folder
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -82,7 +82,7 @@ export function ImportDatabaseSection() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-gray-500">
|
||||
Restore the database from a <span className="font-medium text-gray-700">.sql</span> backup file.
|
||||
Restore the database from a <span className="font-medium text-gray-700">.sql</span> or <span className="font-medium text-gray-700">.zip</span> backup file.
|
||||
This will overwrite all existing data.
|
||||
</p>
|
||||
|
||||
@@ -90,7 +90,7 @@ export function ImportDatabaseSection() {
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".sql"
|
||||
accept=".sql,.zip"
|
||||
onChange={handleFileChange}
|
||||
className="block text-sm text-gray-600 file:mr-3 file:py-1.5 file:px-3 file:rounded file:border file:border-gray-300 file:text-sm file:bg-white file:text-gray-700 hover:file:bg-gray-50 cursor-pointer"
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,12 @@ import {
|
||||
Microscope,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
UserCog,
|
||||
User,
|
||||
ShieldCheck,
|
||||
Stethoscope,
|
||||
Workflow,
|
||||
Bot,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
@@ -30,6 +36,8 @@ type NavChild = {
|
||||
name: string;
|
||||
path: string;
|
||||
icon: React.ReactNode;
|
||||
adminOnly?: boolean;
|
||||
groupLabel?: string; // renders a group heading before this item
|
||||
};
|
||||
|
||||
type NavItem = {
|
||||
@@ -42,13 +50,14 @@ type NavItem = {
|
||||
|
||||
export function Sidebar() {
|
||||
const [location] = useLocation();
|
||||
const { state, openMobile, setOpenMobile } = useSidebar(); // "expanded" | "collapsed"
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -56,6 +65,9 @@ export function Sidebar() {
|
||||
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) => {
|
||||
@@ -163,6 +175,53 @@ export function Sidebar() {
|
||||
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: "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" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[]
|
||||
@@ -172,16 +231,16 @@ export function Sidebar() {
|
||||
<div
|
||||
className={cn(
|
||||
"bg-white border-r border-gray-200 shadow-sm z-20",
|
||||
"overflow-hidden will-change-[width]",
|
||||
"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-auto md:flex-shrink-0",
|
||||
"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">
|
||||
<div className="p-2 h-full overflow-y-auto">
|
||||
<nav role="navigation" aria-label="Main">
|
||||
{navItems
|
||||
.filter((item) => !item.adminOnly || isAdmin)
|
||||
@@ -189,6 +248,9 @@ export function Sidebar() {
|
||||
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}>
|
||||
@@ -214,28 +276,36 @@ export function Sidebar() {
|
||||
|
||||
{isExpanded && (
|
||||
<div className="ml-4 border-l border-gray-200 pl-2 mb-1">
|
||||
{item.children.map((child) => {
|
||||
const isActive = location === child.path || location.startsWith(child.path + "/");
|
||||
{visibleChildren.map((child) => {
|
||||
const isActive =
|
||||
location === child.path ||
|
||||
location.startsWith(child.path + "/");
|
||||
return (
|
||||
<Link
|
||||
to={child.path}
|
||||
key={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"
|
||||
)}
|
||||
<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)}
|
||||
>
|
||||
{child.icon}
|
||||
<span className="whitespace-nowrap select-none text-sm">
|
||||
{child.name}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
<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>
|
||||
@@ -256,7 +326,6 @@ export function Sidebar() {
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
{/* show label only after expand animation completes */}
|
||||
<span className="whitespace-nowrap select-none">
|
||||
{item.name}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user