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;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Student;
|
use App\Models\Student;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use App\Models\SpLetter;
|
use App\Models\SpLetter;
|
||||||
use App\Models\SpThreshold;
|
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
|
public function update(Request $request, Student $student): RedirectResponse
|
||||||
{
|
{
|
||||||
abort_unless(auth()->user()->canViewStudent($student), 403);
|
abort_unless(auth()->user()->canViewStudent($student), 403);
|
||||||
|
|||||||
@@ -26,7 +26,12 @@
|
|||||||
@forelse($letters as $sp)
|
@forelse($letters as $sp)
|
||||||
<tr class="hover:bg-gray-50">
|
<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-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">
|
<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;">
|
<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 ?? '-' }}
|
{{ $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>
|
</td>
|
||||||
{{-- Aksi --}}
|
{{-- Aksi --}}
|
||||||
<td class="px-5 py-4 text-right whitespace-nowrap">
|
<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) }}"
|
<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">
|
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>
|
<i class="fa-solid fa-eye"></i>
|
||||||
Detail
|
Detail
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@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="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 class="text-[6px] font-bold text-gray-500">{{ strtoupper(substr($p->user->name ?? '?', 0, 1)) }}</span>
|
||||||
</span>
|
</span>
|
||||||
{{ $p->user->name }}
|
{{ $p->user?->name ?? '—' }}
|
||||||
@if($p->role)
|
@if($p->role)
|
||||||
<span class="text-[9px] text-gray-400 bg-gray-50 px-1 rounded border border-gray-100">{{ $p->role }}</span>
|
<span class="text-[9px] text-gray-400 bg-gray-50 px-1 rounded border border-gray-100">{{ $p->role }}</span>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::middleware('permission:view-students')->group(function () {
|
Route::middleware('permission:view-students')->group(function () {
|
||||||
Route::get('/students', [StudentReportController::class, 'index'])->name('students.index');
|
Route::get('/students', [StudentReportController::class, 'index'])->name('students.index');
|
||||||
Route::get('/students/{student}', [StudentReportController::class, 'show'])->name('students.show');
|
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');
|
Route::put('/students/{student}', [StudentReportController::class, 'update'])->name('students.update');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user