1
0
forked from test/crm

fix: fix twilio contact viewing, note creation & linking code

This commit is contained in:
Shariq Ansari 2025-01-18 20:34:04 +05:30
parent e6d72146b5
commit cfae3101c5
5 changed files with 190 additions and 158 deletions

View File

@ -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()

View File

@ -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

View File

@ -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 == "+"])

View File

@ -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')

View File

@ -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: '',
}