Initial commit: Animexe Laravel platform
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Kullanıcı Analitiği')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<h4 class="mb-1"><i class="bi bi-people me-2 text-info"></i>Kullanıcı Analitiği</h4>
|
||||
<p class="text-muted mb-0 small">Gerçek kullanıcılar, bot trafiği, ülke dağılımı ve aktivite takibi</p>
|
||||
</div>
|
||||
<form class="d-flex gap-2 align-items-center" method="GET">
|
||||
<input type="hidden" name="tab" value="{{ $tab }}">
|
||||
<select name="period" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
@foreach([7=>'Son 7 Gün',14=>'Son 14 Gün',30=>'Son 30 Gün',90=>'Son 90 Gün'] as $d=>$l)
|
||||
<option value="{{ $d }}" {{ $period==$d?'selected':'' }}>{{ $l }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if($country)
|
||||
<a href="{{ request()->fullUrlWithoutQuery(['country']) }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-x"></i> {{ $country }}</a>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Overview Cards --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card text-center py-3 border-info">
|
||||
<div class="h3 mb-0 text-info">{{ number_format($totalReal) }}</div>
|
||||
<small class="text-muted">Toplam Kullanıcı</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card text-center py-3 border-success">
|
||||
<div class="h3 mb-0 text-success">+{{ number_format($newReal) }}</div>
|
||||
<small class="text-muted">Yeni Üye</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card text-center py-3 border-primary">
|
||||
<div class="h3 mb-0 text-primary">{{ number_format($active30) }}</div>
|
||||
<small class="text-muted">Aktif Kullanıcı</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card text-center py-3 border-success">
|
||||
<div class="h3 mb-0 text-success">{{ number_format($realViews) }}</div>
|
||||
<small class="text-muted">Gerçek Görüntüleme</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card text-center py-3 border-danger">
|
||||
<div class="h3 mb-0 text-danger">{{ number_format($botViews) }}</div>
|
||||
<small class="text-muted">Bot Görüntüleme</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card text-center py-3">
|
||||
@php $ratio = ($realViews+$botViews) > 0 ? round($botViews/($realViews+$botViews)*100,1) : 0; @endphp
|
||||
<div class="h3 mb-0 {{ $ratio > 30 ? 'text-danger' : 'text-warning' }}">{{ $ratio }}%</div>
|
||||
<small class="text-muted">Bot Oranı</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Tabs --}}
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
@foreach(['overview'=>'Genel Bakış','activity'=>'Aktivite Günlüğü','bots'=>'Bot Analizi','country'=>'Ülke Dağılımı'] as $t=>$l)
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ $tab==$t?'active':'' }}" href="{{ request()->fullUrlWithQuery(['tab'=>$t]) }}">{{ $l }}</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
{{-- ── Overview Tab ──────────────────────────────────────────────────────────── --}}
|
||||
@if($tab === 'overview')
|
||||
<div class="row g-4">
|
||||
{{-- Daily new users chart --}}
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Günlük Yeni Üye</strong></div>
|
||||
<div class="card-body"><canvas id="dailyChart" height="80"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Device breakdown --}}
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Cihaz Dağılımı (gerçek kullanıcı)</strong></div>
|
||||
<div class="card-body">
|
||||
@foreach($deviceBreakdown as $dev)
|
||||
@php $pct = $realViews > 0 ? round($dev->cnt/$realViews*100,1) : 0; @endphp
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-capitalize">{{ $dev->device ?: 'bilinmiyor' }}</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div style="width:80px;height:6px;background:#333;border-radius:3px;overflow:hidden">
|
||||
<div style="width:{{ $pct }}%;height:100%;background:var(--bs-info)"></div>
|
||||
</div>
|
||||
<small class="text-muted">{{ number_format($dev->cnt) }} ({{ $pct }}%)</small>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Top active users --}}
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>En Aktif Kullanıcılar</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-dark"><tr><th>#</th><th>Kullanıcı</th><th>Aksiyon</th></tr></thead>
|
||||
<tbody>
|
||||
@foreach($topUsers as $i => $tu)
|
||||
@php $u = $topUserMap[$tu->user_id] ?? null; @endphp
|
||||
<tr>
|
||||
<td class="text-muted">{{ $i+1 }}</td>
|
||||
<td>
|
||||
@if($u)
|
||||
<a href="{{ route('admin.analytics.user-detail', $u) }}" class="text-decoration-none">{{ $u->name }}</a>
|
||||
<br><small class="text-muted">{{ $u->email }}</small>
|
||||
@else <span class="text-muted">#{{ $tu->user_id }}</span> @endif
|
||||
</td>
|
||||
<td><span class="badge bg-primary">{{ number_format($tu->actions) }}</span></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Action breakdown --}}
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Aksiyon Dağılımı</strong></div>
|
||||
<div class="card-body">
|
||||
@foreach($actionBreakdown as $ab)
|
||||
@php $labels = \App\Models\UserActivityLog::$actionLabels; @endphp
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span style="font-size:12px">{{ $labels[$ab->action] ?? $ab->action }}</span>
|
||||
<span class="badge bg-secondary">{{ number_format($ab->cnt) }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
@if($actionBreakdown->isEmpty()) <p class="text-muted text-center small py-3">Henüz aktivite yok</p> @endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Activity Tab ──────────────────────────────────────────────────────────── --}}
|
||||
@elseif($tab === 'activity')
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong>Aktivite Günlüğü</strong>
|
||||
<form class="d-flex gap-2" method="GET">
|
||||
<input type="hidden" name="tab" value="activity">
|
||||
<input type="hidden" name="period" value="{{ $period }}">
|
||||
<select name="action" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
<option value="">Tüm Aksiyonlar</option>
|
||||
@foreach(\App\Models\UserActivityLog::$actionLabels as $k=>$v)
|
||||
<option value="{{ $k }}" {{ request('action')==$k?'selected':'' }}>{{ $v }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="text" name="user_id" class="form-control form-control-sm" placeholder="User ID" value="{{ request('user_id') }}" style="width:100px">
|
||||
<input type="text" name="country" class="form-control form-control-sm" placeholder="Ülke" value="{{ $country }}" style="width:80px">
|
||||
<button type="submit" class="btn btn-sm btn-outline-info"><i class="bi bi-search"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr><th>Kullanıcı</th><th>Aksiyon</th><th>Konu</th><th>IP</th><th>Ülke</th><th>Cihaz</th><th>Zaman</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($actLogs as $log)
|
||||
@php $labels = \App\Models\UserActivityLog::$actionLabels; @endphp
|
||||
<tr>
|
||||
<td>
|
||||
@if($log->user)
|
||||
<a href="{{ route('admin.analytics.user-detail', $log->user) }}" class="text-decoration-none small">{{ $log->user->name }}</a>
|
||||
@else <span class="text-muted small">Misafir</span> @endif
|
||||
</td>
|
||||
<td><span class="badge bg-info text-dark" style="font-size:10px">{{ $labels[$log->action] ?? $log->action }}</span></td>
|
||||
<td><small class="text-muted">{{ $log->subject_type ? class_basename($log->subject_type).'#'.$log->subject_id : '—' }}</small></td>
|
||||
<td><code style="font-size:10px">{{ $log->ip }}</code></td>
|
||||
<td>
|
||||
@if($log->country)
|
||||
<a href="{{ request()->fullUrlWithQuery(['tab'=>'activity','country'=>$log->country]) }}" class="badge bg-secondary text-decoration-none" style="font-size:10px">{{ $log->country }}</a>
|
||||
@else — @endif
|
||||
</td>
|
||||
<td><small class="text-muted text-capitalize">{{ $log->device }}</small></td>
|
||||
<td><small class="text-muted">{{ \Carbon\Carbon::parse($log->created_at)->diffForHumans() }}</small></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="7" class="text-center text-muted py-4">Kayıt bulunamadı</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer">{{ $actLogs->links() }}</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Bots Tab ──────────────────────────────────────────────────────────────── --}}
|
||||
@elseif($tab === 'bots')
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong><i class="bi bi-robot me-1 text-danger"></i>Bot Trafik Analizi</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-dark"><tr><th>Bot / Araç</th><th>Aksiyon</th><th>İstek</th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($botStats as $b)
|
||||
<tr>
|
||||
<td><code style="font-size:11px">{{ $b->bot_name }}</code></td>
|
||||
<td>
|
||||
<span class="badge {{ $b->action==='blocked'?'bg-danger':($b->action==='allowed'?'bg-success':'bg-warning text-dark') }}" style="font-size:10px">
|
||||
{{ $b->action }}
|
||||
</span>
|
||||
</td>
|
||||
<td><span class="badge bg-secondary">{{ number_format($b->cnt) }}</span></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="3" class="text-center text-muted py-3">Bot logu bulunamadı</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong><i class="bi bi-geo-alt me-1 text-danger"></i>En Fazla İstek Atan IP'ler (Botlar)</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-dark"><tr><th>IP</th><th>İstek</th><th>Durum</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($topBotIps as $bip)
|
||||
@php $blocked = $blockedIps->firstWhere('ip', $bip->ip); @endphp
|
||||
<tr>
|
||||
<td><code style="font-size:11px">{{ $bip->ip }}</code></td>
|
||||
<td><span class="badge bg-danger">{{ number_format($bip->cnt) }}</span></td>
|
||||
<td>
|
||||
@if($blocked)
|
||||
<span class="badge bg-warning text-dark" style="font-size:10px">Engelli</span>
|
||||
@else
|
||||
<span class="badge bg-secondary" style="font-size:10px">Serbest</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if(!$blocked)
|
||||
<form method="POST" action="{{ route('admin.analytics.block-ip') }}" class="d-inline">
|
||||
@csrf <input type="hidden" name="ip" value="{{ $bip->ip }}">
|
||||
<input type="hidden" name="reason" value="Bot trafiği — otomatik">
|
||||
<button class="btn btn-xs btn-danger py-0 px-1" style="font-size:10px">Engelle</button>
|
||||
</form>
|
||||
@else
|
||||
<form method="POST" action="{{ route('admin.analytics.unblock-ip') }}" class="d-inline">
|
||||
@csrf <input type="hidden" name="ip" value="{{ $bip->ip }}">
|
||||
<button class="btn btn-xs btn-success py-0 px-1" style="font-size:10px">Serbest Bırak</button>
|
||||
</form>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="text-center text-muted py-3">Bot IP kaydı yok</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong><i class="bi bi-shield-x me-1 text-warning"></i>Engelli IP'ler ({{ $blockedIps->count() }})</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-dark"><tr><th>IP</th><th>Sebep</th><th>Otomatik</th><th>Engellenme</th><th>Bitiş</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($blockedIps as $bi)
|
||||
<tr>
|
||||
<td><code style="font-size:11px">{{ $bi->ip }}</code></td>
|
||||
<td><small class="text-muted">{{ $bi->reason }}</small></td>
|
||||
<td>{{ $bi->auto_blocked ? '<span class="badge bg-warning text-dark" style="font-size:10px">Otomatik</span>' : '<span class="badge bg-secondary" style="font-size:10px">Manuel</span>' }}</td>
|
||||
<td><small class="text-muted">{{ \Carbon\Carbon::parse($bi->blocked_at)->format('d.m.Y H:i') }}</small></td>
|
||||
<td><small class="text-muted">{{ $bi->expires_at ? \Carbon\Carbon::parse($bi->expires_at)->diffForHumans() : 'Kalıcı' }}</small></td>
|
||||
<td>
|
||||
<form method="POST" action="{{ route('admin.analytics.unblock-ip') }}" class="d-inline">
|
||||
@csrf <input type="hidden" name="ip" value="{{ $bi->ip }}">
|
||||
<button class="btn btn-xs btn-outline-success py-0 px-1" style="font-size:10px">Serbest</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="text-center text-muted py-3">Engelli IP yok</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Country Tab ───────────────────────────────────────────────────────────── --}}
|
||||
@elseif($tab === 'country')
|
||||
<div class="card">
|
||||
<div class="card-header"><strong><i class="bi bi-globe me-1"></i>Ülke Bazlı Trafik (Gerçek Kullanıcı)</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-dark"><tr><th>#</th><th>Ülke</th><th>Görüntüleme</th><th>Benzersiz Kullanıcı</th><th>Oran</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
@php $totalViews = $countries->sum('views'); @endphp
|
||||
@forelse($countries as $i => $c)
|
||||
@php $pct = $totalViews > 0 ? round($c->views/$totalViews*100,1) : 0; @endphp
|
||||
<tr>
|
||||
<td class="text-muted">{{ $i+1 }}</td>
|
||||
<td class="fw-semibold">{{ $c->country ?: 'Bilinmiyor' }}</td>
|
||||
<td>{{ number_format($c->views) }}</td>
|
||||
<td>{{ number_format($c->users) }}</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div style="width:100px;height:6px;background:#333;border-radius:3px;overflow:hidden">
|
||||
<div style="width:{{ $pct }}%;height:100%;background:var(--bs-info)"></div>
|
||||
</div>
|
||||
<small class="text-muted">{{ $pct }}%</small>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ request()->fullUrlWithQuery(['tab'=>'activity','country'=>$c->country]) }}" class="btn btn-xs btn-outline-info py-0 px-1" style="font-size:10px">Aktivite</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="text-center text-muted py-4">Ülke verisi bulunamadı</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
@if($tab === 'overview')
|
||||
const labels = @json($dailyNew->keys());
|
||||
const data = @json($dailyNew->values());
|
||||
new Chart(document.getElementById('dailyChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'Yeni Üye',
|
||||
data,
|
||||
backgroundColor: 'rgba(13,202,240,0.5)',
|
||||
borderColor: 'rgba(13,202,240,1)',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: { grid: { color: '#333' }, ticks: { color: '#aaa', maxRotation: 45 } },
|
||||
y: { grid: { color: '#333' }, ticks: { color: '#aaa' }, beginAtZero: true },
|
||||
}
|
||||
}
|
||||
});
|
||||
@endif
|
||||
</script>
|
||||
@endpush
|
||||
Reference in New Issue
Block a user