fix: fix twilio contact viewing, note creation & linking code
This commit is contained in:
parent
e6d72146b5
commit
cfae3101c5
@ -24,34 +24,51 @@ def set_default_calling_medium(medium):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_and_add_note_to_call_log(call_sid, content):
|
||||
"""Add note to call log based on call sid."""
|
||||
note = frappe.get_doc(
|
||||
{
|
||||
"doctype": "FCRM Note",
|
||||
"content": content,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
def add_note_to_call_log(call_sid, note):
|
||||
"""Add/Update note to call log based on call sid."""
|
||||
_note = None
|
||||
if not note.get("name"):
|
||||
_note = frappe.get_doc(
|
||||
{
|
||||
"doctype": "FCRM Note",
|
||||
"content": note.get("content"),
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
call_log = frappe.get_cached_doc("CRM Call Log", call_sid)
|
||||
call_log.link_with_reference_doc("FCRM Note", _note.name)
|
||||
call_log.save(ignore_permissions=True)
|
||||
else:
|
||||
_note = frappe.set_value("FCRM Note", note.get("name"), "content", note.get("content"))
|
||||
|
||||
call_log = frappe.get_doc("CRM Call Log", call_sid)
|
||||
call_log.link_with_reference_doc("FCRM Note", note.name)
|
||||
call_log.save(ignore_permissions=True)
|
||||
return _note
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_and_add_task_to_call_log(call_sid, task):
|
||||
"""Add task to call log based on call sid."""
|
||||
_task = frappe.get_doc(
|
||||
{
|
||||
"doctype": "CRM Task",
|
||||
"title": task.get("title"),
|
||||
"description": task.get("description"),
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
def add_task_to_call_log(call_sid, task):
|
||||
"""Add/Update task to call log based on call sid."""
|
||||
_task = None
|
||||
if not task.get("name"):
|
||||
_task = frappe.get_doc(
|
||||
{
|
||||
"doctype": "CRM Task",
|
||||
"title": task.get("title"),
|
||||
"description": task.get("description"),
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
call_log = frappe.get_doc("CRM Call Log", call_sid)
|
||||
call_log.link_with_reference_doc("CRM Task", _task.name)
|
||||
call_log.save(ignore_permissions=True)
|
||||
else:
|
||||
_task = frappe.get_doc("CRM Task", task.get("name"))
|
||||
_task.update(
|
||||
{
|
||||
"title": task.get("title"),
|
||||
"description": task.get("description"),
|
||||
}
|
||||
)
|
||||
_task.save(ignore_permissions=True)
|
||||
|
||||
call_log = frappe.get_doc("CRM Call Log", call_sid)
|
||||
call_log.link_with_reference_doc("CRM Task", _task.name)
|
||||
call_log.save(ignore_permissions=True)
|
||||
return _task
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@ -4,8 +4,9 @@ import frappe
|
||||
from frappe import _
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from crm.integrations.api import get_contact_by_phone_number
|
||||
|
||||
from .twilio_handler import IncomingCall, Twilio, TwilioCallDetails
|
||||
from .utils import parse_mobile_no
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -69,13 +70,31 @@ def twilio_incoming_call_handler(**kwargs):
|
||||
|
||||
|
||||
def create_call_log(call_details: TwilioCallDetails):
|
||||
call_log = frappe.get_doc(
|
||||
{**call_details.to_dict(), "doctype": "CRM Call Log", "telephony_medium": "Twilio"}
|
||||
)
|
||||
call_log.reference_docname, call_log.reference_doctype = get_lead_or_deal_from_number(call_log)
|
||||
call_log.flags.ignore_permissions = True
|
||||
call_log.save()
|
||||
details = call_details.to_dict()
|
||||
|
||||
call_log = frappe.get_doc({**details, "doctype": "CRM Call Log", "telephony_medium": "Twilio"})
|
||||
|
||||
# link call log with lead/deal
|
||||
contact_number = details.get("from") if details.get("type") == "Incoming" else details.get("to")
|
||||
link(contact_number, call_log)
|
||||
|
||||
call_log.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
return call_log
|
||||
|
||||
|
||||
def link(contact_number, call_log):
|
||||
contact = get_contact_by_phone_number(contact_number)
|
||||
if contact.get("name"):
|
||||
doctype = "Contact"
|
||||
docname = contact.get("name")
|
||||
if contact.get("lead"):
|
||||
doctype = "CRM Lead"
|
||||
docname = contact.get("lead")
|
||||
elif contact.get("deal"):
|
||||
doctype = "CRM Deal"
|
||||
docname = contact.get("deal")
|
||||
call_log.link_with_reference_doc(doctype, docname)
|
||||
|
||||
|
||||
def update_call_log(call_sid, status=None):
|
||||
@ -84,18 +103,19 @@ def update_call_log(call_sid, status=None):
|
||||
if not (twilio and frappe.db.exists("CRM Call Log", call_sid)):
|
||||
return
|
||||
|
||||
call_details = twilio.get_call_info(call_sid)
|
||||
call_log = frappe.get_doc("CRM Call Log", call_sid)
|
||||
call_log.status = TwilioCallDetails.get_call_status(status or call_details.status)
|
||||
call_log.duration = call_details.duration
|
||||
call_log.start_time = get_datetime_from_timestamp(call_details.start_time)
|
||||
call_log.end_time = get_datetime_from_timestamp(call_details.end_time)
|
||||
if call_log.note and call_log.reference_docname:
|
||||
frappe.db.set_value("FCRM Note", call_log.note, "reference_doctype", call_log.reference_doctype)
|
||||
frappe.db.set_value("FCRM Note", call_log.note, "reference_docname", call_log.reference_docname)
|
||||
call_log.flags.ignore_permissions = True
|
||||
call_log.save()
|
||||
frappe.db.commit()
|
||||
try:
|
||||
call_details = twilio.get_call_info(call_sid)
|
||||
call_log = frappe.get_doc("CRM Call Log", call_sid)
|
||||
call_log.status = TwilioCallDetails.get_call_status(status or call_details.status)
|
||||
call_log.duration = call_details.duration
|
||||
call_log.start_time = get_datetime_from_timestamp(call_details.start_time)
|
||||
call_log.end_time = get_datetime_from_timestamp(call_details.end_time)
|
||||
call_log.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
return call_log
|
||||
except Exception:
|
||||
frappe.log_error(title="Error while updating call record")
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@ -106,7 +126,7 @@ def update_recording_info(**kwargs):
|
||||
call_sid = args.CallSid
|
||||
update_call_log(call_sid)
|
||||
frappe.db.set_value("CRM Call Log", call_sid, "recording_url", recording_url)
|
||||
except:
|
||||
except Exception:
|
||||
frappe.log_error(title=_("Failed to capture Twilio recording"))
|
||||
|
||||
|
||||
@ -128,7 +148,7 @@ def update_call_status_info(**kwargs):
|
||||
|
||||
client = Twilio.get_twilio_client()
|
||||
client.calls(args.ParentCallSid).user_defined_messages.create(content=json.dumps(call_info))
|
||||
except:
|
||||
except Exception:
|
||||
frappe.log_error(title=_("Failed to update Twilio call status"))
|
||||
|
||||
|
||||
@ -144,45 +164,3 @@ def get_datetime_from_timestamp(timestamp):
|
||||
system_timezone = frappe.utils.get_system_timezone()
|
||||
converted_datetime = datetime_utc_tz.astimezone(ZoneInfo(system_timezone))
|
||||
return frappe.utils.format_datetime(converted_datetime, "yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_note_to_call_log(call_sid, note):
|
||||
"""Add note to call log. based on child call sid."""
|
||||
twilio = Twilio.connect()
|
||||
if not twilio:
|
||||
return
|
||||
|
||||
call_details = twilio.get_call_info(call_sid)
|
||||
sid = call_sid if call_details.direction == "inbound" else call_details.parent_call_sid
|
||||
|
||||
frappe.db.set_value("CRM Call Log", sid, "note", note)
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_lead_or_deal_from_number(call):
|
||||
"""Get lead/deal from the given number."""
|
||||
|
||||
def find_record(doctype, mobile_no, where=""):
|
||||
mobile_no = parse_mobile_no(mobile_no)
|
||||
|
||||
query = f"""
|
||||
SELECT name, mobile_no
|
||||
FROM `tab{doctype}`
|
||||
WHERE CONCAT('+', REGEXP_REPLACE(mobile_no, '[^0-9]', '')) = {mobile_no}
|
||||
"""
|
||||
|
||||
data = frappe.db.sql(query + where, as_dict=True)
|
||||
return data[0].name if data else None
|
||||
|
||||
doctype = "CRM Deal"
|
||||
number = call.get("to") if call.type == "Outgoing" else call.get("from")
|
||||
|
||||
doc = find_record(doctype, number) or None
|
||||
if not doc:
|
||||
doctype = "CRM Lead"
|
||||
doc = find_record(doctype, number, "AND converted is not True")
|
||||
if not doc:
|
||||
doc = find_record(doctype, number)
|
||||
|
||||
return doc, doctype
|
||||
|
||||
@ -15,10 +15,3 @@ def merge_dicts(d1: dict, d2: dict):
|
||||
"""
|
||||
return {k: {**v, **d2.get(k, {})} for k, v in d1.items()}
|
||||
|
||||
|
||||
def parse_mobile_no(mobile_no: str):
|
||||
"""Parse mobile number to remove spaces, brackets, etc.
|
||||
>>> parse_mobile_no("+91 (766) 667 6666")
|
||||
... "+917666676666"
|
||||
"""
|
||||
return "".join([c for c in mobile_no if c.isdigit() or c == "+"])
|
||||
|
||||
@ -146,8 +146,8 @@
|
||||
ref="content"
|
||||
editor-class="prose-sm h-[290px] text-ink-white overflow-auto mt-1"
|
||||
:bubbleMenu="true"
|
||||
:content="note"
|
||||
@change="(val) => (note = val)"
|
||||
:content="note.content"
|
||||
@change="(val) => (note.content = val)"
|
||||
:placeholder="__('Take a note...')"
|
||||
/>
|
||||
</div>
|
||||
@ -210,8 +210,21 @@
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="(note && note != '<p></p>') || task.title"
|
||||
v-if="(note.name || task.name) && dirty"
|
||||
@click="update"
|
||||
class="bg-surface-white !text-ink-gray-9 hover:!bg-surface-gray-3"
|
||||
variant="solid"
|
||||
:label="__('Update')"
|
||||
size="md"
|
||||
/>
|
||||
<Button
|
||||
v-else-if="
|
||||
((note?.content && note.content != '<p></p>') || task.title) &&
|
||||
!note.name &&
|
||||
!task.name
|
||||
"
|
||||
@click="save"
|
||||
class="bg-surface-white !text-ink-gray-9 hover:!bg-surface-gray-3"
|
||||
variant="solid"
|
||||
@ -234,8 +247,8 @@ import CountUpTimer from '@/components/CountUpTimer.vue'
|
||||
import { createToast } from '@/utils'
|
||||
import { globalStore } from '@/stores/global'
|
||||
import { useDraggable, useWindowSize } from '@vueuse/core'
|
||||
import { TextEditor, Avatar, Button, call, createResource } from 'frappe-ui'
|
||||
import { ref, onBeforeUnmount, watch } from 'vue'
|
||||
import { TextEditor, Avatar, Button, createResource } from 'frappe-ui'
|
||||
import { ref, onBeforeUnmount, watch, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { $socket } = globalStore()
|
||||
@ -289,7 +302,12 @@ const getContact = createResource({
|
||||
},
|
||||
})
|
||||
|
||||
const note = ref('')
|
||||
const dirty = ref(false)
|
||||
|
||||
const note = ref({
|
||||
name: '',
|
||||
content: '',
|
||||
})
|
||||
|
||||
const showNote = ref(false)
|
||||
|
||||
@ -303,15 +321,25 @@ function showNoteWindow() {
|
||||
}
|
||||
}
|
||||
|
||||
function createNote() {
|
||||
call('crm.integrations.api.create_and_add_note_to_call_log', {
|
||||
call_sid: callData.value.CallSid,
|
||||
content: note.value,
|
||||
function createUpdateNote() {
|
||||
createResource({
|
||||
url: 'crm.integrations.api.add_note_to_call_log',
|
||||
params: {
|
||||
call_sid: callData.value.CallSid,
|
||||
note: note.value,
|
||||
},
|
||||
auto: true,
|
||||
onSuccess(_note) {
|
||||
note.value['name'] = _note.name
|
||||
nextTick(() => {
|
||||
dirty.value = false
|
||||
})
|
||||
},
|
||||
})
|
||||
note.value = ''
|
||||
}
|
||||
|
||||
const task = ref({
|
||||
name: '',
|
||||
title: '',
|
||||
description: '',
|
||||
assigned_to: '',
|
||||
@ -332,21 +360,25 @@ function showTaskWindow() {
|
||||
}
|
||||
}
|
||||
|
||||
function createTask() {
|
||||
call('crm.integrations.api.create_and_add_task_to_call_log', {
|
||||
call_sid: callData.value.CallSid,
|
||||
task: task.value,
|
||||
function createUpdateTask() {
|
||||
createResource({
|
||||
url: 'crm.integrations.api.add_task_to_call_log',
|
||||
params: {
|
||||
call_sid: callData.value.CallSid,
|
||||
task: task.value,
|
||||
},
|
||||
auto: true,
|
||||
onSuccess(_task) {
|
||||
task.value['name'] = _task.name
|
||||
nextTick(() => {
|
||||
dirty.value = false
|
||||
})
|
||||
},
|
||||
})
|
||||
task.value = {
|
||||
title: '',
|
||||
description: '',
|
||||
assigned_to: '',
|
||||
due_date: '',
|
||||
status: 'Backlog',
|
||||
priority: 'Low',
|
||||
}
|
||||
}
|
||||
|
||||
watch([note, task], () => (dirty.value = true), { deep: true })
|
||||
|
||||
function updateWindowHeight(condition) {
|
||||
let callPopup = callPopupHeader.value.parentElement
|
||||
let top = parseInt(callPopup.style.top)
|
||||
@ -420,8 +452,12 @@ function openDealOrLead() {
|
||||
function closeCallPopup() {
|
||||
showCallPopup.value = false
|
||||
showSmallCallPopup.value = false
|
||||
note.value = ''
|
||||
note.value = {
|
||||
name: '',
|
||||
content: '',
|
||||
}
|
||||
task.value = {
|
||||
name: '',
|
||||
title: '',
|
||||
description: '',
|
||||
assigned_to: '',
|
||||
@ -432,8 +468,13 @@ function closeCallPopup() {
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (note.value) createNote()
|
||||
if (task.value.title) createTask()
|
||||
if (note.value.content) createUpdateNote()
|
||||
if (task.value.title) createUpdateTask()
|
||||
}
|
||||
|
||||
function update() {
|
||||
if (note.value.content) createUpdateNote()
|
||||
if (task.value.title) createUpdateTask()
|
||||
}
|
||||
|
||||
const callDuration = ref('00:00')
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center gap-3">
|
||||
<Avatar
|
||||
v-if="contact?.image"
|
||||
:image="contact.image"
|
||||
:label="contact.full_name"
|
||||
class="relative flex !h-24 !w-24 items-center justify-center [&>div]:text-[30px]"
|
||||
@ -20,9 +21,9 @@
|
||||
/>
|
||||
<div class="flex flex-col items-center justify-center gap-1">
|
||||
<div class="text-xl font-medium">
|
||||
{{ contact.full_name }}
|
||||
{{ contact?.full_name ?? __('Unknown') }}
|
||||
</div>
|
||||
<div class="text-sm text-ink-gray-5">{{ contact.mobile_no }}</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">
|
||||
@ -120,12 +121,13 @@
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar
|
||||
v-if="contact?.image"
|
||||
: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 }}
|
||||
{{ contact?.full_name ?? __('Unknown') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="onCall" class="flex items-center gap-2">
|
||||
@ -195,20 +197,13 @@ 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 { Avatar, call, createResource } 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)
|
||||
@ -218,8 +213,36 @@ let muted = ref(false)
|
||||
let callPopup = ref(null)
|
||||
let counterUp = ref(null)
|
||||
let callStatus = ref('')
|
||||
|
||||
const phoneNumber = ref('')
|
||||
|
||||
const contact = ref({
|
||||
full_name: '',
|
||||
image: '',
|
||||
mobile_no: '',
|
||||
})
|
||||
|
||||
watch(phoneNumber, (value) => {
|
||||
if (!value) return
|
||||
getContact.fetch()
|
||||
})
|
||||
|
||||
const getContact = createResource({
|
||||
url: 'crm.integrations.api.get_contact_by_phone_number',
|
||||
makeParams() {
|
||||
return {
|
||||
phone_number: phoneNumber.value,
|
||||
}
|
||||
},
|
||||
cache: ['contact', phoneNumber.value],
|
||||
onSuccess(data) {
|
||||
contact.value = data
|
||||
},
|
||||
})
|
||||
|
||||
const showNoteModal = ref(false)
|
||||
const note = ref({
|
||||
name: '',
|
||||
title: '',
|
||||
content: '',
|
||||
})
|
||||
@ -227,9 +250,9 @@ const note = ref({
|
||||
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', {
|
||||
await call('crm.integrations.api.add_note_to_call_log', {
|
||||
call_sid: _call.parameters.CallSid,
|
||||
note: _note.name,
|
||||
note: _note,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -298,19 +321,7 @@ function toggleMute() {
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
phoneNumber.value = call.parameters.From
|
||||
|
||||
showCallPopup.value = true
|
||||
_call = call
|
||||
@ -352,6 +363,7 @@ function hangUpCall() {
|
||||
callStatus.value = ''
|
||||
muted.value = false
|
||||
note.value = {
|
||||
name: '',
|
||||
title: '',
|
||||
content: '',
|
||||
}
|
||||
@ -373,19 +385,7 @@ function handleDisconnectedIncomingCall() {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
phoneNumber.value = number
|
||||
|
||||
if (device) {
|
||||
log.value = `Attempting to call ${number} ...`
|
||||
@ -431,6 +431,7 @@ async function makeOutgoingCall(number) {
|
||||
muted.value = false
|
||||
counterUp.value.stop()
|
||||
note.value = {
|
||||
name: '',
|
||||
title: '',
|
||||
content: '',
|
||||
}
|
||||
@ -445,6 +446,7 @@ async function makeOutgoingCall(number) {
|
||||
callStatus.value = ''
|
||||
muted.value = false
|
||||
note.value = {
|
||||
name: '',
|
||||
title: '',
|
||||
content: '',
|
||||
}
|
||||
@ -471,6 +473,7 @@ function cancelCall() {
|
||||
callStatus.value = ''
|
||||
muted.value = false
|
||||
note.value = {
|
||||
name: '',
|
||||
title: '',
|
||||
content: '',
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user