6de2455917
- Added GLOBAL_MARKETS_TITLE to all translation files - Updated footer with 12 markets (4 active + 8 upcoming) - Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi - Built and deployed to production - CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
425 lines
13 KiB
HTML
425 lines
13 KiB
HTML
<!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>
|