mirror of
https://github.com/HDInnovations/UNIT3D-Community-Edition.git
synced 2026-04-21 09:20:08 -05:00
add: group invite log by user
This commit is contained in:
@@ -15,6 +15,7 @@ namespace App\Http\Livewire;
|
||||
|
||||
use App\Models\Invite;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
@@ -32,6 +33,10 @@ class InviteLogSearch extends Component
|
||||
|
||||
public string $custom = '';
|
||||
|
||||
public string $groupBy = 'none';
|
||||
|
||||
public int $threshold = 25;
|
||||
|
||||
public string $sortField = 'created_at';
|
||||
|
||||
public string $sortDirection = 'desc';
|
||||
@@ -39,19 +44,40 @@ class InviteLogSearch extends Component
|
||||
public int $perPage = 25;
|
||||
|
||||
protected $queryString = [
|
||||
'sender' => ['except' => ''],
|
||||
'email' => ['except' => ''],
|
||||
'code' => ['except' => ''],
|
||||
'receiver' => ['except' => ''],
|
||||
'page' => ['except' => 1],
|
||||
'perPage' => ['except' => ''],
|
||||
'sender' => ['except' => ''],
|
||||
'email' => ['except' => ''],
|
||||
'code' => ['except' => ''],
|
||||
'receiver' => ['except' => ''],
|
||||
'custom' => ['except' => ''],
|
||||
'groupBy' => ['except' => 'none'],
|
||||
'threshold' => ['except' => 25],
|
||||
'page' => ['except' => 1],
|
||||
'sortField' => ['except' => 'created_at'],
|
||||
'sortDirection' => ['except' => 'desc'],
|
||||
'perPage' => ['except' => ''],
|
||||
];
|
||||
|
||||
final public function mount(): void
|
||||
{
|
||||
$this->sortField = match ($this->groupBy) {
|
||||
'user_id' => 'created_at_max',
|
||||
default => 'created_at',
|
||||
};
|
||||
}
|
||||
|
||||
final public function updatedPage(): void
|
||||
{
|
||||
$this->emit('paginationChanged');
|
||||
}
|
||||
|
||||
final public function updatingGroupBy($value): void
|
||||
{
|
||||
$this->sortField = match ($value) {
|
||||
'user_id' => 'created_at_max',
|
||||
default => 'created_at',
|
||||
};
|
||||
}
|
||||
|
||||
final public function getInvitesProperty(): \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
{
|
||||
return Invite::withTrashed()
|
||||
@@ -61,6 +87,46 @@ class InviteLogSearch extends Component
|
||||
->when($this->code, fn ($query) => $query->where('code', 'LIKE', '%'.$this->code.'%'))
|
||||
->when($this->receiver, fn ($query) => $query->whereIn('accepted_by', User::select('id')->where('username', '=', $this->receiver)))
|
||||
->when($this->custom, fn ($query) => $query->where('custom', 'LIKE', '%'.$this->custom.'%'))
|
||||
->when(
|
||||
$this->groupBy === 'user_id',
|
||||
fn ($query) => $query->groupBy('user_id')
|
||||
->from('invites as i1')
|
||||
->select([
|
||||
'user_id',
|
||||
DB::raw('MIN(created_at) as created_at_min'),
|
||||
DB::raw('FROM_UNIXTIME(AVG(UNIX_TIMESTAMP(created_at))) as created_at_avg'),
|
||||
DB::raw('MAX(created_at) as created_at_max'),
|
||||
DB::raw('COUNT(*) as sent_count'),
|
||||
DB::raw('SUM(IF(accepted_by IS NULL, 0, 1)) as accepted_by_count'),
|
||||
DB::raw("
|
||||
(select
|
||||
count(*)
|
||||
from
|
||||
users
|
||||
where
|
||||
id in (select accepted_by from invites i2 where i2.user_id = i1.user_id)
|
||||
and group_id in (select id from `groups` where slug in ('banned', 'pruned', 'disabled'))
|
||||
) as inactive_count
|
||||
"),
|
||||
DB::raw("
|
||||
100.0 *
|
||||
(select
|
||||
count(*)
|
||||
from
|
||||
users
|
||||
where
|
||||
id in (select accepted_by from invites i2 where i2.user_id = i1.user_id)
|
||||
and group_id in (select id from `groups` where slug in ('banned', 'pruned', 'disabled'))
|
||||
)
|
||||
/ COUNT(*) as inactive_ratio
|
||||
"),
|
||||
])
|
||||
->withCasts([
|
||||
'created_at_min' => 'datetime',
|
||||
'created_at_avg' => 'datetime',
|
||||
'created_at_max' => 'datetime',
|
||||
])
|
||||
)
|
||||
->orderBy($this->sortField, $this->sortDirection)
|
||||
->paginate($this->perPage);
|
||||
}
|
||||
|
||||
@@ -1,188 +1,286 @@
|
||||
<section class="panelV2">
|
||||
<header class="panel__header">
|
||||
<h2 class="panel__heading">{{ __('staff.invites-log') }}</h2>
|
||||
<div class="panel__actions">
|
||||
<div class="panel__action">
|
||||
<div class="form__group">
|
||||
<input
|
||||
<div style="display: flex; flex-direction: column; row-gap: 1rem;">
|
||||
<section class="panelV2">
|
||||
<header class="panel__header">
|
||||
<h2 class="panel__heading">{{ __('common.search') }}</h2>
|
||||
</header>
|
||||
<div class="panel__body" style="padding: 5px;">
|
||||
<form class="form">
|
||||
<div class="form__group--short-horizontal">
|
||||
<div class="form__group">
|
||||
<input
|
||||
id="sender"
|
||||
class="form__text"
|
||||
type="text"
|
||||
wire:model="sender"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label class="form__label form__label--floating">
|
||||
{{ __('user.sender') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel__action">
|
||||
<div class="form__group">
|
||||
<input
|
||||
/>
|
||||
<label class="form__label form__label--floating" for="sender">
|
||||
{{ __('user.sender') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form__group">
|
||||
<input
|
||||
id="receiver"
|
||||
class="form__text"
|
||||
type="text"
|
||||
wire:model="receiver"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label class="form__label form__label--floating">
|
||||
{{ __('bon.receiver') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel__action">
|
||||
<div class="form__group">
|
||||
<input
|
||||
/>
|
||||
<label class="form__label form__label--floating" for="receiver">
|
||||
{{ __('bon.receiver') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form__group">
|
||||
<input
|
||||
id="email"
|
||||
class="form__text"
|
||||
type="text"
|
||||
wire:model="email"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label class="form__label form__label--floating">
|
||||
{{ __('common.email') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel__action">
|
||||
<div class="form__group">
|
||||
<input
|
||||
/>
|
||||
<label class="form__label form__label--floating" for="email">
|
||||
{{ __('common.email') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form__group">
|
||||
<input
|
||||
id="threshold"
|
||||
class="form__text"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
max="100"
|
||||
wire:model="threshold"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label class="form__label form__label--floating" for="threshold">
|
||||
Threshold
|
||||
</label>
|
||||
</div>
|
||||
<div class="form__group">
|
||||
<select id="groupBy" wire:model="groupBy" class="form__select" placeholder=" ">
|
||||
<option value="none">None</option>
|
||||
<option value="user_id">Sender</option>
|
||||
</select>
|
||||
<label class="form__label form__label--floating" for="groupBy">Group By</label>
|
||||
</div>
|
||||
<div class="form__group">
|
||||
<input
|
||||
id="code"
|
||||
class="form__text"
|
||||
type="text"
|
||||
wire:model="code"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label class="form__label form__label--floating">
|
||||
{{ __('common.code') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel__action">
|
||||
<div class="form__group">
|
||||
<input
|
||||
id="code"
|
||||
class="form__text"
|
||||
type="text"
|
||||
wire:model="custom"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label class="form__label form__label--floating">
|
||||
{{ __('common.message') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel__action">
|
||||
<div class="form__group">
|
||||
<select
|
||||
/>
|
||||
<label class="form__label form__label--floating">
|
||||
{{ __('common.code') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form__group">
|
||||
<input
|
||||
id="code"
|
||||
class="form__text"
|
||||
type="text"
|
||||
wire:model="custom"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label class="form__label form__label--floating" for="custom">
|
||||
{{ __('common.message') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form__group">
|
||||
<select
|
||||
id="quantity"
|
||||
class="form__select"
|
||||
wire:model="perPage"
|
||||
required
|
||||
>
|
||||
<option>25</option>
|
||||
<option>50</option>
|
||||
<option>100</option>
|
||||
</select>
|
||||
<label class="form__label form__label--floating">
|
||||
{{ __('common.quantity') }}
|
||||
</label>
|
||||
>
|
||||
<option>25</option>
|
||||
<option>50</option>
|
||||
<option>100</option>
|
||||
</select>
|
||||
<label class="form__label form__label--floating">
|
||||
{{ __('common.quantity') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
<div class="data-table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th wire:click="sortBy('id')" role="columnheader button">
|
||||
ID
|
||||
@include('livewire.includes._sort-icon', ['field' => 'id'])
|
||||
</th>
|
||||
<th wire:click="sortBy('user_id')" role="columnheader button">
|
||||
{{ __('user.sender') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'user_id'])
|
||||
</th>
|
||||
<th wire:click="sortBy('email')" role="columnheader button">
|
||||
{{ __('common.email') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'email'])
|
||||
</th>
|
||||
<th wire:click="sortBy('code')" role="columnheader button">
|
||||
Code
|
||||
@include('livewire.includes._sort-icon', ['field' => 'code'])
|
||||
</th>
|
||||
<th wire:click="sortBy('custom')" role="columnheader button">
|
||||
{{ __('common.message') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'custom'])
|
||||
</th>
|
||||
<th wire:click="sortBy('created_at')" role="columnheader button">
|
||||
{{ __('user.created-on') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'created_at'])
|
||||
</th>
|
||||
<th wire:click="sortBy('expires_on')" role="columnheader button">
|
||||
{{ __('user.expires-on') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'expires_on'])
|
||||
</th>
|
||||
<th wire:click="sortBy('accepted_by')" role="columnheader button">
|
||||
{{ __('user.accepted-by') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'accepted_by'])
|
||||
</th>
|
||||
<th wire:click="sortBy('accepted_at')" role="columnheader button">
|
||||
{{ __('user.accepted-at') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'accepted_at'])
|
||||
</th>
|
||||
<th wire:click="sortBy('deleted_at')" role="columnheader button">
|
||||
{{ __('user.deleted-on') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'deleted_at'])
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($invites as $invite)
|
||||
<tr>
|
||||
<td>{{ $invite->id }}</td>
|
||||
<td>
|
||||
<x-user_tag :anon="false" :user="$invite->sender" />
|
||||
</td>
|
||||
<td>{{ $invite->email }}</td>
|
||||
<td>{{ $invite->code }}</td>
|
||||
<td style="white-space: pre-wrap">{{ $invite->custom }}</td>
|
||||
<td>
|
||||
<time datetime="{{ $invite->created_at }}">
|
||||
{{ $invite->created_at }}
|
||||
</time>
|
||||
</td>
|
||||
<td>
|
||||
<time datetime="{{ $invite->expires_on }}">
|
||||
{{ $invite->expires_on }}
|
||||
</time>
|
||||
</td>
|
||||
<td>
|
||||
@if ($invite->accepted_by === null)
|
||||
N/A
|
||||
@else
|
||||
<x-user_tag :anon="false" :user="$invite->receiver" />
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<time datetime="{{ $invite->accepted_at ?? '' }}">
|
||||
{{ $invite->accepted_at ?? 'N/A' }}
|
||||
</time>
|
||||
</td>
|
||||
<td>
|
||||
<time datetime="{{ $invite->deleted_at ?? '' }}">
|
||||
{{ $invite->deleted_at ?? 'N/A' }}
|
||||
</time>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8">No invites</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ $invites->links('partials.pagination') }}
|
||||
</section>
|
||||
</section>
|
||||
<section class="panelV2">
|
||||
<h2 class="panel__heading">{{ __('staff.invites-log') }}</h2>
|
||||
<div class="data-table-wrapper">
|
||||
@switch ($groupBy)
|
||||
@case('user_id')
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th wire:click="sortBy('user_id')" role="columnheader button">
|
||||
{{ __('user.sender') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'user_id'])
|
||||
</th>
|
||||
<th wire:click="sortBy('created_at_min')" role="columnheader button">
|
||||
First Sent At
|
||||
@include('livewire.includes._sort-icon', ['field' => 'created_at_min'])
|
||||
</th>
|
||||
<th wire:click="sortBy('created_at_avg')" role="columnheader button">
|
||||
Average Sent At
|
||||
@include('livewire.includes._sort-icon', ['field' => 'created_at_avg'])
|
||||
</th>
|
||||
<th wire:click="sortBy('created_at_max')" role="columnheader button">
|
||||
Last Sent At
|
||||
@include('livewire.includes._sort-icon', ['field' => 'created_at_max'])
|
||||
</th>
|
||||
<th wire:click="sortBy('sent_count')" role="columnheader button">
|
||||
Invites Sent
|
||||
@include('livewire.includes._sort-icon', ['field' => 'sent_count'])
|
||||
</th>
|
||||
<th wire:click="sortBy('accepted_by_count')" role="columnheader button">
|
||||
Invites Accepted
|
||||
@include('livewire.includes._sort-icon', ['field' => 'accepted_by_count'])
|
||||
</th>
|
||||
<th wire:click="sortBy('inactive_count')" role="columnheader button">
|
||||
Inactive Count
|
||||
@include('livewire.includes._sort-icon', ['field' => 'banned_count'])
|
||||
</th>
|
||||
<th wire:click="sortBy('inactive_ratio')" role="columnheader button">
|
||||
Percent Inactive
|
||||
@include('livewire.includes._sort-icon', ['field' => 'inactive_ratio'])
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($invites as $invite)
|
||||
<tr>
|
||||
<td>
|
||||
<x-user_tag :anon="false" :user="$invite->sender" />
|
||||
</td>
|
||||
<td>
|
||||
<time datetime="{{ $invite->created_at_min }}">
|
||||
{{ $invite->created_at_min->format('Y-m-d') }}
|
||||
</time>
|
||||
</td>
|
||||
<td>
|
||||
<time datetime="{{ $invite->created_at_avg }}">
|
||||
{{ $invite->created_at_avg->format('Y-m-d') }}
|
||||
</time>
|
||||
</td>
|
||||
<td>
|
||||
<time datetime="{{ $invite->created_at_max }}">
|
||||
{{ $invite->created_at_max->format('Y-m-d') }}
|
||||
</time>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ route('users.invites.index', ['user' => $invite->sender]) }}">
|
||||
{{ $invite->sent_count ?? 0 }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ $invite->accepted_by_count ?? 0 }}</td>
|
||||
<td>{{ $invite->inactive_count ?? 0 }}</td>
|
||||
<td class="{{ $invite->inactive_ratio < $threshold ? 'text-green' : 'text-red' }}">{{ number_format($invite->inactive_ratio, 1) }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8">No invites</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@break
|
||||
@default
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th wire:click="sortBy('id')" role="columnheader button">
|
||||
ID
|
||||
@include('livewire.includes._sort-icon', ['field' => 'id'])
|
||||
</th>
|
||||
<th wire:click="sortBy('user_id')" role="columnheader button">
|
||||
{{ __('user.sender') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'user_id'])
|
||||
</th>
|
||||
<th wire:click="sortBy('email')" role="columnheader button">
|
||||
{{ __('common.email') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'email'])
|
||||
</th>
|
||||
<th wire:click="sortBy('code')" role="columnheader button">
|
||||
Code
|
||||
@include('livewire.includes._sort-icon', ['field' => 'code'])
|
||||
</th>
|
||||
<th wire:click="sortBy('custom')" role="columnheader button">
|
||||
{{ __('common.message') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'custom'])
|
||||
</th>
|
||||
<th wire:click="sortBy('created_at')" role="columnheader button">
|
||||
{{ __('user.created-on') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'created_at'])
|
||||
</th>
|
||||
<th wire:click="sortBy('expires_on')" role="columnheader button">
|
||||
{{ __('user.expires-on') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'expires_on'])
|
||||
</th>
|
||||
<th wire:click="sortBy('accepted_by')" role="columnheader button">
|
||||
{{ __('user.accepted-by') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'accepted_by'])
|
||||
</th>
|
||||
<th wire:click="sortBy('accepted_at')" role="columnheader button">
|
||||
{{ __('user.accepted-at') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'accepted_at'])
|
||||
</th>
|
||||
<th wire:click="sortBy('deleted_at')" role="columnheader button">
|
||||
{{ __('user.deleted-on') }}
|
||||
@include('livewire.includes._sort-icon', ['field' => 'deleted_at'])
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($invites as $invite)
|
||||
<tr>
|
||||
<td>{{ $invite->id }}</td>
|
||||
<td>
|
||||
<x-user_tag :anon="false" :user="$invite->sender" />
|
||||
</td>
|
||||
<td>{{ $invite->email }}</td>
|
||||
<td>{{ $invite->code }}</td>
|
||||
<td style="white-space: pre-wrap">{{ $invite->custom }}</td>
|
||||
<td>
|
||||
<time datetime="{{ $invite->created_at }}">
|
||||
{{ $invite->created_at }}
|
||||
</time>
|
||||
</td>
|
||||
<td>
|
||||
<time datetime="{{ $invite->expires_on }}">
|
||||
{{ $invite->expires_on }}
|
||||
</time>
|
||||
</td>
|
||||
<td>
|
||||
@if ($invite->accepted_by === null)
|
||||
N/A
|
||||
@else
|
||||
<x-user_tag :anon="false" :user="$invite->receiver" />
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<time datetime="{{ $invite->accepted_at ?? '' }}">
|
||||
{{ $invite->accepted_at ?? 'N/A' }}
|
||||
</time>
|
||||
</td>
|
||||
<td>
|
||||
<time datetime="{{ $invite->deleted_at ?? '' }}">
|
||||
{{ $invite->deleted_at ?? 'N/A' }}
|
||||
</time>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8">No invites</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@endswitch
|
||||
</div>
|
||||
{{ $invites->links('partials.pagination') }}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user