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>
This commit is contained in:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit bfdaf63ba9
2009 changed files with 749983 additions and 1731 deletions

View File

@@ -0,0 +1,63 @@
"""
School Module
Modular structure for the School frontend (Schulverwaltung).
Matrix-based communication for schools.
Modular Refactoring (2026-02-03):
- Split into sub-modules for maintainability
- Original file: school.py (3,732 lines)
- Now split into: styles.py, templates.py, pages/
"""
from fastapi import APIRouter
from fastapi.responses import HTMLResponse
from .pages import (
school_dashboard,
attendance_page,
grades_page,
timetable_page,
parent_onboarding,
)
router = APIRouter()
# ============================================
# API Routes
# ============================================
@router.get("/school", response_class=HTMLResponse)
def get_school_dashboard():
"""Main school dashboard"""
return school_dashboard()
@router.get("/school/attendance", response_class=HTMLResponse)
def get_attendance_page():
"""Attendance tracking page"""
return attendance_page()
@router.get("/school/grades", response_class=HTMLResponse)
def get_grades_page():
"""Grades overview page"""
return grades_page()
@router.get("/school/timetable", response_class=HTMLResponse)
def get_timetable_page():
"""Timetable page"""
return timetable_page()
@router.get("/onboard-parent", response_class=HTMLResponse)
def get_parent_onboarding():
"""Parent onboarding page (QR code landing)"""
return parent_onboarding()
__all__ = [
"router",
]

View File

@@ -0,0 +1,18 @@
"""
School Module - Pages
Individual page renderers for the school frontend
"""
from .dashboard import school_dashboard
from .attendance import attendance_page
from .grades import grades_page
from .timetable import timetable_page
from .parent_onboarding import parent_onboarding
__all__ = [
"school_dashboard",
"attendance_page",
"grades_page",
"timetable_page",
"parent_onboarding",
]

View File

@@ -0,0 +1,249 @@
"""
School Module - Attendance Page
Attendance tracking for students
"""
from ..styles import SCHOOL_BASE_STYLES, ATTENDANCE_STYLES
from ..templates import ICONS, render_base_page, COMMON_SCRIPTS
def attendance_page() -> str:
"""Attendance tracking page"""
styles = SCHOOL_BASE_STYLES + ATTENDANCE_STYLES
content = f'''
<main class="main-content">
<div class="page-header">
<div>
<h1 class="page-title">Anwesenheit</h1>
<p class="page-subtitle">Erfassen Sie die Anwesenheit Ihrer Schüler</p>
</div>
<button class="btn btn-success" onclick="saveAttendance()">
{ICONS['check']}
Speichern
</button>
</div>
<!-- Controls -->
<div class="controls">
<div class="select-wrapper">
<select id="class-select" onchange="loadClass()">
<option value="5a">Klasse 5a</option>
<option value="5b">Klasse 5b</option>
<option value="6a">Klasse 6a</option>
</select>
</div>
<div class="select-wrapper">
<select id="lesson-select">
<option value="1">1. Stunde (08:00 - 08:45)</option>
<option value="2">2. Stunde (08:50 - 09:35)</option>
<option value="3">3. Stunde (09:50 - 10:35)</option>
<option value="4">4. Stunde (10:40 - 11:25)</option>
<option value="5">5. Stunde (11:40 - 12:25)</option>
<option value="6">6. Stunde (12:30 - 13:15)</option>
</select>
</div>
<input type="date" id="date-select" onchange="loadAttendance()">
</div>
<!-- Stats Bar -->
<div class="stats-bar">
<div class="stat-item">
<span class="stat-dot present"></span>
<span class="stat-value" id="count-present">24</span>
<span class="stat-label">Anwesend</span>
</div>
<div class="stat-item">
<span class="stat-dot absent"></span>
<span class="stat-value" id="count-absent">1</span>
<span class="stat-label">Abwesend</span>
</div>
<div class="stat-item">
<span class="stat-dot late"></span>
<span class="stat-value" id="count-late">1</span>
<span class="stat-label">Verspätet</span>
</div>
<div class="stat-item">
<span class="stat-dot excused"></span>
<span class="stat-value" id="count-excused">0</span>
<span class="stat-label">Entschuldigt</span>
</div>
</div>
<!-- Attendance Table -->
<div class="card" style="padding: 0;">
<div class="card-header" style="padding: 1rem 1.5rem; border-bottom: 1px solid var(--bp-border);">
<span class="card-title">Schülerliste</span>
<button class="btn btn-secondary" onclick="markAllPresent()">Alle anwesend</button>
</div>
<table>
<thead>
<tr>
<th>Schüler</th>
<th>Status</th>
<th>Anmerkung</th>
</tr>
</thead>
<tbody id="student-list">
<!-- Dynamisch geladen -->
</tbody>
</table>
</div>
</main>
<div id="toast" class="toast"></div>'''
scripts = COMMON_SCRIPTS + '''
<script>
// Sample student data
const students = [
{ id: 1, name: 'Anna Schmidt', number: 1 },
{ id: 2, name: 'Ben Müller', number: 2 },
{ id: 3, name: 'Clara Weber', number: 3 },
{ id: 4, name: 'David Fischer', number: 4 },
{ id: 5, name: 'Emma Becker', number: 5 },
{ id: 6, name: 'Felix Braun', number: 6 },
{ id: 7, name: 'Greta Hoffmann', number: 7 },
{ id: 8, name: 'Hans Schneider', number: 8 },
{ id: 9, name: 'Ida Wagner', number: 9 },
{ id: 10, name: 'Jonas Koch', number: 10 },
{ id: 11, name: 'Klara Bauer', number: 11 },
{ id: 12, name: 'Leon Richter', number: 12 },
{ id: 13, name: 'Mia Klein', number: 13 },
{ id: 14, name: 'Noah Wolf', number: 14 },
{ id: 15, name: 'Olivia Meier', number: 15 },
{ id: 16, name: 'Paul Neumann', number: 16 },
{ id: 17, name: 'Quirin Schwarz', number: 17 },
{ id: 18, name: 'Rosa Zimmermann', number: 18 },
{ id: 19, name: 'Samuel Krüger', number: 19 },
{ id: 20, name: 'Tina Lange', number: 20 },
{ id: 21, name: 'Uwe Peters', number: 21 },
{ id: 22, name: 'Vera Meyer', number: 22 },
{ id: 23, name: 'Wilhelm Schulz', number: 23 },
{ id: 24, name: 'Xenia Huber', number: 24 },
{ id: 25, name: 'Yannik Fuchs', number: 25 },
{ id: 26, name: 'Zoe Berger', number: 26 }
];
// Attendance state
let attendance = {};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
const today = new Date().toISOString().split('T')[0];
document.getElementById('date-select').value = today;
students.forEach(s => {
attendance[s.id] = { status: 'present', notes: '' };
});
attendance[1] = { status: 'absent', notes: '' };
attendance[3] = { status: 'late', notes: '10 Minuten' };
renderStudentList();
updateStats();
});
function renderStudentList() {
const tbody = document.getElementById('student-list');
tbody.innerHTML = students.map(student => {
const att = attendance[student.id] || { status: 'present', notes: '' };
return `
<tr>
<td>
<div class="student-info">
<div class="student-avatar">${getInitials(student.name)}</div>
<div>
<div class="student-name">${student.name}</div>
<div class="student-number">Nr. ${student.number}</div>
</div>
</div>
</td>
<td>
<div class="status-toggle">
<button class="status-btn present ${att.status === 'present' ? 'active' : ''}"
onclick="setStatus(${student.id}, 'present')">Anwesend</button>
<button class="status-btn absent ${att.status === 'absent' ? 'active' : ''}"
onclick="setStatus(${student.id}, 'absent')">Abwesend</button>
<button class="status-btn late ${att.status === 'late' ? 'active' : ''}"
onclick="setStatus(${student.id}, 'late')">Verspätet</button>
<button class="status-btn excused ${att.status === 'excused' ? 'active' : ''}"
onclick="setStatus(${student.id}, 'excused')">Entschuldigt</button>
</div>
</td>
<td>
<input type="text" class="notes-input" placeholder="Anmerkung..."
value="${att.notes}"
onchange="setNotes(${student.id}, this.value)">
</td>
</tr>
`;
}).join('');
}
function setStatus(studentId, status) {
attendance[studentId] = {
...attendance[studentId],
status: status
};
renderStudentList();
updateStats();
}
function setNotes(studentId, notes) {
attendance[studentId] = {
...attendance[studentId],
notes: notes
};
}
function markAllPresent() {
students.forEach(s => {
attendance[s.id] = { status: 'present', notes: '' };
});
renderStudentList();
updateStats();
}
function updateStats() {
const counts = { present: 0, absent: 0, late: 0, excused: 0 };
Object.values(attendance).forEach(a => {
counts[a.status]++;
});
document.getElementById('count-present').textContent = counts.present;
document.getElementById('count-absent').textContent = counts.absent;
document.getElementById('count-late').textContent = counts.late;
document.getElementById('count-excused').textContent = counts.excused;
}
function loadClass() {
showToast('Klasse wird geladen...');
}
function loadAttendance() {
showToast('Anwesenheit wird geladen...');
}
async function saveAttendance() {
const classId = document.getElementById('class-select').value;
const lessonId = document.getElementById('lesson-select').value;
const date = document.getElementById('date-select').value;
const records = Object.entries(attendance).map(([studentId, att]) => ({
student_id: studentId,
status: att.status,
notes: att.notes,
lesson_number: parseInt(lessonId),
date: date
}));
try {
showToast('Anwesenheit gespeichert!', 'success');
} catch (error) {
showToast('Fehler beim Speichern', 'error');
}
}
</script>'''
return render_base_page("Anwesenheit", styles, content, scripts, "attendance")

View File

@@ -0,0 +1,183 @@
"""
School Module - Dashboard Page
Main school dashboard with stats and quick actions
"""
from ..styles import SCHOOL_BASE_STYLES, DASHBOARD_STYLES
from ..templates import ICONS, render_base_page, COMMON_SCRIPTS
def school_dashboard() -> str:
"""Main school dashboard"""
styles = SCHOOL_BASE_STYLES + DASHBOARD_STYLES
content = f'''
<main class="main-content">
<div class="page-header">
<div>
<h1 class="page-title">Dashboard</h1>
<p class="page-subtitle">Willkommen zurück! Hier ist die Übersicht für heute.</p>
</div>
</div>
<!-- Stats Grid -->
<div class="dashboard-grid">
<div class="card">
<div class="card-header">
<span class="card-title">Anwesend heute</span>
<div class="card-icon accent">
{ICONS['check_circle']}
</div>
</div>
<div class="stat-value">24/26</div>
<div class="stat-label">92% Anwesenheitsrate</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">Offene Entschuldigungen</span>
<div class="card-icon warning">
{ICONS['warning']}
</div>
</div>
<div class="stat-value">3</div>
<div class="stat-label">Warten auf Bestätigung</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">Ungelesene Nachrichten</span>
<div class="card-icon info">
{ICONS['mail']}
</div>
</div>
<div class="stat-value">5</div>
<div class="stat-label">Neue Elternnachrichten</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">Nächster Elternsprechtag</span>
<div class="card-icon primary">
{ICONS['calendar']}
</div>
</div>
<div class="stat-value">15.01.</div>
<div class="stat-label">8 Termine gebucht</div>
</div>
</div>
<!-- Quick Actions -->
<h3 style="margin-bottom: 1rem; font-weight: 600;">Schnellzugriff</h3>
<div class="quick-actions">
<a href="#" class="quick-action" onclick="recordAttendance()">
<div class="quick-action-icon">
{ICONS['attendance']}
</div>
<span class="quick-action-text">Anwesenheit erfassen</span>
</a>
<a href="#" class="quick-action" onclick="addGrade()">
<div class="quick-action-icon">
{ICONS['edit']}
</div>
<span class="quick-action-text">Note eintragen</span>
</a>
<a href="#" class="quick-action" onclick="sendMessage()">
<div class="quick-action-icon">
{ICONS['messages']}
</div>
<span class="quick-action-text">Nachricht senden</span>
</a>
<a href="#" class="quick-action" onclick="generateQRCode()">
<div class="quick-action-icon">
{ICONS['qr']}
</div>
<span class="quick-action-text">Eltern-QR erstellen</span>
</a>
</div>
<!-- Recent Activity Table -->
<div class="table-container">
<div class="table-header">
<span class="table-title">Heutige Abwesenheiten</span>
<button class="btn btn-secondary" onclick="viewAllAbsences()">Alle anzeigen</button>
</div>
<table>
<thead>
<tr>
<th>Schüler</th>
<th>Klasse</th>
<th>Stunden</th>
<th>Status</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<tr>
<td>Anna Schmidt</td>
<td>5a</td>
<td>1.-4. Stunde</td>
<td><span class="badge absent">⚠ Unentschuldigt</span></td>
<td><button class="btn btn-primary" onclick="confirmAbsence('1')">Bestätigen</button></td>
</tr>
<tr>
<td>Ben Müller</td>
<td>5a</td>
<td>Ganztägig</td>
<td><span class="badge pending">⏳ Gemeldet</span></td>
<td><button class="btn btn-secondary" onclick="confirmAbsence('2')">Prüfen</button></td>
</tr>
<tr>
<td>Clara Weber</td>
<td>5a</td>
<td>3. Stunde</td>
<td><span class="badge late">⏰ Verspätet</span></td>
<td><button class="btn btn-secondary" onclick="viewDetails('3')">Details</button></td>
</tr>
</tbody>
</table>
</div>
</main>'''
scripts = COMMON_SCRIPTS + '''
<script>
function recordAttendance() {
window.location.href = '/school/attendance';
}
function addGrade() {
window.location.href = '/school/grades';
}
function sendMessage() {
alert('Nachrichtenkomponist wird geöffnet...');
}
function generateQRCode() {
alert('QR-Code Generator wird geöffnet...');
}
function confirmAbsence(id) {
alert('Abwesenheit ' + id + ' wird bestätigt...');
}
function viewDetails(id) {
alert('Details für ' + id + ' werden angezeigt...');
}
function viewAllAbsences() {
window.location.href = '/school/attendance';
}
document.addEventListener('DOMContentLoaded', async () => {
if (!getAuthToken()) {
// Redirect to login if not authenticated
// window.location.href = '/app/login';
}
});
</script>'''
return render_base_page("Schulverwaltung", styles, content, scripts, "dashboard")

View File

@@ -0,0 +1,341 @@
"""
School Module - Grades Page
Grades overview and entry
"""
from ..styles import SCHOOL_BASE_STYLES, GRADES_STYLES
from ..templates import ICONS, render_base_page, COMMON_SCRIPTS
def grades_page() -> str:
"""Grades overview page"""
styles = SCHOOL_BASE_STYLES + GRADES_STYLES
content = f'''
<main class="main-content">
<div class="page-header">
<div>
<h1 class="page-title">Notenspiegel</h1>
<p class="page-subtitle">Notenübersicht und -eintragung</p>
</div>
<button class="btn btn-primary" onclick="openGradeModal()">
{ICONS['plus']}
Note eintragen
</button>
</div>
<!-- Controls -->
<div class="controls">
<div class="select-wrapper">
<select id="class-select" onchange="loadGrades()">
<option value="5a">Klasse 5a</option>
<option value="5b">Klasse 5b</option>
<option value="6a">Klasse 6a</option>
</select>
</div>
<div class="select-wrapper">
<select id="subject-select" onchange="loadGrades()">
<option value="math">Mathematik</option>
<option value="german">Deutsch</option>
<option value="english">Englisch</option>
<option value="physics">Physik</option>
<option value="biology">Biologie</option>
<option value="history">Geschichte</option>
</select>
</div>
<div class="select-wrapper">
<select id="type-select" onchange="loadGrades()">
<option value="all">Alle Leistungen</option>
<option value="exam">Klassenarbeiten</option>
<option value="oral">Mündlich</option>
<option value="homework">Hausaufgaben</option>
<option value="test">Tests</option>
</select>
</div>
</div>
<!-- Stats -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-card-value" id="stat-average">2.4</div>
<div class="stat-card-label">Klassendurchschnitt</div>
</div>
<div class="stat-card">
<div class="stat-card-value" id="stat-best">1</div>
<div class="stat-card-label">Beste Note</div>
</div>
<div class="stat-card">
<div class="stat-card-value" id="stat-count">26</div>
<div class="stat-card-label">Eingetragene Noten</div>
</div>
</div>
<!-- Distribution Card -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-header">
<span class="card-title">Notenverteilung</span>
</div>
<div class="card-body">
<div class="distribution-bar" id="distribution-bar">
<div class="distribution-segment dist-1" style="width: 15%">4</div>
<div class="distribution-segment dist-2" style="width: 27%">7</div>
<div class="distribution-segment dist-3" style="width: 31%">8</div>
<div class="distribution-segment dist-4" style="width: 19%">5</div>
<div class="distribution-segment dist-5" style="width: 8%">2</div>
<div class="distribution-segment dist-6" style="width: 0%"></div>
</div>
</div>
</div>
<!-- Grades Table -->
<div class="card" style="padding: 0;">
<div class="card-header" style="padding: 1rem 1.5rem; border-bottom: 1px solid var(--bp-border);">
<span class="card-title">Noten - Mathematik (Klassenarbeit 1)</span>
<button class="btn btn-secondary" onclick="exportGrades()">
{ICONS['download']}
Exportieren
</button>
</div>
<table>
<thead>
<tr>
<th>Schüler</th>
<th>Note</th>
<th>Punkte</th>
<th>Datum</th>
<th>Kommentar</th>
<th>Aktion</th>
</tr>
</thead>
<tbody id="grades-list">
<!-- Dynamisch geladen -->
</tbody>
</table>
</div>
</main>
<!-- Grade Modal -->
<div class="modal-overlay" id="grade-modal">
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">Note eintragen</h2>
<button class="modal-close" onclick="closeGradeModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Schüler</label>
<select class="form-select" id="modal-student"></select>
</div>
<div class="form-group">
<label class="form-label">Fach</label>
<select class="form-select" id="modal-subject">
<option value="math">Mathematik</option>
<option value="german">Deutsch</option>
<option value="english">Englisch</option>
<option value="physics">Physik</option>
<option value="biology">Biologie</option>
<option value="history">Geschichte</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Leistungsart</label>
<select class="form-select" id="modal-type">
<option value="exam">Klassenarbeit</option>
<option value="oral">Mündliche Note</option>
<option value="homework">Hausaufgabe</option>
<option value="test">Test/Quiz</option>
<option value="project">Projektarbeit</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Note</label>
<div class="grade-buttons" id="grade-buttons">
<button type="button" class="grade-btn" onclick="selectGrade(1)">1</button>
<button type="button" class="grade-btn" onclick="selectGrade(2)">2</button>
<button type="button" class="grade-btn" onclick="selectGrade(3)">3</button>
<button type="button" class="grade-btn" onclick="selectGrade(4)">4</button>
<button type="button" class="grade-btn" onclick="selectGrade(5)">5</button>
<button type="button" class="grade-btn" onclick="selectGrade(6)">6</button>
</div>
</div>
<div class="form-group">
<label class="form-label">Punkte (optional)</label>
<input type="number" class="form-input" id="modal-points" placeholder="z.B. 85 von 100">
</div>
<div class="form-group">
<label class="form-label">Kommentar (optional)</label>
<textarea class="form-textarea" id="modal-comment" placeholder="Anmerkungen zur Leistung..."></textarea>
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="modal-notify"> Eltern benachrichtigen
</label>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeGradeModal()">Abbrechen</button>
<button class="btn btn-primary" onclick="saveGrade()">Speichern</button>
</div>
</div>
</div>
<div id="toast" class="toast"></div>'''
scripts = COMMON_SCRIPTS + '''
<script>
const students = [
{ id: 1, name: 'Anna Schmidt' },
{ id: 2, name: 'Ben Müller' },
{ id: 3, name: 'Clara Weber' },
{ id: 4, name: 'David Fischer' },
{ id: 5, name: 'Emma Becker' },
{ id: 6, name: 'Felix Braun' },
{ id: 7, name: 'Greta Hoffmann' },
{ id: 8, name: 'Hans Schneider' },
{ id: 9, name: 'Ida Wagner' },
{ id: 10, name: 'Jonas Koch' },
{ id: 11, name: 'Klara Bauer' },
{ id: 12, name: 'Leon Richter' },
{ id: 13, name: 'Mia Klein' },
{ id: 14, name: 'Noah Wolf' },
{ id: 15, name: 'Olivia Meier' },
{ id: 16, name: 'Paul Neumann' },
{ id: 17, name: 'Quirin Schwarz' },
{ id: 18, name: 'Rosa Zimmermann' },
{ id: 19, name: 'Samuel Krüger' },
{ id: 20, name: 'Tina Lange' },
{ id: 21, name: 'Uwe Peters' },
{ id: 22, name: 'Vera Meyer' },
{ id: 23, name: 'Wilhelm Schulz' },
{ id: 24, name: 'Xenia Huber' },
{ id: 25, name: 'Yannik Fuchs' },
{ id: 26, name: 'Zoe Berger' }
];
const sampleGrades = [
{ studentId: 1, grade: 2, points: 85, date: '2024-12-10', comment: 'Gute Arbeit' },
{ studentId: 2, grade: 3, points: 72, date: '2024-12-10', comment: '' },
{ studentId: 3, grade: 1, points: 95, date: '2024-12-10', comment: 'Sehr gut!' },
{ studentId: 4, grade: 4, points: 58, date: '2024-12-10', comment: 'Mehr üben' },
{ studentId: 5, grade: 2, points: 82, date: '2024-12-10', comment: '' },
{ studentId: 6, grade: 3, points: 70, date: '2024-12-10', comment: '' },
{ studentId: 7, grade: 2, points: 80, date: '2024-12-10', comment: '' },
{ studentId: 8, grade: 3, points: 68, date: '2024-12-10', comment: '' },
{ studentId: 9, grade: 1, points: 92, date: '2024-12-10', comment: 'Ausgezeichnet' },
{ studentId: 10, grade: 4, points: 55, date: '2024-12-10', comment: '' },
{ studentId: 11, grade: 2, points: 78, date: '2024-12-10', comment: '' },
{ studentId: 12, grade: 3, points: 65, date: '2024-12-10', comment: '' },
{ studentId: 13, grade: 2, points: 84, date: '2024-12-10', comment: '' },
{ studentId: 14, grade: 5, points: 42, date: '2024-12-10', comment: 'Nachholtermin?' },
{ studentId: 15, grade: 3, points: 71, date: '2024-12-10', comment: '' },
{ studentId: 16, grade: 2, points: 79, date: '2024-12-10', comment: '' },
{ studentId: 17, grade: 3, points: 67, date: '2024-12-10', comment: '' },
{ studentId: 18, grade: 1, points: 98, date: '2024-12-10', comment: 'Hervorragend!' },
{ studentId: 19, grade: 4, points: 52, date: '2024-12-10', comment: '' },
{ studentId: 20, grade: 3, points: 69, date: '2024-12-10', comment: '' },
{ studentId: 21, grade: 2, points: 81, date: '2024-12-10', comment: '' },
{ studentId: 22, grade: 3, points: 66, date: '2024-12-10', comment: '' },
{ studentId: 23, grade: 4, points: 54, date: '2024-12-10', comment: '' },
{ studentId: 24, grade: 1, points: 94, date: '2024-12-10', comment: 'Toll!' },
{ studentId: 25, grade: 5, points: 45, date: '2024-12-10', comment: '' },
{ studentId: 26, grade: 2, points: 83, date: '2024-12-10', comment: '' }
];
let selectedGrade = null;
document.addEventListener('DOMContentLoaded', () => {
populateStudentSelect();
renderGradesTable();
});
function populateStudentSelect() {
const select = document.getElementById('modal-student');
select.innerHTML = students.map(s =>
`<option value="${s.id}">${s.name}</option>`
).join('');
}
function renderGradesTable() {
const tbody = document.getElementById('grades-list');
tbody.innerHTML = sampleGrades.map(grade => {
const student = students.find(s => s.id === grade.studentId);
return `
<tr>
<td>
<div class="student-info">
<div class="student-avatar">${getInitials(student.name)}</div>
<span class="student-name">${student.name}</span>
</div>
</td>
<td><span class="grade-badge grade-${grade.grade}">${grade.grade}</span></td>
<td>${grade.points}/100</td>
<td>${formatDate(grade.date)}</td>
<td>${grade.comment || '-'}</td>
<td>
<button class="btn btn-secondary" onclick="editGrade(${grade.studentId})">Bearbeiten</button>
</td>
</tr>
`;
}).join('');
}
function formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
function openGradeModal() {
selectedGrade = null;
document.querySelectorAll('.grade-btn').forEach(btn => btn.classList.remove('selected'));
document.getElementById('modal-points').value = '';
document.getElementById('modal-comment').value = '';
document.getElementById('modal-notify').checked = true;
document.getElementById('grade-modal').classList.add('show');
}
function closeGradeModal() {
document.getElementById('grade-modal').classList.remove('show');
}
function selectGrade(grade) {
selectedGrade = grade;
document.querySelectorAll('.grade-btn').forEach(btn => {
btn.classList.remove('selected');
if (parseInt(btn.textContent) === grade) {
btn.classList.add('selected');
}
});
}
function editGrade(studentId) {
const grade = sampleGrades.find(g => g.studentId === studentId);
if (grade) {
document.getElementById('modal-student').value = studentId;
selectGrade(grade.grade);
document.getElementById('modal-points').value = grade.points;
document.getElementById('modal-comment').value = grade.comment;
document.getElementById('grade-modal').classList.add('show');
}
}
async function saveGrade() {
if (!selectedGrade) {
showToast('Bitte wählen Sie eine Note aus', 'error');
return;
}
closeGradeModal();
showToast('Note gespeichert!', 'success');
renderGradesTable();
}
function loadGrades() {
showToast('Noten werden geladen...');
}
function exportGrades() {
showToast('Export wird erstellt...');
}
</script>'''
return render_base_page("Notenspiegel", styles, content, scripts, "grades")

View File

@@ -0,0 +1,180 @@
"""
School Module - Parent Onboarding Page
QR code landing page for parent registration
"""
from ..styles import SCHOOL_BASE_STYLES, ONBOARDING_STYLES
from ..templates import COMMON_SCRIPTS
def parent_onboarding() -> str:
"""Parent onboarding page (QR code landing)"""
# Onboarding page uses its own simplified styles without sidebar
styles = f"""
:root {{
--bp-primary: #6C1B1B;
--bp-bg: #F8F8F8;
--bp-surface: #FFFFFF;
--bp-text: #4A4A4A;
--bp-text-muted: #6B6B6B;
--bp-accent: #5ABF60;
--bp-border: #E0E0E0;
}}
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
{ONBOARDING_STYLES}
"""
content = '''
<div class="onboarding-container">
<div class="logo">
<div class="logo-icon">BP</div>
<span class="logo-text">BreakPilot</span>
</div>
<div id="loading">
<div class="spinner"></div>
<p>QR-Code wird validiert...</p>
</div>
<div id="content" style="display: none;">
<h1>Eltern-Registrierung</h1>
<p class="subtitle">Sie wurden eingeladen, sich für die Schulkommunikation zu registrieren.</p>
<div id="error" class="error" style="display: none;"></div>
<div id="success" class="success" style="display: none;"></div>
<div class="info-card">
<div class="info-row">
<span class="info-label">Schule</span>
<span class="info-value" id="school-name">-</span>
</div>
<div class="info-row">
<span class="info-label">Klasse</span>
<span class="info-value" id="class-name">-</span>
</div>
<div class="info-row">
<span class="info-label">Kind</span>
<span class="info-value" id="student-name">-</span>
</div>
<div class="info-row">
<span class="info-label">Ihre Rolle</span>
<span class="info-value" id="role">Elternteil</span>
</div>
</div>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="consent-terms" required>
<label class="checkbox-label" for="consent-terms">
Ich habe die <a href="/terms" target="_blank">Nutzungsbedingungen</a> gelesen und akzeptiere diese.
</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="consent-privacy" required>
<label class="checkbox-label" for="consent-privacy">
Ich habe die <a href="/privacy" target="_blank">Datenschutzerklärung</a> gelesen und stimme der Verarbeitung meiner Daten zu.
</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="consent-custody" required>
<label class="checkbox-label" for="consent-custody">
Ich bestätige, dass ich sorgeberechtigt für das oben genannte Kind bin.
</label>
</div>
</div>
<button class="btn btn-primary" id="submit-btn" onclick="completeOnboarding()" disabled>
Registrierung abschließen
</button>
</div>
</div>'''
scripts = '''
<script>
const API_BASE = '/api/v1';
let tokenData = null;
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
async function validateToken() {
if (!token) {
showError('Kein gültiger QR-Code. Bitte scannen Sie den QR-Code erneut.');
return;
}
try {
const response = await fetch(API_BASE + '/onboarding/validate?token=' + token);
const data = await response.json();
if (!response.ok || !data.valid) {
showError('Der QR-Code ist ungültig oder abgelaufen. Bitte wenden Sie sich an den Klassenlehrer.');
return;
}
tokenData = data;
document.getElementById('school-name').textContent = data.school_name;
document.getElementById('class-name').textContent = data.class_name;
document.getElementById('student-name').textContent = data.student_name;
document.getElementById('role').textContent = data.role === 'parent_representative' ? 'Elternvertreter' : 'Elternteil';
document.getElementById('loading').style.display = 'none';
document.getElementById('content').style.display = 'block';
} catch (error) {
showError('Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.');
}
}
function showError(message) {
document.getElementById('loading').style.display = 'none';
document.getElementById('content').style.display = 'block';
document.getElementById('error').textContent = message;
document.getElementById('error').style.display = 'block';
}
document.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.addEventListener('change', () => {
const allChecked = document.querySelectorAll('input[type="checkbox"]:checked').length === 3;
document.getElementById('submit-btn').disabled = !allChecked;
});
});
async function completeOnboarding() {
const btn = document.getElementById('submit-btn');
btn.disabled = true;
btn.textContent = 'Wird verarbeitet...';
try {
const returnUrl = encodeURIComponent(window.location.href);
window.location.href = '/app/login?return_to=' + returnUrl + '&onboarding=true';
} catch (error) {
showError('Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.');
btn.disabled = false;
btn.textContent = 'Registrierung abschließen';
}
}
validateToken();
</script>'''
# This page doesn't use the standard base template with sidebar
return f'''<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>BreakPilot Eltern-Onboarding</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
{styles}
</style>
</head>
<body>
{content}
{scripts}
</body>
</html>'''

View File

@@ -0,0 +1,304 @@
"""
School Module - Timetable Page
Weekly timetable view
"""
from ..styles import SCHOOL_BASE_STYLES, TIMETABLE_STYLES
from ..templates import ICONS, render_base_page, COMMON_SCRIPTS
def timetable_page() -> str:
"""Timetable page"""
styles = SCHOOL_BASE_STYLES + TIMETABLE_STYLES
content = f'''
<main class="main-content">
<div class="page-header">
<div>
<h1 class="page-title">Stundenplan</h1>
<p class="page-subtitle">Wochenübersicht für Ihre Klasse</p>
</div>
<button class="btn btn-secondary" onclick="printTimetable()">
{ICONS['print']}
Drucken
</button>
</div>
<!-- Controls -->
<div class="controls">
<div class="select-wrapper">
<select id="class-select" onchange="loadTimetable()">
<option value="5a">Klasse 5a</option>
<option value="5b">Klasse 5b</option>
<option value="6a">Klasse 6a</option>
</select>
</div>
<div class="week-nav">
<button class="week-nav-btn" onclick="prevWeek()">
{ICONS['chevron_left']}
</button>
<span class="week-label" id="week-label">16. - 20. Dezember 2024</span>
<button class="week-nav-btn" onclick="nextWeek()">
{ICONS['chevron_right']}
</button>
</div>
<button class="btn btn-today" onclick="goToToday()">Heute</button>
</div>
<!-- Timetable -->
<div class="timetable-container">
<div class="timetable" id="timetable">
<!-- Header -->
<div class="timetable-header">
<div></div>
<div>Montag<br><small id="day-mon">16.12.</small></div>
<div>Dienstag<br><small id="day-tue">17.12.</small></div>
<div>Mittwoch<br><small id="day-wed">18.12.</small></div>
<div>Donnerstag<br><small id="day-thu">19.12.</small></div>
<div>Freitag<br><small id="day-fri">20.12.</small></div>
</div>
<!-- Rows will be populated by JavaScript -->
</div>
</div>
<!-- Legend -->
<div class="legend">
<div class="legend-item">
<div class="legend-color" style="background: #e0f2fe; border-left: 3px solid #0ea5e9;"></div>
<span>Mathematik</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #fef3c7; border-left: 3px solid #f59e0b;"></div>
<span>Deutsch</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #dbeafe; border-left: 3px solid #3b82f6;"></div>
<span>Englisch</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: repeating-linear-gradient(45deg, #fef3c7, #fef3c7 5px, #fde68a 5px, #fde68a 10px);"></div>
<span>Vertretung</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #fee2e2; border-left: 3px solid #ef4444;"></div>
<span>Entfall</span>
</div>
</div>
</main>
<!-- Lesson Detail Modal -->
<div class="modal-overlay" id="lesson-modal">
<div class="modal" style="max-width: 400px; padding: 1.5rem;">
<div class="modal-header" style="padding: 0; margin-bottom: 1rem; border: none;">
<h2 class="modal-title" id="modal-subject">Mathematik</h2>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
<div class="modal-info">
<div class="modal-info-row">
<span class="modal-info-label">Lehrer</span>
<span class="modal-info-value" id="modal-teacher">Hr. Schmidt</span>
</div>
<div class="modal-info-row">
<span class="modal-info-label">Raum</span>
<span class="modal-info-value" id="modal-room">R 204</span>
</div>
<div class="modal-info-row">
<span class="modal-info-label">Zeit</span>
<span class="modal-info-value" id="modal-time">08:00 - 08:45</span>
</div>
<div class="modal-info-row">
<span class="modal-info-label">Status</span>
<span class="modal-info-value" id="modal-status">Regulär</span>
</div>
</div>
</div>
</div>
<div id="toast" class="toast"></div>'''
scripts = COMMON_SCRIPTS + '''
<script>
const lessonTimes = [
{ num: 1, start: '08:00', end: '08:45' },
{ num: 2, start: '08:50', end: '09:35' },
{ num: 3, start: '09:50', end: '10:35' },
{ num: 4, start: '10:40', end: '11:25' },
{ num: 5, start: '11:40', end: '12:25' },
{ num: 6, start: '12:30', end: '13:15' }
];
const timetableData = {
mon: [
{ subject: 'Mathematik', teacher: 'Hr. Schmidt', room: 'R 204', type: 'math' },
{ subject: 'Mathematik', teacher: 'Hr. Schmidt', room: 'R 204', type: 'math' },
{ subject: 'Deutsch', teacher: 'Fr. Müller', room: 'R 102', type: 'german' },
{ subject: 'Deutsch', teacher: 'Fr. Müller', room: 'R 102', type: 'german' },
{ subject: 'Englisch', teacher: 'Hr. Wagner', room: 'R 305', type: 'english' },
{ subject: 'Sport', teacher: 'Hr. Becker', room: 'Turnhalle', type: 'sport' }
],
tue: [
{ subject: 'Biologie', teacher: 'Fr. Weber', room: 'R 401', type: 'biology' },
{ subject: 'Biologie', teacher: 'Fr. Weber', room: 'R 401', type: 'biology' },
{ subject: 'Englisch', teacher: 'Hr. Wagner', room: 'R 305', type: 'english' },
{ subject: 'Mathematik', teacher: 'Hr. Schmidt', room: 'R 204', type: 'math' },
{ subject: 'Physik', teacher: 'Hr. Hoffmann', room: 'R 402', type: 'physics', substitution: true, subTeacher: 'Fr. Klein' },
null
],
wed: [
{ subject: 'Geschichte', teacher: 'Fr. Braun', room: 'R 203', type: 'history' },
{ subject: 'Geschichte', teacher: 'Fr. Braun', room: 'R 203', type: 'history' },
{ subject: 'Musik', teacher: 'Hr. Fischer', room: 'Musikraum', type: 'music' },
{ subject: 'Deutsch', teacher: 'Fr. Müller', room: 'R 102', type: 'german' },
{ subject: 'Kunst', teacher: 'Fr. Lange', room: 'Kunstraum', type: 'art' },
{ subject: 'Kunst', teacher: 'Fr. Lange', room: 'Kunstraum', type: 'art' }
],
thu: [
{ subject: 'Mathematik', teacher: 'Hr. Schmidt', room: 'R 204', type: 'math' },
{ subject: 'Physik', teacher: 'Hr. Hoffmann', room: 'R 402', type: 'physics' },
{ subject: 'Physik', teacher: 'Hr. Hoffmann', room: 'R 402', type: 'physics' },
{ subject: 'Englisch', teacher: 'Hr. Wagner', room: 'R 305', type: 'english' },
{ subject: 'Deutsch', teacher: 'Fr. Müller', room: 'R 102', type: 'german', cancelled: true },
null
],
fri: [
{ subject: 'Englisch', teacher: 'Hr. Wagner', room: 'R 305', type: 'english' },
{ subject: 'Englisch', teacher: 'Hr. Wagner', room: 'R 305', type: 'english' },
{ subject: 'Mathematik', teacher: 'Hr. Schmidt', room: 'R 204', type: 'math' },
{ subject: 'Biologie', teacher: 'Fr. Weber', room: 'R 401', type: 'biology' },
null,
null
]
};
let currentWeekStart = getMonday(new Date());
document.addEventListener('DOMContentLoaded', () => {
renderTimetable();
updateWeekLabel();
});
function getMonday(d) {
const date = new Date(d);
const day = date.getDay();
const diff = date.getDate() - day + (day === 0 ? -6 : 1);
return new Date(date.setDate(diff));
}
function updateWeekLabel() {
const start = new Date(currentWeekStart);
const end = new Date(start);
end.setDate(end.getDate() + 4);
document.getElementById('week-label').textContent =
`${start.getDate()}. - ${end.getDate()}. ${start.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' })}`;
const days = ['mon', 'tue', 'wed', 'thu', 'fri'];
days.forEach((day, i) => {
const d = new Date(start);
d.setDate(d.getDate() + i);
document.getElementById(`day-${day}`).textContent =
d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
});
}
function prevWeek() {
currentWeekStart.setDate(currentWeekStart.getDate() - 7);
updateWeekLabel();
renderTimetable();
}
function nextWeek() {
currentWeekStart.setDate(currentWeekStart.getDate() + 7);
updateWeekLabel();
renderTimetable();
}
function goToToday() {
currentWeekStart = getMonday(new Date());
updateWeekLabel();
renderTimetable();
showToast('Zur aktuellen Woche gesprungen');
}
function renderTimetable() {
const container = document.getElementById('timetable');
const header = container.querySelector('.timetable-header').outerHTML;
container.innerHTML = header;
for (let i = 0; i < lessonTimes.length; i++) {
const time = lessonTimes[i];
let rowHTML = `
<div class="timetable-row">
<div class="time-cell">
<div class="lesson-num">${time.num}.</div>
<div>${time.start}</div>
<div>${time.end}</div>
</div>
`;
const days = ['mon', 'tue', 'wed', 'thu', 'fri'];
days.forEach(day => {
const lesson = timetableData[day][i];
rowHTML += `<div class="lesson-cell">`;
if (lesson) {
let cardClass = `lesson-card ${lesson.type}`;
if (lesson.substitution) cardClass += ' substitution';
if (lesson.cancelled) cardClass += ' cancelled';
rowHTML += `
<div class="${cardClass}" onclick="showLessonDetail('${lesson.subject}', '${lesson.teacher}', '${lesson.room}', '${time.start} - ${time.end}', ${lesson.substitution || false}, ${lesson.cancelled || false}, '${lesson.subTeacher || ''}')">
<div class="lesson-subject">${lesson.subject}</div>
<div class="lesson-teacher">${lesson.substitution ? lesson.subTeacher : lesson.teacher}</div>
<div class="lesson-room">${lesson.room}</div>
${lesson.substitution ? '<span class="lesson-badge badge-substitution">Vertretung</span>' : ''}
${lesson.cancelled ? '<span class="lesson-badge badge-cancelled">Entfall</span>' : ''}
</div>
`;
}
rowHTML += `</div>`;
});
rowHTML += `</div>`;
container.innerHTML += rowHTML;
}
}
function showLessonDetail(subject, teacher, room, time, isSubstitution, isCancelled, subTeacher) {
document.getElementById('modal-subject').textContent = subject;
document.getElementById('modal-teacher').textContent = isSubstitution ? `${subTeacher} (Vertretung für ${teacher})` : teacher;
document.getElementById('modal-room').textContent = room;
document.getElementById('modal-time').textContent = time;
let status = 'Regulär';
if (isCancelled) status = 'Entfällt';
else if (isSubstitution) status = 'Vertretung';
document.getElementById('modal-status').textContent = status;
document.getElementById('lesson-modal').classList.add('show');
}
function closeModal() {
document.getElementById('lesson-modal').classList.remove('show');
}
function loadTimetable() {
showToast('Stundenplan wird geladen...');
}
function printTimetable() {
window.print();
}
document.getElementById('lesson-modal').addEventListener('click', (e) => {
if (e.target.classList.contains('modal-overlay')) {
closeModal();
}
});
</script>'''
return render_base_page("Stundenplan", styles, content, scripts, "timetable")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,186 @@
"""
School Module - Template Helpers
Reusable components for school pages
"""
# SVG Icons
ICONS = {
'dashboard': '''<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>''',
'messages': '''<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
</svg>''',
'calendar': '''<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>''',
'attendance': '''<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>''',
'grades': '''<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>''',
'book': '''<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>''',
'users': '''<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>''',
'parents': '''<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>''',
'check_circle': '''<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>''',
'warning': '''<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>''',
'mail': '''<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>''',
'edit': '''<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>''',
'qr': '''<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h2M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
</svg>''',
'check': '''<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>''',
'plus': '''<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>''',
'download': '''<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>''',
'print': '''<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
</svg>''',
'chevron_left': '''<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>''',
'chevron_right': '''<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>''',
}
def render_sidebar(active_page: str = "dashboard") -> str:
"""Render the navigation sidebar"""
def nav_item(href: str, icon_key: str, label: str, page_id: str) -> str:
active_class = "active" if active_page == page_id else ""
return f'''
<a href="{href}" class="nav-item {active_class}">
{ICONS[icon_key]}
{label}
</a>'''
return f'''
<nav class="sidebar">
<div class="logo">
<div class="logo-icon">BP</div>
<span class="logo-text">BreakPilot</span>
</div>
<div class="nav-section">
<div class="nav-section-title">Übersicht</div>
{nav_item("/school", "dashboard", "Dashboard", "dashboard")}
{nav_item("#messages", "messages", "Nachrichten", "messages")}
</div>
<div class="nav-section">
<div class="nav-section-title">Unterricht</div>
{nav_item("/school/timetable", "calendar", "Stundenplan", "timetable")}
{nav_item("/school/attendance", "attendance", "Anwesenheit", "attendance")}
{nav_item("/school/grades", "grades", "Notenspiegel", "grades")}
{nav_item("#diary", "book", "Klassenbuch", "diary")}
</div>
<div class="nav-section">
<div class="nav-section-title">Verwaltung</div>
{nav_item("#students", "users", "Schüler", "students")}
{nav_item("#parents", "parents", "Eltern", "parents")}
{nav_item("#meetings", "calendar", "Elternsprechtag", "meetings")}
</div>
<div class="user-info">
<div class="user-card">
<div class="user-avatar">ML</div>
<div>
<div class="user-name">Max Lehrer</div>
<div class="user-role">Klassenlehrer 5a</div>
</div>
</div>
</div>
</nav>'''
def render_base_page(title: str, styles: str, content: str, scripts: str = "",
active_page: str = "dashboard", include_sidebar: bool = True) -> str:
"""Render a complete HTML page with the BreakPilot school design"""
sidebar_html = render_sidebar(active_page) if include_sidebar else ""
return f'''<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>BreakPilot {title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
{styles}
</style>
</head>
<body>
<div class="app-container">
{sidebar_html}
{content}
</div>
{scripts}
</body>
</html>'''
# Toast and utility scripts
COMMON_SCRIPTS = """
<script>
const API_BASE = '/api/v1';
function getAuthToken() {
return localStorage.getItem('bp_access_token');
}
async function apiCall(endpoint, method = 'GET', data = null) {
const options = {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + getAuthToken()
}
};
if (data) {
options.body = JSON.stringify(data);
}
const response = await fetch(API_BASE + endpoint, options);
return response.json();
}
function showToast(message, type = 'info') {
const toast = document.getElementById('toast');
if (toast) {
toast.textContent = message;
toast.className = 'toast ' + type + ' show';
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
}
function getInitials(name) {
return name.split(' ').map(n => n[0]).join('');
}
</script>
"""