6989a98d75
- Arkitektur: docs/auth/passwordless-architecture.md - Backend: iom/quixzoom-auth-service/ (FastAPI + Redis) - Webb: quixzoom-market-pages/se/login/ (QR-kod + polling) - App: iom/quixzoom-app/src/features/auth/ (push + deep links) Flöde: QR-kod → app-godkännande → webb-inloggad
556 lines
35 KiB
HTML
556 lines
35 KiB
HTML
<!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: var(--radius-md);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: var(--radius-md);padding:4px;border:1px solid var(--border);width:fit-content;margin-bottom:20px}
|
||
.tab{padding:7px 18px;border-radius: var(--radius-sm);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: var(--radius-md);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: var(--radius-md);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: var(--radius-sm) 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: var(--radius-xl);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: var(--radius-xl);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: var(--radius-md);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: var(--radius-md);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: var(--radius-sm) 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: var(--radius-md);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: var(--radius-full);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: var(--radius-md);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: var(--radius-md);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: var(--radius-sm);margin-bottom:6px;overflow:hidden}
|
||
.geo-bar{height:100%;background:var(--qx);border-radius: var(--radius-sm);transition:width .5s}
|
||
.geo-meta{font-size:11px;color:var(--text2)}
|
||
/* DAC7 */
|
||
.dac7-panel{background:var(--card);border:1px solid var(--border);border-radius: var(--radius-md);padding:20px;margin-bottom:20px}
|
||
.dac7-donut{width:120px;height:120px;border-radius: var(--radius-full);position:relative;margin:0 auto 12px}
|
||
.compliance-meter{height:12px;background:var(--surface);border-radius: var(--radius-sm);overflow:hidden;margin-bottom:8px}
|
||
.compliance-fill{height:100%;border-radius: var(--radius-sm);background:var(--green);transition:width .5s}
|
||
/* PAYOUT */
|
||
.payout-card{background:linear-gradient(135deg,#1A1A2E,#16213E);color:#fff;border-radius: var(--radius-lg);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"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg></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"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" stroke="#666" stroke-width="1.5"/><path d="M8 2V4M8 12V14M2 8H4M12 8H14M4.3 4.3L5.7 5.7M10.3 10.3L11.7 11.7M4.3 11.7L5.7 10.3M10.3 5.7L11.7 4.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg></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"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>Godkända</button>
|
||
<button class="sidebar-item"><span class="icon">⏳</span>Väntande</button>
|
||
<button class="sidebar-item"><span class="icon"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg></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"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> 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)"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Granska lista</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- REVENUE CHART -->
|
||
<div class="chart-card">
|
||
<h3><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 12L6 8L9 11L14 5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 5H14V9" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> 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: var(--radius-md);padding:18px">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||
<div style="width:32px;height:32px;background:#635BFF;border-radius: var(--radius-md);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: var(--radius-md);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: var(--radius-md);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: var(--radius-md) 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="<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="#666" stroke-width="2"/><path d="M11 11L14 14" stroke="#666" stroke-width="2" stroke-linecap="round"/></svg> 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: var(--radius-md);padding:14px;font-size:12px;color:#854d0e">
|
||
<strong><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> DAC7 deadline:</strong> Rapportering för kalenderår 2026 ska lämnas till Skatteverket senast <strong>31 januari 2027</strong>. Zoomers med intäkter > 2 000 EUR eller > 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:'<span class="flag-se">SE</span>', 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"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> 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: var(--radius-full);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)"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> 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)"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> 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:'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> 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>
|