From 8c82b39747a8caabaee59ccc8ce8b294ca1a8c23 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 11 Jun 2026 21:50:12 -0500 Subject: [PATCH] feat(tickets): add status/priority admin CRUD (#206) New 'ticketsettings' view with inline CRUD for ticket statuses and priorities: - Table display with color preview, default/closed flags, ordering - Inline add/edit form with all fields - Delete with in-use protection (can't delete if tickets reference it) - Auto-generates alias from title - Controller actions: saveStatus, deleteStatus, savePriority, deletePriority - Gated by core.admin ACL --- .../src/Controller/DisplayController.php | 82 ++++++- .../admin/src/Model/TicketsModel.php | 111 ++++++++++ .../src/View/Ticketsettings/HtmlView.php | 41 ++++ .../admin/src/View/Ticketsettings/index.html | 1 + .../admin/tmpl/ticketsettings/default.php | 203 ++++++++++++++++++ .../admin/tmpl/ticketsettings/index.html | 1 + 6 files changed, 436 insertions(+), 3 deletions(-) create mode 100644 source/packages/com_mokosuite/admin/src/View/Ticketsettings/HtmlView.php create mode 100644 source/packages/com_mokosuite/admin/src/View/Ticketsettings/index.html create mode 100644 source/packages/com_mokosuite/admin/tmpl/ticketsettings/default.php create mode 100644 source/packages/com_mokosuite/admin/tmpl/ticketsettings/index.html diff --git a/source/packages/com_mokosuite/admin/src/Controller/DisplayController.php b/source/packages/com_mokosuite/admin/src/Controller/DisplayController.php index 47186369..883ec836 100644 --- a/source/packages/com_mokosuite/admin/src/Controller/DisplayController.php +++ b/source/packages/com_mokosuite/admin/src/Controller/DisplayController.php @@ -33,9 +33,10 @@ class DisplayController extends BaseController 'waflog' => 'core.admin', 'categories' => 'mokosuite.tickets', 'canned' => 'mokosuite.tickets', - 'automation' => 'core.admin', - 'database' => 'core.admin', - 'cleanup' => 'mokosuite.cache', + 'automation' => 'core.admin', + 'database' => 'core.admin', + 'cleanup' => 'mokosuite.cache', + 'ticketsettings' => 'core.admin', ]; public function display($cachable = false, $urlparams = []) @@ -413,6 +414,81 @@ class DisplayController extends BaseController )); } + // ================================================================== + // Ticket Settings — Status/Priority CRUD + // ================================================================== + + public function saveStatus() + { + Session::checkToken() or die(Text::_('JINVALID_TOKEN')); + + if (!$this->checkAcl('core.admin')) + { + $this->jsonForbidden(); + return; + } + + $input = Factory::getApplication()->getInput(); + $this->jsonResponse($this->getModel('Tickets')->saveStatus([ + 'id' => $input->getInt('id', 0), + 'title' => $input->getString('title', ''), + 'alias' => $input->getString('alias', ''), + 'color' => $input->getString('color', 'bg-secondary'), + 'is_default' => $input->getInt('is_default', 0), + 'is_closed' => $input->getInt('is_closed', 0), + 'ordering' => $input->getInt('ordering', 0), + ])); + } + + public function deleteStatus() + { + Session::checkToken() or die(Text::_('JINVALID_TOKEN')); + + if (!$this->checkAcl('core.admin')) + { + $this->jsonForbidden(); + return; + } + + $id = Factory::getApplication()->getInput()->getInt('id', 0); + $this->jsonResponse($this->getModel('Tickets')->deleteStatus($id)); + } + + public function savePriority() + { + Session::checkToken() or die(Text::_('JINVALID_TOKEN')); + + if (!$this->checkAcl('core.admin')) + { + $this->jsonForbidden(); + return; + } + + $input = Factory::getApplication()->getInput(); + $this->jsonResponse($this->getModel('Tickets')->savePriority([ + 'id' => $input->getInt('id', 0), + 'title' => $input->getString('title', ''), + 'alias' => $input->getString('alias', ''), + 'color' => $input->getString('color', 'bg-secondary'), + 'is_default' => $input->getInt('is_default', 0), + 'ordering' => $input->getInt('ordering', 0), + ])); + } + + public function deletePriority() + { + Session::checkToken() or die(Text::_('JINVALID_TOKEN')); + + if (!$this->checkAcl('core.admin')) + { + $this->jsonForbidden(); + return; + } + + $id = Factory::getApplication()->getInput()->getInt('id', 0); + $this->jsonResponse($this->getModel('Tickets')->deletePriority($id)); + } + // ================================================================== // KB Search // ================================================================== diff --git a/source/packages/com_mokosuite/admin/src/Model/TicketsModel.php b/source/packages/com_mokosuite/admin/src/Model/TicketsModel.php index 2838c41c..df73885e 100644 --- a/source/packages/com_mokosuite/admin/src/Model/TicketsModel.php +++ b/source/packages/com_mokosuite/admin/src/Model/TicketsModel.php @@ -1133,6 +1133,117 @@ class TicketsModel extends BaseDatabaseModel return $db->loadObjectList() ?: []; } + // ================================================================== + // Status/Priority CRUD + // ================================================================== + + public function saveStatus(array $data): array + { + $db = $this->getDatabase(); + $obj = (object) $data; + + if (!empty($obj->title) && empty($obj->alias)) + { + $obj->alias = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $obj->title)); + } + + if (empty($obj->id)) + { + unset($obj->id); + $db->insertObject('#__mokosuite_ticket_statuses', $obj, 'id'); + + return ['status' => 'ok', 'id' => (int) $obj->id, 'message' => 'Status created']; + } + + $db->updateObject('#__mokosuite_ticket_statuses', $obj, 'id'); + + return ['status' => 'ok', 'id' => (int) $obj->id, 'message' => 'Status updated']; + } + + public function deleteStatus(int $id): array + { + if ($id < 1) + { + return ['status' => 'error', 'message' => 'Invalid ID']; + } + + $db = $this->getDatabase(); + + // Check no tickets use this status + $db->setQuery( + $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuite_tickets')) + ->where($db->quoteName('status_id') . ' = ' . $id) + ); + + if ((int) $db->loadResult() > 0) + { + return ['status' => 'error', 'message' => 'Cannot delete — status is in use by tickets']; + } + + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__mokosuite_ticket_statuses')) + ->where($db->quoteName('id') . ' = ' . $id) + )->execute(); + + return ['status' => 'ok', 'message' => 'Status deleted']; + } + + public function savePriority(array $data): array + { + $db = $this->getDatabase(); + $obj = (object) $data; + + if (!empty($obj->title) && empty($obj->alias)) + { + $obj->alias = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $obj->title)); + } + + if (empty($obj->id)) + { + unset($obj->id); + $db->insertObject('#__mokosuite_ticket_priorities', $obj, 'id'); + + return ['status' => 'ok', 'id' => (int) $obj->id, 'message' => 'Priority created']; + } + + $db->updateObject('#__mokosuite_ticket_priorities', $obj, 'id'); + + return ['status' => 'ok', 'id' => (int) $obj->id, 'message' => 'Priority updated']; + } + + public function deletePriority(int $id): array + { + if ($id < 1) + { + return ['status' => 'error', 'message' => 'Invalid ID']; + } + + $db = $this->getDatabase(); + + $db->setQuery( + $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuite_tickets')) + ->where($db->quoteName('priority_id') . ' = ' . $id) + ); + + if ((int) $db->loadResult() > 0) + { + return ['status' => 'error', 'message' => 'Cannot delete — priority is in use by tickets']; + } + + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__mokosuite_ticket_priorities')) + ->where($db->quoteName('id') . ' = ' . $id) + )->execute(); + + return ['status' => 'ok', 'message' => 'Priority deleted']; + } + // ================================================================== // Akeeba Ticket System Importer // ================================================================== diff --git a/source/packages/com_mokosuite/admin/src/View/Ticketsettings/HtmlView.php b/source/packages/com_mokosuite/admin/src/View/Ticketsettings/HtmlView.php new file mode 100644 index 00000000..7cab5fe3 --- /dev/null +++ b/source/packages/com_mokosuite/admin/src/View/Ticketsettings/HtmlView.php @@ -0,0 +1,41 @@ + + * + * SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later + * + * @package MokoSuite + * @subpackage Component + */ + +namespace Moko\Component\MokoSuite\Administrator\View\Ticketsettings; + +defined('_JEXEC') or die; + +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + protected $statuses = []; + protected $priorities = []; + + public function display($tpl = null) + { + $model = $this->getModel('Tickets'); + + $this->statuses = $model->getStatuses(); + $this->priorities = $model->getPriorities(); + + $this->addToolbar(); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + ToolbarHelper::title(Text::_('COM_MOKOSUITE_TICKET_SETTINGS'), 'cog'); + ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite&view=tickets'); + } +} diff --git a/source/packages/com_mokosuite/admin/src/View/Ticketsettings/index.html b/source/packages/com_mokosuite/admin/src/View/Ticketsettings/index.html new file mode 100644 index 00000000..94906bce --- /dev/null +++ b/source/packages/com_mokosuite/admin/src/View/Ticketsettings/index.html @@ -0,0 +1 @@ + diff --git a/source/packages/com_mokosuite/admin/tmpl/ticketsettings/default.php b/source/packages/com_mokosuite/admin/tmpl/ticketsettings/default.php new file mode 100644 index 00000000..6f806192 --- /dev/null +++ b/source/packages/com_mokosuite/admin/tmpl/ticketsettings/default.php @@ -0,0 +1,203 @@ + + * + * SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later + * + * @package MokoSuite + * @subpackage Component + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Session\Session; + +$token = Session::getFormToken(); + +$colorOptions = [ + 'bg-primary', 'bg-secondary', 'bg-success', 'bg-danger', + 'bg-warning text-dark', 'bg-info text-dark', 'bg-dark', 'bg-light text-dark', +]; +?> + +
+ +
+
+
+ Ticket Statuses +
+
+ + + + + + + + + + + + + statuses as $s): ?> + + + + + + + + + + +
TitleColorDefaultClosed?OrderActions
escape($s->title); ?> (escape($s->alias); ?>)   is_default ? 'Yes' : ''; ?>is_closed ? 'Closed' : ''; ?>ordering; ?> + + + + +
+
+ +
+
+ + +
+
+
+ Ticket Priorities +
+
+ + + + + + + + + + + + priorities as $p): ?> + + + + + + + + + +
TitleColorDefaultOrderActions
escape($p->title); ?> (escape($p->alias); ?>)   is_default ? 'Yes' : ''; ?>ordering; ?> + + + + +
+
+ +
+
+
+ + diff --git a/source/packages/com_mokosuite/admin/tmpl/ticketsettings/index.html b/source/packages/com_mokosuite/admin/tmpl/ticketsettings/index.html new file mode 100644 index 00000000..94906bce --- /dev/null +++ b/source/packages/com_mokosuite/admin/tmpl/ticketsettings/index.html @@ -0,0 +1 @@ +