Files
boc/quixzoom-landing-fixed/dac7-report.html
T
Bernt 6989a98d75 feat: Passwordless cross-device authentication
- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
2026-07-07 07:11:50 +00:00

909 lines
26 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; }
}
</style>
<style id="wcag-contrast-enforcement">
/* AAMOS GLOBAL CONTRAST ENFORCEMENT — WCAG AA (4.5:1 minimum) */
/* Regel: Ingen text under 70% opacity. Aldrig. */
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
/* Fix common low-contrast patterns */
[style*="rgba(60,60,67,0.4)"] { color: rgba(60,60,67,0.65) !important; }
[style*="rgba(60,60,67,0.3)"] { color: rgba(60,60,67,0.65) !important; }
[style*="rgba(255,255,255,0.4)"] { color: rgba(255,255,255,0.72) !important; }
[style*="rgba(255,255,255,0.3)"] { color: rgba(255,255,255,0.72) !important; }
[style*="opacity: 0.4"] { opacity: 0.72 !important; }
[style*="opacity: 0.3"] { opacity: 0.72 !important; }
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
.card { padding: 1.5rem !important; }
.grid { grid-template-columns: 1fr !important; }
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
}
</style>
</head>
<body>
<!-- Navbar -->
<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 EUR 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: 'EUR', 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>
</body>
</html>