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..a883e9b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -235,7 +235,6 @@ 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"]) @@ -297,7 +296,6 @@ app.include_router(playdate_router, prefix="/api/playdate", ta 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,7 +325,7 @@ 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_VER = "715" # muss mit APP_VER in app.js übereinstimmen @app.get("/api/version") async def app_version(): diff --git a/backend/routes/achievements.py b/backend/routes/achievements.py index bff2421..e8d0cba 100644 --- a/backend/routes/achievements.py +++ b/backend/routes/achievements.py @@ -308,8 +308,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 +321,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/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/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/static/index.html b/backend/static/index.html index 9fb7cef..242e2b7 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -409,10 +409,6 @@
-
-
-
-
@@ -578,7 +574,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index f2245de..b117f18 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 = '715'; // ← 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 }, }; // ---------------------------------------------------------- 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/hilfe.js b/backend/static/js/pages/hilfe.js deleted file mode 100644 index bcee0cc..0000000 --- a/backend/static/js/pages/hilfe.js +++ /dev/null @@ -1,246 +0,0 @@ -/* ============================================================ - BAN YARO — Hilfe & FAQ - Akkordeon-FAQ mit Suche, gruppiert nach Kategorie. - ============================================================ */ - -window.Page_hilfe = (() => { - - let _container = null; - let _appState = null; - let _articles = []; - let _search = ''; - - const KAT_LABEL = { - installation: 'Installation & PWA', - erste_schritte: 'Erste Schritte', - standort: 'Standort & Wetter', - account: 'Account & Passwort', - features: 'Features erklärt', - probleme: 'Technische Probleme', - }; - - // ---------------------------------------------------------- - async function init(container, appState) { - _container = container; - _appState = appState; - _search = ''; - - _renderShell(); - await _load(); - } - - async function refresh() { - await _load(); - } - - // ---------------------------------------------------------- - function _renderShell() { - _container.innerHTML = ` -
- -
- - -
-
- - -
-
- Lade… -
-
- `; - - _container.querySelector('#hilfe-search').addEventListener('input', e => { - _search = e.target.value.trim().toLowerCase(); - _render(); - }); - } - - // ---------------------------------------------------------- - async function _load() { - try { - _articles = await API.get('/help'); - } catch { - _articles = []; - } - _render(); - } - - // ---------------------------------------------------------- - function _render() { - const el = _container.querySelector('#hilfe-articles'); - if (!el) return; - - // Filter nach Suchbegriff - const filtered = _search - ? _articles.filter(a => - a.frage.toLowerCase().includes(_search) || - a.antwort.toLowerCase().includes(_search) - ) - : _articles; - - if (!filtered.length) { - el.innerHTML = ` -
- -

- Keine Ergebnisse -

-

- ${_search - ? `Zu "${_esc(_search)}" wurde nichts gefunden.` - : 'Noch keine FAQ-Artikel vorhanden.'} -

-
- `; - return; - } - - // Gruppieren nach Kategorie (Reihenfolge der KAT_LABEL-Keys) - const katOrder = Object.keys(KAT_LABEL); - const grouped = {}; - for (const a of filtered) { - if (!grouped[a.kategorie]) grouped[a.kategorie] = []; - grouped[a.kategorie].push(a); - } - - // Sortieren nach KAT_LABEL-Reihenfolge, dann unbekannte hinten - const sortedKats = [ - ...katOrder.filter(k => grouped[k]), - ...Object.keys(grouped).filter(k => !katOrder.includes(k)), - ]; - - let html = ''; - for (const kat of sortedKats) { - const items = grouped[kat]; - const label = KAT_LABEL[kat] || kat; - - html += ` -
-
- ${_esc(label)} -
-
- `; - - for (const a of items) { - const answerId = `hilfe-ans-${a.id}`; - const chevronId = `hilfe-chev-${a.id}`; - - // Highlight Suchtreffer in der Frage - const frageHtml = _search - ? _highlight(a.frage, _search) - : _esc(a.frage); - - // Antwort: Zeilenumbrüche in
wandeln - const antwortHtml = _search - ? _highlight(a.antwort, _search).replace(/\n/g, '
') - : _esc(a.antwort).replace(/\n/g, '
'); - - // Bei aktiver Suche: Antwort gleich aufgeklappt - const openByDefault = !!_search; - - html += ` -
- -
-
-
${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/settings.js b/backend/static/js/pages/settings.js index beb8ae0..ffc47f7 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -270,12 +270,6 @@ window.Page_settings = (() => { Welten einrichten -