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(); } }