mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +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:
102
src/hooks/use-performance-alerts.ts
Normal file
102
src/hooks/use-performance-alerts.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useData } from '@/providers/data-provider';
|
||||
import { useAuth } from '@/providers/auth-provider';
|
||||
import { notify } from '@/lib/toast';
|
||||
|
||||
export type PerformanceAlert = {
|
||||
id: string;
|
||||
agent: string;
|
||||
type: 'Excessive Idle Time' | 'Low NPS' | 'Low Conversion';
|
||||
value: string;
|
||||
severity: 'error' | 'warning';
|
||||
dismissed: boolean;
|
||||
};
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4100';
|
||||
|
||||
export const usePerformanceAlerts = () => {
|
||||
const { isAdmin } = useAuth();
|
||||
const { calls, leads } = useData();
|
||||
const [alerts, setAlerts] = useState<PerformanceAlert[]>([]);
|
||||
const [teamPerf, setTeamPerf] = useState<any>(null);
|
||||
const toastsFiredRef = useRef(false);
|
||||
|
||||
// Fetch team performance data from sidecar (same as team-performance page)
|
||||
useEffect(() => {
|
||||
if (!isAdmin) return;
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const token = localStorage.getItem('helix_access_token') ?? '';
|
||||
fetch(`${API_URL}/api/supervisor/team-performance?date=${today}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(data => setTeamPerf(data))
|
||||
.catch(() => {});
|
||||
}, [isAdmin]);
|
||||
|
||||
// Compute alerts from team performance + entity data
|
||||
useMemo(() => {
|
||||
if (!isAdmin || !teamPerf?.agents) return;
|
||||
|
||||
const parseTime = (t: string): number => {
|
||||
const parts = t.split(':').map(Number);
|
||||
return (parts[0] ?? 0) * 3600 + (parts[1] ?? 0) * 60 + (parts[2] ?? 0);
|
||||
};
|
||||
|
||||
const list: PerformanceAlert[] = [];
|
||||
let idx = 0;
|
||||
|
||||
for (const agent of teamPerf.agents) {
|
||||
const agentCalls = calls.filter(c => c.agentName === agent.name || c.agentName === agent.ozonetelagentid);
|
||||
const totalCalls = agentCalls.length;
|
||||
const agentAppts = agentCalls.filter((c: any) => c.disposition === 'APPOINTMENT_BOOKED').length;
|
||||
const convPercent = totalCalls > 0 ? Math.round((agentAppts / totalCalls) * 100) : 0;
|
||||
|
||||
const tb = agent.timeBreakdown;
|
||||
const idleMinutes = tb ? Math.round(parseTime(tb.totalIdleTime ?? '0:0:0') / 60) : 0;
|
||||
|
||||
if (agent.maxidleminutes && idleMinutes > agent.maxidleminutes) {
|
||||
list.push({ id: `idle-${idx++}`, agent: agent.name ?? agent.ozonetelagentid, type: 'Excessive Idle Time', value: `${idleMinutes}m`, severity: 'error', dismissed: false });
|
||||
}
|
||||
if (agent.minnpsthreshold && (agent.npsscore ?? 100) < agent.minnpsthreshold) {
|
||||
list.push({ id: `nps-${idx++}`, agent: agent.name ?? agent.ozonetelagentid, type: 'Low NPS', value: String(agent.npsscore ?? 0), severity: 'warning', dismissed: false });
|
||||
}
|
||||
if (agent.minconversionpercent && convPercent < agent.minconversionpercent) {
|
||||
list.push({ id: `conv-${idx++}`, agent: agent.name ?? agent.ozonetelagentid, type: 'Low Conversion', value: `${convPercent}%`, severity: 'warning', dismissed: false });
|
||||
}
|
||||
}
|
||||
|
||||
setAlerts(list);
|
||||
}, [isAdmin, teamPerf, calls, leads]);
|
||||
|
||||
// Fire toasts once when alerts first load
|
||||
useEffect(() => {
|
||||
if (toastsFiredRef.current || alerts.length === 0) return;
|
||||
toastsFiredRef.current = true;
|
||||
|
||||
const idleCount = alerts.filter(a => a.type === 'Excessive Idle Time').length;
|
||||
const npsCount = alerts.filter(a => a.type === 'Low NPS').length;
|
||||
const convCount = alerts.filter(a => a.type === 'Low Conversion').length;
|
||||
|
||||
const parts: string[] = [];
|
||||
if (idleCount > 0) parts.push(`${idleCount} excessive idle`);
|
||||
if (npsCount > 0) parts.push(`${npsCount} low NPS`);
|
||||
if (convCount > 0) parts.push(`${convCount} low conversion`);
|
||||
|
||||
if (parts.length > 0) {
|
||||
notify.error('Performance Alerts', `${alerts.length} alert(s): ${parts.join(', ')}`);
|
||||
}
|
||||
}, [alerts]);
|
||||
|
||||
const dismiss = (id: string) => {
|
||||
setAlerts(prev => prev.map(a => a.id === id ? { ...a, dismissed: true } : a));
|
||||
};
|
||||
|
||||
const dismissAll = () => {
|
||||
setAlerts(prev => prev.map(a => ({ ...a, dismissed: true })));
|
||||
};
|
||||
|
||||
const activeAlerts = alerts.filter(a => !a.dismissed);
|
||||
|
||||
return { alerts: activeAlerts, allAlerts: alerts, dismiss, dismissAll };
|
||||
};
|
||||
Reference in New Issue
Block a user