From a922300b96c3e162a0b400cd5554a8c5fcf7a3b4 Mon Sep 17 00:00:00 2001 From: breitenbach76 Date: Sat, 6 Jun 2026 15:55:53 +0200 Subject: [PATCH] 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 --- 04_Tablet-Quiz/README.md | 18 ++-- 04_Tablet-Quiz/app/index.html | 187 +++++++++++++++++----------------- 2 files changed, 106 insertions(+), 99 deletions(-) diff --git a/04_Tablet-Quiz/README.md b/04_Tablet-Quiz/README.md index bb7f2a5..d6739c4 100644 --- a/04_Tablet-Quiz/README.md +++ b/04_Tablet-Quiz/README.md @@ -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-c.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) diff --git a/04_Tablet-Quiz/app/index.html b/04_Tablet-Quiz/app/index.html index 490225e..319dea8 100644 --- a/04_Tablet-Quiz/app/index.html +++ b/04_Tablet-Quiz/app/index.html @@ -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(){ `; $("#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])=>`${roleLabel(r)}${c}`).join(""); + return `${rows}
RolleRACI
`; +} + function renderRun(){ renderList(); const st = cur(); @@ -1036,7 +1061,6 @@ function renderRun(){ const chip = st.typ==="gate" ? `⛩ Gate ${st.gateNr}` : `${ph.label}`; - let body = ` ${chip}
${st.name}
@@ -1044,89 +1068,47 @@ function renderRun(){
Action Card: ${USE_CASES[S.service].service} ${CHANGE_TYPES[S.change]}
${acard(S.service,S.change).titel} — ${acard(S.service,S.change).text}
-
- `; - - if(S.stage==="discuss") body += renderDiscuss(st); - else if(S.stage==="quiz") body += renderQuiz(st); - else body += renderReveal(st); - + `; + 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 `
-
1 Diskussion · App noch zu
+
1 Handeln am Brett
- Besprecht in der Gruppe — anhand der Kurzbezeichnung: + Besprecht und legt am Brett ab:
    -
  • Was passiert hier konkret für euer Szenario?
  • -
  • Wer macht es? Steckt die Rollen-Figuren ins Aktiv-Feld (R/A/C/I).
  • -
  • Welches Artefakt entsteht?
  • +
  • Wer? Rollen-Figuren ins Aktiv-Feld (R · A · C · I) stellen.
  • +
  • Was passiert? Kurz klären, was hier für euren Change getan wird.
  • +
  • Artefakt? Passende Artefaktkarte in die Service-Akte legen (oder Status weiterschieben).
+
+ Zeig mir (wenn ihr nicht weiterkommt) +
${raciTable(st)}

Artefakt: ${st.artefakt}

+
- -
`; -} - -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?'': (i===picked?'':'')) : ''; - return ``; - }).join(""); - return ` -
-
2 Quiz · Frage ${qi+1} / ${st.quiz.length}
-
-
${q.frage}
-
${opts}
- ${answered ? `
${q.expl}
` : ``} -
-
-
- -
- ${answered - ? (qi < st.quiz.length-1 - ? `` - : ``) - : ``} +
`; } +/* Aktivitaet — Takt 2: Aufloesung & Abgleich */ function renderReveal(st){ - const raci = st.raci.map(([r,c])=>`${roleLabel(r)}${c}`).join(""); - let extra = ""; - if(st.pfade){ - extra += `

Entscheidungspfade

` + - st.pfade.map(([n,d])=>`
${n}${d}
`).join("") + `
`; - } - if(st.pruef){ - extra += `

Prüf-Dimensionen

    ` + st.pruef.map(([n,d])=>`
  • ${n} — ${d}
  • `).join("") + `
`; - } return `
-
3 Auflösung & Reflexion
+
2 Auflösung & Abgleich

${st.beschreibung}

-

Umfasst

-
    ${st.umfasst.map(u=>`
  • ${u}
  • `).join("")}
-

Rollen / RACI

- ${raci}
RolleRACI
+

Umfasst

    ${st.umfasst.map(u=>`
  • ${u}
  • `).join("")}
+

Rollen / RACI

${raciTable(st)}

Artefakt

${st.artefakt}

- ${extra} +

Gleicht euer Brett ab — Figuren/Artefakt korrigieren, falls nötig.

@@ -1137,19 +1119,58 @@ function renderReveal(st){
`; } +/* Gate — Entscheidung nach Kriterien */ +function renderGate(st){ + const keeper = (st.raci.find(([r,c])=>c==="A")||[])[0]; + const pruef = (st.pruef||[]).map(([n,d])=>`
  • ${n} — ${d}
  • `).join(""); + const opts = (st.pfade||[]).map(([n,d],i)=> + ``).join(""); + return ` +
    +
    Gate-Entscheidung
    +

    ${st.beschreibung}

    +

    Entscheidet: ${roleLabel(keeper)}

    +

    Prüft am Brett (Kriterien)

    +
      ${pruef}
    +

    Liegen die geforderten Artefaktkarten in der Service-Akte? Stehen die Pflicht-Figuren am Gate-Puck? Dann entscheidet:

    +
    ${opts}
    +
    `; +} + +/* 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") + ? `

    Reicht die Ressourcen-/Entscheidungshoheit der SOR nicht (zusätzliche Mittel nötig), wird der Change zum Demand → über DPM ans Mission Board.

    ` : ``; + return ` +
    +
    Entscheidung getroffen
    +

    ${pf[0]}

    ${pf[1]}

    + ${sorNote} +

    Legt den passenden Entscheidungs-Chip ans Gate.

    +
    +
    + +
    + +
    `; +} + /* ====================== 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 = ` -
    ${doneN}/${STATIONEN.length}Stationen
    -
    ${total?Math.round(correct/total*100):0}%Quiz richtig (${correct}/${total})
    +
    ${doneN}/${STATIONEN.length}Stationen bearbeitet
    ${unclearList.length}als unklar markiert
    `; 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();