Files
animexe/resources/views/admin/stats/index.blade.php
T
2026-07-14 00:01:48 +03:00

472 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@extends('admin.layouts.app')
@section('title', 'İçerik İstatistikleri')
@section('page-title', 'İçerik İstatistikleri')
@push('styles')
<style>
.st-card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 22px; }
.st-card .val { font-size: 2rem; font-weight: 800; line-height: 1.1; color: var(--text); letter-spacing:-0.5px; }
.st-card .lbl { font-size: 0.78rem; color: var(--text2); margin-top: 4px; font-weight:600; }
.st-card .sub { font-size: 0.72rem; color: var(--text3); margin-top: 3px; }
.st-card .ico { font-size: 1.8rem; opacity: 0.5; }
.chart-card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px 20px; }
.chart-title { font-size: 0.88rem; font-weight: 700; color: var(--text); margin-bottom: 14px; }
.section-head { font-size: 0.75rem; text-transform: uppercase; letter-spacing: .8px; color: var(--text2); font-weight: 700; margin-bottom: 14px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
.tbl-sm th { font-size: 0.73rem; color: var(--text2); text-transform: uppercase; letter-spacing: .5px; padding: 8px 10px; }
.tbl-sm td { font-size: 0.84rem; padding: 8px 10px; vertical-align: middle; border-top: 1px solid var(--border); color: var(--text); }
.tbl-sm tr:first-child td { border-top: none; }
.pbar-wrap { height: 6px; background: var(--surface); border-radius: 3px; flex: 1; }
.pbar-fill { height: 100%; background: linear-gradient(90deg,var(--accent),var(--accent-lt)); border-radius: 3px; }
/* GitHub-style heatmap */
.heatmap-wrap { overflow-x: auto; padding-bottom: 4px; }
.heatmap { display: flex; gap: 3px; }
.heatmap-col { display: flex; flex-direction: column; gap: 3px; }
.heat-cell {
width: 13px; height: 13px; border-radius: 2px;
background: var(--surface);
cursor: default;
transition: opacity .15s;
position: relative;
}
.heat-cell:hover::after {
content: attr(data-tip);
position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%);
background: var(--bg2); border: 1px solid var(--border2); color: var(--text);
font-size: 11px; padding: 3px 8px; border-radius: 5px; white-space: nowrap;
pointer-events: none; z-index: 100;
}
.heat-0 { background: var(--surface); }
.heat-1 { background: rgba(124,58,237,0.25); }
.heat-2 { background: rgba(124,58,237,0.45); }
.heat-3 { background: rgba(124,58,237,0.7); }
.heat-4 { background: var(--accent); }
.heatmap-legend { display: flex; align-items: center; gap: 4px; margin-top: 8px; font-size: .72rem; color: var(--text2); justify-content: flex-end; }
.heatmap-legend span { width: 13px; height: 13px; border-radius: 2px; display: inline-block; }
.month-labels { display: flex; gap: 3px; margin-bottom: 4px; font-size: .72rem; color: var(--text3); padding-left: 22px; }
.day-labels { display: flex; flex-direction: column; gap: 3px; margin-right: 4px; }
.day-labels span { height: 13px; font-size: .68rem; color: var(--text3); line-height: 13px; }
.ep-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--border); }
.ep-row:last-child { border-bottom: none; }
.ep-badge { font-size: .7rem; padding: 2px 7px; border-radius: 10px; background: var(--surface); color: var(--text2); font-weight: 600; white-space: nowrap; }
</style>
@endpush
@section('content')
{{-- Özet kartlar --}}
<div class="row g-3 mb-4">
<div class="col-6 col-sm-4 col-lg">
<div class="st-card h-100">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="val">{{ number_format($totalAnimes) }}</div>
<div class="lbl">Toplam Anime</div>
<div class="sub">{{ number_format($publishedAnimes) }} yayında</div>
</div>
<i class="bi bi-play-circle ico text-info"></i>
</div>
</div>
</div>
<div class="col-6 col-sm-4 col-lg">
<div class="st-card h-100">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="val">{{ number_format($totalEpisodes) }}</div>
<div class="lbl">Toplam Bölüm</div>
<div class="sub">{{ number_format($publishedEps) }} yayında</div>
</div>
<i class="bi bi-collection-play ico" style="color:#a371f7"></i>
</div>
</div>
</div>
<div class="col-6 col-sm-4 col-lg">
<div class="st-card h-100">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="val">{{ number_format($totalSeasons) }}</div>
<div class="lbl">Toplam Sezon</div>
</div>
<i class="bi bi-layers ico" style="color:#f0883e"></i>
</div>
</div>
</div>
<div class="col-6 col-sm-4 col-lg">
<div class="st-card h-100">
<div class="d-flex justify-content-between align-items-start">
<div>
@php $avgEpPerAnime = $totalAnimes > 0 ? round($totalEpisodes / $totalAnimes, 1) : 0; @endphp
<div class="val">{{ $avgEpPerAnime }}</div>
<div class="lbl">Ort. Bölüm/Anime</div>
</div>
<i class="bi bi-bar-chart ico" style="color:var(--success)"></i>
</div>
</div>
</div>
<div class="col-6 col-sm-4 col-lg">
<div class="st-card h-100">
<div class="d-flex justify-content-between align-items-start">
<div>
@php $unpublished = $totalEpisodes - $publishedEps; @endphp
<div class="val">{{ number_format($unpublished) }}</div>
<div class="lbl">Taslak Bölüm</div>
<div class="sub">Yayında değil</div>
</div>
<i class="bi bi-eye-slash ico" style="color:var(--text2)"></i>
</div>
</div>
</div>
</div>
{{-- Yükleme Isı Haritası --}}
<div class="chart-card mb-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<div class="chart-title mb-0">Bölüm Yükleme Yoğunluk Haritası <span style="color:#6e7681;font-size:.78rem">(son 52 hafta)</span></div>
</div>
@php
// Ay etiketleri için hesaplama
$heatMonths = [];
foreach ($heatData as $wi => $week) {
$firstDay = $week[0]['date'];
$month = \Carbon\Carbon::parse($firstDay)->format('M');
$dayOfMonth = \Carbon\Carbon::parse($firstDay)->day;
if ($dayOfMonth <= 7 && !isset($lastMonth) || (isset($lastMonth) && $lastMonth !== $month)) {
$heatMonths[$wi] = $month;
$lastMonth = $month;
} else {
$heatMonths[$wi] = '';
}
}
$heatMax = collect($heatData)->flatten(1)->max('cnt') ?: 1;
@endphp
<div class="d-flex">
{{-- Gün etiketleri --}}
<div class="day-labels mt-4">
<span>&nbsp;</span>
<span>Pzt</span>
<span>&nbsp;</span>
<span>Çar</span>
<span>&nbsp;</span>
<span>Cum</span>
<span>&nbsp;</span>
</div>
<div style="flex:1;min-width:0">
{{-- Ay etiketleri --}}
<div class="month-labels">
@foreach($heatMonths as $wi => $mon)
<span style="width:16px;text-align:left;flex-shrink:0">{{ $mon }}</span>
@endforeach
</div>
{{-- Hücreler --}}
<div class="heatmap-wrap">
<div class="heatmap">
@foreach($heatData as $week)
<div class="heatmap-col">
@foreach($week as $cell)
@php
$cnt = $cell['cnt'];
if ($cnt === 0) $level = 0;
elseif ($cnt <= 2) $level = 1;
elseif ($cnt <= 5) $level = 2;
elseif ($cnt <= 10) $level = 3;
else $level = 4;
$tip = $cell['date'] . ': ' . $cnt . ' bölüm';
@endphp
<div class="heat-cell heat-{{ $level }}" data-tip="{{ $tip }}" title="{{ $tip }}"></div>
@endforeach
</div>
@endforeach
</div>
</div>
<div class="heatmap-legend">
<span>Az</span>
<span class="heat-0"></span>
<span class="heat-1"></span>
<span class="heat-2"></span>
<span class="heat-3"></span>
<span class="heat-4"></span>
<span>Çok</span>
</div>
</div>
</div>
</div>
{{-- 90 gün trend + Aylık dağılım --}}
<div class="row g-3 mb-4">
<div class="col-lg-8">
<div class="chart-card h-100">
<div class="chart-title">Son 90 Gün Günlük Yükleme Trendi</div>
<div style="position:relative;height:200px">
<canvas id="trendChart"></canvas>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="chart-card h-100">
<div class="chart-title">Aylık Bölüm Dağılımı (24 Ay)</div>
<div style="position:relative;height:200px">
<canvas id="monthChart"></canvas>
</div>
</div>
</div>
</div>
{{-- Saatlik + Haftalık dağılım --}}
<div class="row g-3 mb-4">
<div class="col-lg-7">
<div class="chart-card h-100">
<div class="chart-title">Saatlik Yükleme Dağılımı <span style="color:#6e7681;font-size:.78rem">(tüm zamanlar)</span></div>
<div style="position:relative;height:160px">
<canvas id="hourlyChart"></canvas>
</div>
@php
$peakHour = array_search(max($hourlyEpsData), $hourlyEpsData);
$peakCount = max($hourlyEpsData);
@endphp
@if($peakCount > 0)
<div class="mt-2 text-center" style="font-size:.78rem;color:var(--text2)">
En yoğun saat: <strong style="color:var(--accent)">{{ str_pad($peakHour,2,'0',STR_PAD_LEFT) }}:00</strong>
({{ number_format($peakCount) }} bölüm)
</div>
@endif
</div>
</div>
<div class="col-lg-5">
<div class="chart-card h-100">
<div class="chart-title">Haftanın Günlerine Göre <span style="color:#6e7681;font-size:.78rem">(tüm zamanlar)</span></div>
<div style="position:relative;height:160px">
<canvas id="weekdayChart"></canvas>
</div>
@php
$peakDow = array_search(max($weekdayData), $weekdayData);
$peakDowCount = max($weekdayData);
@endphp
@if($peakDowCount > 0)
<div class="mt-2 text-center" style="font-size:.78rem;color:var(--text2)">
En yoğun gün: <strong style="color:#a371f7">{{ $weekdayLabels[$peakDow] }}</strong>
({{ number_format($peakDowCount) }} bölüm)
</div>
@endif
</div>
</div>
</div>
{{-- Top animeler + Tür dağılımı --}}
<div class="row g-3 mb-4">
<div class="col-lg-6">
<div class="chart-card h-100">
<div class="section-head">En Fazla Bölüm İçeren 10 Anime</div>
@php $maxEpCount = $topByEpisodes->max('episodes_count') ?: 1; @endphp
@foreach($topByEpisodes as $i => $a)
<div class="d-flex align-items-center gap-2 mb-2">
<span style="font-size:.72rem;color:var(--text2);width:16px;text-align:right">{{ $i+1 }}</span>
<span style="font-size:.82rem;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
<a href="{{ route('admin.animes.show', $a) }}" style="color:var(--text);text-decoration:none" class="hover-accent">{{ $a->title }}</a>
</span>
@php
$statusColor = match($a->status) { 'ongoing'=>'var(--success)','completed'=>'#79c0ff','upcoming'=>'#f0883e',default=>'var(--text2)' };
@endphp
<span style="font-size:.7rem;color:{{ $statusColor }}">
{{ match($a->status){'ongoing'=>'Devam','completed'=>'Bitti','upcoming'=>'Yakında',default=>$a->status} }}
</span>
<div class="pbar-wrap" style="width:90px"><div class="pbar-fill" style="width:{{ round($a->episodes_count/$maxEpCount*100) }}%"></div></div>
<span style="font-size:.78rem;color:var(--text2);width:35px;text-align:right">{{ $a->episodes_count }}</span>
</div>
@endforeach
</div>
</div>
<div class="col-lg-6">
<div class="chart-card h-100">
<div class="section-head">Türe Göre Bölüm Sayısı</div>
@php $maxGenreEp = $genreEpStats->max('ep_count') ?: 1; @endphp
@foreach($genreEpStats as $g)
<div class="d-flex align-items-center gap-2 mb-2">
<span style="font-size:.82rem;flex:1">{{ $g->name }}</span>
<div class="pbar-wrap"><div class="pbar-fill" style="width:{{ round($g->ep_count/$maxGenreEp*100) }}%;background:#a371f7"></div></div>
<span style="font-size:.78rem;color:var(--text2);width:38px;text-align:right">{{ number_format($g->ep_count) }}</span>
</div>
@endforeach
</div>
</div>
</div>
{{-- Son eklenen bölümler --}}
<div class="row g-3 mb-4">
<div class="col-lg-7">
<div class="chart-card h-100">
<div class="section-head">Son Eklenen Bölümler</div>
@foreach($recentEpisodes as $ep)
<div class="ep-row">
<div style="flex:1;min-width:0">
<div style="font-size:.83rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
{{ $ep->anime?->title ?? '—' }}
</div>
<div style="font-size:.72rem;color:var(--text2)">
S{{ $ep->season?->season_number ?? '?' }}E{{ $ep->episode_number }}
@if($ep->title) · {{ $ep->title }} @endif
</div>
</div>
<span class="ep-badge {{ $ep->is_published ? '' : '' }}" style="{{ $ep->is_published ? 'background:#0e4429;color:var(--success)' : 'background:#3d1a1a;color:var(--danger)' }}">
{{ $ep->is_published ? 'Yayında' : 'Taslak' }}
</span>
<span style="font-size:.72rem;color:#6e7681;white-space:nowrap">{{ $ep->created_at->diffForHumans() }}</span>
</div>
@endforeach
</div>
</div>
<div class="col-lg-5">
<div class="chart-card h-100">
<div class="section-head">Son Eklenen Animeler</div>
@foreach($recentAnimes as $a)
<div class="ep-row">
<div style="flex:1;min-width:0">
<div style="font-size:.83rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
<a href="{{ route('admin.animes.show', $a) }}" style="color:var(--text);text-decoration:none">{{ $a->title }}</a>
</div>
<div style="font-size:.72rem;color:var(--text2)">
{{ strtoupper($a->type) }}
</div>
</div>
<span style="font-size:.7rem;padding:2px 7px;border-radius:10px;{{ $a->is_published ? 'background:#0e4429;color:var(--success)' : 'background:var(--surface);color:var(--text2)' }}">
{{ $a->is_published ? 'Yayında' : 'Taslak' }}
</span>
<span style="font-size:.72rem;color:#6e7681;white-space:nowrap">{{ $a->created_at->diffForHumans() }}</span>
</div>
@endforeach
</div>
</div>
</div>
@endsection
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script>
<script>
Chart.defaults.color = 'var(--text2)';
Chart.defaults.borderColor = 'var(--border)';
// ── 90 gün trend ─────────────────────────────────────────────────────────────
new Chart(document.getElementById('trendChart'), {
type: 'line',
data: {
labels: @json($trendLabels),
datasets: [
{
label: 'Bölüm',
data: @json($epTrendData),
borderColor: 'var(--accent)',
backgroundColor: 'rgba(232,67,147,.1)',
fill: true, tension: 0.4, pointRadius: 2,
yAxisID: 'y',
},
{
label: 'Anime',
data: @json($animeTrendData),
borderColor: 'var(--success)',
backgroundColor: 'rgba(63,185,80,.08)',
fill: true, tension: 0.4, pointRadius: 2,
yAxisID: 'y',
}
]
},
options: {
responsive: true, maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: true, labels: { font: { size: 11 }, boxWidth: 10, padding: 12 } }
},
scales: {
x: { grid: { color: 'var(--border)' }, ticks: { maxTicksLimit: 10, font: { size: 10 } } },
y: { grid: { color: 'var(--border)' }, ticks: { font: { size: 10 } }, beginAtZero: true },
}
}
});
// ── Aylık bölüm ───────────────────────────────────────────────────────────────
new Chart(document.getElementById('monthChart'), {
type: 'bar',
data: {
labels: @json($monthLabels),
datasets: [{
label: 'Bölüm',
data: @json($monthData),
backgroundColor: 'rgba(163,113,247,.6)',
borderColor: '#a371f7',
borderWidth: 1,
borderRadius: 3,
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { grid: { display: false }, ticks: { font: { size: 9 }, maxTicksLimit: 8 } },
y: { grid: { color: 'var(--border)' }, ticks: { font: { size: 10 } }, beginAtZero: true },
}
}
});
// ── Saatlik dağılım ───────────────────────────────────────────────────────────
new Chart(document.getElementById('hourlyChart'), {
type: 'bar',
data: {
labels: Array.from({length:24}, (_,i) => `${String(i).padStart(2,'0')}:00`),
datasets: [{
label: 'Bölüm',
data: @json($hourlyEpsData),
backgroundColor: ctx => {
const max = Math.max(...@json($hourlyEpsData));
return ctx.parsed.y === max ? 'var(--accent)' : 'rgba(232,67,147,.35)';
},
borderColor: 'var(--accent)',
borderWidth: 1,
borderRadius: 3,
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { grid: { display: false }, ticks: { font: { size: 9 } } },
y: { grid: { color: 'var(--border)' }, ticks: { font: { size: 10 } }, beginAtZero: true },
}
}
});
// ── Haftanın günleri ──────────────────────────────────────────────────────────
new Chart(document.getElementById('weekdayChart'), {
type: 'bar',
data: {
labels: @json($weekdayLabels),
datasets: [{
label: 'Bölüm',
data: @json($weekdayData),
backgroundColor: ctx => {
const max = Math.max(...@json($weekdayData));
return ctx.parsed.y === max ? '#a371f7' : 'rgba(163,113,247,.35)';
},
borderColor: '#a371f7',
borderWidth: 1,
borderRadius: 4,
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { grid: { display: false }, ticks: { font: { size: 11 } } },
y: { grid: { color: 'var(--border)' }, ticks: { font: { size: 10 } }, beginAtZero: true },
}
}
});
</script>
@endpush