feat: penanganan pelanggaran (handling system)
- Migration: handling_status, handled_at, handled_by ke violations - Migration: tabel violation_handlings, handling_participants - Model: ViolationHandling, HandlingParticipant + relasi - Routes: storeHandling, resolveHandling, destroyHandling, calendar data - ViolationController: CRUD penanganan + filter status handling - DashboardController: data handling stats + calendar endpoint - Dashboard view: kalender pelanggaran interaktif (Alpine.js) - Violations index: filter status penanganan + kolom status - Violations show: stat card penanganan + timeline + modal tambah - Menambahkan handling_status filter di index
This commit is contained in:
@@ -10,6 +10,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
|
||||
class ImportController extends Controller
|
||||
{
|
||||
@@ -67,6 +68,68 @@ class ImportController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function exportViolationTypes()
|
||||
{
|
||||
$types = ViolationType::with('category')->orderBy('category_id')->orderBy('name')->get();
|
||||
|
||||
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('Jenis Pelanggaran');
|
||||
|
||||
// Header styling
|
||||
$headerStyle = [
|
||||
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
|
||||
'fill' => [
|
||||
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => '2563EB'],
|
||||
],
|
||||
'borders' => [
|
||||
'allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'BFDBFE']],
|
||||
],
|
||||
];
|
||||
|
||||
// Headers
|
||||
$headers = ['No', 'Kategori', 'Nama Pelanggaran', 'Poin', 'Sanksi Default', 'Deskripsi', 'Status'];
|
||||
foreach (range('A', 'G') as $i => $col) {
|
||||
$sheet->setCellValue($col . '1', $headers[$i]);
|
||||
}
|
||||
$sheet->getStyle('A1:G1')->applyFromArray($headerStyle);
|
||||
|
||||
// Data
|
||||
$row = 2;
|
||||
foreach ($types as $i => $type) {
|
||||
$sheet->setCellValue('A' . $row, $i + 1);
|
||||
$sheet->setCellValue('B' . $row, $type->category?->name ?? '-');
|
||||
$sheet->setCellValue('C' . $row, $type->name);
|
||||
$sheet->setCellValue('D' . $row, $type->points);
|
||||
$sheet->setCellValue('E' . $row, $type->default_sanction ?? '');
|
||||
$sheet->setCellValue('F' . $row, $type->description ?? '');
|
||||
$sheet->setCellValue('G' . $row, $type->is_active ? 'Aktif' : 'Nonaktif');
|
||||
|
||||
// Zebra striping
|
||||
if ($i % 2 === 0) {
|
||||
$sheet->getStyle('A' . $row . ':G' . $row)
|
||||
->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
|
||||
->getStartColor()->setARGB('FFF0F5FF');
|
||||
}
|
||||
|
||||
$row++;
|
||||
}
|
||||
|
||||
// Autofit
|
||||
foreach (range('A', 'G') as $col) {
|
||||
$sheet->getColumnDimension($col)->setAutoSize(true);
|
||||
}
|
||||
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
|
||||
return response()->streamDownload(function () use ($writer) {
|
||||
$writer->save('php://output');
|
||||
}, 'jenis-pelanggaran.xlsx', [
|
||||
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
]);
|
||||
}
|
||||
|
||||
public function importViolationTypes(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\AppNotification;
|
||||
use App\Models\Student;
|
||||
use App\Models\Violation;
|
||||
use App\Models\SpLetter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
@@ -20,6 +21,9 @@ class DashboardController extends Controller
|
||||
'total_violations' => Violation::count(),
|
||||
'total_students' => Student::where('is_active', true)->count(),
|
||||
'active_sp' => SpLetter::where('status', 'draft')->count(),
|
||||
'unhandled_violations' => Violation::where('handling_status', 'unhandled')->count(),
|
||||
'in_progress_violations' => Violation::where('handling_status', 'in_progress')->count(),
|
||||
'resolved_violations' => Violation::where('handling_status', 'resolved')->count(),
|
||||
];
|
||||
|
||||
$recentViolations = Violation::with(['student', 'violationType.category', 'recorder'])
|
||||
@@ -49,12 +53,33 @@ class DashboardController extends Controller
|
||||
|
||||
$spThresholds = \App\Models\SpThreshold::where('is_active', true)->get();
|
||||
|
||||
// Data untuk calendar: pelanggaran per tanggal di bulan ini
|
||||
$calendarData = Violation::selectRaw('DATE(violation_date) as date, COUNT(*) as count')
|
||||
->whereMonth('violation_date', now()->month)
|
||||
->whereYear('violation_date', now()->year)
|
||||
->groupBy('date')
|
||||
->pluck('count', 'date');
|
||||
|
||||
return view('dashboard.index', compact(
|
||||
'stats', 'recentViolations', 'topStudents',
|
||||
'unreadNotifications', 'notificationCount', 'spThresholds'
|
||||
'unreadNotifications', 'notificationCount', 'spThresholds', 'calendarData'
|
||||
));
|
||||
}
|
||||
|
||||
public function getCalendarData(Request $request): JsonResponse
|
||||
{
|
||||
$year = $request->input('year', now()->year);
|
||||
$month = $request->input('month', now()->month);
|
||||
|
||||
$data = Violation::selectRaw('DATE(violation_date) as date, COUNT(*) as count')
|
||||
->whereYear('violation_date', $year)
|
||||
->whereMonth('violation_date', $month)
|
||||
->groupBy('date')
|
||||
->pluck('count', 'date');
|
||||
|
||||
return response()->json($data);
|
||||
}
|
||||
|
||||
public function markNotificationRead(Request $request, $id)
|
||||
{
|
||||
$notif = AppNotification::where('user_id', $request->user()->id)->findOrFail($id);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Student;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use App\Models\SpLetter;
|
||||
use App\Models\SpThreshold;
|
||||
use App\Models\Violation;
|
||||
@@ -88,4 +89,28 @@ class StudentReportController extends Controller
|
||||
'spThresholds', 'currentSpLevel', 'nextSpThreshold', 'activeSpLetters'
|
||||
));
|
||||
}
|
||||
|
||||
public function update(Request $request, Student $student): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'full_name' => ['required', 'string', 'max:255'],
|
||||
'nisn' => ['nullable', 'string', 'max:50'],
|
||||
'student_number' => ['nullable', 'string', 'max:50'],
|
||||
'gender' => ['nullable', 'in:L,P'],
|
||||
'place_of_birth' => ['nullable', 'string', 'max:255'],
|
||||
'date_of_birth' => ['nullable', 'date'],
|
||||
'address' => ['nullable', 'string', 'max:500'],
|
||||
'phone_number' => ['nullable', 'string', 'max:30'],
|
||||
'email' => ['nullable', 'string', 'email', 'max:255'],
|
||||
'class_name' => ['nullable', 'string', 'max:100'],
|
||||
'class_level' => ['nullable', 'string', 'max:20'],
|
||||
'department_code' => ['nullable', 'string', 'max:20'],
|
||||
'department_name' => ['nullable', 'string', 'max:100'],
|
||||
]);
|
||||
|
||||
$student->update($validated);
|
||||
|
||||
return redirect()->route('students.show', $student->id)
|
||||
->with('success', 'Data siswa berhasil diperbarui.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Student;
|
||||
use App\Models\Violation;
|
||||
use App\Models\ViolationHandling;
|
||||
use App\Models\ViolationType;
|
||||
use App\Models\AppNotification;
|
||||
use App\Models\HandlingParticipant;
|
||||
use App\Services\ViolationService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -52,6 +54,10 @@ class ViolationController extends Controller
|
||||
$query->whereDate('violation_date', '<=', $request->date_to);
|
||||
}
|
||||
|
||||
if ($request->filled('handling_status')) {
|
||||
$query->where('handling_status', $request->handling_status);
|
||||
}
|
||||
|
||||
$violations = $query->latest()->paginate(20);
|
||||
$categories = \App\Models\ViolationCategory::where('is_active', true)->get();
|
||||
|
||||
@@ -108,9 +114,87 @@ class ViolationController extends Controller
|
||||
|
||||
public function show(Violation $violation): View
|
||||
{
|
||||
$violation->load(['student', 'violationType.category', 'recorder', 'evidences']);
|
||||
$violation->load([
|
||||
'student', 'violationType.category', 'recorder', 'evidences',
|
||||
'handlings.participants.user', 'handlings.creator',
|
||||
]);
|
||||
|
||||
return view('violations.show', compact('violation'));
|
||||
$users = \App\Models\User::where('is_active', true)->orderBy('name')->get();
|
||||
|
||||
return view('violations.show', compact('violation', 'users'));
|
||||
}
|
||||
|
||||
public function storeHandling(Request $request, Violation $violation): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'handling_type' => ['required', 'string', 'max:50'],
|
||||
'handling_date' => ['required', 'date'],
|
||||
'description' => ['nullable', 'string', 'max:2000'],
|
||||
'location' => ['nullable', 'string', 'max:255'],
|
||||
'evidence' => ['nullable', 'file', 'mimes:jpeg,png,jpg,gif,webp,pdf,doc,docx', 'max:10240'],
|
||||
'participants' => ['nullable', 'array', 'max:20'],
|
||||
'participants.*.user_id' => ['required_with:participants', 'exists:users,id'],
|
||||
'participants.*.role' => ['nullable', 'string', 'max:50'],
|
||||
]);
|
||||
|
||||
$evidencePath = null;
|
||||
if ($request->hasFile('evidence')) {
|
||||
$evidencePath = $request->file('evidence')->store('handling-evidence', 'public');
|
||||
}
|
||||
|
||||
$handling = ViolationHandling::create([
|
||||
'violation_id' => $violation->id,
|
||||
'handling_type' => $validated['handling_type'],
|
||||
'handling_date' => $validated['handling_date'],
|
||||
'description' => $validated['description'] ?? null,
|
||||
'location' => $validated['location'] ?? null,
|
||||
'evidence' => $evidencePath,
|
||||
'created_by' => $request->user()->id,
|
||||
]);
|
||||
|
||||
// Add participants
|
||||
if (!empty($validated['participants'])) {
|
||||
foreach ($validated['participants'] as $p) {
|
||||
HandlingParticipant::create([
|
||||
'handling_id' => $handling->id,
|
||||
'user_id' => $p['user_id'],
|
||||
'role' => $p['role'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Update violation status
|
||||
if ($violation->isUnhandled()) {
|
||||
$violation->update(['handling_status' => 'in_progress']);
|
||||
}
|
||||
|
||||
return redirect()->route('violations.show', $violation->id)
|
||||
->with('success', 'Penanganan berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
public function resolveHandling(Request $request, Violation $violation): RedirectResponse
|
||||
{
|
||||
$violation->update([
|
||||
'handling_status' => 'resolved',
|
||||
'handled_at' => now(),
|
||||
'handled_by' => $request->user()->id,
|
||||
]);
|
||||
|
||||
return redirect()->route('violations.show', $violation->id)
|
||||
->with('success', 'Pelanggaran ditandai selesai ditangani.');
|
||||
}
|
||||
|
||||
public function destroyHandling(Violation $violation, ViolationHandling $handling): RedirectResponse
|
||||
{
|
||||
$handling->delete();
|
||||
|
||||
// If no more handlings, revert to unhandled
|
||||
if ($violation->handlings()->count() === 0) {
|
||||
$violation->update(['handling_status' => 'unhandled']);
|
||||
}
|
||||
|
||||
return redirect()->route('violations.show', $violation->id)
|
||||
->with('success', 'Catatan penanganan berhasil dihapus.');
|
||||
}
|
||||
|
||||
public function verify(Request $request, Violation $violation): RedirectResponse
|
||||
|
||||
Reference in New Issue
Block a user