diff --git a/web/assets/boc.css b/web/assets/boc.css
index 2824b9b14..402b36d0e 100644
--- a/web/assets/boc.css
+++ b/web/assets/boc.css
@@ -1,7 +1,6 @@
/* ============================================
BOC — Business Operations Center
- Design System: Landvex Enterprise
- Light theme, no emojis, professional
+ Enterprise Design System
============================================ */
:root {
@@ -149,7 +148,9 @@ body {
margin-bottom: 6px;
}
-.form-group input {
+.form-group input,
+.form-group select,
+.form-group textarea {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--border-strong);
@@ -161,13 +162,16 @@ body {
transition: border-color 0.15s, box-shadow 0.15s;
}
-.form-group input:focus {
+.form-group input:focus,
+.form-group select:focus,
+.form-group textarea:focus {
outline: none;
border-color: var(--brand);
box-shadow: 0 0 0 3px var(--brand-glow);
}
-.form-group input::placeholder {
+.form-group input::placeholder,
+.form-group textarea::placeholder {
color: var(--text-muted);
}
diff --git a/web/assets/boc.js b/web/assets/boc.js
index 53c62d090..f430490a3 100644
--- a/web/assets/boc.js
+++ b/web/assets/boc.js
@@ -1,342 +1,94 @@
-const API_BASE = window.location.hostname === 'localhost'
- ? 'http://localhost:9092'
- : 'https://landvex.com';
+// BOC — Business Operations Center
+// Shared JavaScript utilities
-let token = localStorage.getItem('boc_token');
+const API_BASE = '/api/v1';
-// Navigation
-document.querySelectorAll('.sidebar-nav a').forEach(link => {
- link.addEventListener('click', (e) => {
- e.preventDefault();
- const module = link.dataset.module;
- showModule(module);
-
- document.querySelectorAll('.sidebar-nav a').forEach(l => l.classList.remove('active'));
- link.classList.add('active');
- });
-});
-
-function showModule(name) {
- document.querySelectorAll('.module').forEach(m => m.classList.remove('active'));
- document.getElementById(name).classList.add('active');
-
- // Load module data
- switch(name) {
- case 'dashboard': loadDashboard(); break;
- case 'crm': loadCRM(); break;
- case 'sales': loadSales(); break;
- case 'finance': loadFinance(); break;
- case 'support': loadSupport(); break;
- case 'analytics': loadAnalytics(); break;
- }
+function getToken() {
+ return localStorage.getItem('boc_token');
}
-async function api(path, options = {}) {
- const res = await fetch(`${API_BASE}${path}`, {
+function getUser() {
+ const user = localStorage.getItem('boc_user');
+ return user ? JSON.parse(user) : 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,
- },
+ ...options.headers
+ }
});
-
- if (res.status === 401) {
- localStorage.removeItem('boc_token');
- window.location.href = '/login.html';
- return;
- }
-
- return res.json();
}
-// Dashboard
-async function loadDashboard() {
- try {
- // Show alerts
- document.getElementById('alerts-container').style.display = 'block';
+function formatCurrency(value, currency = 'USD') {
+ if (value === null || value === undefined) return '—';
+ return new Intl.NumberFormat('sv-SE', {
+ style: 'currency',
+ currency,
+ maximumFractionDigits: 0
+ }).format(value);
+}
- const [mrr, arr, customers, tickets, deals, analytics] = await Promise.all([
- api('/api/v1/sales/mrr'),
- api('/api/v1/sales/arr'),
- api('/api/v1/crm/customers'),
- api('/api/v1/support/tickets'),
- api('/api/v1/sales/deals'),
- api('/api/v1/analytics/users')
- ]);
+function formatDate(date) {
+ if (!date) return '-';
+ return new Date(date).toLocaleDateString('sv-SE');
+}
- document.getElementById('kpi-mrr').textContent = formatCurrency(mrr.mrr);
- document.getElementById('kpi-arr').textContent = formatCurrency(arr.arr);
- document.getElementById('kpi-customers').textContent = customers.total || 0;
- document.getElementById('kpi-tickets').textContent =
- (tickets.tickets || []).filter(t => t.status === 'open').length;
- document.getElementById('kpi-cash').textContent = '276 504';
+function formatDateTime(date) {
+ if (!date) return '-';
+ return new Date(date).toLocaleString('sv-SE');
+}
- const pipelineValue = (deals.deals || [])
- .filter(d => d.status === 'open')
- .reduce((sum, d) => sum + (d.value || 0), 0);
- document.getElementById('kpi-pipeline').textContent = formatCurrency(pipelineValue);
+function timeAgo(timestamp) {
+ const diff = Date.now() - new Date(timestamp).getTime();
+ const mins = Math.floor(diff / 60000);
+ if (mins < 1) return 'nyss';
+ if (mins < 60) return `${mins}m sedan`;
+ const hours = Math.floor(mins / 60);
+ if (hours < 24) return `${hours}h sedan`;
+ const days = Math.floor(hours / 24);
+ if (days < 30) return `${days}d sedan`;
+ return formatDate(timestamp);
+}
- // Recent deals
- const recentDeals = document.getElementById('recent-deals');
- recentDeals.innerHTML = (deals.deals || []).slice(0, 5).map(d => `
-
-
-
${d.name}
-
${d.stage}
-
-
-
${formatCurrency(d.value)}
-
${d.status}
-
-
- `).join('');
+function showToast(message, type = 'info') {
+ const toast = document.createElement('div');
+ toast.className = `alert alert-${type}`;
+ toast.style.cssText = 'position:fixed;top:24px;right:24px;z-index:1000;max-width:400px;animation:slideIn 0.3s ease;';
+ toast.textContent = message;
+ document.body.appendChild(toast);
+ setTimeout(() => toast.remove(), 5000);
+}
- // Recent tickets
- const recentTickets = document.getElementById('recent-tickets');
- recentTickets.innerHTML = (tickets.tickets || []).filter(t => t.status === 'open').slice(0, 5).map(t => `
-
-
-
${t.subject}
-
${t.assigned_to?.String || 'Ej tilldelad'}
-
-
${t.priority}
-
- `).join('');
+function confirmDelete(message) {
+ return confirm(message || 'Ar du saker pa att du vill ta bort detta?');
+}
- } catch (err) {
- console.error('Failed to load dashboard:', err);
+// Check authentication on page load
+function requireAuth() {
+ if (!getToken()) {
+ window.location.href = '/';
+ return false;
+ }
+ return true;
+}
+
+// Update user email in sidebar
+function updateUserInfo() {
+ const user = getUser();
+ const el = document.getElementById('user-email');
+ if (el && user) {
+ el.textContent = user.email || 'erik@landvex.com';
}
}
-// CRM
-async function loadCRM() {
- try {
- const data = await api('/api/v1/crm/customers');
- const tbody = document.querySelector('#customers-table tbody');
- tbody.innerHTML = (data.customers || []).map(c => `
-
- | ${c.name} |
- ${c.company} |
- ${c.status} |
- ${c.source} |
- ${formatDate(c.created_at)} |
-
- `).join('');
- } catch (err) {
- console.error('Failed to load CRM:', err);
- }
-}
-
-// Sales
-async function loadSales() {
- try {
- const data = await api('/api/v1/sales/deals');
- const tbody = document.querySelector('#deals-table tbody');
- tbody.innerHTML = (data.deals || []).map(d => `
-
- | ${d.name} |
- ${d.customer_id} |
- ${formatCurrency(d.value)} |
- ${d.status} |
- ${d.stage} |
- ${formatDate(d.expected_close)} |
-
- `).join('');
- } catch (err) {
- console.error('Failed to load sales:', err);
- }
-}
-
-// Finance
-async function loadFinance() {
- try {
- const data = await api('/api/v1/finance/invoices');
- const tbody = document.querySelector('#invoices-table tbody');
- tbody.innerHTML = (data.invoices || []).map(i => `
-
- | ${i.id} |
- ${i.customer_id} |
- ${formatCurrency(i.amount)} |
- ${i.status} |
- ${i.due_date?.Valid ? formatDate(i.due_date.String) : '-'} |
-
- `).join('');
- } catch (err) {
- console.error('Failed to load finance:', err);
- }
-}
-
-// Support
-async function loadSupport() {
- try {
- const data = await api('/api/v1/support/tickets');
- const tbody = document.querySelector('#tickets-table tbody');
- tbody.innerHTML = (data.tickets || []).map(t => `
-
- | ${t.subject} |
- ${t.customer_id} |
- ${t.priority} |
- ${t.status} |
- ${t.assigned_to?.String || 'Ej tilldelad'} |
-
- `).join('');
- } catch (err) {
- console.error('Failed to load support:', err);
- }
-}
-
-// Analytics
-async function loadAnalytics() {
- try {
- const [users, revenue, retention] = await Promise.all([
- api('/api/v1/analytics/users'),
- api('/api/v1/analytics/revenue'),
- api('/api/v1/analytics/retention')
- ]);
-
- document.getElementById('analytics-dau').textContent =
- (users.daily_active || 0).toLocaleString('sv-SE');
- document.getElementById('analytics-revenue').textContent =
- formatCurrency(revenue.revenue_this_month || 0);
- document.getElementById('analytics-retention').textContent =
- (retention.day_30 || 0) + '%';
- } catch (err) {
- console.error('Failed to load analytics:', err);
- }
-}
-
-// Modal
-function showModal(type) {
- const modal = document.getElementById('modal');
- const title = document.getElementById('modal-title');
- const body = document.getElementById('modal-body');
-
- modal.style.display = 'flex';
-
- if (type === 'new-customer') {
- title.textContent = 'Ny kund';
- body.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
- } else if (type === 'new-deal') {
- title.textContent = 'Ny deal';
- body.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
- }
-}
-
-function hideModal() {
- document.getElementById('modal').style.display = 'none';
-}
-
-async function createCustomer() {
- const name = document.getElementById('new-customer-name').value;
- const email = document.getElementById('new-customer-email').value;
- const phone = document.getElementById('new-customer-phone').value;
- const status = document.getElementById('new-customer-status').value;
-
- if (!name) {
- alert('Namn krävs');
- return;
- }
-
- try {
- await api('/api/v1/crm/customers', {
- method: 'POST',
- body: JSON.stringify({ name, email, phone, status })
- });
-
- hideModal();
- loadCRM();
- } catch (err) {
- alert('Kunde inte skapa kund: ' + err.message);
- }
-}
-
-async function createDeal() {
- const name = document.getElementById('new-deal-name').value;
- const value = parseFloat(document.getElementById('new-deal-value').value);
- const stage = document.getElementById('new-deal-stage').value;
-
- if (!name || !value) {
- alert('Namn och värde krävs');
- return;
- }
-
- try {
- await api('/api/v1/sales/deals', {
- method: 'POST',
- body: JSON.stringify({ name, value, stage, status: 'open' })
- });
-
- hideModal();
- loadSales();
- } catch (err) {
- alert('Kunde inte skapa deal: ' + err.message);
- }
-}
-
-// Helpers
-function formatCurrency(value) {
- return new Intl.NumberFormat('sv-SE').format(value || 0) + ' SEK';
-}
-
-function formatDate(dateStr) {
- if (!dateStr) return '-';
- const date = new Date(dateStr);
- return date.toLocaleDateString('sv-SE');
-}
-
-function logout() {
- localStorage.removeItem('boc_token');
- window.location.href = '/login.html';
-}
-
-// Init
+// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
- if (!token && !window.location.pathname.includes('login')) {
- window.location.href = '/login.html';
- return;
- }
-
- loadDashboard();
+ updateUserInfo();
});
diff --git a/web/automation.html b/web/automation.html
index be6b1be08..9b430502f 100644
--- a/web/automation.html
+++ b/web/automation.html
@@ -9,196 +9,132 @@
-
-
-
-
-
-
✅
-
-
—
-
Lyckade körningar
-
-
-
-
-
-
-
Workflows
-
-
-
-
- | Namn | Trigger | Status | Senaste körning | Antal körningar | Åtgärder |
-
-
-
-
-
-
-
-
-
Schemalagda jobb
-
+ Jobb
+
- | Namn | Typ | Cron | Nästa körning | Status | Åtgärder |
+
+ | Namn |
+ Typ |
+ Schema |
+ Status |
+ Senast kor |
+
-
-
-
-
-
-
-
-
Senaste körningar
-
-
-
- | Jobb/Workflow | Status | Startad | Avslutad | Detaljer |
-
-
+
+ | Laddar... |
+
-
-
-