1a12fb870b
- 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
1718 lines
48 KiB
HTML
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: >30 uppdrag ELLER >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,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
}
|
|
|
|
// 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>
|