@php $fmt = function ($minutes) use ($decimalFormat) { if ($decimalFormat) { $negative = $minutes < 0; return ($negative ? '-' : '') . number_format(abs($minutes) / 60, 2, ',', '.') . ' h.'; } return tbFormat($minutes); }; $num = fn($value) => number_format($value, 0, ',', '.'); $logoPath = theme_email_logo_file(); $logo = file_exists($logoPath) ? 'data:image/png;base64,' . base64_encode(file_get_contents($logoPath)) : null; @endphp {{ $title['header'] }} @if ($logo) @endif
{{ platform_name() }}

{{ $title['header'] }}

@if (!empty($appliedFilters)) {{-- A table, not inline spans: dompdf ignores flex and gap, so cells are the only reliable way to separate these. --}} @foreach ($appliedFilters as $item) @endforeach
{{ $item['label'] }}
{{ $item['value'] }}
@else
{{ $title['sub'] }}
@endif
{{-- On its own row: a fifth column would squeeze the four above it, and dompdf gives the cells a fixed share of the page width. --}}
{{ ($partnerScoped ?? false) ? __('Partner profiles') : __('Profiles') }}
{{ $num($stats['profileCount']) }}
{{ __('Transactions') }}
{{ $num($stats['economic']['transaction_count']) }}
{{ __('Total volume') }}
{{ $fmt($stats['economic']['total_volume']) }}
{{ __('Total balance') }}
{{ $fmt($stats['balance']) }}
{{ __('Incomplete') }}
@php $incompleteCount = (int) ($stats['online']['incomplete'] ?? 0); $profileTotal = (int) ($stats['profileCount'] ?? 0); @endphp {{ $profileTotal > 0 ? number_format(100 * $incompleteCount / $profileTotal, 1, ',', '.') . '%' : '-' }}

{{ __('Economic activity') }}

{{ __('Internal:') }} {{ $fmt($stats['economic']['internal_volume']) }} · {{ __('External:') }} {{ $fmt($stats['economic']['external_volume']) }}
@if ($stats['economic']['by_type']->isNotEmpty()) @foreach ($stats['economic']['by_type'] as $type) @endforeach
{{ __('Transaction type') }} {{ __('Count') }} {{ __('Volume') }}
{{ $type['type_name'] }} {{ $num($type['count']) }} {{ $fmt($type['volume']) }}
@endif @if (!empty($charts['volume'])) @endif

{{ ($partnerScoped ?? false) ? __('Partner profiles and activity') : __('Profiles and activity') }}

@foreach ($stats['online']['by_type'] as $typeName => $row) @endforeach
{{ ($partnerScoped ?? false) ? __('Partner profile type') : __('Profile type') }} {{ __('Total') }} {{ __('Active') }} {{ __('Inactive') }} {{ __('Recent active < 30d') }}
{{ __($typeName) }} {{ $num($row['total']) }} {{ $num($row['active']) }} {{ $num($row['inactive']) }} {{ $row['recent_logins'] === null ? '-' : $num($row['recent_logins']) }}
{{-- One chart per profile type, two to a row — same split as the screen. --}} @php $registrationCharts = collect([ ['key' => 'registrations', 'label' => __('Users')], ['key' => 'registrationsOrg', 'label' => __('Organizations')], ])->filter(fn($c) => !empty($charts[$c['key']]))->values(); @endphp @if ($registrationCharts->isNotEmpty()) @foreach ($registrationCharts as $chart) @endforeach @if ($registrationCharts->count() === 1) @endif
{{ $chart['label'] }}
{{ $chart['label'] }}
@endif

{{ __('Engagement') }}

{{ __('Posts') }}
{{ $num($stats['engagement']['posts']) }}
{{ __('Calls') }}
{{ $num($stats['engagement']['calls']) }}
{{ __('Reactions') }}
{{ $num($stats['engagement']['reactions']) }}
{{ __('Messages') }}
{{ $num($stats['engagement']['messages']) }}
{{-- One chart per metric, two per row: reactions run in the thousands where posts run in single digits, so a shared axis hides the smaller series. --}} @php $engagementCharts = collect([ ['key' => 'engagementPosts', 'label' => __('Posts')], ['key' => 'engagementCalls', 'label' => __('Calls')], ['key' => 'engagementReactions', 'label' => __('Reactions')], ['key' => 'engagementMessages', 'label' => __('Messages')], ])->filter(fn($c) => !empty($charts[$c['key']]))->values(); @endphp @if ($engagementCharts->isNotEmpty()) @foreach ($engagementCharts->chunk(2) as $row) @foreach ($row as $chart) @endforeach @if ($row->count() === 1) @endif @endforeach
{{ $chart['label'] }}
{{ $chart['label'] }}
@endif {{-- Balance distribution: one chart per profile type, same split as the screen. Banks are excluded server-side. --}} @php $balanceCharts = collect([ ['key' => 'balance', 'label' => __('Users')], ['key' => 'balanceOrg', 'label' => __('Organizations')], ])->filter(fn($c) => !empty($charts[$c['key']]))->values(); @endphp @if ($balanceCharts->isNotEmpty())

{{ __('Balance distribution') }}

@if (!empty($stats['balance_distribution']['profiles_counted']))

{{ __('Current balance of the :count user and organization profiles in the selection, in hours. Banks are excluded.', ['count' => $stats['balance_distribution']['profiles_counted']]) }}

@endif {{-- One per row, full width: these histograms carry ~27 banded labels, which are unreadable at half the page width. --}} @foreach ($balanceCharts as $chart)
{{ $chart['label'] }}
{{ $chart['label'] }}
@endforeach @endif {{-- Trading network, captured exactly as rendered on screen. --}} @if (!empty($charts['network']))

{{ __('Trading network') }}

{{-- The question this view answers, matching the caption shown above the graph on screen so the export reads the same way. --}} @if (!empty($networkLegend['view_caption']))

{{ $networkLegend['view_caption'] }}

@endif {{-- Counts from the classifier, which runs in the browser: captured at export time and carried through the session, since the server cannot recompute them. --}} @if (!empty($networkSubtext))

{{ $networkSubtext }}

@endif @if (!empty($networkSettings)) {{-- A printed graph carries no controls, so name the settings that produced it. --}} @foreach ($networkSettings as $item) @endforeach
{{ $item['label'] }}
{{ $item['value'] }}
@endif {{ __('Trading network') }} @include('partials.network-legend-pdf') @endif @if ($stats['distribution']['top_profiles']->isNotEmpty())

{{ ($partnerScoped ?? false) ? __('Most active partner profiles') : __('Most active profiles') }}

@if (!empty($stats['distribution']['excluded_profiles']))

{{ __(':profiles excluded from this table.', ['profiles' => implode(', ', $stats['distribution']['excluded_profiles'])]) }}

@endif @foreach ($stats['distribution']['top_profiles'] as $profile) @endforeach
{{ ($partnerScoped ?? false) ? __('Partner profile') : __('Profile') }} {{ __('Type') }} {{ __('Paid') }} {{ __('Received') }} {{ __('Paid volume') }} {{ __('Received volume') }} {{ __('Trading partners') }} {{ __('Transactions') }}
{{ ($anonymize ?? false) ? $profile['anonymous_name'] : $profile['name'] }} {{ __($profile['type']) }} {{ $num($profile['paid_count']) }} {{ $num($profile['received_count']) }} {{ $fmt($profile['paid_volume']) }} {{ $fmt($profile['received_volume']) }} {{ $num($profile['partners'] ?? 0) }} {{ $num($profile['count']) }}
@endif @if ($stats['distribution']['by_location']->isNotEmpty())

{{ __('Activity by location') }}

@if (!empty($stats['distribution']['excluded_profiles']))

{{ __(':profiles excluded from this table.', ['profiles' => implode(', ', $stats['distribution']['excluded_profiles'])]) }}

@endif @foreach ($stats['distribution']['by_location'] as $row) @endforeach
{{ __('Location') }} {{ __('Paid') }} {{ __('Received') }} {{ __('Paid volume') }} {{ __('Received volume') }} {{ __('Transactions') }}
{{ $row['label'] ?: __('Location not specified') }} {{ $num($row['paid_count']) }} {{ $num($row['received_count']) }} {{ $fmt($row['paid_volume']) }} {{ $fmt($row['received_volume']) }} {{ $num($row['count']) }}
@endif {{-- Profiles marked as inactive: cumulative, so it sits alone at full width. --}} @if (!empty($charts['inactivations']))

{{ ($partnerScoped ?? false) ? __('Partner profiles marked as inactive over time') : __('Profiles marked as inactive over time') }}

{{ __('How many profiles stood marked as inactive at the end of each month, counted cumulatively. Profiles marked as inactive before the period are included in the starting figure.') }}

@foreach (\App\Services\StatsService::chartLegendNotes() as $note)

{{ $note }}

@endforeach {{ __('Profiles marked as inactive over time') }} @endif