|
@@ -0,0 +1,133 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+namespace App\Services;
|
|
|
|
|
+
|
|
|
|
|
+use App\Enums\ReceivableStatus;
|
|
|
|
|
+use Illuminate\Support\Carbon;
|
|
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Agrega os indicadores da tela inicial (Dashboard) da visão do Franqueador.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Premissas de cálculo:
|
|
|
|
|
+ * - "Matrículas por Período" = quantidade de alunos cadastrados por mês (created_at),
|
|
|
|
|
+ * nos últimos 6 meses.
|
|
|
|
|
+ * - "Aniversariantes do Mês" = alunos cujo mês de nascimento é o mês corrente.
|
|
|
|
|
+ * - "Tickets Abertos" = chamados de suporte com status 'in_progress'.
|
|
|
|
|
+ * - "Tarefas Pendentes" = cartões do Kanban que ainda não foram concluídos.
|
|
|
|
|
+ * - "Receita Geral" = total cobrado das franquias (franchisee_account_receives),
|
|
|
|
|
+ * pago + em aberto; a legenda traz a quantidade de pagamentos pendentes.
|
|
|
|
|
+ * - "Estoque Geral de Produtos" = soma das quantidades em estoque dos produtos.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Todas as métricas por unidade respeitam o filtro `unitIds` (vazio = rede inteira);
|
|
|
|
|
+ * o estoque de produtos é do catálogo do franqueador (não filtra por unidade).
|
|
|
|
|
+ */
|
|
|
|
|
+class FranchisorDashboardService
|
|
|
|
|
+{
|
|
|
|
|
+ private const SETTLED = [
|
|
|
|
|
+ ReceivableStatus::PAID->value,
|
|
|
|
|
+ ReceivableStatus::CANCELLED->value,
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ public function overview(array $unitIds = []): array
|
|
|
|
|
+ {
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'enrollments' => $this->enrollments($unitIds),
|
|
|
|
|
+ 'birthdays' => $this->birthdays($unitIds),
|
|
|
|
|
+ 'open_tickets' => $this->openTickets($unitIds),
|
|
|
|
|
+ 'pending_tasks' => $this->pendingTasks($unitIds),
|
|
|
|
|
+ 'general_revenue' => $this->generalRevenue($unitIds),
|
|
|
|
|
+ 'product_stock' => $this->productStock(),
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Matrículas (alunos cadastrados) por mês nos últimos 6 meses.
|
|
|
|
|
+ */
|
|
|
|
|
+ private function enrollments(array $unitIds): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $start = Carbon::now()->startOfMonth()->subMonthsNoOverflow(5);
|
|
|
|
|
+
|
|
|
|
|
+ $counts = DB::table('students')
|
|
|
|
|
+ ->whereNull('deleted_at')
|
|
|
|
|
+ ->where('created_at', '>=', $start)
|
|
|
|
|
+ ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
|
|
|
|
|
+ ->selectRaw("DATE_FORMAT(created_at, '%Y-%m') AS ym, COUNT(*) AS total")
|
|
|
|
|
+ ->groupBy('ym')
|
|
|
|
|
+ ->pluck('total', 'ym');
|
|
|
|
|
+
|
|
|
|
|
+ $labels = [];
|
|
|
|
|
+ $data = [];
|
|
|
|
|
+ $months = ['JAN', 'FEV', 'MAR', 'ABR', 'MAI', 'JUN', 'JUL', 'AGO', 'SET', 'OUT', 'NOV', 'DEZ'];
|
|
|
|
|
+
|
|
|
|
|
+ for ($i = 0; $i < 6; $i++) {
|
|
|
|
|
+ $month = $start->copy()->addMonthsNoOverflow($i);
|
|
|
|
|
+ $labels[] = $months[$month->month - 1];
|
|
|
|
|
+ $data[] = (int) ($counts[$month->format('Y-m')] ?? 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return ['labels' => $labels, 'data' => $data];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Aniversariantes do mês corrente (dia + nome).
|
|
|
|
|
+ */
|
|
|
|
|
+ private function birthdays(array $unitIds): array
|
|
|
|
|
+ {
|
|
|
|
|
+ return DB::table('students')
|
|
|
|
|
+ ->whereNull('deleted_at')
|
|
|
|
|
+ ->whereMonth('birth_date', Carbon::now()->month)
|
|
|
|
|
+ ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
|
|
|
|
|
+ ->orderByRaw('DAYOFMONTH(birth_date)')
|
|
|
|
|
+ ->get(['name', 'birth_date'])
|
|
|
|
|
+ ->map(fn ($s) => [
|
|
|
|
|
+ 'day' => (int) Carbon::parse($s->birth_date)->day,
|
|
|
|
|
+ 'name' => $s->name,
|
|
|
|
|
+ ])
|
|
|
|
|
+ ->all();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function openTickets(array $unitIds): int
|
|
|
|
|
+ {
|
|
|
|
|
+ return (int) DB::table('support_tickets')
|
|
|
|
|
+ ->whereNull('deleted_at')
|
|
|
|
|
+ ->where('status', 'in_progress')
|
|
|
|
|
+ ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
|
|
|
|
|
+ ->count();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function pendingTasks(array $unitIds): int
|
|
|
|
|
+ {
|
|
|
|
|
+ return (int) DB::table('kanbans')
|
|
|
|
|
+ ->whereNull('deleted_at')
|
|
|
|
|
+ ->where('phase', '!=', 'concluido')
|
|
|
|
|
+ ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
|
|
|
|
|
+ ->count();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Receita Geral = total das cobranças às franquias (pago + em aberto),
|
|
|
|
|
+ * com a contagem das que ainda estão pendentes.
|
|
|
|
|
+ */
|
|
|
|
|
+ private function generalRevenue(array $unitIds): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $base = DB::table('franchisee_account_receives')
|
|
|
|
|
+ ->whereNull('deleted_at')
|
|
|
|
|
+ ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds));
|
|
|
|
|
+
|
|
|
|
|
+ $value = (float) (clone $base)->sum('value');
|
|
|
|
|
+
|
|
|
|
|
+ $pending = (int) (clone $base)
|
|
|
|
|
+ ->whereNotIn('status', self::SETTLED)
|
|
|
|
|
+ ->count();
|
|
|
|
|
+
|
|
|
|
|
+ return ['value' => $value, 'pending_count' => $pending];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function productStock(): int
|
|
|
|
|
+ {
|
|
|
|
|
+ return (int) DB::table('products')
|
|
|
|
|
+ ->whereNull('deleted_at')
|
|
|
|
|
+ ->sum('quantity');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|