Sprint 19: Social, UX-Verbesserungen, Nerd2Noob-Hilfe
This commit is contained in:
parent
10d30bf565
commit
89d87030a2
18 changed files with 930 additions and 74 deletions
|
|
@ -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]');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue