from __future__ import annotations """API routes for Ketten (chains).""" from fastapi import APIRouter, Depends, HTTPException, Query from tracker.api.models import ( KetteDetail, KetteKurz, KettenGliedOut, PaginatedKetten, ParteiOut, VorlageKurz, ) from tracker.core.ampel import get_ampel, get_ampel_kompakt from tracker.core.graph import get_kette_graph from tracker.db.session import get_connection router = APIRouter(prefix="/ketten", tags=["Ketten"]) def _db(): conn = get_connection() try: yield conn finally: conn.close() @router.get("", response_model=PaginatedKetten) def list_ketten( page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=200), status: str | None = None, typ: str | None = None, suche: str | None = None, partei: str | None = None, periode: str | None = None, parteien: str | None = None, conn=Depends(_db), ): """List Ketten with optional filters.""" from tracker.core.perioden import periode_date_filter, parteien_kuerzel_filter where_clauses = [] params: list = [] if status: where_clauses.append("k.status = ?") params.append(status) if typ: where_clauses.append("k.typ = ?") params.append(typ) if partei: where_clauses.append( "k.ursprung_id IN (SELECT a.vorlage_id FROM antragsteller a " "JOIN parteien p ON a.partei_id = p.id WHERE p.kuerzel = ?)" ) params.append(partei) if suche: where_clauses.append( "(k.thema LIKE ? OR v.aktenzeichen LIKE ? OR v.betreff LIKE ?)" ) like = f"%{suche}%" params.extend([like, like, like]) # Global filter: Ratsperiode (filter on letzte_aktivitaet) per_clause, per_params = periode_date_filter(periode, "k.letzte_aktivitaet") if per_clause: where_clauses.append(per_clause) params.extend(per_params) # Global filter: Parteien (multi-select on Ursprung-Antragsteller) p_kuerzel = parteien_kuerzel_filter(parteien) if p_kuerzel: placeholders = ",".join("?" * len(p_kuerzel)) where_clauses.append( f"k.ursprung_id IN (SELECT a.vorlage_id FROM antragsteller a " f"JOIN parteien p ON a.partei_id = p.id WHERE p.kuerzel IN ({placeholders}))" ) params.extend(p_kuerzel) where_sql = ("WHERE " + " AND ".join(where_clauses)) if where_clauses else "" total = conn.execute( f"SELECT COUNT(*) as cnt FROM ketten k LEFT JOIN vorlagen v ON k.ursprung_id = v.id {where_sql}", params ).fetchone()["cnt"] offset = (page - 1) * page_size rows = conn.execute( f"""SELECT k.id, k.typ, k.thema, k.status, k.status_seit, k.letzte_aktivitaet, k.vertagungen_count, k.ursprung_id, k.strang, v.aktenzeichen, v.typ as v_typ, v.betreff, v.datum_eingang, v.ist_verwaltungsvorlage, (SELECT COUNT(*) FROM ketten_glieder kg WHERE kg.kette_id = k.id) as glieder_count FROM ketten k LEFT JOIN vorlagen v ON k.ursprung_id = v.id {where_sql} ORDER BY k.letzte_aktivitaet DESC NULLS LAST, k.id DESC LIMIT ? OFFSET ?""", params + [page_size, offset], ).fetchall() items = [ KetteKurz( id=r["id"], ursprung=VorlageKurz( id=r["ursprung_id"], aktenzeichen=r["aktenzeichen"], typ=r["v_typ"], betreff=r["betreff"], datum_eingang=r["datum_eingang"], ist_verwaltungsvorlage=bool(r["ist_verwaltungsvorlage"]), ) if r["ursprung_id"] else None, typ=r["typ"], thema=r["thema"], status=r["status"], status_seit=r["status_seit"], letzte_aktivitaet=r["letzte_aktivitaet"], vertagungen_count=r["vertagungen_count"], glieder_count=r["glieder_count"], strang=r["strang"], ampel=get_ampel_kompakt(r["strang"] or "", r["status"] or ""), ) for r in rows ] return PaginatedKetten(items=items, total=total, page=page, page_size=page_size) @router.get("/{kette_id}", response_model=KetteDetail) def get_kette(kette_id: int, conn=Depends(_db)): """Get a single Kette with all Glieder.""" row = conn.execute( """SELECT k.id, k.typ, k.thema, k.status, k.status_seit, k.letzte_aktivitaet, k.vertagungen_count, k.begruendung, k.ursprung_id, k.strang, v.aktenzeichen, v.typ as v_typ, v.betreff, v.datum_eingang, v.ist_verwaltungsvorlage FROM ketten k LEFT JOIN vorlagen v ON k.ursprung_id = v.id WHERE k.id = ?""", (kette_id,), ).fetchone() if not row: raise HTTPException(status_code=404, detail="Kette nicht gefunden") # Get Glieder glieder_rows = conn.execute( """SELECT kg.position, kg.rolle, v.id, v.aktenzeichen, v.typ, v.betreff, v.datum_eingang, v.ist_verwaltungsvorlage FROM ketten_glieder kg JOIN vorlagen v ON kg.vorlage_id = v.id WHERE kg.kette_id = ? ORDER BY kg.position""", (kette_id,), ).fetchall() glieder = [ KettenGliedOut( vorlage=VorlageKurz( id=g["id"], aktenzeichen=g["aktenzeichen"], typ=g["typ"], betreff=g["betreff"], datum_eingang=g["datum_eingang"], ist_verwaltungsvorlage=bool(g["ist_verwaltungsvorlage"]), ), position=g["position"], rolle=g["rolle"], ) for g in glieder_rows ] # Antragsteller des Ursprungs antragsteller = [] if row["ursprung_id"]: antragsteller_rows = conn.execute(""" SELECT p.id, p.kuerzel, p.name, p.farbe FROM antragsteller a JOIN parteien p ON a.partei_id = p.id WHERE a.vorlage_id = ? """, (row["ursprung_id"],)).fetchall() antragsteller = [ParteiOut(**dict(a)) for a in antragsteller_rows] # Graph/Perlenschnur data graph = get_kette_graph(conn, kette_id) # Umsetzungsbewertung (alle Versionen für diese Kette, neueste zuerst) umsetzung = None umsetzung_versionen = [] import json as _json ub_rows = conn.execute( """SELECT score, begruendung, anmerkungen, erstellt_at, prompt_version FROM ki_bewertungen WHERE kette_id = ? AND typ = 'umsetzung_match' ORDER BY id DESC""", (kette_id,), ).fetchall() for i, ub_row in enumerate(ub_rows): details = {} if ub_row["anmerkungen"]: try: details = _json.loads(ub_row["anmerkungen"]) except Exception: pass entry = { "score": ub_row["score"], "bewertung": details.get("bewertung", ""), "begruendung": ub_row["begruendung"], "kernpunkt_erfuellt": details.get("kernpunkt_erfuellt"), "details": details.get("details", ""), "erstellt_at": ub_row["erstellt_at"], "prompt_version": ub_row["prompt_version"], } if i == 0: umsetzung = entry else: umsetzung_versionen.append(entry) strang = row["strang"] ampel_data = get_ampel(strang or "", row["status"] or "") return KetteDetail( id=row["id"], ursprung=VorlageKurz( id=row["ursprung_id"], aktenzeichen=row["aktenzeichen"], typ=row["v_typ"], betreff=row["betreff"], datum_eingang=row["datum_eingang"], ist_verwaltungsvorlage=bool(row["ist_verwaltungsvorlage"]), ) if row["ursprung_id"] else None, typ=row["typ"], thema=row["thema"], status=row["status"], status_seit=row["status_seit"], letzte_aktivitaet=row["letzte_aktivitaet"], vertagungen_count=row["vertagungen_count"], begruendung=row["begruendung"], glieder=glieder, antragsteller=antragsteller, graph=graph, strang=strang, ampel=ampel_data, umsetzung=umsetzung, umsetzung_versionen=umsetzung_versionen if umsetzung_versionen else None, )