InteractionDetailsModel.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. <?php
  2. namespace Models;
  3. use Libs\Database;
  4. class InteractionDetailsModel
  5. {
  6. private \PDO $pdo;
  7. public function __construct()
  8. {
  9. $this->pdo = Database::pdo();
  10. }
  11. public function getInteractionDetails(int $companyId, int $conversationId): ?array
  12. {
  13. $conversation = $this->getConversation($companyId, $conversationId);
  14. if ($conversation === null) {
  15. return null;
  16. }
  17. $messages = $this->getMessages($conversationId);
  18. $report = $this->buildReport($conversation, $messages);
  19. return [
  20. 'conversation' => [
  21. 'conversationId' => (int) $conversation['conversation_id'],
  22. 'client' => $conversation['client_phone'] ?? '',
  23. 'channel' => $this->formatChannel((string) ($conversation['conversation_channel'] ?? '')),
  24. 'agent' => $conversation['operator_name'] ?? '',
  25. ],
  26. 'thread' => array_map(function (array $message): array {
  27. return [
  28. 'id' => 'm' . (int) $message['message_id'],
  29. 'isAgent' => (bool) $message['message_is_operator'],
  30. 'text' => $message['message_content'] ?? '',
  31. 'time' => $this->formatTime((string) ($message['message_sent_at'] ?? '')),
  32. 'date' => $this->formatDate((string) ($message['message_sent_at'] ?? '')),
  33. ];
  34. }, $messages),
  35. 'report' => $report,
  36. ];
  37. }
  38. private function getConversation(int $companyId, int $conversationId): ?array
  39. {
  40. $stmt = $this->pdo->prepare(
  41. "SELECT
  42. c.conversation_id,
  43. c.conversation_channel,
  44. c.conversation_started_at,
  45. c.conversation_last_message_at,
  46. cl.client_phone,
  47. o.operator_name,
  48. ca.conversation_analysis_aspect,
  49. ca.conversation_analysis_sub_aspect,
  50. ca.conversation_analysis_sentiment,
  51. ca.conversation_analysis_sentiment_score
  52. FROM conversation c
  53. INNER JOIN client cl
  54. ON cl.client_id = c.client_id
  55. AND cl.client_deleted_at = 'infinity'
  56. INNER JOIN operator o
  57. ON o.operator_id = c.operator_id
  58. AND o.operator_deleted_at = 'infinity'
  59. LEFT JOIN conversation_analysis ca
  60. ON ca.conversation_id = c.conversation_id
  61. AND ca.conversation_analysis_deleted_at = 'infinity'
  62. WHERE c.company_id = :company_id
  63. AND c.conversation_id = :conversation_id
  64. AND c.conversation_deleted_at = 'infinity'
  65. LIMIT 1"
  66. );
  67. $stmt->execute([
  68. 'company_id' => $companyId,
  69. 'conversation_id' => $conversationId,
  70. ]);
  71. $conversation = $stmt->fetch(\PDO::FETCH_ASSOC);
  72. return $conversation === false ? null : $conversation;
  73. }
  74. private function getMessages(int $conversationId): array
  75. {
  76. $stmt = $this->pdo->prepare(
  77. "SELECT
  78. message_id,
  79. message_is_operator,
  80. message_content,
  81. message_sent_at
  82. FROM message
  83. WHERE conversation_id = :conversation_id
  84. AND message_deleted_at = 'infinity'
  85. AND message_deleted = FALSE
  86. AND message_hidden = FALSE
  87. AND message_is_event = FALSE
  88. ORDER BY message_sent_at ASC, message_id ASC"
  89. );
  90. $stmt->execute(['conversation_id' => $conversationId]);
  91. return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
  92. }
  93. private function buildReport(array $conversation, array $messages): array
  94. {
  95. $avgResponseSeconds = $this->calculateAverageResponseSeconds($messages);
  96. $avgAgentSeconds = $this->calculateAverageGapByAuthor($messages, true);
  97. $avgClientSeconds = $this->calculateAverageGapByAuthor($messages, false);
  98. $totalDurationSeconds = $this->calculateTotalDurationSeconds($conversation, $messages);
  99. $lastMessageAuthor = $this->getLastMessageAuthor($messages);
  100. $consecutiveMessages = $this->hasTrailingConsecutiveMessages($messages);
  101. return [
  102. 'avgResponse' => $this->formatDuration($avgResponseSeconds),
  103. 'totalDuration' => $this->formatDuration($totalDurationSeconds),
  104. 'avgAgent' => $this->formatDuration($avgAgentSeconds),
  105. 'avgClient' => $this->formatDuration($avgClientSeconds),
  106. 'mainAspect' => $conversation['conversation_analysis_aspect'] ?? '',
  107. 'subAspect' => $conversation['conversation_analysis_sub_aspect'] ?? '',
  108. 'lastMessageAuthor' => $lastMessageAuthor,
  109. 'consecutiveMessages' => $consecutiveMessages,
  110. 'sentiment' => $this->normalizeSentimentLabel((string) ($conversation['conversation_analysis_sentiment'] ?? '')),
  111. 'score' => round((float) ($conversation['conversation_analysis_sentiment_score'] ?? 0), 2),
  112. ];
  113. }
  114. private function calculateAverageResponseSeconds(array $messages): int
  115. {
  116. $responseTimes = [];
  117. $pendingClientTimestamp = null;
  118. foreach ($messages as $message) {
  119. $timestamp = strtotime((string) ($message['message_sent_at'] ?? ''));
  120. if ($timestamp === false) {
  121. continue;
  122. }
  123. $isOperator = (bool) ($message['message_is_operator'] ?? false);
  124. if (!$isOperator) {
  125. if ($pendingClientTimestamp === null) {
  126. $pendingClientTimestamp = $timestamp;
  127. }
  128. continue;
  129. }
  130. if ($pendingClientTimestamp !== null && $timestamp >= $pendingClientTimestamp) {
  131. $responseTimes[] = $timestamp - $pendingClientTimestamp;
  132. $pendingClientTimestamp = null;
  133. }
  134. }
  135. if ($responseTimes === []) {
  136. return 0;
  137. }
  138. return (int) round(array_sum($responseTimes) / count($responseTimes));
  139. }
  140. private function calculateAverageGapByAuthor(array $messages, bool $isOperator): int
  141. {
  142. $timestamps = [];
  143. foreach ($messages as $message) {
  144. if ((bool) ($message['message_is_operator'] ?? false) !== $isOperator) {
  145. continue;
  146. }
  147. $timestamp = strtotime((string) ($message['message_sent_at'] ?? ''));
  148. if ($timestamp === false) {
  149. continue;
  150. }
  151. $timestamps[] = $timestamp;
  152. }
  153. if (count($timestamps) < 2) {
  154. return 0;
  155. }
  156. $gaps = [];
  157. for ($i = 1, $len = count($timestamps); $i < $len; $i++) {
  158. $gap = $timestamps[$i] - $timestamps[$i - 1];
  159. if ($gap >= 0) {
  160. $gaps[] = $gap;
  161. }
  162. }
  163. if ($gaps === []) {
  164. return 0;
  165. }
  166. return (int) round(array_sum($gaps) / count($gaps));
  167. }
  168. private function calculateTotalDurationSeconds(array $conversation, array $messages): int
  169. {
  170. if (count($messages) >= 2) {
  171. $first = strtotime((string) ($messages[0]['message_sent_at'] ?? ''));
  172. $last = strtotime((string) ($messages[count($messages) - 1]['message_sent_at'] ?? ''));
  173. if ($first !== false && $last !== false && $last >= $first) {
  174. return $last - $first;
  175. }
  176. }
  177. $startedAt = strtotime((string) ($conversation['conversation_started_at'] ?? ''));
  178. $lastMessageAt = strtotime((string) ($conversation['conversation_last_message_at'] ?? ''));
  179. if ($startedAt === false || $lastMessageAt === false || $lastMessageAt < $startedAt) {
  180. return 0;
  181. }
  182. return $lastMessageAt - $startedAt;
  183. }
  184. private function getLastMessageAuthor(array $messages): string
  185. {
  186. if ($messages === []) {
  187. return 'Cliente';
  188. }
  189. $lastMessage = $messages[count($messages) - 1];
  190. return (bool) ($lastMessage['message_is_operator'] ?? false) ? 'Operador' : 'Cliente';
  191. }
  192. private function hasTrailingConsecutiveMessages(array $messages): bool
  193. {
  194. $count = count($messages);
  195. if ($count < 2) {
  196. return false;
  197. }
  198. $last = (bool) ($messages[$count - 1]['message_is_operator'] ?? false);
  199. $previous = (bool) ($messages[$count - 2]['message_is_operator'] ?? false);
  200. return $last === $previous;
  201. }
  202. private function normalizeSentimentLabel(string $label): string
  203. {
  204. $normalized = trim($label);
  205. if ($normalized === '') {
  206. return 'NEUTRO';
  207. }
  208. return mb_strtoupper(str_replace('_', ' ', $normalized));
  209. }
  210. private function formatChannel(string $channel): string
  211. {
  212. $normalized = strtolower(trim($channel));
  213. if ($normalized === 'whatsapp') {
  214. return 'WhatsApp';
  215. }
  216. if ($normalized === '') {
  217. return '';
  218. }
  219. return ucfirst($normalized);
  220. }
  221. private function formatTime(string $dateTime): string
  222. {
  223. $timestamp = strtotime($dateTime);
  224. if ($timestamp === false) {
  225. return '00:00';
  226. }
  227. return date('H:i', $timestamp);
  228. }
  229. private function formatDate(string $dateTime): string
  230. {
  231. $timestamp = strtotime($dateTime);
  232. if ($timestamp === false) {
  233. return '';
  234. }
  235. return date('Y-m-d', $timestamp);
  236. }
  237. private function formatDuration(int $seconds): string
  238. {
  239. $seconds = max(0, $seconds);
  240. $minutes = intdiv($seconds, 60);
  241. $remainingSeconds = $seconds % 60;
  242. return str_pad((string) $minutes, 2, '0', STR_PAD_LEFT) . ':' . str_pad((string) $remainingSeconds, 2, '0', STR_PAD_LEFT);
  243. }
  244. }