Implemented management report layout branded report themes as requested. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #437
222 lines
7.8 KiB
PHP
222 lines
7.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\ReviewPacks;
|
|
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\Workspace;
|
|
|
|
final class ReportThemeResolver
|
|
{
|
|
public const string DEFAULT_ACCENT = 'teal';
|
|
|
|
/**
|
|
* @param array<string, mixed> $profile
|
|
* @param array<string, mixed> $guidance
|
|
* @param array<string, mixed> $evidenceBasis
|
|
* @param array<string, mixed> $metrics
|
|
* @return array{
|
|
* identity:array{prepared_by:string,prepared_for:string,generated_by:string,generated_at:mixed,accent:string,logo:null},
|
|
* layout:array{mode:string,appendix_prominence:string,section_order:list<string>,section_ranks:array<string,int>},
|
|
* kpi_strip:list<array{key:string,label:string,value:string,description:string}>
|
|
* }
|
|
*/
|
|
public static function resolve(
|
|
ReviewPack $reviewPack,
|
|
ManagedEnvironment $tenant,
|
|
array $profile,
|
|
array $guidance,
|
|
array $evidenceBasis,
|
|
array $metrics,
|
|
): array {
|
|
$workspace = $tenant->workspace;
|
|
|
|
return [
|
|
'identity' => self::identityFor(
|
|
$workspace instanceof Workspace ? $workspace->name : null,
|
|
$tenant->name,
|
|
$reviewPack->generated_at,
|
|
),
|
|
'layout' => self::layoutForProfile($profile),
|
|
'kpi_strip' => self::kpiStrip($guidance, $evidenceBasis, $metrics),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{prepared_by:string,prepared_for:string,generated_by:string,generated_at:mixed,accent:string,logo:null}
|
|
*/
|
|
public static function identityFor(?string $workspaceName, ?string $environmentName, mixed $generatedAt = null): array
|
|
{
|
|
return [
|
|
'prepared_by' => self::displayText($workspaceName, 'TenantPilot'),
|
|
'prepared_for' => self::displayText($environmentName, __('localization.review.tenant')),
|
|
'generated_by' => 'TenantPilot',
|
|
'generated_at' => $generatedAt,
|
|
'accent' => self::DEFAULT_ACCENT,
|
|
'logo' => null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $profile
|
|
* @return array{mode:string,appendix_prominence:string,section_order:list<string>,section_ranks:array<string,int>}
|
|
*/
|
|
public static function layoutForProfile(array $profile): array
|
|
{
|
|
$profileKey = (string) ($profile['effective_key'] ?? ($profile['profile_key'] ?? ReportProfileRegistry::DEFAULT_PROFILE));
|
|
|
|
[$mode, $appendixProminence, $sectionOrder] = match ($profileKey) {
|
|
ReportProfileRegistry::CUSTOMER_EXECUTIVE => [
|
|
'executive',
|
|
'minimal',
|
|
[
|
|
'executive_summary',
|
|
'profile',
|
|
'limitations',
|
|
'findings',
|
|
'governance_decisions',
|
|
'accepted_risks',
|
|
'evidence_basis',
|
|
'next_actions',
|
|
'disclosure',
|
|
'appendix',
|
|
'technical_details',
|
|
],
|
|
],
|
|
ReportProfileRegistry::CUSTOMER_TECHNICAL => [
|
|
'technical',
|
|
'standard',
|
|
[
|
|
'executive_summary',
|
|
'profile',
|
|
'evidence_basis',
|
|
'limitations',
|
|
'findings',
|
|
'governance_decisions',
|
|
'accepted_risks',
|
|
'next_actions',
|
|
'disclosure',
|
|
'appendix',
|
|
'technical_details',
|
|
],
|
|
],
|
|
ReportProfileRegistry::AUDITOR_APPENDIX => [
|
|
'auditor_appendix',
|
|
'high',
|
|
[
|
|
'profile',
|
|
'evidence_basis',
|
|
'disclosure',
|
|
'limitations',
|
|
'appendix',
|
|
'technical_details',
|
|
'executive_summary',
|
|
'findings',
|
|
'governance_decisions',
|
|
'accepted_risks',
|
|
'next_actions',
|
|
],
|
|
],
|
|
default => [
|
|
'internal',
|
|
'standard',
|
|
[
|
|
'executive_summary',
|
|
'profile',
|
|
'limitations',
|
|
'evidence_basis',
|
|
'findings',
|
|
'governance_decisions',
|
|
'accepted_risks',
|
|
'next_actions',
|
|
'disclosure',
|
|
'appendix',
|
|
'technical_details',
|
|
],
|
|
],
|
|
};
|
|
|
|
$sectionRanks = [];
|
|
|
|
foreach ($sectionOrder as $rank => $sectionKey) {
|
|
$sectionRanks[$sectionKey] = ($rank + 1) * 10;
|
|
}
|
|
|
|
return [
|
|
'mode' => $mode,
|
|
'appendix_prominence' => $appendixProminence,
|
|
'section_order' => $sectionOrder,
|
|
'section_ranks' => $sectionRanks,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $guidance
|
|
* @param array<string, mixed> $evidenceBasis
|
|
* @param array<string, mixed> $metrics
|
|
* @return list<array{key:string,label:string,value:string,description:string}>
|
|
*/
|
|
public static function kpiStrip(array $guidance, array $evidenceBasis, array $metrics): array
|
|
{
|
|
return [
|
|
[
|
|
'key' => 'governance_status',
|
|
'label' => __('localization.review.report_kpi_governance_status'),
|
|
'value' => self::displayText($guidance['boundary_label'] ?? null, __('localization.review.unavailable')),
|
|
'description' => self::displayText(
|
|
$guidance['primary_reason'] ?? null,
|
|
__('localization.review.report_summary_default_impact'),
|
|
),
|
|
],
|
|
[
|
|
'key' => 'evidence_coverage',
|
|
'label' => __('localization.review.report_kpi_evidence_coverage'),
|
|
'value' => self::displayText($evidenceBasis['label'] ?? null, __('localization.review.unavailable')),
|
|
'description' => self::displayText($evidenceBasis['description'] ?? null, __('localization.review.report_evidence_missing_description')),
|
|
],
|
|
[
|
|
'key' => 'key_risks',
|
|
'label' => __('localization.review.report_kpi_key_risks'),
|
|
'value' => self::countValue($metrics['top_findings'] ?? null, (bool) ($metrics['top_findings_measured'] ?? false)),
|
|
'description' => __('localization.review.report_kpi_key_risks_description'),
|
|
],
|
|
[
|
|
'key' => 'open_decisions',
|
|
'label' => __('localization.review.report_kpi_open_decisions'),
|
|
'value' => self::countValue($metrics['governance_decisions'] ?? null, (bool) ($metrics['governance_decisions_measured'] ?? false)),
|
|
'description' => __('localization.review.report_kpi_open_decisions_description'),
|
|
],
|
|
];
|
|
}
|
|
|
|
private static function countValue(mixed $items, bool $measured): string
|
|
{
|
|
if (! $measured) {
|
|
return __('localization.review.report_kpi_not_measured');
|
|
}
|
|
|
|
if (! is_array($items)) {
|
|
return __('localization.review.unavailable');
|
|
}
|
|
|
|
return (string) count($items);
|
|
}
|
|
|
|
private static function displayText(mixed $value, string $fallback): string
|
|
{
|
|
if (! is_scalar($value) && $value !== null) {
|
|
return $fallback;
|
|
}
|
|
|
|
$text = preg_replace('/\s+/', ' ', trim((string) $value));
|
|
|
|
if (! is_string($text) || $text === '') {
|
|
return $fallback;
|
|
}
|
|
|
|
return str_starts_with($text, 'localization.') ? $fallback : $text;
|
|
}
|
|
}
|