Compare commits

..

2 Commits

Author SHA1 Message Date
a18ae487f6 Fix SOP→home flow, stop sample-data fallback for real projects, add custom WP types
- SOP Complete now returns to the project home page after the confirmation
  popup (instead of staying on the SOP tab), and stamps projectId onto the SOP.
- WP creator no longer substitutes the Micron SAMPLE_SOP when a project is
  active but its SOP isn't found — it shows a "complete the SOP first" empty
  state instead. Sample is only used for a standalone (no-project) preview.
  This was the source of "loaded with sample data I didn't select."
- SOP WP Types step gains "+ Add Custom Type" (editable name + remove);
  blank-named custom types are dropped from the generated SOP. Custom types
  round-trip via the saved state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:29:14 -07:00
68c1c803d6 WP size: preset dropdown that sets the split threshold + show band in creator
Replaces the free-text "Typical WP Size" with a preset dropdown (Small /
Standard / Large / Custom). Choosing a preset auto-fills the max-hours split
threshold (still editable). The chosen band is now surfaced next to Est. Hrs
in the creator's size hint (not just the View-SOP modal), alongside the
threshold/over-threshold warning. Sample SOP aligned to Standard (80 hrs).
repopulateForm preserves a non-preset saved value as a dropdown option.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:17:18 -07:00
3 changed files with 88 additions and 19 deletions

View File

@@ -8,6 +8,21 @@ let allComments = [];
// project is active. Keeps each project's SOP separate in the browser. // project is active. Keeps each project's SOP separate in the browser.
function SK(base){ try { return (typeof ProjectData !== 'undefined' && ProjectData.key) ? ProjectData.key(base) : base; } catch(e){ return base; } } function SK(base){ try { return (typeof ProjectData !== 'undefined' && ProjectData.key) ? ProjectData.key(base) : base; } catch(e){ return base; } }
// WP size presets — the dropdown label maps to a default split-threshold (max
// labor hours). The label is exported as governance.woSize (human-readable
// guidance); the number drives the Creator's "consider splitting" warning.
const WP_SIZE_PRESETS = {
'Small — 12 days (≈824 hrs)': 24,
'Standard — 35 days (≈4080 hrs)': 80,
'Large — 12 weeks (≈80160 hrs)': 160
};
function onSizePresetChange(){
const label = document.getElementById('gov_wosize').value;
const max = WP_SIZE_PRESETS[label];
if(max != null){ document.getElementById('gov_size_hours_max').value = max; }
// 'Custom…' / '' leave the threshold for manual entry.
}
let state = { let state = {
project: {name:'', number:'', client:'', division:'', site:''}, project: {name:'', number:'', client:'', division:'', site:''},
team: {pm:'', apm:'', cm:'', qm:''}, team: {pm:'', apm:'', cm:'', qm:''},
@@ -181,10 +196,10 @@ function loadSampleData(){
// Populate Step 5 // Populate Step 5
document.getElementById('gov_woformat').value = 'WP##-[Sector]-[TYPE]'; document.getElementById('gov_woformat').value = 'WP##-[Sector]-[TYPE]';
document.getElementById('gov_wosize').value = '35 days / 4080 hours'; document.getElementById('gov_wosize').value = 'Standard — 35 days (≈4080 hrs)';
document.getElementById('gov_disciplines').value = 'Mechanical, Electrical, Tech'; document.getElementById('gov_disciplines').value = 'Mechanical, Electrical, Tech';
document.getElementById('gov_discmode').value = 'choice'; document.getElementById('gov_discmode').value = 'choice';
document.getElementById('gov_size_hours_max').value = '120'; document.getElementById('gov_size_hours_max').value = '80';
// Populate Step 6 // Populate Step 6
document.getElementById('qual_qcreq').value = 'Yes — Detailed inspection items'; document.getElementById('qual_qcreq').value = 'Yes — Detailed inspection items';
@@ -246,6 +261,12 @@ function repopulateForm(){
if(state.signoffRoles[0]) set('role_super_name', state.signoffRoles[0].name); if(state.signoffRoles[0]) set('role_super_name', state.signoffRoles[0].name);
if(state.signoffRoles[1]) set('role_foreman_name', state.signoffRoles[1].name); if(state.signoffRoles[1]) set('role_foreman_name', state.signoffRoles[1].name);
set('gov_woformat', state.governance.woformat); set('gov_woformat', state.governance.woformat);
// gov_wosize is now a <select>; if a saved value isn't one of the presets
// (e.g. legacy free text), add it as an option so the round-trip preserves it.
const wsEl = document.getElementById('gov_wosize');
if(wsEl && state.governance.wosize && !Array.from(wsEl.options).some(o=>o.value===state.governance.wosize)){
wsEl.add(new Option(state.governance.wosize, state.governance.wosize));
}
set('gov_wosize', state.governance.wosize); set('gov_wosize', state.governance.wosize);
set('gov_disciplines', (state.governance.disciplines||[]).join(', ')); set('gov_disciplines', (state.governance.disciplines||[]).join(', '));
set('gov_discmode', state.governance.discMode); set('gov_discmode', state.governance.discMode);
@@ -360,14 +381,24 @@ function renderWPTypes(){
state.wpTypes.forEach((t,i)=>{ state.wpTypes.forEach((t,i)=>{
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'wp-type-row'; row.className = 'wp-type-row';
const nameCell = t.custom
? `<div style="display:flex; gap:6px; align-items:center;">
<input type="text" placeholder="Custom type name" value="${(t.name||'').replace(/"/g,'&quot;')}" onchange="state.wpTypes[${i}].name=this.value" style="flex:1; padding:0.5rem; border:1px solid var(--border); border-radius:4px; font-weight:600;">
<button onclick="removeWPType(${i})" title="Remove custom type" style="background:var(--danger); color:#fff; border:none; border-radius:4px; width:28px; height:28px; cursor:pointer; font-weight:600; flex:none;">✕</button>
</div>`
: `<div style="font-weight:600;">${t.name}</div>`;
row.innerHTML = ` row.innerHTML = `
<div style="font-weight:600;">${t.name}</div> ${nameCell}
<div style="text-align:center;"><input type="checkbox" ${t.enabled?'checked':''} onchange="toggleWPType(${i})" style="width:18px; height:18px; cursor:pointer;"></div> <div style="text-align:center;"><input type="checkbox" ${t.enabled?'checked':''} onchange="toggleWPType(${i})" style="width:18px; height:18px; cursor:pointer;"></div>
<input type="text" placeholder="Special rules…" value="${(t.notes||'').replace(/"/g,'&quot;')}" onchange="state.wpTypes[${i}].notes=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;"> <input type="text" placeholder="Special rules…" value="${(t.notes||'').replace(/"/g,'&quot;')}" onchange="state.wpTypes[${i}].notes=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
<input type="text" placeholder="PM / CM / QC…" value="${(t.approval||'').replace(/"/g,'&quot;')}" onchange="state.wpTypes[${i}].approval=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;"> <input type="text" placeholder="PM / CM / QC…" value="${(t.approval||'').replace(/"/g,'&quot;')}" onchange="state.wpTypes[${i}].approval=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
`; `;
container.appendChild(row); container.appendChild(row);
}); });
const addRow = document.createElement('div');
addRow.style.cssText = 'margin-top:0.85rem;';
addRow.innerHTML = `<button onclick="addCustomWPType()" style="background:var(--primary,#0f62fe); color:#fff; border:none; padding:0.55rem 1rem; border-radius:4px; font-weight:600; cursor:pointer; font-size:13px;">+ Add Custom Type</button>`;
container.appendChild(addRow);
} }
function toggleWPType(i){ function toggleWPType(i){
@@ -375,6 +406,21 @@ function toggleWPType(i){
renderWPTypes(); renderWPTypes();
} }
function addCustomWPType(){
state.wpTypes.push({name:'', enabled:true, notes:'', approval:'', custom:true});
renderWPTypes();
// Focus the new custom row's name input.
const rows = document.querySelectorAll('#wp-types-table .wp-type-row');
const last = rows[rows.length-1];
const nameInput = last && last.querySelector('input[type="text"]');
if(nameInput) nameInput.focus();
}
function removeWPType(i){
state.wpTypes.splice(i,1);
renderWPTypes();
}
function renderTeamMembers(){ function renderTeamMembers(){
const container = document.getElementById('team-members-list'); const container = document.getElementById('team-members-list');
if(!container) return; if(!container) return;
@@ -696,8 +742,8 @@ function completeSOP(){
instanceSuffix: state.governance.instanceSuffix || 'letter', instanceSuffix: state.governance.instanceSuffix || 'letter',
sizeHoursMax: state.governance.sizeHoursMax || '' sizeHoursMax: state.governance.sizeHoursMax || ''
}, },
woTypes: state.wpTypes.filter(t=>t.enabled).map(t=>({ woTypes: state.wpTypes.filter(t=>t.enabled && (t.name||'').trim()).map(t=>({
name: t.name, name: t.name.trim(),
enabled: true, enabled: true,
notes: t.notes || '', notes: t.notes || '',
approval: t.approval || '' approval: t.approval || ''
@@ -720,6 +766,8 @@ function completeSOP(){
}; };
sopComplete = true; sopComplete = true;
// Stamp the active project onto the SOP so it's unambiguously tied to it.
try { if(typeof ProjectData!=='undefined' && ProjectData.getActiveId()) sop.projectId = ProjectData.getActiveId(); } catch(e){}
updateProjectDisplay(); updateProjectDisplay();
// Persist for the home page (green / "Review") and for the WP Creator tab, // Persist for the home page (green / "Review") and for the WP Creator tab,
@@ -732,10 +780,12 @@ function completeSOP(){
track('sop_generated', {woTypes: sop.woTypes.length, constraints: sop.constraints.length}); track('sop_generated', {woTypes: sop.woTypes.length, constraints: sop.constraints.length});
alert('✓ SOP Configuration Complete!\n\nSwitch to "Work Package Creation" to start creating Work Packages.'); // Hand the SOP to the embedded Work Package Creator and unlock its tab (in case
// the user stays), then return to the project home page per the requested flow.
// Hand the SOP to the embedded Work Package Creator and unlock its tab.
if(typeof onSOPReady === 'function') onSOPReady(sop); if(typeof onSOPReady === 'function') onSOPReady(sop);
alert('✓ SOP Configuration Complete!\n\nReturning to the project home page.');
window.location.href = 'index.html';
} }
// ── COMMENTS ────────────────────────────────────────────────────────────────── // ── COMMENTS ──────────────────────────────────────────────────────────────────

View File

@@ -202,13 +202,20 @@
<div class="notice">A Work Package should be a manageable, trackable chunk of work — typically a 12 week assignment. The Creator warns the planner when a package exceeds the ceiling so it can be broken down.</div> <div class="notice">A Work Package should be a manageable, trackable chunk of work — typically a 12 week assignment. The Creator warns the planner when a package exceeds the ceiling so it can be broken down.</div>
<div class="field-grid"> <div class="field-grid">
<div class="field"> <div class="field">
<label>Typical WP Size (guidance)</label> <label>Typical WP Size</label>
<input type="text" id="gov_wosize" placeholder="e.g., 35 days or 4080 hours"> <select id="gov_wosize" onchange="onSizePresetChange()">
<option value="">Select…</option>
<option value="Small — 12 days (≈824 hrs)">Small — 12 days (≈824 hrs)</option>
<option value="Standard — 35 days (≈4080 hrs)">Standard — 35 days (≈4080 hrs)</option>
<option value="Large — 12 weeks (≈80160 hrs)">Large — 12 weeks (≈80160 hrs)</option>
<option value="Custom…">Custom…</option>
</select>
<small>Sets the split threshold automatically; choose Custom to enter your own.</small>
</div> </div>
<div class="field"> <div class="field">
<label>Split threshold — max labor hours</label> <label>Split threshold — max labor hours</label>
<input type="number" id="gov_size_hours_max" min="0" step="1" placeholder="e.g., 120"> <input type="number" id="gov_size_hours_max" min="0" step="1" placeholder="e.g., 80">
<small>The Creator flags packages above this so they can be split (by discipline or scope).</small> <small>Auto-set from the size above (editable). The Creator flags packages over this so they can be split.</small>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -8,7 +8,7 @@ const SAMPLE_SOP = {
meta:{tool:'Work Package Configuration', sample:true}, meta:{tool:'Work Package Configuration', sample:true},
project:{name:'Micron — INC Construction Work Packages', number:'26-67-008', client:'Micron Technology, Inc.', division:'Semiconductor', pm:'Nick Siegfried', cm:'K. Boyd', qm:'D. Nguyen', site:'Boise, ID — Fab'}, project:{name:'Micron — INC Construction Work Packages', number:'26-67-008', client:'Micron Technology, Inc.', division:'Semiconductor', pm:'Nick Siegfried', cm:'K. Boyd', qm:'D. Nguyen', site:'Boise, ID — Fab'},
roles:[{role:'General Foreman',name:'M. Torres'},{role:'Superintendent',name:'K. Boyd'},{role:'Safety Manager / Lead',name:'A. Reyes'},{role:'Quality Manager',name:'D. Nguyen'},{role:'Planner',name:'L. Graver'}], roles:[{role:'General Foreman',name:'M. Torres'},{role:'Superintendent',name:'K. Boyd'},{role:'Safety Manager / Lead',name:'A. Reyes'},{role:'Quality Manager',name:'D. Nguyen'},{role:'Planner',name:'L. Graver'}],
governance:{ issuance:['By Sector / Area','By Discipline'], woSize:'35 days', woFormat:'WP##-[Sector]-[TYPE]', disciplines:['Mechanical','Electrical','Tech'], discMode:'choice', instanceSuffix:'letter', sizeHoursMax:'120' }, governance:{ issuance:['By Sector / Area','By Discipline'], woSize:'Standard — 35 days (≈4080 hrs)', woFormat:'WP##-[Sector]-[TYPE]', disciplines:['Mechanical','Electrical','Tech'], discMode:'choice', instanceSuffix:'letter', sizeHoursMax:'80' },
woTypes:[ woTypes:[
{name:'Conduit Install', enabled:true}, {name:'Tray Install', enabled:true}, {name:'Conduit Install', enabled:true}, {name:'Tray Install', enabled:true},
{name:'Wire Pull', enabled:true}, {name:'Terminations', enabled:true}, {name:'Wire Pull', enabled:true}, {name:'Terminations', enabled:true},
@@ -136,7 +136,12 @@ function editQuality(id){
} }
function renderCtxBar(){ function renderCtxBar(){
const bar=document.getElementById('ctx-bar'); const bar=document.getElementById('ctx-bar');
if(!SOP){ bar.innerHTML=`<div class="ctx-empty">No SOP loaded — <button class="link-btn" onclick="loadSampleSOP()">load the sample</button> or import one from the Configuration tool.</div>`; return; } if(!SOP){
bar.innerHTML = activeProjectId
? `<div class="ctx-empty">No SOP found for this project yet — complete the <strong>SOP Configuration</strong> first, then return here.</div>`
: `<div class="ctx-empty">No SOP loaded — <button class="link-btn" onclick="loadSampleSOP()">load the sample</button> or import one from the Configuration tool.</div>`;
return;
}
const p=SOP.project||{}, g=SOP.governance||{}; const p=SOP.project||{}, g=SOP.governance||{};
const sample=SOP.meta&&SOP.meta.sample?`<span class="ctx-sample">SAMPLE</span>`:''; const sample=SOP.meta&&SOP.meta.sample?`<span class="ctx-sample">SAMPLE</span>`:'';
bar.innerHTML=`<div class="ctx-main"><div class="ctx-proj">${esc(p.name||'Untitled')} ${sample}</div> bar.innerHTML=`<div class="ctx-main"><div class="ctx-proj">${esc(p.name||'Untitled')} ${sample}</div>
@@ -372,12 +377,15 @@ function rollupDisciplineStatus(){
// ── WP SIZING WARNING (governance.sizeHoursMax) ────────────────────────────── // ── WP SIZING WARNING (governance.sizeHoursMax) ──────────────────────────────
function onHoursChange(){ function onHoursChange(){
const el=document.getElementById('size-check'); if(!el) return; const el=document.getElementById('size-check'); if(!el) return;
const max=parseFloat((SOP&&SOP.governance&&SOP.governance.sizeHoursMax)||''); const g=(SOP&&SOP.governance)||{};
const band=g.woSize?('Target: '+g.woSize+'. '):'';
const max=parseFloat(g.sizeHoursMax||'');
const hrs=parseFloat(gv('wp_hours')); const hrs=parseFloat(gv('wp_hours'));
if(max && hrs && hrs>max){ if(max && hrs && hrs>max){
el.innerHTML=`<span style="color:var(--accent-amber)">⚠ ${hrs} hrs exceeds the ${max}-hr split threshold — consider breaking this package down`+(isMultiDiscipline()?' (try <strong>Split by Discipline</strong>).':'.')+`</span>`; el.innerHTML=`<span style="color:var(--accent-amber)">${esc(band)}${hrs} hrs exceeds the ${max}-hr split threshold — consider breaking this package down`+(isMultiDiscipline()?' (try <strong>Split by Discipline</strong>).':'.')+`</span>`;
} else if(max){ el.textContent=`Split threshold: ${max} hrs (from SOP).`; } } else if(band || max){
else { el.textContent=''; } el.innerHTML=`<span>${esc(band)}${max?'Split threshold: '+max+' hrs.':''}</span>`;
} else { el.textContent=''; }
} }
// ── SPLIT BY DISCIPLINE ────────────────────────────────────────────────────── // ── SPLIT BY DISCIPLINE ──────────────────────────────────────────────────────
@@ -1036,7 +1044,11 @@ loadStore();
if(d && d.woTypes){ SOP = d; applySOP(); newPackage(); return; } if(d && d.woTypes){ SOP = d; applySOP(); newPackage(); return; }
} }
} catch(e){} } catch(e){}
loadSampleSOP(); // No SOP found. Only show the Micron SAMPLE for a standalone preview (no project
// context). For a real project, never substitute sample data — show the empty
// state so it's clear the project's SOP must be completed first.
if(activeProjectId){ SOP=null; renderCtxBar(); newPackage(); }
else { loadSampleSOP(); }
})(); })();
setRadio('status','Draft'); setRadio('status','Draft');
renderSavedList(); renderSavedList();