chg: [genericElements:index_table] Continuation of stats for current view - WiP
parent
25f0f07251
commit
ef91cfcee3
|
@ -97,8 +97,34 @@ class CRUDComponent extends Component
|
||||||
}
|
}
|
||||||
$this->Controller->set('meta_templates', $metaTemplates);
|
$this->Controller->set('meta_templates', $metaTemplates);
|
||||||
}
|
}
|
||||||
|
if (true) { // check if stats are requested
|
||||||
|
$modelStatistics = [];
|
||||||
if ($this->Table->hasBehavior('Timestamp')) {
|
if ($this->Table->hasBehavior('Timestamp')) {
|
||||||
$modelStatistics = $this->Table->getStatisticsForModel($this->Table, !is_numeric($this->request->getQuery('statistics_days')) ? 7 : $this->request->getQuery('statistics_days'));
|
$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('modelStatistics', $modelStatistics);
|
||||||
}
|
}
|
||||||
$this->Controller->set('model', $this->Table);
|
$this->Controller->set('model', $this->Table);
|
||||||
|
|
|
@ -17,6 +17,7 @@ class EncryptionKeysController extends AppController
|
||||||
public $filterFields = ['owner_model', 'organisation_id', 'individual_id', 'encryption_key'];
|
public $filterFields = ['owner_model', 'organisation_id', 'individual_id', 'encryption_key'];
|
||||||
public $quickFilterFields = ['encryption_key'];
|
public $quickFilterFields = ['encryption_key'];
|
||||||
public $containFields = ['Individuals', 'Organisations'];
|
public $containFields = ['Individuals', 'Organisations'];
|
||||||
|
public $statisticsFields = ['type'];
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
|
@ -28,7 +29,8 @@ class EncryptionKeysController extends AppController
|
||||||
'type'
|
'type'
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'contain' => $this->containFields
|
'contain' => $this->containFields,
|
||||||
|
'statisticsFields' => $this->statisticsFields,
|
||||||
]);
|
]);
|
||||||
$responsePayload = $this->CRUD->getResponsePayload();
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
if (!empty($responsePayload)) {
|
if (!empty($responsePayload)) {
|
||||||
|
|
|
@ -16,6 +16,7 @@ class IndividualsController extends AppController
|
||||||
public $quickFilterFields = ['uuid', ['email' => true], ['first_name' => true], ['last_name' => true], 'position'];
|
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 $filterFields = ['uuid', 'email', 'first_name', 'last_name', 'position', 'Organisations.id', 'Alignments.type'];
|
||||||
public $containFields = ['Alignments' => 'Organisations'];
|
public $containFields = ['Alignments' => 'Organisations'];
|
||||||
|
public $statisticsFields = ['position'];
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
|
@ -23,7 +24,8 @@ class IndividualsController extends AppController
|
||||||
'filters' => $this->filterFields,
|
'filters' => $this->filterFields,
|
||||||
'quickFilters' => $this->quickFilterFields,
|
'quickFilters' => $this->quickFilterFields,
|
||||||
'quickFilterForMetaField' => ['enabled' => true, 'wildcard_search' => true],
|
'quickFilterForMetaField' => ['enabled' => true, 'wildcard_search' => true],
|
||||||
'contain' => $this->containFields
|
'contain' => $this->containFields,
|
||||||
|
'statisticsFields' => $this->statisticsFields,
|
||||||
]);
|
]);
|
||||||
$responsePayload = $this->CRUD->getResponsePayload();
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
if (!empty($responsePayload)) {
|
if (!empty($responsePayload)) {
|
||||||
|
|
|
@ -16,13 +16,15 @@ class MailingListsController extends AppController
|
||||||
public $filterFields = ['MailingLists.uuid', 'MailingLists.name', 'description', 'releasability'];
|
public $filterFields = ['MailingLists.uuid', 'MailingLists.name', 'description', 'releasability'];
|
||||||
public $quickFilterFields = ['MailingLists.uuid', ['MailingLists.name' => true], ['description' => true], ['releasability' => true]];
|
public $quickFilterFields = ['MailingLists.uuid', ['MailingLists.name' => true], ['description' => true], ['releasability' => true]];
|
||||||
public $containFields = ['Users', 'Individuals', 'MetaFields'];
|
public $containFields = ['Users', 'Individuals', 'MetaFields'];
|
||||||
|
public $statisticsFields = ['active'];
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$this->CRUD->index([
|
$this->CRUD->index([
|
||||||
'contain' => $this->containFields,
|
'contain' => $this->containFields,
|
||||||
'filters' => $this->filterFields,
|
'filters' => $this->filterFields,
|
||||||
'quickFilters' => $this->quickFilterFields
|
'quickFilters' => $this->quickFilterFields,
|
||||||
|
'statisticsFields' => $this->statisticsFields,
|
||||||
]);
|
]);
|
||||||
$responsePayload = $this->CRUD->getResponsePayload();
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
if (!empty($responsePayload)) {
|
if (!empty($responsePayload)) {
|
||||||
|
|
|
@ -16,6 +16,7 @@ class OrganisationsController extends AppController
|
||||||
public $quickFilterFields = [['name' => true], 'uuid', 'nationality', 'sector', 'type', 'url'];
|
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 $filterFields = ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id', 'MetaFields.field', 'MetaFields.value', 'MetaFields.MetaTemplates.name'];
|
||||||
public $containFields = ['Alignments' => 'Individuals'];
|
public $containFields = ['Alignments' => 'Individuals'];
|
||||||
|
public $statisticsFields = ['nationality', 'sector'];
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
|
@ -59,7 +60,8 @@ class OrganisationsController extends AppController
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'contain' => $this->containFields
|
'contain' => $this->containFields,
|
||||||
|
'statisticsFields' => $this->statisticsFields,
|
||||||
]);
|
]);
|
||||||
$responsePayload = $this->CRUD->getResponsePayload();
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
if (!empty($responsePayload)) {
|
if (!empty($responsePayload)) {
|
||||||
|
|
|
@ -7,6 +7,9 @@ use Cake\Validation\Validator;
|
||||||
use Cake\Core\Configure;
|
use Cake\Core\Configure;
|
||||||
use Cake\Core\Configure\Engine\PhpConfig;
|
use Cake\Core\Configure\Engine\PhpConfig;
|
||||||
use Cake\ORM\TableRegistry;
|
use Cake\ORM\TableRegistry;
|
||||||
|
use Cake\Utility\Hash;
|
||||||
|
use Cake\Database\Expression\QueryExpression;
|
||||||
|
use Cake\ORM\Query;
|
||||||
|
|
||||||
class AppTable extends Table
|
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
|
// Move this into a tool
|
||||||
public function getStatisticsForModel(Object $table, int $days = 30): array
|
public function getActivityStatisticsForModel(Object $table, int $days = 30): array
|
||||||
{
|
{
|
||||||
$statistics = [];
|
$statistics = [];
|
||||||
if ($table->hasBehavior('Timestamp')) {
|
if ($table->hasBehavior('Timestamp')) {
|
||||||
|
|
|
@ -30,7 +30,7 @@ class InstanceTable extends AppTable
|
||||||
$models = ['Individuals', 'Organisations', 'Alignments', 'EncryptionKeys', 'SharingGroups', 'Users', 'Broods', 'Tags.Tags'];
|
$models = ['Individuals', 'Organisations', 'Alignments', 'EncryptionKeys', 'SharingGroups', 'Users', 'Broods', 'Tags.Tags'];
|
||||||
foreach ($models as $model) {
|
foreach ($models as $model) {
|
||||||
$table = TableRegistry::getTableLocator()->get($model);
|
$table = TableRegistry::getTableLocator()->get($model);
|
||||||
$statistics[$model] = $this->getStatisticsForModel($table, $days);
|
$statistics[$model] = $this->getActivityStatisticsForModel($table, $days);
|
||||||
}
|
}
|
||||||
return $statistics;
|
return $statistics;
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,16 +45,6 @@ if (!empty($series)) {
|
||||||
},
|
},
|
||||||
series: <?= json_encode($chartSeries) ?>,
|
series: <?= json_encode($chartSeries) ?>,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
x: {
|
|
||||||
// show: false
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
title: {
|
|
||||||
formatter: function formatter(val) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
theme: 'dark'
|
theme: 'dark'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -64,7 +54,7 @@ if (!empty($series)) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.apexcharts-tooltip.apexcharts-theme-light {
|
#<?= $chartId ?> .apexcharts-tooltip-y-group {
|
||||||
color: black !important
|
padding: 1px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$chartOptions = $chartOptions ?? [];
|
||||||
|
$seed = mt_rand();
|
||||||
|
$chartId = "chart-{$seed}";
|
||||||
|
|
||||||
|
$chartData = $chartData ?? [];
|
||||||
|
$chartSeries = [];
|
||||||
|
if (!empty($series)) {
|
||||||
|
$chartSeries = $series;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div id="<?= $chartId ?>"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
const passedOptions = <?= json_encode($chartOptions) ?>;
|
||||||
|
const defaultOptions = {
|
||||||
|
chart: {
|
||||||
|
dropShadow: {
|
||||||
|
enabled: true,
|
||||||
|
top: 1,
|
||||||
|
left: 1,
|
||||||
|
blur: 2,
|
||||||
|
opacity: 0.2,
|
||||||
|
},
|
||||||
|
animations: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
series: <?= json_encode($chartSeries) ?>,
|
||||||
|
}
|
||||||
|
const chartOptions = mergeDeep({}, defaultOptions, passedOptions)
|
||||||
|
new ApexCharts(document.querySelector('#<?= $chartId ?>'), chartOptions).render();
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#<?= $chartId ?> .apexcharts-tooltip-y-group {
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$chartOptions = $chartOptions ?? [];
|
||||||
|
$seed = mt_rand();
|
||||||
|
$chartId = "chart-{$seed}";
|
||||||
|
|
||||||
|
$data = $data ?? [];
|
||||||
|
$series = [];
|
||||||
|
$labels = [];
|
||||||
|
$totalValue = 0;
|
||||||
|
foreach ($data as $combined) {
|
||||||
|
$combinedValues = array_values($combined);
|
||||||
|
$label = $combinedValues[0];
|
||||||
|
$value = $combinedValues[1];
|
||||||
|
$labels[] = $label;
|
||||||
|
$series[] = $value;
|
||||||
|
$totalValue += $value;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div id="<?= $chartId ?>"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
const totalValue = <?= $totalValue ?>;
|
||||||
|
const passedOptions = <?= json_encode($chartOptions) ?>;
|
||||||
|
const defaultOptions = {
|
||||||
|
chart: {
|
||||||
|
id: '<?= $chartId ?>',
|
||||||
|
type: 'pie',
|
||||||
|
dropShadow: {
|
||||||
|
enabled: true,
|
||||||
|
top: 1,
|
||||||
|
left: 1,
|
||||||
|
blur: 2,
|
||||||
|
opacity: 0.2,
|
||||||
|
},
|
||||||
|
animations: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: <?= json_encode($series) ?>,
|
||||||
|
labels: <?= json_encode($labels) ?>,
|
||||||
|
tooltip: {
|
||||||
|
y: {
|
||||||
|
formatter: function(value, {
|
||||||
|
series,
|
||||||
|
seriesIndex,
|
||||||
|
dataPointIndex,
|
||||||
|
w
|
||||||
|
}) {
|
||||||
|
return value + " (" + (value / totalValue * 100).toFixed(2) + "%)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
noData: {
|
||||||
|
text: '<?= __('No data') ?>',
|
||||||
|
verticalAlign: 'bottom',
|
||||||
|
style: {
|
||||||
|
fontFamily: 'var(--bs-body-font-family)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const chartOptions = mergeDeep({}, defaultOptions, passedOptions)
|
||||||
|
new ApexCharts(document.querySelector('#<?= $chartId ?>'), chartOptions).render();
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#<?= $chartId ?>.apexcharts-tooltip-y-group {
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,116 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Cake\Utility\Inflector;
|
||||||
|
|
||||||
|
$statisticsHtml = '';
|
||||||
|
$statistics_pie_amount = $this->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(
|
||||||
|
'<span class="text-nowrap">%s%s</span>',
|
||||||
|
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(
|
||||||
|
'<div class="d-flex flex-row">%s%s</div>',
|
||||||
|
$titleHtml,
|
||||||
|
$pieChart
|
||||||
|
);
|
||||||
|
$statPie = $this->Bootstrap->card([
|
||||||
|
'variant' => 'secondary',
|
||||||
|
'bodyHTML' => $panelHtml,
|
||||||
|
'bodyClass' => 'py-1 px-2',
|
||||||
|
'class' => ['shadow-sm', 'h-100']
|
||||||
|
]);
|
||||||
|
$statisticsHtml .= sprintf('<div class="col-sm-6 col-md-5 col-lg-4 col-xl-3 mb-1" style="height: 90px;">%s</div>', $statPie);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?= $statisticsHtml ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
var popoverTriggerList = [].slice.call(document.querySelectorAll('.btn-statistics-pie-configurator-<?= $seedPiechart ?>'))
|
||||||
|
var popoverList = popoverTriggerList.map(function(popoverTriggerEl) {
|
||||||
|
return new bootstrap.Popover(popoverTriggerEl, {
|
||||||
|
container: 'body',
|
||||||
|
html: true,
|
||||||
|
sanitize: false,
|
||||||
|
content: () => {
|
||||||
|
return '<div class="popover-form-container"> \
|
||||||
|
<div class="input-group flex-nowrap"> \
|
||||||
|
<span class="input-group-text"><?= __('Amount') ?></span> \
|
||||||
|
<input type="number" min="1" class="form-control entry-amount" placeholder="7" aria-label="<?= __('Days') ?>" value="<?= h($statistics_pie_amount) ?>"> \
|
||||||
|
</div> \
|
||||||
|
<div class="form-check"> \
|
||||||
|
<input class="form-check-input cb-include-remaining" type="checkbox" value="" id="checkbox-include-remaining" <?= $statistics_pie_include_remaining ? 'checked' : '' ?>> \
|
||||||
|
<label class="form-check-label" for="checkbox-include-remaining"> \
|
||||||
|
<?= __('Merge skipped entries') ?> \
|
||||||
|
</label> \
|
||||||
|
</div> \
|
||||||
|
<div class="form-check"> \
|
||||||
|
<input class="form-check-input cb-ignore-null" type="checkbox" value="" id="checkbox-ignore-null" <?= $statistics_pie_ignore_null ? 'checked' : '' ?>> \
|
||||||
|
<label class="form-check-label" for="checkbox-ignore-null"> \
|
||||||
|
<?= __('Ignore NULL values') ?> \
|
||||||
|
</label> \
|
||||||
|
</div> \
|
||||||
|
<button class="btn btn-primary" type="button" onclick="statisticsPieConfigurationRedirect(this)"><?= __('Update chart') ?> </button> \
|
||||||
|
</div>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function statisticsPieConfigurationRedirect(clicked) {
|
||||||
|
const endpoint = window.location.pathname
|
||||||
|
const search = window.location.search
|
||||||
|
let entryAmount = $(clicked).closest('.popover-form-container').find('input.entry-amount').val()
|
||||||
|
let includeRemaining = $(clicked).closest('.popover-form-container').find('input.cb-include-remaining').prop('checked')
|
||||||
|
let ignoreNull = $(clicked).closest('.popover-form-container').find('input.cb-ignore-null').prop('checked')
|
||||||
|
entryAmount = entryAmount !== undefined ? entryAmount : 5
|
||||||
|
includeRemaining = includeRemaining !== undefined ? includeRemaining : true
|
||||||
|
ignoreNull = ignoreNull !== undefined ? ignoreNull : true
|
||||||
|
const searchParams = new URLSearchParams(window.location.search)
|
||||||
|
searchParams.set('statistics_entry_amount', entryAmount);
|
||||||
|
searchParams.set('statistics_include_remainging', includeRemaining);
|
||||||
|
searchParams.set('statistics_ignore_null', ignoreNull);
|
||||||
|
const url = endpoint + '?' + searchParams
|
||||||
|
window.location = url
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$statisticsHtml = '';
|
||||||
|
$panelOptions = [
|
||||||
|
'condensed' => true,
|
||||||
|
'panelNoGrow' => true,
|
||||||
|
'allowConfiguration' => true,
|
||||||
|
'chartType' => 'line',
|
||||||
|
'chartOptions' => [
|
||||||
|
'chart' => [
|
||||||
|
'height' => '60px',
|
||||||
|
],
|
||||||
|
'stroke' => [
|
||||||
|
'width' => 2,
|
||||||
|
'curve' => 'smooth',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($statistics['created'])) {
|
||||||
|
$statisticsHtml .= $this->element('genericElements/IndexTable/Statistics/index_statistic_timestamp', [
|
||||||
|
'timeline' => $statistics,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (!empty($statistics['usage'])) {
|
||||||
|
$statisticsHtml .= $this->element('genericElements/IndexTable/Statistics/index_statistic_field_amount', [
|
||||||
|
'statistics' => $statistics,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$statisticsHtml = sprintf('<div class="container-fluid"><div class="row gx-2">%s</div></div>', $statisticsHtml);
|
||||||
|
echo sprintf('<div class="index-statistic-container">%s</div>', $statisticsHtml);
|
||||||
|
?>
|
|
@ -0,0 +1,126 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$statisticsHtml = '';
|
||||||
|
if (empty($timeline['created']) && empty($timeline['modified'])) {
|
||||||
|
return $statisticsHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$seed = 'timeline-' . mt_rand();
|
||||||
|
$title = __('Activity');
|
||||||
|
$statistics_day_number = $timeline['created']['days'];
|
||||||
|
$subTitle = __('Past {0} days', $statistics_day_number);
|
||||||
|
|
||||||
|
$series = [];
|
||||||
|
if (!empty($timeline['created']['timeline'])) {
|
||||||
|
$series[0]['name'] = __('Created');
|
||||||
|
foreach ($timeline['created']['timeline'] as $entry) {
|
||||||
|
$series[0]['data'][] = ['x' => $entry['time'], 'y' => $entry['count']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($timeline['modified']['timeline'])) {
|
||||||
|
$series[1]['name'] = __('Modified');
|
||||||
|
foreach ($timeline['modified']['timeline'] as $entry) {
|
||||||
|
$series[1]['data'][] = ['x' => $entry['time'], 'y' => $entry['count']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$panelControlHtml = sprintf(
|
||||||
|
'<div class="text-nowrap">
|
||||||
|
%s <span class="fs-8 fw-light">%s</span>%s
|
||||||
|
</div>',
|
||||||
|
$title,
|
||||||
|
$subTitle,
|
||||||
|
$this->Bootstrap->button([
|
||||||
|
'variant' => 'link',
|
||||||
|
'icon' => 'cog',
|
||||||
|
'size' => 'xs',
|
||||||
|
'nodeType' => 'a',
|
||||||
|
'onclick' => '',
|
||||||
|
'class' => ['btn-statistics-days-configurator-' . $seed,],
|
||||||
|
'params' => [
|
||||||
|
'data-bs-toggle' => 'popover',
|
||||||
|
'data-bs-title' => __('Set statistics spanning days'),
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
$activityNumbers = sprintf(
|
||||||
|
'<div class="my-1 fs-5">
|
||||||
|
<div class="lh-1" title="%s">%s<span class=""> %s</span></div>
|
||||||
|
<div class="ms-2 lh-1" title="%s">%s<span class=""> %s</span></div>
|
||||||
|
</div>',
|
||||||
|
__('{0} Created', $timeline['created']['variation']),
|
||||||
|
$this->Bootstrap->icon('plus-square', ['class' => ['ms-2 fa-fw'], 'params' => ['style' => 'font-size: 60%;']]),
|
||||||
|
$timeline['created']['variation'],
|
||||||
|
__('{0} Modified', $timeline['modified']['variation']),
|
||||||
|
$this->Bootstrap->icon('edit', ['class' => ['fa-fw'], 'params' => ['style' => 'font-size: 60%;']]),
|
||||||
|
$timeline['modified']['variation']
|
||||||
|
);
|
||||||
|
|
||||||
|
$leftContent = sprintf(
|
||||||
|
'%s%s',
|
||||||
|
$panelControlHtml,
|
||||||
|
$activityNumbers
|
||||||
|
);
|
||||||
|
$rightContent = sprintf('<div class="">%s</div>', $this->element('charts/bar', [
|
||||||
|
'series' => $series,
|
||||||
|
'chartOptions' => array_merge(
|
||||||
|
[
|
||||||
|
'chart' => [
|
||||||
|
'height' => 60,
|
||||||
|
],
|
||||||
|
'stroke' => [
|
||||||
|
'width' => 2,
|
||||||
|
'curve' => 'smooth',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
!empty($chartOptions) ? $chartOptions : []
|
||||||
|
)
|
||||||
|
]));
|
||||||
|
$cardContent = sprintf(
|
||||||
|
'<div class="highlight-panel-container d-flex align-items-center justify-content-between" style="max-height: 100px">
|
||||||
|
<div class="number-container">%s</div>
|
||||||
|
<div class="chart-container p-2" style="width: 60%%;">%s</div>
|
||||||
|
</div>',
|
||||||
|
$leftContent,
|
||||||
|
$rightContent
|
||||||
|
);
|
||||||
|
|
||||||
|
$card = $this->Bootstrap->card([
|
||||||
|
'variant' => 'secondary',
|
||||||
|
'bodyHTML' => $cardContent,
|
||||||
|
'bodyClass' => 'py-1 px-2',
|
||||||
|
'class' => ['shadow-sm', 'h-100']
|
||||||
|
]);
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="col-sm-6 col-md-5 col-lg-4 col-xl-3 mb-1" style="height: 90px;"><?= $card ?></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
let popovers = new bootstrap.Popover(document.querySelector('.btn-statistics-days-configurator-<?= $seed ?>'), {
|
||||||
|
container: 'body',
|
||||||
|
html: true,
|
||||||
|
sanitize: false,
|
||||||
|
content: () => {
|
||||||
|
return '<div class="input-group flex-nowrap"> \
|
||||||
|
<span class="input-group-text" id="addon-wrapping-<?= $seed ?>"><?= __('Days') ?></span> \
|
||||||
|
<input type="number" min="1" class="form-control" placeholder="7" aria-label="<?= __('Days') ?>" aria-describedby="addon-wrapping-<?= $seed ?>" value="<?= h($statistics_day_number) ?>"> \
|
||||||
|
<button class="btn btn-primary" type="button" onclick="statisticsDaysRedirect(this)"><?= __('Get statistics') ?> </button> \
|
||||||
|
</div>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function statisticsDaysRedirect(clicked) {
|
||||||
|
const endpoint = window.location.pathname
|
||||||
|
const search = window.location.search
|
||||||
|
let days = $(clicked).closest('.input-group').find('input').val()
|
||||||
|
days = days !== undefined ? days : 7
|
||||||
|
const searchParams = new URLSearchParams(window.location.search)
|
||||||
|
searchParams.set('statistics_days', days);
|
||||||
|
const url = endpoint + '?' + searchParams
|
||||||
|
window.location = url
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -54,44 +54,12 @@ if (!empty($data['title'])) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$statisticsHtml = '';
|
|
||||||
if (!empty($modelStatistics)) {
|
if (!empty($modelStatistics)) {
|
||||||
$panelOptions = [
|
echo $this->element('genericElements/IndexTable/Statistics/index_statistic_scaffold', [
|
||||||
'condensed' => true,
|
'statistics' => $modelStatistics,
|
||||||
'panelNoGrow' => true,
|
]);
|
||||||
'allowConfiguration' => true,
|
|
||||||
'chartType' => 'line',
|
|
||||||
'chartOptions' => [
|
|
||||||
'chart' => [
|
|
||||||
'height' => '60px',
|
|
||||||
],
|
|
||||||
'stroke' => [
|
|
||||||
'width' => 2,
|
|
||||||
'curve' => 'smooth',
|
|
||||||
],
|
|
||||||
'colors' => ['#0fd291'],
|
|
||||||
]
|
|
||||||
];
|
|
||||||
if (!empty($modelStatistics['created'])) {
|
|
||||||
$statCreated = $this->element('widgets/highlight-panel', array_merge($panelOptions, [
|
|
||||||
'titleHtml' => __('New {0}', $model->getAlias()),
|
|
||||||
'number' => $modelStatistics['created']['variation'] ?? '',
|
|
||||||
'timeline' => ['created' => $modelStatistics['created']],
|
|
||||||
]));
|
|
||||||
$statisticsHtml .= sprintf('<div class="col-sm-6 col-md-5 col-lg-4 col-xl-3 mb-1">%s</div>', $statCreated);
|
|
||||||
}
|
|
||||||
if (!empty($modelStatistics['modified'])) {
|
|
||||||
$statModified = $this->element('widgets/highlight-panel', array_merge($panelOptions, [
|
|
||||||
'titleHtml' => __('Updated {0}', $model->getAlias()),
|
|
||||||
'number' => $modelStatistics['modified']['variation'] ?? '',
|
|
||||||
'timeline' => ['modified' => $modelStatistics['modified']]
|
|
||||||
]));
|
|
||||||
$statisticsHtml .= sprintf('<div class="col-sm-6 col-md-5 col-lg-4 col-xl-3 mb-1">%s</div>', $statModified);
|
|
||||||
}
|
|
||||||
$statisticsHtml = sprintf('<div class="row gx-2">%s</div>', $statisticsHtml);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
echo sprintf('<div class="index-statistic-container">%s</div>', $statisticsHtml);
|
|
||||||
|
|
||||||
echo '<div class="panel">';
|
echo '<div class="panel">';
|
||||||
if (!empty($data['html'])) {
|
if (!empty($data['html'])) {
|
||||||
|
@ -212,8 +180,6 @@ echo '</div>';
|
||||||
var tooltipList = tooltipTriggerList.map(function(tooltipTriggerEl) {
|
var tooltipList = tooltipTriggerList.map(function(tooltipTriggerEl) {
|
||||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -113,9 +113,11 @@ echo $this->Bootstrap->card([
|
||||||
|
|
||||||
function statisticsDaysRedirect(clicked) {
|
function statisticsDaysRedirect(clicked) {
|
||||||
const endpoint = window.location.pathname
|
const endpoint = window.location.pathname
|
||||||
const searchParams = new URLSearchParams({
|
const search = window.location.search
|
||||||
statistics_days: $(clicked).closest('.input-group').find('input').val()
|
let days = $(clicked).closest('.input-group').find('input').val()
|
||||||
});
|
days = days !== undefined ? days : 7
|
||||||
|
const searchParams = new URLSearchParams(window.location.search)
|
||||||
|
searchParams.set('statistics_days', days);
|
||||||
const url = endpoint + '?' + searchParams
|
const url = endpoint + '?' + searchParams
|
||||||
window.location = url
|
window.location = url
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue