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 += '
'; html += '
' + (t('qa_your_answer') || 'Deine Antwort') + ':
'; html += ''; html += '
'; // Prüfen-Button html += '
'; html += ''; html += '
'; // Vergleichs-Container (versteckt) 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):

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):

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 = ` ${allDocs.map(doc => ` `).join('')}
    Typ Name Beschreibung Status Aktionen
    ${typeLabels[doc.type] || doc.type} ${doc.name} ${doc.description || '-'} ${doc.is_active ? 'Aktiv' : 'Inaktiv'} ${doc.is_mandatory ? 'Pflicht' : ''}
    `; 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 = ` ${versions.map(v => ` `).join('')}
    Version Sprache Titel Status Aktionen
    ${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' ? ` ` : ''}
    `; 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.

    ' : ` ${history.map(h => ` `).join('')}
    Aktion Benutzer Kommentar Datum
    ${h.action} ${h.approver || h.name || '-'} ${h.comment || '-'} ${new Date(h.created_at).toLocaleString('de-DE')}
    `; 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 = ` `; 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 = ` ${adminCookieCategories.map(cat => ` `).join('')}
    Name Anzeigename (DE) Typ Aktionen
    ${cat.name} ${cat.display_name_de} ${cat.is_mandatory ? 'Notwendig' : 'Optional'} ${!cat.is_mandatory ? `` : ''}
    `; 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 = `

    📋 DSGVO-Datenauskunft (Art. 15)

    Exportieren Sie alle personenbezogenen Daten eines Nutzers als PDF-Dokument. Dies erfüllt die Anforderungen der DSGVO Art. 15 (Auskunftsrecht).

    🗄️ Datenkategorien & Löschfristen

    Essentielle Daten (Pflicht für Betrieb)

    ${essential.map(cat => ` `).join('')}
    Kategorie Beschreibung Löschfrist Rechtsgrundlage
    ${cat.name_de} ${cat.description_de} ${cat.retention_period} ${cat.legal_basis}

    Optionale Daten (nur bei Einwilligung)

    ${optional.map(cat => ` `).join('')}
    Kategorie Beschreibung Cookie-Kategorie Löschfrist
    ${cat.name_de} ${cat.description_de} ${cat.cookie_category || '-'} ${cat.retention_period}
    ${dataCategories.length}
    Datenkategorien
    ${essential.length}
    Essentiell
    ${optional.length}
    Optional (Opt-in)
    `; 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 = ` ${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 ` `; }).join('')}
    Nr. Typ Antragsteller Status Priorität Frist Erstellt
    ${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')}
    ${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}

    Objekte

    ${numObjects}
    `; } 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 = ` ${dsmsArchives.map(doc => ` `).join('')}
    CID Dokument Version Archiviert am Aktionen
    ${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') : '-'}
    `; } 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 = `

    Fehler

    ${e.message}

    `; } } 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 || '--'}
    Im Gateway öffnen
    `; } 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 = ` ${emailTemplateVersions.map(v => ` `).join('')}
    Version Sprache Betreff Status Aktualisiert Aktionen
    ${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' ? ` ` : ''}
    `; } 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 = `
    ${text}
    `; 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 => `

    ${c.name}

    ${c.school_type || 'Gymnasium'}
    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 = ` `; 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 = ` `; 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.title}

    ${e.status}
    ${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 = ` `; 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 = ` ${subjectList.map(s => ``).join('')} ${data.grades.map(g => { const avg = g.subjects?.reduce((sum, s) => sum + (s.final_grade || 0), 0) / (g.subjects?.length || 1); return ` ${subjectList.map(subj => { const s = g.subjects?.find(x => x.subject_name === subj); return ``; }).join('')} `; }).join('')}
    Schüler${s}Durchschnitt
    ${g.student_name}${s?.final_grade?.toFixed(1) || '-'}${avg.toFixed(2)}
    `; } 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

    ${(attendance.attendance || []).slice(0, 20).map(a => ` `).join('')}
    SchülerDatumStatusStunden
    ${a.student_name} ${a.date} ${a.status === 'absent_excused' ? 'entschuldigt' : a.status === 'absent_unexcused' ? 'unentschuldigt' : a.status} ${a.periods}

    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 = `
    L
    Sie (Lehrer)
    Moderator
    `; } 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 = `
    Sie Jetzt
    ${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(); } }); } });