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

639 lines
26 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: 16px;
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: 12px;
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: 14px; 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: 14px;
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: 50%;
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: 10px; object-fit: cover; border: 0.5px solid var(--separator); }
.photo-add-btn {
width: 80px; height: 80px; border-radius: 10px;
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: 14px;
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: 20px;
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:50%; 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:20px; 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: 20px;
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:'🏢', 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:'🔍', 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:50%;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">✅</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">📷 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()">
📷<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()">✗ Avböj</button>
<button class="action-btn btn-accept" onclick="acceptMission()">✓ Acceptera uppdrag</button>`;
} else if (mission.status === 'assigned' && isMyMission) {
bar.innerHTML = `
<button class="action-btn btn-camera" onclick="document.getElementById('photo-input').click()">📷</button>
<button class="action-btn btn-complete" onclick="submitWithPhotos()">✅ 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('✅ Uppdrag accepterat! Dra till platsen och starta.');
renderContent();
} else {
toast('⚠️ ' + (data.error || 'Kunde inte acceptera'));
if (btn) { btn.textContent = '✓ Acceptera uppdrag'; btn.disabled = false; }
}
} catch(e) {
toast('⚠️ Nätverksfel');
if (btn) { btn.textContent = '✓ 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(`📷 ${uploadedFiles.length} foto valda`);
const bar = document.getElementById('bottom-actions');
bar.innerHTML = `
<button class="action-btn btn-camera" onclick="document.getElementById('photo-input').click()">📷 (${uploadedFiles.length})</button>
<button class="action-btn btn-complete" onclick="submitWithPhotos()">✅ Skicka in ${uploadedFiles.length} bilder</button>`;
});
async function submitWithPhotos() {
if (uploadedFiles.length === 0) {
toast('📷 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('⚠️ ' + (data.error || 'Fel vid inskick'));
if (btn) { btn.textContent = '✅ Skicka in'; btn.disabled = false; }
}
} catch(e) {
toast('⚠️ Nätverksfel — försök igen');
if (btn) { btn.textContent = '✅ 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>