fix: revamped call log page
This commit is contained in:
parent
448bedd8b0
commit
bf854fc3cc
@ -1,9 +1,39 @@
|
|||||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
# import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
class CRMCallLog(Document):
|
class CRMCallLog(Document):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_call_log(name):
|
||||||
|
doc = frappe.get_doc("CRM Call Log", name)
|
||||||
|
_doc = doc.as_dict()
|
||||||
|
if doc.lead:
|
||||||
|
_doc.lead_name = frappe.db.get_value("CRM Lead", doc.lead, "lead_name")
|
||||||
|
if doc.note:
|
||||||
|
note = frappe.db.get_values("CRM Note", doc.note, ["title", "content"])[0]
|
||||||
|
_doc.note_doc = {
|
||||||
|
"title": note[0],
|
||||||
|
"content": note[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return _doc
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def create_lead_from_call_log(call_log):
|
||||||
|
lead = frappe.new_doc("CRM Lead")
|
||||||
|
lead.first_name = "Lead from call " + call_log.get("from")
|
||||||
|
lead.mobile_no = call_log.get("from")
|
||||||
|
lead.lead_owner = frappe.session.user
|
||||||
|
lead.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
frappe.db.set_value("CRM Call Log", call_log.get("name"), "lead", lead.name)
|
||||||
|
|
||||||
|
if call_log.get("note"):
|
||||||
|
frappe.db.set_value("CRM Note", call_log.get("note"), "lead", lead.name)
|
||||||
|
|
||||||
|
return lead.name
|
||||||
@ -99,7 +99,7 @@
|
|||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<DurationIcon class="w-4 h-4 text-gray-600" />
|
<DurationIcon class="w-4 h-4 text-gray-600" />
|
||||||
<div class="text-sm text-gray-600">Duration</div>
|
<div class="text-sm text-gray-600">Duration</div>
|
||||||
<div class="text-sm">{{ call.duration }}s</div>
|
<div class="text-sm">{{ secondsToDuration(call.duration) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1 cursor-pointer select-none"
|
class="flex items-center gap-1 cursor-pointer select-none"
|
||||||
@ -259,7 +259,7 @@ import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
|||||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||||
import DurationIcon from '@/components/Icons/DurationIcon.vue'
|
import DurationIcon from '@/components/Icons/DurationIcon.vue'
|
||||||
import PlayIcon from '@/components/Icons/PlayIcon.vue'
|
import PlayIcon from '@/components/Icons/PlayIcon.vue'
|
||||||
import { timeAgo, dateFormat, dateTooltipFormat } from '@/utils'
|
import { timeAgo, dateFormat, dateTooltipFormat, secondsToDuration } from '@/utils'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { Button, FeatherIcon, Tooltip, Dropdown, TextEditor } from 'frappe-ui'
|
import { Button, FeatherIcon, Tooltip, Dropdown, TextEditor } from 'frappe-ui'
|
||||||
import { computed, h } from 'vue'
|
import { computed, h } from 'vue'
|
||||||
|
|||||||
@ -1,65 +1,168 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader v-if="callLog.doc">
|
<LayoutHeader v-if="callLog.data">
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<Button v-if="!callLog.doc.lead" variant="solid" label="Create lead" @click="createLead">
|
<Button
|
||||||
|
v-if="!callLog.data.lead"
|
||||||
|
variant="solid"
|
||||||
|
label="Create lead"
|
||||||
|
@click="createLead"
|
||||||
|
>
|
||||||
<template #prefix><FeatherIcon name="plus" class="h-4" /></template>
|
<template #prefix><FeatherIcon name="plus" class="h-4" /></template>
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</LayoutHeader>
|
||||||
<div class="border-b"></div>
|
<div class="border-b"></div>
|
||||||
<div v-if="callLog.doc" class="p-3">
|
<div v-if="callLog.data" class="p-6 max-w-lg">
|
||||||
<div class="px-3 pb-1 text-base font-medium">{{ details.label }}</div>
|
<div class="pb-3 text-base font-medium">Call details</div>
|
||||||
<div class="grid grid-cols-5 gap-4 p-3">
|
<div class="flex flex-col gap-4 border rounded-lg p-4 mb-3 shadow-sm">
|
||||||
<div
|
<div class="flex items-center justify-between">
|
||||||
v-for="field in details.fields"
|
<div class="flex items-center gap-2">
|
||||||
:key="field.key"
|
<FeatherIcon
|
||||||
class="flex flex-col gap-2"
|
:name="
|
||||||
>
|
callLog.data.type == 'Incoming'
|
||||||
<div class="text-sm text-gray-500">{{ field.label }}</div>
|
? 'phone-incoming'
|
||||||
<div class="text-sm text-gray-900">{{ callLog.doc[field.key] }}</div>
|
: 'phone-outgoing'
|
||||||
|
"
|
||||||
|
class="w-4 h-4 text-gray-600"
|
||||||
|
/>
|
||||||
|
<div class="font-medium">
|
||||||
|
{{ callLog.data.type == 'Incoming' ? 'Inbound' : 'Outbound' }} call
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Badge
|
||||||
|
:variant="'subtle'"
|
||||||
|
:theme="callLog.data.status === 'Completed' ? 'green' : 'gray'"
|
||||||
|
size="md"
|
||||||
|
:label="callLog.data.status"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Avatar
|
||||||
|
:image="callLog.data.caller.image"
|
||||||
|
:label="callLog.data.caller.label"
|
||||||
|
size="xl"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col gap-1 ml-1">
|
||||||
|
<div class="text-base font-medium">
|
||||||
|
{{ callLog.data.caller.label }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600">
|
||||||
|
{{ callLog.data.from }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FeatherIcon name="arrow-right" class="w-5 h-5 text-gray-600 mx-2" />
|
||||||
|
<Avatar
|
||||||
|
:image="callLog.data.receiver.image"
|
||||||
|
:label="callLog.data.receiver.label"
|
||||||
|
size="xl"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col gap-1 ml-1">
|
||||||
|
<div class="text-base font-medium">
|
||||||
|
{{ callLog.data.receiver.label }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600">
|
||||||
|
{{ callLog.data.to }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<DurationIcon class="w-4 h-4 text-gray-600" />
|
||||||
|
<div class="text-sm text-gray-600">Duration</div>
|
||||||
|
<div class="text-sm">{{ callLog.data.duration }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Tooltip
|
||||||
|
class="text-gray-600 text-sm"
|
||||||
|
:text="dateFormat(callLog.data.modified, dateTooltipFormat)"
|
||||||
|
>
|
||||||
|
{{ timeAgo(callLog.data.modified) }}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="px-3 pb-1 text-base font-medium mt-3">Call note</div>
|
|
||||||
<div v-if="callNote?.doc" class="flex flex-col p-3">
|
<div v-if="callLog.data.recording_url" class="mt-6">
|
||||||
<TextInput
|
<div class="mb-3 text-base font-medium">Call recording</div>
|
||||||
type="text"
|
<div class="flex items-center justify-between border rounded shadow-sm">
|
||||||
class="text-base bg-white border-none !pl-0 hover:bg-white focus:!shadow-none focus-visible:!ring-0"
|
<audio
|
||||||
v-model="callNote.doc.title"
|
class="audio-control"
|
||||||
placeholder="Untitled note"
|
controls
|
||||||
/>
|
:src="callLog.data.recording_url"
|
||||||
<TextEditor
|
></audio>
|
||||||
ref="content"
|
</div>
|
||||||
editor-class="!prose-sm !leading-5 max-w-none p-2 pl-0 overflow-auto focus:outline-none"
|
</div>
|
||||||
:bubbleMenu="true"
|
|
||||||
:content="callNote.doc.content"
|
<div v-if="callLog.data.note" class="mt-6">
|
||||||
@change="(val) => (callNote.doc.content = val)"
|
<div class="mb-3 text-base font-medium">Call note</div>
|
||||||
placeholder="Type something and press enter"
|
<div
|
||||||
/>
|
class="flex flex-col gap-3 border rounded p-4 shadow-sm cursor-pointer h-56"
|
||||||
</div> -->
|
@click="showNoteModal = true"
|
||||||
|
>
|
||||||
|
<div class="text-lg font-medium truncate">
|
||||||
|
{{ callLog.data.note_doc.title }}
|
||||||
|
</div>
|
||||||
|
<TextEditor
|
||||||
|
v-if="callLog.data.note_doc.content"
|
||||||
|
:content="callLog.data.note_doc.content"
|
||||||
|
:editable="false"
|
||||||
|
editor-class="!prose-sm max-w-none !text-sm text-gray-600 focus:outline-none"
|
||||||
|
class="flex-1 overflow-hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="callLog.data.lead" class="mt-6">
|
||||||
|
<div class="mb-3 text-base font-medium">Lead</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
:route="{ name: 'Lead', params: { leadId: callLog.data.lead } }"
|
||||||
|
:label="callLog.data.lead_name"
|
||||||
|
class="p-4"
|
||||||
|
>
|
||||||
|
<template #prefix><Avatar :label="callLog.data.lead_name" /></template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<NoteModal
|
||||||
|
v-model="showNoteModal"
|
||||||
|
:note="callLog.data?.note_doc"
|
||||||
|
@updateNote="updateNote"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
||||||
|
import DurationIcon from '@/components/Icons/DurationIcon.vue'
|
||||||
|
import NoteModal from '@/components/NoteModal.vue'
|
||||||
|
import { dateFormat, timeAgo, dateTooltipFormat } from '@/utils'
|
||||||
import {
|
import {
|
||||||
createDocumentResource,
|
|
||||||
TextInput,
|
|
||||||
TextEditor,
|
TextEditor,
|
||||||
|
Avatar,
|
||||||
FeatherIcon,
|
FeatherIcon,
|
||||||
call,
|
call,
|
||||||
|
Tooltip,
|
||||||
|
Badge,
|
||||||
|
createResource,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
import { computed } from 'vue'
|
import { secondsToDuration } from '@/utils'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
const { contacts } = contactsStore()
|
const { contacts, getContact } = contactsStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
callLogId: {
|
callLogId: {
|
||||||
@ -68,94 +171,82 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const callLog = createDocumentResource({
|
const showNoteModal = ref(false)
|
||||||
doctype: 'CRM Call Log',
|
|
||||||
name: props.callLogId,
|
const callLog = createResource({
|
||||||
setValue: {},
|
url: 'crm.crm.doctype.crm_call_log.crm_call_log.get_call_log',
|
||||||
|
auto: true,
|
||||||
|
cache: ['callLog', props.callLogId],
|
||||||
|
params: {
|
||||||
|
name: props.callLogId,
|
||||||
|
},
|
||||||
|
transform: (doc) => {
|
||||||
|
doc.duration = secondsToDuration(doc.duration)
|
||||||
|
if (doc.type === 'Incoming') {
|
||||||
|
doc.caller = {
|
||||||
|
label: getContact(doc.from)?.full_name || 'Unknown',
|
||||||
|
image: getContact(doc.from)?.image,
|
||||||
|
}
|
||||||
|
doc.receiver = {
|
||||||
|
label: getUser(doc.receiver).full_name,
|
||||||
|
image: getUser(doc.receiver).user_image,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
doc.caller = {
|
||||||
|
label: getUser(doc.caller).full_name,
|
||||||
|
image: getUser(doc.caller).user_image,
|
||||||
|
}
|
||||||
|
doc.receiver = {
|
||||||
|
label: getContact(doc.to)?.full_name || 'Unknown',
|
||||||
|
image: getContact(doc.to)?.image,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return doc
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const breadcrumbs = computed(() => [
|
async function updateNote(_note) {
|
||||||
{ label: 'Call Logs', route: { name: 'Call Logs' } },
|
if (_note.title || _note.content) {
|
||||||
{ label: callLog.doc?.from },
|
let d = await call('frappe.client.set_value', {
|
||||||
])
|
doctype: 'CRM Note',
|
||||||
|
name: callLog.data?.note,
|
||||||
const details = {
|
fieldname: _note,
|
||||||
label: 'Call Details',
|
})
|
||||||
fields: [
|
if (d.name) {
|
||||||
{
|
callLog.reload()
|
||||||
label: 'From',
|
}
|
||||||
key: 'from',
|
|
||||||
type: 'data',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'To',
|
|
||||||
key: 'to',
|
|
||||||
type: 'data',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Duration',
|
|
||||||
key: 'duration',
|
|
||||||
type: 'data',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Start Time',
|
|
||||||
key: 'start_time',
|
|
||||||
type: 'data',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'End Time',
|
|
||||||
key: 'end_time',
|
|
||||||
type: 'data',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Type',
|
|
||||||
key: 'type',
|
|
||||||
type: 'data',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Status',
|
|
||||||
key: 'status',
|
|
||||||
type: 'data',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
// const callNote = computed(() => {
|
|
||||||
// return createDocumentResource({
|
|
||||||
// doctype: 'CRM Note',
|
|
||||||
// name: callLog.doc?.note,
|
|
||||||
// auto: true,
|
|
||||||
// setValue: {},
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
||||||
async function createLead() {
|
|
||||||
let d = await call('frappe.client.insert', {
|
|
||||||
doc: {
|
|
||||||
doctype: 'CRM Lead',
|
|
||||||
first_name: "Lead from " + callLog.doc.from,
|
|
||||||
mobile_no: callLog.doc.from,
|
|
||||||
lead_owner: getUser().name,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (d.name) {
|
|
||||||
await update_call_log(d.name)
|
|
||||||
await update_note(d.name)
|
|
||||||
contacts.reload()
|
|
||||||
router.push({ name: 'Lead', params: { leadId: d.name } })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function update_note(lead) {
|
function createLead() {
|
||||||
await call('frappe.client.set_value', {
|
call('crm.crm.doctype.crm_call_log.crm_call_log.create_lead_from_call_log', {
|
||||||
doctype: 'CRM Note',
|
call_log: callLog.data,
|
||||||
name: callLog.doc?.note,
|
}).then((d) => {
|
||||||
fieldname: 'lead',
|
if (d) {
|
||||||
value: lead,
|
callLog.reload()
|
||||||
|
contacts.reload()
|
||||||
|
router.push({ name: 'Lead', params: { leadId: d } })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function update_call_log(lead) {
|
const breadcrumbs = computed(() => [
|
||||||
callLog.setValue.submit({ lead: lead })
|
{ label: 'Call Logs', route: { name: 'Call Logs' } },
|
||||||
}
|
{ label: callLog.data?.caller.label },
|
||||||
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.audio-control {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-control::-webkit-media-controls-panel {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -94,7 +94,6 @@ const columns = [
|
|||||||
key: 'duration',
|
key: 'duration',
|
||||||
type: 'icon',
|
type: 'icon',
|
||||||
size: 'w-20',
|
size: 'w-20',
|
||||||
align: 'text-right'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'From (number)',
|
label: 'From (number)',
|
||||||
@ -137,7 +136,7 @@ const rows = computed(() => {
|
|||||||
}
|
}
|
||||||
receiver = {
|
receiver = {
|
||||||
label: getContact(callLog.to)?.full_name || 'Unknown',
|
label: getContact(callLog.to)?.full_name || 'Unknown',
|
||||||
image: getContact(callLog.from)?.image,
|
image: getContact(callLog.to)?.image,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user