A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9788 lines
393 KiB
JavaScript
9788 lines
393 KiB
JavaScript
console.log('studio.js loading...');
|
||
|
||
// ==========================================
|
||
// THEME TOGGLE (Dark/Light Mode)
|
||
// ==========================================
|
||
(function() {
|
||
const savedTheme = localStorage.getItem('bp-theme') || 'dark';
|
||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||
console.log('Initial theme set to:', savedTheme);
|
||
})();
|
||
|
||
function initThemeToggle() {
|
||
const toggle = document.getElementById('theme-toggle');
|
||
const icon = document.getElementById('theme-icon');
|
||
const label = document.getElementById('theme-label');
|
||
|
||
if (!toggle || !icon || !label) {
|
||
console.warn('Theme toggle elements not found');
|
||
return;
|
||
}
|
||
|
||
function updateToggleUI(theme) {
|
||
if (theme === 'light') {
|
||
icon.textContent = '☀️';
|
||
label.textContent = 'Light';
|
||
} else {
|
||
icon.textContent = '🌙';
|
||
label.textContent = 'Dark';
|
||
}
|
||
}
|
||
|
||
// Initialize UI based on current theme
|
||
const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
|
||
updateToggleUI(currentTheme);
|
||
|
||
toggle.addEventListener('click', function() {
|
||
console.log('Theme toggle clicked');
|
||
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
||
const newTheme = current === 'dark' ? 'light' : 'dark';
|
||
console.log('Switching from', current, 'to', newTheme);
|
||
|
||
document.documentElement.setAttribute('data-theme', newTheme);
|
||
localStorage.setItem('bp-theme', newTheme);
|
||
updateToggleUI(newTheme);
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// INTERNATIONALISIERUNG (i18n)
|
||
// ==========================================
|
||
const translations = {
|
||
de: {
|
||
// Navigation & Header
|
||
brand_sub: "Studio",
|
||
nav_compare: "Arbeitsblätter",
|
||
nav_tiles: "Lern-Kacheln",
|
||
login: "Login / Anmeldung",
|
||
mvp_local: "MVP · Lokal auf deinem Mac",
|
||
|
||
// Sidebar
|
||
sidebar_areas: "Bereiche",
|
||
sidebar_studio: "Arbeitsblatt Studio",
|
||
sidebar_active: "aktiv",
|
||
sidebar_parents: "Eltern-Kanal",
|
||
sidebar_soon: "demnächst",
|
||
sidebar_correction: "Korrektur / Noten",
|
||
sidebar_units: "Lerneinheiten (lokal)",
|
||
input_student: "Schüler/in",
|
||
input_subject: "Fach",
|
||
input_grade: "Klasse (z.B. 7a)",
|
||
input_unit_title: "Lerneinheit / Thema",
|
||
btn_create: "Anlegen",
|
||
btn_add_current: "Aktuelles Arbeitsblatt hinzufügen",
|
||
btn_filter_unit: "Nur Lerneinheit",
|
||
btn_filter_all: "Alle Dateien",
|
||
|
||
// Screen 1 - Compare
|
||
uploaded_worksheets: "Hochgeladene Arbeitsblätter",
|
||
files: "Dateien",
|
||
btn_upload: "Hochladen",
|
||
btn_delete: "Löschen",
|
||
original_scan: "Original-Scan",
|
||
cleaned_version: "Bereinigt (Handschrift entfernt)",
|
||
no_cleaned: "Noch keine bereinigte Version vorhanden.",
|
||
process_hint: "Klicke auf 'Verarbeiten', um das Arbeitsblatt zu analysieren und zu bereinigen.",
|
||
worksheet_print: "Drucken",
|
||
worksheet_no_data: "Keine Arbeitsblatt-Daten vorhanden.",
|
||
btn_full_process: "Verarbeiten (Analyse + Bereinigung + HTML)",
|
||
btn_original_generate: "Nur Original-HTML generieren",
|
||
|
||
// Screen 2 - Tiles
|
||
learning_unit: "Lerneinheit",
|
||
no_unit_selected: "Keine Lerneinheit ausgewählt",
|
||
|
||
// MC Tile
|
||
mc_title: "Multiple Choice Test",
|
||
mc_ready: "Bereit",
|
||
mc_generating: "Generiert...",
|
||
mc_done: "Fertig",
|
||
mc_error: "Fehler",
|
||
mc_desc: "Erzeugt passende MC-Aufgaben zur ursprünglichen Schwierigkeit (z. B. Klasse 7), ohne das Niveau zu verändern.",
|
||
mc_generate: "MC generieren",
|
||
mc_show: "Fragen anzeigen",
|
||
mc_quiz_title: "Multiple Choice Quiz",
|
||
mc_evaluate: "Auswerten",
|
||
mc_correct: "Richtig!",
|
||
mc_incorrect: "Leider falsch.",
|
||
mc_not_answered: "Nicht beantwortet. Richtig wäre:",
|
||
mc_result: "von",
|
||
mc_result_correct: "richtig",
|
||
mc_percent: "korrekt",
|
||
mc_no_questions: "Noch keine MC-Fragen für dieses Arbeitsblatt generiert.",
|
||
mc_print: "Drucken",
|
||
mc_print_with_answers: "Mit Lösungen drucken?",
|
||
|
||
// Cloze Tile
|
||
cloze_title: "Lückentext",
|
||
cloze_desc: "Erzeugt Lückentexte mit mehreren sinnvollen Lücken pro Satz. Inkl. Übersetzung für Eltern.",
|
||
cloze_translation: "Übersetzung:",
|
||
cloze_generate: "Lückentext generieren",
|
||
cloze_start: "Übung starten",
|
||
cloze_exercise_title: "Lückentext-Übung",
|
||
cloze_instruction: "Fülle die Lücken aus und klicke auf 'Prüfen'.",
|
||
cloze_check: "Prüfen",
|
||
cloze_show_answers: "Lösungen zeigen",
|
||
cloze_no_texts: "Noch keine Lückentexte für dieses Arbeitsblatt generiert.",
|
||
cloze_sentences: "Sätze",
|
||
cloze_gaps: "Lücken",
|
||
cloze_gaps_total: "Lücken gesamt",
|
||
cloze_with_gaps: "(mit Lücken)",
|
||
cloze_print: "Drucken",
|
||
cloze_print_with_answers: "Mit Lösungen drucken?",
|
||
|
||
// QA Tile
|
||
qa_title: "Frage-Antwort-Blatt",
|
||
qa_desc: "Frage-Antwort-Paare mit Leitner-Box System. Wiederholung nach Schwierigkeitsgrad.",
|
||
qa_generate: "Q&A generieren",
|
||
qa_learn: "Lernen starten",
|
||
qa_print: "Drucken",
|
||
qa_no_questions: "Noch keine Q&A für dieses Arbeitsblatt generiert.",
|
||
qa_box_new: "Neu",
|
||
qa_box_learning: "Gelernt",
|
||
qa_box_mastered: "Gefestigt",
|
||
qa_show_answer: "Antwort zeigen",
|
||
qa_your_answer: "Deine Antwort",
|
||
qa_type_answer: "Schreibe deine Antwort hier...",
|
||
qa_check_answer: "Antwort prüfen",
|
||
qa_correct_answer: "Richtige Antwort",
|
||
qa_self_evaluate: "War deine Antwort richtig?",
|
||
qa_no_answer: "(keine Antwort eingegeben)",
|
||
qa_correct: "Richtig",
|
||
qa_incorrect: "Falsch",
|
||
qa_key_terms: "Schlüsselbegriffe",
|
||
qa_session_correct: "Richtig",
|
||
qa_session_incorrect: "Falsch",
|
||
qa_session_complete: "Lernrunde abgeschlossen!",
|
||
qa_result_correct: "richtig",
|
||
qa_restart: "Nochmal lernen",
|
||
qa_print_with_answers: "Mit Lösungen drucken?",
|
||
question: "Frage",
|
||
answer: "Antwort",
|
||
status_generating_qa: "Generiere Q&A …",
|
||
status_qa_generated: "Q&A generiert",
|
||
|
||
// Common
|
||
close: "Schließen",
|
||
subject: "Fach",
|
||
grade: "Stufe",
|
||
questions: "Fragen",
|
||
worksheet: "Arbeitsblatt",
|
||
loading: "Lädt...",
|
||
error: "Fehler",
|
||
success: "Erfolgreich",
|
||
|
||
// Footer
|
||
imprint: "Impressum",
|
||
privacy: "Datenschutz",
|
||
contact: "Kontakt",
|
||
|
||
// Status messages
|
||
status_ready: "Bereit",
|
||
status_processing: "Verarbeitet...",
|
||
status_generating_mc: "Generiere MC-Fragen …",
|
||
status_generating_cloze: "Generiere Lückentexte …",
|
||
status_please_wait: "Bitte warten, KI arbeitet.",
|
||
status_mc_generated: "MC-Fragen generiert",
|
||
status_cloze_generated: "Lückentexte generiert",
|
||
status_files_created: "Dateien erstellt",
|
||
|
||
// Mindmap Tile
|
||
mindmap_title: "Mindmap Lernposter",
|
||
mindmap_desc: "Erstellt eine kindgerechte Mindmap mit dem Hauptthema in der Mitte und allen Fachbegriffen in farbigen Kategorien.",
|
||
mindmap_generate: "Mindmap erstellen",
|
||
mindmap_show: "Ansehen",
|
||
mindmap_print_a3: "A3 Drucken",
|
||
generating_mindmap: "Erstelle Mindmap...",
|
||
mindmap_generated: "Mindmap erstellt!",
|
||
no_analysis: "Keine Analyse",
|
||
analyze_first: "Bitte zuerst analysieren (Verarbeiten starten)",
|
||
categories: "Kategorien",
|
||
terms: "Begriffe",
|
||
},
|
||
|
||
tr: {
|
||
brand_sub: "Stüdyo",
|
||
nav_compare: "Çalışma Sayfaları",
|
||
nav_tiles: "Öğrenme Kartları",
|
||
login: "Giriş / Kayıt",
|
||
mvp_local: "MVP · Mac'inizde Yerel",
|
||
|
||
sidebar_areas: "Alanlar",
|
||
sidebar_studio: "Çalışma Sayfası Stüdyosu",
|
||
sidebar_active: "aktif",
|
||
sidebar_parents: "Ebeveyn Kanalı",
|
||
sidebar_soon: "yakında",
|
||
sidebar_correction: "Düzeltme / Notlar",
|
||
sidebar_units: "Öğrenme Birimleri (yerel)",
|
||
input_student: "Öğrenci",
|
||
input_subject: "Ders",
|
||
input_grade: "Sınıf (örn. 7a)",
|
||
input_unit_title: "Öğrenme Birimi / Konu",
|
||
btn_create: "Oluştur",
|
||
btn_add_current: "Mevcut çalışma sayfasını ekle",
|
||
btn_filter_unit: "Sadece Birim",
|
||
btn_filter_all: "Tüm Dosyalar",
|
||
|
||
uploaded_worksheets: "Yüklenen Çalışma Sayfaları",
|
||
files: "Dosya",
|
||
btn_upload: "Yükle",
|
||
btn_delete: "Sil",
|
||
original_scan: "Orijinal Tarama",
|
||
cleaned_version: "Temizlenmiş (El yazısı kaldırıldı)",
|
||
no_cleaned: "Henüz temizlenmiş sürüm yok.",
|
||
process_hint: "Çalışma sayfasını analiz etmek ve temizlemek için 'İşle'ye tıklayın.",
|
||
worksheet_print: "Yazdır",
|
||
worksheet_no_data: "Çalışma sayfası verisi yok.",
|
||
btn_full_process: "İşle (Analiz + Temizleme + HTML)",
|
||
btn_original_generate: "Sadece Orijinal HTML Oluştur",
|
||
|
||
learning_unit: "Öğrenme Birimi",
|
||
no_unit_selected: "Öğrenme birimi seçilmedi",
|
||
|
||
mc_title: "Çoktan Seçmeli Test",
|
||
mc_ready: "Hazır",
|
||
mc_generating: "Oluşturuluyor...",
|
||
mc_done: "Tamamlandı",
|
||
mc_error: "Hata",
|
||
mc_desc: "Orijinal zorluğa uygun (örn. 7. sınıf) çoktan seçmeli sorular oluşturur.",
|
||
mc_generate: "ÇS Oluştur",
|
||
mc_show: "Soruları Göster",
|
||
mc_quiz_title: "Çoktan Seçmeli Quiz",
|
||
mc_evaluate: "Değerlendir",
|
||
mc_correct: "Doğru!",
|
||
mc_incorrect: "Maalesef yanlış.",
|
||
mc_not_answered: "Cevaplanmadı. Doğru cevap:",
|
||
mc_result: "/",
|
||
mc_result_correct: "doğru",
|
||
mc_percent: "doğru",
|
||
mc_no_questions: "Bu çalışma sayfası için henüz ÇS sorusu oluşturulmadı.",
|
||
mc_print: "Yazdır",
|
||
mc_print_with_answers: "Cevaplarla yazdır?",
|
||
|
||
cloze_title: "Boşluk Doldurma",
|
||
cloze_desc: "Her cümlede birden fazla anlamlı boşluk içeren metinler oluşturur. Ebeveynler için çeviri dahil.",
|
||
cloze_translation: "Çeviri:",
|
||
cloze_generate: "Boşluk Metni Oluştur",
|
||
cloze_start: "Alıştırmayı Başlat",
|
||
cloze_exercise_title: "Boşluk Doldurma Alıştırması",
|
||
cloze_instruction: "Boşlukları doldurun ve 'Kontrol Et'e tıklayın.",
|
||
cloze_check: "Kontrol Et",
|
||
cloze_show_answers: "Cevapları Göster",
|
||
cloze_no_texts: "Bu çalışma sayfası için henüz boşluk metni oluşturulmadı.",
|
||
cloze_sentences: "Cümle",
|
||
cloze_gaps: "Boşluk",
|
||
cloze_gaps_total: "Toplam boşluk",
|
||
cloze_with_gaps: "(boşluklu)",
|
||
cloze_print: "Yazdır",
|
||
cloze_print_with_answers: "Cevaplarla yazdır?",
|
||
|
||
qa_title: "Soru-Cevap Sayfası",
|
||
qa_desc: "Leitner kutu sistemiyle soru-cevap çiftleri. Zorluk derecesine göre tekrar.",
|
||
qa_generate: "S&C Oluştur",
|
||
qa_learn: "Öğrenmeye Başla",
|
||
qa_print: "Yazdır",
|
||
qa_no_questions: "Bu çalışma sayfası için henüz S&C oluşturulmadı.",
|
||
qa_box_new: "Yeni",
|
||
qa_box_learning: "Öğreniliyor",
|
||
qa_box_mastered: "Pekiştirildi",
|
||
qa_show_answer: "Cevabı Göster",
|
||
qa_your_answer: "Senin Cevabın",
|
||
qa_type_answer: "Cevabını buraya yaz...",
|
||
qa_check_answer: "Cevabı Kontrol Et",
|
||
qa_correct_answer: "Doğru Cevap",
|
||
qa_self_evaluate: "Cevabın doğru muydu?",
|
||
qa_no_answer: "(cevap girilmedi)",
|
||
qa_correct: "Doğru",
|
||
qa_incorrect: "Yanlış",
|
||
qa_key_terms: "Anahtar Kavramlar",
|
||
qa_session_correct: "Doğru",
|
||
qa_session_incorrect: "Yanlış",
|
||
qa_session_complete: "Öğrenme turu tamamlandı!",
|
||
qa_result_correct: "doğru",
|
||
qa_restart: "Tekrar Öğren",
|
||
qa_print_with_answers: "Cevaplarla yazdır?",
|
||
question: "Soru",
|
||
answer: "Cevap",
|
||
status_generating_qa: "S&C oluşturuluyor…",
|
||
status_qa_generated: "S&C oluşturuldu",
|
||
|
||
close: "Kapat",
|
||
subject: "Ders",
|
||
grade: "Seviye",
|
||
questions: "Soru",
|
||
worksheet: "Çalışma Sayfası",
|
||
loading: "Yükleniyor...",
|
||
error: "Hata",
|
||
success: "Başarılı",
|
||
|
||
imprint: "Künye",
|
||
privacy: "Gizlilik",
|
||
contact: "İletişim",
|
||
|
||
status_ready: "Hazır",
|
||
status_processing: "İşleniyor...",
|
||
status_generating_mc: "ÇS soruları oluşturuluyor…",
|
||
status_generating_cloze: "Boşluk metinleri oluşturuluyor…",
|
||
status_please_wait: "Lütfen bekleyin, yapay zeka çalışıyor.",
|
||
status_mc_generated: "ÇS soruları oluşturuldu",
|
||
status_cloze_generated: "Boşluk metinleri oluşturuldu",
|
||
status_files_created: "dosya oluşturuldu",
|
||
|
||
// Mindmap Tile
|
||
mindmap_title: "Zihin Haritası Poster",
|
||
mindmap_desc: "Ana konuyu ortada ve tüm terimleri renkli kategorilerde gösteren çocuk dostu bir zihin haritası oluşturur.",
|
||
mindmap_generate: "Zihin Haritası Oluştur",
|
||
mindmap_show: "Görüntüle",
|
||
mindmap_print_a3: "A3 Yazdır",
|
||
generating_mindmap: "Zihin haritası oluşturuluyor...",
|
||
mindmap_generated: "Zihin haritası oluşturuldu!",
|
||
no_analysis: "Analiz yok",
|
||
analyze_first: "Lütfen önce analiz edin (İşle'ye tıklayın)",
|
||
categories: "Kategoriler",
|
||
terms: "Terimler",
|
||
},
|
||
|
||
ar: {
|
||
brand_sub: "ستوديو",
|
||
nav_compare: "أوراق العمل",
|
||
nav_tiles: "بطاقات التعلم",
|
||
login: "تسجيل الدخول / التسجيل",
|
||
mvp_local: "MVP · محلي على جهازك",
|
||
|
||
sidebar_areas: "الأقسام",
|
||
sidebar_studio: "استوديو أوراق العمل",
|
||
sidebar_active: "نشط",
|
||
sidebar_parents: "قناة الوالدين",
|
||
sidebar_soon: "قريباً",
|
||
sidebar_correction: "التصحيح / الدرجات",
|
||
sidebar_units: "وحدات التعلم (محلية)",
|
||
input_student: "الطالب/ة",
|
||
input_subject: "المادة",
|
||
input_grade: "الصف (مثل 7أ)",
|
||
input_unit_title: "وحدة التعلم / الموضوع",
|
||
btn_create: "إنشاء",
|
||
btn_add_current: "إضافة ورقة العمل الحالية",
|
||
btn_filter_unit: "الوحدة فقط",
|
||
btn_filter_all: "جميع الملفات",
|
||
|
||
uploaded_worksheets: "أوراق العمل المحملة",
|
||
files: "ملفات",
|
||
btn_upload: "تحميل",
|
||
btn_delete: "حذف",
|
||
original_scan: "المسح الأصلي",
|
||
cleaned_version: "منظف (تم إزالة الكتابة اليدوية)",
|
||
no_cleaned: "لا توجد نسخة منظفة بعد.",
|
||
process_hint: "انقر على 'معالجة' لتحليل وتنظيف ورقة العمل.",
|
||
worksheet_print: "طباعة",
|
||
worksheet_no_data: "لا توجد بيانات ورقة العمل.",
|
||
btn_full_process: "معالجة (تحليل + تنظيف + HTML)",
|
||
btn_original_generate: "إنشاء HTML الأصلي فقط",
|
||
|
||
learning_unit: "وحدة التعلم",
|
||
no_unit_selected: "لم يتم اختيار وحدة تعلم",
|
||
|
||
mc_title: "اختبار متعدد الخيارات",
|
||
mc_ready: "جاهز",
|
||
mc_generating: "جاري الإنشاء...",
|
||
mc_done: "تم",
|
||
mc_error: "خطأ",
|
||
mc_desc: "ينشئ أسئلة اختيار من متعدد تناسب مستوى الصعوبة الأصلي (مثل الصف 7).",
|
||
mc_generate: "إنشاء أسئلة",
|
||
mc_show: "عرض الأسئلة",
|
||
mc_quiz_title: "اختبار متعدد الخيارات",
|
||
mc_evaluate: "تقييم",
|
||
mc_correct: "صحيح!",
|
||
mc_incorrect: "للأسف خطأ.",
|
||
mc_not_answered: "لم تتم الإجابة. الإجابة الصحيحة:",
|
||
mc_result: "من",
|
||
mc_result_correct: "صحيح",
|
||
mc_percent: "صحيح",
|
||
mc_no_questions: "لم يتم إنشاء أسئلة بعد لورقة العمل هذه.",
|
||
mc_print: "طباعة",
|
||
mc_print_with_answers: "طباعة مع الإجابات؟",
|
||
|
||
cloze_title: "ملء الفراغات",
|
||
cloze_desc: "ينشئ نصوصاً بفراغات متعددة في كل جملة. يشمل الترجمة للوالدين.",
|
||
cloze_translation: "الترجمة:",
|
||
cloze_generate: "إنشاء نص الفراغات",
|
||
cloze_start: "بدء التمرين",
|
||
cloze_exercise_title: "تمرين ملء الفراغات",
|
||
cloze_instruction: "املأ الفراغات وانقر على 'تحقق'.",
|
||
cloze_check: "تحقق",
|
||
cloze_show_answers: "عرض الإجابات",
|
||
cloze_no_texts: "لم يتم إنشاء نصوص فراغات بعد لورقة العمل هذه.",
|
||
cloze_sentences: "جمل",
|
||
cloze_gaps: "فراغات",
|
||
cloze_gaps_total: "إجمالي الفراغات",
|
||
cloze_with_gaps: "(مع فراغات)",
|
||
cloze_print: "طباعة",
|
||
cloze_print_with_answers: "طباعة مع الإجابات؟",
|
||
|
||
qa_title: "ورقة الأسئلة والأجوبة",
|
||
qa_desc: "أزواج أسئلة وأجوبة مع نظام صندوق لايتنر. التكرار حسب الصعوبة.",
|
||
qa_generate: "إنشاء س&ج",
|
||
qa_learn: "بدء التعلم",
|
||
qa_print: "طباعة",
|
||
qa_no_questions: "لم يتم إنشاء س&ج بعد لورقة العمل هذه.",
|
||
qa_box_new: "جديد",
|
||
qa_box_learning: "قيد التعلم",
|
||
qa_box_mastered: "متقن",
|
||
qa_show_answer: "عرض الإجابة",
|
||
qa_your_answer: "إجابتك",
|
||
qa_type_answer: "اكتب إجابتك هنا...",
|
||
qa_check_answer: "تحقق من الإجابة",
|
||
qa_correct_answer: "الإجابة الصحيحة",
|
||
qa_self_evaluate: "هل كانت إجابتك صحيحة؟",
|
||
qa_no_answer: "(لم يتم إدخال إجابة)",
|
||
qa_correct: "صحيح",
|
||
qa_incorrect: "خطأ",
|
||
qa_key_terms: "المصطلحات الرئيسية",
|
||
qa_session_correct: "صحيح",
|
||
qa_session_incorrect: "خطأ",
|
||
qa_session_complete: "اكتملت جولة التعلم!",
|
||
qa_result_correct: "صحيح",
|
||
qa_restart: "تعلم مرة أخرى",
|
||
qa_print_with_answers: "طباعة مع الإجابات؟",
|
||
question: "سؤال",
|
||
answer: "إجابة",
|
||
status_generating_qa: "جاري إنشاء س&ج…",
|
||
status_qa_generated: "تم إنشاء س&ج",
|
||
|
||
close: "إغلاق",
|
||
subject: "المادة",
|
||
grade: "المستوى",
|
||
questions: "أسئلة",
|
||
worksheet: "ورقة العمل",
|
||
loading: "جاري التحميل...",
|
||
error: "خطأ",
|
||
success: "نجاح",
|
||
|
||
imprint: "البصمة",
|
||
privacy: "الخصوصية",
|
||
contact: "اتصل بنا",
|
||
|
||
status_ready: "جاهز",
|
||
status_processing: "جاري المعالجة...",
|
||
status_generating_mc: "جاري إنشاء الأسئلة…",
|
||
status_generating_cloze: "جاري إنشاء نصوص الفراغات…",
|
||
status_please_wait: "يرجى الانتظار، الذكاء الاصطناعي يعمل.",
|
||
status_mc_generated: "تم إنشاء الأسئلة",
|
||
status_cloze_generated: "تم إنشاء نصوص الفراغات",
|
||
status_files_created: "ملفات تم إنشاؤها",
|
||
|
||
// Mindmap Tile
|
||
mindmap_title: "ملصق خريطة ذهنية",
|
||
mindmap_desc: "ينشئ خريطة ذهنية مناسبة للأطفال مع الموضوع الرئيسي في المنتصف وجميع المصطلحات في فئات ملونة.",
|
||
mindmap_generate: "إنشاء خريطة ذهنية",
|
||
mindmap_show: "عرض",
|
||
mindmap_print_a3: "طباعة A3",
|
||
generating_mindmap: "جاري إنشاء الخريطة الذهنية...",
|
||
mindmap_generated: "تم إنشاء الخريطة الذهنية!",
|
||
no_analysis: "لا يوجد تحليل",
|
||
analyze_first: "يرجى التحليل أولاً (انقر على معالجة)",
|
||
categories: "الفئات",
|
||
terms: "المصطلحات",
|
||
},
|
||
|
||
ru: {
|
||
brand_sub: "Студия",
|
||
nav_compare: "Рабочие листы",
|
||
nav_tiles: "Учебные карточки",
|
||
login: "Вход / Регистрация",
|
||
mvp_local: "MVP · Локально на вашем Mac",
|
||
|
||
sidebar_areas: "Разделы",
|
||
sidebar_studio: "Студия рабочих листов",
|
||
sidebar_active: "активно",
|
||
sidebar_parents: "Канал для родителей",
|
||
sidebar_soon: "скоро",
|
||
sidebar_correction: "Проверка / Оценки",
|
||
sidebar_units: "Учебные блоки (локально)",
|
||
input_student: "Ученик",
|
||
input_subject: "Предмет",
|
||
input_grade: "Класс (напр. 7а)",
|
||
input_unit_title: "Учебный блок / Тема",
|
||
btn_create: "Создать",
|
||
btn_add_current: "Добавить текущий лист",
|
||
btn_filter_unit: "Только блок",
|
||
btn_filter_all: "Все файлы",
|
||
|
||
uploaded_worksheets: "Загруженные рабочие листы",
|
||
files: "файлов",
|
||
btn_upload: "Загрузить",
|
||
btn_delete: "Удалить",
|
||
original_scan: "Оригинальный скан",
|
||
cleaned_version: "Очищено (рукопись удалена)",
|
||
no_cleaned: "Очищенная версия пока недоступна.",
|
||
process_hint: "Нажмите 'Обработать' для анализа и очистки листа.",
|
||
worksheet_print: "Печать",
|
||
worksheet_no_data: "Нет данных рабочего листа.",
|
||
btn_full_process: "Обработать (Анализ + Очистка + HTML)",
|
||
btn_original_generate: "Только оригинальный HTML",
|
||
|
||
learning_unit: "Учебный блок",
|
||
no_unit_selected: "Блок не выбран",
|
||
|
||
mc_title: "Тест с выбором ответа",
|
||
mc_ready: "Готово",
|
||
mc_generating: "Создается...",
|
||
mc_done: "Готово",
|
||
mc_error: "Ошибка",
|
||
mc_desc: "Создает вопросы с выбором ответа соответствующей сложности (напр. 7 класс).",
|
||
mc_generate: "Создать тест",
|
||
mc_show: "Показать вопросы",
|
||
mc_quiz_title: "Тест с выбором ответа",
|
||
mc_evaluate: "Оценить",
|
||
mc_correct: "Правильно!",
|
||
mc_incorrect: "К сожалению, неверно.",
|
||
mc_not_answered: "Нет ответа. Правильный ответ:",
|
||
mc_result: "из",
|
||
mc_result_correct: "правильно",
|
||
mc_percent: "верно",
|
||
mc_no_questions: "Вопросы для этого листа еще не созданы.",
|
||
mc_print: "Печать",
|
||
mc_print_with_answers: "Печатать с ответами?",
|
||
|
||
cloze_title: "Текст с пропусками",
|
||
cloze_desc: "Создает тексты с несколькими пропусками в каждом предложении. Включая перевод для родителей.",
|
||
cloze_translation: "Перевод:",
|
||
cloze_generate: "Создать текст",
|
||
cloze_start: "Начать упражнение",
|
||
cloze_exercise_title: "Упражнение с пропусками",
|
||
cloze_instruction: "Заполните пропуски и нажмите 'Проверить'.",
|
||
cloze_check: "Проверить",
|
||
cloze_show_answers: "Показать ответы",
|
||
cloze_no_texts: "Тексты для этого листа еще не созданы.",
|
||
cloze_sentences: "предложений",
|
||
cloze_gaps: "пропусков",
|
||
cloze_gaps_total: "Всего пропусков",
|
||
cloze_with_gaps: "(с пропусками)",
|
||
cloze_print: "Печать",
|
||
cloze_print_with_answers: "Печатать с ответами?",
|
||
|
||
qa_title: "Лист вопросов и ответов",
|
||
qa_desc: "Пары вопрос-ответ с системой Лейтнера. Повторение по уровню сложности.",
|
||
qa_generate: "Создать В&О",
|
||
qa_learn: "Начать обучение",
|
||
qa_print: "Печать",
|
||
qa_no_questions: "В&О для этого листа еще не созданы.",
|
||
qa_box_new: "Новый",
|
||
qa_box_learning: "Изучается",
|
||
qa_box_mastered: "Освоено",
|
||
qa_show_answer: "Показать ответ",
|
||
qa_your_answer: "Твой ответ",
|
||
qa_type_answer: "Напиши свой ответ здесь...",
|
||
qa_check_answer: "Проверить ответ",
|
||
qa_correct_answer: "Правильный ответ",
|
||
qa_self_evaluate: "Твой ответ был правильным?",
|
||
qa_no_answer: "(ответ не введён)",
|
||
qa_correct: "Правильно",
|
||
qa_incorrect: "Неправильно",
|
||
qa_key_terms: "Ключевые термины",
|
||
qa_session_correct: "Правильно",
|
||
qa_session_incorrect: "Неправильно",
|
||
qa_session_complete: "Раунд обучения завершен!",
|
||
qa_result_correct: "правильно",
|
||
qa_restart: "Учить снова",
|
||
qa_print_with_answers: "Печатать с ответами?",
|
||
question: "Вопрос",
|
||
answer: "Ответ",
|
||
status_generating_qa: "Создание В&О…",
|
||
status_qa_generated: "В&О созданы",
|
||
|
||
close: "Закрыть",
|
||
subject: "Предмет",
|
||
grade: "Уровень",
|
||
questions: "вопросов",
|
||
worksheet: "Рабочий лист",
|
||
loading: "Загрузка...",
|
||
error: "Ошибка",
|
||
success: "Успешно",
|
||
|
||
imprint: "Импрессум",
|
||
privacy: "Конфиденциальность",
|
||
contact: "Контакт",
|
||
|
||
status_ready: "Готово",
|
||
status_processing: "Обработка...",
|
||
status_generating_mc: "Создание вопросов…",
|
||
status_generating_cloze: "Создание текстов…",
|
||
status_please_wait: "Пожалуйста, подождите, ИИ работает.",
|
||
status_mc_generated: "Вопросы созданы",
|
||
status_cloze_generated: "Тексты созданы",
|
||
status_files_created: "файлов создано",
|
||
|
||
// Mindmap Tile
|
||
mindmap_title: "Плакат Майнд-карта",
|
||
mindmap_desc: "Создает детскую ментальную карту с главной темой в центре и всеми терминами в цветных категориях.",
|
||
mindmap_generate: "Создать карту",
|
||
mindmap_show: "Просмотр",
|
||
mindmap_print_a3: "Печать A3",
|
||
generating_mindmap: "Создание карты...",
|
||
mindmap_generated: "Карта создана!",
|
||
no_analysis: "Нет анализа",
|
||
analyze_first: "Сначала выполните анализ (нажмите Обработать)",
|
||
categories: "Категории",
|
||
terms: "Термины",
|
||
},
|
||
|
||
uk: {
|
||
brand_sub: "Студія",
|
||
nav_compare: "Робочі аркуші",
|
||
nav_tiles: "Навчальні картки",
|
||
login: "Вхід / Реєстрація",
|
||
mvp_local: "MVP · Локально на вашому Mac",
|
||
|
||
sidebar_areas: "Розділи",
|
||
sidebar_studio: "Студія робочих аркушів",
|
||
sidebar_active: "активно",
|
||
sidebar_parents: "Канал для батьків",
|
||
sidebar_soon: "незабаром",
|
||
sidebar_correction: "Перевірка / Оцінки",
|
||
sidebar_units: "Навчальні блоки (локально)",
|
||
input_student: "Учень",
|
||
input_subject: "Предмет",
|
||
input_grade: "Клас (напр. 7а)",
|
||
input_unit_title: "Навчальний блок / Тема",
|
||
btn_create: "Створити",
|
||
btn_add_current: "Додати поточний аркуш",
|
||
btn_filter_unit: "Лише блок",
|
||
btn_filter_all: "Усі файли",
|
||
|
||
uploaded_worksheets: "Завантажені робочі аркуші",
|
||
files: "файлів",
|
||
btn_upload: "Завантажити",
|
||
btn_delete: "Видалити",
|
||
original_scan: "Оригінальний скан",
|
||
cleaned_version: "Очищено (рукопис видалено)",
|
||
no_cleaned: "Очищена версія ще недоступна.",
|
||
process_hint: "Натисніть 'Обробити' для аналізу та очищення аркуша.",
|
||
worksheet_print: "Друк",
|
||
worksheet_no_data: "Немає даних робочого аркуша.",
|
||
btn_full_process: "Обробити (Аналіз + Очищення + HTML)",
|
||
btn_original_generate: "Лише оригінальний HTML",
|
||
|
||
learning_unit: "Навчальний блок",
|
||
no_unit_selected: "Блок не вибрано",
|
||
|
||
mc_title: "Тест з вибором відповіді",
|
||
mc_ready: "Готово",
|
||
mc_generating: "Створюється...",
|
||
mc_done: "Готово",
|
||
mc_error: "Помилка",
|
||
mc_desc: "Створює питання з вибором відповіді відповідної складності (напр. 7 клас).",
|
||
mc_generate: "Створити тест",
|
||
mc_show: "Показати питання",
|
||
mc_quiz_title: "Тест з вибором відповіді",
|
||
mc_evaluate: "Оцінити",
|
||
mc_correct: "Правильно!",
|
||
mc_incorrect: "На жаль, неправильно.",
|
||
mc_not_answered: "Немає відповіді. Правильна відповідь:",
|
||
mc_result: "з",
|
||
mc_result_correct: "правильно",
|
||
mc_percent: "вірно",
|
||
mc_no_questions: "Питання для цього аркуша ще не створені.",
|
||
mc_print: "Друк",
|
||
mc_print_with_answers: "Друкувати з відповідями?",
|
||
|
||
cloze_title: "Текст з пропусками",
|
||
cloze_desc: "Створює тексти з кількома пропусками в кожному реченні. Включаючи переклад для батьків.",
|
||
cloze_translation: "Переклад:",
|
||
cloze_generate: "Створити текст",
|
||
cloze_start: "Почати вправу",
|
||
cloze_exercise_title: "Вправа з пропусками",
|
||
cloze_instruction: "Заповніть пропуски та натисніть 'Перевірити'.",
|
||
cloze_check: "Перевірити",
|
||
cloze_show_answers: "Показати відповіді",
|
||
cloze_no_texts: "Тексти для цього аркуша ще не створені.",
|
||
cloze_sentences: "речень",
|
||
cloze_gaps: "пропусків",
|
||
cloze_gaps_total: "Всього пропусків",
|
||
cloze_with_gaps: "(з пропусками)",
|
||
cloze_print: "Друк",
|
||
cloze_print_with_answers: "Друкувати з відповідями?",
|
||
|
||
qa_title: "Аркуш питань і відповідей",
|
||
qa_desc: "Пари питання-відповідь з системою Лейтнера. Повторення за рівнем складності.",
|
||
qa_generate: "Створити П&В",
|
||
qa_learn: "Почати навчання",
|
||
qa_print: "Друк",
|
||
qa_no_questions: "П&В для цього аркуша ще не створені.",
|
||
qa_box_new: "Новий",
|
||
qa_box_learning: "Вивчається",
|
||
qa_box_mastered: "Засвоєно",
|
||
qa_show_answer: "Показати відповідь",
|
||
qa_your_answer: "Твоя відповідь",
|
||
qa_type_answer: "Напиши свою відповідь тут...",
|
||
qa_check_answer: "Перевірити відповідь",
|
||
qa_correct_answer: "Правильна відповідь",
|
||
qa_self_evaluate: "Твоя відповідь була правильною?",
|
||
qa_no_answer: "(відповідь не введена)",
|
||
qa_correct: "Правильно",
|
||
qa_incorrect: "Неправильно",
|
||
qa_key_terms: "Ключові терміни",
|
||
qa_session_correct: "Правильно",
|
||
qa_session_incorrect: "Неправильно",
|
||
qa_session_complete: "Раунд навчання завершено!",
|
||
qa_result_correct: "правильно",
|
||
qa_restart: "Вчити знову",
|
||
qa_print_with_answers: "Друкувати з відповідями?",
|
||
question: "Питання",
|
||
answer: "Відповідь",
|
||
status_generating_qa: "Створення П&В…",
|
||
status_qa_generated: "П&В створені",
|
||
|
||
close: "Закрити",
|
||
subject: "Предмет",
|
||
grade: "Рівень",
|
||
questions: "питань",
|
||
worksheet: "Робочий аркуш",
|
||
loading: "Завантаження...",
|
||
error: "Помилка",
|
||
success: "Успішно",
|
||
|
||
imprint: "Імпресум",
|
||
privacy: "Конфіденційність",
|
||
contact: "Контакт",
|
||
|
||
status_ready: "Готово",
|
||
status_processing: "Обробка...",
|
||
status_generating_mc: "Створення питань…",
|
||
status_generating_cloze: "Створення текстів…",
|
||
status_please_wait: "Будь ласка, зачекайте, ШІ працює.",
|
||
status_mc_generated: "Питання створені",
|
||
status_cloze_generated: "Тексти створені",
|
||
status_files_created: "файлів створено",
|
||
|
||
// Mindmap Tile
|
||
mindmap_title: "Плакат Інтелект-карта",
|
||
mindmap_desc: "Створює дитячу інтелект-карту з головною темою в центрі та всіма термінами в кольорових категоріях.",
|
||
mindmap_generate: "Створити карту",
|
||
mindmap_show: "Переглянути",
|
||
mindmap_print_a3: "Друк A3",
|
||
generating_mindmap: "Створення карти...",
|
||
mindmap_generated: "Карту створено!",
|
||
no_analysis: "Немає аналізу",
|
||
analyze_first: "Спочатку виконайте аналіз (натисніть Обробити)",
|
||
categories: "Категорії",
|
||
terms: "Терміни",
|
||
},
|
||
|
||
pl: {
|
||
brand_sub: "Studio",
|
||
nav_compare: "Karty pracy",
|
||
nav_tiles: "Karty nauki",
|
||
login: "Logowanie / Rejestracja",
|
||
mvp_local: "MVP · Lokalnie na Twoim Mac",
|
||
|
||
sidebar_areas: "Sekcje",
|
||
sidebar_studio: "Studio kart pracy",
|
||
sidebar_active: "aktywne",
|
||
sidebar_parents: "Kanał dla rodziców",
|
||
sidebar_soon: "wkrótce",
|
||
sidebar_correction: "Korekta / Oceny",
|
||
sidebar_units: "Jednostki nauki (lokalnie)",
|
||
input_student: "Uczeń",
|
||
input_subject: "Przedmiot",
|
||
input_grade: "Klasa (np. 7a)",
|
||
input_unit_title: "Jednostka nauki / Temat",
|
||
btn_create: "Utwórz",
|
||
btn_add_current: "Dodaj bieżącą kartę",
|
||
btn_filter_unit: "Tylko jednostka",
|
||
btn_filter_all: "Wszystkie pliki",
|
||
|
||
uploaded_worksheets: "Przesłane karty pracy",
|
||
files: "plików",
|
||
btn_upload: "Prześlij",
|
||
btn_delete: "Usuń",
|
||
original_scan: "Oryginalny skan",
|
||
cleaned_version: "Oczyszczone (pismo ręczne usunięte)",
|
||
no_cleaned: "Oczyszczona wersja jeszcze niedostępna.",
|
||
process_hint: "Kliknij 'Przetwórz', aby przeanalizować i oczyścić kartę.",
|
||
worksheet_print: "Drukuj",
|
||
worksheet_no_data: "Brak danych arkusza.",
|
||
btn_full_process: "Przetwórz (Analiza + Czyszczenie + HTML)",
|
||
btn_original_generate: "Tylko oryginalny HTML",
|
||
|
||
learning_unit: "Jednostka nauki",
|
||
no_unit_selected: "Nie wybrano jednostki",
|
||
|
||
mc_title: "Test wielokrotnego wyboru",
|
||
mc_ready: "Gotowe",
|
||
mc_generating: "Tworzenie...",
|
||
mc_done: "Gotowe",
|
||
mc_error: "Błąd",
|
||
mc_desc: "Tworzy pytania wielokrotnego wyboru o odpowiednim poziomie trudności (np. klasa 7).",
|
||
mc_generate: "Utwórz test",
|
||
mc_show: "Pokaż pytania",
|
||
mc_quiz_title: "Test wielokrotnego wyboru",
|
||
mc_evaluate: "Oceń",
|
||
mc_correct: "Dobrze!",
|
||
mc_incorrect: "Niestety źle.",
|
||
mc_not_answered: "Brak odpowiedzi. Poprawna odpowiedź:",
|
||
mc_result: "z",
|
||
mc_result_correct: "poprawnie",
|
||
mc_percent: "poprawnie",
|
||
mc_no_questions: "Pytania dla tej karty jeszcze nie zostały utworzone.",
|
||
mc_print: "Drukuj",
|
||
mc_print_with_answers: "Drukować z odpowiedziami?",
|
||
|
||
cloze_title: "Tekst z lukami",
|
||
cloze_desc: "Tworzy teksty z wieloma lukami w każdym zdaniu. W tym tłumaczenie dla rodziców.",
|
||
cloze_translation: "Tłumaczenie:",
|
||
cloze_generate: "Utwórz tekst",
|
||
cloze_start: "Rozpocznij ćwiczenie",
|
||
cloze_exercise_title: "Ćwiczenie z lukami",
|
||
cloze_instruction: "Wypełnij luki i kliknij 'Sprawdź'.",
|
||
cloze_check: "Sprawdź",
|
||
cloze_show_answers: "Pokaż odpowiedzi",
|
||
cloze_no_texts: "Teksty dla tej karty jeszcze nie zostały utworzone.",
|
||
cloze_sentences: "zdań",
|
||
cloze_gaps: "luk",
|
||
cloze_gaps_total: "Łącznie luk",
|
||
cloze_with_gaps: "(z lukami)",
|
||
cloze_print: "Drukuj",
|
||
cloze_print_with_answers: "Drukować z odpowiedziami?",
|
||
|
||
qa_title: "Arkusz pytań i odpowiedzi",
|
||
qa_desc: "Pary pytanie-odpowiedź z systemem Leitnera. Powtórki według poziomu trudności.",
|
||
qa_generate: "Utwórz P&O",
|
||
qa_learn: "Rozpocznij naukę",
|
||
qa_print: "Drukuj",
|
||
qa_no_questions: "P&O dla tej karty jeszcze nie zostały utworzone.",
|
||
qa_box_new: "Nowy",
|
||
qa_box_learning: "W nauce",
|
||
qa_box_mastered: "Opanowane",
|
||
qa_show_answer: "Pokaż odpowiedź",
|
||
qa_your_answer: "Twoja odpowiedź",
|
||
qa_type_answer: "Napisz swoją odpowiedź tutaj...",
|
||
qa_check_answer: "Sprawdź odpowiedź",
|
||
qa_correct_answer: "Prawidłowa odpowiedź",
|
||
qa_self_evaluate: "Czy twoja odpowiedź była poprawna?",
|
||
qa_no_answer: "(nie wprowadzono odpowiedzi)",
|
||
qa_correct: "Dobrze",
|
||
qa_incorrect: "Źle",
|
||
qa_key_terms: "Kluczowe pojęcia",
|
||
qa_session_correct: "Dobrze",
|
||
qa_session_incorrect: "Źle",
|
||
qa_session_complete: "Runda nauki zakończona!",
|
||
qa_result_correct: "poprawnie",
|
||
qa_restart: "Ucz się ponownie",
|
||
qa_print_with_answers: "Drukować z odpowiedziami?",
|
||
question: "Pytanie",
|
||
answer: "Odpowiedź",
|
||
status_generating_qa: "Tworzenie P&O…",
|
||
status_qa_generated: "P&O utworzone",
|
||
|
||
close: "Zamknij",
|
||
subject: "Przedmiot",
|
||
grade: "Poziom",
|
||
questions: "pytań",
|
||
worksheet: "Karta pracy",
|
||
loading: "Ładowanie...",
|
||
error: "Błąd",
|
||
success: "Sukces",
|
||
|
||
imprint: "Impressum",
|
||
privacy: "Prywatność",
|
||
contact: "Kontakt",
|
||
|
||
status_ready: "Gotowe",
|
||
status_processing: "Przetwarzanie...",
|
||
status_generating_mc: "Tworzenie pytań…",
|
||
status_generating_cloze: "Tworzenie tekstów…",
|
||
status_please_wait: "Proszę czekać, AI pracuje.",
|
||
status_mc_generated: "Pytania utworzone",
|
||
status_cloze_generated: "Teksty utworzone",
|
||
status_files_created: "plików utworzono",
|
||
|
||
// Mindmap Tile
|
||
mindmap_title: "Plakat Mapa myśli",
|
||
mindmap_desc: "Tworzy przyjazną dla dzieci mapę myśli z głównym tematem w centrum i wszystkimi terminami w kolorowych kategoriach.",
|
||
mindmap_generate: "Utwórz mapę",
|
||
mindmap_show: "Podgląd",
|
||
mindmap_print_a3: "Drukuj A3",
|
||
generating_mindmap: "Tworzenie mapy...",
|
||
mindmap_generated: "Mapa utworzona!",
|
||
no_analysis: "Brak analizy",
|
||
analyze_first: "Najpierw wykonaj analizę (kliknij Przetwórz)",
|
||
categories: "Kategorie",
|
||
terms: "Terminy",
|
||
},
|
||
|
||
en: {
|
||
brand_sub: "Studio",
|
||
nav_compare: "Worksheets",
|
||
nav_tiles: "Learning Tiles",
|
||
login: "Login / Sign Up",
|
||
mvp_local: "MVP · Local on your Mac",
|
||
|
||
sidebar_areas: "Areas",
|
||
sidebar_studio: "Worksheet Studio",
|
||
sidebar_active: "active",
|
||
sidebar_parents: "Parents Channel",
|
||
sidebar_soon: "coming soon",
|
||
sidebar_correction: "Correction / Grades",
|
||
sidebar_units: "Learning Units (local)",
|
||
input_student: "Student",
|
||
input_subject: "Subject",
|
||
input_grade: "Grade (e.g. 7a)",
|
||
input_unit_title: "Learning Unit / Topic",
|
||
btn_create: "Create",
|
||
btn_add_current: "Add current worksheet",
|
||
btn_filter_unit: "Unit only",
|
||
btn_filter_all: "All files",
|
||
|
||
uploaded_worksheets: "Uploaded Worksheets",
|
||
files: "files",
|
||
btn_upload: "Upload",
|
||
btn_delete: "Delete",
|
||
original_scan: "Original Scan",
|
||
cleaned_version: "Cleaned (handwriting removed)",
|
||
no_cleaned: "No cleaned version available yet.",
|
||
process_hint: "Click 'Process' to analyze and clean the worksheet.",
|
||
worksheet_print: "Print",
|
||
worksheet_no_data: "No worksheet data available.",
|
||
btn_full_process: "Process (Analysis + Cleaning + HTML)",
|
||
btn_original_generate: "Generate Original HTML Only",
|
||
|
||
learning_unit: "Learning Unit",
|
||
no_unit_selected: "No unit selected",
|
||
|
||
mc_title: "Multiple Choice Test",
|
||
mc_ready: "Ready",
|
||
mc_generating: "Generating...",
|
||
mc_done: "Done",
|
||
mc_error: "Error",
|
||
mc_desc: "Creates multiple choice questions matching the original difficulty level (e.g. Grade 7).",
|
||
mc_generate: "Generate MC",
|
||
mc_show: "Show Questions",
|
||
mc_quiz_title: "Multiple Choice Quiz",
|
||
mc_evaluate: "Evaluate",
|
||
mc_correct: "Correct!",
|
||
mc_incorrect: "Unfortunately wrong.",
|
||
mc_not_answered: "Not answered. Correct answer:",
|
||
mc_result: "of",
|
||
mc_result_correct: "correct",
|
||
mc_percent: "correct",
|
||
mc_no_questions: "No MC questions generated yet for this worksheet.",
|
||
mc_print: "Print",
|
||
mc_print_with_answers: "Print with answers?",
|
||
|
||
cloze_title: "Fill in the Blanks",
|
||
cloze_desc: "Creates texts with multiple meaningful gaps per sentence. Including translation for parents.",
|
||
cloze_translation: "Translation:",
|
||
cloze_generate: "Generate Cloze Text",
|
||
cloze_start: "Start Exercise",
|
||
cloze_exercise_title: "Fill in the Blanks Exercise",
|
||
cloze_instruction: "Fill in the blanks and click 'Check'.",
|
||
cloze_check: "Check",
|
||
cloze_show_answers: "Show Answers",
|
||
cloze_no_texts: "No cloze texts generated yet for this worksheet.",
|
||
cloze_sentences: "sentences",
|
||
cloze_gaps: "gaps",
|
||
cloze_gaps_total: "Total gaps",
|
||
cloze_with_gaps: "(with gaps)",
|
||
cloze_print: "Print",
|
||
cloze_print_with_answers: "Print with answers?",
|
||
|
||
qa_title: "Question & Answer Sheet",
|
||
qa_desc: "Q&A pairs with Leitner box system. Spaced repetition by difficulty level.",
|
||
qa_generate: "Generate Q&A",
|
||
qa_learn: "Start Learning",
|
||
qa_print: "Print",
|
||
qa_no_questions: "No Q&A generated yet for this worksheet.",
|
||
qa_box_new: "New",
|
||
qa_box_learning: "Learning",
|
||
qa_box_mastered: "Mastered",
|
||
qa_show_answer: "Show Answer",
|
||
qa_your_answer: "Your Answer",
|
||
qa_type_answer: "Write your answer here...",
|
||
qa_check_answer: "Check Answer",
|
||
qa_correct_answer: "Correct Answer",
|
||
qa_self_evaluate: "Was your answer correct?",
|
||
qa_no_answer: "(no answer entered)",
|
||
qa_correct: "Correct",
|
||
qa_incorrect: "Incorrect",
|
||
qa_key_terms: "Key Terms",
|
||
qa_session_correct: "Correct",
|
||
qa_session_incorrect: "Incorrect",
|
||
qa_session_complete: "Learning session complete!",
|
||
qa_result_correct: "correct",
|
||
qa_restart: "Learn Again",
|
||
qa_print_with_answers: "Print with answers?",
|
||
question: "Question",
|
||
answer: "Answer",
|
||
status_generating_qa: "Generating Q&A…",
|
||
status_qa_generated: "Q&A generated",
|
||
|
||
close: "Close",
|
||
subject: "Subject",
|
||
grade: "Level",
|
||
questions: "questions",
|
||
worksheet: "Worksheet",
|
||
loading: "Loading...",
|
||
error: "Error",
|
||
success: "Success",
|
||
|
||
imprint: "Imprint",
|
||
privacy: "Privacy",
|
||
contact: "Contact",
|
||
|
||
status_ready: "Ready",
|
||
status_processing: "Processing...",
|
||
status_generating_mc: "Generating MC questions…",
|
||
status_generating_cloze: "Generating cloze texts…",
|
||
status_please_wait: "Please wait, AI is working.",
|
||
status_mc_generated: "MC questions generated",
|
||
status_cloze_generated: "Cloze texts generated",
|
||
status_files_created: "files created",
|
||
|
||
// Mindmap Tile
|
||
mindmap_title: "Mindmap Learning Poster",
|
||
mindmap_desc: "Creates a child-friendly mindmap with the main topic in the center and all terms in colorful categories.",
|
||
mindmap_generate: "Create Mindmap",
|
||
mindmap_show: "View",
|
||
mindmap_print_a3: "Print A3",
|
||
generating_mindmap: "Creating mindmap...",
|
||
mindmap_generated: "Mindmap created!",
|
||
no_analysis: "No analysis",
|
||
analyze_first: "Please analyze first (click Process)",
|
||
categories: "Categories",
|
||
terms: "Terms",
|
||
}
|
||
};
|
||
|
||
// Aktuelle Sprache (aus localStorage oder Browser-Sprache)
|
||
let currentLang = localStorage.getItem('bp_language') || 'de';
|
||
|
||
// RTL-Sprachen
|
||
const rtlLanguages = ['ar'];
|
||
|
||
// Übersetzungsfunktion
|
||
function t(key) {
|
||
const lang = translations[currentLang] || translations['de'];
|
||
return lang[key] || translations['de'][key] || key;
|
||
}
|
||
|
||
// Sprache anwenden
|
||
function applyLanguage(lang) {
|
||
currentLang = lang;
|
||
localStorage.setItem('bp_language', lang);
|
||
|
||
// RTL für Arabisch
|
||
if (rtlLanguages.includes(lang)) {
|
||
document.documentElement.setAttribute('dir', 'rtl');
|
||
} else {
|
||
document.documentElement.setAttribute('dir', 'ltr');
|
||
}
|
||
|
||
// Alle Elemente mit data-i18n aktualisieren
|
||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||
const key = el.getAttribute('data-i18n');
|
||
if (el.tagName === 'INPUT' && el.hasAttribute('placeholder')) {
|
||
el.placeholder = t(key);
|
||
} else {
|
||
el.textContent = t(key);
|
||
}
|
||
});
|
||
|
||
// Spezielle Elemente manuell aktualisieren
|
||
updateUITexts();
|
||
}
|
||
|
||
// UI-Texte aktualisieren
|
||
function updateUITexts() {
|
||
// Header
|
||
const brandSub = document.querySelector('.brand-text-sub');
|
||
if (brandSub) brandSub.textContent = t('brand_sub');
|
||
|
||
// Navigation (Text entfernt - wird nicht mehr angezeigt)
|
||
// const navItems = document.querySelectorAll('.top-nav-item');
|
||
// if (navItems[0]) navItems[0].textContent = t('nav_compare');
|
||
// if (navItems[1]) navItems[1].textContent = t('nav_tiles');
|
||
|
||
// Sidebar Bereiche
|
||
const sidebarTitles = document.querySelectorAll('.sidebar-section-title');
|
||
if (sidebarTitles[0]) sidebarTitles[0].textContent = t('sidebar_areas');
|
||
if (sidebarTitles[1]) sidebarTitles[1].textContent = t('sidebar_units');
|
||
|
||
const sidebarLabels = document.querySelectorAll('.sidebar-item-label span');
|
||
if (sidebarLabels[0]) sidebarLabels[0].textContent = t('sidebar_studio');
|
||
if (sidebarLabels[1]) sidebarLabels[1].textContent = t('sidebar_parents');
|
||
if (sidebarLabels[2]) sidebarLabels[2].textContent = t('sidebar_correction');
|
||
|
||
const sidebarBadges = document.querySelectorAll('.sidebar-item-badge');
|
||
if (sidebarBadges[0]) sidebarBadges[0].textContent = t('sidebar_active');
|
||
if (sidebarBadges[1]) sidebarBadges[1].textContent = t('sidebar_soon');
|
||
if (sidebarBadges[2]) sidebarBadges[2].textContent = t('sidebar_soon');
|
||
|
||
// Input Placeholders
|
||
if (unitStudentInput) unitStudentInput.placeholder = t('input_student');
|
||
if (unitSubjectInput) unitSubjectInput.placeholder = t('input_subject');
|
||
if (unitGradeInput) unitGradeInput.placeholder = t('input_grade');
|
||
if (unitTitleInput) unitTitleInput.placeholder = t('input_unit_title');
|
||
|
||
// Buttons
|
||
if (btnAddUnit) btnAddUnit.textContent = t('btn_create');
|
||
if (btnAttachCurrentToLu) btnAttachCurrentToLu.textContent = t('btn_add_current');
|
||
if (btnToggleFilter) {
|
||
btnToggleFilter.textContent = showOnlyUnitFiles ? t('btn_filter_unit') : t('btn_filter_all');
|
||
}
|
||
if (btnFullProcess) btnFullProcess.textContent = t('btn_full_process');
|
||
if (btnOriginalGenerate) btnOriginalGenerate.textContent = t('btn_original_generate');
|
||
|
||
// Titles
|
||
const uploadedTitle = document.querySelector('.panel-left > div:first-child');
|
||
if (uploadedTitle) {
|
||
uploadedTitle.innerHTML = '<span data-i18n="uploaded_worksheets">' + t('uploaded_worksheets') + '</span> <span class="pill" id="eingang-count">0 ' + t('files') + '</span>';
|
||
}
|
||
|
||
// MC Tile
|
||
const mcTitle = document.querySelector('[data-tile="mc"] .card-title');
|
||
if (mcTitle) mcTitle.textContent = t('mc_title');
|
||
const mcDesc = document.querySelector('[data-tile="mc"] .card-body > div:first-child');
|
||
if (mcDesc) mcDesc.textContent = t('mc_desc');
|
||
if (btnMcGenerate) btnMcGenerate.textContent = t('mc_generate');
|
||
if (btnMcShow) btnMcShow.textContent = t('mc_show');
|
||
|
||
// Cloze Tile
|
||
const clozeTitle = document.querySelector('[data-tile="cloze"] .card-title');
|
||
if (clozeTitle) clozeTitle.textContent = t('cloze_title');
|
||
const clozeDesc = document.querySelector('[data-tile="cloze"] .card-body > div:first-child');
|
||
if (clozeDesc) clozeDesc.textContent = t('cloze_desc');
|
||
const clozeLabel = document.querySelector('.cloze-language-select label');
|
||
if (clozeLabel) clozeLabel.textContent = t('cloze_translation');
|
||
if (btnClozeGenerate) btnClozeGenerate.textContent = t('cloze_generate');
|
||
if (btnClozeShow) btnClozeShow.textContent = t('cloze_start');
|
||
|
||
// QA Tile
|
||
const qaTitle = document.querySelector('[data-tile="qa"] .card-title');
|
||
if (qaTitle) qaTitle.textContent = t('qa_title');
|
||
const qaDesc = document.querySelector('[data-tile="qa"] .card-body > div:first-child');
|
||
if (qaDesc) qaDesc.textContent = t('qa_desc');
|
||
const qaBadge = document.querySelector('[data-tile="qa"] .card-badge');
|
||
if (qaBadge) qaBadge.textContent = t('qa_soon');
|
||
|
||
// Modal Titles
|
||
const mcModalTitle = document.querySelector('#mc-modal .mc-modal-title');
|
||
if (mcModalTitle) mcModalTitle.textContent = t('mc_quiz_title');
|
||
const clozeModalTitle = document.querySelector('#cloze-modal .mc-modal-title');
|
||
if (clozeModalTitle) clozeModalTitle.textContent = t('cloze_exercise_title');
|
||
|
||
// Close Buttons
|
||
document.querySelectorAll('.mc-modal-close').forEach(btn => {
|
||
btn.textContent = t('close') + ' ✕';
|
||
});
|
||
if (lightboxClose) lightboxClose.textContent = t('close') + ' ✕';
|
||
|
||
// Footer
|
||
const footerLinks = document.querySelectorAll('.footer a');
|
||
if (footerLinks[0]) footerLinks[0].textContent = t('imprint');
|
||
if (footerLinks[1]) footerLinks[1].textContent = t('privacy');
|
||
if (footerLinks[2]) footerLinks[2].textContent = t('contact');
|
||
}
|
||
|
||
const eingangListEl = document.getElementById('eingang-list');
|
||
const eingangCountEl = document.getElementById('eingang-count');
|
||
const previewContainer = document.getElementById('preview-container');
|
||
const fileInput = document.getElementById('file-input');
|
||
const btnUploadInline = document.getElementById('btn-upload-inline');
|
||
const btnFullProcess = document.getElementById('btn-full-process');
|
||
const btnOriginalGenerate = document.getElementById('btn-original-generate');
|
||
const statusBar = document.getElementById('status-bar');
|
||
const statusDot = document.getElementById('status-dot');
|
||
const statusMain = document.getElementById('status-main');
|
||
const statusSub = document.getElementById('status-sub');
|
||
|
||
const panelCompare = document.getElementById('panel-compare');
|
||
const panelTiles = document.getElementById('panel-tiles');
|
||
const pagerPrev = document.getElementById('pager-prev');
|
||
const pagerNext = document.getElementById('pager-next');
|
||
const pagerLabel = document.getElementById('pager-label');
|
||
|
||
const topNavItems = document.querySelectorAll('.top-nav-item');
|
||
|
||
const lightboxEl = document.getElementById('lightbox');
|
||
const lightboxImg = document.getElementById('lightbox-img');
|
||
const lightboxCaption = document.getElementById('lightbox-caption');
|
||
const lightboxClose = document.getElementById('lightbox-close');
|
||
|
||
const unitStudentInput = document.getElementById('unit-student');
|
||
const unitSubjectInput = document.getElementById('unit-subject');
|
||
const unitGradeInput = document.getElementById('unit-grade');
|
||
const unitTitleInput = document.getElementById('unit-title');
|
||
const unitListEl = document.getElementById('unit-list');
|
||
const btnAddUnit = document.getElementById('btn-add-unit');
|
||
const btnAttachCurrentToLu = document.getElementById('btn-attach-current-to-lu');
|
||
const unitHeading1 = document.getElementById('unit-heading-screen1');
|
||
const unitHeading2 = document.getElementById('unit-heading-screen2');
|
||
const btnToggleFilter = document.getElementById('btn-toggle-filter');
|
||
|
||
let currentSelectedFile = null;
|
||
let allEingangFiles = []; // Master-Liste aller Dateien
|
||
let eingangFiles = []; // aktuell gefilterte Ansicht
|
||
let currentIndex = 0;
|
||
let showOnlyUnitFiles = true; // Filter-Modus: true = nur Lerneinheit (Standard), false = alle
|
||
|
||
let allWorksheetPairs = {}; // Master-Mapping original -> { clean_html, clean_image }
|
||
let worksheetPairs = {}; // aktuell gefiltertes Mapping
|
||
|
||
let tileState = {
|
||
mindmap: true,
|
||
qa: true,
|
||
mc: true,
|
||
cloze: true,
|
||
};
|
||
let currentScreen = 1;
|
||
|
||
// Lerneinheiten aus dem Backend
|
||
let units = [];
|
||
let currentUnitId = null;
|
||
|
||
// --- Lightbox / Vollbild ---
|
||
function openLightbox(src, caption) {
|
||
if (!src) return;
|
||
lightboxImg.src = src;
|
||
lightboxCaption.textContent = caption || '';
|
||
lightboxEl.classList.remove('hidden');
|
||
}
|
||
|
||
function closeLightbox() {
|
||
lightboxEl.classList.add('hidden');
|
||
lightboxImg.src = '';
|
||
lightboxCaption.textContent = '';
|
||
}
|
||
|
||
lightboxClose.addEventListener('click', closeLightbox);
|
||
lightboxEl.addEventListener('click', (ev) => {
|
||
if (ev.target === lightboxEl) {
|
||
closeLightbox();
|
||
}
|
||
});
|
||
document.addEventListener('keydown', (ev) => {
|
||
if (ev.key === 'Escape') {
|
||
closeLightbox();
|
||
// Close compare view if open
|
||
const compareView = document.getElementById('version-compare-view');
|
||
if (compareView && compareView.classList.contains('active')) {
|
||
hideCompareView();
|
||
}
|
||
}
|
||
});
|
||
|
||
// --- Status-Balken ---
|
||
function setStatus(main, sub = '', state = 'idle') {
|
||
statusMain.textContent = main;
|
||
statusSub.textContent = sub;
|
||
statusDot.classList.remove('busy', 'error');
|
||
if (state === 'busy') {
|
||
statusDot.classList.add('busy');
|
||
} else if (state === 'error') {
|
||
statusDot.classList.add('error');
|
||
}
|
||
}
|
||
|
||
setStatus('Bereit', 'Lade Arbeitsblätter hoch und starte den Neuaufbau.');
|
||
|
||
// --- API-Helfer ---
|
||
async function apiFetch(url, options = {}) {
|
||
const resp = await fetch(url, options);
|
||
if (!resp.ok) {
|
||
throw new Error('HTTP ' + resp.status);
|
||
}
|
||
return resp.json();
|
||
}
|
||
|
||
// --- Dateien laden & rendern ---
|
||
async function loadEingangFiles() {
|
||
try {
|
||
const data = await apiFetch('/api/eingang-dateien');
|
||
allEingangFiles = data.eingang || [];
|
||
eingangFiles = allEingangFiles.slice();
|
||
currentIndex = 0;
|
||
renderEingangList();
|
||
} catch (e) {
|
||
console.error(e);
|
||
setStatus('Fehler beim Laden der Dateien', String(e), 'error');
|
||
}
|
||
}
|
||
|
||
function renderEingangList() {
|
||
eingangListEl.innerHTML = '';
|
||
|
||
if (!eingangFiles.length) {
|
||
const li = document.createElement('li');
|
||
li.className = 'file-empty';
|
||
li.textContent = 'Noch keine Dateien vorhanden.';
|
||
eingangListEl.appendChild(li);
|
||
eingangCountEl.textContent = '0 Dateien';
|
||
return;
|
||
}
|
||
|
||
eingangFiles.forEach((filename, idx) => {
|
||
const li = document.createElement('li');
|
||
li.className = 'file-item';
|
||
if (idx === currentIndex) {
|
||
li.classList.add('active');
|
||
}
|
||
|
||
const nameSpan = document.createElement('span');
|
||
nameSpan.className = 'file-item-name';
|
||
nameSpan.textContent = filename;
|
||
|
||
const actionsSpan = document.createElement('span');
|
||
actionsSpan.style.display = 'flex';
|
||
actionsSpan.style.gap = '6px';
|
||
|
||
// Button: Aus Lerneinheit entfernen
|
||
const removeFromUnitBtn = document.createElement('span');
|
||
removeFromUnitBtn.className = 'file-item-delete';
|
||
removeFromUnitBtn.textContent = '✕';
|
||
removeFromUnitBtn.title = 'Aus Lerneinheit entfernen';
|
||
removeFromUnitBtn.addEventListener('click', (ev) => {
|
||
ev.stopPropagation();
|
||
|
||
if (!currentUnitId) {
|
||
alert('Zum Entfernen bitte zuerst eine Lerneinheit auswählen.');
|
||
return;
|
||
}
|
||
|
||
const ok = confirm('Dieses Arbeitsblatt aus der aktuellen Lerneinheit entfernen? Die Datei selbst bleibt erhalten.');
|
||
if (!ok) return;
|
||
|
||
removeWorksheetFromCurrentUnit(eingangFiles[idx]);
|
||
});
|
||
|
||
// Button: Datei komplett löschen
|
||
const deleteFileBtn = document.createElement('span');
|
||
deleteFileBtn.className = 'file-item-delete';
|
||
deleteFileBtn.textContent = '🗑️';
|
||
deleteFileBtn.title = 'Datei komplett löschen';
|
||
deleteFileBtn.style.color = '#ef4444';
|
||
deleteFileBtn.addEventListener('click', async (ev) => {
|
||
ev.stopPropagation();
|
||
|
||
const ok = confirm(`Datei "${eingangFiles[idx]}" wirklich komplett löschen? Diese Aktion kann nicht rückgängig gemacht werden.`);
|
||
if (!ok) return;
|
||
|
||
await deleteFileCompletely(eingangFiles[idx]);
|
||
});
|
||
|
||
actionsSpan.appendChild(removeFromUnitBtn);
|
||
actionsSpan.appendChild(deleteFileBtn);
|
||
|
||
li.appendChild(nameSpan);
|
||
li.appendChild(actionsSpan);
|
||
|
||
li.addEventListener('click', () => {
|
||
currentIndex = idx;
|
||
currentSelectedFile = filename;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
});
|
||
|
||
eingangListEl.appendChild(li);
|
||
});
|
||
|
||
eingangCountEl.textContent = eingangFiles.length + (eingangFiles.length === 1 ? ' Datei' : ' Dateien');
|
||
}
|
||
|
||
async function loadWorksheetPairs() {
|
||
try {
|
||
const data = await apiFetch('/api/worksheet-pairs');
|
||
allWorksheetPairs = {};
|
||
(data.pairs || []).forEach((p) => {
|
||
allWorksheetPairs[p.original] = { clean_html: p.clean_html, clean_image: p.clean_image };
|
||
});
|
||
worksheetPairs = { ...allWorksheetPairs };
|
||
renderPreviewForCurrent();
|
||
} catch (e) {
|
||
console.error(e);
|
||
setStatus('Fehler beim Laden der Neuaufbau-Daten', String(e), 'error');
|
||
}
|
||
}
|
||
|
||
function renderPreviewForCurrent() {
|
||
if (!eingangFiles.length) {
|
||
const message = showOnlyUnitFiles && currentUnitId
|
||
? 'Dieser Lerneinheit sind noch keine Arbeitsblätter zugeordnet.'
|
||
: 'Keine Dateien vorhanden.';
|
||
previewContainer.innerHTML = `<div class="preview-placeholder">${message}</div>`;
|
||
return;
|
||
}
|
||
if (currentIndex < 0) currentIndex = 0;
|
||
if (currentIndex >= eingangFiles.length) currentIndex = eingangFiles.length - 1;
|
||
|
||
const filename = eingangFiles[currentIndex];
|
||
const entry = worksheetPairs[filename] || { clean_html: null, clean_image: null };
|
||
|
||
renderPreview(entry, currentIndex);
|
||
}
|
||
|
||
function renderThumbnailsInColumn(container) {
|
||
container.innerHTML = '';
|
||
|
||
if (eingangFiles.length <= 1) {
|
||
return; // Keine Thumbnails nötig wenn nur 1 oder 0 Dateien
|
||
}
|
||
|
||
// Zeige bis zu 5 Thumbnails (die nächsten Dateien nach dem aktuellen)
|
||
const maxThumbs = 5;
|
||
let thumbCount = 0;
|
||
|
||
for (let i = 0; i < eingangFiles.length && thumbCount < maxThumbs; i++) {
|
||
if (i === currentIndex) continue; // Aktuelles Dokument überspringen
|
||
|
||
const filename = eingangFiles[i];
|
||
const thumb = document.createElement('div');
|
||
thumb.className = 'preview-thumb';
|
||
|
||
const img = document.createElement('img');
|
||
img.src = '/preview-file/' + encodeURIComponent(filename);
|
||
img.alt = filename;
|
||
|
||
const label = document.createElement('div');
|
||
label.className = 'preview-thumb-label';
|
||
label.textContent = `${i + 1}`;
|
||
|
||
thumb.appendChild(img);
|
||
thumb.appendChild(label);
|
||
|
||
thumb.addEventListener('click', () => {
|
||
currentIndex = i;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
});
|
||
|
||
container.appendChild(thumb);
|
||
thumbCount++;
|
||
}
|
||
}
|
||
|
||
function renderPreview(entry, index) {
|
||
previewContainer.innerHTML = '';
|
||
|
||
const wrapper = document.createElement('div');
|
||
wrapper.className = 'compare-wrapper';
|
||
|
||
// Original
|
||
const originalSection = document.createElement('div');
|
||
originalSection.className = 'compare-section';
|
||
const origHeader = document.createElement('div');
|
||
origHeader.className = 'compare-header';
|
||
origHeader.innerHTML = '<span>Original-Scan</span><span>Alt (links)</span>';
|
||
const origBody = document.createElement('div');
|
||
origBody.className = 'compare-body';
|
||
const origInner = document.createElement('div');
|
||
origInner.className = 'compare-body-inner';
|
||
|
||
const img = document.createElement('img');
|
||
img.className = 'preview-img';
|
||
const imgSrc = '/preview-file/' + encodeURIComponent(eingangFiles[index]);
|
||
img.src = imgSrc;
|
||
img.alt = 'Original ' + eingangFiles[index];
|
||
img.addEventListener('dblclick', () => openLightbox(imgSrc, eingangFiles[index]));
|
||
|
||
origInner.appendChild(img);
|
||
origBody.appendChild(origInner);
|
||
originalSection.appendChild(origHeader);
|
||
originalSection.appendChild(origBody);
|
||
|
||
// Neu aufgebaut
|
||
const cleanSection = document.createElement('div');
|
||
cleanSection.className = 'compare-section';
|
||
const cleanHeader = document.createElement('div');
|
||
cleanHeader.className = 'compare-header';
|
||
cleanHeader.innerHTML = '<span>Neu aufgebautes Arbeitsblatt</span><span style="display:flex;align-items:center;gap:8px;"><button type="button" class="btn btn-sm btn-ghost no-print" id="btn-print-worksheet" style="padding:4px 10px;font-size:11px;">🖨️ Drucken</button><span>Neu (rechts)</span></span>';
|
||
const cleanBody = document.createElement('div');
|
||
cleanBody.className = 'compare-body';
|
||
const cleanInner = document.createElement('div');
|
||
cleanInner.className = 'compare-body-inner';
|
||
|
||
// Bevorzuge bereinigtes Bild über HTML (für pixel-genaue Darstellung)
|
||
if (entry.clean_image) {
|
||
const imgClean = document.createElement('img');
|
||
imgClean.className = 'preview-img';
|
||
const cleanSrc = '/preview-clean-file/' + encodeURIComponent(entry.clean_image);
|
||
imgClean.src = cleanSrc;
|
||
imgClean.alt = 'Neu aufgebaut ' + eingangFiles[index];
|
||
imgClean.addEventListener('dblclick', () => openLightbox(cleanSrc, eingangFiles[index] + ' (neu)'));
|
||
cleanInner.appendChild(imgClean);
|
||
} else if (entry.clean_html) {
|
||
const frame = document.createElement('iframe');
|
||
frame.className = 'clean-frame';
|
||
frame.src = '/api/clean-html/' + encodeURIComponent(entry.clean_html);
|
||
frame.title = 'Neu aufgebautes Arbeitsblatt';
|
||
frame.addEventListener('dblclick', () => {
|
||
window.open('/api/clean-html/' + encodeURIComponent(entry.clean_html), '_blank');
|
||
});
|
||
cleanInner.appendChild(frame);
|
||
} else {
|
||
cleanInner.innerHTML = '<div class="preview-placeholder">Noch keine Neuaufbau-Daten vorhanden.</div>';
|
||
}
|
||
|
||
cleanBody.appendChild(cleanInner);
|
||
cleanSection.appendChild(cleanHeader);
|
||
cleanSection.appendChild(cleanBody);
|
||
|
||
// Print-Button Event-Listener
|
||
const printWorksheetBtn = cleanHeader.querySelector('#btn-print-worksheet');
|
||
if (printWorksheetBtn) {
|
||
printWorksheetBtn.addEventListener('click', () => {
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) {
|
||
alert(t('worksheet_no_data') || 'Keine Arbeitsblatt-Daten vorhanden.');
|
||
return;
|
||
}
|
||
window.open('/api/print-worksheet/' + encodeURIComponent(currentFile), '_blank');
|
||
});
|
||
}
|
||
|
||
// Thumbnails in der Mitte
|
||
const thumbsColumn = document.createElement('div');
|
||
thumbsColumn.className = 'preview-thumbnails';
|
||
thumbsColumn.id = 'preview-thumbnails-middle';
|
||
renderThumbnailsInColumn(thumbsColumn);
|
||
|
||
wrapper.appendChild(originalSection);
|
||
wrapper.appendChild(thumbsColumn);
|
||
wrapper.appendChild(cleanSection);
|
||
|
||
// Navigation-Buttons hinzufügen
|
||
const navDiv = document.createElement('div');
|
||
navDiv.className = 'preview-nav';
|
||
|
||
const prevBtn = document.createElement('button');
|
||
prevBtn.type = 'button';
|
||
prevBtn.textContent = '‹';
|
||
prevBtn.disabled = currentIndex === 0;
|
||
prevBtn.addEventListener('click', () => {
|
||
if (currentIndex > 0) {
|
||
currentIndex--;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
}
|
||
});
|
||
|
||
const nextBtn = document.createElement('button');
|
||
nextBtn.type = 'button';
|
||
nextBtn.textContent = '›';
|
||
nextBtn.disabled = currentIndex >= eingangFiles.length - 1;
|
||
nextBtn.addEventListener('click', () => {
|
||
if (currentIndex < eingangFiles.length - 1) {
|
||
currentIndex++;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
}
|
||
});
|
||
|
||
const positionSpan = document.createElement('span');
|
||
positionSpan.textContent = `${currentIndex + 1} von ${eingangFiles.length}`;
|
||
|
||
navDiv.appendChild(prevBtn);
|
||
navDiv.appendChild(positionSpan);
|
||
navDiv.appendChild(nextBtn);
|
||
|
||
wrapper.appendChild(navDiv);
|
||
|
||
previewContainer.appendChild(wrapper);
|
||
}
|
||
|
||
// --- Upload ---
|
||
btnUploadInline.addEventListener('click', async (ev) => {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
|
||
const files = fileInput.files;
|
||
if (!files || !files.length) {
|
||
alert('Bitte erst Dateien auswählen.');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
for (const file of files) {
|
||
formData.append('files', file);
|
||
}
|
||
|
||
try {
|
||
setStatus('Upload läuft …', 'Dateien werden in den Ordner „Eingang“ geschrieben.', 'busy');
|
||
|
||
const resp = await fetch('/api/upload-multi', {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
console.error('Upload-Fehler: HTTP', resp.status);
|
||
setStatus('Fehler beim Upload', 'Serverantwort: HTTP ' + resp.status, 'error');
|
||
return;
|
||
}
|
||
|
||
setStatus('Upload abgeschlossen', 'Dateien wurden gespeichert.');
|
||
fileInput.value = '';
|
||
|
||
// Liste neu laden
|
||
await loadEingangFiles();
|
||
await loadWorksheetPairs();
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Upload', e);
|
||
setStatus('Netzwerkfehler beim Upload', String(e), 'error');
|
||
}
|
||
});
|
||
|
||
// --- Vollpipeline ---
|
||
async function runFullPipeline() {
|
||
try {
|
||
setStatus('Entferne Handschrift …', 'Bilder werden aufbereitet.', 'busy');
|
||
await apiFetch('/api/remove-handwriting-all', { method: 'POST' });
|
||
|
||
setStatus('Analysiere Arbeitsblätter …', 'Struktur wird erkannt.', 'busy');
|
||
await apiFetch('/api/analyze-all', { method: 'POST' });
|
||
|
||
setStatus('Erzeuge HTML-Arbeitsblätter …', 'Neuaufbau läuft.', 'busy');
|
||
await apiFetch('/api/generate-clean', { method: 'POST' });
|
||
|
||
setStatus('Fertig', 'Alt & Neu können jetzt verglichen werden.');
|
||
await loadWorksheetPairs();
|
||
renderPreviewForCurrent();
|
||
} catch (e) {
|
||
console.error(e);
|
||
setStatus('Fehler in der Verarbeitung', String(e), 'error');
|
||
}
|
||
}
|
||
|
||
if (btnFullProcess) btnFullProcess.addEventListener('click', runFullPipeline);
|
||
if (btnOriginalGenerate) btnOriginalGenerate.addEventListener('click', runFullPipeline);
|
||
|
||
// --- Screen-Navigation (oben + Pager unten) ---
|
||
function updateScreen() {
|
||
if (currentScreen === 1) {
|
||
panelCompare.style.display = 'flex';
|
||
panelTiles.style.display = 'none';
|
||
pagerLabel.textContent = '1 von 2';
|
||
} else {
|
||
panelCompare.style.display = 'none';
|
||
panelTiles.style.display = 'flex';
|
||
pagerLabel.textContent = '2 von 2';
|
||
}
|
||
|
||
topNavItems.forEach((item) => {
|
||
const screen = Number(item.getAttribute('data-screen'));
|
||
if (screen === currentScreen) {
|
||
item.classList.add('active');
|
||
} else {
|
||
item.classList.remove('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
topNavItems.forEach((item) => {
|
||
item.addEventListener('click', () => {
|
||
const screen = Number(item.getAttribute('data-screen'));
|
||
currentScreen = screen;
|
||
updateScreen();
|
||
});
|
||
});
|
||
|
||
pagerPrev.addEventListener('click', () => {
|
||
if (currentScreen > 1) {
|
||
currentScreen -= 1;
|
||
updateScreen();
|
||
}
|
||
});
|
||
|
||
pagerNext.addEventListener('click', () => {
|
||
if (currentScreen < 2) {
|
||
currentScreen += 1;
|
||
updateScreen();
|
||
}
|
||
});
|
||
|
||
// --- Toggle-Kacheln ---
|
||
const tileToggles = document.querySelectorAll('.toggle-pill');
|
||
const cards = document.querySelectorAll('.card');
|
||
|
||
function updateTiles() {
|
||
let activeTiles = Object.keys(tileState).filter((k) => tileState[k]);
|
||
cards.forEach((card) => {
|
||
const key = card.getAttribute('data-tile');
|
||
if (!tileState[key]) {
|
||
card.classList.add('card-hidden');
|
||
} else {
|
||
card.classList.remove('card-hidden');
|
||
}
|
||
card.classList.remove('card-full');
|
||
});
|
||
|
||
if (activeTiles.length === 1) {
|
||
const only = activeTiles[0];
|
||
cards.forEach((card) => {
|
||
if (card.getAttribute('data-tile') === only) {
|
||
card.classList.add('card-full');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
tileToggles.forEach((btn) => {
|
||
btn.addEventListener('click', () => {
|
||
const key = btn.getAttribute('data-tile');
|
||
tileState[key] = !tileState[key];
|
||
btn.classList.toggle('active', tileState[key]);
|
||
updateTiles();
|
||
});
|
||
});
|
||
|
||
// --- Lerneinheiten-Logik (Backend) ---
|
||
function updateUnitHeading(unit = null) {
|
||
if (!unit && currentUnitId && units && units.length) {
|
||
unit = units.find((u) => u.id === currentUnitId) || null;
|
||
}
|
||
|
||
let text = 'Keine Lerneinheit ausgewählt';
|
||
if (unit) {
|
||
const name = unit.label || unit.title || 'Lerneinheit';
|
||
text = 'Lerneinheit: ' + name;
|
||
}
|
||
|
||
if (unitHeading1) unitHeading1.textContent = text;
|
||
if (unitHeading2) unitHeading2.textContent = text;
|
||
}
|
||
|
||
function applyUnitFilter() {
|
||
let unit = null;
|
||
if (currentUnitId && units && units.length) {
|
||
unit = units.find((u) => u.id === currentUnitId) || null;
|
||
}
|
||
|
||
// Wenn Filter deaktiviert ODER keine Lerneinheit ausgewählt -> alle Dateien anzeigen
|
||
if (!showOnlyUnitFiles || !unit || !Array.isArray(unit.worksheet_files) || unit.worksheet_files.length === 0) {
|
||
eingangFiles = allEingangFiles.slice();
|
||
worksheetPairs = { ...allWorksheetPairs };
|
||
currentIndex = 0;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
updateUnitHeading(unit);
|
||
return;
|
||
}
|
||
|
||
// Filter aktiv: nur Dateien der aktuellen Lerneinheit anzeigen
|
||
const allowed = new Set(unit.worksheet_files || []);
|
||
eingangFiles = allEingangFiles.filter((f) => allowed.has(f));
|
||
|
||
const filteredPairs = {};
|
||
Object.keys(allWorksheetPairs).forEach((key) => {
|
||
if (allowed.has(key)) {
|
||
filteredPairs[key] = allWorksheetPairs[key];
|
||
}
|
||
});
|
||
worksheetPairs = filteredPairs;
|
||
|
||
currentIndex = 0;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
updateUnitHeading(unit);
|
||
}
|
||
|
||
async function loadLearningUnits() {
|
||
try {
|
||
const resp = await fetch('/api/learning-units/');
|
||
if (!resp.ok) {
|
||
console.error('Fehler beim Laden der Lerneinheiten', resp.status);
|
||
return;
|
||
}
|
||
units = await resp.json();
|
||
if (units.length && !currentUnitId) {
|
||
currentUnitId = units[0].id;
|
||
}
|
||
renderUnits();
|
||
applyUnitFilter();
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Laden der Lerneinheiten', e);
|
||
}
|
||
}
|
||
|
||
function renderUnits() {
|
||
unitListEl.innerHTML = '';
|
||
|
||
if (!units.length) {
|
||
const li = document.createElement('li');
|
||
li.className = 'unit-item';
|
||
li.textContent = 'Noch keine Lerneinheiten angelegt.';
|
||
unitListEl.appendChild(li);
|
||
updateUnitHeading(null);
|
||
return;
|
||
}
|
||
|
||
units.forEach((u) => {
|
||
const li = document.createElement('li');
|
||
li.className = 'unit-item';
|
||
if (u.id === currentUnitId) {
|
||
li.classList.add('active');
|
||
}
|
||
|
||
const contentDiv = document.createElement('div');
|
||
contentDiv.style.flex = '1';
|
||
contentDiv.style.minWidth = '0';
|
||
|
||
const titleEl = document.createElement('div');
|
||
titleEl.textContent = u.label || u.title || 'Lerneinheit';
|
||
|
||
const metaEl = document.createElement('div');
|
||
metaEl.className = 'unit-item-meta';
|
||
|
||
const metaParts = [];
|
||
if (u.meta) {
|
||
metaParts.push(u.meta);
|
||
}
|
||
if (Array.isArray(u.worksheet_files)) {
|
||
metaParts.push('Blätter: ' + u.worksheet_files.length);
|
||
}
|
||
|
||
metaEl.textContent = metaParts.join(' · ');
|
||
|
||
contentDiv.appendChild(titleEl);
|
||
contentDiv.appendChild(metaEl);
|
||
|
||
// Delete-Button
|
||
const deleteBtn = document.createElement('span');
|
||
deleteBtn.textContent = '🗑️';
|
||
deleteBtn.style.cursor = 'pointer';
|
||
deleteBtn.style.fontSize = '12px';
|
||
deleteBtn.style.color = '#ef4444';
|
||
deleteBtn.title = 'Lerneinheit löschen';
|
||
deleteBtn.addEventListener('click', async (ev) => {
|
||
ev.stopPropagation();
|
||
const ok = confirm(`Lerneinheit "${u.label || u.title}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.`);
|
||
if (!ok) return;
|
||
await deleteLearningUnit(u.id);
|
||
});
|
||
|
||
li.appendChild(contentDiv);
|
||
li.appendChild(deleteBtn);
|
||
|
||
li.addEventListener('click', () => {
|
||
currentUnitId = u.id;
|
||
renderUnits();
|
||
applyUnitFilter();
|
||
});
|
||
|
||
unitListEl.appendChild(li);
|
||
});
|
||
}
|
||
|
||
async function addUnitFromForm() {
|
||
const student = (unitStudentInput.value || '').trim();
|
||
const subject = (unitSubjectInput.value || '').trim();
|
||
const grade = (unitGradeInput && unitGradeInput.value || '').trim();
|
||
const title = (unitTitleInput.value || '').trim();
|
||
|
||
if (!student && !subject && !title) {
|
||
alert('Bitte mindestens einen Wert (Schüler/in, Fach oder Thema) eintragen.');
|
||
return;
|
||
}
|
||
|
||
const payload = {
|
||
student,
|
||
subject,
|
||
title,
|
||
grade,
|
||
};
|
||
|
||
try {
|
||
const resp = await fetch('/api/learning-units/', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
if (!resp.ok) {
|
||
console.error('Fehler beim Anlegen der Lerneinheit', resp.status);
|
||
alert('Lerneinheit konnte nicht angelegt werden.');
|
||
return;
|
||
}
|
||
|
||
const created = await resp.json();
|
||
units.push(created);
|
||
currentUnitId = created.id;
|
||
|
||
unitStudentInput.value = '';
|
||
unitSubjectInput.value = '';
|
||
unitTitleInput.value = '';
|
||
if (unitGradeInput) unitGradeInput.value = '';
|
||
|
||
renderUnits();
|
||
applyUnitFilter();
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Anlegen der Lerneinheit', e);
|
||
alert('Netzwerkfehler beim Anlegen der Lerneinheit.');
|
||
}
|
||
}
|
||
|
||
function getCurrentWorksheetBasename() {
|
||
if (!eingangFiles.length) return null;
|
||
if (currentIndex < 0 || currentIndex >= eingangFiles.length) return null;
|
||
return eingangFiles[currentIndex];
|
||
}
|
||
|
||
async function attachCurrentWorksheetToUnit() {
|
||
if (!currentUnitId) {
|
||
alert('Bitte zuerst eine Lerneinheit auswählen oder anlegen.');
|
||
return;
|
||
}
|
||
const basename = getCurrentWorksheetBasename();
|
||
if (!basename) {
|
||
alert('Bitte zuerst ein Arbeitsblatt im linken Bereich auswählen.');
|
||
return;
|
||
}
|
||
|
||
const payload = { worksheet_files: [basename] };
|
||
|
||
try {
|
||
const resp = await fetch(`/api/learning-units/${currentUnitId}/attach-worksheets`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
if (!resp.ok) {
|
||
console.error('Fehler beim Zuordnen des Arbeitsblatts', resp.status);
|
||
alert('Arbeitsblatt konnte nicht zugeordnet werden.');
|
||
return;
|
||
}
|
||
|
||
const updated = await resp.json();
|
||
const idx = units.findIndex((u) => u.id === updated.id);
|
||
if (idx !== -1) {
|
||
units[idx] = updated;
|
||
}
|
||
renderUnits();
|
||
applyUnitFilter();
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Zuordnen des Arbeitsblatts', e);
|
||
alert('Netzwerkfehler beim Zuordnen des Arbeitsblatts.');
|
||
}
|
||
}
|
||
async function removeWorksheetFromCurrentUnit(filename) {
|
||
if (!currentUnitId) {
|
||
alert('Bitte zuerst eine Lerneinheit auswählen.');
|
||
return;
|
||
}
|
||
if (!filename) {
|
||
alert('Fehler: kein Dateiname übergeben.');
|
||
return;
|
||
}
|
||
|
||
const payload = { worksheet_file: filename };
|
||
|
||
try {
|
||
const resp = await fetch(`/api/learning-units/${currentUnitId}/remove-worksheet`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
if (!resp.ok) {
|
||
console.error('Fehler beim Entfernen des Arbeitsblatts', resp.status);
|
||
alert('Arbeitsblatt konnte nicht aus der Lerneinheit entfernt werden.');
|
||
return;
|
||
}
|
||
|
||
const updated = await resp.json();
|
||
const idx = units.findIndex((u) => u.id === updated.id);
|
||
if (idx !== -1) {
|
||
units[idx] = updated;
|
||
}
|
||
renderUnits();
|
||
applyUnitFilter();
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Entfernen des Arbeitsblatts', e);
|
||
alert('Netzwerkfehler beim Entfernen des Arbeitsblatts.');
|
||
}
|
||
}
|
||
|
||
async function deleteFileCompletely(filename) {
|
||
if (!filename) {
|
||
alert('Fehler: kein Dateiname übergeben.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setStatus('Lösche Datei …', filename, 'busy');
|
||
|
||
const resp = await fetch(`/api/eingang-dateien/${encodeURIComponent(filename)}`, {
|
||
method: 'DELETE',
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
console.error('Fehler beim Löschen der Datei', resp.status);
|
||
setStatus('Fehler beim Löschen', filename, 'error');
|
||
alert('Datei konnte nicht gelöscht werden.');
|
||
return;
|
||
}
|
||
|
||
const result = await resp.json();
|
||
if (result.status === 'OK') {
|
||
setStatus('Datei gelöscht', filename);
|
||
// Dateien neu laden
|
||
await loadEingangFiles();
|
||
await loadWorksheetPairs();
|
||
await loadLearningUnits();
|
||
} else {
|
||
setStatus('Fehler', result.message, 'error');
|
||
alert(result.message);
|
||
}
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Löschen der Datei', e);
|
||
setStatus('Netzwerkfehler', String(e), 'error');
|
||
alert('Netzwerkfehler beim Löschen der Datei.');
|
||
}
|
||
}
|
||
|
||
async function deleteLearningUnit(unitId) {
|
||
if (!unitId) {
|
||
alert('Fehler: keine Lerneinheit-ID übergeben.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setStatus('Lösche Lerneinheit …', '', 'busy');
|
||
|
||
const resp = await fetch(`/api/learning-units/${unitId}`, {
|
||
method: 'DELETE',
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
console.error('Fehler beim Löschen der Lerneinheit', resp.status);
|
||
setStatus('Fehler beim Löschen', '', 'error');
|
||
alert('Lerneinheit konnte nicht gelöscht werden.');
|
||
return;
|
||
}
|
||
|
||
const result = await resp.json();
|
||
if (result.status === 'deleted') {
|
||
setStatus('Lerneinheit gelöscht', '');
|
||
|
||
// Lerneinheit aus der lokalen Liste entfernen
|
||
units = units.filter((u) => u.id !== unitId);
|
||
|
||
// Wenn die gelöschte Einheit ausgewählt war, Auswahl zurücksetzen
|
||
if (currentUnitId === unitId) {
|
||
currentUnitId = units.length > 0 ? units[0].id : null;
|
||
}
|
||
|
||
renderUnits();
|
||
applyUnitFilter();
|
||
} else {
|
||
setStatus('Fehler', 'Unbekannter Fehler', 'error');
|
||
alert('Fehler beim Löschen der Lerneinheit.');
|
||
}
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Löschen der Lerneinheit', e);
|
||
setStatus('Netzwerkfehler', String(e), 'error');
|
||
alert('Netzwerkfehler beim Löschen der Lerneinheit.');
|
||
}
|
||
}
|
||
|
||
if (btnAddUnit) {
|
||
btnAddUnit.addEventListener('click', (ev) => {
|
||
ev.preventDefault();
|
||
addUnitFromForm();
|
||
});
|
||
}
|
||
|
||
if (btnAttachCurrentToLu) {
|
||
btnAttachCurrentToLu.addEventListener('click', (ev) => {
|
||
ev.preventDefault();
|
||
attachCurrentWorksheetToUnit();
|
||
});
|
||
}
|
||
|
||
// --- Filter-Toggle ---
|
||
if (btnToggleFilter) {
|
||
btnToggleFilter.addEventListener('click', () => {
|
||
showOnlyUnitFiles = !showOnlyUnitFiles;
|
||
if (showOnlyUnitFiles) {
|
||
btnToggleFilter.textContent = 'Nur Lerneinheit';
|
||
btnToggleFilter.classList.add('btn-primary');
|
||
} else {
|
||
btnToggleFilter.textContent = 'Alle Dateien';
|
||
btnToggleFilter.classList.remove('btn-primary');
|
||
}
|
||
applyUnitFilter();
|
||
});
|
||
}
|
||
|
||
// --- Multiple Choice Logik ---
|
||
const btnMcGenerate = document.getElementById('btn-mc-generate');
|
||
const btnMcShow = document.getElementById('btn-mc-show');
|
||
const btnMcPrint = document.getElementById('btn-mc-print');
|
||
const mcPreview = document.getElementById('mc-preview');
|
||
const mcBadge = document.getElementById('mc-badge');
|
||
const mcModal = document.getElementById('mc-modal');
|
||
const mcModalBody = document.getElementById('mc-modal-body');
|
||
const mcModalClose = document.getElementById('mc-modal-close');
|
||
|
||
let currentMcData = null;
|
||
let mcAnswers = {}; // Speichert Nutzerantworten
|
||
|
||
async function generateMcQuestions() {
|
||
try {
|
||
setStatus('Generiere MC-Fragen …', 'Bitte warten, KI arbeitet.', 'busy');
|
||
if (mcBadge) mcBadge.textContent = 'Generiert...';
|
||
|
||
const resp = await fetch('/api/generate-mc', { method: 'POST' });
|
||
if (!resp.ok) {
|
||
throw new Error('HTTP ' + resp.status);
|
||
}
|
||
|
||
const result = await resp.json();
|
||
if (result.status === 'OK' && result.generated.length > 0) {
|
||
setStatus('MC-Fragen generiert', result.generated.length + ' Dateien erstellt.');
|
||
if (mcBadge) mcBadge.textContent = 'Fertig';
|
||
if (btnMcShow) btnMcShow.style.display = 'inline-block';
|
||
if (btnMcPrint) btnMcPrint.style.display = 'inline-block';
|
||
|
||
// Lade die erste MC-Datei für Vorschau
|
||
await loadMcPreviewForCurrent();
|
||
} else if (result.errors && result.errors.length > 0) {
|
||
setStatus('Fehler bei MC-Generierung', result.errors[0].error, 'error');
|
||
if (mcBadge) mcBadge.textContent = 'Fehler';
|
||
} else {
|
||
setStatus('Keine MC-Fragen generiert', 'Möglicherweise fehlen Analyse-Daten.', 'error');
|
||
if (mcBadge) mcBadge.textContent = 'Bereit';
|
||
}
|
||
} catch (e) {
|
||
console.error('MC-Generierung fehlgeschlagen:', e);
|
||
setStatus('Fehler bei MC-Generierung', String(e), 'error');
|
||
if (mcBadge) mcBadge.textContent = 'Fehler';
|
||
}
|
||
}
|
||
|
||
async function loadMcPreviewForCurrent() {
|
||
if (!eingangFiles.length) {
|
||
if (mcPreview) mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Arbeitsblätter vorhanden.</div>';
|
||
return;
|
||
}
|
||
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) return;
|
||
|
||
try {
|
||
const resp = await fetch('/api/mc-data/' + encodeURIComponent(currentFile));
|
||
const result = await resp.json();
|
||
|
||
if (result.status === 'OK' && result.data) {
|
||
currentMcData = result.data;
|
||
renderMcPreview(result.data);
|
||
if (btnMcShow) btnMcShow.style.display = 'inline-block';
|
||
if (btnMcPrint) btnMcPrint.style.display = 'inline-block';
|
||
} else {
|
||
if (mcPreview) mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Noch keine MC-Fragen für dieses Arbeitsblatt generiert.</div>';
|
||
currentMcData = null;
|
||
if (btnMcPrint) btnMcPrint.style.display = 'none';
|
||
}
|
||
} catch (e) {
|
||
console.error('Fehler beim Laden der MC-Daten:', e);
|
||
if (mcPreview) mcPreview.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
function renderMcPreview(mcData) {
|
||
if (!mcPreview) return;
|
||
if (!mcData || !mcData.questions || mcData.questions.length === 0) {
|
||
mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Fragen vorhanden.</div>';
|
||
return;
|
||
}
|
||
|
||
const questions = mcData.questions;
|
||
const metadata = mcData.metadata || {};
|
||
|
||
let html = '';
|
||
|
||
// Zeige Metadaten
|
||
if (metadata.grade_level || metadata.subject) {
|
||
html += '<div class="mc-stats">';
|
||
if (metadata.subject) {
|
||
html += '<div class="mc-stats-item"><strong>Fach:</strong> ' + metadata.subject + '</div>';
|
||
}
|
||
if (metadata.grade_level) {
|
||
html += '<div class="mc-stats-item"><strong>Stufe:</strong> ' + metadata.grade_level + '</div>';
|
||
}
|
||
html += '<div class="mc-stats-item"><strong>Fragen:</strong> ' + questions.length + '</div>';
|
||
html += '</div>';
|
||
}
|
||
|
||
// Zeige erste 2 Fragen als Vorschau
|
||
const previewQuestions = questions.slice(0, 2);
|
||
previewQuestions.forEach((q, idx) => {
|
||
html += '<div class="mc-question">';
|
||
html += '<div class="mc-question-text">' + (idx + 1) + '. ' + q.question + '</div>';
|
||
html += '<div class="mc-options">';
|
||
q.options.forEach(opt => {
|
||
html += '<div class="mc-option" data-qid="' + q.id + '" data-opt="' + opt.id + '">';
|
||
html += '<span class="mc-option-label">' + opt.id + ')</span> ' + opt.text;
|
||
html += '</div>';
|
||
});
|
||
html += '</div>';
|
||
html += '</div>';
|
||
});
|
||
|
||
if (questions.length > 2) {
|
||
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:8px;">+ ' + (questions.length - 2) + ' weitere Fragen</div>';
|
||
}
|
||
|
||
mcPreview.innerHTML = html;
|
||
|
||
// Event-Listener für Antwort-Auswahl
|
||
mcPreview.querySelectorAll('.mc-option').forEach(optEl => {
|
||
optEl.addEventListener('click', () => handleMcOptionClick(optEl));
|
||
});
|
||
}
|
||
|
||
function handleMcOptionClick(optEl) {
|
||
const qid = optEl.getAttribute('data-qid');
|
||
const optId = optEl.getAttribute('data-opt');
|
||
|
||
if (!currentMcData) return;
|
||
|
||
// Finde die Frage
|
||
const question = currentMcData.questions.find(q => q.id === qid);
|
||
if (!question) return;
|
||
|
||
// Markiere alle Optionen dieser Frage
|
||
const questionEl = optEl.closest('.mc-question');
|
||
const allOptions = questionEl.querySelectorAll('.mc-option');
|
||
|
||
allOptions.forEach(opt => {
|
||
opt.classList.remove('selected', 'correct', 'incorrect');
|
||
const thisOptId = opt.getAttribute('data-opt');
|
||
if (thisOptId === question.correct_answer) {
|
||
opt.classList.add('correct');
|
||
} else if (thisOptId === optId) {
|
||
opt.classList.add('incorrect');
|
||
}
|
||
});
|
||
|
||
// Speichere Antwort
|
||
mcAnswers[qid] = optId;
|
||
|
||
// Zeige Feedback wenn gewünscht
|
||
const isCorrect = optId === question.correct_answer;
|
||
let feedbackEl = questionEl.querySelector('.mc-feedback');
|
||
if (!feedbackEl) {
|
||
feedbackEl = document.createElement('div');
|
||
feedbackEl.className = 'mc-feedback';
|
||
questionEl.appendChild(feedbackEl);
|
||
}
|
||
|
||
if (isCorrect) {
|
||
feedbackEl.style.background = 'rgba(34,197,94,0.1)';
|
||
feedbackEl.style.borderColor = 'rgba(34,197,94,0.3)';
|
||
feedbackEl.style.color = 'var(--bp-accent)';
|
||
feedbackEl.textContent = 'Richtig! ' + (question.explanation || '');
|
||
} else {
|
||
feedbackEl.style.background = 'rgba(239,68,68,0.1)';
|
||
feedbackEl.style.borderColor = 'rgba(239,68,68,0.3)';
|
||
feedbackEl.style.color = '#ef4444';
|
||
feedbackEl.textContent = 'Leider falsch. ' + (question.explanation || '');
|
||
}
|
||
}
|
||
|
||
function openMcModal() {
|
||
if (!currentMcData || !currentMcData.questions) {
|
||
alert('Keine MC-Fragen vorhanden. Bitte zuerst generieren.');
|
||
return;
|
||
}
|
||
|
||
mcAnswers = {}; // Reset Antworten
|
||
renderMcModal(currentMcData);
|
||
mcModal.classList.remove('hidden');
|
||
}
|
||
|
||
function closeMcModal() {
|
||
mcModal.classList.add('hidden');
|
||
}
|
||
|
||
function renderMcModal(mcData) {
|
||
const questions = mcData.questions;
|
||
const metadata = mcData.metadata || {};
|
||
|
||
let html = '';
|
||
|
||
// Header mit Metadaten
|
||
html += '<div class="mc-stats" style="margin-bottom:16px;">';
|
||
if (metadata.source_title) {
|
||
html += '<div class="mc-stats-item"><strong>Arbeitsblatt:</strong> ' + metadata.source_title + '</div>';
|
||
}
|
||
if (metadata.subject) {
|
||
html += '<div class="mc-stats-item"><strong>Fach:</strong> ' + metadata.subject + '</div>';
|
||
}
|
||
if (metadata.grade_level) {
|
||
html += '<div class="mc-stats-item"><strong>Stufe:</strong> ' + metadata.grade_level + '</div>';
|
||
}
|
||
html += '</div>';
|
||
|
||
// Alle Fragen
|
||
questions.forEach((q, idx) => {
|
||
html += '<div class="mc-question" data-qid="' + q.id + '">';
|
||
html += '<div class="mc-question-text">' + (idx + 1) + '. ' + q.question + '</div>';
|
||
html += '<div class="mc-options">';
|
||
q.options.forEach(opt => {
|
||
html += '<div class="mc-option" data-qid="' + q.id + '" data-opt="' + opt.id + '">';
|
||
html += '<span class="mc-option-label">' + opt.id + ')</span> ' + opt.text;
|
||
html += '</div>';
|
||
});
|
||
html += '</div>';
|
||
html += '</div>';
|
||
});
|
||
|
||
// Auswertungs-Button
|
||
html += '<div style="margin-top:16px;text-align:center;">';
|
||
html += '<button class="btn btn-primary" id="btn-mc-evaluate">Auswerten</button>';
|
||
html += '</div>';
|
||
|
||
mcModalBody.innerHTML = html;
|
||
|
||
// Event-Listener
|
||
mcModalBody.querySelectorAll('.mc-option').forEach(optEl => {
|
||
optEl.addEventListener('click', () => {
|
||
const qid = optEl.getAttribute('data-qid');
|
||
const optId = optEl.getAttribute('data-opt');
|
||
|
||
// Deselektiere andere Optionen der gleichen Frage
|
||
const questionEl = optEl.closest('.mc-question');
|
||
questionEl.querySelectorAll('.mc-option').forEach(o => o.classList.remove('selected'));
|
||
optEl.classList.add('selected');
|
||
|
||
mcAnswers[qid] = optId;
|
||
});
|
||
});
|
||
|
||
const btnEvaluate = document.getElementById('btn-mc-evaluate');
|
||
if (btnEvaluate) {
|
||
btnEvaluate.addEventListener('click', evaluateMcQuiz);
|
||
}
|
||
}
|
||
|
||
function evaluateMcQuiz() {
|
||
if (!currentMcData) return;
|
||
|
||
let correct = 0;
|
||
let total = currentMcData.questions.length;
|
||
|
||
currentMcData.questions.forEach(q => {
|
||
const questionEl = mcModalBody.querySelector('.mc-question[data-qid="' + q.id + '"]');
|
||
if (!questionEl) return;
|
||
|
||
const userAnswer = mcAnswers[q.id];
|
||
const allOptions = questionEl.querySelectorAll('.mc-option');
|
||
|
||
allOptions.forEach(opt => {
|
||
opt.classList.remove('correct', 'incorrect');
|
||
const optId = opt.getAttribute('data-opt');
|
||
if (optId === q.correct_answer) {
|
||
opt.classList.add('correct');
|
||
} else if (optId === userAnswer && userAnswer !== q.correct_answer) {
|
||
opt.classList.add('incorrect');
|
||
}
|
||
});
|
||
|
||
// Zeige Erklärung
|
||
let feedbackEl = questionEl.querySelector('.mc-feedback');
|
||
if (!feedbackEl) {
|
||
feedbackEl = document.createElement('div');
|
||
feedbackEl.className = 'mc-feedback';
|
||
questionEl.appendChild(feedbackEl);
|
||
}
|
||
|
||
if (userAnswer === q.correct_answer) {
|
||
correct++;
|
||
feedbackEl.style.background = 'rgba(34,197,94,0.1)';
|
||
feedbackEl.style.borderColor = 'rgba(34,197,94,0.3)';
|
||
feedbackEl.style.color = 'var(--bp-accent)';
|
||
feedbackEl.textContent = 'Richtig! ' + (q.explanation || '');
|
||
} else if (userAnswer) {
|
||
feedbackEl.style.background = 'rgba(239,68,68,0.1)';
|
||
feedbackEl.style.borderColor = 'rgba(239,68,68,0.3)';
|
||
feedbackEl.style.color = '#ef4444';
|
||
feedbackEl.textContent = 'Falsch. ' + (q.explanation || '');
|
||
} else {
|
||
feedbackEl.style.background = 'rgba(148,163,184,0.1)';
|
||
feedbackEl.style.borderColor = 'rgba(148,163,184,0.3)';
|
||
feedbackEl.style.color = 'var(--bp-text-muted)';
|
||
feedbackEl.textContent = 'Nicht beantwortet. Richtig wäre: ' + q.correct_answer.toUpperCase();
|
||
}
|
||
});
|
||
|
||
// Zeige Gesamtergebnis
|
||
const resultHtml = '<div style="margin-top:16px;padding:12px;background:rgba(15,23,42,0.6);border-radius:8px;text-align:center;">' +
|
||
'<div style="font-size:18px;font-weight:600;">' + correct + ' von ' + total + ' richtig</div>' +
|
||
'<div style="font-size:12px;color:var(--bp-text-muted);margin-top:4px;">' + Math.round(correct / total * 100) + '% korrekt</div>' +
|
||
'</div>';
|
||
|
||
const existingResult = mcModalBody.querySelector('.mc-result');
|
||
if (existingResult) {
|
||
existingResult.remove();
|
||
}
|
||
|
||
const resultDiv = document.createElement('div');
|
||
resultDiv.className = 'mc-result';
|
||
resultDiv.innerHTML = resultHtml;
|
||
mcModalBody.appendChild(resultDiv);
|
||
}
|
||
|
||
function openMcPrintDialog() {
|
||
if (!currentMcData) {
|
||
alert(t('mc_no_questions') || 'Keine MC-Fragen vorhanden.');
|
||
return;
|
||
}
|
||
const currentFile = eingangFiles[currentIndex];
|
||
const choice = confirm((t('mc_print_with_answers') || 'Mit Lösungen drucken?') + '\\n\\nOK = Lösungsblatt mit markierten Antworten\\nAbbrechen = Übungsblatt ohne Lösungen');
|
||
const url = '/api/print-mc/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
|
||
window.open(url, '_blank');
|
||
}
|
||
|
||
// Event Listener für MC-Buttons
|
||
if (btnMcGenerate) {
|
||
btnMcGenerate.addEventListener('click', generateMcQuestions);
|
||
}
|
||
|
||
if (btnMcShow) {
|
||
btnMcShow.addEventListener('click', openMcModal);
|
||
}
|
||
|
||
if (btnMcPrint) {
|
||
btnMcPrint.addEventListener('click', openMcPrintDialog);
|
||
}
|
||
|
||
if (mcModalClose) {
|
||
mcModalClose.addEventListener('click', closeMcModal);
|
||
}
|
||
|
||
if (mcModal) {
|
||
mcModal.addEventListener('click', (ev) => {
|
||
if (ev.target === mcModal) {
|
||
closeMcModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- Lückentext (Cloze) Logik ---
|
||
const btnClozeGenerate = document.getElementById('btn-cloze-generate');
|
||
const btnClozeShow = document.getElementById('btn-cloze-show');
|
||
const btnClozePrint = document.getElementById('btn-cloze-print');
|
||
const clozePreview = document.getElementById('cloze-preview');
|
||
const clozeBadge = document.getElementById('cloze-badge');
|
||
const clozeLanguageSelect = document.getElementById('cloze-language');
|
||
const clozeModal = document.getElementById('cloze-modal');
|
||
const clozeModalBody = document.getElementById('cloze-modal-body');
|
||
const clozeModalClose = document.getElementById('cloze-modal-close');
|
||
|
||
let currentClozeData = null;
|
||
let clozeAnswers = {}; // Speichert Nutzerantworten
|
||
|
||
async function generateClozeTexts() {
|
||
const targetLang = clozeLanguageSelect ? clozeLanguageSelect.value : 'tr';
|
||
|
||
try {
|
||
setStatus('Generiere Lückentexte …', 'Bitte warten, KI arbeitet.', 'busy');
|
||
if (clozeBadge) clozeBadge.textContent = 'Generiert...';
|
||
|
||
const resp = await fetch('/api/generate-cloze?target_language=' + targetLang, { method: 'POST' });
|
||
if (!resp.ok) {
|
||
throw new Error('HTTP ' + resp.status);
|
||
}
|
||
|
||
const result = await resp.json();
|
||
if (result.status === 'OK' && result.generated.length > 0) {
|
||
setStatus('Lückentexte generiert', result.generated.length + ' Dateien erstellt.');
|
||
if (clozeBadge) clozeBadge.textContent = 'Fertig';
|
||
if (btnClozeShow) btnClozeShow.style.display = 'inline-block';
|
||
if (btnClozePrint) btnClozePrint.style.display = 'inline-block';
|
||
|
||
// Lade Vorschau für aktuelle Datei
|
||
await loadClozePreviewForCurrent();
|
||
} else if (result.errors && result.errors.length > 0) {
|
||
setStatus('Fehler bei Lückentext-Generierung', result.errors[0].error, 'error');
|
||
if (clozeBadge) clozeBadge.textContent = 'Fehler';
|
||
} else {
|
||
setStatus('Keine Lückentexte generiert', 'Möglicherweise fehlen Analyse-Daten.', 'error');
|
||
if (clozeBadge) clozeBadge.textContent = 'Bereit';
|
||
}
|
||
} catch (e) {
|
||
console.error('Lückentext-Generierung fehlgeschlagen:', e);
|
||
setStatus('Fehler bei Lückentext-Generierung', String(e), 'error');
|
||
if (clozeBadge) clozeBadge.textContent = 'Fehler';
|
||
}
|
||
}
|
||
|
||
async function loadClozePreviewForCurrent() {
|
||
if (!eingangFiles.length) {
|
||
if (clozePreview) clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Arbeitsblätter vorhanden.</div>';
|
||
return;
|
||
}
|
||
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) return;
|
||
|
||
try {
|
||
const resp = await fetch('/api/cloze-data/' + encodeURIComponent(currentFile));
|
||
const result = await resp.json();
|
||
|
||
if (result.status === 'OK' && result.data) {
|
||
currentClozeData = result.data;
|
||
renderClozePreview(result.data);
|
||
if (btnClozeShow) btnClozeShow.style.display = 'inline-block';
|
||
if (btnClozePrint) btnClozePrint.style.display = 'inline-block';
|
||
} else {
|
||
if (clozePreview) clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Noch keine Lückentexte für dieses Arbeitsblatt generiert.</div>';
|
||
currentClozeData = null;
|
||
if (btnClozeShow) btnClozeShow.style.display = 'none';
|
||
if (btnClozePrint) btnClozePrint.style.display = 'none';
|
||
}
|
||
} catch (e) {
|
||
console.error('Fehler beim Laden der Lückentext-Daten:', e);
|
||
if (clozePreview) clozePreview.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
function renderClozePreview(clozeData) {
|
||
if (!clozePreview) return;
|
||
if (!clozeData || !clozeData.cloze_items || clozeData.cloze_items.length === 0) {
|
||
clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Lückentexte vorhanden.</div>';
|
||
return;
|
||
}
|
||
|
||
const items = clozeData.cloze_items;
|
||
const metadata = clozeData.metadata || {};
|
||
|
||
let html = '';
|
||
|
||
// Statistiken
|
||
html += '<div class="cloze-stats">';
|
||
if (metadata.subject) {
|
||
html += '<div><strong>Fach:</strong> ' + metadata.subject + '</div>';
|
||
}
|
||
if (metadata.grade_level) {
|
||
html += '<div><strong>Stufe:</strong> ' + metadata.grade_level + '</div>';
|
||
}
|
||
html += '<div><strong>Sätze:</strong> ' + items.length + '</div>';
|
||
if (metadata.total_gaps) {
|
||
html += '<div><strong>Lücken:</strong> ' + metadata.total_gaps + '</div>';
|
||
}
|
||
html += '</div>';
|
||
|
||
// Zeige erste 2 Sätze als Vorschau
|
||
const previewItems = items.slice(0, 2);
|
||
previewItems.forEach((item, idx) => {
|
||
html += '<div class="cloze-item">';
|
||
html += '<div class="cloze-sentence">' + (idx + 1) + '. ' + item.sentence_with_gaps.replace(/___/g, '<span class="cloze-gap">___</span>') + '</div>';
|
||
|
||
// Übersetzung anzeigen
|
||
if (item.translation && item.translation.full_sentence) {
|
||
html += '<div class="cloze-translation">';
|
||
html += '<div class="cloze-translation-label">' + (item.translation.language_name || 'Übersetzung') + ':</div>';
|
||
html += item.translation.full_sentence;
|
||
html += '</div>';
|
||
}
|
||
html += '</div>';
|
||
});
|
||
|
||
if (items.length > 2) {
|
||
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:8px;">+ ' + (items.length - 2) + ' weitere Sätze</div>';
|
||
}
|
||
|
||
clozePreview.innerHTML = html;
|
||
}
|
||
|
||
function openClozeModal() {
|
||
if (!currentClozeData || !currentClozeData.cloze_items) {
|
||
alert('Keine Lückentexte vorhanden. Bitte zuerst generieren.');
|
||
return;
|
||
}
|
||
|
||
clozeAnswers = {}; // Reset Antworten
|
||
renderClozeModal(currentClozeData);
|
||
clozeModal.classList.remove('hidden');
|
||
}
|
||
|
||
function closeClozeModal() {
|
||
clozeModal.classList.add('hidden');
|
||
}
|
||
|
||
function renderClozeModal(clozeData) {
|
||
const items = clozeData.cloze_items;
|
||
const metadata = clozeData.metadata || {};
|
||
|
||
let html = '';
|
||
|
||
// Header
|
||
html += '<div class="cloze-stats" style="margin-bottom:16px;">';
|
||
if (metadata.source_title) {
|
||
html += '<div><strong>Arbeitsblatt:</strong> ' + metadata.source_title + '</div>';
|
||
}
|
||
if (metadata.total_gaps) {
|
||
html += '<div><strong>Lücken gesamt:</strong> ' + metadata.total_gaps + '</div>';
|
||
}
|
||
html += '</div>';
|
||
|
||
html += '<div style="font-size:12px;color:var(--bp-text-muted);margin-bottom:12px;">Fülle die Lücken aus und klicke auf "Prüfen".</div>';
|
||
|
||
// Alle Sätze mit Eingabefeldern
|
||
items.forEach((item, idx) => {
|
||
html += '<div class="cloze-item" data-cid="' + item.id + '">';
|
||
|
||
// Satz mit Eingabefeldern statt ___
|
||
let sentenceHtml = item.sentence_with_gaps;
|
||
const gaps = item.gaps || [];
|
||
|
||
// Ersetze ___ durch Eingabefelder
|
||
let gapIndex = 0;
|
||
sentenceHtml = sentenceHtml.replace(/___/g, () => {
|
||
const gap = gaps[gapIndex] || { id: 'g' + gapIndex, word: '' };
|
||
const inputId = item.id + '_' + gap.id;
|
||
gapIndex++;
|
||
return '<input type="text" class="cloze-gap-input" data-cid="' + item.id + '" data-gid="' + gap.id + '" data-answer="' + gap.word + '" id="input_' + inputId + '" autocomplete="off">';
|
||
});
|
||
|
||
html += '<div class="cloze-sentence">' + (idx + 1) + '. ' + sentenceHtml + '</div>';
|
||
|
||
// Übersetzung als Hilfe
|
||
if (item.translation && item.translation.sentence_with_gaps) {
|
||
html += '<div class="cloze-translation">';
|
||
html += '<div class="cloze-translation-label">' + (item.translation.language_name || 'Übersetzung') + ' (mit Lücken):</div>';
|
||
html += item.translation.sentence_with_gaps;
|
||
html += '</div>';
|
||
}
|
||
|
||
html += '</div>';
|
||
});
|
||
|
||
// Buttons
|
||
html += '<div style="margin-top:16px;text-align:center;display:flex;gap:8px;justify-content:center;">';
|
||
html += '<button class="btn btn-primary" id="btn-cloze-check">Prüfen</button>';
|
||
html += '<button class="btn btn-ghost" id="btn-cloze-show-answers">Lösungen zeigen</button>';
|
||
html += '</div>';
|
||
|
||
clozeModalBody.innerHTML = html;
|
||
|
||
// Event-Listener für Prüfen-Button
|
||
const btnCheck = document.getElementById('btn-cloze-check');
|
||
if (btnCheck) {
|
||
btnCheck.addEventListener('click', checkClozeAnswers);
|
||
}
|
||
|
||
// Event-Listener für Lösungen zeigen
|
||
const btnShowAnswers = document.getElementById('btn-cloze-show-answers');
|
||
if (btnShowAnswers) {
|
||
btnShowAnswers.addEventListener('click', showClozeAnswers);
|
||
}
|
||
|
||
// Enter-Taste zum Prüfen
|
||
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
|
||
input.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
checkClozeAnswers();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function checkClozeAnswers() {
|
||
let correct = 0;
|
||
let total = 0;
|
||
|
||
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
|
||
const userAnswer = input.value.trim().toLowerCase();
|
||
const correctAnswer = input.getAttribute('data-answer').toLowerCase();
|
||
total++;
|
||
|
||
// Entferne vorherige Klassen
|
||
input.classList.remove('correct', 'incorrect');
|
||
|
||
if (userAnswer === correctAnswer) {
|
||
input.classList.add('correct');
|
||
correct++;
|
||
} else if (userAnswer !== '') {
|
||
input.classList.add('incorrect');
|
||
}
|
||
});
|
||
|
||
// Zeige Ergebnis
|
||
let existingResult = clozeModalBody.querySelector('.cloze-result');
|
||
if (existingResult) existingResult.remove();
|
||
|
||
const resultHtml = '<div class="cloze-result" style="margin-top:16px;padding:12px;background:rgba(15,23,42,0.6);border-radius:8px;text-align:center;">' +
|
||
'<div style="font-size:18px;font-weight:600;">' + correct + ' von ' + total + ' richtig</div>' +
|
||
'<div style="font-size:12px;color:var(--bp-text-muted);margin-top:4px;">' + Math.round(correct / total * 100) + '% korrekt</div>' +
|
||
'</div>';
|
||
|
||
const resultDiv = document.createElement('div');
|
||
resultDiv.innerHTML = resultHtml;
|
||
clozeModalBody.appendChild(resultDiv.firstChild);
|
||
}
|
||
|
||
function showClozeAnswers() {
|
||
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
|
||
const correctAnswer = input.getAttribute('data-answer');
|
||
input.value = correctAnswer;
|
||
input.classList.remove('incorrect');
|
||
input.classList.add('correct');
|
||
});
|
||
}
|
||
|
||
function openClozePrintDialog() {
|
||
if (!currentClozeData) {
|
||
alert(t('cloze_no_texts') || 'Keine Lückentexte vorhanden.');
|
||
return;
|
||
}
|
||
|
||
const currentFile = eingangFiles[currentIndex];
|
||
|
||
// Öffne Druck-Optionen
|
||
const choice = confirm((t('cloze_print_with_answers') || 'Mit Lösungen drucken?') + '\\n\\nOK = Mit ausgefüllten Lücken\\nAbbrechen = Übungsblatt mit Wortbank');
|
||
|
||
const url = '/api/print-cloze/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
|
||
window.open(url, '_blank');
|
||
}
|
||
|
||
// Event Listener für Cloze-Buttons
|
||
if (btnClozeGenerate) {
|
||
btnClozeGenerate.addEventListener('click', generateClozeTexts);
|
||
}
|
||
|
||
if (btnClozeShow) {
|
||
btnClozeShow.addEventListener('click', openClozeModal);
|
||
}
|
||
|
||
if (btnClozePrint) {
|
||
btnClozePrint.addEventListener('click', openClozePrintDialog);
|
||
}
|
||
|
||
if (clozeModalClose) {
|
||
clozeModalClose.addEventListener('click', closeClozeModal);
|
||
}
|
||
|
||
if (clozeModal) {
|
||
clozeModal.addEventListener('click', (ev) => {
|
||
if (ev.target === clozeModal) {
|
||
closeClozeModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- Mindmap Lernposter Logik ---
|
||
const btnMindmapGenerate = document.getElementById('btn-mindmap-generate');
|
||
const btnMindmapShow = document.getElementById('btn-mindmap-show');
|
||
const btnMindmapPrint = document.getElementById('btn-mindmap-print');
|
||
const mindmapPreview = document.getElementById('mindmap-preview');
|
||
const mindmapBadge = document.getElementById('mindmap-badge');
|
||
|
||
let currentMindmapData = null;
|
||
|
||
async function generateMindmap() {
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) {
|
||
setStatus('error', t('select_file_first') || 'Bitte zuerst eine Datei auswählen');
|
||
return;
|
||
}
|
||
|
||
if (mindmapBadge) {
|
||
mindmapBadge.textContent = t('generating') || 'Generiere...';
|
||
mindmapBadge.className = 'card-badge badge-working';
|
||
}
|
||
setStatus('working', t('generating_mindmap') || 'Erstelle Mindmap...');
|
||
|
||
try {
|
||
const resp = await fetch('/api/generate-mindmap/' + encodeURIComponent(currentFile), {
|
||
method: 'POST'
|
||
});
|
||
const data = await resp.json();
|
||
|
||
if (data.status === 'OK') {
|
||
if (mindmapBadge) {
|
||
mindmapBadge.textContent = t('ready') || 'Fertig';
|
||
mindmapBadge.className = 'card-badge badge-success';
|
||
}
|
||
setStatus('ok', t('mindmap_generated') || 'Mindmap erstellt!');
|
||
|
||
// Lade Mindmap-Daten
|
||
await loadMindmapData();
|
||
} else if (data.status === 'NOT_FOUND') {
|
||
if (mindmapBadge) {
|
||
mindmapBadge.textContent = t('no_analysis') || 'Keine Analyse';
|
||
mindmapBadge.className = 'card-badge badge-error';
|
||
}
|
||
setStatus('error', t('analyze_first') || 'Bitte zuerst analysieren (Neuaufbau starten)');
|
||
} else {
|
||
throw new Error(data.message || 'Fehler bei der Mindmap-Generierung');
|
||
}
|
||
} catch (err) {
|
||
console.error('Mindmap error:', err);
|
||
if (mindmapBadge) {
|
||
mindmapBadge.textContent = t('error') || 'Fehler';
|
||
mindmapBadge.className = 'card-badge badge-error';
|
||
}
|
||
setStatus('error', err.message);
|
||
}
|
||
}
|
||
|
||
async function loadMindmapData() {
|
||
if (!eingangFiles.length) {
|
||
if (mindmapPreview) mindmapPreview.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) return;
|
||
|
||
try {
|
||
const resp = await fetch('/api/mindmap-data/' + encodeURIComponent(currentFile));
|
||
const data = await resp.json();
|
||
|
||
if (data.status === 'OK' && data.data) {
|
||
currentMindmapData = data.data;
|
||
renderMindmapPreview();
|
||
if (btnMindmapShow) btnMindmapShow.style.display = 'inline-block';
|
||
if (btnMindmapPrint) btnMindmapPrint.style.display = 'inline-block';
|
||
if (mindmapBadge) {
|
||
mindmapBadge.textContent = t('ready') || 'Fertig';
|
||
mindmapBadge.className = 'card-badge badge-success';
|
||
}
|
||
} else {
|
||
currentMindmapData = null;
|
||
if (mindmapPreview) mindmapPreview.innerHTML = '';
|
||
if (btnMindmapShow) btnMindmapShow.style.display = 'none';
|
||
if (btnMindmapPrint) btnMindmapPrint.style.display = 'none';
|
||
if (mindmapBadge) {
|
||
mindmapBadge.textContent = t('ready') || 'Bereit';
|
||
mindmapBadge.className = 'card-badge';
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('Error loading mindmap:', err);
|
||
}
|
||
}
|
||
|
||
function renderMindmapPreview() {
|
||
if (!mindmapPreview) return;
|
||
|
||
if (!currentMindmapData) {
|
||
mindmapPreview.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
const topic = currentMindmapData.topic || 'Thema';
|
||
const categories = currentMindmapData.categories || [];
|
||
const categoryCount = categories.length;
|
||
const termCount = categories.reduce((sum, cat) => sum + (cat.terms ? cat.terms.length : 0), 0);
|
||
|
||
mindmapPreview.innerHTML = '<div style="margin-top:10px;padding:12px;background:linear-gradient(135deg,#f0f9ff,#e0f2fe);border-radius:10px;text-align:center;">' +
|
||
'<div style="font-size:18px;font-weight:bold;color:#0369a1;margin-bottom:8px;">' + topic + '</div>' +
|
||
'<div style="font-size:12px;color:#64748b;">' + categoryCount + ' ' + (t('categories') || 'Kategorien') + ' | ' + termCount + ' ' + (t('terms') || 'Begriffe') + '</div>' +
|
||
'</div>';
|
||
}
|
||
|
||
function openMindmapView() {
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) return;
|
||
window.open('/api/mindmap-html/' + encodeURIComponent(currentFile) + '?format=a4', '_blank');
|
||
}
|
||
|
||
function openMindmapPrint() {
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) return;
|
||
window.open('/api/mindmap-html/' + encodeURIComponent(currentFile) + '?format=a3', '_blank');
|
||
}
|
||
|
||
if (btnMindmapGenerate) {
|
||
btnMindmapGenerate.addEventListener('click', generateMindmap);
|
||
}
|
||
|
||
if (btnMindmapShow) {
|
||
btnMindmapShow.addEventListener('click', openMindmapView);
|
||
}
|
||
|
||
if (btnMindmapPrint) {
|
||
btnMindmapPrint.addEventListener('click', openMindmapPrint);
|
||
}
|
||
|
||
// --- Frage-Antwort (Q&A) mit Leitner-System ---
|
||
const btnQaGenerate = document.getElementById('btn-qa-generate');
|
||
const btnQaLearn = document.getElementById('btn-qa-learn');
|
||
const btnQaPrint = document.getElementById('btn-qa-print');
|
||
const qaPreview = document.getElementById('qa-preview');
|
||
const qaBadge = document.getElementById('qa-badge');
|
||
const qaModal = document.getElementById('qa-modal');
|
||
const qaModalBody = document.getElementById('qa-modal-body');
|
||
const qaModalClose = document.getElementById('qa-modal-close');
|
||
|
||
let currentQaData = null;
|
||
let currentQaIndex = 0;
|
||
let qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
|
||
|
||
async function generateQaQuestions() {
|
||
try {
|
||
setStatus(t('status_generating_qa') || 'Generiere Q&A …', t('status_please_wait'), 'busy');
|
||
if (qaBadge) qaBadge.textContent = t('mc_generating');
|
||
|
||
const resp = await fetch('/api/generate-qa', { method: 'POST' });
|
||
if (!resp.ok) {
|
||
throw new Error('HTTP ' + resp.status);
|
||
}
|
||
|
||
const result = await resp.json();
|
||
if (result.status === 'OK' && result.generated.length > 0) {
|
||
setStatus(t('status_qa_generated') || 'Q&A generiert', result.generated.length + ' ' + t('status_files_created'));
|
||
if (qaBadge) qaBadge.textContent = t('mc_done');
|
||
if (btnQaLearn) btnQaLearn.style.display = 'inline-block';
|
||
if (btnQaPrint) btnQaPrint.style.display = 'inline-block';
|
||
|
||
await loadQaPreviewForCurrent();
|
||
} else if (result.errors && result.errors.length > 0) {
|
||
setStatus(t('error'), result.errors[0].error, 'error');
|
||
if (qaBadge) qaBadge.textContent = t('mc_error');
|
||
} else {
|
||
setStatus(t('error'), 'Keine Q&A generiert.', 'error');
|
||
if (qaBadge) qaBadge.textContent = t('mc_ready');
|
||
}
|
||
} catch (e) {
|
||
console.error('Q&A-Generierung fehlgeschlagen:', e);
|
||
setStatus(t('error'), String(e), 'error');
|
||
if (qaBadge) qaBadge.textContent = t('mc_error');
|
||
}
|
||
}
|
||
|
||
async function loadQaPreviewForCurrent() {
|
||
if (!eingangFiles.length) {
|
||
if (qaPreview) qaPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + t('qa_no_questions') + '</div>';
|
||
return;
|
||
}
|
||
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) return;
|
||
|
||
try {
|
||
const resp = await fetch('/api/qa-data/' + encodeURIComponent(currentFile));
|
||
const result = await resp.json();
|
||
|
||
if (result.status === 'OK' && result.data) {
|
||
currentQaData = result.data;
|
||
renderQaPreview(result.data);
|
||
if (btnQaLearn) btnQaLearn.style.display = 'inline-block';
|
||
if (btnQaPrint) btnQaPrint.style.display = 'inline-block';
|
||
} else {
|
||
if (qaPreview) qaPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('qa_no_questions') || 'Noch keine Q&A für dieses Arbeitsblatt generiert.') + '</div>';
|
||
currentQaData = null;
|
||
if (btnQaLearn) btnQaLearn.style.display = 'none';
|
||
if (btnQaPrint) btnQaPrint.style.display = 'none';
|
||
}
|
||
} catch (e) {
|
||
console.error('Fehler beim Laden der Q&A-Daten:', e);
|
||
if (qaPreview) qaPreview.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
function renderQaPreview(qaData) {
|
||
if (!qaPreview) return;
|
||
if (!qaData || !qaData.qa_items || qaData.qa_items.length === 0) {
|
||
qaPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + t('qa_no_questions') + '</div>';
|
||
return;
|
||
}
|
||
|
||
const items = qaData.qa_items;
|
||
const metadata = qaData.metadata || {};
|
||
|
||
let html = '';
|
||
|
||
// Leitner-Box Statistiken
|
||
html += '<div class="mc-stats" style="margin-bottom:8px;">';
|
||
|
||
// Zähle Fragen nach Box
|
||
let box0 = 0, box1 = 0, box2 = 0;
|
||
items.forEach(item => {
|
||
const box = item.leitner ? item.leitner.box : 0;
|
||
if (box === 0) box0++;
|
||
else if (box === 1) box1++;
|
||
else box2++;
|
||
});
|
||
|
||
html += '<div style="display:flex;gap:12px;font-size:11px;">';
|
||
html += '<div style="color:#ef4444;">' + (t('qa_box_new') || 'Neu') + ': ' + box0 + '</div>';
|
||
html += '<div style="color:#f59e0b;">' + (t('qa_box_learning') || 'Lernt') + ': ' + box1 + '</div>';
|
||
html += '<div style="color:#22c55e;">' + (t('qa_box_mastered') || 'Gefestigt') + ': ' + box2 + '</div>';
|
||
html += '</div>';
|
||
html += '</div>';
|
||
|
||
// Zeige erste 2 Fragen als Vorschau
|
||
const previewItems = items.slice(0, 2);
|
||
previewItems.forEach((item, idx) => {
|
||
html += '<div class="mc-question" style="padding:8px;margin-bottom:6px;background:rgba(255,255,255,0.03);border-radius:6px;">';
|
||
html += '<div style="font-size:12px;font-weight:500;margin-bottom:4px;">' + (idx + 1) + '. ' + item.question + '</div>';
|
||
html += '<div style="font-size:11px;color:var(--bp-text-muted);">→ ' + item.answer.substring(0, 60) + (item.answer.length > 60 ? '...' : '') + '</div>';
|
||
html += '</div>';
|
||
});
|
||
|
||
if (items.length > 2) {
|
||
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:4px;">+ ' + (items.length - 2) + ' ' + (t('questions') || 'weitere Fragen') + '</div>';
|
||
}
|
||
|
||
qaPreview.innerHTML = html;
|
||
}
|
||
|
||
function openQaModal() {
|
||
if (!currentQaData || !currentQaData.qa_items) {
|
||
alert(t('qa_no_questions') || 'Keine Q&A vorhanden. Bitte zuerst generieren.');
|
||
return;
|
||
}
|
||
|
||
currentQaIndex = 0;
|
||
qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
|
||
renderQaLearningCard();
|
||
qaModal.classList.remove('hidden');
|
||
}
|
||
|
||
function closeQaModal() {
|
||
qaModal.classList.add('hidden');
|
||
}
|
||
|
||
function renderQaLearningCard() {
|
||
const items = currentQaData.qa_items;
|
||
|
||
if (currentQaIndex >= items.length) {
|
||
// Alle Fragen durch - Zeige Zusammenfassung
|
||
renderQaSessionSummary();
|
||
return;
|
||
}
|
||
|
||
const item = items[currentQaIndex];
|
||
const leitner = item.leitner || { box: 0 };
|
||
const boxNames = [t('qa_box_new') || 'Neu', t('qa_box_learning') || 'Gelernt', t('qa_box_mastered') || 'Gefestigt'];
|
||
const boxColors = ['#ef4444', '#f59e0b', '#22c55e'];
|
||
|
||
let html = '';
|
||
|
||
// Fortschrittsanzeige
|
||
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">';
|
||
html += '<div style="font-size:12px;color:var(--bp-text-muted);">' + (t('question') || 'Frage') + ' ' + (currentQaIndex + 1) + ' / ' + items.length + '</div>';
|
||
html += '<div style="display:flex;align-items:center;gap:6px;">';
|
||
html += '<span style="font-size:10px;color:' + boxColors[leitner.box] + ';background:' + boxColors[leitner.box] + '22;padding:2px 8px;border-radius:10px;">' + boxNames[leitner.box] + '</span>';
|
||
html += '</div>';
|
||
html += '</div>';
|
||
|
||
// Frage
|
||
html += '<div style="background:rgba(255,255,255,0.05);padding:20px;border-radius:12px;margin-bottom:16px;">';
|
||
html += '<div style="font-size:11px;color:var(--bp-text-muted);margin-bottom:8px;">' + (t('question') || 'Frage') + ':</div>';
|
||
html += '<div style="font-size:16px;font-weight:500;line-height:1.5;">' + item.question + '</div>';
|
||
html += '</div>';
|
||
|
||
// Eingabefeld für eigene Antwort
|
||
html += '<div id="qa-input-container" style="margin-bottom:16px;">';
|
||
html += '<div style="font-size:11px;color:var(--bp-text-muted);margin-bottom:8px;">' + (t('qa_your_answer') || 'Deine Antwort') + ':</div>';
|
||
html += '<textarea id="qa-user-answer" style="width:100%;min-height:80px;padding:12px;border:1px solid rgba(255,255,255,0.2);border-radius:8px;background:rgba(255,255,255,0.05);color:var(--bp-text);font-size:14px;resize:vertical;font-family:inherit;" placeholder="' + (t('qa_type_answer') || 'Schreibe deine Antwort hier...') + '"></textarea>';
|
||
html += '</div>';
|
||
|
||
// Prüfen-Button
|
||
html += '<div id="qa-check-btn-container" style="text-align:center;margin-bottom:16px;">';
|
||
html += '<button class="btn btn-primary" id="btn-qa-check-answer" style="padding:12px 32px;">' + (t('qa_check_answer') || 'Antwort prüfen') + '</button>';
|
||
html += '</div>';
|
||
|
||
// Vergleichs-Container (versteckt)
|
||
html += '<div id="qa-comparison-container" style="display:none;">';
|
||
|
||
// Eigene Antwort (wird befüllt)
|
||
html += '<div id="qa-user-answer-display" style="background:rgba(59,130,246,0.1);padding:16px;border-radius:12px;margin-bottom:12px;border-left:3px solid #3b82f6;">';
|
||
html += '<div style="font-size:11px;color:#3b82f6;margin-bottom:8px;">' + (t('qa_your_answer') || 'Deine Antwort') + ':</div>';
|
||
html += '<div id="qa-user-answer-text" style="font-size:14px;line-height:1.5;"></div>';
|
||
html += '</div>';
|
||
|
||
// Richtige Antwort
|
||
html += '<div style="background:rgba(34,197,94,0.1);padding:16px;border-radius:12px;margin-bottom:16px;border-left:3px solid #22c55e;">';
|
||
html += '<div style="font-size:11px;color:#22c55e;margin-bottom:8px;">' + (t('qa_correct_answer') || 'Richtige Antwort') + ':</div>';
|
||
html += '<div style="font-size:14px;line-height:1.5;">' + item.answer + '</div>';
|
||
if (item.key_terms && item.key_terms.length > 0) {
|
||
html += '<div style="margin-top:12px;font-size:11px;color:var(--bp-text-muted);">' + (t('qa_key_terms') || 'Schlüsselbegriffe') + ': <span style="color:#22c55e;">' + item.key_terms.join(', ') + '</span></div>';
|
||
}
|
||
html += '</div>';
|
||
|
||
// Selbstbewertung
|
||
html += '<div style="text-align:center;margin-bottom:8px;">';
|
||
html += '<div style="font-size:12px;color:var(--bp-text-muted);margin-bottom:12px;">' + (t('qa_self_evaluate') || 'War deine Antwort richtig?') + '</div>';
|
||
html += '<div style="display:flex;gap:12px;justify-content:center;">';
|
||
html += '<button class="btn" id="btn-qa-incorrect" style="background:#ef4444;padding:12px 24px;">' + (t('qa_incorrect') || 'Falsch') + '</button>';
|
||
html += '<button class="btn" id="btn-qa-correct" style="background:#22c55e;padding:12px 24px;">' + (t('qa_correct') || 'Richtig') + '</button>';
|
||
html += '</div>';
|
||
html += '</div>';
|
||
|
||
html += '</div>'; // Ende qa-comparison-container
|
||
|
||
// Session-Statistik
|
||
html += '<div style="margin-top:16px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.1);display:flex;justify-content:center;gap:20px;font-size:11px;">';
|
||
html += '<div style="color:#22c55e;">' + (t('qa_session_correct') || 'Richtig') + ': ' + qaSessionStats.correct + '</div>';
|
||
html += '<div style="color:#ef4444;">' + (t('qa_session_incorrect') || 'Falsch') + ': ' + qaSessionStats.incorrect + '</div>';
|
||
html += '</div>';
|
||
|
||
qaModalBody.innerHTML = html;
|
||
|
||
// Event Listener für Prüfen-Button
|
||
document.getElementById('btn-qa-check-answer').addEventListener('click', () => {
|
||
const userAnswer = document.getElementById('qa-user-answer').value.trim();
|
||
|
||
// Zeige die eigene Antwort im Vergleich
|
||
document.getElementById('qa-user-answer-text').textContent = userAnswer || (t('qa_no_answer') || '(keine Antwort eingegeben)');
|
||
|
||
// Verstecke Eingabe, zeige Vergleich
|
||
document.getElementById('qa-input-container').style.display = 'none';
|
||
document.getElementById('qa-check-btn-container').style.display = 'none';
|
||
document.getElementById('qa-comparison-container').style.display = 'block';
|
||
});
|
||
|
||
// Enter-Taste im Textfeld löst Prüfen aus
|
||
document.getElementById('qa-user-answer').addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter' && !e.shiftKey) {
|
||
e.preventDefault();
|
||
document.getElementById('btn-qa-check-answer').click();
|
||
}
|
||
});
|
||
|
||
// Fokus auf Textfeld setzen
|
||
setTimeout(() => {
|
||
document.getElementById('qa-user-answer').focus();
|
||
}, 100);
|
||
|
||
document.getElementById('btn-qa-correct').addEventListener('click', () => handleQaAnswer(true));
|
||
document.getElementById('btn-qa-incorrect').addEventListener('click', () => handleQaAnswer(false));
|
||
}
|
||
|
||
async function handleQaAnswer(correct) {
|
||
const item = currentQaData.qa_items[currentQaIndex];
|
||
|
||
// Update Session-Statistik
|
||
qaSessionStats.total++;
|
||
if (correct) qaSessionStats.correct++;
|
||
else qaSessionStats.incorrect++;
|
||
|
||
// Update Leitner-Fortschritt auf dem Server
|
||
try {
|
||
const currentFile = eingangFiles[currentIndex];
|
||
await fetch('/api/qa-progress?filename=' + encodeURIComponent(currentFile) + '&item_id=' + encodeURIComponent(item.id) + '&correct=' + correct, {
|
||
method: 'POST'
|
||
});
|
||
} catch (e) {
|
||
console.error('Fehler beim Speichern des Fortschritts:', e);
|
||
}
|
||
|
||
// Nächste Frage
|
||
currentQaIndex++;
|
||
renderQaLearningCard();
|
||
}
|
||
|
||
function renderQaSessionSummary() {
|
||
const percent = qaSessionStats.total > 0 ? Math.round(qaSessionStats.correct / qaSessionStats.total * 100) : 0;
|
||
|
||
let html = '';
|
||
html += '<div style="text-align:center;padding:20px;">';
|
||
html += '<div style="font-size:48px;margin-bottom:16px;">' + (percent >= 80 ? '🎉' : percent >= 50 ? '👍' : '💪') + '</div>';
|
||
html += '<div style="font-size:24px;font-weight:600;margin-bottom:8px;">' + (t('qa_session_complete') || 'Lernrunde abgeschlossen!') + '</div>';
|
||
html += '<div style="font-size:18px;margin-bottom:24px;">' + qaSessionStats.correct + ' / ' + qaSessionStats.total + ' ' + (t('qa_result_correct') || 'richtig') + ' (' + percent + '%)</div>';
|
||
|
||
// Statistik
|
||
html += '<div style="display:flex;justify-content:center;gap:24px;margin-bottom:24px;">';
|
||
html += '<div style="text-align:center;"><div style="font-size:32px;color:#22c55e;">' + qaSessionStats.correct + '</div><div style="font-size:11px;color:var(--bp-text-muted);">' + (t('qa_correct') || 'Richtig') + '</div></div>';
|
||
html += '<div style="text-align:center;"><div style="font-size:32px;color:#ef4444;">' + qaSessionStats.incorrect + '</div><div style="font-size:11px;color:var(--bp-text-muted);">' + (t('qa_incorrect') || 'Falsch') + '</div></div>';
|
||
html += '</div>';
|
||
|
||
html += '<div style="display:flex;gap:12px;justify-content:center;">';
|
||
html += '<button class="btn btn-primary" id="btn-qa-restart">' + (t('qa_restart') || 'Nochmal lernen') + '</button>';
|
||
html += '<button class="btn btn-ghost" id="btn-qa-close-summary">' + (t('close') || 'Schließen') + '</button>';
|
||
html += '</div>';
|
||
html += '</div>';
|
||
|
||
qaModalBody.innerHTML = html;
|
||
|
||
document.getElementById('btn-qa-restart').addEventListener('click', () => {
|
||
currentQaIndex = 0;
|
||
qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
|
||
renderQaLearningCard();
|
||
});
|
||
|
||
document.getElementById('btn-qa-close-summary').addEventListener('click', closeQaModal);
|
||
|
||
// Aktualisiere Preview nach Session
|
||
loadQaPreviewForCurrent();
|
||
}
|
||
|
||
function openQaPrintDialog() {
|
||
if (!currentQaData) {
|
||
alert(t('qa_no_questions') || 'Keine Q&A vorhanden.');
|
||
return;
|
||
}
|
||
|
||
const currentFile = eingangFiles[currentIndex];
|
||
const baseName = currentFile.split('.')[0];
|
||
|
||
// Öffne Druck-Optionen
|
||
const choice = confirm((t('qa_print_with_answers') || 'Mit Lösungen drucken?') + '\\n\\nOK = Mit Lösungen\\nAbbrechen = Nur Fragen');
|
||
|
||
const url = '/api/print-qa/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
|
||
window.open(url, '_blank');
|
||
}
|
||
|
||
// Event Listener für Q&A-Buttons
|
||
if (btnQaGenerate) {
|
||
btnQaGenerate.addEventListener('click', generateQaQuestions);
|
||
}
|
||
|
||
if (btnQaLearn) {
|
||
btnQaLearn.addEventListener('click', openQaModal);
|
||
}
|
||
|
||
if (btnQaPrint) {
|
||
btnQaPrint.addEventListener('click', openQaPrintDialog);
|
||
}
|
||
|
||
if (qaModalClose) {
|
||
qaModalClose.addEventListener('click', closeQaModal);
|
||
}
|
||
|
||
if (qaModal) {
|
||
qaModal.addEventListener('click', (ev) => {
|
||
if (ev.target === qaModal) {
|
||
closeQaModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- Sprachauswahl Event Listener ---
|
||
const languageSelect = document.getElementById('language-select');
|
||
if (languageSelect) {
|
||
// Setze initiale Auswahl basierend auf gespeicherter Sprache
|
||
languageSelect.value = currentLang;
|
||
|
||
languageSelect.addEventListener('change', (e) => {
|
||
applyLanguage(e.target.value);
|
||
});
|
||
}
|
||
|
||
// --- Initial ---
|
||
async function init() {
|
||
// Theme Toggle initialisieren
|
||
initThemeToggle();
|
||
|
||
// Sprache anwenden (aus localStorage oder default)
|
||
applyLanguage(currentLang);
|
||
|
||
updateScreen();
|
||
updateTiles();
|
||
await loadEingangFiles();
|
||
await loadWorksheetPairs();
|
||
await loadLearningUnits();
|
||
// Lade MC-Vorschau für aktuelle Datei
|
||
await loadMcPreviewForCurrent();
|
||
// Lade Lückentext-Vorschau
|
||
await loadClozePreviewForCurrent();
|
||
// Lade Q&A-Vorschau
|
||
await loadQaPreviewForCurrent();
|
||
// Lade Mindmap-Vorschau
|
||
await loadMindmapData();
|
||
}
|
||
|
||
init();
|
||
|
||
// === SCRIPT BLOCK SEPARATOR ===
|
||
|
||
// GDPR Actions
|
||
async function saveCookiePreferences() {
|
||
const functional = document.getElementById('cookie-functional')?.checked || false;
|
||
const analytics = document.getElementById('cookie-analytics')?.checked || false;
|
||
|
||
// Save to localStorage for now
|
||
localStorage.setItem('bp_cookies', JSON.stringify({functional, analytics}));
|
||
alert('Cookie-Einstellungen gespeichert!');
|
||
}
|
||
|
||
// ==========================================
|
||
// GDPR FUNCTIONS (Art. 15-21)
|
||
// ==========================================
|
||
|
||
// Art. 15 - Auskunftsrecht
|
||
async function requestDataExport() {
|
||
alert('Ihre Auskunftsanfrage wurde erstellt. Sie erhalten eine E-Mail, sobald Ihre Daten bereit sind (max. 30 Tage).');
|
||
}
|
||
|
||
// Art. 16 - Recht auf Berichtigung
|
||
async function requestDataCorrection() {
|
||
const reason = prompt('Bitte beschreiben Sie, welche Daten korrigiert werden sollen:');
|
||
if (reason) {
|
||
alert('Ihre Berichtigungsanfrage wurde erstellt. Wir werden sie innerhalb von 30 Tagen bearbeiten.');
|
||
}
|
||
}
|
||
|
||
// Art. 17 - Recht auf Löschung
|
||
async function requestDataDeletion() {
|
||
if (confirm('Sind Sie sicher, dass Sie alle Ihre Daten löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.')) {
|
||
alert('Ihre Löschanfrage wurde erstellt. Wir werden sie innerhalb von 30 Tagen bearbeiten.');
|
||
}
|
||
}
|
||
|
||
// Art. 18 - Einschränkung der Verarbeitung
|
||
async function requestProcessingRestriction() {
|
||
const reason = prompt('Bitte geben Sie den Grund für die Einschränkung an (z.B. Richtigkeit bestritten, unrechtmäßige Verarbeitung):');
|
||
if (reason) {
|
||
alert('Ihre Anfrage auf Einschränkung der Verarbeitung wurde erstellt. Wir werden sie innerhalb von 30 Tagen bearbeiten.');
|
||
}
|
||
}
|
||
|
||
// Art. 20 - Datenübertragbarkeit
|
||
async function requestDataDownload() {
|
||
alert('Ihr Datenexport wurde gestartet. Sie erhalten eine E-Mail mit dem Download-Link (JSON-Format).');
|
||
}
|
||
|
||
// Art. 21 - Widerspruchsrecht
|
||
async function requestProcessingObjection() {
|
||
const reason = prompt('Bitte geben Sie den Grund für Ihren Widerspruch an:');
|
||
if (reason) {
|
||
alert('Ihr Widerspruch wurde erstellt. Wir werden ihn innerhalb von 30 Tagen prüfen.');
|
||
}
|
||
}
|
||
|
||
// Consent Manager öffnen
|
||
function showConsentManager() {
|
||
openLegalModal('cookies');
|
||
}
|
||
|
||
// Settings Modal öffnen (leitet zum GDPR-Tab)
|
||
function openSettingsModal() {
|
||
openLegalModal('gdpr');
|
||
}
|
||
|
||
// Load saved cookie preferences
|
||
const savedCookies = localStorage.getItem('bp_cookies');
|
||
if (savedCookies) {
|
||
const prefs = JSON.parse(savedCookies);
|
||
if (document.getElementById('cookie-functional')) {
|
||
document.getElementById('cookie-functional').checked = prefs.functional;
|
||
}
|
||
if (document.getElementById('cookie-analytics')) {
|
||
document.getElementById('cookie-analytics').checked = prefs.analytics;
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// LEGAL MODAL (now after modal HTML exists)
|
||
// ==========================================
|
||
const legalModal = document.getElementById('legal-modal');
|
||
const legalModalClose = document.getElementById('legal-modal-close');
|
||
const legalTabs = document.querySelectorAll('.legal-tab');
|
||
const legalContents = document.querySelectorAll('.legal-content');
|
||
const btnLegal = document.getElementById('btn-legal');
|
||
|
||
// Imprint Modal
|
||
const imprintModal = document.getElementById('imprint-modal');
|
||
const imprintModalClose = document.getElementById('imprint-modal-close');
|
||
|
||
// Open legal modal from footer
|
||
function openLegalModal(tab = 'terms') {
|
||
legalModal.classList.add('active');
|
||
// Switch to specified tab
|
||
if (tab) {
|
||
legalTabs.forEach(t => t.classList.remove('active'));
|
||
legalContents.forEach(c => c.classList.remove('active'));
|
||
const targetTab = document.querySelector(`.legal-tab[data-tab="${tab}"]`);
|
||
if (targetTab) targetTab.classList.add('active');
|
||
document.getElementById(`legal-${tab}`)?.classList.add('active');
|
||
}
|
||
loadLegalDocuments();
|
||
}
|
||
|
||
// Open imprint modal from footer
|
||
function openImprintModal() {
|
||
imprintModal.classList.add('active');
|
||
loadImprintContent();
|
||
}
|
||
|
||
// Open legal modal
|
||
btnLegal?.addEventListener('click', async () => {
|
||
openLegalModal();
|
||
});
|
||
|
||
// Close legal modal
|
||
legalModalClose?.addEventListener('click', () => {
|
||
legalModal.classList.remove('active');
|
||
});
|
||
|
||
// Close imprint modal
|
||
imprintModalClose?.addEventListener('click', () => {
|
||
imprintModal.classList.remove('active');
|
||
});
|
||
|
||
// Close on background click
|
||
legalModal?.addEventListener('click', (e) => {
|
||
if (e.target === legalModal) {
|
||
legalModal.classList.remove('active');
|
||
}
|
||
});
|
||
|
||
imprintModal?.addEventListener('click', (e) => {
|
||
if (e.target === imprintModal) {
|
||
imprintModal.classList.remove('active');
|
||
}
|
||
});
|
||
|
||
// Tab switching
|
||
legalTabs.forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
const tabId = tab.dataset.tab;
|
||
legalTabs.forEach(t => t.classList.remove('active'));
|
||
legalContents.forEach(c => c.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
document.getElementById(`legal-${tabId}`)?.classList.add('active');
|
||
|
||
// Load cookie categories when switching to cookies tab
|
||
if (tabId === 'cookies') {
|
||
loadCookieCategories();
|
||
}
|
||
});
|
||
});
|
||
|
||
// Load legal documents from consent service
|
||
async function loadLegalDocuments() {
|
||
const lang = document.getElementById('language-select')?.value || 'de';
|
||
|
||
// Load all documents in parallel
|
||
await Promise.all([
|
||
loadDocumentContent('terms', 'legal-terms-content', getDefaultTerms, lang),
|
||
loadDocumentContent('privacy', 'legal-privacy-content', getDefaultPrivacy, lang),
|
||
loadDocumentContent('community_guidelines', 'legal-community-content', getDefaultCommunityGuidelines, lang)
|
||
]);
|
||
}
|
||
|
||
// Load imprint content
|
||
async function loadImprintContent() {
|
||
const lang = document.getElementById('language-select')?.value || 'de';
|
||
await loadDocumentContent('imprint', 'imprint-content', getDefaultImprint, lang);
|
||
}
|
||
|
||
// Generic function to load document content
|
||
async function loadDocumentContent(docType, containerId, defaultFn, lang) {
|
||
const container = document.getElementById(containerId);
|
||
if (!container) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/documents/${docType}/latest?language=${lang}`);
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
if (data.content) {
|
||
container.innerHTML = data.content;
|
||
return;
|
||
}
|
||
}
|
||
} catch(e) {
|
||
console.log(`Could not load ${docType}:`, e);
|
||
}
|
||
|
||
// Fallback to default
|
||
container.innerHTML = defaultFn(lang);
|
||
}
|
||
|
||
// Load cookie categories for the cookie settings tab
|
||
async function loadCookieCategories() {
|
||
const container = document.getElementById('cookie-categories-container');
|
||
if (!container) return;
|
||
|
||
try {
|
||
const res = await fetch('/api/consent/cookies/categories');
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
const categories = data.categories || [];
|
||
|
||
if (categories.length === 0) {
|
||
container.innerHTML = getDefaultCookieCategories();
|
||
return;
|
||
}
|
||
|
||
// Get current preferences from localStorage
|
||
const savedPrefs = JSON.parse(localStorage.getItem('bp_cookie_consent') || '{}');
|
||
|
||
container.innerHTML = categories.map(cat => `
|
||
<label class="cookie-category">
|
||
<input type="checkbox" id="cookie-${cat.name}"
|
||
${cat.is_mandatory ? 'checked disabled' : (savedPrefs[cat.name] ? 'checked' : '')}>
|
||
<span>
|
||
<strong>${cat.display_name_de || cat.name}</strong>
|
||
${cat.is_mandatory ? ' (erforderlich)' : ''}
|
||
${cat.description_de ? ` - ${cat.description_de}` : ''}
|
||
</span>
|
||
</label>
|
||
`).join('');
|
||
} else {
|
||
container.innerHTML = getDefaultCookieCategories();
|
||
}
|
||
} catch(e) {
|
||
container.innerHTML = getDefaultCookieCategories();
|
||
}
|
||
}
|
||
|
||
function getDefaultCookieCategories() {
|
||
return `
|
||
<label class="cookie-category">
|
||
<input type="checkbox" checked disabled>
|
||
<span><strong>Notwendig</strong> (erforderlich) - Erforderlich für die Grundfunktionen</span>
|
||
</label>
|
||
<label class="cookie-category">
|
||
<input type="checkbox" id="cookie-functional">
|
||
<span><strong>Funktional</strong> - Erweiterte Funktionen und Personalisierung</span>
|
||
</label>
|
||
<label class="cookie-category">
|
||
<input type="checkbox" id="cookie-analytics">
|
||
<span><strong>Analyse</strong> - Hilft uns, die Nutzung zu verstehen</span>
|
||
</label>
|
||
`;
|
||
}
|
||
|
||
function getDefaultTerms(lang) {
|
||
const terms = {
|
||
de: '<h3>Allgemeine Geschäftsbedingungen</h3><p>Die BreakPilot-Plattform wird von der BreakPilot UG bereitgestellt.</p><p><strong>Nutzung:</strong> Die Plattform dient zur Erstellung und Verwaltung von Lernmaterialien für Bildungszwecke.</p><p><strong>Haftung:</strong> Die Nutzung erfolgt auf eigene Verantwortung.</p><p><strong>Änderungen:</strong> Wir behalten uns vor, diese AGB jederzeit zu ändern.</p>',
|
||
en: '<h3>Terms of Service</h3><p>The BreakPilot platform is provided by BreakPilot UG.</p><p><strong>Usage:</strong> The platform is designed for creating and managing learning materials for educational purposes.</p><p><strong>Liability:</strong> Use at your own risk.</p><p><strong>Changes:</strong> We reserve the right to modify these terms at any time.</p>'
|
||
};
|
||
return terms[lang] || terms.de;
|
||
}
|
||
|
||
function getDefaultPrivacy(lang) {
|
||
const privacy = {
|
||
de: '<h3>Datenschutzerklärung</h3><p><strong>Verantwortlicher:</strong> BreakPilot UG</p><p><strong>Erhobene Daten:</strong> Bei der Nutzung werden technische Daten (IP-Adresse, Browser-Typ) sowie von Ihnen eingegebene Inhalte verarbeitet.</p><p><strong>Zweck:</strong> Die Daten werden zur Bereitstellung der Plattform und zur Verbesserung unserer Dienste genutzt.</p><p><strong>Ihre Rechte (DSGVO):</strong></p><ul><li>Auskunftsrecht (Art. 15)</li><li>Recht auf Berichtigung (Art. 16)</li><li>Recht auf Löschung (Art. 17)</li><li>Recht auf Datenübertragbarkeit (Art. 20)</li></ul><p><strong>Kontakt:</strong> datenschutz@breakpilot.app</p>',
|
||
en: '<h3>Privacy Policy</h3><p><strong>Controller:</strong> BreakPilot UG</p><p><strong>Data Collected:</strong> Technical data (IP address, browser type) and content you provide are processed.</p><p><strong>Purpose:</strong> Data is used to provide the platform and improve our services.</p><p><strong>Your Rights (GDPR):</strong></p><ul><li>Right of access (Art. 15)</li><li>Right to rectification (Art. 16)</li><li>Right to erasure (Art. 17)</li><li>Right to data portability (Art. 20)</li></ul><p><strong>Contact:</strong> privacy@breakpilot.app</p>'
|
||
};
|
||
return privacy[lang] || privacy.de;
|
||
}
|
||
|
||
function getDefaultCommunityGuidelines(lang) {
|
||
const guidelines = {
|
||
de: '<h3>Community Guidelines</h3><p>Willkommen bei BreakPilot! Um eine positive und respektvolle Umgebung zu gewährleisten, bitten wir alle Nutzer, diese Richtlinien zu befolgen.</p><p><strong>Respektvoller Umgang:</strong> Behandeln Sie andere Nutzer mit Respekt und Höflichkeit.</p><p><strong>Keine illegalen Inhalte:</strong> Das Erstellen oder Teilen von illegalen Inhalten ist streng untersagt.</p><p><strong>Urheberrecht:</strong> Respektieren Sie das geistige Eigentum anderer. Verwenden Sie nur Inhalte, für die Sie die Rechte besitzen.</p><p><strong>Datenschutz:</strong> Teilen Sie keine persönlichen Daten anderer ohne deren ausdrückliche Zustimmung.</p><p><strong>Qualität:</strong> Bemühen Sie sich um qualitativ hochwertige Lerninhalte.</p><p>Verstöße gegen diese Richtlinien können zur Sperrung des Accounts führen.</p>',
|
||
en: '<h3>Community Guidelines</h3><p>Welcome to BreakPilot! To ensure a positive and respectful environment, we ask all users to follow these guidelines.</p><p><strong>Respectful Behavior:</strong> Treat other users with respect and courtesy.</p><p><strong>No Illegal Content:</strong> Creating or sharing illegal content is strictly prohibited.</p><p><strong>Copyright:</strong> Respect the intellectual property of others. Only use content you have rights to.</p><p><strong>Privacy:</strong> Do not share personal data of others without their explicit consent.</p><p><strong>Quality:</strong> Strive for high-quality learning content.</p><p>Violations of these guidelines may result in account suspension.</p>'
|
||
};
|
||
return guidelines[lang] || guidelines.de;
|
||
}
|
||
|
||
function getDefaultImprint(lang) {
|
||
const imprint = {
|
||
de: '<h3>Impressum</h3><p><strong>Angaben gemäß § 5 TMG:</strong></p><p>BreakPilot UG (haftungsbeschränkt)<br>Musterstraße 1<br>12345 Musterstadt<br>Deutschland</p><p><strong>Vertreten durch:</strong><br>Geschäftsführer: Max Mustermann</p><p><strong>Kontakt:</strong><br>Telefon: +49 (0) 123 456789<br>E-Mail: info@breakpilot.app</p><p><strong>Registereintrag:</strong><br>Eintragung im Handelsregister<br>Registergericht: Amtsgericht Musterstadt<br>Registernummer: HRB 12345</p><p><strong>Umsatzsteuer-ID:</strong><br>Umsatzsteuer-Identifikationsnummer gemäß § 27 a UStG: DE123456789</p><p><strong>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV:</strong><br>Max Mustermann<br>Musterstraße 1<br>12345 Musterstadt</p>',
|
||
en: '<h3>Legal Notice</h3><p><strong>Information according to § 5 TMG:</strong></p><p>BreakPilot UG (limited liability)<br>Musterstraße 1<br>12345 Musterstadt<br>Germany</p><p><strong>Represented by:</strong><br>Managing Director: Max Mustermann</p><p><strong>Contact:</strong><br>Phone: +49 (0) 123 456789<br>Email: info@breakpilot.app</p><p><strong>Register entry:</strong><br>Entry in the commercial register<br>Register court: Amtsgericht Musterstadt<br>Register number: HRB 12345</p><p><strong>VAT ID:</strong><br>VAT identification number according to § 27 a UStG: DE123456789</p><p><strong>Responsible for content according to § 55 Abs. 2 RStV:</strong><br>Max Mustermann<br>Musterstraße 1<br>12345 Musterstadt</p>'
|
||
};
|
||
return imprint[lang] || imprint.de;
|
||
}
|
||
|
||
// Save cookie preferences
|
||
function saveCookiePreferences() {
|
||
const prefs = {};
|
||
const checkboxes = document.querySelectorAll('#cookie-categories-container input[type="checkbox"]');
|
||
checkboxes.forEach(cb => {
|
||
const name = cb.id.replace('cookie-', '');
|
||
if (name && !cb.disabled) {
|
||
prefs[name] = cb.checked;
|
||
}
|
||
});
|
||
localStorage.setItem('bp_cookie_consent', JSON.stringify(prefs));
|
||
localStorage.setItem('bp_cookie_consent_date', new Date().toISOString());
|
||
|
||
// TODO: Send to consent service if user is logged in
|
||
alert('Cookie-Einstellungen gespeichert!');
|
||
}
|
||
|
||
// ==========================================
|
||
// AUTH MODAL
|
||
// ==========================================
|
||
const authModal = document.getElementById('auth-modal');
|
||
const authModalClose = document.getElementById('auth-modal-close');
|
||
const authTabs = document.querySelectorAll('.auth-tab');
|
||
const authContents = document.querySelectorAll('.auth-content');
|
||
const btnLogin = document.getElementById('btn-login');
|
||
|
||
// Auth state
|
||
let currentUser = null;
|
||
let accessToken = localStorage.getItem('bp_access_token');
|
||
let refreshToken = localStorage.getItem('bp_refresh_token');
|
||
|
||
// Update UI based on auth state
|
||
function updateAuthUI() {
|
||
const loginBtn = document.getElementById('btn-login');
|
||
const userDropdown = document.querySelector('.auth-user-dropdown');
|
||
const notificationBell = document.getElementById('notification-bell');
|
||
|
||
if (currentUser && accessToken) {
|
||
// User is logged in - hide login button
|
||
if (loginBtn) loginBtn.style.display = 'none';
|
||
|
||
// Show notification bell
|
||
if (notificationBell) {
|
||
notificationBell.classList.add('active');
|
||
loadNotifications(); // Load notifications on login
|
||
startNotificationPolling(); // Start polling for new notifications
|
||
checkSuspensionStatus(); // Check if account is suspended
|
||
}
|
||
|
||
// Show user dropdown if it exists
|
||
if (userDropdown) {
|
||
userDropdown.classList.add('active');
|
||
const avatar = userDropdown.querySelector('.auth-user-avatar');
|
||
const menuName = userDropdown.querySelector('.auth-user-menu-name');
|
||
const menuEmail = userDropdown.querySelector('.auth-user-menu-email');
|
||
|
||
if (avatar) {
|
||
const initials = currentUser.name
|
||
? currentUser.name.substring(0, 2).toUpperCase()
|
||
: currentUser.email.substring(0, 2).toUpperCase();
|
||
avatar.textContent = initials;
|
||
}
|
||
if (menuName) menuName.textContent = currentUser.name || 'Benutzer';
|
||
if (menuEmail) menuEmail.textContent = currentUser.email;
|
||
}
|
||
} else {
|
||
// User is logged out - show login button
|
||
if (loginBtn) loginBtn.style.display = 'block';
|
||
if (userDropdown) userDropdown.classList.remove('active');
|
||
if (notificationBell) notificationBell.classList.remove('active');
|
||
stopNotificationPolling();
|
||
}
|
||
}
|
||
|
||
// Check if user is already logged in
|
||
async function checkAuthStatus() {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/profile', {
|
||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||
});
|
||
|
||
if (response.ok) {
|
||
currentUser = await response.json();
|
||
updateAuthUI();
|
||
} else if (response.status === 401 && refreshToken) {
|
||
// Try to refresh the token
|
||
await refreshAccessToken();
|
||
} else {
|
||
// Clear invalid tokens
|
||
logout(false);
|
||
}
|
||
} catch (e) {
|
||
console.error('Auth check failed:', e);
|
||
}
|
||
}
|
||
|
||
// Refresh access token
|
||
async function refreshAccessToken() {
|
||
if (!refreshToken) return false;
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/refresh', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ refresh_token: refreshToken })
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
accessToken = data.access_token;
|
||
refreshToken = data.refresh_token;
|
||
currentUser = data.user;
|
||
|
||
localStorage.setItem('bp_access_token', accessToken);
|
||
localStorage.setItem('bp_refresh_token', refreshToken);
|
||
updateAuthUI();
|
||
return true;
|
||
} else {
|
||
logout(false);
|
||
return false;
|
||
}
|
||
} catch (e) {
|
||
console.error('Token refresh failed:', e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Logout
|
||
function logout(showMessage = true) {
|
||
if (refreshToken) {
|
||
fetch('/api/auth/logout', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ refresh_token: refreshToken })
|
||
}).catch(() => {});
|
||
}
|
||
|
||
currentUser = null;
|
||
accessToken = null;
|
||
refreshToken = null;
|
||
localStorage.removeItem('bp_access_token');
|
||
localStorage.removeItem('bp_refresh_token');
|
||
updateAuthUI();
|
||
|
||
if (showMessage) {
|
||
alert('Sie wurden erfolgreich abgemeldet.');
|
||
}
|
||
}
|
||
|
||
// Open auth modal
|
||
btnLogin?.addEventListener('click', () => {
|
||
authModal.classList.add('active');
|
||
showAuthTab('login');
|
||
clearAuthErrors();
|
||
});
|
||
|
||
// Close auth modal
|
||
authModalClose?.addEventListener('click', () => {
|
||
authModal.classList.remove('active');
|
||
clearAuthErrors();
|
||
});
|
||
|
||
// Close on background click
|
||
authModal?.addEventListener('click', (e) => {
|
||
if (e.target === authModal) {
|
||
authModal.classList.remove('active');
|
||
clearAuthErrors();
|
||
}
|
||
});
|
||
|
||
// Tab switching
|
||
authTabs.forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
const tabId = tab.dataset.tab;
|
||
showAuthTab(tabId);
|
||
});
|
||
});
|
||
|
||
function showAuthTab(tabId) {
|
||
authTabs.forEach(t => t.classList.remove('active'));
|
||
authContents.forEach(c => c.classList.remove('active'));
|
||
|
||
const activeTab = document.querySelector(`.auth-tab[data-tab="${tabId}"]`);
|
||
if (activeTab) activeTab.classList.add('active');
|
||
|
||
document.getElementById(`auth-${tabId}`)?.classList.add('active');
|
||
clearAuthErrors();
|
||
}
|
||
|
||
function clearAuthErrors() {
|
||
document.querySelectorAll('.auth-error, .auth-success').forEach(el => {
|
||
el.classList.remove('active');
|
||
el.textContent = '';
|
||
});
|
||
}
|
||
|
||
function showAuthError(elementId, message) {
|
||
const el = document.getElementById(elementId);
|
||
if (el) {
|
||
el.textContent = message;
|
||
el.classList.add('active');
|
||
}
|
||
}
|
||
|
||
function showAuthSuccess(elementId, message) {
|
||
const el = document.getElementById(elementId);
|
||
if (el) {
|
||
el.textContent = message;
|
||
el.classList.add('active');
|
||
}
|
||
}
|
||
|
||
// Login form
|
||
document.getElementById('auth-login-form')?.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
clearAuthErrors();
|
||
|
||
const email = document.getElementById('login-email').value;
|
||
const password = document.getElementById('login-password').value;
|
||
const btn = document.getElementById('login-btn');
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = 'Anmelden...';
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email, password })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
accessToken = data.access_token;
|
||
refreshToken = data.refresh_token;
|
||
currentUser = data.user;
|
||
|
||
localStorage.setItem('bp_access_token', accessToken);
|
||
localStorage.setItem('bp_refresh_token', refreshToken);
|
||
|
||
updateAuthUI();
|
||
authModal.classList.remove('active');
|
||
|
||
// Clear form
|
||
document.getElementById('login-email').value = '';
|
||
document.getElementById('login-password').value = '';
|
||
} else {
|
||
showAuthError('auth-login-error', data.detail || data.error || 'Anmeldung fehlgeschlagen');
|
||
}
|
||
} catch (e) {
|
||
showAuthError('auth-login-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = 'Anmelden';
|
||
});
|
||
|
||
// Register form
|
||
document.getElementById('auth-register-form')?.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
clearAuthErrors();
|
||
|
||
const name = document.getElementById('register-name').value;
|
||
const email = document.getElementById('register-email').value;
|
||
const password = document.getElementById('register-password').value;
|
||
const passwordConfirm = document.getElementById('register-password-confirm').value;
|
||
|
||
if (password !== passwordConfirm) {
|
||
showAuthError('auth-register-error', 'Passwörter stimmen nicht überein');
|
||
return;
|
||
}
|
||
|
||
if (password.length < 8) {
|
||
showAuthError('auth-register-error', 'Passwort muss mindestens 8 Zeichen lang sein');
|
||
return;
|
||
}
|
||
|
||
const btn = document.getElementById('register-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Registrieren...';
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/register', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email, password, name: name || undefined })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
showAuthSuccess('auth-register-success',
|
||
'Registrierung erfolgreich! Bitte überprüfen Sie Ihre E-Mails zur Bestätigung.');
|
||
|
||
// Clear form
|
||
document.getElementById('register-name').value = '';
|
||
document.getElementById('register-email').value = '';
|
||
document.getElementById('register-password').value = '';
|
||
document.getElementById('register-password-confirm').value = '';
|
||
} else {
|
||
showAuthError('auth-register-error', data.detail || data.error || 'Registrierung fehlgeschlagen');
|
||
}
|
||
} catch (e) {
|
||
showAuthError('auth-register-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = 'Registrieren';
|
||
});
|
||
|
||
// Forgot password form
|
||
document.getElementById('auth-forgot-form')?.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
clearAuthErrors();
|
||
|
||
const email = document.getElementById('forgot-email').value;
|
||
const btn = document.getElementById('forgot-btn');
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = 'Senden...';
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/forgot-password', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email })
|
||
});
|
||
|
||
// Always show success to prevent email enumeration
|
||
showAuthSuccess('auth-forgot-success',
|
||
'Falls ein Konto mit dieser E-Mail existiert, wurde ein Link zum Zurücksetzen gesendet.');
|
||
|
||
document.getElementById('forgot-email').value = '';
|
||
} catch (e) {
|
||
showAuthError('auth-forgot-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = 'Link senden';
|
||
});
|
||
|
||
// Reset password form
|
||
document.getElementById('auth-reset-form')?.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
clearAuthErrors();
|
||
|
||
const password = document.getElementById('reset-password').value;
|
||
const passwordConfirm = document.getElementById('reset-password-confirm').value;
|
||
const token = document.getElementById('reset-token').value;
|
||
|
||
if (password !== passwordConfirm) {
|
||
showAuthError('auth-reset-error', 'Passwörter stimmen nicht überein');
|
||
return;
|
||
}
|
||
|
||
if (password.length < 8) {
|
||
showAuthError('auth-reset-error', 'Passwort muss mindestens 8 Zeichen lang sein');
|
||
return;
|
||
}
|
||
|
||
const btn = document.getElementById('reset-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Ändern...';
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/reset-password', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ token, new_password: password })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
showAuthSuccess('auth-reset-success',
|
||
'Passwort erfolgreich geändert! Sie können sich jetzt anmelden.');
|
||
|
||
// Clear URL params
|
||
window.history.replaceState({}, document.title, window.location.pathname);
|
||
|
||
// Switch to login after 2 seconds
|
||
setTimeout(() => showAuthTab('login'), 2000);
|
||
} else {
|
||
showAuthError('auth-reset-error', data.detail || data.error || 'Passwort zurücksetzen fehlgeschlagen');
|
||
}
|
||
} catch (e) {
|
||
showAuthError('auth-reset-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = 'Passwort ändern';
|
||
});
|
||
|
||
// Navigation links
|
||
document.getElementById('auth-forgot-password')?.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
showAuthTab('forgot');
|
||
// Hide tabs for forgot password
|
||
document.querySelector('.auth-tabs').style.display = 'none';
|
||
});
|
||
|
||
document.getElementById('auth-back-to-login')?.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
showAuthTab('login');
|
||
document.querySelector('.auth-tabs').style.display = 'flex';
|
||
});
|
||
|
||
document.getElementById('auth-goto-login')?.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
showAuthTab('login');
|
||
});
|
||
|
||
// Check for URL parameters (email verification, password reset)
|
||
function checkAuthUrlParams() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const verifyToken = urlParams.get('verify');
|
||
const resetToken = urlParams.get('reset');
|
||
|
||
if (verifyToken) {
|
||
authModal.classList.add('active');
|
||
document.querySelector('.auth-tabs').style.display = 'none';
|
||
showAuthTab('verify');
|
||
verifyEmail(verifyToken);
|
||
} else if (resetToken) {
|
||
authModal.classList.add('active');
|
||
document.querySelector('.auth-tabs').style.display = 'none';
|
||
showAuthTab('reset');
|
||
document.getElementById('reset-token').value = resetToken;
|
||
}
|
||
}
|
||
|
||
async function verifyEmail(token) {
|
||
try {
|
||
const response = await fetch('/api/auth/verify-email', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ token })
|
||
});
|
||
|
||
const data = await response.json();
|
||
const loadingEl = document.getElementById('auth-verify-loading');
|
||
|
||
if (response.ok) {
|
||
if (loadingEl) loadingEl.style.display = 'none';
|
||
showAuthSuccess('auth-verify-success', 'E-Mail erfolgreich verifiziert! Sie können sich jetzt anmelden.');
|
||
|
||
// Clear URL params
|
||
window.history.replaceState({}, document.title, window.location.pathname);
|
||
|
||
// Switch to login after 2 seconds
|
||
setTimeout(() => {
|
||
showAuthTab('login');
|
||
document.querySelector('.auth-tabs').style.display = 'flex';
|
||
}, 2000);
|
||
} else {
|
||
if (loadingEl) loadingEl.style.display = 'none';
|
||
showAuthError('auth-verify-error', data.detail || data.error || 'Verifizierung fehlgeschlagen. Der Link ist möglicherweise abgelaufen.');
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('auth-verify-loading').style.display = 'none';
|
||
showAuthError('auth-verify-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
||
}
|
||
}
|
||
|
||
// User dropdown toggle
|
||
const authUserBtn = document.getElementById('auth-user-btn');
|
||
const authUserMenu = document.getElementById('auth-user-menu');
|
||
|
||
authUserBtn?.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
authUserMenu.classList.toggle('active');
|
||
});
|
||
|
||
// Close dropdown when clicking outside
|
||
document.addEventListener('click', () => {
|
||
authUserMenu?.classList.remove('active');
|
||
});
|
||
|
||
// Placeholder functions for profile/sessions
|
||
function showProfileModal() {
|
||
alert('Profil-Einstellungen kommen bald!');
|
||
}
|
||
|
||
function showSessionsModal() {
|
||
alert('Sitzungsverwaltung kommt bald!');
|
||
}
|
||
|
||
// ==========================================
|
||
// NOTIFICATION FUNCTIONS
|
||
// ==========================================
|
||
let notificationPollingInterval = null;
|
||
let notificationOffset = 0;
|
||
let notificationPrefs = {
|
||
email_enabled: true,
|
||
push_enabled: false,
|
||
in_app_enabled: true
|
||
};
|
||
|
||
// Toggle notification panel
|
||
document.getElementById('notification-bell-btn')?.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const panel = document.getElementById('notification-panel');
|
||
panel.classList.toggle('active');
|
||
|
||
// Close user menu if open
|
||
const userMenu = document.getElementById('auth-user-menu');
|
||
userMenu?.classList.remove('active');
|
||
});
|
||
|
||
// Close notification panel when clicking outside
|
||
document.addEventListener('click', (e) => {
|
||
const bell = document.getElementById('notification-bell');
|
||
const panel = document.getElementById('notification-panel');
|
||
if (bell && panel && !bell.contains(e.target)) {
|
||
panel.classList.remove('active');
|
||
}
|
||
});
|
||
|
||
// Load notifications from API
|
||
async function loadNotifications(append = false) {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
const limit = 10;
|
||
const offset = append ? notificationOffset : 0;
|
||
|
||
const response = await fetch(`/api/v1/notifications?limit=${limit}&offset=${offset}`, {
|
||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||
});
|
||
|
||
if (!response.ok) return;
|
||
|
||
const data = await response.json();
|
||
|
||
if (!append) {
|
||
notificationOffset = 0;
|
||
}
|
||
notificationOffset += data.notifications?.length || 0;
|
||
|
||
renderNotifications(data.notifications || [], data.total || 0, append);
|
||
updateNotificationBadge();
|
||
} catch (e) {
|
||
console.error('Failed to load notifications:', e);
|
||
}
|
||
}
|
||
|
||
// Render notifications in the panel
|
||
function renderNotifications(notifications, total, append = false) {
|
||
const list = document.getElementById('notification-list');
|
||
if (!list) return;
|
||
|
||
if (!append) {
|
||
list.innerHTML = '';
|
||
}
|
||
|
||
if (notifications.length === 0 && !append) {
|
||
list.innerHTML = `
|
||
<div class="notification-empty">
|
||
<div class="notification-empty-icon">🔔</div>
|
||
<div>Keine Benachrichtigungen</div>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
notifications.forEach(n => {
|
||
const item = document.createElement('div');
|
||
item.className = `notification-item ${!n.read_at ? 'unread' : ''}`;
|
||
item.onclick = () => markNotificationRead(n.id);
|
||
|
||
const icon = getNotificationIcon(n.type);
|
||
const timeAgo = formatTimeAgo(new Date(n.created_at));
|
||
|
||
item.innerHTML = `
|
||
<div class="notification-icon">${icon}</div>
|
||
<div class="notification-content">
|
||
<div class="notification-title">${escapeHtml(n.title)}</div>
|
||
<div class="notification-body">${escapeHtml(n.body)}</div>
|
||
<div class="notification-time">${timeAgo}</div>
|
||
</div>
|
||
`;
|
||
list.appendChild(item);
|
||
});
|
||
|
||
// Show/hide load more button
|
||
const footer = document.querySelector('.notification-footer');
|
||
if (footer) {
|
||
footer.style.display = notificationOffset < total ? 'block' : 'none';
|
||
}
|
||
}
|
||
|
||
// Get icon for notification type
|
||
function getNotificationIcon(type) {
|
||
const icons = {
|
||
'consent_required': '📋',
|
||
'consent_reminder': '⏰',
|
||
'version_published': '📢',
|
||
'version_approved': '✅',
|
||
'version_rejected': '❌',
|
||
'account_suspended': '🚫',
|
||
'account_restored': '🔓',
|
||
'general': '🔔'
|
||
};
|
||
return icons[type] || '🔔';
|
||
}
|
||
|
||
// Format time ago
|
||
function formatTimeAgo(date) {
|
||
const now = new Date();
|
||
const diff = Math.floor((now - date) / 1000);
|
||
|
||
if (diff < 60) return 'Gerade eben';
|
||
if (diff < 3600) return `vor ${Math.floor(diff / 60)} Min.`;
|
||
if (diff < 86400) return `vor ${Math.floor(diff / 3600)} Std.`;
|
||
if (diff < 604800) return `vor ${Math.floor(diff / 86400)} Tagen`;
|
||
return date.toLocaleDateString('de-DE');
|
||
}
|
||
|
||
// Escape HTML to prevent XSS
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// Update notification badge
|
||
async function updateNotificationBadge() {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/notifications/unread-count', {
|
||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||
});
|
||
|
||
if (!response.ok) return;
|
||
|
||
const data = await response.json();
|
||
const badge = document.getElementById('notification-badge');
|
||
|
||
if (badge) {
|
||
const count = data.unread_count || 0;
|
||
badge.textContent = count > 99 ? '99+' : count;
|
||
badge.classList.toggle('hidden', count === 0);
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to update badge:', e);
|
||
}
|
||
}
|
||
|
||
// Mark notification as read
|
||
async function markNotificationRead(id) {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
await fetch(`/api/v1/notifications/${id}/read`, {
|
||
method: 'PUT',
|
||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||
});
|
||
|
||
// Update UI
|
||
const item = document.querySelector(`.notification-item[onclick*="${id}"]`);
|
||
if (item) item.classList.remove('unread');
|
||
updateNotificationBadge();
|
||
} catch (e) {
|
||
console.error('Failed to mark notification as read:', e);
|
||
}
|
||
}
|
||
|
||
// Mark all notifications as read
|
||
async function markAllNotificationsRead() {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
await fetch('/api/v1/notifications/read-all', {
|
||
method: 'PUT',
|
||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||
});
|
||
|
||
// Update UI
|
||
document.querySelectorAll('.notification-item.unread').forEach(item => {
|
||
item.classList.remove('unread');
|
||
});
|
||
updateNotificationBadge();
|
||
} catch (e) {
|
||
console.error('Failed to mark all as read:', e);
|
||
}
|
||
}
|
||
|
||
// Load more notifications
|
||
function loadMoreNotifications() {
|
||
loadNotifications(true);
|
||
}
|
||
|
||
// Start polling for new notifications
|
||
function startNotificationPolling() {
|
||
stopNotificationPolling();
|
||
notificationPollingInterval = setInterval(() => {
|
||
updateNotificationBadge();
|
||
}, 30000); // Poll every 30 seconds
|
||
}
|
||
|
||
// Stop polling
|
||
function stopNotificationPolling() {
|
||
if (notificationPollingInterval) {
|
||
clearInterval(notificationPollingInterval);
|
||
notificationPollingInterval = null;
|
||
}
|
||
}
|
||
|
||
// Show notification preferences modal
|
||
function showNotificationPreferences() {
|
||
document.getElementById('notification-panel')?.classList.remove('active');
|
||
document.getElementById('notification-prefs-modal')?.classList.add('active');
|
||
loadNotificationPreferences();
|
||
}
|
||
|
||
// Close notification preferences modal
|
||
function closeNotificationPreferences() {
|
||
document.getElementById('notification-prefs-modal')?.classList.remove('active');
|
||
}
|
||
|
||
// Load notification preferences
|
||
async function loadNotificationPreferences() {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/notifications/preferences', {
|
||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||
});
|
||
|
||
if (!response.ok) return;
|
||
|
||
const prefs = await response.json();
|
||
notificationPrefs = prefs;
|
||
|
||
// Update UI toggles
|
||
updateToggle('pref-email-toggle', prefs.email_enabled);
|
||
updateToggle('pref-inapp-toggle', prefs.in_app_enabled);
|
||
updateToggle('pref-push-toggle', prefs.push_enabled);
|
||
} catch (e) {
|
||
console.error('Failed to load preferences:', e);
|
||
}
|
||
}
|
||
|
||
// Update toggle UI
|
||
function updateToggle(id, active) {
|
||
const toggle = document.getElementById(id);
|
||
if (toggle) {
|
||
toggle.classList.toggle('active', active);
|
||
}
|
||
}
|
||
|
||
// Toggle notification preference
|
||
function toggleNotificationPref(type) {
|
||
const toggleMap = {
|
||
'email': 'pref-email-toggle',
|
||
'inapp': 'pref-inapp-toggle',
|
||
'push': 'pref-push-toggle'
|
||
};
|
||
const prefMap = {
|
||
'email': 'email_enabled',
|
||
'inapp': 'in_app_enabled',
|
||
'push': 'push_enabled'
|
||
};
|
||
|
||
const toggleId = toggleMap[type];
|
||
const prefKey = prefMap[type];
|
||
const toggle = document.getElementById(toggleId);
|
||
|
||
if (toggle) {
|
||
const isActive = toggle.classList.toggle('active');
|
||
notificationPrefs[prefKey] = isActive;
|
||
}
|
||
}
|
||
|
||
// Save notification preferences
|
||
async function saveNotificationPreferences() {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/notifications/preferences', {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `Bearer ${accessToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(notificationPrefs)
|
||
});
|
||
|
||
if (response.ok) {
|
||
closeNotificationPreferences();
|
||
alert('Einstellungen gespeichert!');
|
||
} else {
|
||
alert('Fehler beim Speichern der Einstellungen');
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to save preferences:', e);
|
||
alert('Fehler beim Speichern der Einstellungen');
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// SUSPENSION CHECK FUNCTIONS
|
||
// ==========================================
|
||
let isSuspended = false;
|
||
|
||
// Check suspension status after login
|
||
async function checkSuspensionStatus() {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/account/suspension-status', {
|
||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||
});
|
||
|
||
if (!response.ok) return;
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.suspended) {
|
||
isSuspended = true;
|
||
showSuspensionOverlay(data);
|
||
} else {
|
||
isSuspended = false;
|
||
hideSuspensionOverlay();
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to check suspension status:', e);
|
||
}
|
||
}
|
||
|
||
// Show suspension overlay
|
||
function showSuspensionOverlay(data) {
|
||
const overlay = document.getElementById('suspension-overlay');
|
||
const docList = document.getElementById('suspension-doc-list');
|
||
|
||
if (!overlay || !docList) return;
|
||
|
||
// Populate document list
|
||
if (data.pending_deadlines && data.pending_deadlines.length > 0) {
|
||
docList.innerHTML = data.pending_deadlines.map(d => {
|
||
const deadline = new Date(d.deadline_at);
|
||
const isOverdue = deadline < new Date();
|
||
return `
|
||
<div class="suspension-doc-item">
|
||
<span class="suspension-doc-name">${escapeHtml(d.document_name)}</span>
|
||
<span class="suspension-doc-deadline">${isOverdue ? 'Überfällig' : deadline.toLocaleDateString('de-DE')}</span>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
} else if (data.details && data.details.documents) {
|
||
docList.innerHTML = data.details.documents.map(doc => `
|
||
<div class="suspension-doc-item">
|
||
<span class="suspension-doc-name">${escapeHtml(doc)}</span>
|
||
<span class="suspension-doc-deadline">Bestätigung erforderlich</span>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
overlay.classList.add('active');
|
||
}
|
||
|
||
// Hide suspension overlay
|
||
function hideSuspensionOverlay() {
|
||
const overlay = document.getElementById('suspension-overlay');
|
||
if (overlay) {
|
||
overlay.classList.remove('active');
|
||
}
|
||
}
|
||
|
||
// Show consent modal from suspension overlay
|
||
function showConsentModal() {
|
||
hideSuspensionOverlay();
|
||
// Open legal modal to consent tab
|
||
document.getElementById('legal-modal')?.classList.add('active');
|
||
// Switch to appropriate tab
|
||
}
|
||
|
||
// Initialize auth on page load
|
||
checkAuthStatus();
|
||
checkAuthUrlParams();
|
||
|
||
// ==========================================
|
||
// RICH TEXT EDITOR FUNCTIONS
|
||
// ==========================================
|
||
const versionEditor = document.getElementById('admin-version-editor');
|
||
const versionContentHidden = document.getElementById('admin-version-content');
|
||
const editorCharCount = document.getElementById('editor-char-count');
|
||
|
||
// Update hidden field and char count when editor content changes
|
||
versionEditor?.addEventListener('input', () => {
|
||
versionContentHidden.value = versionEditor.innerHTML;
|
||
const textLength = versionEditor.textContent.length;
|
||
editorCharCount.textContent = `${textLength} Zeichen`;
|
||
});
|
||
|
||
// Format document with execCommand
|
||
function formatDoc(cmd, value = null) {
|
||
versionEditor.focus();
|
||
document.execCommand(cmd, false, value);
|
||
}
|
||
|
||
// Format block element
|
||
function formatBlock(tag) {
|
||
versionEditor.focus();
|
||
document.execCommand('formatBlock', false, `<${tag}>`);
|
||
}
|
||
|
||
// Insert link
|
||
function insertLink() {
|
||
const url = prompt('Link-URL eingeben:', 'https://');
|
||
if (url) {
|
||
versionEditor.focus();
|
||
document.execCommand('createLink', false, url);
|
||
}
|
||
}
|
||
|
||
// Handle Word document upload
|
||
async function handleWordUpload(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
// Show loading indicator
|
||
const editor = document.getElementById('admin-version-editor');
|
||
const originalContent = editor.innerHTML;
|
||
editor.innerHTML = '<p style="color: var(--bp-text-muted); text-align: center; padding: 40px;">Word-Dokument wird verarbeitet...</p>';
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
try {
|
||
const response = await fetch('/api/consent/admin/versions/upload-word', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
editor.innerHTML = data.html || '<p>Konvertierung fehlgeschlagen</p>';
|
||
versionContentHidden.value = editor.innerHTML;
|
||
|
||
// Update char count
|
||
const textLength = editor.textContent.length;
|
||
editorCharCount.textContent = `${textLength} Zeichen`;
|
||
} else {
|
||
const error = await response.json();
|
||
editor.innerHTML = originalContent;
|
||
alert('Fehler beim Importieren: ' + (error.detail || 'Unbekannter Fehler'));
|
||
}
|
||
} catch (e) {
|
||
editor.innerHTML = originalContent;
|
||
alert('Fehler beim Hochladen: ' + e.message);
|
||
}
|
||
|
||
// Reset file input
|
||
event.target.value = '';
|
||
}
|
||
|
||
// Handle paste from Word - clean up the HTML
|
||
versionEditor?.addEventListener('paste', (e) => {
|
||
// Get pasted data via clipboard API
|
||
const clipboardData = e.clipboardData || window.clipboardData;
|
||
const pastedData = clipboardData.getData('text/html') || clipboardData.getData('text/plain');
|
||
|
||
if (pastedData && clipboardData.getData('text/html')) {
|
||
e.preventDefault();
|
||
|
||
// Clean the HTML
|
||
const cleanHtml = cleanWordHtml(pastedData);
|
||
document.execCommand('insertHTML', false, cleanHtml);
|
||
|
||
// Update hidden field
|
||
versionContentHidden.value = versionEditor.innerHTML;
|
||
}
|
||
});
|
||
|
||
// Clean Word-specific HTML
|
||
function cleanWordHtml(html) {
|
||
// Create a temporary container
|
||
const temp = document.createElement('div');
|
||
temp.innerHTML = html;
|
||
|
||
// Remove Word-specific elements and attributes
|
||
const elementsToRemove = temp.querySelectorAll('style, script, meta, link, xml');
|
||
elementsToRemove.forEach(el => el.remove());
|
||
|
||
// Get text content from Word spans with specific styling
|
||
let cleanedHtml = temp.innerHTML;
|
||
|
||
// Remove mso-* styles and other Office-specific CSS
|
||
cleanedHtml = cleanedHtml.replace(/\s*mso-[^:]+:[^;]+;?/gi, '');
|
||
cleanedHtml = cleanedHtml.replace(/\s*style="[^"]*"/gi, '');
|
||
cleanedHtml = cleanedHtml.replace(/\s*class="[^"]*"/gi, '');
|
||
cleanedHtml = cleanedHtml.replace(/<o:p><\/o:p>/gi, '');
|
||
cleanedHtml = cleanedHtml.replace(/<\/?o:[^>]*>/gi, '');
|
||
cleanedHtml = cleanedHtml.replace(/<\/?w:[^>]*>/gi, '');
|
||
cleanedHtml = cleanedHtml.replace(/<\/?m:[^>]*>/gi, '');
|
||
|
||
// Clean up empty spans
|
||
cleanedHtml = cleanedHtml.replace(/<span[^>]*>\s*<\/span>/gi, '');
|
||
|
||
// Convert Word list markers to proper lists
|
||
cleanedHtml = cleanedHtml.replace(/<p[^>]*>\s*[•·]\s*/gi, '<li>');
|
||
|
||
return cleanedHtml;
|
||
}
|
||
|
||
// ==========================================
|
||
// ADMIN PANEL
|
||
// ==========================================
|
||
const adminModal = document.getElementById('admin-modal');
|
||
const adminModalClose = document.getElementById('admin-modal-close');
|
||
const adminTabs = document.querySelectorAll('.admin-tab');
|
||
const adminContents = document.querySelectorAll('.admin-content');
|
||
const btnAdmin = document.getElementById('btn-admin');
|
||
|
||
// Admin data cache
|
||
let adminDocuments = [];
|
||
let adminCookieCategories = [];
|
||
|
||
// Open admin modal
|
||
btnAdmin?.addEventListener('click', async () => {
|
||
adminModal.classList.add('active');
|
||
await loadAdminDocuments();
|
||
await loadAdminCookieCategories();
|
||
populateDocumentSelect();
|
||
});
|
||
|
||
// Close admin modal
|
||
adminModalClose?.addEventListener('click', () => {
|
||
adminModal.classList.remove('active');
|
||
});
|
||
|
||
// Close on background click
|
||
adminModal?.addEventListener('click', (e) => {
|
||
if (e.target === adminModal) {
|
||
adminModal.classList.remove('active');
|
||
}
|
||
});
|
||
|
||
// Admin tab switching
|
||
adminTabs.forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
const tabId = tab.dataset.tab;
|
||
adminTabs.forEach(t => t.classList.remove('active'));
|
||
adminContents.forEach(c => c.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
document.getElementById(`admin-${tabId}`)?.classList.add('active');
|
||
|
||
// Load stats when stats tab is clicked
|
||
if (tabId === 'stats') {
|
||
loadAdminStats();
|
||
}
|
||
});
|
||
});
|
||
|
||
// ==========================================
|
||
// DOCUMENTS MANAGEMENT
|
||
// ==========================================
|
||
async function loadAdminDocuments() {
|
||
const container = document.getElementById('admin-doc-table-container');
|
||
container.innerHTML = '<div class="admin-loading">Lade Dokumente...</div>';
|
||
|
||
try {
|
||
const res = await fetch('/api/consent/admin/documents');
|
||
if (!res.ok) throw new Error('Failed to load');
|
||
const data = await res.json();
|
||
adminDocuments = data.documents || [];
|
||
renderDocumentsTable();
|
||
} catch(e) {
|
||
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Dokumente.</div>';
|
||
}
|
||
}
|
||
|
||
function renderDocumentsTable() {
|
||
const container = document.getElementById('admin-doc-table-container');
|
||
|
||
// Alle Dokumente anzeigen
|
||
const allDocs = adminDocuments;
|
||
|
||
if (allDocs.length === 0) {
|
||
container.innerHTML = '<div class="admin-empty">Keine Dokumente vorhanden. Klicken Sie auf "+ Neues Dokument" um ein Dokument zu erstellen.</div>';
|
||
return;
|
||
}
|
||
|
||
const typeLabels = {
|
||
'terms': 'AGB',
|
||
'privacy': 'Datenschutz',
|
||
'cookies': 'Cookies',
|
||
'community': 'Community',
|
||
'imprint': 'Impressum'
|
||
};
|
||
|
||
const html = `
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Typ</th>
|
||
<th>Name</th>
|
||
<th>Beschreibung</th>
|
||
<th>Status</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${allDocs.map(doc => `
|
||
<tr>
|
||
<td><span class="admin-badge admin-badge-${doc.is_mandatory ? 'mandatory' : 'optional'}">${typeLabels[doc.type] || doc.type}</span></td>
|
||
<td>${doc.name}</td>
|
||
<td style="color: var(--bp-text-muted); font-size: 12px;">${doc.description || '-'}</td>
|
||
<td>
|
||
${doc.is_active ? '<span class="admin-badge admin-badge-published">Aktiv</span>' : '<span class="admin-badge admin-badge-draft">Inaktiv</span>'}
|
||
${doc.is_mandatory ? '<span class="admin-badge admin-badge-mandatory" style="margin-left: 4px;">Pflicht</span>' : ''}
|
||
</td>
|
||
<td class="admin-actions">
|
||
<button class="admin-btn admin-btn-secondary" onclick="editDocument('${doc.id}')" title="Bearbeiten">✏️</button>
|
||
<button class="admin-btn admin-btn-primary" onclick="goToVersions('${doc.id}')">Versionen</button>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function goToVersions(docId) {
|
||
// Wechsle zum Versionen-Tab und wähle das Dokument aus
|
||
const versionsTab = document.querySelector('.admin-tab[data-tab="versions"]');
|
||
if (versionsTab) {
|
||
versionsTab.click();
|
||
setTimeout(() => {
|
||
const select = document.getElementById('admin-version-doc-select');
|
||
if (select) {
|
||
select.value = docId;
|
||
loadVersionsForDocument();
|
||
}
|
||
}, 100);
|
||
}
|
||
}
|
||
|
||
function showDocumentForm(doc = null) {
|
||
const form = document.getElementById('admin-document-form');
|
||
const title = document.getElementById('admin-document-form-title');
|
||
|
||
if (doc) {
|
||
title.textContent = 'Dokument bearbeiten';
|
||
document.getElementById('admin-document-id').value = doc.id;
|
||
document.getElementById('admin-document-type').value = doc.type;
|
||
document.getElementById('admin-document-name').value = doc.name;
|
||
document.getElementById('admin-document-description').value = doc.description || '';
|
||
document.getElementById('admin-document-mandatory').checked = doc.is_mandatory;
|
||
} else {
|
||
title.textContent = 'Neues Dokument erstellen';
|
||
document.getElementById('admin-document-id').value = '';
|
||
document.getElementById('admin-document-type').value = '';
|
||
document.getElementById('admin-document-name').value = '';
|
||
document.getElementById('admin-document-description').value = '';
|
||
document.getElementById('admin-document-mandatory').checked = true;
|
||
}
|
||
|
||
form.style.display = 'block';
|
||
}
|
||
|
||
function hideDocumentForm() {
|
||
document.getElementById('admin-document-form').style.display = 'none';
|
||
}
|
||
|
||
function editDocument(docId) {
|
||
const doc = adminDocuments.find(d => d.id === docId);
|
||
if (doc) showDocumentForm(doc);
|
||
}
|
||
|
||
async function saveDocument() {
|
||
const docId = document.getElementById('admin-document-id').value;
|
||
const docType = document.getElementById('admin-document-type').value;
|
||
const docName = document.getElementById('admin-document-name').value;
|
||
|
||
if (!docType || !docName) {
|
||
alert('Bitte füllen Sie alle Pflichtfelder aus (Typ und Name).');
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
type: docType,
|
||
name: docName,
|
||
description: document.getElementById('admin-document-description').value || null,
|
||
is_mandatory: document.getElementById('admin-document-mandatory').checked
|
||
};
|
||
|
||
try {
|
||
const url = docId ? `/api/consent/admin/documents/${docId}` : '/api/consent/admin/documents';
|
||
const method = docId ? 'PUT' : 'POST';
|
||
|
||
const res = await fetch(url, {
|
||
method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
if (!res.ok) throw new Error('Failed to save');
|
||
|
||
hideDocumentForm();
|
||
await loadAdminDocuments();
|
||
populateDocumentSelect();
|
||
alert('Dokument gespeichert!');
|
||
} catch(e) {
|
||
alert('Fehler beim Speichern: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function deleteDocument(docId) {
|
||
if (!confirm('Dokument wirklich deaktivieren?')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/documents/${docId}`, { method: 'DELETE' });
|
||
if (!res.ok) throw new Error('Failed to delete');
|
||
|
||
await loadAdminDocuments();
|
||
populateDocumentSelect();
|
||
alert('Dokument deaktiviert!');
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// VERSIONS MANAGEMENT
|
||
// ==========================================
|
||
function populateDocumentSelect() {
|
||
const select = document.getElementById('admin-version-doc-select');
|
||
const uniqueDocs = [...new Map(adminDocuments.map(d => [d.type, d])).values()];
|
||
|
||
select.innerHTML = '<option value="">-- Dokument auswählen --</option>' +
|
||
adminDocuments.filter(d => d.is_active).map(doc =>
|
||
`<option value="${doc.id}">${doc.name} (${doc.type})</option>`
|
||
).join('');
|
||
}
|
||
|
||
async function loadVersionsForDocument() {
|
||
const docId = document.getElementById('admin-version-doc-select').value;
|
||
const container = document.getElementById('admin-version-table-container');
|
||
const btnNew = document.getElementById('btn-new-version');
|
||
|
||
if (!docId) {
|
||
container.innerHTML = '<div class="admin-empty">Wählen Sie ein Dokument aus.</div>';
|
||
btnNew.disabled = true;
|
||
return;
|
||
}
|
||
|
||
btnNew.disabled = false;
|
||
container.innerHTML = '<div class="admin-loading">Lade Versionen...</div>';
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
|
||
if (!res.ok) throw new Error('Failed to load');
|
||
const data = await res.json();
|
||
renderVersionsTable(data.versions || []);
|
||
} catch(e) {
|
||
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Versionen.</div>';
|
||
}
|
||
}
|
||
|
||
function renderVersionsTable(versions) {
|
||
const container = document.getElementById('admin-version-table-container');
|
||
if (versions.length === 0) {
|
||
container.innerHTML = '<div class="admin-empty">Keine Versionen vorhanden.</div>';
|
||
return;
|
||
}
|
||
|
||
const getStatusBadge = (status) => {
|
||
const statusLabels = {
|
||
'draft': 'Entwurf',
|
||
'review': 'In Prüfung',
|
||
'approved': 'Genehmigt',
|
||
'rejected': 'Abgelehnt',
|
||
'scheduled': 'Geplant',
|
||
'published': 'Veröffentlicht',
|
||
'archived': 'Archiviert'
|
||
};
|
||
return statusLabels[status] || status;
|
||
};
|
||
|
||
const formatScheduledDate = (isoDate) => {
|
||
if (!isoDate) return '';
|
||
const date = new Date(isoDate);
|
||
return date.toLocaleString('de-DE', {
|
||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||
hour: '2-digit', minute: '2-digit'
|
||
});
|
||
};
|
||
|
||
const html = `
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Version</th>
|
||
<th>Sprache</th>
|
||
<th>Titel</th>
|
||
<th>Status</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${versions.map(v => `
|
||
<tr>
|
||
<td>${v.version}</td>
|
||
<td>${v.language.toUpperCase()}</td>
|
||
<td>${v.title}</td>
|
||
<td>
|
||
<span class="admin-badge admin-badge-${v.status}">${getStatusBadge(v.status)}</span>
|
||
${v.scheduled_publish_at ? `<br><small>Geplant: ${formatScheduledDate(v.scheduled_publish_at)}</small>` : ''}
|
||
</td>
|
||
<td class="admin-actions">
|
||
${v.status === 'draft' ? `
|
||
<button class="admin-btn admin-btn-edit" onclick="editVersion('${v.id}')">Bearbeiten</button>
|
||
<button class="admin-btn admin-btn-primary" onclick="submitForReview('${v.id}')">Zur Prüfung</button>
|
||
<button class="admin-btn admin-btn-delete" onclick="deleteVersion('${v.id}')" title="Version dauerhaft löschen">🗑️</button>
|
||
` : ''}
|
||
${v.status === 'review' ? `
|
||
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
|
||
<button class="admin-btn admin-btn-primary" onclick="showApprovalDialog('${v.id}')">Genehmigen</button>
|
||
<button class="admin-btn admin-btn-delete" onclick="rejectVersion('${v.id}')">Ablehnen</button>
|
||
` : ''}
|
||
${v.status === 'rejected' ? `
|
||
<button class="admin-btn admin-btn-edit" onclick="editVersion('${v.id}')">Bearbeiten</button>
|
||
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
|
||
<button class="admin-btn admin-btn-delete" onclick="deleteVersion('${v.id}')" title="Version dauerhaft löschen">🗑️</button>
|
||
` : ''}
|
||
${v.status === 'scheduled' ? `
|
||
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
|
||
<span class="admin-info-text">Wartet auf Veröffentlichung</span>
|
||
` : ''}
|
||
${v.status === 'approved' ? `
|
||
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
|
||
<button class="admin-btn admin-btn-publish" onclick="publishVersion('${v.id}')">Sofort veröffentlichen</button>
|
||
<button class="admin-btn admin-btn-delete" onclick="rejectVersion('${v.id}')">Zurücksetzen</button>
|
||
` : ''}
|
||
${v.status === 'published' ? `
|
||
<button class="admin-btn admin-btn-delete" onclick="archiveVersion('${v.id}')">Archivieren</button>
|
||
` : ''}
|
||
<button class="admin-btn" onclick="showApprovalHistory('${v.id}')">Historie</button>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function showVersionForm() {
|
||
const form = document.getElementById('admin-version-form');
|
||
document.getElementById('admin-version-id').value = '';
|
||
document.getElementById('admin-version-number').value = '';
|
||
document.getElementById('admin-version-lang').value = 'de';
|
||
document.getElementById('admin-version-title').value = '';
|
||
document.getElementById('admin-version-summary').value = '';
|
||
document.getElementById('admin-version-content').value = '';
|
||
// Clear rich text editor
|
||
const editor = document.getElementById('admin-version-editor');
|
||
if (editor) {
|
||
editor.innerHTML = '';
|
||
document.getElementById('editor-char-count').textContent = '0 Zeichen';
|
||
}
|
||
form.classList.add('active');
|
||
}
|
||
|
||
function hideVersionForm() {
|
||
document.getElementById('admin-version-form').classList.remove('active');
|
||
}
|
||
|
||
async function editVersion(versionId) {
|
||
// Lade die Version und fülle das Formular
|
||
const docId = document.getElementById('admin-version-doc-select').value;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
|
||
if (!res.ok) throw new Error('Failed to load versions');
|
||
const data = await res.json();
|
||
|
||
const version = (data.versions || []).find(v => v.id === versionId);
|
||
if (!version) {
|
||
alert('Version nicht gefunden');
|
||
return;
|
||
}
|
||
|
||
// Formular öffnen und Daten einfügen
|
||
const form = document.getElementById('admin-version-form');
|
||
document.getElementById('admin-version-id').value = version.id;
|
||
document.getElementById('admin-version-number').value = version.version;
|
||
document.getElementById('admin-version-lang').value = version.language;
|
||
document.getElementById('admin-version-title').value = version.title;
|
||
document.getElementById('admin-version-summary').value = version.summary || '';
|
||
|
||
// Rich-Text-Editor mit Inhalt füllen
|
||
const editor = document.getElementById('admin-version-editor');
|
||
if (editor) {
|
||
editor.innerHTML = version.content || '';
|
||
const charCount = editor.textContent.length;
|
||
document.getElementById('editor-char-count').textContent = charCount + ' Zeichen';
|
||
}
|
||
document.getElementById('admin-version-content').value = version.content || '';
|
||
|
||
form.classList.add('active');
|
||
} catch(e) {
|
||
alert('Fehler beim Laden der Version: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function saveVersion() {
|
||
const docId = document.getElementById('admin-version-doc-select').value;
|
||
const versionId = document.getElementById('admin-version-id').value;
|
||
|
||
// Get content from rich text editor
|
||
const editor = document.getElementById('admin-version-editor');
|
||
const content = editor ? editor.innerHTML : document.getElementById('admin-version-content').value;
|
||
|
||
const data = {
|
||
document_id: docId,
|
||
version: document.getElementById('admin-version-number').value,
|
||
language: document.getElementById('admin-version-lang').value,
|
||
title: document.getElementById('admin-version-title').value,
|
||
summary: document.getElementById('admin-version-summary').value,
|
||
content: content
|
||
};
|
||
|
||
try {
|
||
const url = versionId ? `/api/consent/admin/versions/${versionId}` : '/api/consent/admin/versions';
|
||
const method = versionId ? 'PUT' : 'POST';
|
||
|
||
const res = await fetch(url, {
|
||
method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
if (!res.ok) throw new Error('Failed to save');
|
||
|
||
hideVersionForm();
|
||
await loadVersionsForDocument();
|
||
alert('Version gespeichert!');
|
||
} catch(e) {
|
||
alert('Fehler beim Speichern: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function publishVersion(versionId) {
|
||
if (!confirm('Version wirklich veröffentlichen?')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/publish`, { method: 'POST' });
|
||
if (!res.ok) throw new Error('Failed to publish');
|
||
|
||
await loadVersionsForDocument();
|
||
alert('Version veröffentlicht!');
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function archiveVersion(versionId) {
|
||
if (!confirm('Version wirklich archivieren?')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/archive`, { method: 'POST' });
|
||
if (!res.ok) throw new Error('Failed to archive');
|
||
|
||
await loadVersionsForDocument();
|
||
alert('Version archiviert!');
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function deleteVersion(versionId) {
|
||
if (!confirm('Version wirklich dauerhaft löschen?\\n\\nDie Versionsnummer wird wieder frei und kann erneut verwendet werden.\\n\\nDiese Aktion kann nicht rückgängig gemacht werden!')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}`, { method: 'DELETE' });
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail?.message || err.error || 'Löschen fehlgeschlagen');
|
||
}
|
||
|
||
await loadVersionsForDocument();
|
||
alert('Version wurde dauerhaft gelöscht.');
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// DSB APPROVAL WORKFLOW
|
||
// ==========================================
|
||
|
||
async function submitForReview(versionId) {
|
||
if (!confirm('Version zur DSB-Prüfung einreichen?')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/submit-review`, { method: 'POST' });
|
||
if (!res.ok) {
|
||
const data = await res.json();
|
||
throw new Error(data.detail?.error || 'Einreichung fehlgeschlagen');
|
||
}
|
||
|
||
await loadVersionsForDocument();
|
||
alert('Version wurde zur Prüfung eingereicht!');
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// Dialog für Genehmigung mit Veröffentlichungszeitpunkt
|
||
let approvalVersionId = null;
|
||
|
||
function showApprovalDialog(versionId) {
|
||
approvalVersionId = versionId;
|
||
const dialog = document.getElementById('approval-dialog');
|
||
|
||
// Setze Minimum-Datum auf morgen
|
||
const tomorrow = new Date();
|
||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||
tomorrow.setHours(0, 0, 0, 0);
|
||
document.getElementById('approval-date').min = tomorrow.toISOString().split('T')[0];
|
||
document.getElementById('approval-date').value = '';
|
||
document.getElementById('approval-time').value = '00:00';
|
||
document.getElementById('approval-comment').value = '';
|
||
|
||
dialog.classList.add('active');
|
||
}
|
||
|
||
function hideApprovalDialog() {
|
||
document.getElementById('approval-dialog').classList.remove('active');
|
||
approvalVersionId = null;
|
||
}
|
||
|
||
async function submitApproval() {
|
||
if (!approvalVersionId) return;
|
||
|
||
const dateInput = document.getElementById('approval-date').value;
|
||
const timeInput = document.getElementById('approval-time').value;
|
||
const comment = document.getElementById('approval-comment').value;
|
||
|
||
let scheduledPublishAt = null;
|
||
if (dateInput) {
|
||
// Kombiniere Datum und Zeit zu ISO 8601
|
||
const datetime = new Date(dateInput + 'T' + (timeInput || '00:00') + ':00');
|
||
scheduledPublishAt = datetime.toISOString();
|
||
}
|
||
|
||
try {
|
||
const body = { comment: comment || '' };
|
||
if (scheduledPublishAt) {
|
||
body.scheduled_publish_at = scheduledPublishAt;
|
||
}
|
||
|
||
const res = await fetch(`/api/consent/admin/versions/${approvalVersionId}/approve`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body)
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const data = await res.json();
|
||
throw new Error(data.detail?.error || data.detail || 'Genehmigung fehlgeschlagen');
|
||
}
|
||
|
||
hideApprovalDialog();
|
||
await loadVersionsForDocument();
|
||
|
||
if (scheduledPublishAt) {
|
||
const date = new Date(scheduledPublishAt);
|
||
alert('Version genehmigt! Geplante Veröffentlichung: ' + date.toLocaleString('de-DE'));
|
||
} else {
|
||
alert('Version genehmigt! Sie kann jetzt manuell veröffentlicht werden.');
|
||
}
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// Alte Funktion für Rückwärtskompatibilität
|
||
async function approveVersion(versionId) {
|
||
showApprovalDialog(versionId);
|
||
}
|
||
|
||
async function rejectVersion(versionId) {
|
||
const comment = prompt('Begründung für Ablehnung (erforderlich):');
|
||
if (!comment) {
|
||
alert('Eine Begründung ist erforderlich.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/reject`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ comment: comment })
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const data = await res.json();
|
||
throw new Error(data.detail?.error || data.detail || 'Ablehnung fehlgeschlagen');
|
||
}
|
||
|
||
await loadVersionsForDocument();
|
||
alert('Version abgelehnt und zurück in Entwurf-Status versetzt.');
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// Store current compare version for actions
|
||
let currentCompareVersionId = null;
|
||
let currentCompareVersionStatus = null;
|
||
let currentCompareDocId = null;
|
||
|
||
async function showCompareView(versionId) {
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/compare`);
|
||
if (!res.ok) throw new Error('Vergleich konnte nicht geladen werden');
|
||
const data = await res.json();
|
||
|
||
const currentVersion = data.current_version;
|
||
const publishedVersion = data.published_version;
|
||
const history = data.approval_history || [];
|
||
|
||
// Store version info for actions
|
||
currentCompareVersionId = versionId;
|
||
currentCompareVersionStatus = currentVersion.status;
|
||
currentCompareDocId = currentVersion.document_id;
|
||
|
||
// Update header info
|
||
document.getElementById('compare-published-info').textContent =
|
||
publishedVersion ? `${publishedVersion.title} (v${publishedVersion.version})` : 'Keine Version';
|
||
document.getElementById('compare-draft-info').textContent =
|
||
`${currentVersion.title} (v${currentVersion.version})`;
|
||
document.getElementById('compare-published-version').textContent =
|
||
publishedVersion ? `v${publishedVersion.version}` : '';
|
||
document.getElementById('compare-draft-version').textContent =
|
||
`v${currentVersion.version} - ${currentVersion.status}`;
|
||
|
||
// Populate content panels
|
||
const leftPanel = document.getElementById('compare-content-left');
|
||
const rightPanel = document.getElementById('compare-content-right');
|
||
|
||
leftPanel.innerHTML = publishedVersion
|
||
? publishedVersion.content
|
||
: '<div class="no-content">Keine veröffentlichte Version vorhanden</div>';
|
||
rightPanel.innerHTML = currentVersion.content || '<div class="no-content">Kein Inhalt</div>';
|
||
|
||
// Populate history
|
||
const historyContainer = document.getElementById('compare-history-container');
|
||
if (history.length > 0) {
|
||
historyContainer.innerHTML = `
|
||
<div class="compare-history-title">Genehmigungsverlauf</div>
|
||
<div class="compare-history-list">
|
||
${history.map(h => `
|
||
<span class="compare-history-item">
|
||
<strong>${h.action}</strong> von ${h.approver || 'System'}
|
||
(${new Date(h.created_at).toLocaleString('de-DE')})
|
||
${h.comment ? ': ' + h.comment : ''}
|
||
</span>
|
||
`).join(' | ')}
|
||
</div>
|
||
`;
|
||
} else {
|
||
historyContainer.innerHTML = '';
|
||
}
|
||
|
||
// Render action buttons based on status
|
||
renderCompareActions(currentVersion.status, versionId);
|
||
|
||
// Setup synchronized scrolling
|
||
setupSyncScroll(leftPanel, rightPanel);
|
||
|
||
// Show the overlay
|
||
document.getElementById('version-compare-view').classList.add('active');
|
||
document.body.style.overflow = 'hidden';
|
||
} catch(e) {
|
||
alert('Fehler beim Laden des Vergleichs: ' + e.message);
|
||
}
|
||
}
|
||
|
||
function renderCompareActions(status, versionId) {
|
||
const actionsContainer = document.getElementById('compare-actions-container');
|
||
|
||
let buttons = '';
|
||
|
||
// Edit button - available for draft, review, and rejected
|
||
if (status === 'draft' || status === 'review' || status === 'rejected') {
|
||
buttons += `<button class="btn btn-primary" onclick="editVersionFromCompare('${versionId}')">
|
||
✏️ Version bearbeiten
|
||
</button>`;
|
||
}
|
||
|
||
// Status-specific actions
|
||
if (status === 'draft') {
|
||
buttons += `<button class="btn" onclick="submitForReviewFromCompare('${versionId}')">
|
||
📤 Zur Prüfung einreichen
|
||
</button>`;
|
||
}
|
||
|
||
if (status === 'review') {
|
||
buttons += `<button class="btn btn-success" onclick="approveVersionFromCompare('${versionId}')">
|
||
✅ Genehmigen
|
||
</button>`;
|
||
buttons += `<button class="btn btn-danger" onclick="rejectVersionFromCompare('${versionId}')">
|
||
❌ Ablehnen
|
||
</button>`;
|
||
}
|
||
|
||
if (status === 'approved') {
|
||
buttons += `<button class="btn btn-primary" onclick="publishVersionFromCompare('${versionId}')">
|
||
🚀 Veröffentlichen
|
||
</button>`;
|
||
}
|
||
|
||
// Delete button for draft/rejected
|
||
if (status === 'draft' || status === 'rejected') {
|
||
buttons += `<button class="btn btn-danger" onclick="deleteVersionFromCompare('${versionId}')" style="margin-left: auto;">
|
||
🗑️ Löschen
|
||
</button>`;
|
||
}
|
||
|
||
actionsContainer.innerHTML = buttons;
|
||
}
|
||
|
||
async function editVersionFromCompare(versionId) {
|
||
// Store the doc ID before closing compare view
|
||
const docId = currentCompareDocId;
|
||
|
||
// Close compare view
|
||
hideCompareView();
|
||
|
||
// Switch to versions tab
|
||
const versionsTab = document.querySelector('.admin-tab[data-tab="versions"]');
|
||
if (versionsTab) {
|
||
versionsTab.click();
|
||
}
|
||
|
||
// Wait a moment for the tab to become active
|
||
await new Promise(resolve => setTimeout(resolve, 150));
|
||
|
||
// Ensure document select is populated
|
||
populateDocumentSelect();
|
||
|
||
// Set the document select if we have the doc ID
|
||
if (docId) {
|
||
const select = document.getElementById('admin-version-doc-select');
|
||
if (select) {
|
||
select.value = docId;
|
||
// Load versions for this document
|
||
await loadVersionsForDocument();
|
||
}
|
||
}
|
||
|
||
// Now load the version data directly and open the form
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
|
||
if (!res.ok) throw new Error('Failed to load versions');
|
||
const data = await res.json();
|
||
|
||
const version = (data.versions || []).find(v => v.id === versionId);
|
||
if (!version) {
|
||
alert('Version nicht gefunden');
|
||
return;
|
||
}
|
||
|
||
// Open the form and fill with version data
|
||
const form = document.getElementById('admin-version-form');
|
||
document.getElementById('admin-version-id').value = version.id;
|
||
document.getElementById('admin-version-number').value = version.version;
|
||
document.getElementById('admin-version-lang').value = version.language;
|
||
document.getElementById('admin-version-title').value = version.title;
|
||
document.getElementById('admin-version-summary').value = version.summary || '';
|
||
|
||
// Fill rich text editor with content
|
||
const editor = document.getElementById('admin-version-editor');
|
||
if (editor) {
|
||
editor.innerHTML = version.content || '';
|
||
const charCount = editor.textContent.length;
|
||
document.getElementById('editor-char-count').textContent = charCount + ' Zeichen';
|
||
}
|
||
document.getElementById('admin-version-content').value = version.content || '';
|
||
|
||
form.classList.add('active');
|
||
} catch(e) {
|
||
alert('Fehler beim Laden der Version: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function submitForReviewFromCompare(versionId) {
|
||
await submitForReview(versionId);
|
||
hideCompareView();
|
||
await loadVersionsForDocument();
|
||
}
|
||
|
||
async function approveVersionFromCompare(versionId) {
|
||
const comment = prompt('Kommentar zur Genehmigung (optional):');
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/approve`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ comment: comment || '' })
|
||
});
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail?.error || err.error || 'Genehmigung fehlgeschlagen');
|
||
}
|
||
alert('Version genehmigt!');
|
||
hideCompareView();
|
||
await loadVersionsForDocument();
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function rejectVersionFromCompare(versionId) {
|
||
const comment = prompt('Begründung für die Ablehnung (erforderlich):');
|
||
if (!comment) {
|
||
alert('Eine Begründung ist erforderlich.');
|
||
return;
|
||
}
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/reject`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ comment: comment })
|
||
});
|
||
if (!res.ok) throw new Error('Ablehnung fehlgeschlagen');
|
||
alert('Version abgelehnt. Der Autor kann sie überarbeiten.');
|
||
hideCompareView();
|
||
await loadVersionsForDocument();
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function publishVersionFromCompare(versionId) {
|
||
if (!confirm('Version wirklich veröffentlichen?')) return;
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/publish`, { method: 'POST' });
|
||
if (!res.ok) throw new Error('Veröffentlichung fehlgeschlagen');
|
||
alert('Version veröffentlicht!');
|
||
hideCompareView();
|
||
await loadVersionsForDocument();
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function deleteVersionFromCompare(versionId) {
|
||
if (!confirm('Version wirklich dauerhaft löschen?\\n\\nDie Versionsnummer wird wieder frei.')) return;
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}`, { method: 'DELETE' });
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail?.message || err.error || 'Löschen fehlgeschlagen');
|
||
}
|
||
alert('Version gelöscht!');
|
||
hideCompareView();
|
||
await loadVersionsForDocument();
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
function hideCompareView() {
|
||
document.getElementById('version-compare-view').classList.remove('active');
|
||
document.body.style.overflow = '';
|
||
// Remove scroll listeners
|
||
const leftPanel = document.getElementById('compare-content-left');
|
||
const rightPanel = document.getElementById('compare-content-right');
|
||
if (leftPanel) leftPanel.onscroll = null;
|
||
if (rightPanel) rightPanel.onscroll = null;
|
||
}
|
||
|
||
// Synchronized scrolling between two panels
|
||
let syncScrollActive = false;
|
||
function setupSyncScroll(leftPanel, rightPanel) {
|
||
// Remove any existing listeners first
|
||
leftPanel.onscroll = null;
|
||
rightPanel.onscroll = null;
|
||
|
||
// Flag to prevent infinite scroll loops
|
||
let isScrolling = false;
|
||
|
||
rightPanel.onscroll = function() {
|
||
if (isScrolling) return;
|
||
isScrolling = true;
|
||
|
||
// Calculate scroll percentage
|
||
const rightScrollPercent = rightPanel.scrollTop / (rightPanel.scrollHeight - rightPanel.clientHeight);
|
||
|
||
// Apply same percentage to left panel
|
||
const leftMaxScroll = leftPanel.scrollHeight - leftPanel.clientHeight;
|
||
leftPanel.scrollTop = rightScrollPercent * leftMaxScroll;
|
||
|
||
setTimeout(() => { isScrolling = false; }, 10);
|
||
};
|
||
|
||
leftPanel.onscroll = function() {
|
||
if (isScrolling) return;
|
||
isScrolling = true;
|
||
|
||
// Calculate scroll percentage
|
||
const leftScrollPercent = leftPanel.scrollTop / (leftPanel.scrollHeight - leftPanel.clientHeight);
|
||
|
||
// Apply same percentage to right panel
|
||
const rightMaxScroll = rightPanel.scrollHeight - rightPanel.clientHeight;
|
||
rightPanel.scrollTop = leftScrollPercent * rightMaxScroll;
|
||
|
||
setTimeout(() => { isScrolling = false; }, 10);
|
||
};
|
||
}
|
||
|
||
async function showApprovalHistory(versionId) {
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/approval-history`);
|
||
if (!res.ok) throw new Error('Historie konnte nicht geladen werden');
|
||
const data = await res.json();
|
||
const history = data.approval_history || [];
|
||
|
||
const content = history.length === 0
|
||
? '<p>Keine Genehmigungshistorie vorhanden.</p>'
|
||
: `
|
||
<table class="admin-table" style="font-size: 14px;">
|
||
<thead>
|
||
<tr>
|
||
<th>Aktion</th>
|
||
<th>Benutzer</th>
|
||
<th>Kommentar</th>
|
||
<th>Datum</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${history.map(h => `
|
||
<tr>
|
||
<td><span class="admin-badge admin-badge-${h.action}">${h.action}</span></td>
|
||
<td>${h.approver || h.name || '-'}</td>
|
||
<td>${h.comment || '-'}</td>
|
||
<td>${new Date(h.created_at).toLocaleString('de-DE')}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
|
||
showCustomModal('Genehmigungsverlauf', content, [
|
||
{ text: 'Schließen', onClick: () => hideCustomModal() }
|
||
]);
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// Custom Modal Functions
|
||
function showCustomModal(title, content, buttons = []) {
|
||
let modal = document.getElementById('custom-modal');
|
||
if (!modal) {
|
||
modal = document.createElement('div');
|
||
modal.id = 'custom-modal';
|
||
modal.className = 'modal-overlay';
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
modal.innerHTML = `
|
||
<div class="modal-content" style="max-width: 900px; width: 95%;">
|
||
<div class="modal-header">
|
||
<h2>${title}</h2>
|
||
<button class="modal-close" onclick="hideCustomModal()">×</button>
|
||
</div>
|
||
<div class="modal-body" style="max-height: 80vh; overflow-y: auto;">
|
||
${content}
|
||
</div>
|
||
${buttons.length > 0 ? `
|
||
<div class="modal-footer" style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px;">
|
||
${buttons.map(b => `
|
||
<button class="admin-btn ${b.primary ? 'admin-btn-primary' : ''}" onclick="(${b.onClick.toString()})()">${b.text}</button>
|
||
`).join('')}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
modal.classList.add('active');
|
||
}
|
||
|
||
function hideCustomModal() {
|
||
const modal = document.getElementById('custom-modal');
|
||
if (modal) modal.classList.remove('active');
|
||
}
|
||
|
||
// ==========================================
|
||
// COOKIE CATEGORIES MANAGEMENT
|
||
// ==========================================
|
||
async function loadAdminCookieCategories() {
|
||
const container = document.getElementById('admin-cookie-table-container');
|
||
container.innerHTML = '<div class="admin-loading">Lade Cookie-Kategorien...</div>';
|
||
|
||
try {
|
||
const res = await fetch('/api/consent/admin/cookies/categories');
|
||
if (!res.ok) throw new Error('Failed to load');
|
||
const data = await res.json();
|
||
adminCookieCategories = data.categories || [];
|
||
renderCookieCategoriesTable();
|
||
} catch(e) {
|
||
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Kategorien.</div>';
|
||
}
|
||
}
|
||
|
||
function renderCookieCategoriesTable() {
|
||
const container = document.getElementById('admin-cookie-table-container');
|
||
if (adminCookieCategories.length === 0) {
|
||
container.innerHTML = '<div class="admin-empty">Keine Cookie-Kategorien vorhanden.</div>';
|
||
return;
|
||
}
|
||
|
||
const html = `
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Anzeigename (DE)</th>
|
||
<th>Typ</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${adminCookieCategories.map(cat => `
|
||
<tr>
|
||
<td><code>${cat.name}</code></td>
|
||
<td>${cat.display_name_de}</td>
|
||
<td>
|
||
${cat.is_mandatory ? '<span class="admin-badge admin-badge-mandatory">Notwendig</span>' : '<span class="admin-badge admin-badge-optional">Optional</span>'}
|
||
</td>
|
||
<td class="admin-actions">
|
||
<button class="admin-btn admin-btn-edit" onclick="editCookieCategory('${cat.id}')">Bearbeiten</button>
|
||
${!cat.is_mandatory ? `<button class="admin-btn admin-btn-delete" onclick="deleteCookieCategory('${cat.id}')">Löschen</button>` : ''}
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function showCookieForm(cat = null) {
|
||
const form = document.getElementById('admin-cookie-form');
|
||
|
||
if (cat) {
|
||
document.getElementById('admin-cookie-id').value = cat.id;
|
||
document.getElementById('admin-cookie-name').value = cat.name;
|
||
document.getElementById('admin-cookie-display-de').value = cat.display_name_de;
|
||
document.getElementById('admin-cookie-display-en').value = cat.display_name_en || '';
|
||
document.getElementById('admin-cookie-desc-de').value = cat.description_de || '';
|
||
document.getElementById('admin-cookie-mandatory').checked = cat.is_mandatory;
|
||
} else {
|
||
document.getElementById('admin-cookie-id').value = '';
|
||
document.getElementById('admin-cookie-name').value = '';
|
||
document.getElementById('admin-cookie-display-de').value = '';
|
||
document.getElementById('admin-cookie-display-en').value = '';
|
||
document.getElementById('admin-cookie-desc-de').value = '';
|
||
document.getElementById('admin-cookie-mandatory').checked = false;
|
||
}
|
||
|
||
form.classList.add('active');
|
||
}
|
||
|
||
function hideCookieForm() {
|
||
document.getElementById('admin-cookie-form').classList.remove('active');
|
||
}
|
||
|
||
function editCookieCategory(catId) {
|
||
const cat = adminCookieCategories.find(c => c.id === catId);
|
||
if (cat) showCookieForm(cat);
|
||
}
|
||
|
||
async function saveCookieCategory() {
|
||
const catId = document.getElementById('admin-cookie-id').value;
|
||
const data = {
|
||
name: document.getElementById('admin-cookie-name').value,
|
||
display_name_de: document.getElementById('admin-cookie-display-de').value,
|
||
display_name_en: document.getElementById('admin-cookie-display-en').value,
|
||
description_de: document.getElementById('admin-cookie-desc-de').value,
|
||
is_mandatory: document.getElementById('admin-cookie-mandatory').checked
|
||
};
|
||
|
||
try {
|
||
const url = catId ? `/api/consent/admin/cookies/categories/${catId}` : '/api/consent/admin/cookies/categories';
|
||
const method = catId ? 'PUT' : 'POST';
|
||
|
||
const res = await fetch(url, {
|
||
method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
if (!res.ok) throw new Error('Failed to save');
|
||
|
||
hideCookieForm();
|
||
await loadAdminCookieCategories();
|
||
alert('Kategorie gespeichert!');
|
||
} catch(e) {
|
||
alert('Fehler beim Speichern: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function deleteCookieCategory(catId) {
|
||
if (!confirm('Kategorie wirklich löschen?')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/cookies/categories/${catId}`, { method: 'DELETE' });
|
||
if (!res.ok) throw new Error('Failed to delete');
|
||
|
||
await loadAdminCookieCategories();
|
||
alert('Kategorie gelöscht!');
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// STATISTICS & GDPR EXPORT
|
||
// ==========================================
|
||
let dataCategories = [];
|
||
|
||
async function loadAdminStats() {
|
||
const container = document.getElementById('admin-stats-container');
|
||
container.innerHTML = '<div class="admin-loading">Lade Statistiken & DSGVO-Informationen...</div>';
|
||
|
||
try {
|
||
// Lade Datenkategorien
|
||
const catRes = await fetch('/api/consent/privacy/data-categories');
|
||
if (catRes.ok) {
|
||
const catData = await catRes.json();
|
||
dataCategories = catData.categories || [];
|
||
}
|
||
|
||
renderStatsPanel();
|
||
} catch(e) {
|
||
container.innerHTML = '<div class="admin-empty">Fehler beim Laden: ' + e.message + '</div>';
|
||
}
|
||
}
|
||
|
||
function renderStatsPanel() {
|
||
const container = document.getElementById('admin-stats-container');
|
||
|
||
// Kategorisiere Daten
|
||
const essential = dataCategories.filter(c => c.is_essential);
|
||
const optional = dataCategories.filter(c => !c.is_essential);
|
||
|
||
const html = `
|
||
<div style="display: grid; gap: 24px;">
|
||
<!-- GDPR Export Section -->
|
||
<div class="admin-form" style="padding: 20px;">
|
||
<h3 style="margin: 0 0 16px 0; font-size: 16px; color: var(--bp-text);">
|
||
<span style="margin-right: 8px;">📋</span> DSGVO-Datenauskunft (Art. 15)
|
||
</h3>
|
||
<p style="color: var(--bp-text-muted); font-size: 13px; margin-bottom: 16px;">
|
||
Exportieren Sie alle personenbezogenen Daten eines Nutzers als PDF-Dokument.
|
||
Dies erfüllt die Anforderungen der DSGVO Art. 15 (Auskunftsrecht).
|
||
</p>
|
||
|
||
<div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
|
||
<input type="text" id="gdpr-export-user-id" class="admin-form-input"
|
||
placeholder="Benutzer-ID (optional für eigene Daten)"
|
||
style="flex: 1; min-width: 200px;">
|
||
<button class="admin-btn admin-btn-primary" onclick="exportUserDataPdf()">
|
||
PDF exportieren
|
||
</button>
|
||
<button class="admin-btn" onclick="previewUserDataHtml()">
|
||
HTML-Vorschau
|
||
</button>
|
||
</div>
|
||
|
||
<div id="gdpr-export-status" style="margin-top: 12px; font-size: 13px;"></div>
|
||
</div>
|
||
|
||
<!-- Data Retention Overview -->
|
||
<div class="admin-form" style="padding: 20px;">
|
||
<h3 style="margin: 0 0 16px 0; font-size: 16px; color: var(--bp-text);">
|
||
<span style="margin-right: 8px;">🗄️</span> Datenkategorien & Löschfristen
|
||
</h3>
|
||
|
||
<div style="margin-bottom: 20px;">
|
||
<h4 style="font-size: 14px; color: var(--bp-primary); margin: 0 0 12px 0;">
|
||
Essentielle Daten (Pflicht für Betrieb)
|
||
</h4>
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Kategorie</th>
|
||
<th>Beschreibung</th>
|
||
<th>Löschfrist</th>
|
||
<th>Rechtsgrundlage</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${essential.map(cat => `
|
||
<tr>
|
||
<td><strong>${cat.name_de}</strong></td>
|
||
<td>${cat.description_de}</td>
|
||
<td><span class="admin-badge admin-badge-published">${cat.retention_period}</span></td>
|
||
<td style="font-size: 11px; color: var(--bp-text-muted);">${cat.legal_basis}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div>
|
||
<h4 style="font-size: 14px; color: var(--bp-warning); margin: 0 0 12px 0;">
|
||
Optionale Daten (nur bei Einwilligung)
|
||
</h4>
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Kategorie</th>
|
||
<th>Beschreibung</th>
|
||
<th>Cookie-Kategorie</th>
|
||
<th>Löschfrist</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${optional.map(cat => `
|
||
<tr>
|
||
<td><strong>${cat.name_de}</strong></td>
|
||
<td>${cat.description_de}</td>
|
||
<td><span class="admin-badge">${cat.cookie_category || '-'}</span></td>
|
||
<td><span class="admin-badge admin-badge-optional">${cat.retention_period}</span></td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quick Stats -->
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
|
||
<div class="admin-form" style="padding: 16px; text-align: center;">
|
||
<div style="font-size: 28px; font-weight: bold; color: var(--bp-primary);">${dataCategories.length}</div>
|
||
<div style="color: var(--bp-text-muted); font-size: 13px;">Datenkategorien</div>
|
||
</div>
|
||
<div class="admin-form" style="padding: 16px; text-align: center;">
|
||
<div style="font-size: 28px; font-weight: bold; color: var(--bp-success);">${essential.length}</div>
|
||
<div style="color: var(--bp-text-muted); font-size: 13px;">Essentiell</div>
|
||
</div>
|
||
<div class="admin-form" style="padding: 16px; text-align: center;">
|
||
<div style="font-size: 28px; font-weight: bold; color: var(--bp-warning);">${optional.length}</div>
|
||
<div style="color: var(--bp-text-muted); font-size: 13px;">Optional (Opt-in)</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
async function exportUserDataPdf() {
|
||
const userIdInput = document.getElementById('gdpr-export-user-id');
|
||
const statusDiv = document.getElementById('gdpr-export-status');
|
||
const userId = userIdInput?.value?.trim();
|
||
|
||
statusDiv.innerHTML = '<span style="color: var(--bp-primary);">Generiere PDF...</span>';
|
||
|
||
try {
|
||
let url = '/api/consent/privacy/export-pdf';
|
||
|
||
// Wenn eine User-ID angegeben wurde, verwende den Admin-Endpoint
|
||
if (userId) {
|
||
url = `/api/consent/admin/privacy/export-pdf/${userId}`;
|
||
}
|
||
|
||
const res = await fetch(url, { method: 'POST' });
|
||
|
||
if (!res.ok) {
|
||
const error = await res.json();
|
||
throw new Error(error.detail?.message || error.detail || 'Export fehlgeschlagen');
|
||
}
|
||
|
||
// PDF herunterladen
|
||
const blob = await res.blob();
|
||
const downloadUrl = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = downloadUrl;
|
||
a.download = userId ? `datenauskunft_${userId.slice(0,8)}.pdf` : 'breakpilot_datenauskunft.pdf';
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
window.URL.revokeObjectURL(downloadUrl);
|
||
|
||
statusDiv.innerHTML = '<span style="color: var(--bp-success);">✓ PDF erfolgreich generiert!</span>';
|
||
} catch(e) {
|
||
statusDiv.innerHTML = `<span style="color: var(--bp-danger);">Fehler: ${e.message}</span>`;
|
||
}
|
||
}
|
||
|
||
async function previewUserDataHtml() {
|
||
const statusDiv = document.getElementById('gdpr-export-status');
|
||
statusDiv.innerHTML = '<span style="color: var(--bp-primary);">Lade Vorschau...</span>';
|
||
|
||
try {
|
||
const res = await fetch('/api/consent/privacy/export-html');
|
||
|
||
if (!res.ok) {
|
||
throw new Error('Vorschau konnte nicht geladen werden');
|
||
}
|
||
|
||
const html = await res.text();
|
||
|
||
// In neuem Tab öffnen
|
||
const win = window.open('', '_blank');
|
||
win.document.write(html);
|
||
win.document.close();
|
||
|
||
statusDiv.innerHTML = '<span style="color: var(--bp-success);">✓ Vorschau in neuem Tab geöffnet</span>';
|
||
} catch(e) {
|
||
statusDiv.innerHTML = `<span style="color: var(--bp-danger);">Fehler: ${e.message}</span>`;
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// DSR (DATA SUBJECT REQUESTS) FUNCTIONS
|
||
// ==========================================
|
||
let dsrList = [];
|
||
let currentDSR = null;
|
||
|
||
const DSR_TYPE_LABELS = {
|
||
'access': 'Art. 15 - Auskunft',
|
||
'rectification': 'Art. 16 - Berichtigung',
|
||
'erasure': 'Art. 17 - Löschung',
|
||
'restriction': 'Art. 18 - Einschränkung',
|
||
'portability': 'Art. 20 - Datenübertragbarkeit'
|
||
};
|
||
|
||
const DSR_STATUS_LABELS = {
|
||
'intake': 'Eingang',
|
||
'identity_verification': 'Identitätsprüfung',
|
||
'processing': 'In Bearbeitung',
|
||
'completed': 'Abgeschlossen',
|
||
'rejected': 'Abgelehnt',
|
||
'cancelled': 'Storniert'
|
||
};
|
||
|
||
const DSR_STATUS_COLORS = {
|
||
'intake': '#6366f1',
|
||
'identity_verification': '#f59e0b',
|
||
'processing': '#3b82f6',
|
||
'completed': '#22c55e',
|
||
'rejected': '#ef4444',
|
||
'cancelled': '#6b7280'
|
||
};
|
||
|
||
async function loadDSRStats() {
|
||
const container = document.getElementById('dsr-stats-cards');
|
||
try {
|
||
const res = await fetch('/api/v1/admin/dsr/stats');
|
||
if (!res.ok) throw new Error('Failed to load stats');
|
||
const stats = await res.json();
|
||
|
||
container.innerHTML = `
|
||
<div class="dsms-status-card" style="border-left: 3px solid #ef4444;">
|
||
<h4>Überfällig</h4>
|
||
<div class="value" style="color: #ef4444; font-size: 24px;">${stats.overdue_requests || 0}</div>
|
||
</div>
|
||
<div class="dsms-status-card" style="border-left: 3px solid #3b82f6;">
|
||
<h4>In Bearbeitung</h4>
|
||
<div class="value" style="color: #3b82f6; font-size: 24px;">${stats.pending_requests || 0}</div>
|
||
</div>
|
||
<div class="dsms-status-card" style="border-left: 3px solid #22c55e;">
|
||
<h4>Diesen Monat abgeschlossen</h4>
|
||
<div class="value" style="color: #22c55e; font-size: 24px;">${stats.completed_this_month || 0}</div>
|
||
</div>
|
||
<div class="dsms-status-card" style="border-left: 3px solid var(--bp-primary);">
|
||
<h4>Gesamt</h4>
|
||
<div class="value" style="font-size: 24px;">${stats.total_requests || 0}</div>
|
||
</div>
|
||
<div class="dsms-status-card" style="border-left: 3px solid var(--bp-text-muted);">
|
||
<h4>Ø Bearbeitungszeit</h4>
|
||
<div class="value" style="font-size: 18px;">${(stats.average_processing_days || 0).toFixed(1)} Tage</div>
|
||
</div>
|
||
`;
|
||
} catch(e) {
|
||
container.innerHTML = `<div class="admin-empty">Fehler beim Laden der Statistiken: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
async function loadDSRList() {
|
||
const container = document.getElementById('dsr-table-container');
|
||
const status = document.getElementById('dsr-filter-status').value;
|
||
const requestType = document.getElementById('dsr-filter-type').value;
|
||
const overdueOnly = document.getElementById('dsr-filter-overdue').checked;
|
||
|
||
container.innerHTML = '<div class="admin-loading">Lade Betroffenenanfragen...</div>';
|
||
|
||
try {
|
||
let url = '/api/v1/admin/dsr?limit=50';
|
||
if (status) url += `&status=${status}`;
|
||
if (requestType) url += `&request_type=${requestType}`;
|
||
if (overdueOnly) url += `&overdue_only=true`;
|
||
|
||
const res = await fetch(url);
|
||
if (!res.ok) throw new Error('Failed to load DSRs');
|
||
const data = await res.json();
|
||
dsrList = data.requests || [];
|
||
|
||
if (dsrList.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="admin-empty">
|
||
<p style="font-size: 48px;">📋</p>
|
||
<p>Keine Betroffenenanfragen gefunden.</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Nr.</th>
|
||
<th>Typ</th>
|
||
<th>Antragsteller</th>
|
||
<th>Status</th>
|
||
<th>Priorität</th>
|
||
<th>Frist</th>
|
||
<th>Erstellt</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${dsrList.map(dsr => {
|
||
const isOverdue = new Date(dsr.deadline_at) < new Date() && !['completed', 'rejected', 'cancelled'].includes(dsr.status);
|
||
const deadlineDate = new Date(dsr.deadline_at).toLocaleDateString('de-DE');
|
||
return `
|
||
<tr style="${isOverdue ? 'background: rgba(239, 68, 68, 0.1);' : ''}">
|
||
<td><strong>${dsr.request_number}</strong></td>
|
||
<td>${DSR_TYPE_LABELS[dsr.request_type] || dsr.request_type}</td>
|
||
<td>
|
||
<div>${dsr.requester_email}</div>
|
||
${dsr.requester_name ? `<div style="font-size: 11px; color: var(--bp-text-muted);">${dsr.requester_name}</div>` : ''}
|
||
</td>
|
||
<td>
|
||
<span style="background: ${DSR_STATUS_COLORS[dsr.status]}20; color: ${DSR_STATUS_COLORS[dsr.status]}; padding: 2px 8px; border-radius: 12px; font-size: 11px;">
|
||
${DSR_STATUS_LABELS[dsr.status] || dsr.status}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<span style="color: ${dsr.priority === 'high' ? '#f59e0b' : dsr.priority === 'expedited' ? '#ef4444' : 'var(--bp-text-muted)'};">
|
||
${dsr.priority === 'expedited' ? '🔴' : dsr.priority === 'high' ? '🟡' : ''}
|
||
${dsr.priority === 'expedited' ? 'Beschleunigt' : dsr.priority === 'high' ? 'Hoch' : 'Normal'}
|
||
</span>
|
||
</td>
|
||
<td style="${isOverdue ? 'color: #ef4444; font-weight: 600;' : ''}">${deadlineDate}${isOverdue ? ' ⚠️' : ''}</td>
|
||
<td>${new Date(dsr.created_at).toLocaleDateString('de-DE')}</td>
|
||
<td>
|
||
<button class="btn btn-ghost btn-sm" onclick="showDSRDetail('${dsr.id}')">Details</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>
|
||
<div style="margin-top: 12px; font-size: 12px; color: var(--bp-text-muted);">
|
||
${data.total || dsrList.length} Anfragen gefunden
|
||
</div>
|
||
`;
|
||
} catch(e) {
|
||
container.innerHTML = `<div class="admin-empty">Fehler: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function showDSRCreateForm() {
|
||
document.getElementById('dsr-create-form').style.display = 'block';
|
||
document.getElementById('dsr-create-type').value = '';
|
||
document.getElementById('dsr-create-priority').value = 'normal';
|
||
document.getElementById('dsr-create-email').value = '';
|
||
document.getElementById('dsr-create-name').value = '';
|
||
document.getElementById('dsr-create-phone').value = '';
|
||
}
|
||
|
||
function hideDSRCreateForm() {
|
||
document.getElementById('dsr-create-form').style.display = 'none';
|
||
}
|
||
|
||
async function createDSR() {
|
||
const type = document.getElementById('dsr-create-type').value;
|
||
const priority = document.getElementById('dsr-create-priority').value;
|
||
const email = document.getElementById('dsr-create-email').value;
|
||
const name = document.getElementById('dsr-create-name').value;
|
||
const phone = document.getElementById('dsr-create-phone').value;
|
||
|
||
if (!type || !email) {
|
||
alert('Bitte füllen Sie alle Pflichtfelder aus.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch('/api/v1/admin/dsr', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
request_type: type,
|
||
priority: priority,
|
||
requester_email: email,
|
||
requester_name: name || undefined,
|
||
requester_phone: phone || undefined,
|
||
source: 'admin_panel'
|
||
})
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail?.error || err.detail || 'Fehler beim Erstellen');
|
||
}
|
||
|
||
const data = await res.json();
|
||
alert(`Anfrage ${data.request_number} wurde erstellt.`);
|
||
hideDSRCreateForm();
|
||
loadDSRStats();
|
||
loadDSRList();
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function showDSRDetail(dsrId) {
|
||
try {
|
||
const res = await fetch(`/api/v1/admin/dsr/${dsrId}`);
|
||
if (!res.ok) throw new Error('Failed to load DSR');
|
||
currentDSR = await res.json();
|
||
|
||
// Load history
|
||
const historyRes = await fetch(`/api/v1/admin/dsr/${dsrId}/history`);
|
||
const historyData = historyRes.ok ? await historyRes.json() : { history: [] };
|
||
|
||
document.getElementById('dsr-table-container').style.display = 'none';
|
||
document.getElementById('dsr-create-form').style.display = 'none';
|
||
document.getElementById('dsr-detail-view').style.display = 'block';
|
||
|
||
const isOverdue = new Date(currentDSR.deadline_at) < new Date() && !['completed', 'rejected', 'cancelled'].includes(currentDSR.status);
|
||
|
||
document.getElementById('dsr-detail-content').innerHTML = `
|
||
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 24px;">
|
||
<div>
|
||
<div style="background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 8px; padding: 20px; margin-bottom: 16px;">
|
||
<h3 style="margin: 0 0 16px 0; display: flex; align-items: center; gap: 12px;">
|
||
${currentDSR.request_number}
|
||
<span style="background: ${DSR_STATUS_COLORS[currentDSR.status]}20; color: ${DSR_STATUS_COLORS[currentDSR.status]}; padding: 4px 12px; border-radius: 12px; font-size: 12px;">
|
||
${DSR_STATUS_LABELS[currentDSR.status] || currentDSR.status}
|
||
</span>
|
||
</h3>
|
||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
|
||
<div>
|
||
<div style="font-size: 12px; color: var(--bp-text-muted);">Anfragetyp</div>
|
||
<div style="font-weight: 500;">${DSR_TYPE_LABELS[currentDSR.request_type] || currentDSR.request_type}</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size: 12px; color: var(--bp-text-muted);">Priorität</div>
|
||
<div style="font-weight: 500;">${currentDSR.priority === 'expedited' ? '🔴 Beschleunigt' : currentDSR.priority === 'high' ? '🟡 Hoch' : 'Normal'}</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size: 12px; color: var(--bp-text-muted);">Frist</div>
|
||
<div style="font-weight: 500; ${isOverdue ? 'color: #ef4444;' : ''}">${new Date(currentDSR.deadline_at).toLocaleDateString('de-DE')} ${isOverdue ? '⚠️ ÜBERFÄLLIG' : ''}</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size: 12px; color: var(--bp-text-muted);">Gesetzliche Frist</div>
|
||
<div style="font-weight: 500;">${currentDSR.legal_deadline_days} Tage</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size: 12px; color: var(--bp-text-muted);">Identität verifiziert</div>
|
||
<div style="font-weight: 500;">${currentDSR.identity_verified ? '✅ Ja' : '❌ Nein'}</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size: 12px; color: var(--bp-text-muted);">Quelle</div>
|
||
<div style="font-weight: 500;">${currentDSR.source === 'api' ? 'API' : currentDSR.source === 'admin_panel' ? 'Admin Panel' : currentDSR.source}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 8px; padding: 20px; margin-bottom: 16px;">
|
||
<h4 style="margin: 0 0 12px 0;">Antragsteller</h4>
|
||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
|
||
<div>
|
||
<div style="font-size: 12px; color: var(--bp-text-muted);">E-Mail</div>
|
||
<div style="font-weight: 500;">${currentDSR.requester_email}</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size: 12px; color: var(--bp-text-muted);">Name</div>
|
||
<div style="font-weight: 500;">${currentDSR.requester_name || '-'}</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size: 12px; color: var(--bp-text-muted);">Telefon</div>
|
||
<div style="font-weight: 500;">${currentDSR.requester_phone || '-'}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
${currentDSR.processing_notes ? `
|
||
<div style="background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 8px; padding: 20px; margin-bottom: 16px;">
|
||
<h4 style="margin: 0 0 12px 0;">Bearbeitungsnotizen</h4>
|
||
<div style="white-space: pre-wrap;">${currentDSR.processing_notes}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
${currentDSR.result_summary ? `
|
||
<div style="background: rgba(34, 197, 94, 0.1); border: 1px solid #22c55e; border-radius: 8px; padding: 20px; margin-bottom: 16px;">
|
||
<h4 style="margin: 0 0 12px 0; color: #22c55e;">Ergebnis</h4>
|
||
<div>${currentDSR.result_summary}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
${currentDSR.rejection_reason ? `
|
||
<div style="background: rgba(239, 68, 68, 0.1); border: 1px solid #ef4444; border-radius: 8px; padding: 20px; margin-bottom: 16px;">
|
||
<h4 style="margin: 0 0 12px 0; color: #ef4444;">Ablehnung</h4>
|
||
<div><strong>Rechtsgrundlage:</strong> ${currentDSR.rejection_legal_basis}</div>
|
||
<div style="margin-top: 8px;">${currentDSR.rejection_reason}</div>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
|
||
<div>
|
||
<div style="background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 8px; padding: 20px;">
|
||
<h4 style="margin: 0 0 12px 0;">Verlauf</h4>
|
||
<div style="max-height: 400px; overflow-y: auto;">
|
||
${(historyData.history || []).map(h => `
|
||
<div style="padding: 8px 0; border-bottom: 1px solid var(--bp-border);">
|
||
<div style="font-size: 11px; color: var(--bp-text-muted);">${new Date(h.created_at).toLocaleString('de-DE')}</div>
|
||
<div style="font-size: 13px;">
|
||
${h.from_status ? `${DSR_STATUS_LABELS[h.from_status] || h.from_status} → ` : ''}
|
||
<strong>${DSR_STATUS_LABELS[h.to_status] || h.to_status}</strong>
|
||
</div>
|
||
${h.comment ? `<div style="font-size: 12px; color: var(--bp-text-muted); margin-top: 4px;">${h.comment}</div>` : ''}
|
||
</div>
|
||
`).join('') || '<div style="color: var(--bp-text-muted);">Kein Verlauf vorhanden</div>'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Update button visibility based on status
|
||
const canVerify = !currentDSR.identity_verified && ['intake', 'identity_verification'].includes(currentDSR.status);
|
||
const canComplete = ['processing'].includes(currentDSR.status);
|
||
const canReject = ['intake', 'identity_verification', 'processing'].includes(currentDSR.status);
|
||
const canExtend = !['completed', 'rejected', 'cancelled'].includes(currentDSR.status);
|
||
|
||
document.getElementById('dsr-btn-verify').style.display = canVerify ? 'inline-flex' : 'none';
|
||
document.getElementById('dsr-btn-complete').style.display = canComplete ? 'inline-flex' : 'none';
|
||
document.getElementById('dsr-btn-reject').style.display = canReject ? 'inline-flex' : 'none';
|
||
document.getElementById('dsr-btn-extend').style.display = canExtend ? 'inline-flex' : 'none';
|
||
|
||
} catch(e) {
|
||
alert('Fehler beim Laden: ' + e.message);
|
||
}
|
||
}
|
||
|
||
function hideDSRDetail() {
|
||
document.getElementById('dsr-detail-view').style.display = 'none';
|
||
document.getElementById('dsr-table-container').style.display = 'block';
|
||
currentDSR = null;
|
||
}
|
||
|
||
async function verifyDSRIdentity() {
|
||
if (!currentDSR) return;
|
||
const method = prompt('Verifizierungsmethode (z.B. id_card, passport, video_call, email):', 'email');
|
||
if (!method) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/v1/admin/dsr/${currentDSR.id}/verify-identity`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ method: method })
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail?.error || 'Fehler');
|
||
}
|
||
|
||
alert('Identität wurde verifiziert.');
|
||
showDSRDetail(currentDSR.id);
|
||
loadDSRStats();
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function showDSRExtendDialog() {
|
||
if (!currentDSR) return;
|
||
const reason = prompt('Begründung für die Fristverlängerung:');
|
||
if (!reason) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/v1/admin/dsr/${currentDSR.id}/extend`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ reason: reason, days: 60 })
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail?.error || 'Fehler');
|
||
}
|
||
|
||
alert('Frist wurde um 60 Tage verlängert.');
|
||
showDSRDetail(currentDSR.id);
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function showDSRCompleteDialog() {
|
||
if (!currentDSR) return;
|
||
const summary = prompt('Zusammenfassung des Ergebnisses:');
|
||
if (!summary) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/v1/admin/dsr/${currentDSR.id}/complete`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ summary: summary })
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail?.error || 'Fehler');
|
||
}
|
||
|
||
alert('Anfrage wurde abgeschlossen.');
|
||
showDSRDetail(currentDSR.id);
|
||
loadDSRStats();
|
||
loadDSRList();
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function showDSRRejectDialog() {
|
||
if (!currentDSR) return;
|
||
const legalBasis = prompt('Rechtsgrundlage für die Ablehnung (z.B. Art. 17(3)a, Art. 12(5)):');
|
||
if (!legalBasis) return;
|
||
const reason = prompt('Begründung der Ablehnung:');
|
||
if (!reason) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/v1/admin/dsr/${currentDSR.id}/reject`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ reason: reason, legal_basis: legalBasis })
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail?.error || 'Fehler');
|
||
}
|
||
|
||
alert('Anfrage wurde abgelehnt.');
|
||
showDSRDetail(currentDSR.id);
|
||
loadDSRStats();
|
||
loadDSRList();
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
function showDSRAssignDialog() {
|
||
// TODO: Implement user selection dialog
|
||
alert('Zuweisung noch nicht implementiert. Verwenden Sie die API direkt.');
|
||
}
|
||
|
||
function loadDSRData() {
|
||
loadDSRStats();
|
||
loadDSRList();
|
||
}
|
||
|
||
// Load DSR data when tab is clicked
|
||
document.querySelector('.admin-tab[data-tab="dsr"]')?.addEventListener('click', loadDSRData);
|
||
|
||
// ==========================================
|
||
// DSMS FUNCTIONS
|
||
// ==========================================
|
||
const DSMS_GATEWAY_URL = 'http://localhost:8082';
|
||
let dsmsArchives = [];
|
||
|
||
function switchDsmsTab(tabName) {
|
||
document.querySelectorAll('.dsms-subtab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.dsms-content').forEach(c => c.classList.remove('active'));
|
||
|
||
document.querySelector(`.dsms-subtab[data-dsms-tab="${tabName}"]`)?.classList.add('active');
|
||
document.getElementById(`dsms-${tabName}`)?.classList.add('active');
|
||
|
||
// Load data for specific tabs
|
||
if (tabName === 'settings') {
|
||
loadDsmsNodeInfo();
|
||
}
|
||
}
|
||
|
||
async function loadDsmsData() {
|
||
await Promise.all([
|
||
loadDsmsStatus(),
|
||
loadDsmsArchives(),
|
||
loadDsmsDocumentSelect()
|
||
]);
|
||
}
|
||
|
||
async function loadDsmsStatus() {
|
||
const container = document.getElementById('dsms-status-cards');
|
||
container.innerHTML = '<div class="admin-loading">Lade DSMS Status...</div>';
|
||
|
||
try {
|
||
const [healthRes, nodeRes] = await Promise.all([
|
||
fetch(`${DSMS_GATEWAY_URL}/health`).catch(() => null),
|
||
fetch(`${DSMS_GATEWAY_URL}/api/v1/node/info`).catch(() => null)
|
||
]);
|
||
|
||
const health = healthRes?.ok ? await healthRes.json() : null;
|
||
const nodeInfo = nodeRes?.ok ? await nodeRes.json() : null;
|
||
|
||
const isOnline = health?.ipfs_connected === true;
|
||
const repoSize = nodeInfo?.repo_size ? formatBytes(nodeInfo.repo_size) : '-';
|
||
const storageMax = nodeInfo?.storage_max ? formatBytes(nodeInfo.storage_max) : '-';
|
||
const numObjects = nodeInfo?.num_objects ?? '-';
|
||
|
||
container.innerHTML = `
|
||
<div class="dsms-status-card">
|
||
<h4>Status</h4>
|
||
<div class="value ${isOnline ? 'online' : 'offline'}">${isOnline ? 'Online' : 'Offline'}</div>
|
||
</div>
|
||
<div class="dsms-status-card">
|
||
<h4>Speicher verwendet</h4>
|
||
<div class="value">${repoSize}</div>
|
||
</div>
|
||
<div class="dsms-status-card">
|
||
<h4>Max. Speicher</h4>
|
||
<div class="value">${storageMax}</div>
|
||
</div>
|
||
<div class="dsms-status-card">
|
||
<h4>Objekte</h4>
|
||
<div class="value">${numObjects}</div>
|
||
</div>
|
||
`;
|
||
} catch(e) {
|
||
container.innerHTML = `
|
||
<div class="dsms-status-card" style="grid-column: 1/-1;">
|
||
<h4>Status</h4>
|
||
<div class="value offline">Nicht erreichbar</div>
|
||
<p style="font-size: 12px; color: var(--bp-text-muted); margin-top: 8px;">
|
||
DSMS Gateway ist nicht verfügbar. Stellen Sie sicher, dass die Container laufen.
|
||
</p>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
async function loadDsmsArchives() {
|
||
const container = document.getElementById('dsms-archives-table');
|
||
container.innerHTML = '<div class="admin-loading">Lade archivierte Dokumente...</div>';
|
||
|
||
try {
|
||
const token = localStorage.getItem('bp_token') || '';
|
||
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/documents`, {
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
if (!res.ok) {
|
||
throw new Error('Fehler beim Laden');
|
||
}
|
||
|
||
const data = await res.json();
|
||
dsmsArchives = data.documents || [];
|
||
|
||
if (dsmsArchives.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="admin-empty">
|
||
<p>Keine archivierten Dokumente vorhanden.</p>
|
||
<p style="font-size: 12px; color: var(--bp-text-muted);">
|
||
Klicken Sie auf "+ Dokument archivieren" um ein Legal Document im DSMS zu speichern.
|
||
</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>CID</th>
|
||
<th>Dokument</th>
|
||
<th>Version</th>
|
||
<th>Archiviert am</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${dsmsArchives.map(doc => `
|
||
<tr>
|
||
<td>
|
||
<code style="font-size: 11px; background: var(--bp-surface-elevated); padding: 2px 6px; border-radius: 4px;">
|
||
${doc.cid.substring(0, 12)}...
|
||
</code>
|
||
</td>
|
||
<td>${doc.metadata?.document_id || doc.filename || '-'}</td>
|
||
<td>${doc.metadata?.version || '-'}</td>
|
||
<td>${doc.metadata?.created_at ? new Date(doc.metadata.created_at).toLocaleString('de-DE') : '-'}</td>
|
||
<td>
|
||
<button class="btn btn-ghost btn-sm" onclick="verifyDsmsDocumentByCid('${doc.cid}')" title="Verifizieren">
|
||
✓
|
||
</button>
|
||
<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${doc.cid}')" title="CID kopieren">
|
||
📋
|
||
</button>
|
||
<a href="${DSMS_GATEWAY_URL.replace('8082', '8085')}/ipfs/${doc.cid}" target="_blank" class="btn btn-ghost btn-sm" title="Im Gateway öffnen">
|
||
↗
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
} catch(e) {
|
||
container.innerHTML = `<div class="admin-empty">Fehler: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
async function loadDsmsDocumentSelect() {
|
||
const select = document.getElementById('dsms-archive-doc-select');
|
||
if (!select) return;
|
||
|
||
try {
|
||
const res = await fetch('/api/consent/admin/documents');
|
||
if (!res.ok) return;
|
||
|
||
const data = await res.json();
|
||
const docs = data.documents || [];
|
||
|
||
select.innerHTML = '<option value="">-- Dokument wählen --</option>' +
|
||
docs.map(d => `<option value="${d.id}">${d.name} (${d.type})</option>`).join('');
|
||
} catch(e) {
|
||
console.error('Error loading documents:', e);
|
||
}
|
||
}
|
||
|
||
async function loadDsmsVersionSelect() {
|
||
const docSelect = document.getElementById('dsms-archive-doc-select');
|
||
const versionSelect = document.getElementById('dsms-archive-version-select');
|
||
const docId = docSelect?.value;
|
||
|
||
if (!docId) {
|
||
versionSelect.innerHTML = '<option value="">-- Erst Dokument wählen --</option>';
|
||
versionSelect.disabled = true;
|
||
return;
|
||
}
|
||
|
||
versionSelect.disabled = false;
|
||
versionSelect.innerHTML = '<option value="">Lade Versionen...</option>';
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
|
||
if (!res.ok) throw new Error('Fehler');
|
||
|
||
const data = await res.json();
|
||
const versions = data.versions || [];
|
||
|
||
if (versions.length === 0) {
|
||
versionSelect.innerHTML = '<option value="">Keine Versionen vorhanden</option>';
|
||
return;
|
||
}
|
||
|
||
versionSelect.innerHTML = '<option value="">-- Version wählen --</option>' +
|
||
versions.map(v => `<option value="${v.id}" data-content="${encodeURIComponent(v.content || '')}" data-version="${v.version}">
|
||
${v.version} (${v.language}) - ${v.status}
|
||
</option>`).join('');
|
||
} catch(e) {
|
||
versionSelect.innerHTML = '<option value="">Fehler beim Laden</option>';
|
||
}
|
||
}
|
||
|
||
// Attach event listener for doc select change
|
||
document.getElementById('dsms-archive-doc-select')?.addEventListener('change', loadDsmsVersionSelect);
|
||
|
||
function showArchiveForm() {
|
||
document.getElementById('dsms-archive-form').style.display = 'block';
|
||
loadDsmsDocumentSelect();
|
||
}
|
||
|
||
function hideArchiveForm() {
|
||
document.getElementById('dsms-archive-form').style.display = 'none';
|
||
}
|
||
|
||
async function archiveDocumentToDsms() {
|
||
const docSelect = document.getElementById('dsms-archive-doc-select');
|
||
const versionSelect = document.getElementById('dsms-archive-version-select');
|
||
const selectedOption = versionSelect.options[versionSelect.selectedIndex];
|
||
|
||
if (!docSelect.value || !versionSelect.value) {
|
||
alert('Bitte Dokument und Version auswählen');
|
||
return;
|
||
}
|
||
|
||
const content = decodeURIComponent(selectedOption.dataset.content || '');
|
||
const version = selectedOption.dataset.version;
|
||
const docId = docSelect.value;
|
||
|
||
if (!content) {
|
||
alert('Die ausgewählte Version hat keinen Inhalt');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const token = localStorage.getItem('bp_token') || '';
|
||
const params = new URLSearchParams({
|
||
document_id: docId,
|
||
version: version,
|
||
content: content,
|
||
language: 'de'
|
||
});
|
||
|
||
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/legal-documents/archive?${params}`, {
|
||
method: 'POST',
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail || 'Archivierung fehlgeschlagen');
|
||
}
|
||
|
||
const result = await res.json();
|
||
alert(`Dokument erfolgreich archiviert!\\n\\nCID: ${result.cid}\\nChecksum: ${result.checksum}`);
|
||
hideArchiveForm();
|
||
loadDsmsArchives();
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function verifyDsmsDocument() {
|
||
const cidInput = document.getElementById('dsms-verify-cid');
|
||
const resultDiv = document.getElementById('dsms-verify-result');
|
||
const cid = cidInput?.value?.trim();
|
||
|
||
if (!cid) {
|
||
alert('Bitte CID eingeben');
|
||
return;
|
||
}
|
||
|
||
await verifyDsmsDocumentByCid(cid);
|
||
}
|
||
|
||
async function verifyDsmsDocumentByCid(cid) {
|
||
const resultDiv = document.getElementById('dsms-verify-result');
|
||
resultDiv.style.display = 'block';
|
||
resultDiv.innerHTML = '<div class="admin-loading">Verifiziere...</div>';
|
||
|
||
// Switch to verify tab
|
||
switchDsmsTab('verify');
|
||
document.getElementById('dsms-verify-cid').value = cid;
|
||
|
||
try {
|
||
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/verify/${cid}`);
|
||
const data = await res.json();
|
||
|
||
if (data.exists && data.integrity_valid) {
|
||
resultDiv.innerHTML = `
|
||
<div class="dsms-verify-success">
|
||
<h4 style="margin: 0 0 12px 0;">✓ Dokument verifiziert</h4>
|
||
<div style="display: grid; gap: 8px; font-size: 13px;">
|
||
<div><strong>CID:</strong> <code>${cid}</code></div>
|
||
<div><strong>Integrität:</strong> Gültig</div>
|
||
<div><strong>Typ:</strong> ${data.metadata?.document_type || '-'}</div>
|
||
<div><strong>Dokument-ID:</strong> ${data.metadata?.document_id || '-'}</div>
|
||
<div><strong>Version:</strong> ${data.metadata?.version || '-'}</div>
|
||
<div><strong>Erstellt:</strong> ${data.metadata?.created_at ? new Date(data.metadata.created_at).toLocaleString('de-DE') : '-'}</div>
|
||
<div><strong>Checksum:</strong> <code style="font-size: 10px;">${data.stored_checksum || '-'}</code></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else if (data.exists && !data.integrity_valid) {
|
||
resultDiv.innerHTML = `
|
||
<div class="dsms-verify-error">
|
||
<h4 style="margin: 0 0 12px 0;">⚠ Integritätsfehler</h4>
|
||
<p>Das Dokument existiert, aber die Prüfsumme stimmt nicht überein.</p>
|
||
<p style="font-size: 12px;">Gespeichert: ${data.stored_checksum}</p>
|
||
<p style="font-size: 12px;">Berechnet: ${data.calculated_checksum}</p>
|
||
</div>
|
||
`;
|
||
} else {
|
||
resultDiv.innerHTML = `
|
||
<div class="dsms-verify-error">
|
||
<h4 style="margin: 0 0 12px 0;">✗ Nicht gefunden</h4>
|
||
<p>Kein Dokument mit diesem CID gefunden.</p>
|
||
${data.error ? `<p style="font-size: 12px;">${data.error}</p>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
} catch(e) {
|
||
resultDiv.innerHTML = `
|
||
<div class="dsms-verify-error">
|
||
<h4 style="margin: 0 0 12px 0;">Fehler</h4>
|
||
<p>${e.message}</p>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
async function loadDsmsNodeInfo() {
|
||
const container = document.getElementById('dsms-node-info');
|
||
container.innerHTML = '<div class="admin-loading">Lade Node-Info...</div>';
|
||
|
||
try {
|
||
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/node/info`);
|
||
if (!res.ok) throw new Error('Nicht erreichbar');
|
||
|
||
const info = await res.json();
|
||
|
||
container.innerHTML = `
|
||
<div style="display: grid; gap: 12px; font-size: 13px;">
|
||
<div><strong>Node ID:</strong> <code style="font-size: 11px;">${info.node_id || '-'}</code></div>
|
||
<div><strong>Agent:</strong> ${info.agent_version || '-'}</div>
|
||
<div><strong>Repo-Größe:</strong> ${info.repo_size ? formatBytes(info.repo_size) : '-'}</div>
|
||
<div><strong>Max. Speicher:</strong> ${info.storage_max ? formatBytes(info.storage_max) : '-'}</div>
|
||
<div><strong>Objekte:</strong> ${info.num_objects ?? '-'}</div>
|
||
<div>
|
||
<strong>Adressen:</strong>
|
||
<ul style="margin: 4px 0 0 0; padding-left: 20px; font-size: 11px; color: var(--bp-text-muted);">
|
||
${(info.addresses || []).map(a => `<li><code>${a}</code></li>`).join('')}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} catch(e) {
|
||
container.innerHTML = `<div class="admin-empty">DSMS nicht erreichbar: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function formatBytes(bytes) {
|
||
if (bytes === 0) return '0 B';
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
}
|
||
|
||
function copyToClipboard(text) {
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
// Optional: Show toast
|
||
}).catch(err => {
|
||
console.error('Copy failed:', err);
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// DSMS WEBUI FUNCTIONS
|
||
// ==========================================
|
||
function openDsmsWebUI() {
|
||
document.getElementById('dsms-webui-modal').style.display = 'flex';
|
||
loadDsmsWebUIData();
|
||
}
|
||
|
||
function closeDsmsWebUI() {
|
||
document.getElementById('dsms-webui-modal').style.display = 'none';
|
||
}
|
||
|
||
function switchDsmsWebUISection(section) {
|
||
// Update nav buttons
|
||
document.querySelectorAll('.dsms-webui-nav').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.section === section);
|
||
});
|
||
// Update sections
|
||
document.querySelectorAll('.dsms-webui-section').forEach(sec => {
|
||
sec.classList.remove('active');
|
||
sec.style.display = 'none';
|
||
});
|
||
const activeSection = document.getElementById('dsms-webui-' + section);
|
||
if (activeSection) {
|
||
activeSection.classList.add('active');
|
||
activeSection.style.display = 'block';
|
||
}
|
||
// Load section-specific data
|
||
if (section === 'peers') loadDsmsPeers();
|
||
}
|
||
|
||
async function loadDsmsWebUIData() {
|
||
try {
|
||
// Load node info
|
||
const infoRes = await fetch(DSMS_GATEWAY_URL + '/api/v1/node/info');
|
||
const info = await infoRes.json();
|
||
|
||
document.getElementById('webui-status').innerHTML = '<span style="color: var(--bp-accent);">Online</span>';
|
||
document.getElementById('webui-node-id').textContent = info.node_id || '--';
|
||
document.getElementById('webui-protocol').textContent = info.protocol_version || '--';
|
||
document.getElementById('webui-agent').textContent = info.agent_version || '--';
|
||
document.getElementById('webui-repo-size').textContent = formatBytes(info.repo_size || 0);
|
||
document.getElementById('webui-storage-info').textContent = 'Max: ' + formatBytes(info.storage_max || 0);
|
||
document.getElementById('webui-num-objects').textContent = (info.num_objects || 0).toLocaleString();
|
||
|
||
// Addresses
|
||
const addresses = info.addresses || [];
|
||
document.getElementById('webui-addresses').innerHTML = addresses.length > 0
|
||
? addresses.map(a => '<div style="margin-bottom: 4px;">' + a + '</div>').join('')
|
||
: '<span style="color: var(--bp-text-muted);">Keine Adressen verfügbar</span>';
|
||
|
||
// Load pinned count
|
||
const token = localStorage.getItem('bp_token') || '';
|
||
const docsRes = await fetch(DSMS_GATEWAY_URL + '/api/v1/documents', {
|
||
headers: { 'Authorization': 'Bearer ' + token }
|
||
});
|
||
if (docsRes.ok) {
|
||
const docs = await docsRes.json();
|
||
document.getElementById('webui-pinned-count').textContent = docs.total || 0;
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to load WebUI data:', e);
|
||
document.getElementById('webui-status').innerHTML = '<span style="color: var(--bp-danger);">Offline</span>';
|
||
}
|
||
}
|
||
|
||
async function loadDsmsPeers() {
|
||
const container = document.getElementById('webui-peers-list');
|
||
try {
|
||
// IPFS peers endpoint via proxy would need direct IPFS API access
|
||
// For now, show info that private network has no peers
|
||
container.innerHTML = `
|
||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; text-align: center;">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">🔒</div>
|
||
<h4 style="margin: 0 0 8px 0;">Privates Netzwerk</h4>
|
||
<p style="color: var(--bp-text-muted); margin: 0;">
|
||
DSMS läuft als isolierter Node. Keine externen Peers verbunden.
|
||
</p>
|
||
</div>
|
||
`;
|
||
} catch (e) {
|
||
container.innerHTML = '<div class="dsms-verify-error">Fehler beim Laden der Peers</div>';
|
||
}
|
||
}
|
||
|
||
// File upload handlers
|
||
function handleDsmsDragOver(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
document.getElementById('dsms-upload-zone').classList.add('dragover');
|
||
}
|
||
|
||
function handleDsmsDragLeave(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
document.getElementById('dsms-upload-zone').classList.remove('dragover');
|
||
}
|
||
|
||
async function handleDsmsFileDrop(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
document.getElementById('dsms-upload-zone').classList.remove('dragover');
|
||
const files = e.dataTransfer.files;
|
||
if (files.length > 0) {
|
||
await uploadDsmsFiles(files);
|
||
}
|
||
}
|
||
|
||
async function handleDsmsFileSelect(e) {
|
||
const files = e.target.files;
|
||
if (files.length > 0) {
|
||
await uploadDsmsFiles(files);
|
||
}
|
||
}
|
||
|
||
async function uploadDsmsFiles(files) {
|
||
const token = localStorage.getItem('bp_token') || '';
|
||
const progressDiv = document.getElementById('dsms-upload-progress');
|
||
const statusDiv = document.getElementById('dsms-upload-status');
|
||
const barDiv = document.getElementById('dsms-upload-bar');
|
||
const resultsDiv = document.getElementById('dsms-upload-results');
|
||
|
||
progressDiv.style.display = 'block';
|
||
resultsDiv.innerHTML = '';
|
||
|
||
const results = [];
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i];
|
||
statusDiv.textContent = 'Lade hoch: ' + file.name + ' (' + (i+1) + '/' + files.length + ')';
|
||
barDiv.style.width = ((i / files.length) * 100) + '%';
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('document_type', 'legal_document');
|
||
|
||
const res = await fetch(DSMS_GATEWAY_URL + '/api/v1/documents', {
|
||
method: 'POST',
|
||
headers: { 'Authorization': 'Bearer ' + token },
|
||
body: formData
|
||
});
|
||
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
results.push({ file: file.name, cid: data.cid, success: true });
|
||
} else {
|
||
results.push({ file: file.name, error: 'Upload fehlgeschlagen', success: false });
|
||
}
|
||
} catch (e) {
|
||
results.push({ file: file.name, error: e.message, success: false });
|
||
}
|
||
}
|
||
|
||
barDiv.style.width = '100%';
|
||
statusDiv.textContent = 'Upload abgeschlossen!';
|
||
|
||
// Show results
|
||
resultsDiv.innerHTML = '<h4 style="margin: 0 0 12px 0;">Ergebnisse</h4>' +
|
||
results.map(r => `
|
||
<div class="dsms-webui-file-item">
|
||
<div>
|
||
<div style="font-weight: 500;">${r.file}</div>
|
||
${r.success
|
||
? '<div class="cid" style="color: var(--bp-accent);">CID: ' + r.cid + '</div>'
|
||
: '<div style="color: var(--bp-danger);">' + r.error + '</div>'
|
||
}
|
||
</div>
|
||
${r.success ? `<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${r.cid}')">Kopieren</button>` : ''}
|
||
</div>
|
||
`).join('');
|
||
|
||
setTimeout(() => {
|
||
progressDiv.style.display = 'none';
|
||
barDiv.style.width = '0%';
|
||
}, 2000);
|
||
}
|
||
|
||
async function exploreDsmsCid() {
|
||
const cid = document.getElementById('webui-explore-cid').value.trim();
|
||
if (!cid) return;
|
||
|
||
const resultDiv = document.getElementById('dsms-explore-result');
|
||
const contentDiv = document.getElementById('dsms-explore-content');
|
||
|
||
resultDiv.style.display = 'block';
|
||
contentDiv.innerHTML = '<div class="admin-loading">Lade...</div>';
|
||
|
||
try {
|
||
const res = await fetch(DSMS_GATEWAY_URL + '/api/v1/verify/' + cid);
|
||
const data = await res.json();
|
||
|
||
if (data.exists) {
|
||
contentDiv.innerHTML = `
|
||
<div style="margin-bottom: 16px;">
|
||
<span style="font-size: 24px; ${data.integrity_valid ? 'color: var(--bp-accent);' : 'color: var(--bp-danger);'}">
|
||
${data.integrity_valid ? '✓' : '✗'}
|
||
</span>
|
||
<span style="font-weight: 600; margin-left: 8px;">
|
||
${data.integrity_valid ? 'Dokument verifiziert' : 'Integritätsfehler'}
|
||
</span>
|
||
</div>
|
||
<table style="width: 100%; font-size: 13px;">
|
||
<tr>
|
||
<td style="padding: 8px 0; color: var(--bp-text-muted); width: 150px;">CID</td>
|
||
<td style="padding: 8px 0; font-family: monospace; word-break: break-all;">${cid}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: var(--bp-text-muted);">Typ</td>
|
||
<td style="padding: 8px 0;">${data.metadata?.document_type || '--'}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: var(--bp-text-muted);">Erstellt</td>
|
||
<td style="padding: 8px 0;">${data.metadata?.created_at ? new Date(data.metadata.created_at).toLocaleString('de-DE') : '--'}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: var(--bp-text-muted);">Checksum</td>
|
||
<td style="padding: 8px 0; font-family: monospace; font-size: 11px; word-break: break-all;">${data.stored_checksum || '--'}</td>
|
||
</tr>
|
||
</table>
|
||
<div style="margin-top: 16px;">
|
||
<a href="http://localhost:8085/ipfs/${cid}" target="_blank" class="btn btn-ghost btn-sm">Im Gateway öffnen</a>
|
||
</div>
|
||
`;
|
||
} else {
|
||
contentDiv.innerHTML = `
|
||
<div class="dsms-verify-error">
|
||
<strong>Nicht gefunden</strong><br>
|
||
CID existiert nicht im DSMS: ${cid}
|
||
</div>
|
||
`;
|
||
}
|
||
} catch (e) {
|
||
contentDiv.innerHTML = `
|
||
<div class="dsms-verify-error">
|
||
<strong>Fehler</strong><br>
|
||
${e.message}
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
async function runDsmsGarbageCollection() {
|
||
if (!confirm('Garbage Collection ausführen? Dies entfernt nicht gepinnte Objekte.')) return;
|
||
|
||
try {
|
||
// Note: Direct GC requires IPFS API access - show info for now
|
||
alert('Garbage Collection wird im Hintergrund ausgeführt. Dies kann einige Minuten dauern.');
|
||
} catch (e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// Close modal on escape key
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') {
|
||
closeDsmsWebUI();
|
||
}
|
||
});
|
||
|
||
// Close modal on backdrop click
|
||
document.getElementById('dsms-webui-modal')?.addEventListener('click', (e) => {
|
||
if (e.target.id === 'dsms-webui-modal') {
|
||
closeDsmsWebUI();
|
||
}
|
||
});
|
||
|
||
// Load DSMS data when tab is clicked
|
||
document.querySelector('.admin-tab[data-tab="dsms"]')?.addEventListener('click', loadDsmsData);
|
||
|
||
// ==========================================
|
||
// E-MAIL TEMPLATE MANAGEMENT
|
||
// ==========================================
|
||
|
||
let emailTemplates = [];
|
||
let emailTemplateVersions = [];
|
||
let currentEmailTemplateId = null;
|
||
let currentEmailVersionId = null;
|
||
|
||
// E-Mail-Template-Typen mit deutschen Namen
|
||
const emailTypeNames = {
|
||
'welcome': 'Willkommens-E-Mail',
|
||
'email_verification': 'E-Mail-Verifizierung',
|
||
'password_reset': 'Passwort zurücksetzen',
|
||
'password_changed': 'Passwort geändert',
|
||
'2fa_enabled': '2FA aktiviert',
|
||
'2fa_disabled': '2FA deaktiviert',
|
||
'new_device_login': 'Neues Gerät Login',
|
||
'suspicious_activity': 'Verdächtige Aktivität',
|
||
'account_locked': 'Account gesperrt',
|
||
'account_unlocked': 'Account entsperrt',
|
||
'deletion_requested': 'Löschung angefordert',
|
||
'deletion_confirmed': 'Löschung bestätigt',
|
||
'data_export_ready': 'Datenexport bereit',
|
||
'email_changed': 'E-Mail geändert',
|
||
'new_version_published': 'Neue Version veröffentlicht',
|
||
'consent_reminder': 'Consent Erinnerung',
|
||
'consent_deadline_warning': 'Consent Frist Warnung',
|
||
'account_suspended': 'Account suspendiert'
|
||
};
|
||
|
||
// Load E-Mail Templates when tab is clicked
|
||
document.querySelector('.admin-tab[data-tab="emails"]')?.addEventListener('click', loadEmailTemplates);
|
||
|
||
async function loadEmailTemplates() {
|
||
try {
|
||
const res = await fetch('/api/consent/admin/email-templates');
|
||
if (!res.ok) throw new Error('Fehler beim Laden der Templates');
|
||
const data = await res.json();
|
||
emailTemplates = data.templates || [];
|
||
populateEmailTemplateSelect();
|
||
} catch (e) {
|
||
console.error('Error loading email templates:', e);
|
||
showToast('Fehler beim Laden der E-Mail-Templates', 'error');
|
||
}
|
||
}
|
||
|
||
function populateEmailTemplateSelect() {
|
||
const select = document.getElementById('email-template-select');
|
||
select.innerHTML = '<option value="">-- E-Mail-Vorlage auswählen --</option>';
|
||
|
||
emailTemplates.forEach(item => {
|
||
const template = item.template; // API liefert verschachtelte Struktur
|
||
const opt = document.createElement('option');
|
||
opt.value = template.id;
|
||
opt.textContent = emailTypeNames[template.type] || template.name;
|
||
select.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
async function loadEmailTemplateVersions() {
|
||
const select = document.getElementById('email-template-select');
|
||
const templateId = select.value;
|
||
const newVersionBtn = document.getElementById('btn-new-email-version');
|
||
const infoCard = document.getElementById('email-template-info');
|
||
const container = document.getElementById('email-version-table-container');
|
||
|
||
if (!templateId) {
|
||
newVersionBtn.disabled = true;
|
||
infoCard.style.display = 'none';
|
||
container.innerHTML = '<div class="admin-empty">Wählen Sie eine E-Mail-Vorlage aus, um deren Versionen anzuzeigen.</div>';
|
||
currentEmailTemplateId = null;
|
||
return;
|
||
}
|
||
|
||
currentEmailTemplateId = templateId;
|
||
newVersionBtn.disabled = false;
|
||
|
||
// Finde das Template (API liefert verschachtelte Struktur)
|
||
const templateItem = emailTemplates.find(t => t.template.id === templateId);
|
||
const template = templateItem?.template;
|
||
if (template) {
|
||
infoCard.style.display = 'block';
|
||
document.getElementById('email-template-name').textContent = emailTypeNames[template.type] || template.name;
|
||
document.getElementById('email-template-description').textContent = template.description || 'Keine Beschreibung';
|
||
document.getElementById('email-template-type-badge').textContent = template.type;
|
||
|
||
// Variablen anzeigen (wird aus dem Default-Inhalt ermittelt)
|
||
try {
|
||
const defaultRes = await fetch(`/api/consent/admin/email-templates/default/${template.type}`);
|
||
if (defaultRes.ok) {
|
||
const defaultData = await defaultRes.json();
|
||
const variables = extractVariables(defaultData.body_html || '');
|
||
document.getElementById('email-template-variables').textContent = variables.join(', ') || 'Keine';
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('email-template-variables').textContent = '-';
|
||
}
|
||
}
|
||
|
||
// Lade Versionen
|
||
container.innerHTML = '<div class="admin-loading">Lade Versionen...</div>';
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-templates/${templateId}/versions`);
|
||
if (!res.ok) throw new Error('Fehler beim Laden');
|
||
const data = await res.json();
|
||
emailTemplateVersions = data.versions || [];
|
||
renderEmailVersionsTable();
|
||
} catch (e) {
|
||
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Versionen.</div>';
|
||
}
|
||
}
|
||
|
||
function extractVariables(content) {
|
||
const matches = content.match(/\\{\\{([^}]+)\\}\\}/g) || [];
|
||
return [...new Set(matches.map(m => m.replace(/[{}]/g, '')))];
|
||
}
|
||
|
||
function renderEmailVersionsTable() {
|
||
const container = document.getElementById('email-version-table-container');
|
||
|
||
if (emailTemplateVersions.length === 0) {
|
||
container.innerHTML = '<div class="admin-empty">Keine Versionen vorhanden. Erstellen Sie eine neue Version.</div>';
|
||
return;
|
||
}
|
||
|
||
const statusColors = {
|
||
'draft': 'draft',
|
||
'review': 'review',
|
||
'approved': 'approved',
|
||
'published': 'published',
|
||
'archived': 'archived'
|
||
};
|
||
|
||
const statusNames = {
|
||
'draft': 'Entwurf',
|
||
'review': 'In Prüfung',
|
||
'approved': 'Genehmigt',
|
||
'published': 'Veröffentlicht',
|
||
'archived': 'Archiviert'
|
||
};
|
||
|
||
container.innerHTML = `
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Version</th>
|
||
<th>Sprache</th>
|
||
<th>Betreff</th>
|
||
<th>Status</th>
|
||
<th>Aktualisiert</th>
|
||
<th style="text-align: right;">Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${emailTemplateVersions.map(v => `
|
||
<tr>
|
||
<td><strong>${v.version}</strong></td>
|
||
<td>${v.language === 'de' ? '🇩🇪 DE' : '🇬🇧 EN'}</td>
|
||
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${v.subject}</td>
|
||
<td><span class="admin-badge badge-${statusColors[v.status]}">${statusNames[v.status] || v.status}</span></td>
|
||
<td>${new Date(v.updated_at).toLocaleDateString('de-DE')}</td>
|
||
<td style="text-align: right;">
|
||
<button class="btn btn-ghost btn-xs" onclick="previewEmailVersionById('${v.id}')" title="Vorschau">👁️</button>
|
||
${v.status === 'draft' ? `
|
||
<button class="btn btn-ghost btn-xs" onclick="editEmailVersion('${v.id}')" title="Bearbeiten">✏️</button>
|
||
<button class="btn btn-ghost btn-xs" onclick="submitEmailForReview('${v.id}')" title="Zur Prüfung">📤</button>
|
||
<button class="btn btn-ghost btn-xs" onclick="deleteEmailVersion('${v.id}')" title="Löschen">🗑️</button>
|
||
` : ''}
|
||
${v.status === 'review' ? `
|
||
<button class="btn btn-ghost btn-xs" onclick="showEmailApprovalDialogFor('${v.id}')" title="Genehmigen">✅</button>
|
||
<button class="btn btn-ghost btn-xs" onclick="rejectEmailVersion('${v.id}')" title="Ablehnen">❌</button>
|
||
` : ''}
|
||
${v.status === 'approved' ? `
|
||
<button class="btn btn-primary btn-xs" onclick="publishEmailVersion('${v.id}')" title="Veröffentlichen">🚀</button>
|
||
` : ''}
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
function showEmailVersionForm() {
|
||
document.getElementById('email-version-form').style.display = 'block';
|
||
document.getElementById('email-version-form-title').textContent = 'Neue E-Mail-Version erstellen';
|
||
document.getElementById('email-version-id').value = '';
|
||
document.getElementById('email-version-number').value = '';
|
||
document.getElementById('email-version-subject').value = '';
|
||
document.getElementById('email-version-editor').innerHTML = '';
|
||
document.getElementById('email-version-text').value = '';
|
||
|
||
// Lade Default-Inhalt (API liefert verschachtelte Struktur)
|
||
const templateItem = emailTemplates.find(t => t.template.id === currentEmailTemplateId);
|
||
if (templateItem?.template) {
|
||
loadDefaultEmailContent(templateItem.template.type);
|
||
}
|
||
}
|
||
|
||
async function loadDefaultEmailContent(templateType) {
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-templates/default/${templateType}`);
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
document.getElementById('email-version-subject').value = data.subject || '';
|
||
document.getElementById('email-version-editor').innerHTML = data.body_html || '';
|
||
document.getElementById('email-version-text').value = data.body_text || '';
|
||
}
|
||
} catch (e) {
|
||
console.error('Error loading default content:', e);
|
||
}
|
||
}
|
||
|
||
function hideEmailVersionForm() {
|
||
document.getElementById('email-version-form').style.display = 'none';
|
||
}
|
||
|
||
async function saveEmailVersion() {
|
||
const versionId = document.getElementById('email-version-id').value;
|
||
const templateId = currentEmailTemplateId;
|
||
const version = document.getElementById('email-version-number').value.trim();
|
||
const language = document.getElementById('email-version-lang').value;
|
||
const subject = document.getElementById('email-version-subject').value.trim();
|
||
const bodyHtml = document.getElementById('email-version-editor').innerHTML;
|
||
const bodyText = document.getElementById('email-version-text').value.trim();
|
||
|
||
if (!version || !subject || !bodyHtml) {
|
||
showToast('Bitte füllen Sie alle Pflichtfelder aus', 'error');
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
template_id: templateId,
|
||
version: version,
|
||
language: language,
|
||
subject: subject,
|
||
body_html: bodyHtml,
|
||
body_text: bodyText || stripHtml(bodyHtml)
|
||
};
|
||
|
||
try {
|
||
let res;
|
||
if (versionId) {
|
||
// Update existing version
|
||
res = await fetch(`/api/consent/admin/email-template-versions/${versionId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
} else {
|
||
// Create new version
|
||
res = await fetch('/api/consent/admin/email-template-versions', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
}
|
||
|
||
if (!res.ok) {
|
||
const error = await res.json();
|
||
throw new Error(error.detail || 'Fehler beim Speichern');
|
||
}
|
||
|
||
showToast('E-Mail-Version gespeichert!', 'success');
|
||
hideEmailVersionForm();
|
||
loadEmailTemplateVersions();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
function stripHtml(html) {
|
||
const div = document.createElement('div');
|
||
div.innerHTML = html;
|
||
return div.textContent || div.innerText || '';
|
||
}
|
||
|
||
async function editEmailVersion(versionId) {
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}`);
|
||
if (!res.ok) throw new Error('Version nicht gefunden');
|
||
const version = await res.json();
|
||
|
||
document.getElementById('email-version-form').style.display = 'block';
|
||
document.getElementById('email-version-form-title').textContent = 'E-Mail-Version bearbeiten';
|
||
document.getElementById('email-version-id').value = versionId;
|
||
document.getElementById('email-version-number').value = version.version;
|
||
document.getElementById('email-version-lang').value = version.language;
|
||
document.getElementById('email-version-subject').value = version.subject;
|
||
document.getElementById('email-version-editor').innerHTML = version.body_html;
|
||
document.getElementById('email-version-text').value = version.body_text || '';
|
||
} catch (e) {
|
||
showToast('Fehler beim Laden der Version', 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteEmailVersion(versionId) {
|
||
if (!confirm('Möchten Sie diese Version wirklich löschen?')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
if (!res.ok) throw new Error('Fehler beim Löschen');
|
||
showToast('Version gelöscht', 'success');
|
||
loadEmailTemplateVersions();
|
||
} catch (e) {
|
||
showToast('Fehler beim Löschen', 'error');
|
||
}
|
||
}
|
||
|
||
async function submitEmailForReview(versionId) {
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/submit`, {
|
||
method: 'POST'
|
||
});
|
||
if (!res.ok) throw new Error('Fehler');
|
||
showToast('Zur Prüfung eingereicht', 'success');
|
||
loadEmailTemplateVersions();
|
||
} catch (e) {
|
||
showToast('Fehler beim Einreichen', 'error');
|
||
}
|
||
}
|
||
|
||
function showEmailApprovalDialogFor(versionId) {
|
||
currentEmailVersionId = versionId;
|
||
document.getElementById('email-approval-dialog').style.display = 'flex';
|
||
document.getElementById('email-approval-comment').value = '';
|
||
}
|
||
|
||
function hideEmailApprovalDialog() {
|
||
document.getElementById('email-approval-dialog').style.display = 'none';
|
||
currentEmailVersionId = null;
|
||
}
|
||
|
||
async function submitEmailApproval() {
|
||
if (!currentEmailVersionId) return;
|
||
|
||
const comment = document.getElementById('email-approval-comment').value.trim();
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${currentEmailVersionId}/approve`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ comment: comment })
|
||
});
|
||
if (!res.ok) throw new Error('Fehler');
|
||
showToast('Version genehmigt', 'success');
|
||
hideEmailApprovalDialog();
|
||
loadEmailTemplateVersions();
|
||
} catch (e) {
|
||
showToast('Fehler bei der Genehmigung', 'error');
|
||
}
|
||
}
|
||
|
||
async function rejectEmailVersion(versionId) {
|
||
const reason = prompt('Ablehnungsgrund:');
|
||
if (!reason) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/reject`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ reason: reason })
|
||
});
|
||
if (!res.ok) throw new Error('Fehler');
|
||
showToast('Version abgelehnt', 'success');
|
||
loadEmailTemplateVersions();
|
||
} catch (e) {
|
||
showToast('Fehler bei der Ablehnung', 'error');
|
||
}
|
||
}
|
||
|
||
async function publishEmailVersion(versionId) {
|
||
if (!confirm('Möchten Sie diese Version veröffentlichen? Die vorherige Version wird archiviert.')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/publish`, {
|
||
method: 'POST'
|
||
});
|
||
if (!res.ok) throw new Error('Fehler');
|
||
showToast('Version veröffentlicht!', 'success');
|
||
loadEmailTemplateVersions();
|
||
} catch (e) {
|
||
showToast('Fehler beim Veröffentlichen', 'error');
|
||
}
|
||
}
|
||
|
||
async function previewEmailVersionById(versionId) {
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/preview`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({})
|
||
});
|
||
if (!res.ok) throw new Error('Fehler');
|
||
const data = await res.json();
|
||
|
||
document.getElementById('email-preview-subject').textContent = data.subject;
|
||
document.getElementById('email-preview-content').innerHTML = data.body_html;
|
||
document.getElementById('email-preview-dialog').style.display = 'flex';
|
||
currentEmailVersionId = versionId;
|
||
} catch (e) {
|
||
showToast('Fehler bei der Vorschau', 'error');
|
||
}
|
||
}
|
||
|
||
function previewEmailVersion() {
|
||
const subject = document.getElementById('email-version-subject').value;
|
||
const bodyHtml = document.getElementById('email-version-editor').innerHTML;
|
||
|
||
document.getElementById('email-preview-subject').textContent = subject;
|
||
document.getElementById('email-preview-content').innerHTML = bodyHtml;
|
||
document.getElementById('email-preview-dialog').style.display = 'flex';
|
||
}
|
||
|
||
function hideEmailPreview() {
|
||
document.getElementById('email-preview-dialog').style.display = 'none';
|
||
}
|
||
|
||
async function sendTestEmail() {
|
||
const email = document.getElementById('email-test-address').value.trim();
|
||
if (!email) {
|
||
showToast('Bitte geben Sie eine E-Mail-Adresse ein', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!currentEmailVersionId) {
|
||
showToast('Keine Version ausgewählt', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${currentEmailVersionId}/send-test`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email: email })
|
||
});
|
||
if (!res.ok) throw new Error('Fehler');
|
||
showToast('Test-E-Mail gesendet!', 'success');
|
||
} catch (e) {
|
||
showToast('Fehler beim Senden der Test-E-Mail', 'error');
|
||
}
|
||
}
|
||
|
||
async function initializeEmailTemplates() {
|
||
if (!confirm('Möchten Sie alle Standard-E-Mail-Templates initialisieren?')) return;
|
||
|
||
try {
|
||
const res = await fetch('/api/consent/admin/email-templates/initialize', {
|
||
method: 'POST'
|
||
});
|
||
if (!res.ok) throw new Error('Fehler');
|
||
showToast('Templates initialisiert!', 'success');
|
||
loadEmailTemplates();
|
||
} catch (e) {
|
||
showToast('Fehler bei der Initialisierung', 'error');
|
||
}
|
||
}
|
||
|
||
// E-Mail Editor Helpers
|
||
function formatEmailDoc(command) {
|
||
document.execCommand(command, false, null);
|
||
document.getElementById('email-version-editor').focus();
|
||
}
|
||
|
||
function formatEmailBlock(tag) {
|
||
document.execCommand('formatBlock', false, '<' + tag + '>');
|
||
document.getElementById('email-version-editor').focus();
|
||
}
|
||
|
||
function insertEmailVariable() {
|
||
const variable = prompt('Variablenname eingeben (z.B. user_name, reset_link):');
|
||
if (variable) {
|
||
document.execCommand('insertText', false, '{{' + variable + '}}');
|
||
}
|
||
}
|
||
|
||
function insertEmailLink() {
|
||
const url = prompt('Link-URL:');
|
||
if (url) {
|
||
const text = prompt('Link-Text:', url);
|
||
document.execCommand('insertHTML', false, `<a href="${url}" style="color: #5B21B6;">${text}</a>`);
|
||
}
|
||
}
|
||
|
||
function insertEmailButton() {
|
||
const url = prompt('Button-Link:');
|
||
if (url) {
|
||
const text = prompt('Button-Text:', 'Klicken');
|
||
const buttonHtml = `<table cellpadding="0" cellspacing="0" style="margin: 16px 0;"><tr><td style="background: #5B21B6; border-radius: 6px; padding: 12px 24px;"><a href="${url}" style="color: white; text-decoration: none; font-weight: 600;">${text}</a></td></tr></table>`;
|
||
document.execCommand('insertHTML', false, buttonHtml);
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// INITIALIZATION - DOMContentLoaded
|
||
// ==========================================
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Theme Toggle
|
||
initThemeToggle();
|
||
|
||
// Language initialization
|
||
if (typeof initLanguage === 'function') {
|
||
initLanguage();
|
||
}
|
||
|
||
// Vast Control
|
||
if (typeof initVastControl === 'function') {
|
||
initVastControl();
|
||
}
|
||
|
||
// Legal Modal Close Button
|
||
const legalCloseBtn = document.querySelector('.legal-modal-close');
|
||
if (legalCloseBtn) {
|
||
legalCloseBtn.addEventListener('click', function() {
|
||
document.getElementById('legal-modal').classList.remove('active');
|
||
});
|
||
}
|
||
|
||
// Auth Modal Close Button
|
||
const authCloseBtn = document.querySelector('.auth-modal-close');
|
||
if (authCloseBtn) {
|
||
authCloseBtn.addEventListener('click', function() {
|
||
document.getElementById('auth-modal').classList.remove('active');
|
||
});
|
||
}
|
||
|
||
// Admin Modal Close Button
|
||
const adminCloseBtn = document.querySelector('.admin-modal-close');
|
||
if (adminCloseBtn) {
|
||
adminCloseBtn.addEventListener('click', function() {
|
||
document.getElementById('admin-modal').classList.remove('active');
|
||
});
|
||
}
|
||
|
||
// Legal Button (Footer)
|
||
const legalBtn = document.querySelector('[onclick*="showLegalModal"]');
|
||
if (legalBtn) {
|
||
legalBtn.removeAttribute('onclick');
|
||
legalBtn.addEventListener('click', function() {
|
||
document.getElementById('legal-modal').classList.add('active');
|
||
});
|
||
}
|
||
|
||
// Consent Button (Footer)
|
||
const consentBtn = document.querySelector('[onclick*="showConsentModal"]');
|
||
if (consentBtn) {
|
||
consentBtn.removeAttribute('onclick');
|
||
consentBtn.addEventListener('click', function() {
|
||
document.getElementById('legal-modal').classList.add('active');
|
||
// Switch to consent tab
|
||
document.querySelectorAll('.legal-tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.legal-content').forEach(c => c.classList.remove('active'));
|
||
const consentTab = document.querySelector('.legal-tab[data-tab="consent"]');
|
||
const consentContent = document.getElementById('legal-content-consent');
|
||
if (consentTab) consentTab.classList.add('active');
|
||
if (consentContent) consentContent.classList.add('active');
|
||
});
|
||
}
|
||
|
||
// Legal Tabs
|
||
document.querySelectorAll('.legal-tab').forEach(tab => {
|
||
tab.addEventListener('click', function() {
|
||
const tabName = this.dataset.tab;
|
||
document.querySelectorAll('.legal-tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.legal-content').forEach(c => c.classList.remove('active'));
|
||
this.classList.add('active');
|
||
const content = document.getElementById('legal-content-' + tabName);
|
||
if (content) content.classList.add('active');
|
||
});
|
||
});
|
||
|
||
// Auth Tabs
|
||
document.querySelectorAll('.auth-tab').forEach(tab => {
|
||
tab.addEventListener('click', function() {
|
||
const tabName = this.dataset.tab;
|
||
document.querySelectorAll('.auth-tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.auth-form').forEach(f => f.classList.remove('active'));
|
||
this.classList.add('active');
|
||
const form = document.getElementById('auth-form-' + tabName);
|
||
if (form) form.classList.add('active');
|
||
});
|
||
});
|
||
|
||
// Admin Tabs
|
||
document.querySelectorAll('.admin-tab').forEach(tab => {
|
||
tab.addEventListener('click', function() {
|
||
const tabName = this.dataset.tab;
|
||
document.querySelectorAll('.admin-tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.admin-content').forEach(c => c.classList.remove('active'));
|
||
this.classList.add('active');
|
||
const content = document.getElementById('admin-content-' + tabName);
|
||
if (content) content.classList.add('active');
|
||
});
|
||
});
|
||
|
||
// Login Button (TopBar)
|
||
const loginBtn = document.getElementById('btn-login');
|
||
if (loginBtn) {
|
||
loginBtn.addEventListener('click', function() {
|
||
document.getElementById('auth-modal').classList.add('active');
|
||
});
|
||
}
|
||
|
||
// Admin Button (TopBar)
|
||
const adminBtn = document.getElementById('btn-admin');
|
||
if (adminBtn) {
|
||
adminBtn.addEventListener('click', function() {
|
||
document.getElementById('admin-modal').classList.add('active');
|
||
});
|
||
}
|
||
|
||
// Legal Button (TopBar)
|
||
const legalTopBtn = document.getElementById('btn-legal');
|
||
if (legalTopBtn) {
|
||
legalTopBtn.addEventListener('click', function() {
|
||
document.getElementById('legal-modal').classList.add('active');
|
||
});
|
||
}
|
||
|
||
// Modal Close Buttons (by specific IDs)
|
||
document.getElementById('legal-modal-close')?.addEventListener('click', function() {
|
||
document.getElementById('legal-modal').classList.remove('active');
|
||
});
|
||
document.getElementById('auth-modal-close')?.addEventListener('click', function() {
|
||
document.getElementById('auth-modal').classList.remove('active');
|
||
});
|
||
document.getElementById('admin-modal-close')?.addEventListener('click', function() {
|
||
document.getElementById('admin-modal').classList.remove('active');
|
||
});
|
||
document.getElementById('imprint-modal-close')?.addEventListener('click', function() {
|
||
document.getElementById('imprint-modal').classList.remove('active');
|
||
});
|
||
|
||
// Language selector
|
||
const langSelect = document.getElementById('language-select');
|
||
if (langSelect && typeof setLanguage === 'function') {
|
||
langSelect.addEventListener('change', function() {
|
||
setLanguage(this.value);
|
||
});
|
||
}
|
||
|
||
console.log('BreakPilot Studio initialized');
|
||
});
|
||
|
||
// ==========================================
|
||
// Communication Panel Functions (Matrix + Jitsi)
|
||
// ==========================================
|
||
|
||
// API Base URL for communication endpoints
|
||
const COMM_API_BASE = '/consent/api/v1/communication';
|
||
const JITSI_BASE_URL = 'http://localhost:8443';
|
||
|
||
// Current state
|
||
let currentRoom = null;
|
||
let jitsiApi = null;
|
||
|
||
// Show Messenger Panel (Matrix)
|
||
function showMessengerPanel() {
|
||
console.log('showMessengerPanel called');
|
||
hideAllPanels();
|
||
hideStudioSubMenu();
|
||
const messengerPanel = document.getElementById('panel-messenger');
|
||
if (messengerPanel) {
|
||
messengerPanel.style.display = 'flex';
|
||
console.log('Messenger panel shown');
|
||
} else {
|
||
console.error('panel-messenger not found');
|
||
}
|
||
|
||
updateSidebarActive('sidebar-messenger');
|
||
|
||
// Check service status
|
||
checkCommunicationStatus();
|
||
}
|
||
|
||
// Show Video Panel (Jitsi)
|
||
function showVideoPanel() {
|
||
console.log('showVideoPanel called');
|
||
hideAllPanels();
|
||
hideStudioSubMenu();
|
||
const videoPanel = document.getElementById('panel-video');
|
||
if (videoPanel) {
|
||
videoPanel.style.display = 'flex';
|
||
console.log('Video panel shown');
|
||
} else {
|
||
console.error('panel-video not found');
|
||
}
|
||
|
||
updateSidebarActive('sidebar-video');
|
||
}
|
||
|
||
// Legacy alias for backward compatibility
|
||
function showCommunicationPanel() {
|
||
showMessengerPanel();
|
||
}
|
||
|
||
// Show Studio Panel (original) - zeigt Arbeitsblatt-Tab als Standard
|
||
function showStudioPanel() {
|
||
console.log('showStudioPanel called');
|
||
hideAllPanels();
|
||
// Zeige Sub-Navigation
|
||
const subMenu = document.getElementById('studio-sub-menu');
|
||
if (subMenu) {
|
||
subMenu.style.display = 'flex';
|
||
}
|
||
// Zeige Arbeitsblätter als Standard-Tab
|
||
showWorksheetTab();
|
||
updateSidebarActive('sidebar-studio');
|
||
}
|
||
|
||
// Tab: Arbeitsblätter anzeigen
|
||
function showWorksheetTab() {
|
||
console.log('showWorksheetTab called');
|
||
// Verstecke beide Studio-Panels
|
||
const panelCompare = document.getElementById('panel-compare');
|
||
const panelTiles = document.getElementById('panel-tiles');
|
||
|
||
if (panelCompare) panelCompare.style.display = 'flex';
|
||
if (panelTiles) panelTiles.style.display = 'none';
|
||
|
||
// Update Sub-Navigation active state
|
||
updateSubNavActive('sub-worksheets');
|
||
}
|
||
|
||
// Tab: Lernkacheln anzeigen
|
||
function showTilesTab() {
|
||
console.log('showTilesTab called');
|
||
// Verstecke beide Studio-Panels
|
||
const panelCompare = document.getElementById('panel-compare');
|
||
const panelTiles = document.getElementById('panel-tiles');
|
||
|
||
if (panelCompare) panelCompare.style.display = 'none';
|
||
if (panelTiles) panelTiles.style.display = 'flex';
|
||
|
||
// Update Sub-Navigation active state
|
||
updateSubNavActive('sub-tiles');
|
||
}
|
||
|
||
// Helper: Update Sub-Navigation active state
|
||
function updateSubNavActive(activeSubId) {
|
||
document.querySelectorAll('.sidebar-sub-item').forEach(item => {
|
||
item.classList.remove('active');
|
||
});
|
||
const activeItem = document.getElementById(activeSubId);
|
||
if (activeItem) {
|
||
activeItem.classList.add('active');
|
||
}
|
||
}
|
||
|
||
// Helper: Hide Studio Sub-Menu (when switching to other panels)
|
||
function hideStudioSubMenu() {
|
||
const subMenu = document.getElementById('studio-sub-menu');
|
||
if (subMenu) {
|
||
subMenu.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Show Correction Panel (Klausur-Korrektur)
|
||
function showCorrectionPanel() {
|
||
console.log('showCorrectionPanel called');
|
||
hideAllPanels();
|
||
hideStudioSubMenu();
|
||
const correctionPanel = document.getElementById('panel-correction');
|
||
if (correctionPanel) {
|
||
correctionPanel.style.display = 'flex';
|
||
console.log('Correction panel shown');
|
||
} else {
|
||
console.error('panel-correction not found');
|
||
}
|
||
|
||
updateSidebarActive('sidebar-correction');
|
||
}
|
||
|
||
// Show Letters Panel (Elternbriefe)
|
||
function showLettersPanel() {
|
||
console.log('showLettersPanel called');
|
||
hideAllPanels();
|
||
hideStudioSubMenu();
|
||
const lettersPanel = document.getElementById('panel-letters');
|
||
if (lettersPanel) {
|
||
lettersPanel.style.display = 'flex';
|
||
console.log('Letters panel shown');
|
||
} else {
|
||
console.error('panel-letters not found');
|
||
}
|
||
|
||
updateSidebarActive('sidebar-letters');
|
||
}
|
||
|
||
// ========================================
|
||
// School Service Panel Functions
|
||
// ========================================
|
||
|
||
// School Service API Base URL
|
||
const SCHOOL_SERVICE_URL = '/api/school';
|
||
|
||
// Show Exams Panel (Klausuren & Tests)
|
||
function showExamsPanel() {
|
||
console.log('showExamsPanel called');
|
||
hideAllPanels();
|
||
hideStudioSubMenu();
|
||
const panel = document.getElementById('panel-exams');
|
||
if (panel) {
|
||
panel.style.display = 'flex';
|
||
loadClassesForSelect('exams-class-select');
|
||
loadSubjectsForSelect('exams-subject-select');
|
||
loadExams();
|
||
}
|
||
updateSidebarActive('sidebar-exams');
|
||
}
|
||
|
||
// Show Grades Panel (Notenspiegel)
|
||
function showGradesPanel() {
|
||
console.log('showGradesPanel called');
|
||
hideAllPanels();
|
||
hideStudioSubMenu();
|
||
const panel = document.getElementById('panel-grades');
|
||
if (panel) {
|
||
panel.style.display = 'flex';
|
||
loadClassesForSelect('grades-class-select');
|
||
}
|
||
updateSidebarActive('sidebar-grades');
|
||
}
|
||
|
||
// Show Gradebook Panel (Klassenbuch)
|
||
function showGradebookPanel() {
|
||
console.log('showGradebookPanel called');
|
||
hideAllPanels();
|
||
hideStudioSubMenu();
|
||
const panel = document.getElementById('panel-gradebook');
|
||
if (panel) {
|
||
panel.style.display = 'flex';
|
||
loadClassesForSelect('gradebook-class-select');
|
||
document.getElementById('gradebook-date').valueAsDate = new Date();
|
||
}
|
||
updateSidebarActive('sidebar-gradebook');
|
||
}
|
||
|
||
// Show Certificates Panel (Zeugnisse)
|
||
function showCertificatesPanel() {
|
||
console.log('showCertificatesPanel called');
|
||
hideAllPanels();
|
||
hideStudioSubMenu();
|
||
const panel = document.getElementById('panel-certificates');
|
||
if (panel) {
|
||
panel.style.display = 'flex';
|
||
loadClassesForSelect('cert-class-select');
|
||
loadCertificateTemplates();
|
||
}
|
||
updateSidebarActive('sidebar-certificates');
|
||
}
|
||
|
||
// Show Classes Panel (Klassen & Schüler)
|
||
function showClassesPanel() {
|
||
console.log('showClassesPanel called');
|
||
hideAllPanels();
|
||
hideStudioSubMenu();
|
||
const panel = document.getElementById('panel-classes');
|
||
if (panel) {
|
||
panel.style.display = 'flex';
|
||
loadSchoolYears();
|
||
loadClasses();
|
||
}
|
||
updateSidebarActive('sidebar-classes');
|
||
}
|
||
|
||
// Show Subjects Panel (Fächer)
|
||
function showSubjectsPanel() {
|
||
console.log('showSubjectsPanel called');
|
||
hideAllPanels();
|
||
hideStudioSubMenu();
|
||
const panel = document.getElementById('panel-subjects');
|
||
if (panel) {
|
||
panel.style.display = 'flex';
|
||
loadSubjects();
|
||
}
|
||
updateSidebarActive('sidebar-subjects');
|
||
}
|
||
|
||
// Helper: Load classes for dropdown
|
||
async function loadClassesForSelect(selectId) {
|
||
const select = document.getElementById(selectId);
|
||
if (!select) return;
|
||
|
||
try {
|
||
const response = await fetch(`${SCHOOL_SERVICE_URL}/classes`, {
|
||
headers: { 'Authorization': `Bearer ${localStorage.getItem('jwt')}` }
|
||
});
|
||
if (response.ok) {
|
||
const classes = await response.json();
|
||
select.innerHTML = '<option value="">Klasse wählen...</option>';
|
||
(classes || []).forEach(c => {
|
||
select.innerHTML += `<option value="${c.id}">${c.name} (Stufe ${c.grade_level})</option>`;
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.log('Could not load classes:', e);
|
||
}
|
||
}
|
||
|
||
// Helper: Load subjects for dropdown
|
||
async function loadSubjectsForSelect(selectId) {
|
||
const select = document.getElementById(selectId);
|
||
if (!select) return;
|
||
|
||
try {
|
||
const response = await fetch(`${SCHOOL_SERVICE_URL}/subjects`, {
|
||
headers: { 'Authorization': `Bearer ${localStorage.getItem('jwt')}` }
|
||
});
|
||
if (response.ok) {
|
||
const subjects = await response.json();
|
||
select.innerHTML = '<option value="">Fach wählen...</option>';
|
||
(subjects || []).forEach(s => {
|
||
select.innerHTML += `<option value="${s.id}">${s.name}</option>`;
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.log('Could not load subjects:', e);
|
||
}
|
||
}
|
||
|
||
// Load school years
|
||
async function loadSchoolYears() {
|
||
const select = document.getElementById('classes-year-select');
|
||
if (!select) return;
|
||
|
||
try {
|
||
const response = await fetch(`${SCHOOL_SERVICE_URL}/years`, {
|
||
headers: { 'Authorization': `Bearer ${localStorage.getItem('jwt')}` }
|
||
});
|
||
if (response.ok) {
|
||
const years = await response.json();
|
||
select.innerHTML = '<option value="">Schuljahr...</option>';
|
||
(years || []).forEach(y => {
|
||
select.innerHTML += `<option value="${y.id}">${y.name}${y.is_current ? ' (aktuell)' : ''}</option>`;
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.log('Could not load school years:', e);
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// SCHOOL SERVICE CRUD FUNCTIONS
|
||
// ============================================
|
||
|
||
const SCHOOL_API_BASE = '/api/school';
|
||
|
||
// Get auth headers for school service requests
|
||
function getSchoolAuthHeaders() {
|
||
const token = localStorage.getItem('token');
|
||
return {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
};
|
||
}
|
||
|
||
// Proxy request through backend to school-service
|
||
async function schoolServiceRequest(endpoint, options = {}) {
|
||
const token = localStorage.getItem('token');
|
||
const url = `${SCHOOL_API_BASE}${endpoint}`;
|
||
const response = await fetch(url, {
|
||
...options,
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json',
|
||
...options.headers
|
||
}
|
||
});
|
||
if (!response.ok) {
|
||
const error = await response.json().catch(() => ({ message: 'Request failed' }));
|
||
throw new Error(error.message || `HTTP ${response.status}`);
|
||
}
|
||
return response.json();
|
||
}
|
||
|
||
// ===== CLASSES =====
|
||
async function loadClasses() {
|
||
try {
|
||
const data = await schoolServiceRequest('/classes');
|
||
const container = document.getElementById('classes-list');
|
||
if (!container) return;
|
||
|
||
if (!data.classes || data.classes.length === 0) {
|
||
container.innerHTML = '<div class="empty-state">Keine Klassen vorhanden. Erstellen Sie eine neue Klasse.</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = data.classes.map(c => `
|
||
<div class="class-card" data-id="${c.id}">
|
||
<div class="class-header">
|
||
<h3>${c.name}</h3>
|
||
<span class="badge">${c.school_type || 'Gymnasium'}</span>
|
||
</div>
|
||
<div class="class-info">
|
||
<span>Klasse ${c.grade_level}</span>
|
||
<span>${c.student_count || 0} Schüler</span>
|
||
</div>
|
||
<div class="class-actions">
|
||
<button class="btn btn-sm btn-secondary" onclick="showClassStudents('${c.id}', '${c.name}')">Schüler</button>
|
||
<button class="btn btn-sm btn-danger" onclick="deleteClass('${c.id}')">Löschen</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
console.error('Error loading classes:', e);
|
||
showToast('Fehler beim Laden der Klassen', 'error');
|
||
}
|
||
}
|
||
|
||
async function showClassStudents(classId, className) {
|
||
try {
|
||
const data = await schoolServiceRequest(`/classes/${classId}/students`);
|
||
const students = data.students || [];
|
||
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-overlay';
|
||
modal.innerHTML = `
|
||
<div class="modal-content" style="max-width: 600px;">
|
||
<div class="modal-header">
|
||
<h2>Schüler - ${className}</h2>
|
||
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="student-actions" style="margin-bottom: 1rem;">
|
||
<button class="btn btn-primary btn-sm" onclick="showAddStudentModal('${classId}')">+ Schüler hinzufügen</button>
|
||
<button class="btn btn-secondary btn-sm" onclick="showImportStudentsModal('${classId}')">CSV Import</button>
|
||
</div>
|
||
<div class="students-list">
|
||
${students.length === 0 ? '<div class="empty-state">Keine Schüler vorhanden</div>' :
|
||
students.map(s => `
|
||
<div class="student-row">
|
||
<span class="student-name">${s.last_name}, ${s.first_name}</span>
|
||
<span class="student-number">${s.student_number || ''}</span>
|
||
<button class="btn btn-sm btn-danger" onclick="deleteStudent('${classId}', '${s.id}')">×</button>
|
||
</div>
|
||
`).join('')
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
} catch (e) {
|
||
console.error('Error loading students:', e);
|
||
showToast('Fehler beim Laden der Schüler', 'error');
|
||
}
|
||
}
|
||
|
||
function showCreateClassModal() {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-overlay';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>Neue Klasse anlegen</h2>
|
||
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="create-class-form" onsubmit="createClass(event)">
|
||
<div class="form-group">
|
||
<label>Klassenname*</label>
|
||
<input type="text" name="name" placeholder="z.B. 7a" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Klassenstufe*</label>
|
||
<select name="grade_level" required>
|
||
${[1,2,3,4,5,6,7,8,9,10,11,12,13].map(g => `<option value="${g}">${g}. Klasse</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Schulform</label>
|
||
<select name="school_type">
|
||
<option value="gymnasium">Gymnasium</option>
|
||
<option value="realschule">Realschule</option>
|
||
<option value="hauptschule">Hauptschule</option>
|
||
<option value="gesamtschule">Gesamtschule</option>
|
||
<option value="grundschule">Grundschule</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Bundesland</label>
|
||
<select name="federal_state">
|
||
<option value="niedersachsen">Niedersachsen</option>
|
||
<option value="nrw">Nordrhein-Westfalen</option>
|
||
<option value="bayern">Bayern</option>
|
||
<option value="baden-wuerttemberg">Baden-Württemberg</option>
|
||
<option value="hessen">Hessen</option>
|
||
<option value="other">Sonstiges</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">Klasse erstellen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
async function createClass(event) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
|
||
try {
|
||
await schoolServiceRequest('/classes', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
name: formData.get('name'),
|
||
grade_level: parseInt(formData.get('grade_level')),
|
||
school_type: formData.get('school_type'),
|
||
federal_state: formData.get('federal_state')
|
||
})
|
||
});
|
||
form.closest('.modal-overlay').remove();
|
||
showToast('Klasse erfolgreich erstellt', 'success');
|
||
loadClasses();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteClass(classId) {
|
||
if (!confirm('Klasse wirklich löschen? Alle Schüler werden ebenfalls gelöscht.')) return;
|
||
try {
|
||
await schoolServiceRequest(`/classes/${classId}`, { method: 'DELETE' });
|
||
showToast('Klasse gelöscht', 'success');
|
||
loadClasses();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
function showAddStudentModal(classId) {
|
||
document.querySelector('.modal-overlay')?.remove();
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-overlay';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>Schüler hinzufügen</h2>
|
||
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="add-student-form" onsubmit="addStudent(event, '${classId}')">
|
||
<div class="form-group">
|
||
<label>Vorname*</label>
|
||
<input type="text" name="first_name" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Nachname*</label>
|
||
<input type="text" name="last_name" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Geburtsdatum</label>
|
||
<input type="date" name="birth_date">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Schülernummer</label>
|
||
<input type="text" name="student_number">
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">Hinzufügen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
async function addStudent(event, classId) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
|
||
try {
|
||
await schoolServiceRequest(`/classes/${classId}/students`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
first_name: formData.get('first_name'),
|
||
last_name: formData.get('last_name'),
|
||
birth_date: formData.get('birth_date') || null,
|
||
student_number: formData.get('student_number') || null
|
||
})
|
||
});
|
||
form.closest('.modal-overlay').remove();
|
||
showToast('Schüler hinzugefügt', 'success');
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteStudent(classId, studentId) {
|
||
if (!confirm('Schüler wirklich entfernen?')) return;
|
||
try {
|
||
await schoolServiceRequest(`/classes/${classId}/students/${studentId}`, { method: 'DELETE' });
|
||
showToast('Schüler entfernt', 'success');
|
||
document.querySelector('.modal-overlay')?.remove();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
function showImportStudentsModal(classId) {
|
||
document.querySelector('.modal-overlay')?.remove();
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-overlay';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>CSV Import</h2>
|
||
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>CSV-Format: Vorname,Nachname[,Geburtsdatum][,Schülernummer]</p>
|
||
<form id="import-students-form" onsubmit="importStudents(event, '${classId}')">
|
||
<div class="form-group">
|
||
<label>CSV-Datei</label>
|
||
<input type="file" name="file" accept=".csv" required>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">Importieren</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
async function importStudents(event, classId) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const fileInput = form.querySelector('input[type="file"]');
|
||
const file = fileInput.files[0];
|
||
if (!file) return;
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
try {
|
||
const token = localStorage.getItem('token');
|
||
const response = await fetch(`${SCHOOL_API_BASE}/classes/${classId}/students/import`, {
|
||
method: 'POST',
|
||
headers: { 'Authorization': `Bearer ${token}` },
|
||
body: formData
|
||
});
|
||
const data = await response.json();
|
||
form.closest('.modal-overlay').remove();
|
||
showToast(`${data.imported || 0} Schüler importiert`, 'success');
|
||
} catch (e) {
|
||
showToast('Import fehlgeschlagen: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
// ===== SUBJECTS =====
|
||
async function loadSubjects() {
|
||
try {
|
||
const data = await schoolServiceRequest('/subjects');
|
||
const container = document.getElementById('subjects-list');
|
||
if (!container) return;
|
||
|
||
if (!data.subjects || data.subjects.length === 0) {
|
||
container.innerHTML = '<div class="empty-state">Keine Fächer vorhanden</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = data.subjects.map(s => `
|
||
<div class="subject-row">
|
||
<span class="subject-name">${s.name}</span>
|
||
<span class="subject-short">${s.short_name || ''}</span>
|
||
<span class="badge ${s.is_main_subject ? 'badge-primary' : 'badge-secondary'}">${s.is_main_subject ? 'Hauptfach' : 'Nebenfach'}</span>
|
||
<button class="btn btn-sm btn-danger" onclick="deleteSubject('${s.id}')">×</button>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
console.error('Error loading subjects:', e);
|
||
showToast('Fehler beim Laden der Fächer', 'error');
|
||
}
|
||
}
|
||
|
||
function showCreateSubjectModal() {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-overlay';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>Neues Fach anlegen</h2>
|
||
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="create-subject-form" onsubmit="createSubject(event)">
|
||
<div class="form-group">
|
||
<label>Fachname*</label>
|
||
<input type="text" name="name" placeholder="z.B. Mathematik" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Kürzel</label>
|
||
<input type="text" name="short_name" placeholder="z.B. Ma" maxlength="10">
|
||
</div>
|
||
<div class="form-group">
|
||
<label><input type="checkbox" name="is_main_subject"> Hauptfach (höhere Gewichtung)</label>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">Fach erstellen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
async function createSubject(event) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
|
||
try {
|
||
await schoolServiceRequest('/subjects', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
name: formData.get('name'),
|
||
short_name: formData.get('short_name') || null,
|
||
is_main_subject: form.querySelector('[name="is_main_subject"]').checked
|
||
})
|
||
});
|
||
form.closest('.modal-overlay').remove();
|
||
showToast('Fach erstellt', 'success');
|
||
loadSubjects();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteSubject(subjectId) {
|
||
if (!confirm('Fach wirklich löschen?')) return;
|
||
try {
|
||
await schoolServiceRequest(`/subjects/${subjectId}`, { method: 'DELETE' });
|
||
showToast('Fach gelöscht', 'success');
|
||
loadSubjects();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
// ===== EXAMS =====
|
||
async function loadExams() {
|
||
try {
|
||
const classSelect = document.getElementById('exams-class-select');
|
||
const classId = classSelect?.value;
|
||
const url = classId ? `/exams?class_id=${classId}` : '/exams';
|
||
const data = await schoolServiceRequest(url);
|
||
const container = document.getElementById('exams-list');
|
||
if (!container) return;
|
||
|
||
if (!data.exams || data.exams.length === 0) {
|
||
container.innerHTML = '<div class="empty-state">Keine Klausuren vorhanden</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = data.exams.map(e => `
|
||
<div class="exam-card" data-id="${e.id}">
|
||
<div class="exam-header">
|
||
<h4>${e.title}</h4>
|
||
<span class="badge badge-${e.status === 'active' ? 'success' : 'secondary'}">${e.status}</span>
|
||
</div>
|
||
<div class="exam-info">
|
||
<span>${e.exam_type}</span>
|
||
<span>${e.exam_date || 'Kein Datum'}</span>
|
||
<span>${e.max_points} Punkte</span>
|
||
</div>
|
||
<div class="exam-actions">
|
||
<button class="btn btn-sm btn-primary" onclick="showExamResultsModal('${e.id}')">Ergebnisse</button>
|
||
<button class="btn btn-sm btn-secondary" onclick="generateNachschreiber('${e.id}')">Nachschreiber</button>
|
||
<button class="btn btn-sm btn-danger" onclick="deleteExam('${e.id}')">Löschen</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
console.error('Error loading exams:', e);
|
||
showToast('Fehler beim Laden der Klausuren', 'error');
|
||
}
|
||
}
|
||
|
||
function showCreateExamModal() {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-overlay';
|
||
modal.innerHTML = `
|
||
<div class="modal-content" style="max-width: 700px;">
|
||
<div class="modal-header">
|
||
<h2>Neue Klausur/Arbeit</h2>
|
||
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="create-exam-form" onsubmit="createExam(event)">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Titel*</label>
|
||
<input type="text" name="title" placeholder="z.B. Klassenarbeit Nr. 1" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Typ*</label>
|
||
<select name="exam_type" required>
|
||
<option value="klassenarbeit">Klassenarbeit</option>
|
||
<option value="test">Test</option>
|
||
<option value="klausur">Klausur (Oberstufe)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Klasse*</label>
|
||
<select name="class_id" id="exam-class-select" required></select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Fach*</label>
|
||
<select name="subject_id" id="exam-subject-select" required></select>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Datum</label>
|
||
<input type="date" name="exam_date">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Dauer (Minuten)</label>
|
||
<input type="number" name="duration_minutes" value="45" min="5">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Max. Punkte</label>
|
||
<input type="number" name="max_points" value="50" min="1" step="0.5">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Thema</label>
|
||
<input type="text" name="topic" placeholder="z.B. Bruchrechnung">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Aufgaben (Markdown/HTML)</label>
|
||
<textarea name="content" rows="6" placeholder="## Aufgabe 1 Berechne..."></textarea>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">Klausur erstellen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
loadClassesForSelect('exam-class-select');
|
||
loadSubjectsForSelect('exam-subject-select');
|
||
}
|
||
|
||
async function createExam(event) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
|
||
try {
|
||
await schoolServiceRequest('/exams', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
title: formData.get('title'),
|
||
exam_type: formData.get('exam_type'),
|
||
class_id: formData.get('class_id'),
|
||
subject_id: formData.get('subject_id'),
|
||
exam_date: formData.get('exam_date') || null,
|
||
duration_minutes: parseInt(formData.get('duration_minutes')) || 45,
|
||
max_points: parseFloat(formData.get('max_points')) || 50,
|
||
topic: formData.get('topic') || null,
|
||
content: formData.get('content') || null
|
||
})
|
||
});
|
||
form.closest('.modal-overlay').remove();
|
||
showToast('Klausur erstellt', 'success');
|
||
loadExams();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteExam(examId) {
|
||
if (!confirm('Klausur wirklich löschen?')) return;
|
||
try {
|
||
await schoolServiceRequest(`/exams/${examId}`, { method: 'DELETE' });
|
||
showToast('Klausur gelöscht', 'success');
|
||
loadExams();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function showExamResultsModal(examId) {
|
||
try {
|
||
const exam = await schoolServiceRequest(`/exams/${examId}`);
|
||
const results = await schoolServiceRequest(`/exams/${examId}/results`);
|
||
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-overlay';
|
||
modal.innerHTML = `
|
||
<div class="modal-content" style="max-width: 800px;">
|
||
<div class="modal-header">
|
||
<h2>Ergebnisse: ${exam.title}</h2>
|
||
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>Max. Punkte: ${exam.max_points}</p>
|
||
<table class="results-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Schüler</th>
|
||
<th>Punkte</th>
|
||
<th>%</th>
|
||
<th>Note</th>
|
||
<th>Freigabe</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${(results.results || []).map(r => `
|
||
<tr>
|
||
<td>${r.student_name}</td>
|
||
<td><input type="number" value="${r.points_achieved || ''}" data-student="${r.student_id}" class="result-points" step="0.5" min="0" max="${exam.max_points}"></td>
|
||
<td>${r.percentage ? r.percentage.toFixed(1) + '%' : '-'}</td>
|
||
<td>${r.grade || '-'}</td>
|
||
<td>${r.approved_by_teacher ? '✓' : '<button class="btn btn-xs" onclick="approveResult(\'${examId}\', \'${r.student_id}\')">Freigeben</button>'}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
<div class="form-actions" style="margin-top: 1rem;">
|
||
<button class="btn btn-primary" onclick="saveExamResults('${examId}', ${exam.max_points})">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function saveExamResults(examId, maxPoints) {
|
||
const inputs = document.querySelectorAll('.result-points');
|
||
const results = [];
|
||
inputs.forEach(input => {
|
||
const points = parseFloat(input.value);
|
||
if (!isNaN(points)) {
|
||
results.push({
|
||
student_id: input.dataset.student,
|
||
points_achieved: points
|
||
});
|
||
}
|
||
});
|
||
|
||
try {
|
||
await schoolServiceRequest(`/exams/${examId}/results`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ results })
|
||
});
|
||
showToast('Ergebnisse gespeichert', 'success');
|
||
document.querySelector('.modal-overlay')?.remove();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function approveResult(examId, studentId) {
|
||
try {
|
||
await schoolServiceRequest(`/exams/${examId}/results/${studentId}/approve`, { method: 'PUT' });
|
||
showToast('Freigegeben', 'success');
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function generateNachschreiber(examId) {
|
||
if (!confirm('Nachschreiber-Version mit KI generieren?')) return;
|
||
try {
|
||
showToast('Generiere Nachschreiber...', 'info');
|
||
await schoolServiceRequest(`/exams/${examId}/generate-variant`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ variation_type: 'rewrite' })
|
||
});
|
||
showToast('Nachschreiber erstellt', 'success');
|
||
loadExams();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
function editExam(examId) {
|
||
showToast('Bearbeitungsfunktion in Entwicklung', 'info');
|
||
}
|
||
|
||
// ===== GRADES =====
|
||
async function loadGrades() {
|
||
try {
|
||
const classSelect = document.getElementById('grades-class-select');
|
||
const classId = classSelect?.value;
|
||
if (!classId) return;
|
||
|
||
const data = await schoolServiceRequest(`/grades/${classId}`);
|
||
const container = document.getElementById('grades-table');
|
||
if (!container) return;
|
||
|
||
if (!data.grades || data.grades.length === 0) {
|
||
container.innerHTML = '<div class="empty-state">Keine Noten vorhanden</div>';
|
||
return;
|
||
}
|
||
|
||
// Get all unique subjects
|
||
const subjects = new Set();
|
||
data.grades.forEach(g => g.subjects?.forEach(s => subjects.add(s.subject_name)));
|
||
const subjectList = Array.from(subjects);
|
||
|
||
container.innerHTML = `
|
||
<table class="grades-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Schüler</th>
|
||
${subjectList.map(s => `<th>${s}</th>`).join('')}
|
||
<th>Durchschnitt</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${data.grades.map(g => {
|
||
const avg = g.subjects?.reduce((sum, s) => sum + (s.final_grade || 0), 0) / (g.subjects?.length || 1);
|
||
return `
|
||
<tr>
|
||
<td>${g.student_name}</td>
|
||
${subjectList.map(subj => {
|
||
const s = g.subjects?.find(x => x.subject_name === subj);
|
||
return `<td class="${s?.final_grade_locked ? 'locked' : ''}">${s?.final_grade?.toFixed(1) || '-'}</td>`;
|
||
}).join('')}
|
||
<td><strong>${avg.toFixed(2)}</strong></td>
|
||
</tr>
|
||
`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
} catch (e) {
|
||
console.error('Error loading grades:', e);
|
||
showToast('Fehler beim Laden der Noten', 'error');
|
||
}
|
||
}
|
||
|
||
async function calculateFinalGrades() {
|
||
const classSelect = document.getElementById('grades-class-select');
|
||
const classId = classSelect?.value;
|
||
if (!classId) {
|
||
showToast('Bitte Klasse auswählen', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await schoolServiceRequest('/grades/calculate', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ class_id: classId, semester: 1 })
|
||
});
|
||
showToast('Endnoten berechnet', 'success');
|
||
loadGrades();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
// ===== ATTENDANCE / GRADEBOOK =====
|
||
async function loadGradebook() {
|
||
try {
|
||
const classSelect = document.getElementById('gradebook-class-select');
|
||
const classId = classSelect?.value;
|
||
if (!classId) return;
|
||
|
||
const attendance = await schoolServiceRequest(`/attendance/${classId}`);
|
||
const entries = await schoolServiceRequest(`/gradebook/${classId}`);
|
||
|
||
const container = document.getElementById('gradebook-content');
|
||
if (!container) return;
|
||
|
||
container.innerHTML = `
|
||
<div class="gradebook-section">
|
||
<h4>Fehlzeiten</h4>
|
||
<table class="attendance-table">
|
||
<thead>
|
||
<tr><th>Schüler</th><th>Datum</th><th>Status</th><th>Stunden</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
${(attendance.attendance || []).slice(0, 20).map(a => `
|
||
<tr>
|
||
<td>${a.student_name}</td>
|
||
<td>${a.date}</td>
|
||
<td>${a.status === 'absent_excused' ? 'entschuldigt' : a.status === 'absent_unexcused' ? 'unentschuldigt' : a.status}</td>
|
||
<td>${a.periods}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="gradebook-section">
|
||
<h4>Einträge</h4>
|
||
${(entries.entries || []).slice(0, 10).map(e => `
|
||
<div class="gradebook-entry ${e.entry_type}">
|
||
<span class="date">${e.date}</span>
|
||
<span class="type">${e.entry_type}</span>
|
||
<span class="content">${e.content}</span>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
} catch (e) {
|
||
console.error('Error loading gradebook:', e);
|
||
}
|
||
}
|
||
|
||
function loadGradebookEntries() { loadGradebook(); }
|
||
|
||
function showAttendanceModal() {
|
||
const classSelect = document.getElementById('gradebook-class-select');
|
||
const classId = classSelect?.value;
|
||
if (!classId) {
|
||
showToast('Bitte Klasse auswählen', 'warning');
|
||
return;
|
||
}
|
||
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-overlay';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>Fehlzeit eintragen</h2>
|
||
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="attendance-form" onsubmit="createAttendance(event, '${classId}')">
|
||
<div class="form-group">
|
||
<label>Datum</label>
|
||
<input type="date" name="date" value="${new Date().toISOString().split('T')[0]}" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Schüler</label>
|
||
<select name="student_id" id="attendance-student-select" required></select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Status</label>
|
||
<select name="status" required>
|
||
<option value="absent_excused">Entschuldigt</option>
|
||
<option value="absent_unexcused">Unentschuldigt</option>
|
||
<option value="late">Verspätet</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Stunden</label>
|
||
<input type="number" name="periods" value="1" min="1" max="12">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Grund</label>
|
||
<input type="text" name="reason" placeholder="z.B. Arzttermin">
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">Eintragen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
loadStudentsForSelect(classId, 'attendance-student-select');
|
||
}
|
||
|
||
async function createAttendance(event, classId) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
|
||
try {
|
||
await schoolServiceRequest('/attendance', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
student_id: formData.get('student_id'),
|
||
date: formData.get('date'),
|
||
status: formData.get('status'),
|
||
periods: parseInt(formData.get('periods')),
|
||
reason: formData.get('reason') || null
|
||
})
|
||
});
|
||
form.closest('.modal-overlay').remove();
|
||
showToast('Fehlzeit eingetragen', 'success');
|
||
loadGradebook();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
function showGradebookEntryModal() {
|
||
const classSelect = document.getElementById('gradebook-class-select');
|
||
const classId = classSelect?.value;
|
||
if (!classId) {
|
||
showToast('Bitte Klasse auswählen', 'warning');
|
||
return;
|
||
}
|
||
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-overlay';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>Eintrag erstellen</h2>
|
||
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="entry-form" onsubmit="createGradebookEntry(event, '${classId}')">
|
||
<div class="form-group">
|
||
<label>Datum</label>
|
||
<input type="date" name="date" value="${new Date().toISOString().split('T')[0]}" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Schüler (optional)</label>
|
||
<select name="student_id" id="entry-student-select">
|
||
<option value="">Klasseneintrag</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Art</label>
|
||
<select name="entry_type" required>
|
||
<option value="note">Notiz</option>
|
||
<option value="warning">Verwarnung</option>
|
||
<option value="praise">Lob</option>
|
||
<option value="incident">Vorfall</option>
|
||
<option value="homework">Hausaufgaben</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Inhalt</label>
|
||
<textarea name="content" rows="3" required></textarea>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">Eintragen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
loadStudentsForSelect(classId, 'entry-student-select');
|
||
}
|
||
|
||
async function createGradebookEntry(event, classId) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
|
||
try {
|
||
await schoolServiceRequest('/gradebook', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
class_id: classId,
|
||
student_id: formData.get('student_id') || null,
|
||
date: formData.get('date'),
|
||
entry_type: formData.get('entry_type'),
|
||
content: formData.get('content')
|
||
})
|
||
});
|
||
form.closest('.modal-overlay').remove();
|
||
showToast('Eintrag erstellt', 'success');
|
||
loadGradebook();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
// ===== CERTIFICATES =====
|
||
async function loadCertificates() {
|
||
try {
|
||
const classSelect = document.getElementById('certificates-class-select');
|
||
const classId = classSelect?.value;
|
||
if (!classId) return;
|
||
|
||
const data = await schoolServiceRequest(`/certificates/class/${classId}`);
|
||
const container = document.getElementById('certificates-list');
|
||
if (!container) return;
|
||
|
||
if (!data.certificates || data.certificates.length === 0) {
|
||
container.innerHTML = '<div class="empty-state">Keine Zeugnisse vorhanden</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = data.certificates.map(c => `
|
||
<div class="certificate-row">
|
||
<span>${c.student_name}</span>
|
||
<span class="badge badge-${c.status === 'final' ? 'success' : 'secondary'}">${c.status}</span>
|
||
<div class="certificate-actions">
|
||
${c.status === 'draft' ? `<button class="btn btn-xs" onclick="finalizeCertificate('${c.id}')">Finalisieren</button>` : ''}
|
||
<button class="btn btn-xs" onclick="downloadCertificatePDF('${c.id}')">PDF</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
console.error('Error loading certificates:', e);
|
||
}
|
||
}
|
||
|
||
async function loadCertificateTemplates() {
|
||
try {
|
||
const data = await schoolServiceRequest('/certificates/templates');
|
||
const select = document.getElementById('certificate-template-select');
|
||
if (!select) return;
|
||
|
||
select.innerHTML = (data.templates || []).map(t =>
|
||
`<option value="${t.name}">${t.display_name}</option>`
|
||
).join('');
|
||
} catch (e) {
|
||
console.error('Error loading templates:', e);
|
||
}
|
||
}
|
||
|
||
async function generateAllCertificates() {
|
||
const classSelect = document.getElementById('certificates-class-select');
|
||
const templateSelect = document.getElementById('certificate-template-select');
|
||
const classId = classSelect?.value;
|
||
const template = templateSelect?.value;
|
||
|
||
if (!classId || !template) {
|
||
showToast('Bitte Klasse und Vorlage auswählen', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showToast('Generiere Zeugnisse...', 'info');
|
||
await schoolServiceRequest('/certificates/generate-bulk', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
class_id: classId,
|
||
semester: 1,
|
||
certificate_type: 'halbjahr',
|
||
template_name: template
|
||
})
|
||
});
|
||
showToast('Zeugnisse generiert', 'success');
|
||
loadCertificates();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function finalizeCertificate(certId) {
|
||
if (!confirm('Zeugnis finalisieren? Dies kann nicht rückgängig gemacht werden.')) return;
|
||
try {
|
||
await schoolServiceRequest(`/certificates/detail/${certId}/finalize`, { method: 'PUT' });
|
||
showToast('Zeugnis finalisiert', 'success');
|
||
loadCertificates();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function downloadCertificatePDF(certId) {
|
||
try {
|
||
const token = localStorage.getItem('token');
|
||
const response = await fetch(`${SCHOOL_API_BASE}/certificates/detail/${certId}/pdf`, {
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
if (!response.ok) throw new Error('PDF download failed');
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `zeugnis_${certId}.pdf`;
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
} catch (e) {
|
||
showToast('Fehler beim PDF-Download', 'error');
|
||
}
|
||
}
|
||
|
||
// ===== SCHOOL YEARS =====
|
||
function showCreateSchoolYearModal() {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-overlay';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>Neues Schuljahr</h2>
|
||
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="school-year-form" onsubmit="createSchoolYear(event)">
|
||
<div class="form-group">
|
||
<label>Name*</label>
|
||
<input type="text" name="name" placeholder="z.B. 2024/2025" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Start</label>
|
||
<input type="date" name="start_date" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Ende</label>
|
||
<input type="date" name="end_date" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label><input type="checkbox" name="is_current" checked> Aktuelles Schuljahr</label>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').remove()">Abbrechen</button>
|
||
<button type="submit" class="btn btn-primary">Erstellen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
async function createSchoolYear(event) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
|
||
try {
|
||
await schoolServiceRequest('/years', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
name: formData.get('name'),
|
||
start_date: formData.get('start_date'),
|
||
end_date: formData.get('end_date'),
|
||
is_current: form.querySelector('[name="is_current"]').checked
|
||
})
|
||
});
|
||
form.closest('.modal-overlay').remove();
|
||
showToast('Schuljahr erstellt', 'success');
|
||
loadSchoolYears();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
// ===== HELPER: Load students for select =====
|
||
async function loadStudentsForSelect(classId, selectId) {
|
||
try {
|
||
const data = await schoolServiceRequest(`/classes/${classId}/students`);
|
||
const select = document.getElementById(selectId);
|
||
if (!select) return;
|
||
|
||
const currentOptions = select.innerHTML;
|
||
select.innerHTML = currentOptions + (data.students || []).map(s =>
|
||
`<option value="${s.id}">${s.last_name}, ${s.first_name}</option>`
|
||
).join('');
|
||
} catch (e) {
|
||
console.error('Error loading students for select:', e);
|
||
}
|
||
}
|
||
|
||
// Helper: Hide all panels
|
||
function hideAllPanels() {
|
||
const panels = [
|
||
'panel-compare',
|
||
'panel-tiles',
|
||
'panel-correction',
|
||
'panel-letters',
|
||
'panel-messenger',
|
||
'panel-video',
|
||
'panel-exams',
|
||
'panel-grades',
|
||
'panel-gradebook',
|
||
'panel-certificates',
|
||
'panel-classes',
|
||
'panel-subjects'
|
||
];
|
||
panels.forEach(panelId => {
|
||
const panel = document.getElementById(panelId);
|
||
if (panel) {
|
||
panel.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Helper: Update sidebar active state
|
||
function updateSidebarActive(activeSidebarId) {
|
||
document.querySelectorAll('.sidebar-item').forEach(item => {
|
||
item.classList.remove('active');
|
||
});
|
||
const activeItem = document.getElementById(activeSidebarId);
|
||
if (activeItem) {
|
||
activeItem.classList.add('active');
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// JITSI VIDEOKONFERENZ MODULE
|
||
// ============================================
|
||
|
||
let currentJitsiMeetingUrl = null;
|
||
let jitsiMicMuted = false;
|
||
let jitsiVideoOff = false;
|
||
|
||
// Start instant meeting
|
||
async function startInstantMeeting() {
|
||
console.log('Starting instant meeting...');
|
||
const meetingName = document.getElementById('meeting-name')?.value || '';
|
||
|
||
try {
|
||
// Generate a unique room name
|
||
const roomId = 'bp-' + Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
|
||
const displayName = meetingName || 'BreakPilot Meeting';
|
||
|
||
// Create Jitsi meeting URL
|
||
const jitsiDomain = 'meet.jit.si';
|
||
currentJitsiMeetingUrl = `https://${jitsiDomain}/${roomId}`;
|
||
|
||
// Show the Jitsi iframe
|
||
const placeholder = document.getElementById('jitsi-placeholder');
|
||
const iframeContainer = document.getElementById('jitsi-iframe-container');
|
||
const controls = document.getElementById('jitsi-controls');
|
||
|
||
if (placeholder) placeholder.style.display = 'none';
|
||
if (iframeContainer) {
|
||
iframeContainer.style.display = 'flex';
|
||
iframeContainer.innerHTML = `
|
||
<iframe
|
||
src="${currentJitsiMeetingUrl}#config.prejoinPageEnabled=false&userInfo.displayName=${encodeURIComponent('Lehrer')}"
|
||
style="width: 100%; height: 100%; border: none; border-radius: 8px;"
|
||
allow="camera; microphone; fullscreen; display-capture; autoplay"
|
||
allowfullscreen
|
||
></iframe>
|
||
`;
|
||
}
|
||
if (controls) controls.style.display = 'flex';
|
||
|
||
// Update status
|
||
const statusPill = document.getElementById('jitsi-connection-status');
|
||
if (statusPill) {
|
||
statusPill.textContent = 'Live';
|
||
statusPill.style.background = '#ef4444';
|
||
}
|
||
|
||
// Update participants list
|
||
const participantsList = document.getElementById('jitsi-participants');
|
||
if (participantsList) {
|
||
participantsList.innerHTML = `
|
||
<div style="display: flex; align-items: center; gap: 8px; padding: 8px; background: rgba(16, 185, 129, 0.1); border-radius: 6px;">
|
||
<div style="width: 32px; height: 32px; border-radius: 50%; background: #6C1B1B; color: white; display: flex; align-items: center; justify-content: center; font-weight: 600;">L</div>
|
||
<div>
|
||
<div style="font-weight: 600; font-size: 13px;">Sie (Lehrer)</div>
|
||
<div style="font-size: 11px; color: #888;">Moderator</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
console.log('Meeting started:', currentJitsiMeetingUrl);
|
||
} catch (error) {
|
||
console.error('Error starting meeting:', error);
|
||
alert('Fehler beim Starten des Meetings: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Join a scheduled meeting
|
||
function joinScheduledMeeting(meetingId) {
|
||
console.log('Joining scheduled meeting:', meetingId);
|
||
// For demo, just start a meeting with the ID as room name
|
||
document.getElementById('meeting-name').value = meetingId;
|
||
startInstantMeeting();
|
||
}
|
||
|
||
// Schedule a meeting
|
||
function scheduleMeeting() {
|
||
const title = document.getElementById('schedule-title')?.value;
|
||
const datetime = document.getElementById('schedule-datetime')?.value;
|
||
const participants = document.getElementById('schedule-participants')?.value;
|
||
|
||
if (!title || !datetime) {
|
||
alert('Bitte Titel und Datum/Uhrzeit eingeben');
|
||
return;
|
||
}
|
||
|
||
console.log('Scheduling meeting:', { title, datetime, participants });
|
||
|
||
// Add to scheduled meetings list (demo)
|
||
const scheduledList = document.getElementById('scheduled-meetings');
|
||
if (scheduledList) {
|
||
const dateObj = new Date(datetime);
|
||
const formattedTime = dateObj.toLocaleString('de-DE', {
|
||
weekday: 'short',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
|
||
const meetingItem = document.createElement('div');
|
||
meetingItem.className = 'meeting-item';
|
||
meetingItem.onclick = () => joinScheduledMeeting('m-' + Date.now());
|
||
meetingItem.innerHTML = `
|
||
<div class="meeting-info">
|
||
<div class="meeting-title">${title}</div>
|
||
<div class="meeting-time">${formattedTime}</div>
|
||
</div>
|
||
<button class="btn btn-sm btn-ghost">Beitreten</button>
|
||
`;
|
||
scheduledList.appendChild(meetingItem);
|
||
}
|
||
|
||
// Clear form
|
||
document.getElementById('schedule-title').value = '';
|
||
document.getElementById('schedule-datetime').value = '';
|
||
document.getElementById('schedule-participants').value = '';
|
||
|
||
alert('Meeting geplant: ' + title);
|
||
}
|
||
|
||
// Toggle mute
|
||
function toggleJitsiMute() {
|
||
jitsiMicMuted = !jitsiMicMuted;
|
||
console.log('Mic muted:', jitsiMicMuted);
|
||
// Note: With iframe approach, we can't control the Jitsi directly
|
||
// User should use Jitsi's built-in controls
|
||
}
|
||
|
||
// Toggle video
|
||
function toggleJitsiVideo() {
|
||
jitsiVideoOff = !jitsiVideoOff;
|
||
console.log('Video off:', jitsiVideoOff);
|
||
}
|
||
|
||
// Share screen
|
||
function shareJitsiScreen() {
|
||
console.log('Screen share requested - use Jitsi controls');
|
||
alert('Bitte nutzen Sie die Bildschirmfreigabe-Funktion in der Jitsi-Oberfläche');
|
||
}
|
||
|
||
// Copy meeting link
|
||
function copyMeetingLink() {
|
||
if (currentJitsiMeetingUrl) {
|
||
navigator.clipboard.writeText(currentJitsiMeetingUrl).then(() => {
|
||
alert('Meeting-Link kopiert: ' + currentJitsiMeetingUrl);
|
||
}).catch(err => {
|
||
console.error('Failed to copy:', err);
|
||
prompt('Meeting-Link:', currentJitsiMeetingUrl);
|
||
});
|
||
} else {
|
||
alert('Kein aktives Meeting');
|
||
}
|
||
}
|
||
|
||
// Leave meeting
|
||
function leaveJitsiMeeting() {
|
||
console.log('Leaving meeting...');
|
||
|
||
const placeholder = document.getElementById('jitsi-placeholder');
|
||
const iframeContainer = document.getElementById('jitsi-iframe-container');
|
||
const controls = document.getElementById('jitsi-controls');
|
||
|
||
if (iframeContainer) {
|
||
iframeContainer.innerHTML = '';
|
||
iframeContainer.style.display = 'none';
|
||
}
|
||
if (placeholder) placeholder.style.display = 'flex';
|
||
if (controls) controls.style.display = 'none';
|
||
|
||
// Reset status
|
||
const statusPill = document.getElementById('jitsi-connection-status');
|
||
if (statusPill) {
|
||
statusPill.textContent = 'Bereit';
|
||
statusPill.style.background = '#10b981';
|
||
}
|
||
|
||
// Reset participants
|
||
const participantsList = document.getElementById('jitsi-participants');
|
||
if (participantsList) {
|
||
participantsList.innerHTML = 'Keine aktive Konferenz';
|
||
}
|
||
|
||
currentJitsiMeetingUrl = null;
|
||
}
|
||
|
||
// ============================================
|
||
// MATRIX MESSENGER MODULE (Stubs)
|
||
// ============================================
|
||
|
||
// Quick meeting from messenger panel
|
||
function startQuickMeeting() {
|
||
showVideoPanel();
|
||
setTimeout(() => startInstantMeeting(), 100);
|
||
}
|
||
|
||
// Create class room
|
||
function createClassRoom() {
|
||
console.log('Creating class room...');
|
||
alert('Klassen-Info-Raum wird erstellt...\n\nDiese Funktion wird mit der Matrix-Integration verfügbar sein.');
|
||
}
|
||
|
||
// Schedule parent meeting
|
||
function scheduleParentMeeting() {
|
||
showVideoPanel();
|
||
// Focus on the schedule form
|
||
setTimeout(() => {
|
||
document.getElementById('schedule-title')?.focus();
|
||
}, 100);
|
||
}
|
||
|
||
// Select a room
|
||
function selectRoom(roomId) {
|
||
console.log('Selecting room:', roomId);
|
||
// Remove active from all rooms
|
||
document.querySelectorAll('.room-item').forEach(item => {
|
||
item.classList.remove('active');
|
||
});
|
||
// Add active to clicked room
|
||
event.currentTarget.classList.add('active');
|
||
}
|
||
|
||
// Send message (from messenger panel)
|
||
function sendMessage() {
|
||
const input = document.getElementById('chat-message-input');
|
||
if (input && input.value.trim()) {
|
||
console.log('Sending message:', input.value);
|
||
// Add message to chat (demo)
|
||
const chatContainer = document.querySelector('#panel-messenger .chat-messages-container');
|
||
if (chatContainer) {
|
||
const msgDiv = document.createElement('div');
|
||
msgDiv.className = 'chat-message sent';
|
||
msgDiv.innerHTML = `
|
||
<div class="message-content">${input.value}</div>
|
||
<div class="message-time">${new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}</div>
|
||
`;
|
||
chatContainer.appendChild(msgDiv);
|
||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||
}
|
||
input.value = '';
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// KLAUSUR-KORREKTUR (Exam Correction) Module
|
||
// ============================================
|
||
|
||
let currentExamJob = null;
|
||
let examUploadedFiles = [];
|
||
|
||
// Handle exam file upload
|
||
function handleExamUpload(event) {
|
||
const files = event.target.files;
|
||
if (!files.length) return;
|
||
|
||
examUploadedFiles = Array.from(files);
|
||
console.log(`${examUploadedFiles.length} Dateien für Klausur-Korrektur ausgewählt`);
|
||
|
||
// Update UI
|
||
const uploadArea = document.querySelector('.correction-upload-area');
|
||
if (uploadArea) {
|
||
uploadArea.innerHTML = `
|
||
<div style="color: #10b981; font-size: 48px;">✓</div>
|
||
<h3>${examUploadedFiles.length} Datei(en) ausgewählt</h3>
|
||
<p>${examUploadedFiles.map(f => f.name).join(', ')}</p>
|
||
<button onclick="document.getElementById('exam-file-input').click()"
|
||
style="margin-top: 16px; padding: 8px 16px; background: #374151; border: none; border-radius: 8px; color: white; cursor: pointer;">
|
||
Andere Dateien wählen
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
// Enable start button
|
||
const startBtn = document.getElementById('start-correction-btn');
|
||
if (startBtn) {
|
||
startBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// Start correction job
|
||
async function startCorrectionJob() {
|
||
if (!examUploadedFiles.length) {
|
||
alert('Bitte wähle zuerst Dateien aus.');
|
||
return;
|
||
}
|
||
|
||
const subject = document.getElementById('exam-subject')?.value || 'unbekannt';
|
||
const className = document.getElementById('exam-class')?.value || '';
|
||
const title = document.getElementById('exam-title')?.value || 'Klausur';
|
||
|
||
console.log('Starte Korrektur-Job:', { subject, className, title, files: examUploadedFiles.length });
|
||
|
||
// Show pipeline progress
|
||
updatePipelineStep('upload', 'active');
|
||
|
||
// TODO: Implement actual API call to exam-service
|
||
// For now, simulate progress
|
||
simulateCorrectionPipeline();
|
||
}
|
||
|
||
// Simulate correction pipeline (placeholder)
|
||
function simulateCorrectionPipeline() {
|
||
const steps = ['upload', 'preprocess', 'ocr', 'segment', 'grade', 'review'];
|
||
let currentStep = 0;
|
||
|
||
const interval = setInterval(() => {
|
||
if (currentStep > 0) {
|
||
updatePipelineStep(steps[currentStep - 1], 'completed');
|
||
}
|
||
if (currentStep < steps.length) {
|
||
updatePipelineStep(steps[currentStep], 'active');
|
||
currentStep++;
|
||
} else {
|
||
clearInterval(interval);
|
||
showCorrectionResults();
|
||
}
|
||
}, 1500);
|
||
}
|
||
|
||
// Update pipeline step visualization
|
||
function updatePipelineStep(stepId, status) {
|
||
const step = document.querySelector(`[data-step="${stepId}"]`);
|
||
if (!step) return;
|
||
|
||
step.classList.remove('active', 'completed');
|
||
if (status === 'active') {
|
||
step.classList.add('active');
|
||
step.style.background = '#3b82f6';
|
||
step.style.color = 'white';
|
||
} else if (status === 'completed') {
|
||
step.classList.add('completed');
|
||
step.style.background = '#10b981';
|
||
step.style.color = 'white';
|
||
}
|
||
}
|
||
|
||
// Show correction results (placeholder)
|
||
function showCorrectionResults() {
|
||
const resultsPanel = document.querySelector('.correction-results');
|
||
if (resultsPanel) {
|
||
resultsPanel.innerHTML = `
|
||
<div style="padding: 24px; text-align: center;">
|
||
<div style="font-size: 64px; margin-bottom: 16px;">✓</div>
|
||
<h2>Korrektur abgeschlossen</h2>
|
||
<p style="color: #9ca3af; margin-top: 8px;">
|
||
Die OCR-Erkennung und automatische Bewertung wurde durchgeführt.
|
||
</p>
|
||
<p style="color: #9ca3af;">
|
||
Bitte überprüfe die Ergebnisse im Review-Bereich.
|
||
</p>
|
||
<button onclick="showReviewInterface()"
|
||
style="margin-top: 24px; padding: 12px 24px; background: #3b82f6; border: none; border-radius: 8px; color: white; cursor: pointer; font-size: 16px;">
|
||
Ergebnisse reviewen
|
||
</button>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// Show review interface (placeholder)
|
||
function showReviewInterface() {
|
||
console.log('Review-Interface wird geladen...');
|
||
// TODO: Implement actual review interface
|
||
}
|
||
|
||
// Export correction results
|
||
function exportCorrectionResults(format) {
|
||
console.log(`Exportiere Ergebnisse als ${format}...`);
|
||
// TODO: Implement export functionality
|
||
alert(`Export als ${format} wird in Phase 2 implementiert.`);
|
||
}
|
||
|
||
// ============================================
|
||
// LERNMATERIAL (Learning Material) Module
|
||
// ============================================
|
||
|
||
let learningSourceDocument = null;
|
||
|
||
// Generate learning material
|
||
async function generateLearningMaterial(type) {
|
||
console.log(`Generiere Lernmaterial: ${type}`);
|
||
// TODO: Implement actual generation
|
||
alert(`${type} wird in einer späteren Phase implementiert.`);
|
||
}
|
||
|
||
// ============================================
|
||
// ELTERNBRIEFE (Parent Letters) Module
|
||
// ============================================
|
||
|
||
// Generate parent letter
|
||
async function generateParentLetter(template) {
|
||
console.log(`Generiere Elternbrief mit Template: ${template}`);
|
||
// TODO: Implement actual generation
|
||
alert('Elternbriefe werden in einer späteren Phase implementiert.');
|
||
}
|
||
|
||
// Check Matrix and Jitsi service status
|
||
async function checkCommunicationStatus() {
|
||
try {
|
||
const response = await fetch(`${COMM_API_BASE}/status`);
|
||
const status = await response.json();
|
||
|
||
// Update Matrix status
|
||
const matrixStatus = document.getElementById('matrix-status');
|
||
if (status.matrix && status.matrix.healthy) {
|
||
matrixStatus.innerHTML = '● Online';
|
||
matrixStatus.style.color = '#10b981';
|
||
} else {
|
||
matrixStatus.innerHTML = '● Offline';
|
||
matrixStatus.style.color = '#ef4444';
|
||
}
|
||
|
||
// Update Jitsi status
|
||
const jitsiStatus = document.getElementById('jitsi-status');
|
||
if (status.jitsi && status.jitsi.healthy) {
|
||
jitsiStatus.innerHTML = '● Bereit';
|
||
jitsiStatus.style.color = '#10b981';
|
||
} else {
|
||
jitsiStatus.innerHTML = '● Offline';
|
||
jitsiStatus.style.color = '#ef4444';
|
||
}
|
||
|
||
// Update main status pill
|
||
const statusPill = document.getElementById('comm-status-pill');
|
||
if ((status.matrix && status.matrix.healthy) || (status.jitsi && status.jitsi.healthy)) {
|
||
statusPill.innerHTML = 'Verbunden';
|
||
statusPill.style.background = '#10b981';
|
||
} else {
|
||
statusPill.innerHTML = 'Offline';
|
||
statusPill.style.background = '#ef4444';
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to check communication status:', error);
|
||
document.getElementById('matrix-status').innerHTML = '● Fehler';
|
||
document.getElementById('jitsi-status').innerHTML = '● Fehler';
|
||
}
|
||
}
|
||
|
||
// Room Selection
|
||
function selectRoom(roomId) {
|
||
currentRoom = roomId;
|
||
|
||
// Update active state in room list
|
||
document.querySelectorAll('.room-item').forEach(item => {
|
||
item.classList.remove('active');
|
||
});
|
||
event.currentTarget.classList.add('active');
|
||
|
||
// Update room header
|
||
const roomName = event.currentTarget.querySelector('.room-name').innerText;
|
||
document.getElementById('current-room-name').innerText = roomName;
|
||
|
||
// TODO: Load room messages from Matrix
|
||
console.log('Selected room:', roomId);
|
||
}
|
||
|
||
// Start Quick Video Meeting
|
||
async function startQuickMeeting() {
|
||
try {
|
||
const displayName = 'Lehrer'; // TODO: Get from logged in user
|
||
|
||
const response = await fetch(`${COMM_API_BASE}/meetings`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${localStorage.getItem('token') || ''}`
|
||
},
|
||
body: JSON.stringify({
|
||
type: 'quick',
|
||
display_name: displayName
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
// Fallback: Open Jitsi directly
|
||
const roomName = 'breakpilot-' + Math.random().toString(36).substring(7);
|
||
openJitsiMeeting(roomName, displayName);
|
||
return;
|
||
}
|
||
|
||
const meeting = await response.json();
|
||
openJitsiMeeting(meeting.room_name, displayName);
|
||
} catch (error) {
|
||
console.error('Failed to create meeting:', error);
|
||
// Fallback
|
||
const roomName = 'breakpilot-' + Math.random().toString(36).substring(7);
|
||
openJitsiMeeting(roomName, 'Lehrer');
|
||
}
|
||
}
|
||
|
||
// Open Jitsi Meeting in embedded view
|
||
function openJitsiMeeting(roomName, displayName) {
|
||
// Show video panel
|
||
document.getElementById('video-panel').style.display = 'flex';
|
||
document.getElementById('info-panel').style.display = 'none';
|
||
|
||
const container = document.getElementById('jitsi-container');
|
||
container.innerHTML = '';
|
||
|
||
// Create iframe
|
||
const iframe = document.createElement('iframe');
|
||
iframe.src = `${JITSI_BASE_URL}/${roomName}#userInfo.displayName="${encodeURIComponent(displayName)}"&config.startWithAudioMuted=false&config.startWithVideoMuted=false`;
|
||
iframe.style.width = '100%';
|
||
iframe.style.height = '100%';
|
||
iframe.style.border = 'none';
|
||
iframe.allow = 'camera; microphone; fullscreen; display-capture; autoplay';
|
||
|
||
container.appendChild(iframe);
|
||
|
||
console.log('Opened Jitsi meeting:', roomName);
|
||
}
|
||
|
||
// Close Video
|
||
function closeVideo() {
|
||
document.getElementById('video-panel').style.display = 'none';
|
||
document.getElementById('info-panel').style.display = 'block';
|
||
document.getElementById('jitsi-container').innerHTML = '';
|
||
}
|
||
|
||
// Start Video Call from room
|
||
function startVideoCall() {
|
||
if (currentRoom) {
|
||
openJitsiMeeting('elternkanal-' + currentRoom, 'Lehrer');
|
||
} else {
|
||
startQuickMeeting();
|
||
}
|
||
}
|
||
|
||
// Toggle Mute (placeholder)
|
||
function toggleMute() {
|
||
console.log('Toggle mute');
|
||
}
|
||
|
||
// Toggle Video (placeholder)
|
||
function toggleVideo() {
|
||
console.log('Toggle video');
|
||
}
|
||
|
||
// Leave Call
|
||
function leaveCall() {
|
||
closeVideo();
|
||
}
|
||
|
||
// Create Class Info Room
|
||
async function createClassRoom() {
|
||
const className = prompt('Klassenname eingeben (z.B. 7a):');
|
||
if (!className) return;
|
||
|
||
try {
|
||
const response = await fetch(`${COMM_API_BASE}/rooms`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${localStorage.getItem('token') || ''}`
|
||
},
|
||
body: JSON.stringify({
|
||
type: 'class_info',
|
||
class_name: className,
|
||
school_name: 'BreakPilot Schule',
|
||
teacher_ids: []
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const room = await response.json();
|
||
alert(`Klassen-Info-Raum erstellt!\nRoom ID: ${room.room_id}`);
|
||
// TODO: Refresh room list
|
||
} else {
|
||
alert('Raum konnte nicht erstellt werden. Ist Matrix konfiguriert?');
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to create room:', error);
|
||
alert('Fehler beim Erstellen des Raums');
|
||
}
|
||
}
|
||
|
||
// Schedule Parent Meeting
|
||
function scheduleParentMeeting() {
|
||
const studentName = prompt('Name des Schülers:');
|
||
if (!studentName) return;
|
||
|
||
const parentName = prompt('Name der Eltern:');
|
||
if (!parentName) return;
|
||
|
||
// For now, just create a quick meeting
|
||
const roomName = `elterngespraech-${studentName.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}`;
|
||
const meetingUrl = `${JITSI_BASE_URL}/${roomName}`;
|
||
|
||
alert(`Elterngespräch geplant!\n\nSchüler: ${studentName}\nEltern: ${parentName}\n\nMeeting-Link:\n${meetingUrl}`);
|
||
}
|
||
|
||
// Send Message
|
||
function sendMessage() {
|
||
const input = document.getElementById('chat-message-input');
|
||
const message = input.value.trim();
|
||
|
||
if (!message || !currentRoom) {
|
||
return;
|
||
}
|
||
|
||
// TODO: Send via Matrix API
|
||
console.log('Sending message:', message, 'to room:', currentRoom);
|
||
|
||
// Add to UI (demo)
|
||
const chatMessages = document.getElementById('chat-messages');
|
||
const msgDiv = document.createElement('div');
|
||
msgDiv.className = 'chat-msg chat-msg-self';
|
||
msgDiv.innerHTML = `
|
||
<div class="chat-msg-header">
|
||
<span class="chat-msg-sender">Sie</span>
|
||
<span class="chat-msg-time">Jetzt</span>
|
||
</div>
|
||
<div class="chat-msg-content">${message}</div>
|
||
`;
|
||
chatMessages.appendChild(msgDiv);
|
||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||
|
||
input.value = '';
|
||
}
|
||
|
||
// Attach File (placeholder)
|
||
function attachFile() {
|
||
alert('Datei-Upload wird in einer zukünftigen Version unterstützt.');
|
||
}
|
||
|
||
// Show Room Info (placeholder)
|
||
function showRoomInfo() {
|
||
alert(`Raum: ${currentRoom || 'Nicht ausgewählt'}\n\nWeitere Raum-Informationen folgen.`);
|
||
}
|
||
|
||
// Open Notification Dialog
|
||
function openNotificationDialog() {
|
||
const type = document.getElementById('notification-type').value;
|
||
|
||
if (type === 'announcement') {
|
||
const title = prompt('Titel der Ankündigung:');
|
||
if (!title) return;
|
||
const content = prompt('Inhalt:');
|
||
if (!content) return;
|
||
alert(`Ankündigung gesendet:\n\n${title}\n${content}`);
|
||
} else if (type === 'absence') {
|
||
const student = prompt('Name des Schülers:');
|
||
if (!student) return;
|
||
const lesson = prompt('Unterrichtsstunde (1-10):');
|
||
alert(`Abwesenheitsmeldung für ${student} in Stunde ${lesson} gesendet.`);
|
||
} else if (type === 'grade') {
|
||
const student = prompt('Name des Schülers:');
|
||
if (!student) return;
|
||
const subject = prompt('Fach:');
|
||
const grade = prompt('Note:');
|
||
alert(`Notenbenachrichtigung für ${student}: ${subject} = ${grade} gesendet.`);
|
||
}
|
||
}
|
||
|
||
// Initialize sidebar click handlers
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Studio panel click handler
|
||
const sidebarStudio = document.getElementById('sidebar-studio');
|
||
if (sidebarStudio) {
|
||
sidebarStudio.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
showStudioPanel();
|
||
});
|
||
}
|
||
|
||
// Communication panel click handler
|
||
const sidebarComm = document.getElementById('sidebar-communication');
|
||
if (sidebarComm) {
|
||
sidebarComm.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
showCommunicationPanel();
|
||
});
|
||
}
|
||
|
||
// Enter key in chat input
|
||
const chatInput = document.getElementById('chat-message-input');
|
||
if (chatInput) {
|
||
chatInput.addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
sendMessage();
|
||
}
|
||
});
|
||
}
|
||
}); |