TenantAtlas/apps/platform/app/Support/Navigation/WorkspaceSidebarNavigation.php
ahmido bf43dad3d1 fix: enforce workspace surface scope for customer review workspace (#366)
## Summary
- keep `/admin/reviews/workspace` workspace-scoped in shell and sidebar context
- treat `tenant` query hints on the customer review workspace as page-level filters only
- update the customer review workspace tests and Spec 311 navigation contract to match the workspace-hub IA

## Testing
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php tests/Feature/Filament/PanelNavigationSegregationTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `git diff --check`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #366
2026-05-15 20:52:37 +00:00

168 lines
8.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Navigation;
use App\Filament\Pages\Governance\DecisionRegister;
use App\Filament\Pages\Governance\GovernanceInbox;
use App\Filament\Pages\Monitoring\Alerts;
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Filament\Pages\Reviews\ReviewRegister;
use App\Filament\Pages\Settings\WorkspaceSettings;
use App\Filament\Pages\WorkspaceOverview;
use App\Filament\Resources\AlertDeliveryResource;
use App\Filament\Resources\AlertDestinationResource;
use App\Filament\Resources\AlertRuleResource;
use App\Filament\Resources\ProviderConnectionResource;
use App\Filament\Resources\Workspaces\WorkspaceResource;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Services\Auth\WorkspaceCapabilityResolver;
use App\Services\Auth\WorkspaceRoleCapabilityMap;
use App\Support\Auth\Capabilities;
use App\Support\OperationRunLinks;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Navigation\NavigationBuilder;
use Filament\Navigation\NavigationGroup;
use Filament\Navigation\NavigationItem;
final class WorkspaceSidebarNavigation
{
public function build(): NavigationBuilder
{
return app(NavigationBuilder::class)
->item(WorkspaceOverview::navigationItem())
->groups([
NavigationGroup::make(__('localization.navigation.monitoring'))
->items($this->visibleItems([
NavigationItem::make(FindingExceptionsQueue::getNavigationLabel())
->url(fn (): string => FindingExceptionsQueue::getUrl(panel: 'admin'))
->icon(FindingExceptionsQueue::getNavigationIcon())
->visible(fn (): bool => FindingExceptionsQueue::canAccess()),
NavigationItem::make(__('localization.navigation.operations'))
->url(fn (): string => OperationRunLinks::index())
->icon('heroicon-o-queue-list')
->visible(fn (): bool => true),
NavigationItem::make(__('localization.navigation.alerts'))
->url(fn (): string => route('filament.admin.alerts'))
->icon('heroicon-o-bell-alert')
->visible(fn (): bool => Alerts::canAccess())
->childItems($this->visibleItems([
NavigationItem::make(AlertDestinationResource::getNavigationLabel())
->url(fn (): string => AlertDestinationResource::getUrl(panel: 'admin'))
->icon(AlertDestinationResource::getNavigationIcon())
->visible(fn (): bool => AlertDestinationResource::canViewAny()),
NavigationItem::make(AlertRuleResource::getNavigationLabel())
->url(fn (): string => AlertRuleResource::getUrl(panel: 'admin'))
->icon(AlertRuleResource::getNavigationIcon())
->visible(fn (): bool => AlertRuleResource::canViewAny()),
NavigationItem::make(AlertDeliveryResource::getNavigationLabel())
->url(fn (): string => AlertDeliveryResource::getUrl(panel: 'admin'))
->icon(AlertDeliveryResource::getNavigationIcon())
->visible(fn (): bool => AlertDeliveryResource::canViewAny()),
])),
NavigationItem::make(__('localization.navigation.audit_log'))
->url(fn (): string => route('admin.monitoring.audit-log'))
->icon('heroicon-o-clipboard-document-list'),
])),
NavigationGroup::make(__('localization.review.reporting'))
->items($this->visibleItems([
NavigationItem::make(ReviewRegister::getNavigationLabel())
->url(fn (): string => ReviewRegister::getUrl(panel: 'admin'))
->icon(ReviewRegister::getNavigationIcon()),
NavigationItem::make(CustomerReviewWorkspace::getNavigationLabel())
->url(fn (): string => CustomerReviewWorkspace::getUrl(panel: 'admin'))
->icon(CustomerReviewWorkspace::getNavigationIcon()),
])),
NavigationGroup::make(__('localization.navigation.settings'))
->items($this->visibleItems([
NavigationItem::make(__('localization.navigation.manage_workspaces'))
->url(fn (): string => route('filament.admin.resources.workspaces.index'))
->icon(WorkspaceResource::getNavigationIcon())
->visible(fn (): bool => $this->canManageWorkspaces()),
NavigationItem::make(__('localization.navigation.integrations'))
->url(fn (): string => ProviderConnectionResource::getUrl('index', panel: 'admin'))
->icon(ProviderConnectionResource::getNavigationIcon())
->visible(fn (): bool => ProviderConnectionResource::canViewAny())
->childItems($this->visibleItems([
NavigationItem::make(ProviderConnectionResource::getNavigationLabel())
->url(fn (): string => ProviderConnectionResource::getUrl('index', panel: 'admin'))
->icon(ProviderConnectionResource::getNavigationIcon())
->visible(fn (): bool => ProviderConnectionResource::canViewAny()),
])),
NavigationItem::make(__('localization.navigation.settings'))
->url(fn (): string => WorkspaceSettings::getUrl(panel: 'admin'))
->icon('heroicon-o-cog-6-tooth')
->visible(fn (): bool => $this->canViewWorkspaceSettings()),
])),
NavigationGroup::make(__('localization.navigation.governance'))
->items($this->visibleItems([
NavigationItem::make(GovernanceInbox::getNavigationLabel())
->url(fn (): string => GovernanceInbox::getUrl(panel: 'admin'))
->icon(GovernanceInbox::getNavigationIcon()),
NavigationItem::make(DecisionRegister::getNavigationLabel())
->url(fn (): string => DecisionRegister::getUrl(panel: 'admin'))
->icon(DecisionRegister::getNavigationIcon())
->visible(fn (): bool => DecisionRegister::canAccess()),
])),
]);
}
/**
* @param array<int, NavigationItem> $items
* @return array<int, NavigationItem>
*/
private function visibleItems(array $items): array
{
return array_values(array_filter(
$items,
static fn (NavigationItem $item): bool => $item->isVisible(),
));
}
private function canManageWorkspaces(): bool
{
$user = auth()->user();
if (! $user instanceof User) {
return false;
}
$roles = WorkspaceRoleCapabilityMap::rolesWithCapability(Capabilities::WORKSPACE_MEMBERSHIP_MANAGE);
return WorkspaceMembership::query()
->where('user_id', (int) $user->getKey())
->whereIn('role', $roles)
->exists();
}
private function canViewWorkspaceSettings(): bool
{
$user = auth()->user();
if (! $user instanceof User) {
return false;
}
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
if (! is_int($workspaceId)) {
return false;
}
$workspace = Workspace::query()->whereKey($workspaceId)->first();
if (! $workspace instanceof Workspace) {
return false;
}
$resolver = app(WorkspaceCapabilityResolver::class);
return $resolver->isMember($user, $workspace)
&& $resolver->can($user, $workspace, Capabilities::WORKSPACE_SETTINGS_VIEW);
}
}