Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 5m7s
Added jobs, controllers, and PDF generation logic for management report runtime as defined in Spec 379. Includes artifact migrations, payload builders, and testing coverage.
402 lines
17 KiB
PHP
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();
|
|
}
|
|
}
|