antragstracker/backend/src/tracker/api/routes/fristen.py

199 lines
6.5 KiB
Python
Raw Normal View History

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)}