TenantAtlas/apps/platform/app/Support/ReviewPacks/ReportThemeResolver.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

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