Add CustomerFeedbackHelper — post-service surveys, NPS scores, token-based feedback
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
namespace Moko\Plugin\System\MokoSuiteField\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Customer feedback — post-service surveys, NPS scores, satisfaction tracking.
|
||||
*/
|
||||
class CustomerFeedbackHelper
|
||||
{
|
||||
/**
|
||||
* Send a feedback request after work order completion.
|
||||
*/
|
||||
public static function requestFeedback(int $woId): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('wo.id, wo.wo_number, wo.contact_id, cd.name AS customer_name, cd.email_to, cd.telephone')
|
||||
->from($db->quoteName('#__mokosuitefield_work_orders', 'wo'))
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = wo.contact_id')
|
||||
->where('wo.id = ' . (int) $woId));
|
||||
$wo = $db->loadObject();
|
||||
|
||||
if (!$wo || !$wo->email_to) {
|
||||
return (object) ['success' => false, 'error' => 'No email for feedback request'];
|
||||
}
|
||||
|
||||
// Generate unique feedback token
|
||||
$token = bin2hex(random_bytes(16));
|
||||
|
||||
$request = (object) [
|
||||
'wo_id' => $woId,
|
||||
'contact_id' => $wo->contact_id,
|
||||
'token' => $token,
|
||||
'status' => 'sent',
|
||||
'sent_at' => Factory::getDate()->toSql(),
|
||||
];
|
||||
$db->insertObject('#__mokosuitefield_feedback_requests', $request, 'id');
|
||||
|
||||
return (object) ['success' => true, 'token' => $token, 'email' => $wo->email_to];
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit feedback (called from public survey page via token).
|
||||
*/
|
||||
public static function submitFeedback(string $token, int $rating, int $npsScore, string $comments = ''): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('id, wo_id, contact_id, status')
|
||||
->from('#__mokosuitefield_feedback_requests')
|
||||
->where($db->quoteName('token') . ' = ' . $db->quote($token)));
|
||||
$request = $db->loadObject();
|
||||
|
||||
if (!$request) return (object) ['success' => false, 'error' => 'Invalid feedback link'];
|
||||
if ($request->status === 'completed') return (object) ['success' => false, 'error' => 'Feedback already submitted'];
|
||||
|
||||
// Validate inputs
|
||||
$rating = max(1, min(5, $rating));
|
||||
$npsScore = max(0, min(10, $npsScore));
|
||||
|
||||
$filter = \Joomla\Filter\InputFilter::getInstance();
|
||||
$comments = $filter->clean($comments, 'STRING');
|
||||
|
||||
$db->transactionStart();
|
||||
try {
|
||||
// Record feedback
|
||||
$feedback = (object) [
|
||||
'request_id' => $request->id,
|
||||
'wo_id' => $request->wo_id,
|
||||
'contact_id' => $request->contact_id,
|
||||
'rating' => $rating,
|
||||
'nps_score' => $npsScore,
|
||||
'comments' => $comments,
|
||||
'submitted_at' => Factory::getDate()->toSql(),
|
||||
];
|
||||
$db->insertObject('#__mokosuitefield_feedback', $feedback);
|
||||
|
||||
// Mark request as completed
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->update('#__mokosuitefield_feedback_requests')
|
||||
->set($db->quoteName('status') . ' = ' . $db->quote('completed'))
|
||||
->where('id = ' . (int) $request->id));
|
||||
$db->execute();
|
||||
|
||||
$db->transactionCommit();
|
||||
} catch (\Throwable $e) {
|
||||
$db->transactionRollback();
|
||||
return (object) ['success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
|
||||
return (object) ['success' => true, 'rating' => $rating, 'nps' => $npsScore];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NPS score and satisfaction summary.
|
||||
*/
|
||||
public static function getSatisfactionSummary(string $from = '', string $to = ''): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$from = $from ?: date('Y-01-01');
|
||||
$to = $to ?: date('Y-m-d');
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(*) AS total_responses')
|
||||
->select('COALESCE(AVG(rating), 0) AS avg_rating')
|
||||
->select('COALESCE(AVG(nps_score), 0) AS avg_nps')
|
||||
->select('SUM(CASE WHEN nps_score >= 9 THEN 1 ELSE 0 END) AS promoters')
|
||||
->select('SUM(CASE WHEN nps_score BETWEEN 7 AND 8 THEN 1 ELSE 0 END) AS passives')
|
||||
->select('SUM(CASE WHEN nps_score <= 6 THEN 1 ELSE 0 END) AS detractors')
|
||||
->from('#__mokosuitefield_feedback')
|
||||
->where('DATE(submitted_at) BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to)));
|
||||
|
||||
$stats = $db->loadObject() ?: (object) ['total_responses' => 0, 'avg_rating' => 0, 'avg_nps' => 0, 'promoters' => 0, 'passives' => 0, 'detractors' => 0];
|
||||
|
||||
$total = (int) $stats->total_responses;
|
||||
$stats->nps_score = $total > 0
|
||||
? round(((int) $stats->promoters - (int) $stats->detractors) / $total * 100)
|
||||
: 0;
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user