Merge pull request #228 from frappe/develop
chore: Merge develop to main
This commit is contained in:
commit
9a01517d6f
@ -73,6 +73,7 @@ def get_linked_deals(contact):
|
|||||||
fields=[
|
fields=[
|
||||||
"name",
|
"name",
|
||||||
"organization",
|
"organization",
|
||||||
|
"currency",
|
||||||
"annual_revenue",
|
"annual_revenue",
|
||||||
"status",
|
"status",
|
||||||
"email",
|
"email",
|
||||||
|
|||||||
@ -257,17 +257,18 @@ def get_list_data(
|
|||||||
"user": frappe.session.user,
|
"user": frappe.session.user,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_list = get_controller(doctype)
|
||||||
|
|
||||||
if not custom_view and frappe.db.exists("CRM View Settings", default_view_filters):
|
if not custom_view and frappe.db.exists("CRM View Settings", default_view_filters):
|
||||||
list_view_settings = frappe.get_doc("CRM View Settings", default_view_filters)
|
list_view_settings = frappe.get_doc("CRM View Settings", default_view_filters)
|
||||||
columns = frappe.parse_json(list_view_settings.columns)
|
columns = frappe.parse_json(list_view_settings.columns)
|
||||||
rows = frappe.parse_json(list_view_settings.rows)
|
rows = frappe.parse_json(list_view_settings.rows)
|
||||||
is_default = False
|
is_default = False
|
||||||
elif not custom_view or is_default:
|
elif not custom_view or is_default and hasattr(_list, "default_list_data"):
|
||||||
_list = get_controller(doctype)
|
columns = _list.default_list_data().get("columns")
|
||||||
|
|
||||||
if hasattr(_list, "default_list_data"):
|
if hasattr(_list, "default_list_data"):
|
||||||
columns = _list.default_list_data().get("columns")
|
rows = _list.default_list_data().get("rows")
|
||||||
rows = _list.default_list_data().get("rows")
|
|
||||||
|
|
||||||
# check if rows has all keys from columns if not add them
|
# check if rows has all keys from columns if not add them
|
||||||
for column in columns:
|
for column in columns:
|
||||||
|
|||||||
@ -8,31 +8,41 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"organization_tab",
|
"organization_tab",
|
||||||
|
"naming_series",
|
||||||
"organization",
|
"organization",
|
||||||
"website",
|
|
||||||
"territory",
|
|
||||||
"annual_revenue",
|
|
||||||
"close_date",
|
|
||||||
"probability",
|
|
||||||
"next_step",
|
"next_step",
|
||||||
|
"probability",
|
||||||
|
"column_break_ijan",
|
||||||
|
"status",
|
||||||
|
"close_date",
|
||||||
"deal_owner",
|
"deal_owner",
|
||||||
"contacts_tab",
|
"contacts_tab",
|
||||||
|
"contacts",
|
||||||
|
"contact",
|
||||||
|
"lead_details_tab",
|
||||||
|
"lead",
|
||||||
|
"source",
|
||||||
|
"column_break_wsde",
|
||||||
"lead_name",
|
"lead_name",
|
||||||
|
"organization_details_section",
|
||||||
|
"organization_name",
|
||||||
|
"website",
|
||||||
|
"no_of_employees",
|
||||||
|
"job_title",
|
||||||
|
"column_break_xbyf",
|
||||||
|
"territory",
|
||||||
|
"currency",
|
||||||
|
"annual_revenue",
|
||||||
|
"industry",
|
||||||
|
"person_section",
|
||||||
|
"salutation",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"column_break_xjmy",
|
||||||
"email",
|
"email",
|
||||||
"mobile_no",
|
"mobile_no",
|
||||||
"phone",
|
"phone",
|
||||||
"contacts",
|
"gender",
|
||||||
"others_tab",
|
|
||||||
"naming_series",
|
|
||||||
"status",
|
|
||||||
"section_break_sygz",
|
|
||||||
"no_of_employees",
|
|
||||||
"column_break_nwob",
|
|
||||||
"job_title",
|
|
||||||
"section_break_eepu",
|
|
||||||
"lead",
|
|
||||||
"column_break_bqvs",
|
|
||||||
"source",
|
|
||||||
"sla_tab",
|
"sla_tab",
|
||||||
"sla",
|
"sla",
|
||||||
"sla_creation",
|
"sla_creation",
|
||||||
@ -63,7 +73,8 @@
|
|||||||
"fetch_from": ".annual_revenue",
|
"fetch_from": ".annual_revenue",
|
||||||
"fieldname": "annual_revenue",
|
"fieldname": "annual_revenue",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Amount"
|
"label": "Amount",
|
||||||
|
"options": "currency"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": ".website",
|
"fetch_from": ".website",
|
||||||
@ -88,14 +99,6 @@
|
|||||||
"label": "Lead",
|
"label": "Lead",
|
||||||
"options": "CRM Lead"
|
"options": "CRM Lead"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_eepu",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_bqvs",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "deal_owner",
|
"fieldname": "deal_owner",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@ -142,12 +145,6 @@
|
|||||||
"label": "Contacts",
|
"label": "Contacts",
|
||||||
"options": "CRM Contacts"
|
"options": "CRM Contacts"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "others_tab",
|
|
||||||
"fieldtype": "Tab Break",
|
|
||||||
"label": "Others",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "organization_tab",
|
"fieldname": "organization_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
@ -235,14 +232,6 @@
|
|||||||
"label": "No. of Employees",
|
"label": "No. of Employees",
|
||||||
"options": "1-10\n11-50\n51-200\n201-500\n501-1000\n1000+"
|
"options": "1-10\n11-50\n51-200\n201-500\n501-1000\n1000+"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_sygz",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_nwob",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "job_title",
|
"fieldname": "job_title",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
@ -270,11 +259,87 @@
|
|||||||
"fieldname": "lead_name",
|
"fieldname": "lead_name",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Lead Name"
|
"label": "Lead Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_ijan",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "lead_details_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Lead Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_wsde",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "organization_details_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Organization Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "organization_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Organization Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_xbyf",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "industry",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Industry",
|
||||||
|
"options": "CRM Industry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "person_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Person"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "salutation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Salutation",
|
||||||
|
"options": "Salutation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "first_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "First Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "last_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Last Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_xjmy",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "gender",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Gender",
|
||||||
|
"options": "Gender"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "contact",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Contact",
|
||||||
|
"options": "Contact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "currency",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Currency",
|
||||||
|
"options": "Currency"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-25 21:00:08.216020",
|
"modified": "2024-06-20 12:55:41.602364",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Deal",
|
"name": "CRM Deal",
|
||||||
|
|||||||
@ -178,6 +178,7 @@ class CRMDeal(Document):
|
|||||||
"annual_revenue",
|
"annual_revenue",
|
||||||
"status",
|
"status",
|
||||||
"email",
|
"email",
|
||||||
|
"currency",
|
||||||
"mobile_no",
|
"mobile_no",
|
||||||
"deal_owner",
|
"deal_owner",
|
||||||
"sla_status",
|
"sla_status",
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"organization_name",
|
"organization_name",
|
||||||
"no_of_employees",
|
"no_of_employees",
|
||||||
|
"currency",
|
||||||
"annual_revenue",
|
"annual_revenue",
|
||||||
"organization_logo",
|
"organization_logo",
|
||||||
"column_break_pnpp",
|
"column_break_pnpp",
|
||||||
@ -47,7 +48,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "annual_revenue",
|
"fieldname": "annual_revenue",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Annual Revenue"
|
"label": "Annual Revenue",
|
||||||
|
"options": "currency"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "industry",
|
"fieldname": "industry",
|
||||||
@ -60,12 +62,18 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Territory",
|
"label": "Territory",
|
||||||
"options": "CRM Territory"
|
"options": "CRM Territory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "currency",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Currency",
|
||||||
|
"options": "Currency"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image_field": "organization_logo",
|
"image_field": "organization_logo",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-19 21:53:14.945857",
|
"modified": "2024-06-20 12:59:55.297752",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Organization",
|
"name": "CRM Organization",
|
||||||
|
|||||||
@ -47,6 +47,7 @@ class CRMOrganization(Document):
|
|||||||
"organization_logo",
|
"organization_logo",
|
||||||
"website",
|
"website",
|
||||||
"industry",
|
"industry",
|
||||||
|
"currency",
|
||||||
"annual_revenue",
|
"annual_revenue",
|
||||||
"modified",
|
"modified",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -113,19 +113,19 @@ def add_default_fields_layout():
|
|||||||
layouts = {
|
layouts = {
|
||||||
"CRM Lead-Quick Entry": {
|
"CRM Lead-Quick Entry": {
|
||||||
"doctype": "CRM Lead",
|
"doctype": "CRM Lead",
|
||||||
"layout": '[\n{\n"label": "Person",\n\t"fields": ["salutation", "first_name", "last_name", "email", "mobile_no", "gender"]\n},\n{\n"label": "Organization",\n\t"fields": ["organization", "website", "no_of_employees", "territory", "annual_revenue", "industry"]\n},\n{\n"label": "Other",\n"columns": 2,\n\t"fields": ["status", "lead_owner"]\n}\n]'
|
"layout": '[{"label":"Person","fields":["salutation","first_name","last_name","email","mobile_no", "gender"],"hideLabel":true},{"label":"Organization","fields":["organization","website","no_of_employees","territory","annual_revenue","industry"],"hideLabel":true,"hideBorder":false},{"label":"Other","columns":2,"fields":["status","lead_owner"],"hideLabel":true,"hideBorder":false}]'
|
||||||
},
|
},
|
||||||
"CRM Deal-Quick Entry": {
|
"CRM Deal-Quick Entry": {
|
||||||
"doctype": "CRM Deal",
|
"doctype": "CRM Deal",
|
||||||
"layout": '[\n{\n"label": "Select Organization",\n\t"fields": ["organization"]\n},\n{\n"label": "Organization Details",\n\t"fields": [{"label": "Organization Name", "name": "organization_name", "type": "Data"}, "website", "no_of_employees", "territory", "annual_revenue", {"label": "Industry", "name": "industry", "type": "Link", "options": "CRM Industry"}]\n},\n{\n"label": "Select Contact",\n\t"fields": [{"label": "Contact", "name": "contact", "type": "Link", "options": "Contact"}]\n},\n{\n"label": "Contact Details",\n\t"fields": [{"label": "Salutation", "name": "salutation", "type": "Link", "options": "Salutation"}, {"label": "First Name", "name": "first_name", "type": "Data"}, {"label": "Last Name", "name": "last_name", "type": "Data"}, "email", "mobile_no", {"label": "Gender", "name": "gender", "type": "Link", "options": "Gender"}]\n},\n{\n"label": "Other",\n"columns": 2,\n\t"fields": ["status", "deal_owner"]\n}\n]'
|
"layout": '[{"label": "Select Organization", "fields": ["organization"], "hideLabel": true, "editable": true}, {"label": "Organization Details", "fields": ["organization_name", "website", "no_of_employees", "territory", "annual_revenue", "industry"], "hideLabel": true, "editable": true}, {"label": "Select Contact", "fields": ["contact"], "hideLabel": true, "editable": true}, {"label": "Contact Details", "fields": ["salutation", "first_name", "last_name", "email", "mobile_no", "gender"], "hideLabel": true, "editable": true}, {"label": "Other", "columns": 2, "fields": ["status", "deal_owner"], "hideLabel": true}]'
|
||||||
},
|
},
|
||||||
"Contact-Quick Entry": {
|
"Contact-Quick Entry": {
|
||||||
"doctype": "Contact",
|
"doctype": "Contact",
|
||||||
"layout": '[\n{\n"label": "Salutation",\n"columns": 1,\n"fields": ["salutation"]\n},\n{\n"label": "Full Name",\n"columns": 2,\n"hideBorder": true,\n"fields": ["first_name", "last_name"]\n},\n{\n"label": "Email",\n"columns": 1,\n"hideBorder": true,\n"fields": ["email_id"]\n},\n{\n"label": "Mobile No. & Gender",\n"columns": 2,\n"hideBorder": true,\n"fields": ["mobile_no", "gender"]\n},\n{\n"label": "Organization",\n"columns": 1,\n"hideBorder": true,\n"fields": ["company_name"]\n},\n{\n"label": "Designation",\n"columns": 1,\n"hideBorder": true,\n"fields": ["designation"]\n}\n]'
|
"layout": '[{"label":"Salutation","columns":1,"fields":["salutation"],"hideLabel":true},{"label":"Full Name","columns":2,"hideBorder":true,"fields":["first_name","last_name"],"hideLabel":true},{"label":"Email","columns":1,"hideBorder":true,"fields":["email_id"],"hideLabel":true},{"label":"Mobile No. & Gender","columns":2,"hideBorder":true,"fields":["mobile_no","gender"],"hideLabel":true},{"label":"Organization","columns":1,"hideBorder":true,"fields":["company_name"],"hideLabel":true},{"label":"Designation","columns":1,"hideBorder":true,"fields":["designation"],"hideLabel":true}]'
|
||||||
},
|
},
|
||||||
"Organization-Quick Entry": {
|
"Organization-Quick Entry": {
|
||||||
"doctype": "CRM Organization",
|
"doctype": "CRM Organization",
|
||||||
"layout": '[\n{\n"label": "Organization Name",\n"columns": 1,\n"fields": ["organization_name"]\n},\n{\n"label": "Website & Revenue",\n"columns": 2,\n"hideBorder": true,\n"fields": ["website", "annual_revenue"]\n},\n{\n"label": "Territory",\n"columns": 1,\n"hideBorder": true,\n"fields": ["territory"]\n},\n{\n"label": "No of Employees & Industry",\n"columns": 2,\n"hideBorder": true,\n"fields": ["no_of_employees", "industry"]\n}\n]'
|
"layout": '[{"label":"Organization Name","columns":1,"fields":["organization_name"],"hideLabel":true},{"label":"Website & Revenue","columns":2,"hideBorder":true,"fields":["website","annual_revenue"],"hideLabel":true},{"label":"Territory","columns":1,"hideBorder":true,"fields":["territory"],"hideLabel":true},{"label":"No of Employees & Industry","columns":2,"hideBorder":true,"fields":["no_of_employees","industry"],"hideLabel":true}]'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,4 +7,5 @@ crm.patches.v1_0.move_crm_note_data_to_fcrm_note
|
|||||||
# Patches added in this section will be executed after doctypes are migrated
|
# Patches added in this section will be executed after doctypes are migrated
|
||||||
crm.patches.v1_0.create_email_template_custom_fields
|
crm.patches.v1_0.create_email_template_custom_fields
|
||||||
crm.patches.v1_0.create_default_fields_layout
|
crm.patches.v1_0.create_default_fields_layout
|
||||||
crm.patches.v1_0.create_default_sidebar_fields_layout
|
crm.patches.v1_0.create_default_sidebar_fields_layout
|
||||||
|
crm.patches.v1_0.update_deal_quick_entry_layout
|
||||||
15
crm/patches/v1_0/update_deal_quick_entry_layout.py
Normal file
15
crm/patches/v1_0/update_deal_quick_entry_layout.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import json
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
if not frappe.db.exists("CRM Fields Layout", "CRM Deal-Quick Entry"):
|
||||||
|
return
|
||||||
|
|
||||||
|
deal = frappe.db.get_value("CRM Fields Layout", "CRM Deal-Quick Entry", "layout")
|
||||||
|
|
||||||
|
layout = json.loads(deal)
|
||||||
|
for section in layout:
|
||||||
|
if section.get("label") in ["Select Organization", "Organization Details", "Select Contact", "Contact Details"]:
|
||||||
|
section["editable"] = False
|
||||||
|
|
||||||
|
frappe.db.set_value("CRM Fields Layout", "CRM Deal-Quick Entry", "layout", json.dumps(layout))
|
||||||
@ -6,6 +6,12 @@
|
|||||||
class="first:border-t-0 first:pt-0"
|
class="first:border-t-0 first:pt-0"
|
||||||
:class="section.hideBorder ? '' : 'border-t pt-4'"
|
:class="section.hideBorder ? '' : 'border-t pt-4'"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="!section.hideLabel"
|
||||||
|
class="flex h-7 mb-3 max-w-fit cursor-pointer items-center gap-2 text-base font-semibold leading-5"
|
||||||
|
>
|
||||||
|
{{ section.label }}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="grid gap-4"
|
class="grid gap-4"
|
||||||
:class="
|
:class="
|
||||||
|
|||||||
18
frontend/src/components/Icons/MoneyIcon.vue
Normal file
18
frontend/src/components/Icons/MoneyIcon.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-circle-dollar-sign"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8" />
|
||||||
|
<path d="M12 18V6" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
18
frontend/src/components/Icons/RightSideLayoutIcon.vue
Normal file
18
frontend/src/components/Icons/RightSideLayoutIcon.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-panel-right-close"
|
||||||
|
>
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
<path d="M15 3v18" />
|
||||||
|
<path d="m8 9 3 3-3 3" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@ -147,15 +147,15 @@ const dealStatuses = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function createDeal() {
|
function createDeal() {
|
||||||
|
if (deal.website && !deal.website.startsWith('http')) {
|
||||||
|
deal.website = 'https://' + deal.website
|
||||||
|
}
|
||||||
createResource({
|
createResource({
|
||||||
url: 'crm.fcrm.doctype.crm_deal.crm_deal.create_deal',
|
url: 'crm.fcrm.doctype.crm_deal.crm_deal.create_deal',
|
||||||
params: { args: deal },
|
params: { args: deal },
|
||||||
auto: true,
|
auto: true,
|
||||||
validate() {
|
validate() {
|
||||||
error.value = null
|
error.value = null
|
||||||
if (deal.website && !deal.website.startsWith('http')) {
|
|
||||||
deal.website = 'https://' + deal.website
|
|
||||||
}
|
|
||||||
if (deal.annual_revenue) {
|
if (deal.annual_revenue) {
|
||||||
deal.annual_revenue = deal.annual_revenue.replace(/,/g, '')
|
deal.annual_revenue = deal.annual_revenue.replace(/,/g, '')
|
||||||
if (isNaN(deal.annual_revenue)) {
|
if (isNaN(deal.annual_revenue)) {
|
||||||
@ -182,6 +182,14 @@ function createDeal() {
|
|||||||
show.value = false
|
show.value = false
|
||||||
router.push({ name: 'Deal', params: { dealId: name } })
|
router.push({ name: 'Deal', params: { dealId: name } })
|
||||||
},
|
},
|
||||||
|
onError(err) {
|
||||||
|
isDealCreating.value = false
|
||||||
|
if (!err.messages) {
|
||||||
|
error.value = err.message
|
||||||
|
return
|
||||||
|
}
|
||||||
|
error.value = err.messages.join('\n')
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -97,6 +97,10 @@ const leadStatuses = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function createNewLead() {
|
function createNewLead() {
|
||||||
|
if (lead.website && !lead.website.startsWith('http')) {
|
||||||
|
lead.website = 'https://' + lead.website
|
||||||
|
}
|
||||||
|
|
||||||
createLead.submit(lead, {
|
createLead.submit(lead, {
|
||||||
validate() {
|
validate() {
|
||||||
error.value = null
|
error.value = null
|
||||||
@ -104,9 +108,6 @@ function createNewLead() {
|
|||||||
error.value = __('First Name is mandatory')
|
error.value = __('First Name is mandatory')
|
||||||
return error.value
|
return error.value
|
||||||
}
|
}
|
||||||
if (lead.website && !lead.website.startsWith('http')) {
|
|
||||||
lead.website = 'https://' + lead.website
|
|
||||||
}
|
|
||||||
if (lead.annual_revenue) {
|
if (lead.annual_revenue) {
|
||||||
lead.annual_revenue = lead.annual_revenue.replace(/,/g, '')
|
lead.annual_revenue = lead.annual_revenue.replace(/,/g, '')
|
||||||
if (isNaN(lead.annual_revenue)) {
|
if (isNaN(lead.annual_revenue)) {
|
||||||
@ -133,6 +134,14 @@ function createNewLead() {
|
|||||||
show.value = false
|
show.value = false
|
||||||
router.push({ name: 'Lead', params: { leadId: data.name } })
|
router.push({ name: 'Lead', params: { leadId: data.name } })
|
||||||
},
|
},
|
||||||
|
onError(err) {
|
||||||
|
isLeadCreating.value = false
|
||||||
|
if (!err.messages) {
|
||||||
|
error.value = err.message
|
||||||
|
return
|
||||||
|
}
|
||||||
|
error.value = err.messages.join('\n')
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -61,9 +61,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Fields from '@/components/Fields.vue'
|
import Fields from '@/components/Fields.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
|
import MoneyIcon from '@/components/Icons/MoneyIcon.vue'
|
||||||
import WebsiteIcon from '@/components/Icons/WebsiteIcon.vue'
|
import WebsiteIcon from '@/components/Icons/WebsiteIcon.vue'
|
||||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||||
import TerritoryIcon from '@/components/Icons/TerritoryIcon.vue'
|
import TerritoryIcon from '@/components/Icons/TerritoryIcon.vue'
|
||||||
|
import { formatNumberIntoCurrency } from '@/utils'
|
||||||
import { call, FeatherIcon, createResource } from 'frappe-ui'
|
import { call, FeatherIcon, createResource } from 'frappe-ui'
|
||||||
import { ref, nextTick, watch, computed, h } from 'vue'
|
import { ref, nextTick, watch, computed, h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@ -205,9 +207,12 @@ const fields = computed(() => {
|
|||||||
value: _organization.value.territory,
|
value: _organization.value.territory,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: h(FeatherIcon, { name: 'dollar-sign', class: 'h-4 w-4' }),
|
icon: MoneyIcon,
|
||||||
name: 'annual_revenue',
|
name: 'annual_revenue',
|
||||||
value: _organization.value.annual_revenue,
|
value: formatNumberIntoCurrency(
|
||||||
|
_organization.value.annual_revenue,
|
||||||
|
_organization.value.currency,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: h(FeatherIcon, { name: 'hash', class: 'h-4 w-4' }),
|
icon: h(FeatherIcon, { name: 'hash', class: 'h-4 w-4' }),
|
||||||
@ -227,7 +232,7 @@ const fields = computed(() => {
|
|||||||
const sections = createResource({
|
const sections = createResource({
|
||||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
cache: ['quickEntryFields', 'CRM Organization'],
|
cache: ['quickEntryFields', 'CRM Organization'],
|
||||||
params: { doctype: 'CRM Organization', type: 'Quick Entry'},
|
params: { doctype: 'CRM Organization', type: 'Quick Entry' },
|
||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -246,6 +251,6 @@ watch(
|
|||||||
editMode.value = true
|
editMode.value = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -36,7 +36,7 @@
|
|||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="
|
v-else-if="
|
||||||
['email', 'number', 'date', 'password', 'textarea'].includes(
|
['email', 'number', 'date', 'password', 'textarea'].includes(
|
||||||
field.type
|
field.type,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@ -137,7 +137,10 @@ const _fields = computed(() => {
|
|||||||
props.fields?.forEach((field) => {
|
props.fields?.forEach((field) => {
|
||||||
let df = field.all_properties
|
let df = field.all_properties
|
||||||
if (df?.depends_on) evaluate_depends_on(df.depends_on, field)
|
if (df?.depends_on) evaluate_depends_on(df.depends_on, field)
|
||||||
all_fields.push(field)
|
all_fields.push({
|
||||||
|
...field,
|
||||||
|
placeholder: field.placeholder || field.label,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
return all_fields
|
return all_fields
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,94 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div ref="parentRef" class="flex h-full">
|
|
||||||
<div class="flex-1 flex flex-col justify-between gap-2 p-8">
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5 mb-4">
|
|
||||||
<div>{{ __('Sidebar Fields Layout') }}</div>
|
|
||||||
<Badge
|
|
||||||
v-if="dirty"
|
|
||||||
:label="__('Not Saved')"
|
|
||||||
variant="subtle"
|
|
||||||
theme="orange"
|
|
||||||
/>
|
|
||||||
</h2>
|
|
||||||
<FormControl
|
|
||||||
type="select"
|
|
||||||
v-model="doctype"
|
|
||||||
:label="__('DocType')"
|
|
||||||
:options="['CRM Lead', 'CRM Deal']"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row-reverse gap-2">
|
|
||||||
<Button
|
|
||||||
:loading="loading"
|
|
||||||
:label="__('Save')"
|
|
||||||
variant="solid"
|
|
||||||
@click="saveChanges"
|
|
||||||
/>
|
|
||||||
<Button :label="__('Reset')" @click="sections.reload" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Resizer
|
|
||||||
class="flex flex-col justify-between border-l"
|
|
||||||
:parent="parentRef"
|
|
||||||
side="right"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="sections.data"
|
|
||||||
class="flex flex-1 flex-col justify-between overflow-hidden"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col overflow-y-auto">
|
|
||||||
<SidebarLayoutBuilder :sections="sections.data" :doctype="doctype" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Resizer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import Resizer from '@/components/Resizer.vue'
|
|
||||||
import SidebarLayoutBuilder from '@/components/Settings/SidebarLayoutBuilder.vue'
|
|
||||||
import { Badge, call, createResource } from 'frappe-ui'
|
|
||||||
import { ref, computed, watch } from 'vue'
|
|
||||||
|
|
||||||
const parentRef = ref(null)
|
|
||||||
const doctype = ref('CRM Lead')
|
|
||||||
|
|
||||||
const oldSections = ref([])
|
|
||||||
|
|
||||||
const sections = createResource({
|
|
||||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
|
||||||
cache: ['sidebar-sections', doctype.value],
|
|
||||||
params: { doctype: doctype.value, type: 'Side Panel' },
|
|
||||||
auto: true,
|
|
||||||
onSuccess(data) {
|
|
||||||
oldSections.value = JSON.parse(JSON.stringify(data))
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const dirty = computed(() => {
|
|
||||||
if (!sections.data) return false
|
|
||||||
return JSON.stringify(sections.data) !== JSON.stringify(oldSections.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
function saveChanges() {
|
|
||||||
let _sections = JSON.parse(JSON.stringify(sections.data))
|
|
||||||
_sections.forEach((section) => {
|
|
||||||
if (!section.fields) return
|
|
||||||
section.fields = section.fields.map((field) => field.fieldname || field.name)
|
|
||||||
})
|
|
||||||
loading.value = true
|
|
||||||
call('crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.save_fields_layout', {
|
|
||||||
doctype: doctype.value,
|
|
||||||
type: 'Side Panel',
|
|
||||||
layout: JSON.stringify(_sections),
|
|
||||||
}).then(() => {
|
|
||||||
loading.value = false
|
|
||||||
sections.reload()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(doctype, (val) => sections.fetch({ doctype: val, type: 'Side Panel' }), {
|
|
||||||
immediate: true,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
113
frontend/src/components/Settings/QuickEntryLayout.vue
Normal file
113
frontend/src/components/Settings/QuickEntryLayout.vue
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col overflow-hidden">
|
||||||
|
<div class="flex flex-col gap-2 p-8 pb-5">
|
||||||
|
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5 mb-4">
|
||||||
|
<div>{{ __('Quick Entry Layout') }}</div>
|
||||||
|
<Badge
|
||||||
|
v-if="dirty"
|
||||||
|
:label="__('Not Saved')"
|
||||||
|
variant="subtle"
|
||||||
|
theme="orange"
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
<div class="flex gap-6 items-end">
|
||||||
|
<FormControl
|
||||||
|
class="flex-1"
|
||||||
|
type="select"
|
||||||
|
v-model="doctype"
|
||||||
|
:label="__('DocType')"
|
||||||
|
:options="['CRM Lead', 'CRM Deal', 'Contact', 'CRM Organization']"
|
||||||
|
@change="reload"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-row-reverse gap-2">
|
||||||
|
<Button
|
||||||
|
:loading="loading"
|
||||||
|
:label="__('Save')"
|
||||||
|
variant="solid"
|
||||||
|
@click="saveChanges"
|
||||||
|
/>
|
||||||
|
<Button :label="__('Reset')" @click="reload" />
|
||||||
|
<Button
|
||||||
|
:label="preview ? __('Hide Preview') : __('Show Preview')"
|
||||||
|
@click="preview = !preview"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="sections?.data" class="overflow-y-auto p-8 pt-3">
|
||||||
|
<div
|
||||||
|
class="rounded-xl h-full inline-block w-full px-4 pb-6 pt-5 sm:px-6 transform overflow-y-auto bg-white text-left align-middle shadow-xl transition-all"
|
||||||
|
>
|
||||||
|
<QuickEntryLayoutBuilder
|
||||||
|
v-if="!preview"
|
||||||
|
:sections="sections.data"
|
||||||
|
:doctype="doctype"
|
||||||
|
/>
|
||||||
|
<Fields v-else :sections="sections.data" :data="{}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import Fields from '@/components/Fields.vue'
|
||||||
|
import QuickEntryLayoutBuilder from '@/components/Settings/QuickEntryLayoutBuilder.vue'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
import { Badge, call, createResource } from 'frappe-ui'
|
||||||
|
import { ref, watch, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const doctype = ref('CRM Lead')
|
||||||
|
const loading = ref(false)
|
||||||
|
const dirty = ref(false)
|
||||||
|
const preview = ref(false)
|
||||||
|
|
||||||
|
function getParams() {
|
||||||
|
return { doctype: doctype.value, type: 'Quick Entry' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const sections = createResource({
|
||||||
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
|
cache: ['quick-entry-sections', doctype.value],
|
||||||
|
params: getParams(),
|
||||||
|
onSuccess(data) {
|
||||||
|
sections.originalData = JSON.parse(JSON.stringify(data))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => sections?.data,
|
||||||
|
() => {
|
||||||
|
dirty.value =
|
||||||
|
JSON.stringify(sections?.data) !== JSON.stringify(sections?.originalData)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => useDebounceFn(reload, 100)())
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
sections.params = getParams()
|
||||||
|
sections.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveChanges() {
|
||||||
|
let _sections = JSON.parse(JSON.stringify(sections.data))
|
||||||
|
_sections.forEach((section) => {
|
||||||
|
if (!section.fields) return
|
||||||
|
section.fields = section.fields.map(
|
||||||
|
(field) => field.fieldname || field.name,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
loading.value = true
|
||||||
|
call(
|
||||||
|
'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.save_fields_layout',
|
||||||
|
{
|
||||||
|
doctype: doctype.value,
|
||||||
|
type: 'Quick Entry',
|
||||||
|
layout: JSON.stringify(_sections),
|
||||||
|
},
|
||||||
|
).then(() => {
|
||||||
|
loading.value = false
|
||||||
|
reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
220
frontend/src/components/Settings/QuickEntryLayoutBuilder.vue
Normal file
220
frontend/src/components/Settings/QuickEntryLayoutBuilder.vue
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Draggable :list="sections" item-key="label" class="flex flex-col">
|
||||||
|
<template #item="{ element: section }">
|
||||||
|
<div
|
||||||
|
class="py-2 first:pt-0"
|
||||||
|
:class="section.hideBorder ? '' : 'border-t first:border-t-0'"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between pb-2">
|
||||||
|
<div
|
||||||
|
class="flex h-7 max-w-fit cursor-pointer items-center gap-2 text-base font-semibold leading-5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!section.editingLabel"
|
||||||
|
:class="section.hideLabel ? 'text-gray-400' : ''"
|
||||||
|
>
|
||||||
|
{{ __(section.label) || __('Untitled') }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex gap-2 items-center">
|
||||||
|
<Input
|
||||||
|
v-model="section.label"
|
||||||
|
@keydown.enter="section.editingLabel = false"
|
||||||
|
@blur="section.editingLabel = false"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="section.editingLabel"
|
||||||
|
icon="check"
|
||||||
|
variant="ghost"
|
||||||
|
@click="section.editingLabel = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dropdown :options="getOptions(section)">
|
||||||
|
<template #default>
|
||||||
|
<Button variant="ghost">
|
||||||
|
<FeatherIcon name="more-horizontal" class="h-4" />
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Draggable
|
||||||
|
:list="section.fields"
|
||||||
|
group="fields"
|
||||||
|
item-key="label"
|
||||||
|
class="grid gap-2"
|
||||||
|
:class="
|
||||||
|
section.columns ? 'grid-cols-' + section.columns : 'grid-cols-3'
|
||||||
|
"
|
||||||
|
handle=".cursor-grab"
|
||||||
|
>
|
||||||
|
<template #item="{ element: field }">
|
||||||
|
<div
|
||||||
|
class="px-1.5 py-1 border rounded text-base text-gray-800 flex items-center justify-between gap-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<DragVerticalIcon class="h-3.5 cursor-grab" />
|
||||||
|
<div>{{ field.label }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
icon="x"
|
||||||
|
@click="
|
||||||
|
section.fields.splice(section.fields.indexOf(field), 1)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
<Autocomplete
|
||||||
|
v-if="fields.data"
|
||||||
|
value=""
|
||||||
|
:options="fields.data"
|
||||||
|
@change="(e) => addField(section, e)"
|
||||||
|
>
|
||||||
|
<template #target="{ togglePopover }">
|
||||||
|
<div
|
||||||
|
class="grid gap-2 w-full"
|
||||||
|
:class="
|
||||||
|
section.columns
|
||||||
|
? 'grid-cols-' + section.columns
|
||||||
|
: 'grid-cols-3'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
class="mt-2 w-full !h-[38px] !border-gray-200"
|
||||||
|
variant="outline"
|
||||||
|
@click="togglePopover()"
|
||||||
|
:label="__('Add Field')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #item-label="{ option }">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div>{{ option.label }}</div>
|
||||||
|
<div class="text-gray-500 text-sm">
|
||||||
|
{{ `${option.fieldname} - ${option.fieldtype}` }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Autocomplete>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
<div class="py-2 border-t">
|
||||||
|
<Button
|
||||||
|
class="w-full !h-[38px] !border-gray-200"
|
||||||
|
variant="outline"
|
||||||
|
:label="__('Add Section')"
|
||||||
|
@click="
|
||||||
|
sections.push({
|
||||||
|
label: __('New Section'),
|
||||||
|
opened: true,
|
||||||
|
fields: [],
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||||
|
import DragVerticalIcon from '@/components/Icons/DragVerticalIcon.vue'
|
||||||
|
import Draggable from 'vuedraggable'
|
||||||
|
import { Dropdown, createResource } from 'frappe-ui'
|
||||||
|
import { computed, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
sections: Object,
|
||||||
|
doctype: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const restrictedFieldTypes = [
|
||||||
|
'Table',
|
||||||
|
'Geolocation',
|
||||||
|
'Attach',
|
||||||
|
'Attach Image',
|
||||||
|
'HTML',
|
||||||
|
'Signature',
|
||||||
|
]
|
||||||
|
|
||||||
|
const params = computed(() => {
|
||||||
|
return {
|
||||||
|
doctype: props.doctype,
|
||||||
|
restricted_fieldtypes: restrictedFieldTypes,
|
||||||
|
as_array: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fields = createResource({
|
||||||
|
url: 'crm.api.doc.get_fields_meta',
|
||||||
|
params: params.value,
|
||||||
|
cache: ['fieldsMeta', props.doctype],
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
function addField(section, field) {
|
||||||
|
if (!field) return
|
||||||
|
section.fields.push(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptions(section) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: 'edit',
|
||||||
|
onClick: () => (section.editingLabel = true),
|
||||||
|
condition: () => section.editable !== false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: section.hideLabel ? 'Show Label' : 'Hide Label',
|
||||||
|
icon: section.hideLabel ? 'eye' : 'eye-off',
|
||||||
|
onClick: () => (section.hideLabel = !section.hideLabel),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: section.hideBorder ? 'Show Border' : 'Hide Border',
|
||||||
|
icon: 'minus',
|
||||||
|
onClick: () => (section.hideBorder = !section.hideBorder),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Add Column',
|
||||||
|
icon: 'columns',
|
||||||
|
onClick: () =>
|
||||||
|
(section.columns = section.columns ? section.columns + 1 : 4),
|
||||||
|
condition: () => !section.columns || section.columns < 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Remove Column',
|
||||||
|
icon: 'columns',
|
||||||
|
onClick: () =>
|
||||||
|
(section.columns = section.columns ? section.columns - 1 : 2),
|
||||||
|
condition: () => !section.columns || section.columns > 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Remove Section',
|
||||||
|
icon: 'trash-2',
|
||||||
|
onClick: () => props.sections.splice(props.sections.indexOf(section), 1),
|
||||||
|
condition: () => section.editable !== false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.doctype,
|
||||||
|
() => fields.fetch(params.value),
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@ -3,42 +3,31 @@
|
|||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex h-[calc(100vh_-_8rem)]">
|
<div class="flex h-[calc(100vh_-_8rem)]">
|
||||||
<div class="flex w-52 shrink-0 flex-col bg-gray-50 p-2">
|
<div class="flex w-52 shrink-0 flex-col bg-gray-50 p-2">
|
||||||
<h1 class="px-2 pt-2 text-lg font-semibold">
|
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold">
|
||||||
{{ __('Settings') }}
|
{{ __('Settings') }}
|
||||||
</h1>
|
</h1>
|
||||||
<nav class="mt-3 space-y-1">
|
<div v-for="tab in tabs">
|
||||||
<SidebarLink
|
<div
|
||||||
v-for="tab in tabs"
|
v-if="!tab.hideLabel"
|
||||||
:icon="tab.icon"
|
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base font-medium text-gray-600 transition-all duration-300 ease-in-out"
|
||||||
:label="__(tab.label)"
|
>
|
||||||
class="w-full"
|
<span>{{ __(tab.label) }}</span>
|
||||||
:class="
|
</div>
|
||||||
activeTab?.label == tab.label
|
<nav class="space-y-1">
|
||||||
? 'bg-white shadow-sm'
|
<SidebarLink
|
||||||
: 'hover:bg-gray-100'
|
v-for="i in tab.items"
|
||||||
"
|
:icon="i.icon"
|
||||||
@click="activeTab = tab"
|
:label="__(i.label)"
|
||||||
/>
|
class="w-full"
|
||||||
</nav>
|
:class="
|
||||||
<div
|
activeTab?.label == i.label
|
||||||
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base font-medium text-gray-600 transition-all duration-300 ease-in-out"
|
? 'bg-white shadow-sm'
|
||||||
>
|
: 'hover:bg-gray-100'
|
||||||
<span>{{ __('Integrations') }}</span>
|
"
|
||||||
|
@click="activeTab = i"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<nav class="space-y-1">
|
|
||||||
<SidebarLink
|
|
||||||
v-for="i in integrations"
|
|
||||||
:icon="i.icon"
|
|
||||||
:label="__(i.label)"
|
|
||||||
class="w-full"
|
|
||||||
:class="
|
|
||||||
activeTab?.label == i.label
|
|
||||||
? 'bg-white shadow-sm'
|
|
||||||
: 'hover:bg-gray-100'
|
|
||||||
"
|
|
||||||
@click="activeTab = i"
|
|
||||||
/>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-col overflow-y-auto">
|
<div class="flex flex-1 flex-col overflow-y-auto">
|
||||||
<component :is="activeTab.component" v-if="activeTab" />
|
<component :is="activeTab.component" v-if="activeTab" />
|
||||||
@ -51,10 +40,12 @@
|
|||||||
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
||||||
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
|
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
|
import RightSideLayoutIcon from '@/components/Icons/RightSideLayoutIcon.vue'
|
||||||
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
|
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
|
||||||
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
|
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
|
||||||
import TwilioSettings from '@/components/Settings/TwilioSettings.vue'
|
import TwilioSettings from '@/components/Settings/TwilioSettings.vue'
|
||||||
import FieldsLayout from '@/components/Settings/FieldsLayout.vue'
|
import SidebarFieldsLayout from '@/components/Settings/SidebarFieldsLayout.vue'
|
||||||
|
import QuickEntryLayout from '@/components/Settings/QuickEntryLayout.vue'
|
||||||
import SidebarLink from '@/components/SidebarLink.vue'
|
import SidebarLink from '@/components/SidebarLink.vue'
|
||||||
import { isWhatsappInstalled } from '@/composables/settings'
|
import { isWhatsappInstalled } from '@/composables/settings'
|
||||||
import { Dialog, FeatherIcon } from 'frappe-ui'
|
import { Dialog, FeatherIcon } from 'frappe-ui'
|
||||||
@ -62,38 +53,62 @@ import { ref, markRaw, computed, h } from 'vue'
|
|||||||
|
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
|
|
||||||
let tabs = [
|
const tabs = computed(() => {
|
||||||
{
|
let _tabs = [
|
||||||
label: 'Profile',
|
|
||||||
icon: ContactsIcon,
|
|
||||||
component: markRaw(ProfileSettings),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Fields Layout',
|
|
||||||
icon: h(FeatherIcon, { name: 'grid' }),
|
|
||||||
component: markRaw(FieldsLayout),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
let integrations = computed(() => {
|
|
||||||
let items = [
|
|
||||||
{
|
{
|
||||||
label: 'Twilio',
|
label: 'Settings',
|
||||||
icon: PhoneIcon,
|
hideLabel: true,
|
||||||
component: markRaw(TwilioSettings),
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Profile',
|
||||||
|
icon: ContactsIcon,
|
||||||
|
component: markRaw(ProfileSettings),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Integrations',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Twilio',
|
||||||
|
icon: PhoneIcon,
|
||||||
|
component: markRaw(TwilioSettings),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'WhatsApp',
|
||||||
|
icon: WhatsAppIcon,
|
||||||
|
component: markRaw(WhatsAppSettings),
|
||||||
|
condition: () => isWhatsappInstalled.value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Customizations',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Sidebar Fields Layout',
|
||||||
|
icon: RightSideLayoutIcon,
|
||||||
|
component: markRaw(SidebarFieldsLayout),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Quick Entry Layout',
|
||||||
|
icon: h(FeatherIcon, { name: 'grid' }),
|
||||||
|
component: markRaw(QuickEntryLayout),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
if (isWhatsappInstalled.value) {
|
return _tabs.map((tab) => {
|
||||||
items.push({
|
tab.items = tab.items.filter((item) => {
|
||||||
label: 'WhatsApp',
|
if (item.condition) {
|
||||||
icon: WhatsAppIcon,
|
return item.condition()
|
||||||
component: markRaw(WhatsAppSettings),
|
}
|
||||||
|
return true
|
||||||
})
|
})
|
||||||
}
|
return tab
|
||||||
|
})
|
||||||
return items
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const activeTab = ref(tabs[0])
|
const activeTab = ref(tabs.value[0].items[0])
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
131
frontend/src/components/Settings/SidebarFieldsLayout.vue
Normal file
131
frontend/src/components/Settings/SidebarFieldsLayout.vue
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="parentRef" class="flex h-full">
|
||||||
|
<div class="flex-1 flex flex-col justify-between gap-2 p-8">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5 mb-4">
|
||||||
|
<div>{{ __('Sidebar Fields Layout') }}</div>
|
||||||
|
<Badge
|
||||||
|
v-if="dirty"
|
||||||
|
:label="__('Not Saved')"
|
||||||
|
variant="subtle"
|
||||||
|
theme="orange"
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
<FormControl
|
||||||
|
type="select"
|
||||||
|
v-model="doctype"
|
||||||
|
:label="__('DocType')"
|
||||||
|
:options="['CRM Lead', 'CRM Deal']"
|
||||||
|
@change="reload"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row-reverse gap-2">
|
||||||
|
<Button
|
||||||
|
:loading="loading"
|
||||||
|
:label="__('Save')"
|
||||||
|
variant="solid"
|
||||||
|
@click="saveChanges"
|
||||||
|
/>
|
||||||
|
<Button :label="__('Reset')" @click="reload" />
|
||||||
|
<Button
|
||||||
|
:label="preview ? __('Hide Preview') : __('Show Preview')"
|
||||||
|
@click="preview = !preview"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Resizer
|
||||||
|
v-if="sections.data"
|
||||||
|
class="flex flex-col justify-between border-l"
|
||||||
|
:parent="parentRef"
|
||||||
|
side="right"
|
||||||
|
>
|
||||||
|
<div class="flex flex-1 flex-col justify-between overflow-hidden">
|
||||||
|
<div class="flex flex-col overflow-y-auto">
|
||||||
|
<SidebarLayoutBuilder
|
||||||
|
v-if="!preview"
|
||||||
|
:sections="sections.data"
|
||||||
|
:doctype="doctype"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
v-for="(section, i) in sections.data"
|
||||||
|
:key="section.label"
|
||||||
|
class="flex flex-col p-3"
|
||||||
|
:class="{ 'border-b': i !== sections.data.length - 1 }"
|
||||||
|
>
|
||||||
|
<Section :is-opened="section.opened" :label="section.label">
|
||||||
|
<SectionFields :fields="section.fields" v-model="data" />
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Resizer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import Section from '@/components/Section.vue'
|
||||||
|
import SectionFields from '@/components/SectionFields.vue'
|
||||||
|
import Resizer from '@/components/Resizer.vue'
|
||||||
|
import SidebarLayoutBuilder from '@/components/Settings/SidebarLayoutBuilder.vue'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
import { Badge, call, createResource } from 'frappe-ui'
|
||||||
|
import { ref, watch, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const parentRef = ref(null)
|
||||||
|
const doctype = ref('CRM Lead')
|
||||||
|
const loading = ref(false)
|
||||||
|
const dirty = ref(false)
|
||||||
|
const preview = ref(false)
|
||||||
|
const data = ref({})
|
||||||
|
|
||||||
|
function getParams() {
|
||||||
|
return { doctype: doctype.value, type: 'Side Panel' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const sections = createResource({
|
||||||
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
|
cache: ['sidebar-sections', doctype.value],
|
||||||
|
params: getParams(),
|
||||||
|
onSuccess(data) {
|
||||||
|
sections.originalData = JSON.parse(JSON.stringify(data))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => sections?.data,
|
||||||
|
() => {
|
||||||
|
dirty.value =
|
||||||
|
JSON.stringify(sections?.data) !== JSON.stringify(sections?.originalData)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => useDebounceFn(reload, 100)())
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
sections.params = getParams()
|
||||||
|
sections.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveChanges() {
|
||||||
|
let _sections = JSON.parse(JSON.stringify(sections.data))
|
||||||
|
_sections.forEach((section) => {
|
||||||
|
if (!section.fields) return
|
||||||
|
section.fields = section.fields.map(
|
||||||
|
(field) => field.fieldname || field.name,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
loading.value = true
|
||||||
|
call(
|
||||||
|
'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.save_fields_layout',
|
||||||
|
{
|
||||||
|
doctype: doctype.value,
|
||||||
|
type: 'Side Panel',
|
||||||
|
layout: JSON.stringify(_sections),
|
||||||
|
},
|
||||||
|
).then(() => {
|
||||||
|
loading.value = false
|
||||||
|
reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -16,20 +16,27 @@
|
|||||||
<div v-if="!section.editingLabel">
|
<div v-if="!section.editingLabel">
|
||||||
{{ __(section.label) || __('Untitled') }}
|
{{ __(section.label) || __('Untitled') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else class="flex gap-2 items-center">
|
||||||
<Input
|
<Input
|
||||||
v-model="section.label"
|
v-model="section.label"
|
||||||
@keydown.enter="section.editingLabel = false"
|
@keydown.enter="section.editingLabel = false"
|
||||||
@blur="section.editingLabel = false"
|
@blur="section.editingLabel = false"
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="section.editingLabel"
|
||||||
|
icon="check"
|
||||||
|
variant="ghost"
|
||||||
|
@click.stop="section.editingLabel = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
:icon="section.editingLabel ? 'check' : 'edit'"
|
v-if="!section.editingLabel"
|
||||||
|
icon="edit"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click="section.editingLabel = !section.editingLabel"
|
@click="section.editingLabel = true"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-if="section.editable !== false"
|
v-if="section.editable !== false"
|
||||||
@ -42,6 +49,7 @@
|
|||||||
<div v-show="section.opened" class="p-4 pt-0 pb-2">
|
<div v-show="section.opened" class="p-4 pt-0 pb-2">
|
||||||
<Draggable
|
<Draggable
|
||||||
:list="section.fields"
|
:list="section.fields"
|
||||||
|
group="fields"
|
||||||
item-key="label"
|
item-key="label"
|
||||||
class="flex flex-col gap-1"
|
class="flex flex-col gap-1"
|
||||||
handle=".cursor-grab"
|
handle=".cursor-grab"
|
||||||
@ -111,9 +119,13 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
:label="__('Add Section')"
|
:label="__('Add Section')"
|
||||||
@click="
|
@click="
|
||||||
sections.push({ label: 'New Section', opened: true, fields: [] })
|
sections.push({ label: __('New Section'), opened: true, fields: [] })
|
||||||
"
|
"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -7,8 +7,8 @@
|
|||||||
isCollapsed
|
isCollapsed
|
||||||
? 'w-auto px-0'
|
? 'w-auto px-0'
|
||||||
: open
|
: open
|
||||||
? 'w-52 bg-white px-2 shadow-sm'
|
? 'w-52 bg-white px-2 shadow-sm'
|
||||||
: 'w-52 px-2 hover:bg-gray-200'
|
: 'w-52 px-2 hover:bg-gray-200'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<CRMLogo class="size-8 flex-shrink-0 rounded" />
|
<CRMLogo class="size-8 flex-shrink-0 rounded" />
|
||||||
@ -44,7 +44,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<SettingsModal v-model="showSettingsModal" />
|
<SettingsModal v-if="showSettingsModal" v-model="showSettingsModal" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@ -357,7 +357,10 @@ function getDealRowObject(deal) {
|
|||||||
label: deal.organization,
|
label: deal.organization,
|
||||||
logo: getOrganization(deal.organization)?.organization_logo,
|
logo: getOrganization(deal.organization)?.organization_logo,
|
||||||
},
|
},
|
||||||
annual_revenue: formatNumberIntoCurrency(deal.annual_revenue),
|
annual_revenue: formatNumberIntoCurrency(
|
||||||
|
deal.annual_revenue,
|
||||||
|
deal.currency,
|
||||||
|
),
|
||||||
status: {
|
status: {
|
||||||
label: deal.status,
|
label: deal.status,
|
||||||
color: getDealStatus(deal.status)?.iconColorClass,
|
color: getDealStatus(deal.status)?.iconColorClass,
|
||||||
|
|||||||
@ -488,7 +488,7 @@ const fieldsLayout = createResource({
|
|||||||
transform: (data) => getParsedFields(data),
|
transform: (data) => getParsedFields(data),
|
||||||
})
|
})
|
||||||
|
|
||||||
function getParsedFields(sections, contacts) {
|
function getParsedFields(sections) {
|
||||||
sections.forEach((section) => {
|
sections.forEach((section) => {
|
||||||
if (section.name == 'contacts_section') return
|
if (section.name == 'contacts_section') return
|
||||||
section.fields.forEach((field) => {
|
section.fields.forEach((field) => {
|
||||||
@ -583,7 +583,7 @@ const deal_contacts = createResource({
|
|||||||
cache: ['deal_contacts', props.dealId],
|
cache: ['deal_contacts', props.dealId],
|
||||||
auto: true,
|
auto: true,
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
let contactSection = fieldsLayout.data.find(
|
let contactSection = fieldsLayout.data?.find(
|
||||||
(section) => section.name == 'contacts_section',
|
(section) => section.name == 'contacts_section',
|
||||||
)
|
)
|
||||||
if (!contactSection) return
|
if (!contactSection) return
|
||||||
|
|||||||
@ -109,7 +109,7 @@ const rows = computed(() => {
|
|||||||
if (!deals.value?.data.group_by_field?.name) return []
|
if (!deals.value?.data.group_by_field?.name) return []
|
||||||
return getGroupedByRows(
|
return getGroupedByRows(
|
||||||
deals.value?.data.data,
|
deals.value?.data.data,
|
||||||
deals.value?.data.group_by_field
|
deals.value?.data.group_by_field,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return parseRows(deals.value?.data.data)
|
return parseRows(deals.value?.data.data)
|
||||||
@ -158,7 +158,10 @@ function parseRows(rows) {
|
|||||||
logo: getOrganization(deal.organization)?.organization_logo,
|
logo: getOrganization(deal.organization)?.organization_logo,
|
||||||
}
|
}
|
||||||
} else if (row == 'annual_revenue') {
|
} else if (row == 'annual_revenue') {
|
||||||
_rows[row] = formatNumberIntoCurrency(deal.annual_revenue)
|
_rows[row] = formatNumberIntoCurrency(
|
||||||
|
deal.annual_revenue,
|
||||||
|
deal.currency,
|
||||||
|
)
|
||||||
} else if (row == 'status') {
|
} else if (row == 'status') {
|
||||||
_rows[row] = {
|
_rows[row] = {
|
||||||
label: deal.status,
|
label: deal.status,
|
||||||
@ -171,8 +174,8 @@ function parseRows(rows) {
|
|||||||
deal.sla_status == 'Failed'
|
deal.sla_status == 'Failed'
|
||||||
? 'red'
|
? 'red'
|
||||||
: deal.sla_status == 'Fulfilled'
|
: deal.sla_status == 'Fulfilled'
|
||||||
? 'green'
|
? 'green'
|
||||||
: 'orange'
|
: 'orange'
|
||||||
if (value == 'First Response Due') {
|
if (value == 'First Response Due') {
|
||||||
value = __(timeAgo(deal.response_by))
|
value = __(timeAgo(deal.response_by))
|
||||||
tooltipText = dateFormat(deal.response_by, dateTooltipFormat)
|
tooltipText = dateFormat(deal.response_by, dateTooltipFormat)
|
||||||
@ -207,7 +210,7 @@ function parseRows(rows) {
|
|||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
['first_response_time', 'first_responded_on', 'response_by'].includes(
|
['first_response_time', 'first_responded_on', 'response_by'].includes(
|
||||||
row
|
row,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
let field = row == 'response_by' ? 'response_by' : 'first_responded_on'
|
let field = row == 'response_by' ? 'response_by' : 'first_responded_on'
|
||||||
|
|||||||
@ -58,15 +58,15 @@
|
|||||||
@updateField="updateField"
|
@updateField="updateField"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="detailSections.length"
|
v-if="fieldsLayout.data"
|
||||||
class="flex flex-1 flex-col justify-between overflow-hidden"
|
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col overflow-y-auto">
|
<div class="flex flex-col overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(section, i) in detailSections"
|
v-for="(section, i) in fieldsLayout.data"
|
||||||
:key="section.label"
|
:key="section.label"
|
||||||
class="flex flex-col px-2 py-3 sm:p-3"
|
class="flex flex-col px-2 py-3 sm:p-3"
|
||||||
:class="{ 'border-b': i !== detailSections.length - 1 }"
|
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||||
>
|
>
|
||||||
<Section :is-opened="section.opened" :label="section.label">
|
<Section :is-opened="section.opened" :label="section.label">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@ -441,44 +441,31 @@ const tabs = computed(() => {
|
|||||||
return tabOptions.filter((tab) => (tab.condition ? tab.condition() : true))
|
return tabOptions.filter((tab) => (tab.condition ? tab.condition() : true))
|
||||||
})
|
})
|
||||||
|
|
||||||
const detailSections = computed(() => {
|
const fieldsLayout = createResource({
|
||||||
let data = deal.data
|
url: 'crm.api.doc.get_sidebar_fields',
|
||||||
if (!data) return []
|
cache: ['fieldsLayout', props.dealId],
|
||||||
return getParsedFields(data.doctype_fields, deal_contacts.data)
|
params: { doctype: 'CRM Deal', name: props.dealId },
|
||||||
|
auto: true,
|
||||||
|
transform: (data) => getParsedFields(data),
|
||||||
})
|
})
|
||||||
|
|
||||||
function getParsedFields(sections, contacts) {
|
function getParsedFields(sections) {
|
||||||
sections.forEach((section) => {
|
sections.forEach((section) => {
|
||||||
if (section.name == 'contacts_tab') {
|
if (section.name == 'contacts_section') return
|
||||||
delete section.fields
|
section.fields.forEach((field) => {
|
||||||
section.contacts =
|
if (field.name == 'organization') {
|
||||||
contacts?.map((contact) => {
|
field.create = (value, close) => {
|
||||||
return {
|
_organization.value.organization_name = value
|
||||||
name: contact.name,
|
showOrganizationModal.value = true
|
||||||
full_name: contact.full_name,
|
close()
|
||||||
email: contact.email,
|
|
||||||
mobile_no: contact.mobile_no,
|
|
||||||
image: contact.image,
|
|
||||||
is_primary: contact.is_primary,
|
|
||||||
opened: false,
|
|
||||||
}
|
|
||||||
}) || []
|
|
||||||
} else {
|
|
||||||
section.fields.forEach((field) => {
|
|
||||||
if (field.name == 'organization') {
|
|
||||||
field.create = (value, close) => {
|
|
||||||
_organization.value.organization_name = value
|
|
||||||
showOrganizationModal.value = true
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
field.link = (org) =>
|
|
||||||
router.push({
|
|
||||||
name: 'Organization',
|
|
||||||
params: { organizationId: org },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
field.link = (org) =>
|
||||||
}
|
router.push({
|
||||||
|
name: 'Organization',
|
||||||
|
params: { organizationId: org },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
return sections
|
return sections
|
||||||
}
|
}
|
||||||
@ -556,6 +543,23 @@ const deal_contacts = createResource({
|
|||||||
params: { name: props.dealId },
|
params: { name: props.dealId },
|
||||||
cache: ['deal_contacts', props.dealId],
|
cache: ['deal_contacts', props.dealId],
|
||||||
auto: true,
|
auto: true,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
let contactSection = fieldsLayout.data?.find(
|
||||||
|
(section) => section.name == 'contacts_section',
|
||||||
|
)
|
||||||
|
if (!contactSection) return
|
||||||
|
contactSection.contacts = data.map((contact) => {
|
||||||
|
return {
|
||||||
|
name: contact.name,
|
||||||
|
full_name: contact.full_name,
|
||||||
|
email: contact.email,
|
||||||
|
mobile_no: contact.mobile_no,
|
||||||
|
image: contact.image,
|
||||||
|
is_primary: contact.is_primary,
|
||||||
|
opened: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function updateField(name, value, callback) {
|
function updateField(name, value, callback) {
|
||||||
|
|||||||
@ -63,15 +63,15 @@
|
|||||||
@updateField="updateField"
|
@updateField="updateField"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="detailSections.length"
|
v-if="fieldsLayout.data"
|
||||||
class="flex flex-1 flex-col justify-between overflow-hidden"
|
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col overflow-y-auto">
|
<div class="flex flex-col overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(section, i) in detailSections"
|
v-for="(section, i) in fieldsLayout.data"
|
||||||
:key="section.label"
|
:key="section.label"
|
||||||
class="flex flex-col px-2 py-3 sm:p-3"
|
class="flex flex-col px-2 py-3 sm:p-3"
|
||||||
:class="{ 'border-b': i !== detailSections.length - 1 }"
|
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||||
>
|
>
|
||||||
<Section :is-opened="section.opened" :label="section.label">
|
<Section :is-opened="section.opened" :label="section.label">
|
||||||
<SectionFields
|
<SectionFields
|
||||||
@ -367,10 +367,11 @@ watch(tabs, (value) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const detailSections = computed(() => {
|
const fieldsLayout = createResource({
|
||||||
let data = lead.data
|
url: 'crm.api.doc.get_sidebar_fields',
|
||||||
if (!data) return []
|
cache: ['fieldsLayout', props.leadId],
|
||||||
return data.doctype_fields
|
params: { doctype: 'CRM Lead', name: props.leadId },
|
||||||
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
function updateField(name, value, callback) {
|
function updateField(name, value, callback) {
|
||||||
|
|||||||
@ -103,8 +103,13 @@
|
|||||||
v-if="organization.doc.annual_revenue"
|
v-if="organization.doc.annual_revenue"
|
||||||
class="flex items-center gap-1.5"
|
class="flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
<FeatherIcon name="dollar-sign" class="h-4 w-4" />
|
<MoneyIcon class="size-4" />
|
||||||
<span class="">{{ organization.doc.annual_revenue }}</span>
|
<span class="">{{
|
||||||
|
formatNumberIntoCurrency(
|
||||||
|
organization.doc.annual_revenue,
|
||||||
|
organization.doc.currency,
|
||||||
|
)
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
v-if="organization.doc.annual_revenue"
|
v-if="organization.doc.annual_revenue"
|
||||||
@ -231,6 +236,7 @@ import DealsListView from '@/components/ListViews/DealsListView.vue'
|
|||||||
import ContactsListView from '@/components/ListViews/ContactsListView.vue'
|
import ContactsListView from '@/components/ListViews/ContactsListView.vue'
|
||||||
import WebsiteIcon from '@/components/Icons/WebsiteIcon.vue'
|
import WebsiteIcon from '@/components/Icons/WebsiteIcon.vue'
|
||||||
import TerritoryIcon from '@/components/Icons/TerritoryIcon.vue'
|
import TerritoryIcon from '@/components/Icons/TerritoryIcon.vue'
|
||||||
|
import MoneyIcon from '@/components/Icons/MoneyIcon.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
||||||
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
||||||
@ -347,6 +353,7 @@ const deals = createListResource({
|
|||||||
fields: [
|
fields: [
|
||||||
'name',
|
'name',
|
||||||
'organization',
|
'organization',
|
||||||
|
'currency',
|
||||||
'annual_revenue',
|
'annual_revenue',
|
||||||
'status',
|
'status',
|
||||||
'email',
|
'email',
|
||||||
@ -405,7 +412,10 @@ function getDealRowObject(deal) {
|
|||||||
label: deal.organization,
|
label: deal.organization,
|
||||||
logo: props.organization?.organization_logo,
|
logo: props.organization?.organization_logo,
|
||||||
},
|
},
|
||||||
annual_revenue: formatNumberIntoCurrency(deal.annual_revenue),
|
annual_revenue: formatNumberIntoCurrency(
|
||||||
|
deal.annual_revenue,
|
||||||
|
deal.currency,
|
||||||
|
),
|
||||||
status: {
|
status: {
|
||||||
label: deal.status,
|
label: deal.status,
|
||||||
color: getDealStatus(deal.status)?.iconColorClass,
|
color: getDealStatus(deal.status)?.iconColorClass,
|
||||||
|
|||||||
@ -85,7 +85,7 @@ const showOrganizationModal = ref(false)
|
|||||||
|
|
||||||
const currentOrganization = computed(() => {
|
const currentOrganization = computed(() => {
|
||||||
return organizations.value?.data?.data?.find(
|
return organizations.value?.data?.data?.find(
|
||||||
(organization) => organization.name === route.params.organizationId
|
(organization) => organization.name === route.params.organizationId,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -124,7 +124,10 @@ const rows = computed(() => {
|
|||||||
} else if (row === 'website') {
|
} else if (row === 'website') {
|
||||||
_rows[row] = website(organization.website)
|
_rows[row] = website(organization.website)
|
||||||
} else if (row === 'annual_revenue') {
|
} else if (row === 'annual_revenue') {
|
||||||
_rows[row] = formatNumberIntoCurrency(organization.annual_revenue)
|
_rows[row] = formatNumberIntoCurrency(
|
||||||
|
organization.annual_revenue,
|
||||||
|
organization.currency,
|
||||||
|
)
|
||||||
} else if (['modified', 'creation'].includes(row)) {
|
} else if (['modified', 'creation'].includes(row)) {
|
||||||
_rows[row] = {
|
_rows[row] = {
|
||||||
label: dateFormat(organization[row], dateTooltipFormat),
|
label: dateFormat(organization[row], dateTooltipFormat),
|
||||||
|
|||||||
@ -94,12 +94,12 @@ export function secondsToDuration(seconds) {
|
|||||||
return `${hours}h ${minutes}m ${_seconds}s`
|
return `${hours}h ${minutes}m ${_seconds}s`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatNumberIntoCurrency(value) {
|
export function formatNumberIntoCurrency(value, currency = 'INR') {
|
||||||
if (value) {
|
if (value) {
|
||||||
return value.toLocaleString('en-IN', {
|
return value.toLocaleString('en-IN', {
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 0,
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'INR',
|
currency: currency ? currency : 'INR',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user