From 3a5bbc3f2aa57a30f77365db7d684a33ad4cb5de Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Fri, 20 Mar 2026 18:38:19 +0530 Subject: [PATCH] feat: send disposition to sidecar with UCID for Ozonetel ACW release - Store UCID from outbound dial API response in sipCallUcidAtom - Replace direct createCall GraphQL mutation with sidecar /api/ozonetel/dispose - Remove agent-ready call from handleReset (no longer needed) - Clear UCID on dial error and call reset Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/call-desk/active-call-card.tsx | 41 ++++++++++--------- .../call-desk/click-to-call-button.tsx | 19 +++++++-- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/components/call-desk/active-call-card.tsx b/src/components/call-desk/active-call-card.tsx index 6bf1ccc..f7f89b0 100644 --- a/src/components/call-desk/active-call-card.tsx +++ b/src/components/call-desk/active-call-card.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useRef } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPhone, faPhoneHangup, faMicrophone, faMicrophoneSlash, @@ -7,7 +7,8 @@ import { import { Button } from '@/components/base/buttons/button'; import { Badge } from '@/components/base/badges/badges'; import { useSetAtom } from 'jotai'; -import { sipCallStateAtom, sipCallerNumberAtom } from '@/state/sip-state'; +import { sipCallStateAtom, sipCallerNumberAtom, sipCallUcidAtom } from '@/state/sip-state'; +import { setOutboundPending } from '@/state/sip-manager'; import { useSip } from '@/providers/sip-provider'; import { DispositionForm } from './disposition-form'; import { AppointmentForm } from './appointment-form'; @@ -30,12 +31,15 @@ const formatDuration = (seconds: number): string => { }; export const ActiveCallCard = ({ lead, callerPhone }: ActiveCallCardProps) => { - const { callState, callDuration, isMuted, isOnHold, answer, reject, hangup, toggleMute, toggleHold } = useSip(); + 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 [postCallStage, setPostCallStage] = useState(null); const [savedDisposition, setSavedDisposition] = useState(null); const [appointmentOpen, setAppointmentOpen] = useState(false); + // Capture direction at mount — survives through disposition stage + const callDirectionRef = useRef(callState === 'ringing-out' ? 'OUTBOUND' : 'INBOUND'); const firstName = lead?.contactName?.firstName ?? ''; const lastName = lead?.contactName?.lastName ?? ''; @@ -43,25 +47,20 @@ export const ActiveCallCard = ({ lead, callerPhone }: ActiveCallCardProps) => { const phone = lead?.contactPhone?.[0]; const phoneDisplay = phone ? formatPhone(phone) : callerPhone || 'Unknown'; - const handleDisposition = async (disposition: CallDisposition, _notes: string) => { + const handleDisposition = async (disposition: CallDisposition, notes: string) => { setSavedDisposition(disposition); - // Create call record in platform - try { - await apiClient.graphql(`mutation($data: CallCreateInput!) { createCall(data: $data) { id } }`, { - data: { - name: `${fullName || phoneDisplay} — ${disposition}`, - direction: 'INBOUND', - callStatus: 'COMPLETED', - agentName: null, - startedAt: new Date().toISOString(), - durationSec: callDuration, - disposition, - leadId: lead?.id ?? null, - }, - }, { silent: true }); - } catch { - // non-blocking + // Submit disposition to sidecar — handles Ozonetel ACW release + if (callUcid) { + apiClient.post('/api/ozonetel/dispose', { + ucid: callUcid, + disposition, + callerPhone, + direction: callDirectionRef.current, + durationSec: callDuration, + leadId: lead?.id ?? null, + notes, + }).catch((err) => console.warn('Disposition failed:', err)); } if (disposition === 'APPOINTMENT_BOOKED') { @@ -103,6 +102,8 @@ export const ActiveCallCard = ({ lead, callerPhone }: ActiveCallCardProps) => { setSavedDisposition(null); setCallState('idle'); setCallerNumber(null); + setCallUcid(null); + setOutboundPending(false); }; // Outbound ringing — agent initiated the call diff --git a/src/components/call-desk/click-to-call-button.tsx b/src/components/call-desk/click-to-call-button.tsx index 6a95c92..1821139 100644 --- a/src/components/call-desk/click-to-call-button.tsx +++ b/src/components/call-desk/click-to-call-button.tsx @@ -3,7 +3,9 @@ import { Phone01 } from '@untitledui/icons'; import { useSetAtom } from 'jotai'; import { Button } from '@/components/base/buttons/button'; import { useSip } from '@/providers/sip-provider'; -import { sipCallStateAtom, sipCallerNumberAtom } from '@/state/sip-state'; +import { sipCallStateAtom, sipCallerNumberAtom, sipCallUcidAtom } from '@/state/sip-state'; +import { setOutboundPending } from '@/state/sip-manager'; +import { apiClient } from '@/lib/api-client'; import { notify } from '@/lib/toast'; interface ClickToCallButtonProps { @@ -14,10 +16,11 @@ interface ClickToCallButtonProps { } export const ClickToCallButton = ({ phoneNumber, label, size = 'sm' }: ClickToCallButtonProps) => { - const { isRegistered, isInCall, makeCall } = useSip(); + const { isRegistered, isInCall } = useSip(); const [dialing, setDialing] = useState(false); const setCallState = useSetAtom(sipCallStateAtom); const setCallerNumber = useSetAtom(sipCallerNumberAtom); + const setCallUcid = useSetAtom(sipCallUcidAtom); const handleDial = async () => { setDialing(true); @@ -25,13 +28,21 @@ export const ClickToCallButton = ({ phoneNumber, label, size = 'sm' }: ClickToCa // Show call UI immediately setCallState('ringing-out'); setCallerNumber(phoneNumber); + setOutboundPending(true); + // Safety: reset flag if SIP INVITE doesn't arrive within 30s + const safetyTimer = setTimeout(() => setOutboundPending(false), 30000); try { - // Direct SIP call from browser - makeCall(phoneNumber); + const result = await apiClient.post<{ ucid?: string; status?: string }>('/api/ozonetel/dial', { phoneNumber }); + if (result?.ucid) { + setCallUcid(result.ucid); + } } catch { + clearTimeout(safetyTimer); setCallState('idle'); setCallerNumber(null); + setOutboundPending(false); + setCallUcid(null); notify.error('Dial Failed', 'Could not place the call'); } finally { setDialing(false);