feat: select calling medium while calling and set defaulr medium
This commit is contained in:
parent
49de7c92a5
commit
8a912b21e0
@ -12,7 +12,9 @@
|
|||||||
"brand_logo",
|
"brand_logo",
|
||||||
"favicon",
|
"favicon",
|
||||||
"dropdown_items_tab",
|
"dropdown_items_tab",
|
||||||
"dropdown_items"
|
"dropdown_items",
|
||||||
|
"calling_tab",
|
||||||
|
"default_calling_medium"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -56,12 +58,23 @@
|
|||||||
"fieldname": "favicon",
|
"fieldname": "favicon",
|
||||||
"fieldtype": "Attach",
|
"fieldtype": "Attach",
|
||||||
"label": "Favicon"
|
"label": "Favicon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "calling_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Calling"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "default_calling_medium",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Default calling medium",
|
||||||
|
"options": "\nTwilio\nExotel"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-12-30 19:21:30.847343",
|
"modified": "2025-01-15 17:40:32.784762",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "FCRM Settings",
|
"name": "FCRM Settings",
|
||||||
|
|||||||
0
crm/integrations/__init__.py
Normal file
0
crm/integrations/__init__.py
Normal file
19
crm/integrations/api.py
Normal file
19
crm/integrations/api.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def is_call_integration_enabled():
|
||||||
|
twilio_enabled = frappe.db.get_single_value("Twilio Settings", "enabled")
|
||||||
|
exotel_enabled = frappe.db.get_single_value("CRM Exotel Settings", "enabled")
|
||||||
|
default_calling_medium = frappe.db.get_single_value("FCRM Settings", "default_calling_medium")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"twilio_enabled": twilio_enabled,
|
||||||
|
"exotel_enabled": exotel_enabled,
|
||||||
|
"default_calling_medium": default_calling_medium,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def set_default_calling_medium(medium):
|
||||||
|
return frappe.db.set_value("FCRM Settings", "FCRM Settings", "default_calling_medium", medium)
|
||||||
@ -2,13 +2,11 @@
|
|||||||
<div class="flex border-b pr-5">
|
<div class="flex border-b pr-5">
|
||||||
<div id="app-header" class="flex-1"></div>
|
<div id="app-header" class="flex-1"></div>
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<ExotelCallUI />
|
|
||||||
<CallUI />
|
<CallUI />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ExotelCallUI from '@/components/Telephony/ExotelCallUI.vue'
|
|
||||||
import CallUI from '@/components/Telephony/CallUI.vue'
|
import CallUI from '@/components/Telephony/CallUI.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -12,12 +12,10 @@
|
|||||||
<div id="app-header" class="flex-1" />
|
<div id="app-header" class="flex-1" />
|
||||||
</div>
|
</div>
|
||||||
<CallUI class="mr-3 mt-2" />
|
<CallUI class="mr-3 mt-2" />
|
||||||
<ExotelCallUI class="mr-3 mt-2" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import MenuIcon from '@/components/Icons/MenuIcon.vue'
|
import MenuIcon from '@/components/Icons/MenuIcon.vue'
|
||||||
import ExotelCallUI from '@/components/Telephony/ExotelCallUI.vue'
|
|
||||||
import CallUI from '@/components/Telephony/CallUI.vue'
|
import CallUI from '@/components/Telephony/CallUI.vue'
|
||||||
import { mobileSidebarOpened as sidebarOpened } from '@/composables/settings'
|
import { mobileSidebarOpened as sidebarOpened } from '@/composables/settings'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,549 +1,135 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-show="showCallPopup" v-bind="$attrs">
|
<TwilioCallUI ref="twilio" />
|
||||||
<div
|
<ExotelCallUI ref="exotel" />
|
||||||
ref="callPopup"
|
<Dialog
|
||||||
class="fixed z-20 flex w-60 cursor-move select-none flex-col rounded-lg bg-surface-gray-7 p-4 text-ink-gray-2 shadow-2xl"
|
v-model="show"
|
||||||
:style="style"
|
:options="{
|
||||||
>
|
title: __('Make call'),
|
||||||
<div class="flex flex-row-reverse items-center gap-1">
|
actions: [
|
||||||
<MinimizeIcon
|
{
|
||||||
class="h-4 w-4 cursor-pointer"
|
label: __('Call using {0}', [callMedium]),
|
||||||
@click="toggleCallWindow"
|
variant: 'solid',
|
||||||
/>
|
onClick: makeCallUsing,
|
||||||
</div>
|
},
|
||||||
<div class="flex flex-col items-center justify-center gap-3">
|
],
|
||||||
<Avatar
|
}"
|
||||||
:image="contact.image"
|
|
||||||
:label="contact.full_name"
|
|
||||||
class="relative flex !h-24 !w-24 items-center justify-center [&>div]:text-[30px]"
|
|
||||||
:class="onCall || calling ? '' : 'pulse'"
|
|
||||||
/>
|
|
||||||
<div class="flex flex-col items-center justify-center gap-1">
|
|
||||||
<div class="text-xl font-medium">
|
|
||||||
{{ contact.full_name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-ink-gray-5">{{ contact.mobile_no }}</div>
|
|
||||||
</div>
|
|
||||||
<CountUpTimer ref="counterUp">
|
|
||||||
<div v-if="onCall" class="my-1 text-base">
|
|
||||||
{{ counterUp?.updatedTime }}
|
|
||||||
</div>
|
|
||||||
</CountUpTimer>
|
|
||||||
<div v-if="!onCall" class="my-1 text-base">
|
|
||||||
{{
|
|
||||||
callStatus == 'initiating'
|
|
||||||
? __('Initiating call...')
|
|
||||||
: callStatus == 'ringing'
|
|
||||||
? __('Ringing...')
|
|
||||||
: calling
|
|
||||||
? __('Calling...')
|
|
||||||
: __('Incoming call...')
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div v-if="onCall" class="flex gap-2">
|
|
||||||
<Button
|
|
||||||
:icon="muted ? 'mic-off' : 'mic'"
|
|
||||||
class="rounded-full"
|
|
||||||
@click="toggleMute"
|
|
||||||
/>
|
|
||||||
<!-- <Button class="rounded-full">
|
|
||||||
<template #icon>
|
|
||||||
<DialpadIcon class="cursor-pointer rounded-full" />
|
|
||||||
</template>
|
|
||||||
</Button> -->
|
|
||||||
<Button class="rounded-full">
|
|
||||||
<template #icon>
|
|
||||||
<NoteIcon
|
|
||||||
class="h-4 w-4 cursor-pointer rounded-full text-ink-gray-9"
|
|
||||||
@click="showNoteModal = true"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
<Button class="rounded-full bg-surface-red-5 hover:bg-surface-red-6">
|
|
||||||
<template #icon>
|
|
||||||
<PhoneIcon
|
|
||||||
class="h-4 w-4 rotate-[135deg] fill-white text-ink-white"
|
|
||||||
@click="hangUpCall"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="calling || callStatus == 'initiating'">
|
|
||||||
<Button
|
|
||||||
size="md"
|
|
||||||
variant="solid"
|
|
||||||
theme="red"
|
|
||||||
:label="__('Cancel')"
|
|
||||||
@click="cancelCall"
|
|
||||||
class="rounded-lg"
|
|
||||||
:disabled="callStatus == 'initiating'"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<PhoneIcon class="h-4 w-4 rotate-[135deg] fill-white" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex gap-2">
|
|
||||||
<Button
|
|
||||||
size="md"
|
|
||||||
variant="solid"
|
|
||||||
theme="green"
|
|
||||||
:label="__('Accept')"
|
|
||||||
class="rounded-lg"
|
|
||||||
@click="acceptIncomingCall"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<PhoneIcon class="h-4 w-4 fill-white" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="md"
|
|
||||||
variant="solid"
|
|
||||||
theme="red"
|
|
||||||
:label="__('Reject')"
|
|
||||||
class="rounded-lg"
|
|
||||||
@click="rejectIncomingCall"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<PhoneIcon class="h-4 w-4 rotate-[135deg] fill-white" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="showSmallCallWindow"
|
|
||||||
class="ml-2 flex cursor-pointer select-none items-center justify-between gap-3 rounded-lg bg-surface-gray-7 px-2 py-[7px] text-base text-ink-gray-2"
|
|
||||||
@click="toggleCallWindow"
|
|
||||||
v-bind="$attrs"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<template #body-content>
|
||||||
<Avatar
|
<div class="flex flex-col gap-4">
|
||||||
:image="contact.image"
|
<FormControl
|
||||||
:label="contact.full_name"
|
type="text"
|
||||||
class="relative flex !h-5 !w-5 items-center justify-center"
|
v-model="mobileNumber"
|
||||||
/>
|
:label="__('Mobile Number')"
|
||||||
<div class="max-w-[120px] truncate">
|
/>
|
||||||
{{ contact.full_name }}
|
<FormControl
|
||||||
</div>
|
type="select"
|
||||||
</div>
|
v-model="callMedium"
|
||||||
<div v-if="onCall" class="flex items-center gap-2">
|
:label="__('Call Medium')"
|
||||||
<div class="my-1 min-w-[40px] text-center">
|
:options="['Twilio', 'Exotel']"
|
||||||
{{ counterUp?.updatedTime }}
|
/>
|
||||||
</div>
|
<div class="flex flex-col gap-1">
|
||||||
<Button variant="solid" theme="red" class="!h-6 !w-6 rounded-full">
|
<FormControl
|
||||||
<template #icon>
|
type="checkbox"
|
||||||
<PhoneIcon
|
v-model="isDefaultMedium"
|
||||||
class="h-4 w-4 rotate-[135deg] fill-white"
|
:label="__('Make {0} as default call medium', [callMedium])"
|
||||||
@click.stop="hangUpCall"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
|
||||||
</Button>
|
<div v-if="isDefaultMedium" class="text-sm text-ink-gray-4">
|
||||||
</div>
|
{{ __('You can change the default call medium from the settings') }}
|
||||||
<div v-else-if="calling" class="flex items-center gap-3">
|
</div>
|
||||||
<div class="my-1">
|
</div>
|
||||||
{{ callStatus == 'ringing' ? __('Ringing...') : __('Calling...') }}
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
</template>
|
||||||
variant="solid"
|
</Dialog>
|
||||||
theme="red"
|
|
||||||
class="!h-6 !w-6 rounded-full"
|
|
||||||
@click.stop="cancelCall"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<PhoneIcon class="h-4 w-4 rotate-[135deg] fill-white" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="solid"
|
|
||||||
theme="green"
|
|
||||||
class="pulse relative !h-6 !w-6 rounded-full"
|
|
||||||
@click.stop="acceptIncomingCall"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<PhoneIcon class="h-4 w-4 animate-pulse fill-white" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="solid"
|
|
||||||
theme="red"
|
|
||||||
class="!h-6 !w-6 rounded-full"
|
|
||||||
@click.stop="rejectIncomingCall"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<PhoneIcon class="h-4 w-4 rotate-[135deg] fill-white" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<NoteModal
|
|
||||||
v-model="showNoteModal"
|
|
||||||
:note="note"
|
|
||||||
doctype="CRM Call Log"
|
|
||||||
@after="updateNote"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
import TwilioCallUI from '@/components/Telephony/TwilioCallUI.vue'
|
||||||
import MinimizeIcon from '@/components/Icons/MinimizeIcon.vue'
|
import ExotelCallUI from '@/components/Telephony/ExotelCallUI.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import {
|
||||||
import CountUpTimer from '@/components/CountUpTimer.vue'
|
twilioEnabled,
|
||||||
import NoteModal from '@/components/Modals/NoteModal.vue'
|
exotelEnabled,
|
||||||
import { Device } from '@twilio/voice-sdk'
|
defaultCallingMedium,
|
||||||
import { useDraggable, useWindowSize } from '@vueuse/core'
|
} from '@/composables/settings'
|
||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { createToast } from '@/utils'
|
||||||
import { capture } from '@/telemetry'
|
import { FormControl, call } from 'frappe-ui'
|
||||||
import { Avatar, call } from 'frappe-ui'
|
import { ref, watch } from 'vue'
|
||||||
import { onMounted, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const { getContact, getLeadContact } = contactsStore()
|
const { setMakeCall } = globalStore()
|
||||||
const { setMakeCall, setTwilioEnabled } = globalStore()
|
|
||||||
|
|
||||||
let device = ''
|
const twilio = ref(null)
|
||||||
let log = ref('Connecting...')
|
const exotel = ref(null)
|
||||||
let _call = null
|
|
||||||
const contact = ref({
|
const callMedium = ref('Twilio')
|
||||||
full_name: '',
|
const isDefaultMedium = ref(false)
|
||||||
mobile_no: '',
|
|
||||||
|
const show = ref(false)
|
||||||
|
const mobileNumber = ref('')
|
||||||
|
|
||||||
|
function makeCall(number) {
|
||||||
|
if (
|
||||||
|
twilioEnabled.value &&
|
||||||
|
exotelEnabled.value &&
|
||||||
|
!defaultCallingMedium.value
|
||||||
|
) {
|
||||||
|
mobileNumber.value = number
|
||||||
|
show.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
callMedium.value = twilioEnabled.value ? 'Twilio' : 'Exotel'
|
||||||
|
if (defaultCallingMedium.value) {
|
||||||
|
callMedium.value = defaultCallingMedium.value
|
||||||
|
}
|
||||||
|
|
||||||
|
mobileNumber.value = number
|
||||||
|
makeCallUsing()
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCallUsing() {
|
||||||
|
if (isDefaultMedium.value && callMedium.value) {
|
||||||
|
setDefaultCallingMedium()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callMedium.value === 'Twilio') {
|
||||||
|
twilio.value.makeOutgoingCall(mobileNumber.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callMedium.value === 'Exotel') {
|
||||||
|
exotel.value.makeOutgoingCall(mobileNumber.value)
|
||||||
|
}
|
||||||
|
show.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setDefaultCallingMedium() {
|
||||||
|
await call('crm.integrations.api.set_default_calling_medium', {
|
||||||
|
medium: callMedium.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
defaultCallingMedium.value = callMedium.value
|
||||||
|
createToast({
|
||||||
|
title: __('Default calling medium set successfully to {0}', [
|
||||||
|
callMedium.value,
|
||||||
|
]),
|
||||||
|
icon: 'check',
|
||||||
|
iconClasses: 'text-ink-green-3',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([twilioEnabled, exotelEnabled], ([twilioValue, exotelValue]) => {
|
||||||
|
if (twilioValue) {
|
||||||
|
twilio.value.setup()
|
||||||
|
callMedium.value = 'Twilio'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exotelValue) {
|
||||||
|
exotel.value.setup()
|
||||||
|
callMedium.value = 'Exotel'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (twilioValue || exotelValue) {
|
||||||
|
callMedium.value = 'Twilio'
|
||||||
|
setMakeCall(makeCall)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let showCallPopup = ref(false)
|
|
||||||
let showSmallCallWindow = ref(false)
|
|
||||||
let onCall = ref(false)
|
|
||||||
let calling = ref(false)
|
|
||||||
let muted = ref(false)
|
|
||||||
let callPopup = ref(null)
|
|
||||||
let counterUp = ref(null)
|
|
||||||
let callStatus = ref('')
|
|
||||||
const showNoteModal = ref(false)
|
|
||||||
const note = ref({
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
async function updateNote(_note, insert_mode = false) {
|
|
||||||
note.value = _note
|
|
||||||
if (insert_mode && _note.name) {
|
|
||||||
await call('crm.integrations.twilio.api.add_note_to_call_log', {
|
|
||||||
call_sid: _call.parameters.CallSid,
|
|
||||||
note: _note.name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { width, height } = useWindowSize()
|
|
||||||
|
|
||||||
let { style } = useDraggable(callPopup, {
|
|
||||||
initialValue: { x: width.value - 280, y: height.value - 310 },
|
|
||||||
preventDefault: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
async function is_twilio_enabled() {
|
|
||||||
return await call('crm.integrations.twilio.api.is_enabled')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startupClient() {
|
|
||||||
log.value = 'Requesting Access Token...'
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await call('crm.integrations.twilio.api.generate_access_token')
|
|
||||||
log.value = 'Got a token.'
|
|
||||||
intitializeDevice(data.token)
|
|
||||||
} catch (err) {
|
|
||||||
log.value = 'An error occurred. ' + err.message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function intitializeDevice(token) {
|
|
||||||
device = new Device(token, {
|
|
||||||
codecPreferences: ['opus', 'pcmu'],
|
|
||||||
fakeLocalDTMF: true,
|
|
||||||
enableRingingState: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
addDeviceListeners()
|
|
||||||
|
|
||||||
device.register()
|
|
||||||
}
|
|
||||||
|
|
||||||
function addDeviceListeners() {
|
|
||||||
device.on('registered', () => {
|
|
||||||
log.value = 'Ready to make and receive calls!'
|
|
||||||
})
|
|
||||||
|
|
||||||
device.on('unregistered', (device) => {
|
|
||||||
log.value = 'Logged out'
|
|
||||||
})
|
|
||||||
|
|
||||||
device.on('error', (error) => {
|
|
||||||
log.value = 'Twilio.Device Error: ' + error.message
|
|
||||||
})
|
|
||||||
|
|
||||||
device.on('incoming', handleIncomingCall)
|
|
||||||
|
|
||||||
device.on('tokenWillExpire', async () => {
|
|
||||||
const data = await call('crm.integrations.twilio.api.generate_access_token')
|
|
||||||
device.updateToken(data.token)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleMute() {
|
|
||||||
if (_call.isMuted()) {
|
|
||||||
_call.mute(false)
|
|
||||||
muted.value = false
|
|
||||||
} else {
|
|
||||||
_call.mute()
|
|
||||||
muted.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleIncomingCall(call) {
|
|
||||||
log.value = `Incoming call from ${call.parameters.From}`
|
|
||||||
|
|
||||||
// get name of the caller from the phone number
|
|
||||||
contact.value = getContact(call.parameters.From)
|
|
||||||
if (!contact.value) {
|
|
||||||
contact.value = getLeadContact(call.parameters.From)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!contact.value) {
|
|
||||||
contact.value = {
|
|
||||||
full_name: __('Unknown'),
|
|
||||||
mobile_no: call.parameters.From,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showCallPopup.value = true
|
|
||||||
_call = call
|
|
||||||
|
|
||||||
_call.on('accept', (conn) => {
|
|
||||||
console.log('conn', conn)
|
|
||||||
})
|
|
||||||
|
|
||||||
// add event listener to call object
|
|
||||||
call.on('cancel', handleDisconnectedIncomingCall)
|
|
||||||
call.on('disconnect', handleDisconnectedIncomingCall)
|
|
||||||
call.on('reject', handleDisconnectedIncomingCall)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function acceptIncomingCall() {
|
|
||||||
log.value = 'Accepted incoming call.'
|
|
||||||
onCall.value = true
|
|
||||||
await _call.accept()
|
|
||||||
counterUp.value.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
function rejectIncomingCall() {
|
|
||||||
_call.reject()
|
|
||||||
log.value = 'Rejected incoming call'
|
|
||||||
showCallPopup.value = false
|
|
||||||
if (showSmallCallWindow.value == undefined) {
|
|
||||||
showSmallCallWindow = false
|
|
||||||
} else {
|
|
||||||
showSmallCallWindow.value = false
|
|
||||||
}
|
|
||||||
callStatus.value = ''
|
|
||||||
muted.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function hangUpCall() {
|
|
||||||
_call.disconnect()
|
|
||||||
log.value = 'Hanging up incoming call'
|
|
||||||
onCall.value = false
|
|
||||||
callStatus.value = ''
|
|
||||||
muted.value = false
|
|
||||||
note.value = {
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
}
|
|
||||||
counterUp.value.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDisconnectedIncomingCall() {
|
|
||||||
log.value = `Call ended from handle disconnected Incoming call.`
|
|
||||||
showCallPopup.value = false
|
|
||||||
if (showSmallCallWindow.value == undefined) {
|
|
||||||
showSmallCallWindow = false
|
|
||||||
} else {
|
|
||||||
showSmallCallWindow.value = false
|
|
||||||
}
|
|
||||||
_call = null
|
|
||||||
muted.value = false
|
|
||||||
onCall.value = false
|
|
||||||
counterUp.value.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function makeOutgoingCall(number) {
|
|
||||||
// check if number has a country code
|
|
||||||
// if (number?.replace(/[^0-9+]/g, '').length == 10) {
|
|
||||||
// $dialog({
|
|
||||||
// title: 'Invalid Mobile Number',
|
|
||||||
// message: `${number} is not a valid mobile number. Either add a country code or check the number again.`,
|
|
||||||
// })
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
contact.value = getContact(number)
|
|
||||||
if (!contact.value) {
|
|
||||||
contact.value = getLeadContact(number)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device) {
|
|
||||||
log.value = `Attempting to call ${number} ...`
|
|
||||||
|
|
||||||
try {
|
|
||||||
_call = await device.connect({
|
|
||||||
params: { To: number },
|
|
||||||
})
|
|
||||||
|
|
||||||
showCallPopup.value = true
|
|
||||||
callStatus.value = 'initiating'
|
|
||||||
|
|
||||||
capture('make_outgoing_call')
|
|
||||||
|
|
||||||
_call.on('messageReceived', (message) => {
|
|
||||||
let info = message.content
|
|
||||||
callStatus.value = info.CallStatus
|
|
||||||
|
|
||||||
log.value = `Call status: ${info.CallStatus}`
|
|
||||||
|
|
||||||
if (info.CallStatus == 'in-progress') {
|
|
||||||
log.value = `Call in progress.`
|
|
||||||
calling.value = false
|
|
||||||
onCall.value = true
|
|
||||||
counterUp.value.start()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
_call.on('accept', () => {
|
|
||||||
log.value = `Initiated call!`
|
|
||||||
showCallPopup.value = true
|
|
||||||
calling.value = true
|
|
||||||
onCall.value = false
|
|
||||||
})
|
|
||||||
_call.on('disconnect', (conn) => {
|
|
||||||
log.value = `Call ended from makeOutgoing call disconnect.`
|
|
||||||
calling.value = false
|
|
||||||
onCall.value = false
|
|
||||||
showCallPopup.value = false
|
|
||||||
showSmallCallWindow = false
|
|
||||||
_call = null
|
|
||||||
callStatus.value = ''
|
|
||||||
muted.value = false
|
|
||||||
counterUp.value.stop()
|
|
||||||
note.value = {
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
_call.on('cancel', () => {
|
|
||||||
log.value = `Call ended from makeOutgoing call cancel.`
|
|
||||||
calling.value = false
|
|
||||||
onCall.value = false
|
|
||||||
showCallPopup.value = false
|
|
||||||
showSmallCallWindow = false
|
|
||||||
_call = null
|
|
||||||
callStatus.value = ''
|
|
||||||
muted.value = false
|
|
||||||
note.value = {
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
}
|
|
||||||
counterUp.value.stop()
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
log.value = `Could not connect call: ${error.message}`
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.value = 'Unable to make call.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelCall() {
|
|
||||||
_call.disconnect()
|
|
||||||
showCallPopup.value = false
|
|
||||||
if (showSmallCallWindow.value == undefined) {
|
|
||||||
showSmallCallWindow = false
|
|
||||||
} else {
|
|
||||||
showSmallCallWindow.value = false
|
|
||||||
}
|
|
||||||
calling.value = false
|
|
||||||
onCall.value = false
|
|
||||||
callStatus.value = ''
|
|
||||||
muted.value = false
|
|
||||||
note.value = {
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleCallWindow() {
|
|
||||||
showCallPopup.value = !showCallPopup.value
|
|
||||||
if (showSmallCallWindow.value == undefined) {
|
|
||||||
showSmallCallWindow = !showSmallCallWindow
|
|
||||||
} else {
|
|
||||||
showSmallCallWindow.value = !showSmallCallWindow.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
let enabled = await is_twilio_enabled()
|
|
||||||
setTwilioEnabled(enabled)
|
|
||||||
enabled && startupClient()
|
|
||||||
|
|
||||||
setMakeCall(makeOutgoingCall)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => log.value,
|
|
||||||
(value) => {
|
|
||||||
console.log(value)
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.pulse::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid green;
|
|
||||||
width: calc(100% + 20px);
|
|
||||||
height: calc(100% + 20px);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: pulse 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulse::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid green;
|
|
||||||
width: calc(100% + 20px);
|
|
||||||
height: calc(100% + 20px);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: pulse 1s linear infinite;
|
|
||||||
animation-delay: 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
transform: scale(0.5);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: scale(1.3);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
"
|
"
|
||||||
class="font-normal text-ink-gray-4 mr-1"
|
class="font-normal text-ink-gray-4 mr-1"
|
||||||
>
|
>
|
||||||
{{ callStatus }}
|
{{ __(callStatus) }}
|
||||||
</span>
|
</span>
|
||||||
<span>{{ phoneNumber }}</span>
|
<span>{{ phoneNumber }}</span>
|
||||||
<span
|
<span
|
||||||
@ -43,35 +43,11 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<span class="text-ink-gray-4"> · </span>
|
<span class="text-ink-gray-4"> · </span>
|
||||||
<span>{{ callStatus }}</span>
|
<span>{{ __(callStatus) }}</span>
|
||||||
<span v-if="callStatus == 'Call Ended'"> <span> · </span>00:38 </span>
|
<span v-if="callStatus == 'Call Ended'"> <span> · </span>00:38 </span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Dialog
|
|
||||||
v-model="showCallModal"
|
|
||||||
:options="{
|
|
||||||
title: 'Make a call',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: 'Make a Call',
|
|
||||||
variant: 'solid',
|
|
||||||
onClick: makeCall,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template #body-content>
|
|
||||||
<div>
|
|
||||||
<FormControl
|
|
||||||
v-model="phoneNumber"
|
|
||||||
label="Phone Number"
|
|
||||||
placeholder="+917666980887"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-show="showCallPopup"
|
v-show="showCallPopup"
|
||||||
ref="callPopup"
|
ref="callPopup"
|
||||||
@ -163,14 +139,14 @@ import AvatarIcon from '@/components/Icons/AvatarIcon.vue'
|
|||||||
import MinimizeIcon from '@/components/Icons/MinimizeIcon.vue'
|
import MinimizeIcon from '@/components/Icons/MinimizeIcon.vue'
|
||||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||||
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
||||||
import { Button, Dialog, FormControl, call, Avatar } from 'frappe-ui'
|
import { Avatar, Button, call } from 'frappe-ui'
|
||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
import { useDraggable, useWindowSize } from '@vueuse/core'
|
import { useDraggable, useWindowSize } from '@vueuse/core'
|
||||||
import { ref, computed, onBeforeUnmount, onMounted } from 'vue'
|
import { ref, computed, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
const { getContact, getLeadContact } = contactsStore()
|
const { getContact, getLeadContact } = contactsStore()
|
||||||
const { $socket, setMakeCall } = globalStore()
|
const { $socket } = globalStore()
|
||||||
|
|
||||||
const callPopup = ref(null)
|
const callPopup = ref(null)
|
||||||
const showCallPopup = ref(false)
|
const showCallPopup = ref(false)
|
||||||
@ -192,7 +168,6 @@ let { style } = useDraggable(callPopup, {
|
|||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const showCallModal = ref(false)
|
|
||||||
const callStatus = ref('')
|
const callStatus = ref('')
|
||||||
const phoneNumber = ref('')
|
const phoneNumber = ref('')
|
||||||
const callData = ref(null)
|
const callData = ref(null)
|
||||||
@ -211,9 +186,7 @@ const contact = computed(() => {
|
|||||||
return _contact
|
return _contact
|
||||||
})
|
})
|
||||||
|
|
||||||
const note = ref(
|
const note = ref('')
|
||||||
'This is a note for the call. This is a note for the call. This is a note for the call. This is a note for the call.',
|
|
||||||
)
|
|
||||||
|
|
||||||
const showNote = ref(false)
|
const showNote = ref(false)
|
||||||
|
|
||||||
@ -227,7 +200,7 @@ function showNoteWindow() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const task = ref('This is a task for the call. This is a task for the call.')
|
const task = ref('')
|
||||||
|
|
||||||
const showTask = ref(false)
|
const showTask = ref(false)
|
||||||
|
|
||||||
@ -254,26 +227,18 @@ function updateWindowHeight(condition) {
|
|||||||
callPopup.value.style.top = updatedTop + 'px'
|
callPopup.value.style.top = updatedTop + 'px'
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMakeCallModal(number) {
|
function makeOutgoingCall(number) {
|
||||||
showCallModal.value = true
|
|
||||||
phoneNumber.value = number
|
phoneNumber.value = number
|
||||||
}
|
|
||||||
|
|
||||||
function makeCall() {
|
|
||||||
showCallModal.value = false
|
|
||||||
callStatus.value = 'Calling...'
|
callStatus.value = 'Calling...'
|
||||||
showCallPopup.value = true
|
showCallPopup.value = true
|
||||||
|
showSmallCallPopup.value = false
|
||||||
|
|
||||||
call('crm.integrations.exotel.handler.make_a_call', {
|
call('crm.integrations.exotel.handler.make_a_call', {
|
||||||
to_number: phoneNumber.value,
|
to_number: phoneNumber.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
function setup() {
|
||||||
$socket.off('exotel_call')
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
$socket.on('exotel_call', (data) => {
|
$socket.on('exotel_call', (data) => {
|
||||||
callData.value = data
|
callData.value = data
|
||||||
console.log(data)
|
console.log(data)
|
||||||
@ -284,8 +249,10 @@ onMounted(() => {
|
|||||||
showCallPopup.value = true
|
showCallPopup.value = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
setMakeCall(showMakeCallModal)
|
onBeforeUnmount(() => {
|
||||||
|
$socket.off('exotel_call')
|
||||||
})
|
})
|
||||||
|
|
||||||
function closeCallPopup() {
|
function closeCallPopup() {
|
||||||
@ -318,6 +285,12 @@ function updateStatus(data) {
|
|||||||
(data.Status == 'completed' || data.Status == 'no-answer')
|
(data.Status == 'completed' || data.Status == 'no-answer')
|
||||||
) {
|
) {
|
||||||
return data.Status == 'no-answer' ? 'No Answer' : 'Call Ended'
|
return data.Status == 'no-answer' ? 'No Answer' : 'Call Ended'
|
||||||
|
} else if (
|
||||||
|
data.EventType == 'terminal' &&
|
||||||
|
data.Direction == 'outbound-api' &&
|
||||||
|
data.Status == 'busy'
|
||||||
|
) {
|
||||||
|
return 'No Answer'
|
||||||
}
|
}
|
||||||
|
|
||||||
// incoming call
|
// incoming call
|
||||||
@ -335,4 +308,6 @@ function updateStatus(data) {
|
|||||||
return 'Call Ended'
|
return 'Call Ended'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({ makeOutgoingCall, setup })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
537
frontend/src/components/Telephony/TwilioCallUI.vue
Normal file
537
frontend/src/components/Telephony/TwilioCallUI.vue
Normal file
@ -0,0 +1,537 @@
|
|||||||
|
<template>
|
||||||
|
<div v-show="showCallPopup" v-bind="$attrs">
|
||||||
|
<div
|
||||||
|
ref="callPopup"
|
||||||
|
class="fixed z-20 flex w-60 cursor-move select-none flex-col rounded-lg bg-surface-gray-7 p-4 text-ink-gray-2 shadow-2xl"
|
||||||
|
:style="style"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row-reverse items-center gap-1">
|
||||||
|
<MinimizeIcon
|
||||||
|
class="h-4 w-4 cursor-pointer"
|
||||||
|
@click="toggleCallWindow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center gap-3">
|
||||||
|
<Avatar
|
||||||
|
:image="contact.image"
|
||||||
|
:label="contact.full_name"
|
||||||
|
class="relative flex !h-24 !w-24 items-center justify-center [&>div]:text-[30px]"
|
||||||
|
:class="onCall || calling ? '' : 'pulse'"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col items-center justify-center gap-1">
|
||||||
|
<div class="text-xl font-medium">
|
||||||
|
{{ contact.full_name }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-ink-gray-5">{{ contact.mobile_no }}</div>
|
||||||
|
</div>
|
||||||
|
<CountUpTimer ref="counterUp">
|
||||||
|
<div v-if="onCall" class="my-1 text-base">
|
||||||
|
{{ counterUp?.updatedTime }}
|
||||||
|
</div>
|
||||||
|
</CountUpTimer>
|
||||||
|
<div v-if="!onCall" class="my-1 text-base">
|
||||||
|
{{
|
||||||
|
callStatus == 'initiating'
|
||||||
|
? __('Initiating call...')
|
||||||
|
: callStatus == 'ringing'
|
||||||
|
? __('Ringing...')
|
||||||
|
: calling
|
||||||
|
? __('Calling...')
|
||||||
|
: __('Incoming call...')
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div v-if="onCall" class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
:icon="muted ? 'mic-off' : 'mic'"
|
||||||
|
class="rounded-full"
|
||||||
|
@click="toggleMute"
|
||||||
|
/>
|
||||||
|
<!-- <Button class="rounded-full">
|
||||||
|
<template #icon>
|
||||||
|
<DialpadIcon class="cursor-pointer rounded-full" />
|
||||||
|
</template>
|
||||||
|
</Button> -->
|
||||||
|
<Button class="rounded-full">
|
||||||
|
<template #icon>
|
||||||
|
<NoteIcon
|
||||||
|
class="h-4 w-4 cursor-pointer rounded-full text-ink-gray-9"
|
||||||
|
@click="showNoteModal = true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
<Button class="rounded-full bg-surface-red-5 hover:bg-surface-red-6">
|
||||||
|
<template #icon>
|
||||||
|
<PhoneIcon
|
||||||
|
class="h-4 w-4 rotate-[135deg] fill-white text-ink-white"
|
||||||
|
@click="hangUpCall"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="calling || callStatus == 'initiating'">
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
variant="solid"
|
||||||
|
theme="red"
|
||||||
|
:label="__('Cancel')"
|
||||||
|
@click="cancelCall"
|
||||||
|
class="rounded-lg"
|
||||||
|
:disabled="callStatus == 'initiating'"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<PhoneIcon class="h-4 w-4 rotate-[135deg] fill-white" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
variant="solid"
|
||||||
|
theme="green"
|
||||||
|
:label="__('Accept')"
|
||||||
|
class="rounded-lg"
|
||||||
|
@click="acceptIncomingCall"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<PhoneIcon class="h-4 w-4 fill-white" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
variant="solid"
|
||||||
|
theme="red"
|
||||||
|
:label="__('Reject')"
|
||||||
|
class="rounded-lg"
|
||||||
|
@click="rejectIncomingCall"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<PhoneIcon class="h-4 w-4 rotate-[135deg] fill-white" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-show="showSmallCallWindow"
|
||||||
|
class="ml-2 flex cursor-pointer select-none items-center justify-between gap-3 rounded-lg bg-surface-gray-7 px-2 py-[7px] text-base text-ink-gray-2"
|
||||||
|
@click="toggleCallWindow"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Avatar
|
||||||
|
:image="contact.image"
|
||||||
|
:label="contact.full_name"
|
||||||
|
class="relative flex !h-5 !w-5 items-center justify-center"
|
||||||
|
/>
|
||||||
|
<div class="max-w-[120px] truncate">
|
||||||
|
{{ contact.full_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="onCall" class="flex items-center gap-2">
|
||||||
|
<div class="my-1 min-w-[40px] text-center">
|
||||||
|
{{ counterUp?.updatedTime }}
|
||||||
|
</div>
|
||||||
|
<Button variant="solid" theme="red" class="!h-6 !w-6 rounded-full">
|
||||||
|
<template #icon>
|
||||||
|
<PhoneIcon
|
||||||
|
class="h-4 w-4 rotate-[135deg] fill-white"
|
||||||
|
@click.stop="hangUpCall"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="calling" class="flex items-center gap-3">
|
||||||
|
<div class="my-1">
|
||||||
|
{{ callStatus == 'ringing' ? __('Ringing...') : __('Calling...') }}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
theme="red"
|
||||||
|
class="!h-6 !w-6 rounded-full"
|
||||||
|
@click.stop="cancelCall"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<PhoneIcon class="h-4 w-4 rotate-[135deg] fill-white" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
theme="green"
|
||||||
|
class="pulse relative !h-6 !w-6 rounded-full"
|
||||||
|
@click.stop="acceptIncomingCall"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<PhoneIcon class="h-4 w-4 animate-pulse fill-white" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
theme="red"
|
||||||
|
class="!h-6 !w-6 rounded-full"
|
||||||
|
@click.stop="rejectIncomingCall"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<PhoneIcon class="h-4 w-4 rotate-[135deg] fill-white" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NoteModal
|
||||||
|
v-model="showNoteModal"
|
||||||
|
:note="note"
|
||||||
|
doctype="CRM Call Log"
|
||||||
|
@after="updateNote"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||||
|
import MinimizeIcon from '@/components/Icons/MinimizeIcon.vue'
|
||||||
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
|
import CountUpTimer from '@/components/CountUpTimer.vue'
|
||||||
|
import NoteModal from '@/components/Modals/NoteModal.vue'
|
||||||
|
import { Device } from '@twilio/voice-sdk'
|
||||||
|
import { useDraggable, useWindowSize } from '@vueuse/core'
|
||||||
|
import { contactsStore } from '@/stores/contacts'
|
||||||
|
import { capture } from '@/telemetry'
|
||||||
|
import { Avatar, call } from 'frappe-ui'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const { getContact, getLeadContact } = contactsStore()
|
||||||
|
|
||||||
|
let device = ''
|
||||||
|
let log = ref('Connecting...')
|
||||||
|
let _call = null
|
||||||
|
const contact = ref({
|
||||||
|
full_name: '',
|
||||||
|
mobile_no: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
let showCallPopup = ref(false)
|
||||||
|
let showSmallCallWindow = ref(false)
|
||||||
|
let onCall = ref(false)
|
||||||
|
let calling = ref(false)
|
||||||
|
let muted = ref(false)
|
||||||
|
let callPopup = ref(null)
|
||||||
|
let counterUp = ref(null)
|
||||||
|
let callStatus = ref('')
|
||||||
|
const showNoteModal = ref(false)
|
||||||
|
const note = ref({
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
async function updateNote(_note, insert_mode = false) {
|
||||||
|
note.value = _note
|
||||||
|
if (insert_mode && _note.name) {
|
||||||
|
await call('crm.integrations.twilio.api.add_note_to_call_log', {
|
||||||
|
call_sid: _call.parameters.CallSid,
|
||||||
|
note: _note.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { width, height } = useWindowSize()
|
||||||
|
|
||||||
|
let { style } = useDraggable(callPopup, {
|
||||||
|
initialValue: { x: width.value - 280, y: height.value - 310 },
|
||||||
|
preventDefault: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function startupClient() {
|
||||||
|
log.value = 'Requesting Access Token...'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await call('crm.integrations.twilio.api.generate_access_token')
|
||||||
|
log.value = 'Got a token.'
|
||||||
|
intitializeDevice(data.token)
|
||||||
|
} catch (err) {
|
||||||
|
log.value = 'An error occurred. ' + err.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function intitializeDevice(token) {
|
||||||
|
device = new Device(token, {
|
||||||
|
codecPreferences: ['opus', 'pcmu'],
|
||||||
|
fakeLocalDTMF: true,
|
||||||
|
enableRingingState: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
addDeviceListeners()
|
||||||
|
|
||||||
|
device.register()
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDeviceListeners() {
|
||||||
|
device.on('registered', () => {
|
||||||
|
log.value = 'Ready to make and receive calls!'
|
||||||
|
})
|
||||||
|
|
||||||
|
device.on('unregistered', (device) => {
|
||||||
|
log.value = 'Logged out'
|
||||||
|
})
|
||||||
|
|
||||||
|
device.on('error', (error) => {
|
||||||
|
log.value = 'Twilio.Device Error: ' + error.message
|
||||||
|
})
|
||||||
|
|
||||||
|
device.on('incoming', handleIncomingCall)
|
||||||
|
|
||||||
|
device.on('tokenWillExpire', async () => {
|
||||||
|
const data = await call('crm.integrations.twilio.api.generate_access_token')
|
||||||
|
device.updateToken(data.token)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMute() {
|
||||||
|
if (_call.isMuted()) {
|
||||||
|
_call.mute(false)
|
||||||
|
muted.value = false
|
||||||
|
} else {
|
||||||
|
_call.mute()
|
||||||
|
muted.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleIncomingCall(call) {
|
||||||
|
log.value = `Incoming call from ${call.parameters.From}`
|
||||||
|
|
||||||
|
// get name of the caller from the phone number
|
||||||
|
contact.value = getContact(call.parameters.From)
|
||||||
|
if (!contact.value) {
|
||||||
|
contact.value = getLeadContact(call.parameters.From)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contact.value) {
|
||||||
|
contact.value = {
|
||||||
|
full_name: __('Unknown'),
|
||||||
|
mobile_no: call.parameters.From,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showCallPopup.value = true
|
||||||
|
_call = call
|
||||||
|
|
||||||
|
_call.on('accept', (conn) => {
|
||||||
|
console.log('conn', conn)
|
||||||
|
})
|
||||||
|
|
||||||
|
// add event listener to call object
|
||||||
|
call.on('cancel', handleDisconnectedIncomingCall)
|
||||||
|
call.on('disconnect', handleDisconnectedIncomingCall)
|
||||||
|
call.on('reject', handleDisconnectedIncomingCall)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acceptIncomingCall() {
|
||||||
|
log.value = 'Accepted incoming call.'
|
||||||
|
onCall.value = true
|
||||||
|
await _call.accept()
|
||||||
|
counterUp.value.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectIncomingCall() {
|
||||||
|
_call.reject()
|
||||||
|
log.value = 'Rejected incoming call'
|
||||||
|
showCallPopup.value = false
|
||||||
|
if (showSmallCallWindow.value == undefined) {
|
||||||
|
showSmallCallWindow = false
|
||||||
|
} else {
|
||||||
|
showSmallCallWindow.value = false
|
||||||
|
}
|
||||||
|
callStatus.value = ''
|
||||||
|
muted.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function hangUpCall() {
|
||||||
|
_call.disconnect()
|
||||||
|
log.value = 'Hanging up incoming call'
|
||||||
|
onCall.value = false
|
||||||
|
callStatus.value = ''
|
||||||
|
muted.value = false
|
||||||
|
note.value = {
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
}
|
||||||
|
counterUp.value.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDisconnectedIncomingCall() {
|
||||||
|
log.value = `Call ended from handle disconnected Incoming call.`
|
||||||
|
showCallPopup.value = false
|
||||||
|
if (showSmallCallWindow.value == undefined) {
|
||||||
|
showSmallCallWindow = false
|
||||||
|
} else {
|
||||||
|
showSmallCallWindow.value = false
|
||||||
|
}
|
||||||
|
_call = null
|
||||||
|
muted.value = false
|
||||||
|
onCall.value = false
|
||||||
|
counterUp.value.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeOutgoingCall(number) {
|
||||||
|
// check if number has a country code
|
||||||
|
// if (number?.replace(/[^0-9+]/g, '').length == 10) {
|
||||||
|
// $dialog({
|
||||||
|
// title: 'Invalid Mobile Number',
|
||||||
|
// message: `${number} is not a valid mobile number. Either add a country code or check the number again.`,
|
||||||
|
// })
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
contact.value = getContact(number)
|
||||||
|
if (!contact.value) {
|
||||||
|
contact.value = getLeadContact(number)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device) {
|
||||||
|
log.value = `Attempting to call ${number} ...`
|
||||||
|
|
||||||
|
try {
|
||||||
|
_call = await device.connect({
|
||||||
|
params: { To: number },
|
||||||
|
})
|
||||||
|
|
||||||
|
showCallPopup.value = true
|
||||||
|
callStatus.value = 'initiating'
|
||||||
|
|
||||||
|
capture('make_outgoing_call')
|
||||||
|
|
||||||
|
_call.on('messageReceived', (message) => {
|
||||||
|
let info = message.content
|
||||||
|
callStatus.value = info.CallStatus
|
||||||
|
|
||||||
|
log.value = `Call status: ${info.CallStatus}`
|
||||||
|
|
||||||
|
if (info.CallStatus == 'in-progress') {
|
||||||
|
log.value = `Call in progress.`
|
||||||
|
calling.value = false
|
||||||
|
onCall.value = true
|
||||||
|
counterUp.value.start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
_call.on('accept', () => {
|
||||||
|
log.value = `Initiated call!`
|
||||||
|
showCallPopup.value = true
|
||||||
|
calling.value = true
|
||||||
|
onCall.value = false
|
||||||
|
})
|
||||||
|
_call.on('disconnect', (conn) => {
|
||||||
|
log.value = `Call ended from makeOutgoing call disconnect.`
|
||||||
|
calling.value = false
|
||||||
|
onCall.value = false
|
||||||
|
showCallPopup.value = false
|
||||||
|
showSmallCallWindow = false
|
||||||
|
_call = null
|
||||||
|
callStatus.value = ''
|
||||||
|
muted.value = false
|
||||||
|
counterUp.value.stop()
|
||||||
|
note.value = {
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
_call.on('cancel', () => {
|
||||||
|
log.value = `Call ended from makeOutgoing call cancel.`
|
||||||
|
calling.value = false
|
||||||
|
onCall.value = false
|
||||||
|
showCallPopup.value = false
|
||||||
|
showSmallCallWindow = false
|
||||||
|
_call = null
|
||||||
|
callStatus.value = ''
|
||||||
|
muted.value = false
|
||||||
|
note.value = {
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
}
|
||||||
|
counterUp.value.stop()
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
log.value = `Could not connect call: ${error.message}`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.value = 'Unable to make call.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelCall() {
|
||||||
|
_call.disconnect()
|
||||||
|
showCallPopup.value = false
|
||||||
|
if (showSmallCallWindow.value == undefined) {
|
||||||
|
showSmallCallWindow = false
|
||||||
|
} else {
|
||||||
|
showSmallCallWindow.value = false
|
||||||
|
}
|
||||||
|
calling.value = false
|
||||||
|
onCall.value = false
|
||||||
|
callStatus.value = ''
|
||||||
|
muted.value = false
|
||||||
|
note.value = {
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCallWindow() {
|
||||||
|
showCallPopup.value = !showCallPopup.value
|
||||||
|
if (showSmallCallWindow.value == undefined) {
|
||||||
|
showSmallCallWindow = !showSmallCallWindow
|
||||||
|
} else {
|
||||||
|
showSmallCallWindow.value = !showSmallCallWindow.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => log.value,
|
||||||
|
(value) => {
|
||||||
|
console.log(value)
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
defineExpose({ makeOutgoingCall, setup: startupClient })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pulse::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid green;
|
||||||
|
width: calc(100% + 20px);
|
||||||
|
height: calc(100% + 20px);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: pulse 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid green;
|
||||||
|
width: calc(100% + 20px);
|
||||||
|
height: calc(100% + 20px);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: pulse 1s linear infinite;
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.5);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1.3);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -21,12 +21,18 @@ createResource({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const callEnabled = ref(false)
|
export const callEnabled = ref(false)
|
||||||
|
export const twilioEnabled = ref(false)
|
||||||
|
export const exotelEnabled = ref(false)
|
||||||
|
export const defaultCallingMedium = ref('')
|
||||||
createResource({
|
createResource({
|
||||||
url: 'crm.integrations.twilio.api.is_enabled',
|
url: 'crm.integrations.api.is_call_integration_enabled',
|
||||||
cache: 'Is Twilio Enabled',
|
cache: 'Is Call Integration Enabled',
|
||||||
auto: true,
|
auto: true,
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
callEnabled.value = Boolean(data)
|
twilioEnabled.value = Boolean(data.twilio_enabled)
|
||||||
|
exotelEnabled.value = Boolean(data.exotel_enabled)
|
||||||
|
defaultCallingMedium.value = data.default_calling_medium
|
||||||
|
callEnabled.value = twilioEnabled.value || exotelEnabled.value
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -5,13 +5,8 @@ export const globalStore = defineStore('crm-global', () => {
|
|||||||
const app = getCurrentInstance()
|
const app = getCurrentInstance()
|
||||||
const { $dialog, $socket } = app.appContext.config.globalProperties
|
const { $dialog, $socket } = app.appContext.config.globalProperties
|
||||||
|
|
||||||
let twilioEnabled = ref(false)
|
|
||||||
let callMethod = () => {}
|
let callMethod = () => {}
|
||||||
|
|
||||||
function setTwilioEnabled(value) {
|
|
||||||
twilioEnabled.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMakeCall(value) {
|
function setMakeCall(value) {
|
||||||
callMethod = value
|
callMethod = value
|
||||||
}
|
}
|
||||||
@ -23,9 +18,7 @@ export const globalStore = defineStore('crm-global', () => {
|
|||||||
return {
|
return {
|
||||||
$dialog,
|
$dialog,
|
||||||
$socket,
|
$socket,
|
||||||
twilioEnabled,
|
|
||||||
makeCall,
|
makeCall,
|
||||||
setTwilioEnabled,
|
|
||||||
setMakeCall,
|
setMakeCall,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user