fix: revamped call log page

This commit is contained in:
Shariq Ansari 2023-08-30 20:04:08 +05:30
parent 448bedd8b0
commit bf854fc3cc
4 changed files with 240 additions and 120 deletions

View File

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

View File

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

View File

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

View File

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