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
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();
|
|
}
|
|
}
|