diff --git a/crm/fcrm/doctype/crm_lead/crm_lead.json b/crm/fcrm/doctype/crm_lead/crm_lead.json index 2c774ba4..bfe7dbbd 100644 --- a/crm/fcrm/doctype/crm_lead/crm_lead.json +++ b/crm/fcrm/doctype/crm_lead/crm_lead.json @@ -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", diff --git a/crm/fcrm/doctype/crm_lead/crm_lead.py b/crm/fcrm/doctype/crm_lead/crm_lead.py index 6e0b5422..56436a7c 100644 --- a/crm/fcrm/doctype/crm_lead/crm_lead.py +++ b/crm/fcrm/doctype/crm_lead/crm_lead.py @@ -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' }, diff --git a/crm/fcrm/doctype/crm_organization/crm_organization.json b/crm/fcrm/doctype/crm_organization/crm_organization.json index d58e76aa..eee05919 100644 --- a/crm/fcrm/doctype/crm_organization/crm_organization.json +++ b/crm/fcrm/doctype/crm_organization/crm_organization.json @@ -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", diff --git a/frontend/src/components/Icons/ExternalLinkIcon.vue b/frontend/src/components/Icons/ExternalLinkIcon.vue new file mode 100644 index 00000000..0597fed3 --- /dev/null +++ b/frontend/src/components/Icons/ExternalLinkIcon.vue @@ -0,0 +1,16 @@ + diff --git a/frontend/src/components/ListViews/DealsListView.vue b/frontend/src/components/ListViews/DealsListView.vue index aa53fbc4..542e7417 100644 --- a/frontend/src/components/ListViews/DealsListView.vue +++ b/frontend/src/components/ListViews/DealsListView.vue @@ -21,7 +21,7 @@
-
+
-
+
-
{{ field.label }}
+
{{ field.label }}
- + - +
@@ -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; } diff --git a/frontend/src/pages/Deal.vue b/frontend/src/pages/Deal.vue index d9616bc9..e48efa3c 100644 --- a/frontend/src/pages/Deal.vue +++ b/frontend/src/pages/Deal.vue @@ -4,7 +4,8 @@ - + - + + + {{ field.value }} +
+
@@ -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; } diff --git a/frontend/src/pages/Deals.vue b/frontend/src/pages/Deals.vue index 6359ec2b..9783fddc 100644 --- a/frontend/src/pages/Deals.vue +++ b/frontend/src/pages/Deals.vue @@ -37,7 +37,7 @@ { 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: '', diff --git a/frontend/src/pages/Lead.vue b/frontend/src/pages/Lead.vue index 6ade3c4b..144e4d31 100644 --- a/frontend/src/pages/Lead.vue +++ b/frontend/src/pages/Lead.vue @@ -4,7 +4,8 @@ - + - + + {{ field.value }} +
+ @@ -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; } diff --git a/frontend/src/pages/Leads.vue b/frontend/src/pages/Leads.vue index cc7f6418..b0e26a1d 100644 --- a/frontend/src/pages/Leads.vue +++ b/frontend/src/pages/Leads.vue @@ -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: '', diff --git a/frontend/src/pages/Organization.vue b/frontend/src/pages/Organization.vue index 3270b20e..3e5a42e0 100644 --- a/frontend/src/pages/Organization.vue +++ b/frontend/src/pages/Organization.vue @@ -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() diff --git a/frontend/src/stores/organizations.js b/frontend/src/stores/organizations.js index dc8ada9b..302b2c76 100644 --- a/frontend/src/stores/organizations.js +++ b/frontend/src/stores/organizations.js @@ -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, } })