6989a98d75
- Arkitektur: docs/auth/passwordless-architecture.md - Backend: iom/quixzoom-auth-service/ (FastAPI + Redis) - Webb: quixzoom-market-pages/se/login/ (QR-kod + polling) - App: iom/quixzoom-app/src/features/auth/ (push + deep links) Flöde: QR-kod → app-godkännande → webb-inloggad
489 lines
29 KiB
HTML
489 lines
29 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, viewport-fit=cover">
|
||
<meta name="color-scheme" content="light">
|
||
<meta name="apple-native-compliance" content="pass">
|
||
<title>quiXzoom — Kunddashboard</title>
|
||
<link rel="stylesheet" >
|
||
<link rel="stylesheet" >
|
||
<script defer></script>
|
||
<style>
|
||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||
html{-webkit-text-size-adjust:100%;color-scheme:light}
|
||
body{font-family:-apple-system,'SF Pro Text',system-ui,sans-serif;background:#F2F2F7;color:#000;-webkit-font-smoothing:antialiased;min-height:100dvh;padding-bottom:calc(83px + env(safe-area-inset-bottom,16px))}
|
||
|
||
/* Nav */
|
||
.nav-bar{position:sticky;top:0;z-index:200;padding-top:env(safe-area-inset-top,0px);background:rgba(249,249,249,.94);backdrop-filter:blur(40px) saturate(180%);-webkit-backdrop-filter:blur(40px) saturate(180%);border-bottom:.33px solid rgba(60,60,67,.29)}
|
||
.nav-inner{height:44px;display:flex;align-items:center;padding:0 16px;gap:12px}
|
||
.nav-title{flex:1;text-align:center;font-size:17px;font-weight:600;letter-spacing:-.4px}
|
||
.nav-logo{font-size:20px;font-weight:900;background:linear-gradient(135deg,#FF6B35,#F7C948);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.5px}
|
||
.nav-btn-icon{width:32px;height:32px;border-radius: var(--radius-lg);background:#007AFF;color:#fff;border:none;font-size:14px;display:flex;align-items:center;justify-content:center;cursor:pointer}
|
||
|
||
/* Customer switcher */
|
||
.customer-switcher{background:#fff;border-bottom:.33px solid rgba(60,60,67,.12);padding:10px 16px;display:flex;gap:8px;overflow-x:auto;scrollbar-width:none}
|
||
.customer-switcher::-webkit-scrollbar{display:none}
|
||
.cust-chip{padding:7px 14px;border-radius: var(--radius-xl);font-size:13px;font-weight:600;cursor:pointer;border:none;background:#F2F2F7;color:#000;white-space:nowrap;transition:all .15s;-webkit-tap-highlight-color:transparent}
|
||
.cust-chip.active{background:#007AFF;color:#fff}
|
||
|
||
/* Large title */
|
||
.large-title-area{padding:12px 16px 8px}
|
||
.large-title{font-size:34px;font-weight:700;letter-spacing:.37px;line-height:1.1}
|
||
.large-sub{font-size:15px;color:#666666;margin-top:3px}
|
||
|
||
/* Tab bar */
|
||
.tab-bar{position:fixed;bottom:0;left:0;right:0;z-index:200;padding-bottom:env(safe-area-inset-bottom,0px);background:rgba(249,249,249,.94);backdrop-filter:blur(40px) saturate(180%);-webkit-backdrop-filter:blur(40px) saturate(180%);border-top:.33px solid rgba(60,60,67,.29);display:flex}
|
||
.tab-item{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:8px 4px 4px;cursor:pointer;gap:2px;-webkit-tap-highlight-color:transparent}
|
||
.tab-icon{font-size:22px;line-height:1}
|
||
.tab-label{font-size:10px;font-weight:500;color:#666666}
|
||
.tab-item.active .tab-label{color:#007AFF}
|
||
.tab-item:not(.active) .tab-icon{opacity:.5}
|
||
|
||
/* Content */
|
||
.content{display:none;padding-bottom:8px}
|
||
.content.active{display:block}
|
||
|
||
/* KPI strip */
|
||
.kpi-strip{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;padding:12px 16px 8px}
|
||
.kpi-box{background:#fff;border-radius: var(--radius-md);padding:14px 12px;text-align:center;box-shadow:0 1px 0 rgba(0,0,0,.08)}
|
||
.kpi-val{font-size:20px;font-weight:700;letter-spacing:-.5px}
|
||
.kpi-val-accent{color:#FF6B35}
|
||
.kpi-lbl{font-size:10px;color:#666666;font-weight:500;margin-top:2px}
|
||
|
||
/* Section */
|
||
.section{margin:8px 0}
|
||
.section-header{font-size:13px;font-weight:600;color:#555555;text-transform:uppercase;padding:0 32px 6px}
|
||
|
||
/* Card list */
|
||
.card-list{background:#fff;border-radius: var(--radius-md);margin:0 16px;overflow:hidden;box-shadow:0 1px 0 rgba(0,0,0,.08)}
|
||
.card-item{display:flex;align-items:center;gap:12px;padding:13px 16px;border-bottom:.33px solid rgba(60,60,67,.12);-webkit-tap-highlight-color:transparent;transition:background .1s}
|
||
.card-item:last-child{border-bottom:none}
|
||
.card-item:active{background:#F2F2F7}
|
||
.card-icon{width:40px;height:40px;border-radius: var(--radius-md);display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0}
|
||
.ci-blue{background:#EBF5FF}.ci-green{background:#E8F8EE}.ci-orange{background:#FFF3E0}.ci-red{background:#FFE8E6}.ci-purple{background:#F4EEFF}.ci-gray{background:#F2F2F7}
|
||
.card-body{flex:1;min-width:0}
|
||
.card-title{font-size:15px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.card-sub{font-size:13px;color:#666666;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.card-right{display:flex;align-items:center;gap:6px;flex-shrink:0}
|
||
.chevron{color:#C7C7CC;font-size:16px}
|
||
|
||
/* Badges */
|
||
.badge{display:inline-flex;align-items:center;padding:3px 8px;border-radius: var(--radius-sm);font-size:12px;font-weight:600;white-space:nowrap}
|
||
.bg-green{background:#E8F8EE;color:#1D7A3F}
|
||
.bg-orange{background:#FFF3E0;color:#C07000}
|
||
.bg-blue{background:#EBF5FF;color:#0051A8}
|
||
.bg-red{background:#FFE8E6;color:#C0392B}
|
||
.bg-gray{background:#F2F2F7;color:#555555}
|
||
.bg-purple{background:#F4EEFF;color:#6B2FA0}
|
||
|
||
/* Star rating */
|
||
.stars{color:#FFD60A;font-size:13px}
|
||
|
||
/* Favorite zoomer card */
|
||
.fav-card{background:#fff;border-radius: var(--radius-lg);margin:0 16px;padding:16px;display:flex;align-items:center;gap:14px;box-shadow:0 1px 0 rgba(0,0,0,.08);margin-bottom:8px}
|
||
.fav-avatar{width:52px;height:52px;border-radius: 26px;background:linear-gradient(135deg,#FF6B35,#F7C948);display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0}
|
||
.fav-name{font-size:16px;font-weight:700}
|
||
.fav-spec{font-size:13px;color:#666666}
|
||
.fav-meta{font-size:12px;color:#666666;margin-top:4px}
|
||
.fav-actions{margin-left:auto;display:flex;gap:8px}
|
||
|
||
/* Chart bar */
|
||
.bar-chart-wrap{padding:16px}
|
||
.bar-chart-title{font-size:15px;font-weight:600;margin-bottom:14px}
|
||
.bar-row{display:flex;align-items:center;gap:10px;margin-bottom:10px}
|
||
.bar-label{font-size:12px;color:#555555;width:80px;flex-shrink:0}
|
||
.bar-track{flex:1;height:8px;background:#E9E9EB;border-radius: var(--radius-sm);overflow:hidden}
|
||
.bar-fill{height:8px;border-radius: var(--radius-sm);background:linear-gradient(90deg,#FF6B35,#F7C948);transition:width .7s ease}
|
||
.bar-val{font-size:12px;color:#555555;width:80px;text-align:right;flex-shrink:0}
|
||
|
||
/* Book modal */
|
||
.modal-overlay{position:fixed;inset:0;background: rgba(242,242,247,.4);z-index:300;display:none;align-items:flex-end}
|
||
.modal-overlay.open{display:flex}
|
||
.modal{background:#fff;border-radius: var(--radius-xl) 20px 0 0;padding:20px 20px calc(20px + env(safe-area-inset-bottom,0px));width:100%;max-height:90dvh;overflow-y:auto}
|
||
.modal-handle{width:36px;height:4px;border-radius: var(--radius-sm);background:#C7C7CC;margin:0 auto 16px}
|
||
.modal-title{font-size:20px;font-weight:700;margin-bottom:4px}
|
||
.modal-sub{font-size:14px;color:#666666;margin-bottom:20px}
|
||
.form-group{margin-bottom:14px}
|
||
.form-label{font-size:13px;font-weight:600;color:#555555;margin-bottom:6px;display:block}
|
||
.form-input,.form-select{width:100%;background:#F2F2F7;border:none;border-radius: var(--radius-md);padding:12px 14px;color:#000;font-size:15px;font-family:inherit;outline:none}
|
||
.btn-row{display:flex;flex-direction:column;gap:10px;margin-top:20px}
|
||
.btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:13px;border-radius: var(--radius-md);font-size:17px;font-weight:600;cursor:pointer;border:none;-webkit-tap-highlight-color:transparent;transition:opacity .15s}
|
||
.btn:active{opacity:.7}
|
||
.btn-orange{background:linear-gradient(135deg,#FF6B35,#FF9500);color:#fff}
|
||
.btn-gray{background:#F2F2F7;color:#000}
|
||
|
||
/* Empty */
|
||
.empty{padding:32px;text-align:center;color:#666666}
|
||
.empty-icon{font-size:36px;margin-bottom:8px}
|
||
|
||
/* Toast */
|
||
.toast{position:fixed;bottom:calc(83px + 12px + env(safe-area-inset-bottom,0px));left:50%;transform:translateX(-50%) translateY(60px);background:#323232;color:#fff;padding:10px 18px;border-radius: var(--radius-md);font-size:14px;font-weight:600;z-index:999;transition:transform .3s;white-space:nowrap}
|
||
.toast.show{transform:translateX(-50%) translateY(0)}
|
||
@media (max-width: 640px) {
|
||
.hero h1 { font-size: 2rem !important; }
|
||
.hero p { font-size: 1rem !important; }
|
||
.card { padding: 1.5rem !important; }
|
||
.grid { grid-template-columns: 1fr !important; }
|
||
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
|
||
}
|
||
</style>
|
||
|
||
<style id="wcag-contrast-enforcement">
|
||
/* AAMOS GLOBAL CONTRAST ENFORCEMENT — WCAG AA (4.5:1 minimum) */
|
||
/* Regel: Ingen text under 70% opacity. Aldrig. */
|
||
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
|
||
/* Fix common low-contrast patterns */
|
||
[style*="rgba(60,60,67,0.4)"] { color: rgba(60,60,67,0.65) !important; }
|
||
[style*="rgba(60,60,67,0.3)"] { color: rgba(60,60,67,0.65) !important; }
|
||
[style*="rgba(255,255,255,0.4)"] { color: rgba(255,255,255,0.72) !important; }
|
||
[style*="rgba(255,255,255,0.3)"] { color: rgba(255,255,255,0.72) !important; }
|
||
[style*="opacity: 0.4"] { opacity: 0.72 !important; }
|
||
[style*="opacity: 0.3"] { opacity: 0.72 !important; }
|
||
@media (max-width: 640px) {
|
||
.hero h1 { font-size: 2rem !important; }
|
||
.hero p { font-size: 1rem !important; }
|
||
.card { padding: 1.5rem !important; }
|
||
.grid { grid-template-columns: 1fr !important; }
|
||
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- Nav -->
|
||
<div class="nav-bar">
|
||
<div class="nav-inner">
|
||
<div class="nav-logo">quiX</div>
|
||
<div class="nav-title" id="nav-title">Kunddashboard</div>
|
||
<button class="btn btn-orange" style="padding:7px 14px;font-size:14px;font-weight:700;border-radius: var(--radius-md);border:none;color:#fff;background:linear-gradient(135deg,#FF6B35,#FF9500);cursor:pointer" onclick="openBookModal()">+ Boka</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Customer switcher -->
|
||
<div class="customer-switcher" id="customer-switcher"></div>
|
||
|
||
<!-- Large title -->
|
||
<div class="large-title-area">
|
||
<div class="large-title" id="large-title">Laddar…</div>
|
||
<div class="large-sub" id="large-sub"></div>
|
||
</div>
|
||
|
||
<!-- DASHBOARD -->
|
||
<div class="content active" id="tab-dashboard">
|
||
<div class="kpi-strip" id="kpi-strip"></div>
|
||
<div style="height:8px"></div>
|
||
<div class="section">
|
||
<div class="section-header">Aktiva uppdrag</div>
|
||
<div class="card-list" id="active-missions"></div>
|
||
</div>
|
||
<div style="height:8px"></div>
|
||
<div class="section">
|
||
<div class="section-header">Senaste uppdrag</div>
|
||
<div class="card-list" id="recent-missions"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- HISTORY -->
|
||
<div class="content" id="tab-history">
|
||
<div style="height:12px"></div>
|
||
<div style="background:#fff;border-radius: var(--radius-md);margin:0 16px" id="spending-chart"></div>
|
||
<div style="height:12px"></div>
|
||
<div class="section">
|
||
<div class="section-header">Alla uppdrag</div>
|
||
<div class="card-list" id="all-missions"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- BILLING -->
|
||
<div class="content" id="tab-billing">
|
||
<div style="height:12px"></div>
|
||
<div class="section">
|
||
<div class="section-header">Fakturor</div>
|
||
<div class="card-list" id="invoices-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- FAVORITES -->
|
||
<div class="content" id="tab-favorites">
|
||
<div style="height:12px"></div>
|
||
<div style="padding:0 16px 12px;display:flex;justify-content:flex-end">
|
||
<button class="btn btn-orange" style="font-size:14px;padding:8px 16px;border-radius: var(--radius-md);border:none;color:#fff;background:linear-gradient(135deg,#FF6B35,#FF9500);cursor:pointer" onclick="openAddFavModal()">+ Favorit</button>
|
||
</div>
|
||
<div id="favs-list"></div>
|
||
</div>
|
||
|
||
<!-- Tab bar -->
|
||
<nav class="tab-bar">
|
||
<div class="tab-item active" onclick="showTab('dashboard',this,'Hem')"><div class="tab-icon"></div><div class="tab-label">Hem</div></div>
|
||
<div class="tab-item" onclick="showTab('history',this,'Historik')"><div class="tab-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg></div><div class="tab-label">Historik</div></div>
|
||
<div class="tab-item" onclick="showTab('billing',this,'Fakturering')"><div class="tab-icon"></div><div class="tab-label">Fakturering</div></div>
|
||
<div class="tab-item" onclick="showTab('favorites',this,'Favoriter')"><div class="tab-icon"></div><div class="tab-label">Favoriter</div></div>
|
||
</nav>
|
||
|
||
<!-- Book modal -->
|
||
<div class="modal-overlay" id="book-modal">
|
||
<div class="modal">
|
||
<div class="modal-handle"></div>
|
||
<div class="modal-title">Boka uppdrag</div>
|
||
<div class="modal-sub" id="book-sub">Beskriv ditt uppdrag</div>
|
||
<div class="form-group"><label class="form-label">Titel</label><input class="form-input" id="bk-title" placeholder="Vad behöver du hjälp med?"></div>
|
||
<div class="form-group"><label class="form-label">Typ</label><select class="form-select" id="bk-type"><option value="photography">Fotografi </option><option value="video">Video </option><option value="drone">Drönare <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg></option></select></div>
|
||
<div class="form-group"><label class="form-label">Startdatum & tid</label><input class="form-input" type="datetime-local" id="bk-start"></div>
|
||
<div class="form-group"><label class="form-label">Duration (timmar)</label><input class="form-input" type="number" id="bk-duration" value="2" min="1"></div>
|
||
<div class="form-group"><label class="form-label">Plats</label><input class="form-input" id="bk-location" placeholder="Var ska uppdraget utföras?"></div>
|
||
<div class="form-group"><label class="form-label">Budget (SEK)</label><input class="form-input" type="number" id="bk-budget" placeholder="2500"></div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-orange" onclick="submitBook()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> Boka nu</button>
|
||
<button class="btn btn-gray" onclick="closeModal('book-modal')">Avbryt</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Add favorite modal -->
|
||
<div class="modal-overlay" id="fav-modal">
|
||
<div class="modal">
|
||
<div class="modal-handle"></div>
|
||
<div class="modal-title">Lägg till favorit</div>
|
||
<div class="modal-sub">Lägg till en zoomer som favorit</div>
|
||
<div class="form-group"><label class="form-label">Zoomer ID</label><input class="form-input" id="fav-id" placeholder="t.ex. zoomer-xyz"></div>
|
||
<div class="form-group"><label class="form-label">Namn</label><input class="form-input" id="fav-name" placeholder="Förnamn E."></div>
|
||
<div class="form-group"><label class="form-label">Specialitet</label><select class="form-select" id="fav-spec"><option>Photography</option><option>Video</option><option>Drone</option><option>General</option></select></div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-orange" onclick="submitFav()"> Lägg till</button>
|
||
<button class="btn btn-gray" onclick="closeModal('fav-modal')">Avbryt</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
// State
|
||
const CUSTOMERS = ['cust-101','cust-102','cust-103'];
|
||
let activeCustomer = 'cust-101';
|
||
let customerCache = {};
|
||
let missionCache = {};
|
||
let spendingCache = {};
|
||
let invoiceCache = {};
|
||
let favCache = {};
|
||
|
||
// Utils
|
||
function fmtMoney(n){return new Intl.NumberFormat('sv-SE').format(n)+' kr'}
|
||
function fmtDate(s){if(!s)return'—';return new Date(s).toLocaleDateString('sv-SE',{day:'2-digit',month:'short'})}
|
||
function fmtDateLong(s){if(!s)return'—';return new Date(s).toLocaleDateString('sv-SE',{day:'2-digit',month:'long',year:'numeric'})}
|
||
function toast(msg){const el=document.getElementById('toast');el.textContent=msg;el.classList.add('show');setTimeout(()=>el.classList.remove('show'),2500)}
|
||
function statusBadge(s){return{completed:'<span class="badge bg-green"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Klar</span>',active:'<span class="badge bg-blue"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Aktiv</span>',open:'<span class="badge bg-orange"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="#666" stroke-width="2"/><path d="M11 11L14 14" stroke="#666" stroke-width="2" stroke-linecap="round"/></svg> Öppen</span>',cancelled:'<span class="badge bg-red"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 4L12 12M12 4L4 12" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Avbruten</span>'}[s]||`<span class="badge bg-gray">${s}</span>`}
|
||
function missionIcon(t){return{photography:'',video:'',drone:'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg>'}[t]||'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg>'}
|
||
function missionIconClass(s){return{completed:'ci-green',active:'ci-blue',open:'ci-orange',cancelled:'ci-red'}[s]||'ci-gray'}
|
||
function stars(r){if(!r)return'<span style="color:#666666;font-size:12px">Väntar</span>';return'<span class="stars">'+(''.repeat(Math.round(r))+''.repeat(5-Math.round(r)))+'</span><span style="font-size:12px;color:#666666;margin-left:4px">'+r+'</span>'}
|
||
function invStatusBadge(s){return s==='paid'?'<span class="badge bg-green">Betald</span>':'<span class="badge bg-orange">Obetald</span>'}
|
||
function closeModal(id){document.getElementById(id).classList.remove('open')}
|
||
document.querySelectorAll('.modal-overlay').forEach(m=>m.addEventListener('click',e=>{if(e.target===m)m.classList.remove('open')}));
|
||
|
||
// Tab
|
||
function showTab(id,el,title){
|
||
document.querySelectorAll('.content').forEach(c=>c.classList.remove('active'));
|
||
document.querySelectorAll('.tab-item').forEach(t=>t.classList.remove('active'));
|
||
document.getElementById('tab-'+id).classList.add('active');
|
||
el.classList.add('active');
|
||
document.getElementById('nav-title').textContent=title;
|
||
if(id==='history')renderHistory();
|
||
if(id==='billing')renderBilling();
|
||
if(id==='favorites')renderFavorites();
|
||
}
|
||
|
||
// Customer switcher
|
||
function renderSwitcher(){
|
||
const c = Object.values(customerCache);
|
||
document.getElementById('customer-switcher').innerHTML = c.map(cust=>`
|
||
<button class="cust-chip${cust.id===activeCustomer?' active':''}" onclick="switchCustomer('${cust.id}')">${cust.name.split(' ').slice(0,2).join(' ')}</button>
|
||
`).join('');
|
||
}
|
||
|
||
async function switchCustomer(id){
|
||
activeCustomer=id;
|
||
renderSwitcher();
|
||
await loadCustomerData(id);
|
||
renderDashboard();
|
||
}
|
||
|
||
// Data loading
|
||
async function loadAll(){
|
||
// Load all customers
|
||
const custR=await fetch('https://api.quixzoom.com/api/qz/customers').then(r=>r.json()).catch(()=>({ok:false}));
|
||
if(custR.ok){
|
||
custR.items.forEach(c=>customerCache[c.id]=c);
|
||
renderSwitcher();
|
||
}
|
||
await loadCustomerData(activeCustomer);
|
||
renderDashboard();
|
||
}
|
||
|
||
async function loadCustomerData(id){
|
||
const [mR,sR,iR,fR]=await Promise.all([
|
||
fetch(`/api/quixzoom/customers/${id}/missions`).then(r=>r.json()).catch(()=>({ok:false})),
|
||
fetch(`/api/quixzoom/customers/${id}/spending`).then(r=>r.json()).catch(()=>({ok:false})),
|
||
fetch(`/api/quixzoom/customers/${id}/invoices`).then(r=>r.json()).catch(()=>({ok:false})),
|
||
fetch(`/api/quixzoom/customers/${id}/favorites`).then(r=>r.json()).catch(()=>({ok:false})),
|
||
]);
|
||
if(mR.ok)missionCache[id]=mR.items||[];
|
||
if(sR.ok)spendingCache[id]=sR.spending;
|
||
if(iR.ok)invoiceCache[id]=iR.items||[];
|
||
if(fR.ok)favCache[id]=fR.items||[];
|
||
}
|
||
|
||
// Dashboard
|
||
function renderDashboard(){
|
||
const cust=customerCache[activeCustomer];
|
||
const missions=missionCache[activeCustomer]||[];
|
||
const spending=spendingCache[activeCustomer]||{};
|
||
if(cust){
|
||
document.getElementById('large-title').textContent=cust.name;
|
||
document.getElementById('large-sub').textContent=`${cust.plan.toUpperCase()} · ${cust.contact}`;
|
||
document.getElementById('book-sub').textContent=`Bokar åt ${cust.name}`;
|
||
}
|
||
// KPIs
|
||
const active=missions.filter(m=>m.status==='active'||m.status==='open');
|
||
const completed=missions.filter(m=>m.status==='completed');
|
||
document.getElementById('kpi-strip').innerHTML=`
|
||
<div class="kpi-box"><div class="kpi-val kpi-val-accent">${active.length}</div><div class="kpi-lbl">Aktiva</div></div>
|
||
<div class="kpi-box"><div class="kpi-val">${completed.length}</div><div class="kpi-lbl">Avslutade</div></div>
|
||
<div class="kpi-box"><div class="kpi-val">${fmtMoney(spending.total_completed_sek||0).replace(' kr','')}</div><div class="kpi-lbl">Totalt (kr)</div></div>
|
||
`;
|
||
// Active missions
|
||
const activeMissions=missions.filter(m=>['active','open'].includes(m.status));
|
||
document.getElementById('active-missions').innerHTML=activeMissions.length
|
||
? activeMissions.map(renderMissionCard).join('')
|
||
: '<div class="empty"><div class="empty-icon"></div><div style="font-weight:600">Inga aktiva uppdrag</div><div style="font-size:13px;margin-top:4px">Boka ett nytt uppdrag!</div></div>';
|
||
// Recent (completed, max 3)
|
||
const recent=missions.filter(m=>m.status==='completed').slice(0,3);
|
||
document.getElementById('recent-missions').innerHTML=recent.length
|
||
? recent.map(renderMissionCard).join('')
|
||
: '<div class="empty"><div class="empty-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg></div><div style="font-weight:600">Inga avslutade uppdrag</div></div>';
|
||
}
|
||
|
||
function renderMissionCard(m){
|
||
return `<div class="card-item">
|
||
<div class="card-icon ${missionIconClass(m.status)}">${missionIcon(m.type)}</div>
|
||
<div class="card-body">
|
||
<div class="card-title">${m.title}</div>
|
||
<div class="card-sub">${m.location} · ${fmtDate(m.start_time)}${m.zoomer_name?' · '+m.zoomer_name:''}</div>
|
||
</div>
|
||
<div class="card-right">
|
||
${statusBadge(m.status)}
|
||
<div style="text-align:right;margin-left:6px"><div style="font-size:14px;font-weight:600">${fmtMoney(m.amount_sek)}</div>${m.zoomer_rating?stars(m.zoomer_rating):''}</div>
|
||
<span class="chevron">›</span>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
// History tab
|
||
function renderHistory(){
|
||
const missions=missionCache[activeCustomer]||[];
|
||
const spending=spendingCache[activeCustomer]||{};
|
||
// Spending chart
|
||
const byMonth=spending.by_month||{};
|
||
const months=Object.keys(byMonth).sort();
|
||
const maxVal=Math.max(...Object.values(byMonth),1);
|
||
document.getElementById('spending-chart').innerHTML=`
|
||
<div class="bar-chart-wrap">
|
||
<div class="bar-chart-title">Utgifter per månad</div>
|
||
${months.map(mo=>`
|
||
<div class="bar-row">
|
||
<div class="bar-label">${mo.slice(5)}/${mo.slice(2,4)}</div>
|
||
<div class="bar-track"><div class="bar-fill" style="width:${Math.round(byMonth[mo]/maxVal*100)}%"></div></div>
|
||
<div class="bar-val">${fmtMoney(byMonth[mo])}</div>
|
||
</div>`).join('')}
|
||
${!months.length?'<div style="color:#666666;font-size:14px">Ingen historik ännu</div>':''}
|
||
</div>`;
|
||
// All missions
|
||
document.getElementById('all-missions').innerHTML=missions.length
|
||
? missions.map(renderMissionCard).join('')
|
||
: '<div class="empty"><div class="empty-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg></div>Inga uppdrag</div>';
|
||
}
|
||
|
||
// Billing tab
|
||
function renderBilling(){
|
||
const invs=invoiceCache[activeCustomer]||[];
|
||
document.getElementById('invoices-list').innerHTML=invs.length
|
||
? invs.map(inv=>`
|
||
<div class="card-item">
|
||
<div class="card-icon ${inv.status==='paid'?'ci-green':'ci-orange'}">${inv.status==='paid'?'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>':''}</div>
|
||
<div class="card-body"><div class="card-title">${inv.number}</div><div class="card-sub">Förfaller ${fmtDate(inv.due_date)}</div></div>
|
||
<div class="card-right">${invStatusBadge(inv.status)}<div style="font-size:14px;font-weight:700;margin-left:8px">${fmtMoney(inv.amount_sek)}</div><span class="chevron">›</span></div>
|
||
</div>`).join('')
|
||
: '<div class="empty"><div class="empty-icon"></div>Inga fakturor</div>';
|
||
}
|
||
|
||
// Favorites tab
|
||
function renderFavorites(){
|
||
const favs=favCache[activeCustomer]||[];
|
||
document.getElementById('favs-list').innerHTML=favs.length
|
||
? favs.map(f=>`
|
||
<div class="fav-card">
|
||
<div class="fav-avatar">${f.avatar||'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg>'}</div>
|
||
<div>
|
||
<div class="fav-name">${f.name}</div>
|
||
<div class="fav-spec">${f.specialty}</div>
|
||
<div class="fav-meta">${f.missions_completed} uppdrag · ${f.rating?' '+f.rating:'Ny'}</div>
|
||
</div>
|
||
<div class="fav-actions">
|
||
<button class="btn btn-gray" style="font-size:13px;padding:7px 12px;border-radius: var(--radius-md);border:none;cursor:pointer" onclick="removeFav('${f.zoomer_id}')"></button>
|
||
</div>
|
||
</div>`).join('')
|
||
: '<div class="empty"><div class="empty-icon"></div><div style="font-weight:600">Inga favoriter</div><div style="font-size:13px;margin-top:4px">Lägg till zoomers du gillar!</div></div>';
|
||
}
|
||
|
||
async function removeFav(zid){
|
||
const r=await fetch(`/api/quixzoom/customers/${activeCustomer}/favorites/${zid}`,{method:'DELETE'}).then(r=>r.json());
|
||
if(r.ok){
|
||
toast('Favorit borttagen');
|
||
const fR=await fetch(`/api/quixzoom/customers/${activeCustomer}/favorites`).then(r=>r.json());
|
||
favCache[activeCustomer]=fR.items||[];
|
||
renderFavorites();
|
||
} else toast('Fel: '+r.error);
|
||
}
|
||
|
||
// Book modal
|
||
function openBookModal(){document.getElementById('book-modal').classList.add('open')}
|
||
async function submitBook(){
|
||
const title=document.getElementById('bk-title').value;
|
||
const start=document.getElementById('bk-start').value;
|
||
if(!title||!start){toast('Fyll i titel och startdatum');return}
|
||
const body={
|
||
title,type:document.getElementById('bk-type').value,
|
||
start_time:new Date(start).toISOString(),
|
||
duration_hours:+document.getElementById('bk-duration').value||2,
|
||
location:document.getElementById('bk-location').value||'Phuket',
|
||
budget_sek:+document.getElementById('bk-budget').value||2500,
|
||
};
|
||
const r=await fetch(`/api/quixzoom/customers/${activeCustomer}/book`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(r=>r.json());
|
||
if(r.ok){
|
||
toast('<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> Uppdrag bokat!');closeModal('book-modal');
|
||
const mR=await fetch(`/api/quixzoom/customers/${activeCustomer}/missions`).then(r=>r.json());
|
||
missionCache[activeCustomer]=mR.items||[];renderDashboard();
|
||
document.getElementById('bk-title').value='';document.getElementById('bk-location').value='';document.getElementById('bk-budget').value='';
|
||
} else toast('Fel: '+r.error);
|
||
}
|
||
|
||
// Add favorite modal
|
||
function openAddFavModal(){document.getElementById('fav-modal').classList.add('open')}
|
||
async function submitFav(){
|
||
const body={zoomer_id:document.getElementById('fav-id').value,name:document.getElementById('fav-name').value,specialty:document.getElementById('fav-spec').value};
|
||
if(!body.zoomer_id||!body.name){toast('Fyll i ID och namn');return}
|
||
const r=await fetch(`/api/quixzoom/customers/${activeCustomer}/favorites`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(r=>r.json());
|
||
if(r.ok){
|
||
toast(' Favorit tillagd!');closeModal('fav-modal');
|
||
const fR=await fetch(`/api/quixzoom/customers/${activeCustomer}/favorites`).then(r=>r.json());
|
||
favCache[activeCustomer]=fR.items||[];renderFavorites();
|
||
document.getElementById('fav-id').value='';document.getElementById('fav-name').value='';
|
||
} else toast('Fel: '+r.error);
|
||
}
|
||
|
||
// Init
|
||
loadAll();
|
||
</script>
|
||
</body>
|
||
</html>
|