diff --git a/app/javascript/dashboard/api/channel/whatsapp/whatsappCallsAPI.js b/app/javascript/dashboard/api/channel/whatsapp/whatsappCallsAPI.js new file mode 100644 index 000000000..ec24aae34 --- /dev/null +++ b/app/javascript/dashboard/api/channel/whatsapp/whatsappCallsAPI.js @@ -0,0 +1,45 @@ +/* global axios */ +import ApiClient from '../../ApiClient'; + +class WhatsappCallsAPI extends ApiClient { + constructor() { + super('whatsapp_calls', { accountScoped: true }); + } + + show(callId) { + return axios.get(`${this.url}/${callId}`).then(r => r.data); + } + + initiate(conversationId, sdpOffer) { + return axios + .post(`${this.url}/initiate`, { + conversation_id: conversationId, + sdp_offer: sdpOffer, + }) + .then(r => r.data); + } + + accept(callId, sdpAnswer) { + return axios + .post(`${this.url}/${callId}/accept`, { sdp_answer: sdpAnswer }) + .then(r => r.data); + } + + reject(callId) { + return axios.post(`${this.url}/${callId}/reject`).then(r => r.data); + } + + terminate(callId) { + return axios.post(`${this.url}/${callId}/terminate`).then(r => r.data); + } + + uploadRecording(callId, blob, filename = 'call-recording.webm') { + const formData = new FormData(); + formData.append('recording', blob, filename); + return axios + .post(`${this.url}/${callId}/upload_recording`, formData) + .then(r => r.data); + } +} + +export default new WhatsappCallsAPI(); diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue index 4edfd643c..075f2896e 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue @@ -12,6 +12,13 @@ import wootConstants from 'dashboard/constants/globals'; import { conversationListPageURL } from 'dashboard/helper/URLHelper'; import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers'; import { useInbox } from 'dashboard/composables/useInbox'; +import { + getVoiceCallProvider, + VOICE_CALL_PROVIDERS, +} from 'dashboard/helper/inbox'; +import { useWhatsappCallSession } from 'dashboard/composables/useWhatsappCallSession'; +import { useCallsStore } from 'dashboard/stores/calls'; +import { useAlert } from 'dashboard/composables'; import { useI18n } from 'vue-i18n'; const props = defineProps({ @@ -91,6 +98,45 @@ const hasMultipleInboxes = computed( ); const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id); + +const callsStore = useCallsStore(); +const whatsappCallSession = useWhatsappCallSession(); + +const isWhatsappVoiceInbox = computed( + () => getVoiceCallProvider(inbox.value) === VOICE_CALL_PROVIDERS.WHATSAPP +); + +const startWhatsappCall = async () => { + if (whatsappCallSession.isInitiating.value) return; + try { + const response = await whatsappCallSession.initiateOutboundCall( + currentChat.value.id + ); + + // Permission template path returns no call id — show banner, no widget yet. + if (!response?.id) { + const status = response?.status; + const messageKey = + status === 'permission_pending' + ? 'CONVERSATION.HEADER.WHATSAPP_CALL_PERMISSION_PENDING' + : 'CONVERSATION.HEADER.WHATSAPP_CALL_PERMISSION_REQUESTED'; + useAlert(t(messageKey)); + return; + } + + callsStore.addCall({ + callSid: response.call_id, + callId: response.id, + conversationId: currentChat.value.id, + inboxId: inbox.value?.id, + callDirection: 'outbound', + provider: 'whatsapp', + }); + callsStore.setCallActive(response.call_id); + } catch (error) { + useAlert(error?.message || t('CONVERSATION.HEADER.WHATSAPP_CALL_FAILED')); + } +};