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:
50
backend/frontend/modules/widgets/__init__.py
Normal file
50
backend/frontend/modules/widgets/__init__.py
Normal 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',
|
||||
]
|
||||
272
backend/frontend/modules/widgets/alerts_widget.py
Normal file
272
backend/frontend/modules/widgets/alerts_widget.py
Normal 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 = '🔔' # 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">🔔</span>
|
||||
<span>Google Alerts</span>
|
||||
</div>
|
||||
<button class="widget-settings-btn" onclick="openWidgetSettings('alerts')" title="Einstellungen">⚙</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">🔔</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">📰</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();
|
||||
}
|
||||
"""
|
||||
341
backend/frontend/modules/widgets/arbeiten_widget.py
Normal file
341
backend/frontend/modules/widgets/arbeiten_widget.py
Normal 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 = '📝' # 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">📝</span>
|
||||
<span>Anstehende Arbeiten</span>
|
||||
</div>
|
||||
<button class="widget-settings-btn" onclick="openWidgetSettings('arbeiten')" title="Einstellungen">⚙</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: '📄',
|
||||
test: '📋',
|
||||
korrektur: '📝',
|
||||
abgabe: '📦'
|
||||
};
|
||||
return icons[typ] || '📄';
|
||||
}
|
||||
|
||||
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">🎉</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}">📅 ${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();
|
||||
}
|
||||
"""
|
||||
302
backend/frontend/modules/widgets/fehlzeiten_widget.py
Normal file
302
backend/frontend/modules/widgets/fehlzeiten_widget.py
Normal 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 = '⚠' # 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">⚠</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: '😷 Krank',
|
||||
entschuldigt: '✅ Entschuldigt',
|
||||
unentschuldigt: '❓ 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">✓</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();
|
||||
}
|
||||
"""
|
||||
313
backend/frontend/modules/widgets/kalender_widget.py
Normal file
313
backend/frontend/modules/widgets/kalender_widget.py
Normal 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 = '📆' # 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">📆</span>
|
||||
<span>Anstehende Termine</span>
|
||||
</div>
|
||||
<button class="widget-settings-btn" onclick="openWidgetSettings('kalender')" title="Einstellungen">⚙</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: '👥 Konferenz',
|
||||
elterngespraech: '👪 Eltern',
|
||||
fortbildung: '📚 Fortbildung',
|
||||
pruefung: '📝 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">📆</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>🕑 ${termin.zeit}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function addTermin() {
|
||||
alert('Termin-Editor wird in einer zukuenftigen Version verfuegbar sein.');
|
||||
}
|
||||
|
||||
function initKalenderWidget() {
|
||||
renderKalender();
|
||||
}
|
||||
"""
|
||||
263
backend/frontend/modules/widgets/klassen_widget.py
Normal file
263
backend/frontend/modules/widgets/klassen_widget.py
Normal 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 = '📊' # 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">📊</span>
|
||||
<span>Meine Klassen</span>
|
||||
</div>
|
||||
<button class="widget-settings-btn" onclick="openWidgetSettings('klassen')" title="Einstellungen">⚙</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">🏫</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 ? ' ⭐' : ''}</span>
|
||||
<span class="klasse-meta">${klasse.schueler} Schueler · ${klasse.fach}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="klasse-arrow">→</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();
|
||||
}
|
||||
"""
|
||||
289
backend/frontend/modules/widgets/matrix_widget.py
Normal file
289
backend/frontend/modules/widgets/matrix_widget.py
Normal 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 = '💬' # 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">💬</span>
|
||||
<span>Matrix-Chat</span>
|
||||
</div>
|
||||
<button class="widget-settings-btn" onclick="openWidgetSettings('matrix')" title="Einstellungen">⚙</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">💬</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">💬</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();
|
||||
}
|
||||
"""
|
||||
317
backend/frontend/modules/widgets/nachrichten_widget.py
Normal file
317
backend/frontend/modules/widgets/nachrichten_widget.py
Normal 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 = '📧' # 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">📧</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: '👪',
|
||||
schueler: '🧑',
|
||||
kollegium: '💼'
|
||||
};
|
||||
return icons[type] || '📧';
|
||||
}
|
||||
|
||||
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">📧</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();
|
||||
}
|
||||
"""
|
||||
182
backend/frontend/modules/widgets/notizen_widget.py
Normal file
182
backend/frontend/modules/widgets/notizen_widget.py
Normal 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 = '📋' # 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">📋</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);
|
||||
});
|
||||
}
|
||||
"""
|
||||
196
backend/frontend/modules/widgets/schnellzugriff_widget.py
Normal file
196
backend/frontend/modules/widgets/schnellzugriff_widget.py
Normal 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 = '⚡' # 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">⚡</span>
|
||||
<span>Schnellzugriff</span>
|
||||
</div>
|
||||
<button class="widget-settings-btn" onclick="openWidgetSettings('schnellzugriff')" title="Einstellungen">⚙</button>
|
||||
</div>
|
||||
<div class="quick-links">
|
||||
<div class="quick-link" data-module="worksheets" onclick="loadModule('worksheets')">
|
||||
<div class="quick-link-icon">📝</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">✓</div>
|
||||
<span class="quick-link-label">Klausur</span>
|
||||
</div>
|
||||
<div class="quick-link" data-module="letters" onclick="loadModule('letters')">
|
||||
<div class="quick-link-icon">✉</div>
|
||||
<span class="quick-link-label">Elternbrief</span>
|
||||
</div>
|
||||
<div class="quick-link" data-module="jitsi" onclick="loadModule('jitsi')">
|
||||
<div class="quick-link-icon">🎥</div>
|
||||
<span class="quick-link-label">Konferenz</span>
|
||||
</div>
|
||||
<div class="quick-link" data-module="school" onclick="loadModule('school')">
|
||||
<div class="quick-link-icon">🏫</div>
|
||||
<span class="quick-link-label">Schule</span>
|
||||
</div>
|
||||
<div class="quick-link" data-module="messenger" onclick="loadModule('messenger')">
|
||||
<div class="quick-link-icon">💬</div>
|
||||
<span class="quick-link-label">Messenger</span>
|
||||
</div>
|
||||
<div class="quick-link" data-module="companion" onclick="loadModule('companion')">
|
||||
<div class="quick-link-icon">📚</div>
|
||||
<span class="quick-link-label">Begleiter</span>
|
||||
</div>
|
||||
<div class="quick-link" data-module="correction" onclick="loadModule('correction')">
|
||||
<div class="quick-link-icon">📄</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');
|
||||
}
|
||||
"""
|
||||
311
backend/frontend/modules/widgets/statistik_widget.py
Normal file
311
backend/frontend/modules/widgets/statistik_widget.py
Normal 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 = '📈' # 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">📈</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">⚙</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">📈</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();
|
||||
}
|
||||
"""
|
||||
323
backend/frontend/modules/widgets/stundenplan_widget.py
Normal file
323
backend/frontend/modules/widgets/stundenplan_widget.py
Normal 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 = '📅' # 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">📅</span>
|
||||
<span>Stundenplan heute</span>
|
||||
</div>
|
||||
<button class="widget-settings-btn" onclick="openWidgetSettings('stundenplan')" title="Einstellungen">⚙</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()">📝 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">🌞</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">📅</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>🏫 ${stunde.klasse}</span>
|
||||
<span>📍 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);
|
||||
}
|
||||
"""
|
||||
316
backend/frontend/modules/widgets/todos_widget.py
Normal file
316
backend/frontend/modules/widgets/todos_widget.py
Normal 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 = '✓' # 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">✓</span>
|
||||
<span>Meine To-Dos</span>
|
||||
</div>
|
||||
<button class="widget-settings-btn" onclick="openWidgetSettings('todos')" title="Einstellungen">⚙</button>
|
||||
</div>
|
||||
<div class="todo-list" id="todo-list">
|
||||
<div class="todo-empty">
|
||||
<div class="todo-empty-icon">📝</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">📝</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">✕</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();
|
||||
}
|
||||
"""
|
||||
Reference in New Issue
Block a user