"""Companion Dashboard - JavaScript.""" def get_companion_js() -> str: """JavaScript für das Companion Dashboard.""" return """ // Companion Dashboard JavaScript let companionData = null; let currentMode = 'companion'; // Lesson Mode State let lessonSession = null; let lessonTimerInterval = null; // ==================== WebSocket Real-time (Phase 6) ==================== let lessonWebSocket = null; let wsReconnectAttempts = 0; const WS_MAX_RECONNECT_ATTEMPTS = 5; const WS_RECONNECT_DELAY = 2000; let useWebSocket = true; // Fallback zu Polling wenn false // ==================== Lesson Templates (Feature f37) ==================== let availableTemplates = []; let selectedTemplate = null; let currentPhaseDurations = { einstieg: 8, erarbeitung: 20, sicherung: 10, transfer: 7, reflexion: 5 }; // Templates beim Laden abrufen async function loadLessonTemplates() { try { const response = await fetch('/api/classroom/templates?teacher_id=demo-teacher&include_system=true'); if (response.ok) { const data = await response.json(); availableTemplates = data.templates || []; renderTemplateOptions(); } } catch (error) { console.log('Templates konnten nicht geladen werden:', error); // Fallback: Nur lokale System-Templates anzeigen renderDefaultTemplateOptions(); } } function renderTemplateOptions() { const systemGroup = document.getElementById('systemTemplatesGroup'); const myGroup = document.getElementById('myTemplatesGroup'); if (!systemGroup) return; // System-Templates const systemTemplates = availableTemplates.filter(t => t.is_system_template); systemGroup.innerHTML = systemTemplates.map(t => `` ).join(''); // Eigene Templates const myTemplates = availableTemplates.filter(t => !t.is_system_template && t.teacher_id === 'demo-teacher'); if (myTemplates.length > 0 && myGroup) { myGroup.style.display = ''; myGroup.innerHTML = myTemplates.map(t => `` ).join(''); } } function renderDefaultTemplateOptions() { const systemGroup = document.getElementById('systemTemplatesGroup'); if (!systemGroup) return; // Hardcoded fallback wenn API nicht verfuegbar systemGroup.innerHTML = ` `; } function applyLessonTemplate() { const select = document.getElementById('lessonTemplate'); const option = select.options[select.selectedIndex]; const templateInfo = document.getElementById('templateInfo'); const templateDuration = document.getElementById('templateDuration'); if (!option.value) { selectedTemplate = null; if (templateInfo) templateInfo.style.display = 'none'; // Auf Standardwerte zuruecksetzen currentPhaseDurations = { einstieg: 8, erarbeitung: 20, sicherung: 10, transfer: 7, reflexion: 5 }; } else { selectedTemplate = option.value; const durations = option.dataset.durations; const total = option.dataset.total; if (durations) { currentPhaseDurations = JSON.parse(durations); } if (templateInfo && templateDuration) { templateInfo.style.display = 'flex'; templateDuration.textContent = `Gesamtdauer: ${total} Minuten`; } // Fach aus Template uebernehmen wenn vorhanden const template = availableTemplates.find(t => t.template_id === option.value); if (template && template.subject) { const subjectSelect = document.getElementById('lessonSubject'); for (let i = 0; i < subjectSelect.options.length; i++) { if (subjectSelect.options[i].value === template.subject) { subjectSelect.selectedIndex = i; break; } } } // Thema aus Template uebernehmen wenn vorhanden und Feld leer if (template && template.default_topic) { const topicInput = document.getElementById('lessonTopic'); if (topicInput && !topicInput.value) { topicInput.value = template.default_topic; } } } updatePhaseDurationsPreview(); } function updatePhaseDurationsPreview() { const previewPhases = document.getElementById('previewPhases'); const previewTotal = document.getElementById('previewTotal'); if (!previewPhases) return; const phases = [ { key: 'einstieg', label: 'E' }, { key: 'erarbeitung', label: 'A' }, { key: 'sicherung', label: 'S' }, { key: 'transfer', label: 'T' }, { key: 'reflexion', label: 'R' } ]; previewPhases.innerHTML = phases.map(p => `${p.label}: ${currentPhaseDurations[p.key] || 0}` ).join(''); const total = Object.values(currentPhaseDurations).reduce((sum, v) => sum + v, 0); if (previewTotal) { previewTotal.textContent = `Gesamt: ${total} Min`; } } // ==================== Offline Timer Fallback (Feature f35) ==================== let offlineTimerInterval = null; let lastKnownTimer = null; let isOffline = false; // Netzwerk-Status ueberwachen window.addEventListener('online', () => { isOffline = false; showToast('Verbindung wiederhergestellt'); // Bei Reconnect sofort Timer vom Server holen if (lessonSession && !lessonSession.is_ended) { syncTimerFromServer(); } }); window.addEventListener('offline', () => { isOffline = true; showToast('Offline - Timer laeuft lokal weiter'); startOfflineTimer(); }); function startOfflineTimer() { if (offlineTimerInterval) clearInterval(offlineTimerInterval); if (!lastKnownTimer || !lessonSession) return; // Client-seitiger Timer-Countdown jede Sekunde offlineTimerInterval = setInterval(() => { if (!isOffline || !lastKnownTimer || lessonSession?.is_paused) { return; } // Remaining Zeit decrementieren if (lastKnownTimer.remaining_seconds > 0) { lastKnownTimer.remaining_seconds--; lastKnownTimer.elapsed_seconds++; } else { // Overtime lastKnownTimer.overtime_seconds = (lastKnownTimer.overtime_seconds || 0) + 1; lastKnownTimer.overtime = true; } // Prozente neu berechnen if (lastKnownTimer.total_seconds > 0) { lastKnownTimer.percentage = Math.round( (lastKnownTimer.remaining_seconds / lastKnownTimer.total_seconds) * 100 ); lastKnownTimer.percentage_remaining = lastKnownTimer.percentage; lastKnownTimer.percentage_elapsed = 100 - lastKnownTimer.percentage; } // Zeit formatieren lastKnownTimer.remaining_formatted = formatSeconds(lastKnownTimer.remaining_seconds); if (lastKnownTimer.overtime) { lastKnownTimer.overtime_formatted = formatSeconds(lastKnownTimer.overtime_seconds); } // Warning-Status pruefen lastKnownTimer.warning = lastKnownTimer.remaining_seconds <= 120 && !lastKnownTimer.overtime; // UI aktualisieren updateTimerUI(lastKnownTimer); }, 1000); } function stopOfflineTimer() { if (offlineTimerInterval) { clearInterval(offlineTimerInterval); offlineTimerInterval = null; } } function formatSeconds(seconds) { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } function updateTimerUI(timer) { document.getElementById('lessonTimerDisplay').textContent = timer.remaining_formatted; document.getElementById('lessonProgressBar').style.width = timer.percentage_elapsed + '%'; updateVisualTimer(timer); const progressEl = document.getElementById('lessonProgressBar'); progressEl.classList.remove('warning', 'overtime'); if (timer.overtime) { progressEl.classList.add('overtime'); document.getElementById('lessonOvertimeBadge').style.display = 'inline-block'; document.getElementById('lessonOvertimeBadge').textContent = `+${timer.overtime_formatted} Overtime`; } else if (timer.warning) { progressEl.classList.add('warning'); document.getElementById('lessonOvertimeBadge').style.display = 'none'; } else { document.getElementById('lessonOvertimeBadge').style.display = 'none'; } } async function syncTimerFromServer() { if (!lessonSession) return; try { const res = await fetch(`/api/classroom/sessions/${lessonSession.session_id}/timer`); const timer = await res.json(); lastKnownTimer = timer; lessonSession.timer = timer; updateTimerUI(timer); } catch (error) { console.error('Timer sync error:', error); } } async function loadCompanionDashboard() { try { const response = await fetch('/api/state/dashboard?teacher_id=demo-teacher'); companionData = await response.json(); renderDashboard(); } catch (error) { console.error('Error loading dashboard:', error); showError('Dashboard konnte nicht geladen werden'); } } // Kontext-Info aus /v1/context laden async function loadContextInfo() { try { const response = await fetch('/api/classroom/v1/context?teacher_id=demo-teacher'); if (!response.ok) { console.warn('Context API not available'); return; } const ctx = await response.json(); // Schuljahr const yearEl = document.getElementById('ctxYear'); if (yearEl && ctx.school_year?.id) { yearEl.textContent = ctx.school_year.id; } // Bundesland const stateEl = document.getElementById('ctxState'); if (stateEl && ctx.school?.federal_state) { const stateNames = { 'BY': 'Bayern', 'BW': 'Baden-Württemberg', 'BE': 'Berlin', 'BB': 'Brandenburg', 'HB': 'Bremen', 'HH': 'Hamburg', 'HE': 'Hessen', 'MV': 'Mecklenburg-Vorpommern', 'NI': 'Niedersachsen', 'NW': 'Nordrhein-Westfalen', 'RP': 'Rheinland-Pfalz', 'SL': 'Saarland', 'SN': 'Sachsen', 'ST': 'Sachsen-Anhalt', 'SH': 'Schleswig-Holstein', 'TH': 'Thüringen' }; stateEl.textContent = stateNames[ctx.school.federal_state] || ctx.school.federal_state; } // Woche const weekEl = document.getElementById('ctxWeek'); if (weekEl && ctx.school_year?.current_week) { weekEl.textContent = ctx.school_year.current_week; } } catch (error) { console.warn('Error loading context info:', error); } } // Suggestions von /v1/suggestions API laden async function loadSuggestionsFromAPI() { try { const response = await fetch('/api/classroom/v1/suggestions?teacher_id=demo-teacher'); if (!response.ok) { console.warn('Suggestions API not available'); return; } const data = await response.json(); const container = document.getElementById('suggestionsList'); if (!container) return; if (!data.suggestions || data.suggestions.length === 0) { container.innerHTML = `
check_circle

Alles erledigt!

Keine offenen Aufgaben. Gute Arbeit!

`; return; } // Tone zu Priority-Klasse mappen const toneToClass = { 'hint': 'high', 'suggestion': 'medium', 'optional': 'low' }; container.innerHTML = data.suggestions.map(s => `
${s.icon || 'lightbulb'}
${s.title}
${s.description}
${s.badge ? `
${s.badge}
` : ''}
Los arrow_forward
`).join(''); } catch (error) { console.warn('Error loading suggestions from API:', error); } } // ==================== Onboarding Flow (Phase 8f) ==================== let onboardingStep = 1; let onboardingData = { federal_state: '', school_type: '' }; async function checkOnboardingNeeded() { try { const response = await fetch('/api/classroom/v1/context?teacher_id=demo-teacher'); if (!response.ok) return; const ctx = await response.json(); // Wenn Onboarding nicht abgeschlossen, Modal zeigen if (!ctx.flags?.onboarding_completed) { showOnboardingModal(); } } catch (error) { console.warn('Could not check onboarding status:', error); } } function showOnboardingModal() { document.getElementById('onboardingModalOverlay').classList.add('active'); onboardingStep = 1; updateOnboardingUI(); } function hideOnboardingModal() { document.getElementById('onboardingModalOverlay').classList.remove('active'); } function updateOnboardingUI() { // Steps anzeigen/verstecken for (let i = 1; i <= 3; i++) { const stepEl = document.getElementById(`onboardingStep${i}`); const dotEl = document.getElementById(`onboardingDot${i}`); if (stepEl) { stepEl.classList.toggle('active', i === onboardingStep); } if (dotEl) { dotEl.classList.remove('active', 'done'); if (i < onboardingStep) { dotEl.classList.add('done'); } else if (i === onboardingStep) { dotEl.classList.add('active'); } } } } function onboardingNext(currentStep) { if (currentStep === 1) { const state = document.getElementById('onboardingFederalState').value; if (!state) { alert('Bitte wählen Sie ein Bundesland.'); return; } onboardingData.federal_state = state; onboardingStep = 2; } else if (currentStep === 2) { const type = document.getElementById('onboardingSchoolType').value; if (!type) { alert('Bitte wählen Sie eine Schulart.'); return; } onboardingData.school_type = type; onboardingStep = 3; // Zusammenfassung aktualisieren const stateSelect = document.getElementById('onboardingFederalState'); const typeSelect = document.getElementById('onboardingSchoolType'); document.getElementById('onboardingSummaryState').textContent = stateSelect.options[stateSelect.selectedIndex].text; document.getElementById('onboardingSummaryType').textContent = typeSelect.options[typeSelect.selectedIndex].text; } updateOnboardingUI(); } function onboardingBack(currentStep) { if (currentStep > 1) { onboardingStep = currentStep - 1; updateOnboardingUI(); } } async function completeOnboarding() { try { // 1. Kontext speichern (PUT statt PATCH) const saveResponse = await fetch('/api/classroom/v1/context?teacher_id=demo-teacher', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ federal_state: onboardingData.federal_state, school_type: onboardingData.school_type }) }); if (!saveResponse.ok) { throw new Error('Kontext konnte nicht gespeichert werden'); } // 2. Onboarding abschließen const completeResponse = await fetch('/api/classroom/v1/context/complete-onboarding?teacher_id=demo-teacher', { method: 'POST' }); if (!completeResponse.ok) { throw new Error('Onboarding konnte nicht abgeschlossen werden'); } // Modal schließen und Daten neu laden hideOnboardingModal(); loadContextInfo(); loadSuggestionsFromAPI(); showToast('Einrichtung abgeschlossen!'); } catch (error) { console.error('Onboarding error:', error); alert('Fehler beim Speichern. Bitte versuchen Sie es erneut.'); } } function renderDashboard() { if (!companionData) return; // Phase Badge document.getElementById('phaseBadge').textContent = companionData.context.phase_display_name; // Phase Timeline renderPhaseTimeline(); // Stats document.getElementById('statClasses').textContent = companionData.stats.classes_count || 0; document.getElementById('statStudents').textContent = companionData.stats.students_count || 0; document.getElementById('statUnits').textContent = companionData.stats.learning_units_created || 0; document.getElementById('statGrades').textContent = companionData.stats.grades_entered || 0; // Progress const progress = companionData.progress; document.getElementById('progressPercent').textContent = Math.round(progress.percentage) + '%'; document.getElementById('progressBar').style.width = progress.percentage + '%'; document.getElementById('progressMilestones').textContent = `${progress.completed} von ${progress.total} Meilensteinen erreicht`; // Suggestions renderSuggestions(); // Events renderEvents(); } function renderPhaseTimeline() { const container = document.getElementById('phaseTimeline'); container.innerHTML = ''; companionData.phases.forEach(phase => { const step = document.createElement('div'); step.className = 'phase-step'; step.onclick = () => console.log('Phase clicked:', phase.phase); const dot = document.createElement('div'); dot.className = 'phase-dot'; if (phase.is_completed) { dot.classList.add('completed'); dot.innerHTML = 'check'; } else if (phase.is_current) { dot.classList.add('current'); dot.innerHTML = 'circle'; } const label = document.createElement('div'); label.className = 'phase-label'; if (phase.is_current) label.classList.add('current'); label.textContent = phase.short_name; step.appendChild(dot); step.appendChild(label); container.appendChild(step); }); } function renderSuggestions() { const container = document.getElementById('suggestionsList'); if (!companionData.suggestions || companionData.suggestions.length === 0) { container.innerHTML = `
check_circle

Alles erledigt!

Keine offenen Aufgaben. Gute Arbeit!

`; return; } container.innerHTML = companionData.suggestions.map(s => `
${s.icon}
${s.title}
${s.description}
schedule ca. ${s.estimated_time} Min.
Los arrow_forward
`).join(''); } function renderEvents() { const container = document.getElementById('eventsList'); const card = document.getElementById('eventsCard'); if (!companionData.upcoming_events || companionData.upcoming_events.length === 0) { card.style.display = 'none'; return; } card.style.display = 'block'; const getEventIcon = (type) => { const icons = { 'exam': 'quiz', 'parent_meeting': 'groups', 'deadline': 'alarm', 'default': 'event' }; return icons[type] || icons.default; }; container.innerHTML = companionData.upcoming_events.map(e => `
${getEventIcon(e.type)}
${e.title}
${formatDate(e.date)}
${e.in_days === 0 ? 'Heute' : `In ${e.in_days} Tagen`}
`).join(''); } function formatDate(isoString) { const date = new Date(isoString); return date.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric', month: 'short' }); } function navigateTo(target) { console.log('Navigate to:', target); // In echter App: window.location.href = target; // Oder: router.push(target); // Für Demo: Zeige Nachricht showToast(`Navigiere zu: ${target}`); } function setMode(mode) { currentMode = mode; // Mode Buttons aktualisieren document.getElementById('modeCompanion').classList.toggle('active', mode === 'companion'); document.getElementById('modeLesson').classList.toggle('active', mode === 'lesson'); document.getElementById('modeClassic').classList.toggle('active', mode === 'classic'); // companionDashboard immer sichtbar lassen (fuer Mode-Toggle) document.getElementById('companionDashboard').style.display = 'block'; document.getElementById('lessonContainer').classList.toggle('active', mode === 'lesson'); // Companion-Inhalte (ausser Mode-Toggle) steuern const companionContent = document.querySelectorAll('#companionDashboard > *:not(.mode-toggle)'); if (mode === 'lesson') { // Nur Mode-Toggle sichtbar, Rest ausblenden companionContent.forEach(el => el.style.display = 'none'); // Templates laden (Feature f37) loadLessonTemplates(); } else if (mode === 'companion') { // Alles im Companion anzeigen companionContent.forEach(el => el.style.display = ''); } else if (mode === 'classic') { // Klassisch: Zur Studio-Ansicht wechseln companionContent.forEach(el => el.style.display = 'none'); showToast('Wechsle zum Studio...'); window.location.href = '/studio'; } } function showToast(message) { // Einfache Toast-Nachricht const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #1a1a2e; color: white; padding: 12px 24px; border-radius: 8px; z-index: 1000; animation: fadeIn 0.3s ease; `; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 2000); } function showError(message) { document.getElementById('suggestionsList').innerHTML = `
error

Fehler

${message}

`; } // Milestone abschließen async function completeMilestone(milestone) { try { const response = await fetch('/api/state/milestone?teacher_id=demo-teacher', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ milestone }) }); const result = await response.json(); if (result.success) { showToast(`Meilenstein "${milestone}" abgeschlossen!`); if (result.new_phase) { showToast(`Neue Phase: ${result.new_phase}`); } loadCompanionDashboard(); // Reload } } catch (error) { console.error('Error completing milestone:', error); } } // ============================================ // LESSON MODE - Unterrichtsstunden-Steuerung // ============================================ async function startNewLesson() { const classId = document.getElementById('lessonClassId').value; const subject = document.getElementById('lessonSubject').value; const topic = document.getElementById('lessonTopic').value || null; try { // Session erstellen (mit Template-Phasendauern, Feature f37) const createRes = await fetch('/api/classroom/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ teacher_id: 'demo-teacher', class_id: classId, subject: subject, topic: topic, phase_durations: currentPhaseDurations }) }); lessonSession = await createRes.json(); // Stunde starten (wechselt zu Einstieg) const startRes = await fetch(`/api/classroom/sessions/${lessonSession.session_id}/start`, { method: 'POST' }); lessonSession = await startRes.json(); // UI aktualisieren showLessonActiveView(); startLessonTimerPolling(); renderLessonUI(); loadLessonSuggestions(); showToast('Stunde gestartet!'); } catch (error) { console.error('Error starting lesson:', error); showToast('Fehler beim Starten der Stunde'); } } function showLessonActiveView() { document.getElementById('lessonStartView').style.display = 'none'; document.getElementById('lessonActiveView').style.display = 'block'; document.getElementById('lessonEndedView').style.display = 'none'; } function showLessonStartView() { document.getElementById('lessonStartView').style.display = 'block'; document.getElementById('lessonActiveView').style.display = 'none'; document.getElementById('lessonEndedView').style.display = 'none'; } function showLessonEndedView() { document.getElementById('lessonStartView').style.display = 'none'; document.getElementById('lessonActiveView').style.display = 'none'; document.getElementById('lessonEndedView').style.display = 'block'; } function renderLessonUI() { if (!lessonSession) return; // Header mit Phasen-Farbe (Feature f25) const headerEl = document.querySelector('.lesson-header'); const topicText = lessonSession.topic ? ` - ${lessonSession.topic}` : ''; document.getElementById('lessonSubjectDisplay').textContent = `${lessonSession.subject} - Klasse ${lessonSession.class_id}${topicText}`; // Phasen-Farbschema anwenden (Feature f25) updatePhaseColors(lessonSession.current_phase); // Timer const timer = lessonSession.timer; document.getElementById('lessonTimerDisplay').textContent = timer.remaining_formatted; document.getElementById('lessonPhaseLabel').textContent = lessonSession.phase_display_name; // Visual Pie Timer aktualisieren (Feature f21) updateVisualTimer(timer); // Legacy Timer Styling (fuer Progress Bar) const progressEl = document.getElementById('lessonProgressBar'); progressEl.classList.remove('warning', 'overtime'); if (timer.overtime) { progressEl.classList.add('overtime'); document.getElementById('lessonOvertimeBadge').style.display = 'inline-block'; document.getElementById('lessonOvertimeBadge').textContent = `+${timer.overtime_formatted} Overtime`; } else if (timer.warning) { progressEl.classList.add('warning'); document.getElementById('lessonOvertimeBadge').style.display = 'none'; } else { document.getElementById('lessonOvertimeBadge').style.display = 'none'; } // Progress Bar progressEl.style.width = timer.percentage_elapsed + '%'; // Timeline renderLessonTimeline(); // Pause Button Status (Feature f26) updatePauseButton(lessonSession.is_paused); // Next Phase Button const btnNext = document.getElementById('btnNextPhase'); btnNext.disabled = lessonSession.is_ended; } function renderLessonTimeline() { const container = document.getElementById('lessonTimeline'); container.innerHTML = ''; lessonSession.phases.forEach(phase => { const step = document.createElement('div'); step.className = 'lesson-phase-step'; const dot = document.createElement('div'); dot.className = 'lesson-phase-dot'; // Phasen-Farbe hinzufuegen (Feature f25) dot.classList.add(`phase-${phase.phase}`); if (phase.is_completed) { dot.classList.add('completed'); dot.innerHTML = 'check'; } else if (phase.is_current) { dot.classList.add('current'); dot.innerHTML = `${phase.icon}`; } else { dot.innerHTML = `${phase.icon}`; } const name = document.createElement('div'); name.className = 'lesson-phase-name'; if (phase.is_current) name.classList.add('current'); name.textContent = phase.display_name; const duration = document.createElement('div'); duration.className = 'lesson-phase-duration'; duration.textContent = `${phase.duration_minutes} Min`; step.appendChild(dot); step.appendChild(name); step.appendChild(duration); container.appendChild(step); }); } async function loadLessonSuggestions() { if (!lessonSession) return; try { const res = await fetch(`/api/classroom/sessions/${lessonSession.session_id}/suggestions?limit=4`); const data = await res.json(); const container = document.getElementById('lessonSuggestionsList'); if (!data.suggestions || data.suggestions.length === 0) { container.innerHTML = '

Keine Vorschlaege fuer diese Phase.

'; return; } // Subject-Badge anzeigen wenn fachspezifisch (Feature f18) const subjectBadge = (s) => { if (s.subjects && s.subjects.length > 0 && data.subject) { const normalizedSubject = data.subject.toLowerCase(); if (s.subjects.some(subj => subj.toLowerCase() === normalizedSubject)) { return `${data.subject}`; } } return ''; }; container.innerHTML = data.suggestions.map(s => `
${s.icon}
${s.title} ${subjectBadge(s)}
${s.description}
${s.estimated_minutes} Min
`).join(''); } catch (error) { console.error('Error loading suggestions:', error); } } // ==================== Quick Actions (Feature f26) ==================== async function lessonExtendTime(minutes) { if (!lessonSession) return; try { const res = await fetch(`/api/classroom/sessions/${lessonSession.session_id}/extend`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ minutes: minutes }) }); if (!res.ok) { throw new Error('Extend failed'); } lessonSession = await res.json(); renderLessonUI(); showToast(`+${minutes} Min hinzugefuegt`); } catch (error) { console.error('Error extending time:', error); showToast('Fehler beim Verlaengern'); } } async function lessonTogglePause() { if (!lessonSession) return; try { const res = await fetch(`/api/classroom/sessions/${lessonSession.session_id}/pause`, { method: 'POST' }); if (!res.ok) { throw new Error('Pause toggle failed'); } lessonSession = await res.json(); // UI fuer Pause-Status aktualisieren updatePauseButton(lessonSession.is_paused); renderLessonUI(); showToast(lessonSession.is_paused ? 'Pausiert' : 'Fortgesetzt'); } catch (error) { console.error('Error toggling pause:', error); showToast('Fehler bei Pause'); } } function updatePauseButton(isPaused) { const btn = document.getElementById('btnPauseResume'); const icon = document.getElementById('pauseIcon'); const label = document.getElementById('pauseLabel'); if (!btn || !icon || !label) return; if (isPaused) { btn.classList.add('paused'); icon.textContent = 'play_arrow'; label.textContent = 'Weiter'; } else { btn.classList.remove('paused'); icon.textContent = 'pause'; label.textContent = 'Pause'; } } // ==================== Keyboard Shortcuts (Feature f34) ==================== // Space = Pause/Resume, N = Next Phase, E = Extend +5min, H = High Contrast Toggle document.addEventListener('keydown', function(e) { // Nur wenn Lesson-Modus aktiv und keine Session beendet if (!lessonSession || lessonSession.is_ended) return; // Nicht reagieren wenn in Input-Feld if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return; switch(e.code) { case 'Space': e.preventDefault(); lessonTogglePause(); break; case 'KeyN': e.preventDefault(); lessonNextPhase(); break; case 'KeyE': e.preventDefault(); lessonExtendTime(5); break; case 'KeyH': e.preventDefault(); toggleHighContrast(); break; case 'KeyA': e.preventDefault(); toggleAudio(); break; } }); function toggleHighContrast() { const container = document.querySelector('.companion-container'); if (container) { container.classList.toggle('high-contrast'); const isHighContrast = container.classList.contains('high-contrast'); showToast(isHighContrast ? 'High Contrast aktiviert' : 'High Contrast deaktiviert'); } } // ==================== Audio Cues (Feature f33) ==================== // Sanfte Toene bei Phasenwechsel und Warnungen (keine harten Alarme) let audioContext = null; let audioEnabled = true; function initAudioContext() { if (!audioContext) { try { audioContext = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { console.log('Web Audio API nicht verfuegbar'); audioEnabled = false; } } return audioContext; } function playTone(frequency, duration, type = 'sine', volume = 0.3) { if (!audioEnabled) return; const ctx = initAudioContext(); if (!ctx) return; // AudioContext aktivieren falls suspended if (ctx.state === 'suspended') { ctx.resume(); } const oscillator = ctx.createOscillator(); const gainNode = ctx.createGain(); oscillator.connect(gainNode); gainNode.connect(ctx.destination); oscillator.type = type; oscillator.frequency.setValueAtTime(frequency, ctx.currentTime); // Sanftes Ein- und Ausblenden gainNode.gain.setValueAtTime(0, ctx.currentTime); gainNode.gain.linearRampToValueAtTime(volume, ctx.currentTime + 0.05); gainNode.gain.linearRampToValueAtTime(0, ctx.currentTime + duration); oscillator.start(ctx.currentTime); oscillator.stop(ctx.currentTime + duration); } function playPhaseChangeSound() { // Sanfter aufsteigender Zwei-Ton (freundlich, nicht aufdringlich) playTone(440, 0.15, 'sine', 0.2); setTimeout(() => playTone(554, 0.2, 'sine', 0.2), 100); } function playWarningSound() { // Sanfter einzelner Ton (hinweisend, nicht alarmierend) playTone(392, 0.3, 'sine', 0.15); } function playEndSound() { // Sanfte absteigende Tonfolge (abschliessend) playTone(523, 0.15, 'sine', 0.2); setTimeout(() => playTone(440, 0.15, 'sine', 0.2), 100); setTimeout(() => playTone(349, 0.25, 'sine', 0.2), 200); } function toggleAudio() { audioEnabled = !audioEnabled; showToast(audioEnabled ? 'Audio aktiviert' : 'Audio deaktiviert'); } async function lessonNextPhase() { if (!lessonSession) return; try { const res = await fetch(`/api/classroom/sessions/${lessonSession.session_id}/next-phase`, { method: 'POST' }); lessonSession = await res.json(); renderLessonUI(); loadLessonSuggestions(); showToast(`Phase: ${lessonSession.phase_display_name}`); // Audio Cue abspielen (Feature f33) if (lessonSession.is_ended) { playEndSound(); stopLessonTimerPolling(); showLessonEndedWithSummary(); } else { playPhaseChangeSound(); } } catch (error) { console.error('Error advancing phase:', error); showToast('Fehler beim Phasenwechsel'); } } async function lessonEnd() { if (!lessonSession) return; if (!confirm('Stunde wirklich beenden?')) return; try { const res = await fetch(`/api/classroom/sessions/${lessonSession.session_id}/end`, { method: 'POST' }); lessonSession = await res.json(); playEndSound(); // Audio Cue (Feature f33) stopLessonTimerPolling(); showLessonEndedWithSummary(); showToast('Stunde beendet!'); } catch (error) { console.error('Error ending lesson:', error); showToast('Fehler beim Beenden'); } } function showLessonEndedWithSummary() { showLessonEndedView(); const topicText = lessonSession.topic ? ` - ${lessonSession.topic}` : ''; document.getElementById('lessonEndedTopic').textContent = `${lessonSession.subject} - Klasse ${lessonSession.class_id}${topicText}`; // Dauer berechnen if (lessonSession.lesson_started_at && lessonSession.lesson_ended_at) { const start = new Date(lessonSession.lesson_started_at); const end = new Date(lessonSession.lesson_ended_at); const durationSec = Math.round((end - start) / 1000); const mins = Math.floor(durationSec / 60); const secs = durationSec % 60; document.getElementById('summaryDuration').textContent = `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } // Phasen zaehlen const completedPhases = lessonSession.phases.filter(p => p.is_completed).length; document.getElementById('summaryPhases').textContent = `${completedPhases}/5`; // Hausaufgaben und Materialien laden (Features f19, f20) loadHomeworkList(); loadMaterialsList(); // Due Date auf morgen vorbelegen const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); const dueDateInput = document.getElementById('homeworkDueDate'); if (dueDateInput) { dueDateInput.value = tomorrow.toISOString().split('T')[0]; } // Analytics und Reflection laden (Phase 5) loadSessionAnalytics(); loadExistingReflection(); // Reflection-Formular zuruecksetzen currentReflectionRating = null; const stars = document.querySelectorAll('.star-btn'); stars.forEach(star => star.classList.remove('active')); const reflectionNotes = document.getElementById('reflectionNotes'); if (reflectionNotes) reflectionNotes.value = ''; const reflectionNext = document.getElementById('reflectionNextLesson'); if (reflectionNext) reflectionNext.value = ''; const saveBtn = document.querySelector('.reflection-save-btn'); if (saveBtn) { saveBtn.disabled = false; saveBtn.innerHTML = 'save Reflexion speichern'; } } function resetLesson() { lessonSession = null; stopLessonTimerPolling(); showLessonStartView(); // Formular zuruecksetzen document.getElementById('lessonTopic').value = ''; } // ==================== WebSocket Real-time Connection (Phase 6) ==================== function connectWebSocket() { if (!lessonSession || !useWebSocket) return; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/api/classroom/ws/${lessonSession.session_id}`; try { lessonWebSocket = new WebSocket(wsUrl); lessonWebSocket.onopen = () => { console.log('WebSocket connected'); wsReconnectAttempts = 0; isOffline = false; stopOfflineTimer(); // Zeige Verbindungsstatus updateConnectionStatus('connected'); }; lessonWebSocket.onmessage = (event) => { try { const message = JSON.parse(event.data); handleWebSocketMessage(message); } catch (e) { console.error('WebSocket message parse error:', e); } }; lessonWebSocket.onclose = (event) => { console.log('WebSocket closed:', event.code, event.reason); lessonWebSocket = null; if (lessonSession && !lessonSession.is_ended) { // Versuche Reconnect if (wsReconnectAttempts < WS_MAX_RECONNECT_ATTEMPTS) { wsReconnectAttempts++; updateConnectionStatus('reconnecting'); setTimeout(connectWebSocket, WS_RECONNECT_DELAY); } else { // Fallback zu Polling console.log('WebSocket max reconnects reached, falling back to polling'); useWebSocket = false; updateConnectionStatus('polling'); startLessonTimerPollingFallback(); } } }; lessonWebSocket.onerror = (error) => { console.error('WebSocket error:', error); updateConnectionStatus('error'); }; } catch (error) { console.error('WebSocket connection failed:', error); useWebSocket = false; startLessonTimerPollingFallback(); } } function disconnectWebSocket() { if (lessonWebSocket) { lessonWebSocket.close(); lessonWebSocket = null; } wsReconnectAttempts = 0; } function handleWebSocketMessage(message) { switch (message.type) { case 'connected': console.log('WebSocket initial data:', message.data); if (message.data.timer) { handleTimerUpdate(message.data.timer); } if (message.data.client_count > 1) { showToast(`${message.data.client_count} Geraete verbunden`); } break; case 'timer_update': handleTimerUpdate(message.data); break; case 'phase_change': handlePhaseChange(message.data); break; case 'session_ended': handleSessionEnded(message.data); break; case 'pong': // Keepalive response break; case 'error': console.error('WebSocket server error:', message.data); break; default: console.log('Unknown WebSocket message:', message); } } function handleTimerUpdate(timerData) { if (!lessonSession) return; // Timer in Session und Backup speichern lessonSession.timer = timerData; lastKnownTimer = { ...timerData }; // Offline-Status zuruecksetzen isOffline = false; stopOfflineTimer(); // UI aktualisieren updateTimerUI(timerData); // Warning-Logik if (timerData.warning && !timerData.overtime) { if (!lessonSession._warningShown) { showToast('Noch 2 Minuten in dieser Phase!'); playWarningSound(); lessonSession._warningShown = true; } } else if (!timerData.warning) { lessonSession._warningShown = false; } } function handlePhaseChange(phaseData) { console.log('Phase changed:', phaseData); showToast(`Phase: ${phaseData.phase_info?.phase_display_name || phaseData.new_phase}`); playPhaseSound(); // Session-Daten neu laden refreshLessonSession(); } function handleSessionEnded(data) { console.log('Session ended via WebSocket'); disconnectWebSocket(); refreshLessonSession(); } function updateConnectionStatus(status) { const statusIndicator = document.getElementById('wsConnectionStatus'); if (!statusIndicator) return; const statusConfig = { 'connected': { icon: 'wifi', color: '#22c55e', text: 'Live' }, 'reconnecting': { icon: 'sync', color: '#f59e0b', text: 'Reconnect...' }, 'polling': { icon: 'schedule', color: '#6b7280', text: 'Polling' }, 'error': { icon: 'wifi_off', color: '#ef4444', text: 'Offline' }, 'offline': { icon: 'wifi_off', color: '#ef4444', text: 'Offline' } }; const config = statusConfig[status] || statusConfig['offline']; statusIndicator.innerHTML = ` ${config.icon} ${config.text} `; } async function refreshLessonSession() { if (!lessonSession) return; try { const res = await fetch(`/api/classroom/sessions/${lessonSession.session_id}`); if (res.ok) { lessonSession = await res.json(); renderLessonUI(); if (lessonSession.is_ended) { showLessonEndedWithSummary(); } } } catch (error) { console.error('Failed to refresh session:', error); } } function sendWebSocketPing() { if (lessonWebSocket && lessonWebSocket.readyState === WebSocket.OPEN) { lessonWebSocket.send(JSON.stringify({ type: 'ping' })); } } // WebSocket Keepalive alle 30 Sekunden setInterval(sendWebSocketPing, 30000); // ==================== Timer Polling (Fallback) ==================== function startLessonTimerPolling() { // Phase 6: Versuche zuerst WebSocket if (useWebSocket) { connectWebSocket(); return; } // Fallback: Polling alle 5 Sekunden startLessonTimerPollingFallback(); } function startLessonTimerPollingFallback() { // Timer alle 5 Sekunden aktualisieren lessonTimerInterval = setInterval(async () => { if (!lessonSession || lessonSession.is_ended) { stopLessonTimerPolling(); return; } try { const res = await fetch(`/api/classroom/sessions/${lessonSession.session_id}/timer`); const timer = await res.json(); // Offline Timer stoppen wenn Server antwortet (Feature f35) stopOfflineTimer(); isOffline = false; // Timer in Session und Backup speichern (Feature f35) lessonSession.timer = timer; lastKnownTimer = { ...timer }; // UI aktualisieren updateTimerUI(timer); // Warning-Logik if (timer.warning && !timer.overtime) { if (!lessonSession._warningShown) { showToast('Noch 2 Minuten in dieser Phase!'); playWarningSound(); // Audio Cue (Feature f33) lessonSession._warningShown = true; } } else if (!timer.warning) { lessonSession._warningShown = false; } } catch (error) { console.error('Timer polling error:', error); // Bei Verbindungsfehler: Offline Timer starten (Feature f35) if (!isOffline && lastKnownTimer) { isOffline = true; showToast('Verbindung unterbrochen - Timer laeuft lokal'); startOfflineTimer(); } } }, 5000); } function stopLessonTimerPolling() { // Phase 6: WebSocket trennen disconnectWebSocket(); // Polling stoppen if (lessonTimerInterval) { clearInterval(lessonTimerInterval); lessonTimerInterval = null; } stopOfflineTimer(); // Feature f35: Offline Timer auch stoppen } // ==================== PHASEN-FARBSCHEMA (Feature f25) ==================== const PHASE_COLORS = { 'einstieg': '#4A90E2', // Warmes Blau 'erarbeitung': '#F5A623', // Orange 'sicherung': '#7ED321', // Gruen 'transfer': '#9013FE', // Lila 'reflexion': '#6B7280', // Grau 'not_started': '#6C1B1B', // Default Weinrot 'ended': '#6C1B1B' // Default Weinrot }; function updatePhaseColors(currentPhase) { const headerEl = document.querySelector('.lesson-header'); if (!headerEl) return; // Alle Phasen-Klassen entfernen headerEl.classList.remove( 'phase-einstieg', 'phase-erarbeitung', 'phase-sicherung', 'phase-transfer', 'phase-reflexion', 'phase-not_started', 'phase-ended' ); // Aktuelle Phasen-Klasse hinzufuegen if (currentPhase) { headerEl.classList.add(`phase-${currentPhase}`); } } // ==================== VISUAL PIE TIMER (Feature f21) ==================== // Kreis-Umfang bei r=42: 2 * PI * 42 = 263.89 const TIMER_CIRCUMFERENCE = 263.89; function updateVisualTimer(timer) { const progressEl = document.getElementById('visualTimerProgress'); if (!progressEl) return; // Prozent der verbleibenden Zeit berechnen const percentRemaining = timer.percentage || 0; const dashOffset = TIMER_CIRCUMFERENCE * (1 - percentRemaining / 100); // SVG Stroke-Dashoffset setzen (animiert den Kreis) progressEl.style.strokeDashoffset = dashOffset; // Farbe basierend auf Zeit setzen progressEl.classList.remove('time-plenty', 'time-warning', 'time-critical', 'time-overtime'); if (timer.overtime) { progressEl.classList.add('time-overtime'); progressEl.style.strokeDashoffset = 0; // Voller Kreis bei Overtime } else if (timer.remaining_seconds <= 120) { // Kritisch: 2 Minuten oder weniger progressEl.classList.add('time-critical'); } else if (timer.remaining_seconds <= 300) { // Warnung: 5 Minuten oder weniger progressEl.classList.add('time-warning'); } else { // Normal: Mehr als 5 Minuten progressEl.classList.add('time-plenty'); } } // ==================== HAUSAUFGABEN (Feature f20) ==================== async function addHomework() { const titleInput = document.getElementById('homeworkInput'); const dueDateInput = document.getElementById('homeworkDueDate'); const title = titleInput.value.trim(); if (!title) { showToast('Bitte Hausaufgabe eingeben'); return; } if (!lessonSession) { showToast('Keine aktive Session'); return; } try { const body = { teacher_id: 'demo-teacher', class_id: lessonSession.class_id, title: title, subject: lessonSession.subject, session_id: lessonSession.session_id }; // Due date nur hinzufuegen wenn gesetzt if (dueDateInput.value) { body.due_date = dueDateInput.value + 'T23:59:00'; } const res = await fetch('/api/classroom/homework', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) { throw new Error('Fehler beim Speichern'); } // Eingabefelder leeren titleInput.value = ''; dueDateInput.value = ''; showToast('Hausaufgabe gespeichert'); loadHomeworkList(); } catch (error) { console.error('Error adding homework:', error); showToast('Fehler beim Speichern der Hausaufgabe'); } } async function loadHomeworkList() { if (!lessonSession) return; const container = document.getElementById('homeworkList'); if (!container) return; try { const res = await fetch(`/api/classroom/homework?teacher_id=demo-teacher&class_id=${lessonSession.class_id}&limit=10`); const data = await res.json(); if (!data.homework || data.homework.length === 0) { container.innerHTML = '
Keine Hausaufgaben fuer diese Klasse
'; return; } container.innerHTML = data.homework.map(hw => { const isCompleted = hw.status === 'completed'; const dueText = hw.due_date ? formatDueDate(hw.due_date) : ''; return `
${hw.title}
${dueText ? `
${dueText}
` : ''}
`; }).join(''); } catch (error) { console.error('Error loading homework:', error); container.innerHTML = '
Fehler beim Laden
'; } } function formatDueDate(isoString) { const date = new Date(isoString); const today = new Date(); today.setHours(0, 0, 0, 0); const diff = Math.ceil((date - today) / (1000 * 60 * 60 * 24)); if (diff < 0) return 'Ueberfaellig'; if (diff === 0) return 'Heute'; if (diff === 1) return 'Morgen'; if (diff <= 7) return `In ${diff} Tagen`; return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); } async function toggleHomeworkStatus(homeworkId, isCompleted) { const newStatus = isCompleted ? 'completed' : 'assigned'; try { const res = await fetch(`/api/classroom/homework/${homeworkId}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: newStatus }) }); if (!res.ok) { throw new Error('Fehler beim Aktualisieren'); } loadHomeworkList(); } catch (error) { console.error('Error updating homework status:', error); showToast('Fehler beim Aktualisieren'); loadHomeworkList(); // Reload um Checkbox-Status zu korrigieren } } async function deleteHomework(homeworkId) { if (!confirm('Hausaufgabe wirklich loeschen?')) return; try { const res = await fetch(`/api/classroom/homework/${homeworkId}?teacher_id=demo-teacher`, { method: 'DELETE' }); if (!res.ok) { throw new Error('Fehler beim Loeschen'); } showToast('Hausaufgabe geloescht'); loadHomeworkList(); } catch (error) { console.error('Error deleting homework:', error); showToast('Fehler beim Loeschen'); } } // ==================== MATERIALIEN (Feature f19) ==================== async function showAddMaterialModal() { // Einfaches Prompt fuer URL (spaeter: Modal mit mehr Optionen) const url = prompt('Material-URL eingeben:', 'https://'); if (!url || url === 'https://') return; const title = prompt('Titel des Materials:', ''); if (!title) return; if (!lessonSession) { showToast('Keine aktive Session'); return; } try { // Material erstellen const createRes = await fetch('/api/classroom/materials', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ teacher_id: 'demo-teacher', title: title, url: url, material_type: 'link', phase: lessonSession.current_phase, subject: lessonSession.subject }) }); if (!createRes.ok) { throw new Error('Fehler beim Erstellen'); } const material = await createRes.json(); // Material an Session anhaengen const attachRes = await fetch(`/api/classroom/materials/${material.material_id}/attach/${lessonSession.session_id}`, { method: 'POST' }); if (!attachRes.ok) { throw new Error('Fehler beim Anhaengen'); } showToast('Material hinzugefuegt'); loadMaterialsList(); } catch (error) { console.error('Error adding material:', error); showToast('Fehler beim Hinzufuegen'); } } async function loadMaterialsList() { if (!lessonSession) return; const container = document.getElementById('materialsList'); if (!container) return; try { const res = await fetch(`/api/classroom/materials?teacher_id=demo-teacher&session_id=${lessonSession.session_id}&limit=20`); const data = await res.json(); if (!data.materials || data.materials.length === 0) { container.innerHTML = '
Keine Materialien fuer diese Stunde
'; return; } const typeIcons = { 'document': 'description', 'link': 'link', 'video': 'play_circle', 'image': 'image', 'worksheet': 'assignment', 'presentation': 'slideshow', 'other': 'attach_file' }; container.innerHTML = data.materials.map(m => `
${typeIcons[m.material_type] || 'attach_file'}
${m.title}
${m.phase ? `
Phase: ${m.phase}
` : ''}
${m.url ? ` open_in_new ` : ''}
`).join(''); } catch (error) { console.error('Error loading materials:', error); container.innerHTML = '
Fehler beim Laden
'; } } async function deleteMaterial(materialId) { if (!confirm('Material wirklich loeschen?')) return; try { const res = await fetch(`/api/classroom/materials/${materialId}?teacher_id=demo-teacher`, { method: 'DELETE' }); if (!res.ok) { throw new Error('Fehler beim Loeschen'); } showToast('Material geloescht'); loadMaterialsList(); } catch (error) { console.error('Error deleting material:', error); showToast('Fehler beim Loeschen'); } } // ==================== ANALYTICS (Phase 5) ==================== async function loadSessionAnalytics() { if (!lessonSession) return; const grid = document.getElementById('analyticsGrid'); if (!grid) return; try { const res = await fetch(`/api/classroom/analytics/session/${lessonSession.session_id}`); if (!res.ok) { // Analytics nicht verfuegbar - zeige einfache Version renderSimpleAnalytics(); return; } const data = await res.json(); renderAnalytics(data); } catch (error) { console.log('Analytics nicht verfuegbar:', error); renderSimpleAnalytics(); } } function renderAnalytics(data) { // Phase Bars const barsContainer = document.getElementById('analyticsPhaseBars'); if (barsContainer && data.phase_statistics) { const maxDuration = Math.max( ...data.phase_statistics.map(p => Math.max(p.planned_duration_seconds, p.actual_duration_seconds)) ); barsContainer.innerHTML = data.phase_statistics.map(phase => { const plannedWidth = (phase.planned_duration_seconds / maxDuration) * 100; const actualWidth = (phase.actual_duration_seconds / maxDuration) * 100; // Farbklasse basierend auf Differenz let colorClass = 'on-time'; const diff = phase.actual_duration_seconds - phase.planned_duration_seconds; if (diff < -60) colorClass = 'under-time'; else if (diff > 180) colorClass = 'way-over'; else if (diff > 60) colorClass = 'over-time'; const diffText = phase.difference_formatted || formatDiffSeconds(diff); return `
${phase.display_name}
${diffText}
`; }).join(''); } // Overtime Summary const overtimeValue = document.getElementById('analyticsOvertimeValue'); const overtimePhases = document.getElementById('analyticsOvertimePhases'); if (overtimeValue) { overtimeValue.textContent = data.total_overtime_formatted || '00:00'; overtimeValue.style.color = data.total_overtime_seconds > 0 ? '#f59e0b' : '#10b981'; } if (overtimePhases) { overtimePhases.textContent = `${data.phases_with_overtime || 0} von ${data.total_phases} Phasen`; } } function renderSimpleAnalytics() { // Fallback wenn API nicht verfuegbar - berechne aus Session-Daten if (!lessonSession || !lessonSession.phase_history) return; const barsContainer = document.getElementById('analyticsPhaseBars'); if (!barsContainer) return; const phaseNames = { 'einstieg': 'Einstieg', 'erarbeitung': 'Erarbeitung', 'sicherung': 'Sicherung', 'transfer': 'Transfer', 'reflexion': 'Reflexion' }; let html = ''; let totalOvertime = 0; let phasesWithOvertime = 0; for (const entry of lessonSession.phase_history) { const phase = entry.phase; if (phase === 'not_started' || phase === 'ended') continue; const planned = (lessonSession.phase_durations[phase] || 0) * 60; const actual = entry.duration_seconds || 0; const diff = actual - planned; if (diff > 0) { totalOvertime += diff; phasesWithOvertime++; } let colorClass = 'on-time'; if (diff < -60) colorClass = 'under-time'; else if (diff > 180) colorClass = 'way-over'; else if (diff > 60) colorClass = 'over-time'; const maxSeconds = Math.max(planned, actual, 1); const plannedWidth = (planned / maxSeconds) * 100; const actualWidth = (actual / maxSeconds) * 100; html += `
${phaseNames[phase] || phase}
${formatDiffSeconds(diff)}
`; } barsContainer.innerHTML = html || '
Keine Daten
'; // Overtime Summary const overtimeValue = document.getElementById('analyticsOvertimeValue'); const overtimePhases = document.getElementById('analyticsOvertimePhases'); if (overtimeValue) { overtimeValue.textContent = formatSeconds(totalOvertime); overtimeValue.style.color = totalOvertime > 0 ? '#f59e0b' : '#10b981'; } if (overtimePhases) { overtimePhases.textContent = `${phasesWithOvertime} von 5 Phasen`; } } function formatDiffSeconds(seconds) { const prefix = seconds >= 0 ? '+' : ''; const absSeconds = Math.abs(seconds); const mins = Math.floor(absSeconds / 60); const secs = absSeconds % 60; return `${prefix}${mins}:${secs.toString().padStart(2, '0')}`; } // ==================== REFLECTION (Phase 5) ==================== let currentReflectionRating = null; function setReflectionRating(rating) { currentReflectionRating = rating; // Sterne aktualisieren const stars = document.querySelectorAll('.star-btn'); stars.forEach((star, index) => { if (index < rating) { star.classList.add('active'); } else { star.classList.remove('active'); } }); } async function saveReflection() { if (!lessonSession) { showToast('Keine Session vorhanden'); return; } const notes = document.getElementById('reflectionNotes')?.value || ''; const nextLesson = document.getElementById('reflectionNextLesson')?.value || ''; // Mindestens Notizen oder Rating sollten vorhanden sein if (!notes && !currentReflectionRating && !nextLesson) { showToast('Bitte Notizen oder Bewertung eingeben'); return; } try { const res = await fetch('/api/classroom/reflections', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: lessonSession.session_id, teacher_id: 'demo-teacher', notes: notes, overall_rating: currentReflectionRating, what_worked: [], // Koennte spaeter erweitert werden improvements: [], notes_for_next_lesson: nextLesson }) }); if (res.status === 409) { // Bereits vorhanden - Update stattdessen await updateExistingReflection(notes, nextLesson); return; } if (!res.ok) { throw new Error('Fehler beim Speichern'); } showToast('Reflexion gespeichert'); // Button deaktivieren const saveBtn = document.querySelector('.reflection-save-btn'); if (saveBtn) { saveBtn.disabled = true; saveBtn.innerHTML = 'check Gespeichert'; } } catch (error) { console.error('Error saving reflection:', error); showToast('Fehler beim Speichern der Reflexion'); } } async function updateExistingReflection(notes, nextLesson) { try { // Zuerst existierende Reflection holen const getRes = await fetch(`/api/classroom/reflections/session/${lessonSession.session_id}`); if (!getRes.ok) { throw new Error('Reflection nicht gefunden'); } const existing = await getRes.json(); // Update durchfuehren const updateRes = await fetch(`/api/classroom/reflections/${existing.reflection_id}?teacher_id=demo-teacher`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ notes: notes, overall_rating: currentReflectionRating, notes_for_next_lesson: nextLesson }) }); if (!updateRes.ok) { throw new Error('Update fehlgeschlagen'); } showToast('Reflexion aktualisiert'); const saveBtn = document.querySelector('.reflection-save-btn'); if (saveBtn) { saveBtn.disabled = true; saveBtn.innerHTML = 'check Gespeichert'; } } catch (error) { console.error('Error updating reflection:', error); showToast('Fehler beim Aktualisieren'); } } async function loadExistingReflection() { if (!lessonSession) return; try { const res = await fetch(`/api/classroom/reflections/session/${lessonSession.session_id}`); if (!res.ok) return; // Keine bestehende Reflection const data = await res.json(); // Formular befuellen if (data.notes) { document.getElementById('reflectionNotes').value = data.notes; } if (data.notes_for_next_lesson) { document.getElementById('reflectionNextLesson').value = data.notes_for_next_lesson; } if (data.overall_rating) { setReflectionRating(data.overall_rating); } // Button Text anpassen const saveBtn = document.querySelector('.reflection-save-btn'); if (saveBtn) { saveBtn.innerHTML = 'save Aktualisieren'; } } catch (error) { // Keine bestehende Reflection - OK } } // ==================== EXPORT (Phase 5) ==================== function exportSessionPDF() { if (!lessonSession) { showToast('Keine Session zum Exportieren'); return; } // Neues Fenster mit Export-HTML oeffnen const exportUrl = `/api/classroom/export/session/${lessonSession.session_id}`; window.open(exportUrl, '_blank'); showToast('Export geoeffnet - Strg+P fuer PDF'); } // ==================== Teacher Feedback (Phase 7) ==================== let selectedFeedbackType = 'improvement'; function openFeedbackModal() { document.getElementById('feedbackModalOverlay').classList.add('active'); document.getElementById('feedbackForm').style.display = 'block'; document.getElementById('feedbackSuccess').style.display = 'none'; // Reset form document.getElementById('feedbackTitle').value = ''; document.getElementById('feedbackDescription').value = ''; document.getElementById('feedbackName').value = ''; document.getElementById('feedbackEmail').value = ''; setFeedbackType('improvement'); } function closeFeedbackModal(event) { if (event && event.target !== event.currentTarget) return; document.getElementById('feedbackModalOverlay').classList.remove('active'); } function setFeedbackType(type) { selectedFeedbackType = type; document.querySelectorAll('.feedback-type-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.type === type); }); } async function submitFeedback() { const title = document.getElementById('feedbackTitle').value.trim(); const description = document.getElementById('feedbackDescription').value.trim(); const name = document.getElementById('feedbackName').value.trim(); const email = document.getElementById('feedbackEmail').value.trim(); // Validierung if (!title || title.length < 3) { showToast('Bitte geben Sie einen Titel ein'); return; } if (!description || description.length < 10) { showToast('Bitte beschreiben Sie Ihr Feedback genauer'); return; } const submitBtn = document.getElementById('feedbackSubmitBtn'); submitBtn.disabled = true; submitBtn.textContent = 'Wird gesendet...'; try { // Kontext sammeln const contextPhase = lessonSession?.current_phase || ''; const contextSessionId = lessonSession?.session_id || null; const response = await fetch('/api/classroom/feedback?teacher_id=demo-teacher', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: title, description: description, feedback_type: selectedFeedbackType, priority: 'medium', teacher_name: name, teacher_email: email, context_url: window.location.href, context_phase: contextPhase, context_session_id: contextSessionId, }) }); if (response.ok) { document.getElementById('feedbackForm').style.display = 'none'; document.getElementById('feedbackSuccess').style.display = 'block'; console.log('Feedback submitted successfully'); } else { const error = await response.json(); showToast('Fehler: ' + (error.detail || 'Unbekannter Fehler')); } } catch (error) { console.error('Feedback submission error:', error); showToast('Fehler beim Senden. Bitte versuchen Sie es spaeter.'); } finally { submitBtn.disabled = false; submitBtn.textContent = 'Feedback senden'; } } // ==================== TEACHER SETTINGS (Feature f16) ==================== let teacherSettings = null; const TEACHER_ID = 'demo-teacher'; // Wird spaeter durch Keycloak ersetzt /** * Laedt die Einstellungen des Lehrers vom Server. */ async function loadTeacherSettings() { try { const response = await fetch(`/api/classroom/settings/${TEACHER_ID}`); if (response.ok) { teacherSettings = await response.json(); applySettingsToUI(); applySettingsToSessionDefaults(); console.log('Teacher settings loaded:', teacherSettings); } } catch (error) { console.log('Settings konnten nicht geladen werden, verwende Defaults:', error); // Defaults werden schon im HTML gesetzt } } /** * Wendet die geladenen Einstellungen auf das Settings-Modal an. */ function applySettingsToUI() { if (!teacherSettings) return; const durations = teacherSettings.default_phase_durations || {}; if (durations.einstieg) document.getElementById('settingEinstieg').value = durations.einstieg; if (durations.erarbeitung) document.getElementById('settingErarbeitung').value = durations.erarbeitung; if (durations.sicherung) document.getElementById('settingSicherung').value = durations.sicherung; if (durations.transfer) document.getElementById('settingTransfer').value = durations.transfer; if (durations.reflexion) document.getElementById('settingReflexion').value = durations.reflexion; updateTotalMinutes(); } /** * Wendet die Einstellungen auf die Session-Defaults an. */ function applySettingsToSessionDefaults() { if (!teacherSettings || !teacherSettings.default_phase_durations) return; const durations = teacherSettings.default_phase_durations; currentPhaseDurations = { einstieg: durations.einstieg || 8, erarbeitung: durations.erarbeitung || 20, sicherung: durations.sicherung || 10, transfer: durations.transfer || 7, reflexion: durations.reflexion || 5 }; } /** * Oeffnet das Einstellungen-Modal. */ function openSettingsModal() { document.getElementById('settingsModalOverlay').classList.add('active'); updateTotalMinutes(); } /** * Schliesst das Einstellungen-Modal. */ function closeSettingsModal(event) { if (event && event.target !== event.currentTarget) return; document.getElementById('settingsModalOverlay').classList.remove('active'); } /** * Berechnet und zeigt die Gesamtminuten an. */ function updateTotalMinutes() { const einstieg = parseInt(document.getElementById('settingEinstieg').value) || 0; const erarbeitung = parseInt(document.getElementById('settingErarbeitung').value) || 0; const sicherung = parseInt(document.getElementById('settingSicherung').value) || 0; const transfer = parseInt(document.getElementById('settingTransfer').value) || 0; const reflexion = parseInt(document.getElementById('settingReflexion').value) || 0; const total = einstieg + erarbeitung + sicherung + transfer + reflexion; document.getElementById('settingsTotalMinutes').textContent = total; } // Event Listeners fuer Phasen-Inputs document.addEventListener('DOMContentLoaded', function() { const inputs = ['settingEinstieg', 'settingErarbeitung', 'settingSicherung', 'settingTransfer', 'settingReflexion']; inputs.forEach(id => { const el = document.getElementById(id); if (el) { el.addEventListener('input', updateTotalMinutes); } }); }); /** * Speichert die Einstellungen auf dem Server. */ async function saveTeacherSettings() { const saveBtn = document.getElementById('settingsSaveBtn'); saveBtn.disabled = true; const durations = { einstieg: parseInt(document.getElementById('settingEinstieg').value) || 8, erarbeitung: parseInt(document.getElementById('settingErarbeitung').value) || 20, sicherung: parseInt(document.getElementById('settingSicherung').value) || 10, transfer: parseInt(document.getElementById('settingTransfer').value) || 7, reflexion: parseInt(document.getElementById('settingReflexion').value) || 5 }; // Validierung for (const [phase, value] of Object.entries(durations)) { if (value < 1 || value > 120) { showToast(`${phase}: Wert muss zwischen 1 und 120 liegen`); saveBtn.disabled = false; return; } } try { const response = await fetch(`/api/classroom/settings/${TEACHER_ID}/durations`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ durations }) }); if (response.ok) { teacherSettings = await response.json(); applySettingsToSessionDefaults(); // Erfolgsmeldung const toast = document.getElementById('settingsSavedToast'); toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 2000); // Modal schliessen setTimeout(() => closeSettingsModal(), 500); } else { const error = await response.json(); showToast('Fehler: ' + (error.detail || 'Konnte nicht speichern')); } } catch (error) { console.error('Settings save error:', error); showToast('Fehler beim Speichern. Bitte versuchen Sie es spaeter.'); } finally { saveBtn.disabled = false; } } /** * Setzt die Einstellungen auf Standardwerte zurueck. */ function resetToDefaults() { document.getElementById('settingEinstieg').value = 8; document.getElementById('settingErarbeitung').value = 20; document.getElementById('settingSicherung').value = 10; document.getElementById('settingTransfer').value = 7; document.getElementById('settingReflexion').value = 5; updateTotalMinutes(); } // Modul-Initialisierung (wird von loadModule('companion') aufgerufen) function loadCompanionModule() { console.log('Loading Companion Module...'); loadCompanionDashboard(); loadContextInfo(); // Kontext-Info Zeile laden loadSuggestionsFromAPI(); // Suggestions von neuer API laden loadTeacherSettings(); // Feature f16: Lehrer-Einstellungen laden checkOnboardingNeeded(); // Onboarding-Status prüfen } """