Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
@@ -0,0 +1,989 @@
|
||||
<div class="relative mb-12 mt-0 w-full min-w-full leading-normal" id="single-report-component">
|
||||
|
||||
<!-- Decimal format toggle -->
|
||||
@if($accountsData && $accountsData->count() > 0)
|
||||
<div class="mb-6 flex items-center gap-3">
|
||||
<x-toggle wire:model.live="decimalFormat" id="decimalFormatReport" name="decimalFormatReport" />
|
||||
<span class="text-xs text-theme-light">{{ __('Show in decimals') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Export buttons - always visible but disabled when no period selected -->
|
||||
<div class="mb-8 mt-0 flex w-full justify-end">
|
||||
<div class="space-x-2">
|
||||
@if($this->fromDate && $this->toDate && $accountsData && $accountsData->count() > 0)
|
||||
<x-jetstream.secondary-button wire:click="$dispatch('export-pdf-with-charts')" wire:loading.attr="disabled"
|
||||
data-network-pdf-trigger>
|
||||
<span wire:loading.remove wire:target="handleExportPdfWithCharts">
|
||||
<x-icon class="mr-1 h-4 w-4" name="arrow-down-tray" />
|
||||
{{ __('PDF') }}
|
||||
</span>
|
||||
<span wire:loading wire:target="handleExportPdfWithCharts">
|
||||
<x-icon class="mr-1 h-4 w-4 animate-spin" name="arrow-path" />
|
||||
{{ __('Loading...') }}
|
||||
</span>
|
||||
</x-jetstream.secondary-button>
|
||||
@else
|
||||
<x-jetstream.secondary-button disabled class="cursor-not-allowed opacity-50">
|
||||
<x-icon class="mr-1 h-4 w-4" name="arrow-down-tray" />
|
||||
{{ __('PDF') }}
|
||||
</x-jetstream.secondary-button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($accountsData && $accountsData->count() > 0)
|
||||
<!-- Account Balances Table -->
|
||||
<h3 class="text-lg font-medium text-theme-primary">
|
||||
{{ __('Account Balance') }}
|
||||
</h3>
|
||||
<p class="text-sm text-theme-secondary mt-2">
|
||||
{{ __('Balance overview of your accounts') }}
|
||||
</p>
|
||||
<div class="relative mb-24 w-full min-w-full leading-normal">
|
||||
<table class="w-full" id="account-balances">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b border-theme-primary py-6 text-left">
|
||||
<span class="px-0 py-2 text-sm font-normal text-theme-secondary">
|
||||
{{ __('Account Name') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="border-b border-theme-primary py-6 text-right">
|
||||
<span class="px-0 py-2 text-sm font-normal text-theme-secondary">
|
||||
{{ __('Start Balance') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="border-b border-theme-primary py-6 text-right">
|
||||
<span class="px-0 py-2 text-sm font-normal text-theme-secondary">
|
||||
{{ __('End Balance') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="border-b border-theme-primary py-6 text-right">
|
||||
<span class="px-0 py-2 text-sm font-normal text-theme-secondary">
|
||||
{{ __('Difference') }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($accountsData as $account)
|
||||
<tr>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-sm">
|
||||
<div class="whitespace-no-wrap text-theme-primary font-medium">
|
||||
{{ $account['name'] }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-right text-sm">
|
||||
<span class="whitespace-no-wrap text-theme-primary">
|
||||
{{ $account['start_balance_formatted'] }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-right text-sm">
|
||||
<span class="whitespace-no-wrap text-theme-primary">
|
||||
{{ $account['end_balance_formatted'] }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-right text-sm">
|
||||
<span class="whitespace-no-wrap @if($account['difference'] >= 0) text-theme-primary @else text-red-700 @endif">
|
||||
{{ $account['difference_formatted'] }}@if($account['difference'] > 0) +@endif
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
<!-- Totals Row -->
|
||||
<tr class="bg-gray-100">
|
||||
<td class="border-b border-theme-primary bg-gray-100 px-2 py-3 text-sm">
|
||||
<div class="whitespace-no-wrap text-theme-primary font-bold">
|
||||
{{ __('TOTALS') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border-b border-theme-primary bg-gray-100 px-2 py-3 text-right text-sm">
|
||||
<span class="whitespace-no-wrap text-theme-primary font-bold">
|
||||
{{ $this->formatAmount($accountsData->sum('start_balance')) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="border-b border-theme-primary bg-gray-100 px-2 py-3 text-right text-sm">
|
||||
<span class="whitespace-no-wrap text-theme-primary font-bold">
|
||||
{{ $this->formatAmount($accountsData->sum('end_balance')) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="border-b border-theme-primary bg-gray-100 px-2 py-3 text-right text-sm">
|
||||
@php $totalDifference = $accountsData->sum('difference'); @endphp
|
||||
<span class="whitespace-no-wrap font-bold @if($totalDifference >= 0) text-theme-primary @else text-red-700 @endif">
|
||||
{{ $this->formatAmount($totalDifference) }}@if($totalDifference > 0) +@endif
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Transaction Types Breakdown -->
|
||||
@if($transactionTypesData && $transactionTypesData->count() > 0)
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-medium text-theme-primary">
|
||||
{{ __('Transacion types') }}
|
||||
</h3>
|
||||
<p class="text-sm text-theme-secondary mt-2">
|
||||
{{ __('Overview of the different transaction purposes') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="relative mb-24 mt-12 w-full min-w-full leading-normal">
|
||||
|
||||
<table class="w-full text-left" id="transaction-types">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b border-theme-primary py-6">
|
||||
<span class="px-0 py-2 text-sm font-normal text-theme-secondary">
|
||||
{{ __('Transaction Type') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="border-b border-theme-primary py-6 text-right">
|
||||
<span class="px-0 py-2 text-sm font-normal text-theme-secondary">
|
||||
{{ __('Credit') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="border-b border-theme-primary py-6 text-right">
|
||||
<span class="px-0 py-2 text-sm font-normal text-theme-secondary">
|
||||
{{ __('Debit') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="border-b border-theme-primary py-6 text-right">
|
||||
<span class="px-0 py-2 text-sm font-normal text-theme-secondary">
|
||||
{{ __('Total') }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($transactionTypesData as $transactionType)
|
||||
<tr>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-sm">
|
||||
<div class="whitespace-no-wrap text-theme-primary">
|
||||
{{ $transactionType['type_name'] }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-right text-sm">
|
||||
<span class="whitespace-no-wrap text-theme-primary">
|
||||
{{ $transactionType['incoming_formatted'] }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-right text-sm">
|
||||
<span class="whitespace-no-wrap text-red-700">
|
||||
{{ $transactionType['outgoing_formatted'] }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-right text-sm">
|
||||
<span class="whitespace-no-wrap @if($transactionType['net'] >= 0) text-theme-primary @else text-red-700 @endif">
|
||||
{{ $transactionType['net_formatted'] }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Account Balances Chart -->
|
||||
@if($accountBalancesTimelineData && count($accountBalancesTimelineData) > 4)
|
||||
<div class="relative mb-24 mt-12 w-full min-w-full leading-normal">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-medium text-theme-primary">
|
||||
{{ __('Account Balance Over Time') }}
|
||||
</h3>
|
||||
<p class="text-sm text-theme-secondary mt-2">
|
||||
{{ __('Track your account balance over time') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm border border-theme-primary p-6"
|
||||
data-balance-chart-data="{{ json_encode($accountBalancesTimelineData) }}"
|
||||
data-decimal-format="{{ $decimalFormat ? '1' : '0' }}"
|
||||
data-date-range="{{ $this->fromDate }} to {{ $this->toDate }}">
|
||||
|
||||
{{-- 16:9, like every chart on the statistics page. The
|
||||
sized wrapper is what Chart.js measures. --}}
|
||||
<div class="relative aspect-video">
|
||||
<canvas id="accountBalancesChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
<!-- Balance Distribution of trading partners (organizations and banks,
|
||||
and only above the partner threshold — see
|
||||
SingleReport::balanceDistribution()). Counts per band only: no
|
||||
partner is named, listed or otherwise identifiable here. -->
|
||||
@if($balanceDistribution && ($balanceDistribution['withheld'] ?? false))
|
||||
{{-- Too few trading partners: with only a handful of profiles per
|
||||
band, a reader who knows who this profile deals with could read
|
||||
an individual's balance off the chart. --}}
|
||||
<div class="relative mb-24 mt-12 w-full min-w-full leading-normal">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-medium text-theme-primary">
|
||||
{{ __('Balance distribution of your trading partners') }}
|
||||
</h3>
|
||||
</div>
|
||||
<p class="text-sm text-theme-secondary">
|
||||
{{ __('Not shown: you have :count trading partners in this period, and at least :minimum are needed to keep individual balances private.', [
|
||||
'count' => $balanceDistribution['profiles_counted'],
|
||||
'minimum' => $balanceDistribution['minimum'],
|
||||
]) }}
|
||||
</p>
|
||||
</div>
|
||||
@elseif($balanceDistribution)
|
||||
@php
|
||||
$bdRows = collect($balanceDistribution['bands'])
|
||||
->map(function ($band, $i) use ($balanceDistribution) {
|
||||
foreach ($balanceDistribution['series'] as $type => $counts) {
|
||||
$band[$type] = $counts[$i] ?? 0;
|
||||
}
|
||||
return $band;
|
||||
})
|
||||
->values();
|
||||
@endphp
|
||||
|
||||
<div class="relative mb-24 mt-12 w-full min-w-full leading-normal">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-medium text-theme-primary">
|
||||
{{ __('Balance distribution of your trading partners') }}
|
||||
</h3>
|
||||
<p class="text-sm text-theme-secondary mt-2">
|
||||
{{ __('How the balances of your :count trading partners are spread, in hours.', ['count' => $balanceDistribution['profiles_counted']]) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{-- One chart per profile type, each with its own y-axis, as on
|
||||
the platform statistics page. Organizations are far fewer
|
||||
than users, so on a shared axis scaled to hundreds of users
|
||||
a bar of one organization is sub-pixel and reads as missing
|
||||
data. --}}
|
||||
@php
|
||||
$bdCharts = [
|
||||
[
|
||||
'field' => 'User',
|
||||
'label' => __('Users'),
|
||||
'canvas' => 'reportBalanceDistributionChart',
|
||||
'attr' => 'data-report-balance-distribution',
|
||||
],
|
||||
[
|
||||
'field' => 'Organization',
|
||||
'label' => __('Organizations'),
|
||||
'canvas' => 'reportBalanceDistributionOrgChart',
|
||||
'attr' => 'data-report-balance-distribution-org',
|
||||
],
|
||||
];
|
||||
@endphp
|
||||
|
||||
{{-- Stacked so each chart gets the full width; at half width the
|
||||
banded axis is too cramped to read. --}}
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
@foreach ($bdCharts as $chart)
|
||||
@php
|
||||
$bdTotal = $bdRows->sum(fn($row) => $row[$chart['field']] ?? 0);
|
||||
|
||||
// Trim each axis to its own series. The bands are
|
||||
// computed across both types together, so a band
|
||||
// holding only an organization leaves a run of empty
|
||||
// bars on the user chart — and vice versa. Keep one
|
||||
// empty band past the last occupied one, as the
|
||||
// platform statistics page does.
|
||||
$bdSeriesRows = $bdRows->values();
|
||||
$bdLast = $bdSeriesRows->count() - 1;
|
||||
while (
|
||||
$bdLast > 0
|
||||
&& ($bdSeriesRows[$bdLast][$chart['field']] ?? 0) === 0
|
||||
&& ($bdSeriesRows[$bdLast - 1][$chart['field']] ?? 0) === 0
|
||||
) {
|
||||
$bdLast--;
|
||||
}
|
||||
// Same at the leading end: a chart whose first bar
|
||||
// sits in band five should not open with four empty
|
||||
// ones.
|
||||
$bdFirst = 0;
|
||||
while (
|
||||
$bdFirst < $bdLast
|
||||
&& ($bdSeriesRows[$bdFirst][$chart['field']] ?? 0) === 0
|
||||
&& ($bdSeriesRows[$bdFirst + 1][$chart['field']] ?? 0) === 0
|
||||
) {
|
||||
$bdFirst++;
|
||||
}
|
||||
$bdSeriesRows = $bdSeriesRows->slice($bdFirst, $bdLast - $bdFirst + 1)->values();
|
||||
@endphp
|
||||
@if ($bdTotal > 0)
|
||||
<div class="bg-white rounded-lg shadow-sm border border-theme-primary p-6">
|
||||
<h4 class="text-xs font-medium text-theme-secondary uppercase tracking-wider mb-2">
|
||||
{{ $chart['label'] }}
|
||||
</h4>
|
||||
|
||||
<div {{ $chart['attr'] }}="{{ json_encode($bdSeriesRows) }}"
|
||||
data-series-labels="{{ json_encode([$chart['field'] => $chart['label']]) }}">
|
||||
<div class="relative aspect-video">
|
||||
<canvas id="{{ $chart['canvas'] }}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
{{-- No bordered box: an empty frame reads as a chart
|
||||
that failed to draw. --}}
|
||||
<p class="text-sm text-theme-secondary">
|
||||
{{ $chart['label'] }}: {{ __('No profiles of this type in the selection.') }}
|
||||
</p>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
<!-- Return Ratio Timeline Chart -->
|
||||
@if($isOrganization && $returnRatioTimelineData && count($returnRatioTimelineData) > 4)
|
||||
<!-- Loading indicator positioned above chart -->
|
||||
<div id="chartLoadingIndicator" class="flex items-center justify-center space-x-2 px-3 py-2 rounded-md mb-4" style="display: flex !important; visibility: visible !important;">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-2 border-gray-300 border-t-gray-900"></div>
|
||||
<span class="text-sm font-bold">
|
||||
{{ __('Loading chart...') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative mb-24 mt-12 w-full min-w-full leading-normal">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-medium text-theme-primary">
|
||||
{{ __('Reciprocity Rate Timeline') }}
|
||||
</h3>
|
||||
<p class="text-sm text-theme-secondary mt-2">
|
||||
{{ __('Reciprocity rate: the share of your Hours that were also returned by the same people you helped during this period.') }}
|
||||
</p>
|
||||
<p class="text-sm text-theme-secondary mt-1">
|
||||
{{ __('A high rate means you are part of a more closed exchange network with frequent returning exchange partners, while a low rate suggests you are part of a more open network with many different exchange partners.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm border border-theme-primary p-6"
|
||||
data-chart-data="{{ json_encode($returnRatioTimelineData) }}"
|
||||
data-trend-data="{{ json_encode($returnRatioTrendData) }}"
|
||||
data-date-range="{{ $this->fromDate }} to {{ $this->toDate }}">
|
||||
|
||||
{{-- 16:9, like every chart on the statistics page. The
|
||||
sized wrapper is what Chart.js measures. --}}
|
||||
<div class="relative aspect-video">
|
||||
<canvas id="returnRatioChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Period Statistics Table -->
|
||||
<div class="">
|
||||
<h3 class="text-lg font-medium text-theme-primary">
|
||||
{{ __('Transaction Relation Statistics') }}
|
||||
</h3>
|
||||
<p class="text-sm text-theme-secondary mt-2">
|
||||
{{ __('Which types of profiles have you transacted with?') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="relative mb-12 w-full min-w-full leading-normal">
|
||||
<table class="w-full" id="period-statistics">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b border-theme-primary text-left">
|
||||
<span class="px-0 py-2 text-sm font-normal text-theme-secondary">
|
||||
{{ __('Period Statistics') }}
|
||||
</span>
|
||||
</th>
|
||||
<th class="border-b border-theme-primary py-6 text-right">
|
||||
<span class="px-0 py-2 text-sm font-normal text-theme-secondary">
|
||||
{{ __('Value') }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-sm">
|
||||
<div class="whitespace-no-wrap text-theme-primary">
|
||||
{{ __('Number of Transactions') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-right text-sm">
|
||||
<span class="whitespace-no-wrap text-theme-primary">
|
||||
{{ number_format($statisticsData['transaction_count']) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@if($statisticsData['profiles_by_type'] && $statisticsData['profiles_by_type']->count() > 0)
|
||||
@foreach($statisticsData['profiles_by_type'] as $modelType => $count)
|
||||
<tr>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-sm">
|
||||
<div class="whitespace-no-wrap text-theme-primary">
|
||||
{{ __('Unique ' . class_basename($modelType) . 's') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-right text-sm">
|
||||
<span class="whitespace-no-wrap text-theme-primary">
|
||||
{{ number_format($count) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endif
|
||||
<tr>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-sm">
|
||||
<div class="whitespace-no-wrap text-theme-primary">
|
||||
{{ __('Total unique profiles') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-right text-sm">
|
||||
<span class="whitespace-no-wrap text-theme-primary">
|
||||
{{ number_format($statisticsData['unique_profiles']) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@if($isOrganization)
|
||||
<tr>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-sm">
|
||||
<div class="whitespace-no-wrap text-theme-primary cursor-help"
|
||||
title="{{ __('Of all the time you gave, how much came back. Partners you gave more time are weighted accordingly.') }}">
|
||||
{{ __('Average reciprocity rate') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-right text-sm">
|
||||
<span class="whitespace-no-wrap text-theme-primary">
|
||||
{{ number_format($statisticsData['average_return_ratio'], 1) }}%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{{-- Withheld below the partner threshold: with only a few
|
||||
partners the median is arithmetically the mean and
|
||||
reads as a finding when it is not. --}}
|
||||
@if(!is_null($statisticsData['median_return_ratio'] ?? null))
|
||||
<tr>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-sm">
|
||||
<div class="whitespace-no-wrap text-theme-primary cursor-help"
|
||||
title="{{ __('The typical partnership: half of your partners returned more than this, half returned less. A median far from the average means a few partnerships carry the total.') }}">
|
||||
{{ __('Median reciprocity rate') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border-b border-theme-primary bg-theme-background px-2 py-2 text-right text-sm">
|
||||
<span class="whitespace-no-wrap text-theme-primary">
|
||||
{{ number_format($statisticsData['median_return_ratio'], 1) }}%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@else
|
||||
<div class="text-left py-8">
|
||||
<div class="text-theme-secondary">
|
||||
{{ __('Please select a period to generate the account balance report') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<script>
|
||||
// Register trigger-chart-export listener once globally (guard against re-registration on Livewire morphs)
|
||||
if (!window._chartExportListenerRegistered) {
|
||||
window._chartExportListenerRegistered = true;
|
||||
|
||||
function registerChartExportListener() {
|
||||
Livewire.on('trigger-chart-export', () => {
|
||||
if (typeof window.exportPdfWithChart === 'function') {
|
||||
window.exportPdfWithChart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Livewire may already be initialized (component renders after livewire:init fires)
|
||||
if (typeof window.Livewire !== 'undefined') {
|
||||
registerChartExportListener();
|
||||
} else {
|
||||
document.addEventListener('livewire:init', registerChartExportListener);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
{{-- Trading network. Banks only: it shows who trades with whom among the
|
||||
bank's own clients. Scoping happens server-side in networkScope(). --}}
|
||||
@if ($this->showsNetwork())
|
||||
@php
|
||||
$network = $this->networkData();
|
||||
@endphp
|
||||
|
||||
<div class="mt-10" data-network-section>
|
||||
<div class="cursor-help" title="{{ __('Each profile is a dot, each trading relationship a line. Dot size follows the traded volume, line thickness the number of transactions.') }}">
|
||||
<h3 class="text-sm font-semibold text-theme-primary uppercase tracking-wide">{{ __('Trading network') }}</h3>
|
||||
<p class="text-xs text-theme-secondary">
|
||||
{{ __('Trade between your own clients.') }}
|
||||
{{ __('Period for this graph:') }} {{ $this->networkPeriodLabel() }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<x-jetstream.secondary-button wire:click="toggleNetwork" wire:loading.attr="disabled" wire:target="toggleNetwork">
|
||||
{{ $showNetwork ? __('Hide network') : __('Show network') }}
|
||||
</x-jetstream.secondary-button>
|
||||
{{-- Shown only while the request is in flight: the graph pulls in
|
||||
~570 kB of lazily loaded JavaScript plus its own aggregation,
|
||||
so the wait needs acknowledging rather than looking like
|
||||
nothing happened. --}}
|
||||
<span class="ml-3 text-xs text-theme-secondary"
|
||||
wire:loading wire:target="toggleNetwork">
|
||||
{{ __('Loading... this may take a while.') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if ($showNetwork && $network)
|
||||
@php
|
||||
$ns = $network['stats'];
|
||||
$ceiling = \App\Services\StatsService::DEGREE_CEILING;
|
||||
$networkPeriods = [
|
||||
'inherit' => __('Same period as the report'),
|
||||
'since_one_year_ago' => __('Since one year ago'),
|
||||
'previous_year' => __('Previous year'),
|
||||
'previous_quarter' => __('Previous quarter'),
|
||||
'previous_month' => __('Previous month'),
|
||||
'all_time' => __('All time'),
|
||||
];
|
||||
$typeOptions = [
|
||||
'User' => __('Users'),
|
||||
'Organization' => __('Organizations'),
|
||||
'Bank' => __('Banks'),
|
||||
];
|
||||
$layouts = [
|
||||
'force' => __('Force-directed'),
|
||||
'circle' => __('Circle'),
|
||||
'concentric' => __('Rings by connections'),
|
||||
'tree' => __('Tree'),
|
||||
'grid' => __('Grid'),
|
||||
];
|
||||
// From the component, so the report's buttons cannot drift
|
||||
// from the admin stats page.
|
||||
$views = \App\Http\Livewire\Admin\Stats::networkViewOptions();
|
||||
@endphp
|
||||
|
||||
<div class="mt-4 flex flex-wrap items-center gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xs text-theme-secondary whitespace-nowrap">{{ __('Trading partners') }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-theme-secondary" for="report-network-min">{{ __('min') }}</label>
|
||||
<input id="report-network-min" type="range" min="0" max="{{ $ceiling }}" step="1"
|
||||
wire:model.change="networkMinDegree" class="w-28 accent-theme-brand">
|
||||
<span class="text-xs text-theme-primary font-semibold w-6 text-right">{{ $networkMinDegree }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-theme-secondary" for="report-network-max">{{ __('max') }}</label>
|
||||
<input id="report-network-max" type="range" min="0" max="{{ $ceiling }}" step="1"
|
||||
wire:model.change="networkMaxDegree" class="w-28 accent-theme-brand">
|
||||
<span class="text-xs text-theme-primary font-semibold w-8 text-right">
|
||||
{{ $networkMaxDegree >= $ceiling ? '>' . $ceiling : $networkMaxDegree }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<x-toggle id="report-network-hub" name="report-network-hub" wire:model.live="networkHideHub" />
|
||||
<span class="text-xs text-theme-secondary">{{ __('Hide the largest hub') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xs text-theme-secondary">{{ __('Show') }}</span>
|
||||
@foreach ($typeOptions as $type => $label)
|
||||
<label class="inline-flex items-center gap-1.5 text-xs text-theme-primary cursor-pointer">
|
||||
<input type="checkbox" wire:click="toggleNetworkType('{{ $type }}')"
|
||||
@checked(in_array($type, $networkTypes, true))
|
||||
class="rounded border-theme-border text-theme-brand focus:ring-theme-brand">
|
||||
{{ $label }}
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- No wire:model: purely browser behaviour. --}}
|
||||
<label class="inline-flex items-center gap-1.5 text-xs text-theme-primary cursor-pointer">
|
||||
<input type="checkbox" checked data-network-drag-neighbours
|
||||
class="rounded border-theme-border text-theme-brand focus:ring-theme-brand">
|
||||
{{ __('Drag trading partners along') }}
|
||||
</label>
|
||||
|
||||
<div class="flex items-center gap-3"
|
||||
title="{{ __('Shows numbered labels instead of names in the network graph. The same profile gets the same number as on the statistics page.') }}">
|
||||
<x-toggle id="report-network-anonymize" name="report-network-anonymize"
|
||||
wire:model.live="anonymizeProfiles" />
|
||||
<span class="text-xs text-theme-secondary cursor-help">{{ __('Anonymise profile names') }}</span>
|
||||
</div>
|
||||
|
||||
{{-- Same controls as the admin stats page: these change
|
||||
which profiles the query resolves, so they go through
|
||||
Livewire and rebuild the graph. --}}
|
||||
<label class="inline-flex items-center gap-1.5 text-xs text-theme-primary cursor-pointer"
|
||||
title="{{ __('Profiles closed by their owner. Their hours were transferred out on closure, so those flows only appear here.') }}">
|
||||
<input type="checkbox" wire:model.live="networkIncludeRemoved"
|
||||
class="rounded border-theme-border text-theme-brand focus:ring-theme-brand">
|
||||
{{ __('Include removed profiles') }}
|
||||
</label>
|
||||
|
||||
<label class="inline-flex items-center gap-1.5 text-xs text-theme-primary cursor-pointer"
|
||||
title="{{ __('Profiles marked inactive. Drawn with a red outline.') }}">
|
||||
<input type="checkbox" wire:model.live="networkMarkInactive"
|
||||
class="rounded border-theme-border text-theme-brand focus:ring-theme-brand">
|
||||
{{ __('Highlight inactive profiles') }}
|
||||
</label>
|
||||
|
||||
@if ($networkView === 'circulation')
|
||||
@php $stoppedCeiling = \App\Http\Livewire\Admin\Stats::STOPPED_AFTER_CEILING; @endphp
|
||||
<div class="flex items-center gap-3"
|
||||
title="{{ __('A profile counts as stopped when it has not paid anyone within this period, or when it is flagged inactive.') }}">
|
||||
<span class="text-xs text-theme-secondary whitespace-nowrap cursor-help">{{ __('Define \'stopped\' at') }}</span>
|
||||
<input id="report-network-stopped" type="range" min="1" max="{{ $stoppedCeiling }}" step="1"
|
||||
wire:model.change="networkStoppedAfterMonths" class="w-28 accent-theme-brand">
|
||||
<span class="text-xs text-theme-primary font-semibold whitespace-nowrap">
|
||||
@if ($networkStoppedAfterMonths >= $stoppedCeiling)
|
||||
>{{ $stoppedCeiling }} {{ __('months') }}
|
||||
@else
|
||||
{{ $networkStoppedAfterMonths }} {{ __('months') }}
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<x-toggle id="report-network-edge" name="report-network-edge" wire:model.live="networkEdge" />
|
||||
<span class="text-xs text-theme-secondary">{{ __('Incl. network outside current bank') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs text-theme-secondary">{{ __('Arrangement') }}</span>
|
||||
@foreach ($layouts as $key => $label)
|
||||
<button type="button" wire:click="setNetworkLayout('{{ $key }}')"
|
||||
class="px-2 py-1 text-xs rounded border {{ $networkLayout === $key ? 'border-theme-brand text-theme-primary font-semibold' : 'border-theme-border text-theme-secondary' }}">
|
||||
{{ $label }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs text-theme-secondary">{{ __('View') }}</span>
|
||||
@foreach ($views as $key => $label)
|
||||
<button type="button" wire:click="setNetworkView('{{ $key }}')"
|
||||
class="px-2 py-1 text-xs rounded border {{ $networkView === $key ? 'border-theme-brand text-theme-primary font-semibold' : 'border-theme-border text-theme-secondary' }}">
|
||||
{{ $label }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-xs text-theme-secondary">
|
||||
{{ __('Showing :shown of :total profiles with at least :min trading partners.', [
|
||||
'shown' => number_format($ns['shown_nodes'], 0, ',', '.'),
|
||||
'total' => number_format($ns['total_nodes'], 0, ',', '.'),
|
||||
'min' => $ns['min_degree'],
|
||||
]) }}
|
||||
</p>
|
||||
|
||||
{{-- Search and period stay outside the empty/non-empty guard, so
|
||||
an empty graph does not hide the controls that fix it. --}}
|
||||
<div class="mt-3 mb-3 flex flex-wrap items-start gap-3">
|
||||
@if ($ns['shown_nodes'] >= 2)
|
||||
<div class="relative max-w-sm flex-1 min-w-[16rem]" data-network-search>
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<input type="search" autocomplete="off" data-network-search-input
|
||||
placeholder="{{ __('Search a profile in the graph') }}"
|
||||
class="block w-full rounded-md border border-theme-border bg-white py-2 pl-10 pr-3 leading-5 placeholder-gray-400 shadow-sm sm:text-sm">
|
||||
<div class="absolute z-30 mt-1 w-full rounded-md border border-theme-border bg-white shadow-lg"
|
||||
data-network-search-results hidden></div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="min-w-[13rem]">
|
||||
<select wire:model.live="networkPeriod"
|
||||
class="block w-full rounded-md border border-theme-border bg-white py-2 pl-3 pr-8 leading-5 shadow-sm sm:text-sm">
|
||||
@foreach ($networkPeriods as $key => $label)
|
||||
<option value="{{ $key }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Vector export of the graph alone. No wire:click: a
|
||||
Livewire round trip would rebuild the graph and discard
|
||||
the very view the user is asking to export. --}}
|
||||
<button type="button" data-network-svg
|
||||
data-filename="bank-network"
|
||||
title="{{ __('Download the graph exactly as shown, as an SVG vector file with editable text.') }}"
|
||||
class="inline-flex items-center gap-1.5 self-start rounded-md border border-theme-border bg-theme-surface px-3 py-2 text-sm text-theme-primary shadow-sm hover:bg-theme-brand hover:text-white focus:outline-none focus:ring-2 focus:ring-theme-brand">
|
||||
<x-icon name="arrow-down-tray" class="h-4 w-4" />
|
||||
{{ __('SVG') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if ($ns['shown_nodes'] < 2)
|
||||
<p class="mt-3 text-sm text-theme-secondary">
|
||||
@if ($ns['total_nodes'] > 0)
|
||||
{{ __('Too little mutual trading in this selection to draw a network. Lower the minimum number of trading partners.') }}
|
||||
@else
|
||||
{{ __('Your clients did not trade with each other in this period. Choose a longer period, or include the network outside the current bank.') }}
|
||||
@endif
|
||||
</p>
|
||||
@else
|
||||
<div class="p-4 bg-theme-background rounded-lg border border-theme-border"
|
||||
data-stats-network="{{ json_encode($network) }}"
|
||||
data-network-meta="{{ json_encode((function () use ($network) {
|
||||
$l = $this->networkLegend($network);
|
||||
|
||||
return [
|
||||
'caption' => $l['view_caption'] ?? '',
|
||||
// The legend's explanation line; see the stats view.
|
||||
'hint' => !empty($l['view_hint'])
|
||||
? ($l['view_label'] ?? '') . ': ' . $l['view_hint']
|
||||
: null,
|
||||
// Top right of the SVG export, like the posters.
|
||||
'logo' => \App\Http\Livewire\Admin\Stats::networkExportLogo(),
|
||||
'period' => \App\Http\Livewire\Admin\Stats::exportPeriodLabel(
|
||||
$this->effectiveNetworkPeriodLabel(),
|
||||
$network['stats']['last_date'] ?? null
|
||||
),
|
||||
|
||||
'colorMeaning' => __('Colour') . ': ' . ($l['color_meaning'] ?? ''),
|
||||
'colors' => $l['colors'] ?? [],
|
||||
'sizeLabel' => __('Dot size'),
|
||||
'sizeMeaning' => $l['size_meaning'] ?? '',
|
||||
'widthLabel' => __('Line thickness'),
|
||||
'widthMeaning' => $l['width_meaning'] ?? '',
|
||||
// Only the currency-flow view populates these; the
|
||||
// SVG legend was the one surface that never showed them.
|
||||
'directions' => $l['directions'] ?? [],
|
||||
'settings' => collect($this->networkSettingsSummary())
|
||||
->map(fn ($r) => $r['label'] . ': ' . $r['value'])
|
||||
->implode(' | '),
|
||||
];
|
||||
})()) }}"
|
||||
data-hide-hub="{{ $networkHideHub ? '1' : '0' }}"
|
||||
data-network-anonymize="{{ $anonymizeProfiles ? '1' : '0' }}"
|
||||
data-network-mark-inactive="{{ $networkMarkInactive ? '1' : '0' }}"
|
||||
data-network-stopped-after="{{ $networkStoppedAfterMonths >= \App\Http\Livewire\Admin\Stats::STOPPED_AFTER_CEILING ? '' : $networkStoppedAfterMonths * 30 }}"
|
||||
data-network-pseudonyms="{{ json_encode([
|
||||
'User' => __('User'),
|
||||
'Organization' => __('Organization'),
|
||||
]) }}"
|
||||
{{-- The platform's own currency, so amounts render as the rest
|
||||
of the site does rather than with a hardcoded suffix. --}}
|
||||
data-currency="{{ json_encode([
|
||||
'symbol' => platform_currency_symbol(),
|
||||
'atEnd' => (bool) platform_trans('platform_currency_position_end', null, false),
|
||||
'thousands' => platform_trans('platform_currency_thousand_separator', null, ''),
|
||||
]) }}"
|
||||
data-cluster-too-large="{{ __('Too many profiles to group into clusters (:count, limit :limit). Narrow the period or raise the minimum trading partners.') }}"
|
||||
data-network-view="{{ $networkView }}"
|
||||
data-network-layout="{{ $networkLayout }}"
|
||||
data-network-labels="{{ json_encode([
|
||||
'partners' => __('Trading partners'),
|
||||
'hidden' => __('not shown'),
|
||||
'drawFailed' => __('The network could not be drawn.'),
|
||||
'loading' => __('Loading the network...'),
|
||||
'preparing' => __('Preparing :count profiles...'),
|
||||
'arranging' => __('Arranging :count profiles...'),
|
||||
'clustering' => __('Finding clusters in :count profiles, this can take a moment...'),
|
||||
'drawEmpty' => __('No network to draw for this selection.'),
|
||||
'paid' => __('Paid'),
|
||||
'received' => __('Received'),
|
||||
'total' => __('Total traded'),
|
||||
'flowRole' => __('Hours'),
|
||||
'stale' => __('Stagnant here'),
|
||||
'flow' => [
|
||||
'circulating' => __('Circulating'),
|
||||
'deadEnd' => __('Dead end'),
|
||||
'source' => __('Source'),
|
||||
'isolated' => __('No flow'),
|
||||
],
|
||||
'noMatch' => __('No profile found in this graph'),
|
||||
'noMatchHint' => __('Profiles below the minimum trading partners, or of a type that is not ticked, are not drawn in the graph.'),
|
||||
'bank' => __('Bank network'),
|
||||
'cluster' => __('Cluster'),
|
||||
'profiles' => __('profiles'),
|
||||
'more' => __('+ :count more'),
|
||||
]) }}"
|
||||
wire:key="report-network-{{ $networkMinDegree }}-{{ $networkMaxDegree }}-{{ $networkHideHub ? 1 : 0 }}-{{ $networkView }}-{{ $networkEdge ? 1 : 0 }}-{{ implode(',', $networkTypes) }}-{{ $networkLayout }}-{{ $networkPeriod }}-{{ $networkIncludeRemoved ? 1 : 0 }}-{{ $networkMarkInactive ? 1 : 0 }}-{{ $networkStoppedAfterMonths }}">
|
||||
|
||||
{{-- Info text left, spread button right, on one row. The
|
||||
paragraphs are hidden until they have something to
|
||||
say, so the button keeps its place through min-h
|
||||
and items-start rather than their height. --}}
|
||||
<div class="flex items-start justify-between gap-3 mb-2 min-h-[1.75rem]">
|
||||
<div class="flex-1">
|
||||
{{-- The question this view answers, above the runtime notes and
|
||||
sharing their left margin. --}}
|
||||
<p class="text-xs font-medium text-theme-primary mb-1">
|
||||
{{ \App\Http\Livewire\Admin\Stats::networkViewCaption($networkView) }}
|
||||
</p>
|
||||
|
||||
{{-- Filled by the circulation classifier in the browser;
|
||||
see the stats view. Reserves its line rather
|
||||
than collapsing: the same JS drives both views,
|
||||
and a line that disappears resizes the graph
|
||||
below it. --}}
|
||||
<p class="text-xs text-theme-secondary mb-1 min-h-[1rem]" data-network-stagnant
|
||||
data-drawn-template="{{ __('Showing :nodes profiles and :edges connections.') }}"
|
||||
data-template="{{ __('Of these, :count are not passing hours on (:never never have), holding :hours in total.') }}"></p>
|
||||
|
||||
{{-- Below the subtext, in the warning colour: the graph is
|
||||
drawn but one view could not be computed. --}}
|
||||
<p class="text-xs text-yellow-600 mb-1 min-h-[1rem]" data-network-warning>
|
||||
<span data-network-warning-text></span>
|
||||
</p>
|
||||
|
||||
<p class="text-xs text-theme-primary" data-network-focus hidden
|
||||
data-template="{{ __('Showing :name and its :count trading partners. Click the dot again, or the background, to show everything.') }}"></p>
|
||||
|
||||
<p class="text-xs text-theme-secondary" data-network-note hidden
|
||||
data-template="{{ __(':name is hidden: with :count trading partners it would flatten the rest of the graph.') }}"></p>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 flex items-center gap-1">
|
||||
<button type="button" data-network-shrink
|
||||
title="{{ __('Pull closer together') }}"
|
||||
aria-label="{{ __('Pull closer together') }}"
|
||||
class="inline-flex items-center justify-center rounded-md border border-theme-border bg-theme-surface p-1.5 text-theme-primary hover:bg-theme-brand hover:text-white focus:outline-none focus:ring-2 focus:ring-theme-brand">
|
||||
<x-icon name="arrows-pointing-in" class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<button type="button" data-network-spread
|
||||
title="{{ __('Spread out more') }}"
|
||||
aria-label="{{ __('Spread out more') }}"
|
||||
class="inline-flex items-center justify-center rounded-md border border-theme-border bg-theme-surface p-1.5 text-theme-primary hover:bg-theme-brand hover:text-white focus:outline-none focus:ring-2 focus:ring-theme-brand">
|
||||
<x-icon name="arrows-pointing-out" class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- wire:ignore keeps the canvas out of Livewire's morph, so
|
||||
the zoom, pan and any focused node survive a re-render
|
||||
(exporting a PDF goes through the server and would
|
||||
otherwise reset the view). The wrapper's data-*
|
||||
attributes above are still updated, so changing a
|
||||
setting rebuilds the graph as normal. --}}
|
||||
{{-- See the stats view: the placeholder lives inside
|
||||
the host, so Cytoscape removes it on mount. --}}
|
||||
<div class="relative w-full" style="height: 560px;" wire:ignore data-network-host>
|
||||
<div class="flex h-full items-center justify-center" data-network-placeholder>
|
||||
<span class="text-sm text-theme-secondary">{{ __('Drawing the network...') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$legend = $this->networkLegend($network);
|
||||
@endphp
|
||||
<div class="mt-3 p-3 bg-theme-surface rounded-md border border-theme-border">
|
||||
<div class="text-xs uppercase tracking-wide text-theme-secondary mb-2">{{ __('Legend') }}</div>
|
||||
|
||||
{{-- What the current view encodes. Volume and flow differ
|
||||
in both dot size and line meaning, which the graph
|
||||
alone does not reveal. --}}
|
||||
@if (!empty($legend['view_hint']))
|
||||
<p class="text-xs text-theme-primary mb-3">
|
||||
<span class="font-semibold">{{ $legend['view_label'] ?? '' }}:</span>
|
||||
{{ $legend['view_hint'] }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-wrap gap-x-8 gap-y-3">
|
||||
<div>
|
||||
<div class="text-xs text-theme-secondary mb-1">{{ __('Colour') }}: {{ $legend['color_meaning'] }}</div>
|
||||
|
||||
@if ($networkView === 'cluster')
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1"
|
||||
data-network-cluster-legend hidden></div>
|
||||
@endif
|
||||
|
||||
@if (!empty($legend['colors']))
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1">
|
||||
@foreach ($legend['colors'] as $item)
|
||||
<span class="inline-flex items-center gap-1.5 text-xs text-theme-primary">
|
||||
<span class="inline-block w-3 h-3 rounded-full"
|
||||
style="background-color: {{ $item['color'] }};{{ !empty($item['dashed']) ? ' border: 2px dashed #64748b;' : '' }}"></span>
|
||||
{{ $item['label'] }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (!empty($legend['styles']))
|
||||
<div>
|
||||
<div class="text-xs text-theme-secondary mb-1">{{ __('Node style') }}</div>
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1">
|
||||
@foreach ($legend['styles'] as $style)
|
||||
<span class="inline-flex items-center gap-1.5 text-xs text-theme-primary">
|
||||
<span class="inline-block w-3.5 h-3.5 rounded-full"
|
||||
style="background-color: {{ $style['color'] }};{{ $style['dashed'] ? ' border: 2px dashed #64748b;' : ' border: 1px solid rgba(0,0,0,0.25);' }}"></span>
|
||||
{{ $style['label'] }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div>
|
||||
<div class="text-xs text-theme-secondary mb-1">{{ __('Dot size') }}: {{ $legend['size_meaning'] }}</div>
|
||||
<div class="flex items-center gap-2 text-xs text-theme-secondary">
|
||||
<span class="inline-block rounded-full bg-theme-primary" style="width: 6px; height: 6px;"></span>
|
||||
<span class="inline-block rounded-full bg-theme-primary" style="width: 11px; height: 11px;"></span>
|
||||
<span class="inline-block rounded-full bg-theme-primary" style="width: 17px; height: 17px;"></span>
|
||||
<span class="ml-1">{{ __('small to large') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-xs text-theme-secondary mb-1">{{ __('Line thickness') }}: {{ $legend['width_meaning'] }}</div>
|
||||
<div class="flex items-center gap-2 text-xs text-theme-secondary">
|
||||
<span class="inline-block bg-theme-primary" style="width: 22px; height: 1px;"></span>
|
||||
<span class="inline-block bg-theme-primary" style="width: 22px; height: 3px;"></span>
|
||||
<span class="inline-block bg-theme-primary" style="width: 22px; height: 6px;"></span>
|
||||
<span class="ml-1">{{ __('few to many') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!empty($legend['directions']))
|
||||
<div>
|
||||
<div class="text-xs text-theme-secondary mb-1">{{ __('Transaction type') }}</div>
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1">
|
||||
@foreach ($legend['directions'] as $dir)
|
||||
<span class="inline-flex items-center gap-1.5 text-xs text-theme-primary">
|
||||
{{-- Dashed for bookkeeping edges, matching the graph. --}}
|
||||
@if (!empty($dir['dashed']))
|
||||
<span class="inline-block" style="width: 22px; height: 0; border-top: 3px dashed {{ $dir['color'] }};"></span>
|
||||
@else
|
||||
<span class="inline-block" style="width: 22px; height: 3px; background-color: {{ $dir['color'] }};"></span>
|
||||
@endif
|
||||
{{ $dir['label'] }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-xs text-theme-secondary">
|
||||
{{ __('Hover over a dot to see its name and its trading partners.') }}
|
||||
{{ __('Click a dot to show only that profile and the ones it trades with.') }}
|
||||
{{ __('Drag a dot to spread the graph out; its trading partners move along unless you switch that off.') }}
|
||||
{{ __('Use the search box to find a profile in this graph.') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
Reference in new issue
Block a user