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>
2467 lines
77 KiB
Python
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">👥</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">📄</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">📊</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">📖</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">📝</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);">➜</span>
|
|
<span class="workflow-step" id="workflow-klassenlehrer">Klassenlehrer</span>
|
|
<span style="color: var(--bp-text-muted);">➜</span>
|
|
<span class="workflow-step" id="workflow-zb">Zeugnisbeauftragter</span>
|
|
<span style="color: var(--bp-text-muted);">➜</span>
|
|
<span class="workflow-step" id="workflow-schulleitung">Schulleitung</span>
|
|
<span style="color: var(--bp-text-muted);">➜</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">🏆</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')">×</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')">×</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')">×</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')">×</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) 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')">×</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">👥</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()">×</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">👥</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">📄</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">📊</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">📖</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">🏆</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 ? '✓' : '✗'}</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');
|
|
"""
|