This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/frontend/modules/letters.py
Benjamin Admin bfdaf63ba9 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>
2026-02-09 09:51:32 +01:00

1953 lines
55 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
BreakPilot Studio - Elternkommunikation Modul
Refactored: 2024-12-18
- Kachel-basierte Startansicht
- Module fuer verschiedene Kommunikationsformen
Funktionen (als Kacheln):
- Elterngespraech (Notizen, Protokolle)
- Elterngespraech planen (Terminbuchung)
- Elternbriefe mit Legal Assistant (GFK)
"""
class LettersModule:
"""Elternkommunikation Modul mit Legal Assistant."""
@staticmethod
def get_css() -> str:
"""CSS fuer das Elternkommunikation-Modul."""
return """
/* ==========================================
LETTERS MODULE STYLES - Elternkommunikation
========================================== */
/* Panel Layout */
.panel-letters {
display: none;
flex-direction: column;
height: 100%;
background: var(--bp-bg);
overflow: hidden;
}
.panel-letters.active {
display: flex;
}
/* Letters Header */
.letters-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 32px;
border-bottom: 1px solid var(--bp-border);
background: var(--bp-surface);
}
.letters-title-section h1 {
font-size: 24px;
font-weight: 700;
color: var(--bp-text);
margin-bottom: 4px;
}
.letters-subtitle {
font-size: 14px;
color: var(--bp-text-muted);
}
/* Letters Content - Kacheln */
.letters-content {
flex: 1;
overflow-y: auto;
padding: 32px;
}
/* Tiles Grid */
.letters-tiles {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
max-width: 1200px;
margin: 0 auto;
}
/* Letter Tile */
.letter-tile {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 16px;
padding: 28px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.letter-tile:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
border-color: var(--bp-primary);
}
.letter-tile::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
opacity: 0;
transition: opacity 0.3s;
}
.letter-tile:hover::before {
opacity: 1;
}
.letter-tile.conversation::before { background: linear-gradient(90deg, #3b82f6, #1d4ed8); }
.letter-tile.planning::before { background: linear-gradient(90deg, #10b981, #059669); }
.letter-tile.legal::before { background: linear-gradient(90deg, #8b5cf6, #6d28d9); }
.tile-icon-wrapper {
width: 64px;
height: 64px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
margin-bottom: 20px;
}
.tile-icon-wrapper.conversation { background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); }
.tile-icon-wrapper.planning { background: linear-gradient(135deg, #10b981 0%, #059669 100%); }
.tile-icon-wrapper.legal { background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%); }
.tile-heading {
font-size: 18px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 10px;
}
.tile-description {
font-size: 14px;
color: var(--bp-text-muted);
line-height: 1.6;
margin-bottom: 20px;
}
.tile-features-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tile-feature {
padding: 6px 12px;
background: var(--bp-bg);
border-radius: 16px;
font-size: 12px;
color: var(--bp-text-muted);
}
.tile-arrow-icon {
position: absolute;
bottom: 24px;
right: 24px;
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--bp-bg);
display: flex;
align-items: center;
justify-content: center;
color: var(--bp-text-muted);
transition: all 0.3s;
font-size: 18px;
}
.letter-tile:hover .tile-arrow-icon {
background: var(--bp-primary);
color: white;
transform: translateX(4px);
}
/* Sub-Panel */
.letters-subpanel {
display: none;
flex-direction: column;
height: 100%;
}
.letters-subpanel.active {
display: flex;
}
.subpanel-header {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 24px;
border-bottom: 1px solid var(--bp-border);
background: var(--bp-surface);
}
.subpanel-back {
width: 36px;
height: 36px;
border-radius: 8px;
border: 1px solid var(--bp-border);
background: var(--bp-bg);
color: var(--bp-text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
transition: all 0.2s;
}
.subpanel-back:hover {
background: var(--bp-surface-elevated);
}
.subpanel-title {
font-size: 18px;
font-weight: 600;
}
.subpanel-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
/* ==========================================
SUB-MODULE: Elterngespraech
========================================== */
.conversation-container {
max-width: 900px;
margin: 0 auto;
}
.conversation-header-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.info-card {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 12px;
padding: 16px;
}
.info-card label {
display: block;
font-size: 12px;
color: var(--bp-text-muted);
margin-bottom: 6px;
}
.info-card input,
.info-card select {
width: 100%;
padding: 10px;
border: 1px solid var(--bp-border);
border-radius: 8px;
background: var(--bp-bg);
color: var(--bp-text);
font-size: 14px;
}
.conversation-notes {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
}
.conversation-notes h3 {
font-size: 14px;
color: var(--bp-text-muted);
margin-bottom: 12px;
}
.conversation-notes textarea {
width: 100%;
min-height: 250px;
padding: 16px;
border: 1px solid var(--bp-border);
border-radius: 8px;
background: var(--bp-bg);
color: var(--bp-text);
font-size: 14px;
line-height: 1.6;
resize: vertical;
}
.conversation-notes textarea:focus {
outline: none;
border-color: var(--bp-primary);
}
.conversation-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
/* ==========================================
SUB-MODULE: Terminplanung
========================================== */
.planning-container {
max-width: 800px;
margin: 0 auto;
}
.planning-calendar {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.calendar-nav {
display: flex;
gap: 8px;
}
.calendar-nav button {
width: 32px;
height: 32px;
border-radius: 8px;
border: 1px solid var(--bp-border);
background: var(--bp-bg);
color: var(--bp-text);
cursor: pointer;
}
.calendar-month {
font-size: 18px;
font-weight: 600;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
.calendar-day-header {
text-align: center;
font-size: 12px;
color: var(--bp-text-muted);
padding: 8px;
}
.calendar-day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
}
.calendar-day:hover {
background: var(--bp-bg);
}
.calendar-day.today {
background: var(--bp-primary-soft);
color: var(--bp-primary);
font-weight: 600;
}
.calendar-day.selected {
background: var(--bp-primary);
color: white;
}
.calendar-day.has-appointment::after {
content: '';
position: absolute;
bottom: 4px;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--bp-primary);
}
.time-slots {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 12px;
padding: 20px;
}
.time-slots h3 {
font-size: 14px;
color: var(--bp-text-muted);
margin-bottom: 16px;
}
.time-slot-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 8px;
}
.time-slot {
padding: 10px;
text-align: center;
border: 1px solid var(--bp-border);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
font-size: 13px;
}
.time-slot:hover {
border-color: var(--bp-primary);
}
.time-slot.selected {
background: var(--bp-primary);
border-color: var(--bp-primary);
color: white;
}
.time-slot.booked {
opacity: 0.5;
cursor: not-allowed;
text-decoration: line-through;
}
/* ==========================================
SUB-MODULE: Legal Assistant / Elternbriefe
========================================== */
.legal-container {
display: grid;
grid-template-columns: 1fr 380px;
gap: 24px;
max-width: 1400px;
}
@media (max-width: 1100px) {
.legal-container {
grid-template-columns: 1fr;
}
}
/* Editor Section */
.legal-editor {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 12px;
padding: 24px;
}
.editor-section {
margin-bottom: 24px;
}
.editor-section-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
/* Letter Type Selection */
.letter-types {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 8px;
margin-bottom: 20px;
}
.letter-type-btn {
padding: 12px;
border-radius: 8px;
border: 1px solid var(--bp-border);
background: var(--bp-surface);
cursor: pointer;
text-align: center;
transition: all 0.2s;
}
.letter-type-btn:hover {
border-color: var(--bp-primary);
}
.letter-type-btn.active {
background: var(--bp-primary-soft);
border-color: var(--bp-primary);
color: var(--bp-primary);
}
.letter-type-icon {
font-size: 20px;
margin-bottom: 4px;
}
.letter-type-label {
font-size: 11px;
font-weight: 500;
}
/* Tone Selection */
.tone-options {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tone-btn {
padding: 6px 12px;
border-radius: 20px;
border: 1px solid var(--bp-border);
background: transparent;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.tone-btn:hover {
border-color: var(--bp-primary);
}
.tone-btn.active {
background: var(--bp-primary);
border-color: var(--bp-primary);
color: white;
}
/* Text Editor */
.text-editor {
width: 100%;
min-height: 300px;
padding: 16px;
border-radius: 8px;
border: 1px solid var(--bp-border);
background: var(--bp-surface);
color: var(--bp-text);
font-family: inherit;
font-size: 14px;
line-height: 1.6;
resize: vertical;
}
.text-editor:focus {
outline: none;
border-color: var(--bp-primary);
}
/* Character Counter */
.char-counter {
display: flex;
justify-content: flex-end;
font-size: 12px;
color: var(--bp-text-muted);
margin-top: 8px;
}
/* Legal Assistant Panel */
.legal-assistant-panel {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 12px;
padding: 24px;
height: fit-content;
position: sticky;
top: 24px;
}
.legal-assistant-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.legal-icon-box {
width: 44px;
height: 44px;
background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
}
.legal-assistant-title {
font-size: 16px;
font-weight: 600;
}
.legal-assistant-subtitle {
font-size: 12px;
color: var(--bp-text-muted);
}
/* GFK Score */
.gfk-score-card {
background: var(--bp-bg);
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
}
.gfk-score-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.gfk-score-label {
font-size: 13px;
font-weight: 500;
}
.gfk-score-value {
font-size: 28px;
font-weight: 700;
color: var(--bp-accent);
}
.gfk-score-bar {
height: 8px;
background: var(--bp-border);
border-radius: 4px;
overflow: hidden;
}
.gfk-score-fill {
height: 100%;
background: var(--bp-accent);
border-radius: 4px;
transition: width 0.3s ease;
}
.gfk-info {
font-size: 11px;
color: var(--bp-text-muted);
margin-top: 8px;
}
/* Suggestions */
.suggestions-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
}
.suggestion-item {
background: var(--bp-bg);
border-radius: 8px;
padding: 12px;
border-left: 3px solid var(--bp-warning);
}
.suggestion-item.positive {
border-left-color: var(--bp-success);
}
.suggestion-type {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
color: var(--bp-text-muted);
margin-bottom: 4px;
}
.suggestion-text {
font-size: 13px;
line-height: 1.4;
}
/* Templates */
.templates-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.template-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
background: var(--bp-bg);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.template-item:hover {
background: var(--bp-surface-elevated);
}
.template-name {
font-size: 13px;
font-weight: 500;
}
.template-use-btn {
font-size: 12px;
color: var(--bp-primary);
}
/* Legal References */
.legal-refs-section {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--bp-border);
}
.legal-refs-title {
font-size: 12px;
font-weight: 600;
margin-bottom: 12px;
color: var(--bp-text-muted);
}
.legal-ref-item {
font-size: 12px;
padding: 10px;
background: var(--bp-bg);
border-radius: 8px;
margin-bottom: 8px;
}
.legal-ref-law {
font-weight: 600;
color: var(--bp-info);
}
/* Actions */
.editor-actions {
display: flex;
gap: 12px;
margin-top: 20px;
}
/* Status Bar */
.letters-status {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 24px;
border-top: 1px solid var(--bp-border);
background: var(--bp-surface);
font-size: 12px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #10b981;
}
.status-indicator.busy {
background: #f59e0b;
animation: statusPulse 1.5s infinite;
}
@keyframes statusPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-text {
color: var(--bp-text);
}
.status-detail {
color: var(--bp-text-muted);
}
"""
@staticmethod
def get_html() -> str:
"""HTML fuer das Elternkommunikation-Modul mit Kachel-basierter Startansicht."""
return """
<!-- Letters Panel -->
<div id="panel-letters" class="panel-letters">
<!-- Main Tiles View -->
<div id="letters-tiles-view">
<!-- Header -->
<div class="letters-header">
<div class="letters-title-section">
<h1>Elternkommunikation</h1>
<p class="letters-subtitle">Professionelle Kommunikation mit Eltern und Erziehungsberechtigten</p>
</div>
</div>
<!-- Tiles Content -->
<div class="letters-content">
<div class="letters-tiles">
<!-- Elterngespraech -->
<div class="letter-tile conversation" onclick="openLettersSubpanel('conversation')">
<div class="tile-icon-wrapper conversation">&#128172;</div>
<div class="tile-heading">Elterngespraech</div>
<div class="tile-description">Fuehre strukturierte Elterngespraeche und dokumentiere wichtige Vereinbarungen. Erstelle Protokolle und Notizen.</div>
<div class="tile-features-list">
<span class="tile-feature">Protokolle</span>
<span class="tile-feature">Notizen</span>
<span class="tile-feature">Export</span>
</div>
<div class="tile-arrow-icon">&#8594;</div>
</div>
<!-- Elterngespraech planen -->
<div class="letter-tile planning" onclick="openLettersSubpanel('planning')">
<div class="tile-icon-wrapper planning">&#128197;</div>
<div class="tile-heading">Elterngespraech planen</div>
<div class="tile-description">Plane und verwalte Termine fuer Elterngespraeche. Versende Einladungen und Erinnerungen automatisch.</div>
<div class="tile-features-list">
<span class="tile-feature">Kalender</span>
<span class="tile-feature">Einladungen</span>
<span class="tile-feature">Erinnerungen</span>
</div>
<div class="tile-arrow-icon">&#8594;</div>
</div>
<!-- Elternbriefe mit Legal Assistant -->
<div class="letter-tile legal" onclick="openLettersSubpanel('legal')">
<div class="tile-icon-wrapper legal">&#9878;</div>
<div class="tile-heading">Elternbriefe mit Legal Assistant</div>
<div class="tile-description">Verfasse rechtssichere Elternbriefe mit KI-Unterstuetzung. Der Legal Assistant prueft auf GFK-konforme Sprache.</div>
<div class="tile-features-list">
<span class="tile-feature">GFK-Analyse</span>
<span class="tile-feature">Vorlagen</span>
<span class="tile-feature">KI-Verbesserung</span>
<span class="tile-feature">PDF Export</span>
</div>
<div class="tile-arrow-icon">&#8594;</div>
</div>
</div>
</div>
</div>
<!-- Sub-Panel: Elterngespraech -->
<div id="letters-subpanel-conversation" class="letters-subpanel">
<div class="subpanel-header">
<button class="subpanel-back" onclick="closeLettersSubpanel()">&#8592;</button>
<div class="subpanel-title">Elterngespraech dokumentieren</div>
</div>
<div class="subpanel-content">
<div class="conversation-container">
<div class="conversation-header-info">
<div class="info-card">
<label>Schueler/in</label>
<input type="text" id="conv-student" placeholder="Name des Schuelers">
</div>
<div class="info-card">
<label>Klasse</label>
<input type="text" id="conv-class" placeholder="z.B. 7a">
</div>
<div class="info-card">
<label>Datum</label>
<input type="date" id="conv-date">
</div>
<div class="info-card">
<label>Teilnehmer</label>
<input type="text" id="conv-participants" placeholder="z.B. Mutter, Klassenlehrer">
</div>
</div>
<div class="conversation-notes">
<h3>&#128221; Gespraechsnotizen</h3>
<textarea id="conv-notes" placeholder="Hier koennen Sie Notizen zum Gespraech festhalten...
Themen:
-
Vereinbarungen:
-
Naechste Schritte:
- "></textarea>
</div>
<div class="conversation-actions">
<button class="btn btn-ghost" onclick="saveConversationDraft()">&#128190; Entwurf speichern</button>
<button class="btn btn-ghost" onclick="exportConversation('pdf')">&#128196; Als PDF exportieren</button>
<button class="btn btn-primary" onclick="saveConversation()">&#10004; Abschliessen</button>
</div>
</div>
</div>
</div>
<!-- Sub-Panel: Terminplanung -->
<div id="letters-subpanel-planning" class="letters-subpanel">
<div class="subpanel-header">
<button class="subpanel-back" onclick="closeLettersSubpanel()">&#8592;</button>
<div class="subpanel-title">Elterngespraech planen</div>
</div>
<div class="subpanel-content">
<div class="planning-container">
<div class="planning-calendar">
<div class="calendar-header">
<div class="calendar-nav">
<button onclick="prevMonth()">&#8249;</button>
<button onclick="nextMonth()">&#8250;</button>
</div>
<div class="calendar-month" id="calendar-month">Dezember 2024</div>
</div>
<div class="calendar-grid" id="calendar-grid">
<div class="calendar-day-header">Mo</div>
<div class="calendar-day-header">Di</div>
<div class="calendar-day-header">Mi</div>
<div class="calendar-day-header">Do</div>
<div class="calendar-day-header">Fr</div>
<div class="calendar-day-header">Sa</div>
<div class="calendar-day-header">So</div>
<!-- Days werden dynamisch generiert -->
</div>
</div>
<div class="time-slots">
<h3>&#128337; Verfuegbare Zeitslots</h3>
<div class="time-slot-grid" id="time-slots">
<div class="time-slot" onclick="selectTimeSlot(this)">08:00</div>
<div class="time-slot" onclick="selectTimeSlot(this)">08:30</div>
<div class="time-slot" onclick="selectTimeSlot(this)">09:00</div>
<div class="time-slot" onclick="selectTimeSlot(this)">09:30</div>
<div class="time-slot booked">10:00</div>
<div class="time-slot" onclick="selectTimeSlot(this)">10:30</div>
<div class="time-slot" onclick="selectTimeSlot(this)">14:00</div>
<div class="time-slot" onclick="selectTimeSlot(this)">14:30</div>
<div class="time-slot" onclick="selectTimeSlot(this)">15:00</div>
<div class="time-slot booked">15:30</div>
<div class="time-slot" onclick="selectTimeSlot(this)">16:00</div>
<div class="time-slot" onclick="selectTimeSlot(this)">16:30</div>
</div>
</div>
<div style="margin-top: 24px; display: flex; gap: 12px; justify-content: flex-end;">
<button class="btn btn-primary" onclick="bookAppointment()">&#128197; Termin buchen</button>
</div>
</div>
</div>
</div>
<!-- Sub-Panel: Legal Assistant -->
<div id="letters-subpanel-legal" class="letters-subpanel">
<div class="subpanel-header">
<button class="subpanel-back" onclick="closeLettersSubpanel()">&#8592;</button>
<div class="subpanel-title">Elternbriefe mit Legal Assistant</div>
</div>
<div class="subpanel-content">
<div class="legal-container">
<!-- Editor Section -->
<div class="legal-editor">
<!-- Letter Type -->
<div class="editor-section">
<div class="editor-section-title">&#128221; Art des Schreibens</div>
<div class="letter-types">
<button class="letter-type-btn active" data-type="general" onclick="selectLetterType('general')">
<div class="letter-type-icon">&#128196;</div>
<div class="letter-type-label">Allgemein</div>
</button>
<button class="letter-type-btn" data-type="behavior" onclick="selectLetterType('behavior')">
<div class="letter-type-icon">&#128100;</div>
<div class="letter-type-label">Verhalten</div>
</button>
<button class="letter-type-btn" data-type="academic" onclick="selectLetterType('academic')">
<div class="letter-type-icon">&#128218;</div>
<div class="letter-type-label">Leistung</div>
</button>
<button class="letter-type-btn" data-type="attendance" onclick="selectLetterType('attendance')">
<div class="letter-type-icon">&#128197;</div>
<div class="letter-type-label">Fehlzeiten</div>
</button>
<button class="letter-type-btn" data-type="meeting" onclick="selectLetterType('meeting')">
<div class="letter-type-icon">&#128198;</div>
<div class="letter-type-label">Einladung</div>
</button>
<button class="letter-type-btn" data-type="positive" onclick="selectLetterType('positive')">
<div class="letter-type-icon">&#11088;</div>
<div class="letter-type-label">Positives</div>
</button>
</div>
</div>
<!-- Tone Selection -->
<div class="editor-section">
<div class="editor-section-title">&#127912; Tonalitaet</div>
<div class="tone-options">
<button class="tone-btn" data-tone="formal" onclick="selectTone('formal')">Formell</button>
<button class="tone-btn active" data-tone="professional" onclick="selectTone('professional')">Professionell</button>
<button class="tone-btn" data-tone="warm" onclick="selectTone('warm')">Warmherzig</button>
<button class="tone-btn" data-tone="concerned" onclick="selectTone('concerned')">Besorgt</button>
<button class="tone-btn" data-tone="appreciative" onclick="selectTone('appreciative')">Wertschaetzend</button>
</div>
</div>
<!-- Student Info -->
<div class="editor-section">
<div style="display: grid; grid-template-columns: 1fr 120px; gap: 12px;">
<div>
<label style="display: block; font-size: 12px; color: var(--bp-text-muted); margin-bottom: 6px;">Schueler/in</label>
<input type="text" id="letter-student" placeholder="Max Mustermann" style="width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text);">
</div>
<div>
<label style="display: block; font-size: 12px; color: var(--bp-text-muted); margin-bottom: 6px;">Klasse</label>
<input type="text" id="letter-class" placeholder="7a" style="width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text);">
</div>
</div>
</div>
<!-- Subject -->
<div class="editor-section">
<label style="display: block; font-size: 12px; color: var(--bp-text-muted); margin-bottom: 6px;">Betreff</label>
<input type="text" id="letter-subject" placeholder="z.B. Information zum Halbjahresstand" style="width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text);">
</div>
<!-- Text Editor -->
<div class="editor-section">
<div class="editor-section-title">&#128466; Nachricht</div>
<textarea class="text-editor" id="letter-content" placeholder="Schreiben Sie hier Ihre Nachricht...
Tipps:
- Beginnen Sie mit einer wertschaetzenden Anrede
- Beschreiben Sie Beobachtungen ohne Bewertung
- Formulieren Sie Ihre Beduerfnisse und Bitten klar
- Schliessen Sie mit einem kooperativen Ausblick" oninput="analyzeLetterText()"></textarea>
<div class="char-counter">
<span id="letter-char-count">0</span> Zeichen
</div>
</div>
<!-- Actions -->
<div class="editor-actions">
<button class="btn btn-primary" onclick="improveWithAI()">&#10024; Mit KI verbessern</button>
<button class="btn btn-ghost" onclick="showLetterPreview()">&#128065; Vorschau</button>
<button class="btn btn-ghost" onclick="exportLetterPDF()">&#128196; PDF Export</button>
<button class="btn btn-ghost" onclick="saveLetterTemplate()">&#128190; Als Vorlage</button>
</div>
</div>
<!-- Legal Assistant Panel -->
<div class="legal-assistant-panel">
<div class="legal-assistant-header">
<div class="legal-icon-box">&#9878;</div>
<div>
<div class="legal-assistant-title">Legal Assistant</div>
<div class="legal-assistant-subtitle">Prueft auf rechtssichere Sprache</div>
</div>
</div>
<!-- GFK Score -->
<div class="gfk-score-card">
<div class="gfk-score-header">
<span class="gfk-score-label">GFK-Score</span>
<span class="gfk-score-value" id="gfk-score-display">--</span>
</div>
<div class="gfk-score-bar">
<div class="gfk-score-fill" id="gfk-score-bar-fill" style="width: 0%"></div>
</div>
<div class="gfk-info">Gewaltfreie Kommunikation nach M. Rosenberg</div>
</div>
<!-- Suggestions -->
<div style="margin-bottom: 20px;">
<div class="editor-section-title">&#128161; Verbesserungsvorschlaege</div>
<div class="suggestions-list" id="suggestions-list">
<div class="suggestion-item positive">
<div class="suggestion-type">&#9989; Tipp</div>
<div class="suggestion-text">Beginnen Sie mit dem Schreiben, um Vorschlaege zu erhalten.</div>
</div>
</div>
</div>
<!-- Templates -->
<div style="margin-bottom: 20px;">
<div class="editor-section-title">&#128203; Vorlagen</div>
<div class="templates-list">
<div class="template-item" onclick="loadLetterTemplate('halbjahr')">
<span class="template-name">Halbjahresinformation</span>
<span class="template-use-btn">Verwenden &#8594;</span>
</div>
<div class="template-item" onclick="loadLetterTemplate('fehlzeiten')">
<span class="template-name">Fehlzeiten-Hinweis</span>
<span class="template-use-btn">Verwenden &#8594;</span>
</div>
<div class="template-item" onclick="loadLetterTemplate('elternabend')">
<span class="template-name">Einladung Elternabend</span>
<span class="template-use-btn">Verwenden &#8594;</span>
</div>
<div class="template-item" onclick="loadLetterTemplate('lob')">
<span class="template-name">Positive Rueckmeldung</span>
<span class="template-use-btn">Verwenden &#8594;</span>
</div>
</div>
</div>
<!-- Legal References -->
<div class="legal-refs-section">
<div class="legal-refs-title">&#9878; Rechtliche Hinweise</div>
<div id="legal-refs-container">
<div class="legal-ref-item">
<span class="legal-ref-law">SchulG NRW &sect; 42</span>
<div>Informationspflicht der Schule</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Status Bar -->
<div class="letters-status">
<span class="status-indicator" id="letters-status-indicator"></span>
<span class="status-text" id="letters-status-text">Bereit</span>
<span class="status-detail" id="letters-status-detail"></span>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
"""JavaScript fuer das Elternkommunikation-Modul."""
return """
// ==========================================
// LETTERS MODULE - Elternkommunikation
// ==========================================
let lettersInitialized = false;
let currentLetterType = 'general';
let currentTone = 'professional';
let analysisTimeout = null;
// Letter Templates
const LETTER_TEMPLATES = {
halbjahr: {
subject: 'Information zum Leistungsstand - Halbjahr',
content: `Sehr geehrte Eltern,
ich moechte Sie ueber den aktuellen Leistungsstand Ihres Kindes [SCHUELER] in der Klasse [KLASSE] informieren.
[Beobachtungen einfuegen]
Ich wuerde mich freuen, wenn wir gemeinsam besprechen koennten, wie wir [SCHUELER] weiter unterstuetzen koennen.
Mit freundlichen Gruessen`
},
fehlzeiten: {
subject: 'Mitteilung ueber Fehlzeiten',
content: `Sehr geehrte Eltern,
mir ist aufgefallen, dass [SCHUELER] in letzter Zeit haeufiger dem Unterricht ferngeblieben ist.
Ich mache mir Sorgen darueber und moechte gerne verstehen, ob es Gruende gibt, bei denen wir als Schule unterstuetzen koennen.
Koennten wir einen Termin fuer ein kurzes Gespraech vereinbaren?
Mit freundlichen Gruessen`
},
elternabend: {
subject: 'Einladung zum Elternabend',
content: `Sehr geehrte Eltern,
hiermit lade ich Sie herzlich zum Elternabend der Klasse [KLASSE] ein.
Datum: [DATUM]
Uhrzeit: [UHRZEIT]
Ort: [ORT]
Tagesordnung:
1. Begruessung
2. Informationen zum Halbjahr
3. Verschiedenes
Ich freue mich auf Ihr Kommen und einen konstruktiven Austausch.
Mit freundlichen Gruessen`
},
lob: {
subject: 'Positive Rueckmeldung zu [SCHUELER]',
content: `Sehr geehrte Eltern,
ich freue mich, Ihnen eine positive Rueckmeldung zu [SCHUELER] geben zu koennen.
[Positive Beobachtungen einfuegen]
Es ist schoen zu sehen, wie sich [SCHUELER] entwickelt. Bitte geben Sie dieses Lob auch zu Hause weiter.
Mit freundlichen Gruessen`
}
};
// GFK Patterns
const GFK_POSITIVE_PATTERNS = [
/ich (beobachte|sehe|habe bemerkt)/i,
/ich (fuehle|empfinde)/i,
/ich (brauche|wuensche mir)/i,
/ich (bitte|moechte bitten)/i,
/koennten wir/i,
/gemeinsam/i,
/unterstuetzen/i,
/wertschaetze/i,
/freue mich/i
];
const GFK_NEGATIVE_PATTERNS = [
/muss|muessen/i,
/immer|nie|staendig/i,
/schuld/i,
/versagt/i,
/unfaehig/i,
/sie sollten/i,
/inakzeptabel/i
];
function loadLettersModule() {
if (lettersInitialized) {
console.log('Letters module already initialized');
return;
}
console.log('Loading Letters Module...');
// Set default date
const dateInput = document.getElementById('conv-date');
if (dateInput) {
dateInput.valueAsDate = new Date();
}
// Initialize calendar
initCalendar();
lettersInitialized = true;
console.log('Letters Module loaded successfully');
}
// ==========================================
// VIEW SWITCHING
// ==========================================
function openLettersSubpanel(panelId) {
document.getElementById('letters-tiles-view').style.display = 'none';
document.querySelectorAll('.letters-subpanel').forEach(p => {
p.classList.remove('active');
});
const panel = document.getElementById('letters-subpanel-' + panelId);
if (panel) {
panel.classList.add('active');
}
}
function closeLettersSubpanel() {
document.querySelectorAll('.letters-subpanel').forEach(p => {
p.classList.remove('active');
});
document.getElementById('letters-tiles-view').style.display = 'block';
}
// ==========================================
// STATUS
// ==========================================
function setLettersStatus(text, detail = '', state = 'idle') {
const indicator = document.getElementById('letters-status-indicator');
const textEl = document.getElementById('letters-status-text');
const detailEl = document.getElementById('letters-status-detail');
if (textEl) textEl.textContent = text;
if (detailEl) detailEl.textContent = detail;
if (indicator) {
indicator.classList.remove('busy');
if (state === 'busy') indicator.classList.add('busy');
}
}
// ==========================================
// CONVERSATION (Elterngespraech)
// ==========================================
function saveConversationDraft() {
const data = {
student: document.getElementById('conv-student')?.value,
class: document.getElementById('conv-class')?.value,
date: document.getElementById('conv-date')?.value,
participants: document.getElementById('conv-participants')?.value,
notes: document.getElementById('conv-notes')?.value
};
localStorage.setItem('bp-conversation-draft', JSON.stringify(data));
setLettersStatus('Entwurf gespeichert', '');
}
async function saveConversation() {
const data = {
student: document.getElementById('conv-student')?.value,
class: document.getElementById('conv-class')?.value,
date: document.getElementById('conv-date')?.value,
participants: document.getElementById('conv-participants')?.value,
notes: document.getElementById('conv-notes')?.value
};
if (!data.student || !data.notes) {
alert('Bitte fuellen Sie mindestens Schueler/in und Notizen aus.');
return;
}
setLettersStatus('Speichere Protokoll...', '', 'busy');
try {
// Save as a letter with type 'general' for the conversation record
const resp = await fetch('/api/letters/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipient_name: 'Gespraechsprotokoll',
recipient_address: '',
student_name: data.student,
student_class: data.class || '',
subject: 'Elterngespraech vom ' + (data.date || new Date().toLocaleDateString('de-DE')),
content: 'Teilnehmer: ' + (data.participants || 'k.A.') + '\\n\\n' + data.notes,
letter_type: 'general',
tone: 'professional',
teacher_name: '',
teacher_title: ''
})
});
if (!resp.ok) {
throw new Error('Speichern fehlgeschlagen');
}
const result = await resp.json();
setLettersStatus('Protokoll gespeichert', 'ID: ' + result.id);
alert('Gespraechsprotokoll wurde gespeichert.');
closeLettersSubpanel();
} catch (e) {
console.error('Save conversation error:', e);
setLettersStatus('Speichern fehlgeschlagen', e.message, 'error');
// Fallback to local storage
saveConversationDraft();
alert('Gespraechsprotokoll wurde lokal gespeichert.');
closeLettersSubpanel();
}
}
async function exportConversation(format) {
const data = {
student: document.getElementById('conv-student')?.value || 'Unbekannt',
class: document.getElementById('conv-class')?.value || '',
date: document.getElementById('conv-date')?.value || new Date().toLocaleDateString('de-DE'),
participants: document.getElementById('conv-participants')?.value || '',
notes: document.getElementById('conv-notes')?.value || ''
};
if (!data.notes) {
alert('Bitte geben Sie zuerst Notizen ein.');
return;
}
setLettersStatus('Erstelle ' + format.toUpperCase() + '...', '', 'busy');
if (format === 'pdf') {
try {
const resp = await fetch('/api/letters/export-pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
letter_data: {
recipient_name: 'Gespraechsprotokoll',
recipient_address: '',
student_name: data.student,
student_class: data.class,
subject: 'Elterngespraech vom ' + data.date,
content: 'Teilnehmer: ' + (data.participants || 'k.A.') + '\\n\\n' + data.notes,
letter_type: 'general',
tone: 'professional',
teacher_name: '',
teacher_title: ''
}
})
});
if (!resp.ok) {
throw new Error('PDF-Export fehlgeschlagen');
}
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `Gespraechsprotokoll_${data.student.replace(/\\s+/g, '_')}_${data.date.replace(/\\./g, '-')}.pdf`;
a.click();
URL.revokeObjectURL(url);
setLettersStatus('PDF erstellt', 'Download gestartet');
} catch (e) {
console.error('PDF export error:', e);
setLettersStatus('Export fehlgeschlagen', e.message, 'error');
alert('PDF-Export fehlgeschlagen: ' + e.message);
}
} else {
// Text export fallback
const textContent = [
'GESPRAECHSPROTOKOLL',
'==================',
'',
'Schueler/in: ' + data.student,
'Klasse: ' + data.class,
'Datum: ' + data.date,
'Teilnehmer: ' + data.participants,
'',
'NOTIZEN:',
'--------',
data.notes
].join('\\n');
const blob = new Blob([textContent], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `Gespraechsprotokoll_${data.student.replace(/\\s+/g, '_')}.txt`;
a.click();
URL.revokeObjectURL(url);
setLettersStatus('Export abgeschlossen', '');
}
}
// ==========================================
// PLANNING (Terminplanung)
// ==========================================
let selectedDate = null;
let selectedTimeSlot = null;
let currentCalendarDate = new Date();
function initCalendar() {
renderCalendar();
}
function renderCalendar() {
const grid = document.getElementById('calendar-grid');
const monthLabel = document.getElementById('calendar-month');
if (!grid || !monthLabel) return;
const year = currentCalendarDate.getFullYear();
const month = currentCalendarDate.getMonth();
const monthNames = ['Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
monthLabel.textContent = monthNames[month] + ' ' + year;
// Clear existing days (keep headers)
const headers = Array.from(grid.querySelectorAll('.calendar-day-header'));
grid.innerHTML = '';
headers.forEach(h => grid.appendChild(h));
// First day of month
const firstDay = new Date(year, month, 1);
let startDay = firstDay.getDay();
if (startDay === 0) startDay = 7; // Monday = 1
// Days in month
const daysInMonth = new Date(year, month + 1, 0).getDate();
// Today
const today = new Date();
const isThisMonth = today.getFullYear() === year && today.getMonth() === month;
// Add empty cells for days before start
for (let i = 1; i < startDay; i++) {
const empty = document.createElement('div');
empty.className = 'calendar-day';
grid.appendChild(empty);
}
// Add days
for (let day = 1; day <= daysInMonth; day++) {
const dayEl = document.createElement('div');
dayEl.className = 'calendar-day';
dayEl.textContent = day;
if (isThisMonth && day === today.getDate()) {
dayEl.classList.add('today');
}
dayEl.addEventListener('click', () => {
document.querySelectorAll('.calendar-day.selected').forEach(d => d.classList.remove('selected'));
dayEl.classList.add('selected');
selectedDate = new Date(year, month, day);
});
grid.appendChild(dayEl);
}
}
function prevMonth() {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1);
renderCalendar();
}
function nextMonth() {
currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1);
renderCalendar();
}
function selectTimeSlot(el) {
if (el.classList.contains('booked')) return;
document.querySelectorAll('.time-slot.selected').forEach(s => s.classList.remove('selected'));
el.classList.add('selected');
selectedTimeSlot = el.textContent;
}
function bookAppointment() {
if (!selectedDate || !selectedTimeSlot) {
alert('Bitte waehlen Sie ein Datum und eine Uhrzeit aus.');
return;
}
const dateStr = selectedDate.toLocaleDateString('de-DE');
alert('Termin gebucht: ' + dateStr + ' um ' + selectedTimeSlot + ' Uhr\\n\\nEinladung wird versendet...');
closeLettersSubpanel();
}
// ==========================================
// LEGAL ASSISTANT
// ==========================================
function selectLetterType(type) {
currentLetterType = type;
document.querySelectorAll('.letter-type-btn').forEach(btn => {
btn.classList.remove('active');
});
const btn = document.querySelector(`.letter-type-btn[data-type="${type}"]`);
if (btn) btn.classList.add('active');
updateLegalReferences(type);
}
function selectTone(tone) {
currentTone = tone;
document.querySelectorAll('.tone-btn').forEach(btn => {
btn.classList.remove('active');
});
const btn = document.querySelector(`.tone-btn[data-tone="${tone}"]`);
if (btn) btn.classList.add('active');
}
function analyzeLetterText() {
clearTimeout(analysisTimeout);
analysisTimeout = setTimeout(() => {
const content = document.getElementById('letter-content')?.value || '';
const charCount = content.length;
document.getElementById('letter-char-count').textContent = charCount;
if (charCount < 20) {
document.getElementById('gfk-score-display').textContent = '--';
document.getElementById('gfk-score-bar-fill').style.width = '0%';
return;
}
// Calculate GFK Score
let score = 50;
const suggestions = [];
GFK_POSITIVE_PATTERNS.forEach(pattern => {
if (pattern.test(content)) score += 7;
});
GFK_NEGATIVE_PATTERNS.forEach(pattern => {
if (pattern.test(content)) {
score -= 10;
const match = content.match(pattern);
if (match) {
suggestions.push({
type: 'warning',
text: '"' + match[0] + '" koennte wertend wirken.'
});
}
}
});
const iStatements = (content.match(/\\bich\\b/gi) || []).length;
if (iStatements > 2) {
score += 10;
suggestions.push({ type: 'positive', text: 'Gut: Sie verwenden Ich-Botschaften.' });
}
if ((content.match(/\\?/g) || []).length > 0) {
score += 5;
suggestions.push({ type: 'positive', text: 'Gut: Offene Fragen foerdern den Dialog.' });
}
score = Math.max(0, Math.min(100, score));
document.getElementById('gfk-score-display').textContent = score;
document.getElementById('gfk-score-bar-fill').style.width = score + '%';
const bar = document.getElementById('gfk-score-bar-fill');
if (score >= 70) bar.style.background = 'var(--bp-success)';
else if (score >= 40) bar.style.background = 'var(--bp-warning)';
else bar.style.background = 'var(--bp-danger)';
updateSuggestions(suggestions);
}, 500);
}
function updateSuggestions(suggestions) {
const list = document.getElementById('suggestions-list');
if (!list) return;
if (!suggestions.length) {
list.innerHTML = `
<div class="suggestion-item positive">
<div class="suggestion-type">&#9989; Tipp</div>
<div class="suggestion-text">Verwenden Sie Ich-Botschaften und beschreiben Sie konkrete Beobachtungen.</div>
</div>
`;
return;
}
list.innerHTML = suggestions.map(s => `
<div class="suggestion-item ${s.type === 'positive' ? 'positive' : ''}">
<div class="suggestion-type">${s.type === 'positive' ? '&#9989; GUT' : '&#9888; HINWEIS'}</div>
<div class="suggestion-text">${s.text}</div>
</div>
`).join('');
}
function updateLegalReferences(type) {
const refs = {
general: [{ law: 'SchulG &sect; 42', desc: 'Informationspflicht der Schule' }],
behavior: [
{ law: 'SchulG &sect; 53', desc: 'Erziehungs- und Ordnungsmassnahmen' },
{ law: 'AOGS &sect; 2', desc: 'Verfahren bei Ordnungsmassnahmen' }
],
academic: [
{ law: 'SchulG &sect; 44', desc: 'Information ueber Leistungsentwicklung' },
{ law: 'APO-SI &sect; 7', desc: 'Versetzung und Foerderung' }
],
attendance: [
{ law: 'SchulG &sect; 43', desc: 'Schulpflicht und Teilnahmepflicht' },
{ law: 'SchulG &sect; 41', desc: 'Verantwortung der Erziehungsberechtigten' }
],
meeting: [{ law: 'SchulG &sect; 44', desc: 'Elternberatung und -gespraeche' }],
positive: [{ law: 'SchulG &sect; 2', desc: 'Foerderung individueller Faehigkeiten' }]
};
const container = document.getElementById('legal-refs-container');
if (!container) return;
const typeRefs = refs[type] || refs.general;
container.innerHTML = typeRefs.map(ref => `
<div class="legal-ref-item">
<span class="legal-ref-law">${ref.law}</span>
<div>${ref.desc}</div>
</div>
`).join('');
}
function loadLetterTemplate(templateId) {
const template = LETTER_TEMPLATES[templateId];
if (!template) return;
const student = document.getElementById('letter-student')?.value || '[SCHUELER]';
const classVal = document.getElementById('letter-class')?.value || '[KLASSE]';
let content = template.content
.replace(/\\[SCHUELER\\]/g, student)
.replace(/\\[KLASSE\\]/g, classVal);
document.getElementById('letter-subject').value = template.subject.replace('[SCHUELER]', student);
document.getElementById('letter-content').value = content;
analyzeLetterText();
}
// Improve letter with AI via /api/letters/improve
async function improveWithAI() {
const content = document.getElementById('letter-content')?.value || '';
if (content.length < 20) {
alert('Bitte geben Sie zuerst einen Text ein.');
return;
}
setLettersStatus('Verbessere mit KI...', 'GFK-Analyse', 'busy');
try {
const resp = await fetch('/api/letters/improve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: content,
communication_type: currentLetterType,
tone: currentTone
})
});
if (!resp.ok) {
throw new Error('API-Fehler: ' + resp.status);
}
const result = await resp.json();
// Update GFK score display
const score = Math.round((result.gfk_score || 0.5) * 100);
document.getElementById('gfk-score-display').textContent = score;
document.getElementById('gfk-score-bar-fill').style.width = score + '%';
// Update bar color based on score
const bar = document.getElementById('gfk-score-bar-fill');
if (score >= 70) bar.style.background = 'var(--bp-success)';
else if (score >= 40) bar.style.background = 'var(--bp-warning)';
else bar.style.background = 'var(--bp-danger)';
// Show improvements/suggestions
const changes = result.changes || [];
const suggestions = changes.map(change => ({
type: change.includes('Gut') || change.includes('positiv') ? 'positive' : 'warning',
text: change
}));
updateSuggestions(suggestions);
// If improved content is different, offer to replace
if (result.improved_content && result.improved_content !== content) {
if (confirm('Der verbesserte Text liegt vor. Moechten Sie ihn uebernehmen?')) {
document.getElementById('letter-content').value = result.improved_content;
analyzeLetterText();
}
}
setLettersStatus('GFK-Analyse abgeschlossen', 'Score: ' + score + '%');
} catch (e) {
console.error('AI improvement error:', e);
setLettersStatus('Verbesserung fehlgeschlagen', e.message, 'error');
// Fallback to local analysis
analyzeLetterText();
}
}
// Show letter preview in modal
function showLetterPreview() {
const student = document.getElementById('letter-student')?.value || '[Schueler/in]';
const className = document.getElementById('letter-class')?.value || '[Klasse]';
const subject = document.getElementById('letter-subject')?.value || '[Betreff]';
const content = document.getElementById('letter-content')?.value || '';
const previewHtml = `
<div style="font-family: 'Times New Roman', serif; max-width: 600px; margin: 0 auto; padding: 40px; background: white; color: black;">
<div style="text-align: right; margin-bottom: 30px;">
<div style="font-weight: bold;">Schule XY</div>
<div>Musterstrasse 1</div>
<div>12345 Musterstadt</div>
<div style="margin-top: 10px;">${new Date().toLocaleDateString('de-DE')}</div>
</div>
<div style="margin-bottom: 30px;">
<div>Familie von ${student}</div>
<div>Klasse ${className}</div>
</div>
<div style="font-weight: bold; margin-bottom: 20px;">
Betreff: ${subject}
</div>
<div style="white-space: pre-wrap; line-height: 1.6;">
${content}
</div>
</div>
`;
// Create preview modal
const modal = document.createElement('div');
modal.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 20px;';
modal.innerHTML = `
<div style="background: white; max-height: 90vh; overflow-y: auto; border-radius: 8px; position: relative;">
<button onclick="this.parentElement.parentElement.remove()" style="position: absolute; top: 10px; right: 10px; background: #333; color: white; border: none; width: 30px; height: 30px; border-radius: 50%; cursor: pointer; font-size: 18px;">×</button>
${previewHtml}
</div>
`;
document.body.appendChild(modal);
}
// Export letter as PDF via /api/letters/export-pdf
async function exportLetterPDF() {
const student = document.getElementById('letter-student')?.value || 'Unbekannt';
const className = document.getElementById('letter-class')?.value || '';
const subject = document.getElementById('letter-subject')?.value || 'Elternbrief';
const content = document.getElementById('letter-content')?.value || '';
if (content.length < 20) {
alert('Bitte geben Sie zuerst einen Text ein.');
return;
}
setLettersStatus('Erstelle PDF...', '', 'busy');
try {
const resp = await fetch('/api/letters/export-pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
letter_data: {
recipient_name: 'Familie ' + student,
recipient_address: '',
student_name: student,
student_class: className,
subject: subject,
content: content,
letter_type: currentLetterType,
tone: currentTone,
teacher_name: 'Klassenlehrerin',
teacher_title: ''
}
})
});
if (!resp.ok) {
throw new Error('PDF-Export fehlgeschlagen: ' + resp.status);
}
// Download PDF
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `Elternbrief_${student.replace(/\\s+/g, '_')}_${new Date().toISOString().slice(0,10)}.pdf`;
a.click();
URL.revokeObjectURL(url);
setLettersStatus('PDF erstellt', 'Download gestartet');
} catch (e) {
console.error('PDF export error:', e);
setLettersStatus('PDF-Export fehlgeschlagen', e.message, 'error');
alert('PDF-Export fehlgeschlagen: ' + e.message);
}
}
// Save letter as template via /api/letters
async function saveLetterTemplate() {
const student = document.getElementById('letter-student')?.value || '';
const className = document.getElementById('letter-class')?.value || '';
const subject = document.getElementById('letter-subject')?.value || '';
const content = document.getElementById('letter-content')?.value || '';
if (content.length < 20) {
alert('Bitte geben Sie zuerst einen Text ein.');
return;
}
const name = prompt('Name fuer die Vorlage:', subject);
if (!name) return;
setLettersStatus('Speichere Vorlage...', '', 'busy');
try {
const resp = await fetch('/api/letters/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipient_name: 'Familie ' + (student || '[SCHUELER]'),
recipient_address: '',
student_name: student || '[SCHUELER]',
student_class: className || '[KLASSE]',
subject: name,
content: content,
letter_type: currentLetterType,
tone: currentTone,
teacher_name: '[LEHRER]',
teacher_title: ''
})
});
if (!resp.ok) {
throw new Error('Speichern fehlgeschlagen: ' + resp.status);
}
const result = await resp.json();
setLettersStatus('Vorlage gespeichert', 'ID: ' + result.id);
alert('Vorlage "' + name + '" wurde gespeichert.');
} catch (e) {
console.error('Save template error:', e);
setLettersStatus('Speichern fehlgeschlagen', e.message, 'error');
alert('Speichern fehlgeschlagen: ' + e.message);
}
}
// ==========================================
// SHOW PANEL
// ==========================================
function showLettersPanel() {
console.log('showLettersPanel called');
hideAllPanels();
if (typeof hideStudioSubMenu === 'function') hideStudioSubMenu();
const panel = document.getElementById('panel-letters');
if (panel) {
panel.style.display = 'flex';
loadLettersModule();
console.log('Letters panel shown');
}
}
// Escape key handler
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && document.querySelector('.letters-subpanel.active')) {
closeLettersSubpanel();
}
});
"""
def get_letters_module() -> dict:
"""Gibt das komplette Elternkommunikation-Modul als Dictionary zurueck."""
module = LettersModule()
return {
'css': module.get_css(),
'html': module.get_html(),
'js': module.get_js(),
'init_function': 'loadLettersModule'
}