Files
boc/backups/2026-07-12-1023/quixzoom-landing-prod/zoomer-profile.html
T
Bernt 58ca4e68db feat(boc): Complete Business Operations Center v1.0
- Go backend API with full CRUD for all modules (CRM, Sales, Finance, HR, Legal, Marketing, Support, Purchase, Inventory, Projects, Automation, Analytics)
- Rust analytics service with parallel report generation
- C runtime with POSIX shared memory IPC
- PostgreSQL schema with 30+ tables, full migrations
- Redis cache, sessions, pub/sub
- Kafka event streaming with Zookeeper
- WebSocket hub for real-time updates
- Automation engine with cron jobs, workflows, event triggers
- JWT authentication, multi-tenant from start
- Docker Compose with all services
- Nginx reverse proxy with rate limiting
- Integration tests passing
- Feature gap analysis against Fortnox/Odoo/Visma

Refs: BOC-001
2026-07-12 12:41:35 +00:00

954 lines
42 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="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Profil — quiXzoom</title>
<link rel="stylesheet" >
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-text);
background: var(--grouped-background-primary);
color: var(--label-primary);
min-height: 100dvh;
padding-top: env(safe-area-inset-top, 44px);
padding-bottom: calc(env(safe-area-inset-bottom, 34px) + 83px);
-webkit-font-smoothing: antialiased;
}
/* Nav */
.nav-bar {
position: sticky; top: 0; z-index: 200;
display: flex; align-items: center; justify-content: center;
height: 44px; padding: 0 16px;
background: var(--navbar-bg);
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
border-bottom: 0.5px solid var(--separator);
}
.nav-title { font-size: 17px; font-weight: 600; letter-spacing: -0.41px; }
/* Profile Hero */
.profile-hero {
display: flex; flex-direction: column; align-items: center;
padding: 32px 20px 24px;
}
.avatar {
width: 88px; height: 88px; border-radius: var(--radius-full);
background: linear-gradient(135deg, var(--ios-blue), var(--ios-indigo));
display: flex; align-items: center; justify-content: center;
font-size: 42px; margin-bottom: 14px;
position: relative;
}
.avatar-badge {
position: absolute; bottom: 0; right: 0;
width: 26px; height: 26px; border-radius: var(--radius-full);
border: 2.5px solid var(--grouped-background-primary);
display: flex; align-items: center; justify-content: center;
font-size: 14px;
}
.avatar-badge.verified { background: var(--ios-green); }
.avatar-badge.unverified { background: var(--ios-orange); }
.profile-name {
font-family: var(--font-display);
font-size: 24px; font-weight: 700; letter-spacing: -0.5px;
margin-bottom: 4px;
}
.profile-id {
font-size: 13px; color: var(--label-secondary);
font-family: var(--font-mono);
}
.rating-row {
display: flex; align-items: center; gap: 6px;
margin-top: 10px;
}
.stars { color: var(--ios-yellow); font-size: 16px; }
.rating-num { font-size: 15px; font-weight: 600; }
.rating-count { font-size: 13px; color: var(--label-secondary); }
/* Stats */
.stats-grid {
display: grid; grid-template-columns: 1fr 1fr 1fr;
margin: 0 16px 20px;
background: var(--background-secondary);
border-radius: var(--radius-lg);
border: 0.5px solid var(--separator);
overflow: hidden;
}
.stat-cell {
padding: 16px 8px; text-align: center;
border-right: 0.5px solid var(--separator);
}
.stat-cell:last-child { border-right: none; }
.stat-val { font-size: 22px; font-weight: 700; font-family: var(--font-display); }
.stat-lbl { font-size: 11px; color: var(--label-secondary); margin-top: 2px; }
.green { color: var(--ios-green); }
.blue { color: var(--ios-blue); }
.yellow { color: var(--ios-yellow); }
/* Section */
.section { margin: 0 16px 20px; }
.section-header {
font-size: 13px; font-weight: 500; color: var(--label-secondary);
text-transform: uppercase; letter-spacing: 0.5px;
padding: 0 4px 8px;
}
.section-card {
background: var(--background-secondary);
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
/* List row */
.list-row {
display: flex; align-items: center; gap: 14px;
padding: 13px 16px;
border-bottom: 0.5px solid var(--separator);
cursor: pointer; transition: opacity 0.12s;
}
.list-row:last-child { border-bottom: none; }
.list-row:active { opacity: 0.7; }
.row-icon-wrap {
width: 32px; height: 32px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 18px; flex-shrink: 0;
}
.row-body { flex: 1; }
.row-title { font-size: 16px; }
.row-sub { font-size: 13px; color: var(--label-secondary); margin-top: 2px; }
.row-right {
display: flex; align-items: center; gap: 6px;
color: var(--label-tertiary); font-size: 15px;
}
.chevron { font-size: 15px; color: var(--label-tertiary); }
/* KYC Card */
.kyc-card {
background: var(--background-secondary);
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.kyc-top {
display: flex; gap: 14px; align-items: center;
padding: 16px;
}
.kyc-icon {
width: 52px; height: 52px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 26px; flex-shrink: 0;
}
.kyc-icon.unverified { background: rgba(255,149,0,0.15); }
.kyc-icon.verified { background: rgba(52,199,89,0.15); }
.kyc-icon.pending { background: rgba(0,122,255,0.15); }
.kyc-info { flex: 1; }
.kyc-title { font-size: 17px; font-weight: 600; margin-bottom: 3px; }
.kyc-desc { font-size: 13px; color: var(--label-secondary); }
.kyc-status-bar {
padding: 10px 16px;
font-size: 13px;
border-top: 0.5px solid var(--separator);
display: flex; align-items: center; gap: 8px;
}
.status-chip {
display: inline-flex; align-items: center; gap: 4px;
padding: 4px 10px; border-radius: var(--radius-xl);
font-size: 12px; font-weight: 600;
}
.chip-unverified { background: rgba(255,149,0,0.15); color: var(--ios-orange); }
.chip-verified { background: rgba(52,199,89,0.15); color: var(--ios-green); }
.chip-pending { background: rgba(0,122,255,0.15); color: var(--ios-blue); }
.kyc-btn {
display: block; width: 100%;
padding: 15px;
border: none; border-radius: 0;
font-family: var(--font-text); font-size: 16px; font-weight: 700;
cursor: pointer; transition: opacity 0.15s;
border-top: 0.5px solid var(--separator);
letter-spacing: -0.2px;
}
.kyc-btn.unverified { background: var(--ios-orange); color: #fff; }
.kyc-btn.verified { background: var(--fill-secondary); color: var(--label-secondary); pointer-events: none; }
.kyc-btn.pending { background: var(--fill-secondary); color: var(--ios-blue); pointer-events: none; }
.kyc-btn:active { opacity: 0.8; }
/* Mission type breakdown */
.type-row {
display: flex; align-items: center; gap: 10px;
padding: 11px 16px;
border-bottom: 0.5px solid var(--separator);
}
.type-row:last-child { border-bottom: none; }
.type-icon { font-size: 16px; width: 24px; text-align: center; }
.type-label { flex: 1; font-size: 14px; }
.type-count { font-size: 14px; font-weight: 600; color: var(--label-secondary); }
.type-bar-wrap { width: 80px; }
.type-bar { height: 4px; border-radius: var(--radius-sm); background: var(--ios-blue); }
/* Onboarding Progress Bar */
.onboarding-card {
margin: 0 16px 20px;
background: var(--background-secondary);
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.onboarding-header {
display: flex; justify-content: space-between; align-items: center;
padding: 14px 16px 10px;
}
.onboarding-title { font-size: 16px; font-weight: 600; }
.onboarding-fraction { font-size: 13px; color: var(--label-secondary); font-weight: 500; }
.onboarding-track {
height: 5px; background: var(--fill-secondary);
margin: 0 16px 14px; border-radius: var(--radius-sm); overflow: hidden;
}
.onboarding-fill {
height: 100%; border-radius: var(--radius-sm);
background: linear-gradient(90deg, var(--ios-blue), var(--ios-indigo));
transition: width 0.6s cubic-bezier(0.34,1.56,0.64,1);
}
.onboarding-steps {
display: flex; border-top: 0.5px solid var(--separator);
}
.onboarding-step {
flex: 1; padding: 13px 8px; text-align: center;
border-right: 0.5px solid var(--separator);
}
.onboarding-step:last-child { border-right: none; }
.os-icon { font-size: 22px; margin-bottom: 5px; }
.os-label { font-size: 11px; font-weight: 500; color: var(--label-secondary); }
.os-label.done { color: var(--ios-green); }
.os-label.active { color: var(--ios-blue); }
.onboarding-cta {
display: block; width: calc(100% - 32px);
margin: 0 16px 16px;
padding: 15px;
background: var(--ios-blue); color: #fff;
border: none; border-radius: var(--radius-md);
font-family: var(--font-text); font-size: 16px; font-weight: 700;
cursor: pointer; transition: opacity 0.15s;
letter-spacing: -0.2px; text-align: center;
}
.onboarding-cta:active { opacity: 0.8; }
.onboarding-cta:disabled { background: var(--fill-secondary); color: var(--label-secondary); pointer-events: none; }
/* Earnings Calculator */
.calc-card {
background: var(--background-secondary);
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.calc-header { padding: 14px 16px 12px; border-bottom: 0.5px solid var(--separator); }
.calc-title { font-size: 16px; font-weight: 600; }
.calc-sub { font-size: 13px; color: var(--label-secondary); margin-top: 2px; }
.calc-body { padding: 16px; }
.calc-slider-row {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 6px;
}
.calc-label { font-size: 14px; color: var(--label-secondary); }
.calc-value { font-size: 14px; font-weight: 600; }
.calc-slider {
width: 100%; margin-bottom: 16px;
-webkit-appearance: none; appearance: none;
height: 4px; border-radius: var(--radius-sm);
background: var(--ios-blue); outline: none;
}
.calc-slider::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 22px; height: 22px; border-radius: var(--radius-full);
background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.25);
cursor: pointer;
}
.calc-result {
background: rgba(48,209,88,0.08);
border: 0.5px solid rgba(48,209,88,0.2);
border-radius: var(--radius-md); padding: 16px; text-align: center;
}
.calc-result-label { font-size: 13px; color: var(--label-secondary); margin-bottom: 6px; }
.calc-result-amount {
font-family: var(--font-display);
font-size: 36px; font-weight: 800; letter-spacing: -1px;
color: var(--ios-green);
}
.calc-result-unit { font-size: 13px; color: var(--label-secondary); margin-top: 4px; }
.calc-breakdown {
display: flex; justify-content: space-between;
margin-top: 12px; padding-top: 12px;
border-top: 0.5px solid var(--separator);
}
.calc-breakdown-item { text-align: center; }
.calc-breakdown-num { font-size: 16px; font-weight: 700; }
.calc-breakdown-label { font-size: 11px; color: var(--label-secondary); margin-top: 2px; }
/* Referral Program */
.referral-card {
background: linear-gradient(135deg, rgba(10,132,255,0.08), rgba(94,92,230,0.08));
border: 0.5px solid rgba(10,132,255,0.2);
border-radius: var(--radius-lg); padding: 20px;
}
.referral-header {
display: flex; gap: 12px; align-items: center; margin-bottom: 14px;
}
.referral-icon {
width: 48px; height: 48px; border-radius: var(--radius-md);
background: rgba(10,132,255,0.15);
display: flex; align-items: center; justify-content: center;
font-size: 24px;
}
.referral-info {}
.referral-title { font-size: 17px; font-weight: 700; }
.referral-desc { font-size: 13px; color: var(--label-secondary); margin-top: 3px; }
.referral-code-row {
display: flex; gap: 10px; align-items: stretch; margin-bottom: 14px;
}
.referral-code {
flex: 1; background: var(--fill-secondary); border-radius: var(--radius-md);
padding: 12px 14px;
font-family: var(--font-mono); font-size: 16px; font-weight: 700;
letter-spacing: 2px; color: var(--ios-blue); text-align: center;
}
.referral-copy-btn {
background: var(--ios-blue); color: #fff;
border: none; border-radius: var(--radius-md);
padding: 12px 16px; font-size: 14px; font-weight: 700;
cursor: pointer; transition: opacity 0.15s;
}
.referral-copy-btn:active { opacity: 0.8; }
.referral-share-btn {
display: block; width: 100%;
padding: 13px; background: rgba(10,132,255,0.12); color: var(--ios-blue);
border: 0.5px solid rgba(10,132,255,0.2); border-radius: var(--radius-md);
font-size: 15px; font-weight: 600;
cursor: pointer; transition: opacity 0.15s; text-align: center;
margin-bottom: 12px;
}
.referral-share-btn:active { opacity: 0.7; }
.referral-stats {
display: flex; gap: 10px;
}
.referral-stat {
flex: 1; background: var(--fill-secondary); border-radius: var(--radius-md);
padding: 10px; text-align: center;
}
.referral-stat-num { font-size: 20px; font-weight: 700; font-family: var(--font-display); }
.referral-stat-label { font-size: 11px; color: var(--label-secondary); margin-top: 2px; }
/* Loader */
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { width:24px; height:24px; border-radius: var(--radius-full); border:3px solid var(--fill-secondary); border-top-color:var(--ios-blue); animation:spin 0.8s linear infinite; display:inline-block; }
/* Tab Bar */
.tab-bar {
position: fixed; bottom: 0; left: 0; right: 0; z-index: 300;
height: calc(49px + env(safe-area-inset-bottom, 34px));
background: var(--tabbar-bg);
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
border-top: 0.5px solid var(--separator);
display: flex; align-items: flex-start; padding-top: 8px;
}
.tab-item { flex:1; display:flex; flex-direction:column; align-items:center; gap:3px; cursor:pointer; text-decoration:none; padding:0 4px; }
.tab-icon { font-size:24px; line-height:1; }
.tab-label { font-size:10px; font-weight:500; color:var(--label-tertiary); }
.tab-item.active .tab-label { color:var(--ios-blue); }
/* Toast */
.toast { position:fixed; bottom:calc(env(safe-area-inset-bottom,34px)+100px); left:50%; transform:translateX(-50%) translateY(20px); background:var(--background-secondary); border:0.5px solid var(--separator); border-radius: var(--radius-xl); padding:10px 18px; font-size:14px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.3); opacity:0; transition:all 0.3s; z-index:9999; white-space:nowrap; }
.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
@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>
<script src="i18n.js"></script>
<meta property="og:type" content="website">
<meta property="og:title" content="Profil — quiXzoom">
<meta property="og:description" content="quiXzoom — Earn per Mission. No Platform Fee.">
<meta property="og:url" content="https://www.quixzoom.com/zoomer-profile.html">
<meta property="og:image" content="https://www.quixzoom.com/images/zoomer-earnings.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="quiXzoom Zoomer profile and weekly earnings">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://www.quixzoom.com/images/zoomer-earnings.jpg">
<meta name="twitter:title" content="Profil — quiXzoom">
</head>
<body>
<h1 style="position:absolute;left:-9999px;">Zoomer Profile - quiXzoom</h1>
<!-- Nav -->
<div class="nav-bar">
<span class="nav-title">Profil</span>
</div>
<!-- Profile Hero -->
<div class="profile-hero">
<div class="avatar" id="avatar">
<div class="avatar-badge unverified" id="avatar-badge"><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></div>
</div>
<div class="profile-name" id="profile-name">Zoomer</div>
<div class="profile-id" id="profile-id">Laddar...</div>
<div class="rating-row">
<span class="stars"></span>
<span class="rating-num" id="rating-num"></span>
<span class="rating-count" id="rating-count">(0 uppdrag)</span>
</div>
</div>
<!-- Stats -->
<div class="stats-grid">
<div class="stat-cell">
<div class="stat-val green" id="stat-completed"></div>
<div class="stat-lbl">Slutförda</div>
</div>
<div class="stat-cell">
<div class="stat-val blue" id="stat-pågående"></div>
<div class="stat-lbl">Pågående</div>
</div>
<div class="stat-cell">
<div class="stat-val yellow" id="stat-earned"></div>
<div class="stat-lbl">Intjänat kr</div>
</div>
</div>
<!-- Onboarding Progress -->
<div class="onboarding-card" id="onboarding-section">
<div class="onboarding-header">
<span class="onboarding-title"><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> Kom igång</span>
<span class="onboarding-fraction" id="ob-fraction">0 / 3 klara</span>
</div>
<div class="onboarding-track">
<div class="onboarding-fill" id="ob-fill" style="width:0%"></div>
</div>
<div class="onboarding-steps">
<div class="onboarding-step" id="ob-step-1">
<div class="os-icon"></div>
<div class="os-label" id="ob-lbl-1">KYC</div>
</div>
<div class="onboarding-step" id="ob-step-2">
<div class="os-icon"></div>
<div class="os-label" id="ob-lbl-2">Utbetalning</div>
</div>
<div class="onboarding-step" id="ob-step-3">
<div class="os-icon"></div>
<div class="os-label" id="ob-lbl-3">Första uppdrag</div>
</div>
</div>
</div>
<button class="onboarding-cta" id="ob-cta" onclick="goToFirstMission()" disabled>Ta mitt första uppdrag →</button>
<!-- KYC Section -->
<div class="section">
<div class="section-header">Identitetsverifiering</div>
<div class="kyc-card">
<div class="kyc-top">
<div class="kyc-icon unverified" id="kyc-icon"></div>
<div class="kyc-info">
<div class="kyc-title">Identitetsverifiering</div>
<div class="kyc-desc">Verifiera din identitet för att ta emot utbetalningar. Krävs enligt lag (AML).</div>
</div>
</div>
<div class="kyc-status-bar" id="kyc-status-bar">
<span class="status-chip chip-unverified" id="kyc-chip"><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> Ej verifierad</span>
<span style="color:var(--label-secondary)">Verifiera för att låsa upp alla uppdrag</span>
</div>
<button class="kyc-btn unverified" id="kyc-btn" onclick="startKYC()">
Verifiera identitet →
</button>
</div>
</div>
<!-- Earnings Calculator -->
<div class="section">
<div class="section-header">Inkomstpotential</div>
<div class="calc-card">
<div class="calc-header">
<div class="calc-title"> Vad kan du tjäna?</div>
<div class="calc-sub">Estimera din månadsinkomst</div>
</div>
<div class="calc-body">
<div class="calc-slider-row">
<span class="calc-label">Tillgänglighet</span>
<span class="calc-value" id="calc-hours-lbl">3 dagar/vecka</span>
</div>
<input type="range" class="calc-slider" id="calc-hours" min="1" max="7" value="3" oninput="updateCalc()">
<div class="calc-slider-row">
<span class="calc-label">Uppdragstyp</span>
<span class="calc-value" id="calc-type-lbl">Fasadscanning (800 kr)</span>
</div>
<select id="calc-type" onchange="updateCalc()" style="width:100%;background:var(--fill-secondary);border:0.5px solid var(--separator);border-radius: var(--radius-md);padding:10px 14px;font-size:14px;color:var(--label-primary);font-family:var(--font-text);margin-bottom:16px;-webkit-appearance:none">
<option value="800"><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 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Fasadscanning — 800 kr</option>
<option value="1200"> Takscanning — 1 200 kr</option>
<option value="600"> Vegetationsinventering — 600 kr</option>
<option value="1500"> Väginspektionen — 1 500 kr</option>
<option value="2000"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 14H14M4 14V8L8 4L12 8V14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 14V10H10V14" stroke="#666" stroke-width="1.5"/></svg> Byggnadsdokumentation — 2 000 kr</option>
</select>
<div class="calc-result">
<div class="calc-result-label">Estimerad månadsinkomst</div>
<div class="calc-result-amount" id="calc-amount"></div>
<div class="calc-result-unit">SEK / månad (efter avdrag)</div>
<div class="calc-breakdown">
<div class="calc-breakdown-item">
<div class="calc-breakdown-num" id="calc-missions"></div>
<div class="calc-breakdown-label">Uppdrag/mån</div>
</div>
<div class="calc-breakdown-item">
<div class="calc-breakdown-num" id="calc-gross"></div>
<div class="calc-breakdown-label">Brutto kr</div>
</div>
<div class="calc-breakdown-item">
<div class="calc-breakdown-num">20%</div>
<div class="calc-breakdown-label">quiXzoom-avg</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Referral Program -->
<div class="section">
<div class="section-header">Referral Program</div>
<div class="referral-card">
<div class="referral-header">
<div class="referral-icon"></div>
<div class="referral-info">
<div class="referral-title">Tjäna 200 kr per ny Zoomer</div>
<div class="referral-desc">Dela din kod — du och din vän får 200 kr när de slutför sitt första uppdrag</div>
</div>
</div>
<div class="referral-code-row">
<div class="referral-code" id="referral-code">ZOOM-——</div>
<button class="referral-copy-btn" onclick="copyReferralCode()"><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> Kopiera</button>
</div>
<button class="referral-share-btn" onclick="shareReferral()"> Dela via iOS Share Sheet</button>
<div class="referral-stats">
<div class="referral-stat">
<div class="referral-stat-num" id="ref-count">0</div>
<div class="referral-stat-label">Inbjudna</div>
</div>
<div class="referral-stat">
<div class="referral-stat-num" id="ref-converted">0</div>
<div class="referral-stat-label">Aktiverade</div>
</div>
<div class="referral-stat">
<div class="referral-stat-num" id="ref-earned">0 kr</div>
<div class="referral-stat-label">Intjänat</div>
</div>
</div>
</div>
</div>
<!-- Mission breakdown -->
<div class="section">
<div class="section-header">Uppdragstyper</div>
<div class="section-card" id="type-breakdown">
<div style="padding:20px;text-align:center"><div class="spinner"></div></div>
</div>
</div>
<!-- Settings -->
<div class="section">
<div class="section-header">Inställningar</div>
<div class="section-card">
<div class="list-row" onclick="copyId()">
<div class="row-icon-wrap" style="background:rgba(0,122,255,0.15)"></div>
<div class="row-body">
<div class="row-title">Zoomer-ID</div>
<div class="row-sub" id="full-id" style="font-family:var(--font-mono);font-size:11px"></div>
</div>
<span class="row-right">Kopiera <span class="chevron"></span></span>
</div>
<div class="list-row" onclick="openEarnings()">
<div class="row-icon-wrap" style="background:rgba(52,199,89,0.15)"></div>
<div class="row-body">
<div class="row-title">Intjäning & utbetalningar</div>
<div class="row-sub">Utbetalningskonto, historik</div>
</div>
<span class="chevron"></span>
</div>
<div class="list-row" onclick="openSupport()">
<div class="row-icon-wrap" style="background:rgba(175,82,222,0.15)"></div>
<div class="row-body">
<div class="row-title">Support</div>
<div class="row-sub">support@quixzoom.com</div>
</div>
<span class="chevron"></span>
</div>
<div class="list-row" onclick="openTerms()">
<div class="row-icon-wrap" style="background:rgba(142,142,147,0.15)"></div>
<div class="row-body">
<div class="row-title">Användarvillkor & Integritet</div>
<div class="row-sub">Juridisk information</div>
</div>
<span class="chevron"></span>
</div>
</div>
</div>
<!-- App info -->
<div style="text-align:center;padding:8px 16px 8px;color:var(--label-quaternary);font-size:12px">
quiXzoom Zoomer App · v1.0.0 · 2026
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<!-- Tab Bar -->
<nav class="tab-bar">
<a class="tab-item" href="zoomer-app.html">
<span class="tab-icon"></span>
<span class="tab-label">Uppdrag</span>
</a>
<a class="tab-item" href="zoomer-earnings.html">
<span class="tab-icon"></span>
<span class="tab-label">Intjäning</span>
</a>
<a class="tab-item active" href="zoomer-profile.html">
<span class="tab-icon"></span>
<span class="tab-label">Profil</span>
</a>
</nav>
<script>
const API = 'https://api.quixzoom.com/api/qz/missions';
const KYC_API = 'https://api.quixzoom.com/api/qz/kyc';
function getZoomerId() {
let id = localStorage.getItem('qz_zoomer_id');
if (!id) { id = 'zoomer-' + Math.random().toString(36).slice(2,10); localStorage.setItem('qz_zoomer_id', id); }
return id;
}
function getKYCStatus() {
return localStorage.getItem('qz_kyc_status') || 'unverified';
}
function setKYCStatus(s) {
localStorage.setItem('qz_kyc_status', s);
}
function toast(msg, dur=3000) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), dur);
}
function formatSEK(n) { return new Intl.NumberFormat('sv-SE').format(n); }
function missionTypeLabel(type) {
const m = { facade_scan:'Fasadscanning', roof_scan:'Takscanning', road_inspection:'Väginspekt.', interior_scan:'Interi ör', vegetation_survey:'Vegetation' };
return m[type] || type;
}
function missionTypeEmoji(type) {
const m = { facade_scan:'<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 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg>', roof_scan:'', road_inspection:'', interior_scan:'', vegetation_survey:'', inspection:'<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>' };
return m[type] || '';
}
async function loadProfile() {
const zoomerId = getZoomerId();
document.getElementById('profile-id').textContent = zoomerId.slice(0,16) + '...';
document.getElementById('full-id').textContent = zoomerId;
document.getElementById('profile-name').textContent = 'Zoomer · ' + zoomerId.slice(-6).toUpperCase();
try {
const r = await fetch(`${API}/missions`);
const data = await r.json();
const all = data.missions || [];
const mine = all.filter(m => m.zoomer_id === zoomerId);
const completed = mine.filter(m => m.status === 'approved');
const ongoing = mine.filter(m => ['assigned','submitted'].includes(m.status));
const totalEarned = completed.reduce((s,m) => s + m.reward_sek, 0);
document.getElementById('stat-completed').textContent = completed.length;
document.getElementById('stat-pågående').textContent = ongoing.length;
document.getElementById('stat-earned').textContent = formatSEK(totalEarned);
// Rating (mock based on completions)
const rating = completed.length === 0 ? null : Math.min(5, 4.0 + completed.length * 0.1).toFixed(1);
document.getElementById('rating-num').textContent = rating || '—';
document.getElementById('rating-count').textContent = `(${completed.length} uppdrag)`;
// Type breakdown
const typeCounts = {};
mine.forEach(m => {
typeCounts[m.mission_type] = (typeCounts[m.mission_type] || 0) + 1;
});
const maxCount = Math.max(...Object.values(typeCounts), 1);
const breakdownEl = document.getElementById('type-breakdown');
if (Object.keys(typeCounts).length === 0) {
breakdownEl.innerHTML = `<div style="padding:24px;text-align:center;color:var(--label-secondary)">Inga uppdrag ännu</div>`;
} else {
breakdownEl.innerHTML = Object.entries(typeCounts)
.sort((a,b) => b[1]-a[1])
.map(([type, count]) => `
<div class="type-row">
<span class="type-icon">${missionTypeEmoji(type)}</span>
<span class="type-label">${missionTypeLabel(type)}</span>
<div class="type-bar-wrap">
<div class="type-bar" style="width:${Math.round(count/maxCount*100)}%"></div>
</div>
<span class="type-count">${count}st</span>
</div>`).join('');
}
} catch(e) {
console.error(e);
}
// KYC status
renderKYCStatus(getKYCStatus());
}
function renderKYCStatus(status) {
const iconEl = document.getElementById('kyc-icon');
const chipEl = document.getElementById('kyc-chip');
const btnEl = document.getElementById('kyc-btn');
const badgeEl = document.getElementById('avatar-badge');
if (status === 'verified') {
iconEl.className = 'kyc-icon verified';
iconEl.textContent = '<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>';
chipEl.className = 'status-chip chip-verified';
chipEl.textContent = '<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> Verifierad';
btnEl.className = 'kyc-btn verified';
btnEl.textContent = '<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> Identitet verifierad';
badgeEl.className = 'avatar-badge verified';
badgeEl.textContent = '<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>';
document.getElementById('kyc-status-bar').querySelector('span:last-child').textContent = 'Konto verifierat — alla uppdrag tillgängliga';
} else if (status === 'pending') {
iconEl.className = 'kyc-icon pending';
iconEl.textContent = '⏳';
chipEl.className = 'status-chip chip-pending';
chipEl.textContent = '⏳ Under granskning';
btnEl.className = 'kyc-btn pending';
btnEl.textContent = '⏳ Verifiering pågår...';
badgeEl.textContent = '⏳';
} else {
// unverified
iconEl.className = 'kyc-icon unverified';
iconEl.textContent = '';
chipEl.className = 'status-chip chip-unverified';
chipEl.textContent = '<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> Ej verifierad';
btnEl.className = 'kyc-btn unverified';
btnEl.textContent = ' Verifiera identitet →';
}
}
async function startKYC() {
const btn = document.getElementById('kyc-btn');
btn.textContent = '⏳ Startar verifiering...';
btn.disabled = true;
try {
// Skapa unik session
const sessionId = 'kyc-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
// Öppna KYC-verifiering i ny flik
const kycUrl = 'https://app.quixzoom.com/kyc-verify.html?session=' + sessionId;
setKYCStatus('pending');
renderKYCStatus('pending');
toast(' Öppnar verifiering...');
window.open(kycUrl, '_blank');
// Starta polling för status
pollKYCStatus(sessionId);
} catch(e) {
console.error('KYC error:', e);
toast('<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> Ett fel uppstod: ' + e.message);
btn.textContent = ' Verifiera identitet →';
btn.disabled = false;
}
}
// Polla KYC-status
async function pollKYCStatus(sessionId) {
const check = async () => {
try {
const res = await fetch(KYC_API + '/status/' + sessionId);
const data = await res.json();
if (data.assurance_level === 'photo_low' || data.status === 'approved') {
setKYCStatus('verified');
renderKYCStatus('verified');
toast('<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> Identitet verifierad!');
return;
} else if (data.assurance_level === 'rejected') {
setKYCStatus('unverified');
renderKYCStatus('unverified');
toast('<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> Verifiering nekad: ' + (data.reject_reason || 'Okänt fel'));
return;
}
setTimeout(check, 5000);
} catch(e) {
console.error('Poll error:', e);
}
};
check();
}
function copyId() {
const id = getZoomerId();
navigator.clipboard?.writeText(id).then(() => toast('<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> ID kopierat!')).catch(() => toast('ID: ' + id));
}
function openEarnings() { window.location.href = 'zoomer-earnings.html'; }
function openSupport() { window.open('https://quixzoom.com/support', '_blank'); }
function openTerms() { window.open('https://quixzoom.com/terms', '_blank'); }
loadProfile();
// === Onboarding Progress Bar ===
function updateOnboarding() {
const kycStatus = getKYCStatus();
const hasBank = localStorage.getItem('qz_bank_linked') === 'true';
const hasFirstMission = parseInt(localStorage.getItem('qz_missions_completed') || '0') > 0;
const steps = [
kycStatus === 'verified',
hasBank,
hasFirstMission
];
const done = steps.filter(Boolean).length;
const pct = Math.round(done / 3 * 100);
document.getElementById('ob-fill').style.width = pct + '%';
document.getElementById('ob-fraction').textContent = done + ' / 3 klara';
const labels = ['ob-lbl-1','ob-lbl-2','ob-lbl-3'];
const texts = ['KYC','Utbetalning','Första uppdrag'];
steps.forEach((s,i) => {
const el = document.getElementById(labels[i]);
el.className = 'os-label ' + (s ? 'done' : (i === done ? 'active' : ''));
if (s) el.textContent = '<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> ' + texts[i];
else if (i === done) el.textContent = ' ' + texts[i];
else el.textContent = texts[i];
});
const cta = document.getElementById('ob-cta');
if (done >= 2) {
// KYC + bank done — can take first mission
cta.disabled = false;
cta.textContent = steps[2] ? '<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> Första uppdrag slutfört!' : ' Ta mitt första uppdrag →';
if (steps[2]) cta.disabled = true;
} else {
cta.disabled = true;
cta.textContent = done === 0 ? ' Slutför KYC för att börja' :
done === 1 ? ' Länka bankkonto för att fortsätta' :
' Ta mitt första uppdrag →';
}
}
updateOnboarding();
function goToFirstMission() {
window.location.href = 'zoomer-app.html';
}
// === Earnings Calculator ===
function updateCalc() {
const days = parseInt(document.getElementById('calc-hours').value);
const reward = parseInt(document.getElementById('calc-type').value);
const typeEl = document.getElementById('calc-type');
const selectedText = typeEl.options[typeEl.selectedIndex].text;
const rewardMatch = selectedText.match(/(\d[\d\s]+)\s*kr/);
const rewardLabel = rewardMatch ? rewardMatch[0] : reward + ' kr';
// Estimate: ~2 missions per working day
const missionsPerMonth = days * 4 * 2;
const gross = missionsPerMonth * reward;
const net = Math.round(gross * 0.80); // 20% platform fee
const dayLabels = ['','1 dag','2 dagar','3 dagar','4 dagar','5 dagar','6 dagar','7 dagar'];
document.getElementById('calc-hours-lbl').textContent = dayLabels[days] + '/vecka';
document.getElementById('calc-type-lbl').textContent = selectedText.replace(/^[^\s]+ /, '');
document.getElementById('calc-amount').textContent = new Intl.NumberFormat('sv-SE').format(net);
document.getElementById('calc-missions').textContent = missionsPerMonth;
document.getElementById('calc-gross').textContent = new Intl.NumberFormat('sv-SE').format(gross);
}
updateCalc();
// === Referral Program ===
function getReferralCode() {
let code = localStorage.getItem('qz_referral_code');
if (!code) {
const id = getZoomerId().slice(-6).toUpperCase();
code = 'ZOOM-' + id;
localStorage.setItem('qz_referral_code', code);
}
return code;
}
function loadReferralStats() {
const code = getReferralCode();
document.getElementById('referral-code').textContent = code;
// Mock stats from localStorage
const invited = parseInt(localStorage.getItem('qz_ref_invited') || '0');
const converted = parseInt(localStorage.getItem('qz_ref_converted') || '0');
document.getElementById('ref-count').textContent = invited;
document.getElementById('ref-converted').textContent = converted;
document.getElementById('ref-earned').textContent = (converted * 200) + ' kr';
}
loadReferralStats();
function copyReferralCode() {
const code = getReferralCode();
const msg = 'Gå med i quiXzoom med min kod ' + code + ' och tjäna 200 kr på ditt första uppdrag! https://quixzoom.com/join?ref=' + code;
navigator.clipboard?.writeText(msg).then(() => toast('<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> Referral-kod kopierad!')).catch(() => toast('Kod: ' + code));
}
function shareReferral() {
const code = getReferralCode();
const shareData = {
title: 'quiXzoom — Ta drönarppdrag',
text: 'Gå med i quiXzoom med min kod ' + code + ' och vi båda tjänar 200 kr!',
url: 'https://quixzoom.com/join?ref=' + code
};
if (navigator.share) {
navigator.share(shareData).catch(() => copyReferralCode());
} else {
copyReferralCode();
}
}
</script>
</body>
</html>