EvolutionOverviewModel.php 9.3 KB

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