TenantAtlas/apps/platform/app/Services/ReviewPacks/ManagementReportPdfRenderer.php
ahmido dbff2a0a90 feat(report): implement management report pdf runtime (#450)
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
2026-06-15 11:36:29 +00:00

156 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\ReviewPacks;
use App\Services\Pdf\PdfRenderingGateway;
use App\Services\Pdf\PdfRenderRequest;
use App\Services\Pdf\PdfRenderResult;
final class ManagementReportPdfRenderer
{
public function __construct(
private readonly PdfRenderingGateway $pdfRenderingGateway,
) {}
/**
* @param array<string, mixed> $payload
*/
public function render(array $payload, string $correlationId): PdfRenderResult
{
return $this->pdfRenderingGateway->renderHtml(new PdfRenderRequest(
html: $this->html($payload),
options: [
'printBackground' => true,
'preferCssPageSize' => true,
'displayHeaderFooter' => true,
'headerTemplate' => '<div style="font-size:8px;color:#6b7280;width:100%;padding:0 16mm;">TenantPilot Management Report</div>',
'footerTemplate' => '<div style="font-size:8px;color:#6b7280;width:100%;padding:0 16mm;text-align:right;"><span class="pageNumber"></span>/<span class="totalPages"></span></div>',
],
correlationId: $correlationId,
outputFilename: 'tenantpilot-management-report',
));
}
/**
* @param array<string, mixed> $payload
*/
private function html(array $payload): string
{
$title = e((string) ($payload['title'] ?? 'TenantPilot Management Report'));
$environment = e((string) data_get($payload, 'managed_environment.name', 'Managed environment'));
$profile = e((string) ($payload['profile_label'] ?? 'Customer executive'));
$classification = e((string) ($payload['classification'] ?? 'Customer-facing management report'));
$generatedAt = e((string) data_get($payload, 'provenance.generated_at', now()->toIso8601String()));
$chapters = is_array($payload['chapters'] ?? null) ? $payload['chapters'] : [];
$chapterHtml = collect($chapters)
->filter(static fn (mixed $chapter): bool => is_array($chapter))
->map(fn (array $chapter): string => $this->chapterHtml($chapter))
->implode("\n");
return <<<HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{$title}</title>
<style>
@page { size: A4; margin: 18mm 16mm; }
body { color: #111827; font-family: Arial, sans-serif; font-size: 12px; line-height: 1.5; }
h1 { color: #111827; font-size: 28px; margin: 0 0 8px; }
h2 { border-bottom: 1px solid #d1d5db; color: #111827; font-size: 18px; margin: 28px 0 12px; padding-bottom: 6px; }
h3 { color: #374151; font-size: 13px; margin: 12px 0 4px; }
p { margin: 0 0 8px; }
ul { margin: 0 0 10px 18px; padding: 0; }
li { margin-bottom: 6px; }
.cover { border-bottom: 2px solid #111827; margin-bottom: 22px; padding-bottom: 18px; }
.meta { color: #4b5563; font-size: 11px; }
.kv { display: grid; grid-template-columns: 140px 1fr; gap: 4px 12px; margin-bottom: 10px; }
.kv div:nth-child(odd) { color: #6b7280; font-weight: 700; }
.empty { color: #6b7280; font-style: italic; }
</style>
</head>
<body>
<section class="cover">
<h1>{$title}</h1>
<p>{$environment}</p>
<p class="meta">Profile: {$profile} · Classification: {$classification} · Generated: {$generatedAt}</p>
</section>
{$chapterHtml}
</body>
</html>
HTML;
}
/**
* @param array<string, mixed> $chapter
*/
private function chapterHtml(array $chapter): string
{
$title = e((string) ($chapter['title'] ?? 'Chapter'));
$content = is_array($chapter['content'] ?? null) ? $chapter['content'] : [];
$contentHtml = $this->contentHtml($content);
return <<<HTML
<section>
<h2>{$title}</h2>
{$contentHtml}
</section>
HTML;
}
/**
* @param array<string, mixed> $content
*/
private function contentHtml(array $content): string
{
if ($content === []) {
return '<p class="empty">No items recorded.</p>';
}
$html = '';
foreach ($content as $key => $value) {
if (is_array($value)) {
$html .= '<h3>'.e($this->humanize((string) $key)).'</h3>'.$this->arrayHtml($value);
} elseif ($value !== null && trim((string) $value) !== '') {
$html .= '<div class="kv"><div>'.e($this->humanize((string) $key)).'</div><div>'.e((string) $value).'</div></div>';
}
}
return $html !== '' ? $html : '<p class="empty">No items recorded.</p>';
}
/**
* @param array<mixed> $items
*/
private function arrayHtml(array $items): string
{
if ($items === []) {
return '<p class="empty">No items recorded.</p>';
}
$listItems = collect($items)
->map(function (mixed $item): string {
if (is_array($item)) {
$parts = collect($item)
->map(fn (mixed $value, string|int $key): string => '<strong>'.e($this->humanize((string) $key)).':</strong> '.e((string) $value))
->implode('<br>');
return '<li>'.$parts.'</li>';
}
return '<li>'.e((string) $item).'</li>';
})
->implode('');
return '<ul>'.$listItems.'</ul>';
}
private function humanize(string $value): string
{
return ucfirst(str_replace('_', ' ', $value));
}
}