*/ public function summaryForAccount(User $account): array { $codeIds = $this->codeIdsFor($account); if ($codeIds === []) { return $this->emptySummary(); } $codes = QrCode::query()->whereIn('id', $codeIds)->get(['id', 'scans_total', 'unique_scans_total', 'last_scanned_at']); $now = now(); $events = QrScanEvent::query()->whereIn('qr_code_id', $codeIds); return [ 'total_scans' => (int) $codes->sum('scans_total'), 'unique_scans' => (int) $codes->sum('unique_scans_total'), 'scans_7d' => (clone $events)->where('scanned_at', '>=', $now->copy()->subDays(7))->count(), 'scans_30d' => (clone $events)->where('scanned_at', '>=', $now->copy()->subDays(30))->count(), 'last_scanned_at' => $codes->max('last_scanned_at'), ]; } /** * @return Collection */ public function dailyScansForAccount(User $account, int $days = 30): Collection { $codeIds = $this->codeIdsFor($account); $start = now()->subDays($days - 1)->startOfDay(); if ($codeIds === []) { return $this->emptyDailySeries($start, $days); } $rows = QrScanEvent::query() ->selectRaw('DATE(scanned_at) as scan_date, COUNT(*) as total') ->whereIn('qr_code_id', $codeIds) ->where('scanned_at', '>=', $start) ->groupBy('scan_date') ->orderBy('scan_date') ->get() ->keyBy('scan_date'); return $this->buildDailySeries($start, $days, $rows); } /** * @return array */ public function breakdownForAccount(User $account, string $column, int $limit = 5): array { $codeIds = $this->codeIdsFor($account); if ($codeIds === []) { return []; } return QrScanEvent::query() ->select($column, DB::raw('COUNT(*) as total')) ->whereIn('qr_code_id', $codeIds) ->whereNotNull($column) ->groupBy($column) ->orderByDesc('total') ->limit($limit) ->get() ->map(fn ($row) => [ 'label' => (string) $row->{$column}, 'total' => (int) $row->total, ]) ->all(); } /** * @return \Illuminate\Database\Eloquent\Collection */ public function recentScansForAccount(User $account, int $limit = 20) { $codeIds = $this->codeIdsFor($account); if ($codeIds === []) { return QrScanEvent::query()->whereRaw('0 = 1')->get(); } return QrScanEvent::query() ->whereIn('qr_code_id', $codeIds) ->with('qrCode:id,label,short_code') ->latest('scanned_at') ->limit($limit) ->get(); } /** * @return \Illuminate\Database\Eloquent\Collection */ public function topCodesForAccount(User $account, int $limit = 10) { return $account->qrCodes() ->whereIn('type', QrTypeCatalog::plusTypes()) ->orderByDesc('scans_total') ->limit($limit) ->get(['id', 'label', 'short_code', 'type', 'scans_total', 'last_scanned_at']); } /** * @return array */ public function summaryFor(QrCode $qrCode): array { $now = now(); $events = QrScanEvent::query()->where('qr_code_id', $qrCode->id); return [ 'total_scans' => (int) $qrCode->scans_total, 'unique_scans' => (int) $qrCode->unique_scans_total, 'scans_7d' => (clone $events)->where('scanned_at', '>=', $now->copy()->subDays(7))->count(), 'scans_30d' => (clone $events)->where('scanned_at', '>=', $now->copy()->subDays(30))->count(), 'last_scanned_at' => $qrCode->last_scanned_at, ]; } /** * @return Collection */ public function dailyScans(QrCode $qrCode, int $days = 30): Collection { $start = now()->subDays($days - 1)->startOfDay(); $rows = QrScanEvent::query() ->selectRaw('DATE(scanned_at) as scan_date, COUNT(*) as total') ->where('qr_code_id', $qrCode->id) ->where('scanned_at', '>=', $start) ->groupBy('scan_date') ->orderBy('scan_date') ->get() ->keyBy('scan_date'); return $this->buildDailySeries($start, $days, $rows); } /** * @return array */ public function breakdown(QrCode $qrCode, string $column, int $limit = 5): array { return QrScanEvent::query() ->select($column, DB::raw('COUNT(*) as total')) ->where('qr_code_id', $qrCode->id) ->whereNotNull($column) ->groupBy($column) ->orderByDesc('total') ->limit($limit) ->get() ->map(fn ($row) => [ 'label' => (string) $row->{$column}, 'total' => (int) $row->total, ]) ->all(); } /** * @return \Illuminate\Database\Eloquent\Collection */ public function recentScans(QrCode $qrCode, int $limit = 20) { return QrScanEvent::query() ->where('qr_code_id', $qrCode->id) ->latest('scanned_at') ->limit($limit) ->get(); } /** @return list */ private function codeIdsFor(User $account): array { return $account->qrCodes() ->whereIn('type', QrTypeCatalog::plusTypes()) ->pluck('id') ->all(); } /** @return array */ private function emptySummary(): array { return [ 'total_scans' => 0, 'unique_scans' => 0, 'scans_7d' => 0, 'scans_30d' => 0, 'last_scanned_at' => null, ]; } /** @return Collection */ private function emptyDailySeries(Carbon $start, int $days): Collection { return $this->buildDailySeries($start, $days, collect()); } /** * @param Collection $rows * @return Collection */ private function buildDailySeries(Carbon $start, int $days, Collection $rows): Collection { $series = collect(); for ($i = 0; $i < $days; $i++) { $date = $start->copy()->addDays($i)->toDateString(); $series->push((object) [ 'date' => $date, 'total' => (int) ($rows->get($date)?->total ?? 0), ]); } return $series; } }