This commit is contained in:
breitenbach76 2026-06-07 14:44:36 +02:00
parent 213243f308
commit 7d474a054e
4 changed files with 320 additions and 14 deletions

View file

@ -0,0 +1,56 @@
// Artefakt-Token (Service-Akte) gedruckte "Karte" statt Pappkarte
// SLC-Workshop Tabletop · Einheiten: mm
// Kleine Tile mit gravierter A-Nummer; wird in einen Slot des Artefakt-Trays
// (artefakt-tray.scad) gelegt. Eingefaerbt in der PHASENFARBE der erzeugenden
// Phase (Design/Transition/Operation/Support/Review) -> Farbe via Filament.
//
// "Lebende" Artefakte (A2 Service-Definition, A11 Problem Record,
// A13 Wissensdatenbank) wachsen sichtbar: Basis-Token + je Aktualisierung
// eine duenne Status-Platte oben drauf (Entwurf -> Final -> Aktualisiert).
//
// part = "token" -> ein Artefakt-Token (Nummer via tok_label)
// "plate" -> eine Status-Platte zum Aufstapeln (lebende Artefakte)
part = "token"; // "token" | "plate"
/* [Token] */
tok_w = 30; // Breite
tok_d = 20; // Tiefe
tok_h = 4; // Hoehe (Basis-Token)
tok_r = 2.5; // Eckenradius
tok_label = "A2"; // gravierte A-Nummer
lab_size = 11; // Schriftgroesse A-Nummer
lab_depth = 0.8; // Gravurtiefe
/* [Status-Platte] (lebende Artefakte) */
plate_h = 2.5; // Hoehe je Aktualisierungs-Platte
$fn = 48;
module rrect(l, w, h, r) {
linear_extrude(h) offset(r) offset(-r) square([l, w], center = true);
}
module token(label) {
difference() {
rrect(tok_w, tok_d, tok_h, tok_r);
translate([0, 0, tok_h - lab_depth])
linear_extrude(lab_depth + 0.1)
text(label, size = lab_size, halign = "center", valign = "center");
}
}
// duenne Platte zum Aufstapeln; kleine Mittenrille als Status-Markierung
module status_plate() {
difference() {
rrect(tok_w, tok_d, plate_h, tok_r);
translate([0, 0, plate_h - 0.5])
linear_extrude(0.6)
square([tok_w - 10, 1.2], center = true);
}
}
if (part == "plate") status_plate();
else token(tok_label);
echo(part = part, tok = [tok_w, tok_d, tok_h], plate_h = plate_h);

View file

@ -0,0 +1,107 @@
// Artefakt-Tray (Service-Akte) 3D-Aufnahme fuer die 15 Artefakt-Token
// SLC-Workshop Tabletop · Einheiten: mm
// Flaches Tableau mit 15 beschrifteten Steck-Slots (A1-A15), in 5 PHASEN-Reihen
// gruppiert (Design -> Transition -> Operation -> Support -> Review). Liegt neben
// der aktuellen Station und wandert mit. Die Token (artefakt-token.scad) tragen
// die Phasenfarbe; der Tray selbst ist EINFARBIG (Beschriftung via Gravur).
//
// Mechanik:
// - Artefakt erzeugt -> Token in seinen Slot legen.
// - "Lebende" Artefakte (A2/A11/A13, hier mit eingraviertem Zusatz-Rahmen
// markiert): Status-Platten oben aufstapeln -> Stapel waechst sichtbar.
// - Gate-Kopplung bleibt REGEL: ein Gate "oeffnet" nur, wenn die geforderten
// Token im Tray liegen (keine Mechanik am Gate-Puck).
/* [Platte] */
plate_thick = 6; // Dicke
margin = 8; // Aussenrand
corner_r = 5;
/* [Slots] (Token Ø 30x20 wird REINGELEGT) */
slot_w = 31; // Slot-Innenbreite (Token 30 + Spiel)
slot_d = 21; // Slot-Innentiefe (Token 20 + Spiel)
pocket_dep = 3.0; // Vertiefung (Token 4 hoch -> steht ~1 mm vor = greifbar)
pocket_r = 2.5;
gap_x = 7; // Abstand zwischen Slots in einer Reihe
row_gap = 8; // Abstand zwischen den Phasen-Reihen
header_h = 9; // Hoehe der Phasen-Kopfzeile ueber den Slots
finger_r = 6; // Greifkerbe an der Slot-Unterkante
/* [Gravur] */
num_size = 7; // A-Nummer im Slot-Boden
num_depth = 0.8;
hdr_size = 6; // Phasen-Name
hdr_depth = 0.8;
lf_inset = 3; // "lebend"-Rahmen: Abstand zum Slot-Rand
lf_w = 1.2; // Strichstaerke
lf_depth = 0.6;
$fn = 48;
// [Phasen-Reihe, [[A-Nummer, lebend?], ...]] Reihenfolge = Lebenszyklus
ROWS = [
["DESIGN", [["A1", false], ["A2", true], ["A3", false], ["A4", false]]],
["TRANSITION", [["A5", false], ["A6", false], ["A7", false], ["A8", false]]],
["OPERATION", [["A9", false]]],
["SUPPORT", [["A10", false], ["A11", true], ["A12", false], ["A13", true]]],
["REVIEW", [["A14", false], ["A15", false]]]
];
cols_max = 4;
n_rows = len(ROWS);
// abgeleitete Maße
content_w = cols_max * slot_w + (cols_max - 1) * gap_x; // 145
plate_w = content_w + 2 * margin; // 161
block_h = header_h + slot_d; // 30
content_h = n_rows * block_h + (n_rows - 1) * row_gap; // 182
plate_h = content_h + 2 * margin; // 198
// Platz von Ecke (0,0) aus; y oben = plate_h
function blockTopY(r) = plate_h - margin - r * (block_h + row_gap);
function slotCenterY(r) = blockTopY(r) - header_h - slot_d/2;
function slotCenterX(c) = margin + slot_w/2 + c * (slot_w + gap_x);
module rrect(l, w, h, r) {
linear_extrude(h) offset(r) offset(-r) square([l, w], center = true);
}
module pocket(cx, cy, label, live) {
// Vertiefung
translate([cx, cy, plate_thick - pocket_dep])
rrect(slot_w, slot_d, pocket_dep + 0.1, pocket_r);
// Greifkerbe an der Unterkante
translate([cx, cy - slot_d/2, plate_thick - pocket_dep])
cylinder(r = finger_r, h = pocket_dep + 0.1);
// A-Nummer im Boden (sichtbar bei leerem Slot)
translate([cx, cy, plate_thick - pocket_dep - num_depth])
linear_extrude(num_depth + 0.1)
text(label, size = num_size, halign = "center", valign = "center");
// "lebendes" Artefakt: zusaetzlicher Rahmen als Hinweis (stapeln/aktualisieren)
if (live)
translate([cx, cy, plate_thick - pocket_dep - lf_depth])
linear_extrude(lf_depth + 0.1)
difference() {
square([slot_w - 2*lf_inset, slot_d - 2*lf_inset], center = true);
square([slot_w - 2*lf_inset - 2*lf_w, slot_d - 2*lf_inset - 2*lf_w], center = true);
}
}
module tray() {
difference() {
translate([plate_w/2, plate_h/2, 0]) rrect(plate_w, plate_h, plate_thick, corner_r);
for (r = [0 : n_rows - 1]) {
hdr = ROWS[r][0];
slots = ROWS[r][1];
// Phasen-Kopfzeile (links ueber der Reihe)
translate([margin + 1, blockTopY(r) - header_h/2, plate_thick - hdr_depth])
linear_extrude(hdr_depth + 0.1)
text(hdr, size = hdr_size, halign = "left", valign = "center");
for (c = [0 : len(slots) - 1])
pocket(slotCenterX(c), slotCenterY(r), slots[c][0], slots[c][1]);
}
}
}
tray();
echo(plate_w = plate_w, plate_h = plate_h, plate_thick = plate_thick, slots = 15);

View file

@ -68,6 +68,31 @@
body.navOpen aside { transform: translateX(0); } 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; } .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.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 { 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 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; } .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> <span class="tag">v0.6</span>
<div id="cardBadge" class="cardBadge"></div> <div id="cardBadge" class="cardBadge"></div>
<div class="spacer"></div> <div class="spacer"></div>
<button class="ghost" id="akteBtn" title="Service-Akte (gesammelte Artefakte)">📁&nbsp;Akte</button>
<button class="ghost" id="stationsBtn" title="Stationsübersicht">&nbsp;Stationen</button> <button class="ghost" id="stationsBtn" title="Stationsübersicht">&nbsp;Stationen</button>
<button class="ghost" id="resetBtn" title="Neue Action Card / Durchlauf zurücksetzen">Neu starten</button> <button class="ghost" id="resetBtn" title="Neue Action Card / Durchlauf zurücksetzen">Neu starten</button>
</header> </header>
@ -252,6 +278,7 @@
<div class="navBackdrop" id="navBackdrop"></div> <div class="navBackdrop" id="navBackdrop"></div>
<div class="layout"> <div class="layout">
<aside id="stationList"></aside> <aside id="stationList"></aside>
<div id="akteList"></div>
<main><div class="card" id="panel"></div></main> <main><div class="card" id="panel"></div></main>
</div> </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 ====================== */ /* ====================== STATE ====================== */
const LS_KEY = "slc-companion-proto"; const LS_KEY = "slc-companion-proto";
function defaultState(){ function defaultState(){
@ -888,8 +963,8 @@ function defaultState(){
classifyDone:false, classifyWrong:null, classifyDone:false, classifyWrong:null,
entryDone:false, entryWrong:null, entryDone:false, entryWrong:null,
index:0, stage:"discuss", quizIndex:0, index:0, stage:"discuss", quizIndex:0,
actStep:0, actReveal:false, actDone:false, actStep:0, actReveal:false, actDone:false, arteWrong:null,
picks:{}, done:{}, picks:{}, done:{}, akte:{},
loopback:null, revisit:{}, endReason:null, endGate:null }; loopback:null, revisit:{}, endReason:null, endGate:null };
} }
let S = load(); let S = load();
@ -939,10 +1014,35 @@ function renderList(){
$("#progressBar").style.width = pct+"%"; $("#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 ====================== */ /* ====================== VIEW DISPATCH ====================== */
function draw(){ function draw(){
document.body.classList.toggle("runMode", S.view==="run"); 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(); renderCardBadge();
if(S.view==="deck") return renderDeck(); if(S.view==="deck") return renderDeck();
if(S.view==="classify") return renderClassify(); if(S.view==="classify") return renderClassify();
@ -1084,7 +1184,7 @@ function renderEntry(){
<button class="primary" id="startRun">Los geht's →</button> <button class="primary" id="startRun">Los geht's →</button>
</div>`; </div>`;
$("#backClassify").onclick=()=>{ S.view="classify"; save(); draw(); }; $("#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.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.actStep = 0; S.actReveal = false; S.actDone = false; S.actStep = 0; S.actReveal = false; S.actDone = false; S.arteWrong = null;
} }
function gateGoto(st, i){ function gateGoto(st, i){
S.done[st.id] = true; S.done[st.id] = true;
const t = (GATE_FLOW[st.id] || [])[i] || "next"; 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==="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); } if(t==="next"){ enterStation(S.index+1); }
else { else {
const j = STATIONEN.findIndex(s=>s.id===t); const j = STATIONEN.findIndex(s=>s.id===t);
@ -1148,6 +1249,7 @@ function raciLegendHtml(){
function renderRun(){ function renderRun(){
renderList(); renderList();
renderAkte();
const st = cur(); const st = cur();
const ph = PHASEN[st.phase]; const ph = PHASEN[st.phase];
const chip = st.typ==="gate" 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>.`, 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(), legend: raciLegendHtml(),
auf:`<h4 class="aufH">RACI</h4>${raciTable(st)}` }, 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>?`, 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>` } 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){ function renderActivity(st){
const phaseColor = PHASEN[st.phase].color; const phaseColor = PHASEN[st.phase].color;
const next = STATIONEN[S.index+1]; const next = STATIONEN[S.index+1];
@ -1224,6 +1336,8 @@ function renderActivity(st){
const i = Math.min(S.actStep||0, steps.length-1); const i = Math.min(S.actStep||0, steps.length-1);
const step = steps[i]; const step = steps[i];
const isLast = i === steps.length-1; 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> let html = `<div class="tourProg">Schritt ${i+1}/${steps.length} · ${step.label}</div>
<div class="frageBox" style="border-left-color:${phaseColor}"> <div class="frageBox" style="border-left-color:${phaseColor}">
@ -1231,14 +1345,27 @@ function renderActivity(st){
${step.frage} ${step.frage}
</div>`; </div>`;
if(step.legend) html += step.legend; 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">`; let actions = `<div class="actions">`;
if(S.actReveal || i>0) actions += `<button class="ghost" id="actBack">← zurück</button>`; if(S.actReveal || i>0) actions += `<button class="ghost" id="actBack">← zurück</button>`;
actions += `<div class="spacer"></div>`; actions += `<div class="spacer"></div>`;
if(!S.actReveal) actions += `<button class="primary" id="actReveal">Auflösen →</button>`; if(isArteChoice && !S.actReveal){ /* Antwort per Klick auf eine Option — kein Auflösen-Button */ }
else if(!isLast) actions += `<button class="primary" id="actNext">Weiter →</button>`; else if(!S.actReveal) actions += `<button class="primary" id="actReveal">Auflösen →</button>`;
else actions += `<button class="primary" id="actToDone">Weiter →</button>`; else if(!isLast) actions += `<button class="primary" id="actNext">Weiter →</button>`;
else actions += `<button class="primary" id="actToDone">Weiter →</button>`;
actions += `</div>`; actions += `</div>`;
return html + actions; return html + actions;
} }
@ -1247,13 +1374,21 @@ function renderActivity(st){
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 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)=> 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]) const revisitNote = (S.revisit && S.revisit[st.id])
? `<div class="hint ok">↩ Erneute Vorlage nach Nacharbeit — prüft erneut und entscheidet neu.</div>` : ``; ? `<div class="hint ok">↩ Erneute Vorlage nach Nacharbeit — prüft erneut und entscheidet neu.</div>` : ``;
return ` return `
${revisitNote} ${revisitNote}
<p class="lead"><b>Entscheidet:</b> ${roleLabel(keeper)}</p> <p class="lead"><b>Entscheidet:</b> ${roleLabel(keeper)}</p>
${reqLine}
<div class="choiceGrid">${opts}</div> <div class="choiceGrid">${opts}</div>
<details class="det"> <details class="det">
<summary>Worum geht's & Prüf-Kriterien</summary> <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("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("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(); }; 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(b("actBack")) b("actBack").onclick = ()=>{
if(S.actDone){ S.actDone=false; } if(S.actDone){ S.actDone=false; }
else if(S.actReveal){ S.actReveal=false; } else if(S.actReveal){ S.actReveal=false; }
@ -1349,8 +1491,9 @@ function renderEnd(){
/* ====================== INIT ====================== */ /* ====================== INIT ====================== */
(function init(){ (function init(){
$("#resetBtn").onclick = ()=>{ if(confirm("Neue Action Card ziehen und Durchlauf zurücksetzen?")){ S=defaultState(); save(); draw(); } }; $("#resetBtn").onclick = ()=>{ if(confirm("Neue Action Card ziehen und Durchlauf zurücksetzen?")){ S=defaultState(); save(); draw(); } };
$("#stationsBtn").onclick = ()=> document.body.classList.toggle("navOpen"); $("#stationsBtn").onclick = ()=>{ document.body.classList.remove("akteOpen"); document.body.classList.toggle("navOpen"); };
$("#navBackdrop").onclick = ()=> document.body.classList.remove("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(); draw();
})(); })();

View file

@ -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-v8"; const CACHE = "slc-companion-v9";
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 30). // Action-Card-Grafiken (cards/s<service>-c<change>.png) fuer Offline vorab cachen (alle 30).
const CARDS = []; const CARDS = [];