142 lines
4.0 KiB
JavaScript
142 lines
4.0 KiB
JavaScript
// BOC — Business Operations Center
|
|
// Single-page app shell. One sidebar, dynamic modules.
|
|
// Linus principle: write it once, route everything through it.
|
|
|
|
const API_BASE = '/api/v1';
|
|
|
|
// Module registry — add new modules here
|
|
const modules = {
|
|
'/': () => import('./modules/dashboard.js'),
|
|
'/crm': () => import('./modules/crm.js'),
|
|
'/sales': () => import('./modules/sales.js'),
|
|
'/finance': () => import('./modules/finance.js'),
|
|
'/hr': () => import('./modules/hr.js'),
|
|
'/legal': () => import('./modules/legal.js'),
|
|
'/marketing': () => import('./modules/marketing.js'),
|
|
'/support': () => import('./modules/support.js'),
|
|
'/automation': () => import('./modules/automation.js'),
|
|
};
|
|
|
|
// Sidebar configuration — one source of truth
|
|
const sidebarItems = [
|
|
{ path: '/', label: 'Dashboard', icon: 'grid' },
|
|
{ path: '/crm', label: 'CRM', icon: 'users' },
|
|
{ path: '/sales', label: 'Sales', icon: 'dollar' },
|
|
{ path: '/finance', label: 'Finance', icon: 'bar-chart' },
|
|
{ path: '/hr', label: 'HR', icon: 'user' },
|
|
{ path: '/legal', label: 'Legal', icon: 'file-text' },
|
|
{ path: '/marketing', label: 'Marketing', icon: 'megaphone' },
|
|
{ path: '/support', label: 'Support', icon: 'help-circle' },
|
|
{ path: '/automation', label: 'Automation', icon: 'zap' },
|
|
];
|
|
|
|
// Auth utilities
|
|
function getToken() { return localStorage.getItem('boc_token'); }
|
|
function getUser() {
|
|
const u = localStorage.getItem('boc_user');
|
|
return u ? JSON.parse(u) : null;
|
|
}
|
|
|
|
function apiRequest(endpoint, options = {}) {
|
|
const token = getToken();
|
|
const url = endpoint.startsWith('http') ? endpoint : `${API_BASE}${endpoint}`;
|
|
return fetch(url, {
|
|
...options,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
...options.headers
|
|
}
|
|
});
|
|
}
|
|
|
|
// Router
|
|
async function navigate(path) {
|
|
const app = document.getElementById('app');
|
|
const loader = modules[path] || modules['/'];
|
|
|
|
try {
|
|
const mod = await loader();
|
|
app.innerHTML = '';
|
|
await mod.render(app);
|
|
updateActiveNav(path);
|
|
} catch (err) {
|
|
app.innerHTML = `<div class="alert alert-error">Failed to load module: ${err.message}</div>`;
|
|
}
|
|
}
|
|
|
|
function updateActiveNav(path) {
|
|
document.querySelectorAll('.sidebar-nav a').forEach(a => {
|
|
a.classList.toggle('active', a.getAttribute('href') === '#' + path);
|
|
});
|
|
}
|
|
|
|
// Sidebar renderer
|
|
function renderSidebar() {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const user = getUser();
|
|
|
|
sidebar.innerHTML = `
|
|
<div class="sidebar-logo">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="2" y="7" width="20" height="14" rx="2"/>
|
|
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
|
|
</svg>
|
|
<span>BOC</span>
|
|
</div>
|
|
<nav class="sidebar-nav">
|
|
${sidebarItems.map(item => `
|
|
<a href="#${item.path}" data-path="${item.path}">
|
|
<span>${item.label}</span>
|
|
</a>
|
|
`).join('')}
|
|
</nav>
|
|
<div class="sidebar-footer">
|
|
<span>${user?.email || 'guest'}</span>
|
|
<button onclick="logout()">Logga ut</button>
|
|
</div>
|
|
`;
|
|
|
|
sidebar.querySelectorAll('a').forEach(a => {
|
|
a.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const path = a.getAttribute('data-path');
|
|
location.hash = path;
|
|
});
|
|
});
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem('boc_token');
|
|
localStorage.removeItem('boc_user');
|
|
location.href = '/login.html';
|
|
}
|
|
|
|
// Auth check
|
|
function requireAuth() {
|
|
if (!getToken()) {
|
|
location.href = '/login.html';
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Init
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
if (!requireAuth()) return;
|
|
renderSidebar();
|
|
|
|
const path = location.hash.slice(1) || '/';
|
|
navigate(path);
|
|
});
|
|
|
|
window.addEventListener('hashchange', () => {
|
|
const path = location.hash.slice(1) || '/';
|
|
navigate(path);
|
|
});
|
|
|
|
// Export for modules
|
|
window.apiRequest = apiRequest;
|
|
window.getToken = getToken;
|
|
window.getUser = getUser;
|