TenantAtlas/app/Filament/Pages/Workspaces/ManagedTenantsLanding.php
ahmido 417df4f9aa feat: central tenant operability policy (#177)
## Summary
- centralize tenant operability into a lane-aware, actor-aware policy boundary
- align selector eligibility, administrative discoverability, remembered context, tenant-bound routes, and canonical run viewers
- add focused Pest coverage plus Spec 148 artifacts and final polish task completion

## Validation
- `vendor/bin/sail artisan test --compact tests/Unit/Tenants/TenantOperabilityServiceTest.php tests/Unit/Tenants/TenantOperabilityOutcomeTest.php tests/Feature/Workspaces/ChooseTenantPageTest.php tests/Feature/Workspaces/SelectTenantControllerTest.php tests/Feature/TenantRBAC/ArchivedTenantRouteAccessTest.php tests/Feature/TenantRBAC/TenantRouteDenyAsNotFoundTest.php tests/Feature/Operations/TenantlessOperationRunViewerTest.php tests/Feature/OpsUx/OperateHubShellTest.php tests/Feature/Rbac/TenantLifecycleActionVisibilityTest.php tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php tests/Feature/Rbac/TenantResourceAuthorizationTest.php tests/Feature/Filament/ManagedTenantsLandingLifecycleTest.php tests/Feature/Filament/TenantGlobalSearchLifecycleScopeTest.php tests/Feature/Onboarding/OnboardingDraftLifecycleTest.php tests/Feature/Onboarding/OnboardingDraftAuthorizationTest.php`
- `vendor/bin/sail bin pint --dirty --format agent`
- manual browser smoke checks on `/admin/choose-tenant`, `/admin/tenants`, `/admin/onboarding`, `/admin/onboarding/{draft}`, and `/admin/operations/{run}`

## Filament / platform notes
- Livewire v4 compliance preserved
- panel provider registration unchanged in `bootstrap/providers.php`
- Tenant resource global search remains backed by existing view/edit pages and is now separated from active-only selector eligibility
- destructive actions remain action closures with confirmation and authorization enforcement
- no asset pipeline changes and no new `filament:assets` deployment requirement

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #177
2026-03-17 11:48:55 +00:00

112 lines
3.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Pages\Workspaces;
use App\Filament\Pages\ChooseTenant;
use App\Filament\Resources\TenantResource;
use App\Models\Tenant;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Tenants\TenantOperabilityService;
use App\Support\Tenants\TenantInteractionLane;
use App\Support\Tenants\TenantOperabilityQuestion;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Pages\Page;
use Illuminate\Database\Eloquent\Collection;
class ManagedTenantsLanding extends Page
{
protected static bool $shouldRegisterNavigation = false;
protected static bool $isDiscovered = false;
protected static string $layout = 'filament-panels::components.layout.simple';
protected static ?string $title = 'Managed tenants';
protected string $view = 'filament.pages.workspaces.managed-tenants-landing';
public Workspace $workspace;
/**
* The Filament simple layout renders the topbar by default, which includes
* lazy-loaded database notifications. On this workspace-scoped landing page,
* those background Livewire requests currently 404.
*/
protected function getLayoutData(): array
{
return [
'hasTopbar' => false,
];
}
public function mount(Workspace $workspace): void
{
$this->workspace = $workspace;
}
/**
* @return Collection<int, Tenant>
*/
public function getTenants(): Collection
{
$user = auth()->user();
if (! $user instanceof User) {
return Tenant::query()->whereRaw('1 = 0')->get();
}
$tenantIds = $user->tenantMemberships()
->pluck('tenant_id');
return Tenant::query()
->withTrashed()
->whereIn('id', $tenantIds)
->where('workspace_id', $this->workspace->getKey())
->orderBy('name')
->get()
->filter(function (Tenant $tenant) use ($user): bool {
return app(TenantOperabilityService::class)->outcomeFor(
tenant: $tenant,
question: TenantOperabilityQuestion::AdministrativeDiscoverability,
actor: $user,
workspaceId: app(WorkspaceContext::class)->currentWorkspaceId(request()),
lane: TenantInteractionLane::AdministrativeManagement,
)->allowed;
})
->values();
}
public function goToChooseTenant(): void
{
$this->redirect(ChooseTenant::getUrl());
}
public function openTenant(int $tenantId): void
{
$user = auth()->user();
if (! $user instanceof User) {
abort(403);
}
$tenant = Tenant::query()
->withTrashed()
->where('workspace_id', $this->workspace->getKey())
->whereKey($tenantId)
->first();
if (! $tenant instanceof Tenant) {
abort(404);
}
if (! $user->canAccessTenant($tenant)) {
abort(404);
}
$this->redirect(TenantResource::getUrl('view', ['record' => $tenant]));
}
}