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>
1953 lines
55 KiB
Python
1953 lines
55 KiB
Python
"""
|
||
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">💬</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">→</div>
|
||
</div>
|
||
|
||
<!-- Elterngespraech planen -->
|
||
<div class="letter-tile planning" onclick="openLettersSubpanel('planning')">
|
||
<div class="tile-icon-wrapper planning">📅</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">→</div>
|
||
</div>
|
||
|
||
<!-- Elternbriefe mit Legal Assistant -->
|
||
<div class="letter-tile legal" onclick="openLettersSubpanel('legal')">
|
||
<div class="tile-icon-wrapper legal">⚖</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">→</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()">←</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>📝 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()">💾 Entwurf speichern</button>
|
||
<button class="btn btn-ghost" onclick="exportConversation('pdf')">📄 Als PDF exportieren</button>
|
||
<button class="btn btn-primary" onclick="saveConversation()">✔ 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()">←</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()">‹</button>
|
||
<button onclick="nextMonth()">›</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>🕑 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()">📅 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()">←</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">📝 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">📄</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">👤</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">📚</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">📅</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">📆</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">⭐</div>
|
||
<div class="letter-type-label">Positives</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tone Selection -->
|
||
<div class="editor-section">
|
||
<div class="editor-section-title">🎨 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">🗒 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()">✨ Mit KI verbessern</button>
|
||
<button class="btn btn-ghost" onclick="showLetterPreview()">👁 Vorschau</button>
|
||
<button class="btn btn-ghost" onclick="exportLetterPDF()">📄 PDF Export</button>
|
||
<button class="btn btn-ghost" onclick="saveLetterTemplate()">💾 Als Vorlage</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Legal Assistant Panel -->
|
||
<div class="legal-assistant-panel">
|
||
<div class="legal-assistant-header">
|
||
<div class="legal-icon-box">⚖</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">💡 Verbesserungsvorschlaege</div>
|
||
<div class="suggestions-list" id="suggestions-list">
|
||
<div class="suggestion-item positive">
|
||
<div class="suggestion-type">✅ 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">📋 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 →</span>
|
||
</div>
|
||
<div class="template-item" onclick="loadLetterTemplate('fehlzeiten')">
|
||
<span class="template-name">Fehlzeiten-Hinweis</span>
|
||
<span class="template-use-btn">Verwenden →</span>
|
||
</div>
|
||
<div class="template-item" onclick="loadLetterTemplate('elternabend')">
|
||
<span class="template-name">Einladung Elternabend</span>
|
||
<span class="template-use-btn">Verwenden →</span>
|
||
</div>
|
||
<div class="template-item" onclick="loadLetterTemplate('lob')">
|
||
<span class="template-name">Positive Rueckmeldung</span>
|
||
<span class="template-use-btn">Verwenden →</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Legal References -->
|
||
<div class="legal-refs-section">
|
||
<div class="legal-refs-title">⚖ Rechtliche Hinweise</div>
|
||
<div id="legal-refs-container">
|
||
<div class="legal-ref-item">
|
||
<span class="legal-ref-law">SchulG NRW § 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">✅ 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' ? '✅ GUT' : '⚠ HINWEIS'}</div>
|
||
<div class="suggestion-text">${s.text}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function updateLegalReferences(type) {
|
||
const refs = {
|
||
general: [{ law: 'SchulG § 42', desc: 'Informationspflicht der Schule' }],
|
||
behavior: [
|
||
{ law: 'SchulG § 53', desc: 'Erziehungs- und Ordnungsmassnahmen' },
|
||
{ law: 'AOGS § 2', desc: 'Verfahren bei Ordnungsmassnahmen' }
|
||
],
|
||
academic: [
|
||
{ law: 'SchulG § 44', desc: 'Information ueber Leistungsentwicklung' },
|
||
{ law: 'APO-SI § 7', desc: 'Versetzung und Foerderung' }
|
||
],
|
||
attendance: [
|
||
{ law: 'SchulG § 43', desc: 'Schulpflicht und Teilnahmepflicht' },
|
||
{ law: 'SchulG § 41', desc: 'Verantwortung der Erziehungsberechtigten' }
|
||
],
|
||
meeting: [{ law: 'SchulG § 44', desc: 'Elternberatung und -gespraeche' }],
|
||
positive: [{ law: 'SchulG § 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'
|
||
}
|