1e51ef7def
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/ - 12 code-workspace-Files - STATUS-*.md - viele M/D/R-Änderungen an bereits getrackten Files - .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
573 lines
23 KiB
HTML
573 lines
23 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||
<title>GeoGraSim V2 — Lehrperson-Cockpit</title>
|
||
<link rel="stylesheet" href="./assets/css/design-system-v2.css">
|
||
<style>
|
||
body { background: var(--ggs-bg); min-height: 100dvh; }
|
||
.topbar {
|
||
background: var(--ggs-white); border-bottom: 1px solid var(--ggs-border);
|
||
padding: var(--ggs-gap-sm) var(--ggs-gap-md);
|
||
display: flex; align-items: center; gap: var(--ggs-gap-md);
|
||
position: sticky; top: 0; z-index: 100;
|
||
}
|
||
.topbar h1 { font-size: 1rem; color: var(--ggs-fjord-dark); font-weight: 800; flex: 1; }
|
||
.teacher-pill {
|
||
display: flex; align-items: center; gap: var(--ggs-gap-sm);
|
||
padding: 4px var(--ggs-gap-md); background: var(--ggs-bg); border-radius: var(--ggs-radius-pill);
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.wrap { display: grid; grid-template-columns: 240px 1fr; gap: var(--ggs-gap-lg); padding: var(--ggs-gap-lg); max-width: 1400px; margin: 0 auto; }
|
||
@media (max-width: 900px) { .wrap { grid-template-columns: 1fr; } }
|
||
|
||
aside.classes {
|
||
background: var(--ggs-white); border-radius: var(--ggs-radius-md);
|
||
padding: var(--ggs-gap-md); border: 1px solid var(--ggs-border);
|
||
height: fit-content;
|
||
}
|
||
aside h3 { font-size: 0.7rem; text-transform: uppercase; color: var(--ggs-text-muted); margin-bottom: var(--ggs-gap-sm); }
|
||
.class-item {
|
||
padding: var(--ggs-gap-sm) var(--ggs-gap-md);
|
||
border-radius: var(--ggs-radius-sm); cursor: pointer;
|
||
font-size: 0.85rem;
|
||
margin-bottom: var(--ggs-gap-xs);
|
||
}
|
||
.class-item.active { background: var(--ggs-fjord-dark); color: var(--ggs-white); }
|
||
.class-item .meta { font-size: 0.7rem; opacity: 0.7; }
|
||
|
||
main.content { display: flex; flex-direction: column; gap: var(--ggs-gap-md); }
|
||
.tabs {
|
||
display: flex; gap: var(--ggs-gap-xs); border-bottom: 1px solid var(--ggs-border);
|
||
}
|
||
.tab-btn {
|
||
background: transparent; border: none; cursor: pointer;
|
||
padding: var(--ggs-gap-sm) var(--ggs-gap-md);
|
||
font-family: inherit; font-size: 0.85rem; font-weight: 600;
|
||
color: var(--ggs-text-muted);
|
||
border-bottom: 3px solid transparent;
|
||
min-height: var(--ggs-touch-min);
|
||
}
|
||
.tab-btn.active { color: var(--ggs-fjord-dark); border-bottom-color: var(--ggs-fjord-dark); }
|
||
.panel { display: none; }
|
||
.panel.active { display: block; }
|
||
|
||
.card { background: var(--ggs-white); border: 1px solid var(--ggs-border); border-radius: var(--ggs-radius-md); padding: var(--ggs-gap-md); margin-bottom: var(--ggs-gap-md); }
|
||
|
||
/* Live-Dashboard */
|
||
.live-grid {
|
||
display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: var(--ggs-gap-md);
|
||
}
|
||
.student-status {
|
||
background: var(--ggs-white); border: 2px solid var(--ggs-border);
|
||
border-radius: var(--ggs-radius-md); padding: var(--ggs-gap-md);
|
||
}
|
||
.student-status.stuck { border-color: var(--ggs-orange); background: #fff8e6; }
|
||
.student-status.offline { opacity: 0.5; }
|
||
.student-status header { display: flex; align-items: center; gap: var(--ggs-gap-sm); margin-bottom: var(--ggs-gap-sm); }
|
||
.student-status img { width: 36px; height: 36px; border-radius: 50%; }
|
||
.student-status h4 { font-size: 0.9rem; flex: 1; }
|
||
.student-status .module { font-size: 0.78rem; color: var(--ggs-text-muted); }
|
||
.student-status .phase { font-size: 0.85rem; font-weight: 600; margin-top: 4px; }
|
||
.student-status .score-bar {
|
||
height: 6px; background: var(--ggs-border); border-radius: 999px; overflow: hidden;
|
||
margin-top: var(--ggs-gap-sm);
|
||
}
|
||
.student-status .score-bar > div { height: 100%; background: var(--ggs-moss); }
|
||
.student-status .stuck-icon { color: var(--ggs-orange); font-weight: 700; }
|
||
.student-status .na-pill {
|
||
display: inline-block; padding: 1px 6px; font-size: 0.65rem;
|
||
background: var(--ggs-fjord-light); color: var(--ggs-fjord-dark);
|
||
border-radius: var(--ggs-radius-pill);
|
||
}
|
||
|
||
/* Tabellen (Schüler*innen) */
|
||
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
||
th, td { text-align: left; padding: var(--ggs-gap-sm); border-bottom: 1px solid var(--ggs-border); }
|
||
th { font-weight: 700; color: var(--ggs-text-muted); font-size: 0.75rem; text-transform: uppercase; }
|
||
.checkbox-cell input { width: 18px; height: 18px; cursor: pointer; }
|
||
|
||
/* Auftrags-Modal */
|
||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.4); display: none; align-items: center; justify-content: center; z-index: 1000; padding: var(--ggs-gap-md); }
|
||
.modal-overlay.show { display: flex; }
|
||
.modal-card { background: var(--ggs-white); padding: var(--ggs-gap-lg); border-radius: var(--ggs-radius-md); max-width: 480px; width: 100%; max-height: 94dvh; overflow: auto; }
|
||
.modal-card h3 { color: var(--ggs-fjord-dark); margin-bottom: var(--ggs-gap-md); }
|
||
.modal-card .form-row { margin-bottom: var(--ggs-gap-md); }
|
||
.modal-card label { display: block; font-size: 0.78rem; font-weight: 600; margin-bottom: 4px; }
|
||
.modal-card select, .modal-card input { width: 100%; }
|
||
|
||
.empty { padding: var(--ggs-gap-lg); text-align: center; color: var(--ggs-text-muted); font-size: 0.85rem; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="topbar">
|
||
<h1>GeoGraSim V2 · Lehrperson-Cockpit</h1>
|
||
<div class="teacher-pill" id="teacherPill" hidden>
|
||
<span id="teacherName"></span>
|
||
</div>
|
||
<a href="./admin-config.html" class="ggs-btn ggs-btn-secondary" id="configLink" hidden>⚙ Plattform</a>
|
||
<button class="ggs-btn ggs-btn-secondary" id="logoutBtn">Abmelden</button>
|
||
</div>
|
||
|
||
<div class="wrap">
|
||
<aside class="classes">
|
||
<h3>Meine Klassen</h3>
|
||
<div id="classList"></div>
|
||
</aside>
|
||
|
||
<main class="content">
|
||
<div class="tabs" role="tablist">
|
||
<button class="tab-btn active" data-tab="live" role="tab">📡 Live</button>
|
||
<button class="tab-btn" data-tab="students" role="tab">👥 Klassenliste</button>
|
||
<button class="tab-btn" data-tab="assignments" role="tab">📌 Aufträge</button>
|
||
<button class="tab-btn" data-tab="results" role="tab">📊 Auswertung</button>
|
||
</div>
|
||
|
||
<!-- LIVE -->
|
||
<div class="panel active" id="panel-live" role="tabpanel">
|
||
<div class="card">
|
||
<p style="font-size:.85rem;color:var(--ggs-text-muted)">
|
||
Live-Status der Klasse — Update alle 3 Sekunden.
|
||
Orange Karte = Schüler*in steckt fest (90s+ ohne Eingabe).
|
||
</p>
|
||
</div>
|
||
<div class="live-grid" id="liveGrid"></div>
|
||
<div class="empty" id="liveEmpty" hidden>
|
||
Niemand aus dieser Klasse arbeitet gerade in einem Modul.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- KLASSENLISTE -->
|
||
<div class="panel" id="panel-students" role="tabpanel">
|
||
<div class="card" style="display:flex;gap:var(--ggs-gap-md);flex-wrap:wrap;align-items:center">
|
||
<button class="ggs-btn ggs-btn-primary" id="newStudentBtn">+ Schüler*in anlegen</button>
|
||
<button class="ggs-btn ggs-btn-secondary" id="csvImportBtn">📁 CSV importieren</button>
|
||
<span style="font-size:.78rem;color:var(--ggs-text-muted);margin-left:auto">
|
||
A11y-Detail-Tab pro Schüler*in kommt in einer späteren Iteration.
|
||
</span>
|
||
</div>
|
||
<div class="card">
|
||
<table>
|
||
<thead><tr>
|
||
<th>Name</th><th>Username</th><th>Easy</th><th>HC</th>
|
||
<th>Red.Motion</th><th>Screen-Reader</th><th>Nachteil-Ausgleich</th>
|
||
<th>Zuletzt</th><th></th>
|
||
</tr></thead>
|
||
<tbody id="studentTableBody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- AUFTRÄGE -->
|
||
<div class="panel" id="panel-assignments" role="tabpanel">
|
||
<div class="card">
|
||
<button class="ggs-btn ggs-btn-primary" id="newAssignmentBtn">+ Neuer Auftrag</button>
|
||
</div>
|
||
<div id="assignmentList"></div>
|
||
</div>
|
||
|
||
<!-- AUSWERTUNG -->
|
||
<div class="panel" id="panel-results" role="tabpanel">
|
||
<div class="card empty">
|
||
Auswertung kommt in einer späteren V2-Iteration.
|
||
Heute siehst du Live-Daten + Aufträge.
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Modal: Neue Schüler*in -->
|
||
<div class="modal-overlay" id="studentModal">
|
||
<div class="modal-card">
|
||
<h3>Neue Schüler*in anlegen</h3>
|
||
<div class="form-row">
|
||
<label for="st-username">Benutzername *</label>
|
||
<input class="ggs-input" type="text" id="st-username" autocomplete="off">
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="st-password">Passwort *</label>
|
||
<input class="ggs-input" type="text" id="st-password" autocomplete="off">
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="st-display">Anzeigename (optional)</label>
|
||
<input class="ggs-input" type="text" id="st-display" autocomplete="off">
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--ggs-gap-md)">
|
||
<div class="form-row">
|
||
<label for="st-first">Vorname (optional)</label>
|
||
<input class="ggs-input" type="text" id="st-first" autocomplete="off">
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="st-last">Nachname (optional)</label>
|
||
<input class="ggs-input" type="text" id="st-last" autocomplete="off">
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="st-avatar">Avatar</label>
|
||
<select class="ggs-select" id="st-avatar"></select>
|
||
</div>
|
||
<div id="studentModalMsg" style="font-size:.85rem;color:var(--ggs-coral);margin-bottom:8px;display:none"></div>
|
||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||
<button class="ggs-btn ggs-btn-secondary" id="st-cancel">Abbrechen</button>
|
||
<button class="ggs-btn ggs-btn-primary" id="st-save">Anlegen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: CSV-Import -->
|
||
<div class="modal-overlay" id="csvModal">
|
||
<div class="modal-card">
|
||
<h3>CSV-Import</h3>
|
||
<p style="font-size:.85rem;color:var(--ggs-text-muted);margin-bottom:var(--ggs-gap-md)">
|
||
Datei mit Spaltenkopf. <strong>Pflicht</strong>: <code>username</code>, <code>password</code>.
|
||
Optional: <code>display_name</code>, <code>first_name</code>, <code>last_name</code>, <code>avatar_slug</code>.
|
||
Trenner: Komma oder Semikolon (auto).
|
||
</p>
|
||
<div class="form-row">
|
||
<input class="ggs-input" type="file" id="csv-file" accept=".csv,text/csv">
|
||
</div>
|
||
<div id="csvResult" style="font-size:.85rem;margin-bottom:var(--ggs-gap-md);display:none"></div>
|
||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||
<button class="ggs-btn ggs-btn-secondary" id="csv-cancel">Schließen</button>
|
||
<button class="ggs-btn ggs-btn-primary" id="csv-upload">Hochladen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal für neuen Auftrag -->
|
||
<div class="modal-overlay" id="assignModal">
|
||
<div class="modal-card">
|
||
<h3>Neuer Modul-Auftrag</h3>
|
||
<div class="form-row">
|
||
<label for="m-module">Modul</label>
|
||
<select class="ggs-select" id="m-module"></select>
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="m-difficulty">Schwierigkeit</label>
|
||
<select class="ggs-select" id="m-difficulty"></select>
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="m-endsAt">Bis wann (optional)</label>
|
||
<input class="ggs-input" type="datetime-local" id="m-endsAt">
|
||
</div>
|
||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||
<button class="ggs-btn ggs-btn-secondary" id="m-cancel">Abbrechen</button>
|
||
<button class="ggs-btn ggs-btn-primary" id="m-save">Auftrag erstellen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="module">
|
||
import { auth } from './assets/js/auth-client.js';
|
||
|
||
if (!auth.token || (auth.role !== 'teacher' && auth.role !== 'admin')) {
|
||
location.replace('./login.html');
|
||
}
|
||
|
||
const $ = id => document.getElementById(id);
|
||
$('logoutBtn').addEventListener('click', async () => { await auth.logout(); location.replace('./login.html'); });
|
||
|
||
let currentClass = null;
|
||
let allModules = [];
|
||
let liveTimer = null;
|
||
let liveSinceId = 0;
|
||
|
||
async function init() {
|
||
await auth.maybeRefresh();
|
||
$('teacherName').textContent = auth.user?.displayName || 'Lehrperson';
|
||
$('teacherPill').hidden = false;
|
||
if (auth.role === 'admin') $('configLink').hidden = false;
|
||
const r = await auth.api('GET', 'teacher/classes.php');
|
||
renderClassList(r.classes);
|
||
if (r.classes.length) selectClass(r.classes[0]);
|
||
// Module für Auftrag-Dropdown vorab laden
|
||
try {
|
||
const m = await auth.api('GET', 'modules/all.php');
|
||
allModules = m.modules || [];
|
||
} catch (e) { allModules = []; }
|
||
}
|
||
|
||
function renderClassList(classes) {
|
||
$('classList').innerHTML = classes.length
|
||
? classes.map(c => `
|
||
<div class="class-item" data-id="${c.id}">
|
||
<div>${escapeHtml(c.name)}</div>
|
||
<div class="meta">${escapeHtml(c.schoolYear || '')} · ${c.studentCount} Sch*in${c.studentCount===1?'':'en'}</div>
|
||
</div>
|
||
`).join('')
|
||
: '<div class="empty">Noch keine Klassen.</div>';
|
||
document.querySelectorAll('.class-item').forEach(el => {
|
||
el.addEventListener('click', () => {
|
||
const c = classes.find(c => c.id == el.dataset.id);
|
||
if (c) selectClass(c);
|
||
});
|
||
});
|
||
}
|
||
|
||
function selectClass(c) {
|
||
currentClass = c;
|
||
liveSinceId = 0;
|
||
document.querySelectorAll('.class-item').forEach(el => el.classList.toggle('active', el.dataset.id == c.id));
|
||
loadActiveTab();
|
||
startLivePolling();
|
||
}
|
||
|
||
// Tabs
|
||
document.querySelectorAll('.tab-btn').forEach(b => {
|
||
b.addEventListener('click', () => {
|
||
document.querySelectorAll('.tab-btn').forEach(x => x.classList.toggle('active', x === b));
|
||
document.querySelectorAll('.panel').forEach(p => p.classList.toggle('active', p.id === 'panel-' + b.dataset.tab));
|
||
loadActiveTab();
|
||
});
|
||
});
|
||
|
||
function activeTab() {
|
||
return document.querySelector('.tab-btn.active')?.dataset.tab;
|
||
}
|
||
|
||
async function loadActiveTab() {
|
||
if (!currentClass) return;
|
||
const tab = activeTab();
|
||
if (tab === 'live') return loadLive();
|
||
if (tab === 'students') return loadStudents();
|
||
if (tab === 'assignments') return loadAssignments();
|
||
}
|
||
|
||
// LIVE-Tab
|
||
function startLivePolling() {
|
||
if (liveTimer) clearInterval(liveTimer);
|
||
liveTimer = setInterval(() => { if (activeTab() === 'live') loadLive(); }, 3000);
|
||
}
|
||
async function loadLive() {
|
||
const r = await auth.api('GET', `teacher/live-status.php?classId=${currentClass.id}&sinceId=${liveSinceId}`);
|
||
if (r.maxId) liveSinceId = Math.max(liveSinceId, r.maxId);
|
||
const status = r.studentStatus || [];
|
||
if (status.length === 0) {
|
||
$('liveGrid').innerHTML = '';
|
||
$('liveEmpty').hidden = false;
|
||
return;
|
||
}
|
||
$('liveEmpty').hidden = true;
|
||
$('liveGrid').innerHTML = status.map(s => {
|
||
const score = s.scoreMax ? Math.round((s.currentScore || 0) / s.scoreMax * 100) : 0;
|
||
const avatarUrl = `./assets/avatars/avatar-${s.avatarSlug || 'fuchs-explorer'}.png`;
|
||
return `
|
||
<div class="student-status ${s.isStuck ? 'stuck' : ''}">
|
||
<header>
|
||
<img src="${escapeAttr(avatarUrl)}" alt="">
|
||
<h4>${escapeHtml(s.displayName)}</h4>
|
||
${s.nachteilsausgleich ? '<span class="na-pill" title="Nachteilsausgleich">NA</span>' : ''}
|
||
${s.isStuck ? '<span class="stuck-icon" title="Steckt fest">⚠</span>' : ''}
|
||
</header>
|
||
<div class="module">${escapeHtml(s.moduleSlug || '–')}</div>
|
||
<div class="phase">${escapeHtml(s.phaseLabel || s.phase || '–')}</div>
|
||
<div class="score-bar"><div style="width:${score}%"></div></div>
|
||
<div style="font-size:.72rem;color:var(--ggs-text-muted);margin-top:4px">
|
||
${s.currentScore != null ? s.currentScore + '/' + (s.scoreMax || '?') + ' · ' : ''}
|
||
${s.lastTs ? 'zuletzt ' + new Date(s.lastTs).toLocaleTimeString('de-AT') : ''}
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// KLASSENLISTE-Tab
|
||
const AVATAR_SLUGS = [
|
||
// Geo-Welt
|
||
'pilotin','pilot','kartografin','geologe','meteorologin','stadtplaner',
|
||
'forscherin','forscher','taucherin','astronaut',
|
||
// Cool & Lustig
|
||
'coole-girl','coole-boy','lustige-girl','lustige-boy','nerd-girl',
|
||
'sportlich-boy','kuenstlerin','musiker','gaertnerin','abenteurer',
|
||
// Tier-Begleiter
|
||
'fuchs-explorer','eule-prof','baer-foerster','pinguin-polar','affe-tropen',
|
||
'delfin-meer','adler-pilot','schildkroete-w','katze-stadt','biene-natur',
|
||
];
|
||
|
||
async function loadStudents() {
|
||
const r = await auth.api('GET', `teacher/students.php?classId=${currentClass.id}`);
|
||
$('studentTableBody').innerHTML = (r.students || []).map(s => `
|
||
<tr data-id="${s.id}">
|
||
<td><strong>${escapeHtml(s.displayName)}</strong>
|
||
<div style="font-size:.7rem;color:var(--ggs-text-muted)">${escapeHtml([s.firstName, s.lastName].filter(Boolean).join(' '))}</div></td>
|
||
<td><code>${escapeHtml(s.username)}</code></td>
|
||
${['easyLanguage','highContrast','reducedMotion','screenReader','nachteilsausgleich'].map(k =>
|
||
`<td class="checkbox-cell"><input type="checkbox" data-key="${k}" ${s[k]?'checked':''}></td>`
|
||
).join('')}
|
||
<td>${s.lastLogin ? new Date(s.lastLogin).toLocaleDateString('de-AT') : '–'}</td>
|
||
<td><button class="ggs-btn ggs-btn-ghost" data-del="${s.id}" title="Entfernen">🗑</button></td>
|
||
</tr>
|
||
`).join('') || `<tr><td colspan="9" class="empty">Noch keine Schüler*innen in dieser Klasse.</td></tr>`;
|
||
document.querySelectorAll('#studentTableBody input').forEach(cb => {
|
||
cb.addEventListener('change', async () => {
|
||
const tr = cb.closest('tr');
|
||
try {
|
||
await auth.api('PATCH', 'teacher/students.php', {
|
||
studentId: parseInt(tr.dataset.id, 10),
|
||
[cb.dataset.key]: cb.checked,
|
||
});
|
||
} catch (e) {
|
||
cb.checked = !cb.checked; alert('Fehler: ' + e.message);
|
||
}
|
||
});
|
||
});
|
||
document.querySelectorAll('[data-del]').forEach(b => {
|
||
b.addEventListener('click', async () => {
|
||
const id = b.dataset.del;
|
||
if (!confirm('Schüler*in wirklich aus der Liste entfernen? (Daten bleiben in der DB, nur verborgen.)')) return;
|
||
try {
|
||
await auth.api('DELETE', `teacher/students.php?studentId=${id}`);
|
||
loadStudents();
|
||
} catch (e) { alert('Fehler: ' + e.message); }
|
||
});
|
||
});
|
||
}
|
||
|
||
// Schüler*in-anlegen Modal
|
||
$('newStudentBtn').addEventListener('click', () => {
|
||
$('st-username').value = '';
|
||
$('st-password').value = generatePassword();
|
||
$('st-display').value = '';
|
||
$('st-first').value = '';
|
||
$('st-last').value = '';
|
||
$('st-avatar').innerHTML = AVATAR_SLUGS.map(s =>
|
||
`<option value="${s}"${s==='fuchs-explorer'?' selected':''}>${s}</option>`).join('');
|
||
$('studentModalMsg').style.display = 'none';
|
||
$('studentModal').classList.add('show');
|
||
setTimeout(() => $('st-username').focus(), 50);
|
||
});
|
||
$('st-cancel').addEventListener('click', () => $('studentModal').classList.remove('show'));
|
||
$('st-save').addEventListener('click', async () => {
|
||
const msg = $('studentModalMsg');
|
||
msg.style.display = 'none';
|
||
const username = $('st-username').value.trim();
|
||
const password = $('st-password').value;
|
||
if (!username || username.length < 2) { msg.textContent = 'Username mindestens 2 Zeichen.'; msg.style.display = 'block'; return; }
|
||
if (!password || password.length < 4) { msg.textContent = 'Passwort mindestens 4 Zeichen.'; msg.style.display = 'block'; return; }
|
||
try {
|
||
await auth.api('POST', 'teacher/students.php', {
|
||
classId: currentClass.id,
|
||
username,
|
||
password,
|
||
displayName: $('st-display').value.trim() || username,
|
||
firstName: $('st-first').value.trim() || null,
|
||
lastName: $('st-last').value.trim() || null,
|
||
avatarSlug: $('st-avatar').value,
|
||
});
|
||
$('studentModal').classList.remove('show');
|
||
loadStudents();
|
||
} catch (e) {
|
||
msg.textContent = e.message; msg.style.display = 'block';
|
||
}
|
||
});
|
||
|
||
function generatePassword() {
|
||
const chars = 'abcdefghjkmnpqrstuvwxyz23456789';
|
||
let p = '';
|
||
for (let i = 0; i < 6; i++) p += chars[Math.floor(Math.random() * chars.length)];
|
||
return p;
|
||
}
|
||
|
||
// CSV-Import Modal
|
||
$('csvImportBtn').addEventListener('click', () => {
|
||
$('csv-file').value = '';
|
||
$('csvResult').style.display = 'none';
|
||
$('csvModal').classList.add('show');
|
||
});
|
||
$('csv-cancel').addEventListener('click', () => $('csvModal').classList.remove('show'));
|
||
$('csv-upload').addEventListener('click', async () => {
|
||
const file = $('csv-file').files[0];
|
||
if (!file) { alert('Bitte CSV-Datei wählen.'); return; }
|
||
const form = new FormData();
|
||
form.append('classId', currentClass.id);
|
||
form.append('file', file);
|
||
const r = await fetch(`./php/api/teacher/students-csv.php`, {
|
||
method: 'POST',
|
||
headers: { Authorization: `Bearer ${auth.token}` },
|
||
body: form,
|
||
});
|
||
const data = await r.json();
|
||
const res = $('csvResult');
|
||
res.style.display = 'block';
|
||
if (!r.ok) {
|
||
res.style.color = 'var(--ggs-coral)';
|
||
res.textContent = 'Fehler: ' + (data.message || 'unbekannt');
|
||
return;
|
||
}
|
||
res.style.color = 'var(--ggs-moss-dark)';
|
||
res.innerHTML = `✓ ${data.created} angelegt, ${data.skipped.length} übersprungen.` +
|
||
(data.skipped.length ? '<br><small>' + data.skipped.map(s =>
|
||
`Zeile ${s.line} (${escapeHtml(s.username)}): ${escapeHtml(s.reason)}`).join('<br>') + '</small>' : '');
|
||
loadStudents();
|
||
});
|
||
|
||
// AUFTRÄGE-Tab
|
||
async function loadAssignments() {
|
||
const r = await auth.api('GET', `teacher/assignments.php?classId=${currentClass.id}`);
|
||
$('assignmentList').innerHTML = (r.assignments || []).map(a => `
|
||
<div class="card" style="display:flex;align-items:center;gap:12px">
|
||
<div style="font-size:1.5rem">${escapeHtml(a.icon)}</div>
|
||
<div style="flex:1">
|
||
<div><strong>${escapeHtml(a.displayName)}</strong> · ${escapeHtml(a.difficulty)}</div>
|
||
<div style="font-size:.72rem;color:var(--ggs-text-muted)">
|
||
${a.active ? 'aktiv seit ' : 'beendet '}${a.startsAt ? new Date(a.startsAt).toLocaleString('de-AT') : ''}
|
||
</div>
|
||
</div>
|
||
${a.active ? `<button class="ggs-btn ggs-btn-secondary" data-stop="${a.id}">Beenden</button>` : ''}
|
||
</div>
|
||
`).join('') || '<div class="empty">Keine Aufträge in dieser Klasse.</div>';
|
||
document.querySelectorAll('[data-stop]').forEach(b => {
|
||
b.addEventListener('click', async () => {
|
||
if (!confirm('Auftrag wirklich beenden?')) return;
|
||
await auth.api('DELETE', `teacher/assignments.php?id=${b.dataset.stop}`);
|
||
loadAssignments();
|
||
});
|
||
});
|
||
}
|
||
|
||
// Auftrag erstellen Modal
|
||
$('newAssignmentBtn').addEventListener('click', () => {
|
||
if (!allModules.length) { alert('Keine Module verfügbar.'); return; }
|
||
$('m-module').innerHTML = allModules.map(m =>
|
||
`<option value="${escapeAttr(m.slug)}">${escapeHtml(m.icon)} ${escapeHtml(m.displayName)}</option>`
|
||
).join('');
|
||
updateDifficultyOptions();
|
||
$('assignModal').classList.add('show');
|
||
});
|
||
$('m-module').addEventListener('change', updateDifficultyOptions);
|
||
function updateDifficultyOptions() {
|
||
const slug = $('m-module').value;
|
||
const m = allModules.find(x => x.slug === slug);
|
||
const levels = m?.difficultyLevels || [{id:'L1',label:'Einsteiger'}];
|
||
$('m-difficulty').innerHTML = levels.map(l =>
|
||
`<option value="${escapeAttr(l.id)}">${escapeHtml(l.id)} — ${escapeHtml(l.label)}</option>`
|
||
).join('');
|
||
}
|
||
$('m-cancel').addEventListener('click', () => $('assignModal').classList.remove('show'));
|
||
$('m-save').addEventListener('click', async () => {
|
||
try {
|
||
await auth.api('POST', 'teacher/assignments.php', {
|
||
classId: currentClass.id,
|
||
moduleSlug: $('m-module').value,
|
||
difficulty: $('m-difficulty').value,
|
||
endsAt: $('m-endsAt').value || null,
|
||
});
|
||
$('assignModal').classList.remove('show');
|
||
loadAssignments();
|
||
} catch (e) { alert('Fehler: ' + e.message); }
|
||
});
|
||
|
||
function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||
function escapeAttr(s) { return escapeHtml(s); }
|
||
|
||
init().catch(err => {
|
||
document.body.innerHTML = `<p style="padding:2rem;color:#c85c4a">Fehler: ${err.message}</p>`;
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|