1
0
forked from test/crm

Merge pull request #55 from shariquerik/call-ui-fix

fix: Call UI fixes
This commit is contained in:
Shariq Ansari 2024-01-13 13:55:10 +05:30 committed by GitHub
commit 4718843c4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 271 additions and 108 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
} }
} }
}) })

View File

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

View File

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

View File

@ -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="{

View File

@ -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="{

View File

@ -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="{

View File

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

View File

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

View File

@ -70,14 +70,14 @@
> >
&middot; &middot;
</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()

View File

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

View File

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

View File

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

View File

@ -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,
} }
}) })

View File

@ -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,
} }
}) })