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