/* ============================================================ BAN YARO — Giftköder-Alarm (Sprint 2) Seiten-Modul: Leaflet-Karte + Meldungsliste + Melden-Formular. ============================================================ */ window.Page_poison = (() => { // ---------------------------------------------------------- // MODUL-STATE // ---------------------------------------------------------- let _container = null; let _appState = null; let _map = null; let _markers = []; let _userMarker = null; let _reports = []; let _userPos = null; const TYPEN = { unbekannt: { label: 'Unbekannt', icon: '❓', color: '#e67e22' }, koeoder: { label: 'Köder', icon: '🎣', color: '#e74c3c' }, vergiftet: { label: 'Vergiftetes Tier', icon: '☠️', color: '#8e44ad' }, chemikalie: { label: 'Chemikalie', icon: '⚗️', color: '#c0392b' }, andere: { label: 'Andere Gefahr', icon: '⚠️', color: '#d35400' }, }; // ---------------------------------------------------------- // 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 = `
© OpenStreetMap-Mitwirkende
${UI.icon('phone')} 110 Polizei ${UI.icon('first-aid')} 089 19240 Tiergift München ${UI.icon('first-aid')} 030 19240 Tiergift Berlin ${UI.icon('first-aid')} 01 4064343 Tiergift Wien

Standort wird ermittelt…

`; document.getElementById('poison-btn-locate') ?.addEventListener('click', _locateUser); document.getElementById('poison-btn-report') ?.addEventListener('click', _showReportForm); await UI.loadLeaflet(); _initMap(); // Leaflet muss nach CSS-Load die Container-Größe neu berechnen setTimeout(() => _map?.invalidateSize(), 100); await _locateAndLoad(); } // ---------------------------------------------------------- // KARTE INITIALISIEREN // ---------------------------------------------------------- function _initMap() { const mapEl = document.getElementById('poison-map'); if (!mapEl || !window.L || _map) return; _map = L.map('poison-map', { zoomControl: true, attributionControl: false }) .setView([51.1657, 10.4515], 6); // Deutschland-Mitte L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, }).addTo(_map); } // ---------------------------------------------------------- // 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('poison-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('poison-info'); if (!_userPos) { _reports = []; _renderList(); if (infoEl) infoEl.textContent = 'Standort unbekannt — bitte Standort freigeben (📍 Mein Standort).'; return; } try { _reports = await API.poison.listNearby(_userPos.lat, _userPos.lon, 10000); _renderMarkers(); _renderList(); _updateBadge(_reports.length); if (infoEl) { infoEl.textContent = _reports.length > 0 ? `${_reports.length} aktive Meldung${_reports.length !== 1 ? 'en' : ''} im Umkreis von 10 km` : 'Keine aktiven Giftköder-Meldungen in deiner Nähe (10 km Radius).'; } } catch { 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 typ = TYPEN[r.typ] || TYPEN.unbekannt; const distStr = r.distanz_m < 1000 ? `${r.distanz_m} m` : `${(r.distanz_m / 1000).toFixed(1)} km`; const marker = UI.leafletMarker({ lat: r.lat, lon: r.lon, color: typ.color, icon: typ.icon, size: 34 }) .addTo(_map) .bindPopup(` ${typ.icon} ${typ.label}
${r.beschreibung ? UI.escape(r.beschreibung.slice(0, 80)) + '
' : ''} 📍 ${distStr} entfernt
📅 ${_fmtDate(r.created_at)} ${r.bestaetigt ? '
✅ Bestätigt' : ''} `); marker.on('click', () => _openDetail(r)); _markers.push(marker); }); } // ---------------------------------------------------------- // LISTE // ---------------------------------------------------------- function _renderList() { const listEl = document.getElementById('poison-list'); if (!listEl) return; if (_reports.length === 0) { listEl.innerHTML = UI.emptyState({ icon : '✅', title : 'Alles sicher', text : 'In deiner Nähe (10 km) gibt es aktuell keine Giftköder-Meldungen.', action: ``, }); listEl.querySelector('#poison-empty-report') ?.addEventListener('click', _showReportForm); return; } listEl.innerHTML = _reports.map(r => _reportCard(r)).join(''); listEl.querySelectorAll('[data-poison-id]').forEach(card => { card.addEventListener('click', () => { const r = _reports.find(x => x.id === parseInt(card.dataset.poisonId)); if (r) _openDetail(r); }); }); } function _reportCard(r) { const typ = TYPEN[r.typ] || TYPEN.unbekannt; const distStr = r.distanz_m < 1000 ? `${r.distanz_m} m` : `${(r.distanz_m / 1000).toFixed(1)} km`; return `
${typ.icon}
${typ.label} ${r.bestaetigt ? '✅ Bestätigt' : ''} ${distStr}
${r.beschreibung ? `

${UI.escape(r.beschreibung.slice(0, 120))}${r.beschreibung.length > 120 ? '…' : ''}

` : ''}
Gemeldet ${_fmtDate(r.created_at)} · läuft ab ${_fmtDate(r.expires_at)}
${r.foto_url ? `Foto` : ''}
`; } // ---------------------------------------------------------- // DETAIL-MODAL // ---------------------------------------------------------- function _openDetail(r) { const typ = TYPEN[r.typ] || TYPEN.unbekannt; const isOwnEntry = _appState.user && _appState.user.id === r.user_id; const isAdmin = _appState.user?.rolle === 'admin'; const body = ` ${r.foto_url ? `Foto` : ''}
${typ.icon} ${typ.label} ${r.bestaetigt ? '✅ Bestätigt' : ''}
${r.beschreibung ? `

${UI.escape(r.beschreibung)}

` : ''}
📍 ${r.lat.toFixed(5)}, ${r.lon.toFixed(5)}
📅 Gemeldet: ${_fmtDate(r.created_at)}
⏰ Läuft ab: ${_fmtDate(r.expires_at)}
${r.melder_name ? `
👤 Gemeldet von: ${UI.escape(r.melder_name)}
` : ''}
${!r.bestaetigt && _appState.user && !isOwnEntry ? `` : ''} ${isOwnEntry || isAdmin ? `` : ''}
`; UI.modal.open({ title: `${typ.icon} Giftköder-Meldung`, body }); document.getElementById('detail-confirm')?.addEventListener('click', async () => { try { const updated = await API.poison.confirm(r.id); const idx = _reports.findIndex(x => x.id === r.id); if (idx !== -1) _reports[idx] = { ..._reports[idx], ...updated }; UI.toast.success('Meldung bestätigt. Danke!'); UI.modal.close(); _renderMarkers(); _renderList(); } catch (err) { UI.toast.error(err.message || 'Fehler beim Bestätigen.'); } }); document.getElementById('detail-show-map')?.addEventListener('click', () => { UI.modal.close(); if (_map) { _map.setView([r.lat, r.lon], 16); document.getElementById('poison-map') ?.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Popup des Markers öffnen const marker = _markers[_reports.findIndex(x => x.id === r.id)]; marker?.openPopup(); } }); document.getElementById('detail-resolve')?.addEventListener('click', () => { _showResolveDialog(r); }); } // ---------------------------------------------------------- // ERLEDIGT-DIALOG — mit Grundauswahl für KI-Analyse // ---------------------------------------------------------- function _showResolveDialog(r) { UI.modal.open({ title: '✔ Meldung als erledigt markieren', body: `

Die Meldung wird inaktiv gesetzt. Die Daten bleiben für spätere Musteranalysen gespeichert.

`, footer: ` `, }); document.getElementById('resolve-cancel') ?.addEventListener('click', UI.modal.close); document.getElementById('resolve-confirm') ?.addEventListener('click', async () => { const grund = document.getElementById('resolve-grund')?.value || 'beseitigt'; const btn = document.getElementById('resolve-confirm'); await UI.asyncButton(btn, async () => { await API.poison.resolve(r.id, { grund }); _reports = _reports.filter(x => x.id !== r.id); _renderMarkers(); _renderList(); _updateBadge(_reports.length); UI.modal.close(); UI.toast.success('Meldung als erledigt markiert.'); }); }); } // ---------------------------------------------------------- // MELDE-FORMULAR // ---------------------------------------------------------- function _showReportForm() { if (!_appState.user) { UI.toast.warning('Bitte zuerst anmelden, um eine Meldung abzuschicken.'); App.navigate('settings'); return; } const typOpts = Object.entries(TYPEN) .map(([val, { icon, label }]) => ``) .join(''); const body = `
`; const footer = ` `; UI.modal.open({ title: '⚠️ Giftköder melden', body, footer }); // Location-Picker initialisieren + ggf. bekannten Standort vorausfüllen const _picker = UI.locationPicker({ containerId: 'poison-location-picker' }); if (_userPos) { _picker.setValue(_userPos.lat, _userPos.lon, null); } // Foto-Vorschau const photoInput = document.querySelector('#poison-form [name="photo"]'); const photoPreview = document.getElementById('pf-photo-preview'); if (photoInput && photoPreview) { UI.setupPhotoPreview(photoInput, photoPreview); photoInput.addEventListener('change', () => { photoPreview.style.display = photoInput.files[0] ? 'block' : 'none'; }); } document.getElementById('pf-cancel') ?.addEventListener('click', UI.modal.close); // Formular absenden document.getElementById('poison-form')?.addEventListener('submit', async e => { e.preventDefault(); const submitBtn = document.querySelector('[form="poison-form"][type="submit"]') || e.target.querySelector('[type="submit"]'); const fd = UI.formData(e.target); const loc = _picker.getValue(); if (!loc.lat || !loc.lon) { UI.toast.warning('Bitte zuerst den GPS-Standort ermitteln (📍).'); return; } await UI.asyncButton(submitBtn, async () => { const payload = { lat : loc.lat, lon : loc.lon, typ : fd.typ, beschreibung : fd.beschreibung || null, }; const created = await API.poison.report(payload); // Foto hochladen if (photoInput?.files[0]) { try { const formData = new FormData(); formData.append('file', photoInput.files[0]); const media = await API.poison.uploadPhoto(created.id, formData); created.foto_url = media.foto_url; } catch { UI.toast.warning('Meldung erstellt — Foto konnte nicht hochgeladen werden.'); } } // Distanz client-seitig berechnen (für sofortige Anzeige) // _userPos aktualisieren falls Picker neuen Standort geliefert hat if (loc.lat && loc.lon) _userPos = { lat: loc.lat, lon: loc.lon }; 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('Giftköder gemeldet! Danke für die Warnung.'); UI.modal.close(); }); }); } // ---------------------------------------------------------- // BADGE (Sidebar + Bottom-Nav) // ---------------------------------------------------------- function _updateBadge(count) { const b1 = document.getElementById('poison-badge'); const b2 = document.getElementById('poison-nav-badge'); if (b1) { b1.textContent = count; b1.style.display = count > 0 ? '' : 'none'; } if (b2) { b2.textContent = count; b2.classList.toggle('hidden', count === 0); } } // ---------------------------------------------------------- // HELPER // ---------------------------------------------------------- // Haversine client-seitig (für frisch gemeldete Einträge) 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)); } // _fmtDate: bewusst lokal behalten — UI.time.format() liefert langen Monats-Namen // und behandelt kein SQLite-Leerzeichen-Format ("2026-04-12 00:00:00") function _fmtDate(isoStr) { if (!isoStr) return ''; // SQLite speichert "2026-04-12T00:00:00" oder "2026-04-12 00:00:00" const d = new Date(isoStr.replace(' ', 'T')); return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); } // ---------------------------------------------------------- // PUBLIC // ---------------------------------------------------------- return { init, refresh, openNew, openDetail: _openDetail }; })();