""" BreakPilot Studio - School Service Modul Funktionen: - Klassen & Schueler verwalten - Klausuren & Tests erstellen und bewerten - Notenspiegel fuehren - Klassenbuch (Fehlzeiten, Eintragungen) - Zeugnisse generieren Kommuniziert mit dem Go School-Service (Port 8084) """ class SchoolModule: """Modul fuer Schulverwaltung und Leistungsbewertung.""" @staticmethod def get_css() -> str: """CSS fuer das School-Modul.""" return """ /* ============================================= SCHOOL MODULE - Leistungsbewertung ============================================= */ /* Gemeinsame Panel-Styles */ .panel-school-classes, .panel-school-exams, .panel-school-grades, .panel-school-gradebook, .panel-school-certificates { display: none; flex-direction: column; height: 100%; background: var(--bp-bg); overflow-y: auto; } .panel-school-classes.active, .panel-school-exams.active, .panel-school-grades.active, .panel-school-gradebook.active, .panel-school-certificates.active { display: flex; } /* School Header */ .school-header { padding: 24px 32px; background: var(--bp-surface); border-bottom: 1px solid var(--bp-border); display: flex; justify-content: space-between; align-items: center; } .school-header h2 { font-size: 24px; font-weight: 600; color: var(--bp-text); margin: 0; } .school-header-actions { display: flex; gap: 12px; } /* School Content */ .school-content { padding: 24px 32px; flex: 1; } /* Cards Grid */ .school-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; margin-bottom: 24px; } /* School Card */ .school-card { background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 12px; padding: 20px; transition: all 0.2s ease; } .school-card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); border-color: var(--bp-primary); } .school-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; } .school-card-title { font-size: 16px; font-weight: 600; color: var(--bp-text); } .school-card-badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; background: var(--bp-primary-soft); color: var(--bp-primary); } .school-card-info { font-size: 13px; color: var(--bp-text-muted); margin-bottom: 16px; } .school-card-actions { display: flex; gap: 8px; } /* School Table */ .school-table { width: 100%; border-collapse: collapse; background: var(--bp-surface); border-radius: 12px; overflow: hidden; border: 1px solid var(--bp-border); } .school-table th, .school-table td { padding: 12px 16px; text-align: left; border-bottom: 1px solid var(--bp-border); } .school-table th { background: var(--bp-bg); font-weight: 600; font-size: 12px; color: var(--bp-text-muted); text-transform: uppercase; letter-spacing: 0.5px; } .school-table td { font-size: 14px; color: var(--bp-text); } .school-table tr:last-child td { border-bottom: none; } .school-table tr:hover td { background: var(--bp-bg); } /* Grade Badge */ .grade-badge { display: inline-block; padding: 4px 10px; border-radius: 8px; font-weight: 600; font-size: 13px; } .grade-badge.grade-1 { background: #d4edda; color: #155724; } .grade-badge.grade-2 { background: #d1ecf1; color: #0c5460; } .grade-badge.grade-3 { background: #fff3cd; color: #856404; } .grade-badge.grade-4 { background: #ffe5d0; color: #a94442; } .grade-badge.grade-5 { background: #f8d7da; color: #721c24; } .grade-badge.grade-6 { background: #f5c6cb; color: #721c24; } /* Status Badge */ .status-badge { display: inline-block; padding: 4px 10px; border-radius: 8px; font-size: 12px; font-weight: 500; } .status-badge.status-present { background: #d4edda; color: #155724; } .status-badge.status-absent { background: #f8d7da; color: #721c24; } .status-badge.status-excused { background: #fff3cd; color: #856404; } .status-badge.status-late { background: #d1ecf1; color: #0c5460; } /* School Form */ .school-form { background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 12px; padding: 24px; margin-bottom: 24px; } .school-form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 16px; } .school-form-group { display: flex; flex-direction: column; gap: 6px; } .school-form-group label { font-size: 13px; font-weight: 500; color: var(--bp-text-muted); } .school-form-group input, .school-form-group select, .school-form-group textarea { padding: 10px 14px; border: 1px solid var(--bp-border); border-radius: 8px; font-size: 14px; background: var(--bp-bg); color: var(--bp-text); transition: border-color 0.2s; } .school-form-group input:focus, .school-form-group select:focus, .school-form-group textarea:focus { outline: none; border-color: var(--bp-primary); } /* School Tabs */ .school-tabs { display: flex; gap: 4px; padding: 4px; background: var(--bp-bg); border-radius: 10px; margin-bottom: 24px; } .school-tab { padding: 10px 20px; border-radius: 8px; font-size: 14px; font-weight: 500; color: var(--bp-text-muted); cursor: pointer; transition: all 0.2s; border: none; background: transparent; } .school-tab:hover { color: var(--bp-text); } .school-tab.active { background: var(--bp-surface); color: var(--bp-primary); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } /* Empty State */ .school-empty-state { text-align: center; padding: 60px 20px; color: var(--bp-text-muted); } .school-empty-state-icon { font-size: 48px; margin-bottom: 16px; } .school-empty-state h3 { font-size: 18px; font-weight: 600; color: var(--bp-text); margin-bottom: 8px; } .school-empty-state p { font-size: 14px; margin-bottom: 24px; } /* Calendar View for Gradebook */ .school-calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 12px; padding: 16px; } .school-calendar-header { font-size: 12px; font-weight: 600; color: var(--bp-text-muted); text-align: center; padding: 8px; } .school-calendar-day { aspect-ratio: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 8px; cursor: pointer; transition: all 0.2s; font-size: 13px; } .school-calendar-day:hover { background: var(--bp-bg); } .school-calendar-day.today { background: var(--bp-primary-soft); color: var(--bp-primary); font-weight: 600; } .school-calendar-day.has-entries { position: relative; } .school-calendar-day.has-entries::after { content: ''; position: absolute; bottom: 4px; width: 4px; height: 4px; border-radius: 50%; background: var(--bp-primary); } /* Modal for School */ .school-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: none; align-items: center; justify-content: center; z-index: 1000; } .school-modal.active { display: flex; } .school-modal-content { background: var(--bp-surface); border-radius: 16px; width: 90%; max-width: 600px; max-height: 90vh; overflow-y: auto; } .school-modal-header { padding: 20px 24px; border-bottom: 1px solid var(--bp-border); display: flex; justify-content: space-between; align-items: center; } .school-modal-header h3 { font-size: 18px; font-weight: 600; color: var(--bp-text); margin: 0; } .school-modal-close { width: 32px; height: 32px; border-radius: 8px; border: none; background: var(--bp-bg); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; color: var(--bp-text-muted); } .school-modal-body { padding: 24px; } .school-modal-footer { padding: 16px 24px; border-top: 1px solid var(--bp-border); display: flex; justify-content: flex-end; gap: 12px; } /* Statistics Cards */ .school-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-bottom: 24px; } .school-stat-card { background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 12px; padding: 16px; text-align: center; } .school-stat-value { font-size: 28px; font-weight: 700; color: var(--bp-primary); margin-bottom: 4px; } .school-stat-label { font-size: 12px; color: var(--bp-text-muted); text-transform: uppercase; letter-spacing: 0.5px; } /* Loading Spinner */ .school-loading { display: flex; justify-content: center; padding: 40px; } .school-spinner { width: 32px; height: 32px; border: 3px solid var(--bp-border); border-top-color: var(--bp-primary); border-radius: 50%; animation: school-spin 1s linear infinite; } @keyframes school-spin { to { transform: rotate(360deg); } } """ @staticmethod def get_html() -> str: """HTML fuer das School-Modul.""" return """

Klassen & Schueler

👥

Keine Klassen vorhanden

Erstellen Sie Ihre erste Klasse, um Schueler zu verwalten.

Klausuren & Tests

📄

Keine Klausuren vorhanden

Erstellen Sie Ihre erste Klausur oder Test.

Notenspiegel

📊

Klasse waehlen

Waehlen Sie eine Klasse, um den Notenspiegel anzuzeigen.

Klassenbuch

📖

Klasse waehlen

Waehlen Sie eine Klasse, um das Klassenbuch anzuzeigen.

Zeugnisse

🏆

Keine Zeugnisse

Waehlen Sie eine Klasse und generieren Sie Zeugnisse.

Zeugnis-Wizard

1. Klasse
Klasse auswaehlen
2. Noten
Noten pruefen
3. Vorlage
Zeugnisvorlage
4. Bemerkungen
Bemerkungen
5. Generieren
Erstellen

Neue Klasse

Schueler hinzufuegen

Neue Klausur

Muendliche Note eintragen

""" @staticmethod def get_js() -> str: """JavaScript fuer das School-Modul.""" return """ /* ============================================= SCHOOL MODULE - JavaScript ============================================= */ // School API Base URL const SCHOOL_API_BASE = '/api/school'; // State let schoolState = { years: [], classes: [], subjects: [], currentYearId: null, currentClassId: null, }; // ========== INITIALIZATION ========== async function schoolInit() { console.log('School module initializing...'); await schoolLoadYears(); await schoolLoadSubjects(); schoolSetTodayDate(); } function schoolSetTodayDate() { const today = new Date().toISOString().split('T')[0]; const dateInput = document.getElementById('gradebook-date'); if (dateInput) { dateInput.value = today; } } // ========== API CALLS ========== async function schoolApiCall(endpoint, method = 'GET', data = null) { const options = { method, headers: { 'Content-Type': 'application/json', } }; if (data) { options.body = JSON.stringify(data); } try { const response = await fetch(`${SCHOOL_API_BASE}${endpoint}`, options); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'API Error'); } return await response.json(); } catch (error) { console.error('School API Error:', error); showToast('Fehler: ' + error.message, 'error'); throw error; } } // ========== YEARS ========== async function schoolLoadYears() { try { const years = await schoolApiCall('/years'); schoolState.years = years || []; const select = document.getElementById('school-year-select'); if (select) { select.innerHTML = ''; schoolState.years.forEach(year => { const option = document.createElement('option'); option.value = year.id; option.textContent = year.name; if (year.is_current) { option.selected = true; schoolState.currentYearId = year.id; } select.appendChild(option); }); } if (schoolState.currentYearId) { schoolLoadClasses(); } } catch (error) { console.log('No years found or error loading years'); } } function schoolShowYearModal() { // Simplified - just show a prompt const name = prompt('Schuljahr Name (z.B. 2024/2025):'); if (name) { const startDate = prompt('Startdatum (YYYY-MM-DD):'); const endDate = prompt('Enddatum (YYYY-MM-DD):'); if (startDate && endDate) { schoolCreateYear(name, startDate, endDate); } } } async function schoolCreateYear(name, startDate, endDate) { try { await schoolApiCall('/years', 'POST', { name, start_date: startDate, end_date: endDate, is_current: true }); showToast('Schuljahr erstellt', 'success'); schoolLoadYears(); } catch (error) { // Error handled in schoolApiCall } } // ========== CLASSES ========== async function schoolLoadClasses() { const yearId = document.getElementById('school-year-select')?.value; if (!yearId) return; schoolState.currentYearId = yearId; try { const classes = await schoolApiCall('/classes'); schoolState.classes = classes || []; schoolRenderClasses(); schoolUpdateClassSelects(); } catch (error) { schoolRenderClasses(); } } function schoolRenderClasses() { const container = document.getElementById('school-classes-list'); if (!container) return; if (!schoolState.classes || schoolState.classes.length === 0) { container.innerHTML = `
👥

Keine Klassen vorhanden

Erstellen Sie Ihre erste Klasse, um Schueler zu verwalten.

`; return; } container.innerHTML = schoolState.classes.map(cls => `
${cls.name}
${cls.grade_level}. Klasse
${cls.school_type || 'Gymnasium'} | ${cls.student_count || 0} Schueler
`).join(''); } function schoolUpdateClassSelects() { const selects = [ 'exam-class-filter', 'grades-class-select', 'gradebook-class-select', 'cert-class-select', 'exam-class' ]; selects.forEach(id => { const select = document.getElementById(id); if (select) { const currentValue = select.value; select.innerHTML = ''; schoolState.classes.forEach(cls => { const option = document.createElement('option'); option.value = cls.id; option.textContent = cls.name; select.appendChild(option); }); if (currentValue) { select.value = currentValue; } } }); } function schoolShowClassModal(classId = null) { document.getElementById('class-modal-title').textContent = classId ? 'Klasse bearbeiten' : 'Neue Klasse'; document.getElementById('class-edit-id').value = classId || ''; document.getElementById('school-class-form').reset(); document.getElementById('school-class-modal').classList.add('active'); } async function schoolSaveClass() { const editId = document.getElementById('class-edit-id').value; const data = { name: document.getElementById('class-name').value, grade_level: parseInt(document.getElementById('class-grade-level').value), school_type: document.getElementById('class-school-type').value, federal_state: document.getElementById('class-federal-state').value, school_year_id: schoolState.currentYearId }; try { if (editId) { await schoolApiCall(`/classes/${editId}`, 'PUT', data); showToast('Klasse aktualisiert', 'success'); } else { await schoolApiCall('/classes', 'POST', data); showToast('Klasse erstellt', 'success'); } schoolCloseModal('class'); schoolLoadClasses(); } catch (error) { // Error handled in schoolApiCall } } async function schoolDeleteClass(classId) { if (!confirm('Klasse wirklich loeschen? Alle Schueler werden ebenfalls geloescht.')) { return; } try { await schoolApiCall(`/classes/${classId}`, 'DELETE'); showToast('Klasse geloescht', 'success'); schoolLoadClasses(); } catch (error) { // Error handled in schoolApiCall } } function schoolEditClass(classId) { const cls = schoolState.classes.find(c => c.id === classId); if (!cls) return; document.getElementById('class-modal-title').textContent = 'Klasse bearbeiten'; document.getElementById('class-edit-id').value = classId; document.getElementById('class-name').value = cls.name; document.getElementById('class-grade-level').value = cls.grade_level; document.getElementById('class-school-type').value = cls.school_type || 'gymnasium'; document.getElementById('class-federal-state').value = cls.federal_state || 'niedersachsen'; document.getElementById('school-class-modal').classList.add('active'); } // ========== STUDENTS ========== async function schoolViewStudents(classId) { schoolState.currentClassId = classId; document.getElementById('student-class-id').value = classId; try { const students = await schoolApiCall(`/classes/${classId}/students`); schoolShowStudentList(students || []); } catch (error) { schoolShowStudentList([]); } } function schoolShowStudentList(students) { const cls = schoolState.classes.find(c => c.id === schoolState.currentClassId); const clsName = cls ? cls.name : 'Klasse'; let html = `

Schueler - ${clsName}

`; if (students.length === 0) { html += `
👥

Keine Schueler

Fuegen Sie Schueler zur Klasse hinzu.

`; } else { html += ` `; students.forEach(student => { html += ` `; }); html += '
Name Geburtsdatum Schuelernr. Aktionen
${student.last_name}, ${student.first_name} ${student.birth_date || '-'} ${student.student_number || '-'}
'; } html += '
'; // Remove existing modal if any const existing = document.getElementById('student-list-modal'); if (existing) existing.remove(); document.body.insertAdjacentHTML('beforeend', html); } function schoolShowStudentModal() { document.getElementById('school-student-form').reset(); document.getElementById('school-student-modal').classList.add('active'); } function schoolStudentTab(tab) { const singleForm = document.getElementById('student-single-form'); const csvForm = document.getElementById('student-csv-form'); const tabs = document.querySelectorAll('#school-student-modal .school-tab'); tabs.forEach(t => t.classList.remove('active')); if (tab === 'single') { singleForm.style.display = 'block'; csvForm.style.display = 'none'; tabs[0].classList.add('active'); } else { singleForm.style.display = 'none'; csvForm.style.display = 'block'; tabs[1].classList.add('active'); } } async function schoolSaveStudent() { const classId = schoolState.currentClassId; if (!classId) return; const data = { first_name: document.getElementById('student-first-name').value, last_name: document.getElementById('student-last-name').value, birth_date: document.getElementById('student-birth-date').value || null, student_number: document.getElementById('student-number').value || null }; try { await schoolApiCall(`/classes/${classId}/students`, 'POST', data); showToast('Schueler hinzugefuegt', 'success'); schoolCloseModal('student'); schoolViewStudents(classId); schoolLoadClasses(); } catch (error) { // Error handled in schoolApiCall } } async function schoolDeleteStudent(studentId) { if (!confirm('Schueler wirklich loeschen?')) return; const classId = schoolState.currentClassId; try { await schoolApiCall(`/classes/${classId}/students/${studentId}`, 'DELETE'); showToast('Schueler geloescht', 'success'); schoolViewStudents(classId); schoolLoadClasses(); } catch (error) { // Error handled in schoolApiCall } } // ========== SUBJECTS ========== async function schoolLoadSubjects() { try { const subjects = await schoolApiCall('/subjects'); schoolState.subjects = subjects || []; schoolUpdateSubjectSelects(); } catch (error) { // Create some default subjects if none exist schoolState.subjects = []; } } function schoolUpdateSubjectSelects() { const selects = ['exam-subject-filter', 'exam-subject']; selects.forEach(id => { const select = document.getElementById(id); if (select) { select.innerHTML = ''; schoolState.subjects.forEach(subj => { const option = document.createElement('option'); option.value = subj.id; option.textContent = subj.name; select.appendChild(option); }); } }); } // ========== EXAMS ========== async function schoolLoadExams() { const classId = document.getElementById('exam-class-filter')?.value; const subjectId = document.getElementById('exam-subject-filter')?.value; const status = document.getElementById('exam-status-filter')?.value; let url = '/exams?'; if (classId) url += `class_id=${classId}&`; if (subjectId) url += `subject_id=${subjectId}&`; if (status) url += `status=${status}&`; try { const exams = await schoolApiCall(url); schoolRenderExams(exams || []); } catch (error) { schoolRenderExams([]); } } function schoolRenderExams(exams) { const container = document.getElementById('school-exams-list'); if (!container) return; if (exams.length === 0) { container.innerHTML = `
📄

Keine Klausuren vorhanden

Erstellen Sie Ihre erste Klausur oder Test.

`; return; } container.innerHTML = ` ${exams.map(exam => ` `).join('')}
Titel Klasse Fach Typ Datum Status Aktionen
${exam.title} ${exam.class_name || '-'} ${exam.subject_name || '-'} ${exam.exam_type} ${exam.exam_date || '-'} ${exam.status}
`; } function schoolShowExamModal(examId = null) { document.getElementById('exam-modal-title').textContent = examId ? 'Klausur bearbeiten' : 'Neue Klausur'; document.getElementById('exam-edit-id').value = examId || ''; document.getElementById('school-exam-form').reset(); document.getElementById('school-exam-modal').classList.add('active'); } async function schoolSaveExam() { const editId = document.getElementById('exam-edit-id').value; const data = { title: document.getElementById('exam-title').value, class_id: document.getElementById('exam-class').value, subject_id: document.getElementById('exam-subject').value, exam_type: document.getElementById('exam-type').value, exam_date: document.getElementById('exam-date').value || null, topic: document.getElementById('exam-topic').value || null, max_points: parseFloat(document.getElementById('exam-max-points').value) || null, content: document.getElementById('exam-content').value || null }; try { if (editId) { await schoolApiCall(`/exams/${editId}`, 'PUT', data); showToast('Klausur aktualisiert', 'success'); } else { await schoolApiCall('/exams', 'POST', data); showToast('Klausur erstellt', 'success'); } schoolCloseModal('exam'); schoolLoadExams(); } catch (error) { // Error handled in schoolApiCall } } function schoolEditExam(examId) { // TODO: Load exam data and populate form schoolShowExamModal(examId); } function schoolShowResults(examId) { // TODO: Show exam results modal showToast('Ergebnisse-Ansicht in Entwicklung', 'info'); } // ========== GRADES ========== async function schoolLoadGrades() { const classId = document.getElementById('grades-class-select')?.value; const semester = document.getElementById('grades-semester-select')?.value; if (!classId) return; try { const grades = await schoolApiCall(`/grades/${classId}?semester=${semester}`); schoolRenderGrades(grades); document.getElementById('grades-stats').style.display = 'grid'; } catch (error) { schoolRenderGrades(null); } } function schoolRenderGrades(grades) { const container = document.getElementById('school-grades-table'); if (!container) return; if (!grades || !grades.students || grades.students.length === 0) { container.innerHTML = `
📊

Keine Noten vorhanden

Es wurden noch keine Noten fuer diese Klasse eingetragen.

`; return; } // Calculate stats let totalGrades = 0; let gradeSum = 0; let bestGrade = 6; let pendingCount = 0; grades.students.forEach(student => { if (student.final_grade) { totalGrades++; gradeSum += student.final_grade; if (student.final_grade < bestGrade) { bestGrade = student.final_grade; } } else { pendingCount++; } }); document.getElementById('stat-avg-grade').textContent = totalGrades > 0 ? (gradeSum / totalGrades).toFixed(2) : '-'; document.getElementById('stat-best-grade').textContent = bestGrade < 6 ? bestGrade.toFixed(1) : '-'; document.getElementById('stat-students').textContent = grades.students.length; document.getElementById('stat-pending').textContent = pendingCount; // Render table container.innerHTML = ` ${grades.students.map(student => ` `).join('')}
Name Schriftl. Muendl. Endnote Aktionen
${student.last_name}, ${student.first_name} ${student.written_grade_avg ? student.written_grade_avg.toFixed(2) : '-'} ${student.oral_grade ? student.oral_grade.toFixed(1) : '-'} ${student.final_grade ? `${student.final_grade.toFixed(1)}` : '-' }
`; } function schoolShowOralGradeModal(studentId, subjectId) { document.getElementById('oral-student-id').value = studentId; document.getElementById('oral-subject-id').value = subjectId || ''; document.getElementById('school-oral-grade-form').reset(); document.getElementById('school-oral-grade-modal').classList.add('active'); } async function schoolSaveOralGrade() { const studentId = document.getElementById('oral-student-id').value; const subjectId = document.getElementById('oral-subject-id').value; const grade = parseFloat(document.getElementById('oral-grade').value); const notes = document.getElementById('oral-notes').value; try { await schoolApiCall(`/grades/${studentId}/${subjectId}/oral`, 'PUT', { oral_grade: grade, oral_notes: notes }); showToast('Muendliche Note gespeichert', 'success'); schoolCloseModal('oral-grade'); schoolLoadGrades(); } catch (error) { // Error handled in schoolApiCall } } async function schoolCalculateGrades() { const classId = document.getElementById('grades-class-select')?.value; const semester = document.getElementById('grades-semester-select')?.value; if (!classId) { showToast('Bitte waehlen Sie eine Klasse', 'warning'); return; } try { await schoolApiCall('/grades/calculate', 'POST', { class_id: classId, semester: parseInt(semester) }); showToast('Noten berechnet', 'success'); schoolLoadGrades(); } catch (error) { // Error handled in schoolApiCall } } function schoolExportGrades() { showToast('Export-Funktion in Entwicklung', 'info'); } // ========== GRADEBOOK ========== function schoolSwitchGradebookTab(tab) { const attendanceTab = document.getElementById('gradebook-attendance-tab'); const entriesTab = document.getElementById('gradebook-entries-tab'); const tabs = document.querySelectorAll('#panel-school-gradebook .school-tab'); tabs.forEach(t => t.classList.remove('active')); if (tab === 'attendance') { attendanceTab.style.display = 'block'; entriesTab.style.display = 'none'; tabs[0].classList.add('active'); } else { attendanceTab.style.display = 'none'; entriesTab.style.display = 'block'; tabs[1].classList.add('active'); } } async function schoolLoadGradebook() { const classId = document.getElementById('gradebook-class-select')?.value; const date = document.getElementById('gradebook-date')?.value; if (!classId) return; try { const attendance = await schoolApiCall(`/attendance/${classId}?date=${date}`); schoolRenderAttendance(attendance); } catch (error) { schoolRenderAttendance([]); } } function schoolRenderAttendance(attendance) { const container = document.getElementById('gradebook-attendance-tab'); if (!container) return; if (!attendance || attendance.length === 0) { container.innerHTML = `
📖

Keine Fehlzeiten

Erfassen Sie Fehlzeiten fuer die Klasse.

`; return; } container.innerHTML = ` ${attendance.map(a => ` `).join('')}
Name Status Stunden Grund
${a.student_name} ${a.status} ${a.periods || 1} ${a.reason || '-'}
`; } function schoolShowAttendanceModal() { showToast('Fehlzeiten-Erfassung in Entwicklung', 'info'); } function schoolShowEntryModal() { showToast('Eintrag-Funktion in Entwicklung', 'info'); } // ========== CERTIFICATES ========== // Wizard state let wizardState = { currentStep: 1, classId: null, semester: 1, certType: 'halbjahr', template: 'generic_sekundarstufe1', students: [], remarks: {}, defaultRemark: '' }; async function schoolLoadCertificates() { const classId = document.getElementById('cert-class-select')?.value; const semester = document.getElementById('cert-semester-select')?.value; if (!classId) { document.getElementById('cert-stats').style.display = 'none'; document.getElementById('cert-notenspiegel').style.display = 'none'; document.getElementById('cert-workflow').style.display = 'none'; return; } try { // Load class statistics const stats = await schoolApiCall(`/statistics/${classId}?semester=${semester}`); schoolRenderCertificateStats(stats); // Load notenspiegel const notenspiegel = await schoolApiCall(`/statistics/${classId}/notenspiegel?semester=${semester}`); schoolRenderNotenspiegel(notenspiegel); // Load certificates const certs = await schoolApiCall(`/certificates/class/${classId}?semester=${semester}`); schoolRenderCertificatesList(certs); // Show workflow document.getElementById('cert-workflow').style.display = 'block'; } catch (error) { console.error('Error loading certificates:', error); document.getElementById('cert-stats').style.display = 'none'; document.getElementById('cert-notenspiegel').style.display = 'none'; schoolRenderCertificatesList([]); } } function schoolRenderCertificateStats(stats) { if (!stats) return; document.getElementById('cert-stats').style.display = 'grid'; document.getElementById('cert-stat-avg').textContent = stats.class_average ? stats.class_average.toFixed(2) : '-'; document.getElementById('cert-stat-pass').textContent = stats.pass_rate ? Math.round(stats.pass_rate) + '%' : '-'; document.getElementById('cert-stat-risk').textContent = stats.students_at_risk || '0'; document.getElementById('cert-stat-ready').textContent = stats.student_count || '0'; } function schoolRenderNotenspiegel(data) { if (!data || !data.distribution) return; document.getElementById('cert-notenspiegel').style.display = 'block'; const maxCount = Math.max(...Object.values(data.distribution), 1); for (let grade = 1; grade <= 6; grade++) { const bar = document.querySelector(`.notenspiegel-bar[data-grade="${grade}"] .notenspiegel-bar-fill`); if (bar) { const count = data.distribution[String(grade)] || 0; const height = (count / maxCount) * 100; bar.style.height = height + '%'; bar.title = count + ' Schueler'; } } } function schoolRenderCertificatesList(certs) { const container = document.getElementById('school-certificates-list'); if (!container) return; if (!certs || certs.length === 0) { container.innerHTML = `
🏆

Bereit zur Generierung

Klicken Sie auf "Zeugnisse generieren" oder nutzen Sie den Wizard.

`; return; } container.innerHTML = ` ${certs.map(cert => ` `).join('')}
Schueler Typ Status Erstellt Aktionen
${cert.student_name} ${cert.certificate_type} ${cert.status} ${cert.created_at ? new Date(cert.created_at).toLocaleDateString('de-DE') : '-'}
`; } async function schoolGenerateCertificates() { const classId = document.getElementById('cert-class-select')?.value; const semester = document.getElementById('cert-semester-select')?.value; const template = document.getElementById('cert-template-select')?.value; if (!classId) { showToast('Bitte waehlen Sie eine Klasse', 'warning'); return; } try { await schoolApiCall('/certificates/generate-bulk', 'POST', { class_id: classId, semester: parseInt(semester), template_name: template, certificate_type: 'halbjahr' }); showToast('Zeugnisse werden generiert...', 'success'); setTimeout(() => schoolLoadCertificates(), 2000); } catch (error) { showToast('Fehler bei der Generierung', 'error'); } } function schoolViewCertificate(certId) { showToast('Zeugnis-Ansicht in Entwicklung', 'info'); } function schoolDownloadCertificate(certId) { window.open(`${SCHOOL_API_BASE}/certificates/detail/${certId}/pdf`, '_blank'); } // ========== WIZARD FUNCTIONS ========== function schoolShowCertificateWizard() { wizardState = { currentStep: 1, classId: null, semester: 1, certType: 'halbjahr', template: 'generic_sekundarstufe1', students: [], remarks: {}, defaultRemark: '' }; // Populate class select const select = document.getElementById('wizard-class'); select.innerHTML = ''; schoolState.classes.forEach(cls => { const option = document.createElement('option'); option.value = cls.id; option.textContent = cls.name; select.appendChild(option); }); // Show first step wizardShowStep(1); document.getElementById('school-cert-wizard-modal').classList.add('active'); } function wizardShowStep(step) { wizardState.currentStep = step; // Hide all steps for (let i = 1; i <= 5; i++) { const stepEl = document.getElementById(`wizard-step-${i}`); if (stepEl) stepEl.style.display = 'none'; } // Show current step const currentStep = document.getElementById(`wizard-step-${step}`); if (currentStep) currentStep.style.display = 'block'; // Update step indicators document.querySelectorAll('.wizard-step').forEach((el, idx) => { const stepNum = idx + 1; const title = el.querySelector('div:first-child'); if (stepNum <= step) { el.classList.add('active'); if (title) title.style.color = 'var(--bp-primary)'; } else { el.classList.remove('active'); if (title) title.style.color = 'var(--bp-text-muted)'; } }); // Update buttons document.getElementById('wizard-prev-btn').style.display = step > 1 ? 'inline-flex' : 'none'; document.getElementById('wizard-next-btn').textContent = step === 5 ? 'Fertig' : 'Weiter'; // Load step content if (step === 2) wizardLoadGrades(); if (step === 4) wizardLoadRemarks(); if (step === 5) wizardLoadSummary(); } function wizardNextStep() { if (wizardState.currentStep === 5) { schoolCloseModal('cert-wizard'); return; } // Validate current step if (wizardState.currentStep === 1) { const classId = document.getElementById('wizard-class').value; if (!classId) { showToast('Bitte waehlen Sie eine Klasse', 'warning'); return; } wizardState.classId = classId; wizardState.semester = parseInt(document.getElementById('wizard-semester').value); wizardState.certType = document.getElementById('wizard-cert-type').value; } if (wizardState.currentStep === 3) { wizardState.template = document.getElementById('wizard-template').value; } if (wizardState.currentStep === 4) { wizardState.defaultRemark = document.getElementById('wizard-default-remark').value; } wizardShowStep(wizardState.currentStep + 1); } function wizardPrevStep() { if (wizardState.currentStep > 1) { wizardShowStep(wizardState.currentStep - 1); } } async function wizardLoadClassPreview() { const classId = document.getElementById('wizard-class').value; if (!classId) { document.getElementById('wizard-class-preview').style.display = 'none'; return; } try { const stats = await schoolApiCall(`/statistics/${classId}`); document.getElementById('wizard-class-preview').style.display = 'block'; document.getElementById('wizard-class-stats').innerHTML = `
Schueler: ${stats.student_count || 0}
Durchschnitt: ${stats.class_average ? stats.class_average.toFixed(2) : '-'}
Gefaehrdet: ${stats.students_at_risk || 0}
`; } catch (error) { document.getElementById('wizard-class-preview').style.display = 'none'; } } async function wizardLoadGrades() { const container = document.getElementById('wizard-grades-table'); container.innerHTML = '
'; try { // Get students const students = await schoolApiCall(`/classes/${wizardState.classId}/students`); wizardState.students = students || []; // Get grades const grades = await schoolApiCall(`/grades/${wizardState.classId}?semester=${wizardState.semester}`); container.innerHTML = ` ${wizardState.students.map(student => { const grade = grades?.find?.(g => g.student_id === student.id); const hasAllGrades = grade?.final_grade != null; return ` `; }).join('')}
Name Schnitt Muendl. Endnote Status
${student.last_name}, ${student.first_name} ${grade?.written_grade_avg?.toFixed(2) || '-'} ${grade?.oral_grade?.toFixed(1) || '-'} ${grade?.final_grade ? `${grade.final_grade.toFixed(1)}` : 'Fehlt' } ${hasAllGrades ? '✓' : '✗'}
`; } catch (error) { container.innerHTML = '

Fehler beim Laden der Noten.

'; } } function wizardUpdateTemplates() { const bundesland = document.getElementById('wizard-bundesland').value; const templateSelect = document.getElementById('wizard-template'); // Templates based on Bundesland const templates = { 'niedersachsen': [ { value: 'niedersachsen_gymnasium', text: 'Niedersachsen Gymnasium' }, { value: 'niedersachsen_realschule', text: 'Niedersachsen Realschule' } ], 'nordrhein-westfalen': [ { value: 'nrw_gymnasium', text: 'NRW Gymnasium' }, { value: 'nrw_gesamtschule', text: 'NRW Gesamtschule' } ], 'bayern': [ { value: 'bayern_gymnasium', text: 'Bayern Gymnasium' }, { value: 'bayern_realschule', text: 'Bayern Realschule' } ] }; const defaultTemplates = [ { value: 'generic_sekundarstufe1', text: 'Standard Sek I' }, { value: 'generic_sekundarstufe2', text: 'Standard Sek II' } ]; const options = templates[bundesland] || defaultTemplates; templateSelect.innerHTML = options.map(opt => `` ).join(''); } function wizardLoadRemarks() { const container = document.getElementById('wizard-remarks-list'); container.innerHTML = wizardState.students.map(student => `
${student.last_name}, ${student.first_name}
`).join(''); } function wizardLoadSummary() { const cls = schoolState.classes.find(c => c.id === wizardState.classId); document.getElementById('wizard-summary').innerHTML = `
Klasse: ${cls?.name || wizardState.classId} Halbjahr: ${wizardState.semester}. Halbjahr Zeugnisart: ${wizardState.certType} Vorlage: ${wizardState.template} Schueler: ${wizardState.students.length} Mit Bemerkungen: ${Object.keys(wizardState.remarks).filter(k => wizardState.remarks[k]).length}
`; } async function wizardGenerateCertificates() { const progressDiv = document.getElementById('wizard-progress'); const progressBar = document.getElementById('wizard-progress-bar'); const progressText = document.getElementById('wizard-progress-text'); progressDiv.style.display = 'block'; progressBar.style.width = '0%'; progressText.textContent = 'Starte Generierung...'; try { let completed = 0; const total = wizardState.students.length; for (const student of wizardState.students) { progressText.textContent = `Generiere Zeugnis fuer ${student.first_name} ${student.last_name}...`; await schoolApiCall('/certificates/generate', 'POST', { student_id: student.id, school_year_id: schoolState.currentYearId, semester: wizardState.semester, certificate_type: wizardState.certType, template_name: wizardState.template, remarks: wizardState.remarks[student.id] || wizardState.defaultRemark }); completed++; progressBar.style.width = (completed / total * 100) + '%'; } progressText.textContent = 'Alle Zeugnisse generiert!'; showToast(`${total} Zeugnisse erfolgreich generiert`, 'success'); setTimeout(() => { schoolCloseModal('cert-wizard'); schoolLoadCertificates(); }, 1500); } catch (error) { progressText.textContent = 'Fehler bei der Generierung'; showToast('Fehler: ' + error.message, 'error'); } } // ========== MODAL HELPERS ========== function schoolCloseModal(type) { const modal = document.getElementById(`school-${type}-modal`); if (modal) { modal.classList.remove('active'); } } // ========== MODULE LOADING HOOKS ========== // Define load functions for each school module panel // These are called by the global loadModule function in base.py window.loadSchoolClassesModule = function() { console.log('Loading School Classes module'); schoolInit(); }; window.loadSchoolExamsModule = function() { console.log('Loading School Exams module'); schoolInit(); }; window.loadSchoolGradesModule = function() { console.log('Loading School Grades module'); schoolInit(); }; window.loadSchoolGradebookModule = function() { console.log('Loading School Gradebook module'); schoolInit(); }; window.loadSchoolCertificatesModule = function() { console.log('Loading School Certificates module'); schoolInit(); }; // Initialize on DOM ready if school panel is active document.addEventListener('DOMContentLoaded', function() { const activeSchoolPanel = document.querySelector('.panel-school-classes.active, .panel-school-exams.active, .panel-school-grades.active, .panel-school-gradebook.active, .panel-school-certificates.active'); if (activeSchoolPanel) { schoolInit(); } }); console.log('School module loaded'); """