Teil 3: Terminvorschläge + KI-Limit-Bypass für Admins/Mods — SW by-v435, APP_VER 414

- timeutils: next_appointment_slot() parst OSM opening_hours, findet Slot
- GET /health/terminvorschlaege: fällige/überfällige Einträge (30-Tage-Horizont)
  Impfung/Tierarzt nutzen Praxis-Öffnungszeiten, Rest nächster Werktag 09:00
- Frontend: Terminvorschlags-Karten, bestätigbares Modal, legt Event an
- ki.py: Admins, Moderatoren, Media Manager bypassen CLOUD_WEEKLY_LIMIT
This commit is contained in:
rene 2026-04-26 17:08:18 +02:00
parent 570dcd4e93
commit c935d3fbd4
7 changed files with 300 additions and 9 deletions

View file

@ -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