fix: rendered versions on activity tab
This commit is contained in:
parent
762f487a5f
commit
d3a2d8faba
98
crm/crm/doctype/crm_lead/api.py
Normal file
98
crm/crm/doctype/crm_lead/api.py
Normal file
@ -0,0 +1,98 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.form.load import get_docinfo
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_lead(name):
|
||||
Lead = frappe.qb.DocType("CRM Lead")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(Lead)
|
||||
.select("*")
|
||||
.where(Lead.name == name)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
lead = query.run(as_dict=True)
|
||||
if not len(lead):
|
||||
frappe.throw(_("Lead not found"), frappe.DoesNotExistError)
|
||||
lead = lead.pop()
|
||||
|
||||
get_docinfo('', "CRM Lead", name)
|
||||
docinfo = frappe.response["docinfo"]
|
||||
activities = get_activities(lead, docinfo)
|
||||
|
||||
return { **lead, 'activities': activities }
|
||||
|
||||
def get_activities(doc, docinfo):
|
||||
activities = [{
|
||||
"activity_type": "creation",
|
||||
"creation": doc.creation,
|
||||
"owner": doc.owner,
|
||||
"data": "created this lead",
|
||||
}]
|
||||
|
||||
for version in docinfo.versions:
|
||||
data = json.loads(version.data)
|
||||
if change := data.get("changed")[0]:
|
||||
activity_type = "changed"
|
||||
data = {
|
||||
"field": change[0],
|
||||
"old_value": change[1],
|
||||
"value": change[2],
|
||||
}
|
||||
if not change[1] and not change[2]:
|
||||
continue
|
||||
if not change[1] and change[2]:
|
||||
activity_type = "added"
|
||||
data = {
|
||||
"field": change[0],
|
||||
"value": change[2],
|
||||
}
|
||||
elif change[1] and not change[2]:
|
||||
activity_type = "removed"
|
||||
data = {
|
||||
"field": change[0],
|
||||
"value": change[1],
|
||||
}
|
||||
|
||||
activity = {
|
||||
"activity_type": activity_type,
|
||||
"creation": version.creation,
|
||||
"owner": version.owner,
|
||||
"data": data,
|
||||
}
|
||||
activities.append(activity)
|
||||
|
||||
for comment in docinfo.comments:
|
||||
activity = {
|
||||
"activity_type": "comment",
|
||||
"creation": comment.creation,
|
||||
"owner": comment.owner,
|
||||
"data": comment.content,
|
||||
}
|
||||
activities.append(activity)
|
||||
|
||||
for communication in docinfo.communications:
|
||||
activity = {
|
||||
"activity_type": "communication",
|
||||
"creation": communication.creation,
|
||||
"data": {
|
||||
"subject": communication.subject,
|
||||
"content": communication.content,
|
||||
"sender_full_name": communication.sender_full_name,
|
||||
"sender": communication.sender,
|
||||
"recipients": communication.recipients,
|
||||
"cc": communication.cc,
|
||||
"bcc": communication.bcc,
|
||||
"read_by_recipient": communication.read_by_recipient,
|
||||
},
|
||||
}
|
||||
activities.append(activity)
|
||||
|
||||
activities.sort(key=lambda x: x["creation"], reverse=True)
|
||||
|
||||
return activities
|
||||
@ -1,15 +1,127 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div v-for="activity in activities">
|
||||
<div>{{ activity.value }}</div>
|
||||
<div class="p-5 flex items-center justify-between font-medium text-lg">
|
||||
<div>{{ title }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-for="(activity, i) in activities">
|
||||
<div class="grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-5">
|
||||
<div
|
||||
class="relative flex justify-center"
|
||||
:class="{
|
||||
'after:absolute after:border-l after:border-gray-300 after:top-0 after:left-[50%] after:h-full after:-z-10':
|
||||
i != activities.length - 1,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center rounded-full outline outline-4 outline-white w-6 h-6 bg-gray-200 z-10"
|
||||
>
|
||||
<FeatherIcon
|
||||
:name="timelineIcon(activity.activity_type)"
|
||||
class="w-3.5 h-3.5 text-gray-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 pb-6">
|
||||
<div
|
||||
class="flex items-start justify-stretch gap-2 text-base leading-6"
|
||||
>
|
||||
<Avatar
|
||||
:image="getUser(activity.owner).user_image"
|
||||
:label="getUser(activity.owner).full_name"
|
||||
size="md"
|
||||
/>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<div>{{ getUser(activity.owner).full_name }}</div>
|
||||
<div
|
||||
v-if="activity.activity_type == 'creation'"
|
||||
class="text-gray-600"
|
||||
>
|
||||
{{ activity.data }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="activity.activity_type == 'added'"
|
||||
class="inline-flex gap-1 text-gray-600"
|
||||
>
|
||||
<span>added</span>
|
||||
<span class="text-gray-900 truncate max-w-xs">
|
||||
{{ activity.data.field }}
|
||||
</span>
|
||||
<span>value as</span>
|
||||
<span class="text-gray-900 truncate max-w-xs">
|
||||
{{ activity.data.value }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="activity.activity_type == 'removed'"
|
||||
class="inline-flex gap-1 text-gray-600"
|
||||
>
|
||||
<span>removed</span>
|
||||
<span class="text-gray-900 truncate max-w-xs">
|
||||
{{ activity.data.field }}
|
||||
</span>
|
||||
<span>value</span>
|
||||
<span class="text-gray-900 truncate max-w-xs">
|
||||
{{ activity.data.value }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="activity.activity_type == 'changed'"
|
||||
class="inline-flex gap-1 text-gray-600"
|
||||
>
|
||||
<span>changed</span>
|
||||
<span class="text-gray-900 truncate max-w-xs">
|
||||
{{ activity.data.field }}
|
||||
</span>
|
||||
<span>value from</span>
|
||||
<span class="text-gray-900 truncate max-w-xs">
|
||||
{{ activity.data.old_value }}
|
||||
</span>
|
||||
<span>to</span>
|
||||
<span class="text-gray-900 truncate max-w-xs">
|
||||
{{ activity.data.value }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto whitespace-nowrap">
|
||||
<Tooltip
|
||||
:text="dateFormat(activity.creation, dateTooltipFormat)"
|
||||
class="text-sm text-gray-600 leading-6"
|
||||
>
|
||||
{{ timeAgo(activity.creation) }}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Avatar, FeatherIcon, Tooltip, Button } from 'frappe-ui'
|
||||
import { timeAgo, dateFormat, dateTooltipFormat } from '@/utils'
|
||||
import { usersStore } from '@/stores/users'
|
||||
|
||||
const { getUser } = usersStore()
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Activity',
|
||||
},
|
||||
activities: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
})
|
||||
|
||||
function timelineIcon(activity_type) {
|
||||
if (activity_type == 'creation') {
|
||||
return 'plus'
|
||||
} else if (activity_type == 'removed') {
|
||||
return 'trash-2'
|
||||
}
|
||||
return 'edit'
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<LayoutHeader v-if="lead.doc">
|
||||
<LayoutHeader v-if="lead.data">
|
||||
<template #left-header>
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
</template>
|
||||
<template #right-header>
|
||||
<Autocomplete
|
||||
:options="activeAgents"
|
||||
:value="getUser(lead.doc.lead_owner).full_name"
|
||||
@change="(option) => (lead.doc.lead_owner = option.email)"
|
||||
:value="getUser(lead.data.lead_owner).full_name"
|
||||
@change="(option) => (lead.data.lead_owner = option.email)"
|
||||
placeholder="Lead owner"
|
||||
>
|
||||
<template #prefix>
|
||||
<Avatar
|
||||
class="mr-2"
|
||||
:image="getUser(lead.doc.lead_owner).user_image"
|
||||
:label="getUser(lead.doc.lead_owner).full_name"
|
||||
:image="getUser(lead.data.lead_owner).user_image"
|
||||
:label="getUser(lead.data.lead_owner).full_name"
|
||||
size="sm"
|
||||
/>
|
||||
</template>
|
||||
@ -29,9 +29,9 @@
|
||||
</Autocomplete>
|
||||
<Dropdown :options="statusDropdownOptions">
|
||||
<template #default="{ open }">
|
||||
<Button :label="lead.doc.status">
|
||||
<Button :label="lead.data.status">
|
||||
<template #prefix>
|
||||
<IndicatorIcon :class="indicatorColor[lead.doc.status]" />
|
||||
<IndicatorIcon :class="indicatorColor[lead.data.status]" />
|
||||
</template>
|
||||
<template #suffix
|
||||
><FeatherIcon
|
||||
@ -44,7 +44,7 @@
|
||||
<Button icon="more-horizontal" />
|
||||
</template>
|
||||
</LayoutHeader>
|
||||
<TabGroup v-if="lead.doc" @change="onTabChange">
|
||||
<TabGroup v-if="lead.data" @change="onTabChange">
|
||||
<TabList class="flex items-center gap-6 border-b pl-5 relative">
|
||||
<Tab
|
||||
ref="tabRef"
|
||||
@ -73,7 +73,7 @@
|
||||
v-for="tab in tabs"
|
||||
:key="tab.label"
|
||||
>
|
||||
<Activities :activities="tab.content" />
|
||||
<Activities :title="tab.activityTitle" :activities="tab.content" />
|
||||
</TabPanel>
|
||||
<div
|
||||
class="flex flex-col justify-between border-l w-[390px] overflow-hidden"
|
||||
@ -115,33 +115,33 @@
|
||||
v-if="field.type === 'select'"
|
||||
type="select"
|
||||
:options="field.options"
|
||||
v-model="lead.doc[field.name]"
|
||||
v-model="lead.data[field.name]"
|
||||
>
|
||||
<template #prefix>
|
||||
<IndicatorIcon
|
||||
:class="indicatorColor[lead.doc[field.name]]"
|
||||
:class="indicatorColor[lead.data[field.name]]"
|
||||
/>
|
||||
</template>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
v-else-if="field.type === 'email'"
|
||||
type="email"
|
||||
v-model="lead.doc[field.name]"
|
||||
v-model="lead.data[field.name]"
|
||||
/>
|
||||
<Autocomplete
|
||||
v-else-if="field.type === 'link'"
|
||||
:options="activeAgents"
|
||||
:value="getUser(lead.doc[field.name]).full_name"
|
||||
:value="getUser(lead.data[field.name]).full_name"
|
||||
@change="
|
||||
(option) => (lead.doc[field.name] = option.email)
|
||||
(option) => (lead.data[field.name] = option.email)
|
||||
"
|
||||
placeholder="Lead owner"
|
||||
>
|
||||
<template #prefix>
|
||||
<Avatar
|
||||
class="mr-2"
|
||||
:image="getUser(lead.doc[field.name]).user_image"
|
||||
:label="getUser(lead.doc[field.name]).full_name"
|
||||
:image="getUser(lead.data[field.name]).user_image"
|
||||
:label="getUser(lead.data[field.name]).full_name"
|
||||
size="sm"
|
||||
/>
|
||||
</template>
|
||||
@ -161,16 +161,16 @@
|
||||
>
|
||||
<template #default="{ open }">
|
||||
<Button
|
||||
:label="lead.doc[field.name]"
|
||||
:label="lead.data[field.name]"
|
||||
class="justify-between w-full"
|
||||
>
|
||||
<template #prefix>
|
||||
<IndicatorIcon
|
||||
:class="indicatorColor[lead.doc[field.name]]"
|
||||
:class="indicatorColor[lead.data[field.name]]"
|
||||
/>
|
||||
</template>
|
||||
<template #default>{{
|
||||
lead.doc[field.name]
|
||||
lead.data[field.name]
|
||||
}}</template>
|
||||
<template #suffix
|
||||
><FeatherIcon
|
||||
@ -183,7 +183,7 @@
|
||||
<FormControl
|
||||
v-else
|
||||
type="text"
|
||||
v-model="lead.doc[field.name]"
|
||||
v-model="lead.data[field.name]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -196,13 +196,13 @@
|
||||
class="flex items-center gap-1 text-sm px-6 p-3 leading-5 cursor-pointer"
|
||||
>
|
||||
<span class="text-gray-600">Created </span>
|
||||
<Tooltip :text="dateFormat(lead.doc.creation, dateTooltipFormat)">
|
||||
{{ timeAgo(lead.doc.creation) }}
|
||||
<Tooltip :text="dateFormat(lead.data.creation, dateTooltipFormat)">
|
||||
{{ timeAgo(lead.data.creation) }}
|
||||
</Tooltip>
|
||||
<span> · </span>
|
||||
<span class="text-gray-600">Updated </span>
|
||||
<Tooltip :text="dateFormat(lead.doc.modified, dateTooltipFormat)">
|
||||
{{ timeAgo(lead.doc.modified) }}
|
||||
<Tooltip :text="dateFormat(lead.data.modified, dateTooltipFormat)">
|
||||
{{ timeAgo(lead.data.modified) }}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@ -221,7 +221,7 @@ import Toggler from '@/components/Toggler.vue'
|
||||
import Activities from '@/components/Activities.vue'
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
|
||||
import {
|
||||
createDocumentResource,
|
||||
createResource,
|
||||
Avatar,
|
||||
FeatherIcon,
|
||||
Autocomplete,
|
||||
@ -231,7 +231,7 @@ import {
|
||||
} from 'frappe-ui'
|
||||
import { TransitionPresets, useTransition } from '@vueuse/core'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { dateFormat, timeAgo } from '@/utils'
|
||||
import { dateFormat, timeAgo, dateTooltipFormat } from '@/utils'
|
||||
import { ref, computed, h } from 'vue'
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.vue'
|
||||
|
||||
@ -243,74 +243,61 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
const lead = createDocumentResource({
|
||||
doctype: 'CRM Lead',
|
||||
name: props.leadId,
|
||||
|
||||
const lead = createResource({
|
||||
url: 'crm.crm.doctype.crm_lead.api.get_lead',
|
||||
params: { name: props.leadId },
|
||||
auto: true,
|
||||
onSuccess: (response) => {
|
||||
console.log(response)
|
||||
},
|
||||
})
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
let items = [{ label: 'Leads', route: { name: 'Leads' } }]
|
||||
items.push({
|
||||
label: lead.doc.lead_name,
|
||||
route: { name: 'Lead', params: { leadId: lead.doc.name } },
|
||||
label: lead.data.lead_name,
|
||||
route: { name: 'Lead', params: { leadId: lead.data.name } },
|
||||
})
|
||||
return items
|
||||
})
|
||||
|
||||
const activities = [
|
||||
{
|
||||
type: 'change',
|
||||
datetime: '2021-08-20 12:00:00',
|
||||
value: 'Status changed from New to Contact made',
|
||||
},
|
||||
{
|
||||
type: 'change',
|
||||
datetime: '2021-08-20 12:00:00',
|
||||
value: 'Status changed from Proposal made to New',
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
datetime: '2021-08-20 12:00:00',
|
||||
value: 'Email sent to Sharon',
|
||||
},
|
||||
{
|
||||
type: 'change',
|
||||
datetime: '2021-08-20 12:00:00',
|
||||
value: 'Status changed from Contact made to Proposal made',
|
||||
},
|
||||
{
|
||||
type: 'call',
|
||||
datetime: '2021-08-20 12:00:00',
|
||||
value: 'Call made to Sharon',
|
||||
},
|
||||
]
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: 'Activity',
|
||||
icon: ActivityIcon,
|
||||
content: activities,
|
||||
},
|
||||
{
|
||||
label: 'Emails',
|
||||
icon: EmailIcon,
|
||||
content: activities.filter((activity) => activity.type === 'email'),
|
||||
},
|
||||
{
|
||||
label: 'Calls',
|
||||
icon: PhoneIcon,
|
||||
content: activities.filter((activity) => activity.type === 'call'),
|
||||
},
|
||||
{
|
||||
label: 'Tasks',
|
||||
icon: TaskIcon,
|
||||
},
|
||||
{
|
||||
label: 'Notes',
|
||||
icon: NoteIcon,
|
||||
},
|
||||
]
|
||||
const tabs = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Activity',
|
||||
icon: ActivityIcon,
|
||||
content: lead.data.activities,
|
||||
activityTitle: 'Activity log'
|
||||
},
|
||||
{
|
||||
label: 'Emails',
|
||||
icon: EmailIcon,
|
||||
content: lead.data.activities.filter(
|
||||
(activity) => activity.activity_type === 'communication'
|
||||
),
|
||||
activityTitle: 'Emails'
|
||||
},
|
||||
{
|
||||
label: 'Calls',
|
||||
icon: PhoneIcon,
|
||||
content: lead.data.activities.filter(
|
||||
(activity) => activity.activity_type === 'call'
|
||||
),
|
||||
activityTitle: 'Calls'
|
||||
},
|
||||
{
|
||||
label: 'Tasks',
|
||||
icon: TaskIcon,
|
||||
activityTitle: 'Tasks',
|
||||
},
|
||||
{
|
||||
label: 'Notes',
|
||||
icon: NoteIcon,
|
||||
activityTitle: 'Notes',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const tabRef = ref([])
|
||||
const indicator = ref(null)
|
||||
@ -332,35 +319,35 @@ const statusDropdownOptions = [
|
||||
label: 'New',
|
||||
icon: () => h(IndicatorIcon, { class: '!text-gray-600' }),
|
||||
onClick: () => {
|
||||
lead.doc.status = 'New'
|
||||
lead.data.status = 'New'
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Contact made',
|
||||
icon: () => h(IndicatorIcon, { class: 'text-orange-600' }),
|
||||
onClick: () => {
|
||||
lead.doc.status = 'Contact made'
|
||||
lead.data.status = 'Contact made'
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Proposal made',
|
||||
icon: () => h(IndicatorIcon, { class: '!text-blue-600' }),
|
||||
onClick: () => {
|
||||
lead.doc.status = 'Proposal made'
|
||||
lead.data.status = 'Proposal made'
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Negotiation',
|
||||
icon: () => h(IndicatorIcon, { class: 'text-red-600' }),
|
||||
onClick: () => {
|
||||
lead.doc.status = 'Negotiation'
|
||||
lead.data.status = 'Negotiation'
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Converted',
|
||||
icon: () => h(IndicatorIcon, { class: 'text-green-600' }),
|
||||
onClick: () => {
|
||||
lead.doc.status = 'Converted'
|
||||
lead.data.status = 'Converted'
|
||||
},
|
||||
},
|
||||
]
|
||||
@ -398,7 +385,7 @@ const detailSections = computed(() => {
|
||||
{
|
||||
label: 'Website',
|
||||
type: 'data',
|
||||
name: 'organization_website',
|
||||
name: 'website',
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -439,6 +426,4 @@ const activeAgents = computed(() => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const dateTooltipFormat = 'dddd, MMMM D, YYYY h:mm A'
|
||||
</script>
|
||||
|
||||
@ -8,3 +8,5 @@ export function dateFormat(date, format) {
|
||||
export function timeAgo(date) {
|
||||
return useTimeAgo(date).value
|
||||
}
|
||||
|
||||
export const dateTooltipFormat = 'ddd, MMM D, YYYY h:mm A'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user