Sprint 19: Social, UX-Verbesserungen, Nerd2Noob-Hilfe

This commit is contained in:
rene 2026-04-17 23:53:50 +02:00
parent 10d30bf565
commit 89d87030a2
18 changed files with 930 additions and 74 deletions

View file

@ -18,13 +18,17 @@ window.Page_sitting = (() => {
// ----------------------------------------------------------
// State
// ----------------------------------------------------------
let _container = null;
let _state = null;
let _tab = 'suchen'; // suchen | profil | anfragen
let _sitters = [];
let _mySitter = null;
let _myRequests = [];
let _inbox = [];
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
@ -46,6 +50,7 @@ window.Page_sitting = (() => {
<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>
@ -70,6 +75,7 @@ window.Page_sitting = (() => {
tasks.push(API.sitting.me());
tasks.push(API.sitting.requests());
tasks.push(API.sitting.inbox());
tasks.push(API.services.me());
}
try {
@ -78,6 +84,8 @@ window.Page_sitting = (() => {
_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();
@ -92,6 +100,7 @@ window.Page_sitting = (() => {
if (_tab === 'suchen') _renderSuchen(content);
if (_tab === 'profil') _renderProfil(content);
if (_tab === 'anfragen') _renderAnfragen(content);
if (_tab === 'matching') _renderMatching(content);
}
// ---- Tab: Sitter suchen ----
@ -441,6 +450,239 @@ window.Page_sitting = (() => {
});
}
// ----------------------------------------------------------
// 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
// ----------------------------------------------------------
@ -467,6 +709,13 @@ window.Page_sitting = (() => {
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]');