diff --git a/crm/api/dashboard.py b/crm/api/dashboard.py
index da3eaada..5a9c56b8 100644
--- a/crm/api/dashboard.py
+++ b/crm/api/dashboard.py
@@ -1,14 +1,54 @@
+import json
+
import frappe
from frappe import _
+from crm.fcrm.doctype.crm_dashboard.crm_dashboard import create_default_manager_dashboard
from crm.utils import sales_user_only
@frappe.whitelist()
@sales_user_only
-def get_number_card_data(from_date="", to_date="", user="", lead_conds="", deal_conds=""):
+def get_dashboard(from_date="", to_date="", user=""):
"""
- Get number card data for the dashboard.
+ Get the dashboard data for the CRM 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())
+
+ roles = frappe.get_roles(frappe.session.user)
+ is_sales_user = "Sales User" in roles and "Sales Manager" not in roles and "System Manager" not in roles
+ if is_sales_user and not user:
+ user = frappe.session.user
+
+ dashboard = frappe.db.exists("CRM Dashboard", "Manager Dashboard")
+
+ layout = []
+
+ if not dashboard:
+ layout = json.loads(create_default_manager_dashboard())
+ frappe.db.commit()
+ else:
+ layout = json.loads(frappe.db.get_value("CRM Dashboard", "Manager Dashboard", "layout") or "[]")
+
+ for l in layout:
+ method_name = f"get_{l['name']}"
+ if hasattr(frappe.get_attr("crm.api.dashboard"), method_name):
+ method = getattr(frappe.get_attr("crm.api.dashboard"), method_name)
+ l["data"] = method(from_date, to_date, user)
+ else:
+ l["data"] = None
+
+ return layout
+
+
+@frappe.whitelist()
+@sales_user_only
+def get_chart(name, type, from_date="", to_date="", user=""):
+ """
+ Get number chart 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())
@@ -19,31 +59,19 @@ def get_number_card_data(from_date="", to_date="", user="", lead_conds="", deal_
if is_sales_user and not user:
user = frappe.session.user
- lead_count = get_lead_count(from_date, to_date, user, lead_conds)
- ongoing_deal_count = get_ongoing_deal_count(from_date, to_date, user, deal_conds)["count"]
- average_ongoing_deal_value = get_ongoing_deal_count(from_date, to_date, user, deal_conds)["average"]
- won_deal_count = get_won_deal_count(from_date, to_date, user, deal_conds)["count"]
- average_won_deal_value = get_won_deal_count(from_date, to_date, user, deal_conds)["average"]
- average_deal_value = get_average_deal_value(from_date, to_date, user, deal_conds)
- average_time_to_close_a_lead = get_average_time_to_close(from_date, to_date, user, deal_conds)["lead"]
- average_time_to_close_a_deal = get_average_time_to_close(from_date, to_date, user, deal_conds)["deal"]
-
- return [
- lead_count,
- ongoing_deal_count,
- average_ongoing_deal_value,
- won_deal_count,
- average_won_deal_value,
- average_deal_value,
- average_time_to_close_a_lead,
- average_time_to_close_a_deal,
- ]
+ method_name = f"get_{name}"
+ if hasattr(frappe.get_attr("crm.api.dashboard"), method_name):
+ method = getattr(frappe.get_attr("crm.api.dashboard"), method_name)
+ return method(from_date, to_date, user)
+ else:
+ return {"error": _("Invalid chart name")}
-def get_lead_count(from_date, to_date, user="", conds="", return_result=False):
+def get_total_leads(from_date, to_date, user=""):
"""
Get lead count for the dashboard.
"""
+ conds = ""
diff = frappe.utils.date_diff(to_date, from_date)
if diff == 0:
@@ -78,9 +106,6 @@ def get_lead_count(from_date, to_date, user="", conds="", return_result=False):
as_dict=1,
)
- if return_result:
- return result
-
current_month_leads = result[0].current_month_leads or 0
prev_month_leads = result[0].prev_month_leads or 0
@@ -90,17 +115,18 @@ def get_lead_count(from_date, to_date, user="", conds="", return_result=False):
return {
"title": _("Total leads"),
+ "tooltip": _("Total number of leads"),
"value": current_month_leads,
"delta": delta_in_percentage,
"deltaSuffix": "%",
- "tooltip": _("Total number of leads"),
}
-def get_ongoing_deal_count(from_date, to_date, user="", conds="", return_result=False):
+def get_ongoing_deals(from_date, to_date, user=""):
"""
Get ongoing deal count for the dashboard, and also calculate average deal value for ongoing deals.
"""
+ conds = ""
diff = frappe.utils.date_diff(to_date, from_date)
if diff == 0:
@@ -126,8 +152,50 @@ def get_ongoing_deal_count(from_date, to_date, user="", conds="", return_result=
{conds}
THEN d.name
ELSE NULL
- END) as prev_month_deals,
+ END) as prev_month_deals
+ FROM `tabCRM Deal` d
+ JOIN `tabCRM Deal Status` s ON d.status = s.name
+ """,
+ {
+ "from_date": from_date,
+ "to_date": to_date,
+ "prev_from_date": frappe.utils.add_days(from_date, -diff),
+ },
+ as_dict=1,
+ )
+ current_month_deals = result[0].current_month_deals or 0
+ prev_month_deals = result[0].prev_month_deals or 0
+
+ delta_in_percentage = (
+ (current_month_deals - prev_month_deals) / prev_month_deals * 100 if prev_month_deals else 0
+ )
+
+ return {
+ "title": _("Ongoing deals"),
+ "tooltip": _("Total number of non won/lost deals"),
+ "value": current_month_deals,
+ "delta": delta_in_percentage,
+ "deltaSuffix": "%",
+ }
+
+
+def get_average_ongoing_deal_value(from_date, to_date, user=""):
+ """
+ Get ongoing deal count for the dashboard, and also calculate average deal value for ongoing deals.
+ """
+ conds = ""
+
+ diff = frappe.utils.date_diff(to_date, from_date)
+ if diff == 0:
+ diff = 1
+
+ if user:
+ conds += f" AND d.deal_owner = '{user}'"
+
+ result = frappe.db.sql(
+ f"""
+ SELECT
AVG(CASE
WHEN d.creation >= %(from_date)s AND d.creation < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
AND s.type NOT IN ('Won', 'Lost')
@@ -154,39 +222,21 @@ def get_ongoing_deal_count(from_date, to_date, user="", conds="", return_result=
as_dict=1,
)
- if return_result:
- return result
-
- current_month_deals = result[0].current_month_deals or 0
- prev_month_deals = result[0].prev_month_deals or 0
current_month_avg_value = result[0].current_month_avg_value or 0
prev_month_avg_value = result[0].prev_month_avg_value or 0
- delta_in_percentage = (
- (current_month_deals - prev_month_deals) / prev_month_deals * 100 if prev_month_deals else 0
- )
avg_value_delta = current_month_avg_value - prev_month_avg_value if prev_month_avg_value else 0
return {
- "count": {
- "title": _("Ongoing deals"),
- "value": current_month_deals,
- "delta": delta_in_percentage,
- "deltaSuffix": "%",
- "tooltip": _("Total number of ongoing deals"),
- },
- "average": {
- "title": _("Avg ongoing deal value"),
- "value": current_month_avg_value,
- "delta": avg_value_delta,
- "prefix": get_base_currency_symbol(),
- # "suffix": "K",
- "tooltip": _("Average deal value of ongoing deals"),
- },
+ "title": _("Avg. ongoing deal value"),
+ "tooltip": _("Average deal value of non won/lost deals"),
+ "value": current_month_avg_value,
+ "delta": avg_value_delta,
+ "prefix": get_base_currency_symbol(),
}
-def get_won_deal_count(from_date, to_date, user="", conds="", return_result=False):
+def get_won_deals(from_date, to_date, user=""):
"""
Get won deal count for the dashboard, and also calculate average deal value for won deals.
"""
@@ -195,6 +245,8 @@ def get_won_deal_count(from_date, to_date, user="", conds="", return_result=Fals
if diff == 0:
diff = 1
+ conds = ""
+
if user:
conds += f" AND d.deal_owner = '{user}'"
@@ -215,8 +267,51 @@ def get_won_deal_count(from_date, to_date, user="", conds="", return_result=Fals
{conds}
THEN d.name
ELSE NULL
- END) as prev_month_deals,
+ END) as prev_month_deals
+ FROM `tabCRM Deal` d
+ JOIN `tabCRM Deal Status` s ON d.status = s.name
+ """,
+ {
+ "from_date": from_date,
+ "to_date": to_date,
+ "prev_from_date": frappe.utils.add_days(from_date, -diff),
+ },
+ as_dict=1,
+ )
+ current_month_deals = result[0].current_month_deals or 0
+ prev_month_deals = result[0].prev_month_deals or 0
+
+ delta_in_percentage = (
+ (current_month_deals - prev_month_deals) / prev_month_deals * 100 if prev_month_deals else 0
+ )
+
+ return {
+ "title": _("Won deals"),
+ "tooltip": _("Total number of won deals based on its closure date"),
+ "value": current_month_deals,
+ "delta": delta_in_percentage,
+ "deltaSuffix": "%",
+ }
+
+
+def get_average_won_deal_value(from_date, to_date, user=""):
+ """
+ Get won deal count for the dashboard, and also calculate average deal value for won deals.
+ """
+
+ diff = frappe.utils.date_diff(to_date, from_date)
+ if diff == 0:
+ diff = 1
+
+ conds = ""
+
+ if user:
+ conds += f" AND d.deal_owner = '{user}'"
+
+ result = frappe.db.sql(
+ f"""
+ SELECT
AVG(CASE
WHEN d.closed_date >= %(from_date)s AND d.closed_date < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
AND s.type = 'Won'
@@ -243,39 +338,21 @@ def get_won_deal_count(from_date, to_date, user="", conds="", return_result=Fals
as_dict=1,
)
- if return_result:
- return result
-
- current_month_deals = result[0].current_month_deals or 0
- prev_month_deals = result[0].prev_month_deals or 0
current_month_avg_value = result[0].current_month_avg_value or 0
prev_month_avg_value = result[0].prev_month_avg_value or 0
- delta_in_percentage = (
- (current_month_deals - prev_month_deals) / prev_month_deals * 100 if prev_month_deals else 0
- )
avg_value_delta = current_month_avg_value - prev_month_avg_value if prev_month_avg_value else 0
return {
- "count": {
- "title": _("Won deals"),
- "value": current_month_deals,
- "delta": delta_in_percentage,
- "deltaSuffix": "%",
- "tooltip": _("Total number of won deals based on its closure date"),
- },
- "average": {
- "title": _("Avg won deal value"),
- "value": current_month_avg_value,
- "delta": avg_value_delta,
- "prefix": get_base_currency_symbol(),
- # "suffix": "K",
- "tooltip": _("Average deal value of won deals"),
- },
+ "title": _("Avg. won deal value"),
+ "tooltip": _("Average deal value of won deals"),
+ "value": current_month_avg_value,
+ "delta": avg_value_delta,
+ "prefix": get_base_currency_symbol(),
}
-def get_average_deal_value(from_date, to_date, user="", conds="", return_result=False):
+def get_average_deal_value(from_date, to_date, user=""):
"""
Get average deal value for the dashboard.
"""
@@ -284,6 +361,8 @@ def get_average_deal_value(from_date, to_date, user="", conds="", return_result=
if diff == 0:
diff = 1
+ conds = ""
+
if user:
conds += f" AND d.deal_owner = '{user}'"
@@ -322,28 +401,26 @@ def get_average_deal_value(from_date, to_date, user="", conds="", return_result=
delta = current_month_avg - prev_month_avg if prev_month_avg else 0
return {
- "title": _("Avg deal value"),
- "value": current_month_avg,
+ "title": _("Avg. deal value"),
"tooltip": _("Average deal value of ongoing & won deals"),
+ "value": current_month_avg,
"prefix": get_base_currency_symbol(),
- # "suffix": "K",
"delta": delta,
"deltaSuffix": "%",
}
-def get_average_time_to_close(from_date, to_date, user="", conds="", return_result=False):
+def get_average_time_to_close_a_lead(from_date, to_date, user=""):
"""
Get average time to close deals for the dashboard.
- Returns both:
- - Average time from lead creation to deal closure
- - Average time from deal creation to deal closure
"""
diff = frappe.utils.date_diff(to_date, from_date)
if diff == 0:
diff = 1
+ conds = ""
+
if user:
conds += f" AND d.deal_owner = '{user}'"
@@ -356,7 +433,57 @@ def get_average_time_to_close(from_date, to_date, user="", conds="", return_resu
AVG(CASE WHEN d.closed_date >= %(from_date)s AND d.closed_date < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
THEN TIMESTAMPDIFF(DAY, COALESCE(l.creation, d.creation), d.closed_date) END) as current_avg_lead,
AVG(CASE WHEN d.closed_date >= %(prev_from_date)s AND d.closed_date < %(prev_to_date)s
- THEN TIMESTAMPDIFF(DAY, COALESCE(l.creation, d.creation), d.closed_date) END) as prev_avg_lead,
+ THEN TIMESTAMPDIFF(DAY, COALESCE(l.creation, d.creation), d.closed_date) END) as prev_avg_lead
+ FROM `tabCRM Deal` AS d
+ JOIN `tabCRM Deal Status` s ON d.status = s.name
+ LEFT JOIN `tabCRM Lead` l ON d.lead = l.name
+ WHERE d.closed_date IS NOT NULL AND s.type = 'Won'
+ {conds}
+ """,
+ {
+ "from_date": from_date,
+ "to_date": to_date,
+ "prev_from_date": prev_from_date,
+ "prev_to_date": prev_to_date,
+ },
+ as_dict=1,
+ )
+
+ current_avg_lead = result[0].current_avg_lead or 0
+ prev_avg_lead = result[0].prev_avg_lead or 0
+ delta_lead = current_avg_lead - prev_avg_lead if prev_avg_lead else 0
+
+ return {
+ "title": _("Avg. time to close a lead"),
+ "tooltip": _("Average time taken from lead creation to deal closure"),
+ "value": current_avg_lead,
+ "suffix": " days",
+ "delta": delta_lead,
+ "deltaSuffix": " days",
+ "negativeIsBetter": True,
+ }
+
+
+def get_average_time_to_close_a_deal(from_date, to_date, user=""):
+ """
+ Get average time to close deals for the dashboard.
+ """
+
+ diff = frappe.utils.date_diff(to_date, from_date)
+ if diff == 0:
+ diff = 1
+
+ conds = ""
+
+ if user:
+ conds += f" AND d.deal_owner = '{user}'"
+
+ prev_from_date = frappe.utils.add_days(from_date, -diff)
+ prev_to_date = from_date
+
+ result = frappe.db.sql(
+ f"""
+ SELECT
AVG(CASE WHEN d.closed_date >= %(from_date)s AND d.closed_date < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
THEN TIMESTAMPDIFF(DAY, d.creation, d.closed_date) END) as current_avg_deal,
AVG(CASE WHEN d.closed_date >= %(prev_from_date)s AND d.closed_date < %(prev_to_date)s
@@ -376,41 +503,22 @@ def get_average_time_to_close(from_date, to_date, user="", conds="", return_resu
as_dict=1,
)
- if return_result:
- return result
-
- current_avg_lead = result[0].current_avg_lead or 0
- prev_avg_lead = result[0].prev_avg_lead or 0
- delta_lead = current_avg_lead - prev_avg_lead if prev_avg_lead else 0
-
current_avg_deal = result[0].current_avg_deal or 0
prev_avg_deal = result[0].prev_avg_deal or 0
delta_deal = current_avg_deal - prev_avg_deal if prev_avg_deal else 0
return {
- "lead": {
- "title": _("Avg time to close a lead"),
- "value": current_avg_lead,
- "tooltip": _("Average time taken from lead creation to deal closure"),
- "suffix": " days",
- "delta": delta_lead,
- "deltaSuffix": " days",
- "negativeIsBetter": True,
- },
- "deal": {
- "title": _("Avg time to close a deal"),
- "value": current_avg_deal,
- "tooltip": _("Average time taken from deal creation to deal closure"),
- "suffix": " days",
- "delta": delta_deal,
- "deltaSuffix": " days",
- "negativeIsBetter": True,
- },
+ "title": _("Avg. time to close a deal"),
+ "tooltip": _("Average time taken from deal creation to deal closure"),
+ "value": current_avg_deal,
+ "suffix": " days",
+ "delta": delta_deal,
+ "deltaSuffix": " days",
+ "negativeIsBetter": True,
}
-@frappe.whitelist()
-def get_sales_trend_data(from_date="", to_date="", user="", lead_conds="", deal_conds=""):
+def get_sales_trend(from_date="", to_date="", user=""):
"""
Get sales trend data for the dashboard.
[
@@ -420,6 +528,9 @@ def get_sales_trend_data(from_date="", to_date="", user="", lead_conds="", deal_
]
"""
+ lead_conds = ""
+ 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())
@@ -466,7 +577,7 @@ def get_sales_trend_data(from_date="", to_date="", user="", lead_conds="", deal_
as_dict=True,
)
- return [
+ sales_trend = [
{
"date": frappe.utils.get_datetime(row.date).strftime("%Y-%m-%d"),
"leads": row.leads or 0,
@@ -476,128 +587,28 @@ def get_sales_trend_data(from_date="", to_date="", user="", lead_conds="", deal_
for row in result
]
-
-@frappe.whitelist()
-def get_deals_by_salesperson(from_date="", to_date="", user="", deal_conds=""):
- """
- Get deal data by salesperson for the dashboard.
- [
- { salesperson: 'John Smith', deals: 45, value: 2300000 },
- { salesperson: 'Jane Doe', deals: 30, value: 1500000 },
- ...
- ]
- """
-
- 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
- IFNULL(u.full_name, d.deal_owner) AS salesperson,
- COUNT(*) AS deals,
- SUM(COALESCE(d.deal_value, 0) * IFNULL(d.exchange_rate, 1)) AS value
- 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}
- GROUP BY d.deal_owner
- ORDER BY value DESC
- """,
- {"from": from_date, "to": to_date},
- as_dict=True,
- )
-
return {
- "data": result or [],
- "currency_symbol": get_base_currency_symbol(),
+ "data": sales_trend,
+ "title": _("Sales trend"),
+ "subtitle": _("Daily performance of leads, deals, and wins"),
+ "xAxis": {
+ "title": _("Date"),
+ "key": "date",
+ "type": "time",
+ "timeGrain": "day",
+ },
+ "yAxis": {
+ "title": _("Count"),
+ },
+ "series": [
+ {"name": "leads", "type": "line", "showDataPoints": True},
+ {"name": "deals", "type": "line", "showDataPoints": True},
+ {"name": "won_deals", "type": "line", "showDataPoints": True},
+ ],
}
-@frappe.whitelist()
-def get_deals_by_territory(from_date="", to_date="", user="", deal_conds=""):
- """
- Get deal data by territory for the dashboard.
- [
- { territory: 'North America', deals: 45, value: 2300000 },
- { territory: 'Europe', deals: 30, value: 1500000 },
- ...
- ]
- """
-
- 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
- IFNULL(d.territory, 'Empty') AS territory,
- COUNT(*) AS deals,
- SUM(COALESCE(d.deal_value, 0) * IFNULL(d.exchange_rate, 1)) AS value
- FROM `tabCRM Deal` AS d
- WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s
- {deal_conds}
- GROUP BY d.territory
- ORDER BY value DESC
- """,
- {"from": from_date, "to": to_date},
- as_dict=True,
- )
-
- return {
- "data": result or [],
- "currency_symbol": get_base_currency_symbol(),
- }
-
-
-@frappe.whitelist()
-def get_lost_deal_reasons(from_date="", to_date="", user="", deal_conds=""):
- """
- Get lost deal reasons for the dashboard.
- [
- { reason: 'Price too high', count: 20 },
- { reason: 'Competitor won', count: 15 },
- ...
- ]
- """
-
- 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
- JOIN `tabCRM Deal Status` s ON d.status = s.name
- WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s AND s.type = 'Lost'
- {deal_conds}
- GROUP BY d.lost_reason
- HAVING reason IS NOT NULL AND reason != ''
- ORDER BY count DESC
- """,
- {"from": from_date, "to": to_date},
- as_dict=True,
- )
-
- return result or []
-
-
-@frappe.whitelist()
-def get_forecasted_revenue(user="", deal_conds=""):
+def get_forecasted_revenue(from_date="", to_date="", user=""):
"""
Get forecasted revenue for the dashboard.
[
@@ -608,6 +619,7 @@ def get_forecasted_revenue(user="", deal_conds=""):
...
]
"""
+ deal_conds = ""
if user:
deal_conds += f" AND d.deal_owner = '{user}'"
@@ -637,8 +649,6 @@ def get_forecasted_revenue(user="", deal_conds=""):
""",
as_dict=True,
)
- if not result:
- return []
for row in result:
row["month"] = frappe.utils.get_datetime(row["month"]).strftime("%Y-%m-01")
@@ -647,12 +657,25 @@ def get_forecasted_revenue(user="", deal_conds=""):
return {
"data": result or [],
- "currency_symbol": get_base_currency_symbol(),
+ "title": _("Forecasted revenue"),
+ "subtitle": _("Projected vs actual revenue based on deal probability"),
+ "xAxis": {
+ "title": _("Month"),
+ "key": "month",
+ "type": "time",
+ "timeGrain": "month",
+ },
+ "yAxis": {
+ "title": _("Revenue") + f" ({get_base_currency_symbol()})",
+ },
+ "series": [
+ {"name": "forecasted", "type": "line", "showDataPoints": True},
+ {"name": "actual", "type": "line", "showDataPoints": True},
+ ],
}
-@frappe.whitelist()
-def get_funnel_conversion_data(from_date="", to_date="", user="", lead_conds="", deal_conds=""):
+def get_funnel_conversion(from_date="", to_date="", user=""):
"""
Get funnel conversion data for the dashboard.
[
@@ -664,6 +687,8 @@ def get_funnel_conversion_data(from_date="", to_date="", user="", lead_conds="",
...
]
"""
+ lead_conds = ""
+ deal_conds = ""
if not from_date or not to_date:
from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate())
@@ -692,11 +717,32 @@ def get_funnel_conversion_data(from_date="", to_date="", user="", lead_conds="",
result += get_deal_status_change_counts(from_date, to_date, deal_conds)
- return result or []
+ return {
+ "data": result or [],
+ "title": _("Funnel conversion"),
+ "subtitle": _("Lead to deal conversion pipeline"),
+ "xAxis": {
+ "title": _("Stage"),
+ "key": "stage",
+ "type": "category",
+ },
+ "yAxis": {
+ "title": _("Count"),
+ },
+ "swapXY": True,
+ "series": [
+ {
+ "name": "count",
+ "type": "bar",
+ "echartOptions": {
+ "colorBy": "data",
+ },
+ },
+ ],
+ }
-@frappe.whitelist()
-def get_deals_by_stage(from_date="", to_date="", user="", deal_conds=""):
+def get_deals_by_stage_axis(from_date="", to_date="", user=""):
"""
Get deal data by stage for the dashboard.
[
@@ -705,6 +751,57 @@ def get_deals_by_stage(from_date="", to_date="", user="", deal_conds=""):
...
]
"""
+ 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.status AS stage,
+ COUNT(*) AS count,
+ s.type AS status_type
+ FROM `tabCRM Deal` AS d
+ JOIN `tabCRM Deal Status` s ON d.status = s.name
+ WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s AND s.type NOT IN ('Lost')
+ {deal_conds}
+ GROUP BY d.status
+ ORDER BY count DESC
+ """,
+ {"from": from_date, "to": to_date},
+ as_dict=True,
+ )
+
+ return {
+ "data": result or [],
+ "title": _("Deals by ongoing & won stage"),
+ "xAxis": {
+ "title": _("Stage"),
+ "key": "stage",
+ "type": "category",
+ },
+ "yAxis": {"title": _("Count")},
+ "series": [
+ {"name": "count", "type": "bar"},
+ ],
+ }
+
+
+def get_deals_by_stage_donut(from_date="", to_date="", user=""):
+ """
+ Get deal data by stage for the dashboard.
+ [
+ { stage: 'Prospecting', count: 120 },
+ { stage: 'Negotiation', count: 45 },
+ ...
+ ]
+ """
+ deal_conds = ""
if not from_date or not to_date:
from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate())
@@ -730,11 +827,70 @@ def get_deals_by_stage(from_date="", to_date="", user="", deal_conds=""):
as_dict=True,
)
- return result or []
+ return {
+ "data": result or [],
+ "title": _("Deals by stage"),
+ "subtitle": _("Current pipeline distribution"),
+ "categoryColumn": "stage",
+ "valueColumn": "count",
+ }
-@frappe.whitelist()
-def get_leads_by_source(from_date="", to_date="", user="", lead_conds=""):
+def get_lost_deal_reasons(from_date="", to_date="", user=""):
+ """
+ Get lost deal reasons for the dashboard.
+ [
+ { reason: 'Price too high', count: 20 },
+ { reason: 'Competitor won', count: 15 },
+ ...
+ ]
+ """
+
+ 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
+ JOIN `tabCRM Deal Status` s ON d.status = s.name
+ WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s AND s.type = 'Lost'
+ {deal_conds}
+ GROUP BY d.lost_reason
+ HAVING reason IS NOT NULL AND reason != ''
+ ORDER BY count DESC
+ """,
+ {"from": from_date, "to": to_date},
+ as_dict=True,
+ )
+
+ return {
+ "data": result or [],
+ "title": _("Lost deal reasons"),
+ "subtitle": _("Common reasons for losing deals"),
+ "xAxis": {
+ "title": _("Reason"),
+ "key": "reason",
+ "type": "category",
+ },
+ "yAxis": {
+ "title": _("Count"),
+ },
+ "series": [
+ {"name": "count", "type": "bar"},
+ ],
+ }
+
+
+def get_leads_by_source(from_date="", to_date="", user=""):
"""
Get lead data by source for the dashboard.
[
@@ -743,6 +899,7 @@ def get_leads_by_source(from_date="", to_date="", user="", lead_conds=""):
...
]
"""
+ lead_conds = ""
if not from_date or not to_date:
from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate())
@@ -766,7 +923,168 @@ def get_leads_by_source(from_date="", to_date="", user="", lead_conds=""):
as_dict=True,
)
- return result or []
+ return {
+ "data": result or [],
+ "title": _("Leads by source"),
+ "subtitle": _("Lead generation channel analysis"),
+ "categoryColumn": "source",
+ "valueColumn": "count",
+ }
+
+
+def get_deals_by_source(from_date="", to_date="", user=""):
+ """
+ Get deal data by source for the dashboard.
+ [
+ { source: 'Website', count: 120 },
+ { source: 'Referral', count: 45 },
+ ...
+ ]
+ """
+ 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 deal_owner = '{user}'"
+
+ result = frappe.db.sql(
+ f"""
+ SELECT
+ IFNULL(source, 'Empty') AS source,
+ COUNT(*) AS count
+ FROM `tabCRM Deal`
+ WHERE DATE(creation) BETWEEN %(from)s AND %(to)s
+ {deal_conds}
+ GROUP BY source
+ ORDER BY count DESC
+ """,
+ {"from": from_date, "to": to_date},
+ as_dict=True,
+ )
+
+ return {
+ "data": result or [],
+ "title": _("Deals by source"),
+ "subtitle": _("Deal generation channel analysis"),
+ "categoryColumn": "source",
+ "valueColumn": "count",
+ }
+
+
+def get_deals_by_territory(from_date="", to_date="", user=""):
+ """
+ Get deal data by territory for the dashboard.
+ [
+ { territory: 'North America', deals: 45, value: 2300000 },
+ { territory: 'Europe', deals: 30, value: 1500000 },
+ ...
+ ]
+ """
+ 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
+ IFNULL(d.territory, 'Empty') AS territory,
+ COUNT(*) AS deals,
+ SUM(COALESCE(d.deal_value, 0) * IFNULL(d.exchange_rate, 1)) AS value
+ FROM `tabCRM Deal` AS d
+ WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s
+ {deal_conds}
+ GROUP BY d.territory
+ ORDER BY value DESC
+ """,
+ {"from": from_date, "to": to_date},
+ as_dict=True,
+ )
+
+ return {
+ "data": result or [],
+ "title": _("Deals by territory"),
+ "subtitle": _("Geographic distribution of deals and revenue"),
+ "xAxis": {
+ "title": _("Territory"),
+ "key": "territory",
+ "type": "category",
+ },
+ "yAxis": {
+ "title": _("Number of deals"),
+ },
+ "y2Axis": {
+ "title": _("Deal value") + f" ({get_base_currency_symbol()})",
+ },
+ "series": [
+ {"name": "deals", "type": "bar"},
+ {"name": "value", "type": "line", "showDataPoints": True, "axis": "y2"},
+ ],
+ }
+
+
+def get_deals_by_salesperson(from_date="", to_date="", user=""):
+ """
+ Get deal data by salesperson for the dashboard.
+ [
+ { salesperson: 'John Smith', deals: 45, value: 2300000 },
+ { salesperson: 'Jane Doe', deals: 30, value: 1500000 },
+ ...
+ ]
+ """
+ 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
+ IFNULL(u.full_name, d.deal_owner) AS salesperson,
+ COUNT(*) AS deals,
+ SUM(COALESCE(d.deal_value, 0) * IFNULL(d.exchange_rate, 1)) AS value
+ 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}
+ GROUP BY d.deal_owner
+ ORDER BY value DESC
+ """,
+ {"from": from_date, "to": to_date},
+ as_dict=True,
+ )
+
+ return {
+ "data": result or [],
+ "title": _("Deals by salesperson"),
+ "subtitle": _("Number of deals and total value per salesperson"),
+ "xAxis": {
+ "title": _("Salesperson"),
+ "key": "salesperson",
+ "type": "category",
+ },
+ "yAxis": {
+ "title": _("Number of deals"),
+ },
+ "y2Axis": {
+ "title": _("Deal value") + f" ({get_base_currency_symbol()})",
+ },
+ "series": [
+ {"name": "deals", "type": "bar"},
+ {"name": "value", "type": "line", "showDataPoints": True, "axis": "y2"},
+ ],
+ }
def get_base_currency_symbol():
diff --git a/crm/fcrm/doctype/crm_dashboard/__init__.py b/crm/fcrm/doctype/crm_dashboard/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/crm/fcrm/doctype/crm_dashboard/crm_dashboard.js b/crm/fcrm/doctype/crm_dashboard/crm_dashboard.js
new file mode 100644
index 00000000..9aee84c7
--- /dev/null
+++ b/crm/fcrm/doctype/crm_dashboard/crm_dashboard.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("CRM Dashboard", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/crm/fcrm/doctype/crm_dashboard/crm_dashboard.json b/crm/fcrm/doctype/crm_dashboard/crm_dashboard.json
new file mode 100644
index 00000000..11dcdec4
--- /dev/null
+++ b/crm/fcrm/doctype/crm_dashboard/crm_dashboard.json
@@ -0,0 +1,105 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:title",
+ "creation": "2025-07-14 12:19:49.725022",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "private",
+ "column_break_exbw",
+ "user",
+ "section_break_hfza",
+ "layout"
+ ],
+ "fields": [
+ {
+ "fieldname": "column_break_exbw",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_hfza",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "[]",
+ "fieldname": "layout",
+ "fieldtype": "Code",
+ "label": "Layout",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "label": "Name",
+ "unique": 1
+ },
+ {
+ "depends_on": "private",
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "label": "User",
+ "mandatory_depends_on": "private",
+ "options": "User"
+ },
+ {
+ "default": "0",
+ "fieldname": "private",
+ "fieldtype": "Check",
+ "label": "Private"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2025-07-14 12:36:10.831351",
+ "modified_by": "Administrator",
+ "module": "FCRM",
+ "name": "CRM Dashboard",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "row_format": "Dynamic",
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "title"
+}
diff --git a/crm/fcrm/doctype/crm_dashboard/crm_dashboard.py b/crm/fcrm/doctype/crm_dashboard/crm_dashboard.py
new file mode 100644
index 00000000..0f6ffaa3
--- /dev/null
+++ b/crm/fcrm/doctype/crm_dashboard/crm_dashboard.py
@@ -0,0 +1,33 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+
+
+class CRMDashboard(Document):
+ pass
+
+
+def default_manager_dashboard_layout():
+ """
+ Returns the default layout for the CRM Manager Dashboard.
+ """
+ return '[{"name":"total_leads","type":"number_chart","tooltip":"Total number of leads","layout":{"x":0,"y":0,"w":4,"h":3,"i":"total_leads"}},{"name":"ongoing_deals","type":"number_chart","tooltip":"Total number of ongoing deals","layout":{"x":8,"y":0,"w":4,"h":3,"i":"ongoing_deals"}},{"name":"won_deals","type":"number_chart","tooltip":"Total number of won deals","layout":{"x":12,"y":0,"w":4,"h":3,"i":"won_deals"}},{"name":"average_won_deal_value","type":"number_chart","tooltip":"Average value of won deals","layout":{"x":16,"y":0,"w":4,"h":3,"i":"average_won_deal_value"}},{"name":"average_deal_value","type":"number_chart","tooltip":"Average deal value of ongoing and won deals","layout":{"x":0,"y":2,"w":4,"h":3,"i":"average_deal_value"}},{"name":"average_time_to_close_a_lead","type":"number_chart","tooltip":"Average time taken to close a lead","layout":{"x":4,"y":0,"w":4,"h":3,"i":"average_time_to_close_a_lead"}},{"name":"average_time_to_close_a_deal","type":"number_chart","layout":{"x":4,"y":2,"w":4,"h":3,"i":"average_time_to_close_a_deal"}},{"name":"spacer","type":"spacer","layout":{"x":8,"y":2,"w":12,"h":3,"i":"spacer"}},{"name":"sales_trend","type":"axis_chart","layout":{"x":0,"y":4,"w":10,"h":9,"i":"sales_trend"}},{"name":"forecasted_revenue","type":"axis_chart","layout":{"x":10,"y":4,"w":10,"h":9,"i":"forecasted_revenue"}},{"name":"funnel_conversion","type":"axis_chart","layout":{"x":0,"y":11,"w":10,"h":9,"i":"funnel_conversion"}},{"name":"deals_by_stage_donut","type":"donut_chart","layout":{"x":10,"y":11,"w":10,"h":9,"i":"deals_by_stage_donut"}},{"name":"lost_deal_reasons","type":"axis_chart","layout":{"x":0,"y":32,"w":20,"h":9,"i":"lost_deal_reasons"}},{"name":"leads_by_source","type":"donut_chart","layout":{"x":0,"y":18,"w":10,"h":9,"i":"leads_by_source"}},{"name":"deals_by_source","type":"donut_chart","layout":{"x":10,"y":18,"w":10,"h":9,"i":"deals_by_source"}},{"name":"deals_by_territory","type":"axis_chart","layout":{"x":0,"y":25,"w":10,"h":9,"i":"deals_by_territory"}},{"name":"deals_by_salesperson","type":"axis_chart","layout":{"x":10,"y":25,"w":10,"h":9,"i":"deals_by_salesperson"}}]'
+
+
+def create_default_manager_dashboard(force=False):
+ """
+ Creates the default CRM Manager Dashboard if it does not exist.
+ """
+ if not frappe.db.exists("CRM Dashboard", "Manager Dashboard"):
+ doc = frappe.new_doc("CRM Dashboard")
+ doc.title = "Manager Dashboard"
+ doc.layout = default_manager_dashboard_layout()
+ doc.insert(ignore_permissions=True)
+ elif force:
+ doc = frappe.get_doc("CRM Dashboard", "Manager Dashboard")
+ doc.layout = default_manager_dashboard_layout()
+ doc.save(ignore_permissions=True)
+ return doc.layout
diff --git a/crm/fcrm/doctype/crm_dashboard/test_crm_dashboard.py b/crm/fcrm/doctype/crm_dashboard/test_crm_dashboard.py
new file mode 100644
index 00000000..7e915613
--- /dev/null
+++ b/crm/fcrm/doctype/crm_dashboard/test_crm_dashboard.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests import IntegrationTestCase, UnitTestCase
+
+
+# On IntegrationTestCase, the doctype test records and all
+# link-field test record dependencies are recursively loaded
+# Use these module variables to add/remove to/from that list
+EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+
+
+class UnitTestCRMDashboard(UnitTestCase):
+ """
+ Unit tests for CRMDashboard.
+ Use this class for testing individual functions and methods.
+ """
+
+ pass
+
+
+class IntegrationTestCRMDashboard(IntegrationTestCase):
+ """
+ Integration tests for CRMDashboard.
+ Use this class for testing interactions between multiple components.
+ """
+
+ pass
diff --git a/crm/install.py b/crm/install.py
index 1b401db2..473ba5f6 100644
--- a/crm/install.py
+++ b/crm/install.py
@@ -4,6 +4,7 @@ import click
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from crm.fcrm.doctype.crm_dashboard.crm_dashboard import create_default_manager_dashboard
from crm.fcrm.doctype.crm_products.crm_products import create_product_details_script
@@ -23,6 +24,7 @@ def after_install(force=False):
add_default_lost_reasons()
add_standard_dropdown_items()
add_default_scripts()
+ create_default_manager_dashboard(force)
frappe.db.commit()
diff --git a/crm/patches/v1_0/update_deal_status_type.py b/crm/patches/v1_0/update_deal_status_type.py
index 230f3776..d3e4084c 100644
--- a/crm/patches/v1_0/update_deal_status_type.py
+++ b/crm/patches/v1_0/update_deal_status_type.py
@@ -27,7 +27,7 @@ def execute():
]
for status in deal_statuses:
- if status.type is None or status.type == "":
+ if not status.type or status.type is None or status.type == "Open":
if status.deal_status in openStatuses:
type = "Open"
elif status.deal_status in ongoingStatuses:
diff --git a/frappe-ui b/frappe-ui
index 424288f7..b295b54a 160000
--- a/frappe-ui
+++ b/frappe-ui
@@ -1 +1 @@
-Subproject commit 424288f77af4779dd3bb71dc3d278fc627f95179
+Subproject commit b295b54aaa3a2a22f455df819d87433dc6b9ff6a
diff --git a/frontend/components.d.ts b/frontend/components.d.ts
index cff4e482..c1dd22b6 100644
--- a/frontend/components.d.ts
+++ b/frontend/components.d.ts
@@ -12,6 +12,7 @@ declare module 'vue' {
Activities: typeof import('./src/components/Activities/Activities.vue')['default']
ActivityHeader: typeof import('./src/components/Activities/ActivityHeader.vue')['default']
ActivityIcon: typeof import('./src/components/Icons/ActivityIcon.vue')['default']
+ AddChartModal: typeof import('./src/components/Dashboard/AddChartModal.vue')['default']
AddExistingUserModal: typeof import('./src/components/Modals/AddExistingUserModal.vue')['default']
AddressIcon: typeof import('./src/components/Icons/AddressIcon.vue')['default']
AddressModal: typeof import('./src/components/Modals/AddressModal.vue')['default']
@@ -62,7 +63,9 @@ declare module 'vue' {
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
+ DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.vue')['default']
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
+ DashboardItem: typeof import('./src/components/Dashboard/DashboardItem.vue')['default']
DataFields: typeof import('./src/components/Activities/DataFields.vue')['default']
DataFieldsModal: typeof import('./src/components/Modals/DataFieldsModal.vue')['default']
DealModal: typeof import('./src/components/Modals/DealModal.vue')['default']
@@ -99,11 +102,9 @@ declare module 'vue' {
EmailIcon: typeof import('./src/components/Icons/EmailIcon.vue')['default']
EmailProviderIcon: typeof import('./src/components/Settings/EmailProviderIcon.vue')['default']
EmailTemplateIcon: typeof import('./src/components/Icons/EmailTemplateIcon.vue')['default']
- EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default']
EmailTemplatePage: typeof import('./src/components/Settings/EmailTemplate/EmailTemplatePage.vue')['default']
EmailTemplates: typeof import('./src/components/Settings/EmailTemplate/EmailTemplates.vue')['default']
EmailTemplateSelectorModal: typeof import('./src/components/Modals/EmailTemplateSelectorModal.vue')['default']
- EmailTemplatesListView: typeof import('./src/components/ListViews/EmailTemplatesListView.vue')['default']
ERPNextIcon: typeof import('./src/components/Icons/ERPNextIcon.vue')['default']
ERPNextSettings: typeof import('./src/components/Settings/ERPNextSettings.vue')['default']
ErrorPage: typeof import('./src/components/ErrorPage.vue')['default']
@@ -167,11 +168,9 @@ declare module 'vue' {
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
LostReasonModal: typeof import('./src/components/Modals/LostReasonModal.vue')['default']
LucideCalendar: typeof import('~icons/lucide/calendar')['default']
- LucideInfo: typeof import('~icons/lucide/info')['default']
- LucideMoreHorizontal: typeof import('~icons/lucide/more-horizontal')['default']
- LucidePlus: typeof import('~icons/lucide/plus')['default']
- LucideSearch: typeof import('~icons/lucide/search')['default']
- LucideX: typeof import('~icons/lucide/x')['default']
+ LucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
+ LucidePenLine: typeof import('~icons/lucide/pen-line')['default']
+ LucideRefreshCcw: typeof import('~icons/lucide/refresh-ccw')['default']
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default']
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
MenuIcon: typeof import('./src/components/Icons/MenuIcon.vue')['default']
@@ -205,7 +204,6 @@ declare module 'vue' {
PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default']
PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default']
Popover: typeof import('./src/components/frappe-ui/Popover.vue')['default']
- ProfileImageEditor: typeof import('./src/components/Settings/ProfileImageEditor.vue')['default']
ProfileSettings: typeof import('./src/components/Settings/ProfileSettings.vue')['default']
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']
diff --git a/frontend/package.json b/frontend/package.json
index 7672e8df..ec932138 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -13,7 +13,7 @@
"@tiptap/extension-paragraph": "^2.12.0",
"@twilio/voice-sdk": "^2.10.2",
"@vueuse/integrations": "^10.3.0",
- "frappe-ui": "^0.1.166",
+ "frappe-ui": "^0.1.171",
"gemoji": "^8.1.0",
"lodash": "^4.17.21",
"mime": "^4.0.1",
diff --git a/frontend/src/components/Dashboard/AddChartModal.vue b/frontend/src/components/Dashboard/AddChartModal.vue
new file mode 100644
index 00000000..31c2931c
--- /dev/null
+++ b/frontend/src/components/Dashboard/AddChartModal.vue
@@ -0,0 +1,165 @@
+
+
+
+
+
diff --git a/frontend/src/components/Dashboard/DashboardGrid.vue b/frontend/src/components/Dashboard/DashboardGrid.vue
new file mode 100644
index 00000000..ca984a0c
--- /dev/null
+++ b/frontend/src/components/Dashboard/DashboardGrid.vue
@@ -0,0 +1,62 @@
+
+