mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-12 02:38:15 +00:00
feat: worklist sorting, contextual disposition, context panel redesign, notifications
- Worklist default sort descending (newest first), sortable column headers (PRIORITY, PATIENT, SLA) via React Aria - Contextual disposition: auto-selects based on in-call actions (appointment → APPOINTMENT_BOOKED, enquiry → INFO_PROVIDED, transfer → FOLLOW_UP_SCHEDULED) - Context panel redesign: collapsible AI Insight, Upcoming (appointments + follow-ups + linked patient), Recent (calls + activities) sections; auto-collapse on AI chat start - Appointments added to DataProvider with APPOINTMENTS_QUERY, Appointment type, transform - Notification bell for admin/supervisor: performance alerts (idle time, NPS, conversion thresholds) with toast on load + bell dropdown with dismiss; demo alerts as fallback - Slideout z-index fix: added z-50 to slideout ModalOverlay matching modal component Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import { useSip } from '@/providers/sip-provider';
|
||||
import { CallWidget } from '@/components/call-desk/call-widget';
|
||||
import { MaintOtpModal } from '@/components/modals/maint-otp-modal';
|
||||
import { AgentStatusToggle } from '@/components/call-desk/agent-status-toggle';
|
||||
import { NotificationBell } from './notification-bell';
|
||||
import { useAuth } from '@/providers/auth-provider';
|
||||
import { useMaintShortcuts } from '@/hooks/use-maint-shortcuts';
|
||||
import { useNetworkStatus } from '@/hooks/use-network-status';
|
||||
@@ -19,7 +20,7 @@ interface AppShellProps {
|
||||
|
||||
export const AppShell = ({ children }: AppShellProps) => {
|
||||
const { pathname } = useLocation();
|
||||
const { isCCAgent } = useAuth();
|
||||
const { isCCAgent, isAdmin } = useAuth();
|
||||
const { isOpen, activeAction, close } = useMaintShortcuts();
|
||||
const { connectionStatus, isRegistered } = useSip();
|
||||
const networkQuality = useNetworkStatus();
|
||||
@@ -50,23 +51,28 @@ export const AppShell = ({ children }: AppShellProps) => {
|
||||
<Sidebar activeUrl={pathname} />
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
{/* Persistent top bar — visible on all pages */}
|
||||
{hasAgentConfig && (
|
||||
{(hasAgentConfig || isAdmin) && (
|
||||
<div className="flex shrink-0 items-center justify-end gap-2 border-b border-secondary px-4 py-2">
|
||||
<div className={cx(
|
||||
'flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium',
|
||||
networkQuality === 'good'
|
||||
? 'bg-success-primary text-success-primary'
|
||||
: networkQuality === 'offline'
|
||||
? 'bg-error-secondary text-error-primary'
|
||||
: 'bg-warning-secondary text-warning-primary',
|
||||
)}>
|
||||
<FontAwesomeIcon
|
||||
icon={networkQuality === 'offline' ? faWifiSlash : faWifi}
|
||||
className="size-3"
|
||||
/>
|
||||
{networkQuality === 'good' ? 'Connected' : networkQuality === 'offline' ? 'No connection' : 'Unstable'}
|
||||
</div>
|
||||
<AgentStatusToggle isRegistered={isRegistered} connectionStatus={connectionStatus} />
|
||||
{isAdmin && <NotificationBell />}
|
||||
{hasAgentConfig && (
|
||||
<>
|
||||
<div className={cx(
|
||||
'flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium',
|
||||
networkQuality === 'good'
|
||||
? 'bg-success-primary text-success-primary'
|
||||
: networkQuality === 'offline'
|
||||
? 'bg-error-secondary text-error-primary'
|
||||
: 'bg-warning-secondary text-warning-primary',
|
||||
)}>
|
||||
<FontAwesomeIcon
|
||||
icon={networkQuality === 'offline' ? faWifiSlash : faWifi}
|
||||
className="size-3"
|
||||
/>
|
||||
{networkQuality === 'good' ? 'Connected' : networkQuality === 'offline' ? 'No connection' : 'Unstable'}
|
||||
</div>
|
||||
<AgentStatusToggle isRegistered={isRegistered} connectionStatus={connectionStatus} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<main className="flex flex-1 flex-col overflow-hidden">{children}</main>
|
||||
|
||||
142
src/components/layout/notification-bell.tsx
Normal file
142
src/components/layout/notification-bell.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faBell, faTriangleExclamation, faXmark, faCheck } from '@fortawesome/pro-duotone-svg-icons';
|
||||
import { Badge } from '@/components/base/badges/badges';
|
||||
import { usePerformanceAlerts, type PerformanceAlert } from '@/hooks/use-performance-alerts';
|
||||
import { cx } from '@/utils/cx';
|
||||
|
||||
const DEMO_ALERTS: PerformanceAlert[] = [
|
||||
{ id: 'demo-1', agent: 'Riya Mehta', type: 'Excessive Idle Time', value: '120m', severity: 'error', dismissed: false },
|
||||
{ id: 'demo-2', agent: 'Arjun Kapoor', type: 'Excessive Idle Time', value: '180m', severity: 'error', dismissed: false },
|
||||
{ id: 'demo-3', agent: 'Sneha Iyer', type: 'Excessive Idle Time', value: '250m', severity: 'error', dismissed: false },
|
||||
{ id: 'demo-4', agent: 'Vikrant Desai', type: 'Excessive Idle Time', value: '300m', severity: 'error', dismissed: false },
|
||||
{ id: 'demo-5', agent: 'Vikrant Desai', type: 'Low NPS', value: '35', severity: 'warning', dismissed: false },
|
||||
{ id: 'demo-6', agent: 'Vikrant Desai', type: 'Low Conversion', value: '40%', severity: 'warning', dismissed: false },
|
||||
{ id: 'demo-7', agent: 'Pooja Rao', type: 'Excessive Idle Time', value: '200m', severity: 'error', dismissed: false },
|
||||
{ id: 'demo-8', agent: 'Mohammed Rizwan', type: 'Excessive Idle Time', value: '80m', severity: 'error', dismissed: false },
|
||||
];
|
||||
|
||||
export const NotificationBell = () => {
|
||||
const { alerts: liveAlerts, dismiss: liveDismiss, dismissAll: liveDismissAll } = usePerformanceAlerts();
|
||||
const [demoAlerts, setDemoAlerts] = useState<PerformanceAlert[]>(DEMO_ALERTS);
|
||||
const [open, setOpen] = useState(true);
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Use live alerts if available, otherwise demo
|
||||
const alerts = liveAlerts.length > 0 ? liveAlerts : demoAlerts.filter(a => !a.dismissed);
|
||||
const isDemo = liveAlerts.length === 0;
|
||||
|
||||
const dismiss = (id: string) => {
|
||||
if (isDemo) {
|
||||
setDemoAlerts(prev => prev.map(a => a.id === id ? { ...a, dismissed: true } : a));
|
||||
} else {
|
||||
liveDismiss(id);
|
||||
}
|
||||
};
|
||||
|
||||
const dismissAll = () => {
|
||||
if (isDemo) {
|
||||
setDemoAlerts(prev => prev.map(a => ({ ...a, dismissed: true })));
|
||||
} else {
|
||||
liveDismissAll();
|
||||
}
|
||||
};
|
||||
|
||||
// Close on outside click
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={panelRef}>
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className={cx(
|
||||
'relative flex size-9 items-center justify-center rounded-lg border transition duration-100 ease-linear',
|
||||
alerts.length > 0
|
||||
? 'border-error bg-error-primary text-fg-error-primary hover:bg-error-secondary'
|
||||
: open
|
||||
? 'border-brand bg-active text-brand-secondary'
|
||||
: 'border-secondary bg-primary text-fg-secondary hover:bg-primary_hover',
|
||||
)}
|
||||
title="Notifications"
|
||||
>
|
||||
<FontAwesomeIcon icon={faBell} className="size-5" />
|
||||
{alerts.length > 0 && (
|
||||
<span className="absolute -top-1.5 -right-1.5 flex size-5 items-center justify-center rounded-full bg-error-solid text-[10px] font-bold text-white ring-2 ring-white">
|
||||
{alerts.length > 9 ? '9+' : alerts.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute top-full right-0 mt-2 w-96 rounded-xl bg-primary shadow-xl ring-1 ring-secondary z-50 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-secondary">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-primary">Notifications</span>
|
||||
{alerts.length > 0 && (
|
||||
<Badge size="sm" color="error" type="pill-color">{alerts.length}</Badge>
|
||||
)}
|
||||
</div>
|
||||
{alerts.length > 0 && (
|
||||
<button
|
||||
onClick={dismissAll}
|
||||
className="text-xs font-medium text-brand-secondary hover:text-brand-secondary_hover transition duration-100 ease-linear"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Alert list */}
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{alerts.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<FontAwesomeIcon icon={faCheck} className="size-5 text-fg-success-primary mb-2" />
|
||||
<p className="text-xs text-tertiary">No active alerts</p>
|
||||
</div>
|
||||
) : (
|
||||
alerts.map(alert => (
|
||||
<div
|
||||
key={alert.id}
|
||||
className={cx(
|
||||
'flex items-center gap-3 px-4 py-3 border-b border-secondary last:border-b-0',
|
||||
alert.severity === 'error' ? 'bg-error-primary' : 'bg-warning-primary',
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faTriangleExclamation}
|
||||
className={cx(
|
||||
'size-4 shrink-0',
|
||||
alert.severity === 'error' ? 'text-fg-error-primary' : 'text-fg-warning-primary',
|
||||
)}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-primary">{alert.agent}</p>
|
||||
<p className="text-xs text-tertiary">{alert.type}</p>
|
||||
</div>
|
||||
<Badge size="sm" color={alert.severity} type="pill-color">{alert.value}</Badge>
|
||||
<button
|
||||
onClick={() => dismiss(alert.id)}
|
||||
className="text-fg-quaternary hover:text-fg-secondary shrink-0 transition duration-100 ease-linear"
|
||||
title="Dismiss"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user