diff --git a/source/packages/com_mokosuiteclient/admin/src/Model/TicketsModel.php b/source/packages/com_mokosuiteclient/admin/src/Model/TicketsModel.php deleted file mode 100644 index 1d0c58fb..00000000 --- a/source/packages/com_mokosuiteclient/admin/src/Model/TicketsModel.php +++ /dev/null @@ -1,1448 +0,0 @@ -getDatabase(); - $query = $db->getQuery(true) - ->select([ - $db->quoteName('t.id'), - $db->quoteName('t.subject'), - $db->quoteName('t.status_id'), - $db->quoteName('t.priority_id'), - $db->quoteName('t.created'), - $db->quoteName('t.modified'), - $db->quoteName('t.contact_id'), - $db->quoteName('t.sla_response_due'), - $db->quoteName('t.sla_resolution_due'), - $db->quoteName('t.sla_responded'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('u.name', 'created_by_name'), - $db->quoteName('ct.name', 'contact_name'), - $db->quoteName('st.title', 'status_title'), - $db->quoteName('st.alias', 'status_alias'), - $db->quoteName('st.color', 'status_color'), - $db->quoteName('pr.title', 'priority_title'), - $db->quoteName('pr.alias', 'priority_alias'), - $db->quoteName('pr.color', 'priority_color'), - $db->quoteName('st.is_closed', 'status_is_closed'), - ]) - ->from($db->quoteName('#__mokosuiteclient_tickets', 't')) - ->leftJoin($db->quoteName('#__mokosuiteclient_ticket_categories', 'c') . ' ON c.id = t.category_id') - ->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by') - ->leftJoin($db->quoteName('#__contact_details', 'ct') . ' ON ct.id = t.contact_id') - ->leftJoin($db->quoteName('#__mokosuiteclient_ticket_statuses', 'st') . ' ON st.id = t.status_id') - ->leftJoin($db->quoteName('#__mokosuiteclient_ticket_priorities', 'pr') . ' ON pr.id = t.priority_id'); - - if (!empty($filters['status_id'])) - { - $query->where($db->quoteName('t.status_id') . ' = ' . (int) $filters['status_id']); - } - - if (!empty($filters['priority_id'])) - { - $query->where($db->quoteName('t.priority_id') . ' = ' . (int) $filters['priority_id']); - } - - if (!empty($filters['assigned_to'])) - { - $query->where($db->quoteName('t.assigned_to') . ' = ' . (int) $filters['assigned_to']); - } - - if (!empty($filters['category_id'])) - { - $query->where($db->quoteName('t.category_id') . ' = ' . (int) $filters['category_id']); - } - - if (!empty($filters['contact_id'])) - { - $query->where($db->quoteName('t.contact_id') . ' = ' . (int) $filters['contact_id']); - } - - $query->order($db->quoteName('t.created') . ' DESC'); - $query->setLimit(50); - - $db->setQuery($query); - $tickets = $db->loadObjectList() ?: []; - - // Load assignees for each ticket - foreach ($tickets as $ticket) - { - $ticket->assignees = $this->getTicketAssignees((int) $ticket->id); - } - - return $tickets; - } - - /** - * Get a single ticket with all replies. - */ - public function getTicket(int $id): ?object - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select([ - $db->quoteName('t') . '.*', - $db->quoteName('c.title', 'category_title'), - $db->quoteName('u.name', 'created_by_name'), - $db->quoteName('u.email', 'created_by_email'), - $db->quoteName('ct.name', 'contact_name'), - $db->quoteName('ct.email_to', 'contact_email'), - $db->quoteName('ct.telephone', 'contact_phone'), - $db->quoteName('st.title', 'status_title'), - $db->quoteName('st.alias', 'status_alias'), - $db->quoteName('st.color', 'status_color'), - $db->quoteName('st.is_closed', 'status_is_closed'), - $db->quoteName('pr.title', 'priority_title'), - $db->quoteName('pr.alias', 'priority_alias'), - $db->quoteName('pr.color', 'priority_color'), - ]) - ->from($db->quoteName('#__mokosuiteclient_tickets', 't')) - ->leftJoin($db->quoteName('#__mokosuiteclient_ticket_categories', 'c') . ' ON c.id = t.category_id') - ->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by') - ->leftJoin($db->quoteName('#__contact_details', 'ct') . ' ON ct.id = t.contact_id') - ->leftJoin($db->quoteName('#__mokosuiteclient_ticket_statuses', 'st') . ' ON st.id = t.status_id') - ->leftJoin($db->quoteName('#__mokosuiteclient_ticket_priorities', 'pr') . ' ON pr.id = t.priority_id') - ->where($db->quoteName('t.id') . ' = ' . $id); - $db->setQuery($query); - $ticket = $db->loadObject(); - - if (!$ticket) - { - return null; - } - - // Load replies - $query = $db->getQuery(true) - ->select([ - $db->quoteName('r') . '.*', - $db->quoteName('u.name', 'user_name'), - ]) - ->from($db->quoteName('#__mokosuiteclient_ticket_replies', 'r')) - ->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id') - ->where($db->quoteName('r.ticket_id') . ' = ' . $id) - ->order($db->quoteName('r.created') . ' ASC'); - $db->setQuery($query); - $ticket->replies = $db->loadObjectList() ?: []; - - // Reply count - $ticket->reply_count = \count($ticket->replies); - - // Load assignees (users + groups) - $ticket->assignees = $this->getTicketAssignees($id); - - return $ticket; - } - - /** - * Create a new ticket. - */ - public function createTicket(array $data): array - { - try - { - $db = $this->getDatabase(); - $user = Factory::getApplication()->getIdentity(); - $now = Factory::getDate()->toSql(); - - // Resolve default status/priority from lookup tables - $defaultStatus = $this->getDefaultStatus(); - $defaultPriority = $this->getDefaultPriority(); - - $ticket = (object) [ - 'subject' => $data['subject'] ?? '', - 'body' => $data['body'] ?? '', - 'status' => $defaultStatus->alias ?? 'open', - 'status_id' => (int) ($data['status_id'] ?? $defaultStatus->id ?? 1), - 'priority' => $defaultPriority->alias ?? 'normal', - 'priority_id' => (int) ($data['priority_id'] ?? $defaultPriority->id ?? 2), - 'category_id' => (int) ($data['category_id'] ?? 0) ?: null, - 'contact_id' => (int) ($data['contact_id'] ?? 0) ?: null, - 'created_by' => $user->id, - 'assigned_to' => (int) ($data['assigned_to'] ?? 0) ?: null, - 'created' => $now, - 'modified' => $now, - ]; - - // Auto-assign from category - if (!$ticket->assigned_to && $ticket->category_id) - { - $query = $db->getQuery(true) - ->select($db->quoteName('auto_assign_user')) - ->from($db->quoteName('#__mokosuiteclient_ticket_categories')) - ->where($db->quoteName('id') . ' = ' . (int) $ticket->category_id); - $db->setQuery($query); - $autoAssign = (int) $db->loadResult(); - - if ($autoAssign) - { - $ticket->assigned_to = $autoAssign; - } - } - - // SLA deadlines from category - if ($ticket->category_id) - { - $query = $db->getQuery(true) - ->select([$db->quoteName('sla_response_minutes'), $db->quoteName('sla_resolution_minutes')]) - ->from($db->quoteName('#__mokosuiteclient_ticket_categories')) - ->where($db->quoteName('id') . ' = ' . (int) $ticket->category_id); - $db->setQuery($query); - $sla = $db->loadObject(); - - if ($sla) - { - $ticket->sla_response_due = Factory::getDate($now)->modify('+' . (int) $sla->sla_response_minutes . ' minutes')->toSql(); - $ticket->sla_resolution_due = Factory::getDate($now)->modify('+' . (int) $sla->sla_resolution_minutes . ' minutes')->toSql(); - } - } - - $db->insertObject('#__mokosuiteclient_tickets', $ticket, 'id'); - - // Handle multi-assignee (users and groups) - $assignUsers = array_filter(array_map('intval', (array) ($data['assign_users'] ?? []))); - $assignGroups = array_filter(array_map('intval', (array) ($data['assign_groups'] ?? []))); - - // Backward compat: single assigned_to becomes a user assignee - if (empty($assignUsers) && $ticket->assigned_to) - { - $assignUsers = [$ticket->assigned_to]; - } - - if (!empty($assignUsers) || !empty($assignGroups)) - { - $this->setTicketAssignees((int) $ticket->id, $assignUsers, $assignGroups); - } - - // Save custom field values - $fieldValues = (array) ($data['custom_fields'] ?? []); - - if (!empty($fieldValues)) - { - $this->saveFieldValues((int) $ticket->id, $fieldValues); - } - - // Run automation + notifications - $this->runAutomation('ticket_created', (int) $ticket->id); - NotificationService::notify('ticket_created', $this->getTicket((int) $ticket->id)); - - return ['success' => true, 'message' => 'Ticket #' . $ticket->id . ' created.', 'id' => (int) $ticket->id]; - } - catch (\Throwable $e) - { - return ['success' => false, 'message' => 'Failed: ' . $e->getMessage()]; - } - } - - /** - * Add a reply to a ticket. - */ - public function addReply(int $ticketId, string $body, bool $isInternal = false): array - { - try - { - $db = $this->getDatabase(); - $user = Factory::getApplication()->getIdentity(); - $now = Factory::getDate()->toSql(); - - $reply = (object) [ - 'ticket_id' => $ticketId, - 'user_id' => $user->id, - 'body' => $body, - 'is_internal' => $isInternal ? 1 : 0, - 'created' => $now, - ]; - - $db->insertObject('#__mokosuiteclient_ticket_replies', $reply, 'id'); - - // Mark SLA as responded only for staff replies (not customer self-replies) - $ticket = $this->getTicket($ticketId); - $isStaffReply = $ticket && (int) $user->id !== (int) $ticket->created_by; - - $updateQuery = $db->getQuery(true) - ->update($db->quoteName('#__mokosuiteclient_tickets')) - ->set($db->quoteName('modified') . ' = ' . $db->quote($now)) - ->where($db->quoteName('id') . ' = ' . $ticketId); - - if ($isStaffReply) - { - $updateQuery->set($db->quoteName('sla_responded') . ' = 1') - ->where($db->quoteName('sla_responded') . ' = 0'); - } - - $db->setQuery($updateQuery)->execute(); - - // Run automation + notifications (skip internal notes) - $this->runAutomation('ticket_replied', $ticketId); - - if (!$isInternal) - { - NotificationService::notify('ticket_replied', $this->getTicket($ticketId), ['reply_body' => $body]); - } - - return ['success' => true, 'message' => 'Reply added.']; - } - catch (\Throwable $e) - { - return ['success' => false, 'message' => 'Failed: ' . $e->getMessage()]; - } - } - - /** @var bool Guard against automation recursion */ - private bool $automationRunning = false; - - /** - * Update ticket status by status ID (lookup table). - */ - public function updateStatus(int $ticketId, int $statusId): array - { - try - { - $db = $this->getDatabase(); - $now = Factory::getDate()->toSql(); - - // Validate status ID against lookup table - $db->setQuery( - $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuiteclient_ticket_statuses')) - ->where($db->quoteName('id') . ' = ' . $statusId) - ); - $status = $db->loadObject(); - - if (!$status) - { - return ['success' => false, 'message' => 'Invalid status.']; - } - - // Capture old status for notification - $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('status_id')) - ->from($db->quoteName('#__mokosuiteclient_tickets')) - ->where($db->quoteName('id') . ' = ' . $ticketId) - ); - $oldStatusId = (int) $db->loadResult(); - - $sets = [ - $db->quoteName('status') . ' = ' . $db->quote($status->alias), - $db->quoteName('status_id') . ' = ' . $statusId, - $db->quoteName('modified') . ' = ' . $db->quote($now), - ]; - - if ($status->is_closed) - { - $sets[] = $db->quoteName('closed') . ' = ' . $db->quote($now); - } - - // Set resolved timestamp for "resolved" alias (backward compat) - if ($status->alias === 'resolved') - { - $sets[] = $db->quoteName('resolved') . ' = ' . $db->quote($now); - } - - $db->setQuery( - $db->getQuery(true) - ->update($db->quoteName('#__mokosuiteclient_tickets')) - ->set($sets) - ->where($db->quoteName('id') . ' = ' . $ticketId) - )->execute(); - - // Run automation + notifications (with recursion guard) - if (!$this->automationRunning) - { - $this->automationRunning = true; - $this->runAutomation('status_changed', $ticketId); - NotificationService::notify('status_changed', $this->getTicket($ticketId), ['old_status_id' => $oldStatusId]); - $this->automationRunning = false; - } - - return ['success' => true, 'message' => 'Status updated to ' . $status->title . '.']; - } - catch (\Throwable $e) - { - $this->automationRunning = false; - - return ['success' => false, 'message' => 'Failed: ' . $e->getMessage()]; - } - } - - /** - * Get all ticket categories. - */ - public function getCategories(): array - { - $db = $this->getDatabase(); - $db->setQuery( - $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuiteclient_ticket_categories')) - ->where($db->quoteName('published') . ' = 1') - ->order($db->quoteName('ordering') . ' ASC') - ); - - return $db->loadObjectList() ?: []; - } - - /** - * Get assignees for a ticket (users and groups with resolved names). - */ - public function getTicketAssignees(int $ticketId): array - { - $db = $this->getDatabase(); - $db->setQuery( - $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuiteclient_ticket_assignees')) - ->where($db->quoteName('ticket_id') . ' = ' . $ticketId) - ); - $rows = $db->loadObjectList() ?: []; - - foreach ($rows as $row) - { - if ($row->assignee_type === 'user') - { - $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('id') . ' = ' . (int) $row->assignee_id) - ); - $row->name = (string) $db->loadResult() ?: 'User #' . $row->assignee_id; - } - else - { - $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('id') . ' = ' . (int) $row->assignee_id) - ); - $row->name = (string) $db->loadResult() ?: 'Group #' . $row->assignee_id; - } - } - - return $rows; - } - - /** - * Set assignees for a ticket (replaces existing assignments). - * - * @param int $ticketId Ticket ID - * @param array $users Array of user IDs - * @param array $groups Array of user group IDs - */ - public function setTicketAssignees(int $ticketId, array $users = [], array $groups = []): void - { - $db = $this->getDatabase(); - - // Clear existing - $db->setQuery( - $db->getQuery(true) - ->delete($db->quoteName('#__mokosuiteclient_ticket_assignees')) - ->where($db->quoteName('ticket_id') . ' = ' . $ticketId) - )->execute(); - - // Insert users - foreach ($users as $uid) - { - $uid = (int) $uid; - - if ($uid > 0) - { - $db->insertObject('#__mokosuiteclient_ticket_assignees', (object) [ - 'ticket_id' => $ticketId, - 'assignee_type' => 'user', - 'assignee_id' => $uid, - ]); - } - } - - // Insert groups - foreach ($groups as $gid) - { - $gid = (int) $gid; - - if ($gid > 0) - { - $db->insertObject('#__mokosuiteclient_ticket_assignees', (object) [ - 'ticket_id' => $ticketId, - 'assignee_type' => 'group', - 'assignee_id' => $gid, - ]); - } - } - } - - /** - * Get all published Joomla contact records for ticket linking. - */ - public function getContacts(): array - { - $db = $this->getDatabase(); - $db->setQuery( - $db->getQuery(true) - ->select([$db->quoteName('id'), $db->quoteName('name')]) - ->from($db->quoteName('#__contact_details')) - ->where($db->quoteName('published') . ' = 1') - ->order($db->quoteName('name') . ' ASC') - ); - - return $db->loadObjectList() ?: []; - } - - /** - * Get the default ticket status. - */ - public function getDefaultStatus(): ?object - { - $db = $this->getDatabase(); - $db->setQuery( - $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuiteclient_ticket_statuses')) - ->where($db->quoteName('is_default') . ' = 1') - ->setLimit(1) - ); - - return $db->loadObject() ?: null; - } - - /** - * Get the default ticket priority. - */ - public function getDefaultPriority(): ?object - { - $db = $this->getDatabase(); - $db->setQuery( - $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuiteclient_ticket_priorities')) - ->where($db->quoteName('is_default') . ' = 1') - ->setLimit(1) - ); - - return $db->loadObject() ?: null; - } - - /** - * Get all ticket statuses. - */ - public function getStatuses(): array - { - $db = $this->getDatabase(); - $db->setQuery( - $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuiteclient_ticket_statuses')) - ->order($db->quoteName('ordering') . ' ASC') - ); - - return $db->loadObjectList() ?: []; - } - - /** - * Get all ticket priorities. - */ - public function getPriorities(): array - { - $db = $this->getDatabase(); - $db->setQuery( - $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuiteclient_ticket_priorities')) - ->order($db->quoteName('ordering') . ' ASC') - ); - - return $db->loadObjectList() ?: []; - } - - /** - * Get backend users for assignee selection. - */ - public function getBackendUsers(): array - { - $db = $this->getDatabase(); - $db->setQuery( - $db->getQuery(true) - ->select(['u.id', 'u.name', 'u.username']) - ->from($db->quoteName('#__users', 'u')) - ->where($db->quoteName('u.block') . ' = 0') - ->order($db->quoteName('u.name') . ' ASC') - ); - - return $db->loadObjectList() ?: []; - } - - /** - * Get Joomla user groups for assignee selection. - */ - public function getUserGroups(): array - { - $db = $this->getDatabase(); - $db->setQuery( - $db->getQuery(true) - ->select(['id', 'title']) - ->from($db->quoteName('#__usergroups')) - ->order($db->quoteName('title') . ' ASC') - ); - - return $db->loadObjectList() ?: []; - } - - /** - * Get Joomla custom field groups assigned to a ticket category. - */ - public function getFieldGroupsForCategory(int $categoryId): array - { - $db = $this->getDatabase(); - $db->setQuery( - $db->getQuery(true) - ->select([$db->quoteName('fg.id'), $db->quoteName('fg.title')]) - ->from($db->quoteName('#__mokosuiteclient_ticket_category_field_groups', 'cfg')) - ->innerJoin($db->quoteName('#__fields_groups', 'fg') . ' ON fg.id = cfg.field_group_id') - ->where($db->quoteName('cfg.category_id') . ' = ' . $categoryId) - ->where($db->quoteName('fg.state') . ' = 1') - ->order($db->quoteName('fg.ordering') . ' ASC') - ); - - return $db->loadObjectList() ?: []; - } - - /** - * Get Joomla custom fields for given field group IDs (context: com_mokosuiteclient.ticket). - */ - public function getFieldsForGroups(array $groupIds): array - { - if (empty($groupIds)) - { - return []; - } - - $db = $this->getDatabase(); - $ids = implode(',', array_map('intval', $groupIds)); - $db->setQuery( - $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__fields')) - ->where($db->quoteName('context') . ' = ' . $db->quote('com_mokosuiteclient.ticket')) - ->where($db->quoteName('group_id') . ' IN (' . $ids . ')') - ->where($db->quoteName('state') . ' = 1') - ->order($db->quoteName('ordering') . ' ASC') - ); - - return $db->loadObjectList() ?: []; - } - - /** - * Get custom field values for a ticket. - */ - public function getFieldValues(int $ticketId): array - { - $db = $this->getDatabase(); - $db->setQuery( - $db->getQuery(true) - ->select([$db->quoteName('field_id'), $db->quoteName('value')]) - ->from($db->quoteName('#__fields_values')) - ->where($db->quoteName('item_id') . ' = ' . $db->quote((string) $ticketId)) - ); - $rows = $db->loadObjectList() ?: []; - - $values = []; - - foreach ($rows as $row) - { - $values[(int) $row->field_id] = $row->value; - } - - return $values; - } - - /** - * Save custom field values for a ticket. - */ - public function saveFieldValues(int $ticketId, array $fieldValues): void - { - $db = $this->getDatabase(); - - foreach ($fieldValues as $fieldId => $value) - { - $fieldId = (int) $fieldId; - - // Delete existing - $db->setQuery( - $db->getQuery(true) - ->delete($db->quoteName('#__fields_values')) - ->where($db->quoteName('field_id') . ' = ' . $fieldId) - ->where($db->quoteName('item_id') . ' = ' . $db->quote((string) $ticketId)) - )->execute(); - - // Insert new value (skip empty) - if ($value !== '' && $value !== null) - { - $db->insertObject('#__fields_values', (object) [ - 'field_id' => $fieldId, - 'item_id' => (string) $ticketId, - 'value' => $value, - ]); - } - } - } - - /** - * Get canned responses, optionally filtered by category. - */ - public function getCannedResponses(int $categoryId = 0): array - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuiteclient_ticket_canned')) - ->order($db->quoteName('ordering') . ' ASC'); - - if ($categoryId) - { - $query->where('(' . $db->quoteName('category_id') . ' = ' . $categoryId - . ' OR ' . $db->quoteName('category_id') . ' IS NULL)'); - } - - $db->setQuery($query); - - return $db->loadObjectList() ?: []; - } - - /** - * Get ticket counts by status for dashboard. - */ - public function getStatusCounts(): array - { - $db = $this->getDatabase(); - $db->setQuery( - $db->getQuery(true) - ->select([ - $db->quoteName('s.id'), - $db->quoteName('s.title'), - $db->quoteName('s.alias'), - $db->quoteName('s.color'), - $db->quoteName('s.is_closed'), - 'COUNT(' . $db->quoteName('t.id') . ') AS ' . $db->quoteName('cnt'), - ]) - ->from($db->quoteName('#__mokosuiteclient_ticket_statuses', 's')) - ->leftJoin($db->quoteName('#__mokosuiteclient_tickets', 't') . ' ON t.status_id = s.id') - ->group($db->quoteName('s.id')) - ->order($db->quoteName('s.ordering') . ' ASC') - ); - - return $db->loadObjectList() ?: []; - } - - /** - * Get overdue tickets (SLA breached). - */ - public function getOverdueTickets(): array - { - $db = $this->getDatabase(); - $now = Factory::getDate()->toSql(); - - $query = $db->getQuery(true) - ->select(['t.' . $db->quoteName('id'), $db->quoteName('t.subject'), $db->quoteName('t.priority'), - $db->quoteName('t.sla_response_due'), $db->quoteName('t.sla_resolution_due'), $db->quoteName('t.sla_responded')]) - ->from($db->quoteName('#__mokosuiteclient_tickets', 't')) - ->leftJoin($db->quoteName('#__mokosuiteclient_ticket_statuses', 's') . ' ON s.id = t.status_id') - ->where('(' . $db->quoteName('s.is_closed') . ' = 0 OR ' . $db->quoteName('s.is_closed') . ' IS NULL)') - ->where('((' . $db->quoteName('sla_response_due') . ' < ' . $db->quote($now) . ' AND ' . $db->quoteName('sla_responded') . ' = 0)' - . ' OR ' . $db->quoteName('sla_resolution_due') . ' < ' . $db->quote($now) . ')') - ->order($db->quoteName('sla_resolution_due') . ' ASC'); - $db->setQuery($query); - - 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('#__mokosuiteclient_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, 'mokosuiteclient'); - } - } - - /** - * 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('#__mokosuiteclient_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('#__mokosuiteclient_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('#__mokosuiteclient_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('#__mokosuiteclient_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('#__mokosuiteclient_ticket_replies', $reply, 'id'); - break; - - case 'send_email': - // value = email address or comma-separated list - $emails = array_filter(array_map('trim', explode(',', $value))); - - foreach ($emails as $email) - { - try - { - $mailer = Factory::getMailer(); - $mailer->addRecipient($email); - $mailer->setSubject('[Ticket #' . $ticketId . '] Automation Alert'); - $mailer->setBody('Automation rule triggered for ticket #' . $ticketId . ': ' . ($ticket->subject ?? '')); - $mailer->isHtml(false); - $mailer->Send(); - } - catch (\Throwable $e) - { - \Joomla\CMS\Log\Log::add('Automation email failed: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokosuiteclient'); - } - } - break; - - case 'create_ticket': - // value = JSON: {"subject":"...","body":"...","category_id":1,"priority":"normal","behavior":"append"} - $ticketData = json_decode($value, true) ?: []; - $behavior = $ticketData['behavior'] ?? 'append'; - $userId = (int) ($ticket->created_by ?? 0); - $catId = (int) ($ticketData['category_id'] ?? 0); - - if ($behavior === 'append' && $userId > 0) - { - // Check for existing open ticket from this user in this category - $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__mokosuiteclient_tickets')) - ->where($db->quoteName('created_by') . ' = ' . $userId) - ->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')') - ->where($catId ? $db->quoteName('category_id') . ' = ' . $catId : '1=1') - ->order($db->quoteName('created') . ' DESC') - ->setLimit(1) - ); - $existingId = (int) $db->loadResult(); - - if ($existingId) - { - $this->addReply($existingId, $ticketData['body'] ?? 'Automation event', true); - break; - } - } - elseif ($behavior === 'skip_if_open' && $userId > 0) - { - $db->setQuery( - $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__mokosuiteclient_tickets')) - ->where($db->quoteName('created_by') . ' = ' . $userId) - ->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')') - ); - - if ((int) $db->loadResult() > 0) - { - break; - } - } - - // Create new ticket - $this->createTicket([ - 'subject' => $ticketData['subject'] ?? 'Automation: ' . ($ticket->subject ?? 'System event'), - 'body' => $ticketData['body'] ?? '', - 'priority' => $ticketData['priority'] ?? 'normal', - 'category_id' => $catId, - ]); - break; - } - } - } - - /** - * Run automation for a system event (not tied to a specific ticket). - * Creates a virtual ticket context from event data. - */ - public function runSystemEventAutomation(string $event, array $eventData = []): void - { - try - { - $db = $this->getDatabase(); - - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuiteclient_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; - } - - // Build a virtual ticket-like object from event data - $context = (object) array_merge([ - 'id' => 0, - 'subject' => $eventData['subject'] ?? $event, - 'body' => $eventData['body'] ?? '', - 'status' => 'open', - 'priority' => $eventData['priority'] ?? 'normal', - 'created_by' => $eventData['user_id'] ?? 0, - 'created' => gmdate('Y-m-d H:i:s'), - 'age_hours' => 0, - ], $eventData); - - foreach ($rules as $rule) - { - $conditions = json_decode($rule->conditions, true) ?: []; - $actions = json_decode($rule->actions, true) ?: []; - - if (empty($conditions) || $this->evaluateConditions($conditions, $context)) - { - $this->executeActions($actions, 0, $context); - } - } - } - catch (\Throwable $e) - { - \Joomla\CMS\Log\Log::add('System event automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokosuiteclient'); - } - } - - /** - * Get all automation rules. - */ - public function getAutomationRules(): array - { - $db = $this->getDatabase(); - $db->setQuery( - $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuiteclient_ticket_automation')) - ->order($db->quoteName('ordering') . ' ASC') - ); - - 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('#__mokosuiteclient_ticket_statuses', $obj, 'id'); - - return ['status' => 'ok', 'id' => (int) $obj->id, 'message' => 'Status created']; - } - - $db->updateObject('#__mokosuiteclient_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('#__mokosuiteclient_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('#__mokosuiteclient_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('#__mokosuiteclient_ticket_priorities', $obj, 'id'); - - return ['status' => 'ok', 'id' => (int) $obj->id, 'message' => 'Priority created']; - } - - $db->updateObject('#__mokosuiteclient_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('#__mokosuiteclient_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('#__mokosuiteclient_ticket_priorities')) - ->where($db->quoteName('id') . ' = ' . $id) - )->execute(); - - return ['status' => 'ok', 'message' => 'Priority deleted']; - } - - // ================================================================== - // Akeeba Ticket System Importer - // ================================================================== - - /** - * Check if ATS tables exist and return counts. - */ - public function checkAtsAvailable(): ?object - { - $db = $this->getDatabase(); - - try - { - $db->setQuery('SELECT COUNT(*) FROM #__ats_tickets'); - $tickets = (int) $db->loadResult(); - - $db->setQuery('SELECT COUNT(*) FROM #__ats_posts'); - $posts = (int) $db->loadResult(); - - $db->setQuery('SELECT COUNT(*) FROM #__ats_cannedreplies'); - $canned = (int) $db->loadResult(); - - return (object) ['tickets' => $tickets, 'posts' => $posts, 'canned' => $canned]; - } - catch (\Throwable $e) - { - return null; - } - } - - /** - * Import tickets, replies, and canned responses from Akeeba Ticket System. - */ - public function importFromAts(): array - { - $db = $this->getDatabase(); - $results = ['tickets' => 0, 'replies' => 0, 'canned' => 0, 'errors' => []]; - - try - { - // Status mapping: ATS → MokoSuiteClient - $statusMap = [ - 'O' => 'open', // Open - 'P' => 'in_progress', // Pending (staff action needed) - 'C' => 'closed', // Closed - ]; - // Numeric statuses 1-99 are custom — map to open - for ($i = 1; $i <= 99; $i++) - { - $statusMap[(string) $i] = 'open'; - } - - // Priority mapping: ATS uses 1-5, we use enum - $priorityMap = [ - 1 => 'low', - 2 => 'low', - 3 => 'normal', - 4 => 'high', - 5 => 'urgent', - ]; - - // Category mapping: ATS uses Joomla categories, map catid to our category - // Default all to General Support (1) — admin can reassign later - $defaultCategory = 1; - - // Import canned replies first - $db->setQuery('SELECT * FROM #__ats_cannedreplies WHERE enabled = 1 ORDER BY ordering'); - $atsCanned = $db->loadObjectList() ?: []; - - foreach ($atsCanned as $c) - { - $exists = $db->setQuery( - $db->getQuery(true) - ->select('COUNT(*)') - ->from('#__mokosuiteclient_ticket_canned') - ->where($db->quoteName('title') . ' = ' . $db->quote($c->title)) - )->loadResult(); - - if ((int) $exists > 0) - { - continue; - } - - $row = (object) [ - 'title' => $c->title, - 'body' => strip_tags($c->reply ?? ''), - 'category_id' => null, - 'ordering' => (int) ($c->ordering ?? 0), - ]; - $db->insertObject('#__mokosuiteclient_ticket_canned', $row, 'id'); - $results['canned']++; - } - - // Import tickets - $db->setQuery('SELECT * FROM #__ats_tickets ORDER BY id'); - $atsTickets = $db->loadObjectList() ?: []; - - $ticketIdMap = []; // ATS id → MokoSuiteClient id - - foreach ($atsTickets as $t) - { - // Skip if already imported (check by subject + created_by + created) - $exists = $db->setQuery( - $db->getQuery(true) - ->select('COUNT(*)') - ->from('#__mokosuiteclient_tickets') - ->where($db->quoteName('subject') . ' = ' . $db->quote($t->title)) - ->where($db->quoteName('created_by') . ' = ' . (int) $t->created_by) - )->loadResult(); - - if ((int) $exists > 0) - { - continue; - } - - $status = $statusMap[$t->status] ?? 'open'; - $priority = $priorityMap[(int) $t->priority] ?? 'normal'; - - $row = (object) [ - 'subject' => $t->title, - 'body' => '', - 'status' => $status, - 'priority' => $priority, - 'category_id' => $defaultCategory, - 'created_by' => (int) $t->created_by, - 'assigned_to' => (int) $t->assigned_to ?: null, - 'created' => $t->created ?: Factory::getDate()->toSql(), - 'modified' => $t->modified, - 'resolved' => $status === 'closed' ? ($t->modified ?: $t->created) : null, - 'closed' => $status === 'closed' ? ($t->modified ?: $t->created) : null, - 'sla_responded' => 1, - ]; - - $db->insertObject('#__mokosuiteclient_tickets', $row, 'id'); - $ticketIdMap[(int) $t->id] = (int) $row->id; - $results['tickets']++; - } - - // Import posts (replies) - $db->setQuery('SELECT * FROM #__ats_posts ORDER BY id'); - $atsPosts = $db->loadObjectList() ?: []; - - foreach ($atsPosts as $p) - { - $newTicketId = $ticketIdMap[(int) $p->ticket_id] ?? null; - - if (!$newTicketId) - { - continue; - } - - // First post of a ticket is usually the ticket body — update the ticket - if (empty($results['first_post_' . $p->ticket_id])) - { - $results['first_post_' . $p->ticket_id] = true; - $body = strip_tags($p->content_html ?? ''); - $db->setQuery( - $db->getQuery(true) - ->update('#__mokosuiteclient_tickets') - ->set($db->quoteName('body') . ' = ' . $db->quote($body)) - ->where($db->quoteName('id') . ' = ' . $newTicketId) - )->execute(); - - continue; - } - - $row = (object) [ - 'ticket_id' => $newTicketId, - 'user_id' => (int) $p->created_by, - 'body' => strip_tags($p->content_html ?? ''), - 'is_internal' => 0, - 'created' => $p->created ?: Factory::getDate()->toSql(), - ]; - - $db->insertObject('#__mokosuiteclient_ticket_replies', $row, 'id'); - $results['replies']++; - } - - // Clean up temp tracking keys - foreach (array_keys($results) as $k) - { - if (str_starts_with($k, 'first_post_')) - { - unset($results[$k]); - } - } - - return [ - 'success' => true, - 'message' => sprintf( - 'Imported %d tickets, %d replies, %d canned responses from Akeeba Ticket System.', - $results['tickets'], $results['replies'], $results['canned'] - ), - 'counts' => $results, - ]; - } - catch (\Throwable $e) - { - return ['success' => false, 'message' => 'Import failed: ' . $e->getMessage()]; - } - } -} diff --git a/source/packages/com_mokosuiteclient/admin/src/View/Ticket/HtmlView.php b/source/packages/com_mokosuiteclient/admin/src/View/Ticket/HtmlView.php deleted file mode 100644 index 6e209ffe..00000000 --- a/source/packages/com_mokosuiteclient/admin/src/View/Ticket/HtmlView.php +++ /dev/null @@ -1,72 +0,0 @@ -getModel('Tickets'); - $id = Factory::getApplication()->getInput()->getInt('id', 0); - - $this->ticket = $model->getTicket($id); - $this->cannedResponses = $model->getCannedResponses((int) ($this->ticket->category_id ?? 0)); - $this->statuses = $model->getStatuses(); - $this->priorities = $model->getPriorities(); - - // Load custom fields for this ticket's category - if ($this->ticket && $this->ticket->category_id) - { - $groups = $model->getFieldGroupsForCategory((int) $this->ticket->category_id); - $groupIds = array_column($groups, 'id'); - $this->customFields = $model->getFieldsForGroups($groupIds); - $this->fieldValues = $model->getFieldValues($id); - } - - // Load attachments - $this->attachments = \Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService::getForTicket($id); - - if (!$this->ticket) - { - Factory::getApplication()->enqueueMessage('Ticket not found.', 'error'); - Factory::getApplication()->redirect('index.php?option=com_mokosuiteclient&view=tickets'); - - return; - } - - $this->addToolbar(); - - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - $wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css'); - - parent::display($tpl); - } - - protected function addToolbar(): void - { - $title = $this->ticket ? 'Ticket #' . $this->ticket->id . ' — ' . $this->ticket->subject : 'Ticket'; - ToolbarHelper::title($title, 'headphones'); - ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient&view=tickets'); - } -} diff --git a/source/packages/com_mokosuiteclient/admin/src/View/Tickets/HtmlView.php b/source/packages/com_mokosuiteclient/admin/src/View/Tickets/HtmlView.php deleted file mode 100644 index 16c65e31..00000000 --- a/source/packages/com_mokosuiteclient/admin/src/View/Tickets/HtmlView.php +++ /dev/null @@ -1,67 +0,0 @@ -getModel(); - $app = Factory::getApplication(); - - $filters = [ - 'status_id' => $app->getInput()->getInt('filter_status', 0), - 'priority_id' => $app->getInput()->getInt('filter_priority', 0), - 'category_id' => $app->getInput()->getInt('filter_category', 0), - 'contact_id' => $app->getInput()->getInt('filter_contact', 0), - ]; - - $this->tickets = $model->getTickets($filters); - $this->categories = $model->getCategories(); - $this->statuses = $model->getStatuses(); - $this->priorities = $model->getPriorities(); - $this->statusCounts = $model->getStatusCounts(); - $this->overdue = $model->getOverdueTickets(); - $this->atsAvailable = $model->checkAtsAvailable(); - $this->contacts = $model->getContacts(); - $this->backendUsers = $model->getBackendUsers(); - $this->userGroups = $model->getUserGroups(); - - $this->addToolbar(); - - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - $wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css'); - - parent::display($tpl); - } - - protected function addToolbar(): void - { - ToolbarHelper::title(Text::_('COM_MOKOSUITECLIENT_TICKETS_TITLE'), 'headphones'); - ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient'); - } -} diff --git a/source/packages/com_mokosuiteclient/admin/src/View/Ticketsettings/HtmlView.php b/source/packages/com_mokosuiteclient/admin/src/View/Ticketsettings/HtmlView.php deleted file mode 100644 index daac6e96..00000000 --- a/source/packages/com_mokosuiteclient/admin/src/View/Ticketsettings/HtmlView.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later - * - * @package MokoSuiteClient - * @subpackage Component - */ - -namespace Moko\Component\MokoSuiteClient\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_MOKOSUITECLIENT_TICKET_SETTINGS'), 'cog'); - ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient&view=tickets'); - } -} diff --git a/source/packages/com_mokosuiteclient/admin/src/View/Ticketsettings/index.html b/source/packages/com_mokosuiteclient/admin/src/View/Ticketsettings/index.html deleted file mode 100644 index 94906bce..00000000 --- a/source/packages/com_mokosuiteclient/admin/src/View/Ticketsettings/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/source/packages/com_mokosuiteclient/admin/tmpl/ticket/default.php b/source/packages/com_mokosuiteclient/admin/tmpl/ticket/default.php deleted file mode 100644 index f1904907..00000000 --- a/source/packages/com_mokosuiteclient/admin/tmpl/ticket/default.php +++ /dev/null @@ -1,365 +0,0 @@ -ticket; -$canned = $this->cannedResponses; -$token = Session::getFormToken(); -$attachments = $this->attachments; -$downloadUrl = Route::_('index.php?option=com_mokosuiteclient&task=display.downloadAttachment'); -$uploadUrl = Route::_('index.php?option=com_mokosuiteclient&task=display.uploadAttachment&format=json'); -$deleteAttUrl = Route::_('index.php?option=com_mokosuiteclient&task=display.deleteAttachment&format=json'); - -// Group attachments by reply_id (null = ticket-level) -$attByReply = []; -foreach ($attachments as $att) { - $key = $att->reply_id ?? 0; - $attByReply[$key][] = $att; -} - -$statuses = $this->statuses ?? []; -$priorities = $this->priorities ?? []; -?> - -
- -
- -
-
-
- escape($t->created_by_name); ?> - created, Text::_('DATE_FORMAT_LC2')); ?> -
- Original -
-
- escape($t->body)); ?> - -
- - -
-
- - - replies as $reply): ?> -
-
-
- escape($reply->user_name ?? 'System'); ?> - created, Text::_('DATE_FORMAT_LC2')); ?> -
- is_internal): ?> - Internal Note - -
-
- escape($reply->body)); ?> - id])): ?> -
-
- Attachments: - id] as $att): ?> - - escape($att->filename); ?> - (filesize); ?>) - - -
- -
-
- - - -
-
Reply
-
- -
- -
- - -
- -
-
- - -
-
-
-
- - -
-
-
Details
-
- - - - - - - contact_id): ?> - - - - resolved): ?> - closed): ?> - -
Statusescape($t->status_title ?? $t->status); ?>
Priorityescape($t->priority_title ?? $t->priority); ?>
Categoryescape($t->category_title ?? '—'); ?>
Created Byescape($t->created_by_name); ?>
escape($t->created_by_email ?? ''); ?>
Assigned Toassignees)) { - foreach ($t->assignees as $a) { - $icon = $a->assignee_type === 'group' ? ' ' : ' '; - echo '
' . $icon . $this->escape($a->name) . '
'; - } - } else { - echo 'Unassigned'; - } - ?>
Contact - - escape($t->contact_name ?? 'Contact #' . $t->contact_id); ?> - - contact_email)): ?>
escape($t->contact_email); ?> - contact_phone)): ?>
escape($t->contact_phone); ?> -
Createdcreated, Text::_('DATE_FORMAT_LC2')); ?>
Resolvedresolved, Text::_('DATE_FORMAT_LC2')); ?>
Closedclosed, Text::_('DATE_FORMAT_LC2')); ?>
Repliesreply_count; ?>
-
-
- - - sla_response_due || $t->sla_resolution_due): ?> -
-
SLA
-
- sla_response_due): ?> -
- Response Due
- sla_responded && strtotime($t->sla_response_due) < time(); - ?> - - sla_responded ? 'Responded' : HTMLHelper::_('date', $t->sla_response_due, Text::_('DATE_FORMAT_LC4')); ?> - - -
- - sla_resolution_due): ?> -
- Resolution Due
- status_is_closed) && strtotime($t->sla_resolution_due) < time(); - ?> - - status_is_closed) ? 'Met' : HTMLHelper::_('date', $t->sla_resolution_due, Text::_('DATE_FORMAT_LC4')); ?> - - -
- -
-
- - - - status, ['resolved', 'closed'], true); - $hasRating = !empty($t->satisfaction_rating); - ?> - -
-
Satisfaction
-
-
- - - -
-
satisfaction_rating; ?>/5
- satisfaction_feedback)): ?> -

escape($t->satisfaction_feedback); ?>

- -
-
- -
-
Rate this Support
-
-
- - - -
- - -
-
- - - -
-
Actions
-
- - id !== (int) $t->status_id): ?> - - - -
-
- - - customFields)): ?> -
-
Custom Fields
-
- - customFields as $field): ?> - - - - - -
escape($field->title); ?>escape($this->fieldValues[(int) $field->id] ?? '—'); ?>
-
-
- -
-
- - diff --git a/source/packages/com_mokosuiteclient/admin/tmpl/tickets/default.php b/source/packages/com_mokosuiteclient/admin/tmpl/tickets/default.php deleted file mode 100644 index 83808651..00000000 --- a/source/packages/com_mokosuiteclient/admin/tmpl/tickets/default.php +++ /dev/null @@ -1,317 +0,0 @@ -tickets; -$categories = $this->categories; -$statuses = $this->statuses; -$priorities = $this->priorities; -$counts = $this->statusCounts; -$overdue = $this->overdue; -$atsAvailable = $this->atsAvailable; -$token = Session::getFormToken(); -?> - -
- -
- -
cnt; ?>escape($sc->title); ?>
- - 0): ?> -
SLA Overdue
- -
- - -
-
- - - - -
-
- - - - -
-
- - -
-
- - - - - - - - - - - - - - - - - - - - - sla_response_due && !$t->sla_responded && strtotime($t->sla_response_due) < $now) $slaClass = 'table-danger'; - elseif ($t->sla_resolution_due && strtotime($t->sla_resolution_due) < $now && empty($t->status_is_closed)) $slaClass = 'table-danger'; - elseif ($t->sla_response_due && !$t->sla_responded && strtotime($t->sla_response_due) < $now + 3600) $slaClass = 'table-warning'; - ?> - - - - - - - - - - - - - - - -
#SubjectStatusPriorityCategoryContactCreated ByAssigned ToCreatedSLA
No tickets found.
id; ?>escape(mb_substr($t->subject, 0, 60)); ?>escape($t->status_title ?? $t->status); ?>escape($t->priority_title ?? $t->priority); ?>escape($t->category_title ?? '—'); ?>contact_name ? '' . $this->escape($t->contact_name) . '' : '—'; ?>escape($t->created_by_name ?? ''); ?>assignees)) { - $names = []; - foreach ($t->assignees as $a) { - $icon = $a->assignee_type === 'group' ? ' ' : ''; - $names[] = $icon . $this->escape($a->name); - } - echo implode(', ', $names); - } else { - echo 'Unassigned'; - } - ?>created, Text::_('DATE_FORMAT_LC4')); ?> - sla_response_due && !$t->sla_responded): ?> - sla_response_due, Text::_('DATE_FORMAT_LC4')); ?> - sla_resolution_due): ?> - sla_resolution_due, Text::_('DATE_FORMAT_LC4')); ?> - -
-
-
-
- - - - - diff --git a/source/packages/com_mokosuiteclient/admin/tmpl/ticketsettings/default.php b/source/packages/com_mokosuiteclient/admin/tmpl/ticketsettings/default.php deleted file mode 100644 index e75f6d09..00000000 --- a/source/packages/com_mokosuiteclient/admin/tmpl/ticketsettings/default.php +++ /dev/null @@ -1,203 +0,0 @@ - - * - * SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later - * - * @package MokoSuiteClient - * @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_mokosuiteclient/admin/tmpl/ticketsettings/index.html b/source/packages/com_mokosuiteclient/admin/tmpl/ticketsettings/index.html deleted file mode 100644 index 94906bce..00000000 --- a/source/packages/com_mokosuiteclient/admin/tmpl/ticketsettings/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/source/packages/mod_mokosuiteclient_menu/tmpl/default.php b/source/packages/mod_mokosuiteclient_menu/tmpl/default.php index d42ec973..35cb9322 100644 --- a/source/packages/mod_mokosuiteclient_menu/tmpl/default.php +++ b/source/packages/mod_mokosuiteclient_menu/tmpl/default.php @@ -20,7 +20,6 @@ $currentView = $app->getInput()->get('view', ''); // ── Static views for com_mokosuiteclient ────────────────────────────────── $mokosuiteclientStaticViews = [ ['icon' => 'icon-cogs', 'title' => 'Dashboard', 'link' => 'index.php?option=com_mokosuiteclient'], - ['icon' => 'fa-solid fa-handshake-angle', 'title' => 'Helpdesk', 'link' => 'index.php?option=com_mokosuiteclient&view=tickets'], ['icon' => 'icon-puzzle-piece', 'title' => 'Extensions', 'link' => 'index.php?option=com_mokosuiteclient&view=extensions'], ['icon' => 'fa-solid fa-file-code', 'title' => '.htaccess Maker', 'link' => 'index.php?option=com_mokosuiteclient&view=htaccess'], ['icon' => 'icon-lock', 'title' => 'Privacy Guard', 'link' => 'index.php?option=com_mokosuiteclient&view=privacy'], diff --git a/source/packages/plg_task_mokosuiteclient_tickets/language/en-GB/plg_task_mokosuiteclient_tickets.ini b/source/packages/plg_task_mokosuiteclient_tickets/language/en-GB/plg_task_mokosuiteclient_tickets.ini deleted file mode 100644 index f796d882..00000000 --- a/source/packages/plg_task_mokosuiteclient_tickets/language/en-GB/plg_task_mokosuiteclient_tickets.ini +++ /dev/null @@ -1,8 +0,0 @@ -PLG_TASK_MOKOSUITECLIENT_TICKETS="Task - MokoSuiteClient Ticket Automation" -PLG_TASK_MOKOSUITECLIENT_TICKETS_DESC="Runs scheduled helpdesk automation rules." -PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOMATION_TITLE="MokoSuiteClient: Ticket Automation" -PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOMATION_DESC="Runs time-based automation rules against open tickets (auto-close, SLA escalation, etc.)." -PLG_TASK_MOKOSUITECLIENT_TICKETS_IMAP_POLL_TITLE="MokoSuiteClient: IMAP Email Polling" -PLG_TASK_MOKOSUITECLIENT_TICKETS_IMAP_POLL_DESC="Polls an IMAP inbox for new emails and creates tickets or replies from unread messages." -PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOCLOSE_TITLE="MokoSuiteClient: Auto-Close Resolved Tickets" -PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOCLOSE_DESC="Automatically closes tickets that have been in resolved status longer than the configured number of days." diff --git a/source/packages/plg_task_mokosuiteclient_tickets/language/en-GB/plg_task_mokosuiteclient_tickets.sys.ini b/source/packages/plg_task_mokosuiteclient_tickets/language/en-GB/plg_task_mokosuiteclient_tickets.sys.ini deleted file mode 100644 index 8077fabe..00000000 --- a/source/packages/plg_task_mokosuiteclient_tickets/language/en-GB/plg_task_mokosuiteclient_tickets.sys.ini +++ /dev/null @@ -1,2 +0,0 @@ -PLG_TASK_MOKOSUITECLIENT_TICKETS="Task - MokoSuiteClient Ticket Automation" -PLG_TASK_MOKOSUITECLIENT_TICKETS_DESC="Runs scheduled helpdesk automation rules — auto-close, SLA escalation, and time-based actions." diff --git a/source/packages/plg_task_mokosuiteclient_tickets/mokosuiteclient_tickets.xml b/source/packages/plg_task_mokosuiteclient_tickets/mokosuiteclient_tickets.xml deleted file mode 100644 index c45a9adc..00000000 --- a/source/packages/plg_task_mokosuiteclient_tickets/mokosuiteclient_tickets.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - Task - MokoSuiteClient Ticket Automation - mokosuiteclient_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.46.78 - Runs scheduled helpdesk automation rules — auto-close resolved tickets, SLA breach escalation, and time-based actions. - Moko\Plugin\Task\MokoSuiteClientTickets - - - src - services - language - - - - en-GB/plg_task_mokosuiteclient_tickets.ini - en-GB/plg_task_mokosuiteclient_tickets.sys.ini - - diff --git a/source/packages/plg_task_mokosuiteclient_tickets/services/provider.php b/source/packages/plg_task_mokosuiteclient_tickets/services/provider.php deleted file mode 100644 index d885c3f7..00000000 --- a/source/packages/plg_task_mokosuiteclient_tickets/services/provider.php +++ /dev/null @@ -1,27 +0,0 @@ -set( - PluginInterface::class, - function (Container $container) { - $dispatcher = $container->get(DispatcherInterface::class); - $plugin = new TicketAutomation($dispatcher, (array) PluginHelper::getPlugin('task', 'mokosuiteclient_tickets')); - $plugin->setApplication(Factory::getApplication()); - - return $plugin; - } - ); - } -}; diff --git a/source/packages/plg_task_mokosuiteclient_tickets/src/Extension/TicketAutomation.php b/source/packages/plg_task_mokosuiteclient_tickets/src/Extension/TicketAutomation.php deleted file mode 100644 index 16bc8d6c..00000000 --- a/source/packages/plg_task_mokosuiteclient_tickets/src/Extension/TicketAutomation.php +++ /dev/null @@ -1,367 +0,0 @@ - [ - 'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOMATION', - 'method' => 'runAutomation', - ], - 'mokosuiteclient.ticket.imap_poll' => [ - 'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_TICKETS_IMAP_POLL', - 'method' => 'runImapPoll', - ], - 'mokosuiteclient.ticket.autoclose' => [ - 'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOCLOSE', - 'method' => 'runAutoClose', - ], - 'mokosuiteclient.license.validate' => [ - 'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_LICENSE_VALIDATE', - 'method' => 'runLicenseValidation', - ], - 'mokosuiteclient.license.heartbeat' => [ - 'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_LICENSE_HEARTBEAT', - 'method' => 'runLicenseHeartbeat', - ], - ]; - - 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; - } - } - - /** - * Poll IMAP inbox and create tickets from unread emails (#136). - */ - private function runImapPoll(ExecuteTaskEvent $event): int - { - $config = $this->getComponentConfig(); - $host = $config['imap_host'] ?? ''; - $port = (int) ($config['imap_port'] ?? 993); - $user = $config['imap_user'] ?? ''; - $pass = $config['imap_password'] ?? ''; - $ssl = ($config['imap_ssl'] ?? '1') === '1'; - $folder = $config['imap_folder'] ?? 'INBOX'; - $processed = $config['imap_processed_folder'] ?? 'INBOX.Processed'; - $defaultCat = (int) ($config['default_category'] ?? 0) ?: null; - - if (empty($host) || empty($user) || empty($pass)) - { - $this->logTask('IMAP not configured — skipping', 'warning'); - return Status::OK; - } - - if (!function_exists('imap_open')) - { - $this->logTask('php-imap extension not available', 'error'); - return Status::KNOCKOUT; - } - - $mailbox = '{' . $host . ':' . $port . '/imap' . ($ssl ? '/ssl' : '') . '/novalidate-cert}' . $folder; - $mbox = @imap_open($mailbox, $user, $pass); - - if (!$mbox) - { - $this->logTask('IMAP connection failed: ' . imap_last_error(), 'error'); - return Status::KNOCKOUT; - } - - $db = Factory::getDbo(); - $created = 0; - $replied = 0; - - $emails = imap_search($mbox, 'UNSEEN'); - - if ($emails === false) - { - imap_close($mbox); - $this->logTask('No new emails'); - return Status::OK; - } - - foreach ($emails as $msgNum) - { - try - { - $header = imap_headerinfo($mbox, $msgNum); - $subject = isset($header->subject) ? imap_utf8($header->subject) : '(no subject)'; - $fromAddr = $header->from[0]->mailbox . '@' . $header->from[0]->host; - $body = $this->getImapBody($mbox, $msgNum); - - // Match sender to Joomla user - $userId = $this->findUserByEmail($fromAddr); - - // Check if this is a reply (subject contains [#123]) - $ticketId = 0; - if (preg_match('/\[#(\d+)\]/', $subject, $m)) - { - $ticketId = (int) $m[1]; - } - - if ($ticketId > 0) - { - // Add as reply to existing ticket - $reply = (object) [ - 'ticket_id' => $ticketId, - 'user_id' => $userId, - 'body' => $body, - 'is_internal' => 0, - 'created' => Factory::getDate()->toSql(), - ]; - $db->insertObject('#__mokosuiteclient_ticket_replies', $reply, 'id'); - $replied++; - - // Notify - $db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuiteclient_tickets')->where('id = ' . $ticketId)); - $ticket = $db->loadObject(); - if ($ticket) { - NotificationService::notify('ticket_replied', $ticket, ['reply_body' => $body]); - } - } - else - { - // Create new ticket - $ticket = (object) [ - 'subject' => $subject, - 'body' => $body, - 'status' => 'open', - 'priority' => 'normal', - 'category_id' => $defaultCat, - 'created_by' => $userId, - 'created' => Factory::getDate()->toSql(), - ]; - $db->insertObject('#__mokosuiteclient_tickets', $ticket, 'id'); - $created++; - - NotificationService::notify('ticket_created', $ticket); - } - - // Mark as seen / move to processed folder - imap_setflag_full($mbox, (string) $msgNum, '\\Seen'); - - if ($processed && $processed !== $folder) - { - @imap_mail_move($mbox, (string) $msgNum, $processed); - } - } - catch (\Throwable $e) - { - Log::add('IMAP message processing error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient'); - } - } - - imap_expunge($mbox); - imap_close($mbox); - - $this->logTask("IMAP poll: {$created} tickets created, {$replied} replies added"); - return Status::OK; - } - - /** - * Auto-close resolved tickets after configured days. - */ - private function runAutoClose(ExecuteTaskEvent $event): int - { - $config = $this->getComponentConfig(); - $days = (int) ($config['autoclose_days'] ?? 7); - - if ($days <= 0) - { - $this->logTask('Auto-close disabled (days = 0)'); - return Status::OK; - } - - $db = Factory::getDbo(); - $cutoff = Factory::getDate('-' . $days . ' days')->toSql(); - - $db->setQuery( - "UPDATE {$db->quoteName('#__mokosuiteclient_tickets')}" - . " SET status = 'closed', closed = {$db->quote(Factory::getDate()->toSql())}" - . " WHERE status = 'resolved'" - . " AND resolved IS NOT NULL" - . " AND resolved < {$db->quote($cutoff)}" - ); - $db->execute(); - $closed = $db->getAffectedRows(); - - $this->logTask("Auto-close: {$closed} tickets closed (resolved > {$days} days ago)"); - return Status::OK; - } - - // ── Helpers ────────────────────────────────────────────────── - - private function getComponentConfig(): array - { - try - { - $db = Factory::getDbo(); - $db->setQuery( - $db->getQuery(true) - ->select('params') - ->from('#__extensions') - ->where('element = ' . $db->quote('com_mokosuiteclient')) - ->where('type = ' . $db->quote('component')) - ); - return json_decode($db->loadResult() ?? '{}', true) ?: []; - } - catch (\Throwable $e) - { - Log::add('Failed to load component config: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient'); - return []; - } - } - - private function findUserByEmail(string $email): int - { - $db = Factory::getDbo(); - $db->setQuery( - $db->getQuery(true) - ->select('id') - ->from('#__users') - ->where('email = ' . $db->quote($email)) - ->setLimit(1) - ); - return (int) $db->loadResult(); - } - - private function getImapBody($mbox, int $msgNum): string - { - $structure = imap_fetchstructure($mbox, $msgNum); - - // Simple single-part message - if (empty($structure->parts)) - { - $body = imap_fetchbody($mbox, $msgNum, '1'); - if ($structure->encoding === 3) $body = base64_decode($body); - if ($structure->encoding === 4) $body = quoted_printable_decode($body); - return trim(strip_tags($body)); - } - - // Multipart — find text/plain or text/html - $textBody = ''; - - foreach ($structure->parts as $i => $part) - { - $partNum = (string) ($i + 1); - - if ($part->type === 0) // text - { - $content = imap_fetchbody($mbox, $msgNum, $partNum); - if ($part->encoding === 3) $content = base64_decode($content); - if ($part->encoding === 4) $content = quoted_printable_decode($content); - - $subtype = strtolower($part->subtype ?? ''); - - if ($subtype === 'plain' && empty($textBody)) - { - $textBody = $content; - } - elseif ($subtype === 'html' && empty($textBody)) - { - $textBody = strip_tags($content); - } - } - } - - return trim($textBody); - } - - /** - * Daily license revalidation — refresh cached license status from MokoGitea. - * Recommended schedule: daily at 3:00 AM. - */ - private function runLicenseValidation(ExecuteTaskEvent $event): int - { - try { - $result = \Moko\Plugin\System\MokoSuiteClient\Helper\LicenseValidator::validate(true); - - $status = $result->valid ? 'valid' : ($result->status ?? 'invalid'); - $tier = $result->tier ?? 'none'; - $entitlements = count($result->entitlements ?? []); - - $this->logTask(sprintf( - 'License validation: status=%s tier=%s entitlements=%d', - $status, $tier, $entitlements - )); - - if (!$result->valid) { - Log::add('License validation failed: ' . ($result->message ?? 'unknown'), Log::WARNING, 'mokosuite.license'); - } - - return Status::OK; - } catch (\Throwable $e) { - $this->logTask('License validation error: ' . $e->getMessage()); - return Status::KNOCKOUT; - } - } - - /** - * Daily heartbeat — report active installation to MokoGitea. - * Recommended schedule: daily at 4:00 AM. - */ - private function runLicenseHeartbeat(ExecuteTaskEvent $event): int - { - try { - $result = \Moko\Plugin\System\MokoSuiteClient\Helper\LicenseValidator::heartbeat(); - - $this->logTask('License heartbeat: ' . ($result->success ?? false ? 'sent' : ($result->error ?? 'failed'))); - return Status::OK; - } catch (\Throwable $e) { - $this->logTask('License heartbeat error: ' . $e->getMessage()); - return Status::KNOCKOUT; - } - } -} diff --git a/source/pkg_mokosuiteclient.xml b/source/pkg_mokosuiteclient.xml index e7f67d05..30ca1ec6 100644 --- a/source/pkg_mokosuiteclient.xml +++ b/source/pkg_mokosuiteclient.xml @@ -28,7 +28,6 @@ plg_webservices_mokosuiteclient.zip plg_task_mokosuiteclientdemo.zip plg_task_mokosuiteclientsync.zip - plg_task_mokosuiteclient_tickets.zip diff --git a/source/script.php b/source/script.php index fd10b4e8..ec7b16a0 100644 --- a/source/script.php +++ b/source/script.php @@ -92,7 +92,6 @@ class Pkg_MokosuiteclientInstallerScript $this->enablePlugin('webservices', 'mokosuiteclient'); $this->enablePlugin('task', 'mokosuiteclientdemo'); $this->enablePlugin('task', 'mokosuiteclientsync'); - $this->enablePlugin('task', 'mokosuiteclient_tickets'); // Migrate params from core plugin to feature plugins (one-time) $this->migrateFeatureParams(); @@ -691,7 +690,7 @@ class Pkg_MokosuiteclientInstallerScript // Plugins that should exist on disk $expected = [ 'system' => ['mokosuiteclient_offline', 'mokosuiteclient_firewall', 'mokosuiteclient_tenant', 'mokosuiteclient_devtools', 'mokosuiteclient_dbip'], - 'task' => ['mokosuiteclient_tickets'], + 'task' => [], ]; foreach ($expected as $group => $elements) @@ -754,7 +753,6 @@ class Pkg_MokosuiteclientInstallerScript $db->quote('mod_mokosuiteclient_cpanel'), $db->quote('mokosuiteclientdemo'), $db->quote('mokosuiteclientsync'), - $db->quote('mokosuiteclient_tickets'), $db->quote('mokoonyx'), ];