/* ============================================================ BAN YARO — Verlorener Hund (Sprint 11) Seiten-Modul: Leaflet-Karte + Meldungsliste + Melden-Formular. ============================================================ */ window.Page_lost = (() => { // ---------------------------------------------------------- // OFFLINE-CACHE // ---------------------------------------------------------- const _CACHE_KEY = 'by_lost_cache'; const _PENDING_KEY = 'by_lost_pending'; function _getPending() { try { return JSON.parse(localStorage.getItem(_PENDING_KEY) || '[]'); } catch { return []; } } function _setPending(list) { try { localStorage.setItem(_PENDING_KEY, JSON.stringify(list)); } catch {} } function _addPending(data) { const list = _getPending(); const entry = { ...data, id: `pending_${Date.now()}`, _isPending: true, created_at: new Date().toISOString() }; list.push(entry); _setPending(list); return entry; } async function _syncPending() { if (!navigator.onLine) return; const list = _getPending(); if (!list.length) return; let ok = 0; for (const item of [...list]) { try { const { id: _pid, _isPending, ...payload } = item; await API.lost.report(payload); _setPending(_getPending().filter(x => x.id !== item.id)); ok++; } catch {} } if (ok > 0) { UI.toast.success(`${ok} Meldung(en) synchronisiert.`); _loadReports(); } } window.addEventListener('online', _syncPending); // ---------------------------------------------------------- // MODUL-STATE // ---------------------------------------------------------- let _container = null; let _appState = null; let _map = null; let _markers = []; let _userMarker = null; let _reports = []; let _userPos = null; let _leafletLoaded = false; let _stylesInjected = false; function _injectStyles() { if (_stylesInjected) return; _stylesInjected = true; const s = document.createElement('style'); s.textContent = ` @keyframes by-lost-pulse-r { 0%,100% { box-shadow: 0 0 0 0 rgba(231,76,60,.55), 0 2px 6px rgba(0,0,0,.3); } 50% { box-shadow: 0 0 0 11px rgba(231,76,60,0), 0 2px 6px rgba(0,0,0,.3); } } @keyframes by-lost-pulse-p { 0%,100% { box-shadow: 0 0 0 0 rgba(217,119,6,.55), 0 2px 6px rgba(0,0,0,.3); } 50% { box-shadow: 0 0 0 11px rgba(217,119,6,0), 0 2px 6px rgba(0,0,0,.3); } } `; document.head.appendChild(s); } // ---------------------------------------------------------- // INIT // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; await _render(); } // ---------------------------------------------------------- // REFRESH — Navigation zur bereits geladenen Seite // ---------------------------------------------------------- async function refresh() { if (_userPos) await _loadReports(); } // ---------------------------------------------------------- // OPEN NEW — vom + Button // ---------------------------------------------------------- function openNew() { _showReportForm(); } // ---------------------------------------------------------- // RENDER — Grundstruktur aufbauen // ---------------------------------------------------------- async function _render() { _container.innerHTML = `
Standort wird ermittelt…
`; document.getElementById('lost-btn-locate') ?.addEventListener('click', _locateUser); document.getElementById('lost-btn-report') ?.addEventListener('click', _showReportForm); await _initMap(); setTimeout(() => _map?.invalidateSize(), 100); await _locateAndLoad(); } // ---------------------------------------------------------- // KARTE INITIALISIEREN (lädt Leaflet via UI.map.create) // ---------------------------------------------------------- async function _initMap() { _injectStyles(); const mapEl = document.getElementById('lost-map'); if (!mapEl || _map) return; _map = await UI.map.create('lost-map', { center: [51.1657, 10.4515], zoom: 6, zoomControl: true, attributionControl: false, }); _leafletLoaded = true; } // ---------------------------------------------------------- // STANDORT ERMITTELN + LADEN // ---------------------------------------------------------- async function _locateAndLoad() { try { _userPos = await API.getLocation({ timeout: 8000 }); _showUserOnMap(); } catch { _userPos = null; } await _loadReports(); } async function _locateUser() { const btn = document.getElementById('lost-btn-locate'); UI.setLoading(btn, true); try { _userPos = await API.getLocation({ timeout: 8000 }); _showUserOnMap(); if (_map) _map.setView([_userPos.lat, _userPos.lon], 13); await _loadReports(); } catch { UI.toast.warning('Standort konnte nicht ermittelt werden.'); } UI.setLoading(btn, false); } function _showUserOnMap() { if (!_map || !window.L || !_userPos) return; if (_userMarker) _map.removeLayer(_userMarker); _userMarker = L.circleMarker([_userPos.lat, _userPos.lon], { radius : 9, fillColor : '#3498db', color : '#fff', weight : 2, fillOpacity : 0.9, }).addTo(_map).bindPopup('Du bist hier'); _map.setView([_userPos.lat, _userPos.lon], 13); } // ---------------------------------------------------------- // MELDUNGEN LADEN // ---------------------------------------------------------- async function _loadReports() { const infoEl = document.getElementById('lost-info'); if (!_userPos) { _reports = []; _renderHeld(); _renderList(); if (infoEl) infoEl.textContent = 'Standort unbekannt — bitte Standort freigeben (📍 Mein Standort).'; return; } try { const fetched = await API.lost.list(_userPos.lat, _userPos.lon, 25); try { localStorage.setItem(_CACHE_KEY, JSON.stringify({ ts: Date.now(), data: fetched })); } catch {} // Remove pending items already on the server (race: sync completed during fetch) const rawPending = _getPending(); const dedupedPending = rawPending.filter(p => !fetched.some(f => f.name === p.name && Math.abs(f.lat - p.lat) < 0.0001 && Math.abs(f.lon - p.lon) < 0.0001) ); if (dedupedPending.length < rawPending.length) _setPending(dedupedPending); const pending = dedupedPending.map(p => ({ ...p, distanz_m: _haversine(_userPos.lat, _userPos.lon, p.lat, p.lon), })); _reports = [...pending, ...fetched]; _renderMarkers(); _renderHeld(); _renderList(); _updateBadge(_reports.length); if (infoEl) { infoEl.textContent = _reports.length > 0 ? `${_reports.length} vermisste${_reports.length !== 1 ? 'r Hund' : 'r Hund'} im Umkreis von 25 km` : 'Keine vermissten Hunde in deiner Nähe (25 km Radius). 🐾'; } } catch { const offline_pending = _getPending().map(p => ({ ...p, distanz_m: _haversine(_userPos.lat, _userPos.lon, p.lat, p.lon), })); try { const raw = localStorage.getItem(_CACHE_KEY); if (raw) { const cached = JSON.parse(raw).data || []; _reports = [...offline_pending, ...cached]; _renderMarkers(); _renderHeld(); _renderList(); _updateBadge(_reports.length); if (infoEl) infoEl.textContent = 'Offline — zeige zuletzt geladene Meldungen.'; return; } } catch {} _reports = offline_pending; if (offline_pending.length) { _renderMarkers(); _renderHeld(); _renderList(); _updateBadge(_reports.length); return; } UI.toast.error('Meldungen konnten nicht geladen werden.'); } } // ---------------------------------------------------------- // KARTEN-MARKER // ---------------------------------------------------------- function _renderMarkers() { if (!_map || !window.L) return; _markers.forEach(m => _map.removeLayer(m)); _markers = []; _reports.forEach(r => { const dotColor = r._isPending ? '#d97706' : '#e74c3c'; const anim = r._isPending ? 'by-lost-pulse-p' : 'by-lost-pulse-r'; const html = `${_escape(r.beschreibung.slice(0, 120))}${r.beschreibung.length > 120 ? '…' : ''}
${_escape(r.beschreibung)}
Wurde ${_escape(r.name)} wiedergefunden? Die Meldung wird als abgeschlossen markiert und aus der Liste entfernt.
`, footer: ` `, }); document.getElementById('found-cancel') ?.addEventListener('click', UI.modal.close); document.getElementById('found-confirm')?.addEventListener('click', async () => { const btn = document.getElementById('found-confirm'); await UI.asyncButton(btn, async () => { await API.lost.markFound(r.id); _reports = _reports.filter(x => x.id !== r.id); _renderMarkers(); _renderList(); _updateBadge(_reports.length); UI.modal.close(); UI.toast.success(`${r.name} ist wieder da! 🎉`); }); }); } // ---------------------------------------------------------- // MELDE-FORMULAR // ---------------------------------------------------------- function _showReportForm() { if (!_appState.user) { UI.toast.warning('Bitte zuerst anmelden, um eine Meldung abzuschicken.'); App.navigate('settings'); return; } // Eigene registrierte Hunde für Dropdown const dogs = _appState.dogs || []; const dogOpts = dogs.length > 0 ? `` + dogs.map(d => ``).join('') : ''; const body = ` `; const footer = ` `; UI.modal.open({ title: '🔍 Hund vermisst melden', body, footer }); // Standort vorausfüllen if (_userPos) { document.getElementById('lf-lat').value = _userPos.lat; document.getElementById('lf-lon').value = _userPos.lon; document.getElementById('lf-lat-disp').value = _userPos.lat.toFixed(6); document.getElementById('lf-lon-disp').value = _userPos.lon.toFixed(6); } // Wenn registrierter Hund gewählt → Name+Rasse vorausfüllen document.getElementById('lf-dog-select')?.addEventListener('change', e => { const dogId = parseInt(e.target.value); const dog = dogs.find(d => d.id === dogId); if (dog) { document.getElementById('lf-name').value = dog.name; const rasseInput = document.querySelector('#lost-form [name="rasse"]'); if (rasseInput && dog.rasse) rasseInput.value = dog.rasse; } }); // GPS-Button document.getElementById('lf-gps-btn')?.addEventListener('click', async () => { const btn = document.getElementById('lf-gps-btn'); UI.setLoading(btn, true); try { const pos = await API.getLocation({ timeout: 10000, enableHighAccuracy: true }); document.getElementById('lf-lat').value = pos.lat; document.getElementById('lf-lon').value = pos.lon; document.getElementById('lf-lat-disp').value = pos.lat.toFixed(6); document.getElementById('lf-lon-disp').value = pos.lon.toFixed(6); document.getElementById('lf-gps-hint').textContent = '✅ Standort aktualisiert'; _userPos = pos; } catch { UI.toast.error('GPS-Standort konnte nicht ermittelt werden.'); } UI.setLoading(btn, false); }); // Foto-Vorschau const photoInput = document.querySelector('#lost-form [name="photo"]'); const photoPreview = document.getElementById('lf-photo-preview'); if (photoInput && photoPreview) { UI.setupPhotoPreview(photoInput, photoPreview); photoInput.addEventListener('change', () => { photoPreview.style.display = photoInput.files[0] ? 'block' : 'none'; }); } document.getElementById('lf-cancel') ?.addEventListener('click', UI.modal.close); // Formular absenden document.getElementById('lost-form')?.addEventListener('submit', async e => { e.preventDefault(); const submitBtn = document.querySelector('[form="lost-form"][type="submit"]') || e.target.querySelector('[type="submit"]'); const fd = UI.formData(e.target); if (!fd.lat || !fd.lon) { UI.toast.warning('Bitte zuerst den GPS-Standort ermitteln (📍).'); return; } if (!fd.name?.trim()) { UI.toast.warning('Bitte den Namen des Hundes eingeben.'); return; } await UI.asyncButton(submitBtn, async () => { const payload = { name : fd.name.trim(), rasse : fd.rasse?.trim() || null, beschreibung : fd.beschreibung?.trim() || '', lat : parseFloat(fd.lat), lon : parseFloat(fd.lon), dog_id : fd.dog_id ? parseInt(fd.dog_id) : null, client_time : API.clientNow(), }; let created; try { created = await API.lost.report(payload); } catch (netErr) { // Netzwerkfehler (TypeError = fetch failed) → offline speichern if (netErr instanceof TypeError || !navigator.onLine) { const pending = _addPending(payload); pending.distanz_m = _userPos ? Math.round(_haversine(_userPos.lat, _userPos.lon, pending.lat, pending.lon)) : 0; UI.modal.close(); UI.toast.success('Offline gespeichert — wird synchronisiert sobald Verbindung besteht.'); _loadReports(); return; } throw netErr; // API-Fehler (z.B. 422) → weitergeben } // Foto hochladen if (photoInput?.files[0]) { try { const toUpload = await API.compressImage(photoInput.files[0]); const formData = new FormData(); formData.append('file', toUpload); const media = await API.lost.uploadFoto(created.id, formData); created.foto_url = media.foto_url; } catch { UI.toast.warning('Meldung erstellt — Foto konnte nicht hochgeladen werden.'); } } // Distanz client-seitig berechnen created.distanz_m = _userPos ? Math.round(_haversine(_userPos.lat, _userPos.lon, created.lat, created.lon)) : 0; _reports.unshift(created); _renderMarkers(); _renderList(); _updateBadge(_reports.length); UI.toast.success('Hund als vermisst gemeldet. Wir drücken die Daumen!'); UI.modal.close(); }); }); } // ---------------------------------------------------------- // BADGE // ---------------------------------------------------------- function _updateBadge(count) { const b = document.getElementById('lost-badge'); if (b) { b.textContent = count; b.style.display = count > 0 ? '' : 'none'; } } // ---------------------------------------------------------- // HELPER // ---------------------------------------------------------- function _haversine(lat1, lon1, lat2, lon2) { const R = 6_371_000; const p1 = lat1 * Math.PI / 180; const p2 = lat2 * Math.PI / 180; const dp = (lat2 - lat1) * Math.PI / 180; const dl = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dp / 2) ** 2 + Math.cos(p1) * Math.cos(p2) * Math.sin(dl / 2) ** 2; return 2 * R * Math.asin(Math.sqrt(a)); } function _fmtDate(isoStr) { if (!isoStr) return ''; const d = new Date(isoStr.replace(' ', 'T')); return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); } function _escape(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function _emptyState(icon, title, text, cta = '') { return `${text}
` : ''} ${cta ? `