TenantAtlas/apps/platform/resources/views/review-packs/rendered-report.blade.php
ahmido b7907bd69d feat: add report profile and disclosure policy to rendered review reports (#428)
Implementing report profiles and disclosure policy as per spec 357.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #428
2026-06-06 09:41:19 +00:00

672 lines
32 KiB
PHP

@php
/** @var array<string, mixed> $report */
$badgeClasses = [
'success' => 'background:#ecfdf3;color:#166534;border-color:#bbf7d0;',
'warning' => 'background:#fffbeb;color:#92400e;border-color:#fde68a;',
'danger' => 'background:#fef2f2;color:#991b1b;border-color:#fecaca;',
'gray' => 'background:#f3f4f6;color:#374151;border-color:#d1d5db;',
];
$hero = is_array($report['hero'] ?? null) ? $report['hero'] : [];
$branding = is_array($report['branding'] ?? null) ? $report['branding'] : [];
$managementSummary = is_array($report['management_summary'] ?? null) ? $report['management_summary'] : [];
$evidenceBasis = is_array($report['evidence_basis'] ?? null) ? $report['evidence_basis'] : [];
$profile = is_array($report['profile'] ?? null) ? $report['profile'] : [];
$disclosurePolicy = is_array($report['disclosure_policy'] ?? null) ? $report['disclosure_policy'] : [];
$sourceMetadata = is_array($report['source_metadata'] ?? null) ? $report['source_metadata'] : [];
$heroBadgeStyle = $badgeClasses[$hero['color'] ?? 'gray'] ?? $badgeClasses['gray'];
$boundaryBadgeStyle = $badgeClasses[$report['guidance']['boundary_color'] ?? 'gray'] ?? $badgeClasses['gray'];
$proofBadgeStyles = [
'verified' => $badgeClasses['success'],
'assumed' => $badgeClasses['warning'],
'missing' => $badgeClasses['danger'],
'unknown' => $badgeClasses['gray'],
'not_applicable' => $badgeClasses['gray'],
];
$requestedProfile = $sourceMetadata['requested_profile'] ?? ($profile['requested_key'] ?? null);
$generatedAt = $report['generated_at'] ?? null;
$publishedAt = $report['published_at'] ?? null;
@endphp
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $report['title'] }} · {{ $report['tenant_name'] }}</title>
<style>
:root {
color-scheme: light;
--ink: #111827;
--muted: #4b5563;
--soft: #64748b;
--line: #d9e2ec;
--paper: #fffdf8;
--panel: #f8fafc;
--accent: #0f766e;
--accent-soft: #ccfbf1;
}
* { box-sizing: border-box; }
body {
margin: 0;
color: var(--ink);
background:
radial-gradient(circle at 8% 8%, rgba(20, 83, 45, 0.08), transparent 34%),
radial-gradient(circle at 90% 4%, rgba(15, 118, 110, 0.12), transparent 28%),
linear-gradient(180deg, #eef7f4 0%, #f8fafc 24%, #e5e7eb 100%);
font-family: Avenir, "Avenir Next", "Segoe UI", sans-serif;
}
a { color: inherit; }
.page {
width: min(1080px, calc(100% - 32px));
margin: 0 auto;
padding: 28px 0 64px;
}
.report-toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
color: var(--soft);
font-size: 13px;
}
.toolbar-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.app-action {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(148, 163, 184, 0.5);
border-radius: 999px;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.82);
color: #334155;
text-decoration: none;
font: 700 12px/1.2 Avenir, "Avenir Next", "Segoe UI", sans-serif;
cursor: pointer;
}
.app-action--primary {
background: #0f172a;
border-color: #0f172a;
color: #fff;
}
.report-canvas {
overflow: hidden;
border: 1px solid rgba(148, 163, 184, 0.42);
border-radius: 30px;
background: var(--paper);
box-shadow: 0 28px 70px rgba(15, 23, 42, 0.14);
}
.cover {
padding: clamp(28px, 5vw, 56px);
background:
linear-gradient(135deg, rgba(15, 118, 110, 0.14), transparent 46%),
linear-gradient(180deg, #ffffff 0%, #fffdf8 100%);
border-bottom: 1px solid rgba(148, 163, 184, 0.35);
}
.co-brand {
display: flex;
flex-wrap: wrap;
gap: 10px 18px;
justify-content: space-between;
color: var(--soft);
font: 700 12px/1.4 Avenir, "Avenir Next", "Segoe UI", sans-serif;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.eyebrow {
margin-top: 46px;
color: var(--accent);
font: 800 12px/1.4 Avenir, "Avenir Next", "Segoe UI", sans-serif;
letter-spacing: 0.14em;
text-transform: uppercase;
}
h1 {
max-width: 12ch;
margin: 12px 0 14px;
font-family: Georgia, Cambria, "Times New Roman", serif;
font-size: clamp(2.6rem, 6vw, 5.2rem);
line-height: 0.93;
letter-spacing: -0.05em;
}
.hero-summary {
max-width: 76ch;
margin: 0;
color: var(--muted);
font: 500 17px/1.7 Avenir, "Avenir Next", "Segoe UI", sans-serif;
}
.badges {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 22px 0 0;
}
.badge {
display: inline-flex;
align-items: center;
padding: 8px 12px;
border: 1px solid;
border-radius: 999px;
font: 800 11px/1.2 Avenir, "Avenir Next", "Segoe UI", sans-serif;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.share-warning {
margin-top: 22px;
border: 1px solid #fbbf24;
border-left: 5px solid #d97706;
border-radius: 18px;
background: #fffbeb;
padding: 15px 18px;
color: #78350f;
font: 800 14px/1.55 Avenir, "Avenir Next", "Segoe UI", sans-serif;
}
.meta-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
margin-top: 26px;
}
.meta-item {
border-top: 1px solid rgba(148, 163, 184, 0.5);
padding-top: 12px;
}
.meta-label, .field-label {
margin: 0;
color: var(--soft);
font: 800 10px/1.2 Avenir, "Avenir Next", "Segoe UI", sans-serif;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.meta-value, .field-value {
margin: 7px 0 0;
color: var(--ink);
font: 700 14px/1.45 Avenir, "Avenir Next", "Segoe UI", sans-serif;
}
.content {
display: grid;
gap: 24px;
padding: clamp(24px, 4vw, 44px);
}
.section {
padding-bottom: 24px;
border-bottom: 1px solid rgba(148, 163, 184, 0.32);
}
.section:last-child {
border-bottom: 0;
padding-bottom: 0;
}
h2 {
margin: 0 0 12px;
font-family: Georgia, Cambria, "Times New Roman", serif;
font-size: clamp(1.55rem, 3vw, 2.15rem);
line-height: 1.05;
letter-spacing: -0.025em;
}
h3 {
margin: 0;
font-size: 1rem;
line-height: 1.35;
}
.copy, .empty-state, .list {
color: var(--ink);
font: 500 15px/1.75 Avenir, "Avenir Next", "Segoe UI", sans-serif;
}
.empty-state {
margin: 0;
color: var(--muted);
}
.summary-grid, .risk-grid, .appendix-grid {
display: grid;
gap: 14px;
}
.policy-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 14px;
margin-top: 14px;
}
.summary-grid {
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
margin-top: 14px;
}
.summary-field, .risk-card, .appendix-card, .technical-card {
border: 1px solid rgba(148, 163, 184, 0.32);
border-radius: 18px;
background: rgba(248, 250, 252, 0.72);
padding: 16px;
}
.limitations {
display: grid;
gap: 12px;
margin-top: 14px;
}
.limitation {
border: 1px solid #fde68a;
border-left: 5px solid #d97706;
border-radius: 18px;
background: #fffbeb;
padding: 16px 18px;
}
.limitation p, .risk-card p, .appendix-card p {
margin: 8px 0 0;
}
.note {
color: var(--muted);
font: 500 13px/1.65 Avenir, "Avenir Next", "Segoe UI", sans-serif;
}
.list {
margin: 0;
padding-left: 20px;
}
.list li + li { margin-top: 10px; }
.risk-grid {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
.supporting {
background: #f8fafc;
border-radius: 24px;
padding: 22px;
}
.appendix-grid {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
margin-top: 14px;
}
.appendix-card + .appendix-card { margin-top: 14px; }
.technical-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px;
margin-top: 14px;
}
.screen-only { display: block; }
body.print-preview-smoke .report-toolbar,
body.print-preview-smoke .screen-only {
display: none !important;
}
@media print {
body { background: #fff; }
.page { width: 100%; padding: 0; }
.report-toolbar, .screen-only { display: none !important; }
.report-canvas {
border: 0;
border-radius: 0;
box-shadow: none;
}
.cover, .section, .appendix-card, .risk-card, .summary-field, .technical-card {
break-inside: avoid;
}
}
</style>
</head>
<body>
<div class="page">
<div class="report-toolbar screen-only" data-testid="rendered-report-toolbar">
<div>{{ __('localization.review.rendered_report_html_only') }}</div>
<div class="toolbar-actions">
@if (filled($report['review_url'] ?? null))
<a class="app-action" href="{{ $report['review_url'] }}" target="_blank" rel="noreferrer">
{{ __('localization.review.return_to_review_detail') }}
</a>
@endif
@if (filled($report['review_pack_url'] ?? null))
<a class="app-action" href="{{ $report['review_pack_url'] }}" target="_blank" rel="noreferrer">
{{ __('localization.review.return_to_review_pack_detail') }}
</a>
@endif
@if (filled($report['download_url'] ?? null))
<a class="app-action" href="{{ $report['download_url'] }}" target="_blank" rel="noreferrer">
{{ $report['download_label'] ?? __('localization.review.download_review_pack_with_limitations') }}
</a>
@endif
<button class="app-action app-action--primary" type="button" onclick="window.print()">
{{ __('localization.review.print_rendered_report') }}
</button>
</div>
</div>
<main class="report-canvas" data-testid="rendered-report-canvas">
<section class="cover" data-testid="rendered-report-hero">
<div class="co-brand">
<span>{{ __('localization.review.prepared_by_for', [
'prepared_by' => $branding['prepared_by'] ?? 'TenantPilot',
'prepared_for' => $branding['prepared_for'] ?? $report['tenant_name'],
]) }}</span>
<span>{{ __('localization.review.generated_by', ['generated_by' => $branding['generated_by'] ?? 'TenantPilot']) }}</span>
</div>
<div class="eyebrow">{{ __('localization.review.governance_review_report') }}</div>
<h1>{{ $hero['title'] ?? $report['title'] }}</h1>
<p class="hero-summary">{{ $hero['summary'] ?? $report['executive_summary'] }}</p>
<div class="badges">
<span class="badge" style="{{ $heroBadgeStyle }}">{{ $hero['badge'] ?? __('localization.review.requires_review') }}</span>
<span class="badge" style="{{ $boundaryBadgeStyle }}">{{ $report['guidance']['boundary_label'] ?? __('localization.review.requires_review') }}</span>
</div>
@if (filled($hero['warning'] ?? null))
<div class="share-warning" data-testid="rendered-report-sharing-warning">{{ $hero['warning'] }}</div>
@endif
<div class="meta-row">
<div class="meta-item">
<p class="meta-label">{{ __('localization.review.review_status') }}</p>
<p class="meta-value">#{{ $report['review_id'] }} · {{ $report['review_status_label'] }}</p>
</div>
<div class="meta-item">
<p class="meta-label">{{ __('localization.review.evidence_basis') }}</p>
<p class="meta-value">{{ $evidenceBasis['label'] ?? __('localization.review.unavailable') }}</p>
</div>
<div class="meta-item">
<p class="meta-label">{{ __('localization.review.generated_at') }}</p>
<p class="meta-value">{{ $generatedAt?->format('Y-m-d H:i') ?? '—' }}</p>
</div>
<div class="meta-item">
<p class="meta-label">{{ __('localization.review.published_at') }}</p>
<p class="meta-value">{{ $publishedAt?->format('Y-m-d H:i') ?? '—' }}</p>
</div>
<div class="meta-item">
<p class="meta-label">{{ __('localization.review.report_effective_profile') }}</p>
<p class="meta-value">{{ $profile['label'] ?? __('localization.review.unavailable') }}</p>
</div>
<div class="meta-item">
<p class="meta-label">{{ __('localization.review.report_audience') }}</p>
<p class="meta-value">{{ $profile['audience_label'] ?? __('localization.review.unavailable') }}</p>
</div>
</div>
</section>
<div class="content">
<section class="section" data-testid="rendered-report-executive-summary">
<h2>{{ __('localization.review.executive_summary') }}</h2>
<p class="copy">{{ $report['executive_summary'] }}</p>
<div class="summary-grid">
<div class="summary-field">
<p class="field-label">{{ __('localization.review.overall_state') }}</p>
<p class="field-value">{{ $managementSummary['overall_state'] ?? ($hero['title'] ?? '') }}</p>
</div>
<div class="summary-field">
<p class="field-label">{{ __('localization.review.reason') }}</p>
<p class="field-value">{{ $managementSummary['reason'] ?? '' }}</p>
</div>
<div class="summary-field">
<p class="field-label">{{ __('localization.review.impact') }}</p>
<p class="field-value">{{ $managementSummary['impact'] ?? '' }}</p>
</div>
<div class="summary-field">
<p class="field-label">{{ __('localization.review.recommended_next_action') }}</p>
<p class="field-value">{{ $managementSummary['next_action'] ?? '' }}</p>
</div>
</div>
@if (($managementSummary['top_limitations'] ?? []) !== [])
<ul class="list" style="margin-top:16px;">
@foreach (($managementSummary['top_limitations'] ?? []) as $limitation)
<li>{{ $limitation }}</li>
@endforeach
</ul>
@endif
</section>
<section class="section" data-testid="rendered-report-profile">
<h2>{{ __('localization.review.report_profile') }}</h2>
<div class="summary-grid">
<div class="summary-field">
<p class="field-label">{{ __('localization.review.report_effective_profile') }}</p>
<p class="field-value">{{ $profile['label'] ?? __('localization.review.unavailable') }}</p>
<p class="note">{{ $profile['effective_key'] ?? '—' }}</p>
</div>
<div class="summary-field">
<p class="field-label">{{ __('localization.review.report_audience') }}</p>
<p class="field-value">{{ $profile['audience_label'] ?? __('localization.review.unavailable') }}</p>
</div>
<div class="summary-field">
<p class="field-label">{{ __('localization.review.report_requested_profile') }}</p>
<p class="field-value">{{ filled($requestedProfile) ? $requestedProfile : '—' }}</p>
</div>
<div class="summary-field">
<p class="field-label">{{ __('localization.review.report_source_surface') }}</p>
<p class="field-value">{{ $sourceMetadata['source_surface'] ?? 'review_pack' }}</p>
<p class="note">{{ __('localization.review.interpretation_version') }}: {{ $sourceMetadata['interpretation_version'] ?? '—' }}</p>
</div>
</div>
@if (($profile['is_fallback'] ?? false) === true)
<div class="share-warning" style="margin-top:16px;" data-testid="rendered-report-profile-fallback">
{{ __('localization.review.report_profile_fallback_notice') }}
{{ __('localization.review.report_profile_fallback_summary') }}
</div>
@endif
</section>
@if (($report['limitations'] ?? []) !== [])
<section class="section" data-testid="rendered-report-output-limitations">
<h2>{{ __('localization.review.output_limitations') }}</h2>
<div class="limitations">
@foreach (($report['limitations'] ?? []) as $limitation)
<article class="limitation">
<h3>{{ $limitation['title'] }}</h3>
<p class="copy">{{ $limitation['summary'] }}</p>
<p class="note">{{ $limitation['next_action'] }}</p>
</article>
@endforeach
</div>
</section>
@endif
<section class="section" data-testid="rendered-report-findings">
<h2>{{ __('localization.review.findings_and_open_risks') }}</h2>
@if (($report['top_findings'] ?? []) === [])
<p class="empty-state">{{ __('localization.review.no_open_risks_listed') }}</p>
@else
<ul class="list">
@foreach (($report['top_findings'] ?? []) as $finding)
<li>
<strong>{{ $finding['title'] ?? __('localization.review.control') }}</strong>
@if (filled($finding['summary'] ?? null))
: {{ $finding['summary'] }}
@endif
</li>
@endforeach
</ul>
@endif
</section>
<section class="section" data-testid="rendered-report-accepted-risks">
<h2>{{ __('localization.review.accepted_risks') }}</h2>
@if (($report['accepted_risks'] ?? []) === [])
<p class="empty-state">{{ __('localization.review.no_accepted_risks_listed_for_review') }}</p>
@else
<div class="risk-grid">
@foreach (($report['accepted_risks'] ?? []) as $risk)
<article class="risk-card">
<h3>{{ $risk['title'] }}</h3>
<p class="note">{{ __('localization.review.status') }}: {{ $risk['status'] }} · {{ $risk['review_state'] }}</p>
@if (filled($risk['owner'] ?? null))
<p class="note">{{ __('localization.review.accepted_risk_owner') }}: {{ $risk['owner'] }}</p>
@endif
@if (filled($risk['summary'] ?? null))
<p class="copy">{{ $risk['summary'] }}</p>
@elseif (filled($risk['limitation'] ?? null))
<p class="copy">{{ $risk['limitation'] }}</p>
@endif
</article>
@endforeach
</div>
@endif
</section>
<section class="section" data-testid="rendered-report-governance-decisions">
<h2>{{ __('localization.review.governance_decisions_requiring_awareness') }}</h2>
@if (filled($report['decision_summary']['summary'] ?? null))
<p class="copy">{{ $report['decision_summary']['summary'] }}</p>
@endif
@if (($report['governance_decisions'] ?? []) === [])
<p class="empty-state">{{ __('localization.review.no_decisions_require_awareness_description') }}</p>
@else
<ul class="list">
@foreach (($report['governance_decisions'] ?? []) as $decision)
<li>
<strong>{{ $decision['title'] ?? __('localization.review.governance_decisions') }}</strong>
@if (filled($decision['summary'] ?? null))
: {{ $decision['summary'] }}
@endif
@if (filled($decision['next_action'] ?? null))
<div class="note">{{ $decision['next_action'] }}</div>
@endif
</li>
@endforeach
</ul>
@endif
</section>
<section class="section" data-testid="rendered-report-evidence-basis">
<h2>{{ __('localization.review.evidence_basis') }}</h2>
<p class="copy">{{ $evidenceBasis['description'] ?? $report['evidence_basis_summary'] }}</p>
<p class="note">{{ $evidenceBasis['operator_action'] ?? '' }}</p>
</section>
<section class="section">
<h2>{{ __('localization.review.next_actions') }}</h2>
@if (($report['next_actions'] ?? []) === [])
<p class="empty-state">{{ __('localization.review.no_next_action_listed') }}</p>
@else
<ul class="list">
@foreach (($report['next_actions'] ?? []) as $nextAction)
<li>{{ $nextAction }}</li>
@endforeach
</ul>
@endif
</section>
<section class="section" data-testid="rendered-report-disclosure">
<h2>{{ __('localization.review.disclosure_policy') }}</h2>
@if (($disclosurePolicy['blocking_reasons'] ?? []) !== [])
<div class="limitations">
@foreach (($disclosurePolicy['blocking_reasons'] ?? []) as $reason)
<article class="limitation">
<h3>{{ $reason['label'] ?? __('localization.review.blocked') }}</h3>
<p class="copy">{{ $reason['summary'] ?? '' }}</p>
</article>
@endforeach
</div>
@endif
@if (($disclosurePolicy['warnings'] ?? []) !== [])
<div class="summary-grid" style="margin-top:14px;">
@foreach (($disclosurePolicy['warnings'] ?? []) as $warning)
<div class="summary-field">
<p class="field-label">{{ $warning['label'] ?? __('localization.review.requires_review') }}</p>
<p class="field-value">{{ $warning['summary'] ?? '' }}</p>
</div>
@endforeach
</div>
@endif
<div class="policy-grid">
@foreach (($disclosurePolicy['mandatory_disclosures'] ?? []) as $disclosure)
<article class="technical-card">
<div style="display:flex;flex-wrap:wrap;justify-content:space-between;gap:10px;align-items:center;">
<p class="field-label">{{ $disclosure['label'] ?? __('localization.review.non_certification_disclosure') }}</p>
<span class="badge" style="{{ $proofBadgeStyles[$disclosure['proof_state'] ?? 'unknown'] ?? $proofBadgeStyles['unknown'] }}">
{{ __('localization.review.proof_state_'.($disclosure['proof_state'] ?? 'unknown')) }}
</span>
</div>
<p class="copy">{{ $disclosure['summary'] ?? '' }}</p>
</article>
@endforeach
</div>
</section>
<section class="section supporting" data-testid="rendered-report-supporting-appendix">
<h2>{{ __('localization.review.supporting_appendix') }}</h2>
<p class="copy">{{ __('localization.review.rendered_report_appendix_note') }}</p>
@if (($disclosurePolicy['show_section_appendix'] ?? false) !== true)
<p class="copy">{{ __('localization.review.report_appendix_hidden_for_profile') }}</p>
@else
<div class="technical-grid" data-testid="rendered-report-technical-details">
<div class="technical-card">
<p class="field-label">{{ __('localization.review.executive_entrypoint') }}</p>
<p class="field-value">{{ $report['entrypoint_file'] }}</p>
</div>
<div class="technical-card">
<p class="field-label">{{ __('localization.review.auditor_appendix') }}</p>
<p class="field-value">{{ implode(', ', $report['appendix_files'] ?? []) ?: '—' }}</p>
</div>
@foreach (($report['technical_details'] ?? []) as $label => $value)
<div class="technical-card">
<p class="field-label">{{ $label }}</p>
<p class="field-value">{{ $value }}</p>
</div>
@endforeach
</div>
@foreach (($report['section_appendix'] ?? []) as $section)
<article class="appendix-card">
<div style="display:flex;flex-wrap:wrap;justify-content:space-between;gap:10px;align-items:baseline;">
<h3>{{ $section['title'] }}</h3>
<span class="badge" style="{{ $badgeClasses['gray'] }}">{{ $section['completeness_label'] }}</span>
</div>
@if (($section['highlights'] ?? []) !== [])
<p class="copy">{{ implode(' ', $section['highlights']) }}</p>
@endif
@if (($section['entries'] ?? []) !== [])
<div class="appendix-grid">
@foreach (($section['entries'] ?? []) as $entry)
<div class="technical-card">
<p class="field-value">{{ $entry['title'] }}</p>
@if (filled($entry['summary'] ?? null))
<p class="note">{{ $entry['summary'] }}</p>
@endif
</div>
@endforeach
</div>
@endif
@if (($section['summary_items'] ?? []) !== [])
<div class="appendix-grid">
@foreach (($section['summary_items'] ?? []) as $item)
<div class="technical-card">
<p class="field-label">{{ $item['label'] }}</p>
<p class="field-value">{{ $item['value'] }}</p>
</div>
@endforeach
</div>
@endif
@if (($section['next_actions'] ?? []) !== [])
<ul class="list" style="margin-top:16px;">
@foreach (($section['next_actions'] ?? []) as $nextAction)
<li>{{ $nextAction }}</li>
@endforeach
</ul>
@endif
@if (filled($section['disclosure'] ?? null))
<p class="note">{{ $section['disclosure'] }}</p>
@endif
</article>
@endforeach
@endif
</section>
</div>
</main>
</div>
</body>
</html>