""" State Engine Rules - Antizipations-Regeln für proaktive Vorschläge. Definiert: - Suggestion: Ein Vorschlag mit Priorität und Aktion - Rule: Eine Regel die auf TeacherContext angewendet wird - RULES: Vordefinierte Regeln (15+) """ from dataclasses import dataclass from enum import Enum from typing import Callable, List, Optional, TYPE_CHECKING if TYPE_CHECKING: from .models import TeacherContext class SuggestionPriority(Enum): """Priorität eines Vorschlags.""" URGENT = 1 # Sofort erforderlich HIGH = 2 # Heute/Diese Woche MEDIUM = 3 # Diese Woche/Bald LOW = 4 # Irgendwann @dataclass class Suggestion: """Ein Vorschlag für den Lehrer.""" id: str title: str description: str action_type: str # "navigate", "create", "remind" action_target: str # URL oder Action-ID priority: SuggestionPriority category: str # "classes", "grades", "communication", etc. icon: str # Material Icon Name estimated_time: int # Minuten def to_dict(self): return { "id": self.id, "title": self.title, "description": self.description, "action_type": self.action_type, "action_target": self.action_target, "priority": self.priority.name, "priority_value": self.priority.value, "category": self.category, "icon": self.icon, "estimated_time": self.estimated_time, } @dataclass class Rule: """Eine Antizipations-Regel.""" id: str name: str condition: Callable[['TeacherContext'], bool] suggestion_generator: Callable[['TeacherContext'], Suggestion] applies_to_phases: List[str] # Leere Liste = alle Phasen def evaluate(self, ctx: 'TeacherContext') -> Optional[Suggestion]: """ Evaluiert die Regel gegen den Kontext. Returns: Suggestion wenn Regel zutrifft, sonst None """ # Prüfe Phasen-Einschränkung if self.applies_to_phases: if ctx.current_phase.value not in self.applies_to_phases: return None # Prüfe Condition try: if not self.condition(ctx): return None except Exception: return None return self.suggestion_generator(ctx) # ============================================================================ # Helper Functions für Regeln # ============================================================================ def _get_parent_meeting_days(ctx: 'TeacherContext') -> int: """Gibt Tage bis zum nächsten Elternabend zurück.""" for e in ctx.upcoming_events: if e.type == "parent_meeting": return e.in_days return 999 def _get_next_exam_days(ctx: 'TeacherContext') -> int: """Gibt Tage bis zur nächsten Klausur zurück.""" for e in ctx.upcoming_events: if e.type == "exam" and e.in_days > 0: return e.in_days return 999 def _get_uncorrected_exams(ctx: 'TeacherContext') -> int: """Gibt Anzahl unkorrigierter Klausuren zurück.""" return ctx.stats.exams_scheduled - ctx.stats.exams_graded # ============================================================================ # Vordefinierte Regeln (15+) # ============================================================================ RULES: List[Rule] = [ # ═══════════════════════════════════════════════════════════════════════ # ONBOARDING / SCHULJAHRESSTART # ═══════════════════════════════════════════════════════════════════════ Rule( id="no_classes", name="Keine Klassen angelegt", condition=lambda ctx: len(ctx.classes) == 0, suggestion_generator=lambda ctx: Suggestion( id="create_first_class", title="Erste Klasse anlegen", description="Lege deine erste Klasse an, um loszulegen.", action_type="navigate", action_target="/studio/school", priority=SuggestionPriority.URGENT, category="classes", icon="group_add", estimated_time=5, ), applies_to_phases=["onboarding", "school_year_start"], ), Rule( id="no_students", name="Klassen ohne Schüler", condition=lambda ctx: len(ctx.classes) > 0 and ctx.total_students == 0, suggestion_generator=lambda ctx: Suggestion( id="add_students", title="Schüler hinzufügen", description=f"Deine {len(ctx.classes)} Klasse(n) haben noch keine Schüler.", action_type="navigate", action_target="/studio/school", priority=SuggestionPriority.HIGH, category="classes", icon="person_add", estimated_time=10, ), applies_to_phases=["school_year_start"], ), Rule( id="consent_missing", name="Einwilligung ausstehend", condition=lambda ctx: not ctx.has_completed_milestone("consent_accept"), suggestion_generator=lambda ctx: Suggestion( id="accept_consent", title="Datenschutz-Einwilligung", description="Bitte akzeptiere die Datenschutzbestimmungen.", action_type="navigate", action_target="/studio/consent", priority=SuggestionPriority.URGENT, category="settings", icon="security", estimated_time=2, ), applies_to_phases=["onboarding"], ), Rule( id="profile_incomplete", name="Profil unvollständig", condition=lambda ctx: not ctx.has_completed_milestone("profile_complete"), suggestion_generator=lambda ctx: Suggestion( id="complete_profile", title="Profil vervollständigen", description="Vervollständige dein Profil für bessere Personalisierung.", action_type="navigate", action_target="/studio/profile", priority=SuggestionPriority.HIGH, category="settings", icon="account_circle", estimated_time=5, ), applies_to_phases=["onboarding", "school_year_start"], ), # ═══════════════════════════════════════════════════════════════════════ # UNTERRICHT # ═══════════════════════════════════════════════════════════════════════ Rule( id="no_learning_units", name="Keine Lerneinheiten", condition=lambda ctx: ( ctx.current_phase.value in ["teaching_setup", "teaching_2"] and ctx.stats.learning_units_created == 0 ), suggestion_generator=lambda ctx: Suggestion( id="create_learning_unit", title="Erste Lerneinheit erstellen", description="Erstelle Lerneinheiten für deine Klassen.", action_type="navigate", action_target="/studio/worksheets", priority=SuggestionPriority.HIGH, category="teaching", icon="auto_stories", estimated_time=15, ), applies_to_phases=["teaching_setup", "teaching_2"], ), Rule( id="few_learning_units", name="Wenige Lerneinheiten", condition=lambda ctx: ( ctx.current_phase.value in ["teaching_setup", "teaching_2"] and 0 < ctx.stats.learning_units_created < 3 ), suggestion_generator=lambda ctx: Suggestion( id="create_more_units", title="Weitere Lerneinheiten erstellen", description=f"Du hast {ctx.stats.learning_units_created} Lerneinheit(en). Erstelle mehr für abwechslungsreichen Unterricht.", action_type="navigate", action_target="/studio/worksheets", priority=SuggestionPriority.MEDIUM, category="teaching", icon="library_add", estimated_time=15, ), applies_to_phases=["teaching_setup", "teaching_2"], ), # ═══════════════════════════════════════════════════════════════════════ # ELTERNKOMMUNIKATION # ═══════════════════════════════════════════════════════════════════════ Rule( id="parent_meeting_upcoming", name="Elternabend steht bevor", condition=lambda ctx: any( e.type == "parent_meeting" and e.in_days <= 14 for e in ctx.upcoming_events ), suggestion_generator=lambda ctx: Suggestion( id="prepare_parent_meeting", title="Elternabend vorbereiten", description=f"In {_get_parent_meeting_days(ctx)} Tagen ist Elternabend.", action_type="navigate", action_target="/studio/letters", priority=SuggestionPriority.HIGH, category="communication", icon="groups", estimated_time=30, ), applies_to_phases=[], # Alle Phasen ), Rule( id="unanswered_parent_messages", name="Unbeantwortete Elternnachrichten", condition=lambda ctx: ctx.stats.unanswered_messages > 0, suggestion_generator=lambda ctx: Suggestion( id="answer_messages", title="Elternnachrichten beantworten", description=f"{ctx.stats.unanswered_messages} Nachricht(en) warten auf Antwort.", action_type="navigate", action_target="/studio/messenger", priority=SuggestionPriority.HIGH if ctx.stats.unanswered_messages > 3 else SuggestionPriority.MEDIUM, category="communication", icon="mail", estimated_time=15, ), applies_to_phases=[], # Alle Phasen ), Rule( id="no_parent_contact", name="Keine Elternkontakte", condition=lambda ctx: ( ctx.total_students > 0 and ctx.stats.parent_messages_count == 0 and ctx.weeks_since_start >= 4 ), suggestion_generator=lambda ctx: Suggestion( id="start_parent_communication", title="Elternkommunikation starten", description="Nimm Kontakt mit den Eltern auf.", action_type="navigate", action_target="/studio/letters", priority=SuggestionPriority.MEDIUM, category="communication", icon="family_restroom", estimated_time=20, ), applies_to_phases=["teaching_setup", "teaching_2"], ), # ═══════════════════════════════════════════════════════════════════════ # LEISTUNG / KLAUSUREN # ═══════════════════════════════════════════════════════════════════════ Rule( id="exam_in_7_days", name="Klausur in 7 Tagen", condition=lambda ctx: any( e.type == "exam" and 0 < e.in_days <= 7 for e in ctx.upcoming_events ), suggestion_generator=lambda ctx: Suggestion( id="prepare_exam", title="Klausur vorbereiten", description=f"Klausur in {_get_next_exam_days(ctx)} Tagen.", action_type="navigate", action_target="/studio/worksheets", priority=SuggestionPriority.URGENT, category="exams", icon="quiz", estimated_time=60, ), applies_to_phases=["performance_1", "performance_2"], ), Rule( id="exam_needs_correction", name="Klausur wartet auf Korrektur", condition=lambda ctx: _get_uncorrected_exams(ctx) > 0, suggestion_generator=lambda ctx: Suggestion( id="correct_exam", title="Klausur korrigieren", description=f"{_get_uncorrected_exams(ctx)} Klausur(en) warten auf Korrektur.", action_type="navigate", action_target="/studio/correction", priority=SuggestionPriority.URGENT, category="exams", icon="grading", estimated_time=120, ), applies_to_phases=["performance_1", "performance_2"], ), Rule( id="no_exams_scheduled", name="Keine Klausuren geplant", condition=lambda ctx: ( ctx.current_phase.value in ["performance_1", "performance_2"] and ctx.stats.exams_scheduled == 0 ), suggestion_generator=lambda ctx: Suggestion( id="schedule_exams", title="Klausuren planen", description="Plane die Klausuren für diese Leistungsphase.", action_type="navigate", action_target="/studio/school", priority=SuggestionPriority.HIGH, category="exams", icon="event", estimated_time=15, ), applies_to_phases=["performance_1", "performance_2"], ), # ═══════════════════════════════════════════════════════════════════════ # NOTEN / ZEUGNISSE # ═══════════════════════════════════════════════════════════════════════ Rule( id="grades_missing", name="Noten fehlen", condition=lambda ctx: ( ctx.current_phase.value in ["semester_end", "year_end"] and not ctx.has_completed_milestone("complete_grades") ), suggestion_generator=lambda ctx: Suggestion( id="enter_grades", title="Noten eintragen", description="Trage alle Noten für die Zeugnisse ein.", action_type="navigate", action_target="/studio/gradebook", priority=SuggestionPriority.URGENT, category="grades", icon="calculate", estimated_time=30, ), applies_to_phases=["semester_end", "year_end"], ), Rule( id="certificates_due", name="Zeugnisse erstellen", condition=lambda ctx: ( ctx.current_phase.value in ["semester_end", "year_end"] and ctx.has_completed_milestone("complete_grades") and not ctx.has_completed_milestone("generate_certificates") ), suggestion_generator=lambda ctx: Suggestion( id="generate_certificates", title="Zeugnisse erstellen", description="Alle Noten sind eingetragen. Erstelle jetzt die Zeugnisse.", action_type="navigate", action_target="/studio/certificates", priority=SuggestionPriority.HIGH, category="certificates", icon="description", estimated_time=45, ), applies_to_phases=["semester_end", "year_end"], ), Rule( id="gradebook_empty", name="Notenbuch leer", condition=lambda ctx: ( ctx.stats.grades_entered == 0 and ctx.weeks_since_start >= 6 ), suggestion_generator=lambda ctx: Suggestion( id="start_gradebook", title="Notenbuch nutzen", description="Beginne mit der Notenverwaltung im Notenbuch.", action_type="navigate", action_target="/studio/gradebook", priority=SuggestionPriority.MEDIUM, category="grades", icon="grade", estimated_time=10, ), applies_to_phases=["teaching_setup", "performance_1", "teaching_2", "performance_2"], ), # ═══════════════════════════════════════════════════════════════════════ # ALLGEMEINE ERINNERUNGEN # ═══════════════════════════════════════════════════════════════════════ Rule( id="long_time_inactive", name="Lange inaktiv", condition=lambda ctx: ctx.days_in_phase > 14 and len(ctx.completed_milestones) == 0, suggestion_generator=lambda ctx: Suggestion( id="get_started", title="Loslegen", description="Du bist schon eine Weile in dieser Phase. Lass uns starten!", action_type="navigate", action_target="/studio", priority=SuggestionPriority.HIGH, category="general", icon="rocket_launch", estimated_time=5, ), applies_to_phases=[], # Alle Phasen ), Rule( id="deadline_approaching", name="Deadline naht", condition=lambda ctx: any( e.type == "deadline" and 0 < e.in_days <= 3 for e in ctx.upcoming_events ), suggestion_generator=lambda ctx: Suggestion( id="check_deadline", title="Deadline beachten", description=f"Eine Deadline in {ctx.get_next_deadline().in_days if ctx.get_next_deadline() else 0} Tagen.", action_type="navigate", action_target="/studio", priority=SuggestionPriority.URGENT, category="general", icon="alarm", estimated_time=5, ), applies_to_phases=[], ), ] def get_rules_for_phase(phase: str) -> List[Rule]: """Gibt alle Regeln für eine bestimmte Phase zurück.""" return [ rule for rule in RULES if not rule.applies_to_phases or phase in rule.applies_to_phases ]