feat(helpdesk): satisfaction ratings on resolved tickets (#140)
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
- 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
This commit is contained in:
@@ -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`),
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -188,6 +188,45 @@ $priorities = $this->priorities ?? [];
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Satisfaction Rating -->
|
||||
<?php
|
||||
$isClosed = in_array($t->status, ['resolved', 'closed'], true);
|
||||
$hasRating = !empty($t->satisfaction_rating);
|
||||
?>
|
||||
<?php if ($hasRating): ?>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><strong>Satisfaction</strong></div>
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-1">
|
||||
<?php for ($s = 1; $s <= 5; $s++): ?>
|
||||
<span style="font-size:1.5rem;color:<?php echo $s <= $t->satisfaction_rating ? '#f5a623' : '#dee2e6'; ?>;">★</span>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<div class="text-muted small"><?php echo $t->satisfaction_rating; ?>/5</div>
|
||||
<?php if (!empty($t->satisfaction_feedback)): ?>
|
||||
<p class="small mt-2 mb-0"><?php echo $this->escape($t->satisfaction_feedback); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php elseif ($isClosed): ?>
|
||||
<div class="card mb-3" id="rating-card">
|
||||
<div class="card-header"><strong>Rate this Support</strong></div>
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-2" id="star-rating">
|
||||
<?php for ($s = 1; $s <= 5; $s++): ?>
|
||||
<span class="star-btn" data-value="<?php echo $s; ?>" style="font-size:2rem;cursor:pointer;color:#dee2e6;">★</span>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<textarea id="rating-feedback" class="form-control form-control-sm mb-2" rows="2" placeholder="Optional feedback..."></textarea>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="btn-rate"
|
||||
data-url="<?php echo Route::_('index.php?option=com_mokosuite&task=display.rateTicket&format=json'); ?>"
|
||||
data-ticket="<?php echo $t->id; ?>" data-token="<?php echo $token; ?>" disabled>
|
||||
Submit Rating
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Status actions -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><strong>Actions</strong></div>
|
||||
@@ -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; });
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user