diff --git a/kpi_dashboard/README.rst b/kpi_dashboard/README.rst new file mode 100644 index 00000000..0c3200d9 --- /dev/null +++ b/kpi_dashboard/README.rst @@ -0,0 +1,96 @@ +============= +Kpi Dashboard +============= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Freporting--engine-lightgray.png?logo=github + :target: https://github.com/OCA/reporting-engine/tree/12.0/kpi_dashboard + :alt: OCA/reporting-engine +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/reporting-engine-12-0/reporting-engine-12-0-kpi_dashboard + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/143/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds new kinds of dashboards on a specific new type of view. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Configure KPIs +~~~~~~~~~~~~~~ + +#. Access `Dashboards > Configuration > KPI Dashboards > Configure KPI` +#. Create a new KPI specifying the computation method and the kpi type + + #. Number: result must contain a `value` and, if needed, a `previous` + #. Meter: result must contain `value`, `min` and `max` + #. Graph: result must contain a list on `graphs` containing `values`, `title` and `key` + + +Configure dashboards +~~~~~~~~~~~~~~~~~~~~ + +#. Access `Dashboards > Configuration > KPI Dashboards > Configure Dashboards` +#. Create a new dashboard and specify all the standard parameters on `Widget configuration` +#. Append elements on KPIs +#. You can preview the element using the dashboard view +#. You can create the menu entry directly using the `Generate menu` button + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Creu Blanca + +Contributors +~~~~~~~~~~~~ + +* Enric Tobella + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/reporting-engine `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/kpi_dashboard/__init__.py b/kpi_dashboard/__init__.py new file mode 100644 index 00000000..aee8895e --- /dev/null +++ b/kpi_dashboard/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/kpi_dashboard/__manifest__.py b/kpi_dashboard/__manifest__.py new file mode 100644 index 00000000..deb7d214 --- /dev/null +++ b/kpi_dashboard/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2020 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Kpi Dashboard", + "summary": """ + Create Dashboards using kpis""", + "version": "12.0.1.0.0", + "license": "AGPL-3", + "author": "Creu Blanca,Odoo Community Association (OCA)", + "website": "https://github.com/reporting-engine", + "depends": ["bus", "board", "base_sparse_field", "web_widget_color"], + "qweb": ["static/src/xml/dashboard.xml"], + "data": [ + "wizards/kpi_dashboard_menu.xml", + "security/security.xml", + "security/ir.model.access.csv", + "views/kpi_menu.xml", + "views/webclient_templates.xml", + "views/kpi_kpi.xml", + "views/kpi_dashboard.xml", + ], +} diff --git a/kpi_dashboard/models/__init__.py b/kpi_dashboard/models/__init__.py new file mode 100644 index 00000000..b67f67ba --- /dev/null +++ b/kpi_dashboard/models/__init__.py @@ -0,0 +1,4 @@ +from . import kpi_dashboard +from . import kpi_kpi +from . import ir_actions_act_window_view +from . import ir_ui_view diff --git a/kpi_dashboard/models/ir_actions_act_window_view.py b/kpi_dashboard/models/ir_actions_act_window_view.py new file mode 100644 index 00000000..ad2a2a8c --- /dev/null +++ b/kpi_dashboard/models/ir_actions_act_window_view.py @@ -0,0 +1,10 @@ +# Copyright 2019 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class IrActionsActWindowView(models.Model): + _inherit = "ir.actions.act_window.view" + + view_mode = fields.Selection(selection_add=[("dashboard", "Dashboard")]) diff --git a/kpi_dashboard/models/ir_ui_view.py b/kpi_dashboard/models/ir_ui_view.py new file mode 100644 index 00000000..6c4e3273 --- /dev/null +++ b/kpi_dashboard/models/ir_ui_view.py @@ -0,0 +1,10 @@ +# Copyright 2019 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class IrUiView(models.Model): + _inherit = "ir.ui.view" + + type = fields.Selection(selection_add=[("dashboard", "Dashboard")]) diff --git a/kpi_dashboard/models/kpi_dashboard.py b/kpi_dashboard/models/kpi_dashboard.py new file mode 100644 index 00000000..bdfb3683 --- /dev/null +++ b/kpi_dashboard/models/kpi_dashboard.py @@ -0,0 +1,182 @@ +# Copyright 2020 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + + +class KpiDashboard(models.Model): + + _name = "kpi.dashboard" + _description = "Dashboard" + + name = fields.Char(required=True) + active = fields.Boolean(default=True,) + item_ids = fields.One2many( + "kpi.dashboard.item", inverse_name="dashboard_id", copy=True, + ) + number_of_columns = fields.Integer(default=5, required=True) + width = fields.Integer(compute="_compute_width") + margin_y = fields.Integer(default=10, required=True) + margin_x = fields.Integer(default=10, required=True) + widget_dimension_x = fields.Integer(default=250, required=True) + widget_dimension_y = fields.Integer(default=250, required=True) + background_color = fields.Char(required=True, default="#f9f9f9") + group_ids = fields.Many2many("res.groups",) + menu_id = fields.Many2one("ir.ui.menu", copy=False) + + def write(self, vals): + res = super().write(vals) + if "group_ids" in vals: + for rec in self: + if rec.menu_id: + rec.menu_id.write( + {"groups_id": [(6, 0, rec.group_ids.ids)]} + ) + return res + + @api.depends("widget_dimension_x", "margin_x", "number_of_columns") + def _compute_width(self): + for rec in self: + rec.width = ( + rec.margin_x * (rec.number_of_columns + 1) + + rec.widget_dimension_x * rec.number_of_columns + ) + + def read_dashboard(self): + self.ensure_one() + result = { + "name": self.name, + "width": self.width, + "item_ids": self.item_ids.read_dashboard(), + "max_cols": self.number_of_columns, + "margin_x": self.margin_x, + "margin_y": self.margin_y, + "widget_dimension_x": self.widget_dimension_x, + "widget_dimension_y": self.widget_dimension_y, + "background_color": self.background_color, + } + if self.menu_id: + result["action_id"] = self.menu_id.action.id + return result + + def _generate_menu_vals(self, menu, action): + return { + "parent_id": menu.id or False, + "name": self.name, + "action": "%s,%s" % (action._name, action.id), + "groups_id": [(6, 0, self.group_ids.ids)], + } + + def _generate_action_vals(self, menu): + return { + "name": self.name, + "res_model": self._name, + "view_mode": "dashboard", + "res_id": self.id, + } + + def _generate_menu(self, menu): + action = self.env["ir.actions.act_window"].create( + self._generate_action_vals(menu) + ) + self.menu_id = self.env["ir.ui.menu"].create( + self._generate_menu_vals(menu, action) + ) + + +class KpiDashboardItem(models.Model): + _name = "kpi.dashboard.item" + _description = "Dashboard Items" + _order = "row asc, column asc" + + name = fields.Char(required=True) + kpi_id = fields.Many2one("kpi.kpi") + dashboard_id = fields.Many2one("kpi.dashboard", required=True,) + column = fields.Integer(required=True, default=1) + row = fields.Integer(required=True, default=1) + end_row = fields.Integer(store=True, compute='_compute_end_row') + end_column = fields.Integer(store=True, compute='_compute_end_column') + size_x = fields.Integer(required=True, default=1) + size_y = fields.Integer(required=True, default=1) + color = fields.Char() + font_color = fields.Char() + + @api.depends('row', 'size_y') + def _compute_end_row(self): + for r in self: + r.end_row = r.row + r.size_y - 1 + + @api.depends('column', 'size_x') + def _compute_end_column(self): + for r in self: + r.end_column = r.column + r.size_x - 1 + + @api.constrains('size_y') + def _check_size_y(self): + for rec in self: + if rec.size_y > 10: + raise ValidationError(_( + 'Size Y of the widget cannot be bigger than 10')) + + def _check_size_domain(self): + return [ + ('dashboard_id', '=', self.dashboard_id.id), + ('id', '!=', self.id), + ('row', '<=', self.end_row), + ('end_row', '>=', self.row), + ('column', '<=', self.end_column), + ('end_column', '>=', self.column), + ] + + @api.constrains('end_row', 'end_column', 'row', 'column') + def _check_size(self): + for r in self: + if self.search(r._check_size_domain(), limit=1): + raise ValidationError(_( + 'Widgets cannot be crossed by other widgets' + )) + if r.end_column > r.dashboard_id.number_of_columns: + raise ValidationError(_( + 'Widget %s is bigger than expected' + ) % r.display_name) + + @api.onchange("kpi_id") + def _onchange_kpi(self): + for rec in self: + if not rec.name and rec.kpi_id: + rec.name = rec.kpi_id.name + + def _read_dashboard(self): + vals = { + "id": self.id, + "name": self.name, + "col": self.column, + "row": self.row, + "sizex": self.size_x, + "sizey": self.size_y, + "color": self.color, + "font_color": self.font_color or "000000", + } + if self.kpi_id: + vals.update( + { + "widget": self.kpi_id.widget, + "kpi_id": self.kpi_id.id, + "suffix": self.kpi_id.suffix or "", + "prefix": self.kpi_id.prefix or "", + "value": self.kpi_id.value, + "value_last_update": self.kpi_id.value_last_update, + } + ) + if self.kpi_id.action_ids: + vals["actions"] = self.kpi_id.action_ids.read_dashboard() + else: + vals["widget"] = "base_text" + return vals + + def read_dashboard(self): + result = [] + for kpi in self: + result.append(kpi._read_dashboard()) + return result diff --git a/kpi_dashboard/models/kpi_kpi.py b/kpi_dashboard/models/kpi_kpi.py new file mode 100644 index 00000000..41e6c5b6 --- /dev/null +++ b/kpi_dashboard/models/kpi_kpi.py @@ -0,0 +1,110 @@ +# Copyright 2020 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models +import ast + + +class KpiKpi(models.Model): + _name = "kpi.kpi" + _description = "Kpi Kpi" + + name = fields.Char(required=True) + active = fields.Boolean(default=True) + cron_id = fields.Many2one("ir.cron", readonly=True, copy=False) + computation_method = fields.Selection( + [("function", "Function")], required=True + ) + value = fields.Serialized() + dashboard_item_ids = fields.One2many("kpi.dashboard.item", inverse_name="kpi_id") + model_id = fields.Many2one("ir.model",) + function = fields.Char() + args = fields.Char() + kwargs = fields.Char() + widget = fields.Selection( + [("number", "Number"), ("meter", "Meter"), ("graph", "Graph")], + required=True, + default="number", + ) + value_last_update = fields.Datetime(readonly=True) + prefix = fields.Char() + suffix = fields.Char() + action_ids = fields.One2many( + "kpi.kpi.action", + inverse_name='kpi_id', + help="Actions that can be opened from the KPI" + ) + + def _cron_vals(self): + return { + "name": self.name, + "model_id": self.env.ref("kpi_dashboard.model_kpi_kpi").id, + "interval_number": 1, + "interval_type": "hours", + "state": "code", + "code": "model.browse(%s).compute()" % self.id, + "active": True, + } + + def compute(self): + for record in self: + record._compute() + return True + + def _compute(self): + self.write( + { + "value": getattr( + self, "_compute_value_%s" % self.computation_method + )() + } + ) + notifications = [] + for dashboard_item in self.dashboard_item_ids: + channel = "kpi_dashboard_%s" % dashboard_item.dashboard_id.id + notifications.append([channel, dashboard_item._read_dashboard()]) + if notifications: + self.env["bus.bus"].sendmany(notifications) + + def _compute_value_function(self): + obj = self + if self.model_id: + obj = self.env[self.model_id.model] + args = ast.literal_eval(self.args or "[]") + kwargs = ast.literal_eval(self.kwargs or "{}") + return getattr(obj, self.function)(*args, **kwargs) + + def generate_cron(self): + self.ensure_one() + self.cron_id = self.env["ir.cron"].create(self._cron_vals()) + + @api.multi + def write(self, vals): + if "value" in vals: + vals["value_last_update"] = fields.Datetime.now() + return super().write(vals) + + +class KpiKpiAction(models.Model): + _name = 'kpi.kpi.action' + _description = 'KPI action' + + kpi_id = fields.Many2one('kpi.kpi', required=True, ondelete='cascade') + action = fields.Reference( + selection=[('ir.actions.report', 'ir.actions.report'), + ('ir.actions.act_window', 'ir.actions.act_window'), + ('ir.actions.act_url', 'ir.actions.act_url'), + ('ir.actions.server', 'ir.actions.server'), + ('ir.actions.client', 'ir.actions.client')], + required=True, + ) + + def read_dashboard(self): + result = [] + for r in self: + result.append({ + 'id': r.action.id, + 'type': r.action._name, + 'name': r.action.name + }) + return result diff --git a/kpi_dashboard/readme/CONFIGURE.rst b/kpi_dashboard/readme/CONFIGURE.rst new file mode 100644 index 00000000..c4d0bea0 --- /dev/null +++ b/kpi_dashboard/readme/CONFIGURE.rst @@ -0,0 +1,19 @@ +Configure KPIs +~~~~~~~~~~~~~~ + +#. Access `Dashboards > Configuration > KPI Dashboards > Configure KPI` +#. Create a new KPI specifying the computation method and the kpi type + + #. Number: result must contain a `value` and, if needed, a `previous` + #. Meter: result must contain `value`, `min` and `max` + #. Graph: result must contain a list on `graphs` containing `values`, `title` and `key` + + +Configure dashboards +~~~~~~~~~~~~~~~~~~~~ + +#. Access `Dashboards > Configuration > KPI Dashboards > Configure Dashboards` +#. Create a new dashboard and specify all the standard parameters on `Widget configuration` +#. Append elements on KPIs +#. You can preview the element using the dashboard view +#. You can create the menu entry directly using the `Generate menu` button diff --git a/kpi_dashboard/readme/CONTRIBUTORS.rst b/kpi_dashboard/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..93ec993e --- /dev/null +++ b/kpi_dashboard/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Enric Tobella diff --git a/kpi_dashboard/readme/DESCRIPTION.rst b/kpi_dashboard/readme/DESCRIPTION.rst new file mode 100644 index 00000000..82da260f --- /dev/null +++ b/kpi_dashboard/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module adds new kinds of dashboards on a specific new type of view. diff --git a/kpi_dashboard/security/ir.model.access.csv b/kpi_dashboard/security/ir.model.access.csv new file mode 100644 index 00000000..6f7b25b7 --- /dev/null +++ b/kpi_dashboard/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_kpi_dashboard,access_kpi_dashboard,model_kpi_dashboard,base.group_user,1,0,0,0 +access_kpi_dashboard_kpi,access_kpi_dashboard_kpi,model_kpi_dashboard_item,base.group_user,1,0,0,0 +access_kpi_kpi,access_kpi_kpi,model_kpi_kpi,base.group_user,1,0,0,0 +access_kpi_kpi_action,access_kpi_kpi_action,model_kpi_kpi_action,base.group_user,1,0,0,0 +manage_kpi_dashboard,manage_kpi_dashboard,model_kpi_dashboard,group_kpi_dashboard_manager,1,1,1,1 +manage_kpi_dashboard_kpi,manage_kpi_dashboard_kpi,model_kpi_dashboard_item,group_kpi_dashboard_manager,1,1,1,1 +manage_kpi_kpi,manage_kpi_kpi,model_kpi_kpi,group_kpi_dashboard_manager,1,1,1,1 +manage_kpi_kpi_action,manage_kpi_kpi_action,model_kpi_kpi_action,group_kpi_dashboard_manager,1,1,1,1 diff --git a/kpi_dashboard/security/security.xml b/kpi_dashboard/security/security.xml new file mode 100755 index 00000000..fb16b0f7 --- /dev/null +++ b/kpi_dashboard/security/security.xml @@ -0,0 +1,22 @@ + + + + Manage KPI Dashboards + + + + + + KPI Dashboard: User + + ['|', ('group_ids', '=', False), ('group_ids', 'in', user.groups_id.ids)] + + + + KPI Dashboard: All + + [(1, '=', 1)] + + + + diff --git a/kpi_dashboard/static/description/icon.png b/kpi_dashboard/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/kpi_dashboard/static/description/icon.png differ diff --git a/kpi_dashboard/static/description/index.html b/kpi_dashboard/static/description/index.html new file mode 100644 index 00000000..ae2c154b --- /dev/null +++ b/kpi_dashboard/static/description/index.html @@ -0,0 +1,449 @@ + + + + + + +Kpi Dashboard + + + +
+

Kpi Dashboard

+ + +

Beta License: AGPL-3 OCA/reporting-engine Translate me on Weblate Try me on Runbot

+

This module adds new kinds of dashboards on a specific new type of view.

+

Table of contents

+ +
+

Configuration

+
+

Configure KPIs

+
    +
  1. Access Dashboards > Configuration > KPI Dashboards > Configure KPI
  2. +
  3. Create a new KPI specifying the computation method and the kpi type
      +
    1. Number: result must contain a value and, if needed, a previous
    2. +
    3. Meter: result must contain value, min and max
    4. +
    5. Graph: result must contain a list on graphs containing values, title and key
    6. +
    +
  4. +
+
+
+

Configure dashboards

+
    +
  1. Access Dashboards > Configuration > KPI Dashboards > Configure Dashboards
  2. +
  3. Create a new dashboard and specify all the standard parameters on Widget configuration
  4. +
  5. Append elements on KPIs
  6. +
  7. You can preview the element using the dashboard view
  8. +
  9. You can create the menu entry directly using the Generate menu button
  10. +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Creu Blanca
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/reporting-engine project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/kpi_dashboard/static/lib/gauge/GaugeMeter.js b/kpi_dashboard/static/lib/gauge/GaugeMeter.js new file mode 100644 index 00000000..214d1c24 --- /dev/null +++ b/kpi_dashboard/static/lib/gauge/GaugeMeter.js @@ -0,0 +1,276 @@ +; +/* + * AshAlom Gauge Meter. Version 2.0.0 + * Copyright AshAlom.com All rights reserved. + * https://github.com/AshAlom/GaugeMeter <- Deleted! + * https://github.com/githubsrinath/GaugeMeter <- Backup original. + * + * Original created by Dr Ash Alom + * + * This is a bug fixed and modified version of the AshAlom Gauge Meter. + * Copyright 2018 Michael Wolf (Mictronics) + * https://github.com/mictronics/GaugeMeter + * + */ +!function ($) { + $.fn.gaugeMeter = function (t) { + var defaults = $.extend({ + id: "", + percent: 0, + used: null, + min: null, + total: null, + size: 100, + prepend: "", + append: "", + theme: "Red-Gold-Green", + color: "", + back: "RGBa(0,0,0,.06)", + width: 3, + style: "Full", + stripe: "0", + animationstep: 1, + animate_gauge_colors: false, + animate_text_colors: false, + label: "", + label_color: "Black", + text: "", + text_size: 0.22, + fill: "", + showvalue: false + }, t); + return this.each(function () { + + function getThemeColor(e) { + var t = "#2C94E0"; + return e || (e = 1e-14), + "Red-Gold-Green" === option.theme && (e > 0 && (t = "#d90000"), e > 10 && (t = "#e32100"), e > 20 && (t = "#f35100"), e > 30 && (t = "#ff8700"), e > 40 && (t = "#ffb800"), e > 50 && (t = "#ffd900"), e > 60 && (t = "#dcd800"), e > 70 && (t = "#a6d900"), e > 80 && (t = "#69d900"), e > 90 && (t = "#32d900")), + "Green-Gold-Red" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#69d900"), e > 20 && (t = "#a6d900"), e > 30 && (t = "#dcd800"), e > 40 && (t = "#ffd900"), e > 50 && (t = "#ffb800"), e > 60 && (t = "#ff8700"), e > 70 && (t = "#f35100"), e > 80 && (t = "#e32100"), e > 90 && (t = "#d90000")), + "Green-Red" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#41c900"), e > 20 && (t = "#56b300"), e > 30 && (t = "#6f9900"), e > 40 && (t = "#8a7b00"), e > 50 && (t = "#a75e00"), e > 60 && (t = "#c24000"), e > 70 && (t = "#db2600"), e > 80 && (t = "#f01000"), e > 90 && (t = "#ff0000")), + "Red-Green" === option.theme && (e > 0 && (t = "#ff0000"), e > 10 && (t = "#f01000"), e > 20 && (t = "#db2600"), e > 30 && (t = "#c24000"), e > 40 && (t = "#a75e00"), e > 50 && (t = "#8a7b00"), e > 60 && (t = "#6f9900"), e > 70 && (t = "#56b300"), e > 80 && (t = "#41c900"), e > 90 && (t = "#32d900")), + "DarkBlue-LightBlue" === option.theme && (e > 0 && (t = "#2c94e0"), e > 10 && (t = "#2b96e1"), e > 20 && (t = "#2b99e4"), e > 30 && (t = "#2a9ce7"), e > 40 && (t = "#28a0e9"), e > 50 && (t = "#26a4ed"), e > 60 && (t = "#25a8f0"), e > 70 && (t = "#24acf3"), e > 80 && (t = "#23aff5"), e > 90 && (t = "#21b2f7")), + "LightBlue-DarkBlue" === option.theme && (e > 0 && (t = "#21b2f7"), e > 10 && (t = "#23aff5"), e > 20 && (t = "#24acf3"), e > 30 && (t = "#25a8f0"), e > 40 && (t = "#26a4ed"), e > 50 && (t = "#28a0e9"), e > 60 && (t = "#2a9ce7"), e > 70 && (t = "#2b99e4"), e > 80 && (t = "#2b96e1"), e > 90 && (t = "#2c94e0")), + "DarkRed-LightRed" === option.theme && (e > 0 && (t = "#d90000"), e > 10 && (t = "#dc0000"), e > 20 && (t = "#e00000"), e > 30 && (t = "#e40000"), e > 40 && (t = "#ea0000"), e > 50 && (t = "#ee0000"), e > 60 && (t = "#f30000"), e > 70 && (t = "#f90000"), e > 80 && (t = "#fc0000"), e > 90 && (t = "#ff0000")), + "LightRed-DarkRed" === option.theme && (e > 0 && (t = "#ff0000"), e > 10 && (t = "#fc0000"), e > 20 && (t = "#f90000"), e > 30 && (t = "#f30000"), e > 40 && (t = "#ee0000"), e > 50 && (t = "#ea0000"), e > 60 && (t = "#e40000"), e > 70 && (t = "#e00000"), e > 80 && (t = "#dc0000"), e > 90 && (t = "#d90000")), + "DarkGreen-LightGreen" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#33db00"), e > 20 && (t = "#34df00"), e > 30 && (t = "#34e200"), e > 40 && (t = "#36e700"), e > 50 && (t = "#37ec00"), e > 60 && (t = "#38f100"), e > 70 && (t = "#38f600"), e > 80 && (t = "#39f900"), e > 90 && (t = "#3afc00")), + "LightGreen-DarkGreen" === option.theme && (e > 0 && (t = "#3afc00"), e > 10 && (t = "#39f900"), e > 20 && (t = "#38f600"), e > 30 && (t = "#38f100"), e > 40 && (t = "#37ec00"), e > 50 && (t = "#36e700"), e > 60 && (t = "#34e200"), e > 70 && (t = "#34df00"), e > 80 && (t = "#33db00"), e > 90 && (t = "#32d900")), + "DarkGold-LightGold" === option.theme && (e > 0 && (t = "#ffb800"), e > 10 && (t = "#ffba00"), e > 20 && (t = "#ffbd00"), e > 30 && (t = "#ffc200"), e > 40 && (t = "#ffc600"), e > 50 && (t = "#ffcb00"), e > 60 && (t = "#ffcf00"), e > 70 && (t = "#ffd400"), e > 80 && (t = "#ffd600"), e > 90 && (t = "#ffd900")), + "LightGold-DarkGold" === option.theme && (e > 0 && (t = "#ffd900"), e > 10 && (t = "#ffd600"), e > 20 && (t = "#ffd400"), e > 30 && (t = "#ffcf00"), e > 40 && (t = "#ffcb00"), e > 50 && (t = "#ffc600"), e > 60 && (t = "#ffc200"), e > 70 && (t = "#ffbd00"), e > 80 && (t = "#ffba00"), e > 90 && (t = "#ffb800")), + "White" === option.theme && (t = "#fff"), + "Black" === option.theme && (t = "#000"), + t; + } + /* The label below gauge. */ + function createLabel(t, a) { + if(t.children("b").length === 0){ + $("").appendTo(t).html(option.label).css({ + "line-height": option.size + 5 * a + "px", + color: option.label_color + }); + } + } + /* Prepend and append text, the gauge text or percentage value. */ + function createSpanTag(t) { + var fgcolor = ""; + if (option.animate_text_colors === true){ + fgcolor = option.fgcolor; + } + var child = t.children("span"); + if(child.length !== 0){ + child.html(r).css({color: fgcolor}); + return; + } + if(option.text_size <= 0.0 || Number.isNaN(option.text_size)){ + option.text_size = 0.22; + } + if(option.text_size > 0.5){ + option.text_size = 0.5; + } + $("").appendTo(t).html(r).css({ + "line-height": option.size + "px", + "font-size": option.text_size * option.size + "px", + color: fgcolor + }); + } + /* Get data attributes as options from div tag. Fall back to defaults when not exists. */ + function getDataAttr(t) { + $.each(dataAttr, function (index, element) { + if(t.data(element) !== undefined && t.data(element) !== null){ + option[element] = t.data(element); + } else { + option[element] = $(defaults).attr(element); + } + + if(element === "fill"){ + s = option[element]; + } + + if((element === "size" || + element === "width" || + element === "animationstep" || + element === "stripe" + ) && !Number.isInteger(option[element])){ + option[element] = parseInt(option[element]); + } + + if(element === "text_size"){ + option[element] = parseFloat(option[element]); + } + }); + } + /* Draws the gauge. */ + function drawGauge(a) { + if(M < 0) M = 0; + if(M > 100) M = 100; + var lw = option.width < 1 || isNaN(option.width) ? option.size / 20 : option.width; + g.clearRect(0, 0, b.width, b.height); + g.beginPath(); + g.arc(m, v, x, G, k, !1); + if(s){ + g.fillStyle = option.fill; + g.fill(); + } + g.lineWidth = lw; + g.strokeStyle = option.back; + option.stripe > parseInt(0) ? g.setLineDash([option.stripe], 1) : g.lineCap = "round"; + g.stroke(); + g.beginPath(); + g.arc(m, v, x, -I, P * a - I, !1); + g.lineWidth = lw; + g.strokeStyle = option.fgcolor; + g.stroke(); + c > M && (M += z, requestAnimationFrame(function(){ + drawGauge(Math.min(M, c) / 100); + }, p)); + } + + $(this).attr("data-id", $(this).attr("id")); + var r, + dataAttr = ["percent", + "used", + "min", + "total", + "size", + "prepend", + "append", + "theme", + "color", + "back", + "width", + "style", + "stripe", + "animationstep", + "animate_gauge_colors", + "animate_text_colors", + "label", + "label_color", + "text", + "text_size", + "fill", + "showvalue"], + option = {}, + c = 0, + p = $(this), + s = false; + p.addClass("gaugeMeter"); + getDataAttr(p); + + if(Number.isInteger(option.used) && Number.isInteger(option.total)){ + var u = option.used; + var t = option.total; + if(Number.isInteger(option.min)) { + if(option.min < 0) { + t -= option.min; + u -= option.min; + } + } + c = u / (t / 100); + } else { + if(Number.isInteger(option.percent)){ + c = option.percent; + } else { + c = parseInt(defaults.percent); + } + } + if(c < 0) c = 0; + if(c > 100) c = 100; + + if( option.text !== "" && option.text !== null && option.text !== undefined){ + if(option.append !== "" && option.append !== null && option.append !== undefined){ + r = option.text + "" + option.append + ""; + } else { + r = option.text; + } + if(option.prepend !== "" && option.prepend !== null && option.prepend !== undefined){ + r = "" + option.prepend + "" + r; + } + } else { + if(defaults.showvalue === true || option.showvalue === true){ + r = option.used; + } else { + r = c.toString(); + } + if(option.prepend !== "" && option.prepend !== null && option.prepend !== undefined){ + r = "" + option.prepend + "" + r; + } + + if(option.append !== "" && option.append !== null && option.append !== undefined){ + r = r + "" + option.append + ""; + } + } + + option.fgcolor = getThemeColor(c); + if(option.color !== "" && option.color !== null && option.color !== undefined){ + option.fgcolor = option.color; + } + + if(option.animate_gauge_colors === true){ + option.fgcolor = getThemeColor(c); + } + createSpanTag(p); + + if(option.style !== "" && option.style !== null && option.style !== undefined){ + createLabel(p, option.size / 13); + } + + $(this).width(option.size + "px"); + + var b = $("").attr({width: option.size, height: option.size}).get(0), + g = b.getContext("2d"), + m = b.width / 2, + v = b.height / 2, + _ = 360 * option.percent, + x = (_ * (Math.PI / 180), b.width / 2.5), + k = 2.3 * Math.PI, + G = 0, + M = 0 === option.animationstep ? c : 0, + z = Math.max(option.animationstep, 0), + P = 2 * Math.PI, + I = Math.PI / 2, + R = option.style; + var child = $(this).children("canvas"); + if(child.length !== 0){ + /* Replace existing canvas when new percentage was written. */ + child.replaceWith(b); + } else { + /* Initially create canvas. */ + $(b).appendTo($(this)); + } + + if ("Semi" === R){ + k = 2 * Math.PI; + G = 3.13; + P = 1 * Math.PI; + I = Math.PI / .996; + } + if ("Arch" === R){ + k = 2.195 * Math.PI; + G = 1, G = 655.99999; + P = 1.4 * Math.PI; + I = Math.PI / .8335; + } + drawGauge(M / 100); + }); + }; +} +(jQuery); diff --git a/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.css b/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.css new file mode 100644 index 00000000..dddc94a0 --- /dev/null +++ b/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.css @@ -0,0 +1,2 @@ +/*! gridster.js - v0.8.0 - 2019-01-10 - * https://dsmorse.github.io/gridster.js/ - Copyright (c) 2019 ducksboard; Licensed MIT */ +.gridster{position:relative}.gridster>*{-webkit-transition:height .4s,width .4s;-moz-transition:height .4s,width .4s;-o-transition:height .4s,width .4s;-ms-transition:height .4s,width .4s;transition:height .4s,width .4s}.gridster .gs-w{z-index:2;position:absolute}.gridster .preview-holder{z-index:1;position:absolute;background-color:#fff;border-color:#fff;opacity:.3}.gridster .player-revert{z-index:10!important;-webkit-transition:left .3s,top .3s!important;-moz-transition:left .3s,top .3s!important;-o-transition:left .3s,top .3s!important;transition:left .3s,top .3s!important}.gridster.collapsed{height:auto!important}.gridster.collapsed .gs-w{position:static!important}.ready .gs-w:not(.preview-holder),.ready .resize-preview-holder{-webkit-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-moz-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-o-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;transition:opacity .3s,left .3s,top .3s,width .3s,height .3s}.gridster .dragging,.gridster .resizing{z-index:10!important;-webkit-transition:all 0s!important;-moz-transition:all 0s!important;-o-transition:all 0s!important;transition:all 0s!important}.gs-resize-handle{position:absolute;z-index:1}.gs-resize-handle-both{width:20px;height:20px;bottom:-8px;right:-8px;background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4=);background-position:top left;background-repeat:no-repeat;cursor:se-resize;z-index:20}.gs-resize-handle-x{top:0;bottom:13px;right:-5px;width:10px;cursor:e-resize}.gs-resize-handle-y{left:0;right:13px;bottom:-5px;height:10px;cursor:s-resize}.gs-w:hover .gs-resize-handle,.resizing .gs-resize-handle{opacity:1}.gs-resize-handle,.gs-w.dragging .gs-resize-handle{opacity:0}.gs-resize-disabled .gs-resize-handle,[data-max-sizex="1"] .gs-resize-handle-x,[data-max-sizey="1"] .gs-resize-handle-y,[data-max-sizey="1"][data-max-sizex="1"] .gs-resize-handle{display:none!important} \ No newline at end of file diff --git a/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.js b/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.js new file mode 100644 index 00000000..7bcfaf03 --- /dev/null +++ b/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.js @@ -0,0 +1,2 @@ +/*! gridster.js - v0.8.0 - 2019-01-10 - * https://dsmorse.github.io/gridster.js/ - Copyright (c) 2019 ducksboard; Licensed MIT */ !function(a,b){"use strict";"object"==typeof exports?module.exports=b(require("jquery")):"function"==typeof define&&define.amd?define("gridster-coords",["jquery"],b):a.GridsterCoords=b(a.$||a.jQuery)}(this,function(a){"use strict";function b(b){return b[0]&&a.isPlainObject(b[0])?this.data=b[0]:this.el=b,this.isCoords=!0,this.coords={},this.init(),this}var c=b.prototype;return c.init=function(){this.set(),this.original_coords=this.get()},c.set=function(a,b){var c=this.el;if(c&&!a&&(this.data=c.offset(),this.data.width=c[0].scrollWidth,this.data.height=c[0].scrollHeight),c&&a&&!b){var d=c.offset();this.data.top=d.top,this.data.left=d.left}var e=this.data;return void 0===e.left&&(e.left=e.x1),void 0===e.top&&(e.top=e.y1),this.coords.x1=e.left,this.coords.y1=e.top,this.coords.x2=e.left+e.width,this.coords.y2=e.top+e.height,this.coords.cx=e.left+e.width/2,this.coords.cy=e.top+e.height/2,this.coords.width=e.width,this.coords.height=e.height,this.coords.el=c||!1,this},c.update=function(b){if(!b&&!this.el)return this;if(b){var c=a.extend({},this.data,b);return this.data=c,this.set(!0,!0)}return this.set(!0),this},c.get=function(){return this.coords},c.destroy=function(){this.el.removeData("coords"),delete this.el},a.fn.coords=function(){if(this.data("coords"))return this.data("coords");var a=new b(this);return this.data("coords",a),a},b}),function(a,b){"use strict";"object"==typeof exports?module.exports=b(require("jquery")):"function"==typeof define&&define.amd?define("gridster-collision",["jquery","gridster-coords"],b):a.GridsterCollision=b(a.$||a.jQuery,a.GridsterCoords)}(this,function(a,b){"use strict";function c(b,c,e){this.options=a.extend(d,e),this.$element=b,this.last_colliders=[],this.last_colliders_coords=[],this.set_colliders(c),this.init()}var d={colliders_context:document.body,overlapping_region:"C"};c.defaults=d;var e=c.prototype;return e.init=function(){this.find_collisions()},e.overlaps=function(a,b){var c=!1,d=!1;return(b.x1>=a.x1&&b.x1<=a.x2||b.x2>=a.x1&&b.x2<=a.x2||a.x1>=b.x1&&a.x2<=b.x2)&&(c=!0),(b.y1>=a.y1&&b.y1<=a.y2||b.y2>=a.y1&&b.y2<=a.y2||a.y1>=b.y1&&a.y2<=b.y2)&&(d=!0),c&&d},e.detect_overlapping_region=function(a,b){var c="",d="";return a.y1>b.cy&&a.y1b.y1&&a.y2b.cx&&a.x1b.x1&&a.x2this.player_max_left?e=this.player_max_left:ethis.player_max_top?f=this.player_max_top:f=q&&(l=n+h)0&&(this.$scroll_container[i](l),this["scroll_offset_"+a]-=h),this},j.manage_scroll=function(a){this.scroll_in("x",a),this.scroll_in("y",a)},j.calculate_dimensions=function(){this.scroller_height=this.$scroll_container.height(),this.scroller_width=this.$scroll_container.width()},j.drag_handler=function(b){if(!this.disabled&&(1===b.which||f)&&!this.ignore_drag(b)){var c=this,d=!0;return this.$player=a(b.currentTarget),this.el_init_pos=this.get_actual_pos(this.$player),this.mouse_init_pos=this.get_mouse_pos(b),this.offsetY=this.mouse_init_pos.top-this.el_init_pos.top,this.$document.on(this.pointer_events.move,function(a){var b=c.get_mouse_pos(a),e=Math.abs(b.left-c.mouse_init_pos.left),f=Math.abs(b.top-c.mouse_init_pos.top);return(e>c.options.distance||f>c.options.distance)&&(d?(d=!1,c.on_dragstart.call(c,a),!1):(!0===c.is_dragging&&c.on_dragmove.call(c,a),!1))}),!!f&&void 0}},j.on_dragstart=function(a){if(a.preventDefault(),this.is_dragging)return this;this.drag_start=this.is_dragging=!0;var b=this.$container.offset();return this.baseX=Math.round(b.left),this.baseY=Math.round(b.top),"clone"===this.options.helper?(this.$helper=this.$player.clone().appendTo(this.$container).addClass("helper"),this.helper=!0):this.helper=!1,this.scroll_container_offset_y=this.$scroll_container.scrollTop(),this.scroll_container_offset_x=this.$scroll_container.scrollLeft(),this.el_init_offset=this.$player.offset(),this.player_width=this.$player.width(),this.player_height=this.$player.height(),this.set_limits(this.options.container_width),this.options.start&&this.options.start.call(this.$player,a,this.get_drag_data(a)),!1},j.on_dragmove=function(a){var b=this.get_drag_data(a);this.options.autoscroll&&this.manage_scroll(b),this.options.move_element&&(this.helper?this.$helper:this.$player).css({position:"absolute",left:b.position.left,top:b.position.top});var c=this.last_position||b.position;return b.prev_position=c,this.options.drag&&this.options.drag.call(this.$player,a,b),this.last_position=b.position,!1},j.on_dragstop=function(a){var b=this.get_drag_data(a);return this.drag_start=!1,this.options.stop&&this.options.stop.call(this.$player,a,b),this.helper&&this.options.remove_helper&&this.$helper.remove(),!1},j.on_select_start=function(a){if(!this.disabled&&!this.ignore_drag(a))return!1},j.enable=function(){this.disabled=!1},j.disable=function(){this.disabled=!0},j.destroy=function(){this.disable(),this.$container.off(this.ns),this.$document.off(this.ns),d.off(this.ns),a.removeData(this.$container,"drag")},j.ignore_drag=function(b){return this.options.handle?!a(b.target).is(this.options.handle):a.isFunction(this.options.ignore_dragging)?this.options.ignore_dragging(b):this.options.resize?!a(b.target).is(this.options.items):a(b.target).is(this.options.ignore_dragging.join(", "))},a.fn.gridDraggable=function(a){return new b(this,a)},a.fn.dragg=function(c){return this.each(function(){a.data(this,"drag")||a.data(this,"drag",new b(this,c))})},b}),function(a,b){"use strict";"object"==typeof exports?module.exports=b(require("jquery"),require("./jquery.draggable.js"),require("./jquery.collision.js"),require("./jquery.coords.js"),require("./utils.js")):"function"==typeof define&&define.amd?define(["jquery","gridster-draggable","gridster-collision"],b):a.Gridster=b(a.$||a.jQuery,a.GridsterDraggable,a.GridsterCollision)}(this,function(a,b,c){"use strict";function d(b,c){this.options=a.extend(!0,{},g,c),this.options.draggable=this.options.draggable||{},this.options.draggable=a.extend(!0,{},this.options.draggable,{scroll_container:this.options.scroll_container}),this.$el=a(b),this.$scroll_container=this.options.scroll_container===window?a(window):this.$el.closest(this.options.scroll_container),this.$wrapper=this.$el.parent(),this.$widgets=this.$el.children(this.options.widget_selector).addClass("gs-w"),this.$changed=a([]),this.w_queue={},this.is_responsive()?this.min_widget_width=this.get_responsive_col_width():this.min_widget_width=this.options.widget_base_dimensions[0],this.min_widget_height=this.options.widget_base_dimensions[1],this.is_resizing=!1,this.min_col_count=this.options.min_cols,this.prev_col_count=this.min_col_count,this.generated_stylesheets=[],this.$style_tags=a([]),typeof this.options.limit==typeof!0&&(console.log("limit: bool is deprecated, consider using limit: { width: boolean, height: boolean} instead"),this.options.limit={width:this.options.limit,height:this.options.limit}),this.options.auto_init&&this.init()}function e(a){for(var b=["col","row","size_x","size_y"],c={},d=0,e=b.length;dc.row?1:-1})},d.sort_by_row_and_col_asc=function(a){return a=a.sort(function(a,b){return a=e(a),b=e(b),a.row>b.row||a.row===b.row&&a.col>b.col?1:-1})},d.sort_by_col_asc=function(a){return a=a.sort(function(a,b){return a=e(a),b=e(b),a.col>b.col?1:-1})},d.sort_by_row_desc=function(a){return a=a.sort(function(a,b){return a=e(a),b=e(b),a.row+a.size_yi&&this.add_faux_rows(Math.max(d-i,0));var k={col:j,row:g.row,size_x:c,size_y:d};return this.mutate_widget_in_gridmap(b,g,k),this.set_dom_grid_height(),this.set_dom_grid_width(),f&&f.call(this,k.size_x,k.size_y),b},h.collapse_widget=function(a,b){var c=a.coords().grid,d=parseInt(a.attr("pre_expand_sizex")),e=parseInt(a.attr("pre_expand_sizey")),f=parseInt(a.attr("pre_expand_col")),g={col:f,row:c.row,size_x:d,size_y:e};return this.mutate_widget_in_gridmap(a,c,g),this.set_dom_grid_height(),this.set_dom_grid_width(),b&&b.call(this,g.size_x,g.size_y),a},h.fit_to_content=function(a,b,c,d){var e=a.coords().grid,f=this.$wrapper.width(),g=this.$wrapper.height(),h=this.options.widget_base_dimensions[0]+2*this.options.widget_margins[0],i=this.options.widget_base_dimensions[1]+2*this.options.widget_margins[1],j=Math.ceil((f+2*this.options.widget_margins[0])/h),k=Math.ceil((g+2*this.options.widget_margins[1])/i),l={col:e.col,row:e.row,size_x:Math.min(b,j),size_y:Math.min(c,k)};return this.mutate_widget_in_gridmap(a,e,l),this.set_dom_grid_height(),this.set_dom_grid_width(),d&&d.call(this,l.size_x,l.size_y),a},h.center_widgets=debounce(function(){var b,c=this.$wrapper.width();b=this.is_responsive()?this.get_responsive_col_width():this.options.widget_base_dimensions[0]+2*this.options.widget_margins[0];var d=2*Math.floor(Math.max(Math.floor(c/b),this.min_col_count)/2);this.options.min_cols=d,this.options.max_cols=d,this.options.extra_cols=0,this.set_dom_grid_width(d),this.cols=d;var e=(d-this.prev_col_count)/2;return e<0?(this.get_min_col()>-1*e?this.shift_cols(e):this.resize_widget_dimensions(this.options),setTimeout(a.proxy(function(){this.resize_widget_dimensions(this.options)},this),0)):e>0?(this.resize_widget_dimensions(this.options),setTimeout(a.proxy(function(){this.shift_cols(e)},this),0)):(this.resize_widget_dimensions(this.options),setTimeout(a.proxy(function(){this.resize_widget_dimensions(this.options)},this),0)),this.prev_col_count=d,this},200),h.get_min_col=function(){return Math.min.apply(Math,this.$widgets.map(a.proxy(function(b,c){return this.get_cells_occupied(a(c).coords().grid).cols},this)).get())},h.shift_cols=function(b){var c=this.$widgets.map(a.proxy(function(b,c){var d=a(c);return this.dom_to_coords(d)},this));c=d.sort_by_row_and_col_asc(c),c.each(a.proxy(function(c,d){var e=a(d.el),f=e.coords().grid,g=parseInt(e.attr("data-col")),h={col:Math.max(Math.round(g+b),1),row:f.row,size_x:f.size_x,size_y:f.size_y};setTimeout(a.proxy(function(){this.mutate_widget_in_gridmap(e,f,h)},this),0)},this))},h.mutate_widget_in_gridmap=function(b,c,d){var e=c.size_y,f=this.get_cells_occupied(c),g=this.get_cells_occupied(d),h=[];a.each(f.cols,function(b,c){-1===a.inArray(c,g.cols)&&h.push(c)});var i=[];a.each(g.cols,function(b,c){-1===a.inArray(c,f.cols)&&i.push(c)});var j=[];a.each(f.rows,function(b,c){-1===a.inArray(c,g.rows)&&j.push(c)});var k=[];if(a.each(g.rows,function(b,c){-1===a.inArray(c,f.rows)&&k.push(c)}),this.remove_from_gridmap(c),i.length){var l=[d.col,d.row,d.size_x,Math.min(e,d.size_y),b];this.empty_cells.apply(this,l)}if(k.length){var m=[d.col,d.row,d.size_x,d.size_y,b];this.empty_cells.apply(this,m)}if(c.col=d.col,c.row=d.row,c.size_x=d.size_x,c.size_y=d.size_y,this.add_to_gridmap(d,b),b.removeClass("player-revert"),this.update_widget_dimensions(b,d),this.options.shift_widgets_up){if(h.length){var n=[h[0],d.row,h[h.length-1]-h[0]+1,Math.min(e,d.size_y),b];this.remove_empty_cells.apply(this,n)}if(j.length){var o=[d.col,d.row,d.size_x,d.size_y,b];this.remove_empty_cells.apply(this,o)}}return this.move_widget_up(b),this},h.empty_cells=function(b,c,d,e,f){return this.widgets_below({col:b,row:c-e,size_x:d,size_y:e}).not(f).each(a.proxy(function(b,d){var f=a(d),g=f.coords().grid;if(g.row<=c+e-1){var h=c+e-g.row;this.move_widget_down(f,h)}},this)),this.is_resizing||this.set_dom_grid_height(),this},h.remove_empty_cells=function(b,c,d,e,f){return this.widgets_below({col:b,row:c,size_x:d,size_y:e}).not(f).each(a.proxy(function(b,c){this.move_widget_up(a(c),e)},this)),this.set_dom_grid_height(),this},h.next_position=function(a,b){a||(a=1),b||(b=1);for(var c,e=this.gridmap,f=e.length,g=[],h=1;h';return this.resize_handle_tpl=a.map(b,function(a){return c.replace("{type}",a)}).join(""),a.isArray(this.options.draggable.ignore_dragging)&&this.options.draggable.ignore_dragging.push("."+this.resize_handle_class),this},h.on_start_drag=function(b,c){this.$helper.add(this.$player).add(this.$wrapper).addClass("dragging"),this.highest_col=this.get_highest_occupied_cell().col,this.$player.addClass("player"),this.player_grid_data=this.$player.coords().grid,this.placeholder_grid_data=a.extend({},this.player_grid_data),this.get_highest_occupied_cell().row+this.player_grid_data.size_y<=this.options.max_rows&&this.set_dom_grid_height(this.$el.height()+this.player_grid_data.size_y*this.min_widget_height),this.set_dom_grid_width(this.cols);var d=this.player_grid_data.size_x,e=this.cols-this.highest_col;this.options.max_cols===1/0&&e<=d&&this.add_faux_cols(Math.min(d-e,1));var f=this.faux_grid,g=this.$player.data("coords").coords;this.cells_occupied_by_player=this.get_cells_occupied(this.player_grid_data),this.cells_occupied_by_placeholder=this.get_cells_occupied(this.placeholder_grid_data),this.last_cols=[],this.last_rows=[],this.collision_api=this.$helper.collision(f,this.options.collision),this.$preview_holder=a("<"+this.$player.get(0).tagName+" />",{class:"preview-holder","data-row":this.$player.attr("data-row"),"data-col":this.$player.attr("data-col"),css:{width:g.width,height:g.height}}).appendTo(this.$el),this.options.draggable.start&&this.options.draggable.start.call(this,b,c)},h.on_drag=function(a,b){if(null===this.$player)return!1;var c=this.options.widget_margins,d=this.$preview_holder.attr("data-col"),e=this.$preview_holder.attr("data-row"),f={left:b.position.left+this.baseX-c[0]*d,top:b.position.top+this.baseY-c[1]*e};if(this.options.max_cols===1/0){this.placeholder_grid_data.col+this.placeholder_grid_data.size_x-1>=this.cols-1&&this.options.max_cols>=this.cols+1&&(this.add_faux_cols(1),this.set_dom_grid_width(this.cols+1),this.drag_api.set_limits(this.cols*this.min_widget_width+(this.cols+1)*this.options.widget_margins[0])),this.collision_api.set_colliders(this.faux_grid)}this.colliders_data=this.collision_api.get_closest_colliders(f),this.on_overlapped_column_change(this.on_start_overlapping_column,this.on_stop_overlapping_column),this.on_overlapped_row_change(this.on_start_overlapping_row,this.on_stop_overlapping_row),this.helper&&this.$player&&this.$player.css({left:b.position.left,top:b.position.top}),this.options.draggable.drag&&this.options.draggable.drag.call(this,a,b)},h.on_stop_drag=function(a,b){this.$helper.add(this.$player).add(this.$wrapper).removeClass("dragging");var c=this.options.widget_margins,d=this.$preview_holder.attr("data-col"),e=this.$preview_holder.attr("data-row");b.position.left=b.position.left+this.baseX-c[0]*d,b.position.top=b.position.top+this.baseY-c[1]*e,this.colliders_data=this.collision_api.get_closest_colliders(b.position),this.on_overlapped_column_change(this.on_start_overlapping_column,this.on_stop_overlapping_column),this.on_overlapped_row_change(this.on_start_overlapping_row,this.on_stop_overlapping_row),this.$changed=this.$changed.add(this.$player);var f=this.placeholder_grid_data.el.coords().grid;f.col===this.placeholder_grid_data.col&&f.row===this.placeholder_grid_data.row||(this.update_widget_position(f,!1),this.options.collision.wait_for_mouseup&&this.for_each_cell_occupied(this.placeholder_grid_data,function(a,b){if(this.is_widget(a,b)){var c=this.placeholder_grid_data.row+this.placeholder_grid_data.size_y,d=parseInt(this.gridmap[a][b][0].getAttribute("data-row")),e=c-d;!this.move_widget_down(this.is_widget(a,b),e)&&this.set_placeholder(this.placeholder_grid_data.el.coords().grid.col,this.placeholder_grid_data.el.coords().grid.row)}})),this.cells_occupied_by_player=this.get_cells_occupied(this.placeholder_grid_data);var g=this.placeholder_grid_data.col,h=this.placeholder_grid_data.row;this.set_cells_player_occupies(g,h),this.$player.coords().grid.row=h,this.$player.coords().grid.col=g,this.$player.addClass("player-revert").removeClass("player").attr({"data-col":g,"data-row":h}).css({left:"",top:""}),this.options.draggable.stop&&this.options.draggable.stop.call(this,a,b),this.$preview_holder.remove(),this.$player=null,this.$helper=null,this.placeholder_grid_data={},this.player_grid_data={},this.cells_occupied_by_placeholder={},this.cells_occupied_by_player={},this.w_queue={},this.set_dom_grid_height(),this.set_dom_grid_width(),this.options.max_cols===1/0&&this.drag_api.set_limits(this.cols*this.min_widget_width+(this.cols+1)*this.options.widget_margins[0])},h.on_start_resize=function(b,c){this.$resized_widget=c.$player.closest(".gs-w"),this.resize_coords=this.$resized_widget.coords(),this.resize_wgd=this.resize_coords.grid,this.resize_initial_width=this.resize_coords.coords.width,this.resize_initial_height=this.resize_coords.coords.height,this.resize_initial_sizex=this.resize_coords.grid.size_x,this.resize_initial_sizey=this.resize_coords.grid.size_y,this.resize_initial_col=this.resize_coords.grid.col,this.resize_last_sizex=this.resize_initial_sizex, +this.resize_last_sizey=this.resize_initial_sizey,this.resize_max_size_x=Math.min(this.resize_wgd.max_size_x||this.options.resize.max_size[0],this.options.max_cols-this.resize_initial_col+1),this.resize_max_size_y=this.resize_wgd.max_size_y||this.options.resize.max_size[1],this.resize_min_size_x=this.resize_wgd.min_size_x||this.options.resize.min_size[0]||1,this.resize_min_size_y=this.resize_wgd.min_size_y||this.options.resize.min_size[1]||1,this.resize_initial_last_col=this.get_highest_occupied_cell().col,this.set_dom_grid_width(this.cols),this.resize_dir={right:c.$player.is("."+this.resize_handle_class+"-x"),bottom:c.$player.is("."+this.resize_handle_class+"-y")},this.is_responsive()||this.$resized_widget.css({"min-width":this.options.widget_base_dimensions[0],"min-height":this.options.widget_base_dimensions[1]});var d=this.$resized_widget.get(0).tagName;this.$resize_preview_holder=a("<"+d+" />",{class:"preview-holder resize-preview-holder","data-row":this.$resized_widget.attr("data-row"),"data-col":this.$resized_widget.attr("data-col"),css:{width:this.resize_initial_width,height:this.resize_initial_height}}).appendTo(this.$el),this.$resized_widget.addClass("resizing"),this.options.resize.start&&this.options.resize.start.call(this,b,c,this.$resized_widget),this.$el.trigger("gridster:resizestart")},h.on_stop_resize=function(b,c){this.$resized_widget.removeClass("resizing").css({width:"",height:"","min-width":"","min-height":""}),delay(a.proxy(function(){this.$resize_preview_holder.remove().css({"min-width":"","min-height":""}),this.options.resize.stop&&this.options.resize.stop.call(this,b,c,this.$resized_widget),this.$el.trigger("gridster:resizestop")},this),300),this.set_dom_grid_width(),this.set_dom_grid_height(),this.options.max_cols===1/0&&this.drag_api.set_limits(this.cols*this.min_widget_width)},h.on_resize=function(a,b){var c,d=b.pointer.diff_left,e=b.pointer.diff_top,f=this.is_responsive()?this.get_responsive_col_width():this.options.widget_base_dimensions[0],g=this.options.widget_base_dimensions[1],h=this.options.widget_margins[0],i=this.options.widget_margins[1],j=this.resize_max_size_x,k=this.resize_min_size_x,l=this.resize_max_size_y,m=this.resize_min_size_y,n=this.options.max_cols===1/0,o=Math.ceil(d/(f+2*h)-.2),p=Math.ceil(e/(g+2*i)-.2),q=Math.max(1,this.resize_initial_sizex+o),r=Math.max(1,this.resize_initial_sizey+p),s=Math.floor(this.container_width/this.min_widget_width-this.resize_initial_col+1),t=s*this.min_widget_width+(s-1)*h;q=Math.max(Math.min(q,j),k),q=Math.min(s,q),c=j*f+(q-1)*h;var u=Math.min(c,t),v=k*f+(q-1)*h;r=Math.max(Math.min(r,l),m);var w=l*g+(r-1)*i,x=m*g+(r-1)*i;if(this.resize_dir.right?r=this.resize_initial_sizey:this.resize_dir.bottom&&(q=this.resize_initial_sizex),n){var y=this.resize_initial_col+q-1;n&&this.resize_initial_last_col<=y&&(this.set_dom_grid_width(Math.max(y+1,this.cols)),this.colsparseInt(this.options.max_rows)&&(e=!0),h>parseInt(this.options.max_cols)&&(e=!0),this.is_player_in(h,i)&&(e=!0)}return e},h.can_placeholder_be_set=function(a,b,c,d){for(var e=!0,f=0;fparseInt(this.options.max_rows)&&(e=!1),h>parseInt(this.options.max_cols)&&(e=!1),this.is_occupied(h,i)&&!this.is_widget_queued_and_can_move(j)&&(e=!1)}return e},h.queue_widget=function(a,b,c){var d=c,e=d.coords().grid,f=a+"_"+b;if(f in this.w_queue)return!1;this.w_queue[f]=d;for(var g=0;g=0&&a.inArray(c,d.rows)>=0},h.is_placeholder_in=function(b,c){var d=this.cells_occupied_by_placeholder||{};return this.is_placeholder_in_col(b)&&a.inArray(c,d.rows)>=0},h.is_placeholder_in_col=function(b){var c=this.cells_occupied_by_placeholder||[];return a.inArray(b,c.cols)>=0},h.is_empty=function(a,b){return void 0===this.gridmap[a]||!this.gridmap[a][b]},h.is_valid_col=function(a,b){return this.options.max_cols===1/0||this.cols>=this.calculate_highest_col(a,b)},h.is_valid_row=function(a,b){return this.rows>=this.calculate_highest_row(a,b)},h.calculate_highest_col=function(a,b){return a+(b||1)-1},h.calculate_highest_row=function(a,b){return a+(b||1)-1},h.is_occupied=function(b,c){return!!this.gridmap[b]&&(!this.is_player(b,c)&&(!!this.gridmap[b][c]&&(!this.options.ignore_self_occupied||this.$player.data()!==a(this.gridmap[b][c]).data())))},h.is_widget=function(a,b){var c=this.gridmap[a];return!!c&&((c=c[b])||!1)},h.is_static=function(a,b){var c=this.gridmap[a];return!!c&&!(!(c=c[b])||!c.hasClass(this.options.static_class))},h.is_widget_under_player=function(a,b){return!!this.is_widget(a,b)&&this.is_player_in(a,b)},h.get_widgets_under_player=function(b){b||(b=this.cells_occupied_by_player||{cols:[],rows:[]});var c=a([]);return a.each(b.cols,a.proxy(function(d,e){a.each(b.rows,a.proxy(function(a,b){this.is_widget(e,b)&&(c=c.add(this.gridmap[e][b]))},this))},this)),c},h.set_placeholder=function(b,c){var d=a.extend({},this.placeholder_grid_data),e=b+d.size_x-1;e>this.cols&&(b-=e-b);var f=this.placeholder_grid_data.row0&&(this.is_empty(a,h)||this.is_player(a,h)||this.is_widget(a,h)&&g[h].is(f));)d[a].push(h),e=h0&&(!this.is_widget(f,h)||this.is_player_in(f,h)||g[h].is(a.el));)this.is_player(f,h)||this.is_placeholder_in(f,h)||this.is_player_in(f,h)||d[f].push(h),h=b&&a[d[0]]},h.get_widgets_overlapped=function(){var b=a([]),c=[],d=this.cells_occupied_by_player.rows.slice(0);return d.reverse(),a.each(this.cells_occupied_by_player.cols,a.proxy(function(e,f){a.each(d,a.proxy(function(d,e){if(!this.gridmap[f])return!0;var g=this.gridmap[f][e];this.is_occupied(f,e)&&!this.is_player(g)&&-1===a.inArray(g,c)&&(b=b.add(g),c.push(g))},this))},this)),b},h.on_start_overlapping_column=function(a){this.set_player(a,void 0,!1)},h.on_start_overlapping_row=function(a){this.set_player(void 0,a,!1)},h.on_stop_overlapping_column=function(a){var b=this;this.options.shift_larger_widgets_down&&this.for_each_widget_below(a,this.cells_occupied_by_player.rows[0],function(a,c){b.move_widget_up(this,b.player_grid_data.size_y)})},h.on_stop_overlapping_row=function(a){var b=this,c=this.cells_occupied_by_player.cols;if(this.options.shift_larger_widgets_down)for(var d=0,e=c.length;dthis.options.max_rows)return!1;if(f=[],g=c,!b)return!1;if(this.failed=!1,-1===a.inArray(b,f)){var h=b.coords().grid,i=e+c;if(this.widgets_below(b).each(a.proxy(function(b,c){if(!0!==this.failed){var d=a(c),e=d.coords().grid,f=this.displacement_diff(e,h,g);f>0&&(this.failed=!1===this.move_widget_down(d,f))}},this)),this.failed)return!1;this.remove_from_gridmap(h),h.row=i,this.update_widget_position(h,b),b.attr("data-row",h.row),this.$changed=this.$changed.add(b),f.push(b)}return!0},h.can_go_up_to_row=function(b,c,d){var e,f=!0,g=[],h=b.row;if(this.for_each_column_occupied(b,function(a){for(g[a]=[],e=h;e--&&this.is_empty(a,e)&&!this.is_placeholder_in(a,e);)g[a].push(e);if(!g[a].length)return f=!1,!0}),!f)return!1;for(e=d,e=1;e0?c:0},h.widgets_below=function(b){var c=a([]),e=a.isPlainObject(b)?b:b.coords().grid;if(void 0===e)return c;var f=this,g=e.row+e.size_y-1;return this.for_each_column_occupied(e,function(b){f.for_each_widget_below(b,g,function(b,d){if(!f.is_player(this)&&-1===a.inArray(this,c))return c=c.add(this),!0})}),d.sort_by_row_asc(c)},h.set_cells_player_occupies=function(a,b){return this.remove_from_gridmap(this.placeholder_grid_data),this.placeholder_grid_data.col=a,this.placeholder_grid_data.row=b,this.add_to_gridmap(this.placeholder_grid_data,this.$player),this},h.empty_cells_player_occupies=function(){return this.remove_from_gridmap(this.placeholder_grid_data),this},h.can_go_down=function(b){var c=!0,d=this;return b.hasClass(this.options.static_class)&&(c=!1),this.widgets_below(b).each(function(){a(this).hasClass(d.options.static_class)&&(c=!1)}),c},h.can_go_up=function(a){var b=a.coords().grid,c=b.row,d=c-1,e=!0;return 1!==c&&(this.for_each_column_occupied(b,function(a){if(this.is_occupied(a,d)||this.is_player(a,d)||this.is_placeholder_in(a,d)||this.is_player_in(a,d))return e=!1,!0}),e)},h.can_move_to=function(a,b,c){var d=a.el,e={size_y:a.size_y,size_x:a.size_x,col:b,row:c},f=!0;if(this.options.max_cols!==1/0){if(b+a.size_x-1>this.cols)return!1}return!(this.options.max_rows0&&this.is_widget(d,m)&&-1===a.inArray(g[d][m],l)&&(h=f.call(g[d][m],d,m),l.push(g[d][m]),h)););},"for_each/below":function(){for(m=e+1,i=g[d].length;m=1;f--)for(a=c-1;a>=1;a--)if(this.is_widget(f,a)){d.push(a),e.push(f);break}return{col:Math.max.apply(Math,e),row:Math.max.apply(Math,d)}},h.get_widgets_in_range=function(b,c,d,e){var f,g,h,i,j=a([]);for(f=d;f>=b;f--)for(g=e;g>=c;g--)!1!==(h=this.is_widget(f,g))&&(i=h.data("coords").grid,i.col>=b&&i.col<=d&&i.row>=c&&i.row<=e&&(j=j.add(h)));return j},h.get_widgets_at_cell=function(a,b){return this.get_widgets_in_range(a,b,a,b)},h.get_widgets_from=function(b,c){var d=a();return b&&(d=d.add(this.$widgets.filter(function(){var c=parseInt(a(this).attr("data-col"));return c===b||c>b}))),c&&(d=d.add(this.$widgets.filter(function(){var b=parseInt(a(this).attr("data-row"));return b===c||b>c}))),d},h.set_dom_grid_height=function(a){if(void 0===a){var b=this.get_highest_occupied_cell().row;a=(b+1)*this.options.widget_margins[1]+b*this.min_widget_height}return this.container_height=a,this.$el.css("height",this.container_height),this},h.set_dom_grid_width=function(a){void 0===a&&(a=this.get_highest_occupied_cell().col);var b=this.options.max_cols===1/0?this.options.max_cols:this.cols;return a=Math.min(b,Math.max(a,this.options.min_cols)),this.container_width=(a+1)*this.options.widget_margins[0]+a*this.min_widget_width,this.is_responsive()?(this.$el.css({"min-width":"100%","max-width":"100%"}),this):(this.$el.css("width",this.container_width),this)},h.is_responsive=function(){return this.options.autogenerate_stylesheet&&"auto"===this.options.widget_base_dimensions[0]&&this.options.max_cols!==1/0},h.get_responsive_col_width=function(){var a=this.cols||this.options.max_cols;return(this.$el[0].clientWidth-3-(a+1)*this.options.widget_margins[0])/a},h.resize_responsive_layout=function(){return this.min_widget_width=this.get_responsive_col_width(),this.generate_stylesheet(),this.update_widgets_dimensions(),this.drag_api.set_limits(this.cols*this.min_widget_width+(this.cols+1)*this.options.widget_margins[0]),this},h.toggle_collapsed_grid=function(a,b){return a?(this.$widgets.css({"margin-top":b.widget_margins[0],"margin-bottom":b.widget_margins[0],"min-height":b.widget_base_dimensions[1]}),this.$el.addClass("collapsed"),this.resize_api&&this.disable_resize(),this.drag_api&&this.disable()):(this.$widgets.css({"margin-top":"auto","margin-bottom":"auto","min-height":"auto"}),this.$el.removeClass("collapsed"),this.resize_api&&this.enable_resize(),this.drag_api&&this.enable()),this},h.generate_stylesheet=function(b){var c,e="",f=this.is_responsive()&&this.options.responsive_breakpoint&&a(window).width()=0)return!1;for(this.generated_stylesheets.push(g),d.generated_stylesheets.push(g),c=1;c<=b.cols+1;c++)e+=b.namespace+' [data-col="'+c+'"] { left:'+(f?this.options.widget_margins[0]:c*b.widget_margins[0]+(c-1)*b.widget_base_dimensions[0])+"px; }\n";for(c=1;c<=b.rows+1;c++)e+=b.namespace+' [data-row="'+c+'"] { top:'+(c*b.widget_margins[1]+(c-1)*b.widget_base_dimensions[1])+"px; }\n";for(var h=1;h<=b.rows;h++)e+=b.namespace+' [data-sizey="'+h+'"] { height:'+(f?"auto":h*b.widget_base_dimensions[1]+(h-1)*b.widget_margins[1])+(f?"":"px")+"; }\n";for(var i=1;i<=b.cols;i++){var j=i*b.widget_base_dimensions[0]+(i-1)*b.widget_margins[0];e+=b.namespace+' [data-sizex="'+i+'"] { width:'+(f?this.$wrapper.width()-2*this.options.widget_margins[0]:j>this.$wrapper.width()?this.$wrapper.width():j)+"px; }\n"}return this.remove_style_tags(),this.add_style_tag(e)},h.add_style_tag=function(a){var b=document,c="gridster-stylesheet";if(""!==this.options.namespace&&(c=c+"-"+this.options.namespace),!document.getElementById(c)){var d=b.createElement("style");d.id=c,b.getElementsByTagName("head")[0].appendChild(d),d.setAttribute("type","text/css"),d.styleSheet?d.styleSheet.cssText=a:d.appendChild(document.createTextNode(a)),this.remove_style_tags(),this.$style_tags=this.$style_tags.add(d)}return this},h.remove_style_tags=function(){var b=d.generated_stylesheets,c=this.generated_stylesheets;this.$style_tags.remove(),d.generated_stylesheets=a.map(b,function(b){if(-1===a.inArray(b,c))return b})},h.generate_faux_grid=function(a,b){this.faux_grid=[],this.gridmap=[];var c,d;for(c=b;c>0;c--)for(this.gridmap[c]=[],d=a;d>0;d--)this.add_faux_cell(d,c);return this},h.add_faux_cell=function(b,c){var d=a({left:this.baseX+(c-1)*this.min_widget_width,top:this.baseY+(b-1)*this.min_widget_height,width:this.min_widget_width,height:this.min_widget_height,col:c,row:b,original_col:c,original_row:b}).coords();return a.isArray(this.gridmap[c])||(this.gridmap[c]=[]),void 0===this.gridmap[c][b]&&(this.gridmap[c][b]=!1),this.faux_grid.push(d),this},h.add_faux_rows=function(a){a=window.parseInt(a,10);for(var b=this.rows,c=b+parseInt(a||1),d=c;d>b;d--)for(var e=this.cols;e>=1;e--)this.add_faux_cell(d,e);return this.rows=c,this.options.autogenerate_stylesheet&&this.generate_stylesheet(),this},h.add_faux_cols=function(a){a=window.parseInt(a,10);var b=this.cols,c=b+parseInt(a||1);c=Math.min(c,this.options.max_cols);for(var d=b+1;d<=c;d++)for(var e=this.rows;e>=1;e--)this.add_faux_cell(e,d);return this.cols=c,this.options.autogenerate_stylesheet&&this.generate_stylesheet(),this},h.recalculate_faux_grid=function(){var b=this.$wrapper.width();return this.baseX=(f.width()-b)/2,this.baseY=this.$wrapper.offset().top,"relative"===this.$wrapper.css("position")&&(this.baseX=this.baseY=0),a.each(this.faux_grid,a.proxy(function(a,b){this.faux_grid[a]=b.update({left:this.baseX+(b.data.col-1)*this.min_widget_width,top:this.baseY+(b.data.row-1)*this.min_widget_height})},this)),this.is_responsive()&&this.resize_responsive_layout(),this.options.center_widgets&&this.center_widgets(),this},h.resize_widget_dimensions=function(b){return b.widget_margins&&(this.options.widget_margins=b.widget_margins),b.widget_base_dimensions&&(this.options.widget_base_dimensions=b.widget_base_dimensions),this.min_widget_width=2*this.options.widget_margins[0]+this.options.widget_base_dimensions[0],this.min_widget_height=2*this.options.widget_margins[1]+this.options.widget_base_dimensions[1],this.$widgets.each(a.proxy(function(b,c){var d=a(c);this.resize_widget(d)},this)),this.generate_grid_and_stylesheet(),this.get_widgets_from_DOM(),this.set_dom_grid_height(),this.set_dom_grid_width(),this},h.get_widgets_from_DOM=function(){var b=this.$widgets.map(a.proxy(function(b,c){var d=a(c);return this.dom_to_coords(d)},this));return b=d.sort_by_row_and_col_asc(b),a(b).map(a.proxy(function(a,b){return this.register_widget(b)||null},this)).length&&this.$el.trigger("gridster:positionschanged"),this},h.get_num_widgets=function(){return this.$widgets.length},h.set_num_columns=function(b){var c=this.options.max_cols,d=Math.floor(b/(this.min_widget_width+this.options.widget_margins[0]))+this.options.extra_cols,e=this.$widgets.map(function(){return a(this).attr("data-col")}).get();e.length||(e=[0]);var f=Math.max.apply(Math,e);this.cols=Math.max(f,d,this.options.min_cols),c!==1/0&&c>=f&&c'); + this.$buttons.append(qweb.render( + "kpi_dashboard.buttons", {widget: this})); + + this._updateButtons(); + this.$buttons.appendTo($node); + }, + }); + + return DashboardController; + +}); diff --git a/kpi_dashboard/static/src/js/dashboard_model.js b/kpi_dashboard/static/src/js/dashboard_model.js new file mode 100644 index 00000000..ddf7425e --- /dev/null +++ b/kpi_dashboard/static/src/js/dashboard_model.js @@ -0,0 +1,23 @@ +odoo.define('kpi_dashboard.DashboardModel', function (require) { + "use strict"; + + var BasicModel = require('web.BasicModel'); + + var DashboardModel = BasicModel.extend({ + _fetchRecord: function (record, options) { + return this._rpc({ + model: record.model, + method: 'read_dashboard', + args: [[record.res_id]], + context: _.extend({}, record.getContext(), {bin_size: true}), + }) + .then(function (result) { + record.specialData = result; + return result + }) + } + }); + + return DashboardModel; + +}); diff --git a/kpi_dashboard/static/src/js/dashboard_renderer.js b/kpi_dashboard/static/src/js/dashboard_renderer.js new file mode 100644 index 00000000..6ff43ef5 --- /dev/null +++ b/kpi_dashboard/static/src/js/dashboard_renderer.js @@ -0,0 +1,74 @@ +odoo.define('kpi_dashboard.DashboardRenderer', function (require) { + "use strict"; + + var BasicRenderer = require('web.BasicRenderer'); + var core = require('web.core'); + var registry = require('kpi_dashboard.widget_registry'); + var BusService = require('bus.BusService'); + var qweb = core.qweb; + + var DashboardRenderer= BasicRenderer.extend({ + className: "o_dashboard_view", + _getDashboardWidget: function (kpi) { + var Widget = registry.getAny([ + kpi.widget, 'abstract', + ]); + var widget = new Widget(this, kpi); + return widget; + }, + _renderView: function () { + this.$el.html($(qweb.render('dashboard_kpi.dashboard'))); + this.$el.css( + 'background-color', this.state.specialData.background_color); + this.$el.find('.gridster') + .css('width', this.state.specialData.width); + this.$grid = this.$el.find('.gridster ul'); + var self = this; + this.kpi_widget = {}; + _.each(this.state.specialData.item_ids, function (kpi) { + var element = $(qweb.render( + 'kpi_dashboard.kpi', {widget: kpi})); + element.css('background-color', kpi.color); + element.css('color', kpi.font_color); + self.$grid.append(element); + self.kpi_widget[kpi.id] = self._getDashboardWidget(kpi); + self.kpi_widget[kpi.id].appendTo(element); + }); + this.$grid.gridster({ + widget_margins: [ + this.state.specialData.margin_x, + this.state.specialData.margin_y, + ], + widget_base_dimensions: [ + this.state.specialData.widget_dimension_x, + this.state.specialData.widget_dimension_y, + ], + cols: this.state.specialData.max_cols, + }).data('gridster').disable(); + this.channel = 'kpi_dashboard_' + this.state.res_id; + this.call( + 'bus_service', 'addChannel', this.channel); + this.call('bus_service', 'startPolling'); + this.call( + 'bus_service', 'onNotification', + this, this._onNotification + ); + return $.when(); + }, + _onNotification: function (notifications) { + var self = this; + _.each(notifications, function (notification) { + var channel = notification[0]; + var message = notification[1]; + if (channel === self.channel && message) { + var widget = self.kpi_widget[message.id]; + if (widget !== undefined) { + widget._fillWidget(message); + } + } + }); + }, + }); + + return DashboardRenderer; +}); diff --git a/kpi_dashboard/static/src/js/dashboard_view.js b/kpi_dashboard/static/src/js/dashboard_view.js new file mode 100644 index 00000000..b31df5cb --- /dev/null +++ b/kpi_dashboard/static/src/js/dashboard_view.js @@ -0,0 +1,44 @@ +odoo.define('kpi_dashboard.DashboardView', function (require) { + "use strict"; + + var BasicView = require('web.BasicView'); + var DashboardController = require('kpi_dashboard.DashboardController'); + var DashboardModel = require('kpi_dashboard.DashboardModel'); + var DashboardRenderer = require('kpi_dashboard.DashboardRenderer'); + var view_registry = require('web.view_registry'); + var core = require('web.core'); + + var _lt = core._lt; + + var DashboardView = BasicView.extend({ + jsLibs: [ + '/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.js', + ], + cssLibs: [ + '/kpi_dashboard/static/lib/gridster/jquery.dsmorse-gridster.min.css', + ], + accesskey: "d", + display_name: _lt("Dashboard"), + icon: 'fa-tachometer', + viewType: 'dashboard', + config: _.extend({}, BasicView.prototype.config, { + Controller: DashboardController, + Renderer: DashboardRenderer, + Model: DashboardModel, + }), + multi_record: false, + searchable: false, + init: function () { + this._super.apply(this, arguments); + this.controllerParams.mode = 'readonly'; + this.loadParams.type = 'record'; + if (! this.loadParams.res_id && this.loadParams.context.res_id) { + this.loadParams.res_id = this.loadParams.context.res_id; + } + }, + }); + + view_registry.add('dashboard', DashboardView); + + return DashboardView; +}); diff --git a/kpi_dashboard/static/src/js/widget/abstract_widget.js b/kpi_dashboard/static/src/js/widget/abstract_widget.js new file mode 100644 index 00000000..b99b2d9e --- /dev/null +++ b/kpi_dashboard/static/src/js/widget/abstract_widget.js @@ -0,0 +1,91 @@ +odoo.define('kpi_dashboard.AbstractWidget', function (require) { + "use strict"; + var Widget = require('web.Widget'); + var field_utils = require('web.field_utils'); + var time = require('web.time'); + var ajax = require('web.ajax'); + var registry = require('kpi_dashboard.widget_registry'); + + var AbstractWidget = Widget.extend({ + template: 'kpi_dashboard.base_widget', // Template used by the widget + cssLibs: [], // Specific css of the widget + jsLibs: [], // Specific Javascript libraries of the widget + events: { + 'click .o_kpi_dashboard_toggle_button': '_onClickToggleButton', + 'click .direct_action': '_onClickDirectAction', + }, + init: function (parent, kpi_values) { + this._super(parent); + this.col = kpi_values.col; + this.row = kpi_values.row; + this.sizex = kpi_values.sizex; + this.sizey = kpi_values.sizey; + this.color = kpi_values.color; + this.values = kpi_values; + this.margin_x = parent.state.specialData.margin_x; + this.margin_y = parent.state.specialData.margin_y; + this.widget_dimension_x = parent.state.specialData.widget_dimension_x; + this.widget_dimension_y = parent.state.specialData.widget_dimension_y; + this.prefix = kpi_values.prefix; + this.suffix = kpi_values.suffix; + this.actions = kpi_values.actions; + this.widget_size_x = this.widget_dimension_x * this.sizex + + (this.sizex - 1) * this.margin_x; + this.widget_size_y = this.widget_dimension_y * this.sizey + + (this.sizey - 1) * this.margin_y; + }, + willStart: function () { + // We need to load the libraries before the start + return $.when(ajax.loadLibs(this), this._super.apply(this, arguments)); + }, + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self._fillWidget(self.values); + }); + }, + _onClickToggleButton: function (event) { + event.preventDefault(); + this.$el.toggleClass('o_dropdown_open'); + }, + _fillWidget: function (values) { + // This function fills the widget values + if (this.$el === undefined) + return; + this.fillWidget(values); + var item = this.$el.find('[data-bind="value_last_update_display"]'); + if (item && values.value_last_update !== undefined) { + var value = field_utils.parse.datetime(values.value_last_update); + item.text(value.clone().add( + this.getSession().getTZOffset(value), 'minutes').format( + time.getLangDatetimeFormat() + )); + } + var $manage = this.$el.find('.o_kpi_dashboard_manage'); + if ($manage && this.showManagePanel(values)) + $manage.toggleClass('hidden', false); + }, + showManagePanel: function (values) { + // Hook for extensions + return (values.actions !== undefined); + }, + fillWidget: function (values) { + // Specific function that will be changed by specific widget + var value = values.value; + var self = this; + _.each(value, function (val, key) { + var item = self.$el.find('[data-bind=' + key + ']') + if (item) + item.text(val); + }) + }, + _onClickDirectAction: function(event) { + event.preventDefault(); + var $data = $(event.currentTarget).closest('a'); + return this.do_action($($data).data('id')); + } + }); + + registry.add('abstract', AbstractWidget); + return AbstractWidget; +}); diff --git a/kpi_dashboard/static/src/js/widget/graph_widget.js b/kpi_dashboard/static/src/js/widget/graph_widget.js new file mode 100644 index 00000000..94d60688 --- /dev/null +++ b/kpi_dashboard/static/src/js/widget/graph_widget.js @@ -0,0 +1,108 @@ +odoo.define('kpi_dashboard.GraphWidget', function (require) { + "use strict"; + + var AbstractWidget = require('kpi_dashboard.AbstractWidget'); + var registry = require('kpi_dashboard.widget_registry'); + var core = require('web.core'); + var qweb = core.qweb; + + + var GraphWidget = AbstractWidget.extend({ + template: 'kpi_dashboard.graph', + jsLibs: [ + '/web/static/lib/nvd3/d3.v3.js', + '/web/static/lib/nvd3/nv.d3.js', + '/web/static/src/js/libs/nvd3.js', + ], + cssLibs: [ + '/web/static/lib/nvd3/nv.d3.css', + ], + start: function () { + this._onResize = this._onResize.bind(this); + nv.utils.windowResize(this._onResize); + return this._super.apply(this, arguments); + }, + destroy: function () { + if ('nv' in window && nv.utils && nv.utils.offWindowResize) { + // if the widget is destroyed before the lazy loaded libs (nv) are + // actually loaded (i.e. after the widget has actually started), + // nv is undefined, but the handler isn't bound yet anyway + nv.utils.offWindowResize(this._onResize); + } + this._super.apply(this, arguments); + }, + _getChartOptions: function (values) { + return { + x: function (d, u) { return u; }, + margin: {'left': 0, 'right': 0, 'top': 5, 'bottom': 0}, + showYAxis: false, + showXAxis: false, + showLegend: false, + height: this.widget_size_y - 90, + width: this.widget_size_x - 20, + }; + }, + _chartConfiguration: function (values) { + + this.chart.forceY([0]); + this.chart.xAxis.tickFormat(function (d) { + var label = ''; + _.each(values.value.graphs, function (v) { + if (v.values[d] && v.values[d].x) { + label = v.values[d].x; + } + }); + return label; + }); + this.chart.yAxis.tickFormat(d3.format(',.2f')); + + this.chart.tooltip.contentGenerator(function (key) { + return qweb.render('GraphCustomTooltip', { + 'color': key.point.color, + 'key': key.series[0].title, + 'value': d3.format(',.2f')(key.point.y) + }); + }); + }, + _addGraph: function (values) { + var data = values.value.graphs; + this.$svg.addClass('o_graph_linechart'); + this.chart = nv.models.lineChart(); + this.chart.options( + this._getChartOptions(values) + ); + this._chartConfiguration(values); + d3.select(this.$('svg')[0]) + .datum(data) + .transition().duration(600) + .call(this.chart); + this.$('svg').css('height', this.widget_size_y - 90); + this._customizeChart(); + }, + fillWidget: function (values) { + var self = this; + var element = this.$el.find('[data-bind="value"]'); + element.empty(); + element.css('padding-left', 10).css('padding-right', 10); + this.chart = null; + nv.addGraph(function () { + self.$svg = self.$el.find( + '[data-bind="value"]' + ).append(''); + self._addGraph(values); + }); + }, + _customizeChart: function () { + // Hook function + }, + _onResize: function () { + if (this.chart) { + this.chart.update(); + this._customizeChart(); + } + }, + }); + + registry.add('graph', GraphWidget); + return GraphWidget; +}); diff --git a/kpi_dashboard/static/src/js/widget/meter_widget.js b/kpi_dashboard/static/src/js/widget/meter_widget.js new file mode 100644 index 00000000..3d342a2a --- /dev/null +++ b/kpi_dashboard/static/src/js/widget/meter_widget.js @@ -0,0 +1,39 @@ +odoo.define('kpi_dashboard.MeterWidget', function (require) { + "use strict"; + + var AbstractWidget = require('kpi_dashboard.AbstractWidget'); + var registry = require('kpi_dashboard.widget_registry'); + + + var MeterWidget = AbstractWidget.extend({ + template: 'kpi_dashboard.meter', + jsLibs: [ + '/kpi_dashboard/static/lib/gauge/GaugeMeter.js', + ], + fillWidget: function (values) { + var input = this.$el.find('[data-bind="value"]'); + var options = this._getMeterOptions(values); + var margin = (this.widget_dimension_x - options.size)/2; + input.gaugeMeter(options); + input.parent().css('padding-left', margin); + }, + _getMeterOptions: function (values) { + var size = Math.min( + this.widget_size_x, + this.widget_size_y - 40) - 10; + return { + percent: values.value.value, + style: 'Arch', + width: 10, + size: size, + prepend: values.prefix !== undefined ? values.prefix : '', + append: values.suffix !== undefined ? values.suffix : '', + color: values.font_color, + animate_text_colors: true, + }; + }, + }); + + registry.add('meter', MeterWidget); + return MeterWidget; +}); diff --git a/kpi_dashboard/static/src/js/widget/number_widget.js b/kpi_dashboard/static/src/js/widget/number_widget.js new file mode 100644 index 00000000..987c612e --- /dev/null +++ b/kpi_dashboard/static/src/js/widget/number_widget.js @@ -0,0 +1,72 @@ +odoo.define('kpi_dashboard.NumberWidget', function (require) { + "use strict"; + + var AbstractWidget = require('kpi_dashboard.AbstractWidget'); + var registry = require('kpi_dashboard.widget_registry'); + var field_utils = require('web.field_utils'); + + + var NumberWidget = AbstractWidget.extend({ + template: 'kpi_dashboard.number', + shortNumber: function (num) { + if (Math.abs(num) >= 1000000000000) { + return field_utils.format.integer(num / 1000000000000, false, { + digits: [3, 1]}) + 'T'; + } + if (Math.abs(num) >= 1000000000) { + return field_utils.format.integer(num / 1000000000, false, { + digits: [3,1]}) + 'G'; + } + if (Math.abs(num) >= 1000000) { + return field_utils.format.integer(num / 1000000, false, { + digits: [3, 1]}) + 'M'; + } + if (Math.abs(num) >= 1000) { + return field_utils.format.float(num / 1000, false, { + digits: [3, 1]}) + 'K'; + } + if (Math.abs(num) >= 10) { + return field_utils.format.float(num, false, { + digits: [3, 1]}); + } + return field_utils.format.float(num, false, { + digits: [3, 2]}); + }, + fillWidget: function (values) { + var widget = this.$el; + var value = values.value.value; + if (value === undefined) { + value = 0; + } + var item = widget.find('[data-bind="value"]'); + if (item) { + item.text(this.shortNumber(value)); + } + var previous = values.value.previous; + + var $change_rate = widget.find('.change-rate'); + if (previous === undefined) { + $change_rate.toggleClass('active', false); + } else { + var difference = 0; + if (previous !== 0) { + difference = field_utils.format.integer( + (100 * value / previous) - 100) + '%'; + } + $change_rate.toggleClass('active', true); + var $difference = widget.find('[data-bind="difference"]'); + $difference.text(difference); + var $arrow = widget.find('[data-bind="arrow"]'); + if (value < previous) { + $arrow.toggleClass('fa-arrow-up', false); + $arrow.toggleClass('fa-arrow-down', true); + } else { + $arrow.toggleClass('fa-arrow-up', true); + $arrow.toggleClass('fa-arrow-down', false); + } + } + }, + }); + registry.add('number', NumberWidget); + return NumberWidget; +}); diff --git a/kpi_dashboard/static/src/js/widget/text_widget.js b/kpi_dashboard/static/src/js/widget/text_widget.js new file mode 100644 index 00000000..2af3774f --- /dev/null +++ b/kpi_dashboard/static/src/js/widget/text_widget.js @@ -0,0 +1,17 @@ +odoo.define('kpi_dashboard.TextWidget', function (require) { + "use strict"; + + var AbstractWidget = require('kpi_dashboard.AbstractWidget'); + var registry = require('kpi_dashboard.widget_registry'); + + + var TextWidget = AbstractWidget.extend({ + template: 'kpi_dashboard.base_text', + fillWidget: function () { + return; + }, + }); + + registry.add('base_text', TextWidget); + return TextWidget; +}); diff --git a/kpi_dashboard/static/src/js/widget_registry.js b/kpi_dashboard/static/src/js/widget_registry.js new file mode 100644 index 00000000..1616886b --- /dev/null +++ b/kpi_dashboard/static/src/js/widget_registry.js @@ -0,0 +1,7 @@ +odoo.define('kpi_dashboard.widget_registry', function (require) { + "use strict"; + + var Registry = require('web.Registry'); + + return new Registry(); +}); diff --git a/kpi_dashboard/static/src/scss/kpi_dashboard.scss b/kpi_dashboard/static/src/scss/kpi_dashboard.scss new file mode 100644 index 00000000..fb580e2a --- /dev/null +++ b/kpi_dashboard/static/src/scss/kpi_dashboard.scss @@ -0,0 +1,112 @@ +.o_dashboard_view { + height: 100%; + @include o-webclient-padding($top: $o-horizontal-padding/2, $bottom: $o-horizontal-padding/2); + display: flex; + >.gridster { + margin: 0 auto; + >ul { + >li { + text-align: center; + list-style: none outside none; + } + } + } + .updated_at { + font-size: 15px; + position: absolute; + bottom: 0px; + left: 0; + right: 0; + } + .gs-w { + padding: 10px; + } + .centered { + position: absolute; + left: 0; + right: 0; + } + .numbervalue { + text-transform: uppercase; + font-size: 54px; + font-weight: 700; + } + .change-rate { + font-weight: 500; + font-size: 30px; + } + .hidden { + display: none; + } + .o_kpi_dashboard_toggle_button { + position: absolute; + right: 0px; + top: 0px; + margin: -1px -1px auto auto; + padding: 8px 16px; + border: 1px solid transparent; + border-bottom: none; + height: 35px; + } + .o_kpi_dashboard_manage_panel { + @include o-position-absolute($right: -1px, $top: 34px); + margin-top: -1px; + &.container { + width: 95%; + max-width: 400px; + } + .o_kpi_dashboard_manage_section { + border-bottom: 1px solid gray('300'); + margin-bottom: 10px; + } + > div { + padding: 3px 0 3px 20px; + visibility: visible; + margin-bottom: 5px; + } + } + .o_dropdown_open { + .o_kpi_dashboard_manage_panel { + display: block; + } + .o_kpi_dashboard_toggle_button { + background: white; + border-color: gray('400'); + z-index: $zindex-dropdown + 1; + } + } + .GaugeMeter { + position: relative; + text-align: center; + left: 0; + right: 0; + overflow: hidden; + cursor: default; + span, b{ + margin: 0 23%; + width: 54%; + position: absolute; + text-align: center; + display: inline-block; + font-height: 100; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + [data-style="Semi"] B{ + Margin: 0 10%; + Width: 80%; + } + S, U{ + Text-Decoration:None; + font-height: 100; + } + B{ + Color: Black; + Font-Weight: 200; + Font-Size: 0.85em; + Opacity: .8; + } + } + +} diff --git a/kpi_dashboard/static/src/xml/dashboard.xml b/kpi_dashboard/static/src/xml/dashboard.xml new file mode 100644 index 00000000..c9c81541 --- /dev/null +++ b/kpi_dashboard/static/src/xml/dashboard.xml @@ -0,0 +1,76 @@ + +
+
    +
+ + +
  • + + +
    +

    +

    +
    + + + +
    + Go to +
    +
    +
    +
    + +
    + +

    +

    +

    +

    +
    + + +

    + +

    +

    + + +

    +
    +
    + + +
    +
    +
    + + + + +
    +
    +
    + + + + + + diff --git a/kpi_dashboard/views/kpi_dashboard.xml b/kpi_dashboard/views/kpi_dashboard.xml new file mode 100644 index 00000000..eba04663 --- /dev/null +++ b/kpi_dashboard/views/kpi_dashboard.xml @@ -0,0 +1,111 @@ + + + + + + + kpi.dashboard.form (in kpi_dashboard) + kpi.dashboard + +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + kpi.dashboard.search (in kpi_dashboard) + kpi.dashboard + + + + + + + + + kpi.dashboard.tree (in kpi_dashboard) + kpi.dashboard + + + + + + + + + kpi.dashboard.dashboard (in kpi_dashboard) + kpi.dashboard + + + + + + + Kpi Dashboard + kpi.dashboard + tree,form,dashboard + [] + {} + + + + Configure Dashboard + + + + + + diff --git a/kpi_dashboard/views/kpi_kpi.xml b/kpi_dashboard/views/kpi_kpi.xml new file mode 100644 index 00000000..924d9706 --- /dev/null +++ b/kpi_dashboard/views/kpi_kpi.xml @@ -0,0 +1,89 @@ + + + + + + + kpi.kpi.form (in kpi_dashboard) + kpi.kpi + +
    +
    +
    + +
    +

    + +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + kpi.kpi.search (in kpi_dashboard) + kpi.kpi + + + + + + + + + kpi.kpi.tree (in kpi_dashboard) + kpi.kpi + + + + + + + + + Kpi + kpi.kpi + tree,form + [] + {} + + + + Configure Kpi + + + + + + diff --git a/kpi_dashboard/views/kpi_menu.xml b/kpi_dashboard/views/kpi_menu.xml new file mode 100644 index 00000000..eb836c90 --- /dev/null +++ b/kpi_dashboard/views/kpi_menu.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/kpi_dashboard/views/webclient_templates.xml b/kpi_dashboard/views/webclient_templates.xml new file mode 100644 index 00000000..0dc8fce7 --- /dev/null +++ b/kpi_dashboard/views/webclient_templates.xml @@ -0,0 +1,25 @@ + + + +