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,50 @@
"""
Widget-Registry fuer das Lehrer-Dashboard.
Alle verfuegbaren Widgets werden hier registriert und exportiert.
"""
from .todos_widget import TodosWidget
from .schnellzugriff_widget import SchnellzugriffWidget
from .notizen_widget import NotizenWidget
from .stundenplan_widget import StundenplanWidget
from .klassen_widget import KlassenWidget
from .fehlzeiten_widget import FehlzeitenWidget
from .arbeiten_widget import ArbeitenWidget
from .nachrichten_widget import NachrichtenWidget
from .matrix_widget import MatrixWidget
from .alerts_widget import AlertsWidget
from .statistik_widget import StatistikWidget
from .kalender_widget import KalenderWidget
# Widget-Registry mit allen verfuegbaren Widgets
WIDGET_REGISTRY = {
'todos': TodosWidget,
'schnellzugriff': SchnellzugriffWidget,
'notizen': NotizenWidget,
'stundenplan': StundenplanWidget,
'klassen': KlassenWidget,
'fehlzeiten': FehlzeitenWidget,
'arbeiten': ArbeitenWidget,
'nachrichten': NachrichtenWidget,
'matrix': MatrixWidget,
'alerts': AlertsWidget,
'statistik': StatistikWidget,
'kalender': KalenderWidget,
}
__all__ = [
'WIDGET_REGISTRY',
'TodosWidget',
'SchnellzugriffWidget',
'NotizenWidget',
'StundenplanWidget',
'KlassenWidget',
'FehlzeitenWidget',
'ArbeitenWidget',
'NachrichtenWidget',
'MatrixWidget',
'AlertsWidget',
'StatistikWidget',
'KalenderWidget',
]

View File

@@ -0,0 +1,272 @@
"""
Alerts Widget fuer das Lehrer-Dashboard.
Zeigt Google Alerts und andere Benachrichtigungen.
"""
class AlertsWidget:
widget_id = 'alerts'
widget_name = 'Alerts'
widget_icon = '&#128276;' # Bell
widget_color = '#f59e0b' # Orange
default_width = 'half'
has_settings = True
@staticmethod
def get_css() -> str:
return """
/* ===== Alerts Widget Styles ===== */
.widget-alerts {
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 12px;
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.widget-alerts .widget-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-alerts .widget-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-alerts .widget-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
border-radius: 8px;
font-size: 14px;
}
.widget-alerts .alerts-list {
flex: 1;
overflow-y: auto;
}
.widget-alerts .alert-item {
display: flex;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.05));
cursor: pointer;
transition: background 0.2s;
}
.widget-alerts .alert-item:last-child {
border-bottom: none;
}
.widget-alerts .alert-item:hover {
background: var(--bp-surface-elevated, #334155);
margin: 0 -12px;
padding: 12px;
border-radius: 8px;
}
.widget-alerts .alert-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
border-radius: 8px;
font-size: 14px;
flex-shrink: 0;
}
.widget-alerts .alert-content {
flex: 1;
min-width: 0;
}
.widget-alerts .alert-title {
font-size: 13px;
font-weight: 500;
color: var(--bp-text, #e5e7eb);
margin-bottom: 4px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.widget-alerts .alert-meta {
font-size: 11px;
color: var(--bp-text-muted, #9ca3af);
display: flex;
gap: 8px;
}
.widget-alerts .alert-source {
color: #f59e0b;
}
.widget-alerts .alerts-empty {
text-align: center;
padding: 24px;
color: var(--bp-text-muted, #9ca3af);
font-size: 13px;
}
.widget-alerts .alerts-empty-icon {
font-size: 32px;
margin-bottom: 8px;
opacity: 0.5;
}
.widget-alerts .alerts-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-alerts .alerts-all-btn {
width: 100%;
padding: 10px;
background: transparent;
border: 1px dashed var(--bp-border, #475569);
border-radius: 8px;
color: var(--bp-text-muted, #9ca3af);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.widget-alerts .alerts-all-btn:hover {
border-color: #f59e0b;
color: #f59e0b;
}
"""
@staticmethod
def get_html() -> str:
return """
<div class="widget-alerts" data-widget-id="alerts">
<div class="widget-header">
<div class="widget-title">
<span class="widget-icon">&#128276;</span>
<span>Google Alerts</span>
</div>
<button class="widget-settings-btn" onclick="openWidgetSettings('alerts')" title="Einstellungen">&#9881;</button>
</div>
<div class="alerts-list" id="alerts-widget-list">
<!-- Wird dynamisch gefuellt -->
</div>
<div class="alerts-footer">
<button class="alerts-all-btn" onclick="openAlertsModule()">+ Alle Alerts anzeigen</button>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
return """
// ===== Alerts Widget JavaScript =====
function getDefaultAlerts() {
const now = Date.now();
return [
{
id: 1,
title: 'Neue Mathelehrer-Studie zeigt verbesserte Lernergebnisse durch digitale Tools',
source: 'Google Alert: Digitales Lernen',
url: '#',
time: new Date(now - 60 * 60 * 1000).toISOString()
},
{
id: 2,
title: 'Kultusministerium kuendigt neue Fortbildungsreihe an',
source: 'Google Alert: Bildungspolitik NI',
url: '#',
time: new Date(now - 5 * 60 * 60 * 1000).toISOString()
},
{
id: 3,
title: 'Best Practices: Differenzierter Deutschunterricht',
source: 'Google Alert: Deutschunterricht',
url: '#',
time: new Date(now - 24 * 60 * 60 * 1000).toISOString()
}
];
}
function formatAlertTime(timeStr) {
const time = new Date(timeStr);
const now = new Date();
const diffMs = now - time;
const diffHours = Math.floor(diffMs / (60 * 60 * 1000));
const diffDays = Math.floor(diffMs / (24 * 60 * 60 * 1000));
if (diffHours < 1) return 'gerade eben';
if (diffHours < 24) return `vor ${diffHours} Std.`;
if (diffDays === 1) return 'gestern';
return `vor ${diffDays} Tagen`;
}
function renderAlertsWidget() {
const list = document.getElementById('alerts-widget-list');
if (!list) return;
const alerts = getDefaultAlerts();
if (alerts.length === 0) {
list.innerHTML = `
<div class="alerts-empty">
<div class="alerts-empty-icon">&#128276;</div>
<div>Keine neuen Alerts</div>
</div>
`;
return;
}
list.innerHTML = alerts.map(alert => `
<div class="alert-item" onclick="openAlert('${alert.url}')">
<div class="alert-icon">&#128240;</div>
<div class="alert-content">
<div class="alert-title">${alert.title}</div>
<div class="alert-meta">
<span class="alert-source">${alert.source}</span>
<span>${formatAlertTime(alert.time)}</span>
</div>
</div>
</div>
`).join('');
}
function openAlert(url) {
if (url && url !== '#') {
window.open(url, '_blank');
}
}
function openAlertsModule() {
if (typeof loadModule === 'function') {
loadModule('alerts');
}
}
function initAlertsWidget() {
renderAlertsWidget();
}
"""

View File

@@ -0,0 +1,341 @@
"""
Arbeiten Widget fuer das Lehrer-Dashboard.
Zeigt anstehende Arbeiten und Fristen.
"""
class ArbeitenWidget:
widget_id = 'arbeiten'
widget_name = 'Anstehende Arbeiten'
widget_icon = '&#128221;' # Memo
widget_color = '#f59e0b' # Orange
default_width = 'half'
has_settings = True
@staticmethod
def get_css() -> str:
return """
/* ===== Arbeiten Widget Styles ===== */
.widget-arbeiten {
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 12px;
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.widget-arbeiten .widget-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-arbeiten .widget-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-arbeiten .widget-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
border-radius: 8px;
font-size: 14px;
}
.widget-arbeiten .arbeiten-list {
flex: 1;
overflow-y: auto;
}
.widget-arbeiten .arbeit-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
margin-bottom: 8px;
background: var(--bp-bg, #0f172a);
border: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.widget-arbeiten .arbeit-item:last-child {
margin-bottom: 0;
}
.widget-arbeiten .arbeit-item:hover {
background: var(--bp-surface-elevated, #334155);
border-color: #f59e0b;
}
.widget-arbeiten .arbeit-item.urgent {
border-left: 3px solid #ef4444;
}
.widget-arbeiten .arbeit-item.soon {
border-left: 3px solid #f59e0b;
}
.widget-arbeiten .arbeit-icon {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
border-radius: 8px;
font-size: 16px;
flex-shrink: 0;
}
.widget-arbeiten .arbeit-content {
flex: 1;
min-width: 0;
}
.widget-arbeiten .arbeit-title {
font-size: 14px;
font-weight: 500;
color: var(--bp-text, #e5e7eb);
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.widget-arbeiten .arbeit-meta {
font-size: 12px;
color: var(--bp-text-muted, #9ca3af);
display: flex;
align-items: center;
gap: 8px;
}
.widget-arbeiten .arbeit-frist {
display: flex;
align-items: center;
gap: 4px;
}
.widget-arbeiten .arbeit-frist.urgent {
color: #ef4444;
}
.widget-arbeiten .arbeit-frist.soon {
color: #f59e0b;
}
.widget-arbeiten .arbeit-type {
padding: 2px 6px;
background: rgba(107, 114, 128, 0.2);
border-radius: 4px;
font-size: 10px;
text-transform: uppercase;
}
.widget-arbeiten .arbeiten-empty {
text-align: center;
padding: 24px;
color: var(--bp-text-muted, #9ca3af);
font-size: 13px;
}
.widget-arbeiten .arbeiten-empty-icon {
font-size: 32px;
margin-bottom: 8px;
opacity: 0.5;
}
.widget-arbeiten .arbeiten-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-arbeiten .arbeiten-all-btn {
width: 100%;
padding: 10px;
background: transparent;
border: 1px dashed var(--bp-border, #475569);
border-radius: 8px;
color: var(--bp-text-muted, #9ca3af);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.widget-arbeiten .arbeiten-all-btn:hover {
border-color: #f59e0b;
color: #f59e0b;
}
"""
@staticmethod
def get_html() -> str:
return """
<div class="widget-arbeiten" data-widget-id="arbeiten">
<div class="widget-header">
<div class="widget-title">
<span class="widget-icon">&#128221;</span>
<span>Anstehende Arbeiten</span>
</div>
<button class="widget-settings-btn" onclick="openWidgetSettings('arbeiten')" title="Einstellungen">&#9881;</button>
</div>
<div class="arbeiten-list" id="arbeiten-list">
<!-- Wird dynamisch gefuellt -->
</div>
<div class="arbeiten-footer">
<button class="arbeiten-all-btn" onclick="openAllArbeiten()">+ Alle Arbeiten anzeigen</button>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
return """
// ===== Arbeiten Widget JavaScript =====
const ARBEITEN_STORAGE_KEY = 'bp-lehrer-arbeiten';
function getDefaultArbeiten() {
const today = new Date();
return [
{
id: 1,
titel: 'Klausur Deutsch - Gedichtanalyse',
klasse: '12c',
typ: 'klausur',
frist: new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
status: 'geplant'
},
{
id: 2,
titel: 'Aufsatz Korrektur',
klasse: '10a',
typ: 'korrektur',
frist: new Date(today.getTime() + 4 * 24 * 60 * 60 * 1000).toISOString(),
status: 'korrektur'
},
{
id: 3,
titel: 'Vokabeltest',
klasse: '11b',
typ: 'test',
frist: new Date(today.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString(),
status: 'geplant'
}
];
}
function loadArbeiten() {
const stored = localStorage.getItem(ARBEITEN_STORAGE_KEY);
return stored ? JSON.parse(stored) : getDefaultArbeiten();
}
function saveArbeiten(arbeiten) {
localStorage.setItem(ARBEITEN_STORAGE_KEY, JSON.stringify(arbeiten));
}
function getDaysUntil(dateStr) {
const date = new Date(dateStr);
const today = new Date();
today.setHours(0, 0, 0, 0);
date.setHours(0, 0, 0, 0);
return Math.ceil((date - today) / (24 * 60 * 60 * 1000));
}
function formatFrist(dateStr) {
const days = getDaysUntil(dateStr);
if (days < 0) return 'ueberfaellig';
if (days === 0) return 'heute';
if (days === 1) return 'morgen';
return `in ${days} Tagen`;
}
function getFristClass(dateStr) {
const days = getDaysUntil(dateStr);
if (days <= 2) return 'urgent';
if (days <= 5) return 'soon';
return '';
}
function getTypIcon(typ) {
const icons = {
klausur: '&#128196;',
test: '&#128203;',
korrektur: '&#128221;',
abgabe: '&#128230;'
};
return icons[typ] || '&#128196;';
}
function renderArbeiten() {
const list = document.getElementById('arbeiten-list');
if (!list) return;
let arbeiten = loadArbeiten();
// Sort by deadline
arbeiten.sort((a, b) => new Date(a.frist) - new Date(b.frist));
if (arbeiten.length === 0) {
list.innerHTML = `
<div class="arbeiten-empty">
<div class="arbeiten-empty-icon">&#127881;</div>
<div>Keine anstehenden Arbeiten</div>
</div>
`;
return;
}
list.innerHTML = arbeiten.map(arbeit => {
const fristClass = getFristClass(arbeit.frist);
return `
<div class="arbeit-item ${fristClass}" onclick="openArbeit(${arbeit.id})">
<div class="arbeit-icon">${getTypIcon(arbeit.typ)}</div>
<div class="arbeit-content">
<div class="arbeit-title">${arbeit.titel}</div>
<div class="arbeit-meta">
<span class="arbeit-type">${arbeit.typ}</span>
<span>${arbeit.klasse}</span>
<span class="arbeit-frist ${fristClass}">&#128197; ${formatFrist(arbeit.frist)}</span>
</div>
</div>
</div>
`;
}).join('');
}
function openArbeit(arbeitId) {
if (typeof loadModule === 'function') {
loadModule('klausur-korrektur');
console.log('Opening work:', arbeitId);
}
}
function openAllArbeiten() {
if (typeof loadModule === 'function') {
loadModule('klausur-korrektur');
}
}
function initArbeitenWidget() {
renderArbeiten();
}
"""

View File

@@ -0,0 +1,302 @@
"""
Fehlzeiten Widget fuer das Lehrer-Dashboard.
Zeigt abwesende Schueler fuer heute.
"""
class FehlzeitenWidget:
widget_id = 'fehlzeiten'
widget_name = 'Fehlzeiten'
widget_icon = '&#9888;' # Warning
widget_color = '#ef4444' # Red
default_width = 'half'
has_settings = True
@staticmethod
def get_css() -> str:
return """
/* ===== Fehlzeiten Widget Styles ===== */
.widget-fehlzeiten {
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 12px;
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.widget-fehlzeiten .widget-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-fehlzeiten .widget-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-fehlzeiten .widget-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
border-radius: 8px;
font-size: 14px;
}
.widget-fehlzeiten .fehlzeiten-count {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.widget-fehlzeiten .fehlzeiten-list {
flex: 1;
overflow-y: auto;
}
.widget-fehlzeiten .fehlzeit-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.05));
}
.widget-fehlzeiten .fehlzeit-item:last-child {
border-bottom: none;
}
.widget-fehlzeiten .fehlzeit-status {
width: 10px;
height: 10px;
border-radius: 50%;
margin-top: 5px;
flex-shrink: 0;
}
.widget-fehlzeiten .fehlzeit-status.krank {
background: #ef4444;
}
.widget-fehlzeiten .fehlzeit-status.entschuldigt {
background: #f59e0b;
}
.widget-fehlzeiten .fehlzeit-status.unentschuldigt {
background: #6b7280;
}
.widget-fehlzeiten .fehlzeit-content {
flex: 1;
}
.widget-fehlzeiten .fehlzeit-name {
font-size: 14px;
font-weight: 500;
color: var(--bp-text, #e5e7eb);
margin-bottom: 4px;
}
.widget-fehlzeiten .fehlzeit-details {
font-size: 12px;
color: var(--bp-text-muted, #9ca3af);
}
.widget-fehlzeiten .fehlzeit-details .grund {
display: inline-flex;
align-items: center;
gap: 4px;
margin-right: 8px;
}
.widget-fehlzeiten .fehlzeiten-empty {
text-align: center;
padding: 24px;
color: var(--bp-text-muted, #9ca3af);
font-size: 13px;
}
.widget-fehlzeiten .fehlzeiten-empty-icon {
font-size: 32px;
margin-bottom: 8px;
}
.widget-fehlzeiten .fehlzeiten-empty.success {
color: #10b981;
}
.widget-fehlzeiten .fehlzeiten-empty.success .fehlzeiten-empty-icon {
opacity: 1;
}
.widget-fehlzeiten .fehlzeiten-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-fehlzeiten .fehlzeiten-all-btn {
width: 100%;
padding: 10px;
background: transparent;
border: 1px dashed var(--bp-border, #475569);
border-radius: 8px;
color: var(--bp-text-muted, #9ca3af);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.widget-fehlzeiten .fehlzeiten-all-btn:hover {
border-color: #ef4444;
color: #ef4444;
}
"""
@staticmethod
def get_html() -> str:
return """
<div class="widget-fehlzeiten" data-widget-id="fehlzeiten">
<div class="widget-header">
<div class="widget-title">
<span class="widget-icon">&#9888;</span>
<span>Fehlzeiten heute</span>
</div>
<span class="fehlzeiten-count" id="fehlzeiten-count">0</span>
</div>
<div class="fehlzeiten-list" id="fehlzeiten-list">
<!-- Wird dynamisch gefuellt -->
</div>
<div class="fehlzeiten-footer">
<button class="fehlzeiten-all-btn" onclick="openAllFehlzeiten()">+ Alle Fehlzeiten anzeigen</button>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
return """
// ===== Fehlzeiten Widget JavaScript =====
const FEHLZEITEN_STORAGE_KEY = 'bp-lehrer-fehlzeiten';
function getDefaultFehlzeiten() {
// Demo data - in production this would come from an API
return [
{
id: 1,
name: 'Max Mueller',
klasse: '10a',
grund: 'krank',
seit: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
notiz: 'Attest liegt vor'
},
{
id: 2,
name: 'Lisa Schmidt',
klasse: '11b',
grund: 'entschuldigt',
seit: new Date().toISOString(),
notiz: 'Arzttermin'
},
{
id: 3,
name: 'Tom Weber',
klasse: '10a',
grund: 'unentschuldigt',
seit: new Date().toISOString(),
notiz: null
}
];
}
function loadFehlzeiten() {
const stored = localStorage.getItem(FEHLZEITEN_STORAGE_KEY);
return stored ? JSON.parse(stored) : getDefaultFehlzeiten();
}
function saveFehlzeiten(fehlzeiten) {
localStorage.setItem(FEHLZEITEN_STORAGE_KEY, JSON.stringify(fehlzeiten));
}
function formatFehlzeitDauer(seit) {
const seitDate = new Date(seit);
const now = new Date();
const diffDays = Math.floor((now - seitDate) / (24 * 60 * 60 * 1000));
if (diffDays === 0) return 'heute';
if (diffDays === 1) return 'seit gestern';
return `seit ${diffDays} Tagen`;
}
function getGrundLabel(grund) {
const labels = {
krank: '&#128567; Krank',
entschuldigt: '&#9989; Entschuldigt',
unentschuldigt: '&#10067; Unentschuldigt'
};
return labels[grund] || grund;
}
function renderFehlzeiten() {
const list = document.getElementById('fehlzeiten-list');
const countEl = document.getElementById('fehlzeiten-count');
if (!list) return;
const fehlzeiten = loadFehlzeiten();
if (countEl) {
countEl.textContent = fehlzeiten.length;
}
if (fehlzeiten.length === 0) {
list.innerHTML = `
<div class="fehlzeiten-empty success">
<div class="fehlzeiten-empty-icon">&#10003;</div>
<div>Alle Schueler anwesend!</div>
</div>
`;
return;
}
list.innerHTML = fehlzeiten.map(f => `
<div class="fehlzeit-item">
<div class="fehlzeit-status ${f.grund}"></div>
<div class="fehlzeit-content">
<div class="fehlzeit-name">${f.name} (${f.klasse})</div>
<div class="fehlzeit-details">
<span class="grund">${getGrundLabel(f.grund)}</span>
<span>${formatFehlzeitDauer(f.seit)}</span>
</div>
</div>
</div>
`).join('');
}
function openAllFehlzeiten() {
if (typeof loadModule === 'function') {
loadModule('school');
console.log('Opening all absences');
}
}
function initFehlzeitenWidget() {
renderFehlzeiten();
}
"""

View File

@@ -0,0 +1,313 @@
"""
Kalender Widget fuer das Lehrer-Dashboard.
Zeigt anstehende Termine und Events.
"""
class KalenderWidget:
widget_id = 'kalender'
widget_name = 'Termine'
widget_icon = '&#128198;' # Calendar
widget_color = '#ec4899' # Pink
default_width = 'half'
has_settings = True
@staticmethod
def get_css() -> str:
return """
/* ===== Kalender Widget Styles ===== */
.widget-kalender {
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 12px;
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.widget-kalender .widget-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-kalender .widget-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-kalender .widget-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(236, 72, 153, 0.15);
color: #ec4899;
border-radius: 8px;
font-size: 14px;
}
.widget-kalender .kalender-list {
flex: 1;
overflow-y: auto;
}
.widget-kalender .termin-item {
display: flex;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.05));
}
.widget-kalender .termin-item:last-child {
border-bottom: none;
}
.widget-kalender .termin-date {
width: 48px;
text-align: center;
flex-shrink: 0;
}
.widget-kalender .termin-day {
font-size: 20px;
font-weight: 700;
color: var(--bp-text, #e5e7eb);
line-height: 1;
}
.widget-kalender .termin-month {
font-size: 11px;
color: var(--bp-text-muted, #9ca3af);
text-transform: uppercase;
}
.widget-kalender .termin-content {
flex: 1;
min-width: 0;
}
.widget-kalender .termin-title {
font-size: 13px;
font-weight: 500;
color: var(--bp-text, #e5e7eb);
margin-bottom: 4px;
}
.widget-kalender .termin-meta {
font-size: 11px;
color: var(--bp-text-muted, #9ca3af);
display: flex;
gap: 8px;
}
.widget-kalender .termin-type {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
}
.widget-kalender .termin-type.konferenz {
background: rgba(139, 92, 246, 0.15);
color: #8b5cf6;
}
.widget-kalender .termin-type.elterngespraech {
background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
}
.widget-kalender .termin-type.fortbildung {
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
}
.widget-kalender .termin-type.pruefung {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.widget-kalender .kalender-empty {
text-align: center;
padding: 24px;
color: var(--bp-text-muted, #9ca3af);
font-size: 13px;
}
.widget-kalender .kalender-empty-icon {
font-size: 32px;
margin-bottom: 8px;
opacity: 0.5;
}
.widget-kalender .kalender-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-kalender .kalender-add-btn {
width: 100%;
padding: 10px;
background: transparent;
border: 1px dashed var(--bp-border, #475569);
border-radius: 8px;
color: var(--bp-text-muted, #9ca3af);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.widget-kalender .kalender-add-btn:hover {
border-color: #ec4899;
color: #ec4899;
}
"""
@staticmethod
def get_html() -> str:
return """
<div class="widget-kalender" data-widget-id="kalender">
<div class="widget-header">
<div class="widget-title">
<span class="widget-icon">&#128198;</span>
<span>Anstehende Termine</span>
</div>
<button class="widget-settings-btn" onclick="openWidgetSettings('kalender')" title="Einstellungen">&#9881;</button>
</div>
<div class="kalender-list" id="kalender-list">
<!-- Wird dynamisch gefuellt -->
</div>
<div class="kalender-footer">
<button class="kalender-add-btn" onclick="addTermin()">+ Termin hinzufuegen</button>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
return """
// ===== Kalender Widget JavaScript =====
const KALENDER_STORAGE_KEY = 'bp-lehrer-kalender';
function getDefaultTermine() {
const today = new Date();
return [
{
id: 1,
titel: 'Fachkonferenz Deutsch',
datum: new Date(today.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString(),
zeit: '14:00 - 16:00',
ort: 'Konferenzraum A',
typ: 'konferenz'
},
{
id: 2,
titel: 'Elterngespraech Mueller',
datum: new Date(today.getTime() + 4 * 24 * 60 * 60 * 1000).toISOString(),
zeit: '17:30 - 18:00',
ort: 'Klassenraum 204',
typ: 'elterngespraech'
},
{
id: 3,
titel: 'Fortbildung: Digitale Medien',
datum: new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
zeit: '09:00 - 15:00',
ort: 'Online',
typ: 'fortbildung'
}
];
}
function loadTermine() {
const stored = localStorage.getItem(KALENDER_STORAGE_KEY);
return stored ? JSON.parse(stored) : getDefaultTermine();
}
function saveTermine(termine) {
localStorage.setItem(KALENDER_STORAGE_KEY, JSON.stringify(termine));
}
function formatTerminDate(dateStr) {
const date = new Date(dateStr);
return {
day: date.getDate(),
month: date.toLocaleDateString('de-DE', { month: 'short' })
};
}
function getTypLabel(typ) {
const labels = {
konferenz: '&#128101; Konferenz',
elterngespraech: '&#128106; Eltern',
fortbildung: '&#128218; Fortbildung',
pruefung: '&#128221; Pruefung'
};
return labels[typ] || typ;
}
function renderKalender() {
const list = document.getElementById('kalender-list');
if (!list) return;
let termine = loadTermine();
// Sort by date
termine.sort((a, b) => new Date(a.datum) - new Date(b.datum));
// Filter only future events
const now = new Date();
termine = termine.filter(t => new Date(t.datum) >= now);
if (termine.length === 0) {
list.innerHTML = `
<div class="kalender-empty">
<div class="kalender-empty-icon">&#128198;</div>
<div>Keine anstehenden Termine</div>
</div>
`;
return;
}
list.innerHTML = termine.slice(0, 4).map(termin => {
const dateInfo = formatTerminDate(termin.datum);
return `
<div class="termin-item">
<div class="termin-date">
<div class="termin-day">${dateInfo.day}</div>
<div class="termin-month">${dateInfo.month}</div>
</div>
<div class="termin-content">
<div class="termin-title">${termin.titel}</div>
<div class="termin-meta">
<span class="termin-type ${termin.typ}">${getTypLabel(termin.typ)}</span>
<span>&#128337; ${termin.zeit}</span>
</div>
</div>
</div>
`;
}).join('');
}
function addTermin() {
alert('Termin-Editor wird in einer zukuenftigen Version verfuegbar sein.');
}
function initKalenderWidget() {
renderKalender();
}
"""

View File

@@ -0,0 +1,263 @@
"""
Klassen Widget fuer das Lehrer-Dashboard.
Zeigt eine Uebersicht aller Klassen des Lehrers.
"""
class KlassenWidget:
widget_id = 'klassen'
widget_name = 'Meine Klassen'
widget_icon = '&#128202;' # Chart
widget_color = '#8b5cf6' # Purple
default_width = 'half'
has_settings = True
@staticmethod
def get_css() -> str:
return """
/* ===== Klassen Widget Styles ===== */
.widget-klassen {
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 12px;
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.widget-klassen .widget-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-klassen .widget-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-klassen .widget-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(139, 92, 246, 0.15);
color: #8b5cf6;
border-radius: 8px;
font-size: 14px;
}
.widget-klassen .klassen-list {
flex: 1;
overflow-y: auto;
}
.widget-klassen .klasse-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
margin-bottom: 8px;
background: var(--bp-bg, #0f172a);
border: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.widget-klassen .klasse-item:last-child {
margin-bottom: 0;
}
.widget-klassen .klasse-item:hover {
background: var(--bp-surface-elevated, #334155);
border-color: #8b5cf6;
transform: translateX(4px);
}
.widget-klassen .klasse-info {
display: flex;
align-items: center;
gap: 12px;
}
.widget-klassen .klasse-badge {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(139, 92, 246, 0.15);
color: #8b5cf6;
border-radius: 8px;
font-size: 14px;
font-weight: 700;
}
.widget-klassen .klasse-details {
display: flex;
flex-direction: column;
gap: 2px;
}
.widget-klassen .klasse-name {
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-klassen .klasse-meta {
font-size: 12px;
color: var(--bp-text-muted, #9ca3af);
}
.widget-klassen .klasse-arrow {
color: var(--bp-text-muted, #9ca3af);
font-size: 16px;
transition: transform 0.2s;
}
.widget-klassen .klasse-item:hover .klasse-arrow {
transform: translateX(4px);
color: #8b5cf6;
}
.widget-klassen .klassen-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-klassen .klassen-add-btn {
width: 100%;
padding: 10px;
background: transparent;
border: 1px dashed var(--bp-border, #475569);
border-radius: 8px;
color: var(--bp-text-muted, #9ca3af);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.widget-klassen .klassen-add-btn:hover {
border-color: #8b5cf6;
color: #8b5cf6;
}
.widget-klassen .klassen-empty {
text-align: center;
padding: 24px;
color: var(--bp-text-muted, #9ca3af);
font-size: 13px;
}
.widget-klassen .klassen-empty-icon {
font-size: 32px;
margin-bottom: 8px;
opacity: 0.5;
}
"""
@staticmethod
def get_html() -> str:
return """
<div class="widget-klassen" data-widget-id="klassen">
<div class="widget-header">
<div class="widget-title">
<span class="widget-icon">&#128202;</span>
<span>Meine Klassen</span>
</div>
<button class="widget-settings-btn" onclick="openWidgetSettings('klassen')" title="Einstellungen">&#9881;</button>
</div>
<div class="klassen-list" id="klassen-list">
<!-- Wird dynamisch gefuellt -->
</div>
<div class="klassen-footer">
<button class="klassen-add-btn" onclick="openKlassenManagement()">+ Alle Klassen anzeigen</button>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
return """
// ===== Klassen Widget JavaScript =====
const KLASSEN_STORAGE_KEY = 'bp-lehrer-klassen';
function getDefaultKlassen() {
return [
{ id: '10a', name: 'Klasse 10a', schueler: 28, fach: 'Deutsch', klassenlehrer: false },
{ id: '11b', name: 'Klasse 11b', schueler: 26, fach: 'Deutsch', klassenlehrer: true },
{ id: '12c', name: 'Klasse 12c', schueler: 24, fach: 'Deutsch', klassenlehrer: false }
];
}
function loadKlassen() {
const stored = localStorage.getItem(KLASSEN_STORAGE_KEY);
return stored ? JSON.parse(stored) : getDefaultKlassen();
}
function saveKlassen(klassen) {
localStorage.setItem(KLASSEN_STORAGE_KEY, JSON.stringify(klassen));
}
function renderKlassen() {
const list = document.getElementById('klassen-list');
if (!list) return;
const klassen = loadKlassen();
if (klassen.length === 0) {
list.innerHTML = `
<div class="klassen-empty">
<div class="klassen-empty-icon">&#127979;</div>
<div>Keine Klassen zugewiesen</div>
</div>
`;
return;
}
list.innerHTML = klassen.map(klasse => `
<div class="klasse-item" onclick="openKlasse('${klasse.id}')">
<div class="klasse-info">
<div class="klasse-badge">${klasse.id}</div>
<div class="klasse-details">
<span class="klasse-name">${klasse.name}${klasse.klassenlehrer ? ' &#11088;' : ''}</span>
<span class="klasse-meta">${klasse.schueler} Schueler &middot; ${klasse.fach}</span>
</div>
</div>
<span class="klasse-arrow">&#8594;</span>
</div>
`).join('');
}
function openKlasse(klasseId) {
// Navigate to school module with class selected
if (typeof loadModule === 'function') {
loadModule('school');
// Could set a flag to auto-select the class
console.log('Opening class:', klasseId);
}
}
function openKlassenManagement() {
if (typeof loadModule === 'function') {
loadModule('school');
}
}
function initKlassenWidget() {
renderKlassen();
}
"""

View File

@@ -0,0 +1,289 @@
"""
Matrix Chat Widget fuer das Lehrer-Dashboard.
Zeigt die letzten Chat-Nachrichten aus dem Matrix Messenger.
"""
class MatrixWidget:
widget_id = 'matrix'
widget_name = 'Matrix-Chat'
widget_icon = '&#128172;' # Speech bubble
widget_color = '#8b5cf6' # Purple
default_width = 'half'
has_settings = True
@staticmethod
def get_css() -> str:
return """
/* ===== Matrix Widget Styles ===== */
.widget-matrix {
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 12px;
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.widget-matrix .widget-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-matrix .widget-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-matrix .widget-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(139, 92, 246, 0.15);
color: #8b5cf6;
border-radius: 8px;
font-size: 14px;
}
.widget-matrix .matrix-list {
flex: 1;
overflow-y: auto;
}
.widget-matrix .chat-item {
display: flex;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.05));
cursor: pointer;
transition: background 0.2s;
}
.widget-matrix .chat-item:last-child {
border-bottom: none;
}
.widget-matrix .chat-item:hover {
background: var(--bp-surface-elevated, #334155);
margin: 0 -12px;
padding: 10px 12px;
border-radius: 8px;
}
.widget-matrix .chat-avatar {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(139, 92, 246, 0.15);
color: #8b5cf6;
border-radius: 50%;
font-size: 12px;
flex-shrink: 0;
}
.widget-matrix .chat-content {
flex: 1;
min-width: 0;
}
.widget-matrix .chat-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2px;
}
.widget-matrix .chat-room {
font-size: 12px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-matrix .chat-time {
font-size: 10px;
color: var(--bp-text-muted, #9ca3af);
}
.widget-matrix .chat-message {
font-size: 12px;
color: var(--bp-text-muted, #9ca3af);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.widget-matrix .chat-unread {
width: 8px;
height: 8px;
background: #8b5cf6;
border-radius: 50%;
margin-left: 8px;
}
.widget-matrix .matrix-empty {
text-align: center;
padding: 24px;
color: var(--bp-text-muted, #9ca3af);
font-size: 13px;
}
.widget-matrix .matrix-empty-icon {
font-size: 32px;
margin-bottom: 8px;
opacity: 0.5;
}
.widget-matrix .matrix-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-matrix .matrix-all-btn {
width: 100%;
padding: 10px;
background: transparent;
border: 1px dashed var(--bp-border, #475569);
border-radius: 8px;
color: var(--bp-text-muted, #9ca3af);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.widget-matrix .matrix-all-btn:hover {
border-color: #8b5cf6;
color: #8b5cf6;
}
"""
@staticmethod
def get_html() -> str:
return """
<div class="widget-matrix" data-widget-id="matrix">
<div class="widget-header">
<div class="widget-title">
<span class="widget-icon">&#128172;</span>
<span>Matrix-Chat</span>
</div>
<button class="widget-settings-btn" onclick="openWidgetSettings('matrix')" title="Einstellungen">&#9881;</button>
</div>
<div class="matrix-list" id="matrix-list">
<!-- Wird dynamisch gefuellt -->
</div>
<div class="matrix-footer">
<button class="matrix-all-btn" onclick="openMessenger()">+ Messenger oeffnen</button>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
return """
// ===== Matrix Widget JavaScript =====
function getDefaultMatrixChats() {
const now = Date.now();
return [
{
id: 'room1',
room: 'Kollegium Deutsch',
lastMessage: 'Hat jemand das neue Curriculum?',
sender: 'Fr. Becker',
time: new Date(now - 30 * 60 * 1000).toISOString(),
unread: true
},
{
id: 'room2',
room: 'Klassenfahrt 10a',
lastMessage: 'Die Anmeldungen sind komplett!',
sender: 'Hr. Klein',
time: new Date(now - 3 * 60 * 60 * 1000).toISOString(),
unread: false
},
{
id: 'room3',
room: 'Fachschaft',
lastMessage: 'Termin fuer naechste Sitzung...',
sender: 'Sie',
time: new Date(now - 24 * 60 * 60 * 1000).toISOString(),
unread: false
}
];
}
function formatMatrixTime(timeStr) {
const time = new Date(timeStr);
const now = new Date();
const diffMs = now - time;
const diffMins = Math.floor(diffMs / (60 * 1000));
const diffHours = Math.floor(diffMs / (60 * 60 * 1000));
if (diffMins < 1) return 'jetzt';
if (diffMins < 60) return `${diffMins}m`;
if (diffHours < 24) return `${diffHours}h`;
return time.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
}
function renderMatrixChats() {
const list = document.getElementById('matrix-list');
if (!list) return;
const chats = getDefaultMatrixChats();
if (chats.length === 0) {
list.innerHTML = `
<div class="matrix-empty">
<div class="matrix-empty-icon">&#128172;</div>
<div>Keine Chats verfuegbar</div>
</div>
`;
return;
}
list.innerHTML = chats.map(chat => `
<div class="chat-item" onclick="openMatrixRoom('${chat.id}')">
<div class="chat-avatar">&#128172;</div>
<div class="chat-content">
<div class="chat-header">
<span class="chat-room">${chat.room}</span>
<span class="chat-time">${formatMatrixTime(chat.time)}</span>
</div>
<div class="chat-message">${chat.sender}: ${chat.lastMessage}</div>
</div>
${chat.unread ? '<div class="chat-unread"></div>' : ''}
</div>
`).join('');
}
function openMatrixRoom(roomId) {
if (typeof loadModule === 'function') {
loadModule('messenger');
console.log('Opening room:', roomId);
}
}
function openMessenger() {
if (typeof loadModule === 'function') {
loadModule('messenger');
}
}
function initMatrixWidget() {
renderMatrixChats();
}
"""

View File

@@ -0,0 +1,317 @@
"""
Nachrichten Widget fuer das Lehrer-Dashboard.
Zeigt die letzten E-Mails aus dem Mail-Inbox Modul.
"""
class NachrichtenWidget:
widget_id = 'nachrichten'
widget_name = 'E-Mails'
widget_icon = '&#128231;' # Email
widget_color = '#06b6d4' # Cyan
default_width = 'half'
has_settings = True
@staticmethod
def get_css() -> str:
return """
/* ===== Nachrichten Widget Styles ===== */
.widget-nachrichten {
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 12px;
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.widget-nachrichten .widget-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-nachrichten .widget-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-nachrichten .widget-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(6, 182, 212, 0.15);
color: #06b6d4;
border-radius: 8px;
font-size: 14px;
}
.widget-nachrichten .nachrichten-unread {
background: #ef4444;
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
}
.widget-nachrichten .nachrichten-list {
flex: 1;
overflow-y: auto;
}
.widget-nachrichten .nachricht-item {
display: flex;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.05));
cursor: pointer;
transition: background 0.2s;
}
.widget-nachrichten .nachricht-item:last-child {
border-bottom: none;
}
.widget-nachrichten .nachricht-item:hover {
background: var(--bp-surface-elevated, #334155);
margin: 0 -12px;
padding: 12px;
border-radius: 8px;
}
.widget-nachrichten .nachricht-item.unread .nachricht-sender {
font-weight: 600;
}
.widget-nachrichten .nachricht-avatar {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(6, 182, 212, 0.15);
color: #06b6d4;
border-radius: 50%;
font-size: 14px;
flex-shrink: 0;
}
.widget-nachrichten .nachricht-content {
flex: 1;
min-width: 0;
}
.widget-nachrichten .nachricht-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.widget-nachrichten .nachricht-sender {
font-size: 13px;
color: var(--bp-text, #e5e7eb);
}
.widget-nachrichten .nachricht-time {
font-size: 11px;
color: var(--bp-text-muted, #9ca3af);
}
.widget-nachrichten .nachricht-preview {
font-size: 12px;
color: var(--bp-text-muted, #9ca3af);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.widget-nachrichten .nachrichten-empty {
text-align: center;
padding: 24px;
color: var(--bp-text-muted, #9ca3af);
font-size: 13px;
}
.widget-nachrichten .nachrichten-empty-icon {
font-size: 32px;
margin-bottom: 8px;
opacity: 0.5;
}
.widget-nachrichten .nachrichten-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-nachrichten .nachrichten-all-btn {
width: 100%;
padding: 10px;
background: transparent;
border: 1px dashed var(--bp-border, #475569);
border-radius: 8px;
color: var(--bp-text-muted, #9ca3af);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.widget-nachrichten .nachrichten-all-btn:hover {
border-color: #06b6d4;
color: #06b6d4;
}
"""
@staticmethod
def get_html() -> str:
return """
<div class="widget-nachrichten" data-widget-id="nachrichten">
<div class="widget-header">
<div class="widget-title">
<span class="widget-icon">&#128231;</span>
<span>Letzte Nachrichten</span>
</div>
<span class="nachrichten-unread" id="nachrichten-unread" style="display: none;">0</span>
</div>
<div class="nachrichten-list" id="nachrichten-list">
<!-- Wird dynamisch gefuellt -->
</div>
<div class="nachrichten-footer">
<button class="nachrichten-all-btn" onclick="openAllNachrichten()">+ Alle Nachrichten anzeigen</button>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
return """
// ===== Nachrichten Widget JavaScript =====
function getDefaultNachrichten() {
const now = Date.now();
return [
{
id: 1,
sender: 'Fr. Mueller (Eltern)',
email: 'mueller@example.com',
preview: 'Frage zu den Hausaufgaben von gestern...',
time: new Date(now - 2 * 60 * 60 * 1000).toISOString(),
unread: true,
type: 'eltern'
},
{
id: 2,
sender: 'Hr. Weber (Schulleitung)',
email: 'weber@schule.de',
preview: 'Terminabsprache fuer naechste Woche...',
time: new Date(now - 24 * 60 * 60 * 1000).toISOString(),
unread: false,
type: 'kollegium'
},
{
id: 3,
sender: 'Lisa Schmidt (11b)',
email: 'schmidt@schueler.de',
preview: 'Krankmeldung fuer morgen...',
time: new Date(now - 48 * 60 * 60 * 1000).toISOString(),
unread: false,
type: 'schueler'
}
];
}
function formatNachrichtenTime(timeStr) {
const time = new Date(timeStr);
const now = new Date();
const diffMs = now - time;
const diffMins = Math.floor(diffMs / (60 * 1000));
const diffHours = Math.floor(diffMs / (60 * 60 * 1000));
const diffDays = Math.floor(diffMs / (24 * 60 * 60 * 1000));
if (diffMins < 1) return 'gerade eben';
if (diffMins < 60) return `vor ${diffMins} Min.`;
if (diffHours < 24) return `vor ${diffHours} Std.`;
if (diffDays === 1) return 'gestern';
return `vor ${diffDays} Tagen`;
}
function getTypeIcon(type) {
const icons = {
eltern: '&#128106;',
schueler: '&#129489;',
kollegium: '&#128188;'
};
return icons[type] || '&#128231;';
}
function renderNachrichten() {
const list = document.getElementById('nachrichten-list');
const unreadBadge = document.getElementById('nachrichten-unread');
if (!list) return;
const nachrichten = getDefaultNachrichten();
const unreadCount = nachrichten.filter(n => n.unread).length;
if (unreadBadge) {
if (unreadCount > 0) {
unreadBadge.textContent = unreadCount;
unreadBadge.style.display = 'inline';
} else {
unreadBadge.style.display = 'none';
}
}
if (nachrichten.length === 0) {
list.innerHTML = `
<div class="nachrichten-empty">
<div class="nachrichten-empty-icon">&#128231;</div>
<div>Keine Nachrichten</div>
</div>
`;
return;
}
list.innerHTML = nachrichten.map(n => `
<div class="nachricht-item ${n.unread ? 'unread' : ''}" onclick="openNachricht(${n.id})">
<div class="nachricht-avatar">${getTypeIcon(n.type)}</div>
<div class="nachricht-content">
<div class="nachricht-header">
<span class="nachricht-sender">${n.sender}</span>
<span class="nachricht-time">${formatNachrichtenTime(n.time)}</span>
</div>
<div class="nachricht-preview">${n.preview}</div>
</div>
</div>
`).join('');
}
function openNachricht(nachrichtId) {
if (typeof loadModule === 'function') {
loadModule('mail-inbox');
console.log('Opening message:', nachrichtId);
}
}
function openAllNachrichten() {
if (typeof loadModule === 'function') {
loadModule('mail-inbox');
}
}
function initNachrichtenWidget() {
renderNachrichten();
}
"""

View File

@@ -0,0 +1,182 @@
"""
Notizen Widget fuer das Lehrer-Dashboard.
Zeigt ein einfaches Notizfeld mit localStorage-Persistierung.
"""
class NotizenWidget:
widget_id = 'notizen'
widget_name = 'Notizen'
widget_icon = '&#128203;' # Clipboard
widget_color = '#fbbf24' # Yellow
default_width = 'half'
has_settings = False
@staticmethod
def get_css() -> str:
return """
/* ===== Notizen Widget Styles ===== */
.widget-notizen {
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 12px;
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.widget-notizen .widget-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.widget-notizen .widget-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-notizen .widget-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(251, 191, 36, 0.15);
color: #fbbf24;
border-radius: 8px;
font-size: 14px;
}
.widget-notizen .notizen-save-indicator {
font-size: 11px;
color: var(--bp-text-muted, #9ca3af);
opacity: 0;
transition: opacity 0.3s;
}
.widget-notizen .notizen-save-indicator.visible {
opacity: 1;
}
.widget-notizen .notizen-textarea {
flex: 1;
width: 100%;
min-height: 120px;
background: var(--bp-bg, #0f172a);
border: 1px solid var(--bp-border, #475569);
border-radius: 8px;
padding: 12px;
font-size: 13px;
font-family: inherit;
color: var(--bp-text, #e5e7eb);
resize: none;
outline: none;
transition: border-color 0.2s;
line-height: 1.5;
}
.widget-notizen .notizen-textarea:focus {
border-color: #fbbf24;
}
.widget-notizen .notizen-textarea::placeholder {
color: var(--bp-text-muted, #9ca3af);
}
.widget-notizen .notizen-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
font-size: 11px;
color: var(--bp-text-muted, #9ca3af);
}
.widget-notizen .notizen-char-count {
opacity: 0.7;
}
"""
@staticmethod
def get_html() -> str:
return """
<div class="widget-notizen" data-widget-id="notizen">
<div class="widget-header">
<div class="widget-title">
<span class="widget-icon">&#128203;</span>
<span>Schnelle Notizen</span>
</div>
<span class="notizen-save-indicator" id="notizen-save-indicator">Gespeichert</span>
</div>
<textarea class="notizen-textarea" id="notizen-textarea" placeholder="Schreiben Sie hier Ihre Notizen..."></textarea>
<div class="notizen-footer">
<span class="notizen-char-count" id="notizen-char-count">0 Zeichen</span>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
return """
// ===== Notizen Widget JavaScript =====
const NOTIZEN_STORAGE_KEY = 'bp-lehrer-notizen';
let notizenSaveTimeout = null;
function loadNotizen() {
const stored = localStorage.getItem(NOTIZEN_STORAGE_KEY);
return stored || '';
}
function saveNotizen(text) {
localStorage.setItem(NOTIZEN_STORAGE_KEY, text);
showNotizenSaved();
}
function showNotizenSaved() {
const indicator = document.getElementById('notizen-save-indicator');
if (indicator) {
indicator.classList.add('visible');
setTimeout(() => {
indicator.classList.remove('visible');
}, 2000);
}
}
function updateNotizenCharCount() {
const textarea = document.getElementById('notizen-textarea');
const counter = document.getElementById('notizen-char-count');
if (textarea && counter) {
counter.textContent = textarea.value.length + ' Zeichen';
}
}
function initNotizenWidget() {
const textarea = document.getElementById('notizen-textarea');
if (!textarea) return;
// Load saved notes
textarea.value = loadNotizen();
updateNotizenCharCount();
// Auto-save on input with debounce
textarea.addEventListener('input', function() {
updateNotizenCharCount();
if (notizenSaveTimeout) {
clearTimeout(notizenSaveTimeout);
}
notizenSaveTimeout = setTimeout(() => {
saveNotizen(textarea.value);
}, 500);
});
}
"""

View File

@@ -0,0 +1,196 @@
"""
Schnellzugriff Widget fuer das Lehrer-Dashboard.
Zeigt schnelle Links zu den wichtigsten Modulen.
"""
class SchnellzugriffWidget:
widget_id = 'schnellzugriff'
widget_name = 'Schnellzugriff'
widget_icon = '&#9889;' # Lightning
widget_color = '#6b7280' # Gray
default_width = 'full'
has_settings = True
@staticmethod
def get_css() -> str:
return """
/* ===== Schnellzugriff Widget Styles ===== */
.widget-schnellzugriff {
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 12px;
padding: 16px;
}
.widget-schnellzugriff .widget-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.widget-schnellzugriff .widget-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-schnellzugriff .widget-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(107, 114, 128, 0.15);
color: #6b7280;
border-radius: 8px;
font-size: 14px;
}
.widget-schnellzugriff .quick-links {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 12px;
}
.widget-schnellzugriff .quick-link {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 12px;
background: var(--bp-bg, #0f172a);
border: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
border-radius: 10px;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.widget-schnellzugriff .quick-link:hover {
background: var(--bp-surface-elevated, #334155);
border-color: var(--bp-primary, #6C1B1B);
transform: translateY(-2px);
}
.widget-schnellzugriff .quick-link-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
font-size: 20px;
}
.widget-schnellzugriff .quick-link-label {
font-size: 12px;
font-weight: 500;
color: var(--bp-text, #e5e7eb);
text-align: center;
}
/* Icon colors */
.widget-schnellzugriff .quick-link[data-module="worksheets"] .quick-link-icon {
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
}
.widget-schnellzugriff .quick-link[data-module="correction"] .quick-link-icon {
background: rgba(16, 185, 129, 0.15);
color: #10b981;
}
.widget-schnellzugriff .quick-link[data-module="letters"] .quick-link-icon {
background: rgba(139, 92, 246, 0.15);
color: #8b5cf6;
}
.widget-schnellzugriff .quick-link[data-module="jitsi"] .quick-link-icon {
background: rgba(236, 72, 153, 0.15);
color: #ec4899;
}
.widget-schnellzugriff .quick-link[data-module="klausur-korrektur"] .quick-link-icon {
background: rgba(168, 85, 247, 0.15);
color: #a855f7;
}
.widget-schnellzugriff .quick-link[data-module="messenger"] .quick-link-icon {
background: rgba(6, 182, 212, 0.15);
color: #06b6d4;
}
.widget-schnellzugriff .quick-link[data-module="school"] .quick-link-icon {
background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
}
.widget-schnellzugriff .quick-link[data-module="companion"] .quick-link-icon {
background: rgba(34, 197, 94, 0.15);
color: #22c55e;
}
"""
@staticmethod
def get_html() -> str:
return """
<div class="widget-schnellzugriff" data-widget-id="schnellzugriff">
<div class="widget-header">
<div class="widget-title">
<span class="widget-icon">&#9889;</span>
<span>Schnellzugriff</span>
</div>
<button class="widget-settings-btn" onclick="openWidgetSettings('schnellzugriff')" title="Einstellungen">&#9881;</button>
</div>
<div class="quick-links">
<div class="quick-link" data-module="worksheets" onclick="loadModule('worksheets')">
<div class="quick-link-icon">&#128221;</div>
<span class="quick-link-label">Arbeitsblatt</span>
</div>
<div class="quick-link" data-module="klausur-korrektur" onclick="loadModule('klausur-korrektur')">
<div class="quick-link-icon">&#10003;</div>
<span class="quick-link-label">Klausur</span>
</div>
<div class="quick-link" data-module="letters" onclick="loadModule('letters')">
<div class="quick-link-icon">&#9993;</div>
<span class="quick-link-label">Elternbrief</span>
</div>
<div class="quick-link" data-module="jitsi" onclick="loadModule('jitsi')">
<div class="quick-link-icon">&#127909;</div>
<span class="quick-link-label">Konferenz</span>
</div>
<div class="quick-link" data-module="school" onclick="loadModule('school')">
<div class="quick-link-icon">&#127979;</div>
<span class="quick-link-label">Schule</span>
</div>
<div class="quick-link" data-module="messenger" onclick="loadModule('messenger')">
<div class="quick-link-icon">&#128172;</div>
<span class="quick-link-label">Messenger</span>
</div>
<div class="quick-link" data-module="companion" onclick="loadModule('companion')">
<div class="quick-link-icon">&#128218;</div>
<span class="quick-link-label">Begleiter</span>
</div>
<div class="quick-link" data-module="correction" onclick="loadModule('correction')">
<div class="quick-link-icon">&#128196;</div>
<span class="quick-link-label">Material</span>
</div>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
return """
// ===== Schnellzugriff Widget JavaScript =====
function initSchnellzugriffWidget() {
// Quick links are already set up with onclick handlers
console.log('Schnellzugriff widget initialized');
}
"""

View File

@@ -0,0 +1,311 @@
"""
Statistik Widget fuer das Lehrer-Dashboard.
Zeigt Noten-Statistiken und Klassenauswertungen.
"""
class StatistikWidget:
widget_id = 'statistik'
widget_name = 'Klassenstatistik'
widget_icon = '&#128200;' # Chart
widget_color = '#3b82f6' # Blue
default_width = 'full'
has_settings = True
@staticmethod
def get_css() -> str:
return """
/* ===== Statistik Widget Styles ===== */
.widget-statistik {
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 12px;
padding: 16px;
}
.widget-statistik .widget-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.widget-statistik .widget-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-statistik .widget-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
border-radius: 8px;
font-size: 14px;
}
.widget-statistik .statistik-select {
background: var(--bp-bg, #0f172a);
border: 1px solid var(--bp-border, #475569);
border-radius: 6px;
padding: 6px 12px;
font-size: 12px;
color: var(--bp-text, #e5e7eb);
cursor: pointer;
}
.widget-statistik .statistik-content {
display: grid;
grid-template-columns: 1fr auto;
gap: 24px;
}
.widget-statistik .statistik-chart {
min-height: 120px;
}
.widget-statistik .statistik-bars {
display: flex;
align-items: flex-end;
gap: 8px;
height: 100px;
padding: 0 8px;
}
.widget-statistik .statistik-bar {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.widget-statistik .statistik-bar-fill {
width: 100%;
background: linear-gradient(180deg, #3b82f6 0%, #1d4ed8 100%);
border-radius: 4px 4px 0 0;
min-height: 4px;
transition: height 0.5s ease;
}
.widget-statistik .statistik-bar-label {
font-size: 11px;
color: var(--bp-text-muted, #9ca3af);
}
.widget-statistik .statistik-bar-value {
font-size: 10px;
color: var(--bp-text, #e5e7eb);
font-weight: 600;
}
.widget-statistik .statistik-summary {
min-width: 160px;
padding: 12px;
background: var(--bp-bg, #0f172a);
border-radius: 8px;
}
.widget-statistik .statistik-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.05));
}
.widget-statistik .statistik-item:last-child {
border-bottom: none;
}
.widget-statistik .statistik-label {
font-size: 12px;
color: var(--bp-text-muted, #9ca3af);
}
.widget-statistik .statistik-value {
font-size: 12px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-statistik .statistik-value.good {
color: #10b981;
}
.widget-statistik .statistik-value.warning {
color: #f59e0b;
}
.widget-statistik .statistik-value.bad {
color: #ef4444;
}
.widget-statistik .statistik-test-info {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
font-size: 11px;
color: var(--bp-text-muted, #9ca3af);
}
.widget-statistik .statistik-empty {
text-align: center;
padding: 32px;
color: var(--bp-text-muted, #9ca3af);
}
.widget-statistik .statistik-empty-icon {
font-size: 40px;
margin-bottom: 8px;
opacity: 0.5;
}
@media (max-width: 600px) {
.widget-statistik .statistik-content {
grid-template-columns: 1fr;
}
}
"""
@staticmethod
def get_html() -> str:
return """
<div class="widget-statistik" data-widget-id="statistik">
<div class="widget-header">
<div class="widget-title">
<span class="widget-icon">&#128200;</span>
<span>Klassenstatistik</span>
</div>
<div style="display: flex; gap: 8px; align-items: center;">
<select class="statistik-select" id="statistik-klasse" onchange="renderStatistik()">
<option value="10a">Klasse 10a</option>
<option value="11b">Klasse 11b</option>
<option value="12c">Klasse 12c</option>
</select>
<button class="widget-settings-btn" onclick="openWidgetSettings('statistik')" title="Einstellungen">&#9881;</button>
</div>
</div>
<div id="statistik-content">
<!-- Wird dynamisch gefuellt -->
</div>
</div>
"""
@staticmethod
def get_js() -> str:
return """
// ===== Statistik Widget JavaScript =====
function getDefaultStatistik() {
return {
'10a': {
testName: 'Klassenarbeit: Gedichtanalyse',
testDate: '12.01.2026',
noten: { 1: 3, 2: 5, 3: 8, 4: 7, 5: 4, 6: 1 },
durchschnitt: 3.2,
median: 3,
bestNote: 1,
schlechteste: 6,
schuelerAnzahl: 28
},
'11b': {
testName: 'Vokabeltest',
testDate: '18.01.2026',
noten: { 1: 6, 2: 8, 3: 7, 4: 4, 5: 1, 6: 0 },
durchschnitt: 2.3,
median: 2,
bestNote: 1,
schlechteste: 5,
schuelerAnzahl: 26
},
'12c': {
testName: 'Probeabitur',
testDate: '05.01.2026',
noten: { 1: 2, 2: 4, 3: 9, 4: 6, 5: 2, 6: 1 },
durchschnitt: 3.1,
median: 3,
bestNote: 1,
schlechteste: 6,
schuelerAnzahl: 24
}
};
}
function getDurchschnittClass(durchschnitt) {
if (durchschnitt <= 2.5) return 'good';
if (durchschnitt <= 3.5) return 'warning';
return 'bad';
}
function renderStatistik() {
const content = document.getElementById('statistik-content');
const klasseSelect = document.getElementById('statistik-klasse');
if (!content) return;
const selectedKlasse = klasseSelect ? klasseSelect.value : '10a';
const allStats = getDefaultStatistik();
const stats = allStats[selectedKlasse];
if (!stats) {
content.innerHTML = `
<div class="statistik-empty">
<div class="statistik-empty-icon">&#128200;</div>
<div>Keine Statistik verfuegbar</div>
</div>
`;
return;
}
const maxCount = Math.max(...Object.values(stats.noten));
content.innerHTML = `
<div class="statistik-content">
<div class="statistik-chart">
<div class="statistik-bars">
${Object.entries(stats.noten).map(([note, count]) => `
<div class="statistik-bar">
<div class="statistik-bar-value">${count}</div>
<div class="statistik-bar-fill" style="height: ${(count / maxCount) * 80}px;"></div>
<div class="statistik-bar-label">${note}</div>
</div>
`).join('')}
</div>
</div>
<div class="statistik-summary">
<div class="statistik-item">
<span class="statistik-label">Durchschnitt</span>
<span class="statistik-value ${getDurchschnittClass(stats.durchschnitt)}">${stats.durchschnitt.toFixed(1)}</span>
</div>
<div class="statistik-item">
<span class="statistik-label">Median</span>
<span class="statistik-value">${stats.median}</span>
</div>
<div class="statistik-item">
<span class="statistik-label">Beste Note</span>
<span class="statistik-value good">${stats.bestNote} (${stats.noten[stats.bestNote]}x)</span>
</div>
<div class="statistik-item">
<span class="statistik-label">Schueler</span>
<span class="statistik-value">${stats.schuelerAnzahl}</span>
</div>
<div class="statistik-test-info">
${stats.testName}<br>
${stats.testDate}
</div>
</div>
</div>
`;
}
function initStatistikWidget() {
renderStatistik();
}
"""

View File

@@ -0,0 +1,323 @@
"""
Stundenplan Widget fuer das Lehrer-Dashboard.
Zeigt den heutigen Stundenplan des Lehrers.
"""
class StundenplanWidget:
widget_id = 'stundenplan'
widget_name = 'Stundenplan'
widget_icon = '&#128197;' # Calendar
widget_color = '#3b82f6' # Blue
default_width = 'half'
has_settings = True
@staticmethod
def get_css() -> str:
return """
/* ===== Stundenplan Widget Styles ===== */
.widget-stundenplan {
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 12px;
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.widget-stundenplan .widget-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-stundenplan .widget-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-stundenplan .widget-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
border-radius: 8px;
font-size: 14px;
}
.widget-stundenplan .stundenplan-date {
font-size: 12px;
color: var(--bp-text-muted, #9ca3af);
}
.widget-stundenplan .stundenplan-list {
flex: 1;
overflow-y: auto;
}
.widget-stundenplan .stunde-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.05));
}
.widget-stundenplan .stunde-item:last-child {
border-bottom: none;
}
.widget-stundenplan .stunde-item.current {
background: rgba(59, 130, 246, 0.1);
margin: 0 -12px;
padding: 12px;
border-radius: 8px;
border-left: 3px solid #3b82f6;
}
.widget-stundenplan .stunde-item.frei {
opacity: 0.5;
}
.widget-stundenplan .stunde-zeit {
width: 70px;
flex-shrink: 0;
}
.widget-stundenplan .stunde-zeit-von,
.widget-stundenplan .stunde-zeit-bis {
font-size: 12px;
color: var(--bp-text-muted, #9ca3af);
}
.widget-stundenplan .stunde-zeit-von {
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-stundenplan .stunde-content {
flex: 1;
}
.widget-stundenplan .stunde-fach {
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
margin-bottom: 4px;
}
.widget-stundenplan .stunde-details {
font-size: 12px;
color: var(--bp-text-muted, #9ca3af);
display: flex;
gap: 12px;
}
.widget-stundenplan .stunde-details span {
display: flex;
align-items: center;
gap: 4px;
}
.widget-stundenplan .stundenplan-empty {
text-align: center;
padding: 24px;
color: var(--bp-text-muted, #9ca3af);
font-size: 13px;
}
.widget-stundenplan .stundenplan-empty-icon {
font-size: 32px;
margin-bottom: 8px;
opacity: 0.5;
}
.widget-stundenplan .stundenplan-edit-btn {
margin-top: 12px;
width: 100%;
padding: 10px;
background: var(--bp-bg, #0f172a);
border: 1px dashed var(--bp-border, #475569);
border-radius: 8px;
color: var(--bp-text-muted, #9ca3af);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.widget-stundenplan .stundenplan-edit-btn:hover {
border-color: #3b82f6;
color: #3b82f6;
}
"""
@staticmethod
def get_html() -> str:
return """
<div class="widget-stundenplan" data-widget-id="stundenplan">
<div class="widget-header">
<div class="widget-title">
<span class="widget-icon">&#128197;</span>
<span>Stundenplan heute</span>
</div>
<button class="widget-settings-btn" onclick="openWidgetSettings('stundenplan')" title="Einstellungen">&#9881;</button>
</div>
<div class="stundenplan-date" id="stundenplan-date"></div>
<div class="stundenplan-list" id="stundenplan-list">
<!-- Wird dynamisch gefuellt -->
</div>
<button class="stundenplan-edit-btn" onclick="editStundenplan()">&#128221; Stundenplan bearbeiten</button>
</div>
"""
@staticmethod
def get_js() -> str:
return """
// ===== Stundenplan Widget JavaScript =====
const STUNDENPLAN_STORAGE_KEY = 'bp-lehrer-stundenplan';
function getDefaultStundenplan() {
return {
montag: [
{ von: '08:00', bis: '09:30', fach: 'Deutsch', klasse: '10a', raum: '204' },
{ von: '09:45', bis: '11:15', fach: 'Deutsch', klasse: '11b', raum: '108' },
{ von: '11:30', bis: '13:00', fach: null, klasse: null, raum: null },
{ von: '14:00', bis: '15:30', fach: 'Deutsch', klasse: '12c', raum: '301' }
],
dienstag: [
{ von: '08:00', bis: '09:30', fach: 'Deutsch', klasse: '12c', raum: '301' },
{ von: '09:45', bis: '11:15', fach: null, klasse: null, raum: null },
{ von: '11:30', bis: '13:00', fach: 'Deutsch', klasse: '10a', raum: '204' }
],
mittwoch: [
{ von: '08:00', bis: '09:30', fach: null, klasse: null, raum: null },
{ von: '09:45', bis: '11:15', fach: 'Deutsch', klasse: '11b', raum: '108' },
{ von: '11:30', bis: '13:00', fach: 'Deutsch', klasse: '10a', raum: '204' }
],
donnerstag: [
{ von: '08:00', bis: '09:30', fach: 'Deutsch', klasse: '10a', raum: '204' },
{ von: '09:45', bis: '11:15', fach: 'Deutsch', klasse: '12c', raum: '301' },
{ von: '11:30', bis: '13:00', fach: null, klasse: null, raum: null }
],
freitag: [
{ von: '08:00', bis: '09:30', fach: 'Deutsch', klasse: '11b', raum: '108' },
{ von: '09:45', bis: '11:15', fach: null, klasse: null, raum: null }
]
};
}
function loadStundenplan() {
const stored = localStorage.getItem(STUNDENPLAN_STORAGE_KEY);
return stored ? JSON.parse(stored) : getDefaultStundenplan();
}
function saveStundenplan(plan) {
localStorage.setItem(STUNDENPLAN_STORAGE_KEY, JSON.stringify(plan));
}
function getTodayKey() {
const days = ['sonntag', 'montag', 'dienstag', 'mittwoch', 'donnerstag', 'freitag', 'samstag'];
return days[new Date().getDay()];
}
function formatDate() {
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
return new Date().toLocaleDateString('de-DE', options);
}
function isCurrentStunde(von, bis) {
const now = new Date();
const currentTime = now.getHours() * 60 + now.getMinutes();
const [vonH, vonM] = von.split(':').map(Number);
const [bisH, bisM] = bis.split(':').map(Number);
const vonTime = vonH * 60 + vonM;
const bisTime = bisH * 60 + bisM;
return currentTime >= vonTime && currentTime <= bisTime;
}
function renderStundenplan() {
const list = document.getElementById('stundenplan-list');
const dateEl = document.getElementById('stundenplan-date');
if (!list) return;
if (dateEl) {
dateEl.textContent = formatDate();
}
const plan = loadStundenplan();
const todayKey = getTodayKey();
if (todayKey === 'samstag' || todayKey === 'sonntag') {
list.innerHTML = `
<div class="stundenplan-empty">
<div class="stundenplan-empty-icon">&#127774;</div>
<div>Heute ist Wochenende!</div>
</div>
`;
return;
}
const todayPlan = plan[todayKey] || [];
if (todayPlan.length === 0) {
list.innerHTML = `
<div class="stundenplan-empty">
<div class="stundenplan-empty-icon">&#128197;</div>
<div>Kein Stundenplan fuer heute</div>
</div>
`;
return;
}
list.innerHTML = todayPlan.map(stunde => {
const isCurrent = isCurrentStunde(stunde.von, stunde.bis);
const isFrei = !stunde.fach;
return `
<div class="stunde-item ${isCurrent ? 'current' : ''} ${isFrei ? 'frei' : ''}">
<div class="stunde-zeit">
<div class="stunde-zeit-von">${stunde.von}</div>
<div class="stunde-zeit-bis">${stunde.bis}</div>
</div>
<div class="stunde-content">
<div class="stunde-fach">${isFrei ? 'Freistunde' : stunde.fach}</div>
${!isFrei ? `
<div class="stunde-details">
<span>&#127979; ${stunde.klasse}</span>
<span>&#128205; Raum ${stunde.raum}</span>
</div>
` : ''}
</div>
</div>
`;
}).join('');
}
function editStundenplan() {
// Open a modal or redirect to settings
alert('Stundenplan-Editor wird in einer zukuenftigen Version verfuegbar sein.\\n\\nVorlaeufig koennen Sie den Stundenplan in den Widget-Einstellungen anpassen.');
}
function initStundenplanWidget() {
renderStundenplan();
// Update every minute to highlight current lesson
setInterval(renderStundenplan, 60000);
}
"""

View File

@@ -0,0 +1,316 @@
"""
To-Do Widget fuer das Lehrer-Dashboard.
Zeigt eine interaktive To-Do-Liste mit localStorage-Persistierung.
"""
class TodosWidget:
widget_id = 'todos'
widget_name = 'To-Dos'
widget_icon = '&#10003;' # Checkmark
widget_color = '#10b981' # Green
default_width = 'half'
has_settings = True
@staticmethod
def get_css() -> str:
return """
/* ===== To-Do Widget Styles ===== */
.widget-todos {
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 12px;
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.widget-todos .widget-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
}
.widget-todos .widget-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
}
.widget-todos .widget-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(16, 185, 129, 0.15);
color: #10b981;
border-radius: 8px;
font-size: 14px;
}
.widget-todos .todo-list {
flex: 1;
overflow-y: auto;
margin-bottom: 12px;
}
.widget-todos .todo-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.05));
transition: background 0.2s;
}
.widget-todos .todo-item:last-child {
border-bottom: none;
}
.widget-todos .todo-item:hover {
background: var(--bp-surface-elevated, #334155);
margin: 0 -8px;
padding: 10px 8px;
border-radius: 6px;
}
.widget-todos .todo-checkbox {
width: 18px;
height: 18px;
border: 2px solid var(--bp-border, #475569);
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.widget-todos .todo-checkbox:hover {
border-color: #10b981;
}
.widget-todos .todo-checkbox.checked {
background: #10b981;
border-color: #10b981;
}
.widget-todos .todo-checkbox.checked::after {
content: '\\2713';
color: white;
font-size: 12px;
font-weight: bold;
}
.widget-todos .todo-text {
flex: 1;
font-size: 13px;
color: var(--bp-text, #e5e7eb);
line-height: 1.4;
}
.widget-todos .todo-item.completed .todo-text {
text-decoration: line-through;
color: var(--bp-text-muted, #9ca3af);
}
.widget-todos .todo-delete {
opacity: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
}
.widget-todos .todo-item:hover .todo-delete {
opacity: 1;
}
.widget-todos .todo-delete:hover {
background: rgba(239, 68, 68, 0.2);
}
.widget-todos .todo-add {
display: flex;
gap: 8px;
}
.widget-todos .todo-input {
flex: 1;
background: var(--bp-bg, #0f172a);
border: 1px solid var(--bp-border, #475569);
border-radius: 6px;
padding: 8px 12px;
font-size: 13px;
color: var(--bp-text, #e5e7eb);
outline: none;
transition: border-color 0.2s;
}
.widget-todos .todo-input:focus {
border-color: #10b981;
}
.widget-todos .todo-input::placeholder {
color: var(--bp-text-muted, #9ca3af);
}
.widget-todos .todo-add-btn {
background: #10b981;
color: white;
border: none;
border-radius: 6px;
padding: 8px 14px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.widget-todos .todo-add-btn:hover {
background: #059669;
}
.widget-todos .todo-empty {
text-align: center;
padding: 24px;
color: var(--bp-text-muted, #9ca3af);
font-size: 13px;
}
.widget-todos .todo-empty-icon {
font-size: 32px;
margin-bottom: 8px;
opacity: 0.5;
}
"""
@staticmethod
def get_html() -> str:
return """
<div class="widget-todos" data-widget-id="todos">
<div class="widget-header">
<div class="widget-title">
<span class="widget-icon">&#10003;</span>
<span>Meine To-Dos</span>
</div>
<button class="widget-settings-btn" onclick="openWidgetSettings('todos')" title="Einstellungen">&#9881;</button>
</div>
<div class="todo-list" id="todo-list">
<div class="todo-empty">
<div class="todo-empty-icon">&#128221;</div>
<div>Keine Aufgaben vorhanden</div>
</div>
</div>
<div class="todo-add">
<input type="text" class="todo-input" id="todo-input" placeholder="Neue Aufgabe..." onkeypress="if(event.key==='Enter')addTodo()">
<button class="todo-add-btn" onclick="addTodo()">+ Hinzufuegen</button>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
return """
// ===== To-Do Widget JavaScript =====
const TODOS_STORAGE_KEY = 'bp-lehrer-todos';
function loadTodos() {
const stored = localStorage.getItem(TODOS_STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
}
function saveTodos(todos) {
localStorage.setItem(TODOS_STORAGE_KEY, JSON.stringify(todos));
}
function renderTodos() {
const todoList = document.getElementById('todo-list');
if (!todoList) return;
const todos = loadTodos();
if (todos.length === 0) {
todoList.innerHTML = `
<div class="todo-empty">
<div class="todo-empty-icon">&#128221;</div>
<div>Keine Aufgaben vorhanden</div>
</div>
`;
return;
}
todoList.innerHTML = todos.map((todo, index) => `
<div class="todo-item ${todo.completed ? 'completed' : ''}" data-index="${index}">
<div class="todo-checkbox ${todo.completed ? 'checked' : ''}" onclick="toggleTodo(${index})"></div>
<span class="todo-text">${escapeHtml(todo.text)}</span>
<button class="todo-delete" onclick="deleteTodo(${index})" title="Loeschen">&#10005;</button>
</div>
`).join('');
}
function addTodo() {
const input = document.getElementById('todo-input');
if (!input) return;
const text = input.value.trim();
if (!text) return;
const todos = loadTodos();
todos.unshift({
id: Date.now(),
text: text,
completed: false,
createdAt: new Date().toISOString()
});
saveTodos(todos);
renderTodos();
input.value = '';
}
function toggleTodo(index) {
const todos = loadTodos();
if (todos[index]) {
todos[index].completed = !todos[index].completed;
saveTodos(todos);
renderTodos();
}
}
function deleteTodo(index) {
const todos = loadTodos();
todos.splice(index, 1);
saveTodos(todos);
renderTodos();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Initialize todos when widget is rendered
function initTodosWidget() {
renderTodos();
}
"""