/* ============================================================ BAN YARO — Orte (Hundefreundliche Orte) Karte + Liste, Eigene Orte anlegen/bearbeiten ============================================================ */ window.Page_places = (() => { let _container = null; let _appState = null; let _map = null; let _markers = []; let _data = []; let _activeTyp = null; // null = alle let _leafletLoaded = false; let _userPos = null; // ---------------------------------------------------------- // Typen-Konfiguration // ---------------------------------------------------------- const TYPEN = { restaurant: { icon: '', label: 'Restaurant & Café', color: '#F97316' }, freilauf: { icon: '', label: 'Freilauffläche', color: '#22C55E' }, shop: { icon: '', label: 'Shop', color: '#3B82F6' }, kotbeutel: { icon: '', label: 'Kotbeutel-Station', color: '#84A98C' }, tierarzt: { icon: '', label: 'Tierarzt', color: '#EF4444' }, hundeschule: { icon: '', label: 'Hundeschule', color: '#8B5CF6' }, }; function _esc(s) { return String(s || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } // ---------------------------------------------------------- // INIT // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; _render(); _loadData(); try { _userPos = await API.getLocation(); } catch {} } function refresh() { _loadData(); } function onDogChange() {} // ---------------------------------------------------------- // RENDER — Grundstruktur // ---------------------------------------------------------- function _render() { _container.innerHTML = `
${Object.entries(TYPEN).map(([k, t]) => `` ).join('')}

Lädt…

`; // Events document.getElementById('places-filter').addEventListener('click', e => { const btn = e.target.closest('.places-filter-btn'); if (!btn) return; document.querySelectorAll('.places-filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); _activeTyp = btn.dataset.typ || null; _applyFilter(); }); document.getElementById('places-add-btn').addEventListener('click', () => { if (!_appState.user) { UI.toast.warning('Bitte zuerst anmelden.'); App.navigate('settings'); return; } _showForm(null); }); _loadLeaflet().then(_initMap); } // ---------------------------------------------------------- // Leaflet laden (wie poison.js) // ---------------------------------------------------------- async function _loadLeaflet() { if (_leafletLoaded || window.L) { _leafletLoaded = true; return; } const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = '/css/leaflet.css'; document.head.appendChild(link); await new Promise(resolve => { const s = document.createElement('script'); s.src = '/js/leaflet.js'; s.onload = resolve; document.head.appendChild(s); }); _leafletLoaded = true; } // ---------------------------------------------------------- // Karte initialisieren // ---------------------------------------------------------- function _initMap() { const el = document.getElementById('places-map'); if (!el || !window.L || _map) return; const center = _userPos ? [_userPos.lat, _userPos.lon] : [51.1657, 10.4515]; const zoom = _userPos ? 13 : 6; _map = L.map('places-map', { zoomControl: true, attributionControl: false }) .setView(center, zoom); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }) .addTo(_map); // GPS-Locate-Button L.Control.Locate = L.Control.extend({ onAdd() { const btn = L.DomUtil.create('button', 'places-locate-btn'); btn.innerHTML = ''; btn.title = 'Meinen Standort'; btn.onclick = async () => { try { const pos = await API.getLocation({ enableHighAccuracy: true }); _userPos = pos; _map.setView([pos.lat, pos.lon], 14); } catch { UI.toast.error('Standort konnte nicht ermittelt werden.'); } }; return btn; }, onRemove() {}, }); new L.Control.Locate({ position: 'bottomright' }).addTo(_map); _renderMarkers(); } // ---------------------------------------------------------- // Daten laden // ---------------------------------------------------------- async function _loadData() { try { _data = await API.places.list(); _renderList(); _renderMarkers(); } catch (err) { UI.toast.error(err.message || 'Fehler beim Laden der Orte.'); } } // ---------------------------------------------------------- // Filter anwenden // ---------------------------------------------------------- function _filtered() { return _activeTyp ? _data.filter(p => p.typ === _activeTyp) : _data; } function _applyFilter() { _renderList(); _renderMarkers(); } // ---------------------------------------------------------- // Marker rendern // ---------------------------------------------------------- function _renderMarkers() { if (!_map || !window.L) return; _markers.forEach(m => m.remove()); _markers = []; _filtered().forEach(place => { const t = TYPEN[place.typ] || { icon: '', color: '#6B7280' }; const icon = L.divIcon({ className: '', html: `
${t.icon}
`, iconSize: [34, 34], iconAnchor: [17, 17], }); const marker = L.marker([place.lat, place.lon], { icon }) .addTo(_map) .on('click', () => _openDetail(place)); _markers.push(marker); }); } // ---------------------------------------------------------- // Liste rendern // ---------------------------------------------------------- function _renderList() { const list = document.getElementById('places-list'); if (!list) return; const items = _filtered(); if (!items.length) { list.innerHTML = `

${_activeTyp ? 'Keine Orte in dieser Kategorie.' : 'Noch keine Orte eingetragen.'}

`; return; } list.innerHTML = `
${items.map(p => _cardHTML(p)).join('')}
`; list.querySelectorAll('.places-card').forEach(card => { const id = parseInt(card.dataset.id); const place = _data.find(p => p.id === id); if (place) card.addEventListener('click', () => _openDetail(place)); }); } function _cardHTML(p) { const t = TYPEN[p.typ] || { icon: '', label: p.typ, color: '#6B7280' }; const flags = [ p.hund_rein === true ? `${UI.icon('dog')} Hund rein` : null, p.leine_pflicht === true ? `${UI.icon('tag')} Leinenpflicht` : null, p.wasser_fuer_hunde === true ? `${UI.icon('drop')} Wasser` : null, ].filter(Boolean); return `
${t.icon}
${_esc(p.name)}
${t.label} ${p.adresse ? `· ${_esc(p.adresse)}` : ''}
${flags.length ? `
${flags.map(f => `${f}`).join('')}
` : ''}
${UI.icon('arrow-right')}
`; } // ---------------------------------------------------------- // Detail-Modal // ---------------------------------------------------------- function _openDetail(place) { const t = TYPEN[place.typ] || { icon: '', label: place.typ, color: '#6B7280' }; const isOwn = _appState.user?.id === place.user_id; const flags = [ place.hund_rein === true ? `${UI.icon('dog')} Hund erlaubt` : (place.hund_rein === false ? `${UI.icon('x')} Kein Hund` : null), place.leine_pflicht === true ? `${UI.icon('tag')} Leinenpflicht` : (place.leine_pflicht === false ? `${UI.icon('check')} Leine optional` : null), place.wasser_fuer_hunde === true ? `${UI.icon('drop')} Wasser vorhanden`: null, ].filter(Boolean); const body = `
${t.icon}
${_esc(place.name)}
${t.label}
${place.adresse ? `

${UI.icon('map-pin')} ${_esc(place.adresse)}

` : ''} ${place.telefon ? `

${UI.icon('phone')} ${_esc(place.telefon)}

` : ''} ${place.website ? `

${UI.icon('arrow-square-out')} ${_esc(place.website)}

` : ''} ${flags.length ? `
${flags.map(f => `${f}`).join('')}
` : ''}

Eingetragen von ${_esc(place.user_name || 'Unbekannt')}

`; const footer = isOwn ? ` ` : ` `; UI.modal.open({ title: `${t.icon} ${_esc(place.name)}`, body, footer }); document.getElementById('place-detail-close')?.addEventListener('click', UI.modal.close); document.getElementById('place-detail-edit')?.addEventListener('click', () => { UI.modal.close(); _showForm(place); }); document.getElementById('place-detail-delete')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ title: 'Ort löschen?', message: `„${place.name}" wird dauerhaft entfernt.`, confirmText: 'Löschen', danger: true, }); if (!ok) return; try { await API.places.delete(place.id); _data = _data.filter(p => p.id !== place.id); UI.modal.close(); _renderList(); _renderMarkers(); UI.toast.success('Ort gelöscht.'); } catch (err) { UI.toast.error(err.message || 'Fehler beim Löschen.'); } }); // Auf Karte zentrieren if (_map) _map.setView([place.lat, place.lon], 15); } // ---------------------------------------------------------- // Formular — Ort anlegen / bearbeiten // ---------------------------------------------------------- function _showForm(place) { const isEdit = !!place; const typOpts = Object.entries(TYPEN) .map(([k, t]) => ``) .join(''); const body = `
${place ? 'Position gespeichert' : 'GPS-Button drücken oder Standort ermitteln'}
`; const footer = ` `; UI.modal.open({ title: isEdit ? `${_esc(place.name)} bearbeiten` : ' Neuer Ort', body, footer }); document.getElementById('place-form-cancel')?.addEventListener('click', UI.modal.close); // GPS-Button document.getElementById('pf-gps-btn')?.addEventListener('click', async () => { const btn = document.getElementById('pf-gps-btn'); UI.setLoading(btn, true); try { const pos = await API.getLocation({ enableHighAccuracy: true }); _userPos = pos; document.getElementById('pf-lat').value = pos.lat; document.getElementById('pf-lon').value = pos.lon; document.getElementById('pf-lat-disp').value = pos.lat.toFixed(6); document.getElementById('pf-lon-disp').value = pos.lon.toFixed(6); document.getElementById('pf-gps-hint').textContent = 'Standort ermittelt'; } catch { UI.toast.error('GPS nicht verfügbar.'); } UI.setLoading(btn, false); }); document.getElementById('place-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = document.querySelector('[form="place-form"][type="submit"]') || e.target.querySelector('[type="submit"]'); const fd = UI.formData(e.target); if (!fd.lat || !fd.lon) { UI.toast.warning('Bitte GPS-Position ermitteln.'); return; } await UI.asyncButton(btn, async () => { const payload = { name: fd.name?.trim(), typ: fd.typ, lat: parseFloat(fd.lat), lon: parseFloat(fd.lon), adresse: fd.adresse || null, website: fd.website || null, telefon: fd.telefon || null, hund_rein: 'hund_rein' in fd, leine_pflicht: 'leine_pflicht' in fd, wasser_fuer_hunde: 'wasser_fuer_hunde' in fd, }; if (isEdit) { const updated = await API.places.update(place.id, payload); const idx = _data.findIndex(p => p.id === place.id); if (idx !== -1) _data[idx] = updated; UI.toast.success('Gespeichert.'); } else { const created = await API.places.create(payload); _data.unshift(created); UI.toast.success('Ort hinzugefügt!'); } UI.modal.close(); _renderList(); _renderMarkers(); }); }); } return { init, refresh, onDogChange }; })();