""" Finite State Machine fuer Unterrichtsphasen. Definiert die erlaubten Phasen-Uebergaenge und fuehrt Transitionen durch. """ from datetime import datetime from typing import Optional, List, Dict from .models import LessonPhase, LessonSession, LESSON_PHASES class LessonStateMachine: """ Finite State Machine fuer die Unterrichtsphasen. Erlaubte Uebergaenge: NOT_STARTED -> EINSTIEG -> ERARBEITUNG -> SICHERUNG -> TRANSFER -> REFLEXION -> ENDED """ # Definiert erlaubte Uebergaenge: von_phase -> [moeglich_zu_phasen] TRANSITIONS: Dict[LessonPhase, List[LessonPhase]] = { LessonPhase.NOT_STARTED: [LessonPhase.EINSTIEG], LessonPhase.EINSTIEG: [LessonPhase.ERARBEITUNG], LessonPhase.ERARBEITUNG: [LessonPhase.SICHERUNG], LessonPhase.SICHERUNG: [LessonPhase.TRANSFER], LessonPhase.TRANSFER: [LessonPhase.REFLEXION], LessonPhase.REFLEXION: [LessonPhase.ENDED], LessonPhase.ENDED: [], # Terminal State } # Phasen-Reihenfolge fuer Timeline PHASE_ORDER: List[LessonPhase] = [ LessonPhase.EINSTIEG, LessonPhase.ERARBEITUNG, LessonPhase.SICHERUNG, LessonPhase.TRANSFER, LessonPhase.REFLEXION, ] def can_transition(self, from_phase: LessonPhase, to_phase: LessonPhase) -> bool: """ Prueft ob ein Uebergang von from_phase zu to_phase erlaubt ist. Args: from_phase: Ausgangsphase to_phase: Zielphase Returns: True wenn Uebergang erlaubt, False sonst """ allowed_targets = self.TRANSITIONS.get(from_phase, []) return to_phase in allowed_targets def next_phase(self, current: LessonPhase) -> Optional[LessonPhase]: """ Gibt die naechste Phase zurueck (oder None wenn keine vorhanden). Args: current: Aktuelle Phase Returns: Naechste Phase oder None """ transitions = self.TRANSITIONS.get(current, []) return transitions[0] if transitions else None def previous_phase(self, current: LessonPhase) -> Optional[LessonPhase]: """ Gibt die vorherige Phase zurueck (oder None wenn keine vorhanden). Args: current: Aktuelle Phase Returns: Vorherige Phase oder None """ if current == LessonPhase.NOT_STARTED or current == LessonPhase.ENDED: return None try: idx = self.PHASE_ORDER.index(current) if idx > 0: return self.PHASE_ORDER[idx - 1] except ValueError: pass return None def transition(self, session: LessonSession, to_phase: LessonPhase) -> LessonSession: """ Fuehrt einen Phasen-Uebergang durch. Args: session: Die aktuelle Session to_phase: Zielphase Returns: Aktualisierte Session Raises: ValueError: Wenn Uebergang nicht erlaubt """ if not self.can_transition(session.current_phase, to_phase): raise ValueError( f"Ungueltiger Uebergang: {session.current_phase.value} -> {to_phase.value}. " f"Erlaubte Ziele: {[p.value for p in self.TRANSITIONS.get(session.current_phase, [])]}" ) now = datetime.utcnow() # Historie der aktuellen Phase speichern (wenn nicht NOT_STARTED) if session.phase_started_at and session.current_phase != LessonPhase.NOT_STARTED: duration_seconds = (now - session.phase_started_at).total_seconds() session.phase_history.append({ "phase": session.current_phase.value, "started_at": session.phase_started_at.isoformat(), "ended_at": now.isoformat(), "duration_seconds": int(duration_seconds), }) # State aktualisieren session.current_phase = to_phase session.phase_started_at = now # Spezielle Behandlung fuer Start und Ende if to_phase == LessonPhase.EINSTIEG: session.lesson_started_at = now elif to_phase == LessonPhase.ENDED: session.lesson_ended_at = now session.phase_started_at = None # Beendete Stunde hat keinen aktiven Timer return session def get_phase_index(self, phase: LessonPhase) -> int: """ Gibt den Index der Phase in der Reihenfolge zurueck. Args: phase: Die Phase Returns: Index (0-4) oder -1 fuer NOT_STARTED/ENDED """ if phase in [LessonPhase.NOT_STARTED, LessonPhase.ENDED]: return -1 try: return self.PHASE_ORDER.index(phase) except ValueError: return -1 def get_phases_info(self, session: LessonSession) -> List[Dict]: """ Gibt Informationen ueber alle Phasen fuer die Timeline zurueck. Args: session: Die aktuelle Session Returns: Liste mit Phasen-Informationen """ current_index = self.get_phase_index(session.current_phase) phases_info = [] for i, phase in enumerate(self.PHASE_ORDER): phase_id = phase.value phase_config = LESSON_PHASES.get(phase_id, {}) phases_info.append({ "phase": phase_id, "display_name": phase_config.get("display_name", phase_id.capitalize()), "icon": phase_config.get("icon", "circle"), "duration_minutes": session.phase_durations.get( phase_id, phase_config.get("default_duration_minutes", 10) ), "is_completed": i < current_index if current_index >= 0 else session.current_phase == LessonPhase.ENDED, "is_current": i == current_index, "is_future": i > current_index if current_index >= 0 else session.current_phase == LessonPhase.NOT_STARTED, }) return phases_info def is_lesson_active(self, session: LessonSession) -> bool: """ Prueft ob die Stunde gerade aktiv ist. Args: session: Die Session Returns: True wenn aktiv, False sonst """ return session.current_phase not in [LessonPhase.NOT_STARTED, LessonPhase.ENDED] def is_lesson_ended(self, session: LessonSession) -> bool: """ Prueft ob die Stunde beendet ist. Args: session: Die Session Returns: True wenn beendet, False sonst """ return session.current_phase == LessonPhase.ENDED def get_total_elapsed_seconds(self, session: LessonSession) -> int: """ Berechnet die gesamte verstrichene Zeit seit Stundenbeginn. Args: session: Die Session Returns: Verstrichene Sekunden oder 0 wenn nicht gestartet """ if not session.lesson_started_at: return 0 end_time = session.lesson_ended_at or datetime.utcnow() return int((end_time - session.lesson_started_at).total_seconds())