Files
boc/quixzoom-landing-fixed/mission-detail.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

639 lines
31 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="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Uppdragsdetaljer — quiXzoom</title>
<link rel="stylesheet" >
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-text);
background: var(--background-primary);
color: var(--label-primary);
min-height: 100dvh;
padding-top: env(safe-area-inset-top, 44px);
padding-bottom: calc(env(safe-area-inset-bottom, 34px) + 90px);
-webkit-font-smoothing: antialiased;
}
/* Nav */
.nav-bar {
position: sticky; top: 0; z-index: 200;
display: flex; align-items: center; height: 44px; padding: 0 16px;
background: var(--navbar-bg);
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
border-bottom: 0.5px solid var(--separator);
gap: 12px;
}
.back-btn {
background: none; border: none;
color: var(--ios-blue); font-size: 17px;
cursor: pointer; display: flex; align-items: center; gap: 4px;
font-family: var(--font-text);
}
.nav-center {
flex: 1; text-align: center;
font-size: 17px; font-weight: 600; letter-spacing: -0.41px;
}
.nav-spacer { width: 60px; }
/* Hero map */
#detail-map { width: 100%; height: 200px; }
/* Content */
.content { padding: 0 16px; }
/* Hero Card */
.hero-card {
background: var(--background-secondary);
border-radius: var(--radius-lg);
margin: 16px 0;
overflow: hidden;
border: 0.5px solid var(--separator);
}
.hero-top {
display: flex; align-items: flex-start; gap: 14px;
padding: 16px;
}
.hero-icon {
width: 52px; height: 52px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 26px; flex-shrink: 0;
}
.hero-info { flex: 1; }
.hero-title {
font-family: var(--font-display);
font-size: 20px; font-weight: 700; letter-spacing: -0.5px;
margin-bottom: 4px;
}
.hero-address {
font-size: 14px; color: var(--label-secondary);
}
.hero-reward {
font-family: var(--font-display);
font-size: 28px; font-weight: 800; color: var(--ios-green);
letter-spacing: -0.5px;
}
.hero-reward-label { font-size: 13px; color: var(--label-secondary); font-weight: 500; }
.hero-stats {
display: grid; grid-template-columns: 1fr 1fr 1fr;
border-top: 0.5px solid var(--separator);
}
.stat-cell {
padding: 12px;
display: flex; flex-direction: column; align-items: center; gap: 3px;
border-right: 0.5px solid var(--separator);
}
.stat-cell:last-child { border-right: none; }
.stat-value { font-size: 15px; font-weight: 600; }
.stat-label { font-size: 11px; color: var(--label-secondary); text-align: center; }
/* Sections */
.section { margin-bottom: 20px; }
.section-title {
font-size: 20px; font-weight: 700; letter-spacing: -0.5px;
margin-bottom: 10px;
}
.section-card {
background: var(--background-secondary);
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.section-row {
display: flex; align-items: flex-start; gap: 12px;
padding: 13px 16px;
border-bottom: 0.5px solid var(--separator);
}
.section-row:last-child { border-bottom: none; }
.row-icon { font-size: 18px; flex-shrink: 0; width: 24px; text-align: center; }
.row-content { flex: 1; }
.row-title { font-size: 15px; font-weight: 500; }
.row-sub { font-size: 13px; color: var(--label-secondary); margin-top: 2px; }
/* Description */
.description-text {
font-size: 15px; line-height: 1.55;
color: var(--label-primary); padding: 14px 16px;
background: var(--background-secondary);
border-radius: var(--radius-lg);
border: 0.5px solid var(--separator);
}
/* Steps */
.steps { display: flex; flex-direction: column; gap: 0; }
.step {
display: flex; gap: 14px; padding: 12px 16px;
position: relative;
}
.step-num {
width: 28px; height: 28px; border-radius: var(--radius-full);
background: var(--ios-blue); color: #fff;
display: flex; align-items: center; justify-content: center;
font-size: 14px; font-weight: 700; flex-shrink: 0;
}
.step-num.done { background: var(--ios-green); }
.step-body { flex: 1; padding-top: 4px; }
.step-title { font-size: 15px; font-weight: 600; }
.step-desc { font-size: 13px; color: var(--label-secondary); margin-top: 3px; }
/* Photo Section */
.photo-wrap { padding: 14px 16px; }
.photo-grid { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.photo-thumb { width: 80px; height: 80px; border-radius: var(--radius-md); object-fit: cover; border: 0.5px solid var(--separator); }
.photo-add-btn {
width: 80px; height: 80px; border-radius: var(--radius-md);
background: var(--fill-secondary);
border: 1.5px dashed var(--ios-blue);
display: flex; flex-direction: column;
align-items: center; justify-content: center;
font-size: 24px; cursor: pointer;
color: var(--ios-blue); gap: 2px;
}
.photo-add-btn span { font-size: 10px; font-weight: 600; }
/* Bottom Actions */
.bottom-actions {
position: fixed; bottom: 0; left: 0; right: 0;
background: var(--tabbar-bg);
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
border-top: 0.5px solid var(--separator);
padding: 12px 16px calc(env(safe-area-inset-bottom,34px) + 12px);
display: flex; gap: 10px;
}
.action-btn {
flex: 1; padding: 14px;
border: none; border-radius: var(--radius-lg);
font-family: var(--font-text); font-size: 16px; font-weight: 700;
cursor: pointer; transition: transform 0.1s, opacity 0.1s;
letter-spacing: -0.2px;
}
.action-btn:active { transform: scale(0.97); opacity: 0.85; }
.btn-accept { background: var(--ios-blue); color: #fff; }
.btn-decline { background: var(--fill-secondary); color: var(--label-primary); }
.btn-camera { background: var(--fill-secondary); color: var(--label-primary); flex: 0 0 52px; font-size: 22px; }
.btn-complete { background: var(--ios-green); color: #fff; }
.btn-disabled { background: var(--fill-secondary); color: var(--label-tertiary); pointer-events: none; }
/* Badge */
.status-badge {
display: inline-block; padding: 4px 10px; border-radius: var(--radius-xl);
font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px;
}
.badge-open { background: rgba(52,199,89,0.15); color: var(--ios-green); }
.badge-assigned { background: rgba(255,149,0,0.15); color: var(--ios-orange); }
.badge-submitted { background: rgba(0,122,255,0.15); color: var(--ios-blue); }
.badge-approved { background: rgba(88,86,214,0.15); color: var(--ios-indigo); }
/* Loader */
.loader { display:flex; justify-content:center; padding:80px; }
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { width:32px; height:32px; border-radius: var(--radius-full); border:3px solid var(--fill-secondary); border-top-color:var(--ios-blue); animation:spin 0.8s linear infinite; }
/* Toast */
.toast { position:fixed; bottom:calc(env(safe-area-inset-bottom,34px)+100px); left:50%; transform:translateX(-50%) translateY(20px); background:var(--background-secondary); border:0.5px solid var(--separator); border-radius: var(--radius-xl); padding:10px 18px; font-size:14px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.3); opacity:0; transition:all 0.3s; z-index:9999; white-space:nowrap; }
.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
/* Icons bg */
.icon-facade { background: rgba(0,122,255,0.15); }
.icon-roof { background: rgba(255,149,0,0.15); }
.icon-road { background: rgba(142,142,147,0.15); }
.icon-interior { background: rgba(52,199,89,0.15); }
.icon-vegetation { background: rgba(48,209,88,0.15); }
.icon-inspection { background: rgba(175,82,222,0.15); }
.gps-chip {
display: inline-flex; align-items: center; gap: 4px;
padding: 4px 10px; border-radius: var(--radius-xl);
background: rgba(52,199,89,0.15); color: var(--ios-green);
font-size: 12px; font-weight: 600;
}
.gps-chip.searching { background: rgba(255,149,0,0.15); color: var(--ios-orange); }
@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>
<script src="i18n.js"></script>
<meta property="og:type" content="website">
<meta property="og:title" content="Uppdragsdetaljer — quiXzoom">
<meta property="og:description" content="quiXzoom — Earn per Mission. No Platform Fee.">
<meta property="og:url" content="https://www.quixzoom.com/mission-detail.html">
<meta property="og:image" content="https://www.quixzoom.com/images/infrastructure-scan.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="quiXzoom mission detail — how photo missions work">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://www.quixzoom.com/images/infrastructure-scan.jpg">
<meta name="twitter:title" content="Uppdragsdetaljer — quiXzoom">
</head>
<body>
<!-- Nav -->
<div class="nav-bar">
<button class="back-btn" onclick="history.back()">
Tillbaka
</button>
<div class="nav-center">Uppdragsdetaljer</div>
<div class="nav-spacer"></div>
</div>
<!-- Map -->
<div id="detail-map"></div>
<!-- Main content -->
<div class="content" id="main-content">
<div class="loader"><div class="spinner"></div></div>
</div>
<!-- Hidden file input -->
<input type="file" id="photo-input" accept="image/*" capture="environment" multiple style="display:none">
<!-- Bottom actions -->
<div class="bottom-actions" id="bottom-actions" style="display:none"></div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script>
const API = 'https://api.quixzoom.com/api/qz/missions';
let mission = null;
let map = null;
let uploadedFiles = [];
let userLat = null, userLon = null;
const urlParams = new URLSearchParams(location.search);
const missionId = urlParams.get('id');
function getZoomerId() {
let id = localStorage.getItem('qz_zoomer_id');
if (!id) { id = 'zoomer-' + Math.random().toString(36).slice(2,10); localStorage.setItem('qz_zoomer_id', id); }
return id;
}
function getActiveMission() {
try { return JSON.parse(localStorage.getItem('qz_active_mission') || 'null'); } catch { return null; }
}
function setActiveMission(m) {
if (m) localStorage.setItem('qz_active_mission', JSON.stringify(m));
else localStorage.removeItem('qz_active_mission');
}
function toast(msg, dur=3000) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), dur);
}
function missionTypeConfig(type) {
const map = {
facade_scan: { 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 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg>', cls:'icon-facade', label:'Fasadscanning' },
roof_scan: { icon:'', cls:'icon-roof', label:'Takscanning' },
road_inspection: { icon:'', cls:'icon-road', label:'Väginspekt.' },
interior_scan: { icon:'', cls:'icon-interior', label:'Interi ör' },
vegetation_survey: { icon:'', cls:'icon-vegetation', label:'Vegetationssurv.' },
inspection: { icon:'<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>', cls:'icon-inspection', label:'Inspektion' },
};
return map[type] || { icon:'', cls:'icon-inspection', label: type };
}
function getInstructions(type) {
const m = {
facade_scan: [
{ title: 'Anländ till adressen', desc: 'Bekräfta GPS-position är inom 50m' },
{ title: 'Fotografera hela fasaden', desc: 'Minst 8 bilder från olika vinklar' },
{ title: 'Detaljer & skador', desc: 'Närbild på brandskadat/skadat parti' },
{ title: 'Ladda upp & slutför', desc: 'Skicka in bilder för 3D-processering' },
],
roof_scan: [
{ title: 'Anländ till fastigheten', desc: 'Bekräfta GPS och se upp för hinder' },
{ title: 'Fotografera takytan', desc: 'Täck hela taket med överlappande bilder' },
{ title: 'Dokumentera skador', desc: 'Markera stormskadade partier' },
{ title: 'Ladda upp & slutför', desc: 'Skicka in för AI-analys' },
],
road_inspection: [
{ title: 'Gå längs sträckan', desc: 'Strandvägen 200 m enl. uppdraget' },
{ title: 'Fotografera varje skada', desc: 'Potthål, sprickor, markskador' },
{ title: 'GPS-verifiering', desc: 'Systemet loggar din rutt automatiskt' },
{ title: 'Slutför', desc: 'Skicka in dokumentation' },
],
};
return m[type] || [
{ title: 'Anländ till platsen', desc: 'Bekräfta GPS-position' },
{ title: 'Dokumentera objektet', desc: 'Minst 5 bilder från olika vinklar' },
{ title: 'Ladda upp & slutför', desc: 'Skicka in bilderna' },
];
}
function formatSEK(n) { return new Intl.NumberFormat('sv-SE').format(n) + ' kr'; }
function formatDate(s) { return s ? new Date(s).toLocaleDateString('sv-SE', { day:'numeric', month:'short', hour:'2-digit', minute:'2-digit'}) : '—'; }
function statusBadge(status) {
const labels = { open:'Öppen', assigned:'Tilldelad', submitted:'Inskickad', approved:'Godkänd' };
return `<span class="status-badge badge-${status}">${labels[status]||status}</span>`;
}
function initMap(lat, lon) {
map = L.map('detail-map', { zoomControl: false }).setView([lat, lon], 16);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution:'© OpenStreetMap' }).addTo(map);
const icon = L.divIcon({
html: `<div style="width:40px;height:40px;border-radius: var(--radius-full);background:var(--ios-blue);display:flex;align-items:center;justify-content:center;font-size:20px;box-shadow:0 2px 10px rgba(0,0,0,0.4);border:2.5px solid white;">${missionTypeConfig(mission.mission_type).icon}</div>`,
className:'', iconSize:[40,40], iconAnchor:[20,20]
});
L.marker([lat, lon], {icon}).addTo(map).openPopup();
// Accuracy circle
L.circle([lat, lon], { radius: 50, color: 'rgba(0,122,255,0.3)', fillColor: 'rgba(0,122,255,0.1)', fillOpacity: 0.5, weight: 1 }).addTo(map);
}
function renderContent() {
const cfg = missionTypeConfig(mission.mission_type);
const instructions = getInstructions(mission.mission_type);
const zoomerId = getZoomerId();
const isMyMission = mission.zoomer_id === zoomerId;
const activeMission = getActiveMission();
const distText = (userLat && userLon)
? `${Math.round(calcDist(userLat, userLon, mission.lat, mission.lon))} m bort`
: 'Hämtar GPS...';
const html = `
<div class="hero-card">
<div class="hero-top">
<div class="hero-icon ${cfg.cls}">${cfg.icon}</div>
<div class="hero-info">
<div class="hero-title">${mission.title}</div>
<div class="hero-address"> ${mission.address || 'Okänd plats'}</div>
<div style="margin-top:6px">${statusBadge(mission.status)}</div>
</div>
</div>
<div class="hero-stats">
<div class="stat-cell">
<div class="stat-value" style="color:var(--ios-green)">${formatSEK(mission.reward_sek)}</div>
<div class="stat-label">Ersättning</div>
</div>
<div class="stat-cell">
<div class="stat-value">${cfg.label}</div>
<div class="stat-label">Uppdragstyp</div>
</div>
<div class="stat-cell">
<div class="stat-value gps-chip ${!userLat ? 'searching' : ''}">${!userLat ? ' GPS' : ' ' + distText}</div>
<div class="stat-label">Avstånd</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Om uppdraget</div>
<div class="description-text">${mission.description || 'Ingen ytterligare beskrivning.'}</div>
</div>
<div class="section">
<div class="section-title">Information</div>
<div class="section-card">
<div class="section-row">
<div class="row-icon"></div>
<div class="row-content">
<div class="row-title">Skapat</div>
<div class="row-sub">${formatDate(mission.created_at)}</div>
</div>
</div>
${mission.accepted_at ? `
<div class="section-row">
<div class="row-icon"><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="row-content">
<div class="row-title">Accepterat</div>
<div class="row-sub">${formatDate(mission.accepted_at)}</div>
</div>
</div>` : ''}
${mission.submitted_at ? `
<div class="section-row">
<div class="row-icon"></div>
<div class="row-content">
<div class="row-title">Inskickat</div>
<div class="row-sub">${formatDate(mission.submitted_at)}</div>
</div>
</div>` : ''}
${mission.approved_at ? `
<div class="section-row">
<div class="row-icon"></div>
<div class="row-content">
<div class="row-title">Godkänt & betalt</div>
<div class="row-sub">${formatDate(mission.approved_at)} · Ref: ${mission.payment_ref || '—'}</div>
</div>
</div>` : ''}
<div class="section-row">
<div class="row-icon"></div>
<div class="row-content">
<div class="row-title">Uppdragsnr</div>
<div class="row-sub" style="font-family:var(--font-mono);font-size:12px">${mission.id}</div>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Instruktioner</div>
<div class="section-card steps">
${instructions.map((step, i) => `
<div class="step">
<div class="step-num ${mission.status !== 'open' && i === 0 ? 'done' : ''}">${i+1}</div>
<div class="step-body">
<div class="step-title">${step.title}</div>
<div class="step-desc">${step.desc}</div>
</div>
</div>`).join('')}
</div>
</div>
${(isMyMission && mission.status === 'assigned') ? `
<div class="section">
<div class="section-title"><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> Dokumentation</div>
<div class="section-card">
<div class="photo-wrap">
<div style="font-size:13px;color:var(--label-secondary);margin-bottom:6px">
Ta minst 3 bilder för att kunna slutföra uppdraget
</div>
<div class="photo-grid" id="photo-grid">
<div class="photo-add-btn" onclick="document.getElementById('photo-input').click()">
<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><span>Lägg till</span>
</div>
</div>
</div>
</div>
</div>` : ''}
`;
document.getElementById('main-content').innerHTML = html;
renderBottomActions();
}
function renderBottomActions() {
const bar = document.getElementById('bottom-actions');
bar.style.display = 'flex';
const zoomerId = getZoomerId();
const isMyMission = mission.zoomer_id === zoomerId;
if (mission.status === 'open') {
bar.innerHTML = `
<button class="action-btn btn-decline" onclick="history.back()"><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> Avböj</button>
<button class="action-btn btn-accept" onclick="acceptMission()"><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> Acceptera uppdrag</button>`;
} else if (mission.status === 'assigned' && isMyMission) {
bar.innerHTML = `
<button class="action-btn btn-camera" onclick="document.getElementById('photo-input').click()"><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></button>
<button class="action-btn btn-complete" onclick="submitWithPhotos()"><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> Slutför & skicka in</button>`;
} else if (mission.status === 'submitted') {
bar.innerHTML = `<button class="action-btn btn-disabled">⏳ Väntar på granskning</button>`;
} else if (mission.status === 'approved') {
bar.innerHTML = `<button class="action-btn btn-disabled" style="background:rgba(52,199,89,0.15);color:var(--ios-green)"> Godkänt & betalt</button>`;
} else {
bar.innerHTML = `<button class="action-btn btn-decline" onclick="history.back()">← Tillbaka</button>`;
}
}
async function acceptMission() {
const zoomerId = getZoomerId();
const btn = document.querySelector('.btn-accept');
if (btn) { btn.textContent = '⏳ Accepterar...'; btn.disabled = true; }
try {
const r = await fetch(`${API}/missions/${missionId}/accept`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ zoomer_id: zoomerId })
});
const data = await r.json();
if (r.ok) {
mission = data;
setActiveMission(data);
toast('<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> Uppdrag accepterat! Dra till platsen och starta.');
renderContent();
} else {
toast('<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> ' + (data.error || 'Kunde inte acceptera'));
if (btn) { btn.textContent = '<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> Acceptera uppdrag'; btn.disabled = false; }
}
} catch(e) {
toast('<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> Nätverksfel');
if (btn) { btn.textContent = '<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> Acceptera uppdrag'; btn.disabled = false; }
}
}
document.getElementById('photo-input').addEventListener('change', e => {
uploadedFiles = Array.from(e.target.files);
const grid = document.getElementById('photo-grid');
if (!grid) return;
// Remove old thumbs, keep add btn
grid.querySelectorAll('.photo-thumb').forEach(el => el.remove());
uploadedFiles.forEach(f => {
const img = document.createElement('img');
img.className = 'photo-thumb';
img.src = URL.createObjectURL(f);
grid.insertBefore(img, grid.firstChild);
});
toast(`<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> ${uploadedFiles.length} foto valda`);
const bar = document.getElementById('bottom-actions');
bar.innerHTML = `
<button class="action-btn btn-camera" onclick="document.getElementById('photo-input').click()"><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> (${uploadedFiles.length})</button>
<button class="action-btn btn-complete" onclick="submitWithPhotos()"><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> Skicka in ${uploadedFiles.length} bilder</button>`;
});
async function submitWithPhotos() {
if (uploadedFiles.length === 0) {
toast('<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> Ta minst ett foto innan du slutför');
return;
}
const btn = document.querySelector('.btn-complete');
if (btn) { btn.textContent = '⏳ Skickar...'; btn.disabled = true; }
const fd = new FormData();
uploadedFiles.forEach(f => fd.append('images', f));
try {
const r = await fetch(`${API}/missions/${missionId}/submit`, { method:'POST', body: fd });
const data = await r.json();
if (r.ok) {
mission = data.mission;
setActiveMission(null);
toast(' Inskickat! Väntar på granskning och utbetalning.');
uploadedFiles = [];
setTimeout(() => window.location.href = 'zoomer-app.html', 2000);
} else {
toast('<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> ' + (data.error || 'Fel vid inskick'));
if (btn) { btn.textContent = '<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> Skicka in'; btn.disabled = false; }
}
} catch(e) {
toast('<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> Nätverksfel — försök igen');
if (btn) { btn.textContent = '<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> Skicka in'; btn.disabled = false; }
}
}
function calcDist(lat1, lon1, lat2, lon2) {
const R = 6371000;
const φ1 = lat1 * Math.PI/180, φ2 = lat2 * Math.PI/180;
const Δφ = (lat2-lat1) * Math.PI/180, Δλ = (lon2-lon1) * Math.PI/180;
const a = Math.sin(Δφ/2)*Math.sin(Δφ/2) + Math.cos(φ1)*Math.cos(φ2)*Math.sin(Δλ/2)*Math.sin(Δλ/2);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
async function load() {
if (!missionId) {
document.getElementById('main-content').innerHTML = '<div style="padding:40px;text-align:center;color:var(--label-secondary)">Inget uppdrag valt</div>';
return;
}
// Get GPS in parallel
navigator.geolocation?.getCurrentPosition(pos => {
userLat = pos.coords.latitude;
userLon = pos.coords.longitude;
});
try {
const r = await fetch(`${API}/missions/${missionId}`);
mission = await r.json();
initMap(mission.lat, mission.lon);
renderContent();
} catch(e) {
document.getElementById('main-content').innerHTML = `<div style="padding:40px;text-align:center"><div style="font-size:48px"></div><div style="margin-top:12px;color:var(--label-secondary)">Kunde inte ladda uppdrag</div></div>`;
}
}
load();
</script>
</body>
</html>