This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/frontend/static/js/studio.js
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

9788 lines
393 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
console.log('studio.js loading...');
// ==========================================
// THEME TOGGLE (Dark/Light Mode)
// ==========================================
(function() {
const savedTheme = localStorage.getItem('bp-theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
console.log('Initial theme set to:', savedTheme);
})();
function initThemeToggle() {
const toggle = document.getElementById('theme-toggle');
const icon = document.getElementById('theme-icon');
const label = document.getElementById('theme-label');
if (!toggle || !icon || !label) {
console.warn('Theme toggle elements not found');
return;
}
function updateToggleUI(theme) {
if (theme === 'light') {
icon.textContent = '☀️';
label.textContent = 'Light';
} else {
icon.textContent = '🌙';
label.textContent = 'Dark';
}
}
// Initialize UI based on current theme
const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
updateToggleUI(currentTheme);
toggle.addEventListener('click', function() {
console.log('Theme toggle clicked');
const current = document.documentElement.getAttribute('data-theme') || 'dark';
const newTheme = current === 'dark' ? 'light' : 'dark';
console.log('Switching from', current, 'to', newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('bp-theme', newTheme);
updateToggleUI(newTheme);
});
}
// ==========================================
// INTERNATIONALISIERUNG (i18n)
// ==========================================
const translations = {
de: {
// Navigation & Header
brand_sub: "Studio",
nav_compare: "Arbeitsblätter",
nav_tiles: "Lern-Kacheln",
login: "Login / Anmeldung",
mvp_local: "MVP · Lokal auf deinem Mac",
// Sidebar
sidebar_areas: "Bereiche",
sidebar_studio: "Arbeitsblatt Studio",
sidebar_active: "aktiv",
sidebar_parents: "Eltern-Kanal",
sidebar_soon: "demnächst",
sidebar_correction: "Korrektur / Noten",
sidebar_units: "Lerneinheiten (lokal)",
input_student: "Schüler/in",
input_subject: "Fach",
input_grade: "Klasse (z.B. 7a)",
input_unit_title: "Lerneinheit / Thema",
btn_create: "Anlegen",
btn_add_current: "Aktuelles Arbeitsblatt hinzufügen",
btn_filter_unit: "Nur Lerneinheit",
btn_filter_all: "Alle Dateien",
// Screen 1 - Compare
uploaded_worksheets: "Hochgeladene Arbeitsblätter",
files: "Dateien",
btn_upload: "Hochladen",
btn_delete: "Löschen",
original_scan: "Original-Scan",
cleaned_version: "Bereinigt (Handschrift entfernt)",
no_cleaned: "Noch keine bereinigte Version vorhanden.",
process_hint: "Klicke auf 'Verarbeiten', um das Arbeitsblatt zu analysieren und zu bereinigen.",
worksheet_print: "Drucken",
worksheet_no_data: "Keine Arbeitsblatt-Daten vorhanden.",
btn_full_process: "Verarbeiten (Analyse + Bereinigung + HTML)",
btn_original_generate: "Nur Original-HTML generieren",
// Screen 2 - Tiles
learning_unit: "Lerneinheit",
no_unit_selected: "Keine Lerneinheit ausgewählt",
// MC Tile
mc_title: "Multiple Choice Test",
mc_ready: "Bereit",
mc_generating: "Generiert...",
mc_done: "Fertig",
mc_error: "Fehler",
mc_desc: "Erzeugt passende MC-Aufgaben zur ursprünglichen Schwierigkeit (z. B. Klasse 7), ohne das Niveau zu verändern.",
mc_generate: "MC generieren",
mc_show: "Fragen anzeigen",
mc_quiz_title: "Multiple Choice Quiz",
mc_evaluate: "Auswerten",
mc_correct: "Richtig!",
mc_incorrect: "Leider falsch.",
mc_not_answered: "Nicht beantwortet. Richtig wäre:",
mc_result: "von",
mc_result_correct: "richtig",
mc_percent: "korrekt",
mc_no_questions: "Noch keine MC-Fragen für dieses Arbeitsblatt generiert.",
mc_print: "Drucken",
mc_print_with_answers: "Mit Lösungen drucken?",
// Cloze Tile
cloze_title: "Lückentext",
cloze_desc: "Erzeugt Lückentexte mit mehreren sinnvollen Lücken pro Satz. Inkl. Übersetzung für Eltern.",
cloze_translation: "Übersetzung:",
cloze_generate: "Lückentext generieren",
cloze_start: "Übung starten",
cloze_exercise_title: "Lückentext-Übung",
cloze_instruction: "Fülle die Lücken aus und klicke auf 'Prüfen'.",
cloze_check: "Prüfen",
cloze_show_answers: "Lösungen zeigen",
cloze_no_texts: "Noch keine Lückentexte für dieses Arbeitsblatt generiert.",
cloze_sentences: "Sätze",
cloze_gaps: "Lücken",
cloze_gaps_total: "Lücken gesamt",
cloze_with_gaps: "(mit Lücken)",
cloze_print: "Drucken",
cloze_print_with_answers: "Mit Lösungen drucken?",
// QA Tile
qa_title: "Frage-Antwort-Blatt",
qa_desc: "Frage-Antwort-Paare mit Leitner-Box System. Wiederholung nach Schwierigkeitsgrad.",
qa_generate: "Q&A generieren",
qa_learn: "Lernen starten",
qa_print: "Drucken",
qa_no_questions: "Noch keine Q&A für dieses Arbeitsblatt generiert.",
qa_box_new: "Neu",
qa_box_learning: "Gelernt",
qa_box_mastered: "Gefestigt",
qa_show_answer: "Antwort zeigen",
qa_your_answer: "Deine Antwort",
qa_type_answer: "Schreibe deine Antwort hier...",
qa_check_answer: "Antwort prüfen",
qa_correct_answer: "Richtige Antwort",
qa_self_evaluate: "War deine Antwort richtig?",
qa_no_answer: "(keine Antwort eingegeben)",
qa_correct: "Richtig",
qa_incorrect: "Falsch",
qa_key_terms: "Schlüsselbegriffe",
qa_session_correct: "Richtig",
qa_session_incorrect: "Falsch",
qa_session_complete: "Lernrunde abgeschlossen!",
qa_result_correct: "richtig",
qa_restart: "Nochmal lernen",
qa_print_with_answers: "Mit Lösungen drucken?",
question: "Frage",
answer: "Antwort",
status_generating_qa: "Generiere Q&A …",
status_qa_generated: "Q&A generiert",
// Common
close: "Schließen",
subject: "Fach",
grade: "Stufe",
questions: "Fragen",
worksheet: "Arbeitsblatt",
loading: "Lädt...",
error: "Fehler",
success: "Erfolgreich",
// Footer
imprint: "Impressum",
privacy: "Datenschutz",
contact: "Kontakt",
// Status messages
status_ready: "Bereit",
status_processing: "Verarbeitet...",
status_generating_mc: "Generiere MC-Fragen …",
status_generating_cloze: "Generiere Lückentexte …",
status_please_wait: "Bitte warten, KI arbeitet.",
status_mc_generated: "MC-Fragen generiert",
status_cloze_generated: "Lückentexte generiert",
status_files_created: "Dateien erstellt",
// Mindmap Tile
mindmap_title: "Mindmap Lernposter",
mindmap_desc: "Erstellt eine kindgerechte Mindmap mit dem Hauptthema in der Mitte und allen Fachbegriffen in farbigen Kategorien.",
mindmap_generate: "Mindmap erstellen",
mindmap_show: "Ansehen",
mindmap_print_a3: "A3 Drucken",
generating_mindmap: "Erstelle Mindmap...",
mindmap_generated: "Mindmap erstellt!",
no_analysis: "Keine Analyse",
analyze_first: "Bitte zuerst analysieren (Verarbeiten starten)",
categories: "Kategorien",
terms: "Begriffe",
},
tr: {
brand_sub: "Stüdyo",
nav_compare: "Çalışma Sayfaları",
nav_tiles: "Öğrenme Kartları",
login: "Giriş / Kayıt",
mvp_local: "MVP · Mac'inizde Yerel",
sidebar_areas: "Alanlar",
sidebar_studio: "Çalışma Sayfası Stüdyosu",
sidebar_active: "aktif",
sidebar_parents: "Ebeveyn Kanalı",
sidebar_soon: "yakında",
sidebar_correction: "Düzeltme / Notlar",
sidebar_units: "Öğrenme Birimleri (yerel)",
input_student: "Öğrenci",
input_subject: "Ders",
input_grade: "Sınıf (örn. 7a)",
input_unit_title: "Öğrenme Birimi / Konu",
btn_create: "Oluştur",
btn_add_current: "Mevcut çalışma sayfasını ekle",
btn_filter_unit: "Sadece Birim",
btn_filter_all: "Tüm Dosyalar",
uploaded_worksheets: "Yüklenen Çalışma Sayfaları",
files: "Dosya",
btn_upload: "Yükle",
btn_delete: "Sil",
original_scan: "Orijinal Tarama",
cleaned_version: "Temizlenmiş (El yazısı kaldırıldı)",
no_cleaned: "Henüz temizlenmiş sürüm yok.",
process_hint: "Çalışma sayfasını analiz etmek ve temizlemek için 'İşle'ye tıklayın.",
worksheet_print: "Yazdır",
worksheet_no_data: "Çalışma sayfası verisi yok.",
btn_full_process: "İşle (Analiz + Temizleme + HTML)",
btn_original_generate: "Sadece Orijinal HTML Oluştur",
learning_unit: "Öğrenme Birimi",
no_unit_selected: "Öğrenme birimi seçilmedi",
mc_title: "Çoktan Seçmeli Test",
mc_ready: "Hazır",
mc_generating: "Oluşturuluyor...",
mc_done: "Tamamlandı",
mc_error: "Hata",
mc_desc: "Orijinal zorluğa uygun (örn. 7. sınıf) çoktan seçmeli sorular oluşturur.",
mc_generate: "ÇS Oluştur",
mc_show: "Soruları Göster",
mc_quiz_title: "Çoktan Seçmeli Quiz",
mc_evaluate: "Değerlendir",
mc_correct: "Doğru!",
mc_incorrect: "Maalesef yanlış.",
mc_not_answered: "Cevaplanmadı. Doğru cevap:",
mc_result: "/",
mc_result_correct: "doğru",
mc_percent: "doğru",
mc_no_questions: "Bu çalışma sayfası için henüz ÇS sorusu oluşturulmadı.",
mc_print: "Yazdır",
mc_print_with_answers: "Cevaplarla yazdır?",
cloze_title: "Boşluk Doldurma",
cloze_desc: "Her cümlede birden fazla anlamlı boşluk içeren metinler oluşturur. Ebeveynler için çeviri dahil.",
cloze_translation: "Çeviri:",
cloze_generate: "Boşluk Metni Oluştur",
cloze_start: "Alıştırmayı Başlat",
cloze_exercise_title: "Boşluk Doldurma Alıştırması",
cloze_instruction: "Boşlukları doldurun ve 'Kontrol Et'e tıklayın.",
cloze_check: "Kontrol Et",
cloze_show_answers: "Cevapları Göster",
cloze_no_texts: "Bu çalışma sayfası için henüz boşluk metni oluşturulmadı.",
cloze_sentences: "Cümle",
cloze_gaps: "Boşluk",
cloze_gaps_total: "Toplam boşluk",
cloze_with_gaps: "(boşluklu)",
cloze_print: "Yazdır",
cloze_print_with_answers: "Cevaplarla yazdır?",
qa_title: "Soru-Cevap Sayfası",
qa_desc: "Leitner kutu sistemiyle soru-cevap çiftleri. Zorluk derecesine göre tekrar.",
qa_generate: "S&C Oluştur",
qa_learn: "Öğrenmeye Başla",
qa_print: "Yazdır",
qa_no_questions: "Bu çalışma sayfası için henüz S&C oluşturulmadı.",
qa_box_new: "Yeni",
qa_box_learning: "Öğreniliyor",
qa_box_mastered: "Pekiştirildi",
qa_show_answer: "Cevabı Göster",
qa_your_answer: "Senin Cevabın",
qa_type_answer: "Cevabını buraya yaz...",
qa_check_answer: "Cevabı Kontrol Et",
qa_correct_answer: "Doğru Cevap",
qa_self_evaluate: "Cevabın doğru muydu?",
qa_no_answer: "(cevap girilmedi)",
qa_correct: "Doğru",
qa_incorrect: "Yanlış",
qa_key_terms: "Anahtar Kavramlar",
qa_session_correct: "Doğru",
qa_session_incorrect: "Yanlış",
qa_session_complete: "Öğrenme turu tamamlandı!",
qa_result_correct: "doğru",
qa_restart: "Tekrar Öğren",
qa_print_with_answers: "Cevaplarla yazdır?",
question: "Soru",
answer: "Cevap",
status_generating_qa: "S&C oluşturuluyor…",
status_qa_generated: "S&C oluşturuldu",
close: "Kapat",
subject: "Ders",
grade: "Seviye",
questions: "Soru",
worksheet: "Çalışma Sayfası",
loading: "Yükleniyor...",
error: "Hata",
success: "Başarılı",
imprint: "Künye",
privacy: "Gizlilik",
contact: "İletişim",
status_ready: "Hazır",
status_processing: "İşleniyor...",
status_generating_mc: "ÇS soruları oluşturuluyor…",
status_generating_cloze: "Boşluk metinleri oluşturuluyor…",
status_please_wait: "Lütfen bekleyin, yapay zeka çalışıyor.",
status_mc_generated: "ÇS soruları oluşturuldu",
status_cloze_generated: "Boşluk metinleri oluşturuldu",
status_files_created: "dosya oluşturuldu",
// Mindmap Tile
mindmap_title: "Zihin Haritası Poster",
mindmap_desc: "Ana konuyu ortada ve tüm terimleri renkli kategorilerde gösteren çocuk dostu bir zihin haritası oluşturur.",
mindmap_generate: "Zihin Haritası Oluştur",
mindmap_show: "Görüntüle",
mindmap_print_a3: "A3 Yazdır",
generating_mindmap: "Zihin haritası oluşturuluyor...",
mindmap_generated: "Zihin haritası oluşturuldu!",
no_analysis: "Analiz yok",
analyze_first: "Lütfen önce analiz edin (İşle'ye tıklayın)",
categories: "Kategoriler",
terms: "Terimler",
},
ar: {
brand_sub: "ستوديو",
nav_compare: "أوراق العمل",
nav_tiles: "بطاقات التعلم",
login: "تسجيل الدخول / التسجيل",
mvp_local: "MVP · محلي على جهازك",
sidebar_areas: "الأقسام",
sidebar_studio: "استوديو أوراق العمل",
sidebar_active: "نشط",
sidebar_parents: "قناة الوالدين",
sidebar_soon: "قريباً",
sidebar_correction: "التصحيح / الدرجات",
sidebar_units: "وحدات التعلم (محلية)",
input_student: "الطالب/ة",
input_subject: "المادة",
input_grade: "الصف (مثل 7أ)",
input_unit_title: "وحدة التعلم / الموضوع",
btn_create: "إنشاء",
btn_add_current: "إضافة ورقة العمل الحالية",
btn_filter_unit: "الوحدة فقط",
btn_filter_all: "جميع الملفات",
uploaded_worksheets: "أوراق العمل المحملة",
files: "ملفات",
btn_upload: "تحميل",
btn_delete: "حذف",
original_scan: "المسح الأصلي",
cleaned_version: "منظف (تم إزالة الكتابة اليدوية)",
no_cleaned: "لا توجد نسخة منظفة بعد.",
process_hint: "انقر على 'معالجة' لتحليل وتنظيف ورقة العمل.",
worksheet_print: "طباعة",
worksheet_no_data: "لا توجد بيانات ورقة العمل.",
btn_full_process: "معالجة (تحليل + تنظيف + HTML)",
btn_original_generate: "إنشاء HTML الأصلي فقط",
learning_unit: "وحدة التعلم",
no_unit_selected: "لم يتم اختيار وحدة تعلم",
mc_title: "اختبار متعدد الخيارات",
mc_ready: "جاهز",
mc_generating: "جاري الإنشاء...",
mc_done: "تم",
mc_error: "خطأ",
mc_desc: "ينشئ أسئلة اختيار من متعدد تناسب مستوى الصعوبة الأصلي (مثل الصف 7).",
mc_generate: "إنشاء أسئلة",
mc_show: "عرض الأسئلة",
mc_quiz_title: "اختبار متعدد الخيارات",
mc_evaluate: "تقييم",
mc_correct: "صحيح!",
mc_incorrect: "للأسف خطأ.",
mc_not_answered: "لم تتم الإجابة. الإجابة الصحيحة:",
mc_result: "من",
mc_result_correct: "صحيح",
mc_percent: "صحيح",
mc_no_questions: "لم يتم إنشاء أسئلة بعد لورقة العمل هذه.",
mc_print: "طباعة",
mc_print_with_answers: "طباعة مع الإجابات؟",
cloze_title: "ملء الفراغات",
cloze_desc: "ينشئ نصوصاً بفراغات متعددة في كل جملة. يشمل الترجمة للوالدين.",
cloze_translation: "الترجمة:",
cloze_generate: "إنشاء نص الفراغات",
cloze_start: "بدء التمرين",
cloze_exercise_title: "تمرين ملء الفراغات",
cloze_instruction: "املأ الفراغات وانقر على 'تحقق'.",
cloze_check: "تحقق",
cloze_show_answers: "عرض الإجابات",
cloze_no_texts: "لم يتم إنشاء نصوص فراغات بعد لورقة العمل هذه.",
cloze_sentences: "جمل",
cloze_gaps: "فراغات",
cloze_gaps_total: "إجمالي الفراغات",
cloze_with_gaps: "(مع فراغات)",
cloze_print: "طباعة",
cloze_print_with_answers: "طباعة مع الإجابات؟",
qa_title: "ورقة الأسئلة والأجوبة",
qa_desc: "أزواج أسئلة وأجوبة مع نظام صندوق لايتنر. التكرار حسب الصعوبة.",
qa_generate: "إنشاء س&ج",
qa_learn: "بدء التعلم",
qa_print: "طباعة",
qa_no_questions: "لم يتم إنشاء س&ج بعد لورقة العمل هذه.",
qa_box_new: "جديد",
qa_box_learning: "قيد التعلم",
qa_box_mastered: "متقن",
qa_show_answer: "عرض الإجابة",
qa_your_answer: "إجابتك",
qa_type_answer: "اكتب إجابتك هنا...",
qa_check_answer: "تحقق من الإجابة",
qa_correct_answer: "الإجابة الصحيحة",
qa_self_evaluate: "هل كانت إجابتك صحيحة؟",
qa_no_answer: "(لم يتم إدخال إجابة)",
qa_correct: "صحيح",
qa_incorrect: "خطأ",
qa_key_terms: "المصطلحات الرئيسية",
qa_session_correct: "صحيح",
qa_session_incorrect: "خطأ",
qa_session_complete: "اكتملت جولة التعلم!",
qa_result_correct: "صحيح",
qa_restart: "تعلم مرة أخرى",
qa_print_with_answers: "طباعة مع الإجابات؟",
question: "سؤال",
answer: "إجابة",
status_generating_qa: "جاري إنشاء س&ج…",
status_qa_generated: "تم إنشاء س&ج",
close: "إغلاق",
subject: "المادة",
grade: "المستوى",
questions: "أسئلة",
worksheet: "ورقة العمل",
loading: "جاري التحميل...",
error: "خطأ",
success: "نجاح",
imprint: "البصمة",
privacy: "الخصوصية",
contact: "اتصل بنا",
status_ready: "جاهز",
status_processing: "جاري المعالجة...",
status_generating_mc: "جاري إنشاء الأسئلة…",
status_generating_cloze: "جاري إنشاء نصوص الفراغات…",
status_please_wait: "يرجى الانتظار، الذكاء الاصطناعي يعمل.",
status_mc_generated: "تم إنشاء الأسئلة",
status_cloze_generated: "تم إنشاء نصوص الفراغات",
status_files_created: "ملفات تم إنشاؤها",
// Mindmap Tile
mindmap_title: "ملصق خريطة ذهنية",
mindmap_desc: "ينشئ خريطة ذهنية مناسبة للأطفال مع الموضوع الرئيسي في المنتصف وجميع المصطلحات في فئات ملونة.",
mindmap_generate: "إنشاء خريطة ذهنية",
mindmap_show: "عرض",
mindmap_print_a3: "طباعة A3",
generating_mindmap: "جاري إنشاء الخريطة الذهنية...",
mindmap_generated: "تم إنشاء الخريطة الذهنية!",
no_analysis: "لا يوجد تحليل",
analyze_first: "يرجى التحليل أولاً (انقر على معالجة)",
categories: "الفئات",
terms: "المصطلحات",
},
ru: {
brand_sub: "Студия",
nav_compare: "Рабочие листы",
nav_tiles: "Учебные карточки",
login: "Вход / Регистрация",
mvp_local: "MVP · Локально на вашем Mac",
sidebar_areas: "Разделы",
sidebar_studio: "Студия рабочих листов",
sidebar_active: "активно",
sidebar_parents: "Канал для родителей",
sidebar_soon: "скоро",
sidebar_correction: "Проверка / Оценки",
sidebar_units: "Учебные блоки (локально)",
input_student: "Ученик",
input_subject: "Предмет",
input_grade: "Класс (напр. 7а)",
input_unit_title: "Учебный блок / Тема",
btn_create: "Создать",
btn_add_current: "Добавить текущий лист",
btn_filter_unit: "Только блок",
btn_filter_all: "Все файлы",
uploaded_worksheets: "Загруженные рабочие листы",
files: "файлов",
btn_upload: "Загрузить",
btn_delete: "Удалить",
original_scan: "Оригинальный скан",
cleaned_version: "Очищено (рукопись удалена)",
no_cleaned: "Очищенная версия пока недоступна.",
process_hint: "Нажмите 'Обработать' для анализа и очистки листа.",
worksheet_print: "Печать",
worksheet_no_data: "Нет данных рабочего листа.",
btn_full_process: "Обработать (Анализ + Очистка + HTML)",
btn_original_generate: "Только оригинальный HTML",
learning_unit: "Учебный блок",
no_unit_selected: "Блок не выбран",
mc_title: "Тест с выбором ответа",
mc_ready: "Готово",
mc_generating: "Создается...",
mc_done: "Готово",
mc_error: "Ошибка",
mc_desc: "Создает вопросы с выбором ответа соответствующей сложности (напр. 7 класс).",
mc_generate: "Создать тест",
mc_show: "Показать вопросы",
mc_quiz_title: "Тест с выбором ответа",
mc_evaluate: "Оценить",
mc_correct: "Правильно!",
mc_incorrect: "К сожалению, неверно.",
mc_not_answered: "Нет ответа. Правильный ответ:",
mc_result: "из",
mc_result_correct: "правильно",
mc_percent: "верно",
mc_no_questions: "Вопросы для этого листа еще не созданы.",
mc_print: "Печать",
mc_print_with_answers: "Печатать с ответами?",
cloze_title: "Текст с пропусками",
cloze_desc: "Создает тексты с несколькими пропусками в каждом предложении. Включая перевод для родителей.",
cloze_translation: "Перевод:",
cloze_generate: "Создать текст",
cloze_start: "Начать упражнение",
cloze_exercise_title: "Упражнение с пропусками",
cloze_instruction: "Заполните пропуски и нажмите 'Проверить'.",
cloze_check: "Проверить",
cloze_show_answers: "Показать ответы",
cloze_no_texts: "Тексты для этого листа еще не созданы.",
cloze_sentences: "предложений",
cloze_gaps: "пропусков",
cloze_gaps_total: "Всего пропусков",
cloze_with_gaps: "(с пропусками)",
cloze_print: "Печать",
cloze_print_with_answers: "Печатать с ответами?",
qa_title: "Лист вопросов и ответов",
qa_desc: "Пары вопрос-ответ с системой Лейтнера. Повторение по уровню сложности.",
qa_generate: "Создать В&О",
qa_learn: "Начать обучение",
qa_print: "Печать",
qa_no_questions: "В&О для этого листа еще не созданы.",
qa_box_new: "Новый",
qa_box_learning: "Изучается",
qa_box_mastered: "Освоено",
qa_show_answer: "Показать ответ",
qa_your_answer: "Твой ответ",
qa_type_answer: "Напиши свой ответ здесь...",
qa_check_answer: "Проверить ответ",
qa_correct_answer: "Правильный ответ",
qa_self_evaluate: "Твой ответ был правильным?",
qa_no_answer: "(ответ не введён)",
qa_correct: "Правильно",
qa_incorrect: "Неправильно",
qa_key_terms: "Ключевые термины",
qa_session_correct: "Правильно",
qa_session_incorrect: "Неправильно",
qa_session_complete: "Раунд обучения завершен!",
qa_result_correct: "правильно",
qa_restart: "Учить снова",
qa_print_with_answers: "Печатать с ответами?",
question: "Вопрос",
answer: "Ответ",
status_generating_qa: "Создание В&О…",
status_qa_generated: "В&О созданы",
close: "Закрыть",
subject: "Предмет",
grade: "Уровень",
questions: "вопросов",
worksheet: "Рабочий лист",
loading: "Загрузка...",
error: "Ошибка",
success: "Успешно",
imprint: "Импрессум",
privacy: "Конфиденциальность",
contact: "Контакт",
status_ready: "Готово",
status_processing: "Обработка...",
status_generating_mc: "Создание вопросов…",
status_generating_cloze: "Создание текстов…",
status_please_wait: "Пожалуйста, подождите, ИИ работает.",
status_mc_generated: "Вопросы созданы",
status_cloze_generated: "Тексты созданы",
status_files_created: "файлов создано",
// Mindmap Tile
mindmap_title: "Плакат Майнд-карта",
mindmap_desc: "Создает детскую ментальную карту с главной темой в центре и всеми терминами в цветных категориях.",
mindmap_generate: "Создать карту",
mindmap_show: "Просмотр",
mindmap_print_a3: "Печать A3",
generating_mindmap: "Создание карты...",
mindmap_generated: "Карта создана!",
no_analysis: "Нет анализа",
analyze_first: "Сначала выполните анализ (нажмите Обработать)",
categories: "Категории",
terms: "Термины",
},
uk: {
brand_sub: "Студія",
nav_compare: "Робочі аркуші",
nav_tiles: "Навчальні картки",
login: "Вхід / Реєстрація",
mvp_local: "MVP · Локально на вашому Mac",
sidebar_areas: "Розділи",
sidebar_studio: "Студія робочих аркушів",
sidebar_active: "активно",
sidebar_parents: "Канал для батьків",
sidebar_soon: "незабаром",
sidebar_correction: "Перевірка / Оцінки",
sidebar_units: "Навчальні блоки (локально)",
input_student: "Учень",
input_subject: "Предмет",
input_grade: "Клас (напр. 7а)",
input_unit_title: "Навчальний блок / Тема",
btn_create: "Створити",
btn_add_current: "Додати поточний аркуш",
btn_filter_unit: "Лише блок",
btn_filter_all: "Усі файли",
uploaded_worksheets: "Завантажені робочі аркуші",
files: "файлів",
btn_upload: "Завантажити",
btn_delete: "Видалити",
original_scan: "Оригінальний скан",
cleaned_version: "Очищено (рукопис видалено)",
no_cleaned: "Очищена версія ще недоступна.",
process_hint: "Натисніть 'Обробити' для аналізу та очищення аркуша.",
worksheet_print: "Друк",
worksheet_no_data: "Немає даних робочого аркуша.",
btn_full_process: "Обробити (Аналіз + Очищення + HTML)",
btn_original_generate: "Лише оригінальний HTML",
learning_unit: "Навчальний блок",
no_unit_selected: "Блок не вибрано",
mc_title: "Тест з вибором відповіді",
mc_ready: "Готово",
mc_generating: "Створюється...",
mc_done: "Готово",
mc_error: "Помилка",
mc_desc: "Створює питання з вибором відповіді відповідної складності (напр. 7 клас).",
mc_generate: "Створити тест",
mc_show: "Показати питання",
mc_quiz_title: "Тест з вибором відповіді",
mc_evaluate: "Оцінити",
mc_correct: "Правильно!",
mc_incorrect: "На жаль, неправильно.",
mc_not_answered: "Немає відповіді. Правильна відповідь:",
mc_result: "з",
mc_result_correct: "правильно",
mc_percent: "вірно",
mc_no_questions: "Питання для цього аркуша ще не створені.",
mc_print: "Друк",
mc_print_with_answers: "Друкувати з відповідями?",
cloze_title: "Текст з пропусками",
cloze_desc: "Створює тексти з кількома пропусками в кожному реченні. Включаючи переклад для батьків.",
cloze_translation: "Переклад:",
cloze_generate: "Створити текст",
cloze_start: "Почати вправу",
cloze_exercise_title: "Вправа з пропусками",
cloze_instruction: "Заповніть пропуски та натисніть 'Перевірити'.",
cloze_check: "Перевірити",
cloze_show_answers: "Показати відповіді",
cloze_no_texts: "Тексти для цього аркуша ще не створені.",
cloze_sentences: "речень",
cloze_gaps: "пропусків",
cloze_gaps_total: "Всього пропусків",
cloze_with_gaps: "(з пропусками)",
cloze_print: "Друк",
cloze_print_with_answers: "Друкувати з відповідями?",
qa_title: "Аркуш питань і відповідей",
qa_desc: "Пари питання-відповідь з системою Лейтнера. Повторення за рівнем складності.",
qa_generate: "Створити П&В",
qa_learn: "Почати навчання",
qa_print: "Друк",
qa_no_questions: "П&В для цього аркуша ще не створені.",
qa_box_new: "Новий",
qa_box_learning: "Вивчається",
qa_box_mastered: "Засвоєно",
qa_show_answer: "Показати відповідь",
qa_your_answer: "Твоя відповідь",
qa_type_answer: "Напиши свою відповідь тут...",
qa_check_answer: "Перевірити відповідь",
qa_correct_answer: "Правильна відповідь",
qa_self_evaluate: "Твоя відповідь була правильною?",
qa_no_answer: "(відповідь не введена)",
qa_correct: "Правильно",
qa_incorrect: "Неправильно",
qa_key_terms: "Ключові терміни",
qa_session_correct: "Правильно",
qa_session_incorrect: "Неправильно",
qa_session_complete: "Раунд навчання завершено!",
qa_result_correct: "правильно",
qa_restart: "Вчити знову",
qa_print_with_answers: "Друкувати з відповідями?",
question: "Питання",
answer: "Відповідь",
status_generating_qa: "Створення П&В…",
status_qa_generated: "П&В створені",
close: "Закрити",
subject: "Предмет",
grade: "Рівень",
questions: "питань",
worksheet: "Робочий аркуш",
loading: "Завантаження...",
error: "Помилка",
success: "Успішно",
imprint: "Імпресум",
privacy: "Конфіденційність",
contact: "Контакт",
status_ready: "Готово",
status_processing: "Обробка...",
status_generating_mc: "Створення питань…",
status_generating_cloze: "Створення текстів…",
status_please_wait: "Будь ласка, зачекайте, ШІ працює.",
status_mc_generated: "Питання створені",
status_cloze_generated: "Тексти створені",
status_files_created: "файлів створено",
// Mindmap Tile
mindmap_title: "Плакат Інтелект-карта",
mindmap_desc: "Створює дитячу інтелект-карту з головною темою в центрі та всіма термінами в кольорових категоріях.",
mindmap_generate: "Створити карту",
mindmap_show: "Переглянути",
mindmap_print_a3: "Друк A3",
generating_mindmap: "Створення карти...",
mindmap_generated: "Карту створено!",
no_analysis: "Немає аналізу",
analyze_first: "Спочатку виконайте аналіз (натисніть Обробити)",
categories: "Категорії",
terms: "Терміни",
},
pl: {
brand_sub: "Studio",
nav_compare: "Karty pracy",
nav_tiles: "Karty nauki",
login: "Logowanie / Rejestracja",
mvp_local: "MVP · Lokalnie na Twoim Mac",
sidebar_areas: "Sekcje",
sidebar_studio: "Studio kart pracy",
sidebar_active: "aktywne",
sidebar_parents: "Kanał dla rodziców",
sidebar_soon: "wkrótce",
sidebar_correction: "Korekta / Oceny",
sidebar_units: "Jednostki nauki (lokalnie)",
input_student: "Uczeń",
input_subject: "Przedmiot",
input_grade: "Klasa (np. 7a)",
input_unit_title: "Jednostka nauki / Temat",
btn_create: "Utwórz",
btn_add_current: "Dodaj bieżącą kartę",
btn_filter_unit: "Tylko jednostka",
btn_filter_all: "Wszystkie pliki",
uploaded_worksheets: "Przesłane karty pracy",
files: "plików",
btn_upload: "Prześlij",
btn_delete: "Usuń",
original_scan: "Oryginalny skan",
cleaned_version: "Oczyszczone (pismo ręczne usunięte)",
no_cleaned: "Oczyszczona wersja jeszcze niedostępna.",
process_hint: "Kliknij 'Przetwórz', aby przeanalizować i oczyścić kartę.",
worksheet_print: "Drukuj",
worksheet_no_data: "Brak danych arkusza.",
btn_full_process: "Przetwórz (Analiza + Czyszczenie + HTML)",
btn_original_generate: "Tylko oryginalny HTML",
learning_unit: "Jednostka nauki",
no_unit_selected: "Nie wybrano jednostki",
mc_title: "Test wielokrotnego wyboru",
mc_ready: "Gotowe",
mc_generating: "Tworzenie...",
mc_done: "Gotowe",
mc_error: "Błąd",
mc_desc: "Tworzy pytania wielokrotnego wyboru o odpowiednim poziomie trudności (np. klasa 7).",
mc_generate: "Utwórz test",
mc_show: "Pokaż pytania",
mc_quiz_title: "Test wielokrotnego wyboru",
mc_evaluate: "Oceń",
mc_correct: "Dobrze!",
mc_incorrect: "Niestety źle.",
mc_not_answered: "Brak odpowiedzi. Poprawna odpowiedź:",
mc_result: "z",
mc_result_correct: "poprawnie",
mc_percent: "poprawnie",
mc_no_questions: "Pytania dla tej karty jeszcze nie zostały utworzone.",
mc_print: "Drukuj",
mc_print_with_answers: "Drukować z odpowiedziami?",
cloze_title: "Tekst z lukami",
cloze_desc: "Tworzy teksty z wieloma lukami w każdym zdaniu. W tym tłumaczenie dla rodziców.",
cloze_translation: "Tłumaczenie:",
cloze_generate: "Utwórz tekst",
cloze_start: "Rozpocznij ćwiczenie",
cloze_exercise_title: "Ćwiczenie z lukami",
cloze_instruction: "Wypełnij luki i kliknij 'Sprawdź'.",
cloze_check: "Sprawdź",
cloze_show_answers: "Pokaż odpowiedzi",
cloze_no_texts: "Teksty dla tej karty jeszcze nie zostały utworzone.",
cloze_sentences: "zdań",
cloze_gaps: "luk",
cloze_gaps_total: "Łącznie luk",
cloze_with_gaps: "(z lukami)",
cloze_print: "Drukuj",
cloze_print_with_answers: "Drukować z odpowiedziami?",
qa_title: "Arkusz pytań i odpowiedzi",
qa_desc: "Pary pytanie-odpowiedź z systemem Leitnera. Powtórki według poziomu trudności.",
qa_generate: "Utwórz P&O",
qa_learn: "Rozpocznij naukę",
qa_print: "Drukuj",
qa_no_questions: "P&O dla tej karty jeszcze nie zostały utworzone.",
qa_box_new: "Nowy",
qa_box_learning: "W nauce",
qa_box_mastered: "Opanowane",
qa_show_answer: "Pokaż odpowiedź",
qa_your_answer: "Twoja odpowiedź",
qa_type_answer: "Napisz swoją odpowiedź tutaj...",
qa_check_answer: "Sprawdź odpowiedź",
qa_correct_answer: "Prawidłowa odpowiedź",
qa_self_evaluate: "Czy twoja odpowiedź była poprawna?",
qa_no_answer: "(nie wprowadzono odpowiedzi)",
qa_correct: "Dobrze",
qa_incorrect: "Źle",
qa_key_terms: "Kluczowe pojęcia",
qa_session_correct: "Dobrze",
qa_session_incorrect: "Źle",
qa_session_complete: "Runda nauki zakończona!",
qa_result_correct: "poprawnie",
qa_restart: "Ucz się ponownie",
qa_print_with_answers: "Drukować z odpowiedziami?",
question: "Pytanie",
answer: "Odpowiedź",
status_generating_qa: "Tworzenie P&O…",
status_qa_generated: "P&O utworzone",
close: "Zamknij",
subject: "Przedmiot",
grade: "Poziom",
questions: "pytań",
worksheet: "Karta pracy",
loading: "Ładowanie...",
error: "Błąd",
success: "Sukces",
imprint: "Impressum",
privacy: "Prywatność",
contact: "Kontakt",
status_ready: "Gotowe",
status_processing: "Przetwarzanie...",
status_generating_mc: "Tworzenie pytań…",
status_generating_cloze: "Tworzenie tekstów…",
status_please_wait: "Proszę czekać, AI pracuje.",
status_mc_generated: "Pytania utworzone",
status_cloze_generated: "Teksty utworzone",
status_files_created: "plików utworzono",
// Mindmap Tile
mindmap_title: "Plakat Mapa myśli",
mindmap_desc: "Tworzy przyjazną dla dzieci mapę myśli z głównym tematem w centrum i wszystkimi terminami w kolorowych kategoriach.",
mindmap_generate: "Utwórz mapę",
mindmap_show: "Podgląd",
mindmap_print_a3: "Drukuj A3",
generating_mindmap: "Tworzenie mapy...",
mindmap_generated: "Mapa utworzona!",
no_analysis: "Brak analizy",
analyze_first: "Najpierw wykonaj analizę (kliknij Przetwórz)",
categories: "Kategorie",
terms: "Terminy",
},
en: {
brand_sub: "Studio",
nav_compare: "Worksheets",
nav_tiles: "Learning Tiles",
login: "Login / Sign Up",
mvp_local: "MVP · Local on your Mac",
sidebar_areas: "Areas",
sidebar_studio: "Worksheet Studio",
sidebar_active: "active",
sidebar_parents: "Parents Channel",
sidebar_soon: "coming soon",
sidebar_correction: "Correction / Grades",
sidebar_units: "Learning Units (local)",
input_student: "Student",
input_subject: "Subject",
input_grade: "Grade (e.g. 7a)",
input_unit_title: "Learning Unit / Topic",
btn_create: "Create",
btn_add_current: "Add current worksheet",
btn_filter_unit: "Unit only",
btn_filter_all: "All files",
uploaded_worksheets: "Uploaded Worksheets",
files: "files",
btn_upload: "Upload",
btn_delete: "Delete",
original_scan: "Original Scan",
cleaned_version: "Cleaned (handwriting removed)",
no_cleaned: "No cleaned version available yet.",
process_hint: "Click 'Process' to analyze and clean the worksheet.",
worksheet_print: "Print",
worksheet_no_data: "No worksheet data available.",
btn_full_process: "Process (Analysis + Cleaning + HTML)",
btn_original_generate: "Generate Original HTML Only",
learning_unit: "Learning Unit",
no_unit_selected: "No unit selected",
mc_title: "Multiple Choice Test",
mc_ready: "Ready",
mc_generating: "Generating...",
mc_done: "Done",
mc_error: "Error",
mc_desc: "Creates multiple choice questions matching the original difficulty level (e.g. Grade 7).",
mc_generate: "Generate MC",
mc_show: "Show Questions",
mc_quiz_title: "Multiple Choice Quiz",
mc_evaluate: "Evaluate",
mc_correct: "Correct!",
mc_incorrect: "Unfortunately wrong.",
mc_not_answered: "Not answered. Correct answer:",
mc_result: "of",
mc_result_correct: "correct",
mc_percent: "correct",
mc_no_questions: "No MC questions generated yet for this worksheet.",
mc_print: "Print",
mc_print_with_answers: "Print with answers?",
cloze_title: "Fill in the Blanks",
cloze_desc: "Creates texts with multiple meaningful gaps per sentence. Including translation for parents.",
cloze_translation: "Translation:",
cloze_generate: "Generate Cloze Text",
cloze_start: "Start Exercise",
cloze_exercise_title: "Fill in the Blanks Exercise",
cloze_instruction: "Fill in the blanks and click 'Check'.",
cloze_check: "Check",
cloze_show_answers: "Show Answers",
cloze_no_texts: "No cloze texts generated yet for this worksheet.",
cloze_sentences: "sentences",
cloze_gaps: "gaps",
cloze_gaps_total: "Total gaps",
cloze_with_gaps: "(with gaps)",
cloze_print: "Print",
cloze_print_with_answers: "Print with answers?",
qa_title: "Question & Answer Sheet",
qa_desc: "Q&A pairs with Leitner box system. Spaced repetition by difficulty level.",
qa_generate: "Generate Q&A",
qa_learn: "Start Learning",
qa_print: "Print",
qa_no_questions: "No Q&A generated yet for this worksheet.",
qa_box_new: "New",
qa_box_learning: "Learning",
qa_box_mastered: "Mastered",
qa_show_answer: "Show Answer",
qa_your_answer: "Your Answer",
qa_type_answer: "Write your answer here...",
qa_check_answer: "Check Answer",
qa_correct_answer: "Correct Answer",
qa_self_evaluate: "Was your answer correct?",
qa_no_answer: "(no answer entered)",
qa_correct: "Correct",
qa_incorrect: "Incorrect",
qa_key_terms: "Key Terms",
qa_session_correct: "Correct",
qa_session_incorrect: "Incorrect",
qa_session_complete: "Learning session complete!",
qa_result_correct: "correct",
qa_restart: "Learn Again",
qa_print_with_answers: "Print with answers?",
question: "Question",
answer: "Answer",
status_generating_qa: "Generating Q&A…",
status_qa_generated: "Q&A generated",
close: "Close",
subject: "Subject",
grade: "Level",
questions: "questions",
worksheet: "Worksheet",
loading: "Loading...",
error: "Error",
success: "Success",
imprint: "Imprint",
privacy: "Privacy",
contact: "Contact",
status_ready: "Ready",
status_processing: "Processing...",
status_generating_mc: "Generating MC questions…",
status_generating_cloze: "Generating cloze texts…",
status_please_wait: "Please wait, AI is working.",
status_mc_generated: "MC questions generated",
status_cloze_generated: "Cloze texts generated",
status_files_created: "files created",
// Mindmap Tile
mindmap_title: "Mindmap Learning Poster",
mindmap_desc: "Creates a child-friendly mindmap with the main topic in the center and all terms in colorful categories.",
mindmap_generate: "Create Mindmap",
mindmap_show: "View",
mindmap_print_a3: "Print A3",
generating_mindmap: "Creating mindmap...",
mindmap_generated: "Mindmap created!",
no_analysis: "No analysis",
analyze_first: "Please analyze first (click Process)",
categories: "Categories",
terms: "Terms",
}
};
// Aktuelle Sprache (aus localStorage oder Browser-Sprache)
let currentLang = localStorage.getItem('bp_language') || 'de';
// RTL-Sprachen
const rtlLanguages = ['ar'];
// Übersetzungsfunktion
function t(key) {
const lang = translations[currentLang] || translations['de'];
return lang[key] || translations['de'][key] || key;
}
// Sprache anwenden
function applyLanguage(lang) {
currentLang = lang;
localStorage.setItem('bp_language', lang);
// RTL für Arabisch
if (rtlLanguages.includes(lang)) {
document.documentElement.setAttribute('dir', 'rtl');
} else {
document.documentElement.setAttribute('dir', 'ltr');
}
// Alle Elemente mit data-i18n aktualisieren
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
if (el.tagName === 'INPUT' && el.hasAttribute('placeholder')) {
el.placeholder = t(key);
} else {
el.textContent = t(key);
}
});
// Spezielle Elemente manuell aktualisieren
updateUITexts();
}
// UI-Texte aktualisieren
function updateUITexts() {
// Header
const brandSub = document.querySelector('.brand-text-sub');
if (brandSub) brandSub.textContent = t('brand_sub');
// Navigation (Text entfernt - wird nicht mehr angezeigt)
// const navItems = document.querySelectorAll('.top-nav-item');
// if (navItems[0]) navItems[0].textContent = t('nav_compare');
// if (navItems[1]) navItems[1].textContent = t('nav_tiles');
// Sidebar Bereiche
const sidebarTitles = document.querySelectorAll('.sidebar-section-title');
if (sidebarTitles[0]) sidebarTitles[0].textContent = t('sidebar_areas');
if (sidebarTitles[1]) sidebarTitles[1].textContent = t('sidebar_units');
const sidebarLabels = document.querySelectorAll('.sidebar-item-label span');
if (sidebarLabels[0]) sidebarLabels[0].textContent = t('sidebar_studio');
if (sidebarLabels[1]) sidebarLabels[1].textContent = t('sidebar_parents');
if (sidebarLabels[2]) sidebarLabels[2].textContent = t('sidebar_correction');
const sidebarBadges = document.querySelectorAll('.sidebar-item-badge');
if (sidebarBadges[0]) sidebarBadges[0].textContent = t('sidebar_active');
if (sidebarBadges[1]) sidebarBadges[1].textContent = t('sidebar_soon');
if (sidebarBadges[2]) sidebarBadges[2].textContent = t('sidebar_soon');
// Input Placeholders
if (unitStudentInput) unitStudentInput.placeholder = t('input_student');
if (unitSubjectInput) unitSubjectInput.placeholder = t('input_subject');
if (unitGradeInput) unitGradeInput.placeholder = t('input_grade');
if (unitTitleInput) unitTitleInput.placeholder = t('input_unit_title');
// Buttons
if (btnAddUnit) btnAddUnit.textContent = t('btn_create');
if (btnAttachCurrentToLu) btnAttachCurrentToLu.textContent = t('btn_add_current');
if (btnToggleFilter) {
btnToggleFilter.textContent = showOnlyUnitFiles ? t('btn_filter_unit') : t('btn_filter_all');
}
if (btnFullProcess) btnFullProcess.textContent = t('btn_full_process');
if (btnOriginalGenerate) btnOriginalGenerate.textContent = t('btn_original_generate');
// Titles
const uploadedTitle = document.querySelector('.panel-left > div:first-child');
if (uploadedTitle) {
uploadedTitle.innerHTML = '<span data-i18n="uploaded_worksheets">' + t('uploaded_worksheets') + '</span> <span class="pill" id="eingang-count">0 ' + t('files') + '</span>';
}
// MC Tile
const mcTitle = document.querySelector('[data-tile="mc"] .card-title');
if (mcTitle) mcTitle.textContent = t('mc_title');
const mcDesc = document.querySelector('[data-tile="mc"] .card-body > div:first-child');
if (mcDesc) mcDesc.textContent = t('mc_desc');
if (btnMcGenerate) btnMcGenerate.textContent = t('mc_generate');
if (btnMcShow) btnMcShow.textContent = t('mc_show');
// Cloze Tile
const clozeTitle = document.querySelector('[data-tile="cloze"] .card-title');
if (clozeTitle) clozeTitle.textContent = t('cloze_title');
const clozeDesc = document.querySelector('[data-tile="cloze"] .card-body > div:first-child');
if (clozeDesc) clozeDesc.textContent = t('cloze_desc');
const clozeLabel = document.querySelector('.cloze-language-select label');
if (clozeLabel) clozeLabel.textContent = t('cloze_translation');
if (btnClozeGenerate) btnClozeGenerate.textContent = t('cloze_generate');
if (btnClozeShow) btnClozeShow.textContent = t('cloze_start');
// QA Tile
const qaTitle = document.querySelector('[data-tile="qa"] .card-title');
if (qaTitle) qaTitle.textContent = t('qa_title');
const qaDesc = document.querySelector('[data-tile="qa"] .card-body > div:first-child');
if (qaDesc) qaDesc.textContent = t('qa_desc');
const qaBadge = document.querySelector('[data-tile="qa"] .card-badge');
if (qaBadge) qaBadge.textContent = t('qa_soon');
// Modal Titles
const mcModalTitle = document.querySelector('#mc-modal .mc-modal-title');
if (mcModalTitle) mcModalTitle.textContent = t('mc_quiz_title');
const clozeModalTitle = document.querySelector('#cloze-modal .mc-modal-title');
if (clozeModalTitle) clozeModalTitle.textContent = t('cloze_exercise_title');
// Close Buttons
document.querySelectorAll('.mc-modal-close').forEach(btn => {
btn.textContent = t('close') + ' ✕';
});
if (lightboxClose) lightboxClose.textContent = t('close') + ' ✕';
// Footer
const footerLinks = document.querySelectorAll('.footer a');
if (footerLinks[0]) footerLinks[0].textContent = t('imprint');
if (footerLinks[1]) footerLinks[1].textContent = t('privacy');
if (footerLinks[2]) footerLinks[2].textContent = t('contact');
}
const eingangListEl = document.getElementById('eingang-list');
const eingangCountEl = document.getElementById('eingang-count');
const previewContainer = document.getElementById('preview-container');
const fileInput = document.getElementById('file-input');
const btnUploadInline = document.getElementById('btn-upload-inline');
const btnFullProcess = document.getElementById('btn-full-process');
const btnOriginalGenerate = document.getElementById('btn-original-generate');
const statusBar = document.getElementById('status-bar');
const statusDot = document.getElementById('status-dot');
const statusMain = document.getElementById('status-main');
const statusSub = document.getElementById('status-sub');
const panelCompare = document.getElementById('panel-compare');
const panelTiles = document.getElementById('panel-tiles');
const pagerPrev = document.getElementById('pager-prev');
const pagerNext = document.getElementById('pager-next');
const pagerLabel = document.getElementById('pager-label');
const topNavItems = document.querySelectorAll('.top-nav-item');
const lightboxEl = document.getElementById('lightbox');
const lightboxImg = document.getElementById('lightbox-img');
const lightboxCaption = document.getElementById('lightbox-caption');
const lightboxClose = document.getElementById('lightbox-close');
const unitStudentInput = document.getElementById('unit-student');
const unitSubjectInput = document.getElementById('unit-subject');
const unitGradeInput = document.getElementById('unit-grade');
const unitTitleInput = document.getElementById('unit-title');
const unitListEl = document.getElementById('unit-list');
const btnAddUnit = document.getElementById('btn-add-unit');
const btnAttachCurrentToLu = document.getElementById('btn-attach-current-to-lu');
const unitHeading1 = document.getElementById('unit-heading-screen1');
const unitHeading2 = document.getElementById('unit-heading-screen2');
const btnToggleFilter = document.getElementById('btn-toggle-filter');
let currentSelectedFile = null;
let allEingangFiles = []; // Master-Liste aller Dateien
let eingangFiles = []; // aktuell gefilterte Ansicht
let currentIndex = 0;
let showOnlyUnitFiles = true; // Filter-Modus: true = nur Lerneinheit (Standard), false = alle
let allWorksheetPairs = {}; // Master-Mapping original -> { clean_html, clean_image }
let worksheetPairs = {}; // aktuell gefiltertes Mapping
let tileState = {
mindmap: true,
qa: true,
mc: true,
cloze: true,
};
let currentScreen = 1;
// Lerneinheiten aus dem Backend
let units = [];
let currentUnitId = null;
// --- Lightbox / Vollbild ---
function openLightbox(src, caption) {
if (!src) return;
lightboxImg.src = src;
lightboxCaption.textContent = caption || '';
lightboxEl.classList.remove('hidden');
}
function closeLightbox() {
lightboxEl.classList.add('hidden');
lightboxImg.src = '';
lightboxCaption.textContent = '';
}
lightboxClose.addEventListener('click', closeLightbox);
lightboxEl.addEventListener('click', (ev) => {
if (ev.target === lightboxEl) {
closeLightbox();
}
});
document.addEventListener('keydown', (ev) => {
if (ev.key === 'Escape') {
closeLightbox();
// Close compare view if open
const compareView = document.getElementById('version-compare-view');
if (compareView && compareView.classList.contains('active')) {
hideCompareView();
}
}
});
// --- Status-Balken ---
function setStatus(main, sub = '', state = 'idle') {
statusMain.textContent = main;
statusSub.textContent = sub;
statusDot.classList.remove('busy', 'error');
if (state === 'busy') {
statusDot.classList.add('busy');
} else if (state === 'error') {
statusDot.classList.add('error');
}
}
setStatus('Bereit', 'Lade Arbeitsblätter hoch und starte den Neuaufbau.');
// --- API-Helfer ---
async function apiFetch(url, options = {}) {
const resp = await fetch(url, options);
if (!resp.ok) {
throw new Error('HTTP ' + resp.status);
}
return resp.json();
}
// --- Dateien laden & rendern ---
async function loadEingangFiles() {
try {
const data = await apiFetch('/api/eingang-dateien');
allEingangFiles = data.eingang || [];
eingangFiles = allEingangFiles.slice();
currentIndex = 0;
renderEingangList();
} catch (e) {
console.error(e);
setStatus('Fehler beim Laden der Dateien', String(e), 'error');
}
}
function renderEingangList() {
eingangListEl.innerHTML = '';
if (!eingangFiles.length) {
const li = document.createElement('li');
li.className = 'file-empty';
li.textContent = 'Noch keine Dateien vorhanden.';
eingangListEl.appendChild(li);
eingangCountEl.textContent = '0 Dateien';
return;
}
eingangFiles.forEach((filename, idx) => {
const li = document.createElement('li');
li.className = 'file-item';
if (idx === currentIndex) {
li.classList.add('active');
}
const nameSpan = document.createElement('span');
nameSpan.className = 'file-item-name';
nameSpan.textContent = filename;
const actionsSpan = document.createElement('span');
actionsSpan.style.display = 'flex';
actionsSpan.style.gap = '6px';
// Button: Aus Lerneinheit entfernen
const removeFromUnitBtn = document.createElement('span');
removeFromUnitBtn.className = 'file-item-delete';
removeFromUnitBtn.textContent = '✕';
removeFromUnitBtn.title = 'Aus Lerneinheit entfernen';
removeFromUnitBtn.addEventListener('click', (ev) => {
ev.stopPropagation();
if (!currentUnitId) {
alert('Zum Entfernen bitte zuerst eine Lerneinheit auswählen.');
return;
}
const ok = confirm('Dieses Arbeitsblatt aus der aktuellen Lerneinheit entfernen? Die Datei selbst bleibt erhalten.');
if (!ok) return;
removeWorksheetFromCurrentUnit(eingangFiles[idx]);
});
// Button: Datei komplett löschen
const deleteFileBtn = document.createElement('span');
deleteFileBtn.className = 'file-item-delete';
deleteFileBtn.textContent = '🗑️';
deleteFileBtn.title = 'Datei komplett löschen';
deleteFileBtn.style.color = '#ef4444';
deleteFileBtn.addEventListener('click', async (ev) => {
ev.stopPropagation();
const ok = confirm(`Datei "${eingangFiles[idx]}" wirklich komplett löschen? Diese Aktion kann nicht rückgängig gemacht werden.`);
if (!ok) return;
await deleteFileCompletely(eingangFiles[idx]);
});
actionsSpan.appendChild(removeFromUnitBtn);
actionsSpan.appendChild(deleteFileBtn);
li.appendChild(nameSpan);
li.appendChild(actionsSpan);
li.addEventListener('click', () => {
currentIndex = idx;
currentSelectedFile = filename;
renderEingangList();
renderPreviewForCurrent();
});
eingangListEl.appendChild(li);
});
eingangCountEl.textContent = eingangFiles.length + (eingangFiles.length === 1 ? ' Datei' : ' Dateien');
}
async function loadWorksheetPairs() {
try {
const data = await apiFetch('/api/worksheet-pairs');
allWorksheetPairs = {};
(data.pairs || []).forEach((p) => {
allWorksheetPairs[p.original] = { clean_html: p.clean_html, clean_image: p.clean_image };
});
worksheetPairs = { ...allWorksheetPairs };
renderPreviewForCurrent();
} catch (e) {
console.error(e);
setStatus('Fehler beim Laden der Neuaufbau-Daten', String(e), 'error');
}
}
function renderPreviewForCurrent() {
if (!eingangFiles.length) {
const message = showOnlyUnitFiles && currentUnitId
? 'Dieser Lerneinheit sind noch keine Arbeitsblätter zugeordnet.'
: 'Keine Dateien vorhanden.';
previewContainer.innerHTML = `<div class="preview-placeholder">${message}</div>`;
return;
}
if (currentIndex < 0) currentIndex = 0;
if (currentIndex >= eingangFiles.length) currentIndex = eingangFiles.length - 1;
const filename = eingangFiles[currentIndex];
const entry = worksheetPairs[filename] || { clean_html: null, clean_image: null };
renderPreview(entry, currentIndex);
}
function renderThumbnailsInColumn(container) {
container.innerHTML = '';
if (eingangFiles.length <= 1) {
return; // Keine Thumbnails nötig wenn nur 1 oder 0 Dateien
}
// Zeige bis zu 5 Thumbnails (die nächsten Dateien nach dem aktuellen)
const maxThumbs = 5;
let thumbCount = 0;
for (let i = 0; i < eingangFiles.length && thumbCount < maxThumbs; i++) {
if (i === currentIndex) continue; // Aktuelles Dokument überspringen
const filename = eingangFiles[i];
const thumb = document.createElement('div');
thumb.className = 'preview-thumb';
const img = document.createElement('img');
img.src = '/preview-file/' + encodeURIComponent(filename);
img.alt = filename;
const label = document.createElement('div');
label.className = 'preview-thumb-label';
label.textContent = `${i + 1}`;
thumb.appendChild(img);
thumb.appendChild(label);
thumb.addEventListener('click', () => {
currentIndex = i;
renderEingangList();
renderPreviewForCurrent();
});
container.appendChild(thumb);
thumbCount++;
}
}
function renderPreview(entry, index) {
previewContainer.innerHTML = '';
const wrapper = document.createElement('div');
wrapper.className = 'compare-wrapper';
// Original
const originalSection = document.createElement('div');
originalSection.className = 'compare-section';
const origHeader = document.createElement('div');
origHeader.className = 'compare-header';
origHeader.innerHTML = '<span>Original-Scan</span><span>Alt (links)</span>';
const origBody = document.createElement('div');
origBody.className = 'compare-body';
const origInner = document.createElement('div');
origInner.className = 'compare-body-inner';
const img = document.createElement('img');
img.className = 'preview-img';
const imgSrc = '/preview-file/' + encodeURIComponent(eingangFiles[index]);
img.src = imgSrc;
img.alt = 'Original ' + eingangFiles[index];
img.addEventListener('dblclick', () => openLightbox(imgSrc, eingangFiles[index]));
origInner.appendChild(img);
origBody.appendChild(origInner);
originalSection.appendChild(origHeader);
originalSection.appendChild(origBody);
// Neu aufgebaut
const cleanSection = document.createElement('div');
cleanSection.className = 'compare-section';
const cleanHeader = document.createElement('div');
cleanHeader.className = 'compare-header';
cleanHeader.innerHTML = '<span>Neu aufgebautes Arbeitsblatt</span><span style="display:flex;align-items:center;gap:8px;"><button type="button" class="btn btn-sm btn-ghost no-print" id="btn-print-worksheet" style="padding:4px 10px;font-size:11px;">🖨️ Drucken</button><span>Neu (rechts)</span></span>';
const cleanBody = document.createElement('div');
cleanBody.className = 'compare-body';
const cleanInner = document.createElement('div');
cleanInner.className = 'compare-body-inner';
// Bevorzuge bereinigtes Bild über HTML (für pixel-genaue Darstellung)
if (entry.clean_image) {
const imgClean = document.createElement('img');
imgClean.className = 'preview-img';
const cleanSrc = '/preview-clean-file/' + encodeURIComponent(entry.clean_image);
imgClean.src = cleanSrc;
imgClean.alt = 'Neu aufgebaut ' + eingangFiles[index];
imgClean.addEventListener('dblclick', () => openLightbox(cleanSrc, eingangFiles[index] + ' (neu)'));
cleanInner.appendChild(imgClean);
} else if (entry.clean_html) {
const frame = document.createElement('iframe');
frame.className = 'clean-frame';
frame.src = '/api/clean-html/' + encodeURIComponent(entry.clean_html);
frame.title = 'Neu aufgebautes Arbeitsblatt';
frame.addEventListener('dblclick', () => {
window.open('/api/clean-html/' + encodeURIComponent(entry.clean_html), '_blank');
});
cleanInner.appendChild(frame);
} else {
cleanInner.innerHTML = '<div class="preview-placeholder">Noch keine Neuaufbau-Daten vorhanden.</div>';
}
cleanBody.appendChild(cleanInner);
cleanSection.appendChild(cleanHeader);
cleanSection.appendChild(cleanBody);
// Print-Button Event-Listener
const printWorksheetBtn = cleanHeader.querySelector('#btn-print-worksheet');
if (printWorksheetBtn) {
printWorksheetBtn.addEventListener('click', () => {
const currentFile = eingangFiles[currentIndex];
if (!currentFile) {
alert(t('worksheet_no_data') || 'Keine Arbeitsblatt-Daten vorhanden.');
return;
}
window.open('/api/print-worksheet/' + encodeURIComponent(currentFile), '_blank');
});
}
// Thumbnails in der Mitte
const thumbsColumn = document.createElement('div');
thumbsColumn.className = 'preview-thumbnails';
thumbsColumn.id = 'preview-thumbnails-middle';
renderThumbnailsInColumn(thumbsColumn);
wrapper.appendChild(originalSection);
wrapper.appendChild(thumbsColumn);
wrapper.appendChild(cleanSection);
// Navigation-Buttons hinzufügen
const navDiv = document.createElement('div');
navDiv.className = 'preview-nav';
const prevBtn = document.createElement('button');
prevBtn.type = 'button';
prevBtn.textContent = '';
prevBtn.disabled = currentIndex === 0;
prevBtn.addEventListener('click', () => {
if (currentIndex > 0) {
currentIndex--;
renderEingangList();
renderPreviewForCurrent();
}
});
const nextBtn = document.createElement('button');
nextBtn.type = 'button';
nextBtn.textContent = '';
nextBtn.disabled = currentIndex >= eingangFiles.length - 1;
nextBtn.addEventListener('click', () => {
if (currentIndex < eingangFiles.length - 1) {
currentIndex++;
renderEingangList();
renderPreviewForCurrent();
}
});
const positionSpan = document.createElement('span');
positionSpan.textContent = `${currentIndex + 1} von ${eingangFiles.length}`;
navDiv.appendChild(prevBtn);
navDiv.appendChild(positionSpan);
navDiv.appendChild(nextBtn);
wrapper.appendChild(navDiv);
previewContainer.appendChild(wrapper);
}
// --- Upload ---
btnUploadInline.addEventListener('click', async (ev) => {
ev.preventDefault();
ev.stopPropagation();
const files = fileInput.files;
if (!files || !files.length) {
alert('Bitte erst Dateien auswählen.');
return;
}
const formData = new FormData();
for (const file of files) {
formData.append('files', file);
}
try {
setStatus('Upload läuft …', 'Dateien werden in den Ordner „Eingang“ geschrieben.', 'busy');
const resp = await fetch('/api/upload-multi', {
method: 'POST',
body: formData,
});
if (!resp.ok) {
console.error('Upload-Fehler: HTTP', resp.status);
setStatus('Fehler beim Upload', 'Serverantwort: HTTP ' + resp.status, 'error');
return;
}
setStatus('Upload abgeschlossen', 'Dateien wurden gespeichert.');
fileInput.value = '';
// Liste neu laden
await loadEingangFiles();
await loadWorksheetPairs();
} catch (e) {
console.error('Netzwerkfehler beim Upload', e);
setStatus('Netzwerkfehler beim Upload', String(e), 'error');
}
});
// --- Vollpipeline ---
async function runFullPipeline() {
try {
setStatus('Entferne Handschrift …', 'Bilder werden aufbereitet.', 'busy');
await apiFetch('/api/remove-handwriting-all', { method: 'POST' });
setStatus('Analysiere Arbeitsblätter …', 'Struktur wird erkannt.', 'busy');
await apiFetch('/api/analyze-all', { method: 'POST' });
setStatus('Erzeuge HTML-Arbeitsblätter …', 'Neuaufbau läuft.', 'busy');
await apiFetch('/api/generate-clean', { method: 'POST' });
setStatus('Fertig', 'Alt & Neu können jetzt verglichen werden.');
await loadWorksheetPairs();
renderPreviewForCurrent();
} catch (e) {
console.error(e);
setStatus('Fehler in der Verarbeitung', String(e), 'error');
}
}
if (btnFullProcess) btnFullProcess.addEventListener('click', runFullPipeline);
if (btnOriginalGenerate) btnOriginalGenerate.addEventListener('click', runFullPipeline);
// --- Screen-Navigation (oben + Pager unten) ---
function updateScreen() {
if (currentScreen === 1) {
panelCompare.style.display = 'flex';
panelTiles.style.display = 'none';
pagerLabel.textContent = '1 von 2';
} else {
panelCompare.style.display = 'none';
panelTiles.style.display = 'flex';
pagerLabel.textContent = '2 von 2';
}
topNavItems.forEach((item) => {
const screen = Number(item.getAttribute('data-screen'));
if (screen === currentScreen) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
}
topNavItems.forEach((item) => {
item.addEventListener('click', () => {
const screen = Number(item.getAttribute('data-screen'));
currentScreen = screen;
updateScreen();
});
});
pagerPrev.addEventListener('click', () => {
if (currentScreen > 1) {
currentScreen -= 1;
updateScreen();
}
});
pagerNext.addEventListener('click', () => {
if (currentScreen < 2) {
currentScreen += 1;
updateScreen();
}
});
// --- Toggle-Kacheln ---
const tileToggles = document.querySelectorAll('.toggle-pill');
const cards = document.querySelectorAll('.card');
function updateTiles() {
let activeTiles = Object.keys(tileState).filter((k) => tileState[k]);
cards.forEach((card) => {
const key = card.getAttribute('data-tile');
if (!tileState[key]) {
card.classList.add('card-hidden');
} else {
card.classList.remove('card-hidden');
}
card.classList.remove('card-full');
});
if (activeTiles.length === 1) {
const only = activeTiles[0];
cards.forEach((card) => {
if (card.getAttribute('data-tile') === only) {
card.classList.add('card-full');
}
});
}
}
tileToggles.forEach((btn) => {
btn.addEventListener('click', () => {
const key = btn.getAttribute('data-tile');
tileState[key] = !tileState[key];
btn.classList.toggle('active', tileState[key]);
updateTiles();
});
});
// --- Lerneinheiten-Logik (Backend) ---
function updateUnitHeading(unit = null) {
if (!unit && currentUnitId && units && units.length) {
unit = units.find((u) => u.id === currentUnitId) || null;
}
let text = 'Keine Lerneinheit ausgewählt';
if (unit) {
const name = unit.label || unit.title || 'Lerneinheit';
text = 'Lerneinheit: ' + name;
}
if (unitHeading1) unitHeading1.textContent = text;
if (unitHeading2) unitHeading2.textContent = text;
}
function applyUnitFilter() {
let unit = null;
if (currentUnitId && units && units.length) {
unit = units.find((u) => u.id === currentUnitId) || null;
}
// Wenn Filter deaktiviert ODER keine Lerneinheit ausgewählt -> alle Dateien anzeigen
if (!showOnlyUnitFiles || !unit || !Array.isArray(unit.worksheet_files) || unit.worksheet_files.length === 0) {
eingangFiles = allEingangFiles.slice();
worksheetPairs = { ...allWorksheetPairs };
currentIndex = 0;
renderEingangList();
renderPreviewForCurrent();
updateUnitHeading(unit);
return;
}
// Filter aktiv: nur Dateien der aktuellen Lerneinheit anzeigen
const allowed = new Set(unit.worksheet_files || []);
eingangFiles = allEingangFiles.filter((f) => allowed.has(f));
const filteredPairs = {};
Object.keys(allWorksheetPairs).forEach((key) => {
if (allowed.has(key)) {
filteredPairs[key] = allWorksheetPairs[key];
}
});
worksheetPairs = filteredPairs;
currentIndex = 0;
renderEingangList();
renderPreviewForCurrent();
updateUnitHeading(unit);
}
async function loadLearningUnits() {
try {
const resp = await fetch('/api/learning-units/');
if (!resp.ok) {
console.error('Fehler beim Laden der Lerneinheiten', resp.status);
return;
}
units = await resp.json();
if (units.length && !currentUnitId) {
currentUnitId = units[0].id;
}
renderUnits();
applyUnitFilter();
} catch (e) {
console.error('Netzwerkfehler beim Laden der Lerneinheiten', e);
}
}
function renderUnits() {
unitListEl.innerHTML = '';
if (!units.length) {
const li = document.createElement('li');
li.className = 'unit-item';
li.textContent = 'Noch keine Lerneinheiten angelegt.';
unitListEl.appendChild(li);
updateUnitHeading(null);
return;
}
units.forEach((u) => {
const li = document.createElement('li');
li.className = 'unit-item';
if (u.id === currentUnitId) {
li.classList.add('active');
}
const contentDiv = document.createElement('div');
contentDiv.style.flex = '1';
contentDiv.style.minWidth = '0';
const titleEl = document.createElement('div');
titleEl.textContent = u.label || u.title || 'Lerneinheit';
const metaEl = document.createElement('div');
metaEl.className = 'unit-item-meta';
const metaParts = [];
if (u.meta) {
metaParts.push(u.meta);
}
if (Array.isArray(u.worksheet_files)) {
metaParts.push('Blätter: ' + u.worksheet_files.length);
}
metaEl.textContent = metaParts.join(' · ');
contentDiv.appendChild(titleEl);
contentDiv.appendChild(metaEl);
// Delete-Button
const deleteBtn = document.createElement('span');
deleteBtn.textContent = '🗑️';
deleteBtn.style.cursor = 'pointer';
deleteBtn.style.fontSize = '12px';
deleteBtn.style.color = '#ef4444';
deleteBtn.title = 'Lerneinheit löschen';
deleteBtn.addEventListener('click', async (ev) => {
ev.stopPropagation();
const ok = confirm(`Lerneinheit "${u.label || u.title}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.`);
if (!ok) return;
await deleteLearningUnit(u.id);
});
li.appendChild(contentDiv);
li.appendChild(deleteBtn);
li.addEventListener('click', () => {
currentUnitId = u.id;
renderUnits();
applyUnitFilter();
});
unitListEl.appendChild(li);
});
}
async function addUnitFromForm() {
const student = (unitStudentInput.value || '').trim();
const subject = (unitSubjectInput.value || '').trim();
const grade = (unitGradeInput && unitGradeInput.value || '').trim();
const title = (unitTitleInput.value || '').trim();
if (!student && !subject && !title) {
alert('Bitte mindestens einen Wert (Schüler/in, Fach oder Thema) eintragen.');
return;
}
const payload = {
student,
subject,
title,
grade,
};
try {
const resp = await fetch('/api/learning-units/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!resp.ok) {
console.error('Fehler beim Anlegen der Lerneinheit', resp.status);
alert('Lerneinheit konnte nicht angelegt werden.');
return;
}
const created = await resp.json();
units.push(created);
currentUnitId = created.id;
unitStudentInput.value = '';
unitSubjectInput.value = '';
unitTitleInput.value = '';
if (unitGradeInput) unitGradeInput.value = '';
renderUnits();
applyUnitFilter();
} catch (e) {
console.error('Netzwerkfehler beim Anlegen der Lerneinheit', e);
alert('Netzwerkfehler beim Anlegen der Lerneinheit.');
}
}
function getCurrentWorksheetBasename() {
if (!eingangFiles.length) return null;
if (currentIndex < 0 || currentIndex >= eingangFiles.length) return null;
return eingangFiles[currentIndex];
}
async function attachCurrentWorksheetToUnit() {
if (!currentUnitId) {
alert('Bitte zuerst eine Lerneinheit auswählen oder anlegen.');
return;
}
const basename = getCurrentWorksheetBasename();
if (!basename) {
alert('Bitte zuerst ein Arbeitsblatt im linken Bereich auswählen.');
return;
}
const payload = { worksheet_files: [basename] };
try {
const resp = await fetch(`/api/learning-units/${currentUnitId}/attach-worksheets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!resp.ok) {
console.error('Fehler beim Zuordnen des Arbeitsblatts', resp.status);
alert('Arbeitsblatt konnte nicht zugeordnet werden.');
return;
}
const updated = await resp.json();
const idx = units.findIndex((u) => u.id === updated.id);
if (idx !== -1) {
units[idx] = updated;
}
renderUnits();
applyUnitFilter();
} catch (e) {
console.error('Netzwerkfehler beim Zuordnen des Arbeitsblatts', e);
alert('Netzwerkfehler beim Zuordnen des Arbeitsblatts.');
}
}
async function removeWorksheetFromCurrentUnit(filename) {
if (!currentUnitId) {
alert('Bitte zuerst eine Lerneinheit auswählen.');
return;
}
if (!filename) {
alert('Fehler: kein Dateiname übergeben.');
return;
}
const payload = { worksheet_file: filename };
try {
const resp = await fetch(`/api/learning-units/${currentUnitId}/remove-worksheet`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!resp.ok) {
console.error('Fehler beim Entfernen des Arbeitsblatts', resp.status);
alert('Arbeitsblatt konnte nicht aus der Lerneinheit entfernt werden.');
return;
}
const updated = await resp.json();
const idx = units.findIndex((u) => u.id === updated.id);
if (idx !== -1) {
units[idx] = updated;
}
renderUnits();
applyUnitFilter();
} catch (e) {
console.error('Netzwerkfehler beim Entfernen des Arbeitsblatts', e);
alert('Netzwerkfehler beim Entfernen des Arbeitsblatts.');
}
}
async function deleteFileCompletely(filename) {
if (!filename) {
alert('Fehler: kein Dateiname übergeben.');
return;
}
try {
setStatus('Lösche Datei …', filename, 'busy');
const resp = await fetch(`/api/eingang-dateien/${encodeURIComponent(filename)}`, {
method: 'DELETE',
});
if (!resp.ok) {
console.error('Fehler beim Löschen der Datei', resp.status);
setStatus('Fehler beim Löschen', filename, 'error');
alert('Datei konnte nicht gelöscht werden.');
return;
}
const result = await resp.json();
if (result.status === 'OK') {
setStatus('Datei gelöscht', filename);
// Dateien neu laden
await loadEingangFiles();
await loadWorksheetPairs();
await loadLearningUnits();
} else {
setStatus('Fehler', result.message, 'error');
alert(result.message);
}
} catch (e) {
console.error('Netzwerkfehler beim Löschen der Datei', e);
setStatus('Netzwerkfehler', String(e), 'error');
alert('Netzwerkfehler beim Löschen der Datei.');
}
}
async function deleteLearningUnit(unitId) {
if (!unitId) {
alert('Fehler: keine Lerneinheit-ID übergeben.');
return;
}
try {
setStatus('Lösche Lerneinheit …', '', 'busy');
const resp = await fetch(`/api/learning-units/${unitId}`, {
method: 'DELETE',
});
if (!resp.ok) {
console.error('Fehler beim Löschen der Lerneinheit', resp.status);
setStatus('Fehler beim Löschen', '', 'error');
alert('Lerneinheit konnte nicht gelöscht werden.');
return;
}
const result = await resp.json();
if (result.status === 'deleted') {
setStatus('Lerneinheit gelöscht', '');
// Lerneinheit aus der lokalen Liste entfernen
units = units.filter((u) => u.id !== unitId);
// Wenn die gelöschte Einheit ausgewählt war, Auswahl zurücksetzen
if (currentUnitId === unitId) {
currentUnitId = units.length > 0 ? units[0].id : null;
}
renderUnits();
applyUnitFilter();
} else {
setStatus('Fehler', 'Unbekannter Fehler', 'error');
alert('Fehler beim Löschen der Lerneinheit.');
}
} catch (e) {
console.error('Netzwerkfehler beim Löschen der Lerneinheit', e);
setStatus('Netzwerkfehler', String(e), 'error');
alert('Netzwerkfehler beim Löschen der Lerneinheit.');
}
}
if (btnAddUnit) {
btnAddUnit.addEventListener('click', (ev) => {
ev.preventDefault();
addUnitFromForm();
});
}
if (btnAttachCurrentToLu) {
btnAttachCurrentToLu.addEventListener('click', (ev) => {
ev.preventDefault();
attachCurrentWorksheetToUnit();
});
}
// --- Filter-Toggle ---
if (btnToggleFilter) {
btnToggleFilter.addEventListener('click', () => {
showOnlyUnitFiles = !showOnlyUnitFiles;
if (showOnlyUnitFiles) {
btnToggleFilter.textContent = 'Nur Lerneinheit';
btnToggleFilter.classList.add('btn-primary');
} else {
btnToggleFilter.textContent = 'Alle Dateien';
btnToggleFilter.classList.remove('btn-primary');
}
applyUnitFilter();
});
}
// --- Multiple Choice Logik ---
const btnMcGenerate = document.getElementById('btn-mc-generate');
const btnMcShow = document.getElementById('btn-mc-show');
const btnMcPrint = document.getElementById('btn-mc-print');
const mcPreview = document.getElementById('mc-preview');
const mcBadge = document.getElementById('mc-badge');
const mcModal = document.getElementById('mc-modal');
const mcModalBody = document.getElementById('mc-modal-body');
const mcModalClose = document.getElementById('mc-modal-close');
let currentMcData = null;
let mcAnswers = {}; // Speichert Nutzerantworten
async function generateMcQuestions() {
try {
setStatus('Generiere MC-Fragen …', 'Bitte warten, KI arbeitet.', 'busy');
if (mcBadge) mcBadge.textContent = 'Generiert...';
const resp = await fetch('/api/generate-mc', { method: 'POST' });
if (!resp.ok) {
throw new Error('HTTP ' + resp.status);
}
const result = await resp.json();
if (result.status === 'OK' && result.generated.length > 0) {
setStatus('MC-Fragen generiert', result.generated.length + ' Dateien erstellt.');
if (mcBadge) mcBadge.textContent = 'Fertig';
if (btnMcShow) btnMcShow.style.display = 'inline-block';
if (btnMcPrint) btnMcPrint.style.display = 'inline-block';
// Lade die erste MC-Datei für Vorschau
await loadMcPreviewForCurrent();
} else if (result.errors && result.errors.length > 0) {
setStatus('Fehler bei MC-Generierung', result.errors[0].error, 'error');
if (mcBadge) mcBadge.textContent = 'Fehler';
} else {
setStatus('Keine MC-Fragen generiert', 'Möglicherweise fehlen Analyse-Daten.', 'error');
if (mcBadge) mcBadge.textContent = 'Bereit';
}
} catch (e) {
console.error('MC-Generierung fehlgeschlagen:', e);
setStatus('Fehler bei MC-Generierung', String(e), 'error');
if (mcBadge) mcBadge.textContent = 'Fehler';
}
}
async function loadMcPreviewForCurrent() {
if (!eingangFiles.length) {
if (mcPreview) mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Arbeitsblätter vorhanden.</div>';
return;
}
const currentFile = eingangFiles[currentIndex];
if (!currentFile) return;
try {
const resp = await fetch('/api/mc-data/' + encodeURIComponent(currentFile));
const result = await resp.json();
if (result.status === 'OK' && result.data) {
currentMcData = result.data;
renderMcPreview(result.data);
if (btnMcShow) btnMcShow.style.display = 'inline-block';
if (btnMcPrint) btnMcPrint.style.display = 'inline-block';
} else {
if (mcPreview) mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Noch keine MC-Fragen für dieses Arbeitsblatt generiert.</div>';
currentMcData = null;
if (btnMcPrint) btnMcPrint.style.display = 'none';
}
} catch (e) {
console.error('Fehler beim Laden der MC-Daten:', e);
if (mcPreview) mcPreview.innerHTML = '';
}
}
function renderMcPreview(mcData) {
if (!mcPreview) return;
if (!mcData || !mcData.questions || mcData.questions.length === 0) {
mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Fragen vorhanden.</div>';
return;
}
const questions = mcData.questions;
const metadata = mcData.metadata || {};
let html = '';
// Zeige Metadaten
if (metadata.grade_level || metadata.subject) {
html += '<div class="mc-stats">';
if (metadata.subject) {
html += '<div class="mc-stats-item"><strong>Fach:</strong> ' + metadata.subject + '</div>';
}
if (metadata.grade_level) {
html += '<div class="mc-stats-item"><strong>Stufe:</strong> ' + metadata.grade_level + '</div>';
}
html += '<div class="mc-stats-item"><strong>Fragen:</strong> ' + questions.length + '</div>';
html += '</div>';
}
// Zeige erste 2 Fragen als Vorschau
const previewQuestions = questions.slice(0, 2);
previewQuestions.forEach((q, idx) => {
html += '<div class="mc-question">';
html += '<div class="mc-question-text">' + (idx + 1) + '. ' + q.question + '</div>';
html += '<div class="mc-options">';
q.options.forEach(opt => {
html += '<div class="mc-option" data-qid="' + q.id + '" data-opt="' + opt.id + '">';
html += '<span class="mc-option-label">' + opt.id + ')</span> ' + opt.text;
html += '</div>';
});
html += '</div>';
html += '</div>';
});
if (questions.length > 2) {
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:8px;">+ ' + (questions.length - 2) + ' weitere Fragen</div>';
}
mcPreview.innerHTML = html;
// Event-Listener für Antwort-Auswahl
mcPreview.querySelectorAll('.mc-option').forEach(optEl => {
optEl.addEventListener('click', () => handleMcOptionClick(optEl));
});
}
function handleMcOptionClick(optEl) {
const qid = optEl.getAttribute('data-qid');
const optId = optEl.getAttribute('data-opt');
if (!currentMcData) return;
// Finde die Frage
const question = currentMcData.questions.find(q => q.id === qid);
if (!question) return;
// Markiere alle Optionen dieser Frage
const questionEl = optEl.closest('.mc-question');
const allOptions = questionEl.querySelectorAll('.mc-option');
allOptions.forEach(opt => {
opt.classList.remove('selected', 'correct', 'incorrect');
const thisOptId = opt.getAttribute('data-opt');
if (thisOptId === question.correct_answer) {
opt.classList.add('correct');
} else if (thisOptId === optId) {
opt.classList.add('incorrect');
}
});
// Speichere Antwort
mcAnswers[qid] = optId;
// Zeige Feedback wenn gewünscht
const isCorrect = optId === question.correct_answer;
let feedbackEl = questionEl.querySelector('.mc-feedback');
if (!feedbackEl) {
feedbackEl = document.createElement('div');
feedbackEl.className = 'mc-feedback';
questionEl.appendChild(feedbackEl);
}
if (isCorrect) {
feedbackEl.style.background = 'rgba(34,197,94,0.1)';
feedbackEl.style.borderColor = 'rgba(34,197,94,0.3)';
feedbackEl.style.color = 'var(--bp-accent)';
feedbackEl.textContent = 'Richtig! ' + (question.explanation || '');
} else {
feedbackEl.style.background = 'rgba(239,68,68,0.1)';
feedbackEl.style.borderColor = 'rgba(239,68,68,0.3)';
feedbackEl.style.color = '#ef4444';
feedbackEl.textContent = 'Leider falsch. ' + (question.explanation || '');
}
}
function openMcModal() {
if (!currentMcData || !currentMcData.questions) {
alert('Keine MC-Fragen vorhanden. Bitte zuerst generieren.');
return;
}
mcAnswers = {}; // Reset Antworten
renderMcModal(currentMcData);
mcModal.classList.remove('hidden');
}
function closeMcModal() {
mcModal.classList.add('hidden');
}
function renderMcModal(mcData) {
const questions = mcData.questions;
const metadata = mcData.metadata || {};
let html = '';
// Header mit Metadaten
html += '<div class="mc-stats" style="margin-bottom:16px;">';
if (metadata.source_title) {
html += '<div class="mc-stats-item"><strong>Arbeitsblatt:</strong> ' + metadata.source_title + '</div>';
}
if (metadata.subject) {
html += '<div class="mc-stats-item"><strong>Fach:</strong> ' + metadata.subject + '</div>';
}
if (metadata.grade_level) {
html += '<div class="mc-stats-item"><strong>Stufe:</strong> ' + metadata.grade_level + '</div>';
}
html += '</div>';
// Alle Fragen
questions.forEach((q, idx) => {
html += '<div class="mc-question" data-qid="' + q.id + '">';
html += '<div class="mc-question-text">' + (idx + 1) + '. ' + q.question + '</div>';
html += '<div class="mc-options">';
q.options.forEach(opt => {
html += '<div class="mc-option" data-qid="' + q.id + '" data-opt="' + opt.id + '">';
html += '<span class="mc-option-label">' + opt.id + ')</span> ' + opt.text;
html += '</div>';
});
html += '</div>';
html += '</div>';
});
// Auswertungs-Button
html += '<div style="margin-top:16px;text-align:center;">';
html += '<button class="btn btn-primary" id="btn-mc-evaluate">Auswerten</button>';
html += '</div>';
mcModalBody.innerHTML = html;
// Event-Listener
mcModalBody.querySelectorAll('.mc-option').forEach(optEl => {
optEl.addEventListener('click', () => {
const qid = optEl.getAttribute('data-qid');
const optId = optEl.getAttribute('data-opt');
// Deselektiere andere Optionen der gleichen Frage
const questionEl = optEl.closest('.mc-question');
questionEl.querySelectorAll('.mc-option').forEach(o => o.classList.remove('selected'));
optEl.classList.add('selected');
mcAnswers[qid] = optId;
});
});
const btnEvaluate = document.getElementById('btn-mc-evaluate');
if (btnEvaluate) {
btnEvaluate.addEventListener('click', evaluateMcQuiz);
}
}
function evaluateMcQuiz() {
if (!currentMcData) return;
let correct = 0;
let total = currentMcData.questions.length;
currentMcData.questions.forEach(q => {
const questionEl = mcModalBody.querySelector('.mc-question[data-qid="' + q.id + '"]');
if (!questionEl) return;
const userAnswer = mcAnswers[q.id];
const allOptions = questionEl.querySelectorAll('.mc-option');
allOptions.forEach(opt => {
opt.classList.remove('correct', 'incorrect');
const optId = opt.getAttribute('data-opt');
if (optId === q.correct_answer) {
opt.classList.add('correct');
} else if (optId === userAnswer && userAnswer !== q.correct_answer) {
opt.classList.add('incorrect');
}
});
// Zeige Erklärung
let feedbackEl = questionEl.querySelector('.mc-feedback');
if (!feedbackEl) {
feedbackEl = document.createElement('div');
feedbackEl.className = 'mc-feedback';
questionEl.appendChild(feedbackEl);
}
if (userAnswer === q.correct_answer) {
correct++;
feedbackEl.style.background = 'rgba(34,197,94,0.1)';
feedbackEl.style.borderColor = 'rgba(34,197,94,0.3)';
feedbackEl.style.color = 'var(--bp-accent)';
feedbackEl.textContent = 'Richtig! ' + (q.explanation || '');
} else if (userAnswer) {
feedbackEl.style.background = 'rgba(239,68,68,0.1)';
feedbackEl.style.borderColor = 'rgba(239,68,68,0.3)';
feedbackEl.style.color = '#ef4444';
feedbackEl.textContent = 'Falsch. ' + (q.explanation || '');
} else {
feedbackEl.style.background = 'rgba(148,163,184,0.1)';
feedbackEl.style.borderColor = 'rgba(148,163,184,0.3)';
feedbackEl.style.color = 'var(--bp-text-muted)';
feedbackEl.textContent = 'Nicht beantwortet. Richtig wäre: ' + q.correct_answer.toUpperCase();
}
});
// Zeige Gesamtergebnis
const resultHtml = '<div style="margin-top:16px;padding:12px;background:rgba(15,23,42,0.6);border-radius:8px;text-align:center;">' +
'<div style="font-size:18px;font-weight:600;">' + correct + ' von ' + total + ' richtig</div>' +
'<div style="font-size:12px;color:var(--bp-text-muted);margin-top:4px;">' + Math.round(correct / total * 100) + '% korrekt</div>' +
'</div>';
const existingResult = mcModalBody.querySelector('.mc-result');
if (existingResult) {
existingResult.remove();
}
const resultDiv = document.createElement('div');
resultDiv.className = 'mc-result';
resultDiv.innerHTML = resultHtml;
mcModalBody.appendChild(resultDiv);
}
function openMcPrintDialog() {
if (!currentMcData) {
alert(t('mc_no_questions') || 'Keine MC-Fragen vorhanden.');
return;
}
const currentFile = eingangFiles[currentIndex];
const choice = confirm((t('mc_print_with_answers') || 'Mit Lösungen drucken?') + '\\n\\nOK = Lösungsblatt mit markierten Antworten\\nAbbrechen = Übungsblatt ohne Lösungen');
const url = '/api/print-mc/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
window.open(url, '_blank');
}
// Event Listener für MC-Buttons
if (btnMcGenerate) {
btnMcGenerate.addEventListener('click', generateMcQuestions);
}
if (btnMcShow) {
btnMcShow.addEventListener('click', openMcModal);
}
if (btnMcPrint) {
btnMcPrint.addEventListener('click', openMcPrintDialog);
}
if (mcModalClose) {
mcModalClose.addEventListener('click', closeMcModal);
}
if (mcModal) {
mcModal.addEventListener('click', (ev) => {
if (ev.target === mcModal) {
closeMcModal();
}
});
}
// --- Lückentext (Cloze) Logik ---
const btnClozeGenerate = document.getElementById('btn-cloze-generate');
const btnClozeShow = document.getElementById('btn-cloze-show');
const btnClozePrint = document.getElementById('btn-cloze-print');
const clozePreview = document.getElementById('cloze-preview');
const clozeBadge = document.getElementById('cloze-badge');
const clozeLanguageSelect = document.getElementById('cloze-language');
const clozeModal = document.getElementById('cloze-modal');
const clozeModalBody = document.getElementById('cloze-modal-body');
const clozeModalClose = document.getElementById('cloze-modal-close');
let currentClozeData = null;
let clozeAnswers = {}; // Speichert Nutzerantworten
async function generateClozeTexts() {
const targetLang = clozeLanguageSelect ? clozeLanguageSelect.value : 'tr';
try {
setStatus('Generiere Lückentexte …', 'Bitte warten, KI arbeitet.', 'busy');
if (clozeBadge) clozeBadge.textContent = 'Generiert...';
const resp = await fetch('/api/generate-cloze?target_language=' + targetLang, { method: 'POST' });
if (!resp.ok) {
throw new Error('HTTP ' + resp.status);
}
const result = await resp.json();
if (result.status === 'OK' && result.generated.length > 0) {
setStatus('Lückentexte generiert', result.generated.length + ' Dateien erstellt.');
if (clozeBadge) clozeBadge.textContent = 'Fertig';
if (btnClozeShow) btnClozeShow.style.display = 'inline-block';
if (btnClozePrint) btnClozePrint.style.display = 'inline-block';
// Lade Vorschau für aktuelle Datei
await loadClozePreviewForCurrent();
} else if (result.errors && result.errors.length > 0) {
setStatus('Fehler bei Lückentext-Generierung', result.errors[0].error, 'error');
if (clozeBadge) clozeBadge.textContent = 'Fehler';
} else {
setStatus('Keine Lückentexte generiert', 'Möglicherweise fehlen Analyse-Daten.', 'error');
if (clozeBadge) clozeBadge.textContent = 'Bereit';
}
} catch (e) {
console.error('Lückentext-Generierung fehlgeschlagen:', e);
setStatus('Fehler bei Lückentext-Generierung', String(e), 'error');
if (clozeBadge) clozeBadge.textContent = 'Fehler';
}
}
async function loadClozePreviewForCurrent() {
if (!eingangFiles.length) {
if (clozePreview) clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Arbeitsblätter vorhanden.</div>';
return;
}
const currentFile = eingangFiles[currentIndex];
if (!currentFile) return;
try {
const resp = await fetch('/api/cloze-data/' + encodeURIComponent(currentFile));
const result = await resp.json();
if (result.status === 'OK' && result.data) {
currentClozeData = result.data;
renderClozePreview(result.data);
if (btnClozeShow) btnClozeShow.style.display = 'inline-block';
if (btnClozePrint) btnClozePrint.style.display = 'inline-block';
} else {
if (clozePreview) clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Noch keine Lückentexte für dieses Arbeitsblatt generiert.</div>';
currentClozeData = null;
if (btnClozeShow) btnClozeShow.style.display = 'none';
if (btnClozePrint) btnClozePrint.style.display = 'none';
}
} catch (e) {
console.error('Fehler beim Laden der Lückentext-Daten:', e);
if (clozePreview) clozePreview.innerHTML = '';
}
}
function renderClozePreview(clozeData) {
if (!clozePreview) return;
if (!clozeData || !clozeData.cloze_items || clozeData.cloze_items.length === 0) {
clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Lückentexte vorhanden.</div>';
return;
}
const items = clozeData.cloze_items;
const metadata = clozeData.metadata || {};
let html = '';
// Statistiken
html += '<div class="cloze-stats">';
if (metadata.subject) {
html += '<div><strong>Fach:</strong> ' + metadata.subject + '</div>';
}
if (metadata.grade_level) {
html += '<div><strong>Stufe:</strong> ' + metadata.grade_level + '</div>';
}
html += '<div><strong>Sätze:</strong> ' + items.length + '</div>';
if (metadata.total_gaps) {
html += '<div><strong>Lücken:</strong> ' + metadata.total_gaps + '</div>';
}
html += '</div>';
// Zeige erste 2 Sätze als Vorschau
const previewItems = items.slice(0, 2);
previewItems.forEach((item, idx) => {
html += '<div class="cloze-item">';
html += '<div class="cloze-sentence">' + (idx + 1) + '. ' + item.sentence_with_gaps.replace(/___/g, '<span class="cloze-gap">___</span>') + '</div>';
// Übersetzung anzeigen
if (item.translation && item.translation.full_sentence) {
html += '<div class="cloze-translation">';
html += '<div class="cloze-translation-label">' + (item.translation.language_name || 'Übersetzung') + ':</div>';
html += item.translation.full_sentence;
html += '</div>';
}
html += '</div>';
});
if (items.length > 2) {
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:8px;">+ ' + (items.length - 2) + ' weitere Sätze</div>';
}
clozePreview.innerHTML = html;
}
function openClozeModal() {
if (!currentClozeData || !currentClozeData.cloze_items) {
alert('Keine Lückentexte vorhanden. Bitte zuerst generieren.');
return;
}
clozeAnswers = {}; // Reset Antworten
renderClozeModal(currentClozeData);
clozeModal.classList.remove('hidden');
}
function closeClozeModal() {
clozeModal.classList.add('hidden');
}
function renderClozeModal(clozeData) {
const items = clozeData.cloze_items;
const metadata = clozeData.metadata || {};
let html = '';
// Header
html += '<div class="cloze-stats" style="margin-bottom:16px;">';
if (metadata.source_title) {
html += '<div><strong>Arbeitsblatt:</strong> ' + metadata.source_title + '</div>';
}
if (metadata.total_gaps) {
html += '<div><strong>Lücken gesamt:</strong> ' + metadata.total_gaps + '</div>';
}
html += '</div>';
html += '<div style="font-size:12px;color:var(--bp-text-muted);margin-bottom:12px;">Fülle die Lücken aus und klicke auf "Prüfen".</div>';
// Alle Sätze mit Eingabefeldern
items.forEach((item, idx) => {
html += '<div class="cloze-item" data-cid="' + item.id + '">';
// Satz mit Eingabefeldern statt ___
let sentenceHtml = item.sentence_with_gaps;
const gaps = item.gaps || [];
// Ersetze ___ durch Eingabefelder
let gapIndex = 0;
sentenceHtml = sentenceHtml.replace(/___/g, () => {
const gap = gaps[gapIndex] || { id: 'g' + gapIndex, word: '' };
const inputId = item.id + '_' + gap.id;
gapIndex++;
return '<input type="text" class="cloze-gap-input" data-cid="' + item.id + '" data-gid="' + gap.id + '" data-answer="' + gap.word + '" id="input_' + inputId + '" autocomplete="off">';
});
html += '<div class="cloze-sentence">' + (idx + 1) + '. ' + sentenceHtml + '</div>';
// Übersetzung als Hilfe
if (item.translation && item.translation.sentence_with_gaps) {
html += '<div class="cloze-translation">';
html += '<div class="cloze-translation-label">' + (item.translation.language_name || 'Übersetzung') + ' (mit Lücken):</div>';
html += item.translation.sentence_with_gaps;
html += '</div>';
}
html += '</div>';
});
// Buttons
html += '<div style="margin-top:16px;text-align:center;display:flex;gap:8px;justify-content:center;">';
html += '<button class="btn btn-primary" id="btn-cloze-check">Prüfen</button>';
html += '<button class="btn btn-ghost" id="btn-cloze-show-answers">Lösungen zeigen</button>';
html += '</div>';
clozeModalBody.innerHTML = html;
// Event-Listener für Prüfen-Button
const btnCheck = document.getElementById('btn-cloze-check');
if (btnCheck) {
btnCheck.addEventListener('click', checkClozeAnswers);
}
// Event-Listener für Lösungen zeigen
const btnShowAnswers = document.getElementById('btn-cloze-show-answers');
if (btnShowAnswers) {
btnShowAnswers.addEventListener('click', showClozeAnswers);
}
// Enter-Taste zum Prüfen
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
checkClozeAnswers();
}
});
});
}
function checkClozeAnswers() {
let correct = 0;
let total = 0;
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
const userAnswer = input.value.trim().toLowerCase();
const correctAnswer = input.getAttribute('data-answer').toLowerCase();
total++;
// Entferne vorherige Klassen
input.classList.remove('correct', 'incorrect');
if (userAnswer === correctAnswer) {
input.classList.add('correct');
correct++;
} else if (userAnswer !== '') {
input.classList.add('incorrect');
}
});
// Zeige Ergebnis
let existingResult = clozeModalBody.querySelector('.cloze-result');
if (existingResult) existingResult.remove();
const resultHtml = '<div class="cloze-result" style="margin-top:16px;padding:12px;background:rgba(15,23,42,0.6);border-radius:8px;text-align:center;">' +
'<div style="font-size:18px;font-weight:600;">' + correct + ' von ' + total + ' richtig</div>' +
'<div style="font-size:12px;color:var(--bp-text-muted);margin-top:4px;">' + Math.round(correct / total * 100) + '% korrekt</div>' +
'</div>';
const resultDiv = document.createElement('div');
resultDiv.innerHTML = resultHtml;
clozeModalBody.appendChild(resultDiv.firstChild);
}
function showClozeAnswers() {
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
const correctAnswer = input.getAttribute('data-answer');
input.value = correctAnswer;
input.classList.remove('incorrect');
input.classList.add('correct');
});
}
function openClozePrintDialog() {
if (!currentClozeData) {
alert(t('cloze_no_texts') || 'Keine Lückentexte vorhanden.');
return;
}
const currentFile = eingangFiles[currentIndex];
// Öffne Druck-Optionen
const choice = confirm((t('cloze_print_with_answers') || 'Mit Lösungen drucken?') + '\\n\\nOK = Mit ausgefüllten Lücken\\nAbbrechen = Übungsblatt mit Wortbank');
const url = '/api/print-cloze/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
window.open(url, '_blank');
}
// Event Listener für Cloze-Buttons
if (btnClozeGenerate) {
btnClozeGenerate.addEventListener('click', generateClozeTexts);
}
if (btnClozeShow) {
btnClozeShow.addEventListener('click', openClozeModal);
}
if (btnClozePrint) {
btnClozePrint.addEventListener('click', openClozePrintDialog);
}
if (clozeModalClose) {
clozeModalClose.addEventListener('click', closeClozeModal);
}
if (clozeModal) {
clozeModal.addEventListener('click', (ev) => {
if (ev.target === clozeModal) {
closeClozeModal();
}
});
}
// --- Mindmap Lernposter Logik ---
const btnMindmapGenerate = document.getElementById('btn-mindmap-generate');
const btnMindmapShow = document.getElementById('btn-mindmap-show');
const btnMindmapPrint = document.getElementById('btn-mindmap-print');
const mindmapPreview = document.getElementById('mindmap-preview');
const mindmapBadge = document.getElementById('mindmap-badge');
let currentMindmapData = null;
async function generateMindmap() {
const currentFile = eingangFiles[currentIndex];
if (!currentFile) {
setStatus('error', t('select_file_first') || 'Bitte zuerst eine Datei auswählen');
return;
}
if (mindmapBadge) {
mindmapBadge.textContent = t('generating') || 'Generiere...';
mindmapBadge.className = 'card-badge badge-working';
}
setStatus('working', t('generating_mindmap') || 'Erstelle Mindmap...');
try {
const resp = await fetch('/api/generate-mindmap/' + encodeURIComponent(currentFile), {
method: 'POST'
});
const data = await resp.json();
if (data.status === 'OK') {
if (mindmapBadge) {
mindmapBadge.textContent = t('ready') || 'Fertig';
mindmapBadge.className = 'card-badge badge-success';
}
setStatus('ok', t('mindmap_generated') || 'Mindmap erstellt!');
// Lade Mindmap-Daten
await loadMindmapData();
} else if (data.status === 'NOT_FOUND') {
if (mindmapBadge) {
mindmapBadge.textContent = t('no_analysis') || 'Keine Analyse';
mindmapBadge.className = 'card-badge badge-error';
}
setStatus('error', t('analyze_first') || 'Bitte zuerst analysieren (Neuaufbau starten)');
} else {
throw new Error(data.message || 'Fehler bei der Mindmap-Generierung');
}
} catch (err) {
console.error('Mindmap error:', err);
if (mindmapBadge) {
mindmapBadge.textContent = t('error') || 'Fehler';
mindmapBadge.className = 'card-badge badge-error';
}
setStatus('error', err.message);
}
}
async function loadMindmapData() {
if (!eingangFiles.length) {
if (mindmapPreview) mindmapPreview.innerHTML = '';
return;
}
const currentFile = eingangFiles[currentIndex];
if (!currentFile) return;
try {
const resp = await fetch('/api/mindmap-data/' + encodeURIComponent(currentFile));
const data = await resp.json();
if (data.status === 'OK' && data.data) {
currentMindmapData = data.data;
renderMindmapPreview();
if (btnMindmapShow) btnMindmapShow.style.display = 'inline-block';
if (btnMindmapPrint) btnMindmapPrint.style.display = 'inline-block';
if (mindmapBadge) {
mindmapBadge.textContent = t('ready') || 'Fertig';
mindmapBadge.className = 'card-badge badge-success';
}
} else {
currentMindmapData = null;
if (mindmapPreview) mindmapPreview.innerHTML = '';
if (btnMindmapShow) btnMindmapShow.style.display = 'none';
if (btnMindmapPrint) btnMindmapPrint.style.display = 'none';
if (mindmapBadge) {
mindmapBadge.textContent = t('ready') || 'Bereit';
mindmapBadge.className = 'card-badge';
}
}
} catch (err) {
console.error('Error loading mindmap:', err);
}
}
function renderMindmapPreview() {
if (!mindmapPreview) return;
if (!currentMindmapData) {
mindmapPreview.innerHTML = '';
return;
}
const topic = currentMindmapData.topic || 'Thema';
const categories = currentMindmapData.categories || [];
const categoryCount = categories.length;
const termCount = categories.reduce((sum, cat) => sum + (cat.terms ? cat.terms.length : 0), 0);
mindmapPreview.innerHTML = '<div style="margin-top:10px;padding:12px;background:linear-gradient(135deg,#f0f9ff,#e0f2fe);border-radius:10px;text-align:center;">' +
'<div style="font-size:18px;font-weight:bold;color:#0369a1;margin-bottom:8px;">' + topic + '</div>' +
'<div style="font-size:12px;color:#64748b;">' + categoryCount + ' ' + (t('categories') || 'Kategorien') + ' | ' + termCount + ' ' + (t('terms') || 'Begriffe') + '</div>' +
'</div>';
}
function openMindmapView() {
const currentFile = eingangFiles[currentIndex];
if (!currentFile) return;
window.open('/api/mindmap-html/' + encodeURIComponent(currentFile) + '?format=a4', '_blank');
}
function openMindmapPrint() {
const currentFile = eingangFiles[currentIndex];
if (!currentFile) return;
window.open('/api/mindmap-html/' + encodeURIComponent(currentFile) + '?format=a3', '_blank');
}
if (btnMindmapGenerate) {
btnMindmapGenerate.addEventListener('click', generateMindmap);
}
if (btnMindmapShow) {
btnMindmapShow.addEventListener('click', openMindmapView);
}
if (btnMindmapPrint) {
btnMindmapPrint.addEventListener('click', openMindmapPrint);
}
// --- Frage-Antwort (Q&A) mit Leitner-System ---
const btnQaGenerate = document.getElementById('btn-qa-generate');
const btnQaLearn = document.getElementById('btn-qa-learn');
const btnQaPrint = document.getElementById('btn-qa-print');
const qaPreview = document.getElementById('qa-preview');
const qaBadge = document.getElementById('qa-badge');
const qaModal = document.getElementById('qa-modal');
const qaModalBody = document.getElementById('qa-modal-body');
const qaModalClose = document.getElementById('qa-modal-close');
let currentQaData = null;
let currentQaIndex = 0;
let qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
async function generateQaQuestions() {
try {
setStatus(t('status_generating_qa') || 'Generiere Q&A …', t('status_please_wait'), 'busy');
if (qaBadge) qaBadge.textContent = t('mc_generating');
const resp = await fetch('/api/generate-qa', { method: 'POST' });
if (!resp.ok) {
throw new Error('HTTP ' + resp.status);
}
const result = await resp.json();
if (result.status === 'OK' && result.generated.length > 0) {
setStatus(t('status_qa_generated') || 'Q&A generiert', result.generated.length + ' ' + t('status_files_created'));
if (qaBadge) qaBadge.textContent = t('mc_done');
if (btnQaLearn) btnQaLearn.style.display = 'inline-block';
if (btnQaPrint) btnQaPrint.style.display = 'inline-block';
await loadQaPreviewForCurrent();
} else if (result.errors && result.errors.length > 0) {
setStatus(t('error'), result.errors[0].error, 'error');
if (qaBadge) qaBadge.textContent = t('mc_error');
} else {
setStatus(t('error'), 'Keine Q&A generiert.', 'error');
if (qaBadge) qaBadge.textContent = t('mc_ready');
}
} catch (e) {
console.error('Q&A-Generierung fehlgeschlagen:', e);
setStatus(t('error'), String(e), 'error');
if (qaBadge) qaBadge.textContent = t('mc_error');
}
}
async function loadQaPreviewForCurrent() {
if (!eingangFiles.length) {
if (qaPreview) qaPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + t('qa_no_questions') + '</div>';
return;
}
const currentFile = eingangFiles[currentIndex];
if (!currentFile) return;
try {
const resp = await fetch('/api/qa-data/' + encodeURIComponent(currentFile));
const result = await resp.json();
if (result.status === 'OK' && result.data) {
currentQaData = result.data;
renderQaPreview(result.data);
if (btnQaLearn) btnQaLearn.style.display = 'inline-block';
if (btnQaPrint) btnQaPrint.style.display = 'inline-block';
} else {
if (qaPreview) qaPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('qa_no_questions') || 'Noch keine Q&A für dieses Arbeitsblatt generiert.') + '</div>';
currentQaData = null;
if (btnQaLearn) btnQaLearn.style.display = 'none';
if (btnQaPrint) btnQaPrint.style.display = 'none';
}
} catch (e) {
console.error('Fehler beim Laden der Q&A-Daten:', e);
if (qaPreview) qaPreview.innerHTML = '';
}
}
function renderQaPreview(qaData) {
if (!qaPreview) return;
if (!qaData || !qaData.qa_items || qaData.qa_items.length === 0) {
qaPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + t('qa_no_questions') + '</div>';
return;
}
const items = qaData.qa_items;
const metadata = qaData.metadata || {};
let html = '';
// Leitner-Box Statistiken
html += '<div class="mc-stats" style="margin-bottom:8px;">';
// Zähle Fragen nach Box
let box0 = 0, box1 = 0, box2 = 0;
items.forEach(item => {
const box = item.leitner ? item.leitner.box : 0;
if (box === 0) box0++;
else if (box === 1) box1++;
else box2++;
});
html += '<div style="display:flex;gap:12px;font-size:11px;">';
html += '<div style="color:#ef4444;">' + (t('qa_box_new') || 'Neu') + ': ' + box0 + '</div>';
html += '<div style="color:#f59e0b;">' + (t('qa_box_learning') || 'Lernt') + ': ' + box1 + '</div>';
html += '<div style="color:#22c55e;">' + (t('qa_box_mastered') || 'Gefestigt') + ': ' + box2 + '</div>';
html += '</div>';
html += '</div>';
// Zeige erste 2 Fragen als Vorschau
const previewItems = items.slice(0, 2);
previewItems.forEach((item, idx) => {
html += '<div class="mc-question" style="padding:8px;margin-bottom:6px;background:rgba(255,255,255,0.03);border-radius:6px;">';
html += '<div style="font-size:12px;font-weight:500;margin-bottom:4px;">' + (idx + 1) + '. ' + item.question + '</div>';
html += '<div style="font-size:11px;color:var(--bp-text-muted);">→ ' + item.answer.substring(0, 60) + (item.answer.length > 60 ? '...' : '') + '</div>';
html += '</div>';
});
if (items.length > 2) {
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:4px;">+ ' + (items.length - 2) + ' ' + (t('questions') || 'weitere Fragen') + '</div>';
}
qaPreview.innerHTML = html;
}
function openQaModal() {
if (!currentQaData || !currentQaData.qa_items) {
alert(t('qa_no_questions') || 'Keine Q&A vorhanden. Bitte zuerst generieren.');
return;
}
currentQaIndex = 0;
qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
renderQaLearningCard();
qaModal.classList.remove('hidden');
}
function closeQaModal() {
qaModal.classList.add('hidden');
}
function renderQaLearningCard() {
const items = currentQaData.qa_items;
if (currentQaIndex >= items.length) {
// Alle Fragen durch - Zeige Zusammenfassung
renderQaSessionSummary();
return;
}
const item = items[currentQaIndex];
const leitner = item.leitner || { box: 0 };
const boxNames = [t('qa_box_new') || 'Neu', t('qa_box_learning') || 'Gelernt', t('qa_box_mastered') || 'Gefestigt'];
const boxColors = ['#ef4444', '#f59e0b', '#22c55e'];
let html = '';
// Fortschrittsanzeige
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">';
html += '<div style="font-size:12px;color:var(--bp-text-muted);">' + (t('question') || 'Frage') + ' ' + (currentQaIndex + 1) + ' / ' + items.length + '</div>';
html += '<div style="display:flex;align-items:center;gap:6px;">';
html += '<span style="font-size:10px;color:' + boxColors[leitner.box] + ';background:' + boxColors[leitner.box] + '22;padding:2px 8px;border-radius:10px;">' + boxNames[leitner.box] + '</span>';
html += '</div>';
html += '</div>';
// Frage
html += '<div style="background:rgba(255,255,255,0.05);padding:20px;border-radius:12px;margin-bottom:16px;">';
html += '<div style="font-size:11px;color:var(--bp-text-muted);margin-bottom:8px;">' + (t('question') || 'Frage') + ':</div>';
html += '<div style="font-size:16px;font-weight:500;line-height:1.5;">' + item.question + '</div>';
html += '</div>';
// Eingabefeld für eigene Antwort
html += '<div id="qa-input-container" style="margin-bottom:16px;">';
html += '<div style="font-size:11px;color:var(--bp-text-muted);margin-bottom:8px;">' + (t('qa_your_answer') || 'Deine Antwort') + ':</div>';
html += '<textarea id="qa-user-answer" style="width:100%;min-height:80px;padding:12px;border:1px solid rgba(255,255,255,0.2);border-radius:8px;background:rgba(255,255,255,0.05);color:var(--bp-text);font-size:14px;resize:vertical;font-family:inherit;" placeholder="' + (t('qa_type_answer') || 'Schreibe deine Antwort hier...') + '"></textarea>';
html += '</div>';
// Prüfen-Button
html += '<div id="qa-check-btn-container" style="text-align:center;margin-bottom:16px;">';
html += '<button class="btn btn-primary" id="btn-qa-check-answer" style="padding:12px 32px;">' + (t('qa_check_answer') || 'Antwort prüfen') + '</button>';
html += '</div>';
// Vergleichs-Container (versteckt)
html += '<div id="qa-comparison-container" style="display:none;">';
// Eigene Antwort (wird befüllt)
html += '<div id="qa-user-answer-display" style="background:rgba(59,130,246,0.1);padding:16px;border-radius:12px;margin-bottom:12px;border-left:3px solid #3b82f6;">';
html += '<div style="font-size:11px;color:#3b82f6;margin-bottom:8px;">' + (t('qa_your_answer') || 'Deine Antwort') + ':</div>';
html += '<div id="qa-user-answer-text" style="font-size:14px;line-height:1.5;"></div>';
html += '</div>';
// Richtige Antwort
html += '<div style="background:rgba(34,197,94,0.1);padding:16px;border-radius:12px;margin-bottom:16px;border-left:3px solid #22c55e;">';
html += '<div style="font-size:11px;color:#22c55e;margin-bottom:8px;">' + (t('qa_correct_answer') || 'Richtige Antwort') + ':</div>';
html += '<div style="font-size:14px;line-height:1.5;">' + item.answer + '</div>';
if (item.key_terms && item.key_terms.length > 0) {
html += '<div style="margin-top:12px;font-size:11px;color:var(--bp-text-muted);">' + (t('qa_key_terms') || 'Schlüsselbegriffe') + ': <span style="color:#22c55e;">' + item.key_terms.join(', ') + '</span></div>';
}
html += '</div>';
// Selbstbewertung
html += '<div style="text-align:center;margin-bottom:8px;">';
html += '<div style="font-size:12px;color:var(--bp-text-muted);margin-bottom:12px;">' + (t('qa_self_evaluate') || 'War deine Antwort richtig?') + '</div>';
html += '<div style="display:flex;gap:12px;justify-content:center;">';
html += '<button class="btn" id="btn-qa-incorrect" style="background:#ef4444;padding:12px 24px;">' + (t('qa_incorrect') || 'Falsch') + '</button>';
html += '<button class="btn" id="btn-qa-correct" style="background:#22c55e;padding:12px 24px;">' + (t('qa_correct') || 'Richtig') + '</button>';
html += '</div>';
html += '</div>';
html += '</div>'; // Ende qa-comparison-container
// Session-Statistik
html += '<div style="margin-top:16px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.1);display:flex;justify-content:center;gap:20px;font-size:11px;">';
html += '<div style="color:#22c55e;">' + (t('qa_session_correct') || 'Richtig') + ': ' + qaSessionStats.correct + '</div>';
html += '<div style="color:#ef4444;">' + (t('qa_session_incorrect') || 'Falsch') + ': ' + qaSessionStats.incorrect + '</div>';
html += '</div>';
qaModalBody.innerHTML = html;
// Event Listener für Prüfen-Button
document.getElementById('btn-qa-check-answer').addEventListener('click', () => {
const userAnswer = document.getElementById('qa-user-answer').value.trim();
// Zeige die eigene Antwort im Vergleich
document.getElementById('qa-user-answer-text').textContent = userAnswer || (t('qa_no_answer') || '(keine Antwort eingegeben)');
// Verstecke Eingabe, zeige Vergleich
document.getElementById('qa-input-container').style.display = 'none';
document.getElementById('qa-check-btn-container').style.display = 'none';
document.getElementById('qa-comparison-container').style.display = 'block';
});
// Enter-Taste im Textfeld löst Prüfen aus
document.getElementById('qa-user-answer').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
document.getElementById('btn-qa-check-answer').click();
}
});
// Fokus auf Textfeld setzen
setTimeout(() => {
document.getElementById('qa-user-answer').focus();
}, 100);
document.getElementById('btn-qa-correct').addEventListener('click', () => handleQaAnswer(true));
document.getElementById('btn-qa-incorrect').addEventListener('click', () => handleQaAnswer(false));
}
async function handleQaAnswer(correct) {
const item = currentQaData.qa_items[currentQaIndex];
// Update Session-Statistik
qaSessionStats.total++;
if (correct) qaSessionStats.correct++;
else qaSessionStats.incorrect++;
// Update Leitner-Fortschritt auf dem Server
try {
const currentFile = eingangFiles[currentIndex];
await fetch('/api/qa-progress?filename=' + encodeURIComponent(currentFile) + '&item_id=' + encodeURIComponent(item.id) + '&correct=' + correct, {
method: 'POST'
});
} catch (e) {
console.error('Fehler beim Speichern des Fortschritts:', e);
}
// Nächste Frage
currentQaIndex++;
renderQaLearningCard();
}
function renderQaSessionSummary() {
const percent = qaSessionStats.total > 0 ? Math.round(qaSessionStats.correct / qaSessionStats.total * 100) : 0;
let html = '';
html += '<div style="text-align:center;padding:20px;">';
html += '<div style="font-size:48px;margin-bottom:16px;">' + (percent >= 80 ? '🎉' : percent >= 50 ? '👍' : '💪') + '</div>';
html += '<div style="font-size:24px;font-weight:600;margin-bottom:8px;">' + (t('qa_session_complete') || 'Lernrunde abgeschlossen!') + '</div>';
html += '<div style="font-size:18px;margin-bottom:24px;">' + qaSessionStats.correct + ' / ' + qaSessionStats.total + ' ' + (t('qa_result_correct') || 'richtig') + ' (' + percent + '%)</div>';
// Statistik
html += '<div style="display:flex;justify-content:center;gap:24px;margin-bottom:24px;">';
html += '<div style="text-align:center;"><div style="font-size:32px;color:#22c55e;">' + qaSessionStats.correct + '</div><div style="font-size:11px;color:var(--bp-text-muted);">' + (t('qa_correct') || 'Richtig') + '</div></div>';
html += '<div style="text-align:center;"><div style="font-size:32px;color:#ef4444;">' + qaSessionStats.incorrect + '</div><div style="font-size:11px;color:var(--bp-text-muted);">' + (t('qa_incorrect') || 'Falsch') + '</div></div>';
html += '</div>';
html += '<div style="display:flex;gap:12px;justify-content:center;">';
html += '<button class="btn btn-primary" id="btn-qa-restart">' + (t('qa_restart') || 'Nochmal lernen') + '</button>';
html += '<button class="btn btn-ghost" id="btn-qa-close-summary">' + (t('close') || 'Schließen') + '</button>';
html += '</div>';
html += '</div>';
qaModalBody.innerHTML = html;
document.getElementById('btn-qa-restart').addEventListener('click', () => {
currentQaIndex = 0;
qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
renderQaLearningCard();
});
document.getElementById('btn-qa-close-summary').addEventListener('click', closeQaModal);
// Aktualisiere Preview nach Session
loadQaPreviewForCurrent();
}
function openQaPrintDialog() {
if (!currentQaData) {
alert(t('qa_no_questions') || 'Keine Q&A vorhanden.');
return;
}
const currentFile = eingangFiles[currentIndex];
const baseName = currentFile.split('.')[0];
// Öffne Druck-Optionen
const choice = confirm((t('qa_print_with_answers') || 'Mit Lösungen drucken?') + '\\n\\nOK = Mit Lösungen\\nAbbrechen = Nur Fragen');
const url = '/api/print-qa/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
window.open(url, '_blank');
}
// Event Listener für Q&A-Buttons
if (btnQaGenerate) {
btnQaGenerate.addEventListener('click', generateQaQuestions);
}
if (btnQaLearn) {
btnQaLearn.addEventListener('click', openQaModal);
}
if (btnQaPrint) {
btnQaPrint.addEventListener('click', openQaPrintDialog);
}
if (qaModalClose) {
qaModalClose.addEventListener('click', closeQaModal);
}
if (qaModal) {
qaModal.addEventListener('click', (ev) => {
if (ev.target === qaModal) {
closeQaModal();
}
});
}
// --- Sprachauswahl Event Listener ---
const languageSelect = document.getElementById('language-select');
if (languageSelect) {
// Setze initiale Auswahl basierend auf gespeicherter Sprache
languageSelect.value = currentLang;
languageSelect.addEventListener('change', (e) => {
applyLanguage(e.target.value);
});
}
// --- Initial ---
async function init() {
// Theme Toggle initialisieren
initThemeToggle();
// Sprache anwenden (aus localStorage oder default)
applyLanguage(currentLang);
updateScreen();
updateTiles();
await loadEingangFiles();
await loadWorksheetPairs();
await loadLearningUnits();
// Lade MC-Vorschau für aktuelle Datei
await loadMcPreviewForCurrent();
// Lade Lückentext-Vorschau
await loadClozePreviewForCurrent();
// Lade Q&A-Vorschau
await loadQaPreviewForCurrent();
// Lade Mindmap-Vorschau
await loadMindmapData();
}
init();
// === SCRIPT BLOCK SEPARATOR ===
// GDPR Actions
async function saveCookiePreferences() {
const functional = document.getElementById('cookie-functional')?.checked || false;
const analytics = document.getElementById('cookie-analytics')?.checked || false;
// Save to localStorage for now
localStorage.setItem('bp_cookies', JSON.stringify({functional, analytics}));
alert('Cookie-Einstellungen gespeichert!');
}
// ==========================================
// GDPR FUNCTIONS (Art. 15-21)
// ==========================================
// Art. 15 - Auskunftsrecht
async function requestDataExport() {
alert('Ihre Auskunftsanfrage wurde erstellt. Sie erhalten eine E-Mail, sobald Ihre Daten bereit sind (max. 30 Tage).');
}
// Art. 16 - Recht auf Berichtigung
async function requestDataCorrection() {
const reason = prompt('Bitte beschreiben Sie, welche Daten korrigiert werden sollen:');
if (reason) {
alert('Ihre Berichtigungsanfrage wurde erstellt. Wir werden sie innerhalb von 30 Tagen bearbeiten.');
}
}
// Art. 17 - Recht auf Löschung
async function requestDataDeletion() {
if (confirm('Sind Sie sicher, dass Sie alle Ihre Daten löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.')) {
alert('Ihre Löschanfrage wurde erstellt. Wir werden sie innerhalb von 30 Tagen bearbeiten.');
}
}
// Art. 18 - Einschränkung der Verarbeitung
async function requestProcessingRestriction() {
const reason = prompt('Bitte geben Sie den Grund für die Einschränkung an (z.B. Richtigkeit bestritten, unrechtmäßige Verarbeitung):');
if (reason) {
alert('Ihre Anfrage auf Einschränkung der Verarbeitung wurde erstellt. Wir werden sie innerhalb von 30 Tagen bearbeiten.');
}
}
// Art. 20 - Datenübertragbarkeit
async function requestDataDownload() {
alert('Ihr Datenexport wurde gestartet. Sie erhalten eine E-Mail mit dem Download-Link (JSON-Format).');
}
// Art. 21 - Widerspruchsrecht
async function requestProcessingObjection() {
const reason = prompt('Bitte geben Sie den Grund für Ihren Widerspruch an:');
if (reason) {
alert('Ihr Widerspruch wurde erstellt. Wir werden ihn innerhalb von 30 Tagen prüfen.');
}
}
// Consent Manager öffnen
function showConsentManager() {
openLegalModal('cookies');
}
// Settings Modal öffnen (leitet zum GDPR-Tab)
function openSettingsModal() {
openLegalModal('gdpr');
}
// Load saved cookie preferences
const savedCookies = localStorage.getItem('bp_cookies');
if (savedCookies) {
const prefs = JSON.parse(savedCookies);
if (document.getElementById('cookie-functional')) {
document.getElementById('cookie-functional').checked = prefs.functional;
}
if (document.getElementById('cookie-analytics')) {
document.getElementById('cookie-analytics').checked = prefs.analytics;
}
}
// ==========================================
// LEGAL MODAL (now after modal HTML exists)
// ==========================================
const legalModal = document.getElementById('legal-modal');
const legalModalClose = document.getElementById('legal-modal-close');
const legalTabs = document.querySelectorAll('.legal-tab');
const legalContents = document.querySelectorAll('.legal-content');
const btnLegal = document.getElementById('btn-legal');
// Imprint Modal
const imprintModal = document.getElementById('imprint-modal');
const imprintModalClose = document.getElementById('imprint-modal-close');
// Open legal modal from footer
function openLegalModal(tab = 'terms') {
legalModal.classList.add('active');
// Switch to specified tab
if (tab) {
legalTabs.forEach(t => t.classList.remove('active'));
legalContents.forEach(c => c.classList.remove('active'));
const targetTab = document.querySelector(`.legal-tab[data-tab="${tab}"]`);
if (targetTab) targetTab.classList.add('active');
document.getElementById(`legal-${tab}`)?.classList.add('active');
}
loadLegalDocuments();
}
// Open imprint modal from footer
function openImprintModal() {
imprintModal.classList.add('active');
loadImprintContent();
}
// Open legal modal
btnLegal?.addEventListener('click', async () => {
openLegalModal();
});
// Close legal modal
legalModalClose?.addEventListener('click', () => {
legalModal.classList.remove('active');
});
// Close imprint modal
imprintModalClose?.addEventListener('click', () => {
imprintModal.classList.remove('active');
});
// Close on background click
legalModal?.addEventListener('click', (e) => {
if (e.target === legalModal) {
legalModal.classList.remove('active');
}
});
imprintModal?.addEventListener('click', (e) => {
if (e.target === imprintModal) {
imprintModal.classList.remove('active');
}
});
// Tab switching
legalTabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.dataset.tab;
legalTabs.forEach(t => t.classList.remove('active'));
legalContents.forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById(`legal-${tabId}`)?.classList.add('active');
// Load cookie categories when switching to cookies tab
if (tabId === 'cookies') {
loadCookieCategories();
}
});
});
// Load legal documents from consent service
async function loadLegalDocuments() {
const lang = document.getElementById('language-select')?.value || 'de';
// Load all documents in parallel
await Promise.all([
loadDocumentContent('terms', 'legal-terms-content', getDefaultTerms, lang),
loadDocumentContent('privacy', 'legal-privacy-content', getDefaultPrivacy, lang),
loadDocumentContent('community_guidelines', 'legal-community-content', getDefaultCommunityGuidelines, lang)
]);
}
// Load imprint content
async function loadImprintContent() {
const lang = document.getElementById('language-select')?.value || 'de';
await loadDocumentContent('imprint', 'imprint-content', getDefaultImprint, lang);
}
// Generic function to load document content
async function loadDocumentContent(docType, containerId, defaultFn, lang) {
const container = document.getElementById(containerId);
if (!container) return;
try {
const res = await fetch(`/api/consent/documents/${docType}/latest?language=${lang}`);
if (res.ok) {
const data = await res.json();
if (data.content) {
container.innerHTML = data.content;
return;
}
}
} catch(e) {
console.log(`Could not load ${docType}:`, e);
}
// Fallback to default
container.innerHTML = defaultFn(lang);
}
// Load cookie categories for the cookie settings tab
async function loadCookieCategories() {
const container = document.getElementById('cookie-categories-container');
if (!container) return;
try {
const res = await fetch('/api/consent/cookies/categories');
if (res.ok) {
const data = await res.json();
const categories = data.categories || [];
if (categories.length === 0) {
container.innerHTML = getDefaultCookieCategories();
return;
}
// Get current preferences from localStorage
const savedPrefs = JSON.parse(localStorage.getItem('bp_cookie_consent') || '{}');
container.innerHTML = categories.map(cat => `
<label class="cookie-category">
<input type="checkbox" id="cookie-${cat.name}"
${cat.is_mandatory ? 'checked disabled' : (savedPrefs[cat.name] ? 'checked' : '')}>
<span>
<strong>${cat.display_name_de || cat.name}</strong>
${cat.is_mandatory ? ' (erforderlich)' : ''}
${cat.description_de ? ` - ${cat.description_de}` : ''}
</span>
</label>
`).join('');
} else {
container.innerHTML = getDefaultCookieCategories();
}
} catch(e) {
container.innerHTML = getDefaultCookieCategories();
}
}
function getDefaultCookieCategories() {
return `
<label class="cookie-category">
<input type="checkbox" checked disabled>
<span><strong>Notwendig</strong> (erforderlich) - Erforderlich für die Grundfunktionen</span>
</label>
<label class="cookie-category">
<input type="checkbox" id="cookie-functional">
<span><strong>Funktional</strong> - Erweiterte Funktionen und Personalisierung</span>
</label>
<label class="cookie-category">
<input type="checkbox" id="cookie-analytics">
<span><strong>Analyse</strong> - Hilft uns, die Nutzung zu verstehen</span>
</label>
`;
}
function getDefaultTerms(lang) {
const terms = {
de: '<h3>Allgemeine Geschäftsbedingungen</h3><p>Die BreakPilot-Plattform wird von der BreakPilot UG bereitgestellt.</p><p><strong>Nutzung:</strong> Die Plattform dient zur Erstellung und Verwaltung von Lernmaterialien für Bildungszwecke.</p><p><strong>Haftung:</strong> Die Nutzung erfolgt auf eigene Verantwortung.</p><p><strong>Änderungen:</strong> Wir behalten uns vor, diese AGB jederzeit zu ändern.</p>',
en: '<h3>Terms of Service</h3><p>The BreakPilot platform is provided by BreakPilot UG.</p><p><strong>Usage:</strong> The platform is designed for creating and managing learning materials for educational purposes.</p><p><strong>Liability:</strong> Use at your own risk.</p><p><strong>Changes:</strong> We reserve the right to modify these terms at any time.</p>'
};
return terms[lang] || terms.de;
}
function getDefaultPrivacy(lang) {
const privacy = {
de: '<h3>Datenschutzerklärung</h3><p><strong>Verantwortlicher:</strong> BreakPilot UG</p><p><strong>Erhobene Daten:</strong> Bei der Nutzung werden technische Daten (IP-Adresse, Browser-Typ) sowie von Ihnen eingegebene Inhalte verarbeitet.</p><p><strong>Zweck:</strong> Die Daten werden zur Bereitstellung der Plattform und zur Verbesserung unserer Dienste genutzt.</p><p><strong>Ihre Rechte (DSGVO):</strong></p><ul><li>Auskunftsrecht (Art. 15)</li><li>Recht auf Berichtigung (Art. 16)</li><li>Recht auf Löschung (Art. 17)</li><li>Recht auf Datenübertragbarkeit (Art. 20)</li></ul><p><strong>Kontakt:</strong> datenschutz@breakpilot.app</p>',
en: '<h3>Privacy Policy</h3><p><strong>Controller:</strong> BreakPilot UG</p><p><strong>Data Collected:</strong> Technical data (IP address, browser type) and content you provide are processed.</p><p><strong>Purpose:</strong> Data is used to provide the platform and improve our services.</p><p><strong>Your Rights (GDPR):</strong></p><ul><li>Right of access (Art. 15)</li><li>Right to rectification (Art. 16)</li><li>Right to erasure (Art. 17)</li><li>Right to data portability (Art. 20)</li></ul><p><strong>Contact:</strong> privacy@breakpilot.app</p>'
};
return privacy[lang] || privacy.de;
}
function getDefaultCommunityGuidelines(lang) {
const guidelines = {
de: '<h3>Community Guidelines</h3><p>Willkommen bei BreakPilot! Um eine positive und respektvolle Umgebung zu gewährleisten, bitten wir alle Nutzer, diese Richtlinien zu befolgen.</p><p><strong>Respektvoller Umgang:</strong> Behandeln Sie andere Nutzer mit Respekt und Höflichkeit.</p><p><strong>Keine illegalen Inhalte:</strong> Das Erstellen oder Teilen von illegalen Inhalten ist streng untersagt.</p><p><strong>Urheberrecht:</strong> Respektieren Sie das geistige Eigentum anderer. Verwenden Sie nur Inhalte, für die Sie die Rechte besitzen.</p><p><strong>Datenschutz:</strong> Teilen Sie keine persönlichen Daten anderer ohne deren ausdrückliche Zustimmung.</p><p><strong>Qualität:</strong> Bemühen Sie sich um qualitativ hochwertige Lerninhalte.</p><p>Verstöße gegen diese Richtlinien können zur Sperrung des Accounts führen.</p>',
en: '<h3>Community Guidelines</h3><p>Welcome to BreakPilot! To ensure a positive and respectful environment, we ask all users to follow these guidelines.</p><p><strong>Respectful Behavior:</strong> Treat other users with respect and courtesy.</p><p><strong>No Illegal Content:</strong> Creating or sharing illegal content is strictly prohibited.</p><p><strong>Copyright:</strong> Respect the intellectual property of others. Only use content you have rights to.</p><p><strong>Privacy:</strong> Do not share personal data of others without their explicit consent.</p><p><strong>Quality:</strong> Strive for high-quality learning content.</p><p>Violations of these guidelines may result in account suspension.</p>'
};
return guidelines[lang] || guidelines.de;
}
function getDefaultImprint(lang) {
const imprint = {
de: '<h3>Impressum</h3><p><strong>Angaben gemäß § 5 TMG:</strong></p><p>BreakPilot UG (haftungsbeschränkt)<br>Musterstraße 1<br>12345 Musterstadt<br>Deutschland</p><p><strong>Vertreten durch:</strong><br>Geschäftsführer: Max Mustermann</p><p><strong>Kontakt:</strong><br>Telefon: +49 (0) 123 456789<br>E-Mail: info@breakpilot.app</p><p><strong>Registereintrag:</strong><br>Eintragung im Handelsregister<br>Registergericht: Amtsgericht Musterstadt<br>Registernummer: HRB 12345</p><p><strong>Umsatzsteuer-ID:</strong><br>Umsatzsteuer-Identifikationsnummer gemäß § 27 a UStG: DE123456789</p><p><strong>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV:</strong><br>Max Mustermann<br>Musterstraße 1<br>12345 Musterstadt</p>',
en: '<h3>Legal Notice</h3><p><strong>Information according to § 5 TMG:</strong></p><p>BreakPilot UG (limited liability)<br>Musterstraße 1<br>12345 Musterstadt<br>Germany</p><p><strong>Represented by:</strong><br>Managing Director: Max Mustermann</p><p><strong>Contact:</strong><br>Phone: +49 (0) 123 456789<br>Email: info@breakpilot.app</p><p><strong>Register entry:</strong><br>Entry in the commercial register<br>Register court: Amtsgericht Musterstadt<br>Register number: HRB 12345</p><p><strong>VAT ID:</strong><br>VAT identification number according to § 27 a UStG: DE123456789</p><p><strong>Responsible for content according to § 55 Abs. 2 RStV:</strong><br>Max Mustermann<br>Musterstraße 1<br>12345 Musterstadt</p>'
};
return imprint[lang] || imprint.de;
}
// Save cookie preferences
function saveCookiePreferences() {
const prefs = {};
const checkboxes = document.querySelectorAll('#cookie-categories-container input[type="checkbox"]');
checkboxes.forEach(cb => {
const name = cb.id.replace('cookie-', '');
if (name && !cb.disabled) {
prefs[name] = cb.checked;
}
});
localStorage.setItem('bp_cookie_consent', JSON.stringify(prefs));
localStorage.setItem('bp_cookie_consent_date', new Date().toISOString());
// TODO: Send to consent service if user is logged in
alert('Cookie-Einstellungen gespeichert!');
}
// ==========================================
// AUTH MODAL
// ==========================================
const authModal = document.getElementById('auth-modal');
const authModalClose = document.getElementById('auth-modal-close');
const authTabs = document.querySelectorAll('.auth-tab');
const authContents = document.querySelectorAll('.auth-content');
const btnLogin = document.getElementById('btn-login');
// Auth state
let currentUser = null;
let accessToken = localStorage.getItem('bp_access_token');
let refreshToken = localStorage.getItem('bp_refresh_token');
// Update UI based on auth state
function updateAuthUI() {
const loginBtn = document.getElementById('btn-login');
const userDropdown = document.querySelector('.auth-user-dropdown');
const notificationBell = document.getElementById('notification-bell');
if (currentUser && accessToken) {
// User is logged in - hide login button
if (loginBtn) loginBtn.style.display = 'none';
// Show notification bell
if (notificationBell) {
notificationBell.classList.add('active');
loadNotifications(); // Load notifications on login
startNotificationPolling(); // Start polling for new notifications
checkSuspensionStatus(); // Check if account is suspended
}
// Show user dropdown if it exists
if (userDropdown) {
userDropdown.classList.add('active');
const avatar = userDropdown.querySelector('.auth-user-avatar');
const menuName = userDropdown.querySelector('.auth-user-menu-name');
const menuEmail = userDropdown.querySelector('.auth-user-menu-email');
if (avatar) {
const initials = currentUser.name
? currentUser.name.substring(0, 2).toUpperCase()
: currentUser.email.substring(0, 2).toUpperCase();
avatar.textContent = initials;
}
if (menuName) menuName.textContent = currentUser.name || 'Benutzer';
if (menuEmail) menuEmail.textContent = currentUser.email;
}
} else {
// User is logged out - show login button
if (loginBtn) loginBtn.style.display = 'block';
if (userDropdown) userDropdown.classList.remove('active');
if (notificationBell) notificationBell.classList.remove('active');
stopNotificationPolling();
}
}
// Check if user is already logged in
async function checkAuthStatus() {
if (!accessToken) return;
try {
const response = await fetch('/api/auth/profile', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (response.ok) {
currentUser = await response.json();
updateAuthUI();
} else if (response.status === 401 && refreshToken) {
// Try to refresh the token
await refreshAccessToken();
} else {
// Clear invalid tokens
logout(false);
}
} catch (e) {
console.error('Auth check failed:', e);
}
}
// Refresh access token
async function refreshAccessToken() {
if (!refreshToken) return false;
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken })
});
if (response.ok) {
const data = await response.json();
accessToken = data.access_token;
refreshToken = data.refresh_token;
currentUser = data.user;
localStorage.setItem('bp_access_token', accessToken);
localStorage.setItem('bp_refresh_token', refreshToken);
updateAuthUI();
return true;
} else {
logout(false);
return false;
}
} catch (e) {
console.error('Token refresh failed:', e);
return false;
}
}
// Logout
function logout(showMessage = true) {
if (refreshToken) {
fetch('/api/auth/logout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken })
}).catch(() => {});
}
currentUser = null;
accessToken = null;
refreshToken = null;
localStorage.removeItem('bp_access_token');
localStorage.removeItem('bp_refresh_token');
updateAuthUI();
if (showMessage) {
alert('Sie wurden erfolgreich abgemeldet.');
}
}
// Open auth modal
btnLogin?.addEventListener('click', () => {
authModal.classList.add('active');
showAuthTab('login');
clearAuthErrors();
});
// Close auth modal
authModalClose?.addEventListener('click', () => {
authModal.classList.remove('active');
clearAuthErrors();
});
// Close on background click
authModal?.addEventListener('click', (e) => {
if (e.target === authModal) {
authModal.classList.remove('active');
clearAuthErrors();
}
});
// Tab switching
authTabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.dataset.tab;
showAuthTab(tabId);
});
});
function showAuthTab(tabId) {
authTabs.forEach(t => t.classList.remove('active'));
authContents.forEach(c => c.classList.remove('active'));
const activeTab = document.querySelector(`.auth-tab[data-tab="${tabId}"]`);
if (activeTab) activeTab.classList.add('active');
document.getElementById(`auth-${tabId}`)?.classList.add('active');
clearAuthErrors();
}
function clearAuthErrors() {
document.querySelectorAll('.auth-error, .auth-success').forEach(el => {
el.classList.remove('active');
el.textContent = '';
});
}
function showAuthError(elementId, message) {
const el = document.getElementById(elementId);
if (el) {
el.textContent = message;
el.classList.add('active');
}
}
function showAuthSuccess(elementId, message) {
const el = document.getElementById(elementId);
if (el) {
el.textContent = message;
el.classList.add('active');
}
}
// Login form
document.getElementById('auth-login-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
clearAuthErrors();
const email = document.getElementById('login-email').value;
const password = document.getElementById('login-password').value;
const btn = document.getElementById('login-btn');
btn.disabled = true;
btn.textContent = 'Anmelden...';
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (response.ok) {
accessToken = data.access_token;
refreshToken = data.refresh_token;
currentUser = data.user;
localStorage.setItem('bp_access_token', accessToken);
localStorage.setItem('bp_refresh_token', refreshToken);
updateAuthUI();
authModal.classList.remove('active');
// Clear form
document.getElementById('login-email').value = '';
document.getElementById('login-password').value = '';
} else {
showAuthError('auth-login-error', data.detail || data.error || 'Anmeldung fehlgeschlagen');
}
} catch (e) {
showAuthError('auth-login-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
}
btn.disabled = false;
btn.textContent = 'Anmelden';
});
// Register form
document.getElementById('auth-register-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
clearAuthErrors();
const name = document.getElementById('register-name').value;
const email = document.getElementById('register-email').value;
const password = document.getElementById('register-password').value;
const passwordConfirm = document.getElementById('register-password-confirm').value;
if (password !== passwordConfirm) {
showAuthError('auth-register-error', 'Passwörter stimmen nicht überein');
return;
}
if (password.length < 8) {
showAuthError('auth-register-error', 'Passwort muss mindestens 8 Zeichen lang sein');
return;
}
const btn = document.getElementById('register-btn');
btn.disabled = true;
btn.textContent = 'Registrieren...';
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, name: name || undefined })
});
const data = await response.json();
if (response.ok) {
showAuthSuccess('auth-register-success',
'Registrierung erfolgreich! Bitte überprüfen Sie Ihre E-Mails zur Bestätigung.');
// Clear form
document.getElementById('register-name').value = '';
document.getElementById('register-email').value = '';
document.getElementById('register-password').value = '';
document.getElementById('register-password-confirm').value = '';
} else {
showAuthError('auth-register-error', data.detail || data.error || 'Registrierung fehlgeschlagen');
}
} catch (e) {
showAuthError('auth-register-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
}
btn.disabled = false;
btn.textContent = 'Registrieren';
});
// Forgot password form
document.getElementById('auth-forgot-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
clearAuthErrors();
const email = document.getElementById('forgot-email').value;
const btn = document.getElementById('forgot-btn');
btn.disabled = true;
btn.textContent = 'Senden...';
try {
const response = await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
// Always show success to prevent email enumeration
showAuthSuccess('auth-forgot-success',
'Falls ein Konto mit dieser E-Mail existiert, wurde ein Link zum Zurücksetzen gesendet.');
document.getElementById('forgot-email').value = '';
} catch (e) {
showAuthError('auth-forgot-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
}
btn.disabled = false;
btn.textContent = 'Link senden';
});
// Reset password form
document.getElementById('auth-reset-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
clearAuthErrors();
const password = document.getElementById('reset-password').value;
const passwordConfirm = document.getElementById('reset-password-confirm').value;
const token = document.getElementById('reset-token').value;
if (password !== passwordConfirm) {
showAuthError('auth-reset-error', 'Passwörter stimmen nicht überein');
return;
}
if (password.length < 8) {
showAuthError('auth-reset-error', 'Passwort muss mindestens 8 Zeichen lang sein');
return;
}
const btn = document.getElementById('reset-btn');
btn.disabled = true;
btn.textContent = 'Ändern...';
try {
const response = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, new_password: password })
});
const data = await response.json();
if (response.ok) {
showAuthSuccess('auth-reset-success',
'Passwort erfolgreich geändert! Sie können sich jetzt anmelden.');
// Clear URL params
window.history.replaceState({}, document.title, window.location.pathname);
// Switch to login after 2 seconds
setTimeout(() => showAuthTab('login'), 2000);
} else {
showAuthError('auth-reset-error', data.detail || data.error || 'Passwort zurücksetzen fehlgeschlagen');
}
} catch (e) {
showAuthError('auth-reset-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
}
btn.disabled = false;
btn.textContent = 'Passwort ändern';
});
// Navigation links
document.getElementById('auth-forgot-password')?.addEventListener('click', (e) => {
e.preventDefault();
showAuthTab('forgot');
// Hide tabs for forgot password
document.querySelector('.auth-tabs').style.display = 'none';
});
document.getElementById('auth-back-to-login')?.addEventListener('click', (e) => {
e.preventDefault();
showAuthTab('login');
document.querySelector('.auth-tabs').style.display = 'flex';
});
document.getElementById('auth-goto-login')?.addEventListener('click', (e) => {
e.preventDefault();
showAuthTab('login');
});
// Check for URL parameters (email verification, password reset)
function checkAuthUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
const verifyToken = urlParams.get('verify');
const resetToken = urlParams.get('reset');
if (verifyToken) {
authModal.classList.add('active');
document.querySelector('.auth-tabs').style.display = 'none';
showAuthTab('verify');
verifyEmail(verifyToken);
} else if (resetToken) {
authModal.classList.add('active');
document.querySelector('.auth-tabs').style.display = 'none';
showAuthTab('reset');
document.getElementById('reset-token').value = resetToken;
}
}
async function verifyEmail(token) {
try {
const response = await fetch('/api/auth/verify-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
const data = await response.json();
const loadingEl = document.getElementById('auth-verify-loading');
if (response.ok) {
if (loadingEl) loadingEl.style.display = 'none';
showAuthSuccess('auth-verify-success', 'E-Mail erfolgreich verifiziert! Sie können sich jetzt anmelden.');
// Clear URL params
window.history.replaceState({}, document.title, window.location.pathname);
// Switch to login after 2 seconds
setTimeout(() => {
showAuthTab('login');
document.querySelector('.auth-tabs').style.display = 'flex';
}, 2000);
} else {
if (loadingEl) loadingEl.style.display = 'none';
showAuthError('auth-verify-error', data.detail || data.error || 'Verifizierung fehlgeschlagen. Der Link ist möglicherweise abgelaufen.');
}
} catch (e) {
document.getElementById('auth-verify-loading').style.display = 'none';
showAuthError('auth-verify-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
}
}
// User dropdown toggle
const authUserBtn = document.getElementById('auth-user-btn');
const authUserMenu = document.getElementById('auth-user-menu');
authUserBtn?.addEventListener('click', (e) => {
e.stopPropagation();
authUserMenu.classList.toggle('active');
});
// Close dropdown when clicking outside
document.addEventListener('click', () => {
authUserMenu?.classList.remove('active');
});
// Placeholder functions for profile/sessions
function showProfileModal() {
alert('Profil-Einstellungen kommen bald!');
}
function showSessionsModal() {
alert('Sitzungsverwaltung kommt bald!');
}
// ==========================================
// NOTIFICATION FUNCTIONS
// ==========================================
let notificationPollingInterval = null;
let notificationOffset = 0;
let notificationPrefs = {
email_enabled: true,
push_enabled: false,
in_app_enabled: true
};
// Toggle notification panel
document.getElementById('notification-bell-btn')?.addEventListener('click', (e) => {
e.stopPropagation();
const panel = document.getElementById('notification-panel');
panel.classList.toggle('active');
// Close user menu if open
const userMenu = document.getElementById('auth-user-menu');
userMenu?.classList.remove('active');
});
// Close notification panel when clicking outside
document.addEventListener('click', (e) => {
const bell = document.getElementById('notification-bell');
const panel = document.getElementById('notification-panel');
if (bell && panel && !bell.contains(e.target)) {
panel.classList.remove('active');
}
});
// Load notifications from API
async function loadNotifications(append = false) {
if (!accessToken) return;
try {
const limit = 10;
const offset = append ? notificationOffset : 0;
const response = await fetch(`/api/v1/notifications?limit=${limit}&offset=${offset}`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (!response.ok) return;
const data = await response.json();
if (!append) {
notificationOffset = 0;
}
notificationOffset += data.notifications?.length || 0;
renderNotifications(data.notifications || [], data.total || 0, append);
updateNotificationBadge();
} catch (e) {
console.error('Failed to load notifications:', e);
}
}
// Render notifications in the panel
function renderNotifications(notifications, total, append = false) {
const list = document.getElementById('notification-list');
if (!list) return;
if (!append) {
list.innerHTML = '';
}
if (notifications.length === 0 && !append) {
list.innerHTML = `
<div class="notification-empty">
<div class="notification-empty-icon">🔔</div>
<div>Keine Benachrichtigungen</div>
</div>
`;
return;
}
notifications.forEach(n => {
const item = document.createElement('div');
item.className = `notification-item ${!n.read_at ? 'unread' : ''}`;
item.onclick = () => markNotificationRead(n.id);
const icon = getNotificationIcon(n.type);
const timeAgo = formatTimeAgo(new Date(n.created_at));
item.innerHTML = `
<div class="notification-icon">${icon}</div>
<div class="notification-content">
<div class="notification-title">${escapeHtml(n.title)}</div>
<div class="notification-body">${escapeHtml(n.body)}</div>
<div class="notification-time">${timeAgo}</div>
</div>
`;
list.appendChild(item);
});
// Show/hide load more button
const footer = document.querySelector('.notification-footer');
if (footer) {
footer.style.display = notificationOffset < total ? 'block' : 'none';
}
}
// Get icon for notification type
function getNotificationIcon(type) {
const icons = {
'consent_required': '📋',
'consent_reminder': '⏰',
'version_published': '📢',
'version_approved': '✅',
'version_rejected': '❌',
'account_suspended': '🚫',
'account_restored': '🔓',
'general': '🔔'
};
return icons[type] || '🔔';
}
// Format time ago
function formatTimeAgo(date) {
const now = new Date();
const diff = Math.floor((now - date) / 1000);
if (diff < 60) return 'Gerade eben';
if (diff < 3600) return `vor ${Math.floor(diff / 60)} Min.`;
if (diff < 86400) return `vor ${Math.floor(diff / 3600)} Std.`;
if (diff < 604800) return `vor ${Math.floor(diff / 86400)} Tagen`;
return date.toLocaleDateString('de-DE');
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Update notification badge
async function updateNotificationBadge() {
if (!accessToken) return;
try {
const response = await fetch('/api/v1/notifications/unread-count', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (!response.ok) return;
const data = await response.json();
const badge = document.getElementById('notification-badge');
if (badge) {
const count = data.unread_count || 0;
badge.textContent = count > 99 ? '99+' : count;
badge.classList.toggle('hidden', count === 0);
}
} catch (e) {
console.error('Failed to update badge:', e);
}
}
// Mark notification as read
async function markNotificationRead(id) {
if (!accessToken) return;
try {
await fetch(`/api/v1/notifications/${id}/read`, {
method: 'PUT',
headers: { 'Authorization': `Bearer ${accessToken}` }
});
// Update UI
const item = document.querySelector(`.notification-item[onclick*="${id}"]`);
if (item) item.classList.remove('unread');
updateNotificationBadge();
} catch (e) {
console.error('Failed to mark notification as read:', e);
}
}
// Mark all notifications as read
async function markAllNotificationsRead() {
if (!accessToken) return;
try {
await fetch('/api/v1/notifications/read-all', {
method: 'PUT',
headers: { 'Authorization': `Bearer ${accessToken}` }
});
// Update UI
document.querySelectorAll('.notification-item.unread').forEach(item => {
item.classList.remove('unread');
});
updateNotificationBadge();
} catch (e) {
console.error('Failed to mark all as read:', e);
}
}
// Load more notifications
function loadMoreNotifications() {
loadNotifications(true);
}
// Start polling for new notifications
function startNotificationPolling() {
stopNotificationPolling();
notificationPollingInterval = setInterval(() => {
updateNotificationBadge();
}, 30000); // Poll every 30 seconds
}
// Stop polling
function stopNotificationPolling() {
if (notificationPollingInterval) {
clearInterval(notificationPollingInterval);
notificationPollingInterval = null;
}
}
// Show notification preferences modal
function showNotificationPreferences() {
document.getElementById('notification-panel')?.classList.remove('active');
document.getElementById('notification-prefs-modal')?.classList.add('active');
loadNotificationPreferences();
}
// Close notification preferences modal
function closeNotificationPreferences() {
document.getElementById('notification-prefs-modal')?.classList.remove('active');
}
// Load notification preferences
async function loadNotificationPreferences() {
if (!accessToken) return;
try {
const response = await fetch('/api/v1/notifications/preferences', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (!response.ok) return;
const prefs = await response.json();
notificationPrefs = prefs;
// Update UI toggles
updateToggle('pref-email-toggle', prefs.email_enabled);
updateToggle('pref-inapp-toggle', prefs.in_app_enabled);
updateToggle('pref-push-toggle', prefs.push_enabled);
} catch (e) {
console.error('Failed to load preferences:', e);
}
}
// Update toggle UI
function updateToggle(id, active) {
const toggle = document.getElementById(id);
if (toggle) {
toggle.classList.toggle('active', active);
}
}
// Toggle notification preference
function toggleNotificationPref(type) {
const toggleMap = {
'email': 'pref-email-toggle',
'inapp': 'pref-inapp-toggle',
'push': 'pref-push-toggle'
};
const prefMap = {
'email': 'email_enabled',
'inapp': 'in_app_enabled',
'push': 'push_enabled'
};
const toggleId = toggleMap[type];
const prefKey = prefMap[type];
const toggle = document.getElementById(toggleId);
if (toggle) {
const isActive = toggle.classList.toggle('active');
notificationPrefs[prefKey] = isActive;
}
}
// Save notification preferences
async function saveNotificationPreferences() {
if (!accessToken) return;
try {
const response = await fetch('/api/v1/notifications/preferences', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(notificationPrefs)
});
if (response.ok) {
closeNotificationPreferences();
alert('Einstellungen gespeichert!');
} else {
alert('Fehler beim Speichern der Einstellungen');
}
} catch (e) {
console.error('Failed to save preferences:', e);
alert('Fehler beim Speichern der Einstellungen');
}
}
// ==========================================
// SUSPENSION CHECK FUNCTIONS
// ==========================================
let isSuspended = false;
// Check suspension status after login
async function checkSuspensionStatus() {
if (!accessToken) return;
try {
const response = await fetch('/api/v1/account/suspension-status', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (!response.ok) return;
const data = await response.json();
if (data.suspended) {
isSuspended = true;
showSuspensionOverlay(data);
} else {
isSuspended = false;
hideSuspensionOverlay();
}
} catch (e) {
console.error('Failed to check suspension status:', e);
}
}
// Show suspension overlay
function showSuspensionOverlay(data) {
const overlay = document.getElementById('suspension-overlay');
const docList = document.getElementById('suspension-doc-list');
if (!overlay || !docList) return;
// Populate document list
if (data.pending_deadlines && data.pending_deadlines.length > 0) {
docList.innerHTML = data.pending_deadlines.map(d => {
const deadline = new Date(d.deadline_at);
const isOverdue = deadline < new Date();
return `
<div class="suspension-doc-item">
<span class="suspension-doc-name">${escapeHtml(d.document_name)}</span>
<span class="suspension-doc-deadline">${isOverdue ? 'Überfällig' : deadline.toLocaleDateString('de-DE')}</span>
</div>
`;
}).join('');
} else if (data.details && data.details.documents) {
docList.innerHTML = data.details.documents.map(doc => `
<div class="suspension-doc-item">
<span class="suspension-doc-name">${escapeHtml(doc)}</span>
<span class="suspension-doc-deadline">Bestätigung erforderlich</span>
</div>
`).join('');
}
overlay.classList.add('active');
}
// Hide suspension overlay
function hideSuspensionOverlay() {
const overlay = document.getElementById('suspension-overlay');
if (overlay) {
overlay.classList.remove('active');
}
}
// Show consent modal from suspension overlay
function showConsentModal() {
hideSuspensionOverlay();
// Open legal modal to consent tab
document.getElementById('legal-modal')?.classList.add('active');
// Switch to appropriate tab
}
// Initialize auth on page load
checkAuthStatus();
checkAuthUrlParams();
// ==========================================
// RICH TEXT EDITOR FUNCTIONS
// ==========================================
const versionEditor = document.getElementById('admin-version-editor');
const versionContentHidden = document.getElementById('admin-version-content');
const editorCharCount = document.getElementById('editor-char-count');
// Update hidden field and char count when editor content changes
versionEditor?.addEventListener('input', () => {
versionContentHidden.value = versionEditor.innerHTML;
const textLength = versionEditor.textContent.length;
editorCharCount.textContent = `${textLength} Zeichen`;
});
// Format document with execCommand
function formatDoc(cmd, value = null) {
versionEditor.focus();
document.execCommand(cmd, false, value);
}
// Format block element
function formatBlock(tag) {
versionEditor.focus();
document.execCommand('formatBlock', false, `<${tag}>`);
}
// Insert link
function insertLink() {
const url = prompt('Link-URL eingeben:', 'https://');
if (url) {
versionEditor.focus();
document.execCommand('createLink', false, url);
}
}
// Handle Word document upload
async function handleWordUpload(event) {
const file = event.target.files[0];
if (!file) return;
// Show loading indicator
const editor = document.getElementById('admin-version-editor');
const originalContent = editor.innerHTML;
editor.innerHTML = '<p style="color: var(--bp-text-muted); text-align: center; padding: 40px;">Word-Dokument wird verarbeitet...</p>';
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/consent/admin/versions/upload-word', {
method: 'POST',
body: formData
});
if (response.ok) {
const data = await response.json();
editor.innerHTML = data.html || '<p>Konvertierung fehlgeschlagen</p>';
versionContentHidden.value = editor.innerHTML;
// Update char count
const textLength = editor.textContent.length;
editorCharCount.textContent = `${textLength} Zeichen`;
} else {
const error = await response.json();
editor.innerHTML = originalContent;
alert('Fehler beim Importieren: ' + (error.detail || 'Unbekannter Fehler'));
}
} catch (e) {
editor.innerHTML = originalContent;
alert('Fehler beim Hochladen: ' + e.message);
}
// Reset file input
event.target.value = '';
}
// Handle paste from Word - clean up the HTML
versionEditor?.addEventListener('paste', (e) => {
// Get pasted data via clipboard API
const clipboardData = e.clipboardData || window.clipboardData;
const pastedData = clipboardData.getData('text/html') || clipboardData.getData('text/plain');
if (pastedData && clipboardData.getData('text/html')) {
e.preventDefault();
// Clean the HTML
const cleanHtml = cleanWordHtml(pastedData);
document.execCommand('insertHTML', false, cleanHtml);
// Update hidden field
versionContentHidden.value = versionEditor.innerHTML;
}
});
// Clean Word-specific HTML
function cleanWordHtml(html) {
// Create a temporary container
const temp = document.createElement('div');
temp.innerHTML = html;
// Remove Word-specific elements and attributes
const elementsToRemove = temp.querySelectorAll('style, script, meta, link, xml');
elementsToRemove.forEach(el => el.remove());
// Get text content from Word spans with specific styling
let cleanedHtml = temp.innerHTML;
// Remove mso-* styles and other Office-specific CSS
cleanedHtml = cleanedHtml.replace(/\s*mso-[^:]+:[^;]+;?/gi, '');
cleanedHtml = cleanedHtml.replace(/\s*style="[^"]*"/gi, '');
cleanedHtml = cleanedHtml.replace(/\s*class="[^"]*"/gi, '');
cleanedHtml = cleanedHtml.replace(/<o:p><\/o:p>/gi, '');
cleanedHtml = cleanedHtml.replace(/<\/?o:[^>]*>/gi, '');
cleanedHtml = cleanedHtml.replace(/<\/?w:[^>]*>/gi, '');
cleanedHtml = cleanedHtml.replace(/<\/?m:[^>]*>/gi, '');
// Clean up empty spans
cleanedHtml = cleanedHtml.replace(/<span[^>]*>\s*<\/span>/gi, '');
// Convert Word list markers to proper lists
cleanedHtml = cleanedHtml.replace(/<p[^>]*>\s*[•·]\s*/gi, '<li>');
return cleanedHtml;
}
// ==========================================
// ADMIN PANEL
// ==========================================
const adminModal = document.getElementById('admin-modal');
const adminModalClose = document.getElementById('admin-modal-close');
const adminTabs = document.querySelectorAll('.admin-tab');
const adminContents = document.querySelectorAll('.admin-content');
const btnAdmin = document.getElementById('btn-admin');
// Admin data cache
let adminDocuments = [];
let adminCookieCategories = [];
// Open admin modal
btnAdmin?.addEventListener('click', async () => {
adminModal.classList.add('active');
await loadAdminDocuments();
await loadAdminCookieCategories();
populateDocumentSelect();
});
// Close admin modal
adminModalClose?.addEventListener('click', () => {
adminModal.classList.remove('active');
});
// Close on background click
adminModal?.addEventListener('click', (e) => {
if (e.target === adminModal) {
adminModal.classList.remove('active');
}
});
// Admin tab switching
adminTabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.dataset.tab;
adminTabs.forEach(t => t.classList.remove('active'));
adminContents.forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById(`admin-${tabId}`)?.classList.add('active');
// Load stats when stats tab is clicked
if (tabId === 'stats') {
loadAdminStats();
}
});
});
// ==========================================
// DOCUMENTS MANAGEMENT
// ==========================================
async function loadAdminDocuments() {
const container = document.getElementById('admin-doc-table-container');
container.innerHTML = '<div class="admin-loading">Lade Dokumente...</div>';
try {
const res = await fetch('/api/consent/admin/documents');
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
adminDocuments = data.documents || [];
renderDocumentsTable();
} catch(e) {
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Dokumente.</div>';
}
}
function renderDocumentsTable() {
const container = document.getElementById('admin-doc-table-container');
// Alle Dokumente anzeigen
const allDocs = adminDocuments;
if (allDocs.length === 0) {
container.innerHTML = '<div class="admin-empty">Keine Dokumente vorhanden. Klicken Sie auf "+ Neues Dokument" um ein Dokument zu erstellen.</div>';
return;
}
const typeLabels = {
'terms': 'AGB',
'privacy': 'Datenschutz',
'cookies': 'Cookies',
'community': 'Community',
'imprint': 'Impressum'
};
const html = `
<table class="admin-table">
<thead>
<tr>
<th>Typ</th>
<th>Name</th>
<th>Beschreibung</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
${allDocs.map(doc => `
<tr>
<td><span class="admin-badge admin-badge-${doc.is_mandatory ? 'mandatory' : 'optional'}">${typeLabels[doc.type] || doc.type}</span></td>
<td>${doc.name}</td>
<td style="color: var(--bp-text-muted); font-size: 12px;">${doc.description || '-'}</td>
<td>
${doc.is_active ? '<span class="admin-badge admin-badge-published">Aktiv</span>' : '<span class="admin-badge admin-badge-draft">Inaktiv</span>'}
${doc.is_mandatory ? '<span class="admin-badge admin-badge-mandatory" style="margin-left: 4px;">Pflicht</span>' : ''}
</td>
<td class="admin-actions">
<button class="admin-btn admin-btn-secondary" onclick="editDocument('${doc.id}')" title="Bearbeiten">✏️</button>
<button class="admin-btn admin-btn-primary" onclick="goToVersions('${doc.id}')">Versionen</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = html;
}
function goToVersions(docId) {
// Wechsle zum Versionen-Tab und wähle das Dokument aus
const versionsTab = document.querySelector('.admin-tab[data-tab="versions"]');
if (versionsTab) {
versionsTab.click();
setTimeout(() => {
const select = document.getElementById('admin-version-doc-select');
if (select) {
select.value = docId;
loadVersionsForDocument();
}
}, 100);
}
}
function showDocumentForm(doc = null) {
const form = document.getElementById('admin-document-form');
const title = document.getElementById('admin-document-form-title');
if (doc) {
title.textContent = 'Dokument bearbeiten';
document.getElementById('admin-document-id').value = doc.id;
document.getElementById('admin-document-type').value = doc.type;
document.getElementById('admin-document-name').value = doc.name;
document.getElementById('admin-document-description').value = doc.description || '';
document.getElementById('admin-document-mandatory').checked = doc.is_mandatory;
} else {
title.textContent = 'Neues Dokument erstellen';
document.getElementById('admin-document-id').value = '';
document.getElementById('admin-document-type').value = '';
document.getElementById('admin-document-name').value = '';
document.getElementById('admin-document-description').value = '';
document.getElementById('admin-document-mandatory').checked = true;
}
form.style.display = 'block';
}
function hideDocumentForm() {
document.getElementById('admin-document-form').style.display = 'none';
}
function editDocument(docId) {
const doc = adminDocuments.find(d => d.id === docId);
if (doc) showDocumentForm(doc);
}
async function saveDocument() {
const docId = document.getElementById('admin-document-id').value;
const docType = document.getElementById('admin-document-type').value;
const docName = document.getElementById('admin-document-name').value;
if (!docType || !docName) {
alert('Bitte füllen Sie alle Pflichtfelder aus (Typ und Name).');
return;
}
const data = {
type: docType,
name: docName,
description: document.getElementById('admin-document-description').value || null,
is_mandatory: document.getElementById('admin-document-mandatory').checked
};
try {
const url = docId ? `/api/consent/admin/documents/${docId}` : '/api/consent/admin/documents';
const method = docId ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!res.ok) throw new Error('Failed to save');
hideDocumentForm();
await loadAdminDocuments();
populateDocumentSelect();
alert('Dokument gespeichert!');
} catch(e) {
alert('Fehler beim Speichern: ' + e.message);
}
}
async function deleteDocument(docId) {
if (!confirm('Dokument wirklich deaktivieren?')) return;
try {
const res = await fetch(`/api/consent/admin/documents/${docId}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed to delete');
await loadAdminDocuments();
populateDocumentSelect();
alert('Dokument deaktiviert!');
} catch(e) {
alert('Fehler: ' + e.message);
}
}
// ==========================================
// VERSIONS MANAGEMENT
// ==========================================
function populateDocumentSelect() {
const select = document.getElementById('admin-version-doc-select');
const uniqueDocs = [...new Map(adminDocuments.map(d => [d.type, d])).values()];
select.innerHTML = '<option value="">-- Dokument auswählen --</option>' +
adminDocuments.filter(d => d.is_active).map(doc =>
`<option value="${doc.id}">${doc.name} (${doc.type})</option>`
).join('');
}
async function loadVersionsForDocument() {
const docId = document.getElementById('admin-version-doc-select').value;
const container = document.getElementById('admin-version-table-container');
const btnNew = document.getElementById('btn-new-version');
if (!docId) {
container.innerHTML = '<div class="admin-empty">Wählen Sie ein Dokument aus.</div>';
btnNew.disabled = true;
return;
}
btnNew.disabled = false;
container.innerHTML = '<div class="admin-loading">Lade Versionen...</div>';
try {
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
renderVersionsTable(data.versions || []);
} catch(e) {
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Versionen.</div>';
}
}
function renderVersionsTable(versions) {
const container = document.getElementById('admin-version-table-container');
if (versions.length === 0) {
container.innerHTML = '<div class="admin-empty">Keine Versionen vorhanden.</div>';
return;
}
const getStatusBadge = (status) => {
const statusLabels = {
'draft': 'Entwurf',
'review': 'In Prüfung',
'approved': 'Genehmigt',
'rejected': 'Abgelehnt',
'scheduled': 'Geplant',
'published': 'Veröffentlicht',
'archived': 'Archiviert'
};
return statusLabels[status] || status;
};
const formatScheduledDate = (isoDate) => {
if (!isoDate) return '';
const date = new Date(isoDate);
return date.toLocaleString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit'
});
};
const html = `
<table class="admin-table">
<thead>
<tr>
<th>Version</th>
<th>Sprache</th>
<th>Titel</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
${versions.map(v => `
<tr>
<td>${v.version}</td>
<td>${v.language.toUpperCase()}</td>
<td>${v.title}</td>
<td>
<span class="admin-badge admin-badge-${v.status}">${getStatusBadge(v.status)}</span>
${v.scheduled_publish_at ? `<br><small>Geplant: ${formatScheduledDate(v.scheduled_publish_at)}</small>` : ''}
</td>
<td class="admin-actions">
${v.status === 'draft' ? `
<button class="admin-btn admin-btn-edit" onclick="editVersion('${v.id}')">Bearbeiten</button>
<button class="admin-btn admin-btn-primary" onclick="submitForReview('${v.id}')">Zur Prüfung</button>
<button class="admin-btn admin-btn-delete" onclick="deleteVersion('${v.id}')" title="Version dauerhaft löschen">🗑️</button>
` : ''}
${v.status === 'review' ? `
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
<button class="admin-btn admin-btn-primary" onclick="showApprovalDialog('${v.id}')">Genehmigen</button>
<button class="admin-btn admin-btn-delete" onclick="rejectVersion('${v.id}')">Ablehnen</button>
` : ''}
${v.status === 'rejected' ? `
<button class="admin-btn admin-btn-edit" onclick="editVersion('${v.id}')">Bearbeiten</button>
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
<button class="admin-btn admin-btn-delete" onclick="deleteVersion('${v.id}')" title="Version dauerhaft löschen">🗑️</button>
` : ''}
${v.status === 'scheduled' ? `
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
<span class="admin-info-text">Wartet auf Veröffentlichung</span>
` : ''}
${v.status === 'approved' ? `
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
<button class="admin-btn admin-btn-publish" onclick="publishVersion('${v.id}')">Sofort veröffentlichen</button>
<button class="admin-btn admin-btn-delete" onclick="rejectVersion('${v.id}')">Zurücksetzen</button>
` : ''}
${v.status === 'published' ? `
<button class="admin-btn admin-btn-delete" onclick="archiveVersion('${v.id}')">Archivieren</button>
` : ''}
<button class="admin-btn" onclick="showApprovalHistory('${v.id}')">Historie</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = html;
}
function showVersionForm() {
const form = document.getElementById('admin-version-form');
document.getElementById('admin-version-id').value = '';
document.getElementById('admin-version-number').value = '';
document.getElementById('admin-version-lang').value = 'de';
document.getElementById('admin-version-title').value = '';
document.getElementById('admin-version-summary').value = '';
document.getElementById('admin-version-content').value = '';
// Clear rich text editor
const editor = document.getElementById('admin-version-editor');
if (editor) {
editor.innerHTML = '';
document.getElementById('editor-char-count').textContent = '0 Zeichen';
}
form.classList.add('active');
}
function hideVersionForm() {
document.getElementById('admin-version-form').classList.remove('active');
}
async function editVersion(versionId) {
// Lade die Version und fülle das Formular
const docId = document.getElementById('admin-version-doc-select').value;
try {
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
if (!res.ok) throw new Error('Failed to load versions');
const data = await res.json();
const version = (data.versions || []).find(v => v.id === versionId);
if (!version) {
alert('Version nicht gefunden');
return;
}
// Formular öffnen und Daten einfügen
const form = document.getElementById('admin-version-form');
document.getElementById('admin-version-id').value = version.id;
document.getElementById('admin-version-number').value = version.version;
document.getElementById('admin-version-lang').value = version.language;
document.getElementById('admin-version-title').value = version.title;
document.getElementById('admin-version-summary').value = version.summary || '';
// Rich-Text-Editor mit Inhalt füllen
const editor = document.getElementById('admin-version-editor');
if (editor) {
editor.innerHTML = version.content || '';
const charCount = editor.textContent.length;
document.getElementById('editor-char-count').textContent = charCount + ' Zeichen';
}
document.getElementById('admin-version-content').value = version.content || '';
form.classList.add('active');
} catch(e) {
alert('Fehler beim Laden der Version: ' + e.message);
}
}
async function saveVersion() {
const docId = document.getElementById('admin-version-doc-select').value;
const versionId = document.getElementById('admin-version-id').value;
// Get content from rich text editor
const editor = document.getElementById('admin-version-editor');
const content = editor ? editor.innerHTML : document.getElementById('admin-version-content').value;
const data = {
document_id: docId,
version: document.getElementById('admin-version-number').value,
language: document.getElementById('admin-version-lang').value,
title: document.getElementById('admin-version-title').value,
summary: document.getElementById('admin-version-summary').value,
content: content
};
try {
const url = versionId ? `/api/consent/admin/versions/${versionId}` : '/api/consent/admin/versions';
const method = versionId ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!res.ok) throw new Error('Failed to save');
hideVersionForm();
await loadVersionsForDocument();
alert('Version gespeichert!');
} catch(e) {
alert('Fehler beim Speichern: ' + e.message);
}
}
async function publishVersion(versionId) {
if (!confirm('Version wirklich veröffentlichen?')) return;
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/publish`, { method: 'POST' });
if (!res.ok) throw new Error('Failed to publish');
await loadVersionsForDocument();
alert('Version veröffentlicht!');
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function archiveVersion(versionId) {
if (!confirm('Version wirklich archivieren?')) return;
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/archive`, { method: 'POST' });
if (!res.ok) throw new Error('Failed to archive');
await loadVersionsForDocument();
alert('Version archiviert!');
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function deleteVersion(versionId) {
if (!confirm('Version wirklich dauerhaft löschen?\\n\\nDie Versionsnummer wird wieder frei und kann erneut verwendet werden.\\n\\nDiese Aktion kann nicht rückgängig gemacht werden!')) return;
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}`, { method: 'DELETE' });
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.message || err.error || 'Löschen fehlgeschlagen');
}
await loadVersionsForDocument();
alert('Version wurde dauerhaft gelöscht.');
} catch(e) {
alert('Fehler: ' + e.message);
}
}
// ==========================================
// DSB APPROVAL WORKFLOW
// ==========================================
async function submitForReview(versionId) {
if (!confirm('Version zur DSB-Prüfung einreichen?')) return;
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/submit-review`, { method: 'POST' });
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail?.error || 'Einreichung fehlgeschlagen');
}
await loadVersionsForDocument();
alert('Version wurde zur Prüfung eingereicht!');
} catch(e) {
alert('Fehler: ' + e.message);
}
}
// Dialog für Genehmigung mit Veröffentlichungszeitpunkt
let approvalVersionId = null;
function showApprovalDialog(versionId) {
approvalVersionId = versionId;
const dialog = document.getElementById('approval-dialog');
// Setze Minimum-Datum auf morgen
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
document.getElementById('approval-date').min = tomorrow.toISOString().split('T')[0];
document.getElementById('approval-date').value = '';
document.getElementById('approval-time').value = '00:00';
document.getElementById('approval-comment').value = '';
dialog.classList.add('active');
}
function hideApprovalDialog() {
document.getElementById('approval-dialog').classList.remove('active');
approvalVersionId = null;
}
async function submitApproval() {
if (!approvalVersionId) return;
const dateInput = document.getElementById('approval-date').value;
const timeInput = document.getElementById('approval-time').value;
const comment = document.getElementById('approval-comment').value;
let scheduledPublishAt = null;
if (dateInput) {
// Kombiniere Datum und Zeit zu ISO 8601
const datetime = new Date(dateInput + 'T' + (timeInput || '00:00') + ':00');
scheduledPublishAt = datetime.toISOString();
}
try {
const body = { comment: comment || '' };
if (scheduledPublishAt) {
body.scheduled_publish_at = scheduledPublishAt;
}
const res = await fetch(`/api/consent/admin/versions/${approvalVersionId}/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail?.error || data.detail || 'Genehmigung fehlgeschlagen');
}
hideApprovalDialog();
await loadVersionsForDocument();
if (scheduledPublishAt) {
const date = new Date(scheduledPublishAt);
alert('Version genehmigt! Geplante Veröffentlichung: ' + date.toLocaleString('de-DE'));
} else {
alert('Version genehmigt! Sie kann jetzt manuell veröffentlicht werden.');
}
} catch(e) {
alert('Fehler: ' + e.message);
}
}
// Alte Funktion für Rückwärtskompatibilität
async function approveVersion(versionId) {
showApprovalDialog(versionId);
}
async function rejectVersion(versionId) {
const comment = prompt('Begründung für Ablehnung (erforderlich):');
if (!comment) {
alert('Eine Begründung ist erforderlich.');
return;
}
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/reject`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment: comment })
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail?.error || data.detail || 'Ablehnung fehlgeschlagen');
}
await loadVersionsForDocument();
alert('Version abgelehnt und zurück in Entwurf-Status versetzt.');
} catch(e) {
alert('Fehler: ' + e.message);
}
}
// Store current compare version for actions
let currentCompareVersionId = null;
let currentCompareVersionStatus = null;
let currentCompareDocId = null;
async function showCompareView(versionId) {
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/compare`);
if (!res.ok) throw new Error('Vergleich konnte nicht geladen werden');
const data = await res.json();
const currentVersion = data.current_version;
const publishedVersion = data.published_version;
const history = data.approval_history || [];
// Store version info for actions
currentCompareVersionId = versionId;
currentCompareVersionStatus = currentVersion.status;
currentCompareDocId = currentVersion.document_id;
// Update header info
document.getElementById('compare-published-info').textContent =
publishedVersion ? `${publishedVersion.title} (v${publishedVersion.version})` : 'Keine Version';
document.getElementById('compare-draft-info').textContent =
`${currentVersion.title} (v${currentVersion.version})`;
document.getElementById('compare-published-version').textContent =
publishedVersion ? `v${publishedVersion.version}` : '';
document.getElementById('compare-draft-version').textContent =
`v${currentVersion.version} - ${currentVersion.status}`;
// Populate content panels
const leftPanel = document.getElementById('compare-content-left');
const rightPanel = document.getElementById('compare-content-right');
leftPanel.innerHTML = publishedVersion
? publishedVersion.content
: '<div class="no-content">Keine veröffentlichte Version vorhanden</div>';
rightPanel.innerHTML = currentVersion.content || '<div class="no-content">Kein Inhalt</div>';
// Populate history
const historyContainer = document.getElementById('compare-history-container');
if (history.length > 0) {
historyContainer.innerHTML = `
<div class="compare-history-title">Genehmigungsverlauf</div>
<div class="compare-history-list">
${history.map(h => `
<span class="compare-history-item">
<strong>${h.action}</strong> von ${h.approver || 'System'}
(${new Date(h.created_at).toLocaleString('de-DE')})
${h.comment ? ': ' + h.comment : ''}
</span>
`).join(' | ')}
</div>
`;
} else {
historyContainer.innerHTML = '';
}
// Render action buttons based on status
renderCompareActions(currentVersion.status, versionId);
// Setup synchronized scrolling
setupSyncScroll(leftPanel, rightPanel);
// Show the overlay
document.getElementById('version-compare-view').classList.add('active');
document.body.style.overflow = 'hidden';
} catch(e) {
alert('Fehler beim Laden des Vergleichs: ' + e.message);
}
}
function renderCompareActions(status, versionId) {
const actionsContainer = document.getElementById('compare-actions-container');
let buttons = '';
// Edit button - available for draft, review, and rejected
if (status === 'draft' || status === 'review' || status === 'rejected') {
buttons += `<button class="btn btn-primary" onclick="editVersionFromCompare('${versionId}')">
✏️ Version bearbeiten
</button>`;
}
// Status-specific actions
if (status === 'draft') {
buttons += `<button class="btn" onclick="submitForReviewFromCompare('${versionId}')">
📤 Zur Prüfung einreichen
</button>`;
}
if (status === 'review') {
buttons += `<button class="btn btn-success" onclick="approveVersionFromCompare('${versionId}')">
✅ Genehmigen
</button>`;
buttons += `<button class="btn btn-danger" onclick="rejectVersionFromCompare('${versionId}')">
❌ Ablehnen
</button>`;
}
if (status === 'approved') {
buttons += `<button class="btn btn-primary" onclick="publishVersionFromCompare('${versionId}')">
🚀 Veröffentlichen
</button>`;
}
// Delete button for draft/rejected
if (status === 'draft' || status === 'rejected') {
buttons += `<button class="btn btn-danger" onclick="deleteVersionFromCompare('${versionId}')" style="margin-left: auto;">
🗑️ Löschen
</button>`;
}
actionsContainer.innerHTML = buttons;
}
async function editVersionFromCompare(versionId) {
// Store the doc ID before closing compare view
const docId = currentCompareDocId;
// Close compare view
hideCompareView();
// Switch to versions tab
const versionsTab = document.querySelector('.admin-tab[data-tab="versions"]');
if (versionsTab) {
versionsTab.click();
}
// Wait a moment for the tab to become active
await new Promise(resolve => setTimeout(resolve, 150));
// Ensure document select is populated
populateDocumentSelect();
// Set the document select if we have the doc ID
if (docId) {
const select = document.getElementById('admin-version-doc-select');
if (select) {
select.value = docId;
// Load versions for this document
await loadVersionsForDocument();
}
}
// Now load the version data directly and open the form
try {
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
if (!res.ok) throw new Error('Failed to load versions');
const data = await res.json();
const version = (data.versions || []).find(v => v.id === versionId);
if (!version) {
alert('Version nicht gefunden');
return;
}
// Open the form and fill with version data
const form = document.getElementById('admin-version-form');
document.getElementById('admin-version-id').value = version.id;
document.getElementById('admin-version-number').value = version.version;
document.getElementById('admin-version-lang').value = version.language;
document.getElementById('admin-version-title').value = version.title;
document.getElementById('admin-version-summary').value = version.summary || '';
// Fill rich text editor with content
const editor = document.getElementById('admin-version-editor');
if (editor) {
editor.innerHTML = version.content || '';
const charCount = editor.textContent.length;
document.getElementById('editor-char-count').textContent = charCount + ' Zeichen';
}
document.getElementById('admin-version-content').value = version.content || '';
form.classList.add('active');
} catch(e) {
alert('Fehler beim Laden der Version: ' + e.message);
}
}
async function submitForReviewFromCompare(versionId) {
await submitForReview(versionId);
hideCompareView();
await loadVersionsForDocument();
}
async function approveVersionFromCompare(versionId) {
const comment = prompt('Kommentar zur Genehmigung (optional):');
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment: comment || '' })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.error || err.error || 'Genehmigung fehlgeschlagen');
}
alert('Version genehmigt!');
hideCompareView();
await loadVersionsForDocument();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function rejectVersionFromCompare(versionId) {
const comment = prompt('Begründung für die Ablehnung (erforderlich):');
if (!comment) {
alert('Eine Begründung ist erforderlich.');
return;
}
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/reject`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment: comment })
});
if (!res.ok) throw new Error('Ablehnung fehlgeschlagen');
alert('Version abgelehnt. Der Autor kann sie überarbeiten.');
hideCompareView();
await loadVersionsForDocument();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function publishVersionFromCompare(versionId) {
if (!confirm('Version wirklich veröffentlichen?')) return;
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/publish`, { method: 'POST' });
if (!res.ok) throw new Error('Veröffentlichung fehlgeschlagen');
alert('Version veröffentlicht!');
hideCompareView();
await loadVersionsForDocument();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function deleteVersionFromCompare(versionId) {
if (!confirm('Version wirklich dauerhaft löschen?\\n\\nDie Versionsnummer wird wieder frei.')) return;
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}`, { method: 'DELETE' });
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.message || err.error || 'Löschen fehlgeschlagen');
}
alert('Version gelöscht!');
hideCompareView();
await loadVersionsForDocument();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
function hideCompareView() {
document.getElementById('version-compare-view').classList.remove('active');
document.body.style.overflow = '';
// Remove scroll listeners
const leftPanel = document.getElementById('compare-content-left');
const rightPanel = document.getElementById('compare-content-right');
if (leftPanel) leftPanel.onscroll = null;
if (rightPanel) rightPanel.onscroll = null;
}
// Synchronized scrolling between two panels
let syncScrollActive = false;
function setupSyncScroll(leftPanel, rightPanel) {
// Remove any existing listeners first
leftPanel.onscroll = null;
rightPanel.onscroll = null;
// Flag to prevent infinite scroll loops
let isScrolling = false;
rightPanel.onscroll = function() {
if (isScrolling) return;
isScrolling = true;
// Calculate scroll percentage
const rightScrollPercent = rightPanel.scrollTop / (rightPanel.scrollHeight - rightPanel.clientHeight);
// Apply same percentage to left panel
const leftMaxScroll = leftPanel.scrollHeight - leftPanel.clientHeight;
leftPanel.scrollTop = rightScrollPercent * leftMaxScroll;
setTimeout(() => { isScrolling = false; }, 10);
};
leftPanel.onscroll = function() {
if (isScrolling) return;
isScrolling = true;
// Calculate scroll percentage
const leftScrollPercent = leftPanel.scrollTop / (leftPanel.scrollHeight - leftPanel.clientHeight);
// Apply same percentage to right panel
const rightMaxScroll = rightPanel.scrollHeight - rightPanel.clientHeight;
rightPanel.scrollTop = leftScrollPercent * rightMaxScroll;
setTimeout(() => { isScrolling = false; }, 10);
};
}
async function showApprovalHistory(versionId) {
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/approval-history`);
if (!res.ok) throw new Error('Historie konnte nicht geladen werden');
const data = await res.json();
const history = data.approval_history || [];
const content = history.length === 0
? '<p>Keine Genehmigungshistorie vorhanden.</p>'
: `
<table class="admin-table" style="font-size: 14px;">
<thead>
<tr>
<th>Aktion</th>
<th>Benutzer</th>
<th>Kommentar</th>
<th>Datum</th>
</tr>
</thead>
<tbody>
${history.map(h => `
<tr>
<td><span class="admin-badge admin-badge-${h.action}">${h.action}</span></td>
<td>${h.approver || h.name || '-'}</td>
<td>${h.comment || '-'}</td>
<td>${new Date(h.created_at).toLocaleString('de-DE')}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
showCustomModal('Genehmigungsverlauf', content, [
{ text: 'Schließen', onClick: () => hideCustomModal() }
]);
} catch(e) {
alert('Fehler: ' + e.message);
}
}
// Custom Modal Functions
function showCustomModal(title, content, buttons = []) {
let modal = document.getElementById('custom-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'custom-modal';
modal.className = 'modal-overlay';
document.body.appendChild(modal);
}
modal.innerHTML = `
<div class="modal-content" style="max-width: 900px; width: 95%;">
<div class="modal-header">
<h2>${title}</h2>
<button class="modal-close" onclick="hideCustomModal()">&times;</button>
</div>
<div class="modal-body" style="max-height: 80vh; overflow-y: auto;">
${content}
</div>
${buttons.length > 0 ? `
<div class="modal-footer" style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px;">
${buttons.map(b => `
<button class="admin-btn ${b.primary ? 'admin-btn-primary' : ''}" onclick="(${b.onClick.toString()})()">${b.text}</button>
`).join('')}
</div>
` : ''}
</div>
`;
modal.classList.add('active');
}
function hideCustomModal() {
const modal = document.getElementById('custom-modal');
if (modal) modal.classList.remove('active');
}
// ==========================================
// COOKIE CATEGORIES MANAGEMENT
// ==========================================
async function loadAdminCookieCategories() {
const container = document.getElementById('admin-cookie-table-container');
container.innerHTML = '<div class="admin-loading">Lade Cookie-Kategorien...</div>';
try {
const res = await fetch('/api/consent/admin/cookies/categories');
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
adminCookieCategories = data.categories || [];
renderCookieCategoriesTable();
} catch(e) {
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Kategorien.</div>';
}
}
function renderCookieCategoriesTable() {
const container = document.getElementById('admin-cookie-table-container');
if (adminCookieCategories.length === 0) {
container.innerHTML = '<div class="admin-empty">Keine Cookie-Kategorien vorhanden.</div>';
return;
}
const html = `
<table class="admin-table">
<thead>
<tr>
<th>Name</th>
<th>Anzeigename (DE)</th>
<th>Typ</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
${adminCookieCategories.map(cat => `
<tr>
<td><code>${cat.name}</code></td>
<td>${cat.display_name_de}</td>
<td>
${cat.is_mandatory ? '<span class="admin-badge admin-badge-mandatory">Notwendig</span>' : '<span class="admin-badge admin-badge-optional">Optional</span>'}
</td>
<td class="admin-actions">
<button class="admin-btn admin-btn-edit" onclick="editCookieCategory('${cat.id}')">Bearbeiten</button>
${!cat.is_mandatory ? `<button class="admin-btn admin-btn-delete" onclick="deleteCookieCategory('${cat.id}')">Löschen</button>` : ''}
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = html;
}
function showCookieForm(cat = null) {
const form = document.getElementById('admin-cookie-form');
if (cat) {
document.getElementById('admin-cookie-id').value = cat.id;
document.getElementById('admin-cookie-name').value = cat.name;
document.getElementById('admin-cookie-display-de').value = cat.display_name_de;
document.getElementById('admin-cookie-display-en').value = cat.display_name_en || '';
document.getElementById('admin-cookie-desc-de').value = cat.description_de || '';
document.getElementById('admin-cookie-mandatory').checked = cat.is_mandatory;
} else {
document.getElementById('admin-cookie-id').value = '';
document.getElementById('admin-cookie-name').value = '';
document.getElementById('admin-cookie-display-de').value = '';
document.getElementById('admin-cookie-display-en').value = '';
document.getElementById('admin-cookie-desc-de').value = '';
document.getElementById('admin-cookie-mandatory').checked = false;
}
form.classList.add('active');
}
function hideCookieForm() {
document.getElementById('admin-cookie-form').classList.remove('active');
}
function editCookieCategory(catId) {
const cat = adminCookieCategories.find(c => c.id === catId);
if (cat) showCookieForm(cat);
}
async function saveCookieCategory() {
const catId = document.getElementById('admin-cookie-id').value;
const data = {
name: document.getElementById('admin-cookie-name').value,
display_name_de: document.getElementById('admin-cookie-display-de').value,
display_name_en: document.getElementById('admin-cookie-display-en').value,
description_de: document.getElementById('admin-cookie-desc-de').value,
is_mandatory: document.getElementById('admin-cookie-mandatory').checked
};
try {
const url = catId ? `/api/consent/admin/cookies/categories/${catId}` : '/api/consent/admin/cookies/categories';
const method = catId ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!res.ok) throw new Error('Failed to save');
hideCookieForm();
await loadAdminCookieCategories();
alert('Kategorie gespeichert!');
} catch(e) {
alert('Fehler beim Speichern: ' + e.message);
}
}
async function deleteCookieCategory(catId) {
if (!confirm('Kategorie wirklich löschen?')) return;
try {
const res = await fetch(`/api/consent/admin/cookies/categories/${catId}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed to delete');
await loadAdminCookieCategories();
alert('Kategorie gelöscht!');
} catch(e) {
alert('Fehler: ' + e.message);
}
}
// ==========================================
// STATISTICS & GDPR EXPORT
// ==========================================
let dataCategories = [];
async function loadAdminStats() {
const container = document.getElementById('admin-stats-container');
container.innerHTML = '<div class="admin-loading">Lade Statistiken & DSGVO-Informationen...</div>';
try {
// Lade Datenkategorien
const catRes = await fetch('/api/consent/privacy/data-categories');
if (catRes.ok) {
const catData = await catRes.json();
dataCategories = catData.categories || [];
}
renderStatsPanel();
} catch(e) {
container.innerHTML = '<div class="admin-empty">Fehler beim Laden: ' + e.message + '</div>';
}
}
function renderStatsPanel() {
const container = document.getElementById('admin-stats-container');
// Kategorisiere Daten
const essential = dataCategories.filter(c => c.is_essential);
const optional = dataCategories.filter(c => !c.is_essential);
const html = `
<div style="display: grid; gap: 24px;">
<!-- GDPR Export Section -->
<div class="admin-form" style="padding: 20px;">
<h3 style="margin: 0 0 16px 0; font-size: 16px; color: var(--bp-text);">
<span style="margin-right: 8px;">📋</span> DSGVO-Datenauskunft (Art. 15)
</h3>
<p style="color: var(--bp-text-muted); font-size: 13px; margin-bottom: 16px;">
Exportieren Sie alle personenbezogenen Daten eines Nutzers als PDF-Dokument.
Dies erfüllt die Anforderungen der DSGVO Art. 15 (Auskunftsrecht).
</p>
<div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
<input type="text" id="gdpr-export-user-id" class="admin-form-input"
placeholder="Benutzer-ID (optional für eigene Daten)"
style="flex: 1; min-width: 200px;">
<button class="admin-btn admin-btn-primary" onclick="exportUserDataPdf()">
PDF exportieren
</button>
<button class="admin-btn" onclick="previewUserDataHtml()">
HTML-Vorschau
</button>
</div>
<div id="gdpr-export-status" style="margin-top: 12px; font-size: 13px;"></div>
</div>
<!-- Data Retention Overview -->
<div class="admin-form" style="padding: 20px;">
<h3 style="margin: 0 0 16px 0; font-size: 16px; color: var(--bp-text);">
<span style="margin-right: 8px;">🗄️</span> Datenkategorien & Löschfristen
</h3>
<div style="margin-bottom: 20px;">
<h4 style="font-size: 14px; color: var(--bp-primary); margin: 0 0 12px 0;">
Essentielle Daten (Pflicht für Betrieb)
</h4>
<table class="admin-table">
<thead>
<tr>
<th>Kategorie</th>
<th>Beschreibung</th>
<th>Löschfrist</th>
<th>Rechtsgrundlage</th>
</tr>
</thead>
<tbody>
${essential.map(cat => `
<tr>
<td><strong>${cat.name_de}</strong></td>
<td>${cat.description_de}</td>
<td><span class="admin-badge admin-badge-published">${cat.retention_period}</span></td>
<td style="font-size: 11px; color: var(--bp-text-muted);">${cat.legal_basis}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<div>
<h4 style="font-size: 14px; color: var(--bp-warning); margin: 0 0 12px 0;">
Optionale Daten (nur bei Einwilligung)
</h4>
<table class="admin-table">
<thead>
<tr>
<th>Kategorie</th>
<th>Beschreibung</th>
<th>Cookie-Kategorie</th>
<th>Löschfrist</th>
</tr>
</thead>
<tbody>
${optional.map(cat => `
<tr>
<td><strong>${cat.name_de}</strong></td>
<td>${cat.description_de}</td>
<td><span class="admin-badge">${cat.cookie_category || '-'}</span></td>
<td><span class="admin-badge admin-badge-optional">${cat.retention_period}</span></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
<!-- Quick Stats -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
<div class="admin-form" style="padding: 16px; text-align: center;">
<div style="font-size: 28px; font-weight: bold; color: var(--bp-primary);">${dataCategories.length}</div>
<div style="color: var(--bp-text-muted); font-size: 13px;">Datenkategorien</div>
</div>
<div class="admin-form" style="padding: 16px; text-align: center;">
<div style="font-size: 28px; font-weight: bold; color: var(--bp-success);">${essential.length}</div>
<div style="color: var(--bp-text-muted); font-size: 13px;">Essentiell</div>
</div>
<div class="admin-form" style="padding: 16px; text-align: center;">
<div style="font-size: 28px; font-weight: bold; color: var(--bp-warning);">${optional.length}</div>
<div style="color: var(--bp-text-muted); font-size: 13px;">Optional (Opt-in)</div>
</div>
</div>
</div>
`;
container.innerHTML = html;
}
async function exportUserDataPdf() {
const userIdInput = document.getElementById('gdpr-export-user-id');
const statusDiv = document.getElementById('gdpr-export-status');
const userId = userIdInput?.value?.trim();
statusDiv.innerHTML = '<span style="color: var(--bp-primary);">Generiere PDF...</span>';
try {
let url = '/api/consent/privacy/export-pdf';
// Wenn eine User-ID angegeben wurde, verwende den Admin-Endpoint
if (userId) {
url = `/api/consent/admin/privacy/export-pdf/${userId}`;
}
const res = await fetch(url, { method: 'POST' });
if (!res.ok) {
const error = await res.json();
throw new Error(error.detail?.message || error.detail || 'Export fehlgeschlagen');
}
// PDF herunterladen
const blob = await res.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = userId ? `datenauskunft_${userId.slice(0,8)}.pdf` : 'breakpilot_datenauskunft.pdf';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(downloadUrl);
statusDiv.innerHTML = '<span style="color: var(--bp-success);">✓ PDF erfolgreich generiert!</span>';
} catch(e) {
statusDiv.innerHTML = `<span style="color: var(--bp-danger);">Fehler: ${e.message}</span>`;
}
}
async function previewUserDataHtml() {
const statusDiv = document.getElementById('gdpr-export-status');
statusDiv.innerHTML = '<span style="color: var(--bp-primary);">Lade Vorschau...</span>';
try {
const res = await fetch('/api/consent/privacy/export-html');
if (!res.ok) {
throw new Error('Vorschau konnte nicht geladen werden');
}
const html = await res.text();
// In neuem Tab öffnen
const win = window.open('', '_blank');
win.document.write(html);
win.document.close();
statusDiv.innerHTML = '<span style="color: var(--bp-success);">✓ Vorschau in neuem Tab geöffnet</span>';
} catch(e) {
statusDiv.innerHTML = `<span style="color: var(--bp-danger);">Fehler: ${e.message}</span>`;
}
}
// ==========================================
// DSR (DATA SUBJECT REQUESTS) FUNCTIONS
// ==========================================
let dsrList = [];
let currentDSR = null;
const DSR_TYPE_LABELS = {
'access': 'Art. 15 - Auskunft',
'rectification': 'Art. 16 - Berichtigung',
'erasure': 'Art. 17 - Löschung',
'restriction': 'Art. 18 - Einschränkung',
'portability': 'Art. 20 - Datenübertragbarkeit'
};
const DSR_STATUS_LABELS = {
'intake': 'Eingang',
'identity_verification': 'Identitätsprüfung',
'processing': 'In Bearbeitung',
'completed': 'Abgeschlossen',
'rejected': 'Abgelehnt',
'cancelled': 'Storniert'
};
const DSR_STATUS_COLORS = {
'intake': '#6366f1',
'identity_verification': '#f59e0b',
'processing': '#3b82f6',
'completed': '#22c55e',
'rejected': '#ef4444',
'cancelled': '#6b7280'
};
async function loadDSRStats() {
const container = document.getElementById('dsr-stats-cards');
try {
const res = await fetch('/api/v1/admin/dsr/stats');
if (!res.ok) throw new Error('Failed to load stats');
const stats = await res.json();
container.innerHTML = `
<div class="dsms-status-card" style="border-left: 3px solid #ef4444;">
<h4>Überfällig</h4>
<div class="value" style="color: #ef4444; font-size: 24px;">${stats.overdue_requests || 0}</div>
</div>
<div class="dsms-status-card" style="border-left: 3px solid #3b82f6;">
<h4>In Bearbeitung</h4>
<div class="value" style="color: #3b82f6; font-size: 24px;">${stats.pending_requests || 0}</div>
</div>
<div class="dsms-status-card" style="border-left: 3px solid #22c55e;">
<h4>Diesen Monat abgeschlossen</h4>
<div class="value" style="color: #22c55e; font-size: 24px;">${stats.completed_this_month || 0}</div>
</div>
<div class="dsms-status-card" style="border-left: 3px solid var(--bp-primary);">
<h4>Gesamt</h4>
<div class="value" style="font-size: 24px;">${stats.total_requests || 0}</div>
</div>
<div class="dsms-status-card" style="border-left: 3px solid var(--bp-text-muted);">
<h4>Ø Bearbeitungszeit</h4>
<div class="value" style="font-size: 18px;">${(stats.average_processing_days || 0).toFixed(1)} Tage</div>
</div>
`;
} catch(e) {
container.innerHTML = `<div class="admin-empty">Fehler beim Laden der Statistiken: ${e.message}</div>`;
}
}
async function loadDSRList() {
const container = document.getElementById('dsr-table-container');
const status = document.getElementById('dsr-filter-status').value;
const requestType = document.getElementById('dsr-filter-type').value;
const overdueOnly = document.getElementById('dsr-filter-overdue').checked;
container.innerHTML = '<div class="admin-loading">Lade Betroffenenanfragen...</div>';
try {
let url = '/api/v1/admin/dsr?limit=50';
if (status) url += `&status=${status}`;
if (requestType) url += `&request_type=${requestType}`;
if (overdueOnly) url += `&overdue_only=true`;
const res = await fetch(url);
if (!res.ok) throw new Error('Failed to load DSRs');
const data = await res.json();
dsrList = data.requests || [];
if (dsrList.length === 0) {
container.innerHTML = `
<div class="admin-empty">
<p style="font-size: 48px;">📋</p>
<p>Keine Betroffenenanfragen gefunden.</p>
</div>
`;
return;
}
container.innerHTML = `
<table class="admin-table">
<thead>
<tr>
<th>Nr.</th>
<th>Typ</th>
<th>Antragsteller</th>
<th>Status</th>
<th>Priorität</th>
<th>Frist</th>
<th>Erstellt</th>
<th></th>
</tr>
</thead>
<tbody>
${dsrList.map(dsr => {
const isOverdue = new Date(dsr.deadline_at) < new Date() && !['completed', 'rejected', 'cancelled'].includes(dsr.status);
const deadlineDate = new Date(dsr.deadline_at).toLocaleDateString('de-DE');
return `
<tr style="${isOverdue ? 'background: rgba(239, 68, 68, 0.1);' : ''}">
<td><strong>${dsr.request_number}</strong></td>
<td>${DSR_TYPE_LABELS[dsr.request_type] || dsr.request_type}</td>
<td>
<div>${dsr.requester_email}</div>
${dsr.requester_name ? `<div style="font-size: 11px; color: var(--bp-text-muted);">${dsr.requester_name}</div>` : ''}
</td>
<td>
<span style="background: ${DSR_STATUS_COLORS[dsr.status]}20; color: ${DSR_STATUS_COLORS[dsr.status]}; padding: 2px 8px; border-radius: 12px; font-size: 11px;">
${DSR_STATUS_LABELS[dsr.status] || dsr.status}
</span>
</td>
<td>
<span style="color: ${dsr.priority === 'high' ? '#f59e0b' : dsr.priority === 'expedited' ? '#ef4444' : 'var(--bp-text-muted)'};">
${dsr.priority === 'expedited' ? '🔴' : dsr.priority === 'high' ? '🟡' : ''}
${dsr.priority === 'expedited' ? 'Beschleunigt' : dsr.priority === 'high' ? 'Hoch' : 'Normal'}
</span>
</td>
<td style="${isOverdue ? 'color: #ef4444; font-weight: 600;' : ''}">${deadlineDate}${isOverdue ? ' ⚠️' : ''}</td>
<td>${new Date(dsr.created_at).toLocaleDateString('de-DE')}</td>
<td>
<button class="btn btn-ghost btn-sm" onclick="showDSRDetail('${dsr.id}')">Details</button>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
<div style="margin-top: 12px; font-size: 12px; color: var(--bp-text-muted);">
${data.total || dsrList.length} Anfragen gefunden
</div>
`;
} catch(e) {
container.innerHTML = `<div class="admin-empty">Fehler: ${e.message}</div>`;
}
}
function showDSRCreateForm() {
document.getElementById('dsr-create-form').style.display = 'block';
document.getElementById('dsr-create-type').value = '';
document.getElementById('dsr-create-priority').value = 'normal';
document.getElementById('dsr-create-email').value = '';
document.getElementById('dsr-create-name').value = '';
document.getElementById('dsr-create-phone').value = '';
}
function hideDSRCreateForm() {
document.getElementById('dsr-create-form').style.display = 'none';
}
async function createDSR() {
const type = document.getElementById('dsr-create-type').value;
const priority = document.getElementById('dsr-create-priority').value;
const email = document.getElementById('dsr-create-email').value;
const name = document.getElementById('dsr-create-name').value;
const phone = document.getElementById('dsr-create-phone').value;
if (!type || !email) {
alert('Bitte füllen Sie alle Pflichtfelder aus.');
return;
}
try {
const res = await fetch('/api/v1/admin/dsr', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
request_type: type,
priority: priority,
requester_email: email,
requester_name: name || undefined,
requester_phone: phone || undefined,
source: 'admin_panel'
})
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.error || err.detail || 'Fehler beim Erstellen');
}
const data = await res.json();
alert(`Anfrage ${data.request_number} wurde erstellt.`);
hideDSRCreateForm();
loadDSRStats();
loadDSRList();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function showDSRDetail(dsrId) {
try {
const res = await fetch(`/api/v1/admin/dsr/${dsrId}`);
if (!res.ok) throw new Error('Failed to load DSR');
currentDSR = await res.json();
// Load history
const historyRes = await fetch(`/api/v1/admin/dsr/${dsrId}/history`);
const historyData = historyRes.ok ? await historyRes.json() : { history: [] };
document.getElementById('dsr-table-container').style.display = 'none';
document.getElementById('dsr-create-form').style.display = 'none';
document.getElementById('dsr-detail-view').style.display = 'block';
const isOverdue = new Date(currentDSR.deadline_at) < new Date() && !['completed', 'rejected', 'cancelled'].includes(currentDSR.status);
document.getElementById('dsr-detail-content').innerHTML = `
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 24px;">
<div>
<div style="background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 8px; padding: 20px; margin-bottom: 16px;">
<h3 style="margin: 0 0 16px 0; display: flex; align-items: center; gap: 12px;">
${currentDSR.request_number}
<span style="background: ${DSR_STATUS_COLORS[currentDSR.status]}20; color: ${DSR_STATUS_COLORS[currentDSR.status]}; padding: 4px 12px; border-radius: 12px; font-size: 12px;">
${DSR_STATUS_LABELS[currentDSR.status] || currentDSR.status}
</span>
</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Anfragetyp</div>
<div style="font-weight: 500;">${DSR_TYPE_LABELS[currentDSR.request_type] || currentDSR.request_type}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Priorität</div>
<div style="font-weight: 500;">${currentDSR.priority === 'expedited' ? '🔴 Beschleunigt' : currentDSR.priority === 'high' ? '🟡 Hoch' : 'Normal'}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Frist</div>
<div style="font-weight: 500; ${isOverdue ? 'color: #ef4444;' : ''}">${new Date(currentDSR.deadline_at).toLocaleDateString('de-DE')} ${isOverdue ? '⚠️ ÜBERFÄLLIG' : ''}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Gesetzliche Frist</div>
<div style="font-weight: 500;">${currentDSR.legal_deadline_days} Tage</div>
</div>
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Identität verifiziert</div>
<div style="font-weight: 500;">${currentDSR.identity_verified ? '✅ Ja' : '❌ Nein'}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Quelle</div>
<div style="font-weight: 500;">${currentDSR.source === 'api' ? 'API' : currentDSR.source === 'admin_panel' ? 'Admin Panel' : currentDSR.source}</div>
</div>
</div>
</div>
<div style="background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 8px; padding: 20px; margin-bottom: 16px;">
<h4 style="margin: 0 0 12px 0;">Antragsteller</h4>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">E-Mail</div>
<div style="font-weight: 500;">${currentDSR.requester_email}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Name</div>
<div style="font-weight: 500;">${currentDSR.requester_name || '-'}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Telefon</div>
<div style="font-weight: 500;">${currentDSR.requester_phone || '-'}</div>
</div>
</div>
</div>
${currentDSR.processing_notes ? `
<div style="background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 8px; padding: 20px; margin-bottom: 16px;">
<h4 style="margin: 0 0 12px 0;">Bearbeitungsnotizen</h4>
<div style="white-space: pre-wrap;">${currentDSR.processing_notes}</div>
</div>
` : ''}
${currentDSR.result_summary ? `
<div style="background: rgba(34, 197, 94, 0.1); border: 1px solid #22c55e; border-radius: 8px; padding: 20px; margin-bottom: 16px;">
<h4 style="margin: 0 0 12px 0; color: #22c55e;">Ergebnis</h4>
<div>${currentDSR.result_summary}</div>
</div>
` : ''}
${currentDSR.rejection_reason ? `
<div style="background: rgba(239, 68, 68, 0.1); border: 1px solid #ef4444; border-radius: 8px; padding: 20px; margin-bottom: 16px;">
<h4 style="margin: 0 0 12px 0; color: #ef4444;">Ablehnung</h4>
<div><strong>Rechtsgrundlage:</strong> ${currentDSR.rejection_legal_basis}</div>
<div style="margin-top: 8px;">${currentDSR.rejection_reason}</div>
</div>
` : ''}
</div>
<div>
<div style="background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 8px; padding: 20px;">
<h4 style="margin: 0 0 12px 0;">Verlauf</h4>
<div style="max-height: 400px; overflow-y: auto;">
${(historyData.history || []).map(h => `
<div style="padding: 8px 0; border-bottom: 1px solid var(--bp-border);">
<div style="font-size: 11px; color: var(--bp-text-muted);">${new Date(h.created_at).toLocaleString('de-DE')}</div>
<div style="font-size: 13px;">
${h.from_status ? `${DSR_STATUS_LABELS[h.from_status] || h.from_status}` : ''}
<strong>${DSR_STATUS_LABELS[h.to_status] || h.to_status}</strong>
</div>
${h.comment ? `<div style="font-size: 12px; color: var(--bp-text-muted); margin-top: 4px;">${h.comment}</div>` : ''}
</div>
`).join('') || '<div style="color: var(--bp-text-muted);">Kein Verlauf vorhanden</div>'}
</div>
</div>
</div>
</div>
`;
// Update button visibility based on status
const canVerify = !currentDSR.identity_verified && ['intake', 'identity_verification'].includes(currentDSR.status);
const canComplete = ['processing'].includes(currentDSR.status);
const canReject = ['intake', 'identity_verification', 'processing'].includes(currentDSR.status);
const canExtend = !['completed', 'rejected', 'cancelled'].includes(currentDSR.status);
document.getElementById('dsr-btn-verify').style.display = canVerify ? 'inline-flex' : 'none';
document.getElementById('dsr-btn-complete').style.display = canComplete ? 'inline-flex' : 'none';
document.getElementById('dsr-btn-reject').style.display = canReject ? 'inline-flex' : 'none';
document.getElementById('dsr-btn-extend').style.display = canExtend ? 'inline-flex' : 'none';
} catch(e) {
alert('Fehler beim Laden: ' + e.message);
}
}
function hideDSRDetail() {
document.getElementById('dsr-detail-view').style.display = 'none';
document.getElementById('dsr-table-container').style.display = 'block';
currentDSR = null;
}
async function verifyDSRIdentity() {
if (!currentDSR) return;
const method = prompt('Verifizierungsmethode (z.B. id_card, passport, video_call, email):', 'email');
if (!method) return;
try {
const res = await fetch(`/api/v1/admin/dsr/${currentDSR.id}/verify-identity`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ method: method })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.error || 'Fehler');
}
alert('Identität wurde verifiziert.');
showDSRDetail(currentDSR.id);
loadDSRStats();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function showDSRExtendDialog() {
if (!currentDSR) return;
const reason = prompt('Begründung für die Fristverlängerung:');
if (!reason) return;
try {
const res = await fetch(`/api/v1/admin/dsr/${currentDSR.id}/extend`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason: reason, days: 60 })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.error || 'Fehler');
}
alert('Frist wurde um 60 Tage verlängert.');
showDSRDetail(currentDSR.id);
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function showDSRCompleteDialog() {
if (!currentDSR) return;
const summary = prompt('Zusammenfassung des Ergebnisses:');
if (!summary) return;
try {
const res = await fetch(`/api/v1/admin/dsr/${currentDSR.id}/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ summary: summary })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.error || 'Fehler');
}
alert('Anfrage wurde abgeschlossen.');
showDSRDetail(currentDSR.id);
loadDSRStats();
loadDSRList();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function showDSRRejectDialog() {
if (!currentDSR) return;
const legalBasis = prompt('Rechtsgrundlage für die Ablehnung (z.B. Art. 17(3)a, Art. 12(5)):');
if (!legalBasis) return;
const reason = prompt('Begründung der Ablehnung:');
if (!reason) return;
try {
const res = await fetch(`/api/v1/admin/dsr/${currentDSR.id}/reject`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason: reason, legal_basis: legalBasis })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.error || 'Fehler');
}
alert('Anfrage wurde abgelehnt.');
showDSRDetail(currentDSR.id);
loadDSRStats();
loadDSRList();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
function showDSRAssignDialog() {
// TODO: Implement user selection dialog
alert('Zuweisung noch nicht implementiert. Verwenden Sie die API direkt.');
}
function loadDSRData() {
loadDSRStats();
loadDSRList();
}
// Load DSR data when tab is clicked
document.querySelector('.admin-tab[data-tab="dsr"]')?.addEventListener('click', loadDSRData);
// ==========================================
// DSMS FUNCTIONS
// ==========================================
const DSMS_GATEWAY_URL = 'http://localhost:8082';
let dsmsArchives = [];
function switchDsmsTab(tabName) {
document.querySelectorAll('.dsms-subtab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.dsms-content').forEach(c => c.classList.remove('active'));
document.querySelector(`.dsms-subtab[data-dsms-tab="${tabName}"]`)?.classList.add('active');
document.getElementById(`dsms-${tabName}`)?.classList.add('active');
// Load data for specific tabs
if (tabName === 'settings') {
loadDsmsNodeInfo();
}
}
async function loadDsmsData() {
await Promise.all([
loadDsmsStatus(),
loadDsmsArchives(),
loadDsmsDocumentSelect()
]);
}
async function loadDsmsStatus() {
const container = document.getElementById('dsms-status-cards');
container.innerHTML = '<div class="admin-loading">Lade DSMS Status...</div>';
try {
const [healthRes, nodeRes] = await Promise.all([
fetch(`${DSMS_GATEWAY_URL}/health`).catch(() => null),
fetch(`${DSMS_GATEWAY_URL}/api/v1/node/info`).catch(() => null)
]);
const health = healthRes?.ok ? await healthRes.json() : null;
const nodeInfo = nodeRes?.ok ? await nodeRes.json() : null;
const isOnline = health?.ipfs_connected === true;
const repoSize = nodeInfo?.repo_size ? formatBytes(nodeInfo.repo_size) : '-';
const storageMax = nodeInfo?.storage_max ? formatBytes(nodeInfo.storage_max) : '-';
const numObjects = nodeInfo?.num_objects ?? '-';
container.innerHTML = `
<div class="dsms-status-card">
<h4>Status</h4>
<div class="value ${isOnline ? 'online' : 'offline'}">${isOnline ? 'Online' : 'Offline'}</div>
</div>
<div class="dsms-status-card">
<h4>Speicher verwendet</h4>
<div class="value">${repoSize}</div>
</div>
<div class="dsms-status-card">
<h4>Max. Speicher</h4>
<div class="value">${storageMax}</div>
</div>
<div class="dsms-status-card">
<h4>Objekte</h4>
<div class="value">${numObjects}</div>
</div>
`;
} catch(e) {
container.innerHTML = `
<div class="dsms-status-card" style="grid-column: 1/-1;">
<h4>Status</h4>
<div class="value offline">Nicht erreichbar</div>
<p style="font-size: 12px; color: var(--bp-text-muted); margin-top: 8px;">
DSMS Gateway ist nicht verfügbar. Stellen Sie sicher, dass die Container laufen.
</p>
</div>
`;
}
}
async function loadDsmsArchives() {
const container = document.getElementById('dsms-archives-table');
container.innerHTML = '<div class="admin-loading">Lade archivierte Dokumente...</div>';
try {
const token = localStorage.getItem('bp_token') || '';
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/documents`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) {
throw new Error('Fehler beim Laden');
}
const data = await res.json();
dsmsArchives = data.documents || [];
if (dsmsArchives.length === 0) {
container.innerHTML = `
<div class="admin-empty">
<p>Keine archivierten Dokumente vorhanden.</p>
<p style="font-size: 12px; color: var(--bp-text-muted);">
Klicken Sie auf "+ Dokument archivieren" um ein Legal Document im DSMS zu speichern.
</p>
</div>
`;
return;
}
container.innerHTML = `
<table class="admin-table">
<thead>
<tr>
<th>CID</th>
<th>Dokument</th>
<th>Version</th>
<th>Archiviert am</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
${dsmsArchives.map(doc => `
<tr>
<td>
<code style="font-size: 11px; background: var(--bp-surface-elevated); padding: 2px 6px; border-radius: 4px;">
${doc.cid.substring(0, 12)}...
</code>
</td>
<td>${doc.metadata?.document_id || doc.filename || '-'}</td>
<td>${doc.metadata?.version || '-'}</td>
<td>${doc.metadata?.created_at ? new Date(doc.metadata.created_at).toLocaleString('de-DE') : '-'}</td>
<td>
<button class="btn btn-ghost btn-sm" onclick="verifyDsmsDocumentByCid('${doc.cid}')" title="Verifizieren">
</button>
<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${doc.cid}')" title="CID kopieren">
📋
</button>
<a href="${DSMS_GATEWAY_URL.replace('8082', '8085')}/ipfs/${doc.cid}" target="_blank" class="btn btn-ghost btn-sm" title="Im Gateway öffnen">
</a>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
} catch(e) {
container.innerHTML = `<div class="admin-empty">Fehler: ${e.message}</div>`;
}
}
async function loadDsmsDocumentSelect() {
const select = document.getElementById('dsms-archive-doc-select');
if (!select) return;
try {
const res = await fetch('/api/consent/admin/documents');
if (!res.ok) return;
const data = await res.json();
const docs = data.documents || [];
select.innerHTML = '<option value="">-- Dokument wählen --</option>' +
docs.map(d => `<option value="${d.id}">${d.name} (${d.type})</option>`).join('');
} catch(e) {
console.error('Error loading documents:', e);
}
}
async function loadDsmsVersionSelect() {
const docSelect = document.getElementById('dsms-archive-doc-select');
const versionSelect = document.getElementById('dsms-archive-version-select');
const docId = docSelect?.value;
if (!docId) {
versionSelect.innerHTML = '<option value="">-- Erst Dokument wählen --</option>';
versionSelect.disabled = true;
return;
}
versionSelect.disabled = false;
versionSelect.innerHTML = '<option value="">Lade Versionen...</option>';
try {
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
if (!res.ok) throw new Error('Fehler');
const data = await res.json();
const versions = data.versions || [];
if (versions.length === 0) {
versionSelect.innerHTML = '<option value="">Keine Versionen vorhanden</option>';
return;
}
versionSelect.innerHTML = '<option value="">-- Version wählen --</option>' +
versions.map(v => `<option value="${v.id}" data-content="${encodeURIComponent(v.content || '')}" data-version="${v.version}">
${v.version} (${v.language}) - ${v.status}
</option>`).join('');
} catch(e) {
versionSelect.innerHTML = '<option value="">Fehler beim Laden</option>';
}
}
// Attach event listener for doc select change
document.getElementById('dsms-archive-doc-select')?.addEventListener('change', loadDsmsVersionSelect);
function showArchiveForm() {
document.getElementById('dsms-archive-form').style.display = 'block';
loadDsmsDocumentSelect();
}
function hideArchiveForm() {
document.getElementById('dsms-archive-form').style.display = 'none';
}
async function archiveDocumentToDsms() {
const docSelect = document.getElementById('dsms-archive-doc-select');
const versionSelect = document.getElementById('dsms-archive-version-select');
const selectedOption = versionSelect.options[versionSelect.selectedIndex];
if (!docSelect.value || !versionSelect.value) {
alert('Bitte Dokument und Version auswählen');
return;
}
const content = decodeURIComponent(selectedOption.dataset.content || '');
const version = selectedOption.dataset.version;
const docId = docSelect.value;
if (!content) {
alert('Die ausgewählte Version hat keinen Inhalt');
return;
}
try {
const token = localStorage.getItem('bp_token') || '';
const params = new URLSearchParams({
document_id: docId,
version: version,
content: content,
language: 'de'
});
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/legal-documents/archive?${params}`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail || 'Archivierung fehlgeschlagen');
}
const result = await res.json();
alert(`Dokument erfolgreich archiviert!\\n\\nCID: ${result.cid}\\nChecksum: ${result.checksum}`);
hideArchiveForm();
loadDsmsArchives();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function verifyDsmsDocument() {
const cidInput = document.getElementById('dsms-verify-cid');
const resultDiv = document.getElementById('dsms-verify-result');
const cid = cidInput?.value?.trim();
if (!cid) {
alert('Bitte CID eingeben');
return;
}
await verifyDsmsDocumentByCid(cid);
}
async function verifyDsmsDocumentByCid(cid) {
const resultDiv = document.getElementById('dsms-verify-result');
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<div class="admin-loading">Verifiziere...</div>';
// Switch to verify tab
switchDsmsTab('verify');
document.getElementById('dsms-verify-cid').value = cid;
try {
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/verify/${cid}`);
const data = await res.json();
if (data.exists && data.integrity_valid) {
resultDiv.innerHTML = `
<div class="dsms-verify-success">
<h4 style="margin: 0 0 12px 0;">✓ Dokument verifiziert</h4>
<div style="display: grid; gap: 8px; font-size: 13px;">
<div><strong>CID:</strong> <code>${cid}</code></div>
<div><strong>Integrität:</strong> Gültig</div>
<div><strong>Typ:</strong> ${data.metadata?.document_type || '-'}</div>
<div><strong>Dokument-ID:</strong> ${data.metadata?.document_id || '-'}</div>
<div><strong>Version:</strong> ${data.metadata?.version || '-'}</div>
<div><strong>Erstellt:</strong> ${data.metadata?.created_at ? new Date(data.metadata.created_at).toLocaleString('de-DE') : '-'}</div>
<div><strong>Checksum:</strong> <code style="font-size: 10px;">${data.stored_checksum || '-'}</code></div>
</div>
</div>
`;
} else if (data.exists && !data.integrity_valid) {
resultDiv.innerHTML = `
<div class="dsms-verify-error">
<h4 style="margin: 0 0 12px 0;">⚠ Integritätsfehler</h4>
<p>Das Dokument existiert, aber die Prüfsumme stimmt nicht überein.</p>
<p style="font-size: 12px;">Gespeichert: ${data.stored_checksum}</p>
<p style="font-size: 12px;">Berechnet: ${data.calculated_checksum}</p>
</div>
`;
} else {
resultDiv.innerHTML = `
<div class="dsms-verify-error">
<h4 style="margin: 0 0 12px 0;">✗ Nicht gefunden</h4>
<p>Kein Dokument mit diesem CID gefunden.</p>
${data.error ? `<p style="font-size: 12px;">${data.error}</p>` : ''}
</div>
`;
}
} catch(e) {
resultDiv.innerHTML = `
<div class="dsms-verify-error">
<h4 style="margin: 0 0 12px 0;">Fehler</h4>
<p>${e.message}</p>
</div>
`;
}
}
async function loadDsmsNodeInfo() {
const container = document.getElementById('dsms-node-info');
container.innerHTML = '<div class="admin-loading">Lade Node-Info...</div>';
try {
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/node/info`);
if (!res.ok) throw new Error('Nicht erreichbar');
const info = await res.json();
container.innerHTML = `
<div style="display: grid; gap: 12px; font-size: 13px;">
<div><strong>Node ID:</strong> <code style="font-size: 11px;">${info.node_id || '-'}</code></div>
<div><strong>Agent:</strong> ${info.agent_version || '-'}</div>
<div><strong>Repo-Größe:</strong> ${info.repo_size ? formatBytes(info.repo_size) : '-'}</div>
<div><strong>Max. Speicher:</strong> ${info.storage_max ? formatBytes(info.storage_max) : '-'}</div>
<div><strong>Objekte:</strong> ${info.num_objects ?? '-'}</div>
<div>
<strong>Adressen:</strong>
<ul style="margin: 4px 0 0 0; padding-left: 20px; font-size: 11px; color: var(--bp-text-muted);">
${(info.addresses || []).map(a => `<li><code>${a}</code></li>`).join('')}
</ul>
</div>
</div>
`;
} catch(e) {
container.innerHTML = `<div class="admin-empty">DSMS nicht erreichbar: ${e.message}</div>`;
}
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
// Optional: Show toast
}).catch(err => {
console.error('Copy failed:', err);
});
}
// ==========================================
// DSMS WEBUI FUNCTIONS
// ==========================================
function openDsmsWebUI() {
document.getElementById('dsms-webui-modal').style.display = 'flex';
loadDsmsWebUIData();
}
function closeDsmsWebUI() {
document.getElementById('dsms-webui-modal').style.display = 'none';
}
function switchDsmsWebUISection(section) {
// Update nav buttons
document.querySelectorAll('.dsms-webui-nav').forEach(btn => {
btn.classList.toggle('active', btn.dataset.section === section);
});
// Update sections
document.querySelectorAll('.dsms-webui-section').forEach(sec => {
sec.classList.remove('active');
sec.style.display = 'none';
});
const activeSection = document.getElementById('dsms-webui-' + section);
if (activeSection) {
activeSection.classList.add('active');
activeSection.style.display = 'block';
}
// Load section-specific data
if (section === 'peers') loadDsmsPeers();
}
async function loadDsmsWebUIData() {
try {
// Load node info
const infoRes = await fetch(DSMS_GATEWAY_URL + '/api/v1/node/info');
const info = await infoRes.json();
document.getElementById('webui-status').innerHTML = '<span style="color: var(--bp-accent);">Online</span>';
document.getElementById('webui-node-id').textContent = info.node_id || '--';
document.getElementById('webui-protocol').textContent = info.protocol_version || '--';
document.getElementById('webui-agent').textContent = info.agent_version || '--';
document.getElementById('webui-repo-size').textContent = formatBytes(info.repo_size || 0);
document.getElementById('webui-storage-info').textContent = 'Max: ' + formatBytes(info.storage_max || 0);
document.getElementById('webui-num-objects').textContent = (info.num_objects || 0).toLocaleString();
// Addresses
const addresses = info.addresses || [];
document.getElementById('webui-addresses').innerHTML = addresses.length > 0
? addresses.map(a => '<div style="margin-bottom: 4px;">' + a + '</div>').join('')
: '<span style="color: var(--bp-text-muted);">Keine Adressen verfügbar</span>';
// Load pinned count
const token = localStorage.getItem('bp_token') || '';
const docsRes = await fetch(DSMS_GATEWAY_URL + '/api/v1/documents', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (docsRes.ok) {
const docs = await docsRes.json();
document.getElementById('webui-pinned-count').textContent = docs.total || 0;
}
} catch (e) {
console.error('Failed to load WebUI data:', e);
document.getElementById('webui-status').innerHTML = '<span style="color: var(--bp-danger);">Offline</span>';
}
}
async function loadDsmsPeers() {
const container = document.getElementById('webui-peers-list');
try {
// IPFS peers endpoint via proxy would need direct IPFS API access
// For now, show info that private network has no peers
container.innerHTML = `
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; text-align: center;">
<div style="font-size: 48px; margin-bottom: 16px;">&#128274;</div>
<h4 style="margin: 0 0 8px 0;">Privates Netzwerk</h4>
<p style="color: var(--bp-text-muted); margin: 0;">
DSMS läuft als isolierter Node. Keine externen Peers verbunden.
</p>
</div>
`;
} catch (e) {
container.innerHTML = '<div class="dsms-verify-error">Fehler beim Laden der Peers</div>';
}
}
// File upload handlers
function handleDsmsDragOver(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('dsms-upload-zone').classList.add('dragover');
}
function handleDsmsDragLeave(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('dsms-upload-zone').classList.remove('dragover');
}
async function handleDsmsFileDrop(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('dsms-upload-zone').classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
await uploadDsmsFiles(files);
}
}
async function handleDsmsFileSelect(e) {
const files = e.target.files;
if (files.length > 0) {
await uploadDsmsFiles(files);
}
}
async function uploadDsmsFiles(files) {
const token = localStorage.getItem('bp_token') || '';
const progressDiv = document.getElementById('dsms-upload-progress');
const statusDiv = document.getElementById('dsms-upload-status');
const barDiv = document.getElementById('dsms-upload-bar');
const resultsDiv = document.getElementById('dsms-upload-results');
progressDiv.style.display = 'block';
resultsDiv.innerHTML = '';
const results = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
statusDiv.textContent = 'Lade hoch: ' + file.name + ' (' + (i+1) + '/' + files.length + ')';
barDiv.style.width = ((i / files.length) * 100) + '%';
try {
const formData = new FormData();
formData.append('file', file);
formData.append('document_type', 'legal_document');
const res = await fetch(DSMS_GATEWAY_URL + '/api/v1/documents', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token },
body: formData
});
if (res.ok) {
const data = await res.json();
results.push({ file: file.name, cid: data.cid, success: true });
} else {
results.push({ file: file.name, error: 'Upload fehlgeschlagen', success: false });
}
} catch (e) {
results.push({ file: file.name, error: e.message, success: false });
}
}
barDiv.style.width = '100%';
statusDiv.textContent = 'Upload abgeschlossen!';
// Show results
resultsDiv.innerHTML = '<h4 style="margin: 0 0 12px 0;">Ergebnisse</h4>' +
results.map(r => `
<div class="dsms-webui-file-item">
<div>
<div style="font-weight: 500;">${r.file}</div>
${r.success
? '<div class="cid" style="color: var(--bp-accent);">CID: ' + r.cid + '</div>'
: '<div style="color: var(--bp-danger);">' + r.error + '</div>'
}
</div>
${r.success ? `<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${r.cid}')">Kopieren</button>` : ''}
</div>
`).join('');
setTimeout(() => {
progressDiv.style.display = 'none';
barDiv.style.width = '0%';
}, 2000);
}
async function exploreDsmsCid() {
const cid = document.getElementById('webui-explore-cid').value.trim();
if (!cid) return;
const resultDiv = document.getElementById('dsms-explore-result');
const contentDiv = document.getElementById('dsms-explore-content');
resultDiv.style.display = 'block';
contentDiv.innerHTML = '<div class="admin-loading">Lade...</div>';
try {
const res = await fetch(DSMS_GATEWAY_URL + '/api/v1/verify/' + cid);
const data = await res.json();
if (data.exists) {
contentDiv.innerHTML = `
<div style="margin-bottom: 16px;">
<span style="font-size: 24px; ${data.integrity_valid ? 'color: var(--bp-accent);' : 'color: var(--bp-danger);'}">
${data.integrity_valid ? '&#10003;' : '&#10007;'}
</span>
<span style="font-weight: 600; margin-left: 8px;">
${data.integrity_valid ? 'Dokument verifiziert' : 'Integritätsfehler'}
</span>
</div>
<table style="width: 100%; font-size: 13px;">
<tr>
<td style="padding: 8px 0; color: var(--bp-text-muted); width: 150px;">CID</td>
<td style="padding: 8px 0; font-family: monospace; word-break: break-all;">${cid}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: var(--bp-text-muted);">Typ</td>
<td style="padding: 8px 0;">${data.metadata?.document_type || '--'}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: var(--bp-text-muted);">Erstellt</td>
<td style="padding: 8px 0;">${data.metadata?.created_at ? new Date(data.metadata.created_at).toLocaleString('de-DE') : '--'}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: var(--bp-text-muted);">Checksum</td>
<td style="padding: 8px 0; font-family: monospace; font-size: 11px; word-break: break-all;">${data.stored_checksum || '--'}</td>
</tr>
</table>
<div style="margin-top: 16px;">
<a href="http://localhost:8085/ipfs/${cid}" target="_blank" class="btn btn-ghost btn-sm">Im Gateway öffnen</a>
</div>
`;
} else {
contentDiv.innerHTML = `
<div class="dsms-verify-error">
<strong>Nicht gefunden</strong><br>
CID existiert nicht im DSMS: ${cid}
</div>
`;
}
} catch (e) {
contentDiv.innerHTML = `
<div class="dsms-verify-error">
<strong>Fehler</strong><br>
${e.message}
</div>
`;
}
}
async function runDsmsGarbageCollection() {
if (!confirm('Garbage Collection ausführen? Dies entfernt nicht gepinnte Objekte.')) return;
try {
// Note: Direct GC requires IPFS API access - show info for now
alert('Garbage Collection wird im Hintergrund ausgeführt. Dies kann einige Minuten dauern.');
} catch (e) {
alert('Fehler: ' + e.message);
}
}
// Close modal on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeDsmsWebUI();
}
});
// Close modal on backdrop click
document.getElementById('dsms-webui-modal')?.addEventListener('click', (e) => {
if (e.target.id === 'dsms-webui-modal') {
closeDsmsWebUI();
}
});
// Load DSMS data when tab is clicked
document.querySelector('.admin-tab[data-tab="dsms"]')?.addEventListener('click', loadDsmsData);
// ==========================================
// E-MAIL TEMPLATE MANAGEMENT
// ==========================================
let emailTemplates = [];
let emailTemplateVersions = [];
let currentEmailTemplateId = null;
let currentEmailVersionId = null;
// E-Mail-Template-Typen mit deutschen Namen
const emailTypeNames = {
'welcome': 'Willkommens-E-Mail',
'email_verification': 'E-Mail-Verifizierung',
'password_reset': 'Passwort zurücksetzen',
'password_changed': 'Passwort geändert',
'2fa_enabled': '2FA aktiviert',
'2fa_disabled': '2FA deaktiviert',
'new_device_login': 'Neues Gerät Login',
'suspicious_activity': 'Verdächtige Aktivität',
'account_locked': 'Account gesperrt',
'account_unlocked': 'Account entsperrt',
'deletion_requested': 'Löschung angefordert',
'deletion_confirmed': 'Löschung bestätigt',
'data_export_ready': 'Datenexport bereit',
'email_changed': 'E-Mail geändert',
'new_version_published': 'Neue Version veröffentlicht',
'consent_reminder': 'Consent Erinnerung',
'consent_deadline_warning': 'Consent Frist Warnung',
'account_suspended': 'Account suspendiert'
};
// Load E-Mail Templates when tab is clicked
document.querySelector('.admin-tab[data-tab="emails"]')?.addEventListener('click', loadEmailTemplates);
async function loadEmailTemplates() {
try {
const res = await fetch('/api/consent/admin/email-templates');
if (!res.ok) throw new Error('Fehler beim Laden der Templates');
const data = await res.json();
emailTemplates = data.templates || [];
populateEmailTemplateSelect();
} catch (e) {
console.error('Error loading email templates:', e);
showToast('Fehler beim Laden der E-Mail-Templates', 'error');
}
}
function populateEmailTemplateSelect() {
const select = document.getElementById('email-template-select');
select.innerHTML = '<option value="">-- E-Mail-Vorlage auswählen --</option>';
emailTemplates.forEach(item => {
const template = item.template; // API liefert verschachtelte Struktur
const opt = document.createElement('option');
opt.value = template.id;
opt.textContent = emailTypeNames[template.type] || template.name;
select.appendChild(opt);
});
}
async function loadEmailTemplateVersions() {
const select = document.getElementById('email-template-select');
const templateId = select.value;
const newVersionBtn = document.getElementById('btn-new-email-version');
const infoCard = document.getElementById('email-template-info');
const container = document.getElementById('email-version-table-container');
if (!templateId) {
newVersionBtn.disabled = true;
infoCard.style.display = 'none';
container.innerHTML = '<div class="admin-empty">Wählen Sie eine E-Mail-Vorlage aus, um deren Versionen anzuzeigen.</div>';
currentEmailTemplateId = null;
return;
}
currentEmailTemplateId = templateId;
newVersionBtn.disabled = false;
// Finde das Template (API liefert verschachtelte Struktur)
const templateItem = emailTemplates.find(t => t.template.id === templateId);
const template = templateItem?.template;
if (template) {
infoCard.style.display = 'block';
document.getElementById('email-template-name').textContent = emailTypeNames[template.type] || template.name;
document.getElementById('email-template-description').textContent = template.description || 'Keine Beschreibung';
document.getElementById('email-template-type-badge').textContent = template.type;
// Variablen anzeigen (wird aus dem Default-Inhalt ermittelt)
try {
const defaultRes = await fetch(`/api/consent/admin/email-templates/default/${template.type}`);
if (defaultRes.ok) {
const defaultData = await defaultRes.json();
const variables = extractVariables(defaultData.body_html || '');
document.getElementById('email-template-variables').textContent = variables.join(', ') || 'Keine';
}
} catch (e) {
document.getElementById('email-template-variables').textContent = '-';
}
}
// Lade Versionen
container.innerHTML = '<div class="admin-loading">Lade Versionen...</div>';
try {
const res = await fetch(`/api/consent/admin/email-templates/${templateId}/versions`);
if (!res.ok) throw new Error('Fehler beim Laden');
const data = await res.json();
emailTemplateVersions = data.versions || [];
renderEmailVersionsTable();
} catch (e) {
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Versionen.</div>';
}
}
function extractVariables(content) {
const matches = content.match(/\\{\\{([^}]+)\\}\\}/g) || [];
return [...new Set(matches.map(m => m.replace(/[{}]/g, '')))];
}
function renderEmailVersionsTable() {
const container = document.getElementById('email-version-table-container');
if (emailTemplateVersions.length === 0) {
container.innerHTML = '<div class="admin-empty">Keine Versionen vorhanden. Erstellen Sie eine neue Version.</div>';
return;
}
const statusColors = {
'draft': 'draft',
'review': 'review',
'approved': 'approved',
'published': 'published',
'archived': 'archived'
};
const statusNames = {
'draft': 'Entwurf',
'review': 'In Prüfung',
'approved': 'Genehmigt',
'published': 'Veröffentlicht',
'archived': 'Archiviert'
};
container.innerHTML = `
<table class="admin-table">
<thead>
<tr>
<th>Version</th>
<th>Sprache</th>
<th>Betreff</th>
<th>Status</th>
<th>Aktualisiert</th>
<th style="text-align: right;">Aktionen</th>
</tr>
</thead>
<tbody>
${emailTemplateVersions.map(v => `
<tr>
<td><strong>${v.version}</strong></td>
<td>${v.language === 'de' ? '🇩🇪 DE' : '🇬🇧 EN'}</td>
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${v.subject}</td>
<td><span class="admin-badge badge-${statusColors[v.status]}">${statusNames[v.status] || v.status}</span></td>
<td>${new Date(v.updated_at).toLocaleDateString('de-DE')}</td>
<td style="text-align: right;">
<button class="btn btn-ghost btn-xs" onclick="previewEmailVersionById('${v.id}')" title="Vorschau">👁️</button>
${v.status === 'draft' ? `
<button class="btn btn-ghost btn-xs" onclick="editEmailVersion('${v.id}')" title="Bearbeiten">✏️</button>
<button class="btn btn-ghost btn-xs" onclick="submitEmailForReview('${v.id}')" title="Zur Prüfung">📤</button>
<button class="btn btn-ghost btn-xs" onclick="deleteEmailVersion('${v.id}')" title="Löschen">🗑️</button>
` : ''}
${v.status === 'review' ? `
<button class="btn btn-ghost btn-xs" onclick="showEmailApprovalDialogFor('${v.id}')" title="Genehmigen">✅</button>
<button class="btn btn-ghost btn-xs" onclick="rejectEmailVersion('${v.id}')" title="Ablehnen">❌</button>
` : ''}
${v.status === 'approved' ? `
<button class="btn btn-primary btn-xs" onclick="publishEmailVersion('${v.id}')" title="Veröffentlichen">🚀</button>
` : ''}
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
function showEmailVersionForm() {
document.getElementById('email-version-form').style.display = 'block';
document.getElementById('email-version-form-title').textContent = 'Neue E-Mail-Version erstellen';
document.getElementById('email-version-id').value = '';
document.getElementById('email-version-number').value = '';
document.getElementById('email-version-subject').value = '';
document.getElementById('email-version-editor').innerHTML = '';
document.getElementById('email-version-text').value = '';
// Lade Default-Inhalt (API liefert verschachtelte Struktur)
const templateItem = emailTemplates.find(t => t.template.id === currentEmailTemplateId);
if (templateItem?.template) {
loadDefaultEmailContent(templateItem.template.type);
}
}
async function loadDefaultEmailContent(templateType) {
try {
const res = await fetch(`/api/consent/admin/email-templates/default/${templateType}`);
if (res.ok) {
const data = await res.json();
document.getElementById('email-version-subject').value = data.subject || '';
document.getElementById('email-version-editor').innerHTML = data.body_html || '';
document.getElementById('email-version-text').value = data.body_text || '';
}
} catch (e) {
console.error('Error loading default content:', e);
}
}
function hideEmailVersionForm() {
document.getElementById('email-version-form').style.display = 'none';
}
async function saveEmailVersion() {
const versionId = document.getElementById('email-version-id').value;
const templateId = currentEmailTemplateId;
const version = document.getElementById('email-version-number').value.trim();
const language = document.getElementById('email-version-lang').value;
const subject = document.getElementById('email-version-subject').value.trim();
const bodyHtml = document.getElementById('email-version-editor').innerHTML;
const bodyText = document.getElementById('email-version-text').value.trim();
if (!version || !subject || !bodyHtml) {
showToast('Bitte füllen Sie alle Pflichtfelder aus', 'error');
return;
}
const data = {
template_id: templateId,
version: version,
language: language,
subject: subject,
body_html: bodyHtml,
body_text: bodyText || stripHtml(bodyHtml)
};
try {
let res;
if (versionId) {
// Update existing version
res = await fetch(`/api/consent/admin/email-template-versions/${versionId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
} else {
// Create new version
res = await fetch('/api/consent/admin/email-template-versions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
if (!res.ok) {
const error = await res.json();
throw new Error(error.detail || 'Fehler beim Speichern');
}
showToast('E-Mail-Version gespeichert!', 'success');
hideEmailVersionForm();
loadEmailTemplateVersions();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
function stripHtml(html) {
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
}
async function editEmailVersion(versionId) {
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}`);
if (!res.ok) throw new Error('Version nicht gefunden');
const version = await res.json();
document.getElementById('email-version-form').style.display = 'block';
document.getElementById('email-version-form-title').textContent = 'E-Mail-Version bearbeiten';
document.getElementById('email-version-id').value = versionId;
document.getElementById('email-version-number').value = version.version;
document.getElementById('email-version-lang').value = version.language;
document.getElementById('email-version-subject').value = version.subject;
document.getElementById('email-version-editor').innerHTML = version.body_html;
document.getElementById('email-version-text').value = version.body_text || '';
} catch (e) {
showToast('Fehler beim Laden der Version', 'error');
}
}
async function deleteEmailVersion(versionId) {
if (!confirm('Möchten Sie diese Version wirklich löschen?')) return;
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}`, {
method: 'DELETE'
});
if (!res.ok) throw new Error('Fehler beim Löschen');
showToast('Version gelöscht', 'success');
loadEmailTemplateVersions();
} catch (e) {
showToast('Fehler beim Löschen', 'error');
}
}
async function submitEmailForReview(versionId) {
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/submit`, {
method: 'POST'
});
if (!res.ok) throw new Error('Fehler');
showToast('Zur Prüfung eingereicht', 'success');
loadEmailTemplateVersions();
} catch (e) {
showToast('Fehler beim Einreichen', 'error');
}
}
function showEmailApprovalDialogFor(versionId) {
currentEmailVersionId = versionId;
document.getElementById('email-approval-dialog').style.display = 'flex';
document.getElementById('email-approval-comment').value = '';
}
function hideEmailApprovalDialog() {
document.getElementById('email-approval-dialog').style.display = 'none';
currentEmailVersionId = null;
}
async function submitEmailApproval() {
if (!currentEmailVersionId) return;
const comment = document.getElementById('email-approval-comment').value.trim();
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${currentEmailVersionId}/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment: comment })
});
if (!res.ok) throw new Error('Fehler');
showToast('Version genehmigt', 'success');
hideEmailApprovalDialog();
loadEmailTemplateVersions();
} catch (e) {
showToast('Fehler bei der Genehmigung', 'error');
}
}
async function rejectEmailVersion(versionId) {
const reason = prompt('Ablehnungsgrund:');
if (!reason) return;
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/reject`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason: reason })
});
if (!res.ok) throw new Error('Fehler');
showToast('Version abgelehnt', 'success');
loadEmailTemplateVersions();
} catch (e) {
showToast('Fehler bei der Ablehnung', 'error');
}
}
async function publishEmailVersion(versionId) {
if (!confirm('Möchten Sie diese Version veröffentlichen? Die vorherige Version wird archiviert.')) return;
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/publish`, {
method: 'POST'
});
if (!res.ok) throw new Error('Fehler');
showToast('Version veröffentlicht!', 'success');
loadEmailTemplateVersions();
} catch (e) {
showToast('Fehler beim Veröffentlichen', 'error');
}
}
async function previewEmailVersionById(versionId) {
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/preview`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
if (!res.ok) throw new Error('Fehler');
const data = await res.json();
document.getElementById('email-preview-subject').textContent = data.subject;
document.getElementById('email-preview-content').innerHTML = data.body_html;
document.getElementById('email-preview-dialog').style.display = 'flex';
currentEmailVersionId = versionId;
} catch (e) {
showToast('Fehler bei der Vorschau', 'error');
}
}
function previewEmailVersion() {
const subject = document.getElementById('email-version-subject').value;
const bodyHtml = document.getElementById('email-version-editor').innerHTML;
document.getElementById('email-preview-subject').textContent = subject;
document.getElementById('email-preview-content').innerHTML = bodyHtml;
document.getElementById('email-preview-dialog').style.display = 'flex';
}
function hideEmailPreview() {
document.getElementById('email-preview-dialog').style.display = 'none';
}
async function sendTestEmail() {
const email = document.getElementById('email-test-address').value.trim();
if (!email) {
showToast('Bitte geben Sie eine E-Mail-Adresse ein', 'error');
return;
}
if (!currentEmailVersionId) {
showToast('Keine Version ausgewählt', 'error');
return;
}
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${currentEmailVersionId}/send-test`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email })
});
if (!res.ok) throw new Error('Fehler');
showToast('Test-E-Mail gesendet!', 'success');
} catch (e) {
showToast('Fehler beim Senden der Test-E-Mail', 'error');
}
}
async function initializeEmailTemplates() {
if (!confirm('Möchten Sie alle Standard-E-Mail-Templates initialisieren?')) return;
try {
const res = await fetch('/api/consent/admin/email-templates/initialize', {
method: 'POST'
});
if (!res.ok) throw new Error('Fehler');
showToast('Templates initialisiert!', 'success');
loadEmailTemplates();
} catch (e) {
showToast('Fehler bei der Initialisierung', 'error');
}
}
// E-Mail Editor Helpers
function formatEmailDoc(command) {
document.execCommand(command, false, null);
document.getElementById('email-version-editor').focus();
}
function formatEmailBlock(tag) {
document.execCommand('formatBlock', false, '<' + tag + '>');
document.getElementById('email-version-editor').focus();
}
function insertEmailVariable() {
const variable = prompt('Variablenname eingeben (z.B. user_name, reset_link):');
if (variable) {
document.execCommand('insertText', false, '{{' + variable + '}}');
}
}
function insertEmailLink() {
const url = prompt('Link-URL:');
if (url) {
const text = prompt('Link-Text:', url);
document.execCommand('insertHTML', false, `<a href="${url}" style="color: #5B21B6;">${text}</a>`);
}
}
function insertEmailButton() {
const url = prompt('Button-Link:');
if (url) {
const text = prompt('Button-Text:', 'Klicken');
const buttonHtml = `<table cellpadding="0" cellspacing="0" style="margin: 16px 0;"><tr><td style="background: #5B21B6; border-radius: 6px; padding: 12px 24px;"><a href="${url}" style="color: white; text-decoration: none; font-weight: 600;">${text}</a></td></tr></table>`;
document.execCommand('insertHTML', false, buttonHtml);
}
}
// ==========================================
// INITIALIZATION - DOMContentLoaded
// ==========================================
document.addEventListener('DOMContentLoaded', function() {
// Theme Toggle
initThemeToggle();
// Language initialization
if (typeof initLanguage === 'function') {
initLanguage();
}
// Vast Control
if (typeof initVastControl === 'function') {
initVastControl();
}
// Legal Modal Close Button
const legalCloseBtn = document.querySelector('.legal-modal-close');
if (legalCloseBtn) {
legalCloseBtn.addEventListener('click', function() {
document.getElementById('legal-modal').classList.remove('active');
});
}
// Auth Modal Close Button
const authCloseBtn = document.querySelector('.auth-modal-close');
if (authCloseBtn) {
authCloseBtn.addEventListener('click', function() {
document.getElementById('auth-modal').classList.remove('active');
});
}
// Admin Modal Close Button
const adminCloseBtn = document.querySelector('.admin-modal-close');
if (adminCloseBtn) {
adminCloseBtn.addEventListener('click', function() {
document.getElementById('admin-modal').classList.remove('active');
});
}
// Legal Button (Footer)
const legalBtn = document.querySelector('[onclick*="showLegalModal"]');
if (legalBtn) {
legalBtn.removeAttribute('onclick');
legalBtn.addEventListener('click', function() {
document.getElementById('legal-modal').classList.add('active');
});
}
// Consent Button (Footer)
const consentBtn = document.querySelector('[onclick*="showConsentModal"]');
if (consentBtn) {
consentBtn.removeAttribute('onclick');
consentBtn.addEventListener('click', function() {
document.getElementById('legal-modal').classList.add('active');
// Switch to consent tab
document.querySelectorAll('.legal-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.legal-content').forEach(c => c.classList.remove('active'));
const consentTab = document.querySelector('.legal-tab[data-tab="consent"]');
const consentContent = document.getElementById('legal-content-consent');
if (consentTab) consentTab.classList.add('active');
if (consentContent) consentContent.classList.add('active');
});
}
// Legal Tabs
document.querySelectorAll('.legal-tab').forEach(tab => {
tab.addEventListener('click', function() {
const tabName = this.dataset.tab;
document.querySelectorAll('.legal-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.legal-content').forEach(c => c.classList.remove('active'));
this.classList.add('active');
const content = document.getElementById('legal-content-' + tabName);
if (content) content.classList.add('active');
});
});
// Auth Tabs
document.querySelectorAll('.auth-tab').forEach(tab => {
tab.addEventListener('click', function() {
const tabName = this.dataset.tab;
document.querySelectorAll('.auth-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.auth-form').forEach(f => f.classList.remove('active'));
this.classList.add('active');
const form = document.getElementById('auth-form-' + tabName);
if (form) form.classList.add('active');
});
});
// Admin Tabs
document.querySelectorAll('.admin-tab').forEach(tab => {
tab.addEventListener('click', function() {
const tabName = this.dataset.tab;
document.querySelectorAll('.admin-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.admin-content').forEach(c => c.classList.remove('active'));
this.classList.add('active');
const content = document.getElementById('admin-content-' + tabName);
if (content) content.classList.add('active');
});
});
// Login Button (TopBar)
const loginBtn = document.getElementById('btn-login');
if (loginBtn) {
loginBtn.addEventListener('click', function() {
document.getElementById('auth-modal').classList.add('active');
});
}
// Admin Button (TopBar)
const adminBtn = document.getElementById('btn-admin');
if (adminBtn) {
adminBtn.addEventListener('click', function() {
document.getElementById('admin-modal').classList.add('active');
});
}
// Legal Button (TopBar)
const legalTopBtn = document.getElementById('btn-legal');
if (legalTopBtn) {
legalTopBtn.addEventListener('click', function() {
document.getElementById('legal-modal').classList.add('active');
});
}
// Modal Close Buttons (by specific IDs)
document.getElementById('legal-modal-close')?.addEventListener('click', function() {
document.getElementById('legal-modal').classList.remove('active');
});
document.getElementById('auth-modal-close')?.addEventListener('click', function() {
document.getElementById('auth-modal').classList.remove('active');
});
document.getElementById('admin-modal-close')?.addEventListener('click', function() {
document.getElementById('admin-modal').classList.remove('active');
});
document.getElementById('imprint-modal-close')?.addEventListener('click', function() {
document.getElementById('imprint-modal').classList.remove('active');
});
// Language selector
const langSelect = document.getElementById('language-select');
if (langSelect && typeof setLanguage === 'function') {
langSelect.addEventListener('change', function() {
setLanguage(this.value);
});
}
console.log('BreakPilot Studio initialized');
});
// ==========================================
// Communication Panel Functions (Matrix + Jitsi)
// ==========================================
// API Base URL for communication endpoints
const COMM_API_BASE = '/consent/api/v1/communication';
const JITSI_BASE_URL = 'http://localhost:8443';
// Current state
let currentRoom = null;
let jitsiApi = null;
// Show Messenger Panel (Matrix)
function showMessengerPanel() {
console.log('showMessengerPanel called');
hideAllPanels();
hideStudioSubMenu();
const messengerPanel = document.getElementById('panel-messenger');
if (messengerPanel) {
messengerPanel.style.display = 'flex';
console.log('Messenger panel shown');
} else {
console.error('panel-messenger not found');
}
updateSidebarActive('sidebar-messenger');
// Check service status
checkCommunicationStatus();
}
// Show Video Panel (Jitsi)
function showVideoPanel() {
console.log('showVideoPanel called');
hideAllPanels();
hideStudioSubMenu();
const videoPanel = document.getElementById('panel-video');
if (videoPanel) {
videoPanel.style.display = 'flex';
console.log('Video panel shown');
} else {
console.error('panel-video not found');
}
updateSidebarActive('sidebar-video');
}
// Legacy alias for backward compatibility
function showCommunicationPanel() {
showMessengerPanel();
}
// Show Studio Panel (original) - zeigt Arbeitsblatt-Tab als Standard
function showStudioPanel() {
console.log('showStudioPanel called');
hideAllPanels();
// Zeige Sub-Navigation
const subMenu = document.getElementById('studio-sub-menu');
if (subMenu) {
subMenu.style.display = 'flex';
}
// Zeige Arbeitsblätter als Standard-Tab
showWorksheetTab();
updateSidebarActive('sidebar-studio');
}
// Tab: Arbeitsblätter anzeigen
function showWorksheetTab() {
console.log('showWorksheetTab called');
// Verstecke beide Studio-Panels
const panelCompare = document.getElementById('panel-compare');
const panelTiles = document.getElementById('panel-tiles');
if (panelCompare) panelCompare.style.display = 'flex';
if (panelTiles) panelTiles.style.display = 'none';
// Update Sub-Navigation active state
updateSubNavActive('sub-worksheets');
}
// Tab: Lernkacheln anzeigen
function showTilesTab() {
console.log('showTilesTab called');
// Verstecke beide Studio-Panels
const panelCompare = document.getElementById('panel-compare');
const panelTiles = document.getElementById('panel-tiles');
if (panelCompare) panelCompare.style.display = 'none';
if (panelTiles) panelTiles.style.display = 'flex';
// Update Sub-Navigation active state
updateSubNavActive('sub-tiles');
}
// Helper: Update Sub-Navigation active state
function updateSubNavActive(activeSubId) {
document.querySelectorAll('.sidebar-sub-item').forEach(item => {
item.classList.remove('active');
});
const activeItem = document.getElementById(activeSubId);
if (activeItem) {
activeItem.classList.add('active');
}
}
// Helper: Hide Studio Sub-Menu (when switching to other panels)
function hideStudioSubMenu() {
const subMenu = document.getElementById('studio-sub-menu');
if (subMenu) {
subMenu.style.display = 'none';
}
}
// Show Correction Panel (Klausur-Korrektur)
function showCorrectionPanel() {
console.log('showCorrectionPanel called');
hideAllPanels();
hideStudioSubMenu();
const correctionPanel = document.getElementById('panel-correction');
if (correctionPanel) {
correctionPanel.style.display = 'flex';
console.log('Correction panel shown');
} else {
console.error('panel-correction not found');
}
updateSidebarActive('sidebar-correction');
}
// Show Letters Panel (Elternbriefe)
function showLettersPanel() {
console.log('showLettersPanel called');
hideAllPanels();
hideStudioSubMenu();
const lettersPanel = document.getElementById('panel-letters');
if (lettersPanel) {
lettersPanel.style.display = 'flex';
console.log('Letters panel shown');
} else {
console.error('panel-letters not found');
}
updateSidebarActive('sidebar-letters');
}
// ========================================
// School Service Panel Functions
// ========================================
// School Service API Base URL
const SCHOOL_SERVICE_URL = '/api/school';
// Show Exams Panel (Klausuren & Tests)
function showExamsPanel() {
console.log('showExamsPanel called');
hideAllPanels();
hideStudioSubMenu();
const panel = document.getElementById('panel-exams');
if (panel) {
panel.style.display = 'flex';
loadClassesForSelect('exams-class-select');
loadSubjectsForSelect('exams-subject-select');
loadExams();
}
updateSidebarActive('sidebar-exams');
}
// Show Grades Panel (Notenspiegel)
function showGradesPanel() {
console.log('showGradesPanel called');
hideAllPanels();
hideStudioSubMenu();
const panel = document.getElementById('panel-grades');
if (panel) {
panel.style.display = 'flex';
loadClassesForSelect('grades-class-select');
}
updateSidebarActive('sidebar-grades');
}
// Show Gradebook Panel (Klassenbuch)
function showGradebookPanel() {
console.log('showGradebookPanel called');
hideAllPanels();
hideStudioSubMenu();
const panel = document.getElementById('panel-gradebook');
if (panel) {
panel.style.display = 'flex';
loadClassesForSelect('gradebook-class-select');
document.getElementById('gradebook-date').valueAsDate = new Date();
}
updateSidebarActive('sidebar-gradebook');
}
// Show Certificates Panel (Zeugnisse)
function showCertificatesPanel() {
console.log('showCertificatesPanel called');
hideAllPanels();
hideStudioSubMenu();
const panel = document.getElementById('panel-certificates');
if (panel) {
panel.style.display = 'flex';
loadClassesForSelect('cert-class-select');
loadCertificateTemplates();
}
updateSidebarActive('sidebar-certificates');
}
// Show Classes Panel (Klassen & Schüler)
function showClassesPanel() {
console.log('showClassesPanel called');
hideAllPanels();
hideStudioSubMenu();
const panel = document.getElementById('panel-classes');
if (panel) {
panel.style.display = 'flex';
loadSchoolYears();
loadClasses();
}
updateSidebarActive('sidebar-classes');
}
// Show Subjects Panel (Fächer)
function showSubjectsPanel() {
console.log('showSubjectsPanel called');
hideAllPanels();
hideStudioSubMenu();
const panel = document.getElementById('panel-subjects');
if (panel) {
panel.style.display = 'flex';
loadSubjects();
}
updateSidebarActive('sidebar-subjects');
}
// Helper: Load classes for dropdown
async function loadClassesForSelect(selectId) {
const select = document.getElementById(selectId);
if (!select) return;
try {
const response = await fetch(`${SCHOOL_SERVICE_URL}/classes`, {
headers: { 'Authorization': `Bearer ${localStorage.getItem('jwt')}` }
});
if (response.ok) {
const classes = await response.json();
select.innerHTML = '<option value="">Klasse wählen...</option>';
(classes || []).forEach(c => {
select.innerHTML += `<option value="${c.id}">${c.name} (Stufe ${c.grade_level})</option>`;
});
}
} catch (e) {
console.log('Could not load classes:', e);
}
}
// Helper: Load subjects for dropdown
async function loadSubjectsForSelect(selectId) {
const select = document.getElementById(selectId);
if (!select) return;
try {
const response = await fetch(`${SCHOOL_SERVICE_URL}/subjects`, {
headers: { 'Authorization': `Bearer ${localStorage.getItem('jwt')}` }
});
if (response.ok) {
const subjects = await response.json();
select.innerHTML = '<option value="">Fach wählen...</option>';
(subjects || []).forEach(s => {
select.innerHTML += `<option value="${s.id}">${s.name}</option>`;
});
}
} catch (e) {
console.log('Could not load subjects:', e);
}
}
// Load school years
async function loadSchoolYears() {
const select = document.getElementById('classes-year-select');
if (!select) return;
try {
const response = await fetch(`${SCHOOL_SERVICE_URL}/years`, {
headers: { 'Authorization': `Bearer ${localStorage.getItem('jwt')}` }
});
if (response.ok) {
const years = await response.json();
select.innerHTML = '<option value="">Schuljahr...</option>';
(years || []).forEach(y => {
select.innerHTML += `<option value="${y.id}">${y.name}${y.is_current ? ' (aktuell)' : ''}</option>`;
});
}
} catch (e) {
console.log('Could not load school years:', e);
}
}
// ============================================
// SCHOOL SERVICE CRUD FUNCTIONS
// ============================================
const SCHOOL_API_BASE = '/api/school';
// Get auth headers for school service requests
function getSchoolAuthHeaders() {
const token = localStorage.getItem('token');
return {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
}
// Proxy request through backend to school-service
async function schoolServiceRequest(endpoint, options = {}) {
const token = localStorage.getItem('token');
const url = `${SCHOOL_API_BASE}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Request failed' }));
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.json();
}
// ===== CLASSES =====
async function loadClasses() {
try {
const data = await schoolServiceRequest('/classes');
const container = document.getElementById('classes-list');
if (!container) return;
if (!data.classes || data.classes.length === 0) {
container.innerHTML = '<div class="empty-state">Keine Klassen vorhanden. Erstellen Sie eine neue Klasse.</div>';
return;
}
container.innerHTML = data.classes.map(c => `
<div class="class-card" data-id="${c.id}">
<div class="class-header">
<h3>${c.name}</h3>
<span class="badge">${c.school_type || 'Gymnasium'}</span>
</div>
<div class="class-info">
<span>Klasse ${c.grade_level}</span>
<span>${c.student_count || 0} Schüler</span>
</div>
<div class="class-actions">
<button class="btn btn-sm btn-secondary" onclick="showClassStudents('${c.id}', '${c.name}')">Schüler</button>
<button class="btn btn-sm btn-danger" onclick="deleteClass('${c.id}')">Löschen</button>
</div>
</div>
`).join('');
} catch (e) {
console.error('Error loading classes:', e);
showToast('Fehler beim Laden der Klassen', 'error');
}
}
async function showClassStudents(classId, className) {
try {
const data = await schoolServiceRequest(`/classes/${classId}/students`);
const students = data.students || [];
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h2>Schüler - ${className}</h2>
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">&times;</button>
</div>
<div class="modal-body">
<div class="student-actions" style="margin-bottom: 1rem;">
<button class="btn btn-primary btn-sm" onclick="showAddStudentModal('${classId}')">+ Schüler hinzufügen</button>
<button class="btn btn-secondary btn-sm" onclick="showImportStudentsModal('${classId}')">CSV Import</button>
</div>
<div class="students-list">
${students.length === 0 ? '<div class="empty-state">Keine Schüler vorhanden</div>' :
students.map(s => `
<div class="student-row">
<span class="student-name">${s.last_name}, ${s.first_name}</span>
<span class="student-number">${s.student_number || ''}</span>
<button class="btn btn-sm btn-danger" onclick="deleteStudent('${classId}', '${s.id}')">&times;</button>
</div>
`).join('')
}
</div>
</div>
</div>
`;
document.body.appendChild(modal);
} catch (e) {
console.error('Error loading students:', e);
showToast('Fehler beim Laden der Schüler', 'error');
}
}
function showCreateClassModal() {
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>Neue Klasse anlegen</h2>
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">&times;</button>
</div>
<div class="modal-body">
<form id="create-class-form" onsubmit="createClass(event)">
<div class="form-group">
<label>Klassenname*</label>
<input type="text" name="name" placeholder="z.B. 7a" required>
</div>
<div class="form-group">
<label>Klassenstufe*</label>
<select name="grade_level" required>
${[1,2,3,4,5,6,7,8,9,10,11,12,13].map(g => `<option value="${g}">${g}. Klasse</option>`).join('')}
</select>
</div>
<div class="form-group">
<label>Schulform</label>
<select name="school_type">
<option value="gymnasium">Gymnasium</option>
<option value="realschule">Realschule</option>
<option value="hauptschule">Hauptschule</option>
<option value="gesamtschule">Gesamtschule</option>
<option value="grundschule">Grundschule</option>
</select>
</div>
<div class="form-group">
<label>Bundesland</label>
<select name="federal_state">
<option value="niedersachsen">Niedersachsen</option>
<option value="nrw">Nordrhein-Westfalen</option>
<option value="bayern">Bayern</option>
<option value="baden-wuerttemberg">Baden-Württemberg</option>
<option value="hessen">Hessen</option>
<option value="other">Sonstiges</option>
</select>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
<button type="submit" class="btn btn-primary">Klasse erstellen</button>
</div>
</form>
</div>
</div>
`;
document.body.appendChild(modal);
}
async function createClass(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
try {
await schoolServiceRequest('/classes', {
method: 'POST',
body: JSON.stringify({
name: formData.get('name'),
grade_level: parseInt(formData.get('grade_level')),
school_type: formData.get('school_type'),
federal_state: formData.get('federal_state')
})
});
form.closest('.modal-overlay').remove();
showToast('Klasse erfolgreich erstellt', 'success');
loadClasses();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
async function deleteClass(classId) {
if (!confirm('Klasse wirklich löschen? Alle Schüler werden ebenfalls gelöscht.')) return;
try {
await schoolServiceRequest(`/classes/${classId}`, { method: 'DELETE' });
showToast('Klasse gelöscht', 'success');
loadClasses();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
function showAddStudentModal(classId) {
document.querySelector('.modal-overlay')?.remove();
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>Schüler hinzufügen</h2>
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">&times;</button>
</div>
<div class="modal-body">
<form id="add-student-form" onsubmit="addStudent(event, '${classId}')">
<div class="form-group">
<label>Vorname*</label>
<input type="text" name="first_name" required>
</div>
<div class="form-group">
<label>Nachname*</label>
<input type="text" name="last_name" required>
</div>
<div class="form-group">
<label>Geburtsdatum</label>
<input type="date" name="birth_date">
</div>
<div class="form-group">
<label>Schülernummer</label>
<input type="text" name="student_number">
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
<button type="submit" class="btn btn-primary">Hinzufügen</button>
</div>
</form>
</div>
</div>
`;
document.body.appendChild(modal);
}
async function addStudent(event, classId) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
try {
await schoolServiceRequest(`/classes/${classId}/students`, {
method: 'POST',
body: JSON.stringify({
first_name: formData.get('first_name'),
last_name: formData.get('last_name'),
birth_date: formData.get('birth_date') || null,
student_number: formData.get('student_number') || null
})
});
form.closest('.modal-overlay').remove();
showToast('Schüler hinzugefügt', 'success');
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
async function deleteStudent(classId, studentId) {
if (!confirm('Schüler wirklich entfernen?')) return;
try {
await schoolServiceRequest(`/classes/${classId}/students/${studentId}`, { method: 'DELETE' });
showToast('Schüler entfernt', 'success');
document.querySelector('.modal-overlay')?.remove();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
function showImportStudentsModal(classId) {
document.querySelector('.modal-overlay')?.remove();
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>CSV Import</h2>
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">&times;</button>
</div>
<div class="modal-body">
<p>CSV-Format: Vorname,Nachname[,Geburtsdatum][,Schülernummer]</p>
<form id="import-students-form" onsubmit="importStudents(event, '${classId}')">
<div class="form-group">
<label>CSV-Datei</label>
<input type="file" name="file" accept=".csv" required>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
<button type="submit" class="btn btn-primary">Importieren</button>
</div>
</form>
</div>
</div>
`;
document.body.appendChild(modal);
}
async function importStudents(event, classId) {
event.preventDefault();
const form = event.target;
const fileInput = form.querySelector('input[type="file"]');
const file = fileInput.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const token = localStorage.getItem('token');
const response = await fetch(`${SCHOOL_API_BASE}/classes/${classId}/students/import`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formData
});
const data = await response.json();
form.closest('.modal-overlay').remove();
showToast(`${data.imported || 0} Schüler importiert`, 'success');
} catch (e) {
showToast('Import fehlgeschlagen: ' + e.message, 'error');
}
}
// ===== SUBJECTS =====
async function loadSubjects() {
try {
const data = await schoolServiceRequest('/subjects');
const container = document.getElementById('subjects-list');
if (!container) return;
if (!data.subjects || data.subjects.length === 0) {
container.innerHTML = '<div class="empty-state">Keine Fächer vorhanden</div>';
return;
}
container.innerHTML = data.subjects.map(s => `
<div class="subject-row">
<span class="subject-name">${s.name}</span>
<span class="subject-short">${s.short_name || ''}</span>
<span class="badge ${s.is_main_subject ? 'badge-primary' : 'badge-secondary'}">${s.is_main_subject ? 'Hauptfach' : 'Nebenfach'}</span>
<button class="btn btn-sm btn-danger" onclick="deleteSubject('${s.id}')">&times;</button>
</div>
`).join('');
} catch (e) {
console.error('Error loading subjects:', e);
showToast('Fehler beim Laden der Fächer', 'error');
}
}
function showCreateSubjectModal() {
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>Neues Fach anlegen</h2>
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">&times;</button>
</div>
<div class="modal-body">
<form id="create-subject-form" onsubmit="createSubject(event)">
<div class="form-group">
<label>Fachname*</label>
<input type="text" name="name" placeholder="z.B. Mathematik" required>
</div>
<div class="form-group">
<label>Kürzel</label>
<input type="text" name="short_name" placeholder="z.B. Ma" maxlength="10">
</div>
<div class="form-group">
<label><input type="checkbox" name="is_main_subject"> Hauptfach (höhere Gewichtung)</label>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
<button type="submit" class="btn btn-primary">Fach erstellen</button>
</div>
</form>
</div>
</div>
`;
document.body.appendChild(modal);
}
async function createSubject(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
try {
await schoolServiceRequest('/subjects', {
method: 'POST',
body: JSON.stringify({
name: formData.get('name'),
short_name: formData.get('short_name') || null,
is_main_subject: form.querySelector('[name="is_main_subject"]').checked
})
});
form.closest('.modal-overlay').remove();
showToast('Fach erstellt', 'success');
loadSubjects();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
async function deleteSubject(subjectId) {
if (!confirm('Fach wirklich löschen?')) return;
try {
await schoolServiceRequest(`/subjects/${subjectId}`, { method: 'DELETE' });
showToast('Fach gelöscht', 'success');
loadSubjects();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
// ===== EXAMS =====
async function loadExams() {
try {
const classSelect = document.getElementById('exams-class-select');
const classId = classSelect?.value;
const url = classId ? `/exams?class_id=${classId}` : '/exams';
const data = await schoolServiceRequest(url);
const container = document.getElementById('exams-list');
if (!container) return;
if (!data.exams || data.exams.length === 0) {
container.innerHTML = '<div class="empty-state">Keine Klausuren vorhanden</div>';
return;
}
container.innerHTML = data.exams.map(e => `
<div class="exam-card" data-id="${e.id}">
<div class="exam-header">
<h4>${e.title}</h4>
<span class="badge badge-${e.status === 'active' ? 'success' : 'secondary'}">${e.status}</span>
</div>
<div class="exam-info">
<span>${e.exam_type}</span>
<span>${e.exam_date || 'Kein Datum'}</span>
<span>${e.max_points} Punkte</span>
</div>
<div class="exam-actions">
<button class="btn btn-sm btn-primary" onclick="showExamResultsModal('${e.id}')">Ergebnisse</button>
<button class="btn btn-sm btn-secondary" onclick="generateNachschreiber('${e.id}')">Nachschreiber</button>
<button class="btn btn-sm btn-danger" onclick="deleteExam('${e.id}')">Löschen</button>
</div>
</div>
`).join('');
} catch (e) {
console.error('Error loading exams:', e);
showToast('Fehler beim Laden der Klausuren', 'error');
}
}
function showCreateExamModal() {
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content" style="max-width: 700px;">
<div class="modal-header">
<h2>Neue Klausur/Arbeit</h2>
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">&times;</button>
</div>
<div class="modal-body">
<form id="create-exam-form" onsubmit="createExam(event)">
<div class="form-row">
<div class="form-group">
<label>Titel*</label>
<input type="text" name="title" placeholder="z.B. Klassenarbeit Nr. 1" required>
</div>
<div class="form-group">
<label>Typ*</label>
<select name="exam_type" required>
<option value="klassenarbeit">Klassenarbeit</option>
<option value="test">Test</option>
<option value="klausur">Klausur (Oberstufe)</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Klasse*</label>
<select name="class_id" id="exam-class-select" required></select>
</div>
<div class="form-group">
<label>Fach*</label>
<select name="subject_id" id="exam-subject-select" required></select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Datum</label>
<input type="date" name="exam_date">
</div>
<div class="form-group">
<label>Dauer (Minuten)</label>
<input type="number" name="duration_minutes" value="45" min="5">
</div>
<div class="form-group">
<label>Max. Punkte</label>
<input type="number" name="max_points" value="50" min="1" step="0.5">
</div>
</div>
<div class="form-group">
<label>Thema</label>
<input type="text" name="topic" placeholder="z.B. Bruchrechnung">
</div>
<div class="form-group">
<label>Aufgaben (Markdown/HTML)</label>
<textarea name="content" rows="6" placeholder="## Aufgabe 1&#10;Berechne..."></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
<button type="submit" class="btn btn-primary">Klausur erstellen</button>
</div>
</form>
</div>
</div>
`;
document.body.appendChild(modal);
loadClassesForSelect('exam-class-select');
loadSubjectsForSelect('exam-subject-select');
}
async function createExam(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
try {
await schoolServiceRequest('/exams', {
method: 'POST',
body: JSON.stringify({
title: formData.get('title'),
exam_type: formData.get('exam_type'),
class_id: formData.get('class_id'),
subject_id: formData.get('subject_id'),
exam_date: formData.get('exam_date') || null,
duration_minutes: parseInt(formData.get('duration_minutes')) || 45,
max_points: parseFloat(formData.get('max_points')) || 50,
topic: formData.get('topic') || null,
content: formData.get('content') || null
})
});
form.closest('.modal-overlay').remove();
showToast('Klausur erstellt', 'success');
loadExams();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
async function deleteExam(examId) {
if (!confirm('Klausur wirklich löschen?')) return;
try {
await schoolServiceRequest(`/exams/${examId}`, { method: 'DELETE' });
showToast('Klausur gelöscht', 'success');
loadExams();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
async function showExamResultsModal(examId) {
try {
const exam = await schoolServiceRequest(`/exams/${examId}`);
const results = await schoolServiceRequest(`/exams/${examId}/results`);
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
<h2>Ergebnisse: ${exam.title}</h2>
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">&times;</button>
</div>
<div class="modal-body">
<p>Max. Punkte: ${exam.max_points}</p>
<table class="results-table">
<thead>
<tr>
<th>Schüler</th>
<th>Punkte</th>
<th>%</th>
<th>Note</th>
<th>Freigabe</th>
</tr>
</thead>
<tbody>
${(results.results || []).map(r => `
<tr>
<td>${r.student_name}</td>
<td><input type="number" value="${r.points_achieved || ''}" data-student="${r.student_id}" class="result-points" step="0.5" min="0" max="${exam.max_points}"></td>
<td>${r.percentage ? r.percentage.toFixed(1) + '%' : '-'}</td>
<td>${r.grade || '-'}</td>
<td>${r.approved_by_teacher ? '✓' : '<button class="btn btn-xs" onclick="approveResult(\'${examId}\', \'${r.student_id}\')">Freigeben</button>'}</td>
</tr>
`).join('')}
</tbody>
</table>
<div class="form-actions" style="margin-top: 1rem;">
<button class="btn btn-primary" onclick="saveExamResults('${examId}', ${exam.max_points})">Speichern</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
async function saveExamResults(examId, maxPoints) {
const inputs = document.querySelectorAll('.result-points');
const results = [];
inputs.forEach(input => {
const points = parseFloat(input.value);
if (!isNaN(points)) {
results.push({
student_id: input.dataset.student,
points_achieved: points
});
}
});
try {
await schoolServiceRequest(`/exams/${examId}/results`, {
method: 'POST',
body: JSON.stringify({ results })
});
showToast('Ergebnisse gespeichert', 'success');
document.querySelector('.modal-overlay')?.remove();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
async function approveResult(examId, studentId) {
try {
await schoolServiceRequest(`/exams/${examId}/results/${studentId}/approve`, { method: 'PUT' });
showToast('Freigegeben', 'success');
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
async function generateNachschreiber(examId) {
if (!confirm('Nachschreiber-Version mit KI generieren?')) return;
try {
showToast('Generiere Nachschreiber...', 'info');
await schoolServiceRequest(`/exams/${examId}/generate-variant`, {
method: 'POST',
body: JSON.stringify({ variation_type: 'rewrite' })
});
showToast('Nachschreiber erstellt', 'success');
loadExams();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
function editExam(examId) {
showToast('Bearbeitungsfunktion in Entwicklung', 'info');
}
// ===== GRADES =====
async function loadGrades() {
try {
const classSelect = document.getElementById('grades-class-select');
const classId = classSelect?.value;
if (!classId) return;
const data = await schoolServiceRequest(`/grades/${classId}`);
const container = document.getElementById('grades-table');
if (!container) return;
if (!data.grades || data.grades.length === 0) {
container.innerHTML = '<div class="empty-state">Keine Noten vorhanden</div>';
return;
}
// Get all unique subjects
const subjects = new Set();
data.grades.forEach(g => g.subjects?.forEach(s => subjects.add(s.subject_name)));
const subjectList = Array.from(subjects);
container.innerHTML = `
<table class="grades-table">
<thead>
<tr>
<th>Schüler</th>
${subjectList.map(s => `<th>${s}</th>`).join('')}
<th>Durchschnitt</th>
</tr>
</thead>
<tbody>
${data.grades.map(g => {
const avg = g.subjects?.reduce((sum, s) => sum + (s.final_grade || 0), 0) / (g.subjects?.length || 1);
return `
<tr>
<td>${g.student_name}</td>
${subjectList.map(subj => {
const s = g.subjects?.find(x => x.subject_name === subj);
return `<td class="${s?.final_grade_locked ? 'locked' : ''}">${s?.final_grade?.toFixed(1) || '-'}</td>`;
}).join('')}
<td><strong>${avg.toFixed(2)}</strong></td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
} catch (e) {
console.error('Error loading grades:', e);
showToast('Fehler beim Laden der Noten', 'error');
}
}
async function calculateFinalGrades() {
const classSelect = document.getElementById('grades-class-select');
const classId = classSelect?.value;
if (!classId) {
showToast('Bitte Klasse auswählen', 'warning');
return;
}
try {
await schoolServiceRequest('/grades/calculate', {
method: 'POST',
body: JSON.stringify({ class_id: classId, semester: 1 })
});
showToast('Endnoten berechnet', 'success');
loadGrades();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
// ===== ATTENDANCE / GRADEBOOK =====
async function loadGradebook() {
try {
const classSelect = document.getElementById('gradebook-class-select');
const classId = classSelect?.value;
if (!classId) return;
const attendance = await schoolServiceRequest(`/attendance/${classId}`);
const entries = await schoolServiceRequest(`/gradebook/${classId}`);
const container = document.getElementById('gradebook-content');
if (!container) return;
container.innerHTML = `
<div class="gradebook-section">
<h4>Fehlzeiten</h4>
<table class="attendance-table">
<thead>
<tr><th>Schüler</th><th>Datum</th><th>Status</th><th>Stunden</th></tr>
</thead>
<tbody>
${(attendance.attendance || []).slice(0, 20).map(a => `
<tr>
<td>${a.student_name}</td>
<td>${a.date}</td>
<td>${a.status === 'absent_excused' ? 'entschuldigt' : a.status === 'absent_unexcused' ? 'unentschuldigt' : a.status}</td>
<td>${a.periods}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<div class="gradebook-section">
<h4>Einträge</h4>
${(entries.entries || []).slice(0, 10).map(e => `
<div class="gradebook-entry ${e.entry_type}">
<span class="date">${e.date}</span>
<span class="type">${e.entry_type}</span>
<span class="content">${e.content}</span>
</div>
`).join('')}
</div>
`;
} catch (e) {
console.error('Error loading gradebook:', e);
}
}
function loadGradebookEntries() { loadGradebook(); }
function showAttendanceModal() {
const classSelect = document.getElementById('gradebook-class-select');
const classId = classSelect?.value;
if (!classId) {
showToast('Bitte Klasse auswählen', 'warning');
return;
}
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>Fehlzeit eintragen</h2>
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">&times;</button>
</div>
<div class="modal-body">
<form id="attendance-form" onsubmit="createAttendance(event, '${classId}')">
<div class="form-group">
<label>Datum</label>
<input type="date" name="date" value="${new Date().toISOString().split('T')[0]}" required>
</div>
<div class="form-group">
<label>Schüler</label>
<select name="student_id" id="attendance-student-select" required></select>
</div>
<div class="form-group">
<label>Status</label>
<select name="status" required>
<option value="absent_excused">Entschuldigt</option>
<option value="absent_unexcused">Unentschuldigt</option>
<option value="late">Verspätet</option>
</select>
</div>
<div class="form-group">
<label>Stunden</label>
<input type="number" name="periods" value="1" min="1" max="12">
</div>
<div class="form-group">
<label>Grund</label>
<input type="text" name="reason" placeholder="z.B. Arzttermin">
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
<button type="submit" class="btn btn-primary">Eintragen</button>
</div>
</form>
</div>
</div>
`;
document.body.appendChild(modal);
loadStudentsForSelect(classId, 'attendance-student-select');
}
async function createAttendance(event, classId) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
try {
await schoolServiceRequest('/attendance', {
method: 'POST',
body: JSON.stringify({
student_id: formData.get('student_id'),
date: formData.get('date'),
status: formData.get('status'),
periods: parseInt(formData.get('periods')),
reason: formData.get('reason') || null
})
});
form.closest('.modal-overlay').remove();
showToast('Fehlzeit eingetragen', 'success');
loadGradebook();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
function showGradebookEntryModal() {
const classSelect = document.getElementById('gradebook-class-select');
const classId = classSelect?.value;
if (!classId) {
showToast('Bitte Klasse auswählen', 'warning');
return;
}
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>Eintrag erstellen</h2>
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">&times;</button>
</div>
<div class="modal-body">
<form id="entry-form" onsubmit="createGradebookEntry(event, '${classId}')">
<div class="form-group">
<label>Datum</label>
<input type="date" name="date" value="${new Date().toISOString().split('T')[0]}" required>
</div>
<div class="form-group">
<label>Schüler (optional)</label>
<select name="student_id" id="entry-student-select">
<option value="">Klasseneintrag</option>
</select>
</div>
<div class="form-group">
<label>Art</label>
<select name="entry_type" required>
<option value="note">Notiz</option>
<option value="warning">Verwarnung</option>
<option value="praise">Lob</option>
<option value="incident">Vorfall</option>
<option value="homework">Hausaufgaben</option>
</select>
</div>
<div class="form-group">
<label>Inhalt</label>
<textarea name="content" rows="3" required></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
<button type="submit" class="btn btn-primary">Eintragen</button>
</div>
</form>
</div>
</div>
`;
document.body.appendChild(modal);
loadStudentsForSelect(classId, 'entry-student-select');
}
async function createGradebookEntry(event, classId) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
try {
await schoolServiceRequest('/gradebook', {
method: 'POST',
body: JSON.stringify({
class_id: classId,
student_id: formData.get('student_id') || null,
date: formData.get('date'),
entry_type: formData.get('entry_type'),
content: formData.get('content')
})
});
form.closest('.modal-overlay').remove();
showToast('Eintrag erstellt', 'success');
loadGradebook();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
// ===== CERTIFICATES =====
async function loadCertificates() {
try {
const classSelect = document.getElementById('certificates-class-select');
const classId = classSelect?.value;
if (!classId) return;
const data = await schoolServiceRequest(`/certificates/class/${classId}`);
const container = document.getElementById('certificates-list');
if (!container) return;
if (!data.certificates || data.certificates.length === 0) {
container.innerHTML = '<div class="empty-state">Keine Zeugnisse vorhanden</div>';
return;
}
container.innerHTML = data.certificates.map(c => `
<div class="certificate-row">
<span>${c.student_name}</span>
<span class="badge badge-${c.status === 'final' ? 'success' : 'secondary'}">${c.status}</span>
<div class="certificate-actions">
${c.status === 'draft' ? `<button class="btn btn-xs" onclick="finalizeCertificate('${c.id}')">Finalisieren</button>` : ''}
<button class="btn btn-xs" onclick="downloadCertificatePDF('${c.id}')">PDF</button>
</div>
</div>
`).join('');
} catch (e) {
console.error('Error loading certificates:', e);
}
}
async function loadCertificateTemplates() {
try {
const data = await schoolServiceRequest('/certificates/templates');
const select = document.getElementById('certificate-template-select');
if (!select) return;
select.innerHTML = (data.templates || []).map(t =>
`<option value="${t.name}">${t.display_name}</option>`
).join('');
} catch (e) {
console.error('Error loading templates:', e);
}
}
async function generateAllCertificates() {
const classSelect = document.getElementById('certificates-class-select');
const templateSelect = document.getElementById('certificate-template-select');
const classId = classSelect?.value;
const template = templateSelect?.value;
if (!classId || !template) {
showToast('Bitte Klasse und Vorlage auswählen', 'warning');
return;
}
try {
showToast('Generiere Zeugnisse...', 'info');
await schoolServiceRequest('/certificates/generate-bulk', {
method: 'POST',
body: JSON.stringify({
class_id: classId,
semester: 1,
certificate_type: 'halbjahr',
template_name: template
})
});
showToast('Zeugnisse generiert', 'success');
loadCertificates();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
async function finalizeCertificate(certId) {
if (!confirm('Zeugnis finalisieren? Dies kann nicht rückgängig gemacht werden.')) return;
try {
await schoolServiceRequest(`/certificates/detail/${certId}/finalize`, { method: 'PUT' });
showToast('Zeugnis finalisiert', 'success');
loadCertificates();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
async function downloadCertificatePDF(certId) {
try {
const token = localStorage.getItem('token');
const response = await fetch(`${SCHOOL_API_BASE}/certificates/detail/${certId}/pdf`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) throw new Error('PDF download failed');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `zeugnis_${certId}.pdf`;
a.click();
window.URL.revokeObjectURL(url);
} catch (e) {
showToast('Fehler beim PDF-Download', 'error');
}
}
// ===== SCHOOL YEARS =====
function showCreateSchoolYearModal() {
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>Neues Schuljahr</h2>
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">&times;</button>
</div>
<div class="modal-body">
<form id="school-year-form" onsubmit="createSchoolYear(event)">
<div class="form-group">
<label>Name*</label>
<input type="text" name="name" placeholder="z.B. 2024/2025" required>
</div>
<div class="form-group">
<label>Start</label>
<input type="date" name="start_date" required>
</div>
<div class="form-group">
<label>Ende</label>
<input type="date" name="end_date" required>
</div>
<div class="form-group">
<label><input type="checkbox" name="is_current" checked> Aktuelles Schuljahr</label>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
<button type="submit" class="btn btn-primary">Erstellen</button>
</div>
</form>
</div>
</div>
`;
document.body.appendChild(modal);
}
async function createSchoolYear(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
try {
await schoolServiceRequest('/years', {
method: 'POST',
body: JSON.stringify({
name: formData.get('name'),
start_date: formData.get('start_date'),
end_date: formData.get('end_date'),
is_current: form.querySelector('[name="is_current"]').checked
})
});
form.closest('.modal-overlay').remove();
showToast('Schuljahr erstellt', 'success');
loadSchoolYears();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
// ===== HELPER: Load students for select =====
async function loadStudentsForSelect(classId, selectId) {
try {
const data = await schoolServiceRequest(`/classes/${classId}/students`);
const select = document.getElementById(selectId);
if (!select) return;
const currentOptions = select.innerHTML;
select.innerHTML = currentOptions + (data.students || []).map(s =>
`<option value="${s.id}">${s.last_name}, ${s.first_name}</option>`
).join('');
} catch (e) {
console.error('Error loading students for select:', e);
}
}
// Helper: Hide all panels
function hideAllPanels() {
const panels = [
'panel-compare',
'panel-tiles',
'panel-correction',
'panel-letters',
'panel-messenger',
'panel-video',
'panel-exams',
'panel-grades',
'panel-gradebook',
'panel-certificates',
'panel-classes',
'panel-subjects'
];
panels.forEach(panelId => {
const panel = document.getElementById(panelId);
if (panel) {
panel.style.display = 'none';
}
});
}
// Helper: Update sidebar active state
function updateSidebarActive(activeSidebarId) {
document.querySelectorAll('.sidebar-item').forEach(item => {
item.classList.remove('active');
});
const activeItem = document.getElementById(activeSidebarId);
if (activeItem) {
activeItem.classList.add('active');
}
}
// ============================================
// JITSI VIDEOKONFERENZ MODULE
// ============================================
let currentJitsiMeetingUrl = null;
let jitsiMicMuted = false;
let jitsiVideoOff = false;
// Start instant meeting
async function startInstantMeeting() {
console.log('Starting instant meeting...');
const meetingName = document.getElementById('meeting-name')?.value || '';
try {
// Generate a unique room name
const roomId = 'bp-' + Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
const displayName = meetingName || 'BreakPilot Meeting';
// Create Jitsi meeting URL
const jitsiDomain = 'meet.jit.si';
currentJitsiMeetingUrl = `https://${jitsiDomain}/${roomId}`;
// Show the Jitsi iframe
const placeholder = document.getElementById('jitsi-placeholder');
const iframeContainer = document.getElementById('jitsi-iframe-container');
const controls = document.getElementById('jitsi-controls');
if (placeholder) placeholder.style.display = 'none';
if (iframeContainer) {
iframeContainer.style.display = 'flex';
iframeContainer.innerHTML = `
<iframe
src="${currentJitsiMeetingUrl}#config.prejoinPageEnabled=false&userInfo.displayName=${encodeURIComponent('Lehrer')}"
style="width: 100%; height: 100%; border: none; border-radius: 8px;"
allow="camera; microphone; fullscreen; display-capture; autoplay"
allowfullscreen
></iframe>
`;
}
if (controls) controls.style.display = 'flex';
// Update status
const statusPill = document.getElementById('jitsi-connection-status');
if (statusPill) {
statusPill.textContent = 'Live';
statusPill.style.background = '#ef4444';
}
// Update participants list
const participantsList = document.getElementById('jitsi-participants');
if (participantsList) {
participantsList.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px; padding: 8px; background: rgba(16, 185, 129, 0.1); border-radius: 6px;">
<div style="width: 32px; height: 32px; border-radius: 50%; background: #6C1B1B; color: white; display: flex; align-items: center; justify-content: center; font-weight: 600;">L</div>
<div>
<div style="font-weight: 600; font-size: 13px;">Sie (Lehrer)</div>
<div style="font-size: 11px; color: #888;">Moderator</div>
</div>
</div>
`;
}
console.log('Meeting started:', currentJitsiMeetingUrl);
} catch (error) {
console.error('Error starting meeting:', error);
alert('Fehler beim Starten des Meetings: ' + error.message);
}
}
// Join a scheduled meeting
function joinScheduledMeeting(meetingId) {
console.log('Joining scheduled meeting:', meetingId);
// For demo, just start a meeting with the ID as room name
document.getElementById('meeting-name').value = meetingId;
startInstantMeeting();
}
// Schedule a meeting
function scheduleMeeting() {
const title = document.getElementById('schedule-title')?.value;
const datetime = document.getElementById('schedule-datetime')?.value;
const participants = document.getElementById('schedule-participants')?.value;
if (!title || !datetime) {
alert('Bitte Titel und Datum/Uhrzeit eingeben');
return;
}
console.log('Scheduling meeting:', { title, datetime, participants });
// Add to scheduled meetings list (demo)
const scheduledList = document.getElementById('scheduled-meetings');
if (scheduledList) {
const dateObj = new Date(datetime);
const formattedTime = dateObj.toLocaleString('de-DE', {
weekday: 'short',
hour: '2-digit',
minute: '2-digit'
});
const meetingItem = document.createElement('div');
meetingItem.className = 'meeting-item';
meetingItem.onclick = () => joinScheduledMeeting('m-' + Date.now());
meetingItem.innerHTML = `
<div class="meeting-info">
<div class="meeting-title">${title}</div>
<div class="meeting-time">${formattedTime}</div>
</div>
<button class="btn btn-sm btn-ghost">Beitreten</button>
`;
scheduledList.appendChild(meetingItem);
}
// Clear form
document.getElementById('schedule-title').value = '';
document.getElementById('schedule-datetime').value = '';
document.getElementById('schedule-participants').value = '';
alert('Meeting geplant: ' + title);
}
// Toggle mute
function toggleJitsiMute() {
jitsiMicMuted = !jitsiMicMuted;
console.log('Mic muted:', jitsiMicMuted);
// Note: With iframe approach, we can't control the Jitsi directly
// User should use Jitsi's built-in controls
}
// Toggle video
function toggleJitsiVideo() {
jitsiVideoOff = !jitsiVideoOff;
console.log('Video off:', jitsiVideoOff);
}
// Share screen
function shareJitsiScreen() {
console.log('Screen share requested - use Jitsi controls');
alert('Bitte nutzen Sie die Bildschirmfreigabe-Funktion in der Jitsi-Oberfläche');
}
// Copy meeting link
function copyMeetingLink() {
if (currentJitsiMeetingUrl) {
navigator.clipboard.writeText(currentJitsiMeetingUrl).then(() => {
alert('Meeting-Link kopiert: ' + currentJitsiMeetingUrl);
}).catch(err => {
console.error('Failed to copy:', err);
prompt('Meeting-Link:', currentJitsiMeetingUrl);
});
} else {
alert('Kein aktives Meeting');
}
}
// Leave meeting
function leaveJitsiMeeting() {
console.log('Leaving meeting...');
const placeholder = document.getElementById('jitsi-placeholder');
const iframeContainer = document.getElementById('jitsi-iframe-container');
const controls = document.getElementById('jitsi-controls');
if (iframeContainer) {
iframeContainer.innerHTML = '';
iframeContainer.style.display = 'none';
}
if (placeholder) placeholder.style.display = 'flex';
if (controls) controls.style.display = 'none';
// Reset status
const statusPill = document.getElementById('jitsi-connection-status');
if (statusPill) {
statusPill.textContent = 'Bereit';
statusPill.style.background = '#10b981';
}
// Reset participants
const participantsList = document.getElementById('jitsi-participants');
if (participantsList) {
participantsList.innerHTML = 'Keine aktive Konferenz';
}
currentJitsiMeetingUrl = null;
}
// ============================================
// MATRIX MESSENGER MODULE (Stubs)
// ============================================
// Quick meeting from messenger panel
function startQuickMeeting() {
showVideoPanel();
setTimeout(() => startInstantMeeting(), 100);
}
// Create class room
function createClassRoom() {
console.log('Creating class room...');
alert('Klassen-Info-Raum wird erstellt...\n\nDiese Funktion wird mit der Matrix-Integration verfügbar sein.');
}
// Schedule parent meeting
function scheduleParentMeeting() {
showVideoPanel();
// Focus on the schedule form
setTimeout(() => {
document.getElementById('schedule-title')?.focus();
}, 100);
}
// Select a room
function selectRoom(roomId) {
console.log('Selecting room:', roomId);
// Remove active from all rooms
document.querySelectorAll('.room-item').forEach(item => {
item.classList.remove('active');
});
// Add active to clicked room
event.currentTarget.classList.add('active');
}
// Send message (from messenger panel)
function sendMessage() {
const input = document.getElementById('chat-message-input');
if (input && input.value.trim()) {
console.log('Sending message:', input.value);
// Add message to chat (demo)
const chatContainer = document.querySelector('#panel-messenger .chat-messages-container');
if (chatContainer) {
const msgDiv = document.createElement('div');
msgDiv.className = 'chat-message sent';
msgDiv.innerHTML = `
<div class="message-content">${input.value}</div>
<div class="message-time">${new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}</div>
`;
chatContainer.appendChild(msgDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
input.value = '';
}
}
// ============================================
// KLAUSUR-KORREKTUR (Exam Correction) Module
// ============================================
let currentExamJob = null;
let examUploadedFiles = [];
// Handle exam file upload
function handleExamUpload(event) {
const files = event.target.files;
if (!files.length) return;
examUploadedFiles = Array.from(files);
console.log(`${examUploadedFiles.length} Dateien für Klausur-Korrektur ausgewählt`);
// Update UI
const uploadArea = document.querySelector('.correction-upload-area');
if (uploadArea) {
uploadArea.innerHTML = `
<div style="color: #10b981; font-size: 48px;">✓</div>
<h3>${examUploadedFiles.length} Datei(en) ausgewählt</h3>
<p>${examUploadedFiles.map(f => f.name).join(', ')}</p>
<button onclick="document.getElementById('exam-file-input').click()"
style="margin-top: 16px; padding: 8px 16px; background: #374151; border: none; border-radius: 8px; color: white; cursor: pointer;">
Andere Dateien wählen
</button>
`;
}
// Enable start button
const startBtn = document.getElementById('start-correction-btn');
if (startBtn) {
startBtn.disabled = false;
}
}
// Start correction job
async function startCorrectionJob() {
if (!examUploadedFiles.length) {
alert('Bitte wähle zuerst Dateien aus.');
return;
}
const subject = document.getElementById('exam-subject')?.value || 'unbekannt';
const className = document.getElementById('exam-class')?.value || '';
const title = document.getElementById('exam-title')?.value || 'Klausur';
console.log('Starte Korrektur-Job:', { subject, className, title, files: examUploadedFiles.length });
// Show pipeline progress
updatePipelineStep('upload', 'active');
// TODO: Implement actual API call to exam-service
// For now, simulate progress
simulateCorrectionPipeline();
}
// Simulate correction pipeline (placeholder)
function simulateCorrectionPipeline() {
const steps = ['upload', 'preprocess', 'ocr', 'segment', 'grade', 'review'];
let currentStep = 0;
const interval = setInterval(() => {
if (currentStep > 0) {
updatePipelineStep(steps[currentStep - 1], 'completed');
}
if (currentStep < steps.length) {
updatePipelineStep(steps[currentStep], 'active');
currentStep++;
} else {
clearInterval(interval);
showCorrectionResults();
}
}, 1500);
}
// Update pipeline step visualization
function updatePipelineStep(stepId, status) {
const step = document.querySelector(`[data-step="${stepId}"]`);
if (!step) return;
step.classList.remove('active', 'completed');
if (status === 'active') {
step.classList.add('active');
step.style.background = '#3b82f6';
step.style.color = 'white';
} else if (status === 'completed') {
step.classList.add('completed');
step.style.background = '#10b981';
step.style.color = 'white';
}
}
// Show correction results (placeholder)
function showCorrectionResults() {
const resultsPanel = document.querySelector('.correction-results');
if (resultsPanel) {
resultsPanel.innerHTML = `
<div style="padding: 24px; text-align: center;">
<div style="font-size: 64px; margin-bottom: 16px;">✓</div>
<h2>Korrektur abgeschlossen</h2>
<p style="color: #9ca3af; margin-top: 8px;">
Die OCR-Erkennung und automatische Bewertung wurde durchgeführt.
</p>
<p style="color: #9ca3af;">
Bitte überprüfe die Ergebnisse im Review-Bereich.
</p>
<button onclick="showReviewInterface()"
style="margin-top: 24px; padding: 12px 24px; background: #3b82f6; border: none; border-radius: 8px; color: white; cursor: pointer; font-size: 16px;">
Ergebnisse reviewen
</button>
</div>
`;
}
}
// Show review interface (placeholder)
function showReviewInterface() {
console.log('Review-Interface wird geladen...');
// TODO: Implement actual review interface
}
// Export correction results
function exportCorrectionResults(format) {
console.log(`Exportiere Ergebnisse als ${format}...`);
// TODO: Implement export functionality
alert(`Export als ${format} wird in Phase 2 implementiert.`);
}
// ============================================
// LERNMATERIAL (Learning Material) Module
// ============================================
let learningSourceDocument = null;
// Generate learning material
async function generateLearningMaterial(type) {
console.log(`Generiere Lernmaterial: ${type}`);
// TODO: Implement actual generation
alert(`${type} wird in einer späteren Phase implementiert.`);
}
// ============================================
// ELTERNBRIEFE (Parent Letters) Module
// ============================================
// Generate parent letter
async function generateParentLetter(template) {
console.log(`Generiere Elternbrief mit Template: ${template}`);
// TODO: Implement actual generation
alert('Elternbriefe werden in einer späteren Phase implementiert.');
}
// Check Matrix and Jitsi service status
async function checkCommunicationStatus() {
try {
const response = await fetch(`${COMM_API_BASE}/status`);
const status = await response.json();
// Update Matrix status
const matrixStatus = document.getElementById('matrix-status');
if (status.matrix && status.matrix.healthy) {
matrixStatus.innerHTML = '● Online';
matrixStatus.style.color = '#10b981';
} else {
matrixStatus.innerHTML = '● Offline';
matrixStatus.style.color = '#ef4444';
}
// Update Jitsi status
const jitsiStatus = document.getElementById('jitsi-status');
if (status.jitsi && status.jitsi.healthy) {
jitsiStatus.innerHTML = '● Bereit';
jitsiStatus.style.color = '#10b981';
} else {
jitsiStatus.innerHTML = '● Offline';
jitsiStatus.style.color = '#ef4444';
}
// Update main status pill
const statusPill = document.getElementById('comm-status-pill');
if ((status.matrix && status.matrix.healthy) || (status.jitsi && status.jitsi.healthy)) {
statusPill.innerHTML = 'Verbunden';
statusPill.style.background = '#10b981';
} else {
statusPill.innerHTML = 'Offline';
statusPill.style.background = '#ef4444';
}
} catch (error) {
console.error('Failed to check communication status:', error);
document.getElementById('matrix-status').innerHTML = '● Fehler';
document.getElementById('jitsi-status').innerHTML = '● Fehler';
}
}
// Room Selection
function selectRoom(roomId) {
currentRoom = roomId;
// Update active state in room list
document.querySelectorAll('.room-item').forEach(item => {
item.classList.remove('active');
});
event.currentTarget.classList.add('active');
// Update room header
const roomName = event.currentTarget.querySelector('.room-name').innerText;
document.getElementById('current-room-name').innerText = roomName;
// TODO: Load room messages from Matrix
console.log('Selected room:', roomId);
}
// Start Quick Video Meeting
async function startQuickMeeting() {
try {
const displayName = 'Lehrer'; // TODO: Get from logged in user
const response = await fetch(`${COMM_API_BASE}/meetings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token') || ''}`
},
body: JSON.stringify({
type: 'quick',
display_name: displayName
})
});
if (!response.ok) {
// Fallback: Open Jitsi directly
const roomName = 'breakpilot-' + Math.random().toString(36).substring(7);
openJitsiMeeting(roomName, displayName);
return;
}
const meeting = await response.json();
openJitsiMeeting(meeting.room_name, displayName);
} catch (error) {
console.error('Failed to create meeting:', error);
// Fallback
const roomName = 'breakpilot-' + Math.random().toString(36).substring(7);
openJitsiMeeting(roomName, 'Lehrer');
}
}
// Open Jitsi Meeting in embedded view
function openJitsiMeeting(roomName, displayName) {
// Show video panel
document.getElementById('video-panel').style.display = 'flex';
document.getElementById('info-panel').style.display = 'none';
const container = document.getElementById('jitsi-container');
container.innerHTML = '';
// Create iframe
const iframe = document.createElement('iframe');
iframe.src = `${JITSI_BASE_URL}/${roomName}#userInfo.displayName="${encodeURIComponent(displayName)}"&config.startWithAudioMuted=false&config.startWithVideoMuted=false`;
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = 'none';
iframe.allow = 'camera; microphone; fullscreen; display-capture; autoplay';
container.appendChild(iframe);
console.log('Opened Jitsi meeting:', roomName);
}
// Close Video
function closeVideo() {
document.getElementById('video-panel').style.display = 'none';
document.getElementById('info-panel').style.display = 'block';
document.getElementById('jitsi-container').innerHTML = '';
}
// Start Video Call from room
function startVideoCall() {
if (currentRoom) {
openJitsiMeeting('elternkanal-' + currentRoom, 'Lehrer');
} else {
startQuickMeeting();
}
}
// Toggle Mute (placeholder)
function toggleMute() {
console.log('Toggle mute');
}
// Toggle Video (placeholder)
function toggleVideo() {
console.log('Toggle video');
}
// Leave Call
function leaveCall() {
closeVideo();
}
// Create Class Info Room
async function createClassRoom() {
const className = prompt('Klassenname eingeben (z.B. 7a):');
if (!className) return;
try {
const response = await fetch(`${COMM_API_BASE}/rooms`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token') || ''}`
},
body: JSON.stringify({
type: 'class_info',
class_name: className,
school_name: 'BreakPilot Schule',
teacher_ids: []
})
});
if (response.ok) {
const room = await response.json();
alert(`Klassen-Info-Raum erstellt!\nRoom ID: ${room.room_id}`);
// TODO: Refresh room list
} else {
alert('Raum konnte nicht erstellt werden. Ist Matrix konfiguriert?');
}
} catch (error) {
console.error('Failed to create room:', error);
alert('Fehler beim Erstellen des Raums');
}
}
// Schedule Parent Meeting
function scheduleParentMeeting() {
const studentName = prompt('Name des Schülers:');
if (!studentName) return;
const parentName = prompt('Name der Eltern:');
if (!parentName) return;
// For now, just create a quick meeting
const roomName = `elterngespraech-${studentName.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}`;
const meetingUrl = `${JITSI_BASE_URL}/${roomName}`;
alert(`Elterngespräch geplant!\n\nSchüler: ${studentName}\nEltern: ${parentName}\n\nMeeting-Link:\n${meetingUrl}`);
}
// Send Message
function sendMessage() {
const input = document.getElementById('chat-message-input');
const message = input.value.trim();
if (!message || !currentRoom) {
return;
}
// TODO: Send via Matrix API
console.log('Sending message:', message, 'to room:', currentRoom);
// Add to UI (demo)
const chatMessages = document.getElementById('chat-messages');
const msgDiv = document.createElement('div');
msgDiv.className = 'chat-msg chat-msg-self';
msgDiv.innerHTML = `
<div class="chat-msg-header">
<span class="chat-msg-sender">Sie</span>
<span class="chat-msg-time">Jetzt</span>
</div>
<div class="chat-msg-content">${message}</div>
`;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
input.value = '';
}
// Attach File (placeholder)
function attachFile() {
alert('Datei-Upload wird in einer zukünftigen Version unterstützt.');
}
// Show Room Info (placeholder)
function showRoomInfo() {
alert(`Raum: ${currentRoom || 'Nicht ausgewählt'}\n\nWeitere Raum-Informationen folgen.`);
}
// Open Notification Dialog
function openNotificationDialog() {
const type = document.getElementById('notification-type').value;
if (type === 'announcement') {
const title = prompt('Titel der Ankündigung:');
if (!title) return;
const content = prompt('Inhalt:');
if (!content) return;
alert(`Ankündigung gesendet:\n\n${title}\n${content}`);
} else if (type === 'absence') {
const student = prompt('Name des Schülers:');
if (!student) return;
const lesson = prompt('Unterrichtsstunde (1-10):');
alert(`Abwesenheitsmeldung für ${student} in Stunde ${lesson} gesendet.`);
} else if (type === 'grade') {
const student = prompt('Name des Schülers:');
if (!student) return;
const subject = prompt('Fach:');
const grade = prompt('Note:');
alert(`Notenbenachrichtigung für ${student}: ${subject} = ${grade} gesendet.`);
}
}
// Initialize sidebar click handlers
document.addEventListener('DOMContentLoaded', function() {
// Studio panel click handler
const sidebarStudio = document.getElementById('sidebar-studio');
if (sidebarStudio) {
sidebarStudio.addEventListener('click', function(e) {
e.preventDefault();
showStudioPanel();
});
}
// Communication panel click handler
const sidebarComm = document.getElementById('sidebar-communication');
if (sidebarComm) {
sidebarComm.addEventListener('click', function(e) {
e.preventDefault();
showCommunicationPanel();
});
}
// Enter key in chat input
const chatInput = document.getElementById('chat-message-input');
if (chatInput) {
chatInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
}
});