v10
This commit is contained in:
parent
213243f308
commit
7d474a054e
4 changed files with 320 additions and 14 deletions
56
01_3D-Druck/openscad/artefakt-token.scad
Normal file
56
01_3D-Druck/openscad/artefakt-token.scad
Normal 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);
|
||||
107
01_3D-Druck/openscad/artefakt-tray.scad
Normal file
107
01_3D-Druck/openscad/artefakt-tray.scad
Normal 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);
|
||||
|
|
@ -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,12 +1345,25 @@ 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>`;
|
||||
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>`;
|
||||
|
|
@ -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