359 lines
14 KiB
HTML
359 lines
14 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Dozenten-Dashboard | IT-Praxisportal</title>
|
||
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
|
||
<link rel="stylesheet" href="assets/style.css">
|
||
<style>
|
||
.class-card {
|
||
background: #ffffff;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--radius-md);
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
.class-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
.invite-code-box {
|
||
background: #e0e7ff;
|
||
color: #3730a3;
|
||
padding: 6px 14px;
|
||
border-radius: var(--radius-sm);
|
||
font-family: var(--font-mono);
|
||
font-weight: 800;
|
||
font-size: 1.1rem;
|
||
letter-spacing: 1px;
|
||
}
|
||
.student-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-top: 14px;
|
||
font-size: 0.9rem;
|
||
}
|
||
.student-table th, .student-table td {
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
text-align: left;
|
||
}
|
||
.student-table th {
|
||
background: #f8fafc;
|
||
font-weight: 700;
|
||
color: var(--text-muted);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="app-layout">
|
||
|
||
<aside class="app-sidebar">
|
||
<div class="sidebar-header">
|
||
<a href="index.html" class="sidebar-brand-link">
|
||
<div class="sidebar-logo">🐍</div>
|
||
<div>
|
||
<div class="sidebar-title">Python Portal</div>
|
||
<div class="sidebar-subtitle">Lehrerbereich</div>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
|
||
<div class="sidebar-quick-nav">
|
||
<a href="index.html" class="sidebar-link">🏠 Schüler-Dashboard</a>
|
||
<a href="teacher.html" class="sidebar-link active">👨🏫 Klassenverwaltung</a>
|
||
<a href="index.html#zertifikate" class="sidebar-link">🏆 Zertifikate</a>
|
||
</div>
|
||
</aside>
|
||
|
||
<div class="app-main">
|
||
<header>
|
||
<div class="header-container">
|
||
<div class="header-badge">👨🏫 Dozenten- & Lehrer-Portal</div>
|
||
<h1>Klassenverwaltung & Lernfortschritts-Matrix</h1>
|
||
<p>Behalte den Überblick über deine Klassen, lade Schüler per Code ein, inspiziere ihren Code und exportiere Notenlisten als CSV.</p>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="container">
|
||
|
||
<!-- Neue Klasse erstellen -->
|
||
<section class="card">
|
||
<h2>✨ Neue Schulklasse / Kursgruppe anlegen</h2>
|
||
<form id="create-class-form" style="display: flex; gap: 12px; flex-wrap: wrap; margin-top: 14px;">
|
||
<input type="text" id="input-class-name" required placeholder="Klassenname (z.B. Informatik 10a 2026/27)" style="flex: 1; min-width: 260px; padding: 10px 14px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.95rem;">
|
||
<button type="submit" class="btn" style="padding: 10px 20px;">➕ Klasse erstellen</button>
|
||
</form>
|
||
</section>
|
||
|
||
<!-- Vorhandene Klassen -->
|
||
<section>
|
||
<div class="section-header">
|
||
<h2>📚 Deine aktiven Klassen</h2>
|
||
</div>
|
||
|
||
<div id="classrooms-list-container">
|
||
<div class="card" style="text-align: center; color: var(--text-muted); padding: 40px;">
|
||
Lade Klassenräume...
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Student Code Inspector Modal -->
|
||
<div class="certificate-overlay" id="code-inspector-modal" style="display: none;">
|
||
<div class="certificate-container" style="max-width: 720px; text-align: left; padding: 24px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||
<h3 id="inspector-title" style="margin: 0; color: var(--text-main);">🔍 Code-Inspektor</h3>
|
||
<button class="btn btn-secondary" style="padding: 4px 10px;" onclick="document.getElementById('code-inspector-modal').style.display='none'">✕</button>
|
||
</div>
|
||
<div id="inspector-meta" style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 10px;"></div>
|
||
<pre id="inspector-code-content" style="background: #0b0f19; color: #f8fafc; padding: 16px; border-radius: 6px; font-family: monospace; font-size: 0.88rem; max-height: 380px; overflow-y: auto; white-space: pre-wrap; margin: 0;"></pre>
|
||
<div style="text-align: right; margin-top: 14px;">
|
||
<button class="btn" onclick="document.getElementById('code-inspector-modal').style.display='none'">Schließen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="assets/app_auth.js"></script>
|
||
<script>
|
||
let ALL_CLASSROOMS = [];
|
||
let CURRENT_MATRIX = {};
|
||
|
||
function getAuthToken() {
|
||
return (window.AUTH && window.AUTH.token) || sessionStorage.getItem("auth_token") || localStorage.getItem("auth_token") || "";
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
if (!text) return "";
|
||
return String(text)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
async function loadClassrooms() {
|
||
const container = document.getElementById("classrooms-list-container");
|
||
const token = getAuthToken();
|
||
|
||
if (!token) {
|
||
container.innerHTML = `
|
||
<div class="card" style="text-align: center; padding: 30px;">
|
||
<p>Du musst als <strong>Lehrkraft</strong> angemeldet sein, um Klassen zu verwalten.</p>
|
||
<button class="btn" onclick="window.openAuthModal('login')">🔑 Als Lehrer anmelden</button>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch("/api/classrooms/list", {
|
||
headers: { Authorization: `Bearer ${token}` }
|
||
});
|
||
const data = await res.json();
|
||
const classrooms = data.classrooms || [];
|
||
ALL_CLASSROOMS = classrooms;
|
||
|
||
if (classrooms.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="card" style="text-align: center; padding: 30px; color: var(--text-muted);">
|
||
Noch keine Klassen angelegt. Erstelle oben deine erste Klasse!
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = classrooms.map(cl => `
|
||
<div class="class-card">
|
||
<div class="class-header">
|
||
<div>
|
||
<h3 style="margin: 0 0 4px 0; color: var(--text-main); font-size: 1.25rem;">${escapeHtml(cl.name)}</h3>
|
||
<span style="font-size: 0.85rem; color: var(--text-muted);">${cl.student_count || 0} Schüler eingeschrieben</span>
|
||
</div>
|
||
<div style="text-align: right;">
|
||
<div style="font-size: 0.72rem; color: var(--text-muted); margin-bottom: 2px;">EINLADUNGSCODE:</div>
|
||
<div class="invite-code-box">${escapeHtml(cl.invite_code)}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap;">
|
||
<button class="btn btn-secondary" style="padding: 6px 12px; font-size: 0.82rem;" onclick="loadClassMatrix(${cl.id})">📊 Lernmatrix anzeigen</button>
|
||
<button class="btn btn-secondary" style="padding: 6px 12px; font-size: 0.82rem;" onclick="exportClassCsv(${cl.id})">📥 CSV-Export</button>
|
||
<button class="btn btn-secondary" style="padding: 6px 12px; font-size: 0.82rem; color: var(--danger); border-color: var(--danger);" onclick="deleteClassroom(${cl.id})">🗑️ Löschen</button>
|
||
</div>
|
||
<div id="matrix-container-${cl.id}" style="display: none; margin-top: 14px;"></div>
|
||
</div>
|
||
`).join("");
|
||
|
||
} catch (err) {
|
||
container.innerHTML = `<div class="box box-danger">Fehler beim Laden: ${escapeHtml(err.message)}</div>`;
|
||
}
|
||
}
|
||
|
||
async function loadClassMatrix(classId) {
|
||
const target = document.getElementById(`matrix-container-${classId}`);
|
||
if (target.style.display === "block") {
|
||
target.style.display = "none";
|
||
return;
|
||
}
|
||
|
||
target.style.display = "block";
|
||
target.innerHTML = "Lade Schüler-Fortschritt...";
|
||
|
||
const token = getAuthToken();
|
||
try {
|
||
const res = await fetch(`/api/classrooms/matrix?classroom_id=${classId}`, {
|
||
headers: { Authorization: `Bearer ${token}` }
|
||
});
|
||
const data = await res.json();
|
||
const matrix = data.matrix || [];
|
||
CURRENT_MATRIX[classId] = matrix;
|
||
|
||
if (matrix.length === 0) {
|
||
target.innerHTML = "<p style='color: var(--text-muted); font-size: 0.88rem;'>Noch keine Schüler in dieser Klasse beigetreten.</p>";
|
||
return;
|
||
}
|
||
|
||
target.innerHTML = `
|
||
<table class="student-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Schüler</th>
|
||
<th>Level</th>
|
||
<th>XP</th>
|
||
<th>Streak</th>
|
||
<th>Gelöste Kapitel</th>
|
||
<th>Aktion</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${matrix.map(m => `
|
||
<tr>
|
||
<td><strong>${escapeHtml(m.student.name)}</strong><br><span style="font-size: 0.75rem; color: #64748b;">${escapeHtml(m.student.email)}</span></td>
|
||
<td>⭐ Level ${m.student.level}</td>
|
||
<td><span style="font-weight: 700; color: #0284c7;">${m.student.xp} XP</span></td>
|
||
<td>🔥 ${m.student.streak_days} Tage</td>
|
||
<td><span style="background: #ecfdf5; color: #059669; padding: 2px 8px; border-radius: 99px; font-weight: 700; font-size: 0.8rem;">${Object.values(m.solved_chapters).filter(Boolean).length} / 27</span></td>
|
||
<td><button class="btn btn-secondary" style="padding: 4px 8px; font-size: 0.75rem;" onclick="inspectStudentCode(${classId}, ${m.student.id})">🔍 Code ansehen</button></td>
|
||
</tr>
|
||
`).join("")}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
} catch (e) {
|
||
target.innerHTML = `<span style="color: var(--danger);">Fehler: ${escapeHtml(e.message)}</span>`;
|
||
}
|
||
}
|
||
|
||
async function inspectStudentCode(classId, studentId) {
|
||
const matrix = CURRENT_MATRIX[classId] || [];
|
||
const entry = matrix.find(m => m.student.id === studentId);
|
||
const studentName = entry ? entry.student.name : `#${studentId}`;
|
||
|
||
const chapterId = prompt("Welches Kapitel möchtest du inspizieren? (z.B. lehrpfad_1_grundlagen/01_erste_schritte_taschenrechner)", "lehrpfad_1_grundlagen/01_erste_schritte_taschenrechner");
|
||
if (!chapterId) return;
|
||
|
||
const token = getAuthToken();
|
||
try {
|
||
const res = await fetch(`/api/classrooms/student-code?classroom_id=${classId}&student_id=${studentId}&chapter_id=${encodeURIComponent(chapterId)}`, {
|
||
headers: { Authorization: `Bearer ${token}` }
|
||
});
|
||
const data = await res.json();
|
||
|
||
document.getElementById("inspector-title").innerText = `🔍 Code von ${studentName}`;
|
||
document.getElementById("inspector-meta").innerText = `Kapitel: ${chapterId} | Gelöst: ${data.progress && data.progress.is_solved ? '✅ Ja' : '❌ Nein'}`;
|
||
document.getElementById("inspector-code-content").innerText = (data.progress && data.progress.code_draft) ? data.progress.code_draft : "# Kein Code-Entwurf für dieses Kapitel vorhanden.";
|
||
document.getElementById("code-inspector-modal").style.display = "flex";
|
||
} catch (err) {
|
||
alert("Fehler beim Abrufen des Codes: " + err.message);
|
||
}
|
||
}
|
||
|
||
async function exportClassCsv(classId) {
|
||
const cl = ALL_CLASSROOMS.find(c => c.id === classId);
|
||
const className = cl ? cl.name : `Klasse_${classId}`;
|
||
const token = getAuthToken();
|
||
try {
|
||
const res = await fetch(`/api/classrooms/matrix?classroom_id=${classId}`, {
|
||
headers: { Authorization: `Bearer ${token}` }
|
||
});
|
||
const data = await res.json();
|
||
const matrix = data.matrix || [];
|
||
|
||
let csv = "Name,E-Mail,Level,XP,Streak,Geloeste_Kapitel_Anzahl\n";
|
||
matrix.forEach(m => {
|
||
const solved = Object.values(m.solved_chapters).filter(Boolean).length;
|
||
csv += `"${m.student.name.replace(/"/g, '""')}","${m.student.email.replace(/"/g, '""')}",${m.student.level},${m.student.xp},${m.student.streak_days},${solved}\n`;
|
||
});
|
||
|
||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
a.download = `Notenmatrix_${className.replace(/\s+/g, "_")}.csv`;
|
||
a.click();
|
||
} catch (e) {
|
||
alert("CSV-Export fehlgeschlagen: " + e.message);
|
||
}
|
||
}
|
||
|
||
async function deleteClassroom(classId) {
|
||
const cl = ALL_CLASSROOMS.find(c => c.id === classId);
|
||
const className = cl ? cl.name : `Klasse_${classId}`;
|
||
if (!confirm(`Möchtest du die Klasse '${className}' wirklich unwiderruflich löschen?`)) return;
|
||
const token = getAuthToken();
|
||
try {
|
||
await fetch("/api/classrooms/delete", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||
body: JSON.stringify({ classroom_id: classId })
|
||
});
|
||
alert("Klasse gelöscht.");
|
||
loadClassrooms();
|
||
} catch (e) {
|
||
alert("Fehler beim Löschen: " + e.message);
|
||
}
|
||
}
|
||
|
||
document.getElementById("create-class-form").onsubmit = async (e) => {
|
||
e.preventDefault();
|
||
const name = document.getElementById("input-class-name").value.trim();
|
||
const token = getAuthToken();
|
||
|
||
if (!token) {
|
||
window.openAuthModal("login");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch("/api/classrooms/create", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||
body: JSON.stringify({ name })
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || "Fehler beim Erstellen");
|
||
|
||
alert(`Klasse '${name}' mit Einladungscode '${data.classroom.invite_code}' erstellt! 🎉`);
|
||
document.getElementById("input-class-name").value = "";
|
||
loadClassrooms();
|
||
} catch (err) {
|
||
alert("Fehler: " + err.message);
|
||
}
|
||
};
|
||
|
||
document.addEventListener("DOMContentLoaded", loadClassrooms);
|
||
</script>
|
||
</body>
|
||
</html>
|