Files

2374 lines
104 KiB
PHP

{{--
/**
* ============================================================
* @project biiproject
* @author Andika Debi Putra
* @email andikadebiputra@gmail.com
* @website https://biiproject.com
* @copyright Copyright (c) 2026 Andika Debi Putra
* @license Proprietary - All Rights Reserved
* @version 1.0.0
* @created 2026-05-01
* ============================================================
*
* Unauthorized copying, modification, distribution, or use
* of this file is strictly prohibited without prior written
* permission from the author.
* ============================================================
*/
--}}
<!DOCTYPE html>
<html lang="en">
<head>
{{-- meta --}}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="user-id" content="{{ auth()->id() }}">
<title>{{ $app_name ?? config('app.name', 'Laravel') }}</title>
<link id="dynamic-favicon" rel="icon" type="image/png"
href="{{ asset($app_favicon ?? 'assets/img/favicon.png') }}?v={{ file_exists(public_path($app_favicon ?? '')) ? filemtime(public_path($app_favicon ?? '')) : time() }}">
{{-- font --}}
@vite(['resources/js/app.js'])
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700;800;900&display=swap"
rel="stylesheet">
{{-- dynamic theme logic --}}
@php
$mobileConfig = app(\App\Services\MobileConfig\MobileConfigService::class)->all();
$primaryHex = $mobileConfig['branding']['theme_color_primary'] ?? '#1e1e1e';
// Helper to convert hex to rgb
$hex = str_replace('#', '', $primaryHex);
if(strlen($hex) == 3) {
$r = hexdec(substr($hex,0,1).substr($hex,0,1));
$g = hexdec(substr($hex,1,1).substr($hex,1,1));
$b = hexdec(substr($hex,2,1).substr($hex,2,1));
} else {
$r = hexdec(substr($hex,0,2));
$g = hexdec(substr($hex,2,2));
$b = hexdec(substr($hex,4,2));
}
// Helper to convert rgb to hsl
$r_prime = $r / 255;
$g_prime = $g / 255;
$b_prime = $b / 255;
$max = max($r_prime, $g_prime, $b_prime);
$min = min($r_prime, $g_prime, $b_prime);
$l = ($max + $min) / 2;
$h = $s = 0;
if ($max != $min) {
$d = $max - $min;
$s = $l > 0.5 ? $d / (2 - $max - $min) : $d / ($max + $min);
switch ($max) {
case $r_prime: $h = ($g_prime - $b_prime) / $d + ($g_prime < $b_prime ? 6 : 0); break;
case $g_prime: $h = ($b_prime - $r_prime) / $d + 2; break;
case $b_prime: $h = ($r_prime - $g_prime) / $d + 4; break;
}
$h /= 6;
}
$h = round($h * 360);
$s = round($s * 100);
$l = round($l * 100);
@endphp
{{-- custom root --}}
<style>
:root {
--adminuiux-content-font: "Inter", sans-serif;
--adminuiux-title-font: "Outfit", sans-serif;
/* Dynamic Theme Variables */
--adminuiux-theme-1-h: {{ $h }};
--adminuiux-theme-1-s: {{ $s }}%;
--adminuiux-theme-1-l: {{ $l }}%;
--adminuiux-theme-1: hsl(var(--adminuiux-theme-1-h), var(--adminuiux-theme-1-s), var(--adminuiux-theme-1-l));
--adminuiux-theme-1-rgb: {{ $r }}, {{ $g }}, {{ $b }};
/* Theme variations */
--adminuiux-theme-1-subtle: hsla(var(--adminuiux-theme-1-h), var(--adminuiux-theme-1-s), var(--adminuiux-theme-1-l), 0.1);
--adminuiux-theme-1-deep: hsl(var(--adminuiux-theme-1-h), var(--adminuiux-theme-1-s), calc(var(--adminuiux-theme-1-l) - 10%));
}
.notification-clickable {
cursor: pointer;
transition: transform 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
}
.notification-clickable:hover {
background-color: rgba(var(--adminuiux-theme-1-rgb), 0.03) !important;
transform: translateY(-1px);
}
.notification-clickable:active {
transform: translateY(0);
}
.notification-clickable .btn, .notification-clickable a {
position: relative;
z-index: 2;
}
/* DATA TABLES MODERNIZATION */
.dataTables_filter, .dt-search {
display: none !important;
}
table.dataTable {
border-collapse: separate !important;
border-spacing: 0 !important;
margin-top: 1rem !important;
margin-bottom: 1rem !important;
border: 0 !important;
}
table.dataTable.no-footer {
border-bottom: 0 !important;
}
table.dataTable thead th {
background-color: #fff !important;
color: #64748b !important;
font-size: 0.75rem !important;
text-transform: uppercase !important;
letter-spacing: 0.025em !important;
font-weight: 700 !important;
padding: 0.75rem 0.75rem !important;
border-bottom: 1px solid #f1f5f9 !important;
border-top: 0 !important;
}
table.dataTable thead th:first-child,
table.dataTable tbody td:first-child {
padding-left: 2rem !important;
}
table.dataTable thead th:last-child,
table.dataTable tbody td:last-child {
padding-right: 2rem !important;
}
table.dataTable thead tr.filter-row th {
padding: 0.4rem 0.75rem 0.8rem 0.75rem !important;
background-color: #fff !important;
border-bottom: 1px solid #f1f5f9 !important;
}
table.dataTable tbody td {
padding: 0.6rem 0.75rem !important;
border-bottom: 1px solid #f8fafc !important;
color: #1e293b !important;
font-size: 0.875rem !important;
}
table.dataTable tbody tr:hover {
background-color: #fcfcfc !important;
}
.dataTables_info {
font-size: 0.8125rem !important;
color: #64748b !important;
padding-top: 1.5rem !important;
}
.dataTables_length label {
font-size: 0.8125rem !important;
color: #64748b !important;
}
.dataTables_length select {
border-radius: 20px !important;
padding: 0.25rem 0.75rem !important;
border: 1px solid #e2e8f0 !important;
margin: 0 0.5rem !important;
}
/* Premium Seamless Button Group */
.dt-buttons {
display: inline-flex !important;
gap: 0 !important;
margin-bottom: 1rem !important;
}
.dt-buttons .btn {
padding: 0.4rem 1.25rem !important;
font-size: 0.8125rem !important;
font-weight: 500 !important;
border: 1px solid #e2e8f0 !important;
background: #fff !important;
color: #475569 !important;
transition: all 0.2s ease;
margin: 0 !important;
margin-left: -1px !important;
border-radius: 0 !important;
position: relative;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
line-height: 1 !important;
}
.dt-buttons .btn:hover {
background: #f8fafc !important;
color: #0f172a !important;
border-color: #cbd5e1 !important;
z-index: 5 !important;
}
.dt-buttons .btn:first-child,
.dt-buttons .btn-group:first-child .btn:first-child {
border-top-left-radius: 100px !important;
border-bottom-left-radius: 100px !important;
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
margin-left: 0 !important;
padding-left: 2rem !important;
}
.dt-buttons .btn:last-child,
.dt-buttons .btn-group:last-child .btn:last-child {
border-top-right-radius: 100px !important;
border-bottom-right-radius: 100px !important;
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
padding-right: 2rem !important;
}
/* Final Aggressive Fix for ColVis dropdown visibility */
.dt-button-collection {
z-index: 9999 !important;
background-color: #ffffff !important;
border: 1px solid #e2e8f0 !important;
border-radius: 12px !important;
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1) !important;
padding: 10px !important;
min-width: 200px !important;
}
.dt-button-collection .dt-button,
.dt-button-collection .dropdown-item {
color: #1e293b !important;
background: transparent !important;
padding: 8px 15px !important;
font-size: 0.8125rem !important;
border-radius: 6px !important;
border: none !important;
display: block !important;
width: 100% !important;
text-align: left !important;
position: relative !important;
}
.dt-button-collection .dt-button span {
color: inherit !important;
}
.dt-button-collection .dt-button:hover,
.dt-button-collection .dropdown-item:hover {
background-color: #f1f5f9 !important;
color: #0f172a !important;
}
.dt-button-collection .dt-button.active,
.dt-button-collection .dropdown-item.active {
background-color: #f1f5f9 !important;
color: #1e1e1e !important;
font-weight: 700 !important;
}
/* Ensure the text is visible */
.dt-button-collection .dt-button-text,
.buttons-columnVisibility .dt-button-text {
color: #1e293b !important;
display: inline !important;
}
.dataTables_paginate {
padding-top: 1.5rem !important;
}
.dataTables_paginate .paginate_button {
border: 0 !important;
background: transparent !important;
color: #64748b !important;
font-size: 0.8125rem !important;
font-weight: 600 !important;
margin: 0 2px !important;
border-radius: 50% !important;
width: 32px !important;
height: 32px !important;
padding: 0 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
}
.dataTables_paginate .paginate_button:hover {
background: #f1f5f9 !important;
color: #1e293b !important;
}
.dataTables_paginate .paginate_button.current {
background: #1e1e1e !important;
color: #fff !important;
}
/* Custom Checkbox/Toggle styling in table */
table.dataTable .form-check-input:checked {
background-color: #1e1e1e;
border-color: #1e1e1e;
}
body {
font-family: var(--adminuiux-content-font);
color: #1e293b;
background-color: #f8fafc;
}
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
font-family: var(--adminuiux-title-font);
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: 0.5rem;
line-height: 1.2;
}
.card, .adminuiux-card {
background-color: rgba(255, 255, 255, 0.8) !important;
backdrop-filter: blur(12px) saturate(180%) !important;
-webkit-backdrop-filter: blur(12px) saturate(180%) !important;
border: 1px solid rgba(255, 255, 255, 0.3) !important;
border-radius: 20px !important;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.02) !important;
}
.adminuiux-sidebar {
background: rgba(255, 255, 255, 0.7) !important;
backdrop-filter: blur(25px) saturate(200%) !important;
-webkit-backdrop-filter: blur(25px) saturate(200%) !important;
border-right: 1px solid rgba(0, 0, 0, 0.05) !important;
}
.adminuiux-sidebar .nav-link {
border-radius: 12px !important;
margin: 2px 15px !important;
padding: 10px 15px !important;
transition: all 0.2s ease !important;
color: #64748b !important;
font-weight: 500 !important;
}
.adminuiux-sidebar .nav-link:hover {
background: rgba(0, 0, 0, 0.03) !important;
color: #1e293b !important;
transform: translateX(4px) !important;
}
.adminuiux-sidebar .nav-link.active {
background: var(--adminuiux-theme-1) !important;
color: {{ $l > 60 ? '#1a1a1a' : '#ffffff' }} !important;
box-shadow: 0 8px 15px -5px hsla(var(--adminuiux-theme-1-h), var(--adminuiux-theme-1-s), var(--adminuiux-theme-1-l), 0.4) !important;
}
.adminuiux-sidebar .nav-link.active .menu-icon {
color: inherit !important;
}
.swal2-popup {
border-radius: 30px !important;
padding: 2.2rem !important;
font-family: var(--adminuiux-content-font) !important;
}
.swal2-title {
font-family: var(--adminuiux-title-font) !important;
font-weight: 600 !important;
color: #1e1e1e !important;
}
.swal2-actions {
gap: 15px !important;
margin-top: 1.5rem !important;
}
/* Standardized Modern Pill Buttons */
.btn-theme-1 {
background-color: var(--adminuiux-theme-1) !important;
color: {{ $l > 60 ? '#1a1a1a' : '#ffffff' }} !important;
border: 2px solid var(--adminuiux-theme-1) !important;
border-radius: 50px !important;
padding: 10px 28px !important;
font-weight: 600 !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
.btn-theme-1:hover {
background-color: var(--adminuiux-theme-1-deep) !important;
border-color: var(--adminuiux-theme-1-deep) !important;
transform: translateY(-2px) !important;
box-shadow: 0 10px 20px -5px hsla(var(--adminuiux-theme-1-h), var(--adminuiux-theme-1-s), var(--adminuiux-theme-1-l), 0.4) !important;
}
.btn-pill-primary {
background-color: #111827 !important;
color: #ffffff !important;
border: 1.5px solid #111827 !important;
border-radius: 50px !important;
padding: 12px 32px !important;
font-weight: 600 !important;
font-family: var(--adminuiux-title-font) !important;
transition: all 0.2s ease !important;
min-width: 120px !important;
font-size: 15px !important;
margin: 0 !important;
}
.btn-pill-primary:hover {
background-color: #000000 !important;
border-color: #000000 !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
}
.btn-pill-danger {
background-color: #dc3545 !important;
color: #ffffff !important;
border: 2px solid #dc3545 !important;
border-radius: 50px !important;
padding: 12px 32px !important;
font-weight: 600 !important;
font-family: var(--adminuiux-title-font) !important;
transition: all 0.2s ease !important;
min-width: 120px !important;
font-size: 15px !important;
margin: 0 !important;
}
.btn-pill-danger:hover {
background-color: #a71d2a !important;
border-color: #a71d2a !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.2) !important;
}
.btn-pill-cancel {
background-color: #ffffff !important;
color: #1e1e1e !important;
border: 2px solid #1e1e1e !important;
border-radius: 50px !important;
padding: 12px 32px !important;
font-weight: 600 !important;
font-family: var(--adminuiux-title-font) !important;
transition: all 0.2s ease !important;
min-width: 120px !important;
font-size: 15px !important;
margin: 0 !important;
}
.btn-pill-cancel:hover {
background-color: #f8f9fa !important;
color: #000000 !important;
border-color: #000000 !important;
}
/* Target specific SWAL classes to override defaults */
.swal2-confirm.btn-pill-primary,
.swal2-confirm.btn-pill-danger {
box-shadow: none !important;
}
.swal2-cancel.btn-pill-cancel {
box-shadow: none !important;
}
/* GLOBAL FIX: Choices.js UI - Professional Pill Design */
.choices {
margin-bottom: 0 !important;
overflow: visible !important;
}
.choices__inner {
background-color: #ffffff !important;
border: 1px solid #ced4da !important;
border-radius: 50rem !important; /* Full pill shape */
padding: 8px 25px !important; /* Spacious padding */
min-height: 48px !important;
display: flex !important;
align-items: center !important;
width: 100% !important;
box-sizing: border-box !important;
transition: all 0.2s ease !important;
}
.choices.is-focused .choices__inner {
border-color: #1e1e1e !important; /* Professional dark focus */
box-shadow: 0 0 0 0.2rem rgba(0, 0, 0, 0.05) !important;
}
.choices__list--multiple .choices__item {
background-color: #f1f1f1 !important;
border: 1px solid #e2e2e2 !important;
color: #1e1e1e !important;
border-radius: 50rem !important;
padding: 2px 14px !important;
font-weight: 600 !important;
font-size: 0.8rem !important;
margin-top: 2px !important;
margin-bottom: 2px !important;
}
.choices__list--multiple .choices__item .choices__button {
border-left: 1px solid #d1d1d1 !important;
margin-left: 10px !important;
filter: grayscale(1);
}
.choices__placeholder {
opacity: 0.6 !important;
color: #6c757d !important;
}
.choices__list--dropdown {
z-index: 1060 !important;
border-radius: 1.2rem !important;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2) !important;
border: 1px solid #f1f1f1 !important;
background-color: #ffffff !important;
}
.choices__list--dropdown .choices__item--selectable.is-highlighted {
background-color: #1e1e1e !important;
color: #ffffff !important;
}
.choices__list--multiple .choices__item {
background-color: #1e1e1e !important;
border: 1px solid #1e1e1e !important;
color: #ffffff !important;
border-radius: 50px !important;
padding: 4px 14px !important;
font-weight: 600 !important;
font-size: 0.85rem !important;
margin-top: 2px !important;
margin-bottom: 2px !important;
}
.choices__list--multiple .choices__item .choices__button {
border-left: 1px solid rgba(255, 255, 255, 0.3) !important;
margin-left: 8px !important;
opacity: 0.8 !important;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='white' d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E") !important;
filter: none !important;
}
.btn-primary {
--bs-btn-color: #fff !important;
--bs-btn-bg: #1e1e1e !important;
--bs-btn-border-color: #1e1e1e !important;
--bs-btn-hover-color: #fff !important;
--bs-btn-hover-bg: #000 !important;
--bs-btn-hover-border-color: #000 !important;
--bs-btn-focus-shadow-rgb: 30, 30, 30 !important;
--bs-btn-active-color: #fff !important;
--bs-btn-active-bg: #000 !important;
--bs-btn-active-border-color: #000 !important;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125) !important;
--bs-btn-disabled-color: #fff !important;
--bs-btn-disabled-bg: #1e1e1e !important;
--bs-btn-disabled-border-color: #1e1e1e !important;
}
/* Soft High-Density Action Buttons */
.btn-square {
width: 34px !important;
height: 34px !important;
padding: 0 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
border-radius: 10px !important;
border: none !important;
}
.btn-light-primary {
background-color: #f0f0f0 !important;
color: #1e1e1e !important;
}
.btn-light-primary:hover {
background-color: #1e1e1e !important;
color: #ffffff !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
}
.btn-light-danger {
background-color: #fff5f5 !important;
color: #dc3545 !important;
}
.btn-light-danger:hover {
background-color: #dc3545 !important;
color: #ffffff !important;
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.15) !important;
}
.btn-light-warning {
background-color: #fff9e6 !important;
color: #ffc107 !important;
}
.btn-light-warning:hover {
background-color: #ffc107 !important;
color: #ffffff !important;
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.15) !important;
}
/* New Specific Styles to Match Reference Image */
.btn-pill-standard-primary {
background-color: #ffffff !important;
color: #212529 !important;
border: 1px solid #212529 !important;
border-radius: 50rem !important;
padding: 8px 24px !important;
font-weight: 500 !important;
font-family: var(--adminuiux-title-font) !important;
min-width: 110px !important;
transition: all 0.2s ease !important;
}
.btn-pill-standard-primary:hover {
background-color: #f8f9fa !important;
border-color: #000000 !important;
}
.btn-pill-standard-secondary {
background-color: #ffffff !important;
color: #212529 !important;
border: 1px solid #212529 !important;
border-radius: 50rem !important;
padding: 8px 24px !important;
font-weight: 500 !important;
font-family: var(--adminuiux-title-font) !important;
min-width: 110px !important;
transition: all 0.2s ease !important;
}
.btn-pill-standard-secondary:hover {
background-color: #f8f9fa !important;
}
.text-primary {
color: var(--adminuiux-theme-1) !important;
}
.bg-primary {
background-color: var(--adminuiux-theme-1) !important;
}
.border-primary {
border-color: var(--adminuiux-theme-1) !important;
}
.btn-outline-primary {
--bs-btn-color: var(--adminuiux-theme-1) !important;
--bs-btn-border-color: var(--adminuiux-theme-1) !important;
--bs-btn-hover-color: {{ $l > 60 ? '#1a1a1a' : '#ffffff' }} !important;
--bs-btn-hover-bg: var(--adminuiux-theme-1) !important;
--bs-btn-hover-border-color: var(--adminuiux-theme-1) !important;
}
.dt-buttons {
float: right !important;
}
div.dt-button-collection {
border-radius: 12px;
padding: 6px;
}
div.dt-button-collection button.dt-button {
font-size: 14px;
line-height: 1.3;
}
.permission-clamp {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.permission-clamp .badge {
margin-right: 4px;
margin-bottom: 2px;
}
.badge-slate-pill {
background-color: #5f6d7a !important;
color: #ffffff !important;
border-radius: 50rem !important;
padding: 0.35em 0.85em !important;
font-weight: 500 !important;
font-size: 0.75rem !important;
display: inline-block;
line-height: 1.2;
}
/* html {
zoom: 0.8;
} */
/* Fixed Full Screen Overlays (Loaders, Backdrops, etc.) must cover 100% of viewport */
/* .pageloader,
.modal-backdrop,
.offcanvas-backdrop,
.swal2-container,
.select2-container--open,
.choices__list--dropdown {
zoom: 1.25;
} */
/* Global 100% Fade-In Policy - Without Exception */
.adminuiux-content,
.card,
.modal-content,
.offcanvas,
.dropdown-menu,
.adminuiux-card,
.table-responsive,
.list-group-item {
animation: fadeIn 0.6s ease-out both !important;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Ensure Animate.css compatibility for these elements if they use classes */
.animate__fadeIn {
animation-name: fadeIn !important;
}
/* Professional Fixed Footer */
.adminuiux-footer {
position: fixed !important;
bottom: 0;
left: 0;
right: 0;
width: 100%;
background: rgba(255, 255, 255, 0.7) !important;
backdrop-filter: blur(20px) saturate(180%) !important;
-webkit-backdrop-filter: blur(20px) saturate(180%) !important;
border-top: 1px solid rgba(0, 0, 0, 0.05);
z-index: 1000;
padding: 12px 0;
}
/* .adminuiux-content {
padding-bottom: 80px !important;
} */
/* Unified Global Modal Policy - Majestic UI */
.modal {
z-index: 1060 !important;
/* Level parity with SweetAlert2 */
}
.modal-backdrop {
z-index: 1050 !important;
}
.modal-backdrop.show,
.swal2-container.swal2-backdrop-show {
opacity: 1 !important;
background-color: rgba(0, 0, 0, 0.10) !important;
backdrop-filter: blur(3px) !important;
-webkit-backdrop-filter: blur(3px) !important;
}
/* CRITICAL: Fix for modal trapped behind backdrop due to stacking context */
body.modal-open .adminuiux-content,
body.modal-open .adminuiux-content>.container-fluid>.row>div {
animation: none !important;
/* Stop animation to break stacking context */
transform: none !important;
opacity: 1 !important;
filter: none !important;
isolation: auto !important;
will-change: auto !important;
}
.modal-dialog {
display: flex !important;
align-items: flex-start !important;
justify-content: center !important;
margin-top: 4rem !important;
margin-left: auto;
margin-right: auto;
width: 850px;
max-width: 94% !important;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) !important;
}
@media (min-width: 992px) {
.modal-dialog {
width: 850px !important;
}
}
/* Large modal override — applies globally to any .modal-xl */
.modal-dialog.modal-xl,
.modal-dialog.modal-permission {
width: calc(70vw - 40px) !important;
max-width: calc(70vw - 40px) !important;
margin-top: 20px !important;
}
@media (min-width: 992px) {
.modal-dialog.modal-xl,
.modal-dialog.modal-permission {
width: calc(70vw - 40px) !important;
max-width: calc(70vw - 40px) !important;
}
}
.modal-content {
border: none !important;
border-radius: 20px !important;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.35) !important;
background: #ffffff;
overflow: clip !important;
}
.modal-content .modal-header {
border-radius: 20px 20px 0 0 !important;
}
.modal-content .modal-footer {
border-radius: 0 0 20px 20px !important;
}
.modal.fade .modal-dialog {
transform: translateY(-20px) !important;
}
.modal.show .modal-dialog {
transform: translateY(0) !important;
}
/* Floating Top Impersonation Pill */
.impersonate-floating-pill {
position: fixed;
top: 12px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 193, 7, 0.9);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
color: #000;
z-index: 3000;
display: flex;
align-items: center;
gap: 12px;
padding: 6px 12px 6px 18px;
border-radius: 50px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
font-weight: 700;
text-transform: uppercase;
font-size: 0.7rem;
letter-spacing: 0.5px;
white-space: nowrap;
}
.impersonate-floating-pill .btn-end {
background: #000;
color: #fff;
border: none;
border-radius: 50px;
padding: 4px 12px;
font-size: 0.65rem;
transition: all 0.2s ease;
}
.impersonate-floating-pill .btn-end:hover {
background: #222;
transform: scale(1.05);
}
@media (max-width: 576px) {
.impersonate-floating-pill {
top: 4px;
padding: 4px 8px 4px 12px;
font-size: 0.6rem;
}
}
/* Target User Security Notice (Centered Red Pill) */
.security-awareness-pill {
position: fixed;
top: 12px;
left: 50%;
transform: translateX(-50%);
background: rgba(220, 53, 69, 0.9);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
color: #fff;
z-index: 3000;
display: flex;
align-items: center;
gap: 12px;
padding: 8px 24px;
border-radius: 50px;
box-shadow: 0 4px 20px rgba(220, 53, 69, 0.3);
border: 1px solid rgba(255, 255, 255, 0.3);
font-weight: 700;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
white-space: nowrap;
/* animation: pulse-danger-majestic 2s infinite; */
}
@keyframes pulse-danger-majestic {
0% {
transform: translate(-50%, 0) scale(1);
box-shadow: 0 4px 20px rgba(220, 53, 69, 0.3);
}
50% {
transform: translate(-50%, 0) scale(1.03);
box-shadow: 0 10px 40px rgba(220, 53, 69, 0.5);
}
100% {
transform: translate(-50%, 0) scale(1);
box-shadow: 0 4px 20px rgba(220, 53, 69, 0.3);
}
}
@media (max-width: 576px) {
.security-awareness-pill {
top: 4px;
padding: 6px 16px;
font-size: 0.6rem;
width: 90%;
justify-content: center;
}
}
</style>
{{-- apps css --}}
<link rel="stylesheet" href="{{ asset('assets/css/app.css') }}">
{{-- markdown --}}
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/2.3.8/css/dataTables.bootstrap5.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" />
<style>
/* 🔍 SMART SEARCH (CMD+K) STYLES */
#global-search-modal .modal-content {
background: rgba(255, 255, 255, 0.9) !important;
backdrop-filter: blur(20px) saturate(180%) !important;
-webkit-backdrop-filter: blur(20px) saturate(180%) !important;
border: 1px solid rgba(255, 255, 255, 0.5) !important;
border-radius: 24px !important;
}
.search-input-wrapper {
position: relative;
padding: 20px;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
#global-search-input {
border: none !important;
background: transparent !important;
font-size: 1.25rem !important;
font-weight: 500 !important;
box-shadow: none !important;
padding-left: 40px !important;
}
.search-icon-fixed {
position: absolute;
left: 32px;
top: 50%;
transform: translateY(-50%);
font-size: 1.2rem;
color: #adb5bd;
}
.search-results-list {
max-height: 450px;
overflow-y: auto;
padding: 10px;
}
.search-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-radius: 14px;
text-decoration: none;
color: inherit;
transition: all 0.2s;
margin-bottom: 4px;
}
.search-item:hover, .search-item.active {
background: rgba(0,0,0,0.04);
transform: translateX(5px);
}
.search-item .icon-box {
width: 40px;
height: 40px;
background: #fff;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
}
.search-item .category-tag {
font-size: 0.65rem;
font-weight: 800;
text-transform: uppercase;
padding: 2px 8px;
border-radius: 50px;
background: #f1f5f9;
color: #64748b;
margin-left: auto;
}
.search-shortcut-hint {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(255,255,255,0.8);
backdrop-filter: blur(10px);
padding: 8px 16px;
border-radius: 50px;
font-size: 0.75rem;
font-weight: 700;
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
border: 1px solid rgba(0,0,0,0.05);
z-index: 999;
cursor: pointer;
transition: all 0.3s;
}
.search-shortcut-hint:hover { transform: scale(1.05); background: #fff; }
.kbd-capsule {
background: #f1f5f9;
border: 1px solid #cbd5e1;
border-radius: 4px;
padding: 1px 6px;
font-family: monospace;
font-size: 0.7rem;
box-shadow: 0 2px 0 #cbd5e1;
margin: 0 2px;
}
</style>
@stack('styles')
<style>[x-cloak]{display:none!important}</style>
<style>
.adminuiux-sidebar .nav-link .bi-chevron-down {
transition: transform 0.2s ease;
}
.adminuiux-sidebar .nav-link:not(.collapsed) .bi-chevron-down {
transform: rotate(180deg);
}
/*
* Align content card top edge with sidebar card top edge.
* Navbar (fixed-top): py-2 (16px) + logo 32px + boxed outer .5rem (8px) = ~56px.
* Sidebar-inner: margin-top .5rem = 8px → first visible pixel of sidebar card.
* Total = 56px + 8px = 64px.
*/
main.adminuiux-content {
padding-top: 64px !important;
}
main.adminuiux-content > .container-fluid {
margin-top: 20px !important;
padding-top: 0 !important;
}
</style>
</head>
<body
class="main-bg main-bg-opac roundedui adminuiux-header-boxed adminuiux-header-transparent adminuiux-sidebar-fill-white adminuiux-sidebar-boxed theme-black bg-gradient-1 scrollup {{ session()->has('impersonator_id') ? 'is-impersonating' : '' }}"
data-theme="theme-black" data-sidebarfill="adminuiux-sidebar-fill-white" data-bs-spy="scroll"
data-bs-target="#list-example" data-bs-smooth-scroll="true" tabindex="0"
data-sidebarlayout="adminuiux-sidebar-boxed" data-headerlayout="adminuiux-header-standard"
data-headerfill="adminuiux-header-transparent" data-bggradient="bg-gradient-1">
{{-- page loader --}}
<div class="pageloader">
<div class="container h-100">
<div class="row justify-content-center align-items-center text-center h-100">
<div class="col-12 mb-auto pt-4"></div>
<div class="col-auto">
<div class="loader5 mb-2 mx-auto"></div>
</div>
<div class="col-12 mt-auto pb-4">
<p class="text-secondary">Please wait for awesome things...</p>
</div>
</div>
</div>
</div>
{{-- header --}}
<header class="adminuiux-header">
<nav class="navbar navbar-expand-lg fixed-top">
<div class="container-fluid py-2">
{{-- sidebar toggle --}}
<button class="btn btn-link btn-square sidebar-toggler text-dark" type="button" onclick="initSidebar()">
<i class="sidebar-svg" data-feather="menu"></i>
</button>
{{-- logo --}}
<a class="navbar-brand d-flex align-items-center" href="{{ url('/') }}">
<div id="header-logo-container" class="me-2">
<img id="header-logo"
src="{{ asset($app_logo) }}?v={{ file_exists(public_path($app_logo ?? '')) ? filemtime(public_path($app_logo ?? '')) : time() }}"
alt="Logo" class="rounded" style="height: 32px; width: auto; object-fit: contain;"
onerror="this.parentElement.style.display='none';">
</div>
<div class="d-block ps-1">
<h6 class="mb-0 app-name-text">{{ $app_name ?? config('app.name', 'Laravel') }}</h6>
@if(!empty($app_tagline))
<p class="company-tagline small mb-0">{{ $app_tagline }}</p>
@endif
</div>
</a>
{{-- right icons --}}
<div class="ms-auto">
<button class="btn btn-link btn-square btnsunmoon btn-link-header d-none"
id="btn-layout-modes-dark-page">
<i class="sun mx-auto" data-feather="sun"></i>
<i class="moon mx-auto" data-feather="moon"></i>
</button>
<div class="dropdown d-none">
<button class="btn btn-link btn-square btn-icon btn-link-header dropdown-toggle no-caret"
type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-translate"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item active" data-value="EN">EN - English</a></li>
<li><a class="dropdown-item" data-value="ID">ID - Indonesia</a></li>
</ul>
</div>
<button class="btn btn-link btn-square btn-icon btn-link-header position-relative"
data-bs-toggle="offcanvas" data-bs-target="#view-notification" id="header-bell-btn">
<i data-feather="bell"></i>
<span class="position-absolute top-0 end-0 badge rounded-pill bg-danger p-1 d-none"
id="header-bell-badge">
<small id="header-badge-count">0</small>
</span>
</button>
</div>
</div>
</nav>
</header>
@include('layouts.navigation')
{{-- page content inside adminuiux-wrap so theme sidebar/content selectors apply --}}
<main class="adminuiux-content has-sidebar animate__animated animate__fadeIn" onclick="contentClick()">
{{ $slot }}
</main>
</div>{{-- /adminuiux-wrap --}}
{{-- impersonate alert (Floating Top Pill) --}}
@if(session()->has('impersonator_id'))
<div class="impersonate-floating-pill animate__animated animate__fadeIn">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-incognito fs-6"></i>
<span><span class="d-none d-sm-inline">{{ __('Acting as') }}:</span> {{ auth()->user()->email }}</span>
</div>
<form action="{{ route('impersonate.stop') }}" method="POST" class="m-0">
@csrf
<button type="submit" class="btn-end fw-bold">
{{ __('Stop') }}
</button>
</form>
</div>
@endif
{{-- security awareness (For the user BEING impersonated - ONLY show in non-admin session) --}}
@if(Auth::check() && !session()->has('impersonator_id'))
<div id="security-awareness-alert"
class="security-awareness-pill animate__animated animate__fadeIn {{ \Illuminate\Support\Facades\Cache::has('is_being_impersonated:' . Auth::id()) ? '' : 'd-none' }}">
<i class="bi bi-shield-lock-fill fs-6 animate__animated animate__flash animate__infinite animate__slow"></i>
<span>{{ __('Security Alert: Admin Access is Active') }}</span>
</div>
@endif
{{-- notification drawer --}}
<div class="offcanvas offcanvas-end shadow border-0 maxwidth-300 pt-ios" tabindex="-1" id="view-notification"
data-bs-scroll="true">
<div class="offcanvas-header border-bottom">
<div class="flex-grow-1">
<h6 class="mb-0">{{ __('Notifications') }}</h6>
<p class="small text-secondary" id="notification-stat-text">{{ __('0 unread updates') }}</p>
</div>
<button class="btn btn-sm btn-link text-success text-decoration-none me-2 d-none" id="header-mark-all-read">
{{ __('Mark all as read') }}
</button>
@hasanyrole('Developer|Administrator')
<button class="btn btn-sm btn-link text-danger text-decoration-none me-2 d-none" id="header-clear-read"
title="{{ __('Clear all read') }}">
<i class="bi bi-trash3"></i>
</button>
@endhasanyrole
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body px-0 pb-ios" id="header-notification-container">
<div class="text-center py-5">
<div class="spinner-border text-secondary spinner-border-sm" role="status"></div>
<p class="small text-secondary mt-2">{{ __('Loading...') }}</p>
</div>
</div>
<div class="offcanvas-footer p-3 border-top text-center">
<a href="{{ route('notification-center.index') }}"
class="btn btn-sm btn-outline-dark fw-semibold rounded-pill px-3">
{{ __('View Notification Center') }}
</a>
</div>
</div>
{{-- page footer --}}
{{-- footer --}}
<footer class="adminuiux-footer has-adminuiux-sidebar mt-auto d-none">
<div class="container-fluid">
<div class="row">
<div class="col-12 col-md col-lg py-3">
<span class="small">
{{ $footer_text }}
</span>
</div>
<div class="col-12 col-md-auto col-lg-auto align-self-center d-none">
<ul class="nav small">
<li class="nav-item"><a class="nav-link" href="#">Help</a></li>
<li class="nav-item">|</li>
<li class="nav-item"><a class="nav-link" href="#">Terms of Use</a></li>
<li class="nav-item">|</li>
<li class="nav-item"><a class="nav-link" href="#">Privacy Policy</a></li>
</ul>
</div>
</div>
</div>
</footer>
<div class="position-fixed bottom-0 end-0 m-3 z-index-5 d-none" id="fixedbuttons">
<button class="btn btn-square btn-theme shadow rounded-circle" type="button" data-bs-toggle="offcanvas"
data-bs-target="#theming" aria-controls="theming">
<i class="bi bi-palette"></i>
</button>
<br />
<button class="btn btn-theme btn-square shadow mt-2 d-none rounded-circle" id="backtotop">
<i class="bi bi-arrow-up"></i>
</button>
</div>
{{-- theming offcanvas --}}
<div class="offcanvas offcanvas-end shadow border-0" tabindex="-1" id="theming" data-bs-scroll="true"
data-bs-backdrop="false" aria-labelledby="theminglabel">
<div class="offcanvas-header border-bottom">
<div>
<h5 class="offcanvas-title" id="theminglabel">Personalize</h5>
<p class="text-secondary small">Make it more like your own</p>
</div>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<h6 class="offcanvas-title">Colors</h6>
<p class="text-secondary small mb-4">Change colors of templates</p>
<div class="row gx-3 mb-4 theme-select">
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="">
<span class="avatar avatar-40 rounded-circle mb-2 bg-default">
<i class="bi bi-arrow-clockwise"></i>
</span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-blue">
<span class="avatar avatar-40 rounded-circle mb-2 bg-blue"></span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-indigo">
<span class="avatar avatar-40 rounded-circle mb-2 bg-indigo"></span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-purple">
<span class="avatar avatar-40 rounded-circle mb-2 bg-purple"></span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-pink">
<span class="avatar avatar-40 rounded-circle mb-2 bg-pink"></span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-red">
<span class="avatar avatar-40 rounded-circle mb-2 bg-red"></span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-orange">
<span class="avatar avatar-40 rounded-circle mb-2 bg-orange"></span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-yellow">
<span class="avatar avatar-40 rounded-circle mb-2 bg-yellow"></span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-green">
<span class="avatar avatar-40 rounded-circle mb-2 bg-green"></span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-teal">
<span class="avatar avatar-40 rounded-circle mb-2 bg-teal"></span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-pista">
<span class="avatar avatar-40 rounded-circle mb-2 bg-pista"></span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-cyan">
<span class="avatar avatar-40 rounded-circle mb-2 bg-cyan"></span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-grey">
<span class="avatar avatar-40 rounded-circle mb-2 bg-grey"></span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-brown">
<span class="avatar avatar-40 rounded-circle mb-2 bg-brown"></span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-chocolate">
<span class="avatar avatar-40 rounded-circle mb-2 bg-chocolate"></span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="theme-black">
<span class="avatar avatar-40 rounded-circle mb-2 bg-dark"></span>
</div>
</div>
</div>
<h6 class="offcanvas-title">Backgrounds</h6>
<p class="text-secondary small mb-4">Change color for background</p>
<div class="row gx-3 mb-4 theme-background">
<div class="col-auto">
<div class="gradient-box text-center mb-2" data-title="bg-default">
<span class="avatar avatar-40 rounded-circle mb-2 bg-default"><i
class="bi bi-arrow-clockwise"></i></span>
</div>
</div>
<div class="col-auto">
<div class="gradient-box text-center mb-2" data-title="bg-white">
<span class="avatar avatar-40 rounded-circle mb-2 bg-white"></span>
</div>
</div>
<div class="col-auto">
<div class="gradient-box text-center mb-2" data-title="bg-r-gradient">
<span class="avatar avatar-40 rounded-circle mb-2 bg-r-gradient"></span>
</div>
</div>
<div class="col-auto">
<div class="gradient-box text-center mb-2" data-title="bg-gradient-1">
<span class="avatar avatar-40 rounded-circle mb-2 bg-gradient-1"></span>
</div>
</div>
<div class="col-auto">
<div class="gradient-box text-center mb-2" data-title="bg-gradient-2">
<span class="avatar avatar-40 rounded-circle mb-2 bg-gradient-2"></span>
</div>
</div>
<div class="col-auto">
<div class="gradient-box text-center mb-2" data-title="bg-gradient-3">
<span class="avatar avatar-40 rounded-circle mb-2 bg-gradient-3"></span>
</div>
</div>
<div class="col-auto">
<div class="gradient-box text-center mb-2" data-title="bg-gradient-4">
<span class="avatar avatar-40 rounded-circle mb-2 bg-gradient-4"></span>
</div>
</div>
<div class="col-auto">
<div class="gradient-box text-center mb-2" data-title="bg-gradient-5">
<span class="avatar avatar-40 rounded-circle mb-2 bg-gradient-5"></span>
</div>
</div>
<div class="col-auto">
<div class="gradient-box text-center mb-2" data-title="bg-gradient-6">
<span class="avatar avatar-40 rounded-circle mb-2 bg-gradient-6"></span>
</div>
</div>
<div class="col-auto">
<div class="gradient-box text-center mb-2" data-title="bg-gradient-7">
<span class="avatar avatar-40 rounded-circle mb-2 bg-gradient-7"></span>
</div>
</div>
<div class="col-auto">
<div class="gradient-box text-center mb-2" data-title="bg-gradient-8">
<span class="avatar avatar-40 rounded-circle mb-2 bg-gradient-8"></span>
</div>
</div>
<div class="col-auto">
<div class="gradient-box text-center mb-2" data-title="bg-gradient-9">
<span class="avatar avatar-40 rounded-circle mb-2 bg-gradient-9"></span>
</div>
</div>
<div class="col-auto">
<div class="gradient-box text-center mb-2" data-title="bg-gradient-10">
<span class="avatar avatar-40 rounded-circle mb-2 bg-gradient-10"></span>
</div>
</div>
</div>
<h6 class="offcanvas-title">Sidebar Layout</h6>
<p class="text-secondary small mb-4">Change sidebar layout style</p>
<div class="row gx-3 mb-4 sidebar-layout">
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="adminuiux-sidebar-standard"
data-bs-toggle="tooltip" title="None">
<span class="avatar avatar-40 rounded-circle mb-2 bg-default">
<i class="bi bi-arrow-clockwise"></i>
</span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="adminuiux-sidebar-iconic"
data-bs-toggle="tooltip" title="Iconic">
<span class="avatar avatar-40 rounded-circle mb-2 bg-default">
<i class="bi bi-bezier h4"></i>
</span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2" data-title="adminuiux-sidebar-boxed"
data-bs-toggle="tooltip" title="Boxed">
<span class="avatar avatar-40 rounded-circle mb-2 bg-default">
<i class="bi bi-box h5"></i>
</span>
</div>
</div>
<div class="col-auto">
<div class="select-box text-center mb-2"
data-title="adminuiux-sidebar-boxed adminuiux-sidebar-iconic" data-bs-toggle="tooltip"
title="Iconic+Boxed">
<span class="avatar avatar-40 rounded-circle mb-2 bg-default">
<i class="bi bi-bounding-box h5"></i>
</span>
</div>
</div>
</div>
<div class="text-center mb-4">
<a href="mobileux-personalization.html" class="btn btn-sm btn-outline-theme">More options <i
class="bi bi-arrow-right-short"></i></a>
</div>
</div>
</div>
{{-- jQuery --}}
<script src="https://code.jquery.com/jquery-3.7.1.min.js" crossorigin="anonymous"></script>
{{-- DataTables Core --}}
<script src="https://cdn.datatables.net/2.3.8/js/dataTables.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/2.3.8/js/dataTables.bootstrap5.min.js" crossorigin="anonymous"></script>
{{-- SweetAlert --}}
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11" crossorigin="anonymous"></script>
{{-- Markdown JS --}}
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" crossorigin="anonymous"></script>
{{-- app js (Modern Vite handled) --}}
<script
src="{{ asset('assets/js/app.js') }}?v={{ file_exists(public_path('assets/js/app.js')) ? filemtime(public_path('assets/js/app.js')) : '1' }}"></script>
<script
src="{{ asset('assets/js/ajax-form-handler.js') }}?v={{ file_exists(public_path('assets/js/ajax-form-handler.js')) ? filemtime(public_path('assets/js/ajax-form-handler.js')) : '1' }}"></script>
<script src="{{ asset('assets/js/password-toggle.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js" crossorigin="anonymous"></script>
{{-- datatables --}}
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/3.1.2/css/buttons.bootstrap5.min.css">
<script src="https://cdn.datatables.net/buttons/3.1.2/js/dataTables.buttons.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/buttons/3.1.2/js/buttons.bootstrap5.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/buttons/3.1.2/js/buttons.html5.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/buttons/3.1.2/js/buttons.print.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/buttons/3.1.2/js/buttons.colVis.min.js" crossorigin="anonymous"></script>
{{-- datatables filter --}}
<script>
document.addEventListener("DOMContentLoaded", () => {
const tableElement = document.getElementById('datatables');
if (!tableElement) {
return;
}
const headerCells = Array.from(tableElement.querySelectorAll('thead tr:first-child th'));
const hiddenTargets = [];
const nonOrderableTargets = [];
const nonSearchableTargets = [];
headerCells.forEach((cell, index) => {
if (cell.dataset.hide === 'audit') {
hiddenTargets.push(index);
}
if (cell.dataset.orderable === 'false') {
nonOrderableTargets.push(index);
}
if (cell.dataset.searchable === 'false') {
nonSearchableTargets.push(index);
}
});
const realTitles = Array.from(tableElement.querySelectorAll('thead tr:first-child th')).map(th => {
const $clone = $(th).clone();
$clone.find('input, select, .dt-column-order, .dataTables_sizing').remove();
let text = $clone.text().trim();
if (!text && th.querySelector('input[type="checkbox"]')) return 'Checkbox';
return text;
});
const isServerSide = tableElement.dataset.serverSide === 'true';
const buttons = [];
buttons.push({
extend: 'colvis',
text: 'Columns',
className: 'btn btn-outline-dark btn-sm px-3',
columnText: function ( dt, idx, title ) {
return realTitles[idx] || title;
}
}, {
extend: 'copy',
text: 'Copy',
className: 'btn btn-outline-dark btn-sm px-3'
}, {
extend: 'excel',
text: 'Excel',
className: 'btn btn-outline-dark btn-sm px-3',
filename: 'datatable_export_' + new Date().toISOString().slice(0, 10)
}, {
extend: 'print',
text: 'Print',
className: 'btn btn-outline-dark btn-sm px-3'
});
const columnDefs = [];
if (hiddenTargets.length) {
columnDefs.push({
targets: hiddenTargets,
visible: false,
});
}
if (nonOrderableTargets.length) {
columnDefs.push({
targets: nonOrderableTargets,
orderable: false,
});
}
if (nonSearchableTargets.length) {
columnDefs.push({
targets: nonSearchableTargets,
searchable: false,
});
}
const table = $('#datatables').DataTable({
destroy: true,
responsive: true,
fixedHeader: true,
autoWidth: false,
stateSave: false,
processing: isServerSide,
serverSide: isServerSide,
pageLength: 25,
lengthMenu: [10, 25, 50, 100, 250, 500],
ajax: isServerSide ? {
url: tableElement.dataset.ajaxUrl || window.location.href,
type: 'GET',
data: function(d) {
// Dynamically add search values from any .filter-row inputs not already handled
// Or just pick up specific extra filters
document.querySelectorAll('.filter-extra').forEach(el => {
d[el.name || el.id] = el.value;
});
return d;
}
} : undefined,
layout: {
topStart: 'pageLength',
topEnd: 'buttons',
bottomStart: 'info',
bottomEnd: 'paging'
},
buttons,
columnDefs,
order: tableElement.dataset.order ? JSON.parse(tableElement.dataset.order) : [],
language: {
emptyTable: "{{ __('No records found.') }}",
zeroRecords: "{{ __('No matching records found.') }}"
},
createdRow: row => row.classList.add('align-middle'),
drawCallback: function() {
window.dispatchEvent(new Event('dataTableReloaded'));
}
});
window.appDataTable = table;
window.reloadDataTable = () => {
if (window.appDataTable?.ajax) {
window.appDataTable.ajax.reload(null, false);
return;
}
window.location.reload();
};
$('#datatables thead tr.filter-row th').each(function (i) {
$(this).find('input,select').on('keyup change', function () {
table.column(i).search(this.value).draw();
});
});
});
// prevent sorting when clicking inside search inputs (header filter)
$('#datatables thead').on('click', 'input, select', function (e) {
e.stopPropagation();
});
</script>
<script>
// Global Standardized SweetAlert Mixin
window.StandardSwal = Swal.mixin({
buttonsStyling: false,
padding: '2.2rem',
customClass: {
popup: 'swal2-popup',
title: 'swal2-title',
confirmButton: 'btn-pill-primary',
cancelButton: 'btn-pill-cancel'
}
});
// Fail-safe to hide pageloader if legacy scripts fail
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
const loader = document.querySelector('.pageloader');
if (loader && loader.style.display !== 'none') {
console.warn('Pageloader still visible after 3s, hiding manually.');
$(loader).fadeOut('slow');
}
}, 3000);
});
</script>
{{-- SweetAlert for success / common error --}}
@if (session('success') || session('error'))
<script>
StandardSwal.fire({
icon: "{{ session('success') ? 'success' : 'error' }}",
title: "{{ session('success') ? 'Success' : 'Error' }}",
text: "{{ session('success') ?? session('error') }}",
confirmButtonText: "{{ __('OK') }}",
timer: "{{ session('success') ? 2000 : null }}",
timerProgressBar: true
});
</script>
@endif
{{-- SweetAlert for validation errors (e.g., unique) --}}
@if ($errors->any())
<script>
StandardSwal.fire({
icon: "error",
title: "Error Validation",
html: `
<ul style="text-align:left; font-size: 14px; line-height: 1.6;">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
`,
confirmButtonText: "OK"
});
</script>
@endif
{{-- Real-time & Dynamic Notification Handler --}}
<script>
// Shared Render Helper (Moved outside DOMContentLoaded to avoid race condition)
window.renderNotificationCard = function (n, context = 'default') {
const isUnread = !!n.is_unread;
const isSidebar = context === 'sidebar';
const avatarClass = n.type === 'warning' ? 'bg-warning text-white' : 'bg-info text-white';
const iconClass = n.type === 'warning' ? 'bi-exclamation-triangle' : 'bi-info-circle';
// Robust escaping for JS Template Literals and HTML Attributes
const escapeJS = (str) => (str || '').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
const escapeAttr = (str) => (str || '').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
const safeTitle = escapeJS(n.title);
const safeMessage = escapeJS(n.message);
return `
<div class="px-3 ${isSidebar ? 'mb-2' : 'mb-3'} animate__animated animate__fadeIn">
<div class="card adminuiux-card border-0 shadow-none rounded-3 mb-0 notification-clickable ${isUnread ? 'bg-primary-subtle' : (isSidebar ? 'bg-light border-bottom' : 'bg-white border-bottom')}"
data-title="${escapeAttr(n.title)}"
data-message="${escapeAttr(n.message)}"
data-type="${n.type}"
data-time="${n.time_ago}"
data-recipient="${n.recipient}">
<div class="card-body p-3">
<div class="d-flex align-items-start">
<div class="avatar avatar-40 rounded-circle ${avatarClass} d-flex align-items-center justify-content-center me-3 flex-shrink-0">
<i class="bi ${iconClass} fw-bold" style="font-size: 16px;"></i>
</div>
<div class="flex-grow-1 overflow-hidden" style="min-width: 0;">
<div class="d-flex justify-content-between align-items-start mb-1">
<p class="${isSidebar ? '' : 'h6'} fw-bold text-dark mb-0 text-truncate" title="${escapeAttr(n.title)}">${safeTitle}</p>
<span class="text-secondary flex-shrink-0 ms-2" style="font-size: 11px;">${n.time_ago}</span>
</div>
<div class="text-secondary text-truncate small" style="max-width: 100%;">${safeMessage}</div>
<div class="d-flex justify-content-end mt-2 gap-2">
${isUnread ? `
<button class="btn btn-sm btn-link text-primary p-0 btn-mark-read" data-url="${n.read_url}" style="font-size: 12px; text-decoration: none;">
<i class="bi bi-check2 me-1"></i>{{ __('Mark Read') }}
</button>
` : `
<span class="text-success fw-medium" style="font-size: 12px;"><i class="bi bi-check2-circle me-1"></i>{{ __('Read') }}</span>
`}
${(!isSidebar && n.delete_url) ? `
<button class="btn btn-sm btn-link text-danger p-0 btn-delete" data-url="${n.delete_url}" style="font-size: 12px; text-decoration: none;">
<i class="bi bi-trash3 me-1"></i>{{ __('Delete') }}
</button>
` : ''}
</div>
</div>
</div>
</div>
</div>
</div>
`;
};
window.reloadNotificationUI = function () {
if (typeof window.updateNotificationUI === 'function') {
window.updateNotificationUI(false);
}
if (typeof window.reloadFeed === 'function') window.reloadFeed();
};
window.showNotificationToast = function (icon, title) {
StandardSwal.fire({
icon: icon,
title: title,
timer: 2000,
showConfirmButton: false,
position: 'center'
});
};
// Optimistic UI Helper for Read Status
window.updateNotificationCardToRead = function ($card) {
if (!$card || !$card.length) return;
// 1. Remove unread background
$card.removeClass('bg-primary-subtle');
// 2. Add appropriate "read" background
const isSidebar = $card.closest('#offcanvasNotification').length > 0;
if (isSidebar) {
$card.addClass('bg-light border-bottom');
} else {
$card.addClass('bg-white border-bottom');
}
// 3. Update Mark Read button to "Read" badge
const $actionArea = $card.find('.d-flex.justify-content-end');
if ($actionArea.length) {
const deleteBtn = $card.find('.btn-delete');
const deleteBtnUrl = deleteBtn.length ? deleteBtn.data('url') : '';
const deleteHtml = (!isSidebar && deleteBtnUrl) ? `
<button class="btn btn-sm btn-link text-danger p-0 btn-delete" data-url="${deleteBtnUrl}" style="font-size: 13px; text-decoration: none;">
<i class="bi bi-trash3 me-1"></i>Delete
</button>
` : '';
$actionArea.html(`
<span class="text-success fw-medium" style="font-size: 12px;"><i class="bi bi-check2-circle me-1"></i>{{ __('Read') }}</span>
${deleteHtml}
`);
}
// 4. Update the unread count badge in header (using correct IDs)
const $badgeCount = $('#header-badge-count');
const $bellBadge = $('#header-bell-badge');
if ($badgeCount.length) {
let currentCount = parseInt($badgeCount.text()) || 0;
if (currentCount > 0) {
currentCount--;
if (currentCount <= 0) {
$badgeCount.text('0');
if ($bellBadge.length) $bellBadge.addClass('d-none');
} else {
$badgeCount.text(currentCount > 9 ? '9+' : currentCount);
}
}
}
};
document.addEventListener('DOMContentLoaded', function () {
const bellBadge = document.getElementById('header-bell-badge');
const badgeCount = document.getElementById('header-badge-count');
const offcanvasBody = document.getElementById('header-notification-container');
const statText = document.getElementById('notification-stat-text');
const headerMarkAllRead = document.getElementById('header-mark-all-read');
let notificationOffset = 0;
const notificationLimit = 10;
// =====================================================
// NOTIFICATION SYSTEM (Resilient)
// =====================================================
try {
window.updateNotificationUI = function (isLoadMore = false) {
if (!isLoadMore) notificationOffset = 0;
fetch(`{{ route('notification-center.api.recent') }}?offset=${notificationOffset}&limit=${notificationLimit}`, {
headers: { 'Accept': 'application/json' }
})
.then(res => res.json())
.then(data => {
try {
if (!data.success) throw new Error('API reported failure');
// Update Badge
if (bellBadge && badgeCount) {
if (data.unread_count > 0) {
bellBadge.classList.remove('d-none');
badgeCount.innerText = data.unread_count > 9 ? '9+' : data.unread_count;
} else {
bellBadge.classList.add('d-none');
badgeCount.innerText = '0';
}
}
// Update Stat Text
if (statText) statText.innerText = `${data.unread_count} unread updates`;
// Show/Hide Mark All Read
if (headerMarkAllRead) {
if (data.unread_count > 0) headerMarkAllRead.classList.remove('d-none');
else headerMarkAllRead.classList.add('d-none');
}
// Show/Hide Clear Read (Admin only)
const headerClearRead = document.getElementById('header-clear-read');
if (headerClearRead) {
if (data.notifications && data.notifications.length > 0) headerClearRead.classList.remove('d-none');
else headerClearRead.classList.add('d-none');
}
// Populate Drawer
if (offcanvasBody) {
const notifications = Array.isArray(data.notifications) ? data.notifications : [];
if (notifications.length === 0 && !isLoadMore) {
offcanvasBody.innerHTML = `
<div class="text-center py-5 opacity-50">
<i class="bi bi-bell-slash h1 display-1"></i>
<p class="small mt-2">${"{{ __('No notifications yet') }}"}</p>
</div>`;
} else {
let cardsHtml = '';
notifications.forEach(n => {
cardsHtml += window.renderNotificationCard(n, 'sidebar');
});
if (!isLoadMore) {
offcanvasBody.innerHTML = '<div id="drawer-list-group">' + cardsHtml + '</div>';
} else {
$('#drawer-list-group').append(cardsHtml);
}
// Load More Button
if (data.has_more) {
if (!$('#drawer-load-more').length) {
$(offcanvasBody).append(`
<div class="text-center py-3" id="drawer-load-more-container">
<button class="btn btn-xs btn-outline-secondary rounded-pill px-4" id="drawer-load-more">Load more</button>
</div>
`);
}
} else {
$('#drawer-load-more-container').remove();
}
}
}
} catch (err) {
console.error('Notification UI Update Error:', err);
}
}).catch(err => console.warn("Notification API currently unavailable."));
}
// Initial Load
window.updateNotificationUI();
// Intercept Offcanvas Opening
const offcanvasEl = document.getElementById('view-notification');
if (offcanvasEl) {
offcanvasEl.addEventListener('show.bs.offcanvas', () => window.updateNotificationUI(false));
}
// Load More Click
$(document).on('click', '#drawer-load-more', function () {
notificationOffset += notificationLimit;
window.updateNotificationUI(true);
});
// Mark All as Read logic
if (headerMarkAllRead) {
headerMarkAllRead.addEventListener('click', function () {
fetch("{{ route('notification-center.read-all') }}", {
method: 'PATCH',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json'
}
})
.then(res => res.json())
.then(data => {
if (data.success) {
window.reloadNotificationUI();
window.showNotificationToast('success', data.message || 'Success');
}
})
.catch(err => window.showNotificationToast('error', 'Failed to process request'));
});
}
// Mark as Read Action (Shared)
$(document).on('click', '.btn-mark-read', function (e, extraData) {
e.preventDefault();
const $btn = $(this);
const $card = $btn.closest('.notification-clickable');
const isSilent = extraData && extraData.silent;
const url = $btn.data('url');
// Optimistic UI Update
if ($card.length) window.updateNotificationCardToRead($card);
fetch(url, {
method: 'PATCH',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json'
}
})
.then(res => res.json())
.then(data => {
if (data.success) {
window.reloadNotificationUI();
if (!isSilent) window.showNotificationToast('success', data.message || 'Marked as read');
}
})
.catch(err => {
if (!isSilent) window.showNotificationToast('error', 'Failed to mark as read');
});
});
// Header Clear Read
$(document).on('click', '#header-clear-read', function () {
StandardSwal.fire({
title: "Clear all read notifications?",
text: "This will permanently remove all read updates from your notification center.",
icon: 'warning',
showCancelButton: true,
confirmButtonText: "Yes, Clear All",
cancelButtonText: "Cancel",
}).then(result => {
if (result.isConfirmed) {
fetch("{{ route('notification-center.clear-read') }}", {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json'
}
})
.then(res => res.json())
.then(data => {
if (data.success) {
window.reloadNotificationUI();
window.showNotificationToast('success', data.message || 'Read notifications cleared');
}
})
.catch(err => window.showNotificationToast('error', 'Failed to clear notifications'));
}
});
});
// Real-time Listen (Laravel Reverb)
if (window.Echo) {
const currentUserId = document.querySelector('meta[name="user-id"]')?.content;
const handleNotification = (e) => {
window.reloadNotificationUI();
if (currentUserId && e.notification.created_by == currentUserId) return;
try {
const audio = new Audio('data:audio/mp3;base64,//uQxAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAAYAAA4EAAFBwcOCAgRERIWFhkZHBwfHx8jIyInJycrKzAwMTIyNjc3Ozs7Pz9DR0dJSUtLTU1PUFJTVVdbW11dXV9fY2NjZ2dra21tb19fYmJjY2NlZWVnZ2dpZGVmZmVlZWRkY2RiYmJiYmJiYmIAAAADTEFNRTMuOTlyAwAAAAAlAAsAABRCAE8SAAAAAQEAAAcIAAAAHwAAAAAAsK5vSwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/uQxAAAD+oHIAAAAFGZ9fOswABu8H5/uXIAAAAAAAAAAAAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAPWfA8N9oH9vP5/uXIAAAAAAAAAAAAAAAAD/uQxAAAD/LzOABUAAH/KCcACoAA/+5DEAQAA//uQxAoAA//uQxAsAA//uQxAwAA//uQxBAAA//uQxBEAA//uQxBIAA//uQxBMAA//uQxBQAA//uQxBUAA/8A');
audio.volume = 0.8;
audio.play().catch(() => { });
} catch (err) { }
if (window.showNotificationToast) {
window.showNotificationToast(e.notification.type || 'info', e.notification.title || 'New Notification');
}
};
window.Echo.channel('notifications').listen('.notification.sent', handleNotification);
@auth
window.Echo.private('App.Models.User.' + {{ auth()->id() }}).listen('.notification.sent', handleNotification);
// Hierarchical Role Channels
@php
$userRoles = auth()->user()->getRoleNames()->toArray();
$listenTo = array_unique($userRoles);
if (in_array('Developer', $userRoles)) {
$listenTo = array_unique(array_merge($listenTo, ['Administrator', 'User']));
} elseif (in_array('Administrator', $userRoles)) {
$listenTo = array_unique(array_merge($listenTo, ['User']));
}
@endphp
@foreach($listenTo as $roleChannel)
window.Echo.private('roles.{{ $roleChannel }}').listen('.notification.sent', handleNotification);
@endforeach
@endauth
}
} catch (globalError) {
console.error("Critical Failure in Notification System:", globalError);
}
// Majestic UI: Global Modal Re-anchoring to Body
// Prevents modals from being trapped in animated stacking contexts
const relocateModals = () => {
document.querySelectorAll('.modal:not(.relocated)').forEach(modal => {
document.body.appendChild(modal);
modal.classList.add('relocated');
});
};
relocateModals();
// Also watch for dynamically added modals
const observer = new MutationObserver(relocateModals);
observer.observe(document.body, { childList: true, subtree: true });
});
// Sticky Notification Fix: Hide alerts when navigating back to a cached page
window.addEventListener('pageshow', function(event) {
if (event.persisted || (window.performance && window.performance.navigation.type === 2)) {
// Hide common alert/toast elements
const alerts = document.querySelectorAll('.alert-success, .alert-info, .swal2-container');
alerts.forEach(el => {
if (el.classList.contains('swal2-container')) {
if (window.Swal) Swal.close();
} else {
el.style.display = 'none';
}
});
}
});
</script>
{{-- 🔍 SMART SEARCH (CMD+K) MODAL --}}
<div class="modal fade" id="global-search-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content overflow-hidden">
<div class="search-input-wrapper">
<i class="bi bi-search search-icon-fixed"></i>
<input type="text" id="global-search-input" class="form-control" placeholder="Search users, settings, pages..." autocomplete="off">
</div>
<div class="search-results-list" id="search-results">
<div class="p-5 text-center text-muted">
<i class="bi bi-keyboard fs-1 mb-3 d-block opacity-25"></i>
<p>Type something to search across the system...</p>
</div>
</div>
<div class="modal-footer bg-light border-0 py-2 px-3 d-flex justify-content-between">
<div class="small text-muted d-none d-sm-block">
<span class="kbd-capsule">ESC</span> to close
<span class="kbd-capsule">↑↓</span> to navigate
<span class="kbd-capsule">ENTER</span> to select
</div>
<div class="small fw-bold text-theme-1">
<i class="bi bi-lightning-charge-fill me-1"></i> SMART SEARCH
</div>
</div>
</div>
</div>
</div>
{{-- Shortcut Hint --}}
<div class="search-shortcut-hint" onclick="$('#global-search-modal').modal('show')">
<span class="kbd-capsule"></span> <span class="kbd-capsule">K</span> SEARCH
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchModal = new bootstrap.Modal('#global-search-modal');
const searchInput = $('#global-search-input');
const searchResults = $('#search-results');
let searchTimeout = null;
let activeIndex = -1;
// Shortcut listener
$(document).on('keydown', function(e) {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
searchModal.show();
}
});
$('#global-search-modal').on('shown.bs.modal', function () {
searchInput.focus();
});
// Clear input & reset state when modal closes
$('#global-search-modal').on('hidden.bs.modal', function () {
searchInput.val('');
searchResults.html('<div class="p-5 text-center text-muted"><i class="bi bi-keyboard fs-1 mb-3 d-block opacity-25"></i><p>Type something to search across the system...</p></div>');
activeIndex = -1;
clearTimeout(searchTimeout);
});
// Live Search
searchInput.on('input', function() {
const q = $(this).val();
clearTimeout(searchTimeout);
if (q.length < 2) {
searchResults.html('<div class="p-5 text-center text-muted"><p>Type at least 2 characters...</p></div>');
activeIndex = -1;
return;
}
searchResults.html('<div class="p-5 text-center"><div class="spinner-border text-primary" role="status"></div></div>');
searchTimeout = setTimeout(() => {
$.get('{{ route("global-search") }}', { q: q })
.done(function(data) {
if (data.length === 0) {
searchResults.html('<div class="p-5 text-center text-muted"><i class="bi bi-search fs-1 mb-3 d-block opacity-25"></i><p>No results found for "<strong>' + $('<span>').text(q).html() + '</strong>"</p></div>');
return;
}
let html = '';
data.forEach((item, index) => {
const safeTitle = $('<span>').text(item.title).html();
const safeSubtitle = $('<span>').text(item.subtitle).html();
const safeCategory = $('<span>').text(item.category).html();
const safeIcon = $('<span>').text(item.icon).html();
const safeUrl = item.url === '#ask-ai' ? '#ask-ai' : $('<span>').text(item.url).html();
html += `
<a href="${safeUrl}" class="search-item${item.is_ai ? ' search-item-ai' : ''}" data-index="${index}" data-is-ai="${item.is_ai ? '1' : '0'}">
<div class="icon-box">
<i class="bi ${safeIcon} fs-5 ${item.is_ai ? 'text-warning' : 'text-theme-1'}"></i>
</div>
<div class="flex-grow-1">
<div class="fw-bold text-dark">${safeTitle}</div>
<div class="small text-muted">${safeSubtitle}</div>
</div>
<span class="category-tag${item.is_ai ? ' bg-warning text-dark' : ''}">${safeCategory}</span>
</a>
`;
});
searchResults.html(html);
activeIndex = -1;
})
.fail(function() {
searchResults.html('<div class="p-5 text-center text-danger"><i class="bi bi-wifi-off fs-1 mb-3 d-block opacity-50"></i><p>Search unavailable. Please try again.</p></div>');
});
}, 300);
});
// Keyboard Navigation
searchInput.on('keydown', function(e) {
const items = searchResults.find('.search-item');
if (items.length === 0) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
activeIndex = (activeIndex + 1) % items.length;
updateActiveItem(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
activeIndex = (activeIndex - 1 + items.length) % items.length;
updateActiveItem(items);
} else if (e.key === 'Enter') {
if (activeIndex > -1) {
e.preventDefault();
const activeItem = $(items[activeIndex]);
if (activeItem.data('is-ai') === 1 || activeItem.attr('href') === '#ask-ai') {
askAiAssistant(searchInput.val());
} else {
window.location.href = activeItem.attr('href');
}
}
}
});
$(document).on('click', '.search-item', function(e) {
if ($(this).data('is-ai') === 1 || $(this).attr('href') === '#ask-ai') {
e.preventDefault();
askAiAssistant(searchInput.val());
}
});
function askAiAssistant(question) {
searchModal.hide();
StandardSwal.fire({
title: '<i class="bi bi-robot text-theme-1 me-2"></i>AI Assistant',
html: '<div class="text-start py-3"><div class="spinner-border text-primary spinner-border-sm me-2"></div> Thinking...</div>',
showConfirmButton: false,
allowOutsideClick: false,
didOpen: () => {
$.post('{{ route("ai.assistant.ask") }}', {
_token: '{{ csrf_token() }}',
question: question
}, function(d) {
StandardSwal.update({
html: `<div class="text-start py-2" style="line-height: 1.6;">${d.answer.replace(/\n/g, '<br>')}</div>`,
showConfirmButton: true,
confirmButtonText: 'Got it!'
});
}).fail(function() {
StandardSwal.fire('Error', 'AI Assistant is currently unavailable.', 'error');
});
}
});
}
function updateActiveItem(items) {
items.removeClass('active');
if (activeIndex > -1) {
const active = $(items[activeIndex]);
active.addClass('active');
active[0].scrollIntoView({ block: 'nearest' });
}
}
});
</script>
<!-- Notification Detail Modal -->
<div class="modal fade" id="notificationDetailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-4 overflow-hidden">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold" id="notif-detail-title"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body pt-3">
<div class="d-flex align-items-center mb-3">
<div id="notif-detail-avatar" class="avatar avatar-40 rounded-circle d-flex align-items-center justify-content-center me-3">
<i id="notif-detail-icon" class="bi fw-bold"></i>
</div>
<div>
<div class="small text-secondary" id="notif-detail-time"></div>
<div class="small fw-bold text-dark" id="notif-detail-sender"></div>
</div>
</div>
<div class="notification-content-wrapper p-3 bg-light rounded-3 mb-3">
<div id="notif-detail-message" style="line-height: 1.6;"></div>
</div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-outline-dark rounded-pill px-4" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
@stack('scripts')
<script>
$(document).on('click', '.notification-clickable', function(e) {
// Prevent modal if clicking on actions
if ($(e.target).closest('.header-mark-read-single, .btn-delete, .btn-mark-read').length) {
return;
}
const $card = $(this);
const title = $card.data('title');
let message = $card.data('message');
const type = $card.data('type');
const time = $card.data('time');
const recipient = $card.data('recipient');
// Escape raw HTML before markdown-lite conversion to prevent XSS.
const escapeHtml = (str) => (str || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
if (message) {
message = escapeHtml(message)
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/\n/g, '<br>');
}
const avatarClass = type === 'warning' ? 'bg-warning text-white' : 'bg-info text-white';
const iconClass = type === 'warning' ? 'bi-exclamation-triangle' : 'bi-info-circle';
$('#notif-detail-title').text(title);
$('#notif-detail-message').html(message);
$('#notif-detail-time').text(time);
$('#notif-detail-sender').text(recipient);
$('#notif-detail-avatar').removeClass('bg-warning bg-info text-white').addClass(avatarClass);
$('#notif-detail-icon').removeClass('bi-exclamation-triangle bi-info-circle').addClass(iconClass);
new bootstrap.Modal('#notificationDetailModal').show();
// Autoread: If unread, mark as read automatically
const $markReadBtn = $card.find('.btn-mark-read');
if ($markReadBtn.length && $card.hasClass('bg-primary-subtle')) {
// Trigger the mark-read logic (this also calls window.updateNotificationCardToRead)
$markReadBtn.first().trigger('click', [{silent: true}]);
}
});
</script>
{{-- Cookie Consent Banner --}}
@include('layouts.partials.cookie-banner')
{{-- Real-time Security Awareness Listener --}}
@auth
@if(!session()->has('impersonator_id'))
<script type="module">
document.addEventListener('DOMContentLoaded', function() {
if (window.Echo) {
window.Echo.private(`App.Models.User.{{ auth()->id() }}`)
.listen('.impersonation.updated', (e) => {
const alertEl = document.getElementById('security-awareness-alert');
if (alertEl) {
if (e.isImpersonated) {
alertEl.classList.remove('d-none');
alertEl.classList.add('animate__fadeIn');
alertEl.classList.remove('animate__fadeOutUp');
} else {
alertEl.classList.add('animate__fadeOutUp');
setTimeout(() => {
alertEl.classList.add('d-none');
}, 800);
}
}
});
}
});
</script>
@endif
@endauth
</body>
</html>