Merge pull request #29 from shariquerik/custom-fields
feat: Custom fields
This commit is contained in:
commit
b58523a433
@ -45,3 +45,79 @@ def get_filterable_fields(doctype: str):
|
|||||||
res = []
|
res = []
|
||||||
res.extend(from_doc_fields)
|
res.extend(from_doc_fields)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_doctype_fields(doctype):
|
||||||
|
not_allowed_fieldtypes = [
|
||||||
|
"Section Break",
|
||||||
|
"Column Break",
|
||||||
|
]
|
||||||
|
|
||||||
|
fields = frappe.get_meta(doctype).fields
|
||||||
|
fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes]
|
||||||
|
|
||||||
|
sections = {}
|
||||||
|
section_fields = []
|
||||||
|
last_section = None
|
||||||
|
|
||||||
|
for field in fields:
|
||||||
|
if field.fieldtype == "Tab Break" and last_section:
|
||||||
|
sections[last_section]["fields"] = section_fields
|
||||||
|
last_section = None
|
||||||
|
if field.read_only:
|
||||||
|
section_fields = []
|
||||||
|
continue
|
||||||
|
if field.fieldtype == "Tab Break":
|
||||||
|
if field.read_only:
|
||||||
|
section_fields = []
|
||||||
|
continue
|
||||||
|
section_fields = []
|
||||||
|
last_section = field.fieldname
|
||||||
|
sections[field.fieldname] = {
|
||||||
|
"label": field.label,
|
||||||
|
"opened": True,
|
||||||
|
"fields": [],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
section_fields.append(get_field_obj(field))
|
||||||
|
|
||||||
|
all_fields = []
|
||||||
|
for section in sections:
|
||||||
|
all_fields.append(sections[section])
|
||||||
|
|
||||||
|
return all_fields
|
||||||
|
|
||||||
|
def get_field_obj(field):
|
||||||
|
obj = {
|
||||||
|
"label": field.label,
|
||||||
|
"type": get_type(field),
|
||||||
|
"name": field.fieldname,
|
||||||
|
}
|
||||||
|
|
||||||
|
obj["placeholder"] = "Add " + field.label.lower() + "..."
|
||||||
|
|
||||||
|
if field.fieldtype == "Link":
|
||||||
|
obj["placeholder"] = "Select " + field.label.lower() + "..."
|
||||||
|
obj["doctype"] = field.options
|
||||||
|
elif field.fieldtype == "Select":
|
||||||
|
obj["options"] = [{"label": option, "value": option} for option in field.options.split("\n")]
|
||||||
|
|
||||||
|
if field.read_only:
|
||||||
|
obj["tooltip"] = "This field is read only and cannot be edited."
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def get_type(field):
|
||||||
|
if field.fieldtype == "Data" and field.options == "Phone":
|
||||||
|
return "phone"
|
||||||
|
elif field.fieldtype == "Data" and field.options == "Email":
|
||||||
|
return "email"
|
||||||
|
elif field.fieldtype == "Check":
|
||||||
|
return "checkbox"
|
||||||
|
elif field.fieldtype == "Int":
|
||||||
|
return "number"
|
||||||
|
elif field.fieldtype in ["Small Text", "Text", "Long Text"]:
|
||||||
|
return "textarea"
|
||||||
|
elif field.read_only:
|
||||||
|
return "read_only"
|
||||||
|
return field.fieldtype.lower()
|
||||||
@ -6,23 +6,24 @@
|
|||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"naming_series",
|
"organization_tab",
|
||||||
"organization",
|
"organization",
|
||||||
"website",
|
"website",
|
||||||
"annual_revenue",
|
"annual_revenue",
|
||||||
"column_break_afce",
|
|
||||||
"deal_owner",
|
|
||||||
"close_date",
|
"close_date",
|
||||||
"status",
|
|
||||||
"probability",
|
"probability",
|
||||||
"next_step",
|
"next_step",
|
||||||
"section_break_eepu",
|
|
||||||
"lead",
|
|
||||||
"column_break_bqvs",
|
|
||||||
"contacts_tab",
|
"contacts_tab",
|
||||||
"email",
|
"email",
|
||||||
"mobile_no",
|
"mobile_no",
|
||||||
"contacts"
|
"contacts",
|
||||||
|
"others_tab",
|
||||||
|
"naming_series",
|
||||||
|
"status",
|
||||||
|
"deal_owner",
|
||||||
|
"section_break_eepu",
|
||||||
|
"lead",
|
||||||
|
"column_break_bqvs"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -39,19 +40,17 @@
|
|||||||
{
|
{
|
||||||
"fetch_from": "organization.annual_revenue",
|
"fetch_from": "organization.annual_revenue",
|
||||||
"fieldname": "annual_revenue",
|
"fieldname": "annual_revenue",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Currency",
|
||||||
"label": "Annual Revenue"
|
"label": "Amount",
|
||||||
},
|
"read_only": 1
|
||||||
{
|
|
||||||
"fieldname": "column_break_afce",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "organization.website",
|
"fetch_from": "organization.website",
|
||||||
"fieldname": "website",
|
"fieldname": "website",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Website",
|
"label": "Website",
|
||||||
"options": "URL"
|
"options": "URL",
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "close_date",
|
"fieldname": "close_date",
|
||||||
@ -92,7 +91,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "contacts_tab",
|
"fieldname": "contacts_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Contacts"
|
"label": "Contacts",
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "email",
|
"fieldname": "email",
|
||||||
@ -121,11 +121,22 @@
|
|||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Contacts",
|
"label": "Contacts",
|
||||||
"options": "CRM Contacts"
|
"options": "CRM Contacts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "others_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Others",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "organization_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Organization"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-09 19:58:15.620483",
|
"modified": "2023-11-22 17:52:31.595525",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Deal",
|
"name": "CRM Deal",
|
||||||
|
|||||||
@ -1,24 +1,16 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.desk.form.load import get_docinfo
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_lead(name):
|
def get_lead(name):
|
||||||
Lead = frappe.qb.DocType("CRM Lead")
|
Lead = frappe.qb.DocType("CRM Lead")
|
||||||
|
|
||||||
query = (
|
query = frappe.qb.from_(Lead).select("*").where(Lead.name == name).limit(1)
|
||||||
frappe.qb.from_(Lead)
|
|
||||||
.select("*")
|
|
||||||
.where(Lead.name == name)
|
|
||||||
.limit(1)
|
|
||||||
)
|
|
||||||
|
|
||||||
lead = query.run(as_dict=True)
|
lead = query.run(as_dict=True)
|
||||||
if not len(lead):
|
if not len(lead):
|
||||||
frappe.throw(_("Lead not found"), frappe.DoesNotExistError)
|
frappe.throw(_("Lead not found"), frappe.DoesNotExistError)
|
||||||
lead = lead.pop()
|
lead = lead.pop()
|
||||||
|
|
||||||
return lead
|
return lead
|
||||||
@ -8,36 +8,32 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"naming_series",
|
"details",
|
||||||
"salutation",
|
"organization",
|
||||||
"first_name",
|
"website",
|
||||||
"middle_name",
|
"industry",
|
||||||
"last_name",
|
|
||||||
"column_break_izjs",
|
|
||||||
"lead_name",
|
|
||||||
"gender",
|
|
||||||
"image",
|
|
||||||
"column_break_lcuv",
|
|
||||||
"lead_owner",
|
|
||||||
"status",
|
|
||||||
"job_title",
|
"job_title",
|
||||||
"source",
|
"source",
|
||||||
"converted",
|
"person_tab",
|
||||||
|
"salutation",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"email",
|
||||||
|
"mobile_no",
|
||||||
"organization_tab",
|
"organization_tab",
|
||||||
"section_break_uixv",
|
"section_break_uixv",
|
||||||
"organization",
|
"naming_series",
|
||||||
"no_of_employees",
|
"lead_name",
|
||||||
|
"middle_name",
|
||||||
|
"gender",
|
||||||
|
"phone",
|
||||||
"column_break_dbsv",
|
"column_break_dbsv",
|
||||||
"website",
|
"status",
|
||||||
|
"lead_owner",
|
||||||
|
"no_of_employees",
|
||||||
"annual_revenue",
|
"annual_revenue",
|
||||||
"industry",
|
"image",
|
||||||
"contact_tab",
|
"converted"
|
||||||
"section_break_ymew",
|
|
||||||
"email",
|
|
||||||
"column_break_sijm",
|
|
||||||
"mobile_no",
|
|
||||||
"column_break_sjtw",
|
|
||||||
"phone"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -67,20 +63,12 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Last Name"
|
"label": "Last Name"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_lcuv",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "gender",
|
"fieldname": "gender",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Gender",
|
"label": "Gender",
|
||||||
"options": "Gender"
|
"options": "Gender"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_izjs",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "Open",
|
"default": "Open",
|
||||||
"fieldname": "status",
|
"fieldname": "status",
|
||||||
@ -91,10 +79,6 @@
|
|||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_ymew",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "email",
|
"fieldname": "email",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
@ -110,20 +94,12 @@
|
|||||||
"options": "URL",
|
"options": "URL",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_sijm",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "mobile_no",
|
"fieldname": "mobile_no",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Mobile No",
|
"label": "Mobile No",
|
||||||
"options": "Phone"
|
"options": "Phone"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_sjtw",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "phone",
|
"fieldname": "phone",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
@ -192,12 +168,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "organization_tab",
|
"fieldname": "organization_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Organization"
|
"label": "Others",
|
||||||
},
|
"read_only": 1
|
||||||
{
|
|
||||||
"fieldname": "contact_tab",
|
|
||||||
"fieldtype": "Tab Break",
|
|
||||||
"label": "Contact"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "organization",
|
"fieldname": "organization",
|
||||||
@ -212,12 +184,22 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Converted"
|
"label": "Converted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "person_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Person"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "details",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Details"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-13 13:35:35.783003",
|
"modified": "2023-11-22 13:03:02.261001",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Lead",
|
"name": "CRM Lead",
|
||||||
|
|||||||
56
frontend/src/components/Section.vue
Normal file
56
frontend/src/components/Section.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<slot name="header" v-bind="{ opened, open, close, toggle }">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="flex h-7 max-w-fit cursor-pointer items-center gap-2 pl-2 pr-3 text-base font-semibold leading-5"
|
||||||
|
@click="toggle()"
|
||||||
|
>
|
||||||
|
<FeatherIcon
|
||||||
|
name="chevron-right"
|
||||||
|
class="h-4 text-gray-900 transition-all duration-300 ease-in-out"
|
||||||
|
:class="{ 'rotate-90': opened }"
|
||||||
|
/>
|
||||||
|
{{ label || 'Untitled' }}
|
||||||
|
</div>
|
||||||
|
<slot name="actions"></slot>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
<transition
|
||||||
|
enter-active-class="duration-300 ease-in"
|
||||||
|
leave-active-class="duration-300 ease-[cubic-bezier(0, 1, 0.5, 1)]"
|
||||||
|
enter-to-class="max-h-[200px] overflow-hidden"
|
||||||
|
leave-from-class="max-h-[200px] overflow-hidden"
|
||||||
|
enter-from-class="max-h-0 overflow-hidden"
|
||||||
|
leave-to-class="max-h-0 overflow-hidden"
|
||||||
|
>
|
||||||
|
<div v-if="opened">
|
||||||
|
<slot v-bind="{ opened, open, close, toggle }" />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { FeatherIcon } from 'frappe-ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
const props = defineProps({
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
isOpened: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
function toggle() {
|
||||||
|
opened.value = !opened.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
opened.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
opened.value = false
|
||||||
|
}
|
||||||
|
let opened = ref(props.isOpened)
|
||||||
|
</script>
|
||||||
119
frontend/src/components/SectionFields.vue
Normal file
119
frontend/src/components/SectionFields.vue
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-1.5 max-h-[300px] overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="field in fields"
|
||||||
|
:key="field.label"
|
||||||
|
class="flex items-center gap-2 px-3 text-base leading-5 first:mt-3"
|
||||||
|
>
|
||||||
|
<div class="w-[106px] shrink-0 text-gray-600">
|
||||||
|
{{ field.label }}
|
||||||
|
</div>
|
||||||
|
<div class="grid flex-1 items-center overflow-hidden min-h-[28px]">
|
||||||
|
<FormControl
|
||||||
|
v-if="
|
||||||
|
[
|
||||||
|
'email',
|
||||||
|
'number',
|
||||||
|
'date',
|
||||||
|
'password',
|
||||||
|
'textarea',
|
||||||
|
'checkbox',
|
||||||
|
].includes(field.type)
|
||||||
|
"
|
||||||
|
class="form-control"
|
||||||
|
:type="field.type"
|
||||||
|
:value="data[field.name]"
|
||||||
|
:placeholder="field.placeholder"
|
||||||
|
:debounce="500"
|
||||||
|
@change.stop="emit('update', field.name, $event.target.value)"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else-if="field.type === 'select'"
|
||||||
|
class="form-control cursor-pointer [&_select]:cursor-pointer"
|
||||||
|
type="select"
|
||||||
|
:value="data[field.name]"
|
||||||
|
:options="field.options"
|
||||||
|
:debounce="500"
|
||||||
|
@change.stop="emit('update', field.name, $event.target.value)"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
v-else-if="field.type === 'link'"
|
||||||
|
class="form-control"
|
||||||
|
:value="data[field.name]"
|
||||||
|
:doctype="field.doctype"
|
||||||
|
:placeholder="field.placeholder"
|
||||||
|
@change="(data) => emit('update', field.name, data)"
|
||||||
|
:onCreate="field.create"
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
v-else-if="field.type === 'read_only'"
|
||||||
|
class="flex h-7 cursor-pointer items-center px-2 py-1"
|
||||||
|
:text="field.tooltip"
|
||||||
|
>
|
||||||
|
{{ field.value }}
|
||||||
|
</Tooltip>
|
||||||
|
<FormControl
|
||||||
|
v-else
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
:value="data[field.name]"
|
||||||
|
:placeholder="field.placeholder"
|
||||||
|
:debounce="500"
|
||||||
|
@change.stop="emit('update', field.name, $event.target.value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ExternalLinkIcon
|
||||||
|
v-if="field.type === 'link' && field.link && data[field.name]"
|
||||||
|
class="h-4 w-4 shrink-0 cursor-pointer text-gray-600"
|
||||||
|
@click="field.link(data[field.name])"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ExternalLinkIcon from '@/components/Icons/ExternalLinkIcon.vue'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
import { FormControl, Tooltip } from 'frappe-ui'
|
||||||
|
import { defineModel } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
fields: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update'])
|
||||||
|
|
||||||
|
const data = defineModel()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.form-control input:not([type='checkbox'])),
|
||||||
|
:deep(.form-control select),
|
||||||
|
:deep(.form-control textarea),
|
||||||
|
:deep(.form-control button) {
|
||||||
|
border-color: transparent;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.form-control button) {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
:deep(.form-control [type='checkbox']) {
|
||||||
|
margin-left: 9px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.form-control button > div) {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.form-control button svg) {
|
||||||
|
color: white;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,24 +0,0 @@
|
|||||||
<template>
|
|
||||||
<slot v-bind="{ opened, open, close, toggle }"></slot>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
const props = defineProps({
|
|
||||||
isOpened: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
function toggle() {
|
|
||||||
opened.value = !opened.value
|
|
||||||
}
|
|
||||||
|
|
||||||
function open() {
|
|
||||||
opened.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
opened.value = false
|
|
||||||
}
|
|
||||||
let opened = ref(props.isOpened)
|
|
||||||
</script>
|
|
||||||
@ -96,24 +96,13 @@
|
|||||||
<div class="flex flex-1 flex-col justify-between overflow-hidden">
|
<div 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 detailSections.data"
|
||||||
:key="section.label"
|
:key="section.label"
|
||||||
class="flex flex-col p-3"
|
class="flex flex-col p-3"
|
||||||
:class="{ 'border-b': i !== detailSections.length - 1 }"
|
:class="{ 'border-b': i !== detailSections.data.length - 1 }"
|
||||||
>
|
>
|
||||||
<Toggler :is-opened="section.opened" v-slot="{ opened, toggle }">
|
<Section :is-opened="section.opened" :label="section.label">
|
||||||
<div class="flex items-center justify-between">
|
<template #actions>
|
||||||
<div
|
|
||||||
class="flex h-7 max-w-fit cursor-pointer items-center gap-2 pl-2 pr-3 text-base font-semibold leading-5"
|
|
||||||
@click="toggle()"
|
|
||||||
>
|
|
||||||
<FeatherIcon
|
|
||||||
name="chevron-right"
|
|
||||||
class="h-4 text-gray-900 transition-all duration-300 ease-in-out"
|
|
||||||
:class="{ 'rotate-90': opened }"
|
|
||||||
/>
|
|
||||||
{{ section.label }}
|
|
||||||
</div>
|
|
||||||
<div v-if="section.contacts" class="pr-2">
|
<div v-if="section.contacts" class="pr-2">
|
||||||
<Link
|
<Link
|
||||||
value=""
|
value=""
|
||||||
@ -143,180 +132,103 @@
|
|||||||
</template>
|
</template>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<transition
|
<SectionFields
|
||||||
enter-active-class="duration-300 ease-in"
|
v-if="section.fields"
|
||||||
leave-active-class="duration-300 ease-[cubic-bezier(0, 1, 0.5, 1)]"
|
:fields="section.fields"
|
||||||
enter-to-class="max-h-[200px] overflow-hidden"
|
v-model="deal.data"
|
||||||
leave-from-class="max-h-[200px] overflow-hidden"
|
@update="updateField"
|
||||||
enter-from-class="max-h-0 overflow-hidden"
|
/>
|
||||||
leave-to-class="max-h-0 overflow-hidden"
|
<div v-else>
|
||||||
>
|
<div
|
||||||
<div v-if="opened" class="flex flex-col gap-1.5">
|
v-if="section.contacts.length"
|
||||||
|
v-for="(contact, i) in section.contacts"
|
||||||
|
:key="contact.name"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="section.fields"
|
class="px-2 pb-2.5"
|
||||||
v-for="field in section.fields"
|
:class="[i == 0 ? 'pt-5' : 'pt-2.5']"
|
||||||
:key="field.label"
|
|
||||||
class="flex items-center gap-2 px-3 text-base leading-5 first:mt-3"
|
|
||||||
>
|
>
|
||||||
<div class="w-[106px] shrink-0 text-gray-600">
|
<Section :is-opened="contact.opened">
|
||||||
{{ field.label }}
|
<template #header="{ opened, toggle }">
|
||||||
</div>
|
<div
|
||||||
<div class="flex-1 overflow-hidden">
|
class="flex cursor-pointer items-center justify-between gap-2 pr-1 text-base leading-5 text-gray-700"
|
||||||
<Link
|
|
||||||
v-if="field.type === 'link'"
|
|
||||||
class="form-control"
|
|
||||||
:value="deal.data[field.name]"
|
|
||||||
:doctype="field.doctype"
|
|
||||||
:placeholder="field.placeholder"
|
|
||||||
@change="(e) => field.change(e)"
|
|
||||||
:onCreate="field.create"
|
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
v-else-if="field.type === 'date'"
|
|
||||||
type="date"
|
|
||||||
:value="deal.data[field.name]"
|
|
||||||
@change.stop="
|
|
||||||
updateDeal(field.name, $event.target.value)
|
|
||||||
"
|
|
||||||
:debounce="500"
|
|
||||||
class="form-control"
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
:text="field.tooltip"
|
|
||||||
class="flex h-7 cursor-pointer items-center px-2 py-1"
|
|
||||||
v-else-if="field.type === 'read_only'"
|
|
||||||
>
|
|
||||||
{{ field.value }}
|
|
||||||
</Tooltip>
|
|
||||||
<FormControl
|
|
||||||
v-else
|
|
||||||
type="text"
|
|
||||||
:value="deal.data[field.name]"
|
|
||||||
@change.stop="
|
|
||||||
updateDeal(field.name, $event.target.value)
|
|
||||||
"
|
|
||||||
:debounce="500"
|
|
||||||
class="form-control"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ExternalLinkIcon
|
|
||||||
v-if="
|
|
||||||
field.type === 'link' &&
|
|
||||||
field.link &&
|
|
||||||
deal.data[field.name]
|
|
||||||
"
|
|
||||||
class="h-4 w-4 shrink-0 cursor-pointer text-gray-600"
|
|
||||||
@click="field.link(deal.data[field.name])"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div
|
|
||||||
v-if="section.contacts.length"
|
|
||||||
v-for="(contact, i) in section.contacts"
|
|
||||||
:key="contact.name"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="px-2 pb-2.5"
|
|
||||||
:class="[i == 0 ? 'pt-5' : 'pt-2.5']"
|
|
||||||
>
|
|
||||||
<Toggler
|
|
||||||
:is-opened="contact.opened"
|
|
||||||
v-slot="{ opened: cOpened, toggle: cToggle }"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex cursor-pointer items-center justify-between gap-2 pr-1 text-base leading-5 text-gray-700"
|
class="flex h-7 items-center gap-2"
|
||||||
|
@click="toggle()"
|
||||||
>
|
>
|
||||||
<div
|
<Avatar
|
||||||
class="flex h-7 items-center gap-2"
|
:label="getContactByName(contact.name).full_name"
|
||||||
@click="cToggle()"
|
:image="getContactByName(contact.name).image"
|
||||||
>
|
size="md"
|
||||||
<Avatar
|
/>
|
||||||
:label="
|
{{ getContactByName(contact.name).full_name }}
|
||||||
getContactByName(contact.name).full_name
|
<Badge
|
||||||
"
|
v-if="contact.is_primary"
|
||||||
:image="getContactByName(contact.name).image"
|
class="ml-2"
|
||||||
size="md"
|
variant="outline"
|
||||||
/>
|
label="Primary"
|
||||||
{{ getContactByName(contact.name).full_name }}
|
theme="green"
|
||||||
<Badge
|
/>
|
||||||
v-if="contact.is_primary"
|
</div>
|
||||||
class="ml-2"
|
<div class="flex items-center">
|
||||||
variant="outline"
|
<Dropdown :options="contactOptions(contact)">
|
||||||
label="Primary"
|
<Button variant="ghost">
|
||||||
theme="green"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Dropdown :options="contactOptions(contact)">
|
|
||||||
<Button variant="ghost">
|
|
||||||
<FeatherIcon
|
|
||||||
name="more-horizontal"
|
|
||||||
class="h-4 text-gray-600"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
@click="
|
|
||||||
router.push({
|
|
||||||
name: 'Contact',
|
|
||||||
params: { contactId: contact.name },
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<ExternalLinkIcon class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" @click="cToggle()">
|
|
||||||
<FeatherIcon
|
<FeatherIcon
|
||||||
name="chevron-right"
|
name="more-horizontal"
|
||||||
class="h-4 w-4 text-gray-900 transition-all duration-300 ease-in-out"
|
class="h-4 text-gray-600"
|
||||||
:class="{ 'rotate-90': cOpened }"
|
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Dropdown>
|
||||||
</div>
|
<Button
|
||||||
<transition
|
variant="ghost"
|
||||||
enter-active-class="duration-300 ease-in"
|
@click="
|
||||||
leave-active-class="duration-300 ease-[cubic-bezier(0, 1, 0.5, 1)]"
|
router.push({
|
||||||
enter-to-class="max-h-[200px] overflow-hidden"
|
name: 'Contact',
|
||||||
leave-from-class="max-h-[200px] overflow-hidden"
|
params: { contactId: contact.name },
|
||||||
enter-from-class="max-h-0 overflow-hidden"
|
})
|
||||||
leave-to-class="max-h-0 overflow-hidden"
|
"
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="cOpened"
|
|
||||||
class="flex flex-col gap-1.5 text-base text-gray-800"
|
|
||||||
>
|
>
|
||||||
<div
|
<ExternalLinkIcon class="h-4 w-4" />
|
||||||
class="flex items-center gap-3 pb-1.5 pl-1 pt-4"
|
</Button>
|
||||||
>
|
<Button variant="ghost" @click="toggle()">
|
||||||
<EmailIcon class="h-4 w-4" />
|
<FeatherIcon
|
||||||
{{ getContactByName(contact.name).email_id }}
|
name="chevron-right"
|
||||||
</div>
|
class="h-4 w-4 text-gray-900 transition-all duration-300 ease-in-out"
|
||||||
<div class="flex items-center gap-3 p-1 py-1.5">
|
:class="{ 'rotate-90': opened }"
|
||||||
<PhoneIcon class="h-4 w-4" />
|
/>
|
||||||
{{ getContactByName(contact.name).mobile_no }}
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</template>
|
||||||
</Toggler>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="i != section.contacts.length - 1"
|
class="flex flex-col gap-1.5 text-base text-gray-800"
|
||||||
class="mx-2 h-px border-t border-gray-200"
|
>
|
||||||
/>
|
<div class="flex items-center gap-3 pb-1.5 pl-1 pt-4">
|
||||||
</div>
|
<EmailIcon class="h-4 w-4" />
|
||||||
<div
|
{{ getContactByName(contact.name).email_id }}
|
||||||
v-else
|
</div>
|
||||||
class="flex h-20 items-center justify-center text-base text-gray-600"
|
<div class="flex items-center gap-3 p-1 py-1.5">
|
||||||
>
|
<PhoneIcon class="h-4 w-4" />
|
||||||
No contacts added
|
{{ getContactByName(contact.name).mobile_no }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="i != section.contacts.length - 1"
|
||||||
|
class="mx-2 h-px border-t border-gray-200"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
<div
|
||||||
</Toggler>
|
v-else
|
||||||
|
class="flex h-20 items-center justify-center text-base text-gray-600"
|
||||||
|
>
|
||||||
|
No contacts added
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -353,12 +265,13 @@ import LinkIcon from '@/components/Icons/LinkIcon.vue'
|
|||||||
import ExternalLinkIcon from '@/components/Icons/ExternalLinkIcon.vue'
|
import ExternalLinkIcon from '@/components/Icons/ExternalLinkIcon.vue'
|
||||||
import SuccessIcon from '@/components/Icons/SuccessIcon.vue'
|
import SuccessIcon from '@/components/Icons/SuccessIcon.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Toggler from '@/components/Toggler.vue'
|
|
||||||
import Activities from '@/components/Activities.vue'
|
import Activities from '@/components/Activities.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
import Section from '@/components/Section.vue'
|
||||||
|
import SectionFields from '@/components/SectionFields.vue'
|
||||||
import {
|
import {
|
||||||
dealStatuses,
|
dealStatuses,
|
||||||
statusDropdownOptions,
|
statusDropdownOptions,
|
||||||
@ -474,78 +387,53 @@ const tabs = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const detailSections = computed(() => {
|
const detailSections = createResource({
|
||||||
return [
|
url: 'crm.api.doc.get_doctype_fields',
|
||||||
{
|
params: { doctype: 'CRM Deal' },
|
||||||
label: 'Organization',
|
cache: 'dealFields',
|
||||||
opened: true,
|
auto: true,
|
||||||
fields: [
|
transform: (data) => {
|
||||||
{
|
return getParsedFields(data)
|
||||||
label: 'Organization',
|
},
|
||||||
type: 'link',
|
|
||||||
name: 'organization',
|
|
||||||
placeholder: 'Select organization',
|
|
||||||
doctype: 'CRM Organization',
|
|
||||||
change: (data) => updateField('organization', data),
|
|
||||||
create: (value, close) => {
|
|
||||||
_organization.value.organization_name = value
|
|
||||||
showOrganizationModal.value = true
|
|
||||||
close()
|
|
||||||
},
|
|
||||||
link: () => {
|
|
||||||
router.push({
|
|
||||||
name: 'Organization',
|
|
||||||
params: { organizationId: organization.value.name },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Website',
|
|
||||||
type: 'read_only',
|
|
||||||
name: 'website',
|
|
||||||
value: organization.value?.website,
|
|
||||||
tooltip:
|
|
||||||
'It is a read only field, value is fetched from organization',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Amount',
|
|
||||||
type: 'read_only',
|
|
||||||
name: 'annual_revenue',
|
|
||||||
value: organization.value?.annual_revenue,
|
|
||||||
tooltip:
|
|
||||||
'It is a read only field, value is fetched from organization',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Close date',
|
|
||||||
type: 'date',
|
|
||||||
name: 'close_date',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Probability',
|
|
||||||
type: 'data',
|
|
||||||
name: 'probability',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Next step',
|
|
||||||
type: 'data',
|
|
||||||
name: 'next_step',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Contacts',
|
|
||||||
opened: true,
|
|
||||||
contacts: deal.data.contacts.map((contact) => {
|
|
||||||
return {
|
|
||||||
name: contact.contact,
|
|
||||||
is_primary: contact.is_primary,
|
|
||||||
opened: false,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function getParsedFields(sections) {
|
||||||
|
sections.forEach((section) => {
|
||||||
|
section.fields.forEach((field) => {
|
||||||
|
if (['website', 'annual_revenue'].includes(field.name)) {
|
||||||
|
field.value = organization.value?.[field.name]
|
||||||
|
field.tooltip =
|
||||||
|
'This field is read-only and is fetched from the organization'
|
||||||
|
} else if (field.name == 'organization') {
|
||||||
|
field.create = (value, close) => {
|
||||||
|
_organization.value.organization_name = value
|
||||||
|
showOrganizationModal.value = true
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
field.link = () =>
|
||||||
|
router.push({
|
||||||
|
name: 'Organization',
|
||||||
|
params: { organizationId: deal.data.organization },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let contactSection = {
|
||||||
|
label: 'Contacts',
|
||||||
|
opened: true,
|
||||||
|
contacts: deal.data.contacts.map((contact) => {
|
||||||
|
return {
|
||||||
|
name: contact.contact,
|
||||||
|
is_primary: contact.is_primary,
|
||||||
|
opened: false,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...sections, contactSection]
|
||||||
|
}
|
||||||
|
|
||||||
const showContactModal = ref(false)
|
const showContactModal = ref(false)
|
||||||
const _contact = ref({})
|
const _contact = ref({})
|
||||||
|
|
||||||
@ -628,26 +516,3 @@ function updateField(name, value, callback) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
:deep(.form-control input),
|
|
||||||
:deep(.form-control button) {
|
|
||||||
border-color: transparent;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.form-control button) {
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.form-control button > div) {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.form-control button svg) {
|
|
||||||
color: white;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -57,7 +57,7 @@
|
|||||||
:validateFile="validateFile"
|
:validateFile="validateFile"
|
||||||
>
|
>
|
||||||
<template #default="{ openFileSelector, error }">
|
<template #default="{ openFileSelector, error }">
|
||||||
<div class="flex items-center justify-start gap-5 p-5">
|
<div class="flex items-center justify-start gap-5 border-b p-5">
|
||||||
<div class="group relative h-[88px] w-[88px]">
|
<div class="group relative h-[88px] w-[88px]">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="3xl"
|
size="3xl"
|
||||||
@ -135,143 +135,18 @@
|
|||||||
<div class="flex flex-1 flex-col justify-between overflow-hidden">
|
<div 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 in detailSections"
|
v-for="(section, i) in detailSections.data"
|
||||||
:key="section.label"
|
:key="section.label"
|
||||||
class="flex flex-col"
|
class="flex flex-col p-3"
|
||||||
|
:class="{ 'border-b': i !== detailSections.data.length - 1 }"
|
||||||
>
|
>
|
||||||
<Toggler :is-opened="section.opened" v-slot="{ opened, toggle }">
|
<Section :is-opened="section.opened" :label="section.label">
|
||||||
<div class="sticky top-0 z-10 border-t bg-white p-3">
|
<SectionFields
|
||||||
<div
|
:fields="section.fields"
|
||||||
class="flex max-w-fit cursor-pointer items-center gap-2 px-2 text-base font-semibold leading-5"
|
v-model="lead.data"
|
||||||
@click="toggle()"
|
@update="updateField"
|
||||||
>
|
/>
|
||||||
<FeatherIcon
|
</Section>
|
||||||
name="chevron-right"
|
|
||||||
class="h-4 text-gray-600 transition-all duration-300 ease-in-out"
|
|
||||||
:class="{ 'rotate-90': opened }"
|
|
||||||
/>
|
|
||||||
{{ section.label }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<transition
|
|
||||||
enter-active-class="duration-300 ease-in"
|
|
||||||
leave-active-class="duration-300 ease-[cubic-bezier(0, 1, 0.5, 1)]"
|
|
||||||
enter-to-class="max-h-[200px] overflow-hidden"
|
|
||||||
leave-from-class="max-h-[200px] overflow-hidden"
|
|
||||||
enter-from-class="max-h-0 overflow-hidden"
|
|
||||||
leave-to-class="max-h-0 overflow-hidden"
|
|
||||||
>
|
|
||||||
<div v-if="opened" class="flex flex-col gap-1.5 px-3">
|
|
||||||
<div
|
|
||||||
v-for="field in section.fields"
|
|
||||||
:key="field.name"
|
|
||||||
class="flex items-center gap-2 px-3 text-base leading-5 last:mb-3"
|
|
||||||
>
|
|
||||||
<div class="w-[106px] shrink-0 text-gray-600">
|
|
||||||
{{ field.label }}
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 overflow-hidden">
|
|
||||||
<FormControl
|
|
||||||
v-if="field.type === 'select'"
|
|
||||||
type="select"
|
|
||||||
:options="field.options"
|
|
||||||
:value="lead.data[field.name]"
|
|
||||||
@change.stop="
|
|
||||||
updateLead(field.name, $event.target.value)
|
|
||||||
"
|
|
||||||
:debounce="500"
|
|
||||||
class="form-control cursor-pointer [&_select]:cursor-pointer"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<IndicatorIcon
|
|
||||||
:class="leadStatuses[lead.data[field.name]].color"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl
|
|
||||||
v-else-if="field.type === 'email'"
|
|
||||||
type="email"
|
|
||||||
class="form-control"
|
|
||||||
:value="lead.data[field.name]"
|
|
||||||
@change.stop="
|
|
||||||
updateLead(field.name, $event.target.value)
|
|
||||||
"
|
|
||||||
:debounce="500"
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
v-else-if="field.type === 'link'"
|
|
||||||
class="form-control"
|
|
||||||
:value="lead.data[field.name]"
|
|
||||||
:doctype="field.doctype"
|
|
||||||
:placeholder="field.placeholder"
|
|
||||||
@change="(e) => field.change(e)"
|
|
||||||
:onCreate="field.create"
|
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
v-else-if="field.type === 'user'"
|
|
||||||
type="autocomplete"
|
|
||||||
:options="activeAgents"
|
|
||||||
:value="getUser(lead.data[field.name]).full_name"
|
|
||||||
@change="
|
|
||||||
(option) => updateField('lead_owner', option.email)
|
|
||||||
"
|
|
||||||
class="form-control"
|
|
||||||
:placeholder="field.placeholder"
|
|
||||||
>
|
|
||||||
<template #target="{ togglePopover }">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
@click="togglePopover()"
|
|
||||||
:label="getUser(lead.data[field.name]).full_name"
|
|
||||||
class="w-full !justify-start"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<UserAvatar
|
|
||||||
:user="lead.data[field.name]"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
<template #item-prefix="{ option }">
|
|
||||||
<UserAvatar
|
|
||||||
class="mr-2"
|
|
||||||
:user="option.email"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</FormControl>
|
|
||||||
<Tooltip
|
|
||||||
:text="field.tooltip"
|
|
||||||
class="flex h-7 cursor-pointer items-center px-2 py-1"
|
|
||||||
v-else-if="field.type === 'read_only'"
|
|
||||||
>
|
|
||||||
{{ field.value }}
|
|
||||||
</Tooltip>
|
|
||||||
<FormControl
|
|
||||||
v-else
|
|
||||||
type="text"
|
|
||||||
:value="lead.data[field.name]"
|
|
||||||
@change.stop="
|
|
||||||
updateLead(field.name, $event.target.value)
|
|
||||||
"
|
|
||||||
class="form-control"
|
|
||||||
:debounce="500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ExternalLinkIcon
|
|
||||||
v-if="
|
|
||||||
field.type === 'link' &&
|
|
||||||
field.link &&
|
|
||||||
lead.data[field.name]
|
|
||||||
"
|
|
||||||
class="h-4 w-4 shrink-0 cursor-pointer text-gray-600"
|
|
||||||
@click="field.link(lead.data[field.name])"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</Toggler>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -298,13 +173,12 @@ import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
|||||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||||
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
||||||
import LinkIcon from '@/components/Icons/LinkIcon.vue'
|
import LinkIcon from '@/components/Icons/LinkIcon.vue'
|
||||||
import ExternalLinkIcon from '@/components/Icons/ExternalLinkIcon.vue'
|
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Toggler from '@/components/Toggler.vue'
|
|
||||||
import Activities from '@/components/Activities.vue'
|
import Activities from '@/components/Activities.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Section from '@/components/Section.vue'
|
||||||
|
import SectionFields from '@/components/SectionFields.vue'
|
||||||
import {
|
import {
|
||||||
leadStatuses,
|
leadStatuses,
|
||||||
statusDropdownOptions,
|
statusDropdownOptions,
|
||||||
@ -427,98 +301,41 @@ function validateFile(file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const detailSections = computed(() => {
|
const detailSections = createResource({
|
||||||
return [
|
url: 'crm.api.doc.get_doctype_fields',
|
||||||
{
|
params: { doctype: 'CRM Lead' },
|
||||||
label: 'Details',
|
cache: 'leadFields',
|
||||||
opened: true,
|
auto: true,
|
||||||
fields: [
|
transform: (data) => {
|
||||||
{
|
return getParsedFields(data)
|
||||||
label: 'Organization',
|
},
|
||||||
type: 'link',
|
|
||||||
name: 'organization',
|
|
||||||
placeholder: 'Select organization',
|
|
||||||
doctype: 'CRM Organization',
|
|
||||||
change: (data) => data && updateField('organization', data),
|
|
||||||
create: (value, close) => {
|
|
||||||
_organization.value.organization_name = value
|
|
||||||
showOrganizationModal.value = true
|
|
||||||
close()
|
|
||||||
},
|
|
||||||
link: () =>
|
|
||||||
router.push({
|
|
||||||
name: 'Organization',
|
|
||||||
params: { organizationId: lead.data.organization },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Website',
|
|
||||||
type: 'read_only',
|
|
||||||
name: 'website',
|
|
||||||
value: organization.value?.website,
|
|
||||||
tooltip:
|
|
||||||
'It is a read only field, value is fetched from organization',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Industry',
|
|
||||||
type: 'read_only',
|
|
||||||
name: 'industry',
|
|
||||||
value: organization.value?.industry,
|
|
||||||
tooltip:
|
|
||||||
'It is a read only field, value is fetched from organization',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Job title',
|
|
||||||
type: 'data',
|
|
||||||
name: 'job_title',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Source',
|
|
||||||
type: 'link',
|
|
||||||
name: 'source',
|
|
||||||
placeholder: 'Select source...',
|
|
||||||
doctype: 'CRM Lead Source',
|
|
||||||
change: (data) => updateField('source', data),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Person',
|
|
||||||
opened: true,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
label: 'Salutation',
|
|
||||||
type: 'link',
|
|
||||||
name: 'salutation',
|
|
||||||
placeholder: 'Mr./Mrs./Ms...',
|
|
||||||
doctype: 'Salutation',
|
|
||||||
change: (data) => updateField('salutation', data),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'First name',
|
|
||||||
type: 'data',
|
|
||||||
name: 'first_name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Last name',
|
|
||||||
type: 'data',
|
|
||||||
name: 'last_name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Email',
|
|
||||||
type: 'email',
|
|
||||||
name: 'email',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Mobile no.',
|
|
||||||
type: 'phone',
|
|
||||||
name: 'mobile_no',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function getParsedFields(sections) {
|
||||||
|
sections.forEach((section) => {
|
||||||
|
section.fields.forEach((field) => {
|
||||||
|
if (['website', 'industry'].includes(field.name)) {
|
||||||
|
field.value = organization.value?.[field.name]
|
||||||
|
field.tooltip =
|
||||||
|
'This field is read-only and is fetched from the organization'
|
||||||
|
} else if (field.name == 'organization') {
|
||||||
|
field.create = (value, close) => {
|
||||||
|
_organization.value.organization_name = value
|
||||||
|
showOrganizationModal.value = true
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
field.link = () =>
|
||||||
|
router.push({
|
||||||
|
name: 'Organization',
|
||||||
|
params: { organizationId: lead.data.organization },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return sections
|
||||||
|
}
|
||||||
|
|
||||||
const organization = computed(() => {
|
const organization = computed(() => {
|
||||||
return getOrganization(lead.data.organization)
|
return getOrganization(lead.data.organization)
|
||||||
})
|
})
|
||||||
@ -540,27 +357,3 @@ function updateField(name, value, callback) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
:deep(.form-control input),
|
|
||||||
:deep(.form-control select),
|
|
||||||
:deep(.form-control button) {
|
|
||||||
border-color: transparent;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.form-control button) {
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.form-control button > div) {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.form-control button svg) {
|
|
||||||
color: white;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user