Files
boc/aamos-admin-upgrade/web/unified-dashboard.html
T

425 lines
13 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="sv" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AAMOS Admin — Unified Dashboard</title>
<link rel="stylesheet" href="assets/admin.css">
<style>
/* Alert banner */
.alert-banner {
background: linear-gradient(135deg, #FF3B30, #FF6B6B);
color: white;
padding: 12px 20px;
border-radius: 12px;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
animation: slideDown 0.4s ease;
}
.alert-banner.warning {
background: linear-gradient(135deg, #FF9500, #FFB84D);
}
.alert-banner.info {
background: linear-gradient(135deg, #007AFF, #5AC8FA);
}
.alert-banner .alert-icon { font-size: 22px; }
.alert-banner .alert-content { flex: 1; }
.alert-banner .alert-title { font-weight: 700; font-size: 15px; }
.alert-banner .alert-meta { font-size: 13px; opacity: 0.9; }
.alert-banner .alert-amount { font-weight: 700; font-size: 18px; }
@keyframes slideDown {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: none; }
}
/* System cards grid */
.systems-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.system-card {
background: var(--bg-card);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow);
transition: transform 0.2s, box-shadow 0.2s;
border-left: 4px solid var(--primary);
}
.system-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.system-card.quixzoom { border-left-color: #34C759; }
.system-card.landvex { border-left-color: #AF52DE; }
.system-card.economy { border-left-color: #FF9500; }
.system-card.crm { border-left-color: #32ADE6; }
.system-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.system-icon {
width: 40px; height: 40px;
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 20px;
}
.system-card.quixzoom .system-icon { background: rgba(52,199,89,0.12); }
.system-card.landvex .system-icon { background: rgba(175,82,222,0.12); }
.system-card.economy .system-icon { background: rgba(255,149,0,0.12); }
.system-card.crm .system-icon { background: rgba(50,173,230,0.12); }
.system-name { font-size: 17px; font-weight: 700; }
.system-status {
margin-left: auto;
font-size: 12px;
font-weight: 600;
padding: 4px 10px;
border-radius: 20px;
background: rgba(52,199,89,0.12);
color: var(--success);
}
.metrics-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.metric-item {
text-align: center;
padding: 10px;
background: var(--bg-input);
border-radius: var(--radius-sm);
}
.metric-value {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
}
.metric-label {
font-size: 12px;
color: var(--text-tertiary);
margin-top: 2px;
}
/* Service health mini */
.health-mini {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 12px;
}
.health-pill {
font-size: 11px;
font-weight: 600;
padding: 3px 10px;
border-radius: 20px;
background: var(--bg-input);
}
.health-pill.ok { background: rgba(52,199,89,0.12); color: var(--success); }
.health-pill.warn { background: rgba(255,149,0,0.12); color: var(--warning); }
.health-pill.error { background: rgba(255,59,48,0.12); color: var(--danger); }
/* Cash position */
.cash-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-top: 12px;
}
.cash-item {
text-align: center;
padding: 12px;
background: var(--bg-input);
border-radius: var(--radius-sm);
}
.cash-value { font-size: 20px; font-weight: 700; }
.cash-label { font-size: 11px; color: var(--text-tertiary); }
/* Deadline countdown */
.countdown {
font-family: 'SF Mono', monospace;
font-size: 14px;
font-weight: 600;
color: #FF3B30;
}
</style>
</head>
<body>
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-brand">
<div class="brand-icon">
<svg viewBox="0 0 24 24"><path d="M12 2L2 7v10l10 5 10-5V7L12 2zm0 2.18L20 8.5v7L12 19.82 4 15.5v-7l8-4.32z"/></svg>
</div>
<div class="brand-text">
<div class="brand-name">AAMOS Admin</div>
<div class="brand-sub">Unified v2.0</div>
</div>
</div>
<nav class="sidebar-nav">
<div class="nav-section-label">Overview</div>
<a class="nav-item active" href="unified-dashboard.html">
<span class="nav-icon">📊</span>
Dashboard
</a>
<a class="nav-item" href="users.html">
<span class="nav-icon">👥</span>
Users
</a>
<a class="nav-item" href="modules.html">
<span class="nav-icon">🔧</span>
Modules
</a>
<div class="nav-section-label">Systems</div>
<a class="nav-item" href="quixzoom.html">
<span class="nav-icon">📸</span>
quiXzoom
</a>
<a class="nav-item" href="landvex.html">
<span class="nav-icon">🌍</span>
LandveX
</a>
<a class="nav-item" href="economy.html">
<span class="nav-icon">💰</span>
Economy
</a>
<a class="nav-item" href="crm.html">
<span class="nav-icon">🤝</span>
CRM
</a>
<div class="nav-section-label">System</div>
<a class="nav-item" href="audit.html">
<span class="nav-icon">📋</span>
Audit Log
</a>
<a class="nav-item" href="settings.html">
<span class="nav-icon">⚙️</span>
Settings
</a>
</nav>
<div class="sidebar-footer">
<div class="sidebar-user">
<div class="user-avatar" id="user-avatar">SA</div>
<div class="user-info">
<div class="user-name" id="user-name">System Admin</div>
<div class="user-role">Super Admin</div>
</div>
</div>
</div>
</aside>
<!-- Main -->
<div class="main">
<header class="topbar">
<div class="topbar-title">Unified Dashboard</div>
<div class="topbar-actions">
<div class="refresh-indicator">
<div class="refresh-dot" id="refresh-dot"></div>
<span id="refresh-label">Live</span>
</div>
<button class="icon-btn" id="refresh-btn" title="Refresh">🔄</button>
</div>
</header>
<section class="page-content active">
<!-- Alerts -->
<div id="alerts-container"></div>
<!-- Systems Overview -->
<div class="systems-grid" id="systems-grid">
<!-- Filled by JS -->
</div>
<!-- Service Health -->
<div class="card">
<div class="section-header">
<span class="section-title">Service Health</span>
<span id="services-status">Loading...</span>
</div>
<div id="services-list" class="health-mini"></div>
</div>
</section>
</div>
<script src="assets/api.js"></script>
<script>
'use strict';
const api = createAdminAPI(window.location.origin);
function formatNumber(n) {
return n?.toLocaleString('sv-SE') || '—';
}
function formatCurrency(sek) {
if (!sek) return '—';
return new Intl.NumberFormat('sv-SE', {
style: 'currency', currency: 'SEK', maximumFractionDigits: 0
}).format(sek);
}
function daysUntil(date) {
const diff = Math.ceil((new Date(date) - Date.now()) / (1000 * 60 * 60 * 24));
return diff;
}
function renderAlerts(alerts) {
const container = document.getElementById('alerts-container');
if (!alerts?.length) {
container.innerHTML = '';
return;
}
container.innerHTML = alerts.map(a => {
const days = a.deadline ? daysUntil(a.deadline) : null;
const levelClass = a.level === 'critical' ? '' : a.level;
const icon = a.level === 'critical' ? '🚨' : a.level === 'warning' ? '⚠️' : '️';
const countdown = days !== null ? `<span class="countdown">${days} dagar kvar</span>` : '';
const amount = a.amount_sek ? `<span class="alert-amount">${formatCurrency(a.amount_sek)}</span>` : '';
return `
<div class="alert-banner ${levelClass}">
<span class="alert-icon">${icon}</span>
<div class="alert-content">
<div class="alert-title">${a.message}</div>
<div class="alert-meta">${countdown} ${amount}</div>
</div>
</div>
`;
}).join('');
}
function renderSystems(data) {
const grid = document.getElementById('systems-grid');
const systems = [
{
id: 'quixzoom',
name: 'quiXzoom',
icon: '📸',
metrics: [
{ label: 'Missions', value: data.quixzoom?.total_missions },
{ label: 'Active', value: data.quixzoom?.active_missions },
{ label: 'Submissions', value: data.quixzoom?.total_submissions },
{ label: 'Contributors', value: data.quixzoom?.contributors },
]
},
{
id: 'landvex',
name: 'LandveX',
icon: '🌍',
metrics: [
{ label: 'Reports', value: data.landvex?.intelligence_reports },
{ label: 'Data Points', value: data.landvex?.urban_data_points },
{ label: 'Customers', value: data.landvex?.active_customers },
{ label: 'Modules', value: data.modules_total },
]
},
{
id: 'economy',
name: 'Economy',
icon: '💰',
metrics: [
{ label: 'Vouchers', value: data.economy?.total_vouchers },
{ label: 'Accounts', value: data.economy?.total_accounts },
{ label: 'Pending', value: data.economy?.pending_invoices },
{ label: 'Balance', value: data.economy?.trial_balance },
]
},
{
id: 'crm',
name: 'CRM',
icon: '🤝',
metrics: [
{ label: 'Customers', value: data.crm?.total_customers },
{ label: 'Leads', value: data.crm?.active_leads },
{ label: 'Won', value: data.crm?.deals_won },
{ label: 'Pipeline', value: data.crm?.pipeline_value_sek },
]
}
];
grid.innerHTML = systems.map(sys => `
<div class="system-card ${sys.id}">
<div class="system-header">
<span class="system-icon">${sys.icon}</span>
<span class="system-name">${sys.name}</span>
<span class="system-status">● Live</span>
</div>
<div class="metrics-row">
${sys.metrics.map(m => `
<div class="metric-item">
<div class="metric-value">${typeof m.value === 'number' && m.value > 1000 ? formatNumber(m.value) : m.value || '—'}</div>
<div class="metric-label">${m.label}</div>
</div>
`).join('')}
</div>
</div>
`).join('');
}
function renderServices(services) {
const list = document.getElementById('services-list');
const status = document.getElementById('services-status');
if (!services?.length) {
list.innerHTML = '<span style="color:var(--text-tertiary)">No services found</span>';
status.textContent = '';
return;
}
const healthy = services.filter(s => s.status === 'running').length;
status.textContent = `${healthy}/${services.length} healthy`;
list.innerHTML = services.slice(0, 12).map(s => {
const cls = s.status === 'running' ? 'ok' : s.status === 'degraded' ? 'warn' : 'error';
return `<span class="health-pill ${cls}">${s.name}</span>`;
}).join('');
}
async function loadDashboard() {
const dot = document.getElementById('refresh-dot');
dot.classList.add('loading');
try {
// Load unified dashboard
const data = await api._get('/api/v1/unified-dashboard');
renderAlerts(data.alerts);
renderSystems(data);
// Load service health
const health = await api._get('/api/v1/service-health');
renderServices(health.services);
document.getElementById('refresh-label').textContent = 'Live';
} catch (err) {
console.error('Dashboard load error:', err);
document.getElementById('refresh-label').textContent = 'Error';
dot.style.background = 'var(--danger)';
} finally {
dot.classList.remove('loading');
}
}
// Init
loadDashboard();
setInterval(loadDashboard, 30000);
document.getElementById('refresh-btn').addEventListener('click', loadDashboard);
</script>
</body>
</html>