Files
boc/quixzoom-landing-fixed/revenue.html
T
Bernt aee0f09db8 landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar
- Vision: Identify-modell, FAISS, OCR alla testade
- API: Alla 7 integrationstester passerade
- Upplösare: Entitetsupplösning verifierad
2026-07-05 06:41:32 +00:00

556 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="sv" data-theme="light">
<head>
<link rel="stylesheet" >
<link rel="stylesheet" >
<script defer></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="brand-tier" content="quixzoom">
<meta name="module" content="revenue-dashboard">
<meta name="last-audit" content="2026-05-18T23:50:00Z">
<title>quiXzoom — Revenue Dashboard</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#F2F2F7;--navy:#007AFF;--accent:#FF6B35;--accent2:#FF8C5A;--text:#1C1C1E;--text2:#4A5568;--text3:#8A9BB0;--border:#E8E3DC;--card:#FFFFFF;--surface:#F4F1EC;--green:#16a34a;--yellow:#d97706;--red:#dc2626;--blue:#2563eb;--purple:#7c3aed;--sidebar:260px;--qx:#FF6B35}
html,body{height:100%;background:var(--bg);color:var(--text);font-family:-apple-system,'Inter',system-ui,sans-serif;font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}
.layout{display:flex;height:100vh;overflow:hidden}
.sidebar{width:var(--sidebar);background:#1A1A2E;display:flex;flex-direction:column;flex-shrink:0;overflow-y:auto}
.sidebar-logo{padding:20px 20px 16px;border-bottom:1px solid rgba(255,255,255,.08)}
.sidebar-logo .brand{font-size:10px;font-weight:700;letter-spacing:.14em;color:rgba(255,255,255,.75);text-transform:uppercase;margin-bottom:4px}
.sidebar-logo .name{font-size:21px;font-weight:900;color:#fff;letter-spacing:-.03em}
.sidebar-logo .name span{color:var(--qx)}
.sidebar-logo .meta{font-size:11px;color:rgba(255,255,255,.75);margin-top:3px}
.sidebar-section{padding:16px 12px 6px}
.sidebar-section-label{font-size:10px;font-weight:700;letter-spacing:.1em;color:rgba(255,255,255,.75);text-transform:uppercase;padding:0 8px;margin-bottom:6px}
.sidebar-item{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:8px;cursor:pointer;color:rgba(255,255,255,.6);font-size:13px;font-weight:500;transition:all .15s;border:none;background:none;width:100%;text-align:left}
.sidebar-item:hover{background:rgba(255,255,255,.08);color:#fff}
.sidebar-item.active{background:var(--qx);color:#fff}
.sidebar-item .icon{font-size:15px;width:22px;text-align:center;flex-shrink:0}
.sidebar-footer{margin-top:auto;padding:16px 12px;border-top:1px solid rgba(255,255,255,.08)}
.main{flex:1;overflow-y:auto;display:flex;flex-direction:column;min-width:0}
.topbar{background:var(--card);border-bottom:1px solid var(--border);padding:0 28px;height:56px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;position:sticky;top:0;z-index:10}
.topbar-left{display:flex;align-items:center;gap:12px}
.topbar-title{font-size:16px;font-weight:700}
.topbar-breadcrumb{font-size:12px;color:var(--text3)}
.topbar-actions{display:flex;gap:8px;align-items:center}
.content{padding:24px 28px;flex:1}
.tabs{display:flex;gap:4px;background:var(--surface);border-radius:10px;padding:4px;border:1px solid var(--border);width:fit-content;margin-bottom:20px}
.tab{padding:7px 18px;border-radius:7px;font-size:13px;font-weight:600;cursor:pointer;color:var(--text2);border:none;background:none;transition:all .15s}
.tab.active{background:var(--card);color:var(--text);box-shadow:0 1px 4px rgba(0,0,0,.08)}
.view{display:none}.view.active{display:block}
.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;border:none;transition:all .15s}
.btn-primary{background:var(--qx);color:#fff}.btn-primary:hover{background:var(--accent2)}
.btn-secondary{background:var(--surface);color:var(--text);border:1px solid var(--border)}.btn-secondary:hover{background:var(--border)}
.btn-sm{padding:5px 12px;font-size:12px}
.btn-xs{padding:3px 8px;font-size:11px}
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:14px;margin-bottom:24px}
.stat-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px 20px;position:relative;overflow:hidden}
.stat-card::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:var(--qx);border-radius:3px 3px 0 0}
.stat-card.g::after{background:var(--green)}.stat-card.r::after{background:var(--red)}.stat-card.b::after{background:var(--blue)}.stat-card.p::after{background:var(--purple)}.stat-card.y::after{background:var(--yellow)}
.stat-card .label{font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}
.stat-card .value{font-size:24px;font-weight:800;letter-spacing:-.02em;line-height:1}
.stat-card .sub{font-size:11px;color:var(--text3);margin-top:5px}
.stat-card .change{font-size:11px;font-weight:700;padding:2px 7px;border-radius:20px;display:inline-block;margin-top:5px}
.change-up{background:#dcfce7;color:#166534}.change-flat{background:#f1f5f9;color:#475569}
.badge{display:inline-flex;align-items:center;gap:4px;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:700;white-space:nowrap}
.badge-green{background:#dcfce7;color:#166534}.badge-red{background:#fee2e2;color:#991b1b}.badge-yellow{background:#fef9c3;color:#854d0e}.badge-blue{background:#dbeafe;color:#1e40af}.badge-gray{background:#f1f5f9;color:#475569}.badge-orange{background:#ffedd5;color:#9a3412}.badge-purple{background:#ede9fe;color:#4c1d95}
.table-wrap{background:var(--card);border:1px solid var(--border);border-radius:12px;overflow:hidden;margin-bottom:20px}
.table-header{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border);gap:10px;flex-wrap:wrap}
.table-header h3{font-size:14px;font-weight:700}
.table-header-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
table{width:100%;border-collapse:collapse}
thead th{padding:9px 14px;text-align:left;font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.06em;background:var(--surface);border-bottom:1px solid var(--border);white-space:nowrap}
thead th.right,td.right{text-align:right}
tbody tr{border-bottom:1px solid rgba(232,227,220,.6);transition:background .1s}
tbody tr:last-child{border-bottom:none}
tbody tr:hover{background:#fafaf8}
td{padding:11px 14px;font-size:13px;vertical-align:middle}
td.mono{font-variant-numeric:tabular-nums;font-family:'SF Mono',monospace;font-size:12px}
.tbl-scroll{overflow-x:auto}
.section-title{font-size:13px;font-weight:700;color:var(--text2);margin-bottom:14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
/* CHARTS */
.chart-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:20px}
.chart-card h3{font-size:14px;font-weight:700;margin-bottom:16px}
.bar-chart{display:flex;align-items:flex-end;gap:6px;height:120px;padding-top:8px}
.bar-wrap{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px}
.bar{width:100%;border-radius:4px 4px 0 0;background:var(--qx);opacity:.8;transition:opacity .15s;cursor:pointer;position:relative}
.bar:hover{opacity:1}
.bar-label{font-size:10px;color:var(--text3);white-space:nowrap}
.bar-value{font-size:10px;font-weight:700;color:var(--text2)}
/* ZOOMER CARDS */
.zoomer-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px;margin-bottom:20px}
.zoomer-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px;transition:box-shadow .15s}
.zoomer-card:hover{box-shadow:0 4px 16px rgba(0,0,0,.07)}
.zoomer-card .zc-header{display:flex;align-items:center;gap:12px;margin-bottom:12px}
.zc-avatar{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--qx),var(--purple));display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:15px;flex-shrink:0}
.zc-name{font-size:14px;font-weight:700}
.zc-role{font-size:11px;color:var(--text3)}
.zc-stats{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}
.zc-stat{text-align:center;background:var(--surface);border-radius:8px;padding:8px 4px}
.zc-stat .cs-val{font-size:15px;font-weight:800;color:var(--text)}
.zc-stat .cs-lbl{font-size:9px;font-weight:700;text-transform:uppercase;color:var(--text3);letter-spacing:.06em}
/* GEO */
.geo-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}
.geo-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px}
.geo-card .gc-name{font-size:13px;font-weight:700;margin-bottom:6px}
.geo-bar-wrap{height:6px;background:var(--surface);border-radius:3px;margin-bottom:6px;overflow:hidden}
.geo-bar{height:100%;background:var(--qx);border-radius:3px;transition:width .5s}
.geo-meta{font-size:11px;color:var(--text2)}
/* DAC7 */
.dac7-panel{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:20px}
.dac7-donut{width:120px;height:120px;border-radius:50%;position:relative;margin:0 auto 12px}
.compliance-meter{height:12px;background:var(--surface);border-radius:6px;overflow:hidden;margin-bottom:8px}
.compliance-fill{height:100%;border-radius:6px;background:var(--green);transition:width .5s}
/* PAYOUT */
.payout-card{background:linear-gradient(135deg,#1A1A2E,#16213E);color:#fff;border-radius:14px;padding:24px;margin-bottom:20px}
.payout-card .pc-title{font-size:12px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:rgba(255,255,255,.75);margin-bottom:8px}
.payout-card .pc-amount{font-size:36px;font-weight:900;letter-spacing:-.02em;margin-bottom:4px}
.payout-card .pc-sub{font-size:13px;color:rgba(255,255,255,.6)}
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
.card { padding: 1.5rem !important; }
.grid { grid-template-columns: 1fr !important; }
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
}
</style>
<style id="wcag-contrast-enforcement">
/* AAMOS GLOBAL CONTRAST ENFORCEMENT — WCAG AA (4.5:1 minimum) */
/* Regel: Ingen text under 70% opacity. Aldrig. */
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
/* Fix common low-contrast patterns */
[style*="rgba(60,60,67,0.4)"] { color: rgba(60,60,67,0.65) !important; }
[style*="rgba(60,60,67,0.3)"] { color: rgba(60,60,67,0.65) !important; }
[style*="rgba(255,255,255,0.4)"] { color: rgba(255,255,255,0.72) !important; }
[style*="rgba(255,255,255,0.3)"] { color: rgba(255,255,255,0.72) !important; }
[style*="opacity: 0.4"] { opacity: 0.72 !important; }
[style*="opacity: 0.3"] { opacity: 0.72 !important; }
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
.card { padding: 1.5rem !important; }
.grid { grid-template-columns: 1fr !important; }
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
}
</style>
</head>
<body>
<div class="layout">
<!-- SIDEBAR -->
<nav class="sidebar">
<div class="sidebar-logo">
<div class="brand">AAMOS · quiXzoom</div>
<div class="name">quiX<span>zoom</span></div>
<div class="meta">Revenue Dashboard</div>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">quiXzoom</div>
<button class="sidebar-item" onclick="location.href='launch.html'"><span class="icon">🚀</span>Launch Center</button>
<button class="sidebar-item active"><span class="icon">💰</span>Revenue Dashboard</button>
<button class="sidebar-item" onclick="location.href='admin.html'"><span class="icon">⚙️</span>Admin</button>
<button class="sidebar-item" onclick="location.href='dac7-reporting.html'"><span class="icon">🏛️</span>DAC7 Reporting</button>
<button class="sidebar-item" onclick="location.href='zoomer-earnings.html'"><span class="icon">📊</span>Zoomer Earnings</button>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Utbetalningar</div>
<button class="sidebar-item"><span class="icon"></span>Godkända</button>
<button class="sidebar-item"><span class="icon"></span>Väntande</button>
<button class="sidebar-item"><span class="icon"></span>Misslyckade</button>
</div>
<div class="sidebar-footer">
<button class="sidebar-item" onclick="location.href='../ouroboros/index.html'"><span class="icon"></span>Ouroboros</button>
</div>
</nav>
<!-- MAIN -->
<div class="main">
<div class="topbar">
<div class="topbar-left">
<span class="topbar-breadcrumb">quiXzoom</span>
<span style="color:var(--text3)"></span>
<span class="topbar-title">💰 Revenue Dashboard</span>
</div>
<div class="topbar-actions">
<select class="btn btn-secondary btn-sm" id="periodFilter" onchange="renderAll()" style="border:1px solid var(--border)">
<option value="all">Alla perioder</option>
<option value="may2026">Maj 2026</option>
<option value="apr2026">April 2026</option>
<option value="q2">Q2 2026</option>
</select>
<button class="btn btn-primary btn-sm" onclick="exportCSV()">📥 Exportera CSV</button>
</div>
</div>
<div class="content">
<!-- TOP STATS -->
<div class="stats-grid" id="topStats">
<div class="stat-card">
<div class="label">Totalt intjänat</div>
<div class="value" id="statTotal">0 kr</div>
<div class="sub">Alla zoomers</div>
<span class="change change-up" id="statTotalChange">+0%</span>
</div>
<div class="stat-card g">
<div class="label">Aktiva zoomers</div>
<div class="value" id="statActive">0</div>
<div class="sub">Senaste 30 dagar</div>
</div>
<div class="stat-card b">
<div class="label">Uppdrag (totalt)</div>
<div class="value" id="statAssignments">0</div>
<div class="sub">Genomförda</div>
</div>
<div class="stat-card y">
<div class="label">Snitt per zoomer</div>
<div class="value" id="statAvg">0 kr</div>
<div class="sub">Intjänat/zoomer</div>
</div>
<div class="stat-card p">
<div class="label">Pending utbetalning</div>
<div class="value" id="statPending">0 kr</div>
<div class="sub">Ej utbetalt ännu</div>
</div>
<div class="stat-card r">
<div class="label">DAC7 Compliance</div>
<div class="value" id="statDac7">0%</div>
<div class="sub">Verifierade TIN</div>
</div>
</div>
<!-- TABS -->
<div class="tabs">
<button class="tab active" onclick="switchTab('overview',this)">📊 Översikt</button>
<button class="tab" onclick="switchTab('zoomers',this)">👤 Per zoomer</button>
<button class="tab" onclick="switchTab('payouts',this)">💸 Utbetalningar</button>
<button class="tab" onclick="switchTab('geo',this)">🌍 Geografi</button>
<button class="tab" onclick="switchTab('dac7',this)">🏛️ DAC7</button>
</div>
<!-- OVERVIEW -->
<div class="view active" id="view-overview">
<!-- PAYOUT NEXT -->
<div class="payout-card">
<div class="pc-title">Nästa utbetalningsdatum</div>
<div class="pc-amount" id="pcAmount">132 450 kr</div>
<div class="pc-sub">📅 <span id="pcDate">1 augusti 2026</span><span id="pcCount">23 zoomers</span> väntar på utbetalning</div>
<div style="margin-top:16px;display:flex;gap:8px">
<button class="btn btn-sm" style="background:var(--qx);color:#fff">🚀 Initiera utbetalning</button>
<button class="btn btn-sm" style="background:rgba(255,255,255,.15);color:#fff;border:1px solid rgba(255,255,255,.2)">📋 Granska lista</button>
</div>
</div>
<!-- REVENUE CHART -->
<div class="chart-card">
<h3>📈 Intjänat per månad (2026)</h3>
<div class="bar-chart" id="revenueChart"></div>
</div>
<!-- STRIPE CONNECT SUMMARY -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-bottom:20px">
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<div style="width:32px;height:32px;background:#635BFF;border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px">$</div>
<div>
<div style="font-size:12px;font-weight:700">Stripe Connect</div>
<div style="font-size:11px;color:var(--text3)">Betalningsplattform</div>
</div>
</div>
<div style="font-size:11px;color:var(--text2)">Anslutna konton: <strong id="stripeConnected">0</strong></div>
<div style="font-size:11px;color:var(--text2)">Under granskning: <strong id="stripeReview">0</strong></div>
<div style="margin-top:10px"><span class="badge badge-green">● Live</span></div>
</div>
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px">
<div style="font-size:12px;font-weight:700;margin-bottom:12px">📊 Utbetalningsstatus</div>
<div id="payoutStatusChart"></div>
</div>
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px">
<div style="font-size:12px;font-weight:700;margin-bottom:12px">⭐ Genomsnittlig rating</div>
<div style="font-size:36px;font-weight:900;letter-spacing:-.02em;color:var(--yellow)" id="avgRating">4.7</div>
<div style="font-size:11px;color:var(--text2)">Baserat på <span id="ratingCount">0</span> omdömen</div>
<div style="margin-top:8px;font-size:18px">⭐⭐⭐⭐⭐</div>
</div>
</div>
</div>
<!-- PER ZOOMER -->
<div class="view" id="view-zoomers">
<div class="table-header" style="background:var(--card);border:1px solid var(--border);border-radius:12px 12px 0 0;padding:14px 20px;margin-bottom:0">
<h3>👤 Per zoomer</h3>
<div class="table-header-actions">
<input type="text" class="btn btn-secondary btn-sm" placeholder="🔍 Sök zoomer..." id="zoomerSearch" oninput="filterZoomers()" style="min-width:200px;text-align:left">
<select class="btn btn-secondary btn-sm" id="zoomerStatusFilter" onchange="filterZoomers()" style="border:1px solid var(--border)">
<option value="">Alla</option>
<option value="active">Aktiva</option>
<option value="inactive">Inaktiva</option>
</select>
</div>
</div>
<div class="tbl-scroll" style="background:var(--card);border:1px solid var(--border);border-top:none;border-radius:0 0 12px 12px;margin-bottom:20px">
<table>
<thead>
<tr><th>Zoomer</th><th>Land</th><th class="right">Uppdrag</th><th class="right">Intjänat</th><th class="right">Rating</th><th>Status</th><th>Utbetalning</th><th>Åtgärd</th></tr>
</thead>
<tbody id="zoomerTable"></tbody>
</table>
</div>
</div>
<!-- PAYOUTS -->
<div class="view" id="view-payouts">
<div class="table-wrap">
<div class="table-header">
<h3>💸 Utbetalningshistorik</h3>
<div class="table-header-actions">
<select class="btn btn-secondary btn-sm" id="payoutStatusFilter2" onchange="filterPayouts()" style="border:1px solid var(--border)">
<option value="">Alla statusar</option>
<option value="pending">Pending</option>
<option value="sent">Sent</option>
<option value="failed">Failed</option>
</select>
</div>
</div>
<div class="tbl-scroll">
<table>
<thead><tr><th>Datum</th><th>Zoomer</th><th class="right">Belopp</th><th>Status</th><th>Stripe ID</th><th>Åtgärd</th></tr></thead>
<tbody id="payoutTable"></tbody>
</table>
</div>
</div>
</div>
<!-- GEO -->
<div class="view" id="view-geo">
<div class="section-title">🌍 Geografisk fördelning</div>
<div class="geo-grid" id="geoGrid"></div>
</div>
<!-- DAC7 -->
<div class="view" id="view-dac7">
<div class="dac7-panel">
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:20px">
<div>
<h3 style="font-size:15px;font-weight:700;margin-bottom:4px">🏛️ DAC7 Compliance</h3>
<p style="font-size:12px;color:var(--text2)">EU-direktiv 2021/514 — rapportering av plattformsekonomin</p>
</div>
<button class="btn btn-primary btn-sm">📥 Exportera DAC7-rapport</button>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:20px">
<div style="text-align:center">
<div style="font-size:12px;font-weight:700;color:var(--text2);margin-bottom:8px">Verifierade TIN</div>
<div style="font-size:42px;font-weight:900;color:var(--green)" id="dac7Pct">0%</div>
<div class="compliance-meter"><div class="compliance-fill" id="dac7Bar" style="width:0%"></div></div>
<div style="font-size:11px;color:var(--text3)"><span id="dac7Verified">0</span> av <span id="dac7Total">0</span> zoomers</div>
</div>
<div>
<div style="font-size:12px;font-weight:700;color:var(--text2);margin-bottom:12px">Status per kategori</div>
<div id="dac7StatusList"></div>
</div>
</div>
<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:10px;padding:14px;font-size:12px;color:#854d0e">
<strong>⚠️ DAC7 deadline:</strong> Rapportering för kalenderår 2026 ska lämnas till Skatteverket senast <strong>31 januari 2027</strong>. Zoomers med intäkter &gt; 2 000 EUR eller &gt; 25 transaktioner måste ha verifierat TIN/PNR.
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// ── DATA ─────────────────────────────────────────────────────────────────
const zoomers = [];
const payoutHistory = [];
{ date:'2026-05-15', zoomer:'Pedro Santos', amount:2800, status:'failed', stripeId:'po_3Px9f2026051502' },
{ date:'2026-06-01', zoomer:'Sofia Bergman', amount:13200, status:'pending', stripeId:'—' },
{ date:'2026-06-01', zoomer:'Erik Johansson', amount:8900, status:'pending', stripeId:'—' },
{ date:'2026-06-01', zoomer:'Emma Strand', amount:10400, status:'pending', stripeId:'—' },
{ date:'2026-06-01', zoomer:'Hans Weber', amount:11300, status:'pending', stripeId:'—' },
{ date:'2026-06-01', zoomer:'Jens Andersen', amount:5200, status:'pending', stripeId:'—' },
{ date:'2026-06-01', zoomer:'Piet van Dijk', amount:4200, status:'pending', stripeId:'—' },
{ date:'2026-06-01', zoomer:'Anneliese de Boer', amount:6100, status:'pending', stripeId:'—' },
];
const monthlyRevenue = [
{ month:'Jan', val:28400 }, { month:'Feb', val:34200 }, { month:'Mar', val:41800 },
{ month:'Apr', val:57600 }, { month:'Maj', val:82300 }, { month:'Jun', val:0 },
{ month:'Jul', val:0 }, { month:'Aug', val:0 }, { month:'Sep', val:0 },
{ month:'Okt', val:0 }, { month:'Nov', val:0 }, { month:'Dec', val:0 },
];
const geoData = [
{ country:'Sverige', flag:'🇸🇪', zoomers:5, earned:142000, pct:52 },
{ country:'Norge', flag:'🇳🇴', zoomers:2, earned:52300, pct:19 },
{ country:'Danmark', flag:'🇩🇰', zoomers:2, earned:41100, pct:15 },
{ country:'Tyskland',flag:'🇩🇪', zoomers:3, earned:66600, pct:24 },
{ country:'Nederländerna',flag:'🇳🇱', zoomers:2, earned:31200, pct:11 },
{ country:'Portugal',flag:'🇵🇹', zoomers:1, earned:8500, pct:3 },
];
// ── COMPUTED ──────────────────────────────────────────────────────────────────
const totalEarned = zoomers.reduce((s,z) => s+z.earned, 0);
const totalAssignments = zoomers.reduce((s,z) => s+z.assignments, 0);
const activeCount = zoomers.filter(z => z.status==='active').length;
const avgEarned = Math.round(totalEarned / zoomers.length);
const pendingPayout = payoutHistory.filter(p => p.status==='pending').reduce((s,p) => s+p.amount, 0);
const tinVerified = zoomers.filter(z => z.tin).length;
const dac7Pct = Math.round(tinVerified/zoomers.length*100);
const avgRating = (zoomers.reduce((s,z) => s+z.rating, 0)/zoomers.length).toFixed(1);
const totalRatingCount = zoomers.reduce((s,z) => s+z.assignments, 0);
// ── RENDER ALL ────────────────────────────────────────────────────────────────
function renderAll() {
// Stats
document.getElementById('statTotal').textContent = fmtK(totalEarned)+' kr';
document.getElementById('statTotalChange').textContent = '+34% vs förra månaden';
document.getElementById('statActive').textContent = activeCount;
document.getElementById('statAssignments').textContent = totalAssignments;
document.getElementById('statAvg').textContent = fmtK(avgEarned)+' kr';
document.getElementById('statPending').textContent = fmtK(pendingPayout)+' kr';
document.getElementById('statDac7').textContent = dac7Pct+'%';
document.getElementById('statDac7').style.color = dac7Pct>=80?'var(--green)':dac7Pct>=60?'var(--yellow)':'var(--red)';
document.getElementById('pcAmount').textContent = fmt(pendingPayout)+' kr';
document.getElementById('pcCount').textContent = payoutHistory.filter(p=>p.status==='pending').length+' zoomers';
document.getElementById('stripeConnected').textContent = zoomers.filter(z=>z.tin).length;
document.getElementById('stripeReview').textContent = zoomers.filter(z=>!z.tin).length;
document.getElementById('avgRating').textContent = avgRating;
document.getElementById('ratingCount').textContent = totalRatingCount;
renderChart();
renderPayoutStatusChart();
renderZoomerTable(zoomers);
renderPayoutTable(payoutHistory);
renderGeo();
renderDAC7();
}
function renderChart() {
const maxVal = Math.max(...monthlyRevenue.map(m => m.val), 1);
document.getElementById('revenueChart').innerHTML = monthlyRevenue.map(m => `
<div class="bar-wrap">
<div class="bar-value">${m.val ? fmtK(m.val)+'k' : ''}</div>
<div class="bar" style="height:${m.val ? Math.max(8, m.val/maxVal*100) : 4}px;${!m.val?'background:var(--surface);opacity:1':''}"></div>
<div class="bar-label">${m.month}</div>
</div>`).join('');
}
function renderPayoutStatusChart() {
const sent = payoutHistory.filter(p=>p.status==='sent').length;
const pending = payoutHistory.filter(p=>p.status==='pending').length;
const failed = payoutHistory.filter(p=>p.status==='failed').length;
document.getElementById('payoutStatusChart').innerHTML = `
<div style="display:flex;flex-direction:column;gap:8px">
<div style="display:flex;justify-content:space-between;align-items:center"><span class="badge badge-green">✓ Sent</span><strong>${sent}</strong></div>
<div style="display:flex;justify-content:space-between;align-items:center"><span class="badge badge-yellow">⏳ Pending</span><strong>${pending}</strong></div>
<div style="display:flex;justify-content:space-between;align-items:center"><span class="badge badge-red">✕ Failed</span><strong>${failed}</strong></div>
</div>`;
}
function renderZoomerTable(data) {
document.getElementById('zoomerTable').innerHTML = data.map(z => `
<tr>
<td>
<div style="display:flex;align-items:center;gap:10px">
<div style="width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,var(--qx),var(--purple));display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:12px;flex-shrink:0">${z.name.split(' ').map(n=>n[0]).join('')}</div>
<div>
<div style="font-weight:700;font-size:13px">${z.name}</div>
<div style="font-size:11px;color:var(--text3)">${z.email}</div>
</div>
</div>
</td>
<td>${z.flag} ${z.country}</td>
<td class="right">${z.assignments}</td>
<td class="right mono"><strong>${fmt(z.earned)} kr</strong></td>
<td class="right">⭐ ${z.rating}</td>
<td><span class="badge ${z.status==='active'?'badge-green':'badge-gray'}">${z.status==='active'?'● Aktiv':'○ Inaktiv'}</span></td>
<td><span class="badge ${payoutBadge(z.payout)}">${payoutLabel(z.payout)}</span></td>
<td><button class="btn btn-xs btn-secondary">Detaljer</button></td>
</tr>`).join('');
}
function filterZoomers() {
const q = document.getElementById('zoomerSearch').value.toLowerCase();
const s = document.getElementById('zoomerStatusFilter').value;
let filtered = zoomers.filter(z => {
if (q && !z.name.toLowerCase().includes(q)) return false;
if (s && z.status !== s) return false;
return true;
});
renderZoomerTable(filtered);
}
function renderPayoutTable(data) {
const sf = document.getElementById('payoutStatusFilter2')?.value||'';
const filtered = sf ? data.filter(p=>p.status===sf) : data;
document.getElementById('payoutTable').innerHTML = filtered.map(p => `
<tr>
<td style="font-size:12px;color:var(--text3)">${p.date}</td>
<td><strong>${p.zoomer}</strong></td>
<td class="right mono"><strong>${fmt(p.amount)} kr</strong></td>
<td><span class="badge ${payoutBadge(p.status)}">${payoutLabel(p.status)}</span></td>
<td style="font-size:11px;font-family:monospace;color:var(--text3)">${p.stripeId}</td>
<td>${p.status==='failed'?`<button class="btn btn-xs btn-danger" onclick="alert('Retry initiated for ${p.zoomer}')">↩ Retry</button>`:`<button class="btn btn-xs btn-secondary">Detaljer</button>`}</td>
</tr>`).join('');
}
function filterPayouts() { renderPayoutTable(payoutHistory); }
function renderGeo() {
const total = geoData.reduce((s,g) => s+g.earned, 0);
document.getElementById('geoGrid').innerHTML = geoData.map(g => `
<div class="geo-card">
<div class="gc-name">${g.flag} ${g.country}</div>
<div class="geo-bar-wrap"><div class="geo-bar" style="width:${g.pct}%"></div></div>
<div class="geo-meta"><strong>${g.zoomers} zoomers</strong> · ${fmtK(g.earned)} kr · ${g.pct}% av total</div>
</div>`).join('');
}
function renderDAC7() {
document.getElementById('dac7Pct').textContent = dac7Pct+'%';
document.getElementById('dac7Bar').style.width = dac7Pct+'%';
document.getElementById('dac7Bar').style.background = dac7Pct>=80?'var(--green)':dac7Pct>=60?'var(--yellow)':'var(--red)';
document.getElementById('dac7Verified').textContent = tinVerified;
document.getElementById('dac7Total').textContent = zoomers.length;
document.getElementById('dac7StatusList').innerHTML = `
<div style="display:flex;flex-direction:column;gap:8px">
<div style="display:flex;justify-content:space-between"><span style="font-size:12px;color:var(--text2)">✅ TIN verifierat</span><strong style="color:var(--green)">${tinVerified}</strong></div>
<div style="display:flex;justify-content:space-between"><span style="font-size:12px;color:var(--text2)">⏳ Saknar TIN</span><strong style="color:var(--red)">${zoomers.length-tinVerified}</strong></div>
<div style="display:flex;justify-content:space-between"><span style="font-size:12px;color:var(--text2)">📋 Rapporteringspliktiga</span><strong>${zoomers.filter(z=>z.earned>25000||z.assignments>25).length}</strong></div>
<div style="display:flex;justify-content:space-between"><span style="font-size:12px;color:var(--text2)">🏳️ Under tröskel</span><strong style="color:var(--text3)">${zoomers.filter(z=>z.earned<=25000&&z.assignments<=25).length}</strong></div>
</div>`;
}
// ── UTILS ─────────────────────────────────────────────────────────────────────
function payoutBadge(s) { return {sent:'badge-green',pending:'badge-yellow',failed:'badge-red'}[s]||'badge-gray'; }
function payoutLabel(s) { return {sent:'✓ Sent',pending:'⏳ Pending',failed:'✕ Failed'}[s]||s; }
function fmt(n) { return Math.round(n).toLocaleString('sv-SE'); }
function fmtK(n) { return n>=1000000?(n/1000000).toFixed(1)+'M':n>=1000?Math.round(n/1000)+'k':Math.round(n).toString(); }
function switchTab(id, el) {
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
document.getElementById('view-'+id).classList.add('active');
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
el.classList.add('active');
}
function exportCSV() {
let csv = 'Zoomer,Land,Uppdrag,Intjänat,Rating,Status,Utbetalning,TIN\n';
zoomers.forEach(z => {
csv += `"${z.name}","${z.country}",${z.assignments},${z.earned},${z.rating},"${z.status}","${z.payout}","${z.tin?'Ja':'Nej'}"\n`;
});
const blob = new Blob([csv], {type:'text/csv;charset=utf-8;'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'quixzoom-revenue-'+new Date().toISOString().split('T')[0]+'.csv';
a.click();
}
renderAll();
</script>
</body>
</html>