45077671fa
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Universal: PR Check / Validate PR (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
Generic: Repo Health / Access control (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
- GET /v1/mokosuite/tickets — list with status/category/assigned filters
- GET /v1/mokosuite/tickets/{id} — single ticket with replies + attachments
- POST /v1/mokosuite/tickets — create ticket
- PATCH /v1/mokosuite/tickets/{id} — update status/priority/category/assignment
- POST /v1/mokosuite/tickets/{id}/reply — add reply with notification
- Routes registered in plg_webservices_mokosuite
262 lines
8.1 KiB
PHP
262 lines
8.1 KiB
PHP
<?php
|
|
/**
|
|
* @package MokoSuite
|
|
* @subpackage com_mokosuite
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*/
|
|
|
|
namespace Moko\Component\MokoSuite\Api\Controller;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\MVC\Controller\BaseController;
|
|
|
|
/**
|
|
* Helpdesk Tickets REST API controller.
|
|
*
|
|
* GET /api/index.php/v1/mokosuite/tickets - list tickets
|
|
* GET /api/index.php/v1/mokosuite/tickets/{id} - get single ticket with replies
|
|
* POST /api/index.php/v1/mokosuite/tickets - create ticket
|
|
* PATCH /api/index.php/v1/mokosuite/tickets/{id} - update ticket fields
|
|
* POST /api/index.php/v1/mokosuite/tickets/{id}/reply - add reply
|
|
*
|
|
* @since 02.35.00
|
|
*/
|
|
class TicketsController extends BaseController
|
|
{
|
|
/**
|
|
* GET /tickets — list tickets with optional filters.
|
|
*/
|
|
public function displayList(): void
|
|
{
|
|
$this->requireAuth('core.manage', 'com_mokosuite');
|
|
|
|
$app = Factory::getApplication();
|
|
$db = Factory::getDbo();
|
|
$input = $app->getInput();
|
|
|
|
$query = $db->getQuery(true)
|
|
->select('t.*, s.title AS status_title, p.title AS priority_title, c.title AS category_title, u.name AS created_by_name')
|
|
->from($db->quoteName('#__mokosuite_tickets', 't'))
|
|
->leftJoin($db->quoteName('#__mokosuite_ticket_statuses', 's') . ' ON s.id = t.status_id')
|
|
->leftJoin($db->quoteName('#__mokosuite_ticket_priorities', 'p') . ' ON p.id = t.priority_id')
|
|
->leftJoin($db->quoteName('#__mokosuite_ticket_categories', 'c') . ' ON c.id = t.category_id')
|
|
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
|
|
->order('t.created DESC');
|
|
|
|
// Filters
|
|
$status = $input->getString('status', '');
|
|
if ($status) {
|
|
$query->where($db->quoteName('t.status') . ' = ' . $db->quote($status));
|
|
}
|
|
|
|
$categoryId = $input->getInt('category_id', 0);
|
|
if ($categoryId) {
|
|
$query->where($db->quoteName('t.category_id') . ' = ' . $categoryId);
|
|
}
|
|
|
|
$assignedTo = $input->getInt('assigned_to', 0);
|
|
if ($assignedTo) {
|
|
$query->where($db->quoteName('t.assigned_to') . ' = ' . $assignedTo);
|
|
}
|
|
|
|
$limit = min($input->getInt('limit', 25), 100);
|
|
$offset = $input->getInt('offset', 0);
|
|
$db->setQuery($query, $offset, $limit);
|
|
|
|
$tickets = $db->loadObjectList() ?: [];
|
|
|
|
// Total count
|
|
$countQuery = $db->getQuery(true)->select('COUNT(*)')->from('#__mokosuite_tickets');
|
|
$db->setQuery($countQuery);
|
|
$total = (int) $db->loadResult();
|
|
|
|
$this->sendJson(200, [
|
|
'tickets' => $tickets,
|
|
'total' => $total,
|
|
'limit' => $limit,
|
|
'offset' => $offset,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* GET /tickets/{id} — single ticket with replies and attachments.
|
|
*/
|
|
public function displayItem(): void
|
|
{
|
|
$this->requireAuth('core.manage', 'com_mokosuite');
|
|
|
|
$id = Factory::getApplication()->getInput()->getInt('id', 0);
|
|
$db = Factory::getDbo();
|
|
|
|
// Ticket
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->select('t.*, s.title AS status_title, p.title AS priority_title, c.title AS category_title, u.name AS created_by_name')
|
|
->from($db->quoteName('#__mokosuite_tickets', 't'))
|
|
->leftJoin($db->quoteName('#__mokosuite_ticket_statuses', 's') . ' ON s.id = t.status_id')
|
|
->leftJoin($db->quoteName('#__mokosuite_ticket_priorities', 'p') . ' ON p.id = t.priority_id')
|
|
->leftJoin($db->quoteName('#__mokosuite_ticket_categories', 'c') . ' ON c.id = t.category_id')
|
|
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
|
|
->where('t.id = ' . $id)
|
|
);
|
|
$ticket = $db->loadObject();
|
|
|
|
if (!$ticket) {
|
|
$this->sendJson(404, ['error' => 'Ticket not found']);
|
|
return;
|
|
}
|
|
|
|
// Replies
|
|
$db->setQuery(
|
|
$db->getQuery(true)
|
|
->select('r.*, u.name AS user_name')
|
|
->from($db->quoteName('#__mokosuite_ticket_replies', 'r'))
|
|
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id')
|
|
->where('r.ticket_id = ' . $id)
|
|
->order('r.created ASC')
|
|
);
|
|
$ticket->replies = $db->loadObjectList() ?: [];
|
|
|
|
// Attachments
|
|
$ticket->attachments = \Moko\Component\MokoSuite\Administrator\Service\AttachmentService::getForTicket($id);
|
|
|
|
$this->sendJson(200, $ticket);
|
|
}
|
|
|
|
/**
|
|
* POST /tickets — create a new ticket.
|
|
*/
|
|
public function create(): void
|
|
{
|
|
$this->requireAuth('core.manage', 'com_mokosuite');
|
|
|
|
$input = Factory::getApplication()->getInput();
|
|
$db = Factory::getDbo();
|
|
|
|
$subject = $input->getString('subject', '');
|
|
$body = $input->getRaw('body', '');
|
|
|
|
if (empty($subject)) {
|
|
$this->sendJson(400, ['error' => 'Subject is required']);
|
|
return;
|
|
}
|
|
|
|
$ticket = (object) [
|
|
'subject' => $subject,
|
|
'body' => $body,
|
|
'status' => 'open',
|
|
'status_id' => $input->getInt('status_id', 0) ?: null,
|
|
'priority' => $input->getString('priority', 'normal'),
|
|
'priority_id' => $input->getInt('priority_id', 0) ?: null,
|
|
'category_id' => $input->getInt('category_id', 0) ?: null,
|
|
'created_by' => (int) Factory::getUser()->id,
|
|
'assigned_to' => $input->getInt('assigned_to', 0) ?: null,
|
|
'created' => Factory::getDate()->toSql(),
|
|
];
|
|
|
|
$db->insertObject('#__mokosuite_tickets', $ticket, 'id');
|
|
|
|
// Trigger notification
|
|
\Moko\Component\MokoSuite\Administrator\Service\NotificationService::notify('ticket_created', $ticket);
|
|
|
|
$this->sendJson(201, ['id' => (int) $ticket->id, 'message' => 'Ticket created']);
|
|
}
|
|
|
|
/**
|
|
* PATCH /tickets/{id} — update ticket fields.
|
|
*/
|
|
public function update(): void
|
|
{
|
|
$this->requireAuth('core.manage', 'com_mokosuite');
|
|
|
|
$input = Factory::getApplication()->getInput();
|
|
$id = $input->getInt('id', 0);
|
|
$db = Factory::getDbo();
|
|
|
|
$fields = [];
|
|
$updatable = ['status', 'status_id', 'priority', 'priority_id', 'category_id', 'assigned_to'];
|
|
|
|
foreach ($updatable as $field) {
|
|
$value = $input->get($field, null, 'raw');
|
|
if ($value !== null) {
|
|
$fields[$field] = $value;
|
|
}
|
|
}
|
|
|
|
if (empty($fields)) {
|
|
$this->sendJson(400, ['error' => 'No fields to update']);
|
|
return;
|
|
}
|
|
|
|
$sets = [];
|
|
foreach ($fields as $k => $v) {
|
|
$sets[] = $db->quoteName($k) . ' = ' . $db->quote($v);
|
|
}
|
|
$sets[] = 'modified = ' . $db->quote(Factory::getDate()->toSql());
|
|
|
|
$db->setQuery('UPDATE ' . $db->quoteName('#__mokosuite_tickets') . ' SET ' . implode(', ', $sets) . ' WHERE id = ' . $id)->execute();
|
|
|
|
$this->sendJson(200, ['id' => $id, 'message' => 'Ticket updated', 'updated' => array_keys($fields)]);
|
|
}
|
|
|
|
/**
|
|
* POST /tickets/{id}/reply — add a reply.
|
|
*/
|
|
public function reply(): void
|
|
{
|
|
$this->requireAuth('core.manage', 'com_mokosuite');
|
|
|
|
$input = Factory::getApplication()->getInput();
|
|
$ticketId = $input->getInt('id', 0);
|
|
$body = $input->getRaw('body', '');
|
|
|
|
if (!$ticketId || empty($body)) {
|
|
$this->sendJson(400, ['error' => 'ticket_id and body are required']);
|
|
return;
|
|
}
|
|
|
|
$db = Factory::getDbo();
|
|
|
|
$reply = (object) [
|
|
'ticket_id' => $ticketId,
|
|
'user_id' => (int) Factory::getUser()->id,
|
|
'body' => $body,
|
|
'is_internal' => $input->getInt('is_internal', 0),
|
|
'created' => Factory::getDate()->toSql(),
|
|
];
|
|
|
|
$db->insertObject('#__mokosuite_ticket_replies', $reply, 'id');
|
|
|
|
// Notify
|
|
$db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuite_tickets')->where('id = ' . $ticketId));
|
|
$ticket = $db->loadObject();
|
|
if ($ticket) {
|
|
\Moko\Component\MokoSuite\Administrator\Service\NotificationService::notify('ticket_replied', $ticket, ['reply_body' => $body]);
|
|
}
|
|
|
|
$this->sendJson(201, ['reply_id' => (int) $reply->id, 'message' => 'Reply added']);
|
|
}
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────
|
|
|
|
private function requireAuth(string $action, string $asset): void
|
|
{
|
|
$user = Factory::getUser();
|
|
if (!$user->authorise($action, $asset)) {
|
|
$this->sendJson(403, ['error' => 'Not authorized']);
|
|
}
|
|
}
|
|
|
|
private function sendJson(int $code, $payload): void
|
|
{
|
|
$app = Factory::getApplication();
|
|
$app->setHeader('Content-Type', 'application/json', true);
|
|
$app->setHeader('Status', (string) $code, true);
|
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
$app->close();
|
|
}
|
|
}
|