dd4de77202
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 46s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
- 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
170 lines
6.4 KiB
PHP
170 lines
6.4 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
|
|
*/
|
|
|
|
namespace Joomla\Component\MokoSuiteCross\Administrator\Model;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
|
|
|
class AnalyticsModel extends BaseDatabaseModel
|
|
{
|
|
public function getHeatmap(int $days = 90, ?int $serviceId = null): array
|
|
{
|
|
$db = $this->getDatabase();
|
|
|
|
$cutoff = Factory::getDate('now - ' . $days . ' days')->format('Y-m-d H:i:s');
|
|
|
|
$query = $db->getQuery(true)
|
|
->select([
|
|
'DAYOFWEEK(' . $db->quoteName('posted_at') . ') AS dow',
|
|
'HOUR(' . $db->quoteName('posted_at') . ') AS hour_of_day',
|
|
'COUNT(*) AS total',
|
|
'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS success',
|
|
])
|
|
->from($db->quoteName('#__mokosuitecross_posts'))
|
|
->where($db->quoteName('posted_at') . ' IS NOT NULL')
|
|
->where($db->quoteName('posted_at') . ' >= ' . $db->quote($cutoff))
|
|
->group('DAYOFWEEK(' . $db->quoteName('posted_at') . '), HOUR(' . $db->quoteName('posted_at') . ')')
|
|
->order('dow ASC, hour_of_day ASC');
|
|
|
|
if ($serviceId !== null && $serviceId > 0) {
|
|
$query->where($db->quoteName('service_id') . ' = ' . (int) $serviceId);
|
|
}
|
|
|
|
$db->setQuery($query);
|
|
$rows = $db->loadAssocList() ?: [];
|
|
|
|
$grid = [];
|
|
|
|
for ($d = 1; $d <= 7; $d++) {
|
|
for ($h = 0; $h < 24; $h++) {
|
|
$grid[$d][$h] = ['total' => 0, 'success' => 0, 'rate' => 0];
|
|
}
|
|
}
|
|
|
|
foreach ($rows as $row) {
|
|
$d = (int) $row['dow'];
|
|
$h = (int) $row['hour_of_day'];
|
|
$grid[$d][$h] = [
|
|
'total' => (int) $row['total'],
|
|
'success' => (int) $row['success'],
|
|
'rate' => (int) $row['total'] > 0
|
|
? round(((int) $row['success'] / (int) $row['total']) * 100)
|
|
: 0,
|
|
];
|
|
}
|
|
|
|
return $grid;
|
|
}
|
|
|
|
public function getBestTimes(int $days = 90, ?int $serviceId = null, int $limit = 5): array
|
|
{
|
|
$db = $this->getDatabase();
|
|
|
|
$cutoff = Factory::getDate('now - ' . $days . ' days')->format('Y-m-d H:i:s');
|
|
|
|
$query = $db->getQuery(true)
|
|
->select([
|
|
'DAYOFWEEK(' . $db->quoteName('posted_at') . ') AS dow',
|
|
'HOUR(' . $db->quoteName('posted_at') . ') AS hour_of_day',
|
|
'COUNT(*) AS total',
|
|
'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS success',
|
|
])
|
|
->from($db->quoteName('#__mokosuitecross_posts'))
|
|
->where($db->quoteName('posted_at') . ' IS NOT NULL')
|
|
->where($db->quoteName('posted_at') . ' >= ' . $db->quote($cutoff))
|
|
->group('DAYOFWEEK(' . $db->quoteName('posted_at') . '), HOUR(' . $db->quoteName('posted_at') . ')')
|
|
->having('COUNT(*) >= 3')
|
|
->order('success DESC, total DESC');
|
|
|
|
if ($serviceId !== null && $serviceId > 0) {
|
|
$query->where($db->quoteName('service_id') . ' = ' . (int) $serviceId);
|
|
}
|
|
|
|
$db->setQuery($query, 0, $limit);
|
|
|
|
return $db->loadAssocList() ?: [];
|
|
}
|
|
|
|
public function getHourlyDistribution(int $days = 90, ?int $serviceId = null): array
|
|
{
|
|
$db = $this->getDatabase();
|
|
|
|
$cutoff = Factory::getDate('now - ' . $days . ' days')->format('Y-m-d H:i:s');
|
|
|
|
$query = $db->getQuery(true)
|
|
->select([
|
|
'HOUR(' . $db->quoteName('posted_at') . ') AS hour_of_day',
|
|
'COUNT(*) AS total',
|
|
'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS success',
|
|
'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('failed') . ' THEN 1 ELSE 0 END) AS failed',
|
|
])
|
|
->from($db->quoteName('#__mokosuitecross_posts'))
|
|
->where($db->quoteName('posted_at') . ' IS NOT NULL')
|
|
->where($db->quoteName('posted_at') . ' >= ' . $db->quote($cutoff))
|
|
->group('HOUR(' . $db->quoteName('posted_at') . ')')
|
|
->order('hour_of_day ASC');
|
|
|
|
if ($serviceId !== null && $serviceId > 0) {
|
|
$query->where($db->quoteName('service_id') . ' = ' . (int) $serviceId);
|
|
}
|
|
|
|
$db->setQuery($query);
|
|
|
|
return $db->loadAssocList() ?: [];
|
|
}
|
|
|
|
public function getDayOfWeekDistribution(int $days = 90, ?int $serviceId = null): array
|
|
{
|
|
$db = $this->getDatabase();
|
|
|
|
$cutoff = Factory::getDate('now - ' . $days . ' days')->format('Y-m-d H:i:s');
|
|
|
|
$query = $db->getQuery(true)
|
|
->select([
|
|
'DAYOFWEEK(' . $db->quoteName('posted_at') . ') AS dow',
|
|
'COUNT(*) AS total',
|
|
'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS success',
|
|
'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('failed') . ' THEN 1 ELSE 0 END) AS failed',
|
|
])
|
|
->from($db->quoteName('#__mokosuitecross_posts'))
|
|
->where($db->quoteName('posted_at') . ' IS NOT NULL')
|
|
->where($db->quoteName('posted_at') . ' >= ' . $db->quote($cutoff))
|
|
->group('DAYOFWEEK(' . $db->quoteName('posted_at') . ')')
|
|
->order('dow ASC');
|
|
|
|
if ($serviceId !== null && $serviceId > 0) {
|
|
$query->where($db->quoteName('service_id') . ' = ' . (int) $serviceId);
|
|
}
|
|
|
|
$db->setQuery($query);
|
|
|
|
return $db->loadAssocList() ?: [];
|
|
}
|
|
|
|
public function getServices(): array
|
|
{
|
|
$db = $this->getDatabase();
|
|
|
|
$query = $db->getQuery(true)
|
|
->select([$db->quoteName('id'), $db->quoteName('title'), $db->quoteName('service_type')])
|
|
->from($db->quoteName('#__mokosuitecross_services'))
|
|
->where($db->quoteName('published') . ' = 1')
|
|
->order($db->quoteName('title') . ' ASC');
|
|
|
|
$db->setQuery($query);
|
|
|
|
return $db->loadAssocList() ?: [];
|
|
}
|
|
}
|