TenantAtlas/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php
ahmido 1b88d28739 feat: consolidate operation naming surfaces (#202)
## Summary
- align operator-visible OperationRun terminology to canonical `Operations` / `Operation` labels across shared links, notifications, verification/onboarding surfaces, summary widgets, and monitoring/detail pages
- add the Spec 171 planning artifacts under `specs/171-operations-naming-consolidation/`
- close the remaining tenant dashboard and admin copy drift found during browser smoke validation

## Validation
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && vendor/bin/sail artisan test --compact tests/Unit/Support/RelatedNavigationResolverTest.php tests/Unit/Support/References/RelatedContextReferenceAdapterTest.php tests/Feature/OpsUx/NotificationViewRunLinkTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Feature/Operations/TenantlessOperationRunViewerTest.php tests/Feature/Filament/BackupSetResolvedReferencePresentationTest.php tests/Feature/Filament/TenantVerificationReportWidgetTest.php tests/Feature/Onboarding/OnboardingVerificationTest.php tests/Feature/Onboarding/OnboardingVerificationClustersTest.php tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php tests/Feature/Filament/BaselineCompareSummaryConsistencyTest.php tests/Feature/Filament/WorkspaceOverviewContentTest.php tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php tests/Feature/Monitoring/OperationLifecycleAggregateVisibilityTest.php tests/Feature/System/Spec114/OpsTriageActionsTest.php tests/Feature/System/Spec114/OpsFailuresViewTest.php tests/Feature/System/Spec114/OpsStuckViewTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && vendor/bin/sail artisan test --compact tests/Browser/OnboardingDraftRefreshTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && vendor/bin/sail bin pint --dirty --format agent`

## Notes
- no schema or route renames
- Filament / Livewire surface behavior stays within the existing admin and tenant panels
- OperationRunResource remains excluded from global search

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #202
2026-03-30 22:51:06 +00:00

206 lines
8.6 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Resources\TenantReviewResource\Pages;
use App\Filament\Resources\TenantReviewResource;
use App\Models\Tenant;
use App\Models\TenantReview;
use App\Models\User;
use App\Services\TenantReviews\TenantReviewLifecycleService;
use App\Services\TenantReviews\TenantReviewService;
use App\Support\Auth\Capabilities;
use App\Support\OperationRunLinks;
use App\Support\Rbac\UiEnforcement;
use App\Support\TenantReviewStatus;
use Filament\Actions;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ViewRecord;
use Illuminate\Database\Eloquent\Model;
class ViewTenantReview extends ViewRecord
{
protected static string $resource = TenantReviewResource::class;
protected function resolveRecord(int|string $key): Model
{
return TenantReviewResource::resolveScopedRecordOrFail($key);
}
protected function authorizeAccess(): void
{
$tenant = TenantReviewResource::panelTenantContext();
$record = $this->getRecord();
$user = auth()->user();
if (! $user instanceof User || ! $tenant instanceof Tenant || ! $record instanceof TenantReview) {
abort(404);
}
if ((int) $record->tenant_id !== (int) $tenant->getKey()) {
abort(404);
}
if (! $user->canAccessTenant($tenant)) {
abort(404);
}
if (! $user->can('view', $record)) {
abort(403);
}
}
protected function getHeaderActions(): array
{
return [
Actions\Action::make('view_run')
->label('Open operation')
->icon('heroicon-o-eye')
->color('gray')
->hidden(fn (): bool => ! is_numeric($this->record->operation_run_id))
->url(fn (): ?string => $this->record->operation_run_id
? OperationRunLinks::tenantlessView((int) $this->record->operation_run_id)
: null),
Actions\Action::make('view_export')
->label('View executive pack')
->icon('heroicon-o-document-arrow-down')
->color('gray')
->hidden(fn (): bool => ! $this->record->currentExportReviewPack)
->url(fn (): ?string => $this->record->currentExportReviewPack
? \App\Filament\Resources\ReviewPackResource::getUrl('view', ['record' => $this->record->currentExportReviewPack], tenant: $this->record->tenant)
: null),
Actions\Action::make('view_evidence')
->label('View evidence snapshot')
->icon('heroicon-o-shield-check')
->color('gray')
->hidden(fn (): bool => ! $this->record->evidenceSnapshot)
->url(fn (): ?string => $this->record->evidenceSnapshot
? \App\Filament\Resources\EvidenceSnapshotResource::getUrl('view', ['record' => $this->record->evidenceSnapshot], tenant: $this->record->tenant)
: null),
UiEnforcement::forAction(
Actions\Action::make('refresh_review')
->label('Refresh review')
->icon('heroicon-o-arrow-path')
->hidden(fn (): bool => ! $this->record->isMutable())
->requiresConfirmation()
->action(function (): void {
$user = auth()->user();
if (! $user instanceof User) {
abort(403);
}
try {
app(TenantReviewService::class)->refresh($this->record, $user);
} catch (\Throwable $throwable) {
Notification::make()->danger()->title('Unable to refresh review')->body($throwable->getMessage())->send();
return;
}
Notification::make()->success()->title('Refresh review queued')->send();
}),
)
->requireCapability(Capabilities::TENANT_REVIEW_MANAGE)
->preserveVisibility()
->apply(),
UiEnforcement::forAction(
Actions\Action::make('publish_review')
->label('Publish review')
->icon('heroicon-o-check-badge')
->hidden(fn (): bool => ! $this->record->isMutable())
->requiresConfirmation()
->action(function (): void {
$user = auth()->user();
if (! $user instanceof User) {
abort(403);
}
try {
app(TenantReviewLifecycleService::class)->publish($this->record, $user);
} catch (\Throwable $throwable) {
Notification::make()->danger()->title('Unable to publish review')->body($throwable->getMessage())->send();
return;
}
$this->refreshFormData(['status', 'published_at', 'published_by_user_id', 'summary']);
Notification::make()->success()->title('Review published')->send();
}),
)
->requireCapability(Capabilities::TENANT_REVIEW_MANAGE)
->preserveVisibility()
->apply(),
UiEnforcement::forAction(
Actions\Action::make('export_executive_pack')
->label('Export executive pack')
->icon('heroicon-o-arrow-down-tray')
->hidden(fn (): bool => ! in_array($this->record->status, [
TenantReviewStatus::Ready->value,
TenantReviewStatus::Published->value,
], true))
->action(fn (): mixed => TenantReviewResource::executeExport($this->record)),
)
->requireCapability(Capabilities::TENANT_REVIEW_MANAGE)
->preserveVisibility()
->apply(),
Actions\ActionGroup::make([
UiEnforcement::forAction(
Actions\Action::make('create_next_review')
->label('Create next review')
->icon('heroicon-o-document-duplicate')
->hidden(fn (): bool => ! $this->record->isPublished())
->action(function (): void {
$user = auth()->user();
if (! $user instanceof User) {
abort(403);
}
try {
$nextReview = app(TenantReviewLifecycleService::class)->createNextReview($this->record, $user);
} catch (\Throwable $throwable) {
Notification::make()->danger()->title('Unable to create next review')->body($throwable->getMessage())->send();
return;
}
$this->redirect(TenantReviewResource::tenantScopedUrl('view', ['record' => $nextReview], $nextReview->tenant));
}),
)
->requireCapability(Capabilities::TENANT_REVIEW_MANAGE)
->preserveVisibility()
->apply(),
UiEnforcement::forAction(
Actions\Action::make('archive_review')
->label('Archive review')
->icon('heroicon-o-archive-box')
->color('danger')
->hidden(fn (): bool => $this->record->statusEnum()->isTerminal())
->requiresConfirmation()
->action(function (): void {
$user = auth()->user();
if (! $user instanceof User) {
abort(403);
}
app(TenantReviewLifecycleService::class)->archive($this->record, $user);
$this->refreshFormData(['status', 'archived_at']);
Notification::make()->success()->title('Review archived')->send();
}),
)
->requireCapability(Capabilities::TENANT_REVIEW_MANAGE)
->preserveVisibility()
->apply(),
])
->label('More')
->icon('heroicon-m-ellipsis-vertical')
->color('gray'),
];
}
}