Explorar el Código

feat(franchisor-dashboard): add overview endpoint and service for franchisor dashboard metrics

ebagabee hace 1 mes
padre
commit
af6a4ac2e7

+ 19 - 0
app/Http/Controllers/FranchisorDashboardController.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Services\FranchisorDashboardService;
+use Illuminate\Http\JsonResponse;
+
+class FranchisorDashboardController extends Controller
+{
+    public function __construct(
+        protected FranchisorDashboardService $service,
+    ) {}
+
+    public function overview(): JsonResponse
+    {
+        $unitIds = array_filter((array) request()->input('unit_ids', []));
+        return $this->successResponse(payload: $this->service->overview($unitIds));
+    }
+}

+ 1 - 0
app/Http/Resources/TreasuryAccountResource.php

@@ -28,6 +28,7 @@ public function toArray(Request $request): array
             'bank_type_account' => $this->bank_type_account,
             'active' => (bool) $this->active,
             'balance' => (float) ($this->balance ?? 0),
+            'balance_previous' => (float) ($this->balance_previous ?? 0),
             'created_at' => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
             'updated_at' => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
         ];

+ 11 - 6
app/Services/FinancialDashboardService.php

@@ -13,7 +13,10 @@
  * - "Receita de Mensalidade" = parcelas de aluno pagas no mês (paid_value).
  * - "TBR Total" = TBR faturada à franquia no mês (franchisee_account_receives.value).
  * - "Fundo de Marketing" = soma do fundo configurado por unidade (unit_financials).
+ * - "Contas a Receber" = TBR pendente das franquias (franchisee_account_receives).
  * - "Saldo Bancário" = entradas (to_account) menos saídas (from_account) por conta.
+ * - "Desempenho por Franquia": a "Despesa" de cada unidade é o total que ela deve
+ *   pagar ao franqueador (TBR) acumulado até o mês corrente (due_date <= fim do mês).
  */
 class FinancialDashboardService
 {
@@ -94,10 +97,11 @@ private function pendingPayables(): array
 
     private function pendingReceivables(): array
     {
-        $row = DB::table('financial_account_receives')
+        // Recebíveis do franqueador = TBR cobrada das franquias ainda em aberto.
+        $row = DB::table('franchisee_account_receives')
             ->whereNull('deleted_at')
             ->whereNotIn('status', self::SETTLED)
-            ->selectRaw('COALESCE(SUM(price + fine + fees), 0) AS total, COUNT(*) AS qty')
+            ->selectRaw('COALESCE(SUM(value + fees - discount - paid_value), 0) AS total, COUNT(*) AS qty')
             ->first();
 
         return ['value' => (float) $row->total, 'count' => (int) $row->qty];
@@ -148,12 +152,13 @@ private function franchisePerformance(Carbon $start, Carbon $end): array
             ->selectRaw('unit_id, SUM(paid_value) AS total')
             ->pluck('total', 'unit_id');
 
-        $expense = DB::table('financial_account_payables')
+        // Despesa = total que a unidade deve pagar ao franqueador (TBR) acumulado
+        // até o mês corrente (toda cobrança com vencimento até o fim do mês).
+        $expense = DB::table('franchisee_account_receives')
             ->whereNull('deleted_at')
-            ->where('status', ReceivableStatus::PAID->value)
-            ->whereBetween('payment_date', [$start, $end])
+            ->where('due_date', '<=', $end)
             ->groupBy('unit_id')
-            ->selectRaw('unit_id, SUM(price + fine + fees) AS total')
+            ->selectRaw('unit_id, SUM(value) AS total')
             ->pluck('total', 'unit_id');
 
         return DB::table('units')

+ 133 - 0
app/Services/FranchisorDashboardService.php

@@ -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');
+    }
+}

+ 23 - 1
app/Services/TreasuryAccountService.php

@@ -5,6 +5,7 @@
 use App\Models\TreasuryAccount;
 use App\Models\TreasuryLaunch;
 use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Carbon;
 
 class TreasuryAccountService
 {
@@ -31,8 +32,12 @@ public function getAll(?int $unitId = null): Collection
     }
 
     /**
-     * Calcula o saldo de cada conta em duas agregações:
+     * Calcula o saldo de cada conta:
      * saldo = soma das entradas (to_account) - soma das saídas (from_account).
+     *
+     * Também expõe `balance_previous` = saldo no fim do mês anterior (saldo atual
+     * menos o fluxo líquido do mês corrente), permitindo ao frontend exibir a
+     * variação percentual "vs o mês anterior" em cada card.
      */
     private function attachBalances(Collection $accounts): void
     {
@@ -52,9 +57,26 @@ private function attachBalances(Collection $accounts): void
             ->selectRaw('from_account_id, SUM(amount) as total')
             ->pluck('total', 'from_account_id');
 
+        $monthStart = Carbon::now()->startOfMonth();
+
+        $incomingMonth = TreasuryLaunch::whereIn('to_account_id', $ids)
+            ->where('launch_date', '>=', $monthStart)
+            ->groupBy('to_account_id')
+            ->selectRaw('to_account_id, SUM(amount) as total')
+            ->pluck('total', 'to_account_id');
+
+        $outgoingMonth = TreasuryLaunch::whereIn('from_account_id', $ids)
+            ->where('launch_date', '>=', $monthStart)
+            ->groupBy('from_account_id')
+            ->selectRaw('from_account_id, SUM(amount) as total')
+            ->pluck('total', 'from_account_id');
+
         foreach ($accounts as $account) {
             $balance = (float) ($incoming[$account->id] ?? 0) - (float) ($outgoing[$account->id] ?? 0);
+            $monthNet = (float) ($incomingMonth[$account->id] ?? 0) - (float) ($outgoingMonth[$account->id] ?? 0);
+
             $account->setAttribute('balance', $balance);
+            $account->setAttribute('balance_previous', $balance - $monthNet);
         }
     }
 

+ 8 - 0
routes/authRoutes/franchisor_dashboard.php

@@ -0,0 +1,8 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use App\Http\Controllers\FranchisorDashboardController;
+
+Route::controller(FranchisorDashboardController::class)->prefix('franchisor-dashboard')->group(function () {
+    Route::get('/overview', 'overview')->middleware('permission:student,view');
+});