.
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();
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue