Merge pull request #235 from frappe/develop
chore: Merge develop to main
This commit is contained in:
commit
4bdc1b2932
264
crm/api/doc.py
264
crm/api/doc.py
@ -4,6 +4,7 @@ from frappe import _
|
||||
from frappe.model.document import get_controller
|
||||
from frappe.model import no_value_fields
|
||||
from pypika import Criterion
|
||||
from frappe.utils import make_filter_tuple
|
||||
|
||||
from crm.api.views import get_views
|
||||
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
|
||||
@ -200,19 +201,27 @@ def get_quick_filters(doctype: str):
|
||||
return quick_filters
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_list_data(
|
||||
def get_data(
|
||||
doctype: str,
|
||||
filters: dict,
|
||||
order_by: str,
|
||||
page_length=20,
|
||||
page_length_count=20,
|
||||
columns=None,
|
||||
rows=None,
|
||||
column_field=None,
|
||||
title_field=None,
|
||||
columns=[],
|
||||
rows=[],
|
||||
kanban_columns=[],
|
||||
kanban_fields=[],
|
||||
view=None,
|
||||
default_filters=None,
|
||||
):
|
||||
custom_view = False
|
||||
filters = frappe._dict(filters)
|
||||
rows = frappe.parse_json(rows or "[]")
|
||||
columns = frappe.parse_json(columns or "[]")
|
||||
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
|
||||
@ -235,61 +244,133 @@ def get_list_data(
|
||||
filters.update(default_filters)
|
||||
|
||||
is_default = True
|
||||
if columns or rows:
|
||||
custom_view = True
|
||||
is_default = False
|
||||
columns = frappe.parse_json(columns)
|
||||
rows = frappe.parse_json(rows)
|
||||
|
||||
if not columns:
|
||||
columns = [
|
||||
{"label": "Name", "type": "Data", "key": "name", "width": "16rem"},
|
||||
{"label": "Last Modified", "type": "Datetime", "key": "modified", "width": "8rem"},
|
||||
]
|
||||
|
||||
if not rows:
|
||||
rows = ["name"]
|
||||
|
||||
default_view_filters = {
|
||||
"dt": doctype,
|
||||
"type": view_type or 'list',
|
||||
"is_default": 1,
|
||||
"user": frappe.session.user,
|
||||
}
|
||||
|
||||
data = []
|
||||
_list = get_controller(doctype)
|
||||
|
||||
if not custom_view and frappe.db.exists("CRM View Settings", default_view_filters):
|
||||
list_view_settings = frappe.get_doc("CRM View Settings", default_view_filters)
|
||||
columns = frappe.parse_json(list_view_settings.columns)
|
||||
rows = frappe.parse_json(list_view_settings.rows)
|
||||
is_default = False
|
||||
elif not custom_view or is_default and hasattr(_list, "default_list_data"):
|
||||
columns = _list.default_list_data().get("columns")
|
||||
|
||||
if hasattr(_list, "default_list_data"):
|
||||
rows = _list.default_list_data().get("rows")
|
||||
|
||||
# check if rows has all keys from columns if not add them
|
||||
for column in columns:
|
||||
if column.get("key") not in rows:
|
||||
rows.append(column.get("key"))
|
||||
column["label"] = _(column.get("label"))
|
||||
if view_type != "kanban":
|
||||
if columns or rows:
|
||||
custom_view = True
|
||||
is_default = False
|
||||
columns = frappe.parse_json(columns)
|
||||
rows = frappe.parse_json(rows)
|
||||
|
||||
if column.get("key") == "_liked_by" and column.get("width") == "10rem":
|
||||
column["width"] = "50px"
|
||||
if not columns:
|
||||
columns = [
|
||||
{"label": "Name", "type": "Data", "key": "name", "width": "16rem"},
|
||||
{"label": "Last Modified", "type": "Datetime", "key": "modified", "width": "8rem"},
|
||||
]
|
||||
|
||||
# check if rows has group_by_field if not add it
|
||||
if group_by_field and group_by_field not in rows:
|
||||
rows.append(group_by_field)
|
||||
if not rows:
|
||||
rows = ["name"]
|
||||
|
||||
data = frappe.get_list(
|
||||
doctype,
|
||||
fields=rows,
|
||||
filters=filters,
|
||||
order_by=order_by,
|
||||
page_length=page_length,
|
||||
) or []
|
||||
default_view_filters = {
|
||||
"dt": doctype,
|
||||
"type": view_type or 'list',
|
||||
"is_default": 1,
|
||||
"user": frappe.session.user,
|
||||
}
|
||||
|
||||
if not custom_view and frappe.db.exists("CRM View Settings", default_view_filters):
|
||||
list_view_settings = frappe.get_doc("CRM View Settings", default_view_filters)
|
||||
columns = frappe.parse_json(list_view_settings.columns)
|
||||
rows = frappe.parse_json(list_view_settings.rows)
|
||||
is_default = False
|
||||
elif not custom_view or is_default and hasattr(_list, "default_list_data"):
|
||||
columns = _list.default_list_data().get("columns")
|
||||
|
||||
# check if rows has all keys from columns if not add them
|
||||
for column in columns:
|
||||
if column.get("key") not in rows:
|
||||
rows.append(column.get("key"))
|
||||
column["label"] = _(column.get("label"))
|
||||
|
||||
if column.get("key") == "_liked_by" and column.get("width") == "10rem":
|
||||
column["width"] = "50px"
|
||||
|
||||
# check if rows has group_by_field if not add it
|
||||
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 []
|
||||
|
||||
if view_type == "kanban":
|
||||
if not kanban_columns and column_field:
|
||||
field_meta = frappe.get_meta(doctype).get_field(column_field)
|
||||
if field_meta.fieldtype == "Link":
|
||||
kanban_columns = frappe.get_all(
|
||||
field_meta.options,
|
||||
fields=["name"],
|
||||
order_by="modified asc",
|
||||
)
|
||||
elif field_meta.fieldtype == "Select":
|
||||
kanban_columns = [{"name": option} for option in field_meta.options.split("\n")]
|
||||
|
||||
if not title_field:
|
||||
title_field = "name"
|
||||
if hasattr(_list, "default_kanban_settings"):
|
||||
title_field = _list.default_kanban_settings().get("title_field")
|
||||
|
||||
if title_field not in rows:
|
||||
rows.append(title_field)
|
||||
|
||||
if not kanban_fields:
|
||||
kanban_fields = ["name"]
|
||||
if hasattr(_list, "default_kanban_settings"):
|
||||
kanban_fields = json.loads(_list.default_kanban_settings().get("kanban_fields"))
|
||||
|
||||
for field in kanban_fields:
|
||||
if field not in rows:
|
||||
rows.append(field)
|
||||
|
||||
for kc in kanban_columns:
|
||||
column_filters = { column_field: kc.get('name') }
|
||||
if column_field in filters and filters.get(column_field) != kc.name:
|
||||
column_data = []
|
||||
else:
|
||||
column_filters.update(filters.copy())
|
||||
page_length = 20
|
||||
|
||||
if kc.get("page_length"):
|
||||
page_length = kc.get("page_length")
|
||||
|
||||
order = kc.get("order")
|
||||
if order:
|
||||
column_data = get_records_based_on_order(doctype, rows, column_filters, page_length, order)
|
||||
else:
|
||||
column_data = frappe.get_list(
|
||||
doctype,
|
||||
fields=rows,
|
||||
filters=convert_filter_to_tuple(doctype, column_filters),
|
||||
order_by=order_by,
|
||||
page_length=page_length,
|
||||
)
|
||||
|
||||
new_filters = filters.copy()
|
||||
new_filters.update({ column_field: kc.get('name') })
|
||||
|
||||
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)
|
||||
|
||||
for d in column_data:
|
||||
getCounts(d, doctype)
|
||||
|
||||
if order:
|
||||
column_data = sorted(
|
||||
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})
|
||||
|
||||
fields = frappe.get_meta(doctype).fields
|
||||
fields = [field for field in fields if field.fieldtype not in no_value_fields]
|
||||
@ -365,6 +446,10 @@ def get_list_data(
|
||||
"columns": columns,
|
||||
"rows": rows,
|
||||
"fields": fields,
|
||||
"column_field": column_field,
|
||||
"title_field": title_field,
|
||||
"kanban_columns": kanban_columns,
|
||||
"kanban_fields": kanban_fields,
|
||||
"group_by_field": group_by_field,
|
||||
"page_length": page_length,
|
||||
"page_length_count": page_length_count,
|
||||
@ -374,8 +459,46 @@ def get_list_data(
|
||||
"row_count": len(data),
|
||||
"form_script": get_form_script(doctype),
|
||||
"list_script": get_form_script(doctype, "List"),
|
||||
"view_type": view_type,
|
||||
}
|
||||
|
||||
def convert_filter_to_tuple(doctype, filters):
|
||||
if isinstance(filters, dict):
|
||||
filters_items = filters.items()
|
||||
filters = []
|
||||
for key, value in filters_items:
|
||||
filters.append(make_filter_tuple(doctype, key, value))
|
||||
return filters
|
||||
|
||||
|
||||
def get_records_based_on_order(doctype, rows, filters, page_length, order):
|
||||
records = []
|
||||
filters = convert_filter_to_tuple(doctype, filters)
|
||||
in_filters = filters.copy()
|
||||
in_filters.append([doctype, "name", "in", order[:page_length]])
|
||||
records = frappe.get_list(
|
||||
doctype,
|
||||
fields=rows,
|
||||
filters=in_filters,
|
||||
order_by="creation desc",
|
||||
page_length=page_length,
|
||||
)
|
||||
|
||||
if len(records) < page_length:
|
||||
not_in_filters = filters.copy()
|
||||
not_in_filters.append([doctype, "name", "not in", order])
|
||||
remaining_records = frappe.get_list(
|
||||
doctype,
|
||||
fields=rows,
|
||||
filters=not_in_filters,
|
||||
order_by="creation desc",
|
||||
page_length=page_length - len(records),
|
||||
)
|
||||
for record in remaining_records:
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
|
||||
not_allowed_fieldtypes = [
|
||||
@ -391,12 +514,38 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
|
||||
fields = frappe.get_meta(doctype).fields
|
||||
fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes]
|
||||
|
||||
standard_fields = [
|
||||
{"fieldname": "name", "fieldtype": "Link", "label": "ID", "options": doctype},
|
||||
{
|
||||
"fieldname": "owner",
|
||||
"fieldtype": "Link",
|
||||
"label": "Created By",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "modified_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Last Updated By",
|
||||
"options": "User",
|
||||
},
|
||||
{"fieldname": "_user_tags", "fieldtype": "Data", "label": "Tags"},
|
||||
{"fieldname": "_liked_by", "fieldtype": "Data", "label": "Like"},
|
||||
{"fieldname": "_comments", "fieldtype": "Text", "label": "Comments"},
|
||||
{"fieldname": "_assign", "fieldtype": "Text", "label": "Assigned To"},
|
||||
{"fieldname": "creation", "fieldtype": "Datetime", "label": "Created On"},
|
||||
{"fieldname": "modified", "fieldtype": "Datetime", "label": "Last Updated On"},
|
||||
]
|
||||
|
||||
for field in standard_fields:
|
||||
if not restricted_fieldtypes or field.get('fieldtype') not in restricted_fieldtypes:
|
||||
fields.append(field)
|
||||
|
||||
if as_array:
|
||||
return fields
|
||||
|
||||
fields_meta = {}
|
||||
for field in fields:
|
||||
fields_meta[field.fieldname] = field
|
||||
fields_meta[field.get('fieldname')] = field
|
||||
|
||||
return fields_meta
|
||||
|
||||
@ -531,4 +680,13 @@ def get_fields(doctype: str, allow_all_fieldtypes: bool = False):
|
||||
"mandatory": field.reqd,
|
||||
})
|
||||
|
||||
return _fields
|
||||
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
|
||||
@ -190,6 +190,14 @@ class CRMDeal(Document):
|
||||
]
|
||||
return {'columns': columns, 'rows': rows}
|
||||
|
||||
@staticmethod
|
||||
def default_kanban_settings():
|
||||
return {
|
||||
"column_field": "status",
|
||||
"title_field": "organization",
|
||||
"kanban_fields": '["annual_revenue", "email", "mobile_no", "_assign", "modified"]'
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_contact(deal, contact):
|
||||
if not frappe.has_permission("CRM Deal", "write", deal):
|
||||
|
||||
@ -324,6 +324,15 @@ class CRMLead(Document):
|
||||
]
|
||||
return {'columns': columns, 'rows': rows}
|
||||
|
||||
@staticmethod
|
||||
def default_kanban_settings():
|
||||
return {
|
||||
"column_field": "status",
|
||||
"title_field": "lead_name",
|
||||
"kanban_fields": '["organization", "email", "mobile_no", "_assign", "modified"]'
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_to_deal(lead, doc=None):
|
||||
if not (doc and doc.flags.get("ignore_permissions")) and not frappe.has_permission("CRM Lead", "write", lead):
|
||||
|
||||
@ -60,3 +60,11 @@ class CRMTask(Document):
|
||||
"modified",
|
||||
]
|
||||
return {'columns': columns, 'rows': rows}
|
||||
|
||||
@staticmethod
|
||||
def default_kanban_settings():
|
||||
return {
|
||||
"column_field": "status",
|
||||
"title_field": "title",
|
||||
"kanban_fields": '["description", "priority", "creation"]'
|
||||
}
|
||||
|
||||
@ -15,16 +15,23 @@
|
||||
"route_name",
|
||||
"pinned",
|
||||
"public",
|
||||
"columns_tab",
|
||||
"load_default_columns",
|
||||
"columns",
|
||||
"rows",
|
||||
"filters_tab",
|
||||
"filters",
|
||||
"order_by_tab",
|
||||
"order_by",
|
||||
"list_tab",
|
||||
"list_section",
|
||||
"load_default_columns",
|
||||
"columns",
|
||||
"rows",
|
||||
"group_by_tab",
|
||||
"group_by_field"
|
||||
"group_by_field",
|
||||
"kanban_tab",
|
||||
"kanban_section",
|
||||
"column_field",
|
||||
"title_field",
|
||||
"kanban_columns",
|
||||
"kanban_fields"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -48,11 +55,6 @@
|
||||
"fieldtype": "Code",
|
||||
"label": "Filters"
|
||||
},
|
||||
{
|
||||
"fieldname": "columns_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Columns"
|
||||
},
|
||||
{
|
||||
"fieldname": "filters_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
@ -126,7 +128,7 @@
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Type",
|
||||
"options": "list\ngroup_by"
|
||||
"options": "list\ngroup_by\nkanban"
|
||||
},
|
||||
{
|
||||
"fieldname": "group_by_tab",
|
||||
@ -137,11 +139,50 @@
|
||||
"fieldname": "group_by_field",
|
||||
"fieldtype": "Data",
|
||||
"label": "Group By Field"
|
||||
},
|
||||
{
|
||||
"fieldname": "list_section",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "kanban_section",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_field",
|
||||
"fieldtype": "Data",
|
||||
"label": "Column Field"
|
||||
},
|
||||
{
|
||||
"fieldname": "list_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "List"
|
||||
},
|
||||
{
|
||||
"fieldname": "kanban_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Kanban"
|
||||
},
|
||||
{
|
||||
"fieldname": "kanban_columns",
|
||||
"fieldtype": "Code",
|
||||
"label": "Kanban Columns"
|
||||
},
|
||||
{
|
||||
"fieldname": "kanban_fields",
|
||||
"fieldtype": "Code",
|
||||
"label": "Kanban Fields"
|
||||
},
|
||||
{
|
||||
"default": "name",
|
||||
"fieldname": "title_field",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title Field"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-01 16:58:34.952945",
|
||||
"modified": "2024-06-25 19:40:12.067788",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM View Settings",
|
||||
|
||||
@ -16,13 +16,17 @@ def create(view):
|
||||
view.filters = parse_json(view.filters) or {}
|
||||
view.columns = parse_json(view.columns or '[]')
|
||||
view.rows = parse_json(view.rows or '[]')
|
||||
view.kanban_columns = parse_json(view.kanban_columns or '[]')
|
||||
view.kanban_fields = parse_json(view.kanban_fields or '[]')
|
||||
|
||||
default_rows = sync_default_list_rows(view.doctype)
|
||||
default_rows = sync_default_rows(view.doctype)
|
||||
view.rows = view.rows + default_rows if default_rows else view.rows
|
||||
view.rows = remove_duplicates(view.rows)
|
||||
|
||||
if not view.columns:
|
||||
view.columns = sync_default_list_columns(view.doctype)
|
||||
if not view.kanban_columns and view.type == "kanban":
|
||||
view.kanban_columns = sync_default_columns(view)
|
||||
elif not view.columns:
|
||||
view.columns = sync_default_columns(view)
|
||||
|
||||
doc = frappe.new_doc("CRM View Settings")
|
||||
doc.name = view.label
|
||||
@ -36,6 +40,10 @@ def create(view):
|
||||
doc.filters = json.dumps(view.filters)
|
||||
doc.order_by = view.order_by
|
||||
doc.group_by_field = view.group_by_field
|
||||
doc.column_field = view.column_field
|
||||
doc.title_field = view.title_field
|
||||
doc.kanban_columns = json.dumps(view.kanban_columns)
|
||||
doc.kanban_fields = json.dumps(view.kanban_fields)
|
||||
doc.columns = json.dumps(view.columns)
|
||||
doc.rows = json.dumps(view.rows)
|
||||
doc.insert()
|
||||
@ -48,8 +56,10 @@ def update(view):
|
||||
filters = parse_json(view.filters) or {}
|
||||
columns = parse_json(view.columns) or []
|
||||
rows = parse_json(view.rows) or []
|
||||
kanban_columns = parse_json(view.kanban_columns) or []
|
||||
kanban_fields = parse_json(view.kanban_fields) or []
|
||||
|
||||
default_rows = sync_default_list_rows(view.doctype)
|
||||
default_rows = sync_default_rows(view.doctype)
|
||||
rows = rows + default_rows if default_rows else rows
|
||||
rows = remove_duplicates(rows)
|
||||
|
||||
@ -62,6 +72,10 @@ def update(view):
|
||||
doc.filters = json.dumps(filters)
|
||||
doc.order_by = view.order_by
|
||||
doc.group_by_field = view.group_by_field
|
||||
doc.column_field = view.column_field
|
||||
doc.title_field = view.title_field
|
||||
doc.kanban_columns = json.dumps(kanban_columns)
|
||||
doc.kanban_fields = json.dumps(kanban_fields)
|
||||
doc.columns = json.dumps(columns)
|
||||
doc.rows = json.dumps(rows)
|
||||
doc.save()
|
||||
@ -91,7 +105,7 @@ def pin(name, value):
|
||||
def remove_duplicates(l):
|
||||
return list(dict.fromkeys(l))
|
||||
|
||||
def sync_default_list_rows(doctype):
|
||||
def sync_default_rows(doctype, type="list"):
|
||||
list = get_controller(doctype)
|
||||
rows = []
|
||||
|
||||
@ -100,11 +114,21 @@ def sync_default_list_rows(doctype):
|
||||
|
||||
return rows
|
||||
|
||||
def sync_default_list_columns(doctype):
|
||||
list = get_controller(doctype)
|
||||
def sync_default_columns(view):
|
||||
list = get_controller(view.doctype)
|
||||
columns = []
|
||||
|
||||
if hasattr(list, "default_list_data"):
|
||||
if view.type == "kanban" and view.column_field:
|
||||
field_meta = frappe.get_meta(view.doctype).get_field(view.column_field)
|
||||
if field_meta.fieldtype == "Link":
|
||||
columns = frappe.get_all(
|
||||
field_meta.options,
|
||||
fields=["name"],
|
||||
order_by="modified asc",
|
||||
)
|
||||
elif field_meta.fieldtype == "Select":
|
||||
columns = [{"name": option} for option in field_meta.options.split("\n")]
|
||||
elif hasattr(list, "default_list_data"):
|
||||
columns = list.default_list_data().get("columns")
|
||||
|
||||
return columns
|
||||
@ -117,13 +141,17 @@ def create_or_update_default_view(view):
|
||||
filters = parse_json(view.filters) or {}
|
||||
columns = parse_json(view.columns or '[]')
|
||||
rows = parse_json(view.rows or '[]')
|
||||
kanban_columns = parse_json(view.kanban_columns or '[]')
|
||||
kanban_fields = parse_json(view.kanban_fields or '[]')
|
||||
|
||||
default_rows = sync_default_list_rows(view.doctype)
|
||||
default_rows = sync_default_rows(view.doctype, view.type)
|
||||
rows = rows + default_rows if default_rows else rows
|
||||
rows = remove_duplicates(rows)
|
||||
|
||||
if not columns:
|
||||
columns = sync_default_list_columns(view.doctype)
|
||||
if not kanban_columns and view.type == "kanban":
|
||||
kanban_columns = sync_default_columns(view)
|
||||
elif not columns:
|
||||
columns = sync_default_columns(view)
|
||||
|
||||
doc = frappe.db.exists(
|
||||
"CRM View Settings",
|
||||
@ -143,6 +171,10 @@ def create_or_update_default_view(view):
|
||||
doc.filters = json.dumps(filters)
|
||||
doc.order_by = view.order_by
|
||||
doc.group_by_field = view.group_by_field
|
||||
doc.column_field = view.column_field
|
||||
doc.title_field = view.title_field
|
||||
doc.kanban_columns = json.dumps(kanban_columns)
|
||||
doc.kanban_fields = json.dumps(kanban_fields)
|
||||
doc.columns = json.dumps(columns)
|
||||
doc.rows = json.dumps(rows)
|
||||
doc.save()
|
||||
@ -159,6 +191,10 @@ def create_or_update_default_view(view):
|
||||
doc.filters = json.dumps(filters)
|
||||
doc.order_by = view.order_by
|
||||
doc.group_by_field = view.group_by_field
|
||||
doc.column_field = view.column_field
|
||||
doc.title_field = view.title_field
|
||||
doc.kanban_columns = json.dumps(kanban_columns)
|
||||
doc.kanban_fields = json.dumps(kanban_fields)
|
||||
doc.columns = json.dumps(columns)
|
||||
doc.rows = json.dumps(rows)
|
||||
doc.is_default = True
|
||||
|
||||
18
frontend/src/components/Icons/KanbanIcon.vue
Normal file
18
frontend/src/components/Icons/KanbanIcon.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-kanban"
|
||||
>
|
||||
<path d="M6 5v11" />
|
||||
<path d="M12 5v6" />
|
||||
<path d="M18 5v14" />
|
||||
</svg>
|
||||
</template>
|
||||
@ -3,6 +3,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
class="text-gray-700"
|
||||
:aria-label="status"
|
||||
|
||||
221
frontend/src/components/Kanban/KanbanSettings.vue
Normal file
221
frontend/src/components/Kanban/KanbanSettings.vue
Normal file
@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<Button
|
||||
:label="__('Kanban Settings')"
|
||||
@click="showDialog = true"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template #prefix>
|
||||
<KanbanIcon class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Dialog v-model="showDialog" :options="{ title: __('Kanban Settings') }">
|
||||
<template #body-content>
|
||||
<div>
|
||||
<div class="text-base text-gray-800 mb-2">{{ __('Column Field') }}</div>
|
||||
<Autocomplete
|
||||
v-if="columnFields"
|
||||
value=""
|
||||
:options="columnFields"
|
||||
@change="(f) => (columnField = f)"
|
||||
>
|
||||
<template #target="{ togglePopover }">
|
||||
<Button
|
||||
class="w-full !justify-start"
|
||||
variant="subtle"
|
||||
@click="togglePopover()"
|
||||
:label="columnField.label"
|
||||
/>
|
||||
</template>
|
||||
</Autocomplete>
|
||||
<div class="text-base text-gray-800 mb-2 mt-4">
|
||||
{{ __('Title Field') }}
|
||||
</div>
|
||||
<Autocomplete
|
||||
v-if="fields.data"
|
||||
value=""
|
||||
:options="fields.data"
|
||||
@change="(f) => (titleField = f)"
|
||||
>
|
||||
<template #target="{ togglePopover }">
|
||||
<Button
|
||||
class="w-full !justify-start"
|
||||
variant="subtle"
|
||||
@click="togglePopover()"
|
||||
:label="titleField.label"
|
||||
/>
|
||||
</template>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="text-base text-gray-800 mb-2">{{ __('Fields Order') }}</div>
|
||||
<Draggable
|
||||
:list="allFields"
|
||||
@end="reorder"
|
||||
group="fields"
|
||||
item-key="name"
|
||||
class="flex flex-col gap-1"
|
||||
>
|
||||
<template #item="{ element: field }">
|
||||
<div
|
||||
class="px-1 py-0.5 border rounded text-base text-gray-800 flex items-center justify-between gap-2"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<DragVerticalIcon class="h-3.5 cursor-grab" />
|
||||
<div>{{ field.label }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="ghost" icon="x" @click="removeField(field)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
<Autocomplete
|
||||
v-if="fields.data"
|
||||
value=""
|
||||
:options="fields.data"
|
||||
@change="(e) => addField(e)"
|
||||
>
|
||||
<template #target="{ togglePopover }">
|
||||
<Button
|
||||
class="w-full mt-2"
|
||||
variant="outline"
|
||||
@click="togglePopover()"
|
||||
:label="__('Add Field')"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
<template #item-label="{ option }">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div>{{ option.label }}</div>
|
||||
<div class="text-gray-500 text-sm">
|
||||
{{ `${option.fieldname} - ${option.fieldtype}` }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="solid"
|
||||
@click="apply"
|
||||
:label="__('Apply')"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import DragVerticalIcon from '@/components/Icons/DragVerticalIcon.vue'
|
||||
import KanbanIcon from '@/components/Icons/KanbanIcon.vue'
|
||||
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||
import { Dialog, createResource } from 'frappe-ui'
|
||||
import Draggable from 'vuedraggable'
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
doctype: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
const list = defineModel()
|
||||
const showDialog = ref(false)
|
||||
|
||||
const columnField = computed({
|
||||
get: () => {
|
||||
let fieldname = list.value?.data?.column_field
|
||||
if (!fieldname) return ''
|
||||
|
||||
return columnFields.value?.find((field) => field.fieldname === fieldname)
|
||||
},
|
||||
set: (val) => {
|
||||
list.value.data.column_field = val.fieldname
|
||||
},
|
||||
})
|
||||
|
||||
const titleField = computed({
|
||||
get: () => {
|
||||
let fieldname = list.value?.data?.title_field
|
||||
if (!fieldname) return ''
|
||||
|
||||
return fields.data?.find((field) => field.fieldname === fieldname)
|
||||
},
|
||||
set: (val) => {
|
||||
list.value.data.title_field = val.fieldname
|
||||
},
|
||||
})
|
||||
|
||||
const columnFields = computed(() => {
|
||||
return (
|
||||
fields.data?.filter((field) =>
|
||||
['Link', 'Select'].includes(field.fieldtype),
|
||||
) || []
|
||||
)
|
||||
})
|
||||
|
||||
const fields = createResource({
|
||||
url: 'crm.api.doc.get_fields_meta',
|
||||
params: { doctype: props.doctype, as_array: true },
|
||||
cache: ['kanban_fields', props.doctype],
|
||||
auto: true,
|
||||
onSuccess: (data) => {
|
||||
data
|
||||
},
|
||||
})
|
||||
|
||||
const allFields = computed({
|
||||
get: () => {
|
||||
let rows = list.value?.data?.kanban_fields
|
||||
if (!rows) return []
|
||||
|
||||
if (typeof rows === 'string') {
|
||||
rows = JSON.parse(rows)
|
||||
}
|
||||
|
||||
if (rows && fields.data) {
|
||||
rows = rows.map((row) => {
|
||||
return fields.data.find((field) => field.fieldname === row) || {}
|
||||
})
|
||||
}
|
||||
return rows.filter((row) => row.label)
|
||||
},
|
||||
set: (val) => {
|
||||
list.value.data.kanban_fields = val
|
||||
},
|
||||
})
|
||||
|
||||
function reorder() {
|
||||
allFields.value = allFields.value.map((row) => row.fieldname)
|
||||
}
|
||||
|
||||
function addField(field) {
|
||||
if (!field) return
|
||||
let rows = allFields.value || []
|
||||
rows.push(field)
|
||||
allFields.value = rows.map((row) => row.fieldname)
|
||||
}
|
||||
|
||||
function removeField(field) {
|
||||
let rows = allFields.value
|
||||
rows = rows.filter((row) => row.fieldname !== field.fieldname)
|
||||
allFields.value = rows.map((row) => row.fieldname)
|
||||
}
|
||||
|
||||
function apply() {
|
||||
nextTick(() => {
|
||||
showDialog.value = false
|
||||
emit('update', {
|
||||
column_field: columnField.value.fieldname,
|
||||
title_field: titleField.value.fieldname,
|
||||
kanban_fields: allFields.value.map((row) => row.fieldname),
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
253
frontend/src/components/Kanban/KanbanView.vue
Normal file
253
frontend/src/components/Kanban/KanbanView.vue
Normal file
@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<Draggable
|
||||
v-if="columns"
|
||||
:list="columns"
|
||||
item-key="column"
|
||||
@end="updateColumn"
|
||||
class="flex sm:mx-2.5 mx-2 pb-3.5 overflow-x-auto"
|
||||
>
|
||||
<template #item="{ element: column }">
|
||||
<div
|
||||
v-if="!column.delete"
|
||||
class="flex flex-col gap-2.5 min-w-72 w-72 hover:bg-gray-100 rounded-lg p-2.5"
|
||||
>
|
||||
<div class="flex gap-2 items-center group justify-between">
|
||||
<div class="flex items-center text-base">
|
||||
<NestedPopover>
|
||||
<template #target>
|
||||
<Button variant="ghost" size="sm" class="hover:!bg-gray-100">
|
||||
<IndicatorIcon
|
||||
:class="colorClasses(column.column.color, true)"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
<template #body="{ close }">
|
||||
<div
|
||||
class="flex flex-col gap-3 px-3 py-2.5 rounded-lg border border-gray-100 bg-white shadow-xl"
|
||||
>
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
:class="colorClasses(color)"
|
||||
variant="ghost"
|
||||
v-for="color in colors"
|
||||
:key="color"
|
||||
@click="() => (column.column.color = color)"
|
||||
>
|
||||
<IndicatorIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-row-reverse">
|
||||
<Button
|
||||
variant="solid"
|
||||
:label="__('Apply')"
|
||||
@click="updateColumn"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</NestedPopover>
|
||||
<div>{{ column.column.name }}</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<Dropdown :options="actions(column)">
|
||||
<template #default>
|
||||
<Button
|
||||
class="hidden group-hover:flex"
|
||||
icon="more-horizontal"
|
||||
variant="ghost"
|
||||
/>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="ghost"
|
||||
@click="options.onNewClick(column)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex flex-col gap-2 h-full">
|
||||
<Draggable
|
||||
:list="column.data"
|
||||
group="fields"
|
||||
item-key="name"
|
||||
class="flex flex-col gap-3.5 flex-1"
|
||||
@end="updateColumn"
|
||||
:data-column="column.column.name"
|
||||
>
|
||||
<template #item="{ element: fields }">
|
||||
<component
|
||||
:is="options.getRoute ? 'router-link' : 'div'"
|
||||
class="pt-3 px-3.5 pb-2.5 rounded-lg border bg-white text-base flex flex-col"
|
||||
:data-name="fields.name"
|
||||
v-bind="{
|
||||
to: options.getRoute ? options.getRoute(fields) : undefined,
|
||||
onClick: options.onClick
|
||||
? () => options.onClick(fields)
|
||||
: undefined,
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
name="title"
|
||||
v-bind="{ fields, titleField, itemName: fields.name }"
|
||||
>
|
||||
<div class="h-5 flex items-center">
|
||||
<div v-if="fields[titleField]">
|
||||
{{ fields[titleField] }}
|
||||
</div>
|
||||
<div class="text-gray-500" v-else>{{ __('No Title') }}</div>
|
||||
</div>
|
||||
</slot>
|
||||
<div class="border-b h-px my-2.5" />
|
||||
|
||||
<div class="flex flex-col gap-3.5">
|
||||
<template v-for="value in column.fields" :key="value">
|
||||
<slot
|
||||
name="fields"
|
||||
v-bind="{
|
||||
fields,
|
||||
fieldName: value,
|
||||
itemName: fields.name,
|
||||
}"
|
||||
>
|
||||
<div v-if="fields[value]" class="truncate">
|
||||
{{ fields[value] }}
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
</div>
|
||||
<div class="border-b h-px mt-2.5 mb-2" />
|
||||
<slot name="actions" v-bind="{ itemName: fields.name }">
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<div></div>
|
||||
<Button icon="plus" variant="ghost" @click.stop.prevent />
|
||||
</div>
|
||||
</slot>
|
||||
</component>
|
||||
</template>
|
||||
</Draggable>
|
||||
<div
|
||||
v-if="column.column.count < column.column.all_count"
|
||||
class="flex items-center justify-center"
|
||||
>
|
||||
<Button
|
||||
:label="__('Load More')"
|
||||
@click="emit('loadMore', column.column.name)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
</template>
|
||||
<script setup>
|
||||
import NestedPopover from '@/components/NestedPopover.vue'
|
||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
import { Dropdown } from 'frappe-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
getRoute: null,
|
||||
onClick: null,
|
||||
onNewClick: null,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update', 'loadMore'])
|
||||
|
||||
const kanban = defineModel()
|
||||
|
||||
const titleField = computed(() => {
|
||||
return kanban.value?.data?.title_field
|
||||
})
|
||||
|
||||
const columns = computed(() => {
|
||||
if (!kanban.value?.data?.data || kanban.value.data.view_type != 'kanban')
|
||||
return []
|
||||
let _columns = kanban.value.data.data
|
||||
|
||||
let has_color = _columns.some((column) => column.column?.color)
|
||||
if (!has_color) {
|
||||
_columns.forEach((column, i) => {
|
||||
column.column['color'] = colors[i % colors.length]
|
||||
})
|
||||
}
|
||||
return _columns
|
||||
})
|
||||
|
||||
function actions(column) {
|
||||
return [
|
||||
{
|
||||
group: __('Options'),
|
||||
hideLabel: true,
|
||||
items: [
|
||||
{
|
||||
label: __('Delete'),
|
||||
icon: 'trash-2',
|
||||
onClick: () => {
|
||||
column['delete'] = true
|
||||
updateColumn()
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function updateColumn({ item, from, to }) {
|
||||
let toColumn = to?.dataset.column
|
||||
let fromColumn = from?.dataset.column
|
||||
let itemName = item?.dataset.name
|
||||
|
||||
let _columns = []
|
||||
columns.value.forEach((col) => {
|
||||
if (col.delete) return
|
||||
col.column['order'] = col.data.map((d) => d.name)
|
||||
if (col.column.page_length) {
|
||||
delete col.column.page_length
|
||||
}
|
||||
_columns.push(col.column)
|
||||
})
|
||||
|
||||
let data = { kanban_columns: _columns }
|
||||
|
||||
if (toColumn != fromColumn) {
|
||||
data = { item: itemName, to: toColumn, kanban_columns: _columns }
|
||||
}
|
||||
|
||||
emit('update', data)
|
||||
}
|
||||
|
||||
function colorClasses(color, onlyIcon = false) {
|
||||
let textColor = `!text-${color}-600`
|
||||
if (color == 'black') {
|
||||
textColor = '!text-gray-900'
|
||||
} else if (['gray', 'green'].includes(color)) {
|
||||
textColor = `!text-${color}-700`
|
||||
}
|
||||
|
||||
let bgColor = `!bg-${color}-100 hover:!bg-${color}-200 active:!bg-${color}-300`
|
||||
|
||||
return [textColor, onlyIcon ? '' : bgColor]
|
||||
}
|
||||
|
||||
const colors = [
|
||||
'gray',
|
||||
'blue',
|
||||
'green',
|
||||
'red',
|
||||
'pink',
|
||||
'orange',
|
||||
'amber',
|
||||
'yellow',
|
||||
'cyan',
|
||||
'teal',
|
||||
'violet',
|
||||
'purple',
|
||||
'black',
|
||||
]
|
||||
</script>
|
||||
@ -46,6 +46,10 @@ import { Switch, createResource } from 'frappe-ui'
|
||||
import { computed, ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
defaults: Object,
|
||||
})
|
||||
|
||||
const { getUser } = usersStore()
|
||||
const { getDealStatus, statusOptions } = statusesStore()
|
||||
|
||||
@ -194,6 +198,7 @@ function createDeal() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
Object.assign(deal, props.defaults)
|
||||
if (!deal.deal_owner) {
|
||||
deal.deal_owner = getUser().email
|
||||
}
|
||||
|
||||
@ -31,6 +31,10 @@ import { createResource } from 'frappe-ui'
|
||||
import { computed, onMounted, ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
defaults: Object,
|
||||
})
|
||||
|
||||
const { getUser } = usersStore()
|
||||
const { getLeadStatus, statusOptions } = statusesStore()
|
||||
|
||||
@ -146,6 +150,7 @@ function createNewLead() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
Object.assign(lead, props.defaults)
|
||||
if (!lead.lead_owner) {
|
||||
lead.lead_owner = getUser().email
|
||||
}
|
||||
|
||||
@ -119,7 +119,7 @@ import Link from '@/components/Controls/Link.vue'
|
||||
import { taskStatusOptions, taskPriorityOptions } from '@/utils'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { TextEditor, Dropdown, Tooltip, call, DateTimePicker } from 'frappe-ui'
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { ref, watch, nextTick, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
@ -205,20 +205,23 @@ async function updateTask() {
|
||||
show.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => show.value,
|
||||
(value) => {
|
||||
if (!value) return
|
||||
editMode.value = false
|
||||
nextTick(() => {
|
||||
title.value.el.focus()
|
||||
_task.value = { ...props.task }
|
||||
if (_task.value.title) {
|
||||
editMode.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
function render() {
|
||||
editMode.value = false
|
||||
nextTick(() => {
|
||||
title.value.el.focus()
|
||||
_task.value = { ...props.task }
|
||||
if (_task.value.title) {
|
||||
editMode.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => render())
|
||||
|
||||
watch(show, (value) => {
|
||||
if (!value) return
|
||||
render()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
shape="circle"
|
||||
:image="avatars[0].image"
|
||||
:label="avatars[0].label"
|
||||
size="sm"
|
||||
:size="size"
|
||||
/>
|
||||
<div class="truncate">{{ avatars[0].label }}</div>
|
||||
</div>
|
||||
|
||||
@ -60,13 +60,20 @@
|
||||
|
||||
<div class="flex gap-2">
|
||||
<SortBy
|
||||
v-if="route.params.viewType !== 'kanban'"
|
||||
v-model="list"
|
||||
:doctype="doctype"
|
||||
@update="updateSort"
|
||||
:hideLabel="isMobileView"
|
||||
/>
|
||||
<KanbanSettings
|
||||
v-if="route.params.viewType === 'kanban'"
|
||||
v-model="list"
|
||||
:doctype="doctype"
|
||||
@update="updateKanbanSettings"
|
||||
/>
|
||||
<ColumnSettings
|
||||
v-if="!options.hideColumnsButton"
|
||||
v-else-if="!options.hideColumnsButton"
|
||||
v-model="list"
|
||||
:doctype="doctype"
|
||||
:hideLabel="isMobileView"
|
||||
@ -155,9 +162,20 @@
|
||||
:default_filters="filters"
|
||||
@update="updateFilter"
|
||||
/>
|
||||
<SortBy v-model="list" :doctype="doctype" @update="updateSort" />
|
||||
<SortBy
|
||||
v-if="route.params.viewType !== 'kanban'"
|
||||
v-model="list"
|
||||
:doctype="doctype"
|
||||
@update="updateSort"
|
||||
/>
|
||||
<KanbanSettings
|
||||
v-if="route.params.viewType === 'kanban'"
|
||||
v-model="list"
|
||||
:doctype="doctype"
|
||||
@update="updateKanbanSettings"
|
||||
/>
|
||||
<ColumnSettings
|
||||
v-if="!options.hideColumnsButton"
|
||||
v-else-if="!options.hideColumnsButton"
|
||||
v-model="list"
|
||||
:doctype="doctype"
|
||||
@update="(isDefault) => updateColumns(isDefault)"
|
||||
@ -249,6 +267,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||
import KanbanIcon from '@/components/Icons/KanbanIcon.vue'
|
||||
import QuickFilterField from '@/components/QuickFilterField.vue'
|
||||
import RefreshIcon from '@/components/Icons/RefreshIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
@ -261,6 +280,7 @@ import Filter from '@/components/Filter.vue'
|
||||
import GroupBy from '@/components/GroupBy.vue'
|
||||
import FadedScrollableDiv from '@/components/FadedScrollableDiv.vue'
|
||||
import ColumnSettings from '@/components/ColumnSettings.vue'
|
||||
import KanbanSettings from '@/components/Kanban/KanbanSettings.vue'
|
||||
import { globalStore } from '@/stores/global'
|
||||
import { viewsStore } from '@/stores/views'
|
||||
import { usersStore } from '@/stores/users'
|
||||
@ -319,6 +339,10 @@ function getViewType() {
|
||||
label: __('Group By View'),
|
||||
icon: markRaw(DetailsIcon),
|
||||
},
|
||||
kanban: {
|
||||
label: __('Kanban View'),
|
||||
icon: markRaw(KanbanIcon),
|
||||
},
|
||||
}
|
||||
|
||||
return types[viewType]
|
||||
@ -340,6 +364,10 @@ const view = ref({
|
||||
icon: '',
|
||||
filters: {},
|
||||
order_by: 'modified desc',
|
||||
column_field: 'status',
|
||||
title_field: '',
|
||||
kanban_columns: '',
|
||||
kanban_fields: '',
|
||||
columns: '',
|
||||
rows: '',
|
||||
load_default_columns: false,
|
||||
@ -367,64 +395,61 @@ watch(updatedPageCount, (value) => {
|
||||
|
||||
function getParams() {
|
||||
let _view = getView(route.query.view, route.params.viewType, props.doctype)
|
||||
const view_name = _view?.name || ''
|
||||
const view_type = _view?.type || route.params.viewType || 'list'
|
||||
const filters = (_view?.filters && JSON.parse(_view.filters)) || {}
|
||||
const order_by = _view?.order_by || 'modified desc'
|
||||
const group_by_field = _view?.group_by_field || 'owner'
|
||||
const columns = _view?.columns || ''
|
||||
const rows = _view?.rows || ''
|
||||
const column_field = _view?.column_field || 'status'
|
||||
const title_field = _view?.title_field || ''
|
||||
const kanban_columns = _view?.kanban_columns || ''
|
||||
const kanban_fields = _view?.kanban_fields || ''
|
||||
|
||||
if (_view) {
|
||||
view.value = {
|
||||
name: _view.name,
|
||||
label: _view.label,
|
||||
type: _view.type || 'list',
|
||||
icon: _view.icon,
|
||||
filters: _view.filters,
|
||||
order_by: _view.order_by,
|
||||
group_by_field: _view.group_by_field,
|
||||
columns: _view.columns,
|
||||
rows: _view.rows,
|
||||
route_name: _view.route_name,
|
||||
load_default_columns: _view.row,
|
||||
pinned: _view.pinned,
|
||||
public: _view.public,
|
||||
}
|
||||
} else {
|
||||
view.value = {
|
||||
name: '',
|
||||
label: getViewType().label,
|
||||
type: route.params.viewType || 'list',
|
||||
icon: '',
|
||||
filters: {},
|
||||
order_by: 'modified desc',
|
||||
group_by_field: 'owner',
|
||||
columns: '',
|
||||
rows: '',
|
||||
route_name: route.name,
|
||||
load_default_columns: true,
|
||||
pinned: false,
|
||||
public: false,
|
||||
}
|
||||
view.value = {
|
||||
name: view_name,
|
||||
label: _view?.label || getViewType().label,
|
||||
type: view_type,
|
||||
icon: _view?.icon || '',
|
||||
filters: filters,
|
||||
order_by: order_by,
|
||||
group_by_field: group_by_field,
|
||||
column_field: column_field,
|
||||
title_field: title_field,
|
||||
kanban_columns: kanban_columns,
|
||||
kanban_fields: kanban_fields,
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
route_name: _view?.route_name || route.name,
|
||||
load_default_columns: _view?.row || true,
|
||||
pinned: _view?.pinned || false,
|
||||
public: _view?.public || false,
|
||||
}
|
||||
|
||||
return {
|
||||
doctype: props.doctype,
|
||||
filters: filters,
|
||||
order_by: order_by,
|
||||
default_filters: props.filters,
|
||||
view: {
|
||||
custom_view_name: view_name,
|
||||
view_type: view_type,
|
||||
group_by_field: group_by_field,
|
||||
},
|
||||
column_field: column_field,
|
||||
title_field: title_field,
|
||||
kanban_columns: kanban_columns,
|
||||
kanban_fields: kanban_fields,
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
page_length: pageLength.value,
|
||||
page_length_count: pageLengthCount.value,
|
||||
view: {
|
||||
custom_view_name: _view?.name || '',
|
||||
view_type: _view?.type || route.params.viewType || 'list',
|
||||
group_by_field: _view?.group_by_field || 'owner',
|
||||
},
|
||||
default_filters: props.filters,
|
||||
}
|
||||
}
|
||||
|
||||
list.value = createResource({
|
||||
url: 'crm.api.doc.get_list_data',
|
||||
url: 'crm.api.doc.get_data',
|
||||
params: getParams(),
|
||||
cache: [props.doctype, route.query.view, route.params.viewType],
|
||||
onSuccess(data) {
|
||||
@ -434,16 +459,20 @@ list.value = createResource({
|
||||
doctype: props.doctype,
|
||||
filters: params.filters,
|
||||
order_by: params.order_by,
|
||||
page_length: params.page_length,
|
||||
page_length_count: params.page_length_count,
|
||||
columns: data.columns,
|
||||
rows: data.rows,
|
||||
default_filters: props.filters,
|
||||
view: {
|
||||
custom_view_name: cv?.name || '',
|
||||
view_type: cv?.type || route.params.viewType || 'list',
|
||||
group_by_field: params?.view?.group_by_field || 'owner',
|
||||
},
|
||||
default_filters: props.filters,
|
||||
column_field: params.column_field,
|
||||
title_field: params.title_field,
|
||||
kanban_columns: params.kanban_columns,
|
||||
kanban_fields: params.kanban_fields,
|
||||
columns: data.columns,
|
||||
rows: data.rows,
|
||||
page_length: params.page_length,
|
||||
page_length_count: params.page_length_count,
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -499,6 +528,16 @@ if (allowedViews.includes('group_by')) {
|
||||
},
|
||||
})
|
||||
}
|
||||
if (allowedViews.includes('kanban')) {
|
||||
defaultViews.push({
|
||||
label: __(props.options?.defaultViewName) || __('Kanban View'),
|
||||
icon: markRaw(KanbanIcon),
|
||||
onClick() {
|
||||
viewUpdated.value = false
|
||||
router.push({ name: route.name, params: { viewType: 'kanban' } })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function getIcon(icon, type) {
|
||||
if (isEmoji(icon)) {
|
||||
@ -538,7 +577,7 @@ const viewsDropdownOptions = computed(() => {
|
||||
})
|
||||
let publicViews = list.value.data.views.filter((v) => v.public)
|
||||
let savedViews = list.value.data.views.filter(
|
||||
(v) => !v.pinned && !v.public && !v.is_default
|
||||
(v) => !v.pinned && !v.public && !v.is_default,
|
||||
)
|
||||
let pinnedViews = list.value.data.views.filter((v) => v.pinned)
|
||||
|
||||
@ -576,7 +615,7 @@ const quickFilterList = computed(() => {
|
||||
if (Array.isArray(value)) {
|
||||
if (
|
||||
(['Check', 'Select', 'Link', 'Date', 'Datetime'].includes(
|
||||
filter.type
|
||||
filter.type,
|
||||
) &&
|
||||
value[0]?.toLowerCase() == 'like') ||
|
||||
value[0]?.toLowerCase() != 'like'
|
||||
@ -695,6 +734,87 @@ function updateColumns(obj) {
|
||||
}
|
||||
}
|
||||
|
||||
async function updateKanbanSettings(data) {
|
||||
if (data.item && data.to) {
|
||||
await call('frappe.client.set_value', {
|
||||
doctype: props.doctype,
|
||||
name: data.item,
|
||||
fieldname: view.value.column_field,
|
||||
value: data.to,
|
||||
})
|
||||
}
|
||||
let isDirty = viewUpdated.value
|
||||
|
||||
viewUpdated.value = true
|
||||
if (!defaultParams.value) {
|
||||
defaultParams.value = getParams()
|
||||
}
|
||||
list.value.params = defaultParams.value
|
||||
if (data.kanban_columns) {
|
||||
list.value.params.kanban_columns = data.kanban_columns
|
||||
view.value.kanban_columns = data.kanban_columns
|
||||
}
|
||||
if (data.kanban_fields) {
|
||||
list.value.params.kanban_fields = data.kanban_fields
|
||||
view.value.kanban_fields = data.kanban_fields
|
||||
}
|
||||
if (data.column_field && data.column_field != view.value.column_field) {
|
||||
list.value.params.column_field = data.column_field
|
||||
view.value.column_field = data.column_field
|
||||
list.value.params.kanban_columns = ''
|
||||
view.value.kanban_columns = ''
|
||||
}
|
||||
if (data.title_field && data.title_field != view.value.title_field) {
|
||||
list.value.params.title_field = data.title_field
|
||||
view.value.title_field = data.title_field
|
||||
}
|
||||
|
||||
list.value.reload()
|
||||
|
||||
if (!route.query.view) {
|
||||
create_or_update_default_view()
|
||||
} else if (!data.column_field) {
|
||||
if (isDirty) {
|
||||
$dialog({
|
||||
title: __('Unsaved Changes'),
|
||||
message: __('You have unsaved changes. Do you want to save them?'),
|
||||
variant: 'danger',
|
||||
actions: [
|
||||
{
|
||||
label: __('Update'),
|
||||
variant: 'solid',
|
||||
onClick: (close) => {
|
||||
update_custom_view()
|
||||
close()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
} else {
|
||||
update_custom_view()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadMoreKanban(columnName) {
|
||||
let columns = list.value.params.kanban_columns
|
||||
|
||||
if (typeof columns === 'string') {
|
||||
columns = JSON.parse(columns)
|
||||
}
|
||||
|
||||
let column = columns.find((c) => c.name == columnName)
|
||||
|
||||
if (!column.page_length) {
|
||||
column.page_length = 40
|
||||
} else {
|
||||
column.page_length += 20
|
||||
}
|
||||
list.value.params.kanban_columns = columns
|
||||
view.value.kanban_columns = columns
|
||||
list.value.reload()
|
||||
}
|
||||
|
||||
function create_or_update_default_view() {
|
||||
if (route.query.view) return
|
||||
view.value.doctype = props.doctype
|
||||
@ -702,7 +822,7 @@ function create_or_update_default_view() {
|
||||
'crm.fcrm.doctype.crm_view_settings.crm_view_settings.create_or_update_default_view',
|
||||
{
|
||||
view: view.value,
|
||||
}
|
||||
},
|
||||
).then(() => {
|
||||
reloadView()
|
||||
view.value = {
|
||||
@ -712,7 +832,11 @@ function create_or_update_default_view() {
|
||||
name: view.value.name,
|
||||
filters: defaultParams.value.filters,
|
||||
order_by: defaultParams.value.order_by,
|
||||
group_by_field: defaultParams.value.view.group_by_field,
|
||||
group_by_field: defaultParams.value.view?.group_by_field,
|
||||
column_field: defaultParams.value.column_field,
|
||||
title_field: defaultParams.value.title_field,
|
||||
kanban_columns: defaultParams.value.kanban_columns,
|
||||
kanban_fields: defaultParams.value.kanban_fields,
|
||||
columns: defaultParams.value.columns,
|
||||
rows: defaultParams.value.rows,
|
||||
route_name: route.name,
|
||||
@ -722,6 +846,31 @@ function create_or_update_default_view() {
|
||||
})
|
||||
}
|
||||
|
||||
function update_custom_view() {
|
||||
viewUpdated.value = false
|
||||
view.value = {
|
||||
doctype: props.doctype,
|
||||
label: view.value.label,
|
||||
type: view.value.type || 'list',
|
||||
icon: view.value.icon,
|
||||
name: view.value.name,
|
||||
filters: defaultParams.value.filters,
|
||||
order_by: defaultParams.value.order_by,
|
||||
group_by_field: defaultParams.value.view.group_by_field,
|
||||
column_field: defaultParams.value.column_field,
|
||||
title_field: defaultParams.value.title_field,
|
||||
kanban_columns: defaultParams.value.kanban_columns,
|
||||
kanban_fields: defaultParams.value.kanban_fields,
|
||||
columns: defaultParams.value.columns,
|
||||
rows: defaultParams.value.rows,
|
||||
route_name: route.name,
|
||||
load_default_columns: view.value.load_default_columns,
|
||||
}
|
||||
call('crm.fcrm.doctype.crm_view_settings.crm_view_settings.update', {
|
||||
view: view.value,
|
||||
}).then(() => reloadView())
|
||||
}
|
||||
|
||||
function updatePageLength(value, loadMore = false) {
|
||||
if (!defaultParams.value) {
|
||||
defaultParams.value = getParams()
|
||||
@ -818,7 +967,7 @@ const viewModalObj = ref({})
|
||||
function duplicateView() {
|
||||
let label =
|
||||
__(
|
||||
getView(route.query.view, route.params.viewType, props.doctype)?.label
|
||||
getView(route.query.view, route.params.viewType, props.doctype)?.label,
|
||||
) || getViewType().label
|
||||
view.value.name = ''
|
||||
view.value.label = label + __(' (New)')
|
||||
@ -879,6 +1028,10 @@ function saveView() {
|
||||
filters: defaultParams.value.filters,
|
||||
order_by: defaultParams.value.order_by,
|
||||
group_by_field: defaultParams.value.view.group_by_field,
|
||||
column_field: defaultParams.value.column_field,
|
||||
title_field: defaultParams.value.title_field,
|
||||
kanban_columns: defaultParams.value.kanban_columns,
|
||||
kanban_fields: defaultParams.value.kanban_fields,
|
||||
columns: defaultParams.value.columns,
|
||||
rows: defaultParams.value.rows,
|
||||
route_name: route.name,
|
||||
@ -939,7 +1092,13 @@ function likeDoc({ name, liked }) {
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ applyFilter, applyLikeFilter, likeDoc })
|
||||
defineExpose({
|
||||
applyFilter,
|
||||
applyLikeFilter,
|
||||
likeDoc,
|
||||
updateKanbanSettings,
|
||||
loadMoreKanban,
|
||||
})
|
||||
|
||||
// Watchers
|
||||
watch(
|
||||
@ -948,7 +1107,7 @@ watch(
|
||||
if (_.isEqual(value, old_value)) return
|
||||
reload()
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
watch([() => route, () => route.params.viewType], (value, old_value) => {
|
||||
|
||||
@ -76,7 +76,11 @@ const updatedPageCount = ref(20)
|
||||
const viewControls = ref(null)
|
||||
|
||||
const rows = computed(() => {
|
||||
if (!callLogs.value?.data?.data) return []
|
||||
if (
|
||||
!callLogs.value?.data?.data ||
|
||||
!['list', 'group_by'].includes(callLogs.value.data.view_type)
|
||||
)
|
||||
return []
|
||||
return callLogs.value?.data.data.map((callLog) => {
|
||||
let _rows = {}
|
||||
callLogs.value?.data.rows.forEach((row) => {
|
||||
|
||||
@ -82,7 +82,7 @@ const showContactModal = ref(false)
|
||||
|
||||
const currentContact = computed(() => {
|
||||
return contacts.value?.data?.data?.find(
|
||||
(contact) => contact.name === route.params.contactId
|
||||
(contact) => contact.name === route.params.contactId,
|
||||
)
|
||||
})
|
||||
|
||||
@ -109,7 +109,11 @@ const updatedPageCount = ref(20)
|
||||
const viewControls = ref(null)
|
||||
|
||||
const rows = computed(() => {
|
||||
if (!contacts.value?.data?.data) return []
|
||||
if (
|
||||
!contacts.value?.data?.data ||
|
||||
!['list', 'group_by'].includes(contacts.value.data.view_type)
|
||||
)
|
||||
return []
|
||||
return contacts.value?.data.data.map((contact) => {
|
||||
let _rows = {}
|
||||
contacts.value?.data.rows.forEach((row) => {
|
||||
|
||||
@ -25,12 +25,184 @@
|
||||
v-model:updatedPageCount="updatedPageCount"
|
||||
doctype="CRM Deal"
|
||||
:options="{
|
||||
allowedViews: ['list', 'group_by'],
|
||||
allowedViews: ['list', 'group_by', 'kanban'],
|
||||
}"
|
||||
/>
|
||||
<KanbanView
|
||||
v-if="route.params.viewType == 'kanban'"
|
||||
v-model="deals"
|
||||
:options="{
|
||||
getRoute: (row) => ({ name: 'Deal', params: { dealId: row.name } }),
|
||||
onNewClick: (column) => onNewClick(column),
|
||||
}"
|
||||
@update="(data) => viewControls.updateKanbanSettings(data)"
|
||||
@loadMore="(columnName) => viewControls.loadMoreKanban(columnName)"
|
||||
>
|
||||
<template #title="{ titleField, itemName }">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div v-if="titleField === 'status'">
|
||||
<IndicatorIcon :class="getRow(itemName, titleField).color" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
titleField === 'organization' && getRow(itemName, titleField).label
|
||||
"
|
||||
>
|
||||
<Avatar
|
||||
class="flex items-center"
|
||||
:image="getRow(itemName, titleField).logo"
|
||||
:label="getRow(itemName, titleField).label"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
titleField === 'deal_owner' &&
|
||||
getRow(itemName, titleField).full_name
|
||||
"
|
||||
>
|
||||
<Avatar
|
||||
class="flex items-center"
|
||||
:image="getRow(itemName, titleField).user_image"
|
||||
:label="getRow(itemName, titleField).full_name"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
[
|
||||
'modified',
|
||||
'creation',
|
||||
'first_response_time',
|
||||
'first_responded_on',
|
||||
'response_by',
|
||||
].includes(titleField)
|
||||
"
|
||||
class="truncate text-base"
|
||||
>
|
||||
<Tooltip :text="getRow(itemName, titleField).label">
|
||||
<div>{{ getRow(itemName, titleField).timeAgo }}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div v-else-if="titleField === 'sla_status'" class="truncate text-base">
|
||||
<Badge
|
||||
v-if="getRow(itemName, titleField).value"
|
||||
:variant="'subtle'"
|
||||
:theme="getRow(itemName, titleField).color"
|
||||
size="md"
|
||||
:label="getRow(itemName, titleField).value"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="getRow(itemName, titleField).label"
|
||||
class="truncate text-base"
|
||||
>
|
||||
{{ getRow(itemName, titleField).label }}
|
||||
</div>
|
||||
<div class="text-gray-500" v-else>{{ __('No Title') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #fields="{ fieldName, itemName }">
|
||||
<div
|
||||
v-if="getRow(itemName, fieldName).label"
|
||||
class="truncate flex items-center gap-2"
|
||||
>
|
||||
<div v-if="fieldName === 'status'">
|
||||
<IndicatorIcon :class="getRow(itemName, fieldName).color" />
|
||||
</div>
|
||||
<div v-else-if="fieldName === 'organization'">
|
||||
<Avatar
|
||||
v-if="getRow(itemName, fieldName).label"
|
||||
class="flex items-center"
|
||||
:image="getRow(itemName, fieldName).logo"
|
||||
:label="getRow(itemName, fieldName).label"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="fieldName === 'deal_owner'">
|
||||
<Avatar
|
||||
v-if="getRow(itemName, fieldName).full_name"
|
||||
class="flex items-center"
|
||||
:image="getRow(itemName, fieldName).user_image"
|
||||
:label="getRow(itemName, fieldName).full_name"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
[
|
||||
'modified',
|
||||
'creation',
|
||||
'first_response_time',
|
||||
'first_responded_on',
|
||||
'response_by',
|
||||
].includes(fieldName)
|
||||
"
|
||||
class="truncate text-base"
|
||||
>
|
||||
<Tooltip :text="getRow(itemName, fieldName).label">
|
||||
<div>{{ getRow(itemName, fieldName).timeAgo }}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div v-else-if="fieldName === 'sla_status'" class="truncate text-base">
|
||||
<Badge
|
||||
v-if="getRow(itemName, fieldName).value"
|
||||
:variant="'subtle'"
|
||||
:theme="getRow(itemName, fieldName).color"
|
||||
size="md"
|
||||
:label="getRow(itemName, fieldName).value"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="fieldName === '_assign'" class="flex items-center">
|
||||
<MultipleAvatar
|
||||
:avatars="getRow(itemName, fieldName).label"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="truncate text-base">
|
||||
{{ getRow(itemName, fieldName).label }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions="{ itemName }">
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<div class="text-gray-600 flex items-center gap-1.5">
|
||||
<EmailAtIcon class="h-4 w-4" />
|
||||
<span v-if="getRow(itemName, '_email_count').label">
|
||||
{{ getRow(itemName, '_email_count').label }}
|
||||
</span>
|
||||
<span class="text-3xl leading-[0]"> · </span>
|
||||
<NoteIcon class="h-4 w-4" />
|
||||
<span v-if="getRow(itemName, '_note_count').label">
|
||||
{{ getRow(itemName, '_note_count').label }}
|
||||
</span>
|
||||
<span class="text-3xl leading-[0]"> · </span>
|
||||
<TaskIcon class="h-4 w-4" />
|
||||
<span v-if="getRow(itemName, '_task_count').label">
|
||||
{{ getRow(itemName, '_task_count').label }}
|
||||
</span>
|
||||
<span class="text-3xl leading-[0]"> · </span>
|
||||
<CommentIcon class="h-4 w-4" />
|
||||
<span v-if="getRow(itemName, '_comment_count').label">
|
||||
{{ getRow(itemName, '_comment_count').label }}
|
||||
</span>
|
||||
</div>
|
||||
<Dropdown
|
||||
class="flex items-center gap-2"
|
||||
:options="actions(itemName)"
|
||||
variant="ghost"
|
||||
@click.stop.prevent
|
||||
>
|
||||
<Button icon="plus" variant="ghost" />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</KanbanView>
|
||||
<DealsListView
|
||||
ref="dealsListView"
|
||||
v-if="deals.data && rows.length"
|
||||
v-else-if="deals.data && rows.length"
|
||||
v-model="deals.data.page_length_count"
|
||||
v-model:list="deals"
|
||||
:rows="rows"
|
||||
@ -59,20 +231,47 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<DealModal v-model="showDealModal" />
|
||||
<DealModal
|
||||
v-if="showDealModal"
|
||||
v-model="showDealModal"
|
||||
:defaults="defaults"
|
||||
/>
|
||||
<NoteModal
|
||||
v-model="showNoteModal"
|
||||
:note="note"
|
||||
doctype="CRM Deal"
|
||||
:doc="docname"
|
||||
/>
|
||||
<TaskModal
|
||||
v-model="showTaskModal"
|
||||
:task="task"
|
||||
doctype="CRM Deal"
|
||||
:doc="docname"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||
import CustomActions from '@/components/CustomActions.vue'
|
||||
import EmailAtIcon from '@/components/Icons/EmailAtIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
||||
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import DealsListView from '@/components/ListViews/DealsListView.vue'
|
||||
import KanbanView from '@/components/Kanban/KanbanView.vue'
|
||||
import DealModal from '@/components/Modals/DealModal.vue'
|
||||
import NoteModal from '@/components/Modals/NoteModal.vue'
|
||||
import TaskModal from '@/components/Modals/TaskModal.vue'
|
||||
import ViewControls from '@/components/ViewControls.vue'
|
||||
import { globalStore } from '@/stores/global'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { organizationsStore } from '@/stores/organizations'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import { callEnabled } from '@/composables/settings'
|
||||
import {
|
||||
dateFormat,
|
||||
dateTooltipFormat,
|
||||
@ -80,12 +279,13 @@ import {
|
||||
formatNumberIntoCurrency,
|
||||
formatTime,
|
||||
} from '@/utils'
|
||||
import { Breadcrumbs } from 'frappe-ui'
|
||||
import { Breadcrumbs, Tooltip, Avatar, Dropdown } from 'frappe-ui'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref, computed, h } from 'vue'
|
||||
import { ref, reactive, computed, h } from 'vue'
|
||||
|
||||
const breadcrumbs = [{ label: __('Deals'), route: { name: 'Deals' } }]
|
||||
|
||||
const { makeCall } = globalStore()
|
||||
const { getUser } = usersStore()
|
||||
const { getOrganization } = organizationsStore()
|
||||
const { getDealStatus } = statusesStore()
|
||||
@ -95,6 +295,8 @@ const route = useRoute()
|
||||
const dealsListView = ref(null)
|
||||
const showDealModal = ref(false)
|
||||
|
||||
const defaults = reactive({})
|
||||
|
||||
// deals data is loaded in the ViewControls component
|
||||
const deals = ref({})
|
||||
const loadMore = ref(1)
|
||||
@ -102,15 +304,27 @@ const triggerResize = ref(1)
|
||||
const updatedPageCount = ref(20)
|
||||
const viewControls = ref(null)
|
||||
|
||||
function getRow(name, field) {
|
||||
function getValue(value) {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
return { label: value }
|
||||
}
|
||||
return getValue(rows.value?.find((row) => row.name == name)[field])
|
||||
}
|
||||
|
||||
// Rows
|
||||
const rows = computed(() => {
|
||||
if (!deals.value?.data?.data) return []
|
||||
if (route.params.viewType === 'group_by') {
|
||||
if (deals.value.data.view_type === 'group_by') {
|
||||
if (!deals.value?.data.group_by_field?.name) return []
|
||||
return getGroupedByRows(
|
||||
deals.value?.data.data,
|
||||
deals.value?.data.group_by_field,
|
||||
)
|
||||
} else if (deals.value.data.view_type === 'kanban') {
|
||||
return getKanbanRows(deals.value.data.data)
|
||||
} else {
|
||||
return parseRows(deals.value?.data.data)
|
||||
}
|
||||
@ -146,6 +360,16 @@ function getGroupedByRows(listRows, groupByField) {
|
||||
return groupedRows || listRows
|
||||
}
|
||||
|
||||
function getKanbanRows(data) {
|
||||
let _rows = []
|
||||
data.forEach((column) => {
|
||||
column.data?.forEach((row) => {
|
||||
_rows.push(row)
|
||||
})
|
||||
})
|
||||
return parseRows(_rows)
|
||||
}
|
||||
|
||||
function parseRows(rows) {
|
||||
return rows.map((deal) => {
|
||||
let _rows = {}
|
||||
@ -224,7 +448,73 @@ function parseRows(rows) {
|
||||
}
|
||||
}
|
||||
})
|
||||
_rows['_email_count'] = deal._email_count
|
||||
_rows['_note_count'] = deal._note_count
|
||||
_rows['_task_count'] = deal._task_count
|
||||
_rows['_comment_count'] = deal._comment_count
|
||||
return _rows
|
||||
})
|
||||
}
|
||||
|
||||
function onNewClick(column) {
|
||||
let column_field = deals.value.params.column_field
|
||||
|
||||
if (column_field) {
|
||||
defaults[column_field] = column.column.name
|
||||
}
|
||||
|
||||
showDealModal.value = true
|
||||
}
|
||||
|
||||
function actions(itemName) {
|
||||
let mobile_no = getRow(itemName, 'mobile_no')?.label || ''
|
||||
let actions = [
|
||||
{
|
||||
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
|
||||
label: __('Make a Call'),
|
||||
onClick: () => makeCall(mobile_no),
|
||||
condition: () => mobile_no && callEnabled.value,
|
||||
},
|
||||
{
|
||||
icon: h(NoteIcon, { class: 'h-4 w-4' }),
|
||||
label: __('New Note'),
|
||||
onClick: () => showNote(itemName),
|
||||
},
|
||||
{
|
||||
icon: h(TaskIcon, { class: 'h-4 w-4' }),
|
||||
label: __('New Task'),
|
||||
onClick: () => showTask(itemName),
|
||||
},
|
||||
]
|
||||
return actions.filter((action) =>
|
||||
action.condition ? action.condition() : true,
|
||||
)
|
||||
}
|
||||
|
||||
const docname = ref('')
|
||||
const showNoteModal = ref(false)
|
||||
const note = ref({
|
||||
title: '',
|
||||
content: '',
|
||||
})
|
||||
|
||||
function showNote(name) {
|
||||
docname.value = name
|
||||
showNoteModal.value = true
|
||||
}
|
||||
|
||||
const showTaskModal = ref(false)
|
||||
const task = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
assigned_to: '',
|
||||
due_date: '',
|
||||
priority: 'Low',
|
||||
status: 'Backlog',
|
||||
})
|
||||
|
||||
function showTask(name) {
|
||||
docname.value = name
|
||||
showTaskModal.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -92,7 +92,11 @@ const updatedPageCount = ref(20)
|
||||
const viewControls = ref(null)
|
||||
|
||||
const rows = computed(() => {
|
||||
if (!emailTemplates.value?.data?.data) return []
|
||||
if (
|
||||
!emailTemplates.value?.data?.data ||
|
||||
!['list', 'group_by'].includes(emailTemplates.value.data.view_type)
|
||||
)
|
||||
return []
|
||||
return emailTemplates.value?.data.data.map((emailTemplate) => {
|
||||
let _rows = {}
|
||||
emailTemplates.value?.data.rows.forEach((row) => {
|
||||
|
||||
@ -26,12 +26,209 @@
|
||||
doctype="CRM Lead"
|
||||
:filters="{ converted: 0 }"
|
||||
:options="{
|
||||
allowedViews: ['list', 'group_by'],
|
||||
allowedViews: ['list', 'group_by', 'kanban'],
|
||||
}"
|
||||
/>
|
||||
<KanbanView
|
||||
v-if="route.params.viewType == 'kanban'"
|
||||
v-model="leads"
|
||||
:options="{
|
||||
getRoute: (row) => ({ name: 'Lead', params: { leadId: row.name } }),
|
||||
onNewClick: (column) => onNewClick(column),
|
||||
}"
|
||||
@update="(data) => viewControls.updateKanbanSettings(data)"
|
||||
@loadMore="(columnName) => viewControls.loadMoreKanban(columnName)"
|
||||
>
|
||||
<template #title="{ titleField, itemName }">
|
||||
<div class="flex items-center gap-2">
|
||||
<div v-if="titleField === 'status'">
|
||||
<IndicatorIcon :class="getRow(itemName, titleField).color" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
titleField === 'organization' && getRow(itemName, titleField).label
|
||||
"
|
||||
>
|
||||
<Avatar
|
||||
class="flex items-center"
|
||||
:image="getRow(itemName, titleField).logo"
|
||||
:label="getRow(itemName, titleField).label"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
titleField === 'lead_name' && getRow(itemName, titleField).label
|
||||
"
|
||||
>
|
||||
<Avatar
|
||||
class="flex items-center"
|
||||
:image="getRow(itemName, titleField).image"
|
||||
:label="getRow(itemName, titleField).image_label"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
titleField === 'lead_owner' &&
|
||||
getRow(itemName, titleField).full_name
|
||||
"
|
||||
>
|
||||
<Avatar
|
||||
class="flex items-center"
|
||||
:image="getRow(itemName, titleField).user_image"
|
||||
:label="getRow(itemName, titleField).full_name"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="titleField === 'mobile_no'">
|
||||
<PhoneIcon class="h-4 w-4" />
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
[
|
||||
'modified',
|
||||
'creation',
|
||||
'first_response_time',
|
||||
'first_responded_on',
|
||||
'response_by',
|
||||
].includes(titleField)
|
||||
"
|
||||
class="truncate text-base"
|
||||
>
|
||||
<Tooltip :text="getRow(itemName, titleField).label">
|
||||
<div>{{ getRow(itemName, titleField).timeAgo }}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div v-else-if="titleField === 'sla_status'" class="truncate text-base">
|
||||
<Badge
|
||||
v-if="getRow(itemName, titleField).value"
|
||||
:variant="'subtle'"
|
||||
:theme="getRow(itemName, titleField).color"
|
||||
size="md"
|
||||
:label="getRow(itemName, titleField).value"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="getRow(itemName, titleField).label"
|
||||
class="truncate text-base"
|
||||
>
|
||||
{{ getRow(itemName, titleField).label }}
|
||||
</div>
|
||||
<div class="text-gray-500" v-else>{{ __('No Title') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #fields="{ fieldName, itemName }">
|
||||
<div
|
||||
v-if="getRow(itemName, fieldName).label"
|
||||
class="truncate flex items-center gap-2"
|
||||
>
|
||||
<div v-if="fieldName === 'status'">
|
||||
<IndicatorIcon :class="getRow(itemName, fieldName).color" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
fieldName === 'organization' && getRow(itemName, fieldName).label
|
||||
"
|
||||
>
|
||||
<Avatar
|
||||
class="flex items-center"
|
||||
:image="getRow(itemName, fieldName).logo"
|
||||
:label="getRow(itemName, fieldName).label"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="fieldName === 'lead_name'">
|
||||
<Avatar
|
||||
v-if="getRow(itemName, fieldName).label"
|
||||
class="flex items-center"
|
||||
:image="getRow(itemName, fieldName).image"
|
||||
:label="getRow(itemName, fieldName).image_label"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="fieldName === 'lead_owner'">
|
||||
<Avatar
|
||||
v-if="getRow(itemName, fieldName).full_name"
|
||||
class="flex items-center"
|
||||
:image="getRow(itemName, fieldName).user_image"
|
||||
:label="getRow(itemName, fieldName).full_name"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
[
|
||||
'modified',
|
||||
'creation',
|
||||
'first_response_time',
|
||||
'first_responded_on',
|
||||
'response_by',
|
||||
].includes(fieldName)
|
||||
"
|
||||
class="truncate text-base"
|
||||
>
|
||||
<Tooltip :text="getRow(itemName, fieldName).label">
|
||||
<div>{{ getRow(itemName, fieldName).timeAgo }}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div v-else-if="fieldName === 'sla_status'" class="truncate text-base">
|
||||
<Badge
|
||||
v-if="getRow(itemName, fieldName).value"
|
||||
:variant="'subtle'"
|
||||
:theme="getRow(itemName, fieldName).color"
|
||||
size="md"
|
||||
:label="getRow(itemName, fieldName).value"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="fieldName === '_assign'" class="flex items-center">
|
||||
<MultipleAvatar
|
||||
:avatars="getRow(itemName, fieldName).label"
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="truncate text-base">
|
||||
{{ getRow(itemName, fieldName).label }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ itemName }">
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<div class="text-gray-600 flex items-center gap-1.5">
|
||||
<EmailAtIcon class="h-4 w-4" />
|
||||
<span v-if="getRow(itemName, '_email_count').label">
|
||||
{{ getRow(itemName, '_email_count').label }}
|
||||
</span>
|
||||
<span class="text-3xl leading-[0]"> · </span>
|
||||
<NoteIcon class="h-4 w-4" />
|
||||
<span v-if="getRow(itemName, '_note_count').label">
|
||||
{{ getRow(itemName, '_note_count').label }}
|
||||
</span>
|
||||
<span class="text-3xl leading-[0]"> · </span>
|
||||
<TaskIcon class="h-4 w-4" />
|
||||
<span v-if="getRow(itemName, '_task_count').label">
|
||||
{{ getRow(itemName, '_task_count').label }}
|
||||
</span>
|
||||
<span class="text-3xl leading-[0]"> · </span>
|
||||
<CommentIcon class="h-4 w-4" />
|
||||
<span v-if="getRow(itemName, '_comment_count').label">
|
||||
{{ getRow(itemName, '_comment_count').label }}
|
||||
</span>
|
||||
</div>
|
||||
<Dropdown
|
||||
class="flex items-center gap-2"
|
||||
:options="actions(itemName)"
|
||||
variant="ghost"
|
||||
@click.stop.prevent
|
||||
>
|
||||
<Button icon="plus" variant="ghost" />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</KanbanView>
|
||||
<LeadsListView
|
||||
ref="leadsListView"
|
||||
v-if="leads.data && rows.length"
|
||||
v-else-if="leads.data && rows.length"
|
||||
v-model="leads.data.page_length_count"
|
||||
v-model:list="leads"
|
||||
:rows="rows"
|
||||
@ -60,43 +257,66 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<LeadModal v-model="showLeadModal" />
|
||||
<LeadModal
|
||||
v-if="showLeadModal"
|
||||
v-model="showLeadModal"
|
||||
:defaults="defaults"
|
||||
/>
|
||||
<NoteModal
|
||||
v-model="showNoteModal"
|
||||
:note="note"
|
||||
doctype="CRM Lead"
|
||||
:doc="docname"
|
||||
/>
|
||||
<TaskModal
|
||||
v-model="showTaskModal"
|
||||
:task="task"
|
||||
doctype="CRM Lead"
|
||||
:doc="docname"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||
import CustomActions from '@/components/CustomActions.vue'
|
||||
import EmailAtIcon from '@/components/Icons/EmailAtIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
||||
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import LeadsListView from '@/components/ListViews/LeadsListView.vue'
|
||||
import KanbanView from '@/components/Kanban/KanbanView.vue'
|
||||
import LeadModal from '@/components/Modals/LeadModal.vue'
|
||||
import NoteModal from '@/components/Modals/NoteModal.vue'
|
||||
import TaskModal from '@/components/Modals/TaskModal.vue'
|
||||
import ViewControls from '@/components/ViewControls.vue'
|
||||
import { globalStore } from '@/stores/global'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { organizationsStore } from '@/stores/organizations'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import {
|
||||
dateFormat,
|
||||
dateTooltipFormat,
|
||||
timeAgo,
|
||||
formatTime,
|
||||
createToast,
|
||||
} from '@/utils'
|
||||
import { createResource, Breadcrumbs } from 'frappe-ui'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { callEnabled } from '@/composables/settings'
|
||||
import { dateFormat, dateTooltipFormat, timeAgo, formatTime } from '@/utils'
|
||||
import { Breadcrumbs, Avatar, Tooltip, Dropdown } from 'frappe-ui'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref, computed, reactive, h } from 'vue'
|
||||
|
||||
const breadcrumbs = [{ label: __('Leads'), route: { name: 'Leads' } }]
|
||||
|
||||
const { makeCall } = globalStore()
|
||||
const { getUser } = usersStore()
|
||||
const { getOrganization } = organizationsStore()
|
||||
const { getLeadStatus } = statusesStore()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const leadsListView = ref(null)
|
||||
const showLeadModal = ref(false)
|
||||
|
||||
const defaults = reactive({})
|
||||
|
||||
// leads data is loaded in the ViewControls component
|
||||
const leads = ref({})
|
||||
const loadMore = ref(1)
|
||||
@ -104,15 +324,27 @@ const triggerResize = ref(1)
|
||||
const updatedPageCount = ref(20)
|
||||
const viewControls = ref(null)
|
||||
|
||||
function getRow(name, field) {
|
||||
function getValue(value) {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
return { label: value }
|
||||
}
|
||||
return getValue(rows.value?.find((row) => row.name == name)[field])
|
||||
}
|
||||
|
||||
// Rows
|
||||
const rows = computed(() => {
|
||||
if (!leads.value?.data?.data) return []
|
||||
if (route.params.viewType === 'group_by') {
|
||||
if (leads.value.data.view_type === 'group_by') {
|
||||
if (!leads.value?.data.group_by_field?.name) return []
|
||||
return getGroupedByRows(
|
||||
leads.value?.data.data,
|
||||
leads.value?.data.group_by_field
|
||||
leads.value?.data.group_by_field,
|
||||
)
|
||||
} else if (leads.value.data.view_type === 'kanban') {
|
||||
return getKanbanRows(leads.value.data.data)
|
||||
} else {
|
||||
return parseRows(leads.value?.data.data)
|
||||
}
|
||||
@ -148,6 +380,16 @@ function getGroupedByRows(listRows, groupByField) {
|
||||
return groupedRows || listRows
|
||||
}
|
||||
|
||||
function getKanbanRows(data) {
|
||||
let _rows = []
|
||||
data.forEach((column) => {
|
||||
column.data?.forEach((row) => {
|
||||
_rows.push(row)
|
||||
})
|
||||
})
|
||||
return parseRows(_rows)
|
||||
}
|
||||
|
||||
function parseRows(rows) {
|
||||
return rows.map((lead) => {
|
||||
let _rows = {}
|
||||
@ -177,8 +419,8 @@ function parseRows(rows) {
|
||||
lead.sla_status == 'Failed'
|
||||
? 'red'
|
||||
: lead.sla_status == 'Fulfilled'
|
||||
? 'green'
|
||||
: 'orange'
|
||||
? 'green'
|
||||
: 'orange'
|
||||
if (value == 'First Response Due') {
|
||||
value = __(timeAgo(lead.response_by))
|
||||
tooltipText = dateFormat(lead.response_by, dateTooltipFormat)
|
||||
@ -213,7 +455,7 @@ function parseRows(rows) {
|
||||
}
|
||||
} else if (
|
||||
['first_response_time', 'first_responded_on', 'response_by'].includes(
|
||||
row
|
||||
row,
|
||||
)
|
||||
) {
|
||||
let field = row == 'response_by' ? 'response_by' : 'first_responded_on'
|
||||
@ -227,57 +469,73 @@ function parseRows(rows) {
|
||||
}
|
||||
}
|
||||
})
|
||||
_rows['_email_count'] = lead._email_count
|
||||
_rows['_note_count'] = lead._note_count
|
||||
_rows['_task_count'] = lead._task_count
|
||||
_rows['_comment_count'] = lead._comment_count
|
||||
return _rows
|
||||
})
|
||||
}
|
||||
|
||||
let newLead = reactive({
|
||||
salutation: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
lead_name: '',
|
||||
organization: '',
|
||||
status: '',
|
||||
email: '',
|
||||
mobile_no: '',
|
||||
lead_owner: '',
|
||||
function onNewClick(column) {
|
||||
let column_field = leads.value.params.column_field
|
||||
|
||||
if (column_field) {
|
||||
defaults[column_field] = column.column.name
|
||||
}
|
||||
|
||||
showLeadModal.value = true
|
||||
}
|
||||
|
||||
function actions(itemName) {
|
||||
let mobile_no = getRow(itemName, 'mobile_no')?.label || ''
|
||||
let actions = [
|
||||
{
|
||||
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
|
||||
label: __('Make a Call'),
|
||||
onClick: () => makeCall(mobile_no),
|
||||
condition: () => mobile_no && callEnabled.value,
|
||||
},
|
||||
{
|
||||
icon: h(NoteIcon, { class: 'h-4 w-4' }),
|
||||
label: __('New Note'),
|
||||
onClick: () => showNote(itemName),
|
||||
},
|
||||
{
|
||||
icon: h(TaskIcon, { class: 'h-4 w-4' }),
|
||||
label: __('New Task'),
|
||||
onClick: () => showTask(itemName),
|
||||
},
|
||||
]
|
||||
return actions.filter((action) =>
|
||||
action.condition ? action.condition() : true,
|
||||
)
|
||||
}
|
||||
|
||||
const docname = ref('')
|
||||
const showNoteModal = ref(false)
|
||||
const note = ref({
|
||||
title: '',
|
||||
content: '',
|
||||
})
|
||||
|
||||
const createLead = createResource({
|
||||
url: 'frappe.client.insert',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doc: {
|
||||
doctype: 'CRM Lead',
|
||||
...values,
|
||||
},
|
||||
}
|
||||
},
|
||||
function showNote(name) {
|
||||
docname.value = name
|
||||
showNoteModal.value = true
|
||||
}
|
||||
|
||||
const showTaskModal = ref(false)
|
||||
const task = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
assigned_to: '',
|
||||
due_date: '',
|
||||
priority: 'Low',
|
||||
status: 'Backlog',
|
||||
})
|
||||
|
||||
function createNewLead(close) {
|
||||
createLead
|
||||
.submit(newLead, {
|
||||
validate() {
|
||||
if (!newLead.first_name) {
|
||||
createToast({
|
||||
title: __('Error creating lead'),
|
||||
text: __('First name is required'),
|
||||
icon: 'x',
|
||||
iconClasses: 'text-red-600',
|
||||
})
|
||||
return __('First name is required')
|
||||
}
|
||||
},
|
||||
onSuccess(data) {
|
||||
router.push({
|
||||
name: 'Lead',
|
||||
params: {
|
||||
leadId: data.name,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
.then(close)
|
||||
function showTask(name) {
|
||||
docname.value = name
|
||||
showTaskModal.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -110,7 +110,11 @@ const updatedPageCount = ref(20)
|
||||
const viewControls = ref(null)
|
||||
|
||||
const rows = computed(() => {
|
||||
if (!organizations.value?.data?.data) return []
|
||||
if (
|
||||
!organizations.value?.data?.data ||
|
||||
!['list', 'group_by'].includes(organizations.value.data.view_type)
|
||||
)
|
||||
return []
|
||||
return organizations.value?.data.data.map((organization) => {
|
||||
let _rows = {}
|
||||
organizations.value?.data.rows.forEach((row) => {
|
||||
|
||||
@ -20,10 +20,141 @@
|
||||
v-model:resizeColumn="triggerResize"
|
||||
v-model:updatedPageCount="updatedPageCount"
|
||||
doctype="CRM Task"
|
||||
:options="{
|
||||
allowedViews: ['list', 'kanban'],
|
||||
}"
|
||||
/>
|
||||
<KanbanView
|
||||
v-if="$route.params.viewType == 'kanban' && rows.length"
|
||||
v-model="tasks"
|
||||
:options="{
|
||||
onClick: (row) => showTask(row.name),
|
||||
onNewClick: (column) => createTask(column),
|
||||
}"
|
||||
@update="(data) => viewControls.updateKanbanSettings(data)"
|
||||
@loadMore="(columnName) => viewControls.loadMoreKanban(columnName)"
|
||||
>
|
||||
<template #title="{ titleField, itemName }">
|
||||
<div class="flex items-center gap-2">
|
||||
<div v-if="titleField === 'status'">
|
||||
<TaskStatusIcon :status="getRow(itemName, titleField).label" />
|
||||
</div>
|
||||
<div v-else-if="titleField === 'priority'">
|
||||
<TaskPriorityIcon :priority="getRow(itemName, titleField).label" />
|
||||
</div>
|
||||
<div v-else-if="titleField === 'assigned_to'">
|
||||
<Avatar
|
||||
v-if="getRow(itemName, titleField).full_name"
|
||||
class="flex items-center"
|
||||
:image="getRow(itemName, titleField).user_image"
|
||||
:label="getRow(itemName, titleField).full_name"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="['modified', 'creation'].includes(titleField)"
|
||||
class="truncate text-base"
|
||||
>
|
||||
<Tooltip :text="getRow(itemName, titleField).label">
|
||||
<div>{{ getRow(itemName, titleField).timeAgo }}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="getRow(itemName, titleField).label"
|
||||
class="truncate text-base"
|
||||
>
|
||||
{{ getRow(itemName, titleField).label }}
|
||||
</div>
|
||||
<div class="text-gray-500" v-else>{{ __('No Title') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #fields="{ fieldName, itemName }">
|
||||
<div
|
||||
v-if="getRow(itemName, fieldName).label"
|
||||
class="truncate flex items-center gap-2"
|
||||
>
|
||||
<div v-if="fieldName === 'status'">
|
||||
<TaskStatusIcon
|
||||
class="size-3"
|
||||
:status="getRow(itemName, fieldName).label"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="fieldName === 'priority'">
|
||||
<TaskPriorityIcon :priority="getRow(itemName, fieldName).label" />
|
||||
</div>
|
||||
<div v-else-if="fieldName === 'assigned_to'">
|
||||
<Avatar
|
||||
v-if="getRow(itemName, fieldName).full_name"
|
||||
class="flex items-center"
|
||||
:image="getRow(itemName, fieldName).user_image"
|
||||
:label="getRow(itemName, fieldName).full_name"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="['modified', 'creation'].includes(fieldName)"
|
||||
class="truncate text-base"
|
||||
>
|
||||
<Tooltip :text="getRow(itemName, fieldName).label">
|
||||
<div>{{ getRow(itemName, fieldName).timeAgo }}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="fieldName == 'description'"
|
||||
class="truncate text-base max-h-44"
|
||||
>
|
||||
<TextEditor
|
||||
v-if="getRow(itemName, fieldName).label"
|
||||
:content="getRow(itemName, fieldName).label"
|
||||
:editable="false"
|
||||
editor-class="!prose-sm max-w-none focus:outline-none"
|
||||
class="flex-1 overflow-hidden"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="truncate text-base">
|
||||
{{ getRow(itemName, fieldName).label }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ itemName }">
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<div>
|
||||
<Button
|
||||
class="-ml-2"
|
||||
v-if="getRow(itemName, 'reference_docname').label"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:label="
|
||||
getRow(itemName, 'reference_doctype').label == 'CRM Deal'
|
||||
? __('Deal')
|
||||
: __('Lead')
|
||||
"
|
||||
@click.stop="
|
||||
redirect(
|
||||
getRow(itemName, 'reference_doctype').label,
|
||||
getRow(itemName, 'reference_docname').label,
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #suffix>
|
||||
<ArrowUpRightIcon class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<Dropdown
|
||||
class="flex items-center gap-2"
|
||||
:options="actions(itemName)"
|
||||
variant="ghost"
|
||||
@click.stop.prevent
|
||||
>
|
||||
<Button icon="more-horizontal" variant="ghost" />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</KanbanView>
|
||||
<TasksListView
|
||||
ref="tasksListView"
|
||||
v-if="tasks.data && rows.length"
|
||||
v-else-if="tasks.data && rows.length"
|
||||
v-model="tasks.data.page_length_count"
|
||||
v-model:list="tasks"
|
||||
:rows="rows"
|
||||
@ -53,25 +184,44 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TaskModal v-model="showTaskModal" v-model:reloadTasks="tasks" :task="task" />
|
||||
<TaskModal
|
||||
v-if="showTaskModal"
|
||||
v-model="showTaskModal"
|
||||
v-model:reloadTasks="tasks"
|
||||
:task="task"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import CustomActions from '@/components/CustomActions.vue'
|
||||
import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue'
|
||||
import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
|
||||
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
|
||||
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import ViewControls from '@/components/ViewControls.vue'
|
||||
import TasksListView from '@/components/ListViews/TasksListView.vue'
|
||||
import KanbanView from '@/components/Kanban/KanbanView.vue'
|
||||
import TaskModal from '@/components/Modals/TaskModal.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { dateFormat, dateTooltipFormat, timeAgo } from '@/utils'
|
||||
import { Breadcrumbs } from 'frappe-ui'
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Tooltip,
|
||||
Avatar,
|
||||
TextEditor,
|
||||
Dropdown,
|
||||
call,
|
||||
} from 'frappe-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const breadcrumbs = [{ label: __('Tasks'), route: { name: 'Tasks' } }]
|
||||
|
||||
const { getUser } = usersStore()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const tasksListView = ref(null)
|
||||
|
||||
// tasks data is loaded in the ViewControls component
|
||||
@ -81,9 +231,38 @@ const triggerResize = ref(1)
|
||||
const updatedPageCount = ref(20)
|
||||
const viewControls = ref(null)
|
||||
|
||||
function getRow(name, field) {
|
||||
function getValue(value) {
|
||||
if (value && typeof value === 'object') {
|
||||
return value
|
||||
}
|
||||
return { label: value }
|
||||
}
|
||||
return getValue(rows.value?.find((row) => row.name == name)[field])
|
||||
}
|
||||
|
||||
const rows = computed(() => {
|
||||
if (!tasks.value?.data?.data) return []
|
||||
return tasks.value?.data.data.map((task) => {
|
||||
|
||||
if (tasks.value.data.view_type === 'kanban') {
|
||||
return getKanbanRows(tasks.value.data.data)
|
||||
}
|
||||
|
||||
return parseRows(tasks.value?.data.data)
|
||||
})
|
||||
|
||||
function getKanbanRows(data) {
|
||||
let _rows = []
|
||||
data.forEach((column) => {
|
||||
column.data?.forEach((row) => {
|
||||
_rows.push(row)
|
||||
})
|
||||
})
|
||||
return parseRows(_rows)
|
||||
}
|
||||
|
||||
function parseRows(rows) {
|
||||
return rows.map((task) => {
|
||||
let _rows = {}
|
||||
tasks.value?.data.rows.forEach((row) => {
|
||||
_rows[row] = task[row]
|
||||
@ -102,7 +281,7 @@ const rows = computed(() => {
|
||||
})
|
||||
return _rows
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const showTaskModal = ref(false)
|
||||
|
||||
@ -134,7 +313,7 @@ function showTask(name) {
|
||||
showTaskModal.value = true
|
||||
}
|
||||
|
||||
function createTask() {
|
||||
function createTask(column) {
|
||||
task.value = {
|
||||
name: '',
|
||||
title: '',
|
||||
@ -146,6 +325,44 @@ function createTask() {
|
||||
reference_doctype: 'CRM Lead',
|
||||
reference_docname: '',
|
||||
}
|
||||
|
||||
if (column.column?.name) {
|
||||
let column_field = tasks.value.params.column_field
|
||||
if (column_field) {
|
||||
task.value[column_field] = column.column.name
|
||||
}
|
||||
}
|
||||
|
||||
showTaskModal.value = true
|
||||
}
|
||||
|
||||
function actions(name) {
|
||||
return [
|
||||
{
|
||||
label: __('Delete'),
|
||||
icon: 'trash-2',
|
||||
onClick: () => {
|
||||
deletetask(name)
|
||||
tasks.value.reload()
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async function deletetask(name) {
|
||||
await call('frappe.client.delete', {
|
||||
doctype: 'CRM Task',
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
function redirect(doctype, docname) {
|
||||
if (!docname) return
|
||||
let name = doctype == 'CRM Deal' ? 'Deal' : 'Lead'
|
||||
let params = { leadId: docname }
|
||||
if (name == 'Deal') {
|
||||
params = { dealId: docname }
|
||||
}
|
||||
router.push({ name: name, params: params })
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -45,7 +45,8 @@ const routes = [
|
||||
component: () => import('@/pages/Notes.vue'),
|
||||
},
|
||||
{
|
||||
path: '/tasks',
|
||||
alias: '/tasks',
|
||||
path: '/tasks/view/:viewType?',
|
||||
name: 'Tasks',
|
||||
component: () => import('@/pages/Tasks.vue'),
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user