Files
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

359 lines
11 KiB
Python

"""
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"""
<tr>
<td>{phase.display_name}</td>
<td class="center">{phase.planned_duration_seconds // 60}:{phase.planned_duration_seconds % 60:02d}</td>
<td class="center">{phase.actual_duration_seconds // 60}:{phase.actual_duration_seconds % 60:02d}</td>
<td class="center {diff_class}">{phase.difference_formatted}</td>
</tr>
"""
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"""
<tr>
<td>{phase_names.get(phase, phase)}</td>
<td class="center">{planned // 60}:{planned % 60:02d}</td>
<td class="center">{actual // 60}:{actual % 60:02d}</td>
<td class="center {diff_class}">{'+' if diff >= 0 else ''}{diff // 60}:{abs(diff) % 60:02d}</td>
</tr>
"""
# 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"""
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Stundenprotokoll - {subject}</title>
<style>
@page {{
size: A4;
margin: 2cm;
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 11pt;
line-height: 1.5;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}}
.header {{
border-bottom: 2px solid #1a1a2e;
padding-bottom: 20px;
margin-bottom: 20px;
}}
.title {{
font-size: 20pt;
font-weight: bold;
color: #1a1a2e;
margin: 0;
}}
.subtitle {{
color: #666;
font-size: 12pt;
margin-top: 5px;
}}
.meta-grid {{
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-bottom: 25px;
}}
.meta-item {{
background: #f5f5f5;
padding: 12px 15px;
border-radius: 8px;
}}
.meta-label {{
font-size: 10pt;
color: #666;
margin-bottom: 4px;
}}
.meta-value {{
font-size: 14pt;
font-weight: 600;
color: #1a1a2e;
}}
table {{
width: 100%;
border-collapse: collapse;
margin-bottom: 25px;
}}
th, td {{
border: 1px solid #ddd;
padding: 10px 12px;
text-align: left;
}}
th {{
background: #1a1a2e;
color: white;
font-weight: 600;
}}
tr:nth-child(even) {{
background: #f9f9f9;
}}
.center {{
text-align: center;
}}
.on-time {{ color: #3b82f6; }}
.under-time {{ color: #10b981; }}
.over-time {{ color: #f59e0b; }}
.way-over {{ color: #ef4444; }}
.summary-box {{
background: #fff8e1;
border-left: 4px solid #f59e0b;
padding: 15px;
margin-bottom: 25px;
}}
.footer {{
margin-top: 40px;
padding-top: 15px;
border-top: 1px solid #ddd;
font-size: 9pt;
color: #999;
text-align: center;
}}
@media print {{
body {{
padding: 0;
}}
.no-print {{
display: none;
}}
}}
</style>
</head>
<body>
<div class="header">
<h1 class="title">Stundenprotokoll</h1>
<p class="subtitle">{subject} - Klasse {class_id}{f" - {topic}" if topic else ""}</p>
</div>
<div class="meta-grid">
<div class="meta-item">
<div class="meta-label">Datum</div>
<div class="meta-value">{date_formatted}</div>
</div>
<div class="meta-item">
<div class="meta-label">Gesamtdauer</div>
<div class="meta-value">{total_duration_formatted}</div>
</div>
<div class="meta-item">
<div class="meta-label">Phasen abgeschlossen</div>
<div class="meta-value">{phases_completed}/{total_phases}</div>
</div>
<div class="meta-item">
<div class="meta-label">Overtime gesamt</div>
<div class="meta-value">{total_overtime_formatted}</div>
</div>
</div>
<h2>Phasen-Analyse</h2>
<table>
<thead>
<tr>
<th>Phase</th>
<th class="center">Geplant</th>
<th class="center">Tatsaechlich</th>
<th class="center">Differenz</th>
</tr>
</thead>
<tbody>
{phases_html}
</tbody>
</table>
{f'''
<div class="summary-box">
<strong>Overtime-Zusammenfassung:</strong><br>
{phases_with_overtime} von {total_phases} Phasen hatten Overtime
(gesamt: {total_overtime_formatted})
</div>
''' if total_overtime_seconds > 0 else ''}
{f'''
<h2>Reflexion</h2>
<p>{reflection_notes}</p>
''' if reflection_notes else ''}
<div class="footer">
<p>Erstellt mit BreakPilot Classroom Engine | {datetime.utcnow().strftime("%d.%m.%Y %H:%M")}</p>
<p class="no-print" style="margin-top: 10px;">
<button onclick="window.print()" style="padding: 10px 20px; cursor: pointer;">
Als PDF speichern (Strg+P)
</button>
</p>
</div>
</body>
</html>
"""