diff --git a/backend/ki.py b/backend/ki.py index d82522e..ab776ef 100644 --- a/backend/ki.py +++ b/backend/ki.py @@ -90,6 +90,15 @@ def _check_weekly_cloud_limit(user_id: int | None) -> None: try: from database import db with db() as conn: + user = conn.execute( + "SELECT rolle, is_moderator FROM users WHERE id=?", (user_id,) + ).fetchone() + # Admins, Moderatoren und Media Manager haben kein Limit + if user and ( + user["rolle"] in ("admin", "moderator", "media_manager") + or user["is_moderator"] + ): + return used = conn.execute( """SELECT COALESCE(SUM(count), 0) FROM ki_daily_calls WHERE user_id=? AND source='cloud' diff --git a/backend/routes/health.py b/backend/routes/health.py index 0de348b..bad2d01 100644 --- a/backend/routes/health.py +++ b/backend/routes/health.py @@ -467,3 +467,84 @@ async def list_ki_berichte(dog_id: int, user=Depends(get_current_user)): (dog_id,) ).fetchall() return [dict(r) for r in rows] + + +# ------------------------------------------------------------------ +# GET /api/dogs/{dog_id}/health/terminvorschlaege +# Gibt strukturierte Termin-Vorschläge auf Basis fälliger health-Einträge. +# ------------------------------------------------------------------ +_TERMIN_TYPEN = { + 'impfung': {'label': 'Impfung', 'beim_tierarzt': True, 'icon': 'syringe'}, + 'entwurmung': {'label': 'Entwurmung', 'beim_tierarzt': False, 'icon': 'pill'}, + 'tierarzt': {'label': 'Tierarztbesuch','beim_tierarzt': True, 'icon': 'first-aid'}, + 'medikament': {'label': 'Medikament', 'beim_tierarzt': False, 'icon': 'pill'}, + 'laeufigkeit': {'label': 'Läufigkeit', 'beim_tierarzt': False, 'icon': 'calendar'}, +} + +@router.get("/{dog_id}/health/terminvorschlaege") +async def terminvorschlaege(dog_id: int, user=Depends(get_current_user)): + from timeutils import next_appointment_slot + from datetime import date, timedelta + + today = date.today() + horizon = today + timedelta(days=30) + + with db() as conn: + _check_dog_owner(conn, dog_id, user["id"]) + + # Einträge mit fälligem naechstes (überfällig oder in 30 Tagen) + rows = conn.execute( + """SELECT id, typ, bezeichnung, naechstes, tierarzt_id + FROM health + WHERE dog_id=? AND naechstes IS NOT NULL + AND naechstes <= ? AND aktiv=1 + ORDER BY naechstes ASC""", + (dog_id, horizon.isoformat()) + ).fetchall() + + # Primäre Praxis des Users (erste aktive) + praxis = conn.execute( + "SELECT name, opening_hours, lat, lon FROM tieraerzte " + "WHERE user_id=? AND aktiv=1 ORDER BY id LIMIT 1", + (user["id"],) + ).fetchone() + + oh = praxis["opening_hours"] if praxis else None + praxis_name = praxis["name"] if praxis else None + praxis_lat = praxis["lat"] if praxis else None + praxis_lon = praxis["lon"] if praxis else None + + vorschlaege = [] + for r in rows: + cfg = _TERMIN_TYPEN.get(r["typ"]) + if not cfg: + continue + + naechstes = date.fromisoformat(r["naechstes"]) + ueberfaellig = naechstes < today + delta_tage = (naechstes - today).days + + # Terminfindung: bei Tierarzt-Typen Öffnungszeiten nutzen + slot_oh = oh if cfg["beim_tierarzt"] else None + # Frühestens ab morgen, aber nicht vor dem Fälligkeitsdatum wenn noch in der Zukunft + start = today if ueberfaellig else naechstes - timedelta(days=1) + datum_v, uhrzeit_v = next_appointment_slot(slot_oh, start_from=start) + + vorschlaege.append({ + "health_id": r["id"], + "typ": r["typ"], + "label": cfg["label"], + "icon": cfg["icon"], + "bezeichnung": r["bezeichnung"], + "naechstes": r["naechstes"], + "ueberfaellig": ueberfaellig, + "delta_tage": delta_tage, + "beim_tierarzt": cfg["beim_tierarzt"], + "datum_vorschlag": datum_v, + "uhrzeit_vorschlag": uhrzeit_v, + "praxis_name": praxis_name if cfg["beim_tierarzt"] else None, + "praxis_lat": praxis_lat if cfg["beim_tierarzt"] else None, + "praxis_lon": praxis_lon if cfg["beim_tierarzt"] else None, + }) + + return vorschlaege diff --git a/backend/static/js/api.js b/backend/static/js/api.js index 39c423e..a48474f 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -168,7 +168,8 @@ const API = (() => { kiZusammenfassung(dogId) { return post(`/dogs/${dogId}/health/ki-zusammenfassung`); }, - kiBerichte(dogId) { return get(`/dogs/${dogId}/health/ki-berichte`); }, + kiBerichte(dogId) { return get(`/dogs/${dogId}/health/ki-berichte`); }, + terminvorschlaege(dogId) { return get(`/dogs/${dogId}/health/terminvorschlaege`); }, symptomCheck(dogId, symptoms) { return post(`/dogs/${dogId}/health/symptom-check`, { symptoms }); }, diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 7bd4f37..4826d6d 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '413'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '414'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { diff --git a/backend/static/js/pages/health.js b/backend/static/js/pages/health.js index aef0c03..413df57 100644 --- a/backend/static/js/pages/health.js +++ b/backend/static/js/pages/health.js @@ -153,6 +153,7 @@ window.Page_health = (() => { ${transponderHtml}
+ @@ -168,6 +169,7 @@ window.Page_health = (() => { _renderErinnerungen(); _renderTab(); _loadKiBerichte(dog.id); + _loadTerminvorschlaege(dog.id); } // ---------------------------------------------------------- @@ -1990,6 +1992,126 @@ window.Page_health = (() => { // ---------------------------------------------------------- // KI-ZUSAMMENFASSUNG + // ---------------------------------------------------------- + // TERMINVORSCHLÄGE + // ---------------------------------------------------------- + async function _loadTerminvorschlaege(dogId) { + const el = _container.querySelector('#health-terminvorschlaege'); + if (!el) return; + try { + const vorschlaege = await API.health.terminvorschlaege(dogId); + if (!vorschlaege || !vorschlaege.length) return; + + const _fmtDatum = iso => new Date(iso + 'T00:00:00').toLocaleDateString('de-DE', { + weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric' + }); + + el.innerHTML = ` +