kundenplattform/routers/struktur.py
Thomas Stallinger 232d621245 feat: Struktur (OE-Selfservice) + Gruppen-Verwaltung (Admin)
Kundenportal:
- Neue Seite /struktur - OE-Baum (rekursives Jinja-Makro), Anlegen/
  Umbenennen/Löschen (Standard-OE 'Neue Geräte' geschützt), Merkmale-
  Editor pro OE mit Tristate aktiv/inaktiv/nicht festgelegt.
- geraete_liste.html: neue Spalte 'OE', Aktionen-Dropdown um 'In OE
  verschieben' erweitert.
- auftragskatalog.html: zeigt jetzt die Herkunft einer Einstellung an
  (OE/Gruppe/manuell), sofern sie nicht vom Workspace-Default kommt.

Admin (/admin/kunden/{id}):
- Neue Sektion 'Gruppen' - Anlegen/Löschen, verlinkt auf neue
  Detailseite (admin/gruppe_detail.html) mit Merkmale-Editor (wie OE)
  und Geräte-Mitgliedschaft-Checkliste. Bewusst nur intern verwaltbar
  in v1 (siehe Konfigurationsgruppen-Diskussion).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 16:41:04 +02:00

157 lines
5.7 KiB
Python

from fastapi import APIRouter, Depends, Form, HTTPException, Request
from fastapi.responses import RedirectResponse
from anode_client import anode_request
from auth import get_current_user
from templating import templates
router = APIRouter()
def fetch_organisationseinheiten(organization_id: str) -> list[dict]:
result = anode_request("GET", f"/api/v1/organizations/{organization_id}/organisationseinheiten")
return result.get("organisationseinheiten", []) if result.get("success") else []
def find_oe_in_organization(oe_id: str, organization_id: str) -> dict | None:
"""
Besitz-Validierung analog zu find_device_in_organization() in
routers/geraete.py (siehe ADR-0011): eine oe_id aus der URL wird nur
akzeptiert, wenn sie tatsächlich in der OE-Liste der eigenen
Organisation auftaucht.
"""
for oe in fetch_organisationseinheiten(organization_id):
if oe["id"] == oe_id:
return oe
return None
@router.get("/struktur")
def struktur_ansicht(request: Request, user=Depends(get_current_user)):
oes = fetch_organisationseinheiten(user.organization_id)
return templates.TemplateResponse(
request, "struktur.html", {"user": user, "organisationseinheiten": oes}
)
@router.post("/struktur")
def oe_anlegen(
name: str = Form(...),
parent_id: str = Form(default=""),
user=Depends(get_current_user),
):
anode_request(
"POST",
f"/api/v1/organizations/{user.organization_id}/organisationseinheiten",
json={"name": name, "parent_id": parent_id or None},
)
return RedirectResponse("/struktur", status_code=303)
@router.get("/struktur/{oe_id}/umbenennen")
def oe_umbenennen_form(request: Request, oe_id: str, user=Depends(get_current_user)):
oe = find_oe_in_organization(oe_id, user.organization_id)
if oe is None:
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
return templates.TemplateResponse(request, "struktur_umbenennen.html", {"user": user, "oe": oe})
@router.post("/struktur/{oe_id}/umbenennen")
def oe_umbenennen(oe_id: str, name: str = Form(...), user=Depends(get_current_user)):
oe = find_oe_in_organization(oe_id, user.organization_id)
if oe is None:
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
anode_request(
"PATCH",
f"/api/v1/organisationseinheiten/{oe_id}",
json={"name": name, "parent_id": oe["parent_id"]},
)
return RedirectResponse("/struktur", status_code=303)
@router.get("/struktur/{oe_id}/loeschen")
def oe_loeschen_form(request: Request, oe_id: str, user=Depends(get_current_user)):
oe = find_oe_in_organization(oe_id, user.organization_id)
if oe is None:
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
return templates.TemplateResponse(request, "struktur_loeschen.html", {"user": user, "oe": oe, "fehler": None})
@router.post("/struktur/{oe_id}/loeschen")
def oe_loeschen(request: Request, oe_id: str, user=Depends(get_current_user)):
oe = find_oe_in_organization(oe_id, user.organization_id)
if oe is None:
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
result = anode_request("DELETE", f"/api/v1/organisationseinheiten/{oe_id}")
if not result.get("success"):
return templates.TemplateResponse(
request, "struktur_loeschen.html", {"user": user, "oe": oe, "fehler": result.get("message")}
)
return RedirectResponse("/struktur", status_code=303)
@router.get("/struktur/{oe_id}/merkmale")
def oe_merkmale_ansicht(request: Request, oe_id: str, user=Depends(get_current_user)):
oe = find_oe_in_organization(oe_id, user.organization_id)
if oe is None:
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
result = anode_request("GET", f"/api/v1/organisationseinheiten/{oe_id}/merkmale")
merkmale = result.get("merkmale", []) if result.get("success") else []
kategorien: dict[str, list[dict]] = {}
for eintrag in merkmale:
kategorie_name = eintrag["kategorie"]["name"] if eintrag["kategorie"] else "Ohne Kategorie"
kategorien.setdefault(kategorie_name, []).append(eintrag)
return templates.TemplateResponse(
request, "struktur_merkmale.html", {"user": user, "oe": oe, "kategorien": kategorien}
)
@router.post("/struktur/{oe_id}/merkmale/{merkmal_key}/select")
def oe_merkmal_select(oe_id: str, merkmal_key: str, user=Depends(get_current_user)):
if find_oe_in_organization(oe_id, user.organization_id) is None:
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
anode_request("POST", f"/api/v1/organisationseinheiten/{oe_id}/merkmale/{merkmal_key}/select")
return RedirectResponse(f"/struktur/{oe_id}/merkmale", status_code=303)
@router.post("/struktur/{oe_id}/merkmale/{merkmal_key}/deselect")
def oe_merkmal_deselect(oe_id: str, merkmal_key: str, user=Depends(get_current_user)):
if find_oe_in_organization(oe_id, user.organization_id) is None:
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
anode_request("POST", f"/api/v1/organisationseinheiten/{oe_id}/merkmale/{merkmal_key}/deselect")
return RedirectResponse(f"/struktur/{oe_id}/merkmale", status_code=303)
@router.post("/struktur/{oe_id}/merkmale/{merkmal_key}/unset")
def oe_merkmal_unset(oe_id: str, merkmal_key: str, user=Depends(get_current_user)):
if find_oe_in_organization(oe_id, user.organization_id) is None:
raise HTTPException(status_code=404, detail="OE wurde nicht gefunden.")
anode_request("POST", f"/api/v1/organisationseinheiten/{oe_id}/merkmale/{merkmal_key}/unset")
return RedirectResponse(f"/struktur/{oe_id}/merkmale", status_code=303)