From c4bd4efbedf27e745c76978943e9735be531a9f5 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sat, 6 Jun 2026 17:21:27 +0000 Subject: [PATCH] Add a real hosting overview dashboard. Replace the smart redirect on /dashboard with an overview hub (stats, recent accounts, pending orders) matching Domains and Email, and move the full account list to /hosting/accounts. Co-authored-by: Cursor --- .../Hosting/HostingProductController.php | 65 ----------- .../Hosting/OverviewController.php | 107 +++++++++++++++++- .../views/hosting/accounts-list.blade.php | 21 ++-- resources/views/hosting/dashboard.blade.php | 102 +++++++++++++++++ routes/web.php | 6 +- 5 files changed, 220 insertions(+), 81 deletions(-) create mode 100644 resources/views/hosting/dashboard.blade.php diff --git a/app/Http/Controllers/Hosting/HostingProductController.php b/app/Http/Controllers/Hosting/HostingProductController.php index d207d36..d4a26f7 100644 --- a/app/Http/Controllers/Hosting/HostingProductController.php +++ b/app/Http/Controllers/Hosting/HostingProductController.php @@ -25,71 +25,6 @@ use Illuminate\View\View; class HostingProductController extends Controller { - /** - * Smart hosting redirect - routes user based on their hosting accounts - */ - public function index(Request $request): View|RedirectResponse - { - $user = $request->user(); - - // Get all shared hosting types (not VPS/dedicated) - $sharedTypes = [ - HostingProduct::TYPE_SINGLE_DOMAIN, - HostingProduct::TYPE_MULTI_DOMAIN, - HostingProduct::TYPE_WORDPRESS, - ]; - - // Get user's hosting accounts (owned + team member access) - $sharedAccountIds = HostingAccountMember::query() - ->where('user_id', $user->id) - ->pluck('hosting_account_id'); - - $hostingAccounts = HostingAccount::query() - ->where(function ($query) use ($user, $sharedAccountIds) { - $query->where('user_id', $user->id); - if ($sharedAccountIds->isNotEmpty()) { - $query->orWhereIn('id', $sharedAccountIds); - } - }) - ->whereHas('product', fn ($q) => $q->whereIn('type', $sharedTypes)) - ->with('product') - ->latest() - ->get(); - - // Get user's hosting orders - $orders = CustomerHostingOrder::query() - ->forUser($user->id) - ->whereHas('product', fn ($q) => $q->whereIn('type', $sharedTypes)) - ->with(['product', 'hostingAccount']) - ->latest() - ->get(); - - // Combine accounts from orders and direct assignments - $allAccounts = $hostingAccounts->merge( - $orders->filter(fn ($o) => $o->hostingAccount)->map(fn ($o) => $o->hostingAccount) - )->unique('id'); - - // No hosting accounts - show selection modal - if ($allAccounts->isEmpty()) { - return view('hosting.select-type', [ - 'title' => 'Choose Hosting Plan', - ]); - } - - // Single account — account overview (matches monolith; panel via Control Panel) - if ($allAccounts->count() === 1) { - $account = $allAccounts->first(); - - return redirect()->route('hosting.accounts.show', $account); - } - - // Multiple accounts - show list page - return view('hosting.accounts-list', [ - 'title' => 'Your Hosting Accounts', - 'accounts' => $allAccounts, - ]); - } - public function singleDomain(Request $request): View { return $this->renderHostingPage($request, HostingProduct::TYPE_SINGLE_DOMAIN, 'Single Domain Hosting'); diff --git a/app/Http/Controllers/Hosting/OverviewController.php b/app/Http/Controllers/Hosting/OverviewController.php index 0d13e6c..6ee8c68 100644 --- a/app/Http/Controllers/Hosting/OverviewController.php +++ b/app/Http/Controllers/Hosting/OverviewController.php @@ -3,13 +3,114 @@ namespace App\Http\Controllers\Hosting; use App\Http\Controllers\Controller; -use Illuminate\Http\RedirectResponse; +use App\Models\CustomerHostingOrder; +use App\Models\HostedSite; +use App\Models\HostingAccount; +use App\Models\HostingAccountMember; +use App\Models\HostingProduct; +use App\Models\User; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\Request; +use Illuminate\View\View; class OverviewController extends Controller { - public function index(Request $request): RedirectResponse + /** Authenticated hosting dashboard (overview hub). */ + public function index(Request $request): View { - return redirect()->route('hosting.dashboard'); + $user = $request->user(); + $accounts = $this->sharedHostingAccounts($user); + $accountIds = $accounts->pluck('id'); + + $pendingOrders = CustomerHostingOrder::query() + ->forUser($user->id) + ->whereHas('product', fn ($q) => $q->whereIn('type', $this->sharedTypes())) + ->whereIn('status', [ + CustomerHostingOrder::STATUS_PENDING_PAYMENT, + CustomerHostingOrder::STATUS_PENDING_APPROVAL, + CustomerHostingOrder::STATUS_APPROVED, + CustomerHostingOrder::STATUS_PROVISIONING, + ]) + ->with('product') + ->latest() + ->take(5) + ->get(); + + $linkedDomains = $accountIds->isEmpty() + ? 0 + : HostedSite::query()->whereIn('hosting_account_id', $accountIds)->count(); + + return view('hosting.dashboard', [ + 'accounts' => $accounts, + 'activeCount' => $accounts->where('status', HostingAccount::STATUS_ACTIVE)->count(), + 'expiringCount' => $accounts + ->where('status', HostingAccount::STATUS_ACTIVE) + ->filter(fn (HostingAccount $account) => $account->expires_at !== null && $account->expires_at->between(now(), now()->addDays(30))) + ->count(), + 'pendingOrderCount' => CustomerHostingOrder::query() + ->forUser($user->id) + ->whereHas('product', fn ($q) => $q->whereIn('type', $this->sharedTypes())) + ->whereIn('status', [ + CustomerHostingOrder::STATUS_PENDING_PAYMENT, + CustomerHostingOrder::STATUS_PENDING_APPROVAL, + CustomerHostingOrder::STATUS_APPROVED, + CustomerHostingOrder::STATUS_PROVISIONING, + ]) + ->count(), + 'linkedDomains' => $linkedDomains, + 'recent' => $accounts->take(5), + 'pendingOrders' => $pendingOrders, + ]); + } + + public function accounts(Request $request): View + { + return view('hosting.accounts-list', [ + 'title' => 'Your Hosting Accounts', + 'accounts' => $this->sharedHostingAccounts($request->user()), + ]); + } + + /** @return list */ + private function sharedTypes(): array + { + return [ + HostingProduct::TYPE_SINGLE_DOMAIN, + HostingProduct::TYPE_MULTI_DOMAIN, + HostingProduct::TYPE_WORDPRESS, + ]; + } + + /** @return Collection */ + private function sharedHostingAccounts(User $user): Collection + { + $sharedTypes = $this->sharedTypes(); + + $sharedAccountIds = HostingAccountMember::query() + ->where('user_id', $user->id) + ->pluck('hosting_account_id'); + + $hostingAccounts = HostingAccount::query() + ->where(function ($query) use ($user, $sharedAccountIds) { + $query->where('user_id', $user->id); + if ($sharedAccountIds->isNotEmpty()) { + $query->orWhereIn('id', $sharedAccountIds); + } + }) + ->whereHas('product', fn ($q) => $q->whereIn('type', $sharedTypes)) + ->with(['product', 'sites']) + ->latest() + ->get(); + + $orderAccounts = CustomerHostingOrder::query() + ->forUser($user->id) + ->whereHas('product', fn ($q) => $q->whereIn('type', $sharedTypes)) + ->with(['hostingAccount.product', 'hostingAccount.sites']) + ->latest() + ->get() + ->filter(fn (CustomerHostingOrder $order) => $order->hostingAccount !== null) + ->map(fn (CustomerHostingOrder $order) => $order->hostingAccount); + + return $hostingAccounts->merge($orderAccounts)->unique('id')->values(); } } diff --git a/resources/views/hosting/accounts-list.blade.php b/resources/views/hosting/accounts-list.blade.php index 6504aa9..71d9042 100644 --- a/resources/views/hosting/accounts-list.blade.php +++ b/resources/views/hosting/accounts-list.blade.php @@ -1,12 +1,12 @@ - - {{ $title }} - -
-
-
-

{{ $title }}

-

Select a hosting account to manage

-
+@extends('layouts.hosting') +@section('title', $title . ' — Ladill Hosting') +@section('content') +
+
+ ← Overview +

{{ $title }}

+

Select a hosting account to manage

+
@foreach($accounts as $account) @@ -76,6 +76,5 @@
-
-
+@endsection diff --git a/resources/views/hosting/dashboard.blade.php b/resources/views/hosting/dashboard.blade.php new file mode 100644 index 0000000..a66f3bf --- /dev/null +++ b/resources/views/hosting/dashboard.blade.php @@ -0,0 +1,102 @@ +@extends('layouts.hosting') +@section('title', 'Overview — Ladill Hosting') +@section('content') +
+
+
+

Overview

+

Your shared hosting at a glance.

+
+ + New hosting + +
+ +
+ @foreach([ + ['Active accounts', $activeCount, route('hosting.accounts.index')], + ['Expiring (30 days)', $expiringCount, route('hosting.accounts.index')], + ['Pending orders', $pendingOrderCount, route('hosting.single-domain')], + ['Linked domains', $linkedDomains, route('hosting.accounts.index')], + ] as [$label, $value, $link]) + +

{{ $label }}

+

{{ $value }}

+
+ @endforeach +
+ +
+
+

Hosting accounts

+ @if ($accounts->count() > 5) + View all + @endif +
+ @if ($recent->isEmpty()) +
+

No hosting accounts yet

+

Choose a plan to get started — single domain, multi domain, or WordPress hosting.

+ +
+ @else + + @endif +
+ + @if ($pendingOrders->isNotEmpty()) + + @endif +
+@endsection diff --git a/routes/web.php b/routes/web.php index ddc9ce1..8ed553e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -9,6 +9,7 @@ use App\Http\Controllers\Hosting\HostingPanelController; use App\Http\Controllers\Hosting\HostingProductController; use App\Http\Controllers\Hosting\HostingTerminalSessionController; use App\Http\Controllers\Hosting\MailboxLinkBannerController; +use App\Http\Controllers\Hosting\OverviewController; use App\Http\Controllers\Hosting\TeamController; use Illuminate\Support\Facades\Route; @@ -28,9 +29,10 @@ Route::get('/sso/logout-frontchannel', [SsoLoginController::class, 'frontchannel Route::get('/signed-out', fn () => auth()->check() ? redirect()->route('hosting.dashboard') : view('hosting.signed-out'))->name('hosting.signed-out'); Route::middleware(['auth'])->group(function () use ($serversApp) { - Route::get('/dashboard', [HostingProductController::class, 'index'])->name('hosting.dashboard'); + Route::get('/dashboard', [OverviewController::class, 'index'])->name('hosting.dashboard'); - Route::get('/hosting', [HostingProductController::class, 'index'])->name('hosting.index'); + Route::redirect('/hosting', '/dashboard')->name('hosting.index'); + Route::get('/hosting/accounts', [OverviewController::class, 'accounts'])->name('hosting.accounts.index'); Route::get('/hosting/single-domain', [HostingProductController::class, 'singleDomain'])->name('hosting.single-domain'); Route::get('/hosting/multi-domain', [HostingProductController::class, 'multiDomain'])->name('hosting.multi-domain'); Route::get('/hosting/wordpress', [HostingProductController::class, 'wordpress'])->name('hosting.wordpress');