InteractionsModel.php 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. <?php
  2. namespace Models;
  3. use Libs\Database;
  4. class InteractionsModel
  5. {
  6. private \PDO $pdo;
  7. public function __construct()
  8. {
  9. $this->pdo = Database::pdo();
  10. }
  11. public function getInteractionsData(int $companyId, string $userEmail, array $queryParams): array
  12. {
  13. $filters = $this->normalizeFilters($queryParams);
  14. $myOperatorId = $this->getOperatorIdByUserEmail($companyId, $userEmail);
  15. [$whereSql, $params] = $this->buildWhereClause($companyId, $filters, $myOperatorId);
  16. $total = $this->getTotalCount($whereSql, $params);
  17. $items = $this->getItems($whereSql, $params, $filters['page'], $filters['per_page']);
  18. return [
  19. 'items' => $items,
  20. 'pagination' => [
  21. 'page' => $filters['page'],
  22. 'per_page' => $filters['per_page'],
  23. 'total' => $total,
  24. 'total_pages' => $filters['per_page'] > 0 ? (int) ceil($total / $filters['per_page']) : 0,
  25. ],
  26. ];
  27. }
  28. private function getOperatorIdByUserEmail(int $companyId, string $userEmail): ?int
  29. {
  30. $normalizedEmail = mb_strtolower(trim($userEmail));
  31. if ($normalizedEmail === '') {
  32. return null;
  33. }
  34. $stmt = $this->pdo->prepare(
  35. "SELECT operator_id
  36. FROM operator
  37. WHERE company_id = :company_id
  38. AND operator_deleted_at = 'infinity'
  39. AND lower(operator_email) = :email
  40. LIMIT 1"
  41. );
  42. $stmt->execute([
  43. 'company_id' => $companyId,
  44. 'email' => $normalizedEmail,
  45. ]);
  46. $operatorId = $stmt->fetchColumn();
  47. return $operatorId === false ? null : (int) $operatorId;
  48. }
  49. private function normalizeFilters(array $queryParams): array
  50. {
  51. $page = max(1, (int) ($queryParams['page'] ?? 1));
  52. $perPage = (int) ($queryParams['per_page'] ?? 20);
  53. $perPage = max(1, min(100, $perPage));
  54. $filter = strtolower(trim((string) ($queryParams['filter'] ?? 'all')));
  55. if (!in_array($filter, ['all', 'my_clients', 'new', 'unfinished'], true)) {
  56. $filter = 'all';
  57. }
  58. $sentiment = strtolower(trim((string) ($queryParams['sentiment'] ?? 'all')));
  59. if (!in_array($sentiment, ['all', 'positive', 'neutral', 'negative'], true)) {
  60. $sentiment = 'all';
  61. }
  62. return [
  63. 'page' => $page,
  64. 'per_page' => $perPage,
  65. 'search' => trim((string) ($queryParams['search'] ?? '')),
  66. 'filter' => $filter,
  67. 'sentiment' => $sentiment,
  68. 'operator_id' => max(0, (int) ($queryParams['operator_id'] ?? 0)),
  69. ];
  70. }
  71. private function buildWhereClause(int $companyId, array $filters, ?int $myOperatorId): array
  72. {
  73. $where = [
  74. "c.company_id = :company_id",
  75. "c.conversation_deleted_at = 'infinity'",
  76. "cl.client_deleted_at = 'infinity'",
  77. ];
  78. $params = ['company_id' => $companyId];
  79. if ($filters['search'] !== '') {
  80. $where[] = '(cl.client_name ILIKE :search OR cl.client_phone ILIKE :search OR o.operator_name ILIKE :search OR c.conversation_last_message_preview ILIKE :search)';
  81. $params['search'] = '%' . $filters['search'] . '%';
  82. }
  83. if ($filters['operator_id'] > 0) {
  84. $where[] = 'c.operator_id = :operator_id';
  85. $params['operator_id'] = $filters['operator_id'];
  86. }
  87. if ($filters['filter'] === 'unfinished') {
  88. $where[] = "lower(c.conversation_status) <> 'closed'";
  89. }
  90. if ($filters['filter'] === 'new') {
  91. $where[] = "c.conversation_started_at >= NOW() - INTERVAL '24 hours'";
  92. }
  93. if ($filters['filter'] === 'my_clients') {
  94. if ($myOperatorId !== null) {
  95. $where[] = 'c.operator_id = :my_operator_id';
  96. $params['my_operator_id'] = $myOperatorId;
  97. } else {
  98. $where[] = '1 = 0';
  99. }
  100. }
  101. if ($filters['sentiment'] !== 'all') {
  102. $where[] = $this->getSentimentWhereClause('ca', $filters['sentiment']);
  103. }
  104. return [implode("\n AND ", $where), $params];
  105. }
  106. private function getTotalCount(string $whereSql, array $params): int
  107. {
  108. $stmt = $this->pdo->prepare(
  109. "SELECT COUNT(*)
  110. FROM conversation c
  111. INNER JOIN client cl ON cl.client_id = c.client_id
  112. LEFT JOIN operator o
  113. ON o.operator_id = c.operator_id
  114. AND o.operator_deleted_at = 'infinity'
  115. LEFT JOIN conversation_analysis ca
  116. ON ca.conversation_id = c.conversation_id
  117. AND ca.conversation_analysis_deleted_at = 'infinity'
  118. WHERE {$whereSql}"
  119. );
  120. $stmt->execute($params);
  121. return (int) $stmt->fetchColumn();
  122. }
  123. private function getItems(string $whereSql, array $params, int $page, int $perPage): array
  124. {
  125. $offset = ($page - 1) * $perPage;
  126. $params['limit'] = $perPage;
  127. $params['offset'] = $offset;
  128. $stmt = $this->pdo->prepare(
  129. "SELECT
  130. c.conversation_id,
  131. cl.client_phone,
  132. o.operator_name,
  133. COALESCE(ca.conversation_analysis_sentiment, c.conversation_status) AS sentiment_label,
  134. COALESCE(ca.conversation_analysis_sentiment_score, 0) AS sentiment_score,
  135. COALESCE(ca.conversation_analysis_aspect, '') AS aspect,
  136. COALESCE(ca.conversation_analysis_sub_aspect, '') AS sub_aspect,
  137. c.conversation_last_message_at
  138. FROM conversation c
  139. INNER JOIN client cl ON cl.client_id = c.client_id
  140. LEFT JOIN operator o
  141. ON o.operator_id = c.operator_id
  142. AND o.operator_deleted_at = 'infinity'
  143. LEFT JOIN conversation_analysis ca
  144. ON ca.conversation_id = c.conversation_id
  145. AND ca.conversation_analysis_deleted_at = 'infinity'
  146. WHERE {$whereSql}
  147. ORDER BY c.conversation_last_message_at DESC, c.conversation_id DESC
  148. LIMIT :limit OFFSET :offset"
  149. );
  150. foreach ($params as $key => $value) {
  151. if (in_array($key, ['limit', 'offset', 'operator_id', 'my_operator_id', 'company_id'], true)) {
  152. $stmt->bindValue(':' . $key, (int) $value, \PDO::PARAM_INT);
  153. continue;
  154. }
  155. $stmt->bindValue(':' . $key, $value);
  156. }
  157. $stmt->execute();
  158. $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
  159. return array_map(function (array $row): array {
  160. return [
  161. 'conversationId' => (int) $row['conversation_id'],
  162. 'client' => $row['client_phone'] ?? '',
  163. 'agent' => $row['operator_name'] ?? '',
  164. 'sentiment' => $this->normalizeSentimentLabel((string) ($row['sentiment_label'] ?? '')),
  165. 'score' => round((float) ($row['sentiment_score'] ?? 0), 2),
  166. 'aspect' => $row['aspect'] ?? '',
  167. 'subaspect' => $row['sub_aspect'] ?? '',
  168. 'datetime' => $this->formatIsoDateTime($row['conversation_last_message_at'] ?? null),
  169. ];
  170. }, $rows);
  171. }
  172. private function getSentimentWhereClause(string $analysisAlias, string $sentiment): string
  173. {
  174. if ($sentiment === 'positive') {
  175. return "(
  176. lower(COALESCE({$analysisAlias}.conversation_analysis_sentiment, '')) IN ('positive', 'positivo')
  177. OR {$analysisAlias}.conversation_analysis_sentiment_score >= 0.15
  178. )";
  179. }
  180. if ($sentiment === 'negative') {
  181. return "(
  182. lower(COALESCE({$analysisAlias}.conversation_analysis_sentiment, '')) IN ('negative', 'negativo')
  183. OR {$analysisAlias}.conversation_analysis_sentiment_score <= -0.15
  184. )";
  185. }
  186. return "(
  187. {$analysisAlias}.conversation_id IS NOT NULL
  188. AND lower(COALESCE({$analysisAlias}.conversation_analysis_sentiment, '')) NOT IN ('positive', 'positivo', 'negative', 'negativo')
  189. AND {$analysisAlias}.conversation_analysis_sentiment_score > -0.15
  190. AND {$analysisAlias}.conversation_analysis_sentiment_score < 0.15
  191. )";
  192. }
  193. private function normalizeSentimentLabel(string $label): string
  194. {
  195. $normalized = trim($label);
  196. if ($normalized === '') {
  197. return 'NEUTRO';
  198. }
  199. return mb_strtoupper(str_replace('_', ' ', $normalized));
  200. }
  201. private function formatIsoDateTime(?string $dateTime): ?string
  202. {
  203. if (!$dateTime) {
  204. return null;
  205. }
  206. $timestamp = strtotime($dateTime);
  207. if ($timestamp === false) {
  208. return null;
  209. }
  210. return gmdate('Y-m-d\TH:i:s\Z', $timestamp);
  211. }
  212. }