feat(tatib): ekspor PDF surat peringatan — unduh PDF per surat (F4 resmi via dompdf) + rekap daftar SP (A4 landscape: ringkasan status, tabel rinci, ttd); tombol Ekspor PDF di /sp-letters & Unduh PDF di detail
This commit is contained in:
@@ -6,6 +6,7 @@ use App\Models\SpLetter;
|
||||
use App\Models\Setting;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SpLetterController extends Controller
|
||||
@@ -39,10 +40,56 @@ class SpLetterController extends Controller
|
||||
|
||||
$spLetter->update(['printed_at' => now(), 'status' => 'printed']);
|
||||
|
||||
// For now, return an HTML print view
|
||||
// HTML print view (untuk cetak langsung dari browser)
|
||||
return view('sp-letters.print', compact('spLetter', 'school'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unduh PDF surat peringatan (format F4 resmi).
|
||||
*/
|
||||
public function pdf(SpLetter $spLetter)
|
||||
{
|
||||
abort_unless(auth()->user()->canViewStudent($spLetter->student_id), 403);
|
||||
|
||||
$spLetter->load(['student', 'spThreshold', 'generator']);
|
||||
$school = $this->schoolSettings();
|
||||
$forPdf = true;
|
||||
|
||||
$spLetter->update(['printed_at' => now(), 'status' => 'printed']);
|
||||
|
||||
$pdf = Pdf::loadView('sp-letters.print', compact('spLetter', 'school', 'forPdf'))
|
||||
->setPaper([0, 0, 595.28, 935.43], 'portrait'); // F4 (210×330 mm)
|
||||
|
||||
$filename = 'SP-'.Str::slug(str_replace(['/', '\\'], '-', $spLetter->letter_number)).'.pdf';
|
||||
|
||||
return $pdf->download($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ekspor PDF rekap seluruh surat peringatan (daftar, A4 landscape).
|
||||
*/
|
||||
public function exportPdf()
|
||||
{
|
||||
$letters = SpLetter::with(['student', 'spThreshold', 'generator'])
|
||||
->when(auth()->user()->isScopedWaliKelas(), fn ($q) => $q->whereHas('student', fn ($qq) => $qq->whereIn('class_id', auth()->user()->homeroomClassIds())))
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
$summary = [
|
||||
'total' => $letters->count(),
|
||||
'total_poin' => $letters->sum('total_points_at_time'),
|
||||
'siswa' => $letters->pluck('student_id')->unique()->count(),
|
||||
'per_status' => $letters->groupBy('status')->map->count(),
|
||||
];
|
||||
|
||||
$school = $this->schoolSettings();
|
||||
|
||||
$pdf = Pdf::loadView('sp-letters.export-pdf', compact('letters', 'summary', 'school'))
|
||||
->setPaper('a4', 'landscape');
|
||||
|
||||
return $pdf->download('rekap-surat-peringatan-'.now()->format('Ymd-His').'.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
* Data sekolah untuk kop surat (format resmi).
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Rekap Surat Peringatan — {{ $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 { width: 100%; border-collapse: collapse; margin-bottom: 0; }
|
||||
.kop td { border: none; padding: 0; }
|
||||
.kop-logo { width: 118px; text-align: center; vertical-align: middle; }
|
||||
.kop-logo img { height: 88px; width: auto; max-width: 106px; object-fit: contain; }
|
||||
.kop-text { text-align: center; vertical-align: middle; }
|
||||
.kop-1, .kop-2 { font-size: 12px; font-weight: bold; color: #000; }
|
||||
.kop-3 { font-size: 14.5px; font-weight: bold; color: #000; margin: 2px 0; }
|
||||
.kop-4, .kop-5, .kop-6 { font-size: 8.5px; color: #000; }
|
||||
.kop-line { border-bottom: 2.5px solid #000; margin: 8px 0 14px; }
|
||||
.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; }
|
||||
.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-draft { background: #fef3c7; color: #92400e; }
|
||||
.badge-printed { background: #dbeafe; color: #1e40af; }
|
||||
.badge-signed { background: #dcfce7; color: #166534; }
|
||||
.badge-delivered { background: #ede9fe; color: #6b21a8; }
|
||||
.badge-other { background: #e2e8f0; color: #334155; }
|
||||
.poin { font-weight: bold; color: #b91c1c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{{-- Kop Surat (format resmi: logo kiri + teks tengah + garis tebal) --}}
|
||||
<table class="kop">
|
||||
<tr>
|
||||
<td class="kop-logo">
|
||||
@if (! empty($school['kop_logo']) && file_exists(public_path('storage/'.$school['kop_logo'])))
|
||||
<img src="{{ public_path('storage/'.$school['kop_logo']) }}" alt="logo">
|
||||
@elseif (! empty($school['logo']) && file_exists(public_path('storage/'.$school['logo'])))
|
||||
<img src="{{ public_path('storage/'.$school['logo']) }}" alt="logo">
|
||||
@endif
|
||||
</td>
|
||||
<td class="kop-text">
|
||||
<div class="kop-1">{{ $school['government'] }}</div>
|
||||
<div class="kop-2">{{ $school['agency'] }}</div>
|
||||
<div class="kop-3">{{ $school['full_name'] }}</div>
|
||||
<div class="kop-4">{{ $school['address_detail'] }}</div>
|
||||
<div class="kop-5">{{ $school['website_email'] }}</div>
|
||||
<div class="kop-6">{{ $school['postal'] }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="kop-line"></div>
|
||||
|
||||
{{-- Judul --}}
|
||||
<div class="judul">
|
||||
<h2>Rekap Surat Peringatan Siswa</h2>
|
||||
<p>Dicetak {{ now()->locale('id')->isoFormat('dddd, D MMMM YYYY') }}</p>
|
||||
</div>
|
||||
|
||||
{{-- Ringkasan --}}
|
||||
<table class="summary">
|
||||
<tr>
|
||||
<td><span class="angka">{{ $summary['total'] }}</span><span class="label">Total Surat</span></td>
|
||||
<td><span class="angka">{{ $summary['total_poin'] }}</span><span class="label">Total Poin</span></td>
|
||||
<td><span class="angka">{{ $summary['siswa'] }}</span><span class="label">Siswa Terkena SP</span></td>
|
||||
<td>
|
||||
<span class="angka" style="font-size:12px; color:#92400e;">
|
||||
{{ $summary['per_status']->get('draft', 0) }} / {{ $summary['per_status']->get('printed', 0) }}
|
||||
</span>
|
||||
<span class="label">Draft / Tercetak</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="angka" style="font-size:12px; color:#166534;">
|
||||
{{ $summary['per_status']->get('signed', 0) }} / {{ $summary['per_status']->get('delivered', 0) }}
|
||||
</span>
|
||||
<span class="label">TTD / Diserahkan</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{-- Rincian --}}
|
||||
<table class="detail">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:24px">No</th>
|
||||
<th>No. Surat</th>
|
||||
<th style="width:70px">Tanggal</th>
|
||||
<th style="width:70px">NISN</th>
|
||||
<th>Nama Siswa</th>
|
||||
<th style="width:52px">Kelas</th>
|
||||
<th style="width:74px">Jenis SP</th>
|
||||
<th class="center" style="width:34px">Poin</th>
|
||||
<th style="width:70px">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($letters as $i => $sp)
|
||||
@php
|
||||
$statusLabels = ['draft' => 'Draft', 'printed' => 'Tercetak', 'signed' => 'Ditandatangani', 'delivered' => 'Diserahkan'];
|
||||
$label = $statusLabels[$sp->status] ?? ucfirst($sp->status);
|
||||
$badge = isset($statusLabels[$sp->status]) ? 'badge-'.$sp->status : 'badge-other';
|
||||
@endphp
|
||||
<tr>
|
||||
<td>{{ $i + 1 }}</td>
|
||||
<td>{{ $sp->letter_number }}</td>
|
||||
<td>{{ $sp->created_at?->format('d/m/Y') }}</td>
|
||||
<td>{{ $sp->student?->nisn }}</td>
|
||||
<td>{{ $sp->student?->full_name }}</td>
|
||||
<td>{{ $sp->student?->class_name }}</td>
|
||||
<td>{{ $sp->spThreshold?->name }}</td>
|
||||
<td class="center"><span class="poin">{{ $sp->total_points_at_time }}</span></td>
|
||||
<td class="center"><span class="badge {{ $badge }}">{{ $label }}</span></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="9" class="center">Belum ada surat peringatan</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>
|
||||
@@ -3,9 +3,18 @@
|
||||
@section('title', 'Surat Peringatan')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Surat Peringatan</h1>
|
||||
<p class="text-sm text-gray-500">Daftar surat peringatan siswa</p>
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Surat Peringatan</h1>
|
||||
<p class="text-sm text-gray-500">Daftar surat peringatan siswa</p>
|
||||
</div>
|
||||
<a href="{{ route('sp-letters.export-pdf') }}"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition shadow-sm">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5 5-5M12 15V3"/>
|
||||
</svg>
|
||||
Ekspor PDF
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200/80 overflow-hidden">
|
||||
@@ -50,6 +59,7 @@
|
||||
<td class="px-4 py-3 text-sm text-gray-500">{{ $sp->created_at->format('d/m/Y') }}</td>
|
||||
<td class="px-4 py-3 text-right space-x-2">
|
||||
<a href="{{ route('sp-letters.show', $sp->id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">Detail</a>
|
||||
<a href="{{ route('sp-letters.pdf', $sp->id) }}" class="text-red-600 hover:text-red-800 text-sm font-medium">PDF</a>
|
||||
<a href="{{ route('sp-letters.print', $sp->id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium" target="_blank">Cetak</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<title>Cetak SP - {{ $spLetter->student->full_name }}</title>
|
||||
<style>
|
||||
@page { margin: 2cm 1cm; size: 210mm 330mm; } /* F4 — kiri/kanan 1cm */
|
||||
body { font-family: 'Times New Roman', Times, serif; font-size: 12pt; line-height: 1.5; color: #000; }
|
||||
body { font-family: 'Times New Roman', 'DejaVu Serif', Times, serif; font-size: 12pt; line-height: 1.5; color: #000; }
|
||||
.kop-surat { margin-bottom: 24px; }
|
||||
.kop-table { width: 100%; border-collapse: collapse; margin-bottom: 6px; }
|
||||
.kop-table td { border: none; padding: 0; }
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ route('sp-letters.index') }}" class="px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition">Kembali</a>
|
||||
<a href="{{ route('sp-letters.pdf', $spLetter->id) }}" class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700 transition">Unduh PDF</a>
|
||||
<a href="{{ route('sp-letters.print', $spLetter->id) }}" target="_blank" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition">Cetak</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,8 +95,10 @@ Route::middleware('auth')->group(function () {
|
||||
|
||||
Route::middleware('permission:view-sp-letters')->group(function () {
|
||||
Route::get('/sp-letters', [SpLetterController::class, 'index'])->name('sp-letters.index');
|
||||
Route::get('/sp-letters/export-pdf', [SpLetterController::class, 'exportPdf'])->name('sp-letters.export-pdf');
|
||||
Route::get('/sp-letters/{spLetter}', [SpLetterController::class, 'show'])->name('sp-letters.show');
|
||||
Route::get('/sp-letters/{spLetter}/print', [SpLetterController::class, 'print'])->name('sp-letters.print');
|
||||
Route::get('/sp-letters/{spLetter}/pdf', [SpLetterController::class, 'pdf'])->name('sp-letters.pdf');
|
||||
});
|
||||
|
||||
// ===== Master Data (permission-based) =====
|
||||
|
||||
Reference in New Issue
Block a user