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:
2026-03-30 14:45:52 +05:30
parent 0477064b3e
commit c3c3f4b3d7
18 changed files with 882 additions and 389 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect, useRef } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSidebarFlip, faSidebar, faPhone, faXmark, faDeleteLeft } from '@fortawesome/pro-duotone-svg-icons';
import { useAuth } from '@/providers/auth-provider';
@@ -11,12 +11,13 @@ import { ContextPanel } from '@/components/call-desk/context-panel';
import { ActiveCallCard } from '@/components/call-desk/active-call-card';
import { Badge } from '@/components/base/badges/badges';
import { apiClient } from '@/lib/api-client';
import { notify } from '@/lib/toast';
import { cx } from '@/utils/cx';
export const CallDeskPage = () => {
const { user } = useAuth();
const { leadActivities } = useData();
const { leadActivities, calls, followUps: dataFollowUps, patients, appointments } = useData();
const { callState, callerNumber, callUcid, dialOutbound } = useSip();
const { missedCalls, followUps, marketingLeads, totalPending, loading } = useWorklist();
const [selectedLead, setSelectedLead] = useState<WorklistLead | null>(null);
@@ -49,12 +50,53 @@ export const CallDeskPage = () => {
const isInCall = !callDismissed && (callState === 'ringing-in' || callState === 'ringing-out' || callState === 'active' || callState === 'ended' || callState === 'failed');
const callerLead = callerNumber
? marketingLeads.find((l) => l.contactPhone?.[0]?.number?.endsWith(callerNumber) || callerNumber.endsWith(l.contactPhone?.[0]?.number ?? '---'))
: null;
// Resolve caller identity via sidecar (lookup-or-create lead+patient pair)
const [resolvedCaller, setResolvedCaller] = useState<{
leadId: string; patientId: string; firstName: string; lastName: string; phone: string;
} | null>(null);
const resolveAttemptedRef = useRef<string | null>(null);
// For inbound calls, only use matched lead (don't fall back to previously selected worklist lead)
// For outbound (agent initiated from worklist), selectedLead is the intended target
useEffect(() => {
if (!callerNumber || !isInCall) return;
if (resolveAttemptedRef.current === callerNumber) return; // already resolving/resolved this number
resolveAttemptedRef.current = callerNumber;
apiClient.post<{
leadId: string; patientId: string; firstName: string; lastName: string; phone: string; isNew: boolean;
}>('/api/caller/resolve', { phone: callerNumber }, { silent: true })
.then((result) => {
setResolvedCaller(result);
if (result.isNew) {
notify.info('New Caller', 'Lead and patient records created');
}
})
.catch((err) => {
console.warn('[RESOLVE] Caller resolution failed:', err);
resolveAttemptedRef.current = null; // allow retry
});
}, [callerNumber, isInCall]);
// Reset resolved caller when call ends
useEffect(() => {
if (!isInCall) {
setResolvedCaller(null);
resolveAttemptedRef.current = null;
}
}, [isInCall]);
// Build activeLead from resolved caller or fallback to client-side match
const callerLead = resolvedCaller
? marketingLeads.find((l) => l.id === resolvedCaller.leadId) ?? {
id: resolvedCaller.leadId,
contactName: { firstName: resolvedCaller.firstName, lastName: resolvedCaller.lastName },
contactPhone: [{ number: resolvedCaller.phone, callingCode: '+91' }],
patientId: resolvedCaller.patientId,
}
: callerNumber
? marketingLeads.find((l) => l.contactPhone?.[0]?.number?.endsWith(callerNumber) || callerNumber.endsWith(l.contactPhone?.[0]?.number ?? '---'))
: null;
// For inbound calls, use resolved/matched lead. For outbound, use selectedLead.
const activeLead = isInCall
? (callerLead ?? (callState === 'ringing-out' ? selectedLead : null))
: selectedLead;
@@ -147,7 +189,7 @@ export const CallDeskPage = () => {
{/* Main content */}
<div className="flex flex-1 overflow-hidden">
{/* Main panel */}
<div className="flex flex-1 flex-col min-h-0">
<div className="flex flex-1 flex-col min-h-0 overflow-hidden">
{/* Active call */}
{isInCall && (
<div className="p-5">
@@ -164,6 +206,7 @@ export const CallDeskPage = () => {
loading={loading}
onSelectLead={(lead) => setSelectedLead(lead)}
selectedLeadId={selectedLead?.id ?? null}
onDialMissedCall={(id) => setActiveMissedCallId(id)}
/>
)}
</div>
@@ -177,6 +220,10 @@ export const CallDeskPage = () => {
<ContextPanel
selectedLead={activeLeadFull}
activities={leadActivities}
calls={calls}
followUps={dataFollowUps}
appointments={appointments}
patients={patients}
callerPhone={callerNumber ?? undefined}
isInCall={isInCall}
callUcid={callUcid}