1
0
forked from test/crm

Merge branch 'develop' into fix-exotel-call-ui

This commit is contained in:
Shariq Ansari 2025-02-14 11:31:20 +05:30 committed by GitHub
commit 9f321929ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 371 additions and 270 deletions

View File

@ -255,6 +255,8 @@ def get_data(
if hasattr(_list, "default_list_data"):
default_rows = _list.default_list_data().get("rows")
meta = frappe.get_meta(doctype)
if view_type != "kanban":
if columns or rows:
custom_view = True
@ -296,6 +298,11 @@ def get_data(
if column.get("key") == "_liked_by" and column.get("width") == "10rem":
column["width"] = "50px"
# remove column if column.hidden is True
column_meta = meta.get_field(column.get("key"))
if column_meta and column_meta.get("hidden"):
columns.remove(column)
# check if rows has group_by_field if not add it
if group_by_field and group_by_field not in rows:
rows.append(group_by_field)

View File

@ -1,22 +1,24 @@
# Copyright (c) 2024, Frappe and contributors
# For license information, please see license.txt
import json
import frappe
from frappe import _
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.model.document import Document
from frappe.frappeclient import FrappeClient
from frappe.model.document import Document
from frappe.utils import get_url_to_form, get_url_to_list
import json
class ERPNextCRMSettings(Document):
def validate(self):
if self.enabled:
self.validate_if_erpnext_installed()
self.add_quotation_to_option()
self.create_custom_fields()
self.create_custom_fields()
self.create_crm_form_script()
def validate_if_erpnext_installed(self):
if not self.is_erpnext_in_different_site:
if "erpnext" not in frappe.get_installed_apps():
@ -37,6 +39,7 @@ class ERPNextCRMSettings(Document):
def create_custom_fields(self):
if not self.is_erpnext_in_different_site:
from erpnext.crm.frappe_crm_api import create_custom_fields_for_frappe_crm
create_custom_fields_for_frappe_crm()
else:
self.create_custom_fields_in_remote_site()
@ -48,22 +51,24 @@ class ERPNextCRMSettings(Document):
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"Error while creating custom field in the remote erpnext site: {self.erpnext_site_url}"
f"Error while creating custom field in the remote erpnext site: {self.erpnext_site_url}",
)
frappe.throw("Error while creating custom field in ERPNext, check error log for more details")
def create_crm_form_script(self):
if not frappe.db.exists("CRM Form Script", "Create Quotation from CRM Deal"):
script = get_crm_form_script()
frappe.get_doc({
"doctype": "CRM Form Script",
"name": "Create Quotation from CRM Deal",
"dt": "CRM Deal",
"view": "Form",
"script": script,
"enabled": 1,
"is_standard": 1
}).insert()
frappe.get_doc(
{
"doctype": "CRM Form Script",
"name": "Create Quotation from CRM Deal",
"dt": "CRM Deal",
"view": "Form",
"script": script,
"enabled": 1,
"is_standard": 1,
}
).insert()
@frappe.whitelist()
def reset_erpnext_form_script(self):
@ -77,14 +82,14 @@ class ERPNextCRMSettings(Document):
frappe.log_error(frappe.get_traceback(), "Error while resetting form script")
return False
def get_erpnext_site_client(erpnext_crm_settings):
site_url = erpnext_crm_settings.erpnext_site_url
api_key = erpnext_crm_settings.api_key
api_secret = erpnext_crm_settings.get_password("api_secret", raise_exception=False)
return FrappeClient(
site_url, api_key=api_key, api_secret=api_secret
)
return FrappeClient(site_url, api_key=api_key, api_secret=api_secret)
@frappe.whitelist()
def get_customer_link(crm_deal):
@ -107,7 +112,7 @@ def get_customer_link(crm_deal):
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"Error while fetching customer in remote site: {erpnext_crm_settings.erpnext_site_url}"
f"Error while fetching customer in remote site: {erpnext_crm_settings.erpnext_site_url}",
)
frappe.throw(_("Error while fetching customer in ERPNext, check error log for more details"))
@ -118,23 +123,28 @@ def get_quotation_url(crm_deal, organization):
if not erpnext_crm_settings.enabled:
frappe.throw(_("ERPNext is not integrated with the CRM"))
contact = get_contact(crm_deal)
address = get_organization_address(organization).get("name") if organization else None
if not erpnext_crm_settings.is_erpnext_in_different_site:
quotation_url = get_url_to_list("Quotation")
return f"{quotation_url}/new?quotation_to=CRM Deal&crm_deal={crm_deal}&party_name={crm_deal}&company={erpnext_crm_settings.erpnext_company}"
return f"{quotation_url}/new?quotation_to=CRM Deal&crm_deal={crm_deal}&party_name={crm_deal}&company={erpnext_crm_settings.erpnext_company}&contact_person={contact}&customer_address={address}"
else:
site_url = erpnext_crm_settings.get("erpnext_site_url")
quotation_url = f"{site_url}/app/quotation"
prospect = create_prospect_in_remote_site(crm_deal, erpnext_crm_settings)
return f"{quotation_url}/new?quotation_to=Prospect&crm_deal={crm_deal}&party_name={prospect}&company={erpnext_crm_settings.erpnext_company}"
return f"{quotation_url}/new?quotation_to=Prospect&crm_deal={crm_deal}&party_name={prospect}&company={erpnext_crm_settings.erpnext_company}&contact_person={contact}&customer_address={address}"
def create_prospect_in_remote_site(crm_deal, erpnext_crm_settings):
try:
client = get_erpnext_site_client(erpnext_crm_settings)
doc = frappe.get_doc("CRM Deal", crm_deal)
doc = frappe.get_cached_doc("CRM Deal", crm_deal)
contacts = get_contacts(doc)
address = get_organization_address(doc.organization)
return client.post_api("erpnext.crm.frappe_crm_api.create_prospect_against_crm_deal",
return client.post_api(
"erpnext.crm.frappe_crm_api.create_prospect_against_crm_deal",
{
"organization": doc.organization,
"lead_name": doc.lead_name,
@ -147,32 +157,47 @@ def create_prospect_in_remote_site(crm_deal, erpnext_crm_settings):
"annual_revenue": doc.annual_revenue,
"contacts": json.dumps(contacts),
"erpnext_company": erpnext_crm_settings.erpnext_company,
"address": address.as_dict() if address else None
"address": address.as_dict() if address else None,
},
)
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"Error while creating prospect in remote site: {erpnext_crm_settings.erpnext_site_url}"
f"Error while creating prospect in remote site: {erpnext_crm_settings.erpnext_site_url}",
)
frappe.throw(_("Error while creating prospect in ERPNext, check error log for more details"))
def get_contact(crm_deal):
doc = frappe.get_cached_doc("CRM Deal", crm_deal)
contact = None
for c in doc.contacts:
if c.is_primary:
contact = c.contact
break
return contact
def get_contacts(doc):
contacts = []
for c in doc.contacts:
contacts.append({
"contact": c.contact,
"full_name": c.full_name,
"email": c.email,
"mobile_no": c.mobile_no,
"gender": c.gender,
"is_primary": c.is_primary,
})
contacts.append(
{
"contact": c.contact,
"full_name": c.full_name,
"email": c.email,
"mobile_no": c.mobile_no,
"gender": c.gender,
"is_primary": c.is_primary,
}
)
return contacts
def get_organization_address(organization):
address = frappe.db.get_value("CRM Organization", organization, "address")
address = frappe.get_doc("Address", address) if address else None
address = frappe.get_cached_doc("Address", address) if address else None
if not address:
return None
return {
@ -188,6 +213,7 @@ def get_organization_address(organization):
"pincode": address.pincode,
}
def create_customer_in_erpnext(doc, method):
erpnext_crm_settings = frappe.get_single("ERPNext CRM Settings")
if (
@ -196,7 +222,7 @@ def create_customer_in_erpnext(doc, method):
or doc.status != erpnext_crm_settings.deal_status
):
return
contacts = get_contacts(doc)
address = get_organization_address(doc.organization)
customer = {
@ -213,26 +239,26 @@ def create_customer_in_erpnext(doc, method):
}
if not erpnext_crm_settings.is_erpnext_in_different_site:
from erpnext.crm.frappe_crm_api import create_customer
create_customer(customer)
else:
create_customer_in_remote_site(customer, erpnext_crm_settings)
frappe.publish_realtime("crm_customer_created")
def create_customer_in_remote_site(customer, erpnext_crm_settings):
client = get_erpnext_site_client(erpnext_crm_settings)
try:
client.post_api("erpnext.crm.frappe_crm_api.create_customer", customer)
except Exception:
frappe.log_error(
frappe.get_traceback(),
"Error while creating customer in remote site"
)
frappe.log_error(frappe.get_traceback(), "Error while creating customer in remote site")
frappe.throw(_("Error while creating customer in ERPNext, check error log for more details"))
@frappe.whitelist()
def get_crm_form_script():
return """
return """
async function setupForm({ doc, call, $dialog, updateField, createToast }) {
let actions = [];
let is_erpnext_integration_enabled = await call("frappe.client.get_single_value", {doctype: "ERPNext CRM Settings", field: "enabled"});

View File

@ -216,7 +216,7 @@ async function startCamera() {
}
function stopStream() {
stream.value.getTracks().forEach((track) => track.stop())
stream.value?.getTracks()?.forEach((track) => track.stop())
showCamera.value = false
cameraImage.value = null
}

View File

@ -85,9 +85,9 @@ async function createContact() {
delete _contact.value.email_id
}
if (_contact.value.actual_mobile_no) {
_contact.value.phone_nos = [{ phone: _contact.value.actual_mobile_no }]
delete _contact.value.actual_mobile_no
if (_contact.value.mobile_no) {
_contact.value.phone_nos = [{ phone: _contact.value.mobile_no }]
delete _contact.value.mobile_no
}
const doc = await call('frappe.client.insert', {

View File

@ -197,8 +197,9 @@ function createDeal() {
validate() {
error.value = null
if (deal.annual_revenue) {
deal.annual_revenue = deal.annual_revenue.replace(/,/g, '')
if (isNaN(deal.annual_revenue)) {
if (typeof deal.annual_revenue === 'string') {
deal.annual_revenue = deal.annual_revenue.replace(/,/g, '')
} else if (isNaN(deal.annual_revenue)) {
error.value = __('Annual Revenue should be a number')
return error.value
}

View File

@ -140,8 +140,9 @@ function createNewLead() {
return error.value
}
if (lead.annual_revenue) {
lead.annual_revenue = lead.annual_revenue.replace(/,/g, '')
if (isNaN(lead.annual_revenue)) {
if (typeof lead.annual_revenue === 'string') {
lead.annual_revenue = lead.annual_revenue.replace(/,/g, '')
} else if (isNaN(lead.annual_revenue)) {
error.value = __('Annual Revenue should be a number')
return error.value
}

View File

@ -15,40 +15,16 @@
:opened="section.opened"
>
<template v-if="!preview" #actions>
<div v-if="section.name == 'contacts_section'" class="pr-2">
<Link
value=""
doctype="Contact"
@change="(e) => addContact(e)"
:onCreate="
(value, close) => {
_contact = {
first_name: value,
company_name: deal.data.organization,
}
showContactModal = true
close()
}
"
<slot name="actions" v-bind="{ section }">
<Button
v-if="section.showEditButton"
variant="ghost"
class="w-7 mr-2"
@click="showSidePanelModal = true"
>
<template #target="{ togglePopover }">
<Button
class="h-7 px-3"
variant="ghost"
icon="plus"
@click="togglePopover()"
/>
</template>
</Link>
</div>
<Button
v-else-if="section.showEditButton"
variant="ghost"
class="w-7 mr-2"
@click="showSidePanelModal = true"
>
<EditIcon class="h-4 w-4" />
</Button>
<EditIcon class="h-4 w-4" />
</Button>
</slot>
</template>
<slot v-bind="{ section }">
<FadedScrollableDiv

View File

@ -431,13 +431,15 @@ function setup() {
callStatus.value = updateStatus(data)
const { user } = sessionStore()
if (
!showCallPopup.value &&
!showSmallCallPopup.value &&
data.AgentEmail &&
data.AgentEmail == (user || user.value)
) {
phoneNumber.value = data.CallTo || data.To
if (!showCallPopup.value && !showSmallCallPopup.value) {
if (data.AgentEmail && data.AgentEmail == (user || user.value)) {
// Incoming call
phoneNumber.value = data.CallFrom || data.From
} else {
// Outgoing call
phoneNumber.value = data.To
}
showCallPopup.value = true
}
})

View File

@ -461,7 +461,12 @@ const export_all = ref(false)
async function exportRows() {
let fields = JSON.stringify(list.value.data.columns.map((f) => f.key))
let filters = JSON.stringify(list.value.params.filters)
let filters = JSON.stringify({
...props.filters,
...list.value.params.filters,
})
let order_by = list.value.params.order_by
let page_length = list.value.params.page_length
if (export_all.value) {

View File

@ -125,102 +125,136 @@
:sections="sections.data"
:addContact="addContact"
doctype="CRM Deal"
v-slot="{ section }"
@update="updateField"
@reload="sections.reload"
>
<div v-if="section.name == 'contacts_section'" class="contacts-area">
<div
v-if="dealContacts?.loading && dealContacts?.data?.length == 0"
class="flex min-h-20 flex-1 items-center justify-center gap-3 text-base text-ink-gray-4"
>
<LoadingIndicator class="h-4 w-4" />
<span>{{ __('Loading...') }}</span>
<template #actions="{ section }">
<div v-if="section.name == 'contacts_section'" class="pr-2">
<Link
value=""
doctype="Contact"
@change="(e) => addContact(e)"
:onCreate="
(value, close) => {
_contact = {
first_name: value,
company_name: deal.data.organization,
}
showContactModal = true
close()
}
"
>
<template #target="{ togglePopover }">
<Button
class="h-7 px-3"
variant="ghost"
icon="plus"
@click="togglePopover()"
/>
</template>
</Link>
</div>
</template>
<template #default="{ section }">
<div
v-else-if="dealContacts?.data?.length"
v-for="(contact, i) in dealContacts.data"
:key="contact.name"
v-if="section.name == 'contacts_section'"
class="contacts-area"
>
<div class="px-2 pb-2.5" :class="[i == 0 ? 'pt-5' : 'pt-2.5']">
<Section :opened="contact.opened">
<template #header="{ opened, toggle }">
<div
class="flex cursor-pointer items-center justify-between gap-2 pr-1 text-base leading-5 text-ink-gray-7"
>
<div
class="flex h-7 items-center gap-2 truncate"
@click="toggle()"
>
<Avatar
:label="contact.full_name"
:image="contact.image"
size="md"
/>
<div class="truncate">
{{ contact.full_name }}
</div>
<Badge
v-if="contact.is_primary"
class="ml-2"
variant="outline"
:label="__('Primary')"
theme="green"
/>
</div>
<div class="flex items-center">
<Dropdown :options="contactOptions(contact)">
<Button
icon="more-horizontal"
class="text-ink-gray-5"
variant="ghost"
/>
</Dropdown>
<Button
variant="ghost"
@click="
router.push({
name: 'Contact',
params: { contactId: contact.name },
})
"
>
<ArrowUpRightIcon class="h-4 w-4" />
</Button>
<Button variant="ghost" @click="toggle()">
<FeatherIcon
name="chevron-right"
class="h-4 w-4 text-ink-gray-9 transition-all duration-300 ease-in-out"
:class="{ 'rotate-90': opened }"
/>
</Button>
</div>
</div>
</template>
<div class="flex flex-col gap-1.5 text-base text-ink-gray-8">
<div class="flex items-center gap-3 pb-1.5 pl-1 pt-4">
<Email2Icon class="h-4 w-4" />
{{ contact.email }}
</div>
<div class="flex items-center gap-3 p-1 py-1.5">
<PhoneIcon class="h-4 w-4" />
{{ contact.mobile_no }}
</div>
</div>
</Section>
<div
v-if="dealContacts?.loading && dealContacts?.data?.length == 0"
class="flex min-h-20 flex-1 items-center justify-center gap-3 text-base text-ink-gray-4"
>
<LoadingIndicator class="h-4 w-4" />
<span>{{ __('Loading...') }}</span>
</div>
<div
v-if="i != dealContacts.data.length - 1"
class="mx-2 h-px border-t border-outline-gray-modals"
/>
v-else-if="dealContacts?.data?.length"
v-for="(contact, i) in dealContacts.data"
:key="contact.name"
>
<div class="px-2 pb-2.5" :class="[i == 0 ? 'pt-5' : 'pt-2.5']">
<Section :opened="contact.opened">
<template #header="{ opened, toggle }">
<div
class="flex cursor-pointer items-center justify-between gap-2 pr-1 text-base leading-5 text-ink-gray-7"
>
<div
class="flex h-7 items-center gap-2 truncate"
@click="toggle()"
>
<Avatar
:label="contact.full_name"
:image="contact.image"
size="md"
/>
<div class="truncate">
{{ contact.full_name }}
</div>
<Badge
v-if="contact.is_primary"
class="ml-2"
variant="outline"
:label="__('Primary')"
theme="green"
/>
</div>
<div class="flex items-center">
<Dropdown :options="contactOptions(contact)">
<Button
icon="more-horizontal"
class="text-ink-gray-5"
variant="ghost"
/>
</Dropdown>
<Button
variant="ghost"
@click="
router.push({
name: 'Contact',
params: { contactId: contact.name },
})
"
>
<ArrowUpRightIcon class="h-4 w-4" />
</Button>
<Button variant="ghost" @click="toggle()">
<FeatherIcon
name="chevron-right"
class="h-4 w-4 text-ink-gray-9 transition-all duration-300 ease-in-out"
:class="{ 'rotate-90': opened }"
/>
</Button>
</div>
</div>
</template>
<div
class="flex flex-col gap-1.5 text-base text-ink-gray-8"
>
<div class="flex items-center gap-3 pb-1.5 pl-1 pt-4">
<Email2Icon class="h-4 w-4" />
{{ contact.email }}
</div>
<div class="flex items-center gap-3 p-1 py-1.5">
<PhoneIcon class="h-4 w-4" />
{{ contact.mobile_no }}
</div>
</div>
</Section>
</div>
<div
v-if="i != dealContacts.data.length - 1"
class="mx-2 h-px border-t border-outline-gray-modals"
/>
</div>
<div
v-else
class="flex h-20 items-center justify-center text-base text-ink-gray-5"
>
{{ __('No contacts added') }}
</div>
</div>
<div
v-else
class="flex h-20 items-center justify-center text-base text-ink-gray-5"
>
{{ __('No contacts added') }}
</div>
</div>
</template>
</SidePanelLayout>
</div>
</Resizer>
@ -278,6 +312,7 @@ import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
import AssignTo from '@/components/AssignTo.vue'
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
import ContactModal from '@/components/Modals/ContactModal.vue'
import Link from '@/components/Controls/Link.vue'
import Section from '@/components/Section.vue'
import SidePanelLayout from '@/components/SidePanelLayout.vue'
import SLASection from '@/components/SLASection.vue'
@ -571,6 +606,15 @@ function contactOptions(contact) {
}
async function addContact(contact) {
if (dealContacts.data?.find((c) => c.name === contact)) {
createToast({
title: __('Contact already added'),
icon: 'x',
iconClasses: 'text-ink-red-3',
})
return
}
let d = await call('crm.fcrm.doctype.crm_deal.crm_deal.add_contact', {
deal: props.dealId,
contact,

View File

@ -65,112 +65,141 @@
v-model="deal.data"
:sections="sections.data"
doctype="CRM Deal"
v-slot="{ section }"
@update="updateField"
@reload="sections.reload"
>
<div
v-if="section.name == 'contacts_section'"
class="contacts-area"
>
<div
v-if="
dealContacts?.loading && dealContacts?.data?.length == 0
"
class="flex min-h-20 flex-1 items-center justify-center gap-3 text-base text-ink-gray-4"
>
<LoadingIndicator class="h-4 w-4" />
<span>{{ __('Loading...') }}</span>
<template #actions="{ section }">
<div v-if="section.name == 'contacts_section'" class="pr-2">
<Link
value=""
doctype="Contact"
@change="(e) => addContact(e)"
:onCreate="
(value, close) => {
_contact = {
first_name: value,
company_name: deal.data.organization,
}
showContactModal = true
close()
}
"
>
<template #target="{ togglePopover }">
<Button
class="h-7 px-3"
variant="ghost"
icon="plus"
@click="togglePopover()"
/>
</template>
</Link>
</div>
</template>
<template #default="{ section }">
<div
v-else-if="section.contacts.length"
v-for="(contact, i) in section.contacts"
:key="contact.name"
v-if="section.name == 'contacts_section'"
class="contacts-area"
>
<div
class="px-2 pb-2.5"
:class="[i == 0 ? 'pt-5' : 'pt-2.5']"
v-if="
dealContacts?.loading && dealContacts?.data?.length == 0
"
class="flex min-h-20 flex-1 items-center justify-center gap-3 text-base text-ink-gray-4"
>
<Section :opened="contact.opened">
<template #header="{ opened, toggle }">
<div
class="flex cursor-pointer items-center justify-between gap-2 pr-1 text-base leading-5 text-ink-gray-7"
>
<div
class="flex h-7 items-center gap-2 truncate"
@click="toggle()"
>
<Avatar
:label="contact.full_name"
:image="contact.image"
size="md"
/>
<div class="truncate">
{{ contact.full_name }}
</div>
<Badge
v-if="contact.is_primary"
class="ml-2"
variant="outline"
:label="__('Primary')"
theme="green"
/>
</div>
<div class="flex items-center">
<Dropdown :options="contactOptions(contact.name)">
<Button
icon="more-horizontal"
class="text-ink-gray-5"
variant="ghost"
/>
</Dropdown>
<Button
variant="ghost"
@click="
router.push({
name: 'Contact',
params: { contactId: contact.name },
})
"
>
<ArrowUpRightIcon class="h-4 w-4" />
</Button>
<Button variant="ghost" @click="toggle()">
<FeatherIcon
name="chevron-right"
class="h-4 w-4 text-ink-gray-9 transition-all duration-300 ease-in-out"
:class="{ 'rotate-90': opened }"
/>
</Button>
</div>
</div>
</template>
<div
class="flex flex-col gap-1.5 text-base text-ink-gray-8"
>
<div class="flex items-center gap-3 pb-1.5 pl-1 pt-4">
<Email2Icon class="h-4 w-4" />
{{ contact.email }}
</div>
<div class="flex items-center gap-3 p-1 py-1.5">
<PhoneIcon class="h-4 w-4" />
{{ contact.mobile_no }}
</div>
</div>
</Section>
<LoadingIndicator class="h-4 w-4" />
<span>{{ __('Loading...') }}</span>
</div>
<div
v-if="i != section.contacts.length - 1"
class="mx-2 h-px border-t border-outline-gray-modals"
/>
v-else-if="section.contacts.length"
v-for="(contact, i) in section.contacts"
:key="contact.name"
>
<div
class="px-2 pb-2.5"
:class="[i == 0 ? 'pt-5' : 'pt-2.5']"
>
<Section :opened="contact.opened">
<template #header="{ opened, toggle }">
<div
class="flex cursor-pointer items-center justify-between gap-2 pr-1 text-base leading-5 text-ink-gray-7"
>
<div
class="flex h-7 items-center gap-2 truncate"
@click="toggle()"
>
<Avatar
:label="contact.full_name"
:image="contact.image"
size="md"
/>
<div class="truncate">
{{ contact.full_name }}
</div>
<Badge
v-if="contact.is_primary"
class="ml-2"
variant="outline"
:label="__('Primary')"
theme="green"
/>
</div>
<div class="flex items-center">
<Dropdown :options="contactOptions(contact.name)">
<Button
icon="more-horizontal"
class="text-ink-gray-5"
variant="ghost"
/>
</Dropdown>
<Button
variant="ghost"
@click="
router.push({
name: 'Contact',
params: { contactId: contact.name },
})
"
>
<ArrowUpRightIcon class="h-4 w-4" />
</Button>
<Button variant="ghost" @click="toggle()">
<FeatherIcon
name="chevron-right"
class="h-4 w-4 text-ink-gray-9 transition-all duration-300 ease-in-out"
:class="{ 'rotate-90': opened }"
/>
</Button>
</div>
</div>
</template>
<div
class="flex flex-col gap-1.5 text-base text-ink-gray-8"
>
<div class="flex items-center gap-3 pb-1.5 pl-1 pt-4">
<Email2Icon class="h-4 w-4" />
{{ contact.email }}
</div>
<div class="flex items-center gap-3 p-1 py-1.5">
<PhoneIcon class="h-4 w-4" />
{{ contact.mobile_no }}
</div>
</div>
</Section>
</div>
<div
v-if="i != section.contacts.length - 1"
class="mx-2 h-px border-t border-outline-gray-modals"
/>
</div>
<div
v-else
class="flex h-20 items-center justify-center text-base text-ink-gray-5"
>
{{ __('No contacts added') }}
</div>
</div>
<div
v-else
class="flex h-20 items-center justify-center text-base text-ink-gray-5"
>
{{ __('No contacts added') }}
</div>
</div>
</template>
</SidePanelLayout>
</div>
</div>
@ -224,6 +253,7 @@ import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
import AssignTo from '@/components/AssignTo.vue'
import ContactModal from '@/components/Modals/ContactModal.vue'
import Section from '@/components/Section.vue'
import Link from '@/components/Controls/Link.vue'
import SidePanelLayout from '@/components/SidePanelLayout.vue'
import SLASection from '@/components/SLASection.vue'
import CustomActions from '@/components/CustomActions.vue'
@ -504,6 +534,15 @@ function contactOptions(contact) {
}
async function addContact(contact) {
if (dealContacts.data?.find((c) => c.name === contact)) {
createToast({
title: __('Contact already added'),
icon: 'x',
iconClasses: 'text-ink-red-3',
})
return
}
let d = await call('crm.fcrm.doctype.crm_deal.crm_deal.add_contact', {
deal: props.dealId,
contact,