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"):
|
||||
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)
|
||||
|
||||
@ -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"});
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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', {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user