/* ============================================================ BAN YARO — Events (Hundeveranstaltungen) Liste/Karte · Filter · Erstellen/Bearbeiten ============================================================ */ window.Page_events = (() => { // ---------------------------------------------------------- // Konstanten // ---------------------------------------------------------- const TYPEN = [ { id: 'alle', label: 'Alle', icon: 'ticket' }, { id: 'ausstellung', label: 'Ausstellung', icon: 'trophy' }, { id: 'training', label: 'Training', icon: 'graduation-cap'}, { id: 'treffen', label: 'Treffen', icon: 'dog' }, { id: 'markt', label: 'Markt', icon: 'shopping-bag' }, { id: 'wettkampf', label: 'Wettkampf', icon: 'medal' }, { id: 'sonstiges', label: 'Sonstiges', icon: 'push-pin' }, ]; 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 _search = ''; let _view = 'liste'; // liste | karte let _map = null; let _markers = []; let _clusterGroup = null; let _myRsvp = {}; // { [event_id]: 'going'|'maybe'|null } // ---------------------------------------------------------- // Phosphor-Icon-Helper // ---------------------------------------------------------- function _icon(name) { return ``; } // _emptyState ersetzt durch UI.emptyState() // ---------------------------------------------------------- // 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); // Suche mit Debounce let _searchTimer = null; document.getElementById('ev-search')?.addEventListener('input', e => { clearTimeout(_searchTimer); _searchTimer = setTimeout(() => { _search = e.target.value.trim().toLowerCase(); if (_view === 'karte') { _renderMap(_filtered()); } else { _renderList(); } }, 300); }); } // ---------------------------------------------------------- // 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); } if (_search) { const q = _search; evs = evs.filter(e => (e.titel || '').toLowerCase().includes(q) || (e.ort_name || '').toLowerCase().includes(q) || (e.beschreibung|| '').toLowerCase().includes(q) ); } 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: UI.icon('calendar-blank'), title: _search ? 'Keine Events gefunden' : 'Keine Events in der Nähe', text: _search ? `Keine Events passen zu „${UI.escape(_search)}".` : 'Hier erscheinen Hundeveranstaltungen, Treffen und Aktivitäten in deiner Umgebung.', }); 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.escape(ev.titel)} ${isVdh ? `VDH` : ''}
${UI.icon(typ.icon)} ${typ.label} ${ev.uhrzeit ? `· ${_icon('clock')} ${ev.uhrzeit} Uhr` : ''} ${ev.ort_name ? `· ${_icon('map-pin')} ${UI.escape(ev.ort_name)}` : ''}
${ev.rsvp_count ? `${_icon('users')} ${ev.rsvp_count} nehmen teil` : ''} ${ev.link ? `` : ''}
${isOwn ? `` : ''} ${_state.user ? `` : ''}
`; } // ---------------------------------------------------------- // Karte // ---------------------------------------------------------- async function _renderMap(filtered) { const mapEl = document.getElementById('ev-map'); if (!mapEl) return; await UI.loadLeaflet(true); // true = mit MarkerCluster 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' }); // Events nutzen rotierten Diamant-Marker (nicht Kreis) — UI.leafletMarker() nicht anwendbar const icon = L.divIcon({ className: '', html: `
`, iconSize: [32, 32], iconAnchor: [16, 32], }); const popup = `
${UI.escape(ev.titel)}
${datum}
${ev.ort_name ? `📍 ${UI.escape(ev.ort_name)}
` : ''} ${ev.beschreibung ? `${UI.escape(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); } // _loadLeaflet und _loadMarkerCluster ersetzt durch UI.loadLeaflet(true) // ---------------------------------------------------------- // 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 myRsvp = _myRsvp[id] ?? null; // RSVP-Bar (nur für eingeloggte User) const rsvpBar = _state.user ? `
${ev.rsvp_count ? `${_icon('users')} ${ev.rsvp_count} nehmen teil` : ``}
` : (ev.rsvp_count ? `
${_icon('users')} ${ev.rsvp_count} nehmen teil
` : ''); const body = `
${UI.icon(typ.icon)} ${typ.label} ${isVdh ? `VDH` : ''}
${_icon('calendar-dots')} ${datum}${ev.uhrzeit ? ' · ' + ev.uhrzeit + ' Uhr' : ''}
${ev.ort_name ? `
${_icon('map-pin')} ${UI.escape(ev.ort_name)}
` : ''} ${ev.beschreibung ? `
${UI.escape(ev.beschreibung)}
` : ''} ${ev.link ? `
${_icon('arrow-square-out')} Mehr Infos
` : ''}
${_icon('user')} Veranstalter: ${UI.escape(ev.veranstalter_name || '–')}
${rsvpBar}
`; const footer = isOwn ? ` ` : (ev.link ? ` ${_icon('arrow-square-out')} Zur Veranstaltung ` : ''); UI.modal.open({ title: UI.escape(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)); // RSVP-Buttons document.querySelectorAll(`[data-rsvp-id="${id}"]`).forEach(btn => { btn.addEventListener('click', () => _handleRsvp(id, btn.dataset.rsvpStatus)); }); } async function _handleRsvp(eventId, status) { const current = _myRsvp[eventId] ?? null; try { if (current === status) { // Toggle off → absagen await API.events.cancelRsvp(eventId); _myRsvp[eventId] = null; } else { const res = await API.events.rsvp(eventId, status); _myRsvp[eventId] = status; // Teilnehmerzähler aktualisieren _updateAttendeeCount(eventId, res.rsvp_count); } // Button-Styles aktualisieren document.querySelectorAll(`[data-rsvp-id="${eventId}"]`).forEach(btn => { btn.classList.toggle('active', btn.dataset.rsvpStatus === (_myRsvp[eventId] ?? '')); }); // Bei Absage Zähler neu laden if (current === status) { const attendees = await API.events.listRsvp(eventId); const goingCount = attendees.filter(a => a.status === 'going').length; _updateAttendeeCount(eventId, goingCount); } } catch (e) { UI.toast(e.message, 'error'); } } function _updateAttendeeCount(eventId, count) { // Im Modal const span = document.getElementById(`ev-attendees-${eventId}`); if (span) { if (count > 0) { span.innerHTML = `${_icon('users')} ${count} nehmen teil`; span.style.display = ''; } else { span.style.display = 'none'; } } // In der Listenansicht (Event-Objekt aktualisieren) const ev = _events.find(x => x.id === eventId); if (ev) { ev.rsvp_count = count; // Karte neu rendern falls sichtbar const card = document.querySelector(`[data-ev-id="${eventId}"]`); if (card) card.outerHTML = _cardHTML(ev); } } async function _showAttendees(eventId) { const panel = document.getElementById(`ev-attendees-panel-${eventId}`); if (!panel) return; if (panel.dataset.loaded) { panel.innerHTML = ''; delete panel.dataset.loaded; return; } try { const attendees = await API.events.listRsvp(eventId); if (!attendees.length) { panel.innerHTML = '

Noch keine Zusagen.

'; } else { panel.innerHTML = `
${attendees.map(a => ` ${a.status === 'going' ? _icon('check-circle') : _icon('question')} ${UI.escape(a.name)} `).join('')}
`; } panel.dataset.loaded = '1'; } catch { /* ignore */ } } 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 = `
${isEdit ? `` : ''}
`; UI.modal.open({ title: isEdit ? 'Event bearbeiten' : 'Neues Event', body, footer }); document.getElementById('ev-form-delete')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ title: 'Event löschen?', message: 'Nicht rückgängig.', confirmText: 'Löschen', danger: true, }); if (ok) await _deleteEvent(ev); }); // Location-Picker initialisieren const _picker = UI.locationPicker({ containerId: 'ev-location-picker', }); if (ev?.lat && ev?.lon) { _picker.setValue(ev.lat, ev.lon, ev.ort_name || null); } 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 loc = _picker.getValue(); const data = { titel: fd.get('titel'), datum: fd.get('datum'), uhrzeit: fd.get('uhrzeit') || null, typ: fd.get('typ'), ort_name: loc.name || fd.get('ort_name') || null, lat: loc.lat || null, lon: loc.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; } // Teilnehmer-Liste anzeigen (Karten-Ansicht oder Modal) const attendeesBtn = e.target.closest('[data-ev-attendees]'); if (attendeesBtn) { e.stopPropagation(); _showAttendees(parseInt(attendeesBtn.dataset.evAttendees)); return; } // Notiz-Button const noteBtn = e.target.closest('.ev-note-btn'); if (noteBtn) { e.stopPropagation(); _openNoteModal( 'event', parseInt(noteBtn.dataset.evNoteId), noteBtn.dataset.evNoteLabel, noteBtn.dataset.evNoteOrt || null ); return; } // Karten-Klick → Detail const card = e.target.closest('[data-ev-id]'); if (card) { _showDetail(parseInt(card.dataset.evId)); } } // ---------------------------------------------------------- // Notiz-Modal (Custom DOM — kein UI.modal um Konflikte zu vermeiden) // ---------------------------------------------------------- async function _openNoteModal(parentType, parentId, parentLabel, locationName) { let existingNote = null; try { existingNote = await API.notes.get(parentType, String(parentId)); } catch {} const ovl = document.createElement('div'); ovl.style.cssText = 'position:fixed;inset:0;z-index:1200;background:rgba(0,0,0,0.55);display:flex;align-items:flex-end;justify-content:center'; ovl.innerHTML = `
Notiz — ${UI.escape(parentLabel)}
`; document.body.appendChild(ovl); const close = () => ovl.remove(); ovl.querySelector('#ev-note-close')?.addEventListener('click', close); ovl.querySelector('#ev-note-cancel')?.addEventListener('click', close); ovl.addEventListener('click', e => { if (e.target === ovl) close(); }); ovl.querySelector('#ev-note-save')?.addEventListener('click', async () => { const text = ovl.querySelector('#ev-note-text')?.value?.trim() || ''; const payload = { text, parent_label: parentLabel, location_name: locationName || null }; try { if (existingNote?.id) { await API.notes.update(existingNote.id, payload); } else { await API.notes.create(parentType, String(parentId), payload); } UI.toast.success('Notiz gespeichert.'); close(); } catch (err) { UI.toast.error(err.message || 'Fehler beim Speichern.'); } }); } return { init, refresh, openNew, _openDetail: _showDetail }; })();