mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
Worklist:
- SSE stream replaces 30s poll — EventSource on /api/supervisor/worklist/stream
triggers immediate fetchWorklist() on missed-call events
- Toast notification: 'Missed Call — {name} — needs callback'
- No polling fallback — SSE is the source of truth
Call History split by role:
- Agent: 'My Call History' — own calls only (matched by agent relation
or chain-parsed agentName), missed calls excluded (they belong on
the Call Desk queue), no Agent/Recording/SLA columns, phone clickable
via PhoneActionCell instead of separate Call button
- Supervisor: 'Call History' — all calls, Agent + Recording columns visible
Worklist panel:
- SOURCE/BRANCH column removed from display (data stays on row)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
165 lines
5.6 KiB
TypeScript
165 lines
5.6 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
import { apiClient } from '@/lib/api-client';
|
|
import { notify } from '@/lib/toast';
|
|
|
|
type MissedCall = {
|
|
id: string;
|
|
createdAt: string;
|
|
callDirection: string | null;
|
|
callStatus: string | null;
|
|
callerNumber: { number: string; callingCode: string }[] | null;
|
|
agentName: string | null;
|
|
startedAt: string | null;
|
|
endedAt: string | null;
|
|
durationSeconds: number | null;
|
|
disposition: string | null;
|
|
callNotes: string | null;
|
|
leadId: string | null;
|
|
leadName: string | null;
|
|
callbackStatus: string | null;
|
|
callSourceNumber: string | null;
|
|
missedCallCount: number | null;
|
|
callbackAttemptedAt: string | null;
|
|
};
|
|
|
|
type WorklistFollowUp = {
|
|
id: string;
|
|
createdAt: string | null;
|
|
followUpType: string | null;
|
|
followUpStatus: string | null;
|
|
scheduledAt: string | null;
|
|
completedAt: string | null;
|
|
priority: string | null;
|
|
assignedAgent: string | null;
|
|
patientId: string | null;
|
|
callId: string | null;
|
|
patientName?: string;
|
|
patientPhone?: string;
|
|
};
|
|
|
|
type WorklistLead = {
|
|
id: string;
|
|
createdAt: string;
|
|
contactName: { firstName: string; lastName: string } | null;
|
|
contactPhone: { number: string; callingCode: string }[] | null;
|
|
contactEmail: { address: string }[] | null;
|
|
leadSource: string | null;
|
|
leadStatus: string | null;
|
|
interestedService: string | null;
|
|
assignedAgent: string | null;
|
|
campaignId: string | null;
|
|
adId: string | null;
|
|
contactAttempts: number | null;
|
|
spamScore: number | null;
|
|
isSpam: boolean | null;
|
|
aiSummary: string | null;
|
|
aiSuggestedAction: string | null;
|
|
lastContacted: string | null;
|
|
utmCampaign: string | null;
|
|
};
|
|
|
|
type WorklistData = {
|
|
missedCalls: MissedCall[];
|
|
followUps: WorklistFollowUp[];
|
|
marketingLeads: WorklistLead[];
|
|
totalPending: number;
|
|
};
|
|
|
|
type UseWorklistResult = WorklistData & {
|
|
loading: boolean;
|
|
error: string | null;
|
|
refresh: () => void;
|
|
};
|
|
|
|
const EMPTY_WORKLIST: WorklistData = {
|
|
missedCalls: [],
|
|
followUps: [],
|
|
marketingLeads: [],
|
|
totalPending: 0,
|
|
};
|
|
|
|
export const useWorklist = (): UseWorklistResult => {
|
|
const [data, setData] = useState<WorklistData>(EMPTY_WORKLIST);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchWorklist = useCallback(async () => {
|
|
if (!apiClient.isAuthenticated()) {
|
|
setError('Not authenticated');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const json = await apiClient.get<any>('/api/worklist', { silent: true });
|
|
|
|
// Transform platform field shapes to frontend types
|
|
const transformed: WorklistData = {
|
|
...json,
|
|
marketingLeads: (json.marketingLeads ?? []).map((lead: any) => ({
|
|
...lead,
|
|
leadSource: lead.source ?? lead.leadSource,
|
|
leadStatus: lead.status ?? lead.leadStatus,
|
|
contactPhone: lead.contactPhone?.primaryPhoneNumber
|
|
? [{ number: lead.contactPhone.primaryPhoneNumber, callingCode: lead.contactPhone.primaryPhoneCallingCode ?? '+91' }]
|
|
: lead.contactPhone,
|
|
contactEmail: lead.contactEmail?.primaryEmail
|
|
? [{ address: lead.contactEmail.primaryEmail }]
|
|
: lead.contactEmail,
|
|
})),
|
|
missedCalls: (json.missedCalls ?? []).map((call: any) => ({
|
|
...call,
|
|
callDirection: call.direction ?? call.callDirection,
|
|
durationSeconds: call.durationSec ?? call.durationSeconds ?? 0,
|
|
callerNumber: call.callerNumber?.primaryPhoneNumber
|
|
? [{ number: call.callerNumber.primaryPhoneNumber, callingCode: '+91' }]
|
|
: call.callerNumber,
|
|
})),
|
|
followUps: (json.followUps ?? []).map((fu: any) => ({
|
|
...fu,
|
|
followUpType: fu.typeCustom ?? fu.followUpType,
|
|
followUpStatus: fu.status ?? fu.followUpStatus,
|
|
})),
|
|
};
|
|
setData(transformed);
|
|
setError(null);
|
|
} catch {
|
|
setError('Sidecar not reachable');
|
|
}
|
|
|
|
setLoading(false);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetchWorklist();
|
|
|
|
// SSE stream for instant worklist updates. No polling fallback —
|
|
// if SSE breaks, the worklist stops updating and we fix the SSE,
|
|
// not paper over it with a poll.
|
|
const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4100';
|
|
let es: EventSource | null = null;
|
|
try {
|
|
es = new EventSource(`${API_URL}/api/supervisor/worklist/stream`);
|
|
es.onmessage = (event) => {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
console.log('[WORKLIST-SSE]', data);
|
|
fetchWorklist();
|
|
if (data.type === 'missed-call') {
|
|
const name = data.callerName ?? data.callerPhone ?? 'Unknown';
|
|
notify.warning('Missed Call', `${name} — needs callback`);
|
|
}
|
|
} catch {}
|
|
};
|
|
es.onerror = () => {
|
|
console.warn('[WORKLIST-SSE] Connection error — EventSource will auto-reconnect');
|
|
};
|
|
} catch {}
|
|
|
|
return () => { es?.close(); };
|
|
}, [fetchWorklist]);
|
|
|
|
return { ...data, loading, error, refresh: fetchWorklist };
|
|
};
|