Sprint 15: Zeitzone-Fix, Gewichts-Sync, Öffnungszeiten, KI-Bericht, POI-Moderation — SW by-v432, APP_VER 411
- client_time: Browser-Lokalzeit bei allen Creates mitschicken (Tagebuch, Notizen, Forum, Verlorener Hund, Routen) — kein UTC-Versatz mehr bei Einträgen - Gewicht-Sync: health typ=gewicht schreibt dogs.gewicht_kg, einmalige Migration - Praxen: opening_hours + lat/lon/osm_id in tieraerzte-Tabelle, OSM-Nearby-Lookup, Öffnungszeiten in Karte und Detailansicht - KI-Gesundheitsbericht: alle 2 Wochen automatisch, ki_health_reports-Tabelle, Frontend-Banner mit Archiv (letzten 5 Berichte) - POI-Korrekturen: User schlägt Öffnungszeiten-Änderung vor, Moderatoren-Tab genehmigt/lehnt ab, user_edited-Flag schützt vor Overpass-Überschreibung - timeutils.py: safe_client_time() zentral für alle Routen
This commit is contained in:
parent
679dbdd862
commit
06bd8525ed
21 changed files with 724 additions and 75 deletions
|
|
@ -168,6 +168,7 @@ const API = (() => {
|
|||
kiZusammenfassung(dogId) {
|
||||
return post(`/dogs/${dogId}/health/ki-zusammenfassung`);
|
||||
},
|
||||
kiBerichte(dogId) { return get(`/dogs/${dogId}/health/ki-berichte`); },
|
||||
symptomCheck(dogId, symptoms) {
|
||||
return post(`/dogs/${dogId}/health/symptom-check`, { symptoms });
|
||||
},
|
||||
|
|
@ -180,9 +181,10 @@ const API = (() => {
|
|||
// TIERÄRZTE
|
||||
// ----------------------------------------------------------
|
||||
const tieraerzte = {
|
||||
list() { return get('/tieraerzte'); },
|
||||
create(data) { return post('/tieraerzte', data); },
|
||||
update(id, d) { return patch(`/tieraerzte/${id}`, d); },
|
||||
list() { return get('/tieraerzte'); },
|
||||
create(data) { return post('/tieraerzte', data); },
|
||||
update(id, d) { return patch(`/tieraerzte/${id}`, d); },
|
||||
osmNearby(lat, lon) { return get(`/tieraerzte/osm-nearby?lat=${lat}&lon=${lon}`); },
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -598,13 +600,18 @@ const API = (() => {
|
|||
}
|
||||
}
|
||||
|
||||
// Lokale Gerätezeit als ISO-String ("2026-04-26T12:00:00") für server-seitige created_at
|
||||
function clientNow() {
|
||||
return new Date().toLocaleString('sv').replace(' ', 'T');
|
||||
}
|
||||
|
||||
// Öffentliche API
|
||||
return {
|
||||
get, post, put, patch, del, upload,
|
||||
auth, dogs, diary, health, tieraerzte, poison,
|
||||
places, routes, walks, events, sitting, forum, lost, knigge, weather, push,
|
||||
friends, chat, webcal, importData, sharing, widget, notifications, services, ratings, sittingAccess, training, notes,
|
||||
subscribeToPush, getLocation,
|
||||
subscribeToPush, getLocation, clientNow,
|
||||
APIError,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '408'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '411'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
|
||||
const App = (() => {
|
||||
|
||||
|
|
|
|||
|
|
@ -1732,6 +1732,7 @@ window.Page_diary = (() => {
|
|||
gps_lat: _locLat,
|
||||
gps_lon: _locLon,
|
||||
location_name: _locName,
|
||||
client_time: API.clientNow(),
|
||||
};
|
||||
|
||||
async function _uploadNewFiles(entryId) {
|
||||
|
|
@ -2012,7 +2013,7 @@ window.Page_diary = (() => {
|
|||
const text = textarea.value.trim();
|
||||
UI.setLoading(saveBtn, true);
|
||||
try {
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName };
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName, client_time: API.clientNow() };
|
||||
if (existingNoteId) {
|
||||
await API.notes.update(existingNoteId, payload);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -531,7 +531,7 @@ window.Page_forum = (() => {
|
|||
if (!text) { UI.toast.warning('Bitte Text eingeben.'); return; }
|
||||
|
||||
await UI.asyncButton(btn, async () => {
|
||||
const post = await API.forum.addPost(thread.id, { text });
|
||||
const post = await API.forum.addPost(thread.id, { text, client_time: API.clientNow() });
|
||||
|
||||
// Foto hochladen falls vorhanden
|
||||
const files = Array.from(document.getElementById('forum-reply-file')?.files || []);
|
||||
|
|
@ -900,12 +900,13 @@ window.Page_forum = (() => {
|
|||
const loc = _picker ? _picker.getValue() : { lat: null, lon: null, name: null };
|
||||
|
||||
const created = await API.forum.create({
|
||||
kategorie: fd.kategorie,
|
||||
titel: (fd.titel || '').trim(),
|
||||
text: (fd.text || '').trim(),
|
||||
thread_lat: loc.lat ?? null,
|
||||
thread_lon: loc.lon ?? null,
|
||||
thread_ort: loc.name ?? null,
|
||||
kategorie: fd.kategorie,
|
||||
titel: (fd.titel || '').trim(),
|
||||
text: (fd.text || '').trim(),
|
||||
thread_lat: loc.lat ?? null,
|
||||
thread_lon: loc.lon ?? null,
|
||||
thread_ort: loc.name ?? null,
|
||||
client_time: API.clientNow(),
|
||||
});
|
||||
|
||||
// Fotos hochladen
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ window.Page_health = (() => {
|
|||
</button>
|
||||
</div>
|
||||
${transponderHtml}
|
||||
<div id="health-ki-berichte"></div>
|
||||
<div id="health-reminders"></div>
|
||||
<div class="by-tabs" id="by-tabs"></div>
|
||||
<div id="by-tab-content"></div>
|
||||
|
|
@ -166,6 +167,7 @@ window.Page_health = (() => {
|
|||
await _loadAll();
|
||||
_renderErinnerungen();
|
||||
_renderTab();
|
||||
_loadKiBerichte(dog.id);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -1009,7 +1011,8 @@ window.Page_health = (() => {
|
|||
if (praxis) {
|
||||
const adresse = [praxis.strasse, [praxis.plz, praxis.ort].filter(Boolean).join(' ')].filter(Boolean).join(', ');
|
||||
const tel = praxis.telefon ? ` · <a href="tel:${_esc(praxis.telefon)}">${_esc(praxis.telefon)}</a>` : '';
|
||||
rows.push(['Praxis', `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> ${_esc(praxis.name)}${adresse ? `<br><small style="color:var(--c-text-secondary)">${_esc(adresse)}${tel}</small>` : tel}`]);
|
||||
const oh = praxis.opening_hours ? `<br><small style="color:var(--c-text-secondary)">🕐 ${_esc(_fmtOeffnungszeiten(praxis.opening_hours))}</small>` : '';
|
||||
rows.push(['Praxis', `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> ${_esc(praxis.name)}${adresse ? `<br><small style="color:var(--c-text-secondary)">${_esc(adresse)}${tel}</small>` : tel}${oh}`]);
|
||||
}
|
||||
} else if (e.tierarzt_name) {
|
||||
rows.push(['Tierarzt', _esc(e.tierarzt_name)]);
|
||||
|
|
@ -1561,6 +1564,11 @@ window.Page_health = (() => {
|
|||
<div class="health-card-meta">
|
||||
${[p.strasse, [p.plz, p.ort].filter(Boolean).join(' ')].filter(Boolean).map(_esc).join(', ')}
|
||||
</div>` : ''}
|
||||
${p.opening_hours ? `
|
||||
<div class="health-card-meta" style="margin-top:var(--space-1)">
|
||||
<svg class="ph-icon" aria-hidden="true" style="font-size:0.9em"><use href="/icons/phosphor.svg#clock"></use></svg>
|
||||
${_esc(_fmtOeffnungszeiten(p.opening_hours))}
|
||||
</div>` : ''}
|
||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2);flex-wrap:wrap">
|
||||
${p.telefon ? `
|
||||
<a href="tel:${_esc(p.telefon)}" class="btn btn-secondary btn-sm"
|
||||
|
|
@ -1642,10 +1650,24 @@ window.Page_health = (() => {
|
|||
<input class="form-control" type="email" name="email"
|
||||
value="${_esc(praxis?.email || '')}" placeholder="praxis@beispiel.de">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
Öffnungszeiten
|
||||
<button type="button" id="praxis-osm-lookup" class="btn btn-secondary btn-sm"
|
||||
style="margin-left:var(--space-2);font-size:var(--text-xs)">
|
||||
📍 Aus Karte laden
|
||||
</button>
|
||||
</label>
|
||||
<input class="form-control" type="text" name="opening_hours"
|
||||
id="praxis-opening-hours"
|
||||
value="${_esc(praxis?.opening_hours || '')}"
|
||||
placeholder="z.B. Mo–Fr 08:00–18:00 · Sa 09:00–13:00">
|
||||
<div id="praxis-osm-results" style="display:none;margin-top:var(--space-2)"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Notizen</label>
|
||||
<textarea class="form-control" name="notizen" rows="2"
|
||||
placeholder="Öffnungszeiten, Besonderheiten…">${_esc(praxis?.notizen || '')}</textarea>
|
||||
placeholder="Besonderheiten, interne Hinweise…">${_esc(praxis?.notizen || '')}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
|
||||
|
|
@ -1670,6 +1692,68 @@ window.Page_health = (() => {
|
|||
|
||||
document.getElementById('praxis-cancel')?.addEventListener('click', UI.modal.close);
|
||||
|
||||
// OSM-Lookup: Tierarztpraxen in der Nähe suchen und Öffnungszeiten übernehmen
|
||||
document.getElementById('praxis-osm-lookup')?.addEventListener('click', async btn => {
|
||||
const lookupBtn = document.getElementById('praxis-osm-lookup');
|
||||
const resultsEl = document.getElementById('praxis-osm-results');
|
||||
lookupBtn.disabled = true;
|
||||
lookupBtn.textContent = 'Suche…';
|
||||
try {
|
||||
const pos = await API.getLocation();
|
||||
const hits = await API.tieraerzte.osmNearby(pos.lat, pos.lon);
|
||||
if (!hits.length) {
|
||||
resultsEl.style.display = 'block';
|
||||
resultsEl.innerHTML = '<p style="font-size:var(--text-sm);color:var(--c-text-secondary)">Keine Praxen in der Nähe im OSM-Cache gefunden.</p>';
|
||||
} else {
|
||||
resultsEl.style.display = 'block';
|
||||
resultsEl.innerHTML = hits.map(h => `
|
||||
<div class="health-card" style="margin-bottom:var(--space-2)">
|
||||
<div style="cursor:pointer;flex:1"
|
||||
data-osm-id="${_esc(h.osm_id)}"
|
||||
data-name="${_esc(h.name)}"
|
||||
data-oh="${_esc(h.opening_hours || '')}"
|
||||
data-phone="${_esc(h.phone || '')}"
|
||||
data-action="pick-osm">
|
||||
<div style="font-weight:600">${_esc(h.name)}</div>
|
||||
${h.opening_hours_fmt ? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">${_esc(h.opening_hours_fmt)}</div>` : '<div style="font-size:var(--text-sm);color:var(--c-text-muted)">Öffnungszeiten unbekannt</div>'}
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">${h.distanz_km} km entfernt</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" style="flex-shrink:0;align-self:flex-start"
|
||||
data-action="korrigieren"
|
||||
data-osm-id="${_esc(h.osm_id)}"
|
||||
data-poi-name="${_esc(h.name)}"
|
||||
data-current-oh="${_esc(h.opening_hours || '')}">
|
||||
✏️
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
resultsEl.querySelectorAll('[data-action="pick-osm"]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const nameInput = document.querySelector('[name="name"]');
|
||||
const ohInput = document.getElementById('praxis-opening-hours');
|
||||
const telInput = document.querySelector('[name="telefon"]');
|
||||
if (nameInput && !nameInput.value) nameInput.value = el.dataset.name;
|
||||
if (ohInput) ohInput.value = el.dataset.oh;
|
||||
if (telInput && !telInput.value) telInput.value = el.dataset.phone;
|
||||
resultsEl.style.display = 'none';
|
||||
UI.toast.success('Daten übernommen.');
|
||||
});
|
||||
});
|
||||
resultsEl.querySelectorAll('[data-action="korrigieren"]').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
_showPoiKorrekturModal(btn.dataset.osmId, btn.dataset.poiName, btn.dataset.currentOh);
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
UI.toast.warning('Standort nicht verfügbar oder kein OSM-Cache in der Nähe.');
|
||||
} finally {
|
||||
lookupBtn.disabled = false;
|
||||
lookupBtn.textContent = '📍 Aus Karte laden';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('praxis-form')?.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const btn = document.querySelector('[form="praxis-form"][type="submit"]') || e.target.querySelector('[type="submit"]');
|
||||
|
|
@ -1686,6 +1770,7 @@ window.Page_health = (() => {
|
|||
email: fd.email || null,
|
||||
notizen: fd.notizen || null,
|
||||
ist_notfallpraxis: 'ist_notfallpraxis' in fd,
|
||||
opening_hours: fd.opening_hours || null,
|
||||
};
|
||||
if (!payload.name) { UI.toast.warning('Bitte einen Namen eingeben.'); return; }
|
||||
|
||||
|
|
@ -1850,6 +1935,59 @@ window.Page_health = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// KI-GESUNDHEITSBERICHTE (gespeicherte automatische Berichte)
|
||||
// ----------------------------------------------------------
|
||||
async function _loadKiBerichte(dogId) {
|
||||
const el = _container.querySelector('#health-ki-berichte');
|
||||
if (!el) return;
|
||||
try {
|
||||
const berichte = await API.health.kiBerichte(dogId);
|
||||
if (!berichte || berichte.length === 0) return;
|
||||
const neuester = berichte[0];
|
||||
const datum = neuester.erstellt_at
|
||||
? new Date(neuester.erstellt_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
: '';
|
||||
const preview = neuester.bericht.length > 180
|
||||
? _esc(neuester.bericht.slice(0, 180)) + '…'
|
||||
: _esc(neuester.bericht);
|
||||
el.innerHTML = `
|
||||
<div class="health-ki-bericht-banner" style="
|
||||
background:var(--c-surface-2,#f7f2eb);
|
||||
border:1px solid var(--c-border,#e2d9ce);
|
||||
border-radius:var(--radius-md,10px);
|
||||
padding:var(--space-3) var(--space-4);
|
||||
margin-bottom:var(--space-3);
|
||||
cursor:pointer;
|
||||
">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-1)">
|
||||
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0"><use href="/icons/phosphor.svg#star"></use></svg>
|
||||
<strong style="font-size:var(--text-sm)">KI-Gesundheitsbericht</strong>
|
||||
${datum ? `<span style="font-size:var(--text-xs);color:var(--c-text-muted);margin-left:auto">${datum}</span>` : ''}
|
||||
</div>
|
||||
<div style="font-size:var(--text-sm);color:var(--c-text-muted);line-height:1.5">${preview}</div>
|
||||
${berichte.length > 1 ? `<div style="font-size:var(--text-xs);color:var(--c-accent,#c4843a);margin-top:var(--space-1)">${berichte.length} Berichte gespeichert — zum Öffnen tippen</div>` : ''}
|
||||
</div>`;
|
||||
el.querySelector('.health-ki-bericht-banner').addEventListener('click', () => {
|
||||
const listeHtml = berichte.map((b, i) => {
|
||||
const d = b.erstellt_at
|
||||
? new Date(b.erstellt_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
: '';
|
||||
return `<div style="${i > 0 ? 'border-top:1px solid var(--c-border);padding-top:var(--space-3);margin-top:var(--space-3)' : ''}">
|
||||
${d ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-1)">${d}</div>` : ''}
|
||||
<div style="white-space:pre-wrap;line-height:1.7;font-size:var(--text-sm)">${_esc(b.bericht)}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
UI.modal.open({
|
||||
title: `${UI.icon('star')} KI-Gesundheitsberichte`,
|
||||
body: listeHtml,
|
||||
});
|
||||
});
|
||||
} catch (_) {
|
||||
// Silently ignore — Berichte sind optional
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// KI-ZUSAMMENFASSUNG
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -1886,6 +2024,56 @@ window.Page_health = (() => {
|
|||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function _showPoiKorrekturModal(osmId, poiName, currentOh) {
|
||||
UI.modal.open({
|
||||
title: 'Öffnungszeiten korrigieren',
|
||||
body: `
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-3)">
|
||||
Korrektur für <strong>${_esc(poiName)}</strong>.<br>
|
||||
Dein Vorschlag wird von einem Moderator geprüft und dann für alle übernommen.
|
||||
</p>
|
||||
<form id="poi-korrektur-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Aktuelle Angabe</label>
|
||||
<input class="form-control" type="text" value="${_esc(currentOh)}" disabled
|
||||
style="color:var(--c-text-muted)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Korrekte Öffnungszeiten *</label>
|
||||
<input class="form-control" type="text" name="new_value" required
|
||||
placeholder="z.B. Mo–Fr 08:00–18:00 · Sa 09:00–13:00"
|
||||
value="${_esc(currentOh)}">
|
||||
</div>
|
||||
</form>
|
||||
`,
|
||||
footer: `
|
||||
<button type="button" class="btn btn-secondary flex-1" id="poi-kor-cancel">Abbrechen</button>
|
||||
<button type="submit" form="poi-korrektur-form" class="btn btn-primary flex-1">Einreichen</button>
|
||||
`,
|
||||
});
|
||||
document.getElementById('poi-kor-cancel')?.addEventListener('click', UI.modal.close);
|
||||
document.getElementById('poi-korrektur-form')?.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const btn = document.querySelector('[form="poi-korrektur-form"][type="submit"]');
|
||||
const fd = UI.formData(e.target);
|
||||
await UI.asyncButton(btn, async () => {
|
||||
await API.post(`/osm/pois/${encodeURIComponent(osmId)}/edit`, {
|
||||
poi_name: poiName,
|
||||
field: 'opening_hours',
|
||||
new_value: fd.new_value.trim(),
|
||||
});
|
||||
UI.modal.close();
|
||||
UI.toast.success('Danke! Dein Vorschlag wird geprüft.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _fmtOeffnungszeiten(raw) {
|
||||
if (!raw) return '';
|
||||
if (raw.trim().toLowerCase() === '24/7') return '24/7 geöffnet';
|
||||
return raw.split(';').map(s => s.trim()).filter(Boolean).join(' · ');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// NOTIZ-MODAL (custom DOM, kein UI.modal um Konflikte zu vermeiden)
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -629,6 +629,7 @@ window.Page_lost = (() => {
|
|||
lat : parseFloat(fd.lat),
|
||||
lon : parseFloat(fd.lon),
|
||||
dog_id : fd.dog_id ? parseInt(fd.dog_id) : null,
|
||||
client_time : API.clientNow(),
|
||||
};
|
||||
|
||||
const created = await API.lost.report(payload);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ window.Page_moderation = (() => {
|
|||
{ id: 'fotos', label: 'Fotos', icon: 'image' },
|
||||
{ id: 'user', label: 'User', icon: 'users' },
|
||||
{ id: 'forum', label: 'Forum', icon: 'chat-circle-dots' },
|
||||
{ id: 'poi-edits', label: 'POI-Edits', icon: 'clock' },
|
||||
];
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -77,6 +78,7 @@ window.Page_moderation = (() => {
|
|||
case 'fotos': await _renderFotos(el); break;
|
||||
case 'user': await _renderUsers(el); break;
|
||||
case 'forum': await _renderForum(el); break;
|
||||
case 'poi-edits': await _renderPoiEdits(el); break;
|
||||
}
|
||||
} catch (e) {
|
||||
el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.');
|
||||
|
|
@ -106,6 +108,10 @@ window.Page_moderation = (() => {
|
|||
'Züchter ausstehend',
|
||||
s.pending_zuchter,
|
||||
s.pending_zuchter > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')}
|
||||
${_statCard('clock',
|
||||
'POI-Korrekturen',
|
||||
s.pending_poi_edits ?? 0,
|
||||
(s.pending_poi_edits ?? 0) > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')}
|
||||
</div>
|
||||
|
||||
<div class="card" style="padding:var(--space-4);margin-top:var(--space-4)">
|
||||
|
|
@ -456,6 +462,76 @@ window.Page_moderation = (() => {
|
|||
`;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB: POI-KORREKTUREN
|
||||
// ------------------------------------------------------------------
|
||||
async function _renderPoiEdits(el) {
|
||||
const edits = await API.get('/moderation/poi-edits');
|
||||
if (!edits.length) {
|
||||
el.innerHTML = _emptyState('check-circle', 'Alles erledigt', 'Keine ausstehenden POI-Korrekturen.');
|
||||
return;
|
||||
}
|
||||
|
||||
const STATUS_LABEL = { pending: 'Ausstehend', approved: 'Genehmigt', rejected: 'Abgelehnt' };
|
||||
const STATUS_COLOR = { pending: 'var(--c-warning)', approved: 'var(--c-success,#22c55e)', rejected: 'var(--c-danger)' };
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-3)">
|
||||
${edits.map(e => `
|
||||
<div class="card" style="padding:var(--space-4)" data-edit-id="${e.id}">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-2);flex-wrap:wrap">
|
||||
<div>
|
||||
<div style="font-weight:600">${_esc(e.poi_name)}</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">
|
||||
OSM-ID: ${_esc(e.osm_id)} · Feld: ${_esc(e.field)} · von ${_esc(e.einreicher_name)}
|
||||
· ${new Date(e.created_at).toLocaleDateString('de-DE')}
|
||||
</div>
|
||||
</div>
|
||||
<span style="font-size:var(--text-xs);font-weight:600;color:${STATUS_COLOR[e.status] || 'inherit'}">
|
||||
${STATUS_LABEL[e.status] || e.status}
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin-top:var(--space-3);display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2)">
|
||||
<div style="background:var(--c-surface-2);border-radius:var(--radius-sm);padding:var(--space-2)">
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Aktuell</div>
|
||||
<div style="font-size:var(--text-sm)">${_esc(e.old_value) || '<em style="color:var(--c-text-muted)">leer</em>'}</div>
|
||||
</div>
|
||||
<div style="background:var(--c-surface-2);border-radius:var(--radius-sm);padding:var(--space-2)">
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Vorschlag</div>
|
||||
<div style="font-size:var(--text-sm);font-weight:600">${_esc(e.new_value)}</div>
|
||||
</div>
|
||||
</div>
|
||||
${e.status === 'pending' ? `
|
||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3)">
|
||||
<button class="btn btn-primary btn-sm flex-1" data-action="approve" data-id="${e.id}">
|
||||
Übernehmen
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm flex-1" data-action="reject" data-id="${e.id}">
|
||||
Ablehnen
|
||||
</button>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
el.querySelectorAll('[data-action]').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = parseInt(btn.dataset.id);
|
||||
const action = btn.dataset.action;
|
||||
btn.disabled = true;
|
||||
try {
|
||||
await API.patch(`/moderation/poi-edits/${id}`, { action });
|
||||
UI.toast.success(action === 'approve' ? 'Übernommen!' : 'Abgelehnt.');
|
||||
await _renderPoiEdits(el);
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler.');
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _esc(s) {
|
||||
if (!s) return '';
|
||||
return String(s)
|
||||
|
|
|
|||
|
|
@ -630,6 +630,7 @@ window.Page_routes = (() => {
|
|||
leine_empfohlen: 'leine_empfohlen' in fd,
|
||||
is_public: 'is_public' in fd,
|
||||
hunde_tauglichkeit: fd.hunde_tauglichkeit || 'sehr_gut',
|
||||
client_time: API.clientNow(),
|
||||
});
|
||||
UI.modal.close();
|
||||
UI.toast.success(`Route „${saved.name}" gespeichert!`);
|
||||
|
|
@ -2428,6 +2429,7 @@ window.Page_routes = (() => {
|
|||
leine_empfohlen: document.getElementById('ri-leine')?.checked,
|
||||
is_public: document.getElementById('ri-public')?.checked,
|
||||
hunde_tauglichkeit: _selPaw,
|
||||
client_time: API.clientNow(),
|
||||
});
|
||||
UI.modal.close();
|
||||
UI.toast.success('Route importiert! 🥾');
|
||||
|
|
@ -2551,7 +2553,7 @@ window.Page_routes = (() => {
|
|||
|
||||
ovl.querySelector('#rk-note-save')?.addEventListener('click', async () => {
|
||||
const text = ovl.querySelector('#rk-note-text')?.value?.trim() || '';
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName || null };
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName || null, client_time: API.clientNow() };
|
||||
try {
|
||||
if (existingNote?.id) {
|
||||
await API.notes.update(existingNote.id, payload);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue