142 lines
7.7 KiB
JavaScript
142 lines
7.7 KiB
JavaScript
import { getUser, clearToken } from '../../lib/auth.mjs';
|
|
import { router } from '../../lib/router.mjs';
|
|
|
|
const NAV = [
|
|
{ label: 'Dashboard', href: '/', icon: 'layout-dashboard', section: 'main' },
|
|
{ label: 'Work Orders', href: '/work-orders', icon: 'clipboard-list', section: 'main' },
|
|
{ label: 'People', href: '/registry/people', icon: 'users', section: 'resources' },
|
|
{ label: 'Vehicles', href: '/registry/vehicles', icon: 'truck', section: 'resources' },
|
|
{ label: 'Equipment', href: '/registry/equipment', icon: 'wrench', section: 'resources' },
|
|
{ label: 'Materials', href: '/registry/materials', icon: 'package', section: 'resources' },
|
|
{ label: 'Reports', href: '/reports', icon: 'bar-chart-2', section: 'operations' },
|
|
];
|
|
|
|
const ADMIN_NAV = [
|
|
{ label: 'Users', href: '/users', icon: 'user-cog' },
|
|
{ label: 'Settings', href: '/settings', icon: 'settings' },
|
|
];
|
|
|
|
class AppSidebar extends HTMLElement {
|
|
#collapsed = localStorage.getItem('sidebar-collapsed') === 'true';
|
|
|
|
connectedCallback() { this.#render(); this.#listenRoute(); }
|
|
|
|
#listenRoute() {
|
|
window.addEventListener('hashchange', () => this.#updateActive());
|
|
}
|
|
|
|
#toggle() {
|
|
this.#collapsed = !this.#collapsed;
|
|
localStorage.setItem('sidebar-collapsed', this.#collapsed);
|
|
const appRoot = document.querySelector('app-root');
|
|
if (appRoot) appRoot.classList.toggle('collapsed', this.#collapsed);
|
|
this.#render();
|
|
}
|
|
|
|
#updateActive() {
|
|
const path = decodeURIComponent(location.hash.slice(1)) || '/';
|
|
this.querySelectorAll('.nav-item').forEach(el => {
|
|
const href = el.dataset.href;
|
|
const active = href === '/' ? path === '/' : path.startsWith(href);
|
|
el.classList.toggle('active', active);
|
|
});
|
|
}
|
|
|
|
#navItemHTML(item, currentPath) {
|
|
const active = item.href === '/' ? currentPath === '/' : currentPath.startsWith(item.href);
|
|
return `
|
|
<a class="nav-item${active ? ' active' : ''}" data-href="${item.href}" href="#${item.href}" title="${item.label}">
|
|
<i data-lucide="${item.icon}"></i>
|
|
<span class="label">${item.label}</span>
|
|
</a>`;
|
|
}
|
|
|
|
#render() {
|
|
const user = getUser();
|
|
const path = decodeURIComponent(location.hash.slice(1)) || '/';
|
|
const c = this.#collapsed;
|
|
const appRoot = document.querySelector('app-root');
|
|
if (appRoot) appRoot.classList.toggle('collapsed', c);
|
|
|
|
const initials = (user?.displayName || user?.username || 'U')
|
|
.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
|
|
|
|
this.innerHTML = `
|
|
<style>
|
|
app-sidebar { overflow: hidden; }
|
|
.top { display: flex; align-items: center; justify-content: space-between; padding: 1rem; min-height: 56px; border-bottom: 1px solid rgba(255,255,255,.06); }
|
|
.brand { display: flex; align-items: center; gap: .6rem; text-decoration: none; }
|
|
.brand-icon { width: 28px; height: 28px; background: var(--teal); border-radius: 6px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
.brand-icon svg { color: #fff; }
|
|
.brand-name { font-size: .9rem; font-weight: 700; color: #fff; white-space: nowrap; overflow: hidden; transition: opacity .2s; }
|
|
.collapsed .brand-name { opacity: 0; width: 0; }
|
|
.toggle-btn { background: none; border: none; cursor: pointer; color: rgba(255,255,255,.5); padding: .3rem; border-radius: 4px; display: flex; flex-shrink: 0; transition: color .15s; }
|
|
.toggle-btn:hover { color: #fff; }
|
|
.nav { flex: 1; overflow-y: auto; padding: .5rem 0; }
|
|
.section-label { padding: .5rem 1rem .25rem; font-size: .65rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: rgba(255,255,255,.3); white-space: nowrap; overflow: hidden; transition: opacity .2s; }
|
|
.collapsed .section-label { opacity: 0; }
|
|
.nav-item { display: flex; align-items: center; gap: .75rem; padding: .55rem 1rem; color: rgba(255,255,255,.7); text-decoration: none; font-size: .875rem; font-weight: 500; transition: background .15s, color .15s; cursor: pointer; border-left: 3px solid transparent; }
|
|
.nav-item:hover { background: var(--sidebar-hover); color: #fff; text-decoration: none; }
|
|
.nav-item.active { background: rgba(10,126,164,.2); color: #fff; border-left-color: var(--teal); }
|
|
.nav-item i { width: 18px; height: 18px; flex-shrink: 0; }
|
|
.nav-item .label { white-space: nowrap; overflow: hidden; transition: opacity .2s; }
|
|
.collapsed .nav-item .label { opacity: 0; width: 0; }
|
|
.collapsed .nav-item { justify-content: center; padding: .55rem; }
|
|
.divider { height: 1px; background: rgba(255,255,255,.06); margin: .5rem 0; }
|
|
.bottom { border-top: 1px solid rgba(255,255,255,.06); padding: .75rem 1rem; display: flex; align-items: center; gap: .6rem; }
|
|
.avatar { width: 32px; height: 32px; border-radius: 50%; background: var(--teal); color: #fff; display: flex; align-items: center; justify-content: center; font-size: .75rem; font-weight: 700; flex-shrink: 0; }
|
|
.user-info { flex: 1; overflow: hidden; transition: opacity .2s; }
|
|
.collapsed .user-info { opacity: 0; width: 0; }
|
|
.user-name { font-size: .813rem; font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.user-role { font-size: .7rem; color: rgba(255,255,255,.45); white-space: nowrap; }
|
|
.logout-btn { background: none; border: none; cursor: pointer; color: rgba(255,255,255,.4); padding: .3rem; border-radius: 4px; display: flex; flex-shrink: 0; transition: color .15s; }
|
|
.logout-btn:hover { color: #fff; }
|
|
.collapsed .logout-btn { display: none; }
|
|
</style>
|
|
|
|
<div class="top ${c ? 'collapsed' : ''}">
|
|
<a class="brand" href="#/">
|
|
<div class="brand-icon"><i data-lucide="clipboard-list" style="width:16px;height:16px"></i></div>
|
|
<span class="brand-name">Work Orders</span>
|
|
</a>
|
|
<button class="toggle-btn" id="toggle-btn" title="Toggle sidebar">
|
|
<i data-lucide="${c ? 'chevrons-right' : 'chevrons-left'}" style="width:18px;height:18px"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<nav class="nav ${c ? 'collapsed' : ''}">
|
|
<div class="section-label">Main</div>
|
|
${NAV.filter(n => n.section === 'main').map(n => this.#navItemHTML(n, path)).join('')}
|
|
<div class="divider"></div>
|
|
<div class="section-label">Resources</div>
|
|
${NAV.filter(n => n.section === 'resources').map(n => this.#navItemHTML(n, path)).join('')}
|
|
<div class="divider"></div>
|
|
<div class="section-label">Operations</div>
|
|
${NAV.filter(n => n.section === 'operations').map(n => this.#navItemHTML(n, path)).join('')}
|
|
${user?.role === 'admin' ? `
|
|
<div class="divider"></div>
|
|
<div class="section-label">Admin</div>
|
|
${ADMIN_NAV.map(n => this.#navItemHTML(n, path)).join('')}` : ''}
|
|
</nav>
|
|
|
|
<div class="bottom ${c ? 'collapsed' : ''}">
|
|
<div class="avatar">${initials}</div>
|
|
<div class="user-info">
|
|
<div class="user-name">${user?.displayName || user?.username || 'User'}</div>
|
|
<div class="user-role">${user?.role || ''}</div>
|
|
</div>
|
|
<button class="logout-btn" id="logout-btn" title="Sign out">
|
|
<i data-lucide="log-out" style="width:16px;height:16px"></i>
|
|
</button>
|
|
</div>`;
|
|
|
|
if (window.lucide) lucide.createIcons({ nodes: [this] });
|
|
this.querySelector('#toggle-btn')?.addEventListener('click', () => this.#toggle());
|
|
this.querySelector('#logout-btn')?.addEventListener('click', () => {
|
|
clearToken();
|
|
window.dispatchEvent(new CustomEvent('auth:logout'));
|
|
});
|
|
}
|
|
}
|
|
customElements.define('app-sidebar', AppSidebar);
|