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 @@

Automation

-

Workflows, schemalagda jobb och triggers

+

Automatiserade arbetsfloden

- -
-
-
-
-
-
Aktiva workflows
-
-
-
-
📅
-
-
-
Schemalagda jobb
-
-
-
-
-
-
-
Lyckade körningar
-
-
-
-
-
-
-
Misslyckade
-
-
-
- -
-

Workflows

- -
- - - - - -
NamnTriggerStatusSenaste körningAntal körningarÅtgärder
-
- - -
-
-

Schemalagda jobb

- +

Jobb

+
- + + + + + + + - -
NamnTypCronNästa körningStatusÅtgärder
NamnTypSchemaStatusSenast kor
-
- - -
-
-

Senaste körningar

-
- - - - - + + +
Jobb/WorkflowStatusStartadAvslutadDetaljer
Laddar...
- - - diff --git a/web/crm.html b/web/crm.html new file mode 100644 index 000000000..4a6901f4a --- /dev/null +++ b/web/crm.html @@ -0,0 +1,141 @@ + + + + + + BOC CRM — Business Operations Center + + + +
+ + + +
+
+

CRM

+

Kunder och kontakter

+
+ +
+
+

Kunder

+ +
+ + + + + + + + + + + + + +
NamnE-postTelefonStatusSkapad
Laddar...
+
+
+
+ + + + + diff --git a/web/dashboard.html b/web/dashboard.html index ead19379d..d988965e2 100644 --- a/web/dashboard.html +++ b/web/dashboard.html @@ -66,7 +66,7 @@

Dashboard

-

Overblick over hela verksamheten · Uppdateras live

+

Overblick over hela verksamheten

@@ -76,7 +76,7 @@ - +
@@ -159,7 +159,7 @@ @@ -167,7 +167,7 @@
- +

Snabbatgarder

@@ -195,7 +195,7 @@

Automation

- Hantera → + Hantera
@@ -225,7 +225,7 @@
- +