v10
This commit is contained in:
parent
213243f308
commit
7d474a054e
4 changed files with 320 additions and 14 deletions
|
|
@ -68,6 +68,31 @@
|
|||
body.navOpen aside { transform: translateX(0); }
|
||||
.navBackdrop { position:fixed; inset:0; background:rgba(15,22,35,.42); z-index:29; opacity:0; pointer-events:none; transition:opacity .2s; }
|
||||
body.navOpen .navBackdrop { opacity:1; pointer-events:auto; }
|
||||
body.akteOpen .navBackdrop { opacity:1; pointer-events:auto; }
|
||||
/* Service-Akte als Overlay von rechts (Header-Button 📁 Akte) */
|
||||
#akteList {
|
||||
position: fixed; top:0; right:0; bottom:0; width: 340px; max-width: 88vw;
|
||||
background: var(--panel); border-left:1px solid var(--line); padding: 14px;
|
||||
overflow:auto; z-index: 30;
|
||||
transform: translateX(100%); transition: transform .22s ease;
|
||||
box-shadow: 0 0 40px rgba(20,30,50,.18);
|
||||
}
|
||||
body.akteOpen #akteList { transform: translateX(0); }
|
||||
body:not(.runMode) #akteList { display:none; }
|
||||
body:not(.runMode) #akteBtn { display:none; }
|
||||
#akteList h3 { font-size:11px; text-transform:uppercase; letter-spacing:.8px; color:var(--muted); margin:14px 0 4px; }
|
||||
.akteCount { font-size:13px; color:var(--muted); margin:2px 0 6px; font-variant-numeric:tabular-nums; }
|
||||
.akteItem { display:flex; align-items:center; gap:10px; padding:7px 4px; border-radius:8px; font-size:13px; opacity:.5; }
|
||||
.akteItem.have { opacity:1; }
|
||||
.akteItem .aId { color:#fff; font-size:11px; font-weight:700; border-radius:6px; padding:2px 7px; min-width:30px; text-align:center; flex:none; }
|
||||
.akteItem .aNm { flex:1; }
|
||||
.akteItem .aNm i { color:var(--muted); font-style:italic; }
|
||||
.akteItem .aChk { color:var(--ok); font-weight:700; }
|
||||
/* Gate: Artefakt-Anforderung (harte Kopplung) */
|
||||
.gateReq { border-radius:8px; padding:9px 12px; margin:8px 0 12px; font-size:14px; font-weight:600; line-height:1.4; }
|
||||
.gateReq.ok { background:#f1faf4; color:#177a44; border:1px solid #cde6d6; }
|
||||
.gateReq.bad { background:#fdf3f3; color:#b5202a; border:1px solid #f0c9c9; }
|
||||
.choice[disabled] { opacity:.45; cursor:default; }
|
||||
.navTop { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; }
|
||||
.navTop b { font-size:13px; text-transform:uppercase; letter-spacing:.6px; color:var(--muted); }
|
||||
.navTop button { border:none; background:none; font-size:20px; line-height:1; color:var(--muted); cursor:pointer; padding:4px 8px; }
|
||||
|
|
@ -244,6 +269,7 @@
|
|||
<span class="tag">v0.6</span>
|
||||
<div id="cardBadge" class="cardBadge"></div>
|
||||
<div class="spacer"></div>
|
||||
<button class="ghost" id="akteBtn" title="Service-Akte (gesammelte Artefakte)">📁 Akte</button>
|
||||
<button class="ghost" id="stationsBtn" title="Stationsübersicht">☰ Stationen</button>
|
||||
<button class="ghost" id="resetBtn" title="Neue Action Card / Durchlauf zurücksetzen">Neu starten</button>
|
||||
</header>
|
||||
|
|
@ -252,6 +278,7 @@
|
|||
<div class="navBackdrop" id="navBackdrop"></div>
|
||||
<div class="layout">
|
||||
<aside id="stationList"></aside>
|
||||
<div id="akteList"></div>
|
||||
<main><div class="card" id="panel"></div></main>
|
||||
</div>
|
||||
|
||||
|
|
@ -881,6 +908,54 @@ const STATIONEN = [
|
|||
]}
|
||||
];
|
||||
|
||||
/* ====================== SERVICE-AKTE (Artefakte A1-A15, App-gefuehrt) ======================
|
||||
Die Akte ist rein digital: erzeugte Artefakte werden per Choice bestimmt und
|
||||
gesammelt; Gates sind hart gekoppelt (oeffnen nur mit den geforderten Artefakten). */
|
||||
const ARTEFAKTE = {
|
||||
A1:{name:"Projektauftrag", phase:"design"},
|
||||
A2:{name:"Service-Definition", phase:"design", live:true},
|
||||
A3:{name:"Service Design Document", phase:"design"},
|
||||
A4:{name:"Implementation Blueprint", phase:"design"},
|
||||
A5:{name:"Gate-/SOR-Vorlage", phase:"transition"},
|
||||
A6:{name:"Betriebsdokumentation", phase:"transition"},
|
||||
A7:{name:"Test-Report", phase:"transition"},
|
||||
A8:{name:"Aktivierter Service", phase:"transition"},
|
||||
A9:{name:"Service-Qualitätsbericht", phase:"operation"},
|
||||
A10:{name:"Incident Record", phase:"support"},
|
||||
A11:{name:"Problem Record", phase:"support", live:true},
|
||||
A12:{name:"Workaround", phase:"support"},
|
||||
A13:{name:"Wissensdatenbank-Eintrag", phase:"support", live:true},
|
||||
A14:{name:"Service-Review-Bericht", phase:"review"},
|
||||
A15:{name:"DPM-Rücklauf", phase:"review"}
|
||||
};
|
||||
// Welche Station erzeugt welches A-Artefakt (Choice-Schritt -> Aufnahme in die Akte).
|
||||
const STATION_ARTEFAKT = {
|
||||
ds_01:"A2", ds_02:"A3", ds_03:"A4",
|
||||
tr_06:"A6", tr_07:"A7", tr_08:"A5",
|
||||
op_06:"A9",
|
||||
sp_02:"A13", sp_07:"A10", sp_09:"A11", sp_11:"A12",
|
||||
rv_02:"A14", rv_05:"A15"
|
||||
};
|
||||
// Gate erzeugt Artefakt (beim Vorwaerts-Durchschreiten).
|
||||
const GATE_PRODUCES = { tr_12:"A8" };
|
||||
// Geforderte Artefakte je Gate (HARTE Kopplung).
|
||||
const GATE_REQ = { tr_01:["A2","A3","A4"], tr_09:["A6","A7"], tr_12:["A6","A7","A2"] };
|
||||
|
||||
function addArtefakt(a){ if(a){ S.akte = S.akte || {}; S.akte[a] = true; } }
|
||||
// Beim Start nach Einstiegspunkt vorbefuellen: alles, was VOR der Einstiegs-Station
|
||||
// (bzw. vor durchschrittenen Gates) entsteht, "liegt schon vor".
|
||||
function seedAkte(entryIdx){
|
||||
S.akte = { A1:true };
|
||||
for(const sid in STATION_ARTEFAKT){
|
||||
const j = STATIONEN.findIndex(s=>s.id===sid);
|
||||
if(j>=0 && j < entryIdx) addArtefakt(STATION_ARTEFAKT[sid]);
|
||||
}
|
||||
for(const gid in GATE_PRODUCES){
|
||||
const j = STATIONEN.findIndex(s=>s.id===gid);
|
||||
if(j>=0 && j < entryIdx) addArtefakt(GATE_PRODUCES[gid]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ====================== STATE ====================== */
|
||||
const LS_KEY = "slc-companion-proto";
|
||||
function defaultState(){
|
||||
|
|
@ -888,8 +963,8 @@ function defaultState(){
|
|||
classifyDone:false, classifyWrong:null,
|
||||
entryDone:false, entryWrong:null,
|
||||
index:0, stage:"discuss", quizIndex:0,
|
||||
actStep:0, actReveal:false, actDone:false,
|
||||
picks:{}, done:{},
|
||||
actStep:0, actReveal:false, actDone:false, arteWrong:null,
|
||||
picks:{}, done:{}, akte:{},
|
||||
loopback:null, revisit:{}, endReason:null, endGate:null };
|
||||
}
|
||||
let S = load();
|
||||
|
|
@ -939,10 +1014,35 @@ function renderList(){
|
|||
$("#progressBar").style.width = pct+"%";
|
||||
}
|
||||
|
||||
/* ====================== RENDER: SERVICE-AKTE (Overlay) ====================== */
|
||||
function renderAkte(){
|
||||
const order = ["design","transition","operation","support","review"];
|
||||
const ids = Object.keys(ARTEFAKTE);
|
||||
const have = S.akte || {};
|
||||
const n = ids.filter(a=>have[a]).length;
|
||||
let html = `<div class="navTop"><b>📁 Service-Akte</b><button id="akteClose" title="Schließen">✕</button></div>`;
|
||||
html += `<div class="akteCount">${n}/15 Artefakten gesammelt</div>`;
|
||||
for(const ph of order){
|
||||
const group = ids.filter(a => ARTEFAKTE[a].phase === ph);
|
||||
if(!group.length) continue;
|
||||
html += `<h3>${PHASEN[ph].label}</h3>`;
|
||||
group.forEach(a=>{
|
||||
const ok = !!have[a];
|
||||
html += `<div class="akteItem ${ok?'have':''}">
|
||||
<span class="aId" style="background:${PHASEN[ph].color}">${a}</span>
|
||||
<span class="aNm">${ARTEFAKTE[a].name}${ARTEFAKTE[a].live?' · <i>lebend</i>':''}</span>
|
||||
<span class="aChk">${ok?'✓':'○'}</span>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
$("#akteList").innerHTML = html;
|
||||
const c = $("#akteClose"); if(c) c.onclick = ()=> document.body.classList.remove("akteOpen");
|
||||
}
|
||||
|
||||
/* ====================== VIEW DISPATCH ====================== */
|
||||
function draw(){
|
||||
document.body.classList.toggle("runMode", S.view==="run");
|
||||
if(S.view!=="run") document.body.classList.remove("navOpen");
|
||||
if(S.view!=="run"){ document.body.classList.remove("navOpen"); document.body.classList.remove("akteOpen"); }
|
||||
renderCardBadge();
|
||||
if(S.view==="deck") return renderDeck();
|
||||
if(S.view==="classify") return renderClassify();
|
||||
|
|
@ -1084,7 +1184,7 @@ function renderEntry(){
|
|||
<button class="primary" id="startRun">Los geht's →</button>
|
||||
</div>`;
|
||||
$("#backClassify").onclick=()=>{ S.view="classify"; save(); draw(); };
|
||||
$("#startRun").onclick=()=>{ enterStation(recIndex); S.view="run"; save(); draw(); };
|
||||
$("#startRun").onclick=()=>{ seedAkte(recIndex); enterStation(recIndex); S.view="run"; save(); draw(); };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1104,12 +1204,13 @@ function enterStation(idx){
|
|||
S.index = idx;
|
||||
S.stage = STATIONEN[idx].typ==="gate" ? "gate" : "act";
|
||||
S.gatePick = null; S.quizIndex = 0;
|
||||
S.actStep = 0; S.actReveal = false; S.actDone = false;
|
||||
S.actStep = 0; S.actReveal = false; S.actDone = false; S.arteWrong = null;
|
||||
}
|
||||
function gateGoto(st, i){
|
||||
S.done[st.id] = true;
|
||||
const t = (GATE_FLOW[st.id] || [])[i] || "next";
|
||||
if(t==="end"){ S.view="end"; S.endReason="rejected"; S.endGate=st.id; save(); draw(); return; }
|
||||
if(t==="next" && GATE_PRODUCES[st.id]) addArtefakt(GATE_PRODUCES[st.id]); // z. B. Gate 3 Go-Live -> A8
|
||||
if(t==="next"){ enterStation(S.index+1); }
|
||||
else {
|
||||
const j = STATIONEN.findIndex(s=>s.id===t);
|
||||
|
|
@ -1148,6 +1249,7 @@ function raciLegendHtml(){
|
|||
|
||||
function renderRun(){
|
||||
renderList();
|
||||
renderAkte();
|
||||
const st = cur();
|
||||
const ph = PHASEN[st.phase];
|
||||
const chip = st.typ==="gate"
|
||||
|
|
@ -1188,11 +1290,21 @@ function activitySteps(st){
|
|||
frage:`Klärt die <b>RACI</b>: Wer ist R, A, C, I? Sortiert die Figuren ins <b>Aktiv-Feld (R·A·C·I)</b>.`,
|
||||
legend: raciLegendHtml(),
|
||||
auf:`<h4 class="aufH">RACI</h4>${raciTable(st)}` },
|
||||
{ label:"Artefakt",
|
||||
{ label:"Artefakt", artefakt:true,
|
||||
frage:`Welche <b>Artefaktkarte</b> entsteht hier und gehört in die <b>Service-Akte</b>?`,
|
||||
auf:`<h4 class="aufH">Artefakt</h4><p style="margin:0"><b>${st.artefakt}</b></p>` }
|
||||
];
|
||||
}
|
||||
// Antwort-Optionen fuer die Artefakt-Choice: richtiges A + 3 Distraktoren
|
||||
// (bevorzugt aus derselben Phase), deterministisch nach A-Nummer sortiert.
|
||||
function arteOptions(correct){
|
||||
const ids = Object.keys(ARTEFAKTE);
|
||||
const ph = ARTEFAKTE[correct].phase;
|
||||
const same = ids.filter(a => a!==correct && ARTEFAKTE[a].phase===ph);
|
||||
const other = ids.filter(a => a!==correct && ARTEFAKTE[a].phase!==ph);
|
||||
const opts = [correct].concat(same.concat(other).slice(0,3));
|
||||
return opts.sort((a,b)=> a.localeCompare(b, "en", {numeric:true}));
|
||||
}
|
||||
function renderActivity(st){
|
||||
const phaseColor = PHASEN[st.phase].color;
|
||||
const next = STATIONEN[S.index+1];
|
||||
|
|
@ -1224,6 +1336,8 @@ function renderActivity(st){
|
|||
const i = Math.min(S.actStep||0, steps.length-1);
|
||||
const step = steps[i];
|
||||
const isLast = i === steps.length-1;
|
||||
const arteId = STATION_ARTEFAKT[st.id]; // A-Nummer, falls diese Station eine erzeugt
|
||||
const isArteChoice = step.artefakt && arteId; // Artefakt-Schritt mit echter Choice
|
||||
|
||||
let html = `<div class="tourProg">Schritt ${i+1}/${steps.length} · ${step.label}</div>
|
||||
<div class="frageBox" style="border-left-color:${phaseColor}">
|
||||
|
|
@ -1231,14 +1345,27 @@ function renderActivity(st){
|
|||
${step.frage}
|
||||
</div>`;
|
||||
if(step.legend) html += step.legend;
|
||||
if(S.actReveal) html += `<div class="aufBox">${step.auf}</div>`;
|
||||
|
||||
if(isArteChoice && !S.actReveal){
|
||||
// Auswahl: welches Artefakt entsteht? (richtiges A + Distraktoren)
|
||||
const opts = arteOptions(arteId).map(a =>
|
||||
`<button class="choice arteChoice ${S.arteWrong===a?'bad':''}" data-a="${a}">${a} — ${ARTEFAKTE[a].name}</button>`).join("");
|
||||
html += `<div class="choiceGrid">${opts}</div>`;
|
||||
if(S.arteWrong) html += `<div class="hint bad">Nicht ganz — überlegt nochmal, welches Ergebnis diese Station liefert.</div>`;
|
||||
} else if(S.actReveal){
|
||||
if(isArteChoice)
|
||||
html += `<div class="aufBox"><h4 class="aufH">Artefakt</h4><p style="margin:0">✓ <b>${arteId} — ${ARTEFAKTE[arteId].name}</b> in die Service-Akte gelegt.</p></div>`;
|
||||
else
|
||||
html += `<div class="aufBox">${step.auf}</div>`;
|
||||
}
|
||||
|
||||
let actions = `<div class="actions">`;
|
||||
if(S.actReveal || i>0) actions += `<button class="ghost" id="actBack">← zurück</button>`;
|
||||
actions += `<div class="spacer"></div>`;
|
||||
if(!S.actReveal) actions += `<button class="primary" id="actReveal">Auflösen →</button>`;
|
||||
else if(!isLast) actions += `<button class="primary" id="actNext">Weiter →</button>`;
|
||||
else actions += `<button class="primary" id="actToDone">Weiter →</button>`;
|
||||
if(isArteChoice && !S.actReveal){ /* Antwort per Klick auf eine Option — kein Auflösen-Button */ }
|
||||
else if(!S.actReveal) actions += `<button class="primary" id="actReveal">Auflösen →</button>`;
|
||||
else if(!isLast) actions += `<button class="primary" id="actNext">Weiter →</button>`;
|
||||
else actions += `<button class="primary" id="actToDone">Weiter →</button>`;
|
||||
actions += `</div>`;
|
||||
return html + actions;
|
||||
}
|
||||
|
|
@ -1247,13 +1374,21 @@ function renderActivity(st){
|
|||
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("");
|
||||
// Harte Artefakt-Kopplung: Gate "oeffnet" nur mit den geforderten Artefakten in der Akte.
|
||||
const req = GATE_REQ[st.id] || [];
|
||||
const missing = req.filter(a => !(S.akte && S.akte[a]));
|
||||
const blocked = missing.length > 0;
|
||||
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("");
|
||||
`<button class="choice" data-i="${i}" ${blocked?"disabled":""}><b>${n}</b><br><span style="color:var(--muted);font-weight:400">${d}</span></button>`).join("");
|
||||
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 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>` : ``;
|
||||
return `
|
||||
${revisitNote}
|
||||
<p class="lead"><b>Entscheidet:</b> ${roleLabel(keeper)}</p>
|
||||
${reqLine}
|
||||
<div class="choiceGrid">${opts}</div>
|
||||
<details class="det">
|
||||
<summary>Worum geht's & Prüf-Kriterien</summary>
|
||||
|
|
@ -1306,6 +1441,13 @@ function wire(st){
|
|||
if(b("actReveal")) b("actReveal").onclick = ()=>{ S.actReveal=true; save(); draw(); };
|
||||
if(b("actNext")) b("actNext").onclick = ()=>{ S.actStep=(S.actStep||0)+1; S.actReveal=false; save(); draw(); };
|
||||
if(b("actToDone")) b("actToDone").onclick = ()=>{ S.actDone=true; save(); draw(); };
|
||||
// Artefakt-Choice
|
||||
$("#panel").querySelectorAll(".arteChoice[data-a]").forEach(el=>{
|
||||
el.onclick = ()=>{ const a = el.dataset.a;
|
||||
if(a === STATION_ARTEFAKT[st.id]){ S.arteWrong=null; addArtefakt(a); S.actReveal=true; }
|
||||
else { S.arteWrong = a; }
|
||||
save(); draw(); };
|
||||
});
|
||||
if(b("actBack")) b("actBack").onclick = ()=>{
|
||||
if(S.actDone){ S.actDone=false; }
|
||||
else if(S.actReveal){ S.actReveal=false; }
|
||||
|
|
@ -1349,8 +1491,9 @@ function renderEnd(){
|
|||
/* ====================== INIT ====================== */
|
||||
(function init(){
|
||||
$("#resetBtn").onclick = ()=>{ if(confirm("Neue Action Card ziehen und Durchlauf zurücksetzen?")){ S=defaultState(); save(); draw(); } };
|
||||
$("#stationsBtn").onclick = ()=> document.body.classList.toggle("navOpen");
|
||||
$("#navBackdrop").onclick = ()=> document.body.classList.remove("navOpen");
|
||||
$("#stationsBtn").onclick = ()=>{ document.body.classList.remove("akteOpen"); document.body.classList.toggle("navOpen"); };
|
||||
$("#akteBtn").onclick = ()=>{ document.body.classList.remove("navOpen"); document.body.classList.toggle("akteOpen"); };
|
||||
$("#navBackdrop").onclick = ()=>{ document.body.classList.remove("navOpen"); document.body.classList.remove("akteOpen"); };
|
||||
draw();
|
||||
})();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* Service Worker — SLC-Workshop Companion (App-Shell, offline-first) */
|
||||
const CACHE = "slc-companion-v8";
|
||||
const CACHE = "slc-companion-v9";
|
||||
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 = [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue