Sprint 18: Notification Center, Routen entdecken, Onboarding, Admin-Erweiterungen
- Notifications: History-Tabelle, /api/notifications Endpoints, push.py schreibt in DB - Notifications: Page (notifications.js) mit Badge, Typen-Icons, gelesen-Markierung - Routen: Entdecken-Modus mit Ersteller-Anzeige, Nearby-Filter, Mine/Discover Toggle - Onboarding: Willkommens-Modal nach Registrierung, Push-Angebot nach Login - Admin: Scheduler-Tab (Jobs anzeigen + manuell triggern), System-Health (DB/Disk/Uptime) - Admin: Audit-Log (wer hat was wann gemacht), erweiterte Stats (Push-Abos, aktive User, Routen) - SW: by-v152, APP_VER 125
This commit is contained in:
parent
5927d384bf
commit
92620c2c52
14 changed files with 1035 additions and 46 deletions
|
|
@ -17,6 +17,9 @@ window.Page_routes = (() => {
|
|||
let _sortBy = 'newest';
|
||||
let _onlyMine = false;
|
||||
|
||||
// 'mine' | 'discover'
|
||||
let _browseMode = 'mine';
|
||||
|
||||
// Ansichts-Modus: 'list' | 'map'
|
||||
let _viewMode = 'list';
|
||||
let _searchMap = null; // L.map Instanz der Suchkarte
|
||||
|
|
@ -76,6 +79,10 @@ window.Page_routes = (() => {
|
|||
_container.innerHTML = `
|
||||
<div class="rk-layout">
|
||||
<div class="rk-header">
|
||||
<div class="rk-mode-toggle" id="rk-mode-toggle">
|
||||
<button class="rk-mode-btn${_browseMode==='mine'?' active':''}" id="rk-mode-mine">${UI.icon('user')} Meine Routen</button>
|
||||
<button class="rk-mode-btn${_browseMode==='discover'?' active':''}" id="rk-mode-discover">${UI.icon('compass')} Entdecken</button>
|
||||
</div>
|
||||
<div class="rk-search-row">
|
||||
<input class="rk-search" id="rk-search" type="search"
|
||||
placeholder="🔍 Route suchen…" autocomplete="off">
|
||||
|
|
@ -83,7 +90,7 @@ window.Page_routes = (() => {
|
|||
<button class="rk-view-btn${_viewMode==='list'?' active':''}" id="rk-view-list" title="Liste">${UI.icon('list')}</button>
|
||||
<button class="rk-view-btn${_viewMode==='map'?' active':''}" id="rk-view-map" title="Karte">${UI.icon('map-trifold')}</button>
|
||||
</div>
|
||||
<label class="btn btn-secondary btn-sm rk-imp-btn" title="GPX / KML / TCX importieren">
|
||||
<label class="btn btn-secondary btn-sm rk-imp-btn" id="rk-imp-wrap" title="GPX / KML / TCX importieren">
|
||||
${UI.icon('download-simple')} Import
|
||||
<input type="file" id="rk-import-input" accept=".gpx,.kml,.tcx" style="display:none">
|
||||
</label>
|
||||
|
|
@ -112,6 +119,9 @@ window.Page_routes = (() => {
|
|||
<div class="rk-filter-group" id="rk-mine-group" style="display:none">
|
||||
<button class="rk-chip" data-filter="mine" data-val="mine">🔒 Nur meine</button>
|
||||
</div>
|
||||
<div class="rk-filter-group" id="rk-nearby-group" style="display:none">
|
||||
<button class="rk-chip" id="rk-nearby-btn" data-filter="nearby" data-val="">${UI.icon('map-pin')} In meiner Nähe</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rk-grid" id="rk-grid">
|
||||
|
|
@ -145,8 +155,50 @@ window.Page_routes = (() => {
|
|||
if (filter === 'terrain') _terrain = val;
|
||||
if (filter === 'sort') _sortBy = val;
|
||||
if (filter === 'mine') _onlyMine = chip.classList.contains('active') && val === 'mine';
|
||||
if (filter === 'nearby') { _loadDataNearby(); return; } // async, calls _applyFilter itself
|
||||
_applyFilter();
|
||||
});
|
||||
// Mode toggle
|
||||
document.getElementById('rk-mode-mine').addEventListener('click', () => _setBrowseMode('mine'));
|
||||
document.getElementById('rk-mode-discover').addEventListener('click', () => _setBrowseMode('discover'));
|
||||
}
|
||||
|
||||
function _setBrowseMode(mode) {
|
||||
_browseMode = mode;
|
||||
document.getElementById('rk-mode-mine')?.classList.toggle('active', mode === 'mine');
|
||||
document.getElementById('rk-mode-discover')?.classList.toggle('active', mode === 'discover');
|
||||
|
||||
const recBtn = document.getElementById('rk-rec-btn');
|
||||
const impWrap = document.getElementById('rk-imp-wrap');
|
||||
const mineGrp = document.getElementById('rk-mine-group');
|
||||
const nearbyGrp = document.getElementById('rk-nearby-group');
|
||||
|
||||
if (mode === 'discover') {
|
||||
if (recBtn) recBtn.style.display = 'none';
|
||||
if (impWrap) impWrap.style.display = 'none';
|
||||
if (mineGrp) mineGrp.style.display = 'none';
|
||||
if (nearbyGrp && _userPos) nearbyGrp.style.display = '';
|
||||
} else {
|
||||
if (recBtn) recBtn.style.display = '';
|
||||
if (impWrap) impWrap.style.display = '';
|
||||
if (_appState.user && mineGrp) mineGrp.style.display = '';
|
||||
if (nearbyGrp) nearbyGrp.style.display = 'none';
|
||||
}
|
||||
_onlyMine = false;
|
||||
document.querySelectorAll('#rk-mine-group .rk-chip').forEach(c => c.classList.remove('active'));
|
||||
_applyFilter();
|
||||
}
|
||||
|
||||
async function _loadDataNearby() {
|
||||
if (!_userPos) {
|
||||
try { _userPos = await API.getLocation(); } catch { UI.toast.warning('Standort nicht verfügbar.'); return; }
|
||||
}
|
||||
try {
|
||||
_data = await API.routes.listNearby(_userPos.lat, _userPos.lon, 10000);
|
||||
_applyFilter();
|
||||
} catch (err) {
|
||||
UI.toast.error('Fehler beim Laden: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -328,10 +380,14 @@ window.Page_routes = (() => {
|
|||
async function _loadData() {
|
||||
try {
|
||||
_data = await API.routes.list();
|
||||
// "Meine Routen"-Filter nur zeigen wenn eingeloggt
|
||||
if (_appState.user) {
|
||||
// "Meine Routen"-Filter nur zeigen wenn eingeloggt und im Mine-Modus
|
||||
if (_appState.user && _browseMode === 'mine') {
|
||||
document.getElementById('rk-mine-group')?.style.setProperty('display', '');
|
||||
}
|
||||
// Standort-abhängiger Filter im Entdecken-Modus
|
||||
if (_browseMode === 'discover' && _userPos) {
|
||||
document.getElementById('rk-nearby-group')?.style.setProperty('display', '');
|
||||
}
|
||||
_applyFilter();
|
||||
} catch (err) {
|
||||
document.getElementById('rk-grid').innerHTML =
|
||||
|
|
@ -346,13 +402,21 @@ window.Page_routes = (() => {
|
|||
|
||||
function _applyFilter() {
|
||||
let list = [..._data];
|
||||
|
||||
// Browse-Modus-Filter
|
||||
if (_browseMode === 'mine' && _appState.user) {
|
||||
list = list.filter(r => r.user_id === _appState.user.id);
|
||||
} else if (_browseMode === 'discover' && _appState.user) {
|
||||
list = list.filter(r => r.user_id !== _appState.user.id);
|
||||
}
|
||||
|
||||
if (_search) list = list.filter(r =>
|
||||
(r.name||'').toLowerCase().includes(_search) ||
|
||||
(r.beschreibung||'').toLowerCase().includes(_search) ||
|
||||
(r.user_name||'').toLowerCase().includes(_search));
|
||||
if (_difficulty) list = list.filter(r => r.schwierigkeit === _difficulty);
|
||||
if (_terrain) list = list.filter(r => r.untergrund === _terrain);
|
||||
if (_onlyMine && _appState.user)
|
||||
if (_onlyMine && _appState.user && _browseMode === 'mine')
|
||||
list = list.filter(r => r.user_id === _appState.user.id);
|
||||
|
||||
if (_sortBy === 'distance') list.sort((a,b) => (b.distanz_km||0) - (a.distanz_km||0));
|
||||
|
|
@ -387,8 +451,15 @@ window.Page_routes = (() => {
|
|||
document.querySelector('.rk-chip[data-val="newest"]')?.classList.add('active');
|
||||
_applyFilter();
|
||||
});
|
||||
} else if (_browseMode === 'discover') {
|
||||
// Entdecken: keine fremden Routen vorhanden
|
||||
grid.innerHTML = `<div class="rk-empty">
|
||||
<div class="rk-empty-icon">${UI.icon('compass')}</div>
|
||||
<h3 class="rk-empty-title">Noch keine öffentlichen Routen</h3>
|
||||
<p class="rk-empty-text">Andere Nutzer haben noch keine Routen geteilt. Sei der Erste!</p>
|
||||
</div>`;
|
||||
} else {
|
||||
// Noch gar keine Routen
|
||||
// Noch gar keine eigenen Routen
|
||||
grid.innerHTML = `<div class="rk-empty rk-empty--onboarding">
|
||||
<div class="rk-empty-icon">🥾</div>
|
||||
<h3 class="rk-empty-title">Deine erste Gassi-Route</h3>
|
||||
|
|
@ -440,6 +511,7 @@ window.Page_routes = (() => {
|
|||
// Karte HTML
|
||||
// ----------------------------------------------------------
|
||||
function _cardHTML(r) {
|
||||
const isDiscover = _browseMode === 'discover';
|
||||
const privBadge = !r.is_public ? `<span class="rk-badge rk-badge--private">${UI.icon('lock')} Privat</span>` : '';
|
||||
const diffLabel = DIFFICULTY_LABEL[r.schwierigkeit] || '';
|
||||
const terrain = TERRAIN_LABEL[r.untergrund] || '';
|
||||
|
|
@ -453,10 +525,15 @@ window.Page_routes = (() => {
|
|||
data-track='${JSON.stringify(r.preview_track||[])}'
|
||||
style="width:100%;height:100%"></div>`;
|
||||
|
||||
const authorLine = isDiscover
|
||||
? `<div class="rk-card-creator">${UI.icon('user')} ${_esc(r.user_name||'Anonym')}</div>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="rk-card" data-id="${r.id}">
|
||||
<div class="rk-card-preview">${previewContent}</div>
|
||||
<div class="rk-card-body">
|
||||
${authorLine}
|
||||
<div class="rk-card-name">${_esc(r.name)}</div>
|
||||
<div class="rk-card-stats">
|
||||
${dist ? `<span>${UI.icon('map-trifold')} ${dist}</span>` : ''}
|
||||
|
|
@ -465,7 +542,7 @@ window.Page_routes = (() => {
|
|||
${paws ? `<span title="Hundetauglichkeit">${paws}</span>` : ''}
|
||||
</div>
|
||||
<div class="rk-card-tags">
|
||||
${privBadge}
|
||||
${isDiscover ? '' : privBadge}
|
||||
${diffLabel ? `<span class="rk-badge rk-badge--${r.schwierigkeit}">${diffLabel}</span>` : ''}
|
||||
${r.schatten ? `<span class="rk-badge">${UI.icon('tree')} Schatten</span>` : ''}
|
||||
${r.leine_empfohlen ? `<span class="rk-badge">${UI.icon('link')} Leine</span>` : ''}
|
||||
|
|
@ -473,7 +550,7 @@ window.Page_routes = (() => {
|
|||
<div class="rk-card-footer">
|
||||
<div class="rk-stars">${_starsHTML(r.id, r.bewertung||0, r.anz_bewertungen||0)}</div>
|
||||
<div class="rk-card-actions">
|
||||
<span class="rk-card-author">${_esc(r.user_name||'Anonym')}</span>
|
||||
${isDiscover ? '' : `<span class="rk-card-author">${_esc(r.user_name||'Anonym')}</span>`}
|
||||
<button class="rk-dl-btn" data-id="${r.id}">${UI.icon('download-simple')} GPX</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue