Files
Project-SDE-WP-Suite/sop-config-app.js
2026-06-11 12:10:03 -07:00

588 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ── STATE ────────────────────────────────────────────────────────────────────
let currentStep = 1;
let allComments = []; // Track all comments for export
let state = {
project: {name:'', number:'', client:'', division:'', site:''},
team: {pm:'', cm:'', qm:''},
signoffRoles: [{role:'Superintendent',name:''},{role:'Foreman',name:''}],
wpTypes: [],
governance: {woformat:'', wosize:'', issuance:[]},
quality: {qcreq:'', photo:'', hold:''},
platforms: {tracking:'CxAlloy', commissioning:'CxAlloy'},
sequence: [],
constraints: [],
sources: []
};
// ── LIBRARIES ────────────────────────────────────────────────────────────────
const DISCIPLINES = [
'Conduit Installation',
'Cable Tray Installation',
'Instrument Raceway Installation',
'Wire / Cable Pull',
'Electrical/Instrument Cable Installation',
'Cable Terminations',
'Electrical Junction Box Installation',
'Instrument Installation',
'Control Panel Installation',
'Heat Tracing Installation'
];
const DEFAULT_WP_TYPES = [
{name:'Rough-In', enabled:true},
{name:'Mechanical Install', enabled:true},
{name:'Panel Install', enabled:true},
{name:'Conduit Install', enabled:true},
{name:'Tray Install', enabled:true},
{name:'Mechanical Tubing', enabled:false},
{name:'Instrument Install', enabled:true},
{name:'Wire Pull', enabled:true},
{name:'Terminations', enabled:true},
{name:'Prefab/Kitting', enabled:false},
{name:'Network Cabling', enabled:false},
{name:'Fiber', enabled:false},
{name:'Calibrations', enabled:false}
];
const TYPE_DISCIPLINE_MAP = {
'Rough-In':'Conduit Installation',
'Mechanical Install':'Heat Tracing Installation',
'Panel Install':'Control Panel Installation',
'Conduit Install':'Conduit Installation',
'Tray Install':'Cable Tray Installation',
'Mechanical Tubing':'Heat Tracing Installation',
'Instrument Install':'Instrument Installation',
'Wire Pull':'Wire / Cable Pull',
'Terminations':'Cable Terminations',
'Prefab/Kitting':'Electrical Junction Box Installation',
'Network Cabling':'Cable Tray Installation',
'Fiber':'Wire / Cable Pull',
'Calibrations':'Instrument Installation'
};
const STANDARD_10_CONSTRAINTS = [
{name:'Safety & Permitting', description:'Permits, safety reviews, environmental clearances'},
{name:'Quality Control / Inspection', description:'QC approval, inspection readiness'},
{name:'IFC Drawings & Specs', description:'Issued-for-Construction drawings and specifications'},
{name:'Schedule', description:'Scheduled sequence timing confirmed'},
{name:'Materials (on site, bagged & tagged)', description:'All required materials on site and staged'},
{name:'Prefabrication', description:'Prefabricated assemblies complete'},
{name:'Work Access & Laydown', description:'Work area accessible and staged'},
{name:'Craft Availability', description:'Required craft trades available'},
{name:'Construction Equipment & Tools', description:'Special equipment and tools on site'},
{name:'Scaffolding / Access Equipment', description:'Temporary access structures in place'}
];
const CONSTRAINT_LIBRARY = [
'Utility Clearances',
'Third-Party Approvals',
'Engineering Change Notices',
'Commissioning Sign-Off',
'As-Built Documentation',
'Labeling & Identification',
'Testing & Certification',
'Rework Completion',
'Coordination with Other Trades',
'Environmental Controls',
'Critical Path Gate',
'Client Walkthrough Approval'
];
const OPTIONAL_ROLES = [
'General Foreman',
'Construction Manager',
'HSE Professional',
'Quality Representative',
'Planner',
'Safety Manager',
'Project Controls Manager'
];
const LABOR_COST_CODES = [
'1000|Project Management',
'2000|Design and Development',
'2100|Design',
'2110|Control System Design',
'2120|Instrument Design',
'2130|Electrical Design',
'2140|Panel Design',
'2141|Panel Design Rework',
'2150|BIM',
'2151|BIM Rework',
'2160|Documentation',
'2200|Development',
'2210|PLC Programming',
'2220|OIT Programming',
'2230|SCADA Programming',
'2240|Simulation Development',
'2300|Customer Training',
'3000|Operational Technology',
'3100|OT Design',
'3200|Rack Assembly',
'3300|Network Configuration',
'3400|Computer Configuration',
'4000|Construction',
'4010|Instruments Install',
'4020|Network & Computers Install',
'4040|PLC Install',
'4050|Panel Install',
'4060|Electrical Install',
'4070|Mechanical Install',
'4080|Security Install',
'4090|Radio Install',
'4100|Commissioning',
'6000|Production',
'7000|Quality',
'7100|Panel Quality Control',
'7200|Factory Acceptance Testing',
'7300|Site Acceptance Testing',
'8000|Safety',
'9000|Administration'
];
// ── INITIALIZATION ────────────────────────────────────────────────────────────
window.addEventListener('DOMContentLoaded',()=>{
initializeWPTypes();
renderOptionalRoles();
renderStandardConstraints();
renderSequenceSteps();
renderSources();
updateStepUI();
});
function initializeWPTypes(){
state.wpTypes = JSON.parse(JSON.stringify(DEFAULT_WP_TYPES)).map(t=>({...t,discipline:TYPE_DISCIPLINE_MAP[t.name]||''}));
renderWPTypes();
}
// ── LOAD SAMPLE DATA ──────────────────────────────────────────────────────────
function loadSampleData(){
// Populate Step 1
document.getElementById('proj_name').value = 'Micron — INC Construction Work Packages';
document.getElementById('proj_number').value = '26-67-008';
document.getElementById('proj_client').value = 'Micron Technology, Inc.';
document.getElementById('proj_division').value = 'Semiconductor';
document.getElementById('proj_site').value = 'Boise, ID — Fab 7';
// Populate Step 2
document.getElementById('proj_pm').value = 'Nick Siegfried';
document.getElementById('proj_cm').value = 'K. Boyd';
document.getElementById('proj_qm').value = 'D. Nguyen';
// Step 3 already has defaults
document.getElementById('role_super_name').value = 'John Smith';
document.getElementById('role_foreman_name').value = 'Mike Jones';
// Populate Step 5
document.getElementById('gov_woformat').value = 'WP##-[Sector]-[Discipline]-[TYPE]';
document.getElementById('gov_wosize').value = '35 days / 4080 hours';
// Populate Step 6
document.getElementById('qual_qcreq').value = 'Yes — Detailed inspection items';
document.getElementById('qual_photo').value = 'Key checkpoints only';
document.getElementById('qual_hold').value = 'HOLD: Prime QAQC to inspect rough-in before cover/cover-up.\nWITNESS: Client QC to observe megger test before energization.';
// Step 7 already has defaults
// Collect all data
collectStepData();
alert('✓ Sample data loaded!\n\nNavigate through the SOP steps to see example values. You can edit or replace any field.');
// Switch to step 1
currentStep = 1;
updateStepUI();
}
// ── RENDERING ─────────────────────────────────────────────────────────────────
function renderWPTypes(){
const container = document.getElementById('wp-types-table');
container.innerHTML = `<div style="padding:0.5rem; font-size:12px; font-weight:600; display:grid; grid-template-columns:30px 200px 200px; gap:1rem; margin-bottom:1rem; border-bottom:1px solid var(--border);">
<div></div><div>Type</div><div>Discipline</div>
</div>`;
state.wpTypes.forEach((t,i)=>{
const row = document.createElement('div');
row.className = 'wp-type-row';
row.innerHTML = `
<input type="checkbox" ${t.enabled?'checked':''} onchange="toggleWPType(${i})">
<div>${t.name}</div>
<select onchange="state.wpTypes[${i}].discipline=this.value">
${DISCIPLINES.map(d=>`<option ${t.discipline===d?'selected':''}>${d}</option>`).join('')}
</select>
`;
container.appendChild(row);
});
}
function toggleWPType(i){
state.wpTypes[i].enabled = !state.wpTypes[i].enabled;
renderWPTypes();
}
function renderOptionalRoles(){
const container = document.getElementById('optional-roles-list');
const current = state.signoffRoles.filter(r=>r.role!=='Superintendent'&&r.role!=='Foreman');
container.innerHTML = current.map((r,i)=>`
<div style="display:grid; grid-template-columns:1fr 200px 30px; gap:1rem; align-items:center; padding:0.75rem; background:var(--bg); border-radius:6px; margin-bottom:0.5rem; border:1px solid var(--border);">
<select onchange="state.signoffRoles[${state.signoffRoles.indexOf(r)}].role=this.value">
${OPTIONAL_ROLES.map(o=>`<option ${r.role===o?'selected':''}>${o}</option>`).join('')}
</select>
<input type="text" placeholder="Name (optional)" value="${r.name||''}" onchange="state.signoffRoles[${state.signoffRoles.indexOf(r)}].name=this.value">
<button class="row-del" onclick="removeRole(${state.signoffRoles.indexOf(r)})">✕</button>
</div>
`).join('');
}
function addOptionalRole(){
state.signoffRoles.push({role:'General Foreman',name:''});
renderOptionalRoles();
}
function removeRole(i){
state.signoffRoles.splice(i,1);
renderOptionalRoles();
}
function renderStandardConstraints(){
const container = document.getElementById('standard-constraints');
container.innerHTML = STANDARD_10_CONSTRAINTS.map(c=>`
<div class="constraint-check">
<input type="checkbox" id="const_${c.name}" checked onchange="toggleConstraint('${c.name}')">
<div style="flex:1;">
<label for="const_${c.name}" style="margin:0; font-weight:600;">${c.name}</label>
<div style="font-size:12px; color:var(--text-dim); margin-top:0.25rem;">${c.description}</div>
</div>
</div>
`).join('');
// Initialize constraints with all standard 10
state.constraints = STANDARD_10_CONSTRAINTS.map(c=>({...c}));
}
function toggleConstraint(name){
const idx = state.constraints.findIndex(c=>c.name===name);
if(idx>=0) state.constraints.splice(idx,1);
else state.constraints.push(STANDARD_10_CONSTRAINTS.find(c=>c.name===name));
}
function showConstraintLibrary(){
const modal = document.getElementById('constraint-modal');
const lib = document.getElementById('constraint-library');
lib.innerHTML = CONSTRAINT_LIBRARY.map(c=>`
<div class="constraint-option" onclick="addCustomConstraint('${c}')">
<strong>${c}</strong>
<div style="font-size:12px; color:var(--text-light); margin-top:0.25rem;">Click to add to this project</div>
</div>
`).join('');
modal.style.display = 'flex';
}
function closeConstraintModal(){
document.getElementById('constraint-modal').style.display = 'none';
}
function addCustomConstraint(name){
if(!state.constraints.find(c=>c.name===name)){
state.constraints.push({name,description:''});
}
closeConstraintModal();
renderStandardConstraints();
}
function renderSequenceSteps(){
const container = document.getElementById('sequence-list');
if(!state.sequence.length) state.sequence = [{label:'Layout',kind:'step'},{label:'Installation',kind:'step'},{label:'Testing',kind:'step'},{label:'Commissioning',kind:'step'}];
container.innerHTML = state.sequence.map((s,i)=>`
<div class="sequence-row">
<input type="text" value="${s.label}" placeholder="Step name" onchange="state.sequence[${i}].label=this.value">
<select onchange="state.sequence[${i}].kind=this.value">
<option ${s.kind==='step'?'selected':''}>step</option>
<option ${s.kind==='gate'?'selected':''}>gate</option>
</select>
<button class="row-del" onclick="state.sequence.splice(${i},1); renderSequenceSteps()">✕</button>
</div>
`).join('');
}
function addSequenceStep(){
state.sequence.push({label:'',kind:'step'});
renderSequenceSteps();
}
function renderSources(){
const container = document.getElementById('sources-list');
if(!state.sources.length) state.sources = [
{label:'Design Drawings',system:'Procore',notes:'',link:''},
{label:'Specifications',system:'Procore',notes:'',link:''},
{label:'IO List',system:'controls.dev',notes:'',link:''},
{label:'Cable Schedule',system:'SharePoint',notes:'',link:''}
];
container.innerHTML = state.sources.map((s,i)=>`
<div class="source-row">
<input type="text" value="${s.label}" placeholder="Label" onchange="state.sources[${i}].label=this.value">
<input type="text" value="${s.system}" placeholder="System" onchange="state.sources[${i}].system=this.value">
<input type="text" value="${s.link}" placeholder="URL" onchange="state.sources[${i}].link=this.value">
<input type="text" value="${s.notes}" placeholder="Notes" onchange="state.sources[${i}].notes=this.value">
<button class="row-del" onclick="state.sources.splice(${i},1); renderSources()">✕</button>
</div>
`).join('');
}
function addSource(){
state.sources.push({label:'',system:'',notes:'',link:''});
renderSources();
}
// ── STEP NAVIGATION ───────────────────────────────────────────────────────────
function goToStep(n){
if(!validateStep(currentStep)) return;
currentStep = n;
updateStepUI();
}
function nextStep(){
if(!validateStep(currentStep)) return;
if(currentStep < 10){
currentStep++;
updateStepUI();
}else{
exportSOP();
}
}
function previousStep(){
if(currentStep > 1){
currentStep--;
updateStepUI();
}
}
function updateStepUI(){
// Hide all steps
document.querySelectorAll('.step').forEach(s=>s.style.display='none');
document.getElementById(`step-${currentStep}`).style.display='block';
// Update step indicators
document.querySelectorAll('.step-item').forEach(s=>s.classList.remove('active'));
document.querySelector(`[data-step="${currentStep}"]`).classList.add('active');
// Update counter
document.getElementById('current-step').textContent = currentStep;
// Update buttons
document.getElementById('prev-btn').disabled = currentStep === 1;
document.getElementById('next-btn').style.display = currentStep < 10 ? 'block' : 'none';
document.getElementById('export-btn').style.display = currentStep === 10 ? 'block' : 'none';
// Collect form data on display
collectStepData();
}
function collectStepData(){
switch(currentStep){
case 1:
state.project.name = document.getElementById('proj_name').value;
state.project.number = document.getElementById('proj_number').value;
state.project.client = document.getElementById('proj_client').value;
state.project.division = document.getElementById('proj_division').value;
state.project.site = document.getElementById('proj_site').value;
break;
case 2:
state.team.pm = document.getElementById('proj_pm').value;
state.team.cm = document.getElementById('proj_cm').value;
state.team.qm = document.getElementById('proj_qm').value;
break;
case 3:
state.signoffRoles[0].name = document.getElementById('role_super_name').value;
state.signoffRoles[1].name = document.getElementById('role_foreman_name').value;
break;
case 5:
state.governance.woformat = document.getElementById('gov_woformat').value;
state.governance.wosize = document.getElementById('gov_wosize').value;
const sel = document.getElementById('gov_issuance');
state.governance.issuance = Array.from(sel.selectedOptions).map(o=>o.value);
break;
case 6:
state.quality.qcreq = document.getElementById('qual_qcreq').value;
state.quality.photo = document.getElementById('qual_photo').value;
state.quality.hold = document.getElementById('qual_hold').value;
break;
case 7:
state.platforms.tracking = document.getElementById('plat_tracking').value;
state.platforms.commissioning = document.getElementById('plat_commissioning').value;
break;
}
}
function validateStep(n){
collectStepData();
const proj = state.project;
const team = state.team;
if(n===1 && (!proj.name || !proj.number || !proj.client || !proj.division || !proj.site)){
alert('Please complete all required fields: Project Name, Number, Client, Division, and Site Location.');
return false;
}
if(n===5 && !state.governance.woformat){
alert('Please enter a Work Package Number Format.');
return false;
}
if(n===6 && !state.quality.qcreq){
alert('Please select a QC requirement.');
return false;
}
return true;
}
// ── EXPORT ────────────────────────────────────────────────────────────────────
function exportSOP(){
collectStepData();
const sop = {
meta: {tool:'Work Package Configuration', sample:false},
project: {
name: state.project.name,
number: state.project.number,
client: state.project.client,
division: state.project.division,
pm: state.team.pm,
cm: state.team.cm,
qm: state.team.qm,
site: state.project.site
},
roles: state.signoffRoles.filter(r=>r.role),
governance: {
issuance: state.governance.issuance.length ? state.governance.issuance : ['By Sector / Area'],
woSize: state.governance.wosize,
woFormat: state.governance.woformat
},
woTypes: state.wpTypes.filter(t=>t.enabled).map(t=>({
name: t.name,
enabled: true,
discipline: t.discipline || ''
})),
sources: state.sources.filter(s=>s.label),
field: {trackPlatform: state.platforms.tracking},
commissioning: {tool: state.platforms.commissioning},
quality: {
qcReq: state.quality.qcreq,
photo: state.quality.photo,
holdPoints: state.quality.hold,
cxTool: state.platforms.commissioning
},
sequence: state.sequence.filter(s=>s.label).map(s=>({
label: s.label,
kind: s.kind || 'step'
})),
costCodes: LABOR_COST_CODES,
constraints: state.constraints.map(c=>({name: c.name, description: c.description || ''}))
};
// Validate SOP
if(!sop.project.name || !sop.project.number || !sop.governance.woFormat){
alert('Cannot export: Missing critical fields (Project Name, Number, or WP Format).');
return;
}
// Download as JSON
const json = JSON.stringify(sop, null, 2);
const blob = new Blob([json], {type: 'application/json'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `wp-sop-${sop.project.name.replace(/\s+/g,'-').toLowerCase()}.json`;
document.body.appendChild(a);
a.click();
a.remove();
alert(`✓ SOP exported: ${a.download}`);
// Also offer to export comments
if(allComments.length > 0){
setTimeout(()=>{
if(confirm('Export feedback comments as well? (CSV file)')){
exportComments();
}
}, 500);
}
}
// ── COMMENTS ──────────────────────────────────────────────────────────────────
function toggleComments(){
const panel = document.getElementById('comments-panel');
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
if(panel.style.display === 'block'){
loadStepComments();
}
}
function submitComment(){
const name = document.getElementById('commenter-name').value || 'Anonymous';
const text = document.getElementById('comment-text').value.trim();
if(!text){
alert('Please enter a comment.');
return;
}
const comment = {
step: currentStep,
name: name,
text: text,
timestamp: new Date().toLocaleString()
};
allComments.push(comment);
localStorage.setItem('sop_config_comments', JSON.stringify(allComments));
document.getElementById('comment-text').value = '';
document.getElementById('commenter-name').value = '';
loadStepComments();
alert('✓ Comment submitted! Thank you for your feedback.');
}
function loadStepComments(){
const saved = localStorage.getItem('sop_config_comments');
if(saved) allComments = JSON.parse(saved);
const stepComments = allComments.filter(c=>c.step===currentStep);
const list = document.getElementById('comments-list');
if(stepComments.length === 0){
list.innerHTML = '<div style="font-size:12px; color:var(--text-dim); font-style:italic;">No comments yet on this step.</div>';
}else{
list.innerHTML = stepComments.map(c=>`
<div style="padding:0.5rem; background:white; border:1px solid var(--border); border-radius:4px; margin-bottom:0.5rem;">
<div style="font-size:11px; color:var(--text-dim); margin-bottom:0.25rem;"><strong>${c.name}</strong> • ${c.timestamp}</div>
<div style="font-size:12px; color:var(--text);">${c.text.replace(/</g,'&lt;').replace(/>/g,'&gt;')}</div>
</div>
`).join('');
}
}
function exportComments(){
const saved = localStorage.getItem('sop_config_comments');
if(!saved || JSON.parse(saved).length === 0){
alert('No comments to export.');
return;
}
allComments = JSON.parse(saved);
// Build CSV
let csv = 'Step,Name,Timestamp,Feedback\n';
allComments.forEach(c=>{
const text = `"${c.text.replace(/"/g,'""')}"`;
csv += `${c.step},"${c.name}","${c.timestamp}",${text}\n`;
});
const blob = new Blob([csv], {type: 'text/csv'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `sop-config-feedback-${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
a.remove();
alert(`${allComments.length} comments exported to CSV.`);
}