v22
This commit is contained in:
parent
a504511ba4
commit
61b6ec23eb
2 changed files with 66 additions and 21 deletions
|
|
@ -173,8 +173,9 @@
|
||||||
.pfad b { display:block; }
|
.pfad b { display:block; }
|
||||||
|
|
||||||
/* ---- Minimal Run-Screens ---- */
|
/* ---- Minimal Run-Screens ---- */
|
||||||
.sHead{display:flex;align-items:center;gap:10px}
|
.sHead{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
||||||
.sHead .sId{color:var(--muted);font-size:13px;font-variant-numeric:tabular-nums}
|
.sHead .phaseChip{font-size:15px;padding:8px 18px;letter-spacing:.8px}
|
||||||
|
.sHead .sId{color:var(--ink);font-size:18px;font-weight:800;font-variant-numeric:tabular-nums}
|
||||||
.sTitle{font-size:22px;font-weight:700;line-height:1.25;margin:10px 0 4px}
|
.sTitle{font-size:22px;font-weight:700;line-height:1.25;margin:10px 0 4px}
|
||||||
.caseLine{color:var(--muted);font-size:13px;margin:0 0 22px}
|
.caseLine{color:var(--muted);font-size:13px;margin:0 0 22px}
|
||||||
.lead{font-size:16px;margin:0 0 16px}
|
.lead{font-size:16px;margin:0 0 16px}
|
||||||
|
|
@ -182,6 +183,13 @@
|
||||||
.todo li{margin:8px 0}
|
.todo li{margin:8px 0}
|
||||||
.crit{margin:8px 0 0;padding-left:20px;color:var(--muted)}
|
.crit{margin:8px 0 0;padding-left:20px;color:var(--muted)}
|
||||||
.crit li{margin:4px 0}
|
.crit li{margin:4px 0}
|
||||||
|
.critHead{font-size:13px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin:16px 0 8px}
|
||||||
|
.gateCrit{display:flex;flex-direction:column;gap:8px;margin:0 0 16px}
|
||||||
|
.critItem{display:flex;align-items:flex-start;gap:10px;text-align:left;padding:11px 14px;border:1px solid var(--line);border-radius:10px;background:var(--soft);color:var(--ink);cursor:pointer;font-size:14px;line-height:1.45;animation:critIn .22s ease}
|
||||||
|
.critItem:hover{border-color:#c9d2dd}
|
||||||
|
.critItem.checked{border-color:rgba(31,157,87,.4);background:var(--okBg)}
|
||||||
|
.critItem .critBox{font-size:17px;flex:none;line-height:1.25}
|
||||||
|
@keyframes critIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
||||||
details.det{margin:8px 0 4px;border-top:1px solid var(--line);padding-top:12px}
|
details.det{margin:8px 0 4px;border-top:1px solid var(--line);padding-top:12px}
|
||||||
details.det>summary{cursor:pointer;color:var(--muted);font-size:14px;font-weight:600;list-style:none}
|
details.det>summary{cursor:pointer;color:var(--muted);font-size:14px;font-weight:600;list-style:none}
|
||||||
details.det>summary::-webkit-details-marker{display:none}
|
details.det>summary::-webkit-details-marker{display:none}
|
||||||
|
|
@ -613,8 +621,8 @@ const STATIONEN = [
|
||||||
expl:"Early Life Support = erhöhte Betreuung direkt nach Go-Live (siehe op_01)."}
|
expl:"Early Life Support = erhöhte Betreuung direkt nach Go-Live (siehe op_01)."}
|
||||||
]},
|
]},
|
||||||
{ id:"tr_01", phase:"transition", typ:"gate", gateNr:1,
|
{ id:"tr_01", phase:"transition", typ:"gate", gateNr:1,
|
||||||
name:"Gate 1: Entwicklung oder Konfiguration?",
|
name:"Gate 1: Darf es in die Transition?",
|
||||||
beschreibung:"Entry-Gate der Transition: Entscheidung, ob die Service-Komponenten entwickelt oder nur konfiguriert werden. Erfordert SOR-Approval (Budget- und Ressourcenimplikationen).",
|
beschreibung:"Entry-Gate der Transition: Die SOR entscheidet zuerst, ob das Vorhaben überhaupt in die Transition darf — und wenn ja, ob die Service-Komponenten neu 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"],
|
umfasst:["Aufwandsschätzung (Build vs. Configure)","Technische Risiken","Budget-Abgleich","ggf. Lieferanten-Einbindung","SOR-Vorlage für Freigabe"],
|
||||||
artefakt:"Gate-Entscheidung + SOR-Vorlage",
|
artefakt:"Gate-Entscheidung + SOR-Vorlage",
|
||||||
pfade:[
|
pfade:[
|
||||||
|
|
@ -1135,7 +1143,7 @@ function defaultState(){
|
||||||
freigabeDone:false, freigabeWrong:null,
|
freigabeDone:false, freigabeWrong:null,
|
||||||
entryDone:false, entryWrong:null,
|
entryDone:false, entryWrong:null,
|
||||||
bonusReveal:false, bonusDone:{}, servicesDone:{}, akteFlash:null,
|
bonusReveal:false, bonusDone:{}, servicesDone:{}, akteFlash:null,
|
||||||
index:0, stage:"discuss", quizIndex:0,
|
index:0, stage:"discuss", quizIndex:0, gateDeciderDone:false, gateCrit:0,
|
||||||
actStep:0, actReveal:false, actDone:false, arteWrong:null,
|
actStep:0, actReveal:false, actDone:false, arteWrong:null,
|
||||||
picks:{}, done:{}, akte:{},
|
picks:{}, done:{}, akte:{},
|
||||||
loopback:null, revisit:{}, endReason:null, endGate:null };
|
loopback:null, revisit:{}, endReason:null, endGate:null };
|
||||||
|
|
@ -1495,7 +1503,7 @@ function renderBonusPick(){
|
||||||
const done = S.bonusDone && S.bonusDone[ci];
|
const done = S.bonusDone && S.bonusDone[ci];
|
||||||
return `<button class="deckCard bonusCard ${done?'svcDone':''}" data-c="${ci}" title="${c.titel}">
|
return `<button class="deckCard bonusCard ${done?'svcDone':''}" data-c="${ci}" title="${c.titel}">
|
||||||
<img src="cards/s${S.service}-c${ci}.png" alt="${c.titel}" loading="lazy">
|
<img src="cards/s${S.service}-c${ci}.png" alt="${c.titel}" loading="lazy">
|
||||||
<div class="deckMeta"><b>${c.titel}</b><span>${CHANGE_TYPES[ci]}</span></div>
|
<div class="deckMeta"><b>${c.titel}</b><span>Bonus-Variante</span></div>
|
||||||
${done?'<span class="svcBadge">✓ besprochen</span>':''}
|
${done?'<span class="svcBadge">✓ besprochen</span>':''}
|
||||||
</button>`;
|
</button>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|
@ -1512,7 +1520,7 @@ function renderBonusPick(){
|
||||||
</div>`;
|
</div>`;
|
||||||
$("#panel").querySelectorAll(".bonusCard").forEach(el=>{
|
$("#panel").querySelectorAll(".bonusCard").forEach(el=>{
|
||||||
el.onclick=()=>{ S.change=+el.dataset.c; S.mode="bonus";
|
el.onclick=()=>{ S.change=+el.dataset.c; S.mode="bonus";
|
||||||
S.classifyDone=true; S.classifyWrong=null; // Typ ist auf der Karte ausgewiesen → nur Begründung, kein Quiz
|
S.classifyDone=false; S.classifyWrong=null; // Change-Art ist auf der Karte verdeckt → Gruppe bestimmt sie selbst (Quiz)
|
||||||
S.freigabeDone=false; S.freigabeWrong=null;
|
S.freigabeDone=false; S.freigabeWrong=null;
|
||||||
S.entryDone=false; S.entryWrong=null; S.bonusReveal=false;
|
S.entryDone=false; S.entryWrong=null; S.bonusReveal=false;
|
||||||
S.view="classify"; save(); draw(); };
|
S.view="classify"; save(); draw(); };
|
||||||
|
|
@ -1584,7 +1592,7 @@ function enterStation(idx){
|
||||||
}
|
}
|
||||||
S.index = idx;
|
S.index = idx;
|
||||||
S.stage = STATIONEN[idx].typ==="gate" ? "gate" : "act";
|
S.stage = STATIONEN[idx].typ==="gate" ? "gate" : "act";
|
||||||
S.gatePick = null; S.quizIndex = 0;
|
S.gatePick = null; S.quizIndex = 0; S.gateDeciderDone = false; S.gateCrit = 0;
|
||||||
S.actStep = 0; S.actReveal = false; S.actDone = false; S.arteWrong = null;
|
S.actStep = 0; S.actReveal = false; S.actDone = false; S.arteWrong = null;
|
||||||
S.akteFlash = null; document.body.classList.remove("akteOpen");
|
S.akteFlash = null; document.body.classList.remove("akteOpen");
|
||||||
}
|
}
|
||||||
|
|
@ -1773,7 +1781,28 @@ function renderActivity(st){
|
||||||
/* Gate — Entscheidung nach Kriterien */
|
/* Gate — Entscheidung nach Kriterien */
|
||||||
function renderGate(st){
|
function renderGate(st){
|
||||||
const keeper = (st.raci.find(([r,c])=>c==="A")||[])[0];
|
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 sorNote = (keeper==="sor")
|
||||||
|
? `<p class="muted" style="margin:8px 0 0">Die <b>SOR</b> ist ein <b>Gremium</b> (SPM · Betrieb · Support-Manager · Service Owner …), das am Gate-Puck zusammenkommt — keine Einzelfigur.</p>` : ``;
|
||||||
|
const revisitNote = (S.revisit && S.revisit[st.id])
|
||||||
|
? `<div class="hint ok">↩ Erneute Vorlage nach Nacharbeit — prüft erneut und entscheidet neu.</div>` : ``;
|
||||||
|
|
||||||
|
// Schritt 1: Entscheider ermitteln (nur Aufforderung — Figuren auf den Gate-Puck)
|
||||||
|
if(!S.gateDeciderDone){
|
||||||
|
return `
|
||||||
|
${revisitNote}
|
||||||
|
<div class="frageBox" style="border-left-color:var(--ink)">
|
||||||
|
<div class="frageLabel">Schritt 1 · Entscheider ermitteln</div>
|
||||||
|
<b>Wer entscheidet an diesem Gate?</b> Stellt die Entscheider-Figur(en) auf den <b>Gate-Puck</b>.
|
||||||
|
</div>
|
||||||
|
<p class="lead"><b>Entscheidet:</b> ${roleLabel(keeper)}</p>
|
||||||
|
${sorNote}
|
||||||
|
<div class="actions">
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<button class="primary" id="gateDeciderNext">Weiter → Entscheidung</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schritt 2: Prüf-Kriterien (nacheinander) + Entscheidung
|
||||||
// Harte Artefakt-Kopplung: Gate "oeffnet" nur mit den geforderten Artefakten in der Akte.
|
// Harte Artefakt-Kopplung: Gate "oeffnet" nur mit den geforderten Artefakten in der Akte.
|
||||||
const req = GATE_REQ[st.id] || [];
|
const req = GATE_REQ[st.id] || [];
|
||||||
const missing = req.filter(a => !(S.akte && S.akte[a]));
|
const missing = req.filter(a => !(S.akte && S.akte[a]));
|
||||||
|
|
@ -1783,21 +1812,31 @@ function renderGate(st){
|
||||||
const reqLine = req.length === 0 ? `` : blocked
|
const reqLine = req.length === 0 ? `` : blocked
|
||||||
? `<div class="gateReq bad">🔒 Gate öffnet nicht — es fehlt in der Akte: ${missing.map(a=>a+" "+ARTEFAKTE[a].name).join(" · ")}. Erzeugt diese Artefakte zuerst.</div>`
|
? `<div class="gateReq bad">🔒 Gate öffnet nicht — es fehlt in der Akte: ${missing.map(a=>a+" "+ARTEFAKTE[a].name).join(" · ")}. Erzeugt diese Artefakte zuerst.</div>`
|
||||||
: `<div class="gateReq ok">✓ Alle geforderten Artefakte liegen in der Akte (${req.join(" · ")}).</div>`;
|
: `<div class="gateReq ok">✓ Alle geforderten Artefakte liegen in der Akte (${req.join(" · ")}).</div>`;
|
||||||
const revisitNote = (S.revisit && S.revisit[st.id])
|
|
||||||
? `<div class="hint ok">↩ Erneute Vorlage nach Nacharbeit — prüft erneut und entscheidet neu.</div>` : ``;
|
// Prüf-Kriterien als nacheinander erscheinende Checkboxen (nur Anzeige, nicht blockierend)
|
||||||
|
const pruef = st.pruef || [];
|
||||||
|
const shown = Math.min(S.gateCrit||0, pruef.length); // Anzahl abgehakter Kriterien
|
||||||
|
const critItems = pruef.map(([n,d],i)=>{
|
||||||
|
if(i > shown) return ""; // noch nicht eingeblendet
|
||||||
|
const checked = i < shown;
|
||||||
|
return `<button class="critItem ${checked?'checked':''}" data-ci="${i}">
|
||||||
|
<span class="critBox">${checked?'☑':'☐'}</span>
|
||||||
|
<span><b>${n}</b> — ${d}</span>
|
||||||
|
</button>`;
|
||||||
|
}).join("");
|
||||||
|
const allChecked = pruef.length>0 && shown >= pruef.length;
|
||||||
|
const critWrap = pruef.length ? `
|
||||||
|
<div class="critHead">Worum geht's & Prüf-Kriterien</div>
|
||||||
|
<p class="muted" style="margin:0 0 10px">${st.beschreibung}</p>
|
||||||
|
<div class="gateCrit">${critItems}</div>
|
||||||
|
${allChecked?`<p class="muted" style="margin:0 0 16px">Alle Kriterien geprüft. Geforderte Artefakte in der Akte (siehe oben)? Pflicht-Figuren am Gate-Puck?</p>`:``}` : ``;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${revisitNote}
|
${revisitNote}
|
||||||
<p class="lead"><b>Entscheidet:</b> ${roleLabel(keeper)}</p>
|
<p class="lead"><b>Entscheidet:</b> ${roleLabel(keeper)}</p>
|
||||||
${reqLine}
|
${reqLine}
|
||||||
<div class="choiceGrid">${opts}</div>
|
${critWrap}
|
||||||
<details class="det">
|
<div class="choiceGrid">${opts}</div>`;
|
||||||
<summary>Worum geht's & Prüf-Kriterien</summary>
|
|
||||||
<div>
|
|
||||||
<p>${st.beschreibung}</p>
|
|
||||||
<ul class="crit">${pruef}</ul>
|
|
||||||
<p class="muted">Geforderte Artefakte in der Akte (siehe oben)? Pflicht-Figuren am Gate-Puck?</p>
|
|
||||||
</div>
|
|
||||||
</details>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gate — Konsequenz der Entscheidung */
|
/* Gate — Konsequenz der Entscheidung */
|
||||||
|
|
@ -1859,6 +1898,12 @@ function wire(st){
|
||||||
if(b("nextStation")) b("nextStation").onclick = ()=>{ S.done[st.id]=true; enterStation(S.index+1); 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; S.view="end"; S.endReason="done"; save(); draw(); };
|
if(b("finish")) b("finish").onclick = ()=>{ S.done[st.id]=true; S.view="end"; S.endReason="done"; save(); draw(); };
|
||||||
// Gate
|
// Gate
|
||||||
|
if(b("gateDeciderNext")) b("gateDeciderNext").onclick = ()=>{ S.gateDeciderDone=true; save(); draw(); };
|
||||||
|
$("#panel").querySelectorAll(".critItem[data-ci]").forEach(el=>{
|
||||||
|
el.onclick = ()=>{ const i=+el.dataset.ci;
|
||||||
|
S.gateCrit = (i < (S.gateCrit||0)) ? i : i+1; // abgehaktes wieder lösen vs. nächstes aufdecken
|
||||||
|
save(); draw(); };
|
||||||
|
});
|
||||||
$("#panel").querySelectorAll(".choiceGrid .choice[data-i]").forEach(el=>{
|
$("#panel").querySelectorAll(".choiceGrid .choice[data-i]").forEach(el=>{
|
||||||
el.onclick = ()=>{ S.gatePick=+el.dataset.i; S.stage="gateDone"; save(); draw(); };
|
el.onclick = ()=>{ S.gatePick=+el.dataset.i; S.stage="gateDone"; save(); draw(); };
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/* Service Worker — SLC-Workshop Companion (App-Shell, offline-first) */
|
/* Service Worker — SLC-Workshop Companion (App-Shell, offline-first) */
|
||||||
const CACHE = "slc-companion-v28";
|
const CACHE = "slc-companion-v29";
|
||||||
const SHELL = ["./", "index.html", "manifest.webmanifest", "icon.svg"];
|
const SHELL = ["./", "index.html", "manifest.webmanifest", "icon.svg"];
|
||||||
// Action-Card-Grafiken (cards/s<service>-c<change>.png) fuer Offline vorab cachen (alle 24).
|
// Action-Card-Grafiken (cards/s<service>-c<change>.png) fuer Offline vorab cachen (alle 24).
|
||||||
const CARDS = [];
|
const CARDS = [];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue