Companion-App: neue Stations-Choreografie (Brett antwortet, App fragt+loest auf)
- Aktivitaets-Station: 2 Takte "Handeln am Brett" (Figuren ins RACI-Feld, Artefaktkarte in die Akte) -> "Aufloesung & Abgleich". MC-Quiz aus dem Flow. "Zeig mir"-Option (einklappbare RACI/Artefakt-Hilfe) als On-Ramp. - Gates interaktiv: Kriterien (pruef) pruefen -> Entscheidung (pfade) waehlen -> Konsequenz + Verzweigung (enterStation/GATE_FLOW: Gate1 Konfig springt zu tr_05, Zurueck/Ablehnung-Routing) + SOR->DPM->Mission-Board-Hinweis. - enterStation()/gateGoto() steuern Stage je Stationstyp; Sidebar-Sprung + Start ueber enterStation. Debrief von MC-Statistik bereinigt. - End-to-end im Browser verifiziert (Gate1 Konfig -> tr_05, Act->Reveal). README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9ecb3d3dfc
commit
a922300b96
2 changed files with 106 additions and 99 deletions
|
|
@ -5,7 +5,7 @@
|
|||
> **Umsetzungsstand:** Die App liegt unter [`app/`](app/) als statische **PWA**
|
||||
> (offline-/kioskfähig). Flow: **Karten-Raster** (Action Card ziehen) → **Change-Art
|
||||
> bestimmen** (mit Legende, „nochmal versuchen" bis richtig) → **Phasen-Einstieg**
|
||||
> (Lebenszyklus-Phase anklicken) → **Stationen** (Diskussion/Quiz/Auflösung) →
|
||||
> (Lebenszyklus-Phase anklicken) → **Stationen** (Handeln am Brett → Auflösung; Gates als Entscheidung) →
|
||||
> **Debrief** mit **Markdown-/JSON-Export**. Inhalte (Stationen, Quizfragen, Use-Cases)
|
||||
> sind derzeit in `app/index.html` eingebettet. Die **finalen Action-Card-Grafiken**
|
||||
> (Freiburg-digital-Layout) liegen in `app/cards/` (`s<service>-c<change>.png`, **24** = 6 Services × 4 Change-Arten; Major/Normal/Standard/Emergency).
|
||||
|
|
@ -68,14 +68,16 @@ steht, sondern in der App liegt.
|
|||
[4] Einstieg finden → Lebenszyklus groß: Phase anklicken (falsch = "nochmal")
|
||||
[5] Los geht's → App nennt Start-Station + Begründung
|
||||
→ App führt ab Start-Station durch die Stationen (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
|
||||
→ Aktivitäts-Station (2 Takte):
|
||||
→ Handeln am Brett: Figuren ins RACI-Feld, Artefaktkarte in die Service-Akte
|
||||
(App fragt; "Zeig mir" als Hilfe, wenn die Gruppe nicht weiterkommt)
|
||||
→ Auflösung & Abgleich: App zeigt RACI/Artefakt → Brett korrigieren, "unklar" markieren
|
||||
→ "Nächste Station"
|
||||
→ an Gates: Gate-Frage + Rollen-Check
|
||||
→ [Ende] → Debrief-Export (unklare Aktivitäten, Quote, Pfad)
|
||||
→ Gate-Station: Kriterien prüfen (Artefakte in der Akte? Pflicht-Figuren?)
|
||||
→ Entscheidung wählen (Go/Auflagen/Zurück/Ablehnung bzw. Entwicklung/Konfiguration)
|
||||
→ Konsequenz + Verzweigung (z.B. Konfiguration überspringt den Build;
|
||||
reicht die SOR-Hoheit nicht → Demand via DPM ans Mission Board)
|
||||
→ [Ende] → Debrief-Export (bearbeitete Stationen, unklare, Pfad)
|
||||
```
|
||||
|
||||
## 5. Funktionsumfang (MVP)
|
||||
|
|
|
|||
|
|
@ -894,7 +894,7 @@ function renderList(){
|
|||
}
|
||||
$("#stationList").innerHTML = html;
|
||||
$("#stationList").querySelectorAll(".stationItem").forEach(el=>{
|
||||
el.onclick = ()=>{ S.index = +el.dataset.i; S.stage="discuss"; S.quizIndex=0; save(); draw(); };
|
||||
el.onclick = ()=>{ enterStation(+el.dataset.i); save(); draw(); };
|
||||
});
|
||||
const pct = Math.round(Object.keys(S.done).length / STATIONEN.length * 100);
|
||||
$("#progressBar").style.width = pct+"%";
|
||||
|
|
@ -1024,11 +1024,36 @@ function renderEntry(){
|
|||
<button class="primary" id="startRun">Los geht's →</button>
|
||||
</div>`;
|
||||
$("#backClassify").onclick=()=>{ S.view="classify"; save(); draw(); };
|
||||
$("#startRun").onclick=()=>{ S.index=recIndex; S.view="run"; S.stage="discuss"; S.quizIndex=0; save(); draw(); };
|
||||
$("#startRun").onclick=()=>{ enterStation(recIndex); S.view="run"; save(); draw(); };
|
||||
}
|
||||
}
|
||||
|
||||
/* ====================== RENDER: RUN (Station) ====================== */
|
||||
const GATE_FLOW = {
|
||||
tr_01: ["next","tr_05"], // Entwicklung / Konfiguration (Konfig ueberspringt Build)
|
||||
tr_09: ["next","next","tr_02","end"], // Freigabe / m. Auflagen / Zurueck an Build / Ablehnung
|
||||
tr_12: ["next","next","tr_10","end"] // Go-Live / m. Auflagen / Zurueck / Ablehnung
|
||||
};
|
||||
function enterStation(idx){
|
||||
idx = Math.max(0, Math.min(idx, STATIONEN.length-1));
|
||||
S.index = idx;
|
||||
S.stage = STATIONEN[idx].typ==="gate" ? "gate" : "act";
|
||||
S.gatePick = null; S.quizIndex = 0;
|
||||
}
|
||||
function gateGoto(st, i){
|
||||
S.done[st.id] = true;
|
||||
const t = (GATE_FLOW[st.id] || [])[i] || "next";
|
||||
if(t==="end"){ save(); openDebrief(); return; }
|
||||
if(t==="next"){ enterStation(S.index+1); }
|
||||
else { const j = STATIONEN.findIndex(s=>s.id===t); enterStation(j>=0 ? j : S.index+1); }
|
||||
save(); draw();
|
||||
}
|
||||
|
||||
function raciTable(st){
|
||||
const rows = st.raci.map(([r,c])=>`<tr><td>${roleLabel(r)}</td><td><span class="raciBadge raci-${c}">${c}</span></td></tr>`).join("");
|
||||
return `<table class="raci"><thead><tr><th>Rolle</th><th>RACI</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
}
|
||||
|
||||
function renderRun(){
|
||||
renderList();
|
||||
const st = cur();
|
||||
|
|
@ -1036,7 +1061,6 @@ function renderRun(){
|
|||
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>
|
||||
|
|
@ -1044,89 +1068,47 @@ function renderRun(){
|
|||
<div class="token">Action Card: <b>${USE_CASES[S.service].service}</b>
|
||||
<span class="ctChip">${CHANGE_TYPES[S.change]}</span>
|
||||
<div class="ctText"><b>${acard(S.service,S.change).titel}</b> — ${acard(S.service,S.change).text}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if(S.stage==="discuss") body += renderDiscuss(st);
|
||||
else if(S.stage==="quiz") body += renderQuiz(st);
|
||||
else body += renderReveal(st);
|
||||
|
||||
</div>`;
|
||||
if(st.typ==="gate") body += (S.stage==="gateDone") ? renderGateDone(st) : renderGate(st);
|
||||
else body += (S.stage==="reveal") ? renderReveal(st) : renderAct(st);
|
||||
$("#panel").innerHTML = body;
|
||||
wire(st);
|
||||
}
|
||||
|
||||
function renderDiscuss(st){
|
||||
/* Aktivitaet — Takt 1: Handeln am Brett (mit "Zeig mir") */
|
||||
function renderAct(st){
|
||||
return `
|
||||
<div class="step">
|
||||
<div class="stepHead"><span class="n">1</span> Diskussion · App noch zu</div>
|
||||
<div class="stepHead"><span class="n">1</span> Handeln am Brett</div>
|
||||
<div class="discuss">
|
||||
<strong>Besprecht in der Gruppe — anhand der Kurzbezeichnung:</strong>
|
||||
<strong>Besprecht und legt am Brett ab:</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>
|
||||
<li><b>Wer?</b> Rollen-Figuren ins <b>Aktiv-Feld (R · A · C · I)</b> stellen.</li>
|
||||
<li><b>Was passiert?</b> Kurz klären, was hier für euren Change getan wird.</li>
|
||||
<li><b>Artefakt?</b> Passende <b>Artefaktkarte in die Service-Akte</b> legen (oder Status weiterschieben).</li>
|
||||
</ul>
|
||||
<details style="margin-top:10px">
|
||||
<summary style="cursor:pointer;color:var(--muted);font-weight:600">Zeig mir (wenn ihr nicht weiterkommt)</summary>
|
||||
<div style="margin-top:8px">${raciTable(st)}<p style="margin:6px 0 0"><b>Artefakt:</b> ${st.artefakt}</p></div>
|
||||
</details>
|
||||
</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>`}
|
||||
<button class="primary" id="toReveal">Auflösen →</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* Aktivitaet — Takt 2: Aufloesung & Abgleich */
|
||||
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.pfade){
|
||||
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>`;
|
||||
}
|
||||
if(st.pruef){
|
||||
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>
|
||||
<div class="stepHead"><span class="n">2</span> Auflösung & Abgleich</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>Umfasst</h4><ul>${st.umfasst.map(u=>`<li>${u}</li>`).join("")}</ul>
|
||||
<h4>Rollen / RACI</h4>${raciTable(st)}
|
||||
<h4>Artefakt</h4><p>${st.artefakt}</p>
|
||||
${extra}
|
||||
<p class="muted">Gleicht euer Brett ab — Figuren/Artefakt korrigieren, falls nötig.</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<label class="unclear"><input type="checkbox" id="unclear" ${S.unclear[st.id]?'checked':''}/> War unklar</label>
|
||||
|
|
@ -1137,19 +1119,58 @@ function renderReveal(st){
|
|||
</div>`;
|
||||
}
|
||||
|
||||
/* Gate — Entscheidung nach Kriterien */
|
||||
function renderGate(st){
|
||||
const keeper = (st.raci.find(([r,c])=>c==="A")||[])[0];
|
||||
const pruef = (st.pruef||[]).map(([n,d])=>`<li><b>${n}</b> — ${d}</li>`).join("");
|
||||
const opts = (st.pfade||[]).map(([n,d],i)=>
|
||||
`<button class="choice" data-i="${i}"><b>${n}</b><br><span style="color:var(--muted);font-weight:400">${d}</span></button>`).join("");
|
||||
return `
|
||||
<div class="step">
|
||||
<div class="stepHead"><span class="n">⛩</span> Gate-Entscheidung</div>
|
||||
<p>${st.beschreibung}</p>
|
||||
<p><b>Entscheidet:</b> ${roleLabel(keeper)}</p>
|
||||
<h4>Prüft am Brett (Kriterien)</h4>
|
||||
<ul>${pruef}</ul>
|
||||
<p class="muted">Liegen die geforderten <b>Artefaktkarten</b> in der Service-Akte? Stehen die <b>Pflicht-Figuren</b> am Gate-Puck? Dann entscheidet:</p>
|
||||
<div class="choiceGrid">${opts}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* Gate — Konsequenz der Entscheidung */
|
||||
function renderGateDone(st){
|
||||
const i = S.gatePick || 0;
|
||||
const pf = st.pfade[i] || st.pfade[0];
|
||||
const keeper = (st.raci.find(([r,c])=>c==="A")||[])[0];
|
||||
const sorNote = (keeper==="sor")
|
||||
? `<p class="muted">Reicht die <b>Ressourcen-/Entscheidungshoheit der SOR</b> nicht (zusätzliche Mittel nötig), wird der Change zum <b>Demand</b> → über DPM ans <b>Mission Board</b>.</p>` : ``;
|
||||
return `
|
||||
<div class="step reveal">
|
||||
<div class="stepHead"><span class="n">⛩</span> Entscheidung getroffen</div>
|
||||
<div class="recBox"><h4>${pf[0]}</h4><p style="margin:0;color:var(--muted)">${pf[1]}</p></div>
|
||||
${sorNote}
|
||||
<p class="muted">Legt den passenden <b>Entscheidungs-Chip</b> ans Gate.</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ghost" id="gateBack">← andere Entscheidung</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="primary" id="gateNext">Weiter →</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ====================== WIRING ====================== */
|
||||
function wire(st){
|
||||
const b = id => $("#"+id);
|
||||
if(b("toQuiz")) b("toQuiz").onclick = ()=>{ S.stage="quiz"; S.quizIndex=0; save(); draw(); };
|
||||
if(b("backDiscuss")) b("backDiscuss").onclick = ()=>{ S.stage="discuss"; save(); draw(); };
|
||||
$("#panel").querySelectorAll(".opt[data-opt]").forEach(el=>{
|
||||
el.onclick = ()=>{ S.picks[pkey(st.id,S.quizIndex)] = +el.dataset.opt; save(); draw(); };
|
||||
});
|
||||
if(b("nextQ")) b("nextQ").onclick = ()=>{ S.quizIndex++; save(); draw(); };
|
||||
if(b("toReveal")) b("toReveal").onclick = ()=>{ S.stage="reveal"; save(); draw(); };
|
||||
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(); draw(); };
|
||||
if(b("nextStation")) b("nextStation").onclick = ()=>{ S.done[st.id]=true; enterStation(S.index+1); save(); draw(); };
|
||||
if(b("finish")) b("finish").onclick = ()=>{ S.done[st.id]=true; save(); openDebrief(); };
|
||||
// Gate
|
||||
$("#panel").querySelectorAll(".choiceGrid .choice[data-i]").forEach(el=>{
|
||||
el.onclick = ()=>{ S.gatePick=+el.dataset.i; S.stage="gateDone"; save(); draw(); };
|
||||
});
|
||||
if(b("gateBack")) b("gateBack").onclick = ()=>{ S.stage="gate"; save(); draw(); };
|
||||
if(b("gateNext")) b("gateNext").onclick = ()=>{ gateGoto(st, S.gatePick||0); };
|
||||
}
|
||||
|
||||
/* ====================== DEBRIEF ====================== */
|
||||
|
|
@ -1170,27 +1191,18 @@ function download(name, text, mime){
|
|||
a.remove(); setTimeout(()=>URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
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>${doneN}/${STATIONEN.length}</b><span>Stationen bearbeitet</span></div>
|
||||
<div><b>${unclearList.length}</b><span>als unklar markiert</span></div>`;
|
||||
let md = `# SLC-Workshop — Debrief\n\n`;
|
||||
md += `**Service:** ${USE_CASES[S.service].service}\n`;
|
||||
md += `**Change-Typ:** ${CHANGE_TYPES[S.change]}\n`;
|
||||
md += `**Action Card:** „${acard(S.service,S.change).titel}“ — ${acard(S.service,S.change).text}\n`;
|
||||
md += `**Stationen bearbeitet:** ${doneN}/${STATIONEN.length}\n`;
|
||||
md += `**Quiz:** ${correct}/${total} richtig\n\n`;
|
||||
md += `**Stationen bearbeitet:** ${doneN}/${STATIONEN.length}\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`;
|
||||
}));
|
||||
const stamp = new Date().toISOString().slice(0,16).replace("T"," ");
|
||||
const json = {
|
||||
erzeugt: stamp,
|
||||
|
|
@ -1200,15 +1212,8 @@ function openDebrief(){
|
|||
ausloeser: acard(S.service,S.change).text,
|
||||
stationenBearbeitet: doneN,
|
||||
stationenGesamt: STATIONEN.length,
|
||||
quiz: { richtig: correct, gesamt: total },
|
||||
unklar: unclearList.map(st=>({id:st.id, name:st.name})),
|
||||
quizDetail: []
|
||||
unklar: unclearList.map(st=>({id:st.id, name:st.name}))
|
||||
};
|
||||
STATIONEN.forEach(st=> st.quiz.forEach((q,qi)=>{
|
||||
const p = S.picks[pkey(st.id,qi)];
|
||||
if(p===undefined) return;
|
||||
json.quizDetail.push({station:st.id, frage:q.frage, gewaehlt:q.optionen[p], richtig:p===q.richtig});
|
||||
}));
|
||||
DEBRIEF = { md, json };
|
||||
$("#debriefText").textContent = md;
|
||||
$("#debriefDlg").showModal();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue