/* ============================================================ BAN YARO — Gassi-Treffen Treffen entdecken, erstellen, beitreten ============================================================ */ window.Page_walks = (() => { // ---------------------------------------------------------- // OFFLINE-CACHE // ---------------------------------------------------------- const _CACHE_KEY = 'by_walks_cache'; const _PENDING_KEY = 'by_walks_pending'; function _getPending() { try { return JSON.parse(localStorage.getItem(_PENDING_KEY) || '[]'); } catch { return []; } } function _setPending(list) { try { localStorage.setItem(_PENDING_KEY, JSON.stringify(list)); } catch {} } function _addPending(data) { const list = _getPending(); const entry = { ...data, id: `pending_${Date.now()}`, _isPending: true, created_at: new Date().toISOString(), teilnehmer_count: 1, max_teilnehmer: data.max_teilnehmer || 10, status: 'open' }; list.push(entry); _setPending(list); return entry; } async function _syncPending() { if (!navigator.onLine) return; const list = _getPending(); if (!list.length) return; let ok = 0; for (const item of [...list]) { try { const { id: _pid, _isPending, created_at: _ca, teilnehmer_count: _tc, status: _st, ...payload } = item; await API.walks.create(payload); _setPending(_getPending().filter(x => x.id !== item.id)); ok++; } catch {} } if (ok > 0) { UI.toast.success(`${ok} Treffen synchronisiert.`); _loadData(); } } window.addEventListener('online', _syncPending); let _container = null; let _appState = null; let _data = []; let _view = 'liste'; // 'liste' | 'karte' let _tab = 'treffen'; // 'treffen' | 'challenge' | 'stamm' let _map = null; let _markers = []; let _userPos = null; let _challengeData = null; let _gassiZeiten = []; // _esc ersetzt durch UI.escape() // Datum deutsch formatieren: "2026-04-20" → "Sonntag, 20. April 2026" // Hinweis: UI.time.format() liefert kein weekday — daher lokale Funktion beibehalten 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." — UI.time.formatShort() gibt "20. Apr." ohne Wochentag // Hinweis: Format nicht äquivalent zu UI.time.formatShort() — daher lokal beibehalten 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); } function _sourceIcon(source) { if (source === 'places') return 'star'; if (source === 'osm') return 'map-pin'; return 'map-trifold'; } // ---------------------------------------------------------- // INIT // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; _render(); // Desktop: Leaflet sofort laden damit Karte bereit ist wenn Daten kommen if (window.innerWidth >= 1024) UI.loadLeaflet(); try { _userPos = await API.getLocation(); } catch {} _loadData(); } function refresh() { _loadData(); if (_tab === 'challenge') _loadChallenge(); if (_tab === 'stamm') _loadGassiZeiten(); } function onDogChange() {} function openNew() { if (_tab === 'challenge') { _showSubmitForm(); return; } if (_tab === 'stamm') { _showGassiZeitForm(); return; } _showCreateForm(); } // ---------------------------------------------------------- // RENDER — Grundstruktur // ---------------------------------------------------------- function _render() { _container.innerHTML = `

Lädt…

`; // Tab-Bar Events document.getElementById('walks-tab-bar').addEventListener('click', e => { const btn = e.target.closest('.by-tab'); if (!btn) return; _switchTab(btn.dataset.tab); }); 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(); }); document.getElementById('gassi-zeit-add-btn').addEventListener('click', () => { if (!_appState.user) { UI.toast.warning('Bitte zuerst anmelden.'); App.navigate('settings'); return; } _showGassiZeitForm(); }); } function _switchTab(tab) { _tab = tab; document.querySelectorAll('.by-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === tab)); document.querySelectorAll('.walks-tab-panel').forEach(p => p.style.display = 'none'); const panel = document.getElementById(`walks-tab-${tab}`); if (panel) panel.style.display = ''; if (tab === 'challenge' && !_challengeData) _loadChallenge(); if (tab === 'stamm' && !_gassiZeiten.length) _loadGassiZeiten(); } function _switchView(view) { _view = view; document.querySelectorAll('.walks-view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view)); // Desktop: beide Panels bleiben via CSS sichtbar if (window.innerWidth >= 1024) return; document.getElementById('walks-list-view').style.display = view === 'liste' ? '' : 'none'; document.getElementById('walks-map-view').style.display = view === 'karte' ? '' : 'none'; if (view === 'karte') { _initMap().then(() => { setTimeout(() => _map?.invalidateSize(), 150); setTimeout(() => _map?.invalidateSize(), 400); }); } } // ---------------------------------------------------------- // Daten laden // ---------------------------------------------------------- async function _loadData() { const pending = _getPending(); try { const fetched = await API.walks.list( _userPos?.lat ?? null, _userPos?.lon ?? null ); try { localStorage.setItem(_CACHE_KEY, JSON.stringify({ ts: Date.now(), data: fetched })); } catch {} _data = [...pending, ...fetched]; _renderList(); _renderMarkers(); if (window.innerWidth >= 1024) { _initMap().then(() => { setTimeout(() => _map?.invalidateSize(), 150); setTimeout(() => _map?.invalidateSize(), 400); }); } } catch { try { const raw = localStorage.getItem(_CACHE_KEY); if (raw) { _data = [...pending, ...(JSON.parse(raw).data || [])]; _renderList(); _renderMarkers(); UI.toast.info('Offline — zeige zuletzt geladene Treffen.'); return; } } catch {} _data = pending; if (pending.length) { _renderList(); _renderMarkers(); return; } UI.toast.error('Treffen konnten nicht geladen werden.'); } } // ---------------------------------------------------------- // 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))); }); el.querySelectorAll('.wk-note-btn').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); UI.noteModal( 'walk', parseInt(btn.dataset.wkNoteId), btn.dataset.wkNoteLabel, btn.dataset.wkNoteOrt || null ); }); }); } 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}
${UI.escape(w.titel)}
${w.ort_name ? `
${UI.icon('map-pin')} ${UI.escape(w.ort_name)}
` : ''}
${isFull ? '🔴 Voll' : `🟢 ${spots} Platz${spots !== 1 ? 'e' : ''} frei`} ${UI.icon('paw-print')} ${w.teilnehmer_count}/${w.max_teilnehmer} ${isOwn ? 'Mein Treffen' : ''} ${w._isPending ? `⏳ Sync ausstehend` : ''}
${_appState.user ? `` : ''}
`; } // ---------------------------------------------------------- // Leaflet + Karte // ---------------------------------------------------------- async function _initMap() { const el = document.getElementById('walks-map'); if (!el || _map) return; const center = _userPos ? [_userPos.lat, _userPos.lon] : [51.1657, 10.4515]; _map = await UI.map.create('walks-map', { center, zoom: _userPos ? 12 : 6 }); _renderMarkers(); } function _renderMarkers() { if (!_map || !window.L) return; _markers.forEach(m => m.remove()); _markers = []; _data.forEach(w => { if (!w.lat || !w.lon) return; const isFull = w.status === 'voll' || w.teilnehmer_count >= w.max_teilnehmer; const color = _isToday(w.datum) ? 'var(--c-primary)' : (isFull ? '#6B7280' : '#22C55E'); const m = UI.leafletMarker({ lat: w.lat, lon: w.lon, color, icon: UI.icon('dog') }) .addTo(_map) .bindTooltip(`${w.titel} · ${_fmtDateShort(w.datum)} ${w.uhrzeit}`, { direction: 'top', offset: [0,-16] }) .on('click', () => _openDetail(w.id)); _markers.push(m); }); // Karte auf alle Marker zoomen damit alle Treffen sichtbar sind if (_markers.length === 1) { _map.setView(_markers[0].getLatLng(), 13); } else if (_markers.length > 1) { const group = L.featureGroup(_markers); _map.fitBounds(group.getBounds().pad(0.2)); } } // ---------------------------------------------------------- // RSVP-Status → Label + Farbe // ---------------------------------------------------------- function _rsvpBadge(status) { if (status === 'yes') return `Zusage`; if (status === 'maybe') return `Vielleicht`; if (status === 'no') return `Absage`; return `Eingeladen`; } function _avatarInitials(name) { const parts = (name || '?').trim().split(/\s+/); const initials = parts.length >= 2 ? parts[0][0] + parts[parts.length - 1][0] : parts[0].slice(0, 2); return initials.toUpperCase(); } function _invitationRowHTML(inv) { return `
${_avatarInitials(inv.user_name)}
${UI.escape(inv.user_name)}
${inv.hunde ? `
${UI.icon('dog')} ${UI.escape(inv.hunde)}
` : ''}
${_rsvpBadge(inv.status)}
`; } // ---------------------------------------------------------- // Detail-Modal // ---------------------------------------------------------- async function _openDetail(walkId) { let walk, participantData; try { walk = await API.walks.get(walkId); if (_appState.user) { try { participantData = await API.walks.participants(walkId); } catch {} } } 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 myRsvp = participantData?.my_rsvp ?? null; const isInvited = !!myRsvp; const invitations = participantData?.invitations ?? []; // Teilnehmerliste mit Hundefotos const teilnehmerHTML = walk.teilnehmer?.length ? walk.teilnehmer.map(t => { const dogsHTML = (t.hunde_liste || []).map(d => { const av = d.foto_url ? `${UI.escape(d.name)}` : `
`; return `
${av} ${UI.escape(d.name)}${d.rasse ? ` · ${UI.escape(d.rasse)}` : ''}
`; }).join(''); return `
${_avatarInitials(t.user_name)}
${UI.escape(t.user_name)}
${dogsHTML ? `
${dogsHTML}
` : ''}
`; }).join('') : ''; // Einladungsliste const invListHTML = invitations.length ? invitations.map(inv => _invitationRowHTML(inv)).join('') : `

Noch keine Einladungen.

`; // RSVP-Section für eingeladene Nutzer const rsvpSectionHTML = (isInvited && !isOwn) ? `
${UI.icon('check-circle')} Deine Antwort
` : ''; const body = `
${_fmtDate(walk.datum)}
um ${walk.uhrzeit} Uhr
${walk.ort_name ? `
${UI.icon('map-pin')} ${UI.escape(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 ? `

${UI.escape(walk.beschreibung)}

` : ''} ${rsvpSectionHTML}
${isOwn && !isPast ? `` : ''}
${invListHTML}
${walk.teilnehmer?.length ? `
${UI.icon('check-circle')} Beigetreten (${walk.teilnehmer_count}/${walk.max_teilnehmer})
${teilnehmerHTML}
` : ''}
${UI.icon('star')} Bewertung
${(isPast || _isToday(walk.datum)) && (isJoined || isOwn) ? ` ` : ''}
${(walk.photos || []).length === 0 ? `

Noch keine Fotos.

` : (walk.photos || []).map(p => `
${p.user_id === _appState.user?.id || isOwn ? ` ` : ''}
`).join('')}

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

${isOwn && !isPast ? `
` : ''} ${isJoined && !isOwn ? `
` : ''} `; 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 }); // Bewertungskomponente initialisieren (nur nach abgelaufenem Treffen sinnvoll, aber immer anzeigen) UI.ratingStars({ containerId: `wd-rating-${walk.id}`, targetType: 'walk', targetId: walk.id, isLoggedIn: !!_appState.user, }); document.getElementById('wd-close')?.addEventListener('click', UI.modal.close); // Foto-Upload document.getElementById('wd-photo-input')?.addEventListener('change', async function() { if (!this.files.length) return; const file = this.files[0]; // Client-Side-Kompression vor Upload (HEIC bleibt unverändert) const toUpload = await API.compressImage(file); const formData = new FormData(); formData.append('file', toUpload); try { const photo = await API.walks.uploadPhoto(walk.id, formData); const grid = document.getElementById('wd-photos-grid'); if (grid) { grid.querySelector('p')?.remove(); const div = document.createElement('div'); div.style.cssText = 'position:relative;aspect-ratio:1'; div.innerHTML = ` `; grid.appendChild(div); _bindPhotoDel(walk.id, div); UI.toast.success('Foto hochgeladen.'); } } catch (err) { UI.toast.error(err.message || 'Fehler beim Hochladen.'); } this.value = ''; }); // Foto löschen — alle bestehenden Buttons function _bindPhotoDel(walkId, container) { container.querySelectorAll('.wd-photo-del').forEach(btn => { btn.addEventListener('click', async () => { if (!window.confirm('Foto löschen?')) return; try { await API.walks.deletePhoto(walkId, parseInt(btn.dataset.photoId)); btn.closest('[style*="aspect-ratio"]')?.remove(); UI.toast.success('Foto gelöscht.'); } catch (err) { UI.toast.error(err.message || 'Fehler.'); } }); }); } _bindPhotoDel(walk.id, document); document.getElementById('wd-login')?.addEventListener('click', () => { UI.modal.close(); App.navigate('settings'); }); document.getElementById('wd-edit')?.addEventListener('click', () => { UI.modal.close(); _showEditForm(walk); }); // Einladen-Button document.getElementById('wd-invite-btn')?.addEventListener('click', () => { UI.modal.close(); _showInviteModal(walk); }); // RSVP-Buttons document.querySelectorAll('.walks-rsvp-btn').forEach(btn => { btn.addEventListener('click', async () => { const status = btn.dataset.rsvp; try { await API.walks.rsvp(walk.id, status); // Buttons aktualisieren document.querySelectorAll('.walks-rsvp-btn').forEach(b => { b.classList.toggle('active', b.dataset.rsvp === status); }); UI.toast.success( status === 'yes' ? 'Zugesagt!' : status === 'maybe' ? 'Antwort: Vielleicht.' : 'Abgesagt.' ); } catch (err) { UI.toast.error(err.message); } }); }); // Stornieren: Zwei-Klick-Pattern (kein UI.modal.confirm im Modal) let _cancelPending = false; document.getElementById('wd-cancel-walk')?.addEventListener('click', async () => { const btn = document.getElementById('wd-cancel-walk'); if (!_cancelPending) { _cancelPending = true; btn.textContent = 'Wirklich stornieren? (nochmal tippen)'; btn.style.fontWeight = 'var(--weight-semibold)'; setTimeout(() => { _cancelPending = false; if (btn) { btn.innerHTML = `${UI.icon('x-circle')} Treffen stornieren`; btn.style.fontWeight = ''; } }, 3000); return; } _cancelPending = false; 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); }); // Austreten: Zwei-Klick-Pattern let _leavePending = false; document.getElementById('wd-leave')?.addEventListener('click', async () => { const btn = document.getElementById('wd-leave'); if (!_leavePending) { _leavePending = true; btn.textContent = 'Wirklich austreten? (nochmal tippen)'; btn.style.fontWeight = 'var(--weight-semibold)'; setTimeout(() => { _leavePending = false; if (btn) { btn.innerHTML = `${UI.icon('sign-out')} Nicht mehr teilnehmen`; btn.style.fontWeight = ''; } }, 3000); return; } _leavePending = false; 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); } }); } // ---------------------------------------------------------- // Freunde einladen // ---------------------------------------------------------- async function _showInviteModal(walk) { let candidates; try { candidates = await API.walks.inviteCandidates(walk.id); } catch (err) { UI.toast.error(err.message); return; } const listHTML = candidates.length ? candidates.map(f => `
${_avatarInitials(f.friend_name)}
${UI.escape(f.friend_name)}
`).join('') : `

Alle Freunde wurden bereits eingeladen.

`; const body = `

${_fmtDate(walk.datum)} · ${walk.uhrzeit} Uhr ${walk.ort_name ? `· ${UI.escape(walk.ort_name)}` : ''}

${listHTML}
`; const footer = ` `; UI.modal.open({ title: `${UI.icon('user-plus')} Freunde einladen`, body, footer }); document.getElementById('invite-back')?.addEventListener('click', () => { UI.modal.close(); _openDetail(walk.id); }); document.querySelectorAll('.walks-invite-send').forEach(btn => { btn.addEventListener('click', async () => { const row = btn.closest('.walks-invite-row'); const friendId = parseInt(row.dataset.friendId); const name = row.dataset.friendName; await UI.asyncButton(btn, async () => { await API.walks.invite(walk.id, friendId); row.innerHTML = `
${_avatarInitials(name)}
${UI.escape(name)}
Eingeladen `; UI.toast.success(`${name} eingeladen.`); }); }); }); } // ---------------------------------------------------------- // 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')} ${UI.escape(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-form')?.addEventListener('submit', async e => { e.preventDefault(); 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 / bearbeiten — Formular // ---------------------------------------------------------- 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; // Location-State (verwaltet außerhalb des DOM) let _locLat = v.lat != null ? parseFloat(v.lat) : null; let _locLon = v.lon != null ? parseFloat(v.lon) : null; let _locName = v.ort_name || null; const body = `
`; const footer = `
`; UI.modal.open({ title: isEdit ? `${UI.icon('pencil-simple')} Treffen bearbeiten` : `${UI.icon('dog')} Treffen planen`, body, footer }); document.getElementById('wf-cancel')?.addEventListener('click', UI.modal.close); // Location Picker let _wfPicker = null; setTimeout(() => { _wfPicker = UI.locationPicker({ containerId: 'wf-location-picker', onSelect: (lat, lon, name) => { _locLat = lat; _locLon = lon; _locName = name; }, }); if (_locLat != null) _wfPicker.setValue(_locLat, _locLon, _locName); }, 50); // Formular absenden 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); // Koordinaten aus State lesen (nicht aus fd, da hidden) const lat = _locLat; const lon = _locLon; if (!lat || !lon) { UI.toast.warning('Bitte einen Treffpunkt auf der Karte wählen oder GPS nutzen.'); return; } await UI.asyncButton(btn, async () => { const payload = { titel: fd.titel?.trim(), datum: fd.datum, uhrzeit: fd.uhrzeit, lat: parseFloat(lat), lon: parseFloat(lon), ort_name: _locName || 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.'); UI.modal.close(); _renderList(); _renderMarkers(); } else { let created; try { created = await API.walks.create(payload); } catch (netErr) { if (netErr instanceof TypeError || !navigator.onLine) { _addPending(payload); UI.modal.close(); UI.toast.success('Offline gespeichert — wird synchronisiert sobald Verbindung besteht.'); _loadData(); return; } throw netErr; } if (created?._queued) { UI.modal.close(); _loadData(); return; } _data.unshift({ ...created, teilnehmer_count: 0 }); UI.toast.success('Treffen geplant! 🎉'); UI.modal.close(); _renderList(); _renderMarkers(); } }); }); } // ---------------------------------------------------------- // Notiz-Modal (Custom DOM — kein UI.modal um Konflikte zu vermeiden) // ---------------------------------------------------------- // ============================================================== // FEATURE 1: Foto-Challenge der Woche // ============================================================== async function _loadChallenge() { const el = document.getElementById('challenge-content'); if (!el) return; try { _challengeData = await API.get('challenges/current'); _renderChallenge(); } catch (e) { el.innerHTML = `

Konnte Challenge nicht laden.

`; } } function _renderChallenge() { const el = document.getElementById('challenge-content'); if (!el || !_challengeData) return; const { challenge, submissions, my_submission_id, days_left } = _challengeData; const canSubmit = _appState.user && !my_submission_id; const dayLabel = days_left === 1 ? '1 Tag' : `${days_left} Tage`; el.innerHTML = `
${UI.escape(challenge.thema)}
${UI.icon('calendar')} ${_fmtDate(challenge.start_date)} – ${_fmtDate(challenge.end_date)}  ·  ${UI.icon('timer')} Noch ${dayLabel}
${canSubmit ? `` : ''} ${my_submission_id ? `${UI.icon('check')} Du hast bereits teilgenommen` : ''}

Letzte Gewinner

Lädt…

${UI.icon('images')} Einreichungen dieser Woche (${submissions.length})

`; // Submit-Button const submitBtn = document.getElementById('challenge-submit-btn'); if (submitBtn) submitBtn.addEventListener('click', _showSubmitForm); // Vote-Buttons el.querySelectorAll('.challenge-vote-btn').forEach(btn => { btn.addEventListener('click', async () => { if (!_appState.user) { UI.toast.warning('Bitte anmelden.'); return; } const subId = parseInt(btn.dataset.id); try { const res = await API.post(`challenges/submissions/${subId}/vote`, {}); btn.querySelector('.vote-count').textContent = res.votes; btn.classList.toggle('voted', res.voted); } catch (e) { UI.toast.error(e.message || 'Fehler beim Abstimmen.'); } }); }); // Gewinner laden _loadChallengeWinners(); } function _challengeSubmissionCard(s) { const voted = s.i_voted; return `
Challenge-Foto
${UI.icon('user')} ${UI.escape(s.user_name || 'Anonym')} ${s.dog_name ? ` · 🐕 ${UI.escape(s.dog_name)}` : ''}
${s.caption ? `
${UI.escape(s.caption)}
` : ''}
`; } async function _loadChallengeWinners() { const el = document.getElementById('challenge-winners-list'); if (!el) return; try { const winners = await API.get('challenges/winners'); if (!winners.length) { el.innerHTML = '

Noch keine vergangenen Challenges.

'; return; } el.innerHTML = `
` + winners.map(w => { if (!w.winner) return `
${UI.escape(w.challenge.thema)}Kein Gewinner
`; return `
Gewinner
${UI.escape(w.challenge.thema)}
${UI.escape(w.winner.user_name)} · ${w.winner.votes} ❤️
`; }).join('') + `
`; } catch {} } async function _showSubmitForm() { if (!_challengeData) return; const dogs = _appState.dogs || []; const dogOptions = dogs.map(d => ``).join(''); UI.modal.open({ title: `📸 ${UI.escape(_challengeData.challenge.thema)}`, body: `
${dogs.length ? `
` : ''}
`, footer: ` `, }); document.getElementById('challenge-submit-ok').addEventListener('click', async () => { const fotoInput = document.getElementById('challenge-foto-input'); if (!fotoInput.files.length) { UI.toast.warning('Bitte ein Foto auswählen.'); return; } const caption = document.getElementById('challenge-caption')?.value?.trim() || ''; const dogSelect = document.getElementById('challenge-dog-select'); const dogId = dogSelect ? (parseInt(dogSelect.value) || '') : ''; const fd = new FormData(); fd.append('foto', fotoInput.files[0]); if (caption) fd.append('caption', caption); if (dogId) fd.append('dog_id', dogId); try { await API.upload(`challenges/${_challengeData.challenge.id}/submit`, fd); UI.toast.success('Foto eingereicht! Viel Erfolg 🎉'); UI.modal.close(); _challengeData = null; _loadChallenge(); } catch (e) { UI.toast.error(e.message || 'Fehler beim Einreichen.'); } }); } // ============================================================== // FEATURE 2: Gassi-Zeiten-Pool (Stamm-Gassis) // ============================================================== const _WOCHENTAGE = [ { key: 'mo', label: 'Mo' }, { key: 'di', label: 'Di' }, { key: 'mi', label: 'Mi' }, { key: 'do', label: 'Do' }, { key: 'fr', label: 'Fr' }, { key: 'sa', label: 'Sa' }, { key: 'so', label: 'So' }, ]; async function _loadGassiZeiten() { const el = document.getElementById('gassi-zeiten-content'); if (!el) return; try { const params = _userPos ? `?lat=${_userPos.lat}&lon=${_userPos.lon}&radius=10000` : ''; _gassiZeiten = await API.get(`gassi-zeiten${params}`); _renderGassiZeiten(); } catch (e) { el.innerHTML = `

Konnte Gassi-Zeiten nicht laden.

`; } } function _renderGassiZeiten() { const el = document.getElementById('gassi-zeiten-content'); if (!el) return; if (!_gassiZeiten.length) { el.innerHTML = `
${UI.icon('clock')}

Noch keine Stamm-Gassi-Zeiten in deiner Nähe.

Trag deine regelmäßigen Zeiten ein — andere finden dich dann!

`; return; } const myZeiten = _gassiZeiten.filter(z => z.is_mine); const andereZeiten = _gassiZeiten.filter(z => !z.is_mine); let html = ''; if (myZeiten.length) { html += `
Meine Zeiten
`; html += myZeiten.map(z => _gassiZeitCard(z)).join(''); } if (andereZeiten.length) { html += `
In deiner Nähe
`; html += andereZeiten.map(z => _gassiZeitCard(z)).join(''); } el.innerHTML = html; // Events el.querySelectorAll('.gz-delete-btn').forEach(btn => { btn.addEventListener('click', async () => { if (!confirm('Gassi-Zeit löschen?')) return; try { await API.del(`gassi-zeiten/${btn.dataset.id}`); _gassiZeiten = _gassiZeiten.filter(z => z.id !== parseInt(btn.dataset.id)); _renderGassiZeiten(); UI.toast.success('Gelöscht.'); } catch (e) { UI.toast.error(e.message || 'Fehler.'); } }); }); el.querySelectorAll('.gz-toggle-btn').forEach(btn => { btn.addEventListener('click', async () => { const gz = _gassiZeiten.find(z => z.id === parseInt(btn.dataset.id)); if (!gz) return; try { const updated = await API.patch(`gassi-zeiten/${gz.id}`, { aktiv: gz.aktiv ? 0 : 1 }); const idx = _gassiZeiten.findIndex(z => z.id === gz.id); if (idx !== -1) _gassiZeiten[idx] = { ..._gassiZeiten[idx], aktiv: updated.aktiv }; _renderGassiZeiten(); } catch (e) { UI.toast.error(e.message || 'Fehler.'); } }); }); el.querySelectorAll('.gz-chat-btn').forEach(btn => { btn.addEventListener('click', () => { const userId = parseInt(btn.dataset.userId); App.navigate('chat', { user_id: userId }); }); }); } function _gassiZeitCard(z) { const wochentageLabel = (z.wochentage || []).join(', ').toUpperCase(); const distLabel = z.distance_m != null ? `${z.distance_m < 1000 ? z.distance_m + ' m' : (z.distance_m/1000).toFixed(1) + ' km'}` : ''; const pausedStyle = z.aktiv ? '' : 'opacity:.5'; return `
${z.dog_foto_url ? `${UI.escape(z.dog_name || '')}` : `
${UI.icon('paw-print')}
`}
${UI.escape(z.dog_name || z.user_name || '?')} ${z.dog_rasse ? `${UI.escape(z.dog_rasse)}` : ''} ${!z.aktiv ? `Pausiert` : ''}
${UI.icon('clock')} ${UI.escape(z.uhrzeit)}  ·  ${wochentageLabel} ${z.ort_name ? ` ·  ${UI.icon('map-pin')} ${UI.escape(z.ort_name)}` : ''} ${distLabel}
${z.notiz ? `
${UI.escape(z.notiz)}
` : ''}
${z.is_mine ? ` ` : ` `}
`; } async function _showGassiZeitForm() { const dogs = _appState.dogs || []; const dogOptions = dogs.map(d => ``).join(''); const wdBtns = _WOCHENTAGE.map(w => `` ).join(''); UI.modal.open({ title: `${UI.icon('clock')} Stamm-Gassi-Zeit eintragen`, body: `
${dogs.length ? `
` : ''}
${wdBtns}
`, footer: ` `, }); document.getElementById('gz-save-btn').addEventListener('click', async () => { const uhrzeit = document.getElementById('gz-uhrzeit')?.value; if (!uhrzeit) { UI.toast.warning('Bitte Uhrzeit angeben.'); return; } const wochentage = Array.from(document.querySelectorAll('.wd-btn input:checked')).map(cb => cb.value); if (!wochentage.length) { UI.toast.warning('Bitte mindestens einen Wochentag wählen.'); return; } const dogId = parseInt(document.getElementById('gz-dog-select')?.value) || null; const ortName = document.getElementById('gz-ort-name')?.value?.trim() || null; const notiz = document.getElementById('gz-notiz')?.value?.trim() || null; const payload = { wochentage, uhrzeit, ort_name: ortName, notiz }; if (dogId) payload.dog_id = dogId; if (_userPos) { payload.lat = _userPos.lat; payload.lon = _userPos.lon; } try { const created = await API.post('gassi-zeiten', payload); _gassiZeiten.unshift({ ...created }); _renderGassiZeiten(); UI.toast.success('Gassi-Zeit eingetragen! 🐾'); UI.modal.close(); } catch (e) { UI.toast.error(e.message || 'Fehler beim Speichern.'); } }); } return { init, refresh, onDogChange, openNew, openDetail: _openDetail }; })();