/* ============================================================ BAN YARO — Events (Hundeveranstaltungen) Liste/Karte · Filter · Erstellen/Bearbeiten ============================================================ */ window.Page_events = (() => { // ---------------------------------------------------------- // Konstanten // ---------------------------------------------------------- const TYPEN = [ { id: 'alle', label: 'Alle', icon: '🎪' }, { id: 'ausstellung', label: 'Ausstellung', icon: '🏆' }, { id: 'training', label: 'Training', icon: '🎓' }, { id: 'treffen', label: 'Treffen', icon: '🐕' }, { id: 'markt', label: 'Markt', icon: '🛍️' }, { id: 'wettkampf', label: 'Wettkampf', icon: '🥇' }, { id: 'sonstiges', label: 'Sonstiges', icon: '📌' }, ]; const TYP_COLOR = { ausstellung: '#8b5cf6', training: '#3b82f6', treffen: '#10b981', markt: '#f59e0b', wettkampf: '#ef4444', sonstiges: '#6b7280', }; // ---------------------------------------------------------- // State // ---------------------------------------------------------- let _container = null; let _state = null; let _events = []; let _filter = 'alle'; let _quellFilter = 'alle'; // 'alle' | 'vdh' | 'nutzer' let _view = 'liste'; // liste | karte let _map = null; let _markers = []; let _clusterGroup = null; // ---------------------------------------------------------- // Phosphor-Icon-Helper // ---------------------------------------------------------- function _icon(name) { return ``; } // ---------------------------------------------------------- // init // ---------------------------------------------------------- async function init(container, appState) { _container = container; _state = appState; _render(); await _load(); } async function refresh() { await _load(); } // ---------------------------------------------------------- // Render Grundstruktur // ---------------------------------------------------------- function _render() { _container.innerHTML = `
${_state.user ? `` : ''}
${TYPEN.map(t => ` `).join('')}
`; _container.addEventListener('click', _onClick); } // ---------------------------------------------------------- // Daten laden // ---------------------------------------------------------- async function _load() { const listEl = document.getElementById('ev-list'); if (!listEl) return; listEl.innerHTML = UI.skeleton(3); try { _events = await API.events.list(); _renderList(); } catch (e) { UI.toast(e.message, 'error'); } } // ---------------------------------------------------------- // Gefilterte Events ermitteln // ---------------------------------------------------------- function _filtered() { let evs = _filter === 'alle' ? _events : _events.filter(e => e.typ === _filter); if (_quellFilter !== 'alle') { evs = evs.filter(e => (e.quelle || 'nutzer') === _quellFilter); } return evs; } // ---------------------------------------------------------- // Liste rendern // ---------------------------------------------------------- function _renderList() { const listEl = document.getElementById('ev-list'); if (!listEl) return; const filtered = _filtered(); if (!filtered.length) { listEl.innerHTML = UI.emptyState({ icon: '🎪', title: 'Keine Events', text: 'Noch keine Veranstaltungen geplant.' }); return; } // Monats-Gruppierung const groups = {}; for (const ev of filtered) { const d = new Date(ev.datum + 'T00:00:00'); const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; const label = d.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }); if (!groups[key]) groups[key] = { label, items: [] }; groups[key].items.push(ev); } let html = ''; for (const key of Object.keys(groups).sort()) { const g = groups[key]; html += `
${g.label}
`; for (const ev of g.items) { html += _cardHTML(ev); } } listEl.innerHTML = html; } function _cardHTML(ev) { const d = new Date(ev.datum + 'T00:00:00'); const dow = d.toLocaleDateString('de-DE', { weekday: 'short' }); const day = d.getDate(); const mon = d.toLocaleDateString('de-DE', { month: 'short' }); const typ = TYPEN.find(t => t.id === ev.typ) || TYPEN[TYPEN.length - 1]; const color = TYP_COLOR[ev.typ] || '#6b7280'; const isOwn = _state.user?.id === ev.user_id; const isVdh = ev.quelle === 'vdh'; return `
${dow} ${day} ${mon}
${UI.escHtml(ev.titel)} ${isVdh ? `VDH` : ''}
${typ.icon} ${typ.label} ${ev.uhrzeit ? `· ${_icon('clock')} ${ev.uhrzeit} Uhr` : ''} ${ev.ort_name ? `· ${_icon('map-pin')} ${UI.escHtml(ev.ort_name)}` : ''}
${ev.link ? `` : ''}
${isOwn ? `` : ''}
`; } // ---------------------------------------------------------- // Karte // ---------------------------------------------------------- async function _renderMap(filtered) { const mapEl = document.getElementById('ev-map'); if (!mapEl) return; await _loadLeaflet(); await _loadMarkerCluster(); if (!_map) { _map = L.map('ev-map', { zoomControl: true }).setView([51.1657, 10.4515], 6); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(_map); } // Cluster-Gruppe aufräumen und neu befüllen if (_clusterGroup) { _map.removeLayer(_clusterGroup); } _clusterGroup = L.markerClusterGroup(); _markers = []; const bounds = []; for (const ev of filtered) { if (!ev.lat || !ev.lon) continue; const color = TYP_COLOR[ev.typ] || '#6b7280'; const typ = TYPEN.find(t => t.id === ev.typ) || TYPEN[TYPEN.length - 1]; const d = new Date(ev.datum + 'T00:00:00'); const datum = d.toLocaleDateString('de-DE', { day: 'numeric', month: 'short', year: 'numeric' }); const icon = L.divIcon({ className: '', html: `
${typ.icon}
`, iconSize: [32, 32], iconAnchor: [16, 32], }); const popup = `
${UI.escHtml(ev.titel)}
${datum}
${ev.ort_name ? `📍 ${UI.escHtml(ev.ort_name)}
` : ''} ${ev.beschreibung ? `${UI.escHtml(ev.beschreibung.slice(0, 80))}${ev.beschreibung.length > 80 ? '…' : ''}
` : ''} Details
`; const m = L.marker([ev.lat, ev.lon], { icon }).bindPopup(popup); _clusterGroup.addLayer(m); _markers.push(m); bounds.push([ev.lat, ev.lon]); } _map.addLayer(_clusterGroup); if (bounds.length) { _map.fitBounds(bounds, { padding: [40, 40], maxZoom: 12 }); } else { // Versuche Nutzerstandort, sonst Deutschland-Übersicht try { const pos = await API.getLocation({ timeout: 5000 }); _map.setView([pos.lat, pos.lon], 10); } catch { _map.setView([51.1657, 10.4515], 6); } } _map.invalidateSize(); setTimeout(() => _map.invalidateSize(), 100); } function _loadLeaflet() { if (window.L) return Promise.resolve(); return new Promise((resolve, reject) => { const cssLoaded = document.querySelector('link[href*="leaflet"]') ? Promise.resolve() : new Promise(res => { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = '/css/leaflet.css'; link.onload = res; link.onerror = res; document.head.appendChild(link); }); cssLoaded.then(() => { if (document.querySelector('script[src*="leaflet.js"]')) { resolve(); return; } const s = document.createElement('script'); s.src = '/js/leaflet.js'; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); }); } function _loadMarkerCluster() { if (window.L && L.markerClusterGroup) return Promise.resolve(); return new Promise((resolve, reject) => { const cssLoaded = document.querySelector('link[href*="MarkerCluster"]') ? Promise.resolve() : new Promise(res => { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = '/css/MarkerCluster.css'; link.onload = res; link.onerror = res; document.head.appendChild(link); const link2 = document.createElement('link'); link2.rel = 'stylesheet'; link2.href = '/css/MarkerCluster.Default.css'; link2.onload = res; link2.onerror = res; document.head.appendChild(link2); }); cssLoaded.then(() => { if (document.querySelector('script[src*="markercluster"]') || document.querySelector('script[src*="MarkerCluster"]')) { resolve(); return; } const s = document.createElement('script'); s.src = '/js/leaflet.markercluster.js'; s.onload = resolve; s.onerror = resolve; // Cluster ist optional — graceful degradation document.head.appendChild(s); }); }); } // ---------------------------------------------------------- // Detail-Modal // ---------------------------------------------------------- async function _showDetail(id) { let ev; try { ev = await API.events.get(id); } catch { return; } const typ = TYPEN.find(t => t.id === ev.typ) || TYPEN[TYPEN.length - 1]; const color = TYP_COLOR[ev.typ] || '#6b7280'; const d = new Date(ev.datum + 'T00:00:00'); const datum = d.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); const isOwn = _state.user?.id === ev.user_id; const isVdh = ev.quelle === 'vdh'; const body = `
${typ.icon} ${typ.label} ${isVdh ? `VDH` : ''}
${_icon('calendar-dots')} ${datum}${ev.uhrzeit ? ' · ' + ev.uhrzeit + ' Uhr' : ''}
${ev.ort_name ? `
${_icon('map-pin')} ${UI.escHtml(ev.ort_name)}
` : ''} ${ev.beschreibung ? `
${UI.escHtml(ev.beschreibung)}
` : ''} ${ev.link ? `
${_icon('arrow-square-out')} Mehr Infos
` : ''}
${_icon('user')} Veranstalter: ${UI.escHtml(ev.veranstalter_name || '–')}
`; const footer = isOwn ? ` ` : (ev.link ? ` ${_icon('arrow-square-out')} Zur Veranstaltung ` : ''); UI.modal.open({ title: UI.escHtml(ev.titel), body, footer }); document.getElementById('ev-detail-edit')?.addEventListener('click', () => { UI.modal.close(); setTimeout(() => _openForm(ev), 50); }); document.getElementById('ev-detail-del')?.addEventListener('click', () => _deleteEvent(ev)); } async function _deleteEvent(ev) { if (!confirm(`"${ev.titel}" wirklich löschen?`)) return; try { await API.events.delete(ev.id); UI.modal.close(); UI.toast('Event gelöscht.'); await _load(); } catch (e) { UI.toast(e.message, 'error'); } } // ---------------------------------------------------------- // Erstellen / Bearbeiten // ---------------------------------------------------------- function openNew() { _openForm(null); } function _openForm(ev) { const isEdit = !!ev; const id = 'ev-form'; const body = `
`; const footer = ` `; UI.modal.open({ title: isEdit ? 'Event bearbeiten' : 'Neues Event', body, footer }); document.getElementById('ev-gps-btn')?.addEventListener('click', async () => { try { const pos = await API.getLocation(); document.getElementById('ev-lat').value = pos.lat.toFixed(6); document.getElementById('ev-lon').value = pos.lon.toFixed(6); } catch { UI.toast('GPS nicht verfügbar.', 'error'); } }); const form = document.getElementById(id); const submitBtn = document.querySelector(`[form="${id}"][type="submit"]`) || form.querySelector('[type="submit"]'); form.addEventListener('submit', async e => { e.preventDefault(); const fd = new FormData(form); const data = { titel: fd.get('titel'), datum: fd.get('datum'), uhrzeit: fd.get('uhrzeit') || null, typ: fd.get('typ'), ort_name: fd.get('ort_name') || null, lat: fd.get('lat') ? parseFloat(fd.get('lat')) : null, lon: fd.get('lon') ? parseFloat(fd.get('lon')) : null, beschreibung: fd.get('beschreibung') || null, link: fd.get('link') || null, }; if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = '…'; } try { isEdit ? await API.events.update(ev.id, data) : await API.events.create(data); UI.modal.close(); UI.toast(isEdit ? 'Event aktualisiert.' : 'Event erstellt!'); await _load(); } catch (err) { UI.toast(err.message, 'error'); } finally { if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = isEdit ? 'Speichern' : 'Event erstellen'; } } }); } // ---------------------------------------------------------- // Click-Handler // ---------------------------------------------------------- function _onClick(e) { // Quelle-Filter const sourceBtn = e.target.closest('[data-ev-quelle]'); if (sourceBtn) { _quellFilter = sourceBtn.dataset.evQuelle; document.querySelectorAll('[data-ev-quelle]').forEach(b => b.classList.toggle('active', b.dataset.evQuelle === _quellFilter)); if (_view === 'karte') { _renderMap(_filtered()); } else { _renderList(); } return; } // Typ-Filter const filterBtn = e.target.closest('[data-ev-typ]'); if (filterBtn) { _filter = filterBtn.dataset.evTyp; document.querySelectorAll('[data-ev-typ]').forEach(b => b.classList.toggle('active', b.dataset.evTyp === _filter)); if (_view === 'karte') { _renderMap(_filtered()); } else { _renderList(); } return; } // View-Toggle const viewBtn = e.target.closest('[data-ev-view]'); if (viewBtn) { _view = viewBtn.dataset.evView; document.querySelectorAll('[data-ev-view]').forEach(b => b.classList.toggle('active', b.dataset.evView === _view)); const listEl = document.getElementById('ev-list'); const mapEl = document.getElementById('ev-map'); if (_view === 'karte') { listEl.style.display = 'none'; mapEl.style.display = 'block'; // Erst div sichtbar machen, dann Karte initialisieren _renderMap(_filtered()); } else { // Karte sauber entfernen if (_map) { _map.remove(); _map = null; _clusterGroup = null; _markers = []; } mapEl.style.display = 'none'; listEl.style.display = ''; _renderList(); } return; } // Neu-Button if (e.target.closest('#ev-new-btn')) { openNew(); return; } // Externer Link — nicht als Karten-Klick behandeln if (e.target.closest('.ev-ext-link')) return; // Bearbeiten-Icon auf Karte const editBtn = e.target.closest('[data-ev-edit]'); if (editBtn) { e.stopPropagation(); const id = parseInt(editBtn.dataset.evEdit); const ev = _events.find(x => x.id === id); if (ev) _openForm(ev); return; } // Karten-Klick → Detail const card = e.target.closest('[data-ev-id]'); if (card) { _showDetail(parseInt(card.dataset.evId)); } } return { init, refresh, openNew, _openDetail: _showDetail }; })();