Compare commits
2 Commits
savepoint-
...
a18ae487f6
| Author | SHA1 | Date | |
|---|---|---|---|
| a18ae487f6 | |||
| 68c1c803d6 |
@@ -8,6 +8,21 @@ let allComments = [];
|
||||
// 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; } }
|
||||
|
||||
// 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 — 1–2 days (≈8–24 hrs)': 24,
|
||||
'Standard — 3–5 days (≈40–80 hrs)': 80,
|
||||
'Large — 1–2 weeks (≈80–160 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 = {
|
||||
project: {name:'', number:'', client:'', division:'', site:''},
|
||||
team: {pm:'', apm:'', cm:'', qm:''},
|
||||
@@ -181,10 +196,10 @@ function loadSampleData(){
|
||||
|
||||
// Populate Step 5
|
||||
document.getElementById('gov_woformat').value = 'WP##-[Sector]-[TYPE]';
|
||||
document.getElementById('gov_wosize').value = '3–5 days / 40–80 hours';
|
||||
document.getElementById('gov_wosize').value = 'Standard — 3–5 days (≈40–80 hrs)';
|
||||
document.getElementById('gov_disciplines').value = 'Mechanical, Electrical, Tech';
|
||||
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
|
||||
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[1]) set('role_foreman_name', state.signoffRoles[1].name);
|
||||
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_disciplines', (state.governance.disciplines||[]).join(', '));
|
||||
set('gov_discmode', state.governance.discMode);
|
||||
@@ -360,14 +381,24 @@ function renderWPTypes(){
|
||||
state.wpTypes.forEach((t,i)=>{
|
||||
const row = document.createElement('div');
|
||||
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,'"')}" 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 = `
|
||||
<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>
|
||||
<input type="text" placeholder="Special rules…" value="${(t.notes||'').replace(/"/g,'"')}" 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,'"')}" onchange="state.wpTypes[${i}].approval=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
|
||||
`;
|
||||
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){
|
||||
@@ -375,6 +406,21 @@ function toggleWPType(i){
|
||||
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(){
|
||||
const container = document.getElementById('team-members-list');
|
||||
if(!container) return;
|
||||
@@ -696,8 +742,8 @@ function completeSOP(){
|
||||
instanceSuffix: state.governance.instanceSuffix || 'letter',
|
||||
sizeHoursMax: state.governance.sizeHoursMax || ''
|
||||
},
|
||||
woTypes: state.wpTypes.filter(t=>t.enabled).map(t=>({
|
||||
name: t.name,
|
||||
woTypes: state.wpTypes.filter(t=>t.enabled && (t.name||'').trim()).map(t=>({
|
||||
name: t.name.trim(),
|
||||
enabled: true,
|
||||
notes: t.notes || '',
|
||||
approval: t.approval || ''
|
||||
@@ -720,6 +766,8 @@ function completeSOP(){
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
// 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});
|
||||
|
||||
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.
|
||||
// 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.
|
||||
if(typeof onSOPReady === 'function') onSOPReady(sop);
|
||||
|
||||
alert('✓ SOP Configuration Complete!\n\nReturning to the project home page.');
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
// ── COMMENTS ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -202,13 +202,20 @@
|
||||
<div class="notice">A Work Package should be a manageable, trackable chunk of work — typically a 1–2 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">
|
||||
<label>Typical WP Size (guidance)</label>
|
||||
<input type="text" id="gov_wosize" placeholder="e.g., 3–5 days or 40–80 hours">
|
||||
<label>Typical WP Size</label>
|
||||
<select id="gov_wosize" onchange="onSizePresetChange()">
|
||||
<option value="">Select…</option>
|
||||
<option value="Small — 1–2 days (≈8–24 hrs)">Small — 1–2 days (≈8–24 hrs)</option>
|
||||
<option value="Standard — 3–5 days (≈40–80 hrs)">Standard — 3–5 days (≈40–80 hrs)</option>
|
||||
<option value="Large — 1–2 weeks (≈80–160 hrs)">Large — 1–2 weeks (≈80–160 hrs)</option>
|
||||
<option value="Custom…">Custom…</option>
|
||||
</select>
|
||||
<small>Sets the split threshold automatically; choose Custom to enter your own.</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Split threshold — max labor hours</label>
|
||||
<input type="number" id="gov_size_hours_max" min="0" step="1" placeholder="e.g., 120">
|
||||
<small>The Creator flags packages above this so they can be split (by discipline or scope).</small>
|
||||
<input type="number" id="gov_size_hours_max" min="0" step="1" placeholder="e.g., 80">
|
||||
<small>Auto-set from the size above (editable). The Creator flags packages over this so they can be split.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ const SAMPLE_SOP = {
|
||||
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'},
|
||||
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:'3–5 days', woFormat:'WP##-[Sector]-[TYPE]', disciplines:['Mechanical','Electrical','Tech'], discMode:'choice', instanceSuffix:'letter', sizeHoursMax:'120' },
|
||||
governance:{ issuance:['By Sector / Area','By Discipline'], woSize:'Standard — 3–5 days (≈40–80 hrs)', woFormat:'WP##-[Sector]-[TYPE]', disciplines:['Mechanical','Electrical','Tech'], discMode:'choice', instanceSuffix:'letter', sizeHoursMax:'80' },
|
||||
woTypes:[
|
||||
{name:'Conduit Install', enabled:true}, {name:'Tray Install', enabled:true},
|
||||
{name:'Wire Pull', enabled:true}, {name:'Terminations', enabled:true},
|
||||
@@ -136,7 +136,12 @@ function editQuality(id){
|
||||
}
|
||||
function renderCtxBar(){
|
||||
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 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>
|
||||
@@ -372,12 +377,15 @@ function rollupDisciplineStatus(){
|
||||
// ── WP SIZING WARNING (governance.sizeHoursMax) ──────────────────────────────
|
||||
function onHoursChange(){
|
||||
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'));
|
||||
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>`;
|
||||
} else if(max){ el.textContent=`Split threshold: ${max} hrs (from SOP).`; }
|
||||
else { el.textContent=''; }
|
||||
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(band || max){
|
||||
el.innerHTML=`<span>${esc(band)}${max?'Split threshold: '+max+' hrs.':''}</span>`;
|
||||
} else { el.textContent=''; }
|
||||
}
|
||||
|
||||
// ── SPLIT BY DISCIPLINE ──────────────────────────────────────────────────────
|
||||
@@ -1036,7 +1044,11 @@ loadStore();
|
||||
if(d && d.woTypes){ SOP = d; applySOP(); newPackage(); return; }
|
||||
}
|
||||
} 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');
|
||||
renderSavedList();
|
||||
|
||||
Reference in New Issue
Block a user