/* ============================================================ BAN YARO — Gassi-Treffen Treffen entdecken, erstellen, beitreten ============================================================ */ window.Page_walks = (() => { let _container = null; let _appState = null; let _data = []; let _view = 'liste'; // 'liste' | 'karte' let _map = null; let _markers = []; let _leafletLoaded = false; let _userPos = null; function _esc(s) { return String(s || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } // Datum deutsch formatieren: "2026-04-20" → "Sonntag, 20. April 2026" function _fmtDate(iso) { if (!iso) return '—'; const d = new Date(iso + 'T12:00:00'); return d.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); } // Datum kurz: "So, 20.04." function _fmtDateShort(iso) { if (!iso) return '—'; const d = new Date(iso + 'T12:00:00'); return d.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit' }); } function _isToday(iso) { return iso === new Date().toISOString().slice(0, 10); } function _isPast(iso) { return iso < new Date().toISOString().slice(0, 10); } // ---------------------------------------------------------- // INIT // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; _render(); try { _userPos = await API.getLocation(); } catch {} _loadData(); } function refresh() { _loadData(); } function onDogChange() {} function openNew() { _showCreateForm(); } // ---------------------------------------------------------- // RENDER — Grundstruktur // ---------------------------------------------------------- function _render() { _container.innerHTML = `

Lädt…

`; document.getElementById('walks-view-toggle').addEventListener('click', e => { const btn = e.target.closest('.walks-view-btn'); if (!btn) return; _switchView(btn.dataset.view); }); document.getElementById('walks-create-btn').addEventListener('click', () => { if (!_appState.user) { UI.toast.warning('Bitte zuerst anmelden.'); App.navigate('settings'); return; } _showCreateForm(); }); } function _switchView(view) { _view = view; document.querySelectorAll('.walks-view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view)); document.getElementById('walks-list-view').style.display = view === 'liste' ? '' : 'none'; document.getElementById('walks-map-view').style.display = view === 'karte' ? '' : 'none'; if (view === 'karte') { _loadLeaflet().then(() => { _initMap(); setTimeout(() => _map?.invalidateSize(), 50); setTimeout(() => _map?.invalidateSize(), 300); }); } } // ---------------------------------------------------------- // Daten laden // ---------------------------------------------------------- async function _loadData() { try { _data = await API.walks.list( _userPos?.lat ?? null, _userPos?.lon ?? null ); _renderList(); _renderMarkers(); } catch (err) { UI.toast.error(err.message || 'Fehler beim Laden.'); } } // ---------------------------------------------------------- // Liste rendern // ---------------------------------------------------------- function _renderList() { const el = document.getElementById('walks-list'); if (!el) return; if (!_data.length) { el.innerHTML = `
${UI.icon('dog')}

Noch keine Treffen in deiner Nähe.

`; document.getElementById('walks-first-btn')?.addEventListener('click', _showCreateForm); return; } // Heute + zukünftige Treffen const heute = _data.filter(w => _isToday(w.datum)); const upcoming = _data.filter(w => !_isToday(w.datum) && !_isPast(w.datum)); let html = ''; if (heute.length) { html += `
${UI.icon('star')} Heute
`; html += heute.map(w => _walkCardHTML(w)).join(''); } if (upcoming.length) { html += `
${UI.icon('calendar-dots')} Demnächst
`; html += upcoming.map(w => _walkCardHTML(w)).join(''); } el.innerHTML = `
${html}
`; el.querySelectorAll('.walks-card').forEach(card => { card.addEventListener('click', () => _openDetail(parseInt(card.dataset.id))); }); } function _walkCardHTML(w) { const isOwn = _appState.user?.id === w.user_id; const isFull = w.status === 'voll' || w.teilnehmer_count >= w.max_teilnehmer; const today = _isToday(w.datum); const spots = w.max_teilnehmer - w.teilnehmer_count; return `
${_fmtDateShort(w.datum)}
${w.uhrzeit}
${_esc(w.titel)}
${w.ort_name ? `
${UI.icon('map-pin')} ${_esc(w.ort_name)}
` : ''}
${isFull ? '🔴 Voll' : `🟢 ${spots} Platz${spots !== 1 ? 'e' : ''} frei`} ${UI.icon('paw-print')} ${w.teilnehmer_count}/${w.max_teilnehmer} ${isOwn ? 'Mein Treffen' : ''}
`; } // ---------------------------------------------------------- // Leaflet + Karte // ---------------------------------------------------------- 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; } function _initMap() { const el = document.getElementById('walks-map'); if (!el || !window.L || _map) return; const center = _userPos ? [_userPos.lat, _userPos.lon] : [51.1657, 10.4515]; _map = L.map('walks-map', { zoomControl: true, attributionControl: false }) .setView(center, _userPos ? 12 : 6); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(_map); _renderMarkers(); } function _renderMarkers() { if (!_map || !window.L) return; _markers.forEach(m => m.remove()); _markers = []; _data.forEach(w => { const isFull = w.status === 'voll' || w.teilnehmer_count >= w.max_teilnehmer; const color = _isToday(w.datum) ? 'var(--c-primary)' : (isFull ? '#6B7280' : '#22C55E'); const icon = L.divIcon({ className: '', html: `
${UI.icon('dog')}
`, iconSize: [32, 32], iconAnchor: [16, 16], }); const m = L.marker([w.lat, w.lon], { icon }) .addTo(_map) .bindTooltip(`${w.titel} · ${_fmtDateShort(w.datum)} ${w.uhrzeit}`, { direction: 'top', offset: [0,-16] }) .on('click', () => _openDetail(w.id)); _markers.push(m); }); } // ---------------------------------------------------------- // Detail-Modal // ---------------------------------------------------------- async function _openDetail(walkId) { let walk; try { walk = await API.walks.get(walkId); } catch (err) { UI.toast.error(err.message); return; } const isOwn = _appState.user?.id === walk.user_id; const isJoined = walk.teilnehmer?.some(t => t.user_id === _appState.user?.id); const isFull = walk.status === 'voll' || walk.teilnehmer_count >= walk.max_teilnehmer; const isPast = _isPast(walk.datum); const spots = walk.max_teilnehmer - walk.teilnehmer_count; const teilnehmerHTML = walk.teilnehmer?.length ? walk.teilnehmer.map(t => `
${UI.icon('user')} ${_esc(t.user_name)} ${t.hunde ? `${UI.icon('dog')} ${_esc(t.hunde)}` : ''}
`).join('') : `

Noch keine Teilnehmer.

`; const body = `
${_fmtDate(walk.datum)}
um ${walk.uhrzeit} Uhr
${walk.ort_name ? `
${UI.icon('map-pin')} ${_esc(walk.ort_name)}
` : ''}
${isFull ? '🔴 Voll' : `🟢 ${spots} Platz${spots !== 1 ? 'e' : ''} frei`} ${UI.icon('paw-print')} ${walk.teilnehmer_count}/${walk.max_teilnehmer} Teilnehmer ${isOwn ? 'Dein Treffen' : ''}
${walk.beschreibung ? `

${_esc(walk.beschreibung)}

` : ''}
Teilnehmer
${teilnehmerHTML}

Veranstaltet von ${_esc(walk.veranstalter_name || 'Unbekannt')}

`; let footer; if (isOwn) { footer = ` `; } else if (!_appState.user) { footer = ` `; } else if (isJoined) { footer = ` `; } else if (isPast || isFull) { footer = ``; } else { footer = ` `; } UI.modal.open({ title: `${UI.icon('dog')} ${walk.titel}`, body, footer }); document.getElementById('wd-close')?.addEventListener('click', UI.modal.close); document.getElementById('wd-login')?.addEventListener('click', () => { UI.modal.close(); App.navigate('settings'); }); document.getElementById('wd-edit')?.addEventListener('click', () => { UI.modal.close(); _showEditForm(walk); }); document.getElementById('wd-cancel-walk')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ title: 'Treffen stornieren?', message: 'Alle Teilnehmer werden benachrichtigt. Nicht rückgängig.', confirmText: 'Stornieren', danger: true, }); if (!ok) return; try { await API.walks.cancel(walk.id); _data = _data.filter(w => w.id !== walk.id); UI.modal.close(); _renderList(); _renderMarkers(); UI.toast.success('Treffen storniert.'); } catch (err) { UI.toast.error(err.message); } }); document.getElementById('wd-join')?.addEventListener('click', () => { UI.modal.close(); _showJoinForm(walk); }); document.getElementById('wd-leave')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ title: 'Nicht mehr teilnehmen?', message: `Du verlässt „${walk.titel}".`, confirmText: 'Austreten', }); if (!ok) return; try { const res = await API.walks.leave(walk.id); const idx = _data.findIndex(w => w.id === walk.id); if (idx !== -1) _data[idx].teilnehmer_count = res.teilnehmer_count; UI.modal.close(); _renderList(); UI.toast.success('Du nimmst nicht mehr teil.'); } catch (err) { UI.toast.error(err.message); } }); } // ---------------------------------------------------------- // Beitreten-Formular (Hunde wählen) // ---------------------------------------------------------- function _showJoinForm(walk) { const dogs = _appState.dogs || []; const dogsHtml = dogs.length ? dogs.map(d => ` `).join('') : `

Keine Hunde im Profil — du kannst trotzdem mitmachen.

`; const body = `

${_fmtDate(walk.datum)} um ${walk.uhrzeit} Uhr
${walk.ort_name ? `${UI.icon('map-pin')} ${_esc(walk.ort_name)}` : ''}

${dogsHtml}
`; const footer = ` `; UI.modal.open({ title: `Treffen beitreten`, body, footer }); document.getElementById('join-cancel')?.addEventListener('click', UI.modal.close); document.getElementById('join-confirm')?.addEventListener('click', async () => { const btn = document.getElementById('join-confirm'); const checked = [...document.querySelectorAll('[name="dog"]:checked')]; const dogIds = checked.map(cb => parseInt(cb.value)); await UI.asyncButton(btn, async () => { const res = await API.walks.join(walk.id, dogIds); const idx = _data.findIndex(w => w.id === walk.id); if (idx !== -1) _data[idx].teilnehmer_count = res.teilnehmer_count; UI.modal.close(); _renderList(); _renderMarkers(); UI.toast.success(`Du nimmst teil! 🎉`); }); }); } // ---------------------------------------------------------- // Treffen erstellen // ---------------------------------------------------------- function _showCreateForm(prefill = {}) { const today = new Date().toISOString().slice(0, 10); _showWalkForm(null, { datum: today, uhrzeit: '10:00', ...prefill }); } function _showEditForm(walk) { _showWalkForm(walk); } function _showWalkForm(walk, defaults = {}) { const isEdit = !!walk; const v = walk || defaults; const body = `
${v.lat ? `${UI.icon('check')} Position gespeichert` : 'GPS-Button für aktuellen Standort'}
`; const footer = `
`; UI.modal.open({ title: isEdit ? 'Treffen bearbeiten' : `${UI.icon('dog')} Treffen planen`, body, footer }); document.getElementById('wf-cancel')?.addEventListener('click', UI.modal.close); document.getElementById('walk-gps-btn')?.addEventListener('click', async () => { const btn = document.getElementById('walk-gps-btn'); UI.setLoading(btn, true); try { const pos = await API.getLocation({ enableHighAccuracy: true }); _userPos = pos; document.getElementById('walk-lat').value = pos.lat; document.getElementById('walk-lon').value = pos.lon; document.getElementById('walk-gps-hint').innerHTML = `${UI.icon('check')} Standort ermittelt`; } catch { UI.toast.error('GPS nicht verfügbar.'); } UI.setLoading(btn, false); }); document.getElementById('walk-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = document.querySelector('[form="walk-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 = { titel: fd.titel?.trim(), datum: fd.datum, uhrzeit: fd.uhrzeit, lat: parseFloat(fd.lat), lon: parseFloat(fd.lon), ort_name: fd.ort_name || null, max_teilnehmer: parseInt(fd.max_teilnehmer) || 10, beschreibung: fd.beschreibung || null, }; if (isEdit) { const updated = await API.walks.update(walk.id, payload); const idx = _data.findIndex(w => w.id === walk.id); if (idx !== -1) _data[idx] = { ..._data[idx], ...updated }; UI.toast.success('Treffen aktualisiert.'); } else { const created = await API.walks.create(payload); _data.unshift({ ...created, teilnehmer_count: 0 }); // Beim eigenen neuen Treffen gleich beitreten? // Nein — Veranstalter ist automatisch dabei (für Teilnehmer-Sicht) UI.toast.success('Treffen geplant! 🎉'); } UI.modal.close(); _renderList(); _renderMarkers(); }); }); } return { init, refresh, onDogChange, openNew }; })();