diff --git a/backend/database.py b/backend/database.py index efb9266..5e38a96 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2058,214 +2058,3 @@ def _migrate(conn_factory): ON gassi_zeiten(user_id, aktiv); """) logger.info("Migration: Gassi-Zeiten-Tabelle bereit.") - - # ---- Feature: Hilfe/FAQ ---- - conn.executescript(""" - CREATE TABLE IF NOT EXISTS help_articles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - kategorie TEXT NOT NULL, - frage TEXT NOT NULL, - antwort TEXT NOT NULL, - sort_order INTEGER DEFAULT 0, - aktiv INTEGER DEFAULT 1, - created_at TEXT DEFAULT (datetime('now')) - ); - CREATE INDEX IF NOT EXISTS idx_help_kat ON help_articles(kategorie, sort_order); - """) - _seed_help_articles(conn) - logger.info("Migration: Hilfe/FAQ-Tabelle bereit.") - - -def _seed_help_articles(conn): - """Befüllt help_articles mit Starter-FAQs — nur wenn die Tabelle noch leer ist.""" - count = conn.execute("SELECT COUNT(*) FROM help_articles").fetchone()[0] - if count > 0: - return - - _SEED = [ - # ── installation ────────────────────────────────────────────── - ("installation", "Was ist eine PWA?", - "Eine Progressive Web App (PWA) ist eine Website, die sich wie eine richtige App verhält. " - "Du kannst Ban Yaro direkt im Browser nutzen oder als App auf deinem Startbildschirm speichern — " - "ohne den App Store.\n\n" - "Vorteile: immer aktuell, kein Download, funktioniert auch offline.", - 1), - ("installation", "Warum ist Ban Yaro nicht im App Store?", - "Als PWA können wir Ban Yaro viel schneller mit neuen Funktionen ausstatten und Fehler sofort " - "beheben. App-Store-Apps müssen oft tagelang auf Freigabe warten.\n\n" - "Außerdem sparst du Speicherplatz auf deinem Handy — Ban Yaro braucht nur wenige Megabyte.", - 2), - ("installation", "iPhone: Wie füge ich Ban Yaro zum Startbildschirm hinzu?", - "1. Öffne banyaro.app in Safari (nicht Chrome oder Firefox).\n" - "2. Tippe auf das Teilen-Symbol (Quadrat mit Pfeil nach oben) unten in der Mitte.\n" - "3. Scrolle im Menü nach unten und wähle 'Zum Home-Bildschirm'.\n" - "4. Bestätige mit 'Hinzufügen'.\n\n" - "Ban Yaro erscheint jetzt als App-Icon auf deinem Startbildschirm.", - 3), - ("installation", "Android: Wie füge ich Ban Yaro zum Startbildschirm hinzu?", - "1. Öffne banyaro.app in Chrome.\n" - "2. Tippe oben rechts auf die drei Punkte (Menü).\n" - "3. Wähle 'App installieren' oder 'Zum Startbildschirm hinzufügen'.\n" - "4. Bestätige die Installation.\n\n" - "Bei manchen Android-Geräten erscheint auch automatisch ein Banner am unteren Bildschirmrand.", - 4), - ("installation", "Wie aktualisiere ich die App?", - "Ban Yaro aktualisiert sich automatisch im Hintergrund. Wenn ein Update bereit ist, " - "siehst du einen Hinweis in der App.\n\n" - "Falls du Probleme hast und die App sich komisch verhält: Einstellungen öffnen, " - "ganz unten 'Auf Update prüfen' tippen. Danach die App schließen und neu öffnen.", - 5), - - # ── erste_schritte ──────────────────────────────────────────── - ("erste_schritte", "Wie lege ich meinen Hund an?", - "Tippe in der Navigation unten auf den Hund-Tab (HUND) und dann auf 'Hund hinzufügen'. " - "Du gibst Name, Rasse und Geburtsdatum ein — ein Foto ist optional, macht aber gleich mehr Spaß.\n\n" - "Du kannst mehrere Hunde anlegen und zwischen ihnen wechseln.", - 1), - ("erste_schritte", "Wie navigiere ich zwischen den Welten?", - "Ban Yaro ist in drei Welten aufgeteilt: JETZT, HUND und WELT. " - "Du kannst links-rechts wischen oder die drei Buttons unten in der Navigation tippen.\n\n" - "JETZT zeigt dir alles, was gerade relevant ist. HUND ist alles über deinen Vierbeiner. " - "WELT verbindet dich mit der Hunde-Community.", - 2), - ("erste_schritte", "Was bedeuten JETZT, HUND und WELT?", - "JETZT: Dein persönliches Dashboard — Wetter, Gassi-Planer, aktuelle Hinweise in deiner Nähe.\n\n" - "HUND: Alles rund um deinen Hund — Tagebuch, Gesundheit, Training, Ernährung, Tierarzt.\n\n" - "WELT: Die Community — Forum, Wiki, Events, andere Hundebesitzer, Gassi-Treffen, Karte.", - 3), - ("erste_schritte", "Wie passe ich die Chips auf der JETZT-Seite an?", - "Tippe oben rechts auf der JETZT-Seite auf das Zahnrad-Symbol. " - "Dort kannst du auswählen, welche Funktions-Chips du sehen möchtest " - "und in welcher Reihenfolge sie angezeigt werden.", - 4), - ("erste_schritte", "Wie schreibe ich mein erstes Tagebuch?", - "Gehe in die HUND-Welt und tippe auf 'Tagebuch'. " - "Mit dem Plus-Button kannst du einen neuen Eintrag anlegen.\n\n" - "Du kannst Text schreiben, Fotos hinzufügen, deine Stimmung eintragen und den Ort markieren. " - "Alle Einträge sind nur für dich sichtbar.", - 5), - - # ── standort ────────────────────────────────────────────────── - ("standort", "Wie gebe ich den Standort auf dem iPhone frei?", - "1. Öffne die Einstellungen deines iPhones.\n" - "2. Scrolle zu Safari.\n" - "3. Tippe auf 'Standort'.\n" - "4. Wähle 'Beim Benutzen der App erlauben'.\n\n" - "Alternativ: Wenn Ban Yaro fragt, ob es deinen Standort nutzen darf, " - "tippe auf 'Erlauben'. Diese Abfrage erscheint beim ersten Öffnen der Karte oder Wetter-Funktion.", - 1), - ("standort", "Wie gebe ich den Standort auf Android frei?", - "1. Öffne die Einstellungen deines Handys.\n" - "2. Gehe zu Apps → Chrome → Berechtigungen → Standort.\n" - "3. Wähle 'Beim Benutzen der App erlauben'.\n\n" - "Beim ersten Öffnen der Karte oder Wetter-Funktion fragt Ban Yaro automatisch nach der Berechtigung.", - 2), - ("standort", "Der Standort ist blockiert — wie setze ich das zurück?", - "Wenn du versehentlich 'Ablehnen' getippt hast, kannst du das zurücksetzen:\n\n" - "iPhone: Einstellungen → Safari → Standort → 'Beim Benutzen erlauben'\n\n" - "Android: In Chrome tippe auf das Schloss-Symbol links in der Adresszeile → " - "Website-Einstellungen → Standort → Erlauben.\n\n" - "Tipp: Manchmal hilft es, die Website-Daten zu löschen und Ban Yaro neu zu öffnen.", - 3), - ("standort", "Das Wetter lädt nicht — was kann ich tun?", - "Das Wetter benötigt deinen aktuellen Standort. Prüfe zuerst, ob die Standort-Berechtigung " - "erteilt ist (siehe 'Standort freigeben').\n\n" - "Falls es trotzdem nicht klappt: Schließe Ban Yaro vollständig und öffne es erneut. " - "Bei anhaltenden Problemen tippe in den Einstellungen auf 'Auf Update prüfen'.", - 4), - - # ── account ─────────────────────────────────────────────────── - ("account", "Ich habe mein Passwort vergessen — was nun?", - "Auf der Anmeldeseite findest du den Link 'Passwort vergessen'. " - "Gib dort deine E-Mail-Adresse ein — du erhältst innerhalb weniger Minuten " - "einen Link zum Zurücksetzen.\n\n" - "Schau auch im Spam-Ordner, falls die E-Mail nicht ankommt.", - 1), - ("account", "Die E-Mail-Bestätigung ist nicht angekommen.", - "Bitte prüfe deinen Spam- oder Junk-Ordner. E-Mails von Ban Yaro kommen von noreply@banyaro.app.\n\n" - "Falls du die E-Mail dort nicht findest, kannst du die Bestätigung in den Einstellungen " - "unter 'Konto' erneut anfordern.\n\n" - "Stelle sicher, dass deine E-Mail-Adresse korrekt geschrieben ist.", - 2), - ("account", "Wie kann ich mein Konto löschen?", - "Gehe in die Einstellungen (Zahnrad-Symbol) → Konto → 'Konto löschen'.\n\n" - "Achtung: Die Löschung ist endgültig. Alle deine Daten, Hunde-Profile und " - "Tagebuch-Einträge werden dauerhaft entfernt. Diese Aktion kann nicht rückgängig gemacht werden.", - 3), - ("account", "Wie ändere ich meine E-Mail-Adresse?", - "Das Ändern der E-Mail-Adresse ist aus Sicherheitsgründen aktuell nur über den " - "Support möglich. Schreibe uns an support@banyaro.app mit deiner aktuellen und " - "gewünschten neuen E-Mail-Adresse.", - 4), - - # ── features ────────────────────────────────────────────────── - ("features", "Was ist der Gassi-Score?", - "Der Gassi-Score zeigt dir auf einen Blick, wie gut das Wetter gerade für einen Spaziergang ist. " - "Er berücksichtigt Temperatur, Regen, Wind, UV-Index und — bei heißem Wetter — " - "die Asphalt-Temperatur.\n\n" - "Grün = super. Gelb = geht noch. Rot = lieber warten oder kürzer machen.", - 1), - ("features", "Was kann der KI-Tierarzt?", - "Der KI-Tierarzt beantwortet allgemeine Fragen rund um die Gesundheit deines Hundes — " - "zum Beispiel zu Symptomen, Ernährung oder Verhalten.\n\n" - "Wichtig: Er ersetzt keinen echten Tierarzt. Bei ernsten Symptomen oder Notfällen " - "wende dich bitte sofort an einen Tierarzt in deiner Nähe.", - 2), - ("features", "Wie funktioniert der Offline-Modus?", - "Ban Yaro speichert die wichtigsten Funktionen lokal auf deinem Gerät. " - "Auch ohne Internet kannst du dein Tagebuch lesen, Einträge anlegen und " - "gespeicherte Karten nutzen.\n\n" - "Neue Daten werden automatisch synchronisiert, sobald du wieder online bist.", - 3), - ("features", "Wie richte ich Push-Benachrichtigungen ein?", - "Gehe in die Einstellungen (Zahnrad-Symbol) und tippe auf 'Push-Benachrichtigungen'. " - "Dort kannst du auswählen, für welche Ereignisse du Benachrichtigungen erhalten möchtest — " - "z.B. Giftköder-Warnungen, neue Nachrichten oder Gassi-Erinnerungen.\n\n" - "Auf dem iPhone muss Ban Yaro als App auf dem Startbildschirm installiert sein, " - "damit Push-Nachrichten funktionieren.", - 4), - ("features", "Kann ich mein Tagebuch-Hintergrundbild ändern?", - "Ja! Im Tagebuch tippe oben auf das Bild-Symbol oder auf 'Hintergrund anpassen'. " - "Du kannst ein eigenes Foto wählen oder eines der vorhandenen Motive nutzen.\n\n" - "Das Hintergrundbild gilt für alle Einträge und macht dein Tagebuch ganz persönlich.", - 5), - ("features", "Was ist der Giftköder-Alarm?", - "Der Giftköder-Alarm zeigt dir gemeldete Giftköder in deiner Nähe auf einer Karte. " - "Du kannst selbst Funde melden und andere Hundebesitzer warnen.\n\n" - "In den Einstellungen kannst du Push-Benachrichtigungen aktivieren, " - "damit du sofort gewarnt wirst, wenn in deiner Nähe ein Giftköder gemeldet wird.", - 6), - - # ── probleme ────────────────────────────────────────────────── - ("probleme", "Die App zeigt alte Daten — was tun?", - "Tippe in den Einstellungen ganz unten auf 'Auf Update prüfen'. " - "Danach schließe Ban Yaro vollständig (App aus dem Multitasking entfernen) " - "und öffne sie erneut.\n\n" - "Falls das nicht hilft: Lösche die Website-Daten in deinem Browser und öffne " - "Ban Yaro erneut. Du bleibst dabei angemeldet.", - 1), - ("probleme", "Das Wetter lädt nicht.", - "Stelle sicher, dass die Standort-Berechtigung erteilt ist. " - "Ohne Standort kann Ban Yaro kein lokales Wetter laden.\n\n" - "Prüfe außerdem deine Internetverbindung. Bei schlechtem WLAN oder Mobilfunk " - "kann es zu Verzögerungen kommen. Eine kurze Wartezeit und erneutes Tippen hilft meist.", - 2), - ("probleme", "Die App reagiert nicht oder friert ein.", - "Schließe Ban Yaro vollständig (aus dem Multitasking entfernen) und öffne sie erneut.\n\n" - "Falls das Problem anhält, prüfe ob dein Gerät ausreichend Speicher hat. " - "Starte dein Handy neu — das löst in den meisten Fällen temporäre Hänger.", - 3), - ("probleme", "Wie melde ich einen Fehler?", - "Wir freuen uns über Feedback! Schreibe uns an support@banyaro.app mit einer kurzen " - "Beschreibung des Problems.\n\n" - "Hilfreich sind: Was hast du getan? Was hast du erwartet? Was ist stattdessen passiert? " - "Welches Gerät und Browser nutzt du?\n\n" - "Wir melden uns so schnell wie möglich.", - 4), - ] - - for kat, frage, antwort, sort in _SEED: - conn.execute( - "INSERT INTO help_articles (kategorie, frage, antwort, sort_order) VALUES (?, ?, ?, ?)", - (kat, frage, antwort, sort), - ) diff --git a/backend/main.py b/backend/main.py index dff2344..1d23aef 100644 --- a/backend/main.py +++ b/backend/main.py @@ -233,9 +233,6 @@ from routes.health_docs import router as health_docs_router from routes.passport import router as passport_router from routes.playdate import router as playdate_router from routes.ernaehrung import router as ernaehrung_router -from routes.challenges import router as challenges_router -from routes.gassi_zeiten import router as gassi_zeiten_router -from routes.help import router as help_router app.include_router(auth_router, prefix="/api/auth", tags=["Auth"]) app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"]) @@ -295,9 +292,6 @@ app.include_router(health_docs_router, prefix="/api/health-docs", t app.include_router(passport_router, prefix="/api/passport", tags=["Hundepass"]) app.include_router(playdate_router, prefix="/api/playdate", tags=["Playdate"]) app.include_router(ernaehrung_router, prefix="/api/dogs", tags=["Ernährung"]) -app.include_router(challenges_router, prefix="/api/challenges", tags=["Foto-Challenge"]) -app.include_router(gassi_zeiten_router, prefix="/api/gassi-zeiten", tags=["Gassi-Zeiten"]) -app.include_router(help_router, prefix="/api/help", tags=["Hilfe/FAQ"]) # ------------------------------------------------------------------ @@ -327,18 +321,6 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") os.makedirs(MEDIA_DIR, exist_ok=True) app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media") -APP_VER = "727" # muss mit APP_VER in app.js übereinstimmen - -@app.get("/api/version") -async def app_version(): - """Aktuelle Frontend-Version — wird beim App-Start gecheckt.""" - return Response( - content=f'{{"version":"{APP_VER}"}}', - media_type="application/json", - headers={"Cache-Control": "no-store"}, - ) - - @app.get("/stats/script.js") async def umami_script_proxy(): async with httpx.AsyncClient(timeout=10) as client: diff --git a/backend/media_utils.py b/backend/media_utils.py index 4cb2e28..8a8698f 100644 --- a/backend/media_utils.py +++ b/backend/media_utils.py @@ -178,17 +178,6 @@ def generate_preview(data: bytes, ext: str) -> bytes | None: return None -def get_image_size(data: bytes) -> tuple[int, int] | None: - """Gibt (width, height) eines Bildes zurück, oder None bei Fehler.""" - try: - from PIL import Image, ImageOps - img = Image.open(io.BytesIO(data)) - img = ImageOps.exif_transpose(img) - return img.size # (width, height) - except Exception: - return None - - def preview_url_from(url: str | None) -> str | None: """Leitet die Preview-URL aus der Original-URL ab (fügt _preview vor Extension ein). Gibt None für Videos, PDFs, bereits generierte Previews und leere URLs zurück.""" diff --git a/backend/routes/achievements.py b/backend/routes/achievements.py index bff2421..0a2988e 100644 --- a/backend/routes/achievements.py +++ b/backend/routes/achievements.py @@ -203,11 +203,11 @@ def check_and_award(user_id: int, conn): "SELECT current_streak FROM users WHERE id=?", (user_id,) ).fetchone() - # Wetter-Tapferkeit: Diary-Einträge bei schlechtem Wetter (über Dog-Join) + # Wetter-Tapferkeit: Diary-Einträge bei schlechtem Wetter wetter_row = conn.execute(""" SELECT COUNT(*) AS cnt FROM diary d - JOIN dogs dog ON dog.id = d.dog_id - WHERE dog.user_id = ? + LEFT JOIN diary_dogs dd ON dd.diary_id = d.id + WHERE d.user_id = ? AND d.weather_json IS NOT NULL AND ( CAST(json_extract(d.weather_json, '$.precip_prob') AS INTEGER) > 60 @@ -216,28 +216,23 @@ def check_and_award(user_id: int, conn): ) """, (user_id,)).fetchone() - # Jahreszeiten: Anzahl Jahreszeiten mit mind. 5 Diary-Einträgen (über Dog-Join) + # Jahreszeiten: Anzahl Jahreszeiten mit mind. 5 Diary-Einträgen jahreszeiten_row = conn.execute(""" SELECT - (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id - WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (3,4,5)) >= 5 THEN 1 ELSE 0 END) + - (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id - WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (6,7,8)) >= 5 THEN 1 ELSE 0 END) + - (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id - WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (9,10,11)) >= 5 THEN 1 ELSE 0 END) + - (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id - WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (12,1,2)) >= 5 THEN 1 ELSE 0 END) + (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (3,4,5)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (6,7,8)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (9,10,11)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (12,1,2)) >= 5 THEN 1 ELSE 0 END) AS jahreszeiten_score FROM (SELECT 1) """, (user_id, user_id, user_id, user_id)).fetchone() - # Schnee: Diary-Einträge bei Schnee (weathercode 71-77, über Dog-Join) + # Schnee: Diary-Einträge bei Schnee (weathercode 71-77) schnee_row = conn.execute(""" - SELECT COUNT(*) AS cnt FROM diary d - JOIN dogs dog ON dog.id = d.dog_id - WHERE dog.user_id = ? - AND d.weather_json IS NOT NULL - AND CAST(json_extract(d.weather_json, '$.weathercode') AS INTEGER) BETWEEN 71 AND 77 + SELECT COUNT(*) AS cnt FROM diary + WHERE user_id = ? + AND weather_json IS NOT NULL + AND CAST(json_extract(weather_json, '$.weathercode') AS INTEGER) BETWEEN 71 AND 77 """, (user_id,)).fetchone() metrics = { @@ -308,8 +303,8 @@ async def my_achievements(user=Depends(get_current_user)): # Wetter-Tapferkeit wetter_row = conn.execute(""" SELECT COUNT(*) AS cnt FROM diary d - JOIN dogs dog ON dog.id = d.dog_id - WHERE dog.user_id = ? + LEFT JOIN diary_dogs dd ON dd.diary_id = d.id + WHERE d.user_id = ? AND d.weather_json IS NOT NULL AND ( CAST(json_extract(d.weather_json, '$.precip_prob') AS INTEGER) > 60 @@ -321,25 +316,20 @@ async def my_achievements(user=Depends(get_current_user)): # Jahreszeiten jahreszeiten_row = conn.execute(""" SELECT - (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id - WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (3,4,5)) >= 5 THEN 1 ELSE 0 END) + - (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id - WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (6,7,8)) >= 5 THEN 1 ELSE 0 END) + - (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id - WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (9,10,11)) >= 5 THEN 1 ELSE 0 END) + - (CASE WHEN (SELECT COUNT(*) FROM diary d2 JOIN dogs g2 ON g2.id=d2.dog_id - WHERE g2.user_id=? AND CAST(strftime('%m', d2.datum) AS INTEGER) IN (12,1,2)) >= 5 THEN 1 ELSE 0 END) + (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (3,4,5)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (6,7,8)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (9,10,11)) >= 5 THEN 1 ELSE 0 END) + + (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (12,1,2)) >= 5 THEN 1 ELSE 0 END) AS jahreszeiten_score FROM (SELECT 1) """, (uid, uid, uid, uid)).fetchone() # Schnee-Einträge schnee_row = conn.execute(""" - SELECT COUNT(*) AS cnt FROM diary d - JOIN dogs dog ON dog.id = d.dog_id - WHERE dog.user_id = ? - AND d.weather_json IS NOT NULL - AND CAST(json_extract(d.weather_json, '$.weathercode') AS INTEGER) BETWEEN 71 AND 77 + SELECT COUNT(*) AS cnt FROM diary + WHERE user_id = ? + AND weather_json IS NOT NULL + AND CAST(json_extract(weather_json, '$.weathercode') AS INTEGER) BETWEEN 71 AND 77 """, (uid,)).fetchone() earned_rows = conn.execute( diff --git a/backend/routes/challenges.py b/backend/routes/challenges.py deleted file mode 100644 index f3e3f0d..0000000 --- a/backend/routes/challenges.py +++ /dev/null @@ -1,304 +0,0 @@ -"""BAN YARO — Foto-Challenge der Woche""" - -import os -import uuid -import logging -from datetime import date, timedelta -from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form -from pydantic import BaseModel -from typing import Optional -from database import db -from auth import get_current_user, get_current_user_optional -from media_utils import convert_media, generate_preview - -logger = logging.getLogger(__name__) - -router = APIRouter() - -MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") -CHALLENGE_DIR = os.path.join(MEDIA_DIR, "challenges") - -_CHALLENGE_THEMEN = [ - "Bestes Schnüffel-Foto 👃", - "Action-Aufnahme 🏃", - "Schlafendes Tier 😴", - "Gassi im Regen 🌧️", - "Hundeblick in die Kamera 👀", - "Spielzeit mit Freunden 🐕", - "Herbstspaziergang 🍂", - "Beste Sprung-Aufnahme 🦘", - "Hund am Wasser 🌊", - "Erstes Mal im Schnee ❄️", - "Genuss-Moment 🦴", - "Versteckt im Gebüsch 🌿", - "Tierisches Selfie 🤳", - "Hund & Kind 👶", - "Hund & Katze zusammen 🐱", - "Der beste Buddel-Moment 🐾", - "Freude beim Apportieren 🎾", - "Hund in seiner Lieblingshöhle 🛋️", - "Sonnenuntergangs-Gassi 🌅", - "Hundebegegnung auf dem Spaziergang 🐕🐕", - "Ausdrucksstarker Hundeblick 😍", - "Hund im Herbstlaub 🍁", - "Welpenfoto 🍼", - "Seniorenhund im Porträt 👴", - "Lustigste Schlafposition 💤", - "Hund trägt etwas 🎀", - "Hund + Besitzer Spiegelfoto 🪞", - "Hund auf Abenteuer 🏕️", - "Beste Lauf-Action 💨", - "Hund im Café ☕", -] - - -def _current_week_monday() -> str: - today = date.today() - monday = today - timedelta(days=today.weekday()) - return monday.isoformat() - - -def _current_week_sunday() -> str: - monday = date.fromisoformat(_current_week_monday()) - return (monday + timedelta(days=6)).isoformat() - - -def _ensure_current_challenge(conn) -> int: - """Stellt sicher dass eine Challenge für die aktuelle Woche existiert. Gibt die ID zurück.""" - monday = _current_week_monday() - sunday = _current_week_sunday() - - existing = conn.execute( - "SELECT id FROM foto_challenge WHERE start_date = ?", (monday,) - ).fetchone() - if existing: - return existing["id"] - - # Thema aus Rotation wählen (Wochennummer % Anzahl Themen) - week_num = date.today().isocalendar()[1] - thema = _CHALLENGE_THEMEN[week_num % len(_CHALLENGE_THEMEN)] - - cur = conn.execute( - "INSERT INTO foto_challenge (thema, beschreibung, start_date, end_date, created_by) " - "VALUES (?, ?, ?, ?, NULL)", - (thema, f"Diese Woche: {thema}", monday, sunday) - ) - return cur.lastrowid - - -# ------------------------------------------------------------------ -# GET /api/challenges/current -# ------------------------------------------------------------------ -@router.get("/current") -async def get_current_challenge(user=Depends(get_current_user_optional)): - with db() as conn: - challenge_id = _ensure_current_challenge(conn) - challenge = conn.execute( - "SELECT * FROM foto_challenge WHERE id = ?", (challenge_id,) - ).fetchone() - - submissions = conn.execute(""" - SELECT cs.id, cs.user_id, cs.dog_id, cs.foto_url, cs.caption, cs.votes, cs.created_at, - u.name AS user_name, u.avatar_url, - d.name AS dog_name, d.foto_url AS dog_foto_url - FROM challenge_submissions cs - LEFT JOIN users u ON u.id = cs.user_id - LEFT JOIN dogs d ON d.id = cs.dog_id - WHERE cs.challenge_id = ? - ORDER BY cs.votes DESC, cs.created_at ASC - """, (challenge_id,)).fetchall() - - my_submission = None - my_votes = set() - if user: - mine = conn.execute( - "SELECT id FROM challenge_submissions WHERE challenge_id=? AND user_id=?", - (challenge_id, user["id"]) - ).fetchone() - if mine: - my_submission = mine["id"] - voted_rows = conn.execute( - "SELECT cv.submission_id FROM challenge_votes cv " - "JOIN challenge_submissions cs ON cs.id = cv.submission_id " - "WHERE cv.user_id = ? AND cs.challenge_id = ?", - (user["id"], challenge_id) - ).fetchall() - my_votes = {r["submission_id"] for r in voted_rows} - - # Countdown bis Sonntag - end = date.fromisoformat(challenge["end_date"]) - days_left = (end - date.today()).days + 1 - - result_subs = [] - for s in submissions: - sd = dict(s) - sd["i_voted"] = (sd["id"] in my_votes) if user else False - result_subs.append(sd) - - return { - "challenge": dict(challenge), - "submissions": result_subs, - "my_submission_id": my_submission, - "days_left": max(0, days_left), - } - - -# ------------------------------------------------------------------ -# POST /api/challenges/{id}/submit -# ------------------------------------------------------------------ -@router.post("/{challenge_id}/submit", status_code=201) -async def submit_photo( - challenge_id: int, - caption: Optional[str] = Form(None), - dog_id: Optional[int] = Form(None), - foto: UploadFile = File(...), - user=Depends(get_current_user), -): - with db() as conn: - challenge = conn.execute( - "SELECT * FROM foto_challenge WHERE id = ?", (challenge_id,) - ).fetchone() - if not challenge: - raise HTTPException(404, "Challenge nicht gefunden.") - - today = date.today().isoformat() - if today > challenge["end_date"]: - raise HTTPException(400, "Die Challenge ist bereits beendet.") - - # Doppelt-Check - existing = conn.execute( - "SELECT id FROM challenge_submissions WHERE challenge_id=? AND user_id=?", - (challenge_id, user["id"]) - ).fetchone() - if existing: - raise HTTPException(409, "Du hast bereits ein Foto eingereicht.") - - # Foto speichern - os.makedirs(CHALLENGE_DIR, exist_ok=True) - orig_filename = foto.filename or "foto.jpg" - ext = os.path.splitext(orig_filename)[1] or ".jpg" - base = uuid.uuid4().hex - - raw = await foto.read() - - # HEIC→JPEG Konvertierung falls nötig - try: - converted, out_ext = convert_media(raw, orig_filename) - except Exception: - converted, out_ext = raw, ext - - save_filename = f"{base}{out_ext}" - save_path = os.path.join(CHALLENGE_DIR, save_filename) - with open(save_path, "wb") as f: - f.write(converted) - foto_url = f"/media/challenges/{save_filename}" - - # Preview - try: - preview = generate_preview(converted, out_ext) - if preview: - prev_path = os.path.join(CHALLENGE_DIR, f"{base}_preview.webp") - with open(prev_path, "wb") as f: - f.write(preview) - except Exception: - pass - - with db() as conn: - cur = conn.execute( - "INSERT INTO challenge_submissions (challenge_id, user_id, dog_id, foto_url, caption) " - "VALUES (?, ?, ?, ?, ?)", - (challenge_id, user["id"], dog_id, foto_url, caption) - ) - row = conn.execute(""" - SELECT cs.*, u.name AS user_name, d.name AS dog_name - FROM challenge_submissions cs - LEFT JOIN users u ON u.id = cs.user_id - LEFT JOIN dogs d ON d.id = cs.dog_id - WHERE cs.id = ? - """, (cur.lastrowid,)).fetchone() - - return dict(row) - - -# ------------------------------------------------------------------ -# POST /api/challenges/submissions/{id}/vote — Toggle-Vote -# ------------------------------------------------------------------ -@router.post("/submissions/{submission_id}/vote") -async def vote_submission(submission_id: int, user=Depends(get_current_user)): - with db() as conn: - sub = conn.execute( - "SELECT * FROM challenge_submissions WHERE id = ?", (submission_id,) - ).fetchone() - if not sub: - raise HTTPException(404, "Einreichung nicht gefunden.") - if sub["user_id"] == user["id"]: - raise HTTPException(400, "Du kannst nicht für dein eigenes Foto abstimmen.") - - existing = conn.execute( - "SELECT id FROM challenge_votes WHERE submission_id=? AND user_id=?", - (submission_id, user["id"]) - ).fetchone() - - if existing: - # Toggle: Vote entfernen - conn.execute( - "DELETE FROM challenge_votes WHERE submission_id=? AND user_id=?", - (submission_id, user["id"]) - ) - conn.execute( - "UPDATE challenge_submissions SET votes = MAX(0, votes - 1) WHERE id=?", - (submission_id,) - ) - voted = False - else: - conn.execute( - "INSERT INTO challenge_votes (submission_id, user_id) VALUES (?, ?)", - (submission_id, user["id"]) - ) - conn.execute( - "UPDATE challenge_submissions SET votes = votes + 1 WHERE id=?", - (submission_id,) - ) - voted = True - - votes = conn.execute( - "SELECT votes FROM challenge_submissions WHERE id=?", (submission_id,) - ).fetchone()["votes"] - - return {"voted": voted, "votes": votes} - - -# ------------------------------------------------------------------ -# GET /api/challenges/winners — letzte 4 Gewinner -# ------------------------------------------------------------------ -@router.get("/winners") -async def get_winners(): - with db() as conn: - # Vergangene Challenges (ohne aktuelle Woche) - monday = _current_week_monday() - challenges = conn.execute( - "SELECT id, thema, start_date, end_date FROM foto_challenge " - "WHERE end_date < ? ORDER BY end_date DESC LIMIT 4", - (monday,) - ).fetchall() - - winners = [] - for ch in challenges: - winner = conn.execute(""" - SELECT cs.id, cs.user_id, cs.foto_url, cs.caption, cs.votes, - u.name AS user_name, u.avatar_url, - d.name AS dog_name - FROM challenge_submissions cs - LEFT JOIN users u ON u.id = cs.user_id - LEFT JOIN dogs d ON d.id = cs.dog_id - WHERE cs.challenge_id = ? - ORDER BY cs.votes DESC, cs.created_at ASC - LIMIT 1 - """, (ch["id"],)).fetchone() - - winners.append({ - "challenge": dict(ch), - "winner": dict(winner) if winner else None, - }) - - return winners diff --git a/backend/routes/friends.py b/backend/routes/friends.py index 7df14e4..cac5f4b 100644 --- a/backend/routes/friends.py +++ b/backend/routes/friends.py @@ -342,56 +342,3 @@ async def remove_friend(friend_user_id: int, user=Depends(get_current_user)): AND ((requester_id=? AND addressee_id=?) OR (requester_id=? AND addressee_id=?)) """, (uid, friend_user_id, friend_user_id, uid)) return {"ok": True} - - -# ------------------------------------------------------------------ -# GET /api/friends/same-breed — andere User mit gleicher Rasse -# ------------------------------------------------------------------ -@router.get("/same-breed") -async def same_breed(user=Depends(get_current_user)): - """Findet andere User mit Hunden derselben Rasse. Gibt Anzahl + Forum-Suche zurück.""" - uid = user["id"] - with db() as conn: - # Rassen des eingeloggten Users - my_dogs = conn.execute( - "SELECT rasse FROM dogs WHERE user_id=? AND rasse IS NOT NULL AND rasse != ''", - (uid,) - ).fetchall() - - if not my_dogs: - return {"count": 0, "rassen": [], "forum_query": None} - - rassen = list({d["rasse"].strip() for d in my_dogs if d["rasse"]}) - - # Andere User (nicht ich) die eine dieser Rassen haben - ph = ",".join("?" * len(rassen)) - count_row = conn.execute(f""" - SELECT COUNT(DISTINCT d.user_id) AS cnt - FROM dogs d - WHERE d.user_id != ? - AND d.rasse IN ({ph}) - """, (uid, *rassen)).fetchone() - - count = count_row["cnt"] if count_row else 0 - - # Für jede Rasse: wie viele andere User - rassen_detail = [] - for rasse in rassen: - n = conn.execute( - "SELECT COUNT(DISTINCT user_id) AS cnt FROM dogs " - "WHERE user_id != ? AND rasse = ?", - (uid, rasse) - ).fetchone()["cnt"] - if n > 0: - rassen_detail.append({"rasse": rasse, "count": n}) - - rassen_detail.sort(key=lambda x: -x["count"]) - - # Forum-Suche-Link für die häufigste Rasse - forum_query = rassen_detail[0]["rasse"] if rassen_detail else None - - return { - "count": count, - "rassen": rassen_detail, - "forum_query": forum_query, - } diff --git a/backend/routes/gassi_zeiten.py b/backend/routes/gassi_zeiten.py deleted file mode 100644 index 77ff52f..0000000 --- a/backend/routes/gassi_zeiten.py +++ /dev/null @@ -1,190 +0,0 @@ -"""BAN YARO — Gassi-Zeiten-Pool (regelmäßige Gassi-Zeiten mit Gleichgesinnten)""" - -import json -import math -import logging -from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel -from typing import Optional, List -from database import db -from auth import get_current_user - -logger = logging.getLogger(__name__) - -router = APIRouter() - - -def _haversine(lat1, lon1, lat2, lon2): - R = 6_371_000 - p1, p2 = math.radians(lat1), math.radians(lat2) - dp = math.radians(lat2 - lat1) - dl = math.radians(lon2 - lon1) - a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2 - return 2 * R * math.asin(math.sqrt(a)) - - -class GassiZeitCreate(BaseModel): - dog_id: Optional[int] = None - wochentage: List[str] # ["mo", "mi", "fr"] - uhrzeit: str # "17:00" - ort_name: Optional[str] = None - lat: Optional[float] = None - lon: Optional[float] = None - radius_m: int = 500 - notiz: Optional[str] = None - - -class GassiZeitUpdate(BaseModel): - aktiv: Optional[int] = None - - -# ------------------------------------------------------------------ -# GET /api/gassi-zeiten — alle in der Nähe (oder eigene) -# ------------------------------------------------------------------ -@router.get("") -async def list_gassi_zeiten( - lat: Optional[float] = None, - lon: Optional[float] = None, - radius: int = 5000, # Meter - nur_eigene: bool = False, - user=Depends(get_current_user), -): - with db() as conn: - if nur_eigene: - rows = conn.execute(""" - SELECT gz.*, u.name AS user_name, u.avatar_url, - d.name AS dog_name, d.foto_url AS dog_foto_url, d.rasse AS dog_rasse - FROM gassi_zeiten gz - LEFT JOIN users u ON u.id = gz.user_id - LEFT JOIN dogs d ON d.id = gz.dog_id - WHERE gz.user_id = ? - ORDER BY gz.uhrzeit ASC - """, (user["id"],)).fetchall() - else: - rows = conn.execute(""" - SELECT gz.*, u.name AS user_name, u.avatar_url, - d.name AS dog_name, d.foto_url AS dog_foto_url, d.rasse AS dog_rasse - FROM gassi_zeiten gz - LEFT JOIN users u ON u.id = gz.user_id - LEFT JOIN dogs d ON d.id = gz.dog_id - WHERE gz.aktiv = 1 - ORDER BY gz.uhrzeit ASC - """).fetchall() - - result = [] - for r in rows: - d = dict(r) - # wochentage JSON parsen - try: - d["wochentage"] = json.loads(d["wochentage"]) if isinstance(d["wochentage"], str) else d["wochentage"] - except Exception: - d["wochentage"] = [] - d["is_mine"] = (d["user_id"] == user["id"]) - - # Distanz-Filter - if lat is not None and lon is not None and d.get("lat") and d.get("lon"): - dist = _haversine(lat, lon, d["lat"], d["lon"]) - if not nur_eigene and dist > radius: - continue - d["distance_m"] = int(dist) - else: - d["distance_m"] = None - - result.append(d) - - # Sortierung: eigene zuerst, dann nach Distanz - result.sort(key=lambda x: (0 if x["is_mine"] else 1, x.get("distance_m") or 99999)) - return result - - -# ------------------------------------------------------------------ -# POST /api/gassi-zeiten — eigene Zeit anlegen -# ------------------------------------------------------------------ -@router.post("", status_code=201) -async def create_gassi_zeit(data: GassiZeitCreate, user=Depends(get_current_user)): - if not data.wochentage: - raise HTTPException(400, "Mindestens ein Wochentag muss angegeben werden.") - if not data.uhrzeit: - raise HTTPException(400, "Uhrzeit muss angegeben werden.") - - wochentage_json = json.dumps(data.wochentage) - - with db() as conn: - # Hund-Prüfung - if data.dog_id: - dog = conn.execute( - "SELECT id FROM dogs WHERE id=? AND user_id=?", (data.dog_id, user["id"]) - ).fetchone() - if not dog: - raise HTTPException(403, "Hund nicht gefunden oder gehört nicht dir.") - - cur = conn.execute(""" - INSERT INTO gassi_zeiten (user_id, dog_id, wochentage, uhrzeit, - ort_name, lat, lon, radius_m, notiz) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - """, (user["id"], data.dog_id, wochentage_json, data.uhrzeit, - data.ort_name, data.lat, data.lon, data.radius_m, data.notiz)) - - row = conn.execute(""" - SELECT gz.*, u.name AS user_name, d.name AS dog_name, d.foto_url AS dog_foto_url - FROM gassi_zeiten gz - LEFT JOIN users u ON u.id = gz.user_id - LEFT JOIN dogs d ON d.id = gz.dog_id - WHERE gz.id = ? - """, (cur.lastrowid,)).fetchone() - - result = dict(row) - try: - result["wochentage"] = json.loads(result["wochentage"]) - except Exception: - pass - result["is_mine"] = True - return result - - -# ------------------------------------------------------------------ -# PATCH /api/gassi-zeiten/{id} — pausieren / aktivieren -# ------------------------------------------------------------------ -@router.patch("/{gz_id}") -async def update_gassi_zeit(gz_id: int, data: GassiZeitUpdate, user=Depends(get_current_user)): - with db() as conn: - gz = conn.execute( - "SELECT * FROM gassi_zeiten WHERE id=?", (gz_id,) - ).fetchone() - if not gz: - raise HTTPException(404, "Gassi-Zeit nicht gefunden.") - if gz["user_id"] != user["id"]: - raise HTTPException(403, "Nicht deine Gassi-Zeit.") - - updates = data.model_dump(exclude_none=True) - if updates: - cols = ", ".join(f"{k} = ?" for k in updates) - conn.execute(f"UPDATE gassi_zeiten SET {cols} WHERE id=?", [*updates.values(), gz_id]) - - row = conn.execute( - "SELECT * FROM gassi_zeiten WHERE id=?", (gz_id,) - ).fetchone() - - result = dict(row) - try: - result["wochentage"] = json.loads(result["wochentage"]) - except Exception: - pass - result["is_mine"] = True - return result - - -# ------------------------------------------------------------------ -# DELETE /api/gassi-zeiten/{id} -# ------------------------------------------------------------------ -@router.delete("/{gz_id}", status_code=204) -async def delete_gassi_zeit(gz_id: int, user=Depends(get_current_user)): - with db() as conn: - gz = conn.execute( - "SELECT * FROM gassi_zeiten WHERE id=?", (gz_id,) - ).fetchone() - if not gz: - raise HTTPException(404, "Gassi-Zeit nicht gefunden.") - if gz["user_id"] != user["id"]: - raise HTTPException(403, "Nicht deine Gassi-Zeit.") - conn.execute("DELETE FROM gassi_zeiten WHERE id=?", (gz_id,)) diff --git a/backend/routes/help.py b/backend/routes/help.py deleted file mode 100644 index 4c77018..0000000 --- a/backend/routes/help.py +++ /dev/null @@ -1,98 +0,0 @@ -"""BAN YARO — Hilfe / FAQ Routes""" - -from fastapi import APIRouter, Depends, Query -from pydantic import BaseModel -from typing import Optional -from database import db -from auth import get_current_user_optional, require_admin - -router = APIRouter() - - -# ------------------------------------------------------------------ -# Schemas -# ------------------------------------------------------------------ -class ArticleCreate(BaseModel): - kategorie: str - frage: str - antwort: str - sort_order: int = 0 - aktiv: int = 1 - - -class ArticleUpdate(BaseModel): - kategorie: Optional[str] = None - frage: Optional[str] = None - antwort: Optional[str] = None - sort_order: Optional[int] = None - aktiv: Optional[int] = None - - -# ------------------------------------------------------------------ -# GET /api/help — öffentlich (nur aktive); ?all=1 für Admins -# ------------------------------------------------------------------ -@router.get("") -def get_help( - all: int = Query(0), - user=Depends(get_current_user_optional), -): - is_admin = user and user.get("rolle") == "admin" - show_all = all == 1 and is_admin - - with db() as conn: - if show_all: - rows = conn.execute( - "SELECT id, kategorie, frage, antwort, sort_order, aktiv " - "FROM help_articles " - "ORDER BY kategorie, sort_order, id" - ).fetchall() - else: - rows = conn.execute( - "SELECT id, kategorie, frage, antwort, sort_order, aktiv " - "FROM help_articles " - "WHERE aktiv = 1 " - "ORDER BY kategorie, sort_order, id" - ).fetchall() - - return [dict(r) for r in rows] - - -# ------------------------------------------------------------------ -# POST /api/help — Admin: neuen Artikel anlegen -# ------------------------------------------------------------------ -@router.post("", status_code=201) -def create_article(body: ArticleCreate, admin=Depends(require_admin)): - with db() as conn: - cur = conn.execute( - "INSERT INTO help_articles (kategorie, frage, antwort, sort_order, aktiv) " - "VALUES (?, ?, ?, ?, ?)", - (body.kategorie, body.frage, body.antwort, body.sort_order, body.aktiv), - ) - return {"ok": True, "id": cur.lastrowid} - - -# ------------------------------------------------------------------ -# PATCH /api/help/{article_id} — Admin: Artikel bearbeiten -# ------------------------------------------------------------------ -@router.patch("/{article_id}") -def update_article(article_id: int, body: ArticleUpdate, admin=Depends(require_admin)): - updates = {k: v for k, v in body.model_dump(exclude_none=True).items()} - if not updates: - return {"ok": True} - set_clause = ", ".join(f"{k}=?" for k in updates) - with db() as conn: - conn.execute( - f"UPDATE help_articles SET {set_clause} WHERE id=?", - (*updates.values(), article_id), - ) - return {"ok": True} - - -# ------------------------------------------------------------------ -# DELETE /api/help/{article_id} — Admin: Artikel löschen -# ------------------------------------------------------------------ -@router.delete("/{article_id}") -def delete_article(article_id: int, admin=Depends(require_admin)): - with db() as conn: - conn.execute("DELETE FROM help_articles WHERE id=?", (article_id,)) - return {"ok": True} diff --git a/backend/routes/osm.py b/backend/routes/osm.py index 998cd4b..de4cb45 100644 --- a/backend/routes/osm.py +++ b/backend/routes/osm.py @@ -280,14 +280,9 @@ class UserPoiIn(BaseModel): ALLOWED_TYPES = { 'waste_basket', 'drinking_water', 'dog_park', 'giftkoeder', # Giftköder (exklusiv, kein Kombi) - 'gefahr', # Allgemeine Gefahr / Hinweis - 'freilauf', # Freilauffläche - 'restaurant', # Hundefreundliches Restaurant / Café - 'shop', # Hundefreundlicher Shop - 'tierarzt', # Tierarzt / Tierklinik - 'hundeschule', # Hundeschule / Trainer 'kotbeutel', # Kotbeutelspender 'bank', # Sitzbank + 'gefahr', # Allgemeine Gefahr / Hinweis 'parkplatz', # Hundefreundlicher Parkplatz 'treffpunkt', # Treffpunkt für Hundehalter 'sonstiges', diff --git a/backend/routes/training.py b/backend/routes/training.py index 05e8e94..48e37fe 100644 --- a/backend/routes/training.py +++ b/backend/routes/training.py @@ -6,7 +6,7 @@ from typing import Optional import datetime import ki from database import db -from auth import get_current_user, require_admin +from auth import get_current_user router = APIRouter() @@ -50,36 +50,6 @@ async def get_exercises(): }) return by_tab -# ------------------------------------------------------------------ -# Admin: Übung bearbeiten (beschreibung / schritte / tipp) -# ------------------------------------------------------------------ -class ExerciseUpdate(BaseModel): - beschreibung: Optional[str] = None - schritte: Optional[str] = None # JSON-String: '["Schritt 1", ...]' - tipp: Optional[str] = None - -@router.put("/exercises/{exercise_id}") -async def update_exercise(exercise_id: int, body: ExerciseUpdate, _=Depends(require_admin)): - """Partial update von beschreibung/schritte/tipp einer Übung (nur Admin).""" - with db() as conn: - row = conn.execute( - "SELECT id FROM training_exercises WHERE id=?", (exercise_id,) - ).fetchone() - if not row: - raise HTTPException(status_code=404, detail="Übung nicht gefunden") - fields, vals = [], [] - if body.beschreibung is not None: - fields.append("beschreibung=?"); vals.append(body.beschreibung) - if body.schritte is not None: - fields.append("schritte=?"); vals.append(body.schritte) - if body.tipp is not None: - fields.append("tipp=?"); vals.append(body.tipp) - if not fields: - return {"ok": True, "updated": 0} - vals.append(exercise_id) - conn.execute(f"UPDATE training_exercises SET {', '.join(fields)} WHERE id=?", vals) - return {"ok": True, "updated": len(fields)} - # ------------------------------------------------------------------ # Übungs-Status # ------------------------------------------------------------------ diff --git a/backend/routes/weather.py b/backend/routes/weather.py index 2167b19..ba45306 100644 --- a/backend/routes/weather.py +++ b/backend/routes/weather.py @@ -43,8 +43,7 @@ async def weather_records(user=Depends(get_current_user)): rows = conn.execute(""" SELECT d.datum, d.weather_json, d.titel FROM diary d - JOIN dogs dog ON dog.id = d.dog_id - WHERE dog.user_id = ? AND d.weather_json IS NOT NULL + WHERE d.user_id = ? AND d.weather_json IS NOT NULL ORDER BY d.datum ASC """, (uid,)).fetchall() diff --git a/backend/routes/widget.py b/backend/routes/widget.py index 2c04ae8..4af2473 100644 --- a/backend/routes/widget.py +++ b/backend/routes/widget.py @@ -1,6 +1,6 @@ """BAN YARO — Widget-Snapshot + Tagesspruch Endpoints""" -import json +import json, random from datetime import date from fastapi import APIRouter, Depends, Query from typing import Optional @@ -59,8 +59,7 @@ async def widget_snapshot(user=Depends(get_current_user)): (dog_id,) ).fetchall() - day_num = (date.today() - date(2024, 1, 1)).days - random_photo = dict(photos[day_num % len(photos)]) if photos else None + random_photo = dict(random.choice(photos)) if photos else None # Anzahl überfälliger Erinnerungen overdue = conn.execute( diff --git a/backend/scheduler.py b/backend/scheduler.py index 22d1533..4d1dbff 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -156,14 +156,6 @@ def start(): replace_existing=True, misfire_grace_time=3600, ) - # Jeden Montag 08:00 Uhr — Neue Foto-Challenge anlegen - _scheduler.add_job( - _job_new_foto_challenge, - CronTrigger(day_of_week='mon', hour=8, minute=0), - id="new_foto_challenge", - replace_existing=True, - misfire_grace_time=3600, - ) # Täglich 07:00 Uhr — Goldene Gassi-Stunde _scheduler.add_job( _job_golden_gassi_hour, @@ -189,7 +181,7 @@ def start(): misfire_grace_time=3600, ) _scheduler.start() - logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00, Streak-Reminder 19:00, Rückruf-Check 08:00, Goldene-Gassi-Stunde 07:00, Jahrestags-Erinnerungen 09:00, Monatlicher-Rückblick 1. des Monats 10:00, Foto-Challenge Mo 08:00. OSM-Cache: on-demand (kein Prewarm).") + logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00, Streak-Reminder 19:00, Rückruf-Check 08:00, Goldene-Gassi-Stunde 07:00, Jahrestags-Erinnerungen 09:00, Monatlicher-Rückblick 1. des Monats 10:00. OSM-Cache: on-demand (kein Prewarm).") def stop(): @@ -1552,46 +1544,6 @@ async def _job_monthly_recap(): _log_job("monthly_recap", "ok", f"{sent_total} Push für {month_label}") -async def _job_new_foto_challenge(): - """Jeden Montag 08:00 — neue Foto-Challenge für die aktuelle Woche anlegen.""" - from datetime import date, timedelta - from routes.challenges import _CHALLENGE_THEMEN, _current_week_monday, _current_week_sunday - - monday = _current_week_monday() - sunday = _current_week_sunday() - - week_num = date.today().isocalendar()[1] - thema = _CHALLENGE_THEMEN[week_num % len(_CHALLENGE_THEMEN)] - - with db() as conn: - existing = conn.execute( - "SELECT id FROM foto_challenge WHERE start_date = ?", (monday,) - ).fetchone() - if existing: - logger.info(f"Foto-Challenge: Woche {monday} bereits vorhanden (id={existing['id']}).") - _log_job("new_foto_challenge", "ok", f"Bereits vorhanden für {monday}") - return - - cur = conn.execute( - "INSERT INTO foto_challenge (thema, beschreibung, start_date, end_date, created_by) " - "VALUES (?, ?, ?, ?, NULL)", - (thema, f"Diese Woche: {thema}", monday, sunday) - ) - challenge_id = cur.lastrowid - - # Push an alle User - send_push_to_all({ - "type": "foto_challenge", - "title": "📸 Neue Foto-Challenge!", - "body": f"Diese Woche: {thema} — mach mit!", - "data": {"page": "walks", "tab": "challenge"}, - "tag": f"challenge-{monday}", - }) - - logger.info(f"Foto-Challenge angelegt: '{thema}' für {monday}–{sunday} (id={challenge_id}).") - _log_job("new_foto_challenge", "ok", f"'{thema}' für {monday}") - - async def _fetch_hourly_weather(lat: float, lon: float) -> list[dict]: """Holt stündliche Wetterdaten für heute von Open-Meteo.""" import httpx diff --git a/backend/static/css/components.css b/backend/static/css/components.css index d3dcc37..c8de6a6 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -7304,20 +7304,6 @@ svg.empty-state-icon { color: var(--c-text-secondary); line-height: 1.2; } -.exp-kachel-jahr { - font-size: 9px; - color: var(--c-text-muted); - margin-top: 2px; - line-height: 1.2; -} -.exp-kachel-add { - display: flex; - align-items: center; - gap: 2px; - font-size: 10px; - color: var(--c-text-muted); - margin-top: 3px; -} /* ---- Sektion-Block (Verlauf etc.) ---- */ .exp-section { @@ -7493,36 +7479,6 @@ svg.empty-state-icon { border-radius: 999px; padding: 1px 6px; } -.exp-dog-selector { - display: flex; - gap: 8px; - padding: 10px 16px 4px; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; -} -.exp-dog-selector::-webkit-scrollbar { display: none; } -.exp-dog-pill { - flex-shrink: 0; - display: inline-flex; - align-items: center; - gap: 4px; - padding: 5px 14px; - border-radius: 999px; - border: 1px solid var(--c-border); - background: var(--c-bg-card); - color: var(--c-text-secondary); - font-size: var(--text-sm); - font-weight: var(--weight-medium); - cursor: pointer; - white-space: nowrap; - transition: background .15s, color .15s, border-color .15s; -} -.exp-dog-pill.active { - background: var(--c-primary); - color: #fff; - border-color: var(--c-primary); -} /* Rechte Spalte: Betrag + Löschen-Icon */ .exp-entry-right { @@ -8061,35 +8017,28 @@ svg.empty-state-icon { backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-radius: 16px; - padding: 12px 6px; + padding: 14px 6px 11px; text-align: center; cursor: pointer; display: flex; flex-direction: column; align-items: center; - justify-content: center; - gap: 6px; + gap: 7px; color: white; transition: background 0.12s, transform 0.1s; -webkit-tap-highlight-color: transparent; user-select: none; - min-height: 80px; /* alle Chips gleich hoch */ } .world-chip:active { background: rgba(0, 0, 0, 0.6); transform: scale(0.93); } -.world-chip svg { color: white; flex-shrink: 0; } +.world-chip svg { color: white; } .world-chip-label { font-size: 10px; font-weight: 600; color: rgba(255, 255, 255, 0.9); line-height: 1.2; - max-height: 2.4em; /* max. 2 Zeilen */ - overflow: hidden; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; } /* Chip-Umrandung je Welt */ @@ -8184,189 +8133,3 @@ svg.empty-state-icon { 0%, 100% { opacity: 0.5; } 50% { opacity: 0.25; } } - -/* ── COMMUNITY-FEATURES ──────────────────────────────────── */ - -/* Walks-Tab-Bar */ -.walks-tab-panel { display: flex; flex-direction: column; min-height: 0; flex: 1; } - -/* Foto-Challenge */ -.challenge-banner { - background: linear-gradient(135deg, var(--c-amber, #f59e0b), var(--c-primary, #C4843A)); - border-radius: var(--radius-lg); - margin: var(--space-4); - overflow: hidden; -} -.challenge-banner-inner { - padding: var(--space-5) var(--space-4); - color: #fff; -} -.challenge-thema { - font-size: var(--text-xl); - font-weight: var(--weight-bold); - line-height: 1.2; - margin-bottom: var(--space-2); -} -.challenge-meta { - font-size: var(--text-sm); - opacity: 0.88; -} -.challenge-gallery { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); - gap: var(--space-3); - padding: 0 var(--space-4) var(--space-6); -} -.challenge-sub-card { - background: var(--c-surface); - border-radius: var(--radius-md); - overflow: hidden; - box-shadow: var(--shadow-sm); -} -.challenge-sub-card img { - width: 100%; - aspect-ratio: 1; - object-fit: cover; - display: block; - cursor: pointer; -} -.challenge-sub-info { - padding: var(--space-2); -} -.challenge-sub-user { - font-size: var(--text-xs); - color: var(--c-text-secondary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-bottom: 2px; -} -.challenge-sub-caption { - font-size: var(--text-xs); - color: var(--c-text); - margin-bottom: var(--space-1); - overflow: hidden; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} -.challenge-vote-btn { - border: none; - background: transparent; - color: var(--c-text-secondary); - font-size: var(--text-xs); - cursor: pointer; - padding: 2px 6px; - border-radius: var(--radius-sm); - display: flex; - align-items: center; - gap: 3px; -} -.challenge-vote-btn.voted { - color: var(--c-danger, #ef4444); -} -.challenge-winners { border-top: 1px solid var(--c-border); } -.challenge-winners-row { - display: flex; - gap: var(--space-3); - overflow-x: auto; - padding: var(--space-2) var(--space-4) var(--space-3); - scroll-snap-type: x mandatory; -} -.challenge-winner-chip { - display: flex; - align-items: center; - gap: var(--space-2); - background: var(--c-surface-alt, #fdf6ef); - border-radius: var(--radius-md); - padding: var(--space-2) var(--space-3); - min-width: 160px; - flex-shrink: 0; - scroll-snap-align: start; -} -.challenge-winner-chip img { - width: 40px; - height: 40px; - border-radius: var(--radius-full); - object-fit: cover; - flex-shrink: 0; -} - -/* Wochentag-Selector */ -.wd-selector { - display: flex; - gap: var(--space-2); - flex-wrap: wrap; -} -.wd-btn { - display: flex; - align-items: center; - gap: 4px; - cursor: pointer; - padding: 4px 10px; - border: 1.5px solid var(--c-border); - border-radius: var(--radius-full); - font-size: var(--text-sm); - user-select: none; - transition: background .15s, border-color .15s; -} -.wd-btn input { display: none; } -.wd-btn:has(input:checked) { - background: var(--c-primary, #C4843A); - border-color: var(--c-primary, #C4843A); - color: #fff; -} - -/* Gassi-Zeit-Karten */ -.gassi-zeit-card { - display: flex; - align-items: center; - gap: var(--space-3); - padding: var(--space-3) var(--space-4); - border-bottom: 1px solid var(--c-border); - background: var(--c-surface); -} -.gz-avatar { - width: 48px; - height: 48px; - border-radius: var(--radius-full); - overflow: hidden; - flex-shrink: 0; - background: var(--c-surface-alt); - display: flex; - align-items: center; - justify-content: center; -} -.gz-avatar img { width: 100%; height: 100%; object-fit: cover; } -.gz-avatar-placeholder { font-size: 1.5rem; color: var(--c-text-secondary); } -.gz-body { flex: 1; min-width: 0; } -.gz-name { - font-weight: var(--weight-semibold); - font-size: var(--text-sm); - display: flex; - align-items: center; - gap: var(--space-1); - flex-wrap: wrap; -} -.gz-meta { font-size: var(--text-xs); color: var(--c-text-secondary); margin-top: 2px; } -.gz-notiz { font-size: var(--text-xs); color: var(--c-text-secondary); margin-top: 2px; font-style: italic; } -.gz-actions { display: flex; gap: var(--space-1); flex-shrink: 0; } - -/* Rassen-Community-Chip */ -.breed-community-chip { - display: inline-flex; - align-items: center; - gap: var(--space-1); - background: var(--c-surface-alt, #fdf6ef); - border: 1.5px solid var(--c-amber, #f59e0b); - border-radius: var(--radius-full); - padding: 6px 16px; - font-size: var(--text-sm); - font-weight: var(--weight-medium); - color: var(--c-text); - cursor: pointer; - transition: background .15s; -} -.breed-community-chip:hover, .breed-community-chip:active { - background: #fff3e0; -} diff --git a/backend/static/img/banyaro/fruehling_playdate.webp b/backend/static/img/banyaro/fruehling_playdate.webp deleted file mode 100644 index 7defe9e..0000000 Binary files a/backend/static/img/banyaro/fruehling_playdate.webp and /dev/null differ diff --git a/backend/static/img/banyaro/herbst_bach.webp b/backend/static/img/banyaro/herbst_bach.webp deleted file mode 100644 index 5b7a594..0000000 Binary files a/backend/static/img/banyaro/herbst_bach.webp and /dev/null differ diff --git a/backend/static/img/banyaro/herbst_baum.webp b/backend/static/img/banyaro/herbst_baum.webp deleted file mode 100644 index 4ea4312..0000000 Binary files a/backend/static/img/banyaro/herbst_baum.webp and /dev/null differ diff --git a/backend/static/img/banyaro/hires/banyaro_fruehling_playdate_hires.jpg b/backend/static/img/banyaro/hires/banyaro_fruehling_playdate_hires.jpg deleted file mode 100644 index eb4e55a..0000000 Binary files a/backend/static/img/banyaro/hires/banyaro_fruehling_playdate_hires.jpg and /dev/null differ diff --git a/backend/static/img/banyaro/hires/banyaro_herbst_bach_hires.jpg b/backend/static/img/banyaro/hires/banyaro_herbst_bach_hires.jpg deleted file mode 100644 index aab4923..0000000 Binary files a/backend/static/img/banyaro/hires/banyaro_herbst_bach_hires.jpg and /dev/null differ diff --git a/backend/static/img/banyaro/hires/banyaro_herbst_baum_hires.jpg b/backend/static/img/banyaro/hires/banyaro_herbst_baum_hires.jpg deleted file mode 100644 index d14e80d..0000000 Binary files a/backend/static/img/banyaro/hires/banyaro_herbst_baum_hires.jpg and /dev/null differ diff --git a/backend/static/img/banyaro/hires/banyaro_winter_schnee_hires.jpg b/backend/static/img/banyaro/hires/banyaro_winter_schnee_hires.jpg deleted file mode 100644 index 155d65b..0000000 Binary files a/backend/static/img/banyaro/hires/banyaro_winter_schnee_hires.jpg and /dev/null differ diff --git a/backend/static/img/banyaro/winter_schnee.webp b/backend/static/img/banyaro/winter_schnee.webp deleted file mode 100644 index 87269d4..0000000 Binary files a/backend/static/img/banyaro/winter_schnee.webp and /dev/null differ diff --git a/backend/static/index.html b/backend/static/index.html index 9fb7cef..77f0433 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -93,9 +93,9 @@ - - - + + + @@ -409,10 +409,6 @@
-
-
-
-
@@ -578,7 +574,7 @@ - + @@ -680,6 +676,5 @@ } - diff --git a/backend/static/js/app.js b/backend/static/js/app.js index f2245de..deb05c5 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,8 +3,8 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '727'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen -const APP_VERSION = '1.4.0'; // ← semantische Version, wird bei make release gesetzt +const APP_VER = '700'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VERSION = '1.3.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; const App = (() => { @@ -79,7 +79,6 @@ const App = (() => { ernaehrung: { title: 'Ernährung', module: null, requiresAuth: true }, personality: { title: 'Persönlichkeitstest', module: null }, reise: { title: 'Reise mit Hund', module: null }, - hilfe: { title: 'Hilfe & FAQ', module: null }, }; // ---------------------------------------------------------- @@ -485,9 +484,6 @@ const App = (() => { navigate('onboarding'); } - // Drei Welten nach Login starten (falls noch nicht initialisiert) - if (window.Worlds) window.Worlds.init(state); - _showVerifyBanner(); _updateNotifBadge(); _updateChatBadge(); @@ -563,8 +559,7 @@ const App = (() => { _updateHeaderUserBtn(false); - window.Worlds?.hide(); - document.getElementById('worlds-back')?.classList.remove('worlds-back-visible'); + // Nicht eingeloggte User immer zur Welcome-Seite navigate('welcome', false); } @@ -860,8 +855,11 @@ const App = (() => { } const startPage = (hashPage && pages[hashPage]) ? hashPage : 'welcome'; + // Nicht eingeloggte User immer zur Welcome-Seite — auch bei direktem Link auf Forum, Map etc. navigate(state.user ? startPage : 'welcome', false, hashParams); - if (window.Worlds && state.user) window.Worlds.init(state); + + // Drei Welten nach initialer Navigation starten (damit hide() in navigate() sie nicht gleich killt) + if (window.Worlds) window.Worlds.init(state); } async function _handleInvite(token) { diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index 3e485d5..4e89f32 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -23,8 +23,6 @@ window.Page_admin = (() => { { id: 'partner', label: 'Partner', icon: 'handshake' }, { id: 'outreach', label: 'Outreach', icon: 'envelope-simple' }, { id: 'audit', label: 'Audit-Log', icon: 'clipboard-text' }, - { id: 'hilfe', label: 'Hilfe/FAQ', icon: 'question' }, - { id: 'uebungen_admin', label: 'Übungen', icon: 'barbell' }, ]; // ------------------------------------------------------------------ @@ -159,8 +157,6 @@ window.Page_admin = (() => { case 'outreach': await _renderOutreach(el); break; case 'audit': await _renderAudit(el); break; case 'bewerbungen': await _renderBewerbungen(el); break; - case 'hilfe': await _renderHilfe(el); break; - case 'uebungen_admin': await _renderUebungenAdmin(el); break; } } catch (e) { el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.'); @@ -2813,469 +2809,6 @@ window.Page_admin = (() => { await _load(); } - // ------------------------------------------------------------------ - // TAB: HILFE / FAQ - async function _renderHilfe(el) { - const KAT_LABEL = { - installation: 'Installation & PWA', - erste_schritte: 'Erste Schritte', - standort: 'Standort & Wetter', - account: 'Account & Passwort', - features: 'Features erklärt', - probleme: 'Technische Probleme', - }; - - el.innerHTML = ` -
-
-

Hilfe / FAQ

- -
- - - - - -
-
- Lade… -
-
-
- `; - - async function _load() { - const listEl = el.querySelector('#adm-hilfe-list'); - try { - const articles = await API.get('/help?all=1'); - if (!articles.length) { - listEl.innerHTML = _emptyState('question', 'Noch keine FAQ-Artikel', ''); - return; - } - - // Gruppieren nach Kategorie - const grouped = {}; - for (const a of articles) { - if (!grouped[a.kategorie]) grouped[a.kategorie] = []; - grouped[a.kategorie].push(a); - } - - let html = ''; - for (const [kat, items] of Object.entries(grouped)) { - const label = KAT_LABEL[kat] || kat; - html += ` -
-
- ${_esc(label)} -
- `; - for (const a of items) { - html += ` -
- -
- - ${_esc(a.frage)} - - - #${a.sort_order} - - - - -
- - - -
- `; - } - html += `
`; - } - listEl.innerHTML = html; - _bindListEvents(listEl); - } catch (e) { - listEl.innerHTML = _emptyState('warning', 'Fehler beim Laden', e.message || ''); - } - } - - function _bindListEvents(listEl) { - // Edit-Button: Inline-Formular auf/zu klappen - listEl.querySelectorAll('.adm-hilfe-edit-btn').forEach(btn => { - btn.addEventListener('click', () => { - const id = btn.dataset.id; - const form = listEl.querySelector(`.adm-hilfe-edit-form[data-id="${id}"]`); - if (form) form.style.display = form.style.display === 'none' ? '' : 'none'; - }); - }); - - // Edit-Cancel - listEl.querySelectorAll('.adm-hilfe-edit-cancel').forEach(btn => { - btn.addEventListener('click', () => { - const id = btn.dataset.id; - const form = listEl.querySelector(`.adm-hilfe-edit-form[data-id="${id}"]`); - if (form) form.style.display = 'none'; - }); - }); - - // Edit-Save - listEl.querySelectorAll('.adm-hilfe-edit-save').forEach(btn => { - btn.addEventListener('click', async () => { - const id = btn.dataset.id; - const row = listEl.querySelector(`.adm-hilfe-edit-form[data-id="${id}"]`); - const payload = { - kategorie: row.querySelector('.adm-hilfe-edit-kat').value, - frage: row.querySelector('.adm-hilfe-edit-frage').value.trim(), - antwort: row.querySelector('.adm-hilfe-edit-antwort').value.trim(), - sort_order: parseInt(row.querySelector('.adm-hilfe-edit-sort').value, 10) || 0, - }; - if (!payload.frage || !payload.antwort) { - UI.toast.error('Frage und Antwort sind Pflichtfelder.'); - return; - } - try { - await API.patch(`/help/${id}`, payload); - UI.toast.success('Artikel gespeichert.'); - _load(); - } catch (e) { UI.toast.error(e.message || 'Fehler beim Speichern.'); } - }); - }); - - // Toggle aktiv/inaktiv - listEl.querySelectorAll('.adm-hilfe-toggle-btn').forEach(btn => { - btn.addEventListener('click', async () => { - const id = btn.dataset.id; - const aktiv = parseInt(btn.dataset.aktiv, 10); - try { - await API.patch(`/help/${id}`, { aktiv: aktiv ? 0 : 1 }); - UI.toast.success(aktiv ? 'Artikel ausgeblendet.' : 'Artikel eingeblendet.'); - _load(); - } catch (e) { UI.toast.error(e.message || 'Fehler.'); } - }); - }); - - // Delete - listEl.querySelectorAll('.adm-hilfe-del-btn').forEach(btn => { - btn.addEventListener('click', async () => { - const id = btn.dataset.id; - const frage = btn.dataset.frage; - if (!window.confirm(`Artikel wirklich löschen?\n\n"${frage}"`)) return; - try { - await API.del(`/help/${id}`); - UI.toast.success('Artikel gelöscht.'); - _load(); - } catch (e) { UI.toast.error(e.message || 'Fehler beim Löschen.'); } - }); - }); - } - - // Neuer-Artikel-Button - el.querySelector('#adm-hilfe-neu').addEventListener('click', () => { - const form = el.querySelector('#adm-hilfe-form'); - form.style.display = form.style.display === 'none' ? '' : 'none'; - }); - - // Formular abbrechen - el.querySelector('#adm-hilfe-form-cancel').addEventListener('click', () => { - el.querySelector('#adm-hilfe-form').style.display = 'none'; - }); - - // Formular speichern - el.querySelector('#adm-hilfe-form-save').addEventListener('click', async () => { - const kat = el.querySelector('#adm-hilfe-kat').value; - const frage = el.querySelector('#adm-hilfe-frage').value.trim(); - const antwort= el.querySelector('#adm-hilfe-antwort').value.trim(); - const sort = parseInt(el.querySelector('#adm-hilfe-sort').value, 10) || 0; - if (!frage || !antwort) { - UI.toast.error('Frage und Antwort sind Pflichtfelder.'); - return; - } - try { - await API.post('/help', { kategorie: kat, frage, antwort, sort_order: sort }); - UI.toast.success('Artikel angelegt.'); - el.querySelector('#adm-hilfe-form').style.display = 'none'; - el.querySelector('#adm-hilfe-frage').value = ''; - el.querySelector('#adm-hilfe-antwort').value = ''; - el.querySelector('#adm-hilfe-sort').value = '0'; - _load(); - } catch (e) { UI.toast.error(e.message || 'Fehler beim Anlegen.'); } - }); - - await _load(); - } - - // ------------------------------------------------------------------ - async function _renderUebungenAdmin(el) { - el.innerHTML = `
Lade Übungen…
`; - - let byTab; - try { - byTab = await API.get('/training/exercises'); - } catch (e) { - el.innerHTML = `
Fehler: ${e.message}
`; - return; - } - - // Flatten to sorted list grouped by kategorie - const allExercises = []; - for (const [kat, list] of Object.entries(byTab)) { - for (const ex of list) allExercises.push({ ...ex, _kat: kat }); - } - allExercises.sort((a, b) => a._kat.localeCompare(b._kat) || a.name.localeCompare(b.name)); - - // Group by kategorie - const grouped = {}; - for (const ex of allExercises) { - grouped[ex._kat] = grouped[ex._kat] || []; - grouped[ex._kat].push(ex); - } - - const KAT_LABELS = { - 'grundkommandos': 'Grundkommandos', 'tricks': 'Tricks', - 'problemverhalten': 'Problemverhalten', 'mentale-auslastung': 'Mentale Auslastung', - 'koerperpflege': 'Körperpflege', 'hundesport': 'Hundesport', 'welpe-basics': 'Welpe Basics', - }; - - let html = `
-

- Trainingsübungen bearbeiten -

`; - - for (const [kat, list] of Object.entries(grouped)) { - html += `
-
- ${KAT_LABELS[kat] || kat} (${list.length}) -
`; - for (const ex of list) { - const schritte = Array.isArray(ex.schritte) ? ex.schritte.join('\n') : ''; - const exId = ex.exercise_id; - html += `
-
- ${ex.name} - -
- -
`; - } - html += `
`; - } - html += `
`; - el.innerHTML = html; - - // Edit toggle - el.querySelectorAll('.adm-ueb-edit-btn').forEach(btn => { - btn.addEventListener('click', () => { - const form = el.querySelector(`.adm-ueb-form[data-ex-id="${btn.dataset.exId}"]`); - form.style.display = form.style.display === 'none' ? '' : 'none'; - }); - }); - - // Cancel - el.querySelectorAll('.adm-ueb-cancel-btn').forEach(btn => { - btn.addEventListener('click', () => { - el.querySelector(`.adm-ueb-form[data-ex-id="${btn.dataset.exId}"]`).style.display = 'none'; - }); - }); - - // Save - el.querySelectorAll('.adm-ueb-save-btn').forEach(btn => { - btn.addEventListener('click', async () => { - const id = btn.dataset.exId; - const form = el.querySelector(`.adm-ueb-form[data-ex-id="${id}"]`); - const beschreibung = form.querySelector('.adm-ueb-beschreibung').value.trim(); - const schritte = form.querySelector('.adm-ueb-schritte').value - .split('\n').map(s => s.trim()).filter(Boolean); - const tipp = form.querySelector('.adm-ueb-tipp').value.trim(); - btn.disabled = true; - btn.textContent = 'Speichert…'; - try { - await API.put(`/training/exercises/${id}`, { - beschreibung, - schritte: JSON.stringify(schritte), - tipp, - }); - UI.toast.success('Übung gespeichert.'); - form.style.display = 'none'; - } catch (e) { - UI.toast.error(e.message || 'Fehler beim Speichern.'); - } finally { - btn.disabled = false; - btn.textContent = 'Speichern'; - } - }); - }); - } - // ------------------------------------------------------------------ return { init, refresh, onDogChange }; diff --git a/backend/static/js/pages/dog-profile.js b/backend/static/js/pages/dog-profile.js index 58d5aa7..09b729a 100644 --- a/backend/static/js/pages/dog-profile.js +++ b/backend/static/js/pages/dog-profile.js @@ -97,11 +97,8 @@ window.Page_dog_profile = (() => {

${_esc(dog.name)}

${dog.rasse - ? `

${_esc(dog.rasse)}

` - : `

`} - - -
+ ? `

${_esc(dog.rasse)}

` + : `

`}
- 🐕 ${label} — Forum ansehen - - `; - document.getElementById('dp-breed-chip-btn')?.addEventListener('click', () => { - App.navigate('forum', false, { search: hauptRasse }); - }); - } catch {} - } - - // ---------------------------------------------------------- // PUBLIC // ---------------------------------------------------------- diff --git a/backend/static/js/pages/ernaehrung.js b/backend/static/js/pages/ernaehrung.js index be58151..ec1951f 100644 --- a/backend/static/js/pages/ernaehrung.js +++ b/backend/static/js/pages/ernaehrung.js @@ -125,64 +125,47 @@ window.Page_ernaehrung = (() => { el.innerHTML = `
- +

+ Berechne den täglichen Kalorienbedarf deines Hundes. +

- -
-
- - -
-
- - +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
- -
- -
- - - - -
-
- - -
- -
- - -
-
- - @@ -225,28 +208,13 @@ window.Page_ernaehrung = (() => {
`; - // Aktivität Pills - el.querySelectorAll('[data-akt]').forEach(btn => { - btn.addEventListener('click', () => { - el.querySelectorAll('[data-akt]').forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - }); - }); - // Kastriert Pills - el.querySelectorAll('[data-kas]').forEach(btn => { - btn.addEventListener('click', () => { - el.querySelectorAll('[data-kas]').forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - }); - }); - el.querySelector('#ern-rechner-btn').addEventListener('click', () => _berechne(el)); } function _berechne(el) { - const gewicht = parseFloat(el.querySelector('#ern-gewicht').value); - const aktivitaet = el.querySelector('[data-akt].active')?.dataset.akt || 'normal'; - const kastriert = el.querySelector('[data-kas].active')?.dataset.kas === 'ja'; + const gewicht = parseFloat(el.querySelector('#ern-gewicht').value); + const aktivitaet = el.querySelector('#ern-aktivitaet').value; + const kastriert = el.querySelector('input[name="ern-kastriert"]:checked')?.value === 'ja'; if (!gewicht || gewicht < 0.5) { UI.toast.warning('Bitte ein gültiges Gewicht eingeben.'); diff --git a/backend/static/js/pages/expenses.js b/backend/static/js/pages/expenses.js index c516c40..3bed1dd 100644 --- a/backend/static/js/pages/expenses.js +++ b/backend/static/js/pages/expenses.js @@ -5,26 +5,15 @@ window.Page_expenses = (() => { - let _container = null; - let _appState = null; - let _tab = 'uebersicht'; - let _selectedDogId = null; + let _container = null; + let _appState = null; + let _tab = 'uebersicht'; // Cache let _summary = null; let _entries = []; let _statsData = null; - function _dogParam() { - return _selectedDogId ? `?dog_id=${_selectedDogId}` : ''; - } - function _dogParamAnd() { - return _selectedDogId ? `&dog_id=${_selectedDogId}` : ''; - } - function _clearCache() { - _summary = null; _entries = []; _statsData = null; - } - const TABS = [ { id: 'uebersicht', label: 'Übersicht', icon: 'house-line' }, { id: 'eintraege', label: 'Ausgaben', icon: 'currency-eur' }, @@ -49,10 +38,11 @@ window.Page_expenses = (() => { // LIFECYCLE // ---------------------------------------------------------- async function init(container, appState) { - _container = container; - _appState = appState; - _selectedDogId = null; - _clearCache(); + _container = container; + _appState = appState; + _summary = null; + _entries = []; + _statsData = null; _render(); } @@ -66,16 +56,6 @@ window.Page_expenses = (() => { // ---------------------------------------------------------- // SHELL // ---------------------------------------------------------- - function _dogSelectorHtml() { - const dogs = _appState?.dogs || []; - if (dogs.length < 2) return ''; - const pills = [{ id: null, name: 'Alle' }, ...dogs].map(d => ` - `).join(''); - return `
${pills}
`; - } - function _render() { _container.innerHTML = `
@@ -85,7 +65,6 @@ window.Page_expenses = (() => { `).join('')}
- ${_dogSelectorHtml()}
-
-
-
${antwortHtml}
-
-
-
- `; - } - - html += `
`; - } - - el.innerHTML = html; - - // Akkordeon-Interaktion - el.querySelectorAll('.hilfe-frage-btn').forEach(btn => { - btn.addEventListener('click', () => { - const targetId = btn.dataset.target; - const chevronId = btn.dataset.chevron; - const answer = document.getElementById(targetId); - const chevron = document.getElementById(chevronId); - if (!answer) return; - - const isOpen = answer.style.maxHeight !== '0px' && answer.style.maxHeight !== ''; - if (isOpen) { - answer.style.maxHeight = '0'; - if (chevron) chevron.style.transform = 'rotate(0deg)'; - btn.setAttribute('aria-expanded', 'false'); - } else { - answer.style.maxHeight = '2000px'; - if (chevron) chevron.style.transform = 'rotate(180deg)'; - btn.setAttribute('aria-expanded', 'true'); - } - }); - }); - } - - // ---------------------------------------------------------- - function _esc(s) { - if (s == null) return ''; - return String(s) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); - } - - function _highlight(text, term) { - if (!term) return text; - const safe = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const re = new RegExp(`(${safe})`, 'gi'); - return _esc(text).replace(re, - '$1' - ); - } - - // ---------------------------------------------------------- - return { init, refresh }; - -})(); diff --git a/backend/static/js/pages/jobs.js b/backend/static/js/pages/jobs.js index 875fe2c..b056d24 100644 --- a/backend/static/js/pages/jobs.js +++ b/backend/static/js/pages/jobs.js @@ -30,7 +30,7 @@ window.Page_jobs = (() => { } _container.innerHTML = ` -
+
@@ -156,7 +156,7 @@ window.Page_jobs = (() => { value="${u ? _esc(u.email || '') : ''}" placeholder="deine@email.de" required>
-
+
diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index f210960..fd15e1d 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -840,21 +840,15 @@ window.Page_map = (() => { // Einzelne Basis-Typen — Mehrfachauswahl möglich (außer giftkoeder = exklusiv) const PIN_TYPES = [ - { type: 'giftkoeder', icon: '', label: 'Giftköder', color: '#DC2626', exclusive: true }, - { type: 'gefahr', icon: '', label: 'Gefahr', color: '#F59E0B' }, - { type: 'freilauf', icon: '', label: 'Freilauf', color: '#22C55E' }, - { type: 'dog_park', icon: '', label: 'Hundewiese', color: '#15803D' }, - { type: 'restaurant', icon: '', label: 'Restaurant', color: '#F97316' }, - { type: 'shop', icon: '', label: 'Shop', color: '#3B82F6' }, - { type: 'tierarzt', icon: '', label: 'Tierarzt', color: '#EF4444' }, - { type: 'hundeschule', icon: '', label: 'Hundeschule', color: '#8B5CF6' }, - { type: 'waste_basket', icon: '', label: 'Mülleimer', color: '#6B7280' }, - { type: 'kotbeutel', icon: '', label: 'Kotbeutel', color: '#84A98C' }, - { type: 'bank', icon: '', label: 'Sitzbank', color: '#92400E' }, - { type: 'drinking_water',icon: '', label: 'Wasserstelle',color: '#0EA5E9' }, - { type: 'parkplatz', icon: '', label: 'Parkplatz', color: '#2563EB' }, - { type: 'treffpunkt', icon: '', label: 'Treffpunkt', color: '#7C3AED' }, - { type: 'sonstiges', icon: '', label: 'Sonstiges', color: '#F59E0B' }, + { type: 'giftkoeder', icon: '', label: 'Giftköder', color: '#DC2626', exclusive: true }, + { type: 'waste_basket', icon: '', label: 'Mülleimer', color: '#6B7280' }, + { type: 'kotbeutel', icon: '', label: 'Kotbeutel', color: '#84A98C' }, + { type: 'bank', icon: '', label: 'Sitzbank', color: '#92400E' }, + { type: 'drinking_water',icon: '', label: 'Wasserstelle',color: '#0EA5E9' }, + { type: 'dog_park', icon: '', label: 'Hundewiese', color: '#15803D' }, + { type: 'parkplatz', icon: '', label: 'Parkplatz', color: '#2563EB' }, + { type: 'treffpunkt', icon: '', label: 'Treffpunkt', color: '#7C3AED' }, + { type: 'sonstiges', icon: '', label: 'Sonstiges', color: '#F59E0B' }, ]; function _confirmPlacement(latlng) { diff --git a/backend/static/js/pages/reise.js b/backend/static/js/pages/reise.js deleted file mode 100644 index 0b8b3a4..0000000 --- a/backend/static/js/pages/reise.js +++ /dev/null @@ -1,496 +0,0 @@ -/* ============================================================ - BAN YARO — Reise mit Hund - Tabs: Checkliste | EU-Länder | Notfälle - ============================================================ */ - -window.Page_reise = (() => { - - let _container = null; - let _appState = null; - let _activeTab = 'checkliste'; - - const TABS = [ - { key: 'checkliste', label: 'Checkliste', icon: '' }, - { key: 'laender', label: 'EU-Länder', icon: '' }, - ]; - - const CHECKLIST = [ - { - key: 'dokumente', - label: 'Dokumente', - icon: 'file-text', - items: [ - 'EU-Heimtierausweis (Pflicht innerhalb EU)', - 'Impfpass (Tollwut mind. 21 Tage alt)', - 'Krankenkassen-Notfallkarte Tierarzt', - 'Foto des Hundes (für Vermisst-Fall)', - 'Chip-Nummer notiert', - ], - }, - { - key: 'gesundheit', - label: 'Gesundheit', - icon: 'heartbeat', - items: [ - 'Zecken-/Flohschutz aufgefrischt', - 'Reisekrankheit-Mittel (falls nötig)', - 'Medikamente ausreichend eingepackt', - 'Tierarzt-Kontakt am Zielort recherchiert', - 'Verbandszeug für Hunde', - ], - }, - { - key: 'ausruestung', - label: 'Ausrüstung', - icon: 'backpack', - items: [ - 'Leine + Ersatzleine', - 'Halsband mit Adressanhänger', - 'Transportbox/Reisekorb', - 'Lieblingsdecke/Schlafplatz', - 'Spielzeug (2–3 Stück)', - ], - }, - { - key: 'futter', - label: 'Futter & Wasser', - icon: 'bowl-food', - items: [ - 'Genug Futter (+ Reserve)', - 'Wassernapf + Flasche', - 'Futternapf', - 'Bekannte Leckerlis', - ], - }, - { - key: 'auto', - label: 'Im Auto', - icon: 'car', - items: [ - 'Sicherheitsgurt-Adapter oder Box gesichert', - 'Sonnenschutz-Netz für Fenster', - 'Pausen alle 2h eingeplant', - ], - }, - ]; - - const LAENDER = [ - { flag: '🇩🇪', name: 'Deutschland', regel: 'Keine Einschränkungen bei EU-Pass + Chip' }, - { flag: '🇦🇹', name: 'Österreich', regel: 'Gleiche Regeln wie DE, Leinenpflicht in Bergbahnen' }, - { flag: '🇨🇭', name: 'Schweiz', regel: 'Nicht-EU → eigene Einfuhrregeln, Tollwut-Titer-Test', warn: true }, - { flag: '🇮🇹', name: 'Italien', regel: 'Leinenpflicht öffentlich, Maulkorb in öffentlichen Verkehrsmitteln' }, - { flag: '🇫🇷', name: 'Frankreich', regel: 'Manche Strände im Sommer hundeverboten' }, - { flag: '🇬🇷', name: 'Griechenland', regel: 'Hunde erlaubt, kaum Einschränkungen' }, - { flag: '🇭🇷', name: 'Kroatien', regel: 'Viele Strände hundefreundlich, EU-Pass genügt' }, - { flag: '🇬🇧', name: 'Großbritannien', regel: 'Strenge Einreise! PETS-Zertifikat + Tollwut-Impfung + Bandwurm-Behandlung nötig', warn: true }, - ]; - - const SOFORTMASSNAHMEN = [ - { icon: 'thermometer-hot', text: 'Hitzschlag: Sofort Schatten, kühlen mit lauwarmem Wasser, Tierarzt rufen' }, - { icon: 'skull', text: 'Vergiftung: Ruhig halten, NICHT erbrechen lassen, sofort Tiergift-Notfall anrufen' }, - { icon: 'drop', text: 'Starke Blutung: Druckverband anlegen, Druck halten, Tierarzt aufsuchen' }, - { icon: 'bone', text: 'Knochenbruch: Ruhigstellen, nicht bewegen, Tierarzt aufsuchen' }, - { icon: 'heartbeat', text: 'Bewusstlosigkeit: Atemwege freihalten, stabile Seitenlage, 112 rufen' }, - ]; - - const LS_KEY = 'banyaro_reise_checkliste'; - const LS_CUSTOM_KEY = 'banyaro_reise_custom'; // {catKey: ["custom item",...]} - const LS_HIDDEN_KEY = 'banyaro_reise_hidden'; // {itemKey: true} — gelöschte Standard-Items - let _editMode = false; - - function _loadCustom() { try { return JSON.parse(localStorage.getItem(LS_CUSTOM_KEY) || '{}'); } catch { return {}; } } - function _saveCustom(d) { try { localStorage.setItem(LS_CUSTOM_KEY, JSON.stringify(d)); } catch {} } - function _loadHidden() { try { return JSON.parse(localStorage.getItem(LS_HIDDEN_KEY) || '{}'); } catch { return {}; } } - function _saveHidden(d) { try { localStorage.setItem(LS_HIDDEN_KEY, JSON.stringify(d)); } catch {} } - - // ------------------------------------------------------------------ - // Helpers - // ------------------------------------------------------------------ - function _esc(s) { - if (s == null) return ''; - return String(s) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); - } - - function _loadChecked() { - try { return JSON.parse(localStorage.getItem(LS_KEY) || '{}'); } - catch { return {}; } - } - - function _saveChecked(state) { - try { localStorage.setItem(LS_KEY, JSON.stringify(state)); } - catch {} - } - - function _itemKey(catKey, idx) { return `${catKey}__${idx}`; } - - // ------------------------------------------------------------------ - // LIFECYCLE - // ------------------------------------------------------------------ - function init(container, appState, params = {}) { - _container = container; - _appState = appState; - if (params?.tab && TABS.some(t => t.key === params.tab)) { - _activeTab = params.tab; - } - _render(); - } - - function refresh() { _renderTabContent(); } - function onDogChange() {} - - // ------------------------------------------------------------------ - // RENDER - // ------------------------------------------------------------------ - function _render() { - _container.innerHTML = ` -
-
- `; - _renderTabBar(); - _renderTabContent(); - } - - function _renderTabBar() { - const el = _container.querySelector('#reise-tabs'); - if (!el) return; - el.innerHTML = TABS.map(t => ` - `).join(''); - el.querySelectorAll('.by-tab').forEach(btn => { - btn.addEventListener('click', () => { - _activeTab = btn.dataset.tab; - el.querySelectorAll('.by-tab').forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - _renderTabContent(); - }); - }); - } - - function _renderTabContent() { - const el = _container.querySelector('#reise-content'); - if (!el) return; - if (_activeTab === 'checkliste') _renderCheckliste(el); - else if (_activeTab === 'laender') _renderLaender(el); - else if (_activeTab === 'notfall') _renderNotfall(el); - } - - // ------------------------------------------------------------------ - // TAB 1: CHECKLISTE - // ------------------------------------------------------------------ - function _renderCheckliste(el) { - const checked = _loadChecked(); - const custom = _loadCustom(); - const hidden = _loadHidden(); - - // Alle sichtbaren Items zählen - let totalItems = 0, doneItems = 0; - CHECKLIST.forEach(cat => { - cat.items.forEach((_, idx) => { - if (!hidden[_itemKey(cat.key, idx)]) { - totalItems++; - if (checked[_itemKey(cat.key, idx)]) doneItems++; - } - }); - (custom[cat.key] || []).forEach((_, i) => { - totalItems++; - if (checked[`${cat.key}__custom__${i}`]) doneItems++; - }); - }); - const pct = totalItems > 0 ? Math.round((doneItems / totalItems) * 100) : 0; - - const cats = CHECKLIST.map(cat => { - const customItems = custom[cat.key] || []; - - const stdRows = cat.items.map((item, idx) => { - if (hidden[_itemKey(cat.key, idx)]) return ''; - const key = _itemKey(cat.key, idx); - const done = !!checked[key]; - if (_editMode) { - return `
- ${_esc(item)} - -
`; - } - return ``; - }).join(''); - - const customRows = customItems.map((item, i) => { - const key = `${cat.key}__custom__${i}`; - const done = !!checked[key]; - if (_editMode) { - return `
- ${_esc(item)} - -
`; - } - return ``; - }).join(''); - - const addRow = _editMode ? ` -
-
- - -
-
` : ''; - - return `
-
- - ${_esc(cat.label)} -
-
- ${stdRows}${customRows}${addRow} -
-
`; - }).join(''); - - el.innerHTML = ` - - -
-
- ${doneItems} von ${totalItems} erledigt -
- ${pct}% - -
-
-
-
-
-
- ${cats} - ${!_editMode ? `
- -
` : ''} - `; - - // Checkbox events - el.querySelectorAll('.reise-cb').forEach(cb => { - cb.addEventListener('change', () => { - const key = cb.dataset.key; - const cur = _loadChecked(); - cur[key] = cb.checked; - _saveChecked(cur); - _renderTabContent(); - }); - }); - - // Edit-Toggle - el.querySelector('#reise-edit-toggle')?.addEventListener('click', () => { - _editMode = !_editMode; - _renderTabContent(); - }); - - // Standard-Item löschen (verstecken) - el.querySelectorAll('.reise-del-btn').forEach(btn => { - btn.addEventListener('click', () => { - const h = _loadHidden(); - h[btn.dataset.hide] = true; - _saveHidden(h); - _renderTabContent(); - }); - }); - - // Custom-Item löschen - el.querySelectorAll('.reise-del-custom-btn').forEach(btn => { - btn.addEventListener('click', () => { - const c = _loadCustom(); - c[btn.dataset.cat] = (c[btn.dataset.cat] || []).filter((_, i) => i !== parseInt(btn.dataset.idx)); - _saveCustom(c); - _renderTabContent(); - }); - }); - - // Custom-Item hinzufügen - el.querySelectorAll('.reise-add-btn').forEach(btn => { - btn.addEventListener('click', () => { - const cat = btn.dataset.cat; - const input = el.querySelector(`.reise-add-input[data-cat="${cat}"]`); - const val = (input?.value || '').trim(); - if (!val) return; - const c = _loadCustom(); - if (!c[cat]) c[cat] = []; - c[cat].push(val); - _saveCustom(c); - _renderTabContent(); - }); - }); - - // Enter in Add-Input - el.querySelectorAll('.reise-add-input').forEach(input => { - input.addEventListener('keydown', e => { - if (e.key === 'Enter') el.querySelector(`.reise-add-btn[data-cat="${input.dataset.cat}"]`)?.click(); - }); - }); - - el.querySelector('#reise-reset-btn')?.addEventListener('click', () => { - _saveChecked({}); - _renderTabContent(); - }); - } - - // ------------------------------------------------------------------ - // TAB 2: EU-LÄNDER - // ------------------------------------------------------------------ - function _renderLaender(el) { - const cards = LAENDER.map(l => ` -
-
- ${l.flag} -
-
- ${_esc(l.name)} -
-
- ${_esc(l.regel)} -
-
- ${l.warn ? `` : ''} -
-
`).join(''); - - el.innerHTML = ` -
- - EU-Heimtierausweis + Mikrochip + gültige Tollwut-Impfung (mindestens 21 Tage alt) - sind Pflicht für alle EU-Reisen. Informationen können sich ändern — immer beim - Zielland-Konsulat oder Tierarzt aktuell prüfen. -
- ${cards} - `; - } - - // ------------------------------------------------------------------ - // TAB 3: NOTFÄLLE - // ------------------------------------------------------------------ - function _renderNotfall(el) { - const massnahmen = SOFORTMASSNAHMEN.map(m => ` -
- - ${_esc(m.text)} -
`).join(''); - - el.innerHTML = ` -
-
- - Notrufnummern -
-
- - - 112 — EU-Notruf - - - - Tiergift-Notruf München - -
- +49 89 19240 (Tierärztliche Hochschule) -
-
-
- -
-
Tierarzt finden
-
- -
-
- -
-
- - Sofortmaßnahmen -
-
- ${massnahmen} -
-
- `; - - el.querySelector('#reise-map-btn')?.addEventListener('click', () => { - App.navigate('map'); - }); - } - - // ------------------------------------------------------------------ - // PUBLIC - // ------------------------------------------------------------------ - return { init, refresh, onDogChange }; -})(); diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index beb8ae0..b829dea 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -270,12 +270,6 @@ window.Page_settings = (() => { Welten einrichten
-