Aufgaben-Python/teacher.html

359 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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- &amp; Lehrer-Portal</div>
<h1>Klassenverwaltung &amp; 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
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>