This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

2467 lines
77 KiB
Python

"""
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 """
<!-- =============================================
SCHOOL MODULE - Klassen & Schueler Panel
============================================= -->
<div class="panel panel-school-classes" id="panel-school-classes">
<div class="school-header">
<h2>Klassen & Schueler</h2>
<div class="school-header-actions">
<button class="btn btn-secondary" onclick="schoolShowYearModal()">
Schuljahr
</button>
<button class="btn btn-primary" onclick="schoolShowClassModal()">
+ Neue Klasse
</button>
</div>
</div>
<div class="school-content">
<!-- Schuljahr Auswahl -->
<div class="school-form" style="padding: 16px;">
<div style="display: flex; gap: 16px; align-items: center;">
<label style="font-weight: 500;">Schuljahr:</label>
<select id="school-year-select" onchange="schoolLoadClasses()" style="flex: 1; max-width: 200px;">
<option value="">Schuljahr waehlen...</option>
</select>
</div>
</div>
<!-- Klassen Grid -->
<div id="school-classes-list" class="school-cards-grid">
<div class="school-empty-state">
<div class="school-empty-state-icon">&#128101;</div>
<h3>Keine Klassen vorhanden</h3>
<p>Erstellen Sie Ihre erste Klasse, um Schueler zu verwalten.</p>
<button class="btn btn-primary" onclick="schoolShowClassModal()">
Erste Klasse anlegen
</button>
</div>
</div>
</div>
</div>
<!-- =============================================
SCHOOL MODULE - Klausuren & Tests Panel
============================================= -->
<div class="panel panel-school-exams" id="panel-school-exams">
<div class="school-header">
<h2>Klausuren & Tests</h2>
<div class="school-header-actions">
<button class="btn btn-primary" onclick="schoolShowExamModal()">
+ Neue Klausur
</button>
</div>
</div>
<div class="school-content">
<!-- Filter -->
<div class="school-form" style="padding: 16px;">
<div class="school-form-row">
<div class="school-form-group">
<label>Klasse</label>
<select id="exam-class-filter" onchange="schoolLoadExams()">
<option value="">Alle Klassen</option>
</select>
</div>
<div class="school-form-group">
<label>Fach</label>
<select id="exam-subject-filter" onchange="schoolLoadExams()">
<option value="">Alle Faecher</option>
</select>
</div>
<div class="school-form-group">
<label>Status</label>
<select id="exam-status-filter" onchange="schoolLoadExams()">
<option value="">Alle</option>
<option value="draft">Entwurf</option>
<option value="active">Aktiv</option>
<option value="archived">Archiviert</option>
</select>
</div>
</div>
</div>
<!-- Klausuren Liste -->
<div id="school-exams-list">
<div class="school-empty-state">
<div class="school-empty-state-icon">&#128196;</div>
<h3>Keine Klausuren vorhanden</h3>
<p>Erstellen Sie Ihre erste Klausur oder Test.</p>
<button class="btn btn-primary" onclick="schoolShowExamModal()">
Erste Klausur anlegen
</button>
</div>
</div>
</div>
</div>
<!-- =============================================
SCHOOL MODULE - Notenspiegel Panel
============================================= -->
<div class="panel panel-school-grades" id="panel-school-grades">
<div class="school-header">
<h2>Notenspiegel</h2>
<div class="school-header-actions">
<button class="btn btn-secondary" onclick="schoolCalculateGrades()">
Noten berechnen
</button>
<button class="btn btn-secondary" onclick="schoolExportGrades()">
Exportieren
</button>
</div>
</div>
<div class="school-content">
<!-- Filter -->
<div class="school-form" style="padding: 16px;">
<div class="school-form-row">
<div class="school-form-group">
<label>Klasse</label>
<select id="grades-class-select" onchange="schoolLoadGrades()">
<option value="">Klasse waehlen...</option>
</select>
</div>
<div class="school-form-group">
<label>Halbjahr</label>
<select id="grades-semester-select" onchange="schoolLoadGrades()">
<option value="1">1. Halbjahr</option>
<option value="2">2. Halbjahr</option>
</select>
</div>
</div>
</div>
<!-- Statistiken -->
<div class="school-stats" id="grades-stats" style="display: none;">
<div class="school-stat-card">
<div class="school-stat-value" id="stat-avg-grade">-</div>
<div class="school-stat-label">Durchschnitt</div>
</div>
<div class="school-stat-card">
<div class="school-stat-value" id="stat-best-grade">-</div>
<div class="school-stat-label">Beste Note</div>
</div>
<div class="school-stat-card">
<div class="school-stat-value" id="stat-students">-</div>
<div class="school-stat-label">Schueler</div>
</div>
<div class="school-stat-card">
<div class="school-stat-value" id="stat-pending">-</div>
<div class="school-stat-label">Ausstehend</div>
</div>
</div>
<!-- Noten Tabelle -->
<div id="school-grades-table">
<div class="school-empty-state">
<div class="school-empty-state-icon">&#128202;</div>
<h3>Klasse waehlen</h3>
<p>Waehlen Sie eine Klasse, um den Notenspiegel anzuzeigen.</p>
</div>
</div>
</div>
</div>
<!-- =============================================
SCHOOL MODULE - Klassenbuch Panel
============================================= -->
<div class="panel panel-school-gradebook" id="panel-school-gradebook">
<div class="school-header">
<h2>Klassenbuch</h2>
<div class="school-header-actions">
<button class="btn btn-secondary" onclick="schoolShowAttendanceModal()">
Fehlzeiten erfassen
</button>
<button class="btn btn-primary" onclick="schoolShowEntryModal()">
+ Neuer Eintrag
</button>
</div>
</div>
<div class="school-content">
<!-- Tabs -->
<div class="school-tabs">
<button class="school-tab active" onclick="schoolSwitchGradebookTab('attendance')">
Fehlzeiten
</button>
<button class="school-tab" onclick="schoolSwitchGradebookTab('entries')">
Eintragungen
</button>
</div>
<!-- Filter -->
<div class="school-form" style="padding: 16px;">
<div class="school-form-row">
<div class="school-form-group">
<label>Klasse</label>
<select id="gradebook-class-select" onchange="schoolLoadGradebook()">
<option value="">Klasse waehlen...</option>
</select>
</div>
<div class="school-form-group">
<label>Datum</label>
<input type="date" id="gradebook-date" onchange="schoolLoadGradebook()">
</div>
</div>
</div>
<!-- Fehlzeiten Tab -->
<div id="gradebook-attendance-tab">
<div class="school-empty-state">
<div class="school-empty-state-icon">&#128214;</div>
<h3>Klasse waehlen</h3>
<p>Waehlen Sie eine Klasse, um das Klassenbuch anzuzeigen.</p>
</div>
</div>
<!-- Eintragungen Tab -->
<div id="gradebook-entries-tab" style="display: none;">
<div class="school-empty-state">
<div class="school-empty-state-icon">&#128221;</div>
<h3>Keine Eintragungen</h3>
<p>Es gibt keine Eintragungen fuer diesen Tag.</p>
</div>
</div>
</div>
</div>
<!-- =============================================
SCHOOL MODULE - Zeugnisse Panel
============================================= -->
<div class="panel panel-school-certificates" id="panel-school-certificates">
<div class="school-header">
<h2>Zeugnisse</h2>
<div class="school-header-actions">
<button class="btn btn-secondary" onclick="schoolShowCertificateWizard()">
Zeugnis-Wizard
</button>
<button class="btn btn-primary" onclick="schoolGenerateCertificates()">
Zeugnisse generieren
</button>
</div>
</div>
<div class="school-content">
<!-- Filter -->
<div class="school-form" style="padding: 16px;">
<div class="school-form-row">
<div class="school-form-group">
<label>Klasse</label>
<select id="cert-class-select" onchange="schoolLoadCertificates()">
<option value="">Klasse waehlen...</option>
</select>
</div>
<div class="school-form-group">
<label>Halbjahr</label>
<select id="cert-semester-select" onchange="schoolLoadCertificates()">
<option value="1">1. Halbjahr</option>
<option value="2">2. Halbjahr</option>
</select>
</div>
<div class="school-form-group">
<label>Vorlage</label>
<select id="cert-template-select">
<option value="generic_sekundarstufe1">Standard Sek I</option>
<option value="generic_sekundarstufe2">Standard Sek II</option>
<option value="niedersachsen_gymnasium">Niedersachsen Gymnasium</option>
</select>
</div>
</div>
</div>
<!-- Statistik-Karten -->
<div class="school-stats" id="cert-stats" style="display: none;">
<div class="school-stat-card">
<div class="school-stat-value" id="cert-stat-avg">-</div>
<div class="school-stat-label">Klassendurchschnitt</div>
</div>
<div class="school-stat-card">
<div class="school-stat-value" id="cert-stat-pass">-</div>
<div class="school-stat-label">Bestanden</div>
</div>
<div class="school-stat-card">
<div class="school-stat-value" id="cert-stat-risk">-</div>
<div class="school-stat-label">Gefaehrdet</div>
</div>
<div class="school-stat-card">
<div class="school-stat-value" id="cert-stat-ready">-</div>
<div class="school-stat-label">Zeugnis-bereit</div>
</div>
</div>
<!-- Notenspiegel Chart -->
<div id="cert-notenspiegel" style="display: none; margin-bottom: 24px;">
<h4 style="margin-bottom: 12px;">Notenverteilung</h4>
<div style="display: flex; gap: 8px; align-items: flex-end; height: 120px;">
<div class="notenspiegel-bar" data-grade="1" style="flex: 1; text-align: center;">
<div class="notenspiegel-bar-fill" style="background: #28a745; height: 0%; transition: height 0.3s;"></div>
<span style="font-size: 12px; color: var(--bp-text-muted);">1</span>
</div>
<div class="notenspiegel-bar" data-grade="2" style="flex: 1; text-align: center;">
<div class="notenspiegel-bar-fill" style="background: #17a2b8; height: 0%; transition: height 0.3s;"></div>
<span style="font-size: 12px; color: var(--bp-text-muted);">2</span>
</div>
<div class="notenspiegel-bar" data-grade="3" style="flex: 1; text-align: center;">
<div class="notenspiegel-bar-fill" style="background: #ffc107; height: 0%; transition: height 0.3s;"></div>
<span style="font-size: 12px; color: var(--bp-text-muted);">3</span>
</div>
<div class="notenspiegel-bar" data-grade="4" style="flex: 1; text-align: center;">
<div class="notenspiegel-bar-fill" style="background: #fd7e14; height: 0%; transition: height 0.3s;"></div>
<span style="font-size: 12px; color: var(--bp-text-muted);">4</span>
</div>
<div class="notenspiegel-bar" data-grade="5" style="flex: 1; text-align: center;">
<div class="notenspiegel-bar-fill" style="background: #dc3545; height: 0%; transition: height 0.3s;"></div>
<span style="font-size: 12px; color: var(--bp-text-muted);">5</span>
</div>
<div class="notenspiegel-bar" data-grade="6" style="flex: 1; text-align: center;">
<div class="notenspiegel-bar-fill" style="background: #6c757d; height: 0%; transition: height 0.3s;"></div>
<span style="font-size: 12px; color: var(--bp-text-muted);">6</span>
</div>
</div>
</div>
<!-- Workflow-Status -->
<div id="cert-workflow" style="display: none; margin-bottom: 24px;">
<h4 style="margin-bottom: 12px;">Workflow-Status</h4>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<span class="workflow-step" id="workflow-noten">Noten eingegeben</span>
<span style="color: var(--bp-text-muted);">&#10140;</span>
<span class="workflow-step" id="workflow-klassenlehrer">Klassenlehrer</span>
<span style="color: var(--bp-text-muted);">&#10140;</span>
<span class="workflow-step" id="workflow-zb">Zeugnisbeauftragter</span>
<span style="color: var(--bp-text-muted);">&#10140;</span>
<span class="workflow-step" id="workflow-schulleitung">Schulleitung</span>
<span style="color: var(--bp-text-muted);">&#10140;</span>
<span class="workflow-step" id="workflow-druck">Gedruckt</span>
</div>
</div>
<!-- Zeugnisse Liste -->
<div id="school-certificates-list">
<div class="school-empty-state">
<div class="school-empty-state-icon">&#127942;</div>
<h3>Keine Zeugnisse</h3>
<p>Waehlen Sie eine Klasse und generieren Sie Zeugnisse.</p>
</div>
</div>
</div>
</div>
<!-- Zeugnis-Wizard Modal -->
<div class="school-modal" id="school-cert-wizard-modal">
<div class="school-modal-content" style="max-width: 900px;">
<div class="school-modal-header">
<h3>Zeugnis-Wizard</h3>
<button class="school-modal-close" onclick="schoolCloseModal('cert-wizard')">&times;</button>
</div>
<div class="school-modal-body">
<!-- Wizard Steps -->
<div class="wizard-steps" style="display: flex; margin-bottom: 24px; border-bottom: 1px solid var(--bp-border); padding-bottom: 16px;">
<div class="wizard-step active" data-step="1" style="flex: 1; text-align: center; padding: 8px; cursor: pointer;">
<div style="font-weight: 600; color: var(--bp-primary);">1. Klasse</div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Klasse auswaehlen</div>
</div>
<div class="wizard-step" data-step="2" style="flex: 1; text-align: center; padding: 8px; cursor: pointer;">
<div style="font-weight: 600; color: var(--bp-text-muted);">2. Noten</div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Noten pruefen</div>
</div>
<div class="wizard-step" data-step="3" style="flex: 1; text-align: center; padding: 8px; cursor: pointer;">
<div style="font-weight: 600; color: var(--bp-text-muted);">3. Vorlage</div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Zeugnisvorlage</div>
</div>
<div class="wizard-step" data-step="4" style="flex: 1; text-align: center; padding: 8px; cursor: pointer;">
<div style="font-weight: 600; color: var(--bp-text-muted);">4. Bemerkungen</div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Bemerkungen</div>
</div>
<div class="wizard-step" data-step="5" style="flex: 1; text-align: center; padding: 8px; cursor: pointer;">
<div style="font-weight: 600; color: var(--bp-text-muted);">5. Generieren</div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Erstellen</div>
</div>
</div>
<!-- Step 1: Klasse -->
<div class="wizard-content" id="wizard-step-1">
<div class="school-form-row">
<div class="school-form-group">
<label>Klasse auswaehlen</label>
<select id="wizard-class" onchange="wizardLoadClassPreview()">
<option value="">Klasse waehlen...</option>
</select>
</div>
<div class="school-form-group">
<label>Halbjahr</label>
<select id="wizard-semester">
<option value="1">1. Halbjahr</option>
<option value="2">2. Halbjahr</option>
</select>
</div>
<div class="school-form-group">
<label>Zeugnisart</label>
<select id="wizard-cert-type">
<option value="halbjahr">Halbjahreszeugnis</option>
<option value="jahres">Jahreszeugnis</option>
<option value="abschluss">Abschlusszeugnis</option>
</select>
</div>
</div>
<div id="wizard-class-preview" style="margin-top: 16px; padding: 16px; background: var(--bp-bg); border-radius: 8px; display: none;">
<h4 style="margin-bottom: 12px;">Klassenvorschau</h4>
<div id="wizard-class-stats"></div>
</div>
</div>
<!-- Step 2: Noten -->
<div class="wizard-content" id="wizard-step-2" style="display: none;">
<p style="margin-bottom: 16px; color: var(--bp-text-muted);">
Ueberpruefen Sie die Noten der Schueler. Rot markierte Felder haben noch keine Note.
</p>
<div id="wizard-grades-table" style="max-height: 400px; overflow-y: auto;">
<div class="school-loading"><div class="school-spinner"></div></div>
</div>
</div>
<!-- Step 3: Vorlage -->
<div class="wizard-content" id="wizard-step-3" style="display: none;">
<div class="school-form-row">
<div class="school-form-group">
<label>Bundesland</label>
<select id="wizard-bundesland" onchange="wizardUpdateTemplates()">
<option value="niedersachsen">Niedersachsen</option>
<option value="nordrhein-westfalen">Nordrhein-Westfalen</option>
<option value="bayern">Bayern</option>
<option value="baden-wuerttemberg">Baden-Wuerttemberg</option>
<option value="hessen">Hessen</option>
</select>
</div>
<div class="school-form-group">
<label>Vorlage</label>
<select id="wizard-template">
<option value="generic_sekundarstufe1">Standard Sek I</option>
<option value="generic_sekundarstufe2">Standard Sek II</option>
<option value="niedersachsen_gymnasium">Niedersachsen Gymnasium</option>
</select>
</div>
</div>
<div class="school-form-group" style="margin-top: 16px;">
<label>Vorlage hochladen (optional)</label>
<input type="file" id="wizard-template-file" accept=".docx,.odt,.pdf">
<p style="font-size: 12px; color: var(--bp-text-muted); margin-top: 4px;">
Laden Sie eine eigene Zeugnisvorlage hoch (DOCX, ODT oder PDF).
</p>
</div>
</div>
<!-- Step 4: Bemerkungen -->
<div class="wizard-content" id="wizard-step-4" style="display: none;">
<div class="school-form-group">
<label>Standardbemerkung (fuer alle Schueler)</label>
<textarea id="wizard-default-remark" rows="3" placeholder="Z.B. Versetzt in Klasse X..."></textarea>
</div>
<div style="margin-top: 16px;">
<h4 style="margin-bottom: 12px;">Individuelle Bemerkungen</h4>
<p style="font-size: 13px; color: var(--bp-text-muted); margin-bottom: 12px;">
Klicken Sie auf einen Schueler, um individuelle Bemerkungen hinzuzufuegen.
</p>
<div id="wizard-remarks-list" style="max-height: 300px; overflow-y: auto;"></div>
</div>
</div>
<!-- Step 5: Generieren -->
<div class="wizard-content" id="wizard-step-5" style="display: none;">
<div style="text-align: center; padding: 20px;">
<h3 style="margin-bottom: 16px;">Zusammenfassung</h3>
<div id="wizard-summary" style="text-align: left; background: var(--bp-bg); padding: 16px; border-radius: 8px; margin-bottom: 20px;"></div>
<button class="btn btn-primary btn-lg" onclick="wizardGenerateCertificates()">
Zeugnisse jetzt generieren
</button>
</div>
<div id="wizard-progress" style="display: none; margin-top: 20px;">
<div style="background: var(--bp-border); border-radius: 8px; height: 8px; overflow: hidden;">
<div id="wizard-progress-bar" style="background: var(--bp-primary); height: 100%; width: 0%; transition: width 0.3s;"></div>
</div>
<p id="wizard-progress-text" style="text-align: center; margin-top: 8px; font-size: 13px; color: var(--bp-text-muted);"></p>
</div>
</div>
</div>
<div class="school-modal-footer">
<button class="btn btn-secondary" id="wizard-prev-btn" onclick="wizardPrevStep()" style="display: none;">
Zurueck
</button>
<button class="btn btn-primary" id="wizard-next-btn" onclick="wizardNextStep()">
Weiter
</button>
</div>
</div>
</div>
<!-- =============================================
SCHOOL MODALS
============================================= -->
<!-- Klasse Modal -->
<div class="school-modal" id="school-class-modal">
<div class="school-modal-content">
<div class="school-modal-header">
<h3 id="class-modal-title">Neue Klasse</h3>
<button class="school-modal-close" onclick="schoolCloseModal('class')">&times;</button>
</div>
<div class="school-modal-body">
<form id="school-class-form">
<input type="hidden" id="class-edit-id">
<div class="school-form-row">
<div class="school-form-group">
<label>Klassenname *</label>
<input type="text" id="class-name" placeholder="z.B. 7a" required>
</div>
<div class="school-form-group">
<label>Jahrgang *</label>
<select id="class-grade-level" required>
<option value="5">5. Klasse</option>
<option value="6">6. Klasse</option>
<option value="7" selected>7. Klasse</option>
<option value="8">8. Klasse</option>
<option value="9">9. Klasse</option>
<option value="10">10. Klasse</option>
<option value="11">11. Klasse (Q1)</option>
<option value="12">12. Klasse (Q2)</option>
<option value="13">13. Klasse</option>
</select>
</div>
</div>
<div class="school-form-row">
<div class="school-form-group">
<label>Schulform</label>
<select id="class-school-type">
<option value="gymnasium">Gymnasium</option>
<option value="realschule">Realschule</option>
<option value="hauptschule">Hauptschule</option>
<option value="gesamtschule">Gesamtschule</option>
<option value="grundschule">Grundschule</option>
</select>
</div>
<div class="school-form-group">
<label>Bundesland</label>
<select id="class-federal-state">
<option value="niedersachsen">Niedersachsen</option>
<option value="nrw">Nordrhein-Westfalen</option>
<option value="bayern">Bayern</option>
<option value="bw">Baden-Wuerttemberg</option>
<option value="hessen">Hessen</option>
<option value="andere">Andere</option>
</select>
</div>
</div>
</form>
</div>
<div class="school-modal-footer">
<button class="btn btn-secondary" onclick="schoolCloseModal('class')">Abbrechen</button>
<button class="btn btn-primary" onclick="schoolSaveClass()">Speichern</button>
</div>
</div>
</div>
<!-- Schueler Modal -->
<div class="school-modal" id="school-student-modal">
<div class="school-modal-content">
<div class="school-modal-header">
<h3>Schueler hinzufuegen</h3>
<button class="school-modal-close" onclick="schoolCloseModal('student')">&times;</button>
</div>
<div class="school-modal-body">
<div class="school-tabs" style="margin-bottom: 20px;">
<button class="school-tab active" onclick="schoolStudentTab('single')">Einzeln</button>
<button class="school-tab" onclick="schoolStudentTab('csv')">CSV Import</button>
</div>
<div id="student-single-form">
<form id="school-student-form">
<input type="hidden" id="student-class-id">
<div class="school-form-row">
<div class="school-form-group">
<label>Vorname *</label>
<input type="text" id="student-first-name" required>
</div>
<div class="school-form-group">
<label>Nachname *</label>
<input type="text" id="student-last-name" required>
</div>
</div>
<div class="school-form-row">
<div class="school-form-group">
<label>Geburtsdatum</label>
<input type="date" id="student-birth-date">
</div>
<div class="school-form-group">
<label>Schuelernummer</label>
<input type="text" id="student-number">
</div>
</div>
</form>
</div>
<div id="student-csv-form" style="display: none;">
<div class="school-form-group">
<label>CSV-Datei (Vorname;Nachname;Geburtsdatum)</label>
<input type="file" id="student-csv-file" accept=".csv">
</div>
<p style="font-size: 12px; color: var(--bp-text-muted);">
Format: Eine Zeile pro Schueler, Semikolon-getrennt.<br>
Beispiel: Max;Mustermann;2010-05-15
</p>
</div>
</div>
<div class="school-modal-footer">
<button class="btn btn-secondary" onclick="schoolCloseModal('student')">Abbrechen</button>
<button class="btn btn-primary" onclick="schoolSaveStudent()">Speichern</button>
</div>
</div>
</div>
<!-- Exam Modal -->
<div class="school-modal" id="school-exam-modal">
<div class="school-modal-content" style="max-width: 700px;">
<div class="school-modal-header">
<h3 id="exam-modal-title">Neue Klausur</h3>
<button class="school-modal-close" onclick="schoolCloseModal('exam')">&times;</button>
</div>
<div class="school-modal-body">
<form id="school-exam-form">
<input type="hidden" id="exam-edit-id">
<div class="school-form-row">
<div class="school-form-group">
<label>Titel *</label>
<input type="text" id="exam-title" placeholder="z.B. 1. Klassenarbeit Mathematik" required>
</div>
</div>
<div class="school-form-row">
<div class="school-form-group">
<label>Klasse *</label>
<select id="exam-class" required>
<option value="">Klasse waehlen...</option>
</select>
</div>
<div class="school-form-group">
<label>Fach *</label>
<select id="exam-subject" required>
<option value="">Fach waehlen...</option>
</select>
</div>
</div>
<div class="school-form-row">
<div class="school-form-group">
<label>Typ</label>
<select id="exam-type">
<option value="klassenarbeit">Klassenarbeit</option>
<option value="test">Test</option>
<option value="klausur">Klausur</option>
</select>
</div>
<div class="school-form-group">
<label>Datum</label>
<input type="date" id="exam-date">
</div>
</div>
<div class="school-form-row">
<div class="school-form-group">
<label>Thema</label>
<input type="text" id="exam-topic" placeholder="z.B. Lineare Gleichungen">
</div>
<div class="school-form-group">
<label>Max. Punkte</label>
<input type="number" id="exam-max-points" placeholder="100">
</div>
</div>
<div class="school-form-group">
<label>Aufgaben (Markdown)</label>
<textarea id="exam-content" rows="6" placeholder="# Aufgabe 1 (10 Punkte)&#10;Berechne..."></textarea>
</div>
</form>
</div>
<div class="school-modal-footer">
<button class="btn btn-secondary" onclick="schoolCloseModal('exam')">Abbrechen</button>
<button class="btn btn-primary" onclick="schoolSaveExam()">Speichern</button>
</div>
</div>
</div>
<!-- Oral Grade Modal -->
<div class="school-modal" id="school-oral-grade-modal">
<div class="school-modal-content">
<div class="school-modal-header">
<h3>Muendliche Note eintragen</h3>
<button class="school-modal-close" onclick="schoolCloseModal('oral-grade')">&times;</button>
</div>
<div class="school-modal-body">
<form id="school-oral-grade-form">
<input type="hidden" id="oral-student-id">
<input type="hidden" id="oral-subject-id">
<div class="school-form-group">
<label>Note (1-6)</label>
<select id="oral-grade">
<option value="1.0">1.0 - Sehr gut</option>
<option value="1.3">1.3</option>
<option value="1.7">1.7</option>
<option value="2.0">2.0 - Gut</option>
<option value="2.3">2.3</option>
<option value="2.7">2.7</option>
<option value="3.0">3.0 - Befriedigend</option>
<option value="3.3">3.3</option>
<option value="3.7">3.7</option>
<option value="4.0">4.0 - Ausreichend</option>
<option value="4.3">4.3</option>
<option value="4.7">4.7</option>
<option value="5.0">5.0 - Mangelhaft</option>
<option value="5.3">5.3</option>
<option value="5.7">5.7</option>
<option value="6.0">6.0 - Ungenuegend</option>
</select>
</div>
<div class="school-form-group">
<label>Bemerkungen</label>
<textarea id="oral-notes" rows="3" placeholder="Optionale Bemerkungen..."></textarea>
</div>
</form>
</div>
<div class="school-modal-footer">
<button class="btn btn-secondary" onclick="schoolCloseModal('oral-grade')">Abbrechen</button>
<button class="btn btn-primary" onclick="schoolSaveOralGrade()">Speichern</button>
</div>
</div>
</div>
"""
@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 = '<option value="">Schuljahr waehlen...</option>';
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 = `
<div class="school-empty-state">
<div class="school-empty-state-icon">&#128101;</div>
<h3>Keine Klassen vorhanden</h3>
<p>Erstellen Sie Ihre erste Klasse, um Schueler zu verwalten.</p>
<button class="btn btn-primary" onclick="schoolShowClassModal()">
Erste Klasse anlegen
</button>
</div>
`;
return;
}
container.innerHTML = schoolState.classes.map(cls => `
<div class="school-card">
<div class="school-card-header">
<div class="school-card-title">${cls.name}</div>
<div class="school-card-badge">${cls.grade_level}. Klasse</div>
</div>
<div class="school-card-info">
${cls.school_type || 'Gymnasium'} | ${cls.student_count || 0} Schueler
</div>
<div class="school-card-actions">
<button class="btn btn-sm btn-secondary" onclick="schoolViewStudents('${cls.id}')">
Schueler
</button>
<button class="btn btn-sm btn-secondary" onclick="schoolEditClass('${cls.id}')">
Bearbeiten
</button>
<button class="btn btn-sm btn-danger" onclick="schoolDeleteClass('${cls.id}')">
Loeschen
</button>
</div>
</div>
`).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 = '<option value="">Klasse waehlen...</option>';
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 = `
<div class="school-modal active" id="student-list-modal">
<div class="school-modal-content" style="max-width: 800px;">
<div class="school-modal-header">
<h3>Schueler - ${clsName}</h3>
<button class="school-modal-close" onclick="document.getElementById('student-list-modal').remove()">&times;</button>
</div>
<div class="school-modal-body">
<button class="btn btn-primary" onclick="schoolShowStudentModal()" style="margin-bottom: 16px;">
+ Schueler hinzufuegen
</button>
`;
if (students.length === 0) {
html += `
<div class="school-empty-state">
<div class="school-empty-state-icon">&#128101;</div>
<h3>Keine Schueler</h3>
<p>Fuegen Sie Schueler zur Klasse hinzu.</p>
</div>
`;
} else {
html += `
<table class="school-table">
<thead>
<tr>
<th>Name</th>
<th>Geburtsdatum</th>
<th>Schuelernr.</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
`;
students.forEach(student => {
html += `
<tr>
<td>${student.last_name}, ${student.first_name}</td>
<td>${student.birth_date || '-'}</td>
<td>${student.student_number || '-'}</td>
<td>
<button class="btn btn-sm btn-danger" onclick="schoolDeleteStudent('${student.id}')">
Loeschen
</button>
</td>
</tr>
`;
});
html += '</tbody></table>';
}
html += '</div></div></div>';
// 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 = '<option value="">Fach waehlen...</option>';
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 = `
<div class="school-empty-state">
<div class="school-empty-state-icon">&#128196;</div>
<h3>Keine Klausuren vorhanden</h3>
<p>Erstellen Sie Ihre erste Klausur oder Test.</p>
<button class="btn btn-primary" onclick="schoolShowExamModal()">
Erste Klausur anlegen
</button>
</div>
`;
return;
}
container.innerHTML = `
<table class="school-table">
<thead>
<tr>
<th>Titel</th>
<th>Klasse</th>
<th>Fach</th>
<th>Typ</th>
<th>Datum</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
${exams.map(exam => `
<tr>
<td>${exam.title}</td>
<td>${exam.class_name || '-'}</td>
<td>${exam.subject_name || '-'}</td>
<td>${exam.exam_type}</td>
<td>${exam.exam_date || '-'}</td>
<td><span class="status-badge status-${exam.status}">${exam.status}</span></td>
<td>
<button class="btn btn-sm btn-secondary" onclick="schoolEditExam('${exam.id}')">Bearbeiten</button>
<button class="btn btn-sm btn-secondary" onclick="schoolShowResults('${exam.id}')">Ergebnisse</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
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 = `
<div class="school-empty-state">
<div class="school-empty-state-icon">&#128202;</div>
<h3>Keine Noten vorhanden</h3>
<p>Es wurden noch keine Noten fuer diese Klasse eingetragen.</p>
</div>
`;
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 = `
<table class="school-table">
<thead>
<tr>
<th>Name</th>
<th>Schriftl.</th>
<th>Muendl.</th>
<th>Endnote</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
${grades.students.map(student => `
<tr>
<td>${student.last_name}, ${student.first_name}</td>
<td>${student.written_grade_avg ? student.written_grade_avg.toFixed(2) : '-'}</td>
<td>${student.oral_grade ? student.oral_grade.toFixed(1) : '-'}</td>
<td>
${student.final_grade
? `<span class="grade-badge grade-${Math.round(student.final_grade)}">${student.final_grade.toFixed(1)}</span>`
: '-'
}
</td>
<td>
<button class="btn btn-sm btn-secondary" onclick="schoolShowOralGradeModal('${student.id}')">
Muendl. Note
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
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 = `
<div class="school-empty-state">
<div class="school-empty-state-icon">&#128214;</div>
<h3>Keine Fehlzeiten</h3>
<p>Erfassen Sie Fehlzeiten fuer die Klasse.</p>
<button class="btn btn-primary" onclick="schoolShowAttendanceModal()">
Fehlzeiten erfassen
</button>
</div>
`;
return;
}
container.innerHTML = `
<table class="school-table">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Stunden</th>
<th>Grund</th>
</tr>
</thead>
<tbody>
${attendance.map(a => `
<tr>
<td>${a.student_name}</td>
<td><span class="status-badge status-${a.status}">${a.status}</span></td>
<td>${a.periods || 1}</td>
<td>${a.reason || '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
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 = `
<div class="school-empty-state">
<div class="school-empty-state-icon">&#127942;</div>
<h3>Bereit zur Generierung</h3>
<p>Klicken Sie auf "Zeugnisse generieren" oder nutzen Sie den Wizard.</p>
</div>
`;
return;
}
container.innerHTML = `
<table class="school-table">
<thead>
<tr>
<th>Schueler</th>
<th>Typ</th>
<th>Status</th>
<th>Erstellt</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
${certs.map(cert => `
<tr>
<td>${cert.student_name}</td>
<td>${cert.certificate_type}</td>
<td><span class="status-badge status-${cert.status}">${cert.status}</span></td>
<td>${cert.created_at ? new Date(cert.created_at).toLocaleDateString('de-DE') : '-'}</td>
<td>
<button class="btn btn-sm btn-secondary" onclick="schoolViewCertificate('${cert.id}')">Ansehen</button>
<button class="btn btn-sm btn-secondary" onclick="schoolDownloadCertificate('${cert.id}')">PDF</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
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 = '<option value="">Klasse waehlen...</option>';
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 = `
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;">
<div><strong>Schueler:</strong> ${stats.student_count || 0}</div>
<div><strong>Durchschnitt:</strong> ${stats.class_average ? stats.class_average.toFixed(2) : '-'}</div>
<div><strong>Gefaehrdet:</strong> ${stats.students_at_risk || 0}</div>
</div>
`;
} catch (error) {
document.getElementById('wizard-class-preview').style.display = 'none';
}
}
async function wizardLoadGrades() {
const container = document.getElementById('wizard-grades-table');
container.innerHTML = '<div class="school-loading"><div class="school-spinner"></div></div>';
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 = `
<table class="school-table">
<thead>
<tr>
<th>Name</th>
<th>Schnitt</th>
<th>Muendl.</th>
<th>Endnote</th>
<th>Status</th>
</tr>
</thead>
<tbody>
${wizardState.students.map(student => {
const grade = grades?.find?.(g => g.student_id === student.id);
const hasAllGrades = grade?.final_grade != null;
return `
<tr style="${!hasAllGrades ? 'background: #fff3cd;' : ''}">
<td>${student.last_name}, ${student.first_name}</td>
<td>${grade?.written_grade_avg?.toFixed(2) || '-'}</td>
<td>${grade?.oral_grade?.toFixed(1) || '-'}</td>
<td>
${grade?.final_grade
? `<span class="grade-badge grade-${Math.round(grade.final_grade)}">${grade.final_grade.toFixed(1)}</span>`
: '<span style="color: #dc3545;">Fehlt</span>'
}
</td>
<td>${hasAllGrades ? '&#10003;' : '&#10007;'}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
} catch (error) {
container.innerHTML = '<p style="color: var(--bp-text-muted);">Fehler beim Laden der Noten.</p>';
}
}
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 =>
`<option value="${opt.value}">${opt.text}</option>`
).join('');
}
function wizardLoadRemarks() {
const container = document.getElementById('wizard-remarks-list');
container.innerHTML = wizardState.students.map(student => `
<div style="display: flex; align-items: center; padding: 8px; border-bottom: 1px solid var(--bp-border);">
<span style="flex: 1;">${student.last_name}, ${student.first_name}</span>
<input type="text"
id="remark-${student.id}"
value="${wizardState.remarks[student.id] || ''}"
onchange="wizardState.remarks['${student.id}'] = this.value"
placeholder="Individuelle Bemerkung..."
style="flex: 2; padding: 6px; border: 1px solid var(--bp-border); border-radius: 4px;">
</div>
`).join('');
}
function wizardLoadSummary() {
const cls = schoolState.classes.find(c => c.id === wizardState.classId);
document.getElementById('wizard-summary').innerHTML = `
<div style="display: grid; grid-template-columns: auto 1fr; gap: 8px 16px;">
<strong>Klasse:</strong> <span>${cls?.name || wizardState.classId}</span>
<strong>Halbjahr:</strong> <span>${wizardState.semester}. Halbjahr</span>
<strong>Zeugnisart:</strong> <span>${wizardState.certType}</span>
<strong>Vorlage:</strong> <span>${wizardState.template}</span>
<strong>Schueler:</strong> <span>${wizardState.students.length}</span>
<strong>Mit Bemerkungen:</strong> <span>${Object.keys(wizardState.remarks).filter(k => wizardState.remarks[k]).length}</span>
</div>
`;
}
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');
"""