diff --git a/backend/database.py b/backend/database.py index 50f50ee..ab82594 100644 --- a/backend/database.py +++ b/backend/database.py @@ -580,8 +580,6 @@ def _migrate(conn_factory): ("users", "password_reset_expires", "TEXT"), # Fell-Typ für personalisierte Wetter-Hinweise ("dogs", "fell_typ", "TEXT"), # kurz|mittel|lang|drahtaar|doppel|nackt - # Widerristhöhe in cm (höchster Punkt Schulterblatt → Boden) - ("dogs", "widerrist_cm", "REAL"), # Tierarzt-Bewertungen: Durchschnitt + Anzahl am Tierarzt-Datensatz ("tieraerzte", "avg_rating", "REAL DEFAULT 0"), ("tieraerzte", "anz_bewertungen", "INTEGER DEFAULT 0"), @@ -2210,45 +2208,6 @@ def _migrate(conn_factory): except Exception: pass - # Versicherungs-Verwaltung - try: - conn.execute(""" - CREATE TABLE IF NOT EXISTS dog_insurance ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, - anbieter TEXT NOT NULL, - police_nr TEXT, - jahresbeitrag REAL, - kontakt TEXT, - ablaufdatum TEXT, - notizen TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ) - """) - logger.info("Migration: dog_insurance bereit.") - except Exception as e: - logger.warning(f"Migration dog_insurance: {e}") - - # Verhaltens-Protokoll - try: - conn.execute(""" - CREATE TABLE IF NOT EXISTS behavior_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, - datum TEXT NOT NULL, - uhrzeit TEXT, - kategorie TEXT NOT NULL, - intensitaet INTEGER NOT NULL DEFAULT 3, - trigger TEXT, - notiz TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ) - """) - conn.execute("CREATE INDEX IF NOT EXISTS idx_behavior_dog ON behavior_log(dog_id, datum DESC)") - logger.info("Migration: behavior_log bereit.") - except Exception as e: - logger.warning(f"Migration behavior_log: {e}") - # route_dogs: bestehende Routen allen Hunden des Users zuweisen try: existing = conn.execute("SELECT COUNT(*) FROM route_dogs").fetchone()[0] diff --git a/backend/main.py b/backend/main.py index ea20480..dc86828 100644 --- a/backend/main.py +++ b/backend/main.py @@ -376,7 +376,7 @@ if STAGING and os.path.isdir(PROD_MEDIA_DIR): else: app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media") -APP_VER = "875" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "872" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): @@ -1407,7 +1407,6 @@ async def ausweis_page(dog_id: int, request: Request):
Geschlecht
{geschlecht}
Gewicht
{f'{dog["gewicht_kg"]} kg' if dog.get("gewicht_kg") else "–"}
Transponder
{esc(dog.get("chip_nr")) or "–"}
- {f'
Widerrist
{dog["widerrist_cm"]} cm
' if dog.get("widerrist_cm") else ''}
Besitzer
{esc(owner["name"]) if owner else "–"}
@@ -1415,7 +1414,7 @@ async def ausweis_page(dog_id: int, request: Request):
-
` : ''} - ${dog.widerrist_cm ? ` -
-
Widerrist
-
${dog.widerrist_cm} cm
-
- ` : ''}
@@ -1139,18 +1133,6 @@ window.Page_dog_profile = (() => { value="${dog?.gewicht_kg || ''}" min="0.1" max="120" step="0.1" placeholder="z. B. 28.5">
-
- - -
-
- -
-
@@ -1346,9 +1327,8 @@ window.Page_dog_profile = (() => { rasse_id: fd.rasse_id ? parseInt(fd.rasse_id) : null, geburtstag: fd.geburtstag || null, geschlecht: fd.geschlecht || null, - gewicht_kg: fd.gewicht_kg ? parseFloat(fd.gewicht_kg) : null, - widerrist_cm: fd.widerrist_cm ? parseFloat(fd.widerrist_cm) : null, - chip_nr: fd.chip_nr || null, + gewicht_kg: fd.gewicht_kg ? parseFloat(fd.gewicht_kg) : null, + chip_nr: fd.chip_nr || null, bio: fd.bio || null, is_public: 'is_public' in fd, fell_typ: fd.fell_typ || null, diff --git a/backend/static/js/pages/health.js b/backend/static/js/pages/health.js index bf68615..b984515 100644 --- a/backend/static/js/pages/health.js +++ b/backend/static/js/pages/health.js @@ -22,8 +22,6 @@ window.Page_health = (() => { { key: 'allergie', label: 'Allergien', icon: '' }, { key: 'dokument', label: 'Dokumente', icon: '' }, { key: 'praxen', label: 'Praxen', icon: '' }, - { key: 'versicherung', label: 'Versicherung', icon: '' }, - { key: 'verhalten', label: 'Verhalten', icon: '' }, ]; const LAEUFIGKEIT_TAB = { key: 'laeufigkeit', label: 'Läufigkeit', icon: '' }; @@ -113,14 +111,12 @@ window.Page_health = (() => {
-
`; _renderTabBar(); UI.bindDogChip(_container, _appState); - _loadRemindersBanner(); _container.querySelector('#health-ki-btn') .addEventListener('click', _showKiSummary); _container.querySelector('#health-ki-tierarzt-btn') @@ -336,8 +332,6 @@ window.Page_health = (() => { case 'allergie': content.innerHTML = _renderAllergien(entries); break; case 'dokument': content.innerHTML = _renderDokumente(entries); break; case 'praxen': content.innerHTML = _renderPraxen(); break; - case 'versicherung': _renderVersicherung(content); break; - case 'verhalten': _renderVerhalten(content); break; } _bindTabEvents(content); @@ -3056,331 +3050,6 @@ window.Page_health = (() => { }); } - // ============================================================== - // BEVORSTEHENDE ERINNERUNGEN (Banner oben in der Health-Seite) - // ============================================================== - async function _loadRemindersBanner() { - const dog = _appState?.activeDog; - if (!dog) return; - const wrap = _container?.querySelector('#health-reminders-banner'); - if (!wrap) return; - let items; - try { items = await API.health.reminders(dog.id); } - catch { return; } - if (!items.length) { wrap.style.display = 'none'; return; } - - const TYPE_LABEL = { impfung: 'Impfung', entwurmung: 'Entwurmung', medikament: 'Medikament' }; - const fmt = d => { try { const p = d.split('-'); return `${parseInt(p[2])}.${parseInt(p[1])}.${p[0]}`; } catch { return d; } }; - - wrap.style.display = ''; - wrap.innerHTML = items.slice(0, 3).map(r => { - const overdue = r.ueberfaellig; - const color = overdue ? 'var(--c-danger,#ef4444)' : r.delta_tage <= 3 ? '#f59e0b' : 'var(--c-primary)'; - const bg = overdue ? 'rgba(239,68,68,0.08)' : r.delta_tage <= 3 ? 'rgba(245,158,11,0.08)' : 'var(--c-primary-subtle)'; - const label = overdue ? `Überfällig seit ${Math.abs(r.delta_tage)} Tag${Math.abs(r.delta_tage)!==1?'en':''}` : - r.delta_tage === 0 ? 'Heute fällig' : - `in ${r.delta_tage} Tag${r.delta_tage!==1?'en':''}`; - return ` -
- -
- ${_esc(r.bezeichnung)} - ${TYPE_LABEL[r.typ] || r.typ} -
- ${label} -
`; - }).join(''); - } - - // ============================================================== - // TAB: VERSICHERUNG - // ============================================================== - async function _renderVersicherung(content) { - const dog = _appState?.activeDog; - if (!dog) return; - content.innerHTML = `
-
- -
`; - - let policies; - try { policies = await API.health.insuranceList(dog.id); } - catch { content.innerHTML = `

Fehler beim Laden.

`; return; } - - const _fmtDate = d => { if (!d) return '–'; try { const p=d.split('-'); return `${parseInt(p[2])}.${parseInt(p[1])}.${p[0]}`; } catch { return d; } }; - const _fmtEur = v => v ? `${v.toFixed(2).replace('.',',')} €/Jahr` : '–'; - - const cardsHtml = policies.length ? policies.map(p => ` -
-
-
-
${_esc(p.anbieter)}
- ${p.police_nr ? `
Police: ${_esc(p.police_nr)}
` : ''} -
-
- - -
-
-
-
Jahresbeitrag
${_fmtEur(p.jahresbeitrag)}
-
Läuft ab
${_fmtDate(p.ablaufdatum)}
- ${p.kontakt ? `
Kontakt
${_esc(p.kontakt)}
` : ''} - ${p.notizen ? `
Notizen
${_esc(p.notizen)}
` : ''} -
-
`).join('') : ` -
- -
Noch keine Versicherung eingetragen.
-
`; - - content.innerHTML = `
- ${cardsHtml} - -
`; - - content.querySelector('#ins-add-btn')?.addEventListener('click', () => _openInsuranceForm(dog, null, () => _renderVersicherung(content))); - content.querySelectorAll('.ins-edit-btn').forEach(btn => { - const pol = policies.find(p => p.id === parseInt(btn.dataset.id)); - btn.addEventListener('click', () => _openInsuranceForm(dog, pol, () => _renderVersicherung(content))); - }); - content.querySelectorAll('.ins-del-btn').forEach(btn => { - btn.addEventListener('click', async () => { - if (!window.confirm('Versicherung löschen?')) return; - await API.health.insuranceDelete(dog.id, parseInt(btn.dataset.id)); - _renderVersicherung(content); - }); - }); - } - - function _openInsuranceForm(dog, existing, onSave) { - const id = `ins-form-${Date.now()}`; - const body = `
-
- -
-
- -
-
-
- -
-
- -
-
-
- -
-
- -
-
`; - const footer = ` - - `; - UI.modal.open({ title: existing ? 'Versicherung bearbeiten' : 'Versicherung eintragen', body, footer }); - setTimeout(() => { - document.getElementById('ins-save-btn')?.addEventListener('click', async ev => { - ev.preventDefault(); - const form = document.getElementById(id); - if (!form) return; - const fd = new FormData(form); - const data = { - anbieter: (fd.get('anbieter')||'').trim(), - police_nr: fd.get('police_nr')||null, - jahresbeitrag: fd.get('jahresbeitrag') ? parseFloat(fd.get('jahresbeitrag')) : null, - ablaufdatum: fd.get('ablaufdatum')||null, - kontakt: fd.get('kontakt')||null, - notizen: fd.get('notizen')||null, - }; - if (!data.anbieter) { UI.toast.warning('Bitte Anbieter angeben.'); return; } - await UI.asyncButton(document.getElementById('ins-save-btn'), async () => { - try { - if (existing) await API.health.insuranceUpdate(dog.id, existing.id, data); - else await API.health.insuranceCreate(dog.id, data); - UI.modal.close(); - UI.toast.success('Gespeichert.'); - onSave(); - } catch (err) { UI.toast.error(err.message || 'Fehler.'); } - }); - }); - }, 50); - } - - // ============================================================== - // TAB: VERHALTEN - // ============================================================== - const _KAT_LABELS = { - angst: 'Angst / Panik', aggression: 'Aggression', ueberreaktion: 'Überreaktion', - ressource: 'Ressourcenverteidigung', separation: 'Trennungsangst', - leine: 'Leinenprobleme', sozial: 'Sozialkompetenz', sonstiges: 'Sonstiges', - }; - const _KAT_COLORS = { - angst: '#3b82f6', aggression: '#ef4444', ueberreaktion: '#f59e0b', - ressource: '#8b5cf6', separation: '#ec4899', leine: '#06b6d4', - sozial: '#22c55e', sonstiges: '#6b7280', - }; - const _TRIGGER_LABELS = { - fremde_hunde: 'Fremde Hunde', fremde_menschen: 'Fremde Menschen', kinder: 'Kinder', - laerm_feuerwerk: 'Feuerwerk', laerm_gewitter: 'Gewitter', auto_fahrrad: 'Autos/Fahrräder', - tierarzt: 'Tierarztbesuch', allein_zuhause: 'Allein zuhause', - andere_tiere: 'Andere Tiere', besucher_zuhause: 'Besucher', sonstiges: 'Sonstiges', - }; - - async function _renderVerhalten(content) { - const dog = _appState?.activeDog; - if (!dog) return; - content.innerHTML = `
-
- -
`; - - let resp; - try { resp = await API.health.behaviorList(dog.id); } - catch { content.innerHTML = `

Fehler beim Laden.

`; return; } - - const entries = resp.entries || []; - const fmtDate = d => { try { const p=d.split('-'); return `${parseInt(p[2])}.${parseInt(p[1])}.${p[0]}`; } catch { return d; } }; - - const listHtml = entries.length ? entries.map(e => { - const color = _KAT_COLORS[e.kategorie] || '#6b7280'; - const katLabel = _KAT_LABELS[e.kategorie] || e.kategorie; - const trigLabel = _TRIGGER_LABELS[e.trigger] || e.trigger || ''; - const dots = Array.from({length: 5}, (_,i) => - `
` - ).join(''); - return ` -
-
-
-
- ${_esc(katLabel)} - ${trigLabel ? `${_esc(trigLabel)}` : ''} - ${fmtDate(e.datum)}${e.uhrzeit ? ' ' + e.uhrzeit : ''} -
-
${dots}
- ${e.notiz ? `
${_esc(e.notiz)}
` : ''} -
- -
`; - }).join('') : ` -
- -
Noch keine Einträge. Protokolliere auffälliges Verhalten um Muster zu erkennen.
-
`; - - content.innerHTML = `
- ${listHtml} - -
`; - - content.querySelector('#beh-add-btn')?.addEventListener('click', () => _openBehaviorForm(dog, () => _renderVerhalten(content))); - content.querySelectorAll('.beh-del-btn').forEach(btn => { - btn.addEventListener('click', async () => { - if (!window.confirm('Eintrag löschen?')) return; - await API.health.behaviorDelete(dog.id, parseInt(btn.dataset.id)); - _renderVerhalten(content); - }); - }); - } - - function _openBehaviorForm(dog, onSave) { - const id = `beh-form-${Date.now()}`; - const today = new Date().toISOString().slice(0, 10); - const nowTime = (() => { const d=new Date(); return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; })(); - const body = `
-
-
- -
-
- -
-
-
- -
-
-
- ${[1,2,3,4,5].map(n => ``).join('')} -
- -
-
- -
-
- -
-
`; - const footer = ` - - `; - UI.modal.open({ title: 'Verhalten erfassen', body, footer }); - setTimeout(() => { - document.querySelectorAll('.beh-int-btn').forEach(btn => { - btn.addEventListener('click', () => { - const val = parseInt(btn.dataset.val); - document.querySelectorAll('.beh-int-btn').forEach((b,i) => { - b.style.background = i < val ? 'var(--c-primary)' : 'var(--c-bg-card)'; - b.style.color = i < val ? '#fff' : 'var(--c-text-secondary)'; - }); - const hi = document.querySelector('[name="intensitaet"]'); - if (hi) hi.value = val; - }); - }); - document.getElementById('beh-save-btn')?.addEventListener('click', async ev => { - ev.preventDefault(); - const form = document.getElementById(id); - if (!form) return; - const fd = new FormData(form); - const data = { - datum: fd.get('datum'), - uhrzeit: fd.get('uhrzeit')||null, - kategorie: fd.get('kategorie'), - intensitaet: parseInt(fd.get('intensitaet')||'3'), - trigger: fd.get('trigger')||null, - notiz: (fd.get('notiz')||'').trim()||null, - }; - if (!data.kategorie) { UI.toast.warning('Bitte Kategorie wählen.'); return; } - await UI.asyncButton(document.getElementById('beh-save-btn'), async () => { - try { - await API.health.behaviorCreate(dog.id, data); - UI.modal.close(); - UI.toast.success('Eintrag gespeichert.'); - onSave(); - } catch (err) { UI.toast.error(err.message || 'Fehler.'); } - }); - }); - }, 50); - } - return { init, refresh, openNew, onDogChange }; })(); diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index dda7473..de58da5 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -75,7 +75,7 @@ window.Page_map = (() => { // z: zIndexOffset — höher = weiter oben bei Überlappung const TYPEN = { - restaurant: { icon: '', label: 'Hundefreundl. Café/Restaurant', color: '#F97316', z: 10 }, + restaurant: { icon: '', label: 'Restaurant', color: '#F97316', z: 10 }, freilauf: { icon: '', label: 'Freilauf', color: '#22C55E', z: 20 }, shop: { icon: '', label: 'Shop', color: '#3B82F6', z: 15 }, kotbeutel: { icon: '', label: 'Kotbeutel', color: '#84A98C', z: 5 }, @@ -92,7 +92,6 @@ window.Page_map = (() => { treffpunkt: { icon: '', label: 'Treffpunkt', color: '#7C3AED', z: 25 }, community: { icon: '', label: 'Sonstiges', color: '#F59E0B', z: 30 }, zuechter: { icon: '', label: 'Züchter', color: '#7C3AED', z: 50 }, - hotel: { icon: '', label: 'Hundefreundl. Hotel', color: '#0369a1', z: 20 }, }; // Frontend-Layer → Backend-Typ Mapping @@ -110,7 +109,6 @@ window.Page_map = (() => { parkplatz: 'parkplatz', treffpunkt: 'treffpunkt', community: 'sonstiges', - hotel: 'hotel', }; // Gefahren-Radius-Kreis: prominente rote Fläche diff --git a/backend/static/sw.js b/backend/static/sw.js index 797d5b7..e9b1388 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-v875'; +const CACHE_VERSION = 'by-v872'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache