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=[
|
||||
"name",
|
||||
"organization",
|
||||
"currency",
|
||||
"annual_revenue",
|
||||
"status",
|
||||
"email",
|
||||
|
||||
@ -257,17 +257,18 @@ def get_list_data(
|
||||
"user": frappe.session.user,
|
||||
}
|
||||
|
||||
_list = get_controller(doctype)
|
||||
|
||||
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)
|
||||
columns = frappe.parse_json(list_view_settings.columns)
|
||||
rows = frappe.parse_json(list_view_settings.rows)
|
||||
is_default = False
|
||||
elif not custom_view or is_default:
|
||||
_list = get_controller(doctype)
|
||||
elif not custom_view or is_default and hasattr(_list, "default_list_data"):
|
||||
columns = _list.default_list_data().get("columns")
|
||||
|
||||
if hasattr(_list, "default_list_data"):
|
||||
columns = _list.default_list_data().get("columns")
|
||||
rows = _list.default_list_data().get("rows")
|
||||
if hasattr(_list, "default_list_data"):
|
||||
rows = _list.default_list_data().get("rows")
|
||||
|
||||
# check if rows has all keys from columns if not add them
|
||||
for column in columns:
|
||||
|
||||
@ -8,31 +8,41 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"organization_tab",
|
||||
"naming_series",
|
||||
"organization",
|
||||
"website",
|
||||
"territory",
|
||||
"annual_revenue",
|
||||
"close_date",
|
||||
"probability",
|
||||
"next_step",
|
||||
"probability",
|
||||
"column_break_ijan",
|
||||
"status",
|
||||
"close_date",
|
||||
"deal_owner",
|
||||
"contacts_tab",
|
||||
"contacts",
|
||||
"contact",
|
||||
"lead_details_tab",
|
||||
"lead",
|
||||
"source",
|
||||
"column_break_wsde",
|
||||
"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",
|
||||
"mobile_no",
|
||||
"phone",
|
||||
"contacts",
|
||||
"others_tab",
|
||||
"naming_series",
|
||||
"status",
|
||||
"section_break_sygz",
|
||||
"no_of_employees",
|
||||
"column_break_nwob",
|
||||
"job_title",
|
||||
"section_break_eepu",
|
||||
"lead",
|
||||
"column_break_bqvs",
|
||||
"source",
|
||||
"gender",
|
||||
"sla_tab",
|
||||
"sla",
|
||||
"sla_creation",
|
||||
@ -63,7 +73,8 @@
|
||||
"fetch_from": ".annual_revenue",
|
||||
"fieldname": "annual_revenue",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount"
|
||||
"label": "Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fetch_from": ".website",
|
||||
@ -88,14 +99,6 @@
|
||||
"label": "Lead",
|
||||
"options": "CRM Lead"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_eepu",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_bqvs",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "deal_owner",
|
||||
"fieldtype": "Link",
|
||||
@ -142,12 +145,6 @@
|
||||
"label": "Contacts",
|
||||
"options": "CRM Contacts"
|
||||
},
|
||||
{
|
||||
"fieldname": "others_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Others",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "organization_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
@ -235,14 +232,6 @@
|
||||
"label": "No. of Employees",
|
||||
"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",
|
||||
"fieldtype": "Data",
|
||||
@ -270,11 +259,87 @@
|
||||
"fieldname": "lead_name",
|
||||
"fieldtype": "Data",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2024-01-25 21:00:08.216020",
|
||||
"modified": "2024-06-20 12:55:41.602364",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Deal",
|
||||
|
||||
@ -178,6 +178,7 @@ class CRMDeal(Document):
|
||||
"annual_revenue",
|
||||
"status",
|
||||
"email",
|
||||
"currency",
|
||||
"mobile_no",
|
||||
"deal_owner",
|
||||
"sla_status",
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
"field_order": [
|
||||
"organization_name",
|
||||
"no_of_employees",
|
||||
"currency",
|
||||
"annual_revenue",
|
||||
"organization_logo",
|
||||
"column_break_pnpp",
|
||||
@ -47,7 +48,8 @@
|
||||
{
|
||||
"fieldname": "annual_revenue",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Annual Revenue"
|
||||
"label": "Annual Revenue",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "industry",
|
||||
@ -60,12 +62,18 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Territory",
|
||||
"options": "CRM Territory"
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
}
|
||||
],
|
||||
"image_field": "organization_logo",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-19 21:53:14.945857",
|
||||
"modified": "2024-06-20 12:59:55.297752",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Organization",
|
||||
|
||||
@ -47,6 +47,7 @@ class CRMOrganization(Document):
|
||||
"organization_logo",
|
||||
"website",
|
||||
"industry",
|
||||
"currency",
|
||||
"annual_revenue",
|
||||
"modified",
|
||||
]
|
||||
|
||||
@ -113,19 +113,19 @@ def add_default_fields_layout():
|
||||
layouts = {
|
||||
"CRM Lead-Quick Entry": {
|
||||
"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": {
|
||||
"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": {
|
||||
"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": {
|
||||
"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
|
||||
crm.patches.v1_0.create_email_template_custom_fields
|
||||
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="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
|
||||
class="grid gap-4"
|
||||
: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() {
|
||||
if (deal.website && !deal.website.startsWith('http')) {
|
||||
deal.website = 'https://' + deal.website
|
||||
}
|
||||
createResource({
|
||||
url: 'crm.fcrm.doctype.crm_deal.crm_deal.create_deal',
|
||||
params: { args: deal },
|
||||
auto: true,
|
||||
validate() {
|
||||
error.value = null
|
||||
if (deal.website && !deal.website.startsWith('http')) {
|
||||
deal.website = 'https://' + deal.website
|
||||
}
|
||||
if (deal.annual_revenue) {
|
||||
deal.annual_revenue = deal.annual_revenue.replace(/,/g, '')
|
||||
if (isNaN(deal.annual_revenue)) {
|
||||
@ -182,6 +182,14 @@ function createDeal() {
|
||||
show.value = false
|
||||
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() {
|
||||
if (lead.website && !lead.website.startsWith('http')) {
|
||||
lead.website = 'https://' + lead.website
|
||||
}
|
||||
|
||||
createLead.submit(lead, {
|
||||
validate() {
|
||||
error.value = null
|
||||
@ -104,9 +108,6 @@ function createNewLead() {
|
||||
error.value = __('First Name is mandatory')
|
||||
return error.value
|
||||
}
|
||||
if (lead.website && !lead.website.startsWith('http')) {
|
||||
lead.website = 'https://' + lead.website
|
||||
}
|
||||
if (lead.annual_revenue) {
|
||||
lead.annual_revenue = lead.annual_revenue.replace(/,/g, '')
|
||||
if (isNaN(lead.annual_revenue)) {
|
||||
@ -133,6 +134,14 @@ function createNewLead() {
|
||||
show.value = false
|
||||
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>
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import MoneyIcon from '@/components/Icons/MoneyIcon.vue'
|
||||
import WebsiteIcon from '@/components/Icons/WebsiteIcon.vue'
|
||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||
import TerritoryIcon from '@/components/Icons/TerritoryIcon.vue'
|
||||
import { formatNumberIntoCurrency } from '@/utils'
|
||||
import { call, FeatherIcon, createResource } from 'frappe-ui'
|
||||
import { ref, nextTick, watch, computed, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@ -205,9 +207,12 @@ const fields = computed(() => {
|
||||
value: _organization.value.territory,
|
||||
},
|
||||
{
|
||||
icon: h(FeatherIcon, { name: 'dollar-sign', class: 'h-4 w-4' }),
|
||||
icon: MoneyIcon,
|
||||
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' }),
|
||||
@ -227,7 +232,7 @@ const fields = computed(() => {
|
||||
const sections = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||
cache: ['quickEntryFields', 'CRM Organization'],
|
||||
params: { doctype: 'CRM Organization', type: 'Quick Entry'},
|
||||
params: { doctype: 'CRM Organization', type: 'Quick Entry' },
|
||||
auto: true,
|
||||
})
|
||||
|
||||
@ -246,6 +251,6 @@ watch(
|
||||
editMode.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
<FormControl
|
||||
v-else-if="
|
||||
['email', 'number', 'date', 'password', 'textarea'].includes(
|
||||
field.type
|
||||
field.type,
|
||||
)
|
||||
"
|
||||
class="form-control"
|
||||
@ -137,7 +137,10 @@ const _fields = computed(() => {
|
||||
props.fields?.forEach((field) => {
|
||||
let df = field.all_properties
|
||||
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
|
||||
})
|
||||
|
||||
@ -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>
|
||||
<div class="flex h-[calc(100vh_-_8rem)]">
|
||||
<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') }}
|
||||
</h1>
|
||||
<nav class="mt-3 space-y-1">
|
||||
<SidebarLink
|
||||
v-for="tab in tabs"
|
||||
:icon="tab.icon"
|
||||
:label="__(tab.label)"
|
||||
class="w-full"
|
||||
:class="
|
||||
activeTab?.label == tab.label
|
||||
? 'bg-white shadow-sm'
|
||||
: 'hover:bg-gray-100'
|
||||
"
|
||||
@click="activeTab = tab"
|
||||
/>
|
||||
</nav>
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<span>{{ __('Integrations') }}</span>
|
||||
<div v-for="tab in tabs">
|
||||
<div
|
||||
v-if="!tab.hideLabel"
|
||||
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"
|
||||
>
|
||||
<span>{{ __(tab.label) }}</span>
|
||||
</div>
|
||||
<nav class="space-y-1">
|
||||
<SidebarLink
|
||||
v-for="i in tab.items"
|
||||
: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>
|
||||
<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 class="flex flex-1 flex-col overflow-y-auto">
|
||||
<component :is="activeTab.component" v-if="activeTab" />
|
||||
@ -51,10 +40,12 @@
|
||||
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
||||
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import RightSideLayoutIcon from '@/components/Icons/RightSideLayoutIcon.vue'
|
||||
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
|
||||
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.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 { isWhatsappInstalled } from '@/composables/settings'
|
||||
import { Dialog, FeatherIcon } from 'frappe-ui'
|
||||
@ -62,38 +53,62 @@ import { ref, markRaw, computed, h } from 'vue'
|
||||
|
||||
const show = defineModel()
|
||||
|
||||
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 = [
|
||||
const tabs = computed(() => {
|
||||
let _tabs = [
|
||||
{
|
||||
label: 'Twilio',
|
||||
icon: PhoneIcon,
|
||||
component: markRaw(TwilioSettings),
|
||||
label: 'Settings',
|
||||
hideLabel: true,
|
||||
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) {
|
||||
items.push({
|
||||
label: 'WhatsApp',
|
||||
icon: WhatsAppIcon,
|
||||
component: markRaw(WhatsAppSettings),
|
||||
return _tabs.map((tab) => {
|
||||
tab.items = tab.items.filter((item) => {
|
||||
if (item.condition) {
|
||||
return item.condition()
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
return tab
|
||||
})
|
||||
})
|
||||
|
||||
const activeTab = ref(tabs[0])
|
||||
const activeTab = ref(tabs.value[0].items[0])
|
||||
</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">
|
||||
{{ __(section.label) || __('Untitled') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<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.stop="section.editingLabel = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
:icon="section.editingLabel ? 'check' : 'edit'"
|
||||
v-if="!section.editingLabel"
|
||||
icon="edit"
|
||||
variant="ghost"
|
||||
@click="section.editingLabel = !section.editingLabel"
|
||||
@click="section.editingLabel = true"
|
||||
/>
|
||||
<Button
|
||||
v-if="section.editable !== false"
|
||||
@ -42,6 +49,7 @@
|
||||
<div v-show="section.opened" class="p-4 pt-0 pb-2">
|
||||
<Draggable
|
||||
:list="section.fields"
|
||||
group="fields"
|
||||
item-key="label"
|
||||
class="flex flex-col gap-1"
|
||||
handle=".cursor-grab"
|
||||
@ -111,9 +119,13 @@
|
||||
variant="outline"
|
||||
:label="__('Add Section')"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
isCollapsed
|
||||
? 'w-auto px-0'
|
||||
: open
|
||||
? 'w-52 bg-white px-2 shadow-sm'
|
||||
: 'w-52 px-2 hover:bg-gray-200'
|
||||
? 'w-52 bg-white px-2 shadow-sm'
|
||||
: 'w-52 px-2 hover:bg-gray-200'
|
||||
"
|
||||
>
|
||||
<CRMLogo class="size-8 flex-shrink-0 rounded" />
|
||||
@ -44,7 +44,7 @@
|
||||
</button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<SettingsModal v-model="showSettingsModal" />
|
||||
<SettingsModal v-if="showSettingsModal" v-model="showSettingsModal" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@ -357,7 +357,10 @@ function getDealRowObject(deal) {
|
||||
label: deal.organization,
|
||||
logo: getOrganization(deal.organization)?.organization_logo,
|
||||
},
|
||||
annual_revenue: formatNumberIntoCurrency(deal.annual_revenue),
|
||||
annual_revenue: formatNumberIntoCurrency(
|
||||
deal.annual_revenue,
|
||||
deal.currency,
|
||||
),
|
||||
status: {
|
||||
label: deal.status,
|
||||
color: getDealStatus(deal.status)?.iconColorClass,
|
||||
|
||||
@ -488,7 +488,7 @@ const fieldsLayout = createResource({
|
||||
transform: (data) => getParsedFields(data),
|
||||
})
|
||||
|
||||
function getParsedFields(sections, contacts) {
|
||||
function getParsedFields(sections) {
|
||||
sections.forEach((section) => {
|
||||
if (section.name == 'contacts_section') return
|
||||
section.fields.forEach((field) => {
|
||||
@ -583,7 +583,7 @@ const deal_contacts = createResource({
|
||||
cache: ['deal_contacts', props.dealId],
|
||||
auto: true,
|
||||
onSuccess: (data) => {
|
||||
let contactSection = fieldsLayout.data.find(
|
||||
let contactSection = fieldsLayout.data?.find(
|
||||
(section) => section.name == 'contacts_section',
|
||||
)
|
||||
if (!contactSection) return
|
||||
|
||||
@ -109,7 +109,7 @@ const rows = computed(() => {
|
||||
if (!deals.value?.data.group_by_field?.name) return []
|
||||
return getGroupedByRows(
|
||||
deals.value?.data.data,
|
||||
deals.value?.data.group_by_field
|
||||
deals.value?.data.group_by_field,
|
||||
)
|
||||
} else {
|
||||
return parseRows(deals.value?.data.data)
|
||||
@ -158,7 +158,10 @@ function parseRows(rows) {
|
||||
logo: getOrganization(deal.organization)?.organization_logo,
|
||||
}
|
||||
} else if (row == 'annual_revenue') {
|
||||
_rows[row] = formatNumberIntoCurrency(deal.annual_revenue)
|
||||
_rows[row] = formatNumberIntoCurrency(
|
||||
deal.annual_revenue,
|
||||
deal.currency,
|
||||
)
|
||||
} else if (row == 'status') {
|
||||
_rows[row] = {
|
||||
label: deal.status,
|
||||
@ -171,8 +174,8 @@ function parseRows(rows) {
|
||||
deal.sla_status == 'Failed'
|
||||
? 'red'
|
||||
: deal.sla_status == 'Fulfilled'
|
||||
? 'green'
|
||||
: 'orange'
|
||||
? 'green'
|
||||
: 'orange'
|
||||
if (value == 'First Response Due') {
|
||||
value = __(timeAgo(deal.response_by))
|
||||
tooltipText = dateFormat(deal.response_by, dateTooltipFormat)
|
||||
@ -207,7 +210,7 @@ function parseRows(rows) {
|
||||
}
|
||||
} else if (
|
||||
['first_response_time', 'first_responded_on', 'response_by'].includes(
|
||||
row
|
||||
row,
|
||||
)
|
||||
) {
|
||||
let field = row == 'response_by' ? 'response_by' : 'first_responded_on'
|
||||
|
||||
@ -58,15 +58,15 @@
|
||||
@updateField="updateField"
|
||||
/>
|
||||
<div
|
||||
v-if="detailSections.length"
|
||||
v-if="fieldsLayout.data"
|
||||
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||
>
|
||||
<div class="flex flex-col overflow-y-auto">
|
||||
<div
|
||||
v-for="(section, i) in detailSections"
|
||||
v-for="(section, i) in fieldsLayout.data"
|
||||
:key="section.label"
|
||||
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">
|
||||
<template #actions>
|
||||
@ -441,44 +441,31 @@ const tabs = computed(() => {
|
||||
return tabOptions.filter((tab) => (tab.condition ? tab.condition() : true))
|
||||
})
|
||||
|
||||
const detailSections = computed(() => {
|
||||
let data = deal.data
|
||||
if (!data) return []
|
||||
return getParsedFields(data.doctype_fields, deal_contacts.data)
|
||||
const fieldsLayout = createResource({
|
||||
url: 'crm.api.doc.get_sidebar_fields',
|
||||
cache: ['fieldsLayout', props.dealId],
|
||||
params: { doctype: 'CRM Deal', name: props.dealId },
|
||||
auto: true,
|
||||
transform: (data) => getParsedFields(data),
|
||||
})
|
||||
|
||||
function getParsedFields(sections, contacts) {
|
||||
function getParsedFields(sections) {
|
||||
sections.forEach((section) => {
|
||||
if (section.name == 'contacts_tab') {
|
||||
delete section.fields
|
||||
section.contacts =
|
||||
contacts?.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,
|
||||
}
|
||||
}) || []
|
||||
} 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 },
|
||||
})
|
||||
if (section.name == 'contacts_section') return
|
||||
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 },
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
return sections
|
||||
}
|
||||
@ -556,6 +543,23 @@ const deal_contacts = createResource({
|
||||
params: { name: props.dealId },
|
||||
cache: ['deal_contacts', props.dealId],
|
||||
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) {
|
||||
|
||||
@ -63,15 +63,15 @@
|
||||
@updateField="updateField"
|
||||
/>
|
||||
<div
|
||||
v-if="detailSections.length"
|
||||
v-if="fieldsLayout.data"
|
||||
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||
>
|
||||
<div class="flex flex-col overflow-y-auto">
|
||||
<div
|
||||
v-for="(section, i) in detailSections"
|
||||
v-for="(section, i) in fieldsLayout.data"
|
||||
:key="section.label"
|
||||
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">
|
||||
<SectionFields
|
||||
@ -367,10 +367,11 @@ watch(tabs, (value) => {
|
||||
}
|
||||
})
|
||||
|
||||
const detailSections = computed(() => {
|
||||
let data = lead.data
|
||||
if (!data) return []
|
||||
return data.doctype_fields
|
||||
const fieldsLayout = createResource({
|
||||
url: 'crm.api.doc.get_sidebar_fields',
|
||||
cache: ['fieldsLayout', props.leadId],
|
||||
params: { doctype: 'CRM Lead', name: props.leadId },
|
||||
auto: true,
|
||||
})
|
||||
|
||||
function updateField(name, value, callback) {
|
||||
|
||||
@ -103,8 +103,13 @@
|
||||
v-if="organization.doc.annual_revenue"
|
||||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<FeatherIcon name="dollar-sign" class="h-4 w-4" />
|
||||
<span class="">{{ organization.doc.annual_revenue }}</span>
|
||||
<MoneyIcon class="size-4" />
|
||||
<span class="">{{
|
||||
formatNumberIntoCurrency(
|
||||
organization.doc.annual_revenue,
|
||||
organization.doc.currency,
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<span
|
||||
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 WebsiteIcon from '@/components/Icons/WebsiteIcon.vue'
|
||||
import TerritoryIcon from '@/components/Icons/TerritoryIcon.vue'
|
||||
import MoneyIcon from '@/components/Icons/MoneyIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
||||
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
||||
@ -347,6 +353,7 @@ const deals = createListResource({
|
||||
fields: [
|
||||
'name',
|
||||
'organization',
|
||||
'currency',
|
||||
'annual_revenue',
|
||||
'status',
|
||||
'email',
|
||||
@ -405,7 +412,10 @@ function getDealRowObject(deal) {
|
||||
label: deal.organization,
|
||||
logo: props.organization?.organization_logo,
|
||||
},
|
||||
annual_revenue: formatNumberIntoCurrency(deal.annual_revenue),
|
||||
annual_revenue: formatNumberIntoCurrency(
|
||||
deal.annual_revenue,
|
||||
deal.currency,
|
||||
),
|
||||
status: {
|
||||
label: deal.status,
|
||||
color: getDealStatus(deal.status)?.iconColorClass,
|
||||
|
||||
@ -85,7 +85,7 @@ const showOrganizationModal = ref(false)
|
||||
|
||||
const currentOrganization = computed(() => {
|
||||
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') {
|
||||
_rows[row] = website(organization.website)
|
||||
} 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)) {
|
||||
_rows[row] = {
|
||||
label: dateFormat(organization[row], dateTooltipFormat),
|
||||
|
||||
@ -94,12 +94,12 @@ export function secondsToDuration(seconds) {
|
||||
return `${hours}h ${minutes}m ${_seconds}s`
|
||||
}
|
||||
|
||||
export function formatNumberIntoCurrency(value) {
|
||||
export function formatNumberIntoCurrency(value, currency = 'INR') {
|
||||
if (value) {
|
||||
return value.toLocaleString('en-IN', {
|
||||
maximumFractionDigits: 2,
|
||||
maximumFractionDigits: 0,
|
||||
style: 'currency',
|
||||
currency: 'INR',
|
||||
currency: currency ? currency : 'INR',
|
||||
})
|
||||
}
|
||||
return ''
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user