/* ============================================================ BAN YARO — Hundesitting Sitter suchen · Profil anbieten · Anfragen verwalten ============================================================ */ window.Page_sitting = (() => { // ---------------------------------------------------------- // Konstanten // ---------------------------------------------------------- const SERVICES = [ { id: 'tagesbetreuung', label: 'Tagesbetreuung', icon: '' }, { id: 'uebernachtung', label: 'Übernachtung', icon: '' }, { id: 'gassi', label: 'Gassi gehen', icon: '' }, { id: 'hausbesuch', label: 'Hausbesuch', icon: '' }, ]; // ---------------------------------------------------------- // State // ---------------------------------------------------------- let _container = null; let _state = null; let _tab = 'suchen'; // suchen | profil | anfragen | matching let _sitters = []; let _mySitter = null; let _myRequests = []; let _inbox = []; // Matching-State let _matchResults = null; // null = noch nicht geladen let _matchLoading = false; let _myServiceOffer = null; // eigenes Angebot (type='sitting') // ---------------------------------------------------------- // 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 ? ` ` : ''}
`; _container.addEventListener('click', _onClick); } // ---------------------------------------------------------- // Daten laden // ---------------------------------------------------------- async function _load() { const content = document.getElementById('sit-content'); if (!content) return; content.innerHTML = UI.skeleton(3); const tasks = [API.sitting.list()]; if (_state.user) { tasks.push(API.sitting.me()); tasks.push(API.sitting.requests()); tasks.push(API.sitting.inbox()); tasks.push(API.services.me()); } try { const results = await Promise.allSettled(tasks); _sitters = results[0].status === 'fulfilled' ? results[0].value : []; _mySitter = results[1]?.status === 'fulfilled' ? results[1].value : null; _myRequests = results[2]?.status === 'fulfilled' ? results[2].value : []; _inbox = results[3]?.status === 'fulfilled' ? results[3].value : []; const myOffers = results[4]?.status === 'fulfilled' ? results[4].value : []; _myServiceOffer = myOffers?.find(o => o.type === 'sitting') || null; } catch {} _renderTab(); } // ---------------------------------------------------------- // Tab-Inhalte // ---------------------------------------------------------- function _renderTab() { const content = document.getElementById('sit-content'); if (!content) return; if (_tab === 'suchen') _renderSuchen(content); if (_tab === 'profil') _renderProfil(content); if (_tab === 'anfragen') _renderAnfragen(content); if (_tab === 'matching') _renderMatching(content); } // ---- Tab: Sitter suchen ---- function _renderSuchen(el) { if (!_sitters.length) { el.innerHTML = UI.emptyState({ icon: UI.icon('dog'), title: 'Keine Sitter', text: 'Noch keine Sitter in deiner Nähe registriert.' }); return; } el.innerHTML = `
${_sitters.map(s => _sitterCardHTML(s)).join('')}
`; } function _sitterCardHTML(s) { const svcs = (s.services || []).map(id => { const svc = SERVICES.find(x => x.id === id); return svc ? `${svc.icon} ${svc.label}` : ''; }).join(''); const dist = s.distanz_m != null ? (s.distanz_m >= 1000 ? (s.distanz_m / 1000).toFixed(1) + ' km' : s.distanz_m + ' m') : ''; return `
${UI.icon('paw-print')}
${UI.escHtml(s.sitter_name)}
${dist ? `
${UI.icon('map-pin')} ${dist} entfernt
` : ''} ${s.beschreibung ? `
${UI.escHtml(s.beschreibung)}
` : ''}
${svcs}
${s.preis_pro_tag > 0 ? s.preis_pro_tag.toFixed(0) + ' €/Tag' : 'Preis anfragen'}
max. ${s.max_hunde} Hund${s.max_hunde !== 1 ? 'e' : ''}
${_state.user ? `` : ''}
`; } // ---- Tab: Mein Profil ---- function _renderProfil(el) { if (!_mySitter) { el.innerHTML = `
${UI.icon('paw-print')}

Werde Hundesitter

Biete anderen Hundebesitzern deine Dienste an und verdiene etwas dazu.

`; return; } const s = _mySitter; const svcs = (s.services || []).map(id => SERVICES.find(x => x.id === id)?.label || id).join(', '); el.innerHTML = `
${s.aktiv ? `${UI.icon('check')} Aktiv` : `${UI.icon('pause')} Pausiert`}
${s.beschreibung ? `

${UI.escHtml(s.beschreibung)}

` : ''}
${s.preis_pro_tag > 0 ? s.preis_pro_tag.toFixed(0) + ' €' : '–'} pro Tag
${s.max_hunde} Hund${s.max_hunde !== 1 ? 'e' : ''} max.
${s.radius_km} km Umkreis
${svcs || '(keine Services angegeben)'}
`; } // ---- Tab: Anfragen ---- function _renderAnfragen(el) { const inbox = _inbox; const myReqs = _myRequests; let html = ''; if (inbox.length) { html += `
${UI.icon('bell')} Eingehende Anfragen (als Sitter)
`; html += inbox.map(r => _requestCardHTML(r, 'inbox')).join(''); } if (myReqs.length) { html += `
${UI.icon('upload')} Meine Anfragen
`; html += myReqs.map(r => _requestCardHTML(r, 'sent')).join(''); } if (!inbox.length && !myReqs.length) { html = UI.emptyState({ icon: UI.icon('bell'), title: 'Keine Anfragen', text: 'Noch keine Sitting-Anfragen vorhanden.' }); } el.innerHTML = html; } const STATUS_ICON = { offen: 'clock', angenommen: 'check-circle', abgelehnt: 'x-circle', abgebrochen: 'minus-circle' }; const STATUS_LABEL = { offen: 'Offen', angenommen: 'Angenommen', abgelehnt: 'Abgelehnt', abgebrochen: 'Abgebrochen' }; const STATUS_COLOR = { offen: '#f59e0b', angenommen: '#10b981', abgelehnt: '#ef4444', abgebrochen: '#6b7280' }; function _requestCardHTML(r, mode) { const color = STATUS_COLOR[r.status] || '#6b7280'; const icon = STATUS_ICON[r.status] || 'question'; const label = STATUS_LABEL[r.status] || r.status; const name = mode === 'inbox' ? r.anfragender_name : r.sitter_name; return `
${UI.escHtml(name || '?')} ${UI.icon(icon)} ${label}
${UI.icon('calendar-dots')} ${r.von} – ${r.bis}
${r.nachricht ? `
${UI.escHtml(r.nachricht)}
` : ''} ${r.status === 'offen' ? _requestActions(r.id, mode) : ''}
`; } function _requestActions(id, mode) { if (mode === 'inbox') { return `
`; } return `
`; } // ---------------------------------------------------------- // Sitter Detail + Anfrage senden // ---------------------------------------------------------- function _showSitterDetail(id) { const s = _sitters.find(x => x.id === id); if (!s) return; const svcs = (s.services || []).map(sv => { const f = SERVICES.find(x => x.id === sv); return f ? `${f.icon} ${f.label}` : ''; }).join(''); const dist = s.distanz_m != null ? (s.distanz_m >= 1000 ? (s.distanz_m / 1000).toFixed(1) + ' km' : s.distanz_m + ' m') : null; const body = `
${UI.icon('paw-print')}

${UI.escHtml(s.sitter_name)}

${dist ? `
${UI.icon('map-pin')} ${dist} entfernt
` : ''} ${s.beschreibung ? `

${UI.escHtml(s.beschreibung)}

` : ''}
${svcs}
${s.preis_pro_tag > 0 ? s.preis_pro_tag.toFixed(0) + ' €' : '–'} pro Tag
${s.max_hunde} max. Hund${s.max_hunde !== 1 ? 'e' : ''}
${s.radius_km} km Umkreis
`; const footer = _state.user && _mySitter?.user_id !== s.user_id ? ` ` : (!_state.user ? `Zum Anfragen bitte einloggen.` : ''); UI.modal.open({ title: 'Sitter-Profil', body, footer }); UI.ratingStars({ containerId: `sit-rating-${s.id}`, targetType: 'sitting', targetId: s.id, isLoggedIn: !!_state.user, }); document.getElementById('sit-anfrage-btn')?.addEventListener('click', () => { UI.modal.close(); setTimeout(() => _openAnfrageForm(s), 50); }); } function _openAnfrageForm(s) { const dogs = _state.dogs || []; const id = 'sit-anfrage-form'; const body = `

Anfrage an ${UI.escHtml(s.sitter_name)}

${dogs.length ? `
${dogs.map(d => ` `).join('')}
` : ''}
`; const footer = ` `; UI.modal.open({ title: 'Anfrage senden', body, footer }); 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 dogIds = [...form.querySelectorAll('[name="dog_ids"]:checked')].map(cb => parseInt(cb.value)); const data = { sitter_id: s.id, von: fd.get('von'), bis: fd.get('bis'), dog_ids: dogIds, nachricht: fd.get('nachricht') || null, }; if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = '…'; } try { await API.sitting.sendRequest(data); UI.modal.close(); UI.toast('Anfrage gesendet!'); await _load(); } catch (err) { UI.toast(err.message, 'error'); } finally { if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = `${UI.icon('paper-plane-tilt')} Anfrage senden`; } } }); } // ---------------------------------------------------------- // Sitter-Profil Formular // ---------------------------------------------------------- function _openProfilForm() { const s = _mySitter; const id = 'sit-profil-form'; const body = `
${SERVICES.map(svc => ` `).join('')}
${s ? `
` : ''}
`; const footer = `
`; UI.modal.open({ title: s ? 'Sitter-Profil bearbeiten' : 'Sitter-Profil erstellen', body, footer }); // Location Picker initialisieren let _picker = null; setTimeout(() => { _picker = UI.locationPicker({ containerId: 'sit-loc-picker', onSelect(lat, lon, name) { /* State wird über picker.getValue() ausgelesen */ }, }); if (s?.lat && s?.lon) { _picker.setValue(s.lat, s.lon, s.ort_name || null); } }, 50); 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 svcs = [...form.querySelectorAll('[name="services"]:checked')].map(cb => cb.value); const loc = _picker ? _picker.getValue() : { lat: s?.lat || null, lon: s?.lon || null, name: null }; const data = { beschreibung: fd.get('beschreibung') || null, preis_pro_tag: parseFloat(fd.get('preis_pro_tag')) || 0, max_hunde: parseInt(fd.get('max_hunde')) || 1, services: svcs, lat: loc.lat, lon: loc.lon, radius_km: parseInt(fd.get('radius_km')) || 20, }; if (s) data.aktiv = form.querySelector('[name="aktiv"]')?.checked ? 1 : 0; if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = '…'; } try { if (s) { await API.sitting.updateMe(data); } else { await API.sitting.create(data); } UI.modal.close(); UI.toast(s ? 'Profil aktualisiert.' : 'Profil erstellt!'); await _load(); } catch (err) { UI.toast(err.message, 'error'); } finally { if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = s ? `${UI.icon('floppy-disk')} Speichern` : `${UI.icon('plus')} Profil erstellen`; } } }); } // ---------------------------------------------------------- // Tab: Anbieter in deiner Nähe (service_offers Matching) // ---------------------------------------------------------- function _renderMatching(el) { const offerActive = _myServiceOffer?.aktiv; const offerDesc = _myServiceOffer?.beschreibung || ''; const offerPreis = _myServiceOffer?.preis_pro_tag ?? ''; el.innerHTML = `
${UI.icon('handshake')} Mein Angebot ${_state.user ? ` ` : ``}
${_state.user ? `
` : ''}

Klicke auf "Suchen" um Hundesitting-Anbieter in deiner Nähe zu finden.

`; // Toggle document.getElementById('svc-offer-toggle')?.addEventListener('change', async e => { const form = document.getElementById('svc-offer-form'); if (e.target.checked) { form?.classList.remove('svc-offer-form--hidden'); } else { form?.classList.add('svc-offer-form--hidden'); if (_myServiceOffer) { try { await API.services.deactivate(_myServiceOffer.id); _myServiceOffer = { ..._myServiceOffer, aktiv: 0 }; UI.toast('Angebot deaktiviert.'); } catch (err) { UI.toast(err.message, 'error'); } } } }); // GPS document.getElementById('svc-gps-btn')?.addEventListener('click', async () => { const btn = document.getElementById('svc-gps-btn'); btn.disabled = true; try { const pos = await API.getLocation(); document.getElementById('svc-lat').value = pos.lat.toFixed(6); document.getElementById('svc-lon').value = pos.lon.toFixed(6); UI.toast('Position gespeichert.'); } catch { UI.toast('GPS nicht verfügbar.', 'error'); } btn.disabled = false; }); // Formular speichern document.getElementById('svc-offer-form')?.addEventListener('submit', async e => { e.preventDefault(); const form = e.target; const fd = new FormData(form); const submitBtn = form.querySelector('[type="submit"]'); // Wenn noch keine Position gespeichert, GPS holen if (!fd.get('lat') || !fd.get('lon')) { try { const pos = await API.getLocation(); document.getElementById('svc-lat').value = pos.lat.toFixed(6); document.getElementById('svc-lon').value = pos.lon.toFixed(6); fd.set('lat', pos.lat.toFixed(6)); fd.set('lon', pos.lon.toFixed(6)); } catch { UI.toast('Bitte GPS-Position ermitteln.', 'error'); return; } } if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = '…'; } try { const payload = { type: 'sitting', beschreibung: fd.get('beschreibung') || null, preis_pro_tag: fd.get('preis_pro_tag') ? parseFloat(fd.get('preis_pro_tag')) : null, lat: parseFloat(fd.get('lat')), lon: parseFloat(fd.get('lon')), radius_km: parseInt(fd.get('radius_km')) || 10, }; _myServiceOffer = await API.services.upsert(payload); UI.toast('Angebot gespeichert!'); } catch (err) { UI.toast(err.message, 'error'); } finally { if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = `${UI.icon('floppy-disk')} Speichern`; } } }); // Suche document.getElementById('svc-find-btn')?.addEventListener('click', _searchProviders); } async function _searchProviders() { if (_matchLoading) return; _matchLoading = true; const btn = document.getElementById('svc-find-btn'); if (btn) { btn.disabled = true; btn.innerHTML = `${UI.icon('spinner')} Suche…`; } const resultsEl = document.getElementById('svc-results'); let pos = null; try { pos = await API.getLocation(); } catch { UI.toast('GPS nicht verfügbar — Suche ohne Entfernung.', 'error'); } try { const offers = await API.services.list('sitting', pos?.lat ?? null, pos?.lon ?? null, 50); _matchResults = offers; _renderMatchResults(resultsEl, pos); } catch (err) { UI.toast(err.message, 'error'); if (resultsEl) resultsEl.innerHTML = `

Fehler beim Laden.

`; } _matchLoading = false; if (btn) { btn.disabled = false; btn.innerHTML = `${UI.icon('map-pin')} Erneut suchen`; } } function _renderMatchResults(el, pos) { if (!el) return; const list = _matchResults || []; // Eigene Angebote ausblenden const filtered = list.filter(o => o.user_id !== _state.user?.id); if (!filtered.length) { el.innerHTML = `

Keine Anbieter in deiner Nähe gefunden.

`; return; } el.innerHTML = `
${filtered.map(o => _serviceCardHTML(o)).join('')}
`; } function _serviceCardHTML(o) { const dist = o.distanz_km != null ? `${o.distanz_km} km entfernt` : ''; const preis = o.preis_pro_tag != null ? `${o.preis_pro_tag.toFixed(0)} €/Tag` : 'Preis anfragen'; return `
${UI.icon('paw-print')}
${UI.escape(o.anbieter_name || `Nutzer #${o.user_id}`)}
${dist ? `
${UI.icon('map-pin')} ${dist}
` : ''} ${o.beschreibung ? `
${UI.escape(o.beschreibung)}
` : ''}
${preis}
`; } async function _openChatWithProvider(userId) { if (!_state.user) { UI.toast('Bitte zuerst anmelden.', 'error'); App.navigate('settings'); return; } try { const { conversation_id } = await API.chat.start(userId); App.navigate('chat', true, { conversation_id }); } catch (err) { UI.toast(err.message, 'error'); } } // ---------------------------------------------------------- // Click-Handler // ---------------------------------------------------------- function _onClick(e) { // Tab-Wechsel const tabBtn = e.target.closest('[data-sit-tab]'); if (tabBtn) { _tab = tabBtn.dataset.sitTab; document.querySelectorAll('[data-sit-tab]').forEach(b => b.classList.toggle('active', b.dataset.sitTab === _tab)); _renderTab(); return; } // Notiz-Button auf Sitter-Karte const noteBtn = e.target.closest('.sit-note-btn'); if (noteBtn) { e.stopPropagation(); _openNoteModal( 'sitting', parseInt(noteBtn.dataset.sitNoteId), noteBtn.dataset.sitNoteLabel, null ); return; } // Sitter-Karte const sitterCard = e.target.closest('[data-sit-id]'); if (sitterCard && !e.target.closest('button')) { _showSitterDetail(parseInt(sitterCard.dataset.sitId)); return; } // Profil erstellen if (e.target.closest('#sit-create-profil-btn') || e.target.closest('#sit-edit-profil-btn')) { _openProfilForm(); return; } // Anbieter kontaktieren const chatBtn = e.target.closest('[data-svc-chat]'); if (chatBtn) { _openChatWithProvider(parseInt(chatBtn.dataset.svcChat)); return; } // Anfragen-Aktionen const acceptBtn = e.target.closest('[data-sit-accept]'); const declineBtn = e.target.closest('[data-sit-decline]'); const cancelBtn = e.target.closest('[data-sit-cancel]'); if (acceptBtn) { _changeReqStatus(parseInt(acceptBtn.dataset.sitAccept), 'angenommen'); return; } if (declineBtn) { _changeReqStatus(parseInt(declineBtn.dataset.sitDecline), 'abgelehnt'); return; } if (cancelBtn) { _changeReqStatus(parseInt(cancelBtn.dataset.sitCancel), 'abgebrochen'); return; } } async function _changeReqStatus(id, status) { try { await API.sitting.updateRequest(id, status); UI.toast(`Anfrage ${status}.`); await _load(); } catch (e) { UI.toast(e.message, 'error'); } } // ---------------------------------------------------------- // 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('#sit-note-close')?.addEventListener('click', close); ovl.querySelector('#sit-note-cancel')?.addEventListener('click', close); ovl.addEventListener('click', e => { if (e.target === ovl) close(); }); ovl.querySelector('#sit-note-save')?.addEventListener('click', async () => { const text = ovl.querySelector('#sit-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 }; })();