504b0098a6
- badges.js: 5 Basis-Abzeichen + je Sim 1 thematisches (Vier-Jahreszeiten, Sterne-Koch, Alpen-Pilot …), 19 sauber generierte Flat-Emblem-Icons (keine Emojis) - Erfolge-Tab: Sammlung mit Bild-Icons, gesperrte grau = Ziele; alte Emoji-Badges raus - Lehrer-Matrix: neue Spalte „Abzeichen" (freigeschaltet von gesamt) pro Schüler:in Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
859 lines
47 KiB
HTML
859 lines
47 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 — Meine Simulationen</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;color:#1a1a1a;min-height:100vh}
|
||
|
||
.topbar{background:#fff;padding:.55rem 1.2rem;display:flex;align-items:center;gap:.7rem;border-bottom:1px solid rgba(0,0,0,.06);position:sticky;top:0;z-index:10}
|
||
.topbar .logo{height:28px}
|
||
.topbar .spacer{flex:1}
|
||
.topbar .user-pill{display:flex;align-items:center;gap:.35rem;padding:.15rem .6rem .15rem .15rem;background:#dae8ec;border-radius:99px;font-size:.72rem;font-weight:600;color:#4a7c8a;text-decoration:none;cursor:pointer}
|
||
.topbar .user-pill:hover{background:#c8dce2}
|
||
.topbar .user-pill .emoji{font-size:1.2rem;width:28px;height:28px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:#fff;overflow:hidden}
|
||
.topbar .user-pill .emoji img{width:100%;height:100%;object-fit:cover;display:block}
|
||
.topbar .btn-sm{padding:.25rem .55rem;border:1px solid rgba(0,0,0,.08);border-radius:6px;background:#fff;font-size:.65rem;font-weight:600;cursor:pointer;color:#4a4a4a;text-decoration:none}
|
||
.topbar .btn-sm:hover{background:#dae8ec;color:#4a7c8a}
|
||
|
||
.wrap{max-width:900px;margin:0 auto;padding:1.2rem 1rem}
|
||
|
||
/* Tabs */
|
||
.tabs{display:flex;gap:0;margin-bottom:1.2rem;border-radius:10px;overflow:hidden;border:1.5px solid rgba(0,0,0,.06);background:#fff}
|
||
.tab{flex:1;padding:.6rem;text-align:center;font-size:.78rem;font-weight:700;cursor:pointer;color:#8a8a8a;transition:all .15s;border:none;background:transparent;text-decoration:none}
|
||
.tab.on{background:#4a7c8a;color:#fff}
|
||
|
||
/* Module grid */
|
||
.mod-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem}
|
||
.mod-card{background:#fff;border-radius:12px;overflow:hidden;border:1.5px solid rgba(0,0,0,.05);transition:all .25s;display:flex;flex-direction:column}
|
||
.mod-card.spielbar{cursor:default}
|
||
.mod-card.locked{opacity:.55}
|
||
.mod-card.coming{opacity:.7}
|
||
.mod-card.coming .mod-img{filter:grayscale(.5)}
|
||
.coming-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:1rem;background:rgba(255,255,255,.92);border-radius:8px;padding:.3rem .55rem;font-weight:700;color:#8a6a3a;white-space:nowrap}
|
||
.coming-divider{margin:1.4rem 0 .8rem;font-size:.74rem;font-weight:700;color:#8a8a8a;text-transform:uppercase;letter-spacing:.06em;padding:.4rem .2rem;border-top:1px solid rgba(0,0,0,.06)}
|
||
.mod-img{height:210px;background:linear-gradient(135deg,#dae8ec,#c8dce2);display:flex;align-items:center;justify-content:center;font-size:2.5rem;position:relative;overflow:hidden}
|
||
.mod-img img{width:100%;height:100%;object-fit:cover;transition:transform .35s}
|
||
.mod-card.spielbar:hover .mod-img img{transform:scale(1.04)}
|
||
.mod-body{padding:.7rem .8rem .9rem;flex:1;display:flex;flex-direction:column}
|
||
.mod-body h3{font-size:.92rem;font-weight:700;margin-bottom:.25rem;line-height:1.2}
|
||
.mod-body p{font-size:.72rem;color:#4a4a4a;margin-bottom:.6rem;line-height:1.4;flex:1}
|
||
.mod-actions{display:flex;flex-wrap:wrap;gap:.35rem;margin-top:auto}
|
||
.mod-btn{display:inline-flex;align-items:center;gap:.25rem;padding:.42rem .7rem;border-radius:7px;font-size:.7rem;font-weight:600;text-decoration:none;border:1.5px solid rgba(0,0,0,.08);background:#f7f6f3;color:#4a4a4a;transition:all .15s;min-height:32px;white-space:nowrap}
|
||
.mod-btn:hover{background:#fff;border-color:rgba(74,124,138,.4);color:#1a1a1a}
|
||
.mod-btn.primary{background:#4a7c8a;color:#fff;border-color:#4a7c8a;flex:1;justify-content:center}
|
||
.mod-btn.primary:hover{background:#3d6671;color:#fff}
|
||
.lock-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:1.5rem;background:rgba(255,255,255,.9);border-radius:8px;padding:.3rem .6rem}
|
||
|
||
/* Erfolge */
|
||
.badge-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:.6rem}
|
||
.badge{background:#fff;border-radius:10px;padding:.8rem;text-align:center;border:1.5px solid rgba(0,0,0,.04)}
|
||
.badge .icon{font-size:2rem;margin-bottom:.3rem}
|
||
.badge .icon img{width:48px;height:48px;object-fit:contain;display:block;margin:0 auto}
|
||
.badge.locked-b .icon img{filter:grayscale(.9)}
|
||
.badge .name{font-size:.72rem;font-weight:700}
|
||
.badge .desc{font-size:.6rem;color:#8a8a8a}
|
||
.badge.earned{border-color:#4a7c8a;background:linear-gradient(135deg,#f0f8fa,#fff)}
|
||
.badge.locked-b{opacity:.4}
|
||
.badge .desc{min-height:1.6em}
|
||
.sim-coll{margin-bottom:1.1rem}
|
||
.sim-coll-head{display:flex;align-items:center;gap:.5rem;margin:.5rem 0 .45rem;font-weight:700;font-size:.82rem}
|
||
.sim-coll-head .sc-ic{font-size:1.3rem}
|
||
.sim-coll-head .sc-t{flex:1}
|
||
.sim-coll-head .sc-stars{color:#e0a838;letter-spacing:1px;font-size:.82rem}
|
||
.sim-coll-head .sc-count{font-size:.68rem;color:#8a8a8a;background:#f0eeea;border-radius:99px;padding:.12rem .55rem;font-weight:700}
|
||
/* Erfolgs-Liste pro Modul */
|
||
.ach-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:.6rem;margin-bottom:1.2rem}
|
||
.ach-summary .card{background:#fff;border-radius:10px;padding:.9rem .8rem;text-align:center;border:1.5px solid rgba(0,0,0,.05)}
|
||
.ach-summary .n{font-size:1.6rem;font-weight:900;color:#4a7c8a;line-height:1}
|
||
.ach-summary .l{font-size:.62rem;color:#8a8a8a;margin-top:.25rem;text-transform:uppercase;letter-spacing:.04em}
|
||
.ach-modules{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1.4rem}
|
||
.ach-mod{background:#fff;border-radius:10px;padding:.7rem .9rem;display:flex;align-items:center;gap:.7rem;border:1.5px solid rgba(0,0,0,.05)}
|
||
.ach-mod .ic{font-size:1.5rem;width:36px;text-align:center;flex-shrink:0}
|
||
.ach-mod .info{flex:1;min-width:0}
|
||
.ach-mod .info h4{font-size:.85rem;font-weight:700;margin:0 0 .15rem 0}
|
||
.ach-mod .info .meta{font-size:.65rem;color:#8a8a8a}
|
||
.ach-mod .stats{display:flex;gap:.5rem;flex-shrink:0}
|
||
.ach-mod .stat-pill{background:#f0eeea;color:#4a4a4a;padding:.2rem .55rem;border-radius:6px;font-size:.62rem;font-weight:700}
|
||
.ach-mod .stat-pill.stars{background:#fff4d6;color:#a37800}
|
||
.ach-mod .stat-pill.lvl{background:#dae8ec;color:#4a7c8a}
|
||
.ach-empty{background:#fff;border-radius:10px;padding:1.5rem 1rem;text-align:center;border:1.5px dashed rgba(0,0,0,.1);color:#8a8a8a;font-size:.78rem}
|
||
|
||
/* Profil-Karte */
|
||
.profile-card{background:#fff;border-radius:12px;padding:1.5rem;text-align:center;border:1.5px solid rgba(0,0,0,.04)}
|
||
.profile-avatar{font-size:3.5rem;cursor:pointer;transition:transform .15s}
|
||
.profile-avatar:hover{transform:scale(1.15)}
|
||
.profile-name{font-size:1.1rem;font-weight:800;margin-top:.3rem}
|
||
.profile-class{font-size:.75rem;color:#8a8a8a}
|
||
.profile-stats{display:flex;justify-content:center;gap:1.5rem;margin-top:.8rem}
|
||
.profile-stat .n{font-size:1.3rem;font-weight:900;color:#4a7c8a}
|
||
.profile-stat .l{font-size:.6rem;color:#8a8a8a}
|
||
|
||
.hidden{display:none}
|
||
@media(max-width:550px){.mod-grid{grid-template-columns:1fr}.badge-grid{grid-template-columns:repeat(2,1fr)}}
|
||
</style>
|
||
<script src="assets/js/badges.js"></script>
|
||
</head>
|
||
<body>
|
||
<div class="topbar">
|
||
<a href="schueler.html" aria-label="Cockpit"><img src="assets/img/geograsim_t.svg" alt="GeoGraSim" class="logo"></a>
|
||
<span class="spacer"></span>
|
||
<a href="profil.html" class="user-pill" id="user-pill"><span class="emoji">🧑🎓</span><span id="user-name">…</span></a>
|
||
<button class="btn-sm" onclick="logout()">Abmelden</button>
|
||
</div>
|
||
|
||
<div class="wrap">
|
||
<!-- Tabs -->
|
||
<div class="tabs">
|
||
<div class="tab on" data-tab="modules" onclick="switchTab('modules')">Module</div>
|
||
<div class="tab" data-tab="achievements" onclick="switchTab('achievements')">Erfolge</div>
|
||
<a class="tab" href="buch">Schulbuch</a>
|
||
<div class="tab" data-tab="profile" onclick="switchTab('profile')">Profil</div>
|
||
</div>
|
||
|
||
<div id="tab-modules"></div>
|
||
<div id="tab-achievements" class="hidden"></div>
|
||
<div id="tab-profile" class="hidden"></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 profile = null;
|
||
var modules = [];
|
||
|
||
// Module kommen aus /api/modules?student=1 (inkl. play_url, icon, name, mode,
|
||
// card_image, desc). Bild-Pfad ist ein Dateiname wie "card-farmer.webp" und wird
|
||
// gegen "assets/img/" relativ zur App-Basis aufgeloest.
|
||
|
||
var BADGES = [
|
||
{id:'first_sim', icon:'🎯', name:'Erste Simulation', desc:'Eine Simulation abgeschlossen'},
|
||
{id:'three_sims', icon:'🏅', name:'Dreifach', desc:'Drei verschiedene Simulationen gespielt'},
|
||
{id:'all_sims', icon:'🏆', name:'Komplett', desc:'Alle Module abgeschlossen'},
|
||
{id:'high_score', icon:'⭐', name:'Spitzenscore', desc:'Score über 80 in einer Simulation'},
|
||
{id:'speed_run', icon:'⚡', name:'Blitzschnell', desc:'Eine Simulation in unter 10 Minuten'},
|
||
{id:'explorer', icon:'🧭', name:'Entdecker', desc:'Alle Maßnahmen in einer Simulation ausprobiert'},
|
||
{id:'climate_hero', icon:'🌍', name:'Klimaheld', desc:'Temperaturanstieg unter 1.5°C gehalten'},
|
||
{id:'city_planner', icon:'🏙️', name:'Stadtplaner', desc:'Stadt mit über 90% Zufriedenheit gebaut'},
|
||
];
|
||
|
||
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();
|
||
}
|
||
|
||
function logout() { api('auth', {action:'logout'}).then(function() { location.href = 'index.html'; }); }
|
||
|
||
function switchTab(tab) {
|
||
document.querySelectorAll('.tab[data-tab]').forEach(function(t) {
|
||
t.classList.toggle('on', t.getAttribute('data-tab') === tab);
|
||
});
|
||
document.getElementById('tab-modules').classList.toggle('hidden', tab !== 'modules');
|
||
document.getElementById('tab-achievements').classList.toggle('hidden', tab !== 'achievements');
|
||
document.getElementById('tab-profile').classList.toggle('hidden', tab !== 'profile');
|
||
}
|
||
|
||
async function init() {
|
||
var status = await api('auth', {action:'status'});
|
||
if (status.role !== 'student') { location.href = 'login.html'; return; }
|
||
|
||
document.getElementById('user-name').textContent = status.session?.display_name || 'Schüler';
|
||
|
||
// Load profile
|
||
var pr = await api('profile');
|
||
profile = pr.profile;
|
||
if (profile?.emoji_avatar) {
|
||
var v = profile.emoji_avatar;
|
||
var pillEmoji = document.querySelector('#user-pill .emoji');
|
||
if (typeof v === 'string' && v.indexOf('avatar:') === 0) {
|
||
var slug = v.substring(7);
|
||
pillEmoji.innerHTML = '<img src="assets/img/avatars/thumbs/avatar-' + slug + '.webp" alt="">';
|
||
} else {
|
||
pillEmoji.textContent = v;
|
||
}
|
||
}
|
||
|
||
await renderModules();
|
||
renderAchievements();
|
||
renderProfile();
|
||
|
||
// Hash-Routing: schueler.html#achievements / #profile → entsprechenden Tab aktiv.
|
||
// Profil-Seite verlinkt mit #achievements zurück, damit „Meine Erfolge" direkt
|
||
// landet. Default ohne Hash bleibt „modules".
|
||
var validTabs = {modules:1, achievements:1, profile:1};
|
||
var initialTab = (location.hash || '').replace('#','');
|
||
if (validTabs[initialTab]) switchTab(initialTab);
|
||
window.addEventListener('hashchange', function() {
|
||
var t = (location.hash || '').replace('#','');
|
||
if (validTabs[t]) switchTab(t);
|
||
});
|
||
}
|
||
|
||
function buildModuleUrl(mod) {
|
||
// Bevorzugt play_url aus module_info; Fallback: module_id
|
||
var base = mod.play_url || mod.id;
|
||
// Relativ zum aktuellen Front-Controller-Pfad — Apache-Rewrite macht den Rest
|
||
var url = base.indexOf('/') === 0 ? base : base;
|
||
if (mod.mode === 'teacher_started' && mod.forcedLevel) {
|
||
url += (url.indexOf('?') === -1 ? '?' : '&') + 'level=' + mod.forcedLevel;
|
||
}
|
||
return url;
|
||
}
|
||
|
||
// Module mit Schulbuch-Kapitel (pages/buch-<id>.php). klima-3d teilt sich das Klima-Kapitel.
|
||
var BUCH_KAP = {tourismustal:1,tourismusregion:1,kofferdetektiv:1,busfahrt:1,logistik:1,
|
||
weltkueche:1,farmer:1,klima:1,'klima-3d':'klima',fluss:1,energiemanager:1,heli:1,
|
||
sonnensystem:1,'eu-werkstatt':1};
|
||
function buchKapitel(id){ var t = BUCH_KAP[id]; if (!t) return null; return 'buch-' + (t === 1 ? id : t); }
|
||
|
||
async function renderModules() {
|
||
var html = '';
|
||
var modList = [];
|
||
try {
|
||
modList = await api('modules?student=1');
|
||
if (modList.error) modList = [];
|
||
} catch(e) {}
|
||
|
||
// Lehrer-gesteuerte Auftraege zuerst — erledigte Auftraege fallen automatisch
|
||
// in die regulaere Modul-Liste, damit der Auftrag aus dem Cockpit verschwindet,
|
||
// das Modul aber spielbar bleibt.
|
||
var teacherStarted = modList.filter(function(m) { return m.mode === 'teacher_started' && !m.assignmentCompleted; });
|
||
if (teacherStarted.length > 0) {
|
||
html += '<div style="margin-bottom:1rem">';
|
||
html += '<p style="font-size:.78rem;font-weight:700;color:#e8833a;margin-bottom:.4rem">📋 Aufträge von deiner Lehrperson</p>';
|
||
teacherStarted.forEach(function(m) {
|
||
var due = m.dueDate ? '<br><span style="font-size:.6rem;color:#8a8a8a">Zu erledigen bis ' + new Date(m.dueDate).toLocaleDateString('de-AT',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) + '</span>' : '';
|
||
var quiz = m.quizEnabled ? '<br><span style="font-size:.6rem;color:#4a7c8a">📝 Mit kurzem Quiz</span>' : '';
|
||
var url = buildModuleUrl(m);
|
||
html += '<a href="'+url+'" class="mod-card" style="border-color:#e8833a;border-width:2px;margin-bottom:.5rem">';
|
||
html += '<div class="mod-img" style="height:80px;background:linear-gradient(135deg,#e8833a,#f4a261)"><span style="font-size:2.5rem">'+(m.icon||'📚')+'</span></div>';
|
||
html += '<div class="mod-body">';
|
||
html += '<h3 style="color:#e8833a">'+m.icon+' '+m.name+(m.forcedLevel ? ' — Stufe '+m.forcedLevel : '')+'</h3>';
|
||
html += '<p>Auftrag von deiner Lehrperson'+due+quiz+'</p>';
|
||
html += '<span class="mod-status" style="background:#fde8d0;color:#e8833a">▶ Jetzt starten</span>';
|
||
html += '</div></a>';
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
// Regulaere Module: spielbar zuerst, "in Kürze" am Ende.
|
||
function renderModCard(m) {
|
||
var isComing = m.mode === 'coming_soon';
|
||
var isLocked = m.mode === 'locked';
|
||
var isSpielbar = !isComing && !isLocked;
|
||
var cardImg = m.card_image ? 'assets/img/' + m.card_image : null;
|
||
var imgHtml = cardImg ? '<img src="'+cardImg+'" alt="">' : '<span style="font-size:3rem">'+(m.icon||'📚')+'</span>';
|
||
var url = buildModuleUrl(m);
|
||
var moduleId = m.id;
|
||
var infoHref = 'modul-' + moduleId;
|
||
var glossarHref = 'glossar?module=' + moduleId;
|
||
var buchHref = buchKapitel(moduleId);
|
||
|
||
var cls = isComing ? 'coming' : (isLocked ? 'locked' : 'spielbar');
|
||
var s = '<div class="mod-card '+cls+'">';
|
||
var overlay = isComing ? '<div class="coming-icon">🚧 In Kürze</div>'
|
||
: isLocked ? '<div class="lock-icon">🔒</div>'
|
||
: '';
|
||
// NEU-Pill: zeigt sich wenn available_from in den letzten 30 Tagen ODER in den nächsten 14 Tagen liegt
|
||
var newPill = '';
|
||
if (isSpielbar && m.available_from) {
|
||
var diffDays = (Date.now() - new Date(m.available_from).getTime()) / 86400000;
|
||
if (diffDays >= -14 && diffDays <= 30) {
|
||
newPill = '<span style="position:absolute;top:8px;right:8px;background:#c97b5a;color:#fff;font-size:.66rem;font-weight:800;padding:3px 10px;border-radius:9999px;letter-spacing:.06em;box-shadow:0 2px 4px rgba(0,0,0,.18);z-index:5">✨ NEU</span>';
|
||
}
|
||
}
|
||
s += '<div class="mod-img" style="position:relative">'+imgHtml+overlay+newPill+'</div>';
|
||
s += '<div class="mod-body">';
|
||
s += '<h3>'+(m.icon||'📚')+' '+m.name+'</h3>';
|
||
s += '<p>'+(m.desc||'')+'</p>';
|
||
s += '<div class="mod-actions">';
|
||
if (isSpielbar) s += '<a href="'+url+'" class="mod-btn primary">▶ Starten</a>';
|
||
s += '<a href="'+infoHref+'" class="mod-btn" title="Mehr Infos">📖 Infos</a>';
|
||
if (!isComing) s += '<a href="'+glossarHref+'" class="mod-btn" title="Glossar zu diesem Modul">📚 Glossar</a>';
|
||
if (buchHref) s += '<a href="'+buchHref+'" class="mod-btn" title="Schulbuch-Kapitel zu diesem Modul">📘 Schulbuch</a>';
|
||
s += '</div>';
|
||
s += '</div></div>';
|
||
return s;
|
||
}
|
||
|
||
// Regulaere Liste enthaelt alles ausser noch offene Lehrer-Auftraege.
|
||
// Erledigte Auftraege erscheinen hier als spielbar, damit der Schueler sie
|
||
// freiwillig wiederholen kann.
|
||
// Schueler-Sicht: coming_soon-Module werden NICHT angezeigt.
|
||
// Gesperrte (von Lehrperson noch nicht freigegebene) Module kommen unter
|
||
// einen eigenen Abschnitt "Kann durch die Lehrperson freigegeben werden".
|
||
var regular = modList.filter(function(m) { return (m.mode !== 'teacher_started' || m.assignmentCompleted) && m.mode !== 'coming_soon'; });
|
||
var spielbar = regular.filter(function(m) { return m.mode !== 'locked'; });
|
||
var lockedByTeacher = regular.filter(function(m) { return m.mode === 'locked'; });
|
||
|
||
html += '<div class="mod-grid">';
|
||
spielbar.forEach(function(m) { html += renderModCard(m); });
|
||
html += '</div>';
|
||
|
||
if (lockedByTeacher.length > 0) {
|
||
html += '<div class="coming-divider">Kann durch die Lehrperson freigegeben werden</div>';
|
||
html += '<div class="mod-grid">';
|
||
lockedByTeacher.forEach(function(m) { html += renderModCard(m); });
|
||
html += '</div>';
|
||
}
|
||
|
||
document.getElementById('tab-modules').innerHTML = html;
|
||
}
|
||
|
||
// Modul-Beschreibungen kommen jetzt aus module_info.short_desc via API.
|
||
|
||
async function renderAchievements() {
|
||
var allProgress = [];
|
||
try {
|
||
var resp = await api('progress?student_id=0');
|
||
if (Array.isArray(resp)) allProgress = resp;
|
||
} catch(e) {}
|
||
|
||
var totalPlays = 0, totalXP = 0, bestStarsOverall = 0;
|
||
allProgress.forEach(function(p) {
|
||
totalPlays += parseInt(p.plays || 0);
|
||
totalXP += parseInt(p.xp || 0);
|
||
bestStarsOverall = Math.max(bestStarsOverall, parseInt(p.best_stars || 0));
|
||
});
|
||
var simCount = allProgress.length;
|
||
|
||
var earned = [];
|
||
if (totalPlays >= 1) earned.push('first_sim');
|
||
if (simCount >= 3) earned.push('three_sims');
|
||
if (simCount >= 8) earned.push('all_sims');
|
||
if (bestStarsOverall >= 4) earned.push('high_score');
|
||
if (totalXP >= 100) earned.push('speed_run');
|
||
|
||
var html = '';
|
||
|
||
// Top-Kennzahlen
|
||
html += '<div class="ach-summary">';
|
||
html += '<div class="card"><div class="n">'+simCount+'</div><div class="l">Module begonnen</div></div>';
|
||
html += '<div class="card"><div class="n">'+totalPlays+'</div><div class="l">Durchgänge</div></div>';
|
||
html += '<div class="card"><div class="n">'+totalXP+'</div><div class="l">XP gesamt</div></div>';
|
||
html += '</div>';
|
||
|
||
// Modul-Erfolge-Liste
|
||
html += '<p style="font-size:.85rem;font-weight:700;color:#4a7c8a;margin-bottom:.5rem">Was du in den Modulen geschafft hast</p>';
|
||
if (allProgress.length === 0) {
|
||
html += '<div class="ach-empty">Noch keine Module gespielt. Geh auf <strong>Module</strong> und starte einen Durchgang.</div>';
|
||
} else {
|
||
html += '<div class="ach-modules">';
|
||
allProgress.forEach(function(p) {
|
||
var stars = '★'.repeat(p.best_stars || 0) + '☆'.repeat(Math.max(0, 5 - (p.best_stars || 0)));
|
||
var last = p.last_played_at ? new Date(p.last_played_at.replace(' ','T')+'Z').toLocaleDateString('de-AT', {day:'2-digit',month:'2-digit',year:'2-digit'}) : '—';
|
||
// Klick → Detail-Modal mit allen Runs dieser Sim
|
||
html += '<div class="ach-mod" style="cursor:pointer" onclick="openOwnRuns(\''+(p.sim_id||'').replace(/[^a-z0-9_-]/gi,'')+'\',\''+(p.title||p.sim_id||'').replace(/[\'"<>]/g,'')+'\',\''+(p.icon||'📚').replace(/[\'"<>]/g,'')+'\')" title="Klicken für alle Durchgänge">';
|
||
html += '<div class="ic">'+(p.icon || '📚')+'</div>';
|
||
html += '<div class="info"><h4>'+(p.title || p.sim_id)+' <span style="color:#8a8a8a;font-size:.7rem;font-weight:500">▸ Details</span></h4>';
|
||
html += '<div class="meta">'+p.plays+'× gespielt · '+p.xp+' XP · zuletzt '+last+'</div></div>';
|
||
html += '<div class="stats">';
|
||
html += '<span class="stat-pill stars" title="Beste Sterne">'+stars+'</span>';
|
||
html += '<span class="stat-pill lvl">Lvl '+(p.level || 1)+'</span>';
|
||
html += '</div></div>';
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
// === Abzeichen-Sammlung pro Simulation (gesperrte = sichtbare Ziele) ===
|
||
if (typeof GGS_SIMS !== 'undefined') {
|
||
var progBySim = {};
|
||
allProgress.forEach(function(p){ progBySim[p.sim_id] = p; });
|
||
html += '<p style="font-size:.85rem;font-weight:700;color:#4a7c8a;margin:1.3rem 0 .3rem">Abzeichen-Sammlung</p>';
|
||
html += '<p style="font-size:.68rem;color:#8a8a8a;margin:0 0 .7rem">Jede Simulation hat fünf Abzeichen. Blasse sind noch nicht geschafft — das sind deine Ziele.</p>';
|
||
GGS_SIMS.forEach(function(sim){
|
||
var p = progBySim[sim.id] || {plays:0, best_stars:0, level:1};
|
||
var list = ggsSimBadges(sim.id);
|
||
var got = ggsBadgeCount(p, sim.id);
|
||
var st = (p.best_stars||0);
|
||
var stars = '★'.repeat(st) + '☆'.repeat(Math.max(0, 5-st));
|
||
html += '<div class="sim-coll"><div class="sim-coll-head">'
|
||
+ '<span class="sc-ic">'+sim.icon+'</span><span class="sc-t">'+sim.title+'</span>'
|
||
+ '<span class="sc-stars" title="Beste Sterne">'+stars+'</span>'
|
||
+ '<span class="sc-count">'+got+' / '+list.length+'</span></div>';
|
||
html += '<div class="badge-grid">';
|
||
list.forEach(function(b){
|
||
var e = b.cond(p);
|
||
html += '<div class="badge '+(e?'earned':'locked-b')+(b.thematic?' thematic':'')+'" title="'+(e?'Geschafft!':'Ziel: '+b.desc)+'">'
|
||
+ '<div class="icon"><img src="'+b.icon+'" alt="" loading="lazy"></div>'
|
||
+ '<div class="name">'+b.name+'</div>'
|
||
+ '<div class="desc">'+b.desc+'</div></div>';
|
||
});
|
||
html += '</div></div>';
|
||
});
|
||
}
|
||
|
||
document.getElementById('tab-achievements').innerHTML = html;
|
||
}
|
||
|
||
async function renderProfile() {
|
||
if (!profile) {
|
||
document.getElementById('tab-profile').innerHTML = '<p style="color:#8a8a8a">Profil wird geladen…</p>';
|
||
return;
|
||
}
|
||
// Lade Fortschritt
|
||
var totalPlays = 0, totalXP = 0, maxLevel = 1;
|
||
try {
|
||
var prog = await api('progress?student_id=0');
|
||
if (Array.isArray(prog)) prog.forEach(function(p) {
|
||
totalPlays += parseInt(p.plays||0);
|
||
totalXP += parseInt(p.xp||0);
|
||
maxLevel = Math.max(maxLevel, parseInt(p.level||1));
|
||
});
|
||
} catch(e) {}
|
||
|
||
var html = '<div class="profile-card">';
|
||
var pAv = profile.emoji_avatar || '🧑🎓';
|
||
var pAvHtml = (typeof pAv === 'string' && pAv.indexOf('avatar:') === 0)
|
||
? '<img src="assets/img/avatars/thumbs/avatar-' + pAv.substring(7) + '.webp" alt="" style="width:100%;height:100%;border-radius:50%;object-fit:cover">'
|
||
: pAv;
|
||
html += '<div class="profile-avatar" onclick="location.href=\'profil.html\'" style="width:80px;height:80px;border-radius:50%;background:#dae8ec;display:flex;align-items:center;justify-content:center;margin:0 auto;overflow:hidden">'+pAvHtml+'</div>';
|
||
html += '<div class="profile-name">'+(profile.display_name || profile.username)+'</div>';
|
||
html += '<div class="profile-class">Klasse '+(profile.class_name || '—')+'</div>';
|
||
html += '<div class="profile-stats">';
|
||
html += '<div class="profile-stat"><div class="n">'+totalPlays+'</div><div class="l">Simulationen</div></div>';
|
||
html += '<div class="profile-stat"><div class="n">'+totalXP+'</div><div class="l">XP</div></div>';
|
||
html += '<div class="profile-stat"><div class="n">Level '+maxLevel+'</div><div class="l">Höchstes Level</div></div>';
|
||
html += '</div></div>';
|
||
html += '<div style="text-align:center;margin-top:1rem"><a href="profil.html" class="tab" style="display:inline-block;padding:.5rem 1.2rem;background:#4a7c8a;color:#fff;border-radius:8px;text-decoration:none;font-size:.78rem;font-weight:700">Profil bearbeiten</a></div>';
|
||
document.getElementById('tab-profile').innerHTML = html;
|
||
}
|
||
|
||
// ===== Detail-Modal: alle Durchgänge einer Sim ansehen =====
|
||
async function openOwnRuns(simId, title, icon) {
|
||
if (!simId) return;
|
||
showOwnRunsModal();
|
||
document.getElementById('ownruns-title').textContent = (icon || '📚') + ' ' + (title || simId);
|
||
document.getElementById('ownruns-body').innerHTML = '<p style="color:#8a8a8a;font-size:.78rem">Lade Verlauf…</p>';
|
||
var r = await api('progress?sim_id=' + encodeURIComponent(simId) + '&runs=1');
|
||
if (r.error) { document.getElementById('ownruns-body').innerHTML = '<p style="color:#c07a6b">'+r.error+'</p>'; return; }
|
||
var runs = r.runs || [];
|
||
// Sonnensystem submittet zwei Events: task_solved (pro Einzel-Aufgabe,
|
||
// als Live-Heartbeat fürs Lehrer-Cockpit) + set_complete (Tour-Abschluss).
|
||
// Für die Schüler-Eigen-Verlauf-Ansicht gruppieren wir das zu Sessions:
|
||
// - Eine Session = task_solved-Einträge + (optional) ein set_complete
|
||
// - Trennkriterium: zeitliche Lücke > 1 h ODER ein set_complete schließt ab
|
||
// - Vollständige Sessions = grün, abgebrochene = orange (halbfertig)
|
||
if (simId === 'sonnensystem') {
|
||
var html = '<div class="ownruns-list">';
|
||
var sessions = groupSonnensystemSessions(runs);
|
||
if (!sessions.length) {
|
||
document.getElementById('ownruns-body').innerHTML = '<p style="font-size:.78rem;color:#8a8a8a">Noch keine Aufgaben gelöst.</p>';
|
||
return;
|
||
}
|
||
sessions.forEach(function(sess) { html += renderSonnensystemSession(sess); });
|
||
html += '</div>';
|
||
document.getElementById('ownruns-body').innerHTML = html;
|
||
return;
|
||
}
|
||
if (!runs.length) {
|
||
document.getElementById('ownruns-body').innerHTML = '<p style="font-size:.78rem;color:#8a8a8a">Noch keine abgeschlossenen Durchgänge gespeichert.</p>';
|
||
return;
|
||
}
|
||
var html = '<div class="ownruns-list">';
|
||
runs.forEach(function(run) {
|
||
html += renderOwnRunCard(simId, run);
|
||
});
|
||
html += '</div>';
|
||
document.getElementById('ownruns-body').innerHTML = html;
|
||
}
|
||
function showOwnRunsModal() {
|
||
document.getElementById('modal-ownruns').classList.add('open');
|
||
}
|
||
function closeOwnRunsModal() {
|
||
document.getElementById('modal-ownruns').classList.remove('open');
|
||
}
|
||
function fmtSecondsSh(s) {
|
||
s = Math.max(0, Math.round(s||0));
|
||
var mm = Math.floor(s/60), ss = s%60;
|
||
return mm + ':' + (ss<10?'0':'') + ss;
|
||
}
|
||
function escSh(s) { return String(s==null?'':s).replace(/[&<>"]/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"'}[c]; }); }
|
||
function renderOwnRunCard(simId, run) {
|
||
var dt = run.submitted_at ? new Date(run.submitted_at.replace(' ','T')+'Z').toLocaleString('de-AT', {day:'2-digit',month:'2-digit',year:'2-digit',hour:'2-digit',minute:'2-digit'}) : '—';
|
||
var dur = run.duration_ms ? fmtSecondsSh(Math.round(run.duration_ms/1000)) : '—';
|
||
var r = run.results || {};
|
||
var d = r.data || r;
|
||
if (simId === 'eu-werkstatt') return renderEuWerkstattOwnRun(dt, dur, r, d);
|
||
if (simId === 'energiemanager') return renderEnergiemanagerOwnRun(dt, dur, r, d);
|
||
if (simId === 'sonnensystem') return renderSonnensystemOwnRun(dt, dur, r, d);
|
||
if (simId === 'busfahrt') return renderBusfahrtOwnRun(dt, dur, r, d);
|
||
if (simId === 'fluggesellschaft') return renderFluggesellschaftOwnRun(dt, dur, r, d);
|
||
if (simId === 'heli') return renderHeliOwnRun(dt, dur, r, d);
|
||
return '<div class="ownruns-card"><div class="ownruns-head"><strong>'+dt+'</strong><span class="ownruns-meta">'+dur+'</span></div></div>';
|
||
}
|
||
function renderBusfahrtOwnRun(dt, dur, r, d) {
|
||
var stars = Math.max(0, Math.min(5, r.stars || 0));
|
||
var starsHtml = '<span class="ownruns-stars">'+'★'.repeat(stars)+'<span class="ownruns-stars-empty">'+'★'.repeat(5-stars)+'</span></span>';
|
||
var res = d.results || d;
|
||
var budget = res.budget || 0;
|
||
var won = res.wonOrders || 0;
|
||
var total = res.totalOrders || 0;
|
||
var quizFt = res.quizFirstTry || 0;
|
||
return ''
|
||
+ '<div class="ownruns-card">'
|
||
+ '<div class="ownruns-head">'
|
||
+ '<div><strong>🚌 Bus-Tour '+((d.level||'l1').toUpperCase())+'</strong><div class="ownruns-meta">'+dt+' · '+dur+'</div></div>'
|
||
+ '<div style="text-align:right">'+starsHtml+'<div style="font-size:.7rem;color:#5a8a5e;font-weight:700;margin-top:.2rem">'+budget.toLocaleString('de-AT')+' €</div></div>'
|
||
+ '</div>'
|
||
+ '<div class="ownruns-stats"><span><b>'+won+'</b>/'+total+' Aufträge</span><span>🧠 <b>'+quizFt+'</b>/'+total+' auf Anhieb</span></div>'
|
||
+ '</div>';
|
||
}
|
||
function renderHeliOwnRun(dt, dur, r, d) {
|
||
var res = d.results || d;
|
||
// Sterne-Fallback-Reihenfolge: top-level stars (neuer submitHeliMission)
|
||
// → res.totalStars (geschachtelt) → res.sterneGesamt (alter assessmentSend)
|
||
// → 0. So bekommen auch ältere Run-Formate korrekte Sterne.
|
||
var stars = Math.max(0, Math.min(5, r.stars || d.stars || res.totalStars || res.sterneGesamt || 0));
|
||
var starsHtml = '<span class="ownruns-stars">'+'★'.repeat(stars)+'<span class="ownruns-stars-empty">'+'★'.repeat(5-stars)+'</span></span>';
|
||
var failed = !!res.failed;
|
||
var icon = failed ? '🧑✈️' : '🚁';
|
||
var status = failed ? 'Chefpilot übernahm' : 'Erfolgreich';
|
||
var statusCol = failed ? '#c07a6b' : '#5a8a5e';
|
||
var title = res.missionTitle || res.missionId || 'Heli-Einsatz';
|
||
var planCorrect = res.planCorrect || 0;
|
||
var planTotal = res.planTotal || 0;
|
||
var planPct = res.planPct != null ? res.planPct : 0;
|
||
return ''
|
||
+ '<div class="ownruns-card">'
|
||
+ '<div class="ownruns-head">'
|
||
+ '<div><strong>'+icon+' '+escSh(title)+'</strong><div class="ownruns-meta">'+dt+' · '+dur+'</div></div>'
|
||
+ '<div style="text-align:right">'+starsHtml+'<div style="font-size:.7rem;color:'+statusCol+';font-weight:700;margin-top:.2rem">'+status+'</div></div>'
|
||
+ '</div>'
|
||
+ '<div class="ownruns-stats"><span>🗺️ <b>'+planCorrect+'</b>/'+planTotal+' Navigation ('+planPct+'%)</span><span>🛫 <b>'+'★'.repeat(res.startScore||0)+'</b> Start</span><span>🛬 <b>'+'★'.repeat(res.landingScore||0)+'</b> Landung</span></div>'
|
||
+ '</div>';
|
||
}
|
||
function renderFluggesellschaftOwnRun(dt, dur, r, d) {
|
||
var stars = Math.max(0, Math.min(5, r.stars || 0));
|
||
var starsHtml = '<span class="ownruns-stars">'+'★'.repeat(stars)+'<span class="ownruns-stars-empty">'+'★'.repeat(5-stars)+'</span></span>';
|
||
var res = d.results || d;
|
||
var budget = res.budget || 0;
|
||
var won = res.wonOrders || 0;
|
||
var total = res.totalOrders || 0;
|
||
var co2t = ((res.co2Kg || 0) / 1000).toFixed(1);
|
||
return ''
|
||
+ '<div class="ownruns-card">'
|
||
+ '<div class="ownruns-head">'
|
||
+ '<div><strong>✈ Flug-Tour '+((d.level||'l1').toUpperCase())+'</strong><div class="ownruns-meta">'+dt+' · '+dur+'</div></div>'
|
||
+ '<div style="text-align:right">'+starsHtml+'<div style="font-size:.7rem;color:#5a8a5e;font-weight:700;margin-top:.2rem">'+budget.toLocaleString('de-AT')+' €</div></div>'
|
||
+ '</div>'
|
||
+ '<div class="ownruns-stats"><span><b>'+won+'</b>/'+total+' Flüge</span><span>🌍 <b>'+co2t+'</b> t CO₂</span></div>'
|
||
+ '</div>';
|
||
}
|
||
// Sonnensystem: task_solved-Einträge + ggf. set_complete zu Sessions clustern.
|
||
// Eine Session endet entweder mit set_complete (= fertig) oder wenn die nächste
|
||
// Aufgabe > 1 h später kommt (= abgebrochen).
|
||
function groupSonnensystemSessions(runs) {
|
||
var SESSION_GAP_MS = 60 * 60 * 1000;
|
||
var sorted = runs.slice().sort(function(a,b){
|
||
return new Date(a.submitted_at.replace(' ','T')+'Z') - new Date(b.submitted_at.replace(' ','T')+'Z');
|
||
});
|
||
var sessions = [];
|
||
var current = null;
|
||
sorted.forEach(function(run) {
|
||
var d = (run.results && run.results.data) || run.results || {};
|
||
var ts = new Date(run.submitted_at.replace(' ','T')+'Z').getTime();
|
||
var ev = d.event || (run.stars > 0 ? 'set_complete' : null);
|
||
if (!current || (ts - current.endTs) > SESSION_GAP_MS) {
|
||
if (current) sessions.push(current);
|
||
current = { runs: [], complete: false, startTs: ts, endTs: ts, completeRun: null, total: d.total || 0 };
|
||
}
|
||
current.runs.push(run);
|
||
current.endTs = ts;
|
||
if (d.total) current.total = d.total;
|
||
if (ev === 'set_complete') {
|
||
current.complete = true;
|
||
current.completeRun = run;
|
||
sessions.push(current);
|
||
current = null;
|
||
}
|
||
});
|
||
if (current) sessions.push(current);
|
||
return sessions.reverse(); // neueste zuerst
|
||
}
|
||
|
||
function renderSonnensystemSession(sess) {
|
||
var startDate = new Date(sess.startTs);
|
||
var endDate = new Date(sess.endTs);
|
||
var dt = startDate.toLocaleString('de-AT', {day:'2-digit',month:'2-digit',year:'2-digit',hour:'2-digit',minute:'2-digit'});
|
||
var sameDay = startDate.toDateString() === endDate.toDateString();
|
||
var durSec = Math.round((sess.endTs - sess.startTs) / 1000);
|
||
var dur = fmtSecondsSh(durSec);
|
||
|
||
// Unique gelöste Aufgaben in dieser Session
|
||
var solvedTasks = new Set();
|
||
var firstTryTasks = 0;
|
||
var attemptsByTask = {};
|
||
var newAch = [];
|
||
sess.runs.forEach(function(run){
|
||
var d = (run.results && run.results.data) || run.results || {};
|
||
if (d.taskId) {
|
||
solvedTasks.add(d.taskId);
|
||
if (d.firstTry) firstTryTasks++;
|
||
}
|
||
if (d.attemptsByTask) {
|
||
Object.keys(d.attemptsByTask).forEach(function(k){
|
||
attemptsByTask[k] = Math.max(attemptsByTask[k] || 0, d.attemptsByTask[k] || 1);
|
||
});
|
||
}
|
||
if (Array.isArray(d.newAchievements)) newAch = newAch.concat(d.newAchievements);
|
||
});
|
||
// Falls set_complete vorliegt, dessen completed/total bevorzugen
|
||
var done = solvedTasks.size;
|
||
var total = sess.total || 0;
|
||
if (sess.completeRun) {
|
||
var cd = (sess.completeRun.results && sess.completeRun.results.data) || sess.completeRun.results || {};
|
||
if (cd.completed) done = cd.completed;
|
||
if (cd.total) total = cd.total;
|
||
}
|
||
var quote = total > 0 ? Math.round(done/total*100) : 0;
|
||
|
||
// Sterne nur bei abgeschlossenen Sessions
|
||
var stars = 0;
|
||
if (sess.completeRun) {
|
||
var crd = (sess.completeRun.results && sess.completeRun.results.data) || sess.completeRun.results || {};
|
||
stars = Math.max(0, Math.min(5, crd.stars || sess.completeRun.results.stars || 0));
|
||
}
|
||
var starsHtml = sess.complete
|
||
? '<span class="ownruns-stars">'+'★'.repeat(stars)+'<span class="ownruns-stars-empty">'+'★'.repeat(5-stars)+'</span></span>'
|
||
: '<span style="font-size:.7rem;color:#c9913a;font-weight:700">⏸ läuft / abgebrochen</span>';
|
||
|
||
var statusClass = sess.complete ? 'ownruns-card--complete' : 'ownruns-card--partial';
|
||
var statusEmoji = sess.complete ? '🪐' : '⏳';
|
||
var statusLabel = sess.complete ? 'Aufgaben-Runde abgeschlossen' : 'Teil-Durchlauf (nicht abgeschlossen)';
|
||
var quoteColor = sess.complete
|
||
? (quote >= 85 ? '#5a8a5e' : quote >= 50 ? '#c9913a' : '#c07a6b')
|
||
: '#c9913a';
|
||
|
||
// Aufklappbare Details: welche Aufgaben mit mehreren Versuchen + welche Aufgaben gelöst
|
||
var manyTries = [];
|
||
Object.keys(attemptsByTask).forEach(function(k){
|
||
var n = attemptsByTask[k] || 1;
|
||
if (n > 1) manyTries.push({id:k, n:n});
|
||
});
|
||
manyTries.sort(function(a,b){ return b.n - a.n; });
|
||
var solvedList = Array.from(solvedTasks);
|
||
|
||
var detailsHtml = '<details class="ownruns-details"><summary>Details anzeigen</summary>';
|
||
detailsHtml += '<div style="font-size:.72rem;line-height:1.5;padding:.4rem 0">';
|
||
detailsHtml += '<div><b>Zeitraum:</b> ' + dt + (sameDay ? '' : ' bis ' + endDate.toLocaleString('de-AT', {day:'2-digit',month:'2-digit',year:'2-digit',hour:'2-digit',minute:'2-digit'})) + ' · Dauer ≈ ' + dur + '</div>';
|
||
if (firstTryTasks > 0) {
|
||
detailsHtml += '<div style="margin-top:.3rem">✓ <b>'+firstTryTasks+'</b> auf Anhieb richtig</div>';
|
||
}
|
||
if (solvedList.length) {
|
||
detailsHtml += '<div style="margin-top:.3rem">📋 Gelöste Aufgaben:</div>';
|
||
detailsHtml += '<ul style="margin:.15rem 0 0 1.1rem;padding:0">';
|
||
solvedList.forEach(function(tid){
|
||
var nTries = attemptsByTask[tid] || 1;
|
||
var trySuffix = nTries > 1 ? ' <span style="color:#c9913a">(' + nTries + ' Versuche)</span>' : '';
|
||
detailsHtml += '<li>' + escSh(tid) + trySuffix + '</li>';
|
||
});
|
||
detailsHtml += '</ul>';
|
||
}
|
||
if (manyTries.length && !solvedList.length) {
|
||
detailsHtml += '<div style="margin-top:.3rem">🔁 <b>'+manyTries.length+'</b> Aufgaben mit Mehrfachversuchen:</div>';
|
||
detailsHtml += '<ul style="margin:.2rem 0 0 1.1rem;padding:0">';
|
||
manyTries.forEach(function(x){ detailsHtml += '<li>'+escSh(x.id)+': <b>'+x.n+'</b> Versuche</li>'; });
|
||
detailsHtml += '</ul>';
|
||
}
|
||
if (newAch.length) {
|
||
detailsHtml += '<div style="margin-top:.3rem">🎉 Neue Erfolge: <b>'+newAch.map(escSh).join(', ')+'</b></div>';
|
||
}
|
||
if (!sess.complete) {
|
||
detailsHtml += '<div style="margin-top:.4rem;color:#c9913a;font-style:italic">Diese Runde wurde nicht bis zum Ende gespielt. Du kannst sie über das Modul-Cockpit fortsetzen oder neu starten.</div>';
|
||
}
|
||
detailsHtml += '</div></details>';
|
||
|
||
return ''
|
||
+ '<div class="ownruns-card '+statusClass+'">'
|
||
+ '<div class="ownruns-head">'
|
||
+ '<div><strong>'+statusEmoji+' '+statusLabel+'</strong><div class="ownruns-meta">'+dt+' · ≈ '+dur+'</div></div>'
|
||
+ '<div style="text-align:right">'+starsHtml+'<div style="font-size:.7rem;color:'+quoteColor+';font-weight:700;margin-top:.2rem">'+(total ? quote+'% gelöst' : '—')+'</div></div>'
|
||
+ '</div>'
|
||
+ '<div class="ownruns-stats">'
|
||
+ '<span><b>'+done+'</b>/'+(total||'?')+' Aufgaben richtig</span>'
|
||
+ (newAch.length ? '<span>🎉 <b>'+newAch.length+'</b> Erfolge</span>' : '')
|
||
+ '</div>'
|
||
+ detailsHtml
|
||
+ '</div>';
|
||
}
|
||
|
||
function renderSonnensystemOwnRun(dt, dur, r, d) {
|
||
var stars = Math.max(0, Math.min(5, r.stars || (d.stars||0)));
|
||
var starsHtml = '<span class="ownruns-stars">'+'★'.repeat(stars)+'<span class="ownruns-stars-empty">'+'★'.repeat(5-stars)+'</span></span>';
|
||
var done = d.completed || 0;
|
||
var total = d.total || 0;
|
||
var quote = d.scoreQuote != null ? d.scoreQuote : (total > 0 ? Math.round(done/total*100) : 0);
|
||
var quoteColor = quote >= 85 ? '#5a8a5e' : quote >= 50 ? '#c9913a' : '#c07a6b';
|
||
var event = d.event || 'set_complete';
|
||
|
||
// Aufklappbare Details: welche Aufgaben mit mehreren Versuchen + Achievements.
|
||
var attemptsByTask = d.attemptsByTask || {};
|
||
var manyTries = [];
|
||
var firstTryCount = 0;
|
||
Object.keys(attemptsByTask).forEach(function(k){
|
||
var n = attemptsByTask[k] || 1;
|
||
if (n > 1) manyTries.push({id:k, n:n});
|
||
else firstTryCount++;
|
||
});
|
||
manyTries.sort(function(a,b){ return b.n - a.n; });
|
||
var newAch = d.newAchievements || [];
|
||
|
||
var detailsHtml = '';
|
||
if (event === 'set_complete' && (manyTries.length || newAch.length || firstTryCount)) {
|
||
detailsHtml = '<details class="ownruns-details"><summary>Details anzeigen</summary>';
|
||
detailsHtml += '<div style="font-size:.72rem;line-height:1.5;padding:.4rem 0">';
|
||
if (firstTryCount > 0) {
|
||
detailsHtml += '<div>✓ <b>'+firstTryCount+'</b> auf Anhieb richtig</div>';
|
||
}
|
||
if (manyTries.length) {
|
||
detailsHtml += '<div style="margin-top:.3rem">🔁 <b>'+manyTries.length+'</b> Aufgaben mit Mehrfachversuchen:</div>';
|
||
detailsHtml += '<ul style="margin:.2rem 0 0 1.1rem;padding:0">';
|
||
manyTries.forEach(function(x){
|
||
detailsHtml += '<li>'+escSh(x.id)+': <b>'+x.n+'</b> Versuche</li>';
|
||
});
|
||
detailsHtml += '</ul>';
|
||
}
|
||
if (newAch.length) {
|
||
detailsHtml += '<div style="margin-top:.3rem">🎉 Neue Erfolge: <b>'+newAch.map(escSh).join(', ')+'</b></div>';
|
||
}
|
||
detailsHtml += '</div></details>';
|
||
}
|
||
|
||
return ''
|
||
+ '<div class="ownruns-card">'
|
||
+ '<div class="ownruns-head">'
|
||
+ '<div><strong>🪐 '+(event === 'set_complete' ? 'Aufgaben-Runde' : 'Einzel-Lösung')+'</strong><div class="ownruns-meta">'+dt+' · '+dur+'</div></div>'
|
||
+ '<div style="text-align:right">'+starsHtml+'<div style="font-size:.7rem;color:'+quoteColor+';font-weight:700;margin-top:.2rem">'+quote+'% gelöst</div></div>'
|
||
+ '</div>'
|
||
+ '<div class="ownruns-stats">'
|
||
+ '<span><b>'+done+'</b>/'+total+' Aufgaben richtig</span>'
|
||
+ (newAch.length ? '<span>🎉 <b>'+newAch.length+'</b> Erfolge</span>' : '')
|
||
+ '</div>'
|
||
+ detailsHtml
|
||
+ '</div>';
|
||
}
|
||
function renderEnergiemanagerOwnRun(dt, dur, r, d) {
|
||
var stars = Math.max(0, Math.min(5, r.stars || 0));
|
||
var starsHtml = '<span class="ownruns-stars">'+'★'.repeat(stars)+'<span class="ownruns-stars-empty">'+'★'.repeat(5-stars)+'</span></span>';
|
||
var setIcon = d.setIcon || '⚡';
|
||
var setTitle = d.setTitle || d.setId || '?';
|
||
var blocksB = d.blocksBalanced || 0;
|
||
var blocksT = d.blocksTotal || 0;
|
||
var quote = blocksT > 0 ? Math.round(blocksB/blocksT*100) : 0;
|
||
var quoteColor = quote >= 87 ? '#5a8a5e' : quote >= 50 ? '#c9913a' : '#c07a6b';
|
||
var perDay = Array.isArray(d.perDay) ? d.perDay : [];
|
||
var dayRows = perDay.map(function(day, i) {
|
||
var ok = day.balanced === day.blocks;
|
||
var icon = ok ? '🏆' : day.balanced >= 6 ? '✓' : day.balanced >= 4 ? '◐' : '⚠️';
|
||
var notes = [];
|
||
if (day.gap > 0.5) notes.push('<span style="color:#c07a6b">'+day.gap+' GWh Lücke</span>');
|
||
if (day.surplus > 0.5) notes.push('<span style="color:#c9913a">'+day.surplus+' GWh Überschuss</span>');
|
||
return '<tr><td style="width:2em;color:#8a8a8a">'+(i+1)+'.</td><td>'+icon+' '+escSh(day.dayTitle||'?')+'</td><td><b>'+day.balanced+'/'+day.blocks+'</b></td><td>'+(notes.join(' · ')||'–')+'</td></tr>';
|
||
}).join('');
|
||
return ''
|
||
+ '<div class="ownruns-card">'
|
||
+ '<div class="ownruns-head">'
|
||
+ '<div><strong>'+setIcon+' Set '+escSh(setTitle)+'</strong><div class="ownruns-meta">'+dt+' · '+dur+'</div></div>'
|
||
+ '<div style="text-align:right">'+starsHtml+'<div style="font-size:.7rem;color:'+quoteColor+';font-weight:700;margin-top:.2rem">'+quote+'% ausgegl.</div></div>'
|
||
+ '</div>'
|
||
+ '<div class="ownruns-stats">'
|
||
+ '<span><b>'+blocksB+'</b>/'+blocksT+' Blöcke ausgeglichen</span>'
|
||
+ '<span>Σ Lücke: <b>'+(d.totalGap||0)+' GWh</b></span>'
|
||
+ '<span>Σ Überschuss: <b>'+(d.totalSurplus||0)+' GWh</b></span>'
|
||
+ '</div>'
|
||
+ (dayRows
|
||
? '<table class="ownruns-steps"><thead><tr><th>#</th><th>Tag</th><th>Balance</th><th>Anmerkung</th></tr></thead><tbody>'+dayRows+'</tbody></table>'
|
||
: '')
|
||
+ '</div>';
|
||
}
|
||
function renderEuWerkstattOwnRun(dt, dur, r, d) {
|
||
var stars = Math.max(0, Math.min(5, r.stars || 0));
|
||
var starsHtml = '<span class="ownruns-stars">'+'★'.repeat(stars)+'<span class="ownruns-stars-empty">'+'★'.repeat(5-stars)+'</span></span>';
|
||
var endTypeLabel = ({success:'🏆 Gewonnen', mixed:'⚖️ Teilweise', rejected:'❌ Verloren'})[d.endType] || '—';
|
||
var endTypeColor = ({success:'#5a8a5e', mixed:'#c9913a', rejected:'#c07a6b'})[d.endType] || '#8a8a8a';
|
||
var diffLabel = d.difficulty === 'expert' ? '🦉 Profi' : '🌱 Einfach';
|
||
var stepsTotal = d.stepsTotal || 0;
|
||
var attempts = d.attemptsByStep || [];
|
||
var detours = d.detoursByStep || [];
|
||
var stepRows = '';
|
||
for (var i = 0; i < stepsTotal; i++) {
|
||
var n = attempts[i] || 1;
|
||
var firstTry = (n === 1);
|
||
var det = (detours[i] || []).map(function(x){
|
||
var ic = x.kind === 'muelleimer' ? '🗑️' : '🔁';
|
||
return '<span class="ownruns-detour '+(x.kind === 'muelleimer'?'kind-m':'kind-s')+'">'+ic+' '+escSh(x.label||'')+'</span>';
|
||
}).join('');
|
||
var tag = firstTry
|
||
? '<span style="color:#5a8a5e;font-weight:700">✓ auf Anhieb</span>'
|
||
: '<span style="color:#c9913a;font-weight:700">'+n+'. Versuch</span>';
|
||
stepRows += '<tr><td style="width:2em;color:#8a8a8a">'+(i+1)+'.</td><td>'+tag+'</td><td>'+(det||'—')+'</td></tr>';
|
||
}
|
||
return ''
|
||
+ '<div class="ownruns-card">'
|
||
+ '<div class="ownruns-head">'
|
||
+ '<div><strong>'+(d.scenarioIcon||'')+' '+escSh(d.scenarioTitle||d.scenarioId||'?')+'</strong>'
|
||
+ '<div class="ownruns-meta">'+dt+' · '+dur+' · '+diffLabel+'</div>'
|
||
+ '</div>'
|
||
+ '<div style="text-align:right">'+starsHtml+'<div style="font-size:.7rem;color:'+endTypeColor+';font-weight:700;margin-top:.2rem">'+endTypeLabel+'</div></div>'
|
||
+ '</div>'
|
||
+ '<div class="ownruns-stats">'
|
||
+ '<span><b>'+(d.firstTryHits||0)+'</b>/'+stepsTotal+' auf Anhieb</span>'
|
||
+ '<span><b>'+(d.totalAttempts||0)+'</b> Versuche</span>'
|
||
+ '<span><b>'+(d.muelleimerCount||0)+'</b> Mülleimer</span>'
|
||
+ '</div>'
|
||
+ (stepRows
|
||
? '<table class="ownruns-steps"><thead><tr><th>#</th><th>Status</th><th>Sackgassen</th></tr></thead><tbody>'+stepRows+'</tbody></table>'
|
||
: '')
|
||
+ '</div>';
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
<style>
|
||
/* Detail-Modal "alle Durchgänge" */
|
||
#modal-ownruns{position:fixed;inset:0;background:rgba(20,32,26,.55);display:none;align-items:center;justify-content:center;z-index:9000;padding:1rem}
|
||
#modal-ownruns.open{display:flex}
|
||
.ownruns-card-wrap{background:#fff;border-radius:14px;max-width:720px;width:100%;max-height:90vh;overflow-y:auto;padding:1rem 1.2rem;box-shadow:0 8px 30px rgba(0,0,0,.25)}
|
||
.ownruns-head-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:.6rem;padding-bottom:.4rem;border-bottom:1px solid rgba(0,0,0,.07)}
|
||
.ownruns-list{display:flex;flex-direction:column;gap:.6rem}
|
||
.ownruns-card{background:#fdfdfb;border:1px solid rgba(0,0,0,.08);border-radius:10px;padding:.65rem .85rem;border-left:4px solid rgba(0,0,0,.08)}
|
||
.ownruns-card--complete{border-left-color:#5a8a5e;background:linear-gradient(90deg,#f0f7f0 0,#fdfdfb 80px)}
|
||
.ownruns-card--partial{border-left-color:#e8a247;background:linear-gradient(90deg,#fff8ec 0,#fdfdfb 80px)}
|
||
.ownruns-head{display:flex;justify-content:space-between;align-items:flex-start;gap:.6rem;margin-bottom:.5rem}
|
||
.ownruns-meta{font-size:.7rem;color:#8a8a8a;margin-top:.15rem}
|
||
.ownruns-stars{font-size:1.05rem;color:#e8c547;letter-spacing:.05em}
|
||
.ownruns-stars-empty{opacity:.25}
|
||
.ownruns-stats{display:flex;gap:1rem;font-size:.74rem;color:#8a8a8a;margin-bottom:.5rem;flex-wrap:wrap}
|
||
.ownruns-stats b{color:#1f4b37;font-weight:700}
|
||
.ownruns-details{margin-top:.4rem;border-top:1px dashed rgba(0,0,0,.08);padding-top:.3rem}
|
||
.ownruns-details summary{cursor:pointer;color:#4a7c8a;font-size:.72rem;font-weight:600;padding:.2rem 0}
|
||
.ownruns-details summary:hover{color:#3a6470}
|
||
.ownruns-details ul{color:#5a5a5a}
|
||
.ownruns-steps{width:100%;border-collapse:collapse;font-size:.74rem;margin-top:.3rem}
|
||
.ownruns-steps th{text-align:left;padding:.3rem .4rem;background:#f7f6f3;font-size:.66rem;color:#8a8a8a;font-weight:600}
|
||
.ownruns-steps td{padding:.35rem .4rem;border-bottom:1px solid rgba(0,0,0,.04);vertical-align:top}
|
||
.ownruns-detour{display:inline-block;padding:2px 7px;border-radius:7px;font-size:.66rem;font-weight:700;margin-right:.3rem;margin-bottom:.15rem}
|
||
.ownruns-detour.kind-m{background:#b04a3e;color:#fff}
|
||
.ownruns-detour.kind-s{background:#fff3cf;color:#1f4b37;border:1px solid #e8c547}
|
||
.ownruns-close{background:transparent;border:1.5px solid #4a7c8a;color:#4a7c8a;border-radius:7px;padding:.35rem .8rem;cursor:pointer;font-weight:700;font-size:.78rem}
|
||
</style>
|
||
<div id="modal-ownruns" onclick="if(event.target.id==='modal-ownruns')closeOwnRunsModal()">
|
||
<div class="ownruns-card-wrap">
|
||
<div class="ownruns-head-bar">
|
||
<h3 id="ownruns-title" style="margin:0;font-size:1rem;color:#1f4b37">Verlauf</h3>
|
||
<button class="ownruns-close" onclick="closeOwnRunsModal()">Schließen</button>
|
||
</div>
|
||
<div id="ownruns-body"></div>
|
||
</div>
|
||
</div>
|
||
<script src="assets/js/version-tag.js" defer></script>
|
||
</body>
|
||
</html>
|