"""Reine Status-Helper fuer das progressive Cockpit (V1). Trennt Daten-Logik vom HTTP-Layer: die Funktionen hier bekommen eine profil_id und liefern dicts/listen, die das Template direkt verwendet. So bleibt routes/profil.py kurz und das Cockpit-Verhalten ist isoliert testbar. """ from datetime import date, datetime, timedelta from typing import Optional from database import get_db ONBOARDING_LEKTIONEN = [1, 2, 3] # Pflicht fuer Neue, siehe E-013 ONBOARDING_L1 = 1 # Schaltet Slot-Buchung frei (V2) def get_onboarding_fortschritt(profil_id: int) -> dict: """Liefert pro Onboarding-Lektion den Status. Returns: { 'lektionen': [ {'nummer': 1, 'titel': 'Drucker-Kennenlernen + Sicherheit', 'status': 'fertig'|'laeuft'|'offen', 'aktuell': True/False}, ... ], 'alle_fertig': bool, 'l1_fertig': bool, # V2: schaltet Slot-Buchung frei 'naechste': dict | None, # naechste offene/laufende Lektion (Sprung-Vorschlag) } """ conn = get_db() rows = conn.execute( "SELECT l.nummer, l.titel, COALESCE(s.status, 'offen') AS status " "FROM lektion l " "LEFT JOIN profil_lektion_status s " " ON s.lektion_nummer = l.nummer AND s.profil_id = ? " "WHERE l.onboarding = 1 AND l.aktiv = 1 " "ORDER BY l.nummer", (profil_id,) ).fetchall() conn.close() lektionen = [dict(r) for r in rows] alle_fertig = all(l["status"] == "fertig" for l in lektionen) and len(lektionen) > 0 l1_fertig = any(l["nummer"] == ONBOARDING_L1 and l["status"] == "fertig" for l in lektionen) # naechste: erste, die nicht fertig ist naechste = next((l for l in lektionen if l["status"] != "fertig"), None) for l in lektionen: l["aktuell"] = (naechste is not None and l["nummer"] == naechste["nummer"]) return { "lektionen": lektionen, "alle_fertig": alle_fertig, "l1_fertig": l1_fertig, "naechste": naechste, } def get_check_in_heute(profil_id: int) -> Optional[dict]: """Liefert den Check-In von heute (oder None, wenn noch keiner).""" conn = get_db() row = conn.execute( "SELECT id, modus, freitext, erstellt_am FROM check_in " "WHERE profil_id = ? AND besuch_datum = ?", (profil_id, date.today().isoformat()) ).fetchone() conn.close() return dict(row) if row else None def get_offene_rueckfragen(profil_id: int) -> list[dict]: """Rueckfragen zu eigenen Projekten, die noch offen sind (V6-Banner).""" conn = get_db() rows = conn.execute( "SELECT r.id, r.text, r.erstellt_am, p.titel AS projekt_titel, p.id AS projekt_id " "FROM projekt_rueckfrage r " "JOIN projekt p ON p.id = r.projekt_id " "WHERE p.profil_id = ? AND r.status = 'offen' " "ORDER BY r.erstellt_am DESC", (profil_id,) ).fetchall() conn.close() return [dict(r) for r in rows] def get_ungelesene_notizen(profil_id: int) -> list[dict]: """Lehrer-Notizen, die noch nicht gelesen wurden.""" conn = get_db() rows = conn.execute( "SELECT id, text, typ, erstellt_am FROM lehrer_notiz " "WHERE profil_id = ? AND gelesen_am IS NULL " "ORDER BY erstellt_am DESC", (profil_id,) ).fetchall() conn.close() return [dict(r) for r in rows] def get_aktive_projekte(profil_id: int) -> list[dict]: """Eigene Projekte mit status_lebenszyklus='in_arbeit' (Etappe 6 fuellt das voll).""" conn = get_db() rows = conn.execute( "SELECT id, titel, beschreibung_md, geaendert_am " "FROM projekt " "WHERE profil_id = ? AND status_lebenszyklus = 'in_arbeit' " "ORDER BY geaendert_am DESC", (profil_id,) ).fetchall() conn.close() return [dict(r) for r in rows] def get_paarung(profil_id: int) -> Optional[dict]: """Aktive Paarung des SuS (oder None).""" conn = get_db() row = conn.execute( "SELECT pa.id AS paarung_id, " " CASE WHEN pa.profil_a = ? THEN pa.profil_b ELSE pa.profil_a END AS partner_id, " " pa.gebildet_am " "FROM paarung pa " "WHERE pa.aktiv = 1 AND (pa.profil_a = ? OR pa.profil_b = ?)", (profil_id, profil_id, profil_id) ).fetchone() if not row: conn.close() return None partner = conn.execute( "SELECT id, vorname, tier FROM profil WHERE id = ?", (row["partner_id"],) ).fetchone() conn.close() return { "paarung_id": row["paarung_id"], "partner": dict(partner) if partner else None, "gebildet_am": row["gebildet_am"], } def get_offene_anfragen_an_mich(profil_id: int) -> list[dict]: """Paarungs-Anfragen, die jemand an mich gestellt hat (Status 'wartet').""" conn = get_db() # Tabelle existiert evtl. noch nicht, wenn diese Funktion vor _ensure_anfrage_tabelle aufgerufen wird try: rows = conn.execute( "SELECT a.id, a.erstellt_am, p.id AS von_id, p.vorname AS von_vorname, p.tier AS von_tier " "FROM profil_anfrage a " "JOIN profil p ON p.id = a.von_profil_id " "WHERE a.an_profil_id = ? AND a.status = 'wartet' " "ORDER BY a.erstellt_am DESC", (profil_id,) ).fetchall() except Exception: rows = [] conn.close() return [dict(r) for r in rows] def get_andere_profile(profil_id: int) -> list[dict]: """Andere aktive Profile in derselben AG (fuer Paarungs-Anfrage).""" conn = get_db() rows = conn.execute( "SELECT id, vorname, tier " "FROM profil " "WHERE aktiv = 1 AND id != ? AND ag_id = (" " SELECT ag_id FROM profil WHERE id = ?" ") " "ORDER BY vorname COLLATE NOCASE", (profil_id, profil_id) ).fetchall() conn.close() return [dict(r) for r in rows] # =========================================================================== # Cockpit-Status-Bundle: ein dict mit allem, was das Cockpit-Template braucht # =========================================================================== def get_cockpit_status(profil_id: int) -> dict: """Bundle aller Daten fuer das progressive Cockpit (V1). Das Template entscheidet anhand dieses dicts, welche Karten gezeigt werden. """ onb = get_onboarding_fortschritt(profil_id) aktive_projekte = get_aktive_projekte(profil_id) paarung = get_paarung(profil_id) return { "onboarding": onb, "check_in_heute": get_check_in_heute(profil_id), "rueckfragen": get_offene_rueckfragen(profil_id), "notizen": get_ungelesene_notizen(profil_id), "aktive_projekte": aktive_projekte, "paarung": paarung, "offene_anfragen": get_offene_anfragen_an_mich(profil_id), # Progressive-Schalter: "ist_neu": not onb["alle_fertig"], "darf_slots_buchen": onb["l1_fertig"], "hat_projekte": len(aktive_projekte) > 0, }