Initial commit: Aplikasi Ketertiban Siswa
- Fitur: input pelanggaran, sistem poin, surat peringatan otomatis, SP progress - Realtime notifikasi via Laravel Reverb + Echo - Dashboard statistik, manajemen user, manajemen master data - Login 2 kolom premium, user dropdown, pengaturan akun - Tema biru kalem, logo sekolah dinamis, sidebar brand dinamis - Sinkronisasi siswa dari Database Kesiswaan via API
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
@import 'tailwindcss';
|
||||
@import '@fortawesome/fontawesome-free/css/fontawesome.css';
|
||||
@import '@fortawesome/fontawesome-free/css/regular.css';
|
||||
@import '@fortawesome/fontawesome-free/css/solid.css';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
|
||||
/* Custom styles for Aplikasi Ketertiban */
|
||||
[x-cloak] { display: none !important; }
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar { width: 6px; }
|
||||
::-webkit-scrollbar-track { background: #f1f1f1; }
|
||||
::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #a1a1a1; }
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
body { background: white !important; }
|
||||
.no-print { display: none !important; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import './bootstrap';
|
||||
|
||||
import Alpine from 'alpinejs';
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.start();
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
import Echo from 'laravel-echo';
|
||||
import Pusher from 'pusher-js';
|
||||
|
||||
window.Pusher = Pusher;
|
||||
|
||||
// Echo will be initialized when the page has the Reverb config
|
||||
// The actual Echo initialization is done conditionally in the layout
|
||||
@@ -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>
|
||||
@@ -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 →</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
|
||||
@@ -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">×</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">×</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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,294 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Input Pelanggaran')
|
||||
|
||||
@section('content')
|
||||
<div x-data="violationForm()">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Input Pelanggaran</h1>
|
||||
<p class="text-sm text-gray-500">Catat pelanggaran siswa baru</p>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('violations.store') }}" method="POST" enctype="multipart/form-data" class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
@csrf
|
||||
|
||||
<div class="p-6 space-y-6">
|
||||
{{-- Search Siswa --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1.5">Siswa <span class="text-red-500">*</span></label>
|
||||
<div class="relative">
|
||||
<input type="text" x-model="searchQuery" @input.debounce="searchStudents"
|
||||
placeholder="Cari NISN atau Nama siswa..."
|
||||
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
||||
<input type="hidden" name="student_id" x-model="selectedStudentId">
|
||||
|
||||
<div x-show="results.length > 0 && !selectedStudentId"
|
||||
class="absolute z-10 mt-1 w-full bg-white shadow-lg border border-gray-200 rounded-xl max-h-48 overflow-y-auto">
|
||||
<template x-for="s in results" :key="s.id">
|
||||
<div @click="selectStudent(s)"
|
||||
class="px-4 py-2.5 hover:bg-red-50 cursor-pointer border-b border-gray-100 last:border-0 flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900" x-text="s.full_name"></p>
|
||||
<p class="text-xs text-gray-500" x-text="s.nisn + ' - ' + (s.class_name || '')"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div x-show="selectedStudentId"
|
||||
class="mt-2 flex items-center justify-between p-3 bg-red-50 border border-blue-200 rounded-xl">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
|
||||
<span class="text-sm font-bold text-blue-600" x-text="selectedStudentName.charAt(0)"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900" x-text="selectedStudentName"></p>
|
||||
<p class="text-xs text-gray-500" x-text="selectedStudentInfo"></p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" @click="clearStudent()" class="text-red-400 hover:text-blue-600 transition p-1">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@error('student_id') <p class="mt-1 text-sm text-blue-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Jenis Pelanggaran + Poin & Sanksi in ONE scope --}}
|
||||
<div x-data="typeSearch({{ json_encode($typeGroups) }})">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1.5">Jenis Pelanggaran <span class="text-red-500">*</span></label>
|
||||
<div class="relative">
|
||||
<input type="text" x-model="q" @input="open=true" @focus="open=true"
|
||||
@click.away="open=false" @keydown.escape="open=false"
|
||||
placeholder="Ketik nama pelanggaran..."
|
||||
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition"
|
||||
:class="selectedId ? 'border-green-300 bg-green-50' : ''">
|
||||
|
||||
<input type="hidden" name="violation_type_id" x-model="selectedId">
|
||||
|
||||
<div x-show="open && filtered.length > 0"
|
||||
class="absolute z-20 mt-1 w-full bg-white shadow-lg border border-gray-200 rounded-xl max-h-72 overflow-y-auto">
|
||||
<template x-for="(group, gi) in filtered" :key="gi">
|
||||
<div>
|
||||
<div class="sticky top-0 px-4 py-1.5 text-xs font-semibold uppercase tracking-wider border-b flex items-center space-x-1.5 z-10"
|
||||
:style="'background:' + group.color + '15; color:' + group.color + '; border-color:' + group.color + '30'">
|
||||
<span class="w-2 h-2 rounded-full" :style="'background:' + group.color"></span>
|
||||
<span x-text="group.label"></span>
|
||||
<span class="ml-1 opacity-60" x-text="'(' + group.types.length + ')'"></span>
|
||||
</div>
|
||||
<template x-for="t in group.types" :key="t.id">
|
||||
<div @click="pick(t)" @keydown.enter="pick(t)" tabindex="0"
|
||||
class="px-4 py-2.5 cursor-pointer border-b border-gray-50 flex items-center justify-between transition"
|
||||
:class="selectedId == t.id ? 'bg-red-50 border-l-2 border-l-red-500' : 'hover:bg-gray-50'">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 truncate" x-text="t.name"></p>
|
||||
<p class="text-xs text-gray-400 truncate" x-text="t.sanction || '—'"></p>
|
||||
</div>
|
||||
<span class="ml-3 text-sm font-bold flex-shrink-0 px-2 py-0.5 rounded"
|
||||
:class="t.points >= 50 ? 'bg-blue-100 text-red-700' : (t.points >= 15 ? 'bg-yellow-100 text-yellow-700' : 'bg-green-100 text-green-700')"
|
||||
x-text="'+' + t.points"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div x-show="open && filtered.length === 0 && q.length > 0"
|
||||
class="absolute z-20 mt-1 w-full bg-white shadow-lg border border-gray-200 rounded-xl p-5 text-center">
|
||||
<p class="text-sm text-gray-500">Tidak ditemukan untuk "<span x-text="q" class="font-medium"></span>"</p>
|
||||
</div>
|
||||
|
||||
<div x-show="selectedId && selectedName"
|
||||
class="mt-2 flex items-center justify-between p-3 rounded-xl border"
|
||||
:style="'border-color:' + selColor + '40; background:' + selColor + '08'">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<span class="w-3 h-3 rounded-full" :style="'background:' + selColor"></span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900" x-text="selectedName"></p>
|
||||
<p class="text-xs text-gray-500" x-text="selectedSanction ? 'Sanksi: ' + selectedSanction : ''"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="text-xl font-bold" :class="selectedPoints >= 50 ? 'text-blue-600' : (selectedPoints >= 15 ? 'text-yellow-600' : 'text-green-600')" x-text="'+' + selectedPoints"></span>
|
||||
<button @click="clear()" class="text-gray-300 hover:text-red-500 transition">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@error('violation_type_id') <p class="mt-1 text-sm text-blue-600">{{ $message }}</p> @enderror
|
||||
|
||||
{{-- Poin & Sanksi -- same scope as typeSearch --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1.5">Poin</label>
|
||||
<input type="number" name="points" x-model="selectedPoints" readonly
|
||||
class="w-full px-4 py-2.5 bg-gray-50 border border-gray-300 rounded-xl sm:text-sm text-gray-700">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1.5">Sanksi</label>
|
||||
<input type="text" name="sanction" x-model="selectedSanction"
|
||||
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Tanggal & Waktu --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1.5">Tanggal Pelanggaran <span class="text-red-500">*</span></label>
|
||||
<input type="date" name="violation_date" value="{{ date('Y-m-d') }}" required
|
||||
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1.5">Waktu</label>
|
||||
<input type="time" name="violation_time" value="{{ date('H:i') }}"
|
||||
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Lokasi --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1.5">Lokasi</label>
|
||||
<input type="text" name="location" placeholder="Contoh: Depan kelas, Lapangan, dll"
|
||||
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
||||
</div>
|
||||
|
||||
{{-- Deskripsi --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1.5">Deskripsi / Catatan</label>
|
||||
<textarea name="description" rows="3" placeholder="Deskripsi pelanggaran..."
|
||||
class="w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition resize-none"></textarea>
|
||||
</div>
|
||||
|
||||
{{-- Foto Bukti --}}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Bukti Foto <span class="text-gray-400 font-normal">(maks. 5 foto)</span></label>
|
||||
<input type="file" name="evidences[]" id="evidences" multiple accept="image/*" capture="environment" @change="handleFiles" class="hidden">
|
||||
|
||||
<div class="grid grid-cols-3 md:grid-cols-5 gap-3 mb-3">
|
||||
<template x-for="(file, index) in files" :key="index">
|
||||
<div class="relative aspect-square rounded-xl overflow-hidden border border-gray-200 bg-gray-50 group">
|
||||
<img :src="file.url" class="w-full h-full object-cover">
|
||||
<button type="button" @click="removeFile(index)"
|
||||
class="absolute top-1.5 right-1.5 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center text-xs hover:bg-blue-700 transition opacity-0 group-hover:opacity-100">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent text-white text-xs px-2 py-1.5 truncate" x-text="file.name"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="files.length < 5">
|
||||
<div @click="document.getElementById('evidences').click()"
|
||||
class="aspect-square rounded-xl border-2 border-dashed border-gray-300 hover:border-red-400 bg-gray-50 flex flex-col items-center justify-center cursor-pointer transition hover:bg-red-50/30">
|
||||
<i class="fa-solid fa-camera text-gray-300 text-2xl mb-1"></i>
|
||||
<span class="text-xs text-gray-400">Tambah Foto</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2">
|
||||
<button type="button" @click="document.getElementById('evidences').click()" class="px-4 py-2 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 transition flex items-center space-x-2">
|
||||
<i class="fa-solid fa-image text-gray-500"></i><span>Pilih File</span>
|
||||
</button>
|
||||
<button type="button" @click="captureCamera" class="px-4 py-2 text-sm border border-gray-300 rounded-xl hover:bg-gray-50 transition flex items-center space-x-2">
|
||||
<i class="fa-solid fa-camera text-gray-500"></i><span>Ambil Foto</span>
|
||||
</button>
|
||||
</div>
|
||||
@error('evidences.*') <p class="mt-1 text-sm text-blue-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Submit --}}
|
||||
<div class="px-6 py-4 bg-gray-50 border-t border-gray-100 rounded-b-xl flex justify-end space-x-3">
|
||||
<a href="{{ route('violations.index') }}" class="px-5 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-xl hover:bg-gray-50 transition">Batal</a>
|
||||
<button type="submit" class="px-6 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm flex items-center space-x-2">
|
||||
<i class="fa-solid fa-floppy-disk"></i>
|
||||
<span>Simpan Pelanggaran</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function typeSearch(groups) {
|
||||
return {
|
||||
q: '', open: false,
|
||||
selectedId: '', selectedName: '',
|
||||
selectedPoints: 0, selectedSanction: '',
|
||||
selColor: '#6b7280',
|
||||
groups: groups,
|
||||
|
||||
get filtered() {
|
||||
if (!this.q || this.q.length < 1) return this.groups;
|
||||
const q = this.q.toLowerCase();
|
||||
return this.groups.map(g => ({
|
||||
...g,
|
||||
types: g.types.filter(t =>
|
||||
t.name.toLowerCase().includes(q) ||
|
||||
(t.sanction && t.sanction.toLowerCase().includes(q))
|
||||
)
|
||||
})).filter(g => g.types.length > 0);
|
||||
},
|
||||
|
||||
pick(t) {
|
||||
this.selectedId = t.id;
|
||||
this.selectedName = t.name;
|
||||
this.selectedPoints = t.points;
|
||||
this.selectedSanction = t.sanction || '';
|
||||
this.selColor = this.groups.find(g => g.types.some(x => x.id === t.id))?.color || '#6b7280';
|
||||
this.q = t.name;
|
||||
this.open = false;
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.selectedId = ''; this.selectedName = '';
|
||||
this.selectedPoints = 0; this.selectedSanction = '';
|
||||
this.selColor = '#6b7280'; this.q = ''; this.open = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function violationForm() {
|
||||
return {
|
||||
searchQuery: '', results: [],
|
||||
selectedStudentId: null, selectedStudentName: '', selectedStudentInfo: '',
|
||||
files: [],
|
||||
|
||||
searchStudents() {
|
||||
if (this.searchQuery.length < 2) { this.results = []; return; }
|
||||
if (this.selectedStudentId) return;
|
||||
fetch('{{ route("api.students.search") }}?q=' + encodeURIComponent(this.searchQuery))
|
||||
.then(r => r.json()).then(data => { this.results = data; });
|
||||
},
|
||||
selectStudent(s) {
|
||||
this.selectedStudentId = s.id; this.selectedStudentName = s.full_name;
|
||||
this.selectedStudentInfo = s.nisn + ' - ' + (s.class_name || '');
|
||||
this.searchQuery = s.full_name; this.results = [];
|
||||
},
|
||||
clearStudent() {
|
||||
this.selectedStudentId = null; this.selectedStudentName = '';
|
||||
this.selectedStudentInfo = ''; this.searchQuery = '';
|
||||
},
|
||||
handleFiles(e) {
|
||||
const newFiles = Array.from(e.target.files);
|
||||
const toAdd = newFiles.slice(0, 5 - this.files.length);
|
||||
toAdd.forEach(file => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
this.files.push({ file, url: ev.target.result, name: file.name });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
},
|
||||
removeFile(index) { this.files.splice(index, 1); },
|
||||
captureCamera() {
|
||||
const input = document.getElementById('evidences');
|
||||
input.setAttribute('capture', 'environment'); input.click();
|
||||
setTimeout(() => input.removeAttribute('capture'), 100);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
@@ -0,0 +1,110 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Data Pelanggaran')
|
||||
|
||||
@section('content')
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Data Pelanggaran</h1>
|
||||
<p class="text-sm text-gray-500">Riwayat pelanggaran siswa</p>
|
||||
</div>
|
||||
<a href="{{ route('violations.create') }}" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition flex items-center space-x-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
||||
<span>Input Pelanggaran</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- Filter --}}
|
||||
<form method="GET" class="bg-white p-4 rounded-xl shadow-sm border border-gray-200 mb-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<input type="text" name="search" placeholder="Cari NISN/Nama..." value="{{ request('search') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<select name="category_id" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Semua Kategori</option>
|
||||
@foreach($categories as $cat)
|
||||
<option value="{{ $cat->id }}" @selected(request('category_id') == $cat->id)>{{ $cat->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<input type="date" name="date_from" value="{{ request('date_from') }}" placeholder="Dari tanggal"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<input type="date" name="date_to" value="{{ request('date_to') }}" placeholder="Sampai tanggal"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||
<button type="submit" class="px-3 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700">Filter</button>
|
||||
@if(request()->anyFilled(['search','category_id','date_from','date_to']))
|
||||
<a href="{{ route('violations.index') }}" class="px-3 py-2 border border-gray-300 text-gray-600 text-sm rounded-lg hover:bg-gray-50">Reset</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{-- Table --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Tanggal</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Siswa</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Pelanggaran</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">Poin</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">Verifikasi</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Oleh</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
@forelse($violations as $v)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-700">{{ $v->violation_date->format('d/m/Y') }}</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $v->student->full_name ?? '-' }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $v->student->nisn ?? '' }} - {{ $v->student->class_name ?? '' }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-2.5 h-2.5 rounded-full flex-shrink-0" style="background-color: {{ $v->violationType?->category?->color ?? '#6b7280' }}"></span>
|
||||
<span class="text-sm text-gray-700">{{ $v->violationType->name ?? '-' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
+{{ $v->points }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
@if($v->is_verified)
|
||||
<span class="text-xs text-green-600 font-medium">✓ Terverifikasi</span>
|
||||
@else
|
||||
<span class="text-xs text-yellow-600 font-medium">Belum</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-500">{{ $v->recorder->name ?? '-' }}</td>
|
||||
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||||
<a href="{{ route('violations.show', $v->id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">Detail</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-4 py-12 text-center text-gray-500">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"></path></svg>
|
||||
<p>Belum ada pelanggaran tercatat</p>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if($violations->hasPages())
|
||||
<div class="px-4 py-3 border-t border-gray-100">
|
||||
{{ $violations->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,220 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Detail Pelanggaran')
|
||||
|
||||
@section('content')
|
||||
<div>
|
||||
{{-- Header --}}
|
||||
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<a href="{{ route('violations.index') }}" class="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition">
|
||||
<i class="fa-solid fa-arrow-left"></i>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Detail Pelanggaran</h1>
|
||||
<p class="text-sm text-gray-500 mt-0.5">Informasi lengkap pelanggaran siswa</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
@if(!$violation->is_verified)
|
||||
<form action="{{ route('violations.verify', $violation->id) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-xl hover:bg-green-700 transition shadow-sm">
|
||||
<i class="fa-solid fa-check-circle mr-1.5"></i>
|
||||
Verifikasi
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
<form action="{{ route('violations.destroy', $violation->id) }}" method="POST" x-data onsubmit="event.preventDefault(); Swal.fire({title: 'Konfirmasi', text: 'Hapus pelanggaran ini?', icon: 'warning', showCancelButton: true, confirmButtonColor: '#dc2626', cancelButtonColor: '#6b7280', confirmButtonText: 'Ya, hapus!', cancelButtonText: 'Batal'}).then(r => { if(r.isConfirmed) { .target.submit(); } })">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 text-sm font-medium text-blue-600 bg-white border border-blue-200 rounded-xl hover:bg-blue-50 transition">
|
||||
<i class="fa-solid fa-trash-can mr-1.5"></i>
|
||||
Hapus
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Content Grid --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{{-- Left: Info Siswa + Pelanggaran --}}
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
{{-- Card Siswa --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="p-5 flex items-center space-x-4">
|
||||
<div class="w-14 h-14 rounded-full flex items-center justify-center text-lg font-bold flex-shrink-0"
|
||||
style="background-color: {{ $violation->violationType?->category?->color ?? '#6b7280' }}20; color: {{ $violation->violationType?->category?->color ?? '#6b7280' }}">
|
||||
{{ strtoupper(substr($violation->student->full_name ?? '?', 0, 1)) }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-lg font-bold text-gray-900 truncate">{{ $violation->student->full_name ?? '-' }}</h2>
|
||||
<p class="text-sm text-gray-500">{{ $violation->student->nisn ?? '' }} <span class="mx-1.5">•</span> {{ $violation->student->class_name ?? '' }}</p>
|
||||
<p class="text-xs text-gray-400">{{ $violation->student->department_name ?? '' }}</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0 text-right">
|
||||
<p class="text-xs text-gray-500">Total Poin</p>
|
||||
<p class="text-2xl font-bold" style="color: {{ $violation->violationType?->category?->color ?? '#6b7280' }}">{{ $violation->student->total_points }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Card Detail Pelanggaran --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-5 py-4 border-b border-gray-100 flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-gray-900">Detail Pelanggaran</h3>
|
||||
<span class="inline-flex items-center px-2.5 py-1 text-xs font-medium rounded-full"
|
||||
style="background-color: {{ $violation->violationType?->category?->color ?? '#6b7280' }}15; color: {{ $violation->violationType?->category?->color ?? '#6b7280' }}">
|
||||
<span class="w-1.5 h-1.5 rounded-full mr-1.5" style="background-color: {{ $violation->violationType?->category?->color ?? '#6b7280' }}"></span>
|
||||
{{ $violation->violationType?->category?->name ?? '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
{{-- Info bars --}}
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between py-2 border-b border-gray-50 last:border-0">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-gavel text-blue-500 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-600">Jenis Pelanggaran</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900">{{ $violation->violationType->name ?? '-' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2 border-b border-gray-50">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-star text-blue-500 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-600">Poin</span>
|
||||
</div>
|
||||
<span class="text-lg font-bold text-blue-600">+{{ $violation->points }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2 border-b border-gray-50">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-scale-balanced text-blue-500 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-600">Sanksi</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900 text-right max-w-[50%]">{{ $violation->sanction ?? '-' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2 border-b border-gray-50">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-calendar text-blue-500 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-600">Tanggal</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900">{{ $violation->violation_date->format('d F Y') }} @if($violation->violation_time){{ \Carbon\Carbon::parse($violation->violation_time)->format('H:i') }} WIB @endif</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2 border-b border-gray-50">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-location-dot text-blue-500 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-600">Lokasi</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900">{{ $violation->location ?? '-' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<i class="fa-solid fa-user-pen text-blue-500 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-600">Dicatat oleh</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900">{{ $violation->recorder->name ?? '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Deskripsi --}}
|
||||
@if($violation->description)
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-5 py-4 border-b border-gray-100">
|
||||
<h3 class="text-sm font-semibold text-gray-900">Deskripsi / Catatan</h3>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<p class="text-sm text-gray-700 leading-relaxed">{{ $violation->description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Right Sidebar: Status + Evidences --}}
|
||||
<div class="space-y-6">
|
||||
{{-- Status Card --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-5 py-4 border-b border-gray-100">
|
||||
<h3 class="text-sm font-semibold text-gray-900">Status</h3>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
{{-- Verifikasi --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">Verifikasi</span>
|
||||
@if($violation->is_verified)
|
||||
<span class="inline-flex items-center px-2.5 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full">
|
||||
<i class="fa-solid fa-check-circle mr-1"></i>
|
||||
Terverifikasi
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex items-center px-2.5 py-1 text-xs font-medium bg-yellow-100 text-yellow-700 rounded-full">
|
||||
<i class="fa-solid fa-clock mr-1"></i>
|
||||
Belum
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($violation->is_verified)
|
||||
<div class="text-xs text-gray-400 flex items-center space-x-1">
|
||||
<i class="fa-solid fa-check text-green-500"></i>
|
||||
<span>oleh {{ $violation->verifier->name ?? '-' }} • {{ $violation->verified_at?->format('d/m/Y H:i') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Waktu input --}}
|
||||
<div class="pt-3 border-t border-gray-100">
|
||||
<p class="text-xs text-gray-400">Dibuat pada</p>
|
||||
<p class="text-sm text-gray-700">{{ $violation->created_at->format('d F Y H:i') }}</p>
|
||||
</div>
|
||||
@if($violation->updated_at != $violation->created_at)
|
||||
<div>
|
||||
<p class="text-xs text-gray-400">Terakhir diubah</p>
|
||||
<p class="text-sm text-gray-700">{{ $violation->updated_at->format('d F Y H:i') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Evidence Photos --}}
|
||||
@if($violation->evidences->count() > 0)
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<div class="px-5 py-4 border-b border-gray-100 flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-gray-900">Bukti Foto</h3>
|
||||
<span class="text-xs text-gray-400">{{ $violation->evidences->count() }} file</span>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
@foreach($violation->evidences as $evidence)
|
||||
<a href="{{ $evidence->url }}" target="_blank"
|
||||
class="group relative block aspect-square rounded-xl overflow-hidden border border-gray-200 bg-gray-50 transition-all">
|
||||
<img src="{{ $evidence->url }}" class="w-full h-full object-cover scale-100 transition-transform duration-300" alt="{{ $evidence->original_name }}">
|
||||
<div class="absolute inset-0 bg-black/0 bg-black/0 transition-colors"></div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user