TenantAtlas/apps/platform/app/Support/ResolutionGuidance/ReviewOutputResolveActionMapper.php
ahmido f37056e1de feat: implement management report layout branded report themes (#437)
Implemented management report layout branded report themes as requested.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #437
2026-06-08 03:35:20 +00:00

377 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\ResolutionGuidance;
use App\Models\EnvironmentReview;
use App\Support\Audit\AuditActionId;
use App\Support\Auth\Capabilities;
use App\Support\EnvironmentReviewStatus;
use App\Support\OperationRunType;
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
final class ReviewOutputResolveActionMapper
{
/**
* @param array<string, mixed> $guidance
* @param array{
* review:?string,
* evidence:?string,
* operation:?string,
* download:?string,
* successor_review:?string
* } $urls
* @param array{
* can_manage_review?:bool,
* successor_review_status?:?string
* } $execution
* @return array{
* primary_action:array{
* key:string,
* label:string,
* type:string,
* url:?string,
* icon:string,
* kind:string,
* action_name:?string,
* capability:?string,
* requires_confirmation:bool,
* audit_event:?string,
* operation_run_type:?string,
* disabled_reason:?string
* },
* secondary_actions:list<array{
* key:string,
* label:string,
* type:string,
* url:?string,
* icon:string,
* kind:string,
* action_name:?string,
* capability:?string,
* requires_confirmation:bool,
* audit_event:?string,
* operation_run_type:?string,
* disabled_reason:?string
* }>
* }
*/
public static function map(
EnvironmentReview $review,
array $guidance,
string $sourceSurface,
array $urls,
array $execution = [],
): array {
$fallbackPrimary = ResolutionAction::fromArray(
is_array($guidance['primary_action'] ?? null) ? $guidance['primary_action'] : null,
self::caseKey((string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN)).'.primary_action',
__('localization.review.review_output_limitations'),
);
$fallbackSecondary = self::secondaryActions($guidance);
$candidatePrimary = self::candidatePrimaryAction($review, $guidance, $sourceSurface, $urls, $execution);
if ($candidatePrimary === null) {
return [
'primary_action' => $fallbackPrimary,
'secondary_actions' => $fallbackSecondary,
];
}
$primaryAction = ResolutionAction::fromArray(
$candidatePrimary,
self::caseKey((string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN)).'.primary_action',
__('localization.review.review_output_limitations'),
);
$secondaryActions = self::deduplicateActions(array_merge(
self::shouldPromoteFallbackPrimary($fallbackPrimary, $primaryAction) ? [$fallbackPrimary] : [],
$fallbackSecondary,
), $primaryAction['key']);
return [
'primary_action' => $primaryAction,
'secondary_actions' => $secondaryActions,
];
}
/**
* @param array<string, mixed> $guidance
* @param array{
* review:?string,
* evidence:?string,
* operation:?string,
* download:?string,
* successor_review:?string
* } $urls
* @param array{
* can_manage_review?:bool,
* successor_review_status?:?string
* } $execution
* @return array{
* key:string,
* label:string,
* type:string,
* url:?string,
* icon:string,
* kind:string,
* action_name:?string,
* capability:?string,
* requires_confirmation:bool,
* audit_event:?string,
* operation_run_type:?string
* }|null
*/
private static function candidatePrimaryAction(
EnvironmentReview $review,
array $guidance,
string $sourceSurface,
array $urls,
array $execution,
): ?array {
$state = (string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN);
$canManageReview = (bool) ($execution['can_manage_review'] ?? false);
$successorReviewUrl = self::normalizedUrl($urls['successor_review'] ?? null);
$successorReviewStatus = EnvironmentReviewStatus::tryFrom((string) ($execution['successor_review_status'] ?? ''));
if ($successorReviewUrl !== null) {
return [
'key' => 'open_successor_review',
'label' => $successorReviewStatus?->isMutable()
? __('localization.review.open_draft_review')
: __('localization.review.open_successor_review'),
'type' => ResolutionAction::TYPE_NAVIGATION,
'url' => $successorReviewUrl,
'icon' => 'heroicon-o-arrow-top-right-on-square',
'kind' => 'environment_link',
'action_name' => $sourceSurface === 'environment_review_detail'
? 'open_successor_review'
: null,
'capability' => null,
'requires_confirmation' => false,
'audit_event' => null,
'operation_run_type' => null,
];
}
if ($review->statusEnum() === EnvironmentReviewStatus::Ready) {
if (! $canManageReview || ! self::supportsExecutableAction($sourceSurface, 'publish_review')) {
return null;
}
return [
'key' => 'publish_review',
'label' => __('localization.review.publish_review'),
'type' => ResolutionAction::TYPE_DOMAIN_ACTION,
'url' => null,
'icon' => 'heroicon-o-check-badge',
'kind' => 'environment_action',
'action_name' => 'publish_review',
'capability' => Capabilities::ENVIRONMENT_REVIEW_MANAGE,
'requires_confirmation' => true,
'audit_event' => AuditActionId::EnvironmentReviewPublished->value,
'operation_run_type' => null,
];
}
if ($review->isMutable() && $state !== ReviewPackOutputResolutionGuidance::STATE_CUSTOMER_SAFE_READY) {
if (! $canManageReview || ! self::supportsExecutableAction($sourceSurface, 'refresh_review')) {
return null;
}
return [
'key' => 'refresh_review',
'label' => __('localization.review.refresh_review'),
'type' => ResolutionAction::TYPE_OPERATION_ACTION,
'url' => null,
'icon' => 'heroicon-o-arrow-path',
'kind' => 'environment_action',
'action_name' => 'refresh_review',
'capability' => Capabilities::ENVIRONMENT_REVIEW_MANAGE,
'requires_confirmation' => true,
'audit_event' => AuditActionId::EnvironmentReviewRefreshed->value,
'operation_run_type' => OperationRunType::EnvironmentReviewCompose->value,
];
}
if (! $review->isPublished() || ! in_array($state, [
ReviewPackOutputResolutionGuidance::STATE_PUBLICATION_BLOCKED,
ReviewPackOutputResolutionGuidance::STATE_PUBLISHED_WITH_LIMITATIONS,
ReviewPackOutputResolutionGuidance::STATE_INTERNAL_ONLY,
], true)) {
return null;
}
if (! $canManageReview || ! self::supportsExecutableAction($sourceSurface, 'create_next_review')) {
return null;
}
return [
'key' => 'create_next_review',
'label' => __('localization.review.create_next_review'),
'type' => ResolutionAction::TYPE_OPERATION_ACTION,
'url' => null,
'icon' => 'heroicon-o-document-duplicate',
'kind' => 'environment_action',
'action_name' => $sourceSurface === 'customer_review_workspace'
? 'createNextReview'
: 'create_next_review',
'capability' => Capabilities::ENVIRONMENT_REVIEW_MANAGE,
'requires_confirmation' => true,
'audit_event' => AuditActionId::EnvironmentReviewSuccessorCreated->value,
'operation_run_type' => OperationRunType::EnvironmentReviewCompose->value,
];
}
private static function supportsExecutableAction(string $sourceSurface, string $actionName): bool
{
return match ($sourceSurface) {
'customer_review_workspace' => $actionName === 'create_next_review',
'environment_review_detail' => in_array($actionName, [
'refresh_review',
'publish_review',
'create_next_review',
], true),
default => false,
};
}
/**
* @param array<string, mixed> $guidance
* @return list<array{
* key:string,
* label:string,
* type:string,
* url:?string,
* icon:string,
* kind:string,
* action_name:?string,
* capability:?string,
* requires_confirmation:bool,
* audit_event:?string,
* operation_run_type:?string,
* disabled_reason:?string
* }>
*/
private static function secondaryActions(array $guidance): array
{
$secondaryActions = is_array($guidance['secondary_actions'] ?? null) ? $guidance['secondary_actions'] : [];
return array_values(array_map(
static fn (array $action, int $index): array => ResolutionAction::fromArray(
$action,
self::caseKey((string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN)).'.secondary_action_'.$index,
__('localization.review.review_output_limitations'),
),
array_values(array_filter($secondaryActions, static fn (mixed $action): bool => is_array($action))),
array_keys(array_values(array_filter($secondaryActions, static fn (mixed $action): bool => is_array($action)))),
));
}
/**
* @param list<array{
* key:string,
* label:string,
* type:string,
* url:?string,
* icon:string,
* kind:string,
* action_name:?string,
* capability:?string,
* requires_confirmation:bool,
* audit_event:?string,
* operation_run_type:?string,
* disabled_reason:?string
* }> $actions
* @return list<array{
* key:string,
* label:string,
* type:string,
* url:?string,
* icon:string,
* kind:string,
* action_name:?string,
* capability:?string,
* requires_confirmation:bool,
* audit_event:?string,
* operation_run_type:?string,
* disabled_reason:?string
* }>
*/
private static function deduplicateActions(array $actions, string $primaryKey): array
{
$seen = [$primaryKey => true];
$deduplicated = [];
foreach ($actions as $action) {
if (($action['type'] ?? ResolutionAction::TYPE_NONE) === ResolutionAction::TYPE_NONE) {
continue;
}
$key = (string) ($action['key'] ?? '');
if ($key === '' || isset($seen[$key])) {
continue;
}
$seen[$key] = true;
$deduplicated[] = $action;
}
return $deduplicated;
}
/**
* @param array{
* key:string,
* label:string,
* type:string,
* url:?string,
* icon:string,
* kind:string,
* action_name:?string,
* capability:?string,
* requires_confirmation:bool,
* audit_event:?string,
* operation_run_type:?string,
* disabled_reason:?string
* } $fallbackPrimary
* @param array{
* key:string,
* label:string,
* type:string,
* url:?string,
* icon:string,
* kind:string,
* action_name:?string,
* capability:?string,
* requires_confirmation:bool,
* audit_event:?string,
* operation_run_type:?string,
* disabled_reason:?string
* } $primaryAction
*/
private static function shouldPromoteFallbackPrimary(array $fallbackPrimary, array $primaryAction): bool
{
if ($fallbackPrimary['key'] === $primaryAction['key']) {
return false;
}
return $fallbackPrimary['type'] !== ResolutionAction::TYPE_NONE
&& ($fallbackPrimary['url'] !== null || $fallbackPrimary['action_name'] !== null);
}
private static function normalizedUrl(mixed $url): ?string
{
return is_string($url) && trim($url) !== ''
? trim($url)
: null;
}
private static function caseKey(string $state): string
{
return 'review_output.'.$state;
}
}