Skip to content

Commit 4028e15

Browse files
committed
feat: shift tenant onboarding to approval flow and refresh platform UI
Create tenant registrations as pending requests and move domain/DB/user provisioning to admin approval via TenantOnboardingService::approveTenant. Harden central tenant/admin flows: safer redirects, active-only tenant metrics, improved registration input normalization, and tenant plan relationship loading for admin views. Add Alpine.js and replace inline confirm() usage with a reusable dialog triggered by data-confirm forms. Apply industrial design-system overhaul across central/admin/tenant layouts, update typography/colors/navigation/modal behavior, and standardize payroll/currency display to PHP peso.
1 parent 5823d21 commit 4028e15

32 files changed

+889
-383
lines changed

app/Http/Controllers/Central/Admin/AdminAuthController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function store(Request $request)
2929

3030
$request->session()->regenerate();
3131

32-
return redirect()->intended(route('admin.dashboard'));
32+
return redirect()->route('admin.dashboard');
3333
}
3434

3535
public function destroy(Request $request)
@@ -39,6 +39,6 @@ public function destroy(Request $request)
3939
$request->session()->invalidate();
4040
$request->session()->regenerateToken();
4141

42-
return redirect()->route('admin.login');
42+
return redirect('/login');
4343
}
4444
}

app/Http/Controllers/Central/Admin/TenantManagementController.php

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,58 @@
1111
use App\Notifications\Central\TenantApprovedNotification;
1212

1313
use App\Services\Central\PlatformActivityService;
14+
use App\Services\Central\TenantOnboardingService;
15+
use Throwable;
1416

1517
class TenantManagementController extends Controller
1618
{
17-
public function __construct(protected PlatformActivityService $activityService)
19+
public function __construct(
20+
protected PlatformActivityService $activityService,
21+
protected TenantOnboardingService $onboardingService,
22+
)
1823
{}
1924

2025
public function index(): View
2126
{
22-
$tenants = Tenant::latest()->paginate(15);
27+
$tenants = Tenant::with(['tenantPlan', 'plan'])->latest()->paginate(15);
2328
return view('admin.tenants.index', compact('tenants'));
2429
}
2530

2631
public function show(Tenant $tenant): View
2732
{
28-
// Peek into the tenant database to get metrics
29-
$metrics = $tenant->run(function () {
30-
return [
31-
'workers_count' => \App\Models\User::workers()->count(),
32-
'jobs_count' => \App\Models\JobOrder::count(),
33-
'tasks_count' => \App\Models\Task::count(),
34-
'payroll_periods_count' => \App\Models\PayrollPeriod::count(),
35-
'total_payroll_value' => \App\Models\PayrollPeriod::sum('total_amount'),
36-
];
37-
});
33+
$metrics = [
34+
'workers_count' => 0,
35+
'jobs_count' => 0,
36+
'tasks_count' => 0,
37+
'payroll_periods_count' => 0,
38+
'total_payroll_value' => 0,
39+
];
40+
41+
// Pending/suspended workspaces may not have a provisioned tenant DB yet.
42+
if ($tenant->status === 'active') {
43+
$metrics = $tenant->run(function () {
44+
return [
45+
'workers_count' => \App\Models\User::workers()->count(),
46+
'jobs_count' => \App\Models\JobOrder::count(),
47+
'tasks_count' => \App\Models\Task::count(),
48+
'payroll_periods_count' => \App\Models\PayrollPeriod::count(),
49+
'total_payroll_value' => \App\Models\PayrollPeriod::sum('total_amount'),
50+
];
51+
});
52+
}
3853

3954
return view('admin.tenants.show', compact('tenant', 'metrics'));
4055
}
4156

4257
public function approve(Tenant $tenant)
4358
{
44-
$tenant->update(['status' => 'active']);
59+
try {
60+
$this->onboardingService->approveTenant($tenant);
61+
} catch (Throwable $e) {
62+
return back()->withErrors([
63+
'approval' => "Failed to approve tenant '{$tenant->company_name}'. Please try again. Error: {$e->getMessage()}",
64+
]);
65+
}
4566

4667
// Log the action
4768
$this->activityService->log(

app/Http/Controllers/Central/RegisterTenantController.php

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,23 +44,13 @@ public function create()
4444
public function store(RegisterTenantRequest $request)
4545
{
4646
try {
47-
$result = $this->onboardingService->registerTenant($request->validated());
48-
$tenant = $result['tenant'];
49-
$tenantAdminUserId = $result['tenant_admin_user_id'];
50-
$scheme = request()->secure() ? 'https://' : 'http://';
51-
$baseDomain = preg_replace('/:\\d+$/', '', (string) (config('tenancy.central_domains')[0] ?? 'localhost'));
52-
$subdomain = $tenant->subdomain ?: $tenant->getTenantKey();
53-
$port = request()->getPort();
54-
$portSegment = in_array((int) $port, [80, 443], true) ? '' : ':' . $port;
47+
$this->onboardingService->registerTenant($request->validated());
5548

56-
// Create token and redirect to tenant-side impersonation endpoint.
57-
$token = tenancy()->impersonate($tenant, (string) $tenantAdminUserId, '/dashboard', 'web');
58-
59-
return redirect()->away($scheme . $subdomain . '.' . $baseDomain . $portSegment . '/impersonate/' . $token->token);
49+
return redirect()->route('tenant.register.create')->with('success', 'Registration received. Your workspace will be provisioned after admin approval.');
6050
} catch (Exception $e) {
6151
// In a production app, we would log this and return a friendlier error
6252
return back()->withInput()->withErrors([
63-
'domain' => 'Failed to provision tenant workspace. Please try again or choose a different subdomain. Error: ' . $e->getMessage(),
53+
'domain' => 'Failed to submit tenant registration request. Please try again or choose a different subdomain. Error: ' . $e->getMessage(),
6454
]);
6555
}
6656
}

app/Http/Requests/Central/RegisterTenantRequest.php

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Http\Requests\Central;
44

55
use Illuminate\Foundation\Http\FormRequest;
6+
use Illuminate\Support\Str;
67
use Illuminate\Validation\Rules\Password;
78

89
class RegisterTenantRequest extends FormRequest
@@ -16,29 +17,39 @@ public function rules(): array
1617
{
1718
return [
1819
'company_name' => ['required', 'string', 'max:255'],
19-
'subdomain' => ['required', 'string', 'max:60', 'alpha_dash'],
20-
'domain' => ['required', 'string', 'unique:domains,domain'],
21-
'admin_name' => ['required', 'string', 'max:255'],
22-
'admin_email' => ['required', 'string', 'email', 'max:255'],
23-
'password' => ['required', 'confirmed', Password::defaults()],
20+
'subdomain' => ['required', 'string', 'max:60', 'alpha_dash', 'unique:tenants,subdomain'],
21+
'domain' => ['required', 'string', 'unique:domains,domain'],
22+
'admin_name' => ['required', 'string', 'max:255'],
23+
'admin_email' => ['required', 'string', 'email', 'max:255'],
24+
'password' => ['required', 'confirmed', Password::defaults()],
2425
// Legacy fields are optional to avoid breaking existing links or old clients.
25-
'plan_id' => ['nullable', 'exists:plans,id'],
26-
'billing_cycle'=> ['nullable', 'string', 'in:monthly,annual'],
27-
'industry' => ['nullable', 'string', 'max:255'],
28-
'size' => ['nullable', 'string', 'max:255'],
29-
'website' => ['nullable', 'string', 'max:255'],
30-
'terms' => ['accepted'],
26+
'plan_id' => ['nullable', 'exists:plans,id'],
27+
'billing_cycle' => ['nullable', 'string', 'in:monthly,annual'],
28+
'industry' => ['nullable', 'string', 'max:255'],
29+
'size' => ['nullable', 'string', 'max:255'],
30+
'website' => ['nullable', 'string', 'max:255'],
31+
'terms' => ['accepted'],
3132
];
3233
}
3334

3435
protected function prepareForValidation(): void
3536
{
36-
$centralDomain = config('tenancy.central_domains')[0] ?? 'localhost';
37-
$subdomain = strtolower($this->subdomain ?? $this->domain);
37+
$rawCentralDomain = (string) (config('tenancy.central_domains')[0] ?? 'localhost');
38+
$centralDomain = preg_replace('/:\\d+$/', '', $rawCentralDomain) ?: 'localhost';
39+
$subdomainInput = strtolower((string) ($this->subdomain ?? $this->domain ?? ''));
40+
41+
// Allow users to paste either "acme" or "acme.localhost".
42+
$subdomain = Str::of($subdomainInput)
43+
->when(
44+
Str::endsWith($subdomainInput, '.'.$centralDomain),
45+
fn ($value) => $value->beforeLast('.'.$centralDomain)
46+
)
47+
->trim('.')
48+
->toString();
3849

3950
$this->merge([
4051
'subdomain' => $subdomain,
41-
'domain' => $subdomain . '.' . $centralDomain,
52+
'domain' => $subdomain.'.'.$centralDomain,
4253
]);
4354
}
4455
}

app/Models/Tenant.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,12 @@ public function plan()
4444
{
4545
return $this->belongsTo(Plan::class);
4646
}
47+
48+
/**
49+
* Get the latest central plan record for this tenant.
50+
*/
51+
public function tenantPlan()
52+
{
53+
return $this->hasOne(TenantPlan::class)->latestOfMany('valid_until');
54+
}
4755
}

app/Providers/AppServiceProvider.php

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
namespace App\Providers;
44

5-
use Illuminate\Support\ServiceProvider;
6-
7-
use Illuminate\Support\Facades\Gate;
5+
use App\Models\User;
86
use Illuminate\Auth\Middleware\Authenticate;
97
use Illuminate\Auth\Middleware\RedirectIfAuthenticated;
10-
use App\Models\User;
8+
use Illuminate\Support\Facades\Gate;
9+
use Illuminate\Support\ServiceProvider;
1110

1211
class AppServiceProvider extends ServiceProvider
1312
{
@@ -28,7 +27,7 @@ public function boot(): void
2827
$host = $request->getHost();
2928

3029
if (str_starts_with($host, 'admin.')) {
31-
return route('admin.login');
30+
return '/login';
3231
}
3332

3433
return '/login';

0 commit comments

Comments
 (0)