484 lines
29 KiB
PHP
484 lines
29 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="id" class="h-full bg-slate-100">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
{{-- PWA --}}
|
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
<meta name="theme-color" content="#2563eb">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
<meta name="apple-mobile-web-app-title" content="Ketertiban">
|
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
|
<link rel="icon" href="/icons/icon-192.png">
|
|
@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>
|
|
|
|
{{-- Open Graph / Social Preview --}}
|
|
<meta property="og:title" content="@yield('title', $appName) - {{ $schoolName }}">
|
|
<meta property="og:description" content="Aplikasi Ketertiban Siswa {{ $schoolName }} — Manajemen pelanggaran, presensi, dan surat peringatan siswa.">
|
|
<meta property="og:site_name" content="{{ $appName }}">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:url" content="{{ url()->current() }}">
|
|
<meta property="og:image" content="{{ asset('og-image.png') }}">
|
|
<meta property="og:image:width" content="1200">
|
|
<meta property="og:image:height" content="630">
|
|
<meta property="og:image:alt" content="{{ $appName }}">
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:title" content="@yield('title', $appName) - {{ $schoolName }}">
|
|
<meta name="twitter:description" content="Aplikasi Ketertiban Siswa {{ $schoolName }}.">
|
|
<meta name="twitter:image" content="{{ asset('og-image.png') }}">
|
|
|
|
{{-- Favicon mengikuti logo sekolah --}}
|
|
<link rel="icon" href="/favicon.ico" sizes="any">
|
|
<link rel="icon" type="image/png" sizes="192x192" href="/logo.png">
|
|
<link rel="shortcut icon" href="/favicon.ico">
|
|
|
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
<style>
|
|
@keyframes fadeInUp {
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.animate-fade-in { animation: fadeInUp 0.3s ease-out; }
|
|
.stat-card-glow:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.08); transform: translateY(-1px); }
|
|
</style>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
|
<style>
|
|
@keyframes fadeInUp {
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.animate-fade-in { animation: fadeInUp 0.3s ease-out; }
|
|
.stat-card-glow:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.08); transform: translateY(-1px); }
|
|
|
|
/* Modern Flatpickr — clean, minimal, borderless glass */
|
|
.flatpickr-calendar {
|
|
border-radius: 16px !important;
|
|
box-shadow: 0 12px 48px rgba(0,0,0,0.12), 0 4px 12px rgba(0,0,0,0.06) !important;
|
|
border: 1px solid rgba(226,232,240,0.8) !important;
|
|
background: rgba(255,255,255,0.98) !important;
|
|
backdrop-filter: blur(8px);
|
|
margin-top: 6px;
|
|
padding: 8px;
|
|
width: 370px !important;
|
|
}
|
|
.flatpickr-months { margin-bottom: 6px; }
|
|
.flatpickr-current-month .flatpickr-monthDropdown-months { font-weight: 700; font-size: 14px; padding: 4px 8px; border-radius: 8px; }
|
|
.flatpickr-current-month input.cur-year { font-weight: 700; font-size: 14px; }
|
|
.flatpickr-weekday { font-weight: 600; color: #94a3b8 !important; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
.flatpickr-day {
|
|
border-radius: 10px !important;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
margin: 1px;
|
|
height: 42px;
|
|
line-height: 42px;
|
|
transition: all 0.12s ease;
|
|
}
|
|
.flatpickr-day:hover { background: #f1f5f9 !important; transform: scale(1.05); }
|
|
.flatpickr-day.selected {
|
|
background: linear-gradient(135deg, #2563eb, #1d4ed8) !important;
|
|
border: none !important;
|
|
color: white !important;
|
|
font-weight: 700;
|
|
box-shadow: 0 4px 12px rgba(37,99,235,0.3);
|
|
}
|
|
.flatpickr-day.today {
|
|
border: 2px solid #2563eb !important;
|
|
font-weight: 700;
|
|
}
|
|
.flatpickr-day.today:hover { border-color: #1d4ed8 !important; }
|
|
.flatpickr-day.today.selected { border: none !important; }
|
|
.flatpickr-day.inRange { background: #dbeafe !important; border-color: #bfdbfe !important; }
|
|
.flatpickr-day.nextMonthDay, .flatpickr-day.prevMonthDay { opacity: 0.35; }
|
|
.flatpickr-prev-month, .flatpickr-next-month { padding: 6px !important; border-radius: 8px !important; transition: background 0.15s; }
|
|
.flatpickr-prev-month:hover, .flatpickr-next-month:hover { background: #f1f5f9 !important; }
|
|
.flatpickr-prev-month svg, .flatpickr-next-month svg { width: 10px; height: 10px; }
|
|
.numInputWrapper:hover { background: transparent !important; }
|
|
.flatpickr-current-month .numInputWrapper span.arrowUp, .flatpickr-current-month .numInputWrapper span.arrowDown { display: none; }
|
|
.flatpickr-time { border-top: 1px solid #f1f5f9 !important; }
|
|
.flatpickr-time input { font-size: 13px !important; }
|
|
|
|
/* Flatpickr altInput styling */
|
|
.flatpickr-input[readonly] {
|
|
background-color: white !important;
|
|
cursor: pointer !important;
|
|
}
|
|
.flatpickr-altinput {
|
|
background: white;
|
|
}
|
|
</style>
|
|
@stack('styles')
|
|
</head>
|
|
<body class="h-full antialiased">
|
|
<div x-data="notifications()" class="min-h-screen flex bg-gradient-to-br from-slate-100 via-slate-50 to-blue-50/40">
|
|
{{-- 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-slate-200/80 lg:translate-x-0 lg:static lg:inset-auto transition-transform duration-200 ease-in-out flex flex-col shadow-xl lg:shadow-sm">
|
|
<div class="flex items-center justify-between px-4 h-16 border-b border-slate-100 bg-gradient-to-b from-slate-50/80 to-transparent">
|
|
<a href="{{ route('dashboard') }}" class="min-w-0 flex items-center gap-3">
|
|
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-600 to-indigo-600 flex items-center justify-center flex-shrink-0 shadow-md shadow-blue-500/30 overflow-hidden ring-1 ring-white/50">
|
|
@if($logoPath)
|
|
<img src="{{ asset('storage/' . $logoPath) }}" alt="Logo" class="w-full h-full object-contain p-1">
|
|
@else
|
|
<div class="w-full h-full flex items-center justify-center text-white">
|
|
<i class="fa-solid fa-shield-halved"></i>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
<div class="min-w-0">
|
|
<p class="text-[15px] font-extrabold text-slate-900 leading-tight truncate max-w-[150px] tracking-tight">{{ $appName }}</p>
|
|
<p class="text-[10px] font-semibold text-slate-400 truncate max-w-[150px] mt-0.5">{{ $schoolName }}</p>
|
|
</div>
|
|
</a>
|
|
<button @click="sidebarOpen = false" class="lg:hidden text-slate-400 hover:text-slate-600 flex-shrink-0 ml-2 mt-0.5 hover:bg-slate-100 rounded-lg p-1.5 transition">
|
|
<i class="fa-solid fa-xmark text-lg"></i>
|
|
</button>
|
|
</div>
|
|
<nav class="mt-3 px-3 space-y-0.5 flex-1 overflow-y-auto" style="scrollbar-width:thin;">
|
|
@canPermission('access-dashboard')
|
|
<x-nav-item href="{{ route('dashboard') }}" icon="home" :active="request()->routeIs('dashboard')">Dashboard</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('input-violations')
|
|
<x-nav-item href="{{ route('violations.create') }}" icon="plus-circle" :active="request()->routeIs('violations.create')">Input Pelanggaran</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('face-scan')
|
|
<x-nav-item href="{{ route('face.scan') }}" icon="face-scan" :active="request()->routeIs('face.scan')">Scan Wajah</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('view-violations')
|
|
<x-nav-item href="{{ route('violations.index') }}" icon="exclamation-triangle" :active="request()->routeIs('violations.index')">Data Pelanggaran</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('view-students')
|
|
<x-nav-item href="{{ route('students.index') }}" icon="users" :active="request()->routeIs('students.index*')">Data Siswa</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('manage-attendance')
|
|
<x-nav-item href="{{ route('attendances.index') }}" icon="clipboard-check" :active="request()->routeIs('attendances*')">Presensi Siswa</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('view-sp-letters')
|
|
<x-nav-item href="{{ route('sp-letters.index') }}" icon="document-text" :active="request()->routeIs('sp-letters.*')">Surat Peringatan</x-nav-item>
|
|
@endcanPermission
|
|
|
|
@canPermission('violations-export')
|
|
<x-nav-item href="{{ route('reports.violations') }}" icon="file-lines" :active="request()->routeIs('reports.violations*')">Laporan</x-nav-item>
|
|
@endcanPermission
|
|
|
|
@canAnyPermission('categories-manage', 'violation-types-manage', 'thresholds-manage', 'face-register')
|
|
<div class="pt-4 mt-4 border-t border-slate-100">
|
|
<p class="px-3 text-[10px] font-bold text-slate-400 uppercase tracking-[0.14em]">Master Data</p>
|
|
</div>
|
|
@endcanAnyPermission
|
|
@canPermission('face-register')
|
|
<x-nav-item href="{{ route('face.register') }}" icon="user-plus" :active="request()->routeIs('face.register')">Registrasi Wajah</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('categories-manage')
|
|
<x-nav-item href="{{ route('settings.categories') }}" icon="tag" :active="request()->routeIs('settings.categories*')">Kategori Pelanggaran</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('violation-types-manage')
|
|
<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.handling-types') }}" icon="hand-holding-heart" :active="request()->routeIs('settings.handling-types*')">Jenis Penanganan</x-nav-item>
|
|
<x-nav-item href="{{ route('settings.homeroom') }}" icon="chalkboard-user" :active="request()->routeIs('settings.homeroom*')">Wali Kelas</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('thresholds-manage')
|
|
<x-nav-item href="{{ route('settings.thresholds') }}" icon="chart-bar" :active="request()->routeIs('settings.thresholds*')">Ambang SP</x-nav-item>
|
|
@endcanPermission
|
|
|
|
@canPermission('settings-manage')
|
|
<div class="pt-4 mt-4 border-t border-slate-100">
|
|
<p class="px-3 text-[10px] font-bold text-slate-400 uppercase tracking-[0.14em]">Administrasi</p>
|
|
</div>
|
|
@endcanPermission
|
|
@canPermission('view-point-audit')
|
|
<x-nav-item href="{{ route('point-audit.index') }}" icon="clock-rotate-left" :active="request()->routeIs('point-audit.*')">Riwayat Poin</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('parents-verify')
|
|
<x-nav-item href="{{ route('parents.index') }}" icon="people-roof" :active="request()->routeIs('parents.index')">Data Wali Murid</x-nav-item>
|
|
<x-nav-item href="{{ route('parents.verification') }}" icon="user-check" :active="request()->routeIs('parents.verification')">Verifikasi Wali</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('manage-school-info')
|
|
<x-nav-item href="{{ route('settings.school-info') }}" icon="bullhorn" :active="request()->routeIs('settings.school-info*')">Informasi Sekolah</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('sync-data')
|
|
<x-nav-item href="{{ route('settings.sync') }}" icon="refresh" :active="request()->routeIs('settings.sync*')">Sinkronisasi</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('users-manage')
|
|
<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.permissions') }}" icon="lock" :active="request()->routeIs('settings.permissions*')">Hak Akses Role</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('backup-database')
|
|
<x-nav-item href="{{ route('settings.backup') }}" icon="database" :active="request()->routeIs('settings.backup*')">Backup Database</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('reset-application')
|
|
<x-nav-item href="{{ route('settings.reset') }}" icon="arrows-rotate" :active="request()->routeIs('settings.reset*')">Reset Aplikasi</x-nav-item>
|
|
@endcanPermission
|
|
@canPermission('settings-manage')
|
|
<x-nav-item href="{{ route('settings.index') }}" icon="cog" :active="request()->routeIs('settings.index')">Pengaturan</x-nav-item>
|
|
@endcanPermission
|
|
</nav>
|
|
|
|
{{-- Footer --}}
|
|
<div class="mt-auto px-3 py-3 border-t border-slate-100">
|
|
<div class="rounded-xl bg-slate-50/90 border border-slate-100 px-3 py-2.5 text-center">
|
|
<p class="text-[10px] font-medium text-slate-400">Developed by</p>
|
|
<a href="https://noctkj.net/" target="_blank" rel="noopener noreferrer"
|
|
class="text-xs font-extrabold text-blue-600 hover:text-blue-800 transition block mt-0.5 tracking-tight">
|
|
<i class="fa-solid fa-code mr-1 text-[10px]"></i>NOCTKJ.net
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
{{-- Overlay for mobile --}}
|
|
<div @click="sidebarOpen = false" x-show="sidebarOpen" class="fixed inset-0 z-40 bg-transparent lg:hidden" style="display: none;"></div>
|
|
|
|
{{-- Main Content --}}
|
|
<div class="flex-1 flex flex-col min-w-0">
|
|
{{-- Topbar --}}
|
|
<header class="sticky top-0 z-30 h-16 bg-white/80 backdrop-blur-md border-b border-slate-200/80 flex items-center justify-between px-4 lg:px-6">
|
|
<button @click="sidebarOpen = true" class="lg:hidden text-slate-500 hover:text-slate-700 p-2 -ml-2 rounded-lg hover:bg-slate-100 transition">
|
|
<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-slate-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 || '#'" @click="markAsRead(notif.id)" 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 class="px-4 py-2 border-t border-gray-100 text-center">
|
|
<a href="{{ route('notifications.index') }}" class="text-xs text-blue-600 hover:text-blue-800 font-medium">Tampilkan semua notifikasi</a>
|
|
</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>
|
|
<i class="fa-solid fa-chevron-down"></i>
|
|
</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-slate-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">
|
|
<i class="fa-solid fa-right-from-bracket"></i>
|
|
Logout
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{{-- Page Content --}}
|
|
<main class="flex-1 p-4 lg:p-6 animate-fade-in">
|
|
@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>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/id.js"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
flatpickr.localize(flatpickr.l10ns.id);
|
|
document.querySelectorAll('input[type="date"]').forEach(function(el) {
|
|
el.type = 'text';
|
|
flatpickr(el, {
|
|
dateFormat: 'Y-m-d',
|
|
altFormat: 'd/m/Y',
|
|
altInput: true,
|
|
allowInput: true,
|
|
animate: true,
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
|
|
@stack('scripts')
|
|
|
|
{{-- Reverb Config --}}
|
|
<script>
|
|
window.__REVERB_CONFIG = {
|
|
key: @json(config('reverb.apps.apps.0.key') ?: env('REVERB_APP_KEY')),
|
|
host: window.location.hostname,
|
|
port: parseInt(window.location.port || '80'),
|
|
};
|
|
console.log('Reverb config:', window.__REVERB_CONFIG);
|
|
</script>
|
|
|
|
{{-- Reverb + Echo for realtime --}}
|
|
<script>
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data('notifications', () => ({
|
|
showNotif: false,
|
|
sidebarOpen: false,
|
|
showUserMenu: false,
|
|
notifications: [],
|
|
notifCount: 0,
|
|
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;
|
|
},
|
|
markAsRead(id) {
|
|
fetch('/notifications/' + id + '/read', { method: 'POST', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content } });
|
|
this.notifications = this.notifications.filter(n => n.id !== id);
|
|
if (this.notifCount > 0) this.notifCount--;
|
|
},
|
|
connectReverb() {
|
|
if (typeof window.Echo === 'undefined' || window.Echo === null) {
|
|
try {
|
|
window.Echo = new window.EchoClass({
|
|
broadcaster: 'reverb',
|
|
key: window.__REVERB_CONFIG.key,
|
|
wsHost: window.__REVERB_CONFIG.host,
|
|
wsPort: window.__REVERB_CONFIG.port,
|
|
wssPort: 443,
|
|
forceTLS: false,
|
|
enabledTransports: ['ws', 'wss'],
|
|
});
|
|
console.log('Echo initialized ✅');
|
|
} catch(e) {
|
|
console.error('Echo init error:', e);
|
|
}
|
|
}
|
|
if (typeof window.Echo !== 'undefined') {
|
|
window.Echo.channel('violations')
|
|
.listen('.violation.recorded', (e) => {
|
|
// Refresh dari database untuk hindari duplikasi
|
|
this.fetchCount();
|
|
this.fetchNotifications();
|
|
});
|
|
} else {
|
|
// Fallback: poll every 30s if Echo not loaded
|
|
setInterval(() => { this.fetchCount(); }, 30000);
|
|
}
|
|
}
|
|
}));
|
|
});
|
|
</script>
|
|
|
|
{{-- Global helpers --}}
|
|
<script>
|
|
window.confirmSwal = async function (opts = {}) {
|
|
const result = await Swal.fire({
|
|
title: opts.title || 'Konfirmasi',
|
|
text: opts.text || 'Yakin ingin melanjutkan?',
|
|
icon: opts.icon || 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: opts.confirmColor || '#dc2626',
|
|
cancelButtonColor: '#6b7280',
|
|
confirmButtonText: opts.confirmText || 'Ya, hapus!',
|
|
cancelButtonText: 'Batal',
|
|
reverseButtons: true,
|
|
});
|
|
return result.isConfirmed;
|
|
};
|
|
</script>
|
|
{{-- PWA: Service Worker --}}
|
|
<script>
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', () => {
|
|
navigator.serviceWorker.register('/sw.js')
|
|
.catch(err => console.error('SW registration failed:', err));
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|