From fbb467a8324a4f70f47962f17f96dec7a1be7db4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 9 Jun 2026 10:42:33 -0500 Subject: [PATCH] feat(helpdesk): satisfaction ratings on resolved tickets (#140) - satisfaction_rating (1-5), satisfaction_feedback, satisfaction_rated_at columns - Star rating widget in ticket sidebar (appears when resolved/closed) - Hover highlight + click to select rating - Optional feedback textarea - rateTicket controller task persists rating via AJAX - Displays existing rating with stars + feedback when already rated --- .../com_mokosuite/admin/sql/install.mysql.sql | 3 + .../src/Controller/DisplayController.php | 22 ++++++ .../admin/tmpl/ticket/default.php | 76 +++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/source/packages/com_mokosuite/admin/sql/install.mysql.sql b/source/packages/com_mokosuite/admin/sql/install.mysql.sql index dca5aaa2..15f07803 100644 --- a/source/packages/com_mokosuite/admin/sql/install.mysql.sql +++ b/source/packages/com_mokosuite/admin/sql/install.mysql.sql @@ -72,6 +72,9 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_tickets` ( `sla_response_due` DATETIME DEFAULT NULL, `sla_resolution_due` DATETIME DEFAULT NULL, `sla_responded` TINYINT NOT NULL DEFAULT 0, + `satisfaction_rating` TINYINT UNSIGNED DEFAULT NULL, + `satisfaction_feedback` TEXT DEFAULT NULL, + `satisfaction_rated_at` DATETIME DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_status` (`status`), KEY `idx_status_id` (`status_id`), diff --git a/source/packages/com_mokosuite/admin/src/Controller/DisplayController.php b/source/packages/com_mokosuite/admin/src/Controller/DisplayController.php index 8fa94766..2fa5942a 100644 --- a/source/packages/com_mokosuite/admin/src/Controller/DisplayController.php +++ b/source/packages/com_mokosuite/admin/src/Controller/DisplayController.php @@ -621,6 +621,28 @@ class DisplayController extends BaseController $this->jsonResponse(['success' => $ok, 'message' => $ok ? 'Attachment deleted' : 'Not found']); } + public function rateTicket() + { + Session::checkToken() or die(Text::_('JINVALID_TOKEN')); + $input = Factory::getApplication()->getInput(); + $ticketId = $input->getInt('ticket_id', 0); + $rating = $input->getInt('rating', 0); + $feedback = $input->getString('feedback', ''); + if (!$ticketId || $rating < 1 || $rating > 5) { + $this->jsonResponse(['success' => false, 'message' => 'Invalid rating (1-5)']); + return; + } + $db = Factory::getDbo(); + $db->setQuery( + 'UPDATE ' . $db->quoteName('#__mokosuite_tickets') + . ' SET satisfaction_rating = ' . $rating + . ', satisfaction_feedback = ' . $db->quote($feedback) + . ', satisfaction_rated_at = ' . $db->quote(Factory::getDate()->toSql()) + . ' WHERE id = ' . $ticketId + )->execute(); + $this->jsonResponse(['success' => true, 'message' => 'Thank you for your feedback!']); + } + public function saveAutomation() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); diff --git a/source/packages/com_mokosuite/admin/tmpl/ticket/default.php b/source/packages/com_mokosuite/admin/tmpl/ticket/default.php index c6681f06..226c6244 100644 --- a/source/packages/com_mokosuite/admin/tmpl/ticket/default.php +++ b/source/packages/com_mokosuite/admin/tmpl/ticket/default.php @@ -188,6 +188,45 @@ $priorities = $this->priorities ?? []; + + status, ['resolved', 'closed'], true); + $hasRating = !empty($t->satisfaction_rating); + ?> + +
+
Satisfaction
+
+
+ + + +
+
satisfaction_rating; ?>/5
+ satisfaction_feedback)): ?> +

escape($t->satisfaction_feedback); ?>

+ +
+
+ +
+
Rate this Support
+
+
+ + + +
+ + +
+
+ +
Actions
@@ -284,5 +323,42 @@ document.addEventListener('DOMContentLoaded', function() { .finally(function(){ el.disabled = false; }); }); }); + // Star rating + var selectedRating = 0; + document.querySelectorAll('.star-btn').forEach(function(star) { + star.addEventListener('mouseenter', function() { + var val = parseInt(this.dataset.value); + document.querySelectorAll('.star-btn').forEach(function(s) { + s.style.color = parseInt(s.dataset.value) <= val ? '#f5a623' : '#dee2e6'; + }); + }); + star.addEventListener('mouseleave', function() { + document.querySelectorAll('.star-btn').forEach(function(s) { + s.style.color = parseInt(s.dataset.value) <= selectedRating ? '#f5a623' : '#dee2e6'; + }); + }); + star.addEventListener('click', function() { + selectedRating = parseInt(this.dataset.value); + document.getElementById('btn-rate').disabled = false; + }); + }); + + var rateBtn = document.getElementById('btn-rate'); + if (rateBtn) { + rateBtn.addEventListener('click', function() { + if (!selectedRating) return; + var el = this; + el.disabled = true; + var fd = new FormData(); + fd.append('ticket_id', el.dataset.ticket); + fd.append('rating', selectedRating); + fd.append('feedback', document.getElementById('rating-feedback').value); + fd.append(el.dataset.token, '1'); + fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}}) + .then(function(r){return r.json()}) + .then(function(d){ if(d.success) location.reload(); else Joomla.renderMessages({error:[d.message]}); }) + .finally(function(){ el.disabled = false; }); + }); + } });