banyaro/backend/static/js/pages/sitting.js
rene 459cd425f2 Design-System Sprint A: utilities.css + 948 Inline-Styles → Utility-Klassen, SW by-v1102
PHASE 1 — Sofort-Cleanup ohne Risiko:
- Neue Datei utilities.css mit ~25 Klassen für häufige Kombinationen:
  * text-xs-muted, text-xs-secondary, text-sm-muted, text-sm-secondary
  * flex-gap-2/3, flex-col-gap-2/3/4, flex-center-gap-1/2/3
  * flex-between, flex-1-min, mb-1/3, mt-1/3
  * icon-xs/sm/md/lg, label-block, caption
- index.html bindet utilities.css ein
- mb-3/mt-3 ergänzt (waren in design-system.css unvollständig)

PHASE 2 — .by-tab Modifier für Vereinheitlichung:
- .by-tabs.grid (mit --tab-cols Variable für Admin/Health/etc.)
- .by-tabs.sticky (Desktop vertikale Tabs für Admin)
- .by-tabs.wrap (Zuchthunde, flex-wrap statt scroll)
- .by-tabs.separated (Sitting, mit eigenem Hintergrund + Border)

PHASE 3 — Inline-Style → Klassen-Migration (Python-Script):
- 948 Inline-Styles entfernt (5101 → 4153, -18%)
- 962 Migrationen über 47 Page-Dateien
- Top-Treffer: admin.js (180), health.js (67), dog-profile.js (67),
  litters.js (62), settings.js (61), zuchthunde.js (51)
- Patterns: text-muted, text-secondary, text-danger, text-xs-muted,
  text-sm-muted, grid-2 (Duplikat-Bug behoben!), flex-col-gap-3,
  p-3/4, mb-2/3/4, hidden, w-full, flex-1, ...
- Bewahrt bestehende class-Attribute (mergt korrekt)

Alle 19 Tests grün. Kein visueller Diff erwartet (gleiche Property-Werte).
2026-05-27 07:11:27 +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 mt-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 mt-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 class="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 class="text-secondary">(für Umkreis-Suche)</span></label>
<div id="sit-loc-picker"></div>
</div>
<div class="form-group mt-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" class="w-full">
${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" class="text-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 };
})();