""" Classroom API - Analytics & Reflections Endpoints. Endpoints fuer Session-Analytics und Post-Lesson Reflections (Phase 5). """ from uuid import uuid4 from typing import Dict, List, Optional, Any from datetime import datetime, timedelta import logging from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel, Field from .shared import init_db_if_needed, DB_ENABLED, logger try: from classroom_engine.database import SessionLocal from classroom_engine.repository import AnalyticsRepository, ReflectionRepository from classroom_engine.analytics import LessonReflection except ImportError: pass router = APIRouter(tags=["Analytics"]) # === Pydantic Models === class SessionSummaryResponse(BaseModel): """Response fuer Session-Summary.""" session_id: str teacher_id: str class_id: str subject: str topic: Optional[str] date: Optional[str] date_formatted: str total_duration_seconds: int total_duration_formatted: str planned_duration_seconds: int planned_duration_formatted: str phases_completed: int total_phases: int completion_percentage: int phase_statistics: List[Dict[str, Any]] total_overtime_seconds: int total_overtime_formatted: str phases_with_overtime: int total_pause_count: int total_pause_seconds: int reflection_notes: str = "" reflection_rating: Optional[int] = None key_learnings: List[str] = [] class TeacherAnalyticsResponse(BaseModel): """Response fuer Lehrer-Analytics.""" teacher_id: str period_start: Optional[str] period_end: Optional[str] total_sessions: int completed_sessions: int total_teaching_minutes: int total_teaching_hours: float avg_phase_durations: Dict[str, int] sessions_with_overtime: int overtime_percentage: int avg_overtime_seconds: int avg_overtime_formatted: str most_overtime_phase: Optional[str] avg_pause_count: float avg_pause_duration_seconds: int subjects_taught: Dict[str, int] classes_taught: Dict[str, int] class ReflectionCreate(BaseModel): """Request-Body fuer Reflection-Erstellung.""" session_id: str teacher_id: str notes: str = "" overall_rating: Optional[int] = Field(None, ge=1, le=5) what_worked: List[str] = [] improvements: List[str] = [] notes_for_next_lesson: str = "" class ReflectionUpdate(BaseModel): """Request-Body fuer Reflection-Update.""" notes: Optional[str] = None overall_rating: Optional[int] = Field(None, ge=1, le=5) what_worked: Optional[List[str]] = None improvements: Optional[List[str]] = None notes_for_next_lesson: Optional[str] = None class ReflectionResponse(BaseModel): """Response fuer eine einzelne Reflection.""" reflection_id: str session_id: str teacher_id: str notes: str overall_rating: Optional[int] what_worked: List[str] improvements: List[str] notes_for_next_lesson: str created_at: Optional[str] updated_at: Optional[str] class ReflectionListResponse(BaseModel): """Response fuer Reflection-Liste.""" reflections: List[ReflectionResponse] total: int # === Analytics Endpoints === @router.get("/analytics/session/{session_id}") async def get_session_summary(session_id: str) -> SessionSummaryResponse: """Gibt die Analytics-Zusammenfassung einer Session zurueck.""" if not DB_ENABLED: raise HTTPException(status_code=503, detail="Database not available") init_db_if_needed() with SessionLocal() as db: repo = AnalyticsRepository(db) summary = repo.get_session_summary(session_id) if not summary: raise HTTPException(status_code=404, detail=f"Session {session_id} not found") return SessionSummaryResponse(**summary.to_dict()) @router.get("/analytics/teacher/{teacher_id}") async def get_teacher_analytics( teacher_id: str, days: int = Query(30, ge=1, le=365) ) -> TeacherAnalyticsResponse: """Gibt aggregierte Analytics fuer einen Lehrer zurueck.""" if not DB_ENABLED: raise HTTPException(status_code=503, detail="Database not available") init_db_if_needed() period_end = datetime.utcnow() period_start = period_end - timedelta(days=days) with SessionLocal() as db: repo = AnalyticsRepository(db) analytics = repo.get_teacher_analytics(teacher_id, period_start, period_end) return TeacherAnalyticsResponse(**analytics.to_dict()) @router.get("/analytics/phase-trends/{teacher_id}/{phase}") async def get_phase_trends( teacher_id: str, phase: str, limit: int = Query(20, ge=1, le=100) ) -> Dict[str, Any]: """Gibt die Dauer-Trends fuer eine Phase zurueck.""" if phase not in ["einstieg", "erarbeitung", "sicherung", "transfer", "reflexion"]: raise HTTPException(status_code=400, detail=f"Invalid phase: {phase}") if not DB_ENABLED: raise HTTPException(status_code=503, detail="Database not available") init_db_if_needed() with SessionLocal() as db: repo = AnalyticsRepository(db) trends = repo.get_phase_duration_trends(teacher_id, phase, limit) return { "teacher_id": teacher_id, "phase": phase, "data_points": trends, "count": len(trends) } @router.get("/analytics/overtime/{teacher_id}") async def get_overtime_analysis( teacher_id: str, limit: int = Query(30, ge=1, le=100) ) -> Dict[str, Any]: """Analysiert Overtime-Muster nach Phase.""" if not DB_ENABLED: raise HTTPException(status_code=503, detail="Database not available") init_db_if_needed() with SessionLocal() as db: repo = AnalyticsRepository(db) analysis = repo.get_overtime_analysis(teacher_id, limit) return { "teacher_id": teacher_id, "sessions_analyzed": limit, "phases": analysis } # === Reflection Endpoints === @router.post("/reflections", status_code=201) async def create_reflection(data: ReflectionCreate) -> ReflectionResponse: """Erstellt eine Post-Lesson Reflection.""" if not DB_ENABLED: raise HTTPException(status_code=503, detail="Database not available") init_db_if_needed() with SessionLocal() as db: repo = ReflectionRepository(db) existing = repo.get_by_session(data.session_id) if existing: raise HTTPException( status_code=409, detail=f"Reflection for session {data.session_id} already exists" ) reflection = LessonReflection( reflection_id=str(uuid4()), session_id=data.session_id, teacher_id=data.teacher_id, notes=data.notes, overall_rating=data.overall_rating, what_worked=data.what_worked, improvements=data.improvements, notes_for_next_lesson=data.notes_for_next_lesson, created_at=datetime.utcnow(), ) db_reflection = repo.create(reflection) result = repo.to_dataclass(db_reflection) return ReflectionResponse(**result.to_dict()) @router.get("/reflections/session/{session_id}") async def get_reflection_by_session(session_id: str) -> ReflectionResponse: """Holt die Reflection einer Session.""" if not DB_ENABLED: raise HTTPException(status_code=503, detail="Database not available") init_db_if_needed() with SessionLocal() as db: repo = ReflectionRepository(db) db_reflection = repo.get_by_session(session_id) if not db_reflection: raise HTTPException(status_code=404, detail=f"No reflection for session {session_id}") result = repo.to_dataclass(db_reflection) return ReflectionResponse(**result.to_dict()) @router.get("/reflections/teacher/{teacher_id}") async def get_reflections_by_teacher( teacher_id: str, limit: int = Query(20, ge=1, le=100), offset: int = Query(0, ge=0) ) -> ReflectionListResponse: """Holt alle Reflections eines Lehrers.""" if not DB_ENABLED: raise HTTPException(status_code=503, detail="Database not available") init_db_if_needed() with SessionLocal() as db: repo = ReflectionRepository(db) db_reflections = repo.get_by_teacher(teacher_id, limit, offset) reflections = [] for db_ref in db_reflections: result = repo.to_dataclass(db_ref) reflections.append(ReflectionResponse(**result.to_dict())) total = repo.count_by_teacher(teacher_id) return ReflectionListResponse(reflections=reflections, total=total) @router.put("/reflections/{reflection_id}") async def update_reflection(reflection_id: str, data: ReflectionUpdate) -> ReflectionResponse: """Aktualisiert eine Reflection.""" if not DB_ENABLED: raise HTTPException(status_code=503, detail="Database not available") init_db_if_needed() with SessionLocal() as db: repo = ReflectionRepository(db) db_reflection = repo.get_by_id(reflection_id) if not db_reflection: raise HTTPException(status_code=404, detail=f"Reflection {reflection_id} not found") reflection = repo.to_dataclass(db_reflection) if data.notes is not None: reflection.notes = data.notes if data.overall_rating is not None: reflection.overall_rating = data.overall_rating if data.what_worked is not None: reflection.what_worked = data.what_worked if data.improvements is not None: reflection.improvements = data.improvements if data.notes_for_next_lesson is not None: reflection.notes_for_next_lesson = data.notes_for_next_lesson reflection.updated_at = datetime.utcnow() db_reflection = repo.update(reflection) result = repo.to_dataclass(db_reflection) return ReflectionResponse(**result.to_dict()) @router.delete("/reflections/{reflection_id}") async def delete_reflection(reflection_id: str) -> Dict[str, str]: """Loescht eine Reflection.""" if not DB_ENABLED: raise HTTPException(status_code=503, detail="Database not available") init_db_if_needed() with SessionLocal() as db: repo = ReflectionRepository(db) db_reflection = repo.get_by_id(reflection_id) if not db_reflection: raise HTTPException(status_code=404, detail=f"Reflection {reflection_id} not found") repo.delete(reflection_id) return {"status": "deleted", "reflection_id": reflection_id}