* @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; /** * Per-service analytics drill-down model. */ class ServiceStatsModel extends BaseDatabaseModel { /** * Get the service ID from the request. * * @return int */ public function getServiceId(): int { return Factory::getApplication()->input->getInt('id', 0); } /** * Load a single service record by ID. * * @param int $id Service ID * * @return object|null */ public function getService(int $id = 0): ?object { if ($id === 0) { $id = $this->getServiceId(); } if ($id === 0) { return null; } $db = $this->getDatabase(); $query = $db->getQuery(true) ->select('*') ->from($db->quoteName('#__mokosuitecross_services')) ->where($db->quoteName('id') . ' = ' . (int) $id); $db->setQuery($query); return $db->loadObject() ?: null; } /** * Get post status counts for a specific service. * * @param int $serviceId Service ID * * @return object Object with total, posted, failed, queued properties */ public function getPostStats(int $serviceId): object { $db = $this->getDatabase(); $stats = new \stdClass(); foreach (['queued', 'posted', 'failed'] as $status) { $query = $db->getQuery(true) ->select('COUNT(*)') ->from($db->quoteName('#__mokosuitecross_posts')) ->where($db->quoteName('service_id') . ' = ' . (int) $serviceId) ->where($db->quoteName('status') . ' = ' . $db->quote($status)); $db->setQuery($query); $stats->{$status} = (int) $db->loadResult(); } $stats->total = $stats->queued + $stats->posted + $stats->failed; return $stats; } /** * Get daily post trend for a specific service. * * @param int $serviceId Service ID * @param int $days Number of days to look back * * @return array [['day' => '2026-05-28', 'posted' => N, 'failed' => N], ...] */ public function getDailyTrend(int $serviceId, int $days = 30): array { $db = $this->getDatabase(); $cutoff = Factory::getDate('now - ' . $days . ' days')->format('Y-m-d'); $query = $db->getQuery(true) ->select([ 'DATE(' . $db->quoteName('created') . ') AS day', 'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS posted', 'SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('failed') . ' THEN 1 ELSE 0 END) AS failed', 'COUNT(*) AS total', ]) ->from($db->quoteName('#__mokosuitecross_posts')) ->where($db->quoteName('service_id') . ' = ' . (int) $serviceId) ->where('DATE(' . $db->quoteName('created') . ') >= ' . $db->quote($cutoff)) ->group('DATE(' . $db->quoteName('created') . ')') ->order('day ASC'); $db->setQuery($query); return $db->loadAssocList() ?: []; } /** * Get recent posts for a specific service with article titles. * * @param int $serviceId Service ID * @param int $limit Number of posts to return * * @return array */ public function getRecentPosts(int $serviceId, int $limit = 20): array { $db = $this->getDatabase(); $query = $db->getQuery(true) ->select([ $db->quoteName('p.id'), $db->quoteName('p.status'), $db->quoteName('p.posted_at'), $db->quoteName('p.created'), $db->quoteName('p.error_message'), $db->quoteName('p.retry_count'), $db->quoteName('c.title', 'article_title'), ]) ->from($db->quoteName('#__mokosuitecross_posts', 'p')) ->join('LEFT', $db->quoteName('#__content', 'c') . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('p.article_id')) ->where($db->quoteName('p.service_id') . ' = ' . (int) $serviceId) ->order($db->quoteName('p.created') . ' DESC'); $db->setQuery($query, 0, $limit); return $db->loadAssocList() ?: []; } /** * Get the most cross-posted articles for a specific service. * * @param int $serviceId Service ID * @param int $limit Number of articles to return * * @return array */ public function getTopArticles(int $serviceId, int $limit = 10): array { $db = $this->getDatabase(); $query = $db->getQuery(true) ->select([ $db->quoteName('c.id'), $db->quoteName('c.title'), 'COUNT(*) AS post_count', 'SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS success_count', ]) ->from($db->quoteName('#__mokosuitecross_posts', 'p')) ->join('INNER', $db->quoteName('#__content', 'c') . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('p.article_id')) ->where($db->quoteName('p.service_id') . ' = ' . (int) $serviceId) ->group($db->quoteName(['c.id', 'c.title'])) ->order('post_count DESC'); $db->setQuery($query, 0, $limit); return $db->loadAssocList() ?: []; } }