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).
818 lines
34 KiB
JavaScript
818 lines
34 KiB
JavaScript
/* ============================================================
|
||
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 };
|
||
|
||
})();
|