Files
boc/revenue.html
T

542 lines
33 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="sv" data-theme="light">
<head>
<link rel="stylesheet" href="/_design-system/apple-native/tokens.css">
<link rel="stylesheet" href="/_design-system/apple-native/components.css">
<script src="/_design-system/apple-native/interactions.js" 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)}
</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; }
</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>