1
0
forked from test/crm

fix: added more fields in lead and deal page & listview

This commit is contained in:
Shariq Ansari 2023-08-26 12:01:08 +05:30
parent 66420a1444
commit 1154bce89c
6 changed files with 263 additions and 87 deletions

View File

@ -39,6 +39,8 @@ def get_activities(doc, docinfo):
for version in docinfo.versions:
data = json.loads(version.data)
if not data.get("changed"):
continue
if change := data.get("changed")[0]:
activity_type = "changed"
field_label = next((f.label for f in lead_fields_meta if f.fieldname == change[0]), None)

View File

@ -19,9 +19,14 @@
</Autocomplete>
<Dropdown :options="statusDropdownOptions(deal.data, 'deal')">
<template #default="{ open }">
<Button :label="deal.data.deal_status">
<Button
:label="deal.data.deal_status"
:class="dealStatuses[deal.data.deal_status].bgColor"
>
<template #prefix>
<IndicatorIcon :class="dealStatuses[deal.data.deal_status].color" />
<IndicatorIcon
:class="dealStatuses[deal.data.deal_status].color"
/>
</template>
<template #suffix
><FeatherIcon
@ -31,7 +36,6 @@
</Button>
</template>
</Dropdown>
<Button icon="more-horizontal" />
<Button label="Save" variant="solid" @click="() => updateDeal()" />
</template>
</LayoutHeader>
@ -80,10 +84,11 @@
>
<Avatar
size="3xl"
:label="deal.data.first_name"
:image="deal.data.image"
shape="square"
:label="deal.data.organization_name"
:image="deal.data.organization_logo"
/>
<div class="font-medium text-2xl">{{ deal.data.lead_name }}</div>
<div class="font-medium text-2xl">{{ deal.data.organization_name }}</div>
<div class="flex gap-3">
<Tooltip text="Make a call...">
<Button
@ -96,7 +101,13 @@
<Button class="rounded-full h-8 w-8">
<EmailIcon class="h-4 w-4" />
</Button>
<Button icon="message-square" class="rounded-full h-8 w-8" />
<Tooltip text="Go to website...">
<Button
icon="link"
@click="openWebsite(deal.data.website)"
class="rounded-full h-8 w-8"
/>
</Tooltip>
<Button icon="more-horizontal" class="rounded-full h-8 w-8" />
</div>
</div>
@ -218,6 +229,24 @@
</Button>
</template>
</Dropdown>
<FormControl
v-else-if="field.type === 'date'"
type="date"
v-model="deal.data[field.name]"
class="form-control"
/>
<FormControl
v-else-if="field.type === 'number'"
type="number"
v-model="deal.data[field.name]"
class="form-control"
/>
<FormControl
v-else-if="field.type === 'tel'"
type="tel"
v-model="deal.data[field.name]"
class="form-control"
/>
<FormControl
v-else
type="text"
@ -264,7 +293,14 @@ import UserAvatar from '@/components/UserAvatar.vue'
import CommunicationArea from '@/components/CommunicationArea.vue'
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
import { TransitionPresets, useTransition } from '@vueuse/core'
import { dateFormat, timeAgo, dateTooltipFormat, dealStatuses, statusDropdownOptions } from '@/utils'
import {
dateFormat,
timeAgo,
dateTooltipFormat,
dealStatuses,
statusDropdownOptions,
openWebsite,
} from '@/utils'
import { usersStore } from '@/stores/users'
import {
createResource,
@ -315,7 +351,7 @@ function updateDeal() {
const breadcrumbs = computed(() => {
let items = [{ label: 'Deals', route: { name: 'Deals' } }]
items.push({
label: deal.data.lead_name,
label: deal.data.organization_name,
route: { name: 'Deal', params: { dealId: deal.data.name } },
})
return items
@ -345,11 +381,11 @@ const tabs = computed(() => {
),
activityTitle: 'Calls',
},
{
label: 'Tasks',
icon: TaskIcon,
activityTitle: 'Tasks',
},
// {
// label: 'Tasks',
// icon: TaskIcon,
// activityTitle: 'Tasks',
// },
{
label: 'Notes',
icon: NoteIcon,
@ -376,20 +412,9 @@ function onTabChange(index) {
const detailSections = computed(() => {
return [
{
label: 'About this deal',
label: 'Organization',
opened: true,
fields: [
{
label: 'Status',
type: 'select',
name: 'deal_status',
options: statusDropdownOptions(deal.data, 'deal'),
},
{
label: 'Deal owner',
type: 'link',
name: 'lead_owner',
},
{
label: 'Organization',
type: 'data',
@ -400,10 +425,30 @@ const detailSections = computed(() => {
type: 'data',
name: 'website',
},
{
label: 'Amount',
type: 'number',
name: 'annual_revenue',
},
{
label: 'Close date',
type: 'date',
name: 'close_date',
},
{
label: 'Probability',
type: 'data',
name: 'probability',
},
{
label: 'Next step',
type: 'data',
name: 'next_step',
}
],
},
{
label: 'Person',
label: 'Contacts',
opened: true,
fields: [
{
@ -423,7 +468,7 @@ const detailSections = computed(() => {
},
{
label: 'Mobile no.',
type: 'phone',
type: 'tel',
name: 'mobile_no',
},
],

View File

@ -102,11 +102,9 @@ const leads = createListResource({
doctype: 'CRM Lead',
fields: [
'name',
'first_name',
'lead_name',
'image',
'organization_name',
'organization_logo',
'annual_revenue',
'deal_status',
'email',
'mobile_no',
@ -140,23 +138,23 @@ watch(
)
const columns = [
{
label: 'Name',
key: 'lead_name',
type: 'avatar',
size: 'w-44',
},
{
label: 'Organization',
key: 'organization_name',
type: 'logo',
size: 'w-44',
size: 'w-48',
},
{
label: 'Amount',
key: 'annual_revenue',
type: 'data',
size: 'w-24',
},
{
label: 'Status',
key: 'deal_status',
type: 'indicator',
size: 'w-44',
size: 'w-36',
},
{
label: 'Email',
@ -168,13 +166,19 @@ const columns = [
label: 'Mobile no',
key: 'mobile_no',
type: 'phone',
size: 'w-44',
size: 'w-32',
},
{
label: 'Lead owner',
key: 'lead_owner',
type: 'avatar',
size: 'w-44',
size: 'w-36',
},
{
label: 'Last modified',
key: 'modified',
type: 'pretty_date',
size: 'w-28',
},
]
@ -182,15 +186,11 @@ const rows = computed(() => {
return leads.data?.map((lead) => {
return {
name: lead.name,
lead_name: {
label: lead.lead_name,
image: lead.image,
image_label: lead.first_name,
},
organization_name: {
label: lead.organization_name,
logo: lead.organization_logo,
},
annual_revenue: lead.annual_revenue,
deal_status: {
label: lead.deal_status,
color: dealStatuses[lead.deal_status]?.color,
@ -198,6 +198,7 @@ const rows = computed(() => {
email: lead.email,
mobile_no: lead.mobile_no,
lead_owner: lead.lead_owner && getUser(lead.lead_owner),
modified: lead.modified,
}
})
})

View File

@ -19,7 +19,10 @@
</Autocomplete>
<Dropdown :options="statusDropdownOptions(lead.data)">
<template #default="{ open }">
<Button :label="lead.data.status">
<Button
:label="lead.data.status"
:class="leadStatuses[lead.data.status].bgColor"
>
<template #prefix>
<IndicatorIcon :class="leadStatuses[lead.data.status].color" />
</template>
@ -31,8 +34,8 @@
</Button>
</template>
</Dropdown>
<Button icon="more-horizontal" />
<Button label="Save" variant="solid" @click="() => updateLead()" />
<Button label="Save" variant="solid" @click="updateLead()" />
<Button label="Convert to deal" variant="solid" @click="convertToDeal()" />
</template>
</LayoutHeader>
<TabGroup v-slot="{ selectedIndex }" v-if="lead.data" @change="onTabChange">
@ -96,7 +99,13 @@
<Button class="rounded-full h-8 w-8">
<EmailIcon class="h-4 w-4" />
</Button>
<Button icon="message-square" class="rounded-full h-8 w-8" />
<Tooltip text="Go to website...">
<Button
icon="link"
@click="openWebsite(lead.data.website)"
class="rounded-full h-8 w-8"
/>
</Tooltip>
<Button icon="more-horizontal" class="rounded-full h-8 w-8" />
</div>
</div>
@ -158,6 +167,14 @@
/>
<Autocomplete
v-else-if="field.type === 'link'"
:value="lead.data[field.name]"
:options="field.options"
@change="(e) => field.change(e)"
:placeholder="field.placeholder"
class="form-control"
/>
<Autocomplete
v-else-if="field.type === 'user'"
:options="activeAgents"
:value="getUser(lead.data[field.name]).full_name"
@change="
@ -264,7 +281,14 @@ import UserAvatar from '@/components/UserAvatar.vue'
import CommunicationArea from '@/components/CommunicationArea.vue'
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
import { TransitionPresets, useTransition } from '@vueuse/core'
import { dateFormat, timeAgo, dateTooltipFormat, leadStatuses, statusDropdownOptions } from '@/utils'
import {
dateFormat,
timeAgo,
dateTooltipFormat,
leadStatuses,
statusDropdownOptions,
openWebsite,
} from '@/utils'
import { usersStore } from '@/stores/users'
import {
createResource,
@ -277,8 +301,10 @@ import {
Avatar,
} from 'frappe-ui'
import { ref, computed, inject } from 'vue'
import { useRouter } from 'vue-router'
const { getUser, users } = usersStore()
const router = useRouter()
const makeCall = inject('makeOutgoingCall')
@ -345,11 +371,11 @@ const tabs = computed(() => {
),
activityTitle: 'Calls',
},
{
label: 'Tasks',
icon: TaskIcon,
activityTitle: 'Tasks',
},
// {
// label: 'Tasks',
// icon: TaskIcon,
// activityTitle: 'Tasks',
// },
{
label: 'Notes',
icon: NoteIcon,
@ -379,17 +405,6 @@ const detailSections = computed(() => {
label: 'About this lead',
opened: true,
fields: [
{
label: 'Status',
type: 'select',
name: 'status',
options: statusDropdownOptions(lead.data),
},
{
label: 'Lead owner',
type: 'link',
name: 'lead_owner',
},
{
label: 'Organization',
type: 'data',
@ -400,12 +415,66 @@ const detailSections = computed(() => {
type: 'data',
name: 'website',
},
{
label: 'Job title',
type: 'data',
name: 'job_title',
},
{
label: 'Source',
type: 'link',
name: 'source',
placeholder: 'Select source...',
options: [
{ label: 'Advertisement', value: 'Advertisement' },
{ label: 'Web', value: 'Web' },
{ label: 'Others', value: 'Others' },
],
change: (data) => {
lead.data.source = data.value
},
},
{
label: 'Industry',
type: 'link',
name: 'industry',
placeholder: 'Select industry...',
options: [
{ label: 'Advertising', value: 'Advertising' },
{ label: 'Agriculture', value: 'Agriculture' },
{ label: 'Banking', value: 'Banking' },
{ label: 'Others', value: 'Others' },
],
change: (data) => {
lead.data.industry = data.value
},
},
],
},
{
label: 'Person',
opened: true,
fields: [
{
label: 'Salutation',
type: 'link',
name: 'salutation',
placeholder: 'Mr./Mrs./Ms.',
options: [
{ label: 'Dr', value: 'Dr' },
{ label: 'Mr', value: 'Mr' },
{ label: 'Mrs', value: 'Mrs' },
{ label: 'Ms', value: 'Ms' },
{ label: 'Mx', value: 'Mx' },
{ label: 'Prof', value: 'Prof' },
{ label: 'Master', value: 'Master' },
{ label: 'Madam', value: 'Madam' },
{ label: 'Miss', value: 'Miss' },
],
change: (data) => {
lead.data.salutation = data.value
},
},
{
label: 'First name',
type: 'data',
@ -444,6 +513,13 @@ const activeAgents = computed(() => {
}
})
})
function convertToDeal() {
lead.data.status = 'Qualified'
lead.data.is_deal = 1
updateLead()
router.push({ name: 'Deal', params: { dealId: lead.data.name } })
}
</script>
<style scoped>
@ -453,4 +529,8 @@ const activeAgents = computed(() => {
border-color: transparent;
background: white;
}
:deep(.form-control button svg) {
color: white;
}
</style>

View File

@ -96,6 +96,10 @@ function getFilter() {
}
}
function getSortBy() {
return getOrderBy() || 'modified desc'
}
const leads = createListResource({
type: 'list',
doctype: 'CRM Lead',
@ -113,7 +117,7 @@ const leads = createListResource({
'modified',
],
filters: getFilter(),
orderBy: 'modified desc',
orderBy: getSortBy(),
pageLength: 20,
auto: true,
})
@ -122,7 +126,7 @@ watch(
() => getOrderBy(),
(value, old_value) => {
if (!value && !old_value) return
leads.orderBy = getOrderBy() || 'modified desc'
leads.orderBy = getSortBy()
leads.reload()
},
{ immediate: true }
@ -143,37 +147,43 @@ const columns = [
label: 'Name',
key: 'lead_name',
type: 'avatar',
size: 'w-44',
size: 'w-48',
},
{
label: 'Organization',
key: 'organization_name',
type: 'logo',
size: 'w-44',
size: 'w-40',
},
{
label: 'Status',
key: 'status',
type: 'indicator',
size: 'w-44',
size: 'w-36',
},
{
label: 'Email',
key: 'email',
type: 'email',
size: 'w-44',
size: 'w-40',
},
{
label: 'Mobile no',
key: 'mobile_no',
type: 'phone',
size: 'w-44',
size: 'w-32',
},
{
label: 'Lead owner',
key: 'lead_owner',
type: 'avatar',
size: 'w-44',
size: 'w-36',
},
{
label: 'Last modified',
key: 'modified',
type: 'pretty_date',
size: 'w-28',
},
]
@ -197,6 +207,7 @@ const rows = computed(() => {
email: lead.email,
mobile_no: lead.mobile_no,
lead_owner: lead.lead_owner && getUser(lead.lead_owner),
modified: lead.modified,
}
})
})

View File

@ -14,25 +14,58 @@ export function timeAgo(date) {
export const dateTooltipFormat = 'ddd, MMM D, YYYY h:mm A'
export const leadStatuses = {
Open: { label: 'Open', color: '!text-gray-600' },
Contacted: { label: 'Contacted', color: '!text-orange-600' },
Nurture: { label: 'Nurture', color: '!text-blue-600' },
Qualified: { label: 'Qualified', color: '!text-green-600' },
Unqualified: { label: 'Unqualified', color: '!text-red-600' },
Junk: { label: 'Junk', color: '!text-purple-600' },
Open: { label: 'Open', color: '!text-gray-600', bgColor: '!bg-gray-200' },
Contacted: {
label: 'Contacted',
color: '!text-orange-600',
bgColor: '!bg-orange-200',
},
Nurture: {
label: 'Nurture',
color: '!text-blue-600',
bgColor: '!bg-blue-200',
},
Qualified: {
label: 'Qualified',
color: '!text-green-600',
bgColor: '!bg-green-200',
},
Unqualified: {
label: 'Unqualified',
color: '!text-red-600',
bgColor: '!bg-red-200',
},
Junk: { label: 'Junk', color: '!text-purple-600', bgColor: '!bg-purple-200' },
}
export const dealStatuses = {
Qualification: { label: 'Qualification', color: '!text-gray-600' },
'Demo/Making': { label: 'Demo/Making', color: '!text-orange-600' },
Qualification: {
label: 'Qualification',
color: '!text-gray-600',
bgColor: '!bg-gray-200',
},
'Demo/Making': {
label: 'Demo/Making',
color: '!text-orange-600',
bgColor: '!bg-orange-200',
},
'Proposal/Quotation': {
label: 'Proposal/Quotation',
color: '!text-blue-600',
bgColor: '!bg-blue-200',
},
Negotiation: { label: 'Negotiation', color: '!text-yellow-600' },
'Ready to Close': { label: 'Ready to Close', color: '!text-purple-600' },
Won: { label: 'Won', color: '!text-green-600' },
Lost: { label: 'Lost', color: '!text-red-600' },
Negotiation: {
label: 'Negotiation',
color: '!text-yellow-600',
bgColor: '!bg-yellow-100',
},
'Ready to Close': {
label: 'Ready to Close',
color: '!text-purple-600',
bgColor: '!bg-purple-200',
},
Won: { label: 'Won', color: '!text-green-600', bgColor: '!bg-green-200' },
Lost: { label: 'Lost', color: '!text-red-600', bgColor: '!bg-red-200' },
}
export function statusDropdownOptions(data, doctype) {
@ -56,3 +89,7 @@ export function statusDropdownOptions(data, doctype) {
}
return options
}
export function openWebsite(url) {
window.open(url, '_blank')
}