From 4ec90d38f23bec08a5ae755f9e845e29fe7a9079 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 21 Jun 2026 10:57:33 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20CustomerSatisfactionHelper=20=E2=80=94?= =?UTF-8?q?=20post-service=20NPS,=20technician=20ratings,=20survey=20manag?= =?UTF-8?q?ement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Helper/CustomerSatisfactionHelper.php | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 source/packages/plg_system_mokosuitefield/src/Helper/CustomerSatisfactionHelper.php diff --git a/source/packages/plg_system_mokosuitefield/src/Helper/CustomerSatisfactionHelper.php b/source/packages/plg_system_mokosuitefield/src/Helper/CustomerSatisfactionHelper.php new file mode 100644 index 0000000..29e78cb --- /dev/null +++ b/source/packages/plg_system_mokosuitefield/src/Helper/CustomerSatisfactionHelper.php @@ -0,0 +1,118 @@ + 5) { + throw new \InvalidArgumentException('Rating must be 1-5.'); + } + + $db = Factory::getContainer()->get(DatabaseInterface::class); + + // Prevent duplicate surveys per work order + $db->setQuery($db->getQuery(true) + ->select('id') + ->from('#__mokosuitefield_surveys') + ->where('work_order_id = ' . (int) $workOrderId) + ->where('contact_id = ' . (int) $contactId)); + + if ($db->loadResult()) { + return (object) ['success' => false, 'error' => 'Survey already submitted for this work order']; + } + + $filter = \Joomla\Filter\InputFilter::getInstance(); + + $survey = (object) [ + 'work_order_id' => $workOrderId, + 'contact_id' => $contactId, + 'rating' => $rating, + 'comment' => $comment !== null ? $filter->clean($comment, 'STRING') : null, + 'nps_score' => $rating >= 4 ? 'promoter' : ($rating >= 3 ? 'passive' : 'detractor'), + 'created_at' => Factory::getDate()->toSql(), + ]; + + $db->insertObject('#__mokosuitefield_surveys', $survey, 'id'); + + return (object) ['success' => true, 'survey_id' => (int) $survey->id]; + } + + /** + * Get NPS (Net Promoter Score) for a period. + */ + public static function getNps(string $from = '', string $to = ''): object + { + $from = $from ?: date('Y-01-01'); + $to = $to ?: date('Y-m-d'); + + if (!\DateTime::createFromFormat('Y-m-d', $from) || !\DateTime::createFromFormat('Y-m-d', $to)) { + throw new \InvalidArgumentException('Date parameters must be Y-m-d format.'); + } + + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->select('COUNT(*) AS total_responses') + ->select('SUM(CASE WHEN rating >= 4 THEN 1 ELSE 0 END) AS promoters') + ->select('SUM(CASE WHEN rating = 3 THEN 1 ELSE 0 END) AS passives') + ->select('SUM(CASE WHEN rating <= 2 THEN 1 ELSE 0 END) AS detractors') + ->select('AVG(rating) AS avg_rating') + ->from('#__mokosuitefield_surveys') + ->where('DATE(created_at) BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to))); + + $stats = $db->loadObject(); + $total = (int) ($stats->total_responses ?? 0); + $promoterPct = $total > 0 ? (int) $stats->promoters / $total * 100 : 0; + $detractorPct = $total > 0 ? (int) $stats->detractors / $total * 100 : 0; + + return (object) [ + 'nps' => round($promoterPct - $detractorPct), + 'total_responses' => $total, + 'promoters' => (int) ($stats->promoters ?? 0), + 'passives' => (int) ($stats->passives ?? 0), + 'detractors' => (int) ($stats->detractors ?? 0), + 'avg_rating' => round((float) ($stats->avg_rating ?? 0), 1), + ]; + } + + /** + * Get technician satisfaction rankings. + */ + public static function getTechnicianRankings(int $limit = 20): array + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->select('t.id AS tech_id, cd.name AS tech_name') + ->select('COUNT(s.id) AS survey_count') + ->select('AVG(s.rating) AS avg_rating') + ->select('SUM(CASE WHEN s.rating >= 4 THEN 1 ELSE 0 END) AS five_star_count') + ->from($db->quoteName('#__mokosuitefield_surveys', 's')) + ->join('INNER', $db->quoteName('#__mokosuitefield_work_orders', 'wo') . ' ON wo.id = s.work_order_id') + ->join('INNER', $db->quoteName('#__mokosuitefield_technicians', 't') . ' ON t.id = wo.tech_id') + ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id') + ->group('t.id, cd.name') + ->having('COUNT(s.id) >= 3') + ->order('avg_rating DESC'), 0, min(max(1, $limit), 100)); + + $results = $db->loadObjectList() ?: []; + + foreach ($results as &$r) { + $r->avg_rating = round((float) $r->avg_rating, 1); + } + + return $results; + } +}