"""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, }