diff --git a/source/packages/plg_system_mokosuitesupport/src/Helper/ConversationHelper.php b/source/packages/plg_system_mokosuitesupport/src/Helper/ConversationHelper.php new file mode 100644 index 0000000..9c2ce6e --- /dev/null +++ b/source/packages/plg_system_mokosuitesupport/src/Helper/ConversationHelper.php @@ -0,0 +1,149 @@ +get(DatabaseInterface::class); + + $conversation = (object) [ + 'channel' => $channel, + 'contact_id' => $contactId, + 'visitor_name' => $visitorName, + 'status' => 'open', + 'started_at' => Factory::getDate()->toSql(), + ]; + + $db->insertObject('#__mokosuitesupport_conversations', $conversation, 'id'); + + return (object) ['success' => true, 'conversation_id' => (int) $conversation->id]; + } + + /** + * Send a message in a conversation. + */ + public static function sendMessage(int $conversationId, string $body, string $senderType = 'visitor', ?int $senderId = null): object + { + $allowedSenders = ['visitor', 'agent', 'system']; + if (!in_array($senderType, $allowedSenders, true)) { + $senderType = 'visitor'; + } + + $db = Factory::getContainer()->get(DatabaseInterface::class); + $filter = \Joomla\Filter\InputFilter::getInstance(); + + $message = (object) [ + 'conversation_id' => $conversationId, + 'sender_type' => $senderType, + 'sender_id' => $senderId, + 'body' => $filter->clean($body, 'STRING'), + 'sent_at' => Factory::getDate()->toSql(), + ]; + + $db->insertObject('#__mokosuitesupport_messages', $message, 'id'); + + // Update conversation last_message_at + $db->setQuery($db->getQuery(true) + ->update('#__mokosuitesupport_conversations') + ->set('last_message_at = ' . $db->quote(Factory::getDate()->toSql())) + ->where('id = ' . (int) $conversationId)); + $db->execute(); + + return (object) ['success' => true, 'message_id' => (int) $message->id]; + } + + /** + * Get unified inbox — all open conversations across channels. + */ + public static function getInbox(array $filters = [], int $limit = 50): array + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $query = $db->getQuery(true) + ->select('c.id, c.channel, c.status, c.visitor_name, c.started_at, c.last_message_at') + ->select('cd.name AS contact_name, cd.email_to') + ->select('ag.display_name AS agent_name') + ->select('(SELECT COUNT(*) FROM #__mokosuitesupport_messages m WHERE m.conversation_id = c.id) AS message_count') + ->select('(SELECT body FROM #__mokosuitesupport_messages m2 WHERE m2.conversation_id = c.id ORDER BY m2.sent_at DESC LIMIT 1) AS last_message') + ->from($db->quoteName('#__mokosuitesupport_conversations', 'c')) + ->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = c.contact_id') + ->join('LEFT', $db->quoteName('#__mokosuitesupport_agents', 'ag') . ' ON ag.id = c.agent_id') + ->order('c.last_message_at DESC'); + + if (!empty($filters['status'])) { + $query->where($db->quoteName('c.status') . ' = ' . $db->quote($filters['status'])); + } else { + $query->where($db->quoteName('c.status') . ' IN (' . $db->quote('open') . ',' . $db->quote('assigned') . ')'); + } + + if (!empty($filters['channel'])) { + $query->where($db->quoteName('c.channel') . ' = ' . $db->quote($filters['channel'])); + } + + if (!empty($filters['agent_id'])) { + $query->where('c.agent_id = ' . (int) $filters['agent_id']); + } + + $db->setQuery($query, 0, min(max(1, $limit), 200)); + + return $db->loadObjectList() ?: []; + } + + /** + * Assign a conversation to an agent. + */ + public static function assign(int $conversationId, int $agentId): bool + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $db->setQuery($db->getQuery(true) + ->update('#__mokosuitesupport_conversations') + ->set('agent_id = ' . (int) $agentId) + ->set($db->quoteName('status') . ' = ' . $db->quote('assigned')) + ->set('assigned_at = ' . $db->quote(Factory::getDate()->toSql())) + ->where('id = ' . (int) $conversationId)); + $db->execute(); + + return $db->getAffectedRows() > 0; + } + + /** + * Close a conversation with optional CSAT rating. + */ + public static function close(int $conversationId, ?int $csatRating = null): bool + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + + $query = $db->getQuery(true) + ->update('#__mokosuitesupport_conversations') + ->set($db->quoteName('status') . ' = ' . $db->quote('closed')) + ->set('ended_at = ' . $db->quote(Factory::getDate()->toSql())) + ->where('id = ' . (int) $conversationId); + + if ($csatRating !== null && $csatRating >= 1 && $csatRating <= 5) { + $query->set('csat_rating = ' . (int) $csatRating); + } + + $db->setQuery($query); + $db->execute(); + + return $db->getAffectedRows() > 0; + } +} diff --git a/source/pkg_mokosuitesupport.xml b/source/pkg_mokosuitesupport.xml new file mode 100644 index 0000000..2d69870 --- /dev/null +++ b/source/pkg_mokosuitesupport.xml @@ -0,0 +1,26 @@ + + + Package - MokoSuite Support + mokosuitesupport + 01.00.00 + 2026-06-21 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GNU General Public License version 3 or later; see LICENSE + MokoSuite Support - live chat, multi-channel customer support (website, Facebook, Instagram, WhatsApp, email). Interfaces with CRM contacts and ticket system. + 8.3 + + true + script.php + + + plg_system_mokosuitesupport.zip + com_mokosuitesupport.zip + + + + https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteSupport/updates.xml + +