c950f954d1
- admin-todo.html: mobile-freundliche ToDo/Review-Liste (Prioritaeten, Haekchen + Notizfelder je Punkt, localStorage, Notizen-Export), admin-gated, noindex. In admin-licenses.html Nav + Backend-Werkzeuge verlinkt. - admin.php: devPin auch bei STAGING=1 (v3, Mail bewusst tot) -> Self-Service- Admin-Login ohne Mailversand. Prod unveraendert (STAGING dort nie definiert, 2FA-Mail-Pflicht bleibt). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
187 lines
9.6 KiB
HTML
187 lines
9.6 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 — Lizenzverwaltung</title>
|
|
<link rel="icon" type="image/png" href="favicon-96x96.png" sizes="96x96"><link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
<link rel="stylesheet" href="assets/fonts/inter.css">
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:'Inter',sans-serif;background:#f7f6f3;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)}
|
|
.topbar h1{font-size:.95rem;font-weight:800;color:#4a7c8a;flex:1;display:flex;align-items:center;gap:.5rem}
|
|
.topbar h1 img{height:28px}
|
|
.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;text-decoration:none}
|
|
.btn-sm:hover{background:#dae8ec;color:#4a7c8a}
|
|
.wrap{max-width:1000px;margin:1rem auto;padding:0 1rem}
|
|
.card{background:#fff;border-radius:10px;padding:1rem;box-shadow:0 1px 4px rgba(0,0,0,.03);border:1px solid rgba(0,0,0,.04);margin-bottom:1rem}
|
|
.card-h{font-size:.62rem;font-weight:700;color:#4a7c8a;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.5rem}
|
|
.stat-row{display:flex;gap:1rem;margin-bottom:1rem}
|
|
.stat{background:#f7f6f3;border-radius:8px;padding:.8rem 1.2rem;text-align:center;flex:1}
|
|
.stat .n{font-size:1.5rem;font-weight:900;color:#4a7c8a}
|
|
.stat .l{font-size:.62rem;color:#8a8a8a}
|
|
.btn{padding:.45rem .9rem;border:none;border-radius:6px;background:#4a7c8a;color:#fff;font-size:.75rem;font-weight:700;cursor:pointer}
|
|
.btn:hover{background:#3a6470}
|
|
.btn-outline{background:transparent;color:#4a7c8a;border:1.5px solid #4a7c8a}
|
|
table{width:100%;border-collapse:collapse;font-size:.72rem}
|
|
th{background:#f7f6f3;padding:.35rem .5rem;text-align:left;font-weight:600;font-size:.6rem;color:#8a8a8a}
|
|
td{padding:.35rem .5rem;border-bottom:1px solid rgba(0,0,0,.03)}
|
|
.code{font-family:'Courier New',monospace;font-weight:700;letter-spacing:.04em;font-size:.72rem}
|
|
.free{color:#5a8a5e}
|
|
.used{color:#8a8a8a}
|
|
.filter-bar{display:flex;gap:.4rem;margin-bottom:.8rem;flex-wrap:wrap;align-items:center}
|
|
.filter-bar select,.filter-bar input{padding:.35rem .5rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.72rem;font-family:inherit}
|
|
.toolbar{display:flex;gap:.4rem;margin-bottom:.8rem;flex-wrap:wrap}
|
|
.msg{font-size:.72rem;padding:.4rem .6rem;border-radius:6px;margin-top:.5rem}
|
|
.msg.ok{background:#dceadd;color:#5a8a5e}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="topbar">
|
|
<h1><img src="assets/img/geograsim_t.svg" alt="GeoGraSim"> Lizenzverwaltung (Admin)</h1>
|
|
<a href="admin-modules.html" class="btn-sm">🧩 Module</a>
|
|
<a href="admin-levels.html" class="btn-sm">🎮 Level</a>
|
|
<a href="admin-styleguide.html" class="btn-sm">🎨 Styleguide</a>
|
|
<a href="admin-accounts.html" class="btn-sm">👤 Admins</a>
|
|
<a href="admin-weltkueche.html" class="btn-sm">🍽️ Weltküche-Herkunft</a>
|
|
<a href="admin-todo.html" class="btn-sm">📋 ToDo / Review</a>
|
|
<a href="admin.html" class="btn-sm">Logout</a>
|
|
</div>
|
|
|
|
<div class="wrap">
|
|
<div class="card">
|
|
<div class="card-h">🛠️ Backend-Werkzeuge (nur Admin)</div>
|
|
<div style="display:flex;flex-wrap:wrap;gap:.5rem">
|
|
<a class="btn btn-outline" href="sims/tourismusregion/editor.php">🗺️ Tourismus-Karten-Editor (Region)</a>
|
|
<a class="btn btn-outline" href="sims/tourismustal/editor.php">🏠 Tourismustal-Sprite-Editor</a>
|
|
<a class="btn btn-outline" href="stadt-editor">🏙️ Stadt-Raumplanung-Editor</a>
|
|
<a class="btn btn-outline" href="foto-upload">📷 Schulbuch-Bildupload</a>
|
|
<a class="btn btn-outline" href="admin-weltkueche.html">🍽️ Weltküche-Herkunftsübersicht (QA)</a>
|
|
<a class="btn btn-outline" href="admin-todo.html">📋 ToDo / Review-Liste</a>
|
|
</div>
|
|
<div style="font-size:.66rem;color:#8a8a8a;margin-top:.55rem">Alle nur mit Admin-Login erreichbar. Schulbuch-Upload zusätzlich per PIN (0815). Heli-Audio-Werkzeuge liegen nur lokal (<code>.LocalDeveloperTools</code>) und sind nicht auf dem Server.</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="stat-row">
|
|
<div class="stat"><div class="n" id="stat-total">—</div><div class="l">Gesamt</div></div>
|
|
<div class="stat"><div class="n free" id="stat-free">—</div><div class="l">Frei</div></div>
|
|
<div class="stat"><div class="n used" id="stat-used">—</div><div class="l">Zugewiesen</div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-h">Neue Lizenzen generieren</div>
|
|
<div style="display:flex;gap:.5rem;align-items:flex-end;flex-wrap:wrap">
|
|
<div>
|
|
<label style="font-size:.65rem;font-weight:600;color:#4a4a4a;display:block">Anzahl</label>
|
|
<input type="number" id="gen-count" value="100" min="1" max="1000" style="width:80px;padding:.35rem .5rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.78rem">
|
|
</div>
|
|
<div>
|
|
<label style="font-size:.65rem;font-weight:600;color:#4a4a4a;display:block">Jahr</label>
|
|
<input type="number" id="gen-year" value="2026" min="2025" max="2035" style="width:80px;padding:.35rem .5rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.78rem">
|
|
</div>
|
|
<button class="btn" onclick="generate()">Generieren</button>
|
|
</div>
|
|
<div id="gen-msg"></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-h">Lizenzen</div>
|
|
<div class="filter-bar">
|
|
<select id="filter-status" onchange="loadLicenses()">
|
|
<option value="">Alle</option>
|
|
<option value="free">Frei</option>
|
|
<option value="used">Zugewiesen</option>
|
|
</select>
|
|
<button class="btn btn-outline" onclick="exportFree()">Freie exportieren (CSV)</button>
|
|
</div>
|
|
<div id="license-table" style="max-height:500px;overflow-y:auto"></div>
|
|
<div style="display:flex;gap:.4rem;margin-top:.5rem">
|
|
<button class="btn btn-outline" id="btn-prev" onclick="prevPage()" disabled>← Zurück</button>
|
|
<span style="font-size:.68rem;color:#8a8a8a;padding:.35rem" id="page-info"></span>
|
|
<button class="btn btn-outline" id="btn-next" onclick="nextPage()">Weiter →</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
var BASE = (window.__GGS__?.baseUrl) || '';
|
|
if (!BASE) { var p = location.pathname, i = p.indexOf('/geograsim'); if (i !== -1) { var rest = p.substring(i + '/geograsim'.length); BASE = rest.indexOf('/App/') === 0 ? p.substring(0, i) + '/geograsim/App' : p.substring(0, i) + '/geograsim'; } }
|
|
|
|
var page = 0, pageSize = 100;
|
|
|
|
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();
|
|
}
|
|
|
|
async function loadLicenses() {
|
|
var status = document.getElementById('filter-status').value;
|
|
var url = 'licenses?admin=1&limit='+pageSize+'&offset='+(page*pageSize);
|
|
if (status) url += '&status=' + status;
|
|
var r = await api(url);
|
|
if (r.error) return;
|
|
|
|
document.getElementById('stat-total').textContent = r.total;
|
|
document.getElementById('stat-free').textContent = r.free;
|
|
document.getElementById('stat-used').textContent = r.used;
|
|
|
|
var html = '<table><tr><th>Code</th><th>Schuljahr</th><th>Status</th><th>Schüler*in</th><th>Lehrperson</th></tr>';
|
|
r.licenses.forEach(function(l) {
|
|
var st = l.student_id ? '<span class="used">zugewiesen</span>' : '<span class="free">frei</span>';
|
|
html += '<tr>';
|
|
html += '<td class="code">'+l.code+'</td>';
|
|
html += '<td>'+l.school_year+'</td>';
|
|
html += '<td>'+st+'</td>';
|
|
html += '<td>'+(l.student_name||'—')+'</td>';
|
|
html += '<td>'+(l.teacher_name||'—')+'</td>';
|
|
html += '</tr>';
|
|
});
|
|
html += '</table>';
|
|
document.getElementById('license-table').innerHTML = html;
|
|
|
|
var totalPages = Math.ceil(r.total / pageSize);
|
|
document.getElementById('page-info').textContent = 'Seite '+(page+1)+' von '+totalPages;
|
|
document.getElementById('btn-prev').disabled = page === 0;
|
|
document.getElementById('btn-next').disabled = page >= totalPages - 1;
|
|
}
|
|
|
|
function prevPage() { if(page>0){page--;loadLicenses();} }
|
|
function nextPage() { page++; loadLicenses(); }
|
|
|
|
async function generate() {
|
|
var count = parseInt(document.getElementById('gen-count').value) || 100;
|
|
var year = parseInt(document.getElementById('gen-year').value) || 2026;
|
|
var r = await api('licenses', {action:'generate', count:count, year:year});
|
|
if (r.error) { document.getElementById('gen-msg').innerHTML = '<div class="msg" style="background:#f2ddd8;color:#c07a6b">'+r.error+'</div>'; return; }
|
|
document.getElementById('gen-msg').innerHTML = '<div class="msg ok">'+r.generated+' Lizenzen generiert.</div>';
|
|
loadLicenses();
|
|
}
|
|
|
|
function exportFree() {
|
|
api('licenses?admin=1&status=free&limit=10000').then(function(r) {
|
|
if (!r.licenses) return;
|
|
var csv = 'Code;Schuljahr\n';
|
|
r.licenses.forEach(function(l) { csv += l.code + ';' + l.school_year + '\n'; });
|
|
var blob = new Blob([csv], {type:'text/csv'});
|
|
var a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = 'lizenzen_frei.csv';
|
|
a.click();
|
|
});
|
|
}
|
|
|
|
// Admin auth check (2FA required)
|
|
fetch(BASE + '/api/admin', {method:'POST', headers:{'Content-Type':'application/json'}, credentials:'same-origin', body:JSON.stringify({action:'status'})})
|
|
.then(function(r){return r.json()})
|
|
.then(function(r) {
|
|
if (!r.authenticated) { location.href = 'admin.html'; return; }
|
|
loadLicenses();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|