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() ?: [];
|
||
|
|
}
|
||
|
|
}
|