From cd24337dfcc1b394ad33dbc47fe31bcc581f29ad Mon Sep 17 00:00:00 2001 From: zaqouttahir Date: Mon, 7 Jul 2025 13:13:04 +0300 Subject: [PATCH 01/79] fix: set fieldname to handle edit value modal (cherry picked from commit c104b1b8b40d78e11d5a181c046bc4f77e40bb74) --- frontend/src/components/Modals/EditValueModal.vue | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Modals/EditValueModal.vue b/frontend/src/components/Modals/EditValueModal.vue index 76b28a75..59977ad4 100644 --- a/frontend/src/components/Modals/EditValueModal.vue +++ b/frontend/src/components/Modals/EditValueModal.vue @@ -37,7 +37,13 @@ import Link from '@/components/Controls/Link.vue' import Autocomplete from '@/components/frappe-ui/Autocomplete.vue' import { capture } from '@/telemetry' -import { FormControl, call, createResource, TextEditor, DatePicker } from 'frappe-ui' +import { + FormControl, + call, + createResource, + TextEditor, + DatePicker, +} from 'frappe-ui' import { ref, computed, onMounted, h } from 'vue' const typeCheck = ['Check'] @@ -70,7 +76,7 @@ const fields = createResource({ }, transform: (data) => { return data.filter((f) => f.hidden == 0 && f.read_only == 0) - } + }, }) onMounted(() => { @@ -103,7 +109,7 @@ function updateValues() { docnames: Array.from(props.selectedValues), action: 'update', data: { - [field.value.value]: fieldVal || null, + [field.value.fieldname]: fieldVal || null, }, } ).then(() => { From ca15b5e3b84d7754761b399170b9a66430c63ffb Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 8 Jul 2025 19:16:58 +0530 Subject: [PATCH 02/79] fix: fieldtype of value is not changing based on selected field's fieldtype (cherry picked from commit 27f87883f75fb262c73b9ce0b49ba607868e5b69) --- .../src/components/Modals/EditValueModal.vue | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/Modals/EditValueModal.vue b/frontend/src/components/Modals/EditValueModal.vue index 59977ad4..76c6e791 100644 --- a/frontend/src/components/Modals/EditValueModal.vue +++ b/frontend/src/components/Modals/EditValueModal.vue @@ -88,8 +88,8 @@ const recordCount = computed(() => props.selectedValues?.size || 0) const field = ref({ label: '', - type: '', - value: '', + fieldtype: '', + fieldname: '', options: '', }) @@ -98,7 +98,7 @@ const loading = ref(false) function updateValues() { let fieldVal = newValue.value - if (field.value.type == 'Check') { + if (field.value.fieldtype == 'Check') { fieldVal = fieldVal == 'Yes' ? 1 : 0 } loading.value = true @@ -111,12 +111,12 @@ function updateValues() { data: { [field.value.fieldname]: fieldVal || null, }, - } + }, ).then(() => { field.value = { label: '', - type: '', - value: '', + fieldtype: '', + fieldname: '', options: '', } newValue.value = '' @@ -143,9 +143,10 @@ function getSelectOptions(options) { } function getValueComponent(f) { - const { type, options } = f - if (typeSelect.includes(type) || typeCheck.includes(type)) { - const _options = type == 'Check' ? ['Yes', 'No'] : getSelectOptions(options) + const { fieldtype, options } = f + if (typeSelect.includes(fieldtype) || typeCheck.includes(fieldtype)) { + const _options = + fieldtype == 'Check' ? ['Yes', 'No'] : getSelectOptions(options) return h(FormControl, { type: 'select', options: _options.map((o) => ({ @@ -154,16 +155,16 @@ function getValueComponent(f) { })), modelValue: newValue.value, }) - } else if (typeLink.includes(type)) { - if (type == 'Dynamic Link') { + } else if (typeLink.includes(fieldtype)) { + if (fieldtype == 'Dynamic Link') { return h(FormControl, { type: 'text' }) } return h(Link, { class: 'form-control', doctype: options }) - } else if (typeNumber.includes(type)) { + } else if (typeNumber.includes(fieldtype)) { return h(FormControl, { type: 'number' }) - } else if (typeDate.includes(type)) { + } else if (typeDate.includes(fieldtype)) { return h(DatePicker) - } else if (typeEditor.includes(type)) { + } else if (typeEditor.includes(fieldtype)) { return h(TextEditor, { variant: 'outline', editorClass: From 1f8e3f3802580a6e25e4beadc3f686342a35b64b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 28 Jun 2025 10:55:13 +0530 Subject: [PATCH 03/79] feat: initialize dashboard boilerplate (cherry picked from commit 62d5c2a91f5f97b2483bbfb94b31190ba382de54) --- .../src/components/Layouts/AppSidebar.vue | 8 + frontend/src/pages/Dashboard.vue | 312 +++++++++++++++++- frontend/src/router.js | 5 + 3 files changed, 316 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/Layouts/AppSidebar.vue b/frontend/src/components/Layouts/AppSidebar.vue index e78fa0dc..c223e8af 100644 --- a/frontend/src/components/Layouts/AppSidebar.vue +++ b/frontend/src/components/Layouts/AppSidebar.vue @@ -8,6 +8,13 @@
+ - - - +
+ + + + + +
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
- diff --git a/frontend/src/router.js b/frontend/src/router.js index 93e4743f..62385030 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -13,6 +13,11 @@ const routes = [ name: 'Notifications', component: () => import('@/pages/MobileNotification.vue'), }, + { + path: '/dashboard', + name: 'Dashboard', + component: () => import('@/pages/Dashboard.vue'), + }, { alias: '/leads', path: '/leads/view/:viewType?', From 0302e9958bdaac9ecb1e17f1eb41494109916dc8 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 30 Jun 2025 12:56:47 +0530 Subject: [PATCH 04/79] fix: added breadcrumb and made header sticky (cherry picked from commit 9949478b362c77690507bf7d921dd3d60d2ead07) --- frontend/src/pages/Dashboard.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/Dashboard.vue b/frontend/src/pages/Dashboard.vue index 5683f4fd..534410bb 100644 --- a/frontend/src/pages/Dashboard.vue +++ b/frontend/src/pages/Dashboard.vue @@ -1,10 +1,9 @@ From 6caff5cd59f7c5c0026070924a9aae16155af6f1 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 8 Jul 2025 12:21:12 +0530 Subject: [PATCH 11/79] fix: added filters and translated titles (cherry picked from commit 4b12918ba5941ca34fc4149bb43354f8344513f8) --- crm/api/dashboard.py | 167 ++++---- .../src/components/frappe-ui/Autocomplete.vue | 2 +- frontend/src/pages/Dashboard.vue | 362 ++++++++++++++---- frontend/src/utils/dashboard.ts | 28 ++ 4 files changed, 423 insertions(+), 136 deletions(-) create mode 100644 frontend/src/utils/dashboard.ts diff --git a/crm/api/dashboard.py b/crm/api/dashboard.py index 255de505..de7f86ec 100644 --- a/crm/api/dashboard.py +++ b/crm/api/dashboard.py @@ -3,16 +3,19 @@ from frappe import _ @frappe.whitelist() -def get_number_card_data(from_date="", to_date="", lead_conds="", deal_conds=""): +def get_number_card_data(from_date="", to_date="", user="", lead_conds="", deal_conds=""): """ Get number card data for the dashboard. """ + if not from_date or not to_date: + from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) + to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) - lead_chart_data = get_lead_count(from_date, to_date, lead_conds) - deal_chart_data = get_deal_count(from_date, to_date, deal_conds) - get_won_deal_count_data = get_won_deal_count(from_date, to_date, deal_conds) - get_average_deal_value_data = get_average_deal_value(from_date, to_date, deal_conds) - get_average_time_to_close_data = get_average_time_to_close(from_date, to_date, deal_conds) + lead_chart_data = get_lead_count(from_date, to_date, user, lead_conds) + deal_chart_data = get_deal_count(from_date, to_date, user, deal_conds) + get_won_deal_count_data = get_won_deal_count(from_date, to_date, user, deal_conds) + get_average_deal_value_data = get_average_deal_value(from_date, to_date, user, deal_conds) + get_average_time_to_close_data = get_average_time_to_close(from_date, to_date, user, deal_conds) return [ lead_chart_data, @@ -23,19 +26,18 @@ def get_number_card_data(from_date="", to_date="", lead_conds="", deal_conds="") ] -def get_lead_count(from_date, to_date, conds="", return_result=False): +def get_lead_count(from_date, to_date, user="", conds="", return_result=False): """ Get lead count for the dashboard. """ - if not from_date or not to_date: - from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) - to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) - diff = frappe.utils.date_diff(to_date, from_date) if diff == 0: diff = 1 + if user: + conds += f" AND lead_owner = '{user}'" + result = frappe.db.sql( f""" SELECT @@ -73,28 +75,27 @@ def get_lead_count(from_date, to_date, conds="", return_result=False): ) return { - "title": "Total Leads", + "title": _("Total Leads"), "value": current_month_leads, "delta": delta_in_percentage, "deltaSuffix": "%", "negativeIsBetter": False, - "tooltip": "Total number of leads created", + "tooltip": _("Total number of leads created"), } -def get_deal_count(from_date, to_date, conds="", return_result=False): +def get_deal_count(from_date, to_date, user="", conds="", return_result=False): """ Get deal count for the dashboard. """ - if not from_date or not to_date: - from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) - to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) - diff = frappe.utils.date_diff(to_date, from_date) if diff == 0: diff = 1 + if user: + conds += f" AND deal_owner = '{user}'" + result = frappe.db.sql( f""" SELECT @@ -132,28 +133,27 @@ def get_deal_count(from_date, to_date, conds="", return_result=False): ) return { - "title": "Total Deals", + "title": _("Total Deals"), "value": current_month_deals, "delta": delta_in_percentage, "deltaSuffix": "%", "negativeIsBetter": False, - "tooltip": "Total number of deals created", + "tooltip": _("Total number of deals created"), } -def get_won_deal_count(from_date, to_date, conds="", return_result=False): +def get_won_deal_count(from_date, to_date, user="", conds="", return_result=False): """ Get won deal count for the dashboard. """ - if not from_date or not to_date: - from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) - to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) - diff = frappe.utils.date_diff(to_date, from_date) if diff == 0: diff = 1 + if user: + conds += f" AND deal_owner = '{user}'" + result = frappe.db.sql( f""" SELECT @@ -191,28 +191,27 @@ def get_won_deal_count(from_date, to_date, conds="", return_result=False): ) return { - "title": "Won Deals", + "title": _("Won Deals"), "value": current_month_deals, "delta": delta_in_percentage, "deltaSuffix": "%", "negativeIsBetter": False, - "tooltip": "Total number of deals created", + "tooltip": _("Total number of deals created"), } -def get_average_deal_value(from_date, to_date, conds="", return_result=False): +def get_average_deal_value(from_date, to_date, user="", conds="", return_result=False): """ Get average deal value for the dashboard. """ - if not from_date or not to_date: - from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) - to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) - diff = frappe.utils.date_diff(to_date, from_date) if diff == 0: diff = 1 + if user: + conds += f" AND deal_owner = '{user}'" + result = frappe.db.sql( f""" SELECT @@ -245,9 +244,9 @@ def get_average_deal_value(from_date, to_date, conds="", return_result=False): delta = current_month_avg - prev_month_avg if prev_month_avg else 0 return { - "title": "Avg Deal Value", + "title": _("Avg Deal Value"), "value": current_month_avg, - "tooltip": "Average value of deals created", + "tooltip": _("Average value of deals created"), # "prefix": "$", # "suffix": "K", "delta": delta, @@ -255,19 +254,18 @@ def get_average_deal_value(from_date, to_date, conds="", return_result=False): } -def get_average_time_to_close(from_date, to_date, conds="", return_result=False): +def get_average_time_to_close(from_date, to_date, user="", conds="", return_result=False): """ Get average time to close deals for the dashboard. """ - if not from_date or not to_date: - from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) - to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) - diff = frappe.utils.date_diff(to_date, from_date) if diff == 0: diff = 1 + if user: + conds += f" AND d.deal_owner = '{user}'" + prev_from_date = frappe.utils.add_days(from_date, -diff) prev_to_date = from_date @@ -300,9 +298,9 @@ def get_average_time_to_close(from_date, to_date, conds="", return_result=False) delta = current_avg - prev_avg if prev_avg else 0 return { - "title": "Avg Time to Close", + "title": _("Avg Time to Close"), "value": current_avg, - "tooltip": "Average time taken to close deals", + "tooltip": _("Average time taken to close deals"), "suffix": " days", "delta": delta, "deltaSuffix": " days", @@ -311,7 +309,7 @@ def get_average_time_to_close(from_date, to_date, conds="", return_result=False) @frappe.whitelist() -def get_sales_trend_data(from_date="", to_date="", lead_conds="", deal_conds=""): +def get_sales_trend_data(from_date="", to_date="", user="", lead_conds="", deal_conds=""): """ Get sales trend data for the dashboard. [ @@ -325,8 +323,12 @@ def get_sales_trend_data(from_date="", to_date="", lead_conds="", deal_conds="") from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) + if user: + lead_conds += f" AND lead_owner = '{user}'" + deal_conds += f" AND deal_owner = '{user}'" + result = frappe.db.sql( - """ + f""" SELECT DATE_FORMAT(date, '%%Y-%%m-%%d') AS date, SUM(leads) AS leads, @@ -340,7 +342,7 @@ def get_sales_trend_data(from_date="", to_date="", lead_conds="", deal_conds="") 0 AS won_deals FROM `tabCRM Lead` WHERE DATE(creation) BETWEEN %(from)s AND %(to)s - %(lead_conds)s + {lead_conds} GROUP BY DATE(creation) UNION ALL @@ -352,13 +354,13 @@ def get_sales_trend_data(from_date="", to_date="", lead_conds="", deal_conds="") SUM(CASE WHEN status = 'Won' THEN 1 ELSE 0 END) AS won_deals FROM `tabCRM Deal` WHERE DATE(creation) BETWEEN %(from)s AND %(to)s - %(deal_conds)s + {deal_conds} GROUP BY DATE(creation) ) AS daily GROUP BY date ORDER BY date """, - {"from": from_date, "to": to_date, "lead_conds": lead_conds, "deal_conds": deal_conds}, + {"from": from_date, "to": to_date}, as_dict=True, ) @@ -374,7 +376,7 @@ def get_sales_trend_data(from_date="", to_date="", lead_conds="", deal_conds="") @frappe.whitelist() -def get_deals_by_salesperson(from_date="", to_date="", deal_conds=""): +def get_deals_by_salesperson(from_date="", to_date="", user="", deal_conds=""): """ Get deal data by salesperson for the dashboard. [ @@ -388,8 +390,11 @@ def get_deals_by_salesperson(from_date="", to_date="", deal_conds=""): from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) + if user: + deal_conds += f" AND d.deal_owner = '{user}'" + result = frappe.db.sql( - """ + f""" SELECT IFNULL(u.full_name, d.deal_owner) AS salesperson, COUNT(*) AS deals, @@ -397,11 +402,11 @@ def get_deals_by_salesperson(from_date="", to_date="", deal_conds=""): FROM `tabCRM Deal` AS d LEFT JOIN `tabUser` AS u ON u.name = d.deal_owner WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s - %(deal_conds)s + {deal_conds} GROUP BY d.deal_owner ORDER BY value DESC """, - {"from": from_date, "to": to_date, "deal_conds": deal_conds}, + {"from": from_date, "to": to_date}, as_dict=True, ) @@ -409,7 +414,7 @@ def get_deals_by_salesperson(from_date="", to_date="", deal_conds=""): @frappe.whitelist() -def get_deals_by_territory(from_date="", to_date="", deal_conds=""): +def get_deals_by_territory(from_date="", to_date="", user="", deal_conds=""): """ Get deal data by territory for the dashboard. [ @@ -423,19 +428,22 @@ def get_deals_by_territory(from_date="", to_date="", deal_conds=""): from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) + if user: + deal_conds += f" AND d.deal_owner = '{user}'" + result = frappe.db.sql( - """ + f""" SELECT IFNULL(d.territory, 'Empty') AS territory, COUNT(*) AS deals, SUM(COALESCE(d.deal_value, 0)) AS value FROM `tabCRM Deal` AS d WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s - %(deal_conds)s + {deal_conds} GROUP BY d.territory ORDER BY value DESC """, - {"from": from_date, "to": to_date, "deal_conds": deal_conds}, + {"from": from_date, "to": to_date}, as_dict=True, ) @@ -443,7 +451,7 @@ def get_deals_by_territory(from_date="", to_date="", deal_conds=""): @frappe.whitelist() -def get_lost_deal_reasons(from_date="", to_date="", deal_conds=""): +def get_lost_deal_reasons(from_date="", to_date="", user="", deal_conds=""): """ Get lost deal reasons for the dashboard. [ @@ -452,23 +460,27 @@ def get_lost_deal_reasons(from_date="", to_date="", deal_conds=""): ... ] """ + if not from_date or not to_date: from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) + if user: + deal_conds += f" AND d.deal_owner = '{user}'" + result = frappe.db.sql( - """ + f""" SELECT d.lost_reason AS reason, COUNT(*) AS count FROM `tabCRM Deal` AS d WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s AND d.status = 'Lost' - %(deal_conds)s + {deal_conds} GROUP BY d.lost_reason HAVING reason IS NOT NULL AND reason != '' ORDER BY count DESC """, - {"from": from_date, "to": to_date, "deal_conds": deal_conds}, + {"from": from_date, "to": to_date}, as_dict=True, ) @@ -476,7 +488,7 @@ def get_lost_deal_reasons(from_date="", to_date="", deal_conds=""): @frappe.whitelist() -def get_forecasted_revenue(deal_conds=""): +def get_forecasted_revenue(user="", deal_conds=""): """ Get forecasted revenue for the dashboard. [ @@ -488,6 +500,9 @@ def get_forecasted_revenue(deal_conds=""): ] """ + if user: + deal_conds += f" AND deal_owner = '{user}'" + result = frappe.db.sql( f""" SELECT @@ -523,7 +538,7 @@ def get_forecasted_revenue(deal_conds=""): @frappe.whitelist() -def get_funnel_conversion_data(from_date="", to_date="", lead_conds="", deal_conds=""): +def get_funnel_conversion_data(from_date="", to_date="", user="", lead_conds="", deal_conds=""): """ Get funnel conversion data for the dashboard. [ @@ -540,16 +555,20 @@ def get_funnel_conversion_data(from_date="", to_date="", lead_conds="", deal_con from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) + if user: + lead_conds += f" AND lead_owner = '{user}'" + deal_conds += f" AND deal_owner = '{user}'" + result = [] # Get total leads total_leads = frappe.db.sql( - """ SELECT COUNT(*) AS count + f""" SELECT COUNT(*) AS count FROM `tabCRM Lead` WHERE DATE(creation) BETWEEN %(from)s AND %(to)s - %(lead_conds)s + {lead_conds} """, - {"from": from_date, "to": to_date, "lead_conds": lead_conds}, + {"from": from_date, "to": to_date}, as_dict=True, ) total_leads_count = total_leads[0].count if total_leads else 0 @@ -580,7 +599,7 @@ def get_funnel_conversion_data(from_date="", to_date="", lead_conds="", deal_con @frappe.whitelist() -def get_deals_by_stage(from_date="", to_date="", deal_conds=""): +def get_deals_by_stage(from_date="", to_date="", user="", deal_conds=""): """ Get deal data by stage for the dashboard. [ @@ -594,18 +613,21 @@ def get_deals_by_stage(from_date="", to_date="", deal_conds=""): from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) + if user: + deal_conds += f" AND d.deal_owner = '{user}'" + result = frappe.db.sql( - """ + f""" SELECT d.status AS stage, COUNT(*) AS count FROM `tabCRM Deal` AS d WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s - %(deal_conds)s + {deal_conds} GROUP BY d.status ORDER BY count DESC """, - {"from": from_date, "to": to_date, "deal_conds": deal_conds}, + {"from": from_date, "to": to_date}, as_dict=True, ) @@ -613,7 +635,7 @@ def get_deals_by_stage(from_date="", to_date="", deal_conds=""): @frappe.whitelist() -def get_leads_by_source(from_date="", to_date="", lead_conds=""): +def get_leads_by_source(from_date="", to_date="", user="", lead_conds=""): """ Get lead data by source for the dashboard. [ @@ -627,18 +649,21 @@ def get_leads_by_source(from_date="", to_date="", lead_conds=""): from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) + if user: + lead_conds += f" AND lead_owner = '{user}'" + result = frappe.db.sql( - """ + f""" SELECT IFNULL(source, 'Empty') AS source, COUNT(*) AS count FROM `tabCRM Lead` WHERE DATE(creation) BETWEEN %(from)s AND %(to)s - %(lead_conds)s + {lead_conds} GROUP BY source ORDER BY count DESC """, - {"from": from_date, "to": to_date, "lead_conds": lead_conds}, + {"from": from_date, "to": to_date}, as_dict=True, ) diff --git a/frontend/src/components/frappe-ui/Autocomplete.vue b/frontend/src/components/frappe-ui/Autocomplete.vue index 3c7371f8..2ef7f5f3 100644 --- a/frontend/src/components/frappe-ui/Autocomplete.vue +++ b/frontend/src/components/frappe-ui/Autocomplete.vue @@ -14,7 +14,7 @@ >