Merge branch 'develop' into fix-exotel-call-ui
This commit is contained in:
commit
9f321929ad
@ -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)
|
||||||
|
|||||||
@ -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"});
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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', {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user