* @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\Helper; defined('_JEXEC') or die; use Joomla\CMS\Factory; class AnalyticsHelper { private static array $dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; public static function getPostingHeatmap(string $serviceType = '', int $days = 90): array { $db = Factory::getDbo(); $query = $db->getQuery(true) ->select('DAYOFWEEK(' . $db->quoteName('p.posted_at') . ') - 1 AS dow') ->select('HOUR(' . $db->quoteName('p.posted_at') . ') AS hr') ->select('COUNT(*) AS cnt') ->from($db->quoteName('#__mokosuitecross_posts', 'p')) ->where($db->quoteName('p.status') . ' = ' . $db->quote('posted')) ->where($db->quoteName('p.posted_at') . ' IS NOT NULL'); if ($days > 0) { $since = Factory::getDate('now - ' . $days . ' days')->toSql(); $query->where($db->quoteName('p.posted_at') . ' >= ' . $db->quote($since)); } if ($serviceType !== '') { $query->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')) ->where($db->quoteName('s.service_type') . ' = ' . $db->quote($serviceType)); } $query->group('dow, hr') ->order('dow ASC, hr ASC'); $db->setQuery($query); $rows = $db->loadObjectList(); $grid = []; for ($d = 0; $d < 7; $d++) { $grid[$d] = array_fill(0, 24, 0); } foreach ($rows as $row) { $grid[(int) $row->dow][(int) $row->hr] = (int) $row->cnt; } return $grid; } public static function getBestTimes(string $serviceType = '', int $days = 90, int $limit = 5): array { $grid = self::getPostingHeatmap($serviceType, $days); $slots = []; foreach ($grid as $dow => $hours) { foreach ($hours as $hour => $count) { if ($count > 0) { $slots[] = [ 'day' => self::$dayNames[$dow], 'hour' => $hour, 'count' => $count, 'label' => self::$dayNames[$dow] . ' ' . self::formatHour($hour), ]; } } } usort($slots, static fn($a, $b) => $b['count'] <=> $a['count']); return \array_slice($slots, 0, $limit); } public static function getServiceBreakdown(int $days = 30): array { $db = Factory::getDbo(); $query = $db->getQuery(true) ->select($db->quoteName('s.service_type')) ->select($db->quoteName('s.title', 'service_title')) ->select('COUNT(*) AS total') ->select('SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS success') ->select('SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('failed') . ' THEN 1 ELSE 0 END) AS failed') ->from($db->quoteName('#__mokosuitecross_posts', 'p')) ->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')); if ($days > 0) { $since = Factory::getDate('now - ' . $days . ' days')->toSql(); $query->where($db->quoteName('p.created') . ' >= ' . $db->quote($since)); } $query->group($db->quoteName(['s.service_type', 's.title'])) ->order('total DESC'); $db->setQuery($query); $rows = $db->loadObjectList(); $result = []; foreach ($rows as $row) { $total = (int) $row->total; $success = (int) $row->success; $result[] = [ 'service_type' => $row->service_type, 'service_title' => $row->service_title, 'total' => $total, 'success' => $success, 'failed' => (int) $row->failed, 'success_rate' => $total > 0 ? round(($success / $total) * 100, 1) : 0.0, 'avg_per_day' => $days > 0 ? round($total / $days, 1) : 0.0, ]; } return $result; } public static function getServiceTypes(): array { $db = Factory::getDbo(); $query = $db->getQuery(true) ->select('DISTINCT ' . $db->quoteName('service_type')) ->from($db->quoteName('#__mokosuitecross_services')) ->where($db->quoteName('published') . ' = 1') ->order($db->quoteName('service_type') . ' ASC'); $db->setQuery($query); return $db->loadColumn() ?: []; } private static function formatHour(int $hour): string { if ($hour === 0) { return '12:00 AM'; } if ($hour < 12) { return $hour . ':00 AM'; } if ($hour === 12) { return '12:00 PM'; } return ($hour - 12) . ':00 PM'; } }