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:
+591
-117
@@ -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">☰</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 (1–6)" 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 & 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>
|
||||
|
||||
Reference in New Issue
Block a user