This commit is contained in:
breitenbach76 2026-05-28 15:50:08 +02:00
commit c87b0b1775
23 changed files with 2658 additions and 0 deletions

101
04_Tablet-Quiz/README.md Normal file
View file

@ -0,0 +1,101 @@
# Tablet-Quiz — Begleit-App (Teilprojekt)
**Status:** Konzept · **Typ:** eigenständiges Software-Teilprojekt des SLC-Workshops
Das Tablet-Quiz ist der **digitale Begleiter** des Tabletops — kein Ersatz fürs
Brett. Es ist der **erklärende Gegenpart** zu den Plättchen und **ersetzt deren
Rückseite**: Die Plättchen tragen nur noch die Kurzbezeichnung, die ausführliche
Erklärung liefert die App. Sie **führt die Stationsreihenfolge** (linearer
Lifecycle), stellt pro Station ein **vermittelndes Quiz**, gibt danach die
**ausführliche Auflösung** und protokolliert Verständnislücken fürs Debrief.
---
## 1. Ziel & Rolle im Spiel
- **Stationsführung:** schaltet Station für Station automatisch weiter („Nächste Station") — die Plättchen brauchen keinen Code.
- **Active Recall verstärken:** erst Diskussion am Board, dann vermittelndes Quiz, dann Auflösung — Gruppe rät, App bestätigt/korrigiert.
- **Vollständige Erklärung:** liefert nach dem Quiz die ausführliche Auflösung (ersetzt die Plättchenrückseite) aus dem Blueprint (Single Source of Truth).
- **Dokumentation:** erfasst automatisch, welche Aktivitäten unklar waren (→ `../05_Workshop-Dokumentation/`).
Bewusst **nicht** das Ziel: das Spiel digital ersetzen, Echtzeit-Multiplayer,
Accounts/Login, Cloud-Pflicht.
## 2. Datengrundlage (keine Doppelpflege)
Die App liest ausschließlich die bestehenden Blueprint-Dateien und leitet
Fragen daraus ab:
| Quelle | liefert |
|--------|---------|
| `service-lifecycle_*.yaml` | Aktivitäten, Beschreibungen, Reihenfolge, Gates |
| `spm_rollen.yaml` | Rollen, RACI, Gate-Keeper |
Ein Build-Schritt konvertiert die YAMLs in ein statisches `questions.json`.
Damit bleibt der Blueprint die einzige Wahrheit; Inhalte werden nie im App-Code
dupliziert.
## 3. Fragetypen
1. **Reihenfolge:** „Was kommt nach `tr_08`?"
2. **Rolle / RACI:** „Wer ist *Accountable* für `op_06`?"
3. **Artefakt:** „Welches Artefakt entsteht bei `tr_07`?"
4. **Gate-Logik:** „Wer muss an Gate 1 zustimmen?" / „Welche Pfade gibt es?"
5. **Zuordnung:** „In welcher Phase liegt `sp_09`?"
Jede Frage: Gruppentipp → *Auflösen*-Button → Modellantwort. Im Anschluss an das
Quiz folgt die **ausführliche Auflösung** der Station (vollständige Beschreibung +
Rollen/RACI + Artefakt aus der YAML) — das ist der Inhalt, der früher auf der
Plättchenrückseite stand.
## 4. Ablauf (UI-Flow)
```
[Start] → Szenario wählen (= Action Card)
→ App führt zur aktuellen Station (linearer Lifecycle, Fortschritt sichtbar)
→ Station:
→ Gruppe diskutiert am Board anhand der Kurzbezeichnung (App noch zu)
→ Quiz (vermittelnd): Frage(n) → Gruppentipp → "Auflösen" → richtig/falsch
→ ausführliche Auflösung der Station (Erklärung + RACI + Artefakt)
→ Gruppe reflektiert; optional "war unklar" markieren
→ "Nächste Station"
→ an Gates: Gate-Frage + Rollen-Check
→ [Ende] → Debrief-Export (unklare Aktivitäten, Quote, Pfad)
```
## 5. Funktionsumfang (MVP)
- [ ] `questions.json` + Stations-Inhalte aus YAMLs generieren (Build-Skript).
- [ ] Stationsführung: linearer Durchlauf mit „Nächste Station" + Fortschritt/Phasen-Farben.
- [ ] Fragetypen 13 (vermittelndes Quiz).
- [ ] „Auflösen"-Mechanik (Antwort erst auf Klick) **+ ausführliche Stationsauflösung** (Erklärung/RACI/Artefakt) nach dem Quiz.
- [ ] „Unklar"-Markierung je Aktivität.
- [ ] Debrief-Export (Markdown/JSON, lokal).
### Später (Ausbau)
- Gate-Fragen mit Rollen-Check (Typ 45).
- Mehrere Szenarien mit unterschiedlichen Fragesets.
- Punktestand / Team-Modus.
- Mehrsprachigkeit.
## 6. Technik-Empfehlung
- **Single-Page-Web-App**, offline lauffähig (PWA), passt zum bestehenden
HTML-first-Stil im Repo (vgl. MB-Retro-HTMLs).
- Kein Backend nötig: statisches `questions.json` + LocalStorage für das Logbuch.
- Tablet im Kiosk-/Vollbildmodus; keine Konten, keine Cloud.
- Stack-Vorschlag: Vanilla JS oder leichtes Framework, ein Build-Skript (Node/Python)
für die YAML→JSON-Konvertierung.
## 7. Schnittstellen zum restlichen Spiel
- **Eingang:** Szenarioauswahl = gezogene Action Card (`../03_Karten/`).
- **Inhalt:** Aktivitäten/Gates/Rollen = Brett-Elemente (`../00_Konzept/`).
- **Ausgang:** Debrief-Daten → Workshop-Dokumentation (`../05_Workshop-Dokumentation/`).
## 8. Offene Punkte
- [ ] Format `questions.json` spezifizieren.
- [ ] Entscheidung Framework vs. Vanilla.
- [ ] Wer pflegt/baut? (intern DIGIT vs. extern)
- [ ] Datenschutz: rein lokal, keine personenbezogenen Daten — bestätigen.

View file

@ -0,0 +1,491 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>SLC-Workshop — Companion-App (Prototyp)</title>
<style>
:root {
--bg: #f4f5f7;
--panel: #ffffff;
--ink: #1d2430;
--muted: #6b7686;
--line: #e2e6ec;
--accent: #e2001a; /* Freiburg-Rot */
--ok: #1f9d57;
--bad: #d23b3b;
--design: #2f80c9;
--transition: #e8862b;
--operation: #2f9e57;
--support: #18a9a0;
--review: #8358c6;
--radius: 14px;
--shadow: 0 1px 3px rgba(20,30,50,.08), 0 6px 24px rgba(20,30,50,.06);
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body {
font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: var(--ink); background: var(--bg);
-webkit-font-smoothing: antialiased;
}
header {
display: flex; align-items: center; gap: 16px;
padding: 12px 20px; background: var(--panel);
border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 5;
}
header .brand { font-weight: 700; letter-spacing: .2px; }
header .brand b { color: var(--accent); }
header .tag { font-size: 12px; color: var(--muted); border:1px solid var(--line); padding:2px 8px; border-radius:999px; }
header .spacer { flex: 1; }
.scenario select, button {
font: inherit; border-radius: 10px; border: 1px solid var(--line);
background: #fff; color: var(--ink); padding: 8px 12px; cursor: pointer;
}
button.primary { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }
button.ghost { background: #fff; }
button:disabled { opacity: .45; cursor: default; }
.progress { height: 6px; background: var(--line); }
.progress > div { height: 100%; background: var(--accent); transition: width .3s; }
.layout { display: grid; grid-template-columns: 300px 1fr; gap: 0; min-height: calc(100% - 55px); }
@media (max-width: 820px) { .layout { grid-template-columns: 1fr; } aside { display:none; } }
aside { border-right: 1px solid var(--line); background: var(--panel); padding: 14px; overflow:auto; }
aside h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .8px; color: var(--muted); margin: 16px 0 6px; }
.stationItem {
display:flex; align-items:center; gap:8px; padding:8px 10px; border-radius:10px;
cursor:pointer; font-size: 14px;
}
.stationItem:hover { background: #f7f9fb; }
.stationItem.active { background: #eef4fb; font-weight:600; }
.stationItem .dot { width:10px; height:10px; border-radius:50%; flex:none; }
.stationItem .id { color: var(--muted); font-variant-numeric: tabular-nums; font-size:12px; }
.stationItem .flag { margin-left:auto; color: var(--accent); }
.stationItem.done .id::after { content:" ✓"; color: var(--ok); }
main { padding: 28px clamp(20px, 5vw, 64px); overflow:auto; }
.card { background: var(--panel); border:1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 26px; max-width: 860px; margin: 0 auto; }
.phaseChip { display:inline-flex; align-items:center; gap:8px; font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:.6px; color:#fff; padding:5px 12px; border-radius:999px; }
.gateChip { background: var(--ink); }
.stationName { font-size: 28px; font-weight: 700; margin: 14px 0 4px; line-height:1.2; }
.stationId { color: var(--muted); font-size: 14px; }
.token { font-size:13px; color:var(--muted); margin-top:10px; }
.token b { color: var(--ink); }
.step { margin-top: 22px; }
.stepHead { display:flex; align-items:center; gap:10px; font-size:12px; text-transform:uppercase; letter-spacing:.8px; color:var(--muted); margin-bottom:10px; }
.stepHead .n { width:22px; height:22px; border-radius:50%; background:var(--ink); color:#fff; display:grid; place-items:center; font-size:12px; }
.discuss { background:#fbfdff; border:1px dashed var(--line); border-radius:12px; padding:18px 20px; }
.discuss ul { margin:8px 0 0; padding-left:20px; }
.discuss li { margin:4px 0; }
.q { border:1px solid var(--line); border-radius:12px; padding:18px 20px; margin-bottom:14px; }
.q .frage { font-weight:600; margin-bottom:12px; }
.opts { display:grid; gap:8px; }
.opt { text-align:left; padding:12px 14px; border-radius:10px; border:1px solid var(--line); background:#fff; cursor:pointer; }
.opt:hover { border-color:#c9d2dd; }
.opt.sel { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(226,0,26,.12); }
.opt.correct { border-color: var(--ok); background:#f1faf4; }
.opt.wrong { border-color: var(--bad); background:#fdf3f3; }
.opt .mark { float:right; font-weight:700; }
.qExpl { margin-top:10px; font-size:14px; color:var(--muted); border-left:3px solid var(--line); padding-left:12px; }
.reveal h4 { margin: 18px 0 6px; font-size: 13px; text-transform:uppercase; letter-spacing:.6px; color:var(--muted); }
.reveal p { margin: 0 0 8px; }
.reveal ul { margin: 4px 0; padding-left: 20px; }
table.raci { width:100%; border-collapse: collapse; font-size:14px; }
table.raci th, table.raci td { text-align:left; padding:8px 10px; border-bottom:1px solid var(--line); }
table.raci th { color:var(--muted); font-weight:600; font-size:12px; text-transform:uppercase; letter-spacing:.5px; }
.raciBadge { display:inline-block; min-width:22px; text-align:center; font-weight:700; border-radius:6px; padding:2px 6px; font-size:12px; }
.raci-A { background:#fbe3e3; color:#b5202a; }
.raci-R { background:#e3eefb; color:#1f5fae; }
.raci-C { background:#fff1dd; color:#b5701a; }
.raci-I { background:#eef0f3; color:#5a6675; }
.pfade { display:grid; gap:8px; }
.pfad { border:1px solid var(--line); border-left:4px solid var(--transition); border-radius:8px; padding:10px 12px; }
.pfad b { display:block; }
.actions { display:flex; gap:10px; align-items:center; margin-top:24px; flex-wrap:wrap; }
.actions .spacer { flex:1; }
.unclear { display:flex; align-items:center; gap:8px; color:var(--muted); font-size:14px; }
dialog { border:none; border-radius:16px; padding:0; box-shadow: var(--shadow); max-width: 640px; width: 92%; }
dialog::backdrop { background: rgba(15,22,35,.45); }
.modal { padding: 24px; }
.modal h2 { margin:0 0 6px; }
pre.export { background:#0f1623; color:#d6e2f0; padding:14px; border-radius:10px; overflow:auto; max-height:320px; font-size:13px; }
.stat { display:flex; gap:20px; margin:14px 0; }
.stat div b { display:block; font-size:24px; }
.stat div span { color:var(--muted); font-size:12px; }
.note { font-size:12px; color:var(--muted); margin-top:18px; text-align:center; }
</style>
</head>
<body>
<header>
<div class="brand">SLC&nbsp;<b>Companion</b></div>
<span class="tag">Prototyp · v0.4</span>
<div class="scenario">
<select id="scenarioSel" title="Action Card / Szenario"></select>
</div>
<div class="spacer"></div>
<button class="ghost" id="resetBtn" title="Durchlauf zurücksetzen">Zurücksetzen</button>
<button class="primary" id="debriefBtn">Debrief</button>
</header>
<div class="progress"><div id="progressBar" style="width:0%"></div></div>
<div class="layout">
<aside id="stationList"></aside>
<main><div class="card" id="panel"></div></main>
</div>
<dialog id="debriefDlg">
<div class="modal">
<h2>Debrief-Export</h2>
<p style="color:var(--muted);margin-top:0">Lokale Auswertung dieses Durchlaufs — nichts wird hochgeladen.</p>
<div class="stat" id="debriefStat"></div>
<pre class="export" id="debriefText"></pre>
<div class="actions">
<button class="ghost" id="copyBtn">In Zwischenablage</button>
<div class="spacer"></div>
<button class="primary" id="closeDebrief">Schließen</button>
</div>
</div>
</dialog>
<script>
/* =========================================================================
BEISPIELDATEN — Auszug aus den Lifecycle-YAMLs (Design, Gate 1, Operation).
Im Echtbetrieb generiert ein Build-Skript questions.json aus
service-lifecycle_*.yaml + spm_rollen.yaml (keine Doppelpflege).
========================================================================= */
const ROLLEN = {
spm:"Service-Portfolio-Manager", sor:"Service Operations Runde (SOR)",
service_owner:"Service Owner", al_basis_cloud:"AL Basis & Cloud",
al_applikationen:"AL Applikationen", support_manager:"Support Manager",
problem_manager:"Problem Manager", projektleitung:"Projektleitung",
betriebsteam:"Betriebsteam", service_support_team:"Service-Support Team",
projektteam:"Projektteam", queue_koordinator:"Queue Koordinator",
first_level_agent:"1st Level Agent", second_level_agent:"2nd Level Agent",
testmanagement:"Testmanagement", architektur:"Architektur",
lieferant:"Lieferant", operations_manager:"Betrieb (AL B&C / AL App)"
};
const PHASEN = {
design:{label:"Design", color:"var(--design)"},
transition:{label:"Transition", color:"var(--transition)"},
operation:{label:"Operation", color:"var(--operation)"},
support:{label:"Support", color:"var(--support)"},
review:{label:"Review", color:"var(--review)"}
};
const SZENARIEN = [
"Neues Fachverfahren wird eingeführt",
"Cloud-Migration eines Bestandsservices",
"Strategiewechsel: Service soll abgelöst werden"
];
const STATIONEN = [
{ id:"ds_01", phase:"design", typ:"aktivitaet",
name:"Definieren der erforderlichen Service-Eigenschaften",
beschreibung:"Grundlegende Definition des neuen oder geänderten Services aus fachlicher Perspektive. Startpunkt für die Service-Definition als zentrales Artefakt.",
umfasst:["Zweck, Nutzen, Zielgruppen","Utility & Warranty","SLA-/SLO-Anforderungen","Abhängigkeiten zu unterstützenden Services","Fachliche & technische Anforderungen"],
artefakt:"Service-Definition (Entwurf)",
raci:[["service_owner","A"],["projektleitung","R"],["betriebsteam","C"],["architektur","C"],["spm","C"]],
quiz:[
{frage:"Wer trägt die Ergebnisverantwortung (A) für diese Aktivität?",
optionen:["Service Owner","Projektleitung","SPM","Architektur"], richtig:0,
expl:"Der (designierte) Service Owner ist Accountable; die Projektleitung führt operativ aus (R)."},
{frage:"Welches zentrale Artefakt entsteht hier?",
optionen:["Betriebshandbuch","Service-Definition","Review-Bericht","Incident Record"], richtig:1,
expl:"Die Service-Definition ist das zentrale Artefakt der Design-Phase."}
]},
{ id:"ds_02", phase:"design", typ:"aktivitaet",
name:"Designen der Service- und Service-Management-Komponenten",
beschreibung:"Einarbeiten der Service-Eigenschaften in ein vollständiges Designmodell.",
umfasst:["Servicearchitektur (Komponenten, Schnittstellen)","Design der Betriebs- & Supportprozesse","Sicherheit/Compliance/Datenschutz","Monitoring & Reporting","Rollenmodell"],
artefakt:"Service Design Document",
raci:[["service_owner","A"],["architektur","R"],["projektteam","R"],["spm","I"]],
quiz:[
{frage:"Wer führt das Architektur-Design operativ aus (R)?",
optionen:["SPM","Service Owner","Architektur","Support Manager"], richtig:2,
expl:"Architektur und Projektteam sind Responsible; der Service Owner bleibt Accountable."}
]},
{ id:"ds_03", phase:"design", typ:"aktivitaet",
name:"Beschreiben des Vorgehens zur Implementierung",
beschreibung:"Planen, WIE der Service organisatorisch eingeführt wird (nicht technisch deployt wird).",
umfasst:["Organisatorische Integration","Rollenübergaben","Anpassung Richtlinien/Prozesse/Tools","Trainings & Kommunikation","Time-to-Operate"],
artefakt:"Implementation Blueprint",
raci:[["service_owner","A"],["projektleitung","R"],["betriebsteam","C"],["service_support_team","C"],["spm","I"]],
quiz:[
{frage:"Was wird hier geplant?",
optionen:["Das technische Deployment","Wie der Service organisatorisch eingeführt wird","Die Incident-Bearbeitung","Die Portfolio-Strategie"], richtig:1,
expl:"ds_03 plant die organisatorische Einführung — bewusst getrennt vom technischen Build."}
]},
{ id:"ds_04", phase:"design", typ:"aktivitaet",
name:"Vorbereiten der Service-Implementierung",
beschreibung:"Finalisieren aller organisatorischen Voraussetzungen für die spätere Übergabe in den Betrieb.",
umfasst:["Abstimmung mit Betrieb & Support","Prozesse/Tools/Strukturen vorbereiten","ELS-Konzept (Early Life Support)","Service vollständig im Portfolio erfasst"],
artefakt:"Übergabe-Readiness (organisatorisch)",
raci:[["service_owner","A"],["projektleitung","R"],["betriebsteam","C"],["service_support_team","C"],["spm","I"]],
quiz:[
{frage:"Wofür steht das ELS-Konzept?",
optionen:["Early Life Support","End of Life Service","Enterprise License System","Externer Lieferanten-Support"], richtig:0,
expl:"Early Life Support = erhöhte Betreuung direkt nach Go-Live (siehe op_01)."}
]},
{ id:"tr_01", phase:"transition", typ:"gate", gateNr:1,
name:"Gate 1: Entwicklung oder Konfiguration?",
beschreibung:"Entry-Gate der Transition: Entscheidung, ob die Service-Komponenten entwickelt oder nur konfiguriert werden. Erfordert SOR-Approval (Budget- und Ressourcenimplikationen).",
umfasst:["Aufwandsschätzung (Build vs. Configure)","Technische Risiken","Budget-Abgleich","ggf. Lieferanten-Einbindung","SOR-Vorlage für Freigabe"],
artefakt:"Gate-Entscheidung + SOR-Vorlage",
pfade:[
["Entwicklung","Neuentwicklung/wesentliche Anpassung → weiter zu tr_02"],
["Konfiguration","Bestehende Komponenten konfigurieren → springt zu tr_05"]
],
pruef:[
["Design-Vollständigkeit","Ist das Service Design Document vollständig?"],
["Budget-Verfügbarkeit","Ist Budget für die Strategie verfügbar?"],
["Projektressourcen","Können Ressourcen mobilisiert werden?"],
["Betriebs-/Support-Bereitschaft","Grundsätzliches Commitment vorhanden?"]
],
raci:[["sor","A"],["service_owner","R"],["projektleitung","R"],["architektur","R"],["spm","C"],["lieferant","I"]],
quiz:[
{frage:"Wer entscheidet an Gate 1 (Accountable)?",
optionen:["Service Owner","SOR","SPM","Projektleitung"], richtig:1,
expl:"Gate 1 ist eine SOR-Gremienentscheidung (Budget-/Ressourcenimplikationen)."},
{frage:"Welche zwei Pfade öffnet Gate 1?",
optionen:["Go oder Stop","Intern oder Extern","Entwicklung oder Konfiguration","Test oder Release"], richtig:2,
expl:"Entwicklung (tr_02) vs. Konfiguration (tr_05) — Konfiguration überspringt tr_02tr_04."}
]},
{ id:"op_01", phase:"operation", typ:"aktivitaet",
name:"Early Life Support (ELS)",
beschreibung:"Phase erhöhter Aufmerksamkeit direkt nach Go-Live: produktiv, aber intensiver beobachtet und betreut als im Normalbetrieb.",
umfasst:["Erhöhte Monitoring-Bereitschaft","Direkte Eskalation zum Projektteam","Schnelles Troubleshooting","Erste Incidents aufarbeiten","ELS-Exit-Entscheidung (SO)"],
artefakt:"Stabilisierter Service (ELS-Exit)",
raci:[["service_owner","A"],["support_manager","R"],["operations_manager","R"],["projektteam","C"],["spm","I"]],
quiz:[
{frage:"Wer entscheidet über den ELS-Exit?",
optionen:["SOR","Service Owner","Support Manager","Betrieb"], richtig:1,
expl:"Der Service Owner entscheidet eigenständig (Informationspflicht an SPM)."}
]},
{ id:"op_02", phase:"operation", typ:"aktivitaet",
name:"Bereitstellen von Leitlinien für den Service-Betrieb",
beschreibung:"Strukturelle Grundlage für den täglichen Servicebetrieb.",
umfasst:["Betriebshandbuch bereitstellen/pflegen","Betriebsprozesse & Arbeitsanweisungen","Rollen, Eskalation, Kommunikation klar","Standard Changes & Known Errors"],
artefakt:"Betriebshandbuch",
raci:[["service_owner","A"],["operations_manager","R"]],
quiz:[
{frage:"Welches Artefakt ist hier zentral?",
optionen:["Service-Definition","Betriebshandbuch","Testbericht","Gate-Vorlage"], richtig:1,
expl:"Das Betriebshandbuch ist die strukturelle Grundlage des laufenden Betriebs."}
]}
];
/* ====================== STATE ====================== */
const LS_KEY = "slc-companion-proto";
function defaultState(){
return { scenario:0, index:0, stage:"discuss", quizIndex:0,
picks:{}, done:{}, unclear:{} };
}
let S = load();
function load(){ try{ return Object.assign(defaultState(), JSON.parse(localStorage.getItem(LS_KEY)||"{}")); }catch(e){ return defaultState(); } }
function save(){ localStorage.setItem(LS_KEY, JSON.stringify(S)); }
/* ====================== HELPERS ====================== */
const $ = s => document.querySelector(s);
const cur = () => STATIONEN[S.index];
function pkey(sid, qi){ return sid+"#"+qi; }
function roleLabel(id){ return ROLLEN[id] || id; }
/* ====================== RENDER: SIDEBAR ====================== */
function renderList(){
const groups = {};
STATIONEN.forEach((st,i)=>{ (groups[st.phase]=groups[st.phase]||[]).push({st,i}); });
let html = "";
for(const ph in PHASEN){
if(!groups[ph]) continue;
html += `<h3>${PHASEN[ph].label}</h3>`;
groups[ph].forEach(({st,i})=>{
const cls = [ "stationItem", i===S.index?"active":"", S.done[st.id]?"done":"" ].join(" ");
html += `<div class="${cls}" data-i="${i}">
<span class="dot" style="background:${PHASEN[ph].color}"></span>
<span class="id">${st.id}</span>
<span class="nm">${st.typ==="gate"?"⛩ ":""}${st.name.length>34?st.name.slice(0,32)+"…":st.name}</span>
${S.unclear[st.id]?'<span class="flag"></span>':''}
</div>`;
});
}
$("#stationList").innerHTML = html;
$("#stationList").querySelectorAll(".stationItem").forEach(el=>{
el.onclick = ()=>{ S.index = +el.dataset.i; S.stage="discuss"; S.quizIndex=0; save(); render(); };
});
const pct = Math.round(Object.keys(S.done).length / STATIONEN.length * 100);
$("#progressBar").style.width = pct+"%";
}
/* ====================== RENDER: PANEL ====================== */
function render(){
renderList();
const st = cur();
const ph = PHASEN[st.phase];
const chip = st.typ==="gate"
? `<span class="phaseChip gateChip">⛩ Gate ${st.gateNr}</span>`
: `<span class="phaseChip" style="background:${ph.color}">${ph.label}</span>`;
let body = `
${chip}
<div class="stationName">${st.name}</div>
<div class="stationId">${st.id}</div>
<div class="token">Action-Stein: <b>${SZENARIEN[S.scenario]}</b></div>
`;
if(S.stage==="discuss") body += renderDiscuss(st);
else if(S.stage==="quiz") body += renderQuiz(st);
else body += renderReveal(st);
$("#panel").innerHTML = body;
wire(st);
}
function renderDiscuss(st){
return `
<div class="step">
<div class="stepHead"><span class="n">1</span> Diskussion · App noch zu</div>
<div class="discuss">
<strong>Besprecht in der Gruppe — anhand der Kurzbezeichnung:</strong>
<ul>
<li>Was passiert hier konkret für euer Szenario?</li>
<li>Wer macht es? Steckt die Rollen-Figuren ins <b>Aktiv-Feld (R/A/C/I)</b>.</li>
<li>Welches Artefakt entsteht?</li>
</ul>
</div>
</div>
<div class="actions">
<div class="spacer"></div>
<button class="primary" id="toQuiz">Quiz starten →</button>
</div>`;
}
function renderQuiz(st){
const qi = S.quizIndex, q = st.quiz[qi];
const picked = S.picks[pkey(st.id,qi)];
const answered = picked !== undefined;
const opts = q.optionen.map((o,i)=>{
let cls = "opt";
if(answered){
if(i===q.richtig) cls+=" correct";
else if(i===picked) cls+=" wrong";
} else if(i===picked) cls+=" sel";
const mark = answered ? (i===q.richtig?'<span class="mark" style="color:var(--ok)"></span>': (i===picked?'<span class="mark" style="color:var(--bad)"></span>':'')) : '';
return `<button class="${cls}" data-opt="${i}" ${answered?'disabled':''}>${o}${mark}</button>`;
}).join("");
return `
<div class="step">
<div class="stepHead"><span class="n">2</span> Quiz · Frage ${qi+1} / ${st.quiz.length}</div>
<div class="q">
<div class="frage">${q.frage}</div>
<div class="opts">${opts}</div>
${answered ? `<div class="qExpl">${q.expl}</div>` : ``}
</div>
</div>
<div class="actions">
<button class="ghost" id="backDiscuss">← Diskussion</button>
<div class="spacer"></div>
${answered
? (qi < st.quiz.length-1
? `<button class="primary" id="nextQ">Nächste Frage →</button>`
: `<button class="primary" id="toReveal">Auflösung anzeigen →</button>`)
: `<button class="primary" disabled>Antwort wählen…</button>`}
</div>`;
}
function renderReveal(st){
const raci = st.raci.map(([r,c])=>`<tr><td>${roleLabel(r)}</td><td><span class="raciBadge raci-${c}">${c}</span></td></tr>`).join("");
let extra = "";
if(st.typ==="gate"){
extra += `<h4>Entscheidungspfade</h4><div class="pfade">` +
st.pfade.map(([n,d])=>`<div class="pfad"><b>${n}</b><span style="color:var(--muted)">${d}</span></div>`).join("") + `</div>`;
extra += `<h4>Prüf-Dimensionen</h4><ul>` + st.pruef.map(([n,d])=>`<li><b>${n}</b> — ${d}</li>`).join("") + `</ul>`;
}
return `
<div class="step reveal">
<div class="stepHead"><span class="n">3</span> Auflösung & Reflexion</div>
<p>${st.beschreibung}</p>
<h4>Umfasst</h4>
<ul>${st.umfasst.map(u=>`<li>${u}</li>`).join("")}</ul>
<h4>Rollen / RACI</h4>
<table class="raci"><thead><tr><th>Rolle</th><th>RACI</th></tr></thead><tbody>${raci}</tbody></table>
<h4>Artefakt</h4><p>${st.artefakt}</p>
${extra}
</div>
<div class="actions">
<label class="unclear"><input type="checkbox" id="unclear" ${S.unclear[st.id]?'checked':''}/> War unklar</label>
<div class="spacer"></div>
${S.index < STATIONEN.length-1
? `<button class="primary" id="nextStation">Nächste Station →</button>`
: `<button class="primary" id="finish">Durchlauf abschließen</button>`}
</div>`;
}
/* ====================== WIRING ====================== */
function wire(st){
const b = id => $("#"+id);
if(b("toQuiz")) b("toQuiz").onclick = ()=>{ S.stage="quiz"; S.quizIndex=0; save(); render(); };
if(b("backDiscuss")) b("backDiscuss").onclick = ()=>{ S.stage="discuss"; save(); render(); };
$("#panel").querySelectorAll(".opt[data-opt]").forEach(el=>{
el.onclick = ()=>{ S.picks[pkey(st.id,S.quizIndex)] = +el.dataset.opt; save(); render(); };
});
if(b("nextQ")) b("nextQ").onclick = ()=>{ S.quizIndex++; save(); render(); };
if(b("toReveal")) b("toReveal").onclick = ()=>{ S.stage="reveal"; save(); render(); };
if(b("unclear")) b("unclear").onchange = e=>{ if(e.target.checked) S.unclear[st.id]=true; else delete S.unclear[st.id]; save(); renderList(); };
if(b("nextStation")) b("nextStation").onclick = ()=>{ S.done[st.id]=true; S.index++; S.stage="discuss"; S.quizIndex=0; save(); render(); };
if(b("finish")) b("finish").onclick = ()=>{ S.done[st.id]=true; save(); openDebrief(); };
}
/* ====================== DEBRIEF ====================== */
function quizScore(){
let correct=0, total=0;
STATIONEN.forEach(st=> st.quiz.forEach((q,qi)=>{
const p = S.picks[pkey(st.id,qi)];
if(p!==undefined){ total++; if(p===q.richtig) correct++; }
}));
return {correct,total};
}
function openDebrief(){
const {correct,total} = quizScore();
const doneN = Object.keys(S.done).length;
const unclearList = STATIONEN.filter(st=>S.unclear[st.id]);
$("#debriefStat").innerHTML = `
<div><b>${doneN}/${STATIONEN.length}</b><span>Stationen</span></div>
<div><b>${total?Math.round(correct/total*100):0}%</b><span>Quiz richtig (${correct}/${total})</span></div>
<div><b>${unclearList.length}</b><span>als unklar markiert</span></div>`;
let md = `# SLC-Workshop — Debrief\n\n`;
md += `**Szenario:** ${SZENARIEN[S.scenario]}\n`;
md += `**Stationen bearbeitet:** ${doneN}/${STATIONEN.length}\n`;
md += `**Quiz:** ${correct}/${total} richtig\n\n`;
md += `## Als unklar markiert\n`;
md += unclearList.length ? unclearList.map(st=>`- ${st.id} — ${st.name}`).join("\n") : "_keine_";
md += `\n\n## Quiz-Detail\n`;
STATIONEN.forEach(st=> st.quiz.forEach((q,qi)=>{
const p = S.picks[pkey(st.id,qi)];
if(p===undefined) return;
md += `- [${p===q.richtig?"✓":"✗"}] ${st.id}: ${q.frage} → „${q.optionen[p]}"\n`;
}));
$("#debriefText").textContent = md;
$("#debriefDlg").showModal();
}
/* ====================== INIT ====================== */
(function init(){
const sel = $("#scenarioSel");
sel.innerHTML = SZENARIEN.map((s,i)=>`<option value="${i}">${s}</option>`).join("");
sel.value = S.scenario;
sel.onchange = ()=>{ S.scenario = +sel.value; save(); render(); };
$("#debriefBtn").onclick = openDebrief;
$("#closeDebrief").onclick = ()=> $("#debriefDlg").close();
$("#copyBtn").onclick = ()=> navigator.clipboard?.writeText($("#debriefText").textContent);
$("#resetBtn").onclick = ()=>{ if(confirm("Durchlauf zurücksetzen?")){ S=defaultState(); save(); render(); } };
render();
})();
</script>
</body>
</html>