Files
geograsim/App/admin-levels.html
T

361 lines
21 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>GeoGraSim — Administration</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/vendor/leaflet/leaflet.css">
<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:.5rem 1rem;display:flex;align-items:center;gap:.6rem;border-bottom:1px solid rgba(0,0,0,.06);flex-wrap:wrap;position:sticky;top:0;z-index:100}
.topbar h1{font-size:.9rem;font-weight:800;color:#1f4e5a;display:flex;align-items:center;gap:.5rem}
.topbar h1 img{height:28px}
.topbar .spacer{flex:1}
.nav-btn{padding:.35rem .7rem;border:1.5px solid rgba(0,0,0,.08);border-radius:6px;background:#fff;font-size:.7rem;font-weight:600;cursor:pointer;color:#4a4a4a;text-decoration:none}
.nav-btn:hover{background:#dae8ec;color:#4a7c8a}
.nav-btn.on{background:#1f4e5a;color:#fff;border-color:#1f4e5a}
.wrap{max-width:1100px;margin:0 auto;padding: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}
.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}
select,input[type=text],input[type=number]{padding:.35rem .5rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.75rem;font-family:inherit}
/* Sim Cards Grid */
.sim-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.8rem;margin-bottom:1.5rem}
.sim-card{background:#fff;border-radius:12px;overflow:hidden;cursor:pointer;border:2px solid rgba(0,0,0,.05);transition:all .2s}
.sim-card:hover{border-color:#4a7c8a;transform:translateY(-2px);box-shadow:0 6px 20px rgba(74,124,138,.1)}
.sim-card.active{border-color:#1f4e5a;box-shadow:0 0 0 3px rgba(31,78,90,.15)}
.sim-card img{width:100%;height:100px;object-fit:cover}
.sim-card-body{padding:.6rem}
.sim-card-body h3{font-size:.78rem;font-weight:700;color:#1f4e5a}
.sim-card-body p{font-size:.6rem;color:#8a8a8a}
/* Slider rows */
.slider-row{display:grid;grid-template-columns:140px 1fr 40px;gap:8px;align-items:center;margin-bottom:.4rem;font-size:.75rem}
.slider-row label{color:#4a4a4a}
.slider-row input[type=range]{width:100%;accent-color:#4a7c8a}
.slider-row .sval{text-align:right;font-weight:700;color:#1f4e5a;font-variant-numeric:tabular-nums}
/* Waypoint rows */
.wp-row{display:grid;grid-template-columns:100px 1fr 80px 80px 70px 60px 110px 50px;gap:5px;align-items:center;padding:.4rem .2rem;border-bottom:1px solid rgba(0,0,0,.03);font-size:.7rem}
.wp-row:hover{background:#f0f8fa}
.wp-header{font-weight:700;font-size:.58rem;color:#8a8a8a;text-transform:uppercase;border-bottom:2px solid #dae8ec}
.wp-type{font-size:.58rem;padding:1px 5px;border-radius:3px;font-weight:600}
.wp-type-base{background:#f2eacc;color:#8a7020}
.wp-type-city{background:#dae8ec;color:#4a7c8a}
.wp-type-village{background:#f0eeea;color:#666}
.wp-type-peak{background:#dceadd;color:#3a6b3e}
.wp-type-target{background:#f2ddd8;color:#8a3a2a}
.wp-map-mini{width:110px;height:65px;border-radius:5px;border:1px solid rgba(0,0,0,.06)}
/* Level cards */
.level-card{border:1.5px solid rgba(0,0,0,.06);border-radius:8px;padding:.7rem;margin-bottom:.4rem}
.level-card h4{font-size:.78rem;font-weight:700;color:#1f4e5a}
.level-card .meta{font-size:.62rem;color:#8a8a8a}
/* Modal */
.modal{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:1000}
.modal-card{background:#fff;border-radius:14px;padding:1.2rem;max-width:600px;width:95%;max-height:90vh;overflow-y:auto}
.modal-card h3{font-size:.95rem;font-weight:800;color:#1f4e5a;margin-bottom:.6rem}
.modal-card label{display:block;font-size:.65rem;font-weight:600;color:#4a4a4a;margin-top:.5rem}
.modal-card input,.modal-card select{width:100%}
#editMap{height:400px;border-radius:8px;margin-top:.5rem}
.hidden{display:none}
/* Tab content */
.tab-content{display:none}
.tab-content.active{display:block}
</style>
</head>
<body>
<div class="topbar">
<h1><img src="assets/img/geograsim_t.svg" alt="">Administration</h1>
<span class="spacer"></span>
<a href="#" class="nav-btn on" onclick="showTab('sims')">Simulationen</a>
<a href="#" class="nav-btn" onclick="showTab('waypoints')">📍 Waypoints</a>
<a href="heli" class="nav-btn" target="_blank">🗺️ Gesamtkarte</a>
<a href="admin-modules.html" class="nav-btn">🧩 Module</a>
<a href="admin-licenses.html" class="nav-btn">🔑 Lizenzen</a>
<a href="admin-styleguide.html" class="nav-btn">🎨 Styleguide</a>
<a href="admin-accounts.html" class="nav-btn">👤 Admins</a>
<a href="admin.html" class="nav-btn">Logout</a>
</div>
<div class="wrap">
<!-- TAB: Simulationen -->
<div class="tab-content active" id="tab-sims">
<div class="card">
<div class="card-h">Simulationen — Wähle ein Modul zur Konfiguration</div>
<div class="sim-grid" id="simGrid"></div>
</div>
<div id="simDetail"></div>
</div>
<!-- TAB: Waypoints -->
<div class="tab-content" id="tab-waypoints">
<div class="card">
<div class="card-h">📍 Waypoints — Koordinaten (<span id="wp-count">0</span>)</div>
<div style="display:flex;gap:.4rem;margin-bottom:.5rem;flex-wrap:wrap">
<select id="wp-filter-region" onchange="loadWaypoints()"><option value="">Alle</option><option value="vorarlberg">Vorarlberg</option><option value="tirol">Tirol</option></select>
<select id="wp-filter-type" onchange="loadWaypoints()"><option value="">Alle Typen</option><option value="base">Basis</option><option value="city">Stadt</option><option value="village">Dorf</option><option value="peak">Gipfel</option><option value="target">Einsatzort</option></select>
<button class="btn" onclick="openAdd()">+ Waypoint</button>
</div>
<div class="wp-row wp-header"><div>Key</div><div>Name</div><div>Lat</div><div>Lon</div><div>Region</div><div>Typ</div><div>Karte</div><div></div></div>
<div id="wp-list"></div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal hidden" id="editModal">
<div class="modal-card">
<h3 id="editTitle">Waypoint bearbeiten</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem">
<div><label>Key</label><input type="text" id="e-key" placeholder="bregenz"></div>
<div><label>Name</label><input type="text" id="e-name" placeholder="Bregenz"></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:.5rem">
<div><label>Lat</label><input type="number" id="e-lat" step="0.00001"></div>
<div><label>Lon</label><input type="number" id="e-lon" step="0.00001"></div>
<div><label>Region</label><select id="e-region"><option value="vorarlberg">Vorarlberg</option><option value="tirol">Tirol</option></select></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem">
<div><label>Typ</label><select id="e-type"><option value="village">Dorf</option><option value="city">Stadt</option><option value="base">Basis</option><option value="peak">Gipfel</option><option value="target">Einsatzort</option><option value="pass">Pass</option></select></div>
<div><label>Info</label><input type="text" id="e-info" placeholder="Beschreibung"></div>
</div>
<p style="font-size:.68rem;color:#8a8a8a;margin-top:.4rem">Klicke auf die Karte um die Position zu setzen:</p>
<div id="editMap"></div>
<div style="display:flex;gap:.4rem;margin-top:.6rem">
<button class="btn" onclick="saveEdit()">Speichern</button>
<button class="btn btn-outline" onclick="closeEdit()">Abbrechen</button>
</div>
</div>
</div>
<script src="assets/vendor/leaflet/leaflet.js"></script>
<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'; } }
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'};
return (await fetch(BASE + '/api/' + endpoint, opts)).json();
}
// ==========================================
// TABS
// ==========================================
function showTab(tab) {
document.querySelectorAll('.tab-content').forEach(function(el) { el.classList.toggle('active', el.id === 'tab-' + tab); });
document.querySelectorAll('.nav-btn').forEach(function(b) { b.classList.remove('on'); });
event.target.classList.add('on');
if (tab === 'waypoints') loadWaypoints();
}
// ==========================================
// SIMULATIONEN
// ==========================================
var SIMS = [
{id:'klima', name:'Klimawächter V2', icon:'🌍', img:'assets/img/card-climate.webp',
params:{
start_budget:{min:100,max:2000,def:600,label:'Start-Budget (Mio €)'},
start_population:{min:1000,max:20000,def:6000,label:'Start-Bevölkerung'},
income_per_10k:{min:50,max:500,def:220,label:'Einnahmen pro 10.000 Bürger'},
budget_loss:{min:-2000,max:0,def:-500,label:'Pleite-Grenze (Mio €, negativ)'},
event_frequency:{min:0,max:1,def:0.3,step:0.1,label:'Ereignis-Häufigkeit (01)'},
time_limit:{min:30,max:150,def:75,label:'Simulationsdauer (Runden)'}
}},
{id:'sim-05', name:'Klimawächter 2D (V1)', icon:'🌡️', img:'assets/img/card-climate.webp',
params:{start_co2:{min:300,max:600,def:425,label:'Start-CO₂ (ppm)'},start_temp:{min:14,max:18,def:15.2,label:'Start-Temperatur (°C)'},budget:{min:200,max:2000,def:950,label:'Budget (Mrd)'},income:{min:100,max:500,def:290,label:'Einkommen/Jahr'},damage_mult:{min:0.5,max:3,def:1,label:'Schadensmultiplikator'}}},
{id:'sim-05-3d', name:'Klimawächter 3D (V1)', icon:'🌊', img:'assets/img/card-climate.webp',
params:{start_co2:{min:300,max:600,def:425,label:'Start-CO₂ (ppm)'},start_temp:{min:14,max:18,def:15.2,label:'Start-Temperatur (°C)'},budget:{min:200,max:2000,def:950,label:'Budget (Mrd)'},income:{min:100,max:500,def:290,label:'Einkommen/Jahr'}}},
{id:'sim-07', name:'Erdbeben', icon:'🌋', img:'assets/img/card-tectonic.webp',
params:{quake_freq:{min:1,max:10,def:3,label:'Erdbeben-Häufigkeit'},build_cost:{min:50,max:500,def:200,label:'Baukosten'},population:{min:1000,max:50000,def:10000,label:'Bevölkerung'}}},
{id:'sim-09', name:'Energiemix', icon:'⚡', img:'assets/img/card-energy.webp',
params:{demand_growth:{min:0,max:5,def:2,label:'Nachfrage-Wachstum (%/Jahr)'},co2_target:{min:20,max:80,def:50,label:'CO₂-Reduktionsziel (%)'},budget:{min:500,max:5000,def:2000,label:'Budget (Mio)'}}},
{id:'sim-10', name:'Lieferketten', icon:'👕', img:'assets/img/card-globe.webp',
params:{price_weight:{min:0,max:100,def:40,label:'Preis-Gewichtung (%)'},fair_weight:{min:0,max:100,def:30,label:'Fairness-Gewichtung (%)'},co2_weight:{min:0,max:100,def:30,label:'CO₂-Gewichtung (%)'}}},
{id:'sim-11', name:'Regenwald', icon:'🌴', img:'assets/img/card-climate.webp', params:{}},
{id:'sim-12', name:'Flussmanagement', icon:'🏞️', img:'assets/img/card-fluss.webp',
params:{flood_freq:{min:1,max:10,def:3,label:'Hochwasser-Häufigkeit'},budget:{min:500,max:5000,def:2000,label:'Budget'},bio_weight:{min:0,max:100,def:40,label:'Biodiversitäts-Gewichtung (%)'}}},
{id:'sim-13', name:'Stadt & Raumplanung', icon:'🏗️', img:'assets/img/card-city.webp',
params:{rounds:{min:5,max:20,def:10,label:'Runden'},budget_per_round:{min:100,max:1000,def:300,label:'Budget/Runde'},grid_size:{min:6,max:12,def:8,label:'Raster-Größe'}}},
{id:'sim-14', name:'Helikopter-Navigation', icon:'🚁', img:'assets/img/card-heli.webp',
params:{houses:{min:0,max:14,def:8,label:'Häuser'},trees_tall:{min:0,max:14,def:6,label:'Hohe Bäume'},flocks:{min:0,max:8,def:2,label:'Vogelschwärme'},airliners:{min:0,max:3,def:1,label:'Verkehrsflugzeuge'},sportplanes:{min:0,max:4,def:2,label:'Sportflieger'},storms:{min:0,max:4,def:1,label:'Gewitterzellen'}}},
];
var selectedSim = null;
function renderSimGrid() {
var html = '';
SIMS.forEach(function(s) {
var cls = selectedSim === s.id ? ' active' : '';
html += '<div class="sim-card'+cls+'" onclick="selectSim(\''+s.id+'\')">';
html += '<img src="'+s.img+'" alt="">';
html += '<div class="sim-card-body"><h3>'+s.icon+' '+s.name+'</h3><p>'+Object.keys(s.params).length+' Parameter</p></div>';
html += '</div>';
});
document.getElementById('simGrid').innerHTML = html;
}
async function selectSim(id) {
selectedSim = id;
renderSimGrid();
var sim = SIMS.find(function(s){return s.id===id;});
if (!sim) return;
// Bestehende Levels laden
var levels = await api('levels?game_id=' + id);
if (levels.error) levels = [];
var html = '<div class="card"><div class="card-h">'+sim.icon+' '+sim.name+' — Level-Konfiguration</div>';
var params = sim.params || {};
var paramKeys = Object.keys(params);
// Für jedes der 3 Standard-Levels (Leicht/Mittel/Schwer) alle Schieberegler anzeigen
var levelNames = ['Leicht','Mittel','Schwer'];
var levelColors = ['#5a8a5e','#c4a35a','#c85c4a'];
levelNames.forEach(function(lName, li) {
var existingLevel = levels.find(function(l){return l.level_name===lName;});
var existingParams = existingLevel ? existingLevel.params : {};
var levelId = existingLevel ? existingLevel.id : 0;
html += '<div style="border-left:4px solid '+levelColors[li]+';padding:.6rem .8rem;margin-bottom:.6rem;background:rgba(0,0,0,.01);border-radius:0 8px 8px 0">';
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem">';
html += '<strong style="font-size:.85rem;color:'+levelColors[li]+'">'+lName+'</strong>';
html += '<button class="btn" style="font-size:.62rem;padding:.2rem .6rem" onclick="saveLevel(\''+id+'\',\''+lName+'\','+li+','+levelId+')">Speichern</button>';
html += '</div>';
paramKeys.forEach(function(key) {
var p = params[key];
var val = existingParams[key] !== undefined ? existingParams[key] : p.def;
var sliderId = 'sl-'+li+'-'+key;
var valId = 'sv-'+li+'-'+key;
html += '<div class="slider-row">';
html += '<label>'+p.label+'</label>';
html += '<input type="range" min="'+p.min+'" max="'+p.max+'" value="'+val+'" step="'+(p.max-p.min > 10 ? 1 : 0.1)+'" id="'+sliderId+'" oninput="document.getElementById(\''+valId+'\').textContent=this.value">';
html += '<span class="sval" id="'+valId+'">'+val+'</span>';
html += '</div>';
});
html += '</div>';
});
html += '</div>';
document.getElementById('simDetail').innerHTML = html;
}
async function saveLevel(gameId, levelName, levelIndex, existingId) {
var sim = SIMS.find(function(s){return s.id===gameId;});
var params = {};
Object.keys(sim.params||{}).forEach(function(key) {
var el = document.getElementById('sl-'+levelIndex+'-'+key);
if (el) params[key] = parseFloat(el.value);
});
var r = await api('levels', {action:'save', gameId:gameId, levelId:existingId||undefined, levelName:levelName, scenario:'standard', params:params, sortOrder:levelIndex+1});
if (r.error) { alert(r.error); return; }
alert(levelName + ' gespeichert!');
selectSim(gameId);
}
async function deleteLevel(id, gameId) {
if (!confirm('Level löschen?')) return;
await api('levels', {action:'delete', levelId:id});
selectSim(gameId);
}
// ==========================================
// WAYPOINTS
// ==========================================
var allWaypoints = [];
async function loadWaypoints() {
var region = document.getElementById('wp-filter-region').value;
var type = document.getElementById('wp-filter-type').value;
var url = 'waypoints?';
if (region) url += 'region=' + region + '&';
if (type) url += 'type=' + type;
var wps = await api(url);
if (wps.error) wps = [];
allWaypoints = wps;
document.getElementById('wp-count').textContent = wps.length;
var html = '';
wps.forEach(function(w) {
var typeClass = 'wp-type-' + w.wp_type;
var typeLabel = {base:'Basis',city:'Stadt',village:'Dorf',peak:'Gipfel',target:'Ziel',pass:'Pass'}[w.wp_type] || w.wp_type;
var lat = parseFloat(w.lat).toFixed(5), lon = parseFloat(w.lon).toFixed(5);
html += '<div class="wp-row">';
html += '<div style="font-family:monospace;font-size:.65rem;color:#4a7c8a;font-weight:600">'+w.wp_key+'</div>';
html += '<div><strong>'+w.name+'</strong>'+(w.info?'<br><span style="font-size:.58rem;color:#8a8a8a">'+w.info+'</span>':'')+'</div>';
html += '<div style="font-variant-numeric:tabular-nums;font-size:.65rem">'+lat+'</div>';
html += '<div style="font-variant-numeric:tabular-nums;font-size:.65rem">'+lon+'</div>';
html += '<div style="font-size:.58rem;color:#8a8a8a">'+w.region+'</div>';
html += '<div><span class="wp-type '+typeClass+'">'+typeLabel+'</span></div>';
html += '<div><img class="wp-map-mini" src="https://staticmap.openstreetmap.de/staticmap.php?center='+lat+','+lon+'&zoom=13&size=110x65&markers='+lat+','+lon+',ol-marker" loading="lazy"></div>';
html += '<div><button style="padding:.2rem .4rem;font-size:.6rem;border:1px solid rgba(0,0,0,.08);border-radius:4px;background:#fff;cursor:pointer" onclick="openEdit(\''+w.wp_key+'\')">✏️</button></div>';
html += '</div>';
});
document.getElementById('wp-list').innerHTML = html;
}
var editMap = null, editMarker = null;
function openEdit(key) {
var w = allWaypoints.find(function(x){return x.wp_key===key;});
if (!w) return;
document.getElementById('editTitle').textContent = 'Bearbeiten: ' + w.name;
document.getElementById('e-key').value = w.wp_key;
document.getElementById('e-name').value = w.name;
document.getElementById('e-lat').value = parseFloat(w.lat);
document.getElementById('e-lon').value = parseFloat(w.lon);
document.getElementById('e-region').value = w.region;
document.getElementById('e-type').value = w.wp_type;
document.getElementById('e-info').value = w.info || '';
document.getElementById('editModal').classList.remove('hidden');
setTimeout(function() {
if (!editMap) {
editMap = L.map('editMap').setView([w.lat, w.lon], 14);
L.tileLayer('php/tile-proxy.php?z={z}&x={x}&y={y}&p=osm',{attribution:'© OpenStreetMap',maxZoom:18}).addTo(editMap);
editMap.on('click',function(e){document.getElementById('e-lat').value=e.latlng.lat.toFixed(5);document.getElementById('e-lon').value=e.latlng.lng.toFixed(5);if(editMarker)editMap.removeLayer(editMarker);editMarker=L.marker(e.latlng).addTo(editMap);});
} else { editMap.setView([w.lat,w.lon],14); editMap.invalidateSize(); }
if(editMarker)editMap.removeLayer(editMarker);
editMarker=L.marker([w.lat,w.lon]).addTo(editMap);
}, 200);
}
function openAdd() {
document.getElementById('editTitle').textContent = 'Neuer Waypoint';
['e-key','e-name','e-lat','e-lon','e-info'].forEach(function(id){document.getElementById(id).value='';});
document.getElementById('editModal').classList.remove('hidden');
setTimeout(function(){if(!editMap){editMap=L.map('editMap').setView([47.3,10.5],9);L.tileLayer('php/tile-proxy.php?z={z}&x={x}&y={y}&p=osm',{attribution:'© OpenStreetMap',maxZoom:18}).addTo(editMap);editMap.on('click',function(e){document.getElementById('e-lat').value=e.latlng.lat.toFixed(5);document.getElementById('e-lon').value=e.latlng.lng.toFixed(5);if(editMarker)editMap.removeLayer(editMarker);editMarker=L.marker(e.latlng).addTo(editMap);});}else{editMap.setView([47.3,10.5],9);editMap.invalidateSize();}if(editMarker){editMap.removeLayer(editMarker);editMarker=null;}},200);
}
function closeEdit(){document.getElementById('editModal').classList.add('hidden');}
async function saveEdit() {
var r = await api('waypoints',{action:'save',wpKey:document.getElementById('e-key').value.trim().toLowerCase().replace(/[^a-z0-9_]/g,''),name:document.getElementById('e-name').value.trim(),lat:parseFloat(document.getElementById('e-lat').value),lon:parseFloat(document.getElementById('e-lon').value),region:document.getElementById('e-region').value,wpType:document.getElementById('e-type').value,info:document.getElementById('e-info').value.trim()});
if(r.error){alert(r.error);return;}
closeEdit(); loadWaypoints();
}
// ==========================================
// INIT
// ==========================================
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;}
renderSimGrid();
});
</script>
</body>
</html>