"""
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 """
👥
Keine Klassen vorhanden
Erstellen Sie Ihre erste Klasse, um Schueler zu verwalten.
📄
Keine Klausuren vorhanden
Erstellen Sie Ihre erste Klausur oder Test.
📊
Klasse waehlen
Waehlen Sie eine Klasse, um den Notenspiegel anzuzeigen.
📖
Klasse waehlen
Waehlen Sie eine Klasse, um das Klassenbuch anzuzeigen.
📝
Keine Eintragungen
Es gibt keine Eintragungen fuer diesen Tag.
Workflow-Status
Noten eingegeben
➜
Klassenlehrer
➜
Zeugnisbeauftragter
➜
Schulleitung
➜
Gedruckt
🏆
Keine Zeugnisse
Waehlen Sie eine Klasse und generieren Sie Zeugnisse.
1. Klasse
Klasse auswaehlen
3. Vorlage
Zeugnisvorlage
4. Bemerkungen
Bemerkungen
Ueberpruefen Sie die Noten der Schueler. Rot markierte Felder haben noch keine Note.
Individuelle Bemerkungen
Klicken Sie auf einen Schueler, um individuelle Bemerkungen hinzuzufuegen.
Zusammenfassung
"""
@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.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 = `
`;
if (students.length === 0) {
html += `
👥
Keine Schueler
Fuegen Sie Schueler zur Klasse hinzu.
`;
} else {
html += `
| Name |
Geburtsdatum |
Schuelernr. |
Aktionen |
`;
students.forEach(student => {
html += `
| ${student.last_name}, ${student.first_name} |
${student.birth_date || '-'} |
${student.student_number || '-'} |
|
`;
});
html += '
';
}
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 = `
| Titel |
Klasse |
Fach |
Typ |
Datum |
Status |
Aktionen |
${exams.map(exam => `
| ${exam.title} |
${exam.class_name || '-'} |
${exam.subject_name || '-'} |
${exam.exam_type} |
${exam.exam_date || '-'} |
${exam.status} |
|
`).join('')}
`;
}
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 = `
| Name |
Schriftl. |
Muendl. |
Endnote |
Aktionen |
${grades.students.map(student => `
| ${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)}`
: '-'
}
|
|
`).join('')}
`;
}
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 = `
| Name |
Status |
Stunden |
Grund |
${attendance.map(a => `
| ${a.student_name} |
${a.status} |
${a.periods || 1} |
${a.reason || '-'} |
`).join('')}
`;
}
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 = `
| Schueler |
Typ |
Status |
Erstellt |
Aktionen |
${certs.map(cert => `
| ${cert.student_name} |
${cert.certificate_type} |
${cert.status} |
${cert.created_at ? new Date(cert.created_at).toLocaleDateString('de-DE') : '-'} |
|
`).join('')}
`;
}
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 = `
| Name |
Schnitt |
Muendl. |
Endnote |
Status |
${wizardState.students.map(student => {
const grade = grades?.find?.(g => g.student_id === student.id);
const hasAllGrades = grade?.final_grade != null;
return `
| ${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 ? '✓' : '✗'} |
`;
}).join('')}
`;
} 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');
"""