Files
geograsim/App/teacher.html
T
Adminator b06aa025c1 Login-Frontend + Lehrer-Dashboard + Stadt-Simulation komplett
- login.html: Schueler-Login (Klassencode+User+PW) + Lehrer-Login/Register
- teacher.html: Vollstaendiges Dashboard (Klassen, Schueler, Module, Stats)
- stadt.html: Komplette Simulation mit 27 Kachel-Typen + Kennwerten
- tile-test.html: Spital-Sackgassen gefixt
- PHP-Pages fuer alle neuen HTML-Seiten

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:37:08 +02:00

291 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Lehrkraft-Dashboard</title>
<link rel="icon" type="image/png" href="assets/img/logo.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',sans-serif;background:#f7f6f3;color:#1a1a1a;min-height:100vh}
.topbar{background:#fff;padding:.6rem 1.2rem;display:flex;align-items:center;gap:.8rem;border-bottom:1px solid rgba(0,0,0,.06);position:sticky;top:0;z-index:10}
.topbar h1{font-size:.95rem;font-weight:800;color:#4a7c8a;flex:1}
.topbar .user{font-size:.72rem;color:#8a8a8a}
.topbar .btn-sm{padding:.3rem .6rem;border:1px solid rgba(0,0,0,.08);border-radius:6px;background:#fff;font-size:.68rem;font-weight:600;cursor:pointer;color:#4a4a4a}
.topbar .btn-sm:hover{background:#dae8ec;color:#4a7c8a}
.wrap{max-width:1000px;margin:0 auto;padding:1rem}
.grid{display:grid;grid-template-columns:280px 1fr;gap:1rem;min-height:calc(100vh - 60px)}
.sidebar{display:flex;flex-direction:column;gap:.6rem}
.main{display:flex;flex-direction:column;gap:.8rem}
.card{background:#fff;border-radius:10px;padding:.8rem;box-shadow:0 1px 4px rgba(0,0,0,.03);border:1px solid rgba(0,0,0,.04)}
.card-h{font-size:.6rem;font-weight:700;color:#4a7c8a;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.4rem}
.class-item{padding:.5rem;border:1.5px solid rgba(0,0,0,.05);border-radius:7px;cursor:pointer;margin-bottom:.3rem;transition:all .1s}
.class-item:hover{border-color:#4a7c8a;background:#f0f6f8}
.class-item.active{border-color:#4a7c8a;background:#dae8ec}
.class-item h4{font-size:.78rem;font-weight:700}
.class-item p{font-size:.62rem;color:#8a8a8a}
.btn{padding:.4rem .8rem;border:none;border-radius:6px;background:#4a7c8a;color:#fff;font-size:.72rem;font-weight:700;cursor:pointer}
.btn:hover{background:#3a6470}
.btn-outline{background:transparent;color:#4a7c8a;border:1.5px solid #4a7c8a}
.btn-danger{background:#c07a6b}
table{width:100%;border-collapse:collapse;font-size:.75rem}
th{background:#f7f6f3;padding:.4rem .5rem;text-align:left;font-weight:600;font-size:.65rem;color:#8a8a8a}
td{padding:.4rem .5rem;border-bottom:1px solid rgba(0,0,0,.03)}
.module-row{display:flex;align-items:center;gap:.5rem;padding:.4rem .2rem;border-bottom:1px solid rgba(0,0,0,.03)}
.module-row:last-child{border:none}
.module-name{flex:1;font-size:.75rem;font-weight:600}
.module-desc{font-size:.6rem;color:#8a8a8a}
.mode-btn{padding:.2rem .5rem;border:1px solid rgba(0,0,0,.08);border-radius:4px;font-size:.6rem;font-weight:600;cursor:pointer;background:#fff}
.mode-btn.active{background:#4a7c8a;color:#fff;border-color:#4a7c8a}
.modal{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:100}
.modal-card{background:#fff;border-radius:12px;padding:1.2rem;max-width:380px;width:90%}
.modal-card h3{font-size:.9rem;font-weight:700;margin-bottom:.5rem}
.modal-card label{display:block;font-size:.68rem;font-weight:600;margin-top:.5rem;color:#4a4a4a}
.modal-card input{width:100%;padding:.4rem .6rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.78rem;margin-top:.15rem}
.hidden{display:none}
.stat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.4rem;margin-bottom:.5rem}
.stat{text-align:center;background:#f7f6f3;border-radius:6px;padding:.4rem}
.stat .n{font-size:1.1rem;font-weight:800;color:#4a7c8a}
.stat .l{font-size:.55rem;color:#8a8a8a}
@media(max-width:700px){.grid{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="topbar">
<h1>🌍 GeoGraSim — Lehrkraft</h1>
<span class="user" id="user-info"></span>
<button class="btn-sm" onclick="logout()">Abmelden</button>
<a href="index.html" class="btn-sm">← App</a>
</div>
<div class="wrap">
<div class="grid">
<!-- SIDEBAR: Klassen -->
<div class="sidebar">
<div class="card">
<div class="card-h">Meine Klassen</div>
<div id="class-list"></div>
<button class="btn" style="width:100%;margin-top:.4rem" onclick="showCreateClass()">+ Neue Klasse</button>
</div>
<div class="card">
<div class="card-h">Handbuch</div>
<a href="handbuch.html" style="font-size:.72rem;color:#4a7c8a">📖 Didaktisches Handbuch öffnen</a>
</div>
</div>
<!-- MAIN -->
<div class="main" id="main-content">
<div class="card" style="text-align:center;padding:2rem;color:#8a8a8a">
<p>Wähle links eine Klasse aus oder erstelle eine neue.</p>
</div>
</div>
</div>
</div>
<!-- Modal: Klasse erstellen -->
<div class="modal hidden" id="modal-class">
<div class="modal-card">
<h3 id="modal-class-title">Neue Klasse</h3>
<label>Klassenname</label>
<input type="text" id="mc-name" placeholder="z.B. 1A">
<label>Schuljahr</label>
<input type="text" id="mc-year" placeholder="z.B. 2025/26">
<div style="display:flex;gap:.4rem;margin-top:.8rem">
<button class="btn" onclick="saveClass()">Speichern</button>
<button class="btn btn-outline" onclick="hideModal('modal-class')">Abbrechen</button>
</div>
</div>
</div>
<!-- Modal: Schueler erstellen -->
<div class="modal hidden" id="modal-student">
<div class="modal-card">
<h3>Schüler:in erstellen</h3>
<label>Benutzername</label>
<input type="text" id="ms-user" placeholder="z.B. max01">
<label>Anzeigename (optional)</label>
<input type="text" id="ms-name" placeholder="z.B. Max M.">
<label>Passwort</label>
<input type="text" id="ms-pass" placeholder="mind. 4 Zeichen">
<div style="display:flex;gap:.4rem;margin-top:.8rem">
<button class="btn" onclick="saveStudent()">Erstellen</button>
<button class="btn btn-outline" onclick="hideModal('modal-student')">Abbrechen</button>
</div>
</div>
</div>
<!-- Modal: Batch-Schueler -->
<div class="modal hidden" id="modal-batch">
<div class="modal-card">
<h3>Mehrere Schüler:innen erstellen</h3>
<label>Prefix</label>
<input type="text" id="mb-prefix" value="schueler" placeholder="z.B. schueler">
<label>Anzahl</label>
<input type="number" id="mb-count" value="25" min="1" max="50">
<p style="font-size:.65rem;color:#8a8a8a;margin-top:.3rem">Erstellt z.B. schueler01, schueler02, ... mit zufälligen 4-stelligen Passwörtern.</p>
<div style="display:flex;gap:.4rem;margin-top:.8rem">
<button class="btn" onclick="batchCreate()">Erstellen</button>
<button class="btn btn-outline" onclick="hideModal('modal-batch')">Abbrechen</button>
</div>
<div id="batch-result" style="margin-top:.5rem;font-size:.65rem;max-height:200px;overflow-y:auto"></div>
</div>
</div>
<script>
var BASE = (window.__GGS__?.baseUrl) || ''
var currentClassId = null
async function api(endpoint, data) {
var opts = data ? {method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify(data)} : {credentials:'same-origin'}
var r = await fetch(BASE + '/api/' + endpoint, opts)
return await r.json()
}
// Auth check
api('auth', {action:'status'}).then(function(r) {
if (r.role !== 'teacher') { location.href = 'login.html'; return }
document.getElementById('user-info').textContent = r.user?.display_name || 'Lehrkraft'
loadClasses()
})
function logout() { api('auth', {action:'logout'}).then(function() { location.href = 'login.html' }) }
// === KLASSEN ===
async function loadClasses() {
var classes = await api('classes')
if (classes.error) return
var html = ''
classes.forEach(function(c) {
html += '<div class="class-item'+(c.id===currentClassId?' active':'')+'" onclick="selectClass('+c.id+')">'
html += '<h4>'+c.name+'</h4>'
html += '<p>'+c.school_year+' · Code: <strong>'+c.join_code+'</strong> · '+c.student_count+' Schüler</p>'
html += '</div>'
})
document.getElementById('class-list').innerHTML = html || '<p style="font-size:.7rem;color:#aaa">Noch keine Klassen.</p>'
}
function selectClass(id) {
currentClassId = id
loadClasses()
loadClassDetail(id)
}
async function loadClassDetail(id) {
var students = await api('students?class_id=' + id)
var modules = await api('modules?class_id=' + id)
if (students.error) students = []
if (modules.error) modules = []
var html = ''
// Stats
html += '<div class="card"><div class="stat-grid">'
html += '<div class="stat"><div class="n">'+students.length+'</div><div class="l">Schüler:innen</div></div>'
var avgScore = 0; var completions = 0
students.forEach(function(s) { if(s.total_score?.avg_score){avgScore+=parseFloat(s.total_score.avg_score);completions+=parseInt(s.total_score.count)} })
if(students.length>0) avgScore = Math.round(avgScore/students.length)
html += '<div class="stat"><div class="n">'+avgScore+'</div><div class="l">Ø Score</div></div>'
html += '<div class="stat"><div class="n">'+completions+'</div><div class="l">Abschlüsse</div></div>'
html += '</div></div>'
// Modulfreigabe
html += '<div class="card"><div class="card-h">Module</div>'
modules.forEach(function(m) {
html += '<div class="module-row">'
html += '<div><div class="module-name">'+m.name+'</div><div class="module-desc">'+m.desc+'</div></div>'
html += '<button class="mode-btn'+(m.mode==='locked'?' active':'')+'" onclick="setMode('+id+',\''+m.id+'\',\'locked\')">🔒</button>'
html += '<button class="mode-btn'+(m.mode==='free'?' active':'')+'" onclick="setMode('+id+',\''+m.id+'\',\'free\')">🆓</button>'
html += '<button class="mode-btn'+(m.mode==='teacher_started'?' active':'')+'" onclick="setMode('+id+',\''+m.id+'\',\'teacher_started\')">▶ Start</button>'
html += '</div>'
})
html += '</div>'
// Schuelerliste
html += '<div class="card"><div class="card-h">Schüler:innen</div>'
html += '<div style="display:flex;gap:.3rem;margin-bottom:.5rem">'
html += '<button class="btn" onclick="showModal(\'modal-student\')">+ Einzeln</button>'
html += '<button class="btn btn-outline" onclick="showModal(\'modal-batch\')">+ Mehrere</button>'
html += '</div>'
if (students.length > 0) {
html += '<table><tr><th>Benutzer</th><th>Name</th><th>Letzter Login</th><th>Ø Score</th><th></th></tr>'
students.forEach(function(s) {
var avg = s.total_score?.avg_score ? Math.round(parseFloat(s.total_score.avg_score)) : '—'
var last = s.last_login ? new Date(s.last_login).toLocaleDateString('de-AT') : '—'
html += '<tr><td>'+s.username+'</td><td>'+(s.display_name||'—')+'</td><td>'+last+'</td><td>'+avg+'</td>'
html += '<td><button class="btn-sm" onclick="deleteStudent('+s.id+')">🗑</button></td></tr>'
})
html += '</table>'
} else {
html += '<p style="font-size:.7rem;color:#aaa">Noch keine Schüler:innen in dieser Klasse.</p>'
}
html += '</div>'
document.getElementById('main-content').innerHTML = html
}
// === MODALS ===
function showModal(id) { document.getElementById(id).classList.remove('hidden') }
function hideModal(id) { document.getElementById(id).classList.add('hidden') }
function showCreateClass() { document.getElementById('mc-name').value=''; document.getElementById('mc-year').value=''; showModal('modal-class') }
async function saveClass() {
var name = document.getElementById('mc-name').value.trim()
var year = document.getElementById('mc-year').value.trim()
if (!name) return
await api('classes', {action:'create', name:name, schoolYear:year})
hideModal('modal-class')
loadClasses()
}
async function saveStudent() {
var user = document.getElementById('ms-user').value.trim()
var name = document.getElementById('ms-name').value.trim()
var pass = document.getElementById('ms-pass').value
if (!user || !pass) return
var r = await api('students', {action:'create', classId:currentClassId, username:user, displayName:name, password:pass})
if (r.error) { alert(r.error); return }
hideModal('modal-student')
loadClassDetail(currentClassId)
}
async function batchCreate() {
var prefix = document.getElementById('mb-prefix').value.trim() || 'schueler'
var count = parseInt(document.getElementById('mb-count').value) || 25
var r = await api('students', {action:'create_batch', classId:currentClassId, prefix:prefix, count:count})
if (r.error) { alert(r.error); return }
// Zugangsdaten anzeigen
var html = '<strong>Erstellt! Zugangsdaten (bitte notieren):</strong><br>'
html += '<table style="font-size:.6rem"><tr><th>Benutzer</th><th>Passwort</th></tr>'
r.created.forEach(function(s) { html += '<tr><td>'+s.username+'</td><td><code>'+s.password+'</code></td></tr>' })
html += '</table>'
document.getElementById('batch-result').innerHTML = html
loadClassDetail(currentClassId)
}
async function deleteStudent(id) {
if (!confirm('Schüler:in wirklich löschen?')) return
await api('students', {action:'delete', studentId:id})
loadClassDetail(currentClassId)
}
async function setMode(classId, moduleId, mode) {
await api('modules', {action:'set_mode', classId:classId, moduleId:moduleId, mode:mode})
loadClassDetail(classId)
}
</script>
</body>
</html>