Merge pull request #540 from frappe/develop

This commit is contained in:
Shariq Ansari 2025-01-22 19:14:30 +05:30 committed by GitHub
commit 93bc4e6dba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 674 additions and 271 deletions

View File

@ -47,8 +47,7 @@
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Status", "label": "Status",
"options": "Initiated\nRinging\nIn Progress\nCompleted\nFailed\nBusy\nNo Answer\nQueued\nCanceled", "options": "Initiated\nRinging\nIn Progress\nCompleted\nFailed\nBusy\nNo Answer\nQueued\nCanceled"
"read_only": 1
}, },
{ {
"fieldname": "start_time", "fieldname": "start_time",
@ -83,8 +82,7 @@
"fieldname": "duration", "fieldname": "duration",
"fieldtype": "Duration", "fieldtype": "Duration",
"in_list_view": 1, "in_list_view": 1,
"label": "Duration", "label": "Duration"
"read_only": 1
}, },
{ {
"fieldname": "recording_url", "fieldname": "recording_url",
@ -145,7 +143,8 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Telephony Medium", "label": "Telephony Medium",
"options": "\nManual\nTwilio\nExotel" "options": "\nManual\nTwilio\nExotel",
"read_only": 1
}, },
{ {
"fieldname": "section_break_gyqe", "fieldname": "section_break_gyqe",
@ -154,7 +153,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-01-17 21:46:01.558377", "modified": "2025-01-22 17:57:59.289548",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "CRM Call Log", "name": "CRM Call Log",

View File

@ -97,7 +97,7 @@ class CRMCallLog(Document):
def parse_call_log(call): def parse_call_log(call):
call["show_recording"] = False call["show_recording"] = False
call["duration"] = seconds_to_duration(call.get("duration")) call["_duration"] = seconds_to_duration(call.get("duration"))
if call.get("type") == "Incoming": if call.get("type") == "Incoming":
call["activity_type"] = "incoming_call" call["activity_type"] = "incoming_call"
contact = get_contact_by_phone_number(call.get("from")) contact = get_contact_by_phone_number(call.get("from"))
@ -106,11 +106,11 @@ def parse_call_log(call):
if call.get("receiver") if call.get("receiver")
else [None, None] else [None, None]
) )
call["caller"] = { call["_caller"] = {
"label": contact.get("full_name", "Unknown"), "label": contact.get("full_name", "Unknown"),
"image": contact.get("image"), "image": contact.get("image"),
} }
call["receiver"] = { call["_receiver"] = {
"label": receiver[0], "label": receiver[0],
"image": receiver[1], "image": receiver[1],
} }
@ -122,11 +122,11 @@ def parse_call_log(call):
if call.get("caller") if call.get("caller")
else [None, None] else [None, None]
) )
call["caller"] = { call["_caller"] = {
"label": caller[0], "label": caller[0],
"image": caller[1], "image": caller[1],
} }
call["receiver"] = { call["_receiver"] = {
"label": contact.get("full_name", "Unknown"), "label": contact.get("full_name", "Unknown"),
"image": contact.get("image"), "image": contact.get("image"),
} }

View File

@ -138,6 +138,10 @@ def add_default_fields_layout(force=False):
"doctype": "Address", "doctype": "Address",
"layout": '[{"name": "details_section", "columns": [{"name": "column_uSSG", "fields": ["address_title", "address_type", "address_line1", "address_line2", "city", "state", "country", "pincode"]}]}]', "layout": '[{"name": "details_section", "columns": [{"name": "column_uSSG", "fields": ["address_title", "address_type", "address_line1", "address_line2", "city", "state", "country", "pincode"]}]}]',
}, },
"CRM Call Log-Quick Entry": {
"doctype": "CRM Call Log",
"layout": '[{"name":"details_section","columns":[{"name":"column_uMSG","fields":["type","from","duration"]},{"name":"column_wiZT","fields":["to","status","caller","receiver"]}]}]',
},
} }
sidebar_fields_layouts = { sidebar_fields_layouts = {

View File

@ -53,15 +53,17 @@ def add_note_to_call_log(call_sid, note):
_note = frappe.get_doc( _note = frappe.get_doc(
{ {
"doctype": "FCRM Note", "doctype": "FCRM Note",
"title": note.get("title", "Call Note"),
"content": note.get("content"), "content": note.get("content"),
} }
).insert(ignore_permissions=True) ).insert(ignore_permissions=True)
call_log = frappe.get_cached_doc("CRM Call Log", call_sid)
call_log.link_with_reference_doc("FCRM Note", _note.name)
call_log.save(ignore_permissions=True)
else: else:
_note = frappe.set_value("FCRM Note", note.get("name"), "content", note.get("content")) _note = frappe.set_value("FCRM Note", note.get("name"), "content", note.get("content"))
call_log = frappe.get_cached_doc("CRM Call Log", call_sid)
call_log.link_with_reference_doc("FCRM Note", _note.name)
call_log.save(ignore_permissions=True)
return _note return _note
@ -75,21 +77,30 @@ def add_task_to_call_log(call_sid, task):
"doctype": "CRM Task", "doctype": "CRM Task",
"title": task.get("title"), "title": task.get("title"),
"description": task.get("description"), "description": task.get("description"),
"assigned_to": task.get("assigned_to"),
"due_date": task.get("due_date"),
"status": task.get("status"),
"priority": task.get("priority"),
} }
).insert(ignore_permissions=True) ).insert(ignore_permissions=True)
call_log = frappe.get_doc("CRM Call Log", call_sid)
call_log.link_with_reference_doc("CRM Task", _task.name)
call_log.save(ignore_permissions=True)
else: else:
_task = frappe.get_doc("CRM Task", task.get("name")) _task = frappe.get_doc("CRM Task", task.get("name"))
_task.update( _task.update(
{ {
"title": task.get("title"), "title": task.get("title"),
"description": task.get("description"), "description": task.get("description"),
"assigned_to": task.get("assigned_to"),
"due_date": task.get("due_date"),
"status": task.get("status"),
"priority": task.get("priority"),
} }
) )
_task.save(ignore_permissions=True) _task.save(ignore_permissions=True)
call_log = frappe.get_doc("CRM Call Log", call_sid)
call_log.link_with_reference_doc("CRM Task", _task.name)
call_log.save(ignore_permissions=True)
return _task return _task

View File

@ -7,7 +7,7 @@ crm.patches.v1_0.rename_twilio_settings_to_crm_twilio_settings
[post_model_sync] [post_model_sync]
# Patches added in this section will be executed after doctypes are migrated # Patches added in this section will be executed after doctypes are migrated
crm.patches.v1_0.create_email_template_custom_fields crm.patches.v1_0.create_email_template_custom_fields
crm.patches.v1_0.create_default_fields_layout #10/12/2024 crm.patches.v1_0.create_default_fields_layout #22/01/2025
crm.patches.v1_0.create_default_sidebar_fields_layout crm.patches.v1_0.create_default_sidebar_fields_layout
crm.patches.v1_0.update_deal_quick_entry_layout crm.patches.v1_0.update_deal_quick_entry_layout
crm.patches.v1_0.update_layouts_to_new_format crm.patches.v1_0.update_layouts_to_new_format

View File

@ -1,5 +1,5 @@
from crm.install import add_default_fields_layout from crm.install import add_default_fields_layout
def execute(): def execute():
add_default_fields_layout() add_default_fields_layout()

@ -1 +1 @@
Subproject commit 863eaae9ada2edb287fc09fb21d05212bb5eebe9 Subproject commit aea806331c179cc0cdb89b6a2f9de2130bd1061f

View File

@ -1,14 +1,14 @@
<template> <template>
<div @click="showCallLogModal = true" class="cursor-pointer"> <div @click="showCallLogDetailModal = true" class="cursor-pointer">
<div class="mb-1 flex items-center justify-stretch gap-2 py-1 text-base"> <div class="mb-1 flex items-center justify-stretch gap-2 py-1 text-base">
<div class="inline-flex items-center flex-wrap gap-1 text-ink-gray-5"> <div class="inline-flex items-center flex-wrap gap-1 text-ink-gray-5">
<Avatar <Avatar
:image="activity.caller.image" :image="activity._caller.image"
:label="activity.caller.label" :label="activity._caller.label"
size="md" size="md"
/> />
<span class="font-medium text-ink-gray-8 ml-1"> <span class="font-medium text-ink-gray-8 ml-1">
{{ activity.caller.label }} {{ activity._caller.label }}
</span> </span>
<span>{{ <span>{{
activity.type == 'Incoming' activity.type == 'Incoming'
@ -41,14 +41,14 @@
<MultipleAvatar <MultipleAvatar
:avatars="[ :avatars="[
{ {
image: activity.caller.image, image: activity._caller.image,
label: activity.caller.label, label: activity._caller.label,
name: activity.caller.label, name: activity._caller.label,
}, },
{ {
image: activity.receiver.image, image: activity._receiver.image,
label: activity.receiver.label, label: activity._receiver.label,
name: activity.receiver.label, name: activity._receiver.label,
}, },
]" ]"
size="sm" size="sm"
@ -61,7 +61,10 @@
<CalendarIcon class="size-3" /> <CalendarIcon class="size-3" />
</template> </template>
</Badge> </Badge>
<Badge v-if="activity.status == 'Completed'" :label="activity.duration"> <Badge
v-if="activity.status == 'Completed'"
:label="activity._duration"
>
<template #prefix> <template #prefix>
<DurationIcon class="size-3" /> <DurationIcon class="size-3" />
</template> </template>
@ -89,7 +92,12 @@
<AudioPlayer :src="activity.recording_url" /> <AudioPlayer :src="activity.recording_url" />
</div> </div>
</div> </div>
<CallLogModal v-model="showCallLogModal" :name="callLogName" /> <CallLogDetailModal
v-model="showCallLogDetailModal"
v-model:callLogModal="showCallLogModal"
v-model:callLog="callLog"
/>
<CallLogModal v-model="showCallLogModal" v-model:callLog="callLog" />
</div> </div>
</template> </template>
<script setup> <script setup>
@ -98,16 +106,23 @@ import CalendarIcon from '@/components/Icons/CalendarIcon.vue'
import DurationIcon from '@/components/Icons/DurationIcon.vue' import DurationIcon from '@/components/Icons/DurationIcon.vue'
import MultipleAvatar from '@/components/MultipleAvatar.vue' import MultipleAvatar from '@/components/MultipleAvatar.vue'
import AudioPlayer from '@/components/Activities/AudioPlayer.vue' import AudioPlayer from '@/components/Activities/AudioPlayer.vue'
import CallLogDetailModal from '@/components/Modals/CallLogDetailModal.vue'
import CallLogModal from '@/components/Modals/CallLogModal.vue' import CallLogModal from '@/components/Modals/CallLogModal.vue'
import { statusLabelMap, statusColorMap } from '@/utils/callLog.js' import { statusLabelMap, statusColorMap } from '@/utils/callLog.js'
import { formatDate, timeAgo } from '@/utils' import { formatDate, timeAgo } from '@/utils'
import { Avatar, Badge, Tooltip } from 'frappe-ui' import { Avatar, Badge, Tooltip, createResource } from 'frappe-ui'
import { ref } from 'vue' import { ref } from 'vue'
const props = defineProps({ const props = defineProps({
activity: Object, activity: Object,
}) })
const callLogName = ref(props.activity.name) const callLog = createResource({
url: 'crm.fcrm.doctype.crm_call_log.crm_call_log.get_call_log',
params: { name: props.activity.name },
cache: ['call_log', props.activity.name],
auto: true,
})
const showCallLogDetailModal = ref(false)
const showCallLogModal = ref(false) const showCallLogModal = ref(false)
</script> </script>

View File

@ -0,0 +1,372 @@
<template>
<Dialog v-model="show">
<template #body>
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
<div class="mb-5 flex items-center justify-between">
<div>
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
{{ __('Call Details') }}
</h3>
</div>
<div class="flex items-center gap-1">
<Dropdown
:options="[
{
group: __('Options'),
hideLabel: true,
items: [
{
label: note?.name ? __('Edit note') : __('Add note'),
icon: NoteIcon,
onClick: addEditNote,
},
{
label: task?.name ? __('Edit task') : __('Add task'),
icon: TaskIcon,
onClick: addEditTask,
},
],
},
]"
>
<template #default>
<Button variant="ghost" icon="more-horizontal" />
</template>
</Dropdown>
<Button
v-if="isManager() && !isMobileView"
variant="ghost"
class="w-7"
@click="openCallLogModal"
>
<EditIcon class="h-4 w-4" />
</Button>
<Button variant="ghost" class="w-7" @click="show = false">
<FeatherIcon name="x" class="h-4 w-4" />
</Button>
</div>
</div>
<div class="flex flex-col gap-3.5">
<div
v-for="field in detailFields"
:key="field.name"
class="flex gap-2 text-base text-ink-gray-8"
>
<div class="grid size-7 place-content-center">
<component :is="field.icon" />
</div>
<div class="flex min-h-7 w-full items-center gap-2">
<div
v-if="field.name == 'receiver'"
class="flex items-center gap-1"
>
<Avatar
:image="field.value.caller.image"
:label="field.value.caller.label"
size="sm"
/>
<div class="ml-1 flex flex-col gap-1">
{{ field.value.caller.label }}
</div>
<FeatherIcon
name="arrow-right"
class="mx-1 h-4 w-4 text-ink-gray-5"
/>
<Avatar
:image="field.value.receiver.image"
:label="field.value.receiver.label"
size="sm"
/>
<div class="ml-1 flex flex-col gap-1">
{{ field.value.receiver.label }}
</div>
</div>
<Tooltip v-else-if="field.tooltip" :text="field.tooltip">
{{ field.value }}
</Tooltip>
<div class="w-full" v-else-if="field.name == 'recording_url'">
<audio
class="audio-control w-full"
controls
:src="field.value"
></audio>
</div>
<div
class="w-full cursor-pointer rounded border px-2 pt-1.5 text-base text-ink-gray-7"
v-else-if="field.name == 'note'"
@click="() => (showNoteModal = true)"
>
<FadedScrollableDiv class="max-h-24 min-h-16 overflow-y-auto">
<div
v-if="field.value?.title"
:class="[field.value?.content ? 'mb-1 font-bold' : '']"
v-html="field.value?.title"
/>
<div
v-if="field.value?.content"
v-html="field.value?.content"
/>
</FadedScrollableDiv>
</div>
<div
class="w-full cursor-pointer rounded border px-2 pt-1.5 text-base text-ink-gray-7"
v-else-if="field.name == 'task'"
@click="() => (showTaskModal = true)"
>
<FadedScrollableDiv class="max-h-24 min-h-16 overflow-y-auto">
<div
v-if="field.value?.title"
:class="[field.value?.description ? 'mb-1 font-bold' : '']"
v-html="field.value?.title"
/>
<div
v-if="field.value?.description"
v-html="field.value?.description"
/>
</FadedScrollableDiv>
</div>
<div v-else :class="field.color ? `text-${field.color}-600` : ''">
{{ field.value }}
</div>
<div v-if="field.link">
<ArrowUpRightIcon
class="h-4 w-4 shrink-0 cursor-pointer text-ink-gray-5 hover:text-ink-gray-8"
@click="() => field.link()"
/>
</div>
</div>
</div>
</div>
</div>
<div
v-if="!callLog?.data?._lead && !callLog?.data?._deal"
class="px-4 pb-7 pt-4 sm:px-6"
>
<Button
class="w-full"
variant="solid"
:label="__('Create lead')"
@click="createLead"
/>
</div>
</template>
</Dialog>
<NoteModal v-model="showNoteModal" :note="note" @after="addNoteToCallLog" />
<TaskModal v-model="showTaskModal" :task="task" @after="addTaskToCallLog" />
</template>
<script setup>
import EditIcon from '@/components/Icons/EditIcon.vue'
import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue'
import DurationIcon from '@/components/Icons/DurationIcon.vue'
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
import Dealsicon from '@/components/Icons/DealsIcon.vue'
import CalendarIcon from '@/components/Icons/CalendarIcon.vue'
import NoteIcon from '@/components/Icons/NoteIcon.vue'
import TaskIcon from '@/components/Icons/TaskIcon.vue'
import CheckCircleIcon from '@/components/Icons/CheckCircleIcon.vue'
import NoteModal from '@/components/Modals/NoteModal.vue'
import TaskModal from '@/components/Modals/TaskModal.vue'
import FadedScrollableDiv from '@/components/FadedScrollableDiv.vue'
import { getCallLogDetail } from '@/utils/callLog'
import { usersStore } from '@/stores/users'
import { isMobileView } from '@/composables/settings'
import { FeatherIcon, Dropdown, Avatar, Tooltip, call } from 'frappe-ui'
import { ref, computed, h, nextTick } from 'vue'
import { useRouter } from 'vue-router'
const { isManager } = usersStore()
const router = useRouter()
const show = defineModel()
const showNoteModal = ref(false)
const showTaskModal = ref(false)
const callLog = defineModel('callLog')
const note = ref({
title: '',
content: '',
})
const task = ref({
title: '',
description: '',
assigned_to: '',
due_date: '',
status: 'Backlog',
priority: 'Low',
})
const detailFields = computed(() => {
if (!callLog.value?.data) return []
let data = JSON.parse(JSON.stringify(callLog.value?.data))
for (const key in data) {
data[key] = getCallLogDetail(key, data)
}
note.value = data._notes?.[0] ?? null
task.value = data._tasks?.[0] ?? null
let details = [
{
icon: h(FeatherIcon, {
name: data.type.icon,
class: 'h-3.5 w-3.5',
}),
name: 'type',
value: data.type.label + ' Call',
},
{
icon: ContactsIcon,
name: 'receiver',
value: {
receiver: data.receiver,
caller: data.caller,
},
},
{
icon: data._lead ? LeadsIcon : Dealsicon,
name: 'reference_doc',
value: data._lead ? 'Lead' : 'Deal',
link: () => {
if (data._lead) {
router.push({
name: 'Lead',
params: { leadId: data._lead },
})
} else {
router.push({
name: 'Deal',
params: { dealId: data._deal },
})
}
},
condition: () => data._lead || data._deal,
},
{
icon: CalendarIcon,
name: 'creation',
value: data.creation.label,
tooltip: data.creation.label,
},
{
icon: DurationIcon,
name: 'duration',
value: data.duration.label,
},
{
icon: CheckCircleIcon,
name: 'status',
value: data.status.label,
color: data.status.color,
},
{
icon: h(FeatherIcon, {
name: 'play-circle',
class: 'h-4 w-4 mt-2',
}),
name: 'recording_url',
value: data.recording_url,
},
{
icon: NoteIcon,
name: 'note',
value: data._notes?.[0] ?? null,
},
{
icon: TaskIcon,
name: 'task',
value: data._tasks?.[0] ?? null,
},
]
return details
.filter((detail) => detail.value)
.filter((detail) => (detail.condition ? detail.condition() : true))
})
function createLead() {
call('crm.fcrm.doctype.crm_call_log.crm_call_log.create_lead_from_call_log', {
call_log: callLog.value?.data,
}).then((d) => {
if (d) {
router.push({ name: 'Lead', params: { leadId: d } })
}
})
}
const showCallLogModal = defineModel('callLogModal')
function openCallLogModal() {
showCallLogModal.value = true
nextTick(() => {
show.value = false
})
}
function addEditNote() {
if (!note.value?.name) {
note.value = {
title: '',
content: '',
}
}
showNoteModal.value = true
}
function addEditTask() {
if (!task.value?.name) {
task.value = {
title: '',
description: '',
assigned_to: '',
due_date: '',
status: 'Backlog',
priority: 'Low',
}
}
showTaskModal.value = true
}
async function addNoteToCallLog(_note, insert_mode = false) {
note.value = _note
if (insert_mode && _note.name) {
await call('crm.integrations.api.add_note_to_call_log', {
call_sid: callLog.value?.data?.id,
note: _note,
})
}
}
async function addTaskToCallLog(_task, insert_mode = false) {
task.value = _task
if (insert_mode && _task.name) {
await call('crm.integrations.api.add_task_to_call_log', {
call_sid: callLog.value?.data?.id,
task: _task,
})
}
}
</script>
<style scoped>
.audio-control {
height: 36px;
outline: none;
border-radius: 10px;
cursor: pointer;
background-color: rgb(237, 237, 237);
}
audio::-webkit-media-controls-panel {
background-color: rgb(237, 237, 237) !important;
}
.audio-control::-webkit-media-controls-panel {
background-color: white;
}
</style>

View File

@ -1,245 +1,212 @@
<template> <template>
<Dialog v-model="show"> <Dialog v-model="show" :options="dialogOptions">
<template #body-title> <template #body>
<div class="flex items-center gap-3"> <div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9"> <div class="mb-5 flex items-center justify-between">
{{ __('Call Details') }} <div>
</h3> <h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
{{ __(dialogOptions.title) || __('Untitled') }}
</h3>
</div>
<div class="flex items-center gap-1">
<Button
v-if="isManager() && !isMobileView"
variant="ghost"
class="w-7"
@click="openQuickEntryModal"
>
<EditIcon class="h-4 w-4" />
</Button>
<Button variant="ghost" class="w-7" @click="show = false">
<FeatherIcon name="x" class="h-4 w-4" />
</Button>
</div>
</div>
<div v-if="tabs.data">
<FieldLayout
:tabs="tabs.data"
:data="_callLog"
doctype="CRM Call Log"
/>
<ErrorMessage class="mt-2" :message="error" />
</div>
</div> </div>
</template> <div class="px-4 pb-7 pt-4 sm:px-6">
<template #body-content> <div class="space-y-2">
<div class="flex flex-col gap-3.5"> <Button
<div class="w-full"
v-for="field in detailFields" v-for="action in dialogOptions.actions"
:key="field.name" :key="action.label"
class="flex gap-2 text-base text-ink-gray-8" v-bind="action"
> :label="__(action.label)"
<div class="grid size-7 place-content-center"> :loading="loading"
<component :is="field.icon" /> />
</div>
<div class="flex min-h-7 w-full items-center gap-2">
<div
v-if="field.name == 'receiver'"
class="flex items-center gap-1"
>
<Avatar
:image="field.value.caller.image"
:label="field.value.caller.label"
size="sm"
/>
<div class="ml-1 flex flex-col gap-1">
{{ field.value.caller.label }}
</div>
<FeatherIcon
name="arrow-right"
class="mx-1 h-4 w-4 text-ink-gray-5"
/>
<Avatar
:image="field.value.receiver.image"
:label="field.value.receiver.label"
size="sm"
/>
<div class="ml-1 flex flex-col gap-1">
{{ field.value.receiver.label }}
</div>
</div>
<Tooltip v-else-if="field.tooltip" :text="field.tooltip">
{{ field.value }}
</Tooltip>
<div class="w-full" v-else-if="field.name == 'recording_url'">
<audio
class="audio-control w-full"
controls
:src="field.value"
></audio>
</div>
<div
class="w-full cursor-pointer rounded border px-2 pt-1.5 text-base text-ink-gray-7"
v-else-if="field.name == 'note'"
@click="() => (showNoteModal = true)"
>
<FadedScrollableDiv class="max-h-24 min-h-16 overflow-y-auto">
<div
v-if="field.value?.title"
:class="[field.value?.content ? 'mb-1 font-bold' : '']"
v-html="field.value?.title"
/>
<div
v-if="field.value?.content"
v-html="field.value?.content"
/>
</FadedScrollableDiv>
</div>
<div v-else :class="field.color ? `text-${field.color}-600` : ''">
{{ field.value }}
</div>
<div v-if="field.link">
<ArrowUpRightIcon
class="h-4 w-4 shrink-0 cursor-pointer text-ink-gray-5 hover:text-ink-gray-8"
@click="() => field.link()"
/>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<template v-if="!callLog.data?._lead && !callLog.data?._deal" #actions>
<Button
class="w-full"
variant="solid"
:label="__('Create lead')"
@click="createLead"
/>
</template>
</Dialog> </Dialog>
<NoteModal v-model="showNoteModal" :note="callLog.value?.data?._notes?.[0]" /> <QuickEntryModal
v-if="showQuickEntryModal"
v-model="showQuickEntryModal"
doctype="CRM Call Log"
/>
</template> </template>
<script setup> <script setup>
import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue' import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
import DurationIcon from '@/components/Icons/DurationIcon.vue' import FieldLayout from '@/components/FieldLayout/FieldLayout.vue'
import ContactsIcon from '@/components/Icons/ContactsIcon.vue' import EditIcon from '@/components/Icons/EditIcon.vue'
import LeadsIcon from '@/components/Icons/LeadsIcon.vue' import { usersStore } from '@/stores/users'
import Dealsicon from '@/components/Icons/DealsIcon.vue' import { isMobileView } from '@/composables/settings'
import CalendarIcon from '@/components/Icons/CalendarIcon.vue' import { getRandom } from '@/utils'
import NoteIcon from '@/components/Icons/NoteIcon.vue' import { capture } from '@/telemetry'
import CheckCircleIcon from '@/components/Icons/CheckCircleIcon.vue' import { FeatherIcon, createResource, ErrorMessage } from 'frappe-ui'
import NoteModal from '@/components/Modals/NoteModal.vue' import { ref, nextTick, watch, computed } from 'vue'
import FadedScrollableDiv from '@/components/FadedScrollableDiv.vue'
import { FeatherIcon, Avatar, Tooltip, call, createResource } from 'frappe-ui'
import { getCallLogDetail } from '@/utils/callLog'
import { ref, computed, h, watch } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({ const props = defineProps({
name: { options: {
type: String, type: Object,
default: {}, default: {
afterInsert: () => {},
},
}, },
}) })
const show = defineModel() const { isManager } = usersStore()
const showNoteModal = ref(false)
const router = useRouter()
const callLog = ref({})
const detailFields = computed(() => { const show = defineModel()
if (!callLog.value.data) return [] const callLog = defineModel('callLog')
let details = [
const loading = ref(false)
const error = ref(null)
const title = ref(null)
const editMode = ref(false)
let _callLog = ref({
name: '',
type: '',
from: '',
to: '',
medium: '',
duration: '',
caller: '',
receiver: '',
status: '',
recording_url: '',
telephony_medium: 'Manual',
})
const dialogOptions = computed(() => {
let title = !editMode.value ? __('New Call Log') : __('Edit Call Log')
let size = 'xl'
let actions = [
{ {
icon: h(FeatherIcon, { label: editMode.value ? __('Save') : __('Create'),
name: callLog.value.data.type.icon, variant: 'solid',
class: 'h-3.5 w-3.5', onClick: () =>
}), editMode.value ? updateCallLog() : createCallLog.submit(),
name: 'type',
value: callLog.value.data.type.label + ' Call',
},
{
icon: ContactsIcon,
name: 'receiver',
value: {
receiver: callLog.value.data.receiver,
caller: callLog.value.data.caller,
},
},
{
icon: callLog.value.data._lead ? LeadsIcon : Dealsicon,
name: 'reference_doc',
value: callLog.value.data._lead ? 'Lead' : 'Deal',
link: () => {
if (callLog.value.data._lead) {
router.push({
name: 'Lead',
params: { leadId: callLog.value.data._lead },
})
} else {
router.push({
name: 'Deal',
params: { dealId: callLog.value.data._deal },
})
}
},
condition: () => callLog.value.data._lead || callLog.value.data._deal,
},
{
icon: CalendarIcon,
name: 'creation',
value: callLog.value.data.creation.label,
tooltip: callLog.value.data.creation.label,
},
{
icon: DurationIcon,
name: 'duration',
value: callLog.value.data.duration.label,
},
{
icon: CheckCircleIcon,
name: 'status',
value: callLog.value.data.status.label,
color: callLog.value.data.status.color,
},
{
icon: h(FeatherIcon, {
name: 'play-circle',
class: 'h-4 w-4 mt-2',
}),
name: 'recording_url',
value: callLog.value.data.recording_url,
},
{
icon: NoteIcon,
name: 'note',
value: callLog.value.data._notes?.[0] ?? null,
}, },
] ]
return details return { title, size, actions }
.filter((detail) => detail.value)
.filter((detail) => (detail.condition ? detail.condition() : true))
}) })
function createLead() { const tabs = createResource({
call('crm.fcrm.doctype.crm_call_log.crm_call_log.create_lead_from_call_log', { url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
call_log: callLog.value.data, cache: ['QuickEntry', 'CRM Call Log'],
}).then((d) => { params: { doctype: 'CRM Call Log', type: 'Quick Entry' },
if (d) { auto: true,
router.push({ name: 'Lead', params: { leadId: d } }) })
}
let doc = ref({})
function updateCallLog() {
error.value = null
const old = { ...doc.value }
const newCallLog = { ..._callLog.value }
const dirty = JSON.stringify(old) !== JSON.stringify(newCallLog)
if (!dirty) {
show.value = false
return
}
loading.value = true
updateCallLogValues.submit({
doctype: 'CRM Call Log',
name: _callLog.value.name,
fieldname: newCallLog,
}) })
} }
watch(show, (val) => { const updateCallLogValues = createResource({
if (val) { url: 'frappe.client.set_value',
callLog.value = createResource({ onSuccess(doc) {
url: 'crm.fcrm.doctype.crm_call_log.crm_call_log.get_call_log', loading.value = false
params: { name: props.name }, if (doc.name) {
cache: ['call_log', props.name], handleCallLogUpdate(doc)
auto: true, }
transform: (doc) => { },
for (const key in doc) { onError(err) {
doc[key] = getCallLogDetail(key, doc) loading.value = false
} error.value = err
return doc },
},
})
}
}) })
const createCallLog = createResource({
url: 'frappe.client.insert',
makeParams() {
return {
doc: {
doctype: 'CRM Call Log',
id: getRandom(6),
telephony_medium: 'Manual',
..._callLog.value,
},
}
},
onSuccess(doc) {
loading.value = false
if (doc.name) {
capture('call_log_created')
handleCallLogUpdate(doc)
}
},
onError(err) {
loading.value = false
error.value = err
},
})
function handleCallLogUpdate(doc) {
show.value = false
props.options.afterInsert && props.options.afterInsert(doc)
}
watch(
() => show.value,
(value) => {
if (!value) return
editMode.value = false
nextTick(() => {
// TODO: Issue with FormControl
// title.value.el.focus()
doc.value = callLog.value?.data || {}
_callLog.value = { ...doc.value }
if (_callLog.value.name) {
editMode.value = true
}
})
},
)
const showQuickEntryModal = ref(false)
function openQuickEntryModal() {
showQuickEntryModal.value = true
nextTick(() => {
show.value = false
})
}
</script> </script>
<style scoped>
.audio-control {
height: 36px;
outline: none;
border-radius: 10px;
cursor: pointer;
background-color: rgb(237, 237, 237);
}
audio::-webkit-media-controls-panel {
background-color: rgb(237, 237, 237) !important;
}
.audio-control::-webkit-media-controls-panel {
background-color: white;
}
</style>

View File

@ -187,7 +187,8 @@ async function updateTask() {
fieldname: _task.value, fieldname: _task.value,
}) })
if (d.name) { if (d.name) {
tasks.value.reload() tasks.value?.reload()
emit('after', d)
} }
} else { } else {
let d = await call('frappe.client.insert', { let d = await call('frappe.client.insert', {
@ -200,8 +201,8 @@ async function updateTask() {
}) })
if (d.name) { if (d.name) {
capture('task_created') capture('task_created')
tasks.value.reload() tasks.value?.reload()
emit('after') emit('after', d, true)
} }
} }
show.value = false show.value = false

View File

@ -101,7 +101,10 @@
:label="data[field.fieldname]" :label="data[field.fieldname]"
class="dropdown-button flex w-full items-center justify-between rounded border border-gray-100 bg-surface-gray-2 px-2 py-1.5 text-base text-ink-gray-8 placeholder-ink-gray-4 transition-colors hover:border-outline-gray-modals hover:bg-surface-gray-3 focus:border-outline-gray-4 focus:bg-surface-white focus:shadow-sm focus:outline-none focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3" class="dropdown-button flex w-full items-center justify-between rounded border border-gray-100 bg-surface-gray-2 px-2 py-1.5 text-base text-ink-gray-8 placeholder-ink-gray-4 transition-colors hover:border-outline-gray-modals hover:bg-surface-gray-3 focus:border-outline-gray-4 focus:bg-surface-white focus:shadow-sm focus:outline-none focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3"
> >
<div v-if="data[field.fieldname]" class="truncate"> <div
v-if="data[field.fieldname]"
class="truncate"
>
{{ data[field.fieldname] }} {{ data[field.fieldname] }}
</div> </div>
<div <div

View File

@ -8,6 +8,9 @@
v-if="callLogsListView?.customListActions" v-if="callLogsListView?.customListActions"
:actions="callLogsListView.customListActions" :actions="callLogsListView.customListActions"
/> />
<Button variant="solid" :label="__('Create')" @click="createCallLog">
<template #prefix><FeatherIcon name="plus" class="h-4" /></template>
</Button>
</template> </template>
</LayoutHeader> </LayoutHeader>
<ViewControls <ViewControls
@ -50,7 +53,16 @@
<span>{{ __('No {0} Found', [__('Logs')]) }}</span> <span>{{ __('No {0} Found', [__('Logs')]) }}</span>
</div> </div>
</div> </div>
<CallLogModal v-model="showCallLogModal" :name="selectedCallLog" /> <CallLogDetailModal
v-model="showCallLogDetailModal"
v-model:callLogModal="showCallLogModal"
v-model:callLog="callLog"
/>
<CallLogModal
v-model="showCallLogModal"
v-model:callLog="callLog"
:options="{ afterInsert: () => callLogs.reload() }"
/>
</template> </template>
<script setup> <script setup>
@ -60,11 +72,14 @@ import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import LayoutHeader from '@/components/LayoutHeader.vue' import LayoutHeader from '@/components/LayoutHeader.vue'
import ViewControls from '@/components/ViewControls.vue' import ViewControls from '@/components/ViewControls.vue'
import CallLogsListView from '@/components/ListViews/CallLogsListView.vue' import CallLogsListView from '@/components/ListViews/CallLogsListView.vue'
import CallLogDetailModal from '@/components/Modals/CallLogDetailModal.vue'
import CallLogModal from '@/components/Modals/CallLogModal.vue' import CallLogModal from '@/components/Modals/CallLogModal.vue'
import { getCallLogDetail } from '@/utils/callLog' import { getCallLogDetail } from '@/utils/callLog'
import { createResource } from 'frappe-ui'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
const callLogsListView = ref(null) const callLogsListView = ref(null)
const showCallLogModal = ref(false)
// callLogs data is loaded in the ViewControls component // callLogs data is loaded in the ViewControls component
const callLogs = ref({}) const callLogs = ref({})
@ -88,11 +103,21 @@ const rows = computed(() => {
}) })
}) })
const showCallLogModal = ref(false) const showCallLogDetailModal = ref(false)
const selectedCallLog = ref(null) const callLog = ref({})
function showCallLog(name) { function showCallLog(name) {
selectedCallLog.value = name showCallLogDetailModal.value = true
callLog.value = createResource({
url: 'crm.fcrm.doctype.crm_call_log.crm_call_log.get_call_log',
params: { name },
cache: ['call_log', name],
auto: true,
})
}
function createCallLog() {
callLog.value = {}
showCallLogModal.value = true showCallLogModal.value = true
} }
</script> </script>

View File

@ -227,9 +227,7 @@ const _address = ref({})
const contact = createResource({ const contact = createResource({
url: 'crm.api.contact.get_contact', url: 'crm.api.contact.get_contact',
cache: ['contact', props.contactId], cache: ['contact', props.contactId],
params: { params: { name: props.contactId },
name: props.contactId,
},
auto: true, auto: true,
transform: (data) => { transform: (data) => {
return { return {
@ -340,7 +338,7 @@ const sections = createResource({
cache: ['sidePanelSections', 'Contact'], cache: ['sidePanelSections', 'Contact'],
params: { doctype: 'Contact' }, params: { doctype: 'Contact' },
auto: true, auto: true,
transform: (data) => getParsedSections(data), transform: (data) => computed(() => getParsedSections(data)),
}) })
function getParsedSections(_sections) { function getParsedSections(_sections) {

View File

@ -320,9 +320,7 @@ const tabs = [
const deals = createResource({ const deals = createResource({
url: 'crm.api.contact.get_linked_deals', url: 'crm.api.contact.get_linked_deals',
cache: ['deals', props.contactId], cache: ['deals', props.contactId],
params: { params: { contact: props.contactId },
contact: props.contactId,
},
auto: true, auto: true,
}) })
@ -337,7 +335,7 @@ const sections = createResource({
cache: ['sidePanelSections', 'Contact'], cache: ['sidePanelSections', 'Contact'],
params: { doctype: 'Contact' }, params: { doctype: 'Contact' },
auto: true, auto: true,
transform: (data) => getParsedSections(data), transform: (data) => computed(() => getParsedSections(data)),
}) })
function getParsedSections(_sections) { function getParsedSections(_sections) {

View File

@ -9,9 +9,19 @@ export function getCallLogDetail(row, log, columns = []) {
if (row === 'duration') { if (row === 'duration') {
return { return {
label: log.duration, label: log._duration,
icon: 'clock', icon: 'clock',
} }
} else if (row === 'caller') {
return {
label: log._caller?.label,
image: log._caller?.image,
}
} else if (row === 'receiver') {
return {
label: log._receiver?.label,
image: log._receiver?.image,
}
} else if (row === 'type') { } else if (row === 'type') {
return { return {
label: log.type, label: log.type,