chg: [genericElements:index_table] Continuation of stats for current view - WiP
parent
25f0f07251
commit
ef91cfcee3
templates/element
genericElements/IndexTable
widgets
|
@ -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);
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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')) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)) {
|
|||
</script>
|
||||
|
||||
<style>
|
||||
.apexcharts-tooltip.apexcharts-theme-light {
|
||||
color: black !important
|
||||
#<?= $chartId ?> .apexcharts-tooltip-y-group {
|
||||
padding: 1px;
|
||||
}
|
||||
</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)) {
|
||||
$panelOptions = [
|
||||
'condensed' => true,
|
||||
'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 $this->element('genericElements/IndexTable/Statistics/index_statistic_scaffold', [
|
||||
'statistics' => $modelStatistics,
|
||||
]);
|
||||
}
|
||||
|
||||
echo sprintf('<div class="index-statistic-container">%s</div>', $statisticsHtml);
|
||||
|
||||
echo '<div class="panel">';
|
||||
if (!empty($data['html'])) {
|
||||
|
@ -212,8 +180,6 @@ echo '</div>';
|
|||
var tooltipList = tooltipTriggerList.map(function(tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||
})
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -113,9 +113,11 @@ echo $this->Bootstrap->card([
|
|||
|
||||
function statisticsDaysRedirect(clicked) {
|
||||
const endpoint = window.location.pathname
|
||||
const searchParams = new URLSearchParams({
|
||||
statistics_days: $(clicked).closest('.input-group').find('input').val()
|
||||
});
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue