TenantAtlas/apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php
ahmido dbff2a0a90 feat(report): implement management report pdf runtime (#450)
Added jobs, controllers, and PDF generation logic for management report runtime as defined in Spec 379. Includes artifact migrations, payload builders, and testing coverage.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #450
2026-06-15 11:36:29 +00:00

402 lines
17 KiB
PHP

<?php
namespace App\Filament\Resources\ReviewPackResource\Pages;
use App\Exceptions\Entitlements\WorkspaceEntitlementBlockedException;
use App\Exceptions\ReviewPackEvidenceResolutionException;
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Filament\Resources\ReviewPackResource;
use App\Models\EnvironmentReview;
use App\Models\OperationRun;
use App\Models\ReviewPack;
use App\Models\StoredReport;
use App\Models\User;
use App\Services\ReviewPacks\ManagementReportPdfService;
use App\Services\ReviewPackService;
use App\Support\Auth\Capabilities;
use App\Support\OperationRunLinks;
use App\Support\OperationRunType;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\Rbac\UiEnforcement;
use App\Support\ReviewPacks\ReportProfileRegistry;
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
use App\Support\ReviewPackStatus;
use Filament\Actions;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ViewRecord;
use Filament\Schemas\Components\Section;
use Filament\Support\Enums\Width;
class ViewReviewPack extends ViewRecord
{
protected static string $resource = ReviewPackResource::class;
protected function getHeaderActions(): array
{
if (ReviewPackResource::isCustomerWorkspaceFlow()) {
return [
$this->openRenderedReportAction([
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1,
'review_id' => $this->record->environment_review_id,
'tenant_filter_id' => request()->query('tenant_filter_id'),
'interpretation_version' => $this->record->environmentReview?->controlInterpretationVersion(),
], 'primary'),
Actions\Action::make('download')
->label(fn (): string => ReviewPackResource::downloadActionLabelFor($this->record))
->icon('heroicon-o-arrow-down-tray')
->color('gray')
->visible(fn (): bool => $this->record->status === ReviewPackStatus::Ready->value)
->url(fn (): string => app(ReviewPackService::class)->generateDownloadUrl($this->record, [
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
]))
->openUrlInNewTab(),
];
}
$regenerateAction = UiEnforcement::forAction(
Actions\Action::make('regenerate')
->label('Regenerate review pack')
->icon('heroicon-o-arrow-path')
->color('gray')
->disabled(fn (): bool => ReviewPackResource::reviewPackGenerationBlocked($this->record->tenant))
->requiresConfirmation()
->modalDescription('This will generate a new review pack with the same options. The current pack will remain available until it expires.')
->action(function (array $data): void {
/** @var ReviewPack $record */
$record = $this->record;
$options = array_merge($record->options ?? [], [
'include_pii' => (bool) ($data['include_pii'] ?? ($record->options['include_pii'] ?? true)),
'include_operations' => (bool) ($data['include_operations'] ?? ($record->options['include_operations'] ?? true)),
]);
$this->regenerateReviewPack($options);
})
->form(function (): array {
/** @var ReviewPack $record */
$record = $this->record;
$currentOptions = $record->options ?? [];
return [
Section::make('Pack options')
->schema([
Toggle::make('include_pii')
->label('Include PII')
->helperText('Include personally identifiable information in the export.')
->default((bool) ($currentOptions['include_pii'] ?? true)),
Toggle::make('include_operations')
->label('Include operations')
->helperText('Include recent operation history in the export.')
->default((bool) ($currentOptions['include_operations'] ?? true)),
]),
];
})
)
->requireCapability(Capabilities::REVIEW_PACK_MANAGE)
->preserveDisabled()
->apply();
$regenerateAction->tooltip(fn (): ?string => ReviewPackResource::reviewPackGenerationActionTooltip($this->record->tenant));
$readyManagementReportPdf = $this->readyManagementReportPdf();
return [
$this->openRenderedReportAction([
'source_surface' => 'review_pack',
'review_id' => $this->record->environment_review_id,
'tenant_filter_id' => request()->query('tenant_filter_id'),
'interpretation_version' => $this->record->environmentReview?->controlInterpretationVersion(),
], $readyManagementReportPdf instanceof StoredReport ? 'gray' : 'primary'),
$readyManagementReportPdf instanceof StoredReport
? $this->downloadManagementReportPdfAction($readyManagementReportPdf)
: $this->downloadReviewPackAction(),
Actions\ActionGroup::make($readyManagementReportPdf instanceof StoredReport
? [
$this->downloadReviewPackAction(),
$regenerateAction,
]
: [
$this->managementReportPdfOverflowAction(),
$regenerateAction,
])
->label(__('More'))
->icon('heroicon-m-ellipsis-vertical')
->color('gray')
->button()
->dropdownWidth(Width::Medium)
->dropdownPlacement('bottom-end')
->tooltip(__('More actions')),
];
}
/**
* @param array<string, mixed> $data
*/
private function regenerateReviewPack(array $data): void
{
/** @var ReviewPack $record */
$record = $this->record;
$record->loadMissing(['environmentReview', 'tenant']);
$review = $record->environmentReview;
if (! $review instanceof EnvironmentReview) {
ReviewPackResource::executeGeneration($data);
return;
}
$user = auth()->user();
if (! $user instanceof User) {
Notification::make()->danger()->title(__('localization.review.unable_export_missing_context'))->send();
return;
}
$service = app(ReviewPackService::class);
if ($service->checkActiveRunForReview($review)) {
OperationUxPresenter::alreadyQueuedToast(OperationRunType::ReviewPackGenerate->value)
->body(__('localization.review.export_already_queued_body'))
->send();
return;
}
$options = [
'include_pii' => (bool) ($data['include_pii'] ?? true),
'include_operations' => (bool) ($data['include_operations'] ?? true),
];
try {
$reviewPack = $service->generateFromReview($review, $user, $options);
} catch (WorkspaceEntitlementBlockedException $exception) {
Notification::make()
->warning()
->title(__('localization.review.executive_pack_export_unavailable'))
->body($exception->getMessage())
->send();
return;
} catch (ReviewPackEvidenceResolutionException $exception) {
$reasons = $exception->result->reasons;
Notification::make()
->danger()
->title(__('localization.review.unable_export_executive_pack'))
->body($reasons === [] ? $exception->getMessage() : implode(' ', $reasons))
->send();
return;
}
if (! $reviewPack->wasRecentlyCreated) {
Notification::make()
->success()
->title(__('localization.review.executive_pack_already_available'))
->body(__('localization.review.executive_pack_already_available_body'))
->actions([
Actions\Action::make('view_pack')
->label(__('localization.review.view_pack'))
->url(ReviewPackResource::getUrl('view', ['record' => $reviewPack], tenant: $review->tenant)),
])
->send();
return;
}
OperationUxPresenter::queuedToast(OperationRunType::ReviewPackGenerate->value)
->body(__('localization.review.executive_pack_generating_background'))
->send();
}
private function downloadReviewPackAction(): Actions\Action
{
return Actions\Action::make('download')
->label(fn (): string => ReviewPackResource::downloadActionLabelFor($this->record))
->icon('heroicon-o-arrow-down-tray')
->color('gray')
->visible(fn (): bool => $this->record->status === ReviewPackStatus::Ready->value)
->url(fn (): string => app(ReviewPackService::class)->generateDownloadUrl($this->record))
->openUrlInNewTab();
}
private function downloadManagementReportPdfAction(StoredReport $readyReport): Actions\Action
{
return Actions\Action::make('download_management_report_pdf')
->label(__('localization.review.download_management_report_pdf'))
->icon('heroicon-o-document-arrow-down')
->color('primary')
->url(fn (): string => app(ManagementReportPdfService::class)->generateDownloadUrl($readyReport, [
'source_surface' => 'review_pack',
]))
->openUrlInNewTab();
}
private function managementReportPdfOverflowAction(): Actions\Action
{
$activeRun = $this->activeManagementReportPdfOperation();
if ($activeRun instanceof OperationRun) {
return Actions\Action::make('open_management_report_pdf_operation')
->label(__('localization.review.open_management_report_pdf_operation'))
->icon('heroicon-o-clock')
->color('gray')
->url(fn (): string => $this->record->tenant
? OperationRunLinks::view($activeRun, $this->record->tenant)
: url('/admin'))
->openUrlInNewTab();
}
$action = UiEnforcement::forAction(
Actions\Action::make('generate_management_report_pdf')
->label(fn (): string => $this->managementReportPdfActionLabel())
->icon('heroicon-o-document-arrow-down')
->color(fn (): string => $this->managementReportPdfTooltip() === null ? 'primary' : 'gray')
->requiresConfirmation()
->modalDescription(__('localization.review.generate_management_report_pdf_confirmation'))
->modalSubmitActionLabel(__('localization.review.generate_management_report_pdf_submit'))
->disabled(fn (): bool => (bool) (app(ManagementReportPdfService::class)->generationDecision($this->record)['is_blocked'] ?? false))
->tooltip(fn (): ?string => $this->managementReportPdfTooltip())
->action(function (): void {
$user = auth()->user();
if (! $user instanceof User) {
abort(403);
}
$result = app(ManagementReportPdfService::class)->startGeneration($this->record, $user);
$mode = (string) ($result['mode'] ?? '');
if ($mode === 'blocked') {
Notification::make()
->warning()
->title(__('localization.review.management_report_pdf_blocked'))
->body((string) data_get($result, 'decision.reason', __('localization.review.management_report_pdf_blocked_default')))
->send();
return;
}
if ($mode === 'ready') {
Notification::make()
->success()
->title(__('localization.review.management_report_pdf_ready'))
->body(__('localization.review.management_report_pdf_ready_body'))
->send();
return;
}
if ($mode === 'active') {
OperationUxPresenter::alreadyRunningToast(OperationRunType::ManagementReportGenerate->value)->send();
return;
}
OperationUxPresenter::queuedToast(OperationRunType::ManagementReportGenerate->value)->send();
}),
)
->requireCapability(Capabilities::REVIEW_PACK_MANAGE)
->preserveDisabled()
->apply();
return $action;
}
private function readyManagementReportPdf(): ?StoredReport
{
return app(ManagementReportPdfService::class)->findReadyReport($this->record);
}
private function activeManagementReportPdfOperation(): ?OperationRun
{
$report = app(ManagementReportPdfService::class)->findActiveReport($this->record);
return $report?->operationRun;
}
private function managementReportPdfTooltip(): ?string
{
$decision = app(ManagementReportPdfService::class)->generationDecision($this->record);
return (bool) ($decision['is_blocked'] ?? false)
? (string) ($decision['reason'] ?? __('localization.review.management_report_pdf_blocked_default'))
: null;
}
private function managementReportPdfActionLabel(): string
{
return $this->managementReportPdfTooltip() === null
? __('localization.review.generate_management_report_pdf')
: __('localization.review.management_report_pdf_blocked');
}
/**
* @param array<string, scalar|null> $parameters
*/
private function openRenderedReportAction(array $parameters = [], string $color = 'primary'): Actions\Action
{
return Actions\Action::make('open_rendered_report')
->label(fn (): string => \App\Filament\Resources\EnvironmentReviewResource::renderedReportActionLabelFor($this->record->environmentReview))
->icon('heroicon-o-document-text')
->color($color)
->visible(fn (): bool => $this->canOpenRenderedReport())
->url(fn (): string => app(ReviewPackService::class)->generateRenderedReportUrl(
$this->record,
$this->renderedReportParameters($parameters),
))
->openUrlInNewTab();
}
/**
* @param array<string, scalar|null> $parameters
* @return array<string, scalar|null>
*/
private function renderedReportParameters(array $parameters): array
{
$parameters = array_filter($parameters, static fn (mixed $value): bool => $value !== null && $value !== '');
$review = $this->record->environmentReview;
if (! $review instanceof EnvironmentReview) {
return $parameters;
}
if (! array_key_exists(ReportProfileRegistry::QUERY_PARAMETER, $parameters)) {
$guidance = ReviewPackOutputResolutionGuidance::fromReadiness(
ReviewPackOutputResolutionGuidance::readinessForReview($review),
);
$parameters[ReportProfileRegistry::QUERY_PARAMETER] = ReportProfileRegistry::defaultForRenderedReportState(
(string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN),
array_key_exists(CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY, $parameters),
);
}
return $parameters;
}
private function canOpenRenderedReport(): bool
{
/** @var ReviewPack $record */
$record = $this->record;
if ($record->status !== ReviewPackStatus::Ready->value) {
return false;
}
if ($record->expires_at !== null && $record->expires_at->isPast()) {
return false;
}
if (! $record->environmentReview instanceof \App\Models\EnvironmentReview) {
return false;
}
return (int) ($record->environmentReview->current_export_review_pack_id ?? 0) === (int) $record->getKey();
}
}