1
0
forked from test/crm

fix: rendered versions on activity tab

This commit is contained in:
Shariq Ansari 2023-08-03 18:37:13 +05:30
parent 762f487a5f
commit d3a2d8faba
4 changed files with 292 additions and 95 deletions

View 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

View File

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

View File

@ -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>&nbsp;&middot;&nbsp;</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>

View File

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