Initial commit: Aplikasi Ketertiban Siswa
- Fitur: input pelanggaran, sistem poin, surat peringatan otomatis, SP progress - Realtime notifikasi via Laravel Reverb + Echo - Dashboard statistik, manajemen user, manajemen master data - Login 2 kolom premium, user dropdown, pengaturan akun - Tema biru kalem, logo sekolah dinamis, sidebar brand dinamis - Sinkronisasi siswa dari Database Kesiswaan via API
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Input Pelanggaran')
|
||||
|
||||
@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>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('violations.store') }}" method="POST" enctype="multipart/form-data" class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
@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">
|
||||
</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">
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function typeSearch(groups) {
|
||||
return {
|
||||
q: '', open: false,
|
||||
selectedId: '', selectedName: '',
|
||||
selectedPoints: 0, selectedSanction: '',
|
||||
selColor: '#6b7280',
|
||||
groups: groups,
|
||||
|
||||
get filtered() {
|
||||
if (!this.q || this.q.length < 1) return this.groups;
|
||||
const q = 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))
|
||||
)
|
||||
})).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;
|
||||
this.open = false;
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.selectedId = ''; this.selectedName = '';
|
||||
this.selectedPoints = 0; this.selectedSanction = '';
|
||||
this.selColor = '#6b7280'; this.q = ''; this.open = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function violationForm() {
|
||||
return {
|
||||
searchQuery: '', results: [],
|
||||
selectedStudentId: null, selectedStudentName: '', selectedStudentInfo: '',
|
||||
files: [],
|
||||
|
||||
searchStudents() {
|
||||
if (this.searchQuery.length < 2) { this.results = []; return; }
|
||||
if (this.selectedStudentId) return;
|
||||
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);
|
||||
});
|
||||
},
|
||||
removeFile(index) { this.files.splice(index, 1); },
|
||||
captureCamera() {
|
||||
const input = document.getElementById('evidences');
|
||||
input.setAttribute('capture', 'environment'); input.click();
|
||||
setTimeout(() => input.removeAttribute('capture'), 100);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
@@ -0,0 +1,110 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Data Pelanggaran')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Data Pelanggaran</h1>
|
||||
<p class="text-sm text-gray-500">Riwayat pelanggaran siswa</p>
|
||||
</div>
|
||||
<a href="{{ route('violations.create') }}" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition flex items-center space-x-2">
|
||||
<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="M12 4v16m8-8H4"></path></svg>
|
||||
<span>Input Pelanggaran</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- Filter --}}
|
||||
<form method="GET" class="bg-white p-4 rounded-xl shadow-sm border border-gray-200 mb-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<input type="text" name="search" placeholder="Cari NISN/Nama..." value="{{ request('search') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<select name="category_id" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Semua Kategori</option>
|
||||
@foreach($categories as $cat)
|
||||
<option value="{{ $cat->id }}" @selected(request('category_id') == $cat->id)>{{ $cat->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<input type="date" name="date_from" value="{{ request('date_from') }}" placeholder="Dari tanggal"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<input type="date" name="date_to" value="{{ request('date_to') }}" placeholder="Sampai tanggal"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||
<button type="submit" class="px-3 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700">Filter</button>
|
||||
@if(request()->anyFilled(['search','category_id','date_from','date_to']))
|
||||
<a href="{{ route('violations.index') }}" class="px-3 py-2 border border-gray-300 text-gray-600 text-sm rounded-lg hover:bg-gray-50">Reset</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{-- Table --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Tanggal</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Siswa</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Pelanggaran</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">Poin</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">Verifikasi</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Oleh</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
@forelse($violations as $v)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-700">{{ $v->violation_date->format('d/m/Y') }}</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $v->student->full_name ?? '-' }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $v->student->nisn ?? '' }} - {{ $v->student->class_name ?? '' }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-2.5 h-2.5 rounded-full flex-shrink-0" style="background-color: {{ $v->violationType?->category?->color ?? '#6b7280' }}"></span>
|
||||
<span class="text-sm text-gray-700">{{ $v->violationType->name ?? '-' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
+{{ $v->points }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
@if($v->is_verified)
|
||||
<span class="text-xs text-green-600 font-medium">✓ Terverifikasi</span>
|
||||
@else
|
||||
<span class="text-xs text-yellow-600 font-medium">Belum</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-500">{{ $v->recorder->name ?? '-' }}</td>
|
||||
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||||
<a href="{{ route('violations.show', $v->id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">Detail</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-4 py-12 text-center text-gray-500">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"></path></svg>
|
||||
<p>Belum ada pelanggaran tercatat</p>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if($violations->hasPages())
|
||||
<div class="px-4 py-3 border-t border-gray-100">
|
||||
{{ $violations->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,220 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Detail Pelanggaran')
|
||||
|
||||
@section('content')
|
||||
<div>
|
||||
{{-- Header --}}
|
||||
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<a href="{{ route('violations.index') }}" class="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition">
|
||||
<i class="fa-solid fa-arrow-left"></i>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Detail Pelanggaran</h1>
|
||||
<p class="text-sm text-gray-500 mt-0.5">Informasi lengkap pelanggaran siswa</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
@if(!$violation->is_verified)
|
||||
<form action="{{ route('violations.verify', $violation->id) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-xl hover:bg-green-700 transition shadow-sm">
|
||||
<i class="fa-solid fa-check-circle mr-1.5"></i>
|
||||
Verifikasi
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
<form action="{{ route('violations.destroy', $violation->id) }}" method="POST" x-data onsubmit="event.preventDefault(); Swal.fire({title: 'Konfirmasi', text: 'Hapus pelanggaran ini?', icon: 'warning', showCancelButton: true, confirmButtonColor: '#dc2626', cancelButtonColor: '#6b7280', confirmButtonText: 'Ya, hapus!', cancelButtonText: 'Batal'}).then(r => { if(r.isConfirmed) { .target.submit(); } })">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 text-sm font-medium text-blue-600 bg-white border border-blue-200 rounded-xl hover:bg-blue-50 transition">
|
||||
<i class="fa-solid fa-trash-can mr-1.5"></i>
|
||||
Hapus
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Content Grid --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{{-- Left: Info Siswa + Pelanggaran --}}
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
{{-- Card Siswa --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="p-5 flex items-center space-x-4">
|
||||
<div class="w-14 h-14 rounded-full flex items-center justify-center text-lg font-bold flex-shrink-0"
|
||||
style="background-color: {{ $violation->violationType?->category?->color ?? '#6b7280' }}20; color: {{ $violation->violationType?->category?->color ?? '#6b7280' }}">
|
||||
{{ strtoupper(substr($violation->student->full_name ?? '?', 0, 1)) }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-lg font-bold text-gray-900 truncate">{{ $violation->student->full_name ?? '-' }}</h2>
|
||||
<p class="text-sm text-gray-500">{{ $violation->student->nisn ?? '' }} <span class="mx-1.5">•</span> {{ $violation->student->class_name ?? '' }}</p>
|
||||
<p class="text-xs text-gray-400">{{ $violation->student->department_name ?? '' }}</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0 text-right">
|
||||
<p class="text-xs text-gray-500">Total Poin</p>
|
||||
<p class="text-2xl font-bold" style="color: {{ $violation->violationType?->category?->color ?? '#6b7280' }}">{{ $violation->student->total_points }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Card Detail Pelanggaran --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-5 py-4 border-b border-gray-100 flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-gray-900">Detail Pelanggaran</h3>
|
||||
<span class="inline-flex items-center px-2.5 py-1 text-xs font-medium rounded-full"
|
||||
style="background-color: {{ $violation->violationType?->category?->color ?? '#6b7280' }}15; color: {{ $violation->violationType?->category?->color ?? '#6b7280' }}">
|
||||
<span class="w-1.5 h-1.5 rounded-full mr-1.5" style="background-color: {{ $violation->violationType?->category?->color ?? '#6b7280' }}"></span>
|
||||
{{ $violation->violationType?->category?->name ?? '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
{{-- Info bars --}}
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between py-2 border-b border-gray-50 last:border-0">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-gavel text-blue-500 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-600">Jenis Pelanggaran</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900">{{ $violation->violationType->name ?? '-' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2 border-b border-gray-50">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-star text-blue-500 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-600">Poin</span>
|
||||
</div>
|
||||
<span class="text-lg font-bold text-blue-600">+{{ $violation->points }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2 border-b border-gray-50">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-scale-balanced text-blue-500 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-600">Sanksi</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900 text-right max-w-[50%]">{{ $violation->sanction ?? '-' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2 border-b border-gray-50">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-calendar text-blue-500 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-600">Tanggal</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900">{{ $violation->violation_date->format('d F Y') }} @if($violation->violation_time){{ \Carbon\Carbon::parse($violation->violation_time)->format('H:i') }} WIB @endif</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2 border-b border-gray-50">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-location-dot text-blue-500 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-600">Lokasi</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900">{{ $violation->location ?? '-' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-user-pen text-blue-500 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-600">Dicatat oleh</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900">{{ $violation->recorder->name ?? '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Deskripsi --}}
|
||||
@if($violation->description)
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-5 py-4 border-b border-gray-100">
|
||||
<h3 class="text-sm font-semibold text-gray-900">Deskripsi / Catatan</h3>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<p class="text-sm text-gray-700 leading-relaxed">{{ $violation->description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Right Sidebar: Status + Evidences --}}
|
||||
<div class="space-y-6">
|
||||
{{-- Status Card --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-5 py-4 border-b border-gray-100">
|
||||
<h3 class="text-sm font-semibold text-gray-900">Status</h3>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
{{-- Verifikasi --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Verifikasi</span>
|
||||
@if($violation->is_verified)
|
||||
<span class="inline-flex items-center px-2.5 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full">
|
||||
<i class="fa-solid fa-check-circle mr-1"></i>
|
||||
Terverifikasi
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex items-center px-2.5 py-1 text-xs font-medium bg-yellow-100 text-yellow-700 rounded-full">
|
||||
<i class="fa-solid fa-clock mr-1"></i>
|
||||
Belum
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($violation->is_verified)
|
||||
<div class="text-xs text-gray-400 flex items-center space-x-1">
|
||||
<i class="fa-solid fa-check text-green-500"></i>
|
||||
<span>oleh {{ $violation->verifier->name ?? '-' }} • {{ $violation->verified_at?->format('d/m/Y H:i') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Waktu input --}}
|
||||
<div class="pt-3 border-t border-gray-100">
|
||||
<p class="text-xs text-gray-400">Dibuat pada</p>
|
||||
<p class="text-sm text-gray-700">{{ $violation->created_at->format('d F Y H:i') }}</p>
|
||||
</div>
|
||||
@if($violation->updated_at != $violation->created_at)
|
||||
<div>
|
||||
<p class="text-xs text-gray-400">Terakhir diubah</p>
|
||||
<p class="text-sm text-gray-700">{{ $violation->updated_at->format('d F Y H:i') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Evidence Photos --}}
|
||||
@if($violation->evidences->count() > 0)
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-5 py-4 border-b border-gray-100 flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-gray-900">Bukti Foto</h3>
|
||||
<span class="text-xs text-gray-400">{{ $violation->evidences->count() }} file</span>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
@foreach($violation->evidences as $evidence)
|
||||
<a href="{{ $evidence->url }}" target="_blank"
|
||||
class="group relative block aspect-square rounded-xl overflow-hidden border border-gray-200 bg-gray-50 transition-all">
|
||||
<img src="{{ $evidence->url }}" class="w-full h-full object-cover scale-100 transition-transform duration-300" alt="{{ $evidence->original_name }}">
|
||||
<div class="absolute inset-0 bg-black/0 bg-black/0 transition-colors"></div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user