import { useState, useEffect, useRef } from 'react'; import { Phone01, PhoneIncoming01, PhoneOutgoing01, PhoneHangUp, PhoneX, MicrophoneOff01, Microphone01, PauseCircle, CheckCircle, Save01, } from '@untitledui/icons'; import { Button } from '@/components/base/buttons/button'; import { TextArea } from '@/components/base/textarea/textarea'; import { useSip } from '@/providers/sip-provider'; import { useAuth } from '@/providers/auth-provider'; import { cx } from '@/utils/cx'; import type { CallDisposition } from '@/types/entities'; const formatDuration = (seconds: number): string => { const m = Math.floor(seconds / 60) .toString() .padStart(2, '0'); const s = (seconds % 60).toString().padStart(2, '0'); return `${m}:${s}`; }; const statusDotColor: Record = { registered: 'bg-success-500', connecting: 'bg-warning-500', disconnected: 'bg-quaternary', error: 'bg-error-500', }; const statusLabel: Record = { registered: 'Ready', connecting: 'Connecting...', disconnected: 'Offline', error: 'Error', }; const dispositionOptions: Array<{ value: CallDisposition; label: string; activeClass: string; defaultClass: string; }> = [ { value: 'APPOINTMENT_BOOKED', label: 'Appt Booked', activeClass: 'bg-success-solid text-white ring-transparent', defaultClass: 'bg-success-primary text-success-primary border-success', }, { value: 'FOLLOW_UP_SCHEDULED', label: 'Follow-up', activeClass: 'bg-brand-solid text-white ring-transparent', defaultClass: 'bg-brand-primary text-brand-secondary border-brand', }, { value: 'INFO_PROVIDED', label: 'Info Given', activeClass: 'bg-utility-blue-light-600 text-white ring-transparent', defaultClass: 'bg-utility-blue-light-50 text-utility-blue-light-700 border-utility-blue-light-200', }, { value: 'NO_ANSWER', label: 'No Answer', activeClass: 'bg-warning-solid text-white ring-transparent', defaultClass: 'bg-warning-primary text-warning-primary border-warning', }, { value: 'WRONG_NUMBER', label: 'Wrong #', activeClass: 'bg-secondary-solid text-white ring-transparent', defaultClass: 'bg-secondary text-secondary border-secondary', }, { value: 'CALLBACK_REQUESTED', label: 'Not Interested', activeClass: 'bg-error-solid text-white ring-transparent', defaultClass: 'bg-error-primary text-error-primary border-error', }, ]; export const CallWidget = () => { const { connectionStatus, callState, callerNumber, isMuted, isOnHold, callDuration, answer, reject, hangup, toggleMute, toggleHold, } = useSip(); const { user } = useAuth(); const [disposition, setDisposition] = useState(null); const [notes, setNotes] = useState(''); const [lastDuration, setLastDuration] = useState(0); const [matchedLead, setMatchedLead] = useState(null); const [leadActivities, setLeadActivities] = useState([]); const [isSaving, setIsSaving] = useState(false); const callStartTimeRef = useRef(null); // Capture duration right before call ends useEffect(() => { if (callState === 'active' && callDuration > 0) { setLastDuration(callDuration); } }, [callState, callDuration]); // Track call start time useEffect(() => { if (callState === 'active' && !callStartTimeRef.current) { callStartTimeRef.current = new Date().toISOString(); } if (callState === 'idle') { callStartTimeRef.current = null; } }, [callState]); // Look up caller when call becomes active useEffect(() => { if (callState === 'ringing-in' && callerNumber && callerNumber !== 'Unknown') { const lookup = async () => { try { const { apiClient } = await import('@/lib/api-client'); const token = apiClient.getStoredToken(); if (!token) return; const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4100'; const res = await fetch(`${API_URL}/api/call/lookup`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ phoneNumber: callerNumber }), }); const data = await res.json(); if (data.matched && data.lead) { setMatchedLead(data.lead); setLeadActivities(data.activities ?? []); } } catch (err) { console.warn('Lead lookup failed:', err); } }; lookup(); } }, [callState, callerNumber]); // Reset state when returning to idle useEffect(() => { if (callState === 'idle') { setDisposition(null); setNotes(''); setMatchedLead(null); setLeadActivities([]); } }, [callState]); const handleSaveAndClose = async () => { if (!disposition) return; setIsSaving(true); try { const { apiClient } = await import('@/lib/api-client'); // 1. Create Call record on platform await apiClient.graphql( `mutation CreateCall($data: CallCreateInput!) { createCall(data: $data) { id } }`, { data: { callDirection: 'INBOUND', callStatus: 'COMPLETED', agentName: user.name, startedAt: callStartTimeRef.current, endedAt: new Date().toISOString(), durationSeconds: callDuration, disposition, callNotes: notes || null, leadId: matchedLead?.id ?? null, }, }, ).catch(err => console.warn('Failed to create call record:', err)); // 2. Update lead status if matched if (matchedLead?.id) { const statusMap: Partial> = { APPOINTMENT_BOOKED: 'APPOINTMENT_SET', FOLLOW_UP_SCHEDULED: 'CONTACTED', INFO_PROVIDED: 'CONTACTED', NO_ANSWER: 'CONTACTED', WRONG_NUMBER: 'LOST', CALLBACK_REQUESTED: 'CONTACTED', NOT_INTERESTED: 'LOST', }; const newStatus = statusMap[disposition]; if (newStatus) { await apiClient.graphql( `mutation UpdateLead($id: ID!, $data: LeadUpdateInput!) { updateLead(id: $id, data: $data) { id } }`, { id: matchedLead.id, data: { leadStatus: newStatus, lastContactedAt: new Date().toISOString(), }, }, ).catch(err => console.warn('Failed to update lead:', err)); } // 3. Create lead activity await apiClient.graphql( `mutation CreateLeadActivity($data: LeadActivityCreateInput!) { createLeadActivity(data: $data) { id } }`, { data: { activityType: 'CALL_RECEIVED', summary: `Inbound call — ${disposition.replace(/_/g, ' ')}`, occurredAt: new Date().toISOString(), performedBy: user.name, channel: 'PHONE', durationSeconds: callDuration, leadId: matchedLead.id, }, }, ).catch(err => console.warn('Failed to create activity:', err)); } } catch (err) { console.error('Save failed:', err); } setIsSaving(false); hangup(); setDisposition(null); setNotes(''); }; const dotColor = statusDotColor[connectionStatus] ?? 'bg-quaternary'; const label = statusLabel[connectionStatus] ?? connectionStatus; // Idle: collapsed pill if (callState === 'idle') { return (
{label} Helix Phone
); } // Ringing inbound if (callState === 'ringing-in') { return (
Incoming Call {callerNumber ?? 'Unknown'}
); } // Ringing outbound if (callState === 'ringing-out') { return (
Calling... {callerNumber ?? 'Unknown'}
); } // Active call (full widget) if (callState === 'active') { return (
{/* Header */}
Active Call
{formatDuration(callDuration)}
{/* Caller info */}
{matchedLead?.contactName ? `${matchedLead.contactName.firstName ?? ''} ${matchedLead.contactName.lastName ?? ''}`.trim() : callerNumber ?? 'Unknown'} {matchedLead && ( {callerNumber} )}
{/* AI Summary */} {matchedLead?.aiSummary && (
AI Insight

{matchedLead.aiSummary}

{matchedLead.aiSuggestedAction && ( {matchedLead.aiSuggestedAction} )}
)} {/* Recent activity */} {leadActivities.length > 0 && (
Recent Activity
{leadActivities.slice(0, 3).map((a: any, i: number) => (
{a.activityType?.replace(/_/g, ' ')}: {a.summary}
))}
)} {/* Call controls */}
{/* Divider */}
{/* Disposition */}
Disposition
{dispositionOptions.map((opt) => { const isSelected = disposition === opt.value; return ( ); })}