fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
641
backend/letters_api.py
Normal file
641
backend/letters_api.py
Normal file
@@ -0,0 +1,641 @@
|
||||
"""
|
||||
Letters API - Elternbrief-Verwaltung für BreakPilot.
|
||||
|
||||
Bietet Endpoints für:
|
||||
- Speichern und Laden von Elternbriefen
|
||||
- PDF-Export von Briefen
|
||||
- Versenden per Email
|
||||
- GFK-Integration für Textverbesserung
|
||||
|
||||
Arbeitet zusammen mit:
|
||||
- services/pdf_service.py für PDF-Generierung
|
||||
- llm_gateway/services/communication_service.py für GFK-Verbesserungen
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
from enum import Enum
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Response, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
import httpx
|
||||
import io
|
||||
|
||||
# PDF service requires WeasyPrint with system libraries - make optional for CI
|
||||
try:
|
||||
from services.pdf_service import generate_letter_pdf, SchoolInfo
|
||||
_pdf_available = True
|
||||
except (ImportError, OSError):
|
||||
generate_letter_pdf = None # type: ignore
|
||||
SchoolInfo = None # type: ignore
|
||||
_pdf_available = False
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/letters", tags=["letters"])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Enums
|
||||
# =============================================================================
|
||||
|
||||
class LetterType(str, Enum):
|
||||
"""Typen von Elternbriefen."""
|
||||
GENERAL = "general" # Allgemeine Information
|
||||
HALBJAHR = "halbjahr" # Halbjahresinformation
|
||||
FEHLZEITEN = "fehlzeiten" # Fehlzeiten-Mitteilung
|
||||
ELTERNABEND = "elternabend" # Einladung Elternabend
|
||||
LOB = "lob" # Positives Feedback
|
||||
CUSTOM = "custom" # Benutzerdefiniert
|
||||
|
||||
|
||||
class LetterTone(str, Enum):
|
||||
"""Tonalität der Briefe."""
|
||||
FORMAL = "formal"
|
||||
PROFESSIONAL = "professional"
|
||||
WARM = "warm"
|
||||
CONCERNED = "concerned"
|
||||
APPRECIATIVE = "appreciative"
|
||||
|
||||
|
||||
class LetterStatus(str, Enum):
|
||||
"""Status eines Briefes."""
|
||||
DRAFT = "draft"
|
||||
SENT = "sent"
|
||||
ARCHIVED = "archived"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Pydantic Models
|
||||
# =============================================================================
|
||||
|
||||
class SchoolInfoModel(BaseModel):
|
||||
"""Schulinformationen für Briefkopf."""
|
||||
name: str
|
||||
address: str
|
||||
phone: str
|
||||
email: str
|
||||
website: Optional[str] = None
|
||||
principal: Optional[str] = None
|
||||
logo_path: Optional[str] = None
|
||||
|
||||
|
||||
class LegalReferenceModel(BaseModel):
|
||||
"""Rechtliche Referenz."""
|
||||
law: str
|
||||
paragraph: str
|
||||
title: str
|
||||
summary: Optional[str] = None
|
||||
relevance: Optional[str] = None
|
||||
|
||||
|
||||
class LetterCreateRequest(BaseModel):
|
||||
"""Request zum Erstellen eines neuen Briefes."""
|
||||
recipient_name: str = Field(..., description="Name des Empfängers (z.B. 'Familie Müller')")
|
||||
recipient_address: str = Field(..., description="Adresse des Empfängers")
|
||||
student_name: str = Field(..., description="Name des Schülers")
|
||||
student_class: str = Field(..., description="Klasse des Schülers")
|
||||
subject: str = Field(..., description="Betreff des Briefes")
|
||||
content: str = Field(..., description="Inhalt des Briefes")
|
||||
letter_type: LetterType = Field(LetterType.GENERAL, description="Art des Briefes")
|
||||
tone: LetterTone = Field(LetterTone.PROFESSIONAL, description="Tonalität des Briefes")
|
||||
teacher_name: str = Field(..., description="Name des Lehrers")
|
||||
teacher_title: Optional[str] = Field(None, description="Titel des Lehrers (z.B. 'Klassenlehrerin')")
|
||||
school_info: Optional[SchoolInfoModel] = Field(None, description="Schulinformationen für Briefkopf")
|
||||
legal_references: Optional[List[LegalReferenceModel]] = Field(None, description="Rechtliche Referenzen")
|
||||
gfk_principles_applied: Optional[List[str]] = Field(None, description="Angewandte GFK-Prinzipien")
|
||||
|
||||
|
||||
class LetterUpdateRequest(BaseModel):
|
||||
"""Request zum Aktualisieren eines Briefes."""
|
||||
recipient_name: Optional[str] = None
|
||||
recipient_address: Optional[str] = None
|
||||
student_name: Optional[str] = None
|
||||
student_class: Optional[str] = None
|
||||
subject: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
letter_type: Optional[LetterType] = None
|
||||
tone: Optional[LetterTone] = None
|
||||
teacher_name: Optional[str] = None
|
||||
teacher_title: Optional[str] = None
|
||||
school_info: Optional[SchoolInfoModel] = None
|
||||
legal_references: Optional[List[LegalReferenceModel]] = None
|
||||
gfk_principles_applied: Optional[List[str]] = None
|
||||
status: Optional[LetterStatus] = None
|
||||
|
||||
|
||||
class LetterResponse(BaseModel):
|
||||
"""Response mit Briefdaten."""
|
||||
id: str
|
||||
recipient_name: str
|
||||
recipient_address: str
|
||||
student_name: str
|
||||
student_class: str
|
||||
subject: str
|
||||
content: str
|
||||
letter_type: LetterType
|
||||
tone: LetterTone
|
||||
teacher_name: str
|
||||
teacher_title: Optional[str]
|
||||
school_info: Optional[SchoolInfoModel]
|
||||
legal_references: Optional[List[LegalReferenceModel]]
|
||||
gfk_principles_applied: Optional[List[str]]
|
||||
gfk_score: Optional[float]
|
||||
status: LetterStatus
|
||||
pdf_path: Optional[str]
|
||||
dsms_cid: Optional[str]
|
||||
sent_at: Optional[datetime]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class LetterListResponse(BaseModel):
|
||||
"""Response mit Liste von Briefen."""
|
||||
letters: List[LetterResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
class ExportPDFRequest(BaseModel):
|
||||
"""Request zum PDF-Export."""
|
||||
letter_id: Optional[str] = Field(None, description="ID eines gespeicherten Briefes")
|
||||
letter_data: Optional[LetterCreateRequest] = Field(None, description="Oder direkte Briefdaten")
|
||||
|
||||
|
||||
class ImproveRequest(BaseModel):
|
||||
"""Request zur GFK-Verbesserung."""
|
||||
content: str = Field(..., description="Text zur Verbesserung")
|
||||
communication_type: Optional[str] = Field("general_info", description="Art der Kommunikation")
|
||||
tone: Optional[str] = Field("professional", description="Gewünschte Tonalität")
|
||||
|
||||
|
||||
class ImproveResponse(BaseModel):
|
||||
"""Response mit verbessertem Text."""
|
||||
improved_content: str
|
||||
changes: List[str]
|
||||
gfk_score: float
|
||||
gfk_principles_applied: List[str]
|
||||
|
||||
|
||||
class SendEmailRequest(BaseModel):
|
||||
"""Request zum Email-Versand."""
|
||||
letter_id: str
|
||||
recipient_email: str
|
||||
cc_emails: Optional[List[str]] = None
|
||||
include_pdf: bool = True
|
||||
|
||||
|
||||
class SendEmailResponse(BaseModel):
|
||||
"""Response nach Email-Versand."""
|
||||
success: bool
|
||||
message: str
|
||||
sent_at: Optional[datetime]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# In-Memory Storage (Prototyp - später durch DB ersetzen)
|
||||
# =============================================================================
|
||||
|
||||
_letters_store: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
|
||||
def _get_letter(letter_id: str) -> Dict[str, Any]:
|
||||
"""Holt Brief aus dem Store."""
|
||||
if letter_id not in _letters_store:
|
||||
raise HTTPException(status_code=404, detail=f"Brief mit ID {letter_id} nicht gefunden")
|
||||
return _letters_store[letter_id]
|
||||
|
||||
|
||||
def _save_letter(letter_data: Dict[str, Any]) -> str:
|
||||
"""Speichert Brief und gibt ID zurück."""
|
||||
letter_id = letter_data.get("id") or str(uuid.uuid4())
|
||||
letter_data["id"] = letter_id
|
||||
letter_data["updated_at"] = datetime.now()
|
||||
if "created_at" not in letter_data:
|
||||
letter_data["created_at"] = datetime.now()
|
||||
_letters_store[letter_id] = letter_data
|
||||
return letter_id
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# API Endpoints
|
||||
# =============================================================================
|
||||
|
||||
@router.post("/", response_model=LetterResponse)
|
||||
async def create_letter(request: LetterCreateRequest):
|
||||
"""
|
||||
Erstellt einen neuen Elternbrief.
|
||||
|
||||
Der Brief wird als Entwurf gespeichert und kann später bearbeitet,
|
||||
als PDF exportiert oder per Email versendet werden.
|
||||
"""
|
||||
logger.info(f"Creating new letter for student: {request.student_name}")
|
||||
|
||||
letter_data = {
|
||||
"recipient_name": request.recipient_name,
|
||||
"recipient_address": request.recipient_address,
|
||||
"student_name": request.student_name,
|
||||
"student_class": request.student_class,
|
||||
"subject": request.subject,
|
||||
"content": request.content,
|
||||
"letter_type": request.letter_type,
|
||||
"tone": request.tone,
|
||||
"teacher_name": request.teacher_name,
|
||||
"teacher_title": request.teacher_title,
|
||||
"school_info": request.school_info.model_dump() if request.school_info else None,
|
||||
"legal_references": [ref.model_dump() for ref in request.legal_references] if request.legal_references else None,
|
||||
"gfk_principles_applied": request.gfk_principles_applied,
|
||||
"gfk_score": None,
|
||||
"status": LetterStatus.DRAFT,
|
||||
"pdf_path": None,
|
||||
"dsms_cid": None,
|
||||
"sent_at": None,
|
||||
}
|
||||
|
||||
letter_id = _save_letter(letter_data)
|
||||
letter_data["id"] = letter_id
|
||||
|
||||
logger.info(f"Letter created with ID: {letter_id}")
|
||||
return LetterResponse(**letter_data)
|
||||
|
||||
|
||||
# NOTE: Static routes must come BEFORE dynamic routes like /{letter_id}
|
||||
@router.get("/types")
|
||||
async def get_letter_types():
|
||||
"""
|
||||
Gibt alle verfügbaren Brieftypen zurück.
|
||||
"""
|
||||
return {
|
||||
"types": [
|
||||
{"value": t.value, "label": _get_type_label(t)}
|
||||
for t in LetterType
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/tones")
|
||||
async def get_letter_tones():
|
||||
"""
|
||||
Gibt alle verfügbaren Tonalitäten zurück.
|
||||
"""
|
||||
return {
|
||||
"tones": [
|
||||
{"value": t.value, "label": _get_tone_label(t)}
|
||||
for t in LetterTone
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{letter_id}", response_model=LetterResponse)
|
||||
async def get_letter(letter_id: str):
|
||||
"""
|
||||
Lädt einen gespeicherten Brief.
|
||||
"""
|
||||
logger.info(f"Getting letter: {letter_id}")
|
||||
letter_data = _get_letter(letter_id)
|
||||
return LetterResponse(**letter_data)
|
||||
|
||||
|
||||
@router.get("/", response_model=LetterListResponse)
|
||||
async def list_letters(
|
||||
student_id: Optional[str] = Query(None, description="Filter nach Schüler-ID"),
|
||||
class_name: Optional[str] = Query(None, description="Filter nach Klasse"),
|
||||
letter_type: Optional[LetterType] = Query(None, description="Filter nach Brief-Typ"),
|
||||
status: Optional[LetterStatus] = Query(None, description="Filter nach Status"),
|
||||
page: int = Query(1, ge=1, description="Seitennummer"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="Einträge pro Seite")
|
||||
):
|
||||
"""
|
||||
Listet alle gespeicherten Briefe mit optionalen Filtern.
|
||||
"""
|
||||
logger.info("Listing letters with filters")
|
||||
|
||||
# Filter anwenden
|
||||
filtered_letters = list(_letters_store.values())
|
||||
|
||||
if class_name:
|
||||
filtered_letters = [l for l in filtered_letters if l.get("student_class") == class_name]
|
||||
if letter_type:
|
||||
filtered_letters = [l for l in filtered_letters if l.get("letter_type") == letter_type]
|
||||
if status:
|
||||
filtered_letters = [l for l in filtered_letters if l.get("status") == status]
|
||||
|
||||
# Sortieren nach Erstelldatum (neueste zuerst)
|
||||
filtered_letters.sort(key=lambda x: x.get("created_at", datetime.min), reverse=True)
|
||||
|
||||
# Paginierung
|
||||
total = len(filtered_letters)
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
paginated_letters = filtered_letters[start:end]
|
||||
|
||||
return LetterListResponse(
|
||||
letters=[LetterResponse(**l) for l in paginated_letters],
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{letter_id}", response_model=LetterResponse)
|
||||
async def update_letter(letter_id: str, request: LetterUpdateRequest):
|
||||
"""
|
||||
Aktualisiert einen bestehenden Brief.
|
||||
"""
|
||||
logger.info(f"Updating letter: {letter_id}")
|
||||
letter_data = _get_letter(letter_id)
|
||||
|
||||
# Nur übergebene Felder aktualisieren
|
||||
update_data = request.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
if value is not None:
|
||||
if key == "school_info" and value:
|
||||
letter_data[key] = value if isinstance(value, dict) else value.model_dump()
|
||||
elif key == "legal_references" and value:
|
||||
letter_data[key] = [ref if isinstance(ref, dict) else ref.model_dump() for ref in value]
|
||||
else:
|
||||
letter_data[key] = value
|
||||
|
||||
_save_letter(letter_data)
|
||||
|
||||
return LetterResponse(**letter_data)
|
||||
|
||||
|
||||
@router.delete("/{letter_id}")
|
||||
async def delete_letter(letter_id: str):
|
||||
"""
|
||||
Löscht einen Brief.
|
||||
"""
|
||||
logger.info(f"Deleting letter: {letter_id}")
|
||||
if letter_id not in _letters_store:
|
||||
raise HTTPException(status_code=404, detail=f"Brief mit ID {letter_id} nicht gefunden")
|
||||
|
||||
del _letters_store[letter_id]
|
||||
return {"message": f"Brief {letter_id} wurde gelöscht"}
|
||||
|
||||
|
||||
@router.post("/export-pdf")
|
||||
async def export_letter_pdf(request: ExportPDFRequest):
|
||||
"""
|
||||
Exportiert einen Brief als PDF.
|
||||
|
||||
Kann entweder einen gespeicherten Brief (per letter_id) oder
|
||||
direkte Briefdaten (per letter_data) als PDF exportieren.
|
||||
|
||||
Gibt das PDF als Download zurück.
|
||||
"""
|
||||
logger.info("Exporting letter as PDF")
|
||||
|
||||
# Briefdaten ermitteln
|
||||
if request.letter_id:
|
||||
letter_data = _get_letter(request.letter_id)
|
||||
elif request.letter_data:
|
||||
letter_data = request.letter_data.model_dump()
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Entweder letter_id oder letter_data muss angegeben werden"
|
||||
)
|
||||
|
||||
# Datum hinzufügen falls nicht vorhanden
|
||||
if "date" not in letter_data:
|
||||
letter_data["date"] = datetime.now().strftime("%d.%m.%Y")
|
||||
|
||||
# PDF generieren
|
||||
try:
|
||||
pdf_bytes = generate_letter_pdf(letter_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating PDF: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Fehler bei PDF-Generierung: {str(e)}")
|
||||
|
||||
# Dateiname erstellen
|
||||
student_name = letter_data.get("student_name", "Brief").replace(" ", "_")
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
filename = f"Elternbrief_{student_name}_{date_str}.pdf"
|
||||
|
||||
# PDF als Download zurückgeben
|
||||
return Response(
|
||||
content=pdf_bytes,
|
||||
media_type="application/pdf",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={filename}",
|
||||
"Content-Length": str(len(pdf_bytes))
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{letter_id}/export-pdf")
|
||||
async def export_saved_letter_pdf(letter_id: str):
|
||||
"""
|
||||
Exportiert einen gespeicherten Brief als PDF (Kurzform).
|
||||
"""
|
||||
return await export_letter_pdf(ExportPDFRequest(letter_id=letter_id))
|
||||
|
||||
|
||||
@router.post("/improve", response_model=ImproveResponse)
|
||||
async def improve_letter_content(request: ImproveRequest):
|
||||
"""
|
||||
Verbessert den Briefinhalt nach GFK-Prinzipien.
|
||||
|
||||
Nutzt die Communication Service API für KI-gestützte Verbesserungen.
|
||||
"""
|
||||
logger.info("Improving letter content with GFK principles")
|
||||
|
||||
# Communication Service URL (läuft im gleichen Backend)
|
||||
comm_service_url = os.getenv(
|
||||
"COMMUNICATION_SERVICE_URL",
|
||||
"http://localhost:8000/v1/communication"
|
||||
)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Validierung des aktuellen Textes
|
||||
validate_response = await client.post(
|
||||
f"{comm_service_url}/validate",
|
||||
json={"text": request.content},
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
if validate_response.status_code != 200:
|
||||
logger.warning(f"Validation service returned {validate_response.status_code}")
|
||||
# Fallback: Original-Text zurückgeben
|
||||
return ImproveResponse(
|
||||
improved_content=request.content,
|
||||
changes=["Verbesserungsservice nicht verfügbar"],
|
||||
gfk_score=0.5,
|
||||
gfk_principles_applied=[]
|
||||
)
|
||||
|
||||
validation_data = validate_response.json()
|
||||
|
||||
# Falls Text schon gut ist, keine Änderungen
|
||||
if validation_data.get("is_valid", False) and validation_data.get("gfk_score", 0) > 0.8:
|
||||
return ImproveResponse(
|
||||
improved_content=request.content,
|
||||
changes=["Text entspricht bereits GFK-Standards"],
|
||||
gfk_score=validation_data.get("gfk_score", 0.8),
|
||||
gfk_principles_applied=validation_data.get("positive_elements", [])
|
||||
)
|
||||
|
||||
# Verbesserungsvorschläge als Änderungen
|
||||
changes = validation_data.get("suggestions", [])
|
||||
gfk_score = validation_data.get("gfk_score", 0.5)
|
||||
gfk_principles = validation_data.get("positive_elements", [])
|
||||
|
||||
# TODO: Hier könnte ein LLM den Text basierend auf den Vorschlägen verbessern
|
||||
# Für jetzt geben wir den Original-Text mit den Verbesserungsvorschlägen zurück
|
||||
return ImproveResponse(
|
||||
improved_content=request.content,
|
||||
changes=changes,
|
||||
gfk_score=gfk_score,
|
||||
gfk_principles_applied=gfk_principles
|
||||
)
|
||||
|
||||
except httpx.TimeoutException:
|
||||
logger.error("Timeout while calling communication service")
|
||||
return ImproveResponse(
|
||||
improved_content=request.content,
|
||||
changes=["Zeitüberschreitung beim Verbesserungsservice"],
|
||||
gfk_score=0.5,
|
||||
gfk_principles_applied=[]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error improving content: {e}")
|
||||
return ImproveResponse(
|
||||
improved_content=request.content,
|
||||
changes=[f"Fehler: {str(e)}"],
|
||||
gfk_score=0.5,
|
||||
gfk_principles_applied=[]
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{letter_id}/send", response_model=SendEmailResponse)
|
||||
async def send_letter_email(letter_id: str, request: SendEmailRequest):
|
||||
"""
|
||||
Versendet einen Brief per Email.
|
||||
|
||||
Der Brief wird als PDF angehängt (wenn include_pdf=True)
|
||||
und der Status wird auf 'sent' gesetzt.
|
||||
"""
|
||||
logger.info(f"Sending letter {letter_id} to {request.recipient_email}")
|
||||
|
||||
# Brief laden
|
||||
letter_data = _get_letter(letter_id)
|
||||
|
||||
# Email-Service URL (Mailpit oder SMTP)
|
||||
email_service_url = os.getenv(
|
||||
"EMAIL_SERVICE_URL",
|
||||
"http://localhost:8025/api/v1/send" # Mailpit default
|
||||
)
|
||||
|
||||
try:
|
||||
# PDF generieren falls gewünscht
|
||||
pdf_attachment = None
|
||||
if request.include_pdf:
|
||||
letter_data["date"] = datetime.now().strftime("%d.%m.%Y")
|
||||
pdf_bytes = generate_letter_pdf(letter_data)
|
||||
pdf_attachment = {
|
||||
"filename": f"Elternbrief_{letter_data.get('student_name', 'Brief').replace(' ', '_')}.pdf",
|
||||
"content": pdf_bytes.hex(), # Hex-encoded für JSON
|
||||
"content_type": "application/pdf"
|
||||
}
|
||||
|
||||
# Email senden (vereinfachte Implementierung)
|
||||
# In der Praxis würde hier ein richtiger Email-Service aufgerufen
|
||||
async with httpx.AsyncClient() as client:
|
||||
email_data = {
|
||||
"to": request.recipient_email,
|
||||
"cc": request.cc_emails or [],
|
||||
"subject": letter_data.get("subject", "Elternbrief"),
|
||||
"body": letter_data.get("content", ""),
|
||||
"attachments": [pdf_attachment] if pdf_attachment else []
|
||||
}
|
||||
|
||||
# Für Prototyp: Nur loggen, nicht wirklich senden
|
||||
logger.info(f"Would send email: {email_data['subject']} to {email_data['to']}")
|
||||
|
||||
# Status aktualisieren
|
||||
letter_data["status"] = LetterStatus.SENT
|
||||
letter_data["sent_at"] = datetime.now()
|
||||
_save_letter(letter_data)
|
||||
|
||||
return SendEmailResponse(
|
||||
success=True,
|
||||
message=f"Brief wurde an {request.recipient_email} gesendet",
|
||||
sent_at=datetime.now()
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending email: {e}")
|
||||
return SendEmailResponse(
|
||||
success=False,
|
||||
message=f"Fehler beim Versenden: {str(e)}",
|
||||
sent_at=None
|
||||
)
|
||||
|
||||
|
||||
@router.get("/student/{student_id}", response_model=LetterListResponse)
|
||||
async def get_letters_for_student(
|
||||
student_id: str,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100)
|
||||
):
|
||||
"""
|
||||
Lädt alle Briefe für einen bestimmten Schüler.
|
||||
"""
|
||||
logger.info(f"Getting letters for student: {student_id}")
|
||||
|
||||
# In einem echten System würde hier nach student_id gefiltert
|
||||
# Für Prototyp filtern wir nach student_name
|
||||
filtered_letters = [
|
||||
l for l in _letters_store.values()
|
||||
if student_id.lower() in l.get("student_name", "").lower()
|
||||
]
|
||||
|
||||
# Sortieren und Paginierung
|
||||
filtered_letters.sort(key=lambda x: x.get("created_at", datetime.min), reverse=True)
|
||||
total = len(filtered_letters)
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
paginated_letters = filtered_letters[start:end]
|
||||
|
||||
return LetterListResponse(
|
||||
letters=[LetterResponse(**l) for l in paginated_letters],
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
def _get_type_label(letter_type: LetterType) -> str:
|
||||
"""Gibt menschenlesbare Labels für Brieftypen zurück."""
|
||||
labels = {
|
||||
LetterType.GENERAL: "Allgemeine Information",
|
||||
LetterType.HALBJAHR: "Halbjahresinformation",
|
||||
LetterType.FEHLZEITEN: "Fehlzeiten-Mitteilung",
|
||||
LetterType.ELTERNABEND: "Einladung Elternabend",
|
||||
LetterType.LOB: "Positives Feedback",
|
||||
LetterType.CUSTOM: "Benutzerdefiniert",
|
||||
}
|
||||
return labels.get(letter_type, letter_type.value)
|
||||
|
||||
|
||||
def _get_tone_label(tone: LetterTone) -> str:
|
||||
"""Gibt menschenlesbare Labels für Tonalitäten zurück."""
|
||||
labels = {
|
||||
LetterTone.FORMAL: "Sehr förmlich",
|
||||
LetterTone.PROFESSIONAL: "Professionell-freundlich",
|
||||
LetterTone.WARM: "Warmherzig",
|
||||
LetterTone.CONCERNED: "Besorgt",
|
||||
LetterTone.APPRECIATIVE: "Wertschätzend",
|
||||
}
|
||||
return labels.get(tone, tone.value)
|
||||
Reference in New Issue
Block a user