feat: multi-pilih jenis pelanggaran, filter handling, export excel

- Multi-pilih pelanggaran dalam satu form input
- Export Excel dengan 19 kolom (termasuk data penanganan)
- Filter handling_status di index pelanggaran
- Handling info di riwayat pelanggaran siswa
- Fix format handled_at casting
- Perbaikan UI form input (5 bagian terstruktur, severity indicator)
This commit is contained in:
Bro
2026-07-22 22:46:51 +07:00
parent 8529a49174
commit 4bc3a84203
9 changed files with 754 additions and 349 deletions
@@ -0,0 +1,157 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Violation;
use Illuminate\Http\Request;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
class ViolationExportController extends Controller
{
public function export(Request $request)
{
$query = Violation::with([
'student', 'violationType.category', 'recorder',
'handlings.participants.user', 'handler',
]);
if ($request->filled('date_from')) {
$query->whereDate('violation_date', '>=', $request->date_from);
}
if ($request->filled('date_to')) {
$query->whereDate('violation_date', '<=', $request->date_to);
}
$violations = $query->orderBy('violation_date', 'desc')->get();
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Data Pelanggaran');
// Headers
$headers = [
'A1' => 'No',
'B1' => 'Tanggal',
'C1' => 'Waktu',
'D1' => 'NISN',
'E1' => 'Nama Siswa',
'F1' => 'Kelas',
'G1' => 'Jurusan',
'H1' => 'Jenis Pelanggaran',
'I1' => 'Kategori',
'J1' => 'Poin',
'K1' => 'Sanksi',
'L1' => 'Lokasi',
'M1' => 'Deskripsi',
'N1' => 'Dicatat Oleh',
'O1' => 'Status Verifikasi',
'P1' => 'Status Penanganan',
'Q1' => 'Penanganan',
'R1' => 'Yang Menangani',
'S1' => 'Tanggal Dibuat',
];
// Style header
$headerStyle = [
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF'], 'size' => 11],
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '2563EB']],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
];
foreach ($headers as $cell => $value) {
$sheet->setCellValue($cell, $value);
}
$sheet->getStyle('A1:S1')->applyFromArray($headerStyle);
// Column widths
$sheet->getColumnDimension('A')->setWidth(5);
$sheet->getColumnDimension('B')->setWidth(14);
$sheet->getColumnDimension('C')->setWidth(8);
$sheet->getColumnDimension('D')->setWidth(16);
$sheet->getColumnDimension('E')->setWidth(30);
$sheet->getColumnDimension('F')->setWidth(12);
$sheet->getColumnDimension('G')->setWidth(18);
$sheet->getColumnDimension('H')->setWidth(30);
$sheet->getColumnDimension('I')->setWidth(14);
$sheet->getColumnDimension('J')->setWidth(7);
$sheet->getColumnDimension('K')->setWidth(20);
$sheet->getColumnDimension('L')->setWidth(12);
$sheet->getColumnDimension('M')->setWidth(40);
$sheet->getColumnDimension('N')->setWidth(20);
$sheet->getColumnDimension('O')->setWidth(18);
$sheet->getColumnDimension('P')->setWidth(18);
$sheet->getColumnDimension('Q')->setWidth(30);
$sheet->getColumnDimension('R')->setWidth(30);
$sheet->getColumnDimension('S')->setWidth(16);
// Data
$row = 2;
foreach ($violations as $i => $v) {
$handlingTypes = $v->handlings->pluck('handling_type')->implode(', ');
$participants = $v->handlings->flatMap(function ($h) {
return $h->participants->map(fn($p) => ($p->user->name ?? '') . ($p->role ? " ({$p->role})" : ''));
})->implode(', ');
$statusVerifikasi = $v->is_verified ? 'Terverifikasi' : 'Belum';
$statusLabels = ['unhandled' => 'Belum Ditangani', 'in_progress' => 'Dalam Proses', 'resolved' => 'Selesai'];
$statusPenanganan = $statusLabels[$v->handling_status] ?? $v->handling_status;
$sheet->setCellValue("A{$row}", $i + 1);
$sheet->setCellValue("B{$row}", $v->violation_date->format('d/m/Y'));
$sheet->setCellValue("C{$row}", $v->violation_time ? \Carbon\Carbon::parse($v->violation_time)->format('H:i') : '');
$sheet->setCellValue("D{$row}", $v->student->nisn ?? '');
$sheet->setCellValue("E{$row}", $v->student->full_name ?? '');
$sheet->setCellValue("F{$row}", $v->student->class_name ?? '');
$sheet->setCellValue("G{$row}", $v->student->department_name ?? '');
$sheet->setCellValue("H{$row}", $v->violationType->name ?? '');
$sheet->setCellValue("I{$row}", $v->violationType?->category?->name ?? '');
$sheet->setCellValue("J{$row}", $v->points);
$sheet->setCellValue("K{$row}", $v->sanction ?? '');
$sheet->setCellValue("L{$row}", $v->location ?? '');
$sheet->setCellValue("M{$row}", $v->description ?? '');
$sheet->setCellValue("N{$row}", $v->recorder->name ?? '');
$sheet->setCellValue("O{$row}", $statusVerifikasi);
$sheet->setCellValue("P{$row}", $statusPenanganan);
$sheet->setCellValue("Q{$row}", $handlingTypes);
$sheet->setCellValue("R{$row}", $participants);
$sheet->setCellValue("S{$row}", $v->created_at->format('d/m/Y H:i'));
// Alternate row color
if ($i % 2 === 0) {
$sheet->getStyle("A{$row}:S{$row}")
->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()
->setRGB('F8FAFC');
}
$sheet->getStyle("A{$row}:S{$row}")->getAlignment()->setVertical(Alignment::VERTICAL_TOP);
$row++;
}
// Auto filter
$sheet->setAutoFilter("A1:S" . ($row - 1));
// Borders
$styleArray = [
'borders' => [
'allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'E2E8F0']],
],
];
$sheet->getStyle("A1:S" . ($row - 1))->applyFromArray($styleArray);
$writer = new Xlsx($spreadsheet);
$filename = 'data-pelanggaran-' . date('Y-m-d') . '.xlsx';
return response()->streamDownload(function () use ($writer) {
$writer->save('php://output');
}, $filename, [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}
}
@@ -58,7 +58,7 @@ class StudentReportController extends Controller
public function show(Student $student): View
{
$student->load(['violations' => function ($q) {
$q->with(['violationType.category', 'recorder', 'evidences'])
$q->with(['violationType.category', 'recorder', 'evidences', 'handlings.participants.user'])
->latest()
->take(50);
}, 'spLetters' => function ($q) {
+19 -10
View File
@@ -89,7 +89,8 @@ class ViolationController extends Controller
{
$validated = $request->validate([
'student_id' => ['required', 'exists:students,id'],
'violation_type_id' => ['required', 'exists:violation_types,id'],
'violation_type_ids' => ['required', 'array', 'min:1', 'max:10'],
'violation_type_ids.*' => ['required', 'exists:violation_types,id'],
'description' => ['nullable', 'string', 'max:1000'],
'location' => ['nullable', 'string', 'max:255'],
'violation_date' => ['required', 'date'],
@@ -97,19 +98,27 @@ class ViolationController extends Controller
'notes' => ['nullable', 'string', 'max:1000'],
'evidences' => ['nullable', 'array', 'max:5'],
'evidences.*' => ['image', 'mimes:jpeg,png,jpg,gif,webp', 'max:5120'],
'sanction' => ['nullable', 'string', 'max:500'],
]);
$violationType = ViolationType::findOrFail($validated['violation_type_id']);
$validated['points'] = $violationType->points;
$validated['sanction'] = $validated['sanction'] ?? $violationType->default_sanction;
$evidences = $request->file('evidences') ?? [];
$created = 0;
foreach ($validated['violation_type_ids'] as $typeId) {
$type = ViolationType::findOrFail($typeId);
$data = array_merge($validated, [
'violation_type_id' => $typeId,
'points' => $type->points,
'sanction' => $type->default_sanction,
'evidences' => $evidences,
]);
$this->violationService->recordViolation($data, $request->user()->id);
$created++;
}
//$this->violationService->recordViolation($validated, $request->user()->id);
$data = $validated;
$data['evidences'] = $request->file('evidences') ?? [];
$this->violationService->recordViolation($data, $request->user()->id);
return redirect()->route('violations.index')
->with('success', 'Pelanggaran berhasil dicatat.');
->with('success', "{$created} pelanggaran berhasil dicatat.");
}
public function show(Violation $violation): View
+1
View File
@@ -26,6 +26,7 @@ class Violation extends Model
'violation_time' => 'datetime:H:i',
'is_verified' => 'boolean',
'verified_at' => 'datetime',
'handled_at' => 'datetime',
'points' => 'integer',
];
}
+107 -52
View File
@@ -205,73 +205,128 @@
</div>
@forelse($student->violations as $v)
@php
$handleStatusLabels = [
'unhandled' => ['label' => 'Belum Ditangani', 'class' => 'bg-red-50 text-red-600 border-red-200'],
'in_progress' => ['label' => 'Dalam Proses', 'class' => 'bg-yellow-50 text-yellow-600 border-yellow-200'],
'resolved' => ['label' => 'Selesai', 'class' => 'bg-green-50 text-green-600 border-green-200'],
];
$hs = $handleStatusLabels[$v->handling_status] ?? ['label' => $v->handling_status, 'class' => 'bg-gray-50 text-gray-600'];
@endphp
<div class="violation-timeline relative pl-[52px] pr-6 py-4 {{ !$loop->last ? 'border-b border-gray-50' : '' }} hover:bg-gray-50/50 transition">
{{-- Timeline dot --}}
<div class="absolute left-[15px] top-[15px] w-[16px] h-[16px] rounded-full border-[3px] bg-white flex items-center justify-center"
style="border-color: {{ $v->violationType?->category?->color ?? '#9ca3af' }}">
<span class="w-[6px] h-[6px] rounded-full" style="background-color: {{ $v->violationType?->category?->color ?? '#9ca3af' }}"></span>
</div>
<div class="flex items-start justify-between gap-4">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="text-sm font-semibold text-gray-900">{{ $v->violationType->name ?? '—' }}</span>
@if($v->is_verified)
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium bg-emerald-50 text-emerald-600 rounded-full border border-emerald-200">
<span class="w-1 h-1 bg-emerald-500 rounded-full"></span>
Terverifikasi
<div class="space-y-2.5">
{{-- Top row: violation name + badges --}}
<div class="flex items-start justify-between gap-4">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-1.5">
<span class="text-sm font-bold text-gray-900">{{ $v->violationType->name ?? '—' }}</span>
{{-- Verification badge --}}
@if($v->is_verified)
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium bg-emerald-50 text-emerald-700 rounded-full border border-emerald-200">
<span class="w-1 h-1 bg-emerald-500 rounded-full"></span>
Terverifikasi
</span>
@endif
{{-- Handling badge --}}
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium rounded-full border {{ $hs['class'] }}">
<span class="w-1.5 h-1.5 rounded-full {{ $v->handling_status === 'resolved' ? 'bg-green-500' : ($v->handling_status === 'in_progress' ? 'bg-yellow-500' : 'bg-red-500') }}"></span>
{{ $hs['label'] }}
</span>
</div>
@if($v->description)
<p class="text-sm text-gray-500 mt-1 line-clamp-2">{{ $v->description }}</p>
@endif
</div>
@if($v->description)
<p class="text-sm text-gray-500 mt-0.5 line-clamp-2">{{ $v->description }}</p>
@endif
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 mt-2 text-xs text-gray-400">
<span class="inline-flex items-center gap-1">
<i class="fa-solid fa-calendar text-gray-300"></i>
{{ $v->violation_date->format('d M Y') }}
@if($v->violation_time) {{ \Carbon\Carbon::parse($v->violation_time)->format('H:i') }} @endif
</span>
@if($v->location)
<span class="inline-flex items-center gap-1">
<i class="fa-solid fa-location-dot text-gray-300"></i>
{{ $v->location }}
</span>
@endif
<span class="inline-flex items-center gap-1">
<i class="fa-solid fa-user-pen text-gray-300"></i>
{{ $v->recorder->name ?? '—' }}
</span>
</div>
@if($v->evidences && $v->evidences->count() > 0)
<div class="flex gap-2 mt-2.5">
@foreach($v->evidences->take(3) as $ev)
<a href="{{ Storage::url($ev->file_path) }}" target="_blank"
class="w-12 h-12 rounded-lg border border-gray-200 overflow-hidden hover:ring-2 hover:ring-blue-300 transition-all">
<img src="{{ Storage::url($ev->file_path) }}" class="w-full h-full object-cover" alt="Bukti">
</a>
@endforeach
@if($v->evidences->count() > 3)
<div class="w-12 h-12 rounded-lg bg-gray-50 border border-gray-200 flex items-center justify-center text-[10px] text-gray-400 font-medium">
+{{ $v->evidences->count() - 3 }}
</div>
@endif
</div>
@endif
{{-- Poin --}}
<div class="flex flex-col items-center flex-shrink-0 min-w-[72px]">
<span class="inline-flex items-center px-3 py-1 text-xs font-bold rounded-lg shadow-sm
{{ $v->points >= 50 ? 'bg-red-500 text-white' : ($v->points >= 15 ? 'bg-yellow-500 text-white' : 'bg-blue-500 text-white') }}">
+{{ $v->points }}
</span>
@if($v->sanction)
<span class="text-[10px] text-gray-400 leading-tight text-center line-clamp-2 mt-1 max-w-[72px]">{{ $v->sanction }}</span>
@endif
</div>
</div>
<div class="flex flex-col items-center flex-shrink-0 gap-2.5 min-w-[72px]">
<span class="inline-flex items-center px-3 py-1 text-xs font-bold rounded-lg shadow-sm
{{ $v->points >= 50 ? 'bg-red-500 text-white' : ($v->points >= 15 ? 'bg-yellow-500 text-white' : 'bg-blue-500 text-white') }}">
+{{ $v->points }}
{{-- Metadata row --}}
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-gray-400">
<span class="inline-flex items-center gap-1.5">
<i class="fa-regular fa-calendar text-gray-300"></i>
{{ $v->violation_date->format('d M Y') }}
@if($v->violation_time) {{ \Carbon\Carbon::parse($v->violation_time)->format('H:i') }} @endif
</span>
@if($v->sanction)
<span class="text-[10px] text-gray-400 leading-tight text-center line-clamp-2 max-w-[72px]">{{ $v->sanction }}</span>
@if($v->location)
<span class="inline-flex items-center gap-1.5">
<i class="fa-solid fa-location-dot text-gray-300"></i>
{{ $v->location }}
</span>
@endif
<span class="inline-flex items-center gap-1.5">
<i class="fa-solid fa-user-pen text-gray-300"></i>
{{ $v->recorder->name ?? '—' }}
</span>
</div>
{{-- Evidence thumbs --}}
@if($v->evidences && $v->evidences->count() > 0)
<div class="flex gap-2">
@foreach($v->evidences->take(3) as $ev)
<a href="{{ Storage::url($ev->file_path) }}" target="_blank"
class="w-10 h-10 rounded-lg border border-gray-200 overflow-hidden hover:ring-2 hover:ring-blue-300 transition-all flex-shrink-0">
<img src="{{ Storage::url($ev->file_path) }}" class="w-full h-full object-cover" alt="Bukti">
</a>
@endforeach
@if($v->evidences->count() > 3)
<div class="w-10 h-10 rounded-lg bg-gray-50 border border-gray-200 flex items-center justify-center text-[10px] text-gray-400 font-medium flex-shrink-0">
+{{ $v->evidences->count() - 3 }}
</div>
@endif
</div>
@endif
{{-- Handlings --}}
@if($v->handlings && $v->handlings->count() > 0)
<div class="ml-0 space-y-1.5">
@foreach($v->handlings as $h)
<div class="inline-flex items-center gap-1.5 bg-white border border-gray-100 rounded-lg px-2.5 py-1.5 shadow-sm">
<div class="w-5 h-5 rounded bg-gray-50 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-hand-holding-heart text-[9px] text-amber-500"></i>
</div>
<span class="text-[11px] font-medium text-gray-700">{{ $h->handling_type }}</span>
<span class="text-[10px] text-gray-400">{{ $h->handling_date->format('d/m') }}</span>
@if($h->participants->count() > 0)
<span class="w-px h-3 bg-gray-200"></span>
@foreach($h->participants as $p)
<span class="inline-flex items-center gap-1 text-[10px] text-gray-500">
<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 }}
@if($p->role)
<span class="text-[9px] text-gray-400 bg-gray-50 px-1 rounded border border-gray-100">{{ $p->role }}</span>
@endif
</span>
@endforeach
@endif
</div>
@endforeach
</div>
@endif
{{-- Detail link --}}
<div class="flex justify-end">
<a href="{{ route('violations.show', $v->id) }}"
class="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-blue-700 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 hover:border-blue-300 transition-all shadow-sm w-full">
Detail
<i class="fa-solid fa-arrow-right text-[10px]"></i>
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-[11px] font-semibold text-blue-600 bg-blue-50 border border-blue-100 rounded-lg hover:bg-blue-100 transition shadow-sm">
Detail Pelanggaran
<i class="fa-solid fa-arrow-right text-[9px]"></i>
</a>
</div>
</div>
+357 -217
View File
@@ -4,206 +4,334 @@
@section('content')
<div x-data="violationForm()">
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">Input Pelanggaran</h1>
<p class="text-sm text-gray-500">Catat pelanggaran siswa baru</p>
{{-- Header --}}
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<nav class="flex items-center gap-1.5 text-sm text-gray-400 mb-1">
<a href="{{ route('violations.index') }}" class="hover:text-gray-600 transition">Data Pelanggaran</a>
<span class="text-gray-300">/</span>
<span class="text-gray-700 font-medium">Input Baru</span>
</nav>
<h1 class="text-2xl font-bold text-gray-900 tracking-tight">Input Pelanggaran</h1>
<p class="text-sm text-gray-500 mt-1">Catat pelanggaran siswa baru</p>
</div>
</div>
<form action="{{ route('violations.store') }}" method="POST" enctype="multipart/form-data" class="bg-white rounded-xl shadow-sm border border-gray-200">
<form action="{{ route('violations.store') }}" method="POST" enctype="multipart/form-data"
class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
@csrf
<div class="p-6 space-y-6">
{{-- Search Siswa --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Siswa <span class="text-red-500">*</span></label>
<div class="relative">
<input type="text" x-model="searchQuery" @input.debounce="searchStudents"
placeholder="Cari NISN atau Nama siswa..."
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
<input type="hidden" name="student_id" x-model="selectedStudentId">
<div x-show="results.length > 0 && !selectedStudentId"
class="absolute z-10 mt-1 w-full bg-white shadow-lg border border-gray-200 rounded-xl max-h-48 overflow-y-auto">
<template x-for="s in results" :key="s.id">
<div @click="selectStudent(s)"
class="px-4 py-2.5 hover:bg-red-50 cursor-pointer border-b border-gray-100 last:border-0 flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-900" x-text="s.full_name"></p>
<p class="text-xs text-gray-500" x-text="s.nisn + ' - ' + (s.class_name || '')"></p>
</div>
</div>
</template>
</div>
<div x-show="selectedStudentId"
class="mt-2 flex items-center justify-between p-3 bg-red-50 border border-blue-200 rounded-xl">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
<span class="text-sm font-bold text-blue-600" x-text="selectedStudentName.charAt(0)"></span>
</div>
<div>
<p class="text-sm font-medium text-gray-900" x-text="selectedStudentName"></p>
<p class="text-xs text-gray-500" x-text="selectedStudentInfo"></p>
</div>
</div>
<button type="button" @click="clearStudent()" class="text-red-400 hover:text-blue-600 transition p-1">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
@error('student_id') <p class="mt-1 text-sm text-blue-600">{{ $message }}</p> @enderror
</div>
{{-- Jenis Pelanggaran + Poin & Sanksi in ONE scope --}}
<div x-data="typeSearch({{ json_encode($typeGroups) }})">
<label class="block text-sm font-medium text-gray-700 mb-1.5">Jenis Pelanggaran <span class="text-red-500">*</span></label>
<div class="relative">
<input type="text" x-model="q" @input="open=true" @focus="open=true"
@click.away="open=false" @keydown.escape="open=false"
placeholder="Ketik nama pelanggaran..."
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition"
:class="selectedId ? 'border-green-300 bg-green-50' : ''">
<input type="hidden" name="violation_type_id" x-model="selectedId">
<div x-show="open && filtered.length > 0"
class="absolute z-20 mt-1 w-full bg-white shadow-lg border border-gray-200 rounded-xl max-h-72 overflow-y-auto">
<template x-for="(group, gi) in filtered" :key="gi">
<div>
<div class="sticky top-0 px-4 py-1.5 text-xs font-semibold uppercase tracking-wider border-b flex items-center space-x-1.5 z-10"
:style="'background:' + group.color + '15; color:' + group.color + '; border-color:' + group.color + '30'">
<span class="w-2 h-2 rounded-full" :style="'background:' + group.color"></span>
<span x-text="group.label"></span>
<span class="ml-1 opacity-60" x-text="'(' + group.types.length + ')'"></span>
</div>
<template x-for="t in group.types" :key="t.id">
<div @click="pick(t)" @keydown.enter="pick(t)" tabindex="0"
class="px-4 py-2.5 cursor-pointer border-b border-gray-50 flex items-center justify-between transition"
:class="selectedId == t.id ? 'bg-red-50 border-l-2 border-l-red-500' : 'hover:bg-gray-50'">
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 truncate" x-text="t.name"></p>
<p class="text-xs text-gray-400 truncate" x-text="t.sanction || '—'"></p>
</div>
<span class="ml-3 text-sm font-bold flex-shrink-0 px-2 py-0.5 rounded"
:class="t.points >= 50 ? 'bg-blue-100 text-red-700' : (t.points >= 15 ? 'bg-yellow-100 text-yellow-700' : 'bg-green-100 text-green-700')"
x-text="'+' + t.points"></span>
</div>
</template>
</div>
</template>
</div>
<div x-show="open && filtered.length === 0 && q.length > 0"
class="absolute z-20 mt-1 w-full bg-white shadow-lg border border-gray-200 rounded-xl p-5 text-center">
<p class="text-sm text-gray-500">Tidak ditemukan untuk "<span x-text="q" class="font-medium"></span>"</p>
</div>
<div x-show="selectedId && selectedName"
class="mt-2 flex items-center justify-between p-3 rounded-xl border"
:style="'border-color:' + selColor + '40; background:' + selColor + '08'">
<div class="flex items-center space-x-2.5">
<span class="w-3 h-3 rounded-full" :style="'background:' + selColor"></span>
<div>
<p class="text-sm font-medium text-gray-900" x-text="selectedName"></p>
<p class="text-xs text-gray-500" x-text="selectedSanction ? 'Sanksi: ' + selectedSanction : ''"></p>
</div>
</div>
<div class="flex items-center space-x-3">
<span class="text-xl font-bold" :class="selectedPoints >= 50 ? 'text-blue-600' : (selectedPoints >= 15 ? 'text-yellow-600' : 'text-green-600')" x-text="'+' + selectedPoints"></span>
<button @click="clear()" class="text-gray-300 hover:text-red-500 transition">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
</div>
@error('violation_type_id') <p class="mt-1 text-sm text-blue-600">{{ $message }}</p> @enderror
{{-- Poin & Sanksi -- same scope as typeSearch --}}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Poin</label>
<input type="number" name="points" x-model="selectedPoints" readonly
class="w-full px-4 py-2.5 bg-gray-50 border border-gray-300 rounded-xl sm:text-sm text-gray-700">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Sanksi</label>
<input type="text" name="sanction" x-model="selectedSanction"
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
</div>
</div>
</div>
{{-- Tanggal & Waktu --}}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Tanggal Pelanggaran <span class="text-red-500">*</span></label>
<input type="date" name="violation_date" value="{{ date('Y-m-d') }}" required
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
{{-- Gradient header --}}
<div class="bg-gradient-to-r from-blue-500 via-blue-400 to-sky-300 px-6 py-5">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-white/20 flex items-center justify-center backdrop-blur-sm">
<i class="fa-solid fa-gavel text-white text-lg"></i>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Waktu</label>
<input type="time" name="violation_time" value="{{ date('H:i') }}"
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
<h2 class="text-base font-bold text-white">Form Pelanggaran Baru</h2>
<p class="text-xs text-white/70">Isi data pelanggaran dengan lengkap</p>
</div>
</div>
{{-- Lokasi --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Lokasi</label>
<input type="text" name="location" placeholder="Contoh: Depan kelas, Lapangan, dll"
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
</div>
{{-- Deskripsi --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Deskripsi / Catatan</label>
<textarea name="description" rows="3" placeholder="Deskripsi pelanggaran..."
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition resize-none"></textarea>
</div>
{{-- Foto Bukti --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Bukti Foto <span class="text-gray-400 font-normal">(maks. 5 foto)</span></label>
<input type="file" name="evidences[]" id="evidences" multiple accept="image/*" capture="environment" @change="handleFiles" class="hidden">
<div class="grid grid-cols-3 md:grid-cols-5 gap-3 mb-3">
<template x-for="(file, index) in files" :key="index">
<div class="relative aspect-square rounded-xl overflow-hidden border border-gray-200 bg-gray-50 group">
<img :src="file.url" class="w-full h-full object-cover">
<button type="button" @click="removeFile(index)"
class="absolute top-1.5 right-1.5 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center text-xs hover:bg-blue-700 transition opacity-0 group-hover:opacity-100">
<i class="fa-solid fa-xmark"></i>
</button>
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent text-white text-xs px-2 py-1.5 truncate" x-text="file.name"></div>
</div>
</template>
<template x-if="files.length < 5">
<div @click="document.getElementById('evidences').click()"
class="aspect-square rounded-xl border-2 border-dashed border-gray-300 hover:border-red-400 bg-gray-50 flex flex-col items-center justify-center cursor-pointer transition hover:bg-red-50/30">
<i class="fa-solid fa-camera text-gray-300 text-2xl mb-1"></i>
<span class="text-xs text-gray-400">Tambah Foto</span>
</div>
</template>
</div>
<div class="flex space-x-2">
<button type="button" @click="document.getElementById('evidences').click()" class="px-4 py-2 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 transition flex items-center space-x-2">
<i class="fa-solid fa-image text-gray-500"></i><span>Pilih File</span>
</button>
<button type="button" @click="captureCamera" class="px-4 py-2 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 transition flex items-center space-x-2">
<i class="fa-solid fa-camera text-gray-500"></i><span>Ambil Foto</span>
</button>
</div>
@error('evidences.*') <p class="mt-1 text-sm text-blue-600">{{ $message }}</p> @enderror
</div>
</div>
{{-- Submit --}}
<div class="px-6 py-4 bg-gray-50 border-t border-gray-100 rounded-b-xl flex justify-end space-x-3">
<a href="{{ route('violations.index') }}" class="px-5 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-xl hover:bg-gray-50 transition">Batal</a>
<button type="submit" class="px-6 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm flex items-center space-x-2">
<i class="fa-solid fa-floppy-disk"></i>
<span>Simpan Pelanggaran</span>
<div class="p-6 space-y-8">
{{-- ===== BAGIAN 1: DATA SISWA ===== --}}
<div>
<div class="flex items-center gap-2.5 mb-4">
<div class="w-7 h-7 rounded-lg bg-blue-50 flex items-center justify-center">
<i class="fa-solid fa-user-graduate text-blue-500 text-xs"></i>
</div>
<span class="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Data Siswa</span>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1.5">Cari Siswa <span class="text-red-500">*</span></label>
<div class="relative">
<div class="relative">
<i class="fa-solid fa-magnifying-glass absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400 text-sm pointer-events-none"></i>
<input type="text" x-model="searchQuery" @input.debounce="searchStudents"
placeholder="Cari NISN atau Nama siswa..."
class="w-full pl-9 pr-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>
<input type="hidden" name="student_id" x-model="selectedStudentId">
{{-- Search results dropdown --}}
<div x-show="results.length > 0 && !selectedStudentId"
class="absolute z-10 mt-1 w-full bg-white shadow-lg border border-gray-200 rounded-xl max-h-48 overflow-y-auto">
<template x-for="s in results" :key="s.id">
<div @click="selectStudent(s)"
class="px-4 py-2.5 hover:bg-blue-50 cursor-pointer border-b border-gray-100 last:border-0 flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-900" x-text="s.full_name"></p>
<p class="text-xs text-gray-500" x-text="s.nisn + ' - ' + (s.class_name || '')"></p>
</div>
<span class="text-xs text-gray-400" x-text="s.class_level"></span>
</div>
</template>
</div>
{{-- Selected student card --}}
<div x-show="selectedStudentId"
class="mt-2.5 flex items-center justify-between p-3 bg-gradient-to-br from-blue-50 to-indigo-50/50 border border-blue-100 rounded-xl">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center flex-shrink-0 shadow-sm">
<span class="text-sm font-bold text-white" x-text="selectedStudentName.charAt(0)"></span>
</div>
<div>
<p class="text-sm font-bold text-gray-900" x-text="selectedStudentName"></p>
<p class="text-xs text-gray-500" x-text="selectedStudentInfo"></p>
</div>
</div>
<button type="button" @click="clearStudent()"
class="w-7 h-7 rounded-lg flex items-center justify-center text-gray-300 hover:text-red-500 hover:bg-red-50 transition">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
@error('student_id') <p class="mt-1.5 text-xs text-red-500 flex items-center gap-1"><i class="fa-solid fa-circle-exclamation"></i> {{ $message }}</p> @enderror
</div>
</div>
{{-- ===== BAGIAN 2: PELANGGARAN ===== --}}
<div>
<div class="flex items-center gap-2.5 mb-4">
<div class="w-7 h-7 rounded-lg bg-amber-50 flex items-center justify-center">
<i class="fa-solid fa-triangle-exclamation text-amber-500 text-xs"></i>
</div>
<span class="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Detail Pelanggaran</span>
</div>
<div x-data="typeMultiSearch({{ json_encode($typeGroups) }})" class="space-y-4">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1.5">Jenis Pelanggaran <span class="text-red-500">*</span></label>
<p class="text-xs text-gray-400 mb-2">Cari dan pilih satu atau lebih pelanggaran yang dilakukan</p>
{{-- Search with dropdown --}}
<div class="relative">
<div class="relative">
<i class="fa-solid fa-magnifying-glass absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400 text-sm pointer-events-none"></i>
<input type="text" x-model="q" @input="open=true" @focus="open=true"
@click.away="open=false" @keydown.escape="open=false"
placeholder="Ketik nama pelanggaran untuk menambah..."
class="w-full pl-9 pr-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>
{{-- Search results --}}
<div x-show="open && filtered.length > 0"
class="absolute z-20 mt-1 w-full bg-white shadow-lg border border-gray-200 rounded-xl max-h-72 overflow-y-auto">
<template x-for="(group, gi) in filtered" :key="gi">
<div>
<div class="sticky top-0 px-4 py-1.5 text-xs font-bold uppercase tracking-wider border-b flex items-center gap-1.5 z-10"
:style="'background:' + group.color + '12; color:' + group.color + '; border-color:' + group.color + '30'">
<span class="w-2 h-2 rounded-full" :style="'background:' + group.color"></span>
<span x-text="group.label"></span>
<span class="ml-auto text-[10px] opacity-60" x-text="'(' + group.types.length + ')'"></span>
</div>
<template x-for="t in group.types" :key="t.id">
<div @click="addType(t)" @keydown.enter="addType(t)" tabindex="0"
class="px-4 py-2.5 cursor-pointer border-b border-gray-50 flex items-center justify-between transition hover:bg-blue-50"
:class="selectedIds.includes(t.id) ? 'bg-blue-50/50 opacity-50' : ''">
<div class="flex-1 min-w-0">
<p class="text-sm font-semibold text-gray-900 truncate" x-text="t.name"></p>
<p class="text-xs text-gray-400 truncate" x-text="t.sanction || '—'"></p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<span class="text-xs font-bold px-2.5 py-1 rounded-lg"
:class="t.points >= 50 ? 'bg-red-50 text-red-700' : (t.points >= 15 ? 'bg-yellow-50 text-yellow-700' : 'bg-blue-50 text-blue-700')"
x-text="'+' + t.points"></span>
<template x-if="selectedIds.includes(t.id)">
<i class="fa-solid fa-check text-green-500 text-sm"></i>
</template>
<template x-if="!selectedIds.includes(t.id)">
<i class="fa-solid fa-plus text-gray-300 text-sm"></i>
</template>
</div>
</div>
</template>
</div>
</template>
</div>
{{-- No results --}}
<div x-show="open && filtered.length === 0 && q.length > 0"
class="absolute z-20 mt-1 w-full bg-white shadow-lg border border-gray-200 rounded-xl p-5 text-center">
<p class="text-sm text-gray-500">Tidak ditemukan untuk "<span x-text="q" class="font-medium"></span>"</p>
</div>
</div>
@error('violation_type_ids') <p class="mt-1.5 text-xs text-red-500 flex items-center gap-1"><i class="fa-solid fa-circle-exclamation"></i> {{ $message }}</p> @enderror
@error('violation_type_ids.*') <p class="mt-1.5 text-xs text-red-500 flex items-center gap-1"><i class="fa-solid fa-circle-exclamation"></i> {{ $message }}</p> @enderror
</div>
{{-- Selected types list --}}
<template x-if="selected.length > 0">
<div>
<div class="flex items-center justify-between mb-2">
<span class="text-xs font-semibold text-gray-500 uppercase tracking-wider" x-text="'Dipilih (' + selected.length + ')'"></span>
<button type="button" @click="clearAll()" class="text-xs font-medium text-red-500 hover:text-red-700 transition inline-flex items-center gap-1">
<i class="fa-solid fa-xmark"></i> Hapus Semua
</button>
</div>
<div class="space-y-2">
<template x-for="(item, i) in selected" :key="item.id">
<div class="flex items-center justify-between p-3 rounded-xl border shadow-sm"
:style="'border-color:' + item.color + '30; background:' + item.color + '06'">
<input type="hidden" :name="'violation_type_ids[' + i + ']'" :value="item.id">
<div class="flex items-center gap-2.5 min-w-0">
<span class="w-3 h-3 rounded-full flex-shrink-0" :style="'background:' + item.color"></span>
<div class="min-w-0">
<p class="text-sm font-bold text-gray-900" x-text="item.name"></p>
<p class="text-xs text-gray-500" x-text="item.sanction || 'Tanpa sanksi'"></p>
</div>
</div>
<div class="flex items-center gap-3 flex-shrink-0">
<span class="text-base font-bold"
:class="item.points >= 50 ? 'text-red-600' : (item.points >= 15 ? 'text-yellow-600' : 'text-blue-600')"
x-text="'+' + item.points"></span>
<button type="button" @click="removeType(i)"
class="w-7 h-7 rounded-lg flex items-center justify-center text-gray-300 hover:text-red-500 hover:bg-red-50 transition">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
</template>
</div>
{{-- Summary bar --}}
<div class="mt-3 flex items-center justify-between p-3 bg-gradient-to-br from-gray-50 to-gray-100/50 rounded-xl border border-gray-100">
<div class="flex items-center gap-2">
<i class="fa-solid fa-calculator text-gray-400 text-xs"></i>
<span class="text-xs text-gray-600 font-medium">Total</span>
</div>
<div class="flex items-center gap-3">
<span class="text-sm text-gray-500" x-text="selected.length + ' pelanggaran'"></span>
<span class="text-lg font-bold text-gray-800" x-text="'+' + totalPoints"></span>
</div>
</div>
{{-- Severity info --}}
<div class="mt-2 flex items-center gap-2 p-3 rounded-xl text-xs font-medium"
:class="totalPoints >= 100 ? 'bg-red-50 text-red-700 border border-red-200' : (totalPoints >= 50 ? 'bg-orange-50 text-orange-700 border border-orange-200' : (totalPoints >= 15 ? 'bg-yellow-50 text-yellow-700 border border-yellow-200' : 'bg-blue-50 text-blue-700 border border-blue-200'))">
<i class="fa-solid fa-circle-info"></i>
<span x-text="totalPoints >= 100 ? 'Perhatian! Total poin mencapai level SP-2/3 — segera ditindaklanjuti' : (totalPoints >= 50 ? 'Total poin mencapai level SP-1 — perlu penanganan' : (totalPoints >= 15 ? 'Akumulasi poin sedang — perlu dipantau' : 'Total poin masih dalam batas wajar'))"></span>
</div>
</div>
</template>
{{-- Empty state --}}
<template x-if="selected.length === 0">
<div class="flex items-center gap-2 p-3 rounded-xl border border-dashed border-gray-200 bg-gray-50/50">
<i class="fa-solid fa-circle-plus text-gray-300"></i>
<span class="text-xs text-gray-400">Gunakan pencarian di atas untuk memilih pelanggaran</span>
</div>
</template>
</div>
</div>
{{-- ===== BAGIAN 3: WAKTU & LOKASI ===== --}}
<div>
<div class="flex items-center gap-2.5 mb-4">
<div class="w-7 h-7 rounded-lg bg-teal-50 flex items-center justify-center">
<i class="fa-solid fa-clock text-teal-500 text-xs"></i>
</div>
<span class="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Waktu & Lokasi</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1.5">Tanggal <span class="text-red-500">*</span></label>
<input type="date" name="violation_date" value="{{ date('Y-m-d') }}" required
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">Waktu</label>
<input type="time" name="violation_time" value="{{ date('H:i') }}"
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">Lokasi</label>
<input type="text" name="location" placeholder="Depan kelas, Lapangan, dll"
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>
</div>
{{-- ===== BAGIAN 4: CATATAN ===== --}}
<div>
<div class="flex items-center gap-2.5 mb-4">
<div class="w-7 h-7 rounded-lg bg-violet-50 flex items-center justify-center">
<i class="fa-solid fa-note-sticky text-violet-500 text-xs"></i>
</div>
<span class="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Catatan</span>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1.5">Deskripsi / Catatan Pelanggaran</label>
<textarea name="description" rows="3" placeholder="Deskripsi lengkap pelanggaran yang terjadi..."
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 resize-none"></textarea>
</div>
</div>
{{-- ===== BAGIAN 5: BUKTI FOTO ===== --}}
<div>
<div class="flex items-center gap-2.5 mb-4">
<div class="w-7 h-7 rounded-lg bg-sky-50 flex items-center justify-center">
<i class="fa-solid fa-camera text-sky-500 text-xs"></i>
</div>
<span class="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Bukti Foto</span>
<span class="text-[10px] text-gray-400 font-medium">(maks. 5 foto)</span>
</div>
<input type="file" name="evidences[]" id="evidences" multiple accept="image/*" capture="environment" @change="handleFiles" class="hidden">
<div class="grid grid-cols-3 sm:grid-cols-5 gap-3 mb-3">
<template x-for="(file, index) in files" :key="index">
<div class="relative aspect-square rounded-xl overflow-hidden border border-gray-200 bg-gray-50 group shadow-sm">
<img :src="file.url" class="w-full h-full object-cover">
<button type="button" @click="removeFile(index)"
class="absolute top-1.5 right-1.5 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center text-xs hover:bg-red-600 transition opacity-0 group-hover:opacity-100 shadow-lg">
<i class="fa-solid fa-xmark"></i>
</button>
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent text-white text-[10px] px-2 py-1.5 truncate" x-text="file.name"></div>
</div>
</template>
{{-- Upload placeholder --}}
<template x-if="files.length < 5">
<div @click="document.getElementById('evidences').click()"
class="aspect-square rounded-xl border-2 border-dashed border-gray-200 hover:border-blue-300 bg-gray-50/50 flex flex-col items-center justify-center cursor-pointer transition hover:bg-blue-50/30 group">
<i class="fa-solid fa-camera text-gray-300 text-2xl mb-1 group-hover:text-blue-400 transition"></i>
<span class="text-xs text-gray-400 group-hover:text-blue-500 transition font-medium">Tambah Foto</span>
</div>
</template>
</div>
<div class="flex items-center gap-2">
<button type="button" @click="document.getElementById('evidences').click()"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-xl hover:bg-gray-50 transition shadow-sm">
<i class="fa-solid fa-image text-gray-400"></i>
Pilih File
</button>
<button type="button" @click="captureCamera"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-xl hover:bg-gray-50 transition shadow-sm">
<i class="fa-solid fa-camera text-gray-400"></i>
Ambil Foto
</button>
</div>
@error('evidences.*') <p class="mt-1.5 text-xs text-red-500 flex items-center gap-1"><i class="fa-solid fa-circle-exclamation"></i> {{ $message }}</p> @enderror
</div>
</div>
{{-- Footer --}}
<div class="px-6 py-4 bg-gray-50/80 border-t border-gray-100 flex items-center justify-end gap-3">
<a href="{{ route('violations.index') }}"
class="px-5 py-2.5 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-xl hover:bg-gray-50 transition inline-flex items-center gap-2">
<i class="fa-solid fa-arrow-left text-xs"></i>
Batal
</a>
<button type="submit"
class="px-6 py-2.5 text-sm font-semibold text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm inline-flex items-center gap-2">
<i class="fa-solid fa-floppy-disk text-xs"></i>
Simpan Pelanggaran
</button>
</div>
</form>
@@ -211,40 +339,47 @@
@push('scripts')
<script>
function typeSearch(groups) {
function typeMultiSearch(groups) {
return {
q: '', open: false,
selectedId: '', selectedName: '',
selectedPoints: 0, selectedSanction: '',
selColor: '#6b7280',
selected: [],
groups: groups,
get filtered() {
if (!this.q || this.q.length < 1) return this.groups;
const q = this.q.toLowerCase();
const query = this.q.toLowerCase();
return this.groups.map(g => ({
...g,
types: g.types.filter(t =>
t.name.toLowerCase().includes(q) ||
(t.sanction && t.sanction.toLowerCase().includes(q))
t.name.toLowerCase().includes(query) ||
(t.sanction && t.sanction.toLowerCase().includes(query))
)
})).filter(g => g.types.length > 0);
},
pick(t) {
this.selectedId = t.id;
this.selectedName = t.name;
this.selectedPoints = t.points;
this.selectedSanction = t.sanction || '';
this.selColor = this.groups.find(g => g.types.some(x => x.id === t.id))?.color || '#6b7280';
this.q = t.name;
get selectedIds() {
return this.selected.map(s => s.id);
},
get totalPoints() {
return this.selected.reduce((sum, s) => sum + s.points, 0);
},
addType(t) {
if (this.selectedIds.includes(t.id)) return;
const color = this.groups.find(g => g.types.some(x => x.id === t.id))?.color || '#6b7280';
this.selected.push({ ...t, color });
this.q = '';
this.open = false;
},
clear() {
this.selectedId = ''; this.selectedName = '';
this.selectedPoints = 0; this.selectedSanction = '';
this.selColor = '#6b7280'; this.q = ''; this.open = false;
removeType(i) {
this.selected.splice(i, 1);
},
clearAll() {
this.selected = [];
this.q = '';
}
};
}
@@ -261,27 +396,32 @@ function violationForm() {
fetch('{{ route("api.students.search") }}?q=' + encodeURIComponent(this.searchQuery))
.then(r => r.json()).then(data => { this.results = data; });
},
selectStudent(s) {
this.selectedStudentId = s.id; this.selectedStudentName = s.full_name;
this.selectedStudentInfo = s.nisn + ' - ' + (s.class_name || '');
this.searchQuery = s.full_name; this.results = [];
},
clearStudent() {
this.selectedStudentId = null; this.selectedStudentName = '';
this.selectedStudentInfo = ''; this.searchQuery = '';
},
handleFiles(e) {
const newFiles = Array.from(e.target.files);
const toAdd = newFiles.slice(0, 5 - this.files.length);
toAdd.forEach(file => {
const reader = new FileReader();
reader.onload = (ev) => {
this.files.push({ file, url: ev.target.result, name: file.name });
};
reader.readAsDataURL(file);
});
const newFiles = Array.from(e.target.files);
const toAdd = newFiles.slice(0, 5 - this.files.length);
toAdd.forEach(file => {
const reader = new FileReader();
reader.onload = (ev) => {
this.files.push({ file, url: ev.target.result, name: file.name });
};
reader.readAsDataURL(file);
});
},
removeFile(index) { this.files.splice(index, 1); },
captureCamera() {
const input = document.getElementById('evidences');
input.setAttribute('capture', 'environment'); input.click();
+12 -5
View File
@@ -10,11 +10,18 @@
<h1 class="text-2xl font-bold text-gray-900 tracking-tight">Data Pelanggaran</h1>
<p class="text-sm text-gray-500 mt-1">Riwayat pelanggaran siswa</p>
</div>
<a href="{{ route('violations.create') }}"
class="inline-flex items-center gap-2 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-plus text-xs"></i>
Input Pelanggaran
</a>
<div class="flex items-center gap-2">
<a href="{{ route('violations.create') }}"
class="inline-flex items-center gap-2 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-plus text-xs"></i>
Input Pelanggaran
</a>
<a href="{{ route('violations.export', request()->only(['date_from', 'date_to'])) }}"
class="inline-flex items-center gap-2 px-4 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>
<span class="hidden sm:inline">Export Excel</span>
</a>
</div>
</div>
{{-- Filter Card --}}
+99 -64
View File
@@ -439,79 +439,114 @@
</button>
</div>
<div class="divide-y divide-gray-50">
<div class="divide-y divide-gray-100">
@forelse($violation->handlings as $h)
<div class="p-4 sm:p-5 space-y-3">
{{-- Header --}}
<div class="flex items-start justify-between gap-3">
<div class="flex items-center gap-2.5 min-w-0">
<div class="w-7 h-7 rounded-lg bg-amber-50 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-clipboard-list text-amber-500 text-xs"></i>
</div>
<div class="min-w-0">
<p class="text-sm font-semibold text-gray-900">{{ $h->handling_type }}</p>
<p class="text-xs text-gray-400">{{ $h->handling_date->format('d M Y') }}
@if($h->location) • {{ $h->location }} @endif
</p>
</div>
</div>
<form action="{{ route('violations.handling.destroy', [$violation->id, $h->id]) }}" method="POST"
x-data x-on:submit.prevent="if(await window.confirmSwal({text:'Hapus catatan penanganan ini?'})) $el.submit()">
@csrf @method('DELETE')
<button type="submit"
class="text-gray-300 hover:text-red-500 transition flex-shrink-0">
<i class="fa-solid fa-trash-can text-xs"></i>
</button>
</form>
</div>
@php
$typeColors = [
'Teguran Lisan' => ['bg' => 'bg-blue-100', 'text' => 'text-blue-700', 'icon' => 'fa-comment'],
'Teguran Tertulis' => ['bg' => 'bg-blue-100', 'text' => 'text-blue-700', 'icon' => 'fa-file-pen'],
'Pembinaan BK' => ['bg' => 'bg-teal-100', 'text' => 'text-teal-700', 'icon' => 'fa-hand-holding-heart'],
'Panggilan Orang Tua' => ['bg' => 'bg-orange-100', 'text' => 'text-orange-700', 'icon' => 'fa-phone-volume'],
'Home Visit' => ['bg' => 'bg-violet-100', 'text' => 'text-violet-700', 'icon' => 'fa-house-chimney'],
'Skorsing' => ['bg' => 'bg-red-100', 'text' => 'text-red-700', 'icon' => 'fa-ban'],
'Tugas Sosial' => ['bg' => 'bg-emerald-100', 'text' => 'text-emerald-700', 'icon' => 'fa-handshake-angle'],
];
$tc = $typeColors[$h->handling_type] ?? ['bg' => 'bg-amber-100', 'text' => 'text-amber-700', 'icon' => 'fa-clipboard-list'];
@endphp
<div class="relative pl-4 pr-4 sm:pl-5 sm:pr-5 py-4 hover:bg-gray-50/50 transition">
{{-- Left accent bar --}}
<div class="absolute left-0 top-0 bottom-0 w-[3px] {{ $tc['bg'] }}"></div>
{{-- Description --}}
@if($h->description)
<div class="bg-gray-50 rounded-lg border border-gray-100 p-3">
<p class="text-xs text-gray-700 leading-relaxed whitespace-pre-line">{{ $h->description }}</p>
</div>
@endif
{{-- Participants --}}
@if($h->participants->count() > 0)
<div>
<p class="text-[11px] font-semibold text-gray-400 uppercase tracking-wider mb-2">Peserta</p>
<div class="space-y-1.5">
@foreach($h->participants as $p)
<div class="space-y-3">
{{-- Header row --}}
<div class="flex items-start justify-between gap-3">
<div class="flex items-center gap-2.5 min-w-0">
<div class="w-8 h-8 rounded-xl {{ $tc['bg'] }} flex items-center justify-center flex-shrink-0 shadow-sm">
<i class="fa-solid {{ $tc['icon'] }} {{ $tc['text'] }} text-xs"></i>
</div>
<div class="min-w-0">
<div class="flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-gray-100 flex items-center justify-center flex-shrink-0">
<span class="text-[9px] font-bold text-gray-500">{{ strtoupper(substr($p->user->name ?? '?', 0, 1)) }}</span>
</div>
<span class="text-xs text-gray-700 font-medium">{{ $p->user->name ?? '-' }}</span>
@if($p->role)
<span class="text-[10px] text-gray-400 bg-gray-50 px-1.5 py-0.5 rounded border border-gray-100">{{ $p->role }}</span>
@endif
<p class="text-sm font-bold text-gray-900">{{ $h->handling_type }}</p>
<span class="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-bold rounded-md {{ $tc['bg'] }} {{ $tc['text'] }}">
<i class="fa-regular fa-clock"></i>
{{ $h->handling_date->format('d/m') }}
</span>
</div>
@endforeach
@if($h->location || $h->creator)
<p class="text-[11px] text-gray-400 mt-0.5">
@if($h->location)<i class="fa-solid fa-location-dot mr-1"></i>{{ $h->location }}@endif
@if($h->location && $h->creator) <span class="text-gray-300">•</span> @endif
@if($h->creator)oleh {{ $h->creator->name }}@endif
</p>
@endif
</div>
</div>
<form action="{{ route('violations.handling.destroy', [$violation->id, $h->id]) }}" method="POST"
x-data x-on:submit.prevent="if(await window.confirmSwal({text:'Hapus catatan penanganan ini?'})) $el.submit()">
@csrf @method('DELETE')
<button type="submit"
class="w-7 h-7 rounded-lg flex items-center justify-center text-gray-300 hover:text-red-500 hover:bg-red-50 transition flex-shrink-0">
<i class="fa-solid fa-trash-can text-[11px]"></i>
</button>
</form>
</div>
@endif
{{-- Evidence --}}
@if($h->evidence)
<a href="{{ \Storage::url($h->evidence) }}" target="_blank"
class="inline-flex items-center gap-1.5 text-xs text-blue-600 bg-blue-50 px-2.5 py-1.5 rounded-lg border border-blue-100 hover:bg-blue-100 transition">
<i class="fa-solid fa-paperclip text-[10px]"></i>
Lihat Bukti
</a>
@endif
{{-- Description --}}
@if($h->description)
<div class="bg-gradient-to-br from-gray-50 to-gray-100/50 rounded-xl border border-gray-100 p-3.5 ml-[42px]">
<p class="text-xs text-gray-700 leading-relaxed whitespace-pre-line">{{ $h->description }}</p>
</div>
@endif
{{-- Creator --}}
@if($h->creator)
<p class="text-[10px] text-gray-400">Dicatat oleh {{ $h->creator->name }}</p>
@endif
{{-- Participants --}}
@if($h->participants->count() > 0)
<div class="ml-[42px]">
<div class="flex flex-wrap items-center gap-2">
<span class="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Yang Menangani:</span>
@foreach($h->participants as $p)
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 text-[11px] bg-white border border-gray-200 rounded-full shadow-sm">
<span class="w-4 h-4 rounded-full bg-gray-100 flex items-center justify-center flex-shrink-0">
<span class="text-[7px] font-bold text-gray-500">{{ strtoupper(substr($p->user->name ?? '?', 0, 1)) }}</span>
</span>
<span class="font-medium text-gray-700">{{ $p->user->name ?? '-' }}</span>
@if($p->role)
<span class="text-[10px] text-gray-400 bg-gray-50 px-1.5 py-0.5 rounded border border-gray-100">{{ $p->role }}</span>
@endif
</span>
@endforeach
</div>
</div>
@endif
{{-- Evidence + Metadata row --}}
<div class="flex items-center justify-between ml-[42px]">
<div class="flex items-center gap-2">
@if($h->evidence)
<a href="{{ \Storage::url($h->evidence) }}" target="_blank"
class="inline-flex items-center gap-1.5 text-[11px] font-semibold text-blue-600 bg-blue-50/80 px-3 py-1.5 rounded-lg border border-blue-100 hover:bg-blue-100 transition shadow-sm">
<i class="fa-solid fa-paperclip text-[10px]"></i>
Lihat Bukti
<i class="fa-solid fa-arrow-up-right-from-square text-[9px] text-blue-400"></i>
</a>
@endif
</div>
<p class="text-[10px] text-gray-300">
@if($h->created_at != $h->updated_at)
Diubah {{ $h->updated_at->diffForHumans() }}
@else
{{ $h->created_at->diffForHumans() }}
@endif
</p>
</div>
</div>
</div>
@empty
<div class="px-5 py-8 text-center">
<div class="w-10 h-10 rounded-full bg-gray-50 border border-gray-100 flex items-center justify-center mx-auto mb-2">
<i class="fa-solid fa-hand-holding-heart text-gray-300 text-sm"></i>
<div class="px-5 py-10 text-center">
<div class="w-12 h-12 rounded-2xl bg-gray-50 border border-gray-100 flex items-center justify-center mx-auto mb-3">
<i class="fa-solid fa-hand-holding-heart text-gray-300 text-lg"></i>
</div>
<p class="text-xs text-gray-500">Belum ada penanganan</p>
<p class="text-sm font-medium text-gray-500 mb-0.5">Belum ada penanganan</p>
<p class="text-xs text-gray-400">Tambahkan penanganan pertama untuk pelanggaran ini</p>
</div>
@endforelse
</div>
@@ -612,10 +647,10 @@
{{-- Participants --}}
<div>
<div class="flex items-center justify-between mb-1">
<label class="block text-sm font-medium text-gray-700">Peserta</label>
<label class="block text-sm font-medium text-gray-700">Yang Menangani</label>
<button type="button" @click="participants.push({user_id: '', role: ''})"
class="text-xs font-semibold text-blue-600 hover:text-blue-800 transition inline-flex items-center gap-1">
<i class="fa-solid fa-plus"></i> Tambah Peserta
<i class="fa-solid fa-plus"></i> Tambah Penanggungjawab
</button>
</div>
<div class="space-y-2">
+1
View File
@@ -74,6 +74,7 @@ Route::middleware('auth')->group(function () {
// Import
Route::get('/settings/import/template', [ImportController::class, 'downloadTemplate'])->name('settings.import.template');
Route::get('/settings/export/violation-types', [ImportController::class, 'exportViolationTypes'])->name('settings.export.violation-types');
Route::get('/export/violations', [\App\Http\Controllers\Admin\ViolationExportController::class, 'export'])->name('violations.export');
Route::post('/settings/import/violation-types', [ImportController::class, 'importViolationTypes'])->name('settings.import.violation-types');
Route::get('/settings/thresholds', [MasterDataController::class, 'thresholds'])->name('settings.thresholds');