mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
150 lines
5.9 KiB
TypeScript
150 lines
5.9 KiB
TypeScript
import { useState, useCallback, useRef } from 'react';
|
|
import { TopBar } from '@/components/layout/top-bar';
|
|
import { CallSimulator } from '@/components/call-desk/call-simulator';
|
|
import { IncomingCallCard } from '@/components/call-desk/incoming-call-card';
|
|
import { CallLog } from '@/components/call-desk/call-log';
|
|
import { DailyStats } from '@/components/call-desk/daily-stats';
|
|
import { useAuth } from '@/providers/auth-provider';
|
|
import { useData } from '@/providers/data-provider';
|
|
import { useLeads } from '@/hooks/use-leads';
|
|
import type { Call, CallDisposition, LeadStatus } from '@/types/entities';
|
|
|
|
type CallState = 'idle' | 'ringing' | 'active' | 'completed';
|
|
|
|
const isToday = (dateStr: string): boolean => {
|
|
const d = new Date(dateStr);
|
|
const now = new Date();
|
|
return (
|
|
d.getFullYear() === now.getFullYear() &&
|
|
d.getMonth() === now.getMonth() &&
|
|
d.getDate() === now.getDate()
|
|
);
|
|
};
|
|
|
|
const dispositionToStatus: Partial<Record<CallDisposition, LeadStatus>> = {
|
|
APPOINTMENT_BOOKED: 'APPOINTMENT_SET',
|
|
FOLLOW_UP_SCHEDULED: 'CONTACTED',
|
|
INFO_PROVIDED: 'CONTACTED',
|
|
NO_ANSWER: 'NEW',
|
|
WRONG_NUMBER: 'LOST',
|
|
CALLBACK_REQUESTED: 'LOST',
|
|
};
|
|
|
|
export const CallDeskPage = () => {
|
|
const { user } = useAuth();
|
|
const { calls, leadActivities, campaigns, addCall } = useData();
|
|
const { leads, updateLead } = useLeads();
|
|
|
|
const [callState, setCallState] = useState<CallState>('idle');
|
|
const [activeLead, setActiveLead] = useState<ReturnType<typeof useLeads>['leads'][number] | null>(null);
|
|
const [completedDisposition, setCompletedDisposition] = useState<CallDisposition | null>(null);
|
|
const callStartRef = useRef<Date | null>(null);
|
|
const ringingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const completedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const todaysCalls = calls.filter(
|
|
(c) => c.agentName === user.name && c.startedAt !== null && isToday(c.startedAt),
|
|
);
|
|
|
|
const handleSimulateCall = useCallback(() => {
|
|
if (callState !== 'idle') return;
|
|
|
|
// Prefer leads with aiSummary, fall back to any lead
|
|
const leadsWithAi = leads.filter((l) => l.aiSummary !== null);
|
|
const pool = leadsWithAi.length > 0 ? leadsWithAi : leads;
|
|
if (pool.length === 0) return;
|
|
|
|
const randomLead = pool[Math.floor(Math.random() * pool.length)];
|
|
setActiveLead(randomLead);
|
|
setCallState('ringing');
|
|
setCompletedDisposition(null);
|
|
|
|
ringingTimerRef.current = setTimeout(() => {
|
|
setCallState('active');
|
|
callStartRef.current = new Date();
|
|
}, 1500);
|
|
}, [callState, leads]);
|
|
|
|
const handleDisposition = useCallback(
|
|
(disposition: CallDisposition, notes: string) => {
|
|
if (activeLead === null) return;
|
|
|
|
const now = new Date();
|
|
const startedAt = callStartRef.current ?? now;
|
|
const durationSeconds = Math.floor((now.getTime() - startedAt.getTime()) / 1000);
|
|
|
|
const newCall: Call = {
|
|
id: `call-sim-${Date.now()}`,
|
|
createdAt: startedAt.toISOString(),
|
|
callDirection: 'INBOUND',
|
|
callStatus: 'COMPLETED',
|
|
callerNumber: activeLead.contactPhone,
|
|
agentName: user.name,
|
|
startedAt: startedAt.toISOString(),
|
|
endedAt: now.toISOString(),
|
|
durationSeconds: Math.max(durationSeconds, 60),
|
|
recordingUrl: null,
|
|
disposition,
|
|
callNotes: notes || null,
|
|
patientId: null,
|
|
appointmentId: null,
|
|
leadId: activeLead.id,
|
|
leadName:
|
|
`${activeLead.contactName?.firstName ?? ''} ${activeLead.contactName?.lastName ?? ''}`.trim() ||
|
|
'Unknown',
|
|
leadPhone: activeLead.contactPhone?.[0]?.number ?? undefined,
|
|
leadService: activeLead.interestedService ?? undefined,
|
|
};
|
|
|
|
addCall(newCall);
|
|
|
|
const newStatus = dispositionToStatus[disposition];
|
|
if (newStatus !== undefined) {
|
|
updateLead(activeLead.id, {
|
|
leadStatus: newStatus,
|
|
lastContactedAt: now.toISOString(),
|
|
contactAttempts: (activeLead.contactAttempts ?? 0) + 1,
|
|
});
|
|
}
|
|
|
|
setCompletedDisposition(disposition);
|
|
setCallState('completed');
|
|
|
|
completedTimerRef.current = setTimeout(() => {
|
|
setCallState('idle');
|
|
setActiveLead(null);
|
|
setCompletedDisposition(null);
|
|
}, 3000);
|
|
},
|
|
[activeLead, user.name, addCall, updateLead],
|
|
);
|
|
|
|
return (
|
|
<div className="flex flex-1 flex-col">
|
|
<TopBar title="Call Desk" subtitle={`${user.name} \u00B7 Ramaiah Memorial Hospital`} />
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
<div className="flex-1 space-y-5 overflow-y-auto p-7">
|
|
<CallSimulator
|
|
onSimulate={handleSimulateCall}
|
|
isCallActive={callState !== 'idle'}
|
|
/>
|
|
<IncomingCallCard
|
|
callState={callState}
|
|
lead={activeLead}
|
|
activities={leadActivities}
|
|
campaigns={campaigns}
|
|
onDisposition={handleDisposition}
|
|
completedDisposition={completedDisposition}
|
|
/>
|
|
<CallLog calls={todaysCalls} />
|
|
</div>
|
|
|
|
<aside className="hidden w-72 space-y-5 overflow-y-auto border-l border-secondary bg-primary p-5 xl:block">
|
|
<DailyStats calls={todaysCalls} />
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|