commit
b4c766f513
176
crm/api/doc.py
176
crm/api/doc.py
@ -78,12 +78,7 @@ def get_filterable_fields(doctype: str):
|
||||
# append standard fields (getting error when using frappe.model.std_fields)
|
||||
standard_fields = [
|
||||
{"fieldname": "name", "fieldtype": "Link", "label": "ID", "options": doctype},
|
||||
{
|
||||
"fieldname": "owner",
|
||||
"fieldtype": "Link",
|
||||
"label": "Created By",
|
||||
"options": "User"
|
||||
},
|
||||
{"fieldname": "owner", "fieldtype": "Link", "label": "Created By", "options": "User"},
|
||||
{
|
||||
"fieldname": "modified_by",
|
||||
"fieldtype": "Link",
|
||||
@ -98,10 +93,7 @@ def get_filterable_fields(doctype: str):
|
||||
{"fieldname": "modified", "fieldtype": "Datetime", "label": "Last Updated On"},
|
||||
]
|
||||
for field in standard_fields:
|
||||
if (
|
||||
field.get("fieldname") not in restricted_fields and
|
||||
field.get("fieldtype") in allowed_fieldtypes
|
||||
):
|
||||
if field.get("fieldname") not in restricted_fields and field.get("fieldtype") in allowed_fieldtypes:
|
||||
field["name"] = field.get("fieldname")
|
||||
res.append(field)
|
||||
|
||||
@ -128,7 +120,11 @@ def get_group_by_fields(doctype: str):
|
||||
]
|
||||
|
||||
fields = frappe.get_meta(doctype).fields
|
||||
fields = [field for field in fields if field.fieldtype not in no_value_fields and field.fieldtype in allowed_fieldtypes]
|
||||
fields = [
|
||||
field
|
||||
for field in fields
|
||||
if field.fieldtype not in no_value_fields and field.fieldtype in allowed_fieldtypes
|
||||
]
|
||||
fields = [
|
||||
{
|
||||
"label": _(field.label),
|
||||
@ -176,6 +172,7 @@ def get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fi
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_quick_filters(doctype: str):
|
||||
meta = frappe.get_meta(doctype)
|
||||
@ -183,23 +180,25 @@ def get_quick_filters(doctype: str):
|
||||
quick_filters = []
|
||||
|
||||
for field in fields:
|
||||
|
||||
if field.fieldtype == "Select":
|
||||
field.options = field.options.split("\n")
|
||||
field.options = [{"label": option, "value": option} for option in field.options]
|
||||
field.options.insert(0, {"label": "", "value": ""})
|
||||
quick_filters.append({
|
||||
"label": _(field.label),
|
||||
"name": field.fieldname,
|
||||
"type": field.fieldtype,
|
||||
"options": field.options,
|
||||
})
|
||||
quick_filters.append(
|
||||
{
|
||||
"label": _(field.label),
|
||||
"name": field.fieldname,
|
||||
"type": field.fieldtype,
|
||||
"options": field.options,
|
||||
}
|
||||
)
|
||||
|
||||
if doctype == "CRM Lead":
|
||||
quick_filters = [filter for filter in quick_filters if filter.get("name") != "converted"]
|
||||
|
||||
return quick_filters
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_data(
|
||||
doctype: str,
|
||||
@ -223,9 +222,9 @@ def get_data(
|
||||
kanban_fields = frappe.parse_json(kanban_fields or "[]")
|
||||
kanban_columns = frappe.parse_json(kanban_columns or "[]")
|
||||
|
||||
custom_view_name = view.get('custom_view_name') if view else None
|
||||
view_type = view.get('view_type') if view else None
|
||||
group_by_field = view.get('group_by_field') if view else None
|
||||
custom_view_name = view.get("custom_view_name") if view else None
|
||||
view_type = view.get("view_type") if view else None
|
||||
group_by_field = view.get("group_by_field") if view else None
|
||||
|
||||
for key in filters:
|
||||
value = filters[key]
|
||||
@ -268,7 +267,7 @@ def get_data(
|
||||
|
||||
default_view_filters = {
|
||||
"dt": doctype,
|
||||
"type": view_type or 'list',
|
||||
"type": view_type or "list",
|
||||
"is_default": 1,
|
||||
"user": frappe.session.user,
|
||||
}
|
||||
@ -295,13 +294,16 @@ def get_data(
|
||||
if group_by_field and group_by_field not in rows:
|
||||
rows.append(group_by_field)
|
||||
|
||||
data = frappe.get_list(
|
||||
doctype,
|
||||
fields=rows,
|
||||
filters=filters,
|
||||
order_by=order_by,
|
||||
page_length=page_length,
|
||||
) or []
|
||||
data = (
|
||||
frappe.get_list(
|
||||
doctype,
|
||||
fields=rows,
|
||||
filters=filters,
|
||||
order_by=order_by,
|
||||
page_length=page_length,
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
if view_type == "kanban":
|
||||
if not rows:
|
||||
@ -336,9 +338,9 @@ def get_data(
|
||||
rows.append(field)
|
||||
|
||||
for kc in kanban_columns:
|
||||
column_filters = { column_field: kc.get('name') }
|
||||
column_filters = {column_field: kc.get("name")}
|
||||
order = kc.get("order")
|
||||
if column_field in filters and filters.get(column_field) != kc.name or kc.get('delete'):
|
||||
if column_field in filters and filters.get(column_field) != kc.name or kc.get("delete"):
|
||||
column_data = []
|
||||
else:
|
||||
column_filters.update(filters.copy())
|
||||
@ -348,7 +350,9 @@ def get_data(
|
||||
page_length = kc.get("page_length")
|
||||
|
||||
if order:
|
||||
column_data = get_records_based_on_order(doctype, rows, column_filters, page_length, order)
|
||||
column_data = get_records_based_on_order(
|
||||
doctype, rows, column_filters, page_length, order
|
||||
)
|
||||
else:
|
||||
column_data = frappe.get_list(
|
||||
doctype,
|
||||
@ -359,9 +363,11 @@ def get_data(
|
||||
)
|
||||
|
||||
new_filters = filters.copy()
|
||||
new_filters.update({ column_field: kc.get('name') })
|
||||
new_filters.update({column_field: kc.get("name")})
|
||||
|
||||
all_count = len(frappe.get_list(doctype, filters=convert_filter_to_tuple(doctype, new_filters)))
|
||||
all_count = len(
|
||||
frappe.get_list(doctype, filters=convert_filter_to_tuple(doctype, new_filters))
|
||||
)
|
||||
|
||||
kc["all_count"] = all_count
|
||||
kc["count"] = len(column_data)
|
||||
@ -371,8 +377,8 @@ def get_data(
|
||||
|
||||
if order:
|
||||
column_data = sorted(
|
||||
column_data, key=lambda x: order.index(x.get("name"))
|
||||
if x.get("name") in order else len(order)
|
||||
column_data,
|
||||
key=lambda x: order.index(x.get("name")) if x.get("name") in order else len(order),
|
||||
)
|
||||
|
||||
data.append({"column": kc, "fields": kanban_fields, "data": column_data})
|
||||
@ -406,8 +412,8 @@ def get_data(
|
||||
]
|
||||
|
||||
for field in std_fields:
|
||||
if field.get('value') not in rows:
|
||||
rows.append(field.get('value'))
|
||||
if field.get("value") not in rows:
|
||||
rows.append(field.get("value"))
|
||||
if field not in fields:
|
||||
field["label"] = _(field["label"])
|
||||
fields.append(field)
|
||||
@ -416,6 +422,7 @@ def get_data(
|
||||
is_default = frappe.db.get_value("CRM View Settings", custom_view_name, "load_default_columns")
|
||||
|
||||
if group_by_field and view_type == "group_by":
|
||||
|
||||
def get_options(type, options):
|
||||
if type == "Select":
|
||||
return [option for option in options.split("\n")]
|
||||
@ -428,7 +435,9 @@ def get_data(
|
||||
|
||||
if order_by and group_by_field in order_by:
|
||||
order_by_fields = order_by.split(",")
|
||||
order_by_fields = [(field.split(" ")[0], field.split(" ")[1]) for field in order_by_fields]
|
||||
order_by_fields = [
|
||||
(field.split(" ")[0], field.split(" ")[1]) for field in order_by_fields
|
||||
]
|
||||
if (group_by_field, "asc") in order_by_fields:
|
||||
options.sort()
|
||||
elif (group_by_field, "desc") in order_by_fields:
|
||||
@ -467,6 +476,7 @@ def get_data(
|
||||
"view_type": view_type,
|
||||
}
|
||||
|
||||
|
||||
def convert_filter_to_tuple(doctype, filters):
|
||||
if isinstance(filters, dict):
|
||||
filters_items = filters.items()
|
||||
@ -504,6 +514,7 @@ def get_records_based_on_order(doctype, rows, filters, page_length, order):
|
||||
|
||||
return records
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
|
||||
not_allowed_fieldtypes = [
|
||||
@ -521,12 +532,7 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
|
||||
|
||||
standard_fields = [
|
||||
{"fieldname": "name", "fieldtype": "Link", "label": "ID", "options": doctype},
|
||||
{
|
||||
"fieldname": "owner",
|
||||
"fieldtype": "Link",
|
||||
"label": "Created By",
|
||||
"options": "User"
|
||||
},
|
||||
{"fieldname": "owner", "fieldtype": "Link", "label": "Created By", "options": "User"},
|
||||
{
|
||||
"fieldname": "modified_by",
|
||||
"fieldtype": "Link",
|
||||
@ -542,7 +548,7 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
|
||||
]
|
||||
|
||||
for field in standard_fields:
|
||||
if not restricted_fieldtypes or field.get('fieldtype') not in restricted_fieldtypes:
|
||||
if not restricted_fieldtypes or field.get("fieldtype") not in restricted_fieldtypes:
|
||||
fields.append(field)
|
||||
|
||||
if as_array:
|
||||
@ -550,10 +556,11 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
|
||||
|
||||
fields_meta = {}
|
||||
for field in fields:
|
||||
fields_meta[field.get('fieldname')] = field
|
||||
fields_meta[field.get("fieldname")] = field
|
||||
|
||||
return fields_meta
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_sidebar_fields(doctype, name):
|
||||
if not frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}):
|
||||
@ -562,7 +569,7 @@ def get_sidebar_fields(doctype, name):
|
||||
|
||||
if not layout:
|
||||
return []
|
||||
|
||||
|
||||
layout = json.loads(layout)
|
||||
|
||||
not_allowed_fieldtypes = [
|
||||
@ -600,6 +607,7 @@ def get_sidebar_fields(doctype, name):
|
||||
|
||||
return layout
|
||||
|
||||
|
||||
def get_field_obj(field):
|
||||
obj = {
|
||||
"label": field.label,
|
||||
@ -641,6 +649,7 @@ def get_type(field):
|
||||
return "read_only"
|
||||
return field.fieldtype.lower()
|
||||
|
||||
|
||||
def get_assigned_users(doctype, name, default_assigned_to=None):
|
||||
assigned_users = frappe.get_all(
|
||||
"ToDo",
|
||||
@ -671,32 +680,55 @@ def get_fields(doctype: str, allow_all_fieldtypes: bool = False):
|
||||
_fields = []
|
||||
|
||||
for field in fields:
|
||||
if (
|
||||
field.fieldtype not in not_allowed_fieldtypes
|
||||
and field.fieldname
|
||||
):
|
||||
_fields.append({
|
||||
"label": field.label,
|
||||
"type": field.fieldtype,
|
||||
"value": field.fieldname,
|
||||
"options": field.options,
|
||||
"mandatory": field.reqd,
|
||||
"read_only": field.read_only,
|
||||
"hidden": field.hidden,
|
||||
"depends_on": field.depends_on,
|
||||
"mandatory_depends_on": field.mandatory_depends_on,
|
||||
"read_only_depends_on": field.read_only_depends_on,
|
||||
"link_filters": field.get("link_filters"),
|
||||
"placeholder": field.get("placeholder"),
|
||||
})
|
||||
if field.fieldtype not in not_allowed_fieldtypes and field.fieldname:
|
||||
_fields.append(
|
||||
{
|
||||
"label": field.label,
|
||||
"type": field.fieldtype,
|
||||
"value": field.fieldname,
|
||||
"options": field.options,
|
||||
"mandatory": field.reqd,
|
||||
"read_only": field.read_only,
|
||||
"hidden": field.hidden,
|
||||
"depends_on": field.depends_on,
|
||||
"mandatory_depends_on": field.mandatory_depends_on,
|
||||
"read_only_depends_on": field.read_only_depends_on,
|
||||
"link_filters": field.get("link_filters"),
|
||||
"placeholder": field.get("placeholder"),
|
||||
}
|
||||
)
|
||||
|
||||
return _fields
|
||||
|
||||
|
||||
def getCounts(d, doctype):
|
||||
d["_email_count"] = frappe.db.count("Communication", filters={"reference_doctype": doctype, "reference_name": d.get("name"), "communication_type": "Communication"}) or 0
|
||||
d["_email_count"] = d["_email_count"] + frappe.db.count("Communication", filters={"reference_doctype": doctype, "reference_name": d.get("name"), "communication_type": "Automated Message"})
|
||||
d["_comment_count"] = frappe.db.count("Comment", filters={"reference_doctype": doctype, "reference_name": d.get("name"), "comment_type": "Comment"})
|
||||
d["_task_count"] = frappe.db.count("CRM Task", filters={"reference_doctype": doctype, "reference_docname": d.get("name")})
|
||||
d["_note_count"] = frappe.db.count("FCRM Note", filters={"reference_doctype": doctype, "reference_docname": d.get("name")})
|
||||
return d
|
||||
d["_email_count"] = (
|
||||
frappe.db.count(
|
||||
"Communication",
|
||||
filters={
|
||||
"reference_doctype": doctype,
|
||||
"reference_name": d.get("name"),
|
||||
"communication_type": "Communication",
|
||||
},
|
||||
)
|
||||
or 0
|
||||
)
|
||||
d["_email_count"] = d["_email_count"] + frappe.db.count(
|
||||
"Communication",
|
||||
filters={
|
||||
"reference_doctype": doctype,
|
||||
"reference_name": d.get("name"),
|
||||
"communication_type": "Automated Message",
|
||||
},
|
||||
)
|
||||
d["_comment_count"] = frappe.db.count(
|
||||
"Comment",
|
||||
filters={"reference_doctype": doctype, "reference_name": d.get("name"), "comment_type": "Comment"},
|
||||
)
|
||||
d["_task_count"] = frappe.db.count(
|
||||
"CRM Task", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}
|
||||
)
|
||||
d["_note_count"] = frappe.db.count(
|
||||
"FCRM Note", filters={"reference_doctype": doctype, "reference_docname": d.get("name")}
|
||||
)
|
||||
return d
|
||||
|
||||
@ -1,38 +1,19 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from crm.api.doc import get_fields_meta, get_assigned_users
|
||||
from crm.api.doc import get_assigned_users, get_fields_meta
|
||||
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_deal(name):
|
||||
Deal = frappe.qb.DocType("CRM Deal")
|
||||
deal = frappe.get_doc("CRM Deal", name).as_dict()
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(Deal)
|
||||
.select("*")
|
||||
.where(Deal.name == name)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
deal = query.run(as_dict=True)
|
||||
if not len(deal):
|
||||
frappe.throw(_("Deal not found"), frappe.DoesNotExistError)
|
||||
deal = deal.pop()
|
||||
|
||||
|
||||
deal["contacts"] = frappe.get_all(
|
||||
"CRM Contacts",
|
||||
filters={"parenttype": "CRM Deal", "parent": deal.name},
|
||||
fields=["contact", "is_primary"],
|
||||
)
|
||||
|
||||
deal["doctype"] = "CRM Deal"
|
||||
deal["fields_meta"] = get_fields_meta("CRM Deal")
|
||||
deal["_form_script"] = get_form_script('CRM Deal')
|
||||
deal["fields_meta"] = get_fields_meta("CRM Deal")
|
||||
deal["_form_script"] = get_form_script("CRM Deal")
|
||||
deal["_assign"] = get_assigned_users("CRM Deal", deal.name, deal.owner)
|
||||
return deal
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_deal_contacts(name):
|
||||
contacts = frappe.get_all(
|
||||
@ -44,16 +25,19 @@ def get_deal_contacts(name):
|
||||
for contact in contacts:
|
||||
is_primary = contact.is_primary
|
||||
contact = frappe.get_doc("Contact", contact.contact).as_dict()
|
||||
|
||||
def get_primary_email(contact):
|
||||
for email in contact.email_ids:
|
||||
if email.is_primary:
|
||||
return email.email_id
|
||||
return contact.email_ids[0].email_id if contact.email_ids else ""
|
||||
|
||||
def get_primary_mobile_no(contact):
|
||||
for phone in contact.phone_nos:
|
||||
if phone.is_primary:
|
||||
return phone.phone
|
||||
return contact.phone_nos[0].phone if contact.phone_nos else ""
|
||||
|
||||
_contact = {
|
||||
"name": contact.name,
|
||||
"image": contact.image,
|
||||
@ -63,4 +47,4 @@ def get_deal_contacts(name):
|
||||
"is_primary": is_primary,
|
||||
}
|
||||
deal_contacts.append(_contact)
|
||||
return deal_contacts
|
||||
return deal_contacts
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Type",
|
||||
"options": "Quick Entry\nSide Panel"
|
||||
"options": "Quick Entry\nSide Panel\nData Fields"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ttpm",
|
||||
@ -46,7 +46,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-13 15:10:01.612851",
|
||||
"modified": "2024-12-05 13:29:37.021412",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Fields Layout",
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
@ -10,46 +11,54 @@ from frappe.model.document import Document
|
||||
class CRMFieldsLayout(Document):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_fields_layout(doctype: str, type: str):
|
||||
sections = []
|
||||
tabs = []
|
||||
if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}):
|
||||
layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type})
|
||||
else:
|
||||
return []
|
||||
|
||||
if layout.layout:
|
||||
sections = json.loads(layout.layout)
|
||||
tabs = json.loads(layout.layout)
|
||||
|
||||
has_tabs = tabs[0].get("sections") if tabs and tabs[0] else False
|
||||
|
||||
if not has_tabs:
|
||||
tabs = [{"no_tabs": True, "sections": tabs}]
|
||||
|
||||
allowed_fields = []
|
||||
for section in sections:
|
||||
if not section.get("fields"):
|
||||
continue
|
||||
allowed_fields.extend(section.get("fields"))
|
||||
for tab in tabs:
|
||||
for section in tab.get("sections"):
|
||||
if not section.get("fields"):
|
||||
continue
|
||||
allowed_fields.extend(section.get("fields"))
|
||||
|
||||
fields = frappe.get_meta(doctype).fields
|
||||
fields = [field for field in fields if field.fieldname in allowed_fields]
|
||||
|
||||
for section in sections:
|
||||
for field in section.get("fields") if section.get("fields") else []:
|
||||
field = next((f for f in fields if f.fieldname == field), None)
|
||||
if field:
|
||||
if field.fieldtype == "Select" and field.options:
|
||||
field.options = field.options.split("\n")
|
||||
field.options = [{"label": _(option), "value": option} for option in field.options]
|
||||
field.options.insert(0, {"label": "", "value": ""})
|
||||
field = {
|
||||
"label": _(field.label),
|
||||
"name": field.fieldname,
|
||||
"type": field.fieldtype,
|
||||
"options": field.options,
|
||||
"mandatory": field.reqd,
|
||||
"placeholder": field.get("placeholder"),
|
||||
"filters": field.get("link_filters")
|
||||
}
|
||||
section["fields"][section.get("fields").index(field["name"])] = field
|
||||
for tab in tabs:
|
||||
for section in tab.get("sections"):
|
||||
for field in section.get("fields") if section.get("fields") else []:
|
||||
field = next((f for f in fields if f.fieldname == field), None)
|
||||
if field:
|
||||
if field.fieldtype == "Select" and field.options:
|
||||
field.options = field.options.split("\n")
|
||||
field.options = [{"label": _(option), "value": option} for option in field.options]
|
||||
field.options.insert(0, {"label": "", "value": ""})
|
||||
field = {
|
||||
"label": _(field.label),
|
||||
"name": field.fieldname,
|
||||
"type": field.fieldtype,
|
||||
"options": field.options,
|
||||
"mandatory": field.reqd,
|
||||
"placeholder": field.get("placeholder"),
|
||||
"filters": field.get("link_filters"),
|
||||
}
|
||||
section["fields"][section.get("fields").index(field["name"])] = field
|
||||
|
||||
return sections or []
|
||||
return tabs or []
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -59,11 +68,13 @@ def save_fields_layout(doctype: str, type: str, layout: str):
|
||||
else:
|
||||
doc = frappe.new_doc("CRM Fields Layout")
|
||||
|
||||
doc.update({
|
||||
"dt": doctype,
|
||||
"type": type,
|
||||
"layout": layout,
|
||||
})
|
||||
doc.update(
|
||||
{
|
||||
"dt": doctype,
|
||||
"type": type,
|
||||
"layout": layout,
|
||||
}
|
||||
)
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
return doc.layout
|
||||
|
||||
@ -1,22 +1,14 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from crm.api.doc import get_fields_meta, get_assigned_users
|
||||
from crm.api.doc import get_assigned_users, get_fields_meta
|
||||
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_lead(name):
|
||||
Lead = frappe.qb.DocType("CRM Lead")
|
||||
lead = frappe.get_doc("CRM Lead", name).as_dict()
|
||||
|
||||
query = frappe.qb.from_(Lead).select("*").where(Lead.name == name).limit(1)
|
||||
|
||||
lead = query.run(as_dict=True)
|
||||
if not len(lead):
|
||||
frappe.throw(_("Lead not found"), frappe.DoesNotExistError)
|
||||
lead = lead.pop()
|
||||
|
||||
lead["doctype"] = "CRM Lead"
|
||||
lead["fields_meta"] = get_fields_meta("CRM Lead")
|
||||
lead["_form_script"] = get_form_script('CRM Lead')
|
||||
lead["_form_script"] = get_form_script("CRM Lead")
|
||||
lead["_assign"] = get_assigned_users("CRM Lead", lead.name, lead.owner)
|
||||
return lead
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
"@vueuse/core": "^10.3.0",
|
||||
"@vueuse/integrations": "^10.3.0",
|
||||
"feather-icons": "^4.28.0",
|
||||
"frappe-ui": "^0.1.90",
|
||||
"frappe-ui": "^0.1.91",
|
||||
"gemoji": "^8.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime": "^4.0.1",
|
||||
|
||||
@ -364,6 +364,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="title == 'Data'" class="h-full flex flex-col px-3 sm:px-10">
|
||||
<DataFields :doctype="doctype" :docname="doc.data.name" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-1 flex-col items-center justify-center gap-3 text-xl font-medium text-ink-gray-4"
|
||||
@ -454,9 +457,11 @@ import CallArea from '@/components/Activities/CallArea.vue'
|
||||
import NoteArea from '@/components/Activities/NoteArea.vue'
|
||||
import TaskArea from '@/components/Activities/TaskArea.vue'
|
||||
import AttachmentArea from '@/components/Activities/AttachmentArea.vue'
|
||||
import DataFields from '@/components/Activities/DataFields.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
||||
@ -719,6 +724,8 @@ const emptyText = computed(() => {
|
||||
text = 'No Email Communications'
|
||||
} else if (title.value == 'Comments') {
|
||||
text = 'No Comments'
|
||||
} else if (title.value == 'Data') {
|
||||
text = 'No Data'
|
||||
} else if (title.value == 'Calls') {
|
||||
text = 'No Call Logs'
|
||||
} else if (title.value == 'Notes') {
|
||||
@ -739,6 +746,8 @@ const emptyTextIcon = computed(() => {
|
||||
icon = Email2Icon
|
||||
} else if (title.value == 'Comments') {
|
||||
icon = CommentIcon
|
||||
} else if (title.value == 'Data') {
|
||||
icon = DetailsIcon
|
||||
} else if (title.value == 'Calls') {
|
||||
icon = PhoneIcon
|
||||
} else if (title.value == 'Notes') {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="title !== 'Data'"
|
||||
class="mx-4 my-3 flex items-center justify-between text-lg font-medium sm:mx-10 sm:mb-4 sm:mt-8"
|
||||
>
|
||||
<div class="flex h-8 items-center text-xl font-semibold text-ink-gray-8">
|
||||
|
||||
108
frontend/src/components/Activities/DataFields.vue
Normal file
108
frontend/src/components/Activities/DataFields.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div
|
||||
class="my-3 flex items-center justify-between text-lg font-medium sm:mb-4 sm:mt-8"
|
||||
>
|
||||
<div class="flex h-8 items-center text-xl font-semibold text-ink-gray-8">
|
||||
{{ __('Data') }}
|
||||
<Badge
|
||||
v-if="data.isDirty"
|
||||
class="ml-3"
|
||||
:label="'Not Saved'"
|
||||
theme="orange"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<Button v-if="isManager()" @click="showDataFieldsModal = true">
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
label="Save"
|
||||
:disabled="!data.isDirty"
|
||||
variant="solid"
|
||||
:loading="data.save.loading"
|
||||
@click="saveChanges"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="data.get.loading"
|
||||
class="flex flex-1 flex-col items-center justify-center gap-3 text-xl font-medium text-gray-500"
|
||||
>
|
||||
<LoadingIndicator class="h-6 w-6" />
|
||||
<span>{{ __('Loading...') }}</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<FieldLayout v-if="tabs.data" :tabs="tabs.data" :data="data.doc" />
|
||||
</div>
|
||||
<DataFieldsModal
|
||||
v-if="showDataFieldsModal"
|
||||
v-model="showDataFieldsModal"
|
||||
:doctype="doctype"
|
||||
@reload="
|
||||
() => {
|
||||
tabs.reload()
|
||||
data.reload()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import DataFieldsModal from '@/components/Modals/DataFieldsModal.vue'
|
||||
import FieldLayout from '@/components/FieldLayout.vue'
|
||||
import { Badge, createResource, createDocumentResource } from 'frappe-ui'
|
||||
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
||||
import { createToast } from '@/utils'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
doctype: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
docname: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { isManager } = usersStore()
|
||||
|
||||
const showDataFieldsModal = ref(false)
|
||||
|
||||
const data = createDocumentResource({
|
||||
doctype: props.doctype,
|
||||
name: props.docname,
|
||||
setValue: {
|
||||
onSuccess: () => {
|
||||
data.reload()
|
||||
createToast({
|
||||
title: 'Data Updated',
|
||||
icon: 'check',
|
||||
iconClasses: 'text-green-600',
|
||||
})
|
||||
},
|
||||
onError: (err) => {
|
||||
createToast({
|
||||
title: 'Error',
|
||||
text: err.messages[0],
|
||||
icon: 'x',
|
||||
iconClasses: 'text-red-600',
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const tabs = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||
cache: ['DataFields', props.doctype],
|
||||
params: { doctype: props.doctype, type: 'Data Fields' },
|
||||
auto: true,
|
||||
})
|
||||
|
||||
function saveChanges() {
|
||||
data.save.submit()
|
||||
}
|
||||
</script>
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="space-y-1.5">
|
||||
<div class="space-y-1.5 p-[2px] -m-[2px]">
|
||||
<label class="block" :class="labelClasses" v-if="attrs.label">
|
||||
{{ __(attrs.label) }}
|
||||
</label>
|
||||
@ -34,7 +34,7 @@
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
:label="__('Create New')"
|
||||
@click="attrs.onCreate(value, close)"
|
||||
@click="() => attrs.onCreate(value, close)"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
|
||||
271
frontend/src/components/FieldLayout.vue
Normal file
271
frontend/src/components/FieldLayout.vue
Normal file
@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col"
|
||||
:class="{
|
||||
'border border-outline-gray-1 rounded-lg': hasTabs,
|
||||
'border-outline-gray-modals': modal && hasTabs,
|
||||
}"
|
||||
>
|
||||
<Tabs
|
||||
v-model="tabIndex"
|
||||
class="!h-full"
|
||||
:tabs="tabs"
|
||||
v-slot="{ tab }"
|
||||
:tablistClass="
|
||||
!hasTabs ? 'hidden' : modal ? 'border-outline-gray-modals' : ''
|
||||
"
|
||||
>
|
||||
<div :class="{ 'my-4 sm:my-6': hasTabs }">
|
||||
<div
|
||||
v-for="(section, i) in tab.sections"
|
||||
:key="section.label"
|
||||
class="section"
|
||||
>
|
||||
<div
|
||||
v-if="i != 0"
|
||||
class="w-full"
|
||||
:class="[section.hideBorder ? 'mt-4' : 'h-px border-t my-5']"
|
||||
/>
|
||||
<Section
|
||||
class="text-lg font-medium"
|
||||
:class="{ 'px-3 sm:px-5': hasTabs }"
|
||||
:label="section.label"
|
||||
:hideLabel="section.hideLabel"
|
||||
:opened="section.opened"
|
||||
:collapsible="section.collapsible"
|
||||
collapseIconPosition="right"
|
||||
>
|
||||
<div
|
||||
class="grid gap-4"
|
||||
:class="[
|
||||
gridClass(section.columns),
|
||||
{ 'px-3 sm:px-5': hasTabs },
|
||||
{ 'mt-6': !section.hideLabel },
|
||||
]"
|
||||
>
|
||||
<div v-for="field in section.fields" :key="field.name">
|
||||
<div
|
||||
class="settings-field"
|
||||
v-if="
|
||||
(field.type == 'Check' ||
|
||||
(field.read_only && data[field.name]) ||
|
||||
!field.read_only ||
|
||||
!field.hidden) &&
|
||||
(!field.depends_on || field.display_via_depends_on)
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-if="field.type != 'Check'"
|
||||
class="mb-2 text-sm text-ink-gray-5"
|
||||
>
|
||||
{{ __(field.label) }}
|
||||
<span
|
||||
class="text-ink-red-3"
|
||||
v-if="
|
||||
field.mandatory ||
|
||||
(field.mandatory_depends_on &&
|
||||
field.mandatory_via_depends_on)
|
||||
"
|
||||
>*</span
|
||||
>
|
||||
</div>
|
||||
<FormControl
|
||||
v-if="field.read_only && field.type !== 'Check'"
|
||||
type="text"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
v-model="data[field.name]"
|
||||
:disabled="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-else-if="field.type === 'Select'"
|
||||
type="select"
|
||||
class="form-control"
|
||||
:class="field.prefix ? 'prefix' : ''"
|
||||
:options="field.options"
|
||||
v-model="data[field.name]"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
>
|
||||
<template v-if="field.prefix" #prefix>
|
||||
<IndicatorIcon :class="field.prefix" />
|
||||
</template>
|
||||
</FormControl>
|
||||
<div
|
||||
v-else-if="field.type == 'Check'"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<FormControl
|
||||
class="form-control"
|
||||
type="checkbox"
|
||||
v-model="data[field.name]"
|
||||
@change="(e) => (data[field.name] = e.target.checked)"
|
||||
:disabled="Boolean(field.read_only)"
|
||||
/>
|
||||
<label
|
||||
class="text-sm text-ink-gray-5"
|
||||
@click="data[field.name] = !data[field.name]"
|
||||
>
|
||||
{{ __(field.label) }}
|
||||
<span class="text-ink-red-3" v-if="field.mandatory"
|
||||
>*</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex gap-1" v-else-if="field.type === 'Link'">
|
||||
<Link
|
||||
class="form-control flex-1 truncate"
|
||||
:value="data[field.name]"
|
||||
:doctype="field.options"
|
||||
:filters="field.filters"
|
||||
@change="(v) => (data[field.name] = v)"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
:onCreate="field.create"
|
||||
/>
|
||||
<Button
|
||||
v-if="data[field.name] && field.edit"
|
||||
class="shrink-0"
|
||||
:label="__('Edit')"
|
||||
@click="field.edit(data[field.name])"
|
||||
>
|
||||
<template #prefix>
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
v-else-if="field.type === 'User'"
|
||||
class="form-control"
|
||||
:value="getUser(data[field.name]).full_name"
|
||||
:doctype="field.options"
|
||||
:filters="field.filters"
|
||||
@change="(v) => (data[field.name] = v)"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
:hideMe="true"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserAvatar
|
||||
class="mr-2"
|
||||
:user="data[field.name]"
|
||||
size="sm"
|
||||
/>
|
||||
</template>
|
||||
<template #item-prefix="{ option }">
|
||||
<UserAvatar class="mr-2" :user="option.value" size="sm" />
|
||||
</template>
|
||||
<template #item-label="{ option }">
|
||||
<Tooltip :text="option.value">
|
||||
<div class="cursor-pointer">
|
||||
{{ getUser(option.value).full_name }}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</template>
|
||||
</Link>
|
||||
<DateTimePicker
|
||||
v-else-if="field.type === 'Datetime'"
|
||||
v-model="data[field.name]"
|
||||
icon-left=""
|
||||
:formatter="(date) => getFormat(date, '', true, true)"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
input-class="border-none"
|
||||
/>
|
||||
<DatePicker
|
||||
v-else-if="field.type === 'Date'"
|
||||
icon-left=""
|
||||
v-model="data[field.name]"
|
||||
:formatter="(date) => getFormat(date, '', true)"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
input-class="border-none"
|
||||
/>
|
||||
<FormControl
|
||||
v-else-if="
|
||||
['Small Text', 'Text', 'Long Text'].includes(field.type)
|
||||
"
|
||||
type="textarea"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
v-model="data[field.name]"
|
||||
/>
|
||||
<FormControl
|
||||
v-else-if="['Int'].includes(field.type)"
|
||||
type="number"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
v-model="data[field.name]"
|
||||
/>
|
||||
<FormControl
|
||||
v-else
|
||||
type="text"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
v-model="data[field.name]"
|
||||
:disabled="Boolean(field.read_only)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Section from '@/components/Section.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { getFormat } from '@/utils'
|
||||
import { Tabs, Tooltip, DatePicker, DateTimePicker } from 'frappe-ui'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const { getUser } = usersStore()
|
||||
|
||||
const props = defineProps({
|
||||
tabs: Array,
|
||||
data: Object,
|
||||
modal: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const hasTabs = computed(() => !props.tabs[0].no_tabs)
|
||||
|
||||
const tabIndex = ref(0)
|
||||
|
||||
function gridClass(columns) {
|
||||
columns = columns || 3
|
||||
let griColsMap = {
|
||||
1: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-1',
|
||||
2: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-2',
|
||||
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
|
||||
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
|
||||
}
|
||||
return griColsMap[columns]
|
||||
}
|
||||
|
||||
const getPlaceholder = (field) => {
|
||||
if (field.placeholder) {
|
||||
return __(field.placeholder)
|
||||
}
|
||||
if (['Select', 'Link'].includes(field.type)) {
|
||||
return __('Select {0}', [__(field.label)])
|
||||
} else {
|
||||
return __('Enter {0}', [__(field.label)])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.form-control.prefix select) {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.section:has(.settings-field) {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
366
frontend/src/components/FieldLayoutEditor.vue
Normal file
366
frontend/src/components/FieldLayoutEditor.vue
Normal file
@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-5.5">
|
||||
<div
|
||||
class="flex justify-between items-center gap-1 text-base bg-surface-gray-2 rounded py-2 px-2.5"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<Draggable
|
||||
v-if="tabs.length && !tabs[tabIndex].no_tabs"
|
||||
:list="tabs"
|
||||
item-key="label"
|
||||
class="flex items-center gap-1"
|
||||
@end="(e) => (tabIndex = e.newIndex)"
|
||||
>
|
||||
<template #item="{ element: tab, index: i }">
|
||||
<div
|
||||
class="cursor-pointer rounded"
|
||||
:class="[
|
||||
tabIndex == i
|
||||
? 'text-ink-gray-9 bg-surface-white shadow-sm'
|
||||
: 'text-ink-gray-5 hover:text-ink-gray-9 hover:bg-surface-white hover:shadow-sm',
|
||||
tab.editingLabel ? 'p-1' : 'px-2 py-1',
|
||||
]"
|
||||
@click="tabIndex = i"
|
||||
>
|
||||
<div @dblclick="() => (tab.editingLabel = true)">
|
||||
<div v-if="!tab.editingLabel" class="flex items-center gap-2">
|
||||
{{ __(tab.label) || __('Untitled') }}
|
||||
</div>
|
||||
<div v-else class="flex gap-1 items-center">
|
||||
<Input
|
||||
v-model="tab.label"
|
||||
@keydown.enter="tab.editingLabel = false"
|
||||
@blur="tab.editingLabel = false"
|
||||
@click.stop
|
||||
/>
|
||||
<Button
|
||||
v-if="tab.editingLabel"
|
||||
icon="check"
|
||||
variant="ghost"
|
||||
@click="tab.editingLabel = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="!h-6.5 !text-ink-gray-5 hover:!text-ink-gray-9"
|
||||
@click="addTab"
|
||||
:label="__('Add Tab')"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<Dropdown
|
||||
v-if="tabs.length && !tabs[tabIndex].no_tabs"
|
||||
:options="getTabOptions(tabs[tabIndex])"
|
||||
>
|
||||
<template #default>
|
||||
<Button variant="ghost">
|
||||
<FeatherIcon name="more-horizontal" class="h-4" />
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div v-show="tabIndex == i" v-for="(tab, i) in tabs" :key="tab.label">
|
||||
<Draggable
|
||||
:list="tab.sections"
|
||||
item-key="label"
|
||||
class="flex flex-col gap-5.5"
|
||||
>
|
||||
<template #item="{ element: section }">
|
||||
<div class="flex flex-col gap-1.5 p-2.5 bg-surface-gray-2 rounded">
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="flex h-7 max-w-fit cursor-pointer items-center gap-2 text-base font-medium leading-4 text-ink-gray-9"
|
||||
@dblclick="() => (section.editingLabel = true)"
|
||||
>
|
||||
<div
|
||||
v-if="!section.editingLabel"
|
||||
class="flex items-center gap-2"
|
||||
:class="{ 'text-ink-gray-3': section.hideLabel }"
|
||||
>
|
||||
{{ __(section.label) || __('Untitled') }}
|
||||
<FeatherIcon
|
||||
v-if="section.collapsible"
|
||||
name="chevron-down"
|
||||
class="h-4 transition-all duration-300 ease-in-out"
|
||||
/>
|
||||
</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="getSectionOptions(section)">
|
||||
<template #default>
|
||||
<Button variant="ghost">
|
||||
<FeatherIcon name="more-horizontal" class="h-4" />
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<Draggable
|
||||
:list="section.fields"
|
||||
group="fields"
|
||||
item-key="label"
|
||||
class="grid gap-1.5"
|
||||
:class="gridClass(section.columns)"
|
||||
handle=".cursor-grab"
|
||||
>
|
||||
<template #item="{ element: field }">
|
||||
<div
|
||||
class="px-2.5 py-2 border border-outline-gray-2 rounded text-base bg-surface-modal text-ink-gray-8 flex items-center leading-4 justify-between gap-2"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<DragVerticalIcon class="h-3.5 cursor-grab" />
|
||||
<div>{{ field.label }}</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="!size-4 rounded-sm"
|
||||
icon="x"
|
||||
@click="
|
||||
section.fields.splice(section.fields.indexOf(field), 1)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
<Autocomplete
|
||||
v-if="fields.data"
|
||||
value=""
|
||||
:options="fields.data"
|
||||
@change="(e) => addField(section, e)"
|
||||
>
|
||||
<template #target="{ togglePopover }">
|
||||
<div class="gap-2 w-full">
|
||||
<Button
|
||||
class="w-full !h-8 !bg-surface-modal"
|
||||
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 text-ink-gray-9">
|
||||
<div>{{ option.label }}</div>
|
||||
<div class="text-ink-gray-4 text-sm">
|
||||
{{ `${option.fieldname} - ${option.fieldtype}` }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
<div class="mt-5.5">
|
||||
<Button
|
||||
class="w-full h-8"
|
||||
variant="subtle"
|
||||
:label="__('Add Section')"
|
||||
@click="
|
||||
tabs[tabIndex].sections.push({
|
||||
label: __('New Section'),
|
||||
opened: true,
|
||||
fields: [],
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</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 { ref, computed, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
tabs: Object,
|
||||
doctype: String,
|
||||
})
|
||||
|
||||
const tabIndex = ref(0)
|
||||
|
||||
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 addTab() {
|
||||
if (props.tabs.length == 1 && props.tabs[0].no_tabs) {
|
||||
delete props.tabs[0].no_tabs
|
||||
return
|
||||
}
|
||||
props.tabs.push({ label: __('New Tab'), sections: [] })
|
||||
tabIndex.value = props.tabs.length ? props.tabs.length - 1 : 0
|
||||
}
|
||||
|
||||
function addField(section, field) {
|
||||
if (!field) return
|
||||
section.fields.push(field)
|
||||
}
|
||||
|
||||
function getTabOptions(tab) {
|
||||
return [
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: 'edit',
|
||||
onClick: () => (tab.editingLabel = true),
|
||||
},
|
||||
{
|
||||
label: 'Remove tab',
|
||||
icon: 'trash-2',
|
||||
onClick: () => {
|
||||
if (props.tabs.length == 1) {
|
||||
props.tabs[0].no_tabs = true
|
||||
return
|
||||
}
|
||||
props.tabs.splice(tabIndex.value, 1)
|
||||
tabIndex.value = tabIndex.value ? tabIndex.value - 1 : 0
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function getSectionOptions(section) {
|
||||
return [
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: 'edit',
|
||||
onClick: () => (section.editingLabel = true),
|
||||
condition: () => section.editable !== false,
|
||||
},
|
||||
{
|
||||
label: section.collapsible ? 'Uncollapsible' : 'Collapsible',
|
||||
icon: section.collapsible ? 'chevron-up' : 'chevron-down',
|
||||
onClick: () => (section.collapsible = !section.collapsible),
|
||||
},
|
||||
{
|
||||
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: () => {
|
||||
let currentTab = props.tabs[tabIndex.value]
|
||||
currentTab.sections.splice(currentTab.sections.indexOf(section), 1)
|
||||
},
|
||||
condition: () => section.editable !== false,
|
||||
},
|
||||
{
|
||||
label: 'Move to previous tab',
|
||||
icon: 'trash-2',
|
||||
onClick: () => {
|
||||
let previousTab = props.tabs[tabIndex.value - 1]
|
||||
previousTab.sections.push(section)
|
||||
props.tabs[tabIndex.value].sections.splice(
|
||||
props.tabs[tabIndex.value].sections.indexOf(section),
|
||||
1,
|
||||
)
|
||||
tabIndex.value -= 1
|
||||
},
|
||||
condition: () =>
|
||||
section.editable !== false && props.tabs[tabIndex.value - 1],
|
||||
},
|
||||
{
|
||||
label: 'Move to next tab',
|
||||
icon: 'trash-2',
|
||||
onClick: () => {
|
||||
let nextTab = props.tabs[tabIndex.value + 1]
|
||||
nextTab.sections.push(section)
|
||||
props.tabs[tabIndex.value].sections.splice(
|
||||
props.tabs[tabIndex.value].sections.indexOf(section),
|
||||
1,
|
||||
)
|
||||
tabIndex.value += 1
|
||||
},
|
||||
condition: () =>
|
||||
section.editable !== false && props.tabs[tabIndex.value + 1],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function gridClass(columns) {
|
||||
columns = columns || 3
|
||||
let griColsMap = {
|
||||
1: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-1',
|
||||
2: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-2',
|
||||
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
|
||||
4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
|
||||
}
|
||||
return griColsMap[columns]
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.doctype,
|
||||
() => fields.fetch(params.value),
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
@ -1,217 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div
|
||||
v-for="section in sections"
|
||||
:key="section.label"
|
||||
class="section first:border-t-0 border-outline-gray-modals first:pt-0"
|
||||
:class="section.hideBorder ? '' : 'border-t pt-4'"
|
||||
>
|
||||
<div
|
||||
v-if="!section.hideLabel"
|
||||
class="flex h-7 mb-3 max-w-fit cursor-pointer items-center gap-2 text-base font-semibold leading-5"
|
||||
>
|
||||
{{ section.label }}
|
||||
</div>
|
||||
<div
|
||||
class="grid gap-4"
|
||||
:class="
|
||||
section.columns
|
||||
? 'grid-cols-' + section.columns
|
||||
: 'grid-cols-2 sm:grid-cols-3'
|
||||
"
|
||||
>
|
||||
<div v-for="field in section.fields" :key="field.name">
|
||||
<div
|
||||
class="settings-field"
|
||||
v-if="
|
||||
(field.type == 'Check' ||
|
||||
(field.read_only && data[field.name]) ||
|
||||
!field.read_only ||
|
||||
!field.hidden) &&
|
||||
(!field.depends_on || field.display_via_depends_on)
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-if="field.type != 'Check'"
|
||||
class="mb-2 text-sm text-ink-gray-5"
|
||||
>
|
||||
{{ __(field.label) }}
|
||||
<span
|
||||
class="text-ink-red-3"
|
||||
v-if="
|
||||
field.mandatory ||
|
||||
(field.mandatory_depends_on && field.mandatory_via_depends_on)
|
||||
"
|
||||
>*</span
|
||||
>
|
||||
</div>
|
||||
<FormControl
|
||||
v-if="field.read_only && field.type !== 'Check'"
|
||||
type="text"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
v-model="data[field.name]"
|
||||
:disabled="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-else-if="field.type === 'Select'"
|
||||
type="select"
|
||||
class="form-control"
|
||||
:class="field.prefix ? 'prefix' : ''"
|
||||
:options="field.options"
|
||||
v-model="data[field.name]"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
>
|
||||
<template v-if="field.prefix" #prefix>
|
||||
<IndicatorIcon :class="field.prefix" />
|
||||
</template>
|
||||
</FormControl>
|
||||
<div
|
||||
v-else-if="field.type == 'Check'"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<FormControl
|
||||
class="form-control"
|
||||
type="checkbox"
|
||||
v-model="data[field.name]"
|
||||
@change="(e) => (data[field.name] = e.target.checked)"
|
||||
:disabled="Boolean(field.read_only)"
|
||||
/>
|
||||
<label
|
||||
class="text-sm text-ink-gray-5"
|
||||
@click="data[field.name] = !data[field.name]"
|
||||
>
|
||||
{{ __(field.label) }}
|
||||
<span class="text-ink-red-3" v-if="field.mandatory">*</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex gap-1" v-else-if="field.type === 'Link'">
|
||||
<Link
|
||||
class="form-control flex-1"
|
||||
:value="data[field.name]"
|
||||
:doctype="field.options"
|
||||
:filters="field.filters"
|
||||
@change="(v) => (data[field.name] = v)"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
:onCreate="field.create"
|
||||
/>
|
||||
<Button
|
||||
v-if="data[field.name] && field.edit"
|
||||
class="shrink-0"
|
||||
:label="__('Edit')"
|
||||
@click="field.edit(data[field.name])"
|
||||
>
|
||||
<template #prefix>
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
v-else-if="field.type === 'User'"
|
||||
class="form-control"
|
||||
:value="getUser(data[field.name]).full_name"
|
||||
:doctype="field.options"
|
||||
:filters="field.filters"
|
||||
@change="(v) => (data[field.name] = v)"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
:hideMe="true"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserAvatar class="mr-2" :user="data[field.name]" size="sm" />
|
||||
</template>
|
||||
<template #item-prefix="{ option }">
|
||||
<UserAvatar class="mr-2" :user="option.value" size="sm" />
|
||||
</template>
|
||||
<template #item-label="{ option }">
|
||||
<Tooltip :text="option.value">
|
||||
<div class="cursor-pointer">
|
||||
{{ getUser(option.value).full_name }}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</template>
|
||||
</Link>
|
||||
<DateTimePicker
|
||||
v-else-if="field.type === 'Datetime'"
|
||||
v-model="data[field.name]"
|
||||
icon-left=""
|
||||
:formatter="(date) => getFormat(date, '', true, true)"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
input-class="border-none"
|
||||
/>
|
||||
<DatePicker
|
||||
v-else-if="field.type === 'Date'"
|
||||
icon-left=""
|
||||
v-model="data[field.name]"
|
||||
:formatter="(date) => getFormat(date, '', true)"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
input-class="border-none"
|
||||
/>
|
||||
<FormControl
|
||||
v-else-if="
|
||||
['Small Text', 'Text', 'Long Text'].includes(field.type)
|
||||
"
|
||||
type="textarea"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
v-model="data[field.name]"
|
||||
/>
|
||||
<FormControl
|
||||
v-else-if="['Int'].includes(field.type)"
|
||||
type="number"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
v-model="data[field.name]"
|
||||
/>
|
||||
<FormControl
|
||||
v-else
|
||||
type="text"
|
||||
:placeholder="getPlaceholder(field)"
|
||||
v-model="data[field.name]"
|
||||
:disabled="Boolean(field.read_only)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { getFormat } from '@/utils'
|
||||
import { Tooltip, DatePicker, DateTimePicker } from 'frappe-ui'
|
||||
|
||||
const { getUser } = usersStore()
|
||||
|
||||
const props = defineProps({
|
||||
sections: Array,
|
||||
data: Object,
|
||||
})
|
||||
|
||||
const getPlaceholder = (field) => {
|
||||
if (field.placeholder) {
|
||||
return __(field.placeholder)
|
||||
}
|
||||
if (['Select', 'Link'].includes(field.type)) {
|
||||
return __('Select {0}', [__(field.label)])
|
||||
} else {
|
||||
return __('Enter {0}', [__(field.label)])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.form-control.prefix select) {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.section:has(.settings-field) {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@ -37,7 +37,7 @@
|
||||
<Section
|
||||
:label="view.name"
|
||||
:hideLabel="view.hideLabel"
|
||||
:isOpened="view.opened"
|
||||
:opened="view.opened"
|
||||
>
|
||||
<template #header="{ opened, hide, toggle }">
|
||||
<div
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
<Section
|
||||
:label="view.name"
|
||||
:hideLabel="view.hideLabel"
|
||||
:isOpened="view.opened"
|
||||
:opened="view.opened"
|
||||
>
|
||||
<template #header="{ opened, hide, toggle }">
|
||||
<div
|
||||
|
||||
@ -22,8 +22,8 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="sections.data">
|
||||
<Fields :sections="sections.data" :data="_address" />
|
||||
<div v-if="tabs.data">
|
||||
<FieldLayout :tabs="tabs.data" :data="_address" />
|
||||
<ErrorMessage class="mt-2" :message="error" />
|
||||
</div>
|
||||
</div>
|
||||
@ -50,7 +50,7 @@
|
||||
|
||||
<script setup>
|
||||
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import FieldLayout from '@/components/FieldLayout.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { capture } from '@/telemetry'
|
||||
@ -106,9 +106,9 @@ const dialogOptions = computed(() => {
|
||||
return { title, size, actions }
|
||||
})
|
||||
|
||||
const sections = createResource({
|
||||
const tabs = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||
cache: ['quickEntryFields', 'Address'],
|
||||
cache: ['QuickEntry', 'Address'],
|
||||
params: { doctype: 'Address', type: 'Quick Entry' },
|
||||
auto: true,
|
||||
})
|
||||
|
||||
@ -10,10 +10,10 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
v-if="isManager() || detailMode"
|
||||
v-if="isManager()"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="detailMode ? (detailMode = false) : openQuickEntryModal()"
|
||||
@click="openQuickEntryModal"
|
||||
>
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
@ -22,77 +22,36 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="detailMode" class="flex flex-col gap-3.5">
|
||||
<div
|
||||
v-for="field in detailFields"
|
||||
:key="field.name"
|
||||
class="flex h-7 items-center gap-2 text-base text-ink-gray-8"
|
||||
>
|
||||
<div class="grid w-7 place-content-center">
|
||||
<component :is="field.icon" />
|
||||
</div>
|
||||
<div v-if="field.type == 'dropdown'">
|
||||
<Dropdown
|
||||
:options="field.options"
|
||||
class="form-control -ml-2 mr-2 w-full flex-1"
|
||||
>
|
||||
<template #default="{ open }">
|
||||
<Button
|
||||
variant="ghost"
|
||||
:label="contact.data[field.name]"
|
||||
class="dropdown-button w-full justify-between truncate hover:bg-surface-white"
|
||||
>
|
||||
<div class="truncate">{{ contact.data[field.name] }}</div>
|
||||
<template #suffix>
|
||||
<FeatherIcon
|
||||
:name="open ? 'chevron-up' : 'chevron-down'"
|
||||
class="h-4 text-ink-gray-5"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div v-else>{{ field.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Fields
|
||||
v-else-if="filteredSections"
|
||||
:sections="filteredSections"
|
||||
:data="_contact"
|
||||
/>
|
||||
<div v-if="filteredSections.length">
|
||||
<FieldLayout :tabs="filteredSections" :data="_contact" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!detailMode" class="px-4 pb-7 pt-4 sm:px-6">
|
||||
<div class="px-4 pb-7 pt-4 sm:px-6">
|
||||
<div class="space-y-2">
|
||||
<Button
|
||||
class="w-full"
|
||||
v-for="action in dialogOptions.actions"
|
||||
:key="action.label"
|
||||
v-bind="action"
|
||||
>
|
||||
{{ __(action.label) }}
|
||||
</Button>
|
||||
:label="__(action.label)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
<AddressModal v-model="showAddressModal" v-model:address="_address" />
|
||||
<QuickEntryModal
|
||||
v-if="showQuickEntryModal"
|
||||
v-model="showQuickEntryModal"
|
||||
doctype="Contact"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
||||
import FieldLayout from '@/components/FieldLayout.vue'
|
||||
import AddressModal from '@/components/Modals/AddressModal.vue'
|
||||
import ContactIcon from '@/components/Icons/ContactIcon.vue'
|
||||
import GenderIcon from '@/components/Icons/GenderIcon.vue'
|
||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||
import AddressIcon from '@/components/Icons/AddressIcon.vue'
|
||||
import CertificateIcon from '@/components/Icons/CertificateIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import Dropdown from '@/components/frappe-ui/Dropdown.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { capture } from '@/telemetry'
|
||||
import { call, createResource } from 'frappe-ui'
|
||||
@ -108,7 +67,6 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: {
|
||||
redirect: true,
|
||||
detailMode: false,
|
||||
afterInsert: () => {},
|
||||
},
|
||||
},
|
||||
@ -119,7 +77,6 @@ const { isManager } = usersStore()
|
||||
const router = useRouter()
|
||||
const show = defineModel()
|
||||
|
||||
const detailMode = ref(false)
|
||||
const editMode = ref(false)
|
||||
let _contact = ref({})
|
||||
let _address = ref({})
|
||||
@ -186,74 +143,28 @@ function handleContactUpdate(doc) {
|
||||
const dialogOptions = computed(() => {
|
||||
let title = !editMode.value ? 'New Contact' : _contact.value.full_name
|
||||
|
||||
let size = detailMode.value ? '' : 'xl'
|
||||
let actions = detailMode.value
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: editMode.value ? 'Save' : 'Create',
|
||||
variant: 'solid',
|
||||
disabled: !dirty.value,
|
||||
onClick: () => (editMode.value ? updateContact() : callInsertDoc()),
|
||||
},
|
||||
]
|
||||
let size = 'xl'
|
||||
let actions = [
|
||||
{
|
||||
label: editMode.value ? 'Save' : 'Create',
|
||||
variant: 'solid',
|
||||
disabled: !dirty.value,
|
||||
onClick: () => (editMode.value ? updateContact() : callInsertDoc()),
|
||||
},
|
||||
]
|
||||
|
||||
return { title, size, actions }
|
||||
})
|
||||
|
||||
const detailFields = computed(() => {
|
||||
let details = [
|
||||
{
|
||||
icon: ContactIcon,
|
||||
name: 'full_name',
|
||||
value:
|
||||
(_contact.value.salutation ? _contact.value.salutation + '. ' : '') +
|
||||
_contact.value.full_name,
|
||||
},
|
||||
{
|
||||
icon: GenderIcon,
|
||||
name: 'gender',
|
||||
value: _contact.value.gender,
|
||||
},
|
||||
{
|
||||
icon: Email2Icon,
|
||||
name: 'email_id',
|
||||
value: _contact.value.email_id,
|
||||
},
|
||||
{
|
||||
icon: PhoneIcon,
|
||||
name: 'mobile_no',
|
||||
value: _contact.value.actual_mobile_no,
|
||||
},
|
||||
{
|
||||
icon: OrganizationsIcon,
|
||||
name: 'company_name',
|
||||
value: _contact.value.company_name,
|
||||
},
|
||||
{
|
||||
icon: CertificateIcon,
|
||||
name: 'designation',
|
||||
value: _contact.value.designation,
|
||||
},
|
||||
{
|
||||
icon: AddressIcon,
|
||||
name: 'address',
|
||||
value: _contact.value.address,
|
||||
},
|
||||
]
|
||||
|
||||
return details.filter((detail) => detail.value)
|
||||
})
|
||||
|
||||
const sections = createResource({
|
||||
const tabs = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||
cache: ['quickEntryFields', 'Contact'],
|
||||
cache: ['QuickEntry', 'Contact'],
|
||||
params: { doctype: 'Contact', type: 'Quick Entry' },
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const filteredSections = computed(() => {
|
||||
let allSections = sections.data || []
|
||||
let allSections = tabs.data?.[0]?.sections || []
|
||||
if (!allSections.length) return []
|
||||
|
||||
allSections.forEach((s) => {
|
||||
@ -276,7 +187,7 @@ const filteredSections = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
return allSections
|
||||
return [{ no_tabs: true, sections: allSections }]
|
||||
})
|
||||
|
||||
const dirty = computed(() => {
|
||||
@ -287,7 +198,6 @@ watch(
|
||||
() => show.value,
|
||||
(value) => {
|
||||
if (!value) return
|
||||
detailMode.value = props.options.detailMode
|
||||
editMode.value = false
|
||||
nextTick(() => {
|
||||
_contact.value = { ...props.contact.data }
|
||||
@ -298,13 +208,11 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
const showQuickEntryModal = defineModel('quickEntry')
|
||||
const showQuickEntryModal = ref(false)
|
||||
|
||||
function openQuickEntryModal() {
|
||||
showQuickEntryModal.value = true
|
||||
nextTick(() => {
|
||||
show.value = false
|
||||
})
|
||||
nextTick(() => (show.value = false))
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
135
frontend/src/components/Modals/DataFieldsModal.vue
Normal file
135
frontend/src/components/Modals/DataFieldsModal.vue
Normal file
@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<Dialog v-model="show" :options="{ size: '3xl' }">
|
||||
<template #body-title>
|
||||
<h3
|
||||
class="flex items-center gap-2 text-2xl font-semibold leading-6 text-ink-gray-9"
|
||||
>
|
||||
<div>{{ __('Edit Data Fields Layout') }}</div>
|
||||
<Badge
|
||||
v-if="dirty"
|
||||
:label="__('Not Saved')"
|
||||
variant="subtle"
|
||||
theme="orange"
|
||||
/>
|
||||
</h3>
|
||||
</template>
|
||||
<template #body-content>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex justify-between gap-2">
|
||||
<FormControl
|
||||
type="select"
|
||||
class="w-1/4"
|
||||
v-model="_doctype"
|
||||
:options="['CRM Lead', 'CRM Deal']"
|
||||
@change="reload"
|
||||
/>
|
||||
<Switch
|
||||
v-model="preview"
|
||||
:label="preview ? __('Hide preview') : __('Show preview')"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="tabs?.data">
|
||||
<FieldLayoutEditor
|
||||
v-if="!preview"
|
||||
:tabs="tabs.data"
|
||||
:doctype="_doctype"
|
||||
/>
|
||||
<FieldLayout v-else :tabs="tabs.data" :data="{}" :modal="true" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex flex-row-reverse gap-2">
|
||||
<Button
|
||||
:loading="loading"
|
||||
:label="__('Save')"
|
||||
variant="solid"
|
||||
@click="saveChanges"
|
||||
/>
|
||||
<Button :label="__('Reset')" @click="reload" />
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import FieldLayout from '@/components/FieldLayout.vue'
|
||||
import FieldLayoutEditor from '@/components/FieldLayoutEditor.vue'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { capture } from '@/telemetry'
|
||||
import { Dialog, Badge, Switch, call, createResource } from 'frappe-ui'
|
||||
import { ref, watch, onMounted, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
doctype: {
|
||||
type: String,
|
||||
default: 'CRM Lead',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['reload'])
|
||||
|
||||
const show = defineModel()
|
||||
const _doctype = ref(props.doctype)
|
||||
const loading = ref(false)
|
||||
const dirty = ref(false)
|
||||
const preview = ref(false)
|
||||
|
||||
function getParams() {
|
||||
return { doctype: _doctype.value, type: 'Data Fields' }
|
||||
}
|
||||
|
||||
const tabs = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||
cache: ['DataFieldsModal', _doctype.value],
|
||||
params: getParams(),
|
||||
onSuccess(data) {
|
||||
tabs.originalData = JSON.parse(JSON.stringify(data))
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => tabs?.data,
|
||||
() => {
|
||||
dirty.value =
|
||||
JSON.stringify(tabs?.data) !== JSON.stringify(tabs?.originalData)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
onMounted(() => useDebounceFn(reload, 100)())
|
||||
|
||||
function reload() {
|
||||
nextTick(() => {
|
||||
tabs.params = getParams()
|
||||
tabs.reload()
|
||||
})
|
||||
}
|
||||
|
||||
function saveChanges() {
|
||||
let _tabs = JSON.parse(JSON.stringify(tabs.data))
|
||||
_tabs.forEach((tab) => {
|
||||
if (!tab.sections) return
|
||||
tab.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: 'Data Fields',
|
||||
layout: JSON.stringify(_tabs),
|
||||
},
|
||||
).then(() => {
|
||||
loading.value = false
|
||||
show.value = false
|
||||
capture('data_fields_layout_builder', { doctype: _doctype.value })
|
||||
emit('reload')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -33,10 +33,10 @@
|
||||
<Switch v-model="chooseExistingContact" />
|
||||
</div>
|
||||
</div>
|
||||
<Fields
|
||||
v-if="filteredSections"
|
||||
class="border-t pt-4"
|
||||
:sections="filteredSections"
|
||||
<div class="h-px w-full border-t my-5" />
|
||||
<FieldLayout
|
||||
v-if="filteredSections.length"
|
||||
:tabs="filteredSections"
|
||||
:data="deal"
|
||||
/>
|
||||
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
||||
@ -58,7 +58,7 @@
|
||||
|
||||
<script setup>
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import FieldLayout from '@/components/FieldLayout.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import { capture } from '@/telemetry'
|
||||
@ -100,28 +100,30 @@ const isDealCreating = ref(false)
|
||||
const chooseExistingContact = ref(false)
|
||||
const chooseExistingOrganization = ref(false)
|
||||
|
||||
const sections = createResource({
|
||||
const tabs = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||
cache: ['quickEntryFields', 'CRM Deal'],
|
||||
cache: ['QuickEntry', 'CRM Deal'],
|
||||
params: { doctype: 'CRM Deal', type: 'Quick Entry' },
|
||||
auto: true,
|
||||
transform: (data) => {
|
||||
return data.forEach((section) => {
|
||||
section.fields.forEach((field) => {
|
||||
if (field.name == 'status') {
|
||||
field.type = 'Select'
|
||||
field.options = dealStatuses.value
|
||||
field.prefix = getDealStatus(deal.status).iconColorClass
|
||||
} else if (field.name == 'deal_owner') {
|
||||
field.type = 'User'
|
||||
}
|
||||
transform: (_tabs) => {
|
||||
return _tabs.forEach((tab) => {
|
||||
tab.sections.forEach((section) => {
|
||||
section.fields.forEach((field) => {
|
||||
if (field.name == 'status') {
|
||||
field.type = 'Select'
|
||||
field.options = dealStatuses.value
|
||||
field.prefix = getDealStatus(deal.status).iconColorClass
|
||||
} else if (field.name == 'deal_owner') {
|
||||
field.type = 'User'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const filteredSections = computed(() => {
|
||||
let allSections = sections.data || []
|
||||
let allSections = tabs.data?.[0]?.sections || []
|
||||
if (!allSections.length) return []
|
||||
|
||||
let _filteredSections = []
|
||||
@ -159,7 +161,7 @@ const filteredSections = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
return _filteredSections
|
||||
return [{ no_tabs: true, sections: _filteredSections }]
|
||||
})
|
||||
|
||||
const dealStatuses = computed(() => {
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Fields v-if="sections.data" :sections="sections.data" :data="lead" />
|
||||
<FieldLayout v-if="tabs.data" :tabs="tabs.data" :data="lead" />
|
||||
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
||||
</div>
|
||||
</div>
|
||||
@ -43,7 +43,7 @@
|
||||
|
||||
<script setup>
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import FieldLayout from '@/components/FieldLayout.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import { capture } from '@/telemetry'
|
||||
@ -63,21 +63,23 @@ const router = useRouter()
|
||||
const error = ref(null)
|
||||
const isLeadCreating = ref(false)
|
||||
|
||||
const sections = createResource({
|
||||
const tabs = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||
cache: ['quickEntryFields', 'CRM Lead'],
|
||||
cache: ['QuickEntry', 'CRM Lead'],
|
||||
params: { doctype: 'CRM Lead', type: 'Quick Entry' },
|
||||
auto: true,
|
||||
transform: (data) => {
|
||||
return data.forEach((section) => {
|
||||
section.fields.forEach((field) => {
|
||||
if (field.name == 'status') {
|
||||
field.type = 'Select'
|
||||
field.options = leadStatuses.value
|
||||
field.prefix = getLeadStatus(lead.status).iconColorClass
|
||||
} else if (field.name == 'lead_owner') {
|
||||
field.type = 'User'
|
||||
}
|
||||
transform: (_tabs) => {
|
||||
return _tabs.forEach((tab) => {
|
||||
tab.sections.forEach((section) => {
|
||||
section.fields.forEach((field) => {
|
||||
if (field.name == 'status') {
|
||||
field.type = 'Select'
|
||||
field.options = leadStatuses.value
|
||||
field.prefix = getLeadStatus(lead.status).iconColorClass
|
||||
} else if (field.name == 'lead_owner') {
|
||||
field.type = 'User'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
@ -10,10 +10,10 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
v-if="isManager() || detailMode"
|
||||
v-if="isManager()"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="detailMode ? (detailMode = false) : openQuickEntryModal()"
|
||||
@click="openQuickEntryModal"
|
||||
>
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
@ -22,27 +22,11 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="detailMode" class="flex flex-col gap-3.5">
|
||||
<div
|
||||
class="flex h-7 items-center gap-2 text-base text-ink-gray-8"
|
||||
v-for="field in fields"
|
||||
:key="field.name"
|
||||
>
|
||||
<div class="grid w-7 place-content-center">
|
||||
<component :is="field.icon" />
|
||||
</div>
|
||||
<div>{{ field.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Fields
|
||||
v-else-if="filteredSections"
|
||||
:sections="filteredSections"
|
||||
:data="_organization"
|
||||
/>
|
||||
<div v-if="filteredSections.length">
|
||||
<FieldLayout :tabs="filteredSections" :data="_organization" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!detailMode" class="px-4 pb-7 pt-4 sm:px-6">
|
||||
<div class="px-4 pb-7 pt-4 sm:px-6">
|
||||
<div class="space-y-2">
|
||||
<Button
|
||||
class="w-full"
|
||||
@ -57,21 +41,22 @@
|
||||
</template>
|
||||
</Dialog>
|
||||
<AddressModal v-model="showAddressModal" v-model:address="_address" />
|
||||
<QuickEntryModal
|
||||
v-if="showQuickEntryModal"
|
||||
v-model="showQuickEntryModal"
|
||||
doctype="CRM Organization"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
||||
import FieldLayout from '@/components/FieldLayout.vue'
|
||||
import AddressModal from '@/components/Modals/AddressModal.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import MoneyIcon from '@/components/Icons/MoneyIcon.vue'
|
||||
import WebsiteIcon from '@/components/Icons/WebsiteIcon.vue'
|
||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||
import TerritoryIcon from '@/components/Icons/TerritoryIcon.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { formatNumberIntoCurrency } from '@/utils'
|
||||
import { capture } from '@/telemetry'
|
||||
import { call, FeatherIcon, createResource } from 'frappe-ui'
|
||||
import { ref, nextTick, watch, computed, h } from 'vue'
|
||||
import { ref, nextTick, watch, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
@ -79,7 +64,6 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: {
|
||||
redirect: true,
|
||||
detailMode: false,
|
||||
afterInsert: () => {},
|
||||
},
|
||||
},
|
||||
@ -93,7 +77,6 @@ const organization = defineModel('organization')
|
||||
|
||||
const loading = ref(false)
|
||||
const title = ref(null)
|
||||
const detailMode = ref(false)
|
||||
const editMode = ref(false)
|
||||
let _address = ref({})
|
||||
let _organization = ref({
|
||||
@ -186,70 +169,27 @@ const dialogOptions = computed(() => {
|
||||
let title = !editMode.value
|
||||
? __('New Organization')
|
||||
: __(_organization.value.organization_name)
|
||||
let size = detailMode.value ? '' : 'xl'
|
||||
let actions = detailMode.value
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: editMode.value ? __('Save') : __('Create'),
|
||||
variant: 'solid',
|
||||
onClick: () =>
|
||||
editMode.value ? updateOrganization() : callInsertDoc(),
|
||||
},
|
||||
]
|
||||
let size = 'xl'
|
||||
let actions = [
|
||||
{
|
||||
label: editMode.value ? __('Save') : __('Create'),
|
||||
variant: 'solid',
|
||||
onClick: () => (editMode.value ? updateOrganization() : callInsertDoc()),
|
||||
},
|
||||
]
|
||||
|
||||
return { title, size, actions }
|
||||
})
|
||||
|
||||
const fields = computed(() => {
|
||||
let details = [
|
||||
{
|
||||
icon: OrganizationsIcon,
|
||||
name: 'organization_name',
|
||||
value: _organization.value.organization_name,
|
||||
},
|
||||
{
|
||||
icon: WebsiteIcon,
|
||||
name: 'website',
|
||||
value: _organization.value.website,
|
||||
},
|
||||
{
|
||||
icon: TerritoryIcon,
|
||||
name: 'territory',
|
||||
value: _organization.value.territory,
|
||||
},
|
||||
{
|
||||
icon: MoneyIcon,
|
||||
name: 'annual_revenue',
|
||||
value: formatNumberIntoCurrency(
|
||||
_organization.value.annual_revenue,
|
||||
_organization.value.currency,
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: h(FeatherIcon, { name: 'hash', class: 'h-4 w-4' }),
|
||||
name: 'no_of_employees',
|
||||
value: _organization.value.no_of_employees,
|
||||
},
|
||||
{
|
||||
icon: h(FeatherIcon, { name: 'briefcase', class: 'h-4 w-4' }),
|
||||
name: 'industry',
|
||||
value: _organization.value.industry,
|
||||
},
|
||||
]
|
||||
|
||||
return details.filter((field) => field.value)
|
||||
})
|
||||
|
||||
const sections = createResource({
|
||||
const tabs = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||
cache: ['quickEntryFields', 'CRM Organization'],
|
||||
cache: ['QuickEntry', 'CRM Organization'],
|
||||
params: { doctype: 'CRM Organization', type: 'Quick Entry' },
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const filteredSections = computed(() => {
|
||||
let allSections = sections.data || []
|
||||
let allSections = tabs.data?.[0]?.sections || []
|
||||
if (!allSections.length) return []
|
||||
|
||||
allSections.forEach((s) => {
|
||||
@ -272,7 +212,7 @@ const filteredSections = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
return allSections
|
||||
return [{ no_tabs: true, sections: allSections }]
|
||||
})
|
||||
|
||||
watch(
|
||||
@ -280,7 +220,6 @@ watch(
|
||||
(value) => {
|
||||
if (!value) return
|
||||
editMode.value = false
|
||||
detailMode.value = props.options.detailMode
|
||||
nextTick(() => {
|
||||
// TODO: Issue with FormControl
|
||||
// title.value.el.focus()
|
||||
@ -293,12 +232,10 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
const showQuickEntryModal = defineModel('quickEntry')
|
||||
const showQuickEntryModal = ref(false)
|
||||
|
||||
function openQuickEntryModal() {
|
||||
showQuickEntryModal.value = true
|
||||
nextTick(() => {
|
||||
show.value = false
|
||||
})
|
||||
nextTick(() => (show.value = false))
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -35,13 +35,13 @@
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="sections?.data">
|
||||
<QuickEntryLayoutBuilder
|
||||
<div v-if="tabs?.data">
|
||||
<FieldLayoutEditor
|
||||
v-if="!preview"
|
||||
:sections="sections.data"
|
||||
:tabs="tabs.data"
|
||||
:doctype="_doctype"
|
||||
/>
|
||||
<Fields v-else :sections="sections.data" :data="{}" />
|
||||
<FieldLayout v-else :tabs="tabs.data" :data="{}" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -59,8 +59,8 @@
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import QuickEntryLayoutBuilder from '@/components/QuickEntryLayoutBuilder.vue'
|
||||
import FieldLayout from '@/components/FieldLayout.vue'
|
||||
import FieldLayoutEditor from '@/components/FieldLayoutEditor.vue'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { capture } from '@/telemetry'
|
||||
import { Dialog, Badge, Switch, call, createResource } from 'frappe-ui'
|
||||
@ -83,20 +83,20 @@ function getParams() {
|
||||
return { doctype: _doctype.value, type: 'Quick Entry' }
|
||||
}
|
||||
|
||||
const sections = createResource({
|
||||
const tabs = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||
cache: ['quick-entry-sections', _doctype.value],
|
||||
cache: ['QuickEntryModal', _doctype.value],
|
||||
params: getParams(),
|
||||
onSuccess(data) {
|
||||
sections.originalData = JSON.parse(JSON.stringify(data))
|
||||
tabs.originalData = JSON.parse(JSON.stringify(data))
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => sections?.data,
|
||||
() => tabs?.data,
|
||||
() => {
|
||||
dirty.value =
|
||||
JSON.stringify(sections?.data) !== JSON.stringify(sections?.originalData)
|
||||
JSON.stringify(tabs?.data) !== JSON.stringify(tabs?.originalData)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
@ -105,18 +105,21 @@ onMounted(() => useDebounceFn(reload, 100)())
|
||||
|
||||
function reload() {
|
||||
nextTick(() => {
|
||||
sections.params = getParams()
|
||||
sections.reload()
|
||||
tabs.params = getParams()
|
||||
tabs.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,
|
||||
)
|
||||
let _tabs = JSON.parse(JSON.stringify(tabs.data))
|
||||
_tabs.forEach((tab) => {
|
||||
if (!tab.sections) return
|
||||
tab.sections.forEach((section) => {
|
||||
if (!section.fields) return
|
||||
section.fields = section.fields.map(
|
||||
(field) => field.fieldname || field.name,
|
||||
)
|
||||
})
|
||||
})
|
||||
loading.value = true
|
||||
call(
|
||||
@ -124,7 +127,7 @@ function saveChanges() {
|
||||
{
|
||||
doctype: _doctype.value,
|
||||
type: 'Quick Entry',
|
||||
layout: JSON.stringify(_sections),
|
||||
layout: JSON.stringify(_tabs),
|
||||
},
|
||||
).then(() => {
|
||||
loading.value = false
|
||||
|
||||
@ -29,21 +29,27 @@
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="sections.data" class="flex gap-4">
|
||||
<SidePanelLayoutBuilder
|
||||
<div v-if="tabs.data?.[0]?.sections" class="flex gap-4">
|
||||
<SidePanelLayoutEditor
|
||||
class="flex flex-1 flex-col pr-2"
|
||||
:sections="sections.data"
|
||||
:sections="tabs.data[0].sections"
|
||||
:doctype="_doctype"
|
||||
/>
|
||||
<div v-if="preview" class="flex flex-1 flex-col border rounded">
|
||||
<div
|
||||
v-for="(section, i) in sections.data"
|
||||
v-for="(section, i) in tabs.data[0].sections"
|
||||
:key="section.label"
|
||||
class="flex flex-col py-1.5 px-1"
|
||||
:class="{ 'border-b': i !== sections.data?.length - 1 }"
|
||||
:class="{
|
||||
'border-b': i !== tabs.data[0].sections?.length - 1,
|
||||
}"
|
||||
>
|
||||
<Section :is-opened="section.opened" :label="section.label">
|
||||
<SectionFields
|
||||
<Section
|
||||
class="p-2"
|
||||
:label="section.label"
|
||||
:opened="section.opened"
|
||||
>
|
||||
<SidePanelLayout
|
||||
:fields="section.fields"
|
||||
:isLastSection="i == section.data?.length - 1"
|
||||
v-model="data"
|
||||
@ -75,8 +81,8 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import Section from '@/components/Section.vue'
|
||||
import SectionFields from '@/components/SectionFields.vue'
|
||||
import SidePanelLayoutBuilder from '@/components/Settings/SidePanelLayoutBuilder.vue'
|
||||
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||
import SidePanelLayoutEditor from '@/components/SidePanelLayoutEditor.vue'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { capture } from '@/telemetry'
|
||||
import { Dialog, Badge, Switch, call, createResource } from 'frappe-ui'
|
||||
@ -102,20 +108,20 @@ function getParams() {
|
||||
return { doctype: _doctype.value, type: 'Side Panel' }
|
||||
}
|
||||
|
||||
const sections = createResource({
|
||||
const tabs = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||
cache: ['sidebar-sections', _doctype.value],
|
||||
cache: ['SidePanel', _doctype.value],
|
||||
params: getParams(),
|
||||
onSuccess(data) {
|
||||
sections.originalData = JSON.parse(JSON.stringify(data))
|
||||
tabs.originalData = JSON.parse(JSON.stringify(data))
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => sections?.data,
|
||||
() => tabs?.data,
|
||||
() => {
|
||||
dirty.value =
|
||||
JSON.stringify(sections?.data) !== JSON.stringify(sections?.originalData)
|
||||
JSON.stringify(tabs?.data) !== JSON.stringify(tabs?.originalData)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
@ -124,18 +130,20 @@ onMounted(() => useDebounceFn(reload, 100)())
|
||||
|
||||
function reload() {
|
||||
nextTick(() => {
|
||||
sections.params = getParams()
|
||||
sections.reload()
|
||||
tabs.params = getParams()
|
||||
tabs.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)
|
||||
.filter(Boolean)
|
||||
let _tabs = JSON.parse(JSON.stringify(tabs.data))
|
||||
_tabs.forEach((tab) => {
|
||||
tab.sections.forEach((section) => {
|
||||
if (!section.fields) return
|
||||
section.fields = section.fields
|
||||
.map((field) => field.fieldname || field.name)
|
||||
.filter(Boolean)
|
||||
})
|
||||
})
|
||||
loading.value = true
|
||||
call(
|
||||
@ -143,7 +151,7 @@ function saveChanges() {
|
||||
{
|
||||
doctype: _doctype.value,
|
||||
type: 'Side Panel',
|
||||
layout: JSON.stringify(_sections),
|
||||
layout: JSON.stringify(_tabs),
|
||||
},
|
||||
).then(() => {
|
||||
loading.value = false
|
||||
@ -1,207 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Draggable :list="sections" item-key="label" class="flex flex-col gap-5.5">
|
||||
<template #item="{ element: section }">
|
||||
<div class="flex flex-col gap-1.5 p-2.5 bg-surface-gray-2 rounded">
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="flex h-7 max-w-fit cursor-pointer items-center gap-2 text-base font-medium leading-4 text-ink-gray-9"
|
||||
>
|
||||
<div
|
||||
v-if="!section.editingLabel"
|
||||
:class="{ 'text-ink-gray-3': section.hideLabel }"
|
||||
>
|
||||
{{ __(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>
|
||||
<Draggable
|
||||
:list="section.fields"
|
||||
group="fields"
|
||||
item-key="label"
|
||||
class="grid gap-1.5"
|
||||
:class="
|
||||
section.columns ? 'grid-cols-' + section.columns : 'grid-cols-3'
|
||||
"
|
||||
handle=".cursor-grab"
|
||||
>
|
||||
<template #item="{ element: field }">
|
||||
<div
|
||||
class="px-2.5 py-2 border border-outline-gray-2 rounded text-base bg-surface-modal text-ink-gray-8 flex items-center leading-4 justify-between gap-2"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<DragVerticalIcon class="h-3.5 cursor-grab" />
|
||||
<div>{{ field.label }}</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="!size-4 rounded-sm"
|
||||
icon="x"
|
||||
@click="
|
||||
section.fields.splice(section.fields.indexOf(field), 1)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
<Autocomplete
|
||||
v-if="fields.data"
|
||||
value=""
|
||||
:options="fields.data"
|
||||
@change="(e) => addField(section, e)"
|
||||
>
|
||||
<template #target="{ togglePopover }">
|
||||
<div class="gap-2 w-full">
|
||||
<Button
|
||||
class="w-full !h-8 !bg-surface-modal"
|
||||
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 text-ink-gray-9">
|
||||
<div>{{ option.label }}</div>
|
||||
<div class="text-ink-gray-4 text-sm">
|
||||
{{ `${option.fieldname} - ${option.fieldtype}` }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
<div class="mt-5.5">
|
||||
<Button
|
||||
class="w-full h-8"
|
||||
variant="subtle"
|
||||
: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>
|
||||
@ -2,15 +2,25 @@
|
||||
<slot name="header" v-bind="{ opened, hide, open, close, toggle }">
|
||||
<div v-if="!hide" class="flex items-center justify-between">
|
||||
<div
|
||||
class="flex h-7 text-ink-gray-9 max-w-fit cursor-pointer items-center gap-2 pl-2 pr-3 text-base font-semibold leading-5"
|
||||
@click="toggle()"
|
||||
class="flex text-ink-gray-9 max-w-fit cursor-pointer items-center gap-2 text-base"
|
||||
v-bind="$attrs"
|
||||
@click="collapsible && toggle()"
|
||||
>
|
||||
<FeatherIcon
|
||||
v-if="collapsible && collapseIconPosition === 'left'"
|
||||
name="chevron-right"
|
||||
class="h-4 transition-all duration-300 ease-in-out"
|
||||
:class="{ 'rotate-90': opened }"
|
||||
/>
|
||||
<span>
|
||||
{{ __(label) || __('Untitled') }}
|
||||
</span>
|
||||
<FeatherIcon
|
||||
v-if="collapsible && collapseIconPosition === 'right'"
|
||||
name="chevron-right"
|
||||
class="h-4 transition-all duration-300 ease-in-out"
|
||||
:class="{ 'rotate-90': opened }"
|
||||
/>
|
||||
{{ __(label) || __('Untitled') }}
|
||||
</div>
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
@ -23,13 +33,14 @@
|
||||
enter-from-class="max-h-0 overflow-hidden"
|
||||
leave-to-class="max-h-0 overflow-hidden"
|
||||
>
|
||||
<div v-if="opened">
|
||||
<div v-show="opened">
|
||||
<slot v-bind="{ opened, open, close, toggle }" />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
@ -39,11 +50,23 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isOpened: {
|
||||
opened: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
collapsible: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
collapseIconPosition: {
|
||||
type: String,
|
||||
default: 'left',
|
||||
},
|
||||
})
|
||||
|
||||
const hide = ref(props.hideLabel)
|
||||
const opened = ref(props.opened)
|
||||
|
||||
function toggle() {
|
||||
opened.value = !opened.value
|
||||
}
|
||||
@ -55,7 +78,4 @@ function open() {
|
||||
function close() {
|
||||
opened.value = false
|
||||
}
|
||||
|
||||
let opened = ref(props.isOpened)
|
||||
let hide = ref(props.hideLabel)
|
||||
</script>
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-8">
|
||||
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5 text-ink-gray-9">
|
||||
<h2
|
||||
class="flex gap-2 text-xl font-semibold leading-none h-5 text-ink-gray-9"
|
||||
>
|
||||
<div>{{ title || __(doctype) }}</div>
|
||||
<Badge
|
||||
v-if="data.isDirty"
|
||||
@ -10,11 +12,7 @@
|
||||
/>
|
||||
</h2>
|
||||
<div v-if="!data.get.loading" class="flex-1 overflow-y-auto">
|
||||
<Fields
|
||||
v-if="data?.doc && sections"
|
||||
:sections="sections"
|
||||
:data="data.doc"
|
||||
/>
|
||||
<FieldLayout v-if="data?.doc && tabs" :tabs="tabs" :data="data.doc" />
|
||||
<ErrorMessage class="mt-2" :message="error" />
|
||||
</div>
|
||||
<div v-else class="flex flex-1 items-center justify-center">
|
||||
@ -31,7 +29,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import FieldLayout from '@/components/FieldLayout.vue'
|
||||
import {
|
||||
createDocumentResource,
|
||||
createResource,
|
||||
@ -96,7 +94,7 @@ const data = createDocumentResource({
|
||||
},
|
||||
})
|
||||
|
||||
const sections = computed(() => {
|
||||
const tabs = computed(() => {
|
||||
if (!fields.data) return []
|
||||
let _sections = []
|
||||
let fieldsData = fields.data
|
||||
@ -136,7 +134,7 @@ const sections = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
return _sections
|
||||
return [{ no_tabs: true, sections: _sections }]
|
||||
})
|
||||
|
||||
function update() {
|
||||
@ -146,7 +144,8 @@ function update() {
|
||||
}
|
||||
|
||||
function validateMandatoryFields() {
|
||||
for (let section of sections.value) {
|
||||
if (!tabs.value) return false
|
||||
for (let section of tabs.value[0].sections) {
|
||||
for (let field of section.fields) {
|
||||
if (
|
||||
(field.mandatory ||
|
||||
|
||||
@ -18,15 +18,12 @@
|
||||
:class="inputClasses"
|
||||
@click="() => togglePopover()"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="flex text-base leading-5 items-center truncate">
|
||||
<slot name="prefix" />
|
||||
<span
|
||||
class="overflow-hidden text-ellipsis whitespace-nowrap text-base leading-5"
|
||||
v-if="selectedValue"
|
||||
>
|
||||
<span v-if="selectedValue" class="truncate">
|
||||
{{ displayValue(selectedValue) }}
|
||||
</span>
|
||||
<span class="text-base leading-5 text-ink-gray-4" v-else>
|
||||
<span v-else class="text-ink-gray-4 truncate">
|
||||
{{ placeholder || '' }}
|
||||
</span>
|
||||
</div>
|
||||
@ -66,7 +63,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<ComboboxOptions
|
||||
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
|
||||
class="my-1 max-h-[12rem] overflow-y-auto p-1.5 pt-0"
|
||||
static
|
||||
>
|
||||
<div
|
||||
|
||||
@ -60,9 +60,7 @@
|
||||
clip-path: inset(22px 0 0 0);
|
||||
"
|
||||
>
|
||||
<CameraIcon
|
||||
class="h-6 w-6 cursor-pointer text-white"
|
||||
/>
|
||||
<CameraIcon class="h-6 w-6 cursor-pointer text-white" />
|
||||
</div>
|
||||
</component>
|
||||
</div>
|
||||
@ -129,7 +127,7 @@
|
||||
class="flex flex-col p-3"
|
||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||
>
|
||||
<Section :is-opened="section.opened" :label="section.label">
|
||||
<Section :label="section.label" :opened="section.opened">
|
||||
<template #actions>
|
||||
<Button
|
||||
v-if="i == 0 && isManager()"
|
||||
@ -140,7 +138,7 @@
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
</template>
|
||||
<SectionFields
|
||||
<SidePanelLayout
|
||||
v-if="section.fields"
|
||||
:fields="section.fields"
|
||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||
@ -204,14 +202,14 @@
|
||||
import Resizer from '@/components/Resizer.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import Section from '@/components/Section.vue'
|
||||
import SectionFields from '@/components/SectionFields.vue'
|
||||
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
||||
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
||||
import DealsListView from '@/components/ListViews/DealsListView.vue'
|
||||
import SidePanelModal from '@/components/Settings/SidePanelModal.vue'
|
||||
import SidePanelModal from '@/components/Modals/SidePanelModal.vue'
|
||||
import AddressModal from '@/components/Modals/AddressModal.vue'
|
||||
import {
|
||||
formatDate,
|
||||
|
||||
@ -59,16 +59,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ContactModal
|
||||
v-model="showContactModal"
|
||||
v-model:quickEntry="showQuickEntryModal"
|
||||
:contact="{}"
|
||||
/>
|
||||
<QuickEntryModal
|
||||
v-if="showQuickEntryModal"
|
||||
v-model="showQuickEntryModal"
|
||||
doctype="Contact"
|
||||
/>
|
||||
<ContactModal v-model="showContactModal" :contact="{}" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -77,7 +68,6 @@ import CustomActions from '@/components/CustomActions.vue'
|
||||
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
||||
import ContactsListView from '@/components/ListViews/ContactsListView.vue'
|
||||
import ViewControls from '@/components/ViewControls.vue'
|
||||
import { organizationsStore } from '@/stores/organizations.js'
|
||||
@ -87,7 +77,6 @@ import { ref, computed } from 'vue'
|
||||
const { getOrganization } = organizationsStore()
|
||||
|
||||
const showContactModal = ref(false)
|
||||
const showQuickEntryModal = ref(false)
|
||||
|
||||
const contactsListView = ref(null)
|
||||
|
||||
|
||||
@ -124,7 +124,11 @@
|
||||
class="section flex flex-col p-3"
|
||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||
>
|
||||
<Section :is-opened="section.opened" :label="section.label">
|
||||
<Section
|
||||
class="px-2 font-semibold"
|
||||
:label="section.label"
|
||||
:opened="section.opened"
|
||||
>
|
||||
<template #actions>
|
||||
<div v-if="section.contacts" class="pr-2">
|
||||
<Link
|
||||
@ -163,7 +167,7 @@
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
</template>
|
||||
<SectionFields
|
||||
<SidePanelLayout
|
||||
v-if="section.fields"
|
||||
:fields="section.fields"
|
||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||
@ -189,7 +193,7 @@
|
||||
class="px-2 pb-2.5"
|
||||
:class="[i == 0 ? 'pt-5' : 'pt-2.5']"
|
||||
>
|
||||
<Section :is-opened="contact.opened">
|
||||
<Section :opened="contact.opened">
|
||||
<template #header="{ opened, toggle }">
|
||||
<div
|
||||
class="flex cursor-pointer items-center justify-between gap-2 pr-1 text-base leading-5 text-ink-gray-7"
|
||||
@ -326,6 +330,7 @@ import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
||||
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
||||
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||
@ -342,10 +347,10 @@ import AssignmentModal from '@/components/Modals/AssignmentModal.vue'
|
||||
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
|
||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||
import SidePanelModal from '@/components/Settings/SidePanelModal.vue'
|
||||
import SidePanelModal from '@/components/Modals/SidePanelModal.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import Section from '@/components/Section.vue'
|
||||
import SectionFields from '@/components/SectionFields.vue'
|
||||
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||
import SLASection from '@/components/SLASection.vue'
|
||||
import CustomActions from '@/components/CustomActions.vue'
|
||||
import {
|
||||
@ -554,6 +559,11 @@ const tabs = computed(() => {
|
||||
label: __('Comments'),
|
||||
icon: CommentIcon,
|
||||
},
|
||||
{
|
||||
name: 'Data',
|
||||
label: __('Data'),
|
||||
icon: DetailsIcon,
|
||||
},
|
||||
{
|
||||
name: 'Calls',
|
||||
label: __('Calls'),
|
||||
|
||||
@ -177,8 +177,12 @@
|
||||
class="flex flex-col p-3"
|
||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||
>
|
||||
<Section :is-opened="section.opened" :label="section.label">
|
||||
<SectionFields
|
||||
<Section
|
||||
class="px-2 font-semibold"
|
||||
:label="section.label"
|
||||
:opened="section.opened"
|
||||
>
|
||||
<SidePanelLayout
|
||||
:fields="section.fields"
|
||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||
v-model="lead.data"
|
||||
@ -298,6 +302,7 @@ import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
||||
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
||||
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||
@ -312,11 +317,11 @@ import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import Activities from '@/components/Activities/Activities.vue'
|
||||
import AssignmentModal from '@/components/Modals/AssignmentModal.vue'
|
||||
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
|
||||
import SidePanelModal from '@/components/Settings/SidePanelModal.vue'
|
||||
import SidePanelModal from '@/components/Modals/SidePanelModal.vue'
|
||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import Section from '@/components/Section.vue'
|
||||
import SectionFields from '@/components/SectionFields.vue'
|
||||
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||
import SLASection from '@/components/SLASection.vue'
|
||||
import CustomActions from '@/components/CustomActions.vue'
|
||||
import {
|
||||
@ -500,6 +505,11 @@ const tabs = computed(() => {
|
||||
label: __('Comments'),
|
||||
icon: CommentIcon,
|
||||
},
|
||||
{
|
||||
name: 'Data',
|
||||
label: __('Data'),
|
||||
icon: DetailsIcon,
|
||||
},
|
||||
{
|
||||
name: 'Calls',
|
||||
label: __('Calls'),
|
||||
|
||||
@ -141,8 +141,8 @@
|
||||
class="flex flex-col px-2 py-3 sm:p-3"
|
||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||
>
|
||||
<Section :is-opened="section.opened" :label="section.label">
|
||||
<SectionFields
|
||||
<Section :label="section.label" :opened="section.opened">
|
||||
<SidePanelLayout
|
||||
:fields="section.fields"
|
||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||
v-model="contact.data"
|
||||
@ -178,7 +178,7 @@
|
||||
<script setup>
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import Section from '@/components/Section.vue'
|
||||
import SectionFields from '@/components/SectionFields.vue'
|
||||
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
class="flex flex-col px-2 py-3 sm:p-3"
|
||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||
>
|
||||
<Section :is-opened="section.opened" :label="section.label">
|
||||
<Section :label="section.label" :opened="section.opened">
|
||||
<template #actions>
|
||||
<div v-if="section.contacts" class="pr-2">
|
||||
<Link
|
||||
@ -98,7 +98,7 @@
|
||||
</Link>
|
||||
</div>
|
||||
</template>
|
||||
<SectionFields
|
||||
<SidePanelLayout
|
||||
v-if="section.fields"
|
||||
:fields="section.fields"
|
||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||
@ -124,7 +124,7 @@
|
||||
class="px-2 pb-2.5"
|
||||
:class="[i == 0 ? 'pt-5' : 'pt-2.5']"
|
||||
>
|
||||
<Section :is-opened="contact.opened">
|
||||
<Section :opened="contact.opened">
|
||||
<template #header="{ opened, toggle }">
|
||||
<div
|
||||
class="flex cursor-pointer items-center justify-between gap-2 pr-1 text-base leading-5 text-ink-gray-7"
|
||||
@ -267,7 +267,7 @@ import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import Section from '@/components/Section.vue'
|
||||
import SectionFields from '@/components/SectionFields.vue'
|
||||
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||
import SLASection from '@/components/SLASection.vue'
|
||||
import CustomActions from '@/components/CustomActions.vue'
|
||||
import { createToast, setupAssignees, setupCustomizations } from '@/utils'
|
||||
@ -452,6 +452,11 @@ const tabs = computed(() => {
|
||||
label: __('Comments'),
|
||||
icon: CommentIcon,
|
||||
},
|
||||
{
|
||||
name: 'Data',
|
||||
label: __('Data'),
|
||||
icon: DetailsIcon,
|
||||
},
|
||||
{
|
||||
name: 'Calls',
|
||||
label: __('Calls'),
|
||||
|
||||
@ -74,8 +74,8 @@
|
||||
class="flex flex-col px-2 py-3 sm:p-3"
|
||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||
>
|
||||
<Section :is-opened="section.opened" :label="section.label">
|
||||
<SectionFields
|
||||
<Section :label="section.label" :opened="section.opened">
|
||||
<SidePanelLayout
|
||||
:fields="section.fields"
|
||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||
v-model="lead.data"
|
||||
@ -190,7 +190,7 @@ import AssignmentModal from '@/components/Modals/AssignmentModal.vue'
|
||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import Section from '@/components/Section.vue'
|
||||
import SectionFields from '@/components/SectionFields.vue'
|
||||
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||
import SLASection from '@/components/SLASection.vue'
|
||||
import CustomActions from '@/components/CustomActions.vue'
|
||||
import { createToast, setupAssignees, setupCustomizations } from '@/utils'
|
||||
@ -362,6 +362,11 @@ const tabs = computed(() => {
|
||||
label: __('Comments'),
|
||||
icon: CommentIcon,
|
||||
},
|
||||
{
|
||||
name: 'Data',
|
||||
label: __('Data'),
|
||||
icon: DetailsIcon,
|
||||
},
|
||||
{
|
||||
name: 'Calls',
|
||||
label: __('Calls'),
|
||||
|
||||
@ -123,8 +123,8 @@
|
||||
class="flex flex-col px-2 py-3 sm:p-3"
|
||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||
>
|
||||
<Section :is-opened="section.opened" :label="section.label">
|
||||
<SectionFields
|
||||
<Section :label="section.label" :opened="section.opened">
|
||||
<SidePanelLayout
|
||||
:fields="section.fields"
|
||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||
v-model="organization.doc"
|
||||
@ -166,7 +166,7 @@
|
||||
|
||||
<script setup>
|
||||
import Section from '@/components/Section.vue'
|
||||
import SectionFields from '@/components/SectionFields.vue'
|
||||
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import AddressModal from '@/components/Modals/AddressModal.vue'
|
||||
|
||||
@ -112,7 +112,7 @@
|
||||
class="flex flex-col p-3"
|
||||
:class="{ 'border-b': i !== fieldsLayout.data.length - 1 }"
|
||||
>
|
||||
<Section :is-opened="section.opened" :label="section.label">
|
||||
<Section :label="section.label" :opened="section.opened">
|
||||
<template #actions>
|
||||
<Button
|
||||
v-if="i == 0 && isManager()"
|
||||
@ -123,7 +123,7 @@
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
</template>
|
||||
<SectionFields
|
||||
<SidePanelLayout
|
||||
v-if="section.fields"
|
||||
:fields="section.fields"
|
||||
:isLastSection="i == fieldsLayout.data.length - 1"
|
||||
@ -198,8 +198,8 @@
|
||||
<script setup>
|
||||
import Resizer from '@/components/Resizer.vue'
|
||||
import Section from '@/components/Section.vue'
|
||||
import SectionFields from '@/components/SectionFields.vue'
|
||||
import SidePanelModal from '@/components/Settings/SidePanelModal.vue'
|
||||
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||
import SidePanelModal from '@/components/Modals/SidePanelModal.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
||||
|
||||
@ -59,15 +59,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<OrganizationModal
|
||||
v-model="showOrganizationModal"
|
||||
v-model:quickEntry="showQuickEntryModal"
|
||||
/>
|
||||
<QuickEntryModal
|
||||
v-if="showQuickEntryModal"
|
||||
v-model="showQuickEntryModal"
|
||||
doctype="CRM Organization"
|
||||
/>
|
||||
<OrganizationModal v-model="showOrganizationModal" />
|
||||
</template>
|
||||
<script setup>
|
||||
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
||||
@ -75,7 +67,6 @@ import CustomActions from '@/components/CustomActions.vue'
|
||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||
import QuickEntryModal from '@/components/Modals/QuickEntryModal.vue'
|
||||
import OrganizationsListView from '@/components/ListViews/OrganizationsListView.vue'
|
||||
import ViewControls from '@/components/ViewControls.vue'
|
||||
import { formatDate, timeAgo, website, formatNumberIntoCurrency } from '@/utils'
|
||||
@ -83,7 +74,6 @@ import { ref, computed } from 'vue'
|
||||
|
||||
const organizationsListView = ref(null)
|
||||
const showOrganizationModal = ref(false)
|
||||
const showQuickEntryModal = ref(false)
|
||||
|
||||
// organizations data is loaded in the ViewControls component
|
||||
const organizations = ref({})
|
||||
|
||||
@ -6,10 +6,7 @@ module.exports = {
|
||||
'./node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}',
|
||||
'../node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}',
|
||||
],
|
||||
safelist: [
|
||||
{ pattern: /!(text|bg)-/, variants: ['hover', 'active'] },
|
||||
{ pattern: /^grid-cols-/ },
|
||||
],
|
||||
safelist: [{ pattern: /!(text|bg)-/, variants: ['hover', 'active'] }],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
|
||||
@ -19,3 +19,46 @@ build-backend = "flit_core.buildapi"
|
||||
# These dependencies are only installed when developer mode is enabled
|
||||
[tool.bench.dev-dependencies]
|
||||
# package_name = "~=1.1.0"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 110
|
||||
target-version = "py310"
|
||||
exclude = [
|
||||
"**/doctype/*/boilerplate/*.py" # boilerplate are template strings, not valid python
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"F",
|
||||
"E",
|
||||
"W",
|
||||
"I",
|
||||
"UP",
|
||||
"B",
|
||||
"RUF",
|
||||
]
|
||||
ignore = [
|
||||
"B017", # assertRaises(Exception) - should be more specific
|
||||
"B018", # useless expression, not assigned to anything
|
||||
"B023", # function doesn't bind loop variable - will have last iteration's value
|
||||
"B904", # raise inside except without from
|
||||
"E101", # indentation contains mixed spaces and tabs
|
||||
"E402", # module level import not at top of file
|
||||
"E501", # line too long
|
||||
"E741", # ambiguous variable name
|
||||
"F401", # "unused" imports
|
||||
"F403", # can't detect undefined names from * import
|
||||
"F405", # can't detect undefined names from * import
|
||||
"F722", # syntax error in forward type annotation
|
||||
"W191", # indentation contains tabs
|
||||
"RUF001", # string contains ambiguous unicode character
|
||||
"UP030", # Use implicit references for positional format fields (translations)
|
||||
"UP031", # Use format specifiers instead of percent format
|
||||
"UP032", # Use f-string instead of `format` call (translations)
|
||||
]
|
||||
typing-modules = ["frappe.types.DF"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "tab"
|
||||
docstring-code-format = true
|
||||
Loading…
x
Reference in New Issue
Block a user