feat(tatib): kartu pelanggaran PDF per siswa (tombol PDF di /students, dompdf A4: identitas, rekap poin, riwayat 2 tabel Terlambat&Membolos/Lainnya, ttd Ketua Tim); fix null-safe nama user di riwayat handling; kolom NISN+Kelas di SP Letters
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Student;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use App\Models\SpLetter;
|
||||
use App\Models\SpThreshold;
|
||||
@@ -112,6 +113,51 @@ class StudentReportController extends Controller
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Kartu pelanggaran siswa (PDF).
|
||||
*/
|
||||
public function pdf(Student $student)
|
||||
{
|
||||
abort_unless(auth()->user()->canViewStudent($student), 403);
|
||||
|
||||
$student->load([
|
||||
'violations' => fn ($q) => $q->with(['violationType.category', 'recorder'])
|
||||
->whereNull('deleted_at')
|
||||
->latest('violation_date'),
|
||||
'spLetters.spThreshold',
|
||||
]);
|
||||
|
||||
$totalPoints = $student->total_points;
|
||||
$violations = $student->violations;
|
||||
$spThresholds = SpThreshold::where('is_active', true)->orderBy('min_points')->get();
|
||||
|
||||
$currentSpLevel = null;
|
||||
$nextSpThreshold = null;
|
||||
foreach ($spThresholds as $threshold) {
|
||||
if ($totalPoints >= $threshold->min_points) {
|
||||
$currentSpLevel = $threshold;
|
||||
} elseif (! $nextSpThreshold) {
|
||||
$nextSpThreshold = $threshold;
|
||||
}
|
||||
}
|
||||
|
||||
$school = [
|
||||
'name' => \App\Models\Setting::getValue('school_name', 'SMKN 1 WONOREJO'),
|
||||
'address' => \App\Models\Setting::getValue('school_address', ''),
|
||||
];
|
||||
|
||||
$ketuaTim = \App\Models\User::where('role', 'ketua_tim')
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
$pdf = Pdf::loadView('students.pdf-card', compact(
|
||||
'student', 'totalPoints', 'violations',
|
||||
'currentSpLevel', 'nextSpThreshold', 'school', 'ketuaTim'
|
||||
))->setPaper('a4');
|
||||
|
||||
return $pdf->stream('kartu-pelanggaran-'.str_replace(' ', '-', $student->full_name).'.pdf');
|
||||
}
|
||||
|
||||
public function update(Request $request, Student $student): RedirectResponse
|
||||
{
|
||||
abort_unless(auth()->user()->canViewStudent($student), 403);
|
||||
|
||||
@@ -26,7 +26,12 @@
|
||||
@forelse($letters as $sp)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 text-sm font-mono text-gray-700">{{ $sp->letter_number }}</td>
|
||||
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{ $sp->student->full_name ?? '-' }}</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $sp->student->full_name ?? '-' }}</div>
|
||||
<div class="mt-0.5 text-xs text-gray-500">
|
||||
NISN: {{ $sp->student->nisn ?? '-' }} · {{ $sp->student->class_name ?? '-' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="inline-flex px-2 py-1 text-xs font-medium rounded-full" style="color: {{ $sp->spThreshold->color ?? '#ef4444' }}; background-color: {{ $sp->spThreshold->color ?? '#ef4444' }}20;">
|
||||
{{ $sp->spThreshold->name ?? '-' }}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<div class="sub-title">{{ $title }}</div>
|
||||
@if($items->isEmpty())
|
||||
<div class="kosong">Tidak ada catatan {{ strtolower($title) }}.</div>
|
||||
@else
|
||||
<table class="riwayat">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:5%;">No</th>
|
||||
<th style="width:15%;">Tanggal</th>
|
||||
<th style="width:48%;">Jenis Pelanggaran</th>
|
||||
<th style="width:8%;">Poin</th>
|
||||
<th style="width:24%;">Sanksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($items as $i => $v)
|
||||
<tr>
|
||||
<td style="text-align:center;">{{ $i + 1 }}</td>
|
||||
<td>{{ \Carbon\Carbon::parse($v->violation_date)->locale('id')->translatedFormat('l, d/m/Y') }}</td>
|
||||
<td>
|
||||
<b>{{ $v->violationType?->name ?? '—' }}</b>
|
||||
@if($v->description)
|
||||
<div style="color:#475569;font-size:9px;">{{ $v->description }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td style="text-align:center;font-weight:bold;color:#dc2626;">+{{ $v->points }}</td>
|
||||
<td>{{ $v->sanction ?? '—' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
@@ -173,11 +173,18 @@
|
||||
</td>
|
||||
{{-- Aksi --}}
|
||||
<td class="px-5 py-4 text-right whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<a href="{{ route('students.pdf', $s->id) }}" target="_blank"
|
||||
class="inline-flex items-center gap-1.5 px-3.5 py-1.5 text-xs font-medium text-red-600 bg-red-50 border border-red-200 rounded-lg hover:bg-red-100 transition">
|
||||
<i class="fa-solid fa-file-pdf"></i>
|
||||
PDF
|
||||
</a>
|
||||
<a href="{{ route('students.show', $s->id) }}"
|
||||
class="inline-flex items-center gap-1.5 px-3.5 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
Detail
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Kartu Pelanggaran {{ $student->full_name }}</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'DejaVu Sans', sans-serif;
|
||||
font-size: 11px;
|
||||
color: #1e293b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.kop {
|
||||
text-align: center;
|
||||
border-bottom: 3px solid #0f766e;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.kop .nama-sekolah {
|
||||
font-size: 17px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
color: #0f172a;
|
||||
}
|
||||
.kop .alamat {
|
||||
font-size: 9px;
|
||||
color: #475569;
|
||||
}
|
||||
.judul {
|
||||
text-align: center;
|
||||
background: #0f766e;
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
padding: 7px 0;
|
||||
border-radius: 5px;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #0f766e;
|
||||
border-left: 4px solid #0f766e;
|
||||
padding-left: 7px;
|
||||
margin: 14px 0 7px 0;
|
||||
}
|
||||
.sub-title {
|
||||
font-size: 9.5px;
|
||||
font-weight: bold;
|
||||
color: #0f766e;
|
||||
margin: 8px 0 4px 0;
|
||||
}
|
||||
table.identitas {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.identitas td {
|
||||
padding: 4px 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
table.identitas td.label {
|
||||
width: 28%;
|
||||
color: #64748b;
|
||||
font-weight: bold;
|
||||
padding-left: 0;
|
||||
}
|
||||
table.identitas td.value {
|
||||
font-weight: bold;
|
||||
color: #0f172a;
|
||||
width: 22%;
|
||||
}
|
||||
.kartu-poin {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.kartu-poin td {
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
width: 33.33%;
|
||||
}
|
||||
.kartu-poin .angka {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #0f766e;
|
||||
}
|
||||
.kartu-poin .kecil {
|
||||
font-size: 8.5px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #64748b;
|
||||
}
|
||||
table.riwayat {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 6px;
|
||||
}
|
||||
table.riwayat th {
|
||||
background: #0f766e;
|
||||
color: #fff;
|
||||
font-size: 8.5px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 4px 4px;
|
||||
border: 1px solid #0f766e;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
table.riwayat td {
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 3px 5px;
|
||||
font-size: 9px;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.riwayat tr:nth-child(even) td {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.badge-sp {
|
||||
display: inline-block;
|
||||
padding: 2px 9px;
|
||||
border-radius: 8px;
|
||||
font-size: 9.5px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: #dc2626;
|
||||
}
|
||||
.ttd {
|
||||
margin-top: 14px;
|
||||
font-size: 11px;
|
||||
color: #1e293b;
|
||||
text-align: left;
|
||||
padding-left: 118mm;
|
||||
}
|
||||
.ttd-tanggal {
|
||||
font-weight: bold;
|
||||
}
|
||||
.ttd-jabatan {
|
||||
font-weight: bold;
|
||||
}
|
||||
.ttd-nama {
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.ttd-nip {
|
||||
font-size: 10px;
|
||||
color: #475569;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.footer-note {
|
||||
margin-top: 16px;
|
||||
font-size: 8.5px;
|
||||
color: #94a3b8;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
padding-top: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
.kosong {
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
font-style: italic;
|
||||
padding: 14px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="padding: 8mm 14mm 15mm 14mm;">
|
||||
|
||||
<div class="kop">
|
||||
<div class="nama-sekolah">{{ strtoupper($school['name']) }}</div>
|
||||
<div class="alamat">{{ $school['address'] }}</div>
|
||||
</div>
|
||||
|
||||
<div class="judul">KARTU PELANGGARAN SISWA</div>
|
||||
|
||||
<div class="section-title">Identitas Siswa</div>
|
||||
<table class="identitas">
|
||||
<tr>
|
||||
<td class="label">Nama Lengkap</td>
|
||||
<td class="value">: {{ strtoupper($student->full_name) }}</td>
|
||||
<td class="label">NIS</td>
|
||||
<td class="value">: {{ $student->student_number ?? '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">NISN</td>
|
||||
<td class="value">: {{ $student->nisn ?? '-' }}</td>
|
||||
<td class="label">Kelas</td>
|
||||
<td class="value">: {{ $student->class_name ?? '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Jurusan</td>
|
||||
<td class="value">: {{ $student->department_name ?? $student->department_code ?? '-' }}</td>
|
||||
<td class="label">Tahun Akademik</td>
|
||||
<td class="value">: {{ $student->academic_year_name ?? '-' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Rekap Poin Pelanggaran</div>
|
||||
<table class="kartu-poin">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="angka">{{ $totalPoints }}</div>
|
||||
<div class="kecil">Total Poin</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="angka">{{ $violations->count() }}x</div>
|
||||
<div class="kecil">Jumlah Pelanggaran</div>
|
||||
</td>
|
||||
<td>
|
||||
@if($currentSpLevel)
|
||||
<span class="badge-sp">{{ $currentSpLevel->name }}</span>
|
||||
<div class="kecil" style="margin-top:2px;">Level SP</div>
|
||||
@else
|
||||
<div class="angka" style="color:#16a34a;">Aman</div>
|
||||
<div class="kecil">Belum Ada SP</div>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Riwayat Pelanggaran</div>
|
||||
@if($violations->isEmpty())
|
||||
<div class="kosong">Siswa tidak memiliki catatan pelanggaran.</div>
|
||||
@else
|
||||
@php
|
||||
$tlMembolos = $violations->filter(fn ($v) => str_contains(strtolower($v->violationType?->name ?? ''), 'terlambat') || str_contains(strtolower($v->violationType?->name ?? ''), 'membolos'))->values();
|
||||
$lainnya = $violations->filter(fn ($v) => ! str_contains(strtolower($v->violationType?->name ?? ''), 'terlambat') && ! str_contains(strtolower($v->violationType?->name ?? ''), 'membolos'))->values();
|
||||
@endphp
|
||||
@if($tlMembolos->isNotEmpty())
|
||||
@include('students._riwayat', ['title' => 'Terlambat & Membolos', 'items' => $tlMembolos])
|
||||
@endif
|
||||
@if($lainnya->isNotEmpty())
|
||||
@include('students._riwayat', ['title' => 'Pelanggaran Lainnya', 'items' => $lainnya])
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@if($nextSpThreshold)
|
||||
<div style="margin-top:10px;font-size:9px;color:#475569;">
|
||||
* Poin menuju SP berikutnya ({{ $nextSpThreshold->name }}) : {{ $nextSpThreshold->min_points - $totalPoints }} poin lagi.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="ttd">
|
||||
<div class="ttd-tanggal">Wonorejo, {{ \Carbon\Carbon::now()->locale('id')->translatedFormat('d F Y') }}</div>
|
||||
<div>Mengetahui,</div>
|
||||
<div class="ttd-jabatan">Ketua Tatib SMKN 1 Wonorejo</div>
|
||||
<br><br>
|
||||
<div class="ttd-nama">{{ strtoupper($ketuaTim?->name ?? ' ') }}</div>
|
||||
<div class="ttd-nip">{{ $ketuaTim?->username ?? '' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-note">
|
||||
Dokumen ini dibuat otomatis oleh Sistem Tata Tertib (TATIB) SMKN 1 Wonorejo · {{ now()->format('d/m/Y H:i') }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -370,7 +370,7 @@
|
||||
<span class="w-3.5 h-3.5 rounded-full bg-gray-100 flex items-center justify-center">
|
||||
<span class="text-[6px] font-bold text-gray-500">{{ strtoupper(substr($p->user->name ?? '?', 0, 1)) }}</span>
|
||||
</span>
|
||||
{{ $p->user->name }}
|
||||
{{ $p->user?->name ?? '—' }}
|
||||
@if($p->role)
|
||||
<span class="text-[9px] text-gray-400 bg-gray-50 px-1 rounded border border-gray-100">{{ $p->role }}</span>
|
||||
@endif
|
||||
|
||||
@@ -78,6 +78,7 @@ Route::middleware('auth')->group(function () {
|
||||
Route::middleware('permission:view-students')->group(function () {
|
||||
Route::get('/students', [StudentReportController::class, 'index'])->name('students.index');
|
||||
Route::get('/students/{student}', [StudentReportController::class, 'show'])->name('students.show');
|
||||
Route::get('/students/{student}/pdf', [StudentReportController::class, 'pdf'])->name('students.pdf');
|
||||
Route::put('/students/{student}', [StudentReportController::class, 'update'])->name('students.update');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user