it-praxisportal/teacher.html

662 lines
30 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: 24px;
margin-bottom: 24px;
box-shadow: var(--shadow-sm);
transition: all 0.2s ease;
}
.class-card:hover {
box-shadow: var(--shadow-md);
border-color: #cbd5e1;
}
.class-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 14px;
flex-wrap: wrap;
gap: 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;
display: inline-block;
}
.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);
}
.course-pill-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.course-pill-label {
display: inline-flex;
align-items: center;
gap: 6px;
background: #f1f5f9;
border: 1px solid #cbd5e1;
padding: 6px 12px;
border-radius: 99px;
font-size: 0.84rem;
font-weight: 600;
cursor: pointer;
user-select: none;
transition: all 0.15s ease;
}
.course-pill-label:has(input:checked) {
background: #0284c7;
border-color: #0284c7;
color: #ffffff;
}
.course-badge-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
background: #eff6ff;
color: #1e40af;
border: 1px solid #bfdbfe;
border-radius: 6px;
font-size: 0.78rem;
font-weight: 600;
}
.prof-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 700;
}
.badge-fisi { background: #e0f2fe; color: #0369a1; border: 1px solid #bae6fd; }
.badge-fiae { background: #ede9fe; color: #6d28d9; border: 1px solid #ddd6fe; }
.badge-fidp { background: #d1fae5; color: #047857; border: 1px solid #a7f3d0; }
.badge-fidv { background: #ffedd5; color: #c2410c; border: 1px solid #fed7aa; }
.badge-itse { background: #fef3c7; color: #b45309; border: 1px solid #fde68a; }
.badge-itsm { background: #dbeafe; color: #1d4ed8; border: 1px solid #bfdbfe; }
.badge-kdm { background: #cffafe; color: #0e7490; border: 1px solid #a5f3fc; }
</style>
</head>
<body>
<div class="app-layout">
<aside class="app-sidebar">
<div class="sidebar-header">
<a href="dashboard.html" class="sidebar-brand-link">
<div class="sidebar-logo">👨‍🏫</div>
<div>
<div class="sidebar-title">IT-Praxisportal</div>
<div class="sidebar-subtitle">Dozentenbereich</div>
</div>
</a>
</div>
<div class="sidebar-quick-nav">
<a href="dashboard.html" class="sidebar-link">💻 Kurs-Dashboard</a>
<a href="teacher.html" class="sidebar-link active">👨‍🏫 Klassenverwaltung</a>
<a href="dashboard.html#zertifikate-section" class="sidebar-link">🏆 Zertifikate</a>
<a href="index.html" class="sidebar-link">🌐 Zur Startseite</a>
</div>
</aside>
<div class="app-main">
<header>
<div class="header-container">
<div class="header-badge">👨‍🏫 Dozenten- &amp; Ausbilder-Portal</div>
<h1>Klassenverwaltung, Berufe &amp; Azubi-Kurszuweisung</h1>
<p>Verwalte deine Azubi-Klassen, weise spezifische IT-Ausbildungsberufe (FIAE, FISI etc.) und Ausbildungsjahre zu und schalte gezielt Praxiskurse (Git, AD, DNS) frei.</p>
</div>
</header>
<main class="container">
<!-- Neue Klasse erstellen -->
<section class="card">
<h2>✨ Neue Klasse / Azubi-Jahrgang anlegen</h2>
<p style="color: var(--text-muted); font-size: 0.9rem; margin-top: 4px;">
Erstelle eine Klasse für deinen Ausbildungsjahrgang. Den Schülern werden automatisch die passenden Lernfelder und Praxiskurse freigeschaltet.
</p>
<form id="create-class-form" style="margin-top: 18px; display: flex; flex-direction: column; gap: 16px;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px;">
<div>
<label style="display: block; font-weight: 700; font-size: 0.88rem; margin-bottom: 6px;">Klassenname *</label>
<input type="text" id="input-class-name" required placeholder="z.B. FISI 2026/27 – Fachstufe 1" style="width: 100%; padding: 10px 14px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.95rem;">
</div>
<div>
<label style="display: block; font-weight: 700; font-size: 0.88rem; margin-bottom: 6px;">IT-Ausbildungsberuf</label>
<select id="input-class-profession" onchange="autoSelectRecommendedCourses('create')" style="width: 100%; padding: 10px 14px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.95rem; background: #fff;">
<option value="FISI">🖥️ FISI - Fachinformatiker Systemintegration</option>
<option value="FIAE">💻 FIAE - Fachinformatiker Anwendungsentwicklung</option>
<option value="FIDP">📊 FIDP - Fachinformatiker Daten- &amp; Prozessanalyse</option>
<option value="FIDV">🌐 FIDV - Fachinformatiker Digitale Vernetzung</option>
<option value="ITSE">⚡ ITSE - IT-System-Elektroniker</option>
<option value="ITSM">💼 ITSM - Kaufleute IT-System-Management</option>
<option value="KDM">📈 KDM - Kaufleute Digitalisierungsmanagement</option>
</select>
</div>
<div>
<label style="display: block; font-weight: 700; font-size: 0.88rem; margin-bottom: 6px;">Ausbildungsjahr</label>
<select id="input-class-year" onchange="autoSelectRecommendedCourses('create')" style="width: 100%; padding: 10px 14px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.95rem; background: #fff;">
<option value="1">1. Ausbildungsjahr (Grundlagen LF 1–5)</option>
<option value="2">2. Ausbildungsjahr (Netzwerke/Daten LF 6–9 &amp; AP1)</option>
<option value="3">3. Ausbildungsjahr (Fachvertiefung LF 10–12 &amp; AP2)</option>
</select>
</div>
</div>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
<label style="font-weight: 700; font-size: 0.88rem;">Zugewiesene Praxiskurse für diese Klasse:</label>
<span style="font-size: 0.78rem; color: var(--text-muted); cursor: pointer; text-decoration: underline;" onclick="toggleAllCourses('create', true)">Alle auswählen</span>
</div>
<div class="course-pill-grid" id="create-course-pills">
<!-- Dynamisch generiert -->
</div>
</div>
<div style="margin-top: 8px;">
<button type="submit" class="btn" style="padding: 11px 24px; font-weight: 700;">➕ Klasse erstellen &amp; Einladungscode generieren</button>
</div>
</form>
</section>
<!-- Vorhandene Klassen -->
<section>
<div class="section-header" style="margin-top: 24px;">
<h2>📚 Deine aktiven Klassen &amp; Ausbildungsjahrgänge</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>
<!-- Modal: Kurse & Zuweisung bearbeiten -->
<div class="certificate-overlay" id="edit-class-modal" style="display: none;">
<div class="certificate-container" style="max-width: 680px; text-align: left; padding: 24px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px;">
<h3 id="edit-modal-title" style="margin: 0; color: var(--text-main);">⚙️ Klasse &amp; Kurszuweisung bearbeiten</h3>
<button class="btn btn-secondary" style="padding: 4px 10px;" onclick="closeEditModal()">✕</button>
</div>
<form id="edit-class-form" onsubmit="saveClassCourses(event)">
<input type="hidden" id="edit-class-id">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px;">
<div>
<label style="display: block; font-weight: 700; font-size: 0.85rem; margin-bottom: 4px;">Ausbildungsberuf</label>
<select id="edit-class-profession" onchange="autoSelectRecommendedCourses('edit')" style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: 6px;">
<option value="FISI">🖥️ FISI (Systemintegration)</option>
<option value="FIAE">💻 FIAE (Anwendungsentwicklung)</option>
<option value="FIDP">📊 FIDP (Daten- &amp; Prozessanalyse)</option>
<option value="FIDV">🌐 FIDV (Digitale Vernetzung)</option>
<option value="ITSE">⚡ ITSE (System-Elektronik)</option>
<option value="ITSM">💼 ITSM (IT-System-Management)</option>
<option value="KDM">📈 KDM (Digitalisierungsmanagement)</option>
</select>
</div>
<div>
<label style="display: block; font-weight: 700; font-size: 0.85rem; margin-bottom: 4px;">Ausbildungsjahr</label>
<select id="edit-class-year" onchange="autoSelectRecommendedCourses('edit')" style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: 6px;">
<option value="1">1. Ausbildungsjahr (LF 1–5)</option>
<option value="2">2. Ausbildungsjahr (LF 6–9)</option>
<option value="3">3. Ausbildungsjahr (LF 10–12)</option>
</select>
</div>
</div>
<div style="margin-bottom: 18px;">
<label style="display: block; font-weight: 700; font-size: 0.85rem; margin-bottom: 8px;">Freigeschaltete Kurse für die Schüler:</label>
<div class="course-pill-grid" id="edit-course-pills">
<!-- Checkboxen -->
</div>
</div>
<div style="display: flex; justify-content: flex-end; gap: 10px;">
<button type="button" class="btn btn-secondary" onclick="closeEditModal()">Abbrechen</button>
<button type="submit" class="btn" style="background: #0284c7; color: white;">💾 Änderungen speichern</button>
</div>
</form>
</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?v=2.1.0"></script>
<script src="assets/courses_manifest.js?v=2.1.0"></script>
<script src="assets/it_ausbildungen_manifest.js?v=2.1.0"></script>
<script>
// 🔒 1. STRIKTER ROLLEN-GUARD FÜR DOZENTEN / ADMINS
(function guardTeacherAccess() {
const user = (window.AUTH && window.AUTH.user) || JSON.parse(sessionStorage.getItem("auth_user") || localStorage.getItem("auth_user") || "null");
const token = (window.AUTH && window.AUTH.token) || sessionStorage.getItem("auth_token") || localStorage.getItem("auth_token");
if (!token || !user || (user.role !== "teacher" && user.role !== "admin")) {
alert("🔒 Zugriff verweigert: Dieser Bereich ist ausschließlich für Dozenten, Ausbilder und Lehrkräfte reserviert.");
window.location.href = "dashboard.html";
}
})();
let ALL_CLASSROOMS = [];
let CURRENT_MATRIX = {};
const ALL_AVAILABLE_COURSES = [
{ id: "ihk_ap1", title: "IHK AP1 Prüfungstrainer", icon: "📘" },
{ id: "ihk_ap2_fisi", title: "IHK AP2 FISI Prüfungstrainer", icon: "🎓" },
{ id: "python", title: "Python 3 Professional", icon: "🐍" },
{ id: "bash", title: "Linux Bash & DevOps", icon: "🐧" },
{ id: "powershell", title: "PowerShell 7+ Core", icon: "🔷" },
{ id: "active_directory", title: "Active Directory & Server", icon: "🏛️" },
{ id: "dns_records", title: "DNS-Records & Mail Security", icon: "🌐" },
{ id: "git", title: "Git & Version Control", icon: "🔀" },
{ id: "sql", title: "SQL & Relationale DBs", icon: "🗄️" },
{ id: "html_css", title: "HTML5 & Modern CSS3", icon: "🌐" },
{ id: "javascript", title: "JavaScript & Async", icon: "⚡" },
{ id: "java", title: "Java 21 Enterprise", icon: "☕" },
{ id: "csharp", title: "C# & .NET 8", icon: "💜" },
{ id: "cpp", title: "C & Modern C++20", icon: "⚙️" },
{ id: "go", title: "Go (Golang) Microservices", icon: "🔵" },
{ id: "rust", title: "Rust Systems & Memory", icon: "🦀" }
];
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;");
}
// Rendere Kurs-Pills in Formularen
function renderCoursePills(containerId, prefix, selectedCourses = []) {
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = ALL_AVAILABLE_COURSES.map(c => {
const checked = selectedCourses.includes(c.id) ? "checked" : "";
return `
<label class="course-pill-label" for="${prefix}-course-${c.id}">
<input type="checkbox" id="${prefix}-course-${c.id}" name="${prefix}_courses" value="${c.id}" ${checked}>
<span>${c.icon}</span> ${escapeHtml(c.title)}
</label>
`;
}).join("");
}
function autoSelectRecommendedCourses(mode) {
const profId = document.getElementById(`${mode}-class-profession`).value;
const year = parseInt(document.getElementById(`${mode}-class-year`).value, 10);
let recommended = [];
if (window.IT_AUSBILDUNGEN) {
const prof = window.IT_AUSBILDUNGEN.getProfession(profId);
if (prof && prof.years && prof.years[year]) {
recommended = prof.years[year].recommendedCourses || [];
}
}
document.querySelectorAll(`input[name="${mode}_courses"]`).forEach(cb => {
cb.checked = recommended.includes(cb.value);
});
}
function toggleAllCourses(mode, selectAll) {
document.querySelectorAll(`input[name="${mode}_courses"]`).forEach(cb => {
cb.checked = selectAll;
});
}
async function loadClassrooms() {
const container = document.getElementById("classrooms-list-container");
const token = getAuthToken();
if (!token) 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: 36px; color: var(--text-muted);">
Noch keine Klassen angelegt. Erstelle oben deinen ersten Ausbildungsjahrgang!
</div>
`;
return;
}
container.innerHTML = classrooms.map(cl => {
const prof = (window.IT_AUSBILDUNGEN && window.IT_AUSBILDUNGEN.getProfession(cl.profession)) || { name: cl.profession, icon: "💻", badgeClass: "badge-fisi" };
const assignedList = cl.assigned_courses || [];
return `
<div class="class-card">
<div class="class-header">
<div>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
<span class="prof-badge ${prof.badgeClass || 'badge-fisi'}">${prof.icon} ${escapeHtml(cl.profession || 'FISI')}</span>
<span class="badge" style="background: #f1f5f9; color: #475569; font-weight: 700; font-size: 0.78rem;">${cl.training_year || 1}. Lehrjahr</span>
</div>
<h3 style="margin: 0 0 4px 0; color: var(--text-main); font-size: 1.3rem;">${escapeHtml(cl.name)}</h3>
<span style="font-size: 0.85rem; color: var(--text-muted);">👥 ${cl.student_count || 0} Schüler / Azubis 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>
<!-- Zugewiesene Kurse -->
<div style="margin: 12px 0 16px 0; background: #f8fafc; padding: 12px 16px; border-radius: 8px; border: 1px solid #e2e8f0;">
<div style="font-size: 0.78rem; font-weight: 700; color: #64748b; margin-bottom: 6px; text-transform: uppercase;">
Freigeschaltete Kurse für Azubis (${assignedList.length}):
</div>
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
${assignedList.length > 0 ? assignedList.map(cid => {
const found = ALL_AVAILABLE_COURSES.find(c => c.id === cid);
return `<span class="course-badge-tag">${found ? found.icon : '📌'} ${found ? found.title : cid}</span>`;
}).join("") : '<span style="font-size: 0.8rem; color: #94a3b8;">Keine Kurse zugeteilt (Schüler sehen Standard-Lernpfad).</span>'}
</div>
</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button class="btn" style="padding: 7px 14px; font-size: 0.84rem; background: #0284c7; color: white;" onclick="loadClassMatrix(${cl.id})">📊 Lernmatrix anzeigen</button>
<button class="btn btn-secondary" style="padding: 7px 14px; font-size: 0.84rem;" onclick="openEditModal(${cl.id})">⚙️ Kurse &amp; Beruf bearbeiten</button>
<button class="btn btn-secondary" style="padding: 7px 14px; font-size: 0.84rem;" onclick="exportClassCsv(${cl.id})">📥 CSV-Export</button>
<button class="btn btn-secondary" style="padding: 7px 14px; font-size: 0.84rem; 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: 18px;"></div>
</div>
`;
}).join("");
} catch (err) {
container.innerHTML = `<div class="box box-danger">Fehler beim Laden: ${escapeHtml(err.message)}</div>`;
}
}
function openEditModal(classId) {
const cl = ALL_CLASSROOMS.find(c => c.id === classId);
if (!cl) return;
document.getElementById("edit-class-id").value = cl.id;
document.getElementById("edit-modal-title").innerText = `⚙️ Kurszuweisung: ${cl.name}`;
document.getElementById("edit-class-profession").value = cl.profession || "FISI";
document.getElementById("edit-class-year").value = cl.training_year || 1;
renderCoursePills("edit-course-pills", "edit", cl.assigned_courses || []);
document.getElementById("edit-class-modal").style.display = "flex";
}
function closeEditModal() {
document.getElementById("edit-class-modal").style.display = "none";
}
async function saveClassCourses(e) {
e.preventDefault();
const classId = document.getElementById("edit-class-id").value;
const profession = document.getElementById("edit-class-profession").value;
const trainingYear = parseInt(document.getElementById("edit-class-year").value, 10);
const selected = [];
document.querySelectorAll('input[name="edit_courses"]:checked').forEach(cb => {
selected.push(cb.value);
});
const token = getAuthToken();
try {
const res = await fetch("/api/classrooms/update-courses", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body: JSON.stringify({
classroom_id: classId,
profession: profession,
training_year: trainingYear,
assigned_courses: selected
})
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Fehler beim Speichern");
closeEditModal();
loadClassrooms();
} catch (err) {
alert("Fehler: " + err.message);
}
}
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; padding: 12px; background: #f8fafc; border-radius: 6px;'>Noch keine Schüler mit dem Einladungscode beigetreten.</p>";
return;
}
target.innerHTML = `
<table class="student-table">
<thead>
<tr>
<th>Schüler / Azubi</th>
<th>Level</th>
<th>XP</th>
<th>Streak</th>
<th>Gelöste Module</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} Module</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 Modul möchtest du inspizieren? (z.B. courses/git/01_git_init_und_repo_basics oder lehrpfad_1_grundlagen/01_erste_schritte_taschenrechner)", "courses/git/01_git_init_und_repo_basics");
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 = `Modul: ${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_Module_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 })
});
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 profession = document.getElementById("input-class-profession").value;
const trainingYear = parseInt(document.getElementById("input-class-year").value, 10);
const selected = [];
document.querySelectorAll('input[name="create_courses"]:checked').forEach(cb => {
selected.push(cb.value);
});
const token = getAuthToken();
if (!token) return;
try {
const res = await fetch("/api/classrooms/create", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body: JSON.stringify({
name: name,
profession: profession,
training_year: trainingYear,
assigned_courses: selected
})
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Fehler beim Erstellen");
alert(`🎉 Klasse '${name}' erfolgreich angelegt!\nEinladungscode für Schüler: ${data.classroom.invite_code}`);
document.getElementById("input-class-name").value = "";
loadClassrooms();
} catch (err) {
alert("Fehler: " + err.message);
}
};
document.addEventListener("DOMContentLoaded", () => {
renderCoursePills("create-course-pills", "create");
autoSelectRecommendedCourses("create");
loadClassrooms();
});
</script>
</body>
</html>