mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
- Call-desk: active-call-card supervisor presence badges, incoming-call-card polish, transfer-dialog, call-log - Disposition modal: auto-lock based on actions taken, not-interested split - Forms: appointment-form + enquiry-form improvements (placeholder handling, phone format) - Worklist-panel: pagination awareness, filter chips - Pages: all-leads/patients/patient-360/missed-calls/team-performance/call-history/appointments polish - SIP: sip-client reconnect, sip-provider + sip-manager state, agent-status-toggle spinner - Hooks: use-agent-state supervisor SSE events, use-worklist, use-performance-alerts - Types: entities.ts extended Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
143 lines
4.6 KiB
TypeScript
143 lines
4.6 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
import { apiClient } from '@/lib/api-client';
|
|
|
|
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();
|
|
|
|
// Refresh every 30 seconds
|
|
const interval = setInterval(fetchWorklist, 30000);
|
|
return () => clearInterval(interval);
|
|
}, [fetchWorklist]);
|
|
|
|
return { ...data, loading, error, refresh: fetchWorklist };
|
|
};
|