192 lines
7.0 KiB
PHP
192 lines
7.0 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Widgets;
|
|
|
|
use App\Filament\Resources\EventResource;
|
|
use App\Models\Event;
|
|
use App\Models\EventJoinToken;
|
|
use Filament\Tables;
|
|
use Filament\Widgets\Concerns\InteractsWithPageFilters;
|
|
use Filament\Widgets\TableWidget as BaseWidget;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
class JoinTokenTopTokensWidget extends BaseWidget
|
|
{
|
|
use InteractsWithPageFilters;
|
|
|
|
protected static ?int $sort = 3;
|
|
|
|
protected int|string|array $columnSpan = 'full';
|
|
|
|
protected static ?string $heading = null;
|
|
|
|
public function getHeading(): ?string
|
|
{
|
|
return __('admin.join_token_analytics.table.heading');
|
|
}
|
|
|
|
public function table(Tables\Table $table): Tables\Table
|
|
{
|
|
$filters = $this->resolveFilters();
|
|
|
|
return $table
|
|
->query(fn (): Builder => $this->buildQuery($filters))
|
|
->columns([
|
|
Tables\Columns\TextColumn::make('token_preview')
|
|
->label(__('admin.join_token_analytics.table.token'))
|
|
->copyable()
|
|
->copyMessage(__('admin.events.messages.join_link_copied')),
|
|
Tables\Columns\TextColumn::make('event_label')
|
|
->label(__('admin.join_token_analytics.table.event'))
|
|
->getStateUsing(fn (EventJoinToken $record) => $this->formatEventLabel($record->event))
|
|
->url(fn (EventJoinToken $record) => $record->event
|
|
? EventResource::getUrl('view', ['record' => $record->event])
|
|
: null)
|
|
->openUrlInNewTab(),
|
|
Tables\Columns\TextColumn::make('tenant_label')
|
|
->label(__('admin.join_token_analytics.table.tenant'))
|
|
->getStateUsing(fn (EventJoinToken $record) => $record->event?->tenant?->name ?? __('admin.common.unnamed')),
|
|
Tables\Columns\TextColumn::make('success_total')
|
|
->label(__('admin.join_token_analytics.table.success'))
|
|
->numeric(),
|
|
Tables\Columns\TextColumn::make('failure_total')
|
|
->label(__('admin.join_token_analytics.table.failures'))
|
|
->numeric(),
|
|
Tables\Columns\TextColumn::make('rate_limited_total')
|
|
->label(__('admin.join_token_analytics.table.rate_limited'))
|
|
->numeric(),
|
|
Tables\Columns\TextColumn::make('upload_total')
|
|
->label(__('admin.join_token_analytics.table.uploads'))
|
|
->numeric(),
|
|
Tables\Columns\TextColumn::make('last_seen_at')
|
|
->label(__('admin.join_token_analytics.table.last_seen'))
|
|
->since()
|
|
->placeholder('—'),
|
|
])
|
|
->paginated(false);
|
|
}
|
|
|
|
private function buildQuery(array $filters): Builder
|
|
{
|
|
$query = EventJoinToken::query()
|
|
->with(['event.tenant'])
|
|
->when($filters['event_id'], fn (Builder $builder, int $eventId) => $builder->where('event_id', $eventId))
|
|
->withCount([
|
|
'analytics as success_total' => function (Builder $builder) use ($filters) {
|
|
$this->applyAnalyticsFilters($builder, $filters)
|
|
->whereIn('event_type', self::SUCCESS_EVENTS);
|
|
},
|
|
'analytics as failure_total' => function (Builder $builder) use ($filters) {
|
|
$this->applyAnalyticsFilters($builder, $filters)
|
|
->whereIn('event_type', self::FAILURE_EVENTS);
|
|
},
|
|
'analytics as rate_limited_total' => function (Builder $builder) use ($filters) {
|
|
$this->applyAnalyticsFilters($builder, $filters)
|
|
->whereIn('event_type', self::RATE_LIMIT_EVENTS);
|
|
},
|
|
'analytics as upload_total' => function (Builder $builder) use ($filters) {
|
|
$this->applyAnalyticsFilters($builder, $filters)
|
|
->whereIn('event_type', self::UPLOAD_EVENTS);
|
|
},
|
|
])
|
|
->withMax([
|
|
'analytics as last_seen_at' => function (Builder $builder) use ($filters) {
|
|
$this->applyAnalyticsFilters($builder, $filters);
|
|
},
|
|
], 'occurred_at')
|
|
->orderByDesc('failure_total')
|
|
->orderByDesc('rate_limited_total')
|
|
->orderByDesc('success_total')
|
|
->limit(10);
|
|
|
|
return $query;
|
|
}
|
|
|
|
private function applyAnalyticsFilters(Builder $query, array $filters): Builder
|
|
{
|
|
return $query->whereBetween('occurred_at', [$filters['start'], $filters['end']]);
|
|
}
|
|
|
|
private function resolveFilters(): array
|
|
{
|
|
$eventId = $this->pageFilters['event_id'] ?? null;
|
|
$eventId = is_numeric($eventId) ? (int) $eventId : null;
|
|
$range = is_string($this->pageFilters['range'] ?? null) ? $this->pageFilters['range'] : '24h';
|
|
|
|
[$start, $end] = $this->resolveWindow($range, $eventId);
|
|
|
|
return [
|
|
'event_id' => $eventId,
|
|
'range' => $range,
|
|
'start' => $start,
|
|
'end' => $end,
|
|
];
|
|
}
|
|
|
|
private function resolveWindow(string $range, ?int $eventId): array
|
|
{
|
|
$now = now();
|
|
$start = match ($range) {
|
|
'2h' => $now->copy()->subHours(2),
|
|
'6h' => $now->copy()->subHours(6),
|
|
'12h' => $now->copy()->subHours(12),
|
|
'7d' => $now->copy()->subDays(7),
|
|
default => $now->copy()->subHours(24),
|
|
};
|
|
$end = $now;
|
|
|
|
if ($range === 'event_day' && $eventId) {
|
|
$eventDate = Event::query()->whereKey($eventId)->value('date');
|
|
|
|
if ($eventDate) {
|
|
$eventDay = Carbon::parse($eventDate);
|
|
$start = $eventDay->copy()->startOfDay();
|
|
$end = $eventDay->copy()->endOfDay();
|
|
}
|
|
}
|
|
|
|
return [$start, $end];
|
|
}
|
|
|
|
private function formatEventLabel(?Event $event): string
|
|
{
|
|
if (! $event) {
|
|
return __('admin.common.unnamed');
|
|
}
|
|
|
|
$locale = app()->getLocale();
|
|
$name = $event->name[$locale] ?? $event->name['de'] ?? $event->name['en'] ?? $event->slug ?? __('admin.common.unnamed');
|
|
$tenant = $event->tenant?->name ?? __('admin.common.unnamed');
|
|
$date = $event->date?->format('Y-m-d');
|
|
|
|
return $date ? "{$name} ({$tenant}) {$date}" : "{$name} ({$tenant})";
|
|
}
|
|
|
|
private const SUCCESS_EVENTS = [
|
|
'access_granted',
|
|
'gallery_access_granted',
|
|
];
|
|
|
|
private const RATE_LIMIT_EVENTS = [
|
|
'token_rate_limited',
|
|
'access_rate_limited',
|
|
'download_rate_limited',
|
|
];
|
|
|
|
private const FAILURE_EVENTS = [
|
|
'invalid_token',
|
|
'token_expired',
|
|
'token_revoked',
|
|
'event_not_public',
|
|
'gallery_expired',
|
|
'token_rate_limited',
|
|
'access_rate_limited',
|
|
'download_rate_limited',
|
|
];
|
|
|
|
private const UPLOAD_EVENTS = [
|
|
'upload_completed',
|
|
];
|
|
}
|