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"): if hasattr(_list, "default_list_data"):
default_rows = _list.default_list_data().get("rows") default_rows = _list.default_list_data().get("rows")
meta = frappe.get_meta(doctype)
if view_type != "kanban": if view_type != "kanban":
if columns or rows: if columns or rows:
custom_view = True custom_view = True
@ -296,6 +298,11 @@ def get_data(
if column.get("key") == "_liked_by" and column.get("width") == "10rem": if column.get("key") == "_liked_by" and column.get("width") == "10rem":
column["width"] = "50px" 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 # check if rows has group_by_field if not add it
if group_by_field and group_by_field not in rows: if group_by_field and group_by_field not in rows:
rows.append(group_by_field) rows.append(group_by_field)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -125,102 +125,136 @@
:sections="sections.data" :sections="sections.data"
:addContact="addContact" :addContact="addContact"
doctype="CRM Deal" doctype="CRM Deal"
v-slot="{ section }"
@update="updateField" @update="updateField"
@reload="sections.reload" @reload="sections.reload"
> >
<div v-if="section.name == 'contacts_section'" class="contacts-area"> <template #actions="{ section }">
<div <div v-if="section.name == 'contacts_section'" class="pr-2">
v-if="dealContacts?.loading && dealContacts?.data?.length == 0" <Link
class="flex min-h-20 flex-1 items-center justify-center gap-3 text-base text-ink-gray-4" value=""
> doctype="Contact"
<LoadingIndicator class="h-4 w-4" /> @change="(e) => addContact(e)"
<span>{{ __('Loading...') }}</span> :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> </div>
</template>
<template #default="{ section }">
<div <div
v-else-if="dealContacts?.data?.length" v-if="section.name == 'contacts_section'"
v-for="(contact, i) in dealContacts.data" class="contacts-area"
:key="contact.name"
> >
<div class="px-2 pb-2.5" :class="[i == 0 ? 'pt-5' : 'pt-2.5']"> <div
<Section :opened="contact.opened"> v-if="dealContacts?.loading && dealContacts?.data?.length == 0"
<template #header="{ opened, toggle }"> class="flex min-h-20 flex-1 items-center justify-center gap-3 text-base text-ink-gray-4"
<div >
class="flex cursor-pointer items-center justify-between gap-2 pr-1 text-base leading-5 text-ink-gray-7" <LoadingIndicator class="h-4 w-4" />
> <span>{{ __('Loading...') }}</span>
<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>
<div <div
v-if="i != dealContacts.data.length - 1" v-else-if="dealContacts?.data?.length"
class="mx-2 h-px border-t border-outline-gray-modals" 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>
<div </template>
v-else
class="flex h-20 items-center justify-center text-base text-ink-gray-5"
>
{{ __('No contacts added') }}
</div>
</div>
</SidePanelLayout> </SidePanelLayout>
</div> </div>
</Resizer> </Resizer>
@ -278,6 +312,7 @@ import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
import AssignTo from '@/components/AssignTo.vue' import AssignTo from '@/components/AssignTo.vue'
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue' import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
import ContactModal from '@/components/Modals/ContactModal.vue' import ContactModal from '@/components/Modals/ContactModal.vue'
import Link from '@/components/Controls/Link.vue'
import Section from '@/components/Section.vue' import Section from '@/components/Section.vue'
import SidePanelLayout from '@/components/SidePanelLayout.vue' import SidePanelLayout from '@/components/SidePanelLayout.vue'
import SLASection from '@/components/SLASection.vue' import SLASection from '@/components/SLASection.vue'
@ -571,6 +606,15 @@ function contactOptions(contact) {
} }
async function addContact(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', { let d = await call('crm.fcrm.doctype.crm_deal.crm_deal.add_contact', {
deal: props.dealId, deal: props.dealId,
contact, contact,

View File

@ -65,112 +65,141 @@
v-model="deal.data" v-model="deal.data"
:sections="sections.data" :sections="sections.data"
doctype="CRM Deal" doctype="CRM Deal"
v-slot="{ section }"
@update="updateField" @update="updateField"
@reload="sections.reload" @reload="sections.reload"
> >
<div <template #actions="{ section }">
v-if="section.name == 'contacts_section'" <div v-if="section.name == 'contacts_section'" class="pr-2">
class="contacts-area" <Link
> value=""
<div doctype="Contact"
v-if=" @change="(e) => addContact(e)"
dealContacts?.loading && dealContacts?.data?.length == 0 :onCreate="
" (value, close) => {
class="flex min-h-20 flex-1 items-center justify-center gap-3 text-base text-ink-gray-4" _contact = {
> first_name: value,
<LoadingIndicator class="h-4 w-4" /> company_name: deal.data.organization,
<span>{{ __('Loading...') }}</span> }
showContactModal = true
close()
}
"
>
<template #target="{ togglePopover }">
<Button
class="h-7 px-3"
variant="ghost"
icon="plus"
@click="togglePopover()"
/>
</template>
</Link>
</div> </div>
</template>
<template #default="{ section }">
<div <div
v-else-if="section.contacts.length" v-if="section.name == 'contacts_section'"
v-for="(contact, i) in section.contacts" class="contacts-area"
:key="contact.name"
> >
<div <div
class="px-2 pb-2.5" v-if="
:class="[i == 0 ? 'pt-5' : 'pt-2.5']" 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"> <LoadingIndicator class="h-4 w-4" />
<template #header="{ opened, toggle }"> <span>{{ __('Loading...') }}</span>
<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>
<div <div
v-if="i != section.contacts.length - 1" v-else-if="section.contacts.length"
class="mx-2 h-px border-t border-outline-gray-modals" 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>
<div </template>
v-else
class="flex h-20 items-center justify-center text-base text-ink-gray-5"
>
{{ __('No contacts added') }}
</div>
</div>
</SidePanelLayout> </SidePanelLayout>
</div> </div>
</div> </div>
@ -224,6 +253,7 @@ import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
import AssignTo from '@/components/AssignTo.vue' import AssignTo from '@/components/AssignTo.vue'
import ContactModal from '@/components/Modals/ContactModal.vue' import ContactModal from '@/components/Modals/ContactModal.vue'
import Section from '@/components/Section.vue' import Section from '@/components/Section.vue'
import Link from '@/components/Controls/Link.vue'
import SidePanelLayout from '@/components/SidePanelLayout.vue' import SidePanelLayout from '@/components/SidePanelLayout.vue'
import SLASection from '@/components/SLASection.vue' import SLASection from '@/components/SLASection.vue'
import CustomActions from '@/components/CustomActions.vue' import CustomActions from '@/components/CustomActions.vue'
@ -504,6 +534,15 @@ function contactOptions(contact) {
} }
async function addContact(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', { let d = await call('crm.fcrm.doctype.crm_deal.crm_deal.add_contact', {
deal: props.dealId, deal: props.dealId,
contact, contact,