commit
4718843c4c
@ -60,6 +60,27 @@ def get_contacts():
|
|||||||
|
|
||||||
return contacts
|
return contacts
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_lead_contacts():
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
frappe.throw("Authentication failed", exc=frappe.AuthenticationError)
|
||||||
|
|
||||||
|
lead_contacts = frappe.get_all(
|
||||||
|
"CRM Lead",
|
||||||
|
fields=[
|
||||||
|
"name",
|
||||||
|
"lead_name",
|
||||||
|
"mobile_no",
|
||||||
|
"phone",
|
||||||
|
"image",
|
||||||
|
"modified"
|
||||||
|
],
|
||||||
|
order_by="lead_name asc",
|
||||||
|
distinct=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return lead_contacts
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_organizations():
|
def get_organizations():
|
||||||
if frappe.session.user == "Guest":
|
if frappe.session.user == "Guest":
|
||||||
|
|||||||
@ -51,8 +51,7 @@
|
|||||||
"fieldname": "organization",
|
"fieldname": "organization",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Organization",
|
"label": "Organization",
|
||||||
"options": "CRM Organization",
|
"options": "CRM Organization"
|
||||||
"reqd": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "probability",
|
"fieldname": "probability",
|
||||||
@ -270,7 +269,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-06 18:45:26.263974",
|
"modified": "2024-01-13 11:50:37.361190",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Deal",
|
"name": "CRM Deal",
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from .twilio_handler import Twilio, IncomingCall, TwilioCallDetails
|
from .twilio_handler import Twilio, IncomingCall, TwilioCallDetails
|
||||||
|
from .utils import parse_mobile_no
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def is_enabled():
|
def is_enabled():
|
||||||
@ -69,7 +70,7 @@ def create_call_log(call_details: TwilioCallDetails):
|
|||||||
'doctype': 'CRM Call Log',
|
'doctype': 'CRM Call Log',
|
||||||
'medium': 'Twilio'
|
'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.flags.ignore_permissions = True
|
||||||
call_log.save()
|
call_log.save()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
@ -82,11 +83,10 @@ def update_call_log(call_sid, status=None):
|
|||||||
|
|
||||||
call_details = twilio.get_call_info(call_sid)
|
call_details = twilio.get_call_info(call_sid)
|
||||||
call_log = frappe.get_doc("CRM Call Log", call_sid)
|
call_log = frappe.get_doc("CRM Call Log", call_sid)
|
||||||
call_log.status = status or TwilioCallDetails.get_call_status(call_details.status)
|
call_log.status = TwilioCallDetails.get_call_status(status or call_details.status)
|
||||||
call_log.duration = call_details.duration
|
call_log.duration = call_details.duration
|
||||||
call_log.start_time = get_datetime_from_timestamp(call_details.start_time)
|
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.end_time = get_datetime_from_timestamp(call_details.end_time)
|
||||||
call_log.reference_docname, call_log.reference_doctype = get_lead_or_deal_from_number(call_log)
|
|
||||||
if call_log.note and call_log.reference_docname:
|
if call_log.note and call_log.reference_docname:
|
||||||
frappe.db.set_value("CRM Note", call_log.note, "reference_doctype", call_log.reference_doctype)
|
frappe.db.set_value("CRM Note", call_log.note, "reference_doctype", call_log.reference_doctype)
|
||||||
frappe.db.set_value("CRM Note", call_log.note, "reference_docname", call_log.reference_docname)
|
frappe.db.set_value("CRM Note", call_log.note, "reference_docname", call_log.reference_docname)
|
||||||
@ -105,6 +105,15 @@ def update_recording_info(**kwargs):
|
|||||||
except:
|
except:
|
||||||
frappe.log_error(title=_("Failed to capture Twilio recording"))
|
frappe.log_error(title=_("Failed to capture Twilio recording"))
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def update_call_status_info(**kwargs):
|
||||||
|
try:
|
||||||
|
args = frappe._dict(kwargs)
|
||||||
|
parent_call_sid = args.ParentCallSid
|
||||||
|
update_call_log(parent_call_sid, status=args.CallStatus)
|
||||||
|
except:
|
||||||
|
frappe.log_error(title=_("Failed to update Twilio call status"))
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_call_info(**kwargs):
|
def get_call_info(**kwargs):
|
||||||
"""This is a webhook called when the outgoing call status changes.
|
"""This is a webhook called when the outgoing call status changes.
|
||||||
@ -149,17 +158,25 @@ def add_note_to_call_log(call_sid, note):
|
|||||||
def get_lead_or_deal_from_number(call):
|
def get_lead_or_deal_from_number(call):
|
||||||
"""Get lead/deal from the given number.
|
"""Get lead/deal from the given number.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def find_record(doctype, mobile_no):
|
||||||
|
mobile_no = parse_mobile_no(mobile_no)
|
||||||
|
data = frappe.db.sql(
|
||||||
|
"""
|
||||||
|
SELECT name, mobile_no
|
||||||
|
FROM `tab{doctype}`
|
||||||
|
WHERE CONCAT('+', REGEXP_REPLACE(mobile_no, '[^0-9]', '')) = {mobile_no}
|
||||||
|
""".format(doctype=doctype, mobile_no=mobile_no),
|
||||||
|
as_dict=True
|
||||||
|
)
|
||||||
|
return data[0].name if data else None
|
||||||
|
|
||||||
doctype = "CRM Lead"
|
doctype = "CRM Lead"
|
||||||
doc = None
|
number = call.get('to') if call.type == 'Outgoing' else call.get('from')
|
||||||
if call.type == 'Outgoing':
|
|
||||||
doc = frappe.get_cached_value(doctype, { "mobile_no": call.get('to') })
|
doc = find_record(doctype, number) or None
|
||||||
if not doc:
|
if not doc:
|
||||||
doctype = "CRM Deal"
|
doctype = "CRM Deal"
|
||||||
doc = frappe.get_cached_value(doctype, { "mobile_no": call.get('to') })
|
doc = find_record(doctype, number)
|
||||||
else:
|
|
||||||
doc = frappe.get_cached_value(doctype, { "mobile_no": call.get('from') })
|
|
||||||
if not doc:
|
|
||||||
doctype = "CRM Deal"
|
|
||||||
doc = frappe.get_cached_value(doctype, { "mobile_no": call.get('from') })
|
|
||||||
|
|
||||||
return doc, doctype
|
return doc, doctype
|
||||||
@ -72,8 +72,8 @@ class Twilio:
|
|||||||
url_path = "/api/method/crm.twilio.api.update_recording_info"
|
url_path = "/api/method/crm.twilio.api.update_recording_info"
|
||||||
return get_public_url(url_path)
|
return get_public_url(url_path)
|
||||||
|
|
||||||
def get_call_status_callback_url(self):
|
def get_update_call_status_callback_url(self):
|
||||||
url_path = "/api/method/crm.twilio.api.get_call_info"
|
url_path = "/api/method/crm.twilio.api.update_call_status_info"
|
||||||
return get_public_url(url_path)
|
return get_public_url(url_path)
|
||||||
|
|
||||||
def generate_twilio_dial_response(self, from_number: str, to_number: str):
|
def generate_twilio_dial_response(self, from_number: str, to_number: str):
|
||||||
@ -89,15 +89,12 @@ class Twilio:
|
|||||||
dial.number(
|
dial.number(
|
||||||
to_number,
|
to_number,
|
||||||
status_callback_event='initiated ringing answered completed',
|
status_callback_event='initiated ringing answered completed',
|
||||||
status_callback=self.get_call_status_callback_url(),
|
status_callback=self.get_update_call_status_callback_url(),
|
||||||
status_callback_method='POST'
|
status_callback_method='POST'
|
||||||
)
|
)
|
||||||
resp.append(dial)
|
resp.append(dial)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def get_call_info(self, call_sid):
|
|
||||||
return self.twilio_client.calls(call_sid).fetch()
|
|
||||||
|
|
||||||
def generate_twilio_client_response(self, client, ring_tone='at'):
|
def generate_twilio_client_response(self, client, ring_tone='at'):
|
||||||
"""Generates voice call instructions to forward the call to agents computer.
|
"""Generates voice call instructions to forward the call to agents computer.
|
||||||
"""
|
"""
|
||||||
@ -108,7 +105,12 @@ class Twilio:
|
|||||||
recording_status_callback=self.get_recording_status_callback_url(),
|
recording_status_callback=self.get_recording_status_callback_url(),
|
||||||
recording_status_callback_event='completed'
|
recording_status_callback_event='completed'
|
||||||
)
|
)
|
||||||
dial.client(client)
|
dial.client(
|
||||||
|
client,
|
||||||
|
status_callback_event='initiated ringing answered completed',
|
||||||
|
status_callback=self.get_update_call_status_callback_url(),
|
||||||
|
status_callback_method='POST'
|
||||||
|
)
|
||||||
resp.append(dial)
|
resp.append(dial)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@ -156,6 +158,9 @@ def get_twilio_number_owners(phone_number):
|
|||||||
'owner2': {....}
|
'owner2': {....}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
# remove special characters from phone number and get only digits also remove white spaces
|
||||||
|
# keep + sign in the number at start of the number
|
||||||
|
phone_number = ''.join([c for c in phone_number if c.isdigit() or c == '+'])
|
||||||
user_voice_settings = frappe.get_all(
|
user_voice_settings = frappe.get_all(
|
||||||
'Twilio Agents',
|
'Twilio Agents',
|
||||||
filters={'twilio_number': phone_number},
|
filters={'twilio_number': phone_number},
|
||||||
@ -200,8 +205,8 @@ class TwilioCallDetails:
|
|||||||
self.application_sid = call_info.get('ApplicationSid')
|
self.application_sid = call_info.get('ApplicationSid')
|
||||||
self.call_sid = call_info.get('CallSid')
|
self.call_sid = call_info.get('CallSid')
|
||||||
self.call_status = self.get_call_status(call_info.get('CallStatus'))
|
self.call_status = self.get_call_status(call_info.get('CallStatus'))
|
||||||
self._call_from = call_from
|
self._call_from = call_from or call_info.get('From')
|
||||||
self._call_to = call_to
|
self._call_to = call_to or call_info.get('To')
|
||||||
|
|
||||||
def get_direction(self):
|
def get_direction(self):
|
||||||
if self.call_info.get('Caller').lower().startswith('client'):
|
if self.call_info.get('Caller').lower().startswith('client'):
|
||||||
|
|||||||
@ -13,4 +13,11 @@ def merge_dicts(d1: dict, d2: dict):
|
|||||||
)
|
)
|
||||||
... {'name1': {'age': 20, 'phone': '+xxx'}, 'name2': {'age': 30, 'phone': '+yyy'}}
|
... {'name1': {'age': 20, 'phone': '+xxx'}, 'name2': {'age': 30, 'phone': '+yyy'}}
|
||||||
"""
|
"""
|
||||||
return {k:{**v, **d2.get(k, {})} for k, v in d1.items()}
|
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 == '+'])
|
||||||
@ -1 +1 @@
|
|||||||
Subproject commit f5f5665e944bef20a79ece6ad7223db09984c6a6
|
Subproject commit d2de676b1e2eb4c5af36d22c006c1f4843c3e19b
|
||||||
@ -13,7 +13,7 @@
|
|||||||
"@vueuse/core": "^10.3.0",
|
"@vueuse/core": "^10.3.0",
|
||||||
"@vueuse/integrations": "^10.3.0",
|
"@vueuse/integrations": "^10.3.0",
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "^4.28.0",
|
||||||
"frappe-ui": "^0.1.22",
|
"frappe-ui": "^0.1.23",
|
||||||
"mime": "^4.0.1",
|
"mime": "^4.0.1",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "^4.7.2",
|
||||||
|
|||||||
@ -709,6 +709,7 @@ import {
|
|||||||
startCase,
|
startCase,
|
||||||
taskStatusOptions,
|
taskStatusOptions,
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
|
import { globalStore } from '@/stores/global'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
import {
|
import {
|
||||||
@ -724,8 +725,9 @@ import {
|
|||||||
import { useElementVisibility } from '@vueuse/core'
|
import { useElementVisibility } from '@vueuse/core'
|
||||||
import { ref, computed, h, defineModel, markRaw, watch, nextTick } from 'vue'
|
import { ref, computed, h, defineModel, markRaw, watch, nextTick } from 'vue'
|
||||||
|
|
||||||
|
const { makeCall } = globalStore()
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
const { getContact } = contactsStore()
|
const { getContact, getLeadContact } = contactsStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
@ -781,8 +783,11 @@ const calls = createListResource({
|
|||||||
doc.duration = secondsToDuration(doc.duration)
|
doc.duration = secondsToDuration(doc.duration)
|
||||||
if (doc.type === 'Incoming') {
|
if (doc.type === 'Incoming') {
|
||||||
doc.caller = {
|
doc.caller = {
|
||||||
label: getContact(doc.from)?.full_name || 'Unknown',
|
label:
|
||||||
image: getContact(doc.from)?.image,
|
getContact(doc.from)?.full_name ||
|
||||||
|
getLeadContact(doc.from)?.full_name ||
|
||||||
|
'Unknown',
|
||||||
|
image: getContact(doc.from)?.image || getLeadContact(doc.from)?.image,
|
||||||
}
|
}
|
||||||
doc.receiver = {
|
doc.receiver = {
|
||||||
label: getUser(doc.receiver).full_name,
|
label: getUser(doc.receiver).full_name,
|
||||||
@ -794,8 +799,11 @@ const calls = createListResource({
|
|||||||
image: getUser(doc.caller).user_image,
|
image: getUser(doc.caller).user_image,
|
||||||
}
|
}
|
||||||
doc.receiver = {
|
doc.receiver = {
|
||||||
label: getContact(doc.to)?.full_name || 'Unknown',
|
label:
|
||||||
image: getContact(doc.to)?.image,
|
getContact(doc.to)?.full_name ||
|
||||||
|
getLeadContact(doc.to)?.full_name ||
|
||||||
|
'Unknown',
|
||||||
|
image: getContact(doc.to)?.image || getLeadContact(doc.to)?.image,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -183,11 +183,13 @@ import CountUpTimer from '@/components/CountUpTimer.vue'
|
|||||||
import NoteModal from '@/components/Modals/NoteModal.vue'
|
import NoteModal from '@/components/Modals/NoteModal.vue'
|
||||||
import { Device } from '@twilio/voice-sdk'
|
import { Device } from '@twilio/voice-sdk'
|
||||||
import { useDraggable, useWindowSize } from '@vueuse/core'
|
import { useDraggable, useWindowSize } from '@vueuse/core'
|
||||||
|
import { globalStore } from '@/stores/global'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
import { Avatar, call } from 'frappe-ui'
|
import { Avatar, call } from 'frappe-ui'
|
||||||
import { onMounted, ref, watch, getCurrentInstance } from 'vue'
|
import { onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
const { getContact } = contactsStore()
|
const { getContact, getLeadContact } = contactsStore()
|
||||||
|
const { setMakeCall, setTwilioEnabled } = globalStore()
|
||||||
|
|
||||||
let device = ''
|
let device = ''
|
||||||
let log = ref('Connecting...')
|
let log = ref('Connecting...')
|
||||||
@ -197,7 +199,6 @@ const contact = ref({
|
|||||||
mobile_no: '',
|
mobile_no: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
let enabled = ref(false)
|
|
||||||
let showCallPopup = ref(false)
|
let showCallPopup = ref(false)
|
||||||
let showSmallCallWindow = ref(false)
|
let showSmallCallWindow = ref(false)
|
||||||
let onCall = ref(false)
|
let onCall = ref(false)
|
||||||
@ -308,8 +309,10 @@ function handleIncomingCall(call) {
|
|||||||
log.value = `Incoming call from ${call.parameters.From}`
|
log.value = `Incoming call from ${call.parameters.From}`
|
||||||
|
|
||||||
// get name of the caller from the phone number
|
// get name of the caller from the phone number
|
||||||
|
|
||||||
contact.value = getContact(call.parameters.From)
|
contact.value = getContact(call.parameters.From)
|
||||||
|
if (!contact.value) {
|
||||||
|
contact.value = getLeadContact(call.parameters.From)
|
||||||
|
}
|
||||||
|
|
||||||
if (!contact.value) {
|
if (!contact.value) {
|
||||||
contact.value = {
|
contact.value = {
|
||||||
@ -380,6 +383,9 @@ function handleDisconnectedIncomingCall() {
|
|||||||
|
|
||||||
async function makeOutgoingCall(number) {
|
async function makeOutgoingCall(number) {
|
||||||
contact.value = getContact(number)
|
contact.value = getContact(number)
|
||||||
|
if (!contact.value) {
|
||||||
|
contact.value = getLeadContact(number)
|
||||||
|
}
|
||||||
|
|
||||||
if (device) {
|
if (device) {
|
||||||
log.value = `Attempting to call ${contact.value.mobile_no} ...`
|
log.value = `Attempting to call ${contact.value.mobile_no} ...`
|
||||||
@ -475,8 +481,11 @@ function toggleCallWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
enabled.value = await is_twilio_enabled()
|
let enabled = await is_twilio_enabled()
|
||||||
enabled.value && startupClient()
|
setTwilioEnabled(enabled)
|
||||||
|
enabled && startupClient()
|
||||||
|
|
||||||
|
setMakeCall(makeOutgoingCall)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -486,10 +495,6 @@ watch(
|
|||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
const app = getCurrentInstance()
|
|
||||||
app.appContext.config.globalProperties.makeCall = makeOutgoingCall
|
|
||||||
app.appContext.config.globalProperties.is_twilio_enabled = enabled.value
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<NestedPopover>
|
<NestedPopover>
|
||||||
<template #target>
|
<template #target>
|
||||||
<Button label="Column Settings">
|
<Button>
|
||||||
<template #prefix>
|
<SettingsIcon class="h-4" />
|
||||||
<SettingsIcon class="h-4" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<template #body="{ close }">
|
<template #body="{ close }">
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ListView
|
<ListView
|
||||||
|
:class="$attrs.class"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
:options="{
|
:options="{
|
||||||
@ -64,6 +65,7 @@
|
|||||||
<ListSelectBanner />
|
<ListSelectBanner />
|
||||||
</ListView>
|
</ListView>
|
||||||
<ListFooter
|
<ListFooter
|
||||||
|
v-if="pageLengthCount"
|
||||||
class="border-t px-5 py-2"
|
class="border-t px-5 py-2"
|
||||||
v-model="pageLengthCount"
|
v-model="pageLengthCount"
|
||||||
:options="{
|
:options="{
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ListView
|
<ListView
|
||||||
|
:class="$attrs.class"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
:options="{
|
:options="{
|
||||||
@ -87,6 +88,7 @@
|
|||||||
<ListSelectBanner />
|
<ListSelectBanner />
|
||||||
</ListView>
|
</ListView>
|
||||||
<ListFooter
|
<ListFooter
|
||||||
|
v-if="pageLengthCount"
|
||||||
class="border-t px-5 py-2"
|
class="border-t px-5 py-2"
|
||||||
v-model="pageLengthCount"
|
v-model="pageLengthCount"
|
||||||
:options="{
|
:options="{
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ListView
|
<ListView
|
||||||
|
:class="$attrs.class"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
:options="{
|
:options="{
|
||||||
@ -96,6 +97,7 @@
|
|||||||
<ListSelectBanner />
|
<ListSelectBanner />
|
||||||
</ListView>
|
</ListView>
|
||||||
<ListFooter
|
<ListFooter
|
||||||
|
v-if="pageLengthCount"
|
||||||
class="border-t px-5 py-2"
|
class="border-t px-5 py-2"
|
||||||
v-model="pageLengthCount"
|
v-model="pageLengthCount"
|
||||||
:options="{
|
:options="{
|
||||||
|
|||||||
@ -35,9 +35,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<Badge
|
<Badge
|
||||||
:variant="'subtle'"
|
:variant="'subtle'"
|
||||||
:theme="callLog.data.status === 'Completed' ? 'green' : 'gray'"
|
:theme="statusColorMap[callLog.data.status]"
|
||||||
size="md"
|
size="md"
|
||||||
:label="callLog.data.status"
|
:label="statusLabelMap[callLog.data.status]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -164,7 +164,7 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
const { contacts, getContact } = contactsStore()
|
const { contacts, getContact, getLeadContact } = contactsStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
callLogId: {
|
callLogId: {
|
||||||
@ -186,8 +186,11 @@ const callLog = createResource({
|
|||||||
doc.duration = secondsToDuration(doc.duration)
|
doc.duration = secondsToDuration(doc.duration)
|
||||||
if (doc.type === 'Incoming') {
|
if (doc.type === 'Incoming') {
|
||||||
doc.caller = {
|
doc.caller = {
|
||||||
label: getContact(doc.from)?.full_name || 'Unknown',
|
label:
|
||||||
image: getContact(doc.from)?.image,
|
getContact(doc.from)?.full_name ||
|
||||||
|
getLeadContact(doc.from)?.full_name ||
|
||||||
|
'Unknown',
|
||||||
|
image: getContact(doc.from)?.image || getLeadContact(doc.from)?.image,
|
||||||
}
|
}
|
||||||
doc.receiver = {
|
doc.receiver = {
|
||||||
label: getUser(doc.receiver).full_name,
|
label: getUser(doc.receiver).full_name,
|
||||||
@ -199,8 +202,11 @@ const callLog = createResource({
|
|||||||
image: getUser(doc.caller).user_image,
|
image: getUser(doc.caller).user_image,
|
||||||
}
|
}
|
||||||
doc.receiver = {
|
doc.receiver = {
|
||||||
label: getContact(doc.to)?.full_name || 'Unknown',
|
label:
|
||||||
image: getContact(doc.to)?.image,
|
getContact(doc.to)?.full_name ||
|
||||||
|
getLeadContact(doc.to)?.full_name ||
|
||||||
|
'Unknown',
|
||||||
|
image: getContact(doc.to)?.image || getLeadContact(doc.to)?.image,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return doc
|
return doc
|
||||||
@ -239,6 +245,22 @@ const breadcrumbs = computed(() => [
|
|||||||
route: { name: 'Call Log', params: { callLogId: props.callLogId } },
|
route: { name: 'Call Log', params: { callLogId: props.callLogId } },
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const statusLabelMap = {
|
||||||
|
Completed: 'Completed',
|
||||||
|
Busy: 'Declined',
|
||||||
|
Ringing: 'Ringing',
|
||||||
|
'No Answer': 'Missed Call',
|
||||||
|
'In Progress': 'In Progress',
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusColorMap = {
|
||||||
|
Completed: 'green',
|
||||||
|
Busy: 'orange',
|
||||||
|
Ringing: 'gray',
|
||||||
|
'No Answer': 'red',
|
||||||
|
'In Progress': 'blue',
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -4,7 +4,11 @@
|
|||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</LayoutHeader>
|
||||||
<ViewControls v-model="callLogs" v-model:loadMore="loadMore" doctype="CRM Call Log" />
|
<ViewControls
|
||||||
|
v-model="callLogs"
|
||||||
|
v-model:loadMore="loadMore"
|
||||||
|
doctype="CRM Call Log"
|
||||||
|
/>
|
||||||
<CallLogsListView
|
<CallLogsListView
|
||||||
v-if="callLogs.data && rows.length"
|
v-if="callLogs.data && rows.length"
|
||||||
v-model="callLogs.data.page_length_count"
|
v-model="callLogs.data.page_length_count"
|
||||||
@ -46,7 +50,7 @@ import { Breadcrumbs } from 'frappe-ui'
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
const { getContact } = contactsStore()
|
const { getContact, getLeadContact } = contactsStore()
|
||||||
|
|
||||||
const breadcrumbs = [{ label: 'Call Logs', route: { name: 'Call Logs' } }]
|
const breadcrumbs = [{ label: 'Call Logs', route: { name: 'Call Logs' } }]
|
||||||
|
|
||||||
@ -66,20 +70,26 @@ const rows = computed(() => {
|
|||||||
if (row === 'caller') {
|
if (row === 'caller') {
|
||||||
_rows[row] = {
|
_rows[row] = {
|
||||||
label: incoming
|
label: incoming
|
||||||
? getContact(callLog.from)?.full_name || 'Unknown'
|
? getContact(callLog.from)?.full_name ||
|
||||||
|
getLeadContact(callLog.from)?.full_name ||
|
||||||
|
'Unknown'
|
||||||
: getUser(callLog.caller).full_name,
|
: getUser(callLog.caller).full_name,
|
||||||
image: incoming
|
image: incoming
|
||||||
? getContact(callLog.from)?.image
|
? getContact(callLog.from)?.image ||
|
||||||
|
getLeadContact(callLog.from)?.image
|
||||||
: getUser(callLog.caller).user_image,
|
: getUser(callLog.caller).user_image,
|
||||||
}
|
}
|
||||||
} else if (row === 'receiver') {
|
} else if (row === 'receiver') {
|
||||||
_rows[row] = {
|
_rows[row] = {
|
||||||
label: incoming
|
label: incoming
|
||||||
? getUser(callLog.receiver).full_name
|
? getUser(callLog.receiver).full_name
|
||||||
: getContact(callLog.to)?.full_name || 'Unknown',
|
: getContact(callLog.to)?.full_name ||
|
||||||
|
getLeadContact(callLog.to)?.full_name ||
|
||||||
|
'Unknown',
|
||||||
image: incoming
|
image: incoming
|
||||||
? getUser(callLog.receiver).user_image
|
? getUser(callLog.receiver).user_image
|
||||||
: getContact(callLog.to)?.image,
|
: getContact(callLog.to)?.image ||
|
||||||
|
getLeadContact(callLog.to)?.image,
|
||||||
}
|
}
|
||||||
} else if (row === 'duration') {
|
} else if (row === 'duration') {
|
||||||
_rows[row] = {
|
_rows[row] = {
|
||||||
@ -93,8 +103,8 @@ const rows = computed(() => {
|
|||||||
}
|
}
|
||||||
} else if (row === 'status') {
|
} else if (row === 'status') {
|
||||||
_rows[row] = {
|
_rows[row] = {
|
||||||
label: callLog.status,
|
label: statusLabelMap[callLog.status],
|
||||||
color: callLog.status === 'Completed' ? 'green' : 'gray',
|
color: statusColorMap[callLog.status],
|
||||||
}
|
}
|
||||||
} else if (['modified', 'creation'].includes(row)) {
|
} else if (['modified', 'creation'].includes(row)) {
|
||||||
_rows[row] = {
|
_rows[row] = {
|
||||||
@ -106,4 +116,20 @@ const rows = computed(() => {
|
|||||||
return _rows
|
return _rows
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const statusLabelMap = {
|
||||||
|
Completed: 'Completed',
|
||||||
|
Busy: 'Declined',
|
||||||
|
Ringing: 'Ringing',
|
||||||
|
'No Answer': 'Missed Call',
|
||||||
|
'In Progress': 'In Progress',
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusColorMap = {
|
||||||
|
Completed: 'green',
|
||||||
|
Busy: 'orange',
|
||||||
|
Ringing: 'gray',
|
||||||
|
'No Answer': 'red',
|
||||||
|
'In Progress': 'blue',
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -70,14 +70,14 @@
|
|||||||
>
|
>
|
||||||
·
|
·
|
||||||
</span>
|
</span>
|
||||||
<Tooltip
|
<Tooltip text="Make Call" v-if="contact.mobile_no">
|
||||||
text="Make Call"
|
<div
|
||||||
v-if="contact.mobile_no"
|
class="flex cursor-pointer items-center gap-1.5"
|
||||||
class="flex cursor-pointer items-center gap-1.5"
|
@click="makeCall(contact.mobile_no)"
|
||||||
@click="makeCall(contact.mobile_no)"
|
>
|
||||||
>
|
<PhoneIcon class="h-4 w-4" />
|
||||||
<PhoneIcon class="h-4 w-4" />
|
<span class="">{{ contact.mobile_no }}</span>
|
||||||
<span class="">{{ contact.mobile_no }}</span>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span
|
<span
|
||||||
v-if="contact.mobile_no"
|
v-if="contact.mobile_no"
|
||||||
@ -153,7 +153,7 @@
|
|||||||
<Tabs class="overflow-hidden" v-model="tabIndex" :tabs="tabs">
|
<Tabs class="overflow-hidden" v-model="tabIndex" :tabs="tabs">
|
||||||
<template #tab="{ tab, selected }">
|
<template #tab="{ tab, selected }">
|
||||||
<button
|
<button
|
||||||
class="group -mb-px flex items-center gap-2 border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
class="group flex items-center gap-2 border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
||||||
:class="{ 'text-gray-900': selected }"
|
:class="{ 'text-gray-900': selected }"
|
||||||
>
|
>
|
||||||
<component v-if="tab.icon" :is="tab.icon" class="h-5" />
|
<component v-if="tab.icon" :is="tab.icon" class="h-5" />
|
||||||
@ -171,8 +171,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #default="{ tab }">
|
<template #default="{ tab }">
|
||||||
<DealsListView
|
<DealsListView
|
||||||
class="mt-4"
|
|
||||||
v-if="tab.label === 'Deals' && rows.length"
|
v-if="tab.label === 'Deals' && rows.length"
|
||||||
|
class="mt-4"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:options="{ selectable: false }"
|
:options="{ selectable: false }"
|
||||||
@ -229,7 +229,7 @@ import { statusesStore } from '@/stores/statuses'
|
|||||||
import { ref, computed, h } from 'vue'
|
import { ref, computed, h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const { $dialog } = globalStore()
|
const { $dialog, makeCall } = globalStore()
|
||||||
|
|
||||||
const { getContactByName, contacts } = contactsStore()
|
const { getContactByName, contacts } = contactsStore()
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
|
|||||||
@ -310,7 +310,7 @@ import {
|
|||||||
import { ref, computed, h } from 'vue'
|
import { ref, computed, h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const { $dialog } = globalStore()
|
const { $dialog, makeCall } = globalStore()
|
||||||
const { getContactByName, contacts } = contactsStore()
|
const { getContactByName, contacts } = contactsStore()
|
||||||
const { organizations, getOrganization } = organizationsStore()
|
const { organizations, getOrganization } = organizationsStore()
|
||||||
const { statusOptions, getDealStatus } = statusesStore()
|
const { statusOptions, getDealStatus } = statusesStore()
|
||||||
|
|||||||
@ -275,7 +275,7 @@ import {
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const { $dialog } = globalStore()
|
const { $dialog, makeCall } = globalStore()
|
||||||
const { getContactByName, contacts } = contactsStore()
|
const { getContactByName, contacts } = contactsStore()
|
||||||
const { organizations } = organizationsStore()
|
const { organizations } = organizationsStore()
|
||||||
const { statusOptions, getLeadStatus } = statusesStore()
|
const { statusOptions, getLeadStatus } = statusesStore()
|
||||||
|
|||||||
@ -172,7 +172,7 @@
|
|||||||
<Tabs v-model="tabIndex" :tabs="tabs">
|
<Tabs v-model="tabIndex" :tabs="tabs">
|
||||||
<template #tab="{ tab, selected }">
|
<template #tab="{ tab, selected }">
|
||||||
<button
|
<button
|
||||||
class="group -mb-px flex items-center gap-2 border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
class="group flex items-center gap-2 border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
||||||
:class="{ 'text-gray-900': selected }"
|
:class="{ 'text-gray-900': selected }"
|
||||||
>
|
>
|
||||||
<component v-if="tab.icon" :is="tab.icon" class="h-5" />
|
<component v-if="tab.icon" :is="tab.icon" class="h-5" />
|
||||||
@ -189,36 +189,34 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ tab }">
|
<template #default="{ tab }">
|
||||||
<div class="flex h-full">
|
<LeadsListView
|
||||||
<LeadsListView
|
class="mt-4"
|
||||||
class="mt-4"
|
v-if="tab.label === 'Leads' && rows.length"
|
||||||
v-if="tab.label === 'Leads' && rows.length"
|
:rows="rows"
|
||||||
:rows="rows"
|
:columns="columns"
|
||||||
:columns="columns"
|
:options="{ selectable: false }"
|
||||||
:options="{ selectable: false }"
|
/>
|
||||||
/>
|
<DealsListView
|
||||||
<DealsListView
|
class="mt-4"
|
||||||
class="mt-4"
|
v-if="tab.label === 'Deals' && rows.length"
|
||||||
v-if="tab.label === 'Deals' && rows.length"
|
:rows="rows"
|
||||||
:rows="rows"
|
:columns="columns"
|
||||||
:columns="columns"
|
:options="{ selectable: false }"
|
||||||
:options="{ selectable: false }"
|
/>
|
||||||
/>
|
<ContactsListView
|
||||||
<ContactsListView
|
class="mt-4"
|
||||||
class="mt-4"
|
v-if="tab.label === 'Contacts' && rows.length"
|
||||||
v-if="tab.label === 'Contacts' && rows.length"
|
:rows="rows"
|
||||||
:rows="rows"
|
:columns="columns"
|
||||||
:columns="columns"
|
:options="{ selectable: false }"
|
||||||
:options="{ selectable: false }"
|
/>
|
||||||
/>
|
<div
|
||||||
<div
|
v-if="!rows.length"
|
||||||
v-if="!rows.length"
|
class="grid flex-1 place-items-center text-xl font-medium text-gray-500"
|
||||||
class="grid flex-1 place-items-center text-xl font-medium text-gray-500"
|
>
|
||||||
>
|
<div class="flex flex-col items-center justify-center space-y-3">
|
||||||
<div class="flex flex-col items-center justify-center space-y-3">
|
<component :is="tab.icon" class="!h-10 !w-10" />
|
||||||
<component :is="tab.icon" class="!h-10 !w-10" />
|
<div>No {{ tab.label }} Found</div>
|
||||||
<div>No {{ tab.label }} Found</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { reactive } from 'vue'
|
|||||||
export const contactsStore = defineStore('crm-contacts', () => {
|
export const contactsStore = defineStore('crm-contacts', () => {
|
||||||
let contactsByPhone = reactive({})
|
let contactsByPhone = reactive({})
|
||||||
let contactsByName = reactive({})
|
let contactsByName = reactive({})
|
||||||
|
let leadContactsByPhone = reactive({})
|
||||||
|
|
||||||
const contacts = createResource({
|
const contacts = createResource({
|
||||||
url: 'crm.api.session.get_contacts',
|
url: 'crm.api.session.get_contacts',
|
||||||
@ -13,6 +14,9 @@ export const contactsStore = defineStore('crm-contacts', () => {
|
|||||||
auto: true,
|
auto: true,
|
||||||
transform(contacts) {
|
transform(contacts) {
|
||||||
for (let contact of contacts) {
|
for (let contact of contacts) {
|
||||||
|
// remove special characters from phone number to make it easier to search
|
||||||
|
// also remove spaces but keep + sign at the start
|
||||||
|
contact.mobile_no = contact.mobile_no.replace(/[^0-9+]/g, '')
|
||||||
contactsByPhone[contact.mobile_no] = contact
|
contactsByPhone[contact.mobile_no] = contact
|
||||||
contactsByName[contact.name] = contact
|
contactsByName[contact.name] = contact
|
||||||
}
|
}
|
||||||
@ -25,16 +29,44 @@ export const contactsStore = defineStore('crm-contacts', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const leadContacts = createResource({
|
||||||
|
url: 'crm.api.session.get_lead_contacts',
|
||||||
|
cache: 'lead_contacts',
|
||||||
|
initialData: [],
|
||||||
|
auto: true,
|
||||||
|
transform(lead_contacts) {
|
||||||
|
for (let lead_contact of lead_contacts) {
|
||||||
|
// remove special characters from phone number to make it easier to search
|
||||||
|
// also remove spaces but keep + sign at the start
|
||||||
|
lead_contact.mobile_no = lead_contact.mobile_no.replace(/[^0-9+]/g, '')
|
||||||
|
lead_contact.full_name = lead_contact.lead_name
|
||||||
|
leadContactsByPhone[lead_contact.mobile_no] = lead_contact
|
||||||
|
}
|
||||||
|
return lead_contacts
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
if (error && error.exc_type === 'AuthenticationError') {
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
function getContact(mobile_no) {
|
function getContact(mobile_no) {
|
||||||
|
mobile_no = mobile_no.replace(/[^0-9+]/g, '')
|
||||||
return contactsByPhone[mobile_no]
|
return contactsByPhone[mobile_no]
|
||||||
}
|
}
|
||||||
function getContactByName(name) {
|
function getContactByName(name) {
|
||||||
return contactsByName[name]
|
return contactsByName[name]
|
||||||
}
|
}
|
||||||
|
function getLeadContact(mobile_no) {
|
||||||
|
mobile_no = mobile_no.replace(/[^0-9+]/g, '')
|
||||||
|
return leadContactsByPhone[mobile_no]
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contacts,
|
contacts,
|
||||||
getContact,
|
getContact,
|
||||||
getContactByName,
|
getContactByName,
|
||||||
|
getLeadContact,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,11 +1,30 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { getCurrentInstance } from 'vue'
|
import { getCurrentInstance, ref } from 'vue'
|
||||||
|
|
||||||
export const globalStore = defineStore('crm-global', () => {
|
export const globalStore = defineStore('crm-global', () => {
|
||||||
const app = getCurrentInstance()
|
const app = getCurrentInstance()
|
||||||
const { $dialog } = app.appContext.config.globalProperties
|
const { $dialog } = app.appContext.config.globalProperties
|
||||||
|
|
||||||
|
let twilioEnabled = ref(false)
|
||||||
|
let callMethod = () => {}
|
||||||
|
|
||||||
|
function setTwilioEnabled(value) {
|
||||||
|
twilioEnabled.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMakeCall(value) {
|
||||||
|
callMethod = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCall(number) {
|
||||||
|
callMethod(number)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
$dialog,
|
$dialog,
|
||||||
|
twilioEnabled,
|
||||||
|
makeCall,
|
||||||
|
setTwilioEnabled,
|
||||||
|
setMakeCall,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user