From b602a07c7aa6a0e9762f7816e713c915102b2f3f Mon Sep 17 00:00:00 2001 From: Prateeksha Singh Date: Mon, 17 Jul 2017 14:51:30 +0530 Subject: [PATCH] Graphs, and target notifications (#3641) * [sales goal] in company; graph, notifs * cleanup notifications.js, summary and specific val in graph * Add line graph, add goal data methods in goal.py * [tests] targets in notification config * [minor] type of graph as argument in parent * Update graph docs * remove company dependent test for notification * [fix] test * look for monthly history in cache * check for cached graph data in field --- frappe/desk/doctype/event/test_records.json | 13 +- frappe/desk/notifications.py | 48 ++- frappe/docs/assets/img/desk/bar_graph.png | Bin 0 -> 30001 bytes frappe/docs/assets/img/desk/line_graph.png | Bin 0 -> 39700 bytes .../docs/user/en/guides/desk/making_graphs.md | 61 ++++ frappe/public/build.json | 1 + frappe/public/css/desk.css | 11 + frappe/public/css/form.css | 86 +++++ frappe/public/js/frappe/form/dashboard.js | 53 ++- .../frappe/form/templates/form_dashboard.html | 2 +- frappe/public/js/frappe/ui/graph.js | 308 ++++++++++++++++++ .../js/frappe/ui/toolbar/notifications.js | 219 ++++++------- frappe/public/js/legacy/form.js | 2 +- frappe/public/less/desk.less | 46 ++- frappe/public/less/form.less | 117 +++++++ frappe/tests/test_goal.py | 34 ++ frappe/utils/goal.py | 128 ++++++++ 17 files changed, 985 insertions(+), 144 deletions(-) create mode 100644 frappe/docs/assets/img/desk/bar_graph.png create mode 100644 frappe/docs/assets/img/desk/line_graph.png create mode 100644 frappe/docs/user/en/guides/desk/making_graphs.md create mode 100644 frappe/public/js/frappe/ui/graph.js create mode 100644 frappe/tests/test_goal.py create mode 100644 frappe/utils/goal.py diff --git a/frappe/desk/doctype/event/test_records.json b/frappe/desk/doctype/event/test_records.json index aaadc881b8..41d5803083 100644 --- a/frappe/desk/doctype/event/test_records.json +++ b/frappe/desk/doctype/event/test_records.json @@ -3,18 +3,21 @@ "doctype": "Event", "subject":"_Test Event 1", "starts_on": "2014-01-01", - "event_type": "Public" + "event_type": "Public", + "creation": "2014-01-01" }, { "doctype": "Event", - "starts_on": "2014-01-01", "subject":"_Test Event 2", - "event_type": "Private" + "starts_on": "2014-01-01", + "event_type": "Private", + "creation": "2014-01-01" }, { "doctype": "Event", - "starts_on": "2014-01-01", "subject": "_Test Event 3", - "event_type": "Private" + "starts_on": "2014-02-01", + "event_type": "Private", + "creation": "2014-02-01" } ] diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 3924afd7a4..d0ee87a209 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -13,10 +13,12 @@ def get_notifications(): return config = get_notification_config() + groups = config.get("for_doctype").keys() + config.get("for_module").keys() cache = frappe.cache() notification_count = {} + notification_percent = {} for name in groups: count = cache.hget("notification_count:" + name, frappe.session.user) @@ -27,6 +29,7 @@ def get_notifications(): "open_count_doctype": get_notifications_for_doctypes(config, notification_count), "open_count_module": get_notifications_for_modules(config, notification_count), "open_count_other": get_notifications_for_other(config, notification_count), + "targets": get_notifications_for_targets(config, notification_percent), "new_messages": get_new_messages() } @@ -111,6 +114,49 @@ def get_notifications_for_doctypes(config, notification_count): return open_count_doctype +def get_notifications_for_targets(config, notification_percent): + """Notifications for doc targets""" + can_read = frappe.get_user().get_can_read() + doc_target_percents = {} + + # doc_target_percents = { + # "Company": { + # "Acme": 87, + # "RobotsRUs": 50, + # }, {}... + # } + + for doctype in config.targets: + if doctype in can_read: + if doctype in notification_percent: + doc_target_percents[doctype] = notification_percent[doctype] + else: + doc_target_percents[doctype] = {} + d = config.targets[doctype] + condition = d["filters"] + target_field = d["target_field"] + value_field = d["value_field"] + try: + if isinstance(condition, dict): + doc_list = frappe.get_list(doctype, fields=["name", target_field, value_field], + filters=condition, limit_page_length = 100, ignore_ifnull=True) + + except frappe.PermissionError: + frappe.clear_messages() + pass + except Exception as e: + if e.args[0]!=1412: + raise + + else: + for doc in doc_list: + value = doc[value_field] + target = doc[target_field] + doc_target_percents[doctype][doc.name] = (value/target * 100) if value < target else 100 + + return doc_target_percents + + def clear_notifications(user=None): if frappe.flags.in_install: return @@ -163,7 +209,7 @@ def get_notification_config(): config = frappe._dict() for notification_config in frappe.get_hooks().notification_config: nc = frappe.get_attr(notification_config)() - for key in ("for_doctype", "for_module", "for_other"): + for key in ("for_doctype", "for_module", "for_other", "targets"): config.setdefault(key, {}) config[key].update(nc.get(key, {})) return config diff --git a/frappe/docs/assets/img/desk/bar_graph.png b/frappe/docs/assets/img/desk/bar_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..d25254af6d2929026664414604f1d0c3cc1c6f12 GIT binary patch literal 30001 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 J)d$nB{|mG<7k2;v literal 0 HcmV?d00001 diff --git a/frappe/docs/assets/img/desk/line_graph.png b/frappe/docs/assets/img/desk/line_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..02c60c7c1868536060186648c666bd37e8e4de8a GIT binary patch literal 39700 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 literal 0 HcmV?d00001 diff --git a/frappe/docs/user/en/guides/desk/making_graphs.md b/frappe/docs/user/en/guides/desk/making_graphs.md new file mode 100644 index 0000000000..9234fa58b4 --- /dev/null +++ b/frappe/docs/user/en/guides/desk/making_graphs.md @@ -0,0 +1,61 @@ +# Making Graphs + +The Frappe UI **Graph** object enables you to render simple line and bar graphs for a discreet set of data points. You can also set special checkpoint values and summary stats. + +### Example: Line graph +Here's is an example of a simple sales graph: + + render_graph: function() { + $('.form-graph').empty(); + + var months = ['Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']; + var values = [2410, 3100, 1700, 1200, 2700, 1600, 2740, 1000, 850, 1500, 400, 2013]; + + var goal = 2500; + var current_val = 2013; + + new frappe.ui.Graph({ + parent: $('.form-graph'), + width: 700, + height: 140, + mode: 'line-graph', + + title: 'Sales', + subtitle: 'Monthly', + y_values: values, + x_points: months, + + specific_values: [ + { + name: "Goal", + line_type: "dashed", // "dashed" or "solid" + value: goal + }, + ], + summary_values: [ + { + name: "This month", + color: 'green', // Indicator colors: 'grey', 'blue', 'red', + // 'green', 'orange', 'purple', 'darkgrey', + // 'black', 'yellow', 'lightblue' + value: '₹ ' + current_val + }, + { + name: "Goal", + color: 'blue', + value: '₹ ' + goal + }, + { + name: "Completed", + color: 'green', + value: (current_val/goal*100).toFixed(1) + "%" + } + ] + }); + }, + + + +Setting the mode to 'bar-graph': + + diff --git a/frappe/public/build.json b/frappe/public/build.json index 75e4e76469..3b58de727b 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -161,6 +161,7 @@ "public/js/frappe/query_string.js", "public/js/frappe/ui/charts.js", + "public/js/frappe/ui/graph.js", "public/js/frappe/misc/rating_icons.html", "public/js/frappe/feedback.js" diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index fa13c421fa..ebe34f0de2 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -508,6 +508,17 @@ fieldset[disabled] .form-control { cursor: pointer; margin-right: 10px; } +a.progress-small .progress-chart { + width: 60px; + margin-top: 4px; + float: right; +} +a.progress-small .progress { + margin-bottom: 0; +} +a.progress-small .progress-bar { + background-color: #98d85b; +} /* on small screens, show only icons on top */ @media (max-width: 767px) { .module-view-layout .nav-stacked > li { diff --git a/frappe/public/css/form.css b/frappe/public/css/form.css index d822b04975..844c2dc761 100644 --- a/frappe/public/css/form.css +++ b/frappe/public/css/form.css @@ -642,6 +642,92 @@ select.form-control { box-shadow: none; } } +/* goals */ +.goals-page-container { + background-color: #fafbfc; + padding-top: 1px; +} +.goals-page-container .goal-container { + background-color: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + border-radius: 2px; + padding: 10px; + margin: 10px; +} +.graph-container .graphics { + margin-top: 10px; + padding: 10px 0px; +} +.graph-container .stats-group { + display: flex; + justify-content: space-around; + flex: 1; +} +.graph-container .stats-container { + display: flex; + justify-content: space-around; +} +.graph-container .stats-container .stats { + padding-bottom: 15px; +} +.graph-container .stats-container .stats-title { + color: #8D99A6; +} +.graph-container .stats-container .stats-value { + font-size: 20px; + font-weight: 300; +} +.graph-container .stats-container .stats-description { + font-size: 12px; + color: #8D99A6; +} +.graph-container .stats-container .graph-data .stats-value { + color: #98d85b; +} +.bar-graph .axis, +.line-graph .axis { + font-size: 10px; + fill: #6a737d; +} +.bar-graph .axis line, +.line-graph .axis line { + stroke: rgba(27, 31, 35, 0.1); +} +.data-points circle { + fill: #28a745; + stroke: #fff; + stroke-width: 2; +} +.data-points g.mini { + fill: #98d85b; +} +.data-points path { + fill: none; + stroke: #28a745; + stroke-opacity: 1; + stroke-width: 2px; +} +.line-graph .path { + fill: none; + stroke: #28a745; + stroke-opacity: 1; + stroke-width: 2px; +} +line.dashed { + stroke-dasharray: 5,3; +} +.tick.x-axis-label { + display: block; +} +.tick .specific-value { + text-anchor: start; +} +.tick .y-value-text { + text-anchor: end; +} +.tick .x-value-text { + text-anchor: middle; +} body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] { height: 80px !important; } diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index fc3c31fce2..36baa6ca15 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -11,7 +11,7 @@ frappe.ui.form.Dashboard = Class.extend({ this.progress_area = this.wrapper.find(".progress-area"); this.heatmap_area = this.wrapper.find('.form-heatmap'); - this.chart_area = this.wrapper.find('.form-chart'); + this.graph_area = this.wrapper.find('.form-graph'); this.stats_area = this.wrapper.find('.form-stats'); this.stats_area_row = this.stats_area.find('.row'); this.links_area = this.wrapper.find('.form-links'); @@ -43,9 +43,9 @@ frappe.ui.form.Dashboard = Class.extend({ this.frm.layout.show_message(); }, - add_comment: function(text, permanent) { + add_comment: function(text, alert_class, permanent) { var me = this; - this.set_headline_alert(text); + this.set_headline_alert(text, alert_class); if(!permanent) { setTimeout(function() { me.clear_headline(); @@ -91,6 +91,7 @@ frappe.ui.form.Dashboard = Class.extend({ this.show(); }, + format_percent: function(title, percent) { var width = cint(percent) < 1 ? 1 : cint(percent); var progress_class = ""; @@ -138,6 +139,11 @@ frappe.ui.form.Dashboard = Class.extend({ show = true; } + if(this.data.graph) { + this.setup_graph(); + show = true; + } + if(show) { this.show(); } @@ -383,13 +389,50 @@ frappe.ui.form.Dashboard = Class.extend({ }, //graphs + setup_graph: function() { + var me = this; + + var method = this.data.graph_method; + var args = { + doctype: this.frm.doctype, + docname: this.frm.doc.name, + }; + + $.extend(args, this.data.graph_method_args); + + frappe.call({ + type: "GET", + method: method, + args: args, + + callback: function(r) { + if(r.message) { + me.render_graph(r.message); + } + } + }); + }, + + render_graph: function(args) { + var me = this; + this.graph_area.empty().removeClass('hidden'); + $.extend(args, { + parent: me.graph_area, + width: 700, + height: 140, + mode: 'line-graph' + }); + + new frappe.ui.Graph(args); + }, + setup_chart: function(opts) { var me = this; - this.chart_area.removeClass('hidden'); + this.graph_area.removeClass('hidden'); $.extend(opts, { - wrapper: me.wrapper.find('.form-chart'), + wrapper: me.graph_area, padding: { right: 30, bottom: 30 diff --git a/frappe/public/js/frappe/form/templates/form_dashboard.html b/frappe/public/js/frappe/form/templates/form_dashboard.html index b1865a9c94..c41929df73 100644 --- a/frappe/public/js/frappe/form/templates/form_dashboard.html +++ b/frappe/public/js/frappe/form/templates/form_dashboard.html @@ -5,7 +5,7 @@
- + diff --git a/frappe/public/js/frappe/ui/graph.js b/frappe/public/js/frappe/ui/graph.js new file mode 100644 index 0000000000..25718024a1 --- /dev/null +++ b/frappe/public/js/frappe/ui/graph.js @@ -0,0 +1,308 @@ +// specific_values = [ +// { +// name: "Average", +// line_type: "dashed", // "dashed" or "solid" +// value: 10 +// }, + +// summary_values = [ +// { +// name: "Total", +// color: 'blue', // Indicator colors: 'grey', 'blue', 'red', 'green', 'orange', +// // 'purple', 'darkgrey', 'black', 'yellow', 'lightblue' +// value: 80 +// } +// ] + +frappe.ui.Graph = class Graph { + constructor({ + parent = null, + + width = 0, height = 0, + title = '', subtitle = '', + + y_values = [], + x_points = [], + + specific_values = [], + summary_values = [], + + color = '', + mode = '', + } = {}) { + + if(Object.getPrototypeOf(this) === frappe.ui.Graph.prototype) { + if(mode === 'line-graph') { + return new frappe.ui.LineGraph(arguments[0]); + } else if(mode === 'bar-graph') { + return new frappe.ui.BarGraph(arguments[0]); + } + } + + this.parent = parent; + + this.width = width; + this.height = height; + + this.title = title; + this.subtitle = subtitle; + + this.y_values = y_values; + this.x_points = x_points; + + this.specific_values = specific_values; + this.summary_values = summary_values; + + this.color = color; + this.mode = mode; + + this.$graph = null; + + frappe.require("assets/frappe/js/lib/snap.svg-min.js", this.setup.bind(this)); + } + + setup() { + this.setup_container(); + this.refresh(); + } + + refresh() { + this.setup_values(); + this.setup_components(); + this.make_y_axis(); + this.make_x_axis(); + this.make_units(); + if(this.specific_values.length > 0) { + this.show_specific_values(); + } + this.setup_group(); + + if(this.summary_values.length > 0) { + this.show_summary(); + } + } + + setup_container() { + this.container = $('
') + .addClass('graph-container') + .append($(`
${this.title}
`)) + .append($(`
${this.subtitle}
`)) + .append($(`
`)) + .append($(`
`)) + .appendTo(this.parent); + + let $graphics = this.container.find('.graphics'); + this.$stats_container = this.container.find('.stats-container'); + + this.$graph = $('
') + .addClass(this.mode) + .appendTo($graphics); + + this.$svg = $(``); + this.$graph.append(this.$svg); + + this.snap = new Snap(this.$svg[0]); + } + + setup_values() { + this.upper_graph_bound = this.get_upper_limit_and_parts(this.y_values)[0]; + this.y_axis = this.get_y_axis(this.y_values); + this.avg_unit_width = (this.width-50)/(this.x_points.length - 1); + } + + setup_components() { + this.y_axis_group = this.snap.g().attr({ + class: "y axis" + }); + + this.x_axis_group = this.snap.g().attr({ + class: "x axis" + }); + + this.graph_list = this.snap.g().attr({ + class: "data-points", + }); + + this.specific_y_lines = this.snap.g().attr({ + class: "specific axis", + }); + } + + setup_group() { + this.snap.g( + this.y_axis_group, + this.x_axis_group, + this.graph_list, + this.specific_y_lines + ).attr({ + transform: "translate(40, 10)" // default + }); + } + + show_specific_values() { + this.specific_values.map(d => { + this.specific_y_lines.add(this.snap.g( + this.snap.line(0, 0, this.width - 50, 0).attr({ + class: d.line_type === "dashed" ? "dashed": "" + }), + this.snap.text(this.width - 100, 0, d.name.toUpperCase()).attr({ + dy: ".32em", + class: "specific-value", + }) + ).attr({ + class: "tick", + transform: `translate(0, ${100 - 100/(this.upper_graph_bound/d.value) })` + })); + }); + } + + show_summary() { + this.summary_values.map(d => { + this.$stats_container.append($(`
+ ${d.name}: ${d.value} +
`)); + }); + } + + // Helpers + get_upper_limit_and_parts(array) { + let specific_values = this.specific_values.map(d => d.value); + let max_val = Math.max(...array, ...specific_values); + if((max_val+"").length <= 1) { + return [10, 5]; + } else { + let multiplier = Math.pow(10, ((max_val+"").length - 1)); + let significant = Math.ceil(max_val/multiplier); + if(significant % 2 !== 0) significant++; + let parts = (significant < 5) ? significant : significant/2; + return [significant * multiplier, parts]; + } + } + + get_y_axis(array) { + let upper_limit, parts; + [upper_limit, parts] = this.get_upper_limit_and_parts(array); + let y_axis = []; + for(var i = 0; i <= parts; i++){ + y_axis.push(upper_limit / parts * i); + } + return y_axis; + } +}; + +frappe.ui.BarGraph = class BarGraph extends frappe.ui.Graph { + constructor(args = {}) { + super(args); + } + + setup_values() { + super.setup_values(); + this.avg_unit_width = (this.width-50)/(this.x_points.length + 2); + } + + make_y_axis() { + this.y_axis.map((point) => { + this.y_axis_group.add(this.snap.g( + this.snap.line(0, 0, this.width, 0), + this.snap.text(-3, 0, point+"").attr({ + dy: ".32em", + class: "y-value-text" + }) + ).attr({ + class: "tick", + transform: `translate(0, ${100 - (100/(this.y_axis.length-1) * this.y_axis.indexOf(point)) })` + })); + }); + } + + make_x_axis() { + this.x_axis_group.attr({ + transform: "translate(0,100)" + }); + this.x_points.map((point, i) => { + this.x_axis_group.add(this.snap.g( + this.snap.line(0, 0, 0, 6), + this.snap.text(0, 9, point).attr({ + dy: ".71em", + class: "x-value-text" + }) + ).attr({ + class: "tick x-axis-label", + transform: `translate(${ ((this.avg_unit_width - 5)*3/2) + i * (this.avg_unit_width + 5) }, 0)` + })); + }); + } + + make_units() { + this.y_values.map((value, i) => { + this.graph_list.add(this.snap.g( + this.snap.rect( + 0, + (100 - 100/(this.upper_graph_bound/value)), + this.avg_unit_width - 5, + 100/(this.upper_graph_bound/value) + ) + ).attr({ + class: "bar mini", + transform: `translate(${ (this.avg_unit_width - 5) + i * (this.avg_unit_width + 5) }, 0)`, + })); + }); + } +}; + +frappe.ui.LineGraph = class LineGraph extends frappe.ui.Graph { + constructor(args = {}) { + super(args); + } + + make_y_axis() { + this.y_axis.map((point) => { + this.y_axis_group.add(this.snap.g( + this.snap.line(0, 0, -6, 0), + this.snap.text(-9, 0, point+"").attr({ + dy: ".32em", + class: "y-value-text" + }) + ).attr({ + class: "tick", + transform: `translate(0, ${100 - (100/(this.y_axis.length-1) + * this.y_axis.indexOf(point)) })` + })); + }); + } + + make_x_axis() { + this.x_axis_group.attr({ + transform: "translate(0,-7)" + }); + this.x_points.map((point, i) => { + this.x_axis_group.add(this.snap.g( + this.snap.line(0, 0, 0, this.height - 25), + this.snap.text(0, this.height - 15, point).attr({ + dy: ".71em", + class: "x-value-text" + }) + ).attr({ + class: "tick", + transform: `translate(${ i * this.avg_unit_width }, 0)` + })); + }); + } + + make_units() { + let points_list = []; + this.y_values.map((value, i) => { + let x = i * this.avg_unit_width; + let y = (100 - 100/(this.upper_graph_bound/value)); + this.graph_list.add(this.snap.circle( x, y, 4)); + points_list.push(x+","+y); + }); + + this.make_path("M"+points_list.join("L")); + } + + make_path(path_str) { + this.graph_list.prepend(this.snap.path(path_str)); + } + +}; diff --git a/frappe/public/js/frappe/ui/toolbar/notifications.js b/frappe/public/js/frappe/ui/toolbar/notifications.js index 9198594d8e..465edf02a0 100644 --- a/frappe/public/js/frappe/ui/toolbar/notifications.js +++ b/frappe/public/js/frappe/ui/toolbar/notifications.js @@ -1,125 +1,112 @@ -frappe.provide("frappe.ui.notifications") - -frappe.ui.notifications.update_notifications = function() { - frappe.ui.notifications.total = 0; - var doctypes = Object.keys(frappe.boot.notification_info.open_count_doctype).sort(); - var modules = Object.keys(frappe.boot.notification_info.open_count_module).sort(); - var other = Object.keys(frappe.boot.notification_info.open_count_other).sort(); - - // clear toolbar / sidebar notifications - frappe.ui.notifications.dropdown_notification = $("#dropdown-notification").empty(); - - // add these first. - frappe.ui.notifications.add_notification("Comment"); - frappe.ui.notifications.add_notification("ToDo"); - frappe.ui.notifications.add_notification("Event"); - - // add other - $.each(other, function(i, name) { - frappe.ui.notifications.add_notification(name, frappe.boot.notification_info.open_count_other); - }); - - - // add a divider - if(frappe.ui.notifications.total) { - var divider = '
  • '; - frappe.ui.notifications.dropdown_notification.append($(divider)); - } - - // add to toolbar and sidebar - $.each(doctypes, function(i, doctype) { - if(!in_list(["ToDo", "Comment", "Event"], doctype)) { - frappe.ui.notifications.add_notification(doctype); - } - }); - - // set click events - $("#dropdown-notification a").on("click", function() { - var doctype = $(this).attr("data-doctype"); - var config = frappe.ui.notifications.config[doctype] || {}; - if (config.route) { - frappe.set_route(config.route); - } else if (config.click) { - config.click(); - } else { - frappe.views.show_open_count_list(this); - } - }); - - // switch colour on the navbar and disable if no notifications - $(".navbar-new-comments") - .html(frappe.ui.notifications.total > 20 ? '20+' : frappe.ui.notifications.total) - .toggleClass("navbar-new-comments-true", frappe.ui.notifications.total ? true : false) - .parent().toggleClass("disabled", frappe.ui.notifications.total ? false : true); - -} - -frappe.ui.notifications.add_notification = function(doctype, notifications_map) { - if(!notifications_map) { - notifications_map = frappe.boot.notification_info.open_count_doctype; - } +frappe.provide("frappe.ui.notifications"); + +frappe.ui.notifications = { + config: { + "ToDo": { label: __("To Do") }, + "Chat": { label: __("Chat"), route: "chat"}, + "Event": { label: __("Calendar"), route: "List/Event/Calendar" }, + "Email": { label: __("Email"), route: "List/Communication/Inbox" }, + "Likes": { label: __("Likes"), + click: function() { + frappe.route_options = { show_likes: true }; + if (frappe.get_route()[0]=="activity") { + frappe.pages['activity'].page.list.refresh(); + } else { + frappe.set_route("activity"); + } + } + }, + }, - var count = notifications_map[doctype]; - if(count) { - var config = frappe.ui.notifications.config[doctype] || {}; - var label = config.label || doctype; - var notification_row = repl('
  • \ - \ - %(count)s \ - %(label)s
  • ', { - label: __(label), - count: count > 20 ? '20+' : count, - data_doctype: doctype + update_notifications: function() { + this.total = 0; + this.dropdown = $("#dropdown-notification").empty(); + this.boot_info = frappe.boot.notification_info; + let defaults = ["Comment", "ToDo", "Event"]; + + this.get_counts(this.boot_info.open_count_doctype, 0, defaults); + this.get_counts(this.boot_info.open_count_other, 1); + + // Target counts are stored for docs per doctype + let targets = { doctypes : {} }, map = this.boot_info.targets; + Object.keys(map).map(doctype => { + Object.keys(map[doctype]).map(doc => { + targets[doc] = map[doctype][doc]; + targets.doctypes[doc] = doctype; }); + }); + this.get_counts(targets, 1, null, ["doctypes"], true); + this.get_counts(this.boot_info.open_count_doctype, + 0, null, defaults); + + this.bind_list(); + + // switch colour on the navbar and disable if no notifications + $(".navbar-new-comments") + .html(this.total > 20 ? '20+' : this.total) + .toggleClass("navbar-new-comments-true", this.total ? true : false) + .parent().toggleClass("disabled", this.total ? false : true); + }, - frappe.ui.notifications.dropdown_notification.append($(notification_row)); - - frappe.ui.notifications.total += count; - } -} + get_counts: function(map, divide, keys, excluded = [], target = false) { + keys = keys ? keys + : Object.keys(map).sort().filter(e => !excluded.includes(e)); + keys.map(key => { + let doc_dt = (map.doctypes) ? map.doctypes[key] : undefined; + if(map[key] > 0) { + this.add_notification(key, map[key], doc_dt, target); + } + }); + if(divide) + this.dropdown.append($('
  • ')); + }, -// default notification config -frappe.ui.notifications.config = { - "ToDo": { label: __("To Do") }, - "Chat": { label: __("Chat"), route: "chat"}, - "Event": { label: __("Calendar"), route: "List/Event/Calendar" }, - "Email": { label: __("Email"), route: "List/Communication/Inbox" }, - "Likes": { - label: __("Likes"), - click: function() { - frappe.route_options = { - show_likes: true - }; + add_notification: function(name, value, doc_dt, target = false) { + let label = this.config[name] ? this.config[name].label : name; + let $list_item = !target + ? $(`
  • ${label} + ${value} +
  • `) + : $(`
  • ${label} +
    +
    +
    +
  • `); + this.dropdown.append($list_item); + if(!target) this.total += value; + }, - if (frappe.get_route()[0]=="activity") { - frappe.pages['activity'].page.list.refresh(); + bind_list: function() { + var me = this; + $("#dropdown-notification a").on("click", function() { + var doctype = $(this).attr("data-doctype"); + var doc = $(this).attr("data-doc"); + if(!doc) { + var config = me.config[doctype] || {}; + if (config.route) { + frappe.set_route(config.route); + } else if (config.click) { + config.click(); + } else { + frappe.ui.notifications.show_open_count_list(doctype); + } } else { - frappe.set_route("activity"); + frappe.set_route("Form", doctype, doc); } - } + }); }, -}; - -frappe.views.show_open_count_list = function(element) { - var doctype = $(element).attr("data-doctype"); - var filters = frappe.ui.notifications.get_filters(doctype); - if(filters) { - frappe.route_options = filters; - } - - var route = frappe.get_route(); - if(route[0]==="List" && route[1]===doctype) { - frappe.pages["List/" + doctype].list_view.refresh(); - } else { - frappe.set_route("List", doctype); - } -} - -frappe.ui.notifications.get_filters = function(doctype) { - var conditions = frappe.boot.notification_info.conditions[doctype]; - - if(conditions && $.isPlainObject(conditions)) { - return conditions; - } -} + show_open_count_list: function(doctype) { + let filters = this.boot_info.conditions[doctype]; + if(filters && $.isPlainObject(filters)) { + frappe.route_options = filters; + } + let route = frappe.get_route(); + if(route[0]==="List" && route[1]===doctype) { + frappe.pages["List/" + doctype].list_view.refresh(); + } else { + frappe.set_route("List", doctype); + } + }, +} \ No newline at end of file diff --git a/frappe/public/js/legacy/form.js b/frappe/public/js/legacy/form.js index 416a7a1f17..b9f0d1538d 100644 --- a/frappe/public/js/legacy/form.js +++ b/frappe/public/js/legacy/form.js @@ -335,7 +335,7 @@ _f.Frm.prototype.refresh_header = function(is_a_different_doc) { ! this.is_dirty() && ! this.is_new() && this.doc.docstatus===0) { - this.dashboard.add_comment(__('Submit this document to confirm'), true); + this.dashboard.add_comment(__('Submit this document to confirm'), 'alert-warning', true); } this.clear_custom_buttons(); diff --git a/frappe/public/less/desk.less b/frappe/public/less/desk.less index ad3011cb9e..45bde29460 100644 --- a/frappe/public/less/desk.less +++ b/frappe/public/less/desk.less @@ -66,8 +66,8 @@ a[disabled="disabled"] { #alert-container .desk-alert { -webkit-box-shadow: 0 0px 5px rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 0px 5px rgba(0, 0, 0, 0.1); - box-shadow: 0 0px 5px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 0px 5px rgba(0, 0, 0, 0.1); + box-shadow: 0 0px 5px rgba(0, 0, 0, 0.1); padding: 10px 40px 10px 20px; max-width: 400px; @@ -318,19 +318,35 @@ textarea.form-control { } .open-notification { - position:relative; + position:relative; left: 2px; - display:inline-block; - background:#ff5858; - font-size: @text-medium; - line-height:20px; - padding:0 8px; - color:#fff; - border-radius:10px; + display:inline-block; + background:#ff5858; + font-size: @text-medium; + line-height:20px; + padding:0 8px; + color:#fff; + border-radius:10px; cursor: pointer; margin-right: 10px; } +a.progress-small { + .progress-chart { + width: 60px; + margin-top: 4px; + float: right; + } + + .progress { + margin-bottom: 0; + } + + .progress-bar { + background-color: #98d85b; + } +} + /* on small screens, show only icons on top */ @media (max-width: 767px) { .module-view-layout .nav-stacked > li { @@ -825,7 +841,7 @@ textarea.form-control { } .c3-line { - stroke-width: 3px; + stroke-width: 3px; } .c3-tooltip { @@ -897,10 +913,10 @@ input[type="checkbox"] { // Will not be required after commonifying lists with empty state .multiselect-empty-state{ min-height: 300px; - display: flex; - align-items: center; - justify-content: center; - height: 100%; + display: flex; + align-items: center; + justify-content: center; + height: 100%; } // mozilla doesn't support diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less index 5c67bbee03..5bcf903c3b 100644 --- a/frappe/public/less/form.less +++ b/frappe/public/less/form.less @@ -827,6 +827,123 @@ select.form-control { } } +/* goals */ + +.goals-page-container { + background-color: #fafbfc; + padding-top: 1px; + + .goal-container { + background-color: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + border-radius: 2px; + padding: 10px; + margin: 10px; + } +} + +.graph-container { + .graphics { + margin-top: 10px; + padding: 10px 0px; + } + + .stats-group { + display: flex; + justify-content: space-around; + flex: 1; + } + + .stats-container { + display: flex; + justify-content: space-around; + + .stats { + padding-bottom: 15px; + } + + .stats-title { + color: #8D99A6; + } + .stats-value { + font-size: 20px; + font-weight: 300; + } + .stats-description { + font-size: 12px; + color: #8D99A6; + } + .graph-data .stats-value { + color: #98d85b; + } + } +} + +.bar-graph, .line-graph { + + .axis { + font-size: 10px; + fill: #6a737d; + + line { + stroke: rgba(27,31,35,0.1); + } + } +} + +.data-points { + circle { + fill: #28a745; + stroke: #fff; + stroke-width: 2; + } + + g.mini { + fill: #98d85b; + } + + path { + fill: none; + stroke: #28a745; + stroke-opacity: 1; + stroke-width: 2px; + } +} + +.line-graph { + .path { + fill: none; + stroke: #28a745; + stroke-opacity: 1; + stroke-width: 2px; + } +} + +line.dashed { + stroke-dasharray: 5,3; +} + +.tick { + &.x-axis-label { + display: block; + } + + .specific-value { + text-anchor: start; + } + + .y-value-text { + text-anchor: end; + } + + .x-value-text { + text-anchor: middle; + } +} + + body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] { height: 80px !important; } + + diff --git a/frappe/tests/test_goal.py b/frappe/tests/test_goal.py new file mode 100644 index 0000000000..5fe490ab56 --- /dev/null +++ b/frappe/tests/test_goal.py @@ -0,0 +1,34 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import unittest +import frappe + +from frappe.utils.goal import get_monthly_results, get_monthly_goal_graph_data +from frappe.test_runner import make_test_objects +import frappe.utils + +class TestGoal(unittest.TestCase): + def setUp(self): + make_test_objects('Event', reset=True) + + def tearDown(self): + frappe.db.sql('delete from `tabEvent`') + # make_test_objects('Event', reset=True) + frappe.db.commit() + + def test_get_monthly_results(self): + '''Test monthly aggregation values of a field''' + result_dict = get_monthly_results('Event', 'subject', 'creation', 'event_type="Private"', 'count') + + from frappe.utils import today, formatdate + self.assertEquals(result_dict[formatdate(today(), "MM-yyyy")], 2) + + def test_get_monthly_goal_graph_data(self): + '''Test for accurate values in graph data (based on test_get_monthly_results)''' + docname = frappe.get_list('Event', filters = {"subject": ["=", "_Test Event 1"]})[0]["name"] + frappe.db.set_value('Event', docname, 'description', 1) + data = get_monthly_goal_graph_data('Test', 'Event', docname, 'description', 'description', 'description', + 'Event', '', 'description', 'creation', 'starts_on = "2014-01-01"', 'count') + self.assertEquals(float(data['y_values'][-1]), 1) diff --git a/frappe/utils/goal.py b/frappe/utils/goal.py new file mode 100644 index 0000000000..bf1b9c345e --- /dev/null +++ b/frappe/utils/goal.py @@ -0,0 +1,128 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def get_monthly_results(goal_doctype, goal_field, date_col, filter_str, aggregation = 'sum'): + '''Get monthly aggregation values for given field of doctype''' + + where_clause = ('where ' + filter_str) if filter_str else '' + results = frappe.db.sql(''' + select + {0}({1}) as {1}, date_format({2}, '%m-%Y') as month_year + from + `{3}` + {4} + group by + month_year'''.format(aggregation, goal_field, date_col, "tab" + + goal_doctype, where_clause), as_dict=True) + + month_to_value_dict = {} + for d in results: + month_to_value_dict[d['month_year']] = d[goal_field] + + return month_to_value_dict + +@frappe.whitelist() +def get_monthly_goal_graph_data(title, doctype, docname, goal_value_field, goal_total_field, goal_history_field, + goal_doctype, goal_doctype_link, goal_field, date_field, filter_str, aggregation="sum"): + ''' + Get month-wise graph data for a doctype based on aggregation values of a field in the goal doctype + + :param title: Graph title + :param doctype: doctype of graph doc + :param docname: of the doc to set the graph in + :param goal_value_field: goal field of doctype + :param goal_total_field: current month value field of doctype + :param goal_history_field: cached history field + :param goal_doctype: doctype the goal is based on + :param goal_doctype_link: doctype link field in goal_doctype + :param goal_field: field from which the goal is calculated + :param filter_str: where clause condition + :param aggregation: a value like 'count', 'sum', 'avg' + + :return: dict of graph data + ''' + + from frappe.utils.formatters import format_value + import json + + meta = frappe.get_meta(doctype) + doc = frappe.get_doc(doctype, docname) + + goal = doc.get(goal_value_field) + formatted_goal = format_value(goal, meta.get_field(goal_value_field), doc) + + current_month_value = doc.get(goal_total_field) + formatted_value = format_value(current_month_value, meta.get_field(goal_total_field), doc) + + from frappe.utils import today, getdate, formatdate, add_months + current_month_year = formatdate(today(), "MM-yyyy") + + history = doc.get(goal_history_field) + try: + month_to_value_dict = json.loads(history) if history and '{' in history else None + except ValueError: + month_to_value_dict = None + + if month_to_value_dict is None: + doc_filter = (goal_doctype_link + ' = "' + docname + '"') if doctype != goal_doctype else '' + if filter_str: + doc_filter += ' and ' + filter_str if doc_filter else filter_str + month_to_value_dict = get_monthly_results(goal_doctype, goal_field, date_field, doc_filter, aggregation) + frappe.db.set_value(doctype, docname, goal_history_field, json.dumps(month_to_value_dict)) + + month_to_value_dict[current_month_year] = current_month_value + + months = [] + values = [] + for i in xrange(0, 12): + month_value = formatdate(add_months(today(), -i), "MM-yyyy") + month_word = getdate(month_value).strftime('%b') + months.insert(0, month_word) + if month_value in month_to_value_dict: + values.insert(0, month_to_value_dict[month_value]) + else: + values.insert(0, 0) + + specific_values = [] + summary_values = [ + { + 'name': "This month", + 'color': 'green', + 'value': formatted_value + } + ] + + if float(goal) > 0: + specific_values = [ + { + 'name': "Goal", + 'line_type': "dashed", + 'value': goal + }, + ] + summary_values += [ + { + 'name': "Goal", + 'color': 'blue', + 'value': formatted_goal + }, + { + 'name': "Completed", + 'color': 'green', + 'value': str(int(round(float(current_month_value)/float(goal)*100))) + "%" + } + ] + + data = { + 'title': title, + # 'subtitle': + 'y_values': values, + 'x_points': months, + 'specific_values': specific_values, + 'summary_values': summary_values + } + + return data