feat(boc): Complete module pages with enterprise design
- All 9 module pages: Dashboard, CRM, Sales, Finance, HR, Legal, Marketing, Support, Automation - Shared boc.js with API utilities - Landvex enterprise design: dark sidebar, light content - SVG icons, no emojis - Tables with real data loading - Consistent navigation and auth
This commit is contained in:
+9
-5
@@ -1,7 +1,6 @@
|
|||||||
/* ============================================
|
/* ============================================
|
||||||
BOC — Business Operations Center
|
BOC — Business Operations Center
|
||||||
Design System: Landvex Enterprise
|
Enterprise Design System
|
||||||
Light theme, no emojis, professional
|
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -149,7 +148,9 @@ body {
|
|||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input {
|
.form-group input,
|
||||||
|
.form-group select,
|
||||||
|
.form-group textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border-strong);
|
||||||
@@ -161,13 +162,16 @@ body {
|
|||||||
transition: border-color 0.15s, box-shadow 0.15s;
|
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;
|
outline: none;
|
||||||
border-color: var(--brand);
|
border-color: var(--brand);
|
||||||
box-shadow: 0 0 0 3px var(--brand-glow);
|
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);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+70
-318
@@ -1,342 +1,94 @@
|
|||||||
const API_BASE = window.location.hostname === 'localhost'
|
// BOC — Business Operations Center
|
||||||
? 'http://localhost:9092'
|
// Shared JavaScript utilities
|
||||||
: 'https://landvex.com';
|
|
||||||
|
|
||||||
let token = localStorage.getItem('boc_token');
|
const API_BASE = '/api/v1';
|
||||||
|
|
||||||
// Navigation
|
function getToken() {
|
||||||
document.querySelectorAll('.sidebar-nav a').forEach(link => {
|
return localStorage.getItem('boc_token');
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function api(path, options = {}) {
|
function getUser() {
|
||||||
const res = await fetch(`${API_BASE}${path}`, {
|
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,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
...options.headers,
|
...options.headers
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 401) {
|
|
||||||
localStorage.removeItem('boc_token');
|
|
||||||
window.location.href = '/login.html';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json();
|
function formatCurrency(value, currency = 'USD') {
|
||||||
|
if (value === null || value === undefined) return '—';
|
||||||
|
return new Intl.NumberFormat('sv-SE', {
|
||||||
|
style: 'currency',
|
||||||
|
currency,
|
||||||
|
maximumFractionDigits: 0
|
||||||
|
}).format(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dashboard
|
function formatDate(date) {
|
||||||
async function loadDashboard() {
|
if (!date) return '-';
|
||||||
try {
|
return new Date(date).toLocaleDateString('sv-SE');
|
||||||
// Show alerts
|
}
|
||||||
document.getElementById('alerts-container').style.display = 'block';
|
|
||||||
|
|
||||||
const [mrr, arr, customers, tickets, deals, analytics] = await Promise.all([
|
function formatDateTime(date) {
|
||||||
api('/api/v1/sales/mrr'),
|
if (!date) return '-';
|
||||||
api('/api/v1/sales/arr'),
|
return new Date(date).toLocaleString('sv-SE');
|
||||||
api('/api/v1/crm/customers'),
|
}
|
||||||
api('/api/v1/support/tickets'),
|
|
||||||
api('/api/v1/sales/deals'),
|
|
||||||
api('/api/v1/analytics/users')
|
|
||||||
]);
|
|
||||||
|
|
||||||
document.getElementById('kpi-mrr').textContent = formatCurrency(mrr.mrr);
|
function timeAgo(timestamp) {
|
||||||
document.getElementById('kpi-arr').textContent = formatCurrency(arr.arr);
|
const diff = Date.now() - new Date(timestamp).getTime();
|
||||||
document.getElementById('kpi-customers').textContent = customers.total || 0;
|
const mins = Math.floor(diff / 60000);
|
||||||
document.getElementById('kpi-tickets').textContent =
|
if (mins < 1) return 'nyss';
|
||||||
(tickets.tickets || []).filter(t => t.status === 'open').length;
|
if (mins < 60) return `${mins}m sedan`;
|
||||||
document.getElementById('kpi-cash').textContent = '276 504';
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
const pipelineValue = (deals.deals || [])
|
function showToast(message, type = 'info') {
|
||||||
.filter(d => d.status === 'open')
|
const toast = document.createElement('div');
|
||||||
.reduce((sum, d) => sum + (d.value || 0), 0);
|
toast.className = `alert alert-${type}`;
|
||||||
document.getElementById('kpi-pipeline').textContent = formatCurrency(pipelineValue);
|
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 deals
|
function confirmDelete(message) {
|
||||||
const recentDeals = document.getElementById('recent-deals');
|
return confirm(message || 'Ar du saker pa att du vill ta bort detta?');
|
||||||
recentDeals.innerHTML = (deals.deals || []).slice(0, 5).map(d => `
|
}
|
||||||
<div class="list-item">
|
|
||||||
<div>
|
|
||||||
<strong>${d.name}</strong>
|
|
||||||
<div style="color: var(--text-secondary); font-size: 12px;">${d.stage}</div>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: right;">
|
|
||||||
<div>${formatCurrency(d.value)}</div>
|
|
||||||
<span class="status status-${d.status}">${d.status}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
// Recent tickets
|
// Check authentication on page load
|
||||||
const recentTickets = document.getElementById('recent-tickets');
|
function requireAuth() {
|
||||||
recentTickets.innerHTML = (tickets.tickets || []).filter(t => t.status === 'open').slice(0, 5).map(t => `
|
if (!getToken()) {
|
||||||
<div class="list-item">
|
window.location.href = '/';
|
||||||
<div>
|
return false;
|
||||||
<strong>${t.subject}</strong>
|
}
|
||||||
<div style="color: var(--text-secondary); font-size: 12px;">${t.assigned_to?.String || 'Ej tilldelad'}</div>
|
return true;
|
||||||
</div>
|
}
|
||||||
<span class="status status-${t.priority}">${t.priority}</span>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
} catch (err) {
|
// Update user email in sidebar
|
||||||
console.error('Failed to load dashboard:', err);
|
function updateUserInfo() {
|
||||||
|
const user = getUser();
|
||||||
|
const el = document.getElementById('user-email');
|
||||||
|
if (el && user) {
|
||||||
|
el.textContent = user.email || 'erik@landvex.com';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRM
|
// Initialize on page load
|
||||||
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 => `
|
|
||||||
<tr>
|
|
||||||
<td>${c.name}</td>
|
|
||||||
<td>${c.company}</td>
|
|
||||||
<td><span class="status status-${c.status}">${c.status}</span></td>
|
|
||||||
<td>${c.source}</td>
|
|
||||||
<td>${formatDate(c.created_at)}</td>
|
|
||||||
</tr>
|
|
||||||
`).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 => `
|
|
||||||
<tr>
|
|
||||||
<td>${d.name}</td>
|
|
||||||
<td>${d.customer_id}</td>
|
|
||||||
<td>${formatCurrency(d.value)}</td>
|
|
||||||
<td><span class="status status-${d.status}">${d.status}</span></td>
|
|
||||||
<td>${d.stage}</td>
|
|
||||||
<td>${formatDate(d.expected_close)}</td>
|
|
||||||
</tr>
|
|
||||||
`).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 => `
|
|
||||||
<tr>
|
|
||||||
<td>${i.id}</td>
|
|
||||||
<td>${i.customer_id}</td>
|
|
||||||
<td>${formatCurrency(i.amount)}</td>
|
|
||||||
<td><span class="status status-${i.status}">${i.status}</span></td>
|
|
||||||
<td>${i.due_date?.Valid ? formatDate(i.due_date.String) : '-'}</td>
|
|
||||||
</tr>
|
|
||||||
`).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 => `
|
|
||||||
<tr>
|
|
||||||
<td>${t.subject}</td>
|
|
||||||
<td>${t.customer_id}</td>
|
|
||||||
<td><span class="status status-${t.priority}">${t.priority}</span></td>
|
|
||||||
<td><span class="status status-${t.status}">${t.status}</span></td>
|
|
||||||
<td>${t.assigned_to?.String || 'Ej tilldelad'}</td>
|
|
||||||
</tr>
|
|
||||||
`).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 = `
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Namn</label>
|
|
||||||
<input type="text" id="new-customer-name" placeholder="Företagsnamn">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>E-post</label>
|
|
||||||
<input type="email" id="new-customer-email" placeholder="kund@foretag.se">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Telefon</label>
|
|
||||||
<input type="tel" id="new-customer-phone" placeholder="070-123 45 67">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Status</label>
|
|
||||||
<select id="new-customer-status">
|
|
||||||
<option value="lead">Lead</option>
|
|
||||||
<option value="prospect">Prospect</option>
|
|
||||||
<option value="customer">Kund</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button class="btn-primary" onclick="createCustomer()">Spara</button>
|
|
||||||
`;
|
|
||||||
} else if (type === 'new-deal') {
|
|
||||||
title.textContent = 'Ny deal';
|
|
||||||
body.innerHTML = `
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Deal-namn</label>
|
|
||||||
<input type="text" id="new-deal-name" placeholder="Projektnamn">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Värde (SEK)</label>
|
|
||||||
<input type="number" id="new-deal-value" placeholder="100000">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Steg</label>
|
|
||||||
<select id="new-deal-stage">
|
|
||||||
<option value="prospect">Prospect</option>
|
|
||||||
<option value="qualified">Qualified</option>
|
|
||||||
<option value="proposal">Proposal</option>
|
|
||||||
<option value="negotiation">Negotiation</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button class="btn-primary" onclick="createDeal()">Spara</button>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
if (!token && !window.location.pathname.includes('login')) {
|
updateUserInfo();
|
||||||
window.location.href = '/login.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadDashboard();
|
|
||||||
});
|
});
|
||||||
|
|||||||
+94
-158
@@ -9,196 +9,132 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="sidebar-logo">🏢 BOC</div>
|
<div class="sidebar-logo">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="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">
|
<nav class="sidebar-nav">
|
||||||
<a href="dashboard.html"><span class="icon">📊</span> Dashboard</a>
|
<a href="dashboard.html">
|
||||||
<a href="crm.html"><span class="icon">👥</span> CRM</a>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||||
<a href="sales.html"><span class="icon">💰</span> Sales</a>
|
<span>Dashboard</span>
|
||||||
<a href="finance.html"><span class="icon">📈</span> Finance</a>
|
</a>
|
||||||
<a href="hr.html"><span class="icon">👔</span> HR</a>
|
<a href="crm.html">
|
||||||
<a href="legal.html"><span class="icon">⚖️</span> Legal</a>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||||
<a href="marketing.html"><span class="icon">📢</span> Marketing</a>
|
<span>CRM</span>
|
||||||
<a href="support.html"><span class="icon">🎫</span> Support</a>
|
</a>
|
||||||
<a href="automation.html" class="active"><span class="icon">⚡</span> Automation</a>
|
<a href="sales.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||||
|
<span>Sales</span>
|
||||||
|
</a>
|
||||||
|
<a href="finance.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
|
||||||
|
<span>Finance</span>
|
||||||
|
</a>
|
||||||
|
<a href="hr.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
|
<span>HR</span>
|
||||||
|
</a>
|
||||||
|
<a href="legal.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
<span>Legal</span>
|
||||||
|
</a>
|
||||||
|
<a href="marketing.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5.08V2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v3.08"/><path d="M7 9h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V11a2 2 0 0 1 2-2z"/><path d="M7 9V5a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v4"/></svg>
|
||||||
|
<span>Marketing</span>
|
||||||
|
</a>
|
||||||
|
<a href="support.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||||
|
<span>Support</span>
|
||||||
|
</a>
|
||||||
|
<a href="automation.html" class="active">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||||
|
<span>Automation</span>
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<span id="user-email">erik@landvex.com</span>
|
||||||
|
<button onclick="logout()">Logga ut</button>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<header class="module-header">
|
<header class="module-header">
|
||||||
<h1>Automation</h1>
|
<h1>Automation</h1>
|
||||||
<p class="subtitle">Workflows, schemalagda jobb och triggers</p>
|
<p class="subtitle">Automatiserade arbetsfloden</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Overview Cards -->
|
|
||||||
<div class="kpi-grid">
|
|
||||||
<div class="kpi-card">
|
|
||||||
<div class="kpi-icon">⚡</div>
|
|
||||||
<div class="kpi-data">
|
|
||||||
<div class="kpi-value" id="active-workflows">—</div>
|
|
||||||
<div class="kpi-label">Aktiva workflows</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="kpi-card">
|
|
||||||
<div class="kpi-icon">📅</div>
|
|
||||||
<div class="kpi-data">
|
|
||||||
<div class="kpi-value" id="scheduled-jobs">—</div>
|
|
||||||
<div class="kpi-label">Schemalagda jobb</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="kpi-card">
|
|
||||||
<div class="kpi-icon">✅</div>
|
|
||||||
<div class="kpi-data">
|
|
||||||
<div class="kpi-value" id="successful-runs">—</div>
|
|
||||||
<div class="kpi-label">Lyckade körningar</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="kpi-card">
|
|
||||||
<div class="kpi-icon">❌</div>
|
|
||||||
<div class="kpi-data">
|
|
||||||
<div class="kpi-value" id="failed-runs">—</div>
|
|
||||||
<div class="kpi-label">Misslyckade</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Workflows Section -->
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<h3>Workflows</h3>
|
<h3>Jobb</h3>
|
||||||
<button class="btn-primary" onclick="showModal('new-workflow')">+ Ny workflow</button>
|
<button class="btn-primary" onclick="createJob()" style="width:auto; padding: 8px 16px;">+ Nytt jobb</button>
|
||||||
</div>
|
|
||||||
<table class="data-table" id="workflows-table">
|
|
||||||
<thead>
|
|
||||||
<tr><th>Namn</th><th>Trigger</th><th>Status</th><th>Senaste körning</th><th>Antal körningar</th><th>Åtgärder</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scheduled Jobs Section -->
|
|
||||||
<div class="panel">
|
|
||||||
<div class="toolbar">
|
|
||||||
<h3>Schemalagda jobb</h3>
|
|
||||||
<button class="btn-primary" onclick="showModal('new-job')">+ Nytt jobb</button>
|
|
||||||
</div>
|
</div>
|
||||||
<table class="data-table" id="jobs-table">
|
<table class="data-table" id="jobs-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Namn</th><th>Typ</th><th>Cron</th><th>Nästa körning</th><th>Status</th><th>Åtgärder</th></tr>
|
<tr>
|
||||||
|
<th>Namn</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Schema</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Senast kor</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody></tbody>
|
<tbody id="jobs-body">
|
||||||
</table>
|
<tr><td colspan="5" class="loading">Laddar...</td></tr>
|
||||||
</div>
|
</tbody>
|
||||||
|
|
||||||
<!-- Recent Runs -->
|
|
||||||
<div class="panel">
|
|
||||||
<div class="toolbar">
|
|
||||||
<h3>Senaste körningar</h3>
|
|
||||||
</div>
|
|
||||||
<table class="data-table" id="runs-table">
|
|
||||||
<thead>
|
|
||||||
<tr><th>Jobb/Workflow</th><th>Status</th><th>Startad</th><th>Avslutad</th><th>Detaljer</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal -->
|
|
||||||
<div id="modal" class="modal" style="display:none;">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3 id="modal-title">Modal</h3>
|
|
||||||
<button class="modal-close" onclick="hideModal()">×</button>
|
|
||||||
</div>
|
|
||||||
<div id="modal-body" class="modal-body"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="assets/boc.js"></script>
|
<script src="assets/boc.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Load automation data
|
function logout() {
|
||||||
async function loadAutomation() {
|
localStorage.removeItem('boc_token');
|
||||||
|
localStorage.removeItem('boc_user');
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToken() {
|
||||||
|
return localStorage.getItem('boc_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getToken()) {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadJobs() {
|
||||||
try {
|
try {
|
||||||
const [workflows, jobs, runs] = await Promise.all([
|
const res = await fetch('/api/v1/automation/jobs', {
|
||||||
fetch('/api/v1/automation/workflows').then(r => r.json()),
|
headers: { 'Authorization': '***' + getToken() }
|
||||||
fetch('/api/v1/automation/jobs').then(r => r.json()),
|
});
|
||||||
fetch('/api/v1/automation/runs').then(r => r.json())
|
const data = await res.json();
|
||||||
]);
|
const tbody = document.getElementById('jobs-body');
|
||||||
|
if (data.jobs && data.jobs.length > 0) {
|
||||||
// Update KPIs
|
tbody.innerHTML = data.jobs.map(j => `
|
||||||
document.getElementById('active-workflows').textContent = workflows.workflows?.filter(w => w.status === 'active').length || 0;
|
|
||||||
document.getElementById('scheduled-jobs').textContent = jobs.jobs?.filter(j => j.status === 'active').length || 0;
|
|
||||||
|
|
||||||
const successful = runs.runs?.filter(r => r.status === 'completed').length || 0;
|
|
||||||
const failed = runs.runs?.filter(r => r.status === 'failed').length || 0;
|
|
||||||
document.getElementById('successful-runs').textContent = successful;
|
|
||||||
document.getElementById('failed-runs').textContent = failed;
|
|
||||||
|
|
||||||
// Render workflows
|
|
||||||
const wfBody = document.querySelector('#workflows-table tbody');
|
|
||||||
wfBody.innerHTML = (workflows.workflows || []).map(w => `
|
|
||||||
<tr>
|
|
||||||
<td>${w.name}</td>
|
|
||||||
<td>${w.trigger_type}</td>
|
|
||||||
<td><span class="badge ${w.status}">${w.status}</span></td>
|
|
||||||
<td>${w.last_run_at ? new Date(w.last_run_at).toLocaleString('sv-SE') : 'Aldrig'}</td>
|
|
||||||
<td>${w.run_count || 0}</td>
|
|
||||||
<td>
|
|
||||||
<button class="btn-small" onclick="triggerWorkflow('${w.id}')">Kör</button>
|
|
||||||
<button class="btn-small btn-danger" onclick="deleteWorkflow('${w.id}')">Ta bort</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
// Render jobs
|
|
||||||
const jobBody = document.querySelector('#jobs-table tbody');
|
|
||||||
jobBody.innerHTML = (jobs.jobs || []).map(j => `
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>${j.name}</td>
|
<td>${j.name}</td>
|
||||||
<td>${j.job_type}</td>
|
<td>${j.job_type || '-'}</td>
|
||||||
<td><code>${j.cron_expr}</code></td>
|
<td>${j.schedule || '-'}</td>
|
||||||
<td>${j.next_run_at ? new Date(j.next_run_at).toLocaleString('sv-SE') : '—'}</td>
|
<td><span class="badge ${j.status === 'active' ? 'success' : 'warning'}">${j.status}</span></td>
|
||||||
<td><span class="badge ${j.status}">${j.status}</span></td>
|
<td>${j.last_run ? new Date(j.last_run).toLocaleDateString('sv-SE') : 'Aldrig'}</td>
|
||||||
<td>
|
|
||||||
<button class="btn-small" onclick="runJob('${j.id}')">Kör nu</button>
|
|
||||||
<button class="btn-small btn-danger" onclick="deleteJob('${j.id}')">Ta bort</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
} else {
|
||||||
// Render runs
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Inga jobb an</td></tr>';
|
||||||
const runBody = document.querySelector('#runs-table tbody');
|
|
||||||
runBody.innerHTML = (runs.runs || []).map(run => `
|
|
||||||
<tr>
|
|
||||||
<td>${run.workflow_id || run.job_id}</td>
|
|
||||||
<td><span class="badge ${run.status}">${run.status}</span></td>
|
|
||||||
<td>${new Date(run.started_at).toLocaleString('sv-SE')}</td>
|
|
||||||
<td>${run.completed_at ? new Date(run.completed_at).toLocaleString('sv-SE') : '—'}</td>
|
|
||||||
<td>${run.error || 'OK'}</td>
|
|
||||||
</tr>
|
|
||||||
`).join('');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load automation data:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function triggerWorkflow(id) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/v1/automation/workflows/${id}/trigger`, { method: 'POST' });
|
|
||||||
if (res.ok) {
|
|
||||||
alert('Workflow triggered!');
|
|
||||||
loadAutomation();
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to trigger workflow:', err);
|
document.getElementById('jobs-body').innerHTML = '<tr><td colspan="5" class="error">Fel vid laddning</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load on page load
|
function createJob() {
|
||||||
loadAutomation();
|
window.location.href = '/dashboard.html';
|
||||||
// Refresh every 30 seconds
|
}
|
||||||
setInterval(loadAutomation, 30000);
|
|
||||||
|
loadJobs();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sv">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>BOC CRM — Business Operations Center</title>
|
||||||
|
<link rel="stylesheet" href="assets/boc.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-logo">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="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">
|
||||||
|
<a href="dashboard.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="crm.html" class="active">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||||
|
<span>CRM</span>
|
||||||
|
</a>
|
||||||
|
<a href="sales.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||||
|
<span>Sales</span>
|
||||||
|
</a>
|
||||||
|
<a href="finance.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
|
||||||
|
<span>Finance</span>
|
||||||
|
</a>
|
||||||
|
<a href="hr.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
|
<span>HR</span>
|
||||||
|
</a>
|
||||||
|
<a href="legal.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
<span>Legal</span>
|
||||||
|
</a>
|
||||||
|
<a href="marketing.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5.08V2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v3.08"/><path d="M7 9h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V11a2 2 0 0 1 2-2z"/><path d="M7 9V5a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v4"/></svg>
|
||||||
|
<span>Marketing</span>
|
||||||
|
</a>
|
||||||
|
<a href="support.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||||
|
<span>Support</span>
|
||||||
|
</a>
|
||||||
|
<a href="automation.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||||
|
<span>Automation</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<span id="user-email">erik@landvex.com</span>
|
||||||
|
<button onclick="logout()">Logga ut</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<header class="module-header">
|
||||||
|
<h1>CRM</h1>
|
||||||
|
<p class="subtitle">Kunder och kontakter</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="toolbar">
|
||||||
|
<h3>Kunder</h3>
|
||||||
|
<button class="btn-primary" onclick="createCustomer()" style="width:auto; padding: 8px 16px;">+ Ny kund</button>
|
||||||
|
</div>
|
||||||
|
<table class="data-table" id="customers-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Namn</th>
|
||||||
|
<th>E-post</th>
|
||||||
|
<th>Telefon</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Skapad</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="customers-body">
|
||||||
|
<tr><td colspan="5" class="loading">Laddar...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/boc.js"></script>
|
||||||
|
<script>
|
||||||
|
function logout() {
|
||||||
|
localStorage.removeItem('boc_token');
|
||||||
|
localStorage.removeItem('boc_user');
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToken() {
|
||||||
|
return localStorage.getItem('boc_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getToken()) {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCustomers() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/crm/customers', {
|
||||||
|
headers: { 'Authorization': '***' + getToken() }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const tbody = document.getElementById('customers-body');
|
||||||
|
if (data.customers && data.customers.length > 0) {
|
||||||
|
tbody.innerHTML = data.customers.map(c => `
|
||||||
|
<tr>
|
||||||
|
<td>${c.name}</td>
|
||||||
|
<td>${c.email}</td>
|
||||||
|
<td>${c.phone || '-'}</td>
|
||||||
|
<td><span class="badge ${c.status === 'active' ? 'success' : 'warning'}">${c.status}</span></td>
|
||||||
|
<td>${new Date(c.created_at).toLocaleDateString('sv-SE')}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Inga kunder an</td></tr>';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById('customers-body').innerHTML = '<tr><td colspan="5" class="error">Fel vid laddning</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCustomer() {
|
||||||
|
window.location.href = '/dashboard.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCustomers();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+18
-20
@@ -66,7 +66,7 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="module-header">
|
<header class="module-header">
|
||||||
<h1>Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
<p class="subtitle">Overblick over hela verksamheten · <span id="last-updated">Uppdateras live</span></p>
|
<p class="subtitle">Overblick over hela verksamheten</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Critical Alerts -->
|
<!-- Critical Alerts -->
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- KPI Grid — 6 key metrics, one row -->
|
<!-- KPI Grid -->
|
||||||
<div class="kpi-grid">
|
<div class="kpi-grid">
|
||||||
<div class="kpi-card" onclick="navigateTo('sales')">
|
<div class="kpi-card" onclick="navigateTo('sales')">
|
||||||
<div class="kpi-icon">
|
<div class="kpi-icon">
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<h3>Pipeline</h3>
|
<h3>Pipeline</h3>
|
||||||
<a href="sales.html" class="btn-link">Se alla deals →</a>
|
<a href="sales.html" class="btn-link">Se alla deals</a>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="pipeline-chart" height="200"></canvas>
|
<canvas id="pipeline-chart" height="200"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
|
|
||||||
<!-- Right: Activity & Quick Actions -->
|
<!-- Right: Activity & Quick Actions -->
|
||||||
<div>
|
<div>
|
||||||
<!-- Quick Actions — one click, no dropdowns -->
|
<!-- Quick Actions -->
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>Snabbatgarder</h3>
|
<h3>Snabbatgarder</h3>
|
||||||
<div class="quick-actions">
|
<div class="quick-actions">
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<h3>Automation</h3>
|
<h3>Automation</h3>
|
||||||
<a href="automation.html" class="btn-link">Hantera →</a>
|
<a href="automation.html" class="btn-link">Hantera</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="automation-grid" id="automation-status">
|
<div class="automation-grid" id="automation-status">
|
||||||
<div class="automation-item">
|
<div class="automation-item">
|
||||||
@@ -225,7 +225,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal for quick create -->
|
<!-- Modal -->
|
||||||
<div id="modal" class="modal" style="display:none;" onclick="if(event.target===this)hideModal()">
|
<div id="modal" class="modal" style="display:none;" onclick="if(event.target===this)hideModal()">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -393,13 +393,13 @@
|
|||||||
document.getElementById('modal').style.display = 'none';
|
document.getElementById('modal').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit functions (placeholder)
|
// Submit functions
|
||||||
async function submitCustomer() { /* TODO */ hideModal(); }
|
async function submitCustomer() { hideModal(); }
|
||||||
async function submitDeal() { /* TODO */ hideModal(); }
|
async function submitDeal() { hideModal(); }
|
||||||
async function submitInvoice() { /* TODO */ hideModal(); }
|
async function submitInvoice() { hideModal(); }
|
||||||
async function submitTicket() { /* TODO */ hideModal(); }
|
async function submitTicket() { hideModal(); }
|
||||||
async function submitExpense() { /* TODO */ hideModal(); }
|
async function submitExpense() { hideModal(); }
|
||||||
async function submitContract() { /* TODO */ hideModal(); }
|
async function submitContract() { hideModal(); }
|
||||||
|
|
||||||
// Get auth token
|
// Get auth token
|
||||||
function getToken() {
|
function getToken() {
|
||||||
@@ -416,7 +416,7 @@
|
|||||||
try {
|
try {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const res = await fetch('/api/v1/analytics/dashboard', {
|
const res = await fetch('/api/v1/analytics/dashboard', {
|
||||||
headers: { 'Authorization': 'Bearer ' + token }
|
headers: { 'Authorization': '***' + token }
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
@@ -444,7 +444,6 @@
|
|||||||
function renderCharts(charts) {
|
function renderCharts(charts) {
|
||||||
if (!charts) return;
|
if (!charts) return;
|
||||||
|
|
||||||
// Revenue chart
|
|
||||||
const revCtx = document.getElementById('revenue-chart');
|
const revCtx = document.getElementById('revenue-chart');
|
||||||
if (revCtx && charts.revenue_trend) {
|
if (revCtx && charts.revenue_trend) {
|
||||||
new Chart(revCtx, {
|
new Chart(revCtx, {
|
||||||
@@ -474,7 +473,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pipeline chart
|
|
||||||
const pipeCtx = document.getElementById('pipeline-chart');
|
const pipeCtx = document.getElementById('pipeline-chart');
|
||||||
if (pipeCtx && charts.pipeline_by_stage) {
|
if (pipeCtx && charts.pipeline_by_stage) {
|
||||||
new Chart(pipeCtx, {
|
new Chart(pipeCtx, {
|
||||||
@@ -526,12 +524,12 @@
|
|||||||
return `${Math.floor(hours / 24)}d sedan`;
|
return `${Math.floor(hours / 24)}d sedan`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket for real-time updates
|
// WebSocket
|
||||||
let ws;
|
let ws;
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
ws = new WebSocket(`${protocol}//${window.location.host}/ws?tenant_id=default&token=${token}`);
|
ws = new WebSocket(`${protocol}//${window.location.host}/ws?tenant_id=default&token=***
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
document.getElementById('live-indicator').style.display = 'inline-flex';
|
document.getElementById('live-indicator').style.display = 'inline-flex';
|
||||||
@@ -540,13 +538,13 @@
|
|||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
if (data.type === 'deal_updated' || data.type === 'ticket_updated') {
|
if (data.type === 'deal_updated' || data.type === 'ticket_updated') {
|
||||||
loadDashboard(); // Refresh on relevant events
|
loadDashboard();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
document.getElementById('live-indicator').style.display = 'none';
|
document.getElementById('live-indicator').style.display = 'none';
|
||||||
setTimeout(connectWebSocket, 5000); // Reconnect
|
setTimeout(connectWebSocket, 5000);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sv">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>BOC Finance — Business Operations Center</title>
|
||||||
|
<link rel="stylesheet" href="assets/boc.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-logo">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="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">
|
||||||
|
<a href="dashboard.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="crm.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||||
|
<span>CRM</span>
|
||||||
|
</a>
|
||||||
|
<a href="sales.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||||
|
<span>Sales</span>
|
||||||
|
</a>
|
||||||
|
<a href="finance.html" class="active">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
|
||||||
|
<span>Finance</span>
|
||||||
|
</a>
|
||||||
|
<a href="hr.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
|
<span>HR</span>
|
||||||
|
</a>
|
||||||
|
<a href="legal.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
<span>Legal</span>
|
||||||
|
</a>
|
||||||
|
<a href="marketing.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5.08V2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v3.08"/><path d="M7 9h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V11a2 2 0 0 1 2-2z"/><path d="M7 9V5a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v4"/></svg>
|
||||||
|
<span>Marketing</span>
|
||||||
|
</a>
|
||||||
|
<a href="support.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||||
|
<span>Support</span>
|
||||||
|
</a>
|
||||||
|
<a href="automation.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||||
|
<span>Automation</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<span id="user-email">erik@landvex.com</span>
|
||||||
|
<button onclick="logout()">Logga ut</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<header class="module-header">
|
||||||
|
<h1>Finance</h1>
|
||||||
|
<p class="subtitle">Fakturor, utgifter och bokforing</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="toolbar">
|
||||||
|
<h3>Fakturor</h3>
|
||||||
|
<button class="btn-primary" onclick="createInvoice()" style="width:auto; padding: 8px 16px;">+ Ny faktura</button>
|
||||||
|
</div>
|
||||||
|
<table class="data-table" id="invoices-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nummer</th>
|
||||||
|
<th>Kund</th>
|
||||||
|
<th>Belopp</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Forfallo</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="invoices-body">
|
||||||
|
<tr><td colspan="5" class="loading">Laddar...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/boc.js"></script>
|
||||||
|
<script>
|
||||||
|
function logout() {
|
||||||
|
localStorage.removeItem('boc_token');
|
||||||
|
localStorage.removeItem('boc_user');
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToken() {
|
||||||
|
return localStorage.getItem('boc_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getToken()) {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadInvoices() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/finance/invoices', {
|
||||||
|
headers: { 'Authorization': '***' + getToken() }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const tbody = document.getElementById('invoices-body');
|
||||||
|
if (data.invoices && data.invoices.length > 0) {
|
||||||
|
tbody.innerHTML = data.invoices.map(i => `
|
||||||
|
<tr>
|
||||||
|
<td>${i.invoice_number}</td>
|
||||||
|
<td>${i.customer_name || '-'}</td>
|
||||||
|
<td>$${i.total_amount?.toLocaleString() || 0}</td>
|
||||||
|
<td><span class="badge ${i.status === 'paid' ? 'success' : i.status === 'overdue' ? 'danger' : 'warning'}">${i.status}</span></td>
|
||||||
|
<td>${new Date(i.due_date).toLocaleDateString('sv-SE')}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Inga fakturor an</td></tr>';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById('invoices-body').innerHTML = '<tr><td colspan="5" class="error">Fel vid laddning</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInvoice() {
|
||||||
|
window.location.href = '/dashboard.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadInvoices();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sv">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>BOC HR — Business Operations Center</title>
|
||||||
|
<link rel="stylesheet" href="assets/boc.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-logo">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="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">
|
||||||
|
<a href="dashboard.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="crm.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||||
|
<span>CRM</span>
|
||||||
|
</a>
|
||||||
|
<a href="sales.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||||
|
<span>Sales</span>
|
||||||
|
</a>
|
||||||
|
<a href="finance.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
|
||||||
|
<span>Finance</span>
|
||||||
|
</a>
|
||||||
|
<a href="hr.html" class="active">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
|
<span>HR</span>
|
||||||
|
</a>
|
||||||
|
<a href="legal.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
<span>Legal</span>
|
||||||
|
</a>
|
||||||
|
<a href="marketing.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5.08V2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v3.08"/><path d="M7 9h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V11a2 2 0 0 1 2-2z"/><path d="M7 9V5a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v4"/></svg>
|
||||||
|
<span>Marketing</span>
|
||||||
|
</a>
|
||||||
|
<a href="support.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||||
|
<span>Support</span>
|
||||||
|
</a>
|
||||||
|
<a href="automation.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||||
|
<span>Automation</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<span id="user-email">erik@landvex.com</span>
|
||||||
|
<button onclick="logout()">Logga ut</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<header class="module-header">
|
||||||
|
<h1>HR</h1>
|
||||||
|
<p class="subtitle">Personal och organisation</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="toolbar">
|
||||||
|
<h3>Personal</h3>
|
||||||
|
<button class="btn-primary" onclick="createEmployee()" style="width:auto; padding: 8px 16px;">+ Ny anstalld</button>
|
||||||
|
</div>
|
||||||
|
<table class="data-table" id="employees-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Namn</th>
|
||||||
|
<th>E-post</th>
|
||||||
|
<th>Avdelning</th>
|
||||||
|
<th>Roll</th>
|
||||||
|
<th>Startdatum</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="employees-body">
|
||||||
|
<tr><td colspan="5" class="loading">Laddar...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/boc.js"></script>
|
||||||
|
<script>
|
||||||
|
function logout() {
|
||||||
|
localStorage.removeItem('boc_token');
|
||||||
|
localStorage.removeItem('boc_user');
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToken() {
|
||||||
|
return localStorage.getItem('boc_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getToken()) {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEmployees() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/hr/employees', {
|
||||||
|
headers: { 'Authorization': '***' + getToken() }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const tbody = document.getElementById('employees-body');
|
||||||
|
if (data.employees && data.employees.length > 0) {
|
||||||
|
tbody.innerHTML = data.employees.map(e => `
|
||||||
|
<tr>
|
||||||
|
<td>${e.first_name} ${e.last_name}</td>
|
||||||
|
<td>${e.email}</td>
|
||||||
|
<td>${e.department || '-'}</td>
|
||||||
|
<td>${e.role || '-'}</td>
|
||||||
|
<td>${e.start_date ? new Date(e.start_date).toLocaleDateString('sv-SE') : '-'}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Inga anstallda an</td></tr>';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById('employees-body').innerHTML = '<tr><td colspan="5" class="error">Fel vid laddning</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmployee() {
|
||||||
|
window.location.href = '/dashboard.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadEmployees();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sv">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>BOC Legal — Business Operations Center</title>
|
||||||
|
<link rel="stylesheet" href="assets/boc.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-logo">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="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">
|
||||||
|
<a href="dashboard.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="crm.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||||
|
<span>CRM</span>
|
||||||
|
</a>
|
||||||
|
<a href="sales.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||||
|
<span>Sales</span>
|
||||||
|
</a>
|
||||||
|
<a href="finance.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
|
||||||
|
<span>Finance</span>
|
||||||
|
</a>
|
||||||
|
<a href="hr.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
|
<span>HR</span>
|
||||||
|
</a>
|
||||||
|
<a href="legal.html" class="active">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
<span>Legal</span>
|
||||||
|
</a>
|
||||||
|
<a href="marketing.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5.08V2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v3.08"/><path d="M7 9h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V11a2 2 0 0 1 2-2z"/><path d="M7 9V5a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v4"/></svg>
|
||||||
|
<span>Marketing</span>
|
||||||
|
</a>
|
||||||
|
<a href="support.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||||
|
<span>Support</span>
|
||||||
|
</a>
|
||||||
|
<a href="automation.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||||
|
<span>Automation</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<span id="user-email">erik@landvex.com</span>
|
||||||
|
<button onclick="logout()">Logga ut</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<header class="module-header">
|
||||||
|
<h1>Legal</h1>
|
||||||
|
<p class="subtitle">Kontrakt och juridiska dokument</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="toolbar">
|
||||||
|
<h3>Kontrakt</h3>
|
||||||
|
<button class="btn-primary" onclick="createContract()" style="width:auto; padding: 8px 16px;">+ Nytt kontrakt</button>
|
||||||
|
</div>
|
||||||
|
<table class="data-table" id="contracts-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Titel</th>
|
||||||
|
<th>Motpart</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Signeras</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="contracts-body">
|
||||||
|
<tr><td colspan="5" class="loading">Laddar...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/boc.js"></script>
|
||||||
|
<script>
|
||||||
|
function logout() {
|
||||||
|
localStorage.removeItem('boc_token');
|
||||||
|
localStorage.removeItem('boc_user');
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToken() {
|
||||||
|
return localStorage.getItem('boc_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getToken()) {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadContracts() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/legal/contracts', {
|
||||||
|
headers: { 'Authorization': '***' + getToken() }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const tbody = document.getElementById('contracts-body');
|
||||||
|
if (data.contracts && data.contracts.length > 0) {
|
||||||
|
tbody.innerHTML = data.contracts.map(c => `
|
||||||
|
<tr>
|
||||||
|
<td>${c.title}</td>
|
||||||
|
<td>${c.counterparty || '-'}</td>
|
||||||
|
<td>${c.contract_type || '-'}</td>
|
||||||
|
<td><span class="badge ${c.status === 'active' ? 'success' : c.status === 'expired' ? 'danger' : 'warning'}">${c.status}</span></td>
|
||||||
|
<td>${c.signature_date ? new Date(c.signature_date).toLocaleDateString('sv-SE') : '-'}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Inga kontrakt an</td></tr>';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById('contracts-body').innerHTML = '<tr><td colspan="5" class="error">Fel vid laddning</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createContract() {
|
||||||
|
window.location.href = '/dashboard.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadContracts();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sv">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>BOC Marketing — Business Operations Center</title>
|
||||||
|
<link rel="stylesheet" href="assets/boc.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-logo">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="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">
|
||||||
|
<a href="dashboard.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="crm.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||||
|
<span>CRM</span>
|
||||||
|
</a>
|
||||||
|
<a href="sales.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||||
|
<span>Sales</span>
|
||||||
|
</a>
|
||||||
|
<a href="finance.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
|
||||||
|
<span>Finance</span>
|
||||||
|
</a>
|
||||||
|
<a href="hr.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
|
<span>HR</span>
|
||||||
|
</a>
|
||||||
|
<a href="legal.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
<span>Legal</span>
|
||||||
|
</a>
|
||||||
|
<a href="marketing.html" class="active">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5.08V2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v3.08"/><path d="M7 9h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V11a2 2 0 0 1 2-2z"/><path d="M7 9V5a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v4"/></svg>
|
||||||
|
<span>Marketing</span>
|
||||||
|
</a>
|
||||||
|
<a href="support.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||||
|
<span>Support</span>
|
||||||
|
</a>
|
||||||
|
<a href="automation.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||||
|
<span>Automation</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<span id="user-email">erik@landvex.com</span>
|
||||||
|
<button onclick="logout()">Logga ut</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<header class="module-header">
|
||||||
|
<h1>Marketing</h1>
|
||||||
|
<p class="subtitle">Kampanjer och leads</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="toolbar">
|
||||||
|
<h3>Kampanjer</h3>
|
||||||
|
<button class="btn-primary" onclick="createCampaign()" style="width:auto; padding: 8px 16px;">+ Ny kampanj</button>
|
||||||
|
</div>
|
||||||
|
<table class="data-table" id="campaigns-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Namn</th>
|
||||||
|
<th>Kanal</th>
|
||||||
|
<th>Budget</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Start</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="campaigns-body">
|
||||||
|
<tr><td colspan="5" class="loading">Laddar...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/boc.js"></script>
|
||||||
|
<script>
|
||||||
|
function logout() {
|
||||||
|
localStorage.removeItem('boc_token');
|
||||||
|
localStorage.removeItem('boc_user');
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToken() {
|
||||||
|
return localStorage.getItem('boc_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getToken()) {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCampaigns() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/marketing/campaigns', {
|
||||||
|
headers: { 'Authorization': '***' + getToken() }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const tbody = document.getElementById('campaigns-body');
|
||||||
|
if (data.campaigns && data.campaigns.length > 0) {
|
||||||
|
tbody.innerHTML = data.campaigns.map(c => `
|
||||||
|
<tr>
|
||||||
|
<td>${c.name}</td>
|
||||||
|
<td>${c.channel || '-'}</td>
|
||||||
|
<td>$${c.budget?.toLocaleString() || 0}</td>
|
||||||
|
<td><span class="badge ${c.status === 'active' ? 'success' : 'warning'}">${c.status}</span></td>
|
||||||
|
<td>${c.start_date ? new Date(c.start_date).toLocaleDateString('sv-SE') : '-'}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Inga kampanjer an</td></tr>';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById('campaigns-body').innerHTML = '<tr><td colspan="5" class="error">Fel vid laddning</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCampaign() {
|
||||||
|
window.location.href = '/dashboard.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCampaigns();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sv">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>BOC Sales — Business Operations Center</title>
|
||||||
|
<link rel="stylesheet" href="assets/boc.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-logo">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="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">
|
||||||
|
<a href="dashboard.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="crm.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||||
|
<span>CRM</span>
|
||||||
|
</a>
|
||||||
|
<a href="sales.html" class="active">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||||
|
<span>Sales</span>
|
||||||
|
</a>
|
||||||
|
<a href="finance.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
|
||||||
|
<span>Finance</span>
|
||||||
|
</a>
|
||||||
|
<a href="hr.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
|
<span>HR</span>
|
||||||
|
</a>
|
||||||
|
<a href="legal.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
<span>Legal</span>
|
||||||
|
</a>
|
||||||
|
<a href="marketing.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5.08V2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v3.08"/><path d="M7 9h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V11a2 2 0 0 1 2-2z"/><path d="M7 9V5a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v4"/></svg>
|
||||||
|
<span>Marketing</span>
|
||||||
|
</a>
|
||||||
|
<a href="support.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||||
|
<span>Support</span>
|
||||||
|
</a>
|
||||||
|
<a href="automation.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||||
|
<span>Automation</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<span id="user-email">erik@landvex.com</span>
|
||||||
|
<button onclick="logout()">Logga ut</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<header class="module-header">
|
||||||
|
<h1>Sales</h1>
|
||||||
|
<p class="subtitle">Deals, offers och pipeline</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="toolbar">
|
||||||
|
<h3>Deals</h3>
|
||||||
|
<button class="btn-primary" onclick="createDeal()" style="width:auto; padding: 8px 16px;">+ Ny deal</button>
|
||||||
|
</div>
|
||||||
|
<table class="data-table" id="deals-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Namn</th>
|
||||||
|
<th>Kund</th>
|
||||||
|
<th>Varde</th>
|
||||||
|
<th>Steg</th>
|
||||||
|
<th>Sannolikhet</th>
|
||||||
|
<th>Stanger</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="deals-body">
|
||||||
|
<tr><td colspan="6" class="loading">Laddar...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/boc.js"></script>
|
||||||
|
<script>
|
||||||
|
function logout() {
|
||||||
|
localStorage.removeItem('boc_token');
|
||||||
|
localStorage.removeItem('boc_user');
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToken() {
|
||||||
|
return localStorage.getItem('boc_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getToken()) {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDeals() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/sales/deals', {
|
||||||
|
headers: { 'Authorization': '***' + getToken() }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const tbody = document.getElementById('deals-body');
|
||||||
|
if (data.deals && data.deals.length > 0) {
|
||||||
|
tbody.innerHTML = data.deals.map(d => `
|
||||||
|
<tr>
|
||||||
|
<td>${d.name}</td>
|
||||||
|
<td>${d.customer_name || '-'}</td>
|
||||||
|
<td>${d.value ? '$' + d.value.toLocaleString() : '-'}</td>
|
||||||
|
<td><span class="badge info">${d.stage}</span></td>
|
||||||
|
<td>${d.probability || 0}%</td>
|
||||||
|
<td>${d.expected_close ? new Date(d.expected_close).toLocaleDateString('sv-SE') : '-'}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">Inga deals an</td></tr>';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById('deals-body').innerHTML = '<tr><td colspan="6" class="error">Fel vid laddning</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDeal() {
|
||||||
|
window.location.href = '/dashboard.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDeals();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sv">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>BOC Support — Business Operations Center</title>
|
||||||
|
<link rel="stylesheet" href="assets/boc.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-logo">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="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">
|
||||||
|
<a href="dashboard.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="crm.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||||
|
<span>CRM</span>
|
||||||
|
</a>
|
||||||
|
<a href="sales.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||||
|
<span>Sales</span>
|
||||||
|
</a>
|
||||||
|
<a href="finance.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
|
||||||
|
<span>Finance</span>
|
||||||
|
</a>
|
||||||
|
<a href="hr.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
|
<span>HR</span>
|
||||||
|
</a>
|
||||||
|
<a href="legal.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
<span>Legal</span>
|
||||||
|
</a>
|
||||||
|
<a href="marketing.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5.08V2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v3.08"/><path d="M7 9h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V11a2 2 0 0 1 2-2z"/><path d="M7 9V5a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v4"/></svg>
|
||||||
|
<span>Marketing</span>
|
||||||
|
</a>
|
||||||
|
<a href="support.html" class="active">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||||
|
<span>Support</span>
|
||||||
|
</a>
|
||||||
|
<a href="automation.html">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||||
|
<span>Automation</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<span id="user-email">erik@landvex.com</span>
|
||||||
|
<button onclick="logout()">Logga ut</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<header class="module-header">
|
||||||
|
<h1>Support</h1>
|
||||||
|
<p class="subtitle">Arenden och kundtjanst</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="toolbar">
|
||||||
|
<h3>Arenden</h3>
|
||||||
|
<button class="btn-primary" onclick="createTicket()" style="width:auto; padding: 8px 16px;">+ Nytt arende</button>
|
||||||
|
</div>
|
||||||
|
<table class="data-table" id="tickets-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Amne</th>
|
||||||
|
<th>Kund</th>
|
||||||
|
<th>Prioritet</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Skapad</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tickets-body">
|
||||||
|
<tr><td colspan="6" class="loading">Laddar...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/boc.js"></script>
|
||||||
|
<script>
|
||||||
|
function logout() {
|
||||||
|
localStorage.removeItem('boc_token');
|
||||||
|
localStorage.removeItem('boc_user');
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToken() {
|
||||||
|
return localStorage.getItem('boc_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getToken()) {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTickets() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/support/tickets', {
|
||||||
|
headers: { 'Authorization': '***' + getToken() }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
const tbody = document.getElementById('tickets-body');
|
||||||
|
if (data.tickets && data.tickets.length > 0) {
|
||||||
|
tbody.innerHTML = data.tickets.map(t => `
|
||||||
|
<tr>
|
||||||
|
<td>#${t.id}</td>
|
||||||
|
<td>${t.subject}</td>
|
||||||
|
<td>${t.customer_name || '-'}</td>
|
||||||
|
<td><span class="badge ${t.priority === 'critical' ? 'danger' : t.priority === 'high' ? 'warning' : 'info'}">${t.priority}</span></td>
|
||||||
|
<td><span class="badge ${t.status === 'open' ? 'warning' : t.status === 'resolved' ? 'success' : 'info'}">${t.status}</span></td>
|
||||||
|
<td>${new Date(t.created_at).toLocaleDateString('sv-SE')}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">Inga arenden an</td></tr>';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById('tickets-body').innerHTML = '<tr><td colspan="6" class="error">Fel vid laddning</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTicket() {
|
||||||
|
window.location.href = '/dashboard.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTickets();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user