""" BreakPilot Studio - Elternkommunikation Modul Refactored: 2024-12-18 - Kachel-basierte Startansicht - Module fuer verschiedene Kommunikationsformen Funktionen (als Kacheln): - Elterngespraech (Notizen, Protokolle) - Elterngespraech planen (Terminbuchung) - Elternbriefe mit Legal Assistant (GFK) """ class LettersModule: """Elternkommunikation Modul mit Legal Assistant.""" @staticmethod def get_css() -> str: """CSS fuer das Elternkommunikation-Modul.""" return """ /* ========================================== LETTERS MODULE STYLES - Elternkommunikation ========================================== */ /* Panel Layout */ .panel-letters { display: none; flex-direction: column; height: 100%; background: var(--bp-bg); overflow: hidden; } .panel-letters.active { display: flex; } /* Letters Header */ .letters-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 32px; border-bottom: 1px solid var(--bp-border); background: var(--bp-surface); } .letters-title-section h1 { font-size: 24px; font-weight: 700; color: var(--bp-text); margin-bottom: 4px; } .letters-subtitle { font-size: 14px; color: var(--bp-text-muted); } /* Letters Content - Kacheln */ .letters-content { flex: 1; overflow-y: auto; padding: 32px; } /* Tiles Grid */ .letters-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 24px; max-width: 1200px; margin: 0 auto; } /* Letter Tile */ .letter-tile { background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 16px; padding: 28px; cursor: pointer; transition: all 0.3s ease; position: relative; overflow: hidden; } .letter-tile:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1); border-color: var(--bp-primary); } .letter-tile::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; opacity: 0; transition: opacity 0.3s; } .letter-tile:hover::before { opacity: 1; } .letter-tile.conversation::before { background: linear-gradient(90deg, #3b82f6, #1d4ed8); } .letter-tile.planning::before { background: linear-gradient(90deg, #10b981, #059669); } .letter-tile.legal::before { background: linear-gradient(90deg, #8b5cf6, #6d28d9); } .tile-icon-wrapper { width: 64px; height: 64px; border-radius: 16px; display: flex; align-items: center; justify-content: center; font-size: 32px; margin-bottom: 20px; } .tile-icon-wrapper.conversation { background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); } .tile-icon-wrapper.planning { background: linear-gradient(135deg, #10b981 0%, #059669 100%); } .tile-icon-wrapper.legal { background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%); } .tile-heading { font-size: 18px; font-weight: 600; color: var(--bp-text); margin-bottom: 10px; } .tile-description { font-size: 14px; color: var(--bp-text-muted); line-height: 1.6; margin-bottom: 20px; } .tile-features-list { display: flex; flex-wrap: wrap; gap: 8px; } .tile-feature { padding: 6px 12px; background: var(--bp-bg); border-radius: 16px; font-size: 12px; color: var(--bp-text-muted); } .tile-arrow-icon { position: absolute; bottom: 24px; right: 24px; width: 36px; height: 36px; border-radius: 50%; background: var(--bp-bg); display: flex; align-items: center; justify-content: center; color: var(--bp-text-muted); transition: all 0.3s; font-size: 18px; } .letter-tile:hover .tile-arrow-icon { background: var(--bp-primary); color: white; transform: translateX(4px); } /* Sub-Panel */ .letters-subpanel { display: none; flex-direction: column; height: 100%; } .letters-subpanel.active { display: flex; } .subpanel-header { display: flex; align-items: center; gap: 16px; padding: 16px 24px; border-bottom: 1px solid var(--bp-border); background: var(--bp-surface); } .subpanel-back { width: 36px; height: 36px; border-radius: 8px; border: 1px solid var(--bp-border); background: var(--bp-bg); color: var(--bp-text); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; transition: all 0.2s; } .subpanel-back:hover { background: var(--bp-surface-elevated); } .subpanel-title { font-size: 18px; font-weight: 600; } .subpanel-content { flex: 1; overflow-y: auto; padding: 24px; } /* ========================================== SUB-MODULE: Elterngespraech ========================================== */ .conversation-container { max-width: 900px; margin: 0 auto; } .conversation-header-info { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; } .info-card { background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 12px; padding: 16px; } .info-card label { display: block; font-size: 12px; color: var(--bp-text-muted); margin-bottom: 6px; } .info-card input, .info-card select { width: 100%; padding: 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text); font-size: 14px; } .conversation-notes { background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 12px; padding: 20px; margin-bottom: 24px; } .conversation-notes h3 { font-size: 14px; color: var(--bp-text-muted); margin-bottom: 12px; } .conversation-notes textarea { width: 100%; min-height: 250px; padding: 16px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-bg); color: var(--bp-text); font-size: 14px; line-height: 1.6; resize: vertical; } .conversation-notes textarea:focus { outline: none; border-color: var(--bp-primary); } .conversation-actions { display: flex; gap: 12px; justify-content: flex-end; } /* ========================================== SUB-MODULE: Terminplanung ========================================== */ .planning-container { max-width: 800px; margin: 0 auto; } .planning-calendar { background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 12px; padding: 24px; margin-bottom: 24px; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .calendar-nav { display: flex; gap: 8px; } .calendar-nav button { width: 32px; height: 32px; border-radius: 8px; border: 1px solid var(--bp-border); background: var(--bp-bg); color: var(--bp-text); cursor: pointer; } .calendar-month { font-size: 18px; font-weight: 600; } .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; } .calendar-day-header { text-align: center; font-size: 12px; color: var(--bp-text-muted); padding: 8px; } .calendar-day { aspect-ratio: 1; display: flex; align-items: center; justify-content: center; border-radius: 8px; cursor: pointer; transition: all 0.2s; font-size: 14px; } .calendar-day:hover { background: var(--bp-bg); } .calendar-day.today { background: var(--bp-primary-soft); color: var(--bp-primary); font-weight: 600; } .calendar-day.selected { background: var(--bp-primary); color: white; } .calendar-day.has-appointment::after { content: ''; position: absolute; bottom: 4px; width: 4px; height: 4px; border-radius: 50%; background: var(--bp-primary); } .time-slots { background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 12px; padding: 20px; } .time-slots h3 { font-size: 14px; color: var(--bp-text-muted); margin-bottom: 16px; } .time-slot-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 8px; } .time-slot { padding: 10px; text-align: center; border: 1px solid var(--bp-border); border-radius: 8px; cursor: pointer; transition: all 0.2s; font-size: 13px; } .time-slot:hover { border-color: var(--bp-primary); } .time-slot.selected { background: var(--bp-primary); border-color: var(--bp-primary); color: white; } .time-slot.booked { opacity: 0.5; cursor: not-allowed; text-decoration: line-through; } /* ========================================== SUB-MODULE: Legal Assistant / Elternbriefe ========================================== */ .legal-container { display: grid; grid-template-columns: 1fr 380px; gap: 24px; max-width: 1400px; } @media (max-width: 1100px) { .legal-container { grid-template-columns: 1fr; } } /* Editor Section */ .legal-editor { background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 12px; padding: 24px; } .editor-section { margin-bottom: 24px; } .editor-section-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; } /* Letter Type Selection */ .letter-types { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; margin-bottom: 20px; } .letter-type-btn { padding: 12px; border-radius: 8px; border: 1px solid var(--bp-border); background: var(--bp-surface); cursor: pointer; text-align: center; transition: all 0.2s; } .letter-type-btn:hover { border-color: var(--bp-primary); } .letter-type-btn.active { background: var(--bp-primary-soft); border-color: var(--bp-primary); color: var(--bp-primary); } .letter-type-icon { font-size: 20px; margin-bottom: 4px; } .letter-type-label { font-size: 11px; font-weight: 500; } /* Tone Selection */ .tone-options { display: flex; flex-wrap: wrap; gap: 8px; } .tone-btn { padding: 6px 12px; border-radius: 20px; border: 1px solid var(--bp-border); background: transparent; font-size: 12px; cursor: pointer; transition: all 0.2s; } .tone-btn:hover { border-color: var(--bp-primary); } .tone-btn.active { background: var(--bp-primary); border-color: var(--bp-primary); color: white; } /* Text Editor */ .text-editor { width: 100%; min-height: 300px; padding: 16px; border-radius: 8px; border: 1px solid var(--bp-border); background: var(--bp-surface); color: var(--bp-text); font-family: inherit; font-size: 14px; line-height: 1.6; resize: vertical; } .text-editor:focus { outline: none; border-color: var(--bp-primary); } /* Character Counter */ .char-counter { display: flex; justify-content: flex-end; font-size: 12px; color: var(--bp-text-muted); margin-top: 8px; } /* Legal Assistant Panel */ .legal-assistant-panel { background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 12px; padding: 24px; height: fit-content; position: sticky; top: 24px; } .legal-assistant-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; } .legal-icon-box { width: 44px; height: 44px; background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 22px; } .legal-assistant-title { font-size: 16px; font-weight: 600; } .legal-assistant-subtitle { font-size: 12px; color: var(--bp-text-muted); } /* GFK Score */ .gfk-score-card { background: var(--bp-bg); border-radius: 12px; padding: 16px; margin-bottom: 20px; } .gfk-score-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } .gfk-score-label { font-size: 13px; font-weight: 500; } .gfk-score-value { font-size: 28px; font-weight: 700; color: var(--bp-accent); } .gfk-score-bar { height: 8px; background: var(--bp-border); border-radius: 4px; overflow: hidden; } .gfk-score-fill { height: 100%; background: var(--bp-accent); border-radius: 4px; transition: width 0.3s ease; } .gfk-info { font-size: 11px; color: var(--bp-text-muted); margin-top: 8px; } /* Suggestions */ .suggestions-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; } .suggestion-item { background: var(--bp-bg); border-radius: 8px; padding: 12px; border-left: 3px solid var(--bp-warning); } .suggestion-item.positive { border-left-color: var(--bp-success); } .suggestion-type { font-size: 10px; font-weight: 600; text-transform: uppercase; color: var(--bp-text-muted); margin-bottom: 4px; } .suggestion-text { font-size: 13px; line-height: 1.4; } /* Templates */ .templates-list { display: flex; flex-direction: column; gap: 8px; } .template-item { display: flex; align-items: center; justify-content: space-between; padding: 12px; background: var(--bp-bg); border-radius: 8px; cursor: pointer; transition: all 0.2s; } .template-item:hover { background: var(--bp-surface-elevated); } .template-name { font-size: 13px; font-weight: 500; } .template-use-btn { font-size: 12px; color: var(--bp-primary); } /* Legal References */ .legal-refs-section { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--bp-border); } .legal-refs-title { font-size: 12px; font-weight: 600; margin-bottom: 12px; color: var(--bp-text-muted); } .legal-ref-item { font-size: 12px; padding: 10px; background: var(--bp-bg); border-radius: 8px; margin-bottom: 8px; } .legal-ref-law { font-weight: 600; color: var(--bp-info); } /* Actions */ .editor-actions { display: flex; gap: 12px; margin-top: 20px; } /* Status Bar */ .letters-status { display: flex; align-items: center; gap: 12px; padding: 10px 24px; border-top: 1px solid var(--bp-border); background: var(--bp-surface); font-size: 12px; } .status-indicator { width: 8px; height: 8px; border-radius: 50%; background: #10b981; } .status-indicator.busy { background: #f59e0b; animation: statusPulse 1.5s infinite; } @keyframes statusPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .status-text { color: var(--bp-text); } .status-detail { color: var(--bp-text-muted); } """ @staticmethod def get_html() -> str: """HTML fuer das Elternkommunikation-Modul mit Kachel-basierter Startansicht.""" return """

Elternkommunikation

Professionelle Kommunikation mit Eltern und Erziehungsberechtigten

💬
Elterngespraech
Fuehre strukturierte Elterngespraeche und dokumentiere wichtige Vereinbarungen. Erstelle Protokolle und Notizen.
Protokolle Notizen Export
📅
Elterngespraech planen
Plane und verwalte Termine fuer Elterngespraeche. Versende Einladungen und Erinnerungen automatisch.
Kalender Einladungen Erinnerungen
Elterngespraech dokumentieren

📝 Gespraechsnotizen

Elterngespraech planen
Dezember 2024
Mo
Di
Mi
Do
Fr
Sa
So

🕑 Verfuegbare Zeitslots

08:00
08:30
09:00
09:30
10:00
10:30
14:00
14:30
15:00
15:30
16:00
16:30
Bereit
""" @staticmethod def get_js() -> str: """JavaScript fuer das Elternkommunikation-Modul.""" return """ // ========================================== // LETTERS MODULE - Elternkommunikation // ========================================== let lettersInitialized = false; let currentLetterType = 'general'; let currentTone = 'professional'; let analysisTimeout = null; // Letter Templates const LETTER_TEMPLATES = { halbjahr: { subject: 'Information zum Leistungsstand - Halbjahr', content: `Sehr geehrte Eltern, ich moechte Sie ueber den aktuellen Leistungsstand Ihres Kindes [SCHUELER] in der Klasse [KLASSE] informieren. [Beobachtungen einfuegen] Ich wuerde mich freuen, wenn wir gemeinsam besprechen koennten, wie wir [SCHUELER] weiter unterstuetzen koennen. Mit freundlichen Gruessen` }, fehlzeiten: { subject: 'Mitteilung ueber Fehlzeiten', content: `Sehr geehrte Eltern, mir ist aufgefallen, dass [SCHUELER] in letzter Zeit haeufiger dem Unterricht ferngeblieben ist. Ich mache mir Sorgen darueber und moechte gerne verstehen, ob es Gruende gibt, bei denen wir als Schule unterstuetzen koennen. Koennten wir einen Termin fuer ein kurzes Gespraech vereinbaren? Mit freundlichen Gruessen` }, elternabend: { subject: 'Einladung zum Elternabend', content: `Sehr geehrte Eltern, hiermit lade ich Sie herzlich zum Elternabend der Klasse [KLASSE] ein. Datum: [DATUM] Uhrzeit: [UHRZEIT] Ort: [ORT] Tagesordnung: 1. Begruessung 2. Informationen zum Halbjahr 3. Verschiedenes Ich freue mich auf Ihr Kommen und einen konstruktiven Austausch. Mit freundlichen Gruessen` }, lob: { subject: 'Positive Rueckmeldung zu [SCHUELER]', content: `Sehr geehrte Eltern, ich freue mich, Ihnen eine positive Rueckmeldung zu [SCHUELER] geben zu koennen. [Positive Beobachtungen einfuegen] Es ist schoen zu sehen, wie sich [SCHUELER] entwickelt. Bitte geben Sie dieses Lob auch zu Hause weiter. Mit freundlichen Gruessen` } }; // GFK Patterns const GFK_POSITIVE_PATTERNS = [ /ich (beobachte|sehe|habe bemerkt)/i, /ich (fuehle|empfinde)/i, /ich (brauche|wuensche mir)/i, /ich (bitte|moechte bitten)/i, /koennten wir/i, /gemeinsam/i, /unterstuetzen/i, /wertschaetze/i, /freue mich/i ]; const GFK_NEGATIVE_PATTERNS = [ /muss|muessen/i, /immer|nie|staendig/i, /schuld/i, /versagt/i, /unfaehig/i, /sie sollten/i, /inakzeptabel/i ]; function loadLettersModule() { if (lettersInitialized) { console.log('Letters module already initialized'); return; } console.log('Loading Letters Module...'); // Set default date const dateInput = document.getElementById('conv-date'); if (dateInput) { dateInput.valueAsDate = new Date(); } // Initialize calendar initCalendar(); lettersInitialized = true; console.log('Letters Module loaded successfully'); } // ========================================== // VIEW SWITCHING // ========================================== function openLettersSubpanel(panelId) { document.getElementById('letters-tiles-view').style.display = 'none'; document.querySelectorAll('.letters-subpanel').forEach(p => { p.classList.remove('active'); }); const panel = document.getElementById('letters-subpanel-' + panelId); if (panel) { panel.classList.add('active'); } } function closeLettersSubpanel() { document.querySelectorAll('.letters-subpanel').forEach(p => { p.classList.remove('active'); }); document.getElementById('letters-tiles-view').style.display = 'block'; } // ========================================== // STATUS // ========================================== function setLettersStatus(text, detail = '', state = 'idle') { const indicator = document.getElementById('letters-status-indicator'); const textEl = document.getElementById('letters-status-text'); const detailEl = document.getElementById('letters-status-detail'); if (textEl) textEl.textContent = text; if (detailEl) detailEl.textContent = detail; if (indicator) { indicator.classList.remove('busy'); if (state === 'busy') indicator.classList.add('busy'); } } // ========================================== // CONVERSATION (Elterngespraech) // ========================================== function saveConversationDraft() { const data = { student: document.getElementById('conv-student')?.value, class: document.getElementById('conv-class')?.value, date: document.getElementById('conv-date')?.value, participants: document.getElementById('conv-participants')?.value, notes: document.getElementById('conv-notes')?.value }; localStorage.setItem('bp-conversation-draft', JSON.stringify(data)); setLettersStatus('Entwurf gespeichert', ''); } async function saveConversation() { const data = { student: document.getElementById('conv-student')?.value, class: document.getElementById('conv-class')?.value, date: document.getElementById('conv-date')?.value, participants: document.getElementById('conv-participants')?.value, notes: document.getElementById('conv-notes')?.value }; if (!data.student || !data.notes) { alert('Bitte fuellen Sie mindestens Schueler/in und Notizen aus.'); return; } setLettersStatus('Speichere Protokoll...', '', 'busy'); try { // Save as a letter with type 'general' for the conversation record const resp = await fetch('/api/letters/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ recipient_name: 'Gespraechsprotokoll', recipient_address: '', student_name: data.student, student_class: data.class || '', subject: 'Elterngespraech vom ' + (data.date || new Date().toLocaleDateString('de-DE')), content: 'Teilnehmer: ' + (data.participants || 'k.A.') + '\\n\\n' + data.notes, letter_type: 'general', tone: 'professional', teacher_name: '', teacher_title: '' }) }); if (!resp.ok) { throw new Error('Speichern fehlgeschlagen'); } const result = await resp.json(); setLettersStatus('Protokoll gespeichert', 'ID: ' + result.id); alert('Gespraechsprotokoll wurde gespeichert.'); closeLettersSubpanel(); } catch (e) { console.error('Save conversation error:', e); setLettersStatus('Speichern fehlgeschlagen', e.message, 'error'); // Fallback to local storage saveConversationDraft(); alert('Gespraechsprotokoll wurde lokal gespeichert.'); closeLettersSubpanel(); } } async function exportConversation(format) { const data = { student: document.getElementById('conv-student')?.value || 'Unbekannt', class: document.getElementById('conv-class')?.value || '', date: document.getElementById('conv-date')?.value || new Date().toLocaleDateString('de-DE'), participants: document.getElementById('conv-participants')?.value || '', notes: document.getElementById('conv-notes')?.value || '' }; if (!data.notes) { alert('Bitte geben Sie zuerst Notizen ein.'); return; } setLettersStatus('Erstelle ' + format.toUpperCase() + '...', '', 'busy'); if (format === 'pdf') { try { const resp = await fetch('/api/letters/export-pdf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ letter_data: { recipient_name: 'Gespraechsprotokoll', recipient_address: '', student_name: data.student, student_class: data.class, subject: 'Elterngespraech vom ' + data.date, content: 'Teilnehmer: ' + (data.participants || 'k.A.') + '\\n\\n' + data.notes, letter_type: 'general', tone: 'professional', teacher_name: '', teacher_title: '' } }) }); if (!resp.ok) { throw new Error('PDF-Export fehlgeschlagen'); } const blob = await resp.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `Gespraechsprotokoll_${data.student.replace(/\\s+/g, '_')}_${data.date.replace(/\\./g, '-')}.pdf`; a.click(); URL.revokeObjectURL(url); setLettersStatus('PDF erstellt', 'Download gestartet'); } catch (e) { console.error('PDF export error:', e); setLettersStatus('Export fehlgeschlagen', e.message, 'error'); alert('PDF-Export fehlgeschlagen: ' + e.message); } } else { // Text export fallback const textContent = [ 'GESPRAECHSPROTOKOLL', '==================', '', 'Schueler/in: ' + data.student, 'Klasse: ' + data.class, 'Datum: ' + data.date, 'Teilnehmer: ' + data.participants, '', 'NOTIZEN:', '--------', data.notes ].join('\\n'); const blob = new Blob([textContent], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `Gespraechsprotokoll_${data.student.replace(/\\s+/g, '_')}.txt`; a.click(); URL.revokeObjectURL(url); setLettersStatus('Export abgeschlossen', ''); } } // ========================================== // PLANNING (Terminplanung) // ========================================== let selectedDate = null; let selectedTimeSlot = null; let currentCalendarDate = new Date(); function initCalendar() { renderCalendar(); } function renderCalendar() { const grid = document.getElementById('calendar-grid'); const monthLabel = document.getElementById('calendar-month'); if (!grid || !monthLabel) return; const year = currentCalendarDate.getFullYear(); const month = currentCalendarDate.getMonth(); const monthNames = ['Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; monthLabel.textContent = monthNames[month] + ' ' + year; // Clear existing days (keep headers) const headers = Array.from(grid.querySelectorAll('.calendar-day-header')); grid.innerHTML = ''; headers.forEach(h => grid.appendChild(h)); // First day of month const firstDay = new Date(year, month, 1); let startDay = firstDay.getDay(); if (startDay === 0) startDay = 7; // Monday = 1 // Days in month const daysInMonth = new Date(year, month + 1, 0).getDate(); // Today const today = new Date(); const isThisMonth = today.getFullYear() === year && today.getMonth() === month; // Add empty cells for days before start for (let i = 1; i < startDay; i++) { const empty = document.createElement('div'); empty.className = 'calendar-day'; grid.appendChild(empty); } // Add days for (let day = 1; day <= daysInMonth; day++) { const dayEl = document.createElement('div'); dayEl.className = 'calendar-day'; dayEl.textContent = day; if (isThisMonth && day === today.getDate()) { dayEl.classList.add('today'); } dayEl.addEventListener('click', () => { document.querySelectorAll('.calendar-day.selected').forEach(d => d.classList.remove('selected')); dayEl.classList.add('selected'); selectedDate = new Date(year, month, day); }); grid.appendChild(dayEl); } } function prevMonth() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1); renderCalendar(); } function nextMonth() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1); renderCalendar(); } function selectTimeSlot(el) { if (el.classList.contains('booked')) return; document.querySelectorAll('.time-slot.selected').forEach(s => s.classList.remove('selected')); el.classList.add('selected'); selectedTimeSlot = el.textContent; } function bookAppointment() { if (!selectedDate || !selectedTimeSlot) { alert('Bitte waehlen Sie ein Datum und eine Uhrzeit aus.'); return; } const dateStr = selectedDate.toLocaleDateString('de-DE'); alert('Termin gebucht: ' + dateStr + ' um ' + selectedTimeSlot + ' Uhr\\n\\nEinladung wird versendet...'); closeLettersSubpanel(); } // ========================================== // LEGAL ASSISTANT // ========================================== function selectLetterType(type) { currentLetterType = type; document.querySelectorAll('.letter-type-btn').forEach(btn => { btn.classList.remove('active'); }); const btn = document.querySelector(`.letter-type-btn[data-type="${type}"]`); if (btn) btn.classList.add('active'); updateLegalReferences(type); } function selectTone(tone) { currentTone = tone; document.querySelectorAll('.tone-btn').forEach(btn => { btn.classList.remove('active'); }); const btn = document.querySelector(`.tone-btn[data-tone="${tone}"]`); if (btn) btn.classList.add('active'); } function analyzeLetterText() { clearTimeout(analysisTimeout); analysisTimeout = setTimeout(() => { const content = document.getElementById('letter-content')?.value || ''; const charCount = content.length; document.getElementById('letter-char-count').textContent = charCount; if (charCount < 20) { document.getElementById('gfk-score-display').textContent = '--'; document.getElementById('gfk-score-bar-fill').style.width = '0%'; return; } // Calculate GFK Score let score = 50; const suggestions = []; GFK_POSITIVE_PATTERNS.forEach(pattern => { if (pattern.test(content)) score += 7; }); GFK_NEGATIVE_PATTERNS.forEach(pattern => { if (pattern.test(content)) { score -= 10; const match = content.match(pattern); if (match) { suggestions.push({ type: 'warning', text: '"' + match[0] + '" koennte wertend wirken.' }); } } }); const iStatements = (content.match(/\\bich\\b/gi) || []).length; if (iStatements > 2) { score += 10; suggestions.push({ type: 'positive', text: 'Gut: Sie verwenden Ich-Botschaften.' }); } if ((content.match(/\\?/g) || []).length > 0) { score += 5; suggestions.push({ type: 'positive', text: 'Gut: Offene Fragen foerdern den Dialog.' }); } score = Math.max(0, Math.min(100, score)); document.getElementById('gfk-score-display').textContent = score; document.getElementById('gfk-score-bar-fill').style.width = score + '%'; const bar = document.getElementById('gfk-score-bar-fill'); if (score >= 70) bar.style.background = 'var(--bp-success)'; else if (score >= 40) bar.style.background = 'var(--bp-warning)'; else bar.style.background = 'var(--bp-danger)'; updateSuggestions(suggestions); }, 500); } function updateSuggestions(suggestions) { const list = document.getElementById('suggestions-list'); if (!list) return; if (!suggestions.length) { list.innerHTML = `
✅ Tipp
Verwenden Sie Ich-Botschaften und beschreiben Sie konkrete Beobachtungen.
`; return; } list.innerHTML = suggestions.map(s => `
${s.type === 'positive' ? '✅ GUT' : '⚠ HINWEIS'}
${s.text}
`).join(''); } function updateLegalReferences(type) { const refs = { general: [{ law: 'SchulG § 42', desc: 'Informationspflicht der Schule' }], behavior: [ { law: 'SchulG § 53', desc: 'Erziehungs- und Ordnungsmassnahmen' }, { law: 'AOGS § 2', desc: 'Verfahren bei Ordnungsmassnahmen' } ], academic: [ { law: 'SchulG § 44', desc: 'Information ueber Leistungsentwicklung' }, { law: 'APO-SI § 7', desc: 'Versetzung und Foerderung' } ], attendance: [ { law: 'SchulG § 43', desc: 'Schulpflicht und Teilnahmepflicht' }, { law: 'SchulG § 41', desc: 'Verantwortung der Erziehungsberechtigten' } ], meeting: [{ law: 'SchulG § 44', desc: 'Elternberatung und -gespraeche' }], positive: [{ law: 'SchulG § 2', desc: 'Foerderung individueller Faehigkeiten' }] }; const container = document.getElementById('legal-refs-container'); if (!container) return; const typeRefs = refs[type] || refs.general; container.innerHTML = typeRefs.map(ref => ` `).join(''); } function loadLetterTemplate(templateId) { const template = LETTER_TEMPLATES[templateId]; if (!template) return; const student = document.getElementById('letter-student')?.value || '[SCHUELER]'; const classVal = document.getElementById('letter-class')?.value || '[KLASSE]'; let content = template.content .replace(/\\[SCHUELER\\]/g, student) .replace(/\\[KLASSE\\]/g, classVal); document.getElementById('letter-subject').value = template.subject.replace('[SCHUELER]', student); document.getElementById('letter-content').value = content; analyzeLetterText(); } // Improve letter with AI via /api/letters/improve async function improveWithAI() { const content = document.getElementById('letter-content')?.value || ''; if (content.length < 20) { alert('Bitte geben Sie zuerst einen Text ein.'); return; } setLettersStatus('Verbessere mit KI...', 'GFK-Analyse', 'busy'); try { const resp = await fetch('/api/letters/improve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: content, communication_type: currentLetterType, tone: currentTone }) }); if (!resp.ok) { throw new Error('API-Fehler: ' + resp.status); } const result = await resp.json(); // Update GFK score display const score = Math.round((result.gfk_score || 0.5) * 100); document.getElementById('gfk-score-display').textContent = score; document.getElementById('gfk-score-bar-fill').style.width = score + '%'; // Update bar color based on score const bar = document.getElementById('gfk-score-bar-fill'); if (score >= 70) bar.style.background = 'var(--bp-success)'; else if (score >= 40) bar.style.background = 'var(--bp-warning)'; else bar.style.background = 'var(--bp-danger)'; // Show improvements/suggestions const changes = result.changes || []; const suggestions = changes.map(change => ({ type: change.includes('Gut') || change.includes('positiv') ? 'positive' : 'warning', text: change })); updateSuggestions(suggestions); // If improved content is different, offer to replace if (result.improved_content && result.improved_content !== content) { if (confirm('Der verbesserte Text liegt vor. Moechten Sie ihn uebernehmen?')) { document.getElementById('letter-content').value = result.improved_content; analyzeLetterText(); } } setLettersStatus('GFK-Analyse abgeschlossen', 'Score: ' + score + '%'); } catch (e) { console.error('AI improvement error:', e); setLettersStatus('Verbesserung fehlgeschlagen', e.message, 'error'); // Fallback to local analysis analyzeLetterText(); } } // Show letter preview in modal function showLetterPreview() { const student = document.getElementById('letter-student')?.value || '[Schueler/in]'; const className = document.getElementById('letter-class')?.value || '[Klasse]'; const subject = document.getElementById('letter-subject')?.value || '[Betreff]'; const content = document.getElementById('letter-content')?.value || ''; const previewHtml = `
Schule XY
Musterstrasse 1
12345 Musterstadt
${new Date().toLocaleDateString('de-DE')}
Familie von ${student}
Klasse ${className}
Betreff: ${subject}
${content}
`; // Create preview modal const modal = document.createElement('div'); modal.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 20px;'; modal.innerHTML = `
${previewHtml}
`; document.body.appendChild(modal); } // Export letter as PDF via /api/letters/export-pdf async function exportLetterPDF() { const student = document.getElementById('letter-student')?.value || 'Unbekannt'; const className = document.getElementById('letter-class')?.value || ''; const subject = document.getElementById('letter-subject')?.value || 'Elternbrief'; const content = document.getElementById('letter-content')?.value || ''; if (content.length < 20) { alert('Bitte geben Sie zuerst einen Text ein.'); return; } setLettersStatus('Erstelle PDF...', '', 'busy'); try { const resp = await fetch('/api/letters/export-pdf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ letter_data: { recipient_name: 'Familie ' + student, recipient_address: '', student_name: student, student_class: className, subject: subject, content: content, letter_type: currentLetterType, tone: currentTone, teacher_name: 'Klassenlehrerin', teacher_title: '' } }) }); if (!resp.ok) { throw new Error('PDF-Export fehlgeschlagen: ' + resp.status); } // Download PDF const blob = await resp.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `Elternbrief_${student.replace(/\\s+/g, '_')}_${new Date().toISOString().slice(0,10)}.pdf`; a.click(); URL.revokeObjectURL(url); setLettersStatus('PDF erstellt', 'Download gestartet'); } catch (e) { console.error('PDF export error:', e); setLettersStatus('PDF-Export fehlgeschlagen', e.message, 'error'); alert('PDF-Export fehlgeschlagen: ' + e.message); } } // Save letter as template via /api/letters async function saveLetterTemplate() { const student = document.getElementById('letter-student')?.value || ''; const className = document.getElementById('letter-class')?.value || ''; const subject = document.getElementById('letter-subject')?.value || ''; const content = document.getElementById('letter-content')?.value || ''; if (content.length < 20) { alert('Bitte geben Sie zuerst einen Text ein.'); return; } const name = prompt('Name fuer die Vorlage:', subject); if (!name) return; setLettersStatus('Speichere Vorlage...', '', 'busy'); try { const resp = await fetch('/api/letters/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ recipient_name: 'Familie ' + (student || '[SCHUELER]'), recipient_address: '', student_name: student || '[SCHUELER]', student_class: className || '[KLASSE]', subject: name, content: content, letter_type: currentLetterType, tone: currentTone, teacher_name: '[LEHRER]', teacher_title: '' }) }); if (!resp.ok) { throw new Error('Speichern fehlgeschlagen: ' + resp.status); } const result = await resp.json(); setLettersStatus('Vorlage gespeichert', 'ID: ' + result.id); alert('Vorlage "' + name + '" wurde gespeichert.'); } catch (e) { console.error('Save template error:', e); setLettersStatus('Speichern fehlgeschlagen', e.message, 'error'); alert('Speichern fehlgeschlagen: ' + e.message); } } // ========================================== // SHOW PANEL // ========================================== function showLettersPanel() { console.log('showLettersPanel called'); hideAllPanels(); if (typeof hideStudioSubMenu === 'function') hideStudioSubMenu(); const panel = document.getElementById('panel-letters'); if (panel) { panel.style.display = 'flex'; loadLettersModule(); console.log('Letters panel shown'); } } // Escape key handler document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && document.querySelector('.letters-subpanel.active')) { closeLettersSubpanel(); } }); """ def get_letters_module() -> dict: """Gibt das komplette Elternkommunikation-Modul als Dictionary zurueck.""" module = LettersModule() return { 'css': module.get_css(), 'html': module.get_html(), 'js': module.get_js(), 'init_function': 'loadLettersModule' }