b06aa025c1
- 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>
291 lines
13 KiB
HTML
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>
|