442 lines
18 KiB
PHP
442 lines
18 KiB
PHP
<?php
|
||
|
||
/** @var array $rolesFromSession */
|
||
|
||
use App\Services\NavbarService;
|
||
|
||
$rolesFromSession = [];
|
||
$rawRole = session()->get('role'); // your app uses a single role string
|
||
if (is_array($rawRole)) $rolesFromSession = $rawRole;
|
||
else $rolesFromSession = [$rawRole ?? 'guest'];
|
||
|
||
$service = new NavbarService();
|
||
$menu = $service->getMenuForRoles($rolesFromSession);
|
||
|
||
function nav_url(string $path): string
|
||
{
|
||
$path = trim($path);
|
||
if ($path === '' || preg_match('/[<>]/', $path)) {
|
||
return '#';
|
||
}
|
||
// Treat relative paths as site_url, absolute keep as is.
|
||
if (preg_match('#^https?://#i', $path)) return $path;
|
||
return site_url($path);
|
||
}
|
||
|
||
?>
|
||
<style>
|
||
/* Mobile-friendly sidebar behavior and backdrop */
|
||
@media (max-width: 991.98px) {
|
||
#navbarManagement.mgmt-sidebar { width: min(88vw, 360px); transform: translateX(-100%); will-change: transform; }
|
||
html.mgmt-sidebar-open #navbarManagement.mgmt-sidebar { transform: translateX(0); }
|
||
#mgmtSidebarHotzone { display: none !important; }
|
||
.mgmt-sidebar-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.35); z-index: 1043; display: none; }
|
||
html.mgmt-sidebar-open .mgmt-sidebar-backdrop { display: block; }
|
||
body.mgmt-no-scroll { overflow: hidden; touch-action: none; }
|
||
#navbarManagement.mgmt-sidebar .dropdown-menu { display: none !important; }
|
||
}
|
||
@media (min-width: 992px) { .mgmt-sidebar-backdrop { display: none !important; } }
|
||
#navbarManagement .submenu .nav-link, #navbarManagement .mobile-submenu .nav-link { padding: .4rem .5rem; }
|
||
#navbarManagement .submenu .dropdown-header, #navbarManagement .mobile-submenu .dropdown-header { padding: .35rem .5rem; }
|
||
#navbarManagement .submenu-toggle, #navbarManagement .mobile-toggle { width: 100%; text-align: left; }
|
||
|
||
/* Arrow toggle styling – follow page style */
|
||
.mgmt-sidebar-toggle {
|
||
position: fixed;
|
||
left: 10px;
|
||
top: 72px; /* JS nudges based on header height */
|
||
width: 34px;
|
||
height: 34px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: none;
|
||
border-radius: 9999px;
|
||
background-color: var(--mgmt-primary);
|
||
color: #fff;
|
||
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
|
||
z-index: 1060; /* above sidebar */
|
||
cursor: pointer;
|
||
transition: transform .2s ease, background-color .2s ease, opacity .2s ease;
|
||
}
|
||
.mgmt-sidebar-toggle:hover { background-color: var(--mgmt-primary-hover); }
|
||
html.mgmt-sidebar-open .mgmt-sidebar-toggle i { transform: rotate(180deg); }
|
||
/* Rotate submenu chevron when expanded */
|
||
#navbarManagement .submenu-toggle[aria-expanded="true"] i { transform: rotate(180deg); }
|
||
</style>
|
||
|
||
<button id="mgmtSidebarToggle" class="mgmt-sidebar-toggle" aria-label="Toggle sidebar" aria-expanded="false">
|
||
<i class="bi bi-chevron-right"></i>
|
||
</button>
|
||
<nav id="navbarManagement" class="mgmt-sidebar">
|
||
<div class="mgmt-sidebar-content">
|
||
<ul class="nav flex-column" id="mgmtMenuList">
|
||
<?php foreach ($menu as $item): ?>
|
||
<?php $hasChildren = !empty($item['children']); ?>
|
||
<?php if ($hasChildren): ?>
|
||
<li class="nav-item dropdown dropend d-none">
|
||
<a class="nav-link d-flex align-items-center justify-content-between dropdown-toggle" href="#" id="dd_<?= esc($item['id']) ?>" role="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" data-bs-display="static" aria-expanded="false">
|
||
<span class="nav-label-chip flex-shrink-0"><?= esc($item['label']) ?></span>
|
||
<i class="bi bi-chevron-right ms-2"></i>
|
||
</a>
|
||
<div class="dropdown-menu shadow sidebar-dropdown-menu" aria-labelledby="dd_<?= esc($item['id']) ?>">
|
||
<?php foreach ($item['children'] as $child): ?>
|
||
<?php if ($child['url']): ?>
|
||
<a class="dropdown-item" href="<?= esc(nav_url($child['url']), 'attr') ?>" <?= $child['target'] ? 'target="' . esc($child['target']) . '"' : '' ?>>
|
||
<?= esc($child['label']) ?>
|
||
</a>
|
||
<?php else: ?>
|
||
<h6 class="dropdown-header mb-1"><?= esc($child['label']) ?></h6>
|
||
<?php endif; ?>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</li>
|
||
<?php $cid = 'mnav_'.esc($item['id']); ?>
|
||
<li class="nav-item d-block">
|
||
<button class="nav-link d-flex align-items-center justify-content-between mobile-toggle submenu-toggle" type="button" data-bs-toggle="collapse" data-bs-target="#<?= $cid ?>" aria-controls="<?= $cid ?>" aria-expanded="false">
|
||
<span class="nav-label-chip flex-shrink-0"><?= esc($item['label']) ?></span>
|
||
<i class="bi bi-chevron-down ms-2"></i>
|
||
</button>
|
||
<div id="<?= $cid ?>" class="collapse submenu" data-bs-parent="#mgmtMenuList">
|
||
<ul class="nav flex-column ms-2">
|
||
<?php foreach ($item['children'] as $child): ?>
|
||
<?php if ($child['url']): ?>
|
||
<li class="nav-item"><a class="nav-link" href="<?= esc(nav_url($child['url']), 'attr') ?>" <?= $child['target'] ? 'target="' . esc($child['target']) . '"' : '' ?>><?= esc($child['label']) ?></a></li>
|
||
<?php else: ?>
|
||
<li class="nav-item"><span class="dropdown-header mb-1 text-muted small"><?= esc($child['label']) ?></span></li>
|
||
<?php endif; ?>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
</div>
|
||
</li>
|
||
<?php else: ?>
|
||
<li class="nav-item">
|
||
<?php if (!empty($item['url'])): ?>
|
||
<a class="nav-link d-flex align-items-center" href="<?= esc(nav_url($item['url']), 'attr') ?>" <?= $item['target'] ? 'target="' . esc($item['target']) . '"' : '' ?>>
|
||
<span class="nav-label-chip flex-shrink-0"><?= esc($item['label']) ?></span>
|
||
</a>
|
||
<?php else: ?>
|
||
<span class="nav-link disabled d-flex align-items-center">
|
||
<span class="nav-label-chip flex-shrink-0"><?= esc($item['label']) ?></span>
|
||
</span>
|
||
<?php endif; ?>
|
||
</li>
|
||
<?php endif; ?>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
|
||
<div class="d-flex align-items-center">
|
||
<?php
|
||
// Build role switcher options when user has multiple roles
|
||
$allRoles = array_map(fn($r)=> strtolower(trim((string)$r)), (array) (session()->get('roles') ?? []));
|
||
$currentRole = strtolower((string) ($rawRole ?? 'guest'));
|
||
$otherRoles = array_values(array_filter($allRoles, fn($r) => $r !== '' && $r !== $currentRole));
|
||
|
||
// Style settings (with sensible defaults)
|
||
$styleCfg = config('Style');
|
||
$sess = session();
|
||
$currentAccent = $sess->get('style_color') ?? ($styleCfg->defaultStyle ?? 'blue');
|
||
$currentMenu = $sess->get('menu_color') ?? ($styleCfg->defaultMenu ?? 'white');
|
||
?>
|
||
<div class="nav-item dropdown me-2">
|
||
|
||
<div class="dropdown-menu dropdown-menu-end quick-menu" aria-labelledby="dd_tools" style="max-height: 70vh; overflow-y: auto; overflow-x: hidden;">
|
||
<?php if (!empty($otherRoles)): ?>
|
||
<h6 class="dropdown-header">Switch Role</h6>
|
||
<?php foreach ($otherRoles as $r): ?>
|
||
<form method="post" action="<?= site_url('set-role') ?>">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="selected_role" value="<?= esc($r) ?>">
|
||
<button type="submit" class="dropdown-item">Switch to <?= esc(ucfirst(str_replace('_', ' ', $r))) ?></button>
|
||
</form>
|
||
<?php endforeach; ?>
|
||
<div class="dropdown-divider"></div>
|
||
<?php endif; ?>
|
||
|
||
<h6 class="dropdown-header">Style</h6>
|
||
<a class="dropdown-item d-flex justify-content-between align-items-center" data-bs-toggle="collapse" href="#ddAccentMgmtSidebar" role="button" aria-expanded="true" aria-controls="ddAccentMgmtSidebar">
|
||
Accent <i class="bi bi-chevron-down ms-2"></i>
|
||
</a>
|
||
<div class="collapse show" id="ddAccentMgmtSidebar">
|
||
<div class="px-2 pb-2">
|
||
<?php foreach (array_keys($styleCfg->stylePalettes ?? []) as $opt): ?>
|
||
<?php $isActive = ($opt === $currentAccent); ?>
|
||
<a class="dropdown-item <?= $isActive ? 'active' : '' ?>" data-style-link href="<?= site_url('ui/style?accent=' . urlencode($opt)) ?>" <?= $isActive ? 'aria-current="true"' : '' ?>>
|
||
<?= ucfirst(esc($opt)) ?><?= $isActive ? ' <i class=\"bi bi-check-lg ms-2\"></i>' : '' ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
<div class="dropdown-divider"></div>
|
||
<div class="px-2 small text-muted">Menu</div>
|
||
<?php foreach (array_keys($styleCfg->menuPalettes ?? []) as $opt): ?>
|
||
<?php $isActive = ($opt === $currentMenu); ?>
|
||
<a class="dropdown-item <?= $isActive ? 'active' : '' ?>" data-style-link href="<?= site_url('ui/style?menu=' . urlencode($opt)) ?>" <?= $isActive ? 'aria-current="true"' : '' ?>>
|
||
<?= ucfirst(esc($opt)) ?><?= $isActive ? ' <i class=\"bi bi-check-lg ms-2\"></i>' : '' ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
<div id="mgmtSidebarHotzone" aria-hidden="true"></div>
|
||
<div id="mgmtSidebarBackdrop" class="mgmt-sidebar-backdrop" aria-hidden="true"></div>
|
||
<script>
|
||
(function() {
|
||
function applySidebarGeometry() {
|
||
const header = document.querySelector('header.navbar.sticky-top, header.navbar.fixed-top') || document.querySelector('header.navbar');
|
||
const nav = document.getElementById('navbarManagement');
|
||
const hot = document.getElementById('mgmtSidebarHotzone');
|
||
const toggle = document.getElementById('mgmtSidebarToggle');
|
||
if (!nav) return;
|
||
let top = 0;
|
||
if (header) {
|
||
top = Math.max(header.offsetHeight || 0, (header.getBoundingClientRect && header.getBoundingClientRect().height) || 0);
|
||
}
|
||
nav.style.top = top + 'px';
|
||
nav.style.height = 'calc(100vh - ' + top + 'px)';
|
||
// Keep sidebar above backdrop (1043) but below header (1045)
|
||
nav.style.zIndex = 1044;
|
||
if (hot) {
|
||
hot.style.top = top + 'px';
|
||
hot.style.height = 'calc(100vh - ' + top + 'px)';
|
||
hot.style.zIndex = 1019;
|
||
}
|
||
if (toggle) { try { toggle.style.top = (top + 12) + 'px'; } catch(_) {} }
|
||
}
|
||
window.addEventListener('load', applySidebarGeometry);
|
||
window.addEventListener('resize', applySidebarGeometry);
|
||
document.addEventListener('DOMContentLoaded', applySidebarGeometry);
|
||
})();
|
||
</script>
|
||
<script>
|
||
// Toggle sidebar open/close (button + backdrop) and lock body scroll on mobile
|
||
(function() {
|
||
var root = document.documentElement;
|
||
var btn = document.getElementById('mgmtSidebarToggle');
|
||
var backdrop = document.getElementById('mgmtSidebarBackdrop');
|
||
if (!btn) return;
|
||
|
||
function setExpanded(expanded) {
|
||
try { btn.setAttribute('aria-expanded', expanded ? 'true' : 'false'); } catch (_) {}
|
||
try { document.body.classList.toggle('mgmt-no-scroll', expanded && !window.matchMedia('(min-width: 992px)').matches); } catch (_) {}
|
||
}
|
||
|
||
function closeSidebar() {
|
||
root.classList.remove('mgmt-sidebar-open');
|
||
setExpanded(false);
|
||
}
|
||
|
||
btn.addEventListener('click', function(e) {
|
||
e.stopPropagation();
|
||
var willOpen = !root.classList.contains('mgmt-sidebar-open');
|
||
root.classList.toggle('mgmt-sidebar-open');
|
||
setExpanded(willOpen);
|
||
});
|
||
if (backdrop) backdrop.addEventListener('click', function() { closeSidebar(); });
|
||
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeSidebar(); });
|
||
document.addEventListener('click', function(e) {
|
||
if (!root.classList.contains('mgmt-sidebar-open')) return;
|
||
if (e.target.closest('#navbarManagement') || e.target.closest('#mgmtSidebarToggle')) return;
|
||
closeSidebar();
|
||
});
|
||
|
||
// Desktop: hover the arrow to reveal the sidebar (large screens only)
|
||
// Use width-only gating to be robust across devices that misreport pointer/hover
|
||
if (window.matchMedia('(min-width: 992px)').matches) {
|
||
btn.addEventListener('mouseenter', function() {
|
||
root.classList.add('mgmt-sidebar-open');
|
||
setExpanded(true);
|
||
});
|
||
btn.addEventListener('mouseleave', function() {
|
||
setTimeout(function() {
|
||
var navHover = document.querySelector('#navbarManagement:hover');
|
||
var btnHover = document.querySelector('#mgmtSidebarToggle:hover');
|
||
if (!navHover && !btnHover) closeSidebar();
|
||
}, 600);
|
||
});
|
||
}
|
||
})();
|
||
</script>
|
||
<script>
|
||
// Ensure style links return to the current page
|
||
(function() {
|
||
document.addEventListener('click', function(e) {
|
||
var a = e.target.closest('a[data-style-link]');
|
||
if (!a) return;
|
||
try {
|
||
var u = new URL(a.getAttribute('href'), window.location.origin);
|
||
if (!u.searchParams.get('back')) {
|
||
u.searchParams.set('back', window.location.href);
|
||
}
|
||
e.preventDefault();
|
||
window.location.assign(u.toString());
|
||
} catch (_) {
|
||
/* ignore */ }
|
||
});
|
||
})();
|
||
</script>
|
||
<script>
|
||
// Hover-to-open behavior with a thin hotzone at the left edge (desktop only)
|
||
(function() {
|
||
if (!window.matchMedia('(hover: hover) and (pointer: fine) and (min-width: 992px)').matches) return;
|
||
var root = document.documentElement;
|
||
var nav = document.getElementById('navbarManagement');
|
||
var hot = document.getElementById('mgmtSidebarHotzone');
|
||
if (!nav || !hot) return;
|
||
var closing;
|
||
var CLOSE_DELAY = 600; // allow time to move into flyout
|
||
function open() {
|
||
root.classList.add('mgmt-sidebar-open');
|
||
}
|
||
|
||
function scheduleClose() {
|
||
if (closing) clearTimeout(closing);
|
||
closing = setTimeout(function() {
|
||
root.classList.remove('mgmt-sidebar-open');
|
||
}, CLOSE_DELAY);
|
||
}
|
||
|
||
function cancelClose() {
|
||
if (closing) {
|
||
clearTimeout(closing);
|
||
closing = null;
|
||
}
|
||
}
|
||
|
||
hot.addEventListener('mouseenter', function() {
|
||
cancelClose();
|
||
open();
|
||
});
|
||
hot.addEventListener('mouseleave', function() {
|
||
scheduleClose();
|
||
});
|
||
nav.addEventListener('mouseenter', function() {
|
||
cancelClose();
|
||
open();
|
||
});
|
||
nav.addEventListener('mouseleave', function() {
|
||
scheduleClose();
|
||
});
|
||
|
||
/* touch behavior disabled; mobile uses visible toggle */
|
||
|
||
// ESC closes
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') {
|
||
scheduleClose();
|
||
}
|
||
});
|
||
|
||
// Keep open while interacting with dropdown flyouts
|
||
nav.addEventListener('shown.bs.dropdown', function() {
|
||
cancelClose();
|
||
open();
|
||
}, true);
|
||
nav.addEventListener('hide.bs.dropdown', function() {
|
||
scheduleClose();
|
||
}, true);
|
||
nav.addEventListener('mouseenter', function(e) {
|
||
if (e.target && e.target.classList && e.target.classList.contains('dropdown-menu')) {
|
||
cancelClose();
|
||
}
|
||
}, true);
|
||
nav.addEventListener('mouseleave', function(e) {
|
||
if (e.target && e.target.classList && e.target.classList.contains('dropdown-menu')) {
|
||
scheduleClose();
|
||
}
|
||
}, true);
|
||
|
||
// Expose helpers for other scripts
|
||
try {
|
||
window._mgmtSidebarKeepOpen = {
|
||
open: open,
|
||
scheduleClose: scheduleClose,
|
||
cancelClose: cancelClose
|
||
};
|
||
} catch (_) {}
|
||
})();
|
||
</script>
|
||
<script>
|
||
// Simple click-open guard so the sidebar is open before dropdown shows (desktop only)
|
||
(function() {
|
||
if (!window.matchMedia('(min-width: 992px)').matches) return;
|
||
var nav = document.getElementById('navbarManagement');
|
||
if (!nav) return;
|
||
nav.addEventListener('click', function(e) {
|
||
var t = e.target.closest('.dropdown-toggle');
|
||
if (!t || !nav.contains(t)) return;
|
||
var root = document.documentElement;
|
||
if (!root.classList.contains('mgmt-sidebar-open')) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
root.classList.add('mgmt-sidebar-open');
|
||
setTimeout(function() {
|
||
try {
|
||
new bootstrap.Dropdown(t).show();
|
||
} catch (_) {
|
||
try {
|
||
t.click();
|
||
} catch (__) {}
|
||
}
|
||
}, 180);
|
||
}
|
||
}, true);
|
||
})();
|
||
</script>
|
||
<!-- Desktop dropdowns: robust open/close without body portal -->
|
||
<script>
|
||
(function() {
|
||
if (!window.matchMedia('(min-width: 992px)').matches) return;
|
||
var nav = document.getElementById('navbarManagement');
|
||
if (!nav) return;
|
||
|
||
function findMenu(t) {
|
||
if (!t) return null;
|
||
var id = t.id;
|
||
if (id) {
|
||
var menuById = nav.querySelector('.dropdown-menu[aria-labelledby="' + id + '"]');
|
||
if (menuById) return menuById;
|
||
}
|
||
try { return t.parentElement.querySelector(':scope > .dropdown-menu'); } catch (_) { return t.parentElement.querySelector('.dropdown-menu'); }
|
||
}
|
||
|
||
function hideAll() {
|
||
nav.querySelectorAll('.nav-item.dropend .dropdown-menu.show').forEach(function(m){ m.classList.remove('show'); m.style.visibility=''; m.style.opacity=''; });
|
||
nav.querySelectorAll('.nav-item.dropend .dropdown-toggle[aria-expanded="true"]').forEach(function(t){ t.setAttribute('aria-expanded','false'); });
|
||
}
|
||
|
||
function showDropend(t) {
|
||
var menu = findMenu(t);
|
||
if (!menu) return;
|
||
try {
|
||
new bootstrap.Dropdown(t, { autoClose: 'outside' }).show();
|
||
} catch (_) {
|
||
// Fallback without Bootstrap
|
||
t.setAttribute('aria-expanded','true');
|
||
menu.classList.add('show');
|
||
}
|
||
}
|
||
|
||
// Toggle dropdowns on click (prevent duplicate toggles)
|
||
nav.addEventListener('click', function(e) {
|
||
var t = e.target.closest('.nav-item.dropend > .dropdown-toggle');
|
||
if (!t) return;
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
var menu = findMenu(t);
|
||
var isOpen = !!(menu && menu.classList.contains('show'));
|
||
hideAll();
|
||
if (!isOpen) showDropend(t);
|
||
}, true);
|
||
|
||
// Close when clicking outside the management sidebar
|
||
document.addEventListener('click', function(e) {
|
||
if (!e.target.closest('#navbarManagement')) hideAll();
|
||
}, true);
|
||
// Close on ESC
|
||
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') hideAll(); });
|
||
})();
|
||
</script>
|