mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: call desk redesign — 2-panel layout, collapsible sidebar, inline AI, ringtone
- Collapsible sidebar with Jotai atom (icon-only mode, persisted to localStorage) - 2-panel call desk: worklist (60%) + context panel (40%) with AI + Lead 360 tabs - Inline AI call prep card — known lead summary or unknown caller script - Active call card with compact Answer/Decline buttons - Worklist panel with human-readable labels, priority badges, click-to-select - Context panel auto-switches to Lead 360 when lead selected or call incoming - Browser ringtone via Web Audio API on incoming calls - Sonner + Untitled UI IconNotification for toast system - apiClient pattern: centralized post/get/graphql with auto-toast on errors - Remove duplicate avatar from top bar, hide floating widget on call desk - Fix Link routing in collapsed sidebar (was using <a> causing full page reload) - Fix GraphQL field names: adStatus→status, platformUrl needs subfield selection - Silent mode for DataProvider queries to prevent toast spam Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ import {
|
||||
faBell,
|
||||
faBullhorn,
|
||||
faChartMixed,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faClockRotateLeft,
|
||||
faCommentDots,
|
||||
faGear,
|
||||
@@ -12,16 +14,20 @@ import {
|
||||
faPlug,
|
||||
faUsers,
|
||||
} from "@fortawesome/pro-duotone-svg-icons";
|
||||
import { useNavigate } from "react-router";
|
||||
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 { Avatar } from "@/components/base/avatar/avatar";
|
||||
import { useAuth } from "@/providers/auth-provider";
|
||||
import { sidebarCollapsedAtom } from "@/state/sidebar-state";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
const MAIN_SIDEBAR_WIDTH = 292;
|
||||
const EXPANDED_WIDTH = 292;
|
||||
const COLLAPSED_WIDTH = 64;
|
||||
|
||||
// FontAwesome icon wrappers that satisfy FC<HTMLAttributes<HTMLOrSVGElement>>
|
||||
const IconGrid2: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
|
||||
<FontAwesomeIcon icon={faGrid2} className={className} />
|
||||
);
|
||||
@@ -61,70 +67,46 @@ type NavSection = {
|
||||
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: '/analytics', icon: IconChartMixed },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
items: [
|
||||
{ label: 'Integrations', href: '/integrations', icon: IconPlug },
|
||||
{ label: 'Settings', href: '/settings', icon: IconGear },
|
||||
],
|
||||
},
|
||||
{ label: 'Overview', items: [{ label: 'Team Dashboard', href: '/', icon: IconGrid2 }] },
|
||||
{ label: 'Management', items: [
|
||||
{ label: 'Campaigns', href: '/campaigns', icon: IconBullhorn },
|
||||
{ label: 'Analytics', href: '/analytics', 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: 'Follow-ups', href: '/follow-ups', icon: IconBell },
|
||||
{ label: 'Call History', href: '/call-history', icon: IconClockRewind },
|
||||
],
|
||||
},
|
||||
{ label: 'Call Center', items: [
|
||||
{ label: 'Call Desk', href: '/', icon: IconPhone },
|
||||
{ label: 'Follow-ups', href: '/follow-ups', icon: IconBell },
|
||||
{ label: 'Call History', href: '/call-history', icon: IconClockRewind },
|
||||
]},
|
||||
];
|
||||
}
|
||||
|
||||
// Executive (default)
|
||||
return [
|
||||
{
|
||||
label: 'Main',
|
||||
items: [
|
||||
{ label: 'Lead Workspace', href: '/', icon: IconGrid2 },
|
||||
{ label: 'All Leads', href: '/leads', icon: IconUsers },
|
||||
{ label: 'Campaigns', href: '/campaigns', icon: IconBullhorn },
|
||||
{ label: 'Outreach', href: '/outreach', icon: IconCommentDots },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Insights',
|
||||
items: [
|
||||
{ label: 'Analytics', href: '/analytics', icon: IconChartMixed },
|
||||
],
|
||||
},
|
||||
{ label: 'Main', items: [
|
||||
{ label: 'Lead Workspace', href: '/', icon: IconGrid2 },
|
||||
{ label: 'All Leads', href: '/leads', icon: IconUsers },
|
||||
{ label: 'Campaigns', href: '/campaigns', icon: IconBullhorn },
|
||||
{ label: 'Outreach', href: '/outreach', icon: IconCommentDots },
|
||||
]},
|
||||
{ label: 'Insights', items: [
|
||||
{ label: 'Analytics', href: '/analytics', 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';
|
||||
case 'admin': return 'Marketing Admin';
|
||||
case 'cc-agent': return 'Call Center Agent';
|
||||
default: return 'Marketing Executive';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,6 +117,9 @@ interface SidebarProps {
|
||||
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 handleSignOut = () => {
|
||||
logout();
|
||||
@@ -145,34 +130,68 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
|
||||
|
||||
const content = (
|
||||
<aside
|
||||
style={{ "--width": `${MAIN_SIDEBAR_WIDTH}px` } as React.CSSProperties}
|
||||
className="flex h-full w-full max-w-full flex-col justify-between overflow-auto border-secondary bg-primary pt-4 shadow-xs md:border-r lg:w-(--width) lg:rounded-xl lg:border lg:pt-5"
|
||||
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 */}
|
||||
<div className="flex flex-col gap-1 px-4 lg:px-5">
|
||||
<span className="text-md font-bold text-primary">Helix Engage</span>
|
||||
<span className="text-xs text-tertiary">Global Hospital · {getRoleSubtitle(user.role)}</span>
|
||||
{/* Logo + collapse toggle */}
|
||||
<div className={cx("flex items-center gap-2", collapsed ? "justify-center px-2" : "justify-between px-4 lg:px-5")}>
|
||||
{collapsed ? (
|
||||
<div className="flex items-center justify-center bg-brand-solid rounded-lg p-1.5 size-8 shrink-0">
|
||||
<span className="text-white font-bold text-sm leading-none">H</span>
|
||||
</div>
|
||||
) : (
|
||||
<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 · {getRoleSubtitle(user.role)}</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
className="hidden lg:flex size-6 items-center justify-center rounded-md text-fg-quaternary hover:text-fg-secondary hover:bg-secondary transition duration-100 ease-linear"
|
||||
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
<FontAwesomeIcon icon={collapsed ? faChevronRight : faChevronLeft} className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Nav sections */}
|
||||
<ul className="mt-8">
|
||||
<ul className="mt-6">
|
||||
{navSections.map((group) => (
|
||||
<li key={group.label}>
|
||||
<div className="px-5 pb-1">
|
||||
<p className="text-xs font-bold text-quaternary uppercase">{group.label}</p>
|
||||
</div>
|
||||
<ul className="px-4 pb-5">
|
||||
{!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">
|
||||
<NavItemBase
|
||||
icon={item.icon}
|
||||
href={item.href}
|
||||
badge={item.badge}
|
||||
type="link"
|
||||
current={item.href === activeUrl}
|
||||
>
|
||||
{item.label}
|
||||
</NavItemBase>
|
||||
{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>
|
||||
@@ -181,34 +200,39 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
|
||||
</ul>
|
||||
|
||||
{/* Account card */}
|
||||
<div className="mt-auto flex flex-col gap-5 px-2 py-4 lg:gap-6 lg:px-4 lg:py-4">
|
||||
<NavAccountCard
|
||||
items={[{
|
||||
id: 'current',
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
avatar: '',
|
||||
status: 'online' as const,
|
||||
}]}
|
||||
selectedAccountId="current"
|
||||
onSignOut={handleSignOut}
|
||||
/>
|
||||
<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 hover:bg-primary_hover transition duration-100 ease-linear"
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile header navigation */}
|
||||
<MobileNavigationHeader>{content}</MobileNavigationHeader>
|
||||
|
||||
{/* Desktop sidebar navigation */}
|
||||
<div className="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex lg:py-1 lg:pl-1">{content}</div>
|
||||
|
||||
{/* Placeholder to take up physical space because the real sidebar has `fixed` position. */}
|
||||
<div
|
||||
style={{ paddingLeft: MAIN_SIDEBAR_WIDTH + 4 }}
|
||||
className="invisible hidden lg:sticky lg:top-0 lg:bottom-0 lg:left-0 lg:block"
|
||||
style={{ paddingLeft: width + 4 }}
|
||||
className="invisible hidden lg:sticky lg:top-0 lg:bottom-0 lg:left-0 lg:block transition-all duration-200 ease-linear"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user