Sprint 15: Suche, Ausweis, Teilen, Widget

- Volltext-Suche im Tagebuch (LIKE über Titel/Text/Tags, Debounce 350ms)
- Digitaler Heimtierausweis als druckbare HTML-Seite (/ausweis/{dog_id})
  Enthält Impfungen, Medikamente, Allergien, Tierärzte, Chip-Nr.
- Hund teilen: Einladungslink-System (dog_shares-Tabelle, /teilen/{token})
  Geteilte Hunde erscheinen in der Hundeliste, Tagebuch/Gesundheit lesbar
- Widget-Seite /#widget: zufälliges Tagebuchbild + nächste Erinnerung
  Als PWA-Shortcut im Manifest verankert
- SW-Cache by-v144, APP_VER 117
This commit is contained in:
rene 2026-04-17 15:51:09 +02:00
parent d5f09cd16b
commit 34f29f9d0a
16 changed files with 917 additions and 35 deletions

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '116'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '117'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const App = (() => {
@ -56,6 +56,7 @@ const App = (() => {
admin: { title: 'Admin', module: null, requiresAuth: true },
impressum: { title: 'Impressum', module: null },
datenschutz: { title: 'Datenschutz', module: null },
widget: { title: 'Widget', module: null, requiresAuth: true },
};
// ----------------------------------------------------------
@ -573,9 +574,16 @@ const App = (() => {
_bindNavigation();
await _checkAuth();
// Einladungslink /teilen/{token} → direkt annehmen
const inviteMatch = location.pathname.match(/^\/teilen\/([A-Za-z0-9_-]+)$/);
if (inviteMatch) {
const token = inviteMatch[1];
navigate('diary', false);
_handleInvite(token);
return;
}
// Erste Seite laden: Hash aus URL oder Standard 'diary'.
// Bewusst NACH _checkAuth(), damit _loadPage() nur einmal aufgerufen wird
// (vorher war Hash-Navigation auch in _bindNavigation() → doppelter Aufruf).
const rawHash = location.hash.replace('#', '');
const [hashPage, hashQuery] = rawHash.split('?');
const hashParams = {};
@ -588,6 +596,38 @@ const App = (() => {
navigate(startPage, false, hashParams);
}
async function _handleInvite(token) {
try {
const info = await API.sharing.info(token);
if (info.accepted_at) {
UI.toast.success(`Du hast bereits Zugriff auf ${info.dog_name}.`);
history.replaceState(null, '', '/');
return;
}
const ok = await UI.modal.confirm(
`<strong>${UI.escape(info.owner_name)}</strong> möchte das Profil von
<strong>${UI.escape(info.dog_name)}</strong> mit dir teilen
(${info.role === 'editor' ? 'Lesen & Schreiben' : 'Nur lesen'}).
Möchtest du die Einladung annehmen?`
);
if (!ok) { history.replaceState(null, '', '/'); return; }
await API.sharing.accept(token);
// Hundeliste neu laden
state.dogs = await API.dogs.list();
const newDog = state.dogs.find(d => d.name === info.dog_name);
if (newDog) {
state.activeDog = newDog;
localStorage.setItem('by_active_dog', String(newDog.id));
_renderDogSwitcher();
}
history.replaceState(null, '', '/');
UI.toast.success(`${UI.escape(info.dog_name)} wurde deiner Liste hinzugefügt!`);
} catch (e) {
UI.toast.error(e.message || 'Einladungslink ungültig.');
history.replaceState(null, '', '/');
}
}
// ----------------------------------------------------------
// AUTH-GATE HELPER — einheitlicher "Bitte anmelden"-Block
// ----------------------------------------------------------