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 = '' + t('uploaded_worksheets') + ' 0 ' + t('files') + '';
}
// 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 = `
${message}
`;
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 = 'Original-ScanAlt (links)';
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 = 'Neu aufgebautes ArbeitsblattNeu (rechts)';
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 = 'Noch keine Neuaufbau-Daten vorhanden.
';
}
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 = 'Keine Arbeitsblätter vorhanden.
';
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 = 'Noch keine MC-Fragen für dieses Arbeitsblatt generiert.
';
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 = 'Keine Fragen vorhanden.
';
return;
}
const questions = mcData.questions;
const metadata = mcData.metadata || {};
let html = '';
// Zeige Metadaten
if (metadata.grade_level || metadata.subject) {
html += '';
if (metadata.subject) {
html += '
Fach: ' + metadata.subject + '
';
}
if (metadata.grade_level) {
html += '
Stufe: ' + metadata.grade_level + '
';
}
html += '
Fragen: ' + questions.length + '
';
html += '
';
}
// Zeige erste 2 Fragen als Vorschau
const previewQuestions = questions.slice(0, 2);
previewQuestions.forEach((q, idx) => {
html += '';
html += '
' + (idx + 1) + '. ' + q.question + '
';
html += '
';
q.options.forEach(opt => {
html += '
';
html += '' + opt.id + ') ' + opt.text;
html += '
';
});
html += '
';
html += '
';
});
if (questions.length > 2) {
html += '+ ' + (questions.length - 2) + ' weitere Fragen
';
}
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 += '';
if (metadata.source_title) {
html += '
Arbeitsblatt: ' + metadata.source_title + '
';
}
if (metadata.subject) {
html += '
Fach: ' + metadata.subject + '
';
}
if (metadata.grade_level) {
html += '
Stufe: ' + metadata.grade_level + '
';
}
html += '
';
// Alle Fragen
questions.forEach((q, idx) => {
html += '';
html += '
' + (idx + 1) + '. ' + q.question + '
';
html += '
';
q.options.forEach(opt => {
html += '
';
html += '' + opt.id + ') ' + opt.text;
html += '
';
});
html += '
';
html += '
';
});
// Auswertungs-Button
html += '';
html += '';
html += '
';
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 = '' +
'
' + correct + ' von ' + total + ' richtig
' +
'
' + Math.round(correct / total * 100) + '% korrekt
' +
'
';
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 = 'Keine Arbeitsblätter vorhanden.
';
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 = 'Noch keine Lückentexte für dieses Arbeitsblatt generiert.
';
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 = 'Keine Lückentexte vorhanden.
';
return;
}
const items = clozeData.cloze_items;
const metadata = clozeData.metadata || {};
let html = '';
// Statistiken
html += '';
if (metadata.subject) {
html += '
Fach: ' + metadata.subject + '
';
}
if (metadata.grade_level) {
html += '
Stufe: ' + metadata.grade_level + '
';
}
html += '
Sätze: ' + items.length + '
';
if (metadata.total_gaps) {
html += '
Lücken: ' + metadata.total_gaps + '
';
}
html += '
';
// Zeige erste 2 Sätze als Vorschau
const previewItems = items.slice(0, 2);
previewItems.forEach((item, idx) => {
html += '';
html += '
' + (idx + 1) + '. ' + item.sentence_with_gaps.replace(/___/g, '___') + '
';
// Übersetzung anzeigen
if (item.translation && item.translation.full_sentence) {
html += '
';
html += '
' + (item.translation.language_name || 'Übersetzung') + ':
';
html += item.translation.full_sentence;
html += '
';
}
html += '
';
});
if (items.length > 2) {
html += '+ ' + (items.length - 2) + ' weitere Sätze
';
}
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 += '';
if (metadata.source_title) {
html += '
Arbeitsblatt: ' + metadata.source_title + '
';
}
if (metadata.total_gaps) {
html += '
Lücken gesamt: ' + metadata.total_gaps + '
';
}
html += '
';
html += 'Fülle die Lücken aus und klicke auf "Prüfen".
';
// Alle Sätze mit Eingabefeldern
items.forEach((item, idx) => {
html += '';
// 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 '
';
});
html += '
' + (idx + 1) + '. ' + sentenceHtml + '
';
// Übersetzung als Hilfe
if (item.translation && item.translation.sentence_with_gaps) {
html += '
';
html += '
' + (item.translation.language_name || 'Übersetzung') + ' (mit Lücken):
';
html += item.translation.sentence_with_gaps;
html += '
';
}
html += '
';
});
// Buttons
html += '';
html += '';
html += '';
html += '
';
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 = '' +
'
' + correct + ' von ' + total + ' richtig
' +
'
' + Math.round(correct / total * 100) + '% korrekt
' +
'
';
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 = '' +
'
' + topic + '
' +
'
' + categoryCount + ' ' + (t('categories') || 'Kategorien') + ' | ' + termCount + ' ' + (t('terms') || 'Begriffe') + '
' +
'
';
}
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 = '' + t('qa_no_questions') + '
';
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 = '' + (t('qa_no_questions') || 'Noch keine Q&A für dieses Arbeitsblatt generiert.') + '
';
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 = '' + t('qa_no_questions') + '
';
return;
}
const items = qaData.qa_items;
const metadata = qaData.metadata || {};
let html = '';
// Leitner-Box Statistiken
html += '';
// 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 += '
';
html += '
' + (t('qa_box_new') || 'Neu') + ': ' + box0 + '
';
html += '
' + (t('qa_box_learning') || 'Lernt') + ': ' + box1 + '
';
html += '
' + (t('qa_box_mastered') || 'Gefestigt') + ': ' + box2 + '
';
html += '
';
html += '
';
// Zeige erste 2 Fragen als Vorschau
const previewItems = items.slice(0, 2);
previewItems.forEach((item, idx) => {
html += '';
html += '
' + (idx + 1) + '. ' + item.question + '
';
html += '
→ ' + item.answer.substring(0, 60) + (item.answer.length > 60 ? '...' : '') + '
';
html += '
';
});
if (items.length > 2) {
html += '+ ' + (items.length - 2) + ' ' + (t('questions') || 'weitere Fragen') + '
';
}
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 += '';
html += '
' + (t('question') || 'Frage') + ' ' + (currentQaIndex + 1) + ' / ' + items.length + '
';
html += '
';
html += '' + boxNames[leitner.box] + '';
html += '
';
html += '
';
// Frage
html += '';
html += '
' + (t('question') || 'Frage') + ':
';
html += '
' + item.question + '
';
html += '
';
// Eingabefeld für eigene Antwort
html += '';
// Prüfen-Button
html += '';
html += '';
html += '
';
// Vergleichs-Container (versteckt)
html += '';
// Eigene Antwort (wird befüllt)
html += '
';
html += '
' + (t('qa_your_answer') || 'Deine Antwort') + ':
';
html += '
';
html += '
';
// Richtige Antwort
html += '
';
html += '
' + (t('qa_correct_answer') || 'Richtige Antwort') + ':
';
html += '
' + item.answer + '
';
if (item.key_terms && item.key_terms.length > 0) {
html += '
' + (t('qa_key_terms') || 'Schlüsselbegriffe') + ': ' + item.key_terms.join(', ') + '
';
}
html += '
';
// Selbstbewertung
html += '
';
html += '
' + (t('qa_self_evaluate') || 'War deine Antwort richtig?') + '
';
html += '
';
html += '';
html += '';
html += '
';
html += '
';
html += '
'; // Ende qa-comparison-container
// Session-Statistik
html += '';
html += '
' + (t('qa_session_correct') || 'Richtig') + ': ' + qaSessionStats.correct + '
';
html += '
' + (t('qa_session_incorrect') || 'Falsch') + ': ' + qaSessionStats.incorrect + '
';
html += '
';
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 += '';
html += '
' + (percent >= 80 ? '🎉' : percent >= 50 ? '👍' : '💪') + '
';
html += '
' + (t('qa_session_complete') || 'Lernrunde abgeschlossen!') + '
';
html += '
' + qaSessionStats.correct + ' / ' + qaSessionStats.total + ' ' + (t('qa_result_correct') || 'richtig') + ' (' + percent + '%)
';
// Statistik
html += '
';
html += '
' + qaSessionStats.correct + '
' + (t('qa_correct') || 'Richtig') + '
';
html += '
' + qaSessionStats.incorrect + '
' + (t('qa_incorrect') || 'Falsch') + '
';
html += '
';
html += '
';
html += '';
html += '';
html += '
';
html += '
';
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 => `
`).join('');
} else {
container.innerHTML = getDefaultCookieCategories();
}
} catch(e) {
container.innerHTML = getDefaultCookieCategories();
}
}
function getDefaultCookieCategories() {
return `
`;
}
function getDefaultTerms(lang) {
const terms = {
de: 'Allgemeine Geschäftsbedingungen
Die BreakPilot-Plattform wird von der BreakPilot UG bereitgestellt.
Nutzung: Die Plattform dient zur Erstellung und Verwaltung von Lernmaterialien für Bildungszwecke.
Haftung: Die Nutzung erfolgt auf eigene Verantwortung.
Änderungen: Wir behalten uns vor, diese AGB jederzeit zu ändern.
',
en: 'Terms of Service
The BreakPilot platform is provided by BreakPilot UG.
Usage: The platform is designed for creating and managing learning materials for educational purposes.
Liability: Use at your own risk.
Changes: We reserve the right to modify these terms at any time.
'
};
return terms[lang] || terms.de;
}
function getDefaultPrivacy(lang) {
const privacy = {
de: 'Datenschutzerklärung
Verantwortlicher: BreakPilot UG
Erhobene Daten: Bei der Nutzung werden technische Daten (IP-Adresse, Browser-Typ) sowie von Ihnen eingegebene Inhalte verarbeitet.
Zweck: Die Daten werden zur Bereitstellung der Plattform und zur Verbesserung unserer Dienste genutzt.
Ihre Rechte (DSGVO):
- Auskunftsrecht (Art. 15)
- Recht auf Berichtigung (Art. 16)
- Recht auf Löschung (Art. 17)
- Recht auf Datenübertragbarkeit (Art. 20)
Kontakt: datenschutz@breakpilot.app
',
en: 'Privacy Policy
Controller: BreakPilot UG
Data Collected: Technical data (IP address, browser type) and content you provide are processed.
Purpose: Data is used to provide the platform and improve our services.
Your Rights (GDPR):
- Right of access (Art. 15)
- Right to rectification (Art. 16)
- Right to erasure (Art. 17)
- Right to data portability (Art. 20)
Contact: privacy@breakpilot.app
'
};
return privacy[lang] || privacy.de;
}
function getDefaultCommunityGuidelines(lang) {
const guidelines = {
de: 'Community Guidelines
Willkommen bei BreakPilot! Um eine positive und respektvolle Umgebung zu gewährleisten, bitten wir alle Nutzer, diese Richtlinien zu befolgen.
Respektvoller Umgang: Behandeln Sie andere Nutzer mit Respekt und Höflichkeit.
Keine illegalen Inhalte: Das Erstellen oder Teilen von illegalen Inhalten ist streng untersagt.
Urheberrecht: Respektieren Sie das geistige Eigentum anderer. Verwenden Sie nur Inhalte, für die Sie die Rechte besitzen.
Datenschutz: Teilen Sie keine persönlichen Daten anderer ohne deren ausdrückliche Zustimmung.
Qualität: Bemühen Sie sich um qualitativ hochwertige Lerninhalte.
Verstöße gegen diese Richtlinien können zur Sperrung des Accounts führen.
',
en: 'Community Guidelines
Welcome to BreakPilot! To ensure a positive and respectful environment, we ask all users to follow these guidelines.
Respectful Behavior: Treat other users with respect and courtesy.
No Illegal Content: Creating or sharing illegal content is strictly prohibited.
Copyright: Respect the intellectual property of others. Only use content you have rights to.
Privacy: Do not share personal data of others without their explicit consent.
Quality: Strive for high-quality learning content.
Violations of these guidelines may result in account suspension.
'
};
return guidelines[lang] || guidelines.de;
}
function getDefaultImprint(lang) {
const imprint = {
de: 'Impressum
Angaben gemäß § 5 TMG:
BreakPilot UG (haftungsbeschränkt)
Musterstraße 1
12345 Musterstadt
Deutschland
Vertreten durch:
Geschäftsführer: Max Mustermann
Kontakt:
Telefon: +49 (0) 123 456789
E-Mail: info@breakpilot.app
Registereintrag:
Eintragung im Handelsregister
Registergericht: Amtsgericht Musterstadt
Registernummer: HRB 12345
Umsatzsteuer-ID:
Umsatzsteuer-Identifikationsnummer gemäß § 27 a UStG: DE123456789
Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV:
Max Mustermann
Musterstraße 1
12345 Musterstadt
',
en: 'Legal Notice
Information according to § 5 TMG:
BreakPilot UG (limited liability)
Musterstraße 1
12345 Musterstadt
Germany
Represented by:
Managing Director: Max Mustermann
Contact:
Phone: +49 (0) 123 456789
Email: info@breakpilot.app
Register entry:
Entry in the commercial register
Register court: Amtsgericht Musterstadt
Register number: HRB 12345
VAT ID:
VAT identification number according to § 27 a UStG: DE123456789
Responsible for content according to § 55 Abs. 2 RStV:
Max Mustermann
Musterstraße 1
12345 Musterstadt
'
};
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 = `
🔔
Keine Benachrichtigungen
`;
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 = `
${icon}
${escapeHtml(n.title)}
${escapeHtml(n.body)}
${timeAgo}
`;
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 `
${escapeHtml(d.document_name)}
${isOverdue ? 'Überfällig' : deadline.toLocaleDateString('de-DE')}
`;
}).join('');
} else if (data.details && data.details.documents) {
docList.innerHTML = data.details.documents.map(doc => `
${escapeHtml(doc)}
Bestätigung erforderlich
`).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 = 'Word-Dokument wird verarbeitet...
';
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 || 'Konvertierung fehlgeschlagen
';
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>/gi, '');
cleanedHtml = cleanedHtml.replace(/<\/?o:[^>]*>/gi, '');
cleanedHtml = cleanedHtml.replace(/<\/?w:[^>]*>/gi, '');
cleanedHtml = cleanedHtml.replace(/<\/?m:[^>]*>/gi, '');
// Clean up empty spans
cleanedHtml = cleanedHtml.replace(/]*>\s*<\/span>/gi, '');
// Convert Word list markers to proper lists
cleanedHtml = cleanedHtml.replace(/]*>\s*[•·]\s*/gi, '
');
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 = 'Lade Dokumente...
';
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 = 'Fehler beim Laden der Dokumente.
';
}
}
function renderDocumentsTable() {
const container = document.getElementById('admin-doc-table-container');
// Alle Dokumente anzeigen
const allDocs = adminDocuments;
if (allDocs.length === 0) {
container.innerHTML = 'Keine Dokumente vorhanden. Klicken Sie auf "+ Neues Dokument" um ein Dokument zu erstellen.
';
return;
}
const typeLabels = {
'terms': 'AGB',
'privacy': 'Datenschutz',
'cookies': 'Cookies',
'community': 'Community',
'imprint': 'Impressum'
};
const html = `
| Typ |
Name |
Beschreibung |
Status |
Aktionen |
${allDocs.map(doc => `
| ${typeLabels[doc.type] || doc.type} |
${doc.name} |
${doc.description || '-'} |
${doc.is_active ? 'Aktiv' : 'Inaktiv'}
${doc.is_mandatory ? 'Pflicht' : ''}
|
|
`).join('')}
`;
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 = '' +
adminDocuments.filter(d => d.is_active).map(doc =>
``
).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 = 'Wählen Sie ein Dokument aus.
';
btnNew.disabled = true;
return;
}
btnNew.disabled = false;
container.innerHTML = 'Lade Versionen...
';
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 = 'Fehler beim Laden der Versionen.
';
}
}
function renderVersionsTable(versions) {
const container = document.getElementById('admin-version-table-container');
if (versions.length === 0) {
container.innerHTML = 'Keine Versionen vorhanden.
';
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 = `
| Version |
Sprache |
Titel |
Status |
Aktionen |
${versions.map(v => `
| ${v.version} |
${v.language.toUpperCase()} |
${v.title} |
${getStatusBadge(v.status)}
${v.scheduled_publish_at ? ` Geplant: ${formatScheduledDate(v.scheduled_publish_at)}` : ''}
|
${v.status === 'draft' ? `
` : ''}
${v.status === 'review' ? `
` : ''}
${v.status === 'rejected' ? `
` : ''}
${v.status === 'scheduled' ? `
Wartet auf Veröffentlichung
` : ''}
${v.status === 'approved' ? `
` : ''}
${v.status === 'published' ? `
` : ''}
|
`).join('')}
`;
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
: 'Keine veröffentlichte Version vorhanden
';
rightPanel.innerHTML = currentVersion.content || 'Kein Inhalt
';
// Populate history
const historyContainer = document.getElementById('compare-history-container');
if (history.length > 0) {
historyContainer.innerHTML = `
Genehmigungsverlauf
${history.map(h => `
${h.action} von ${h.approver || 'System'}
(${new Date(h.created_at).toLocaleString('de-DE')})
${h.comment ? ': ' + h.comment : ''}
`).join(' | ')}
`;
} 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 += ``;
}
// Status-specific actions
if (status === 'draft') {
buttons += ``;
}
if (status === 'review') {
buttons += ``;
buttons += ``;
}
if (status === 'approved') {
buttons += ``;
}
// Delete button for draft/rejected
if (status === 'draft' || status === 'rejected') {
buttons += ``;
}
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
? 'Keine Genehmigungshistorie vorhanden.
'
: `
| Aktion |
Benutzer |
Kommentar |
Datum |
${history.map(h => `
| ${h.action} |
${h.approver || h.name || '-'} |
${h.comment || '-'} |
${new Date(h.created_at).toLocaleString('de-DE')} |
`).join('')}
`;
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 = `
${content}
${buttons.length > 0 ? `
` : ''}
`;
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 = 'Lade Cookie-Kategorien...
';
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 = 'Fehler beim Laden der Kategorien.
';
}
}
function renderCookieCategoriesTable() {
const container = document.getElementById('admin-cookie-table-container');
if (adminCookieCategories.length === 0) {
container.innerHTML = 'Keine Cookie-Kategorien vorhanden.
';
return;
}
const html = `
| Name |
Anzeigename (DE) |
Typ |
Aktionen |
${adminCookieCategories.map(cat => `
${cat.name} |
${cat.display_name_de} |
${cat.is_mandatory ? 'Notwendig' : 'Optional'}
|
${!cat.is_mandatory ? `` : ''}
|
`).join('')}
`;
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 = 'Lade Statistiken & DSGVO-Informationen...
';
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 = 'Fehler beim Laden: ' + e.message + '
';
}
}
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 = `
`;
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 = 'Generiere PDF...';
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 = '✓ PDF erfolgreich generiert!';
} catch(e) {
statusDiv.innerHTML = `Fehler: ${e.message}`;
}
}
async function previewUserDataHtml() {
const statusDiv = document.getElementById('gdpr-export-status');
statusDiv.innerHTML = 'Lade Vorschau...';
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 = '✓ Vorschau in neuem Tab geöffnet';
} catch(e) {
statusDiv.innerHTML = `Fehler: ${e.message}`;
}
}
// ==========================================
// 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 = `
Überfällig
${stats.overdue_requests || 0}
In Bearbeitung
${stats.pending_requests || 0}
Diesen Monat abgeschlossen
${stats.completed_this_month || 0}
Gesamt
${stats.total_requests || 0}
Ø Bearbeitungszeit
${(stats.average_processing_days || 0).toFixed(1)} Tage
`;
} catch(e) {
container.innerHTML = `Fehler beim Laden der Statistiken: ${e.message}
`;
}
}
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 = 'Lade Betroffenenanfragen...
';
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 = `
📋
Keine Betroffenenanfragen gefunden.
`;
return;
}
container.innerHTML = `
| Nr. |
Typ |
Antragsteller |
Status |
Priorität |
Frist |
Erstellt |
|
${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 `
| ${dsr.request_number} |
${DSR_TYPE_LABELS[dsr.request_type] || dsr.request_type} |
${dsr.requester_email}
${dsr.requester_name ? `${dsr.requester_name} ` : ''}
|
${DSR_STATUS_LABELS[dsr.status] || dsr.status}
|
${dsr.priority === 'expedited' ? '🔴' : dsr.priority === 'high' ? '🟡' : ''}
${dsr.priority === 'expedited' ? 'Beschleunigt' : dsr.priority === 'high' ? 'Hoch' : 'Normal'}
|
${deadlineDate}${isOverdue ? ' ⚠️' : ''} |
${new Date(dsr.created_at).toLocaleDateString('de-DE')} |
|
`;
}).join('')}
${data.total || dsrList.length} Anfragen gefunden
`;
} catch(e) {
container.innerHTML = `Fehler: ${e.message}
`;
}
}
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 = `
${currentDSR.request_number}
${DSR_STATUS_LABELS[currentDSR.status] || currentDSR.status}
Anfragetyp
${DSR_TYPE_LABELS[currentDSR.request_type] || currentDSR.request_type}
Priorität
${currentDSR.priority === 'expedited' ? '🔴 Beschleunigt' : currentDSR.priority === 'high' ? '🟡 Hoch' : 'Normal'}
Frist
${new Date(currentDSR.deadline_at).toLocaleDateString('de-DE')} ${isOverdue ? '⚠️ ÜBERFÄLLIG' : ''}
Gesetzliche Frist
${currentDSR.legal_deadline_days} Tage
Identität verifiziert
${currentDSR.identity_verified ? '✅ Ja' : '❌ Nein'}
Quelle
${currentDSR.source === 'api' ? 'API' : currentDSR.source === 'admin_panel' ? 'Admin Panel' : currentDSR.source}
Antragsteller
E-Mail
${currentDSR.requester_email}
Name
${currentDSR.requester_name || '-'}
Telefon
${currentDSR.requester_phone || '-'}
${currentDSR.processing_notes ? `
Bearbeitungsnotizen
${currentDSR.processing_notes}
` : ''}
${currentDSR.result_summary ? `
Ergebnis
${currentDSR.result_summary}
` : ''}
${currentDSR.rejection_reason ? `
Ablehnung
Rechtsgrundlage: ${currentDSR.rejection_legal_basis}
${currentDSR.rejection_reason}
` : ''}
Verlauf
${(historyData.history || []).map(h => `
${new Date(h.created_at).toLocaleString('de-DE')}
${h.from_status ? `${DSR_STATUS_LABELS[h.from_status] || h.from_status} → ` : ''}
${DSR_STATUS_LABELS[h.to_status] || h.to_status}
${h.comment ? `
${h.comment}
` : ''}
`).join('') || '
Kein Verlauf vorhanden
'}
`;
// 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 = 'Lade DSMS Status...
';
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 = `
Status
${isOnline ? 'Online' : 'Offline'}
Speicher verwendet
${repoSize}
Max. Speicher
${storageMax}
`;
} catch(e) {
container.innerHTML = `
Status
Nicht erreichbar
DSMS Gateway ist nicht verfügbar. Stellen Sie sicher, dass die Container laufen.
`;
}
}
async function loadDsmsArchives() {
const container = document.getElementById('dsms-archives-table');
container.innerHTML = 'Lade archivierte Dokumente...
';
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 = `
Keine archivierten Dokumente vorhanden.
Klicken Sie auf "+ Dokument archivieren" um ein Legal Document im DSMS zu speichern.
`;
return;
}
container.innerHTML = `
| CID |
Dokument |
Version |
Archiviert am |
Aktionen |
${dsmsArchives.map(doc => `
${doc.cid.substring(0, 12)}...
|
${doc.metadata?.document_id || doc.filename || '-'} |
${doc.metadata?.version || '-'} |
${doc.metadata?.created_at ? new Date(doc.metadata.created_at).toLocaleString('de-DE') : '-'} |
↗
|
`).join('')}
`;
} catch(e) {
container.innerHTML = `Fehler: ${e.message}
`;
}
}
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 = '' +
docs.map(d => ``).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 = '';
versionSelect.disabled = true;
return;
}
versionSelect.disabled = false;
versionSelect.innerHTML = '';
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 = '';
return;
}
versionSelect.innerHTML = '' +
versions.map(v => ``).join('');
} catch(e) {
versionSelect.innerHTML = '';
}
}
// 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 = 'Verifiziere...
';
// 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 = `
✓ Dokument verifiziert
CID: ${cid}
Integrität: Gültig
Typ: ${data.metadata?.document_type || '-'}
Dokument-ID: ${data.metadata?.document_id || '-'}
Version: ${data.metadata?.version || '-'}
Erstellt: ${data.metadata?.created_at ? new Date(data.metadata.created_at).toLocaleString('de-DE') : '-'}
Checksum: ${data.stored_checksum || '-'}
`;
} else if (data.exists && !data.integrity_valid) {
resultDiv.innerHTML = `
⚠ Integritätsfehler
Das Dokument existiert, aber die Prüfsumme stimmt nicht überein.
Gespeichert: ${data.stored_checksum}
Berechnet: ${data.calculated_checksum}
`;
} else {
resultDiv.innerHTML = `
✗ Nicht gefunden
Kein Dokument mit diesem CID gefunden.
${data.error ? `
${data.error}
` : ''}
`;
}
} catch(e) {
resultDiv.innerHTML = `
`;
}
}
async function loadDsmsNodeInfo() {
const container = document.getElementById('dsms-node-info');
container.innerHTML = 'Lade Node-Info...
';
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 = `
Node ID: ${info.node_id || '-'}
Agent: ${info.agent_version || '-'}
Repo-Größe: ${info.repo_size ? formatBytes(info.repo_size) : '-'}
Max. Speicher: ${info.storage_max ? formatBytes(info.storage_max) : '-'}
Objekte: ${info.num_objects ?? '-'}
Adressen:
${(info.addresses || []).map(a => `${a} `).join('')}
`;
} catch(e) {
container.innerHTML = `DSMS nicht erreichbar: ${e.message}
`;
}
}
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 = 'Online';
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 => '' + a + '
').join('')
: 'Keine Adressen verfügbar';
// 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 = 'Offline';
}
}
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 = `
🔒
Privates Netzwerk
DSMS läuft als isolierter Node. Keine externen Peers verbunden.
`;
} catch (e) {
container.innerHTML = 'Fehler beim Laden der Peers
';
}
}
// 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 = 'Ergebnisse
' +
results.map(r => `
${r.file}
${r.success
? '
CID: ' + r.cid + '
'
: '
' + r.error + '
'
}
${r.success ? `
` : ''}
`).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 = 'Lade...
';
try {
const res = await fetch(DSMS_GATEWAY_URL + '/api/v1/verify/' + cid);
const data = await res.json();
if (data.exists) {
contentDiv.innerHTML = `
${data.integrity_valid ? '✓' : '✗'}
${data.integrity_valid ? 'Dokument verifiziert' : 'Integritätsfehler'}
| CID |
${cid} |
| Typ |
${data.metadata?.document_type || '--'} |
| Erstellt |
${data.metadata?.created_at ? new Date(data.metadata.created_at).toLocaleString('de-DE') : '--'} |
| Checksum |
${data.stored_checksum || '--'} |
`;
} else {
contentDiv.innerHTML = `
Nicht gefunden
CID existiert nicht im DSMS: ${cid}
`;
}
} catch (e) {
contentDiv.innerHTML = `
Fehler
${e.message}
`;
}
}
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 = '';
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 = 'Wählen Sie eine E-Mail-Vorlage aus, um deren Versionen anzuzeigen.
';
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 = 'Lade Versionen...
';
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 = 'Fehler beim Laden der Versionen.
';
}
}
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 = 'Keine Versionen vorhanden. Erstellen Sie eine neue Version.
';
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 = `
| Version |
Sprache |
Betreff |
Status |
Aktualisiert |
Aktionen |
${emailTemplateVersions.map(v => `
| ${v.version} |
${v.language === 'de' ? '🇩🇪 DE' : '🇬🇧 EN'} |
${v.subject} |
${statusNames[v.status] || v.status} |
${new Date(v.updated_at).toLocaleDateString('de-DE')} |
${v.status === 'draft' ? `
` : ''}
${v.status === 'review' ? `
` : ''}
${v.status === 'approved' ? `
` : ''}
|
`).join('')}
`;
}
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, `${text}`);
}
}
function insertEmailButton() {
const url = prompt('Button-Link:');
if (url) {
const text = prompt('Button-Text:', 'Klicken');
const buttonHtml = ``;
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 = '';
(classes || []).forEach(c => {
select.innerHTML += ``;
});
}
} 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 = '';
(subjects || []).forEach(s => {
select.innerHTML += ``;
});
}
} 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 = '';
(years || []).forEach(y => {
select.innerHTML += ``;
});
}
} 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 = 'Keine Klassen vorhanden. Erstellen Sie eine neue Klasse.
';
return;
}
container.innerHTML = data.classes.map(c => `
Klasse ${c.grade_level}
${c.student_count || 0} Schüler
`).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 = `
${students.length === 0 ? '
Keine Schüler vorhanden
' :
students.map(s => `
${s.last_name}, ${s.first_name}
${s.student_number || ''}
`).join('')
}
`;
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 = `
`;
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 = `
`;
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 = `
CSV-Format: Vorname,Nachname[,Geburtsdatum][,Schülernummer]
`;
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 = 'Keine Fächer vorhanden
';
return;
}
container.innerHTML = data.subjects.map(s => `
${s.name}
${s.short_name || ''}
${s.is_main_subject ? 'Hauptfach' : 'Nebenfach'}
`).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 = `
`;
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 = 'Keine Klausuren vorhanden
';
return;
}
container.innerHTML = data.exams.map(e => `
${e.exam_type}
${e.exam_date || 'Kein Datum'}
${e.max_points} Punkte
`).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 = `
`;
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 = `
Max. Punkte: ${exam.max_points}
`;
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 = 'Keine Noten vorhanden
';
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 = `
| Schüler |
${subjectList.map(s => `${s} | `).join('')}
Durchschnitt |
${data.grades.map(g => {
const avg = g.subjects?.reduce((sum, s) => sum + (s.final_grade || 0), 0) / (g.subjects?.length || 1);
return `
| ${g.student_name} |
${subjectList.map(subj => {
const s = g.subjects?.find(x => x.subject_name === subj);
return `${s?.final_grade?.toFixed(1) || '-'} | `;
}).join('')}
${avg.toFixed(2)} |
`;
}).join('')}
`;
} 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 = `
Fehlzeiten
| Schüler | Datum | Status | Stunden |
${(attendance.attendance || []).slice(0, 20).map(a => `
| ${a.student_name} |
${a.date} |
${a.status === 'absent_excused' ? 'entschuldigt' : a.status === 'absent_unexcused' ? 'unentschuldigt' : a.status} |
${a.periods} |
`).join('')}
Einträge
${(entries.entries || []).slice(0, 10).map(e => `
${e.date}
${e.entry_type}
${e.content}
`).join('')}
`;
} 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 = `
`;
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 = `
`;
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 = 'Keine Zeugnisse vorhanden
';
return;
}
container.innerHTML = data.certificates.map(c => `
${c.student_name}
${c.status}
${c.status === 'draft' ? `` : ''}
`).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 =>
``
).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 = `
`;
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 =>
``
).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 = `
`;
}
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 = `
`;
}
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 = `
${title}
${formattedTime}
`;
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 = `
${input.value}
${new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
`;
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 = `
✓
${examUploadedFiles.length} Datei(en) ausgewählt
${examUploadedFiles.map(f => f.name).join(', ')}
`;
}
// 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 = `
✓
Korrektur abgeschlossen
Die OCR-Erkennung und automatische Bewertung wurde durchgeführt.
Bitte überprüfe die Ergebnisse im Review-Bereich.
`;
}
}
// 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 = `
${message}
`;
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();
}
});
}
});