From 55489d599eeaa37947453aa0d11df0e7213ffe12f0231a9631f808226cc12a78 Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Mon, 9 Feb 2026 20:24:32 -0500 Subject: [PATCH] Enhance admin analytics with ARR, revenue trends, churn, and overdue invoices Add Annual Recurring Revenue stat, 12-month revenue trend with progress bars, customer growth chart, 6-month churn rate tracker with status indicators, and overdue invoices table with days-overdue coloring. Co-Authored-By: Claude Sonnet 4.5 --- .../Controllers/Admin/DashboardController.php | 61 +++ .../resources/ts/Pages/Admin/Dashboard.vue | 413 +++++++++++++++--- 2 files changed, 410 insertions(+), 64 deletions(-) diff --git a/website/app/Http/Controllers/Admin/DashboardController.php b/website/app/Http/Controllers/Admin/DashboardController.php index 252dba3..8cc55e7 100644 --- a/website/app/Http/Controllers/Admin/DashboardController.php +++ b/website/app/Http/Controllers/Admin/DashboardController.php @@ -6,6 +6,7 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Invoice; +use App\Models\PaymentTransaction; use App\Models\Plan; use App\Models\Service; use App\Models\User; @@ -118,6 +119,61 @@ class DashboardController extends Controller ->where('paid_at', '>=', now()->startOfMonth()) ->sum('total'); + // ARR (Annual Recurring Revenue) + $arr = (float) $mrr * 12; + + // Monthly Revenue Trend (last 12 months) + $revenueByMonth = PaymentTransaction::query() + ->where('status', 'completed') + ->where('created_at', '>=', now()->subMonths(12)) + ->selectRaw("DATE_FORMAT(created_at, '%Y-%m') as month, SUM(amount) as total") + ->groupBy('month') + ->orderBy('month') + ->get() + ->map(fn ($row) => ['month' => $row->month, 'total' => (float) $row->total]); + + // Customer Growth (last 12 months - new signups per month) + $customerGrowth = User::role('customer') + ->where('created_at', '>=', now()->subMonths(12)) + ->selectRaw("DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count") + ->groupBy('month') + ->orderBy('month') + ->get() + ->map(fn ($row) => ['month' => $row->month, 'count' => (int) $row->count]); + + // Churn Rate (subscriptions cancelled vs total in last 6 months) + $churnData = []; + for ($i = 5; $i >= 0; $i--) { + $monthStart = now()->subMonths($i)->startOfMonth(); + $monthEnd = now()->subMonths($i)->endOfMonth(); + + $totalAtStart = Subscription::query() + ->where('created_at', '<', $monthStart) + ->where(function ($query) use ($monthStart): void { + $query->whereNull('cancelled_at') + ->orWhere('cancelled_at', '>', $monthStart); + }) + ->count(); + + $cancelled = Subscription::query() + ->whereBetween('cancelled_at', [$monthStart, $monthEnd]) + ->count(); + + $churnData[] = [ + 'month' => $monthStart->format('Y-m'), + 'rate' => $totalAtStart > 0 ? round(($cancelled / $totalAtStart) * 100, 1) : 0, + 'cancelled' => $cancelled, + ]; + } + + // Overdue Invoices (detailed list) + $overdueInvoices = Invoice::query() + ->where('status', 'overdue') + ->with('user:id,name,email') + ->latest('due_date') + ->take(10) + ->get(['id', 'user_id', 'number', 'total', 'due_date', 'status']); + return Inertia::render('Admin/Dashboard', [ 'totalCustomers' => $totalCustomers, 'mrr' => (float) $mrr, @@ -133,6 +189,11 @@ class DashboardController extends Controller 'revenueByServiceType' => $revenueByServiceType, 'newCustomersThisMonth' => $newCustomersThisMonth, 'revenueThisMonth' => (float) $revenueThisMonth, + 'arr' => $arr, + 'revenueByMonth' => $revenueByMonth, + 'customerGrowth' => $customerGrowth, + 'churnData' => $churnData, + 'overdueInvoices' => $overdueInvoices, ]); } } diff --git a/website/resources/ts/Pages/Admin/Dashboard.vue b/website/resources/ts/Pages/Admin/Dashboard.vue index f616457..75c54e2 100644 --- a/website/resources/ts/Pages/Admin/Dashboard.vue +++ b/website/resources/ts/Pages/Admin/Dashboard.vue @@ -1,4 +1,6 @@