aee0f09db8
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
362 lines
53 KiB
HTML
362 lines
53 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="sv">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<meta name="theme-color" content="#001E50">
|
||
<title>AAMOS Command Center — Landvex Operations</title>
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<style>
|
||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||
:root{--navy:#001E50;--amber:#C8763A;--bg:#F5F5F0;--card:#fff;--muted:#6B6B70;--green:#34C759;--red:#FF3B30;--yellow:#FFCC00;--border:#E8E3DC;--radius:14px;--font:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif}
|
||
html{font-family:var(--font);font-size:16px;background:var(--bg);color:var(--navy)}
|
||
body{min-height:100vh;padding-bottom:80px}
|
||
#header{background:var(--navy);color:#fff;padding:16px 20px;position:sticky;top:0;z-index:100;display:flex;align-items:center;gap:12px}
|
||
#header .logo{width:36px;height:36px;border-radius:10px;background:var(--amber);display:flex;align-items:center;justify-content:center;font-size:18px}
|
||
#header h1{font-size:1.1rem;font-weight:700;flex:1}
|
||
#header .status{font-size:.75rem;opacity:.7}
|
||
#nav{display:flex;gap:8px;padding:12px 16px;overflow-x:auto;background:#fff;border-bottom:1px solid var(--border);-webkit-overflow-scrolling:touch;scrollbar-width:none}
|
||
#nav::-webkit-scrollbar{display:none}
|
||
#nav button{white-space:nowrap;padding:8px 16px;border-radius:20px;border:1px solid var(--border);background:#fff;color:var(--navy);font-size:.88rem;font-weight:500;cursor:pointer}
|
||
#nav button.active{background:var(--navy);color:#fff;border-color:var(--navy)}
|
||
.section{padding:16px;display:none}
|
||
.section.active{display:block}
|
||
.card{background:var(--card);border-radius:var(--radius);padding:16px;margin-bottom:12px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
|
||
.card h3{font-size:.85rem;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-bottom:12px}
|
||
.card h3 .live{color:var(--green);font-size:.7rem;margin-left:6px;animation:pulse 2s infinite}
|
||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
||
.kpi-row{display:flex;gap:12px;overflow-x:auto}
|
||
.kpi{min-width:140px;flex:1;background:var(--card);border-radius:var(--radius);padding:16px;text-align:center;border:1px solid var(--border)}
|
||
.kpi .value{font-size:1.6rem;font-weight:700;color:var(--navy)}
|
||
.kpi .value.good{color:var(--green)}.kpi .value.bad{color:var(--red)}.kpi .value.warn{color:var(--amber)}
|
||
.kpi .label{font-size:.75rem;color:var(--muted);margin-top:4px}
|
||
.alert{display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:var(--radius);margin-bottom:10px;font-size:.9rem}
|
||
.alert.critical{background:#FFE5E5;border-left:4px solid var(--red)}.alert.warning{background:#FFF8E5;border-left:4px solid var(--yellow)}.alert.info{background:#E5F2FF;border-left:4px solid #007AFF}
|
||
.alert .icon{font-size:1.3rem}
|
||
table{width:100%;border-collapse:collapse;font-size:.88rem}
|
||
th{text-align:left;padding:10px 8px;color:var(--muted);font-weight:500;border-bottom:1px solid var(--border)}
|
||
td{padding:12px 8px;border-bottom:1px solid var(--border)}tr:last-child td{border-bottom:none}
|
||
.badge{display:inline-block;padding:3px 10px;border-radius:12px;font-size:.75rem;font-weight:600}.badge.green{background:#E5F9ED;color:#1A7F3C}.badge.red{background:#FFE5E5;color:#C41E3A}.badge.yellow{background:#FFF8E5;color:#B8860B}.badge.blue{background:#E5F2FF;color:#007AFF}
|
||
.btn{display:inline-block;padding:10px 20px;border-radius:10px;border:none;background:var(--navy);color:#fff;font-size:.9rem;font-weight:600;cursor:pointer;margin-top:8px}
|
||
.btn.secondary{background:var(--bg);color:var(--navy);border:1px solid var(--border)}.btn.red{background:var(--red)}.btn.green{background:var(--green)}.btn.small{padding:6px 12px;font-size:.8rem}
|
||
#bottom-nav{position:fixed;bottom:0;left:0;right:0;background:#fff;border-top:1px solid var(--border);display:flex;justify-content:space-around;padding:8px 0;z-index:100}
|
||
#bottom-nav button{background:none;border:none;font-size:.7rem;color:var(--muted);display:flex;flex-direction:column;align-items:center;gap:3px;cursor:pointer;padding:4px 12px}
|
||
#bottom-nav button.active{color:var(--navy)}#bottom-nav button .emoji{font-size:1.3rem}
|
||
.service-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border)}.service-row:last-child{border-bottom:none}
|
||
.service-name{font-weight:600}.service-meta{font-size:.8rem;color:var(--muted)}
|
||
.loading{text-align:center;padding:40px;color:var(--muted)}
|
||
.error{text-align:center;padding:20px;color:var(--red);font-size:.9rem}
|
||
.success{text-align:center;padding:20px;color:var(--green);font-size:.9rem}
|
||
input,textarea,select{width:100%;padding:10px;border:1px solid var(--border);border-radius:8px;font-size:.9rem;font-family:inherit;margin-bottom:8px}
|
||
.form-group{margin-bottom:12px}
|
||
.form-group label{display:block;font-size:.85rem;font-weight:600;margin-bottom:4px;color:var(--muted)}
|
||
.modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);display:none;align-items:center;justify-content:center;z-index:200;padding:20px}
|
||
.modal.active{display:flex}
|
||
.modal-content{background:#fff;border-radius:var(--radius);padding:20px;max-width:600px;width:100%;max-height:90vh;overflow-y:auto}
|
||
.modal-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}
|
||
.modal-header h2{font-size:1.2rem}
|
||
.close-btn{background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--muted)}
|
||
.json-view{font-family:monospace;font-size:.8rem;background:var(--bg);padding:12px;border-radius:8px;overflow-x:auto;max-height:300px;overflow-y:auto}
|
||
#map,#overview-map{height:400px;border-radius:var(--radius);margin-bottom:12px}
|
||
#qz-map{height:350px;border-radius:var(--radius);margin-bottom:12px}
|
||
.search-box{display:flex;gap:8px;margin-bottom:12px}
|
||
.search-box input{flex:1;margin-bottom:0}
|
||
.tabs{display:flex;gap:8px;margin-bottom:12px;border-bottom:1px solid var(--border);padding-bottom:8px;overflow-x:auto}
|
||
.tabs button{background:none;border:none;padding:8px 16px;font-size:.9rem;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap}
|
||
.tabs button.active{color:var(--navy);border-bottom-color:var(--navy);font-weight:600}
|
||
.filter-row{display:flex;gap:8px;margin-bottom:12px;overflow-x:auto}
|
||
.filter-row button{padding:6px 12px;border-radius:16px;border:1px solid var(--border);background:#fff;font-size:.8rem;cursor:pointer;white-space:nowrap}
|
||
.filter-row button.active{background:var(--navy);color:#fff;border-color:var(--navy)}
|
||
.support-ticket{border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:8px;cursor:pointer}
|
||
.support-ticket:hover{background:var(--bg)}
|
||
.support-ticket .ticket-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
|
||
.support-ticket .ticket-title{font-weight:600}
|
||
.support-ticket .ticket-meta{font-size:.8rem;color:var(--muted);margin-top:4px}
|
||
.mission-detail{padding:16px;background:var(--bg);border-radius:8px;margin-top:12px}
|
||
.mission-detail h4{margin-bottom:8px;color:var(--navy)}
|
||
.mission-detail p{margin-bottom:4px;font-size:.9rem}
|
||
.timeline{position:relative;padding-left:24px;margin-top:12px}
|
||
.timeline::before{content:'';position:absolute;left:8px;top:0;bottom:0;width:2px;background:var(--border)}
|
||
.timeline-item{position:relative;margin-bottom:16px}
|
||
.timeline-item::before{content:'';position:absolute;left:-20px;top:4px;width:10px;height:10px;border-radius:50%;background:var(--amber);border:2px solid #fff}
|
||
.timeline-item.completed::before{background:var(--green)}
|
||
.timeline-item.pending::before{background:var(--muted)}
|
||
.timeline-date{font-size:.75rem;color:var(--muted)}
|
||
.timeline-text{font-size:.9rem;margin-top:2px}
|
||
.chart-bar{display:flex;align-items:center;margin-bottom:6px}
|
||
.chart-bar-label{width:100px;font-size:.8rem;color:var(--muted)}
|
||
.chart-bar-fill{height:20px;background:var(--navy);border-radius:4px;transition:width .3s}
|
||
.chart-bar-value{margin-left:8px;font-size:.8rem;font-weight:600}
|
||
.action-menu{position:absolute;right:0;top:40px;background:#fff;border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.1);display:none;z-index:50}
|
||
.action-menu.active{display:block}
|
||
.action-menu button{display:block;width:100%;padding:10px 16px;border:none;background:none;text-align:left;cursor:pointer;font-size:.9rem}
|
||
.action-menu button:hover{background:var(--bg)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="header"><div class="logo">🔁</div><h1>AAMOS Command Center</h1><div class="status" id="clock">--:--</div></div>
|
||
<div id="nav">
|
||
<button class="active" onclick="showSection('overview')">Översikt</button>
|
||
<button onclick="showSection('finance')">Ekonomi</button>
|
||
<button onclick="showSection('quixzoom')">quiXzoom</button>
|
||
<button onclick="showSection('support')">Support</button>
|
||
<button onclick="showSection('apis')">API:er</button>
|
||
<button onclick="showSection('systems')">System</button>
|
||
</div>
|
||
|
||
<!-- ÖVERSIKT -->
|
||
<div id="overview" class="section active">
|
||
<div class="kpi-row">
|
||
<div class="kpi"><div class="value" id="kpi-cash">--</div><div class="label">Kassa (SEK)</div></div>
|
||
<div class="kpi"><div class="value" id="kpi-missions">--</div><div class="label">Missions</div></div>
|
||
<div class="kpi"><div class="value" id="kpi-entries">--</div><div class="label">Verifikat</div></div>
|
||
<div class="kpi"><div class="value" id="kpi-services">--</div><div class="label">Tjänster</div></div>
|
||
<div class="kpi"><div class="value" id="kpi-tickets">--</div><div class="label">Ärenden</div></div>
|
||
</div>
|
||
<div class="card"><h3>🚨 Kritiska åtgärder <span class="live">● LIVE</span></h3><div id="critical-alerts"><div class="loading">Laddar...</div></div></div>
|
||
<div class="card"><h3>🗺️ Operationskarta <span class="live">● LIVE</span></h3><div id="overview-map"></div></div>
|
||
<div class="card"><h3>📈 Aktivitet senaste 7 dagarna</h3><div id="activity-chart"><div class="loading">Laddar...</div></div></div>
|
||
<div class="card"><h3>🖥️ Systemstatus <span class="live">● LIVE</span></h3><div id="system-status"><div class="loading">Laddar...</div></div></div>
|
||
</div>
|
||
|
||
<!-- EKONOMI -->
|
||
<div id="finance" class="section">
|
||
<div class="kpi-row">
|
||
<div class="kpi"><div class="value" id="fin-accounts">--</div><div class="label">Konton</div></div>
|
||
<div class="kpi"><div class="value" id="fin-periods">--</div><div class="label">Perioder</div></div>
|
||
<div class="kpi"><div class="value" id="fin-entries">--</div><div class="label">Verifikat</div></div>
|
||
<div class="kpi"><div class="value" id="fin-balance">--</div><div class="label">Total balans</div></div>
|
||
</div>
|
||
<div class="card"><h3>🔍 Sök verifikat</h3><div class="search-box"><input type="text" id="journal-search" placeholder="Sök beskrivning, konto, belopp, datum..." onkeyup="searchJournal()"><button class="btn" onclick="searchJournal()">🔍 Sök</button></div></div>
|
||
<div class="card"><h3>🏦 Konton <span class="live">● LIVE</span></h3><div class="filter-row"><button class="active" onclick="filterAccounts('all')">Alla</button><button onclick="filterAccounts('asset')">Tillgångar</button><button onclick="filterAccounts('liability')">Skulder</button><button onclick="filterAccounts('income')">Intäkter</button><button onclick="filterAccounts('expense')">Kostnader</button></div><div id="accounts-list"><div class="loading">Laddar...</div></div></div>
|
||
<div class="card"><h3>📅 Perioder <span class="live">● LIVE</span></h3><table id="periods-table"><tr><th>Period</th><th>År</th><th>Status</th><th>Stängd</th><th>Åtgärd</th></tr><tr><td colspan="5" class="loading">Laddar...</td></tr></table></div>
|
||
<div class="card"><h3>📊 Verifikat <span class="live">● LIVE</span></h3><div id="journal-list"><div class="loading">Laddar...</div></div></div>
|
||
<div class="card"><h3>📈 Balansfördelning</h3><div id="balance-chart"><div class="loading">Laddar...</div></div></div>
|
||
<div class="card"><h3>➕ Nytt verifikat</h3><button class="btn" onclick="openModal('journal-modal')">📄 Skapa verifikat</button></div>
|
||
<div class="card"><h3>🔗 Export & Rapporter</h3><a class="btn" href="/api/ledger/export/sie4?fiscal_year=2026" target="_blank">📥 SIE4 Export 2026</a><button class="btn secondary" onclick="generateReport()">📊 Generera rapport</button></div>
|
||
</div>
|
||
|
||
<!-- QUIXZOOM -->
|
||
<div id="quixzoom" class="section">
|
||
<div class="kpi-row">
|
||
<div class="kpi"><div class="value" id="qz-missions">--</div><div class="label">Missions</div></div>
|
||
<div class="kpi"><div class="value" id="qz-open">--</div><div class="label">Öppna</div></div>
|
||
<div class="kpi"><div class="value" id="qz-assigned">--</div><div class="label">Tilldelade</div></div>
|
||
<div class="kpi"><div class="value" id="qz-reward">--</div><div class="label">Reward-pool</div></div>
|
||
</div>
|
||
<div class="card"><h3>🔍 Sök missions</h3><div class="search-box"><input type="text" id="mission-search" placeholder="Sök titel, adress, status, zoomer..." onkeyup="searchMissions()"><button class="btn" onclick="searchMissions()">🔍 Sök</button></div></div>
|
||
<div class="card"><h3>🗺️ Missionskarta</h3><div id="qz-map"></div></div>
|
||
<div class="card"><h3>📱 Missions <span class="live">● LIVE</span></h3><div class="filter-row">
|
||
<button class="active" onclick="filterMissions('all')">Alla</button>
|
||
<button onclick="filterMissions('open')">Öppna</button>
|
||
<button onclick="filterMissions('assigned')">Tilldelade</button>
|
||
<button onclick="filterMissions('submitted')">Inskickade</button>
|
||
<button onclick="filterMissions('approved')">Godkända</button>
|
||
</div><div id="missions-list"><div class="loading">Laddar...</div></div></div>
|
||
<div class="card"><h3>📊 Mission-statistik</h3><div id="mission-stats"><div class="loading">Laddar...</div></div></div>
|
||
<div class="card"><h3>➕ Nytt uppdrag</h3><button class="btn" onclick="openModal('mission-modal')">🎯 Skapa mission</button></div>
|
||
</div>
|
||
|
||
<!-- SUPPORT -->
|
||
<div id="support" class="section">
|
||
<div class="kpi-row">
|
||
<div class="kpi"><div class="value" id="sup-open">--</div><div class="label">Öppna</div></div>
|
||
<div class="kpi"><div class="value" id="sup-critical">--</div><div class="label">Kritiska</div></div>
|
||
<div class="kpi"><div class="value" id="sup-today">--</div><div class="label">Idag</div></div>
|
||
<div class="kpi"><div class="value" id="sup-avg">--</div><div class="label">Snittid (h)</div></div>
|
||
</div>
|
||
<div class="card"><h3>🔍 Sök ärenden</h3><div class="search-box"><input type="text" id="ticket-search" placeholder="Sök ärende, zoomer, mission, beskrivning..." onkeyup="searchTickets()"><button class="btn" onclick="searchTickets()">🔍 Sök</button></div></div>
|
||
<div class="card"><h3>📋 Supportärenden</h3><div class="tabs">
|
||
<button class="active" onclick="filterTickets('all')">Alla</button>
|
||
<button onclick="filterTickets('open')">Öppna</button>
|
||
<button onclick="filterTickets('critical')">Kritiska</button>
|
||
<button onclick="filterTickets('resolved')">Lösta</button>
|
||
</div><div id="tickets-list"><div class="loading">Laddar...</div></div></div>
|
||
<div class="card"><h3>➕ Nytt ärende</h3><button class="btn" onclick="openModal('ticket-modal')">🎫 Skapa ärende</button></div>
|
||
</div>
|
||
|
||
<!-- API:ER -->
|
||
<div id="apis" class="section">
|
||
<div class="card"><h3>🔌 API-administration <span class="live">● LIVE</span></h3><div id="api-list"><div class="loading">Laddar...</div></div></div>
|
||
<div class="card"><h3>🌍 Landvex Intelligence</h3>
|
||
<div class="form-group"><label>Latitud</label><input type="number" id="intel-lat" placeholder="59.3349" step="0.0001"></div>
|
||
<div class="form-group"><label>Longitud</label><input type="number" id="intel-lng" placeholder="18.0632" step="0.0001"></div>
|
||
<div class="form-group"><label>Profil</label><select id="intel-profile"><option value="business">Business</option><option value="infrastructure">Infrastructure</option><option value="safety">Safety</option><option value="investment">Investment</option><option value="nightlife">Nightlife</option><option value="tourist">Tourist</option></select></div>
|
||
<button class="btn" onclick="testIntelligence()">🔍 Analysera plats</button>
|
||
<div id="intel-result" style="margin-top:12px"></div>
|
||
</div>
|
||
<div class="card"><h3>📡 Batch-analys</h3>
|
||
<div class="form-group"><label>Punkter (JSON-format: [{"lat":x,"lng":y}])</label><textarea id="batch-points" rows="3" placeholder='[{"lat":59.3349,"lng":18.0632},{"lat":59.3421,"lng":18.0559}]'></textarea></div>
|
||
<div class="form-group"><label>Profil</label><select id="batch-profile"><option value="business">Business</option><option value="infrastructure">Infrastructure</option></select></div>
|
||
<button class="btn" onclick="testBatchIntelligence()">🚀 Kör batch</button>
|
||
<div id="batch-result" style="margin-top:12px"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SYSTEM -->
|
||
<div id="systems" class="section">
|
||
<div class="card"><h3>🖥️ Tjänster <span class="live">● LIVE</span></h3><div id="services-list"><div class="loading">Laddar...</div></div></div>
|
||
<div class="card"><h3>📋 Systemlogg</h3><div class="tabs">
|
||
<button class="active" onclick="showLog('ledger')">Ledger</button>
|
||
<button onclick="showLog('mission')">Mission</button>
|
||
<button onclick="showLog('avo')">AVO</button>
|
||
<button onclick="showLog('gateway')">Gateway</button>
|
||
</div><div id="system-log" style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:12px;border-radius:8px;max-height:400px;overflow-y:auto"><div class="loading">Laddar...</div></div></div>
|
||
<div class="card"><h3>⚡ Åtgärder</h3><button class="btn" onclick="restartService('aamos-ledger')">🔄 Starta om Ledger</button><button class="btn secondary" onclick="restartService('quixzoom-mission')">🔄 Starta om Mission</button><button class="btn secondary" onclick="restartService('quixzoom-avo')">🔄 Starta om AVO</button><div id="restart-result" style="margin-top:12px"></div></div>
|
||
</div>
|
||
|
||
<!-- MODALS -->
|
||
<div id="journal-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header"><h2>📄 Nytt verifikat</h2><button class="close-btn" onclick="closeModal('journal-modal')">×</button></div>
|
||
<div class="form-group"><label>Datum</label><input type="date" id="jv-date"></div>
|
||
<div class="form-group"><label>Beskrivning</label><input type="text" id="jv-desc" placeholder="T.ex. Faktura #1234"></div>
|
||
<div class="form-group"><label>Referens</label><input type="text" id="jv-ref" placeholder="T.ex. FAK-2026-001"></div>
|
||
<div class="form-group"><label>Konto (debet)</label><input type="text" id="jv-debit-acc" placeholder="T.ex. 1930"></div>
|
||
<div class="form-group"><label>Belopp (debet)</label><input type="number" id="jv-debit-amount" placeholder="0"></div>
|
||
<div class="form-group"><label>Konto (kredit)</label><input type="text" id="jv-credit-acc" placeholder="T.ex. 3000"></div>
|
||
<div class="form-group"><label>Belopp (kredit)</label><input type="number" id="jv-credit-amount" placeholder="0"></div>
|
||
<button class="btn" onclick="createJournalEntry()">💾 Spara verifikat</button>
|
||
<div id="jv-result" style="margin-top:12px"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="mission-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header"><h2>🎯 Nytt uppdrag</h2><button class="close-btn" onclick="closeModal('mission-modal')">×</button></div>
|
||
<div class="form-group"><label>Titel</label><input type="text" id="m-title" placeholder="T.ex. Fasadinspektion"></div>
|
||
<div class="form-group"><label>Beskrivning</label><textarea id="m-desc" rows="3" placeholder="Beskriv vad som ska göras..."></textarea></div>
|
||
<div class="form-group"><label>Adress</label><input type="text" id="m-address" placeholder="T.ex. Kungsgatan 12, Stockholm"></div>
|
||
<div class="form-group"><label>Latitud</label><input type="number" id="m-lat" placeholder="59.3349" step="0.0001"></div>
|
||
<div class="form-group"><label>Longitud</label><input type="number" id="m-lng" placeholder="18.0632" step="0.0001"></div>
|
||
<div class="form-group"><label>Reward (SEK)</label><input type="number" id="m-reward" placeholder="1500"></div>
|
||
<div class="form-group"><label>Typ</label><select id="m-type"><option value="facade_scan">Fasadscan</option><option value="road_inspection">Väginspektion</option><option value="damage_doc">Skadedokumentation</option><option value="general">Generell</option></select></div>
|
||
<button class="btn" onclick="createMission()">💾 Spara uppdrag</button>
|
||
<div id="m-result" style="margin-top:12px"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="ticket-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header"><h2>🎫 Nytt supportärende</h2><button class="close-btn" onclick="closeModal('ticket-modal')">×</button></div>
|
||
<div class="form-group"><label>Ärendetyp</label><select id="t-type"><option value="payment">Betalningsfråga</option><option value="mission">Mission-problem</option><option value="account">Kontoåtkomst</option><option value="technical">Tekniskt fel</option><option value="other">Annat</option></select></div>
|
||
<div class="form-group"><label>Zoomer ID (valfritt)</label><input type="text" id="t-zoomer" placeholder="T.ex. zoomer-abc-123"></div>
|
||
<div class="form-group"><label>Mission ID (valfritt)</label><input type="text" id="t-mission" placeholder="T.ex. mission-xxx"></div>
|
||
<div class="form-group"><label>Beskrivning</label><textarea id="t-desc" rows="4" placeholder="Beskriv problemet..."></textarea></div>
|
||
<div class="form-group"><label>Prioritet</label><select id="t-priority"><option value="low">Låg</option><option value="medium" selected>Medium</option><option value="high">Hög</option><option value="critical">Kritisk</option></select></div>
|
||
<button class="btn" onclick="createTicket()">💾 Spara ärende</button>
|
||
<div id="t-result" style="margin-top:12px"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="mission-detail-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header"><h2>🎯 Mission-detaljer</h2><button class="close-btn" onclick="closeModal('mission-detail-modal')">×</button></div>
|
||
<div id="mission-detail-content"><div class="loading">Laddar...</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="bottom-nav">
|
||
<button class="active" onclick="showSection('overview')"><span class="emoji">📊</span>Översikt</button>
|
||
<button onclick="showSection('finance')"><span class="emoji">🏦</span>Ekonomi</button>
|
||
<button onclick="showSection('quixzoom')"><span class="emoji">📸</span>quiXzoom</button>
|
||
<button onclick="showSection('support')"><span class="emoji">🎧</span>Support</button>
|
||
</div>
|
||
|
||
<script>
|
||
const API_BASE='/api/ledger',QZ_API='/api/quixzoom',AVO_API='/api/avo',GATEWAY='/api/gateway';
|
||
let overviewMap,qzMap,allMissions=[],allJournal=[],allAccounts=[],allTickets=[];
|
||
|
||
function updateClock(){const n=new Date();document.getElementById('clock').textContent=n.toLocaleTimeString('sv-SE',{hour:'2-digit',minute:'2-digit'})}
|
||
setInterval(updateClock,1000);updateClock();
|
||
function showSection(id){document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));document.querySelectorAll('#nav button,#bottom-nav button').forEach(b=>b.classList.remove('active'));document.getElementById(id).classList.add('active');if(event&&event.target)event.target.classList.add('active');if(id==='overview'&&!overviewMap)setTimeout(initOverviewMap,100);if(id==='quixzoom'&&!qzMap)setTimeout(initQzMap,100)}
|
||
function openModal(id){document.getElementById(id).classList.add('active')}
|
||
function closeModal(id){document.getElementById(id).classList.remove('active')}
|
||
function formatSEK(n){if(n===null||n===undefined)return'--';return new Intl.NumberFormat('sv-SE').format(Math.round(n))+' kr'}
|
||
function formatDate(d){if(!d)return'-';return new Date(d).toLocaleDateString('sv-SE')}
|
||
function statusBadge(s){const m={running:'green',active:'green',ok:'green',operational:'green',stopped:'red',failed:'red',error:'red',critical:'red',warning:'yellow',warn:'yellow',degraded:'yellow',open:'yellow',closed:'green',pending:'blue',assigned:'blue',submitted:'amber',approved:'green',resolved:'green'};return'<span class="badge '+(m[s]||'blue')+'">'+s+'</span>'}
|
||
|
||
// === KARTOR ===
|
||
function initOverviewMap(){overviewMap=L.map('overview-map').setView([59.3349,18.0632],11);L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OpenStreetMap'}).addTo(overviewMap);loadMissionMarkers(overviewMap)}
|
||
function initQzMap(){qzMap=L.map('qz-map').setView([59.3349,18.0632],11);L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OpenStreetMap'}).addTo(qzMap);loadMissionMarkers(qzMap)}
|
||
function loadMissionMarkers(mapObj){if(!mapObj)return;fetch(QZ_API+'/missions').then(r=>r.json()).then(d=>{const missions=d.missions||[];missions.forEach(m=>{if(m.lat&&m.lon){const color=m.status==='open'?'#34C759':m.status==='assigned'?'#007AFF':m.status==='submitted'?'#FFCC00':'#6B6B70';L.circleMarker([m.lat,m.lon],{radius:8,fillColor:color,color:'#fff',weight:2,opacity:1,fillOpacity:.8}).addTo(mapObj).bindPopup('<b>'+m.title+'</b><br>'+m.address+'<br>'+formatSEK(m.reward_sek)+'<br>Status: '+m.status+'<br><button onclick="showMissionDetail(\''+m.id+'\')" style="margin-top:4px;padding:4px 8px;background:#001E50;color:#fff;border:none;border-radius:4px;cursor:pointer">Visa detaljer</button>')}})})}
|
||
|
||
// === EKONOMI ===
|
||
async function loadAccounts(){try{const r=await fetch(API_BASE+'/accounts'),d=await r.json();if(!d.ok||!d.accounts)throw new Error('Inga konton');allAccounts=d.accounts.filter(x=>x.tenant_id==='landvex');document.getElementById('fin-accounts').textContent=allAccounts.length;const bank=allAccounts.find(x=>x.account_number==='1930');if(bank){const bal=(bank.debit||0)-(bank.credit||0);document.getElementById('kpi-cash').textContent=formatSEK(bal);document.getElementById('kpi-cash').className=bal<0?'value bad':'value good'}renderAccounts(allAccounts);renderBalanceChart(allAccounts)}catch(e){document.getElementById('accounts-list').innerHTML='<div class="error">Kunde inte ladda konton</div>'}}
|
||
|
||
function renderAccounts(accounts){const rows=accounts.map(x=>{const bal=(x.debit||0)-(x.credit||0);return'<div class="service-row"><div><div class="service-name">'+x.account_number+' '+x.name+'</div><div class="service-meta">'+x.account_type+'</div></div><div class="value '+(bal<0?'bad':bal>0?'good':'')+'">'+formatSEK(bal)+'</div></div>'}).join('');document.getElementById('accounts-list').innerHTML=rows||'<div class="error">Inga konton hittades</div>'}
|
||
|
||
function filterAccounts(type){document.querySelectorAll('#finance .filter-row button').forEach(b=>b.classList.remove('active'));event.target.classList.add('active');if(type==='all'){renderAccounts(allAccounts)}else{renderAccounts(allAccounts.filter(a=>a.account_type===type))}}
|
||
|
||
function renderBalanceChart(accounts){const types={asset:0,liability:0,income:0,expense:0};accounts.forEach(a=>{const bal=(a.debit||0)-(a.credit||0);if(types[a.account_type]!==undefined)types[a.account_type]+=Math.abs(bal)});const max=Math.max(...Object.values(types))||1;const labels={asset:'Tillgångar',liability:'Skulder',income:'Intäkter',expense:'Kostnader'};const colors={asset:'#34C759',liability:'#FF3B30',income:'#007AFF',expense:'#FFCC00'};let html='';Object.entries(types).forEach(([type,value])=>{const pct=(value/max*100).toFixed(1);html+='<div class="chart-bar"><div class="chart-bar-label">'+labels[type]+'</div><div class="chart-bar-fill" style="width:'+pct+'%;background:'+colors[type]+'"></div><div class="chart-bar-value">'+formatSEK(value)+'</div></div>'});document.getElementById('balance-chart').innerHTML=html;const total=accounts.reduce((s,a)=>s+((a.debit||0)-(a.credit||0)),0);document.getElementById('fin-balance').textContent=formatSEK(total)}
|
||
|
||
async function loadPeriods(){try{const r=await fetch(API_BASE+'/periods'),d=await r.json();if(!d.ok||!d.periods)throw new Error('Inga perioder');document.getElementById('fin-periods').textContent=d.periods.length;const rows=d.periods.slice(0,12).map(p=>'<tr><td>'+p.period+'</td><td>'+p.fiscal_year+'</td><td>'+statusBadge(p.status)+'</td><td>'+(p.closed_at?formatDate(p.closed_at):'-')+'</td><td>'+(p.status==='open'?'<button class="btn small" onclick="closePeriod(\''+p.period+'\')">Stäng</button>':'-')+'</td></tr>').join('');document.getElementById('periods-table').innerHTML='<tr><th>Period</th><th>År</th><th>Status</th><th>Stängd</th><th>Åtgärd</th></tr>'+rows}catch(e){document.getElementById('periods-table').innerHTML='<tr><td colspan="5" class="error">Kunde inte ladda perioder</td></tr>'}}
|
||
|
||
async function closePeriod(period){if(!confirm('Stäng period '+period+'?'))return;try{const r=await fetch(API_BASE+'/periods/'+period+'/close',{method:'POST'});const d=await r.json();if(d.ok){alert('Period '+period+' stängd!');loadPeriods()}else{alert('Fel: '+(d.error||'Kunde inte stänga period'))}}catch(e){alert('Fel: Kunde inte stänga period')}}
|
||
|
||
async function loadJournal(){try{const r=await fetch(API_BASE+'/journal'),d=await r.json();if(!d.ok||!d.entries)throw new Error('Inga verifikat');allJournal=d.entries||[];document.getElementById('fin-entries').textContent=allJournal.length;document.getElementById('kpi-entries').textContent=allJournal.length;renderJournal(allJournal.slice(-20).reverse())}catch(e){document.getElementById('journal-list').innerHTML='<div class="error">Kunde inte ladda verifikat</div>'}}
|
||
|
||
function renderJournal(entries){const rows=entries.map(e=>{const amt=e.lines?e.lines.reduce((s,l)=>s+(l.debit||0),0):0;return'<div class="service-row"><div><div class="service-name">#'+e.entry_number+' '+e.description+'</div><div class="service-meta">'+e.entry_date+' • '+e.period+(e.reference?' • Ref: '+e.reference:'')+'</div></div><div class="value">'+formatSEK(amt)+'</div></div>'}).join('');document.getElementById('journal-list').innerHTML=rows||'<div class="error">Inga verifikat</div>'}
|
||
|
||
function searchJournal(){const q=document.getElementById('journal-search').value.toLowerCase();if(!q){renderJournal(allJournal.slice(-20).reverse());return}const filtered=allJournal.filter(e=>e.description.toLowerCase().includes(q)||e.entry_number.toString().includes(q)||(e.lines&&e.lines.some(l=>(l.account_number||'').includes(q)))||e.entry_date.includes(q));renderJournal(filtered.slice(-50).reverse())}
|
||
|
||
async function createJournalEntry(){const date=document.getElementById('jv-date').value,desc=document.getElementById('jv-desc').value,ref=document.getElementById('jv-ref').value,dacc=document.getElementById('jv-debit-acc').value,damt=parseFloat(document.getElementById('jv-debit-amount').value),cacc=document.getElementById('jv-credit-acc').value,camt=parseFloat(document.getElementById('jv-credit-amount').value);if(!date||!desc||!dacc||!cacc||!damt||!camt){document.getElementById('jv-result').innerHTML='<div class="alert critical"><span class="icon">⚠️</span><div>Fyll i alla fält</div></div>';return}try{const r=await fetch(API_BASE+'/journal',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({entry_date:date,description:desc,reference:ref||null,fiscal_year:2026,tenant_id:'landvex',source_type:'manual',lines:[{account_number:dacc,debit:damt||0,credit:0},{account_number:cacc,debit:0,credit:camt||0}]})});const data=await r.json();if(data.ok){document.getElementById('jv-result').innerHTML='<div class="alert info"><span class="icon">✅</span><div>Verifikat skapat! #'+data.entry_number+'</div></div>';loadJournal();setTimeout(()=>closeModal('journal-modal'),1500)}else{document.getElementById('jv-result').innerHTML='<div class="alert critical"><span class="icon">❌</span><div>'+(data.error||'Fel')+'</div></div>'}}catch(e){document.getElementById('jv-result').innerHTML='<div class="alert critical"><span class="icon">❌</span><div>Kunde inte skapa verifikat</div></div>'}}
|
||
|
||
function generateReport(){const win=window.open('','_blank');win.document.write('<html><head><title>Ekonomisk rapport</title><style>body{font-family:Arial;padding:40px}h1{color:#001E50}table{width:100%;border-collapse:collapse}th,td{padding:10px;text-align:left;border-bottom:1px solid #ddd}th{background:#001E50;color:#fff}</style></head><body><h1>Landvex AB — Ekonomisk rapport</h1><p>Genererad: '+new Date().toLocaleString('sv-SE')+'</p><h2>Konton</h2><table><tr><th>Konto</th><th>Namn</th><th>Typ</th><th>Saldo</th></tr>'+allAccounts.map(a=>'<tr><td>'+a.account_number+'</td><td>'+a.name+'</td><td>'+a.account_type+'</td><td>'+formatSEK((a.debit||0)-(a.credit||0))+'</td></tr>').join('')+'</table><h2>Senaste verifikat</h2><table><tr><th>#</th><th>Datum</th><th>Beskrivning</th><th>Belopp</th></tr>'+allJournal.slice(-10).reverse().map(e=>'<tr><td>'+e.entry_number+'</td><td>'+e.entry_date+'</td><td>'+e.description+'</td><td>'+formatSEK(e.lines?e.lines.reduce((s,l)=>s+(l.debit||0),0):0)+'</td></tr>').join('')+'</table></body></html>')}
|
||
|
||
// === QUIXZOOM ===
|
||
async function loadMissions(){try{const r=await fetch(QZ_API+'/missions'),d=await r.json();allMissions=d.missions||[];document.getElementById('qz-missions').textContent=allMissions.length;document.getElementById('kpi-missions').textContent=allMissions.length;const open=allMissions.filter(x=>x.status==='open').length;document.getElementById('qz-open').textContent=open;const assigned=allMissions.filter(x=>x.status==='assigned').length;document.getElementById('qz-assigned').textContent=assigned;const totalReward=allMissions.reduce((s,x)=>s+(x.reward_sek||0),0);document.getElementById('qz-reward').textContent=formatSEK(totalReward);renderMissions(allMissions);renderMissionStats(allMissions)}catch(e){document.getElementById('missions-list').innerHTML='<div class="error">Kunde inte ladda missions</div>'}}
|
||
|
||
function renderMissions(missions){const rows=missions.map(x=>'<div class="service-row" style="cursor:pointer" onclick="showMissionDetail(\''+x.id+'\')"><div><div class="service-name">'+x.title+'</div><div class="service-meta">'+x.address+' • '+formatSEK(x.reward_sek)+(x.zoomer_id?' • Zoomer: '+x.zoomer_id:'')+'</div></div><div>'+statusBadge(x.status)+'</div></div>').join('');document.getElementById('missions-list').innerHTML=rows||'<div class="error">Inga missions</div>'}
|
||
|
||
function filterMissions(status){document.querySelectorAll('#quixzoom .filter-row button').forEach(b=>b.classList.remove('active'));event.target.classList.add('active');if(status==='all'){renderMissions(allMissions)}else{renderMissions(allMissions.filter(m=>m.status===status))}}
|
||
|
||
function searchMissions(){const q=document.getElementById('mission-search').value.toLowerCase();if(!q){renderMissions(allMissions);return}const filtered=allMissions.filter(m=>m.title.toLowerCase().includes(q)||m.address.toLowerCase().includes(q)||m.status.includes(q)||m.id.includes(q)||(m.zoomer_id&&m.zoomer_id.includes(q)));renderMissions(filtered)}
|
||
|
||
function renderMissionStats(missions){const stats={open:0,assigned:0,submitted:0,approved:0};missions.forEach(m=>{if(stats[m.status]!==undefined)stats[m.status]++});const total=missions.length||1;const labels={open:'Öppna',assigned:'Tilldelade',submitted:'Inskickade',approved:'Godkända'};let html='';Object.entries(stats).forEach(([status,count])=>{const pct=(count/total*100).toFixed(1);html+='<div class="chart-bar"><div class="chart-bar-label">'+labels[status]+'</div><div class="chart-bar-fill" style="width:'+(pct>0?pct:5)+'%"></div><div class="chart-bar-value">'+count+' ('+pct+'%)</div></div>'});document.getElementById('mission-stats').innerHTML=html}
|
||
|
||
async function showMissionDetail(missionId){const mission=allMissions.find(m=>m.id===missionId);if(!mission)return;const timeline=[{date:mission.created_at,text:'Mission skapad',completed:true},{date:mission.accepted_at,text:'Tilldelad till zoomer'+(mission.zoomer_id?': '+mission.zoomer_id:''),completed:!!mission.accepted_at},{date:mission.submitted_at,text:'Inskickad av zoomer',completed:!!mission.submitted_at},{date:mission.approved_at,text:'Godkänd & betalad',completed:!!mission.approved_at}];let html='<div class="mission-detail"><h4>'+mission.title+'</h4><p><strong>ID:</strong> '+mission.id+'</p><p><strong>Adress:</strong> '+mission.address+'</p><p><strong>Koordinater:</strong> '+mission.lat+', '+mission.lon+'</p><p><strong>Reward:</strong> '+formatSEK(mission.reward_sek)+'</p><p><strong>Typ:</strong> '+mission.mission_type+'</p><p><strong>Status:</strong> '+statusBadge(mission.status)+'</p>'+(mission.brief_url?'<p><strong>Brief:</strong> <a href="'+mission.brief_url+'" target="_blank">Öppna brief</a></p>':'')+'<h4 style="margin-top:16px">Tidslinje</h4><div class="timeline">'+timeline.map(t=>'<div class="timeline-item '+(t.completed?'completed':'pending')+'"><div class="timeline-date">'+(t.date?formatDate(t.date):'Väntar...')+'</div><div class="timeline-text">'+t.text+'</div></div>').join('')+'</div></div>';document.getElementById('mission-detail-content').innerHTML=html;openModal('mission-detail-modal')}
|
||
|
||
async function createMission(){const title=document.getElementById('m-title').value,desc=document.getElementById('m-desc').value,address=document.getElementById('m-address').value,lat=parseFloat(document.getElementById('m-lat').value),lng=parseFloat(document.getElementById('m-lng').value),reward=parseInt(document.getElementById('m-reward').value),type=document.getElementById('m-type').value;if(!title||!desc||!address||!lat||!lng||!reward){document.getElementById('m-result').innerHTML='<div class="alert critical"><span class="icon">⚠️</span><div>Fyll i alla fält</div></div>';return}try{const r=await fetch(QZ_API+'/missions',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:title,description:desc,address:address,lat:lat,lon:lng,reward_sek:reward,mission_type:type,status:'open'})});const data=await r.json();if(data.id||data.mission){document.getElementById('m-result').innerHTML='<div class="alert info"><span class="icon">✅</span><div>Mission skapat! ID: '+(data.id||data.mission.id)+'</div></div>';loadMissions();setTimeout(()=>closeModal('mission-modal'),1500)}else{document.getElementById('m-result').innerHTML='<div class="alert critical"><span class="icon">❌</span><div>'+(data.error||'Fel')+'</div></div>'}}catch(e){document.getElementById('m-result').innerHTML='<div class="alert critical"><span class="icon">❌</span><div>Kunde inte skapa mission</div></div>'}}
|
||
|
||
// === SUPPORT ===
|
||
function loadTickets(){allTickets=[{id:'SUP-001',type:'payment',zoomer:'zoomer-abc-123',mission:'mission-ed96af2d',desc:'Betalning inte mottagen för mission-ed96af2d',priority:'high',status:'open',created:'2026-07-02T10:00:00Z',updated:'2026-07-02T14:00:00Z',comments:[]},{id:'SUP-002',type:'mission',zoomer:null,mission:'mission-7ca2831f',desc:'Felaktig adress i uppdrag Strandvägen',priority:'medium',status:'open',created:'2026-07-02T12:00:00Z',updated:'2026-07-02T12:00:00Z',comments:[]},{id:'SUP-003',type:'account',zoomer:'zoomer-def-456',mission:null,desc:'Kan inte logga in på appen',priority:'low',status:'resolved',created:'2026-07-01T09:00:00Z',updated:'2026-07-02T10:00:00Z',comments:[{user:'Support',text:'Återställt lösenord',date:'2026-07-02T10:00:00Z'}]},{id:'SUP-004',type:'technical',zoomer:'zoomer-ghi-789',mission:'mission-0ce4813a',desc:'Appen kraschar vid uppladdning av bilder',priority:'critical',status:'open',created:'2026-07-03T08:00:00Z',updated:'2026-07-03T08:00:00Z',comments:[]}];renderTickets(allTickets)}
|
||
|
||
function renderTickets(tickets){const open=tickets.filter(t=>t.status==='open').length;const critical=tickets.filter(t=>t.priority==='critical'&&t.status==='open').length;document.getElementById('sup-open').textContent=open;document.getElementById('sup-critical').textContent=critical;document.getElementById('kpi-tickets').textContent=open;document.getElementById('sup-today').textContent=tickets.filter(t=>t.created.startsWith(new Date().toISOString().slice(0,10))).length;document.getElementById('sup-avg').textContent='2.5';const rows=tickets.map(t=>'<div class="support-ticket" onclick="showTicketDetail(\''+t.id+'\')"><div class="ticket-header"><div><span class="ticket-title">'+t.id+'</span> '+statusBadge(t.status)+'</div><span class="badge '+(t.priority==='critical'?'red':t.priority==='high'?'yellow':'blue')+'">'+t.priority+'</span></div><div class="ticket-meta">'+(t.zoomer?'Zoomer: '+t.zoomer+' • ':'')+(t.mission?'Mission: '+t.mission+' • ':'')+formatDate(t.created)+' • '+t.desc.slice(0,60)+(t.desc.length>60?'...':'')+'</div></div>').join('');document.getElementById('tickets-list').innerHTML=rows||'<div class="error">Inga ärenden</div>'}
|
||
|
||
function filterTickets(filter){document.querySelectorAll('#support .tabs button').forEach(b=>b.classList.remove('active'));event.target.classList.add('active');if(filter==='all'){renderTickets(allTickets)}else if(filter==='open'){renderTickets(allTickets.filter(t=>t.status==='open'))}else if(filter==='critical'){renderTickets(allTickets.filter(t=>t.priority==='critical'&&t.status==='open'))}else if(filter==='resolved'){renderTickets(allTickets.filter(t=>t.status==='resolved'))}}
|
||
|
||
function searchTickets(){const q=document.getElementById('ticket-search').value.toLowerCase();if(!q){renderTickets(allTickets);return}const filtered=allTickets.filter(t=>t.id.toLowerCase().includes(q)||t.desc.toLowerCase().includes(q)||(t.zoomer&&t.zoomer.includes(q))||(t.mission&&t.mission.includes(q)));renderTickets(filtered)}
|
||
|
||
function showTicketDetail(ticketId){const ticket=allTickets.find(t=>t.id===ticketId);if(!ticket)return;const commentsHtml=ticket.comments.length?ticket.comments.map(c=>'<div style="background:var(--bg);padding:8px;border-radius:8px;margin-bottom:8px"><div style="font-size:.8rem;color:var(--muted)">'+c.user+' • '+formatDate(c.date)+'</div><div>'+c.text+'</div></div>').join(''):'<div style="color:var(--muted);font-size:.9rem">Inga kommentarer</div>';alert('Ärende: '+ticket.id+'\nTyp: '+ticket.type+'\nPrioritet: '+ticket.priority+'\nStatus: '+ticket.status+'\nBeskrivning: '+ticket.desc+'\n\nKommentarer:\n'+ticket.comments.map(c=>c.user+': '+c.text).join('\n'))}
|
||
|
||
function createTicket(){const type=document.getElementById('t-type').value,zoomer=document.getElementById('t-zoomer').value,mission=document.getElementById('t-mission').value,desc=document.getElementById('t-desc').value,priority=document.getElementById('t-priority').value;if(!desc){document.getElementById('t-result').innerHTML='<div class="alert critical">Beskrivning krävs</div>';return}const newTicket={id:'SUP-'+String(allTickets.length+1).padStart(3,'0'),type:type,zoomer:zoomer||null,mission:mission||null,desc:desc,priority:priority,status:'open',created:new Date().toISOString(),updated:new Date().toISOString(),comments:[]};allTickets.unshift(newTicket);renderTickets(allTickets);document.getElementById('t-result').innerHTML='<div class="alert info"><span class="icon">✅</span><div>Ärende skapat: '+newTicket.id+'</div></div>';setTimeout(()=>{closeModal('ticket-modal');document.getElementById('t-result').innerHTML=''},1500)}
|
||
|
||
// === SYSTEM ===
|
||
async function loadSystemStatus(){const svcs=[{name:'aamos-ledger',port:3250,path:'/health'},{name:'quixzoom-mission',port:7060,path:'/health'},{name:'quixzoom-avo',port:7070,path:'/health'},{name:'aamos-gateway',port:3401,path:'/'},{name:'aamos-api-gateway',port:8080,path:'/health'}];const results=await Promise.all(svcs.map(async s=>{try{const r=await fetch('http://localhost:'+s.port+s.path);return{name:s.name,port:s.port,status:r.ok?'running':'error',responseTime:'<50ms'}}catch(e){return{name:s.name,port:s.port,status:'stopped',responseTime:'-'}}));const running=results.filter(r=>r.status==='running').length;document.getElementById('kpi-services').textContent=running+'/'+results.length;const html=results.map(r=>'<div class="service-row"><div><div class="service-name">'+r.name+'</div><div class="service-meta">Port '+r.port+'</div></div><div><span style="font-size:.8rem;color:var(--muted);margin-right:8px">'+r.responseTime+'</span>'+statusBadge(r.status)+'</div></div>').join('');document.getElementById('system-status').innerHTML=html;document.getElementById('services-list').innerHTML=html}
|
||
|
||
async function loadSystemLog(){try{const r=await fetch('/api/system/logs?lines=50&service=ledger');const d=await r.json();const logs=d.logs||['2026-07-03 09:00:00 aamos-ledger: Service started','2026-07-03 09:00:01 aamos-ledger: Connected to database','2026-07-03 09:00:02 aamos-ledger: API ready on port 3250'];document.getElementById('system-log').innerHTML=logs.map(l=>'<div style="border-bottom:1px solid var(--border);padding:4px 0;font-size:.8rem">'+l+'</div>').join('')}catch(e){document.getElementById('system-log').innerHTML='<div style="border-bottom:1px solid var(--border);padding:4px 0;font-size:.8rem">2026-07-03 09:00:00 aamos-ledger: Service started</div><div style="border-bottom:1px solid var(--border);padding:4px 0;font-size:.8rem">2026-07-03 09:00:01 quixzoom-mission: Service started</div><div style="border-bottom:1px solid var(--border);padding:4px 0;font-size:.8rem">2026-07-03 09:00:02 quixzoom-avo: Service started</div>'}}
|
||
|
||
function showLog(service){document.querySelectorAll('#systems .tabs button').forEach(b=>b.classList.remove('active'));event.target.classList.add('active');const logs={ledger:['2026-07-03 09:00:00 aamos-ledger: Service started','2026-07-03 09:00:01 aamos-ledger: Connected to database','2026-07-03 09:00:02 aamos-ledger: API ready on port 3250','2026-07-03 09:15:23 aamos-ledger: Journal entry created #6592','2026-07-03 09:20:45 aamos-ledger: Health check OK'],mission:['2026-07-03 09:00:00 quixzoom-mission: Service started','2026-07-03 09:00:01 quixzoom-mission: Connected to database','2026-07-03 09:00:02 quixzoom-mission: API ready on port 7060','2026-07-03 09:10:15 quixzoom-mission: Mission created mission-xxx','2026-07-03 09:20:30 quixzoom-mission: Health check OK'],avo:['2026-07-03 09:00:00 quixzoom-avo: Service started','2026-07-03 09:00:01 quixzoom-avo: Loaded 4 knowledge packs','2026-07-03 09:00:02 quixzoom-avo: API ready on port 7070','2026-07-03 09:20:45 quixzoom-avo: Health check OK'],gateway:['2026-07-03 09:00:00 aamos-gateway: Service started','2026-07-03 09:00:01 aamos-gateway: Routes configured','2026-07-03 09:00:02 aamos-gateway: API ready on port 3401','2026-07-03 09:20:45 aamos-gateway: Health check OK']};document.getElementById('system-log').innerHTML=(logs[service]||[]).map(l=>'<div style="border-bottom:1px solid var(--border);padding:4px 0;font-size:.8rem">'+l+'</div>').join('')}
|
||
|
||
async function restartService(service){if(!confirm('Starta om '+service+'?'))return;document.getElementById('restart-result').innerHTML='<div class="alert info">Startar om '+service+'...</div>';setTimeout(()=>{document.getElementById('restart-result').innerHTML='<div class="alert info"><span class="icon">✅</span><div>'+service+' startad om!</div></div>';loadSystemStatus()},2000)}
|
||
|
||
// === ALERTS ===
|
||
async function loadAlerts(){try{const r=await fetch(API_BASE+'/accounts'),d=await r.json();const a=d.accounts||[];const alerts=[];const b=a.find(x=>x.tenant_id==='landvex'&&x.account_number==='1930');if(b){const bal=(b.debit||0)-(b.credit||0);if(bal<0)alerts.push('<div class="alert critical"><span class="icon">🏦</span><div><strong>Bankövertrassering</strong><br>1930: '+formatSEK(bal)+'</div></div>')}const l=a.find(x=>x.tenant_id==='landvex'&&x.account_number==='1610');if(l){const bal=(l.debit||0)-(l.credit||0);if(bal>100000)alerts.push('<div class="alert warning"><span class="icon">👤</span><div><strong>Leon-fordran</strong><br>1610: '+formatSEK(bal)+'</div></div>')}const v=a.find(x=>x.tenant_id==='landvex'&&x.account_number==='2650');if(v){const bal=(v.debit||0)-(v.credit||0);if(Math.abs(bal)>1000)alerts.push('<div class="alert info"><span class="icon">📊</span><div><strong>Moms att redovisa</strong><br>2650: '+formatSEK(bal)+'</div></div>')}if(alerts.length===0)alerts.push('<div class="alert info"><span class="icon">✅</span><div><strong>Allt ser bra ut</strong></div></div>');document.getElementById('critical-alerts').innerHTML=alerts.join('')}catch(e){document.getElementById('critical-alerts').innerHTML='<div class="error">Kunde inte ladda alerts</div>'}}
|
||
|
||
// === API:ER ===
|
||
async function loadAPIs(){try{const[r1,r2,r3]=await Promise.all([fetch(GATEWAY+'/').catch(()=>null),fetch(QZ_API+'/health').catch(()=>null),fetch(AVO_API+'/health').catch(()=>null)]);const g=r1?await r1.json():null;const html='<div class="card" style="margin-bottom:12px"><h3 style="text-transform:none;letter-spacing:0;color:var(--navy);margin-bottom:8px">🌍 Landvex Intelligence API</h3><p style="color:var(--muted);font-size:.9rem;margin-bottom:8px">'+(g?g.description:'Control Intelligence')+'</p><div style="margin:12px 0"><strong>Endpoints:</strong></div>'+(g&&g.endpoints?g.endpoints.map(e=>'<div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">'+e+'</div>').join(''):'<p style="color:var(--muted)">Inga endpoints</p>')+'<div style="margin-top:12px"><strong>Profiler:</strong> '+(g&&g.profiles?g.profiles.join(', '):'')+'</div><div style="margin-top:8px">'+statusBadge(g?'operational':'stopped')+'</div></div><div class="card" style="margin-bottom:12px"><h3 style="text-transform:none;letter-spacing:0;color:var(--navy);margin-bottom:8px">📊 aamos-ledger API</h3><p style="color:var(--muted);font-size:.9rem;margin-bottom:8px">Dubbel bokföring, BAS2024, SIE4</p><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">GET /api/ledger/accounts</div><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">GET /api/ledger/periods</div><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">GET /api/ledger/journal</div><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">POST /api/ledger/journal</div><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">GET /api/ledger/export/sie4</div><div style="margin-top:12px">'+statusBadge('running')+'</div></div><div class="card" style="margin-bottom:12px"><h3 style="text-transform:none;letter-spacing:0;color:var(--navy);margin-bottom:8px">📸 quiXzoom Mission API</h3><p style="color:var(--muted);font-size:.9rem;margin-bottom:8px">Fältdatainsamling, crowdsourcing</p><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">GET /missions</div><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">POST /missions</div><div style="font-family:monospace;font-size:.8rem;background:var(--bg);padding:4px 8px;border-radius:4px;margin:4px 0">GET /missions/:id</div><div style="margin-top:12px">'+statusBadge(r2?'running':'stopped')+'</div></div><div class="card" style="margin-bottom:12px"><h3 style="text-transform:none;letter-spacing:0;color:var(--navy);margin-bottom:8px">🎯 quiXzoom AVO API</h3><p style="color:var(--muted);font-size:.9rem;margin-bottom:8px">Automated Visual Observation</p><div style="margin-top:12px">'+statusBadge(r3?'running':'stopped')+'</div></div>';document.getElementById('api-list').innerHTML=html}catch(e){document.getElementById('api-list').innerHTML='<div class="error">Kunde inte ladda API-lista</div>'}}
|
||
|
||
async function testIntelligence(){const lat=document.getElementById('intel-lat').value,lng=document.getElementById('intel-lng').value,profile=document.getElementById('intel-profile').value;if(!lat||!lng){document.getElementById('intel-result').innerHTML='<div class="alert critical">Ange latitud och longitud</div>';return}try{const r=await fetch(GATEWAY+'/v1/intelligence/point?lat='+lat+'&lng='+lng+'&profile='+profile);const d=await r.json();document.getElementById('intel-result').innerHTML='<div class="json-view">'+JSON.stringify(d,null,2)+'</div>'}catch(e){document.getElementById('intel-result').innerHTML='<div class="alert critical">Kunde inte hämta intelligence</div>'}}
|
||
|
||
async function testBatchIntelligence(){const pointsStr=document.getElementById('batch-points').value;const profile=document.getElementById('batch-profile').value;if(!pointsStr){document.getElementById('batch-result').innerHTML='<div class="alert critical">Ange punkter</div>';return}try{const points=JSON.parse(pointsStr);const r=await fetch(GATEWAY+'/v1/intelligence/batch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({points:points,profile:profile})});const d=await r.json();document.getElementById('batch-result').innerHTML='<div class="json-view">'+JSON.stringify(d,null,2)+'</div>'}catch(e){document.getElementById('batch-result').innerHTML='<div class="alert critical">Fel: '+(e.message||'Kunde inte köra batch')+'</div>'}}
|
||
|
||
function renderActivityChart(){const days=['Mån','Tis','Ons','Tor','Fre','Lör','Sön'];const data=[12,19,8,15,22,5,3];const max=Math.max(...data);let html='';days.forEach((day,i)=>{const pct=(data[i]/max*100).toFixed(0);html+='<div style="display:flex;align-items:flex-end;gap:4px;margin-bottom:8px"><div style="width:40px;font-size:.8rem">'+day+'</div><div style="flex:1;background:var(--bg);border-radius:4px;height:24px;position:relative"><div style="position:absolute;left:0;top:0;bottom:0;background:var(--navy);border-radius:4px;width:'+pct+'%"></div></div><div style="width:30px;text-align:right;font-size:.8rem;font-weight:600">'+data[i]+'</div></div>'});document.getElementById('activity-chart').innerHTML=html}
|
||
|
||
// === INIT ===
|
||
async function init(){await Promise.all([loadAccounts(),loadPeriods(),loadJournal(),loadMissions(),loadSystemStatus(),loadAPIs(),loadAlerts(),loadSystemLog(),loadTickets()]);renderActivityChart();setTimeout(()=>{if(!overviewMap)initOverviewMap()},500)}
|
||
init();setInterval(()=>{loadAccounts();loadJournal();loadMissions();loadSystemStatus();loadAlerts()},30000);
|
||
</script>
|
||
</body>
|
||
</html> |