1
0
forked from test/crm

Merge pull request #228 from frappe/develop

chore: Merge develop to main
This commit is contained in:
Shariq Ansari 2024-06-20 15:22:47 +05:30 committed by GitHub
commit 9a01517d6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 869 additions and 288 deletions

View File

@ -73,6 +73,7 @@ def get_linked_deals(contact):
fields=[ fields=[
"name", "name",
"organization", "organization",
"currency",
"annual_revenue", "annual_revenue",
"status", "status",
"email", "email",

View File

@ -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:

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -47,6 +47,7 @@ class CRMOrganization(Document):
"organization_logo", "organization_logo",
"website", "website",
"industry", "industry",
"currency",
"annual_revenue", "annual_revenue",
"modified", "modified",
] ]

View File

@ -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}]'
}, },
} }

View File

@ -8,3 +8,4 @@ crm.patches.v1_0.move_crm_note_data_to_fcrm_note
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

View 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))

View File

@ -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="

View 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>

View 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>

View File

@ -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')
},
}) })
} }

View File

@ -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')
},
}) })
} }

View File

@ -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>

View File

@ -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
}) })

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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

View File

@ -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'

View File

@ -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) {

View File

@ -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) {

View File

@ -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,

View File

@ -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),

View File

@ -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 ''