diff --git a/backend/database.py b/backend/database.py index a98cda8..5e38a96 100644 --- a/backend/database.py +++ b/backend/database.py @@ -573,6 +573,9 @@ def _migrate(conn_factory): ("users", "password_reset_expires", "TEXT"), # Fell-Typ für personalisierte Wetter-Hinweise ("dogs", "fell_typ", "TEXT"), # kurz|mittel|lang|drahtaar|doppel|nackt + # Tierarzt-Bewertungen: Durchschnitt + Anzahl am Tierarzt-Datensatz + ("tieraerzte", "avg_rating", "REAL DEFAULT 0"), + ("tieraerzte", "anz_bewertungen", "INTEGER DEFAULT 0"), ] with conn_factory() as conn: for table, column, col_type in migrations: @@ -1983,3 +1986,75 @@ def _migrate(conn_factory): ); CREATE INDEX IF NOT EXISTS idx_recurring_user ON recurring_expenses(user_id, aktiv); """) + + # ---- Tierarzt-Bewertungen ---- + conn.executescript(""" + CREATE TABLE IF NOT EXISTS tierarzt_bewertungen ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tierarzt_id INTEGER NOT NULL REFERENCES tieraerzte(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + gesamt INTEGER NOT NULL CHECK(gesamt BETWEEN 1 AND 5), + wartezeit INTEGER CHECK(wartezeit BETWEEN 1 AND 5), + freundlichkeit INTEGER CHECK(freundlichkeit BETWEEN 1 AND 5), + kompetenz INTEGER CHECK(kompetenz BETWEEN 1 AND 5), + text TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(tierarzt_id, user_id) + ); + CREATE INDEX IF NOT EXISTS idx_tierarzt_bew_arzt + ON tierarzt_bewertungen(tierarzt_id); + """) + + # ---- Feature: Foto-Challenge der Woche ---- + conn.executescript(""" + CREATE TABLE IF NOT EXISTS foto_challenge ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + thema TEXT NOT NULL, + beschreibung TEXT, + start_date TEXT NOT NULL, + end_date TEXT NOT NULL, + created_by INTEGER REFERENCES users(id), + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE TABLE IF NOT EXISTS challenge_submissions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + challenge_id INTEGER REFERENCES foto_challenge(id) ON DELETE CASCADE, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + dog_id INTEGER REFERENCES dogs(id) ON DELETE SET NULL, + foto_url TEXT NOT NULL, + caption TEXT, + votes INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')), + UNIQUE(challenge_id, user_id) + ); + CREATE TABLE IF NOT EXISTS challenge_votes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + submission_id INTEGER REFERENCES challenge_submissions(id) ON DELETE CASCADE, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(submission_id, user_id) + ); + CREATE INDEX IF NOT EXISTS idx_challenge_sub_chal + ON challenge_submissions(challenge_id, created_at DESC); + """) + logger.info("Migration: Foto-Challenge-Tabellen bereit.") + + # ---- Feature: Gassi-Zeiten-Pool ---- + conn.executescript(""" + CREATE TABLE IF NOT EXISTS gassi_zeiten ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + dog_id INTEGER REFERENCES dogs(id) ON DELETE SET NULL, + wochentage TEXT NOT NULL, + uhrzeit TEXT NOT NULL, + ort_name TEXT, + lat REAL, + lon REAL, + radius_m INTEGER DEFAULT 500, + notiz TEXT, + aktiv INTEGER DEFAULT 1, + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE INDEX IF NOT EXISTS idx_gassi_zeiten_user + ON gassi_zeiten(user_id, aktiv); + """) + logger.info("Migration: Gassi-Zeiten-Tabelle bereit.") diff --git a/backend/routes/tieraerzte.py b/backend/routes/tieraerzte.py index 48287f9..8448478 100644 --- a/backend/routes/tieraerzte.py +++ b/backend/routes/tieraerzte.py @@ -27,6 +27,14 @@ class TierarztCreate(BaseModel): osm_id: Optional[str] = None +class BewertungCreate(BaseModel): + gesamt: int + wartezeit: Optional[int] = None + freundlichkeit: Optional[int] = None + kompetenz: Optional[int] = None + text: Optional[str] = None + + class TierarztUpdate(BaseModel): name: Optional[str] = None strasse: Optional[str] = None @@ -220,3 +228,109 @@ async def update_tierarzt(tierarzt_id: int, data: TierarztUpdate, ) row = conn.execute("SELECT * FROM tieraerzte WHERE id=?", (tierarzt_id,)).fetchone() return dict(row) + + +# ------------------------------------------------------------------ +# BEWERTUNGEN +# ------------------------------------------------------------------ + +def _refresh_vet_rating(conn, tierarzt_id: int): + """Aktualisiert avg_rating und anz_bewertungen in tieraerzte.""" + row = conn.execute( + """SELECT COUNT(*) AS n, AVG(CAST(gesamt AS REAL)) AS avg + FROM tierarzt_bewertungen WHERE tierarzt_id=?""", + (tierarzt_id,) + ).fetchone() + n = row["n"] or 0 + avg = row["avg"] or 0.0 + conn.execute( + "UPDATE tieraerzte SET avg_rating=?, anz_bewertungen=? WHERE id=?", + (round(avg, 1), n, tierarzt_id) + ) + + +@router.post("/{tierarzt_id}/bewertung", status_code=201) +async def create_bewertung(tierarzt_id: int, data: BewertungCreate, + user=Depends(get_current_user)): + """Bewertung abgeben (1×pro User+Tierarzt, UPSERT).""" + if not (1 <= data.gesamt <= 5): + raise HTTPException(400, "Gesamtbewertung muss zwischen 1 und 5 liegen.") + for field in ("wartezeit", "freundlichkeit", "kompetenz"): + val = getattr(data, field) + if val is not None and not (1 <= val <= 5): + raise HTTPException(400, f"{field} muss zwischen 1 und 5 liegen.") + + text = (data.text or "").strip()[:500] or None + + with db() as conn: + vet = conn.execute("SELECT id FROM tieraerzte WHERE id=?", (tierarzt_id,)).fetchone() + if not vet: + raise HTTPException(404, "Tierarzt nicht gefunden.") + + conn.execute( + """INSERT INTO tierarzt_bewertungen + (tierarzt_id, user_id, gesamt, wartezeit, freundlichkeit, kompetenz, text) + VALUES (?,?,?,?,?,?,?) + ON CONFLICT(tierarzt_id, user_id) DO UPDATE SET + gesamt=excluded.gesamt, + wartezeit=excluded.wartezeit, + freundlichkeit=excluded.freundlichkeit, + kompetenz=excluded.kompetenz, + text=excluded.text, + created_at=datetime('now')""", + (tierarzt_id, user["id"], data.gesamt, data.wartezeit, + data.freundlichkeit, data.kompetenz, text) + ) + _refresh_vet_rating(conn, tierarzt_id) + row = conn.execute( + "SELECT * FROM tieraerzte WHERE id=?", (tierarzt_id,) + ).fetchone() + return dict(row) + + +@router.get("/{tierarzt_id}/bewertungen") +async def list_bewertungen(tierarzt_id: int): + """Alle Bewertungen für einen Tierarzt (public). Gibt Zusammenfassung + letzte 5 Texte.""" + with db() as conn: + vet = conn.execute( + "SELECT id, avg_rating, anz_bewertungen FROM tieraerzte WHERE id=?", + (tierarzt_id,) + ).fetchone() + if not vet: + raise HTTPException(404, "Tierarzt nicht gefunden.") + + # Stern-Verteilung + verteilung = {} + for star in range(1, 6): + r = conn.execute( + "SELECT COUNT(*) AS n FROM tierarzt_bewertungen WHERE tierarzt_id=? AND gesamt=?", + (tierarzt_id, star) + ).fetchone() + verteilung[str(star)] = r["n"] + + # Letzte 5 Kommentare + kommentare = conn.execute( + """SELECT gesamt, wartezeit, freundlichkeit, kompetenz, text, created_at + FROM tierarzt_bewertungen + WHERE tierarzt_id=? AND text IS NOT NULL AND text != '' + ORDER BY created_at DESC LIMIT 5""", + (tierarzt_id,) + ).fetchall() + + return { + "avg_rating": vet["avg_rating"] or 0, + "anz_bewertungen": vet["anz_bewertungen"] or 0, + "verteilung": verteilung, + "kommentare": [dict(k) for k in kommentare], + } + + +@router.get("/{tierarzt_id}/meine-bewertung") +async def get_meine_bewertung(tierarzt_id: int, user=Depends(get_current_user)): + """Eigene Bewertung für einen Tierarzt (oder null).""" + with db() as conn: + row = conn.execute( + "SELECT * FROM tierarzt_bewertungen WHERE tierarzt_id=? AND user_id=?", + (tierarzt_id, user["id"]) + ).fetchone() + return dict(row) if row else None diff --git a/backend/static/index.html b/backend/static/index.html index 37d6fcd..77f0433 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -93,9 +93,9 @@ - - - + + +
@@ -507,6 +507,10 @@ +Bewertungen konnten nicht geladen werden.
' }); + return; + } + + const { avg_rating, anz_bewertungen, verteilung, kommentare } = data; + + // Balkendiagramm + const balken = [5, 4, 3, 2, 1].map(s => { + const n = verteilung[String(s)] || 0; + const pct = anz_bewertungen > 0 ? Math.round((n / anz_bewertungen) * 100) : 0; + return `${_esc(k.text || '')}
+Noch keine Kommentare.
`; + + const bewBody = anz_bewertungen === 0 + ? `+ Noch keine Bewertungen — sei der Erste! +
` + : ` +