/* ============================================================
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 = `
`;
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);
});
}
}
// ----------------------------------------------------------
// 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 = `
🐕
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 += `🌟 Heute
`;
html += heute.map(w => _walkCardHTML(w)).join('');
}
if (upcoming.length) {
html += `📅 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 ? `
📍 ${_esc(w.ort_name)}
` : ''}
${isFull ? '🔴 Voll' : `🟢 ${spots} Platz${spots !== 1 ? 'e' : ''} frei`}
🐾 ${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) ? '#C4843A' : (isFull ? '#6B7280' : '#22C55E');
const icon = L.divIcon({
className: '',
html: `🐕
`,
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 => `
🧑 ${_esc(t.user_name)}
${t.hunde ? `🐕 ${_esc(t.hunde)}` : ''}
`).join('')
: `Noch keine Teilnehmer.
`;
const body = `
${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: `🐕 ${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 ? `📍 ${_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 = `
`;
const footer = `
`;
UI.modal.open({ title: isEdit ? 'Treffen bearbeiten' : '🐕 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').textContent = '✅ 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 };
})();