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:
Bro
2026-07-21 22:43:13 +07:00
commit 1c35734f7b
116 changed files with 19664 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
@php
$schoolName = App\Models\Setting::getValue('school_name', 'Ketertiban');
$appName = App\Models\Setting::getValue('app_name', 'Aplikasi Ketertiban');
$logoPath = App\Models\Setting::getValue('school_logo');
$bgPath = App\Models\Setting::getValue('login_background');
@endphp
<title>Login - {{ $appName }}</title>
@vite(['resources/css/app.css'])
</head>
<body class="min-h-screen bg-slate-100 px-3 py-3 sm:px-6 sm:py-6 antialiased">
<div class="mx-auto grid min-h-[calc(100vh-1.5rem)] max-w-7xl overflow-hidden rounded-[24px] sm:rounded-3xl border border-slate-200 bg-white shadow-2xl lg:grid-cols-2 lg:min-h-[92vh]">
{{-- Left Side: Branding --}}
<div class="relative hidden overflow-hidden lg:flex lg:flex-col lg:justify-between">
@if($bgPath)
<img src="{{ asset('storage/' . $bgPath) }}" alt="Background" class="absolute inset-0 h-full w-full object-cover">
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-black/10"></div>
@else
<div class="absolute inset-0"
style="background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 50%, #3b82f6 100%);"></div>
<div class="absolute inset-0"
style="background-image: radial-gradient(rgba(255,255,255,0.06) 1px, transparent 1px); background-size: 24px 24px;"></div>
<div class="absolute inset-0 bg-gradient-to-t from-black/30 via-transparent to-transparent"></div>
@endif
<div class="relative z-10 p-10">
<div class="inline-flex h-16 w-16 items-center justify-center rounded-3xl bg-white/90 shadow-lg">
@if($logoPath)
<img src="{{ asset('storage/' . $logoPath) }}" alt="Logo" class="w-10 h-10 object-contain">
@else
<span class="text-xl font-extrabold tracking-tight text-blue-600">KT</span>
@endif
</div>
</div>
<div class="relative z-10 mx-10 mb-10 rounded-3xl border border-white/15 bg-white/10 p-6 backdrop-blur-sm">
<p class="text-sm font-semibold text-white">{{ $appName }}</p>
<p class="text-xs text-white/50 mt-1">{{ $schoolName }}</p>
<p class="mt-2 text-sm leading-6 text-white/70">
Sistem pencatatan pelanggaran siswa terintegrasi dengan manajemen poin, surat peringatan otomatis, dan notifikasi realtime.
</p>
</div>
</div>
{{-- Right Side: Form --}}
<div class="flex items-center justify-center bg-white p-4 sm:p-8 lg:p-8">
<div class="w-full max-w-sm">
<div class="mb-5 text-center lg:text-left">
<p class="text-sm font-semibold uppercase tracking-[0.2em] text-blue-600">{{ $appName }}</p>
<p class="text-xs text-slate-400 mt-0.5">{{ $schoolName }}</p>
<h2 class="mt-2 text-2xl sm:text-[28px] font-semibold text-slate-900">Selamat Datang</h2>
<p class="mt-2 text-sm leading-6 text-slate-500">
Masukkan kredensial Anda untuk mengelola data pelanggaran, memantau poin siswa, dan menerbitkan surat peringatan.
</p>
</div>
@if ($errors->any())
<div class="mb-4 rounded-2xl border border-blue-200 bg-red-50 px-4 py-3 text-sm text-red-700">
<div class="flex items-center gap-2">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{{ $errors->first() }}
</div>
</div>
@endif
<form method="POST" action="{{ route('login') }}" class="space-y-3">
@csrf
<div>
<label for="username" class="mb-1.5 block text-sm font-medium text-slate-700">Username</label>
<input
id="username"
name="username"
type="text"
value="{{ old('username') }}"
placeholder="Masukkan username"
required
autofocus
class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-2.5 text-sm text-slate-900 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-100 @error('username') border-red-300 @enderror"
>
</div>
<div>
<label for="password" class="mb-1.5 block text-sm font-medium text-slate-700">Password</label>
<div class="relative">
<input
id="password"
name="password"
type="password"
placeholder="Masukkan password"
required
class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-2.5 pr-12 text-sm text-slate-900 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-100"
>
<button
type="button"
id="toggle-password"
class="absolute inline-flex items-center justify-center text-slate-500 transition hover:text-blue-600"
style="top: 50%; right: 16px; transform: translateY(-50%); width: 20px; height: 20px;"
aria-label="Tampilkan password"
aria-pressed="false"
>
<i id="toggle-password-icon" class="fa-regular fa-eye" style="font-size: 15px; line-height: 1;"></i>
</button>
</div>
</div>
<label class="flex items-center gap-3 text-sm text-slate-600">
<input type="checkbox" name="remember" value="1"
class="rounded border-slate-300 text-blue-600 focus:ring-blue-500">
<span>Ingat Saya</span>
</label>
<button type="submit"
class="inline-flex w-full items-center justify-center rounded-2xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-blue-700">
Masuk
</button>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const passwordInput = document.getElementById('password');
const toggleButton = document.getElementById('toggle-password');
const toggleIcon = document.getElementById('toggle-password-icon');
if (!passwordInput || !toggleButton) return;
toggleButton.addEventListener('click', function () {
const isHidden = passwordInput.type === 'password';
passwordInput.type = isHidden ? 'text' : 'password';
toggleButton.setAttribute('aria-label', isHidden ? 'Sembunyikan password' : 'Tampilkan password');
toggleButton.setAttribute('aria-pressed', isHidden ? 'true' : 'false');
if (toggleIcon) {
toggleIcon.className = isHidden ? 'fa-regular fa-eye-slash text-base' : 'fa-regular fa-eye text-base';
}
});
});
</script>
</body>
</html>
@@ -0,0 +1,28 @@
@props(['href', 'icon', 'active' => false])
@php
$isActive = $active ?? request()->fullUrlIs($href) || request()->is(trim(parse_url($href, PHP_URL_PATH) ?? '', '/'));
$classes = $isActive
? 'bg-blue-50 text-blue-700 border-r-2 border-blue-500'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900';
$faIcons = [
'home' => 'fa-solid fa-house',
'plus-circle' => 'fa-solid fa-circle-plus',
'exclamation-triangle' => 'fa-solid fa-triangle-exclamation',
'users' => 'fa-solid fa-users',
'document-text' => 'fa-solid fa-file-lines',
'tag' => 'fa-solid fa-tag',
'list' => 'fa-solid fa-list',
'chart-bar' => 'fa-solid fa-chart-simple',
'refresh' => 'fa-solid fa-rotate',
'cog' => 'fa-solid fa-gear',
'users-cog' => 'fa-solid fa-user-gear',
];
$faClass = $faIcons[$icon] ?? 'fa-solid fa-circle';
@endphp
<a href="{{ $href }}" {{ $attributes->merge(['class' => 'group flex items-center px-3 py-2 text-sm font-medium rounded-md ' . $classes]) }}>
<i class="{{ $faClass }} mr-3 w-5 text-center text-sm flex-shrink-0"></i>
{{ $slot }}
</a>
+144
View File
@@ -0,0 +1,144 @@
@extends('layouts.app')
@section('title', 'Dashboard')
@section('content')
<div x-data="notifications()">
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
<p class="text-sm text-gray-500">Ringkasan pelanggaran siswa</p>
</div>
{{-- Stats Cards --}}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">Pelanggaran Hari Ini</p>
<p class="text-3xl font-bold text-blue-600 mt-1">{{ $stats['today_violations'] }}</p>
</div>
<div class="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
<i class="fa-solid fa-triangle-exclamation text-blue-600 text-xl"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">Total Pelanggaran</p>
<p class="text-3xl font-bold text-orange-600 mt-1">{{ $stats['total_violations'] }}</p>
</div>
<div class="w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center">
<i class="fa-solid fa-list-check text-orange-600 text-xl"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">Siswa Aktif</p>
<p class="text-3xl font-bold text-blue-600 mt-1">{{ $stats['total_students'] }}</p>
</div>
<div class="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
<i class="fa-solid fa-users text-blue-600 text-xl"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">SP Aktif (Draft)</p>
<p class="text-3xl font-bold text-yellow-600 mt-1">{{ $stats['active_sp'] }}</p>
</div>
<div class="w-12 h-12 bg-yellow-100 rounded-full flex items-center justify-center">
<i class="fa-solid fa-file-lines text-yellow-600 text-xl"></i>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
{{-- Recent Violations --}}
<div class="lg:col-span-2 bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-5 py-4 border-b border-gray-100">
<h2 class="text-lg font-semibold text-gray-900">Pelanggaran Terbaru</h2>
</div>
<div class="divide-y divide-gray-100">
@forelse($recentViolations as $v)
<a href="{{ route('violations.show', $v->id) }}" class="flex items-center justify-between px-5 py-3 hover:bg-gray-50 transition">
<div class="flex items-center space-x-3">
<span class="w-2.5 h-2.5 rounded-full flex-shrink-0" style="background-color: {{ $v->violationType?->category?->color ?? '#6b7280' }}"></span>
<div>
<p class="text-sm font-medium text-gray-900">{{ $v->student->full_name ?? '-' }}</p>
<p class="text-xs text-gray-500">{{ $v->violationType->name ?? '-' }} <span class="text-red-500 font-medium">+{{ $v->points }} poin</span></p>
</div>
</div>
<div class="text-right text-xs text-gray-400">
<p>{{ $v->violation_date->format('d/m/Y') }}</p>
<p>{{ $v->recorder->name ?? '-' }}</p>
</div>
</a>
@empty
<div class="px-5 py-8 text-center text-sm text-gray-500">Belum ada pelanggaran tercatat</div>
@endforelse
</div>
<div class="px-5 py-3 border-t border-gray-100">
<a href="{{ route('violations.index') }}" class="text-sm text-blue-600 hover:text-blue-800 font-medium">Lihat semua &rarr;</a>
</div>
</div>
{{-- Top Students + SP Alerts --}}
<div class="space-y-6">
{{-- Notifikasi Ambang SP --}}
@if($spThresholds->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">
<h2 class="text-lg font-semibold text-gray-900">Ambang SP</h2>
</div>
<div class="p-5 space-y-3">
@foreach($spThresholds as $threshold)
@php
$studentsAtRisk = \App\Models\Student::where('is_active', true)->get()->filter(function($s) use ($threshold) {
$pts = $s->total_points;
$maxOk = $threshold->max_points ?? 9999;
return $pts >= $threshold->min_points && $pts <= $maxOk;
});
@endphp
<div class="flex items-center justify-between p-3 rounded-lg border" style="border-color: {{ $threshold->color }}20; background-color: {{ $threshold->color }}08">
<div>
<p class="text-sm font-semibold" style="color: {{ $threshold->color }}">{{ $threshold->name }}</p>
<p class="text-xs text-gray-500">{{ $threshold->min_points }}-{{ $threshold->max_points ?? '~' }} poin</p>
</div>
<span class="text-lg font-bold" style="color: {{ $threshold->color }}">{{ $studentsAtRisk->count() }}</span>
</div>
@endforeach
</div>
</div>
@endif
{{-- Top 5 Siswa Poin Tertinggi --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-5 py-4 border-b border-gray-100">
<h2 class="text-lg font-semibold text-gray-900">Poin Tertinggi</h2>
</div>
<div class="divide-y divide-gray-100">
@forelse($topStudents as $student)
<div class="flex items-center justify-between px-5 py-3">
<div class="flex items-center space-x-3">
<span class="w-8 h-8 rounded-full bg-blue-100 text-red-700 flex items-center justify-center text-xs font-bold">{{ $loop->iteration }}</span>
<div>
<p class="text-sm font-medium text-gray-900">{{ $student->full_name }}</p>
<p class="text-xs text-gray-500">{{ $student->class_name }}</p>
</div>
</div>
<span class="text-sm font-bold text-blue-600">{{ $student->total_points }}</span>
</div>
@empty
<div class="px-5 py-8 text-center text-sm text-gray-500">Belum ada data</div>
@endforelse
</div>
</div>
</div>
</div>
</div>
@endsection
+260
View File
@@ -0,0 +1,260 @@
<!DOCTYPE html>
<html lang="id" class="h-full bg-gray-50">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
@php
$schoolName = App\Models\Setting::getValue('school_name', 'Ketertiban');
$appName = App\Models\Setting::getValue('app_name', 'Aplikasi Ketertiban');
$logoPath = App\Models\Setting::getValue('school_logo');
@endphp
<title>{{ $schoolName }} - @yield('title', $appName)</title>
<link rel="icon" type="image/png" href="{{ $logoPath ? asset('storage/' . $logoPath) : asset('favicon.ico') }}">
<link rel="shortcut icon" type="image/png" href="{{ $logoPath ? asset('storage/' . $logoPath) : asset('favicon.ico') }}">
@vite(['resources/css/app.css', 'resources/js/app.js'])
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
@stack('styles')
</head>
<body class="h-full antialiased">
<div x-data="{ sidebarOpen: false, showNotif: false, showUserMenu: false, notifications: [], notifCount: 0 }" class="min-h-screen flex">
{{-- Sidebar --}}
<aside :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'" class="fixed inset-y-0 left-0 z-50 w-64 bg-white border-r border-gray-200 lg:translate-x-0 lg:static lg:inset-auto transition-transform duration-200 ease-in-out flex flex-col">
<div class="flex items-start justify-between h-auto px-4 pt-4 pb-3 border-b border-gray-200">
<a href="{{ route('dashboard') }}" class="min-w-0">
<div class="flex items-center gap-2.5 mb-2">
<div class="w-8 h-8 rounded-xl bg-white border border-gray-200 flex items-center justify-center flex-shrink-0 shadow-sm overflow-hidden">
@if($logoPath)
<img src="{{ asset('storage/' . $logoPath) }}" alt="Logo" class="w-full h-full object-contain p-0.5">
@else
<div class="w-full h-full bg-gradient-to-br from-blue-500 to-sky-400 flex items-center justify-center">
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>
</div>
@endif
</div>
<div>
<p class="text-sm font-bold text-gray-800 leading-tight truncate max-w-[140px]">{{ $appName }}</p>
</div>
</div>
</a>
<button @click="sidebarOpen = false" class="lg:hidden text-gray-500 hover:text-gray-700 flex-shrink-0 ml-2 mt-0.5">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<nav class="mt-2 px-2 space-y-1">
<x-nav-item href="{{ route('dashboard') }}" icon="home" :active="request()->routeIs('dashboard')">Dashboard</x-nav-item>
<x-nav-item href="{{ route('violations.create') }}" icon="plus-circle" :active="request()->routeIs('violations.create')">Input Pelanggaran</x-nav-item>
<x-nav-item href="{{ route('violations.index') }}" icon="exclamation-triangle" :active="request()->routeIs('violations.index')">Data Pelanggaran</x-nav-item>
<x-nav-item href="{{ route('students.index') }}" icon="users" :active="request()->routeIs('students.index*')">Data Siswa</x-nav-item>
<x-nav-item href="{{ route('sp-letters.index') }}" icon="document-text" :active="request()->routeIs('sp-letters.*')">Surat Peringatan</x-nav-item>
<div class="pt-4 mt-4 border-t border-gray-200">
<p class="px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">Pengaturan</p>
</div>
<x-nav-item href="{{ route('settings.categories') }}" icon="tag" :active="request()->routeIs('settings.categories*')">Kategori Pelanggaran</x-nav-item>
<x-nav-item href="{{ route('settings.violation-types') }}" icon="list" :active="request()->routeIs('settings.violation-types*')">Jenis Pelanggaran</x-nav-item>
<x-nav-item href="{{ route('settings.thresholds') }}" icon="chart-bar" :active="request()->routeIs('settings.thresholds*')">Ambang SP</x-nav-item>
<x-nav-item href="{{ route('settings.sync') }}" icon="refresh" :active="request()->routeIs('settings.sync*')">Sinkronisasi</x-nav-item>
<x-nav-item href="{{ route('users.index') }}" icon="users-cog" :active="request()->routeIs('users.*')">Manajemen User</x-nav-item>
<x-nav-item href="{{ route('settings.reset') }}" icon="refresh" :active="request()->routeIs('settings.reset*')">Reset Aplikasi</x-nav-item>
<x-nav-item href="{{ route('settings.index') }}" icon="cog" :active="request()->routeIs('settings.index')">Pengaturan</x-nav-item>
</nav>
{{-- Footer --}}
<div class="mt-auto px-4 py-3 border-t border-gray-100">
<div class="text-center">
<p class="text-[10px] text-gray-400">Developed by</p>
<a href="https://noctkj.net/" target="_blank" rel="noopener noreferrer"
class="text-xs font-bold text-blue-500 hover:text-blue-700 transition block mt-0.5">
NOCTKJ.net
</a>
</div>
</div>
</aside>
{{-- Overlay for mobile --}}
<div @click="sidebarOpen = false" x-show="sidebarOpen" class="fixed inset-0 z-40 bg-gray-600 bg-opacity-50 lg:hidden" style="display: none;"></div>
{{-- Main Content --}}
<div class="flex-1 flex flex-col min-w-0">
{{-- Topbar --}}
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-4 lg:px-6">
<button @click="sidebarOpen = true" class="lg:hidden text-gray-500 hover:text-gray-700">
<i class="fa-solid fa-bars text-xl"></i>
</button>
<div class="flex-1"></div>
<div class="flex items-center space-x-4">
{{-- Notifications --}}
<div class="relative" @click.outside="showNotif = false">
<button @click="showNotif = !showNotif" class="relative p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg">
<i class="fa-solid fa-bell text-lg"></i>
<span x-text="notifCount" x-show="notifCount > 0" class="absolute top-0 right-0 inline-flex items-center justify-center px-1.5 py-0.5 text-xs font-bold leading-none text-white transform translate-x-1/2 -translate-y-1/2 bg-red-500 rounded-full"></span>
</button>
{{-- Dropdown --}}
<div x-show="showNotif" class="absolute right-0 mt-2 w-80 bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden z-50" style="display: none;">
<div class="px-4 py-3 border-b border-gray-100 flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-900">Notifikasi</h3>
<button @click="markAllRead" class="text-xs text-blue-600 hover:text-blue-800">Tandai semua dibaca</button>
</div>
<div class="max-h-64 overflow-y-auto">
<template x-for="notif in notifications" :key="notif.id">
<a :href="notif.action_url || '#'" class="block px-4 py-3 hover:bg-gray-50 border-b border-gray-100 last:border-0">
<div class="flex items-start space-x-3">
<div :class="'flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ' + (notif.color ? 'bg-'+notif.color+'-100' : 'bg-blue-100')">
<i class="fa-solid fa-triangle-exclamation"></i>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 truncate" x-text="notif.title"></p>
<p class="text-xs text-gray-500 truncate" x-text="notif.body"></p>
</div>
</div>
</a>
</template>
<div x-show="notifications.length === 0" class="px-4 py-8 text-center text-sm text-gray-500">
Tidak ada notifikasi
</div>
</div>
</div>
</div>
{{-- User Dropdown --}}
<div class="relative" @click.outside="showUserMenu = false">
<button @click="showUserMenu = !showUserMenu"
class="flex items-center gap-3 pl-3 pr-2 py-2 rounded-xl hover:bg-gray-50 transition group">
<div class="hidden sm:block text-right">
<p class="text-sm font-semibold text-gray-900 group-hover:text-blue-600 transition">{{ auth()->user()->name }}</p>
<p class="text-xs text-gray-400 capitalize">{{ auth()->user()->role }}</p>
</div>
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-blue-500 to-sky-400 flex items-center justify-center text-white text-sm font-bold shadow-sm">
{{ strtoupper(substr(auth()->user()->name, 0, 1)) }}
</div>
<svg class="w-3.5 h-3.5 text-gray-400 transition-transform duration-200" :class="{ 'rotate-180': showUserMenu }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
{{-- Dropdown --}}
<div x-show="showUserMenu"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute right-0 mt-2 w-56 bg-white rounded-2xl shadow-lg border border-gray-200 overflow-hidden z-50" style="display: none;">
<div class="px-4 py-3 border-b border-gray-100">
<p class="text-sm font-semibold text-gray-900">{{ auth()->user()->name }}</p>
<p class="text-xs text-gray-400 capitalize">{{ auth()->user()->role }}</p>
</div>
<div class="py-1">
<a href="{{ route('profile.index') }}"
class="flex items-center gap-3 px-4 py-2.5 text-sm text-gray-600 hover:text-blue-600 hover:bg-blue-50 transition">
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
Pengaturan Akun
</a>
<hr class="mx-3 my-1 border-gray-100">
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit"
class="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-gray-600 hover:text-blue-600 hover:bg-red-50 transition">
<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="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
</svg>
Logout
</button>
</form>
</div>
</div>
</div>
</div>
</header>
{{-- Page Content --}}
<main class="flex-1 p-4 lg:p-6">
@if (session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-sm text-green-700 flex items-center justify-between">
<span>{{ session('success') }}</span>
<button onclick="this.parentElement.remove()" class="text-green-500 hover:text-green-700">&times;</button>
</div>
@endif
@if (session('error'))
<div class="mb-4 p-4 bg-red-50 border border-blue-200 rounded-lg text-sm text-red-700 flex items-center justify-between">
<span>{{ session('error') }}</span>
<button onclick="this.parentElement.remove()" class="text-red-500 hover:text-red-700">&times;</button>
</div>
@endif
@yield('content')
</main>
</div>
</div>
@stack('scripts')
{{-- Reverb + Echo for realtime --}}
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('notifications', () => ({
init() {
this.fetchCount();
this.fetchNotifications();
this.connectReverb();
},
fetchCount() {
fetch('{{ route("notifications.unread-count") }}')
.then(r => r.json())
.then(d => { if (d.count !== undefined) this.notifCount = d.count; });
},
fetchNotifications() {
fetch('{{ route("notifications.recent") }}')
.then(r => r.json())
.then(d => { if (d.notifications) this.notifications = d.notifications; });
},
markAllRead() {
fetch('{{ route("notifications.read-all") }}', { method: 'POST', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content } });
this.notifications = [];
this.notifCount = 0;
},
connectReverb() {
if (typeof Echo === 'undefined') {
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY || '"' + REVERB_APP_KEY + '",
wsHost: import.meta.env.VITE_REVERB_HOST || '"' + REVERB_HOST + '",
wsPort: import.meta.env.VITE_REVERB_PORT ? parseInt(import.meta.env.VITE_REVERB_PORT) : 8080,
wssPort: 443,
forceTLS: false,
enabledTransports: ['ws', 'wss'],
});
}
if (typeof Echo !== 'undefined') {
Echo.channel('violations')
.listen('.violation.recorded', (e) => {
this.notifCount++;
this.notifications.unshift({
id: e.violation?.id || Date.now(),
title: 'Pelanggaran Baru: ' + (e.data?.student_name || ''),
body: (e.data?.violation_type || '') + ' (+' + (e.data?.points || 0) + ' poin)',
action_url: e.data?.url || '#',
color: (e.data?.category_color || '#ef4444').replace('#', ''),
});
if (this.notifications.length > 20) this.notifications.pop();
});
} else {
// Fallback: poll every 30s if Echo not loaded
setInterval(() => { this.fetchCount(); }, 30000);
}
}
}));
});
</script>
</body>
</html>
+130
View File
@@ -0,0 +1,130 @@
@extends('layouts.app')
@section('title', 'Pengaturan Akun')
@section('content')
<div>
<div class="mb-6">
<nav class="flex items-center space-x-2 text-sm text-gray-400 mb-2">
<span class="text-gray-700 font-medium">Pengaturan Akun</span>
</nav>
<h1 class="text-2xl font-bold text-gray-900 tracking-tight">Pengaturan Akun</h1>
<p class="text-sm text-gray-500 mt-1">Ubah nama pengguna dan kata sandi</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
{{-- Profile Info Sidebar --}}
<div class="lg:col-span-1">
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 text-center">
<div class="w-20 h-20 rounded-2xl bg-gradient-to-br from-blue-500 to-sky-400 flex items-center justify-center text-white text-3xl font-bold shadow-sm mx-auto mb-4">
{{ strtoupper(substr($user->name, 0, 1)) }}
</div>
<h2 class="text-lg font-semibold text-gray-900">{{ $user->name }}</h2>
<p class="text-sm text-gray-400 capitalize">{{ $user->role }}</p>
<div class="mt-4 pt-4 border-t border-gray-100">
<div class="text-left space-y-3">
<div>
<p class="text-[10px] font-semibold text-gray-400 uppercase tracking-widest">Username</p>
<p class="text-sm font-medium text-gray-900 mt-0.5">{{ $user->username }}</p>
</div>
<div>
<p class="text-[10px] font-semibold text-gray-400 uppercase tracking-widest">Email</p>
<p class="text-sm text-gray-700 mt-0.5">{{ $user->email ?? '—' }}</p>
</div>
<div>
<p class="text-[10px] font-semibold text-gray-400 uppercase tracking-widest">Bergabung</p>
<p class="text-sm text-gray-700 mt-0.5">{{ $user->created_at->format('d M Y') }}</p>
</div>
</div>
</div>
</div>
</div>
{{-- Form --}}
<div class="lg:col-span-2">
<form method="POST" action="{{ route('profile.update') }}" class="bg-white rounded-2xl shadow-sm border border-gray-200">
@csrf
{{-- Ubah Profil --}}
<div class="px-6 py-4 border-b border-gray-100">
<div class="flex items-center gap-2.5">
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
<h3 class="text-base font-semibold text-gray-900">Informasi Profil</h3>
</div>
</div>
<div class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Nama Lengkap</label>
<input type="text" name="name" value="{{ old('name', $user->name) }}" 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">
@error('name') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Username</label>
<input type="text" name="username" value="{{ old('username', $user->username) }}" 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">
@error('username') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Email</label>
<input type="email" name="email" value="{{ old('email', $user->email) }}"
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">
@error('email') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
</div>
</div>
{{-- Ubah Password --}}
<div class="px-6 py-4 border-t border-gray-100 border-b border-gray-100">
<div class="flex items-center gap-2.5">
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</div>
<div>
<h3 class="text-base font-semibold text-gray-900">Ganti Kata Sandi</h3>
<p class="text-xs text-gray-400">Kosongkan jika tidak ingin mengubah</p>
</div>
</div>
</div>
<div class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Kata Sandi Saat Ini</label>
<input type="password" name="current_password"
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">
@error('current_password') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Kata Sandi Baru</label>
<input type="password" name="password"
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">
@error('password') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Konfirmasi Kata Sandi</label>
<input type="password" name="password_confirmation"
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>
{{-- Submit --}}
<div class="px-6 py-4 border-t border-gray-100 bg-gray-50/50 rounded-b-2xl flex items-center justify-end">
<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 gap-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="M5 13l4 4L19 7"/>
</svg>
Simpan Perubahan
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@@ -0,0 +1,301 @@
@extends('layouts.app')
@section('title', 'Kategori Pelanggaran')
@section('content')
<div x-data="categoryManager()" class="">
{{-- Header --}}
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Kategori Pelanggaran</h1>
<p class="text-sm text-gray-500 mt-1">Kelompokkan jenis pelanggaran berdasarkan tingkat keparahan</p>
</div>
<button @click="openCreate()"
class="inline-flex items-center px-4 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition shadow-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Tambah Kategori
</button>
</div>
{{-- Cards Grid --}}
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
@forelse($categories as $cat)
@php
$typeCount = $cat->violationTypes()->count();
@endphp
<div class="group bg-white rounded-xl shadow-sm border border-gray-200 hover:shadow-md transition-all duration-200 @if(!$cat->is_active) opacity-60 @endif">
{{-- Color bar --}}
<div class="h-2 rounded-t-xl" style="background-color: {{ $cat->color }}"></div>
<div class="p-5">
{{-- Header row --}}
<div class="flex items-start justify-between mb-3">
<div class="flex items-center space-x-3">
<span class="w-4 h-4 rounded-full flex-shrink-0 ring-2 ring-offset-1" style="background-color: {{ $cat->color }}; --tw-ring-color: {{ $cat->color }}40"></span>
<div>
<h3 class="text-base font-semibold text-gray-900">{{ $cat->name }}</h3>
@if($cat->description)
<p class="text-xs text-gray-500 mt-0.5">{{ $cat->description }}</p>
@endif
</div>
</div>
@if(!$cat->is_active)
<span class="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-gray-100 text-gray-500 rounded-full">Nonaktif</span>
@endif
</div>
{{-- Stats row --}}
<div class="flex items-center space-x-4 text-sm text-gray-500 mb-4">
<div class="flex items-center space-x-1.5">
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
</svg>
<span>{{ $typeCount }} jenis</span>
</div>
<div class="flex items-center space-x-1.5">
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
</svg>
<span>Urutan #{{ $cat->sort_order }}</span>
</div>
</div>
{{-- Actions --}}
<div class="flex items-center justify-between pt-3 border-t border-gray-100">
<div class="flex space-x-2">
<button @click="openEdit({{ $cat->id }}, '{{ $cat->name }}', '{{ $cat->color }}', '{{ $cat->description }}', {{ $cat->sort_order }}, {{ json_encode(!$cat->is_active) }})"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-gray-600 bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-gray-900 transition">
<svg class="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
Edit
</button>
@if($cat->is_active)
<form action="{{ route('settings.categories.update', $cat->id) }}" method="POST" class="inline">
@csrf @method('PUT')
<input type="hidden" name="name" value="{{ $cat->name }}">
<input type="hidden" name="color" value="{{ $cat->color }}">
<input type="hidden" name="description" value="{{ $cat->description }}">
<input type="hidden" name="sort_order" value="{{ $cat->sort_order }}">
<input type="hidden" name="is_active" value="0">
<button type="submit"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-yellow-600 bg-yellow-50 border border-yellow-200 rounded-lg hover:bg-yellow-100 transition">
<svg class="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
</svg>
Nonaktifkan
</button>
</form>
@else
<form action="{{ route('settings.categories.update', $cat->id) }}" method="POST" class="inline">
@csrf @method('PUT')
<input type="hidden" name="name" value="{{ $cat->name }}">
<input type="hidden" name="color" value="{{ $cat->color }}">
<input type="hidden" name="description" value="{{ $cat->description }}">
<input type="hidden" name="sort_order" value="{{ $cat->sort_order }}">
<input type="hidden" name="is_active" value="1">
<button type="submit"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-green-600 bg-green-50 border border-green-200 rounded-lg hover:bg-green-100 transition">
<svg class="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Aktifkan
</button>
</form>
@endif
</div>
@if($typeCount === 0)
<form action="{{ route('settings.categories.destroy', $cat->id) }}" method="POST"
x-data x-on:submit.prevent="if(await window.confirmSwal({text:'Hapus kategori ini?'})) $el.submit()"">
@csrf @method('DELETE')
<button type="submit"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-blue-600 bg-red-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition">
<svg class="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
Hapus
</button>
</form>
@endif
</div>
</div>
</div>
@empty
<div class="col-span-full">
<div class="text-center py-12 bg-white rounded-xl border border-gray-200">
<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="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
</svg>
<p class="text-sm text-gray-500">Belum ada kategori pelanggaran.</p>
<button @click="openCreate()" class="mt-3 text-sm font-medium text-blue-600 hover:text-blue-800">
+ Tambah kategori pertama
</button>
</div>
</div>
@endforelse
</div>
{{-- Modal Create/Edit --}}
<div x-show="modalOpen" class="fixed inset-0 z-50 overflow-y-auto" style="display: none;">
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
{{-- Overlay --}}
<div x-show="modalOpen" @click="modalOpen = false" class="fixed inset-0 bg-gray-500 bg-opacity-50 transition-opacity"></div>
{{-- Panel --}}
<div x-show="modalOpen" @click.away="modalOpen = false"
class="relative inline-block align-bottom bg-white rounded-2xl shadow-xl border border-gray-200 text-left overflow-hidden transform transition-all sm:align-middle sm:max-w-lg sm:w-full">
{{-- Header --}}
<div class="px-6 py-5 border-b border-gray-100 flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-lg flex items-center justify-center" :class="isEditing ? 'bg-blue-100' : 'bg-blue-100'">
<svg x-show="!isEditing" class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
<svg x-show="isEditing" class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900" x-text="isEditing ? 'Edit Kategori' : 'Tambah Kategori'"></h3>
<p class="text-sm text-gray-500" x-text="isEditing ? 'Ubah detail kategori pelanggaran' : 'Buat kategori pelanggaran baru'"></p>
</div>
</div>
<button @click="modalOpen = false" class="text-gray-400 hover:text-gray-600 transition">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
{{-- Form --}}
<form :action="isEditing ? `/settings/categories/${editId}` : '{{ route('settings.categories.store') }}'"
method="POST" class="p-6 space-y-5">
@csrf
<input type="hidden" name="_method" :value="isEditing ? 'PUT' : 'POST'">
{{-- Nama --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Nama Kategori</label>
<input type="text" x-model="formName" name="name" 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"
placeholder="Contoh: Ringan, Sedang, Berat">
</div>
{{-- Color Picker --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Warna</label>
<div class="flex items-center space-x-3">
<div class="relative">
<input type="color" x-model="formColor" name="color"
class="w-12 h-12 rounded-xl border border-gray-300 cursor-pointer p-0.5">
</div>
<input type="text" x-model="formColor" name="color"
class="flex-1 px-4 py-2.5 border border-gray-300 rounded-xl text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition"
placeholder="#22c55e">
<div class="flex-shrink-0 w-12 h-12 rounded-xl border border-gray-200"
:style="{ backgroundColor: formColor }"></div>
</div>
</div>
{{-- Sort Order --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Urutan Tampil</label>
<input type="number" x-model="formSort" name="sort_order" min="0"
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"
placeholder="0">
</div>
{{-- Description --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Deskripsi <span class="text-gray-400 font-normal">(opsional)</span></label>
<textarea x-model="formDesc" name="description" rows="2"
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"
placeholder="Penjelasan singkat tentang kategori ini"></textarea>
</div>
{{-- Status toggle for edit --}}
<div x-show="isEditing" class="flex items-center justify-between p-4 bg-gray-50 rounded-xl border border-gray-100">
<div>
<p class="text-sm font-medium text-gray-900">Status Aktif</p>
<p class="text-xs text-gray-500">Nonaktifkan untuk menyembunyikan kategori ini</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="formActive" name="is_active" value="1" class="sr-only peer">
<div class="w-11 h-6 bg-gray-200 rounded-full peer peer-checked:after:translate-x-full peer-checked:bg-blue-600 after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all"></div>
</label>
</div>
{{-- Preview --}}
<div class="p-4 bg-gray-50 rounded-xl border border-gray-100">
<p class="text-xs font-medium text-gray-500 mb-2">Pratinjau</p>
<div class="flex items-center space-x-2 p-2.5 bg-white rounded-lg border border-gray-200">
<span class="w-3 h-3 rounded-full flex-shrink-0" :style="{ backgroundColor: formColor }"></span>
<span class="text-sm font-medium text-gray-900" x-text="formName || 'Nama Kategori'"></span>
<span class="inline-flex items-center px-2 py-0.5 text-xs rounded-full font-medium"
:style="{
backgroundColor: formColor + '20',
color: formColor
}"
x-text="formDesc || 'Deskripsi'"></span>
</div>
</div>
{{-- Buttons --}}
<div class="flex justify-end space-x-3 pt-2">
<button type="button" @click="modalOpen = false"
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
</button>
<button type="submit"
class="px-5 py-2.5 text-sm font-medium text-white rounded-xl transition shadow-sm"
:class="isEditing ? 'bg-blue-600 hover:bg-blue-700' : 'bg-blue-600 hover:bg-blue-700'"
x-text="isEditing ? 'Simpan Perubahan' : 'Tambah Kategori'">
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
function categoryManager() {
return {
modalOpen: false,
isEditing: false,
editId: null,
formName: '',
formColor: '#22c55e',
formDesc: '',
formSort: 0,
formActive: true,
openCreate() {
this.isEditing = false;
this.editId = null;
this.formName = '';
this.formColor = '#22c55e';
this.formDesc = '';
this.formSort = 0;
this.formActive = true;
this.modalOpen = true;
},
openEdit(id, name, color, desc, sort, inactive) {
this.isEditing = true;
this.editId = id;
this.formName = name;
this.formColor = color;
this.formDesc = desc || '';
this.formSort = sort;
this.formActive = !inactive;
this.modalOpen = true;
}
};
}
</script>
@endpush
@endsection
+86
View File
@@ -0,0 +1,86 @@
@extends('layouts.app')
@section('title', 'Pengaturan')
@section('content')
<div>
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">Pengaturan Sekolah</h1>
<p class="text-sm text-gray-500">Informasi sekolah untuk kop surat</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<form action="{{ route('settings.update') }}" method="POST" enctype="multipart/form-data" class="p-6 space-y-4">
@csrf
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Nama Aplikasi</label>
<input type="text" name="app_name" value="{{ old('app_name', $settings->get('app_name')?->value ?? 'Aplikasi Ketertiban') }}" required
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">
<p class="text-xs text-gray-400 mt-1">Nama yang tampil di sidebar, login page, dan tab browser.</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Nama Sekolah</label>
<input type="text" name="school_name" value="{{ old('school_name', $settings->get('school_name')?->value ?? '') }}" required
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>
<label class="block text-sm font-medium text-gray-700 mb-1">Alamat Sekolah</label>
<textarea name="school_address" rows="2" 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">{{ old('school_address', $settings->get('school_address')?->value ?? '') }}</textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">No. Telepon</label>
<input type="text" name="school_phone" value="{{ old('school_phone', $settings->get('school_phone')?->value ?? '') }}"
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>
<label class="block text-sm font-medium text-gray-700 mb-1">Logo Sekolah</label>
@if($settings->get('school_logo')?->value)
<div class="mb-2">
<img src="{{ asset('storage/' . $settings->get('school_logo')->value) }}" class="h-16 object-contain">
</div>
@endif
<input type="file" name="school_logo" accept="image/*"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm file:mr-3 file:py-1 file:px-3 file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
</div>
<hr class="border-gray-200">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Background Halaman Login</label>
@if($settings->get('login_background')?->value)
<div class="mb-2">
<img src="{{ asset('storage/' . $settings->get('login_background')->value) }}" class="h-32 w-full object-cover rounded-lg border border-gray-200">
</div>
@endif
<input type="file" name="login_background" accept="image/*"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm file:mr-3 file:py-1 file:px-3 file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
<p class="text-xs text-gray-400 mt-1.5">Dimensi ideal: <strong class="text-gray-500">1200 × 800 px</strong> atau <strong class="text-gray-500">3:2</strong> (landscape). Maksimal <strong class="text-gray-500">2 MB</strong>. Format JPG/PNG/WebP.</p>
</div>
<hr class="border-gray-200">
<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">Nama Kepala Sekolah</label>
<input type="text" name="kepala_sekolah_name" value="{{ old('kepala_sekolah_name', $settings->get('kepala_sekolah_name')?->value ?? '') }}"
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>
<label class="block text-sm font-medium text-gray-700 mb-1">NIP Kepala Sekolah</label>
<input type="text" name="kepala_sekolah_nip" value="{{ old('kepala_sekolah_nip', $settings->get('kepala_sekolah_nip')?->value ?? '') }}"
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>
<button type="submit" class="w-full px-4 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition">
Simpan Pengaturan
</button>
</form>
</div>
</div>
@endsection
+115
View File
@@ -0,0 +1,115 @@
@extends('layouts.app')
@section('title', 'Reset Aplikasi')
@section('content')
<div class="max-w-2xl mx-auto">
{{-- Header --}}
<div class="mb-6">
<div class="flex items-center space-x-3">
<div class="w-12 h-12 rounded-xl bg-red-100 flex items-center justify-center">
<i class="fa-solid fa-rotate-left text-red-600 text-xl"></i>
</div>
<div>
<h1 class="text-2xl font-bold text-gray-900">Reset Aplikasi</h1>
<p class="text-sm text-gray-500 mt-0.5">Kembalikan aplikasi ke kondisi awal pabrik</p>
</div>
</div>
</div>
{{-- Warning Card --}}
<div class="bg-red-50 border border-red-200 rounded-xl p-5 mb-6">
<div class="flex items-start space-x-3">
<i class="fa-solid fa-triangle-exclamation text-red-600 mt-0.5"></i>
<div>
<h3 class="text-sm font-semibold text-red-800">⚠️ Peringatan Keras!</h3>
<p class="text-sm text-red-700 mt-1">Semua data berikut akan <strong>DIHAPUS PERMANEN</strong>:</p>
<ul class="mt-2 space-y-1 text-sm text-red-700">
<li>✕ Pelanggaran & foto bukti</li>
<li>✕ Surat Peringatan & notifikasi</li>
<li>✕ Data siswa & kelas (hasil sinkron)</li>
<li>✕ Kategori, jenis pelanggaran & ambang SP</li>
<li>✕ User lain (selain admin utama)</li>
<li>✕ Pengaturan sekolah</li>
<li>✕ File foto yang sudah diupload</li>
</ul>
<p class="text-sm text-green-700 mt-3 font-medium">Yang <u>tetap</u> tersimpan:</p>
<ul class="mt-1 space-y-0.5 text-sm text-green-700">
<li>✓ Akun super admin (username: <strong>admin</strong>)</li>
</ul>
</div>
</div>
</div>
{{-- Stats Cards --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6">
<div class="px-5 py-4 border-b border-gray-100">
<h3 class="text-sm font-semibold text-gray-900">Data yang akan terhapus</h3>
</div>
<div class="p-5">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div class="p-4 bg-red-50 rounded-xl border border-red-100 text-center">
<p class="text-xs text-red-600 font-medium uppercase tracking-wider">Pelanggaran</p>
<p class="text-2xl font-bold text-red-700 mt-1">{{ $stats['violations'] }}</p>
</div>
<div class="p-4 bg-orange-50 rounded-xl border border-orange-100 text-center">
<p class="text-xs text-orange-600 font-medium uppercase tracking-wider">Foto</p>
<p class="text-2xl font-bold text-orange-700 mt-1">{{ $stats['evidences'] }}</p>
</div>
<div class="p-4 bg-yellow-50 rounded-xl border border-yellow-100 text-center">
<p class="text-xs text-yellow-600 font-medium uppercase tracking-wider">SP</p>
<p class="text-2xl font-bold text-yellow-700 mt-1">{{ $stats['sp_letters'] }}</p>
</div>
<div class="p-4 bg-red-50 rounded-xl border border-red-100 text-center">
<p class="text-xs text-red-600 font-medium uppercase tracking-wider">Notifikasi</p>
<p class="text-2xl font-bold text-red-700 mt-1">{{ $stats['notifications'] }}</p>
</div>
<div class="p-4 bg-purple-50 rounded-xl border border-purple-100 text-center">
<p class="text-xs text-purple-600 font-medium uppercase tracking-wider">Siswa</p>
<p class="text-2xl font-bold text-purple-700 mt-1">{{ $stats['students'] }}</p>
</div>
<div class="p-4 bg-indigo-50 rounded-xl border border-indigo-100 text-center">
<p class="text-xs text-indigo-600 font-medium uppercase tracking-wider">Kelas</p>
<p class="text-2xl font-bold text-indigo-700 mt-1">{{ $stats['classes'] }}</p>
</div>
<div class="p-4 bg-teal-50 rounded-xl border border-teal-100 text-center">
<p class="text-xs text-teal-600 font-medium uppercase tracking-wider">Settings</p>
<p class="text-2xl font-bold text-teal-700 mt-1">{{ $stats['settings'] }}</p>
</div>
<div class="p-4 bg-gray-50 rounded-xl border border-gray-100 text-center">
<p class="text-xs text-gray-600 font-medium uppercase tracking-wider">User Lain</p>
<p class="text-2xl font-bold text-gray-700 mt-1">{{ $stats['users_other'] }}</p>
</div>
</div>
</div>
</div>
{{-- Confirm Form --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<form action="{{ route('settings.reset.run') }}" method="POST" onsubmit="return confirm('⚠️ YAKIN INGIN RESET? Semua data kecuali akun admin akan hilang permanen!')">
@csrf
<div class="p-5 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">
<i class="fa-solid fa-lock mr-1.5 text-gray-400"></i>
Masukkan Password Admin untuk Konfirmasi
</label>
<input type="password" name="confirm_password" required
placeholder="Password akun admin saat ini"
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 class="px-5 py-4 bg-gray-50 border-t border-gray-100 rounded-b-xl flex justify-between items-center">
<a href="{{ route('dashboard') }}" class="text-sm text-gray-500 hover:text-gray-700 transition">
<i class="fa-solid fa-arrow-left mr-1"></i> Kembali
</a>
<button type="submit"
class="inline-flex items-center px-6 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm">
<i class="fa-solid fa-bomb mr-2"></i>
Reset Aplikasi
</button>
</div>
</form>
</div>
</div>
@endsection
+87
View File
@@ -0,0 +1,87 @@
@extends('layouts.app')
@section('title', 'Sinkronisasi Data Siswa')
@section('content')
<div>
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">Sinkronisasi Data Siswa</h1>
<p class="text-sm text-gray-500">Sinkronkan data siswa dari Database Kesiswaan</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<form action="{{ route('settings.sync.run') }}" method="POST" class="p-6 space-y-5">
@csrf
{{-- URL Kesiswaan --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">URL Database Kesiswaan</label>
<input type="url" name="base_url" value="{{ old('base_url', $baseUrl) }}" required
placeholder="http://database-kesiswaan.local"
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">
@error('base_url') <p class="mt-1 text-sm text-blue-600">{{ $message }}</p> @enderror
@if($baseUrl)
<p class="mt-1.5 text-xs text-green-600 flex items-center">
<svg class="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
URL tersimpan: {{ $baseUrl }}
</p>
@endif
</div>
{{-- Token --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Token Sinkronisasi</label>
<div class="relative">
<input type="password" name="token" value="{{ old('token', $hasToken ? '********' : '') }}"
placeholder="Masukkan token akses"
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 pr-24"
id="tokenInput"
@if($hasToken) readonly onfocus="this.removeAttribute('readonly'); this.value=''; this.type='password';" @endif>
@if($hasToken)
<button type="button" onclick="document.getElementById('tokenInput').removeAttribute('readonly'); document.getElementById('tokenInput').value=''; document.getElementById('tokenInput').focus();"
class="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1 text-xs font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition">
Ganti
</button>
@endif
</div>
@error('token') <p class="mt-1 text-sm text-blue-600">{{ $message }}</p> @enderror
@if($hasToken)
<p class="mt-1.5 text-xs text-green-600 flex items-center">
<svg class="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
Token sudah tersimpan. Klik <strong>"Ganti"</strong> untuk memperbarui.
</p>
@else
<p class="mt-1 text-xs text-gray-500">Token didapatkan dari menu Pengaturan → Sync Tokens di Database Kesiswaan.</p>
@endif
</div>
{{-- Info siswa --}}
@if($studentCount > 0)
<div class="p-4 bg-blue-50 border border-blue-200 rounded-xl">
<div class="flex items-center space-x-2">
<svg class="w-5 h-5 text-blue-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div class="text-sm text-blue-700">
<strong>{{ $studentCount }}</strong> siswa sudah tersinkron. Sinkronisasi ulang akan memperbarui data yang sudah ada.
</div>
</div>
</div>
@endif
{{-- Tombol --}}
<button type="submit"
class="w-full px-4 py-3 text-sm font-medium text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm flex items-center justify-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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<span>Jalankan Sinkronisasi</span>
</button>
</form>
</div>
</div>
@endsection
@@ -0,0 +1,54 @@
@extends('layouts.app')
@section('title', 'Ambang Surat Peringatan')
@section('content')
<div>
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">Ambang Surat Peringatan</h1>
<p class="text-sm text-gray-500">Atur batas poin untuk setiap tingkat Surat Peringatan</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<form action="{{ route('settings.thresholds.update') }}" method="POST" class="p-6 space-y-6">
@csrf @method('PUT')
@foreach($thresholds as $t)
<input type="hidden" name="thresholds[{{ $loop->index }}][id]" value="{{ $t->id }}">
<div class="p-4 rounded-lg border-2" style="border-color: {{ $t->color }}20; background-color: {{ $t->color }}08">
<div class="flex items-center space-x-2 mb-3">
<span class="w-3 h-3 rounded-full" style="background-color: {{ $t->color }}"></span>
<h3 class="text-lg font-semibold" style="color: {{ $t->color }}">{{ $t->name }}</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Nama</label>
<input type="text" name="thresholds[{{ $loop->index }}][name]" value="{{ $t->name }}"
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>
<label class="block text-xs font-medium text-gray-700 mb-1">Minimal Poin</label>
<input type="number" name="thresholds[{{ $loop->index }}][min_points]" value="{{ $t->min_points }}" min="0"
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>
<label class="block text-xs font-medium text-gray-700 mb-1">Maksimal Poin (kosongkan = tak terbatas)</label>
<input type="number" name="thresholds[{{ $loop->index }}][max_points]" value="{{ $t->max_points }}" min="0"
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>
<label class="block text-xs font-medium text-gray-700 mb-1">Deskripsi</label>
<input type="text" name="thresholds[{{ $loop->index }}][default_description]" value="{{ $t->default_description }}"
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>
</div>
@endforeach
<button type="submit" class="w-full px-4 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition">
Simpan Perubahan
</button>
</form>
</div>
</div>
@endsection
@@ -0,0 +1,361 @@
@extends('layouts.app')
@section('title', 'Jenis Pelanggaran')
@section('content')
<div x-data="violationTypeManager()">
{{-- Header --}}
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Jenis Pelanggaran</h1>
<p class="text-sm text-gray-500 mt-1">Daftar jenis pelanggaran beserta poin dan sanksi default</p>
</div>
<button @click="openCreate()"
class="inline-flex items-center px-4 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition shadow-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Tambah Jenis
</button>
</div>
{{-- Filter & Search Bar --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6">
<form method="GET" class="p-4">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="relative">
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<input type="text" name="search" value="{{ request('search') }}"
placeholder="Cari jenis pelanggaran..."
class="w-full pl-10 pr-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>
<select name="category_id"
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">
<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 class="flex space-x-2">
<button type="submit"
class="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition">
Filter
</button>
@if(request()->anyFilled(['search','category_id']))
<a href="{{ route('settings.violation-types') }}"
class="px-4 py-2.5 text-sm font-medium text-gray-600 bg-white border border-gray-300 rounded-xl hover:bg-gray-50 transition">
Reset
</a>
@endif
</div>
</div>
</form>
</div>
{{-- 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>
<tr class="bg-gray-50">
<th class="px-5 py-3.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Kategori</th>
<th class="px-5 py-3.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Nama Pelanggaran</th>
<th class="px-5 py-3.5 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">Poin</th>
<th class="px-5 py-3.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Sanksi Default</th>
<th class="px-5 py-3.5 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-5 py-3.5 text-right text-xs font-semibold text-gray-500 uppercase tracking-wider">Aksi</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($types as $type)
<tr class="hover:bg-gray-50 transition {{ !$type->is_active ? 'opacity-60' : '' }}">
{{-- Kategori --}}
<td class="px-5 py-4 whitespace-nowrap">
<div class="flex items-center space-x-2.5">
<span class="w-3 h-3 rounded-full flex-shrink-0" style="background-color: {{ $type->category?->color ?? '#6b7280' }}"></span>
<span class="text-sm font-medium" style="color: {{ $type->category?->color ?? '#6b7280' }}">
{{ $type->category?->name ?? '-' }}
</span>
</div>
</td>
{{-- Nama --}}
<td class="px-5 py-4">
<div class="text-sm font-medium text-gray-900 {{ !$type->is_active ? 'line-through' : '' }}">
{{ $type->name }}
</div>
</td>
{{-- Poin --}}
<td class="px-5 py-4 text-center whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-1 text-sm font-bold rounded-lg
@if($type->points >= 50) bg-blue-100 text-red-700
@elseif($type->points >= 15) bg-yellow-100 text-yellow-700
@else bg-green-100 text-green-700
@endif">
+{{ $type->points }}
</span>
</td>
{{-- Sanksi --}}
<td class="px-5 py-4">
<span class="text-sm text-gray-600 {{ !$type->is_active ? 'line-through' : '' }}">
{{ $type->default_sanction ?: '-' }}
</span>
</td>
{{-- Status --}}
<td class="px-5 py-4 text-center whitespace-nowrap">
@if($type->is_active)
<span class="inline-flex items-center px-2.5 py-0.5 text-xs font-medium bg-green-100 text-green-700 rounded-full">
<span class="w-1.5 h-1.5 bg-green-500 rounded-full mr-1.5"></span>
Aktif
</span>
@else
<span class="inline-flex items-center px-2.5 py-0.5 text-xs font-medium bg-gray-100 text-gray-500 rounded-full">
<span class="w-1.5 h-1.5 bg-gray-400 rounded-full mr-1.5"></span>
Nonaktif
</span>
@endif
</td>
{{-- Aksi --}}
<td class="px-5 py-4 text-right whitespace-nowrap">
<div class="flex items-center justify-end space-x-2">
<button @click="openEdit({{ $type->id }}, {{ $type->category_id }}, '{{ addslashes($type->name) }}', {{ $type->points }}, '{{ addslashes($type->default_sanction ?? '') }}', {{ $type->is_active ? 'true' : 'false' }})"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition">
<svg class="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
Edit
</button>
@if($type->is_active)
<form action="{{ route('settings.violation-types.update', $type->id) }}" method="POST" class="inline">
@csrf @method('PUT')
<input type="hidden" name="category_id" value="{{ $type->category_id }}">
<input type="hidden" name="name" value="{{ $type->name }}">
<input type="hidden" name="points" value="{{ $type->points }}">
<input type="hidden" name="default_sanction" value="{{ $type->default_sanction }}">
<input type="hidden" name="is_active" value="0">
<button type="submit"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-yellow-600 bg-yellow-50 border border-yellow-200 rounded-lg hover:bg-yellow-100 transition">
Nonaktifkan
</button>
</form>
@else
<form action="{{ route('settings.violation-types.update', $type->id) }}" method="POST" class="inline">
@csrf @method('PUT')
<input type="hidden" name="category_id" value="{{ $type->category_id }}">
<input type="hidden" name="name" value="{{ $type->name }}">
<input type="hidden" name="points" value="{{ $type->points }}">
<input type="hidden" name="default_sanction" value="{{ $type->default_sanction }}">
<input type="hidden" name="is_active" value="1">
<button type="submit"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-green-600 bg-green-50 border border-green-200 rounded-lg hover:bg-green-100 transition">
Aktifkan
</button>
</form>
@endif
<form action="{{ route('settings.violation-types.destroy', $type->id) }}" method="POST" class="inline"
x-data x-on:submit.prevent="if(await window.confirmSwal({text:'Hapus jenis pelanggaran ini?'})) $el.submit()"">
@csrf @method('DELETE')
<button type="submit"
class="inline-flex items-center px-3 py-1.5 text-xs font-medium text-blue-600 bg-red-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-5 py-16 text-center">
<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="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
</svg>
<p class="text-sm text-gray-500 mb-1">Tidak ada jenis pelanggaran ditemukan</p>
<p class="text-xs text-gray-400">Coba ubah filter atau tambah jenis pelanggaran baru</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- Pagination --}}
@if($types->hasPages())
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50">
{{ $types->appends(request()->query())->links() }}
</div>
@endif
{{-- Summary --}}
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50 flex items-center justify-between text-xs text-gray-500">
<span>Menampilkan {{ $types->firstItem() ?? 0 }}–{{ $types->lastItem() ?? 0 }} dari {{ $types->total() }} jenis</span>
<span class="font-medium">{{ $types->total() }} total</span>
</div>
</div>
{{-- Modal Tambah / Edit --}}
<div x-show="modalOpen" class="fixed inset-0 z-50 overflow-y-auto" style="display: none;">
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
<div x-show="modalOpen" @click="modalOpen = false" class="fixed inset-0 bg-gray-500 bg-opacity-50 transition-opacity"></div>
<div x-show="modalOpen" @click.away="modalOpen = false"
class="relative inline-block align-bottom bg-white rounded-2xl shadow-xl border border-gray-200 text-left overflow-hidden transform transition-all sm:align-middle sm:max-w-xl sm:w-full">
{{-- Header --}}
<div class="px-6 py-5 border-b border-gray-100 flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-lg flex items-center justify-center" :class="isEditing ? 'bg-blue-100' : 'bg-blue-100'">
<svg x-show="!isEditing" class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
<svg x-show="isEditing" class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900" x-text="isEditing ? 'Edit Jenis Pelanggaran' : 'Tambah Jenis Pelanggaran'"></h3>
<p class="text-sm text-gray-500" x-text="isEditing ? 'Ubah detail jenis pelanggaran' : 'Buat jenis pelanggaran baru'"></p>
</div>
</div>
<button @click="modalOpen = false" class="text-gray-400 hover:text-gray-600 transition">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
{{-- Form --}}
<form :action="isEditing ? `/settings/violation-types/${editId}` : '{{ route('settings.violation-types.store') }}'"
method="POST" class="p-6 space-y-5">
@csrf
<input type="hidden" name="_method" :value="isEditing ? 'PUT' : 'POST'">
{{-- Kategori --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Kategori <span class="text-red-500">*</span></label>
<select x-model="formCategory" @change="updateCategoryColor(formCategory)" name="category_id" 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">
<option value="">-- Pilih Kategori --</option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}">{{ $cat->name }}</option>
@endforeach
</select>
</div>
{{-- Nama --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Nama Pelanggaran <span class="text-red-500">*</span></label>
<input type="text" x-model="formName" name="name" 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"
placeholder="Contoh: Terlambat datang ke sekolah">
</div>
{{-- Poin + Sanksi --}}
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Poin <span class="text-red-500">*</span></label>
<input type="number" x-model="formPoints" name="points" required min="0" max="500"
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">Sanksi Default</label>
<input type="text" x-model="formSanction" name="default_sanction"
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"
placeholder="Contoh: Teguran lisan">
</div>
</div>
{{-- Status toggle (edit only) --}}
<div x-show="isEditing" class="flex items-center justify-between p-4 bg-gray-50 rounded-xl border border-gray-100">
<div>
<p class="text-sm font-medium text-gray-900">Status Aktif</p>
<p class="text-xs text-gray-500">Nonaktifkan untuk menyembunyikan jenis pelanggaran ini</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="formActive" name="is_active" value="1" class="sr-only peer">
<div class="w-11 h-6 bg-gray-200 rounded-full peer peer-checked:after:translate-x-full peer-checked:bg-blue-600 after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all"></div>
</label>
</div>
{{-- Preview --}}
<div class="p-4 bg-gray-50 rounded-xl border border-gray-100">
<p class="text-xs font-medium text-gray-500 mb-2">Pratinjau</p>
<div class="flex items-center justify-between p-3 bg-white rounded-lg border border-gray-200">
<div class="flex items-center space-x-2.5">
<span class="w-3 h-3 rounded-full flex-shrink-0"
:style="{ backgroundColor: categoryColor }"></span>
<span class="text-sm font-medium text-gray-900" x-text="formName || 'Nama Pelanggaran'"></span>
</div>
<span class="text-sm font-bold text-blue-600" x-text="'+' + (formPoints || 0)"></span>
</div>
<p class="text-xs text-gray-400 mt-1.5" x-text="formSanction ? 'Sanksi: ' + formSanction : ''"></p>
</div>
{{-- Buttons --}}
<div class="flex justify-end space-x-3 pt-2">
<button type="button" @click="modalOpen = false"
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
</button>
<button type="submit"
class="px-5 py-2.5 text-sm font-medium text-white rounded-xl transition shadow-sm"
:class="isEditing ? 'bg-blue-600 hover:bg-blue-700' : 'bg-blue-600 hover:bg-blue-700'"
x-text="isEditing ? 'Simpan Perubahan' : 'Tambah Jenis'">
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
function violationTypeManager() {
return {
modalOpen: false,
isEditing: false,
editId: null,
formCategory: '',
formName: '',
formPoints: 0,
formSanction: '',
formActive: true,
categoryColor: '#6b7280',
openCreate() {
this.isEditing = false;
this.editId = null;
this.formCategory = '';
this.formName = '';
this.formPoints = 0;
this.formSanction = '';
this.formActive = true;
this.categoryColor = '#6b7280';
this.modalOpen = true;
},
openEdit(id, catId, name, points, sanction, active) {
this.isEditing = true;
this.editId = id;
this.formCategory = catId;
this.formName = name;
this.formPoints = points;
this.formSanction = sanction;
this.formActive = active;
this.updateCategoryColor(catId);
this.modalOpen = true;
},
updateCategoryColor(catId) {
const colors = @json($categories->pluck('color', 'id'));
this.categoryColor = colors[catId] || '#6b7280';
}
};
}
</script>
@endpush
@endsection
@@ -0,0 +1,67 @@
@extends('layouts.app')
@section('title', 'Surat Peringatan')
@section('content')
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">Surat Peringatan</h1>
<p class="text-sm text-gray-500">Daftar surat peringatan siswa</p>
</div>
<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">No. Surat</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">Jenis SP</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">Status</th>
<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-right text-xs font-medium text-gray-500 uppercase">Aksi</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($letters as $sp)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 text-sm font-mono text-gray-700">{{ $sp->letter_number }}</td>
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{ $sp->student->full_name ?? '-' }}</td>
<td class="px-4 py-3">
<span class="inline-flex px-2 py-1 text-xs font-medium rounded-full" style="color: {{ $sp->spThreshold->color ?? '#ef4444' }}; background-color: {{ $sp->spThreshold->color ?? '#ef4444' }}20;">
{{ $sp->spThreshold->name ?? '-' }}
</span>
</td>
<td class="px-4 py-3 text-center text-sm text-gray-700">{{ $sp->total_points_at_time }}</td>
<td class="px-4 py-3 text-center">
@php
$statusColors = ['draft' => 'yellow', 'printed' => 'blue', 'signed' => 'green', 'delivered' => 'purple'];
$color = $statusColors[$sp->status] ?? 'gray';
@endphp
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full bg-{{ $color }}-100 text-{{ $color }}-800 capitalize">
{{ $sp->status }}
</span>
</td>
<td class="px-4 py-3 text-sm text-gray-500">{{ $sp->created_at->format('d/m/Y') }}</td>
<td class="px-4 py-3 text-right space-x-2">
<a href="{{ route('sp-letters.show', $sp->id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">Detail</a>
<a href="{{ route('sp-letters.print', $sp->id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium" target="_blank">Cetak</a>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-4 py-12 text-center text-gray-500">
<p>Belum ada surat peringatan yang digenerate</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($letters->hasPages())
<div class="px-4 py-3 border-t border-gray-100">
{{ $letters->links() }}
</div>
@endif
</div>
@endsection
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Cetak SP - {{ $spLetter->student->full_name }}</title>
<style>
@page { margin: 2cm; size: A4; }
body { font-family: 'Times New Roman', Times, serif; font-size: 12pt; line-height: 1.5; color: #000; }
.kop-surat { text-align: center; margin-bottom: 30px; }
.kop-surat h1 { font-size: 16pt; font-weight: bold; margin: 0; text-transform: uppercase; }
.kop-surat p { font-size: 11pt; margin: 2px 0; }
.kop-surat .line { border-top: 2px solid #000; margin-top: 8px; }
.kop-surat .sub-line { border-top: 1px solid #000; margin-top: 2px; }
.letter-number { text-align: center; margin: 25px 0; font-weight: bold; font-size: 13pt; }
.to-title { margin-bottom: 15px; }
.body-text { text-align: justify; margin-bottom: 20px; }
.violation-list { margin: 15px 0 15px 20px; }
.violation-list li { margin-bottom: 3px; }
.signature { margin-top: 50px; }
.signature .date { text-align: center; margin-bottom: 5px; }
.signature .title { text-align: center; margin-bottom: 60px; }
.signature .name { text-align: center; font-weight: bold; }
.signature .nip { text-align: center; font-size: 11pt; }
</style>
</head>
<body>
<div class="kop-surat">
<h1>{{ $school['name'] }}</h1>
<p>{{ $school['address'] }}</p>
<p>Telp: {{ $school['phone'] }}</p>
<div class="line"></div>
<div class="sub-line"></div>
</div>
<div class="letter-number">
<p>SURAT PERINGATAN</p>
<p>Nomor: {{ $spLetter->letter_number }}</p>
</div>
<div class="to-title">
<p>Kepada Yth.</p>
<p><strong>Saudara/i {{ $spLetter->student->full_name }}</strong></p>
<p>Kelas: {{ $spLetter->student->class_name ?? '-' }}</p>
<p>NISN: {{ $spLetter->student->nisn }}</p>
<p>di tempat</p>
</div>
<div class="body-text">
<p>Berdasarkan pencatatan pelanggaran yang telah dilakukan, dengan ini kami menyampaikan Surat Peringatan {{ $spLetter->spThreshold->name }} kepada Saudara/i dengan rincian sebagai berikut:</p>
</div>
<div class="body-text">
<p>Total akumulasi poin pelanggaran: <strong>{{ $spLetter->total_points_at_time }} poin</strong></p>
<p>Daftar pelanggaran:</p>
<ul class="violation-list">
@php $violations = is_array($spLetter->violations_included) ? $spLetter->violations_included : json_decode($spLetter->violations_included, true); @endphp
@if($violations)
@foreach($violations as $v)
<li>{{ $v['violation_date'] ?? '-' }} - {{ $v['description'] ?? '-' }} ({{ $v['points'] ?? 0 }} poin)</li>
@endforeach
@endif
</ul>
</div>
<div class="body-text">
<p>Dengan adanya Surat Peringatan ini, Saudara/i diharapkan dapat memperbaiki sikap dan perilaku serta tidak mengulangi pelanggaran yang telah dilakukan. Apabila Saudara/i kembali melakukan pelanggaran, maka pihak sekolah akan memberikan sanksi yang lebih tegas.</p>
</div>
<div class="signature">
<div class="date">
<p>{{ \Carbon\Carbon::now()->locale('id')->isoFormat('D MMMM Y') }}</p>
</div>
<div class="title">
<p>Kepala {{ $school['name'] }},</p>
</div>
<div class="name">
<p><u><strong>{{ $school['kepala_sekolah'] }}</strong></u></p>
</div>
<div class="nip">
<p>NIP. {{ $school['kepala_sekolah_nip'] }}</p>
</div>
</div>
<script>
window.onload = function() { window.print(); };
</script>
</body>
</html>
+77
View File
@@ -0,0 +1,77 @@
@extends('layouts.app')
@section('title', 'Detail SP')
@section('content')
<div>
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Detail Surat Peringatan</h1>
<p class="text-sm text-gray-500">{{ $spLetter->letter_number }}</p>
</div>
<div class="flex space-x-2">
<a href="{{ route('sp-letters.index') }}" class="px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition">Kembali</a>
<a href="{{ route('sp-letters.print', $spLetter->id) }}" target="_blank" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition">Cetak</a>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">No. Surat</p>
<p class="text-sm font-mono font-medium text-gray-900 mt-1">{{ $spLetter->letter_number }}</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">Siswa</p>
<p class="text-sm font-medium text-gray-900 mt-1">{{ $spLetter->student->full_name ?? '-' }}</p>
<p class="text-xs text-gray-500">{{ $spLetter->student->nisn ?? '' }} - {{ $spLetter->student->class_name ?? '' }}</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">Jenis SP</p>
<p class="text-sm font-medium text-gray-900 mt-1">{{ $spLetter->spThreshold->name ?? '-' }}</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">Total Poin Saat Generate</p>
<p class="text-lg font-bold text-blue-600 mt-1">{{ $spLetter->total_points_at_time }} poin</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">Status</p>
<p class="text-sm font-medium text-gray-900 mt-1 capitalize">{{ $spLetter->status }}</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">Dibuat Oleh</p>
<p class="text-sm text-gray-900 mt-1">{{ $spLetter->generator->name ?? 'Sistem' }}</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase tracking-wider">Dibuat Pada</p>
<p class="text-sm text-gray-900 mt-1">{{ $spLetter->created_at->format('d F Y H:i') }}</p>
</div>
@if($spLetter->violations_included)
<div class="border-t border-gray-100 pt-6">
<p class="text-xs text-gray-500 uppercase tracking-wider mb-3">Daftar Pelanggaran Terkait</p>
<div class="bg-gray-50 p-3 rounded-lg">
@php $violations = is_array($spLetter->violations_included) ? $spLetter->violations_included : json_decode($spLetter->violations_included, true); @endphp
@if($violations)
<ul class="space-y-1">
@foreach($violations as $v)
<li class="text-sm text-gray-700">• {{ $v['violation_date'] ?? '-' }} — {{ $v['description'] ?? '(deskripsi)' }} <span class="text-red-500 font-medium">(+{{ $v['points'] ?? 0 }})</span></li>
@endforeach
</ul>
@else
<p class="text-sm text-gray-500">Data tidak tersedia</p>
@endif
</div>
</div>
@endif
@if($school['kepala_sekolah'])
<div class="border-t border-gray-100 pt-6">
<div class="text-right">
<p class="text-sm text-gray-900">{{ $school['kepala_sekolah'] }}</p>
<p class="text-xs text-gray-500">Kepala {{ $school['name'] }}</p>
</div>
</div>
@endif
</div>
</div>
@endsection
+173
View File
@@ -0,0 +1,173 @@
@extends('layouts.app')
@section('title', 'Data Siswa')
@section('content')
<div>
{{-- Header --}}
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div>
<h1 class="text-2xl font-bold text-gray-900">Data Siswa</h1>
<p class="text-sm text-gray-500 mt-1">Daftar siswa aktif — sinkron dari Database Kesiswaan</p>
</div>
<div class="flex items-center space-x-2 text-sm">
<span class="text-gray-500">{{ $students->total() }} siswa</span>
@if(request()->anyFilled(['search','class_level','class_name','department']))
<span class="inline-flex items-center px-2.5 py-1 text-xs font-medium bg-blue-100 text-red-700 rounded-full">
{{ $students->total() }} ditemukan
</span>
@endif
</div>
</div>
{{-- Filter Card --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6">
<form method="GET">
<div class="p-5 space-y-4">
{{-- Baris 1: Search --}}
<div>
<label class="block text-xs font-medium text-gray-500 mb-1.5">Pencarian</label>
<div class="relative">
<svg class="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<input type="text" name="search" value="{{ request('search') }}"
placeholder="Cari NISN, NIS, atau Nama siswa..."
class="w-full pl-10 pr-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>
{{-- Baris 2: Dropdown --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1.5">Tingkat Kelas</label>
<select name="class_level"
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 bg-white">
<option value="">Semua Tingkat</option>
@foreach($classLevels as $level)
<option value="{{ $level }}" @selected(request('class_level') == $level)>Kelas {{ $level }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1.5">Kelas</label>
<select name="class_name"
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 bg-white">
<option value="">Semua Kelas</option>
@foreach($classNames as $cn)
<option value="{{ $cn }}" @selected(request('class_name') == $cn)>{{ $cn }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1.5">Jurusan</label>
<select name="department"
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 bg-white">
<option value="">Semua Jurusan</option>
@foreach($departments as $code => $name)
<option value="{{ $code }}" @selected(request('department') == $code)>{{ $name }} ({{ $code }})</option>
@endforeach
</select>
</div>
<div class="flex items-end space-x-2">
<button type="submit"
class="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm flex items-center justify-center space-x-1.5">
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<span>Cari</span>
</button>
@if(request()->anyFilled(['search','class_level','class_name','department']))
<a href="{{ route('students.index') }}"
class="px-4 py-2.5 text-sm font-medium text-gray-600 bg-white border border-gray-300 rounded-xl hover:bg-gray-50 transition flex items-center space-x-1">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
<span>Reset</span>
</a>
@endif
</div>
</div>
</div>
</form>
</div>
{{-- 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.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">NISN</th>
<th class="px-4 py-3.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Nama</th>
<th class="px-4 py-3.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Kelas</th>
<th class="px-4 py-3.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Jurusan</th>
<th class="px-4 py-3.5 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">Pelanggaran</th>
<th class="px-4 py-3.5 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">Total Poin</th>
<th class="px-4 py-3.5 text-right text-xs font-semibold text-gray-500 uppercase tracking-wider">Aksi</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($students as $s)
@php $pts = $s->total_points; @endphp
<tr class="hover:bg-gray-50 transition">
<td class="px-4 py-3.5 whitespace-nowrap text-sm font-mono text-gray-700">{{ $s->nisn }}</td>
<td class="px-4 py-3.5">
<div class="text-sm font-medium text-gray-900">{{ $s->full_name }}</div>
</td>
<td class="px-4 py-3.5 whitespace-nowrap text-sm text-gray-700">{{ $s->class_name ?? '-' }}</td>
<td class="px-4 py-3.5 text-sm text-gray-500">{{ $s->department_code ?? '-' }}</td>
<td class="px-4 py-3.5 text-center text-sm text-gray-700">
<span class="inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 text-gray-600">
{{ $s->violations_count ?? 0 }}x
</span>
</td>
<td class="px-4 py-3.5 text-center">
@if($pts >= 100)
<span class="inline-flex px-3 py-1 text-xs font-bold rounded-full bg-blue-100 text-blue-800">{{ $pts }}</span>
@elseif($pts >= 50)
<span class="inline-flex px-3 py-1 text-xs font-bold rounded-full bg-yellow-100 text-yellow-800">{{ $pts }}</span>
@elseif($pts > 0)
<span class="inline-flex px-3 py-1 text-xs font-bold rounded-full bg-orange-100 text-orange-700">{{ $pts }}</span>
@else
<span class="inline-flex px-3 py-1 text-xs rounded-full bg-green-100 text-green-700">0</span>
@endif
</td>
<td class="px-4 py-3.5 text-right">
<a href="{{ route('students.show', $s->id) }}"
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-blue-600 bg-red-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition">
<svg class="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
Detail
</a>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-4 py-16 text-center">
<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="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zM6 10h.01M18 10h.01"/>
</svg>
<p class="text-sm text-gray-500 mb-1">Tidak ada siswa ditemukan</p>
<p class="text-xs text-gray-400">Coba ubah filter pencarian atau lakukan sinkronisasi data</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($students->hasPages())
<div class="px-4 py-3 border-t border-gray-100 bg-gray-50">
{{ $students->appends(request()->query())->links() }}
</div>
@endif
<div class="px-4 py-3 border-t border-gray-100 bg-gray-50 flex items-center justify-between text-xs text-gray-500">
<span>Menampilkan {{ $students->firstItem() ?? 0 }}–{{ $students->lastItem() ?? 0 }} dari {{ $students->total() }} siswa</span>
<span class="font-medium">{{ $students->total() }} total</span>
</div>
</div>
</div>
@endsection
+447
View File
@@ -0,0 +1,447 @@
@extends('layouts.app')
@section('title', $student->full_name)
@push('styles')
<style>
.violation-timeline::before {
content: '';
position: absolute;
left: 20px;
top: 8px;
bottom: 8px;
width: 2px;
background: #e5e7eb;
}
.sp-progress-bar {
background: linear-gradient(90deg, #dbeafe 0%, #93c5fd 40%, #60a5fa 70%, #3b82f6 100%);
}
.perspective-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.perspective-card:hover {
transform: translateY(-2px);
box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.08);
}
.glass-stat {
background: linear-gradient(135deg, rgba(255,255,255,0.9), rgba(255,255,255,0.98));
backdrop-filter: blur(8px);
}
</style>
@endpush
@section('content')
<div>
{{-- Header --}}
<div class="mb-6 flex items-center justify-between">
<div>
<nav class="flex items-center space-x-2 text-sm text-gray-400 mb-2">
<a href="{{ route('students.index') }}" class="hover:text-gray-600 transition">Data Siswa</a>
<span>/</span>
<span class="text-gray-700 font-medium truncate max-w-[200px]">{{ $student->full_name }}</span>
</nav>
<h1 class="text-2xl font-bold text-gray-900 tracking-tight">Profil Siswa</h1>
</div>
<a href="{{ route('students.index') }}"
class="inline-flex items-center px-4 py-2.5 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-xl hover:bg-gray-50 hover:border-gray-300 transition-all shadow-sm">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
Kembali
</a>
</div>
{{-- ===== HERO CARD ===== --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden mb-6 perspective-card">
<div class="bg-gradient-to-r from-blue-500 via-blue-400 to-sky-300 h-28 sm:h-36"></div>
<div class="relative px-6 pb-6">
{{-- Avatar --}}
<div class="flex flex-col sm:flex-row sm:items-end -mt-14 sm:-mt-20 mb-4 gap-4">
<div class="w-20 h-20 sm:w-24 sm:h-24 rounded-2xl bg-white shadow-lg border-2 border-white flex items-center justify-center flex-shrink-0">
@if($student->photo_path)
<img src="{{ Storage::url($student->photo_path) }}" alt="{{ $student->full_name }}"
class="w-full h-full rounded-2xl object-cover">
@else
<span class="text-3xl sm:text-4xl font-bold text-blue-600">{{ strtoupper(substr($student->full_name, 0, 1)) }}</span>
@endif
</div>
<div class="flex-1 min-w-0 pt-5 sm:pt-0">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div>
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-extrabold text-gray-900 truncate leading-tight tracking-tight">{{ $student->full_name }}</h2>
<div class="flex flex-wrap items-center gap-2 mt-3">
<span class="inline-flex items-center gap-2 px-3.5 py-1.5 text-sm font-mono font-semibold bg-gray-50 text-blue-700 border border-blue-200 rounded-xl">
<svg class="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0"/></svg>
{{ $student->nisn ?? '—' }}
</span>
<span class="inline-flex items-center gap-2 px-3.5 py-1.5 text-sm font-medium bg-blue-50 text-blue-700 border border-blue-200 rounded-xl">
<svg class="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
{{ $student->class_name ?? '—' }}
</span>
<span class="inline-flex items-center gap-2 px-3.5 py-1.5 text-sm font-medium bg-blue-50 text-blue-700 border border-blue-200 rounded-xl">
<svg class="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
{{ $student->department_name ?? $student->department_code ?? '—' }}
</span>
</div>
</div>
<div class="flex items-center gap-2">
@if($student->is_active)
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200 rounded-full">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
Aktif
</span>
@else
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-gray-100 text-gray-600 border border-gray-200 rounded-full">
<span class="w-1.5 h-1.5 rounded-full bg-gray-400"></span>
Tidak Aktif
</span>
@endif
</div>
</div>
</div>
</div>
{{-- SP Level Badge --}}
@if($currentSpLevel)
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-semibold mb-4"
style="background-color: {{ $currentSpLevel->color }}15; color: {{ $currentSpLevel->color }}; border: 1px solid {{ $currentSpLevel->color }}30;">
<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 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{{ $currentSpLevel->name }} — {{ $currentSpLevel->min_points }}+ poin
</div>
@endif
{{-- Quick Stats --}}
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div class="glass-stat rounded-xl border border-gray-100 p-4 text-center">
<p class="text-xs font-medium text-gray-400 uppercase tracking-wider mb-1">Total Poin</p>
<p class="text-2xl font-bold {{ $totalPoints >= 100 ? 'text-blue-600' : ($totalPoints >= 50 ? 'text-blue-500' : ($totalPoints > 0 ? 'text-blue-400' : 'text-gray-400')) }}">
{{ $totalPoints }}
</p>
</div>
<div class="glass-stat rounded-xl border border-gray-100 p-4 text-center">
<p class="text-xs font-medium text-gray-400 uppercase tracking-wider mb-1">Pelanggaran</p>
<p class="text-2xl font-bold text-gray-800">{{ $violationCount }}<span class="text-sm font-medium text-gray-400">x</span></p>
</div>
<div class="glass-stat rounded-xl border border-gray-100 p-4 text-center">
<p class="text-xs font-medium text-gray-400 uppercase tracking-wider mb-1">SP Terbit</p>
<p class="text-2xl font-bold {{ $activeSpLetters > 0 ? 'text-blue-600' : 'text-gray-400' }}">{{ $activeSpLetters }}</p>
</div>
<div class="glass-stat rounded-xl border border-gray-100 p-4 text-center">
<p class="text-xs font-medium text-gray-400 uppercase tracking-wider mb-1">Terakhir</p>
<p class="text-sm font-semibold text-gray-700 mt-1">
@if($lastViolation)
{{ $lastViolation->violation_date->diffForHumans() }}
@else
—
@endif
</p>
</div>
</div>
</div>
</div>
{{-- ===== SP PROGRESS ===== --}}
@if($spThresholds->count() > 0)
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 mb-6 perspective-card">
<div class="flex items-center justify-between mb-5">
<h3 class="text-base font-semibold text-gray-900">Progress Ambang Surat Peringatan</h3>
<span class="text-xs text-gray-400">{{ $totalPoints }} / {{ $nextSpThreshold ? $nextSpThreshold->min_points : $spThresholds->last()->min_points }} poin</span>
</div>
{{-- Progress Bar --}}
@php
$maxPoints = $nextSpThreshold ? $nextSpThreshold->min_points : $spThresholds->last()->min_points;
if ($totalPoints >= $spThresholds->last()->min_points) {
$progressPercent = 100;
} else {
$progressPercent = min(100, ($totalPoints / $maxPoints) * 100);
}
@endphp
<div class="relative h-3 bg-gray-100 rounded-full overflow-hidden mb-4">
<div class="sp-progress-bar h-full rounded-full transition-all duration-500 ease-out"
style="width: {{ $progressPercent }}%"></div>
</div>
{{-- Threshold Markers --}}
<div class="relative flex justify-between">
@foreach($spThresholds as $threshold)
@php
$tsReached = $totalPoints >= $threshold->min_points;
@endphp
<div class="flex flex-col items-center" style="flex:1">
<div class="flex items-center gap-1.5 mb-1.5">
<span class="w-3 h-3 rounded-full {{ $tsReached ? 'shadow-sm' : 'bg-gray-300' }}"
style="background-color: {{ $tsReached ? $threshold->color : '' }}"></span>
<span class="text-xs font-semibold {{ $tsReached ? '' : 'text-gray-400' }}"
style="color: {{ $tsReached ? $threshold->color : '' }}">
{{ $threshold->min_points }}
</span>
</div>
<span class="text-[10px] {{ $tsReached ? 'font-medium' : 'text-gray-400' }} leading-tight text-center"
style="color: {{ $tsReached ? $threshold->color : '' }}">
{{ $threshold->name }}
</span>
</div>
@endforeach
</div>
{{-- Status --}}
@if($currentSpLevel)
<div class="mt-5 p-3.5 rounded-xl text-sm flex items-start gap-3"
style="background-color: {{ $currentSpLevel->color }}10; border: 1px solid {{ $currentSpLevel->color }}20;">
<svg class="w-5 h-5 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"
style="color: {{ $currentSpLevel->color }}">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div>
<p class="font-semibold" style="color: {{ $currentSpLevel->color }}">{{ $currentSpLevel->name }} sudah tercapai</p>
<p class="text-gray-500 mt-0.5 text-xs">Siswa ini telah mencapai ambang minimal {{ $currentSpLevel->min_points }} poin. Surat peringatan telah diterbitkan.</p>
</div>
</div>
@elseif($nextSpThreshold && $violationCount > 0)
<div class="mt-5 p-3.5 rounded-xl bg-blue-50 border border-blue-100 text-sm flex items-start gap-3">
<svg class="w-5 h-5 mt-0.5 text-blue-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div>
<p class="font-semibold text-blue-700">Menuju {{ $nextSpThreshold->name }}</p>
<p class="text-blue-600/70 mt-0.5 text-xs">{{ $nextSpThreshold->min_points - $totalPoints }} poin lagi untuk mencapai ambang {{ $nextSpThreshold->name }} ({{ $nextSpThreshold->min_points }} poin)</p>
</div>
</div>
@endif
</div>
@endif
{{-- ===== VIOLATIONS TIMELINE ===== --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden mb-6">
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<div class="flex items-center gap-2.5">
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div>
<h3 class="text-base font-semibold text-gray-900">Riwayat Pelanggaran</h3>
<p class="text-xs text-gray-400">{{ $violationCount }} catatan pelanggaran</p>
</div>
</div>
</div>
@forelse($student->violations as $v)
<div class="violation-timeline relative pl-14 pr-6 py-4 {{ !$loop->last ? 'border-b border-gray-50' : '' }} hover:bg-gray-50/50 transition">
{{-- Timeline dot --}}
<div class="absolute left-[13px] top-[18px] w-[15px] h-[15px] rounded-full border-2 flex items-center justify-center"
style="border-color: {{ $v->violationType?->category?->color ?? '#9ca3af' }}; background-color: white;">
<span class="w-[7px] h-[7px] 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-medium text-gray-900">{{ $v->violationType->name ?? '—' }}</span>
@if($v->is_verified)
<span class="inline-flex items-center px-1.5 py-0.5 text-[10px] font-medium bg-blue-50 text-blue-600 border border-blue-200 rounded">✓ Terverifikasi</span>
@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 items-center gap-3 mt-1.5 text-xs text-gray-400">
<span class="inline-flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
{{ $v->violation_date->format('d M Y') }}
@if($v->violation_time)
{{ $v->violation_time }}
@endif
</span>
@if($v->location)
<span class="inline-flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
{{ $v->location }}
</span>
@endif
<span class="inline-flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
{{ $v->recorder->name ?? '—' }}
</span>
</div>
{{-- Evidence photos --}}
@if($v->evidences && $v->evidences->count() > 0)
<div class="flex gap-2 mt-2">
@foreach($v->evidences->take(3) as $ev)
<a href="{{ Storage::url($ev->file_path) }}" target="_blank"
class="w-14 h-14 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-14 h-14 rounded-lg bg-gray-50 border border-gray-200 flex items-center justify-center text-xs text-gray-400 font-medium">
+{{ $v->evidences->count() - 3 }}
</div>
@endif
</div>
@endif
</div>
<div class="flex flex-col items-end flex-shrink-0">
<span class="text-sm font-bold {{ $v->points >= 50 ? 'text-blue-600' : ($v->points >= 15 ? 'text-blue-500' : 'text-blue-400') }}">
+{{ $v->points }}
</span>
@if($v->sanction)
<span class="text-[10px] text-gray-400 mt-0.5 line-clamp-1 max-w-[100px] text-right">{{ $v->sanction }}</span>
@endif
<a href="{{ route('violations.show', $v->id) }}" class="text-[10px] text-blue-500 hover:text-blue-700 font-medium mt-1 transition">Detail</a>
</div>
</div>
</div>
@empty
<div class="py-16 text-center">
<div class="w-16 h-16 rounded-full bg-blue-50 border border-blue-100 flex items-center justify-center mx-auto mb-4">
<svg class="w-7 h-7 text-blue-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<h4 class="text-sm font-medium text-gray-500 mb-1">Belum Ada Pelanggaran</h4>
<p class="text-xs text-gray-400">Siswa ini belum memiliki catatan pelanggaran</p>
</div>
@endforelse
</div>
{{-- ===== SP LETTERS ===== --}}
@if($student->spLetters->count() > 0)
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden mb-6">
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<div class="flex items-center gap-2.5">
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<div>
<h3 class="text-base font-semibold text-gray-900">Surat Peringatan</h3>
<p class="text-xs text-gray-400">{{ $student->spLetters->count() }} surat diterbitkan</p>
</div>
</div>
</div>
<div class="divide-y divide-gray-50">
@foreach($student->spLetters as $sp)
<div class="px-6 py-4 flex items-center justify-between hover:bg-gray-50/50 transition group">
<div class="flex items-center gap-3 min-w-0">
<div class="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0"
style="background-color: {{ $sp->spThreshold?->color ?? '#3b82f6' }}15">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
style="color: {{ $sp->spThreshold?->color ?? '#3b82f6' }}">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<div class="min-w-0">
<p class="text-sm font-medium text-gray-900 truncate">
{{ $sp->spThreshold?->name ?? 'SP' }}
@if($sp->status === 'draft')
<span class="inline-flex items-center px-1.5 py-0.5 text-[10px] font-medium bg-blue-50 text-blue-600 border border-blue-200 rounded ml-1.5">Draft</span>
@elseif($sp->status === 'issued')
<span class="inline-flex items-center px-1.5 py-0.5 text-[10px] font-medium bg-emerald-50 text-emerald-600 border border-emerald-200 rounded ml-1.5">Terbit</span>
@endif
</p>
<p class="text-xs text-gray-400 mt-0.5">{{ $sp->letter_number }} • {{ $sp->created_at->format('d M Y') }}</p>
</div>
</div>
<div class="flex items-center gap-2 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<a href="{{ route('sp-letters.show', $sp->id) }}"
class="px-3 py-1.5 text-xs font-medium text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 hover:border-gray-300 transition-all">
Lihat
</a>
<a href="{{ route('sp-letters.print', $sp->id) }}"
class="px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition-all">
Cetak
</a>
</div>
{{-- Always show on mobile --}}
<div class="flex items-center gap-2 flex-shrink-0 sm:hidden">
<a href="{{ route('sp-letters.show', $sp->id) }}" class="text-xs text-blue-600 font-medium">Lihat</a>
</div>
</div>
@endforeach
</div>
</div>
@endif
{{-- ===== STUDENT INFO DETAILS ===== --}}
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100">
<div class="flex items-center gap-2.5">
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<h3 class="text-base font-semibold text-gray-900">Informasi Lengkap</h3>
</div>
</div>
<div class="p-6">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div>
<p class="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-1.5">NISN</p>
<p class="text-sm font-medium text-gray-900 font-mono">{{ $student->nisn ?? '—' }}</p>
</div>
<div>
<p class="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-1.5">NIS</p>
<p class="text-sm font-medium text-gray-900 font-mono">{{ $student->student_number ?? '—' }}</p>
</div>
<div>
<p class="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-1.5">Jenis Kelamin</p>
<p class="text-sm font-medium text-gray-900">{{ $student->gender === 'L' ? 'Laki-laki' : ($student->gender === 'P' ? 'Perempuan' : '—') }}</p>
</div>
<div>
<p class="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-1.5">Tempat, Tanggal Lahir</p>
<p class="text-sm font-medium text-gray-900">
{{ $student->place_of_birth ?? '—' }}{{ $student->place_of_birth && $student->date_of_birth ? ', ' : '' }}
{{ $student->date_of_birth ? $student->date_of_birth->format('d M Y') : '—' }}
</p>
</div>
<div>
<p class="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-1.5">Kelas</p>
<p class="text-sm font-medium text-gray-900">{{ $student->class_name ?? '—' }}</p>
</div>
<div>
<p class="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-1.5">Jurusan</p>
<p class="text-sm font-medium text-gray-900">{{ $student->department_name ?? $student->department_code ?? '—' }}</p>
</div>
<div class="sm:col-span-2 lg:col-span-3">
<p class="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-1.5">Alamat</p>
<p class="text-sm text-gray-700">{{ $student->address ?? '—' }}</p>
</div>
@if($student->phone_number || $student->email)
<div>
<p class="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-1.5">Telepon</p>
<p class="text-sm font-medium text-gray-900">{{ $student->phone_number ?? '—' }}</p>
</div>
<div>
<p class="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-1.5">Email</p>
<p class="text-sm font-medium text-gray-900">{{ $student->email ?? '—' }}</p>
</div>
@endif
<div>
<p class="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-1.5">Tahun Akademik</p>
<p class="text-sm font-medium text-gray-900">{{ $student->academic_year_name ?? '—' }}</p>
</div>
<div>
<p class="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-1.5">Terakhir Sinkron</p>
<p class="text-sm font-medium text-gray-900">{{ $student->synced_at ? $student->synced_at->format('d M Y H:i') : '—' }}</p>
</div>
</div>
</div>
</div>
</div>
@endsection
+261
View File
@@ -0,0 +1,261 @@
@extends('layouts.app')
@section('title', 'Manajemen User')
@section('content')
<div x-data="userManager()">
{{-- Header --}}
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Manajemen User</h1>
<p class="text-sm text-gray-500 mt-1">Kelola pengguna yang berhak mengakses aplikasi</p>
</div>
<button @click="openCreate()"
class="inline-flex items-center px-4 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm">
<i class="fa-solid fa-plus mr-2"></i>
Tambah User
</button>
</div>
{{-- Filter --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6">
<form method="GET" class="p-4">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<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"></i>
<input type="text" name="search" value="{{ request('search') }}"
placeholder="Cari nama, username, atau email..."
class="w-full pl-10 pr-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>
<select name="role"
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 bg-white">
<option value="">Semua Role</option>
<option value="admin" @selected(request('role') == 'admin')>Admin</option>
<option value="bk" @selected(request('role') == 'bk')>BK</option>
<option value="wali_kelas" @selected(request('role') == 'wali_kelas')>Wali Kelas</option>
<option value="staff" @selected(request('role') == 'staff')>Staff</option>
</select>
<div class="flex space-x-2">
<button type="submit" class="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition">
<i class="fa-solid fa-filter mr-1.5"></i> Filter
</button>
@if(request()->anyFilled(['search','role']))
<a href="{{ route('users.index') }}" class="px-4 py-2.5 text-sm font-medium text-gray-600 bg-white border border-gray-300 rounded-xl hover:bg-gray-50 transition">
<i class="fa-solid fa-xmark"></i>
</a>
@endif
</div>
</div>
</form>
</div>
{{-- 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-5 py-3.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">User</th>
<th class="px-5 py-3.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Username</th>
<th class="px-5 py-3.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Role</th>
<th class="px-5 py-3.5 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-5 py-3.5 text-right text-xs font-semibold text-gray-500 uppercase tracking-wider">Aksi</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($users as $user)
<tr class="hover:bg-gray-50 transition {{ !$user->is_active ? 'opacity-60' : '' }}">
<td class="px-5 py-4">
<div class="flex items-center space-x-3">
<div class="w-9 h-9 rounded-full flex items-center justify-center text-sm font-bold flex-shrink-0
@if($user->role === 'admin') bg-blue-100 text-blue-600
@elseif($user->role === 'bk') bg-blue-100 text-blue-600
@elseif($user->role === 'wali_kelas') bg-green-100 text-green-600
@else bg-gray-100 text-gray-600 @endif">
{{ strtoupper(substr($user->name, 0, 1)) }}
</div>
<div>
<p class="text-sm font-medium text-gray-900">{{ $user->name }}</p>
<p class="text-xs text-gray-400">{{ $user->email }}</p>
</div>
</div>
</td>
<td class="px-5 py-4 text-sm text-gray-700 font-mono">{{ $user->username }}</td>
<td class="px-5 py-4">
<span class="inline-flex items-center px-2.5 py-1 text-xs font-medium rounded-full
@if($user->role === 'admin') bg-blue-100 text-red-700
@elseif($user->role === 'bk') bg-blue-100 text-blue-700
@elseif($user->role === 'wali_kelas') bg-green-100 text-green-700
@else bg-gray-100 text-gray-700 @endif">
@if($user->role === 'admin') <i class="fa-solid fa-shield-halved mr-1"></i>
@elseif($user->role === 'bk') <i class="fa-solid fa-user-tie mr-1"></i>
@elseif($user->role === 'wali_kelas') <i class="fa-solid fa-chalkboard-user mr-1"></i>
@else <i class="fa-solid fa-user mr-1"></i> @endif
{{ ucfirst(str_replace('_', ' ', $user->role)) }}
</span>
</td>
<td class="px-5 py-4 text-center">
@if($user->is_active)
<span class="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-green-100 text-green-700 rounded-full">
<span class="w-1.5 h-1.5 bg-green-500 rounded-full mr-1.5"></span>
Aktif
</span>
@else
<span class="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-gray-100 text-gray-500 rounded-full">
<span class="w-1.5 h-1.5 bg-gray-400 rounded-full mr-1.5"></span>
Nonaktif
</span>
@endif
</td>
<td class="px-5 py-4 text-right">
<div class="flex items-center justify-end space-x-1">
<button @click="openEdit({{ $user->id }}, '{{ addslashes($user->name) }}', '{{ $user->username }}', '{{ $user->email }}', '{{ $user->role }}', {{ $user->is_active ? 'true' : 'false' }})"
class="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition">
<i class="fa-solid fa-pen-to-square"></i>
</button>
@if($user->id !== auth()->id())
<form action="{{ route('users.destroy', $user->id) }}" method="POST" class="inline"
x-data x-on:submit.prevent="if(await window.confirmSwal({text:'Hapus user ini?'})) $el.submit()"">
@csrf @method('DELETE')
<button type="submit"
class="p-2 text-red-500 hover:bg-red-50 rounded-lg transition">
<i class="fa-solid fa-trash-can"></i>
</button>
</form>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-5 py-16 text-center">
<i class="fa-solid fa-users text-gray-300 text-4xl mb-3"></i>
<p class="text-sm text-gray-500">Tidak ada user ditemukan</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($users->hasPages())
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50">
{{ $users->appends(request()->query())->links() }}
</div>
@endif
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50 flex justify-between text-xs text-gray-500">
<span>{{ $users->firstItem() ?? 0 }}–{{ $users->lastItem() ?? 0 }} dari {{ $users->total() }}</span>
<span>{{ $users->total() }} total</span>
</div>
</div>
{{-- Modal --}}
<div x-show="modalOpen" class="fixed inset-0 z-50 overflow-y-auto" style="display: none;">
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
<div @click="modalOpen = false" class="fixed inset-0 bg-gray-500 bg-opacity-50 transition-opacity"></div>
<div @click.away="modalOpen = false"
class="relative inline-block align-bottom bg-white rounded-2xl shadow-xl border border-gray-200 text-left overflow-hidden transform transition-all sm:align-middle sm:max-w-lg sm:w-full">
<div class="px-6 py-5 border-b border-gray-100 flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-lg flex items-center justify-center" :class="isEditing ? 'bg-blue-100' : 'bg-blue-100'">
<i class="fa-solid" :class="isEditing ? 'fa-pen-to-square text-blue-600' : 'fa-user-plus text-blue-600'"></i>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900" x-text="isEditing ? 'Edit User' : 'Tambah User'"></h3>
<p class="text-sm text-gray-500" x-text="isEditing ? 'Ubah data pengguna' : 'Buat akun pengguna baru'"></p>
</div>
</div>
<button @click="modalOpen = false" class="text-gray-400 hover:text-gray-600 transition">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
{{-- Form --}}
<form :action="isEditing ? `/users/${editId}` : '{{ route('users.store') }}'" method="POST" class="p-6 space-y-4">
@csrf
<input type="hidden" name="_method" :value="isEditing ? 'PUT' : 'POST'">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1.5">Nama Lengkap</label>
<input type="text" x-model="formName" name="name" 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"
placeholder="Nama lengkap">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Username</label>
<input type="text" x-model="formUsername" name="username" 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"
placeholder="username">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Email</label>
<input type="email" x-model="formEmail" name="email" 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"
placeholder="email@sekolah.sch.id">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">
Password <span x-text="isEditing ? '(kosongkan jika tidak diubah)' : ''" class="text-gray-400 font-normal"></span>
</label>
<input type="password" x-model="formPassword" name="password" :required="!isEditing" minlength="6"
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"
placeholder="Min. 6 karakter">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1.5">Role / Hak Akses</label>
<select x-model="formRole" name="role" 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 bg-white">
<option value="bk">BK — Input & laporan</option>
<option value="admin">Admin — Full akses</option>
<option value="wali_kelas">Wali Kelas — Lihat kelas sendiri</option>
<option value="staff">Staff — Terbatas</option>
</select>
</div>
</div>
<div x-show="isEditing" class="flex items-center justify-between p-4 bg-gray-50 rounded-xl border border-gray-100">
<div>
<p class="text-sm font-medium text-gray-900">Akun Aktif</p>
<p class="text-xs text-gray-500">Nonaktifkan untuk menonaktifkan akun</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="formActive" name="is_active" value="1" class="sr-only peer">
<div class="w-11 h-6 bg-gray-200 rounded-full peer peer-checked:after:translate-x-full peer-checked:bg-blue-600 after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all"></div>
</label>
</div>
<div class="flex justify-end space-x-3 pt-2">
<button type="button" @click="modalOpen = false"
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</button>
<button type="submit"
class="px-5 py-2.5 text-sm font-medium text-white rounded-xl transition shadow-sm"
:class="isEditing ? 'bg-blue-600 hover:bg-blue-700' : 'bg-blue-600 hover:bg-blue-700'"
x-text="isEditing ? 'Simpan Perubahan' : 'Tambah User'"></button>
</div>
</form>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
function userManager() {
return {
modalOpen: false, isEditing: false, editId: null,
formName: '', formUsername: '', formEmail: '', formPassword: '', formRole: 'bk', formActive: true,
openCreate() {
this.isEditing = false; this.editId = null;
this.formName = ''; this.formUsername = ''; this.formEmail = ''; this.formPassword = '';
this.formRole = 'bk'; this.formActive = true; this.modalOpen = true;
},
openEdit(id, name, username, email, role, active) {
this.isEditing = true; this.editId = id;
this.formName = name; this.formUsername = username; this.formEmail = email;
this.formPassword = ''; this.formRole = role; this.formActive = active; this.modalOpen = true;
}
};
}
</script>
@endpush
@endsection
+294
View File
@@ -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
+110
View File
@@ -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
+220
View File
@@ -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
File diff suppressed because one or more lines are too long