banyaro/backend/static/js/pages/sitting.js
rene 553e9e7854 Sprint 12+13: Tagebuch Day-One-Redesign, Notiz-Feature, Icon-Fixes, SW by-v405
Tagebuch:
- Day-One-Listenansicht: Wochentag + Tageszahl + Meta-Zeile (Zeit/Ort/Wetter)
- 4 Ansichten: Liste, Medien-Mosaik, Kalender (mit Sprungbuttons), Karte (GPS-Marker)
- Detail-Ansicht inline im Content-Bereich (kein Fullscreen-Overlay mehr)
- Hero-Bild vollständig sichtbar (object-fit:contain), Lightbox mit Safe-Area
- 2-Spalten-Layout Desktop: Text + Leaflet-Karte + POI-Liste
- EXIF-GPS-Extraktion bei Foto-Upload, historisches Wetter via Archive-API
- NoteStation-Import: Fotos in diary_media (80 Einträge migriert, 94 Medien)
- Stats-Endpoints: /diary/stats, /diary/calendar, /diary/locations

Notiz-Feature:
- Generische notes-Tabelle (parent_type + parent_id + meta_json)
- 📝-Button in 8 Bereichen, Notizblock-Seite mit KI-Analyse
- KI-Toggle in Einstellungen, notes_ki_enabled in User-Profil

Icons & Design:
- fill:currentColor Fix für welcome/onboarding/friends.js
- --c-icon Variable, --c-text-muted Dark Mode aufgehellt
- 15+ neue Phosphor-Icons aus lokaler Kopie
- CSS Network-First im SW, Cache-Control-Middleware

Infrastruktur:
- Wiki-Anreicherungs-Scheduler-Jobs entfernt (abgeschlossen)
- auth.py: notes_ki_enabled + is_social_media im User-Response
2026-04-25 20:44:46 +02:00

818 lines
34 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 | 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 = `
<div class="sitting-layout">
<div class="sitting-tabs by-tabs" id="sit-tabs">
<button class="by-tab active" data-sit-tab="suchen">${UI.icon('magnifying-glass')} Sitter finden</button>
<button class="by-tab" data-sit-tab="matching">${UI.icon('users')} Anbieter</button>
${_state.user ? `
<button class="by-tab" data-sit-tab="profil">${UI.icon('user')} Mein Profil</button>
<button class="by-tab" data-sit-tab="anfragen">${UI.icon('bell')} Anfragen</button>
` : ''}
</div>
<div id="sit-content" class="sitting-content"></div>
</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());
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 = `
<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>
${_state.user ? `<button class="btn-icon sit-note-btn"
data-sit-note-id="${s.id}"
data-sit-note-label="${UI.escHtml(s.sitter_name + ' ' + (s.datum || ''))}"
title="Notiz" style="color:var(--c-text-muted);margin-top:var(--space-1)"
onclick="event.stopPropagation()">
${UI.icon('note-pencil')}</button>` : ''}
</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">${UI.icon('plus')} 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` : `${UI.icon('pause')} 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="by-section-label">${UI.icon('bell')} Eingehende Anfragen (als Sitter)</div>`;
html += inbox.map(r => _requestCardHTML(r, 'inbox')).join('');
}
if (myReqs.length) {
html += `<div class="by-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: 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 `
<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}">${UI.icon(icon)} ${label}</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>
<div id="sit-rating-${s.id}"></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 });
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 = `
<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">${UI.icon('paper-plane-tilt')} 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.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 = `
<form id="${id}">
<div class="form-group">
<label class="form-label">Über mich / Beschreibung</label>
<textarea class="form-control" name="beschreibung" rows="3">${UI.escape(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-group">
<label class="form-label">Mein Standort <span style="color:var(--c-text-secondary)">(für Umkreis-Suche)</span></label>
<div id="sit-loc-picker"></div>
</div>
<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 = `
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%">
<button class="btn btn-primary" type="submit" form="${id}" id="sit-profil-submit" style="width:100%">
${s ? `${UI.icon('floppy-disk')} Speichern` : `${UI.icon('plus')} Profil erstellen`}
</button>
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
</div>
`;
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 = `
<div class="svc-matching-layout">
<!-- Eigenes Angebot -->
<div class="svc-own-offer by-card">
<div class="svc-own-offer-header">
<span class="svc-own-offer-title">${UI.icon('handshake')} Mein Angebot</span>
${_state.user ? `
<label class="svc-toggle" title="${offerActive ? 'Angebot deaktivieren' : 'Angebot aktivieren'}">
<input type="checkbox" id="svc-offer-toggle" ${offerActive ? 'checked' : ''}>
<span class="svc-toggle-slider"></span>
</label>
` : `<span class="svc-login-hint">Zum Anbieten bitte anmelden</span>`}
</div>
${_state.user ? `
<form id="svc-offer-form" class="svc-offer-form ${offerActive ? '' : 'svc-offer-form--hidden'}">
<div class="form-group">
<label class="form-label">Beschreibung</label>
<textarea class="form-control" name="beschreibung" rows="2"
placeholder="Was bietest du an? Erfahrungen, besondere Stärken…">${UI.escape(offerDesc)}</textarea>
</div>
<div class="form-row-2">
<div class="form-group">
<label class="form-label">Preis/Tag (€, optional)</label>
<input class="form-control" type="number" step="1" min="0" name="preis_pro_tag"
value="${offerPreis}">
</div>
<div class="form-group">
<label class="form-label">Umkreis (km)</label>
<input class="form-control" type="number" min="1" max="100" name="radius_km"
value="${_myServiceOffer?.radius_km ?? 10}">
</div>
</div>
<div class="svc-offer-actions">
<button type="button" class="btn btn-secondary btn-sm" id="svc-gps-btn">
${UI.icon('map-pin')} Position
</button>
<input type="hidden" name="lat" id="svc-lat" value="${_myServiceOffer?.lat || ''}">
<input type="hidden" name="lon" id="svc-lon" value="${_myServiceOffer?.lon || ''}">
<button type="submit" class="btn btn-primary btn-sm">
${UI.icon('floppy-disk')} Speichern
</button>
</div>
</form>
` : ''}
</div>
<!-- Anbieter finden -->
<div class="svc-search-section">
<div class="svc-search-header">
<span class="by-section-label" style="margin:0">${UI.icon('magnifying-glass')} Anbieter in deiner Nähe</span>
<button class="btn btn-primary btn-sm" id="svc-find-btn">
${UI.icon('map-pin')} Suchen
</button>
</div>
<div id="svc-results">
<p class="svc-hint">Klicke auf "Suchen" um Hundesitting-Anbieter in deiner Nähe zu finden.</p>
</div>
</div>
</div>
`;
// 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 = `<p class="svc-hint">Fehler beim Laden.</p>`;
}
_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 = `<p class="svc-hint">Keine Anbieter in deiner Nähe gefunden.</p>`;
return;
}
el.innerHTML = `
<div class="svc-results-list">
${filtered.map(o => _serviceCardHTML(o)).join('')}
</div>
`;
}
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 `
<div class="svc-card">
<div class="svc-card-avatar">${UI.icon('paw-print')}</div>
<div class="svc-card-body">
<div class="svc-card-name">${UI.escape(o.anbieter_name || `Nutzer #${o.user_id}`)}</div>
${dist ? `<div class="svc-card-dist">${UI.icon('map-pin')} ${dist}</div>` : ''}
${o.beschreibung ? `<div class="svc-card-desc">${UI.escape(o.beschreibung)}</div>` : ''}
</div>
<div class="svc-card-side">
<div class="svc-card-price">${preis}</div>
<button class="btn btn-primary btn-sm" data-svc-chat="${o.user_id}">
${UI.icon('chat-circle')} Kontakt
</button>
</div>
</div>
`;
}
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 = `
<div style="width:100%;max-width:600px;background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
padding:var(--space-4);box-sizing:border-box;max-height:80vh;display:flex;flex-direction:column">
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
<svg class="ph-icon" aria-hidden="true" style="color:var(--c-primary)"><use href="/icons/phosphor.svg#note-pencil"></use></svg>
<span style="font-weight:600;flex:1"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz — ${UI.escape(parentLabel)}</span>
<button id="sit-note-close" style="background:none;border:none;cursor:pointer;color:var(--c-text-muted);padding:4px">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
</button>
</div>
<textarea id="sit-note-text" rows="5"
style="width:100%;box-sizing:border-box;padding:var(--space-3);
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
font-size:var(--text-sm);font-family:inherit;
background:var(--c-bg);color:var(--c-text);resize:vertical;flex:1"
placeholder="Deine Notiz zu diesem Sitter…">${UI.escape(existingNote?.text || '')}</textarea>
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3)">
<button id="sit-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
<button id="sit-note-save" class="btn btn-primary flex-1">Speichern</button>
</div>
</div>
`;
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 };
})();