feat: laporan rekap pelanggaran PDF (per periode/kelas, scoping wali kelas) + infrastruktur test MySQL (phpunit, fix migrasi 1093, UserFactory username)
This commit is contained in:
@@ -13,6 +13,7 @@ $faIcons = [
|
||||
'users' => 'fa-solid fa-users',
|
||||
'clipboard-check' => 'fa-solid fa-clipboard-check',
|
||||
'document-text' => 'fa-solid fa-file-lines',
|
||||
'file-lines' => 'fa-solid fa-file-lines',
|
||||
'tag' => 'fa-solid fa-tag',
|
||||
'list' => 'fa-solid fa-list',
|
||||
'chart-bar' => 'fa-solid fa-chart-simple',
|
||||
|
||||
@@ -167,6 +167,10 @@
|
||||
<x-nav-item href="{{ route('sp-letters.index') }}" icon="document-text" :active="request()->routeIs('sp-letters.*')">Surat Peringatan</x-nav-item>
|
||||
@endcanPermission
|
||||
|
||||
@canPermission('violations-export')
|
||||
<x-nav-item href="{{ route('reports.violations') }}" icon="file-lines" :active="request()->routeIs('reports.violations*')">Laporan</x-nav-item>
|
||||
@endcanPermission
|
||||
|
||||
@canPermission('categories-manage')
|
||||
<div class="pt-4 mt-4 border-t border-gray-200">
|
||||
<p class="px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">Master Data</p>
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Rekap Pelanggaran — {{ $school['name'] }}</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: 'DejaVu Sans', sans-serif; font-size: 9px; color: #1a1a1a; margin: 0; padding: 0; }
|
||||
.kop { display: flex; align-items: center; gap: 12px; border-bottom: 3px double #1e3a8a; padding-bottom: 10px; margin-bottom: 14px; }
|
||||
.kop img { width: 62px; height: 62px; object-fit: contain; }
|
||||
.kop .nama-sekolah { font-size: 15px; font-weight: bold; color: #1e3a8a; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.kop .alamat { font-size: 8px; color: #444; margin-top: 2px; }
|
||||
.kop .sub { font-size: 8px; color: #666; }
|
||||
.judul { text-align: center; margin: 12px 0 4px; }
|
||||
.judul h2 { font-size: 13px; margin: 0; text-transform: uppercase; letter-spacing: 1px; }
|
||||
.judul p { font-size: 9px; margin: 3px 0 0; color: #444; }
|
||||
.meta { width: 100%; border-collapse: collapse; margin: 10px 0 6px; }
|
||||
.meta td { padding: 2px 0; }
|
||||
.summary { width: 100%; border-collapse: collapse; margin: 8px 0 14px; }
|
||||
.summary td { border: 1px solid #cbd5e1; padding: 7px 8px; text-align: center; background: #f8fafc; }
|
||||
.summary .angka { font-size: 16px; font-weight: bold; color: #1e3a8a; display: block; }
|
||||
.summary .label { font-size: 8px; color: #555; text-transform: uppercase; }
|
||||
table.detail { width: 100%; border-collapse: collapse; margin: 4px 0 14px; }
|
||||
table.detail th { background: #1e3a8a; color: #fff; padding: 5px 6px; text-align: left; font-size: 8.5px; text-transform: uppercase; letter-spacing: 0.3px; }
|
||||
table.detail td { border: 1px solid #cbd5e1; padding: 4px 6px; }
|
||||
table.detail tr:nth-child(even) td { background: #f8fafc; }
|
||||
h3 { font-size: 10px; margin: 12px 0 4px; color: #1e3a8a; text-transform: uppercase; }
|
||||
.center { text-align: center; }
|
||||
.right { text-align: right; }
|
||||
.ttd { width: 100%; margin-top: 30px; }
|
||||
.ttd td { width: 50%; text-align: center; font-size: 9px; vertical-align: top; }
|
||||
.ttd .nama { margin-top: 70px; font-weight: bold; text-decoration: underline; }
|
||||
.footer { margin-top: 16px; text-align: center; font-size: 7.5px; color: #888; border-top: 1px solid #e2e8f0; padding-top: 6px; }
|
||||
.badge { display: inline-block; padding: 1px 6px; border-radius: 8px; font-size: 7.5px; font-weight: bold; }
|
||||
.badge-selesai { background: #dcfce7; color: #166534; }
|
||||
.badge-proses { background: #fef3c7; color: #92400e; }
|
||||
.badge-baru { background: #dbeafe; color: #1e40af; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{{-- Kop Sekolah --}}
|
||||
<div class="kop">
|
||||
@if (! empty($school['logo']) && file_exists(public_path('storage/'.$school['logo'])))
|
||||
<img src="{{ public_path('storage/'.$school['logo']) }}" alt="logo">
|
||||
@endif
|
||||
<div>
|
||||
<div class="nama-sekolah">{{ $school['name'] }}</div>
|
||||
<div class="alamat">{{ $school['address'] }}</div>
|
||||
<div class="sub">Telp. {{ $school['phone'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Judul --}}
|
||||
<div class="judul">
|
||||
<h2>Rekap Laporan Pelanggaran Siswa</h2>
|
||||
<p>Periode: {{ $periode }} • Kelas: {{ $filterKelas }}</p>
|
||||
</div>
|
||||
|
||||
{{-- Ringkasan --}}
|
||||
<table class="summary">
|
||||
<tr>
|
||||
<td><span class="angka">{{ $summary['total_kasus'] }}</span><span class="label">Total Kasus</span></td>
|
||||
<td><span class="angka">{{ $summary['total_poin'] }}</span><span class="label">Total Poin</span></td>
|
||||
<td><span class="angka">{{ $summary['siswa_terlibat'] }}</span><span class="label">Siswa Terlibat</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{-- Rekap per Jenis --}}
|
||||
<h3>A. Rekap per Jenis Pelanggaran</h3>
|
||||
<table class="detail">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:30px">No</th>
|
||||
<th>Jenis Pelanggaran</th>
|
||||
<th>Kategori</th>
|
||||
<th class="center" style="width:60px">Jumlah</th>
|
||||
<th class="center" style="width:70px">Total Poin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($perJenis as $i => $item)
|
||||
<tr>
|
||||
<td>{{ $i + 1 }}</td>
|
||||
<td>{{ $item['jenis'] }}</td>
|
||||
<td>{{ $item['kategori'] }}</td>
|
||||
<td class="center">{{ $item['jumlah'] }}</td>
|
||||
<td class="center">{{ $item['poin'] }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="5" class="center">Tidak ada data</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{-- Rekap per Kelas --}}
|
||||
<h3>B. Rekap per Kelas</h3>
|
||||
<table class="detail">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:30px">No</th>
|
||||
<th>Kelas</th>
|
||||
<th class="center" style="width:80px">Jumlah</th>
|
||||
<th class="center" style="width:90px">Total Poin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($perKelas as $i => $item)
|
||||
<tr>
|
||||
<td>{{ $i + 1 }}</td>
|
||||
<td>{{ $item['kelas'] }}</td>
|
||||
<td class="center">{{ $item['jumlah'] }}</td>
|
||||
<td class="center">{{ $item['poin'] }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="center">Tidak ada data</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{-- Rincian --}}
|
||||
<h3>C. Rincian Pelanggaran</h3>
|
||||
<table class="detail">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:24px">No</th>
|
||||
<th style="width:64px">Tanggal</th>
|
||||
<th style="width:70px">NISN</th>
|
||||
<th>Nama Siswa</th>
|
||||
<th style="width:56px">Kelas</th>
|
||||
<th>Jenis Pelanggaran</th>
|
||||
<th class="center" style="width:34px">Poin</th>
|
||||
<th style="width:66px">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($violations as $i => $v)
|
||||
@php
|
||||
$status = $v->handlings->where('status', 'selesai')->count() > 0 ? 'Selesai'
|
||||
: ($v->handlings->count() > 0 ? 'Proses' : 'Baru');
|
||||
$badge = $status === 'Selesai' ? 'badge-selesai' : ($status === 'Proses' ? 'badge-proses' : 'badge-baru');
|
||||
@endphp
|
||||
<tr>
|
||||
<td>{{ $i + 1 }}</td>
|
||||
<td>{{ $v->violation_date?->format('d/m/Y') }}</td>
|
||||
<td>{{ $v->student?->nisn }}</td>
|
||||
<td>{{ $v->student?->full_name }}</td>
|
||||
<td>{{ $v->student?->class?->name }}</td>
|
||||
<td>{{ $v->violationType?->name }} ({{ $v->violationType?->category?->name ?? '-' }})</td>
|
||||
<td class="center">{{ $v->points }}</td>
|
||||
<td class="center"><span class="badge {{ $badge }}">{{ $status }}</span></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="8" class="center">Tidak ada data pelanggaran pada periode ini</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{-- TTD --}}
|
||||
<table class="ttd">
|
||||
<tr>
|
||||
<td>
|
||||
<div>Petugas / Guru BK</div>
|
||||
<div class="nama"> </div>
|
||||
<div>______________________</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>Mengetahui,<br>Kepala {{ $school['name'] }}</div>
|
||||
<div class="nama">{{ $school['kepala_sekolah'] }}</div>
|
||||
<div>NIP. {{ $school['kepala_sekolah_nip'] }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="footer">
|
||||
Dicetak {{ now()->locale('id')->isoFormat('dddd, D MMMM YYYY, HH:mm') }} WIB • E-TATIB {{ $school['name'] }}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,70 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Laporan Pelanggaran')
|
||||
|
||||
@section('content')
|
||||
<div>
|
||||
{{-- Header --}}
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 tracking-tight">Laporan Rekap Pelanggaran</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">Cetak rekap pelanggaran per periode untuk BK & Kepala Sekolah</p>
|
||||
</div>
|
||||
|
||||
{{-- Form Filter --}}
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 max-w-3xl">
|
||||
<form method="GET" action="{{ route('reports.violations.pdf') }}" target="_blank" class="p-6 space-y-5">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1.5">Dari Tanggal</label>
|
||||
<input type="date" name="date_from" value="{{ request('date_from') }}"
|
||||
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>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1.5">Sampai Tanggal</label>
|
||||
<input type="date" name="date_to" value="{{ request('date_to') }}"
|
||||
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>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1.5">Filter Kelas (opsional)</label>
|
||||
<select name="class_id"
|
||||
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">
|
||||
<option value="">— Semua Kelas —</option>
|
||||
@foreach ($classes as $class)
|
||||
<option value="{{ $class->id }}" @selected(request('class_id') == $class->id)>{{ $class->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 pt-2">
|
||||
<button type="submit"
|
||||
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">
|
||||
<i class="fa-solid fa-file-pdf text-xs"></i>
|
||||
Cetak Laporan PDF
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Preview ringkas --}}
|
||||
<div class="mt-6 grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-3xl">
|
||||
<div class="bg-gradient-to-br from-blue-600 to-blue-700 text-white rounded-2xl p-5 shadow-sm">
|
||||
<div class="text-3xl font-bold">{{ $totalKasus }}</div>
|
||||
<div class="text-sm text-blue-100 mt-1">Total Kasus Pelanggaran</div>
|
||||
</div>
|
||||
<div class="bg-gradient-to-br from-amber-500 to-orange-600 text-white rounded-2xl p-5 shadow-sm">
|
||||
<div class="text-3xl font-bold">{{ $totalPoin }}</div>
|
||||
<div class="text-sm text-amber-100 mt-1">Total Poin</div>
|
||||
</div>
|
||||
<div class="bg-gradient-to-br from-rose-500 to-rose-700 text-white rounded-2xl p-5 shadow-sm">
|
||||
<div class="text-3xl font-bold">{{ $siswaTerlibat }}</div>
|
||||
<div class="text-sm text-rose-100 mt-1">Siswa Terlibat</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user