# ============================================== # Teacher Dashboard - Analytics & Progress Routes # ============================================== from fastapi import APIRouter, HTTPException, Query, Depends, Request from typing import List, Optional, Dict, Any from datetime import datetime, timedelta import logging from teacher_dashboard_models import ( UnitAssignmentStatus, TeacherControlSettings, UnitAssignment, StudentUnitProgress, ClassUnitProgress, MisconceptionReport, ClassAnalyticsSummary, ContentResource, get_current_teacher, get_teacher_database, get_classes_for_teacher, get_students_in_class, REQUIRE_AUTH, ) logger = logging.getLogger(__name__) router = APIRouter(tags=["Teacher Dashboard"]) # Shared in-memory store reference (set from teacher_dashboard_api) _assignments_store: Dict[str, Dict[str, Any]] = {} def set_assignments_store(store: Dict[str, Dict[str, Any]]): """Share the in-memory assignments store from the main module.""" global _assignments_store _assignments_store = store # ============================================== # API Endpoints - Progress & Analytics # ============================================== @router.get("/assignments/{assignment_id}/progress", response_model=ClassUnitProgress) async def get_assignment_progress( assignment_id: str, teacher: Dict[str, Any] = Depends(get_current_teacher) ) -> ClassUnitProgress: """Get detailed progress for an assignment.""" db = await get_teacher_database() assignment = None if db: try: assignment = await db.get_assignment(assignment_id) except Exception as e: logger.error(f"Failed to get assignment: {e}") if not assignment and assignment_id in _assignments_store: assignment = _assignments_store[assignment_id] if not assignment or assignment["teacher_id"] != teacher["user_id"]: raise HTTPException(status_code=404, detail="Assignment not found") students = await get_students_in_class(assignment["class_id"]) student_progress = [] total_completion = 0.0 total_precheck = 0.0 total_postcheck = 0.0 total_time = 0 precheck_count = 0 postcheck_count = 0 started = 0 completed = 0 for student in students: student_id = student.get("id", student.get("student_id")) progress = StudentUnitProgress( student_id=student_id, student_name=student.get("name", f"Student {student_id[:8]}"), status="not_started", completion_rate=0.0, stops_completed=0, total_stops=0, ) if db: try: session_data = await db.get_student_unit_session( student_id=student_id, unit_id=assignment["unit_id"] ) if session_data: progress.session_id = session_data.get("session_id") progress.status = "completed" if session_data.get("completed_at") else "in_progress" progress.completion_rate = session_data.get("completion_rate", 0.0) progress.precheck_score = session_data.get("precheck_score") progress.postcheck_score = session_data.get("postcheck_score") progress.time_spent_minutes = session_data.get("duration_seconds", 0) // 60 progress.last_activity = session_data.get("updated_at") progress.stops_completed = session_data.get("stops_completed", 0) progress.total_stops = session_data.get("total_stops", 0) if progress.precheck_score is not None and progress.postcheck_score is not None: progress.learning_gain = progress.postcheck_score - progress.precheck_score total_completion += progress.completion_rate total_time += progress.time_spent_minutes if progress.precheck_score is not None: total_precheck += progress.precheck_score precheck_count += 1 if progress.postcheck_score is not None: total_postcheck += progress.postcheck_score postcheck_count += 1 if progress.status != "not_started": started += 1 if progress.status == "completed": completed += 1 except Exception as e: logger.error(f"Failed to get student progress: {e}") student_progress.append(progress) total_students = len(students) or 1 return ClassUnitProgress( assignment_id=assignment_id, unit_id=assignment["unit_id"], unit_title=f"Unit {assignment['unit_id']}", class_id=assignment["class_id"], class_name=f"Class {assignment['class_id'][:8]}", total_students=len(students), started_count=started, completed_count=completed, avg_completion_rate=total_completion / total_students, avg_precheck_score=total_precheck / precheck_count if precheck_count > 0 else None, avg_postcheck_score=total_postcheck / postcheck_count if postcheck_count > 0 else None, avg_learning_gain=(total_postcheck / postcheck_count - total_precheck / precheck_count) if precheck_count > 0 and postcheck_count > 0 else None, avg_time_minutes=total_time / started if started > 0 else 0, students=student_progress, ) @router.get("/classes/{class_id}/analytics", response_model=ClassAnalyticsSummary) async def get_class_analytics( class_id: str, teacher: Dict[str, Any] = Depends(get_current_teacher) ) -> ClassAnalyticsSummary: """Get summary analytics for a class.""" db = await get_teacher_database() assignments = [] if db: try: assignments = await db.list_assignments(teacher_id=teacher["user_id"], class_id=class_id) except Exception as e: logger.error(f"Failed to list assignments: {e}") if not assignments: assignments = [ a for a in _assignments_store.values() if a["class_id"] == class_id and a["teacher_id"] == teacher["user_id"] ] total_units = len(assignments) completed_units = sum(1 for a in assignments if a.get("status") == "completed") active_units = sum(1 for a in assignments if a.get("status") == "active") students = await get_students_in_class(class_id) student_scores = {} misconceptions = [] if db: try: for student in students: student_id = student.get("id", student.get("student_id")) analytics = await db.get_student_analytics(student_id) if analytics: student_scores[student_id] = { "name": student.get("name", student_id[:8]), "avg_score": analytics.get("avg_postcheck_score", 0), "total_time": analytics.get("total_time_minutes", 0), } misconceptions_data = await db.get_class_misconceptions(class_id) for m in misconceptions_data: misconceptions.append(MisconceptionReport( concept_id=m["concept_id"], concept_label=m["concept_label"], misconception=m["misconception"], affected_students=m["affected_students"], frequency=m["frequency"], unit_id=m["unit_id"], stop_id=m["stop_id"], )) except Exception as e: logger.error(f"Failed to aggregate analytics: {e}") sorted_students = sorted(student_scores.items(), key=lambda x: x[1]["avg_score"], reverse=True) top_performers = [s[1]["name"] for s in sorted_students[:3]] struggling_students = [s[1]["name"] for s in sorted_students[-3:] if s[1]["avg_score"] < 0.6] total_time = sum(s["total_time"] for s in student_scores.values()) avg_scores = [s["avg_score"] for s in student_scores.values() if s["avg_score"] > 0] avg_completion = sum(avg_scores) / len(avg_scores) if avg_scores else 0 return ClassAnalyticsSummary( class_id=class_id, class_name=f"Klasse {class_id[:8]}", total_units_assigned=total_units, units_completed=completed_units, active_units=active_units, avg_completion_rate=avg_completion, avg_learning_gain=None, total_time_hours=total_time / 60, top_performers=top_performers, struggling_students=struggling_students, common_misconceptions=misconceptions[:5], ) @router.get("/students/{student_id}/progress") async def get_student_progress( student_id: str, teacher: Dict[str, Any] = Depends(get_current_teacher) ) -> Dict[str, Any]: """Get detailed progress for a specific student.""" db = await get_teacher_database() if db: try: progress = await db.get_student_full_progress(student_id) return progress except Exception as e: logger.error(f"Failed to get student progress: {e}") return { "student_id": student_id, "units_attempted": 0, "units_completed": 0, "avg_score": 0.0, "total_time_minutes": 0, "sessions": [], } # ============================================== # API Endpoints - Content Resources # ============================================== @router.get("/assignments/{assignment_id}/resources", response_model=List[ContentResource]) async def get_assignment_resources( assignment_id: str, teacher: Dict[str, Any] = Depends(get_current_teacher), request: Request = None ) -> List[ContentResource]: """Get generated content resources for an assignment.""" db = await get_teacher_database() assignment = None if db: try: assignment = await db.get_assignment(assignment_id) except Exception as e: logger.error(f"Failed to get assignment: {e}") if not assignment and assignment_id in _assignments_store: assignment = _assignments_store[assignment_id] if not assignment or assignment["teacher_id"] != teacher["user_id"]: raise HTTPException(status_code=404, detail="Assignment not found") unit_id = assignment["unit_id"] base_url = str(request.base_url).rstrip("/") if request else "http://localhost:8000" return [ ContentResource(resource_type="h5p", title=f"{unit_id} - H5P Aktivitaeten", url=f"{base_url}/api/units/content/{unit_id}/h5p", generated_at=datetime.utcnow(), unit_id=unit_id), ContentResource(resource_type="worksheet", title=f"{unit_id} - Arbeitsblatt (HTML)", url=f"{base_url}/api/units/content/{unit_id}/worksheet", generated_at=datetime.utcnow(), unit_id=unit_id), ContentResource(resource_type="pdf", title=f"{unit_id} - Arbeitsblatt (PDF)", url=f"{base_url}/api/units/content/{unit_id}/worksheet.pdf", generated_at=datetime.utcnow(), unit_id=unit_id), ] @router.post("/assignments/{assignment_id}/regenerate-content") async def regenerate_content( assignment_id: str, resource_type: str = Query("all", description="h5p, pdf, or all"), teacher: Dict[str, Any] = Depends(get_current_teacher) ) -> Dict[str, Any]: """Trigger regeneration of content resources.""" db = await get_teacher_database() assignment = None if db: try: assignment = await db.get_assignment(assignment_id) except Exception as e: logger.error(f"Failed to get assignment: {e}") if not assignment and assignment_id in _assignments_store: assignment = _assignments_store[assignment_id] if not assignment or assignment["teacher_id"] != teacher["user_id"]: raise HTTPException(status_code=404, detail="Assignment not found") logger.info(f"Content regeneration triggered for {assignment['unit_id']}: {resource_type}") return { "status": "queued", "assignment_id": assignment_id, "unit_id": assignment["unit_id"], "resource_type": resource_type, "message": "Content regeneration has been queued", }