polish: halaman Laporan premium full-width (hero gradient, filter cepat, KPI 4 kartu, rekap jenis progress bar + kelas, rincian terbaru) + refactor controller (baseQuery/filters/stats)
This commit is contained in:
@@ -7,6 +7,7 @@ use App\Models\Classes;
|
|||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\Violation;
|
use App\Models\Violation;
|
||||||
use Barryvdh\DomPDF\Facade\Pdf;
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
@@ -15,36 +16,31 @@ use Illuminate\View\View;
|
|||||||
*/
|
*/
|
||||||
class ViolationReportController extends Controller
|
class ViolationReportController extends Controller
|
||||||
{
|
{
|
||||||
public function index(): View
|
public function index(Request $request): View
|
||||||
{
|
{
|
||||||
$classes = Classes::orderBy('name')->get();
|
$classes = Classes::orderBy('name')->get();
|
||||||
|
$filters = $this->filters($request);
|
||||||
|
|
||||||
$query = Violation::query()
|
$query = $this->baseQuery($filters);
|
||||||
->when(auth()->user()->isScopedWaliKelas(), fn ($q) => $q->whereHas('student', fn ($qq) => $qq->whereIn('class_id', auth()->user()->homeroomClassIds())));
|
$stats = $this->stats($query);
|
||||||
|
|
||||||
$totalKasus = (clone $query)->count();
|
$recent = (clone $query)
|
||||||
$totalPoin = (clone $query)->sum('points');
|
->with(['student.class', 'violationType'])
|
||||||
$siswaTerlibat = (clone $query)->distinct('student_id')->count('student_id');
|
->orderByDesc('violation_date')
|
||||||
|
->limit(20)
|
||||||
|
->get();
|
||||||
|
|
||||||
return view('reports.violations', compact('classes', 'totalKasus', 'totalPoin', 'siswaTerlibat'));
|
return view('reports.violations', compact('classes', 'filters', 'stats', 'recent'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pdf(Request $request)
|
public function pdf(Request $request)
|
||||||
{
|
{
|
||||||
$data = $request->validate([
|
$filters = $this->filters($request, validate: true);
|
||||||
'date_from' => ['nullable', 'date'],
|
|
||||||
'date_to' => ['nullable', 'date', 'after_or_equal:date_from'],
|
|
||||||
'class_id' => ['nullable', 'exists:classes,id'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$query = Violation::with(['student.class', 'violationType.category', 'handlings'])
|
$violations = $this->baseQuery($filters)
|
||||||
->when($data['date_from'] ?? null, fn ($q) => $q->whereDate('violation_date', '>=', $data['date_from']))
|
->with(['student.class', 'violationType.category', 'handlings'])
|
||||||
->when($data['date_to'] ?? null, fn ($q) => $q->whereDate('violation_date', '<=', $data['date_to']))
|
->orderBy('violation_date')
|
||||||
->when($data['class_id'] ?? null, fn ($q) => $q->whereHas('student', fn ($qq) => $qq->where('class_id', $data['class_id'])))
|
->get();
|
||||||
// Wali kelas murni: hanya kelas yang diwalikan
|
|
||||||
->when(auth()->user()->isScopedWaliKelas(), fn ($q) => $q->whereHas('student', fn ($qq) => $qq->whereIn('class_id', auth()->user()->homeroomClassIds())));
|
|
||||||
|
|
||||||
$violations = $query->orderBy('violation_date')->get();
|
|
||||||
|
|
||||||
$summary = [
|
$summary = [
|
||||||
'total_kasus' => $violations->count(),
|
'total_kasus' => $violations->count(),
|
||||||
@@ -80,17 +76,84 @@ class ViolationReportController extends Controller
|
|||||||
'kepala_sekolah_nip' => Setting::getValue('kepala_sekolah_nip', ''),
|
'kepala_sekolah_nip' => Setting::getValue('kepala_sekolah_nip', ''),
|
||||||
];
|
];
|
||||||
|
|
||||||
$periode = ($data['date_from'] ?? 'Awal').' s/d '.($data['date_to'] ?? 'Sekarang');
|
$periode = ($filters['date_from'] ?? 'Awal').' s/d '.($filters['date_to'] ?? 'Sekarang');
|
||||||
$filterKelas = $data['class_id'] ?? null
|
$filterKelas = $filters['class_id'] ? (Classes::find($filters['class_id'])?->name ?? '-') : 'Semua Kelas';
|
||||||
? Classes::find($data['class_id'])?->name
|
|
||||||
: 'Semua Kelas';
|
|
||||||
|
|
||||||
$pdf = Pdf::loadView('reports.violation-report-pdf', compact(
|
$pdf = Pdf::loadView('reports.violation-report-pdf', compact(
|
||||||
'violations', 'summary', 'perJenis', 'perKelas', 'school', 'periode', 'filterKelas'
|
'violations', 'summary', 'perJenis', 'perKelas', 'school', 'periode', 'filterKelas'
|
||||||
))->setPaper('a4', 'landscape');
|
))->setPaper('a4', 'landscape');
|
||||||
|
|
||||||
$filename = 'rekap-pelanggaran-'.now()->format('Ymd-His').'.pdf';
|
return $pdf->download('rekap-pelanggaran-'.now()->format('Ymd-His').'.pdf');
|
||||||
|
}
|
||||||
|
|
||||||
return $pdf->download($filename);
|
/**
|
||||||
|
* Ambil & validasi filter dari request.
|
||||||
|
*
|
||||||
|
* @return array{date_from: ?string, date_to: ?string, class_id: ?int}
|
||||||
|
*/
|
||||||
|
protected function filters(Request $request, bool $validate = false): array
|
||||||
|
{
|
||||||
|
$rules = [
|
||||||
|
'date_from' => ['nullable', 'date'],
|
||||||
|
'date_to' => ['nullable', 'date', 'after_or_equal:date_from'],
|
||||||
|
'class_id' => ['nullable', 'integer', 'exists:classes,id'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$data = $validate ? $request->validate($rules) : $request->validate($rules);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'date_from' => $data['date_from'] ?? null,
|
||||||
|
'date_to' => $data['date_to'] ?? null,
|
||||||
|
'class_id' => isset($data['class_id']) ? (int) $data['class_id'] : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query dasar pelanggaran: scoping wali kelas + filter periode & kelas.
|
||||||
|
*/
|
||||||
|
protected function baseQuery(array $filters): Builder
|
||||||
|
{
|
||||||
|
return Violation::query()
|
||||||
|
->when($filters['date_from'], fn ($q) => $q->whereDate('violation_date', '>=', $filters['date_from']))
|
||||||
|
->when($filters['date_to'], fn ($q) => $q->whereDate('violation_date', '<=', $filters['date_to']))
|
||||||
|
->when($filters['class_id'], fn ($q) => $q->whereHas('student', fn ($qq) => $qq->where('class_id', $filters['class_id'])))
|
||||||
|
->when(auth()->user()->isScopedWaliKelas(), fn ($q) => $q->whereHas('student', fn ($qq) => $qq->whereIn('class_id', auth()->user()->homeroomClassIds())));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistik ringkas + rekap preview untuk halaman.
|
||||||
|
*/
|
||||||
|
protected function stats(Builder $query): array
|
||||||
|
{
|
||||||
|
$totalKasus = (clone $query)->count();
|
||||||
|
$totalPoin = (clone $query)->sum('points');
|
||||||
|
$siswaTerlibat = (clone $query)->distinct('student_id')->count('student_id');
|
||||||
|
|
||||||
|
$rows = (clone $query)
|
||||||
|
->with(['violationType.category', 'student.class'])
|
||||||
|
->get(['id', 'student_id', 'violation_type_id', 'points', 'violation_date']);
|
||||||
|
|
||||||
|
$perJenis = $rows->groupBy(fn ($v) => $v->violationType?->name ?? 'Tanpa Jenis')
|
||||||
|
->map(fn ($group) => [
|
||||||
|
'jenis' => $group->first()->violationType?->name ?? 'Tanpa Jenis',
|
||||||
|
'kategori' => $group->first()->violationType?->category?->name ?? '-',
|
||||||
|
'jumlah' => $group->count(),
|
||||||
|
'poin' => $group->sum('points'),
|
||||||
|
])
|
||||||
|
->sortByDesc('jumlah')
|
||||||
|
->values();
|
||||||
|
|
||||||
|
$perKelas = $rows->groupBy(fn ($v) => $v->student?->class?->name ?? 'Tanpa Kelas')
|
||||||
|
->map(fn ($group) => [
|
||||||
|
'kelas' => $group->first()->student?->class?->name ?? 'Tanpa Kelas',
|
||||||
|
'jumlah' => $group->count(),
|
||||||
|
'poin' => $group->sum('points'),
|
||||||
|
])
|
||||||
|
->sortByDesc('jumlah')
|
||||||
|
->values();
|
||||||
|
|
||||||
|
$jenisTerbanyak = $perJenis->first();
|
||||||
|
|
||||||
|
return compact('totalKasus', 'totalPoin', 'siswaTerlibat', 'perJenis', 'perKelas', 'jenisTerbanyak');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,68 +3,321 @@
|
|||||||
@section('title', 'Laporan Pelanggaran')
|
@section('title', 'Laporan Pelanggaran')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
@php $selectedClass = $classes->firstWhere('id', $filters['class_id']); @endphp
|
||||||
|
<div x-data="reportPage()" class="space-y-6">
|
||||||
|
|
||||||
|
{{-- ===== Hero ===== --}}
|
||||||
|
<div class="relative overflow-hidden rounded-2xl bg-gradient-to-br from-blue-700 via-blue-800 to-indigo-900 shadow-lg">
|
||||||
|
<div class="absolute inset-0 opacity-10" style="background-image: radial-gradient(circle at 20% 30%, white 1.5px, transparent 1.5px); background-size: 22px 22px;"></div>
|
||||||
|
<div class="absolute -right-20 -top-20 w-72 h-72 rounded-full bg-white/10 blur-2xl"></div>
|
||||||
|
<div class="relative px-6 py-7 lg:px-8 flex flex-col lg:flex-row lg:items-center lg:justify-between gap-5">
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div class="hidden sm:flex items-center justify-center w-14 h-14 rounded-2xl bg-white/15 backdrop-blur border border-white/20">
|
||||||
|
<i class="fa-solid fa-file-lines text-xl text-white"></i>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{-- Header --}}
|
<h1 class="text-2xl lg:text-3xl font-bold text-white tracking-tight">Laporan Rekap Pelanggaran</h1>
|
||||||
<div class="mb-6">
|
<p class="text-sm text-blue-100 mt-1.5">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 tracking-tight">Laporan Rekap Pelanggaran</h1>
|
Rekap pelanggaran per periode untuk BK & Kepala Sekolah — cetak langsung atau unduh PDF.
|
||||||
<p class="text-sm text-gray-500 mt-1">Cetak rekap pelanggaran per periode untuk BK & Kepala Sekolah</p>
|
</p>
|
||||||
|
<div class="flex flex-wrap items-center gap-2 mt-3 text-[11px] font-medium">
|
||||||
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-white/15 text-blue-50 border border-white/15">
|
||||||
|
<i class="fa-solid fa-calendar-days text-[10px]"></i> Periode {{ $filters['date_from'] ?? 'Awal' }} — {{ $filters['date_to'] ?? 'Sekarang' }}
|
||||||
|
</span>
|
||||||
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-white/15 text-blue-50 border border-white/15">
|
||||||
|
<i class="fa-solid fa-users text-[10px]"></i> {{ $selectedClass?->name ?? 'Semua Kelas' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 shrink-0">
|
||||||
|
<button form="filter-form" formaction="{{ route('reports.violations.pdf') }}" formtarget="_blank"
|
||||||
|
class="inline-flex items-center gap-2 px-5 py-3 text-sm font-semibold text-blue-800 bg-white rounded-xl hover:bg-blue-50 transition shadow-md">
|
||||||
|
<i class="fa-solid fa-file-pdf"></i> Cetak Laporan PDF
|
||||||
|
</button>
|
||||||
|
<button form="filter-form" formaction="{{ route('violations.export') }}"
|
||||||
|
class="inline-flex items-center gap-2 px-5 py-3 text-sm font-semibold text-white bg-emerald-500/90 hover:bg-emerald-500 rounded-xl transition shadow-md border border-white/20">
|
||||||
|
<i class="fa-solid fa-file-excel"></i> Export Excel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Form Filter --}}
|
{{-- ===== Filter Bar ===== --}}
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 max-w-3xl">
|
<form method="GET" action="{{ route('reports.violations') }}" id="filter-form"
|
||||||
<form method="GET" action="{{ route('reports.violations.pdf') }}" target="_blank" class="p-6 space-y-5">
|
class="bg-white rounded-2xl shadow-sm border border-gray-200 p-5">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1.5">Dari Tanggal</label>
|
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Dari Tanggal</label>
|
||||||
<input type="date" name="date_from" value="{{ request('date_from') }}"
|
<div class="relative">
|
||||||
class="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
<i class="fa-solid fa-calendar absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400 text-sm pointer-events-none"></i>
|
||||||
|
<input type="date" name="date_from" value="{{ $filters['date_from'] }}" x-model="dateFrom"
|
||||||
|
class="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1.5">Sampai Tanggal</label>
|
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Sampai Tanggal</label>
|
||||||
<input type="date" name="date_to" value="{{ request('date_to') }}"
|
<div class="relative">
|
||||||
class="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
<i class="fa-solid fa-calendar absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400 text-sm pointer-events-none"></i>
|
||||||
|
<input type="date" name="date_to" value="{{ $filters['date_to'] }}" x-model="dateTo"
|
||||||
|
class="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:col-span-2">
|
</div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1.5">Filter Kelas (opsional)</label>
|
<div>
|
||||||
<select name="class_id"
|
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Kelas</label>
|
||||||
class="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
<div class="relative">
|
||||||
|
<i class="fa-solid fa-users absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400 text-sm pointer-events-none"></i>
|
||||||
|
<select name="class_id" x-model="classId"
|
||||||
|
class="w-full pl-9 pr-3 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition appearance-none">
|
||||||
<option value="">— Semua Kelas —</option>
|
<option value="">— Semua Kelas —</option>
|
||||||
@foreach ($classes as $class)
|
@foreach ($classes as $class)
|
||||||
<option value="{{ $class->id }}" @selected(request('class_id') == $class->id)>{{ $class->name }}</option>
|
<option value="{{ $class->id }}" @selected($filters['class_id'] == $class->id)>{{ $class->name }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
|
<i class="fa-solid fa-chevron-down absolute right-3.5 top-1/2 -translate-y-1/2 text-gray-400 text-xs pointer-events-none"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end">
|
||||||
|
<button type="submit"
|
||||||
|
class="inline-flex items-center justify-center gap-2 w-full px-4 py-2.5 text-sm font-semibold text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm">
|
||||||
|
<i class="fa-solid fa-filter text-xs"></i> Terapkan Filter
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3 pt-2">
|
{{-- Quick presets --}}
|
||||||
<button type="submit"
|
<div class="flex flex-wrap items-center gap-2 mt-4 pt-4 border-t border-gray-100">
|
||||||
class="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-semibold text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm">
|
<span class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Cepat:</span>
|
||||||
<i class="fa-solid fa-file-pdf text-xs"></i>
|
@foreach ([
|
||||||
Cetak Laporan PDF
|
'7 Hari Terakhir' => [now()->subDays(6)->format('Y-m-d'), now()->format('Y-m-d')],
|
||||||
|
'30 Hari Terakhir' => [now()->subDays(29)->format('Y-m-d'), now()->format('Y-m-d')],
|
||||||
|
'Semester Ganjil' => [now()->month >= 7 ? now()->year.'-07-01' : now()->subYear()->year.'-07-01', now()->month >= 7 ? now()->year.'-12-31' : now()->subYear()->year.'-12-31'],
|
||||||
|
'Semester Genap' => [now()->month >= 7 ? (now()->year + 1).'-01-01' : now()->year.'-01-01', now()->month >= 7 ? (now()->year + 1).'-06-30' : now()->year.'-06-30'],
|
||||||
|
'Tahun Ini' => [now()->year.'-01-01', now()->year.'-12-31'],
|
||||||
|
] as $label => [$from, $to])
|
||||||
|
<button type="button" @click="applyPreset('{{ $from }}', '{{ $to }}')"
|
||||||
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-gray-600 bg-gray-100 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
||||||
|
{{ $label }}
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
<button type="button" @click="resetFilter()"
|
||||||
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition">
|
||||||
|
<i class="fa-solid fa-rotate-left"></i> Reset
|
||||||
</button>
|
</button>
|
||||||
<a href="{{ route('violations.export', request()->only(['date_from', 'date_to'])) }}"
|
|
||||||
class="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-semibold text-emerald-600 bg-emerald-50 border border-emerald-200 rounded-xl hover:bg-emerald-100 transition shadow-sm">
|
|
||||||
<i class="fa-solid fa-file-excel text-xs"></i>
|
|
||||||
Export Data Excel
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{{-- ===== KPI ===== --}}
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||||
|
<div class="relative overflow-hidden rounded-2xl bg-gradient-to-br from-blue-600 to-blue-700 p-5 shadow-sm">
|
||||||
|
<div class="absolute -right-4 -top-4 w-24 h-24 rounded-full bg-white/10"></div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-3xl font-bold text-white">{{ number_format($stats['totalKasus']) }}</div>
|
||||||
|
<div class="text-xs text-blue-100 mt-1 uppercase tracking-wider font-medium">Total Kasus</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-11 h-11 rounded-xl bg-white/15 flex items-center justify-center">
|
||||||
|
<i class="fa-solid fa-triangle-exclamation text-white"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative overflow-hidden rounded-2xl bg-gradient-to-br from-amber-500 to-orange-600 p-5 shadow-sm">
|
||||||
|
<div class="absolute -right-4 -top-4 w-24 h-24 rounded-full bg-white/10"></div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-3xl font-bold text-white">{{ number_format($stats['totalPoin']) }}</div>
|
||||||
|
<div class="text-xs text-amber-100 mt-1 uppercase tracking-wider font-medium">Total Poin</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-11 h-11 rounded-xl bg-white/15 flex items-center justify-center">
|
||||||
|
<i class="fa-solid fa-bolt text-white"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative overflow-hidden rounded-2xl bg-gradient-to-br from-rose-500 to-rose-700 p-5 shadow-sm">
|
||||||
|
<div class="absolute -right-4 -top-4 w-24 h-24 rounded-full bg-white/10"></div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-3xl font-bold text-white">{{ number_format($stats['siswaTerlibat']) }}</div>
|
||||||
|
<div class="text-xs text-rose-100 mt-1 uppercase tracking-wider font-medium">Siswa Terlibat</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-11 h-11 rounded-xl bg-white/15 flex items-center justify-center">
|
||||||
|
<i class="fa-solid fa-users text-white"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative overflow-hidden rounded-2xl bg-gradient-to-br from-violet-500 to-purple-700 p-5 shadow-sm">
|
||||||
|
<div class="absolute -right-4 -top-4 w-24 h-24 rounded-full bg-white/10"></div>
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="text-lg font-bold text-white leading-tight truncate">{{ $stats['jenisTerbanyak']['jenis'] ?? 'Belum Ada' }}</div>
|
||||||
|
<div class="text-xs text-violet-100 mt-1 uppercase tracking-wider font-medium">Jenis Terbanyak</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-11 h-11 rounded-xl bg-white/15 flex items-center justify-center shrink-0">
|
||||||
|
<i class="fa-solid fa-ranking-star text-white"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Preview ringkas --}}
|
{{-- ===== Rekap: per Jenis & per Kelas ===== --}}
|
||||||
<div class="mt-6 grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-3xl">
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||||
<div class="bg-gradient-to-br from-blue-600 to-blue-700 text-white rounded-2xl p-5 shadow-sm">
|
{{-- Per Jenis --}}
|
||||||
<div class="text-3xl font-bold">{{ $totalKasus }}</div>
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
||||||
<div class="text-sm text-blue-100 mt-1">Total Kasus Pelanggaran</div>
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<h3 class="text-base font-bold text-gray-900 flex items-center gap-2.5">
|
||||||
|
<span class="w-8 h-8 rounded-lg bg-blue-50 text-blue-600 flex items-center justify-center"><i class="fa-solid fa-tag text-sm"></i></span>
|
||||||
|
Rekap per Jenis Pelanggaran
|
||||||
|
</h3>
|
||||||
|
<span class="text-xs font-semibold text-gray-400">{{ $stats['perJenis']->count() }} jenis</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gradient-to-br from-amber-500 to-orange-600 text-white rounded-2xl p-5 shadow-sm">
|
@forelse ($stats['perJenis']->take(8) as $item)
|
||||||
<div class="text-3xl font-bold">{{ $totalPoin }}</div>
|
@php $max = max($stats['perJenis']->max('jumlah'), 1); @endphp
|
||||||
<div class="text-sm text-amber-100 mt-1">Total Poin</div>
|
<div class="mb-4 last:mb-0">
|
||||||
|
<div class="flex items-center justify-between text-sm mb-1.5">
|
||||||
|
<span class="font-semibold text-gray-700 truncate pr-3">{{ $item['jenis'] }}</span>
|
||||||
|
<span class="text-xs text-gray-500 shrink-0">
|
||||||
|
<span class="font-bold text-blue-600">{{ $item['jumlah'] }}×</span> · {{ $item['poin'] }} poin
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gradient-to-br from-rose-500 to-rose-700 text-white rounded-2xl p-5 shadow-sm">
|
<div class="h-2 rounded-full bg-gray-100 overflow-hidden">
|
||||||
<div class="text-3xl font-bold">{{ $siswaTerlibat }}</div>
|
<div class="h-full rounded-full bg-gradient-to-r from-blue-500 to-indigo-500 transition-all"
|
||||||
<div class="text-sm text-rose-100 mt-1">Siswa Terlibat</div>
|
style="width: {{ round($item['jumlah'] / $max * 100) }}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="text-center py-10">
|
||||||
|
<div class="w-14 h-14 mx-auto rounded-2xl bg-gray-50 flex items-center justify-center mb-3">
|
||||||
|
<i class="fa-solid fa-chart-pie text-gray-300 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-400">Belum ada data pada periode ini</p>
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Per Kelas --}}
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<h3 class="text-base font-bold text-gray-900 flex items-center gap-2.5">
|
||||||
|
<span class="w-8 h-8 rounded-lg bg-emerald-50 text-emerald-600 flex items-center justify-center"><i class="fa-solid fa-building-columns text-sm"></i></span>
|
||||||
|
Rekap per Kelas
|
||||||
|
</h3>
|
||||||
|
<span class="text-xs font-semibold text-gray-400">{{ $stats['perKelas']->count() }} kelas</span>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left text-xs text-gray-400 uppercase tracking-wider border-b border-gray-100">
|
||||||
|
<th class="pb-2.5 font-semibold">Kelas</th>
|
||||||
|
<th class="pb-2.5 text-center font-semibold">Kasus</th>
|
||||||
|
<th class="pb-2.5 text-right font-semibold">Poin</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-50">
|
||||||
|
@forelse ($stats['perKelas']->take(10) as $i => $item)
|
||||||
|
<tr class="hover:bg-gray-50/60 transition">
|
||||||
|
<td class="py-2.5 font-semibold text-gray-700">
|
||||||
|
<span class="inline-flex items-center gap-2">
|
||||||
|
<span class="w-6 h-6 rounded-lg {{ ['bg-blue-50 text-blue-600','bg-emerald-50 text-emerald-600','bg-amber-50 text-amber-600','bg-rose-50 text-rose-600','bg-violet-50 text-violet-600'][$i % 5] }} flex items-center justify-center text-[10px] font-bold">
|
||||||
|
{{ strtoupper(substr($item['kelas'], 0, 2)) }}
|
||||||
|
</span>
|
||||||
|
{{ $item['kelas'] }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2.5 text-center">
|
||||||
|
<span class="inline-flex px-2 py-0.5 rounded-full text-xs font-bold bg-blue-50 text-blue-700">{{ $item['jumlah'] }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2.5 text-right font-semibold text-gray-600">{{ $item['poin'] }}</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="py-10 text-center">
|
||||||
|
<div class="w-14 h-14 mx-auto rounded-2xl bg-gray-50 flex items-center justify-center mb-3">
|
||||||
|
<i class="fa-solid fa-building-columns text-gray-300 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-400">Belum ada data pada periode ini</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- ===== Rincian Terbaru ===== --}}
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
|
<div class="px-6 py-5 flex items-center justify-between border-b border-gray-100">
|
||||||
|
<h3 class="text-base font-bold text-gray-900 flex items-center gap-2.5">
|
||||||
|
<span class="w-8 h-8 rounded-lg bg-amber-50 text-amber-600 flex items-center justify-center"><i class="fa-solid fa-list-check text-sm"></i></span>
|
||||||
|
Rincian Pelanggaran Terbaru
|
||||||
|
</h3>
|
||||||
|
<a href="{{ route('violations.index', request()->only(['date_from', 'date_to'])) }}"
|
||||||
|
class="text-xs font-semibold text-blue-600 hover:text-blue-800 inline-flex items-center gap-1.5">
|
||||||
|
Lihat Semua <i class="fa-solid fa-arrow-right text-[10px]"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-gray-50/70">
|
||||||
|
<tr class="text-left text-xs text-gray-500 uppercase tracking-wider">
|
||||||
|
<th class="px-6 py-3 font-semibold">Tanggal</th>
|
||||||
|
<th class="px-4 py-3 font-semibold">NISN</th>
|
||||||
|
<th class="px-4 py-3 font-semibold">Siswa</th>
|
||||||
|
<th class="px-4 py-3 font-semibold">Kelas</th>
|
||||||
|
<th class="px-4 py-3 font-semibold">Jenis</th>
|
||||||
|
<th class="px-4 py-3 text-center font-semibold">Poin</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-50">
|
||||||
|
@forelse ($recent as $v)
|
||||||
|
<tr class="hover:bg-gray-50/60 transition">
|
||||||
|
<td class="px-6 py-3 text-gray-500 whitespace-nowrap">{{ $v->violation_date?->format('d/m/Y') }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-500 font-mono text-xs">{{ $v->student?->nisn }}</td>
|
||||||
|
<td class="px-4 py-3 font-semibold text-gray-800">{{ $v->student?->full_name }}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span class="inline-flex px-2 py-0.5 rounded-md text-xs font-semibold bg-gray-100 text-gray-600">{{ $v->student?->class?->name }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-gray-600">{{ $v->violationType?->name }}</td>
|
||||||
|
<td class="px-4 py-3 text-center">
|
||||||
|
<span class="inline-flex px-2.5 py-1 rounded-full text-xs font-bold {{ $v->points >= 10 ? 'bg-rose-50 text-rose-600' : 'bg-amber-50 text-amber-700' }}">{{ $v->points }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="py-14 text-center">
|
||||||
|
<div class="w-16 h-16 mx-auto rounded-2xl bg-gray-50 flex items-center justify-center mb-4">
|
||||||
|
<i class="fa-solid fa-folder-open text-gray-300 text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm font-semibold text-gray-500">Belum ada pelanggaran pada periode ini</p>
|
||||||
|
<p class="text-xs text-gray-400 mt-1">Ubah periode atau filter kelas untuk melihat data</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function reportPage() {
|
||||||
|
return {
|
||||||
|
dateFrom: {{ json_encode($filters['date_from'] ?? '') }},
|
||||||
|
dateTo: {{ json_encode($filters['date_to'] ?? '') }},
|
||||||
|
classId: {{ json_encode($filters['class_id'] ?? '') }},
|
||||||
|
applyPreset(from, to) {
|
||||||
|
this.dateFrom = from;
|
||||||
|
this.dateTo = to;
|
||||||
|
this.$nextTick(() => document.getElementById('filter-form').submit());
|
||||||
|
},
|
||||||
|
resetFilter() {
|
||||||
|
this.dateFrom = '';
|
||||||
|
this.dateTo = '';
|
||||||
|
this.classId = '';
|
||||||
|
this.$nextTick(() => document.getElementById('filter-form').submit());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
Reference in New Issue
Block a user