Tagebuch — Ort/POI (DayOne-ähnlich):
- diary.location_name Spalte, DiaryCreate/Update mit gps_lat/lon/location_name
- GET /api/dogs/{id}/diary/nearby: Overpass + Nominatim (vor {entry_id}-Route)
- Mini-Karte im Edit-Formular: Leaflet lazy, Edit-Modus, SVG-Pin
- Meilenstein-Toggle: Button statt Checkbox, Filter in Toolbar
- Datenmigration: 97 Ort-Einträge aus text → location_name
Tagebuch — Foto/Video:
- Foto/Video im Edit: Ersetzen + Löschen, DELETE media endpoint
- Media-Picker: Kamera/Mediathek/Datei Buttons
- Video-Wiedergabe (<video controls> in Detail + Edit)
Modal-UX (alle Edit-Karten vereinheitlicht):
- Footer-Pattern: [Speichern vollbreit] / [Löschen][Abbrechen]
- diary, dog-profile, events, health, places, walks, settings, sitting
- Löschen aus Detail-Modal → Edit-Form verschoben
iOS Mobile-Fixes:
- Auto-Zoom: input/select/textarea font-size 16px !important
- Scroll-Through: html.modal-open + touch-action:none auf Overlay
- Kein position:fixed mehr auf body (kein Scroll-Sprung)
PWA & Icons:
- icon-512-any.png + icon-192-any.png (quadratisch, maskable)
- manifest.json: purpose any/maskable getrennt
- Gesundheits-Icon: syringe → first-aid
Import-Fix:
- _HTMLStripper überspringt video/audio/script → kein "Video nicht gefunden" mehr
583 lines
22 KiB
JavaScript
583 lines
22 KiB
JavaScript
/* ============================================================
|
||
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,'>').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 = `
|
||
<div class="walks-layout">
|
||
|
||
<!-- Toolbar -->
|
||
<div class="by-toolbar">
|
||
<div class="walks-view-toggle" id="walks-view-toggle">
|
||
<button class="walks-view-btn active" data-view="liste">${UI.icon('list')} Liste</button>
|
||
<button class="walks-view-btn" data-view="karte">${UI.icon('map-trifold')} Karte</button>
|
||
</div>
|
||
<button class="btn btn-primary btn-sm" id="walks-create-btn">${UI.icon('plus')} Treffen planen</button>
|
||
</div>
|
||
|
||
<!-- Liste -->
|
||
<div id="walks-list-view" class="walks-content">
|
||
<div id="walks-list">
|
||
<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-8)">Lädt…</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Karte -->
|
||
<div id="walks-map-view" class="walks-content" style="display:none">
|
||
<div id="walks-map" class="walks-map"></div>
|
||
</div>
|
||
|
||
</div>
|
||
`;
|
||
|
||
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);
|
||
setTimeout(() => _map?.invalidateSize(), 300);
|
||
});
|
||
}
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// 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 = `
|
||
<div style="text-align:center;padding:var(--space-10) var(--space-4)">
|
||
<div style="font-size:3rem;margin-bottom:var(--space-3)">${UI.icon('dog')}</div>
|
||
<p style="color:var(--c-text-secondary)">Noch keine Treffen in deiner Nähe.</p>
|
||
<button class="btn btn-primary" style="margin-top:var(--space-4)" id="walks-first-btn">
|
||
Erstes Treffen planen
|
||
</button>
|
||
</div>`;
|
||
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 += `<div class="by-section-label">${UI.icon('star')} Heute</div>`;
|
||
html += heute.map(w => _walkCardHTML(w)).join('');
|
||
}
|
||
|
||
if (upcoming.length) {
|
||
html += `<div class="by-section-label">${UI.icon('calendar-dots')} Demnächst</div>`;
|
||
html += upcoming.map(w => _walkCardHTML(w)).join('');
|
||
}
|
||
|
||
el.innerHTML = `<div class="walks-list-inner">${html}</div>`;
|
||
|
||
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 `
|
||
<div class="walks-card ${today ? 'walks-card--today' : ''}" data-id="${w.id}">
|
||
<div class="walks-card-date">
|
||
<div class="walks-card-day">${_fmtDateShort(w.datum)}</div>
|
||
<div class="walks-card-time">${w.uhrzeit}</div>
|
||
</div>
|
||
<div class="walks-card-body">
|
||
<div class="walks-card-title">${_esc(w.titel)}</div>
|
||
${w.ort_name ? `<div class="walks-card-ort">${UI.icon('map-pin')} ${_esc(w.ort_name)}</div>` : ''}
|
||
<div class="walks-card-meta">
|
||
<span class="walks-badge ${isFull ? 'walks-badge--full' : 'walks-badge--open'}">
|
||
${isFull ? '🔴 Voll' : `🟢 ${spots} Platz${spots !== 1 ? 'e' : ''} frei`}
|
||
</span>
|
||
<span class="walks-badge">${UI.icon('paw-print')} ${w.teilnehmer_count}/${w.max_teilnehmer}</span>
|
||
${isOwn ? '<span class="walks-badge walks-badge--own">Mein Treffen</span>' : ''}
|
||
</div>
|
||
</div>
|
||
<div class="walks-card-arrow">›</div>
|
||
</div>`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// 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) ? 'var(--c-primary)' : (isFull ? '#6B7280' : '#22C55E');
|
||
const icon = L.divIcon({
|
||
className: '',
|
||
html: `<div style="background:${color};color:#fff;font-size:14px;font-weight:700;
|
||
width:32px;height:32px;border-radius:50%;display:flex;align-items:center;
|
||
justify-content:center;box-shadow:0 2px 5px rgba(0,0,0,0.3);
|
||
border:2px solid rgba(255,255,255,0.8)">${UI.icon('dog')}</div>`,
|
||
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 => `
|
||
<div class="walks-participant">
|
||
<span class="walks-participant-name">${UI.icon('user')} ${_esc(t.user_name)}</span>
|
||
${t.hunde ? `<span class="walks-participant-hunde">${UI.icon('dog')} ${_esc(t.hunde)}</span>` : ''}
|
||
</div>`).join('')
|
||
: `<p style="color:var(--c-text-muted)">Noch keine Teilnehmer.</p>`;
|
||
|
||
const body = `
|
||
<div class="walks-detail-header">
|
||
<div class="walks-detail-date">
|
||
${_fmtDate(walk.datum)}<br>
|
||
<strong>um ${walk.uhrzeit} Uhr</strong>
|
||
</div>
|
||
${walk.ort_name ? `<div style="margin-top:var(--space-2);color:var(--c-text-secondary)">${UI.icon('map-pin')} ${_esc(walk.ort_name)}</div>` : ''}
|
||
<div style="margin-top:var(--space-2);display:flex;gap:var(--space-2);flex-wrap:wrap">
|
||
<span class="walks-badge ${isFull ? 'walks-badge--full' : 'walks-badge--open'}">
|
||
${isFull ? '🔴 Voll' : `🟢 ${spots} Platz${spots !== 1 ? 'e' : ''} frei`}
|
||
</span>
|
||
<span class="walks-badge">${UI.icon('paw-print')} ${walk.teilnehmer_count}/${walk.max_teilnehmer} Teilnehmer</span>
|
||
${isOwn ? '<span class="walks-badge walks-badge--own">Dein Treffen</span>' : ''}
|
||
</div>
|
||
</div>
|
||
|
||
${walk.beschreibung ? `
|
||
<p style="margin:var(--space-4) 0;color:var(--c-text-secondary)">${_esc(walk.beschreibung)}</p>
|
||
` : ''}
|
||
|
||
<div class="walks-detail-section">
|
||
<div class="walks-detail-section-label">Teilnehmer</div>
|
||
${teilnehmerHTML}
|
||
</div>
|
||
|
||
<p style="color:var(--c-text-muted);font-size:0.8rem;margin-top:var(--space-4)">
|
||
Veranstaltet von ${_esc(walk.veranstalter_name || 'Unbekannt')}
|
||
</p>
|
||
`;
|
||
|
||
let footer;
|
||
if (isOwn) {
|
||
footer = `
|
||
<button type="button" class="btn btn-ghost btn-sm" id="wd-cancel-walk" style="color:var(--c-danger)">Stornieren</button>
|
||
<button type="button" class="btn btn-secondary flex-1" id="wd-edit">Bearbeiten</button>
|
||
<button type="button" class="btn btn-primary flex-1" id="wd-close">Schließen</button>
|
||
`;
|
||
} else if (!_appState.user) {
|
||
footer = `
|
||
<button type="button" class="btn btn-secondary flex-1" id="wd-close">Schließen</button>
|
||
<button type="button" class="btn btn-primary flex-1" id="wd-login">Anmelden zum Beitreten</button>
|
||
`;
|
||
} else if (isJoined) {
|
||
footer = `
|
||
<button type="button" class="btn btn-ghost btn-sm" id="wd-leave" style="color:var(--c-danger)">Nicht mehr teilnehmen</button>
|
||
<button type="button" class="btn btn-primary flex-1" id="wd-close">Schließen</button>
|
||
`;
|
||
} else if (isPast || isFull) {
|
||
footer = `<button type="button" class="btn btn-primary flex-1" id="wd-close">Schließen</button>`;
|
||
} else {
|
||
footer = `
|
||
<button type="button" class="btn btn-secondary flex-1" id="wd-close">Schließen</button>
|
||
<button type="button" class="btn btn-primary flex-1" id="wd-join">${UI.icon('dog')} Mitmachen</button>
|
||
`;
|
||
}
|
||
|
||
UI.modal.open({ title: `${UI.icon('dog')} ${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 => `
|
||
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer;
|
||
padding:var(--space-2) 0">
|
||
<input type="checkbox" name="dog" value="${d.id}" checked>
|
||
${UI.icon('dog')} ${_esc(d.name)}
|
||
</label>`).join('')
|
||
: `<p style="color:var(--c-text-muted)">Keine Hunde im Profil — du kannst trotzdem mitmachen.</p>`;
|
||
|
||
const body = `
|
||
<p style="color:var(--c-text-secondary);margin-bottom:var(--space-4)">
|
||
${_fmtDate(walk.datum)} um ${walk.uhrzeit} Uhr<br>
|
||
${walk.ort_name ? `${UI.icon('map-pin')} ${_esc(walk.ort_name)}` : ''}
|
||
</p>
|
||
<div class="form-group">
|
||
<label class="form-label">Mit welchen Hunden?</label>
|
||
${dogsHtml}
|
||
</div>
|
||
`;
|
||
|
||
const footer = `
|
||
<button type="button" class="btn btn-secondary flex-1" id="join-cancel">Abbrechen</button>
|
||
<button type="button" class="btn btn-primary flex-1" id="join-confirm">${UI.icon('dog')} Mitmachen</button>
|
||
`;
|
||
|
||
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 = `
|
||
<form id="walk-form" autocomplete="off">
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Titel *</label>
|
||
<input class="form-control" type="text" name="titel"
|
||
value="${_esc(v.titel || '')}"
|
||
placeholder="z. B. Sonntagsspaziergang im Stadtpark" required>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||
<div class="form-group">
|
||
<label class="form-label">Datum *</label>
|
||
<input class="form-control" type="date" name="datum"
|
||
value="${_esc(v.datum || '')}" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Uhrzeit *</label>
|
||
<input class="form-control" type="time" name="uhrzeit"
|
||
value="${_esc(v.uhrzeit || '10:00')}" required>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Treffpunkt</label>
|
||
<div style="display:flex;gap:var(--space-2)">
|
||
<input class="form-control" type="text" name="ort_name"
|
||
value="${_esc(v.ort_name || '')}"
|
||
placeholder="z. B. Parkeingang Nordseite, U-Bahn Volkspark"
|
||
style="flex:1">
|
||
<button type="button" class="btn btn-secondary" id="walk-gps-btn" title="GPS">${UI.icon('map-pin')}</button>
|
||
</div>
|
||
<input type="hidden" name="lat" id="walk-lat" value="${v.lat || ''}">
|
||
<input type="hidden" name="lon" id="walk-lon" value="${v.lon || ''}">
|
||
<small id="walk-gps-hint" style="color:var(--c-text-secondary)">
|
||
${v.lat ? `${UI.icon('check')} Position gespeichert` : 'GPS-Button für aktuellen Standort'}
|
||
</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Max. Teilnehmer</label>
|
||
<input class="form-control" type="number" name="max_teilnehmer"
|
||
value="${v.max_teilnehmer || 10}" min="2" max="50">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Beschreibung <span style="color:var(--c-text-secondary)">(optional)</span></label>
|
||
<textarea class="form-control" name="beschreibung" rows="3"
|
||
placeholder="Treffpunkt-Details, Streckenlänge, Hundefreundlichkeit…">${_esc(v.beschreibung || '')}</textarea>
|
||
</div>
|
||
|
||
</form>
|
||
`;
|
||
|
||
const footer = `
|
||
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%">
|
||
<button type="submit" form="walk-form" class="btn btn-primary" style="width:100%">
|
||
${isEdit ? 'Speichern' : `${UI.icon('calendar-dots')} Treffen planen`}
|
||
</button>
|
||
<button type="button" class="btn btn-secondary" id="wf-cancel">Abbrechen</button>
|
||
</div>
|
||
`;
|
||
|
||
UI.modal.open({ title: isEdit ? 'Treffen bearbeiten' : `${UI.icon('dog')} 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').innerHTML = `${UI.icon('check')} 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 };
|
||
|
||
})();
|