LINUS ROUND 1: Delete dead code (rust/c), generic Store[T], tests, slim main.go
- Removed rust-service/, c-runtime/, kafka stubs - Generic Store[T] pattern with real tests - Slimmed main.go from 324 to ~50 lines - Added config, middleware, store, ledger, pdf tests - Frontend SPA shell with router - Binary: 15.5MB -> 12MB
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
// 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;
|
||||
@@ -0,0 +1,10 @@
|
||||
// Automation module
|
||||
export async function render(container) {
|
||||
container.innerHTML = `
|
||||
<header class="module-header">
|
||||
<h1>Automation</h1>
|
||||
<p class="subtitle">Workflows och schemalagda jobb</p>
|
||||
</header>
|
||||
<p>Modulen laddas...</p>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// CRM module
|
||||
export async function render(container) {
|
||||
container.innerHTML = `
|
||||
<header class="module-header">
|
||||
<h1>CRM</h1>
|
||||
<p class="subtitle">Kunder och leads</p>
|
||||
</header>
|
||||
<div class="toolbar">
|
||||
<button class="btn-primary" id="btn-new-customer">+ Ny kund</button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table" id="customers-table">
|
||||
<thead>
|
||||
<tr><th>Namn</th><th>Företag</th><th>Status</th><th>Email</th></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const res = await apiRequest('/crm/customers');
|
||||
const data = await res.json();
|
||||
const tbody = container.querySelector('#customers-table tbody');
|
||||
|
||||
if (data.customers?.length) {
|
||||
tbody.innerHTML = data.customers.map(c => `
|
||||
<tr>
|
||||
<td>${c.name}</td>
|
||||
<td>${c.company || '-'}</td>
|
||||
<td><span class="badge badge-${c.status}">${c.status}</span></td>
|
||||
<td>${c.email || '-'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
tbody.innerHTML = '<tr><td colspan="4">Inga kunder hittades</td></tr>';
|
||||
}
|
||||
} catch (err) {
|
||||
container.innerHTML += `<div class="alert alert-error">Kunde inte ladda kunder: ${err.message}</div>`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Dashboard module — rendered dynamically into #app
|
||||
export async function render(container) {
|
||||
container.innerHTML = `
|
||||
<header class="module-header">
|
||||
<h1>Dashboard</h1>
|
||||
<p class="subtitle">Överblick över hela verksamheten</p>
|
||||
</header>
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value" id="kpi-revenue">—</div>
|
||||
<div class="kpi-label">Månadsintäkt</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value" id="kpi-deals">—</div>
|
||||
<div class="kpi-label">Aktiva deals</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value" id="kpi-customers">—</div>
|
||||
<div class="kpi-label">Kunder</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value" id="kpi-tickets">—</div>
|
||||
<div class="kpi-label">Öppna ärenden</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Snabbåtgärder</h2>
|
||||
<div class="quick-actions">
|
||||
<button onclick="location.hash='/crm'">+ Ny kund</button>
|
||||
<button onclick="location.hash='/sales'">+ Ny offert</button>
|
||||
<button onclick="location.hash='/finance'">+ Ny faktura</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Load real data
|
||||
try {
|
||||
const [deals, customers, tickets] = await Promise.all([
|
||||
apiRequest('/sales/deals').then(r => r.ok ? r.json() : {deals: []}),
|
||||
apiRequest('/crm/customers').then(r => r.ok ? r.json() : {customers: []}),
|
||||
apiRequest('/support/tickets').then(r => r.ok ? r.json() : {tickets: []}),
|
||||
]);
|
||||
|
||||
document.getElementById('kpi-deals').textContent = deals.deals?.length || 0;
|
||||
document.getElementById('kpi-customers').textContent = customers.customers?.length || 0;
|
||||
document.getElementById('kpi-tickets').textContent = tickets.tickets?.length || 0;
|
||||
} catch (err) {
|
||||
console.error('Dashboard load error:', err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Finance module
|
||||
export async function render(container) {
|
||||
container.innerHTML = `
|
||||
<header class="module-header">
|
||||
<h1>Finance</h1>
|
||||
<p class="subtitle">Fakturor och ekonomi</p>
|
||||
</header>
|
||||
<div class="toolbar">
|
||||
<button class="btn-primary">+ Ny faktura</button>
|
||||
</div>
|
||||
<p>Modulen laddas...</p>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// HR module
|
||||
export async function render(container) {
|
||||
container.innerHTML = `
|
||||
<header class="module-header">
|
||||
<h1>HR</h1>
|
||||
<p class="subtitle">Anställda och tidrapporter</p>
|
||||
</header>
|
||||
<p>Modulen laddas...</p>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Legal module
|
||||
export async function render(container) {
|
||||
container.innerHTML = `
|
||||
<header class="module-header">
|
||||
<h1>Legal</h1>
|
||||
<p class="subtitle">Kontrakt och påminnelser</p>
|
||||
</header>
|
||||
<p>Modulen laddas...</p>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Marketing module
|
||||
export async function render(container) {
|
||||
container.innerHTML = `
|
||||
<header class="module-header">
|
||||
<h1>Marketing</h1>
|
||||
<p class="subtitle">Kampanjer och innehåll</p>
|
||||
</header>
|
||||
<p>Modulen laddas...</p>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Sales module
|
||||
export async function render(container) {
|
||||
container.innerHTML = `
|
||||
<header class="module-header">
|
||||
<h1>Sales</h1>
|
||||
<p class="subtitle">Deals och offert</p>
|
||||
</header>
|
||||
<div class="toolbar">
|
||||
<button class="btn-primary">+ Ny deal</button>
|
||||
<button class="btn-secondary">+ Ny offert</button>
|
||||
</div>
|
||||
<p>Modulen laddas...</p>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Support module
|
||||
export async function render(container) {
|
||||
container.innerHTML = `
|
||||
<header class="module-header">
|
||||
<h1>Support</h1>
|
||||
<p class="subtitle">Ärenden och kundtjänst</p>
|
||||
</header>
|
||||
<p>Modulen laddas...</p>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user