Files
org-parking/frontend/js/nav.js

224 lines
10 KiB
JavaScript

/**
* Navigation Component
* Sidebar navigation and user menu
*/
const MENU_ICON = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>`;
const ICONS = {
calendar: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>`,
users: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>`,
rules: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 11l3 3L22 4"></path>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
</svg>`,
user: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>`,
building: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="6" y="2" width="12" height="20"></rect>
<rect x="9" y="6" width="1.5" height="1.5" fill="currentColor"></rect>
<rect x="13.5" y="6" width="1.5" height="1.5" fill="currentColor"></rect>
<rect x="9" y="11" width="1.5" height="1.5" fill="currentColor"></rect>
<rect x="13.5" y="11" width="1.5" height="1.5" fill="currentColor"></rect>
<rect x="9" y="16" width="1.5" height="1.5" fill="currentColor"></rect>
<rect x="13.5" y="16" width="1.5" height="1.5" fill="currentColor"></rect>
</svg>`,
settings: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>`
};
const NAV_ITEMS = [
{ href: '/presence', icon: 'calendar', label: 'La mia presenza' },
{ href: '/team-calendar', icon: 'users', label: 'Calendario del team' },
{ href: '/team-rules', icon: 'rules', label: 'Regole parcheggio', roles: ['admin', 'manager'] },
{ href: '/admin/users', icon: 'user', label: 'Gestione Utenti', roles: ['admin'] },
{ href: '/admin/offices', icon: 'building', label: 'Gestione Uffici', roles: ['admin'] },
{ href: '/parking-settings', icon: 'settings', label: 'Impostazioni Ufficio', roles: ['admin', 'manager'] }
];
function getIcon(name) {
return ICONS[name] || '';
}
function canAccessNavItem(item, userRole) {
if (!item.roles || item.roles.length === 0) return true;
return userRole && item.roles.includes(userRole);
}
function renderNav(currentPath, userRole) {
return NAV_ITEMS
.filter(item => canAccessNavItem(item, userRole))
.map(item => {
const isActive = item.href === currentPath ||
(currentPath !== '/' && item.href !== '/' && currentPath.startsWith(item.href));
const activeClass = isActive ? ' active' : '';
return `<a href="${item.href}" class="nav-item${activeClass}">
${getIcon(item.icon)}
<span>${item.label}</span>
</a>`;
}).join('\n');
}
async function initNav() {
const navContainer = document.querySelector('.sidebar-nav');
if (!navContainer) return;
const currentPath = window.location.pathname;
// Get user info (works with both JWT and Authelia)
const currentUser = await api.checkAuth();
// Update user info in sidebar
if (currentUser) {
const userNameEl = document.getElementById('userName');
const userRoleEl = document.getElementById('userRole');
if (userNameEl) userNameEl.textContent = currentUser.name || 'User';
if (userRoleEl) userRoleEl.textContent = currentUser.role || '-';
}
// Setup user menu (logout) & mobile menu
setupUserMenu();
setupMobileMenu();
// CHECK: Block access if user has no office (and is not admin)
// Admins are allowed to access "Gestione Uffici" even without an office
if (currentUser && !currentUser.office_id && currentUser.role !== 'admin') {
navContainer.innerHTML = ''; // Clear nav
const mainContent = document.querySelector('.main-content');
if (mainContent) {
mainContent.innerHTML = `
<div style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 80vh;
padding: 2rem;
text-align: center;
">
<div class="card" style="max-width: 500px; padding: 2.5rem; border-top: 4px solid #ef4444;">
<div style="color: #ef4444; margin-bottom: 1.5rem;">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
</div>
<h2 style="margin-bottom: 1rem;">Ufficio non assegnato</h2>
<p class="text-secondary" style="margin-bottom: 1.5rem; line-height: 1.6;">
Il tuo account <strong>${currentUser.email}</strong> è attivo, ma non sei ancora stato assegnato a nessuno ufficio.
</p>
<div style="padding: 1.5rem; background: #f8fafc; border-radius: 8px; text-align: left;">
<div style="font-weight: 600; margin-bottom: 0.5rem; color: var(--text);">Cosa fare?</div>
<div style="font-size: 0.95rem; color: var(--text-secondary);">
Contatta l'amministratore di sistema per richiedere l'assegnazione al tuo ufficio di competenza.<br>
<a href="mailto:s.salemi@sielte.it" style="color: var(--primary); text-decoration: none; font-weight: 500; margin-top: 0.5rem; display: inline-block;">s.salemi@sielte.it</a>
</div>
</div>
</div>
</div>
`;
}
return; // STOP rendering nav
}
// Render navigation (Normal Flow)
navContainer.innerHTML = renderNav(currentPath, currentUser?.role);
}
function setupMobileMenu() {
const sidebar = document.querySelector('.sidebar');
const pageHeader = document.querySelector('.page-header');
if (!sidebar || !pageHeader) return;
// Add menu toggle button to page header (at the start)
const menuToggle = document.createElement('button');
menuToggle.className = 'menu-toggle';
menuToggle.innerHTML = MENU_ICON;
menuToggle.setAttribute('aria-label', 'Apri/Chiudi menu');
pageHeader.insertBefore(menuToggle, pageHeader.firstChild);
// Add overlay
const overlay = document.createElement('div');
overlay.className = 'sidebar-overlay';
document.body.appendChild(overlay);
// Toggle sidebar
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('open');
overlay.classList.toggle('open');
});
// Close sidebar when clicking overlay
overlay.addEventListener('click', () => {
sidebar.classList.remove('open');
overlay.classList.remove('open');
});
// Close sidebar when clicking a nav item (on mobile)
sidebar.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => {
if (window.innerWidth <= 768) {
sidebar.classList.remove('open');
overlay.classList.remove('open');
}
});
});
}
function setupUserMenu() {
const userMenuButton = document.getElementById('userMenuButton');
const userDropdown = document.getElementById('userDropdown');
const logoutButton = document.getElementById('logoutButton');
if (userMenuButton && userDropdown) {
userMenuButton.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = userDropdown.style.display === 'block';
userDropdown.style.display = isOpen ? 'none' : 'block';
});
document.addEventListener('click', () => {
userDropdown.style.display = 'none';
});
userDropdown.addEventListener('click', (e) => e.stopPropagation());
}
if (logoutButton) {
logoutButton.addEventListener('click', () => {
api.logout();
});
}
}
// Export for use in other scripts
window.getIcon = getIcon;
// Auto-initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initNav);
} else {
initNav();
}