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

View File

@ -97,7 +97,7 @@ class CRMCallLog(Document):
def parse_call_log(call):
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":
call["activity_type"] = "incoming_call"
contact = get_contact_by_phone_number(call.get("from"))
@ -106,11 +106,11 @@ def parse_call_log(call):
if call.get("receiver")
else [None, None]
)
call["caller"] = {
call["_caller"] = {
"label": contact.get("full_name", "Unknown"),
"image": contact.get("image"),
}
call["receiver"] = {
call["_receiver"] = {
"label": receiver[0],
"image": receiver[1],
}
@ -122,11 +122,11 @@ def parse_call_log(call):
if call.get("caller")
else [None, None]
)
call["caller"] = {
call["_caller"] = {
"label": caller[0],
"image": caller[1],
}
call["receiver"] = {
call["_receiver"] = {
"label": contact.get("full_name", "Unknown"),
"image": contact.get("image"),
}

View File

@ -138,6 +138,10 @@ def add_default_fields_layout(force=False):
"doctype": "Address",
"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 = {

View File

@ -53,15 +53,17 @@ def add_note_to_call_log(call_sid, note):
_note = frappe.get_doc(
{
"doctype": "FCRM Note",
"title": note.get("title", "Call Note"),
"content": note.get("content"),
}
).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:
_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
@ -75,21 +77,30 @@ def add_task_to_call_log(call_sid, task):
"doctype": "CRM Task",
"title": task.get("title"),
"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)
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:
_task = frappe.get_doc("CRM Task", task.get("name"))
_task.update(
{
"title": task.get("title"),
"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)
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

View File

@ -7,7 +7,7 @@ crm.patches.v1_0.rename_twilio_settings_to_crm_twilio_settings
[post_model_sync]
# 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_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.update_deal_quick_entry_layout
crm.patches.v1_0.update_layouts_to_new_format

View File

@ -1,5 +1,5 @@
from crm.install import add_default_fields_layout
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>
<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="inline-flex items-center flex-wrap gap-1 text-ink-gray-5">
<Avatar
:image="activity.caller.image"
:label="activity.caller.label"
:image="activity._caller.image"
:label="activity._caller.label"
size="md"
/>
<span class="font-medium text-ink-gray-8 ml-1">
{{ activity.caller.label }}
{{ activity._caller.label }}
</span>
<span>{{
activity.type == 'Incoming'
@ -41,14 +41,14 @@
<MultipleAvatar
:avatars="[
{
image: activity.caller.image,
label: activity.caller.label,
name: activity.caller.label,
image: activity._caller.image,
label: activity._caller.label,
name: activity._caller.label,
},
{
image: activity.receiver.image,
label: activity.receiver.label,
name: activity.receiver.label,
image: activity._receiver.image,
label: activity._receiver.label,
name: activity._receiver.label,
},
]"
size="sm"
@ -61,7 +61,10 @@
<CalendarIcon class="size-3" />
</template>
</Badge>
<Badge v-if="activity.status == 'Completed'" :label="activity.duration">
<Badge
v-if="activity.status == 'Completed'"
:label="activity._duration"
>
<template #prefix>
<DurationIcon class="size-3" />
</template>
@ -89,7 +92,12 @@
<AudioPlayer :src="activity.recording_url" />
</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>
</template>
<script setup>
@ -98,16 +106,23 @@ import CalendarIcon from '@/components/Icons/CalendarIcon.vue'
import DurationIcon from '@/components/Icons/DurationIcon.vue'
import MultipleAvatar from '@/components/MultipleAvatar.vue'
import AudioPlayer from '@/components/Activities/AudioPlayer.vue'
import CallLogDetailModal from '@/components/Modals/CallLogDetailModal.vue'
import CallLogModal from '@/components/Modals/CallLogModal.vue'
import { statusLabelMap, statusColorMap } from '@/utils/callLog.js'
import { formatDate, timeAgo } from '@/utils'
import { Avatar, Badge, Tooltip } from 'frappe-ui'
import { Avatar, Badge, Tooltip, createResource } from 'frappe-ui'
import { ref } from 'vue'
const props = defineProps({
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)
</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>
<Dialog v-model="show">
<template #body-title>
<div class="flex items-center gap-3">
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
{{ __('Call Details') }}
</h3>
<Dialog v-model="show" :options="dialogOptions">
<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">
{{ __(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>
</template>
<template #body-content>
<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 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 class="px-4 pb-7 pt-4 sm:px-6">
<div class="space-y-2">
<Button
class="w-full"
v-for="action in dialogOptions.actions"
:key="action.label"
v-bind="action"
:label="__(action.label)"
:loading="loading"
/>
</div>
</div>
</template>
<template v-if="!callLog.data?._lead && !callLog.data?._deal" #actions>
<Button
class="w-full"
variant="solid"
:label="__('Create lead')"
@click="createLead"
/>
</template>
</Dialog>
<NoteModal v-model="showNoteModal" :note="callLog.value?.data?._notes?.[0]" />
<QuickEntryModal
v-if="showQuickEntryModal"
v-model="showQuickEntryModal"
doctype="CRM Call Log"
/>
</template>
<script setup>
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 CheckCircleIcon from '@/components/Icons/CheckCircleIcon.vue'
import NoteModal from '@/components/Modals/NoteModal.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'
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
import FieldLayout from '@/components/FieldLayout/FieldLayout.vue'
import EditIcon from '@/components/Icons/EditIcon.vue'
import { usersStore } from '@/stores/users'
import { isMobileView } from '@/composables/settings'
import { getRandom } from '@/utils'
import { capture } from '@/telemetry'
import { FeatherIcon, createResource, ErrorMessage } from 'frappe-ui'
import { ref, nextTick, watch, computed } from 'vue'
const props = defineProps({
name: {
type: String,
default: {},
options: {
type: Object,
default: {
afterInsert: () => {},
},
},
})
const show = defineModel()
const showNoteModal = ref(false)
const router = useRouter()
const callLog = ref({})
const { isManager } = usersStore()
const detailFields = computed(() => {
if (!callLog.value.data) return []
let details = [
const show = defineModel()
const callLog = defineModel('callLog')
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, {
name: callLog.value.data.type.icon,
class: 'h-3.5 w-3.5',
}),
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,
label: editMode.value ? __('Save') : __('Create'),
variant: 'solid',
onClick: () =>
editMode.value ? updateCallLog() : createCallLog.submit(),
},
]
return details
.filter((detail) => detail.value)
.filter((detail) => (detail.condition ? detail.condition() : true))
return { title, size, actions }
})
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 tabs = createResource({
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
cache: ['QuickEntry', 'CRM Call Log'],
params: { doctype: 'CRM Call Log', type: 'Quick Entry' },
auto: true,
})
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) => {
if (val) {
callLog.value = createResource({
url: 'crm.fcrm.doctype.crm_call_log.crm_call_log.get_call_log',
params: { name: props.name },
cache: ['call_log', props.name],
auto: true,
transform: (doc) => {
for (const key in doc) {
doc[key] = getCallLogDetail(key, doc)
}
return doc
},
})
}
const updateCallLogValues = createResource({
url: 'frappe.client.set_value',
onSuccess(doc) {
loading.value = false
if (doc.name) {
handleCallLogUpdate(doc)
}
},
onError(err) {
loading.value = false
error.value = err
},
})
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>
<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,
})
if (d.name) {
tasks.value.reload()
tasks.value?.reload()
emit('after', d)
}
} else {
let d = await call('frappe.client.insert', {
@ -200,8 +201,8 @@ async function updateTask() {
})
if (d.name) {
capture('task_created')
tasks.value.reload()
emit('after')
tasks.value?.reload()
emit('after', d, true)
}
}
show.value = false

View File

@ -101,7 +101,10 @@
: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"
>
<div v-if="data[field.fieldname]" class="truncate">
<div
v-if="data[field.fieldname]"
class="truncate"
>
{{ data[field.fieldname] }}
</div>
<div

View File

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

View File

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

View File

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

View File

@ -9,9 +9,19 @@ export function getCallLogDetail(row, log, columns = []) {
if (row === 'duration') {
return {
label: log.duration,
label: log._duration,
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') {
return {
label: log.type,