InteractionDetailsModel.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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. LEFT 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 = FALSE
  85. AND message_hidden = FALSE
  86. AND message_is_event = FALSE
  87. ORDER BY message_sent_at ASC, message_id ASC"
  88. );
  89. $stmt->execute(['conversation_id' => $conversationId]);
  90. return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
  91. }
  92. private function buildReport(array $conversation, array $messages): array
  93. {
  94. $avgResponseSeconds = $this->calculateAverageResponseSeconds($messages);
  95. $avgAgentSeconds = $this->calculateAverageGapByAuthor($messages, true);
  96. $avgClientSeconds = $this->calculateAverageGapByAuthor($messages, false);
  97. $totalDurationSeconds = $this->calculateTotalDurationSeconds($conversation, $messages);
  98. $lastMessageAuthor = $this->getLastMessageAuthor($messages);
  99. $consecutiveMessages = $this->hasTrailingConsecutiveMessages($messages);
  100. return [
  101. 'avgResponse' => $this->formatDuration($avgResponseSeconds),
  102. 'totalDuration' => $this->formatDuration($totalDurationSeconds),
  103. 'avgAgent' => $this->formatDuration($avgAgentSeconds),
  104. 'avgClient' => $this->formatDuration($avgClientSeconds),
  105. 'mainAspect' => $conversation['conversation_analysis_aspect'] ?? '',
  106. 'subAspect' => $conversation['conversation_analysis_sub_aspect'] ?? '',
  107. 'lastMessageAuthor' => $lastMessageAuthor,
  108. 'consecutiveMessages' => $consecutiveMessages,
  109. 'sentiment' => $this->normalizeSentimentLabel((string) ($conversation['conversation_analysis_sentiment'] ?? '')),
  110. 'score' => round((float) ($conversation['conversation_analysis_sentiment_score'] ?? 0), 2),
  111. ];
  112. }
  113. private function calculateAverageResponseSeconds(array $messages): int
  114. {
  115. $responseTimes = [];
  116. $pendingClientTimestamp = null;
  117. foreach ($messages as $message) {
  118. $timestamp = strtotime((string) ($message['message_sent_at'] ?? ''));
  119. if ($timestamp === false) {
  120. continue;
  121. }
  122. $isOperator = (bool) ($message['message_is_operator'] ?? false);
  123. if (!$isOperator) {
  124. if ($pendingClientTimestamp === null) {
  125. $pendingClientTimestamp = $timestamp;
  126. }
  127. continue;
  128. }
  129. if ($pendingClientTimestamp !== null && $timestamp >= $pendingClientTimestamp) {
  130. $responseTimes[] = $timestamp - $pendingClientTimestamp;
  131. $pendingClientTimestamp = null;
  132. }
  133. }
  134. if ($responseTimes === []) {
  135. return 0;
  136. }
  137. return (int) round(array_sum($responseTimes) / count($responseTimes));
  138. }
  139. private function calculateAverageGapByAuthor(array $messages, bool $isOperator): int
  140. {
  141. $timestamps = [];
  142. foreach ($messages as $message) {
  143. if ((bool) ($message['message_is_operator'] ?? false) !== $isOperator) {
  144. continue;
  145. }
  146. $timestamp = strtotime((string) ($message['message_sent_at'] ?? ''));
  147. if ($timestamp === false) {
  148. continue;
  149. }
  150. $timestamps[] = $timestamp;
  151. }
  152. if (count($timestamps) < 2) {
  153. return 0;
  154. }
  155. $gaps = [];
  156. for ($i = 1, $len = count($timestamps); $i < $len; $i++) {
  157. $gap = $timestamps[$i] - $timestamps[$i - 1];
  158. if ($gap >= 0) {
  159. $gaps[] = $gap;
  160. }
  161. }
  162. if ($gaps === []) {
  163. return 0;
  164. }
  165. return (int) round(array_sum($gaps) / count($gaps));
  166. }
  167. private function calculateTotalDurationSeconds(array $conversation, array $messages): int
  168. {
  169. if (count($messages) >= 2) {
  170. $first = strtotime((string) ($messages[0]['message_sent_at'] ?? ''));
  171. $last = strtotime((string) ($messages[count($messages) - 1]['message_sent_at'] ?? ''));
  172. if ($first !== false && $last !== false && $last >= $first) {
  173. return $last - $first;
  174. }
  175. }
  176. $startedAt = strtotime((string) ($conversation['conversation_started_at'] ?? ''));
  177. $lastMessageAt = strtotime((string) ($conversation['conversation_last_message_at'] ?? ''));
  178. if ($startedAt === false || $lastMessageAt === false || $lastMessageAt < $startedAt) {
  179. return 0;
  180. }
  181. return $lastMessageAt - $startedAt;
  182. }
  183. private function getLastMessageAuthor(array $messages): string
  184. {
  185. if ($messages === []) {
  186. return 'Cliente';
  187. }
  188. $lastMessage = $messages[count($messages) - 1];
  189. return (bool) ($lastMessage['message_is_operator'] ?? false) ? 'Operador' : 'Cliente';
  190. }
  191. private function hasTrailingConsecutiveMessages(array $messages): bool
  192. {
  193. $count = count($messages);
  194. if ($count < 2) {
  195. return false;
  196. }
  197. $last = (bool) ($messages[$count - 1]['message_is_operator'] ?? false);
  198. $previous = (bool) ($messages[$count - 2]['message_is_operator'] ?? false);
  199. return $last === $previous;
  200. }
  201. private function normalizeSentimentLabel(string $label): string
  202. {
  203. $normalized = trim($label);
  204. if ($normalized === '') {
  205. return 'NEUTRO';
  206. }
  207. return mb_strtoupper(str_replace('_', ' ', $normalized));
  208. }
  209. private function formatChannel(string $channel): string
  210. {
  211. $normalized = strtolower(trim($channel));
  212. if ($normalized === 'whatsapp') {
  213. return 'WhatsApp';
  214. }
  215. if ($normalized === '') {
  216. return '';
  217. }
  218. return ucfirst($normalized);
  219. }
  220. private function formatTime(string $dateTime): string
  221. {
  222. $timestamp = strtotime($dateTime);
  223. if ($timestamp === false) {
  224. return '00:00';
  225. }
  226. return date('H:i', $timestamp);
  227. }
  228. private function formatDate(string $dateTime): string
  229. {
  230. $timestamp = strtotime($dateTime);
  231. if ($timestamp === false) {
  232. return '';
  233. }
  234. return date('Y-m-d', $timestamp);
  235. }
  236. private function formatDuration(int $seconds): string
  237. {
  238. $seconds = max(0, $seconds);
  239. $minutes = intdiv($seconds, 60);
  240. $remainingSeconds = $seconds % 60;
  241. return str_pad((string) $minutes, 2, '0', STR_PAD_LEFT) . ':' . str_pad((string) $remainingSeconds, 2, '0', STR_PAD_LEFT);
  242. }
  243. }