mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import {
|
import {
|
||||||
faPhone, faPhoneHangup, faMicrophone, faMicrophoneSlash,
|
faPhone, faPhoneHangup, faMicrophone, faMicrophoneSlash,
|
||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
import { Button } from '@/components/base/buttons/button';
|
import { Button } from '@/components/base/buttons/button';
|
||||||
import { Badge } from '@/components/base/badges/badges';
|
import { Badge } from '@/components/base/badges/badges';
|
||||||
import { useSetAtom } from 'jotai';
|
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 { useSip } from '@/providers/sip-provider';
|
||||||
import { DispositionForm } from './disposition-form';
|
import { DispositionForm } from './disposition-form';
|
||||||
import { AppointmentForm } from './appointment-form';
|
import { AppointmentForm } from './appointment-form';
|
||||||
@@ -30,12 +31,15 @@ const formatDuration = (seconds: number): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ActiveCallCard = ({ lead, callerPhone }: ActiveCallCardProps) => {
|
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 setCallState = useSetAtom(sipCallStateAtom);
|
||||||
const setCallerNumber = useSetAtom(sipCallerNumberAtom);
|
const setCallerNumber = useSetAtom(sipCallerNumberAtom);
|
||||||
|
const setCallUcid = useSetAtom(sipCallUcidAtom);
|
||||||
const [postCallStage, setPostCallStage] = useState<PostCallStage | null>(null);
|
const [postCallStage, setPostCallStage] = useState<PostCallStage | null>(null);
|
||||||
const [savedDisposition, setSavedDisposition] = useState<CallDisposition | null>(null);
|
const [savedDisposition, setSavedDisposition] = useState<CallDisposition | null>(null);
|
||||||
const [appointmentOpen, setAppointmentOpen] = useState(false);
|
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 firstName = lead?.contactName?.firstName ?? '';
|
||||||
const lastName = lead?.contactName?.lastName ?? '';
|
const lastName = lead?.contactName?.lastName ?? '';
|
||||||
@@ -43,25 +47,20 @@ export const ActiveCallCard = ({ lead, callerPhone }: ActiveCallCardProps) => {
|
|||||||
const phone = lead?.contactPhone?.[0];
|
const phone = lead?.contactPhone?.[0];
|
||||||
const phoneDisplay = phone ? formatPhone(phone) : callerPhone || 'Unknown';
|
const phoneDisplay = phone ? formatPhone(phone) : callerPhone || 'Unknown';
|
||||||
|
|
||||||
const handleDisposition = async (disposition: CallDisposition, _notes: string) => {
|
const handleDisposition = async (disposition: CallDisposition, notes: string) => {
|
||||||
setSavedDisposition(disposition);
|
setSavedDisposition(disposition);
|
||||||
|
|
||||||
// Create call record in platform
|
// Submit disposition to sidecar — handles Ozonetel ACW release
|
||||||
try {
|
if (callUcid) {
|
||||||
await apiClient.graphql(`mutation($data: CallCreateInput!) { createCall(data: $data) { id } }`, {
|
apiClient.post('/api/ozonetel/dispose', {
|
||||||
data: {
|
ucid: callUcid,
|
||||||
name: `${fullName || phoneDisplay} — ${disposition}`,
|
disposition,
|
||||||
direction: 'INBOUND',
|
callerPhone,
|
||||||
callStatus: 'COMPLETED',
|
direction: callDirectionRef.current,
|
||||||
agentName: null,
|
durationSec: callDuration,
|
||||||
startedAt: new Date().toISOString(),
|
leadId: lead?.id ?? null,
|
||||||
durationSec: callDuration,
|
notes,
|
||||||
disposition,
|
}).catch((err) => console.warn('Disposition failed:', err));
|
||||||
leadId: lead?.id ?? null,
|
|
||||||
},
|
|
||||||
}, { silent: true });
|
|
||||||
} catch {
|
|
||||||
// non-blocking
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (disposition === 'APPOINTMENT_BOOKED') {
|
if (disposition === 'APPOINTMENT_BOOKED') {
|
||||||
@@ -103,6 +102,8 @@ export const ActiveCallCard = ({ lead, callerPhone }: ActiveCallCardProps) => {
|
|||||||
setSavedDisposition(null);
|
setSavedDisposition(null);
|
||||||
setCallState('idle');
|
setCallState('idle');
|
||||||
setCallerNumber(null);
|
setCallerNumber(null);
|
||||||
|
setCallUcid(null);
|
||||||
|
setOutboundPending(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Outbound ringing — agent initiated the call
|
// Outbound ringing — agent initiated the call
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { Phone01 } from '@untitledui/icons';
|
|||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { Button } from '@/components/base/buttons/button';
|
import { Button } from '@/components/base/buttons/button';
|
||||||
import { useSip } from '@/providers/sip-provider';
|
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';
|
import { notify } from '@/lib/toast';
|
||||||
|
|
||||||
interface ClickToCallButtonProps {
|
interface ClickToCallButtonProps {
|
||||||
@@ -14,10 +16,11 @@ interface ClickToCallButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ClickToCallButton = ({ phoneNumber, label, size = 'sm' }: ClickToCallButtonProps) => {
|
export const ClickToCallButton = ({ phoneNumber, label, size = 'sm' }: ClickToCallButtonProps) => {
|
||||||
const { isRegistered, isInCall, makeCall } = useSip();
|
const { isRegistered, isInCall } = useSip();
|
||||||
const [dialing, setDialing] = useState(false);
|
const [dialing, setDialing] = useState(false);
|
||||||
const setCallState = useSetAtom(sipCallStateAtom);
|
const setCallState = useSetAtom(sipCallStateAtom);
|
||||||
const setCallerNumber = useSetAtom(sipCallerNumberAtom);
|
const setCallerNumber = useSetAtom(sipCallerNumberAtom);
|
||||||
|
const setCallUcid = useSetAtom(sipCallUcidAtom);
|
||||||
|
|
||||||
const handleDial = async () => {
|
const handleDial = async () => {
|
||||||
setDialing(true);
|
setDialing(true);
|
||||||
@@ -25,13 +28,21 @@ export const ClickToCallButton = ({ phoneNumber, label, size = 'sm' }: ClickToCa
|
|||||||
// Show call UI immediately
|
// Show call UI immediately
|
||||||
setCallState('ringing-out');
|
setCallState('ringing-out');
|
||||||
setCallerNumber(phoneNumber);
|
setCallerNumber(phoneNumber);
|
||||||
|
setOutboundPending(true);
|
||||||
|
// Safety: reset flag if SIP INVITE doesn't arrive within 30s
|
||||||
|
const safetyTimer = setTimeout(() => setOutboundPending(false), 30000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Direct SIP call from browser
|
const result = await apiClient.post<{ ucid?: string; status?: string }>('/api/ozonetel/dial', { phoneNumber });
|
||||||
makeCall(phoneNumber);
|
if (result?.ucid) {
|
||||||
|
setCallUcid(result.ucid);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
clearTimeout(safetyTimer);
|
||||||
setCallState('idle');
|
setCallState('idle');
|
||||||
setCallerNumber(null);
|
setCallerNumber(null);
|
||||||
|
setOutboundPending(false);
|
||||||
|
setCallUcid(null);
|
||||||
notify.error('Dial Failed', 'Could not place the call');
|
notify.error('Dial Failed', 'Could not place the call');
|
||||||
} finally {
|
} finally {
|
||||||
setDialing(false);
|
setDialing(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user