FranchisorDashboardService.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  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 __construct(
  29. private readonly TbrCalculationService $tbrCalculationService,
  30. ) {}
  31. public function overview(array $unitIds = [], ?Carbon $startDate = null, ?Carbon $endDate = null): array
  32. {
  33. $startDate ??= Carbon::now()->startOfMonth();
  34. $endDate ??= Carbon::now()->endOfMonth();
  35. return [
  36. 'enrollments' => $this->enrollments($unitIds),
  37. 'birthdays' => $this->birthdays($unitIds),
  38. 'open_tickets' => $this->openTickets($unitIds),
  39. 'pending_tasks' => $this->pendingTasks($unitIds),
  40. 'general_revenue' => $this->generalRevenue($unitIds),
  41. 'product_stock' => $this->productStock(),
  42. 'units_summary' => $this->unitsSummary($unitIds, $startDate, $endDate),
  43. ];
  44. }
  45. /**
  46. * Receita usa a mesma base do cálculo do TBR. O status é derivado da
  47. * existência de contrato de franquia vigente na data atual.
  48. */
  49. private function unitsSummary(array $unitIds, Carbon $startDate, Carbon $endDate): array
  50. {
  51. $units = DB::table('units')
  52. ->whereNull('deleted_at')
  53. ->when(!empty($unitIds), fn ($q) => $q->whereIn('id', $unitIds));
  54. $filteredUnitIds = (clone $units)->pluck('id')->map(fn ($id) => (int) $id)->all();
  55. $total = count($filteredUnitIds);
  56. $today = Carbon::today()->toDateString();
  57. $activeContracts = $total === 0
  58. ? 0
  59. : (int) DB::table('franchisee_contracts')
  60. ->whereNull('deleted_at')
  61. ->whereIn('unit_id', $filteredUnitIds)
  62. ->whereNotNull('start_date')
  63. ->where('start_date', '<=', $today)
  64. ->where(function ($q) use ($today) {
  65. $q->whereNull('end_date')->orWhere('end_date', '>=', $today);
  66. })
  67. ->distinct()
  68. ->count('unit_id');
  69. $revenue = $total === 0
  70. ? 0.0
  71. : $this->tbrCalculationService->resolveRevenueBetween($filteredUnitIds, $startDate, $endDate);
  72. return [
  73. 'total' => $total,
  74. 'revenue' => round($revenue, 2),
  75. 'active_contracts' => $activeContracts,
  76. 'without_active_contract' => max(0, $total - $activeContracts),
  77. ];
  78. }
  79. /**
  80. * Matrículas (alunos cadastrados) por mês nos últimos 6 meses.
  81. */
  82. private function enrollments(array $unitIds): array
  83. {
  84. $start = Carbon::now()->startOfMonth()->subMonthsNoOverflow(5);
  85. // Agrupa por ano-mês de forma portável (independente do SGBD): puxa apenas
  86. // a data de cadastro no período e conta os meses em memória.
  87. $createdAt = DB::table('students')
  88. ->whereNull('deleted_at')
  89. ->where('created_at', '>=', $start)
  90. ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
  91. ->pluck('created_at');
  92. $counts = [];
  93. foreach ($createdAt as $date) {
  94. $ym = Carbon::parse($date)->format('Y-m');
  95. $counts[$ym] = ($counts[$ym] ?? 0) + 1;
  96. }
  97. $labels = [];
  98. $data = [];
  99. $months = ['JAN', 'FEV', 'MAR', 'ABR', 'MAI', 'JUN', 'JUL', 'AGO', 'SET', 'OUT', 'NOV', 'DEZ'];
  100. for ($i = 0; $i < 6; $i++) {
  101. $month = $start->copy()->addMonthsNoOverflow($i);
  102. $labels[] = $months[$month->month - 1];
  103. $data[] = (int) ($counts[$month->format('Y-m')] ?? 0);
  104. }
  105. return ['labels' => $labels, 'data' => $data];
  106. }
  107. /**
  108. * Aniversariantes do mês corrente (dia + nome).
  109. */
  110. private function birthdays(array $unitIds): array
  111. {
  112. return DB::table('students')
  113. ->whereNull('deleted_at')
  114. ->whereMonth('birth_date', Carbon::now()->month)
  115. ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
  116. ->get(['name', 'birth_date'])
  117. ->map(fn ($s) => [
  118. 'day' => (int) Carbon::parse($s->birth_date)->day,
  119. 'name' => $s->name,
  120. ])
  121. ->sortBy('day')
  122. ->values()
  123. ->all();
  124. }
  125. private function openTickets(array $unitIds): int
  126. {
  127. return (int) DB::table('support_tickets')
  128. ->whereNull('deleted_at')
  129. ->where('status', 'in_progress')
  130. ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
  131. ->count();
  132. }
  133. private function pendingTasks(array $unitIds): int
  134. {
  135. return (int) DB::table('kanbans')
  136. ->whereNull('deleted_at')
  137. ->where('phase', '!=', 'concluido')
  138. ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
  139. ->count();
  140. }
  141. /**
  142. * Receita Geral = total das cobranças às franquias (pago + em aberto),
  143. * com a contagem das que ainda estão pendentes.
  144. */
  145. private function generalRevenue(array $unitIds): array
  146. {
  147. $base = DB::table('franchisee_account_receives')
  148. ->whereNull('deleted_at')
  149. ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds));
  150. $value = (float) (clone $base)->sum('value');
  151. $pending = (int) (clone $base)
  152. ->whereNotIn('status', self::SETTLED)
  153. ->count();
  154. return ['value' => $value, 'pending_count' => $pending];
  155. }
  156. private function productStock(): int
  157. {
  158. return (int) DB::table('products')
  159. ->whereNull('deleted_at')
  160. ->sum('quantity');
  161. }
  162. }