add: invite tree user page

This commit is contained in:
Roardom
2024-08-11 16:22:14 +00:00
parent e736d2973f
commit 66f7dc08f0
4 changed files with 481 additions and 0 deletions
@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author Roardom <roardom@protonmail.com>
* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0
*/
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Models\Invite;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class InviteTreeController extends Controller
{
/**
* Invite Tree.
*/
public function index(Request $request, User $user): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
abort_unless($request->user()->group->is_modo || $request->user()->is($user), 403);
$inviters = User::query()
->withTrashed()
->join(
DB::raw('
(
WITH RECURSIVE cte AS (
SELECT invites.user_id
FROM invites
WHERE invites.user_id != '.(int) User::SYSTEM_USER_ID.'
AND invites.accepted_by = '.(int) $user->id.'
UNION ALL
SELECT invites.user_id
FROM invites
JOIN cte
ON cte.user_id = invites.accepted_by
WHERE invites.user_id != '.(int) User::SYSTEM_USER_ID.'
AND invites.accepted_by IS NOT NULL
AND invites.accepted_by != '.(int) User::SYSTEM_USER_ID.'
)
SELECT cte.*
FROM cte
) AS tree
'),
fn ($join) => $join->on('users.id', '=', 'tree.user_id')
)
->with('group')
->withCount([
'warnings' => function ($query): void {
$query->whereNotNull('torrent')->where('active', '=', true);
},
])
->get();
$invites = Invite::query()
->join(
DB::raw('
(
WITH RECURSIVE cte AS (
SELECT invites.id, invites.accepted_by, 0 as depth, CAST(invites.accepted_by AS CHAR(200)) AS path
FROM invites
WHERE invites.user_id = '.(int) $user->id.'
AND invites.accepted_by IS NOT NULL
AND invites.accepted_by != '.(int) User::SYSTEM_USER_ID.'
UNION ALL
SELECT invites.id, invites.accepted_by, cte.depth + 1, CONCAT(cte.path, ", ", invites.accepted_by)
FROM invites
JOIN cte
ON cte.accepted_by = invites.user_id
WHERE invites.user_id != '.(int) User::SYSTEM_USER_ID.'
AND invites.accepted_by IS NOT NULL
AND invites.accepted_by != '.(int) User::SYSTEM_USER_ID.'
)
SELECT cte.*
FROM cte
ORDER BY path
) AS tree
'),
fn ($join) => $join->on('invites.id', '=', 'tree.id')
)
->with([
'receiver' => fn ($query) => $query
->withTrashed()
->with('group')
->withSum('history', 'seedtime')
->withSum('seedingTorrents', 'size')
->withCount([
'warnings' => function ($query): void {
$query->whereNotNull('torrent')->where('active', '=', true);
},
])
])
->orderBy('path')
->get();
return view('user.invite-tree.index', [
'user' => $user,
'invites' => $invites,
'inviters' => $inviters,
'total_uploaded' => $invites
->filter(fn ($invite) => $request->user()->isAllowed($invite->receiver, 'profile', 'show_profile_torrent_ratio'))
->sum('receiver.uploaded'),
'total_downloaded' => $invites
->filter(fn ($invite) => $request->user()->isAllowed($invite->receiver, 'profile', 'show_profile_torrent_ratio'))
->sum('receiver.downloaded'),
'average_ratio' => $invites
->filter(fn ($invite) => $request->user()->isAllowed($invite->receiver, 'profile', 'show_profile_torrent_ratio'))
->map(fn ($invite) => $invite->receiver->ratio)
->average(),
'total_seedtime' => $invites
->filter(fn ($invite) => $request->user()->isAllowed($invite->receiver, 'profile', 'show_profile_torrent_seed'))
->sum('receiver.history_sum_seedtime'),
'total_seedsize' => $invites
->filter(fn ($invite) => $request->user()->isAllowed($invite->receiver, 'profile', 'show_profile_torrent_seed'))
->sum('receiver.seeding_torrents_sum_size'),
'groups' => $invites
->pluck('receiver.group')
->sortByDesc('position')
->groupBy('id'),
]);
}
}
@@ -532,6 +532,16 @@
{{ __('user.invites') }}
</a>
</li>
<li
class="{{ Route::is('users.invite_tree.index') ? 'nav-tab--active' : 'nav-tavV2' }}"
>
<a
class="{{ Route::is('users.invite_tree.index') ? 'nav-tab--active__link' : 'nav-tab__link' }}"
href="{{ route('users.invite_tree.index', ['user' => $user]) }}"
>
{{ __('user.invite-tree') }}
</a>
</li>
@endif
</ul>
</li>
@@ -0,0 +1,332 @@
@extends('layout.default')
@section('title')
<title>
{{ $user->username }} - {{ __('user.invite-tree') }} - {{ config('other.title') }}
</title>
@endsection
@section('breadcrumbs')
<li class="breadcrumbV2">
<a href="{{ route('users.show', ['user' => $user]) }}" class="breadcrumb__link">
{{ $user->username }}
</a>
</li>
<li class="breadcrumb--active">
{{ __('user.invite-tree') }}
</li>
@endsection
@section('nav-tabs')
@include('user.buttons.user')
@endsection
@section('main')
<section class="panelV2">
<h2 class="panel__heading">Invite Tree</h2>
<div class="data-table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>{{ __('common.user') }}</th>
<th style="text-align: right">
{{ __('common.account') }} {{ __('common.upload') }}
</th>
<th style="text-align: right">
{{ __('common.account') }} {{ __('common.download') }}
</th>
<th style="text-align: right">
{{ __('common.account') }} {{ __('common.ratio') }}
</th>
<th style="text-align: right">{{ __('torrent.seedsize') }}</th>
<th style="text-align: right">{{ __('user.avg-seedtime') }}</th>
@if (auth()->user()->group->is_modo)
<th>{{ __('user.registration-date') }}</th>
<th>{{ __('user.last-action') }}</th>
<th>{{ __('common.actions') }}</th>
@endif
</tr>
</thead>
<tbody>
@forelse ($invites as $invite)
<tr>
<td style="padding-left: {{ 24 * $invite->depth + 14 }}px">
<x-user_tag :user="$invite->receiver" :anon="false">
@if ($user->warnings_count > 1)
<x-slot:appended-icons>
<i
class="{{ config('other.font-awesome') }} fa-exclamation-circle text-orange"
title="{{ __('common.active-warning') }} ({{ $user->warnings_count }})"
></i>
</x-slot>
@endif
</x-user_tag>
</td>
@if (auth()->user()->isAllowed($invite->receiver, 'profile', 'show_profile_torrent_ratio'))
<td style="text-align: right">
{{ $invite->receiver->formatted_uploaded }}
</td>
<td style="text-align: right">
{{ $invite->receiver->formatted_downloaded }}
</td>
<td style="text-align: right">
{{ $invite->receiver->formatted_ratio }}
</td>
@else
<td>{{ __('common.hidden') }}</td>
<td>{{ __('common.hidden') }}</td>
<td>{{ __('common.hidden') }}</td>
@endif
@if (auth()->user()->isAllowed($invite->receiver, 'profile', 'show_profile_torrent_seed'))
<td style="text-align: right">
{{ App\Helpers\StringHelper::formatBytes($invite->receiver->seeding_torrents_sum_size ?? 0) }}
</td>
<td style="text-align: right">
{{ \implode(' ', \array_slice(\explode(' ', App\Helpers\StringHelper::timeElapsed($invite->receiver->history_sum_seedtime ?? 0)), 0, 2)) }}
</td>
@else
<td>{{ __('common.hidden') }}</td>
<td>{{ __('common.hidden') }}</td>
@endif
@if (auth()->user()->group->is_modo)
<td>
@if ($user->created_at === null)
N/A
@else
<time
class="{{ $invite->receiver->created_at }}"
datetime="{{ $invite->receiver->created_at }}"
title="{{ $invite->receiver->created_at }}"
>
{{ $invite->receiver->created_at->diffForHumans() }}
</time>
@endif
</td>
<td>
@if ($user->last_action === null)
N/A
@else
<time
class="{{ $user->last_action }}"
datetime="{{ $user->last_action }}"
title="{{ $user->last_action }}"
>
{{ $user->last_action->diffForHumans() }}
</time>
@endif
</td>
<td>
<menu class="data-table__actions">
<li class="data-table__action">
<a
href="{{ route('users.invite_tree.index', ['user' => $invite->receiver]) }}"
class="form__button form__button--text"
style="margin-top: -4px; margin-bottom: -4px"
>
{{ __('common.view') }}
</a>
</li>
</menu>
</td>
@endif
</tr>
@empty
<tr>
<td colspan="{{ auth()->user()->group->is_modo ? 9 : 6 }}">
No Invitees
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</section>
@endsection
@section('sidebar')
<section class="panelV2">
<h2 class="panel__heading">{{ __('common.info') }}</h2>
<dl class="key-value">
<div class="key-value__group">
<dt>Height</dt>
<dd>{{ $invites->max('depth') + 1 }}</dd>
</div>
<div class="key-value__group">
<dt>Count</dt>
<dd>{{ $invites->count() }}</dd>
</div>
</dl>
</section>
<section class="panelV2">
<h2 class="panel__heading">Ancestors</h2>
<dl class="key-value">
<div class="data-table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>{{ __('common.user') }}</th>
@if (auth()->user()->group->is_modo)
<th>{{ __('common.actions') }}</th>
@endif
</tr>
</thead>
<tbody>
<tr>
<td>
<x-user_tag :user="$user" :anon="false">
@if ($user->warnings_exists)
<x-slot:appended-icons>
<i
class="{{ config('other.font-awesome') }} fa-exclamation-circle text-orange"
title="{{ __('common.active-warning') }} ({{ $user->warnings_count }})"
></i>
</x-slot>
@endif
</x-user_tag>
</td>
@if (auth()->user()->group->is_modo)
<td></td>
@endif
</tr>
@foreach ($inviters as $i => $inviter)
<tr>
<td>
<x-user_tag :user="$inviter" :anon="false">
@if ($user->warnings_exists)
<x-slot:appended-icons>
<i
class="{{ config('other.font-awesome') }} fa-exclamation-circle text-orange"
title="{{ __('common.active-warning') }} ({{ $user->warnings_count }})"
></i>
</x-slot>
@endif
</x-user_tag>
</td>
@if (auth()->user()->group->is_modo)
<td>
<menu class="data-table__actions">
<li class="data-table__action">
<a
href="{{ route('users.invite_tree.index', ['user' => $inviter]) }}"
class="form__button form__button--text"
style="margin-top: -4px; margin-bottom: -4px"
>
{{ __('common.view') }}
</a>
</li>
</menu>
</td>
@endif
</tr>
@endforeach
<tr>
<td>{{ __('user.open-registration') }}</td>
@if (auth()->user()->group->is_modo)
<td></td>
@endif
</tr>
</tbody>
</table>
</div>
</dl>
</section>
<section class="panelV2">
<h2 class="panel__heading">{{ __('stat.stats') }}</h2>
<div class="data-table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Stat</th>
<th style="text-align: right">Average</th>
<th style="text-align: right">Total</th>
</tr>
</thead>
<tbody>
<tr>
<th>{{ __('common.ratio') }}</th>
<td style="text-align: right">{{ number_format($average_ratio, 2) }}</td>
<td style="text-align: right">
{{ number_format($total_uploaded / ($total_downloaded ?: 1), 2) }}
</td>
</tr>
<tr>
<th>{{ __('torrent.uploaded') }}</th>
<td style="text-align: right">
{{ App\Helpers\StringHelper::formatBytes($total_uploaded / ($invites->count() ?: 1)) }}
</td>
<td style="text-align: right">
{{ App\Helpers\StringHelper::formatBytes($total_uploaded ?? 0) }}
</td>
</tr>
<tr>
<th>{{ __('torrent.downloaded') }}</th>
<td style="text-align: right">
{{ App\Helpers\StringHelper::formatBytes($total_downloaded / ($invites->count() ?: 1)) }}
</td>
<td style="text-align: right">
{{ App\Helpers\StringHelper::formatBytes($total_downloaded ?? 0) }}
</td>
</tr>
<tr>
<th>{{ __('torrent.seedtime') }}</th>
<td style="text-align: right">
{{ App\Helpers\StringHelper::formatBytes($total_seedsize / ($invites->count() ?: 1)) }}
</td>
<td style="text-align: right">
{{ App\Helpers\StringHelper::formatBytes($total_seedsize ?? 0) }}
</td>
</tr>
<tr>
<th>{{ __('torrent.seedsize') }}</th>
<td style="text-align: right">
{{ \implode(' ', \array_slice(\explode(' ', App\Helpers\StringHelper::timeElapsed($total_seedtime / ($invites->count() ?: 1))), 0, 2)) }}
</td>
<td style="text-align: right">
{{ \implode(' ', \array_slice(\explode(' ', App\Helpers\StringHelper::timeElapsed($total_seedtime ?? 0)), 0, 2)) }}
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="panelV2">
<h2 class="panel__heading">{{ __('common.groups') }}</h2>
<div class="data-table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>{{ __('common.group') }}</th>
<th style="text-align: right">Count</th>
<th style="text-align: right">%</th>
</tr>
</thead>
<tbody>
@foreach ($groups as $groups)
<tr>
<td>
<span class="user-tag">
<a
class="user-tag__link {{ $groups->first()->icon }}"
href="{{ route('group', ['id' => $groups->first()->id]) }}"
style="color: {{ $groups->first()->color }}"
title="{{ $groups->first()->name }}"
>
{{ $groups->first()->name }}
</a>
</span>
</td>
<td style="text-align: right">{{ $groups->count() }}</td>
<td style="text-align: right">
{{ number_format($groups->count() / $invites->count(), 2) * 100 }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</section>
@endsection
+5
View File
@@ -481,6 +481,11 @@ Route::middleware('language')->group(function (): void {
Route::get('/', [App\Http\Controllers\User\InviteController::class, 'index'])->name('index')->withTrashed();
});
// Invite Tree
Route::prefix('invite-tree')->name('invite_tree.')->group(function (): void {
Route::get('/', [App\Http\Controllers\User\InviteTreeController::class, 'index'])->name('index');
});
// Notifications
Route::prefix('notifications')->name('notifications.')->group(function (): void {
Route::get('/', [App\Http\Controllers\User\NotificationController::class, 'index'])->name('index');