diff --git a/backend/routes/walks.py b/backend/routes/walks.py index 0c19987..85bea3d 100644 --- a/backend/routes/walks.py +++ b/backend/routes/walks.py @@ -252,6 +252,7 @@ async def invite_friend(walk_id: int, data: InviteRequest, user=Depends(get_curr "type": "walk_invite", "title": f"Einladung: {walk['titel']}", "body": f"{user['name']} lädt dich zu einem Gassi-Treffen ein ({walk['datum']} {walk['uhrzeit']})", + "page": "walks", "walk_id": walk_id, }) diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 54d264f..69d7a07 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '205'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '206'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { @@ -748,7 +748,13 @@ const App = (() => { // ÖFFENTLICHE API // (andere Module können App.state, App.navigate etc. nutzen) // ---------------------------------------------------------- - return { init, navigate, state, setActiveDog, renderDogSwitcher: _renderDogSwitcher, + function callModule(pageId, method, ...args) { + navigate(pageId); + setTimeout(() => pages[pageId]?.module?.[method]?.(...args), 500); + } + + return { init, navigate, callModule, state, setActiveDog, + renderDogSwitcher: _renderDogSwitcher, getInstallPrompt: () => _installPrompt, requireAuth, showOnboarding: _showOnboardingModal, updateNotifBadge: _updateNotifBadge }; diff --git a/backend/static/js/pages/notifications.js b/backend/static/js/pages/notifications.js index 6cf90bb..6737824 100644 --- a/backend/static/js/pages/notifications.js +++ b/backend/static/js/pages/notifications.js @@ -40,18 +40,60 @@ window.Page_notifications = (() => { try { return JSON.parse(raw); } catch (_) { return {}; } } - /** Ermittelt Ziel-Seite und optionale Label für den Toast */ + /** + * Gibt { page, label, go } zurück. + * go() führt die Navigation inklusive kontextspezifischer Aktion aus. + */ function _navTarget(n) { const d = _parseData(n.data); - // Explizit gesetzte Zielseite hat Vorrang - if (d.page) return { page: d.page, label: d.page }; - // Typ-basiertes Fallback + switch (n.type) { - case 'chat_message': return { page: d.conversation_id ? `chat?id=${d.conversation_id}` : 'chat', label: 'Chat' }; - case 'friend_request': return { page: 'friends', label: 'Freunde' }; - case 'milestone': return { page: 'diary', label: 'Tagebuch' }; - case 'poison_alert': return { page: 'map', label: 'Karte' }; - default: return { page: '', label: '' }; + case 'chat_message': + return { + page: 'chat', label: 'Chat', + go: d.conversation_id + ? () => App.callModule('chat', '_openThread', d.conversation_id) + : () => App.navigate('chat'), + }; + + case 'friend_request': + return { + page: 'friends', label: 'Freunde', + go: () => App.navigate('friends'), + }; + + case 'walk_invite': + return { + page: 'walks', label: 'Gassi-Treffen', + go: () => App.navigate('walks'), + }; + + case 'poison_alert': + return { + page: 'poison', label: 'Giftköder-Alarm', + go: () => App.navigate('poison'), + }; + + case 'health_reminder': + return { + page: 'health', label: 'Gesundheit', + go: () => App.navigate('health'), + }; + + case 'milestone': + return { + page: 'diary', label: 'Tagebuch', + go: () => App.navigate('diary'), + }; + + default: { + const page = d.page || ''; + return { + page, + label: page, + go: page ? () => App.navigate(page) : null, + }; + } } } @@ -61,9 +103,12 @@ window.Page_notifications = (() => { const iconName = unread ? _iconForType(n.type) : 'bell'; const cls = ['notif-item', unread ? 'notif-unread' : ''].filter(Boolean).join(' '); const nav = _navTarget(n); + // Nur serialisierbare Felder speichern (go ist eine Funktion, nicht serialisierbar) + const navData = JSON.stringify({ page: nav.page, label: nav.label, type: n.type, + data: _parseData(n.data) }); return ` -
+
${UI.icon(iconName)}
${UI.escape(n.title)}
@@ -77,6 +122,24 @@ window.Page_notifications = (() => {
`; } + /** Führt die kontextspezifische Navigation aus */ + function _execNav(nav) { + switch (nav.type) { + case 'chat_message': + nav.data?.conversation_id + ? App.callModule('chat', '_openThread', nav.data.conversation_id) + : App.navigate('chat'); + break; + case 'friend_request': App.navigate('friends'); break; + case 'walk_invite': App.navigate('walks'); break; + case 'poison_alert': App.navigate('poison'); break; + case 'health_reminder':App.navigate('health'); break; + case 'milestone': App.navigate('diary'); break; + default: + if (nav.page) App.navigate(nav.page); + } + } + // ---------------------------------------------------------- // init // ---------------------------------------------------------- @@ -141,23 +204,20 @@ window.Page_notifications = (() => { // Löschen-Button nicht doppelt behandeln if (e.target.closest('.notif-del-btn')) return; - const id = parseInt(el.dataset.id, 10); - const page = el.dataset.page; - const navLabel = el.dataset.navLabel; + const id = parseInt(el.dataset.id, 10); + const nav = JSON.parse(el.dataset.nav || '{}'); - // Sofortiges visuelles Feedback: als gelesen markieren + // Sofortiges visuelles Feedback el.classList.remove('notif-unread'); el.style.opacity = '0.6'; - // API-Call im Hintergrund — nicht abwarten + // Als gelesen markieren (fire & forget) API.notifications.read(id).catch(() => {}); - if (page && window.App?.navigate) { - if (navLabel) UI.toast?.(`Öffne ${navLabel}…`, 'info'); - // Kurze Pause damit der Toast sichtbar wird, dann navigieren - setTimeout(() => window.App.navigate(page), 150); + if (nav.page) { + if (nav.label) UI.toast?.(`Öffne ${nav.label}…`, 'info'); + setTimeout(() => _execNav(nav), 150); } else { - // Keine Zielseite — Opacity nach kurzer Zeit zurücksetzen setTimeout(() => { el.style.opacity = ''; }, 800); } }); diff --git a/backend/static/sw.js b/backend/static/sw.js index fc96171..abd3ff1 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v228'; +const CACHE_VERSION = 'by-v229'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten