0d49195f52
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Generic: Project CI / Lint & Validate (pull_request) Successful in 50s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 52s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Authored-by: Moko Consulting
269 lines
8.4 KiB
PHP
269 lines
8.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\Controller;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\MVC\Controller\BaseController;
|
|
use Joomla\CMS\Session\Session;
|
|
use Joomla\Component\MokoSuiteCross\Administrator\Table\PostTable;
|
|
|
|
/**
|
|
* Calendar controller -- provides AJAX endpoints for the visual post calendar.
|
|
*
|
|
* Endpoints:
|
|
* task=calendar.events -- GET JSON feed for FullCalendar (filtered by start/end)
|
|
* task=calendar.reschedule -- POST reschedule a post to a new date/time
|
|
*/
|
|
class CalendarController extends BaseController
|
|
{
|
|
/**
|
|
* Return posts as FullCalendar-compatible JSON events.
|
|
*
|
|
* Query params: start, end (ISO 8601 date range from FullCalendar).
|
|
*
|
|
* @return void
|
|
*/
|
|
public function events(): void
|
|
{
|
|
$app = $this->app;
|
|
$db = Factory::getDbo();
|
|
|
|
// ACL check
|
|
if (!$app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) {
|
|
$this->sendJsonResponse(['error' => 'Forbidden'], 403);
|
|
|
|
return;
|
|
}
|
|
|
|
// FullCalendar sends start/end as ISO date strings
|
|
$start = $this->input->getString('start', '');
|
|
$end = $this->input->getString('end', '');
|
|
|
|
$query = $db->getQuery(true)
|
|
->select([
|
|
'p.' . $db->quoteName('id'),
|
|
'p.' . $db->quoteName('article_id'),
|
|
'p.' . $db->quoteName('service_id'),
|
|
'p.' . $db->quoteName('status'),
|
|
'p.' . $db->quoteName('scheduled_at'),
|
|
'p.' . $db->quoteName('posted_at'),
|
|
'p.' . $db->quoteName('created'),
|
|
'p.' . $db->quoteName('message'),
|
|
'a.' . $db->quoteName('title', 'article_title'),
|
|
's.' . $db->quoteName('title', 'service_title'),
|
|
's.' . $db->quoteName('service_type'),
|
|
])
|
|
->from($db->quoteName('#__mokosuitecross_posts', 'p'))
|
|
->leftJoin(
|
|
$db->quoteName('#__content', 'a')
|
|
. ' ON ' . $db->quoteName('a.id') . ' = ' . $db->quoteName('p.article_id')
|
|
)
|
|
->leftJoin(
|
|
$db->quoteName('#__mokosuitecross_services', 's')
|
|
. ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')
|
|
)
|
|
->order($db->quoteName('p.created') . ' DESC');
|
|
|
|
// Filter by date range when provided
|
|
if ($start !== '') {
|
|
$dateExpr = 'COALESCE(p.scheduled_at, p.posted_at, p.created)';
|
|
$query->where($dateExpr . ' >= ' . $db->quote($start));
|
|
}
|
|
|
|
if ($end !== '') {
|
|
$dateExpr = 'COALESCE(p.scheduled_at, p.posted_at, p.created)';
|
|
$query->where($dateExpr . ' <= ' . $db->quote($end));
|
|
}
|
|
|
|
$db->setQuery($query);
|
|
$rows = $db->loadObjectList() ?: [];
|
|
|
|
// Map status to colour
|
|
$statusColors = [
|
|
'posted' => '#28a745',
|
|
'scheduled' => '#007bff',
|
|
'queued' => '#ffc107',
|
|
'failed' => '#dc3545',
|
|
'posting' => '#17a2b8',
|
|
];
|
|
|
|
$events = [];
|
|
|
|
foreach ($rows as $row) {
|
|
// Pick the best date for the calendar event
|
|
$eventDate = $row->scheduled_at ?: ($row->posted_at ?: $row->created);
|
|
|
|
// Skip rows with no usable date
|
|
if (empty($eventDate) || $eventDate === '0000-00-00 00:00:00') {
|
|
continue;
|
|
}
|
|
|
|
$title = ($row->article_title ?: 'Post #' . $row->id);
|
|
|
|
if ($row->service_title) {
|
|
$title .= ' - ' . $row->service_title;
|
|
}
|
|
|
|
$events[] = [
|
|
'id' => (int) $row->id,
|
|
'title' => $title,
|
|
'start' => $eventDate,
|
|
'color' => $statusColors[$row->status] ?? '#6c757d',
|
|
'url' => 'index.php?option=com_mokosuitecross&task=post.edit&id=' . (int) $row->id,
|
|
'extendedProps' => [
|
|
'status' => $row->status,
|
|
'service_type' => $row->service_type ?? '',
|
|
'article_id' => (int) $row->article_id,
|
|
'service_id' => (int) $row->service_id,
|
|
'message' => mb_substr($row->message ?? '', 0, 200),
|
|
],
|
|
];
|
|
}
|
|
|
|
$this->sendJsonResponse($events, 200);
|
|
}
|
|
|
|
/**
|
|
* Reschedule a post to a new date/time via drag-drop.
|
|
*
|
|
* POST params: post_id (int), new_date (ISO 8601 datetime string).
|
|
*
|
|
* @return void
|
|
*/
|
|
public function reschedule(): void
|
|
{
|
|
$app = $this->app;
|
|
|
|
// CSRF check
|
|
if (!Session::checkToken('post')) {
|
|
$this->sendJsonResponse(['error' => Text::_('JINVALID_TOKEN')], 403);
|
|
|
|
return;
|
|
}
|
|
|
|
// ACL check
|
|
if (!$app->getIdentity()->authorise('core.edit', 'com_mokosuitecross')) {
|
|
$this->sendJsonResponse(['error' => 'Forbidden'], 403);
|
|
|
|
return;
|
|
}
|
|
|
|
$postId = $this->input->getInt('post_id', 0);
|
|
$newDate = $this->input->getString('new_date', '');
|
|
|
|
if ($postId < 1 || $newDate === '') {
|
|
$this->sendJsonResponse(
|
|
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
|
|
400
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
// Validate the date format
|
|
try {
|
|
$dateObj = Factory::getDate($newDate);
|
|
} catch (\Exception $e) {
|
|
$this->sendJsonResponse(
|
|
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
|
|
400
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
// Load the post using Table bind/check/store pattern
|
|
$db = Factory::getDbo();
|
|
$table = new PostTable($db);
|
|
|
|
if (!$table->load($postId)) {
|
|
$this->sendJsonResponse(
|
|
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
|
|
404
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
// Only allow rescheduling of scheduled or queued posts
|
|
$allowedStatuses = ['scheduled', 'queued'];
|
|
|
|
if (!in_array($table->status, $allowedStatuses, true)) {
|
|
$this->sendJsonResponse(
|
|
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
|
|
400
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
// Update the post
|
|
$data = [
|
|
'scheduled_at' => $dateObj->toSql(),
|
|
'status' => 'scheduled',
|
|
'modified' => Factory::getDate()->toSql(),
|
|
];
|
|
|
|
if (!$table->bind($data) || !$table->check() || !$table->store()) {
|
|
$this->sendJsonResponse(
|
|
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
|
|
500
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
// Log the reschedule
|
|
$log = (object) [
|
|
'post_id' => $postId,
|
|
'service_id' => (int) $table->service_id,
|
|
'level' => 'info',
|
|
'message' => sprintf('Post rescheduled to %s via calendar drag-drop', $dateObj->toSql()),
|
|
'context' => '{}',
|
|
'created' => Factory::getDate()->toSql(),
|
|
];
|
|
|
|
$db->insertObject('#__mokosuitecross_logs', $log);
|
|
|
|
$this->sendJsonResponse(
|
|
[
|
|
'success' => true,
|
|
'message' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_SUCCESS'),
|
|
],
|
|
200
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Send a JSON response and close the application.
|
|
*
|
|
* @param array $data Response data
|
|
* @param int $httpCode HTTP status code
|
|
*
|
|
* @return void
|
|
*/
|
|
private function sendJsonResponse(array $data, int $httpCode): void
|
|
{
|
|
$app = $this->app;
|
|
|
|
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
$app->setHeader('Status', (string) $httpCode);
|
|
|
|
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
$app->close();
|
|
}
|
|
}
|