EvolutionOverviewModel.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. <?php
  2. namespace Models;
  3. use Libs\Database;
  4. class EvolutionOverviewModel
  5. {
  6. private \PDO $pdo;
  7. public function __construct()
  8. {
  9. $this->pdo = Database::pdo();
  10. }
  11. public function getOverviewData(int $companyId, array $queryParams = []): array
  12. {
  13. $anchorDate = $this->getAnchorDate($companyId);
  14. $range = $this->resolveRange($anchorDate);
  15. $sentimentSeries = $this->getSentimentSeries($companyId, $range);
  16. $playbooksSeries = $this->getPlaybooksSeries($companyId, $range);
  17. $playbooksTotals = $this->buildPlaybooksTotals($playbooksSeries);
  18. $kpiSnapshots = $this->getRecentKpiSnapshots($companyId, $anchorDate);
  19. $recentSentimentScores = $this->getRecentSentimentScores($companyId, $anchorDate);
  20. return [
  21. 'kpis' => $this->buildKpis($kpiSnapshots, $recentSentimentScores, $playbooksTotals),
  22. 'sentimentSeries' => $sentimentSeries,
  23. 'playbooksSeries' => $playbooksSeries,
  24. 'playbooksTotals' => $playbooksTotals,
  25. ];
  26. }
  27. private function getAnchorDate(int $companyId): \DateTimeImmutable
  28. {
  29. $stmt = $this->pdo->prepare(
  30. "SELECT MAX(snapshot_date)
  31. FROM (
  32. SELECT MAX(evolution_snapshot_date)::date AS snapshot_date
  33. FROM sentiment_evolution
  34. WHERE company_id = :company_id
  35. UNION ALL
  36. SELECT MAX(playbook_snapshot_date)::date AS snapshot_date
  37. FROM playbooks_monitor
  38. WHERE company_id = :company_id
  39. UNION ALL
  40. SELECT MAX(kpi_snapshot_date)::date AS snapshot_date
  41. FROM kpi_snapshot
  42. WHERE company_id = :company_id
  43. ) snapshots"
  44. );
  45. $stmt->execute(['company_id' => $companyId]);
  46. $anchorDate = $stmt->fetchColumn();
  47. if (!is_string($anchorDate) || $anchorDate === '') {
  48. return new \DateTimeImmutable('today');
  49. }
  50. return new \DateTimeImmutable($anchorDate);
  51. }
  52. private function resolveRange(\DateTimeImmutable $anchorDate): array
  53. {
  54. $startDate = $anchorDate->modify('-6 days');
  55. return [
  56. 'start_date' => $startDate->format('Y-m-d'),
  57. 'end_date' => $anchorDate->format('Y-m-d'),
  58. ];
  59. }
  60. private function getSentimentSeries(int $companyId, array $range): array
  61. {
  62. $stmt = $this->pdo->prepare(
  63. "SELECT
  64. evolution_snapshot_date,
  65. evolution_sentiment_score
  66. FROM sentiment_evolution
  67. WHERE company_id = :company_id
  68. AND evolution_snapshot_date >= :start_date
  69. AND evolution_snapshot_date <= :end_date
  70. ORDER BY evolution_snapshot_date ASC"
  71. );
  72. $stmt->execute([
  73. 'company_id' => $companyId,
  74. 'start_date' => $range['start_date'],
  75. 'end_date' => $range['end_date'],
  76. ]);
  77. $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
  78. $mapped = [];
  79. foreach ($rows as $row) {
  80. $mapped[$row['evolution_snapshot_date']] = (float) ($row['evolution_sentiment_score'] ?? 0);
  81. }
  82. $items = [];
  83. $current = new \DateTimeImmutable($range['start_date']);
  84. $end = new \DateTimeImmutable($range['end_date']);
  85. while ($current <= $end) {
  86. $date = $current->format('Y-m-d');
  87. $items[] = [
  88. 'date' => $date,
  89. 'value' => (float) ($mapped[$date] ?? 0),
  90. ];
  91. $current = $current->modify('+1 day');
  92. }
  93. return $items;
  94. }
  95. private function getPlaybooksSeries(int $companyId, array $range): array
  96. {
  97. $stmt = $this->pdo->prepare(
  98. "SELECT
  99. playbook_snapshot_date,
  100. playbook_new_detected,
  101. playbook_converted
  102. FROM playbooks_monitor
  103. WHERE company_id = :company_id
  104. AND playbook_snapshot_date >= :start_date
  105. AND playbook_snapshot_date <= :end_date
  106. ORDER BY playbook_snapshot_date ASC"
  107. );
  108. $stmt->execute([
  109. 'company_id' => $companyId,
  110. 'start_date' => $range['start_date'],
  111. 'end_date' => $range['end_date'],
  112. ]);
  113. $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
  114. $mapped = [];
  115. foreach ($rows as $row) {
  116. $mapped[$row['playbook_snapshot_date']] = [
  117. 'novos' => (int) ($row['playbook_new_detected'] ?? 0),
  118. 'convertidos' => (int) ($row['playbook_converted'] ?? 0),
  119. ];
  120. }
  121. $items = [];
  122. $current = new \DateTimeImmutable($range['start_date']);
  123. $end = new \DateTimeImmutable($range['end_date']);
  124. while ($current <= $end) {
  125. $date = $current->format('Y-m-d');
  126. $items[] = [
  127. 'date' => $date,
  128. 'novos' => (int) (($mapped[$date]['novos'] ?? 0)),
  129. 'convertidos' => (int) (($mapped[$date]['convertidos'] ?? 0)),
  130. ];
  131. $current = $current->modify('+1 day');
  132. }
  133. return $items;
  134. }
  135. private function buildPlaybooksTotals(array $playbooksSeries): array
  136. {
  137. $novos = 0;
  138. $convertidos = 0;
  139. foreach ($playbooksSeries as $item) {
  140. $novos += (int) ($item['novos'] ?? 0);
  141. $convertidos += (int) ($item['convertidos'] ?? 0);
  142. }
  143. return [
  144. 'novos' => $novos,
  145. 'convertidos' => $convertidos,
  146. ];
  147. }
  148. private function getRecentKpiSnapshots(int $companyId, \DateTimeImmutable $anchorDate): array
  149. {
  150. $stmt = $this->pdo->prepare(
  151. "SELECT
  152. kpi_snapshot_date,
  153. kpi_current_sales,
  154. kpi_avg_ticket,
  155. kpi_lifetime_at_risk
  156. FROM kpi_snapshot
  157. WHERE company_id = :company_id
  158. AND kpi_snapshot_date <= :anchor_date
  159. ORDER BY kpi_snapshot_date DESC
  160. LIMIT 2"
  161. );
  162. $stmt->execute([
  163. 'company_id' => $companyId,
  164. 'anchor_date' => $anchorDate->format('Y-m-d'),
  165. ]);
  166. return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
  167. }
  168. private function getRecentSentimentScores(int $companyId, \DateTimeImmutable $anchorDate): array
  169. {
  170. $stmt = $this->pdo->prepare(
  171. "SELECT
  172. evolution_snapshot_date,
  173. evolution_sentiment_score
  174. FROM sentiment_evolution
  175. WHERE company_id = :company_id
  176. AND evolution_snapshot_date <= :anchor_date
  177. ORDER BY evolution_snapshot_date DESC
  178. LIMIT 2"
  179. );
  180. $stmt->execute([
  181. 'company_id' => $companyId,
  182. 'anchor_date' => $anchorDate->format('Y-m-d'),
  183. ]);
  184. return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
  185. }
  186. private function buildKpis(array $kpiSnapshots, array $recentSentimentScores, array $playbooksTotals): array
  187. {
  188. $latestKpi = $kpiSnapshots[0] ?? [];
  189. $previousKpi = $kpiSnapshots[1] ?? $latestKpi;
  190. $latestAtRisk = (float) ($latestKpi['kpi_lifetime_at_risk'] ?? 0);
  191. $previousAtRisk = (float) ($previousKpi['kpi_lifetime_at_risk'] ?? $latestAtRisk);
  192. $currentSales = (float) ($latestKpi['kpi_current_sales'] ?? 0);
  193. $latestSentiment = isset($recentSentimentScores[0]['evolution_sentiment_score'])
  194. ? (float) $recentSentimentScores[0]['evolution_sentiment_score']
  195. : 0.0;
  196. $previousSentiment = isset($recentSentimentScores[1]['evolution_sentiment_score'])
  197. ? (float) $recentSentimentScores[1]['evolution_sentiment_score']
  198. : $latestSentiment;
  199. $latestScore = $this->normalizeSentimentScore($latestSentiment);
  200. $previousScore = $this->normalizeSentimentScore($previousSentiment);
  201. if ($previousScore === 0.0) {
  202. $evolutionRate = $latestScore > 0 ? 100.0 : 0.0;
  203. } else {
  204. $evolutionRate = (($latestScore - $previousScore) / abs($previousScore)) * 100;
  205. }
  206. $novos = (int) ($playbooksTotals['novos'] ?? 0);
  207. $convertidos = (int) ($playbooksTotals['convertidos'] ?? 0);
  208. return [
  209. 'churnEvitado' => round(max(0, $previousAtRisk - $latestAtRisk), 2),
  210. 'roiUpsell' => round($latestAtRisk > 0 ? ($currentSales / $latestAtRisk) : 0, 1),
  211. 'scoreMedio' => (int) round($latestScore),
  212. 'taxaEvolucao' => round($evolutionRate, 1),
  213. 'conversaoEmocao' => $novos > 0 ? (int) round(($convertidos / $novos) * 100) : 0,
  214. ];
  215. }
  216. private function normalizeSentimentScore(float $score): float
  217. {
  218. $normalized = (($score + 1) / 2) * 100;
  219. return max(0, min(100, $normalized));
  220. }
  221. }