Feature: Ratings, Lightbox, Forum-Standort, Notifications, Routen-Recording, Chat-Picker

- Bewertungssystem (ratings.py): Sterne für Sitter/Walks/Places/Routen
- Admin: Server-Log-Viewer + OSM-Cache-Statistiken
- Chat: "Neue Nachricht"-Button mit Freundesliste-Picker
- Forum: 5 neue Kategorien, Standorteingabe (locationPicker), Absende-Toast, Lightbox
- Freunde: Aktivitäts-Filter (Chips), Freundschaftsanfrage → In-App-Notification
- Sitter: locationPicker statt manuelle Koordinateneingabe + ratingStars
- Tagebuch: Bilder-Lightbox im Detail-View, iOS-Modal-Header-Fix (90svh)
- Routen: Start/Stopp-Button wechselt Zustand, nutzt Page_map.isRecording()
- Benachrichtigungen: Delete-Button sichtbar, typ-basierte Navigation, Toast-Feedback
- OSM: globales Semaphore + 429-Retry-Logic; Scheduler: München-Umland, täglich
- SW by-v225, APP_VER 202
This commit is contained in:
rene 2026-04-19 09:40:35 +02:00
parent aa70a838f2
commit e56183b642
21 changed files with 648 additions and 175 deletions

View file

@ -262,6 +262,7 @@ window.Page_sitting = (() => {
<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 ? `
@ -270,6 +271,13 @@ window.Page_sitting = (() => {
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);
@ -353,7 +361,7 @@ window.Page_sitting = (() => {
<form id="${id}">
<div class="form-group">
<label class="form-label">Über mich / Beschreibung</label>
<textarea class="form-control" name="beschreibung" rows="3">${s?.beschreibung || ''}</textarea>
<textarea class="form-control" name="beschreibung" rows="3">${UI.escape(s?.beschreibung || '')}</textarea>
</div>
<div class="form-row-2">
<div class="form-group">
@ -374,17 +382,10 @@ window.Page_sitting = (() => {
</label>
`).join('')}
</div>
<div class="form-row-2">
<div class="form-group">
<label class="form-label">Breitengrad</label>
<input class="form-control" type="number" step="any" name="lat" id="sit-lat" value="${s?.lat || ''}">
</div>
<div class="form-group">
<label class="form-label">Längengrad</label>
<input class="form-control" type="number" step="any" name="lon" id="sit-lon" value="${s?.lon || ''}">
</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>
<button type="button" class="btn btn-secondary btn-sm" id="sit-gps-btn">${UI.icon('map-pin')} Meine Position</button>
<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}">
@ -408,13 +409,17 @@ window.Page_sitting = (() => {
`;
UI.modal.open({ title: s ? 'Sitter-Profil bearbeiten' : 'Sitter-Profil erstellen', body, footer });
document.getElementById('sit-gps-btn')?.addEventListener('click', async () => {
try {
const pos = await API.getLocation();
document.getElementById('sit-lat').value = pos.lat.toFixed(6);
document.getElementById('sit-lon').value = pos.lon.toFixed(6);
} catch { UI.toast('GPS nicht verfügbar.', 'error'); }
});
// 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"]');
@ -423,13 +428,14 @@ window.Page_sitting = (() => {
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: fd.get('lat') ? parseFloat(fd.get('lat')) : null,
lon: fd.get('lon') ? parseFloat(fd.get('lon')) : null,
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;