""" Classroom API - Export Routes PDF/HTML export endpoints (Phase 5). """ from datetime import datetime import logging from fastapi import APIRouter, HTTPException from fastapi.responses import HTMLResponse from classroom_engine import LessonPhase from ..services.persistence import ( sessions, init_db_if_needed, DB_ENABLED, SessionLocal, ) logger = logging.getLogger(__name__) router = APIRouter(tags=["Export"]) @router.get("/export/session/{session_id}", response_class=HTMLResponse) async def export_session_html(session_id: str) -> HTMLResponse: """ Exportiert eine Session-Zusammenfassung als druckbares HTML. Kann im Browser ueber Strg+P als PDF gespeichert werden. """ # Session-Daten aus Memory oder DB holen session = sessions.get(session_id) if not session and DB_ENABLED: init_db_if_needed() with SessionLocal() as db: from classroom_engine.repository import AnalyticsRepository 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" ) # HTML generieren aus Summary return HTMLResponse(content=_generate_export_html_from_summary(summary)) if not session: raise HTTPException( status_code=404, detail=f"Session {session_id} not found" ) # HTML aus In-Memory Session generieren return HTMLResponse(content=_generate_export_html_from_session(session)) def _generate_export_html_from_summary(summary) -> str: """Generiert druckbares HTML aus einer SessionSummary.""" phases_html = "" for phase in summary.phase_statistics: diff_class = "on-time" if phase.difference_seconds < -60: diff_class = "under-time" elif phase.difference_seconds > 180: diff_class = "way-over" elif phase.difference_seconds > 60: diff_class = "over-time" phases_html += f""" {phase.display_name} {phase.planned_duration_seconds // 60}:{phase.planned_duration_seconds % 60:02d} {phase.actual_duration_seconds // 60}:{phase.actual_duration_seconds % 60:02d} {phase.difference_formatted} """ return _get_export_html_template( subject=summary.subject, class_id=summary.class_id, topic=summary.topic, date_formatted=summary.date_formatted, total_duration_formatted=summary.total_duration_formatted, phases_completed=summary.phases_completed, total_phases=summary.total_phases, total_overtime_formatted=summary.total_overtime_formatted, phases_html=phases_html, phases_with_overtime=summary.phases_with_overtime, total_overtime_seconds=summary.total_overtime_seconds, reflection_notes=summary.reflection_notes, ) def _generate_export_html_from_session(session) -> str: """Generiert druckbares HTML aus einer In-Memory Session.""" # Phasen-Tabelle generieren phases_html = "" total_overtime = 0 for entry in session.phase_history: phase = entry.get("phase", "") if phase in ["not_started", "ended"]: continue planned = session.phase_durations.get(phase, 0) * 60 actual = entry.get("duration_seconds", 0) or 0 diff = actual - planned if diff > 0: total_overtime += diff diff_class = "on-time" if diff < -60: diff_class = "under-time" elif diff > 180: diff_class = "way-over" elif diff > 60: diff_class = "over-time" phase_names = { "einstieg": "Einstieg", "erarbeitung": "Erarbeitung", "sicherung": "Sicherung", "transfer": "Transfer", "reflexion": "Reflexion", } phases_html += f""" {phase_names.get(phase, phase)} {planned // 60}:{planned % 60:02d} {actual // 60}:{actual % 60:02d} {'+' if diff >= 0 else ''}{diff // 60}:{abs(diff) % 60:02d} """ # Zeiten total_duration = 0 date_str = "--" if session.lesson_started_at: date_str = session.lesson_started_at.strftime("%d.%m.%Y %H:%M") if session.lesson_ended_at: total_duration = int((session.lesson_ended_at - session.lesson_started_at).total_seconds()) total_mins = total_duration // 60 total_secs = total_duration % 60 overtime_mins = total_overtime // 60 overtime_secs = total_overtime % 60 completed_phases = len([e for e in session.phase_history if e.get("ended_at")]) return _get_export_html_template( subject=session.subject, class_id=session.class_id, topic=session.topic, date_formatted=date_str, total_duration_formatted=f"{total_mins:02d}:{total_secs:02d}", phases_completed=completed_phases, total_phases=5, total_overtime_formatted=f"{overtime_mins:02d}:{overtime_secs:02d}", phases_html=phases_html, phases_with_overtime=len([e for e in session.phase_history if e.get("duration_seconds", 0) > session.phase_durations.get(e.get("phase", ""), 0) * 60]), total_overtime_seconds=total_overtime, reflection_notes="", ) def _get_export_html_template( subject: str, class_id: str, topic: str, date_formatted: str, total_duration_formatted: str, phases_completed: int, total_phases: int, total_overtime_formatted: str, phases_html: str, phases_with_overtime: int, total_overtime_seconds: int, reflection_notes: str, ) -> str: """Returns the full HTML template for export.""" return f""" Stundenprotokoll - {subject}

Stundenprotokoll

{subject} - Klasse {class_id}{f" - {topic}" if topic else ""}

Datum
{date_formatted}
Gesamtdauer
{total_duration_formatted}
Phasen abgeschlossen
{phases_completed}/{total_phases}
Overtime gesamt
{total_overtime_formatted}

Phasen-Analyse

{phases_html}
Phase Geplant Tatsaechlich Differenz
{f'''
Overtime-Zusammenfassung:
{phases_with_overtime} von {total_phases} Phasen hatten Overtime (gesamt: {total_overtime_formatted})
''' if total_overtime_seconds > 0 else ''} {f'''

Reflexion

{reflection_notes}

''' if reflection_notes else ''} """