Merge pull request #23 from shariquerik/make-contact-organization-link-field

refactor: convert organization field to a link field
This commit is contained in:
Shariq Ansari 2023-11-06 18:14:33 +05:30 committed by GitHub
commit 6ad550d986
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 470 additions and 286 deletions

View File

@ -29,9 +29,8 @@
"next_step",
"organization_tab",
"section_break_uixv",
"organization_name",
"organization",
"no_of_employees",
"organization_logo",
"column_break_dbsv",
"website",
"job_title",
@ -111,10 +110,12 @@
"search_index": 1
},
{
"fetch_from": "organization.website",
"fieldname": "website",
"fieldtype": "Data",
"label": "Website",
"options": "URL"
"options": "URL",
"read_only": 1
},
{
"fieldname": "column_break_sijm",
@ -146,17 +147,15 @@
"fieldname": "no_of_employees",
"fieldtype": "Select",
"label": "No. of Employees",
"options": "1-10\n11-50\n51-200\n201-500\n501-1000\n1000+"
},
{
"fieldname": "organization_name",
"fieldtype": "Data",
"label": "Organization Name"
"options": "1-10\n11-50\n51-200\n201-500\n501-1000\n1000+",
"read_only": 1
},
{
"fetch_from": "organization.annual_revenue",
"fieldname": "annual_revenue",
"fieldtype": "Currency",
"label": "Annual Revenue"
"label": "Annual Revenue",
"read_only": 1
},
{
"fieldname": "lead_owner",
@ -171,15 +170,11 @@
"options": "CRM Lead Source"
},
{
"fetch_from": "organization.industry",
"fieldname": "industry",
"fieldtype": "Link",
"fieldtype": "Data",
"label": "Industry",
"options": "CRM Industry"
},
{
"fieldname": "organization_logo",
"fieldtype": "Attach Image",
"label": "Organization Logo"
"read_only": 1
},
{
"fieldname": "image",
@ -214,9 +209,11 @@
"search_index": 1
},
{
"fetch_from": "organization.job_title",
"fieldname": "job_title",
"fieldtype": "Data",
"label": "Job Title"
"label": "Job Title",
"read_only": 1
},
{
"fieldname": "organization_tab",
@ -259,12 +256,18 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Created as Deal"
},
{
"fieldname": "organization",
"fieldtype": "Link",
"label": "Organization",
"options": "CRM Organization"
}
],
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-09-27 18:54:18.196159",
"modified": "2023-11-06 15:29:56.868755",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Lead",

View File

@ -26,15 +26,17 @@ class CRMLead(Document):
def set_lead_name(self):
if not self.lead_name:
# Check for leads being created through data import
if not self.organization_name and not self.email and not self.flags.ignore_mandatory:
if not self.organization and not self.email and not self.flags.ignore_mandatory:
frappe.throw(_("A Lead requires either a person's name or an organization's name"))
elif self.organization_name:
self.lead_name = self.organization_name
else:
elif self.organization:
self.lead_name = self.organization
elif self.email:
self.lead_name = self.email.split("@")[0]
else:
self.lead_name = "Unnamed Lead"
def set_title(self):
self.title = self.organization_name or self.lead_name
self.title = self.organization or self.lead_name
def validate_email(self):
if self.email:
@ -106,7 +108,7 @@ class CRMLead(Document):
"salutation": self.salutation,
"gender": self.gender,
"designation": self.job_title,
"company_name": self.organization_name,
"company_name": self.organization,
"image": self.image or "",
}
)
@ -132,7 +134,7 @@ class CRMLead(Document):
{ "label": 'Modified', "value": 'modified' },
{ "label": 'Status', "value": 'status' },
{ "label": 'Lead owner', "value": 'lead_owner' },
{ "label": 'Organization', "value": 'organization_name' },
{ "label": 'Organization', "value": 'organization' },
{ "label": 'Name', "value": 'lead_name' },
{ "label": 'First Name', "value": 'first_name' },
{ "label": 'Last Name', "value": 'last_name' },

View File

@ -7,8 +7,13 @@
"engine": "InnoDB",
"field_order": [
"organization_name",
"no_of_employees",
"organization_logo",
"column_break_pnpp",
"website",
"organization_logo"
"job_title",
"annual_revenue",
"industry"
],
"fields": [
{
@ -20,18 +25,45 @@
{
"fieldname": "website",
"fieldtype": "Data",
"label": "Website"
"label": "Website",
"options": "URL"
},
{
"fieldname": "organization_logo",
"fieldtype": "Attach Image",
"label": "Organization Logo"
},
{
"fieldname": "no_of_employees",
"fieldtype": "Select",
"label": "No. of Employees",
"options": "1-10\n11-50\n51-200\n201-500\n501-1000\n1000+"
},
{
"fieldname": "column_break_pnpp",
"fieldtype": "Column Break"
},
{
"fieldname": "job_title",
"fieldtype": "Data",
"label": "Job Title"
},
{
"fieldname": "annual_revenue",
"fieldtype": "Currency",
"label": "Annual Revenue"
},
{
"fieldname": "industry",
"fieldtype": "Link",
"label": "Industry",
"options": "CRM Industry"
}
],
"image_field": "organization_logo",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-11-03 16:25:25.366741",
"modified": "2023-11-06 15:28:26.610882",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Organization",

View File

@ -0,0 +1,16 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5.86813 10.1317C5.67286 9.93647 5.67286 9.61988 5.86813 9.42462L12.7926 2.5001L9 2.5001C8.72386 2.5001 8.5 2.27624 8.5 2.0001C8.5 1.72396 8.72386 1.5001 9 1.5001L13.9899 1.5001C14.1076 1.49777 14.2261 1.53678 14.3215 1.61714C14.4306 1.70886 14.5 1.84638 14.5 2.0001V7.0001C14.5 7.27624 14.2761 7.5001 14 7.5001C13.7239 7.5001 13.5 7.27624 13.5 7.0001V3.20696L6.57523 10.1317C6.37997 10.327 6.06339 10.327 5.86813 10.1317ZM2.5 4.0001C2.5 3.17167 3.17157 2.5001 4 2.5001H5.8C6.07614 2.5001 6.3 2.27624 6.3 2.0001C6.3 1.72396 6.07614 1.5001 5.8 1.5001H4C2.61929 1.5001 1.5 2.61939 1.5 4.0001V12.0001C1.5 13.3808 2.61929 14.5001 4 14.5001H12C13.3807 14.5001 14.5 13.3808 14.5 12.0001V10.2001C14.5 9.92396 14.2761 9.7001 14 9.7001C13.7239 9.7001 13.5 9.92396 13.5 10.2001V12.0001C13.5 12.8285 12.8284 13.5001 12 13.5001H4C3.17157 13.5001 2.5 12.8285 2.5 12.0001V4.0001Z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -21,7 +21,7 @@
<div v-if="column.key === 'deal_status'">
<IndicatorIcon :class="item.color" />
</div>
<div v-else-if="column.key === 'organization_name'">
<div v-else-if="column.key === 'organization'">
<Avatar
v-if="item.label"
class="flex items-center"

View File

@ -30,7 +30,7 @@
size="sm"
/>
</div>
<div v-else-if="column.key === 'organization_name'">
<div v-else-if="column.key === 'organization'">
<Avatar
v-if="item.label"
class="flex items-center"

View File

@ -3,7 +3,7 @@
<div v-for="section in allFields" :key="section.section">
<div class="grid grid-cols-3 gap-4">
<div v-for="field in section.fields" :key="field.name">
<div class="text-gray-600 text-sm mb-2">{{ field.label }}</div>
<div class="mb-2 text-sm text-gray-600">{{ field.label }}</div>
<FormControl
v-if="field.type === 'select'"
type="select"
@ -19,8 +19,18 @@
type="email"
v-model="newDeal[field.name]"
/>
<Autocomplete
<FormControl
v-else-if="field.type === 'link'"
type="autocomplete"
:value="newDeal[field.name]"
:options="field.options"
@change="(e) => field.change(e)"
:placeholder="field.placeholder"
class="form-control"
/>
<FormControl
v-else-if="field.type === 'user'"
type="autocomplete"
:options="activeAgents"
:value="getUser(newDeal[field.name]).full_name"
@change="(option) => (newDeal[field.name] = option.email)"
@ -32,7 +42,7 @@
<template #item-prefix="{ option }">
<UserAvatar class="mr-2" :user="option.email" size="sm" />
</template>
</Autocomplete>
</FormControl>
<Dropdown
v-else-if="field.type === 'dropdown'"
:options="statusDropdownOptions(newDeal, 'deal')"
@ -41,10 +51,12 @@
<template #default="{ open }">
<Button
:label="newDeal[field.name]"
class="justify-between w-full"
class="w-full justify-between"
>
<template #prefix>
<IndicatorIcon :class="dealStatuses[newDeal[field.name]].color" />
<IndicatorIcon
:class="dealStatuses[newDeal[field.name]].color"
/>
</template>
<template #default>{{ newDeal[field.name] }}</template>
<template #suffix>
@ -67,17 +79,13 @@
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import { usersStore } from '@/stores/users'
import { organizationsStore } from '@/stores/organizations'
import { dealStatuses, statusDropdownOptions, activeAgents } from '@/utils'
import {
FormControl,
Button,
Autocomplete,
Dropdown,
FeatherIcon,
} from 'frappe-ui'
import { computed } from 'vue'
import { FormControl, Button, Dropdown, FeatherIcon } from 'frappe-ui'
const { getUser } = usersStore()
const { getOrganizationOptions } = organizationsStore()
const { getUser, users } = usersStore()
const props = defineProps({
newDeal: {
type: Object,
@ -135,8 +143,13 @@ const allFields = [
fields: [
{
label: 'Organization',
name: 'organization_name',
type: 'data',
name: 'organization',
type: 'link',
placeholder: 'Organization',
options: getOrganizationOptions(),
change: (option) => {
newDeal.organization = option.name
},
},
{
label: 'Status',

View File

@ -19,8 +19,18 @@
type="email"
v-model="newLead[field.name]"
/>
<Autocomplete
<FormControl
v-else-if="field.type === 'link'"
type="autocomplete"
:value="newLead[field.name]"
:options="field.options"
@change="(e) => field.change(e)"
:placeholder="field.placeholder"
class="form-control"
/>
<FormControl
v-else-if="field.type === 'user'"
type="autocomplete"
:options="activeAgents"
:value="getUser(newLead[field.name]).full_name"
@change="(option) => (newLead[field.name] = option.email)"
@ -32,7 +42,7 @@
<template #item-prefix="{ option }">
<UserAvatar class="mr-2" :user="option.email" size="sm" />
</template>
</Autocomplete>
</FormControl>
<Dropdown
v-else-if="field.type === 'dropdown'"
:options="statusDropdownOptions(newLead)"
@ -69,17 +79,13 @@
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import { usersStore } from '@/stores/users'
import { organizationsStore } from '@/stores/organizations'
import { leadStatuses, statusDropdownOptions, activeAgents } from '@/utils'
import {
FormControl,
Button,
Autocomplete,
Dropdown,
FeatherIcon,
} from 'frappe-ui'
import { computed } from 'vue'
import { FormControl, Button, Dropdown, FeatherIcon } from 'frappe-ui'
const { getUser } = usersStore()
const { getOrganizationOptions } = organizationsStore()
const { getUser, users } = usersStore()
const props = defineProps({
newLead: {
type: Object,
@ -137,8 +143,13 @@ const allFields = [
fields: [
{
label: 'Organization',
name: 'organization_name',
type: 'data',
name: 'organization',
type: 'link',
placeholder: 'Organization',
options: getOrganizationOptions(),
change: (option) => {
newLead.organization = option.name
},
},
{
label: 'Status',
@ -149,7 +160,7 @@ const allFields = [
{
label: 'Lead owner',
name: 'lead_owner',
type: 'link',
type: 'user',
placeholder: 'Lead owner',
},
],

View File

@ -91,10 +91,10 @@
:key="field.name"
class="flex items-center gap-2 px-3 text-base leading-5 last:mb-3"
>
<div class="w-[106px] text-gray-600">
<div class="w-[106px] shrink-0 text-gray-600">
{{ field.label }}
</div>
<div class="flex-1">
<div class="flex-1 overflow-hidden">
<FormControl
v-if="field.type === 'email'"
type="email"
@ -121,6 +121,11 @@
:debounce="500"
/>
</div>
<ExternalLinkIcon
v-if="field.type === 'link' && field.link && contact[field.name]"
class="h-4 w-4 shrink-0 cursor-pointer text-gray-600"
@click="field.link(contact[field.name])"
/>
</div>
</div>
</div>
@ -197,6 +202,7 @@ import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import CameraIcon from '@/components/Icons/CameraIcon.vue'
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue'
import ExternalLinkIcon from '@/components/Icons/ExternalLinkIcon.vue'
import LeadsListView from '@/components/ListViews/LeadsListView.vue'
import DealsListView from '@/components/ListViews/DealsListView.vue'
import {
@ -210,10 +216,12 @@ import {
} from '@/utils'
import { usersStore } from '@/stores/users.js'
import { contactsStore } from '@/stores/contacts.js'
import { organizationsStore } from '@/stores/organizations.js'
import { ref, computed, h } from 'vue'
const { getContactByName, contacts } = contactsStore()
const { getUser } = usersStore()
const { getOrganization, getOrganizationOptions } = organizationsStore()
const showContactModal = ref(false)
@ -297,8 +305,7 @@ const leads = createListResource({
'first_name',
'lead_name',
'image',
'organization_name',
'organization_logo',
'organization',
'status',
'email',
'mobile_no',
@ -320,8 +327,7 @@ const deals = createListResource({
cache: ['deals', props.contactId],
fields: [
'name',
'organization_name',
'organization_logo',
'organization',
'annual_revenue',
'deal_status',
'email',
@ -361,9 +367,9 @@ function getLeadRowObject(lead) {
image: lead.image,
image_label: lead.first_name,
},
organization_name: {
label: lead.organization_name,
logo: lead.organization_logo,
organization: {
label: lead.organization,
logo: getOrganization(lead.organization)?.organization_logo,
},
status: {
label: lead.status,
@ -385,9 +391,9 @@ function getLeadRowObject(lead) {
function getDealRowObject(deal) {
return {
name: deal.name,
organization_name: {
label: deal.organization_name,
logo: deal.organization_logo,
organization: {
label: deal.organization,
logo: getOrganization(deal.organization)?.organization_logo,
},
annual_revenue: formatNumberIntoCurrency(deal.annual_revenue),
deal_status: {
@ -415,7 +421,7 @@ const leadColumns = [
},
{
label: 'Organization',
key: 'organization_name',
key: 'organization',
width: '10rem',
},
{
@ -448,7 +454,7 @@ const leadColumns = [
const dealColumns = [
{
label: 'Organization',
key: 'organization_name',
key: 'organization',
width: '11rem',
},
{
@ -483,54 +489,68 @@ const dealColumns = [
},
]
const details = [
{
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) => {
contact.value.salutation = data.value
updateContact('salutation', data.value)
const details = computed(() => {
return [
{
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) => {
contact.value.salutation = data.value
updateContact('salutation', data.value)
},
},
},
{
label: 'First name',
type: 'data',
name: 'first_name',
},
{
label: 'Last name',
type: 'data',
name: 'last_name',
},
{
label: 'Email',
type: 'email',
name: 'email',
},
{
label: 'Mobile no.',
type: 'phone',
name: 'mobile_no',
},
{
label: 'Organization',
type: 'data',
name: 'company_name',
},
]
{
label: 'First name',
type: 'data',
name: 'first_name',
},
{
label: 'Last name',
type: 'data',
name: 'last_name',
},
{
label: 'Email',
type: 'email',
name: 'email',
},
{
label: 'Mobile no.',
type: 'phone',
name: 'mobile_no',
},
{
label: 'Organization',
type: 'link',
name: 'company_name',
placeholder: 'Select organization',
options: getOrganizationOptions(),
change: (data) => {
contact.value.company_name = data.value
updateContact('company_name', data.value)
},
link: (data) => {
router.push({
name: 'Organization',
params: { organizationId: data.value },
})
},
},
]
})
function updateContact(fieldname, value) {
createResource({
@ -570,7 +590,18 @@ function updateContact(fieldname, value) {
background: white;
}
:deep(.form-control button) {
gap: 0;
}
:deep(.form-control button > div) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.form-control button svg) {
color: white;
width: 0;
}
</style>

View File

@ -4,7 +4,8 @@
<Breadcrumbs :items="breadcrumbs" />
</template>
<template #right-header>
<Autocomplete
<FormControl
type="autocomplete"
:options="activeAgents"
:value="getUser(deal.data.lead_owner).full_name"
@change="(option) => updateAssignedAgent(option.email)"
@ -16,7 +17,7 @@
<template #item-prefix="{ option }">
<UserAvatar class="mr-2" :user="option.email" size="sm" />
</template>
</Autocomplete>
</FormControl>
<Dropdown :options="statusDropdownOptions(deal.data, 'deal', updateDeal)">
<template #default="{ open }">
<Button :label="deal.data.deal_status">
@ -45,79 +46,47 @@
>
About this deal
</div>
<FileUploader @success="changeDealImage" :validateFile="validateFile">
<template #default="{ openFileSelector, error }">
<div class="flex items-center justify-start gap-5 border-b p-5">
<div class="group relative h-[88px] w-[88px]">
<Avatar
size="3xl"
class="h-[88px] w-[88px]"
:label="deal.data.organization_name"
:image="deal.data.organization_logo"
/>
<Dropdown
:options="[
{
icon: 'upload',
label: deal.data.organization_logo
? 'Change image'
: 'Upload image',
onClick: openFileSelector,
},
{
icon: 'trash-2',
label: 'Remove image',
onClick: () => {
deal.data.organization_logo = ''
updateDeal('organization_logo', '')
},
},
]"
class="!absolute bottom-0 left-0 right-0"
<div class="flex items-center justify-start gap-5 border-b p-5">
<Tooltip
text="Organization logo"
class="group relative h-[88px] w-[88px]"
>
<Avatar
size="3xl"
class="h-[88px] w-[88px]"
:label="organization?.name"
:image="organization?.organization_logo"
/>
</Tooltip>
<div class="flex flex-col gap-2.5 truncate">
<Tooltip :text="organization?.name">
<div class="truncate text-2xl font-medium">
{{ organization?.name }}
</div>
</Tooltip>
<div class="flex gap-1.5">
<Tooltip text="Make a call...">
<Button
class="h-7 w-7"
@click="() => makeCall(deal.data.mobile_no)"
>
<div
class="z-1 absolute bottom-0 left-0 right-0 flex h-11 cursor-pointer items-center justify-center rounded-b-full bg-black bg-opacity-40 pt-3 opacity-0 duration-300 ease-in-out group-hover:opacity-100"
style="
-webkit-clip-path: inset(12px 0 0 0);
clip-path: inset(12px 0 0 0);
"
>
<CameraIcon class="h-6 w-6 cursor-pointer text-white" />
</div>
</Dropdown>
</div>
<div class="flex flex-col gap-2.5 truncate">
<Tooltip :text="deal.data.organization_name">
<div class="truncate text-2xl font-medium">
{{ deal.data.organization_name }}
</div>
</Tooltip>
<div class="flex gap-1.5">
<Tooltip text="Make a call...">
<Button
class="h-7 w-7"
@click="() => makeCall(deal.data.mobile_no)"
>
<PhoneIcon class="h-4 w-4" />
</Button>
</Tooltip>
<Button class="h-7 w-7">
<EmailIcon class="h-4 w-4" />
</Button>
<Tooltip text="Go to website...">
<Button class="h-7 w-7">
<LinkIcon
class="h-4 w-4"
@click="openWebsite(deal.data.website)"
/>
</Button>
</Tooltip>
</div>
<ErrorMessage :message="error" />
</div>
<PhoneIcon class="h-4 w-4" />
</Button>
</Tooltip>
<Button class="h-7 w-7">
<EmailIcon class="h-4 w-4" />
</Button>
<Tooltip text="Go to website...">
<Button class="h-7 w-7">
<LinkIcon
class="h-4 w-4"
@click="openWebsite(deal.data.website)"
/>
</Button>
</Tooltip>
</div>
</template>
</FileUploader>
</div>
</div>
<div class="flex flex-1 flex-col justify-between overflow-hidden">
<div class="flex flex-col overflow-y-auto">
<div
@ -152,10 +121,10 @@
:key="field.label"
class="flex items-center gap-2 px-3 text-base leading-5 first:mt-3"
>
<div class="w-[106px] text-gray-600">
<div class="w-[106px] shrink-0 text-gray-600">
{{ field.label }}
</div>
<div class="flex-1">
<div class="flex-1 overflow-hidden">
<FormControl
v-if="field.type === 'select'"
type="select"
@ -183,16 +152,18 @@
"
:debounce="500"
/>
<Autocomplete
<FormControl
v-else-if="field.type === 'link'"
type="autocomplete"
:value="deal.data[field.name]"
:options="field.options"
@change="(e) => field.change(e)"
:placeholder="field.placeholder"
class="form-control"
/>
<Autocomplete
<FormControl
v-else-if="field.type === 'user'"
type="autocomplete"
:options="activeAgents"
:value="getUser(deal.data[field.name]).full_name"
@change="(option) => updateAssignedAgent(option.email)"
@ -221,7 +192,7 @@
size="sm"
/>
</template>
</Autocomplete>
</FormControl>
<Dropdown
v-else-if="field.type === 'dropdown'"
:options="
@ -283,6 +254,12 @@
:debounce="500"
class="form-control"
/>
<Tooltip :text="field.tooltip"
class="flex h-7 cursor-pointer items-center px-2 py-1"
v-else-if="field.type === 'read_only'"
>
{{ field.value }}
</Tooltip>
<FormControl
v-else
type="text"
@ -294,6 +271,11 @@
class="form-control"
/>
</div>
<ExternalLinkIcon
v-if="field.type === 'link' && field.link && deal.data[field.name]"
class="h-4 w-4 shrink-0 cursor-pointer text-gray-600"
@click="field.link(deal.data[field.name])"
/>
</div>
</div>
</transition>
@ -311,8 +293,8 @@ import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import TaskIcon from '@/components/Icons/TaskIcon.vue'
import NoteIcon from '@/components/Icons/NoteIcon.vue'
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import CameraIcon from '@/components/Icons/CameraIcon.vue'
import LinkIcon from '@/components/Icons/LinkIcon.vue'
import ExternalLinkIcon from '@/components/Icons/ExternalLinkIcon.vue'
import LayoutHeader from '@/components/LayoutHeader.vue'
import Toggler from '@/components/Toggler.vue'
import Activities from '@/components/Activities.vue'
@ -326,12 +308,10 @@ import {
} from '@/utils'
import { usersStore } from '@/stores/users'
import { contactsStore } from '@/stores/contacts'
import { organizationsStore } from '@/stores/organizations'
import {
createResource,
FeatherIcon,
FileUploader,
ErrorMessage,
Autocomplete,
FormControl,
Dropdown,
Tooltip,
@ -340,9 +320,12 @@ import {
Breadcrumbs,
} from 'frappe-ui'
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
const { getUser, users } = usersStore()
const { getUser } = usersStore()
const { contacts } = contactsStore()
const { getOrganization, getOrganizationOptions } = organizationsStore()
const router = useRouter()
const props = defineProps({
dealId: {
@ -394,7 +377,7 @@ function updateDeal(fieldname, value) {
const breadcrumbs = computed(() => {
let items = [{ label: 'Deals', route: { name: 'Deals' } }]
items.push({
label: deal.data.organization_name,
label: organization.value.name,
route: { name: 'Deal', params: { dealId: deal.data.name } },
})
return items
@ -424,18 +407,6 @@ const tabs = [
},
]
function changeDealImage(file) {
deal.data.organization_logo = file.file_url
updateDeal('organization_logo', file.file_url)
}
function validateFile(file) {
let extn = file.name.split('.').pop().toLowerCase()
if (!['png', 'jpg', 'jpeg'].includes(extn)) {
return 'Only PNG and JPG images are allowed'
}
}
const detailSections = computed(() => {
return [
{
@ -444,13 +415,27 @@ const detailSections = computed(() => {
fields: [
{
label: 'Organization',
type: 'data',
name: 'organization_name',
type: 'link',
name: 'organization',
placeholder: 'Select organization',
options: getOrganizationOptions(),
change: (data) => {
deal.data.organization = data.value
updateDeal('organization', data.value)
},
link: () => {
router.push({
name: 'Organization',
params: { organizationId: organization.value.name },
})
},
},
{
label: 'Website',
type: 'data',
type: 'read_only',
name: 'website',
value: organization.value?.website,
tooltip: 'It is a read only field, value is fetched from organization',
},
{
label: 'Amount',
@ -524,6 +509,10 @@ const detailSections = computed(() => {
]
})
const organization = computed(() => {
return getOrganization(deal.data.organization)
})
function updateAssignedAgent(email) {
deal.data.lead_owner = email
updateDeal('lead_owner', email)
@ -538,7 +527,18 @@ function updateAssignedAgent(email) {
background: white;
}
:deep(.form-control button) {
gap: 0;
}
:deep(.form-control button > div) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.form-control button svg) {
color: white;
width: 0;
}
</style>

View File

@ -37,7 +37,7 @@
<Dialog
v-model="showNewDialog"
:options="{
width: '3xl',
size: '3xl',
title: 'New Deal',
actions: [{ label: 'Save', variant: 'solid' }],
}"
@ -60,6 +60,7 @@ import NewDeal from '@/components/NewDeal.vue'
import SortBy from '@/components/SortBy.vue'
import Filter from '@/components/Filter.vue'
import { usersStore } from '@/stores/users'
import { organizationsStore } from '@/stores/organizations'
import { useOrderBy } from '@/composables/orderby'
import { useFilter } from '@/composables/filter'
import { useDebounceFn } from '@vueuse/core'
@ -85,6 +86,7 @@ import { ref, computed, reactive, watch } from 'vue'
const breadcrumbs = [{ label: 'Deals', route: { name: 'Deals' } }]
const { getUser } = usersStore()
const { getOrganization } = organizationsStore()
const { get: getOrderBy } = useOrderBy()
const { getArgs, storage } = useFilter()
@ -105,8 +107,7 @@ const leads = createListResource({
doctype: 'CRM Lead',
fields: [
'name',
'organization_name',
'organization_logo',
'organization',
'annual_revenue',
'deal_status',
'email',
@ -143,7 +144,7 @@ watch(
const columns = [
{
label: 'Organization',
key: 'organization_name',
key: 'organization',
width: '11rem',
},
{
@ -183,9 +184,9 @@ const rows = computed(() => {
return leads.data.map((lead) => {
return {
name: lead.name,
organization_name: {
label: lead.organization_name,
logo: lead.organization_logo,
organization: {
label: lead.organization,
logo: getOrganization(lead.organization)?.organization_logo,
},
annual_revenue: formatNumberIntoCurrency(lead.annual_revenue),
deal_status: {
@ -256,7 +257,7 @@ let newDeal = reactive({
first_name: '',
last_name: '',
lead_name: '',
organization_name: '',
organization: '',
deal_status: 'Qualification',
email: '',
mobile_no: '',

View File

@ -4,7 +4,8 @@
<Breadcrumbs :items="breadcrumbs" />
</template>
<template #right-header>
<Autocomplete
<FormControl
type="autocomplete"
:options="activeAgents"
:value="getUser(lead.data.lead_owner).full_name"
@change="(option) => updateAssignedAgent(option.email)"
@ -16,7 +17,7 @@
<template #item-prefix="{ option }">
<UserAvatar class="mr-2" :user="option.email" size="sm" />
</template>
</Autocomplete>
</FormControl>
<Dropdown :options="statusDropdownOptions(lead.data, 'lead', updateLead)">
<template #default="{ open }">
<Button :label="lead.data.status">
@ -58,26 +59,32 @@
:label="lead.data.first_name"
:image="lead.data.image"
/>
<Dropdown
:options="[
{
icon: 'upload',
label: lead.data.image ? 'Change photo' : 'Upload photo',
onClick: openFileSelector,
},
{
icon: 'trash-2',
label: 'Remove photo',
onClick: () => {
lead.data.image = ''
updateLead('image', '')
},
},
]"
<component
:is="lead.data.image ? Dropdown : 'div'"
v-bind="
lead.data.image
? {
options: [
{
icon: 'upload',
label: lead.data.image
? 'Change image'
: 'Upload image',
onClick: openFileSelector,
},
{
icon: 'trash-2',
label: 'Remove image',
onClick: () => changeLeadImage(''),
},
],
}
: { onClick: openFileSelector }
"
class="!absolute bottom-0 left-0 right-0"
>
<div
class="z-1 absolute bottom-0 left-0 right-0 flex h-11 cursor-pointer items-center justify-center rounded-b-full bg-black bg-opacity-40 pt-3 opacity-0 duration-300 ease-in-out group-hover:opacity-100"
class="z-1 absolute bottom-0 left-0 right-0 flex h-13 cursor-pointer items-center justify-center rounded-b-full bg-black bg-opacity-40 pt-3 opacity-0 duration-300 ease-in-out group-hover:opacity-100"
style="
-webkit-clip-path: inset(12px 0 0 0);
clip-path: inset(12px 0 0 0);
@ -85,7 +92,7 @@
>
<CameraIcon class="h-6 w-6 cursor-pointer text-white" />
</div>
</Dropdown>
</component>
</div>
<div class="flex flex-col gap-2.5 truncate">
<Tooltip :text="lead.data.lead_name">
@ -122,7 +129,7 @@
<div class="flex flex-1 flex-col justify-between overflow-hidden">
<div class="flex flex-col overflow-y-auto">
<div
v-for="(section, i) in detailSections"
v-for="section in detailSections"
:key="section.label"
class="flex flex-col"
>
@ -154,10 +161,10 @@
:key="field.name"
class="flex items-center gap-2 px-3 text-base leading-5 last:mb-3"
>
<div class="w-[106px] text-gray-600">
<div class="w-[106px] shrink-0 text-gray-600">
{{ field.label }}
</div>
<div class="flex-1">
<div class="flex-1 overflow-hidden">
<FormControl
v-if="field.type === 'select'"
type="select"
@ -185,16 +192,18 @@
"
:debounce="500"
/>
<Autocomplete
<FormControl
v-else-if="field.type === 'link'"
type="autocomplete"
:value="lead.data[field.name]"
:options="field.options"
@change="(e) => field.change(e)"
:placeholder="field.placeholder"
class="form-control"
/>
<Autocomplete
<FormControl
v-else-if="field.type === 'user'"
type="autocomplete"
:options="activeAgents"
:value="getUser(lead.data[field.name]).full_name"
@change="(option) => updateAssignedAgent(option.email)"
@ -223,7 +232,7 @@
size="sm"
/>
</template>
</Autocomplete>
</FormControl>
<Dropdown
v-else-if="field.type === 'dropdown'"
:options="
@ -255,6 +264,13 @@
</Button>
</template>
</Dropdown>
<Tooltip
:text="field.tooltip"
class="flex h-7 cursor-pointer items-center px-2 py-1"
v-else-if="field.type === 'read_only'"
>
{{ field.value }}
</Tooltip>
<FormControl
v-else
type="text"
@ -266,6 +282,15 @@
:debounce="500"
/>
</div>
<ExternalLinkIcon
v-if="
field.type === 'link' &&
field.link &&
lead.data[field.name]
"
class="h-4 w-4 shrink-0 cursor-pointer text-gray-600"
@click="field.link(lead.data[field.name])"
/>
</div>
</div>
</transition>
@ -283,7 +308,9 @@ import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import TaskIcon from '@/components/Icons/TaskIcon.vue'
import NoteIcon from '@/components/Icons/NoteIcon.vue'
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import CameraIcon from '@/components/Icons/CameraIcon.vue'
import LinkIcon from '@/components/Icons/LinkIcon.vue'
import ExternalLinkIcon from '@/components/Icons/ExternalLinkIcon.vue'
import LayoutHeader from '@/components/LayoutHeader.vue'
import Toggler from '@/components/Toggler.vue'
import Activities from '@/components/Activities.vue'
@ -297,12 +324,12 @@ import {
} from '@/utils'
import { usersStore } from '@/stores/users'
import { contactsStore } from '@/stores/contacts'
import { organizationsStore } from '@/stores/organizations'
import {
createResource,
FileUploader,
ErrorMessage,
FeatherIcon,
Autocomplete,
FormControl,
Dropdown,
Tooltip,
@ -312,10 +339,10 @@ import {
} from 'frappe-ui'
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import CameraIcon from '../components/Icons/CameraIcon.vue'
const { getUser, users } = usersStore()
const { getUser } = usersStore()
const { contacts } = contactsStore()
const { getOrganization, getOrganizationOptions } = organizationsStore()
const router = useRouter()
const props = defineProps({
@ -421,13 +448,28 @@ const detailSections = computed(() => {
fields: [
{
label: 'Organization',
type: 'data',
name: 'organization_name',
type: 'link',
name: 'organization',
placeholder: 'Select organization',
options: getOrganizationOptions(),
change: (data) => {
lead.data.organization = data.value
updateLead('organization', data.value)
},
link: () => {
router.push({
name: 'Organization',
params: { organizationId: organization.value?.name },
})
},
},
{
label: 'Website',
type: 'data',
type: 'read_only',
name: 'website',
value: organization.value?.website,
tooltip:
'It is a read only field, value is fetched from organization',
},
{
label: 'Job title',
@ -517,6 +559,10 @@ const detailSections = computed(() => {
]
})
const organization = computed(() => {
return getOrganization(lead.data.organization)
})
function convertToDeal() {
lead.data.status = 'Qualified'
lead.data.is_deal = 1
@ -537,7 +583,18 @@ function updateAssignedAgent(email) {
background: white;
}
:deep(.form-control button) {
gap: 0;
}
:deep(.form-control button > div) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.form-control button svg) {
color: white;
width: 0;
}
</style>

View File

@ -59,6 +59,7 @@ import NewLead from '@/components/NewLead.vue'
import SortBy from '@/components/SortBy.vue'
import Filter from '@/components/Filter.vue'
import { usersStore } from '@/stores/users'
import { organizationsStore } from '@/stores/organizations'
import { useOrderBy } from '@/composables/orderby'
import { useFilter } from '@/composables/filter'
import { useDebounceFn } from '@vueuse/core'
@ -78,6 +79,7 @@ import { ref, computed, reactive, watch } from 'vue'
const breadcrumbs = [{ label: 'Leads', route: { name: 'Leads' } }]
const { getUser } = usersStore()
const { getOrganization } = organizationsStore()
const { get: getOrderBy } = useOrderBy()
const { getArgs, storage } = useFilter()
@ -105,8 +107,7 @@ const leads = createListResource({
'first_name',
'lead_name',
'image',
'organization_name',
'organization_logo',
'organization',
'status',
'email',
'mobile_no',
@ -147,7 +148,7 @@ const columns = [
},
{
label: 'Organization',
key: 'organization_name',
key: 'organization',
width: '10rem',
},
{
@ -187,9 +188,9 @@ const rows = computed(() => {
image: lead.image,
image_label: lead.first_name,
},
organization_name: {
label: lead.organization_name,
logo: lead.organization_logo,
organization: {
label: lead.organization,
logo: getOrganization(lead.organization)?.organization_logo,
},
status: {
label: lead.status,
@ -259,7 +260,7 @@ let newLead = reactive({
first_name: '',
last_name: '',
lead_name: '',
organization_name: '',
organization: '',
status: 'Open',
email: '',
mobile_no: '',

View File

@ -289,8 +289,7 @@ const leads = createListResource({
'first_name',
'lead_name',
'image',
'organization_name',
'organization_logo',
'organization',
'status',
'email',
'mobile_no',
@ -298,7 +297,7 @@ const leads = createListResource({
'modified',
],
filters: {
organization_name: props.organization.name,
organization: props.organization.name,
is_deal: 0,
},
orderBy: 'modified desc',
@ -312,8 +311,7 @@ const deals = createListResource({
cache: ['deals', props.organization.name],
fields: [
'name',
'organization_name',
'organization_logo',
'organization',
'annual_revenue',
'deal_status',
'email',
@ -322,7 +320,7 @@ const deals = createListResource({
'modified',
],
filters: {
organization_name: props.organization.name,
organization: props.organization.name,
is_deal: 1,
},
orderBy: 'modified desc',
@ -334,7 +332,15 @@ const contacts = createListResource({
type: 'list',
doctype: 'Contact',
cache: ['contacts', props.organization.name],
fields: ['name', 'email_id', 'mobile_no', 'company_name', 'modified'],
fields: [
'name',
'full_name',
'image',
'email_id',
'mobile_no',
'company_name',
'modified',
],
filters: {
company_name: props.organization.name,
},
@ -374,9 +380,9 @@ function getLeadRowObject(lead) {
image: lead.image,
image_label: lead.first_name,
},
organization_name: {
label: lead.organization_name,
logo: lead.organization_logo,
organization: {
label: lead.organization,
logo: props.organization?.organization_logo,
},
status: {
label: lead.status,
@ -398,9 +404,9 @@ function getLeadRowObject(lead) {
function getDealRowObject(deal) {
return {
name: deal.name,
organization_name: {
label: deal.organization_name,
logo: deal.organization_logo,
organization: {
label: deal.organization,
logo: props.organization?.organization_logo,
},
annual_revenue: formatNumberIntoCurrency(deal.annual_revenue),
deal_status: {
@ -449,7 +455,7 @@ const leadColumns = [
},
{
label: 'Organization',
key: 'organization_name',
key: 'organization',
width: '10rem',
},
{
@ -482,7 +488,7 @@ const leadColumns = [
const dealColumns = [
{
label: 'Organization',
key: 'organization_name',
key: 'organization',
width: '11rem',
},
{
@ -546,8 +552,8 @@ const contactColumns = [
]
function reload(val) {
leads.filters.organization_name = val
deals.filters.organization_name = val
leads.filters.organization = val
deals.filters.organization = val
contacts.filters.company_name = val
leads.reload()
deals.reload()

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { createResource } from 'frappe-ui'
import { reactive } from 'vue'
import { reactive, computed } from 'vue'
export const organizationsStore = defineStore('crm-organizations', () => {
let organizationsByName = reactive({})
@ -27,8 +27,19 @@ export const organizationsStore = defineStore('crm-organizations', () => {
return organizationsByName[name]
}
function getOrganizationOptions() {
return [
{ label: '', value: '' },
...organizations.data?.map((org) => ({
label: org.name,
value: org.name,
})),
]
}
return {
organizations,
getOrganizationOptions,
getOrganization,
}
})