mirror of
https://github.com/HDInnovations/UNIT3D-Community-Edition.git
synced 2026-04-28 14:31:10 -05:00
add: invite tree user page
This commit is contained in:
@@ -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
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user