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
Benjamin Admin 21a844cb8a 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

2149 lines
64 KiB
Python

"""
BreakPilot Studio - Messenger Modul
Funktionen:
- Matrix Chat Integration (DSGVO-konform)
- Direktnachrichten an Eltern
- Gruppenchats (Klassen, Fachschaften)
- Vorlagen fuer haeufige Nachrichten
- Dateiaustausch
- Lesebestaetigungen
"""
class MessengerModule:
"""Modul fuer sichere Kommunikation mit Matrix-Integration."""
@staticmethod
def get_css() -> str:
"""CSS fuer das Messenger-Modul."""
return """
/* =============================================
MESSENGER MODULE - Matrix Chat Integration
============================================= */
/* Panel Layout */
.panel-messenger {
display: none;
flex-direction: column;
height: 100%;
background: var(--bp-bg);
overflow: hidden;
}
.panel-messenger.active {
display: flex;
}
/* Messenger Container */
.messenger-container {
display: flex;
flex: 1;
overflow: hidden;
}
/* Left Sidebar - Conversations */
.messenger-sidebar {
width: 320px;
border-right: 1px solid var(--bp-border);
display: flex;
flex-direction: column;
background: var(--bp-surface);
}
.messenger-sidebar-header {
padding: 20px;
border-bottom: 1px solid var(--bp-border);
}
.messenger-sidebar-title {
font-size: 18px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 16px;
}
/* Search Box */
.messenger-search {
position: relative;
}
.messenger-search input {
width: 100%;
padding: 12px 16px 12px 40px;
border: 1px solid var(--bp-border);
border-radius: 10px;
background: var(--bp-bg);
color: var(--bp-text);
font-size: 14px;
}
.messenger-search input:focus {
outline: none;
border-color: var(--bp-primary);
}
.messenger-search-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--bp-text-muted);
font-size: 16px;
}
/* Tab Navigation */
.messenger-tabs {
display: flex;
padding: 12px 20px;
gap: 8px;
border-bottom: 1px solid var(--bp-border);
}
.messenger-tab {
flex: 1;
padding: 10px 16px;
border: none;
border-radius: 8px;
background: transparent;
color: var(--bp-text-muted);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.messenger-tab:hover {
background: var(--bp-surface-elevated);
}
.messenger-tab.active {
background: var(--bp-primary);
color: white;
}
.messenger-tab-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 6px;
margin-left: 6px;
border-radius: 9px;
background: #ef4444;
color: white;
font-size: 11px;
font-weight: 600;
}
/* Conversation List */
.messenger-conversations {
flex: 1;
overflow-y: auto;
}
.messenger-conversation {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
cursor: pointer;
transition: background 0.2s;
border-bottom: 1px solid var(--bp-border-subtle);
}
.messenger-conversation:hover {
background: var(--bp-surface-elevated);
}
.messenger-conversation.active {
background: var(--bp-primary-soft);
border-left: 3px solid var(--bp-primary);
}
.messenger-conversation.unread {
background: rgba(59, 130, 246, 0.05);
}
.messenger-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--bp-surface-elevated);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
position: relative;
}
.messenger-avatar.online::after {
content: '';
position: absolute;
bottom: 2px;
right: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #10b981;
border: 2px solid var(--bp-surface);
}
.messenger-avatar.group {
border-radius: 12px;
background: var(--bp-primary-soft);
}
.messenger-conversation-content {
flex: 1;
min-width: 0;
}
.messenger-conversation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.messenger-conversation-name {
font-size: 14px;
font-weight: 600;
color: var(--bp-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.messenger-conversation-time {
font-size: 11px;
color: var(--bp-text-muted);
flex-shrink: 0;
}
.messenger-conversation-preview {
font-size: 13px;
color: var(--bp-text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.messenger-conversation.unread .messenger-conversation-preview {
color: var(--bp-text);
font-weight: 500;
}
.messenger-unread-badge {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--bp-primary);
color: white;
font-size: 11px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* New Conversation Button */
.messenger-new-btn {
margin: 16px 20px;
padding: 14px;
border: none;
border-radius: 12px;
background: var(--bp-primary);
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
}
.messenger-new-btn:hover {
background: var(--bp-primary-hover);
transform: translateY(-1px);
}
/* Chat Area */
.messenger-chat {
flex: 1;
display: flex;
flex-direction: column;
background: var(--bp-bg);
}
.messenger-chat-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-bottom: 1px solid var(--bp-border);
background: var(--bp-surface);
}
.messenger-chat-info {
display: flex;
align-items: center;
gap: 12px;
}
.messenger-chat-name {
font-size: 16px;
font-weight: 600;
color: var(--bp-text);
}
.messenger-chat-status {
font-size: 12px;
color: var(--bp-text-muted);
}
.messenger-chat-status.online {
color: #10b981;
}
.messenger-chat-actions {
display: flex;
gap: 8px;
}
.messenger-action-btn {
width: 40px;
height: 40px;
border: none;
border-radius: 10px;
background: var(--bp-surface-elevated);
color: var(--bp-text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
transition: all 0.2s;
}
.messenger-action-btn:hover {
background: var(--bp-primary-soft);
color: var(--bp-primary);
}
/* Messages Area */
.messenger-messages {
flex: 1;
overflow-y: auto;
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.messenger-message {
display: flex;
gap: 12px;
max-width: 70%;
}
.messenger-message.sent {
align-self: flex-end;
flex-direction: row-reverse;
}
.messenger-message-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--bp-surface-elevated);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.messenger-message-content {
display: flex;
flex-direction: column;
gap: 4px;
}
.messenger-message-bubble {
padding: 12px 16px;
border-radius: 18px;
background: var(--bp-surface);
color: var(--bp-text);
font-size: 14px;
line-height: 1.5;
border: 1px solid var(--bp-border);
}
.messenger-message.sent .messenger-message-bubble {
background: var(--bp-primary);
color: white;
border-color: var(--bp-primary);
border-bottom-right-radius: 4px;
}
.messenger-message:not(.sent) .messenger-message-bubble {
border-bottom-left-radius: 4px;
}
.messenger-message-meta {
display: flex;
gap: 8px;
font-size: 11px;
color: var(--bp-text-muted);
padding: 0 8px;
}
.messenger-message.sent .messenger-message-meta {
justify-content: flex-end;
}
.messenger-message-status {
color: #10b981;
}
/* Message Input */
.messenger-input-area {
padding: 16px 24px;
border-top: 1px solid var(--bp-border);
background: var(--bp-surface);
}
.messenger-input-wrapper {
display: flex;
gap: 12px;
align-items: flex-end;
}
.messenger-input-actions {
display: flex;
gap: 4px;
}
.messenger-input-btn {
width: 40px;
height: 40px;
border: none;
border-radius: 10px;
background: transparent;
color: var(--bp-text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: all 0.2s;
}
.messenger-input-btn:hover {
background: var(--bp-surface-elevated);
color: var(--bp-primary);
}
.messenger-input {
flex: 1;
padding: 12px 16px;
border: 1px solid var(--bp-border);
border-radius: 20px;
background: var(--bp-bg);
color: var(--bp-text);
font-size: 14px;
resize: none;
min-height: 44px;
max-height: 120px;
line-height: 1.4;
}
.messenger-input:focus {
outline: none;
border-color: var(--bp-primary);
}
.messenger-send-btn {
width: 44px;
height: 44px;
border: none;
border-radius: 50%;
background: var(--bp-primary);
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: all 0.2s;
}
.messenger-send-btn:hover {
background: var(--bp-primary-hover);
transform: scale(1.05);
}
.messenger-send-btn:disabled {
background: var(--bp-surface-elevated);
color: var(--bp-text-muted);
cursor: not-allowed;
transform: none;
}
/* Empty State */
.messenger-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px;
text-align: center;
color: var(--bp-text-muted);
}
.messenger-empty-icon {
font-size: 64px;
margin-bottom: 24px;
opacity: 0.5;
}
.messenger-empty h2 {
font-size: 20px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 8px;
}
.messenger-empty p {
font-size: 14px;
max-width: 400px;
margin-bottom: 24px;
}
/* Templates Panel */
.messenger-templates {
position: absolute;
bottom: 80px;
left: 24px;
right: 24px;
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 16px;
padding: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
display: none;
}
.messenger-templates.active {
display: block;
}
.messenger-templates-title {
font-size: 14px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 12px;
}
.messenger-template-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 200px;
overflow-y: auto;
}
.messenger-template-item {
padding: 12px;
border-radius: 10px;
background: var(--bp-bg);
cursor: pointer;
transition: all 0.2s;
}
.messenger-template-item:hover {
background: var(--bp-primary-soft);
}
.messenger-template-name {
font-size: 13px;
font-weight: 500;
color: var(--bp-text);
margin-bottom: 4px;
}
.messenger-template-preview {
font-size: 12px;
color: var(--bp-text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Connection Status */
.messenger-connection {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
background: var(--bp-surface-elevated);
border-bottom: 1px solid var(--bp-border);
font-size: 12px;
color: var(--bp-text-muted);
}
.messenger-connection-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #10b981;
}
.messenger-connection-dot.connecting {
background: #f59e0b;
animation: pulse 1s infinite;
}
.messenger-connection-dot.offline {
background: #ef4444;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Date Separator */
.messenger-date-separator {
display: flex;
align-items: center;
gap: 16px;
margin: 16px 0;
}
.messenger-date-separator::before,
.messenger-date-separator::after {
content: '';
flex: 1;
height: 1px;
background: var(--bp-border);
}
.messenger-date-separator span {
font-size: 12px;
color: var(--bp-text-muted);
padding: 4px 12px;
background: var(--bp-surface-elevated);
border-radius: 12px;
}
/* Typing Indicator */
.messenger-typing {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
font-size: 12px;
color: var(--bp-text-muted);
}
.messenger-typing-dots {
display: flex;
gap: 4px;
}
.messenger-typing-dots span {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--bp-text-muted);
animation: typingDot 1.4s infinite;
}
.messenger-typing-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.messenger-typing-dots span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typingDot {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-4px); }
}
/* Modal Styles */
.messenger-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.messenger-modal {
background: var(--bp-surface);
border-radius: 16px;
width: 90%;
max-width: 480px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.messenger-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid var(--bp-border);
}
.messenger-modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--bp-text);
}
.messenger-modal-close {
width: 32px;
height: 32px;
border: none;
background: var(--bp-surface-elevated);
border-radius: 8px;
font-size: 20px;
color: var(--bp-text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.messenger-modal-close:hover {
background: var(--bp-primary-soft);
color: var(--bp-primary);
}
.messenger-modal-search {
padding: 16px 20px;
border-bottom: 1px solid var(--bp-border);
}
.messenger-modal-search input {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--bp-border);
border-radius: 10px;
background: var(--bp-bg);
color: var(--bp-text);
font-size: 14px;
}
.messenger-modal-search input:focus {
outline: none;
border-color: var(--bp-primary);
}
.messenger-modal-content {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.messenger-contact-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
cursor: pointer;
transition: background 0.2s;
}
.messenger-contact-item:hover {
background: var(--bp-surface-elevated);
}
.messenger-contact-info {
flex: 1;
}
.messenger-contact-name {
font-size: 14px;
font-weight: 500;
color: var(--bp-text);
}
.messenger-contact-role {
font-size: 12px;
color: var(--bp-text-muted);
}
/* Admin Tab Styles */
.messenger-admin-tab {
padding: 8px 16px;
background: var(--bp-surface-elevated);
color: var(--bp-text-muted);
border: none;
border-radius: 8px;
font-size: 12px;
cursor: pointer;
margin: 8px 20px;
}
.messenger-admin-tab:hover {
background: var(--bp-primary-soft);
color: var(--bp-primary);
}
"""
@staticmethod
def get_html() -> str:
"""HTML fuer das Messenger-Modul."""
return """
<!-- Messenger Panel -->
<div id="panel-messenger" class="panel-messenger" style="display: none;">
<div class="messenger-container">
<!-- Left Sidebar -->
<div class="messenger-sidebar">
<div class="messenger-sidebar-header">
<div class="messenger-sidebar-title">Nachrichten</div>
<div class="messenger-search">
<span class="messenger-search-icon">&#128269;</span>
<input type="text" placeholder="Suchen..." id="messenger-search-input">
</div>
</div>
<!-- Connection Status -->
<div class="messenger-connection">
<span class="messenger-connection-dot" id="messenger-connection-dot"></span>
<span id="messenger-connection-text">Verbunden mit Matrix</span>
</div>
<!-- Tabs -->
<div class="messenger-tabs">
<button class="messenger-tab active" data-tab="all" onclick="switchMessengerTab('all')">
Alle
<span class="messenger-tab-badge" id="messenger-all-badge" style="display: none;">0</span>
</button>
<button class="messenger-tab" data-tab="parents" onclick="switchMessengerTab('parents')">
Eltern
</button>
<button class="messenger-tab" data-tab="groups" onclick="switchMessengerTab('groups')">
Gruppen
</button>
</div>
<!-- Conversation List -->
<div class="messenger-conversations" id="messenger-conversations">
<!-- Demo Conversations -->
<div class="messenger-conversation unread" data-id="1" onclick="openConversation(1)">
<div class="messenger-avatar online">&#128113;</div>
<div class="messenger-conversation-content">
<div class="messenger-conversation-header">
<span class="messenger-conversation-name">Familie Mueller</span>
<span class="messenger-conversation-time">10:24</span>
</div>
<div class="messenger-conversation-preview">Vielen Dank fuer die Rueckmeldung...</div>
</div>
<span class="messenger-unread-badge">2</span>
</div>
<div class="messenger-conversation" data-id="2" onclick="openConversation(2)">
<div class="messenger-avatar online">&#128106;</div>
<div class="messenger-conversation-content">
<div class="messenger-conversation-header">
<span class="messenger-conversation-name">Familie Schmidt</span>
<span class="messenger-conversation-time">Gestern</span>
</div>
<div class="messenger-conversation-preview">Ok, bis morgen dann!</div>
</div>
</div>
<div class="messenger-conversation" data-id="3" onclick="openConversation(3)">
<div class="messenger-avatar group">&#128101;</div>
<div class="messenger-conversation-content">
<div class="messenger-conversation-header">
<span class="messenger-conversation-name">Klasse 10a</span>
<span class="messenger-conversation-time">Mo</span>
</div>
<div class="messenger-conversation-preview">Reminder: Klassenfahrt naechste Woche</div>
</div>
</div>
<div class="messenger-conversation" data-id="4" onclick="openConversation(4)">
<div class="messenger-avatar group">&#128218;</div>
<div class="messenger-conversation-content">
<div class="messenger-conversation-header">
<span class="messenger-conversation-name">Fachschaft Deutsch</span>
<span class="messenger-conversation-time">Fr</span>
</div>
<div class="messenger-conversation-preview">Neue Lektuere-Empfehlungen</div>
</div>
</div>
</div>
<!-- New Conversation Button -->
<button class="messenger-new-btn" onclick="showNewConversationModal()">
<span>&#10133;</span> Neue Nachricht
</button>
<!-- Admin Button -->
<button class="messenger-admin-tab" onclick="showMessengerAdmin()">
&#9881; Kontakte verwalten
</button>
</div>
<!-- Chat Area -->
<div class="messenger-chat" id="messenger-chat">
<!-- Empty State (shown when no conversation selected) -->
<div class="messenger-empty" id="messenger-empty-state">
<div class="messenger-empty-icon">&#128172;</div>
<h2>Sichere Kommunikation</h2>
<p>Waehle eine Konversation aus oder starte eine neue Nachricht. Alle Nachrichten sind DSGVO-konform verschluesselt.</p>
<button class="messenger-new-btn" onclick="showNewConversationModal()">
<span>&#10133;</span> Neue Nachricht
</button>
</div>
<!-- Chat Content (hidden by default) -->
<div id="messenger-chat-content" style="display: none; flex-direction: column; flex: 1;">
<!-- Chat Header -->
<div class="messenger-chat-header">
<div class="messenger-chat-info">
<div class="messenger-avatar" id="messenger-chat-avatar">&#128113;</div>
<div>
<div class="messenger-chat-name" id="messenger-chat-name">Familie Mueller</div>
<div class="messenger-chat-status online" id="messenger-chat-status">Online</div>
</div>
</div>
<div class="messenger-chat-actions">
<button class="messenger-action-btn" title="Videoanruf" onclick="startVideoCall()">&#127909;</button>
<button class="messenger-action-btn" title="Telefonanruf" onclick="startVoiceCall()">&#128222;</button>
<button class="messenger-action-btn" title="Info" onclick="showConversationInfo()">&#9432;</button>
</div>
</div>
<!-- Messages -->
<div class="messenger-messages" id="messenger-messages">
<div class="messenger-date-separator">
<span>Heute</span>
</div>
<div class="messenger-message">
<div class="messenger-message-avatar">&#128113;</div>
<div class="messenger-message-content">
<div class="messenger-message-bubble">
Guten Morgen! Ich wollte mich erkundigen, wie es mit den Hausaufgaben von Max laeuft?
</div>
<div class="messenger-message-meta">
<span>10:15</span>
</div>
</div>
</div>
<div class="messenger-message sent">
<div class="messenger-message-content">
<div class="messenger-message-bubble">
Guten Morgen Frau Mueller! Max macht gute Fortschritte. Die letzten Aufgaben waren sehr gut erledigt.
</div>
<div class="messenger-message-meta">
<span>10:20</span>
<span class="messenger-message-status">&#10003;&#10003;</span>
</div>
</div>
</div>
<div class="messenger-message">
<div class="messenger-message-avatar">&#128113;</div>
<div class="messenger-message-content">
<div class="messenger-message-bubble">
Das freut mich sehr zu hoeren! Vielen Dank fuer die Rueckmeldung. Sollen wir vielleicht einen Termin fuer ein Elterngespraech vereinbaren?
</div>
<div class="messenger-message-meta">
<span>10:24</span>
</div>
</div>
</div>
</div>
<!-- Typing Indicator -->
<div class="messenger-typing" id="messenger-typing" style="display: none;">
<div class="messenger-typing-dots">
<span></span><span></span><span></span>
</div>
<span id="messenger-typing-name">Familie Mueller tippt...</span>
</div>
<!-- Input Area -->
<div class="messenger-input-area">
<div class="messenger-input-wrapper">
<div class="messenger-input-actions">
<button class="messenger-input-btn" title="Datei anhaengen" onclick="attachFile()">&#128206;</button>
<button class="messenger-input-btn" title="Vorlagen" onclick="toggleTemplates()">&#128203;</button>
</div>
<textarea
class="messenger-input"
id="messenger-input"
placeholder="Nachricht schreiben..."
rows="1"
onkeydown="handleMessageKeydown(event)"
oninput="adjustTextareaHeight(this)"
></textarea>
<button class="messenger-send-btn" id="messenger-send-btn" onclick="sendMessage()" disabled>
&#10148;
</button>
</div>
<!-- Templates Panel -->
<div class="messenger-templates" id="messenger-templates">
<div class="messenger-templates-title">Schnellvorlagen</div>
<div class="messenger-template-list">
<div class="messenger-template-item" onclick="useTemplate(0)">
<div class="messenger-template-name">Terminbestaetigung</div>
<div class="messenger-template-preview">Vielen Dank fuer Ihre Terminanfrage. Ich bestaet...</div>
</div>
<div class="messenger-template-item" onclick="useTemplate(1)">
<div class="messenger-template-name">Hausaufgaben-Info</div>
<div class="messenger-template-preview">Zur Information: Die Hausaufgaben fuer diese Woche...</div>
</div>
<div class="messenger-template-item" onclick="useTemplate(2)">
<div class="messenger-template-name">Entschuldigung bestaetigen</div>
<div class="messenger-template-preview">Ich bestaettige den Erhalt der Entschuldigung...</div>
</div>
<div class="messenger-template-item" onclick="useTemplate(3)">
<div class="messenger-template-name">Gespraechsanfrage</div>
<div class="messenger-template-preview">Ich wuerde gerne einen Termin fuer ein Gespraech...</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
"""JavaScript fuer das Messenger-Modul."""
return """
// =============================================
// MESSENGER MODULE - Matrix Chat Integration
// =============================================
let messengerInitialized = false;
let currentConversationId = null;
let messengerSocket = null;
let messengerContacts = [];
let messengerConversations = [];
let messengerMessages = {};
let messengerTemplates = [];
let messengerGroups = [];
// API Base URL
const MESSENGER_API = '/api/messenger';
// Fallback Templates (wenn API nicht erreichbar)
const defaultTemplates = [
{
id: 'default-1',
name: 'Terminbestaetigung',
content: 'Vielen Dank fuer Ihre Terminanfrage. Ich bestaetige den Termin am [DATUM] um [UHRZEIT]. Bitte geben Sie mir Bescheid, falls sich etwas aendern sollte.',
category: 'termin'
},
{
id: 'default-2',
name: 'Hausaufgaben-Info',
content: 'Zur Information: Die Hausaufgaben fuer diese Woche umfassen [THEMA]. Abgabetermin ist [DATUM]. Bei Fragen stehe ich gerne zur Verfuegung.',
category: 'hausaufgaben'
},
{
id: 'default-3',
name: 'Entschuldigung bestaetigen',
content: 'Ich bestaetige den Erhalt der Entschuldigung fuer [NAME] am [DATUM]. Die Fehlzeiten wurden entsprechend vermerkt.',
category: 'entschuldigung'
},
{
id: 'default-4',
name: 'Gespraechsanfrage',
content: 'Ich wuerde gerne einen Termin fuer ein Gespraech mit Ihnen vereinbaren, um [THEMA] zu besprechen. Waeren Sie am [DATUM] um [UHRZEIT] verfuegbar?',
category: 'gespraech'
}
];
async function loadMessengerModule() {
if (messengerInitialized) {
console.log('Messenger module already initialized');
return;
}
console.log('Loading Messenger Module...');
// Initialize search
const searchInput = document.getElementById('messenger-search-input');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
filterConversations(e.target.value);
});
}
// Initialize message input
const messageInput = document.getElementById('messenger-input');
if (messageInput) {
messageInput.addEventListener('input', () => {
const sendBtn = document.getElementById('messenger-send-btn');
if (sendBtn) {
sendBtn.disabled = !messageInput.value.trim();
}
});
}
// Load data from API
await loadMessengerData();
// Connect to Matrix (mock)
connectToMatrix();
messengerInitialized = true;
console.log('Messenger Module loaded successfully');
}
// Load all messenger data from API
async function loadMessengerData() {
try {
// Load contacts, conversations, templates, groups in parallel
const [contactsRes, convsRes, templatesRes, groupsRes] = await Promise.all([
fetch(`${MESSENGER_API}/contacts`),
fetch(`${MESSENGER_API}/conversations`),
fetch(`${MESSENGER_API}/templates`),
fetch(`${MESSENGER_API}/groups`)
]);
if (contactsRes.ok) {
messengerContacts = await contactsRes.json();
console.log(`Loaded ${messengerContacts.length} contacts`);
}
if (convsRes.ok) {
messengerConversations = await convsRes.json();
console.log(`Loaded ${messengerConversations.length} conversations`);
renderConversationList();
}
if (templatesRes.ok) {
messengerTemplates = await templatesRes.json();
console.log(`Loaded ${messengerTemplates.length} templates`);
} else {
messengerTemplates = defaultTemplates;
}
renderTemplateList();
if (groupsRes.ok) {
messengerGroups = await groupsRes.json();
console.log(`Loaded ${messengerGroups.length} groups`);
}
} catch (error) {
console.error('Error loading messenger data:', error);
messengerTemplates = defaultTemplates;
renderTemplateList();
}
}
// Render conversation list from API data
function renderConversationList() {
const container = document.getElementById('messenger-conversations');
if (!container || messengerConversations.length === 0) return;
// Keep demo conversations if no API data
if (messengerConversations.length === 0) return;
container.innerHTML = messengerConversations.map(conv => {
const isGroup = conv.type === 'group';
const unreadCount = conv.unread_count || 0;
const isUnread = unreadCount > 0;
const avatar = isGroup ? '&#128101;' : '&#128113;';
const lastTime = conv.last_message_time ? formatMessageTime(conv.last_message_time) : '';
return `
<div class="messenger-conversation${isUnread ? ' unread' : ''}"
data-id="${conv.id}"
data-type="${conv.type}"
onclick="openConversation('${conv.id}')">
<div class="messenger-avatar${isGroup ? ' group' : ''}${conv.online ? ' online' : ''}">${avatar}</div>
<div class="messenger-conversation-content">
<div class="messenger-conversation-header">
<span class="messenger-conversation-name">${escapeHtml(conv.name)}</span>
<span class="messenger-conversation-time">${lastTime}</span>
</div>
<div class="messenger-conversation-preview">${escapeHtml(conv.last_message || 'Keine Nachrichten')}</div>
</div>
${isUnread ? `<span class="messenger-unread-badge">${unreadCount}</span>` : ''}
</div>
`;
}).join('');
// Update badge count
const totalUnread = messengerConversations.reduce((sum, c) => sum + (c.unread_count || 0), 0);
const badge = document.getElementById('messenger-all-badge');
if (badge) {
badge.textContent = totalUnread;
badge.style.display = totalUnread > 0 ? 'inline-flex' : 'none';
}
}
// Render template list
function renderTemplateList() {
const container = document.querySelector('.messenger-template-list');
if (!container) return;
const templates = messengerTemplates.length > 0 ? messengerTemplates : defaultTemplates;
container.innerHTML = templates.map((tpl, index) => `
<div class="messenger-template-item" onclick="useTemplate('${tpl.id || index}')">
<div class="messenger-template-name">${escapeHtml(tpl.name)}</div>
<div class="messenger-template-preview">${escapeHtml(tpl.content.substring(0, 50))}...</div>
</div>
`).join('');
}
// Format message time
function formatMessageTime(isoTime) {
const date = new Date(isoTime);
const now = new Date();
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return date.getHours().toString().padStart(2, '0') + ':' +
date.getMinutes().toString().padStart(2, '0');
} else if (diffDays === 1) {
return 'Gestern';
} else if (diffDays < 7) {
const days = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
return days[date.getDay()];
} else {
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
}
}
// Connect to Matrix server (mock)
function connectToMatrix() {
const dot = document.getElementById('messenger-connection-dot');
const text = document.getElementById('messenger-connection-text');
if (dot && text) {
dot.classList.add('connecting');
text.textContent = 'Verbinde mit Matrix...';
// Simulate connection
setTimeout(() => {
dot.classList.remove('connecting');
text.textContent = 'Verbunden mit Matrix';
}, 1500);
}
}
// Switch between tabs
function switchMessengerTab(tab) {
// Update tab buttons
document.querySelectorAll('.messenger-tab').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tab);
});
// Filter conversations
const convs = document.querySelectorAll('.messenger-conversation');
convs.forEach(conv => {
if (tab === 'all') {
conv.style.display = 'flex';
} else if (tab === 'parents') {
const isGroup = conv.querySelector('.messenger-avatar.group');
conv.style.display = isGroup ? 'none' : 'flex';
} else if (tab === 'groups') {
const isGroup = conv.querySelector('.messenger-avatar.group');
conv.style.display = isGroup ? 'flex' : 'none';
}
});
}
// Filter conversations by search
function filterConversations(query) {
const convs = document.querySelectorAll('.messenger-conversation');
const lowerQuery = query.toLowerCase();
convs.forEach(conv => {
const name = conv.querySelector('.messenger-conversation-name')?.textContent.toLowerCase() || '';
const preview = conv.querySelector('.messenger-conversation-preview')?.textContent.toLowerCase() || '';
const matches = name.includes(lowerQuery) || preview.includes(lowerQuery);
conv.style.display = matches ? 'flex' : 'none';
});
}
// Open a conversation
async function openConversation(id) {
currentConversationId = id;
// Update active state
document.querySelectorAll('.messenger-conversation').forEach(conv => {
conv.classList.toggle('active', conv.dataset.id == id);
if (conv.dataset.id == id) {
conv.classList.remove('unread');
const badge = conv.querySelector('.messenger-unread-badge');
if (badge) badge.remove();
}
});
// Show chat content
const emptyState = document.getElementById('messenger-empty-state');
const chatContent = document.getElementById('messenger-chat-content');
if (emptyState) emptyState.style.display = 'none';
if (chatContent) chatContent.style.display = 'flex';
// Update chat header based on conversation
const conv = document.querySelector(`.messenger-conversation[data-id="${id}"]`);
if (conv) {
const name = conv.querySelector('.messenger-conversation-name')?.textContent;
const avatar = conv.querySelector('.messenger-avatar')?.innerHTML;
const isOnline = conv.querySelector('.messenger-avatar.online');
document.getElementById('messenger-chat-name').textContent = name;
document.getElementById('messenger-chat-avatar').innerHTML = avatar;
const status = document.getElementById('messenger-chat-status');
if (status) {
status.textContent = isOnline ? 'Online' : 'Offline';
status.className = 'messenger-chat-status' + (isOnline ? ' online' : '');
}
}
// Load messages from API
await loadConversationMessages(id);
// Focus input
document.getElementById('messenger-input')?.focus();
}
// Load messages for a conversation
async function loadConversationMessages(conversationId) {
const container = document.getElementById('messenger-messages');
if (!container) return;
try {
const response = await fetch(`${MESSENGER_API}/conversations/${conversationId}/messages`);
if (!response.ok) {
console.error('Failed to load messages');
return;
}
const messages = await response.json();
messengerMessages[conversationId] = messages;
// Render messages
if (messages.length === 0) {
container.innerHTML = `
<div class="messenger-date-separator">
<span>Keine Nachrichten</span>
</div>
`;
} else {
renderMessages(messages, container);
}
// Scroll to bottom
container.scrollTop = container.scrollHeight;
} catch (error) {
console.error('Error loading messages:', error);
}
}
// Render messages in container
function renderMessages(messages, container) {
let html = '';
let lastDate = null;
messages.forEach(msg => {
const msgDate = new Date(msg.created_at).toDateString();
// Add date separator if needed
if (msgDate !== lastDate) {
const dateLabel = formatDateLabel(msg.created_at);
html += `
<div class="messenger-date-separator">
<span>${dateLabel}</span>
</div>
`;
lastDate = msgDate;
}
const isSent = msg.sender_id === 'teacher'; // TODO: Get actual user ID
const time = formatMessageTime(msg.created_at);
html += `
<div class="messenger-message${isSent ? ' sent' : ''}">
${!isSent ? '<div class="messenger-message-avatar">&#128113;</div>' : ''}
<div class="messenger-message-content">
<div class="messenger-message-bubble">${escapeHtml(msg.content)}</div>
<div class="messenger-message-meta">
<span>${time}</span>
${isSent && msg.read ? '<span class="messenger-message-status">&#10003;&#10003;</span>' : ''}
${isSent && !msg.read ? '<span class="messenger-message-status">&#10003;</span>' : ''}
</div>
</div>
</div>
`;
});
container.innerHTML = html;
}
// Format date label for message separator
function formatDateLabel(isoTime) {
const date = new Date(isoTime);
const now = new Date();
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
if (diffDays === 0) return 'Heute';
if (diffDays === 1) return 'Gestern';
return date.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long' });
}
// Send a message
async function sendMessage() {
const input = document.getElementById('messenger-input');
const message = input?.value.trim();
if (!message || !currentConversationId) return;
// Disable send button while sending
const sendBtn = document.getElementById('messenger-send-btn');
sendBtn.disabled = true;
try {
// Send message via API
const response = await fetch(`${MESSENGER_API}/conversations/${currentConversationId}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: message,
sender_id: 'teacher' // TODO: Get actual user ID
})
});
if (!response.ok) {
throw new Error('Failed to send message');
}
// Add message to UI
addMessageToUI(message, true);
// Clear input
input.value = '';
input.style.height = 'auto';
// Update conversation preview
updateConversationPreview(currentConversationId, message);
} catch (error) {
console.error('Error sending message:', error);
alert('Nachricht konnte nicht gesendet werden.');
}
sendBtn.disabled = false;
}
// Update conversation preview in sidebar
function updateConversationPreview(convId, message) {
const conv = document.querySelector(`.messenger-conversation[data-id="${convId}"]`);
if (conv) {
const preview = conv.querySelector('.messenger-conversation-preview');
if (preview) {
preview.textContent = message.substring(0, 50) + (message.length > 50 ? '...' : '');
}
const timeEl = conv.querySelector('.messenger-conversation-time');
if (timeEl) {
const now = new Date();
timeEl.textContent = now.getHours().toString().padStart(2, '0') + ':' +
now.getMinutes().toString().padStart(2, '0');
}
}
}
// Add message to UI
function addMessageToUI(text, isSent) {
const container = document.getElementById('messenger-messages');
if (!container) return;
const now = new Date();
const time = now.getHours().toString().padStart(2, '0') + ':' +
now.getMinutes().toString().padStart(2, '0');
const messageHtml = `
<div class="messenger-message${isSent ? ' sent' : ''}">
${!isSent ? '<div class="messenger-message-avatar">&#128113;</div>' : ''}
<div class="messenger-message-content">
<div class="messenger-message-bubble">${escapeHtml(text)}</div>
<div class="messenger-message-meta">
<span>${time}</span>
${isSent ? '<span class="messenger-message-status">&#10003;</span>' : ''}
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', messageHtml);
container.scrollTop = container.scrollHeight;
// Update conversation preview
if (currentConversationId) {
const conv = document.querySelector(`.messenger-conversation[data-id="${currentConversationId}"]`);
if (conv) {
const preview = conv.querySelector('.messenger-conversation-preview');
if (preview) {
preview.textContent = text.substring(0, 50) + (text.length > 50 ? '...' : '');
}
const timeEl = conv.querySelector('.messenger-conversation-time');
if (timeEl) {
timeEl.textContent = time;
}
}
}
}
// Show typing indicator
function showTypingIndicator(show) {
const indicator = document.getElementById('messenger-typing');
if (indicator) {
indicator.style.display = show ? 'flex' : 'none';
}
}
// Handle keyboard in message input
function handleMessageKeydown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
// Adjust textarea height
function adjustTextareaHeight(textarea) {
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
}
// Toggle templates panel
function toggleTemplates() {
const panel = document.getElementById('messenger-templates');
if (panel) {
panel.classList.toggle('active');
}
}
// Use a template
function useTemplate(templateId) {
const templates = messengerTemplates.length > 0 ? messengerTemplates : defaultTemplates;
const template = templates.find(t => t.id === templateId) ||
templates[parseInt(templateId)] ||
templates[0];
if (!template) return;
const input = document.getElementById('messenger-input');
if (input) {
input.value = template.content || template.text;
input.focus();
adjustTextareaHeight(input);
document.getElementById('messenger-send-btn').disabled = false;
}
toggleTemplates();
}
// Attach file
function attachFile() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pdf,.doc,.docx,.jpg,.jpeg,.png';
input.onchange = (e) => {
const file = e.target.files?.[0];
if (file) {
console.log('Attaching file:', file.name);
addMessageToUI(`[Datei: ${file.name}]`, true);
}
};
input.click();
}
// Start video call
function startVideoCall() {
if (!currentConversationId) return;
console.log('Starting video call...');
alert('Videokonferenz wird gestartet... (Demo)');
}
// Start voice call
function startVoiceCall() {
if (!currentConversationId) return;
console.log('Starting voice call...');
alert('Anruf wird gestartet... (Demo)');
}
// Show conversation info
function showConversationInfo() {
if (!currentConversationId) return;
console.log('Showing conversation info...');
alert('Konversations-Details (Demo)');
}
// Show new conversation modal
async function showNewConversationModal() {
console.log('Opening new conversation modal...');
// Show contact picker if contacts are loaded
if (messengerContacts.length > 0) {
showContactPicker();
} else {
// Fallback to prompt
const recipient = prompt('Empfaenger eingeben (Name oder E-Mail):');
if (recipient) {
await createNewConversation(recipient, null);
}
}
}
// Show contact picker modal
function showContactPicker() {
// Create modal HTML
const modalHtml = `
<div id="contact-picker-modal" class="messenger-modal-overlay" onclick="closeContactPicker(event)">
<div class="messenger-modal" onclick="event.stopPropagation()">
<div class="messenger-modal-header">
<h3>Kontakt auswaehlen</h3>
<button onclick="closeContactPicker()" class="messenger-modal-close">&times;</button>
</div>
<div class="messenger-modal-search">
<input type="text" placeholder="Kontakt suchen..." oninput="filterContactList(this.value)">
</div>
<div class="messenger-modal-content" id="contact-picker-list">
${messengerContacts.map(c => `
<div class="messenger-contact-item" onclick="selectContact('${c.id}')">
<div class="messenger-avatar">&#128113;</div>
<div class="messenger-contact-info">
<div class="messenger-contact-name">${escapeHtml(c.name)}</div>
<div class="messenger-contact-role">${escapeHtml(c.student_name || '')} ${c.class_name ? '(' + c.class_name + ')' : ''}</div>
</div>
</div>
`).join('')}
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
}
// Close contact picker
function closeContactPicker(event) {
if (event && event.target !== event.currentTarget) return;
const modal = document.getElementById('contact-picker-modal');
if (modal) modal.remove();
}
// Filter contact list in picker
function filterContactList(query) {
const lowerQuery = query.toLowerCase();
document.querySelectorAll('.messenger-contact-item').forEach(item => {
const name = item.querySelector('.messenger-contact-name')?.textContent.toLowerCase() || '';
const role = item.querySelector('.messenger-contact-role')?.textContent.toLowerCase() || '';
item.style.display = (name.includes(lowerQuery) || role.includes(lowerQuery)) ? 'flex' : 'none';
});
}
// Select a contact and create conversation
async function selectContact(contactId) {
closeContactPicker();
const contact = messengerContacts.find(c => c.id === contactId);
if (contact) {
await createNewConversation(contact.name, [contact.id]);
}
}
// Create new conversation via API
async function createNewConversation(name, participantIds) {
try {
const response = await fetch(`${MESSENGER_API}/conversations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: name,
type: 'direct',
participant_ids: participantIds || []
})
});
if (!response.ok) {
throw new Error('Failed to create conversation');
}
const newConv = await response.json();
messengerConversations.unshift(newConv);
renderConversationList();
openConversation(newConv.id);
} catch (error) {
console.error('Error creating conversation:', error);
// Fallback: Create local conversation
const newId = 'local-' + Date.now();
const newConv = {
id: newId,
name: name,
type: 'direct',
last_message: 'Neue Konversation',
last_message_time: new Date().toISOString()
};
messengerConversations.unshift(newConv);
renderConversationList();
openConversation(newId);
}
}
// Escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Show Messenger Panel
function showMessengerPanel() {
console.log('showMessengerPanel called');
hideAllPanels();
const panel = document.getElementById('panel-messenger');
if (panel) {
panel.style.display = 'flex';
panel.classList.add('active');
loadMessengerModule();
console.log('Messenger panel shown');
} else {
console.error('panel-messenger not found');
}
}
// =============================================
// MESSENGER ADMIN FUNCTIONS
// =============================================
// Show Messenger Admin Modal
function showMessengerAdmin() {
const modalHtml = `
<div id="messenger-admin-modal" class="messenger-modal-overlay" onclick="closeMessengerAdmin(event)">
<div class="messenger-modal" style="max-width: 700px;" onclick="event.stopPropagation()">
<div class="messenger-modal-header">
<h3>Kontakte verwalten</h3>
<button onclick="closeMessengerAdmin()" class="messenger-modal-close">&times;</button>
</div>
<div style="display: flex; gap: 8px; padding: 16px 20px; border-bottom: 1px solid var(--bp-border);">
<button class="messenger-new-btn" style="margin: 0; flex: 1;" onclick="showAddContactForm()">
<span>&#10133;</span> Neuer Kontakt
</button>
<button class="messenger-new-btn" style="margin: 0; flex: 1; background: var(--bp-success, #10b981);" onclick="showCSVImport()">
<span>&#128194;</span> CSV Import
</button>
<button class="messenger-new-btn" style="margin: 0; flex: 1; background: var(--bp-surface-elevated); color: var(--bp-text);" onclick="exportContacts()">
<span>&#128190;</span> Export
</button>
</div>
<div class="messenger-modal-search">
<input type="text" placeholder="Kontakte suchen..." oninput="filterAdminContacts(this.value)">
</div>
<div class="messenger-modal-content" id="admin-contact-list" style="max-height: 400px;">
<div style="text-align: center; padding: 40px; color: var(--bp-text-muted);">
<p>Lade Kontakte...</p>
</div>
</div>
<div style="padding: 16px 20px; border-top: 1px solid var(--bp-border); font-size: 12px; color: var(--bp-text-muted);">
${messengerContacts.length} Kontakte
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
loadAdminContacts();
}
// Close admin modal
function closeMessengerAdmin(event) {
if (event && event.target !== event.currentTarget) return;
const modal = document.getElementById('messenger-admin-modal');
if (modal) modal.remove();
}
// Load contacts for admin panel
async function loadAdminContacts() {
const container = document.getElementById('admin-contact-list');
if (!container) return;
try {
const response = await fetch(`${MESSENGER_API}/contacts`);
if (response.ok) {
messengerContacts = await response.json();
}
} catch (error) {
console.error('Error loading contacts:', error);
}
if (messengerContacts.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 40px; color: var(--bp-text-muted);">
<p>Keine Kontakte vorhanden.</p>
<p>Fuegen Sie Kontakte hinzu oder importieren Sie eine CSV-Datei.</p>
</div>
`;
return;
}
container.innerHTML = messengerContacts.map(c => `
<div class="messenger-contact-item" data-id="${c.id}">
<div class="messenger-avatar">&#128113;</div>
<div class="messenger-contact-info" style="flex: 1;">
<div class="messenger-contact-name">${escapeHtml(c.name)}</div>
<div class="messenger-contact-role">
${escapeHtml(c.email || '')}
${c.student_name ? ' | Schueler: ' + escapeHtml(c.student_name) : ''}
${c.class_name ? ' (' + c.class_name + ')' : ''}
</div>
</div>
<button onclick="editContact('${c.id}')" style="background: none; border: none; cursor: pointer; font-size: 16px; padding: 8px;" title="Bearbeiten">&#9998;</button>
<button onclick="deleteContact('${c.id}')" style="background: none; border: none; cursor: pointer; font-size: 16px; padding: 8px; color: #ef4444;" title="Loeschen">&#128465;</button>
</div>
`).join('');
}
// Filter contacts in admin panel
function filterAdminContacts(query) {
const lowerQuery = query.toLowerCase();
document.querySelectorAll('#admin-contact-list .messenger-contact-item').forEach(item => {
const name = item.querySelector('.messenger-contact-name')?.textContent.toLowerCase() || '';
const role = item.querySelector('.messenger-contact-role')?.textContent.toLowerCase() || '';
item.style.display = (name.includes(lowerQuery) || role.includes(lowerQuery)) ? 'flex' : 'none';
});
}
// Show add contact form
function showAddContactForm() {
const formHtml = `
<div id="add-contact-form" class="messenger-modal-overlay" onclick="closeAddContactForm(event)">
<div class="messenger-modal" style="max-width: 500px;" onclick="event.stopPropagation()">
<div class="messenger-modal-header">
<h3>Neuer Kontakt</h3>
<button onclick="closeAddContactForm()" class="messenger-modal-close">&times;</button>
</div>
<form onsubmit="saveNewContact(event)" style="padding: 20px;">
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 4px; font-size: 13px; color: var(--bp-text-muted);">Name *</label>
<input type="text" name="name" required style="width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text);">
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 4px; font-size: 13px; color: var(--bp-text-muted);">E-Mail</label>
<input type="email" name="email" style="width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text);">
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 4px; font-size: 13px; color: var(--bp-text-muted);">Telefon</label>
<input type="tel" name="phone" style="width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text);">
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 4px; font-size: 13px; color: var(--bp-text-muted);">Schueler-Name</label>
<input type="text" name="student_name" style="width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text);">
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 4px; font-size: 13px; color: var(--bp-text-muted);">Klasse</label>
<input type="text" name="class_name" style="width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text);">
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 4px; font-size: 13px; color: var(--bp-text-muted);">Typ</label>
<select name="contact_type" style="width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text);">
<option value="parent">Eltern</option>
<option value="teacher">Lehrer</option>
<option value="student">Schueler</option>
<option value="other">Sonstige</option>
</select>
</div>
<button type="submit" class="messenger-new-btn" style="width: 100%; margin: 0;">Kontakt speichern</button>
</form>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', formHtml);
}
function closeAddContactForm(event) {
if (event && event.target !== event.currentTarget) return;
const form = document.getElementById('add-contact-form');
if (form) form.remove();
}
async function saveNewContact(event) {
event.preventDefault();
const form = event.target;
const data = {
name: form.name.value,
email: form.email.value || null,
phone: form.phone.value || null,
student_name: form.student_name.value || null,
class_name: form.class_name.value || null,
contact_type: form.contact_type.value
};
try {
const response = await fetch(`${MESSENGER_API}/contacts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
closeAddContactForm();
loadAdminContacts();
loadMessengerData(); // Refresh main data
} else {
const error = await response.json();
alert('Fehler: ' + (error.detail || 'Unbekannter Fehler'));
}
} catch (error) {
console.error('Error saving contact:', error);
alert('Kontakt konnte nicht gespeichert werden.');
}
}
// Delete contact
async function deleteContact(contactId) {
if (!confirm('Kontakt wirklich loeschen?')) return;
try {
const response = await fetch(`${MESSENGER_API}/contacts/${contactId}`, {
method: 'DELETE'
});
if (response.ok) {
loadAdminContacts();
loadMessengerData();
} else {
alert('Kontakt konnte nicht geloescht werden.');
}
} catch (error) {
console.error('Error deleting contact:', error);
alert('Fehler beim Loeschen.');
}
}
// Edit contact
async function editContact(contactId) {
const contact = messengerContacts.find(c => c.id === contactId);
if (!contact) return;
const formHtml = `
<div id="edit-contact-form" class="messenger-modal-overlay" onclick="closeEditContactForm(event)">
<div class="messenger-modal" style="max-width: 500px;" onclick="event.stopPropagation()">
<div class="messenger-modal-header">
<h3>Kontakt bearbeiten</h3>
<button onclick="closeEditContactForm()" class="messenger-modal-close">&times;</button>
</div>
<form onsubmit="updateContact(event, '${contactId}')" style="padding: 20px;">
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 4px; font-size: 13px; color: var(--bp-text-muted);">Name *</label>
<input type="text" name="name" value="${escapeHtml(contact.name)}" required style="width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text);">
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 4px; font-size: 13px; color: var(--bp-text-muted);">E-Mail</label>
<input type="email" name="email" value="${escapeHtml(contact.email || '')}" style="width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text);">
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 4px; font-size: 13px; color: var(--bp-text-muted);">Telefon</label>
<input type="tel" name="phone" value="${escapeHtml(contact.phone || '')}" style="width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text);">
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 4px; font-size: 13px; color: var(--bp-text-muted);">Schueler-Name</label>
<input type="text" name="student_name" value="${escapeHtml(contact.student_name || '')}" style="width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text);">
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 4px; font-size: 13px; color: var(--bp-text-muted);">Klasse</label>
<input type="text" name="class_name" value="${escapeHtml(contact.class_name || '')}" style="width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text);">
</div>
<button type="submit" class="messenger-new-btn" style="width: 100%; margin: 0;">Speichern</button>
</form>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', formHtml);
}
function closeEditContactForm(event) {
if (event && event.target !== event.currentTarget) return;
const form = document.getElementById('edit-contact-form');
if (form) form.remove();
}
async function updateContact(event, contactId) {
event.preventDefault();
const form = event.target;
const data = {
name: form.name.value,
email: form.email.value || null,
phone: form.phone.value || null,
student_name: form.student_name.value || null,
class_name: form.class_name.value || null
};
try {
const response = await fetch(`${MESSENGER_API}/contacts/${contactId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
closeEditContactForm();
loadAdminContacts();
loadMessengerData();
} else {
alert('Kontakt konnte nicht aktualisiert werden.');
}
} catch (error) {
console.error('Error updating contact:', error);
alert('Fehler beim Aktualisieren.');
}
}
// Show CSV Import Dialog
function showCSVImport() {
const importHtml = `
<div id="csv-import-form" class="messenger-modal-overlay" onclick="closeCSVImport(event)">
<div class="messenger-modal" style="max-width: 500px;" onclick="event.stopPropagation()">
<div class="messenger-modal-header">
<h3>CSV Import</h3>
<button onclick="closeCSVImport()" class="messenger-modal-close">&times;</button>
</div>
<div style="padding: 20px;">
<p style="font-size: 13px; color: var(--bp-text-muted); margin-bottom: 16px;">
Importieren Sie Kontakte aus einer CSV-Datei. Unterstuetzte Spalten:
<br><br>
<code style="background: var(--bp-surface-elevated); padding: 2px 6px; border-radius: 4px;">Name, Kontakt, Email, Telefon, Schueler, Klasse</code>
</p>
<div style="border: 2px dashed var(--bp-border); border-radius: 12px; padding: 40px; text-align: center; margin-bottom: 16px;">
<input type="file" id="csv-file-input" accept=".csv" style="display: none;" onchange="handleCSVFile(this)">
<button onclick="document.getElementById('csv-file-input').click()" class="messenger-new-btn" style="margin: 0;">
&#128194; CSV-Datei auswaehlen
</button>
</div>
<div id="csv-import-preview" style="display: none;">
<p style="font-size: 13px; color: var(--bp-text-muted); margin-bottom: 8px;">Vorschau:</p>
<div id="csv-preview-content" style="max-height: 200px; overflow-y: auto; background: var(--bp-surface-elevated); border-radius: 8px; padding: 12px; font-size: 12px;"></div>
<button onclick="submitCSVImport()" class="messenger-new-btn" style="width: 100%; margin: 16px 0 0 0; background: var(--bp-success, #10b981);">
&#10003; Importieren
</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', importHtml);
}
function closeCSVImport(event) {
if (event && event.target !== event.currentTarget) return;
const form = document.getElementById('csv-import-form');
if (form) form.remove();
}
let csvFileContent = null;
function handleCSVFile(input) {
const file = input.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
csvFileContent = e.target.result;
const lines = csvFileContent.split('\\n').slice(0, 6);
const preview = lines.map(l => escapeHtml(l)).join('<br>');
document.getElementById('csv-preview-content').innerHTML = preview;
document.getElementById('csv-import-preview').style.display = 'block';
};
reader.readAsText(file);
}
async function submitCSVImport() {
if (!csvFileContent) return;
try {
const formData = new FormData();
const blob = new Blob([csvFileContent], { type: 'text/csv' });
formData.append('file', blob, 'contacts.csv');
const response = await fetch(`${MESSENGER_API}/contacts/import`, {
method: 'POST',
body: formData
});
if (response.ok) {
const result = await response.json();
alert(`Import erfolgreich: ${result.imported} Kontakte importiert.`);
closeCSVImport();
loadAdminContacts();
loadMessengerData();
} else {
const error = await response.json();
alert('Import fehlgeschlagen: ' + (error.detail || 'Unbekannter Fehler'));
}
} catch (error) {
console.error('Error importing CSV:', error);
alert('CSV Import fehlgeschlagen.');
}
}
// Export contacts as CSV
async function exportContacts() {
try {
const response = await fetch(`${MESSENGER_API}/contacts/export/csv`);
if (response.ok) {
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'kontakte_export.csv';
a.click();
URL.revokeObjectURL(url);
} else {
alert('Export fehlgeschlagen.');
}
} catch (error) {
console.error('Error exporting contacts:', error);
alert('Export fehlgeschlagen.');
}
}
"""