resolveFilters(); $events = $this->baseQuery($filters)->get(['event_type', 'occurred_at']); $hourly = $filters['start']->diffInHours($filters['end']) <= 48; $bucketFormat = $hourly ? 'Y-m-d H:00' : 'Y-m-d'; $labelFormat = $hourly ? 'M d H:00' : 'M d'; $periodStart = $hourly ? $filters['start']->copy()->startOfHour() : $filters['start']->copy()->startOfDay(); $period = CarbonPeriod::create($periodStart, $hourly ? '1 hour' : '1 day', $filters['end']); $grouped = $events->groupBy(fn (EventJoinTokenEvent $event) => $event->occurred_at?->format($bucketFormat)); $labels = []; $success = []; $failures = []; $rateLimited = []; $uploads = []; foreach ($period as $point) { $key = $point->format($bucketFormat); $bucket = $grouped->get($key, collect()); $labels[] = $point->translatedFormat($labelFormat); $success[] = $bucket->whereIn('event_type', self::SUCCESS_EVENTS)->count(); $failures[] = $bucket->whereIn('event_type', self::FAILURE_EVENTS)->count(); $rateLimited[] = $bucket->whereIn('event_type', self::RATE_LIMIT_EVENTS)->count(); $uploads[] = $bucket->whereIn('event_type', self::UPLOAD_EVENTS)->count(); } return [ 'datasets' => [ [ 'label' => __('admin.join_token_analytics.trend.success'), 'data' => $success, 'borderColor' => '#16a34a', 'backgroundColor' => 'rgba(22, 163, 74, 0.2)', 'tension' => 0.35, 'fill' => false, ], [ 'label' => __('admin.join_token_analytics.trend.failures'), 'data' => $failures, 'borderColor' => '#dc2626', 'backgroundColor' => 'rgba(220, 38, 38, 0.2)', 'tension' => 0.35, 'fill' => false, ], [ 'label' => __('admin.join_token_analytics.trend.rate_limited'), 'data' => $rateLimited, 'borderColor' => '#f59e0b', 'backgroundColor' => 'rgba(245, 158, 11, 0.2)', 'tension' => 0.35, 'fill' => false, ], [ 'label' => __('admin.join_token_analytics.trend.uploads'), 'data' => $uploads, 'borderColor' => '#2563eb', 'backgroundColor' => 'rgba(37, 99, 235, 0.2)', 'tension' => 0.35, 'fill' => false, ], ], 'labels' => $labels, ]; } protected function getType(): string { return 'line'; } public function getHeading(): ?string { return __('admin.join_token_analytics.trend.heading'); } 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]; } 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', ]; }