Files
boc/quixzoom-landing-fixed/customer-dashboard.html
T
Bernt aee0f09db8 landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar
- Vision: Identify-modell, FAISS, OCR alla testade
- API: Alla 7 integrationstester passerade
- Upplösare: Entitetsupplösning verifierad
2026-07-05 06:41:32 +00:00

489 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="sv" data-theme="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:16px;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:20px;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:12px;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:12px;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:10px;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:6px;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:14px;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:4px;overflow:hidden}
.bar-fill{height:8px;border-radius:4px;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:20px 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:2px;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:10px;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:12px;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:10px;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:10px;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:12px;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:10px;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">📋</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 🚁</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()">🚀 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">✓ Klar</span>',active:'<span class="badge bg-blue">🔄 Aktiv</span>',open:'<span class="badge bg-orange">🔍 Öppen</span>',cancelled:'<span class="badge bg-red">✗ Avbruten</span>'}[s]||`<span class="badge bg-gray">${s}</span>`}
function missionIcon(t){return{photography:'📸',video:'🎬',drone:'🚁'}[t]||'📷'}
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">📋</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">📋</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'?'✅':'🧾'}</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||'📷'}</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:9px;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('🚀 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>