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:
2026-03-18 18:33:36 +05:30
parent 61901eb8fb
commit 526ad18159
25 changed files with 1664 additions and 540 deletions

View File

@@ -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 &middot; {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 &middot; {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"
/>
</>
);