@campxdev/campx-web-utils 2.0.14 → 2.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/types/src/components/Exotel/ExotelPhone.d.ts +2 -2
- package/dist/cjs/types/src/services/exotel/api.d.ts +1 -0
- package/dist/esm/index.js +2 -2
- package/dist/esm/types/src/components/Exotel/ExotelPhone.d.ts +2 -2
- package/dist/esm/types/src/services/exotel/api.d.ts +1 -0
- package/dist/index.d.ts +2 -1
- package/package.json +1 -1
- package/src/components/Exotel/CallButton.tsx +35 -1
- package/src/components/Exotel/CallDispositionForm.tsx +1 -1
- package/src/components/Exotel/ExotelPhone.tsx +109 -104
- package/src/providers/ExotelProvider.tsx +10 -2
- package/src/services/exotel/api.ts +131 -159
- package/src/utils/constants.ts +1 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DispositionFormData } from './CallDispositionForm';
|
|
2
2
|
interface ExotelPhoneProps {
|
|
3
3
|
className?: string;
|
|
4
4
|
prospectId?: string;
|
|
@@ -6,5 +6,5 @@ interface ExotelPhoneProps {
|
|
|
6
6
|
prospectNumber?: string;
|
|
7
7
|
onSaveDisposition?: (data: DispositionFormData) => Promise<any>;
|
|
8
8
|
}
|
|
9
|
-
declare const ExotelPhone: ({ className, prospectId, prospectName, prospectNumber, onSaveDisposition }: ExotelPhoneProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
declare const ExotelPhone: ({ className, prospectId, prospectName, prospectNumber, onSaveDisposition, }: ExotelPhoneProps) => import("react/jsx-runtime").JSX.Element;
|
|
10
10
|
export default ExotelPhone;
|
|
@@ -84,6 +84,7 @@ export interface SaveDispositionInput {
|
|
|
84
84
|
prospectId?: string;
|
|
85
85
|
callStatusDisposition?: 'connected' | 'not_connected';
|
|
86
86
|
dispositionCategory: string;
|
|
87
|
+
prospectStatus?: string;
|
|
87
88
|
dispositionReason?: string;
|
|
88
89
|
dispositionNotes?: string;
|
|
89
90
|
callbackScheduledAt?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -305,6 +305,7 @@ interface SaveDispositionInput {
|
|
|
305
305
|
prospectId?: string;
|
|
306
306
|
callStatusDisposition?: 'connected' | 'not_connected';
|
|
307
307
|
dispositionCategory: string;
|
|
308
|
+
prospectStatus?: string;
|
|
308
309
|
dispositionReason?: string;
|
|
309
310
|
dispositionNotes?: string;
|
|
310
311
|
callbackScheduledAt?: string;
|
|
@@ -491,7 +492,7 @@ interface ExotelPhoneProps {
|
|
|
491
492
|
prospectNumber?: string;
|
|
492
493
|
onSaveDisposition?: (data: DispositionFormData) => Promise<any>;
|
|
493
494
|
}
|
|
494
|
-
declare const ExotelPhone: ({ className, prospectId, prospectName, prospectNumber, onSaveDisposition }: ExotelPhoneProps) => react_jsx_runtime.JSX.Element | null;
|
|
495
|
+
declare const ExotelPhone: ({ className, prospectId, prospectName, prospectNumber, onSaveDisposition, }: ExotelPhoneProps) => react_jsx_runtime.JSX.Element | null;
|
|
495
496
|
|
|
496
497
|
interface CallButtonProps {
|
|
497
498
|
phoneNumber?: string;
|
package/package.json
CHANGED
|
@@ -118,7 +118,41 @@ const CallButton = ({
|
|
|
118
118
|
}
|
|
119
119
|
} catch (err: any) {
|
|
120
120
|
console.error('Failed to make call:', err)
|
|
121
|
-
|
|
121
|
+
|
|
122
|
+
// Parse Exotel error response
|
|
123
|
+
let errorMessage = 'Failed to make call'
|
|
124
|
+
|
|
125
|
+
if (err?.Code || err?.Status === 'Failed') {
|
|
126
|
+
// Exotel API error response
|
|
127
|
+
const code = err.Code
|
|
128
|
+
|
|
129
|
+
// Try to parse the Error field if it's a JSON string
|
|
130
|
+
let errorDetails = null
|
|
131
|
+
if (err.Error && typeof err.Error === 'string') {
|
|
132
|
+
try {
|
|
133
|
+
errorDetails = JSON.parse(err.Error)
|
|
134
|
+
} catch (parseErr) {
|
|
135
|
+
// Not JSON, ignore
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Get description from parsed error or use default messages
|
|
140
|
+
const description = errorDetails?.response?.error_data?.description
|
|
141
|
+
|
|
142
|
+
if (code === 404) {
|
|
143
|
+
errorMessage = description || 'User device is currently busy or unavailable'
|
|
144
|
+
} else if (code >= 400 && code < 500) {
|
|
145
|
+
errorMessage = description || 'Unable to complete call. Please try again.'
|
|
146
|
+
} else if (code >= 500) {
|
|
147
|
+
errorMessage = description || 'Service temporarily unavailable. Please try again later.'
|
|
148
|
+
} else {
|
|
149
|
+
errorMessage = description || 'Failed to make call'
|
|
150
|
+
}
|
|
151
|
+
} else if (err?.message) {
|
|
152
|
+
errorMessage = err.message
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setError(errorMessage)
|
|
122
156
|
} finally {
|
|
123
157
|
setIsDialing(false)
|
|
124
158
|
}
|
|
@@ -69,7 +69,7 @@ const CallDispositionForm = ({
|
|
|
69
69
|
const queryClient = useQueryClient();
|
|
70
70
|
const isEditMode = !!existingData?.dispositionCategory;
|
|
71
71
|
|
|
72
|
-
// Simple state management
|
|
72
|
+
// Simple state management
|
|
73
73
|
const [callStatus, setCallStatus] = useState<CallStatusType | ''>(
|
|
74
74
|
existingData?.callStatusDisposition || '',
|
|
75
75
|
);
|
|
@@ -1,37 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Button, Typography } from '@campxdev/react-blueprint';
|
|
2
|
+
import { Mic, MicOff, Pause, Phone, PhoneIncoming, PhoneOff, PhoneOutgoing, Play, Volume2, VolumeX, X } from 'lucide-react';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
3
5
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
MicOff,
|
|
10
|
-
Pause,
|
|
11
|
-
Play,
|
|
12
|
-
Volume2,
|
|
13
|
-
VolumeX,
|
|
14
|
-
Phone,
|
|
15
|
-
PhoneOff,
|
|
16
|
-
PhoneIncoming,
|
|
17
|
-
PhoneOutgoing,
|
|
18
|
-
} from 'lucide-react'
|
|
19
|
-
|
|
20
|
-
import { useExotel } from '../../providers/ExotelProvider'
|
|
21
|
-
|
|
22
|
-
import { cancelCall, saveCallDisposition } from '../../services/exotel/api'
|
|
23
|
-
|
|
24
|
-
import { formatDurationTimer } from '../../utils/exotel/formatters'
|
|
25
|
-
import { EXOTEL_CONSTANTS } from '../../constants/exotel.constants'
|
|
26
|
-
|
|
27
|
-
import CallDispositionForm, { type DispositionFormData } from './CallDispositionForm'
|
|
6
|
+
import { EXOTEL_CONSTANTS } from '../../constants/exotel.constants';
|
|
7
|
+
import { useExotel } from '../../providers/ExotelProvider';
|
|
8
|
+
import { cancelCall, saveCallDisposition } from '../../services/exotel/api';
|
|
9
|
+
import { formatDurationTimer } from '../../utils/exotel/formatters';
|
|
10
|
+
import CallDispositionForm, { DispositionFormData } from './CallDispositionForm';
|
|
28
11
|
|
|
29
12
|
interface ExotelPhoneProps {
|
|
30
|
-
className?: string
|
|
31
|
-
prospectId?: string
|
|
32
|
-
prospectName?: string
|
|
33
|
-
prospectNumber?: string
|
|
34
|
-
onSaveDisposition?: (data: DispositionFormData) => Promise<any
|
|
13
|
+
className?: string;
|
|
14
|
+
prospectId?: string;
|
|
15
|
+
prospectName?: string;
|
|
16
|
+
prospectNumber?: string;
|
|
17
|
+
onSaveDisposition?: (data: DispositionFormData) => Promise<any>;
|
|
35
18
|
}
|
|
36
19
|
|
|
37
20
|
// CSS for pulsing animation
|
|
@@ -67,9 +50,15 @@ const pulseAnimationStyle = `
|
|
|
67
50
|
.animate-pulse-dot {
|
|
68
51
|
animation: pulse-dot 1.5s ease-in-out infinite;
|
|
69
52
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const ExotelPhone = ({
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
const ExotelPhone = ({
|
|
56
|
+
className,
|
|
57
|
+
prospectId,
|
|
58
|
+
prospectName,
|
|
59
|
+
prospectNumber,
|
|
60
|
+
onSaveDisposition,
|
|
61
|
+
}: ExotelPhoneProps) => {
|
|
73
62
|
const {
|
|
74
63
|
callState,
|
|
75
64
|
dispositionData,
|
|
@@ -80,22 +69,22 @@ const ExotelPhone = ({ className, prospectId, prospectName, prospectNumber, onSa
|
|
|
80
69
|
toggleMute,
|
|
81
70
|
toggleSpeaker,
|
|
82
71
|
clearDisposition,
|
|
83
|
-
} = useExotel()
|
|
72
|
+
} = useExotel();
|
|
84
73
|
|
|
85
|
-
const [isAccepting, setIsAccepting] = useState(false)
|
|
86
|
-
const [isDeclining, setIsDeclining] = useState(false)
|
|
87
|
-
const declineTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
|
74
|
+
const [isAccepting, setIsAccepting] = useState(false);
|
|
75
|
+
const [isDeclining, setIsDeclining] = useState(false);
|
|
76
|
+
const declineTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
88
77
|
|
|
89
78
|
// Inject animation styles
|
|
90
79
|
useEffect(() => {
|
|
91
|
-
const styleId = 'exotel-phone-animations'
|
|
80
|
+
const styleId = 'exotel-phone-animations';
|
|
92
81
|
if (!document.getElementById(styleId)) {
|
|
93
|
-
const styleEl = document.createElement('style')
|
|
94
|
-
styleEl.id = styleId
|
|
95
|
-
styleEl.textContent = pulseAnimationStyle
|
|
96
|
-
document.head.appendChild(styleEl)
|
|
82
|
+
const styleEl = document.createElement('style');
|
|
83
|
+
styleEl.id = styleId;
|
|
84
|
+
styleEl.textContent = pulseAnimationStyle;
|
|
85
|
+
document.head.appendChild(styleEl);
|
|
97
86
|
}
|
|
98
|
-
}, [])
|
|
87
|
+
}, []);
|
|
99
88
|
|
|
100
89
|
const {
|
|
101
90
|
phase,
|
|
@@ -110,43 +99,43 @@ const ExotelPhone = ({ className, prospectId, prospectName, prospectNumber, onSa
|
|
|
110
99
|
callActivityId,
|
|
111
100
|
exotelCallSid,
|
|
112
101
|
prospectUniqueId,
|
|
113
|
-
} = callState
|
|
102
|
+
} = callState;
|
|
114
103
|
|
|
115
104
|
// Use props (from Prospect view) as primary, fall back to SDK values (for incoming calls)
|
|
116
|
-
const displayName = prospectName || sdkCallerName || 'Unknown'
|
|
117
|
-
const displayNumber = prospectNumber || sdkCallerNumber || 'No number'
|
|
105
|
+
const displayName = prospectName || sdkCallerName || 'Unknown';
|
|
106
|
+
const displayNumber = prospectNumber || sdkCallerNumber || 'No number';
|
|
118
107
|
|
|
119
108
|
const handleAcceptCall = async () => {
|
|
120
|
-
setIsAccepting(true)
|
|
109
|
+
setIsAccepting(true);
|
|
121
110
|
try {
|
|
122
|
-
await acceptCall()
|
|
111
|
+
await acceptCall();
|
|
123
112
|
} catch (error: any) {
|
|
124
|
-
console.error('Accept call error:', error)
|
|
113
|
+
console.error('Accept call error:', error);
|
|
125
114
|
} finally {
|
|
126
|
-
setIsAccepting(false)
|
|
115
|
+
setIsAccepting(false);
|
|
127
116
|
}
|
|
128
|
-
}
|
|
117
|
+
};
|
|
129
118
|
|
|
130
119
|
const handleDeclineCall = async () => {
|
|
131
|
-
setIsDeclining(true)
|
|
120
|
+
setIsDeclining(true);
|
|
132
121
|
// Use rejectCall for incoming/ready_to_call (before accepting)
|
|
133
122
|
// Use hangupCall for connected/calling (after accepting)
|
|
134
123
|
if (phase === 'incoming' || phase === 'ready_to_call') {
|
|
135
|
-
rejectCall()
|
|
124
|
+
rejectCall();
|
|
136
125
|
} else {
|
|
137
|
-
hangupCall()
|
|
126
|
+
hangupCall();
|
|
138
127
|
}
|
|
139
128
|
|
|
140
129
|
// Call backend API to update call activity status
|
|
141
130
|
// Use exotelCallSid (from outbound_call API) for outbound calls
|
|
142
131
|
// Use callActivityId as fallback identifier
|
|
143
|
-
const callSidForApi = exotelCallSid || callActivityId
|
|
132
|
+
const callSidForApi = exotelCallSid || callActivityId;
|
|
144
133
|
if (callSidForApi) {
|
|
145
134
|
try {
|
|
146
|
-
const reason = phase === 'connected' ? 'ended' : 'declined'
|
|
147
|
-
await cancelCall({ callSid: callSidForApi, reason })
|
|
135
|
+
const reason = phase === 'connected' ? 'ended' : 'declined';
|
|
136
|
+
await cancelCall({ callSid: callSidForApi, reason });
|
|
148
137
|
} catch (error) {
|
|
149
|
-
console.error('Failed to cancel call in backend:', error)
|
|
138
|
+
console.error('Failed to cancel call in backend:', error);
|
|
150
139
|
}
|
|
151
140
|
}
|
|
152
141
|
|
|
@@ -154,27 +143,27 @@ const ExotelPhone = ({ className, prospectId, prospectName, prospectNumber, onSa
|
|
|
154
143
|
// Set a timeout to force reset the declining state
|
|
155
144
|
// Note: Don't clear disposition here - let the SDK's callEnded event handle it
|
|
156
145
|
declineTimeoutRef.current = setTimeout(() => {
|
|
157
|
-
setIsDeclining(false)
|
|
158
|
-
}, EXOTEL_CONSTANTS.DECLINE_TIMEOUT_MS)
|
|
159
|
-
}
|
|
146
|
+
setIsDeclining(false);
|
|
147
|
+
}, EXOTEL_CONSTANTS.DECLINE_TIMEOUT_MS);
|
|
148
|
+
};
|
|
160
149
|
|
|
161
150
|
// Clear decline timeout when phase changes (meaning SDK handled it)
|
|
162
151
|
useEffect(() => {
|
|
163
152
|
if (phase === 'idle' || phase === 'ended') {
|
|
164
153
|
if (declineTimeoutRef.current) {
|
|
165
|
-
clearTimeout(declineTimeoutRef.current)
|
|
166
|
-
declineTimeoutRef.current = null
|
|
154
|
+
clearTimeout(declineTimeoutRef.current);
|
|
155
|
+
declineTimeoutRef.current = null;
|
|
167
156
|
}
|
|
168
|
-
setIsDeclining(false)
|
|
157
|
+
setIsDeclining(false);
|
|
169
158
|
// Note: Don't auto-clear disposition here - let user submit or close the form manually
|
|
170
159
|
}
|
|
171
|
-
}, [phase])
|
|
160
|
+
}, [phase]);
|
|
172
161
|
|
|
173
162
|
// Handle saving disposition to backend
|
|
174
163
|
const handleSaveDisposition = async (data: DispositionFormData) => {
|
|
175
164
|
// If custom handler is provided, use it
|
|
176
165
|
if (onSaveDisposition) {
|
|
177
|
-
return onSaveDisposition(data)
|
|
166
|
+
return onSaveDisposition(data);
|
|
178
167
|
}
|
|
179
168
|
|
|
180
169
|
// Use the callStatusDisposition from the form (user selected)
|
|
@@ -184,29 +173,30 @@ const ExotelPhone = ({ className, prospectId, prospectName, prospectNumber, onSa
|
|
|
184
173
|
callStatusDisposition: data.callStatusDisposition,
|
|
185
174
|
dispositionCategory: data.dispositionCategory,
|
|
186
175
|
dispositionNotes: data.remarks || undefined,
|
|
187
|
-
})
|
|
188
|
-
}
|
|
176
|
+
});
|
|
177
|
+
};
|
|
189
178
|
|
|
190
179
|
// Cleanup timeout on unmount
|
|
191
180
|
useEffect(() => {
|
|
192
181
|
return () => {
|
|
193
182
|
if (declineTimeoutRef.current) {
|
|
194
|
-
clearTimeout(declineTimeoutRef.current)
|
|
183
|
+
clearTimeout(declineTimeoutRef.current);
|
|
195
184
|
}
|
|
196
|
-
}
|
|
197
|
-
}, [])
|
|
185
|
+
};
|
|
186
|
+
}, []);
|
|
198
187
|
|
|
199
188
|
// Show disposition form after call ends
|
|
200
189
|
if (dispositionData) {
|
|
201
190
|
// Use prospect name from props, or from dispositionData (preserved from call), or fallback
|
|
202
|
-
const finalCallerName =
|
|
191
|
+
const finalCallerName =
|
|
192
|
+
prospectName || dispositionData.callerName || sdkCallerName || 'Unknown';
|
|
203
193
|
|
|
204
194
|
// Override disposition data with prospect details
|
|
205
195
|
const enrichedDispositionData = {
|
|
206
196
|
...dispositionData,
|
|
207
197
|
callerName: finalCallerName,
|
|
208
198
|
callerNumber: displayNumber,
|
|
209
|
-
}
|
|
199
|
+
};
|
|
210
200
|
|
|
211
201
|
return (
|
|
212
202
|
<CallDispositionForm
|
|
@@ -214,17 +204,18 @@ const ExotelPhone = ({ className, prospectId, prospectName, prospectNumber, onSa
|
|
|
214
204
|
prospectId={prospectId}
|
|
215
205
|
onClose={clearDisposition}
|
|
216
206
|
onSave={handleSaveDisposition}
|
|
207
|
+
existingData={undefined}
|
|
217
208
|
/>
|
|
218
|
-
)
|
|
209
|
+
);
|
|
219
210
|
}
|
|
220
211
|
|
|
221
212
|
// Don't show anything if idle or ended
|
|
222
213
|
if (phase === 'idle' || phase === 'ended') {
|
|
223
|
-
return null
|
|
214
|
+
return null;
|
|
224
215
|
}
|
|
225
216
|
|
|
226
|
-
const isOutbound = callDirection === 'outbound'
|
|
227
|
-
const isInbound = callDirection === 'inbound'
|
|
217
|
+
const isOutbound = callDirection === 'outbound';
|
|
218
|
+
const isInbound = callDirection === 'inbound';
|
|
228
219
|
|
|
229
220
|
// Get avatar color based on call direction
|
|
230
221
|
const avatarBgColor =
|
|
@@ -232,31 +223,31 @@ const ExotelPhone = ({ className, prospectId, prospectName, prospectNumber, onSa
|
|
|
232
223
|
? 'bg-green-500'
|
|
233
224
|
: isOutbound
|
|
234
225
|
? 'bg-blue-500'
|
|
235
|
-
: 'bg-orange-500'
|
|
226
|
+
: 'bg-orange-500';
|
|
236
227
|
|
|
237
228
|
// Get status text
|
|
238
229
|
const getStatusText = () => {
|
|
239
230
|
switch (phase) {
|
|
240
231
|
case 'ready_to_call':
|
|
241
|
-
return 'Ready to call...'
|
|
232
|
+
return 'Ready to call...';
|
|
242
233
|
case 'calling':
|
|
243
|
-
return 'Calling...'
|
|
234
|
+
return 'Calling...';
|
|
244
235
|
case 'incoming':
|
|
245
|
-
return 'Incoming call...'
|
|
236
|
+
return 'Incoming call...';
|
|
246
237
|
case 'connected':
|
|
247
|
-
return null // Will show timer instead
|
|
238
|
+
return null; // Will show timer instead
|
|
248
239
|
default:
|
|
249
|
-
return null
|
|
240
|
+
return null;
|
|
250
241
|
}
|
|
251
|
-
}
|
|
242
|
+
};
|
|
252
243
|
|
|
253
244
|
// Get connecting text for calling state
|
|
254
245
|
const getConnectingText = () => {
|
|
255
246
|
if (phase === 'calling') {
|
|
256
|
-
return `Connecting to ${displayName}
|
|
247
|
+
return `Connecting to ${displayName}...`;
|
|
257
248
|
}
|
|
258
|
-
return null
|
|
259
|
-
}
|
|
249
|
+
return null;
|
|
250
|
+
};
|
|
260
251
|
|
|
261
252
|
const modalContent = (
|
|
262
253
|
<>
|
|
@@ -273,14 +264,17 @@ const ExotelPhone = ({ className, prospectId, prospectName, prospectNumber, onSa
|
|
|
273
264
|
style={{ zIndex: EXOTEL_CONSTANTS.MODAL_Z_INDEX }}
|
|
274
265
|
>
|
|
275
266
|
<div
|
|
276
|
-
className="
|
|
277
|
-
style={{
|
|
267
|
+
className="relative w-full max-w-md p-6 bg-white shadow-2xl"
|
|
268
|
+
style={{
|
|
269
|
+
borderRadius: EXOTEL_CONSTANTS.MODAL_BORDER_RADIUS,
|
|
270
|
+
border: '1px solid #e5e7eb',
|
|
271
|
+
}}
|
|
278
272
|
onClick={(e) => e.stopPropagation()}
|
|
279
273
|
>
|
|
280
274
|
{/* Close button */}
|
|
281
275
|
<button
|
|
282
276
|
onClick={handleDeclineCall}
|
|
283
|
-
className="absolute top-4 right-4
|
|
277
|
+
className="absolute p-1 transition-colors rounded-full top-4 right-4 hover:bg-gray-100"
|
|
284
278
|
>
|
|
285
279
|
<X className="w-5 h-5 text-gray-500" />
|
|
286
280
|
</button>
|
|
@@ -321,20 +315,28 @@ const ExotelPhone = ({ className, prospectId, prospectName, prospectNumber, onSa
|
|
|
321
315
|
</div>
|
|
322
316
|
|
|
323
317
|
{/* Caller info */}
|
|
324
|
-
<div className="text-center
|
|
325
|
-
<Typography
|
|
318
|
+
<div className="mb-4 text-center">
|
|
319
|
+
<Typography
|
|
320
|
+
variant="h3"
|
|
321
|
+
className="text-xl font-semibold text-gray-900"
|
|
322
|
+
>
|
|
326
323
|
{displayName}
|
|
327
324
|
</Typography>
|
|
328
325
|
{prospectUniqueId && (
|
|
329
|
-
<Typography variant="small" className="text-blue-600
|
|
326
|
+
<Typography variant="small" className="font-medium text-blue-600">
|
|
327
|
+
ID: {prospectUniqueId}
|
|
328
|
+
</Typography>
|
|
330
329
|
)}
|
|
331
330
|
<Typography variant="muted">{displayNumber}</Typography>
|
|
332
331
|
</div>
|
|
333
332
|
|
|
334
333
|
{/* Status / Timer */}
|
|
335
|
-
<div className="text-center
|
|
334
|
+
<div className="mb-6 text-center">
|
|
336
335
|
{phase === 'connected' ? (
|
|
337
|
-
<Typography
|
|
336
|
+
<Typography
|
|
337
|
+
variant="p"
|
|
338
|
+
className="text-lg font-medium text-green-500"
|
|
339
|
+
>
|
|
338
340
|
{formatDurationTimer(callDuration)}
|
|
339
341
|
</Typography>
|
|
340
342
|
) : (
|
|
@@ -361,7 +363,7 @@ const ExotelPhone = ({ className, prospectId, prospectName, prospectNumber, onSa
|
|
|
361
363
|
<Mic className="w-5 h-5 text-gray-600" />
|
|
362
364
|
)}
|
|
363
365
|
</button>
|
|
364
|
-
<p className="text-xs text-gray-500
|
|
366
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
365
367
|
{isMuted ? 'Unmute' : 'Mute'}
|
|
366
368
|
</p>
|
|
367
369
|
</div>
|
|
@@ -382,7 +384,7 @@ const ExotelPhone = ({ className, prospectId, prospectName, prospectNumber, onSa
|
|
|
382
384
|
<Pause className="w-5 h-5 text-gray-600" />
|
|
383
385
|
)}
|
|
384
386
|
</button>
|
|
385
|
-
<p className="text-xs text-gray-500
|
|
387
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
386
388
|
{isOnHold ? 'Resume' : 'Hold'}
|
|
387
389
|
</p>
|
|
388
390
|
</div>
|
|
@@ -403,7 +405,7 @@ const ExotelPhone = ({ className, prospectId, prospectName, prospectNumber, onSa
|
|
|
403
405
|
<Volume2 className="w-5 h-5 text-gray-600" />
|
|
404
406
|
)}
|
|
405
407
|
</button>
|
|
406
|
-
<p className="text-xs text-gray-500
|
|
408
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
407
409
|
{isSpeakerOff ? 'Speaker On' : 'Speaker Off'}
|
|
408
410
|
</p>
|
|
409
411
|
</div>
|
|
@@ -466,17 +468,20 @@ const ExotelPhone = ({ className, prospectId, prospectName, prospectNumber, onSa
|
|
|
466
468
|
|
|
467
469
|
{/* Connecting text */}
|
|
468
470
|
{getConnectingText() && (
|
|
469
|
-
<Typography
|
|
471
|
+
<Typography
|
|
472
|
+
variant="small"
|
|
473
|
+
className="mt-4 text-center text-gray-500"
|
|
474
|
+
>
|
|
470
475
|
{getConnectingText()}
|
|
471
476
|
</Typography>
|
|
472
477
|
)}
|
|
473
478
|
</div>
|
|
474
479
|
</div>
|
|
475
480
|
</>
|
|
476
|
-
)
|
|
481
|
+
);
|
|
477
482
|
|
|
478
483
|
// Use portal to render at document body level to escape any parent stacking contexts
|
|
479
|
-
return createPortal(modalContent, document.body)
|
|
480
|
-
}
|
|
484
|
+
return createPortal(modalContent, document.body);
|
|
485
|
+
};
|
|
481
486
|
|
|
482
|
-
export default ExotelPhone
|
|
487
|
+
export default ExotelPhone;
|
|
@@ -422,8 +422,16 @@ export const ExotelProvider: React.FC<ExotelProviderProps> = ({
|
|
|
422
422
|
callDuration: 0,
|
|
423
423
|
}))
|
|
424
424
|
|
|
425
|
-
|
|
426
|
-
|
|
425
|
+
try {
|
|
426
|
+
const response = await exotelService.makeCall(phoneNumber) // Use actual number for calling
|
|
427
|
+
return response
|
|
428
|
+
} catch (error: any) {
|
|
429
|
+
// If SDK call fails (4xx/5xx errors from Exotel API), reset state to idle
|
|
430
|
+
// This allows the user to retry the call
|
|
431
|
+
setCallState(initialCallState)
|
|
432
|
+
// Re-throw the error so CallButton can handle it
|
|
433
|
+
throw error
|
|
434
|
+
}
|
|
427
435
|
},
|
|
428
436
|
[defaultCallerName],
|
|
429
437
|
)
|