Files
fotospiel-app/app/Filament/Widgets/JoinTokenOverviewWidget.php
Codex Agent 15e19d4e8b
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Add join token analytics dashboard and align Filament views
2026-01-04 18:21:59 +01:00

148 lines
4.6 KiB
PHP

<?php
namespace App\Filament\Widgets;
use App\Models\Event;
use App\Models\EventJoinTokenEvent;
use Filament\Widgets\Concerns\InteractsWithPageFilters;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
class JoinTokenOverviewWidget extends StatsOverviewWidget
{
use InteractsWithPageFilters;
protected static ?int $sort = 1;
protected int|string|array $columnSpan = 'full';
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',
];
protected function getStats(): array
{
$filters = $this->resolveFilters();
$totals = $this->totalsByEventType($filters);
$success = $this->sumTotals($totals, self::SUCCESS_EVENTS);
$failures = $this->sumTotals($totals, self::FAILURE_EVENTS);
$rateLimited = $this->sumTotals($totals, self::RATE_LIMIT_EVENTS);
$uploads = $this->sumTotals($totals, self::UPLOAD_EVENTS);
$ratio = $success > 0 ? round(($failures / $success) * 100, 1) : null;
$ratioLabel = $ratio !== null ? "{$ratio}%" : __('admin.join_token_analytics.stats.no_data');
return [
Stat::make(__('admin.join_token_analytics.stats.success'), number_format($success))
->color('success'),
Stat::make(__('admin.join_token_analytics.stats.failures'), number_format($failures))
->color($failures > 0 ? 'danger' : 'success'),
Stat::make(__('admin.join_token_analytics.stats.rate_limited'), number_format($rateLimited))
->color($rateLimited > 0 ? 'warning' : 'success'),
Stat::make(__('admin.join_token_analytics.stats.uploads'), number_format($uploads))
->color('primary'),
Stat::make(__('admin.join_token_analytics.stats.failure_ratio'), $ratioLabel)
->color($ratio !== null && $ratio >= 50 ? 'danger' : 'warning'),
];
}
private function totalsByEventType(array $filters): array
{
return $this->baseQuery($filters)
->selectRaw('event_type, COUNT(*) as total')
->groupBy('event_type')
->get()
->mapWithKeys(fn (EventJoinTokenEvent $event) => [$event->event_type => (int) $event->total])
->all();
}
private function sumTotals(array $totals, array $types): int
{
$sum = 0;
foreach ($types as $type) {
$sum += (int) ($totals[$type] ?? 0);
}
return $sum;
}
private function baseQuery(array $filters): Builder
{
$query = EventJoinTokenEvent::query()
->whereBetween('occurred_at', [$filters['start'], $filters['end']]);
if ($filters['event_id']) {
$query->where('event_id', $filters['event_id']);
}
return $query;
}
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];
}
}