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:
63
backend/frontend/school/__init__.py
Normal file
63
backend/frontend/school/__init__.py
Normal 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",
|
||||
]
|
||||
18
backend/frontend/school/pages/__init__.py
Normal file
18
backend/frontend/school/pages/__init__.py
Normal 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",
|
||||
]
|
||||
249
backend/frontend/school/pages/attendance.py
Normal file
249
backend/frontend/school/pages/attendance.py
Normal 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")
|
||||
183
backend/frontend/school/pages/dashboard.py
Normal file
183
backend/frontend/school/pages/dashboard.py
Normal 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")
|
||||
341
backend/frontend/school/pages/grades.py
Normal file
341
backend/frontend/school/pages/grades.py
Normal 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()">×</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")
|
||||
180
backend/frontend/school/pages/parent_onboarding.py
Normal file
180
backend/frontend/school/pages/parent_onboarding.py
Normal 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>'''
|
||||
304
backend/frontend/school/pages/timetable.py
Normal file
304
backend/frontend/school/pages/timetable.py
Normal 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()">×</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")
|
||||
1237
backend/frontend/school/styles.py
Normal file
1237
backend/frontend/school/styles.py
Normal file
File diff suppressed because it is too large
Load Diff
186
backend/frontend/school/templates.py
Normal file
186
backend/frontend/school/templates.py
Normal 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>
|
||||
"""
|
||||
Reference in New Issue
Block a user