diff --git a/crm/api/session.py b/crm/api/session.py index 746de854..3c12947c 100644 --- a/crm/api/session.py +++ b/crm/api/session.py @@ -30,59 +30,6 @@ def get_users(): return users -@frappe.whitelist() -def get_contacts(): - contacts = frappe.get_all( - "Contact", - fields=[ - "name", - "salutation", - "first_name", - "last_name", - "full_name", - "gender", - "address", - "designation", - "image", - "email_id", - "mobile_no", - "phone", - "company_name", - "modified", - ], - order_by="first_name asc", - distinct=True, - ) - - for contact in contacts: - contact["email_ids"] = frappe.get_all( - "Contact Email", - filters={"parenttype": "Contact", "parent": contact.name}, - fields=["name", "email_id", "is_primary"], - ) - - contact["phone_nos"] = frappe.get_all( - "Contact Phone", - filters={"parenttype": "Contact", "parent": contact.name}, - fields=["name", "phone", "is_primary_phone", "is_primary_mobile_no"], - ) - - return contacts - - -@frappe.whitelist() -def get_lead_contacts(): - lead_contacts = frappe.get_all( - "CRM Lead", - fields=["name", "lead_name", "mobile_no", "phone", "image", "modified"], - filters={"converted": 0}, - order_by="lead_name asc", - distinct=True, - ) - - return lead_contacts - - @frappe.whitelist() def get_organizations(): organizations = frappe.qb.get_query( diff --git a/crm/fcrm/doctype/crm_deal/api.py b/crm/fcrm/doctype/crm_deal/api.py index 26a88ca6..9b5ee368 100644 --- a/crm/fcrm/doctype/crm_deal/api.py +++ b/crm/fcrm/doctype/crm_deal/api.py @@ -20,9 +20,13 @@ def get_deal_contacts(name): "CRM Contacts", filters={"parenttype": "CRM Deal", "parent": name}, fields=["contact", "is_primary"], + distinct=True, ) deal_contacts = [] for contact in contacts: + if not contact.contact: + continue + is_primary = contact.is_primary contact = frappe.get_doc("Contact", contact.contact).as_dict() diff --git a/crm/fcrm/doctype/crm_lead/crm_lead.py b/crm/fcrm/doctype/crm_lead/crm_lead.py index 83705a62..31039e76 100644 --- a/crm/fcrm/doctype/crm_lead/crm_lead.py +++ b/crm/fcrm/doctype/crm_lead/crm_lead.py @@ -115,13 +115,14 @@ class CRMLead(Document): elif user != agent: frappe.share.remove(self.doctype, self.name, user) - def create_contact(self, throw=True): + def create_contact(self, existing_contact=None, throw=True): if not self.lead_name: self.set_full_name() self.set_lead_name() - existing_contact = self.contact_exists(throw) + existing_contact = existing_contact or self.contact_exists(throw) if existing_contact: + self.update_lead_contact(existing_contact) return existing_contact contact = frappe.new_doc("Contact") @@ -151,12 +152,15 @@ class CRMLead(Document): return contact.name - def create_organization(self): - if not self.organization: + def create_organization(self, existing_organization=None): + if not self.organization and not existing_organization: return - existing_organization = frappe.db.exists("CRM Organization", {"organization_name": self.organization}) + existing_organization = existing_organization or frappe.db.exists( + "CRM Organization", {"organization_name": self.organization} + ) if existing_organization: + self.db_set("organization", existing_organization) return existing_organization organization = frappe.new_doc("CRM Organization") @@ -172,6 +176,20 @@ class CRMLead(Document): organization.insert(ignore_permissions=True) return organization.name + def update_lead_contact(self, contact): + contact = frappe.get_cached_doc("Contact", contact) + frappe.db.set_value( + "CRM Lead", + self.name, + { + "salutation": contact.salutation, + "first_name": contact.first_name, + "last_name": contact.last_name, + "email": contact.email_id, + "mobile_no": contact.mobile_no, + }, + ) + def contact_exists(self, throw=True): email_exist = frappe.db.exists("Contact Email", {"email_id": self.email}) phone_exist = frappe.db.exists("Contact Phone", {"phone": self.phone}) @@ -383,7 +401,7 @@ class CRMLead(Document): @frappe.whitelist() -def convert_to_deal(lead, doc=None, deal=None): +def convert_to_deal(lead, doc=None, deal=None, existing_contact=None, existing_organization=None): if not (doc and doc.flags.get("ignore_permissions")) and not frappe.has_permission( "CRM Lead", "write", lead ): @@ -395,7 +413,7 @@ def convert_to_deal(lead, doc=None, deal=None): lead.db_set("converted", 1) if lead.sla and frappe.db.exists("CRM Communication Status", "Replied"): lead.db_set("communication_status", "Replied") - contact = lead.create_contact(False) - organization = lead.create_organization() + contact = lead.create_contact(existing_contact, False) + organization = lead.create_organization(existing_organization) _deal = lead.create_deal(contact, organization, deal) return _deal diff --git a/frontend/src/components/Activities/Activities.vue b/frontend/src/components/Activities/Activities.vue index 392bf4b5..8e49b8db 100644 --- a/frontend/src/components/Activities/Activities.vue +++ b/frontend/src/components/Activities/Activities.vue @@ -487,7 +487,6 @@ import FilesUploader from '@/components/FilesUploader/FilesUploader.vue' import { timeAgo, formatDate, startCase } from '@/utils' import { globalStore } from '@/stores/global' import { usersStore } from '@/stores/users' -import { contactsStore } from '@/stores/contacts' import { whatsappEnabled } from '@/composables/settings' import { capture } from '@/telemetry' import { Button, Tooltip, createResource } from 'frappe-ui' @@ -506,7 +505,6 @@ import { useRoute } from 'vue-router' const { makeCall, $socket } = globalStore() const { getUser } = usersStore() -const { getContact, getLeadContact } = contactsStore() const props = defineProps({ doctype: { diff --git a/frontend/src/components/ViewControls.vue b/frontend/src/components/ViewControls.vue index d22bcfa4..fdd40323 100644 --- a/frontend/src/components/ViewControls.vue +++ b/frontend/src/components/ViewControls.vue @@ -203,7 +203,9 @@ label: __('Export'), icon: () => h(ExportIcon, { class: 'h-4 w-4' }), onClick: () => (showExportDialog = true), - condition: () => !options.hideColumnsButton && route.params.viewType !== 'kanban', + condition: () => + !options.hideColumnsButton && + route.params.viewType !== 'kanban', }, { label: __('Customize quick filters'), @@ -535,6 +537,7 @@ onMounted(() => useDebounceFn(reload, 100)()) const isLoading = computed(() => list.value?.loading) function reload() { + if (isLoading.value) return list.value.params = getParams() list.value.reload() } @@ -803,12 +806,13 @@ const quickFilters = createResource({ url: 'crm.api.doc.get_quick_filters', params: { doctype: props.doctype }, cache: ['Quick Filters', props.doctype], - auto: true, onSuccess(filters) { setupNewQuickFilters(filters) }, }) +if (!quickFilters.data) quickFilters.fetch() + function setupNewQuickFilters(filters) { newQuickFilters.value = filters.map((f) => ({ label: f.label, diff --git a/frontend/src/pages/Deal.vue b/frontend/src/pages/Deal.vue index 2554e811..b7dd59eb 100644 --- a/frontend/src/pages/Deal.vue +++ b/frontend/src/pages/Deal.vue @@ -564,10 +564,11 @@ const sections = createResource({ url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections', cache: ['sidePanelSections', 'CRM Deal'], params: { doctype: 'CRM Deal' }, - auto: true, transform: (data) => getParsedSections(data), }) +if (!sections.data) sections.fetch() + function getParsedSections(_sections) { _sections.forEach((section) => { if (section.name == 'contacts_section') return @@ -670,7 +671,6 @@ const dealContacts = createResource({ url: 'crm.fcrm.doctype.crm_deal.api.get_deal_contacts', params: { name: props.dealId }, cache: ['deal_contacts', props.dealId], - auto: true, transform: (data) => { data.forEach((contact) => { contact.opened = false @@ -679,6 +679,8 @@ const dealContacts = createResource({ }, }) +if (!dealContacts.data) dealContacts.fetch() + function triggerCall() { let primaryContact = dealContacts.data?.find((c) => c.is_primary) let mobile_no = primaryContact.mobile_no || null diff --git a/frontend/src/pages/Lead.vue b/frontend/src/pages/Lead.vue index f68e5c53..1d6303db 100644 --- a/frontend/src/pages/Lead.vue +++ b/frontend/src/pages/Lead.vue @@ -341,7 +341,6 @@ import { getView } from '@/utils/view' import { getSettings } from '@/stores/settings' import { usersStore } from '@/stores/users' import { globalStore } from '@/stores/global' -import { contactsStore } from '@/stores/contacts' import { statusesStore } from '@/stores/statuses' import { getMeta } from '@/stores/meta' import { @@ -369,7 +368,6 @@ import { useActiveTabManager } from '@/composables/useActiveTabManager' const { brand } = getSettings() const { isManager } = usersStore() const { $dialog, $socket, makeCall } = globalStore() -const { getContactByName, contacts } = contactsStore() const { statusOptions, getLeadStatus, getDealStatus } = statusesStore() const { doctypeMeta } = getMeta('CRM Lead') const route = useRoute() @@ -599,9 +597,7 @@ const existingOrganizationChecked = ref(false) const existingContact = ref('') const existingOrganization = ref('') -async function convertToDeal(updated) { - let valueUpdated = false - +async function convertToDeal() { if (existingContactChecked.value && !existingContact.value) { createToast({ title: __('Error'), @@ -622,55 +618,35 @@ async function convertToDeal(updated) { return } - if (existingContactChecked.value && existingContact.value) { - lead.data.salutation = getContactByName(existingContact.value).salutation - lead.data.first_name = getContactByName(existingContact.value).first_name - lead.data.last_name = getContactByName(existingContact.value).last_name - lead.data.email_id = getContactByName(existingContact.value).email_id - lead.data.mobile_no = getContactByName(existingContact.value).mobile_no - existingContactChecked.value = false - valueUpdated = true + if (!existingContactChecked.value && existingContact.value) { + existingContact.value = '' } - if (existingOrganizationChecked.value && existingOrganization.value) { - lead.data.organization = existingOrganization.value - existingOrganizationChecked.value = false - valueUpdated = true + if (!existingOrganizationChecked.value && existingOrganization.value) { + existingOrganization.value = '' } - if (valueUpdated) { - updateLead( - { - salutation: lead.data.salutation, - first_name: lead.data.first_name, - last_name: lead.data.last_name, - email_id: lead.data.email_id, - mobile_no: lead.data.mobile_no, - organization: lead.data.organization, - }, - '', - () => convertToDeal(true), - ) - showConvertToDealModal.value = false - } else { - let _deal = await call( - 'crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', - { lead: lead.data.name, deal }, - ).catch((err) => { - createToast({ - title: __('Error converting to deal'), - text: __(err.messages?.[0]), - icon: 'x', - iconClasses: 'text-ink-red-4', - }) + let _deal = await call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', { + lead: lead.data.name, + deal, + existing_contact: existingContact.value, + existing_organization: existingOrganization.value, + }).catch((err) => { + createToast({ + title: __('Error converting to deal'), + text: __(err.messages?.[0]), + icon: 'x', + iconClasses: 'text-ink-red-4', }) - if (_deal) { - capture('convert_lead_to_deal') - if (updated) { - await contacts.reload() - } - router.push({ name: 'Deal', params: { dealId: _deal } }) - } + }) + if (_deal) { + showConvertToDealModal.value = false + existingContactChecked.value = false + existingOrganizationChecked.value = false + existingContact.value = '' + existingOrganization.value = '' + capture('convert_lead_to_deal') + router.push({ name: 'Deal', params: { dealId: _deal } }) } } diff --git a/frontend/src/pages/MobileLead.vue b/frontend/src/pages/MobileLead.vue index 437d65a4..4e304d53 100644 --- a/frontend/src/pages/MobileLead.vue +++ b/frontend/src/pages/MobileLead.vue @@ -178,7 +178,6 @@ import { createToast, setupAssignees, setupCustomizations } from '@/utils' import { getView } from '@/utils/view' import { getSettings } from '@/stores/settings' import { globalStore } from '@/stores/global' -import { contactsStore } from '@/stores/contacts' import { statusesStore } from '@/stores/statuses' import { getMeta } from '@/stores/meta' import { @@ -186,6 +185,7 @@ import { callEnabled, isMobileView, } from '@/composables/settings' +import { capture } from '@/telemetry' import { useActiveTabManager } from '@/composables/useActiveTabManager' import { createResource, @@ -203,7 +203,6 @@ import { useRouter, useRoute } from 'vue-router' const { brand } = getSettings() const { $dialog, $socket } = globalStore() -const { getContactByName, contacts } = contactsStore() const { statusOptions, getLeadStatus } = statusesStore() const { doctypeMeta } = getMeta('CRM Lead') const route = useRoute() @@ -433,9 +432,7 @@ const existingOrganizationChecked = ref(false) const existingContact = ref('') const existingOrganization = ref('') -async function convertToDeal(updated) { - let valueUpdated = false - +async function convertToDeal() { if (existingContactChecked.value && !existingContact.value) { createToast({ title: __('Error'), @@ -456,49 +453,28 @@ async function convertToDeal(updated) { return } - if (existingContactChecked.value && existingContact.value) { - lead.data.salutation = getContactByName(existingContact.value).salutation - lead.data.first_name = getContactByName(existingContact.value).first_name - lead.data.last_name = getContactByName(existingContact.value).last_name - lead.data.email_id = getContactByName(existingContact.value).email_id - lead.data.mobile_no = getContactByName(existingContact.value).mobile_no - existingContactChecked.value = false - valueUpdated = true + if (!existingContactChecked.value && existingContact.value) { + existingContact.value = '' } - if (existingOrganizationChecked.value && existingOrganization.value) { - lead.data.organization = existingOrganization.value - existingOrganizationChecked.value = false - valueUpdated = true + if (!existingOrganizationChecked.value && existingOrganization.value) { + existingOrganization.value = '' } - if (valueUpdated) { - updateLead( - { - salutation: lead.data.salutation, - first_name: lead.data.first_name, - last_name: lead.data.last_name, - email_id: lead.data.email_id, - mobile_no: lead.data.mobile_no, - organization: lead.data.organization, - }, - '', - () => convertToDeal(true), - ) + let deal = await call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', { + lead: lead.data.name, + deal: {}, + existing_contact: existingContact.value, + existing_organization: existingOrganization.value, + }) + if (deal) { showConvertToDealModal.value = false - } else { - let deal = await call( - 'crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', - { - lead: lead.data.name, - }, - ) - if (deal) { - if (updated) { - await contacts.reload() - } - router.push({ name: 'Deal', params: { dealId: deal } }) - } + existingContactChecked.value = false + existingOrganizationChecked.value = false + existingContact.value = '' + existingOrganization.value = '' + capture('convert_lead_to_deal') + router.push({ name: 'Deal', params: { dealId: deal } }) } } diff --git a/frontend/src/stores/contacts.js b/frontend/src/stores/contacts.js deleted file mode 100644 index 06ff23f2..00000000 --- a/frontend/src/stores/contacts.js +++ /dev/null @@ -1,80 +0,0 @@ -import { defineStore } from 'pinia' -import { createResource } from 'frappe-ui' -import { reactive } from 'vue' - -export const contactsStore = defineStore('crm-contacts', () => { - let contactsByPhone = reactive({}) - let contactsByName = reactive({}) - let leadContactsByPhone = reactive({}) - let allContacts = reactive([]) - - const contacts = createResource({ - url: 'crm.api.session.get_contacts', - cache: 'contacts', - initialData: [], - auto: true, - transform(contacts) { - for (let contact of contacts) { - // remove special characters from phone number to make it easier to search - // also remove spaces but keep + sign at the start - contact.actual_mobile_no = contact.mobile_no - contact.mobile_no = contact.mobile_no?.replace(/[^0-9+]/g, '') - contactsByPhone[contact.mobile_no] = contact - contactsByName[contact.name] = contact - } - allContacts = [...contacts] - return contacts - }, - onError(error) { - if (error && error.exc_type === 'AuthenticationError') { - router.push('/login') - } - }, - }) - - const leadContacts = createResource({ - url: 'crm.api.session.get_lead_contacts', - cache: 'lead_contacts', - initialData: [], - auto: true, - transform(lead_contacts) { - for (let lead_contact of lead_contacts) { - // remove special characters from phone number to make it easier to search - // also remove spaces but keep + sign at the start - lead_contact.mobile_no = lead_contact.mobile_no?.replace(/[^0-9+]/g, '') - lead_contact.full_name = lead_contact.lead_name - leadContactsByPhone[lead_contact.mobile_no] = lead_contact - } - return lead_contacts - }, - onError(error) { - if (error && error.exc_type === 'AuthenticationError') { - router.push('/login') - } - }, - }) - - function getContact(mobile_no) { - mobile_no = mobile_no?.replace(/[^0-9+]/g, '') - return contactsByPhone[mobile_no] - } - function getContactByName(name) { - return contactsByName[name] - } - function getLeadContact(mobile_no) { - mobile_no = mobile_no?.replace(/[^0-9+]/g, '') - return leadContactsByPhone[mobile_no] - } - - function getContacts() { - return allContacts || contacts?.data || [] - } - - return { - contacts, - getContacts, - getContact, - getContactByName, - getLeadContact, - } -}) diff --git a/frontend/src/stores/meta.js b/frontend/src/stores/meta.js index 092c948a..f43daab1 100644 --- a/frontend/src/stores/meta.js +++ b/frontend/src/stores/meta.js @@ -24,7 +24,7 @@ export function getMeta(doctype) { }, }) - if (!doctypeMeta[doctype]) { + if (!doctypeMeta[doctype] && !meta.loading) { meta.fetch() } diff --git a/frontend/src/stores/settings.js b/frontend/src/stores/settings.js index 99439587..8a6bcd03 100644 --- a/frontend/src/stores/settings.js +++ b/frontend/src/stores/settings.js @@ -4,17 +4,17 @@ import { reactive, ref } from 'vue' const settings = ref({}) const brand = reactive({}) -export function getSettings() { - const _settings = createDocumentResource({ - doctype: 'FCRM Settings', - name: 'FCRM Settings', - onSuccess: (data) => { - settings.value = data - setupBrand() - return data - }, - }) +const _settings = createDocumentResource({ + doctype: 'FCRM Settings', + name: 'FCRM Settings', + onSuccess: (data) => { + settings.value = data + getSettings().setupBrand() + return data + }, +}) +export function getSettings() { function setupBrand() { brand.name = settings.value?.brand_name brand.logo = settings.value?.brand_logo diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js index 338c4ad8..289e9120 100644 --- a/frontend/src/utils/index.js +++ b/frontend/src/utils/index.js @@ -75,11 +75,9 @@ export function timeAgo(date) { return useTimeAgo(date).value } -const taskMeta = getMeta('CRM Task') - export function taskStatusOptions(action, data) { let options = ['Backlog', 'Todo', 'In Progress', 'Done', 'Canceled'] - let statusMeta = taskMeta + let statusMeta = getMeta('CRM Task') .getFields() ?.find((field) => field.fieldname == 'status') if (statusMeta) { @@ -98,7 +96,7 @@ export function taskStatusOptions(action, data) { export function taskPriorityOptions(action, data) { let options = ['Low', 'Medium', 'High'] - let priorityMeta = taskMeta + let priorityMeta = getMeta('CRM Task') .getFields() ?.find((field) => field.fieldname == 'priority') if (priorityMeta) {