27505f7501
Completes the MokoJoomCross → MokoSuiteCross rebrand across all language string keys, Joomla event names, documentation, and wiki pages. - 1,151 language key references renamed (COM_, PLG_, PKG_ prefixes) - Event names renamed (onMokoJoomCross* → onMokoSuiteCross*) - CLAUDE.md, CHANGELOG.md, wiki docs updated - Zero mokojoomcross references remaining in codebase Closes #128, closes #138
197 lines
6.8 KiB
PHP
197 lines
6.8 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 DashboardModel extends BaseDatabaseModel
|
|
{
|
|
/**
|
|
* Get summary statistics for the dashboard.
|
|
*
|
|
* @return object Stats object with counts
|
|
*/
|
|
public function getStats(): object
|
|
{
|
|
$db = $this->getDatabase();
|
|
|
|
$stats = new \stdClass();
|
|
|
|
// Active services count
|
|
$query = $db->getQuery(true)
|
|
->select('COUNT(*)')
|
|
->from($db->quoteName('#__mokosuitecross_services'))
|
|
->where($db->quoteName('published') . ' = 1');
|
|
$db->setQuery($query);
|
|
$stats->active_services = (int) $db->loadResult();
|
|
|
|
// Posts by status
|
|
foreach (['queued', 'posted', 'failed'] as $status) {
|
|
$query = $db->getQuery(true)
|
|
->select('COUNT(*)')
|
|
->from($db->quoteName('#__mokosuitecross_posts'))
|
|
->where($db->quoteName('status') . ' = ' . $db->quote($status));
|
|
$db->setQuery($query);
|
|
$stats->{$status . '_count'} = (int) $db->loadResult();
|
|
}
|
|
|
|
return $stats;
|
|
}
|
|
|
|
/**
|
|
* Check if Perfect Publisher Pro migration is available.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isMigrationAvailable(): bool
|
|
{
|
|
$db = $this->getDatabase();
|
|
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName('params'))
|
|
->from($db->quoteName('#__extensions'))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuitecross'));
|
|
|
|
$db->setQuery($query);
|
|
$params = json_decode($db->loadResult() ?: '{}', true);
|
|
|
|
return !empty($params['migration_available']);
|
|
}
|
|
|
|
/**
|
|
* Get recent activity log entries.
|
|
*
|
|
* @param int $limit Number of entries to return
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getRecentActivity(int $limit = 10): array
|
|
{
|
|
$db = $this->getDatabase();
|
|
|
|
$query = $db->getQuery(true)
|
|
->select('l.*, s.title AS service_title, s.service_type')
|
|
->from($db->quoteName('#__mokosuitecross_logs', 'l'))
|
|
->join('LEFT', $db->quoteName('#__mokosuitecross_services', 's')
|
|
. ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('l.service_id'))
|
|
->order($db->quoteName('l.created') . ' DESC');
|
|
|
|
$db->setQuery($query, 0, $limit);
|
|
|
|
return $db->loadObjectList() ?: [];
|
|
}
|
|
|
|
/**
|
|
* Get posts-per-service breakdown for the analytics chart.
|
|
*
|
|
* @param string|null $since Only count posts created on or after this datetime
|
|
*
|
|
* @return array [['service_type' => '...', 'posted' => N, 'failed' => N, 'queued' => N], ...]
|
|
*/
|
|
public function getServiceBreakdown(?string $since = null): array
|
|
{
|
|
$db = $this->getDatabase();
|
|
|
|
$query = $db->getQuery(true)
|
|
->select([
|
|
$db->quoteName('s.id', 'service_id'),
|
|
$db->quoteName('s.service_type'),
|
|
$db->quoteName('s.title', 'service_title'),
|
|
'SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS posted',
|
|
'SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('failed') . ' THEN 1 ELSE 0 END) AS failed',
|
|
'SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('queued') . ' THEN 1 ELSE 0 END) AS queued',
|
|
'COUNT(*) AS total',
|
|
])
|
|
->from($db->quoteName('#__mokosuitecross_posts', 'p'))
|
|
->join('INNER', $db->quoteName('#__mokosuitecross_services', 's')
|
|
. ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id'))
|
|
->group($db->quoteName(['s.id', 's.service_type', 's.title']))
|
|
->order('total DESC');
|
|
|
|
if ($since !== null) {
|
|
$query->where($db->quoteName('p.created') . ' >= ' . $db->quote($since));
|
|
}
|
|
|
|
$db->setQuery($query);
|
|
|
|
return $db->loadAssocList() ?: [];
|
|
}
|
|
|
|
/**
|
|
* Get posts-per-day for the last N days (for trend chart).
|
|
*
|
|
* @param int $days Number of days to look back
|
|
*
|
|
* @return array [['day' => '2026-05-28', 'posted' => N, 'failed' => N], ...]
|
|
*/
|
|
public function getDailyTrend(int $days = 14): 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('DATE(' . $db->quoteName('created') . ') >= ' . $db->quote($cutoff))
|
|
->group('DATE(' . $db->quoteName('created') . ')')
|
|
->order('day ASC');
|
|
|
|
$db->setQuery($query);
|
|
|
|
return $db->loadAssocList() ?: [];
|
|
}
|
|
|
|
/**
|
|
* Get most cross-posted articles.
|
|
*
|
|
* @param int $limit Number of articles
|
|
* @param string|null $since Only count posts created on or after this datetime
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getTopArticles(int $limit = 5, ?string $since = null): 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'))
|
|
->group($db->quoteName(['c.id', 'c.title']))
|
|
->order('post_count DESC');
|
|
|
|
if ($since !== null) {
|
|
$query->where($db->quoteName('p.created') . ' >= ' . $db->quote($since));
|
|
}
|
|
|
|
$db->setQuery($query, 0, $limit);
|
|
|
|
return $db->loadAssocList() ?: [];
|
|
}
|
|
}
|