feat: add teacher system with approval flow

- Teacher registration requires subject selection; account starts pending
- Admin approves/rejects via existing admin panel
- Teacher panel (Materialien, Ankündigungen, Prüfungen, Noten) visible only to approved teachers
- Students see class materials and announcements via sidebar overlays
- Teachers can assign grades to students (scoped to own subject)
- New tables: teacher_materials, teacher_announcements, teacher_exams, teacher_assigned_grades
- subject column added to users; included in JWT and /api/me
- requireTeacher middleware fetches fresh status+subject from DB on every request
- Login hint: username is the part of the school email before the @
This commit is contained in:
Simon
2026-04-17 09:28:55 +02:00
parent db5efd8ed9
commit ae789318ba
7 changed files with 965 additions and 130 deletions
+591 -117
View File
@@ -793,60 +793,135 @@ footer {
.col-primary > *:not(#card-tt) { display: none !important; }
}
/* ── SIDEBAR ─────────────────────────────────────────────── */
/* ── PAGE LAYOUT ─────────────────────────────────────────────── */
.sidebar-backdrop {
position: fixed; inset: 0;
background: rgba(0,0,0,.32);
z-index: 150;
opacity: 0; pointer-events: none;
transition: opacity .2s;
.page-body {
display: flex;
flex: 1;
align-items: stretch;
min-height: 0;
}
.sidebar-backdrop.open { opacity: 1; pointer-events: all; }
.sidebar {
position: fixed; top: 0; right: 0;
width: 360px; max-width: 100vw; height: 100%;
background: var(--bg);
border-left: 1px solid var(--border);
box-shadow: var(--shadow-lg);
z-index: 160;
transform: translateX(100%);
transition: transform .22s cubic-bezier(.4,0,.2,1);
display: flex; flex-direction: column;
overflow: hidden;
}
.sidebar.open { transform: translateX(0); }
/* ── LEFT SIDEBAR ─────────────────────────────────────────────── */
.sidebar-head {
height: 54px;
padding: 0 16px;
border-bottom: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between; gap: 10px;
background: var(--surface);
.app-sidebar {
width: 296px;
flex-shrink: 0;
border-right: 1px solid var(--border);
background: var(--surface);
position: sticky;
top: 54px;
height: calc(100vh - 54px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--n-200) transparent;
display: flex;
flex-direction: column;
z-index: 10;
}
.sidebar-title { font-size: 13px; font-weight: 600; color: var(--text); }
.sidebar-close {
width: 28px; height: 28px;
.app-sidebar-inner {
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
}
.sb-nav-item {
display: flex; align-items: center; gap: 9px;
width: 100%; padding: 9px 10px;
border: none; background: none; cursor: pointer;
border-radius: var(--r-sm);
font-size: 13px; font-weight: 500; color: var(--text);
font-family: inherit; text-align: left;
transition: background .12s, color .12s;
}
.sb-nav-item:hover { background: var(--n-100); color: var(--blue); }
.sb-nav-item .lucide { width: 15px; height: 15px; color: var(--text-muted); flex-shrink: 0; }
.sb-nav-item:hover .lucide { color: var(--blue); }
.sb-nav-arrow { margin-left: auto; }
/* Overlay panel */
.overlay-panel {
position: fixed !important; inset: 16px !important;
z-index: 260; border-radius: var(--r-xl) !important;
overflow: hidden !important; box-shadow: var(--shadow-xl) !important;
flex-direction: column;
}
.overlay-panel .card-body {
overflow-y: auto;
flex: 1;
}
.sb-group-label {
font-size: 10px;
font-weight: 700;
letter-spacing: .6px;
text-transform: uppercase;
color: var(--text-subtle);
padding: 6px 6px 2px;
margin-top: 2px;
}
.sb-group-label:first-child { margin-top: 0; padding-top: 2px; }
/* Expand-to-fullscreen button */
.card-expand-btn {
width: 24px; height: 24px;
border: none; background: none; cursor: pointer;
border-radius: var(--r-sm);
display: flex; align-items: center; justify-content: center;
font-size: 18px; line-height: 1; color: var(--text-muted);
transition: background .1s; font-family: inherit;
color: var(--text-subtle);
transition: background .1s, color .1s;
flex-shrink: 0;
padding: 0;
}
.sidebar-close:hover { background: var(--n-100); color: var(--text); }
.card-expand-btn:hover { background: var(--n-100); color: var(--text); }
.card-expand-btn .lucide { width: 13px; height: 13px; }
.sidebar-body {
flex: 1; overflow-y: auto;
padding: 14px;
display: flex; flex-direction: column; gap: 12px;
scrollbar-width: thin;
scrollbar-color: var(--n-200) transparent;
/* Fullscreen card overlay */
.card.fullscreen {
position: fixed !important;
inset: 16px !important;
z-index: 260;
border-radius: var(--r-xl) !important;
overflow: auto !important;
box-shadow: var(--shadow-xl) !important;
display: flex !important;
flex-direction: column;
max-width: none !important;
}
.card.fullscreen .card-body {
flex: 1;
overflow-y: auto;
max-width: 860px;
width: 100%;
margin: 0 auto;
}
.card.fullscreen .chat-msgs { height: auto; min-height: 300px; flex: 1; }
/* burger button badge dot (optional active state) */
#sidebar-btn { position: relative; }
/* Fullscreen backdrop */
#fs-backdrop {
position: fixed; inset: 0;
background: rgba(0,0,0,.45);
z-index: 259;
opacity: 0; pointer-events: none;
transition: opacity .2s;
}
#fs-backdrop.open { opacity: 1; pointer-events: all; }
/* Sidebar overlay (small screens) */
.sidebar-backdrop {
position: fixed; inset: 0;
background: rgba(0,0,0,.32);
z-index: 140;
opacity: 0; pointer-events: none;
transition: opacity .2s;
display: none;
}
.sidebar-backdrop.open { opacity: 1; pointer-events: all; }
#sidebar-btn { position: relative; font-size: 18px; }
/* ── RESPONSIVE ──────────────────────────────────────────── */
@@ -866,7 +941,29 @@ footer {
.thread-reply-row textarea { flex:1; border:1.5px solid var(--border); border-radius:var(--r-sm); padding:8px 10px; font-size:13px; font-family:inherit; resize:none; outline:none; background:var(--surface); color:var(--text); transition:border-color .12s; }
.thread-reply-row textarea:focus { border-color:var(--blue); }
@media (max-width: 860px) {
/* Large: sidebar always visible, no burger */
@media (min-width: 1101px) {
#sidebar-btn { display: none !important; }
.sidebar-backdrop { display: none !important; }
}
/* Small: sidebar becomes overlay, burger appears */
@media (max-width: 1100px) {
.app-sidebar {
position: fixed;
top: 54px; left: 0;
height: calc(100vh - 54px);
transform: translateX(-100%);
transition: transform .22s cubic-bezier(.4,0,.2,1);
box-shadow: var(--shadow-lg);
z-index: 150;
}
.app-sidebar.open { transform: translateX(0); }
.sidebar-backdrop { display: block; }
#sidebar-btn { display: flex !important; }
}
@media (max-width: 900px) {
.main-grid { grid-template-columns: 1fr; }
}
@media (max-width: 600px) {
@@ -881,6 +978,15 @@ footer {
footer { padding: 11px 14px; }
}
/* ── TEACHER TABS ────────────────────────────────────────── */
.t-tabs { display:flex; border-bottom:1px solid var(--border); margin-bottom:12px; gap:0; overflow-x:auto; }
.t-tab { padding:8px 14px; font-size:12px; font-weight:600; color:var(--text-muted); cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-1px; transition:color .12s, border-color .12s; white-space:nowrap; flex-shrink:0; }
.t-tab.active { color:var(--blue); border-bottom-color:var(--blue); }
.t-pane { display:none; }
.t-pane.active { display:block; }
.t-input { width:100%; padding:7px 10px; border:1px solid var(--border); border-radius:var(--r-sm); font-size:13px; font-family:inherit; background:var(--surface); color:var(--text); outline:none; transition:border-color .12s; }
.t-input:focus { border-color:var(--blue); }
/* ── ICONS ───────────────────────────────────────────────── */
.lucide {
display: inline-block;
@@ -908,6 +1014,8 @@ footer {
<body>
<header>
<button id="sidebar-btn" class="h-icon-btn" onclick="openSidebar()" title="Menü" aria-label="Seitenleiste öffnen">&#9776;</button>
<div class="brand" onclick="location.href='/'">
<div class="brand-mark">i1</div>
<div class="brand-text">
@@ -922,8 +1030,7 @@ footer {
<div class="h-right">
<div id="weather"></div>
<div id="clock"></div>
<button id="btn-dark" class="h-icon-btn" onclick="toggleDark()" title="Dark Mode"><i data-lucide="moon" aria-label="Dark Mode"></i></button>
<button id="sidebar-btn" class="h-icon-btn" style="display:none" onclick="openSidebar()" title="Noten, Dateien & Fehlzeiten"><i data-lucide="menu" aria-label="Seitenleiste öffnen"></i></button>
<button id="btn-dark" class="h-icon-btn" onclick="toggleDark()" title="Dark Mode"><i data-lucide="moon"></i></button>
<div id="h-user" style="display:flex;align-items:center;gap:7px">
<a href="/login" class="h-btn">Anmelden</a>
<a href="/login?tab=register" class="h-btn h-btn-primary">Registrieren</a>
@@ -945,6 +1052,200 @@ footer {
<button class="banner-cta" onclick="location.href='/login?tab=register'">Jetzt registrieren →</button>
</div>
<div class="page-body">
<aside class="app-sidebar" id="app-sidebar">
<div class="app-sidebar-inner">
<div class="sb-group-label">Persönlich</div>
<button class="sb-nav-item" id="sb-nav-grades" style="display:none" onclick="openOverlay('card-grades');closeSidebar()">
<i data-lucide="graduation-cap"></i><span>Noten</span><i data-lucide="chevron-right" class="sb-nav-arrow"></i>
</button>
<button class="sb-nav-item" id="sb-nav-files" style="display:none" onclick="openOverlay('card-files');closeSidebar()">
<i data-lucide="folder"></i><span>Dateispeicher</span><i data-lucide="chevron-right" class="sb-nav-arrow"></i>
</button>
<button class="sb-nav-item" id="sb-nav-ab" style="display:none" onclick="openOverlay('card-ab');closeSidebar()">
<i data-lucide="user-x"></i><span>Fehlzeiten</span><i data-lucide="chevron-right" class="sb-nav-arrow"></i>
</button>
<div class="sb-group-label" id="sb-label-komm" style="display:none">Kommunikation</div>
<button class="sb-nav-item" id="sb-nav-chat" style="display:none" onclick="openOverlay('card-chat');closeSidebar()">
<i data-lucide="message-square"></i><span>Klassen-Chat</span><i data-lucide="chevron-right" class="sb-nav-arrow"></i>
</button>
<button class="sb-nav-item" id="sb-nav-tickets" style="display:none" onclick="openOverlay('card-tickets');closeSidebar()">
<i data-lucide="ticket"></i><span>Support-Tickets</span><i data-lucide="chevron-right" class="sb-nav-arrow"></i>
</button>
<div class="sb-group-label" id="sb-label-klasse" style="display:none">Klasse</div>
<button class="sb-nav-item" id="sb-nav-materials" style="display:none" onclick="openOverlay('card-materials');loadStudentMaterials();closeSidebar()">
<i data-lucide="book-open"></i><span>Materialien</span><i data-lucide="chevron-right" class="sb-nav-arrow"></i>
</button>
<button class="sb-nav-item" id="sb-nav-announcements" style="display:none" onclick="openOverlay('card-announcements');loadStudentAnnouncements();closeSidebar()">
<i data-lucide="megaphone"></i><span>Ankündigungen</span><i data-lucide="chevron-right" class="sb-nav-arrow"></i>
</button>
<div class="sb-group-label" id="sb-label-lehrer" style="display:none">Lehrer</div>
<button class="sb-nav-item" id="sb-nav-teacher" style="display:none" onclick="openOverlay('card-teacher');loadTeacherPanel();closeSidebar()">
<i data-lucide="user-cog"></i><span>Lehrer-Panel</span><i data-lucide="chevron-right" class="sb-nav-arrow"></i>
</button>
</div>
</aside>
<!-- Overlay cards hidden until openOverlay() called -->
<input type="file" id="file-input" style="display:none" multiple onchange="uploadFiles(event)">
<div class="card ov-card" id="card-grades" style="display:none">
<div class="card-head">
<div class="card-title"><i data-lucide="graduation-cap" aria-hidden="true"></i> Noten</div>
<div class="card-actions">
<button class="add-btn" onclick="openModal('grades')">+ Note</button>
<button class="card-expand-btn" onclick="closeOverlay()" title="Schließen"><i data-lucide="x"></i></button>
</div>
</div>
<div class="card-body" id="list-grades"></div>
</div>
<div class="card ov-card" id="card-files" style="display:none">
<div class="card-head">
<div class="card-title"><i data-lucide="folder" aria-hidden="true"></i> Dateispeicher</div>
<div class="card-actions">
<button class="add-btn" onclick="document.getElementById('file-input').click()">↑ Hochladen</button>
<button class="card-expand-btn" onclick="closeOverlay()" title="Schließen"><i data-lucide="x"></i></button>
</div>
</div>
<div class="card-body" id="files-body">
<div class="upload-drop" id="files-drop">Dateien hier ablegen oder oben hochladen</div>
<div class="sk-line sk"></div>
</div>
</div>
<div class="card ov-card" id="card-ab" style="display:none">
<div class="card-head">
<div class="card-title"><i data-lucide="user-x" aria-hidden="true"></i> Fehlzeiten</div>
<div class="card-actions">
<button class="add-btn" onclick="openModal('absences')">+ Eintragen</button>
<button class="card-expand-btn" onclick="closeOverlay()" title="Schließen"><i data-lucide="x"></i></button>
</div>
</div>
<div class="card-body" id="list-ab"></div>
</div>
<div class="card ov-card" id="card-chat" style="display:none">
<div class="card-head">
<div class="card-title"><i data-lucide="message-square" aria-hidden="true"></i> Klassen-Chat</div>
<div class="card-actions">
<button class="card-expand-btn" onclick="closeOverlay()" title="Schließen"><i data-lucide="x"></i></button>
</div>
</div>
<div class="card-body" style="display:flex;flex-direction:column;flex:1">
<div class="chat-msgs" id="chat-msgs" style="flex:1;overflow-y:auto"></div>
<div class="chat-input-row">
<input class="chat-input" id="chat-input" type="text" placeholder="Nachricht schreiben…" maxlength="500" autocomplete="off">
<button class="add-btn" onclick="sendChatMsg()">Senden</button>
</div>
</div>
</div>
<div class="card ov-card" id="card-tickets" style="display:none">
<div class="card-head">
<div class="card-title"><i data-lucide="ticket" aria-hidden="true"></i> Support-Tickets</div>
<div class="card-actions">
<button class="add-btn" onclick="openNewTicket()">+ Ticket</button>
<button class="card-expand-btn" onclick="closeOverlay()" title="Schließen"><i data-lucide="x"></i></button>
</div>
</div>
<div class="card-body" id="list-tickets"><div class="empty">Keine Tickets</div></div>
</div>
<!-- STUDENT MATERIALS OVERLAY -->
<div class="card ov-card" id="card-materials" style="display:none">
<div class="card-head">
<div class="card-title"><i data-lucide="book-open" aria-hidden="true"></i> Materialien</div>
<div class="card-actions">
<button class="card-expand-btn" onclick="closeOverlay()" title="Schließen"><i data-lucide="x"></i></button>
</div>
</div>
<div class="card-body" id="list-student-materials"><div class="sk-line sk"></div></div>
</div>
<!-- STUDENT ANNOUNCEMENTS OVERLAY -->
<div class="card ov-card" id="card-announcements" style="display:none">
<div class="card-head">
<div class="card-title"><i data-lucide="megaphone" aria-hidden="true"></i> Ankündigungen</div>
<div class="card-actions">
<button class="card-expand-btn" onclick="closeOverlay()" title="Schließen"><i data-lucide="x"></i></button>
</div>
</div>
<div class="card-body" id="list-student-announcements"><div class="sk-line sk"></div></div>
</div>
<!-- TEACHER PANEL OVERLAY -->
<input type="file" id="teacher-file-input" style="display:none" onchange="uploadTeacherMaterial(event)">
<div class="card ov-card" id="card-teacher" style="display:none">
<div class="card-head">
<div class="card-title"><i data-lucide="user-cog" aria-hidden="true"></i> Lehrer-Panel</div>
<div class="card-actions">
<button class="card-expand-btn" onclick="closeOverlay()" title="Schließen"><i data-lucide="x"></i></button>
</div>
</div>
<div class="card-body">
<div class="t-tabs">
<div class="t-tab active" onclick="teacherTab('materials')">Materialien</div>
<div class="t-tab" onclick="teacherTab('announcements')">Ankündigungen</div>
<div class="t-tab" onclick="teacherTab('exams')">Prüfungen</div>
<div class="t-tab" onclick="teacherTab('grades')">Noten</div>
</div>
<div id="t-pane-materials" class="t-pane active">
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap">
<input type="text" id="mat-title" class="t-input" placeholder="Titel (max. 200 Zeichen)" maxlength="200" style="flex:1;min-width:160px">
<button class="add-btn" onclick="document.getElementById('teacher-file-input').click()">↑ Datei hochladen</button>
</div>
<div id="list-teacher-materials"><div class="empty">Keine Materialien hochgeladen</div></div>
</div>
<div id="t-pane-announcements" class="t-pane">
<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:12px">
<input type="text" id="ann-title" class="t-input" placeholder="Titel (max. 200 Zeichen)" maxlength="200">
<textarea id="ann-content" class="t-input" placeholder="Inhalt (max. 5000 Zeichen)" maxlength="5000" rows="3" style="resize:vertical"></textarea>
<button class="add-btn" style="align-self:flex-start" onclick="createAnnouncement()">+ Ankündigung</button>
</div>
<div id="list-teacher-announcements"><div class="empty">Keine Ankündigungen</div></div>
</div>
<div id="t-pane-exams" class="t-pane">
<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:12px">
<input type="text" id="exam-title" class="t-input" placeholder="Titel (max. 200 Zeichen)" maxlength="200">
<div style="display:flex;gap:8px;flex-wrap:wrap">
<input type="date" id="exam-date" class="t-input" style="flex:1;min-width:140px">
<input type="text" id="exam-desc" class="t-input" placeholder="Beschreibung (optional)" maxlength="1000" style="flex:2;min-width:160px">
</div>
<button class="add-btn" style="align-self:flex-start" onclick="createExam()">+ Prüfung anlegen</button>
</div>
<div id="list-teacher-exams"><div class="empty">Keine Prüfungen angelegt</div></div>
</div>
<div id="t-pane-grades" class="t-pane">
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;align-items:flex-end">
<select id="grade-student" class="t-input" style="flex:2;min-width:140px">
<option value="">Schüler auswählen…</option>
</select>
<select id="grade-type" class="t-input" style="flex:1;min-width:120px">
<option value="schulaufgabe">Schulaufgabe</option>
<option value="kurzarbeit">Kurzarbeit</option>
<option value="stegreifaufgabe">Stegreifaufgabe</option>
<option value="muendlich">Mündlich</option>
<option value="sonstiges">Sonstiges</option>
</select>
<input type="number" id="grade-val" class="t-input" min="1" max="6" step="0.5" placeholder="Note (16)" style="flex:1;min-width:100px">
<input type="text" id="grade-note" class="t-input" placeholder="Anmerkung (optional)" maxlength="500" style="flex:2;min-width:140px">
<button class="add-btn" onclick="assignGrade()">Note vergeben</button>
</div>
<div id="list-teacher-grades"><div class="empty">Keine Noten vergeben</div></div>
</div>
</div>
</div>
<main>
<div class="main-grid">
@@ -1048,72 +1349,11 @@ footer {
</div><!-- /col-secondary -->
</div><!-- /main-grid -->
<!-- Chat (full width) -->
<div class="card" id="card-chat" style="display:none">
<div class="card-head">
<div class="card-title"><i data-lucide="message-square" aria-hidden="true"></i> Klassen-Chat · INFO1</div>
</div>
<div class="card-body">
<div class="chat-msgs" id="chat-msgs"></div>
<div class="chat-input-row">
<input class="chat-input" id="chat-input" type="text" placeholder="Nachricht schreiben…" maxlength="500" autocomplete="off">
<button class="add-btn" onclick="sendChatMsg()">Senden</button>
</div>
</div>
</div>
<!-- Support Tickets (full width, logged-in) -->
<div class="card" id="card-tickets" style="display:none">
<div class="card-head">
<div class="card-title"><span class="ci">🎫</span> Support-Tickets</div>
<button class="add-btn" onclick="openNewTicket()">+ Ticket</button>
</div>
<div class="card-body" id="list-tickets"><div class="empty">Keine Tickets</div></div>
</div>
</main>
</div><!-- /page-body -->
<!-- ── SIDEBAR ──────────────────────────────────────────── -->
<div class="sidebar-backdrop" id="sidebar-backdrop" onclick="closeSidebar()"></div>
<aside class="sidebar" id="sidebar">
<div class="sidebar-head">
<span class="sidebar-title">Noten, Dateien &amp; Fehlzeiten</span>
<button class="sidebar-close" onclick="closeSidebar()" aria-label="Schließen"><i data-lucide="x" aria-hidden="true"></i></button>
</div>
<div class="sidebar-body">
<div class="card" id="card-grades" style="display:none">
<div class="card-head">
<div class="card-title"><i data-lucide="graduation-cap" aria-hidden="true"></i> Noten</div>
<button class="add-btn" onclick="openModal('grades')">+ Note</button>
</div>
<div class="card-body" id="list-grades"></div>
</div>
<div class="card" id="card-files" style="display:none">
<div class="card-head">
<div class="card-title"><i data-lucide="folder" aria-hidden="true"></i> Dateispeicher</div>
<div class="card-actions">
<input type="file" id="file-input" style="display:none" multiple onchange="uploadFiles(event)">
<button class="add-btn" onclick="document.getElementById('file-input').click()">↑ Hochladen</button>
</div>
</div>
<div class="card-body" id="files-body">
<div class="upload-drop" id="files-drop">Dateien hier ablegen oder oben hochladen</div>
<div class="sk-line sk"></div>
</div>
</div>
<div class="card" id="card-ab" style="display:none">
<div class="card-head">
<div class="card-title"><i data-lucide="user-x" aria-hidden="true"></i> Fehlzeiten</div>
<button class="add-btn" onclick="openModal('absences')">+ Eintragen</button>
</div>
<div class="card-body" id="list-ab"></div>
</div>
</div>
</aside>
<div id="fs-backdrop" onclick="closeOverlay()"></div>
<footer>
<div class="footer-left">Daten auf <strong>Hetzner-Server, Nürnberg, Deutschland</strong> · EU-DSGVO konform</div>
@@ -1290,27 +1530,39 @@ async function init(){
loadClassEvents();
const r=await fetch('/api/me');
if(r.ok){ const d=await r.json(); loginUI(d.username,d.id,d.role); }
if(r.ok){ const d=await r.json(); loginUI(d.username,d.id,d.role,d.subject); }
}
function loginUI(username,id,role){
function loginUI(username,id,role,subject){
document.getElementById('banner').style.display='none';
document.getElementById('card-tt-public').style.display='none';
document.getElementById('card-tt').style.display='';
document.getElementById('pair-hw-todo').style.display='grid';
document.getElementById('sidebar-btn').style.display='flex';
document.getElementById('card-tickets').style.display='';
['card-hw','card-grades','card-ab','card-todo','card-cd','card-files'].forEach(id=>document.getElementById(id).style.display='');
document.getElementById('sb-label-komm').style.display='';
['sb-nav-grades','sb-nav-files','sb-nav-ab','sb-nav-chat','sb-nav-tickets'].forEach(id=>document.getElementById(id).style.display='flex');
['card-hw','card-todo','card-cd'].forEach(id=>document.getElementById(id).style.display='');
['btn-add-pruefung','btn-add-ferien','btn-add-ql'].forEach(id=>document.getElementById(id).style.display='');
// Class materials and announcements visible to all logged-in users
document.getElementById('sb-label-klasse').style.display='';
['sb-nav-materials','sb-nav-announcements'].forEach(id=>document.getElementById(id).style.display='flex');
// Teacher-only panel
if(role==='teacher'){
document.getElementById('sb-label-lehrer').style.display='';
document.getElementById('sb-nav-teacher').style.display='flex';
}
const adminLink = role === 'admin' ? `<div class="dd-sep"></div><a class="dd-item" href="/admin">🛡️ Admin</a>` : '';
const adminBtn = role === 'admin' ? `<a href="/admin" class="btn-sm" style="background:#fef3c7;color:#92400e;border-color:#fde68a;font-weight:700">🛡️ Admin</a>` : '';
const subjectBadge = (role==='teacher'&&subject) ? `<span class="dd-item meta" style="color:var(--text-muted);font-size:11px">${esc(subject)}</span>` : '';
document.getElementById('h-user').innerHTML=`
${adminBtn}
<div class="avatar" onclick="toggleDropdown(this)">
${esc(username[0].toUpperCase())}
<div class="dropdown" id="user-dropdown">
<div class="dd-item meta">${esc(username)}</div>
${subjectBadge}
<div class="dd-sep"></div>
<span class="dd-item" onclick="openSettings()">⚙️ Einstellungen</span>
${adminLink}
@@ -1707,7 +1959,6 @@ async function delChatMsg(id) {
function initChat(username) {
chatMyUsername = username;
document.getElementById('card-chat').style.display = '';
loadChat().then(() => pollChat());
document.getElementById('chat-input').addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChatMsg(); }
@@ -1943,23 +2194,246 @@ if(localStorage.getItem('dark')){
document.getElementById('btn-dark').textContent='☀️';
}
// ── SIDEBAR ───────────────────────────────────────────────────
// ── SIDEBAR (small screens overlay) ──────────────────────────
function openSidebar(){
document.getElementById('sidebar').classList.add('open');
document.getElementById('app-sidebar').classList.add('open');
document.getElementById('sidebar-backdrop').classList.add('open');
document.body.style.overflow='hidden';
}
function closeSidebar(){
document.getElementById('sidebar').classList.remove('open');
document.getElementById('app-sidebar').classList.remove('open');
document.getElementById('sidebar-backdrop').classList.remove('open');
document.body.style.overflow='';
}
// ── OVERLAY CARDS ─────────────────────────────────────────────
let _activeOverlay = null;
function openOverlay(id){
closeOverlay();
const card = document.getElementById(id);
if(!card) return;
card.style.display = 'flex';
card.classList.add('overlay-panel');
document.getElementById('fs-backdrop').classList.add('open');
document.body.style.overflow='hidden';
_activeOverlay = id;
lucide.createIcons();
}
function closeOverlay(){
if(_activeOverlay){
const card = document.getElementById(_activeOverlay);
if(card){ card.style.display='none'; card.classList.remove('overlay-panel'); }
_activeOverlay = null;
}
document.getElementById('fs-backdrop').classList.remove('open');
document.body.style.overflow='';
}
// kept for any legacy calls
function toggleFullscreen(id){ openOverlay(id); }
function collapseAllCards(){ closeOverlay(); }
// ── KEYBOARD ──────────────────────────────────────────────────
document.addEventListener('keydown',e=>{
if(e.key==='Escape'){closeModal();closeSettings();closeSidebar();closeThread();closeNewTicket();}
if(e.key==='Escape'){closeModal();closeSettings();closeSidebar();closeOverlay();closeThread();closeNewTicket();}
});
// ── TEACHER PANEL ─────────────────────────────────────────────
function teacherTab(name){
const names=['materials','announcements','exams','grades'];
document.querySelectorAll('.t-tab').forEach((t,i)=>t.classList.toggle('active',names[i]===name));
document.querySelectorAll('.t-pane').forEach(p=>p.classList.remove('active'));
document.getElementById('t-pane-'+name).classList.add('active');
if(name==='grades') loadStudentListForGrades();
}
async function loadTeacherPanel(){
loadTeacherMaterials();
loadTeacherAnnouncements();
loadTeacherExams();
loadTeacherGrades();
}
async function loadTeacherMaterials(){
const mats=await api('GET','teacher/materials');
const el=document.getElementById('list-teacher-materials');
if(mats.error||!mats.length){el.innerHTML='<div class="empty">Keine Materialien hochgeladen</div>';return;}
el.innerHTML=mats.map(m=>`<div class="file-item">
<div class="file-icon">${fileIcon(m.mime_type)}</div>
<div class="file-info">
<div class="file-name">${esc(m.title)} <span style="font-size:11px;color:var(--text-muted)">(${esc(m.original_name)})</span></div>
<div class="file-meta">${esc(m.subject)} · ${fmtBytes(m.size)} · ${fmtDate(m.created_at?m.created_at.slice(0,10):'')}</div>
</div>
<button class="file-dl" onclick="window.location.href='/api/teacher/materials/${m.id}/download'">↓ Laden</button>
<button class="del-btn" onclick="delTeacherMaterial(${m.id})">🗑</button>
</div>`).join('');
}
async function uploadTeacherMaterial(e){
const files=e.target.files;
if(!files.length)return;
const title=document.getElementById('mat-title').value.trim();
if(!title){toast('Bitte einen Titel eingeben','error');e.target.value='';return;}
const fd=new FormData();
fd.append('file',files[0]);
fd.append('title',title);
fd.append('class_id','info1');
e.target.value='';
const r=await fetch('/api/teacher/materials',{method:'POST',body:fd});
const d=await r.json();
if(!r.ok){toast(d.error||'Upload fehlgeschlagen','error');return;}
document.getElementById('mat-title').value='';
toast('Material hochgeladen');
loadTeacherMaterials();
}
async function delTeacherMaterial(id){
await api('DELETE','teacher/materials/'+id);
toast('Gelöscht');
loadTeacherMaterials();
}
async function loadTeacherAnnouncements(){
const anns=await api('GET','teacher/announcements');
const el=document.getElementById('list-teacher-announcements');
if(anns.error||!anns.length){el.innerHTML='<div class="empty">Keine Ankündigungen</div>';return;}
el.innerHTML=anns.map(a=>`<div class="hw-item">
<div class="hw-body">
<div class="hw-title">${esc(a.title)}</div>
<div class="hw-meta">${esc(a.subject)} · ${fmtDate(a.created_at?a.created_at.slice(0,10):'')}</div>
<div style="font-size:12px;color:var(--text-2);margin-top:4px;white-space:pre-wrap;word-break:break-word">${esc(a.content)}</div>
</div>
<button class="del-btn" onclick="delTeacherAnnouncement(${a.id})">🗑</button>
</div>`).join('');
}
async function createAnnouncement(){
const title=document.getElementById('ann-title').value.trim();
const content=document.getElementById('ann-content').value.trim();
if(!title||!content){toast('Titel und Inhalt erforderlich','error');return;}
const r=await api('POST','teacher/announcements',{title,content,class_id:'info1'});
if(r.error){toast(r.error,'error');return;}
document.getElementById('ann-title').value='';
document.getElementById('ann-content').value='';
toast('Ankündigung erstellt');
loadTeacherAnnouncements();
}
async function delTeacherAnnouncement(id){
await api('DELETE','teacher/announcements/'+id);
toast('Gelöscht');
loadTeacherAnnouncements();
}
async function loadTeacherExams(){
const exams=await api('GET','teacher/exams');
const el=document.getElementById('list-teacher-exams');
if(exams.error||!exams.length){el.innerHTML='<div class="empty">Keine Prüfungen angelegt</div>';return;}
el.innerHTML=exams.map(ex=>{
const diff=ex.date?daysUntil(ex.date):null;
return `<div class="ev-item">
<div class="ev-days">${diff===null?'':diff<0?'✓':diff}</div>
<div class="ev-info">
<div class="ev-title">${esc(ex.title)}</div>
<div class="ev-date">${esc(ex.subject)}${ex.date?' · '+fmtDate(ex.date):''}${ex.description?' · '+esc(ex.description):''}</div>
</div>
<button class="ev-del" onclick="delTeacherExam(${ex.id})">🗑</button>
</div>`;
}).join('');
}
async function createExam(){
const title=document.getElementById('exam-title').value.trim();
const date=document.getElementById('exam-date').value;
const description=document.getElementById('exam-desc').value.trim();
if(!title){toast('Titel erforderlich','error');return;}
const r=await api('POST','teacher/exams',{title,date:date||null,description:description||null,class_id:'info1'});
if(r.error){toast(r.error,'error');return;}
document.getElementById('exam-title').value='';
document.getElementById('exam-date').value='';
document.getElementById('exam-desc').value='';
toast('Prüfung angelegt');
loadTeacherExams();
}
async function delTeacherExam(id){
await api('DELETE','teacher/exams/'+id);
toast('Gelöscht');
loadTeacherExams();
}
async function loadTeacherGrades(){
const grades=await api('GET','teacher/grades');
const el=document.getElementById('list-teacher-grades');
if(grades.error||!grades.length){el.innerHTML='<div class="empty">Keine Noten vergeben</div>';return;}
el.innerHTML=grades.map(g=>`<div class="grade-row">
<div class="grade-info">
<div class="grade-subj">${esc(g.student_name)}</div>
<div class="grade-type">${GRADE_TYPES[g.type]||g.type}${g.note?' · '+esc(g.note):''}</div>
</div>
<div class="grade-val g${Math.min(Math.round(g.grade),6)}">${g.grade}</div>
<button class="del-btn" onclick="delTeacherGrade(${g.id})">🗑</button>
</div>`).join('');
}
async function loadStudentListForGrades(){
const students=await api('GET','teacher/students');
if(students.error)return;
const sel=document.getElementById('grade-student');
const cur=sel.value;
sel.innerHTML='<option value="">Schüler auswählen…</option>'+students.map(s=>`<option value="${s.id}">${esc(s.username)}</option>`).join('');
if(cur)sel.value=cur;
}
async function assignGrade(){
const student_id=parseInt(document.getElementById('grade-student').value,10);
const grade=parseFloat(document.getElementById('grade-val').value);
const type=document.getElementById('grade-type').value;
const note=document.getElementById('grade-note').value.trim();
if(!student_id){toast('Schüler auswählen','error');return;}
if(!grade||grade<1||grade>6){toast('Note muss zwischen 1 und 6 liegen','error');return;}
const r=await api('POST','teacher/grades',{student_id,grade,type,note:note||null});
if(r.error){toast(r.error,'error');return;}
document.getElementById('grade-val').value='';
document.getElementById('grade-note').value='';
toast('Note vergeben');
loadTeacherGrades();
}
async function delTeacherGrade(id){
await api('DELETE','teacher/grades/'+id);
toast('Gelöscht');
loadTeacherGrades();
}
// ── STUDENT CLASS CONTENT ──────────────────────────────────────
async function loadStudentMaterials(){
const mats=await api('GET','teacher/materials/class/info1');
const el=document.getElementById('list-student-materials');
if(!mats||mats.error||!mats.length){el.innerHTML='<div class="empty">Keine Materialien vorhanden</div>';return;}
el.innerHTML=mats.map(m=>`<div class="file-item">
<div class="file-icon">${fileIcon(m.mime_type)}</div>
<div class="file-info">
<div class="file-name">${esc(m.title)}</div>
<div class="file-meta">${esc(m.subject)} · ${esc(m.teacher_name)} · ${fmtBytes(m.size)} · ${fmtDate(m.created_at?m.created_at.slice(0,10):'')}</div>
</div>
<button class="file-dl" onclick="window.location.href='/api/teacher/materials/${m.id}/download'">↓ Laden</button>
</div>`).join('');
}
async function loadStudentAnnouncements(){
const anns=await api('GET','teacher/announcements/class/info1');
const el=document.getElementById('list-student-announcements');
if(!anns||anns.error||!anns.length){el.innerHTML='<div class="empty">Keine Ankündigungen</div>';return;}
el.innerHTML=anns.map(a=>`<div class="hw-item">
<div class="hw-body">
<div class="hw-title">${esc(a.title)}</div>
<div class="hw-meta">${esc(a.subject)} · ${esc(a.teacher_name)} · ${fmtDate(a.created_at?a.created_at.slice(0,10):'')}</div>
<div style="font-size:12px;color:var(--text-2);margin-top:4px;white-space:pre-wrap;word-break:break-word">${esc(a.content)}</div>
</div>
</div>`).join('');
}
// ── START ─────────────────────────────────────────────────────
init();
</script>