Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook) and split all 44 files exceeding 500 LOC into domain-focused modules: - consent-service (Go): models, handlers, services, database splits - backend-core (Python): security_api, rbac_api, pdf_service, auth splits - admin-core (TypeScript): 5 page.tsx + sidebar extractions - pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits - voice-service (Python): enhanced_task_orchestrator split Result: 0 violations, 36 exempted (pipeline, tests, pure-data files). Go build verified clean. No behavior changes — pure structural splits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
520 lines
13 KiB
Python
520 lines
13 KiB
Python
"""
|
|
PDF Templates - Inline HTML-Templates und CSS fuer PDF-Generierung.
|
|
|
|
Fallback-Templates die verwendet werden wenn keine externen HTML-Dateien
|
|
im templates/pdf/ Verzeichnis vorhanden sind.
|
|
"""
|
|
|
|
|
|
def get_base_css() -> str:
|
|
"""Basis-CSS fuer alle PDFs (A4, Typografie, Komponenten-Styles)."""
|
|
return """
|
|
@page {
|
|
size: A4;
|
|
margin: 2cm 2.5cm;
|
|
@top-right {
|
|
content: counter(page) " / " counter(pages);
|
|
font-size: 9pt;
|
|
color: #666;
|
|
}
|
|
}
|
|
|
|
body {
|
|
font-family: 'DejaVu Sans', 'Liberation Sans', Arial, sans-serif;
|
|
font-size: 11pt;
|
|
line-height: 1.5;
|
|
color: #333;
|
|
}
|
|
|
|
h1, h2, h3 {
|
|
font-weight: bold;
|
|
margin-top: 1em;
|
|
margin-bottom: 0.5em;
|
|
}
|
|
|
|
h1 { font-size: 16pt; }
|
|
h2 { font-size: 14pt; }
|
|
h3 { font-size: 12pt; }
|
|
|
|
.header {
|
|
border-bottom: 2px solid #2c3e50;
|
|
padding-bottom: 15px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.school-name {
|
|
font-size: 18pt;
|
|
font-weight: bold;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.school-info {
|
|
font-size: 9pt;
|
|
color: #666;
|
|
}
|
|
|
|
.letter-date {
|
|
text-align: right;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.recipient {
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.subject {
|
|
font-weight: bold;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.content {
|
|
text-align: justify;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.signature {
|
|
margin-top: 40px;
|
|
}
|
|
|
|
.legal-references {
|
|
font-size: 9pt;
|
|
color: #666;
|
|
border-top: 1px solid #ddd;
|
|
margin-top: 30px;
|
|
padding-top: 10px;
|
|
}
|
|
|
|
.gfk-badge {
|
|
display: inline-block;
|
|
background: #e8f5e9;
|
|
color: #27ae60;
|
|
font-size: 8pt;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
margin-right: 5px;
|
|
}
|
|
|
|
/* Zeugnis-Styles */
|
|
.certificate-header {
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.certificate-title {
|
|
font-size: 20pt;
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.student-info {
|
|
margin-bottom: 20px;
|
|
padding: 15px;
|
|
background: #f9f9f9;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.grades-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.grades-table th,
|
|
.grades-table td {
|
|
border: 1px solid #ddd;
|
|
padding: 8px 12px;
|
|
text-align: left;
|
|
}
|
|
|
|
.grades-table th {
|
|
background: #2c3e50;
|
|
color: white;
|
|
}
|
|
|
|
.grades-table tr:nth-child(even) {
|
|
background: #f9f9f9;
|
|
}
|
|
|
|
.grade-cell {
|
|
text-align: center;
|
|
font-weight: bold;
|
|
font-size: 12pt;
|
|
}
|
|
|
|
.attendance-box {
|
|
background: #fff3cd;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.signatures-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-top: 50px;
|
|
}
|
|
|
|
.signature-block {
|
|
text-align: center;
|
|
width: 40%;
|
|
}
|
|
|
|
.signature-line {
|
|
border-top: 1px solid #333;
|
|
margin-top: 40px;
|
|
padding-top: 5px;
|
|
}
|
|
|
|
/* Korrektur-Styles */
|
|
.exam-header {
|
|
background: #2c3e50;
|
|
color: white;
|
|
padding: 15px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.result-box {
|
|
background: #e8f5e9;
|
|
padding: 20px;
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.result-grade {
|
|
font-size: 36pt;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.result-points {
|
|
font-size: 14pt;
|
|
color: #666;
|
|
}
|
|
|
|
.corrections-list {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.correction-item {
|
|
border: 1px solid #ddd;
|
|
padding: 15px;
|
|
margin-bottom: 10px;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.correction-question {
|
|
font-weight: bold;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.correction-feedback {
|
|
background: #fff8e1;
|
|
padding: 10px;
|
|
margin-top: 10px;
|
|
border-left: 3px solid #ffc107;
|
|
font-size: 10pt;
|
|
}
|
|
|
|
.stats-table {
|
|
width: 100%;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.stats-table td {
|
|
padding: 5px 10px;
|
|
}
|
|
"""
|
|
|
|
|
|
def get_letter_template_html() -> str:
|
|
"""Inline HTML-Template fuer Elternbriefe."""
|
|
return """
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>{{ data.subject }}</title>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
{% if data.school_info %}
|
|
<div class="school-name">{{ data.school_info.name }}</div>
|
|
<div class="school-info">
|
|
{{ data.school_info.address }}<br>
|
|
Tel: {{ data.school_info.phone }} | E-Mail: {{ data.school_info.email }}
|
|
{% if data.school_info.website %} | {{ data.school_info.website }}{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<div class="school-name">Schule</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="letter-date">
|
|
{{ data.date }}
|
|
</div>
|
|
|
|
<div class="recipient">
|
|
{{ data.recipient_name }}<br>
|
|
{{ data.recipient_address | replace('\\n', '<br>') | safe }}
|
|
</div>
|
|
|
|
<div class="subject">
|
|
Betreff: {{ data.subject }}
|
|
</div>
|
|
|
|
<div class="meta-info" style="font-size: 10pt; color: #666; margin-bottom: 20px;">
|
|
Schüler/in: {{ data.student_name }} | Klasse: {{ data.student_class }}
|
|
</div>
|
|
|
|
<div class="content">
|
|
{{ data.content | replace('\\n', '<br>') | safe }}
|
|
</div>
|
|
|
|
{% if data.gfk_principles_applied %}
|
|
<div style="margin-bottom: 20px;">
|
|
{% for principle in data.gfk_principles_applied %}
|
|
<span class="gfk-badge">✓ {{ principle }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="signature">
|
|
<p>Mit freundlichen Grüßen</p>
|
|
<p style="margin-top: 30px;">
|
|
{{ data.teacher_name }}
|
|
{% if data.teacher_title %}<br><span style="font-size: 10pt;">{{ data.teacher_title }}</span>{% endif %}
|
|
</p>
|
|
</div>
|
|
|
|
{% if data.legal_references %}
|
|
<div class="legal-references">
|
|
<strong>Rechtliche Grundlagen:</strong><br>
|
|
{% for ref in data.legal_references %}
|
|
• {{ ref.law }} {{ ref.paragraph }}: {{ ref.title }}<br>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div style="font-size: 8pt; color: #999; margin-top: 30px; text-align: center;">
|
|
Erstellt mit BreakPilot | {{ generated_at }}
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
def get_certificate_template_html() -> str:
|
|
"""Inline HTML-Template fuer Zeugnisse."""
|
|
return """
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Zeugnis - {{ data.student_name }}</title>
|
|
</head>
|
|
<body>
|
|
<div class="certificate-header">
|
|
{% if data.school_info %}
|
|
<div class="school-name" style="font-size: 14pt;">{{ data.school_info.name }}</div>
|
|
{% endif %}
|
|
<div class="certificate-title">
|
|
{% if data.certificate_type == 'halbjahr' %}
|
|
Halbjahreszeugnis
|
|
{% elif data.certificate_type == 'jahres' %}
|
|
Jahreszeugnis
|
|
{% else %}
|
|
Abschlusszeugnis
|
|
{% endif %}
|
|
</div>
|
|
<div>Schuljahr {{ data.school_year }}</div>
|
|
</div>
|
|
|
|
<div class="student-info">
|
|
<table style="width: 100%;">
|
|
<tr>
|
|
<td><strong>Name:</strong> {{ data.student_name }}</td>
|
|
<td><strong>Geburtsdatum:</strong> {{ data.student_birthdate }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Klasse:</strong> {{ data.student_class }}</td>
|
|
<td> </td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<h3>Leistungen</h3>
|
|
<table class="grades-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 70%;">Fach</th>
|
|
<th style="width: 15%;">Note</th>
|
|
<th style="width: 15%;">Punkte</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for subject in data.subjects %}
|
|
<tr>
|
|
<td>{{ subject.name }}</td>
|
|
<td class="grade-cell" style="color: {{ subject.grade | grade_color }};">
|
|
{{ subject.grade }}
|
|
</td>
|
|
<td class="grade-cell">{{ subject.points | default('-') }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
|
|
{% if data.social_behavior or data.work_behavior %}
|
|
<h3>Verhalten</h3>
|
|
<table class="grades-table" style="width: 50%;">
|
|
{% if data.social_behavior %}
|
|
<tr>
|
|
<td>Sozialverhalten</td>
|
|
<td class="grade-cell">{{ data.social_behavior }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
{% if data.work_behavior %}
|
|
<tr>
|
|
<td>Arbeitsverhalten</td>
|
|
<td class="grade-cell">{{ data.work_behavior }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
</table>
|
|
{% endif %}
|
|
|
|
<div class="attendance-box">
|
|
<strong>Versäumte Tage:</strong> {{ data.attendance.days_absent | default(0) }}
|
|
(davon entschuldigt: {{ data.attendance.days_excused | default(0) }},
|
|
unentschuldigt: {{ data.attendance.days_unexcused | default(0) }})
|
|
</div>
|
|
|
|
{% if data.remarks %}
|
|
<div style="margin-bottom: 20px;">
|
|
<strong>Bemerkungen:</strong><br>
|
|
{{ data.remarks }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div style="margin-top: 30px;">
|
|
<strong>Ausgestellt am:</strong> {{ data.issue_date }}
|
|
</div>
|
|
|
|
<div class="signatures-row">
|
|
<div class="signature-block">
|
|
<div class="signature-line">{{ data.class_teacher }}</div>
|
|
<div style="font-size: 9pt;">Klassenlehrer/in</div>
|
|
</div>
|
|
<div class="signature-block">
|
|
<div class="signature-line">{{ data.principal }}</div>
|
|
<div style="font-size: 9pt;">Schulleiter/in</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="text-align: center; margin-top: 40px;">
|
|
<div style="font-size: 9pt; color: #666;">Siegel der Schule</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
def get_correction_template_html() -> str:
|
|
"""Inline HTML-Template fuer Korrektur-Uebersichten."""
|
|
return """
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Korrektur - {{ data.exam_title }}</title>
|
|
</head>
|
|
<body>
|
|
<div class="exam-header">
|
|
<h1 style="margin: 0; color: white;">{{ data.exam_title }}</h1>
|
|
<div>{{ data.subject }} | {{ data.date }}</div>
|
|
</div>
|
|
|
|
<div class="student-info">
|
|
<strong>{{ data.student.name }}</strong> | Klasse {{ data.student.class_name }}
|
|
</div>
|
|
|
|
<div class="result-box">
|
|
<div class="result-grade" style="color: {{ data.grade | grade_color }};">
|
|
Note: {{ data.grade }}
|
|
</div>
|
|
<div class="result-points">
|
|
{{ data.achieved_points }} von {{ data.max_points }} Punkten
|
|
({{ data.percentage | round(1) }}%)
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Detaillierte Auswertung</h3>
|
|
<div class="corrections-list">
|
|
{% for item in data.corrections %}
|
|
<div class="correction-item">
|
|
<div class="correction-question">
|
|
{{ item.question }}
|
|
</div>
|
|
{% if item.answer %}
|
|
<div style="margin: 5px 0; font-style: italic; color: #555;">
|
|
<strong>Antwort:</strong> {{ item.answer }}
|
|
</div>
|
|
{% endif %}
|
|
<div>
|
|
<strong>Punkte:</strong> {{ item.points }}
|
|
</div>
|
|
{% if item.feedback %}
|
|
<div class="correction-feedback">
|
|
{{ item.feedback }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{% if data.teacher_notes %}
|
|
<div style="background: #e3f2fd; padding: 15px; border-radius: 5px; margin-bottom: 20px;">
|
|
<strong>Lehrerkommentar:</strong><br>
|
|
{{ data.teacher_notes }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if data.ai_feedback %}
|
|
<div style="background: #f3e5f5; padding: 15px; border-radius: 5px; margin-bottom: 20px;">
|
|
<strong>KI-Feedback:</strong><br>
|
|
{{ data.ai_feedback }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if data.class_average or data.grade_distribution %}
|
|
<h3>Klassenstatistik</h3>
|
|
<table class="stats-table">
|
|
{% if data.class_average %}
|
|
<tr>
|
|
<td><strong>Klassendurchschnitt:</strong></td>
|
|
<td>{{ data.class_average }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
{% if data.grade_distribution %}
|
|
<tr>
|
|
<td><strong>Notenverteilung:</strong></td>
|
|
<td>
|
|
{% for grade, count in data.grade_distribution.items() %}
|
|
Note {{ grade }}: {{ count }}x{% if not loop.last %}, {% endif %}
|
|
{% endfor %}
|
|
</td>
|
|
</tr>
|
|
{% endif %}
|
|
</table>
|
|
{% endif %}
|
|
|
|
<div class="signature" style="margin-top: 40px;">
|
|
<p style="font-size: 9pt; color: #666;">Datum: {{ data.date }}</p>
|
|
</div>
|
|
|
|
<div style="font-size: 8pt; color: #999; margin-top: 30px; text-align: center;">
|
|
Erstellt mit BreakPilot | {{ generated_at }}
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|