mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
usePerformanceAlerts now fetches /api/supervisor/performance-alerts every 60s instead of computing client-side. Dismiss + dismiss-all hit the sidecar so state survives reload. Toast fires when new alerts arrive. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
111 lines
5.8 KiB
TypeScript
111 lines
5.8 KiB
TypeScript
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 } from '@/hooks/use-performance-alerts';
|
|
import { cx } from '@/utils/cx';
|
|
|
|
export const NotificationBell = () => {
|
|
const { alerts, dismiss, dismissAll } = usePerformanceAlerts();
|
|
const [open, setOpen] = useState(false);
|
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
|
|
// 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 === 'error' ? 'error' : alert.severity === 'warning' ? 'warning' : 'gray'} 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>
|
|
);
|
|
};
|