Merge pull request #235 from frappe/develop

chore: Merge develop to main
This commit is contained in:
Shariq Ansari 2024-07-01 13:14:25 +05:30 committed by GitHub
commit 4bdc1b2932
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1933 additions and 226 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,18 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-kanban"
>
<path d="M6 5v11" />
<path d="M12 5v6" />
<path d="M18 5v14" />
</svg>
</template>

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]"> &middot; </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]"> &middot; </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]"> &middot; </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>

View File

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

View File

@ -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]"> &middot; </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]"> &middot; </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]"> &middot; </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>

View File

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

View File

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

View File

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