.
This commit is contained in:
parent
36471297f5
commit
917bf1d613
3 changed files with 72 additions and 91 deletions
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>SLC-Workshop — Companion-App</title>
|
||||
<meta name="description" content="Begleit-App zum SLC-Workshop-Tabletop: führt durch die Stationen, vermittelndes Quiz, Auflösung und Debrief. Offline lauffähig." />
|
||||
<meta name="description" content="Begleit-App zum SLC-Workshop-Tabletop: führt durch die Stationen, vermittelndes Quiz und Auflösung. Offline lauffähig." />
|
||||
<meta name="theme-color" content="#1d2430" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
<link rel="apple-touch-icon" href="icon.svg" />
|
||||
|
|
@ -120,16 +120,6 @@
|
|||
.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; }
|
||||
|
||||
/* Setup-Screens (Action Card / Startpunkt) */
|
||||
body:not(.runMode) aside { display:none; }
|
||||
body:not(.runMode) .layout { grid-template-columns: 1fr; }
|
||||
|
|
@ -193,7 +183,6 @@
|
|||
<div id="cardBadge" class="cardBadge"></div>
|
||||
<div class="spacer"></div>
|
||||
<button class="ghost" id="resetBtn" title="Neue Action Card / Durchlauf zurücksetzen">Neu starten</button>
|
||||
<button class="primary" id="debriefBtn">Debrief</button>
|
||||
</header>
|
||||
<div class="progress"><div id="progressBar" style="width:0%"></div></div>
|
||||
|
||||
|
|
@ -202,22 +191,6 @@
|
|||
<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>
|
||||
<button class="ghost" id="dlMd">↓ Markdown</button>
|
||||
<button class="ghost" id="dlJson">↓ JSON</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="primary" id="closeDebrief">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
/* Empfohlener Einstiegspunkt je Change-Typ (didaktische Auflösung in Schritt 2).
|
||||
Reihenfolge entspricht CHANGE_TYPES. */
|
||||
|
|
@ -851,7 +824,8 @@ function defaultState(){
|
|||
classifyDone:false, classifyWrong:null,
|
||||
entryDone:false, entryWrong:null,
|
||||
index:0, stage:"discuss", quizIndex:0,
|
||||
picks:{}, done:{}, unclear:{} };
|
||||
picks:{}, done:{}, unclear:{},
|
||||
loopback:null, revisit:{}, endReason:null, endGate:null };
|
||||
}
|
||||
let S = load();
|
||||
function load(){ try{ return Object.assign(defaultState(), JSON.parse(localStorage.getItem(LS_KEY)||"{}")); }catch(e){ return defaultState(); } }
|
||||
|
|
@ -907,6 +881,7 @@ function draw(){
|
|||
if(S.view==="deck") return renderDeck();
|
||||
if(S.view==="classify") return renderClassify();
|
||||
if(S.view==="entry") return renderEntry();
|
||||
if(S.view==="end") return renderEnd();
|
||||
renderRun();
|
||||
}
|
||||
|
||||
|
|
@ -1036,6 +1011,11 @@ const GATE_FLOW = {
|
|||
};
|
||||
function enterStation(idx){
|
||||
idx = Math.max(0, Math.min(idx, STATIONEN.length-1));
|
||||
// Echte Rueckschleife: erreicht man das Gate wieder (oder ueberholt es), Schleife schliessen.
|
||||
if(S.loopback && idx >= S.loopback.untilIdx){
|
||||
if(idx === S.loopback.untilIdx) (S.revisit = S.revisit || {})[STATIONEN[idx].id] = true;
|
||||
S.loopback = null;
|
||||
}
|
||||
S.index = idx;
|
||||
S.stage = STATIONEN[idx].typ==="gate" ? "gate" : "act";
|
||||
S.gatePick = null; S.quizIndex = 0;
|
||||
|
|
@ -1043,9 +1023,15 @@ function enterStation(idx){
|
|||
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==="end"){ S.view="end"; S.endReason="rejected"; S.endGate=st.id; save(); draw(); return; }
|
||||
if(t==="next"){ enterStation(S.index+1); }
|
||||
else { const j = STATIONEN.findIndex(s=>s.id===t); enterStation(j>=0 ? j : S.index+1); }
|
||||
else {
|
||||
const j = STATIONEN.findIndex(s=>s.id===t);
|
||||
const target = j>=0 ? j : S.index+1;
|
||||
// Sprung nach hinten = echte Rueckschleife: Banner setzen, Gate wird danach erneut vorgelegt.
|
||||
if(target < S.index){ S.loopback = { gateId: st.id, gateNr: st.gateNr, untilIdx: S.index }; }
|
||||
enterStation(target);
|
||||
}
|
||||
save(); draw();
|
||||
}
|
||||
|
||||
|
|
@ -1061,7 +1047,11 @@ 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>`;
|
||||
const loopBanner = (S.loopback && S.index < S.loopback.untilIdx)
|
||||
? `<div class="tourBanner">↩ <b>Nacharbeit nach Gate ${S.loopback.gateNr}</b> — überarbeitet die folgenden Stationen; danach wird das Gate erneut vorgelegt und entscheidet neu.</div>`
|
||||
: ``;
|
||||
let body = `
|
||||
${loopBanner}
|
||||
${chip}
|
||||
<div class="stationName">${st.name}</div>
|
||||
<div class="stationId">${st.id}</div>
|
||||
|
|
@ -1083,7 +1073,8 @@ function renderAct(st){
|
|||
<div class="discuss">
|
||||
<strong>Besprecht und legt am Brett ab:</strong>
|
||||
<ul>
|
||||
<li><b>Wer?</b> Rollen-Figuren ins <b>Aktiv-Feld (R · A · C · I)</b> stellen.</li>
|
||||
<li><b>1 · Wer ist beteiligt?</b> Die Figuren der beteiligten Rollen auf die <b>Mulden des Station-Pucks</b> stellen.</li>
|
||||
<li><b>2 · Wie ist die RACI?</b> Dieselben Figuren ins <b>Aktiv-Feld (R · A · C · I)</b> einsortieren.</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>
|
||||
|
|
@ -1125,10 +1116,13 @@ function renderGate(st){
|
|||
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("");
|
||||
const revisitNote = (S.revisit && S.revisit[st.id])
|
||||
? `<div class="hint ok">↩ Erneute Vorlage nach Nacharbeit — prüft die Kriterien erneut und entscheidet neu.</div>` : ``;
|
||||
return `
|
||||
<div class="step">
|
||||
<div class="stepHead"><span class="n">⛩</span> Gate-Entscheidung</div>
|
||||
<p>${st.beschreibung}</p>
|
||||
${revisitNote}
|
||||
<p><b>Entscheidet:</b> ${roleLabel(keeper)}</p>
|
||||
<h4>Prüft am Brett (Kriterien)</h4>
|
||||
<ul>${pruef}</ul>
|
||||
|
|
@ -1164,7 +1158,7 @@ function wire(st){
|
|||
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; enterStation(S.index+1); save(); draw(); };
|
||||
if(b("finish")) b("finish").onclick = ()=>{ S.done[st.id]=true; save(); openDebrief(); };
|
||||
if(b("finish")) b("finish").onclick = ()=>{ S.done[st.id]=true; S.view="end"; S.endReason="done"; save(); draw(); };
|
||||
// Gate
|
||||
$("#panel").querySelectorAll(".choiceGrid .choice[data-i]").forEach(el=>{
|
||||
el.onclick = ()=>{ S.gatePick=+el.dataset.i; S.stage="gateDone"; save(); draw(); };
|
||||
|
|
@ -1173,59 +1167,33 @@ function wire(st){
|
|||
if(b("gateNext")) b("gateNext").onclick = ()=>{ gateGoto(st, S.gatePick||0); };
|
||||
}
|
||||
|
||||
/* ====================== 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};
|
||||
}
|
||||
let DEBRIEF = { md:"", json:null };
|
||||
function download(name, text, mime){
|
||||
const blob = new Blob([text], {type:mime});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url; a.download = name; document.body.appendChild(a); a.click();
|
||||
a.remove(); setTimeout(()=>URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
function openDebrief(){
|
||||
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 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\n`;
|
||||
md += `## Als unklar markiert\n`;
|
||||
md += unclearList.length ? unclearList.map(st=>`- ${st.id} — ${st.name}`).join("\n") : "_keine_";
|
||||
const stamp = new Date().toISOString().slice(0,16).replace("T"," ");
|
||||
const json = {
|
||||
erzeugt: stamp,
|
||||
service: USE_CASES[S.service].service,
|
||||
changeTyp: CHANGE_TYPES[S.change],
|
||||
actionCard: acard(S.service,S.change).titel,
|
||||
ausloeser: acard(S.service,S.change).text,
|
||||
stationenBearbeitet: doneN,
|
||||
stationenGesamt: STATIONEN.length,
|
||||
unklar: unclearList.map(st=>({id:st.id, name:st.name}))
|
||||
};
|
||||
DEBRIEF = { md, json };
|
||||
$("#debriefText").textContent = md;
|
||||
$("#debriefDlg").showModal();
|
||||
/* ====================== ABSCHLUSS-SCREEN ====================== */
|
||||
function renderEnd(){
|
||||
const rejected = S.endReason==="rejected";
|
||||
const gate = rejected ? STATIONEN.find(s=>s.id===S.endGate) : null;
|
||||
const head = rejected ? "Change abgelehnt" : "Durchlauf abgeschlossen";
|
||||
const icon = rejected ? "✗" : "✓";
|
||||
const box = rejected
|
||||
? `<div class="recBox" style="border-left-color:var(--bad)">
|
||||
<h4>Abgelehnt an ${gate ? "Gate "+gate.gateNr : "einem Gate"}</h4>
|
||||
<p style="margin:0;color:var(--muted)">Das Vorhaben wird in dieser Form nicht weiterverfolgt. Eine endgültige Ablehnung erfordert die <b>SOR-Eskalation</b>.</p></div>`
|
||||
: `<div class="recBox">
|
||||
<h4>Service-Lifecycle durchlaufen</h4>
|
||||
<p style="margin:0;color:var(--muted)">Ihr habt den Change von der Einordnung bis zur Umsetzung begleitet. Im Workshop schließt hier das gemeinsame <b>Debriefing am Tisch</b> an (Reflexion der Stationen, offene Fragen).</p></div>`;
|
||||
$("#panel").innerHTML = `
|
||||
<div class="setupHead">Abschluss</div>
|
||||
<h2 class="setupTitle">${icon} ${head}</h2>
|
||||
<div class="hint ${rejected?"bad":"ok"}">${USE_CASES[S.service].service} · ${CHANGE_TYPES[S.change]}</div>
|
||||
${box}
|
||||
<div class="actions">
|
||||
<div class="spacer"></div>
|
||||
<button class="primary" id="endRestart">Neue Action Card →</button>
|
||||
</div>`;
|
||||
$("#endRestart").onclick = ()=>{ S = defaultState(); save(); draw(); };
|
||||
}
|
||||
|
||||
/* ====================== INIT ====================== */
|
||||
(function init(){
|
||||
$("#debriefBtn").onclick = openDebrief;
|
||||
$("#closeDebrief").onclick = ()=> $("#debriefDlg").close();
|
||||
$("#copyBtn").onclick = ()=> navigator.clipboard?.writeText($("#debriefText").textContent);
|
||||
$("#dlMd").onclick = ()=> download("slc-debrief.md", DEBRIEF.md, "text/markdown");
|
||||
$("#dlJson").onclick = ()=> download("slc-debrief.json", JSON.stringify(DEBRIEF.json, null, 2), "application/json");
|
||||
$("#resetBtn").onclick = ()=>{ if(confirm("Neue Action Card ziehen und Durchlauf zurücksetzen?")){ S=defaultState(); save(); draw(); } };
|
||||
draw();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* Service Worker — SLC-Workshop Companion (App-Shell, offline-first) */
|
||||
const CACHE = "slc-companion-v4";
|
||||
const CACHE = "slc-companion-v5";
|
||||
const SHELL = ["./", "index.html", "manifest.webmanifest", "icon.svg"];
|
||||
// Action-Card-Grafiken (cards/s<service>-c<change>.png) fuer Offline vorab cachen (alle 30).
|
||||
const CARDS = [];
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
Chat). Hier steht, **wo wir stehen, was entschieden wurde, was offen ist** und wie man
|
||||
App lokal startet/deployt.
|
||||
|
||||
**Stand:** 2026-06-06 · **Branch:** `feat/redesign-und-companion-app` · **HEAD:** `a922300`
|
||||
**Stand:** 2026-06-06 · **Branch:** `feat/redesign-und-companion-app` · **HEAD:** `3647129`
|
||||
**Remote:** `https://git.1789.cloud/patrick/SLC_Game.git`
|
||||
|
||||
---
|
||||
|
|
@ -49,11 +49,18 @@ Operations/Service-Owner/Support. Mix aus **Vermittlung** (Lifecycle + Stationen
|
|||
### Companion-App (`04_Tablet-Quiz/app/`) — statische PWA
|
||||
**Flow:** Karten-Raster (Action Card ziehen) → **Change-Art klassifizieren** (Legende,
|
||||
„nochmal" bis richtig) → **Phasen-Einstieg** (Lebenszyklus-Phase anklicken, retry) →
|
||||
**Stationen** → **Debrief** (Markdown/JSON-Export).
|
||||
- **Aktivitäts-Station, 2 Takte:** „Handeln am Brett" (Figuren ins RACI-Feld,
|
||||
Artefaktkarte in die Akte; „Zeig mir"-Hilfe) → „Auflösung & Abgleich".
|
||||
**Stationen** → **Abschluss-Screen** („abgeschlossen" oder „abgelehnt").
|
||||
- **Aktivitäts-Station, 2 Takte:** „Handeln am Brett" → „Auflösung & Abgleich".
|
||||
**Figuren-Regel (zweistufig):** (1) beteiligte Rollen auf die **Mulden des
|
||||
Station-Pucks** stellen, (2) dieselben Figuren ins **Aktiv-Feld (R·A·C·I)**
|
||||
einsortieren = RACI-Antwort. Artefaktkarte in die Akte; „Zeig mir"-Hilfe.
|
||||
- **Gate-Station interaktiv:** Kriterien prüfen → Entscheidung → Konsequenz +
|
||||
**Verzweigung** (z. B. Gate 1 „Konfiguration" → springt zu `tr_05`; SOR→DPM→Mission Board).
|
||||
**Verzweigung**. **Echte Rückschleifen:** „Zurück an Build/Transition" springt
|
||||
zurück, blendet einen Nacharbeits-Banner ein und legt das Gate danach **erneut**
|
||||
vor (Hinweis „Erneute Vorlage"). „Ablehnung" → eigener **End-Screen** (SOR-Eskalation).
|
||||
(z. B. Gate 1 „Konfiguration" → `tr_05`; SOR→DPM→Mission Board.)
|
||||
- **Kein App-Debrief mehr:** Der Export-Dialog (Markdown/JSON) wurde entfernt; das
|
||||
Workshop-**Debriefing am Tisch** bleibt (im Done-Screen erwähnt).
|
||||
- **4 Change-Arten** (Major/Normal/Standard/Emergency), 24 Karten. Multiple-Choice
|
||||
ist aus dem Hauptfluss raus (Daten liegen noch in `index.html`, ungenutzt).
|
||||
- **Inhalte sind in `index.html` eingebettet** (noch keine YAML-Pipeline).
|
||||
|
|
@ -67,6 +74,8 @@ Operations/Service-Owner/Support. Mix aus **Vermittlung** (Lifecycle + Stationen
|
|||
**Kanonisch / stabil:**
|
||||
- Puck-System statt Tiles; ein STL für die Bahn; Beschriftung via Etikett.
|
||||
- Aktiv-Feld 2×2; Phasen-Ring; keine Magnete; kein Action-Stein.
|
||||
- **Figuren-Regel (zweistufig):** Station-Puck-Mulden = „welche Rollen sind beteiligt?",
|
||||
Aktiv-Feld (R·A·C·I) = „wie ist die RACI?". An Gates: Pflicht-Figuren am Gate-Puck.
|
||||
|
||||
**Workshop-Arbeitsstand — bewusst NICHT im Blueprint-YAML und NICHT im kanonischen
|
||||
Konzept (`00_Konzept/README_konzept.md`), bis Rückkopplung mit Michael:**
|
||||
|
|
@ -81,9 +90,13 @@ Konzept (`00_Konzept/README_konzept.md`), bis Rückkopplung mit Michael:**
|
|||
- Details/Quelle: `00_Konzept/review-phase_arbeitsstand-frank.md`.
|
||||
|
||||
## 5. Offene Punkte / nächste Schritte
|
||||
- [ ] **Figuren-Regel festzurren:** Aktiv-Feld = RACI-Antwort, Puck-Mulden = nur Gate-Versammlung?
|
||||
- [ ] **Echte Gate-Rückschleifen** (Zurück/Ablehnung) statt vereinfachtem Weiterspringen.
|
||||
- [ ] **Debrief** auf tatsächliche **Pfadlänge** statt „X/39" umstellen (Verzweigung).
|
||||
- [x] **Figuren-Regel festgezurrt** (zweistufig: Puck = beteiligte Rollen, Aktiv-Feld =
|
||||
RACI). In der App umgesetzt. **Offen:** in `materialliste.md` / `board-layout` /
|
||||
`README_konzept.md` nachziehen.
|
||||
- [x] **Echte Gate-Rückschleifen** umgesetzt (Nacharbeits-Banner + erneute Gate-Vorlage;
|
||||
Ablehnung → End-Screen). **Offen ggf.:** Auflagen-Pfad sichtbar vom reinen „Freigabe"
|
||||
unterscheiden (verhält sich aktuell identisch = weiter).
|
||||
- [x] **App-Debrief entfernt** (Punkt „Pfadlänge X/39" damit hinfällig).
|
||||
- [ ] **MC-Quiz** optional als „Wissens-Check" reaktivieren? (Daten sind noch da.)
|
||||
- [ ] **YAML→Inhalts-Pipeline** (Stationsdaten aus `service-lifecycle_*.yaml`) — **braucht Zugriff aufs Blueprint-Repo**.
|
||||
- [ ] Nach **Michael-Freigabe:** kanonisches Konzept (`README_konzept.md`), YAML und ggf. `bauteile-masse.svg`/`visual-prompts` final nachziehen.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue