feat: create note & task from call window
This commit is contained in:
parent
e6a5da4c1b
commit
b212dac043
@ -6,76 +6,89 @@ from frappe.model.document import Document
|
|||||||
|
|
||||||
|
|
||||||
class CRMCallLog(Document):
|
class CRMCallLog(Document):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def default_list_data():
|
def default_list_data():
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
'label': 'From',
|
"label": "From",
|
||||||
'type': 'Link',
|
"type": "Link",
|
||||||
'key': 'caller',
|
"key": "caller",
|
||||||
'options': 'User',
|
"options": "User",
|
||||||
'width': '9rem',
|
"width": "9rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'To',
|
"label": "To",
|
||||||
'type': 'Link',
|
"type": "Link",
|
||||||
'key': 'receiver',
|
"key": "receiver",
|
||||||
'options': 'User',
|
"options": "User",
|
||||||
'width': '9rem',
|
"width": "9rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Type',
|
"label": "Type",
|
||||||
'type': 'Select',
|
"type": "Select",
|
||||||
'key': 'type',
|
"key": "type",
|
||||||
'width': '9rem',
|
"width": "9rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Status',
|
"label": "Status",
|
||||||
'type': 'Select',
|
"type": "Select",
|
||||||
'key': 'status',
|
"key": "status",
|
||||||
'width': '9rem',
|
"width": "9rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Duration',
|
"label": "Duration",
|
||||||
'type': 'Duration',
|
"type": "Duration",
|
||||||
'key': 'duration',
|
"key": "duration",
|
||||||
'width': '6rem',
|
"width": "6rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'From (number)',
|
"label": "From (number)",
|
||||||
'type': 'Data',
|
"type": "Data",
|
||||||
'key': 'from',
|
"key": "from",
|
||||||
'width': '9rem',
|
"width": "9rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'To (number)',
|
"label": "To (number)",
|
||||||
'type': 'Data',
|
"type": "Data",
|
||||||
'key': 'to',
|
"key": "to",
|
||||||
'width': '9rem',
|
"width": "9rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Created On',
|
"label": "Created On",
|
||||||
'type': 'Datetime',
|
"type": "Datetime",
|
||||||
'key': 'creation',
|
"key": "creation",
|
||||||
'width': '8rem',
|
"width": "8rem",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
rows = [
|
rows = [
|
||||||
"name",
|
"name",
|
||||||
"caller",
|
"caller",
|
||||||
"receiver",
|
"receiver",
|
||||||
"type",
|
"type",
|
||||||
"status",
|
"status",
|
||||||
"duration",
|
"duration",
|
||||||
"from",
|
"from",
|
||||||
"to",
|
"to",
|
||||||
"note",
|
"note",
|
||||||
"recording_url",
|
"recording_url",
|
||||||
"reference_doctype",
|
"reference_doctype",
|
||||||
"reference_docname",
|
"reference_docname",
|
||||||
"creation",
|
"creation",
|
||||||
]
|
]
|
||||||
return {'columns': columns, 'rows': rows}
|
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()
|
@frappe.whitelist()
|
||||||
def create_lead_from_call_log(call_log):
|
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.lead_owner = frappe.session.user
|
||||||
lead.save(ignore_permissions=True)
|
lead.save(ignore_permissions=True)
|
||||||
|
|
||||||
frappe.db.set_value("CRM Call Log", call_log.get("name"), {
|
frappe.db.set_value(
|
||||||
"reference_doctype": "CRM Lead",
|
"CRM Call Log",
|
||||||
"reference_docname": lead.name
|
call_log.get("name"),
|
||||||
})
|
{"reference_doctype": "CRM Lead", "reference_docname": lead.name},
|
||||||
|
)
|
||||||
|
|
||||||
if call_log.get("note"):
|
if call_log.get("note"):
|
||||||
frappe.db.set_value("FCRM Note", call_log.get("note"), {
|
frappe.db.set_value(
|
||||||
"reference_doctype": "CRM Lead",
|
"FCRM Note",
|
||||||
"reference_docname": lead.name
|
call_log.get("note"),
|
||||||
})
|
{"reference_doctype": "CRM Lead", "reference_docname": lead.name},
|
||||||
|
)
|
||||||
|
|
||||||
return lead.name
|
return lead.name
|
||||||
|
|||||||
@ -17,3 +17,32 @@ def is_call_integration_enabled():
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def set_default_calling_medium(medium):
|
def set_default_calling_medium(medium):
|
||||||
return frappe.db.set_value("FCRM Settings", "FCRM Settings", "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)
|
||||||
|
|||||||
@ -48,7 +48,7 @@ def handle_request(**kwargs):
|
|||||||
request_log.status = "Failed"
|
request_log.status = "Failed"
|
||||||
request_log.error = frappe.get_traceback()
|
request_log.error = frappe.get_traceback()
|
||||||
frappe.db.rollback()
|
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()
|
frappe.db.commit()
|
||||||
finally:
|
finally:
|
||||||
request_log.save(ignore_permissions=True)
|
request_log.save(ignore_permissions=True)
|
||||||
|
|||||||
@ -12,6 +12,45 @@ const timer = ref(null)
|
|||||||
const updatedTime = ref('0:00')
|
const updatedTime = ref('0:00')
|
||||||
|
|
||||||
function startCounter() {
|
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) {
|
if (seconds.value === 59) {
|
||||||
seconds.value = 0
|
seconds.value = 0
|
||||||
minutes.value = minutes.value + 1
|
minutes.value = minutes.value + 1
|
||||||
@ -36,22 +75,8 @@ function startCounter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedTime.value = hoursCount + minutesCount + ':' + secondsCount
|
return hoursCount + minutesCount + ':' + secondsCount
|
||||||
}
|
}
|
||||||
|
|
||||||
function start() {
|
defineExpose({ start, stop, getTime, updatedTime })
|
||||||
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 })
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -53,7 +53,7 @@ import {
|
|||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { createToast } from '@/utils'
|
import { createToast } from '@/utils'
|
||||||
import { FormControl, call } from 'frappe-ui'
|
import { FormControl, call } from 'frappe-ui'
|
||||||
import { ref, watch } from 'vue'
|
import { nextTick, ref, watch } from 'vue'
|
||||||
|
|
||||||
const { setMakeCall } = globalStore()
|
const { setMakeCall } = globalStore()
|
||||||
|
|
||||||
@ -116,20 +116,25 @@ async function setDefaultCallingMedium() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([twilioEnabled, exotelEnabled], ([twilioValue, exotelValue]) => {
|
watch(
|
||||||
if (twilioValue) {
|
[twilioEnabled, exotelEnabled],
|
||||||
twilio.value.setup()
|
([twilioValue, exotelValue]) =>
|
||||||
callMedium.value = 'Twilio'
|
nextTick(() => {
|
||||||
}
|
if (twilioValue) {
|
||||||
|
twilio.value.setup()
|
||||||
|
callMedium.value = 'Twilio'
|
||||||
|
}
|
||||||
|
|
||||||
if (exotelValue) {
|
if (exotelValue) {
|
||||||
exotel.value.setup()
|
exotel.value.setup()
|
||||||
callMedium.value = 'Exotel'
|
callMedium.value = 'Exotel'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (twilioValue || exotelValue) {
|
if (twilioValue || exotelValue) {
|
||||||
callMedium.value = 'Twilio'
|
callMedium.value = 'Twilio'
|
||||||
setMakeCall(makeCall)
|
setMakeCall(makeCall)
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -32,18 +32,21 @@
|
|||||||
<span>{{ __(callStatus) }}</span>
|
<span>{{ __(callStatus) }}</span>
|
||||||
<span v-if="callStatus == 'Call ended'">
|
<span v-if="callStatus == 'Call ended'">
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<span>{{ counterUp?.updatedTime }}</span>
|
<span>{{ callDuration }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>{{ __(callStatus) }}</div>
|
<div v-else>{{ __(callStatus) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-show="showCallPopup"
|
v-show="showCallPopup"
|
||||||
ref="callPopup"
|
class="fixed z-20 w-[280px] min-h-44 flex gap-2 flex-col rounded-lg bg-surface-gray-7 p-4 pt-2.5 text-ink-gray-2 shadow-2xl"
|
||||||
class="fixed z-20 w-[280px] min-h-44 flex gap-2 cursor-move select-none flex-col rounded-lg bg-surface-gray-7 p-4 pt-2.5 text-ink-gray-2 shadow-2xl"
|
|
||||||
:style="style"
|
:style="style"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<div class="header flex items-center justify-between gap-1 text-base">
|
<div
|
||||||
|
ref="callPopupHeader"
|
||||||
|
class="header flex items-center justify-between gap-1 text-base cursor-move select-none"
|
||||||
|
>
|
||||||
<div class="flex gap-2 items-center truncate">
|
<div class="flex gap-2 items-center truncate">
|
||||||
<div v-if="showNote || showTask" class="flex items-center gap-3">
|
<div v-if="showNote || showTask" class="flex items-center gap-3">
|
||||||
<Avatar
|
<Avatar
|
||||||
@ -60,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-1 text-base leading-4">
|
<div class="flex flex-col gap-1 text-base leading-4">
|
||||||
<div class="font-medium">
|
<div class="font-medium">
|
||||||
{{ contact.full_name ?? phoneNumber }}
|
{{ contact?.full_name ?? phoneNumber }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-ink-gray-6">
|
<div class="text-ink-gray-6">
|
||||||
<div v-if="callStatus == 'In progress'">
|
<div v-if="callStatus == 'In progress'">
|
||||||
@ -81,7 +84,7 @@
|
|||||||
<span>{{ __(callStatus) }}</span>
|
<span>{{ __(callStatus) }}</span>
|
||||||
<span v-if="callStatus == 'Call ended'">
|
<span v-if="callStatus == 'Call ended'">
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<span>{{ counterUp?.updatedTime }}</span>
|
<span>{{ callDuration }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>{{ __(callStatus) }}</div>
|
<div v-else>{{ __(callStatus) }}</div>
|
||||||
@ -105,7 +108,7 @@
|
|||||||
<span>{{ __(callStatus) }}</span>
|
<span>{{ __(callStatus) }}</span>
|
||||||
<span v-if="callStatus == 'Call ended'">
|
<span v-if="callStatus == 'Call ended'">
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<span>{{ counterUp?.updatedTime }}</span>
|
<span>{{ callDuration }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>{{ __(callStatus) }}</div>
|
<div v-else>{{ __(callStatus) }}</div>
|
||||||
@ -123,8 +126,18 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="body flex-1">
|
<div class="body flex-1">
|
||||||
<div v-if="showNote" class="h-[294px] text-base">{{ note }}</div>
|
<div v-if="showNote">
|
||||||
<div v-else-if="showTask" class="h-[294px] text-base">{{ task }}</div>
|
<TextEditor
|
||||||
|
variant="ghost"
|
||||||
|
ref="content"
|
||||||
|
editor-class="prose-sm h-[290px] text-ink-white overflow-auto mt-1"
|
||||||
|
:bubbleMenu="true"
|
||||||
|
:content="note"
|
||||||
|
@change="(val) => (note = val)"
|
||||||
|
:placeholder="__('Take a note...')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<TaskPanel ref="taskRef" v-else-if="showTask" :task="task" />
|
||||||
<div v-else class="flex items-center gap-3">
|
<div v-else class="flex items-center gap-3">
|
||||||
<Avatar
|
<Avatar
|
||||||
v-if="contact?.image"
|
v-if="contact?.image"
|
||||||
@ -139,10 +152,16 @@
|
|||||||
<AvatarIcon class="size-4" />
|
<AvatarIcon class="size-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="contact?.full_name" class="flex flex-col gap-1">
|
<div v-if="contact?.full_name" class="flex flex-col gap-1">
|
||||||
<div class="text-lg font-medium leading-5">{{ contact.full_name }}</div>
|
<div class="text-lg font-medium leading-5">
|
||||||
<div class="text-base text-ink-gray-6 leading-4">{{ phoneNumber }}</div>
|
{{ contact.full_name }}
|
||||||
|
</div>
|
||||||
|
<div class="text-base text-ink-gray-6 leading-4">
|
||||||
|
{{ phoneNumber }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-lg font-medium leading-5">
|
||||||
|
{{ phoneNumber }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-lg font-medium leading-5">{{ phoneNumber }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer flex justify-between gap-2">
|
<div class="footer flex justify-between gap-2">
|
||||||
@ -166,12 +185,23 @@
|
|||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div class="flex gap-2">
|
||||||
@click="closeCallPopup"
|
<Button
|
||||||
class="bg-surface-gray-6 text-ink-white hover:bg-surface-gray-5"
|
v-if="callStatus == 'Call ended' || callStatus == 'No answer'"
|
||||||
:label="__('Close')"
|
@click="closeCallPopup"
|
||||||
size="md"
|
class="bg-surface-gray-6 text-ink-white hover:bg-surface-gray-5"
|
||||||
/>
|
:label="__('Close')"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="(note && note != '<p></p>') || task.title"
|
||||||
|
@click="save"
|
||||||
|
class="bg-surface-white !text-ink-gray-9 hover:!bg-surface-gray-3"
|
||||||
|
variant="solid"
|
||||||
|
:label="__('Save')"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CountUpTimer ref="counterUp" />
|
<CountUpTimer ref="counterUp" />
|
||||||
@ -182,8 +212,9 @@ import AvatarIcon from '@/components/Icons/AvatarIcon.vue'
|
|||||||
import MinimizeIcon from '@/components/Icons/MinimizeIcon.vue'
|
import MinimizeIcon from '@/components/Icons/MinimizeIcon.vue'
|
||||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||||
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
||||||
|
import TaskPanel from './TaskPanel.vue'
|
||||||
import CountUpTimer from '@/components/CountUpTimer.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 { globalStore } from '@/stores/global'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
import { useDraggable, useWindowSize } from '@vueuse/core'
|
import { useDraggable, useWindowSize } from '@vueuse/core'
|
||||||
@ -192,7 +223,7 @@ import { ref, computed, onBeforeUnmount } from 'vue'
|
|||||||
const { getContact, getLeadContact } = contactsStore()
|
const { getContact, getLeadContact } = contactsStore()
|
||||||
const { $socket } = globalStore()
|
const { $socket } = globalStore()
|
||||||
|
|
||||||
const callPopup = ref(null)
|
const callPopupHeader = ref(null)
|
||||||
const showCallPopup = ref(false)
|
const showCallPopup = ref(false)
|
||||||
const showSmallCallPopup = ref(false)
|
const showSmallCallPopup = ref(false)
|
||||||
|
|
||||||
@ -207,7 +238,7 @@ function toggleCallPopup() {
|
|||||||
|
|
||||||
const { width, height } = useWindowSize()
|
const { width, height } = useWindowSize()
|
||||||
|
|
||||||
let { style } = useDraggable(callPopup, {
|
let { style } = useDraggable(callPopupHeader, {
|
||||||
initialValue: { x: width.value - 350, y: height.value - 250 },
|
initialValue: { x: width.value - 350, y: height.value - 250 },
|
||||||
preventDefault: true,
|
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)
|
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) {
|
function updateWindowHeight(condition) {
|
||||||
let top = parseInt(callPopup.value.style.top)
|
let callPopup = callPopupHeader.value.parentElement
|
||||||
|
let top = parseInt(callPopup.style.top)
|
||||||
let updatedTop = 0
|
let updatedTop = 0
|
||||||
|
|
||||||
updatedTop = condition ? top - 224 : top + 224
|
updatedTop = condition ? top - 224 : top + 224
|
||||||
@ -269,7 +331,7 @@ function updateWindowHeight(condition) {
|
|||||||
updatedTop = 10
|
updatedTop = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
callPopup.value.style.top = updatedTop + 'px'
|
callPopup.style.top = updatedTop + 'px'
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeOutgoingCall(number) {
|
function makeOutgoingCall(number) {
|
||||||
@ -304,9 +366,23 @@ function closeCallPopup() {
|
|||||||
showCallPopup.value = false
|
showCallPopup.value = false
|
||||||
showSmallCallPopup.value = false
|
showSmallCallPopup.value = false
|
||||||
note.value = ''
|
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) {
|
function updateStatus(data) {
|
||||||
// outgoing call
|
// outgoing call
|
||||||
if (
|
if (
|
||||||
@ -339,6 +415,9 @@ function updateStatus(data) {
|
|||||||
data.Status == 'completed'
|
data.Status == 'completed'
|
||||||
) {
|
) {
|
||||||
counterUp.value.stop()
|
counterUp.value.stop()
|
||||||
|
callDuration.value = getTime(
|
||||||
|
data['Legs[0][OnCallDuration]'] || data.DialCallDuration,
|
||||||
|
)
|
||||||
return 'Call ended'
|
return 'Call ended'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,12 +427,16 @@ function updateStatus(data) {
|
|||||||
data.Direction == 'incoming' &&
|
data.Direction == 'incoming' &&
|
||||||
data.Status == 'busy'
|
data.Status == 'busy'
|
||||||
) {
|
) {
|
||||||
|
phoneNumber.value = data.From || data.CallFrom
|
||||||
return 'Incoming call'
|
return 'Incoming call'
|
||||||
} else if (
|
} else if (
|
||||||
data.EventType == 'Terminal' &&
|
|
||||||
data.Direction == 'incoming' &&
|
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'
|
return 'Call ended'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -376,4 +459,8 @@ defineExpose({ makeOutgoingCall, setup })
|
|||||||
.blink {
|
.blink {
|
||||||
animation: blink 1s ease-in-out 6;
|
animation: blink 1s ease-in-out 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.ProseMirror) {
|
||||||
|
caret-color: var(--ink-white);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
144
frontend/src/components/Telephony/TaskPanel.vue
Normal file
144
frontend/src/components/Telephony/TaskPanel.vue
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-[294px] text-base">
|
||||||
|
<FormControl
|
||||||
|
type="text"
|
||||||
|
variant="ghost"
|
||||||
|
class="mb-2 title"
|
||||||
|
v-model="task.title"
|
||||||
|
:placeholder="__('Schedule a task...')"
|
||||||
|
/>
|
||||||
|
<TextEditor
|
||||||
|
variant="ghost"
|
||||||
|
ref="content"
|
||||||
|
editor-class="prose-sm h-[150px] text-ink-white overflow-auto"
|
||||||
|
:bubbleMenu="true"
|
||||||
|
:content="task.description"
|
||||||
|
@change="(val) => (task.description = val)"
|
||||||
|
:placeholder="__('Add description...')"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Dropdown :options="taskStatusOptions(updateTaskStatus)">
|
||||||
|
<Button
|
||||||
|
:label="task.status"
|
||||||
|
class="bg-surface-gray-6 text-ink-white hover:bg-surface-gray-5"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<TaskStatusIcon :status="task.status" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
<Dropdown :options="taskPriorityOptions(updateTaskPriority)">
|
||||||
|
<Button
|
||||||
|
:label="task.priority"
|
||||||
|
class="bg-surface-gray-6 text-ink-white hover:bg-surface-gray-5"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<TaskPriorityIcon :priority="task.priority" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
class="user"
|
||||||
|
:value="getUser(task.assigned_to).full_name"
|
||||||
|
doctype="User"
|
||||||
|
@change="(option) => (task.assigned_to = option)"
|
||||||
|
:placeholder="__('John Doe')"
|
||||||
|
:hideMe="true"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<UserAvatar class="mr-2 !h-4 !w-4" :user="task.assigned_to" />
|
||||||
|
</template>
|
||||||
|
<template #item-prefix="{ option }">
|
||||||
|
<UserAvatar class="mr-2" :user="option.value" size="sm" />
|
||||||
|
</template>
|
||||||
|
<template #item-label="{ option }">
|
||||||
|
<Tooltip :text="option.value">
|
||||||
|
<div class="cursor-pointer text-ink-gray-9">
|
||||||
|
{{ getUser(option.value).full_name }}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
|
</Link>
|
||||||
|
<DateTimePicker
|
||||||
|
class="datepicker w-36"
|
||||||
|
v-model="task.due_date"
|
||||||
|
:placeholder="__('01/04/2024 11:30 PM')"
|
||||||
|
:formatter="(date) => getFormat(date, '', true, true)"
|
||||||
|
input-class="border-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
|
||||||
|
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
|
||||||
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { taskStatusOptions, taskPriorityOptions, getFormat } from '@/utils'
|
||||||
|
import { TextEditor, Dropdown, Tooltip, DateTimePicker } from 'frappe-ui'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
task: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
assigned_to: '',
|
||||||
|
due_date: '',
|
||||||
|
status: 'Backlog',
|
||||||
|
priority: 'Low',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { getUser } = usersStore()
|
||||||
|
|
||||||
|
function updateTaskStatus(status) {
|
||||||
|
props.task.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTaskPriority(priority) {
|
||||||
|
props.task.priority = priority
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
:deep(.title input) {
|
||||||
|
background-color: var(--surface-gray-7);
|
||||||
|
caret-color: var(--ink-white);
|
||||||
|
color: var(--ink-white);
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
:deep(.datepicker input) {
|
||||||
|
background-color: var(--surface-gray-6);
|
||||||
|
caret-color: var(--ink-white);
|
||||||
|
color: var(--ink-white);
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.title input:focus),
|
||||||
|
:deep(.datepicker input:focus) {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.user button) {
|
||||||
|
background-color: var(--surface-gray-6);
|
||||||
|
border: none;
|
||||||
|
color: var(--ink-white);
|
||||||
|
}
|
||||||
|
:deep(.user button:hover) {
|
||||||
|
background-color: var(--surface-gray-5);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
:deep(.user button:focus) {
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user