fix: created exotel call ui
This commit is contained in:
parent
4d868a788e
commit
a716bdf0da
@ -28,6 +28,7 @@ def handle_request(**kwargs):
|
||||
return
|
||||
|
||||
call_payload = kwargs
|
||||
|
||||
frappe.publish_realtime("exotel_call", call_payload)
|
||||
status = call_payload.get("Status")
|
||||
if status == "free":
|
||||
@ -56,11 +57,12 @@ def handle_request(**kwargs):
|
||||
|
||||
# Outgoing Call
|
||||
@frappe.whitelist()
|
||||
def make_a_call(from_number, to_number, caller_id=None, link_to_document=None):
|
||||
def make_a_call(to_number, from_number=None, caller_id=None, link_to_document=None):
|
||||
if not is_integration_enabled():
|
||||
frappe.throw(_("Please setup Exotel intergration"), title=_("Integration Not Enabled"))
|
||||
|
||||
endpoint = get_exotel_endpoint("Calls/connect.json?details=true")
|
||||
|
||||
if not from_number:
|
||||
from_number = frappe.get_value("CRM Exotel Agent", {"user": frappe.session.user}, "mobile_no")
|
||||
|
||||
@ -91,8 +93,10 @@ def make_a_call(from_number, to_number, caller_id=None, link_to_document=None):
|
||||
else:
|
||||
res = response.json()
|
||||
call_payload = res.get("Call", {})
|
||||
|
||||
if link_to_document:
|
||||
link_to_document = json.loads(link_to_document)
|
||||
|
||||
create_call_log(
|
||||
call_id=call_payload.get("Sid"),
|
||||
from_number=call_payload.get("From"),
|
||||
@ -105,7 +109,7 @@ def make_a_call(from_number, to_number, caller_id=None, link_to_document=None):
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_exotel_endpoint(action):
|
||||
def get_exotel_endpoint(action=None):
|
||||
settings = get_exotel_settings()
|
||||
return "https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/{action}".format(
|
||||
api_key=settings.api_key,
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit d82b3a12eeb6cb9e83375550508b462ce5cfdaf2
|
||||
Subproject commit 99f0b86f15e094b95c32e87494e003c974b4f0df
|
||||
17
frontend/src/components/Icons/AvatarIcon.vue
Normal file
17
frontend/src/components/Icons/AvatarIcon.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8 7C9.65685 7 11 5.65685 11 4C11 2.34315 9.65685 1 8 1C6.34315 1 5 2.34315 5 4C5 5.65685 6.34315 7 8 7ZM7.5969 8C4.50582 8 2 10.5058 2 13.5969C2 14.118 2.37677 14.5628 2.89081 14.6485L4.52397 14.9207C6.82545 15.3042 9.17455 15.3042 11.476 14.9207L13.1092 14.6485C13.6232 14.5628 14 14.118 14 13.5969C14 10.5058 11.4942 8 8.4031 8H7.5969Z"
|
||||
fill="currentColor"
|
||||
fill-opacity="0.54"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -9,8 +9,9 @@
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3.99707 14H11.9971C12.8255 14 13.4971 13.3284 13.4971 12.5V6.49988L12 6.49989C10.6193 6.4999 9.5 5.38061 9.5 3.99989V2H3.99707C3.16864 2 2.49707 2.67157 2.49707 3.5V12.5C2.49707 13.3284 3.16864 14 3.99707 14ZM13.8291 4.2806C14.1476 4.62366 14.3612 5.04668 14.4502 5.49987L14.4968 5.49987L14.4968 5.94777L14.4971 5.98173V12.5C14.4971 13.8807 13.3778 15 11.9971 15H3.99707C2.61636 15 1.49707 13.8807 1.49707 12.5V3.5C1.49707 2.11929 2.61636 1 3.99707 1H9.69261C10.3878 1 11.0516 1.28945 11.5246 1.79887L13.8291 4.2806ZM12 5.49989L13.4176 5.49988C13.3502 5.30132 13.2414 5.11735 13.0963 4.96105L10.7918 2.47932C10.7044 2.38525 10.6063 2.30368 10.5 2.23582V3.99989C10.5 4.82832 11.1716 5.4999 12 5.49989ZM5 11C4.72386 11 4.5 11.2239 4.5 11.5C4.5 11.7761 4.72386 12 5 12H8C8.27614 12 8.5 11.7761 8.5 11.5C8.5 11.2239 8.27614 11 8 11H5ZM10 9.5H5H4.75C4.47386 9.5 4.25 9.27614 4.25 9C4.25 8.72386 4.47386 8.5 4.75 8.5H5H10H11.25C11.5261 8.5 11.75 8.72386 11.75 9C11.75 9.27614 11.5261 9.5 11.25 9.5H10Z"
|
||||
d="M3.99707 14H11.9971C12.8255 14 13.4971 13.3284 13.4971 12.5V6.49988L11 6.4999C10.1716 6.49991 9.5 5.82833 9.5 4.9999V2H3.99707C3.16864 2 2.49707 2.67157 2.49707 3.5V12.5C2.49707 13.3284 3.16864 14 3.99707 14ZM13.8291 4.2806C14.1476 4.62366 14.3612 5.04668 14.4502 5.49987L14.4968 5.49987L14.4968 5.94777L14.4971 5.98173V12.5C14.4971 13.8807 13.3778 15 11.9971 15H3.99707C2.61636 15 1.49707 13.8807 1.49707 12.5V3.5C1.49707 2.11929 2.61636 1 3.99707 1H9.69261C10.3878 1 11.0516 1.28945 11.5246 1.79887L13.8291 4.2806ZM11 5.4999L13.4176 5.49988C13.3502 5.30132 13.2414 5.11735 13.0963 4.96105L10.7918 2.47932C10.7044 2.38525 10.6063 2.30368 10.5 2.23582V4.9999C10.5 5.27604 10.7239 5.4999 11 5.4999ZM5 11C4.72386 11 4.5 11.2239 4.5 11.5C4.5 11.7761 4.72386 12 5 12H11C11.2761 12 11.5 11.7761 11.5 11.5C11.5 11.2239 11.2761 11 11 11H5ZM4.5 9C4.5 8.72386 4.72386 8.5 5 8.5H10H11C11.2761 8.5 11.5 8.72386 11.5 9C11.5 9.27614 11.2761 9.5 11 9.5H10H5C4.72386 9.5 4.5 9.27614 4.5 9Z"
|
||||
fill="currentColor"
|
||||
fill-opacity="0.72"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -2,11 +2,13 @@
|
||||
<div class="flex border-b pr-5">
|
||||
<div id="app-header" class="flex-1"></div>
|
||||
<div class="flex items-center justify-center">
|
||||
<ExotelCallUI />
|
||||
<CallUI />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import CallUI from '@/components/CallUI.vue'
|
||||
import ExotelCallUI from '@/components/Telephony/ExotelCallUI.vue'
|
||||
import CallUI from '@/components/Telephony/CallUI.vue'
|
||||
</script>
|
||||
|
||||
@ -12,10 +12,12 @@
|
||||
<div id="app-header" class="flex-1" />
|
||||
</div>
|
||||
<CallUI class="mr-3 mt-2" />
|
||||
<ExotelCallUI class="mr-3 mt-2" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MenuIcon from '@/components/Icons/MenuIcon.vue'
|
||||
import CallUI from '@/components/CallUI.vue'
|
||||
import ExotelCallUI from '@/components/Telephony/ExotelCallUI.vue'
|
||||
import CallUI from '@/components/Telephony/CallUI.vue'
|
||||
import { mobileSidebarOpened as sidebarOpened } from '@/composables/settings'
|
||||
</script>
|
||||
|
||||
306
frontend/src/components/Telephony/ExotelCallUI.vue
Normal file
306
frontend/src/components/Telephony/ExotelCallUI.vue
Normal file
@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-show="showSmallCallPopup"
|
||||
class="ml-2 flex cursor-pointer select-none items-center justify-between gap-2 rounded-full bg-surface-gray-7 px-1.5 py-[7px] text-base text-ink-gray-2"
|
||||
@click="toggleCallPopup"
|
||||
>
|
||||
<div
|
||||
class="flex justify-center items-center size-5 rounded-full bg-surface-gray-6"
|
||||
>
|
||||
<Avatar
|
||||
v-if="contact.image"
|
||||
:image="contact.image"
|
||||
:label="contact.full_name"
|
||||
class="size-3"
|
||||
/>
|
||||
<AvatarIcon v-else class="size-3" />
|
||||
</div>
|
||||
<div class="text-base font-medium">
|
||||
<span class="">{{ phoneNumber }}</span>
|
||||
<span class="font-normal text-ink-gray-4"> · </span>
|
||||
<span
|
||||
v-if="callStatus == 'In Progress'"
|
||||
class="font-normal text-ink-gray-4"
|
||||
>
|
||||
00:38
|
||||
</span>
|
||||
<span v-else class="font-normal text-ink-gray-4">{{ callStatus }}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="solid"
|
||||
theme="red"
|
||||
class="text-white !size-5 rounded-full"
|
||||
@click="endCall"
|
||||
>
|
||||
<template #icon>
|
||||
<PhoneIcon class="w-3 h-3 rotate-[135deg]" />
|
||||
</template>
|
||||
</Button>
|
||||
</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
|
||||
v-show="showCallPopup"
|
||||
ref="callPopup"
|
||||
class="fixed z-20 w-[280px] min-h-44 flex gap-2 cursor-move select-none flex-col rounded-lg bg-surface-gray-7 p-4 pt-2.5 text-ink-gray-2 shadow-2xl"
|
||||
:style="style"
|
||||
>
|
||||
<!-- <pre>{{ callData }}</pre> -->
|
||||
<div class="header flex items-center justify-between gap-2 text-base">
|
||||
<div v-if="callStatus == 'In Progress'">00:38</div>
|
||||
<div v-else>{{ __(callStatus) }}</div>
|
||||
<Button
|
||||
@click="toggleCallPopup"
|
||||
class="bg-surface-gray-7 text-ink-white hover:bg-surface-gray-6"
|
||||
size="md"
|
||||
>
|
||||
<template #icon>
|
||||
<MinimizeIcon class="h-4 w-4 cursor-pointer" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="body flex-1">
|
||||
<div v-if="showNote" class="h-[294px] text-base">{{ note }}</div>
|
||||
<div v-else class="flex items-center gap-3">
|
||||
<Avatar
|
||||
v-if="contact.image"
|
||||
:image="contact.image"
|
||||
:label="contact.full_name"
|
||||
class="!size-8"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex justify-center items-center size-8 rounded-full bg-surface-gray-6"
|
||||
>
|
||||
<AvatarIcon class="size-4" />
|
||||
</div>
|
||||
<div v-if="contact.full_name" class="flex flex-col gap-1.5">
|
||||
<div class="text-lg font-medium">{{ contact.full_name }}</div>
|
||||
<div class="text-base text-ink-gray-6">{{ phoneNumber }}</div>
|
||||
</div>
|
||||
<div v-else class="text-lg font-medium">{{ phoneNumber }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer flex justify-between gap-2">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
class="bg-surface-gray-6 text-ink-white hover:bg-surface-gray-5"
|
||||
:icon="mute ? 'mic-off' : 'mic'"
|
||||
size="md"
|
||||
@click="toggleMute"
|
||||
/>
|
||||
<Button
|
||||
class="bg-surface-gray-6 text-ink-white hover:bg-surface-gray-5"
|
||||
size="md"
|
||||
@click="showNoteWindow"
|
||||
>
|
||||
<template #icon>
|
||||
<NoteIcon class="w-4 h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
:icon="'more-horizontal'"
|
||||
class="bg-surface-gray-6 text-ink-white hover:bg-surface-gray-5"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="solid"
|
||||
theme="red"
|
||||
class="text-white"
|
||||
size="md"
|
||||
@click="endCall"
|
||||
>
|
||||
<template #icon>
|
||||
<PhoneIcon class="w-4 h-4 rotate-[135deg]" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import AvatarIcon from '@/components/Icons/AvatarIcon.vue'
|
||||
import MinimizeIcon from '@/components/Icons/MinimizeIcon.vue'
|
||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import { Button, Dialog, FormControl, call, Avatar } from 'frappe-ui'
|
||||
import { globalStore } from '@/stores/global'
|
||||
import { contactsStore } from '@/stores/contacts'
|
||||
import { useDraggable, useWindowSize } from '@vueuse/core'
|
||||
import { ref, onBeforeUnmount, onMounted } from 'vue'
|
||||
|
||||
const { getContact, getLeadContact } = contactsStore()
|
||||
const { $socket, setMakeCall } = globalStore()
|
||||
|
||||
const callPopup = ref(null)
|
||||
const showCallPopup = ref(false)
|
||||
const showSmallCallPopup = ref(false)
|
||||
|
||||
function toggleCallPopup() {
|
||||
showCallPopup.value = !showCallPopup.value
|
||||
if (showSmallCallPopup.value == undefined) {
|
||||
showSmallCallPopup = !showSmallCallPopup
|
||||
} else {
|
||||
showSmallCallPopup.value = !showSmallCallPopup.value
|
||||
}
|
||||
}
|
||||
|
||||
const { width, height } = useWindowSize()
|
||||
|
||||
let { style } = useDraggable(callPopup, {
|
||||
initialValue: { x: width.value - 350, y: height.value - 250 },
|
||||
preventDefault: true,
|
||||
})
|
||||
|
||||
const showCallModal = ref(false)
|
||||
const callStatus = ref('')
|
||||
const phoneNumber = ref('09821259504')
|
||||
const callData = ref(null)
|
||||
|
||||
const contact = ref({
|
||||
full_name: '',
|
||||
mobile_no: '',
|
||||
})
|
||||
|
||||
const mute = ref(false)
|
||||
|
||||
function toggleMute() {
|
||||
mute.value = !mute.value
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
function showNoteWindow() {
|
||||
showNote.value = !showNote.value
|
||||
|
||||
let top = parseInt(callPopup.value.style.top)
|
||||
let updatedTop = 0
|
||||
|
||||
updatedTop = showNote.value ? top - 224 : top + 224
|
||||
|
||||
if (updatedTop < 0) {
|
||||
updatedTop = 10
|
||||
}
|
||||
|
||||
callPopup.value.style.top = updatedTop + 'px'
|
||||
}
|
||||
|
||||
function showMakeCallModal(number) {
|
||||
showCallModal.value = true
|
||||
phoneNumber.value = number
|
||||
}
|
||||
|
||||
function makeCall() {
|
||||
contact.value = getContact(phoneNumber.value)
|
||||
if (!contact.value) {
|
||||
contact.value = getLeadContact(phoneNumber.value)
|
||||
}
|
||||
|
||||
showCallModal.value = false
|
||||
callStatus.value = 'Calling...'
|
||||
showCallPopup.value = true
|
||||
|
||||
call('crm.integrations.exotel.handler.make_a_call', {
|
||||
to_number: phoneNumber.value,
|
||||
from_number: '07666980887',
|
||||
caller_id: '08047091710',
|
||||
})
|
||||
}
|
||||
|
||||
function endCall() {
|
||||
callStatus.value = ''
|
||||
showCallPopup.value = false
|
||||
showSmallCallPopup.value = false
|
||||
note.value = ''
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
$socket.off('exotel_call')
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
$socket.on('exotel_call', (data) => {
|
||||
callData.value = data
|
||||
console.log(data)
|
||||
|
||||
if (
|
||||
data.EventType == 'answered' &&
|
||||
data.Direction == 'outbound-api' &&
|
||||
data.Status == 'in-progress' &&
|
||||
data['Legs[0][Status]'] == 'in-progress' &&
|
||||
data['Legs[1][Status]'] == ''
|
||||
) {
|
||||
callStatus.value = 'Ringing...'
|
||||
} else if (
|
||||
data.EventType == 'answered' &&
|
||||
data.Direction == 'outbound-api' &&
|
||||
data.Status == 'in-progress' &&
|
||||
data['Legs[1][Status]'] == 'in-progress'
|
||||
) {
|
||||
callStatus.value = 'In Progress'
|
||||
} else if (
|
||||
data.EventType == 'terminal' &&
|
||||
data.Direction == 'outbound-api' &&
|
||||
(data.Status == 'completed' || data.Status == 'no-answer')
|
||||
) {
|
||||
callStatus.value = 'Call Ended'
|
||||
}
|
||||
|
||||
if (
|
||||
data.EventType == 'Dial' &&
|
||||
data.Direction == 'incoming' &&
|
||||
data.Status == 'busy'
|
||||
) {
|
||||
callStatus.value = 'Incoming Call'
|
||||
} else if (
|
||||
data.EventType == 'Terminal' &&
|
||||
data.Direction == 'incoming' &&
|
||||
data.Status == 'free'
|
||||
) {
|
||||
callStatus.value = 'Call Ended'
|
||||
}
|
||||
|
||||
if (!showCallPopup.value && !showSmallCallPopup.value) {
|
||||
showCallPopup.value = true
|
||||
}
|
||||
|
||||
if (callStatus.value == 'Call Ended') {
|
||||
setTimeout(() => {
|
||||
showCallPopup.value = false
|
||||
showSmallCallPopup.value = false
|
||||
note.value = ''
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
|
||||
setMakeCall(showMakeCallModal)
|
||||
})
|
||||
</script>
|
||||
@ -55,7 +55,7 @@
|
||||
v-if="note.content"
|
||||
:content="note.content"
|
||||
:editable="false"
|
||||
editor-class="!prose-sm max-w-none !text-sm text-ink-gray-5 focus:outline-none"
|
||||
editor-class="prose-sm text-sm max-w-none text-ink-gray-5 focus:outline-none"
|
||||
class="flex-1 overflow-hidden"
|
||||
/>
|
||||
<div class="mt-2 flex items-center justify-between gap-2">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user