237 lines
8.1 KiB
Python
237 lines
8.1 KiB
Python
|
|
"""Ampel-Darstellungsschicht: Strang-basierte Klassifikation mit Ampel-Visualisierung.
|
||
|
|
|
||
|
|
Dies ist eine reine Darstellungsschicht über der bestehenden Status-Engine (core/status.py).
|
||
|
|
Die Status-Engine bleibt unverändert — die Ampel mappt deren Ergebnisse auf visuelle Schritte.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
# Definiert die Zustände pro Strang in Reihenfolge
|
||
|
|
STRANG_ZUSTAENDE: dict[str, list[dict]] = {
|
||
|
|
"antrag": [
|
||
|
|
{"id": "eingereicht", "label": "Eingereicht", "endfarbe": None},
|
||
|
|
{"id": "in_beratung", "label": "In Beratung", "endfarbe": None},
|
||
|
|
{"id": "beschlossen", "label": "Beschlossen", "endfarbe": "gelb"},
|
||
|
|
{"id": "umgesetzt", "label": "Umgesetzt", "endfarbe": "gruen"},
|
||
|
|
],
|
||
|
|
"beschlussvorlage": [
|
||
|
|
{"id": "vorgelegt", "label": "Vorgelegt", "endfarbe": None},
|
||
|
|
{"id": "in_beratung", "label": "In Beratung", "endfarbe": None},
|
||
|
|
{"id": "beschlossen", "label": "Beschlossen", "endfarbe": "gelb"},
|
||
|
|
{"id": "umgesetzt", "label": "Umgesetzt", "endfarbe": "gruen"},
|
||
|
|
],
|
||
|
|
"anfrage": [
|
||
|
|
{"id": "angefragt", "label": "Angefragt", "endfarbe": "gelb"},
|
||
|
|
{"id": "beantwortet", "label": "Beantwortet", "endfarbe": "gruen"},
|
||
|
|
],
|
||
|
|
"mitteilung": [
|
||
|
|
{"id": "vorgelegt", "label": "Vorgelegt", "endfarbe": None},
|
||
|
|
{"id": "zur_kenntnis", "label": "Zur Kenntnis genommen", "endfarbe": "grau"},
|
||
|
|
],
|
||
|
|
}
|
||
|
|
|
||
|
|
# Endstatus die von der Hauptreihe abzweigen
|
||
|
|
ABZWEIGUNGEN: dict[str, dict] = {
|
||
|
|
"abgelehnt": {"label": "Abgelehnt", "farbe": "rot"},
|
||
|
|
"abgewiegelt": {"label": "Abgewiegelt", "farbe": "rot"},
|
||
|
|
"versandet": {"label": "Versandet", "farbe": "rot"},
|
||
|
|
"teilweise_umgesetzt": {"label": "Teilweise umgesetzt", "farbe": "amber"},
|
||
|
|
"verwiesen": {"label": "Verwiesen", "farbe": "gelb"},
|
||
|
|
"zurueckgezogen": {"label": "Zurückgezogen", "farbe": "grau"},
|
||
|
|
}
|
||
|
|
|
||
|
|
# Labels für Stränge
|
||
|
|
STRANG_LABELS: dict[str, str] = {
|
||
|
|
"antrag": "Antrag",
|
||
|
|
"beschlussvorlage": "Beschlussvorlage",
|
||
|
|
"anfrage": "Anfrage",
|
||
|
|
"mitteilung": "Mitteilung",
|
||
|
|
"sonstig": "Sonstig",
|
||
|
|
}
|
||
|
|
|
||
|
|
# Kontrollfragen pro Strang
|
||
|
|
KONTROLLFRAGEN: dict[str, str | None] = {
|
||
|
|
"antrag": "Hat die Verwaltung umgesetzt?",
|
||
|
|
"beschlussvorlage": "Wurde so umgesetzt wie beschlossen?",
|
||
|
|
"anfrage": "Wurde befriedigend geantwortet?",
|
||
|
|
"mitteilung": None,
|
||
|
|
}
|
||
|
|
|
||
|
|
# Mapping: DB-Status → (Schritt-ID der letzten erreichten Position, ist Abzweigung?)
|
||
|
|
# Für jeden Strang kann das Mapping unterschiedlich sein.
|
||
|
|
# Wir definieren ein generisches Mapping und strang-spezifische Overrides.
|
||
|
|
|
||
|
|
_STATUS_TO_SCHRITT: dict[str, dict[str, tuple[str, bool]]] = {
|
||
|
|
"antrag": {
|
||
|
|
"eingereicht": ("eingereicht", False),
|
||
|
|
"in_beratung": ("in_beratung", False),
|
||
|
|
"vertagt": ("in_beratung", False), # Vertagt = noch in Beratung
|
||
|
|
"beschlossen": ("beschlossen", False),
|
||
|
|
"umgesetzt": ("umgesetzt", False),
|
||
|
|
"versandet": ("beschlossen", True),
|
||
|
|
"abgelehnt": ("in_beratung", True),
|
||
|
|
"teilweise_umgesetzt": ("beschlossen", True),
|
||
|
|
"verwiesen": ("in_beratung", True),
|
||
|
|
"zurückgezogen": ("in_beratung", True),
|
||
|
|
"zurueckgezogen": ("in_beratung", True),
|
||
|
|
"abgewiegelt": ("beschlossen", True),
|
||
|
|
"offen": ("in_beratung", False),
|
||
|
|
},
|
||
|
|
"beschlussvorlage": {
|
||
|
|
"eingereicht": ("vorgelegt", False),
|
||
|
|
"vorgelegt": ("vorgelegt", False),
|
||
|
|
"in_beratung": ("in_beratung", False),
|
||
|
|
"vertagt": ("in_beratung", False),
|
||
|
|
"beschlossen": ("beschlossen", False),
|
||
|
|
"umgesetzt": ("umgesetzt", False),
|
||
|
|
"versandet": ("beschlossen", True),
|
||
|
|
"abgelehnt": ("in_beratung", True),
|
||
|
|
"teilweise_umgesetzt": ("beschlossen", True),
|
||
|
|
"verwiesen": ("in_beratung", True),
|
||
|
|
"zurückgezogen": ("in_beratung", True),
|
||
|
|
"zurueckgezogen": ("in_beratung", True),
|
||
|
|
"abgewiegelt": ("beschlossen", True),
|
||
|
|
"offen": ("in_beratung", False),
|
||
|
|
},
|
||
|
|
"anfrage": {
|
||
|
|
"angefragt": ("angefragt", False),
|
||
|
|
"beantwortet": ("beantwortet", False),
|
||
|
|
"offen": ("angefragt", False),
|
||
|
|
"abgewiegelt": ("angefragt", True),
|
||
|
|
"versandet": ("angefragt", True),
|
||
|
|
"zurückgezogen": ("angefragt", True),
|
||
|
|
"zurueckgezogen": ("angefragt", True),
|
||
|
|
},
|
||
|
|
"mitteilung": {
|
||
|
|
"vorgelegt": ("vorgelegt", False),
|
||
|
|
"zur_kenntnis": ("zur_kenntnis", False),
|
||
|
|
"eingereicht": ("vorgelegt", False),
|
||
|
|
"beantwortet": ("zur_kenntnis", False), # Mapped to equivalent
|
||
|
|
"beschlossen": ("zur_kenntnis", False),
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def _normalize_abzweigung_id(status: str) -> str:
|
||
|
|
"""Normalize status string to ABZWEIGUNGEN key."""
|
||
|
|
mapping = {
|
||
|
|
"zurückgezogen": "zurueckgezogen",
|
||
|
|
}
|
||
|
|
return mapping.get(status, status)
|
||
|
|
|
||
|
|
|
||
|
|
def get_ampel(strang: str, aktueller_status: str) -> dict | None:
|
||
|
|
"""Gibt die Ampel-Daten für Frontend zurück.
|
||
|
|
|
||
|
|
Returns None if strang is unknown or 'sonstig'.
|
||
|
|
"""
|
||
|
|
if not strang or strang not in STRANG_ZUSTAENDE:
|
||
|
|
return None
|
||
|
|
|
||
|
|
schritte_def = STRANG_ZUSTAENDE[strang]
|
||
|
|
status_map = _STATUS_TO_SCHRITT.get(strang, {})
|
||
|
|
|
||
|
|
# Determine position and whether it's a branch-off
|
||
|
|
mapping = status_map.get(aktueller_status or "")
|
||
|
|
if mapping is None:
|
||
|
|
# Unknown status — show first step as active
|
||
|
|
aktiver_schritt_id = schritte_def[0]["id"]
|
||
|
|
ist_abzweigung = False
|
||
|
|
else:
|
||
|
|
aktiver_schritt_id, ist_abzweigung = mapping
|
||
|
|
|
||
|
|
# Find index of active step
|
||
|
|
schritt_ids = [s["id"] for s in schritte_def]
|
||
|
|
try:
|
||
|
|
aktiver_idx = schritt_ids.index(aktiver_schritt_id)
|
||
|
|
except ValueError:
|
||
|
|
aktiver_idx = 0
|
||
|
|
|
||
|
|
# Build schritte list
|
||
|
|
schritte = []
|
||
|
|
for i, s in enumerate(schritte_def):
|
||
|
|
if ist_abzweigung:
|
||
|
|
# Bei Abzweigung: alle bis aktiver_idx sind "erreicht", keiner ist "aktiv"
|
||
|
|
erreicht = i <= aktiver_idx
|
||
|
|
aktiv = False
|
||
|
|
else:
|
||
|
|
erreicht = i <= aktiver_idx
|
||
|
|
aktiv = i == aktiver_idx
|
||
|
|
|
||
|
|
# Farbe bestimmen
|
||
|
|
if aktiv and s["endfarbe"]:
|
||
|
|
farbe = s["endfarbe"]
|
||
|
|
elif aktiv:
|
||
|
|
farbe = "blau" # Aktiver Schritt ohne spezielle Endfarbe
|
||
|
|
elif erreicht:
|
||
|
|
farbe = "grau" # Bereits durchlaufen
|
||
|
|
else:
|
||
|
|
farbe = "grau" # Noch nicht erreicht
|
||
|
|
|
||
|
|
schritte.append({
|
||
|
|
"id": s["id"],
|
||
|
|
"label": s["label"],
|
||
|
|
"aktiv": aktiv,
|
||
|
|
"erreicht": erreicht,
|
||
|
|
"farbe": farbe,
|
||
|
|
})
|
||
|
|
|
||
|
|
# Abzweigung
|
||
|
|
abzweigung = None
|
||
|
|
if ist_abzweigung:
|
||
|
|
norm_status = _normalize_abzweigung_id(aktueller_status or "")
|
||
|
|
if norm_status in ABZWEIGUNGEN:
|
||
|
|
abzw = ABZWEIGUNGEN[norm_status]
|
||
|
|
abzweigung = {
|
||
|
|
"id": norm_status,
|
||
|
|
"label": abzw["label"],
|
||
|
|
"farbe": abzw["farbe"],
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
"strang": strang,
|
||
|
|
"strang_label": STRANG_LABELS.get(strang, strang.capitalize()),
|
||
|
|
"kontrollfrage": KONTROLLFRAGEN.get(strang),
|
||
|
|
"schritte": schritte,
|
||
|
|
"abzweigung": abzweigung,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def get_ampel_kompakt(strang: str, aktueller_status: str) -> dict | None:
|
||
|
|
"""Kompakte Ampel-Version für Listen: nur aktueller Schritt + Farbe."""
|
||
|
|
ampel = get_ampel(strang, aktueller_status)
|
||
|
|
if not ampel:
|
||
|
|
return None
|
||
|
|
|
||
|
|
if ampel["abzweigung"]:
|
||
|
|
return {
|
||
|
|
"schritt": ampel["abzweigung"]["label"],
|
||
|
|
"farbe": ampel["abzweigung"]["farbe"],
|
||
|
|
"ist_abzweigung": True,
|
||
|
|
}
|
||
|
|
|
||
|
|
aktiver = next((s for s in ampel["schritte"] if s["aktiv"]), None)
|
||
|
|
if aktiver:
|
||
|
|
return {
|
||
|
|
"schritt": aktiver["label"],
|
||
|
|
"farbe": aktiver["farbe"],
|
||
|
|
"ist_abzweigung": False,
|
||
|
|
}
|
||
|
|
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def get_ampel_definition() -> dict:
|
||
|
|
"""Gibt die komplette Strang-Definition zurück (für Legende im Frontend)."""
|
||
|
|
return {
|
||
|
|
"straenge": {
|
||
|
|
strang: {
|
||
|
|
"label": STRANG_LABELS.get(strang, strang.capitalize()),
|
||
|
|
"kontrollfrage": KONTROLLFRAGEN.get(strang),
|
||
|
|
"schritte": schritte,
|
||
|
|
}
|
||
|
|
for strang, schritte in STRANG_ZUSTAENDE.items()
|
||
|
|
},
|
||
|
|
"abzweigungen": ABZWEIGUNGEN,
|
||
|
|
}
|