Files
2026-07-14 00:01:48 +03:00

320 lines
16 KiB
PHP
Raw Permalink 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', 'Bölümler')
@section('page-title', 'Bölümler')
@push('styles')
<style>
.ep-anime { font-size:12.5px; font-weight:700; color:var(--text); }
.ep-code { font-size:12px; font-weight:700; color:var(--accent-lt); font-family:monospace; background:var(--accent-dim); padding:2px 7px; border-radius:5px; }
.ep-title { font-size:12.5px; color:var(--text2); }
.bulk-bar { display:none; align-items:center; gap:10px; padding:10px 16px; background:var(--accent-dim); border:1px solid rgba(124,58,237,0.3); border-radius:var(--radius-sm); margin-bottom:12px; flex-wrap:wrap; }
.bulk-bar.on { display:flex; }
.intro-card { display:none; background:var(--bg2); border:1px solid var(--border2); border-radius:var(--radius); padding:20px; margin-bottom:16px; }
.intro-card.on { display:block; }
</style>
@endpush
@section('content')
{{-- ── Page Header ── --}}
<div class="page-header">
<div>
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">
Bölümler
<span style="font-size:13px;font-weight:600;color:var(--text2);margin-left:6px">{{ number_format($episodes->total()) }} toplam</span>
</h5>
</div>
<div class="page-header-actions">
<button class="btn btn-secondary btn-sm" onclick="document.getElementById('intro-card').classList.toggle('on')">
<i class="bi bi-skip-end-fill"></i>
<span class="d-none d-md-inline">Toplu İntro Ayarı</span>
</button>
<a href="{{ route('admin.episodes.create') }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg"></i> Bölüm Ekle
</a>
</div>
</div>
{{-- ── Toplu İntro Card ── --}}
<div class="intro-card" id="intro-card">
<div class="d-flex align-items-center justify-content-between mb-4">
<div style="font-size:14px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px">
<i class="bi bi-skip-end-fill" style="color:var(--info)"></i> Toplu İntro Zaman Ayarı
</div>
<button class="btn btn-sm btn-secondary" onclick="document.getElementById('intro-card').classList.remove('on')">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label">Anime</label>
<select id="bi-anime" class="form-select">
<option value=""> Anime seç </option>
@foreach($animes as $a)
<option value="{{ $a->id }}">{{ $a->title }}</option>
@endforeach
</select>
</div>
<div class="col-md-2">
<label class="form-label">Sezon <span style="color:var(--text3);font-size:11px">(0 = hepsi)</span></label>
<input type="number" id="bi-season" class="form-control" value="0" min="0">
</div>
<div class="col-md-2">
<label class="form-label">İntro Başlangıç (sn)</label>
<input type="number" id="bi-start" class="form-control" placeholder="örn: 5" min="0">
</div>
<div class="col-md-2">
<label class="form-label">İntro Bitiş (sn)</label>
<input type="number" id="bi-end" class="form-control" placeholder="örn: 95" min="0">
</div>
<div class="col-md-3 d-flex gap-2">
<button class="btn btn-info btn-sm flex-grow-1" onclick="doBulkIntro()">
<i class="bi bi-check-lg"></i> Uygula
</button>
</div>
</div>
<div id="bi-result" class="mt-3" style="font-size:13px"></div>
</div>
{{-- ── Filters ── --}}
<div class="filter-card mb-3">
<form method="GET" class="d-flex flex-wrap gap-2 align-items-end">
<div class="flex-grow-1" style="min-width:180px;max-width:280px">
<select name="anime_id" class="form-select">
<option value="">Tüm Animeler</option>
@foreach($animes as $anime)
<option value="{{ $anime->id }}" {{ request('anime_id') == $anime->id ? 'selected' : '' }}>{{ $anime->title }}</option>
@endforeach
</select>
</div>
<div class="search-input-wrap flex-grow-1" style="min-width:150px;max-width:240px">
<i class="bi bi-search"></i>
<input type="text" name="search" class="form-control" placeholder="Başlık ara..." value="{{ request('search') }}">
</div>
<select name="status" class="form-select" style="width:auto;min-width:140px">
<option value="">Tüm Durumlar</option>
<option value="published" {{ request('status')=='published' ?'selected':'' }}>Yayında</option>
<option value="pending" {{ request('status')=='pending' ?'selected':'' }}>Bekliyor</option>
<option value="processing" {{ request('status')=='processing' ?'selected':'' }}>İşleniyor</option>
<option value="failed" {{ request('status')=='failed' ?'selected':'' }}>Hatalı</option>
</select>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-funnel"></i> Filtrele</button>
<a href="{{ route('admin.episodes.index') }}" class="btn btn-secondary btn-sm"><i class="bi bi-x-lg"></i></a>
</div>
</form>
</div>
{{-- ── Bulk Bar ── --}}
<div class="bulk-bar" id="bulk-bar">
<span style="font-size:13px;font-weight:700;color:var(--accent-lt)" id="bulk-count">0 bölüm seçildi</span>
<span class="badge bg-info d-none" id="bulk-all-note" style="color:#000">Tüm sayfalardaki bölümler</span>
<div style="margin-left:auto;display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-sm btn-secondary d-none" id="btn-sel-page" onclick="selPage()"><i class="bi bi-check2-square"></i> Sayfayı Seç</button>
<button class="btn btn-sm btn-secondary d-none" id="btn-sel-all" onclick="selAllPages()"><i class="bi bi-check2-all"></i> Tümünü Seç ({{ $episodes->total() }})</button>
<button class="btn btn-sm btn-outline-danger" onclick="bulkDelete()"><i class="bi bi-trash3"></i> Seçilenleri Sil</button>
<button class="btn btn-sm btn-secondary" onclick="clearSel()"><i class="bi bi-x-lg"></i> İptal</button>
</div>
</div>
{{-- ── Table ── --}}
<div class="table-wrap">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th style="width:44px;padding-left:20px">
<input type="checkbox" class="form-check-input" id="chk-page" onchange="togglePage(this.checked)">
</th>
<th>Anime & Bölüm</th>
<th>Kaynak</th>
<th style="text-align:center">Durum</th>
<th style="text-align:center">İzlenme</th>
<th style="text-align:right;padding-right:20px">İşlem</th>
</tr>
</thead>
<tbody>
@forelse($episodes as $ep)
<tr id="ep-row-{{ $ep->id }}">
<td style="padding-left:20px">
<input type="checkbox" class="form-check-input chk-item" value="{{ $ep->id }}" onchange="onCheck()">
</td>
<td>
<div class="ep-anime mb-1">{{ $ep->anime->title ?? '—' }}</div>
<div class="d-flex align-items-center gap-2">
<span class="ep-code">S{{ str_pad($ep->season->season_number ?? 0, 2, '0', STR_PAD_LEFT) }}E{{ str_pad($ep->episode_number, 2, '0', STR_PAD_LEFT) }}</span>
@if($ep->title)
<span class="ep-title">{{ Str::limit($ep->title, 45) }}</span>
@endif
</div>
</td>
<td>
<span class="badge bg-secondary">{{ strtoupper($ep->source) }}</span>
</td>
<td style="text-align:center">
@php
$sBadge = match($ep->status) {
'published' => ['bg-success', 'Yayında', ''],
'processing' => ['bg-info', 'İşleniyor', 'color:#000'],
'failed' => ['bg-danger', 'Hatalı', ''],
default => ['bg-secondary','Bekliyor', ''],
};
@endphp
<span class="badge {{ $sBadge[0] }}" style="{{ $sBadge[2] }}">{{ $sBadge[1] }}</span>
</td>
<td style="text-align:center;font-size:13px;font-weight:600;color:var(--text2)">
{{ number_format($ep->view_count) }}
</td>
<td style="text-align:right;padding-right:20px">
<div class="d-flex gap-2 justify-content-end">
<a href="{{ route('admin.episodes.edit', $ep) }}" class="btn btn-sm btn-secondary" title="Düzenle">
<i class="bi bi-pencil"></i>
</a>
<form method="POST" action="{{ route('admin.episodes.destroy', $ep) }}" onsubmit="return confirm('Bu bölümü silmek istediğinize emin misiniz?')">
@csrf @method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger" title="Sil">
<i class="bi bi-trash3"></i>
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6">
<div class="empty-state">
<i class="bi bi-collection-play"></i>
<h6>Bölüm bulunamadı</h6>
<p>Filtrelerinizi değiştirin veya yeni bölüm ekleyin.</p>
<a href="{{ route('admin.episodes.create') }}" class="btn btn-primary btn-sm mt-2">
<i class="bi bi-plus-lg"></i> Bölüm Ekle
</a>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="mt-3">
{{ $episodes->links('admin.partials.pagination') }}
</div>
@endsection
@push('scripts')
<script>
const BULK_DESTROY_URL = '{{ route("admin.episodes.bulk-destroy") }}';
const TOTAL_EPS = {{ $episodes->total() }};
const CSRF = document.querySelector('meta[name=csrf-token]')?.content || '';
let _selAll = false;
function _selBtns(show) {
document.getElementById('btn-sel-page').classList.toggle('d-none', !show);
document.getElementById('btn-sel-all').classList.toggle('d-none', !show);
}
function selPage() {
document.querySelectorAll('.chk-item').forEach(c => c.checked = true);
document.getElementById('chk-page').checked = true;
_selAll = false;
document.getElementById('bulk-all-note').classList.add('d-none');
onCheck();
}
function selAllPages() {
selPage(); _selAll = true;
document.getElementById('bulk-all-note').classList.remove('d-none');
updateBar();
}
function clearSel() {
document.querySelectorAll('.chk-item').forEach(c => c.checked = false);
document.getElementById('chk-page').checked = false;
_selAll = false;
document.getElementById('bulk-all-note').classList.add('d-none');
updateBar();
}
function togglePage(checked) {
document.querySelectorAll('.chk-item').forEach(c => c.checked = checked);
_selAll = false;
document.getElementById('bulk-all-note').classList.add('d-none');
onCheck();
}
function onCheck() {
const all = document.querySelectorAll('.chk-item');
const chk = document.querySelectorAll('.chk-item:checked');
document.getElementById('chk-page').checked = all.length > 0 && all.length === chk.length;
_selAll = false;
document.getElementById('bulk-all-note').classList.add('d-none');
updateBar();
}
function updateBar() {
const chk = document.querySelectorAll('.chk-item:checked');
const bar = document.getElementById('bulk-bar');
const cnt = document.getElementById('bulk-count');
if (_selAll) {
cnt.textContent = `Tüm ${TOTAL_EPS} bölüm seçildi`;
bar.classList.add('on'); _selBtns(false);
} else if (chk.length > 0) {
cnt.textContent = `${chk.length} bölüm seçildi`;
bar.classList.add('on'); _selBtns(true);
} else {
bar.classList.remove('on'); _selBtns(false);
}
}
async function bulkDelete() {
const chk = [...document.querySelectorAll('.chk-item:checked')].map(c => c.value);
const label = _selAll ? `tüm ${TOTAL_EPS} bölümü` : `${chk.length} bölümü`;
if (!confirm(`${label} silmek istediğinize emin misiniz? CDN dosyaları da silinecek!`)) return;
const params = new URLSearchParams(location.search);
const body = _selAll
? { all:true, filters:{ anime_id:params.get('anime_id')||'', status:params.get('status')||'', search:params.get('search')||'' } }
: { ids:chk };
try {
const res = await fetch(BULK_DESTROY_URL, { method:'POST', headers:{'X-CSRF-TOKEN':CSRF,'Content-Type':'application/json','Accept':'application/json'}, body:JSON.stringify(body) });
const d = await res.json();
if (d.success) {
if (_selAll) { location.reload(); return; }
chk.forEach(id => document.getElementById(`ep-row-${id}`)?.remove());
clearSel();
axToast(`${chk.length} bölüm silindi.`, 'success');
} else { axToast('Hata: ' + (d.message || '?'), 'error'); }
} catch(e) { axToast('Hata: ' + e.message, 'error'); }
}
async function doBulkIntro() {
const animeId = document.getElementById('bi-anime').value;
const season = document.getElementById('bi-season').value;
const start = document.getElementById('bi-start').value;
const end = document.getElementById('bi-end').value;
const result = document.getElementById('bi-result');
if (!animeId) { result.innerHTML = '<span style="color:var(--warning)">Anime seçiniz.</span>'; return; }
if (!start || !end) { result.innerHTML = '<span style="color:var(--warning)">Başlangıç ve bitiş saniyelerini girin.</span>'; return; }
if (parseInt(end) <= parseInt(start)) { result.innerHTML = '<span style="color:var(--warning)">Bitiş saniyesi başlangıçtan büyük olmalı.</span>'; return; }
result.innerHTML = '<span style="color:var(--text2)"><span class="spinner-border spinner-border-sm me-1" style="width:12px;height:12px"></span> Uygulanıyor...</span>';
try {
const res = await fetch('{{ route("admin.episodes.bulk-intro") }}', {
method: 'POST',
headers: { 'X-CSRF-TOKEN': CSRF, 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ anime_id: animeId, season: season, intro_start: start, intro_end: end }),
});
const d = await res.json();
if (d.ok) {
result.innerHTML = `<span style="color:var(--success)"><i class="bi bi-check-circle-fill me-1"></i>${d.updated} bölüm güncellendi.</span>`;
axToast(`${d.updated} bölüm intro zamanları güncellendi.`, 'success');
} else {
result.innerHTML = `<span style="color:var(--danger)"><i class="bi bi-x-circle me-1"></i>${d.error}</span>`;
}
} catch(e) {
result.innerHTML = '<span style="color:var(--danger)">Bağlantı hatası.</span>';
}
}
</script>
@endpush