diff --git a/crm/api/doc.py b/crm/api/doc.py index eba670c7..a8627584 100644 --- a/crm/api/doc.py +++ b/crm/api/doc.py @@ -276,7 +276,7 @@ def get_data( default_view_filters = { "dt": doctype, "type": view_type or "list", - "is_default": 1, + "is_standard": 1, "user": frappe.session.user, } diff --git a/crm/api/views.py b/crm/api/views.py index f70246e1..5313753e 100644 --- a/crm/api/views.py +++ b/crm/api/views.py @@ -8,9 +8,9 @@ def get_views(doctype): query = ( frappe.qb.from_(View) .select("*") - .where(Criterion.any([View.user == '', View.user == frappe.session.user])) + .where(Criterion.any([View.user == "", View.user == frappe.session.user])) ) if doctype: query = query.where(View.dt == doctype) views = query.run(as_dict=True) - return views \ No newline at end of file + return views diff --git a/crm/fcrm/doctype/crm_notification/crm_notification.py b/crm/fcrm/doctype/crm_notification/crm_notification.py index 69aa127d..dbc075a1 100644 --- a/crm/fcrm/doctype/crm_notification/crm_notification.py +++ b/crm/fcrm/doctype/crm_notification/crm_notification.py @@ -8,7 +8,8 @@ from frappe.model.document import Document class CRMNotification(Document): def on_update(self): - frappe.publish_realtime("crm_notification") + if self.to_user: + frappe.publish_realtime("crm_notification", user= self.to_user) def notify_user(args): """ @@ -33,4 +34,4 @@ def notify_user(args): if frappe.db.exists("CRM Notification", values): return - frappe.get_doc(values).insert(ignore_permissions=True) \ No newline at end of file + frappe.get_doc(values).insert(ignore_permissions=True) diff --git a/crm/fcrm/doctype/crm_view_settings/crm_view_settings.json b/crm/fcrm/doctype/crm_view_settings/crm_view_settings.json index dde0b128..e8b9d8cc 100644 --- a/crm/fcrm/doctype/crm_view_settings/crm_view_settings.json +++ b/crm/fcrm/doctype/crm_view_settings/crm_view_settings.json @@ -8,6 +8,7 @@ "label", "icon", "user", + "is_standard", "is_default", "column_break_zacm", "type", @@ -112,12 +113,6 @@ "fieldtype": "Check", "label": "Public" }, - { - "default": "0", - "fieldname": "is_default", - "fieldtype": "Check", - "label": "Is Default" - }, { "fieldname": "icon", "fieldtype": "Data", @@ -174,15 +169,26 @@ "label": "Kanban Fields" }, { - "default": "name", "fieldname": "title_field", "fieldtype": "Data", "label": "Title Field" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard" + }, + { + "default": "0", + "fieldname": "is_default", + "fieldtype": "Check", + "label": "Is Default" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-06-25 19:40:12.067788", + "modified": "2025-02-20 15:36:55.059065", "modified_by": "Administrator", "module": "FCRM", "name": "CRM View Settings", diff --git a/crm/fcrm/doctype/crm_view_settings/crm_view_settings.py b/crm/fcrm/doctype/crm_view_settings/crm_view_settings.py index e0cf53d5..2552b901 100644 --- a/crm/fcrm/doctype/crm_view_settings/crm_view_settings.py +++ b/crm/fcrm/doctype/crm_view_settings/crm_view_settings.py @@ -1,6 +1,7 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt import json + import frappe from frappe.model.document import Document, get_controller from frappe.utils import parse_json @@ -9,15 +10,16 @@ from frappe.utils import parse_json class CRMViewSettings(Document): pass + @frappe.whitelist() def create(view): view = frappe._dict(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 '[]') + 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_rows(view.doctype) view.rows = view.rows + default_rows if default_rows else view.rows @@ -31,11 +33,11 @@ def create(view): doc = frappe.new_doc("CRM View Settings") doc.name = view.label doc.label = view.label - doc.type = view.type or 'list' + doc.type = view.type or "list" doc.icon = view.icon doc.dt = view.doctype doc.user = frappe.session.user - doc.route_name = view.route_name or "" + doc.route_name = view.route_name or get_route_name(view.doctype) doc.load_default_columns = view.load_default_columns or False doc.filters = json.dumps(view.filters) doc.order_by = view.order_by @@ -49,6 +51,7 @@ def create(view): doc.insert() return doc + @frappe.whitelist() def update(view): view = frappe._dict(view) @@ -65,9 +68,9 @@ def update(view): doc = frappe.get_doc("CRM View Settings", view.name) doc.label = view.label - doc.type = view.type or 'list' + doc.type = view.type or "list" doc.icon = view.icon - doc.route_name = view.route_name or "" + doc.route_name = view.route_name or get_route_name(view.doctype) doc.load_default_columns = view.load_default_columns or False doc.filters = json.dumps(filters) doc.order_by = view.order_by @@ -81,11 +84,13 @@ def update(view): doc.save() return doc + @frappe.whitelist() def delete(name): if frappe.db.exists("CRM View Settings", name): frappe.delete_doc("CRM View Settings", name) + @frappe.whitelist() def public(name, value): if frappe.session.user != "Administrator" and "Sales Manager" not in frappe.get_roles(): @@ -98,15 +103,18 @@ def public(name, value): doc.user = "" if value else frappe.session.user doc.save() + @frappe.whitelist() def pin(name, value): doc = frappe.get_doc("CRM View Settings", name) doc.pinned = value doc.save() + def remove_duplicates(l): return list(dict.fromkeys(l)) + def sync_default_rows(doctype, type="list"): list = get_controller(doctype) rows = [] @@ -116,6 +124,7 @@ def sync_default_rows(doctype, type="list"): return rows + def sync_default_columns(view): list = get_controller(view.doctype) columns = [] @@ -137,14 +146,32 @@ def sync_default_columns(view): @frappe.whitelist() -def create_or_update_default_view(view): +def set_as_default(name=None, type=None, doctype=None): + if name: + frappe.db.set_value("CRM View Settings", name, "is_default", 1) + else: + doc = create_or_update_standard_view({"type": type, "doctype": doctype, "is_default": 1}) + name = doc.name + + # remove default from other views of same user + frappe.db.set_value( + "CRM View Settings", + {"name": ("!=", name), "user": frappe.session.user, "is_default": 1}, + "is_default", + 0, + ) + + +@frappe.whitelist() +def create_or_update_standard_view(view): view = frappe._dict(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 '[]') + 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 "[]") + view.column_field = view.column_field or "status" default_rows = sync_default_rows(view.doctype, view.type) rows = rows + default_rows if default_rows else rows @@ -157,47 +184,63 @@ def create_or_update_default_view(view): doc = frappe.db.exists( "CRM View Settings", - { - "dt": view.doctype, - "type": view.type or 'list', - "is_default": True, - "user": frappe.session.user - }, + {"dt": view.doctype, "type": view.type or "list", "is_standard": True, "user": frappe.session.user}, ) if doc: doc = frappe.get_doc("CRM View Settings", doc) doc.label = view.label - doc.type = view.type or 'list' - doc.route_name = view.route_name or "" + doc.type = view.type or "list" + doc.route_name = view.route_name or get_route_name(view.doctype) doc.load_default_columns = view.load_default_columns or False doc.filters = json.dumps(filters) - doc.order_by = view.order_by - doc.group_by_field = view.group_by_field + doc.order_by = view.order_by or "modified desc" + doc.group_by_field = view.group_by_field or "owner" 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 = view.is_default or False doc.save() else: doc = frappe.new_doc("CRM View Settings") - label = 'Group By View' if view.type == 'group_by' else 'List View' + + label = "List" + if view.type == "group_by": + label = "Group By" + elif view.type == "kanban": + label = "Kanban" + doc.name = view.label or label doc.label = view.label or label - doc.type = view.type or 'list' + doc.type = view.type or "list" doc.dt = view.doctype doc.user = frappe.session.user - doc.route_name = view.route_name or "" + doc.route_name = view.route_name or get_route_name(view.doctype) doc.load_default_columns = view.load_default_columns or False doc.filters = json.dumps(filters) - doc.order_by = view.order_by - doc.group_by_field = view.group_by_field + doc.order_by = view.order_by or "modified desc" + doc.group_by_field = view.group_by_field or "owner" 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 - doc.insert() \ No newline at end of file + doc.is_standard = True + doc.is_default = view.is_default or False + doc.insert() + + return doc + + +def get_route_name(doctype): + # Example: "CRM Lead" -> "Leads" + if doctype.startswith("CRM "): + doctype = doctype[4:] + + if doctype[-1] != "s": + doctype += "s" + + return doctype diff --git a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json index f445541d..250c8c29 100644 --- a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json +++ b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json @@ -61,7 +61,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-01-19 14:23:05.981355", + "modified": "2025-02-20 12:38:38.088477", "modified_by": "Administrator", "module": "FCRM", "name": "FCRM Settings", diff --git a/frontend/src/components/ViewControls.vue b/frontend/src/components/ViewControls.vue index 12f021ca..ee38ccc0 100644 --- a/frontend/src/components/ViewControls.vue +++ b/frontend/src/components/ViewControls.vue @@ -215,6 +215,7 @@ import QuickFilterField from '@/components/QuickFilterField.vue' import RefreshIcon from '@/components/Icons/RefreshIcon.vue' import EditIcon from '@/components/Icons/EditIcon.vue' import DuplicateIcon from '@/components/Icons/DuplicateIcon.vue' +import CheckIcon from '@/components/Icons/CheckIcon.vue' import PinIcon from '@/components/Icons/PinIcon.vue' import UnpinIcon from '@/components/Icons/UnpinIcon.vue' import ViewModal from '@/components/Modals/ViewModal.vue' @@ -263,7 +264,7 @@ const props = defineProps({ const { brand } = getSettings() const { $dialog } = globalStore() -const { reload: reloadView, getView } = viewsStore() +const { reload: reloadView, getDefaultView, getView } = viewsStore() const { isManager } = usersStore() const list = defineModel() @@ -309,13 +310,13 @@ const currentView = computed(() => { label: _view?.label || props.options?.defaultViewName || getViewType().label, icon: _view?.icon || getViewType().icon, - is_default: !_view || _view.is_default, + is_standard: !_view || _view.is_standard, } }) usePageMeta(() => { let label = currentView.value.label - if (currentView.value.is_default) { + if (currentView.value.is_standard) { let routeName = route.name label = `${routeName} - ${label}` } @@ -479,11 +480,11 @@ async function exportRows() { export_type.value = 'Excel' } -let defaultViews = [] +let standardViews = [] let allowedViews = props.options.allowedViews || ['list'] if (allowedViews.includes('list')) { - defaultViews.push({ + standardViews.push({ name: 'list', label: __(props.options?.defaultViewName) || __('List'), icon: markRaw(ListIcon), @@ -494,7 +495,7 @@ if (allowedViews.includes('list')) { }) } if (allowedViews.includes('kanban')) { - defaultViews.push({ + standardViews.push({ name: 'kanban', label: __(props.options?.defaultViewName) || __('Kanban'), icon: markRaw(KanbanIcon), @@ -505,7 +506,7 @@ if (allowedViews.includes('kanban')) { }) } if (allowedViews.includes('group_by')) { - defaultViews.push({ + standardViews.push({ name: 'group_by', label: __(props.options?.defaultViewName) || __('Group By'), icon: markRaw(GroupByIcon), @@ -530,9 +531,9 @@ function getIcon(icon, type) { const viewsDropdownOptions = computed(() => { let _views = [ { - group: __('Default Views'), + group: __('Standard Views'), hideLabel: true, - items: defaultViews, + items: standardViews, }, ] @@ -557,7 +558,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_standard, ) let pinnedViews = list.value.data.views.filter((v) => v.pinned) @@ -661,7 +662,7 @@ function updateFilter(filters) { list.value.reload() if (!route.query.view) { - create_or_update_default_view() + createOrUpdateStandardView() } } @@ -676,7 +677,7 @@ function updateSort(order_by) { list.value.reload() if (!route.query.view) { - create_or_update_default_view() + createOrUpdateStandardView() } } @@ -691,7 +692,7 @@ function updateGroupBy(group_by_field) { list.value.reload() if (!route.query.view) { - create_or_update_default_view() + createOrUpdateStandardView() } } @@ -725,7 +726,7 @@ function updateColumns(obj) { viewUpdated.value = true if (!route.query.view) { - create_or_update_default_view() + createOrUpdateStandardView() } } @@ -767,7 +768,7 @@ async function updateKanbanSettings(data) { list.value.reload() if (!route.query.view) { - create_or_update_default_view() + createOrUpdateStandardView() } else if (!data.column_field) { if (isDirty) { $dialog({ @@ -779,14 +780,14 @@ async function updateKanbanSettings(data) { label: __('Update'), variant: 'solid', onClick: (close) => { - update_custom_view() + updateCustomView() close() }, }, ], }) } else { - update_custom_view() + updateCustomView() } } } @@ -810,11 +811,11 @@ function loadMoreKanban(columnName) { list.value.reload() } -function create_or_update_default_view() { +function createOrUpdateStandardView() { if (route.query.view) return view.value.doctype = props.doctype call( - 'crm.fcrm.doctype.crm_view_settings.crm_view_settings.create_or_update_default_view', + 'crm.fcrm.doctype.crm_view_settings.crm_view_settings.create_or_update_standard_view', { view: view.value, }, @@ -841,7 +842,7 @@ function create_or_update_default_view() { }) } -function update_custom_view() { +function updateCustomView() { viewUpdated.value = false view.value = { doctype: props.doctype, @@ -887,12 +888,24 @@ function updatePageLength(value, loadMore = false) { // View Actions const viewActions = (view) => { - let isDefault = typeof view.name === 'string' + let isStandard = typeof view.name === 'string' let _view = getView(view.name) + if (isStandard) { + _view = getView(null, view.name, props.doctype) + } + + if (!_view) { + _view = { + label: view.label, + type: view.name, + dt: props.doctype, + } + } + let actions = [ { - group: __('Default Views'), + group: __('Actions'), hideLabel: true, items: [ { @@ -904,7 +917,15 @@ const viewActions = (view) => { }, ] - if (!isDefault && (!_view.public || isManager())) { + if (!isDefaultView(_view, isStandard)) { + actions[0].items.unshift({ + label: __('Set as default'), + icon: () => h(CheckIcon, { class: 'h-4 w-4' }), + onClick: () => setAsDefault(_view), + }) + } + + if (!isStandard && (!_view.public || isManager())) { actions[0].items.push({ label: __('Edit'), icon: () => h(EditIcon, { class: 'h-4 w-4' }), @@ -961,6 +982,14 @@ const viewActions = (view) => { return actions } +function isDefaultView(v, isStandard) { + let defaultView = getDefaultView() + + if (!defaultView || (isStandard && !v.name)) return false + + return defaultView.name == v.name +} + const viewModalObj = ref({}) function createView() { @@ -972,6 +1001,17 @@ function createView() { showViewModal.value = true } +function setAsDefault(v) { + call('crm.fcrm.doctype.crm_view_settings.crm_view_settings.set_as_default', { + name: v.name, + type: v.type, + doctype: v.dt, + }).then(() => { + reloadView() + list.value.reload() + }) +} + function duplicateView(v) { v.label = v.label + __(' (New)') viewModalObj.value = v diff --git a/frontend/src/router.js b/frontend/src/router.js index 7ce0f110..43ff0e33 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -1,11 +1,11 @@ import { createRouter, createWebHistory } from 'vue-router' import { userResource } from '@/stores/user' import { sessionStore } from '@/stores/session' +import { viewsStore } from '@/stores/views' const routes = [ { path: '/', - redirect: { name: 'Leads' }, name: 'Home', }, { @@ -113,7 +113,23 @@ router.beforeEach(async (to, from, next) => { isLoggedIn && (await userResource.promise) if (to.name === 'Home' && isLoggedIn) { - next({ name: 'Leads' }) + const { views, getDefaultView } = viewsStore() + await views.promise + + let defaultView = getDefaultView() + if (!defaultView) { + next({ name: 'Leads' }) + return + } + + let { route_name, type, name, is_standard } = defaultView + route_name = route_name || 'Leads' + + if (name && !is_standard) { + next({ name: route_name, params: { viewType: type }, query: { name } }) + } else { + next({ name: route_name, params: { viewType: type } }) + } } else if (!isLoggedIn) { window.location.href = '/login?redirect-to=/crm' } else if (to.matched.length === 0) { diff --git a/frontend/src/stores/views.js b/frontend/src/stores/views.js index 487870ce..097d2706 100644 --- a/frontend/src/stores/views.js +++ b/frontend/src/stores/views.js @@ -6,7 +6,8 @@ export const viewsStore = defineStore('crm-views', (doctype) => { let viewsByName = reactive({}) let pinnedViews = ref([]) let publicViews = ref([]) - let defaultView = ref({}) + let standardViews = ref({}) + const defaultView = ref(null) // Views const views = createResource({ @@ -27,18 +28,25 @@ export const viewsStore = defineStore('crm-views', (doctype) => { if (view.public) { publicViews.value?.push(view) } - if (view.is_default && view.dt) { - defaultView.value[view.dt + ' ' + view.type] = view + if (view.is_standard && view.dt) { + standardViews.value[view.dt + ' ' + view.type] = view + } + if (view.is_default) { + defaultView.value = view } } return views }, }) + function getDefaultView() { + return defaultView.value + } + function getView(view, type, doctype = null) { type = type || 'list' if (!view && doctype) { - return defaultView.value[doctype + ' ' + type] || null + return standardViews.value[doctype + ' ' + type] || null } return viewsByName[view] } @@ -60,6 +68,8 @@ export const viewsStore = defineStore('crm-views', (doctype) => { return { views, defaultView, + standardViews, + getDefaultView, getPinnedViews, getPublicViews, reload, diff --git a/frontend/src/utils/view.js b/frontend/src/utils/view.js index a6db2fb2..d810eb70 100644 --- a/frontend/src/utils/view.js +++ b/frontend/src/utils/view.js @@ -6,7 +6,7 @@ import { markRaw } from 'vue' const { getView: getViewDetails } = viewsStore() -function defaultView(type) { +function standardView(type) { let types = { list: { label: __('List'), @@ -29,7 +29,7 @@ export function getView(view, type, doctype) { let viewType = type || 'list' let viewDetails = getViewDetails(view, viewType, doctype) if (viewDetails && !viewDetails.icon) { - viewDetails.icon = defaultView(viewType).icon + viewDetails.icon = standardView(viewType).icon } - return viewDetails || defaultView(viewType) + return viewDetails || standardView(viewType) }