From f3a3bc90b3c9fd4edeba6f29cedfcc55ca205c87 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 2 Jun 2026 18:48:13 -0500 Subject: [PATCH] feat: frontend ticket management with smart permissions Ticket List (view=tickets): - Staff see all tickets with Submitted By + Assigned To columns - Customers see only their own tickets - Status filter dropdown for staff - isStaff check: core.admin OR mokowaas.tickets ACL Ticket Detail (view=ticket): - Staff see all tickets + internal notes (yellow highlight) - Customers see only their own + no internal notes - Staff sidebar: ticket details, change status buttons, assign to me - Staff can post internal notes from frontend - Customers can reply (disabled for closed tickets) - Ownership enforced: customers can't view/reply to others' tickets Controller: - updateStatus: staff only - assignTicket: mokowaas.tickets.assign ACL - submitReply: verifies ownership OR staff Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../site/src/View/Ticket/HtmlView.php | 34 +- .../site/src/View/Tickets/HtmlView.php | 31 +- .../com_mokowaas/site/tmpl/ticket/default.php | 299 ++++++++++++------ .../site/tmpl/tickets/default.php | 148 +++------ 4 files changed, 308 insertions(+), 204 deletions(-) diff --git a/src/packages/com_mokowaas/site/src/View/Ticket/HtmlView.php b/src/packages/com_mokowaas/site/src/View/Ticket/HtmlView.php index c5a929f4..4a4289e6 100644 --- a/src/packages/com_mokowaas/site/src/View/Ticket/HtmlView.php +++ b/src/packages/com_mokowaas/site/src/View/Ticket/HtmlView.php @@ -17,6 +17,8 @@ use Joomla\CMS\Router\Route; class HtmlView extends BaseHtmlView { protected $ticket; + protected $isStaff = false; + protected $canAssign = false; public function display($tpl = null) { @@ -24,16 +26,29 @@ class HtmlView extends BaseHtmlView $user = Factory::getApplication()->getIdentity(); $id = Factory::getApplication()->getInput()->getInt('id', 0); - // Get ticket — only if owned by this user + $this->isStaff = $user->authorise('core.admin') || $user->authorise('mokowaas.tickets', 'com_mokowaas'); + $this->canAssign = $user->authorise('core.admin') || $user->authorise('mokowaas.tickets.assign', 'com_mokowaas'); + + // Get ticket — staff see any, customers see only their own $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('a.name', 'assigned_to_name'), ]) ->from($db->quoteName('#__mokowaas_tickets', 't')) ->leftJoin($db->quoteName('#__mokowaas_ticket_categories', 'c') . ' ON c.id = t.category_id') - ->where($db->quoteName('t.id') . ' = ' . $id) - ->where($db->quoteName('t.created_by') . ' = ' . (int) $user->id); + ->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by') + ->leftJoin($db->quoteName('#__users', 'a') . ' ON a.id = t.assigned_to') + ->where($db->quoteName('t.id') . ' = ' . $id); + + if (!$this->isStaff) + { + $query->where($db->quoteName('t.created_by') . ' = ' . (int) $user->id); + } + $db->setQuery($query); $this->ticket = $db->loadObject(); @@ -45,7 +60,7 @@ class HtmlView extends BaseHtmlView return; } - // Load replies (exclude internal notes) + // Load replies — staff see internal notes, customers don't $query = $db->getQuery(true) ->select([ $db->quoteName('r') . '.*', @@ -53,9 +68,14 @@ class HtmlView extends BaseHtmlView ]) ->from($db->quoteName('#__mokowaas_ticket_replies', 'r')) ->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id') - ->where($db->quoteName('r.ticket_id') . ' = ' . $id) - ->where($db->quoteName('r.is_internal') . ' = 0') - ->order($db->quoteName('r.created') . ' ASC'); + ->where($db->quoteName('r.ticket_id') . ' = ' . $id); + + if (!$this->isStaff) + { + $query->where($db->quoteName('r.is_internal') . ' = 0'); + } + + $query->order($db->quoteName('r.created') . ' ASC'); $db->setQuery($query); $this->ticket->replies = $db->loadObjectList() ?: []; diff --git a/src/packages/com_mokowaas/site/src/View/Tickets/HtmlView.php b/src/packages/com_mokowaas/site/src/View/Tickets/HtmlView.php index 933e5c55..5988fba9 100644 --- a/src/packages/com_mokowaas/site/src/View/Tickets/HtmlView.php +++ b/src/packages/com_mokowaas/site/src/View/Tickets/HtmlView.php @@ -17,13 +17,17 @@ class HtmlView extends BaseHtmlView { protected $tickets = []; protected $categories = []; + protected $isStaff = false; public function display($tpl = null) { $db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface'); $user = Factory::getApplication()->getIdentity(); - // Get user's tickets + $this->isStaff = $user->authorise('core.admin') + || $user->authorise('mokowaas.tickets', 'com_mokowaas'); + + // Staff see all tickets, customers see their own $query = $db->getQuery(true) ->select([ $db->quoteName('t.id'), @@ -31,18 +35,33 @@ class HtmlView extends BaseHtmlView $db->quoteName('t.status'), $db->quoteName('t.priority'), $db->quoteName('t.created'), - $db->quoteName('t.modified'), + $db->quoteName('t.assigned_to'), $db->quoteName('c.title', 'category_title'), + $db->quoteName('u.name', 'created_by_name'), + $db->quoteName('a.name', 'assigned_to_name'), ]) ->from($db->quoteName('#__mokowaas_tickets', 't')) ->leftJoin($db->quoteName('#__mokowaas_ticket_categories', 'c') . ' ON c.id = t.category_id') - ->where($db->quoteName('t.created_by') . ' = ' . (int) $user->id) - ->order($db->quoteName('t.created') . ' DESC') - ->setLimit(50); + ->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by') + ->leftJoin($db->quoteName('#__users', 'a') . ' ON a.id = t.assigned_to'); + + if (!$this->isStaff) + { + $query->where($db->quoteName('t.created_by') . ' = ' . (int) $user->id); + } + + $filterStatus = Factory::getApplication()->getInput()->getString('filter_status', ''); + + if ($filterStatus) + { + $query->where($db->quoteName('t.status') . ' = ' . $db->quote($filterStatus)); + } + + $query->order($db->quoteName('t.created') . ' DESC')->setLimit(50); $db->setQuery($query); $this->tickets = $db->loadObjectList() ?: []; - // Get categories for new ticket form + // Categories for new ticket form $query = $db->getQuery(true) ->select([$db->quoteName('id'), $db->quoteName('title')]) ->from($db->quoteName('#__mokowaas_ticket_categories')) diff --git a/src/packages/com_mokowaas/site/tmpl/ticket/default.php b/src/packages/com_mokowaas/site/tmpl/ticket/default.php index 5c6d135d..7f84e579 100644 --- a/src/packages/com_mokowaas/site/tmpl/ticket/default.php +++ b/src/packages/com_mokowaas/site/tmpl/ticket/default.php @@ -1,22 +1,19 @@ ticket; -$token = Session::getFormToken(); +$t = $this->ticket; +$isStaff = $this->isStaff; +$canAssign = $this->canAssign; +$token = Session::getFormToken(); +$userId = Factory::getApplication()->getIdentity()->id; $statusLabel = [ - 'open' => 'Open', 'in_progress' => 'In Progress', 'waiting' => 'Awaiting Your Response', + 'open' => 'Open', 'in_progress' => 'In Progress', 'waiting' => 'Awaiting Response', 'resolved' => 'Resolved', 'closed' => 'Closed', ]; $statusClass = [ @@ -28,99 +25,217 @@ $statusClass = [
- -
-
-
-
-

#id; ?> — subject); ?>

- - category_title ?? 'General'); ?> - · Submitted created, 'M d, Y \a\t H:i'); ?> - · Priority: priority); ?> - +
+ +
+ + +
+
+
+
+

#id; ?> — subject); ?>

+ + category_title ?? 'General'); ?> + · created, 'M d, Y H:i'); ?> + · priority); ?> + + · By: created_by_name); ?> + + +
+ + status] ?? $t->status; ?> + +
- - status] ?? ucfirst($t->status); ?> -
-
-
- -
-
- You - created, 'M d, Y H:i'); ?> -
-
body)); ?>
-
- - - replies as $reply): ?> - user_id !== (int) $t->created_by); - ?> -
-
-
- user_name ?? 'Support'); ?> - - Staff - - created, 'M d, Y H:i'); ?> -
-
-
body)); ?>
-
- - - - status, ['closed', 'resolved'])): ?> -
-
-
Reply
-
-
- + +
+
+ created_by_name); ?> + created, 'M d, Y H:i'); ?>
- - - - +
body)); ?>
+
+ + + replies as $reply): ?> + user_id !== (int) $t->created_by); + $isInternal = (int) $reply->is_internal; + ?> +
+
+
+ user_name ?? 'Support'); ?> + Staff + Internal Note + created, 'M d, Y H:i'); ?> +
+
+
body)); ?>
+
+ + + + status, ['closed'])): ?> +
+
+
Reply
+
+ + + +
+ + + + +
+
+
+
+ status === 'closed'): ?> +
+ This ticket is closed. Open a new ticket if you need further help. +
+
+ + + +
+ +
+
Details
+
+
+
Status
+
status] ?? $t->status; ?>
+
Priority
+
priority); ?>
+
Category
+
category_title ?? '—'); ?>
+
Submitted By
+
created_by_name); ?>
created_by_email ?? ''); ?>
+
Assigned To
+
assigned_to_name ?? 'Unassigned'); ?>
+
Created
+
created, 'M d H:i'); ?>
+
Replies
+
replies); ?>
+
+
+
+ + +
+
Change Status
+
+ 'Reopen', 'in_progress' => 'In Progress', 'waiting' => 'Waiting on Customer', 'resolved' => 'Resolve', 'closed' => 'Close'] as $s => $label): ?> + status): ?> + + + +
+
+ + + +
+
Assign
+
+ +
+
+ +
+
- -
- This ticket is status] ?? $t->status); ?>. If you need further help, please open a new ticket. -
-
diff --git a/src/packages/com_mokowaas/site/tmpl/tickets/default.php b/src/packages/com_mokowaas/site/tmpl/tickets/default.php index f9face45..8ed9e1a3 100644 --- a/src/packages/com_mokowaas/site/tmpl/tickets/default.php +++ b/src/packages/com_mokowaas/site/tmpl/tickets/default.php @@ -1,11 +1,4 @@ tickets; $categories = $this->categories; +$isStaff = $this->isStaff; $token = Session::getFormToken(); $statusLabel = [ @@ -28,106 +22,62 @@ $statusClass = [
-

My Support Tickets

- -
- - -
-
-
-
Submit a Support Request
-
-
- - -
-
-
- - -
-
- - -
-
-
- - -
- - -
-
+

+
+ + New Ticket + + +
+ + + +
+
-
- You haven't submitted any support tickets yet. Click "New Ticket" above to get started. +
-
- - -
-
-
#id; ?> — subject); ?>
- - category_title ?? 'General'); ?> - · created, 'M d, Y'); ?> - -
-
- - status] ?? ucfirst($t->status); ?> - -
- priority); ?> -
-
-
- +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#SubjectStatusPriorityCategorySubmitted ByAssigned ToDate
id; ?>subject, 0, 60)); ?>status] ?? $t->status; ?>priority); ?>category_title ?? '—'); ?>created_by_name ?? ''); ?>assigned_to_name ?? 'Unassigned'); ?>created, 'M d, Y'); ?>
- -