add: group invite log by user

This commit is contained in:
Roardom
2023-07-30 08:09:16 +00:00
parent eeb2b1d133
commit 32e246011c
2 changed files with 331 additions and 167 deletions
+72 -6
View File
@@ -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>