import { useState, useRef, useEffect } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPhone, faPhoneHangup, faMicrophone, faMicrophoneSlash, faPause, faPlay, faCalendarPlus, faPhoneArrowRight, faRecordVinyl, faClipboardQuestion, } from '@fortawesome/pro-duotone-svg-icons'; import { Button } from '@/components/base/buttons/button'; import { Badge } from '@/components/base/badges/badges'; import { useSetAtom } from 'jotai'; import { sipCallStateAtom, sipCallerNumberAtom, sipCallUcidAtom } from '@/state/sip-state'; import { setOutboundPending } from '@/state/sip-manager'; import { useSip } from '@/providers/sip-provider'; import { DispositionModal } from './disposition-modal'; import { AppointmentForm } from './appointment-form'; import { TransferDialog } from './transfer-dialog'; import { EnquiryForm } from './enquiry-form'; import { formatPhone } from '@/lib/format'; import { apiClient } from '@/lib/api-client'; import { useAuth } from '@/providers/auth-provider'; import { cx } from '@/utils/cx'; import { notify } from '@/lib/toast'; import type { Lead, CallDisposition } from '@/types/entities'; interface ActiveCallCardProps { lead: Lead | null; callerPhone: string; missedCallId?: string | null; onCallComplete?: () => void; } const formatDuration = (seconds: number): string => { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m}:${s.toString().padStart(2, '0')}`; }; export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete }: ActiveCallCardProps) => { const { user } = useAuth(); const { callState, callDuration, callUcid, isMuted, isOnHold, answer, reject, hangup, toggleMute, toggleHold } = useSip(); const setCallState = useSetAtom(sipCallStateAtom); const setCallerNumber = useSetAtom(sipCallerNumberAtom); const setCallUcid = useSetAtom(sipCallUcidAtom); const [appointmentOpen, setAppointmentOpen] = useState(false); const [transferOpen, setTransferOpen] = useState(false); const [recordingPaused, setRecordingPaused] = useState(false); const [enquiryOpen, setEnquiryOpen] = useState(false); const [dispositionOpen, setDispositionOpen] = useState(false); const [callerDisconnected, setCallerDisconnected] = useState(false); const [suggestedDisposition, setSuggestedDisposition] = useState(null); const callDirectionRef = useRef(callState === 'ringing-out' ? 'OUTBOUND' : 'INBOUND'); const wasAnsweredRef = useRef(callState === 'active'); useEffect(() => { console.log(`[ACTIVE-CALL-CARD] Mounted: state=${callState} direction=${callDirectionRef.current} ucid=${callUcid ?? 'none'} lead=${lead?.id ?? 'none'} phone=${callerPhone}`); }, []); // eslint-disable-line react-hooks/exhaustive-deps // Detect caller disconnect: call was active and ended without agent pressing End useEffect(() => { if (wasAnsweredRef.current && !dispositionOpen && (callState === 'ended' || callState === 'failed')) { setCallerDisconnected(true); setDispositionOpen(true); } }, [callState, dispositionOpen]); const firstName = lead?.contactName?.firstName ?? ''; const lastName = lead?.contactName?.lastName ?? ''; const fullName = `${firstName} ${lastName}`.trim(); const phone = lead?.contactPhone?.[0]; const phoneDisplay = phone ? formatPhone(phone) : callerPhone || 'Unknown'; const handleDisposition = async (disposition: CallDisposition, notes: string) => { // Hangup if still connected if (callState === 'active' || callState === 'ringing-out' || callState === 'ringing-in') { hangup(); } // Submit disposition to sidecar if (callUcid) { const disposePayload = { ucid: callUcid, disposition, callerPhone, direction: callDirectionRef.current, durationSec: callDuration, leadId: lead?.id ?? null, notes, missedCallId: missedCallId ?? undefined, }; console.log('[DISPOSE] Sending disposition:', JSON.stringify(disposePayload)); apiClient.post('/api/ozonetel/dispose', disposePayload) .then((res) => console.log('[DISPOSE] Response:', JSON.stringify(res))) .catch((err) => console.error('[DISPOSE] Failed:', err)); } else { console.warn('[DISPOSE] No callUcid — skipping disposition'); } // Side effects if (disposition === 'FOLLOW_UP_SCHEDULED') { try { await apiClient.graphql(`mutation($data: FollowUpCreateInput!) { createFollowUp(data: $data) { id } }`, { data: { name: `Follow-up — ${fullName || phoneDisplay}`, typeCustom: 'CALLBACK', status: 'PENDING', assignedAgent: null, priority: 'NORMAL', scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), }, }, { silent: true }); notify.success('Follow-up Created', 'Callback scheduled for tomorrow'); } catch { notify.info('Follow-up', 'Could not auto-create follow-up'); } } notify.success('Call Logged', `Disposition: ${disposition.replace(/_/g, ' ').toLowerCase()}`); handleReset(); }; const handleAppointmentSaved = () => { setAppointmentOpen(false); setSuggestedDisposition('APPOINTMENT_BOOKED'); notify.success('Appointment Booked', 'Payment link will be sent to the patient'); }; const handleReset = () => { setDispositionOpen(false); setCallerDisconnected(false); setCallState('idle'); setCallerNumber(null); setCallUcid(null); setOutboundPending(false); onCallComplete?.(); }; // Outbound ringing if (callState === 'ringing-out') { return (

Calling...

{fullName || phoneDisplay}

{fullName &&

{phoneDisplay}

}
); } // Inbound ringing if (callState === 'ringing-in') { return (

Incoming Call

{fullName || phoneDisplay}

{fullName &&

{phoneDisplay}

}
); } // Unanswered call (ringing → ended without ever reaching active) if (!wasAnsweredRef.current && (callState === 'ended' || callState === 'failed')) { return (

Missed Call

{phoneDisplay} — not answered

); } // Active call if (callState === 'active' || dispositionOpen) { wasAnsweredRef.current = true; return ( <>
{/* Pinned: caller info + controls */}

{fullName || phoneDisplay}

{fullName &&

{phoneDisplay}

}
{formatDuration(callDuration)}
{/* Call controls */}
{/* Scrollable: expanded forms + transfer */} {(appointmentOpen || enquiryOpen || transferOpen) && (
{transferOpen && callUcid && ( setTransferOpen(false)} onTransferred={() => { setTransferOpen(false); setSuggestedDisposition('FOLLOW_UP_SCHEDULED'); setDispositionOpen(true); }} /> )} { setEnquiryOpen(false); setSuggestedDisposition('INFO_PROVIDED'); notify.success('Enquiry Logged'); }} />
)}
{/* Disposition Modal — the ONLY path to end a call */} { // Agent wants to continue the call — close modal, call stays active if (!callerDisconnected) { setDispositionOpen(false); } else { // Caller already disconnected — dismiss goes to worklist handleReset(); } }} /> ); } return null; };