from __future__ import annotations """API routes for Fristen-Tracking (Issue #17).""" from datetime import date, datetime from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from tracker.db.session import get_connection router = APIRouter(prefix="/fristen", tags=["fristen"]) def _db(): conn = get_connection() try: yield conn finally: conn.close() class FristCreate(BaseModel): kette_id: int vorlage_id: Optional[int] = None typ: str # überarbeitung, bericht, prüfung, umsetzung, sonstiges datum: str # YYYY-MM-DD beschreibung: Optional[str] = None class FristUpdate(BaseModel): status: Optional[str] = None datum: Optional[str] = None beschreibung: Optional[str] = None @router.get("") def list_fristen( kette_id: Optional[int] = None, status: Optional[str] = Query(None, description="offen, überfällig, erfüllt, alle"), typ: Optional[str] = None, page: int = 1, page_size: int = 50, conn=Depends(_db), ): """Fristen auflisten mit Joins auf ketten + vorlagen.""" where_clauses = [] params: list = [] if kette_id is not None: where_clauses.append("f.kette_id = ?") params.append(kette_id) if status and status != "alle": if status == "überfällig": where_clauses.append("(f.status = 'überfällig' OR (f.status = 'offen' AND f.datum < date('now')))") else: where_clauses.append("f.status = ?") params.append(status) if typ: where_clauses.append("f.typ = ?") params.append(typ) where_sql = (" WHERE " + " AND ".join(where_clauses)) if where_clauses else "" # Count total = conn.execute(f"SELECT COUNT(*) as cnt FROM fristen f{where_sql}", params).fetchone()["cnt"] # Query with joins offset = (page - 1) * page_size rows = conn.execute( f"""SELECT f.*, k.status AS kette_status, v_ursprung.aktenzeichen AS kette_aktenzeichen, v_ursprung.betreff AS kette_betreff, v_frist.aktenzeichen AS vorlage_aktenzeichen FROM fristen f LEFT JOIN ketten k ON f.kette_id = k.id LEFT JOIN vorlagen v_ursprung ON k.ursprung_id = v_ursprung.id LEFT JOIN vorlagen v_frist ON f.vorlage_id = v_frist.id {where_sql} ORDER BY CASE WHEN f.status = 'überfällig' OR (f.status = 'offen' AND f.datum < date('now')) THEN 0 WHEN f.status = 'offen' THEN 1 ELSE 2 END, f.datum ASC LIMIT ? OFFSET ?""", params + [page_size, offset], ).fetchall() items = [] for r in rows: item = dict(r) # Mark overdue in response if item["status"] == "offen" and item["datum"] and item["datum"] < date.today().isoformat(): item["status"] = "überfällig" items.append(item) return {"items": items, "total": total, "page": page, "page_size": page_size} @router.post("") def create_frist(body: FristCreate, conn=Depends(_db)): """Neue Frist manuell anlegen.""" # Validate kette exists kette = conn.execute("SELECT id FROM ketten WHERE id = ?", (body.kette_id,)).fetchone() if not kette: raise HTTPException(status_code=404, detail="Kette nicht gefunden") if body.vorlage_id: vorlage = conn.execute("SELECT id FROM vorlagen WHERE id = ?", (body.vorlage_id,)).fetchone() if not vorlage: raise HTTPException(status_code=404, detail="Vorlage nicht gefunden") cursor = conn.execute( """INSERT INTO fristen (kette_id, vorlage_id, typ, datum, beschreibung, quelle, status, erstellt_at) VALUES (?, ?, ?, ?, ?, 'manuell', 'offen', ?)""", (body.kette_id, body.vorlage_id, body.typ, body.datum, body.beschreibung, datetime.now().isoformat()), ) conn.commit() frist_id = cursor.lastrowid row = conn.execute("SELECT * FROM fristen WHERE id = ?", (frist_id,)).fetchone() return dict(row) @router.patch("/{frist_id}") def update_frist(frist_id: int, body: FristUpdate, conn=Depends(_db)): """Frist aktualisieren (Status, Datum, Beschreibung).""" existing = conn.execute("SELECT * FROM fristen WHERE id = ?", (frist_id,)).fetchone() if not existing: raise HTTPException(status_code=404, detail="Frist nicht gefunden") updates = [] params = [] if body.status is not None: updates.append("status = ?") params.append(body.status) if body.datum is not None: updates.append("datum = ?") params.append(body.datum) if body.beschreibung is not None: updates.append("beschreibung = ?") params.append(body.beschreibung) if not updates: raise HTTPException(status_code=400, detail="Keine Änderungen angegeben") updates.append("aktualisiert_at = ?") params.append(datetime.now().isoformat()) params.append(frist_id) conn.execute(f"UPDATE fristen SET {', '.join(updates)} WHERE id = ?", params) conn.commit() row = conn.execute("SELECT * FROM fristen WHERE id = ?", (frist_id,)).fetchone() return dict(row) @router.delete("/{frist_id}") def delete_frist(frist_id: int, conn=Depends(_db)): """Frist löschen.""" existing = conn.execute("SELECT * FROM fristen WHERE id = ?", (frist_id,)).fetchone() if not existing: raise HTTPException(status_code=404, detail="Frist nicht gefunden") conn.execute("DELETE FROM fristen WHERE id = ?", (frist_id,)) conn.commit() return {"ok": True, "deleted": frist_id} @router.get("/ueberfaellig") def get_ueberfaellige(conn=Depends(_db)): """Überfällige Fristen abrufen und automatisch Status setzen.""" today = date.today().isoformat() # Update status for overdue fristen conn.execute( "UPDATE fristen SET status = 'überfällig', aktualisiert_at = ? WHERE status = 'offen' AND datum < ?", (datetime.now().isoformat(), today), ) conn.commit() rows = conn.execute( """SELECT f.*, k.status AS kette_status, v_ursprung.aktenzeichen AS kette_aktenzeichen, v_ursprung.betreff AS kette_betreff FROM fristen f LEFT JOIN ketten k ON f.kette_id = k.id LEFT JOIN vorlagen v_ursprung ON k.ursprung_id = v_ursprung.id WHERE f.status = 'überfällig' ORDER BY f.datum ASC""", ).fetchall() return {"items": [dict(r) for r in rows], "total": len(rows)}