banyaro/backend/static/js/pages/sitting.js
rene 097295c628 Sprint 11: Freunde & Chat + Phosphor-Icon-Vollmigration
- Freundschaften (pending/accepted), Nutzersuche, Anfragen per Push
- Direktnachrichten mit Polling, iMessage-Stil, Deep-Links aus Push
- Alle Seiten (map, places, diary, health, dog-profile, sitting, knigge,
  forum, wiki, walks) vollständig auf Phosphor-Icons migriert
- Wikidata-Rassen-Scraper (~833 neue Rassen, lokal gespiegelte Fotos)
- TheDogAPI lokal gespiegelt (169 Rassen + Fotos)
- Quiz-Result-Cards horizontal (korrekte Bildproportionen)
- SW by-v89
2026-04-15 21:33:53 +02:00

482 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
BAN YARO — Hundesitting
Sitter suchen · Profil anbieten · Anfragen verwalten
============================================================ */
window.Page_sitting = (() => {
// ----------------------------------------------------------
// Konstanten
// ----------------------------------------------------------
const SERVICES = [
{ id: 'tagesbetreuung', label: 'Tagesbetreuung', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#sun"></use></svg>' },
{ id: 'uebernachtung', label: 'Übernachtung', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#moon"></use></svg>' },
{ id: 'gassi', label: 'Gassi gehen', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>' },
{ id: 'hausbesuch', label: 'Hausbesuch', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#house-line"></use></svg>' },
];
// ----------------------------------------------------------
// State
// ----------------------------------------------------------
let _container = null;
let _state = null;
let _tab = 'suchen'; // suchen | profil | anfragen
let _sitters = [];
let _mySitter = null;
let _myRequests = [];
let _inbox = [];
// ----------------------------------------------------------
// init
// ----------------------------------------------------------
async function init(container, appState) {
_container = container;
_state = appState;
_render();
await _load();
}
async function refresh() { await _load(); }
// ----------------------------------------------------------
// Render Grundstruktur
// ----------------------------------------------------------
function _render() {
_container.innerHTML = `
<div class="sitting-tabs" id="sit-tabs">
<button class="sitting-tab active" data-sit-tab="suchen">${UI.icon('magnifying-glass')} Sitter finden</button>
${_state.user ? `
<button class="sitting-tab" data-sit-tab="profil">${UI.icon('user')} Mein Profil</button>
<button class="sitting-tab" data-sit-tab="anfragen">${UI.icon('bell')} Anfragen</button>
` : ''}
</div>
<div id="sit-content" class="sitting-content"></div>
`;
_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());
}
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 : [];
} 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);
}
// ---- Tab: Sitter suchen ----
function _renderSuchen(el) {
if (!_sitters.length) {
el.innerHTML = UI.emptyState({ icon: 'dog', title: 'Keine Sitter', text: 'Noch keine Sitter in deiner Nähe registriert.' });
return;
}
el.innerHTML = `
<div class="sitting-list">
${_sitters.map(s => _sitterCardHTML(s)).join('')}
</div>
`;
}
function _sitterCardHTML(s) {
const svcs = (s.services || []).map(id => {
const svc = SERVICES.find(x => x.id === id);
return svc ? `<span class="sit-service-badge">${svc.icon} ${svc.label}</span>` : '';
}).join('');
const dist = s.distanz_m != null
? (s.distanz_m >= 1000 ? (s.distanz_m / 1000).toFixed(1) + ' km' : s.distanz_m + ' m')
: '';
return `
<div class="sitting-card" data-sit-id="${s.id}">
<div class="sitting-card-avatar">${UI.icon('paw-print')}</div>
<div class="sitting-card-body">
<div class="sitting-card-name">${UI.escHtml(s.sitter_name)}</div>
${dist ? `<div class="sitting-card-dist">${UI.icon('map-pin')} ${dist} entfernt</div>` : ''}
${s.beschreibung ? `<div class="sitting-card-desc">${UI.escHtml(s.beschreibung)}</div>` : ''}
<div class="sitting-services">${svcs}</div>
</div>
<div class="sitting-card-side">
<div class="sitting-price">${s.preis_pro_tag > 0 ? s.preis_pro_tag.toFixed(0) + ' €/Tag' : 'Preis anfragen'}</div>
<div class="sitting-dogs">max. ${s.max_hunde} Hund${s.max_hunde !== 1 ? 'e' : ''}</div>
</div>
</div>
`;
}
// ---- Tab: Mein Profil ----
function _renderProfil(el) {
if (!_mySitter) {
el.innerHTML = `
<div class="sitting-empty-profil">
<div style="font-size:3rem">${UI.icon('paw-print')}</div>
<h3>Werde Hundesitter</h3>
<p>Biete anderen Hundebesitzern deine Dienste an und verdiene etwas dazu.</p>
<button class="btn btn-primary" id="sit-create-profil-btn">Profil erstellen</button>
</div>
`;
return;
}
const s = _mySitter;
const svcs = (s.services || []).map(id => SERVICES.find(x => x.id === id)?.label || id).join(', ');
el.innerHTML = `
<div class="sitting-my-profil">
<div class="sitting-profil-header">
<div class="sitting-profil-status ${s.aktiv ? 'active' : 'inactive'}">
${s.aktiv ? `${UI.icon('check')} Aktiv` : 'Pausiert'}
</div>
<button class="btn btn-secondary btn-sm" id="sit-edit-profil-btn">${UI.icon('pencil-simple')} Bearbeiten</button>
</div>
${s.beschreibung ? `<p>${UI.escHtml(s.beschreibung)}</p>` : ''}
<div class="sitting-profil-facts">
<div class="sitting-profil-fact"><strong>${s.preis_pro_tag > 0 ? s.preis_pro_tag.toFixed(0) + ' €' : ''}</strong> pro Tag</div>
<div class="sitting-profil-fact"><strong>${s.max_hunde}</strong> Hund${s.max_hunde !== 1 ? 'e' : ''} max.</div>
<div class="sitting-profil-fact"><strong>${s.radius_km} km</strong> Umkreis</div>
</div>
<div class="sitting-services" style="margin-top:var(--space-3)">${svcs || '(keine Services angegeben)'}</div>
</div>
`;
}
// ---- Tab: Anfragen ----
function _renderAnfragen(el) {
const inbox = _inbox;
const myReqs = _myRequests;
let html = '';
if (inbox.length) {
html += `<div class="sitting-section-label">${UI.icon('bell')} Eingehende Anfragen (als Sitter)</div>`;
html += inbox.map(r => _requestCardHTML(r, 'inbox')).join('');
}
if (myReqs.length) {
html += `<div class="sitting-section-label" style="margin-top:var(--space-4)">${UI.icon('upload')} Meine Anfragen</div>`;
html += myReqs.map(r => _requestCardHTML(r, 'sent')).join('');
}
if (!inbox.length && !myReqs.length) {
html = UI.emptyState({ icon: 'bell', title: 'Keine Anfragen', text: 'Noch keine Sitting-Anfragen vorhanden.' });
}
el.innerHTML = html;
}
function _requestCardHTML(r, mode) {
const STATUS_COLOR = { offen: '#f59e0b', angenommen: '#10b981', abgelehnt: '#ef4444', abgebrochen: '#6b7280' };
const color = STATUS_COLOR[r.status] || '#6b7280';
const name = mode === 'inbox' ? r.anfragender_name : r.sitter_name;
return `
<div class="sitting-request-card" data-sit-req="${r.id}" data-sit-req-mode="${mode}">
<div class="sitting-req-header">
<span class="sitting-req-name">${UI.escHtml(name || '?')}</span>
<span class="sitting-req-status" style="color:${color}">${r.status}</span>
</div>
<div class="sitting-req-dates">${UI.icon('calendar-dots')} ${r.von} ${r.bis}</div>
${r.nachricht ? `<div class="sitting-req-msg">${UI.escHtml(r.nachricht)}</div>` : ''}
${r.status === 'offen' ? _requestActions(r.id, mode) : ''}
</div>
`;
}
function _requestActions(id, mode) {
if (mode === 'inbox') {
return `
<div class="sitting-req-actions">
<button class="btn btn-primary btn-sm" data-sit-accept="${id}">${UI.icon('check')} Annehmen</button>
<button class="btn btn-danger btn-sm" data-sit-decline="${id}">${UI.icon('x')} Ablehnen</button>
</div>
`;
}
return `
<div class="sitting-req-actions">
<button class="btn btn-secondary btn-sm" data-sit-cancel="${id}">${UI.icon('x')} Abbrechen</button>
</div>
`;
}
// ----------------------------------------------------------
// 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 ? `<span class="sit-service-badge">${f.icon} ${f.label}</span>` : '';
}).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 = `
<div class="sitting-detail-avatar">${UI.icon('paw-print')}</div>
<h3 style="margin:var(--space-2) 0">${UI.escHtml(s.sitter_name)}</h3>
${dist ? `<div style="color:var(--c-text-secondary);margin-bottom:var(--space-2)">${UI.icon('map-pin')} ${dist} entfernt</div>` : ''}
${s.beschreibung ? `<p>${UI.escHtml(s.beschreibung)}</p>` : ''}
<div class="sitting-services" style="margin:var(--space-3) 0">${svcs}</div>
<div class="sitting-profil-facts">
<div class="sitting-profil-fact"><strong>${s.preis_pro_tag > 0 ? s.preis_pro_tag.toFixed(0) + ' €' : ''}</strong> pro Tag</div>
<div class="sitting-profil-fact"><strong>${s.max_hunde}</strong> max. Hund${s.max_hunde !== 1 ? 'e' : ''}</div>
<div class="sitting-profil-fact"><strong>${s.radius_km} km</strong> Umkreis</div>
</div>
`;
const footer = _state.user && _mySitter?.user_id !== s.user_id ? `
<button class="btn btn-primary" id="sit-anfrage-btn">${UI.icon('bell')} Anfrage senden</button>
` : (!_state.user ? `<span style="color:var(--c-text-secondary)">Zum Anfragen bitte einloggen.</span>` : '');
UI.modal.open({ title: 'Sitter-Profil', body, footer });
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 = `
<form id="${id}">
<p style="color:var(--c-text-secondary);margin-bottom:var(--space-3)">Anfrage an <strong>${UI.escHtml(s.sitter_name)}</strong></p>
<div class="form-row-2">
<div class="form-group">
<label class="form-label">Von *</label>
<input class="form-control" type="date" name="von" required>
</div>
<div class="form-group">
<label class="form-label">Bis *</label>
<input class="form-control" type="date" name="bis" required>
</div>
</div>
${dogs.length ? `
<div class="form-group">
<label class="form-label">Welche Hunde?</label>
${dogs.map(d => `
<label class="form-check">
<input type="checkbox" name="dog_ids" value="${d.id}" checked>
${UI.escHtml(d.name)}
</label>
`).join('')}
</div>
` : ''}
<div class="form-group">
<label class="form-label">Nachricht</label>
<textarea class="form-control" name="nachricht" rows="3" placeholder="z.B. Besonderheiten deines Hundes…"></textarea>
</div>
</form>
`;
const footer = `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-primary" type="submit" form="${id}" id="sit-anfrage-submit">Anfrage senden</button>
`;
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.textContent = '…'; }
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.textContent = 'Anfrage senden'; }
}
});
}
// ----------------------------------------------------------
// Sitter-Profil Formular
// ----------------------------------------------------------
function _openProfilForm() {
const s = _mySitter;
const id = 'sit-profil-form';
const body = `
<form id="${id}">
<div class="form-group">
<label class="form-label">Über mich / Beschreibung</label>
<textarea class="form-control" name="beschreibung" rows="3">${s?.beschreibung || ''}</textarea>
</div>
<div class="form-row-2">
<div class="form-group">
<label class="form-label">Preis pro Tag (€)</label>
<input class="form-control" type="number" step="1" min="0" name="preis_pro_tag" value="${s?.preis_pro_tag ?? 0}">
</div>
<div class="form-group">
<label class="form-label">Max. Hunde</label>
<input class="form-control" type="number" min="1" max="10" name="max_hunde" value="${s?.max_hunde ?? 1}">
</div>
</div>
<div class="form-group">
<label class="form-label">Services</label>
${SERVICES.map(svc => `
<label class="form-check">
<input type="checkbox" name="services" value="${svc.id}" ${s?.services?.includes(svc.id) ? 'checked' : ''}>
${svc.icon} ${svc.label}
</label>
`).join('')}
</div>
<div class="form-row-2">
<div class="form-group">
<label class="form-label">Breitengrad</label>
<input class="form-control" type="number" step="any" name="lat" id="sit-lat" value="${s?.lat || ''}">
</div>
<div class="form-group">
<label class="form-label">Längengrad</label>
<input class="form-control" type="number" step="any" name="lon" id="sit-lon" value="${s?.lon || ''}">
</div>
</div>
<button type="button" class="btn btn-secondary btn-sm" id="sit-gps-btn">${UI.icon('map-pin')} Meine Position</button>
<div class="form-group" style="margin-top:var(--space-3)">
<label class="form-label">Umkreis (km)</label>
<input class="form-control" type="number" min="1" max="100" name="radius_km" value="${s?.radius_km ?? 20}">
</div>
${s ? `
<div class="form-group">
<label class="form-check">
<input type="checkbox" name="aktiv" value="1" ${s.aktiv ? 'checked' : ''}> Profil aktiv (sichtbar für andere)
</label>
</div>
` : ''}
</form>
`;
const footer = `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-primary" type="submit" form="${id}" id="sit-profil-submit">
${s ? 'Speichern' : 'Profil erstellen'}
</button>
`;
UI.modal.open({ title: s ? 'Sitter-Profil bearbeiten' : 'Sitter-Profil erstellen', body, footer });
document.getElementById('sit-gps-btn')?.addEventListener('click', async () => {
try {
const pos = await API.getLocation();
document.getElementById('sit-lat').value = pos.lat.toFixed(6);
document.getElementById('sit-lon').value = pos.lon.toFixed(6);
} catch { UI.toast('GPS nicht verfügbar.', 'error'); }
});
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 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: fd.get('lat') ? parseFloat(fd.get('lat')) : null,
lon: fd.get('lon') ? parseFloat(fd.get('lon')) : null,
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.textContent = '…'; }
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.textContent = s ? 'Speichern' : 'Profil erstellen'; }
}
});
}
// ----------------------------------------------------------
// 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;
}
// 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;
}
// 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'); }
}
return { init, refresh };
})();