""" Reflection & Analytics Repositories. CRUD-Operationen fuer Lesson-Reflections und Analytics-Abfragen (Phase 5). """ from datetime import datetime from typing import Optional, List, Dict, Any from sqlalchemy.orm import Session as DBSession from .db_models import LessonSessionDB, LessonPhaseEnum, LessonReflectionDB from .analytics import ( LessonReflection, SessionSummary, TeacherAnalytics, AnalyticsCalculator, ) class ReflectionRepository: """Repository fuer LessonReflection CRUD-Operationen.""" def __init__(self, db: DBSession): self.db = db # ==================== CREATE ==================== def create(self, reflection: LessonReflection) -> LessonReflectionDB: """Erstellt eine neue Reflection.""" db_reflection = LessonReflectionDB( id=reflection.reflection_id, session_id=reflection.session_id, teacher_id=reflection.teacher_id, notes=reflection.notes, overall_rating=reflection.overall_rating, what_worked=reflection.what_worked, improvements=reflection.improvements, notes_for_next_lesson=reflection.notes_for_next_lesson, ) self.db.add(db_reflection) self.db.commit() self.db.refresh(db_reflection) return db_reflection # ==================== READ ==================== def get_by_id(self, reflection_id: str) -> Optional[LessonReflectionDB]: """Holt eine Reflection nach ID.""" return self.db.query(LessonReflectionDB).filter( LessonReflectionDB.id == reflection_id ).first() def get_by_session(self, session_id: str) -> Optional[LessonReflectionDB]: """Holt die Reflection einer Session.""" return self.db.query(LessonReflectionDB).filter( LessonReflectionDB.session_id == session_id ).first() def get_by_teacher( self, teacher_id: str, limit: int = 20, offset: int = 0 ) -> List[LessonReflectionDB]: """Holt alle Reflections eines Lehrers.""" return self.db.query(LessonReflectionDB).filter( LessonReflectionDB.teacher_id == teacher_id ).order_by( LessonReflectionDB.created_at.desc() ).offset(offset).limit(limit).all() # ==================== UPDATE ==================== def update(self, reflection: LessonReflection) -> Optional[LessonReflectionDB]: """Aktualisiert eine Reflection.""" db_reflection = self.get_by_id(reflection.reflection_id) if not db_reflection: return None db_reflection.notes = reflection.notes db_reflection.overall_rating = reflection.overall_rating db_reflection.what_worked = reflection.what_worked db_reflection.improvements = reflection.improvements db_reflection.notes_for_next_lesson = reflection.notes_for_next_lesson self.db.commit() self.db.refresh(db_reflection) return db_reflection # ==================== DELETE ==================== def delete(self, reflection_id: str) -> bool: """Loescht eine Reflection.""" db_reflection = self.get_by_id(reflection_id) if not db_reflection: return False self.db.delete(db_reflection) self.db.commit() return True # ==================== CONVERSION ==================== def to_dataclass(self, db_reflection: LessonReflectionDB) -> LessonReflection: """Konvertiert DB-Model zu Dataclass.""" return LessonReflection( reflection_id=db_reflection.id, session_id=db_reflection.session_id, teacher_id=db_reflection.teacher_id, notes=db_reflection.notes or "", overall_rating=db_reflection.overall_rating, what_worked=db_reflection.what_worked or [], improvements=db_reflection.improvements or [], notes_for_next_lesson=db_reflection.notes_for_next_lesson or "", created_at=db_reflection.created_at, updated_at=db_reflection.updated_at, ) class AnalyticsRepository: """Repository fuer Analytics-Abfragen.""" def __init__(self, db: DBSession): self.db = db def get_session_summary(self, session_id: str) -> Optional[SessionSummary]: """ Berechnet die Summary einer abgeschlossenen Session. Args: session_id: ID der Session Returns: SessionSummary oder None wenn Session nicht gefunden """ db_session = self.db.query(LessonSessionDB).filter( LessonSessionDB.id == session_id ).first() if not db_session: return None # Session-Daten zusammenstellen session_data = { "session_id": db_session.id, "teacher_id": db_session.teacher_id, "class_id": db_session.class_id, "subject": db_session.subject, "topic": db_session.topic, "lesson_started_at": db_session.lesson_started_at, "lesson_ended_at": db_session.lesson_ended_at, "phase_durations": db_session.phase_durations or {}, } # Phase History aus DB oder JSON phase_history = db_session.phase_history or [] # Summary berechnen return AnalyticsCalculator.calculate_session_summary( session_data, phase_history ) def get_teacher_analytics( self, teacher_id: str, period_start: Optional[datetime] = None, period_end: Optional[datetime] = None ) -> TeacherAnalytics: """ Berechnet aggregierte Statistiken fuer einen Lehrer. Args: teacher_id: ID des Lehrers period_start: Beginn des Zeitraums (default: 30 Tage zurueck) period_end: Ende des Zeitraums (default: jetzt) Returns: TeacherAnalytics mit aggregierten Statistiken """ from datetime import timedelta if not period_end: period_end = datetime.utcnow() if not period_start: period_start = period_end - timedelta(days=30) # Sessions im Zeitraum abfragen sessions_query = self.db.query(LessonSessionDB).filter( LessonSessionDB.teacher_id == teacher_id, LessonSessionDB.lesson_started_at >= period_start, LessonSessionDB.lesson_started_at <= period_end ).all() # Sessions zu Dictionaries konvertieren sessions_data = [] for db_session in sessions_query: sessions_data.append({ "session_id": db_session.id, "teacher_id": db_session.teacher_id, "class_id": db_session.class_id, "subject": db_session.subject, "topic": db_session.topic, "lesson_started_at": db_session.lesson_started_at, "lesson_ended_at": db_session.lesson_ended_at, "phase_durations": db_session.phase_durations or {}, "phase_history": db_session.phase_history or [], }) return AnalyticsCalculator.calculate_teacher_analytics( sessions_data, period_start, period_end ) def get_phase_duration_trends( self, teacher_id: str, phase: str, limit: int = 20 ) -> List[Dict[str, Any]]: """ Gibt die Dauer-Trends fuer eine bestimmte Phase zurueck. Args: teacher_id: ID des Lehrers phase: Phasen-ID (einstieg, erarbeitung, etc.) limit: Max Anzahl der Datenpunkte Returns: Liste von Datenpunkten [{date, planned, actual, difference}] """ sessions = self.db.query(LessonSessionDB).filter( LessonSessionDB.teacher_id == teacher_id, LessonSessionDB.current_phase == LessonPhaseEnum.ENDED ).order_by( LessonSessionDB.lesson_ended_at.desc() ).limit(limit).all() trends = [] for db_session in sessions: history = db_session.phase_history or [] for entry in history: if entry.get("phase") == phase: planned = (db_session.phase_durations or {}).get(phase, 0) * 60 actual = entry.get("duration_seconds", 0) or 0 trends.append({ "date": db_session.lesson_started_at.isoformat() if db_session.lesson_started_at else None, "session_id": db_session.id, "subject": db_session.subject, "planned_seconds": planned, "actual_seconds": actual, "difference_seconds": actual - planned, }) break return list(reversed(trends)) # Chronologisch sortieren def get_overtime_analysis( self, teacher_id: str, limit: int = 30 ) -> Dict[str, Any]: """ Analysiert Overtime-Muster. Args: teacher_id: ID des Lehrers limit: Anzahl der zu analysierenden Sessions Returns: Dict mit Overtime-Statistiken pro Phase """ sessions = self.db.query(LessonSessionDB).filter( LessonSessionDB.teacher_id == teacher_id, LessonSessionDB.current_phase == LessonPhaseEnum.ENDED ).order_by( LessonSessionDB.lesson_ended_at.desc() ).limit(limit).all() phase_overtime: Dict[str, List[int]] = { "einstieg": [], "erarbeitung": [], "sicherung": [], "transfer": [], "reflexion": [], } for db_session in sessions: history = db_session.phase_history or [] phase_durations = db_session.phase_durations or {} for entry in history: phase = entry.get("phase", "") if phase in phase_overtime: planned = phase_durations.get(phase, 0) * 60 actual = entry.get("duration_seconds", 0) or 0 overtime = max(0, actual - planned) phase_overtime[phase].append(overtime) # Statistiken berechnen result = {} for phase, overtimes in phase_overtime.items(): if overtimes: result[phase] = { "count": len([o for o in overtimes if o > 0]), "total": len(overtimes), "avg_overtime_seconds": sum(overtimes) / len(overtimes), "max_overtime_seconds": max(overtimes), "overtime_percentage": len([o for o in overtimes if o > 0]) / len(overtimes) * 100, } else: result[phase] = { "count": 0, "total": 0, "avg_overtime_seconds": 0, "max_overtime_seconds": 0, "overtime_percentage": 0, } return result