diff --git a/crm/fcrm/doctype/crm_call_log/crm_call_log.py b/crm/fcrm/doctype/crm_call_log/crm_call_log.py index 42752c6a..ba629a9c 100644 --- a/crm/fcrm/doctype/crm_call_log/crm_call_log.py +++ b/crm/fcrm/doctype/crm_call_log/crm_call_log.py @@ -6,76 +6,89 @@ from frappe.model.document import Document class CRMCallLog(Document): - @staticmethod - def default_list_data(): - columns = [ - { - 'label': 'From', - 'type': 'Link', - 'key': 'caller', - 'options': 'User', - 'width': '9rem', - }, - { - 'label': 'To', - 'type': 'Link', - 'key': 'receiver', - 'options': 'User', - 'width': '9rem', - }, - { - 'label': 'Type', - 'type': 'Select', - 'key': 'type', - 'width': '9rem', - }, - { - 'label': 'Status', - 'type': 'Select', - 'key': 'status', - 'width': '9rem', - }, - { - 'label': 'Duration', - 'type': 'Duration', - 'key': 'duration', - 'width': '6rem', - }, - { - 'label': 'From (number)', - 'type': 'Data', - 'key': 'from', - 'width': '9rem', - }, - { - 'label': 'To (number)', - 'type': 'Data', - 'key': 'to', - 'width': '9rem', - }, - { - 'label': 'Created On', - 'type': 'Datetime', - 'key': 'creation', - 'width': '8rem', - }, - ] - rows = [ - "name", - "caller", - "receiver", - "type", - "status", - "duration", - "from", - "to", - "note", - "recording_url", - "reference_doctype", - "reference_docname", - "creation", - ] - return {'columns': columns, 'rows': rows} + @staticmethod + def default_list_data(): + columns = [ + { + "label": "From", + "type": "Link", + "key": "caller", + "options": "User", + "width": "9rem", + }, + { + "label": "To", + "type": "Link", + "key": "receiver", + "options": "User", + "width": "9rem", + }, + { + "label": "Type", + "type": "Select", + "key": "type", + "width": "9rem", + }, + { + "label": "Status", + "type": "Select", + "key": "status", + "width": "9rem", + }, + { + "label": "Duration", + "type": "Duration", + "key": "duration", + "width": "6rem", + }, + { + "label": "From (number)", + "type": "Data", + "key": "from", + "width": "9rem", + }, + { + "label": "To (number)", + "type": "Data", + "key": "to", + "width": "9rem", + }, + { + "label": "Created On", + "type": "Datetime", + "key": "creation", + "width": "8rem", + }, + ] + rows = [ + "name", + "caller", + "receiver", + "type", + "status", + "duration", + "from", + "to", + "note", + "recording_url", + "reference_doctype", + "reference_docname", + "creation", + ] + return {"columns": columns, "rows": rows} + + def has_link(self, doctype, name): + for link in self.links: + if link.link_doctype == doctype and link.link_name == name: + return True + + def link_with_reference_doc(self, reference_doctype, reference_name): + if self.has_link(reference_doctype, reference_name): + return + + self.append("links", {"link_doctype": reference_doctype, "link_name": reference_name}) + self.save(ignore_permissions=True) + @frappe.whitelist() def create_lead_from_call_log(call_log): @@ -85,15 +98,17 @@ def create_lead_from_call_log(call_log): lead.lead_owner = frappe.session.user lead.save(ignore_permissions=True) - frappe.db.set_value("CRM Call Log", call_log.get("name"), { - "reference_doctype": "CRM Lead", - "reference_docname": lead.name - }) + frappe.db.set_value( + "CRM Call Log", + call_log.get("name"), + {"reference_doctype": "CRM Lead", "reference_docname": lead.name}, + ) if call_log.get("note"): - frappe.db.set_value("FCRM Note", call_log.get("note"), { - "reference_doctype": "CRM Lead", - "reference_docname": lead.name - }) + frappe.db.set_value( + "FCRM Note", + call_log.get("note"), + {"reference_doctype": "CRM Lead", "reference_docname": lead.name}, + ) - return lead.name \ No newline at end of file + return lead.name diff --git a/crm/integrations/api.py b/crm/integrations/api.py index 73dae584..d4f8ede1 100644 --- a/crm/integrations/api.py +++ b/crm/integrations/api.py @@ -17,3 +17,32 @@ def is_call_integration_enabled(): @frappe.whitelist() def set_default_calling_medium(medium): return frappe.db.set_value("FCRM Settings", "FCRM Settings", "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) + + call_log = frappe.get_doc("CRM Call Log", call_sid) + call_log.link_with_reference_doc("FCRM Note", note.name) + + +@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) + + call_log = frappe.get_doc("CRM Call Log", call_sid) + call_log.link_with_reference_doc("CRM Task", _task.name) diff --git a/crm/integrations/exotel/handler.py b/crm/integrations/exotel/handler.py index 3b6dd523..8bb91db6 100644 --- a/crm/integrations/exotel/handler.py +++ b/crm/integrations/exotel/handler.py @@ -48,7 +48,7 @@ def handle_request(**kwargs): request_log.status = "Failed" request_log.error = frappe.get_traceback() frappe.db.rollback() - frappe.log_error(title="Error while creating call record") + frappe.log_error(title="Error while creating/updating call record") frappe.db.commit() finally: request_log.save(ignore_permissions=True) diff --git a/frontend/src/components/CountUpTimer.vue b/frontend/src/components/CountUpTimer.vue index 47b6b47d..a34a3924 100644 --- a/frontend/src/components/CountUpTimer.vue +++ b/frontend/src/components/CountUpTimer.vue @@ -12,6 +12,45 @@ const timer = ref(null) const updatedTime = ref('0:00') function startCounter() { + updatedTime.value = getTime() +} + +function start() { + timer.value = setInterval(() => startCounter(), 1000) +} + +function stop() { + clearInterval(timer.value) + let output = updatedTime.value + hours.value = 0 + minutes.value = 0 + seconds.value = 0 + updatedTime.value = '0:00' + return output +} + +function getTime(_seconds = 0) { + if (_seconds) { + if (typeof _seconds === 'string') { + _seconds = parseInt(_seconds) + } + seconds.value = _seconds + + if (seconds.value >= 60) { + minutes.value = Math.floor(seconds.value / 60) + seconds.value = seconds.value % 60 + } else { + minutes.value = 0 + } + + if (minutes.value >= 60) { + hours.value = Math.floor(minutes.value / 60) + minutes.value = minutes.value % 60 + } else { + hours.value = 0 + } + } + if (seconds.value === 59) { seconds.value = 0 minutes.value = minutes.value + 1 @@ -36,22 +75,8 @@ function startCounter() { } } - updatedTime.value = hoursCount + minutesCount + ':' + secondsCount + return hoursCount + minutesCount + ':' + secondsCount } -function start() { - timer.value = setInterval(() => startCounter(), 1000) -} - -function stop() { - clearInterval(timer.value) - let output = updatedTime.value - hours.value = 0 - minutes.value = 0 - seconds.value = 0 - updatedTime.value = '0:00' - return output -} - -defineExpose({ start, stop, updatedTime }) +defineExpose({ start, stop, getTime, updatedTime }) diff --git a/frontend/src/components/Telephony/CallUI.vue b/frontend/src/components/Telephony/CallUI.vue index 666027db..8c313fd8 100644 --- a/frontend/src/components/Telephony/CallUI.vue +++ b/frontend/src/components/Telephony/CallUI.vue @@ -53,7 +53,7 @@ import { import { globalStore } from '@/stores/global' import { createToast } from '@/utils' import { FormControl, call } from 'frappe-ui' -import { ref, watch } from 'vue' +import { nextTick, ref, watch } from 'vue' const { setMakeCall } = globalStore() @@ -116,20 +116,25 @@ async function setDefaultCallingMedium() { }) } -watch([twilioEnabled, exotelEnabled], ([twilioValue, exotelValue]) => { - if (twilioValue) { - twilio.value.setup() - callMedium.value = 'Twilio' - } +watch( + [twilioEnabled, exotelEnabled], + ([twilioValue, exotelValue]) => + nextTick(() => { + if (twilioValue) { + twilio.value.setup() + callMedium.value = 'Twilio' + } - if (exotelValue) { - exotel.value.setup() - callMedium.value = 'Exotel' - } + if (exotelValue) { + exotel.value.setup() + callMedium.value = 'Exotel' + } - if (twilioValue || exotelValue) { - callMedium.value = 'Twilio' - setMakeCall(makeCall) - } -}) + if (twilioValue || exotelValue) { + callMedium.value = 'Twilio' + setMakeCall(makeCall) + } + }), + { immediate: true }, +) diff --git a/frontend/src/components/Telephony/ExotelCallUI.vue b/frontend/src/components/Telephony/ExotelCallUI.vue index 97690959..3c9b7d18 100644 --- a/frontend/src/components/Telephony/ExotelCallUI.vue +++ b/frontend/src/components/Telephony/ExotelCallUI.vue @@ -32,18 +32,21 @@ {{ __(callStatus) }} · - {{ counterUp?.updatedTime }} + {{ callDuration }}
{{ __(callStatus) }}
-
+
- {{ contact.full_name ?? phoneNumber }} + {{ contact?.full_name ?? phoneNumber }}
@@ -81,7 +84,7 @@ {{ __(callStatus) }} · - {{ counterUp?.updatedTime }} + {{ callDuration }}
{{ __(callStatus) }}
@@ -105,7 +108,7 @@ {{ __(callStatus) }} · - {{ counterUp?.updatedTime }} + {{ callDuration }}
{{ __(callStatus) }}
@@ -123,8 +126,18 @@
-
{{ note }}
-
{{ task }}
+
+ +
+
-
{{ contact.full_name }}
-
{{ phoneNumber }}
+
+ {{ contact.full_name }} +
+
+ {{ phoneNumber }} +
+
+
+ {{ phoneNumber }}
-
{{ phoneNumber }}
-
@@ -182,8 +212,9 @@ import AvatarIcon from '@/components/Icons/AvatarIcon.vue' import MinimizeIcon from '@/components/Icons/MinimizeIcon.vue' import NoteIcon from '@/components/Icons/NoteIcon.vue' import TaskIcon from '@/components/Icons/TaskIcon.vue' +import TaskPanel from './TaskPanel.vue' import CountUpTimer from '@/components/CountUpTimer.vue' -import { Avatar, Button, call } from 'frappe-ui' +import { TextEditor, Avatar, Button, call } from 'frappe-ui' import { globalStore } from '@/stores/global' import { contactsStore } from '@/stores/contacts' import { useDraggable, useWindowSize } from '@vueuse/core' @@ -192,7 +223,7 @@ import { ref, computed, onBeforeUnmount } from 'vue' const { getContact, getLeadContact } = contactsStore() const { $socket } = globalStore() -const callPopup = ref(null) +const callPopupHeader = ref(null) const showCallPopup = ref(false) const showSmallCallPopup = ref(false) @@ -207,7 +238,7 @@ function toggleCallPopup() { const { width, height } = useWindowSize() -let { style } = useDraggable(callPopup, { +let { style } = useDraggable(callPopupHeader, { initialValue: { x: width.value - 350, y: height.value - 250 }, preventDefault: true, }) @@ -245,7 +276,22 @@ function showNoteWindow() { } } -const task = ref('') +function createNote() { + call('crm.integrations.api.create_and_add_note_to_call_log', { + call_sid: callData.value.CallSid, + content: note.value, + }) + note.value = '' +} + +const task = ref({ + title: '', + description: '', + assigned_to: '', + due_date: '', + status: 'Backlog', + priority: 'Low', +}) const showTask = ref(false) @@ -259,8 +305,24 @@ function showTaskWindow() { } } +function createTask() { + call('crm.integrations.api.create_and_add_task_to_call_log', { + call_sid: callData.value.CallSid, + task: task.value, + }) + task.value = { + title: '', + description: '', + assigned_to: '', + due_date: '', + status: 'Backlog', + priority: 'Low', + } +} + function updateWindowHeight(condition) { - let top = parseInt(callPopup.value.style.top) + let callPopup = callPopupHeader.value.parentElement + let top = parseInt(callPopup.style.top) let updatedTop = 0 updatedTop = condition ? top - 224 : top + 224 @@ -269,7 +331,7 @@ function updateWindowHeight(condition) { updatedTop = 10 } - callPopup.value.style.top = updatedTop + 'px' + callPopup.style.top = updatedTop + 'px' } function makeOutgoingCall(number) { @@ -304,9 +366,23 @@ function closeCallPopup() { showCallPopup.value = false showSmallCallPopup.value = false note.value = '' - task.value = '' + task.value = { + title: '', + description: '', + assigned_to: '', + due_date: '', + status: 'Backlog', + priority: 'Low', + } } +function save() { + if (note.value) createNote() + if (task.value.title) createTask() +} + +const callDuration = ref('00:00') + function updateStatus(data) { // outgoing call if ( @@ -339,6 +415,9 @@ function updateStatus(data) { data.Status == 'completed' ) { counterUp.value.stop() + callDuration.value = getTime( + data['Legs[0][OnCallDuration]'] || data.DialCallDuration, + ) return 'Call ended' } @@ -348,12 +427,16 @@ function updateStatus(data) { data.Direction == 'incoming' && data.Status == 'busy' ) { + phoneNumber.value = data.From || data.CallFrom return 'Incoming call' } else if ( - data.EventType == 'Terminal' && data.Direction == 'incoming' && - data.Status == 'free' + (data.EventType == 'Terminal' || data.CallType == 'completed') && + (data.Status == 'free' || data.DialCallStatus == 'completed') ) { + callDuration.value = counterUp.value.getTime( + data['Legs[0][OnCallDuration]'] || data.DialCallDuration, + ) return 'Call ended' } } @@ -376,4 +459,8 @@ defineExpose({ makeOutgoingCall, setup }) .blink { animation: blink 1s ease-in-out 6; } + +:deep(.ProseMirror) { + caret-color: var(--ink-white); +} diff --git a/frontend/src/components/Telephony/TaskPanel.vue b/frontend/src/components/Telephony/TaskPanel.vue new file mode 100644 index 00000000..bf9641a5 --- /dev/null +++ b/frontend/src/components/Telephony/TaskPanel.vue @@ -0,0 +1,144 @@ + + +