/* ============================================================ BAN YARO — Events (Hundeveranstaltungen) Liste/Karte · Filter · Erstellen/Bearbeiten ============================================================ */ const 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 _view = 'liste'; // liste | karte let _map = null; let _markers = []; // ---------------------------------------------------------- // 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'); } } // ---------------------------------------------------------- // Liste rendern // ---------------------------------------------------------- function _renderList() { const listEl = document.getElementById('ev-list'); if (!listEl) return; const filtered = _filter === 'alle' ? _events : _events.filter(e => e.typ === _filter); 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; if (_view === 'karte') _renderMap(filtered); } 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; return `
${dow} ${day} ${mon}
${UI.escHtml(ev.titel)}
${typ.icon} ${typ.label} ${ev.uhrzeit ? `· ${ev.uhrzeit} Uhr` : ''} ${ev.ort_name ? `· 📍 ${UI.escHtml(ev.ort_name)}` : ''}
${isOwn ? `` : ''}
`; } // ---------------------------------------------------------- // Karte // ---------------------------------------------------------- async function _renderMap(filtered) { const mapEl = document.getElementById('ev-map'); if (!mapEl) return; await _loadLeaflet(); 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); } else { _markers.forEach(m => m.remove()); _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 icon = L.divIcon({ className: '', html: `
${typ.icon}
`, iconSize: [32, 32], iconAnchor: [16, 32], }); const m = L.marker([ev.lat, ev.lon], { icon }) .addTo(_map) .on('click', () => _showDetail(ev.id)); _markers.push(m); bounds.push([ev.lat, ev.lon]); } if (bounds.length) _map.fitBounds(bounds, { padding: [40, 40], maxZoom: 12 }); setTimeout(() => _map.invalidateSize(), 50); } function _loadLeaflet() { if (window.L) return Promise.resolve(); return new Promise((resolve, reject) => { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = '/js/lib/leaflet.css'; document.head.appendChild(link); const s = document.createElement('script'); s.src = '/js/lib/leaflet.js'; s.onload = resolve; s.onerror = reject; 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 body = `
${typ.icon} ${typ.label}
📅 ${datum}${ev.uhrzeit ? ' · ' + ev.uhrzeit + ' Uhr' : ''}
${ev.ort_name ? `
📍 ${UI.escHtml(ev.ort_name)}
` : ''} ${ev.beschreibung ? `
${UI.escHtml(ev.beschreibung)}
` : ''} ${ev.link ? `
🔗 Mehr Infos
` : ''}
Veranstalter: ${UI.escHtml(ev.veranstalter_name || '–')}
`; const footer = isOwn ? ` ` : ''; 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) { // 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)); _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'; const filtered = _filter === 'alle' ? _events : _events.filter(ev => ev.typ === _filter); _renderMap(filtered); } else { listEl.style.display = ''; mapEl.style.display = 'none'; } return; } // Neu-Button if (e.target.closest('#ev-new-btn')) { openNew(); 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 }; })();