Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
642 lines
22 KiB
Python
642 lines
22 KiB
Python
"""
|
|
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)
|