mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
feat: notification bell in PageHeader + remove wasted top bar row for supervisors
- PageHeader: renders NotificationBell when isAdmin — bell now appears on every page that uses PageHeader (leads, contacts, appointments, patients, call history, missed calls, call recordings, live monitor, team performance, settings) - app-shell: top bar row only renders for agents (network indicator + status toggle). Supervisors no longer see a wasted empty row. - Call Recordings: TopBar → PageHeader with badge + info icon - Live Monitor: TopBar → PageHeader with badge + info icon - Team Performance: TopBar → PageHeader with info icon - Settings: TopBar → PageHeader with info icon - Missed Calls: underline tabs → custom pills (consistent with all pages) - Desktop overlay app-shell synced with same changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,6 @@ import { useSip } from '@/providers/sip-provider';
|
|||||||
import { CallWidget } from '@/components/call-desk/call-widget';
|
import { CallWidget } from '@/components/call-desk/call-widget';
|
||||||
import { MaintOtpModal } from '@/components/modals/maint-otp-modal';
|
import { MaintOtpModal } from '@/components/modals/maint-otp-modal';
|
||||||
import { AgentStatusToggle } from '@/components/call-desk/agent-status-toggle';
|
import { AgentStatusToggle } from '@/components/call-desk/agent-status-toggle';
|
||||||
import { NotificationBell } from './notification-bell';
|
|
||||||
import { ResumeSetupBanner } from '@/components/setup/resume-setup-banner';
|
import { ResumeSetupBanner } from '@/components/setup/resume-setup-banner';
|
||||||
import { Badge } from '@/components/base/badges/badges';
|
import { Badge } from '@/components/base/badges/badges';
|
||||||
import { useAuth } from '@/providers/auth-provider';
|
import { useAuth } from '@/providers/auth-provider';
|
||||||
@@ -25,7 +24,7 @@ interface AppShellProps {
|
|||||||
|
|
||||||
export const AppShell = ({ children }: AppShellProps) => {
|
export const AppShell = ({ children }: AppShellProps) => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { isCCAgent, isAdmin } = useAuth();
|
const { isCCAgent } = useAuth();
|
||||||
const { isOpen, activeAction, close } = useMaintShortcuts();
|
const { isOpen, activeAction, close } = useMaintShortcuts();
|
||||||
const { connectionStatus, isRegistered } = useSip();
|
const { connectionStatus, isRegistered } = useSip();
|
||||||
const networkQuality = useNetworkStatus();
|
const networkQuality = useNetworkStatus();
|
||||||
@@ -118,35 +117,25 @@ export const AppShell = ({ children }: AppShellProps) => {
|
|||||||
<div className="flex h-screen bg-primary">
|
<div className="flex h-screen bg-primary">
|
||||||
<Sidebar activeUrl={pathname} />
|
<Sidebar activeUrl={pathname} />
|
||||||
<div className="flex flex-1 flex-col overflow-hidden">
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
{/* Persistent top bar — visible on all pages */}
|
{/* Agent top bar — network indicator + status toggle (agents only) */}
|
||||||
{(hasAgentConfig || isAdmin) && (
|
{hasAgentConfig && (
|
||||||
<div className="flex shrink-0 items-center gap-2 border-b border-secondary px-4 py-2">
|
<div className="flex shrink-0 items-center gap-2 border-b border-secondary px-4 py-2">
|
||||||
{/* GlobalSearch hidden — navigation on result click
|
|
||||||
routes to Patient 360 with stale appointment state
|
|
||||||
from the call desk. Revisit when the Patient 360
|
|
||||||
route properly resets context on mount. (#4) */}
|
|
||||||
{/* <GlobalSearch /> */}
|
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
{isAdmin && <NotificationBell />}
|
<div className={cx(
|
||||||
{hasAgentConfig && (
|
'flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium',
|
||||||
<>
|
networkQuality === 'good'
|
||||||
<div className={cx(
|
? 'bg-success-primary text-success-primary'
|
||||||
'flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium',
|
: networkQuality === 'offline'
|
||||||
networkQuality === 'good'
|
? 'bg-error-secondary text-error-primary'
|
||||||
? 'bg-success-primary text-success-primary'
|
: 'bg-warning-secondary text-warning-primary',
|
||||||
: networkQuality === 'offline'
|
)}>
|
||||||
? 'bg-error-secondary text-error-primary'
|
<FontAwesomeIcon
|
||||||
: 'bg-warning-secondary text-warning-primary',
|
icon={networkQuality === 'offline' ? faWifiSlash : faWifi}
|
||||||
)}>
|
className="size-3"
|
||||||
<FontAwesomeIcon
|
/>
|
||||||
icon={networkQuality === 'offline' ? faWifiSlash : faWifi}
|
{networkQuality === 'good' ? 'Connected' : networkQuality === 'offline' ? 'No connection' : 'Unstable'}
|
||||||
className="size-3"
|
</div>
|
||||||
/>
|
<AgentStatusToggle isRegistered={isRegistered} connectionStatus={connectionStatus} />
|
||||||
{networkQuality === 'good' ? 'Connected' : networkQuality === 'offline' ? 'No connection' : 'Unstable'}
|
|
||||||
</div>
|
|
||||||
<AgentStatusToggle isRegistered={isRegistered} connectionStatus={connectionStatus} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
import { useState, useRef, useEffect, type ReactNode } from 'react';
|
import { useState, useRef, useEffect, type ReactNode } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faCircleInfo } from '@fortawesome/pro-duotone-svg-icons';
|
import { faCircleInfo } from '@fortawesome/pro-duotone-svg-icons';
|
||||||
|
import { NotificationBell } from './notification-bell';
|
||||||
|
import { useAuth } from '@/providers/auth-provider';
|
||||||
|
|
||||||
interface PageHeaderProps {
|
interface PageHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -65,7 +67,9 @@ const InfoTooltip = ({ text }: { text: string }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageHeader = ({ title, badge, subtitle, infoText, controls, tabs }: PageHeaderProps) => (
|
export const PageHeader = ({ title, badge, subtitle, infoText, controls, tabs }: PageHeaderProps) => {
|
||||||
|
const { isAdmin } = useAuth();
|
||||||
|
return (
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
{/* Row 1: title + controls */}
|
{/* Row 1: title + controls */}
|
||||||
<div className="flex items-center justify-between px-6 py-3">
|
<div className="flex items-center justify-between px-6 py-3">
|
||||||
@@ -81,11 +85,10 @@ export const PageHeader = ({ title, badge, subtitle, infoText, controls, tabs }:
|
|||||||
)}
|
)}
|
||||||
{infoText && <InfoTooltip text={infoText} />}
|
{infoText && <InfoTooltip text={infoText} />}
|
||||||
</div>
|
</div>
|
||||||
{controls && (
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
{controls}
|
||||||
{controls}
|
{isAdmin && <NotificationBell />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Row 2: optional tabs — no container border, tab underline is the separator */}
|
{/* Row 2: optional tabs — no container border, tab underline is the separator */}
|
||||||
@@ -95,4 +98,5 @@ export const PageHeader = ({ title, badge, subtitle, infoText, controls, tabs }:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Badge } from '@/components/base/badges/badges';
|
|||||||
import { Input } from '@/components/base/input/input';
|
import { Input } from '@/components/base/input/input';
|
||||||
import { Table } from '@/components/application/table/table';
|
import { Table } from '@/components/application/table/table';
|
||||||
import { PaginationPageDefault } from '@/components/application/pagination/pagination';
|
import { PaginationPageDefault } from '@/components/application/pagination/pagination';
|
||||||
import { TopBar } from '@/components/layout/top-bar';
|
import { PageHeader } from '@/components/layout/page-header';
|
||||||
import { ColumnToggle, useColumnVisibility } from '@/components/application/table/column-toggle';
|
import { ColumnToggle, useColumnVisibility } from '@/components/application/table/column-toggle';
|
||||||
import { PhoneActionCell } from '@/components/call-desk/phone-action-cell';
|
import { PhoneActionCell } from '@/components/call-desk/phone-action-cell';
|
||||||
import { RecordingAnalysisSlideout } from '@/components/call-desk/recording-analysis';
|
import { RecordingAnalysisSlideout } from '@/components/call-desk/recording-analysis';
|
||||||
@@ -239,18 +239,19 @@ export const CallRecordingsPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBar title="Call Recordings" />
|
<PageHeader
|
||||||
<div className="flex flex-1 flex-col overflow-hidden">
|
title="Call Recordings"
|
||||||
{/* Toolbar */}
|
badge={filtered.length}
|
||||||
<div className="flex shrink-0 items-center justify-between border-b border-secondary px-6 py-3">
|
infoText="All call recordings with AI analysis, dispositions, and playback."
|
||||||
<span className="text-sm text-tertiary">{filtered.length} recordings</span>
|
controls={
|
||||||
<div className="flex items-center gap-3">
|
<>
|
||||||
<ColumnToggle columns={columnDefs} visibleColumns={visibleColumns} onToggle={toggleColumn} />
|
<ColumnToggle columns={columnDefs} visibleColumns={visibleColumns} onToggle={toggleColumn} />
|
||||||
<div className="w-56">
|
<div className="w-56">
|
||||||
<Input placeholder="Search agent, phone, disposition..." icon={SearchLg} size="sm" value={search} onChange={handleSearch} />
|
<Input placeholder="Search agent, phone, disposition..." icon={SearchLg} size="sm" value={search} onChange={handleSearch} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div className="flex flex-1 flex-col min-h-0 overflow-hidden px-4 pt-3">
|
<div className="flex flex-1 flex-col min-h-0 overflow-hidden px-4 pt-3">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faHeadset, faPhoneVolume, faPause, faClock, faSparkles, faCalendarCheck, faClockRotateLeft } from '@fortawesome/pro-duotone-svg-icons';
|
import { faHeadset, faPhoneVolume, faPause, faClock, faSparkles, faCalendarCheck, faClockRotateLeft } from '@fortawesome/pro-duotone-svg-icons';
|
||||||
import { TopBar } from '@/components/layout/top-bar';
|
import { PageHeader } from '@/components/layout/page-header';
|
||||||
import { Badge } from '@/components/base/badges/badges';
|
import { Badge } from '@/components/base/badges/badges';
|
||||||
import { Table } from '@/components/application/table/table';
|
import { Table } from '@/components/application/table/table';
|
||||||
import { BargeControls } from '@/components/call-desk/barge-controls';
|
import { BargeControls } from '@/components/call-desk/barge-controls';
|
||||||
@@ -182,7 +182,11 @@ export const LiveMonitorPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBar title="Live Call Monitor" subtitle="Monitor, whisper, or barge into active calls" />
|
<PageHeader
|
||||||
|
title="Live Call Monitor"
|
||||||
|
badge={activeCalls.length}
|
||||||
|
infoText="Monitor, whisper, or barge into active calls in real-time."
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* Left panel — KPIs + call list */}
|
{/* Left panel — KPIs + call list */}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const SearchLg = faIcon(faMagnifyingGlass);
|
|||||||
import { Badge } from '@/components/base/badges/badges';
|
import { Badge } from '@/components/base/badges/badges';
|
||||||
import { Input } from '@/components/base/input/input';
|
import { Input } from '@/components/base/input/input';
|
||||||
import { Table } from '@/components/application/table/table';
|
import { Table } from '@/components/application/table/table';
|
||||||
import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
|
|
||||||
import { PaginationPageDefault } from '@/components/application/pagination/pagination';
|
import { PaginationPageDefault } from '@/components/application/pagination/pagination';
|
||||||
import { PageHeader } from '@/components/layout/page-header';
|
import { PageHeader } from '@/components/layout/page-header';
|
||||||
import { ColumnToggle, useColumnVisibility } from '@/components/application/table/column-toggle';
|
import { ColumnToggle, useColumnVisibility } from '@/components/application/table/column-toggle';
|
||||||
@@ -253,11 +252,22 @@ export const MissedCallsPage = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
tabs={
|
tabs={
|
||||||
<Tabs selectedKey={tab} onSelectionChange={(key) => handleTab(key as StatusTab)}>
|
<div className="flex items-center gap-1.5">
|
||||||
<TabList items={tabItems} type="underline" size="sm">
|
{tabItems.map((item) => (
|
||||||
{(item) => <Tab key={item.id} id={item.id} label={item.label} badge={item.badge} />}
|
<button
|
||||||
</TabList>
|
key={item.id}
|
||||||
</Tabs>
|
onClick={() => handleTab(item.id)}
|
||||||
|
className={cx(
|
||||||
|
'shrink-0 rounded-full px-3 py-1 text-xs font-medium transition duration-100 ease-linear',
|
||||||
|
tab === item.id
|
||||||
|
? 'bg-brand-solid text-white'
|
||||||
|
: 'bg-secondary text-tertiary hover:text-secondary hover:bg-secondary_hover',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.label}{item.badge ? ` ${item.badge}` : ''}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
faPalette,
|
faPalette,
|
||||||
faShieldHalved,
|
faShieldHalved,
|
||||||
} from '@fortawesome/pro-duotone-svg-icons';
|
} from '@fortawesome/pro-duotone-svg-icons';
|
||||||
import { TopBar } from '@/components/layout/top-bar';
|
import { PageHeader } from '@/components/layout/page-header';
|
||||||
import { SectionCard } from '@/components/setup/section-card';
|
import { SectionCard } from '@/components/setup/section-card';
|
||||||
import {
|
import {
|
||||||
SETUP_STEP_NAMES,
|
SETUP_STEP_NAMES,
|
||||||
@@ -50,7 +50,7 @@ export const SettingsPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col overflow-hidden">
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
<TopBar title="Settings" subtitle="Configure your hospital workspace" />
|
<PageHeader title="Settings" infoText="Configure your hospital workspace." />
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-8">
|
<div className="flex-1 overflow-y-auto p-8">
|
||||||
<div className="mx-auto max-w-5xl">
|
<div className="mx-auto max-w-5xl">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
faUsers, faPhoneVolume, faCalendarCheck, faPhoneMissed,
|
faUsers, faPhoneVolume, faCalendarCheck, faPhoneMissed,
|
||||||
faPercent, faTriangleExclamation,
|
faPercent, faTriangleExclamation,
|
||||||
} from '@fortawesome/pro-duotone-svg-icons';
|
} from '@fortawesome/pro-duotone-svg-icons';
|
||||||
import { TopBar } from '@/components/layout/top-bar';
|
import { PageHeader } from '@/components/layout/page-header';
|
||||||
import { Badge } from '@/components/base/badges/badges';
|
import { Badge } from '@/components/base/badges/badges';
|
||||||
import { Table } from '@/components/application/table/table';
|
import { Table } from '@/components/application/table/table';
|
||||||
import { apiClient } from '@/lib/api-client';
|
import { apiClient } from '@/lib/api-client';
|
||||||
@@ -292,7 +292,7 @@ export const TeamPerformancePage = () => {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBar title="Team Performance" />
|
<PageHeader title="Team Performance" infoText="Aggregated metrics across all agents." />
|
||||||
<div className="flex flex-1 items-center justify-center">
|
<div className="flex flex-1 items-center justify-center">
|
||||||
<p className="text-sm text-tertiary">Loading team performance...</p>
|
<p className="text-sm text-tertiary">Loading team performance...</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -302,7 +302,7 @@ export const TeamPerformancePage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBar title="Team Performance" subtitle="Aggregated metrics across all agents" />
|
<PageHeader title="Team Performance" infoText="Aggregated metrics across all agents." />
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col overflow-y-auto">
|
<div className="flex flex-1 flex-col overflow-y-auto">
|
||||||
{/* Section 1: Key Metrics */}
|
{/* Section 1: Key Metrics */}
|
||||||
|
|||||||
Reference in New Issue
Block a user