842 lines
55 KiB
HTML
842 lines
55 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Project SDE | Field Problem Workshop v5</title>
|
||
<style>
|
||
:root{
|
||
--bg:#f4f4f4; --layer:#ffffff; --layer-hover:#e8e8e8;
|
||
--border:#e0e0e0; --border-strong:#8d8d8d;
|
||
--text:#161616; --text-secondary:#525252; --text-helper:#6f6f6f;
|
||
--interactive:#0f62fe; --interactive-hover:#0353e9;
|
||
--danger:#da1e28; --success:#24a148; --warning-bg:#fdf6dd; --warning:#8e6a00;
|
||
--purple:#8a3ffc; --teal:#007d79; --focus:#0f62fe;
|
||
}
|
||
*{box-sizing:border-box; margin:0; padding:0;}
|
||
body{font-family:'IBM Plex Sans','Segoe UI',system-ui,-apple-system,sans-serif; background:var(--bg); color:var(--text); font-size:16px; line-height:1.4;}
|
||
button{font-family:inherit; cursor:pointer;}
|
||
button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible{outline:2px solid var(--focus); outline-offset:1px;}
|
||
|
||
header{background:var(--text); color:#fff; padding:0 24px; display:flex; align-items:center; gap:12px; height:56px;}
|
||
header .brand{font-size:15px;}
|
||
header .brand strong{font-weight:600;}
|
||
header .brand span{color:#c6c6c6; font-weight:400;}
|
||
header .spacer{flex:1;}
|
||
.persist-note{font-size:12px; color:#c6c6c6;}
|
||
.hdr-btn{background:transparent; color:#fff; border:1px solid #6f6f6f; padding:7px 13px; font-size:13.5px;}
|
||
.hdr-btn:hover{background:#353535;}
|
||
.hdr-btn.primary{background:var(--interactive); border-color:var(--interactive);}
|
||
.hdr-btn.primary:hover{background:var(--interactive-hover);}
|
||
|
||
nav{background:var(--layer); border-bottom:1px solid var(--border); padding:0 24px; display:flex;}
|
||
nav button{background:none; border:none; border-bottom:3px solid transparent; padding:14px 18px; font-size:15px; color:var(--text-secondary);}
|
||
nav button:hover{color:var(--text); background:var(--layer-hover);}
|
||
nav button.active{border-bottom-color:var(--interactive); color:var(--text); font-weight:600;}
|
||
nav button .count{display:inline-block; margin-left:7px; background:var(--border); border-radius:12px; padding:1px 8px; font-size:12px; color:var(--text);}
|
||
nav .flow{align-self:center; color:var(--border-strong); font-size:13px; padding:0 2px;}
|
||
|
||
main{padding:24px; max-width:1500px; margin:0 auto;}
|
||
.panel{display:none;}
|
||
.panel.active{display:block;}
|
||
.panel-head{margin-bottom:18px;}
|
||
.panel-head h1{font-size:25px; font-weight:400;}
|
||
.panel-head p{color:var(--text-secondary); font-size:14.5px; margin-top:4px; max-width:980px;}
|
||
|
||
.btn{background:var(--interactive); color:#fff; border:none; padding:10px 18px; font-size:14px;}
|
||
.btn:hover{background:var(--interactive-hover);}
|
||
.btn:disabled{background:var(--border); color:var(--text-helper); cursor:not-allowed;}
|
||
.btn.ghost{background:transparent; color:var(--interactive); border:1px solid var(--interactive);}
|
||
.btn.ghost:hover{background:#edf5ff;}
|
||
.btn.ghost:disabled{background:transparent; color:var(--text-helper); border-color:var(--border-strong); cursor:not-allowed;}
|
||
.btn.small{padding:6px 12px; font-size:13px;}
|
||
.btn.ai{background:transparent; color:var(--purple); border:1px solid var(--purple);}
|
||
.btn.ai:hover{background:#f6f2ff;}
|
||
.btn.ai:disabled{color:var(--text-helper); border-color:var(--border-strong); background:transparent;}
|
||
.add-row{display:flex; gap:8px; flex-wrap:wrap;}
|
||
.add-row input, .add-row select{border:none; border-bottom:1px solid var(--border-strong); background:var(--layer); padding:10px 12px; font-size:15px; font-family:inherit;}
|
||
.add-row input{flex:1; min-width:280px; max-width:640px;}
|
||
|
||
.type-chip{font-size:11px; font-weight:600; padding:2px 8px; border-radius:12px;}
|
||
.ty-Problem{background:var(--border); color:var(--text);}
|
||
.ty-Question{background:#edf5ff; color:var(--interactive);}
|
||
.ty-Idea{background:#f6f2ff; color:var(--purple);}
|
||
.sug-chip{font-size:11px; font-weight:600; padding:2px 8px; border-radius:12px; background:#f6f2ff; color:var(--purple);}
|
||
.src-tag{font-size:11.5px; color:var(--text-helper);}
|
||
|
||
/* Ghost suggestions inside clusters */
|
||
.ghost-block{margin-top:12px; padding:10px 12px; background:var(--bg); border:1px dashed var(--border-strong);}
|
||
.ghost-label{font-size:11px; letter-spacing:.4px; text-transform:uppercase; color:var(--text-helper); margin-bottom:6px;}
|
||
.ghost-row{display:flex; align-items:center; gap:12px; padding:5px 0;}
|
||
.ghost-row .gtext{flex:1; font-size:13px; color:var(--text-secondary); font-style:italic;}
|
||
.cl-add{display:flex; gap:8px; margin-top:12px; align-items:center;}
|
||
.cl-add input{flex:1; border:none; border-bottom:1px solid var(--border-strong); background:var(--bg); padding:8px 10px; font-size:13.5px; font-family:inherit;}
|
||
.cl-add select{border:none; border-bottom:1px solid var(--border-strong); background:var(--bg); padding:7px 8px; font-size:12.5px; font-family:inherit;}
|
||
|
||
/* Map */
|
||
.tray{background:var(--layer); border:1px solid var(--border); border-left:4px solid var(--danger); padding:14px 16px; margin:16px 0;}
|
||
.tray h2{font-size:15px; font-weight:600; margin-bottom:8px;}
|
||
.tray-row{display:flex; align-items:center; gap:10px; padding:6px 0; border-bottom:1px solid var(--border);}
|
||
.tray-row:last-child{border-bottom:none;}
|
||
.tray-row .ttext{flex:1; font-size:14px;}
|
||
select.assign, .cl-head select, .member select{border:none; border-bottom:1px solid var(--border-strong); background:var(--bg); padding:6px 8px; font-size:12.5px; font-family:inherit; max-width:250px;}
|
||
|
||
.cluster{background:var(--layer); border:1px solid var(--border); border-left:4px solid var(--interactive); margin-bottom:12px;}
|
||
.cluster.promoted{border-left-color:var(--success);}
|
||
.cl-head{display:flex; align-items:center; gap:12px; padding:11px 16px; cursor:pointer;}
|
||
.cl-head:hover{background:var(--layer-hover);}
|
||
.caret{width:20px; font-size:13px; color:var(--text-secondary); text-align:center; flex-shrink:0;}
|
||
.cl-name{font-size:16px; font-weight:600;}
|
||
.cl-count{font-size:13px; color:var(--text-secondary);}
|
||
.rank-chip{font-size:11.5px; font-weight:600; background:var(--interactive); color:#fff; border-radius:12px; padding:2px 9px;}
|
||
.cl-head .spacer{flex:1;}
|
||
.dot-ctl{display:flex; align-items:center; gap:6px;}
|
||
.dot-btn{width:32px; height:32px; border:1px solid var(--border-strong); background:var(--layer); font-size:16px; color:var(--text-secondary);}
|
||
.dot-btn:hover{background:var(--layer-hover); color:var(--text);}
|
||
.dot-n{width:30px; text-align:center; font-size:16px; font-weight:600;}
|
||
.promoted-flag{font-size:12px; color:var(--success); font-weight:600; white-space:nowrap;}
|
||
.cl-body{border-top:1px solid var(--border); padding:14px 16px;}
|
||
.cl-root label{font-size:11px; letter-spacing:.4px; text-transform:uppercase; color:var(--text-helper); display:block; margin-bottom:5px;}
|
||
.cl-root textarea{width:100%; border:none; background:var(--bg); padding:9px 11px; font-size:14.5px; font-weight:500; font-family:inherit; resize:vertical; min-height:48px; color:var(--text);}
|
||
.members{margin-top:10px;}
|
||
.member{display:flex; align-items:center; gap:10px; padding:6px 0 6px 26px; border-bottom:1px solid var(--border); position:relative;}
|
||
.member:last-child{border-bottom:none;}
|
||
.member::before{content:''; position:absolute; left:9px; top:0; bottom:50%; width:12px; border-left:1px solid var(--border-strong); border-bottom:1px solid var(--border-strong);}
|
||
.member:not(:last-child)::after{content:''; position:absolute; left:9px; top:50%; bottom:0; border-left:1px solid var(--border-strong);}
|
||
.member .mtext{flex:1; font-size:13.5px;}
|
||
.row-del{background:none; border:none; color:var(--text-helper); font-size:15px;}
|
||
.row-del:hover{color:var(--danger);}
|
||
.pedit{background:none; border:none; color:var(--text-helper); font-size:14px;}
|
||
.pedit:hover{color:var(--interactive);}
|
||
.medit{width:100%; border:none; background:var(--bg); padding:7px 9px; font-size:13.5px; font-family:inherit; resize:vertical;}
|
||
|
||
/* Breadcrumbs */
|
||
.bc{background:var(--layer); border:1px solid var(--border); margin-bottom:16px; padding:16px;}
|
||
.bc.example{border-left:4px solid var(--teal);}
|
||
.bc-top{display:grid; grid-template-columns:repeat(3, 1fr); gap:14px; margin-bottom:12px;}
|
||
.bc-out{display:grid; grid-template-columns:repeat(4, 1fr); gap:14px; background:var(--bg); padding:12px; border:1px solid var(--border);}
|
||
.bc-field h3{font-size:10.5px; letter-spacing:.5px; text-transform:uppercase; color:var(--text-helper); margin-bottom:5px; display:flex; align-items:center; gap:6px;}
|
||
.stepnum{width:17px; height:17px; border-radius:50%; background:var(--text); color:#fff; font-size:10.5px; display:inline-flex; align-items:center; justify-content:center; flex-shrink:0;}
|
||
.bc-field textarea{width:100%; border:none; background:var(--bg); padding:8px 10px; font-size:13.5px; font-family:inherit; resize:vertical; min-height:56px; color:var(--text);}
|
||
.bc-out .bc-field textarea{background:var(--layer);}
|
||
.bc-out-head{grid-column:1/-1; font-size:10.5px; letter-spacing:.5px; text-transform:uppercase; color:var(--text-helper); display:flex; align-items:center; gap:6px;}
|
||
.bc-related{margin:0 0 12px; font-size:13px;}
|
||
.bc-related summary{cursor:pointer; color:var(--interactive);}
|
||
.bc-related li{margin-left:22px; padding:2px 0; color:var(--text-secondary);}
|
||
.bc-foot{display:flex; align-items:center; gap:14px; margin-top:12px; flex-wrap:wrap;}
|
||
.gate{display:flex; gap:12px; font-size:12.5px; flex-wrap:wrap;}
|
||
.gate span{color:var(--text-helper);}
|
||
.gate span.ok{color:var(--success); font-weight:600;}
|
||
.gate-hint{font-size:12.5px; color:var(--warning); background:var(--warning-bg); padding:5px 10px;}
|
||
.example-tag{font-size:10px; letter-spacing:.4px; text-transform:uppercase; color:var(--teal); border:1px solid var(--teal); padding:0 6px; border-radius:9px;}
|
||
.type-tag{display:inline-block; font-size:11.5px; font-weight:600; padding:3px 10px; border-radius:12px;}
|
||
.tt-outcome{background:#defbe6; color:#0e6027;}
|
||
.tt-adoption{background:#edf5ff; color:var(--interactive);}
|
||
.type-toggle{border:none; background:none; padding:0;}
|
||
.summary-bar{background:var(--layer); border:1px solid var(--border); padding:12px 18px; margin-bottom:16px; display:flex; gap:26px; font-size:13.5px; flex-wrap:wrap;}
|
||
.summary-bar b{font-size:19px; font-weight:600; display:block;}
|
||
.summary-bar .warn b{color:var(--danger);}
|
||
.note-bar{background:var(--warning-bg); border:1px solid var(--warning); color:var(--warning); font-size:13.5px; padding:10px 14px; margin-bottom:16px; max-width:980px;}
|
||
.empty{background:var(--layer); border:1px dashed var(--border-strong); padding:30px; text-align:center; color:var(--text-helper); font-size:15px;}
|
||
|
||
/* Actions */
|
||
.act-row{background:var(--layer); border:1px solid var(--border); margin-bottom:6px; display:flex; align-items:center; gap:12px; padding:10px 14px;}
|
||
.act-row.done{opacity:.5;}
|
||
.act-row.done .act-text{text-decoration:line-through;}
|
||
.act-check{width:22px; height:22px; flex-shrink:0; border:1px solid var(--border-strong); background:var(--layer); font-size:13px;}
|
||
.act-check.on{background:var(--success); border-color:var(--success); color:#fff;}
|
||
.act-text{flex:1; font-size:14px;}
|
||
.act-row input[type=date], .act-row select{border:none; border-bottom:1px solid var(--border-strong); background:var(--bg); padding:6px 8px; font-size:13px; font-family:inherit;}
|
||
|
||
.toast{position:fixed; bottom:24px; left:50%; transform:translateX(-50%); background:var(--text); color:#fff; padding:12px 22px; font-size:14px; opacity:0; pointer-events:none; transition:opacity .2s; z-index:50;}
|
||
.toast.show{opacity:1;}
|
||
#jsonFile{display:none;}
|
||
@media (max-width:1200px){ .bc-top{grid-template-columns:1fr;} .bc-out{grid-template-columns:1fr 1fr;} }
|
||
@media (prefers-reduced-motion: reduce){ *{transition:none !important;} }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<div class="brand"><strong>Project SDE</strong> <span>| Field Problem Workshop v5</span></div>
|
||
<div class="spacer"></div>
|
||
<span class="persist-note">State lives in this session. Save JSON before closing.</span>
|
||
<button class="hdr-btn" id="btnLoad">Load JSON</button>
|
||
<button class="hdr-btn" id="btnSave">Save JSON</button>
|
||
<button class="hdr-btn" id="btnReset">Reset</button>
|
||
<button class="hdr-btn primary" id="btnExport">Export summary</button>
|
||
<input type="file" id="jsonFile" accept=".json,application/json">
|
||
</header>
|
||
|
||
<nav>
|
||
<button class="active" data-panel="map">1. Problem Map <span class="count warn" id="cntUncl">0</span></button>
|
||
<span class="flow">→</span>
|
||
<button data-panel="bc">2. Breadcrumbs <span class="count" id="cntBc">0</span></button>
|
||
<span class="flow">→</span>
|
||
<button data-panel="act">3. Actions <span class="count" id="cntAct">0</span></button>
|
||
</nav>
|
||
|
||
<main>
|
||
|
||
<!-- ===== 1 PROBLEM MAP ===== -->
|
||
<section class="panel active" id="panel-map">
|
||
<div class="panel-head">
|
||
<h1>Problem Map</h1>
|
||
<p>Capture at the top; new items land in the red tray until attached to a root problem. Each cluster is a root problem and the issues under it are the evidence. Vote with dots directly on the cluster headers, then promote the winners to Breadcrumbs.</p>
|
||
</div>
|
||
<div class="add-row">
|
||
<input type="text" id="newProb" placeholder="State what goes wrong and what it costs" aria-label="New item">
|
||
<select id="newProbWho" aria-label="Raised by"></select>
|
||
<select id="newProbCluster" aria-label="Cluster"></select>
|
||
<button class="btn" id="btnAddProb">Add</button>
|
||
</div>
|
||
<div class="tray" id="unclTray"></div>
|
||
<div id="clusterList"></div>
|
||
<div class="add-row" style="margin-top:14px;">
|
||
<input type="text" id="newCluster" placeholder="Add a root problem cluster (short name)" aria-label="New cluster">
|
||
<button class="btn ghost" id="btnAddCluster">Add cluster</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== 2 BREADCRUMBS ===== -->
|
||
<section class="panel" id="panel-bc">
|
||
<div class="panel-head">
|
||
<h1>Breadcrumbs</h1>
|
||
<p>Root problem to value driver to capability to measurable outcome. A completed breadcrumb IS a pilot success criterion; the export builds the criteria table from every complete card. AI draft fills the fields from construction execution methodology (EVM, CII AWP, Last Planner, first-time quality); the room reviews and edits.</p>
|
||
</div>
|
||
<div class="summary-bar" id="bcSummary"></div>
|
||
<div id="bcList"></div>
|
||
<div class="add-row"><button class="btn ghost" id="btnAddBc">Add a blank breadcrumb</button></div>
|
||
</section>
|
||
|
||
<!-- ===== 3 ACTIONS ===== -->
|
||
<section class="panel" id="panel-act">
|
||
<div class="panel-head">
|
||
<h1>Actions</h1>
|
||
<p>Every open item gets an owner and a date. Scan adds an action for each incomplete breadcrumb, each cluster missing a root statement, and any unclustered issues.</p>
|
||
</div>
|
||
<div class="add-row" style="margin-bottom:16px;">
|
||
<button class="btn" id="btnScan">Scan for incomplete items</button>
|
||
</div>
|
||
<div id="actList"></div>
|
||
<div class="add-row" style="margin-top:14px;">
|
||
<input type="text" id="newAct" placeholder="Add an action" aria-label="New action">
|
||
<button class="btn ghost" id="btnAddAct">Add</button>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
'use strict';
|
||
const PEOPLE = ['Nick','Tim','Jason','Brendan','Lee','Cody','Clinton','Wyman','Cameron','Ryan','Team'];
|
||
const TYPES = ['Problem','Question','Idea'];
|
||
|
||
function P(id,t,who,cluster,opts){ return Object.assign({id, t, who, cluster: cluster||'', type:'Problem', sugg:false}, opts||{}); }
|
||
function C(id,name,root,ws){ return {id, name, root, ws: ws||'', open:false, dots:0, promoted:false}; }
|
||
function B(o){ return Object.assign({id:uid(), example:false, type:'Outcome', prob:'', related:[], driver:'', cap:'', metric:'', method:'', baseline:'', target:''}, o||{}); }
|
||
|
||
const SUGGESTIONS = [
|
||
{cluster:'c1', t:'There is no shared definition of what counts as complete for each asset stage, so reported percent complete means different things by team'},
|
||
{cluster:'c1', t:'Percent complete is count based, not effort based: a 2 hour device and a 2 day panel move the number the same amount'},
|
||
{cluster:'c1', t:'Turnover readiness by system is not visible, so incomplete predecessor work is discovered at commissioning'},
|
||
{cluster:'c2', t:'The field gets no feedback that their updates were used, so the discipline to enter data decays'},
|
||
{cluster:'c2', t:'As-built and redline data is not captured at completion, so closeout becomes a reconstruction project'},
|
||
{cluster:'c2', t:'Each project invents its own tracker (Excel, paper, whiteboard), so nothing rolls up across projects'},
|
||
{cluster:'c3', t:'Permits, LOTO, and energization requirements are not confirmed before work is released, causing same day stand downs'},
|
||
{cluster:'c3', t:'Work areas are not confirmed released by the GC or other trades before crews are sent'},
|
||
{cluster:'c3', t:'There is no defined readiness check before work is released; each foreman decides on their own what to verify'},
|
||
{cluster:'c4', t:'Crews do not see a lookahead: the plan lives with the PM or GF, so the next assignment travels verbally'},
|
||
{cluster:'c4', t:'Commitments made at standup are not tracked, so we never know what percent of promised work actually completed'},
|
||
{cluster:'c4', t:'There is no visibility to which crews or skills free up when, so reassignment is reactive'},
|
||
{cluster:'c5', t:'Extra work and T&M are not captured at the time they are performed, so recoverable cost is lost'},
|
||
{cluster:'c5', t:'There is no single owner of the change log; pending changes live in email threads'},
|
||
{cluster:'c5', t:'Scope splits at contract boundaries are not documented anywhere the field can check'},
|
||
{cluster:'c6', t:'There is no traceable record of who installed or verified a termination, so failure investigations start from zero'},
|
||
{cluster:'c6', t:'The field cannot tell whether the drawing in hand is the current revision without calling the office'},
|
||
{cluster:'c6', t:'QA hold points are not enforced by the process; skipping a gate has no visible consequence until rework'},
|
||
{cluster:'c7', t:'Estimating does not receive field actuals in a usable form, so estimates cannot learn from performance'}
|
||
];
|
||
|
||
const DEFAULT_STATE = {
|
||
clusters:[
|
||
C('c1','Progress Visibility','We cannot answer where a project stands: no trustworthy, phase level percent complete exists','Tracking MVP (pilot)'),
|
||
C('c2','Data Freshness & Entry Burden','Field data is captured late, on paper, or more than once, so the record is stale and expensive to maintain',''),
|
||
C('c3','Constraint Readiness','Work is released before it is ready: material, tools, and information are not confirmed available before crews start','IWP Tool Suite'),
|
||
C('c4','Planning & Resources','Crews and planners lack visibility to backlog, priorities, and resource availability, producing idle time and churn',''),
|
||
C('c5','Scope Control','Scope changes and gaps move faster than our change control: work happens without approval or falls out entirely','AWP Process RFP'),
|
||
C('c6','Quality & Spec Conformance','The field cannot reliably build to the approved design: specs, revisions, and QA gates do not reach the point of work','AWP Process RFP'),
|
||
C('c7','Knowledge & Integration','What we learn does not travel: knowledge stays with individuals and external platforms stay closed to us','')
|
||
],
|
||
problems:[
|
||
P('p01','No one can reliably answer "where are we at / what percent complete" on a project','Jason','c1'),
|
||
P('p05','Percent complete is needed by phase (engineering, BIM, tray/conduit), not just one overall number','Jason','c1'),
|
||
P('p27','Work started but not finished. (ie field hits constraint and forgets to return to the scope)','Cody','c1'),
|
||
P('p17','Constraints (ie trade stacking) not being captured in real time. No visibility to GF / Super loosing the ability to better communicate at stand up meetings','Nick','c1'),
|
||
P('p34','Install at risk - BIM has no visibility to what has been installed without being captured in the model.','Team','c1'),
|
||
P('p02','Field data goes stale because updates are not entered when the work completes','Tim','c2'),
|
||
P('p37','Data is being captured in the field on paper and brought back to the office to be converted to digital (ie QA check lists, Time sheets, daily log)','Team','c2'),
|
||
P('p08','Crews walk back to the trailer to turn in daily logs and forms','Tim','c2'),
|
||
P('p09','Getting assets, IO, and cable lists into a workable relational database takes heavy manual entry','Jason','c2'),
|
||
P('p10','Double entry between our tools and the GC platform on every project','Jason','c2'),
|
||
P('p36','cable pull list has duplicate or missing cable','Team','c2'),
|
||
P('p28','material is not available when the scope is executed.','Nick','c3'),
|
||
P('p29','Equipment / tools are not available when the scope is being executed (ladders, lift)','Team','c3'),
|
||
P('p06','We overspend on material because nobody knows where material is','Nick','c3'),
|
||
P('p18','working with incomplete data sets (ie installing with no BIM)','Cody','c3'),
|
||
P('p22','poor visibility to predecessors / requirements for the work being done. (ie if a peice of equipment needs to be commissioned but it requires PTP backbone to be operational on the other side of the building))','Tim','c3'),
|
||
P('p04','Idle time: people stand around waiting for their next assignment','Tim','c4'),
|
||
P('p14','Foreman dont have good visibility to work backlog causing Idle time.','Nick','c4'),
|
||
P('p16','Bim starts work on priority 1, then told to stop and work on priority 3, this repeats','Cody','c4'),
|
||
P('p19','Resource availability is not availible to project teams (ie if a PM needs a tech, where can they get one?)','Nick','c4'),
|
||
P('p42','Progress halts due to loss of resource (ie pto, terminated)','Clinton','c4'),
|
||
P('p30','Scope takes longer than expected (could this be mitigated by including the field team in estimates)','Team','c4'),
|
||
P('p03','scope gap - scope performed without approved change order.','Clinton','c5'),
|
||
P('p40','scope gap - in scope not performed as it was lost in pre-contruction activities (ie dropped from the io list)','Clinton','c5'),
|
||
P('p41','Scope Gap - who terminates the back bone fiber?','Clinton','c5'),
|
||
P('p35','Field team taking direction from external sources causing rework as it was not approved through the proper channels.','Team','c5'),
|
||
P('p33','When field team deviates from plan failure to communicate that back to design (ie redlines during the course of work rather than at the end)','Team','c5'),
|
||
P('p07','Field rework happens because crews do not have the latest drawings','Nick','c6'),
|
||
P('p15','Rework caused by construction tracking not having the correct data','Cody','c6'),
|
||
P('p23','Failure in sequencing, missing the QA gate (ie terminating a cable that is TSP when it should have been a 4 Conductor)','Cody','c6'),
|
||
P('p24','wrong type of conduit installed due to environmental requirements ( ie Class 1 div 2 area had EMT installed) failure mode: out of spec','Clinton','c6'),
|
||
P('p20','installed equipment doesnt match submittals','Brendan','c6'),
|
||
P('p21','install material used was does not match whats approved in the submittal','Nick','c6'),
|
||
P('p25','Specs not accessible to field installation teams','Clinton','c6'),
|
||
P('p31','visability to chain of custody of project scope, drawings, design intent (who do I ask when constructability issue arises)','Team','c6'),
|
||
P('p32','field teams fail to follow the designed workflow/ install.','Team','c6'),
|
||
P('p13','Punch list / issue follow-through is not measured; time to close is unknown','Nick','c7'),
|
||
P('p43','Tribal knowledge is lost after someone leaves or is on PTO','Clinton','c7'),
|
||
P('p38','Lessons learned - Construction methods could be more efficient with one team and this is not shared with the other.','Team','c7'),
|
||
P('p39','What are all of the ways we are reinventing the wheel?','Team','c7',{type:'Question'}),
|
||
P('p26','Suggestion improvement: create info graphics and other simplified ways of communicating project specs','Clinton','c7',{type:'Idea'}),
|
||
P('p11','GC platform sync/access requests get denied; we have never reached the people who actually own the decision','Tim','c7'),
|
||
P('p12','Platform changes GC to GC, so crews face retraining on a new tool nearly every project','Brendan','c7')
|
||
],
|
||
breadcrumbs:[
|
||
B({id:'b1', example:true, type:'Outcome',
|
||
prob:'Crews walk back to the trailer to turn in daily logs and forms',
|
||
driver:'Minimize non productive field time; keep craft hours on the work face',
|
||
cap:'Mobile field entry from the work area (phone / tablet, offline capable)',
|
||
metric:'% of daily logs submitted from the work area on the same day',
|
||
method:'System timestamps on log submission vs work date',
|
||
baseline:'Field poll of minutes per day spent on paperwork and walking, taken before pilot', target:''}),
|
||
B({id:'b2', type:'Outcome',
|
||
prob:'No one can reliably answer "where are we at / what percent complete" on a project',
|
||
driver:'man power / work planning',
|
||
cap:'construction tracking / resource planning',
|
||
metric:'do we have a tool, tool has been implemented and adopted, ~>80% visibility to the project progress',
|
||
method:'', baseline:'', target:''}),
|
||
B({id:'b3', type:'Outcome', prob:'Field data goes stale because updates are not entered when the work completes'}),
|
||
B({id:'b4', type:'Outcome', prob:'scope gap - scope performed without approved change order.'}),
|
||
B({id:'b5', type:'Outcome',
|
||
prob:'Double entry between our tools and the GC platform on every project',
|
||
driver:'Reduced duplicate entry',
|
||
cap:'Single system of entry with automated propagation',
|
||
metric:'Number of systems requiring manual entry of the same record',
|
||
method:'Trace 10 representative records end to end',
|
||
baseline:'Count on a current project before pilot', target:'1 internal system of entry'}),
|
||
B({id:'b6', type:'Adoption',
|
||
prob:'Supporting adoption measure for the pilot',
|
||
driver:'Field usage',
|
||
cap:'Pilot tool in daily field use',
|
||
metric:'% of assigned field users active weekly',
|
||
method:'Platform usage analytics',
|
||
baseline:'n/a, starts at zero', target:''})
|
||
],
|
||
actions:[
|
||
{id:'a1', t:'Review cluster assignments and root problem statements as a team; move issues that landed in the wrong cluster', owner:'', due:'', done:false},
|
||
{id:'a2', t:'Complete breadcrumb: Field data goes stale because updates are not entered when the work completes', owner:'', due:'', done:false},
|
||
{id:'a3', t:'Complete breadcrumb: scope gap - scope performed without approved change order.', owner:'', due:'', done:false},
|
||
{id:'a4', t:'Rework the percent complete breadcrumb: current metric is an adoption statement, needs a real outcome metric with method and baseline', owner:'', due:'', done:false},
|
||
{id:'a5', t:'Assign workstreams to the three unmapped clusters (Data Freshness, Planning & Resources, Knowledge & Integration)', owner:'', due:'', done:false}
|
||
]
|
||
};
|
||
|
||
let S = JSON.parse(JSON.stringify(DEFAULT_STATE));
|
||
function save(){ /* in-memory; use Save JSON to persist */ }
|
||
function uid(){ return 'x' + Math.random().toString(36).slice(2,9); }
|
||
function esc(s){ return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||
function opts(list, sel){ return list.map(v=>`<option${v===sel?' selected':''}>${esc(v)}</option>`).join(''); }
|
||
function shortT(t, n){ t = String(t); return t.length > n ? t.slice(0, n-1) + '…' : t; }
|
||
function clusterById(id){ return S.clusters.find(c=>c.id===id); }
|
||
function membersOf(cid){ return S.problems.filter(p=>p.cluster===cid); }
|
||
function activeMembers(cid){ return S.problems.filter(p=>p.cluster===cid && p.type==='Problem'); }
|
||
|
||
document.querySelectorAll('nav button').forEach(b=>{
|
||
b.addEventListener('click', ()=>{
|
||
document.querySelectorAll('nav button').forEach(x=>x.classList.remove('active'));
|
||
document.querySelectorAll('.panel').forEach(x=>x.classList.remove('active'));
|
||
b.classList.add('active');
|
||
document.getElementById('panel-' + b.dataset.panel).classList.add('active');
|
||
renderAll();
|
||
});
|
||
});
|
||
|
||
/* ===== 1 PROBLEM MAP ===== */
|
||
document.getElementById('newProbWho').innerHTML = opts(PEOPLE,'Team');
|
||
const editingItems = new Set();
|
||
function clusterOpts(sel){
|
||
return `<option value=""${!sel?' selected':''}>Unclustered</option>` +
|
||
S.clusters.map(c=>`<option value="${c.id}"${sel===c.id?' selected':''}>${esc(c.name)}</option>`).join('');
|
||
}
|
||
function renderMap(){
|
||
const uncl = S.problems.filter(p=>!p.cluster || !clusterById(p.cluster));
|
||
document.getElementById('cntUncl').textContent = uncl.length;
|
||
const capSel = document.getElementById('newProbCluster');
|
||
const prevSel = capSel.value;
|
||
capSel.innerHTML = clusterOpts(clusterById(prevSel) ? prevSel : '');
|
||
|
||
const tray = document.getElementById('unclTray');
|
||
tray.innerHTML = `<h2>Unclustered (${uncl.length})</h2>` + (uncl.length
|
||
? uncl.map(p=>`<div class="tray-row">
|
||
<span class="type-chip ty-${p.type}">${p.type}</span>
|
||
<div class="ttext">${esc(p.t)}</div>
|
||
<select class="assign" data-assign="${p.id}">${clusterOpts('')}</select>
|
||
<button class="row-del" data-pdel="${p.id}" title="Remove">✕</button>
|
||
</div>`).join('')
|
||
: '<div style="font-size:13.5px; color:var(--text-helper);">Everything is attached to a root problem.</div>');
|
||
|
||
const ranked = [...S.clusters].sort((a,b)=>b.dots-a.dots).filter(c=>c.dots>0).slice(0,3).map(c=>c.id);
|
||
const list = document.getElementById('clusterList');
|
||
list.innerHTML = S.clusters.map(c=>{
|
||
const mem = membersOf(c.id);
|
||
const rank = ranked.indexOf(c.id);
|
||
const existing = new Set(S.problems.map(p=>p.t));
|
||
const ghosts = SUGGESTIONS.map((s,i)=>({...s, idx:i})).filter(s=>s.cluster===c.id && !existing.has(s.t));
|
||
const ghostHtml = ghosts.length ? `<div class="ghost-block">
|
||
<div class="ghost-label">Also consider (from theme analysis):</div>
|
||
${ghosts.map(s=>`<div class="ghost-row">
|
||
<div class="gtext">${esc(s.t)}</div>
|
||
<button class="btn small ghost" data-sadd="${s.idx}">+ Add</button>
|
||
</div>`).join('')}
|
||
</div>` : '';
|
||
const body = !c.open ? '' : `<div class="cl-body">
|
||
<div class="cl-root">
|
||
<label>Root problem statement</label>
|
||
<textarea data-croot="${c.id}" placeholder="Name the underlying problem these issues share">${esc(c.root)}</textarea>
|
||
</div>
|
||
<div class="members">
|
||
${mem.length ? mem.map(p=>{
|
||
if(editingItems.has(p.id)){
|
||
return `<div class="member">
|
||
<textarea class="medit" data-etext="${p.id}">${esc(p.t)}</textarea>
|
||
<button class="btn small" data-esave="${p.id}">Save</button>
|
||
<button class="btn small ghost" data-ecancel="${p.id}">Cancel</button>
|
||
</div>`;
|
||
}
|
||
return `<div class="member">
|
||
<span class="type-chip ty-${p.type}">${p.type}</span>
|
||
<div class="mtext">${esc(p.t)} ${p.sugg?'<span class="sug-chip">Suggested</span>':''} <span class="src-tag">(${p.sugg?'theme analysis':esc(p.who)})</span></div>
|
||
<button class="pedit" data-pedit="${p.id}" title="Edit">✎</button>
|
||
<select data-assign="${p.id}" title="Move to">${clusterOpts(c.id)}</select>
|
||
<button class="row-del" data-pdel="${p.id}" title="Remove">✕</button>
|
||
</div>`;
|
||
}).join('') : '<div style="font-size:13px; color:var(--text-helper); padding:6px 0 0 26px;">No issues attached yet.</div>'}
|
||
</div>
|
||
${ghostHtml}
|
||
<div class="cl-add">
|
||
<input type="text" data-caddinput="${c.id}" placeholder="Add an issue directly to this cluster" aria-label="Add issue to ${esc(c.name)}">
|
||
<select data-caddwho="${c.id}">${opts(PEOPLE,'Team')}</select>
|
||
<button class="btn small ghost" data-cadd="${c.id}">Add</button>
|
||
</div>
|
||
</div>`;
|
||
return `<div class="cluster${c.promoted?' promoted':''}">
|
||
<div class="cl-head" data-toggle="${c.id}">
|
||
<span class="caret">${c.open?'▼':'▶'}</span>
|
||
${rank>-1?`<span class="rank-chip">#${rank+1}</span>`:''}
|
||
<span class="cl-name">${esc(c.name)}</span>
|
||
<span class="cl-count">${mem.length} issue${mem.length===1?'':'s'}${c.ws?' · '+esc(c.ws):''}</span>
|
||
<span class="spacer"></span>
|
||
${c.promoted?'<span class="promoted-flag">✓ In Breadcrumbs</span>':`<button class="btn small ghost" data-cprom="${c.id}">Promote →</button>`}
|
||
<div class="dot-ctl">
|
||
<button class="dot-btn" data-cdot="${c.id}" data-d="-1">−</button>
|
||
<span class="dot-n">${c.dots}</span>
|
||
<button class="dot-btn" data-cdot="${c.id}" data-d="1">+</button>
|
||
</div>
|
||
<button class="row-del" data-cdel="${c.id}" title="Remove cluster (issues return to Unclustered)">✕</button>
|
||
</div>
|
||
${body}
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
document.getElementById('panel-map').addEventListener('click', e=>{
|
||
const t = e.target;
|
||
if(t.dataset.sadd !== undefined){
|
||
const s = SUGGESTIONS[+t.dataset.sadd];
|
||
if(s && !S.problems.some(p=>p.t===s.t)){
|
||
const cl = clusterById(s.cluster);
|
||
S.problems.push(P(uid(), s.t, 'Team', cl ? s.cluster : '', {sugg:true}));
|
||
save(); renderMap(); toast(cl ? 'Added to ' + cl.name : 'Added (unclustered)');
|
||
}
|
||
return;
|
||
}
|
||
if(t.dataset.cdot){ const c=clusterById(t.dataset.cdot); if(c){ c.dots=Math.max(0,c.dots+(+t.dataset.d)); save(); renderMap(); } return; }
|
||
if(t.dataset.cprom){
|
||
const c = clusterById(t.dataset.cprom);
|
||
if(c){
|
||
c.promoted = true;
|
||
S.breadcrumbs.push(B({prob:(c.root||c.name), related:activeMembers(c.id).map(p=>p.t)}));
|
||
save(); renderAll(); toast('Promoted with ' + activeMembers(c.id).length + ' related issues');
|
||
}
|
||
return;
|
||
}
|
||
if(t.dataset.cdel){
|
||
const c = clusterById(t.dataset.cdel);
|
||
if(c && confirm(`Remove cluster "${c.name}"? Its issues return to Unclustered.`)){
|
||
S.problems.forEach(p=>{ if(p.cluster===c.id) p.cluster=''; });
|
||
S.clusters = S.clusters.filter(x=>x.id!==c.id);
|
||
save(); renderMap();
|
||
}
|
||
return;
|
||
}
|
||
if(t.dataset.cadd){
|
||
const inp = document.querySelector(`input[data-caddinput="${t.dataset.cadd}"]`);
|
||
const who = document.querySelector(`select[data-caddwho="${t.dataset.cadd}"]`);
|
||
const v = inp ? inp.value.trim() : '';
|
||
if(v){
|
||
S.problems.push(P(uid(), v, who ? who.value : 'Team', t.dataset.cadd));
|
||
save(); renderMap(); toast('Added to ' + (clusterById(t.dataset.cadd)||{name:'cluster'}).name);
|
||
}
|
||
return;
|
||
}
|
||
if(t.dataset.pedit){ editingItems.add(t.dataset.pedit); renderMap(); return; }
|
||
if(t.dataset.ecancel){ editingItems.delete(t.dataset.ecancel); renderMap(); return; }
|
||
if(t.dataset.esave){
|
||
const p = S.problems.find(x=>x.id===t.dataset.esave);
|
||
const ta = document.querySelector(`textarea[data-etext="${t.dataset.esave}"]`);
|
||
if(p && ta && ta.value.trim()){ p.t = ta.value.trim(); editingItems.delete(p.id); save(); renderMap(); toast('Updated'); }
|
||
else toast('Text cannot be empty');
|
||
return;
|
||
}
|
||
if(t.dataset.pdel){
|
||
S.problems = S.problems.filter(p=>p.id !== t.dataset.pdel);
|
||
editingItems.delete(t.dataset.pdel);
|
||
save(); renderMap();
|
||
return;
|
||
}
|
||
const head = t.closest('[data-toggle]');
|
||
if(head && t.tagName!=='SELECT' && t.tagName!=='OPTION' && !t.dataset.cdot && !t.dataset.cprom && !t.dataset.cdel){
|
||
const c = clusterById(head.dataset.toggle);
|
||
if(c){ c.open = !c.open; save(); renderMap(); }
|
||
}
|
||
});
|
||
document.getElementById('panel-map').addEventListener('keydown', e=>{
|
||
if(e.key==='Enter' && e.target.dataset && e.target.dataset.caddinput){
|
||
e.preventDefault();
|
||
const btn = document.querySelector(`button[data-cadd="${e.target.dataset.caddinput}"]`);
|
||
if(btn) btn.click();
|
||
}
|
||
});
|
||
document.getElementById('panel-map').addEventListener('change', e=>{
|
||
const t = e.target;
|
||
if(t.dataset.assign){
|
||
const p = S.problems.find(x=>x.id===t.dataset.assign);
|
||
if(p){ p.cluster = t.value; save(); renderMap(); }
|
||
}
|
||
});
|
||
document.getElementById('panel-map').addEventListener('input', e=>{
|
||
if(e.target.dataset.croot){
|
||
const c = clusterById(e.target.dataset.croot);
|
||
if(c){ c.root = e.target.value; save(); }
|
||
}
|
||
});
|
||
document.getElementById('btnAddProb').addEventListener('click', ()=>{
|
||
const inp = document.getElementById('newProb');
|
||
const v = inp.value.trim();
|
||
if(!v) return;
|
||
const cid = document.getElementById('newProbCluster').value;
|
||
S.problems.push(P(uid(), v, document.getElementById('newProbWho').value, clusterById(cid) ? cid : ''));
|
||
inp.value=''; save(); renderMap();
|
||
toast(clusterById(cid) ? 'Added to ' + clusterById(cid).name : 'Captured. Attach it to a root problem.');
|
||
});
|
||
document.getElementById('newProb').addEventListener('keydown', e=>{ if(e.key==='Enter') document.getElementById('btnAddProb').click(); });
|
||
document.getElementById('btnAddCluster').addEventListener('click', ()=>{
|
||
const inp = document.getElementById('newCluster');
|
||
const v = inp.value.trim();
|
||
if(!v) return;
|
||
const c = C(uid(), v, '', ''); c.open = true;
|
||
S.clusters.push(c);
|
||
inp.value=''; save(); renderMap();
|
||
});
|
||
document.getElementById('newCluster').addEventListener('keydown', e=>{ if(e.key==='Enter') document.getElementById('btnAddCluster').click(); });
|
||
|
||
/* ===== 2 BREADCRUMBS ===== */
|
||
const ADOPTION_WORDS = /implement|adopt|roll ?out|have a tool|tool exists|usage|active users?|users? active/i;
|
||
const REQ = ['driver','cap','metric','method','baseline'];
|
||
function bcComplete(b){ return REQ.every(f=>String(b[f]||'').trim().length>0); }
|
||
function footHTML(b){
|
||
const adoptionFlag = b.type==='Outcome' && ADOPTION_WORDS.test(b.metric||'');
|
||
const gate = REQ.map(f=>{
|
||
const ok = String(b[f]||'').trim().length>0;
|
||
return `<span class="${ok?'ok':''}">${ok?'✓':'○'} ${f==='cap'?'capability':f}</span>`;
|
||
}).join('');
|
||
return `<button class="type-toggle" data-tt="${b.id}" title="Toggle Outcome / Adoption"><span class="type-tag ${b.type==='Outcome'?'tt-outcome':'tt-adoption'}">${b.type}</span></button>
|
||
<div class="gate">${gate}</div>
|
||
${adoptionFlag ? '<span class="gate-hint">Metric reads as adoption. Name the outcome the tool should move, or tag this card Adoption.</span>' : ''}
|
||
<button class="btn small ai" data-aibc="${b.id}">✨ AI draft fields</button>
|
||
<button class="row-del" data-bdel="${b.id}" title="Remove" style="margin-left:auto;">✕</button>`;
|
||
}
|
||
function bcSummaryHTML(){
|
||
const nOut = S.breadcrumbs.filter(b=>b.type==='Outcome').length;
|
||
const nDone = S.breadcrumbs.filter(bcComplete).length;
|
||
const nInc = S.breadcrumbs.length - nDone;
|
||
return `<div><b>${nDone}</b>Complete (= criteria)</div>
|
||
<div class="${nInc?'warn':''}"><b>${nInc}</b>Incomplete</div>
|
||
<div><b>${nOut}</b>Outcome</div><div><b>${S.breadcrumbs.length-nOut}</b>Adoption</div>`;
|
||
}
|
||
function renderBc(){
|
||
const wrap = document.getElementById('bcList');
|
||
document.getElementById('cntBc').textContent = S.breadcrumbs.length;
|
||
document.getElementById('bcSummary').innerHTML = bcSummaryHTML();
|
||
if(!S.breadcrumbs.length){ wrap.innerHTML = '<div class="empty">Promote root problems from the map, or add a blank breadcrumb.</div>'; return; }
|
||
wrap.innerHTML = S.breadcrumbs.map(b=>{
|
||
const rel = (b.related && b.related.length)
|
||
? `<details class="bc-related"><summary>Related issues carried from the cluster (${b.related.length})</summary><ul>${b.related.map(r=>`<li>${esc(r)}</li>`).join('')}</ul></details>` : '';
|
||
return `<div class="bc${b.example?' example':''}">
|
||
<div class="bc-top">
|
||
<div class="bc-field">
|
||
<h3><span class="stepnum">1</span>Root problem statement ${b.example?'<span class="example-tag">Worked example</span>':''}</h3>
|
||
<textarea data-bid="${b.id}" data-f="prob" placeholder="What goes wrong and what it costs">${esc(b.prob)}</textarea>
|
||
</div>
|
||
<div class="bc-field">
|
||
<h3><span class="stepnum">2</span>Value driver</h3>
|
||
<textarea data-bid="${b.id}" data-f="driver" placeholder="The end we are trying to achieve, not the tool">${esc(b.driver)}</textarea>
|
||
</div>
|
||
<div class="bc-field">
|
||
<h3><span class="stepnum">3</span>Solution capability</h3>
|
||
<textarea data-bid="${b.id}" data-f="cap" placeholder="The means: the capability that serves the driver">${esc(b.cap)}</textarea>
|
||
</div>
|
||
</div>
|
||
${rel}
|
||
<div class="bc-out">
|
||
<div class="bc-out-head"><span class="stepnum">4</span>Measurable outcome (a complete card is a pilot success criterion)</div>
|
||
<div class="bc-field"><h3>Metric</h3><textarea data-bid="${b.id}" data-f="metric" placeholder="What number moves, in what direction">${esc(b.metric)}</textarea></div>
|
||
<div class="bc-field"><h3>Method</h3><textarea data-bid="${b.id}" data-f="method" placeholder="Data source + comparison + cadence">${esc(b.method)}</textarea></div>
|
||
<div class="bc-field"><h3>Baseline</h3><textarea data-bid="${b.id}" data-f="baseline" placeholder="A number, or the pre-pilot study that produces one">${esc(b.baseline)}</textarea></div>
|
||
<div class="bc-field"><h3>Target (optional, set after baseline)</h3><textarea data-bid="${b.id}" data-f="target" placeholder="Set after baseline exists">${esc(b.target)}</textarea></div>
|
||
</div>
|
||
<div class="bc-foot" data-foot="${b.id}">${footHTML(b)}</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
document.getElementById('bcList').addEventListener('input', e=>{
|
||
if(e.target.dataset.bid){
|
||
const b = S.breadcrumbs.find(x=>x.id===e.target.dataset.bid);
|
||
if(b){
|
||
b[e.target.dataset.f] = e.target.value; save();
|
||
const foot = document.querySelector(`[data-foot="${b.id}"]`);
|
||
if(foot) foot.innerHTML = footHTML(b);
|
||
document.getElementById('bcSummary').innerHTML = bcSummaryHTML();
|
||
}
|
||
}
|
||
});
|
||
document.getElementById('bcList').addEventListener('click', e=>{
|
||
const tt = e.target.closest('[data-tt]');
|
||
if(tt){
|
||
const b = S.breadcrumbs.find(x=>x.id===tt.dataset.tt);
|
||
if(b){ b.type = b.type==='Outcome'?'Adoption':'Outcome'; save(); renderBc(); }
|
||
return;
|
||
}
|
||
if(e.target.dataset.aibc){ aiDraftBreadcrumb(e.target.dataset.aibc, e.target); return; }
|
||
if(e.target.dataset.bdel){
|
||
S.breadcrumbs = S.breadcrumbs.filter(b=>b.id!==e.target.dataset.bdel);
|
||
save(); renderBc();
|
||
}
|
||
});
|
||
document.getElementById('btnAddBc').addEventListener('click', ()=>{
|
||
S.breadcrumbs.push(B({}));
|
||
save(); renderBc();
|
||
});
|
||
|
||
/* ===== AI: draft breadcrumb fields from methodology ===== */
|
||
async function callClaude(prompt){
|
||
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ model: "claude-sonnet-4-6", max_tokens: 1000, messages: [{ role: "user", content: prompt }] })
|
||
});
|
||
if(!response.ok) throw new Error('API ' + response.status);
|
||
const data = await response.json();
|
||
return (data.content || []).filter(i=>i.type==='text').map(i=>i.text).join('\n');
|
||
}
|
||
function parseJson(text){
|
||
const clean = String(text).replace(/```json|```/g,'').trim();
|
||
const start = Math.min(...['[','{'].map(ch=>{ const i=clean.indexOf(ch); return i===-1?Infinity:i; }));
|
||
return JSON.parse(start===Infinity ? clean : clean.slice(start));
|
||
}
|
||
const METHOD_KB = `Domain playbook (anchor methodology -> standard metrics/methods/baselines):
|
||
1 PROGRESS VISIBILITY - Earned Value Mgmt, rules of credit. Metrics: claiming accuracy (reported % vs field-verified % within tolerance, by phase; method: monthly spot audit of 30+ sampled assets; baseline: first audit); forecast reliability (forecast vs actual completion; baseline: last project's reports). Never use "% of assets with a status" (adoption in disguise).
|
||
2 DATA FRESHNESS / ENTRY BURDEN - lean data flow, single source of truth. Metrics: entry latency (% of updates within 24h of completion; method: system timestamp vs completion date, weekly; baseline: audit paper-to-digital lag on current project); touch count (systems the same record is entered into; method: trace 10 records end to end; baseline: pre-pilot trace); non-productive data minutes/person/day (method+baseline: structured field poll pre-pilot, repeat midpoint).
|
||
3 CONSTRAINT READINESS / WORK RELEASE - CII AWP IR 272-2, constraint-free IWP release (drawings, material, tools, labor, access, predecessors, permits, quality docs, scaffolding). Metrics: % releases issued constraint-free (method: release-gate checklist audit weekly, later constraint stop = miss; baseline: audit last month of work starts from daily logs); stand-down count/week (method: foreman log with fixed reason list; baseline: 2-week logging exercise); tool time % (method: activity sampling study; baseline: pre-pilot study; propose only when stakes justify cost).
|
||
4 PLANNING / RESOURCES / IDLE - Last Planner System. Metrics: PPC (% weekly commitments completed as promised; method: log at weekly planning, score next week, publish trend; baseline: first 4 weeks of logging); reasons-for-variance distribution (fixed-list coding of misses); idle hours/crew-week (timecard or foreman reason codes; baseline: 2-week coding).
|
||
5 SCOPE CONTROL / CHANGE - PMBOK change control, CII change research. Metrics: unauthorized-work hours/dollars at time of performance (method: monthly reconciliation of field records vs CO log; baseline: run reconciliation on current project); change capture latency days (field record date vs log date); change cycle time (log timestamps; baseline: last project's log).
|
||
6 QUALITY / REWORK - first-time quality, CII rework research (2-6% direct cost where measured). Metrics: rework % of craft hours (method: dedicated rework cost code + reason coding, monthly; baseline: create the code, first month is baseline); first-pass yield at QC gates; out-of-rev incidents (QC walkdown coding).
|
||
7 KNOWLEDGE / FEEDBACK - CII lessons-learned, estimate feedback. Metrics: closeout compliance % within 60 days (method: closeout checklist audit; baseline: audit last 4-6 projects); % estimates referencing prior actuals (estimating file audit); punch closure median days (issue log timestamps; if untimestamped, that gap is the finding).
|
||
GATES: driver = business end in cost/schedule/quality/safety/predictability terms, never a tool. capability = function not product, no vendor names. metric = outcome ratio/rate, NEVER "implemented/adopted/rolled out/active users/have a tool". method = data source + comparison + cadence. baseline = a number or the named pre-pilot study that produces one (cheapest defensible: poll < record trace < log audit < 2-week coding < sampling study). Set targets after baseline, not before. Each field under ~30 words except method/baseline.`;
|
||
|
||
async function aiDraftBreadcrumb(bid, btn){
|
||
const b = S.breadcrumbs.find(x=>x.id===bid);
|
||
if(!b) return;
|
||
if(!String(b.prob||'').trim()){ toast('Write the problem statement first'); return; }
|
||
const hasContent = REQ.some(f=>String(b[f]||'').trim());
|
||
if(hasContent && !confirm('Replace the current field contents with an AI draft grounded in construction execution methodology?')) return;
|
||
btn.disabled = true; btn.textContent = 'Thinking…';
|
||
try{
|
||
const rel = (b.related && b.related.length) ? `\nRelated issues in this cluster:\n${b.related.map(r=>'- '+r).join('\n')}` : '';
|
||
const prompt = `You populate breadcrumb trails for construction field problems at an industrial I&C contractor, using best-in-class execution methodology.\n\n${METHOD_KB}\n\nProblem statement:\n${b.prob}${rel}\n\nClassify the problem into one domain above, then draft the five fields per that domain's standard metrics, methods, and baselines, obeying every GATE. Respond ONLY with a JSON object, no prose, no markdown fences:\n{"domain":"...","driver":"...","cap":"...","metric":"...","method":"...","baseline":"..."}`;
|
||
const out = await callClaude(prompt);
|
||
const obj = parseJson(out);
|
||
if(!REQ.every(f=>obj[f])) throw new Error('incomplete draft');
|
||
if(ADOPTION_WORDS.test(obj.metric)) throw new Error('draft failed adoption gate; try again');
|
||
REQ.forEach(f=>{ b[f] = String(obj[f]); });
|
||
save(); renderBc();
|
||
toast('Drafted from ' + (obj.domain ? String(obj.domain) : 'methodology') + '. Review as a team.');
|
||
}catch(err){
|
||
console.error('AI breadcrumb error:', err);
|
||
toast('AI draft unavailable: ' + (err.message||'error'));
|
||
btn.disabled = false; btn.textContent = '✨ AI draft fields';
|
||
}
|
||
}
|
||
|
||
/* ===== 3 ACTIONS ===== */
|
||
function renderAct(){
|
||
const wrap = document.getElementById('actList');
|
||
document.getElementById('cntAct').textContent = S.actions.filter(a=>!a.done).length;
|
||
if(!S.actions.length){ wrap.innerHTML = '<div class="empty">No actions. Use Scan or add one manually.</div>'; return; }
|
||
wrap.innerHTML = S.actions.map(a=>`
|
||
<div class="act-row${a.done?' done':''}">
|
||
<button class="act-check${a.done?' on':''}" data-done="${a.id}">${a.done?'✓':''}</button>
|
||
<div class="act-text">${esc(a.t)}</div>
|
||
<select data-aowner="${a.id}"><option value="">Owner…</option>${opts(PEOPLE, a.owner)}</select>
|
||
<input type="date" data-adue="${a.id}" value="${esc(a.due)}">
|
||
<button class="row-del" data-adel="${a.id}" title="Remove">✕</button>
|
||
</div>`).join('');
|
||
}
|
||
document.getElementById('actList').addEventListener('click', e=>{
|
||
if(e.target.dataset.done){ const a=S.actions.find(x=>x.id===e.target.dataset.done); if(a){a.done=!a.done; save(); renderAct();} }
|
||
if(e.target.dataset.adel){ S.actions=S.actions.filter(a=>a.id!==e.target.dataset.adel); save(); renderAct(); }
|
||
});
|
||
document.getElementById('actList').addEventListener('change', e=>{
|
||
const t = e.target;
|
||
if(t.dataset.aowner){ const a=S.actions.find(x=>x.id===t.dataset.aowner); if(a){a.owner=t.value; save();} }
|
||
if(t.dataset.adue){ const a=S.actions.find(x=>x.id===t.dataset.adue); if(a){a.due=t.value; save();} }
|
||
});
|
||
document.getElementById('btnAddAct').addEventListener('click', ()=>{
|
||
const inp = document.getElementById('newAct');
|
||
const v = inp.value.trim();
|
||
if(!v) return;
|
||
S.actions.push({id:uid(), t:v, owner:'', due:'', done:false});
|
||
inp.value=''; save(); renderAct();
|
||
});
|
||
document.getElementById('newAct').addEventListener('keydown', e=>{ if(e.key==='Enter') document.getElementById('btnAddAct').click(); });
|
||
document.getElementById('btnScan').addEventListener('click', ()=>{
|
||
let added = 0;
|
||
function addOnce(t){ if(!S.actions.some(a=>a.t===t)){ S.actions.push({id:uid(), t, owner:'', due:'', done:false}); added++; } }
|
||
S.breadcrumbs.forEach(b=>{ if(!bcComplete(b)) addOnce('Complete breadcrumb: ' + (shortT(b.prob,90) || '(blank breadcrumb)')); });
|
||
S.clusters.forEach(c=>{ if(!String(c.root||'').trim()) addOnce('Write root problem statement for cluster: ' + c.name); });
|
||
const uncl = S.problems.filter(p=>!p.cluster || !clusterById(p.cluster)).length;
|
||
S.actions = S.actions.filter(a=>!/^Cluster assignment: \d+ issues unclustered$/.test(a.t) || a.done);
|
||
if(uncl) addOnce(`Cluster assignment: ${uncl} issues unclustered`);
|
||
save(); renderAct();
|
||
toast(added ? added + ' action(s) added' : 'Nothing incomplete found');
|
||
});
|
||
|
||
/* ===== SAVE / LOAD / EXPORT / RESET ===== */
|
||
document.getElementById('btnSave').addEventListener('click', ()=>{
|
||
const blob = new Blob([JSON.stringify(S, null, 2)], {type:'application/json'});
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = `SDE_Field_Problem_Workshop_state_${new Date().toISOString().slice(0,10)}.json`;
|
||
a.click(); URL.revokeObjectURL(a.href);
|
||
toast('State saved to JSON');
|
||
});
|
||
document.getElementById('btnLoad').addEventListener('click', ()=>document.getElementById('jsonFile').click());
|
||
document.getElementById('jsonFile').addEventListener('change', e=>{
|
||
const f = e.target.files[0];
|
||
if(!f) return;
|
||
const r = new FileReader();
|
||
r.onload = ()=>{
|
||
try{
|
||
const data = JSON.parse(r.result);
|
||
if(!Array.isArray(data.problems) || !Array.isArray(data.clusters)) throw new Error('bad shape');
|
||
S = Object.assign(JSON.parse(JSON.stringify(DEFAULT_STATE)), data);
|
||
save(); renderAll(); toast('State loaded');
|
||
}catch(err){ toast('Could not load: not a valid workshop JSON'); }
|
||
e.target.value = '';
|
||
};
|
||
r.readAsText(f);
|
||
});
|
||
document.getElementById('btnExport').addEventListener('click', ()=>{
|
||
const d = new Date();
|
||
const stamp = d.toISOString().slice(0,10);
|
||
const clean = v => String(v||'').replace(/\|/g,'/').replace(/\n/g,' ');
|
||
let md = `# Project SDE Field Problem Workshop Summary (v5)\n\nDate: ${d.toLocaleDateString()}\n\n`;
|
||
|
||
md += `## 1. Root Problem Map (ranked by dots)\n\n`;
|
||
[...S.clusters].sort((a,b)=>b.dots-a.dots).forEach((c,i)=>{
|
||
const mem = membersOf(c.id);
|
||
md += `### ${i+1}. ${c.name} (${c.dots} dots${c.ws?', workstream: '+c.ws:', workstream: TBD'})\n`;
|
||
md += `Root problem: ${clean(c.root)||'TBD'}\n\nRelated issues:\n`;
|
||
md += mem.length ? mem.map(p=>`- ${p.sugg?'[suggested] ':''}${p.type!=='Problem'?'['+p.type+'] ':''}${clean(p.t)} (${p.sugg?'theme analysis':p.who})`).join('\n') + '\n\n' : '- None attached\n\n';
|
||
});
|
||
const uncl = S.problems.filter(p=>!p.cluster || !clusterById(p.cluster));
|
||
if(uncl.length) md += `### Unclustered (${uncl.length})\n` + uncl.map(p=>`- ${clean(p.t)} (${p.who})`).join('\n') + '\n\n';
|
||
|
||
md += `## 2. Breadcrumb Trails\n\n`;
|
||
S.breadcrumbs.forEach((b,i)=>{
|
||
md += `### ${i+1}. [${b.type}] ${clean(b.prob) || '(no problem statement)'}${b.example?' (worked example)':''} ${bcComplete(b)?'':'[INCOMPLETE]'}\n`;
|
||
md += `- Value driver: ${clean(b.driver)||'TBD'}\n- Solution capability: ${clean(b.cap)||'TBD'}\n`;
|
||
md += `- Metric: ${clean(b.metric)||'TBD'}\n- Method: ${clean(b.method)||'TBD'}\n- Baseline: ${clean(b.baseline)||'TBD'}\n- Target: ${clean(b.target)||'set after baseline'}\n`;
|
||
if(b.related && b.related.length) md += `- Evidence (${b.related.length} related issues): ` + b.related.map(clean).join('; ') + '\n';
|
||
md += '\n';
|
||
});
|
||
|
||
const done = S.breadcrumbs.filter(bcComplete);
|
||
md += `## 3. Pilot Success Criteria (generated from complete breadcrumbs)\n\n`;
|
||
if(done.length){
|
||
md += `| Type | Criterion | Metric | Method | Target | Baseline |\n|---|---|---|---|---|---|\n`;
|
||
done.forEach(b=>{ md += `| ${b.type} | ${clean(b.driver)} | ${clean(b.metric)} | ${clean(b.method)} | ${clean(b.target)} | ${clean(b.baseline)} |\n`; });
|
||
const nOut = done.filter(b=>b.type==='Outcome').length;
|
||
md += `\nMix: ${nOut} outcome, ${done.length-nOut} adoption.\n\n`;
|
||
} else md += 'No complete breadcrumbs yet.\n\n';
|
||
|
||
md += `## 4. Actions\n\n`;
|
||
if(S.actions.length){
|
||
md += `| Done | Action | Owner | Due |\n|---|---|---|---|\n`;
|
||
S.actions.forEach(a=>{ md += `| ${a.done?'✓':''} | ${clean(a.t)} | ${a.owner||'TBD'} | ${a.due||'TBD'} |\n`; });
|
||
const noOwner = S.actions.filter(a=>!a.done && !a.owner).length;
|
||
if(noOwner) md += `\n${noOwner} open action(s) have no owner.\n`;
|
||
} else md += 'None.\n';
|
||
|
||
const blob = new Blob([md], {type:'text/markdown'});
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = `SDE_Field_Problem_Workshop_${stamp}.md`;
|
||
a.click(); URL.revokeObjectURL(a.href);
|
||
toast('Summary downloaded');
|
||
});
|
||
document.getElementById('btnReset').addEventListener('click', ()=>{
|
||
if(confirm('Reset to the clustered July 21 baseline? Anything entered since will be lost unless saved to JSON.')){
|
||
S = JSON.parse(JSON.stringify(DEFAULT_STATE));
|
||
save(); renderAll(); toast('Reset to baseline');
|
||
}
|
||
});
|
||
|
||
let toastTimer;
|
||
function toast(msg){
|
||
const t = document.getElementById('toast');
|
||
t.textContent = msg;
|
||
t.classList.add('show');
|
||
clearTimeout(toastTimer);
|
||
toastTimer = setTimeout(()=>t.classList.remove('show'), 2200);
|
||
}
|
||
|
||
function renderAll(){ renderMap(); renderBc(); renderAct(); }
|
||
renderAll();
|
||
</script>
|
||
</body>
|
||
</html>
|