diff --git a/src/packages/com_mokowaas/admin/sql/install.mysql.sql b/src/packages/com_mokowaas/admin/sql/install.mysql.sql index 12dbca53..208a25bd 100644 --- a/src/packages/com_mokowaas/admin/sql/install.mysql.sql +++ b/src/packages/com_mokowaas/admin/sql/install.mysql.sql @@ -61,6 +61,23 @@ CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_canned` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_automation` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `title` VARCHAR(255) NOT NULL, + `trigger_event` VARCHAR(50) NOT NULL DEFAULT 'ticket_created', + `conditions` TEXT NOT NULL DEFAULT '[]', + `actions` TEXT NOT NULL DEFAULT '[]', + `enabled` TINYINT NOT NULL DEFAULT 1, + `ordering` INT NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Default automation rules +INSERT IGNORE INTO `#__mokowaas_ticket_automation` (`id`, `title`, `trigger_event`, `conditions`, `actions`, `enabled`, `ordering`) VALUES +(1, 'Auto-close resolved tickets after 7 days', 'scheduled', '[{"field":"status","op":"eq","value":"resolved"},{"field":"age_hours","op":"gt","value":"168"}]', '[{"type":"set_status","value":"closed"},{"type":"add_note","value":"Auto-closed after 7 days with no response."}]', 1, 1), +(2, 'Escalate urgent tickets with no response in 1 hour', 'scheduled', '[{"field":"priority","op":"eq","value":"urgent"},{"field":"sla_responded","op":"eq","value":"0"},{"field":"age_hours","op":"gt","value":"1"}]', '[{"type":"add_note","value":"SLA BREACH: Urgent ticket has no staff response after 1 hour."}]', 1, 2), +(3, 'Notify on high priority ticket creation', 'ticket_created', '[{"field":"priority","op":"in","value":"high,urgent"}]', '[{"type":"add_note","value":"High/urgent ticket created — requires immediate attention."}]', 1, 3); + -- Default categories INSERT IGNORE INTO `#__mokowaas_ticket_categories` (`id`, `title`, `alias`, `description`, `sla_response_minutes`, `sla_resolution_minutes`, `ordering`) VALUES (1, 'General Support', 'general-support', 'General questions and assistance', 480, 2880, 1), diff --git a/src/packages/com_mokowaas/admin/src/Model/TicketsModel.php b/src/packages/com_mokowaas/admin/src/Model/TicketsModel.php index 8bad035d..657b7d71 100644 --- a/src/packages/com_mokowaas/admin/src/Model/TicketsModel.php +++ b/src/packages/com_mokowaas/admin/src/Model/TicketsModel.php @@ -173,6 +173,9 @@ class TicketsModel extends BaseDatabaseModel $db->insertObject('#__mokowaas_tickets', $ticket, 'id'); + // Run automation + $this->runAutomation('ticket_created', (int) $ticket->id); + return ['success' => true, 'message' => 'Ticket #' . $ticket->id . ' created.', 'id' => (int) $ticket->id]; } catch (\Throwable $e) @@ -212,6 +215,9 @@ class TicketsModel extends BaseDatabaseModel ->where($db->quoteName('sla_responded') . ' = 0') )->execute(); + // Run automation + $this->runAutomation('ticket_replied', $ticketId); + return ['success' => true, 'message' => 'Reply added.']; } catch (\Throwable $e) @@ -259,6 +265,9 @@ class TicketsModel extends BaseDatabaseModel ->where($db->quoteName('id') . ' = ' . $ticketId) )->execute(); + // Run automation + $this->runAutomation('status_changed', $ticketId); + return ['success' => true, 'message' => 'Status updated to ' . $status . '.']; } catch (\Throwable $e) @@ -351,6 +360,235 @@ class TicketsModel extends BaseDatabaseModel return $db->loadObjectList() ?: []; } + // ================================================================== + // Automation Engine + // ================================================================== + + /** + * Run automation rules for a specific trigger event against a ticket. + * + * @param string $event trigger_event: ticket_created, ticket_replied, status_changed, scheduled + * @param int $ticketId The ticket to evaluate + */ + public function runAutomation(string $event, int $ticketId): void + { + try + { + $db = $this->getDatabase(); + + // Load enabled rules for this event + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokowaas_ticket_automation')) + ->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event)) + ->where($db->quoteName('enabled') . ' = 1') + ->order($db->quoteName('ordering') . ' ASC'); + $db->setQuery($query); + $rules = $db->loadObjectList() ?: []; + + if (empty($rules)) + { + return; + } + + // Load the ticket + $ticket = $this->getTicket($ticketId); + + if (!$ticket) + { + return; + } + + // Calculate age in hours + $ticket->age_hours = (time() - strtotime($ticket->created)) / 3600; + + foreach ($rules as $rule) + { + $conditions = json_decode($rule->conditions, true) ?: []; + $actions = json_decode($rule->actions, true) ?: []; + + if ($this->evaluateConditions($conditions, $ticket)) + { + $this->executeActions($actions, $ticketId, $ticket); + } + } + } + catch (\Throwable $e) + { + \Joomla\CMS\Log\Log::add('Automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas'); + } + } + + /** + * Run all scheduled automation rules against all open tickets. + */ + public function runScheduledAutomation(): array + { + $db = $this->getDatabase(); + $results = ['evaluated' => 0, 'acted' => 0]; + + // Load scheduled rules + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokowaas_ticket_automation')) + ->where($db->quoteName('trigger_event') . ' = ' . $db->quote('scheduled')) + ->where($db->quoteName('enabled') . ' = 1') + ->order($db->quoteName('ordering') . ' ASC'); + $db->setQuery($query); + $rules = $db->loadObjectList() ?: []; + + if (empty($rules)) + { + return $results; + } + + // Load all non-closed tickets + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokowaas_tickets')) + ->where($db->quoteName('status') . ' != ' . $db->quote('closed')); + $db->setQuery($query); + $tickets = $db->loadObjectList() ?: []; + + foreach ($tickets as $ticket) + { + $ticket->age_hours = (time() - strtotime($ticket->created)) / 3600; + $ticket->replies = []; + $results['evaluated']++; + + foreach ($rules as $rule) + { + $conditions = json_decode($rule->conditions, true) ?: []; + $actions = json_decode($rule->actions, true) ?: []; + + if ($this->evaluateConditions($conditions, $ticket)) + { + $this->executeActions($actions, (int) $ticket->id, $ticket); + $results['acted']++; + } + } + } + + return $results; + } + + /** + * Evaluate a set of conditions against a ticket (all must match). + */ + private function evaluateConditions(array $conditions, object $ticket): bool + { + foreach ($conditions as $cond) + { + $field = $cond['field'] ?? ''; + $op = $cond['op'] ?? 'eq'; + $value = $cond['value'] ?? ''; + + $ticketValue = $ticket->{$field} ?? null; + + if ($ticketValue === null) + { + return false; + } + + switch ($op) + { + case 'eq': + if ((string) $ticketValue !== (string) $value) return false; + break; + case 'neq': + if ((string) $ticketValue === (string) $value) return false; + break; + case 'gt': + if ((float) $ticketValue <= (float) $value) return false; + break; + case 'lt': + if ((float) $ticketValue >= (float) $value) return false; + break; + case 'in': + $list = array_map('trim', explode(',', $value)); + if (!\in_array((string) $ticketValue, $list, true)) return false; + break; + case 'not_in': + $list = array_map('trim', explode(',', $value)); + if (\in_array((string) $ticketValue, $list, true)) return false; + break; + default: + return false; + } + } + + return true; + } + + /** + * Execute a set of actions on a ticket. + */ + private function executeActions(array $actions, int $ticketId, object $ticket): void + { + $db = $this->getDatabase(); + $now = Factory::getDate()->toSql(); + + foreach ($actions as $action) + { + $type = $action['type'] ?? ''; + $value = $action['value'] ?? ''; + + switch ($type) + { + case 'set_status': + $this->updateStatus($ticketId, $value); + break; + + case 'set_priority': + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokowaas_tickets')) + ->set($db->quoteName('priority') . ' = ' . $db->quote($value)) + ->set($db->quoteName('modified') . ' = ' . $db->quote($now)) + ->where($db->quoteName('id') . ' = ' . $ticketId) + )->execute(); + break; + + case 'assign': + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__mokowaas_tickets')) + ->set($db->quoteName('assigned_to') . ' = ' . (int) $value) + ->set($db->quoteName('modified') . ' = ' . $db->quote($now)) + ->where($db->quoteName('id') . ' = ' . $ticketId) + )->execute(); + break; + + case 'add_note': + $reply = (object) [ + 'ticket_id' => $ticketId, + 'user_id' => 0, + 'body' => $value, + 'is_internal' => 1, + 'created' => $now, + ]; + $db->insertObject('#__mokowaas_ticket_replies', $reply, 'id'); + break; + } + } + } + + /** + * Get all automation rules. + */ + public function getAutomationRules(): array + { + $db = $this->getDatabase(); + $db->setQuery( + $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokowaas_ticket_automation')) + ->order($db->quoteName('ordering') . ' ASC') + ); + + return $db->loadObjectList() ?: []; + } + // ================================================================== // Akeeba Ticket System Importer // ================================================================== diff --git a/src/packages/com_mokowaas/admin/src/View/Dashboard/HtmlView.php b/src/packages/com_mokowaas/admin/src/View/Dashboard/HtmlView.php index 8638da95..b8b2f0db 100644 --- a/src/packages/com_mokowaas/admin/src/View/Dashboard/HtmlView.php +++ b/src/packages/com_mokowaas/admin/src/View/Dashboard/HtmlView.php @@ -36,9 +36,17 @@ class HtmlView extends BaseHtmlView $this->wafBlocks = $model->getRecentWafBlocks(5); // Check for importable Akeeba data - $importModel = $this->getModel('Import'); - $this->adminToolsAvailable = $importModel->checkAdminToolsAvailable(); - $this->atsAvailable = $importModel->checkAtsAvailable(); + try + { + $importModel = new \Moko\Component\MokoWaaS\Administrator\Model\ImportModel(); + $this->adminToolsAvailable = $importModel->checkAdminToolsAvailable(); + $this->atsAvailable = $importModel->checkAtsAvailable(); + } + catch (\Throwable $e) + { + $this->adminToolsAvailable = null; + $this->atsAvailable = null; + } $this->addToolbar(); diff --git a/src/packages/plg_task_mokowaas_tickets/language/en-GB/plg_task_mokowaas_tickets.ini b/src/packages/plg_task_mokowaas_tickets/language/en-GB/plg_task_mokowaas_tickets.ini new file mode 100644 index 00000000..5b695de4 --- /dev/null +++ b/src/packages/plg_task_mokowaas_tickets/language/en-GB/plg_task_mokowaas_tickets.ini @@ -0,0 +1,4 @@ +PLG_TASK_MOKOWAAS_TICKETS="Task - MokoWaaS Ticket Automation" +PLG_TASK_MOKOWAAS_TICKETS_DESC="Runs scheduled helpdesk automation rules." +PLG_TASK_MOKOWAAS_TICKETS_AUTOMATION_TITLE="MokoWaaS: Ticket Automation" +PLG_TASK_MOKOWAAS_TICKETS_AUTOMATION_DESC="Runs time-based automation rules against open tickets (auto-close, SLA escalation, etc.)." diff --git a/src/packages/plg_task_mokowaas_tickets/language/en-GB/plg_task_mokowaas_tickets.sys.ini b/src/packages/plg_task_mokowaas_tickets/language/en-GB/plg_task_mokowaas_tickets.sys.ini new file mode 100644 index 00000000..c0dc6562 --- /dev/null +++ b/src/packages/plg_task_mokowaas_tickets/language/en-GB/plg_task_mokowaas_tickets.sys.ini @@ -0,0 +1,2 @@ +PLG_TASK_MOKOWAAS_TICKETS="Task - MokoWaaS Ticket Automation" +PLG_TASK_MOKOWAAS_TICKETS_DESC="Runs scheduled helpdesk automation rules — auto-close, SLA escalation, and time-based actions." diff --git a/src/packages/plg_task_mokowaas_tickets/mokowaas_tickets.xml b/src/packages/plg_task_mokowaas_tickets/mokowaas_tickets.xml new file mode 100644 index 00000000..c8e4801f --- /dev/null +++ b/src/packages/plg_task_mokowaas_tickets/mokowaas_tickets.xml @@ -0,0 +1,25 @@ + + + Task - MokoWaaS Ticket Automation + mokowaas_tickets + Moko Consulting + 2026-06-02 + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + hello@mokoconsulting.tech + https://mokoconsulting.tech + 02.32.00 + Runs scheduled helpdesk automation rules — auto-close resolved tickets, SLA breach escalation, and time-based actions. + Moko\Plugin\Task\MokoWaaSTickets + + + src + services + language + + + + en-GB/plg_task_mokowaas_tickets.ini + en-GB/plg_task_mokowaas_tickets.sys.ini + + diff --git a/src/packages/plg_task_mokowaas_tickets/services/provider.php b/src/packages/plg_task_mokowaas_tickets/services/provider.php new file mode 100644 index 00000000..e97c8c8e --- /dev/null +++ b/src/packages/plg_task_mokowaas_tickets/services/provider.php @@ -0,0 +1,27 @@ +set( + PluginInterface::class, + function (Container $container) { + $dispatcher = $container->get(DispatcherInterface::class); + $plugin = new TicketAutomation($dispatcher, (array) PluginHelper::getPlugin('task', 'mokowaas_tickets')); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/src/packages/plg_task_mokowaas_tickets/src/Extension/TicketAutomation.php b/src/packages/plg_task_mokowaas_tickets/src/Extension/TicketAutomation.php new file mode 100644 index 00000000..3daa7aec --- /dev/null +++ b/src/packages/plg_task_mokowaas_tickets/src/Extension/TicketAutomation.php @@ -0,0 +1,65 @@ + [ + 'langConstPrefix' => 'PLG_TASK_MOKOWAAS_TICKETS_AUTOMATION', + 'method' => 'runAutomation', + ], + ]; + + protected $autoloadLanguage = true; + + public static function getSubscribedEvents(): array + { + return [ + 'onTaskOptionsList' => 'advertiseRoutines', + 'onExecuteTask' => 'standardRoutineHandler', + 'onContentPrepareForm' => 'enhanceTaskItemForm', + ]; + } + + /** + * Run all scheduled automation rules against open tickets. + */ + private function runAutomation(ExecuteTaskEvent $event): int + { + try + { + $model = new TicketsModel(); + $results = $model->runScheduledAutomation(); + + $this->logTask( + \sprintf('Ticket automation: evaluated %d tickets, acted on %d', $results['evaluated'], $results['acted']) + ); + + return Status::OK; + } + catch (\Throwable $e) + { + $this->logTask('Ticket automation failed: ' . $e->getMessage(), 'error'); + + return Status::KNOCKOUT; + } + } +} diff --git a/src/pkg_mokowaas.xml b/src/pkg_mokowaas.xml index a5eb97a8..67ea2c6f 100644 --- a/src/pkg_mokowaas.xml +++ b/src/pkg_mokowaas.xml @@ -24,6 +24,7 @@ plg_webservices_perfectpublisher.zip plg_task_mokowaasdemo.zip plg_task_mokowaassync.zip + plg_task_mokowaas_tickets.zip tpl_mokoonyx.zip diff --git a/src/script.php b/src/script.php index 7afd3b8c..882bcba5 100644 --- a/src/script.php +++ b/src/script.php @@ -47,6 +47,7 @@ class Pkg_MokowaasInstallerScript $this->enablePlugin('webservices', 'mokowaas'); $this->enablePlugin('task', 'mokowaasdemo'); $this->enablePlugin('task', 'mokowaassync'); + $this->enablePlugin('task', 'mokowaas_tickets'); // Migrate params from core plugin to feature plugins (one-time) $this->migrateFeatureParams(); @@ -411,6 +412,7 @@ class Pkg_MokowaasInstallerScript $db->quote('mod_mokowaas_cpanel'), $db->quote('mokowaasdemo'), $db->quote('mokowaassync'), + $db->quote('mokowaas_tickets'), $db->quote('perfectpublisher'), $db->quote('mokoonyx'), ];