Files
boc/quixzoom-landing-fixed/dac7-report.html
T
Bernt 1a12fb870b dev-api: add developer portal, OpenAPI spec, and Flatenbadet case study
- New /developers/ page with API docs, SDKs, pricing, use cases
- OpenAPI 3.0 spec for Orders, Missions, Photos, Analytics
- Case study: Glasskiosken i Flatenbadet — complete ROI analysis
- Updated /order/ with recurring missions and frequency dropdown
2026-07-14 15:27:33 +00:00

1718 lines
48 KiB
HTML

<!DOCTYPE html>
<html lang="sv" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="apple-native-compliance" content="full">
<meta name="module" content="dac7-report">
<title>DAC7 Rapportering · quiXzoom</title>
<link rel="stylesheet" >
<link rel="stylesheet" >
<script defer></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--qx-blue: #007AFF;
--qx-green: #34C759;
--qx-red: #FF3B30;
--qx-orange: #FF9500;
--qx-yellow: #FFCC00;
--qx-purple: #AF52DE;
--qx-gray: #8E8E93;
--qx-gray2: #AEAEB2;
--qx-gray3: #C7C7CC;
--qx-gray4: #D1D1D6;
--qx-gray5: #E5E5EA;
--qx-gray6: #F2F2F7;
--label-primary: rgba(0,0,0,1);
--label-secondary: rgba(60,60,67,.6);
--label-tertiary: rgba(60,60,67,.3);
--separator: rgba(60,60,67,.12);
--fill1: rgba(120,120,128,.2);
--fill2: rgba(120,120,128,.16);
--bg-primary: #FFFFFF;
--bg-secondary: #F2F2F7;
--bg-grouped: #F2F2F7;
--bg-card: #FFFFFF;
--glass-bg: rgba(255,255,255,.85);
--glass-blur: saturate(180%) blur(20px);
--safe-top: env(safe-area-inset-top, 0px);
--safe-bottom: env(safe-area-inset-bottom, 0px);
--radius-sm: 10px;
--radius-md: 13px;
--radius-lg: 16px;
--radius-xl: 20px;
--shadow-card: 0 1px 3px rgba(0,0,0,.08), 0 0 0 0.5px rgba(0,0,0,.06);
}
@media (prefers-color-scheme: dark) {
:root {
--label-primary: rgba(255,255,255,1);
--label-secondary: rgba(235,235,245,.6);
--label-tertiary: rgba(235,235,245,.3);
--separator: rgba(84,84,88,.6);
--fill1: rgba(120,120,128,.36);
--fill2: rgba(120,120,128,.32);
--bg-primary: #000000;
--bg-secondary: #1C1C1E;
--bg-grouped: #1C1C1E;
--bg-card: #2C2C2E;
--glass-bg: rgba(28,28,30,.85);
--shadow-card: 0 1px 3px rgba(0,0,0,.4), 0 0 0 0.5px rgba(255,255,255,.05);
}
}
html, body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display', 'Helvetica Neue', sans-serif;
background: var(--bg-grouped);
color: var(--label-primary);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
/* Navbar */
.navbar {
position: fixed;
top: 0; left: 0; right: 0;
height: calc(var(--safe-top) + 52px);
padding-top: var(--safe-top);
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border-bottom: 0.5px solid var(--separator);
display: flex;
align-items: flex-end;
padding-bottom: 10px;
padding-left: 16px;
padding-right: 16px;
z-index: 1000;
gap: 10px;
}
.navbar-logo {
width: 28px; height: 28px;
background: linear-gradient(135deg, #007AFF 0%, #5AC8FA 100%);
border-radius: var(--radius-sm);
display: flex; align-items: center; justify-content: center;
font-size: 15px;
flex-shrink: 0;
}
.navbar-title {
font-size: 17px;
font-weight: 600;
letter-spacing: -0.41px;
color: var(--label-primary);
flex: 1;
}
.navbar-badge {
font-size: 11px;
font-weight: 600;
color: var(--qx-blue);
background: rgba(0,122,255,.1);
padding: 3px 8px;
border-radius: var(--radius-xl);
letter-spacing: 0;
}
/* Layout */
.scroll-content {
padding-top: calc(var(--safe-top) + 52px + 20px);
padding-bottom: calc(var(--safe-bottom) + 40px);
padding-left: 16px;
padding-right: 16px;
max-width: 640px;
margin: 0 auto;
}
/* Section */
.section-label {
font-size: 13px;
font-weight: 600;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--label-secondary);
padding: 0 4px;
margin-bottom: 8px;
margin-top: 28px;
}
.section-label:first-child { margin-top: 0; }
/* Card */
.card {
background: var(--bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
overflow: hidden;
margin-bottom: 20px;
}
/* Hero summary */
.hero-card {
background: linear-gradient(160deg, #007AFF 0%, #5856D6 100%);
border-radius: var(--radius-xl);
padding: 24px 20px;
margin-bottom: 20px;
color: #fff;
position: relative;
overflow: hidden;
}
.hero-card::after {
content: '';
position: absolute;
top: -40px; right: -40px;
width: 180px; height: 180px;
border-radius: var(--radius-full);
background: rgba(255,255,255,.08);
}
.hero-tag {
font-size: 11px;
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase;
opacity: .75;
margin-bottom: 6px;
}
.hero-title {
font-size: 28px;
font-weight: 700;
letter-spacing: -.5px;
margin-bottom: 4px;
}
.hero-subtitle {
font-size: 15px;
opacity: .8;
margin-bottom: 20px;
}
.hero-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.hero-stat {
background: rgba(255,255,255,.15);
border-radius: var(--radius-md);
padding: 12px;
}
.hero-stat-value {
font-size: 22px;
font-weight: 700;
letter-spacing: -.5px;
}
.hero-stat-label {
font-size: 12px;
opacity: .75;
margin-top: 2px;
}
/* Deadline banner */
.deadline-banner {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-bottom: 0.5px solid var(--separator);
}
.deadline-icon {
font-size: 22px;
flex-shrink: 0;
}
.deadline-info { flex: 1; }
.deadline-title {
font-size: 15px;
font-weight: 600;
letter-spacing: -.2px;
}
.deadline-sub {
font-size: 13px;
color: var(--label-secondary);
margin-top: 2px;
}
.deadline-days {
font-size: 24px;
font-weight: 700;
color: var(--qx-orange);
flex-shrink: 0;
}
.deadline-days span {
font-size: 11px;
font-weight: 500;
display: block;
text-align: center;
color: var(--label-tertiary);
margin-top: -2px;
}
/* Status pill */
.status-row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
border-radius: var(--radius-xl);
padding: 4px 12px;
}
.status-pill.ready { background: rgba(52,199,89,.15); color: #34C759; }
.status-pill.progress { background: rgba(255,149,0,.15); color: #FF9500; }
.status-pill.overdue { background: rgba(255,59,48,.15); color: #FF3B30; }
.status-dot { width: 6px; height: 6px; border-radius: var(--radius-full); background: currentColor; }
/* List row */
.list-row {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 16px;
border-bottom: 0.5px solid var(--separator);
min-height: 52px;
}
.list-row:last-child { border-bottom: none; }
.list-row-icon {
width: 34px; height: 34px;
border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 17px;
flex-shrink: 0;
}
.list-row-body { flex: 1; min-width: 0; }
.list-row-title {
font-size: 15px;
font-weight: 500;
letter-spacing: -.2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.list-row-sub {
font-size: 13px;
color: var(--label-secondary);
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.list-row-right {
text-align: right;
flex-shrink: 0;
}
.list-row-value {
font-size: 15px;
font-weight: 600;
letter-spacing: -.3px;
}
.list-row-label {
font-size: 12px;
color: var(--label-secondary);
margin-top: 1px;
}
/* TIN badge */
.tin-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-weight: 600;
border-radius: var(--radius-sm);
padding: 2px 7px;
margin-top: 4px;
}
.tin-badge.verified {
background: rgba(52,199,89,.12);
color: #34C759;
}
.tin-badge.unverified {
background: rgba(255,149,0,.12);
color: #FF9500;
}
/* Threshold badge */
.threshold-badge {
display: inline-flex;
align-items: center;
font-size: 10px;
font-weight: 700;
letter-spacing: .05em;
text-transform: uppercase;
border-radius: var(--radius-sm);
padding: 2px 6px;
}
.threshold-badge.over {
background: rgba(0,122,255,.1);
color: var(--qx-blue);
}
.threshold-badge.under {
background: var(--fill2);
color: var(--label-tertiary);
}
/* Export buttons */
.export-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
padding: 14px 16px;
}
.export-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 11px 16px;
border: none;
border-radius: var(--radius-md);
font-size: 15px;
font-weight: 600;
letter-spacing: -.2px;
cursor: pointer;
transition: opacity .15s ease, transform .1s ease;
-webkit-tap-highlight-color: transparent;
}
.export-btn:active { opacity: .7; transform: scale(.97); }
.export-btn.json {
background: rgba(0,122,255,.1);
color: var(--qx-blue);
}
.export-btn.xml {
background: rgba(52,199,89,.1);
color: var(--qx-green);
}
/* Verify button */
.verify-btn {
font-size: 13px;
font-weight: 600;
color: var(--qx-blue);
background: rgba(0,122,255,.1);
border: none;
border-radius: var(--radius-md);
padding: 5px 12px;
cursor: pointer;
transition: opacity .15s;
-webkit-tap-highlight-color: transparent;
flex-shrink: 0;
}
.verify-btn:active { opacity: .6; }
.verify-btn.done {
background: rgba(52,199,89,.1);
color: var(--qx-green);
cursor: default;
}
/* Progress bar */
.progress-wrap {
padding: 12px 16px;
border-bottom: 0.5px solid var(--separator);
}
.progress-header {
display: flex;
justify-content: space-between;
font-size: 13px;
color: var(--label-secondary);
margin-bottom: 8px;
}
.progress-bar {
height: 6px;
background: var(--fill2);
border-radius: var(--radius-sm);
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: var(--radius-sm);
background: linear-gradient(90deg, #007AFF, #34C759);
transition: width .6s cubic-bezier(.25,.46,.45,.94);
}
/* Loading / error states */
.skeleton {
background: linear-gradient(90deg, var(--fill2) 25%, var(--fill1) 50%, var(--fill2) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: var(--radius-sm);
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.empty-state {
text-align: center;
padding: 40px 20px;
}
.empty-icon { font-size: 44px; margin-bottom: 12px; }
.empty-title {
font-size: 17px;
font-weight: 600;
letter-spacing: -.3px;
margin-bottom: 6px;
}
.empty-sub {
font-size: 15px;
color: var(--label-secondary);
}
.error-banner {
background: rgba(255,59,48,.1);
border-radius: var(--radius-md);
padding: 14px 16px;
margin-bottom: 16px;
font-size: 14px;
color: var(--qx-red);
display: none;
}
.error-banner.visible { display: block; }
/* Footer note */
.footer-note {
font-size: 12px;
color: var(--label-tertiary);
text-align: center;
padding: 0 16px;
margin-top: 8px;
line-height: 1.5;
}
/* Animations */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in { animation: fadeUp .4s cubic-bezier(.25,.46,.45,.94) both; }
.animate-in:nth-child(2) { animation-delay: .05s; }
.animate-in:nth-child(3) { animation-delay: .1s; }
.animate-in:nth-child(4) { animation-delay: .15s; }
.animate-in:nth-child(5) { animation-delay: .2s; }
@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; }
}
/* QUIXZOOM STANDARD — Dark Theme */
:root {
--qz-bg: #0a0a0a;
--qz-surface: #111111;
--qz-surface-2: #1a1a1a;
--qz-border: rgba(255,255,255,0.08);
--qz-text: #e8e8e8;
--qz-text-dim: #a0a0a0;
--qz-text-muted: #6b6b6b;
--qz-blue: #007AFF;
--qz-blue-dark: #0056CC;
--qz-radius: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
background: var(--qz-bg);
color: var(--qz-text);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
/* Navigation */
.qz-nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px; height: 56px;
background: rgba(255,255,255,0.92);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--qz-border);
}
.qz-nav-logo { font-size: 18px; font-weight: 700; color: #fff; text-decoration: none; }
.qz-nav-links { display: flex; gap: 24px; list-style: none; }
.qz-nav-links a { font-size: 14px; color: var(--qz-text-dim); text-decoration: none; transition: color 0.2s; }
.qz-nav-links a:hover { color: #fff; }
/* Buttons */
.qz-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 12px 24px; border-radius: var(--qz-radius);
font-size: 15px; font-weight: 600; text-decoration: none;
transition: all 0.2s; cursor: pointer; border: none;
}
.qz-btn-primary {
background: var(--qz-blue); color: #fff;
}
.qz-btn-primary:hover {
background: var(--qz-blue-dark); transform: translateY(-1px);
}
.qz-btn-ghost {
background: transparent; color: var(--qz-text);
border: 1px solid var(--qz-border);
}
.qz-btn-ghost:hover {
border-color: var(--qz-blue); color: var(--qz-blue);
}
/* Cards */
.qz-card {
background: var(--qz-surface);
border: 1px solid var(--qz-border);
border-radius: var(--qz-radius);
padding: 24px;
transition: transform 0.2s, border-color 0.2s;
}
.qz-card:hover {
transform: translateY(-2px);
border-color: rgba(0,122,255,0.3);
}
/* Footer */
.qz-footer {
border-top: 1px solid var(--qz-border);
padding: 40px 24px;
text-align: center;
color: var(--qz-text-dim);
font-size: 14px;
}
/* Banner */
.qz-banner {
background: #001E50;
color: rgba(255,255,255,0.55);
text-align: center;
padding: 10px;
font-size: 12px;
}
.qz-banner a { color: rgba(255,255,255,0.8); text-decoration: none; }
@media (max-width: 768px) {
.qz-nav-links { display: none; }
}
</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; }
}
/* QUIXZOOM STANDARD — Dark Theme */
:root {
--qz-bg: #0a0a0a;
--qz-surface: #111111;
--qz-surface-2: #1a1a1a;
--qz-border: rgba(255,255,255,0.08);
--qz-text: #e8e8e8;
--qz-text-dim: #a0a0a0;
--qz-text-muted: #6b6b6b;
--qz-blue: #007AFF;
--qz-blue-dark: #0056CC;
--qz-radius: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
background: var(--qz-bg);
color: var(--qz-text);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
/* Navigation */
.qz-nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px; height: 56px;
background: rgba(255,255,255,0.92);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--qz-border);
}
.qz-nav-logo { font-size: 18px; font-weight: 700; color: #fff; text-decoration: none; }
.qz-nav-links { display: flex; gap: 24px; list-style: none; }
.qz-nav-links a { font-size: 14px; color: var(--qz-text-dim); text-decoration: none; transition: color 0.2s; }
.qz-nav-links a:hover { color: #fff; }
/* Buttons */
.qz-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 12px 24px; border-radius: var(--qz-radius);
font-size: 15px; font-weight: 600; text-decoration: none;
transition: all 0.2s; cursor: pointer; border: none;
}
.qz-btn-primary {
background: var(--qz-blue); color: #fff;
}
.qz-btn-primary:hover {
background: var(--qz-blue-dark); transform: translateY(-1px);
}
.qz-btn-ghost {
background: transparent; color: var(--qz-text);
border: 1px solid var(--qz-border);
}
.qz-btn-ghost:hover {
border-color: var(--qz-blue); color: var(--qz-blue);
}
/* Cards */
.qz-card {
background: var(--qz-surface);
border: 1px solid var(--qz-border);
border-radius: var(--qz-radius);
padding: 24px;
transition: transform 0.2s, border-color 0.2s;
}
.qz-card:hover {
transform: translateY(-2px);
border-color: rgba(0,122,255,0.3);
}
/* Footer */
.qz-footer {
border-top: 1px solid var(--qz-border);
padding: 40px 24px;
text-align: center;
color: var(--qz-text-dim);
font-size: 14px;
}
/* Banner */
.qz-banner {
background: #001E50;
color: rgba(255,255,255,0.55);
text-align: center;
padding: 10px;
font-size: 12px;
}
.qz-banner a { color: rgba(255,255,255,0.8); text-decoration: none; }
@media (max-width: 768px) {
.qz-nav-links { display: none; }
}
</style>
<script type="module" src="https://auth.quixzoom.com/site-auth-snippet.js"></script>
</head>
<body>
<!-- Navbar -->
<nav class="qz-nav" class="navbar">
<div class="navbar-logo"></div>
<div class="navbar-title">DAC7 Rapportering</div>
<div class="navbar-badge">quiXzoom</div>
</nav>
<!-- Main scroll content -->
<div class="scroll-content" id="mainContent">
<!-- Error banner -->
<div class="error-banner" id="errorBanner">
Kunde inte ladda DAC7-data. Kontrollera anslutningen och försök igen.
</div>
<!-- Loading skeleton -->
<div id="loadingState">
<div class="section-label">Årssammanfattning</div>
<div class="card animate-in">
<div style="padding:20px">
<div class="skeleton" style="height:22px;width:60%;margin-bottom:10px"></div>
<div class="skeleton" style="height:16px;width:80%;margin-bottom:6px"></div>
<div class="skeleton" style="height:16px;width:40%"></div>
</div>
</div>
<div class="section-label">Zoomer-status</div>
<div class="card animate-in">
<div style="padding:14px 16px">
<div class="skeleton" style="height:18px;width:70%;margin-bottom:12px"></div>
<div class="skeleton" style="height:18px;width:55%"></div>
</div>
</div>
</div>
<!-- Real content (hidden until loaded) -->
<div id="dataContent" style="display:none">
<!-- Hero summary -->
<div class="hero-card animate-in" id="heroCard">
<div class="hero-tag">EU Direktiv 2021/514 · DAC7</div>
<div class="hero-title" id="heroRevenue">$0.00</div>
<div class="hero-subtitle">Total intäkt att rapportera <span id="heroYear">2026</span></div>
<div class="hero-stats">
<div class="hero-stat">
<div class="hero-stat-value" id="heroSellers">0</div>
<div class="hero-stat-label">Rapportpliktiga Zoomers</div>
</div>
<div class="hero-stat">
<div class="hero-stat-value" id="heroTransactions">0</div>
<div class="hero-stat-label">Totalt antal uppdrag</div>
</div>
</div>
</div>
<!-- Deadline & Status -->
<div class="section-label animate-in">Deadline & Status</div>
<div class="card animate-in">
<div class="deadline-banner" id="deadlineBanner">
<div class="deadline-icon" id="deadlineIcon"></div>
<div class="deadline-info">
<div class="deadline-title">Skatteverket · Deadline</div>
<div class="deadline-sub" id="deadlineDate">31 januari 2027</div>
</div>
<div class="deadline-days" id="deadlineDays">
--
<span>dagar</span>
</div>
</div>
<div class="progress-wrap">
<div class="progress-header">
<span>TIN-verifiering</span>
<span id="progressLabel">0 av 0 verifierade</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progressFill" style="width:0%"></div>
</div>
</div>
<div class="status-row">
<div class="status-pill progress" id="statusPill">
<div class="status-dot"></div>
<span id="statusText">Laddar…</span>
</div>
</div>
</div>
<!-- Export -->
<div class="section-label animate-in">Exportera rapport</div>
<div class="card animate-in">
<div class="export-row">
<button class="export-btn json" onclick="exportReport('json')">
JSON
</button>
<button class="export-btn xml" onclick="exportReport('xml')">
XML (Skatteverket)
</button>
</div>
<div style="padding: 0 16px 14px; font-size: 13px; color: var(--label-secondary); line-height: 1.4;">
XML-filen följer OECD DAC7-schema v1.0 kompatibelt med Skatteverkets inlämningssystem.
</div>
</div>
<!-- Zoomer table -->
<div class="section-label animate-in">Rapportpliktiga Zoomers</div>
<div class="card animate-in" id="sellersList"></div>
<!-- Below threshold -->
<div class="section-label animate-in">Under tröskel (ej rapportpliktiga)</div>
<div class="card animate-in" id="belowThresholdList"></div>
<!-- Footer note -->
<div class="footer-note animate-in">
Tröskelvärden: &gt;30 uppdrag ELLER &gt;2 000 USD per kalenderår (OR-logik).<br>
Källa: EU-direktiv 2021/514 (DAC7) · Art. 8ac DAC.
</div>
</div>
</div>
<script>
const ORG_ID = '8493ba70-1232-4544-91d2-9827fe566fd0';
const YEAR = new Date().getFullYear();
let allSellers = [];
let statusData = null;
// Format helpers
function formatEur(v) {
return new Intl.NumberFormat('sv-SE', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(v);
}
function formatDate(isoStr) {
if (!isoStr) return '—';
return new Intl.DateTimeFormat('sv-SE', { year: 'numeric', month: 'short', day: 'numeric' }).format(new Date(isoStr));
}
function statusLabel(status) {
const map = {
ready_to_submit: 'Redo att skicka',
in_progress: 'Pågår — TIN saknas',
no_reportable_sellers: 'Inga rapportpliktiga',
overdue: 'Försenad',
};
return map[status] || status;
}
function statusClass(status) {
if (status === 'ready_to_submit') return 'ready';
if (status === 'overdue') return 'overdue';
return 'progress';
}
function reasonLabel(reason) {
const map = {
exceeds_both_thresholds: '> 30 uppdrag & > $2 000',
exceeds_transaction_threshold: '> 30 uppdrag',
exceeds_consideration_threshold: '> $2 000',
below_threshold: 'Under tröskel',
};
return map[reason] || reason;
}
// Render sellers
function renderSeller(s, showVerify) {
const tin = s.tin_verified
? `<span class="tin-badge verified"><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> TIN verifierad</span>`
: `<span class="tin-badge unverified"> TIN ej verifierad</span>`;
const verifyBtn = showVerify && !s.tin_verified
? `<button class="verify-btn" onclick="verifySeller('${s.seller_id}')">Verifiera</button>`
: showVerify && s.tin_verified
? `<button class="verify-btn done" disabled><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> Klar</button>`
: '';
const thresholdBadge = s.reportable
? `<span class="threshold-badge over">Rapportpliktig</span>`
: `<span class="threshold-badge under">Under tröskel</span>`;
return `
<div class="list-row" id="seller-${s.seller_id}">
<div class="list-row-icon" style="background:${s.reportable ? 'rgba(0,122,255,.1)' : 'var(--fill2)'}">
${s.reportable ? '' : ''}
</div>
<div class="list-row-body">
<div class="list-row-title">${escHtml(s.name)}</div>
<div class="list-row-sub">${s.transaction_count} uppdrag · ${reasonLabel(s.reportable_reason)}</div>
<div style="margin-top:4px;display:flex;gap:6px;flex-wrap:wrap">
${tin}
${thresholdBadge}
</div>
</div>
<div class="list-row-right">
<div class="list-row-value">${formatEur(s.total_consideration_eur)}</div>
<div class="list-row-label">intäkt</div>
${verifyBtn}
</div>
</div>`;
}
function renderAllSellers(sellers) {
const reportable = sellers.filter(s => s.reportable);
const below = sellers.filter(s => !s.reportable);
const sellersEl = document.getElementById('sellersList');
const belowEl = document.getElementById('belowThresholdList');
if (reportable.length === 0) {
sellersEl.innerHTML = `<div class="empty-state"><div class="empty-icon"></div><div class="empty-title">Inga rapportpliktiga Zoomers</div><div class="empty-sub">Alla under tröskelvärdena</div></div>`;
} else {
sellersEl.innerHTML = reportable.map(s => renderSeller(s, true)).join('');
}
if (below.length === 0) {
belowEl.innerHTML = `<div class="empty-state"><div class="empty-icon">—</div><div class="empty-title">Inga</div><div class="empty-sub">Alla Zoomers är rapportpliktiga</div></div>`;
} else {
belowEl.innerHTML = below.map(s => renderSeller(s, false)).join('');
}
}
// Load data
async function loadData() {
const errorBanner = document.getElementById('errorBanner');
try {
const [statusRes, sellersRes] = await Promise.all([
fetch(`/api/v1/organizations/${ORG_ID}/compliance/dac7/status?year=${YEAR}`, { credentials: 'include' }),
fetch(`/api/v1/organizations/${ORG_ID}/compliance/dac7/sellers?year=${YEAR}`, { credentials: 'include' }),
]);
if (!statusRes.ok || !sellersRes.ok) throw new Error('API error');
statusData = await statusRes.json();
const sellersData = await sellersRes.json();
allSellers = sellersData.sellers || [];
renderStatus(statusData);
renderAllSellers(allSellers);
document.getElementById('loadingState').style.display = 'none';
document.getElementById('dataContent').style.display = 'block';
errorBanner.classList.remove('visible');
} catch (e) {
console.error('DAC7 load error:', e);
document.getElementById('loadingState').style.display = 'none';
errorBanner.classList.add('visible');
}
}
function renderStatus(data) {
// Hero
document.getElementById('heroRevenue').textContent = formatEur(data.total_revenue_eur || 0);
document.getElementById('heroYear').textContent = data.reporting_year || YEAR;
document.getElementById('heroSellers').textContent = data.reportable_count || 0;
document.getElementById('heroTransactions').textContent = (data.total_transactions || 0).toLocaleString('sv-SE');
// Deadline
const days = data.days_to_deadline;
document.getElementById('deadlineDays').innerHTML = `${days}<span>dagar</span>`;
document.getElementById('deadlineDate').textContent = `Deadline: ${data.deadline}`;
if (days < 0) {
document.getElementById('deadlineIcon').textContent = '';
document.getElementById('deadlineDays').style.color = 'var(--qx-red)';
} else if (days < 30) {
document.getElementById('deadlineIcon').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>';
document.getElementById('deadlineDays').style.color = 'var(--qx-orange)';
} else {
document.getElementById('deadlineIcon').textContent = '';
document.getElementById('deadlineDays').style.color = 'var(--qx-blue)';
}
// Progress
const verified = data.verified_count || 0;
const reportable = data.reportable_count || 0;
const pct = reportable > 0 ? Math.round((verified / reportable) * 100) : 100;
document.getElementById('progressFill').style.width = `${pct}%`;
document.getElementById('progressLabel').textContent = `${verified} av ${reportable} verifierade`;
// Status pill
const pill = document.getElementById('statusPill');
pill.className = `status-pill ${statusClass(data.status)}`;
document.getElementById('statusText').textContent = statusLabel(data.status);
}
// Verify seller
async function verifySeller(sellerId) {
const btn = document.querySelector(`#seller-${sellerId} .verify-btn`);
if (btn) { btn.textContent = '…'; btn.disabled = true; }
try {
const res = await fetch(
`/api/v1/organizations/${ORG_ID}/compliance/dac7/sellers/${sellerId}/verify?year=${YEAR}`,
{ method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ note: 'Manuell verifiering via DAC7-rapport' }) }
);
if (!res.ok) throw new Error('Verify failed');
const result = await res.json();
// Update local state
const seller = allSellers.find(s => s.seller_id === sellerId);
if (seller) {
seller.tin_verified = true;
seller.verified_at = result.verified_at;
}
// Re-render sellers and reload status
renderAllSellers(allSellers);
await reloadStatus();
} catch (e) {
console.error('Verify error:', e);
if (btn) { btn.textContent = 'Verifiera'; btn.disabled = false; }
alert('Verifiering misslyckades. Försök igen.');
}
}
async function reloadStatus() {
try {
const res = await fetch(`/api/v1/organizations/${ORG_ID}/compliance/dac7/status?year=${YEAR}`, { credentials: 'include' });
if (!res.ok) return;
statusData = await res.json();
renderStatus(statusData);
} catch (e) { /* ignore */ }
}
// Export
async function exportReport(format) {
try {
if (format === 'xml') {
const res = await fetch(
`/api/v1/organizations/${ORG_ID}/compliance/dac7/report?year=${YEAR}&format=xml`,
{ credentials: 'include' }
);
if (!res.ok) throw new Error('Export failed');
const blob = await res.blob();
downloadBlob(blob, `dac7-quixzoom-${YEAR}.xml`, 'application/xml');
} else {
const res = await fetch(
`/api/v1/organizations/${ORG_ID}/compliance/dac7/report?year=${YEAR}&format=json`,
{ credentials: 'include' }
);
if (!res.ok) throw new Error('Export failed');
const data = await res.json();
// Remove XML payload from JSON download
delete data.xml_payload;
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
downloadBlob(blob, `dac7-quixzoom-${YEAR}.json`, 'application/json');
}
} catch (e) {
console.error('Export error:', e);
alert('Export misslyckades. Försök igen.');
}
}
function downloadBlob(blob, filename, type) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 1000);
}
// Utils
function escHtml(str) {
return String(str || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// Init
loadData();
</script>
<!-- quiXzoom Mina Sidor - Universal Header Component -->
<!-- Injicera detta i <head> på alla quiXzoom-sajter -->
<style>
.qz-auth-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 48px;
background: #0A0A0A;
border-bottom: 1px solid rgba(255,255,255,0.1);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif;
}
.qz-auth-bar .qz-logo {
font-size: 18px;
font-weight: 700;
color: #fff;
text-decoration: none;
letter-spacing: -0.5px;
}
.qz-auth-bar .qz-logo span {
color: #007AFF;
}
.qz-auth-bar .qz-nav {
display: flex;
gap: 8px;
align-items: center;
}
.qz-auth-bar .qz-btn {
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
text-decoration: none;
}
.qz-auth-bar .qz-btn-primary {
background: #007AFF;
color: white;
}
.qz-auth-bar .qz-btn-primary:hover {
background: #0056CC;
}
.qz-auth-bar .qz-btn-ghost {
background: transparent;
color: rgba(255,255,255,0.8);
}
.qz-auth-bar .qz-btn-ghost:hover {
background: rgba(255,255,255,0.1);
color: white;
}
.qz-auth-bar .qz-user {
display: flex;
align-items: center;
gap: 8px;
color: white;
font-size: 14px;
}
.qz-auth-bar .qz-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #007AFF;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
}
.qz-auth-bar .qz-dropdown {
position: relative;
}
.qz-auth-bar .qz-dropdown-menu {
position: absolute;
top: 40px;
right: 0;
background: #1C1C1E;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
padding: 8px;
min-width: 200px;
display: none;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
.qz-auth-bar .qz-dropdown-menu.active {
display: block;
}
.qz-auth-bar .qz-dropdown-item {
padding: 10px 12px;
border-radius: 8px;
color: white;
text-decoration: none;
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
transition: background 0.2s;
}
.qz-auth-bar .qz-dropdown-item:hover {
background: rgba(255,255,255,0.1);
}
.qz-auth-bar .qz-dropdown-divider {
height: 1px;
background: rgba(255,255,255,0.1);
margin: 8px 0;
}
.qz-auth-bar .qz-balance {
background: rgba(0,122,255,0.15);
color: #007AFF;
padding: 4px 10px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
}
@media (max-width: 640px) {
.qz-auth-bar .qz-nav .qz-btn-ghost:not(.qz-user) {
display: none;
}
}
/* QUIXZOOM STANDARD — Dark Theme */
:root {
--qz-bg: #0a0a0a;
--qz-surface: #111111;
--qz-surface-2: #1a1a1a;
--qz-border: rgba(255,255,255,0.08);
--qz-text: #e8e8e8;
--qz-text-dim: #a0a0a0;
--qz-text-muted: #6b6b6b;
--qz-blue: #007AFF;
--qz-blue-dark: #0056CC;
--qz-radius: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
background: var(--qz-bg);
color: var(--qz-text);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
/* Navigation */
.qz-nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px; height: 56px;
background: rgba(255,255,255,0.92);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--qz-border);
}
.qz-nav-logo { font-size: 18px; font-weight: 700; color: #fff; text-decoration: none; }
.qz-nav-links { display: flex; gap: 24px; list-style: none; }
.qz-nav-links a { font-size: 14px; color: var(--qz-text-dim); text-decoration: none; transition: color 0.2s; }
.qz-nav-links a:hover { color: #fff; }
/* Buttons */
.qz-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 12px 24px; border-radius: var(--qz-radius);
font-size: 15px; font-weight: 600; text-decoration: none;
transition: all 0.2s; cursor: pointer; border: none;
}
.qz-btn-primary {
background: var(--qz-blue); color: #fff;
}
.qz-btn-primary:hover {
background: var(--qz-blue-dark); transform: translateY(-1px);
}
.qz-btn-ghost {
background: transparent; color: var(--qz-text);
border: 1px solid var(--qz-border);
}
.qz-btn-ghost:hover {
border-color: var(--qz-blue); color: var(--qz-blue);
}
/* Cards */
.qz-card {
background: var(--qz-surface);
border: 1px solid var(--qz-border);
border-radius: var(--qz-radius);
padding: 24px;
transition: transform 0.2s, border-color 0.2s;
}
.qz-card:hover {
transform: translateY(-2px);
border-color: rgba(0,122,255,0.3);
}
/* Footer */
.qz-footer {
border-top: 1px solid var(--qz-border);
padding: 40px 24px;
text-align: center;
color: var(--qz-text-dim);
font-size: 14px;
}
/* Banner */
.qz-banner {
background: #001E50;
color: rgba(255,255,255,0.55);
text-align: center;
padding: 10px;
font-size: 12px;
}
.qz-banner a { color: rgba(255,255,255,0.8); text-decoration: none; }
@media (max-width: 768px) {
.qz-nav-links { display: none; }
}
</style>
<!--
quiXzoom Universal Auth Snippet v2.0
Add this to ALL quiXzoom sites for seamless cross-domain login
Features:
- Silent login (auto-refresh tokens)
- Cross-domain cookie sync
- Real-time auth state updates
- Mobile app compatible
-->
<!-- Auth bar styles -->
<style>
#qz-auth-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 48px;
background: #0A0A0A;
border-bottom: 1px solid rgba(255,255,255,0.1);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif;
}
#qz-auth-bar .qz-logo {
font-size: 18px;
font-weight: 700;
color: #fff;
text-decoration: none;
letter-spacing: -0.5px;
}
#qz-auth-bar .qz-logo span { color: #007AFF; }
#qz-auth-bar .qz-nav {
display: flex;
gap: 8px;
align-items: center;
}
#qz-auth-bar .qz-btn {
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
text-decoration: none;
}
#qz-auth-bar .qz-btn-primary {
background: #007AFF;
color: white;
}
#qz-auth-bar .qz-btn-primary:hover { background: #0056CC; }
#qz-auth-bar .qz-btn-ghost {
background: transparent;
color: rgba(255,255,255,0.8);
}
#qz-auth-bar .qz-btn-ghost:hover {
background: rgba(255,255,255,0.1);
color: white;
}
#qz-auth-bar .qz-user-menu {
position: relative;
}
#qz-auth-bar .qz-dropdown {
position: absolute;
top: 40px;
right: 0;
background: #1C1C1E;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
padding: 8px;
min-width: 220px;
display: none;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
#qz-auth-bar .qz-dropdown.active { display: block; }
#qz-auth-bar .qz-dropdown-item {
padding: 10px 12px;
border-radius: 8px;
color: white;
text-decoration: none;
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
transition: background 0.2s;
}
#qz-auth-bar .qz-dropdown-item:hover { background: rgba(255,255,255,0.1); }
#qz-auth-bar .qz-dropdown-divider {
height: 1px;
background: rgba(255,255,255,0.1);
margin: 8px 0;
}
#qz-auth-bar .qz-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #007AFF;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
color: white;
}
#qz-auth-bar .qz-balance {
background: rgba(0,122,255,0.15);
color: #007AFF;
padding: 4px 10px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
}
/* QUIXZOOM STANDARD — Dark Theme */
:root {
--qz-bg: #0a0a0a;
--qz-surface: #111111;
--qz-surface-2: #1a1a1a;
--qz-border: rgba(255,255,255,0.08);
--qz-text: #e8e8e8;
--qz-text-dim: #a0a0a0;
--qz-text-muted: #6b6b6b;
--qz-blue: #007AFF;
--qz-blue-dark: #0056CC;
--qz-radius: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
background: var(--qz-bg);
color: var(--qz-text);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
/* Navigation */
.qz-nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px; height: 56px;
background: rgba(255,255,255,0.92);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--qz-border);
}
.qz-nav-logo { font-size: 18px; font-weight: 700; color: #fff; text-decoration: none; }
.qz-nav-links { display: flex; gap: 24px; list-style: none; }
.qz-nav-links a { font-size: 14px; color: var(--qz-text-dim); text-decoration: none; transition: color 0.2s; }
.qz-nav-links a:hover { color: #fff; }
/* Buttons */
.qz-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 12px 24px; border-radius: var(--qz-radius);
font-size: 15px; font-weight: 600; text-decoration: none;
transition: all 0.2s; cursor: pointer; border: none;
}
.qz-btn-primary {
background: var(--qz-blue); color: #fff;
}
.qz-btn-primary:hover {
background: var(--qz-blue-dark); transform: translateY(-1px);
}
.qz-btn-ghost {
background: transparent; color: var(--qz-text);
border: 1px solid var(--qz-border);
}
.qz-btn-ghost:hover {
border-color: var(--qz-blue); color: var(--qz-blue);
}
/* Cards */
.qz-card {
background: var(--qz-surface);
border: 1px solid var(--qz-border);
border-radius: var(--qz-radius);
padding: 24px;
transition: transform 0.2s, border-color 0.2s;
}
.qz-card:hover {
transform: translateY(-2px);
border-color: rgba(0,122,255,0.3);
}
/* Footer */
.qz-footer {
border-top: 1px solid var(--qz-border);
padding: 40px 24px;
text-align: center;
color: var(--qz-text-dim);
font-size: 14px;
}
/* Banner */
.qz-banner {
background: #001E50;
color: rgba(255,255,255,0.55);
text-align: center;
padding: 10px;
font-size: 12px;
}
.qz-banner a { color: rgba(255,255,255,0.8); text-decoration: none; }
@media (max-width: 768px) {
.qz-nav-links { display: none; }
}
</style>
<!-- Auth bar HTML -->
<div id="qz-auth-bar">
<a href="https://quixzoom.com" class="qz-logo">qui<span>X</span>zoom</a>
<div class="qz-nav">
<!-- Guest view -->
<div id="qz-guest-view">
<a href="https://app.quixzoom.com/login" class="qz-btn qz-btn-ghost">Logga in</a>
<a href="https://app.quixzoom.com/register" class="qz-btn qz-btn-primary">Bli Zoomer</a>
</div>
<!-- Authenticated view -->
<div id="qz-user-view" style="display:none;">
<span class="qz-balance" id="qz-balance">$0.00</span>
<div class="qz-user-menu">
<button class="qz-btn qz-btn-ghost" onclick="qzToggleMenu()" style="display:flex;align-items:center;gap:8px;">
<div class="qz-avatar" id="qz-avatar">?</div>
<span id="qz-user-name" style="color:white;">User</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2.5 4.5L6 8L9.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="qz-dropdown" id="qz-dropdown">
<a href="https://app.quixzoom.com/dashboard" class="qz-dropdown-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
<rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/>
</svg>
Dashboard
</a>
<a href="https://app.quixzoom.com/wallet" class="qz-dropdown-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/>
<path d="M18 12a2 2 0 0 0 0 4h4v-4h-4z"/>
</svg>
Plånbok
</a>
<a href="https://app.quixzoom.com/missions" class="qz-dropdown-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>
</svg>
Mina uppdrag
</a>
<div class="qz-dropdown-divider"></div>
<a href="https://app.quixzoom.com/settings" class="qz-dropdown-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.67 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.67 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.67a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
Inställningar
</a>
<a href="#" onclick="qzLogout(); return false;" class="qz-dropdown-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
Logga ut
</a>
</div>
</div>
</div>
</div>
</div>
<!-- SSO Client Script -->
<script>
(function() {
const SSO_BASE = 'https://auth.quixzoom.com';
// Check auth status on load
async function qzCheckAuth() {
try {
const res = await fetch(SSO_BASE + '/auth/silent', {
method: 'GET',
credentials: 'include',
});
if (res.ok) {
const data = await res.json();
if (data.authenticated) {
qzShowUser(data.user);
return;
}
}
// Try check endpoint
const checkRes = await fetch(SSO_BASE + '/auth/check', {
credentials: 'include',
});
if (checkRes.ok) {
const checkData = await checkRes.json();
if (checkData.authenticated && checkData.user) {
qzShowUser(checkData.user);
return;
}
}
qzShowGuest();
} catch (e) {
console.log('[QZAuth] Check failed, showing guest');
qzShowGuest();
}
}
function qzShowUser(user) {
document.getElementById('qz-guest-view').style.display = 'none';
document.getElementById('qz-user-view').style.display = 'flex';
document.getElementById('qz-user-name').textContent = user.name || user.email;
document.getElementById('qz-avatar').textContent = (user.name || user.email)[0].toUpperCase();
// Dispatch event for other scripts
window.dispatchEvent(new CustomEvent('qz-auth-change', {
detail: { authenticated: true, user }
}));
}
function qzShowGuest() {
document.getElementById('qz-guest-view').style.display = 'flex';
document.getElementById('qz-user-view').style.display = 'none';
window.dispatchEvent(new CustomEvent('qz-auth-change', {
detail: { authenticated: false }
}));
}
window.qzToggleMenu = function() {
document.getElementById('qz-dropdown').classList.toggle('active');
};
window.qzLogout = async function() {
try {
await fetch(SSO_BASE + '/auth/logout', {
method: 'POST',
credentials: 'include',
});
} catch (e) {}
// Clear all cookies
document.cookie = 'qz_access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;';
document.cookie = 'qz_refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;';
document.cookie = 'qz_auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;';
qzShowGuest();
window.location.reload();
};
// Close dropdown on outside click
document.addEventListener('click', function(e) {
if (!e.target.closest('.qz-user-menu')) {
document.getElementById('qz-dropdown').classList.remove('active');
}
});
// Check auth on page load
qzCheckAuth();
// Re-check auth periodically (every 5 minutes)
setInterval(qzCheckAuth, 5 * 60 * 1000);
// Listen for storage events (login from other tabs)
window.addEventListener('storage', function(e) {
if (e.key === 'qz_auth_event') {
const event = JSON.parse(e.newValue || '{}');
if (event.type === 'login') {
qzCheckAuth();
} else if (event.type === 'logout') {
qzShowGuest();
}
}
});
})();
</script>
</body>
</html>