7b0c3b2e4b
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
- Add 'calendar' and 'analytics' entries to MokoSuiteCrossHelper submenu - Add COM_MOKOSUITECROSS_CALENDAR_PREV_MONTH/NEXT_MONTH/TODAY strings - Add COM_MOKOSUITECROSS_SUBMENU_CALENDAR string Authored-by: Moko Consulting
241 lines
12 KiB
PHP
241 lines
12 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoSuiteCross
|
|
* @subpackage com_mokosuitecross
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Language\Text;
|
|
|
|
/** @var \Joomla\Component\MokoSuiteCross\Administrator\View\Analytics\HtmlView $this */
|
|
|
|
$dayNames = [
|
|
1 => Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAY_SUN'),
|
|
2 => Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAY_MON'),
|
|
3 => Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAY_TUE'),
|
|
4 => Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAY_WED'),
|
|
5 => Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAY_THU'),
|
|
6 => Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAY_FRI'),
|
|
7 => Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAY_SAT'),
|
|
];
|
|
?>
|
|
<form method="get" class="mb-3">
|
|
<input type="hidden" name="option" value="com_mokosuitecross" />
|
|
<input type="hidden" name="view" value="analytics" />
|
|
<div class="row g-2 align-items-end">
|
|
<div class="col-auto">
|
|
<label class="form-label" for="analytics-period"><?php echo Text::_('COM_MOKOSUITECROSS_ANALYTICS_PERIOD'); ?></label>
|
|
<select name="period" id="analytics-period" class="form-select" onchange="this.form.submit();">
|
|
<option value="7" <?php echo $this->period == 7 ? 'selected' : ''; ?>><?php echo Text::_('COM_MOKOSUITECROSS_PERIOD_7_DAYS'); ?></option>
|
|
<option value="30" <?php echo $this->period == 30 ? 'selected' : ''; ?>><?php echo Text::_('COM_MOKOSUITECROSS_PERIOD_30_DAYS'); ?></option>
|
|
<option value="90" <?php echo $this->period == 90 ? 'selected' : ''; ?>><?php echo Text::_('COM_MOKOSUITECROSS_PERIOD_90_DAYS'); ?></option>
|
|
<option value="180" <?php echo $this->period == 180 ? 'selected' : ''; ?>><?php echo Text::_('COM_MOKOSUITECROSS_PERIOD_180_DAYS'); ?></option>
|
|
<option value="365" <?php echo $this->period == 365 ? 'selected' : ''; ?>><?php echo Text::_('COM_MOKOSUITECROSS_PERIOD_365_DAYS'); ?></option>
|
|
</select>
|
|
</div>
|
|
<div class="col-auto">
|
|
<label class="form-label" for="analytics-service"><?php echo Text::_('COM_MOKOSUITECROSS_ANALYTICS_SERVICE_FILTER'); ?></label>
|
|
<select name="service_id" id="analytics-service" class="form-select" onchange="this.form.submit();">
|
|
<option value="0"><?php echo Text::_('COM_MOKOSUITECROSS_ANALYTICS_ALL_SERVICES'); ?></option>
|
|
<?php foreach ($this->services as $svc) : ?>
|
|
<option value="<?php echo (int) $svc['id']; ?>" <?php echo $this->serviceId == $svc['id'] ? 'selected' : ''; ?>>
|
|
<?php echo htmlspecialchars($svc['title'] . ' (' . ucfirst($svc['service_type']) . ')'); ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<?php if (!empty($this->bestTimes)) : ?>
|
|
<div class="card mb-3">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0"><?php echo Text::_('COM_MOKOSUITECROSS_ANALYTICS_BEST_TIMES'); ?></h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<?php foreach ($this->bestTimes as $bt) :
|
|
$rate = (int) $bt['total'] > 0 ? round(((int) $bt['success'] / (int) $bt['total']) * 100) : 0;
|
|
?>
|
|
<div class="col-sm-6 col-md-4 col-lg mb-2">
|
|
<div class="border rounded p-3 text-center h-100">
|
|
<div class="fw-bold text-primary"><?php echo $dayNames[(int) $bt['dow']]; ?></div>
|
|
<div class="display-6"><?php echo sprintf('%02d:00', (int) $bt['hour_of_day']); ?></div>
|
|
<div class="text-muted small">
|
|
<?php echo Text::sprintf('COM_MOKOSUITECROSS_ANALYTICS_POSTS_SUCCESS', (int) $bt['success'], (int) $bt['total']); ?>
|
|
</div>
|
|
<span class="badge bg-success"><?php echo $rate; ?>%</span>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php else : ?>
|
|
<div class="alert alert-info mb-3">
|
|
<?php echo Text::_('COM_MOKOSUITECROSS_ANALYTICS_NO_DATA'); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="card mb-3">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0"><?php echo Text::_('COM_MOKOSUITECROSS_ANALYTICS_HEATMAP'); ?></h5>
|
|
</div>
|
|
<div class="card-body" style="overflow-x: auto;">
|
|
<table class="table table-sm table-bordered text-center mb-0" style="min-width: 700px;">
|
|
<thead>
|
|
<tr>
|
|
<th></th>
|
|
<?php for ($h = 0; $h < 24; $h++) : ?>
|
|
<th class="small" style="width: 3.8%;"><?php echo sprintf('%02d', $h); ?></th>
|
|
<?php endfor; ?>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
$maxTotal = 1;
|
|
|
|
foreach ($this->heatmap as $dayData) {
|
|
foreach ($dayData as $cell) {
|
|
if ($cell['total'] > $maxTotal) {
|
|
$maxTotal = $cell['total'];
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($this->heatmap as $dow => $hours) : ?>
|
|
<tr>
|
|
<th class="text-nowrap small"><?php echo $dayNames[$dow]; ?></th>
|
|
<?php foreach ($hours as $hour => $cell) :
|
|
$intensity = $maxTotal > 0 ? $cell['total'] / $maxTotal : 0;
|
|
$r = 255;
|
|
$g = 255;
|
|
$b = 255;
|
|
|
|
if ($cell['total'] > 0) {
|
|
$rate = $cell['rate'];
|
|
|
|
if ($rate >= 80) {
|
|
$r = (int) (255 - (155 * $intensity));
|
|
$g = (int) (255 - (100 * $intensity));
|
|
$b = (int) (255 - (155 * $intensity));
|
|
} elseif ($rate >= 50) {
|
|
$r = (int) (255 - (50 * $intensity));
|
|
$g = (int) (255 - (50 * $intensity));
|
|
$b = (int) (255 - (200 * $intensity));
|
|
} else {
|
|
$r = (int) (255 - (35 * $intensity));
|
|
$g = (int) (255 - (200 * $intensity));
|
|
$b = (int) (255 - (200 * $intensity));
|
|
}
|
|
}
|
|
?>
|
|
<td style="background: rgb(<?php echo "$r,$g,$b"; ?>); cursor: default;"
|
|
title="<?php echo $dayNames[$dow] . ' ' . sprintf('%02d:00', $hour) . ': ' . $cell['total'] . ' posts, ' . $cell['success'] . ' success (' . $cell['rate'] . '%)'; ?>">
|
|
<?php if ($cell['total'] > 0) : ?>
|
|
<small><?php echo $cell['total']; ?></small>
|
|
<?php endif; ?>
|
|
</td>
|
|
<?php endforeach; ?>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<div class="d-flex justify-content-center gap-3 mt-2 small text-muted">
|
|
<span><span style="display: inline-block; width: 12px; height: 12px; background: rgb(100,155,100); border: 1px solid #ccc;"></span> <?php echo Text::_('COM_MOKOSUITECROSS_ANALYTICS_LEGEND_HIGH'); ?></span>
|
|
<span><span style="display: inline-block; width: 12px; height: 12px; background: rgb(205,205,55); border: 1px solid #ccc;"></span> <?php echo Text::_('COM_MOKOSUITECROSS_ANALYTICS_LEGEND_MEDIUM'); ?></span>
|
|
<span><span style="display: inline-block; width: 12px; height: 12px; background: rgb(220,55,55); border: 1px solid #ccc;"></span> <?php echo Text::_('COM_MOKOSUITECROSS_ANALYTICS_LEGEND_LOW'); ?></span>
|
|
<span><span style="display: inline-block; width: 12px; height: 12px; background: rgb(255,255,255); border: 1px solid #ccc;"></span> <?php echo Text::_('COM_MOKOSUITECROSS_ANALYTICS_LEGEND_NONE'); ?></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-lg-6">
|
|
<div class="card mb-3">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0"><?php echo Text::_('COM_MOKOSUITECROSS_ANALYTICS_HOURLY'); ?></h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<canvas id="hourlyChart" height="200"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-6">
|
|
<div class="card mb-3">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0"><?php echo Text::_('COM_MOKOSUITECROSS_ANALYTICS_DAILY'); ?></h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<canvas id="dayChart" height="200"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js" integrity="sha384-UPIssOjNMqMfON6mDKHvO4sOY4hhxN1ymYcfl2MrDz69idMU/L3MNFlyJGlIRjQH" crossorigin="anonymous"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var hourlyData = <?php echo json_encode(array_values($this->hourlyDistribution)); ?>;
|
|
var hourLabels = [];
|
|
var hourSuccess = [];
|
|
var hourFailed = [];
|
|
|
|
for (var h = 0; h < 24; h++) {
|
|
hourLabels.push(('0' + h).slice(-2) + ':00');
|
|
var found = hourlyData.find(function(d) { return parseInt(d.hour_of_day, 10) === h; });
|
|
hourSuccess.push(found ? parseInt(found.success, 10) : 0);
|
|
hourFailed.push(found ? parseInt(found.failed, 10) : 0);
|
|
}
|
|
|
|
new Chart(document.getElementById('hourlyChart'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: hourLabels,
|
|
datasets: [
|
|
{ label: '<?php echo Text::_('COM_MOKOSUITECROSS_DASHBOARD_POSTED', true); ?>', data: hourSuccess, backgroundColor: 'rgba(25,135,84,0.7)' },
|
|
{ label: '<?php echo Text::_('COM_MOKOSUITECROSS_DASHBOARD_FAILED', true); ?>', data: hourFailed, backgroundColor: 'rgba(220,53,69,0.7)' }
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
scales: { x: { stacked: true }, y: { stacked: true, beginAtZero: true, ticks: { stepSize: 1 } } },
|
|
plugins: { legend: { position: 'bottom' } }
|
|
}
|
|
});
|
|
|
|
var dayData = <?php echo json_encode(array_values($this->dayDistribution)); ?>;
|
|
var dayLabels = <?php echo json_encode(array_values($dayNames)); ?>;
|
|
var daySuccess = [];
|
|
var dayFailed = [];
|
|
|
|
for (var d = 1; d <= 7; d++) {
|
|
var found = dayData.find(function(r) { return parseInt(r.dow, 10) === d; });
|
|
daySuccess.push(found ? parseInt(found.success, 10) : 0);
|
|
dayFailed.push(found ? parseInt(found.failed, 10) : 0);
|
|
}
|
|
|
|
new Chart(document.getElementById('dayChart'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: dayLabels,
|
|
datasets: [
|
|
{ label: '<?php echo Text::_('COM_MOKOSUITECROSS_DASHBOARD_POSTED', true); ?>', data: daySuccess, backgroundColor: 'rgba(25,135,84,0.7)' },
|
|
{ label: '<?php echo Text::_('COM_MOKOSUITECROSS_DASHBOARD_FAILED', true); ?>', data: dayFailed, backgroundColor: 'rgba(220,53,69,0.7)' }
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
scales: { x: { stacked: true }, y: { stacked: true, beginAtZero: true, ticks: { stepSize: 1 } } },
|
|
plugins: { legend: { position: 'bottom' } }
|
|
}
|
|
});
|
|
});
|
|
</script>
|