diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 13f74da..d461edf 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -97,8 +97,34 @@ class CRUDComponent extends Component } $this->Controller->set('meta_templates', $metaTemplates); } - if ($this->Table->hasBehavior('Timestamp')) { - $modelStatistics = $this->Table->getStatisticsForModel($this->Table, !is_numeric($this->request->getQuery('statistics_days')) ? 7 : $this->request->getQuery('statistics_days')); + if (true) { // check if stats are requested + $modelStatistics = []; + if ($this->Table->hasBehavior('Timestamp')) { + $modelStatistics = $this->Table->getActivityStatisticsForModel( + $this->Table, + !is_numeric($this->request->getQuery('statistics_days')) ? 7 : $this->request->getQuery('statistics_days') + ); + } + if (!empty($options['statisticsFields'])) { + $statIncludeRemaining = $this->request->getQuery('statistics_include_remainging', true); + if (is_string($statIncludeRemaining)) { + $statIncludeRemaining = $statIncludeRemaining == 'true' ? true : false; + } + $statIgnoreNull = $this->request->getQuery('statistics_ignore_null', true); + if (is_string($statIgnoreNull)) { + $statIgnoreNull = $statIgnoreNull == 'true' ? true : false; + } + $statsOptions = [ + 'limit' => !is_numeric($this->request->getQuery('statistics_entry_amount')) ? 5 : $this->request->getQuery('statistics_entry_amount'), + 'includeOthers' => $statIncludeRemaining, + 'ignoreNull' => $statIgnoreNull, + ]; + $modelStatistics['usage'] = $this->Table->getStatisticsUsageForModel( + $this->Table, + $options['statisticsFields'], + $statsOptions + ); + } $this->Controller->set('modelStatistics', $modelStatistics); } $this->Controller->set('model', $this->Table); diff --git a/src/Controller/EncryptionKeysController.php b/src/Controller/EncryptionKeysController.php index 65183cb..45621d5 100644 --- a/src/Controller/EncryptionKeysController.php +++ b/src/Controller/EncryptionKeysController.php @@ -17,6 +17,7 @@ class EncryptionKeysController extends AppController public $filterFields = ['owner_model', 'organisation_id', 'individual_id', 'encryption_key']; public $quickFilterFields = ['encryption_key']; public $containFields = ['Individuals', 'Organisations']; + public $statisticsFields = ['type']; public function index() { @@ -28,7 +29,8 @@ class EncryptionKeysController extends AppController 'type' ] ], - 'contain' => $this->containFields + 'contain' => $this->containFields, + 'statisticsFields' => $this->statisticsFields, ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { diff --git a/src/Controller/IndividualsController.php b/src/Controller/IndividualsController.php index 940757f..cc5a8fa 100644 --- a/src/Controller/IndividualsController.php +++ b/src/Controller/IndividualsController.php @@ -16,6 +16,7 @@ class IndividualsController extends AppController public $quickFilterFields = ['uuid', ['email' => true], ['first_name' => true], ['last_name' => true], 'position']; public $filterFields = ['uuid', 'email', 'first_name', 'last_name', 'position', 'Organisations.id', 'Alignments.type']; public $containFields = ['Alignments' => 'Organisations']; + public $statisticsFields = ['position']; public function index() { @@ -23,7 +24,8 @@ class IndividualsController extends AppController 'filters' => $this->filterFields, 'quickFilters' => $this->quickFilterFields, 'quickFilterForMetaField' => ['enabled' => true, 'wildcard_search' => true], - 'contain' => $this->containFields + 'contain' => $this->containFields, + 'statisticsFields' => $this->statisticsFields, ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { diff --git a/src/Controller/MailingListsController.php b/src/Controller/MailingListsController.php index 0282287..a3ea108 100644 --- a/src/Controller/MailingListsController.php +++ b/src/Controller/MailingListsController.php @@ -16,13 +16,15 @@ class MailingListsController extends AppController public $filterFields = ['MailingLists.uuid', 'MailingLists.name', 'description', 'releasability']; public $quickFilterFields = ['MailingLists.uuid', ['MailingLists.name' => true], ['description' => true], ['releasability' => true]]; public $containFields = ['Users', 'Individuals', 'MetaFields']; - + public $statisticsFields = ['active']; + public function index() { $this->CRUD->index([ 'contain' => $this->containFields, 'filters' => $this->filterFields, - 'quickFilters' => $this->quickFilterFields + 'quickFilters' => $this->quickFilterFields, + 'statisticsFields' => $this->statisticsFields, ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { diff --git a/src/Controller/OrganisationsController.php b/src/Controller/OrganisationsController.php index a45f5f3..7f2ce6d 100644 --- a/src/Controller/OrganisationsController.php +++ b/src/Controller/OrganisationsController.php @@ -16,6 +16,7 @@ class OrganisationsController extends AppController public $quickFilterFields = [['name' => true], 'uuid', 'nationality', 'sector', 'type', 'url']; public $filterFields = ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id', 'MetaFields.field', 'MetaFields.value', 'MetaFields.MetaTemplates.name']; public $containFields = ['Alignments' => 'Individuals']; + public $statisticsFields = ['nationality', 'sector']; public function index() { @@ -59,7 +60,8 @@ class OrganisationsController extends AppController ] ], ], - 'contain' => $this->containFields + 'contain' => $this->containFields, + 'statisticsFields' => $this->statisticsFields, ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { diff --git a/src/Model/Table/AppTable.php b/src/Model/Table/AppTable.php index 71633b2..b9ce7d8 100644 --- a/src/Model/Table/AppTable.php +++ b/src/Model/Table/AppTable.php @@ -7,6 +7,9 @@ use Cake\Validation\Validator; use Cake\Core\Configure; use Cake\Core\Configure\Engine\PhpConfig; use Cake\ORM\TableRegistry; +use Cake\Utility\Hash; +use Cake\Database\Expression\QueryExpression; +use Cake\ORM\Query; class AppTable extends Table { @@ -14,8 +17,80 @@ class AppTable extends Table { } + public function getStatisticsUsageForModel(Object $table, array $scopes, array $options=[]): array + { + $defaultOptions = [ + 'limit' => 5, + 'includeOthers' => true, + 'ignoreNull' => true, + ]; + $options = $this->getOptions($defaultOptions, $options); + $stats = []; + foreach ($scopes as $scope) { + $queryTopUsage = $table->find(); + $queryTopUsage + ->select([ + $scope, + 'count' => $queryTopUsage->func()->count('id'), + ]); + if ($queryTopUsage->getDefaultTypes()[$scope] != 'boolean') { + $queryTopUsage->where(function (QueryExpression $exp) use ($scope) { + return $exp + ->isNotNull($scope) + ->notEq($scope, ''); + }); + } + $queryTopUsage + ->group($scope) + ->order(['count' => 'DESC']) + ->limit($options['limit']) + ->page(1) + ->enableHydration(false); + $topUsage = $queryTopUsage->toList(); + $stats[$scope] = $topUsage; + if ( + !empty($options['includeOthers']) && !empty($topUsage) && + $queryTopUsage->getDefaultTypes()[$scope] != 'boolean' // No need to get others as we only have 2 possibilities already considered + ) { + $queryOthersUsage = $table->find(); + $queryOthersUsage + ->select([ + 'count' => $queryOthersUsage->func()->count('id'), + ]) + ->where(function (QueryExpression $exp, Query $query) use ($topUsage, $scope, $options) { + if (!empty($options['ignoreNull'])) { + return $exp + ->isNotNull($scope) + ->notEq($scope, '') + ->notIn($scope, Hash::extract($topUsage, "{n}.{$scope}")); + } else { + return $exp->or([ + $query->newExpr()->isNull($scope), + $query->newExpr()->eq($scope, ''), + $query->newExpr()->notIn($scope, Hash::extract($topUsage, "{n}.{$scope}")), + ]); + } + }) + ->enableHydration(false); + $othersUsage = $queryOthersUsage->toList(); + if (!empty($othersUsage)) { + $stats[$scope][] = [ + $scope => __('Others'), + 'count' => $othersUsage[0]['count'], + ]; + } + } + } + return $stats; + } + + private function getOptions($defaults=[], $options=[]): array + { + return array_merge($defaults, $options); + } + // Move this into a tool - public function getStatisticsForModel(Object $table, int $days = 30): array + public function getActivityStatisticsForModel(Object $table, int $days = 30): array { $statistics = []; if ($table->hasBehavior('Timestamp')) { diff --git a/src/Model/Table/InstanceTable.php b/src/Model/Table/InstanceTable.php index b5fcced..562e642 100644 --- a/src/Model/Table/InstanceTable.php +++ b/src/Model/Table/InstanceTable.php @@ -30,7 +30,7 @@ class InstanceTable extends AppTable $models = ['Individuals', 'Organisations', 'Alignments', 'EncryptionKeys', 'SharingGroups', 'Users', 'Broods', 'Tags.Tags']; foreach ($models as $model) { $table = TableRegistry::getTableLocator()->get($model); - $statistics[$model] = $this->getStatisticsForModel($table, $days); + $statistics[$model] = $this->getActivityStatisticsForModel($table, $days); } return $statistics; } diff --git a/templates/element/charts/bar.php b/templates/element/charts/bar.php index 14fc2bf..5f20316 100644 --- a/templates/element/charts/bar.php +++ b/templates/element/charts/bar.php @@ -45,16 +45,6 @@ if (!empty($series)) { }, series: = json_encode($chartSeries) ?>, tooltip: { - x: { - // show: false - }, - y: { - title: { - formatter: function formatter(val) { - return ''; - } - } - }, theme: 'dark' }, } @@ -64,7 +54,7 @@ if (!empty($series)) { \ No newline at end of file diff --git a/templates/element/charts/generic.php b/templates/element/charts/generic.php new file mode 100644 index 0000000..488577c --- /dev/null +++ b/templates/element/charts/generic.php @@ -0,0 +1,43 @@ + + +
+ + + + \ No newline at end of file diff --git a/templates/element/charts/pie.php b/templates/element/charts/pie.php new file mode 100644 index 0000000..6dd4475 --- /dev/null +++ b/templates/element/charts/pie.php @@ -0,0 +1,73 @@ + + + + + + + \ No newline at end of file diff --git a/templates/element/genericElements/IndexTable/Statistics/index_statistic_field_amount.php b/templates/element/genericElements/IndexTable/Statistics/index_statistic_field_amount.php new file mode 100644 index 0000000..2b0e32d --- /dev/null +++ b/templates/element/genericElements/IndexTable/Statistics/index_statistic_field_amount.php @@ -0,0 +1,116 @@ +request->getQuery('statistics_entry_amount', 5); +$statistics_pie_include_remaining = $this->request->getQuery('statistics_include_remainging', true); +if (is_string($statistics_pie_include_remaining)) { + $statistics_pie_include_remaining = $statistics_pie_include_remaining == 'true' ? true : false; +} +$statistics_pie_ignore_null = $this->request->getQuery('statistics_ignore_null', true); +if (is_string($statistics_pie_ignore_null)) { + $statistics_pie_ignore_null = $statistics_pie_ignore_null == 'true' ? true : false; +} + +$seedPiechart = 's-' . mt_rand(); +foreach ($statistics['usage'] as $scope => $graphData) { + $pieChart = $this->element('charts/pie', [ + 'data' => $graphData, + 'chartOptions' => [ + 'chart' => [ + 'height' => '80px', + 'sparkline' => [ + 'enabled' => true, + ] + ], + 'plotOptions' => [ + 'pie' => [ + 'customScale' => 0.9, + ] + ], + ], + ]); + $titleHtml = sprintf( + '%s%s', + Inflector::Pluralize(Inflector::Humanize(h($scope))), + $this->Bootstrap->button([ + 'variant' => 'link', + 'icon' => 'cog', + 'size' => 'xs', + 'nodeType' => 'a', + 'onclick' => '', + 'class' => ['btn-statistics-pie-configurator-' . $seedPiechart], + 'params' => [ + 'data-bs-toggle' => 'popover', + 'data-bs-title' => __('Configure chart'), + ] + ]) + ); + $panelHtml = sprintf( + '