2018-10-25 16:50:16 +02:00
|
|
|
/*
|
|
|
|
* decaffeinate suggestions:
|
|
|
|
* DS101: Remove unnecessary use of Array.from
|
|
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
|
|
* DS205: Consider reworking code to avoid use of IIFEs
|
|
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
|
|
*/
|
|
|
|
'use strict';
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2016-07-04 17:15:37 +02:00
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
Application.Controllers.controller("StatisticsController", ["$scope", "$state", "$rootScope", '$uibModal', "es", "Member", '_t', 'membersPromise', 'statisticsPromise'
|
|
|
|
, function($scope, $state, $rootScope, $uibModal, es, Member, _t, membersPromise, statisticsPromise) {
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
/* PRIVATE STATIC CONSTANTS */
|
2016-06-20 17:13:39 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# search window size
|
|
|
|
const RESULTS_PER_PAGE = 20;
|
2016-06-20 17:13:39 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# keep search context for (delay in minutes) ...
|
|
|
|
const ES_SCROLL_TIME = 1;
|
2016-06-20 17:13:39 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
/* PUBLIC SCOPE */
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# ui-view transitions optimization: if true, the stats will never be refreshed
|
|
|
|
$scope.preventRefresh = false;
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# statistics structure in elasticSearch
|
|
|
|
$scope.statistics = statisticsPromise;
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# fablab users list
|
|
|
|
$scope.members = membersPromise;
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# statistics data recovered from elasticSearch
|
|
|
|
$scope.data = null;
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# when did the search was triggered
|
|
|
|
$scope.searchDate = null;
|
2016-06-20 17:13:39 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# id of the elastic search context
|
|
|
|
$scope.scrollId = null;
|
2016-06-20 17:13:39 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# total number of results for the current query
|
|
|
|
$scope.totalHits = null;
|
2016-06-20 17:13:39 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# configuration of the widget allowing to pick the ages range
|
|
|
|
$scope.agePicker = {
|
|
|
|
show: false,
|
|
|
|
start: null,
|
2016-03-23 18:39:41 +01:00
|
|
|
end: null
|
2018-10-25 16:50:16 +02:00
|
|
|
};
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# total CA for the current view
|
|
|
|
$scope.sumCA = 0;
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# average users' age for the current view
|
|
|
|
$scope.averageAge = 0;
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# total of the stat field for non simple types
|
|
|
|
$scope.sumStat = 0;
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# Results of custom aggregations for the current type
|
|
|
|
$scope.customAggs = {};
|
2016-09-06 16:53:04 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# default: results are not sorted
|
|
|
|
$scope.sorting = {
|
|
|
|
ca: 'none',
|
2016-07-13 11:12:16 +02:00
|
|
|
date: 'desc'
|
2018-10-25 16:50:16 +02:00
|
|
|
};
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# active tab will be set here
|
|
|
|
$scope.selectedIndex = null;
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# type filter binding
|
|
|
|
$scope.type = {
|
|
|
|
selected: null,
|
2016-03-23 18:39:41 +01:00
|
|
|
active: null
|
2018-10-25 16:50:16 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
//# selected custom filter
|
|
|
|
$scope.customFilter = {
|
|
|
|
show: false,
|
|
|
|
criterion: {},
|
|
|
|
value : null,
|
|
|
|
exclude: false,
|
|
|
|
datePicker: {
|
|
|
|
format: Fablab.uibDateFormat,
|
|
|
|
opened: false, // default: datePicker is not shown
|
|
|
|
minDate: null,
|
|
|
|
maxDate: moment().toDate(),
|
|
|
|
options: {
|
|
|
|
startingDay: 1
|
|
|
|
}
|
|
|
|
} // France: the week starts on monday
|
|
|
|
};
|
|
|
|
|
|
|
|
//# available custom filters
|
|
|
|
$scope.filters = [];
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# default: we do not open the datepicker menu
|
2016-03-23 18:39:41 +01:00
|
|
|
$scope.datePicker =
|
2018-10-25 16:50:16 +02:00
|
|
|
{show: false};
|
|
|
|
|
|
|
|
//# datePicker parameters for interval beginning
|
|
|
|
$scope.datePickerStart = {
|
|
|
|
format: Fablab.uibDateFormat,
|
|
|
|
opened: false, // default: datePicker is not shown
|
|
|
|
minDate: null,
|
|
|
|
maxDate: moment().subtract(1, 'day').toDate(),
|
|
|
|
selected: moment().utc().subtract(1, 'months').subtract(1, 'day').startOf('day').toDate(),
|
|
|
|
options: {
|
2016-03-23 18:39:41 +01:00
|
|
|
startingDay: Fablab.weekStartingDay
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
//# datePicker parameters for interval ending
|
|
|
|
$scope.datePickerEnd = {
|
|
|
|
format: Fablab.uibDateFormat,
|
|
|
|
opened: false, // default: datePicker is not shown
|
|
|
|
minDate: null,
|
|
|
|
maxDate: moment().subtract(1, 'day').toDate(),
|
|
|
|
selected: moment().subtract(1, 'day').endOf('day').toDate(),
|
|
|
|
options: {
|
2016-03-23 18:39:41 +01:00
|
|
|
startingDay: Fablab.weekStartingDay
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
};
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Callback to open the datepicker (interval start)
|
|
|
|
// @param $event {Object} jQuery event object
|
|
|
|
//#
|
|
|
|
$scope.toggleStartDatePicker = $event => toggleDatePicker($event, $scope.datePickerStart);
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Callback to open the datepicker (interval end)
|
|
|
|
// @param $event {Object} jQuery event object
|
|
|
|
//#
|
|
|
|
$scope.toggleEndDatePicker = $event => toggleDatePicker($event, $scope.datePickerEnd);
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Callback to open the datepicker (custom filter)
|
|
|
|
// @param $event {Object} jQuery event object
|
|
|
|
//#
|
|
|
|
$scope.toggleCustomDatePicker = $event => toggleDatePicker($event, $scope.customFilter.datePicker);
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Callback called when the active tab is changed.
|
|
|
|
// recover the current tab and store its value in $scope.selectedIndex
|
|
|
|
// @param tab {Object} elasticsearch statistic structure (from statistic_indices table)
|
|
|
|
//#
|
|
|
|
$scope.setActiveTab = function(tab) {
|
|
|
|
$scope.selectedIndex = tab;
|
|
|
|
$scope.type.selected = tab.types[0];
|
|
|
|
$scope.type.active = $scope.type.selected;
|
|
|
|
$scope.customFilter.criterion = {};
|
|
|
|
$scope.customFilter.value = null;
|
|
|
|
$scope.customFilter.exclude = false;
|
|
|
|
$scope.sorting.ca = 'none';
|
|
|
|
$scope.sorting.date = 'desc';
|
|
|
|
buildCustomFiltersList();
|
|
|
|
return refreshStats();
|
|
|
|
};
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Returns true if the provided tab must be hidden due to some global or local configuration
|
|
|
|
// @param tab {Object} elasticsearch statistic structure (from statistic_indices table)
|
|
|
|
//#
|
|
|
|
$scope.hiddenTab = function(tab) {
|
|
|
|
if (tab.table) {
|
|
|
|
if ((tab.es_type_key === 'subscription') && $rootScope.fablabWithoutPlans) {
|
|
|
|
return true;
|
|
|
|
} else if ((tab.es_type_key === 'space') && $rootScope.fablabWithoutSpaces) {
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-02-15 15:41:25 +01:00
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Callback to validate the filters and send a new request to elastic
|
|
|
|
//#
|
|
|
|
$scope.validateFilterChange = function() {
|
|
|
|
$scope.agePicker.show = false;
|
|
|
|
$scope.customFilter.show = false;
|
|
|
|
$scope.type.active = $scope.type.selected;
|
|
|
|
buildCustomFiltersList();
|
|
|
|
return refreshStats();
|
|
|
|
};
|
2017-02-15 15:41:25 +01:00
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Callback to validate the dates range and refresh the data from elastic
|
|
|
|
//#
|
|
|
|
$scope.validateDateChange = function() {
|
|
|
|
$scope.datePicker.show = false;
|
|
|
|
return refreshStats();
|
|
|
|
};
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Parse the given date and return a user-friendly string
|
|
|
|
// @param date {Date} JS date or ant moment.js compatible date string
|
|
|
|
//#
|
|
|
|
$scope.formatDate = date => moment(date).format("LL");
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Parse the sex and return a user-friendly string
|
|
|
|
// @param sex {string} 'male' | 'female'
|
|
|
|
//#
|
|
|
|
$scope.formatSex = function(sex) {
|
|
|
|
if (sex === 'male') {
|
|
|
|
return _t('man');
|
|
|
|
}
|
|
|
|
if (sex === 'female') {
|
|
|
|
return _t('woman');
|
|
|
|
}
|
|
|
|
};
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
|
|
|
|
|
|
|
|
//#
|
|
|
|
// Retrieve the label for the given subtype in the current type
|
|
|
|
// @param key {string} statistic subtype key
|
|
|
|
//#
|
|
|
|
$scope.formatSubtype = function(key) {
|
|
|
|
let label = "";
|
|
|
|
angular.forEach($scope.type.active.subtypes, function(subtype) {
|
|
|
|
if (subtype.key === key) {
|
|
|
|
return label = subtype.label;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return label;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//#
|
|
|
|
// Helper usable in ng-switch to determine the input type to display for custom filter value
|
|
|
|
// @param filter {Object} custom filter criterion
|
|
|
|
//#
|
|
|
|
$scope.getCustomValueInputType = function(filter) {
|
|
|
|
if (filter && filter.values) {
|
|
|
|
if (typeof(filter.values[0]) === 'string') {
|
|
|
|
return filter.values[0];
|
|
|
|
} else if (typeof(filter.values[0] === 'object')) {
|
|
|
|
return 'input_select';
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return 'input_text';
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//#
|
|
|
|
// Change the sorting order and refresh the results to match the new order
|
|
|
|
// @param filter {Object} any filter
|
|
|
|
//#
|
|
|
|
$scope.toggleSorting = function(filter) {
|
|
|
|
switch ($scope.sorting[filter]) {
|
|
|
|
case 'none': $scope.sorting[filter] = 'asc'; break;
|
|
|
|
case 'asc': $scope.sorting[filter] = 'desc'; break;
|
|
|
|
case 'desc': $scope.sorting[filter] = 'none'; break;
|
|
|
|
}
|
|
|
|
return refreshStats();
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//#
|
|
|
|
// Return the user's name from his given ID
|
|
|
|
// @param id {number} user ID
|
|
|
|
//#
|
|
|
|
$scope.getUserNameFromId = function(id) {
|
|
|
|
const name = $scope.members[id];
|
|
|
|
return (name ? name : `ID ${id}`);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//#
|
|
|
|
// Run a scroll query to elasticsearch to append the next packet of results to those displayed.
|
|
|
|
// If the ES search context has expired when the user ask for more results, we re-run the whole query.
|
|
|
|
//#
|
|
|
|
$scope.showMoreResults = function() {
|
|
|
|
// if all results were retrieved, do nothing
|
|
|
|
if ($scope.data.length >= $scope.totalHits) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (moment($scope.searchDate).add(ES_SCROLL_TIME, 'minutes').isBefore(moment())) {
|
|
|
|
// elastic search context has expired, so we run again the whole query
|
|
|
|
return refreshStats();
|
|
|
|
} else {
|
|
|
|
return es.scroll({
|
|
|
|
"scroll": ES_SCROLL_TIME+'m',
|
2016-06-21 13:16:42 +02:00
|
|
|
"body": {scrollId: $scope.scrollId}
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
, function(error, response) {
|
|
|
|
if (error) {
|
|
|
|
return console.error(`Error: something unexpected occurred during elasticSearch scroll query: ${error}`);
|
|
|
|
} else {
|
|
|
|
$scope.scrollId = response._scroll_id;
|
|
|
|
return $scope.data = $scope.data.concat(response.hits.hits);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//#
|
|
|
|
// Open a modal dialog asking the user for details about exporting the statistics tables to an excel file
|
|
|
|
//#
|
|
|
|
$scope.exportToExcel = function() {
|
|
|
|
const options = {
|
|
|
|
templateUrl: '<%= asset_path "admin/statistics/export.html" %>',
|
|
|
|
size: 'sm',
|
|
|
|
controller: 'ExportStatisticsController',
|
|
|
|
resolve: {
|
|
|
|
dates() {
|
|
|
|
return {
|
|
|
|
start: $scope.datePickerStart.selected,
|
|
|
|
end: $scope.datePickerEnd.selected
|
|
|
|
};
|
|
|
|
},
|
|
|
|
query() {
|
|
|
|
const custom = buildCustomFilterQuery();
|
|
|
|
return buildElasticDataQuery($scope.type.active.key, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting);
|
|
|
|
},
|
|
|
|
index() {
|
|
|
|
return {key: $scope.selectedIndex.es_type_key};
|
|
|
|
},
|
|
|
|
type() {
|
|
|
|
return {key: $scope.type.active.key};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return $uibModal.open(options)
|
|
|
|
.result['finally'](null).then(info=> console.log(info));
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* PRIVATE SCOPE */
|
|
|
|
|
|
|
|
//#
|
|
|
|
// Kind of constructor: these actions will be realized first when the controller is loaded
|
|
|
|
//#
|
|
|
|
const initialize = () =>
|
|
|
|
// workaround for angular-bootstrap::tabs behavior: on tab deletion, another tab will be selected
|
|
|
|
// which will cause every tabs to reload, one by one, when the view is closed
|
|
|
|
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
|
|
|
|
if ((fromState.name === 'app.admin.statistics') && (Object.keys(fromParams).length === 0)) {
|
|
|
|
return $scope.preventRefresh = true;
|
|
|
|
}
|
|
|
|
})
|
|
|
|
;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//#
|
|
|
|
// Generic function to toggle a bootstrap datePicker
|
|
|
|
// @param $event {Object} jQuery event object
|
|
|
|
// @param datePicker {Object} settings object of the concerned datepicker. Must have an 'opened' property
|
|
|
|
//#
|
|
|
|
var toggleDatePicker = function($event, datePicker) {
|
|
|
|
$event.preventDefault();
|
|
|
|
$event.stopPropagation();
|
|
|
|
return datePicker.opened = !datePicker.opened;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//#
|
|
|
|
// Force update the statistics table, querying elasticSearch according to the current config values
|
|
|
|
//#
|
|
|
|
var refreshStats = function() {
|
|
|
|
if ($scope.selectedIndex && !$scope.preventRefresh) {
|
|
|
|
$scope.data = [];
|
|
|
|
$scope.sumCA = 0;
|
|
|
|
$scope.averageAge = 0;
|
|
|
|
$scope.sumStat = 0;
|
|
|
|
$scope.customAggs = {};
|
|
|
|
$scope.totalHits = null;
|
|
|
|
$scope.searchDate = new Date();
|
|
|
|
let custom = buildCustomFilterQuery();
|
|
|
|
return queryElasticStats($scope.selectedIndex.es_type_key, $scope.type.active.key, custom, function(res, err){
|
|
|
|
if (err) {
|
|
|
|
return console.error(`[statisticsController::refreshStats] Unable to refresh due to ${err}`);
|
|
|
|
} else {
|
|
|
|
$scope.data = res.hits.hits;
|
|
|
|
$scope.totalHits = res.hits.total;
|
|
|
|
$scope.sumCA = res.aggregations.total_ca.value;
|
|
|
|
$scope.averageAge = Math.round(res.aggregations.average_age.value * 100) / 100;
|
|
|
|
$scope.sumStat = res.aggregations.total_stat.value;
|
|
|
|
$scope.scrollId = res._scroll_id;
|
|
|
|
return (() => {
|
|
|
|
const result = [];
|
|
|
|
for (custom of Array.from($scope.type.active.custom_aggregations)) {
|
|
|
|
result.push($scope.customAggs[custom.field] = res.aggregations[custom.field].value);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
})();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//#
|
|
|
|
// Run the elasticSearch query to retreive the /stats/type aggregations
|
|
|
|
// @param index {String} elasticSearch document type (account|event|machine|project|subscription|training)
|
|
|
|
// @param type {String} statistics type (month|year|booking|hour|user|project)
|
|
|
|
// @param custom {{key:{string}, value:{string}}|null} custom filter property or null to disable this filter
|
|
|
|
// @param callback {function} function be to run after results were retrieved, it will receive
|
|
|
|
// two parameters : results {Object}, error {String} (if any)
|
|
|
|
//#
|
|
|
|
var queryElasticStats = function(index, type, custom, callback) {
|
|
|
|
// handle invalid callback
|
|
|
|
if (typeof(callback) !== "function") {
|
|
|
|
console.error('[statisticsController::queryElasticStats] Error: invalid callback provided');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// run query
|
|
|
|
return es.search({
|
|
|
|
"index": "stats",
|
|
|
|
"type": index,
|
|
|
|
"size": RESULTS_PER_PAGE,
|
|
|
|
"scroll": ES_SCROLL_TIME+'m',
|
|
|
|
"stat-type": type,
|
|
|
|
"custom-query": custom ? JSON.stringify(Object.assign({exclude: custom.exclude}, buildElasticCustomCriterion(custom))) : '',
|
|
|
|
"start-date": moment($scope.datePickerStart.selected).format(),
|
|
|
|
"end-date": moment($scope.datePickerEnd.selected).format(),
|
2016-03-23 18:39:41 +01:00
|
|
|
"body": buildElasticDataQuery(type, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting)
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
, function(error, response) {
|
|
|
|
if (error) {
|
|
|
|
return callback({}, `Error: something unexpected occurred during elasticSearch query: ${error}`);
|
|
|
|
} else {
|
|
|
|
return callback(response);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//#
|
|
|
|
// Build an object representing the content of the REST-JSON query to elasticSearch,
|
|
|
|
// based on the provided parameters for row data recovering.
|
|
|
|
// @param type {String} statistics type (month|year|booking|hour|user|project)
|
|
|
|
// @param custom {{key:{string}, value:{string}}|null} custom filter property or null to disable this filter
|
|
|
|
// @param ageMin {Number|null} filter by age: range lower value OR null to do not filter
|
|
|
|
// @param ageMax {Number|null} filter by age: range higher value OR null to do not filter
|
|
|
|
// @param intervalBegin {moment} statitics interval beginning (moment.js type)
|
|
|
|
// @param intervalEnd {moment} statitics interval ending (moment.js type)
|
|
|
|
// @param sortings {Array|null} elasticSearch criteria for sorting the results
|
|
|
|
//#
|
|
|
|
var buildElasticDataQuery = function(type, custom, ageMin, ageMax, intervalBegin, intervalEnd, sortings) {
|
|
|
|
const q = {
|
|
|
|
"query": {
|
|
|
|
"bool": {
|
2016-03-23 18:39:41 +01:00
|
|
|
"must": [
|
|
|
|
{
|
2018-10-25 16:50:16 +02:00
|
|
|
"term": {
|
2016-03-23 18:39:41 +01:00
|
|
|
"type": type
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
},
|
2016-03-23 18:39:41 +01:00
|
|
|
{
|
2018-10-25 16:50:16 +02:00
|
|
|
"range": {
|
|
|
|
"date": {
|
|
|
|
"gte": intervalBegin.format(),
|
2016-03-23 18:39:41 +01:00
|
|
|
"lte": intervalEnd.format()
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
}
|
2016-03-23 18:39:41 +01:00
|
|
|
}
|
|
|
|
]
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// optional date range
|
|
|
|
if ((typeof ageMin === 'number') && (typeof ageMax === 'number')) {
|
|
|
|
q.query.bool.must.push({
|
|
|
|
"range": {
|
|
|
|
"age": {
|
|
|
|
"gte": ageMin,
|
2016-03-23 18:39:41 +01:00
|
|
|
"lte": ageMax
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
// optional criterion
|
|
|
|
if (custom) {
|
|
|
|
const criterion = buildElasticCustomCriterion(custom);
|
|
|
|
if (custom.exclude) {
|
2018-06-08 12:27:01 +02:00
|
|
|
q.query.bool.must_not = [
|
2018-10-25 16:50:16 +02:00
|
|
|
{"term": criterion.match}
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
q.query.bool.must.push(criterion);
|
|
|
|
}
|
|
|
|
}
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
if (sortings) {
|
|
|
|
q["sort"] = buildElasticSortCriteria(sortings);
|
|
|
|
}
|
2016-06-20 17:13:39 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
// aggregations (avg age & CA sum)
|
2016-06-20 17:13:39 +02:00
|
|
|
q["aggs"] = {
|
2018-10-25 16:50:16 +02:00
|
|
|
"total_ca": {
|
|
|
|
"sum": {
|
2016-06-20 17:13:39 +02:00
|
|
|
"field": "ca"
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
"average_age": {
|
|
|
|
"avg": {
|
2016-06-20 17:13:39 +02:00
|
|
|
"field": "age"
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
"total_stat": {
|
|
|
|
"sum": {
|
2016-07-04 12:53:56 +02:00
|
|
|
"field": "stat"
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
return q;
|
|
|
|
};
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Build the elasticSearch query DSL to match the selected cutom filter
|
|
|
|
// @param custom {Object} if custom is empty or undefined, an empty string will be returned
|
|
|
|
// @returns {{match:{*}}|string}
|
|
|
|
//#
|
|
|
|
var buildElasticCustomCriterion = function(custom) {
|
|
|
|
if (custom) {
|
|
|
|
const criterion = {
|
2017-01-03 17:07:23 +01:00
|
|
|
"match" : {}
|
2018-10-25 16:50:16 +02:00
|
|
|
};
|
|
|
|
switch ($scope.getCustomValueInputType($scope.customFilter.criterion)) {
|
|
|
|
case 'input_date': criterion.match[custom.key] = moment(custom.value).format('YYYY-MM-DD'); break;
|
|
|
|
case 'input_select': criterion.match[custom.key] = custom.value.key; break;
|
|
|
|
case 'input_list': criterion.match[custom.key+".name"] = custom.value; break;
|
|
|
|
default: criterion.match[custom.key] = custom.value;
|
2017-01-03 17:07:23 +01:00
|
|
|
}
|
2018-10-25 16:50:16 +02:00
|
|
|
return criterion;
|
|
|
|
} else {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//#
|
|
|
|
// Parse the provided criteria array and return the corresponding elasticSearch syntax
|
|
|
|
// @param criteria {Array} array of {key_to_sort:order}
|
|
|
|
//#
|
|
|
|
var buildElasticSortCriteria = function(criteria) {
|
|
|
|
const crits = [];
|
|
|
|
angular.forEach(criteria, function(value, key) {
|
|
|
|
if ((typeof value !== 'undefined') && (value !== null) && (value !== 'none')) {
|
|
|
|
const c = {};
|
|
|
|
c[key] = {'order': value};
|
|
|
|
return crits.push(c);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return crits;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//#
|
|
|
|
// Fullfil the list of available options in the custom filter panel. The list will be based on common
|
|
|
|
// properties and on index-specific properties (additional_fields)
|
|
|
|
//#
|
|
|
|
var buildCustomFiltersList = function() {
|
|
|
|
$scope.filters = [];
|
|
|
|
|
|
|
|
const filters = [
|
2016-03-23 18:39:41 +01:00
|
|
|
{key: 'date', label: _t('date'), values: ['input_date']},
|
|
|
|
{key: 'userId', label: _t('user_id'), values: ['input_number']},
|
|
|
|
{key: 'gender', label: _t('gender'), values: [{key:'male', label:_t('man')}, {key:'female', label:_t('woman')}]},
|
|
|
|
{key: 'age', label: _t('age'), values: ['input_number']},
|
|
|
|
{key: 'subType', label: _t('type'), values: $scope.type.active.subtypes},
|
|
|
|
{key: 'ca', label: _t('revenue'), values: ['input_number']}
|
2018-10-25 16:50:16 +02:00
|
|
|
];
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
$scope.filters = filters;
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
if (!$scope.type.active.simple) {
|
|
|
|
const f = {key: 'stat', label: $scope.type.active.label, values: ['input_number']};
|
|
|
|
$scope.filters.push(f);
|
|
|
|
}
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
return angular.forEach($scope.selectedIndex.additional_fields, function(field) {
|
|
|
|
const filter = {key: field.key, label: field.label, values:[]};
|
|
|
|
switch (field.data_type) {
|
|
|
|
case 'index': filter.values.push('input_number'); break;
|
|
|
|
case 'number': filter.values.push('input_number'); break;
|
|
|
|
case 'date': filter.values.push('input_date'); break;
|
|
|
|
case 'list': filter.values.push('input_list'); break;
|
|
|
|
default: filter.values.push('input_text');
|
|
|
|
}
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
return $scope.filters.push(filter);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//#
|
|
|
|
// Build and return an object according to the custom filter set by the user, used to request elasticsearch
|
|
|
|
// @return {Object|null}
|
|
|
|
//#
|
|
|
|
var buildCustomFilterQuery = function() {
|
|
|
|
let custom = null;
|
|
|
|
if (!angular.isUndefinedOrNull($scope.customFilter.criterion) &&
|
|
|
|
!angular.isUndefinedOrNull($scope.customFilter.criterion.key) &&
|
|
|
|
!angular.isUndefinedOrNull($scope.customFilter.value)) {
|
|
|
|
custom = {};
|
|
|
|
custom.key = $scope.customFilter.criterion.key;
|
|
|
|
custom.value = $scope.customFilter.value;
|
|
|
|
custom.exclude = $scope.customFilter.exclude;
|
|
|
|
}
|
|
|
|
return custom;
|
|
|
|
};
|
2016-07-05 12:21:55 +02:00
|
|
|
|
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
// init the controller (call at the end !)
|
|
|
|
return initialize();
|
|
|
|
}
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
]);
|
2016-07-04 17:15:37 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
Application.Controllers.controller('ExportStatisticsController', [ '$scope', '$uibModalInstance', 'Export','dates', 'query', 'index', 'type', 'CSRF', 'growl', '_t'
|
|
|
|
, function($scope, $uibModalInstance, Export, dates, query, index, type, CSRF, growl, _t) {
|
2016-07-06 15:53:09 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# Retrieve Anti-CSRF tokens from cookies
|
|
|
|
CSRF.setMetaTags();
|
2016-07-04 17:15:37 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# Bindings for date range
|
|
|
|
$scope.dates = dates;
|
2016-07-04 17:15:37 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# Body of the query to export
|
|
|
|
$scope.query = JSON.stringify(query);
|
2016-07-06 15:53:09 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# API URL where the form will be posted
|
|
|
|
$scope.actionUrl = `/stats/${index.key}/export`;
|
2016-07-07 16:57:23 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# Key of the current search' statistic type
|
|
|
|
$scope.typeKey = type.key;
|
2016-07-06 15:53:09 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# Form action on the above URL
|
|
|
|
$scope.method = "post";
|
2016-07-06 15:53:09 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# Anti-CSRF token to inject into the download form
|
|
|
|
$scope.csrfToken = angular.element('meta[name="csrf-token"]')[0].content;
|
2016-07-06 15:53:09 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//# Binding of the export type (global / current)
|
2016-07-04 17:15:37 +02:00
|
|
|
$scope.export =
|
2018-10-25 16:50:16 +02:00
|
|
|
{type: 'current'};
|
|
|
|
|
|
|
|
//# datePicker parameters for interval beginning
|
|
|
|
$scope.exportStart = {
|
|
|
|
format: Fablab.uibDateFormat,
|
|
|
|
opened: false, // default: datePicker is not shown
|
|
|
|
minDate: null,
|
|
|
|
maxDate: moment().subtract(1, 'day').toDate(),
|
|
|
|
options: {
|
2016-07-04 17:15:37 +02:00
|
|
|
startingDay: Fablab.weekStartingDay
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
//# datePicker parameters for interval ending
|
|
|
|
$scope.exportEnd = {
|
|
|
|
format: Fablab.uibDateFormat,
|
|
|
|
opened: false, // default: datePicker is not shown
|
|
|
|
minDate: null,
|
|
|
|
maxDate: moment().subtract(1, 'day').toDate(),
|
|
|
|
options: {
|
2016-07-04 17:15:37 +02:00
|
|
|
startingDay: Fablab.weekStartingDay
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
};
|
2016-07-04 17:15:37 +02:00
|
|
|
|
2016-07-05 12:21:55 +02:00
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Callback to open the datepicker (interval start)
|
|
|
|
// @param $event {Object} jQuery event object
|
|
|
|
//#
|
|
|
|
$scope.toggleStartDatePicker = $event => $scope.exportStart.opened = !$scope.exportStart.opened;
|
2016-07-04 17:15:37 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Callback to open the datepicker (interval end)
|
|
|
|
// @param $event {Object} jQuery event object
|
|
|
|
//#
|
|
|
|
$scope.toggleEndDatePicker = $event => $scope.exportEnd.opened = !$scope.exportEnd.opened;
|
2016-07-04 17:15:37 +02:00
|
|
|
|
|
|
|
|
2016-07-05 12:21:55 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Callback when exchanging the export type between 'global' and 'current view'
|
|
|
|
// Adjust the query and the requesting url according to this type.
|
|
|
|
//#
|
|
|
|
$scope.setRequest = function() {
|
|
|
|
if ($scope.export.type === 'global') {
|
|
|
|
$scope.actionUrl = '/stats/global/export';
|
|
|
|
return $scope.query = JSON.stringify({
|
|
|
|
"query": {
|
|
|
|
"bool": {
|
2016-07-06 15:53:09 +02:00
|
|
|
"must": [
|
|
|
|
{
|
2018-10-25 16:50:16 +02:00
|
|
|
"range": {
|
|
|
|
"date": {
|
|
|
|
"gte": moment($scope.dates.start).format(),
|
2016-07-12 12:03:38 +02:00
|
|
|
"lte": moment($scope.dates.end).format()
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
}
|
2016-07-06 15:53:09 +02:00
|
|
|
}
|
|
|
|
]
|
2018-10-25 16:50:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
$scope.actionUrl = `/stats/${index.key}/export`;
|
|
|
|
return $scope.query = JSON.stringify(query);
|
|
|
|
}
|
|
|
|
};
|
2016-07-12 12:03:38 +02:00
|
|
|
|
|
|
|
|
2016-07-06 15:53:09 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Callback to close the modal, telling the caller what is exported
|
|
|
|
//#
|
|
|
|
$scope.exportData = function() {
|
|
|
|
const statusQry = {category: 'statistics', type: $scope.export.type, query: $scope.query};
|
|
|
|
if ($scope.export.type !== 'global') {
|
|
|
|
statusQry['type'] = index.key;
|
|
|
|
statusQry['key'] = type.key;
|
|
|
|
}
|
2016-07-05 12:21:55 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
Export.status(statusQry).then(function(res) {
|
|
|
|
if (!res.data.exists) {
|
|
|
|
return growl.success(_t('export_is_running_you_ll_be_notified_when_its_ready'));
|
|
|
|
}
|
|
|
|
});
|
2016-07-05 12:21:55 +02:00
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
return $uibModalInstance.close(statusQry);
|
|
|
|
};
|
2016-07-05 12:21:55 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
2018-10-25 16:50:16 +02:00
|
|
|
//#
|
|
|
|
// Callback to cancel the export and close the modal
|
|
|
|
//#
|
|
|
|
return $scope.cancel = () => $uibModalInstance.dismiss('cancel');
|
|
|
|
}
|
|
|
|
]);
|