FranchisorDashboardService.php 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. <?php
  2. namespace App\Services;
  3. use App\Enums\ReceivableStatus;
  4. use Illuminate\Support\Carbon;
  5. use Illuminate\Support\Facades\DB;
  6. /**
  7. * Agrega os indicadores da tela inicial (Dashboard) da visão do Franqueador.
  8. *
  9. * Premissas de cálculo:
  10. * - "Matrículas por Período" = quantidade de alunos cadastrados por mês (created_at),
  11. * nos últimos 6 meses.
  12. * - "Aniversariantes do Mês" = alunos cujo mês de nascimento é o mês corrente.
  13. * - "Tickets Abertos" = chamados de suporte com status 'in_progress'.
  14. * - "Tarefas Pendentes" = cartões do Kanban que ainda não foram concluídos.
  15. * - "Receita Geral" = total cobrado das franquias (franchisee_account_receives),
  16. * pago + em aberto; a legenda traz a quantidade de pagamentos pendentes.
  17. * - "Estoque Geral de Produtos" = soma das quantidades em estoque dos produtos.
  18. *
  19. * Todas as métricas por unidade respeitam o filtro `unitIds` (vazio = rede inteira);
  20. * o estoque de produtos é do catálogo do franqueador (não filtra por unidade).
  21. */
  22. class FranchisorDashboardService
  23. {
  24. private const SETTLED = [
  25. ReceivableStatus::PAID->value,
  26. ReceivableStatus::CANCELLED->value,
  27. ];
  28. public function overview(array $unitIds = []): array
  29. {
  30. return [
  31. 'enrollments' => $this->enrollments($unitIds),
  32. 'birthdays' => $this->birthdays($unitIds),
  33. 'open_tickets' => $this->openTickets($unitIds),
  34. 'pending_tasks' => $this->pendingTasks($unitIds),
  35. 'general_revenue' => $this->generalRevenue($unitIds),
  36. 'product_stock' => $this->productStock(),
  37. ];
  38. }
  39. /**
  40. * Matrículas (alunos cadastrados) por mês nos últimos 6 meses.
  41. */
  42. private function enrollments(array $unitIds): array
  43. {
  44. $start = Carbon::now()->startOfMonth()->subMonthsNoOverflow(5);
  45. // Agrupa por ano-mês de forma portável (independente do SGBD): puxa apenas
  46. // a data de cadastro no período e conta os meses em memória.
  47. $createdAt = DB::table('students')
  48. ->whereNull('deleted_at')
  49. ->where('created_at', '>=', $start)
  50. ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
  51. ->pluck('created_at');
  52. $counts = [];
  53. foreach ($createdAt as $date) {
  54. $ym = Carbon::parse($date)->format('Y-m');
  55. $counts[$ym] = ($counts[$ym] ?? 0) + 1;
  56. }
  57. $labels = [];
  58. $data = [];
  59. $months = ['JAN', 'FEV', 'MAR', 'ABR', 'MAI', 'JUN', 'JUL', 'AGO', 'SET', 'OUT', 'NOV', 'DEZ'];
  60. for ($i = 0; $i < 6; $i++) {
  61. $month = $start->copy()->addMonthsNoOverflow($i);
  62. $labels[] = $months[$month->month - 1];
  63. $data[] = (int) ($counts[$month->format('Y-m')] ?? 0);
  64. }
  65. return ['labels' => $labels, 'data' => $data];
  66. }
  67. /**
  68. * Aniversariantes do mês corrente (dia + nome).
  69. */
  70. private function birthdays(array $unitIds): array
  71. {
  72. return DB::table('students')
  73. ->whereNull('deleted_at')
  74. ->whereMonth('birth_date', Carbon::now()->month)
  75. ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
  76. ->get(['name', 'birth_date'])
  77. ->map(fn ($s) => [
  78. 'day' => (int) Carbon::parse($s->birth_date)->day,
  79. 'name' => $s->name,
  80. ])
  81. ->sortBy('day')
  82. ->values()
  83. ->all();
  84. }
  85. private function openTickets(array $unitIds): int
  86. {
  87. return (int) DB::table('support_tickets')
  88. ->whereNull('deleted_at')
  89. ->where('status', 'in_progress')
  90. ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
  91. ->count();
  92. }
  93. private function pendingTasks(array $unitIds): int
  94. {
  95. return (int) DB::table('kanbans')
  96. ->whereNull('deleted_at')
  97. ->where('phase', '!=', 'concluido')
  98. ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
  99. ->count();
  100. }
  101. /**
  102. * Receita Geral = total das cobranças às franquias (pago + em aberto),
  103. * com a contagem das que ainda estão pendentes.
  104. */
  105. private function generalRevenue(array $unitIds): array
  106. {
  107. $base = DB::table('franchisee_account_receives')
  108. ->whereNull('deleted_at')
  109. ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds));
  110. $value = (float) (clone $base)->sum('value');
  111. $pending = (int) (clone $base)
  112. ->whereNotIn('status', self::SETTLED)
  113. ->count();
  114. return ['value' => $value, 'pending_count' => $pending];
  115. }
  116. private function productStock(): int
  117. {
  118. return (int) DB::table('products')
  119. ->whereNull('deleted_at')
  120. ->sum('quantity');
  121. }
  122. }