odoo.define('website.backend.dashboard', function (require) {
'use strict';
var AbstractAction = require('web.AbstractAction');
var ajax = require('web.ajax');
var core = require('web.core');
var Dialog = require('web.Dialog');
var field_utils = require('web.field_utils');
var pyUtils = require('web.py_utils');
var session = require('web.session');
var time = require('web.time');
var web_client = require('web.web_client');
var _t = core._t;
var QWeb = core.qweb;
var COLORS = ["#1f77b4", "#aec7e8"];
var FORMAT_OPTIONS = {
// allow to decide if utils.human_number should be used
humanReadable: function (value) {
return Math.abs(value) >= 1000;
},
// with the choices below, 1236 is represented by 1.24k
minDigits: 1,
decimals: 2,
// avoid comma separators for thousands in numbers when human_number is used
formatterCallback: function (str) {
return str;
},
};
var Dashboard = AbstractAction.extend({
hasControlPanel: true,
contentTemplate: 'website.WebsiteDashboardMain',
jsLibs: [
'/web/static/lib/Chart/Chart.js',
],
events: {
'click .js_link_analytics_settings': 'on_link_analytics_settings',
'click .o_dashboard_action': 'on_dashboard_action',
'click .o_dashboard_action_form': 'on_dashboard_action_form',
},
init: function(parent, context) {
this._super(parent, context);
this.DATE_FORMAT = time.getLangDateFormat();
this.date_range = 'week'; // possible values : 'week', 'month', year'
this.date_from = moment.utc().subtract(1, 'week');
this.date_to = moment.utc();
this.dashboards_templates = ['website.dashboard_header', 'website.dashboard_content'];
this.graphs = [];
this.chartIds = {};
},
willStart: function() {
var self = this;
return Promise.all([ajax.loadLibs(this), this._super()]).then(function() {
return self.fetch_data();
}).then(function(){
var website = _.findWhere(self.websites, {selected: true});
self.website_id = website ? website.id : false;
});
},
start: function() {
var self = this;
this._computeControlPanelProps();
return this._super().then(function() {
self.render_graphs();
});
},
on_attach_callback: function () {
this._isInDom = true;
this.render_graphs();
this._super.apply(this, arguments);
},
on_detach_callback: function () {
this._isInDom = false;
this._super.apply(this, arguments);
},
/**
* Fetches dashboard data
*/
fetch_data: function() {
var self = this;
var prom = this._rpc({
route: '/website/fetch_dashboard_data',
params: {
website_id: this.website_id || false,
date_from: this.date_from.year()+'-'+(this.date_from.month()+1)+'-'+this.date_from.date(),
date_to: this.date_to.year()+'-'+(this.date_to.month()+1)+'-'+this.date_to.date(),
},
});
prom.then(function (result) {
self.data = result;
self.dashboards_data = result.dashboards;
self.currency_id = result.currency_id;
self.groups = result.groups;
self.websites = result.websites;
});
return prom;
},
on_link_analytics_settings: function(ev) {
ev.preventDefault();
var self = this;
var dialog = new Dialog(this, {
size: 'medium',
title: _t('Connect Google Analytics'),
$content: QWeb.render('website.ga_dialog_content', {
ga_key: this.dashboards_data.visits.ga_client_id,
ga_analytics_key: this.dashboards_data.visits.ga_analytics_key,
}),
buttons: [
{
text: _t("Save"),
classes: 'btn-primary',
close: true,
click: function() {
var ga_client_id = dialog.$el.find('input[name="ga_client_id"]').val();
var ga_analytics_key = dialog.$el.find('input[name="ga_analytics_key"]').val();
self.on_save_ga_client_id(ga_client_id, ga_analytics_key);
},
},
{
text: _t("Cancel"),
close: true,
},
],
}).open();
},
on_go_to_website: function (ev) {
ev.preventDefault();
var website = _.findWhere(this.websites, {selected: true});
window.location.href = `/website/force/${website.id}`;
},
on_save_ga_client_id: function(ga_client_id, ga_analytics_key) {
var self = this;
return this._rpc({
route: '/website/dashboard/set_ga_data',
params: {
'website_id': self.website_id,
'ga_client_id': ga_client_id,
'ga_analytics_key': ga_analytics_key,
},
}).then(function (result) {
if (result.error) {
self.do_warn(result.error.title, result.error.message);
return;
}
self.on_date_range_button('week');
});
},
render_dashboards: function() {
var self = this;
_.each(this.dashboards_templates, function(template) {
self.$('.o_website_dashboard').append(QWeb.render(template, {widget: self}));
});
},
render_graph: function(div_to_display, chart_values, chart_id) {
var self = this;
this.$(div_to_display).empty();
var $canvasContainer = $('
', {class: 'o_graph_canvas_container'});
this.$canvas = $('').attr('id', chart_id);
$canvasContainer.append(this.$canvas);
this.$(div_to_display).append($canvasContainer);
var labels = chart_values[0].values.map(function (date) {
return moment(date[0], "YYYY-MM-DD", 'en');
});
var datasets = chart_values.map(function (group, index) {
return {
label: group.key,
data: group.values.map(function (value) {
return value[1];
}),
dates: group.values.map(function (value) {
return value[0];
}),
fill: false,
borderColor: COLORS[index],
};
});
var ctx = this.$canvas[0];
this.chart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: datasets,
},
options: {
legend: {
display: false,
},
maintainAspectRatio: false,
scales: {
yAxes: [{
type: 'linear',
ticks: {
beginAtZero: true,
callback: this.formatValue.bind(this),
},
}],
xAxes: [{
ticks: {
callback: function (moment) {
return moment.format(self.DATE_FORMAT);
},
}
}],
},
tooltips: {
mode: 'index',
intersect: false,
bodyFontColor: 'rgba(0,0,0,1)',
titleFontSize: 13,
titleFontColor: 'rgba(0,0,0,1)',
backgroundColor: 'rgba(255,255,255,0.6)',
borderColor: 'rgba(0,0,0,0.2)',
borderWidth: 2,
callbacks: {
title: function (tooltipItems, data) {
return data.datasets[0].label;
},
label: function (tooltipItem, data) {
var moment = data.labels[tooltipItem.index];
var date = tooltipItem.datasetIndex === 0 ?
moment :
moment.subtract(1, self.date_range);
return date.format(self.DATE_FORMAT) + ': ' + self.formatValue(tooltipItem.yLabel);
},
labelColor: function (tooltipItem, chart) {
var dataset = chart.data.datasets[tooltipItem.datasetIndex];
return {
borderColor: dataset.borderColor,
backgroundColor: dataset.borderColor,
};
},
}
}
}
});
},
render_graphs: function() {
var self = this;
if (this._isInDom) {
_.each(this.graphs, function(e) {
var renderGraph = self.groups[e.group] &&
self.dashboards_data[e.name].summary.order_count;
if (!self.chartIds[e.name]) {
self.chartIds[e.name] = _.uniqueId('chart_' + e.name);
}
var chart_id = self.chartIds[e.name];
if (renderGraph) {
self.render_graph('.o_graph_' + e.name, self.dashboards_data[e.name].graph, chart_id);
}
});
this.render_graph_analytics(this.dashboards_data.visits.ga_client_id);
}
},
render_graph_analytics: function(client_id) {
if (!this.dashboards_data.visits || !this.dashboards_data.visits.ga_client_id) {
return;
}
this.load_analytics_api();
var $analytics_components = this.$('.js_analytics_components');
this.addLoader($analytics_components);
var self = this;
gapi.analytics.ready(function() {
$analytics_components.empty();
// 1. Authorize component
var $analytics_auth = $('').addClass('col-lg-12');
window.onOriginError = function () {
$analytics_components.find('.js_unauthorized_message').remove();
self.display_unauthorized_message($analytics_components, 'not_initialized');
};
gapi.analytics.auth.authorize({
container: $analytics_auth[0],
clientid: client_id
});
$analytics_auth.appendTo($analytics_components);
self.handle_analytics_auth($analytics_components);
gapi.analytics.auth.on('signIn', function() {
delete window.onOriginError;
self.handle_analytics_auth($analytics_components);
});
});
},
on_date_range_button: function(date_range) {
if (date_range === 'week') {
this.date_range = 'week';
this.date_from = moment.utc().subtract(1, 'weeks');
} else if (date_range === 'month') {
this.date_range = 'month';
this.date_from = moment.utc().subtract(1, 'months');
} else if (date_range === 'year') {
this.date_range = 'year';
this.date_from = moment.utc().subtract(1, 'years');
} else {
console.log('Unknown date range. Choose between [week, month, year]');
return;
}
var self = this;
Promise.resolve(this.fetch_data()).then(function () {
self.$('.o_website_dashboard').empty();
self.render_dashboards();
self.render_graphs();
});
},
on_website_button: function(website_id) {
var self = this;
this.website_id = website_id;
Promise.resolve(this.fetch_data()).then(function () {
self.$('.o_website_dashboard').empty();
self.render_dashboards();
self.render_graphs();
});
},
on_reverse_breadcrumb: function() {
var self = this;
web_client.do_push_state({});
this.fetch_data().then(function() {
self.$('.o_website_dashboard').empty();
self.render_dashboards();
self.render_graphs();
});
},
on_dashboard_action: function (ev) {
ev.preventDefault();
var self = this
var $action = $(ev.currentTarget);
var additional_context = {};
if (this.date_range === 'week') {
additional_context = {search_default_week: true};
} else if (this.date_range === 'month') {
additional_context = {search_default_month: true};
} else if (this.date_range === 'year') {
additional_context = {search_default_year: true};
}
this._rpc({
route: '/web/action/load',
params: {
'action_id': $action.attr('name'),
},
})
.then(function (action) {
action.domain = pyUtils.assembleDomains([action.domain, `[('website_id', '=', ${self.website_id})]`]);
return self.do_action(action, {
'additional_context': additional_context,
'on_reverse_breadcrumb': self.on_reverse_breadcrumb
});
});
},
on_dashboard_action_form: function (ev) {
ev.preventDefault();
var $action = $(ev.currentTarget);
this.do_action({
name: $action.attr('name'),
res_model: $action.data('res_model'),
res_id: $action.data('res_id'),
views: [[false, 'form']],
type: 'ir.actions.act_window',
}, {
on_reverse_breadcrumb: this.on_reverse_breadcrumb
});
},
/**
* @private
*/
_computeControlPanelProps() {
const $searchview = $(QWeb.render("website.DateRangeButtons", {
widget: this,
}));
$searchview.find('button.js_date_range').click((ev) => {
$searchview.find('button.js_date_range.active').removeClass('active');
$(ev.target).addClass('active');
this.on_date_range_button($(ev.target).data('date'));
});
$searchview.find('button.js_website').click((ev) => {
$searchview.find('button.js_website.active').removeClass('active');
$(ev.target).addClass('active');
this.on_website_button($(ev.target).data('website-id'));
});
const $buttons = $(QWeb.render("website.GoToButtons"));
$buttons.on('click', this.on_go_to_website.bind(this));
this.controlPanelProps.cp_content = { $searchview, $buttons };
},
// Loads Analytics API
load_analytics_api: function() {
var self = this;
if (!("gapi" in window)) {
(function(w,d,s,g,js,fjs){
g=w.gapi||(w.gapi={});g.analytics={q:[],ready:function(cb){this.q.push(cb);}};
js=d.createElement(s);fjs=d.getElementsByTagName(s)[0];
js.src='https://apis.google.com/js/platform.js';
fjs.parentNode.insertBefore(js,fjs);js.onload=function(){g.load('analytics');};
}(window,document,'script'));
gapi.analytics.ready(function() {
self.analytics_create_components();
});
}
},
handle_analytics_auth: function($analytics_components) {
$analytics_components.find('.js_unauthorized_message').remove();
// Check if the user is authenticated and has the right to make API calls
if (!gapi.analytics.auth.getAuthResponse()) {
this.display_unauthorized_message($analytics_components, 'not_connected');
} else if (gapi.analytics.auth.getAuthResponse() && gapi.analytics.auth.getAuthResponse().scope.indexOf('https://www.googleapis.com/auth/analytics') === -1) {
this.display_unauthorized_message($analytics_components, 'no_right');
} else {
this.make_analytics_calls($analytics_components);
}
},
display_unauthorized_message: function($analytics_components, reason) {
$analytics_components.prepend($(QWeb.render('website.unauthorized_analytics', {reason: reason})));
},
make_analytics_calls: function($analytics_components) {
// 2. ActiveUsers component
var $analytics_users = $('
');
var activeUsers = new gapi.analytics.ext.ActiveUsers({
container: $analytics_users[0],
pollingInterval: 10,
});
$analytics_users.appendTo($analytics_components);
// 3. View Selector
var $analytics_view_selector = $('
').addClass('col-lg-12 o_properties_selection');
var viewSelector = new gapi.analytics.ViewSelector({
container: $analytics_view_selector[0],
});
viewSelector.execute();
$analytics_view_selector.appendTo($analytics_components);
// 4. Chart graph
var start_date = '7daysAgo';
if (this.date_range === 'month') {
start_date = '30daysAgo';
} else if (this.date_range === 'year') {
start_date = '365daysAgo';
}
var $analytics_chart_2 = $('
').addClass('col-lg-6 col-12');
var breakdownChart = new gapi.analytics.googleCharts.DataChart({
query: {
'dimensions': 'ga:date',
'metrics': 'ga:sessions',
'start-date': start_date,
'end-date': 'yesterday'
},
chart: {
type: 'LINE',
container: $analytics_chart_2[0],
options: {
title: 'All',
width: '100%',
tooltip: {isHtml: true},
}
}
});
$analytics_chart_2.appendTo($analytics_components);
// 5. Chart table
var $analytics_chart_1 = $('
').addClass('col-lg-6 col-12');
var mainChart = new gapi.analytics.googleCharts.DataChart({
query: {
'dimensions': 'ga:medium',
'metrics': 'ga:sessions',
'sort': '-ga:sessions',
'max-results': '6'
},
chart: {
type: 'TABLE',
container: $analytics_chart_1[0],
options: {
width: '100%'
}
}
});
$analytics_chart_1.appendTo($analytics_components);
// Events handling & animations
var table_row_listener;
viewSelector.on('change', function(ids) {
var options = {query: {ids: ids}};
activeUsers.set({ids: ids}).execute();
mainChart.set(options).execute();
breakdownChart.set(options).execute();
if (table_row_listener) { google.visualization.events.removeListener(table_row_listener); }
});
mainChart.on('success', function(response) {
var chart = response.chart;
var dataTable = response.dataTable;
table_row_listener = google.visualization.events.addListener(chart, 'select', function() {
var options;
if (chart.getSelection().length) {
var row = chart.getSelection()[0].row;
var medium = dataTable.getValue(row, 0);
options = {
query: {
filters: 'ga:medium==' + medium,
},
chart: {
options: {
title: medium,
}
}
};
} else {
options = {
chart: {
options: {
title: 'All',
}
}
};
delete breakdownChart.get().query.filters;
}
breakdownChart.set(options).execute();
});
});
// Add CSS animation to visually show the when users come and go.
activeUsers.once('success', function() {
var element = this.container.firstChild;
var timeout;
this.on('change', function(data) {
element = this.container.firstChild;
var animationClass = data.delta > 0 ? 'is-increasing' : 'is-decreasing';
element.className += (' ' + animationClass);
clearTimeout(timeout);
timeout = setTimeout(function() {
element.className = element.className.replace(/ is-(increasing|decreasing)/g, '');
}, 3000);
});
});
},
/*
* Credits to https://github.com/googleanalytics/ga-dev-tools
* This is the Active Users component that polls
* the number of active users on Analytics each 5 secs
*/
analytics_create_components: function() {
gapi.analytics.createComponent('ActiveUsers', {
initialize: function() {
this.activeUsers = 0;
gapi.analytics.auth.once('signOut', this.handleSignOut_.bind(this));
},
execute: function() {
// Stop any polling currently going on.
if (this.polling_) {
this.stop();
}
this.render_();
// Wait until the user is authorized.
if (gapi.analytics.auth.isAuthorized()) {
this.pollActiveUsers_();
} else {
gapi.analytics.auth.once('signIn', this.pollActiveUsers_.bind(this));
}
},
stop: function() {
clearTimeout(this.timeout_);
this.polling_ = false;
this.emit('stop', {activeUsers: this.activeUsers});
},
render_: function() {
var opts = this.get();
// Render the component inside the container.
this.container = typeof opts.container === 'string' ?
document.getElementById(opts.container) : opts.container;
this.container.innerHTML = opts.template || this.template;
this.container.querySelector('b').innerHTML = this.activeUsers;
},
pollActiveUsers_: function() {
var options = this.get();
var pollingInterval = (options.pollingInterval || 5) * 1000;
if (isNaN(pollingInterval) || pollingInterval < 5000) {
throw new Error('Frequency must be 5 seconds or more.');
}
this.polling_ = true;
gapi.client.analytics.data.realtime
.get({ids:options.ids, metrics:'rt:activeUsers'})
.then(function(response) {
var result = response.result;
var newValue = result.totalResults ? +result.rows[0][0] : 0;
var oldValue = this.activeUsers;
this.emit('success', {activeUsers: this.activeUsers});
if (newValue !== oldValue) {
this.activeUsers = newValue;
this.onChange_(newValue - oldValue);
}
if (this.polling_) {
this.timeout_ = setTimeout(this.pollActiveUsers_.bind(this), pollingInterval);
}
}.bind(this));
},
onChange_: function(delta) {
var valueContainer = this.container.querySelector('b');
if (valueContainer) { valueContainer.innerHTML = this.activeUsers; }
this.emit('change', {activeUsers: this.activeUsers, delta: delta});
if (delta > 0) {
this.emit('increase', {activeUsers: this.activeUsers, delta: delta});
} else {
this.emit('decrease', {activeUsers: this.activeUsers, delta: delta});
}
},
handleSignOut_: function() {
this.stop();
gapi.analytics.auth.once('signIn', this.handleSignIn_.bind(this));
},
handleSignIn_: function() {
this.pollActiveUsers_();
gapi.analytics.auth.once('signOut', this.handleSignOut_.bind(this));
},
template:
'
' +
'Active Users: ' +
'
'
});
},
// Utility functions
addLoader: function(selector) {
var loader = '
';
selector.html("
" + loader + "
");
},
getValue: function(d) { return d[1]; },
format_number: function(value, type, digits, symbol) {
if (type === 'currency') {
return this.render_monetary_field(value, this.currency_id);
} else {
return field_utils.format[type](value || 0, {digits: digits}) + ' ' + symbol;
}
},
formatValue: function (value) {
var formatter = field_utils.format.float;
var formatedValue = formatter(value, undefined, FORMAT_OPTIONS);
return formatedValue;
},
render_monetary_field: function(value, currency_id) {
var currency = session.get_currency(currency_id);
var formatted_value = field_utils.format.float(value || 0, {digits: currency && currency.digits});
if (currency) {
if (currency.position === "after") {
formatted_value += currency.symbol;
} else {
formatted_value = currency.symbol + formatted_value;
}
}
return formatted_value;
},
});
core.action_registry.add('backend_dashboard', Dashboard);
return Dashboard;
});