Files
helix-engage/src/components/layout/sidebar.tsx
2026-03-23 16:41:58 +05:30

281 lines
12 KiB
TypeScript

import { useState } from "react";
import {
faArrowRightFromBracket,
faBullhorn,
faChartMixed,
faChevronLeft,
faChevronRight,
faClockRotateLeft,
faCommentDots,
faGear,
faGrid2,
faHospitalUser,
faPhone,
faPlug,
faUsers,
} from "@fortawesome/pro-duotone-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useAtom } from "jotai";
import { Link, useNavigate } from "react-router";
import { MobileNavigationHeader } from "@/components/application/app-navigation/base-components/mobile-header";
import { NavAccountCard } from "@/components/application/app-navigation/base-components/nav-account-card";
import { NavItemBase } from "@/components/application/app-navigation/base-components/nav-item";
import type { NavItemType } from "@/components/application/app-navigation/config";
import { Dialog, Modal, ModalOverlay } from "@/components/application/modals/modal";
import { Avatar } from "@/components/base/avatar/avatar";
import { Button } from "@/components/base/buttons/button";
import { apiClient } from "@/lib/api-client";
import { faIcon } from "@/lib/icon-wrapper";
import { notify } from "@/lib/toast";
import { useAuth } from "@/providers/auth-provider";
import { sidebarCollapsedAtom } from "@/state/sidebar-state";
import { cx } from "@/utils/cx";
const EXPANDED_WIDTH = 292;
const COLLAPSED_WIDTH = 64;
const IconGrid2 = faIcon(faGrid2);
const IconBullhorn = faIcon(faBullhorn);
const IconCommentDots = faIcon(faCommentDots);
const IconChartMixed = faIcon(faChartMixed);
const IconPlug = faIcon(faPlug);
const IconGear = faIcon(faGear);
const IconPhone = faIcon(faPhone);
const IconClockRewind = faIcon(faClockRotateLeft);
const IconUsers = faIcon(faUsers);
const IconHospitalUser = faIcon(faHospitalUser);
type NavSection = {
label: string;
items: NavItemType[];
};
const getNavSections = (role: string): NavSection[] => {
if (role === "admin") {
return [
{ label: "Overview", items: [{ label: "Team Dashboard", href: "/", icon: IconGrid2 }] },
{
label: "Management",
items: [
{ label: "Campaigns", href: "/campaigns", icon: IconBullhorn },
{ label: "Analytics", href: "/reports", icon: IconChartMixed },
],
},
{
label: "Admin",
items: [
{ label: "Integrations", href: "/integrations", icon: IconPlug },
{ label: "Settings", href: "/settings", icon: IconGear },
],
},
];
}
if (role === "cc-agent") {
return [
{
label: "Call Center",
items: [
{ label: "Call Desk", href: "/", icon: IconPhone },
{ label: "Call History", href: "/call-history", icon: IconClockRewind },
{ label: "Patients", href: "/patients", icon: IconHospitalUser },
{ label: "My Performance", href: "/my-performance", icon: IconChartMixed },
],
},
];
}
return [
{
label: "Main",
items: [
{ label: "Lead Workspace", href: "/", icon: IconGrid2 },
{ label: "All Leads", href: "/leads", icon: IconUsers },
{ label: "Patients", href: "/patients", icon: IconHospitalUser },
{ label: "Campaigns", href: "/campaigns", icon: IconBullhorn },
{ label: "Outreach", href: "/outreach", icon: IconCommentDots },
],
},
{ label: "Insights", items: [{ label: "Analytics", href: "/reports", icon: IconChartMixed }] },
];
};
const getRoleSubtitle = (role: string): string => {
switch (role) {
case "admin":
return "Marketing Admin";
case "cc-agent":
return "Call Center Agent";
default:
return "Marketing Executive";
}
};
interface SidebarProps {
activeUrl?: string;
}
export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
const { logout, user } = useAuth();
const navigate = useNavigate();
const [collapsed, setCollapsed] = useAtom(sidebarCollapsedAtom);
const width = collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH;
const [logoutOpen, setLogoutOpen] = useState(false);
const handleSignOut = () => {
setLogoutOpen(true);
};
const confirmSignOut = () => {
setLogoutOpen(false);
logout();
navigate("/login");
};
const handleForceReady = async () => {
try {
await apiClient.post("/api/ozonetel/agent-ready", {});
notify.success("Agent Ready", "Agent state has been reset to Ready");
} catch {
notify.error("Force Ready Failed", "Could not reset agent state");
}
};
const navSections = getNavSections(user.role);
const content = (
<aside
style={{ "--width": `${width}px` } as React.CSSProperties}
className={cx(
"flex h-full w-full max-w-full flex-col justify-between overflow-auto border-secondary bg-primary pt-4 shadow-xs transition-all duration-200 ease-linear md:border-r lg:w-(--width) lg:rounded-xl lg:border lg:pt-5",
)}
>
{/* Logo + collapse toggle */}
<div className={cx("flex items-center gap-2", collapsed ? "justify-center px-2" : "justify-between px-4 lg:px-5")}>
{collapsed ? (
<img src="/favicon-32.png" alt="Helix Engage" className="size-8 shrink-0 rounded-lg" />
) : (
<div className="flex flex-col gap-1">
<span className="text-md font-bold text-primary">Helix Engage</span>
<span className="text-xs text-tertiary">Global Hospital &middot; {getRoleSubtitle(user.role)}</span>
</div>
)}
<button
onClick={() => setCollapsed(!collapsed)}
className="hidden size-6 items-center justify-center rounded-md text-fg-quaternary transition duration-100 ease-linear hover:bg-secondary hover:text-fg-secondary lg:flex"
title={collapsed ? "Expand sidebar" : "Collapse sidebar"}
>
<FontAwesomeIcon icon={collapsed ? faChevronRight : faChevronLeft} className="size-3" />
</button>
</div>
{/* Nav sections */}
<ul className="mt-6">
{navSections.map((group) => (
<li key={group.label}>
{!collapsed && (
<div className="px-5 pb-1">
<p className="text-xs font-bold text-quaternary uppercase">{group.label}</p>
</div>
)}
<ul className={cx(collapsed ? "px-2 pb-3" : "px-4 pb-5")}>
{group.items.map((item) => (
<li key={item.label} className="py-0.5">
{collapsed ? (
<Link
to={item.href ?? "/"}
title={item.label}
className={cx(
"flex size-10 items-center justify-center rounded-lg transition duration-100 ease-linear",
item.href === activeUrl
? "bg-active text-fg-brand-primary"
: "text-fg-quaternary hover:bg-primary_hover hover:text-fg-secondary",
)}
>
{item.icon && <item.icon className="size-5" />}
</Link>
) : (
<NavItemBase icon={item.icon} href={item.href} badge={item.badge} type="link" current={item.href === activeUrl}>
{item.label}
</NavItemBase>
)}
</li>
))}
</ul>
</li>
))}
</ul>
{/* Account card */}
<div className={cx("mt-auto py-4", collapsed ? "flex justify-center px-2" : "px-2 lg:px-4")}>
{collapsed ? (
<button
onClick={handleSignOut}
title={`${user.name}\nSign out`}
className="rounded-lg p-1 transition duration-100 ease-linear hover:bg-primary_hover"
>
<Avatar size="sm" initials={user.initials} status="online" />
</button>
) : (
<NavAccountCard
items={[
{
id: "current",
name: user.name,
email: user.email,
avatar: "",
status: "online" as const,
},
]}
selectedAccountId="current"
onSignOut={handleSignOut}
onForceReady={handleForceReady}
/>
)}
</div>
</aside>
);
return (
<>
<MobileNavigationHeader>{content}</MobileNavigationHeader>
<div className="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex lg:py-1 lg:pl-1">{content}</div>
<div
style={{ paddingLeft: width + 4 }}
className="invisible hidden transition-all duration-200 ease-linear lg:sticky lg:top-0 lg:bottom-0 lg:left-0 lg:block"
/>
{/* Logout confirmation modal */}
<ModalOverlay isOpen={logoutOpen} onOpenChange={setLogoutOpen} isDismissable>
<Modal className="max-w-md">
<Dialog>
<div className="rounded-xl bg-primary p-6 shadow-xl">
<div className="flex flex-col items-center gap-4 text-center">
<div className="flex size-12 items-center justify-center rounded-full bg-warning-secondary">
<FontAwesomeIcon icon={faArrowRightFromBracket} className="size-5 text-fg-warning-primary" />
</div>
<div>
<h3 className="text-lg font-semibold text-primary">Sign out?</h3>
<p className="mt-1 text-sm text-tertiary">
You will be logged out of Helix Engage and your Ozonetel agent session will end. Any active calls will be disconnected.
</p>
</div>
<div className="flex w-full gap-3">
<Button size="md" color="secondary" className="flex-1" onClick={() => setLogoutOpen(false)}>
Cancel
</Button>
<Button size="md" color="primary-destructive" className="flex-1" onClick={confirmSignOut}>
Sign out
</Button>
</div>
</div>
</div>
</Dialog>
</Modal>
</ModalOverlay>
</>
);
};